Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions scripts/actions/deploy_frontend.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
PROJECT_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd)
source "$SCRIPT_DIR/../utils/print.bash"

TF_DIR="$PROJECT_ROOT/terraform/main"
FRONTEND_DIR="$PROJECT_ROOT/frontend"

gen_separator '='
pretty_info "Starting Frontend Deployment to AWS"
gen_separator '='

# 1. Fetch required values from Terraform state
pretty_info "Fetching deployment configuration from Terraform..."
FRONTEND_URL=$(terraform -chdir="$TF_DIR" output -raw frontend_url)
S3_BUCKET=$(terraform -chdir="$TF_DIR" output -raw frontend_s3_bucket_name)
CLOUDFRONT_ID=$(terraform -chdir="$TF_DIR" output -raw frontend_cloudfront_distribution_id)

if [[ -z "$FRONTEND_URL" || -z "$S3_BUCKET" || -z "$CLOUDFRONT_ID" ]]; then
pretty_error "Failed to retrieve necessary deployment values from Terraform."
pretty_error "Ensure Terraform has been applied successfully in '$TF_DIR'."
exit 1
fi

API_BASE_URL="${FRONTEND_URL}/api"

pretty_success "Configuration loaded:"
pretty_clean " > API URL for build: $API_BASE_URL"
pretty_clean " > S3 Bucket: $S3_BUCKET"
pretty_clean " > CloudFront ID: $CLOUDFRONT_ID"

# 2. Build the Flutter web application
gen_separator
pretty_info "Building Flutter web application with API base: $API_BASE_URL"
gen_separator

cd "$FRONTEND_DIR"

if ! flutter build web --dart-define=API_BASE_URL="$API_BASE_URL"; then
pretty_error "Flutter build failed."
exit 1
fi
pretty_success "Flutter web application built successfully."

# 3. Synchronize build output with S3
gen_separator
pretty_info "Uploading built files to S3 bucket: $S3_BUCKET"
gen_separator

BUILD_DIR="$FRONTEND_DIR/build/web"

if ! aws s3 sync "$BUILD_DIR" "s3://$S3_BUCKET" --delete --acl private; then
pretty_error "S3 sync failed."
exit 1
fi
pretty_success "Files successfully uploaded to S3."

# 4. Invalidate the CloudFront cache
gen_separator
pretty_info "Invalidating CloudFront cache to deploy changes..."
gen_separator

if ! aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_ID" --paths "/*"; then
pretty_error "CloudFront invalidation failed."
pretty_warn "Changes may take some time to appear."
exit 1
fi
pretty_success "CloudFront invalidation created successfully."

gen_separator '='
pretty_success "Frontend deployment complete!"
pretty_info "Your application is available at: $FRONTEND_URL"
gen_separator '='
7 changes: 7 additions & 0 deletions terraform/main/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,10 @@ resource "null_resource" "db_initializer" {
EOT
}
}

# Frontend Hosting
module "frontend_hosting" {
source = "../modules/frontend_hosting"
project_name = var.project_name
api_base_url = module.ecs_cluster.alb_dns_name
}
15 changes: 15 additions & 0 deletions terraform/main/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,18 @@ output "project_name" {
description = "The project name used for all resources"
value = var.project_name
}

output "frontend_url" {
description = "Public URL for the frontend application"
value = "https://${module.frontend_hosting.cloudfront_domain_name}"
}

output "frontend_s3_bucket_name" {
description = "Name of the S3 bucket for the frontend files"
value = module.frontend_hosting.s3_bucket_name
}

output "frontend_cloudfront_distribution_id" {
description = "ID of the CloudFront distribution for the frontend"
value = module.frontend_hosting.cloudfront_distribution_id
}
131 changes: 131 additions & 0 deletions terraform/modules/frontend_hosting/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
resource "aws_s3_bucket" "frontend" {
bucket = "${var.project_name}-frontend-hosting"
}

resource "aws_s3_bucket_website_configuration" "frontend" {
bucket = aws_s3_bucket.frontend.id

index_document {
suffix = "index.html"
}

error_document {
key = "index.html"
}
}

resource "aws_s3_bucket_public_access_block" "frontend" {
bucket = aws_s3_bucket.frontend.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}

resource "aws_cloudfront_origin_access_control" "this" {
name = "${var.project_name}-s3-oac"
description = "Origin Access Control for S3"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}

resource "aws_s3_bucket_policy" "frontend" {
bucket = aws_s3_bucket.frontend.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "cloudfront.amazonaws.com" }
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.frontend.arn}/*"
Condition = {
StringEquals = {
"AWS:SourceArn" = aws_cloudfront_distribution.this.arn
}
}
}
]
})
}

resource "aws_cloudfront_distribution" "this" {
enabled = true
is_ipv6_enabled = true
comment = "Frontend for ${var.project_name}"
default_root_object = "index.html"

origin {
domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name
origin_id = "S3-${aws_s3_bucket.frontend.id}"
origin_access_control_id = aws_cloudfront_origin_access_control.this.id
}

origin {
domain_name = var.api_base_url
origin_id = "ALB-${var.project_name}"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1.2"]
origin_read_timeout = 30
origin_keepalive_timeout = 5
}
}

default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD", "OPTIONS"]
target_origin_id = "S3-${aws_s3_bucket.frontend.id}"

forwarded_values {
query_string = false
headers = ["Origin"]
cookies {
forward = "none"
}
}

viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}

ordered_cache_behavior {
path_pattern = "/api/*"
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
cached_methods = ["GET", "HEAD", "OPTIONS"]
target_origin_id = "ALB-${var.project_name}"

viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 0
max_ttl = 0

forwarded_values {
query_string = true
headers = ["*"]
cookies {
forward = "all"
}
}
}

restrictions {
geo_restriction {
restriction_type = "none"
}
}

viewer_certificate {
cloudfront_default_certificate = true
}

tags = {
Project = var.project_name
Environment = "frontend"
}
}
14 changes: 14 additions & 0 deletions terraform/modules/frontend_hosting/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
output "s3_bucket_name" {
description = "The name of the S3 bucket for the frontend static files."
value = aws_s3_bucket.frontend.id
}

output "cloudfront_distribution_id" {
description = "The ID of the CloudFront distribution."
value = aws_cloudfront_distribution.this.id
}

output "cloudfront_domain_name" {
description = "The domain name of the CloudFront distribution."
value = aws_cloudfront_distribution.this.domain_name
}
9 changes: 9 additions & 0 deletions terraform/modules/frontend_hosting/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
variable "project_name" {
type = string
description = "Global project identifier"
}

variable "api_base_url" {
type = string
description = "The DNS name of the backend Application Load Balancer, used to route API requests."
}