Skip to content

feat: host frontend on S3 + CloudFront (static SPA), remove Amplify - #268

Merged
nourshoreibah merged 3 commits into
mainfrom
feat/frontend-static-s3-cloudfront
Jul 1, 2026
Merged

feat: host frontend on S3 + CloudFront (static SPA), remove Amplify#268
nourshoreibah merged 3 commits into
mainfrom
feat/frontend-static-s3-cloudfront

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Why

Amplify SSR hosting is wedged on the build's Unable to assume specified IAM Role quirk (correct trust, regional principal, sts:TagSession — all tried, all fail; it's the documented Amplify-SSR-with-IaC-role issue). The app is actually a client-side SPA (all 'use client', JWT in localStorage, data via apiFetch) — SSR buys nothing. So drop Amplify and host the static export on S3 + CloudFront.

What

Frontend (already committed on this branch):

  • next.config.tsoutput: 'export', trailingSlash, images.unoptimized; dev rewrites removed (don't run in export).
  • projects/[id] split into a server wrapper (generateStaticParams) + ProjectDetailClient.tsx (existing UI). Verified next build emits out/ with index.html, per-route pages, and a projects/placeholder shell.

Infra:

  • frontend_hosting.tf — private S3 + CloudFront (OAC), SPA fallback (403/404 → /index.html), index-rewrite CloudFront Function, HTTPS. Outputs bucket + distribution id + public domain.
  • Removed amplify.tf + amplify_notifications.tf (apply tears down the Amplify app, SSR role, EventBridge) + unused api_base_url var.

CI:

  • frontend-deploy.yml — push to apps/frontend/** → build export (with NEXT_PUBLIC_API_BASE_URL resolved from API Gateway) → s3 sync → CloudFront invalidation. OIDC apply role + production env.

Docs: AGENTS.md (root/frontend/infra/.github) updated to the static model.

/projects/:id routing — verified, not broken

  • In-app nav: the [id] route is in the client bundle → client-side navigation renders it, no server round-trip.
  • Deep link / refresh on /projects/123: S3 404 → CloudFront rewrites to /index.html (200) → Next client router renders it → apiFetch loads the project. Same client-fetch behavior as today.

Dependencies / sequencing

🤖 Generated with Claude Code

nourshoreibah and others added 3 commits June 30, 2026 22:48
The app is fully client-rendered (JWT in localStorage, all data via apiFetch),
so switch Next to `output: 'export'` and host the static bundle on S3+CloudFront
instead of Amplify SSR.

- next.config.ts: output: 'export', trailingSlash, images.unoptimized; drop the
  dev rewrites (they don't run in export — prod routing is NEXT_PUBLIC_API_BASE_URL).
- projects/[id]: export requires generateStaticParams in a Server Component, but
  the page is 'use client'. Split into a server wrapper (page.tsx, emits a
  throwaway shell) + ProjectDetailClient.tsx (the existing UI, reads id via
  useParams). Real /projects/:id is client-rendered; deep links resolve through
  the CloudFront SPA fallback (404 -> /index.html).

Verified: `next build` exports out/ with index.html + per-route pages +
projects/placeholder shell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace Amplify SSR hosting (blocked indefinitely on the build's "Unable to
assume specified IAM Role" quirk) with a static-SPA deploy that fits the app.

- infrastructure/aws/frontend_hosting.tf: private S3 bucket + CloudFront (OAC),
  SPA fallback (403/404 -> /index.html), index-rewrite CloudFront Function,
  HTTPS via the default CloudFront cert. Outputs bucket + distribution id.
- .github/workflows/frontend-deploy.yml: on push to apps/frontend/**, build the
  export with NEXT_PUBLIC_API_BASE_URL, s3 sync out/, invalidate CloudFront.
  Uses the OIDC apply role + production environment.
- Remove amplify.tf + amplify_notifications.tf (apply tears down the Amplify app,
  SSR role, and EventBridge notification plumbing) + the now-unused api_base_url var.
- Docs: root/frontend/infra/.github AGENTS.md updated to the static-hosting model.

Depends on the OIDC roles (infrastructure/aws/oidc.tf) existing for the deploy
workflow's apply role.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  - Auto-formatted .tf files with terraform fmt
  - Updated README.md with terraform-docs

  Co-authored-by: nourshoreibah <nourshoreibah@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
data.infisical_secrets.rds_folder: Reading...
data.infisical_secrets.github_folder: Reading...
aws_cloudwatch_event_api_destination.github_dispatch: Refreshing state... [id=branch-github-dispatch]
aws_iam_role_policy.eventbridge_api_dest: Refreshing state... [id=branch-eventbridge-api-dest-role:invoke-github-dispatch]
aws_iam_role_policy_attachment.amplify_ssr: Refreshing state... [id=branch-amplify-ssr-role/arn:aws:iam::aws:policy/AdministratorAccess-Amplify]
aws_cloudwatch_event_connection.github_dispatch: Refreshing state... [id=branch-github-dispatch]
aws_iam_role.eventbridge_api_dest: Refreshing state... [id=branch-eventbridge-api-dest-role]
aws_amplify_app.frontend: Refreshing state... [id=dbcy90q4o31f7]
aws_cloudwatch_event_target.github_dispatch: Refreshing state... [id=branch-amplify-frontend-deploy-terraform-20260701023824828400000001]
aws_cloudwatch_event_rule.amplify_frontend_deploy: Refreshing state... [id=branch-amplify-frontend-deploy]
data.infisical_secrets.github_folder: Read complete after 0s
aws_iam_role.amplify_ssr: Refreshing state... [id=branch-amplify-ssr-role]
data.infisical_secrets.rds_folder: Read complete after 0s
aws_amplify_branch.main: Refreshing state... [id=dbcy90q4o31f7/main]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_CxTueqe6g]
data.aws_caller_identity.current: Reading...
data.aws_caller_identity.current: Read complete after 0s [id=489881683177]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
data.aws_iam_policy_document.ci_apply_assume: Reading...
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_s3_bucket_policy.reports_bucket_policy: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=klh9jv]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-2apxzxb0r8-prod]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  - destroy
 <= read (data resources)

Terraform will perform the following actions:

  # data.aws_iam_policy_document.frontend_bucket will be read during apply
  # (config refers to values not yet known)
 <= data "aws_iam_policy_document" "frontend_bucket" {
      + id            = (known after apply)
      + json          = (known after apply)
      + minified_json = (known after apply)

      + statement {
          + actions   = [
              + "s3:GetObject",
            ]
          + resources = [
              + (known after apply),
            ]

          + condition {
              + test     = "StringEquals"
              + values   = [
                  + (known after apply),
                ]
              + variable = "AWS:SourceArn"
            }

          + principals {
              + identifiers = [
                  + "cloudfront.amazonaws.com",
                ]
              + type        = "Service"
            }
        }
    }

  # aws_amplify_app.frontend will be destroyed
  # (because aws_amplify_app.frontend is not in configuration)
  - resource "aws_amplify_app" "frontend" {
      - access_token                  = (sensitive value) -> null
      - arn                           = "arn:aws:amplify:us-east-2:489881683177:apps/dbcy90q4o31f7" -> null
      - auto_branch_creation_patterns = [] -> null
      - build_spec                    = <<-EOT
            version: 1
            applications:
              - appRoot: apps/frontend
                frontend:
                  phases:
                    preBuild:
                      commands:
                        - npm ci
                    build:
                      commands:
                        - npm run build
                  artifacts:
                    baseDirectory: .next
                    files:
                      - '**/*'
                  cache:
                    paths:
                      - .next/cache/**/*
        EOT -> null
      - default_domain                = "dbcy90q4o31f7.amplifyapp.com" -> null
      - enable_auto_branch_creation   = false -> null
      - enable_basic_auth             = false -> null
      - enable_branch_auto_build      = false -> null
      - enable_branch_auto_deletion   = true -> null
      - environment_variables         = {
          - "NEXT_PUBLIC_API_BASE_URL" = "https://2apxzxb0r8.execute-api.us-east-2.amazonaws.com/prod"
        } -> null
      - iam_service_role_arn          = "arn:aws:iam::489881683177:role/branch-amplify-ssr-role" -> null
      - id                            = "dbcy90q4o31f7" -> null
      - name                          = "branch-frontend" -> null
      - platform                      = "WEB_COMPUTE" -> null
      - production_branch             = [
          - {
              - branch_name      = "main"
              - last_deploy_time = "2026-07-01T02:40:19Z"
              - status           = "FAILED"
                # (1 unchanged attribute hidden)
            },
        ] -> null
      - region                        = "us-east-2" -> null
      - repository                    = "https://github.com/Code-4-Community/branch" -> null
      - tags                          = {} -> null
      - tags_all                      = {} -> null
        # (4 unchanged attributes hidden)

      - cache_config {
          - type = "AMPLIFY_MANAGED_NO_COOKIES" -> null
        }

      - job_config {
          - build_compute_type = "STANDARD_8GB" -> null
        }
    }

  # aws_amplify_branch.main will be destroyed
  # (because aws_amplify_branch.main is not in configuration)
  - resource "aws_amplify_branch" "main" {
      - app_id                        = "dbcy90q4o31f7" -> null
      - arn                           = "arn:aws:amplify:us-east-2:489881683177:apps/dbcy90q4o31f7/branches/main" -> null
      - associated_resources          = [] -> null
      - branch_name                   = "main" -> null
      - custom_domains                = [] -> null
      - display_name                  = "main" -> null
      - enable_auto_build             = true -> null
      - enable_basic_auth             = false -> null
      - enable_notification           = false -> null
      - enable_performance_mode       = false -> null
      - enable_pull_request_preview   = false -> null
      - enable_skew_protection        = false -> null
      - environment_variables         = {} -> null
      - framework                     = "Next.js - SSR" -> null
      - id                            = "dbcy90q4o31f7/main" -> null
      - region                        = "us-east-2" -> null
      - stage                         = "PRODUCTION" -> null
      - tags                          = {} -> null
      - tags_all                      = {} -> null
      - ttl                           = "5" -> null
        # (6 unchanged attributes hidden)
    }

  # aws_cloudfront_distribution.frontend will be created
  + resource "aws_cloudfront_distribution" "frontend" {
      + arn                             = (known after apply)
      + caller_reference                = (known after apply)
      + comment                         = "branch frontend (static SPA)"
      + continuous_deployment_policy_id = (known after apply)
      + default_root_object             = "index.html"
      + domain_name                     = (known after apply)
      + enabled                         = true
      + etag                            = (known after apply)
      + hosted_zone_id                  = (known after apply)
      + http_version                    = "http2"
      + id                              = (known after apply)
      + in_progress_validation_batches  = (known after apply)
      + is_ipv6_enabled                 = false
      + last_modified_time              = (known after apply)
      + price_class                     = "PriceClass_100"
      + retain_on_delete                = false
      + staging                         = false
      + status                          = (known after apply)
      + tags_all                        = (known after apply)
      + trusted_key_groups              = (known after apply)
      + trusted_signers                 = (known after apply)
      + wait_for_deployment             = true

      + custom_error_response {
          + error_caching_min_ttl = 10
          + error_code            = 403
          + response_code         = 200
          + response_page_path    = "/index.html"
        }
      + custom_error_response {
          + error_caching_min_ttl = 10
          + error_code            = 404
          + response_code         = 200
          + response_page_path    = "/index.html"
        }

      + default_cache_behavior {
          + allowed_methods        = [
              + "GET",
              + "HEAD",
              + "OPTIONS",
            ]
          + cache_policy_id        = "658327ea-f89d-4fab-a63d-7e88639e58f6"
          + cached_methods         = [
              + "GET",
              + "HEAD",
            ]
          + compress               = false
          + default_ttl            = (known after apply)
          + max_ttl                = (known after apply)
          + min_ttl                = 0
          + target_origin_id       = "s3-frontend"
          + trusted_key_groups     = (known after apply)
          + trusted_signers        = (known after apply)
          + viewer_protocol_policy = "redirect-to-https"

          + function_association {
              + event_type   = "viewer-request"
              + function_arn = (known after apply)
            }

          + grpc_config (known after apply)
        }

      + origin {
          + connection_attempts         = 3
          + connection_timeout          = 10
          + domain_name                 = (known after apply)
          + origin_access_control_id    = (known after apply)
          + origin_id                   = "s3-frontend"
          + response_completion_timeout = (known after apply)
            # (1 unchanged attribute hidden)
        }

      + restrictions {
          + geo_restriction {
              + locations        = (known after apply)
              + restriction_type = "none"
            }
        }

      + viewer_certificate {
          + cloudfront_default_certificate = true
          + minimum_protocol_version       = "TLSv1"
        }
    }

  # aws_cloudfront_function.rewrite_index will be created
  + resource "aws_cloudfront_function" "rewrite_index" {
      + arn             = (known after apply)
      + code            = <<-EOT
            function handler(event) {
              var request = event.request;
              var uri = request.uri;
              if (uri.endsWith('/')) {
                request.uri += 'index.html';
              } else if (!uri.includes('.')) {
                request.uri += '/index.html';
              }
              return request;
            }
        EOT
      + comment         = "Append index.html to directory/extensionless requests"
      + etag            = (known after apply)
      + id              = (known after apply)
      + live_stage_etag = (known after apply)
      + name            = "branch-frontend-rewrite-index"
      + publish         = true
      + runtime         = "cloudfront-js-2.0"
      + status          = (known after apply)
    }

  # aws_cloudfront_origin_access_control.frontend will be created
  + resource "aws_cloudfront_origin_access_control" "frontend" {
      + arn                               = (known after apply)
      + description                       = "Managed by Terraform"
      + etag                              = (known after apply)
      + id                                = (known after apply)
      + name                              = "branch-frontend-oac"
      + origin_access_control_origin_type = "s3"
      + signing_behavior                  = "always"
      + signing_protocol                  = "sigv4"
    }

  # aws_cloudwatch_event_api_destination.github_dispatch will be destroyed
  # (because aws_cloudwatch_event_api_destination.github_dispatch is not in configuration)
  - resource "aws_cloudwatch_event_api_destination" "github_dispatch" {
      - arn                              = "arn:aws:events:us-east-2:489881683177:api-destination/branch-github-dispatch/2027e9c4-e61e-4528-9c54-b6b54d4c5280" -> null
      - connection_arn                   = "arn:aws:events:us-east-2:489881683177:connection/branch-github-dispatch/b8f8c860-891b-4f34-ae55-973ae92d4423" -> null
      - http_method                      = "POST" -> null
      - id                               = "branch-github-dispatch" -> null
      - invocation_endpoint              = "https://api.github.com/repos/Code-4-Community/branch/dispatches" -> null
      - invocation_rate_limit_per_second = 10 -> null
      - name                             = "branch-github-dispatch" -> null
      - region                           = "us-east-2" -> null
        # (1 unchanged attribute hidden)
    }

  # aws_cloudwatch_event_connection.github_dispatch will be destroyed
  # (because aws_cloudwatch_event_connection.github_dispatch is not in configuration)
  - resource "aws_cloudwatch_event_connection" "github_dispatch" {
      - arn                = "arn:aws:events:us-east-2:489881683177:connection/branch-github-dispatch/b8f8c860-891b-4f34-ae55-973ae92d4423" -> null
      - authorization_type = "API_KEY" -> null
      - id                 = "branch-github-dispatch" -> null
      - name               = "branch-github-dispatch" -> null
      - region             = "us-east-2" -> null
      - secret_arn         = "arn:aws:secretsmanager:us-east-2:489881683177:secret:events!connection/branch-github-dispatch/4a355643-8865-40b0-875b-ef48a63b155e-BxJVAj" -> null
        # (2 unchanged attributes hidden)

      - auth_parameters {
          - api_key {
              - key   = "Authorization" -> null
              - value = (sensitive value) -> null
            }
        }
    }

  # aws_cloudwatch_event_rule.amplify_frontend_deploy will be destroyed
  # (because aws_cloudwatch_event_rule.amplify_frontend_deploy is not in configuration)
  - resource "aws_cloudwatch_event_rule" "amplify_frontend_deploy" {
      - arn                 = "arn:aws:events:us-east-2:489881683177:rule/branch-amplify-frontend-deploy" -> null
      - description         = "Amplify main-branch deploy status changes for the frontend app" -> null
      - event_bus_name      = "default" -> null
      - event_pattern       = jsonencode(
            {
              - detail      = {
                  - appId      = [
                      - "dbcy90q4o31f7",
                    ]
                  - branchName = [
                      - "main",
                    ]
                  - jobStatus  = [
                      - "SUCCEED",
                      - "FAILED",
                      - "CANCELLED",
                    ]
                }
              - detail-type = [
                  - "Amplify Deployment Status Change",
                ]
              - source      = [
                  - "aws.amplify",
                ]
            }
        ) -> null
      - force_destroy       = false -> null
      - id                  = "branch-amplify-frontend-deploy" -> null
      - is_enabled          = true -> null
      - name                = "branch-amplify-frontend-deploy" -> null
      - region              = "us-east-2" -> null
      - state               = "ENABLED" -> null
      - tags                = {} -> null
      - tags_all            = {} -> null
        # (3 unchanged attributes hidden)
    }

  # aws_cloudwatch_event_target.github_dispatch will be destroyed
  # (because aws_cloudwatch_event_target.github_dispatch is not in configuration)
  - resource "aws_cloudwatch_event_target" "github_dispatch" {
      - arn            = "arn:aws:events:us-east-2:489881683177:api-destination/branch-github-dispatch/2027e9c4-e61e-4528-9c54-b6b54d4c5280" -> null
      - event_bus_name = "default" -> null
      - force_destroy  = false -> null
      - id             = "branch-amplify-frontend-deploy-terraform-20260701023824828400000001" -> null
      - region         = "us-east-2" -> null
      - role_arn       = "arn:aws:iam::489881683177:role/branch-eventbridge-api-dest-role" -> null
      - rule           = "branch-amplify-frontend-deploy" -> null
      - target_id      = "terraform-20260701023824828400000001" -> null
        # (2 unchanged attributes hidden)

      - http_target {
          - header_parameters       = {
              - "Accept"               = "application/vnd.github+json"
              - "Content-Type"         = "application/json"
              - "X-GitHub-Api-Version" = "2022-11-28"
            } -> null
          - path_parameter_values   = [] -> null
          - query_string_parameters = {} -> null
        }

      - input_transformer {
          - input_paths    = {
              - "appId"  = "$.detail.appId"
              - "jobId"  = "$.detail.jobId"
              - "status" = "$.detail.jobStatus"
            } -> null
          - input_template = <<-EOT
                {
                  "event_type": "amplify-deploy",
                  "client_payload": {
                    "jobId": <jobId>,
                    "status": <status>,
                    "appId": <appId>
                  }
                }
            EOT -> null
        }
    }

  # aws_iam_role.amplify_ssr will be destroyed
  # (because aws_iam_role.amplify_ssr is not in configuration)
  - resource "aws_iam_role" "amplify_ssr" {
      - arn                   = "arn:aws:iam::489881683177:role/branch-amplify-ssr-role" -> null
      - assume_role_policy    = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = [
                          - "sts:AssumeRole",
                          - "sts:TagSession",
                        ]
                      - Effect    = "Allow"
                      - Principal = {
                          - Service = [
                              - "amplify.us-east-2.amazonaws.com",
                              - "amplify.amazonaws.com",
                            ]
                        }
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> null
      - create_date           = "2026-06-30T22:48:38Z" -> null
      - force_detach_policies = false -> null
      - id                    = "branch-amplify-ssr-role" -> null
      - managed_policy_arns   = [
          - "arn:aws:iam::aws:policy/AdministratorAccess-Amplify",
        ] -> null
      - max_session_duration  = 3600 -> null
      - name                  = "branch-amplify-ssr-role" -> null
      - path                  = "/" -> null
      - tags                  = {} -> null
      - tags_all              = {} -> null
      - unique_id             = "AROAXEDZZITUTNX27F3KC" -> null
        # (3 unchanged attributes hidden)
    }

  # aws_iam_role.eventbridge_api_dest will be destroyed
  # (because aws_iam_role.eventbridge_api_dest is not in configuration)
  - resource "aws_iam_role" "eventbridge_api_dest" {
      - arn                   = "arn:aws:iam::489881683177:role/branch-eventbridge-api-dest-role" -> null
      - assume_role_policy    = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = "sts:AssumeRole"
                      - Effect    = "Allow"
                      - Principal = {
                          - Service = "events.amazonaws.com"
                        }
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> null
      - create_date           = "2026-07-01T00:32:46Z" -> null
      - force_detach_policies = false -> null
      - id                    = "branch-eventbridge-api-dest-role" -> null
      - managed_policy_arns   = [] -> null
      - max_session_duration  = 3600 -> null
      - name                  = "branch-eventbridge-api-dest-role" -> null
      - path                  = "/" -> null
      - tags                  = {} -> null
      - tags_all              = {} -> null
      - unique_id             = "AROAXEDZZITUWYHNTTLXE" -> null
        # (3 unchanged attributes hidden)

      - inline_policy {
          - name   = "invoke-github-dispatch" -> null
          - policy = jsonencode(
                {
                  - Statement = [
                      - {
                          - Action   = "events:InvokeApiDestination"
                          - Effect   = "Allow"
                          - Resource = "arn:aws:events:us-east-2:489881683177:api-destination/branch-github-dispatch/2027e9c4-e61e-4528-9c54-b6b54d4c5280"
                        },
                    ]
                  - Version   = "2012-10-17"
                }
            ) -> null
        }
    }

  # aws_iam_role_policy.eventbridge_api_dest will be destroyed
  # (because aws_iam_role_policy.eventbridge_api_dest is not in configuration)
  - resource "aws_iam_role_policy" "eventbridge_api_dest" {
      - id          = "branch-eventbridge-api-dest-role:invoke-github-dispatch" -> null
      - name        = "invoke-github-dispatch" -> null
      - policy      = jsonencode(
            {
              - Statement = [
                  - {
                      - Action   = "events:InvokeApiDestination"
                      - Effect   = "Allow"
                      - Resource = "arn:aws:events:us-east-2:489881683177:api-destination/branch-github-dispatch/2027e9c4-e61e-4528-9c54-b6b54d4c5280"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> null
      - role        = "branch-eventbridge-api-dest-role" -> null
        # (1 unchanged attribute hidden)
    }

  # aws_iam_role_policy_attachment.amplify_ssr will be destroyed
  # (because aws_iam_role_policy_attachment.amplify_ssr is not in configuration)
  - resource "aws_iam_role_policy_attachment" "amplify_ssr" {
      - id         = "branch-amplify-ssr-role/arn:aws:iam::aws:policy/AdministratorAccess-Amplify" -> null
      - policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess-Amplify" -> null
      - role       = "branch-amplify-ssr-role" -> null
    }

  # aws_s3_bucket.frontend will be created
  + resource "aws_s3_bucket" "frontend" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "branch-frontend-489881683177"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_region               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = "us-east-2"
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket_policy.frontend will be created
  + resource "aws_s3_bucket_policy" "frontend" {
      + bucket = (known after apply)
      + id     = (known after apply)
      + policy = (known after apply)
      + region = "us-east-2"
    }

  # aws_s3_bucket_public_access_block.frontend will be created
  + resource "aws_s3_bucket_public_access_block" "frontend" {
      + block_public_acls       = true
      + block_public_policy     = true
      + bucket                  = (known after apply)
      + id                      = (known after apply)
      + ignore_public_acls      = true
      + region                  = "us-east-2"
      + restrict_public_buckets = true
    }

Plan: 6 to add, 0 to change, 10 to destroy.

Changes to Outputs:
  + frontend_bucket                     = "branch-frontend-489881683177"
  + frontend_cloudfront_distribution_id = (known after apply)
  + frontend_cloudfront_domain          = (known after apply)

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

Pushed by: @nourshoreibah, Action: pull_request

@nourshoreibah
nourshoreibah marked this pull request as ready for review July 1, 2026 04:06
@github-actions
github-actions Bot requested review from mehanana and tsudhakar87 July 1, 2026 04:06
github-actions Bot added a commit that referenced this pull request Jul 1, 2026
@nourshoreibah
nourshoreibah merged commit 989a279 into main Jul 1, 2026
14 checks passed
@nourshoreibah
nourshoreibah deleted the feat/frontend-static-s3-cloudfront branch July 1, 2026 04:06
github-actions Bot added a commit that referenced this pull request Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

✅ Terraform Apply — succeeded

1/1 workspaces applied

Workflow run · 989a279

Output
… (truncated; see full logs in the workflow run)
aws_amplify_app.frontend: Destroying... [id=dbcy90q4o31f7]
aws_cloudwatch_event_api_destination.github_dispatch: Destruction complete after 1s
aws_cloudwatch_event_connection.github_dispatch: Destroying... [id=branch-github-dispatch]
aws_iam_role.eventbridge_api_dest: Destruction complete after 1s
aws_amplify_app.frontend: Destruction complete after 0s
aws_iam_role.amplify_ssr: Destroying... [id=branch-amplify-ssr-role]
aws_iam_role.amplify_ssr: Destruction complete after 0s
aws_s3_bucket.frontend: Creation complete after 1s [id=branch-frontend-489881683177]
aws_s3_bucket_public_access_block.frontend: Creating...
aws_cloudwatch_event_connection.github_dispatch: Destruction complete after 0s
aws_s3_bucket_public_access_block.frontend: Creation complete after 0s [id=branch-frontend-489881683177]
aws_cloudfront_function.rewrite_index: Creation complete after 3s [id=branch-frontend-rewrite-index]
aws_cloudfront_distribution.frontend: Creating...
aws_cloudfront_distribution.frontend: Still creating... [00m10s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [00m20s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [00m30s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [00m40s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [00m50s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [01m00s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [01m10s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [01m20s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [01m30s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [01m40s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [01m50s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [02m00s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [02m10s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [02m20s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [02m30s elapsed]
aws_cloudfront_distribution.frontend: Still creating... [02m40s elapsed]
aws_cloudfront_distribution.frontend: Creation complete after 2m44s [id=E37FDHRYNZNF4R]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1471335443]
aws_s3_bucket_policy.frontend: Creating...
aws_s3_bucket_policy.frontend: Creation complete after 1s [id=branch-frontend-489881683177]

Apply complete! Resources: 6 added, 0 changed, 10 destroyed.

Outputs:

api_gateway_url = "https://2apxzxb0r8.execute-api.us-east-2.amazonaws.com/prod"
ci_apply_role_arn = "arn:aws:iam::489881683177:role/branch-ci-apply"
ci_plan_role_arn = "arn:aws:iam::489881683177:role/branch-ci-plan"
cognito_region = "us-east-2"
cognito_user_pool_arn = "arn:aws:cognito-idp:us-east-2:489881683177:userpool/us-east-2_CxTueqe6g"
cognito_user_pool_endpoint = "cognito-idp.us-east-2.amazonaws.com/us-east-2_CxTueqe6g"
frontend_bucket = "branch-frontend-489881683177"
frontend_cloudfront_distribution_id = "E37FDHRYNZNF4R"
frontend_cloudfront_domain = "d3jd1l43u0b38s.cloudfront.net"
reports_bucket_name = "c4c-branch-generated-reports20251030194253425700000001"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant