Skip to content

Commit 27de278

Browse files
authored
Added frontend deploy (#64)
1 parent 633e893 commit 27de278

6 files changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
5+
PROJECT_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd)
6+
source "$SCRIPT_DIR/../utils/print.bash"
7+
8+
TF_DIR="$PROJECT_ROOT/terraform/main"
9+
FRONTEND_DIR="$PROJECT_ROOT/frontend"
10+
11+
gen_separator '='
12+
pretty_info "Starting Frontend Deployment to AWS"
13+
gen_separator '='
14+
15+
# 1. Fetch required values from Terraform state
16+
pretty_info "Fetching deployment configuration from Terraform..."
17+
FRONTEND_URL=$(terraform -chdir="$TF_DIR" output -raw frontend_url)
18+
S3_BUCKET=$(terraform -chdir="$TF_DIR" output -raw frontend_s3_bucket_name)
19+
CLOUDFRONT_ID=$(terraform -chdir="$TF_DIR" output -raw frontend_cloudfront_distribution_id)
20+
21+
if [[ -z "$FRONTEND_URL" || -z "$S3_BUCKET" || -z "$CLOUDFRONT_ID" ]]; then
22+
pretty_error "Failed to retrieve necessary deployment values from Terraform."
23+
pretty_error "Ensure Terraform has been applied successfully in '$TF_DIR'."
24+
exit 1
25+
fi
26+
27+
API_BASE_URL="${FRONTEND_URL}/api"
28+
29+
pretty_success "Configuration loaded:"
30+
pretty_clean " > API URL for build: $API_BASE_URL"
31+
pretty_clean " > S3 Bucket: $S3_BUCKET"
32+
pretty_clean " > CloudFront ID: $CLOUDFRONT_ID"
33+
34+
# 2. Build the Flutter web application
35+
gen_separator
36+
pretty_info "Building Flutter web application with API base: $API_BASE_URL"
37+
gen_separator
38+
39+
cd "$FRONTEND_DIR"
40+
41+
if ! flutter build web --dart-define=API_BASE_URL="$API_BASE_URL"; then
42+
pretty_error "Flutter build failed."
43+
exit 1
44+
fi
45+
pretty_success "Flutter web application built successfully."
46+
47+
# 3. Synchronize build output with S3
48+
gen_separator
49+
pretty_info "Uploading built files to S3 bucket: $S3_BUCKET"
50+
gen_separator
51+
52+
BUILD_DIR="$FRONTEND_DIR/build/web"
53+
54+
if ! aws s3 sync "$BUILD_DIR" "s3://$S3_BUCKET" --delete --acl private; then
55+
pretty_error "S3 sync failed."
56+
exit 1
57+
fi
58+
pretty_success "Files successfully uploaded to S3."
59+
60+
# 4. Invalidate the CloudFront cache
61+
gen_separator
62+
pretty_info "Invalidating CloudFront cache to deploy changes..."
63+
gen_separator
64+
65+
if ! aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_ID" --paths "/*"; then
66+
pretty_error "CloudFront invalidation failed."
67+
pretty_warn "Changes may take some time to appear."
68+
exit 1
69+
fi
70+
pretty_success "CloudFront invalidation created successfully."
71+
72+
gen_separator '='
73+
pretty_success "Frontend deployment complete!"
74+
pretty_info "Your application is available at: $FRONTEND_URL"
75+
gen_separator '='

terraform/main/main.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,10 @@ resource "null_resource" "db_initializer" {
153153
EOT
154154
}
155155
}
156+
157+
# Frontend Hosting
158+
module "frontend_hosting" {
159+
source = "../modules/frontend_hosting"
160+
project_name = var.project_name
161+
api_base_url = module.ecs_cluster.alb_dns_name
162+
}

terraform/main/outputs.tf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,18 @@ output "project_name" {
1717
description = "The project name used for all resources"
1818
value = var.project_name
1919
}
20+
21+
output "frontend_url" {
22+
description = "Public URL for the frontend application"
23+
value = "https://${module.frontend_hosting.cloudfront_domain_name}"
24+
}
25+
26+
output "frontend_s3_bucket_name" {
27+
description = "Name of the S3 bucket for the frontend files"
28+
value = module.frontend_hosting.s3_bucket_name
29+
}
30+
31+
output "frontend_cloudfront_distribution_id" {
32+
description = "ID of the CloudFront distribution for the frontend"
33+
value = module.frontend_hosting.cloudfront_distribution_id
34+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
resource "aws_s3_bucket" "frontend" {
2+
bucket = "${var.project_name}-frontend-hosting"
3+
}
4+
5+
resource "aws_s3_bucket_website_configuration" "frontend" {
6+
bucket = aws_s3_bucket.frontend.id
7+
8+
index_document {
9+
suffix = "index.html"
10+
}
11+
12+
error_document {
13+
key = "index.html"
14+
}
15+
}
16+
17+
resource "aws_s3_bucket_public_access_block" "frontend" {
18+
bucket = aws_s3_bucket.frontend.id
19+
block_public_acls = true
20+
block_public_policy = true
21+
ignore_public_acls = true
22+
restrict_public_buckets = true
23+
}
24+
25+
resource "aws_cloudfront_origin_access_control" "this" {
26+
name = "${var.project_name}-s3-oac"
27+
description = "Origin Access Control for S3"
28+
origin_access_control_origin_type = "s3"
29+
signing_behavior = "always"
30+
signing_protocol = "sigv4"
31+
}
32+
33+
resource "aws_s3_bucket_policy" "frontend" {
34+
bucket = aws_s3_bucket.frontend.id
35+
policy = jsonencode({
36+
Version = "2012-10-17"
37+
Statement = [
38+
{
39+
Effect = "Allow"
40+
Principal = { Service = "cloudfront.amazonaws.com" }
41+
Action = "s3:GetObject"
42+
Resource = "${aws_s3_bucket.frontend.arn}/*"
43+
Condition = {
44+
StringEquals = {
45+
"AWS:SourceArn" = aws_cloudfront_distribution.this.arn
46+
}
47+
}
48+
}
49+
]
50+
})
51+
}
52+
53+
resource "aws_cloudfront_distribution" "this" {
54+
enabled = true
55+
is_ipv6_enabled = true
56+
comment = "Frontend for ${var.project_name}"
57+
default_root_object = "index.html"
58+
59+
origin {
60+
domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name
61+
origin_id = "S3-${aws_s3_bucket.frontend.id}"
62+
origin_access_control_id = aws_cloudfront_origin_access_control.this.id
63+
}
64+
65+
origin {
66+
domain_name = var.api_base_url
67+
origin_id = "ALB-${var.project_name}"
68+
custom_origin_config {
69+
http_port = 80
70+
https_port = 443
71+
origin_protocol_policy = "http-only"
72+
origin_ssl_protocols = ["TLSv1.2"]
73+
origin_read_timeout = 30
74+
origin_keepalive_timeout = 5
75+
}
76+
}
77+
78+
default_cache_behavior {
79+
allowed_methods = ["GET", "HEAD", "OPTIONS"]
80+
cached_methods = ["GET", "HEAD", "OPTIONS"]
81+
target_origin_id = "S3-${aws_s3_bucket.frontend.id}"
82+
83+
forwarded_values {
84+
query_string = false
85+
headers = ["Origin"]
86+
cookies {
87+
forward = "none"
88+
}
89+
}
90+
91+
viewer_protocol_policy = "redirect-to-https"
92+
min_ttl = 0
93+
default_ttl = 3600
94+
max_ttl = 86400
95+
}
96+
97+
ordered_cache_behavior {
98+
path_pattern = "/api/*"
99+
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
100+
cached_methods = ["GET", "HEAD", "OPTIONS"]
101+
target_origin_id = "ALB-${var.project_name}"
102+
103+
viewer_protocol_policy = "redirect-to-https"
104+
min_ttl = 0
105+
default_ttl = 0
106+
max_ttl = 0
107+
108+
forwarded_values {
109+
query_string = true
110+
headers = ["*"]
111+
cookies {
112+
forward = "all"
113+
}
114+
}
115+
}
116+
117+
restrictions {
118+
geo_restriction {
119+
restriction_type = "none"
120+
}
121+
}
122+
123+
viewer_certificate {
124+
cloudfront_default_certificate = true
125+
}
126+
127+
tags = {
128+
Project = var.project_name
129+
Environment = "frontend"
130+
}
131+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
output "s3_bucket_name" {
2+
description = "The name of the S3 bucket for the frontend static files."
3+
value = aws_s3_bucket.frontend.id
4+
}
5+
6+
output "cloudfront_distribution_id" {
7+
description = "The ID of the CloudFront distribution."
8+
value = aws_cloudfront_distribution.this.id
9+
}
10+
11+
output "cloudfront_domain_name" {
12+
description = "The domain name of the CloudFront distribution."
13+
value = aws_cloudfront_distribution.this.domain_name
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
variable "project_name" {
2+
type = string
3+
description = "Global project identifier"
4+
}
5+
6+
variable "api_base_url" {
7+
type = string
8+
description = "The DNS name of the backend Application Load Balancer, used to route API requests."
9+
}

0 commit comments

Comments
 (0)