From 695fdbc705790df40137d05fb44a771d4a5370a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kryczka?= Date: Mon, 16 Jun 2025 14:49:25 +0200 Subject: [PATCH] Added frontend deploy --- scripts/actions/deploy_frontend.bash | 75 ++++++++++ terraform/main/main.tf | 7 + terraform/main/outputs.tf | 15 ++ terraform/modules/frontend_hosting/main.tf | 131 ++++++++++++++++++ terraform/modules/frontend_hosting/outputs.tf | 14 ++ .../modules/frontend_hosting/variables.tf | 9 ++ 6 files changed, 251 insertions(+) create mode 100755 scripts/actions/deploy_frontend.bash create mode 100644 terraform/modules/frontend_hosting/main.tf create mode 100644 terraform/modules/frontend_hosting/outputs.tf create mode 100644 terraform/modules/frontend_hosting/variables.tf diff --git a/scripts/actions/deploy_frontend.bash b/scripts/actions/deploy_frontend.bash new file mode 100755 index 0000000..cffd8b7 --- /dev/null +++ b/scripts/actions/deploy_frontend.bash @@ -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 '=' diff --git a/terraform/main/main.tf b/terraform/main/main.tf index e5b4789..7f3f81d 100644 --- a/terraform/main/main.tf +++ b/terraform/main/main.tf @@ -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 +} diff --git a/terraform/main/outputs.tf b/terraform/main/outputs.tf index 43af84c..9b38488 100644 --- a/terraform/main/outputs.tf +++ b/terraform/main/outputs.tf @@ -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 +} diff --git a/terraform/modules/frontend_hosting/main.tf b/terraform/modules/frontend_hosting/main.tf new file mode 100644 index 0000000..6f58ec6 --- /dev/null +++ b/terraform/modules/frontend_hosting/main.tf @@ -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" + } +} diff --git a/terraform/modules/frontend_hosting/outputs.tf b/terraform/modules/frontend_hosting/outputs.tf new file mode 100644 index 0000000..79f66bd --- /dev/null +++ b/terraform/modules/frontend_hosting/outputs.tf @@ -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 +} diff --git a/terraform/modules/frontend_hosting/variables.tf b/terraform/modules/frontend_hosting/variables.tf new file mode 100644 index 0000000..f197b2c --- /dev/null +++ b/terraform/modules/frontend_hosting/variables.tf @@ -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." +}