Skip to content

fix(cms): bootstrap internal api token and stage/prod routing#302

Merged
tataihono merged 12 commits intomainfrom
fix/299-strapi-internal-api-token-routing
Mar 10, 2026
Merged

fix(cms): bootstrap internal api token and stage/prod routing#302
tataihono merged 12 commits intomainfrom
fix/299-strapi-internal-api-token-routing

Conversation

@tataihono
Copy link
Contributor

@tataihono tataihono commented Mar 9, 2026

Summary

  • bootstrap Strapi to ensure a managed read-only internal API token from STRAPI_INTERNAL_API_TOKEN, rotating when value/type changes
  • add CMS/Vercel/GitHub SSM parameters and mappings so only Vercel production uses prod token while preview and GitHub builds use stage token
  • document new token routing in infra READMEs and keep Terraform-managed secret sources explicit

Contracts Changed

  • yes
  • no

Regeneration Required

  • yes
  • no

Validation

  • Contracts validated
  • Generated code verified (no manual edits)
  • Tests and build passed
  • Terraform plan reviewed (if infra change)

Resolves #299

Summary by CodeRabbit

  • New Features

    • Internal read-only CMS API token with bootstrap-on-startup, validation and automated rotation.
  • Chores

    • Infrastructure now generates, stores, and injects the internal token into CMS runtime, CI, and deployment platforms.
    • CI and deployment secrets/environment variables added to surface the token to GitHub Actions and Vercel.
  • Documentation

    • Docs and READMEs updated; .env example now documents the internal token variable.

Ensure Strapi creates or rotates a managed read-only internal API
 token from env at startup. Wire SSM-backed stage/prod token routing
 so only Vercel production uses prod while preview and GitHub builds
 stay on stage. Resolves #299.

Made-with: Cursor
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Bootstraps and enforces a read-only internal Strapi API token from STRAPI_INTERNAL_API_TOKEN at CMS startup; adds SSM parameter and ECS secret wiring in AWS, and routes stage/prod tokens to Vercel and GitHub via new Terraform data/resources.

Changes

Cohort / File(s) Summary
CMS runtime
apps/cms/.env.example, apps/cms/src/index.ts, apps/cms/src/internal-api-token.ts, apps/cms/AGENTS.md
Added STRAPI_INTERNAL_API_TOKEN example; implemented async bootstrap that calls ensureInternalApiToken (new module) to create/validate/rotate a read-only internal API token with advisory DB locking and robust verification; added typing guidance.
AWS — CMS module
infra/aws/modules/cms/main.tf
Created SecureString SSM parameter strapi_internal_api_token (KMS-backed ephemeral secret); injected into ECS task secrets and updated ECS IAM policy to allow read access.
AWS — GitHub infra
infra/aws/github/terraform.tf, infra/aws/github/ssm.tf, infra/github/data.tf, infra/github/actions.tf, infra/github/README.md
Added conditional CMS KMS/SSM data sources and explicit SSM/KMS ARNs for stage/prod; added data source for stage STRAPI_INTERNAL_API_TOKEN and GitHub Actions secret STRAPI_API_TOKEN sourced from the stage SSM; README updated.
Vercel integration
infra/vercel/data.tf, infra/vercel/main.tf, infra/vercel/README.md, infra/aws/vercel/ssm.tf
Added data sources for stage/prod CMS SSM tokens and two Vercel project environment variables mapping STRAPI_API_TOKEN (preview→stage, production→prod); documented routing.
Formatting / trivial
infra/aws/github/ssm.tf, infra/aws/vercel/ssm.tf
Minor trailing-newline/formatting edits only.

Sequence Diagram

sequenceDiagram
    participant App as CMS App (startup)
    participant Env as Env (STRAPI_INTERNAL_API_TOKEN)
    participant Bootstrap as ensureInternalApiToken
    participant Strapi as Strapi admin api-token service
    participant DB as Strapi DB

    App->>Env: read STRAPI_INTERNAL_API_TOKEN
    App->>Bootstrap: invoke with token
    Bootstrap->>Strapi: resolve api-token service
    alt service unavailable
        Strapi-->>Bootstrap: null -> log & exit
    else service available
        Bootstrap->>DB: acquire advisory lock
        Bootstrap->>Strapi: query token by name
        Strapi->>DB: read tokens
        DB-->>Strapi: token record?
        Strapi-->>Bootstrap: existing token or null
        alt no existing token
            Bootstrap->>Strapi: create read-only token (name=internal)
            Strapi->>DB: insert token
            DB-->>Bootstrap: created
        else existing token present
            Bootstrap->>Strapi: compare stored vs env value (hash/check)
            alt match & read-only
                Bootstrap-->>App: done (release lock)
            else mismatch or wrong type
                Bootstrap->>Strapi: create pending token
                Strapi->>DB: insert pending
                Bootstrap->>Strapi: delete existing token
                Strapi->>DB: remove existing
                Bootstrap->>Strapi: promote pending -> internal name
                Strapi->>DB: update token record
                Bootstrap-->>App: rotated token (release lock)
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately reflects the main change: bootstrapping an internal API token and configuring environment-specific routing for Strapi.
Linked Issues check ✅ Passed All acceptance criteria from #299 are met: token creation/rotation logic, SSM integration, ECS injection, Vercel preview/prod routing, and GitHub secrets configured.
Out of Scope Changes check ✅ Passed All changes directly support the linked issue #299 objectives; no unrelated modifications detected beyond the specified scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/299-strapi-internal-api-token-routing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Mar 9, 2026

❌ Terraform plan (vercel) — Plan failed

Changes: n/a
Time:

TZ Time
UTC 2026-03-10, 9:47:55 p.m.
NZ 2026-03-11, 10:47:55 a.m.
PT 2026-03-10, 2:47:55 p.m.
ET 2026-03-10, 5:47:55 p.m.

Run: https://github.com/JesusFilm/forge/actions/runs/22925676470

data.aws_ssm_parameter.strapi_api_token_stage: Reading...
data.aws_ssm_parameter.api_token: Reading...
data.aws_ssm_parameter.strapi_api_token_prod: Reading...
data.aws_ssm_parameter.api_token: Read complete after 0s [id=/forge/vercel/api_token]
vercel_project.web: Refreshing state... [id=prj_bkpwmtIJtBAvAvDXVIS5G9fWLQh4]

Planning failed. Terraform encountered an error while generating this plan.


Error: reading SSM Parameter (/forge/aws/cms/stage/STRAPI_INTERNAL_API_TOKEN): operation error SSM: GetParameter, https response error StatusCode: 400, RequestID: 9619a558-8724-4d6a-86fe-0ab52db40074, api error AccessDeniedException: User: arn:aws:sts::031374266475:assumed-role/forge-github-actions-terraform-vercel-plan/GitHubActions is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/STRAPI_INTERNAL_API_TOKEN because no identity-based policy allows the ssm:GetParameter action

  with data.aws_ssm_parameter.strapi_api_token_stage,
  on data.tf line 7, in data "aws_ssm_parameter" "strapi_api_token_stage":
   7: data "aws_ssm_parameter" "strapi_api_token_stage" {


Error: reading SSM Parameter (/forge/aws/cms/prod/STRAPI_INTERNAL_API_TOKEN): operation error SSM: GetParameter, https response error StatusCode: 400, RequestID: 6c65d4d0-431d-4232-8ff1-1c3b6bf8f730, api error AccessDeniedException: User: arn:aws:sts::031374266475:assumed-role/forge-github-actions-terraform-vercel-plan/GitHubActions is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/STRAPI_INTERNAL_API_TOKEN because no identity-based policy allows the ssm:GetParameter action

  with data.aws_ssm_parameter.strapi_api_token_prod,
  on data.tf line 12, in data "aws_ssm_parameter" "strapi_api_token_prod":
  12: data "aws_ssm_parameter" "strapi_api_token_prod" {

::error::Terraform exited with code 1.

@github-actions
Copy link

github-actions bot commented Mar 9, 2026

❌ Terraform plan (github/prod) — Plan failed

Changes: n/a
Time:

TZ Time
UTC 2026-03-10, 9:47:52 p.m.
NZ 2026-03-11, 10:47:52 a.m.
PT 2026-03-10, 2:47:52 p.m.
ET 2026-03-10, 5:47:52 p.m.

Run: https://github.com/JesusFilm/forge/actions/runs/22925676470

data.aws_ssm_parameter.terraform_vercel_role_apply_arn: Reading...
data.aws_ssm_parameter.terraform_aws_role_apply_arn_stage: Reading...
data.aws_ssm_parameter.terraform_aws_role_plan_arn_stage: Reading...
data.aws_ssm_parameter.terraform_aws_role_plan_arn_prod: Reading...
data.aws_ssm_parameter.cms_deploy_role_arn_prod: Reading...
data.aws_ssm_parameter.cms_deploy_role_arn_stage: Reading...
data.aws_ssm_parameter.github_app_pem: Reading...
data.aws_ssm_parameter.terraform_vercel_role_plan_arn: Reading...
data.aws_ssm_parameter.terraform_aws_role_apply_arn_prod: Reading...
data.aws_ssm_parameter.terraform_github_role_apply_arn: Reading...
data.aws_ssm_parameter.cms_deploy_role_arn_stage: Read complete after 1s [id=/forge/github/cms_deploy_role_arn_stage]
data.aws_ssm_parameter.terraform_github_role_plan_arn: Reading...
data.aws_ssm_parameter.terraform_vercel_role_apply_arn: Read complete after 1s [id=/forge/github/terraform_vercel_role_apply_arn]
data.aws_ssm_parameter.terraform_aws_role_apply_arn_prod: Read complete after 1s [id=/forge/github/terraform_aws_role_apply_prod_arn]
data.aws_ssm_parameter.github_app_id: Reading...
data.aws_ssm_parameter.terraform_aws_role_plan_arn_prod: Read complete after 1s [id=/forge/github/terraform_aws_role_plan_prod_arn]
data.aws_ssm_parameter.terraform_aws_role_apply_arn_stage: Read complete after 1s [id=/forge/github/terraform_aws_role_apply_stage_arn]
data.aws_ssm_parameter.strapi_api_token_stage: Reading...
data.aws_ssm_parameter.github_installation_id: Reading...
data.aws_ssm_parameter.cms_deploy_role_arn_prod: Read complete after 1s [id=/forge/github/cms_deploy_role_arn_prod]
data.aws_ssm_parameter.terraform_github_role_apply_arn: Read complete after 1s [id=/forge/github/terraform_github_role_apply_arn]
data.aws_ssm_parameter.terraform_aws_role_plan_arn_stage: Read complete after 1s [id=/forge/github/terraform_aws_role_plan_stage_arn]
data.aws_ssm_parameter.terraform_vercel_role_plan_arn: Read complete after 1s [id=/forge/github/terraform_vercel_role_plan_arn]
data.aws_ssm_parameter.github_app_pem: Read complete after 1s [id=/forge/github/app_private_key]
data.aws_ssm_parameter.github_installation_id: Read complete after 0s [id=/forge/github/installation_id]
data.aws_ssm_parameter.terraform_github_role_plan_arn: Read complete after 0s [id=/forge/github/terraform_github_role_plan_arn]
data.aws_ssm_parameter.github_app_id: Read complete after 0s [id=/forge/github/app_id]
github_repository.forge: Refreshing state... [id=forge]
github_repository_environment.github_plan: Refreshing state... [id=forge:github-plan]
github_repository_environment.cms_stage: Refreshing state... [id=forge:cms-stage]
github_repository_environment.vercel_prod: Refreshing state... [id=forge:vercel-prod]
github_actions_variable.aws_region: Refreshing state... [id=forge:AWS_REGION]
github_repository_environment.aws_stage: Refreshing state... [id=forge:aws-stage]
github_repository_environment.aws_plan_stage: Refreshing state... [id=forge:aws-plan-stage]
github_repository_environment.vercel_plan: Refreshing state... [id=forge:vercel-plan]
github_repository_environment.github_prod: Refreshing state... [id=forge:github-prod]
github_repository_environment.aws_prod: Refreshing state... [id=forge:aws-prod]
github_branch_default.forge: Refreshing state... [id=forge]
github_repository_environment.aws_plan_prod: Refreshing state... [id=forge:aws-plan-prod]
github_repository_environment.cms_prod: Refreshing state... [id=forge:cms-prod]
github_actions_environment_secret.github_terraform_role_plan: Refreshing state... [id=forge:github-plan:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.vercel_terraform_role_apply: Refreshing state... [id=forge:vercel-prod:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.cms_deploy_role_stage: Refreshing state... [id=forge:cms-stage:CMS_DEPLOY_ROLE_ARN]
github_actions_environment_secret.aws_plan_role_stage: Refreshing state... [id=forge:aws-plan-stage:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.terraform_apply_role_stage: Refreshing state... [id=forge:aws-stage:TERRAFORM_APPLY_ROLE_ARN]
github_actions_environment_secret.vercel_terraform_role_plan: Refreshing state... [id=forge:vercel-plan:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.terraform_apply_role_prod: Refreshing state... [id=forge:aws-prod:TERRAFORM_APPLY_ROLE_ARN]
github_actions_environment_secret.github_terraform_role_apply: Refreshing state... [id=forge:github-prod:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.aws_plan_role_prod: Refreshing state... [id=forge:aws-plan-prod:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.cms_deploy_role_prod: Refreshing state... [id=forge:cms-prod:CMS_DEPLOY_ROLE_ARN]

Planning failed. Terraform encountered an error while generating this plan.


Error: reading SSM Parameter (/forge/aws/cms/stage/STRAPI_INTERNAL_API_TOKEN): operation error SSM: GetParameter, https response error StatusCode: 400, RequestID: cfcd59ee-8069-4b3b-b297-43526cb923f0, api error AccessDeniedException: User: arn:aws:sts::031374266475:assumed-role/forge-github-actions-terraform-github-plan/GitHubActions is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/STRAPI_INTERNAL_API_TOKEN because no identity-based policy allows the ssm:GetParameter action

  with data.aws_ssm_parameter.strapi_api_token_stage,
  on data.tf line 56, in data "aws_ssm_parameter" "strapi_api_token_stage":
  56: data "aws_ssm_parameter" "strapi_api_token_stage" {

::error::Terraform exited with code 1.

@github-actions
Copy link

github-actions bot commented Mar 9, 2026

🟨 Terraform plan (aws/stage) — Changes detected

Changes: +17 ~4 -1
Time:

TZ Time
UTC 2026-03-10, 9:47:56 p.m.
NZ 2026-03-11, 10:47:56 a.m.
PT 2026-03-10, 2:47:56 p.m.
ET 2026-03-10, 5:47:56 p.m.

Run: https://github.com/JesusFilm/forge/actions/runs/22925676470

module.platform.module.application.ephemeral.random_password.app_key_4: Opening...
module.platform.module.application.ephemeral.random_password.app_key_1: Opening...
module.platform.module.application.ephemeral.random_password.strapi_internal_api_token: Opening...
module.platform.module.application.ephemeral.random_password.app_key_3: Opening...
module.platform.module.application.ephemeral.random_password.encryption_key: Opening...
module.platform.module.application.ephemeral.random_password.api_token_salt: Opening...
module.platform.module.application.ephemeral.random_password.admin_jwt_secret: Opening...
module.platform.module.application.ephemeral.random_password.jwt_secret: Opening...
module.platform.module.application.ephemeral.random_password.transfer_token_salt: Opening...
module.platform.module.application.ephemeral.random_password.api_token_salt: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_4: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_3: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.encryption_key: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.admin_jwt_secret: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_2: Opening...
module.platform.module.assets.aws_acm_certificate.assets: Refreshing state... [id=arn:aws:acm:us-east-1:031374266475:certificate/78e7de72-ab8b-49f4-a40a-02bb7c0d4f4e]
module.platform.module.application.ephemeral.random_password.app_key_1: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.strapi_internal_api_token: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.jwt_secret: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.transfer_token_salt: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_2: Opening complete after 0s
data.aws_route53_zone.forge[0]: Reading...
module.platform.data.aws_availability_zones.available: Reading...
module.github.aws_iam_openid_connect_provider.github_actions: Refreshing state... [id=arn:aws:iam::031374266475:oidc-provider/token.actions.githubusercontent.com]
module.github.data.aws_iam_policy_document.github_actions_terraform_apply: Reading...
module.platform.module.assets.aws_kms_key.assets: Refreshing state... [id=ebf807b3-624c-4a0f-9e6f-4c08f0eaff3e]
module.platform.module.application.aws_kms_key.cms_ssm: Refreshing state... [id=5936d262-64d2-447d-8c57-2c8536b7c793]
module.platform.module.application.aws_iam_role.ecs_execution: Refreshing state... [id=forge-cms-stage-execution-role]
module.platform.module.assets.aws_s3_bucket.assets_access_logs: Refreshing state... [id=forge-cms-stage-assets-access-logs]
module.platform.module.application.aws_ecs_cluster.cms: Refreshing state... [id=arn:aws:ecs:us-east-2:031374266475:cluster/forge-cms-stage]
module.github.data.aws_iam_policy_document.github_actions_terraform_apply: Read complete after 0s [id=2575180025]
module.platform.module.assets.aws_cloudfront_origin_access_control.assets: Refreshing state... [id=E2LML7ZZ1C2D9R]
module.platform.aws_eip.nat: Refreshing state... [id=eipalloc-0895205d6a27b7bee]
module.platform.module.application.aws_cloudwatch_log_group.cms: Refreshing state... [id=/ecs/forge-cms-stage]
module.platform.aws_vpc.platform: Refreshing state... [id=vpc-0040ec9fef124ece4]
module.platform.data.aws_availability_zones.available: Read complete after 0s [id=us-east-2]
module.platform.module.application.aws_acm_certificate.alb: Refreshing state... [id=arn:aws:acm:us-east-2:031374266475:certificate/a62eb55a-e4cf-4375-adb1-a73b5232f28a]
module.platform.aws_s3_bucket.alb_logs: Refreshing state... [id=forge-platform-stage-alb-logs]
module.platform.data.aws_caller_identity.current: Reading...
module.platform.module.assets.aws_s3_bucket.assets: Refreshing state... [id=forge-cms-stage-assets]
module.platform.data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.github.data.aws_iam_policy.github_actions_terraform_apply[0]: Reading...
module.iam.module.groups.module.login_profile.data.aws_caller_identity.current: Reading...
data.aws_route53_zone.forge[0]: Read complete after 1s [id=Z078031724OHPW03IKVLO]
module.iam.module.groups.module.require_mfa.data.aws_caller_identity.current: Reading...
data.aws_dynamodb_table.terraform_state_lock: Reading...
module.iam.module.groups.module.login_profile.data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.platform.module.application.aws_iam_role.ecs_task: Refreshing state... [id=forge-cms-stage-task-role]
module.github.data.aws_kms_alias.terraform_state: Reading...
module.iam.module.groups.module.require_mfa.data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.platform.module.assets.data.aws_caller_identity.current: Reading...
module.platform.module.assets.data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.platform.module.application.aws_ecr_repository.cms: Refreshing state... [id=forge-cms-stage]
module.github.data.aws_kms_alias.terraform_state: Read complete after 0s [id=arn:aws:kms:us-east-2:031374266475:alias/forge-terraform-state]
data.aws_caller_identity.current: Reading...
data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.iam.module.groups.module.billing.data.aws_iam_policy_document.billing: Reading...
module.iam.module.groups.module.billing.data.aws_iam_policy_document.billing: Read complete after 0s [id=3512268210]
module.platform.aws_cloudwatch_log_group.waf: Refreshing state... [id=aws-waf-logs-forge-platform-stage]
module.github.data.aws_caller_identity.current: Reading...
module.github.data.aws_iam_policy_document.github_actions_assume_role: Reading...
module.github.data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.github.data.aws_iam_policy_document.github_actions_assume_role: Read complete after 0s [id=754675411]
module.github.data.aws_iam_policy_document.github_actions_terraform_plan_assume_role: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_apply_assume_role: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_plan_assume_role: Read complete after 0s [id=309924271]
module.github.data.aws_iam_policy_document.github_actions_terraform_apply_assume_role: Read complete after 0s [id=4015899160]
module.platform.module.assets.aws_kms_alias.assets: Refreshing state... [id=alias/forge-cms-stage-assets]
module.platform.module.application.aws_iam_role_policy_attachment.ecs_execution: Refreshing state... [id=forge-cms-stage-execution-role/arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy]
module.platform.module.application.aws_ssm_parameter.jwt_secret: Refreshing state... [id=/forge/aws/cms/stage/JWT_SECRET]
module.platform.module.application.aws_ssm_parameter.admin_jwt_secret: Refreshing state... [id=/forge/aws/cms/stage/ADMIN_JWT_SECRET]
data.aws_dynamodb_table.terraform_state_lock: Read complete after 0s [id=forge-terraform-locks]
module.platform.module.application.aws_ssm_parameter.encryption_key: Refreshing state... [id=/forge/aws/cms/stage/ENCRYPTION_KEY]
module.platform.module.application.aws_ssm_parameter.api_token_salt: Refreshing state... [id=/forge/aws/cms/stage/API_TOKEN_SALT]
module.platform.module.application.aws_kms_alias.cms_ssm: Refreshing state... [id=alias/forge-cms-stage-ssm]
module.platform.module.application.aws_ssm_parameter.transfer_token_salt: Refreshing state... [id=/forge/aws/cms/stage/TRANSFER_TOKEN_SALT]
module.platform.module.application.aws_ssm_parameter.app_keys: Refreshing state... [id=/forge/aws/cms/stage/APP_KEYS]
module.iam.module.groups.module.login_profile.data.aws_iam_policy_document.login_profile: Reading...
module.iam.module.groups.module.login_profile.data.aws_iam_policy_document.login_profile: Read complete after 0s [id=352976214]
module.iam.module.groups.module.require_mfa.data.aws_iam_policy_document.require_mfa: Reading...
module.iam.module.groups.module.require_mfa.data.aws_iam_policy_document.require_mfa: Read complete after 0s [id=3095519315]
module.github.data.aws_kms_key.terraform_state: Reading...
data.aws_s3_bucket.terraform_state: Reading...
module.github.data.aws_kms_key.terraform_state: Read complete after 0s [id=68220f21-2ca1-4846-9c2a-c8b6e268e63c]
module.github.data.aws_iam_policy_document.github_actions_cms_deploy: Reading...
module.github.data.aws_iam_policy_document.github_actions_cms_deploy: Read complete after 0s [id=4270719246]
module.github.aws_iam_role.github_actions_cms_deploy: Refreshing state... [id=forge-github-actions-cms-deploy-stage]
module.github.aws_iam_role.github_actions_terraform_plan: Refreshing state... [id=forge-github-actions-terraform-plan-stage]
module.github.aws_iam_role.github_actions_terraform_apply: Refreshing state... [id=forge-github-actions-terraform-apply-stage]
module.platform.module.assets.aws_s3_bucket_public_access_block.assets_access_logs: Refreshing state... [id=forge-cms-stage-assets-access-logs]
module.platform.module.assets.aws_s3_bucket_server_side_encryption_configuration.assets_access_logs: Refreshing state... [id=forge-cms-stage-assets-access-logs]
module.platform.aws_s3_bucket_server_side_encryption_configuration.alb_logs: Refreshing state... [id=forge-platform-stage-alb-logs]
module.platform.data.aws_iam_policy_document.alb_logs: Reading...
module.platform.data.aws_iam_policy_document.alb_logs: Read complete after 0s [id=1858962669]
module.platform.aws_s3_bucket_public_access_block.alb_logs: Refreshing state... [id=forge-platform-stage-alb-logs]
module.platform.aws_internet_gateway.platform: Refreshing state... [id=igw-0dca6115df12a416c]
module.platform.aws_security_group.alb: Refreshing state... [id=sg-0075aef920fa1e986]
module.platform.aws_security_group.ecs_service: Refreshing state... [id=sg-0c75adbe59945b2e2]
data.aws_s3_bucket.terraform_state: Read complete after 0s [id=forge-terraform-state-031374266475]
module.platform.aws_subnet.public[1]: Refreshing state... [id=subnet-0650937425d48dd8f]
module.platform.aws_subnet.public[0]: Refreshing state... [id=subnet-0ae5cc72794571e47]
module.platform.aws_subnet.private[0]: Refreshing state... [id=subnet-08cb983444108cad4]
module.github.data.aws_iam_policy.github_actions_terraform_apply[0]: Read complete after 1s [id=arn:aws:iam::031374266475:policy/forge-github-actions-terraform-apply]
module.platform.aws_subnet.private[1]: Refreshing state... [id=subnet-08acf4d6a0cf91289]
module.platform.module.assets.aws_s3_bucket_server_side_encryption_configuration.assets: Refreshing state... [id=forge-cms-stage-assets]
module.platform.module.assets.aws_s3_bucket_versioning.assets: Refreshing state... [id=forge-cms-stage-assets]
module.platform.module.assets.aws_s3_bucket_public_access_block.assets: Refreshing state... [id=forge-cms-stage-assets]
module.platform.module.assets.aws_s3_bucket_cors_configuration.assets: Refreshing state... [id=forge-cms-stage-assets]
module.platform.module.assets.aws_s3_bucket_logging.assets: Refreshing state... [id=forge-cms-stage-assets]
module.platform.module.assets.data.aws_iam_policy_document.assets_access_logs: Reading...
module.platform.aws_s3_bucket_policy.alb_logs: Refreshing state... [id=forge-platform-stage-alb-logs]
module.platform.module.assets.data.aws_iam_policy_document.assets_access_logs: Read complete after 0s [id=323064003]
module.platform.module.application.aws_lb_target_group.cms: Refreshing state... [id=arn:aws:elasticloadbalancing:us-east-2:031374266475:targetgroup/forge-cms-stage-tg/73976e5b63c4370d]
module.github.aws_iam_role_policy.github_actions_cms_deploy: Refreshing state... [id=forge-github-actions-cms-deploy-stage:cms-deploy]
module.github.aws_ssm_parameter.cms_deploy_role_arn: Refreshing state... [id=/forge/github/cms_deploy_role_arn_stage]
module.github.aws_ssm_parameter.terraform_aws_role_apply_arn: Refreshing state... [id=/forge/github/terraform_aws_role_apply_stage_arn]
module.github.aws_ssm_parameter.terraform_aws_role_plan_arn: Refreshing state... [id=/forge/github/terraform_aws_role_plan_stage_arn]
module.github.aws_iam_role_policy_attachment.github_actions_terraform_plan_readonly: Refreshing state... [id=forge-github-actions-terraform-plan-stage/arn:aws:iam::aws:policy/ReadOnlyAccess]
module.platform.aws_route_table.public: Refreshing state... [id=rtb-02f919913a845c8a3]
module.platform.aws_security_group.rds: Refreshing state... [id=sg-0224a96f3740f4537]
module.github.data.aws_iam_policy_document.github_actions_terraform_plan_ssm_kms: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_plan_ssm_kms: Read complete after 0s [id=206155691]
module.platform.module.assets.aws_s3_bucket_policy.assets_access_logs: Refreshing state... [id=forge-cms-stage-assets-access-logs]
module.platform.aws_wafv2_web_acl.alb: Refreshing state... [id=efb05d09-b544-4c38-995e-b646ec91dd29]
module.github.aws_iam_role_policy_attachment.github_actions_terraform_apply: Refreshing state... [id=forge-github-actions-terraform-apply-stage/arn:aws:iam::031374266475:policy/forge-github-actions-terraform-apply]
module.platform.module.application.aws_route53_record.alb_cert_validation["cms.stage.forge.jesusfilm.org"]: Refreshing state... [id=Z078031724OHPW03IKVLO__8c049a04c1fcb9caa395b6ff6c2322ab.cms.stage.forge.jesusfilm.org._CNAME]
module.platform.aws_nat_gateway.platform: Refreshing state... [id=nat-025e58577ccb794fa]
module.platform.aws_lb.platform: Refreshing state... [id=arn:aws:elasticloadbalancing:us-east-2:031374266475:loadbalancer/app/forge-platform-stage-alb/dc88600f279821b0]
module.platform.module.assets.aws_route53_record.assets_cert_validation["assets.stage.forge.jesusfilm.org"]: Refreshing state... [id=Z078031724OHPW03IKVLO__119fbb353ee3dad6d705782901195439.assets.stage.forge.jesusfilm.org._CNAME]
module.platform.module.application.data.aws_iam_policy_document.ecs_task: Reading...
module.platform.module.application.data.aws_iam_policy_document.ecs_task: Read complete after 0s [id=1330423022]
module.platform.module.application.aws_vpc_security_group_ingress_rule.cms_from_alb: Refreshing state... [id=sgr-028258e5908101779]
module.platform.module.application.aws_vpc_security_group_egress_rule.alb_to_cms: Refreshing state... [id=sgr-06fda286a79c919bb]
module.github.aws_iam_role_policy.github_actions_terraform_plan_ssm_kms: Refreshing state... [id=forge-github-actions-terraform-plan-stage:terraform-plan-ssm-kms]
module.platform.aws_route_table_association.public[1]: Refreshing state... [id=rtbassoc-05ab32ccd7e67f90f]
module.platform.aws_route_table_association.public[0]: Refreshing state... [id=rtbassoc-0a95ea0dfe84fc92b]
module.platform.module.application.aws_iam_role_policy.ecs_task: Refreshing state... [id=forge-cms-stage-task-role:forge-cms-stage-task-policy]
module.platform.module.application.aws_db_subnet_group.cms: Refreshing state... [id=forge-cms-stage-db-subnets]
module.platform.aws_route_table.private: Refreshing state... [id=rtb-090fd093fd497c232]
module.platform.module.application.aws_acm_certificate_validation.alb: Refreshing state... [id=2026-03-05 08:15:50.293 +0000 UTC]
module.platform.module.assets.aws_acm_certificate_validation.assets: Refreshing state... [id=2026-03-05 08:15:45.225 +0000 UTC]
module.platform.module.assets.aws_cloudfront_distribution.assets: Refreshing state... [id=EJWPGU8999NAE]
module.platform.aws_route_table_association.private[1]: Refreshing state... [id=rtbassoc-03a23da7dfede2b5c]
module.platform.aws_route_table_association.private[0]: Refreshing state... [id=rtbassoc-03f8ffd18d6d6b8e7]
module.platform.module.application.aws_route53_record.alb_alias: Refreshing state... [id=Z078031724OHPW03IKVLO_cms.stage.forge.jesusfilm.org_A]
module.platform.aws_lb_listener.http_redirect: Refreshing state... [id=arn:aws:elasticloadbalancing:us-east-2:031374266475:listener/app/forge-platform-stage-alb/dc88600f279821b0/9afbee052137b89b]
module.platform.aws_lb_listener.https: Refreshing state... [id=arn:aws:elasticloadbalancing:us-east-2:031374266475:listener/app/forge-platform-stage-alb/dc88600f279821b0/96798e67efb942a4]
module.platform.module.assets.data.aws_iam_policy_document.assets_bucket: Reading...
module.platform.module.assets.aws_route53_record.assets_alias: Refreshing state... [id=Z078031724OHPW03IKVLO_assets.stage.forge.jesusfilm.org_A]
module.platform.module.assets.data.aws_iam_policy_document.assets_bucket: Read complete after 0s [id=2742714030]
module.platform.module.assets.aws_s3_bucket_policy.assets: Refreshing state... [id=forge-cms-stage-assets]
module.platform.module.application.aws_db_instance.cms: Refreshing state... [id=db-IFW2HVA2RR22XVOXSTKMLAALH4]
module.platform.module.application.aws_lb_listener_rule.cms_host: Refreshing state... [id=arn:aws:elasticloadbalancing:us-east-2:031374266475:listener-rule/app/forge-platform-stage-alb/dc88600f279821b0/96798e67efb942a4/44dd4fd01dabc8c2]
module.platform.module.application.aws_ecs_task_definition.cms: Refreshing state... [id=forge-cms-stage-task]
module.platform.module.application.aws_iam_role_policy.ecs_execution_secrets: Refreshing state... [id=forge-cms-stage-execution-role:forge-cms-stage-execution-secrets]
module.platform.module.application.aws_ecs_service.cms: Refreshing state... [id=arn:aws:ecs:us-east-2:031374266475:service/forge-cms-stage/forge-cms-stage-service]
module.platform.module.application.ephemeral.random_password.strapi_internal_api_token: Closing...
module.platform.module.application.ephemeral.random_password.api_token_salt: Closing...
module.platform.module.application.ephemeral.random_password.jwt_secret: Closing...
module.platform.module.application.ephemeral.random_password.app_key_2: Closing...
module.platform.module.application.ephemeral.random_password.transfer_token_salt: Closing...
module.platform.module.application.ephemeral.random_password.app_key_1: Closing...
module.platform.module.application.ephemeral.random_password.encryption_key: Closing...
module.platform.module.application.ephemeral.random_password.admin_jwt_secret: Closing...
module.platform.module.application.ephemeral.random_password.admin_jwt_secret: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_1: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.jwt_secret: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_2: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.encryption_key: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.transfer_token_salt: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.api_token_salt: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.strapi_internal_api_token: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_4: Closing...
module.platform.module.application.ephemeral.random_password.app_key_4: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_3: Closing...
module.platform.module.application.ephemeral.random_password.app_key_3: Closing complete after 0s
module.platform.aws_wafv2_web_acl_association.alb: Refreshing state... [id=arn:aws:wafv2:us-east-2:031374266475:regional/webacl/forge-platform-stage-waf/efb05d09-b544-4c38-995e-b646ec91dd29,arn:aws:elasticloadbalancing:us-east-2:031374266475:loadbalancer/app/forge-platform-stage-alb/dc88600f279821b0]
module.platform.aws_wafv2_web_acl_logging_configuration.alb: Refreshing state... [id=arn:aws:wafv2:us-east-2:031374266475:regional/webacl/forge-platform-stage-waf/efb05d09-b544-4c38-995e-b646ec91dd29]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
-/+ destroy and then create replacement
 <= read (data resources)

Terraform will perform the following actions:

  # module.platform.aws_s3_bucket_lifecycle_configuration.alb_logs will be created
  + resource "aws_s3_bucket_lifecycle_configuration" "alb_logs" {
      + bucket                                 = "forge-platform-stage-alb-logs"
      + expected_bucket_owner                  = (known after apply)
      + id                                     = (known after apply)
      + region                                 = "us-east-2"
      + transition_default_minimum_object_size = "all_storage_classes_128K"

      + rule {
          + id     = "expire-log-objects"
          + status = "Enabled"
            # (1 unchanged attribute hidden)

          + expiration {
              + days                         = 90
              + expired_object_delete_marker = false
            }
        }
    }

  # module.platform.aws_s3_bucket_versioning.alb_logs will be created
  + resource "aws_s3_bucket_versioning" "alb_logs" {
      + bucket = "forge-platform-stage-alb-logs"
      + id     = (known after apply)
      + region = "us-east-2"

      + versioning_configuration {
          + mfa_delete = (known after apply)
          + status     = "Enabled"
        }
    }

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

      + statement {
          + actions   = [
              + "secretsmanager:DescribeSecret",
              + "secretsmanager:GetSecretValue",
            ]
          + effect    = "Allow"
          + resources = [
              + "arn:aws:secretsmanager:us-east-2:031374266475:secret:rds!db-d7451672-ac58-4367-b108-27a607735078-R3xVdA",
            ]
          + sid       = "ReadRdsSecretForInjection"
        }
      + statement {
          + actions   = [
              + "ssm:GetParameter",
              + "ssm:GetParameters",
            ]
          + effect    = "Allow"
          + resources = [
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/ADMIN_JWT_SECRET",
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/API_TOKEN_SALT",
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/APP_KEYS",
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/ENCRYPTION_KEY",
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/JWT_SECRET",
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/TRANSFER_TOKEN_SALT",
              + (known after apply),
            ]
          + sid       = "ReadSsmParametersForInjection"
        }
      + statement {
          + actions   = [
              + "kms:Decrypt",
            ]
          + effect    = "Allow"
          + resources = [
              + "arn:aws:kms:us-east-2:031374266475:key/5936d262-64d2-447d-8c57-2c8536b7c793",
            ]
          + sid       = "DecryptCmsSsmParameters"
        }
    }

  # module.platform.module.application.aws_db_instance.cms will be updated in-place
  ~ resource "aws_db_instance" "cms" {
      ~ backup_retention_period               = 7 -> 30
        id                                    = "db-IFW2HVA2RR22XVOXSTKMLAALH4"
        tags                                  = {
            "Environment" = "stage"
            "ManagedBy"   = "terraform"
            "Service"     = "cms"
        }
      # Warning: this attribute value will be marked as sensitive and will not
      # display in UI output after applying this change. The value is unchanged.
      ~ username                              = (sensitive value)
        # (72 unchanged attributes hidden)
    }

  # module.platform.module.application.aws_ecs_service.cms will be updated in-place
  ~ resource "aws_ecs_service" "cms" {
      ~ enable_execute_command             = false -> true
        id                                 = "arn:aws:ecs:us-east-2:031374266475:service/forge-cms-stage/forge-cms-stage-service"
        name                               = "forge-cms-stage-service"
      ~ tags                               = {
          - "Environment" = "stage" -> null
          - "ManagedBy"   = "terraform" -> null
          - "Service"     = "cms" -> null
        }
      ~ tags_all                           = {
          - "Environment" = "stage" -> null
          - "ManagedBy"   = "terraform" -> null
          - "Service"     = "cms" -> null
        }
      ~ task_definition                    = "arn:aws:ecs:us-east-2:031374266475:task-definition/forge-cms-stage-task:6" -> (known after apply)
        # (16 unchanged attributes hidden)

        # (5 unchanged blocks hidden)
    }

  # module.platform.module.application.aws_ecs_task_definition.cms must be replaced
-/+ resource "aws_ecs_task_definition" "cms" {
      ~ arn                      = "arn:aws:ecs:us-east-2:031374266475:task-definition/forge-cms-stage-task:6" -> (known after apply)
      ~ arn_without_revision     = "arn:aws:ecs:us-east-2:031374266475:task-definition/forge-cms-stage-task" -> (known after apply)
      # Warning: this attribute value will be marked as sensitive and will not
      # display in UI output after applying this change.
      ~ container_definitions    = (sensitive value) # forces replacement
      ~ enable_fault_injection   = false -> (known after apply)
      ~ id                       = "forge-cms-stage-task" -> (known after apply)
      ~ revision                 = 6 -> (known after apply)
        tags                     = {
            "Environment" = "stage"
            "ManagedBy"   = "terraform"
            "Service"     = "cms"
        }
        # (13 unchanged attributes hidden)
    }

  # module.platform.module.application.aws_iam_role_policy.ecs_execution_secrets will be updated in-place
  ~ resource "aws_iam_role_policy" "ecs_execution_secrets" {
        id          = "forge-cms-stage-execution-role:forge-cms-stage-execution-secrets"
        name        = "forge-cms-stage-execution-secrets"
      ~ policy      = jsonencode(
            {
              - Statement = [
                  - {
                      - Action   = [
                          - "secretsmanager:GetSecretValue",
                          - "secretsmanager:DescribeSecret",
                        ]
                      - Effect   = "Allow"
                      - Resource = "arn:aws:secretsmanager:us-east-2:031374266475:secret:rds!db-d7451672-ac58-4367-b108-27a607735078-R3xVdA"
                      - Sid      = "ReadRdsSecretForInjection"
                    },
                  - {
                      - Action   = [
                          - "ssm:GetParameters",
                          - "ssm:GetParameter",
                        ]
                      - Effect   = "Allow"
                      - Resource = [
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/TRANSFER_TOKEN_SALT",
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/JWT_SECRET",
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/ENCRYPTION_KEY",
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/APP_KEYS",
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/API_TOKEN_SALT",
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/ADMIN_JWT_SECRET",
                        ]
                      - Sid      = "ReadSsmParametersForInjection"
                    },
                  - {
                      - Action   = "kms:Decrypt"
                      - Effect   = "Allow"
                      - Resource = "arn:aws:kms:us-east-2:031374266475:key/5936d262-64d2-447d-8c57-2c8536b7c793"
                      - Sid      = "DecryptCmsSsmParameters"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        # (2 unchanged attributes hidden)
    }

  # module.platform.module.application.aws_iam_role_policy.ecs_task will be updated in-place
  ~ resource "aws_iam_role_policy" "ecs_task" {
        id          = "forge-cms-stage-task-role:forge-cms-stage-task-policy"
        name        = "forge-cms-stage-task-policy"
      ~ policy      = jsonencode(
          ~ {
              ~ Statement = [
                    # (2 unchanged elements hidden)
                    {
                        Action   = [
                            "kms:GenerateDataKey",
                            "kms:Decrypt",
                        ]
                        Effect   = "Allow"
                        Resource = "arn:aws:kms:us-east-2:031374266475:key/ebf807b3-624c-4a0f-9e6f-4c08f0eaff3e"
                        Sid      = "KmsAssetsKeyUse"
                    },
                  + {
                      + Action   = [
                          + "ssmmessages:OpenDataChannel",
                          + "ssmmessages:OpenControlChannel",
                          + "ssmmessages:CreateDataChannel",
                          + "ssmmessages:CreateControlChannel",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "EcsExec"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (2 unchanged attributes hidden)
    }

  # module.platform.module.application.aws_ssm_parameter.strapi_internal_api_token will be created
  + resource "aws_ssm_parameter" "strapi_internal_api_token" {
      + arn              = (known after apply)
      + data_type        = (known after apply)
      + has_value_wo     = (known after apply)
      + id               = (known after apply)
      + insecure_value   = (known after apply)
      + key_id           = "arn:aws:kms:us-east-2:031374266475:key/5936d262-64d2-447d-8c57-2c8536b7c793"
      + name             = "/forge/aws/cms/stage/STRAPI_INTERNAL_API_TOKEN"
      + region           = "us-east-2"
      + tags             = {
          + "Environment" = "stage"
          + "ManagedBy"   = "terraform"
          + "Service"     = "cms"
        }
      + tags_all         = {
          + "Environment" = "stage"
          + "ManagedBy"   = "terraform"
          + "Service"     = "cms"
        }
      + tier             = (known after apply)
      + type             = "SecureString"
      + value            = (sensitive value)
      + value_wo         = (write-only attribute)
      + value_wo_version = 1
      + version          = (known after apply)
    }

  # module.platform.module.assets.aws_s3_bucket_lifecycle_configuration.assets_access_logs will be created
  + resource "aws_s3_bucket_lifecycle_configuration" "assets_access_logs" {
      + bucket                                 = "forge-cms-stage-assets-access-logs"
      + expected_bucket_owner                  = (known after apply)
      + id                                     = (known after apply)
      + region                                 = "us-east-2"
      + transition_default_minimum_object_size = "all_storage_classes_128K"

      + rule {
          + id     = "expire-log-objects"
          + status = "Enabled"
            # (1 unchanged attribute hidden)

          + expiration {
              + days                         = 90
              + expired_object_delete_marker = false
            }
        }
    }

  # module.iam.module.groups.module.admin_readonly.aws_iam_group.admin_readonly will be created
  + resource "aws_iam_group" "admin_readonly" {
      + arn       = (known after apply)
      + id        = (known after apply)
      + name      = "forge-admin-readonly"
      + path      = "/"
      + unique_id = (known after apply)
    }

  # module.iam.module.groups.module.admin_readonly.aws_iam_group_policy_attachment.admin_readonly will be created
  + resource "aws_iam_group_policy_attachment" "admin_readonly" {
      + group      = "forge-admin-readonly"
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
    }

  # module.iam.module.groups.module.admin_readonly.aws_iam_group_policy_attachment.require_mfa will be created
  + resource "aws_iam_group_policy_attachment" "require_mfa" {
      + group      = "forge-admin-readonly"
      + id         = (known after apply)
      + policy_arn = (known after apply)
    }

  # module.iam.module.groups.module.billing.aws_iam_group.billing will be created
  + resource "aws_iam_group" "billing" {
      + arn       = (known after apply)
      + id        = (known after apply)
      + name      = "forge-billing"
      + path      = "/"
      + unique_id = (known after apply)
    }

  # module.iam.module.groups.module.billing.aws_iam_group_policy.billing will be created
  + resource "aws_iam_group_policy" "billing" {
      + group       = "forge-billing"
      + id          = (known after apply)
      + name        = "forge-billing"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "aws-portal:ViewUsage",
                          + "aws-portal:ViewPaymentMethods",
                          + "aws-portal:ViewBilling",
                          + "aws-portal:ViewAccount",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "BillingConsoleView"
                    },
                  + {
                      + Action   = [
                          + "aws-portal:ModifyPaymentMethods",
                          + "aws-portal:ModifyBilling",
                          + "aws-portal:ModifyAccount",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "BillingConsoleModify"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
    }

  # module.iam.module.groups.module.billing.aws_iam_group_policy_attachment.require_mfa will be created
  + resource "aws_iam_group_policy_attachment" "require_mfa" {
      + group      = "forge-billing"
      + id         = (known after apply)
      + policy_arn = (known after apply)
    }

  # module.iam.module.groups.module.login_profile.aws_iam_group.login_profile will be created
  + resource "aws_iam_group" "login_profile" {
      + arn       = (known after apply)
      + id        = (known after apply)
      + name      = "forge-iam-login-profile"
      + path      = "/"
      + unique_id = (known after apply)
    }

  # module.iam.module.groups.module.login_profile.aws_iam_group_policy.login_profile will be created
  + resource "aws_iam_group_policy" "login_profile" {
      + group       = "forge-iam-login-profile"
      + id          = (known after apply)
      + name        = "forge-iam-login-profile"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "iam:UpdateLoginProfile",
                          + "iam:GetLoginProfile",
                          + "iam:DeleteLoginProfile",
                          + "iam:CreateLoginProfile",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:iam::031374266475:user/*"
                      + Sid      = "LoginProfile"
                    },
                  + {
                      + Action   = [
                          + "iam:ListUsers",
                          + "iam:GetUser",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "ListUsers"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
    }

  # module.iam.module.groups.module.login_profile.aws_iam_group_policy_attachment.require_mfa will be created
  + resource "aws_iam_group_policy_attachment" "require_mfa" {
      + group      = "forge-iam-login-profile"
      + id         = (known after apply)
      + policy_arn = (known after apply)
    }

  # module.iam.module.groups.module.require_mfa.aws_iam_policy.require_mfa will be created
  + resource "aws_iam_policy" "require_mfa" {
      + arn              = (known after apply)
      + attachment_count = (known after apply)
      + description      = "Requires MFA for all actions; allows MFA setup and self-service so users can enroll on first sign-in."
      + id               = (known after apply)
      + name             = "forge-require-mfa"
      + name_prefix      = (known after apply)
      + path             = "/"
      + policy           = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "*"
                      + Condition = {
                          + BoolIfExists = {
                              + "aws:MultiFactorAuthPresent" = "false"
                            }
                        }
                      + Effect    = "Deny"
                      + Resource  = "*"
                      + Sid       = "DenyAllUnlessMFAPresent"
                    },
                  + {
                      + Action   = [
                          + "iam:ResyncMFADevice",
                          + "iam:ListVirtualMFADevices",
                          + "iam:ListMFADevices",
                          + "iam:GetUser",
                          + "iam:GetAccountPasswordPolicy",
                          + "iam:EnableMFADevice",
                          + "iam:DeleteVirtualMFADevice",
                          + "iam:CreateVirtualMFADevice",
                          + "iam:ChangePassword",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:iam::031374266475:user/${aws:username}",
                          + "arn:aws:iam::031374266475:mfa/${aws:username}",
                          + "*",
                        ]
                      + Sid      = "AllowMFASetupAndSelfService"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + policy_id        = (known after apply)
      + tags             = {
          + "ManagedBy" = "terraform"
        }
      + tags_all         = {
          + "ManagedBy" = "terraform"
        }
    }

  # module.iam.module.users.module.tataihono.data.aws_iam_group.admin_readonly will be read during apply
  # (depends on a resource or a module with changes pending)
 <= data "aws_iam_group" "admin_readonly" {
      + arn        = (known after apply)
      + group_id   = (known after apply)
      + group_name = "forge-admin-readonly"
      + id         = (known after apply)
      + path       = (known after apply)
      + users      = (known after apply)
    }

  # module.iam.module.users.module.tataihono.data.aws_iam_group.billing will be read during apply
  # (depends on a resource or a module with changes pending)
 <= data "aws_iam_group" "billing" {
      + arn        = (known after apply)
      + group_id   = (known after apply)
      + group_name = "forge-billing"
      + id         = (known after apply)
      + path       = (known after apply)
      + users      = (known after apply)
    }

  # module.iam.module.users.module.tataihono.data.aws_iam_group.login_profile will be read during apply
  # (depends on a resource or a module with changes pending)
 <= data "aws_iam_group" "login_profile" {
      + arn        = (known after apply)
      + group_id   = (known after apply)
      + group_name = "forge-iam-login-profile"
      + id         = (known after apply)
      + path       = (known after apply)
      + users      = (known after apply)
    }

  # module.iam.module.users.module.tataihono.aws_iam_user.user will be created
  + resource "aws_iam_user" "user" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "tataihono.nikora@jesusfilm.org"
      + path          = "/"
      + tags          = {
          + "ManagedBy" = "terraform"
          + "Role"      = "admin-readonly-billing"
        }
      + tags_all      = {
          + "ManagedBy" = "terraform"
          + "Role"      = "admin-readonly-billing"
        }
      + unique_id     = (known after apply)
    }

  # module.iam.module.users.module.tataihono.aws_iam_user_group_membership.admin will be created
  + resource "aws_iam_user_group_membership" "admin" {
      + groups = [
          + "forge-admin-readonly",
          + "forge-billing",
          + "forge-iam-login-profile",
        ]
      + id     = (known after apply)
      + user   = "tataihono.nikora@jesusfilm.org"
    }

Plan: 17 to add, 4 to change, 1 to destroy.

Changes to Outputs:
  - alb_domain_name                         = "cms.stage.forge.jesusfilm.org" -> null
  - assets_domain_name                      = "assets.stage.forge.jesusfilm.org" -> null
  - cloudfront_distribution_domain_name     = "d36jswd8wi2cvn.cloudfront.net" -> null
  - cms_assets_bucket_name                  = "forge-cms-stage-assets" -> null
  - cms_ssm_parameter_prefix                = "/forge/aws/cms/stage/" -> null
  - db_instance_endpoint                    = "forge-cms-stage-db.c9o8m6868p7e.us-east-2.rds.amazonaws.com" -> null
  - db_master_secret_arn                    = "arn:aws:secretsmanager:us-east-2:031374266475:secret:rds!db-d7451672-ac58-4367-b108-27a607735078-R3xVdA" -> null
  - ecr_repository_url                      = "031374266475.dkr.ecr.us-east-2.amazonaws.com/forge-cms-stage" -> null
  - ecs_cluster_name                        = "forge-cms-stage" -> null
  - forge_delegated_zone_name               = "forge.jesusfilm.org" -> null
  - forge_delegated_zone_name_servers       = [
      - "ns-696.awsdns-23.net",
      - "ns-232.awsdns-29.com",
      - "ns-2032.awsdns-62.co.uk",
      - "ns-1149.awsdns-15.org",
    ] -> null
  - github_actions_cms_deploy_role_arn      = "arn:aws:iam::031374266475:role/forge-github-actions-cms-deploy-stage" -> null
  - github_actions_terraform_apply_role_arn = "arn:aws:iam::031374266475:role/forge-github-actions-terraform-apply-stage" -> null
  - github_actions_terraform_plan_role_arn  = "arn:aws:iam::031374266475:role/forge-github-actions-terraform-plan-stage" -> null

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

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

@github-actions
Copy link

github-actions bot commented Mar 9, 2026

🟨 Terraform plan (aws/prod) — Changes detected

Changes: +15 ~6 -1
Time:

TZ Time
UTC 2026-03-10, 9:48:01 p.m.
NZ 2026-03-11, 10:48:01 a.m.
PT 2026-03-10, 2:48:01 p.m.
ET 2026-03-10, 5:48:01 p.m.

Run: https://github.com/JesusFilm/forge/actions/runs/22925676470

module.platform.module.application.ephemeral.random_password.transfer_token_salt: Opening...
module.platform.module.application.ephemeral.random_password.strapi_internal_api_token: Opening...
module.platform.module.application.ephemeral.random_password.app_key_4: Opening...
module.platform.module.application.ephemeral.random_password.encryption_key: Opening...
module.platform.module.application.ephemeral.random_password.app_key_3: Opening...
module.platform.module.application.ephemeral.random_password.app_key_1: Opening...
module.platform.module.application.ephemeral.random_password.app_key_2: Opening...
module.platform.module.application.ephemeral.random_password.api_token_salt: Opening...
module.platform.module.application.ephemeral.random_password.transfer_token_salt: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_4: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.strapi_internal_api_token: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_2: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_1: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.admin_jwt_secret: Opening...
module.platform.module.application.ephemeral.random_password.jwt_secret: Opening...
module.platform.module.application.ephemeral.random_password.api_token_salt: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_3: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.encryption_key: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.jwt_secret: Opening complete after 0s
module.platform.module.application.ephemeral.random_password.admin_jwt_secret: Opening complete after 0s
module.platform.module.assets.aws_acm_certificate.assets: Refreshing state... [id=arn:aws:acm:us-east-1:031374266475:certificate/ab733e08-7e38-4631-9576-ee73d0afee13]
module.github.data.aws_kms_alias.cms_ssm_prod[0]: Reading...
data.aws_dynamodb_table.terraform_state_lock: Reading...
module.github.data.aws_caller_identity.current: Reading...
aws_route53_zone.forge[0]: Refreshing state... [id=Z078031724OHPW03IKVLO]
module.github.aws_kms_key.github_ssm[0]: Refreshing state... [id=b3aebd98-e778-445f-8e66-f23b48dae25c]
module.vercel.aws_kms_key.vercel_ssm[0]: Refreshing state... [id=37e33c00-2088-4cb5-85a4-7136e2b364b1]
module.platform.aws_s3_bucket.alb_logs: Refreshing state... [id=forge-platform-prod-alb-logs]
module.platform.module.application.aws_cloudwatch_log_group.cms: Refreshing state... [id=/ecs/forge-cms-prod]
module.platform.module.assets.aws_s3_bucket.assets: Refreshing state... [id=forge-cms-prod-assets]
module.github.data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.platform.module.application.aws_kms_key.cms_ssm: Refreshing state... [id=ce229066-4018-4d88-8843-4aa9d08e7e47]
module.github.data.aws_kms_alias.cms_ssm_prod[0]: Read complete after 0s [id=arn:aws:kms:us-east-2:031374266475:alias/forge-cms-prod-ssm]
module.platform.data.aws_availability_zones.available: Reading...
module.github.aws_iam_openid_connect_provider.github_actions: Refreshing state... [id=arn:aws:iam::031374266475:oidc-provider/token.actions.githubusercontent.com]
module.platform.module.application.aws_iam_role.ecs_execution: Refreshing state... [id=forge-cms-prod-execution-role]
module.github.aws_ssm_parameter.github_app_id[0]: Refreshing state... [id=/forge/github/app_id]
module.platform.module.application.aws_acm_certificate.alb: Refreshing state... [id=arn:aws:acm:us-east-2:031374266475:certificate/63f93430-798e-4379-b0b5-d65255199a29]
module.platform.data.aws_availability_zones.available: Read complete after 1s [id=us-east-2]
data.aws_dynamodb_table.terraform_state_lock: Read complete after 1s [id=forge-terraform-locks]
module.platform.module.assets.aws_s3_bucket.assets_access_logs: Refreshing state... [id=forge-cms-prod-assets-access-logs]
module.iam.module.groups.module.login_profile.data.aws_caller_identity.current: Reading...
module.iam.module.groups.module.login_profile.data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.platform.module.application.aws_iam_role.ecs_task: Refreshing state... [id=forge-cms-prod-task-role]
module.iam.module.groups.module.require_mfa.data.aws_caller_identity.current: Reading...
module.github.data.aws_kms_alias.terraform_state: Reading...
module.iam.module.groups.module.require_mfa.data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
data.aws_caller_identity.current: Reading...
module.iam.module.groups.module.billing.data.aws_iam_policy_document.billing: Reading...
module.iam.module.groups.module.billing.data.aws_iam_policy_document.billing: Read complete after 0s [id=3512268210]
module.github.data.aws_kms_alias.cms_ssm_stage[0]: Reading...
data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.platform.aws_cloudwatch_log_group.waf: Refreshing state... [id=aws-waf-logs-forge-platform-prod]
module.github.data.aws_kms_alias.terraform_state: Read complete after 0s [id=arn:aws:kms:us-east-2:031374266475:alias/forge-terraform-state]
module.platform.module.assets.aws_cloudfront_origin_access_control.assets: Refreshing state... [id=E1TD5D8QKH7E2K]
module.github.data.aws_kms_alias.cms_ssm_stage[0]: Read complete after 0s [id=arn:aws:kms:us-east-2:031374266475:alias/forge-cms-stage-ssm]
module.platform.module.assets.data.aws_caller_identity.current: Reading...
module.github.aws_ssm_parameter.github_installation_id[0]: Refreshing state... [id=/forge/github/installation_id]
module.platform.module.assets.data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.github.data.aws_iam_policy_document.github_actions_terraform_apply: Reading...
module.platform.module.application.aws_ecr_repository.cms: Refreshing state... [id=forge-cms-prod]
module.github.data.aws_iam_policy_document.github_actions_terraform_apply: Read complete after 0s [id=2575180025]
module.platform.data.aws_caller_identity.current: Reading...
module.platform.data.aws_caller_identity.current: Read complete after 0s [id=031374266475]
module.platform.aws_vpc.platform: Refreshing state... [id=vpc-05ef5d31814417688]
module.platform.module.application.aws_ecs_cluster.cms: Refreshing state... [id=arn:aws:ecs:us-east-2:031374266475:cluster/forge-cms-prod]
module.platform.aws_eip.nat: Refreshing state... [id=eipalloc-0bfddde8d0da76aa6]
module.platform.module.assets.aws_kms_key.assets: Refreshing state... [id=52863204-2209-4678-9c47-151c5cdf08d6]
module.github.data.aws_iam_policy_document.github_actions_cms_deploy: Reading...
module.github.data.aws_iam_policy_document.github_actions_cms_deploy: Read complete after 0s [id=3621128515]
module.github.data.aws_kms_key.cms_ssm_prod[0]: Reading...
module.github.data.aws_kms_key.cms_ssm_prod[0]: Read complete after 0s [id=ce229066-4018-4d88-8843-4aa9d08e7e47]
module.platform.module.application.aws_ssm_parameter.encryption_key: Refreshing state... [id=/forge/aws/cms/prod/ENCRYPTION_KEY]
module.platform.module.application.aws_ssm_parameter.jwt_secret: Refreshing state... [id=/forge/aws/cms/prod/JWT_SECRET]
module.platform.module.application.aws_ssm_parameter.app_keys: Refreshing state... [id=/forge/aws/cms/prod/APP_KEYS]
module.platform.module.application.aws_ssm_parameter.api_token_salt: Refreshing state... [id=/forge/aws/cms/prod/API_TOKEN_SALT]
module.platform.module.application.aws_kms_alias.cms_ssm: Refreshing state... [id=alias/forge-cms-prod-ssm]
module.vercel.aws_ssm_parameter.api_token[0]: Refreshing state... [id=/forge/vercel/api_token]
module.platform.module.application.aws_ssm_parameter.transfer_token_salt: Refreshing state... [id=/forge/aws/cms/prod/TRANSFER_TOKEN_SALT]
module.platform.module.application.aws_ssm_parameter.admin_jwt_secret: Refreshing state... [id=/forge/aws/cms/prod/ADMIN_JWT_SECRET]
module.vercel.aws_kms_alias.vercel_ssm[0]: Refreshing state... [id=alias/forge-vercel-prod-ssm]
module.github.aws_ssm_parameter.github_app_pem[0]: Refreshing state... [id=/forge/github/app_private_key]
module.github.aws_kms_alias.github_ssm[0]: Refreshing state... [id=alias/forge-github-prod-ssm]
module.github.data.aws_iam_policy_document.github_actions_assume_role: Reading...
module.github.data.aws_iam_policy_document.github_actions_assume_role: Read complete after 0s [id=4244198687]
module.github.data.aws_iam_policy_document.github_actions_terraform_plan_assume_role: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_apply_assume_role: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_plan_assume_role: Read complete after 0s [id=1657400500]
module.github.data.aws_iam_policy_document.github_actions_terraform_apply_assume_role: Read complete after 0s [id=3016267585]
module.iam.module.groups.module.login_profile.data.aws_iam_policy_document.login_profile: Reading...
module.iam.module.groups.module.require_mfa.data.aws_iam_policy_document.require_mfa: Reading...
module.iam.module.groups.module.require_mfa.data.aws_iam_policy_document.require_mfa: Read complete after 0s [id=3095519315]
data.aws_s3_bucket.terraform_state: Reading...
module.iam.module.groups.module.login_profile.data.aws_iam_policy_document.login_profile: Read complete after 0s [id=352976214]
module.github.data.aws_kms_key.terraform_state: Reading...
module.github.data.aws_kms_key.cms_ssm_stage[0]: Reading...
module.platform.module.application.aws_iam_role_policy_attachment.ecs_execution: Refreshing state... [id=forge-cms-prod-execution-role/arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy]
module.github.aws_iam_policy.github_actions_terraform_apply[0]: Refreshing state... [id=arn:aws:iam::031374266475:policy/forge-github-actions-terraform-apply]
module.platform.aws_s3_bucket_lifecycle_configuration.alb_logs: Refreshing state... [id=forge-platform-prod-alb-logs]
module.github.data.aws_kms_key.terraform_state: Read complete after 0s [id=68220f21-2ca1-4846-9c2a-c8b6e268e63c]
module.github.data.aws_kms_key.cms_ssm_stage[0]: Read complete after 0s [id=5936d262-64d2-447d-8c57-2c8536b7c793]
module.platform.module.assets.aws_s3_bucket_public_access_block.assets: Refreshing state... [id=forge-cms-prod-assets]
module.platform.module.assets.aws_s3_bucket_versioning.assets: Refreshing state... [id=forge-cms-prod-assets]
module.platform.module.assets.aws_s3_bucket_cors_configuration.assets: Refreshing state... [id=forge-cms-prod-assets]
module.platform.aws_s3_bucket_server_side_encryption_configuration.alb_logs: Refreshing state... [id=forge-platform-prod-alb-logs]
module.platform.aws_s3_bucket_public_access_block.alb_logs: Refreshing state... [id=forge-platform-prod-alb-logs]
module.platform.aws_s3_bucket_versioning.alb_logs: Refreshing state... [id=forge-platform-prod-alb-logs]
module.platform.data.aws_iam_policy_document.alb_logs: Reading...
module.platform.data.aws_iam_policy_document.alb_logs: Read complete after 0s [id=2401369402]
module.platform.module.assets.aws_kms_alias.assets: Refreshing state... [id=alias/forge-cms-prod-assets]
module.platform.module.assets.aws_s3_bucket_server_side_encryption_configuration.assets: Refreshing state... [id=forge-cms-prod-assets]
module.platform.module.assets.aws_s3_bucket_server_side_encryption_configuration.assets_access_logs: Refreshing state... [id=forge-cms-prod-assets-access-logs]
module.platform.module.assets.aws_s3_bucket_public_access_block.assets_access_logs: Refreshing state... [id=forge-cms-prod-assets-access-logs]
data.aws_s3_bucket.terraform_state: Read complete after 0s [id=forge-terraform-state-031374266475]
module.platform.module.assets.aws_s3_bucket_logging.assets: Refreshing state... [id=forge-cms-prod-assets]
module.platform.module.assets.data.aws_iam_policy_document.assets_access_logs: Reading...
module.platform.module.assets.data.aws_iam_policy_document.assets_access_logs: Read complete after 0s [id=1618467773]
module.platform.module.assets.aws_s3_bucket_lifecycle_configuration.assets_access_logs: Refreshing state... [id=forge-cms-prod-assets-access-logs]
module.github.aws_iam_role.github_actions_cms_deploy: Refreshing state... [id=forge-github-actions-cms-deploy-prod]
module.github.aws_iam_role.github_actions_terraform_plan: Refreshing state... [id=forge-github-actions-terraform-plan-prod]
module.github.aws_iam_role.github_actions_terraform_apply: Refreshing state... [id=forge-github-actions-terraform-apply-prod]
module.iam.module.groups.module.require_mfa.aws_iam_policy.require_mfa: Refreshing state... [id=arn:aws:iam::031374266475:policy/forge-require-mfa]
module.platform.aws_s3_bucket_policy.alb_logs: Refreshing state... [id=forge-platform-prod-alb-logs]
module.platform.module.assets.aws_s3_bucket_policy.assets_access_logs: Refreshing state... [id=forge-cms-prod-assets-access-logs]
module.platform.aws_internet_gateway.platform: Refreshing state... [id=igw-0b5613d3e238c8997]
module.platform.aws_security_group.ecs_service: Refreshing state... [id=sg-060df6d60e5a60ea3]
module.github.data.aws_iam_policy_document.github_actions_terraform_plan_ssm_kms: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_plan_ssm_kms: Read complete after 0s [id=551673509]
module.platform.aws_subnet.public[0]: Refreshing state... [id=subnet-00c2e931133e9a9a3]
module.platform.aws_subnet.public[1]: Refreshing state... [id=subnet-097840dc648bae953]
module.platform.aws_subnet.private[0]: Refreshing state... [id=subnet-0f53ac64bf2e3b394]
module.platform.aws_subnet.private[1]: Refreshing state... [id=subnet-05a263f448b51eb44]
module.platform.aws_security_group.alb: Refreshing state... [id=sg-03e6315b5f8558f5b]
module.platform.module.application.data.aws_iam_policy_document.ecs_task: Reading...
module.platform.module.application.data.aws_iam_policy_document.ecs_task: Read complete after 0s [id=109275938]
module.github.data.aws_iam_policy_document.github_actions_terraform_stack_assume["github_apply"]: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_stack_assume["github_apply"]: Read complete after 0s [id=1597332144]
module.github.data.aws_iam_policy_document.github_actions_terraform_stack_assume["github_plan"]: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_stack_assume["github_plan"]: Read complete after 0s [id=2311664580]
module.github.data.aws_iam_policy_document.github_actions_terraform_stack_assume["vercel_plan"]: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_stack_assume["vercel_plan"]: Read complete after 0s [id=3112099037]
module.github.data.aws_iam_policy_document.github_actions_terraform_stack_assume["vercel_apply"]: Reading...
module.platform.module.assets.aws_route53_record.assets_cert_validation["assets.forge.jesusfilm.org"]: Refreshing state... [id=Z078031724OHPW03IKVLO__5438fc20314ef99d95985128ac6c17b1.assets.forge.jesusfilm.org._CNAME]
module.github.data.aws_iam_policy_document.github_actions_terraform_stack_assume["vercel_apply"]: Read complete after 0s [id=2944720027]
module.github.data.aws_iam_policy_document.github_actions_terraform_stack["github_apply"]: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_stack["github_apply"]: Read complete after 0s [id=3585029014]
module.github.data.aws_iam_policy_document.github_actions_terraform_stack["github_plan"]: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_stack["github_plan"]: Read complete after 0s [id=2997719824]
module.github.data.aws_iam_policy_document.github_actions_terraform_stack["vercel_apply"]: Reading...
module.github.data.aws_iam_policy_document.github_actions_terraform_stack["vercel_plan"]: Reading...
module.platform.module.application.aws_route53_record.alb_cert_validation["cms.forge.jesusfilm.org"]: Refreshing state... [id=Z078031724OHPW03IKVLO__4e8973315cfba989c3fb2410ab13b8d9.cms.forge.jesusfilm.org._CNAME]
module.github.data.aws_iam_policy_document.github_actions_terraform_stack["vercel_apply"]: Read complete after 0s [id=133335047]
module.github.data.aws_iam_policy_document.github_actions_terraform_stack["vercel_plan"]: Read complete after 0s [id=187520536]
module.github.aws_iam_role_policy_attachment.github_actions_terraform_apply: Refreshing state... [id=forge-github-actions-terraform-apply-prod/arn:aws:iam::031374266475:policy/forge-github-actions-terraform-apply]
module.github.aws_ssm_parameter.terraform_aws_role_apply_arn: Refreshing state... [id=/forge/github/terraform_aws_role_apply_prod_arn]
module.github.aws_ssm_parameter.cms_deploy_role_arn: Refreshing state... [id=/forge/github/cms_deploy_role_arn_prod]
module.github.aws_iam_role_policy.github_actions_cms_deploy: Refreshing state... [id=forge-github-actions-cms-deploy-prod:cms-deploy]
module.platform.aws_wafv2_web_acl.alb: Refreshing state... [id=5d527a2d-be37-446e-b4ed-53dd11c94e8a]
module.github.aws_ssm_parameter.terraform_aws_role_plan_arn: Refreshing state... [id=/forge/github/terraform_aws_role_plan_prod_arn]
module.github.aws_iam_role_policy_attachment.github_actions_terraform_plan_readonly: Refreshing state... [id=forge-github-actions-terraform-plan-prod/arn:aws:iam::aws:policy/ReadOnlyAccess]
module.github.aws_iam_role_policy.github_actions_terraform_plan_ssm_kms: Refreshing state... [id=forge-github-actions-terraform-plan-prod:terraform-plan-ssm-kms]
module.platform.aws_route_table.public: Refreshing state... [id=rtb-0532fcf2c00112598]
module.platform.aws_security_group.rds: Refreshing state... [id=sg-0124150b9d256b226]
module.platform.module.application.aws_lb_target_group.cms: Refreshing state... [id=arn:aws:elasticloadbalancing:us-east-2:031374266475:targetgroup/forge-cms-prod-tg/f6258b44491c5cd4]
module.platform.module.application.aws_iam_role_policy.ecs_task: Refreshing state... [id=forge-cms-prod-task-role:forge-cms-prod-task-policy]
module.platform.aws_nat_gateway.platform: Refreshing state... [id=nat-0d016d28e5aab58a2]
module.github.aws_iam_role.github_actions_terraform_stack["vercel_apply"]: Refreshing state... [id=forge-github-actions-terraform-vercel-apply]
module.github.aws_iam_role.github_actions_terraform_stack["github_apply"]: Refreshing state... [id=forge-github-actions-terraform-github-apply]
module.github.aws_iam_role.github_actions_terraform_stack["github_plan"]: Refreshing state... [id=forge-github-actions-terraform-github-plan]
module.github.aws_iam_role.github_actions_terraform_stack["vercel_plan"]: Refreshing state... [id=forge-github-actions-terraform-vercel-plan]
module.platform.aws_lb.platform: Refreshing state... [id=arn:aws:elasticloadbalancing:us-east-2:031374266475:loadbalancer/app/forge-platform-prod-alb/32e2a7795932386a]
module.platform.module.assets.aws_acm_certificate_validation.assets: Refreshing state... [id=2026-03-05 09:00:43.975 +0000 UTC]
module.platform.module.application.aws_acm_certificate_validation.alb: Refreshing state... [id=2026-03-05 09:00:41.575 +0000 UTC]
module.platform.aws_route_table_association.public[0]: Refreshing state... [id=rtbassoc-0235d4b16f74bbb86]
module.platform.aws_route_table_association.public[1]: Refreshing state... [id=rtbassoc-072c541cb48d831ae]
module.platform.aws_route_table.private: Refreshing state... [id=rtb-04444ffb223712e53]
module.platform.module.application.aws_vpc_security_group_egress_rule.alb_to_cms: Refreshing state... [id=sgr-0d333a84f55e67028]
module.platform.module.application.aws_vpc_security_group_ingress_rule.cms_from_alb: Refreshing state... [id=sgr-001c90b1521a7d334]
module.platform.module.application.aws_db_subnet_group.cms: Refreshing state... [id=forge-cms-prod-db-subnets]
module.platform.module.assets.aws_cloudfront_distribution.assets: Refreshing state... [id=E19IGKDD5ZI6JV]
module.github.aws_ssm_parameter.terraform_vercel_role_plan_arn[0]: Refreshing state... [id=/forge/github/terraform_vercel_role_plan_arn]
module.github.aws_ssm_parameter.terraform_github_role_apply_arn[0]: Refreshing state... [id=/forge/github/terraform_github_role_apply_arn]
module.github.aws_ssm_parameter.terraform_vercel_role_apply_arn[0]: Refreshing state... [id=/forge/github/terraform_vercel_role_apply_arn]
module.github.aws_iam_role_policy.github_actions_terraform_stack["github_apply"]: Refreshing state... [id=forge-github-actions-terraform-github-apply:terraform-github_apply-access]
module.github.aws_iam_role_policy.github_actions_terraform_stack["vercel_plan"]: Refreshing state... [id=forge-github-actions-terraform-vercel-plan:terraform-vercel_plan-access]
module.github.aws_iam_role_policy.github_actions_terraform_stack["vercel_apply"]: Refreshing state... [id=forge-github-actions-terraform-vercel-apply:terraform-vercel_apply-access]
module.github.aws_ssm_parameter.terraform_github_role_plan_arn[0]: Refreshing state... [id=/forge/github/terraform_github_role_plan_arn]
module.github.aws_iam_role_policy.github_actions_terraform_stack["github_plan"]: Refreshing state... [id=forge-github-actions-terraform-github-plan:terraform-github_plan-access]
module.platform.aws_lb_listener.http_redirect: Refreshing state... [id=arn:aws:elasticloadbalancing:us-east-2:031374266475:listener/app/forge-platform-prod-alb/32e2a7795932386a/097d0c48c307920f]
module.platform.aws_lb_listener.https: Refreshing state... [id=arn:aws:elasticloadbalancing:us-east-2:031374266475:listener/app/forge-platform-prod-alb/32e2a7795932386a/7cad837f4c43d1d4]
module.platform.aws_route_table_association.private[0]: Refreshing state... [id=rtbassoc-0693204bbb9be4eca]
module.platform.aws_route_table_association.private[1]: Refreshing state... [id=rtbassoc-0706f67fd1ea051f9]
module.platform.module.assets.aws_route53_record.assets_alias: Refreshing state... [id=Z078031724OHPW03IKVLO_assets.forge.jesusfilm.org_A]
module.platform.module.assets.data.aws_iam_policy_document.assets_bucket: Reading...
module.platform.module.assets.data.aws_iam_policy_document.assets_bucket: Read complete after 0s [id=2853188073]
module.platform.module.application.aws_route53_record.alb_alias: Refreshing state... [id=Z078031724OHPW03IKVLO_cms.forge.jesusfilm.org_A]
module.platform.module.application.aws_db_instance.cms: Refreshing state... [id=db-DB2WTGAYSHKO5PXR2A3GNESLSY]
module.platform.module.assets.aws_s3_bucket_policy.assets: Refreshing state... [id=forge-cms-prod-assets]
module.platform.module.application.aws_lb_listener_rule.cms_host: Refreshing state... [id=arn:aws:elasticloadbalancing:us-east-2:031374266475:listener-rule/app/forge-platform-prod-alb/32e2a7795932386a/7cad837f4c43d1d4/7a6a0551cd1a52d7]
module.platform.module.application.aws_iam_role_policy.ecs_execution_secrets: Refreshing state... [id=forge-cms-prod-execution-role:forge-cms-prod-execution-secrets]
module.platform.module.application.aws_ecs_task_definition.cms: Refreshing state... [id=forge-cms-prod-task]
module.platform.module.application.aws_ecs_service.cms: Refreshing state... [id=arn:aws:ecs:us-east-2:031374266475:service/forge-cms-prod/forge-cms-prod-service]
module.platform.module.application.ephemeral.random_password.app_key_3: Closing...
module.platform.module.application.ephemeral.random_password.transfer_token_salt: Closing...
module.platform.module.application.ephemeral.random_password.encryption_key: Closing...
module.platform.module.application.ephemeral.random_password.strapi_internal_api_token: Closing...
module.platform.module.application.ephemeral.random_password.transfer_token_salt: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.strapi_internal_api_token: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_3: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.encryption_key: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_4: Closing...
module.platform.module.application.ephemeral.random_password.app_key_4: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.admin_jwt_secret: Closing...
module.platform.module.application.ephemeral.random_password.admin_jwt_secret: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.jwt_secret: Closing...
module.platform.module.application.ephemeral.random_password.api_token_salt: Closing...
module.platform.module.application.ephemeral.random_password.api_token_salt: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.jwt_secret: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_1: Closing...
module.platform.module.application.ephemeral.random_password.app_key_1: Closing complete after 0s
module.platform.module.application.ephemeral.random_password.app_key_2: Closing...
module.platform.module.application.ephemeral.random_password.app_key_2: Closing complete after 0s
module.platform.aws_wafv2_web_acl_association.alb: Refreshing state... [id=arn:aws:wafv2:us-east-2:031374266475:regional/webacl/forge-platform-prod-waf/5d527a2d-be37-446e-b4ed-53dd11c94e8a,arn:aws:elasticloadbalancing:us-east-2:031374266475:loadbalancer/app/forge-platform-prod-alb/32e2a7795932386a]
module.platform.aws_wafv2_web_acl_logging_configuration.alb: Refreshing state... [id=arn:aws:wafv2:us-east-2:031374266475:regional/webacl/forge-platform-prod-waf/5d527a2d-be37-446e-b4ed-53dd11c94e8a]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
-/+ destroy and then create replacement
 <= read (data resources)

Terraform will perform the following actions:

  # module.github.aws_iam_role_policy.github_actions_terraform_stack["github_apply"] will be updated in-place
  ~ resource "aws_iam_role_policy" "github_actions_terraform_stack" {
        id          = "forge-github-actions-terraform-github-apply:terraform-github_apply-access"
        name        = "terraform-github_apply-access"
      ~ policy      = jsonencode(
          ~ {
              ~ Statement = [
                    # (3 unchanged elements hidden)
                    {
                        Action   = [
                            "kms:GenerateDataKey",
                            "kms:Decrypt",
                        ]
                        Effect   = "Allow"
                        Resource = "arn:aws:kms:us-east-2:031374266475:key/68220f21-2ca1-4846-9c2a-c8b6e268e63c"
                        Sid      = "StateKmsAccess"
                    },
                  ~ {
                      ~ Resource = "arn:aws:ssm:us-east-2:031374266475:parameter/forge/github/*" -> [
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/github/*",
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/STRAPI_INTERNAL_API_TOKEN",
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/STRAPI_INTERNAL_API_TOKEN",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = "arn:aws:kms:us-east-2:031374266475:key/b3aebd98-e778-445f-8e66-f23b48dae25c" -> [
                          + "arn:aws:kms:us-east-2:031374266475:key/ce229066-4018-4d88-8843-4aa9d08e7e47",
                          + "arn:aws:kms:us-east-2:031374266475:key/b3aebd98-e778-445f-8e66-f23b48dae25c",
                          + "arn:aws:kms:us-east-2:031374266475:key/5936d262-64d2-447d-8c57-2c8536b7c793",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (2 unchanged attributes hidden)
    }

  # module.github.aws_iam_role_policy.github_actions_terraform_stack["github_plan"] will be updated in-place
  ~ resource "aws_iam_role_policy" "github_actions_terraform_stack" {
        id          = "forge-github-actions-terraform-github-plan:terraform-github_plan-access"
        name        = "terraform-github_plan-access"
      ~ policy      = jsonencode(
          ~ {
              ~ Statement = [
                    # (3 unchanged elements hidden)
                    {
                        Action   = "kms:Decrypt"
                        Effect   = "Allow"
                        Resource = "arn:aws:kms:us-east-2:031374266475:key/68220f21-2ca1-4846-9c2a-c8b6e268e63c"
                        Sid      = "StateKmsAccess"
                    },
                  ~ {
                      ~ Resource = "arn:aws:ssm:us-east-2:031374266475:parameter/forge/github/*" -> [
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/github/*",
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/STRAPI_INTERNAL_API_TOKEN",
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/STRAPI_INTERNAL_API_TOKEN",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = "arn:aws:kms:us-east-2:031374266475:key/b3aebd98-e778-445f-8e66-f23b48dae25c" -> [
                          + "arn:aws:kms:us-east-2:031374266475:key/ce229066-4018-4d88-8843-4aa9d08e7e47",
                          + "arn:aws:kms:us-east-2:031374266475:key/b3aebd98-e778-445f-8e66-f23b48dae25c",
                          + "arn:aws:kms:us-east-2:031374266475:key/5936d262-64d2-447d-8c57-2c8536b7c793",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (2 unchanged attributes hidden)
    }

  # module.github.aws_iam_role_policy.github_actions_terraform_stack["vercel_apply"] will be updated in-place
  ~ resource "aws_iam_role_policy" "github_actions_terraform_stack" {
        id          = "forge-github-actions-terraform-vercel-apply:terraform-vercel_apply-access"
        name        = "terraform-vercel_apply-access"
      ~ policy      = jsonencode(
          ~ {
              ~ Statement = [
                    # (3 unchanged elements hidden)
                    {
                        Action   = [
                            "kms:GenerateDataKey",
                            "kms:Decrypt",
                        ]
                        Effect   = "Allow"
                        Resource = "arn:aws:kms:us-east-2:031374266475:key/68220f21-2ca1-4846-9c2a-c8b6e268e63c"
                        Sid      = "StateKmsAccess"
                    },
                  ~ {
                      ~ Resource = "arn:aws:ssm:us-east-2:031374266475:parameter/forge/vercel/*" -> [
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/vercel/api_token",
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/STRAPI_INTERNAL_API_TOKEN",
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/STRAPI_INTERNAL_API_TOKEN",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = "arn:aws:kms:us-east-2:031374266475:key/37e33c00-2088-4cb5-85a4-7136e2b364b1" -> [
                          + "arn:aws:kms:us-east-2:031374266475:key/ce229066-4018-4d88-8843-4aa9d08e7e47",
                          + "arn:aws:kms:us-east-2:031374266475:key/5936d262-64d2-447d-8c57-2c8536b7c793",
                          + "arn:aws:kms:us-east-2:031374266475:key/37e33c00-2088-4cb5-85a4-7136e2b364b1",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (2 unchanged attributes hidden)
    }

  # module.github.aws_iam_role_policy.github_actions_terraform_stack["vercel_plan"] will be updated in-place
  ~ resource "aws_iam_role_policy" "github_actions_terraform_stack" {
        id          = "forge-github-actions-terraform-vercel-plan:terraform-vercel_plan-access"
        name        = "terraform-vercel_plan-access"
      ~ policy      = jsonencode(
          ~ {
              ~ Statement = [
                    # (3 unchanged elements hidden)
                    {
                        Action   = "kms:Decrypt"
                        Effect   = "Allow"
                        Resource = "arn:aws:kms:us-east-2:031374266475:key/68220f21-2ca1-4846-9c2a-c8b6e268e63c"
                        Sid      = "StateKmsAccess"
                    },
                  ~ {
                      ~ Resource = "arn:aws:ssm:us-east-2:031374266475:parameter/forge/vercel/*" -> [
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/vercel/api_token",
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/stage/STRAPI_INTERNAL_API_TOKEN",
                          + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/STRAPI_INTERNAL_API_TOKEN",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = "arn:aws:kms:us-east-2:031374266475:key/37e33c00-2088-4cb5-85a4-7136e2b364b1" -> [
                          + "arn:aws:kms:us-east-2:031374266475:key/ce229066-4018-4d88-8843-4aa9d08e7e47",
                          + "arn:aws:kms:us-east-2:031374266475:key/5936d262-64d2-447d-8c57-2c8536b7c793",
                          + "arn:aws:kms:us-east-2:031374266475:key/37e33c00-2088-4cb5-85a4-7136e2b364b1",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (2 unchanged attributes hidden)
    }

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

      + statement {
          + actions   = [
              + "secretsmanager:DescribeSecret",
              + "secretsmanager:GetSecretValue",
            ]
          + effect    = "Allow"
          + resources = [
              + "arn:aws:secretsmanager:us-east-2:031374266475:secret:rds!db-4782a6e9-1f78-4cc7-89e3-7f1d909e53b0-UedNB8",
            ]
          + sid       = "ReadRdsSecretForInjection"
        }
      + statement {
          + actions   = [
              + "ssm:GetParameter",
              + "ssm:GetParameters",
            ]
          + effect    = "Allow"
          + resources = [
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/ADMIN_JWT_SECRET",
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/API_TOKEN_SALT",
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/APP_KEYS",
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/ENCRYPTION_KEY",
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/JWT_SECRET",
              + "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/TRANSFER_TOKEN_SALT",
              + (known after apply),
            ]
          + sid       = "ReadSsmParametersForInjection"
        }
      + statement {
          + actions   = [
              + "kms:Decrypt",
            ]
          + effect    = "Allow"
          + resources = [
              + "arn:aws:kms:us-east-2:031374266475:key/ce229066-4018-4d88-8843-4aa9d08e7e47",
            ]
          + sid       = "DecryptCmsSsmParameters"
        }
    }

  # module.platform.module.application.aws_appautoscaling_policy.cms_cpu[0] will be created
  + resource "aws_appautoscaling_policy" "cms_cpu" {
      + alarm_arns         = (known after apply)
      + arn                = (known after apply)
      + id                 = (known after apply)
      + name               = "forge-cms-prod-cpu"
      + policy_type        = "TargetTrackingScaling"
      + region             = "us-east-2"
      + resource_id        = "service/forge-cms-prod/forge-cms-prod-service"
      + scalable_dimension = "ecs:service:DesiredCount"
      + service_namespace  = "ecs"

      + target_tracking_scaling_policy_configuration {
          + disable_scale_in   = false
          + scale_in_cooldown  = 300
          + scale_out_cooldown = 60
          + target_value       = 70

          + predefined_metric_specification {
              + predefined_metric_type = "ECSServiceAverageCPUUtilization"
            }
        }
    }

  # module.platform.module.application.aws_appautoscaling_target.cms[0] will be created
  + resource "aws_appautoscaling_target" "cms" {
      + arn                = (known after apply)
      + id                 = (known after apply)
      + max_capacity       = 3
      + min_capacity       = 1
      + region             = "us-east-2"
      + resource_id        = "service/forge-cms-prod/forge-cms-prod-service"
      + role_arn           = (known after apply)
      + scalable_dimension = "ecs:service:DesiredCount"
      + service_namespace  = "ecs"
      + tags_all           = (known after apply)

      + suspended_state (known after apply)
    }

  # module.platform.module.application.aws_ecs_service.cms will be updated in-place
  ~ resource "aws_ecs_service" "cms" {
        id                                 = "arn:aws:ecs:us-east-2:031374266475:service/forge-cms-prod/forge-cms-prod-service"
        name                               = "forge-cms-prod-service"
        tags                               = {}
      ~ task_definition                    = "arn:aws:ecs:us-east-2:031374266475:task-definition/forge-cms-prod-task:12" -> (known after apply)
        # (18 unchanged attributes hidden)

        # (5 unchanged blocks hidden)
    }

  # module.platform.module.application.aws_ecs_task_definition.cms must be replaced
-/+ resource "aws_ecs_task_definition" "cms" {
      ~ arn                      = "arn:aws:ecs:us-east-2:031374266475:task-definition/forge-cms-prod-task:12" -> (known after apply)
      ~ arn_without_revision     = "arn:aws:ecs:us-east-2:031374266475:task-definition/forge-cms-prod-task" -> (known after apply)
      ~ container_definitions    = (sensitive value) # forces replacement
      ~ enable_fault_injection   = false -> (known after apply)
      ~ id                       = "forge-cms-prod-task" -> (known after apply)
      ~ revision                 = 12 -> (known after apply)
        tags                     = {
            "Environment" = "prod"
            "ManagedBy"   = "terraform"
            "Service"     = "cms"
        }
        # (13 unchanged attributes hidden)
    }

  # module.platform.module.application.aws_iam_role_policy.ecs_execution_secrets will be updated in-place
  ~ resource "aws_iam_role_policy" "ecs_execution_secrets" {
        id          = "forge-cms-prod-execution-role:forge-cms-prod-execution-secrets"
        name        = "forge-cms-prod-execution-secrets"
      ~ policy      = jsonencode(
            {
              - Statement = [
                  - {
                      - Action   = [
                          - "secretsmanager:GetSecretValue",
                          - "secretsmanager:DescribeSecret",
                        ]
                      - Effect   = "Allow"
                      - Resource = "arn:aws:secretsmanager:us-east-2:031374266475:secret:rds!db-4782a6e9-1f78-4cc7-89e3-7f1d909e53b0-UedNB8"
                      - Sid      = "ReadRdsSecretForInjection"
                    },
                  - {
                      - Action   = [
                          - "ssm:GetParameters",
                          - "ssm:GetParameter",
                        ]
                      - Effect   = "Allow"
                      - Resource = [
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/TRANSFER_TOKEN_SALT",
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/JWT_SECRET",
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/ENCRYPTION_KEY",
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/APP_KEYS",
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/API_TOKEN_SALT",
                          - "arn:aws:ssm:us-east-2:031374266475:parameter/forge/aws/cms/prod/ADMIN_JWT_SECRET",
                        ]
                      - Sid      = "ReadSsmParametersForInjection"
                    },
                  - {
                      - Action   = "kms:Decrypt"
                      - Effect   = "Allow"
                      - Resource = "arn:aws:kms:us-east-2:031374266475:key/ce229066-4018-4d88-8843-4aa9d08e7e47"
                      - Sid      = "DecryptCmsSsmParameters"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        # (2 unchanged attributes hidden)
    }

  # module.platform.module.application.aws_ssm_parameter.strapi_internal_api_token will be created
  + resource "aws_ssm_parameter" "strapi_internal_api_token" {
      + arn              = (known after apply)
      + data_type        = (known after apply)
      + has_value_wo     = (known after apply)
      + id               = (known after apply)
      + insecure_value   = (known after apply)
      + key_id           = "arn:aws:kms:us-east-2:031374266475:key/ce229066-4018-4d88-8843-4aa9d08e7e47"
      + name             = "/forge/aws/cms/prod/STRAPI_INTERNAL_API_TOKEN"
      + region           = "us-east-2"
      + tags             = {
          + "Environment" = "prod"
          + "ManagedBy"   = "terraform"
          + "Service"     = "cms"
        }
      + tags_all         = {
          + "Environment" = "prod"
          + "ManagedBy"   = "terraform"
          + "Service"     = "cms"
        }
      + tier             = (known after apply)
      + type             = "SecureString"
      + value            = (sensitive value)
      + value_wo         = (write-only attribute)
      + value_wo_version = 1
      + version          = (known after apply)
    }

  # module.iam.module.groups.module.admin_readonly.aws_iam_group.admin_readonly will be created
  + resource "aws_iam_group" "admin_readonly" {
      + arn       = (known after apply)
      + id        = (known after apply)
      + name      = "forge-admin-readonly"
      + path      = "/"
      + unique_id = (known after apply)
    }

  # module.iam.module.groups.module.admin_readonly.aws_iam_group_policy_attachment.admin_readonly will be created
  + resource "aws_iam_group_policy_attachment" "admin_readonly" {
      + group      = "forge-admin-readonly"
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
    }

  # module.iam.module.groups.module.admin_readonly.aws_iam_group_policy_attachment.require_mfa will be created
  + resource "aws_iam_group_policy_attachment" "require_mfa" {
      + group      = "forge-admin-readonly"
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::031374266475:policy/forge-require-mfa"
    }

  # module.iam.module.groups.module.billing.aws_iam_group.billing will be created
  + resource "aws_iam_group" "billing" {
      + arn       = (known after apply)
      + id        = (known after apply)
      + name      = "forge-billing"
      + path      = "/"
      + unique_id = (known after apply)
    }

  # module.iam.module.groups.module.billing.aws_iam_group_policy.billing will be created
  + resource "aws_iam_group_policy" "billing" {
      + group       = "forge-billing"
      + id          = (known after apply)
      + name        = "forge-billing"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "aws-portal:ViewUsage",
                          + "aws-portal:ViewPaymentMethods",
                          + "aws-portal:ViewBilling",
                          + "aws-portal:ViewAccount",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "Bi

... (truncated)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
apps/cms/src/index.ts (2)

84-95: Add logging when rotating an existing token for auditability.

The rotation case (token exists but doesn't match or isn't read-only) silently deletes and recreates the token. Adding a log here would improve operational visibility and align with the guideline to keep transitions auditable.

📝 Suggested logging addition
     const isReadOnly = existingToken.type === "read-only"
     if (matches && isReadOnly) return
 
+    strapi.log.info(
+      `Rotating internal API token: match=${matches}, readOnly=${isReadOnly}`
+    )
     await strapi.db.query("admin::api-token").delete({
       where: { id: existingToken.id },
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cms/src/index.ts` around lines 84 - 95, Add an info log when an existing
API token is being rotated: inside the branch after determining matches and
isReadOnly (just before deleting and recreating), emit a concise audit log via
the application's logger (e.g., strapi.log.info) mentioning the
existingToken.id, existingToken.type and the accessKey (or masked form) to
record the rotation action, then proceed to call
strapi.db.query("admin::api-token").delete(...) and createReadOnlyToken(strapi,
apiTokenService, accessKey).

7-17: Consider adding the getBy method to the type definition.

The code queries tokens via strapi.db.query() at line 74 rather than using a potential service method. If the api-token service has a getBy or findOne method, using it would be more consistent with the service abstraction pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cms/src/index.ts` around lines 7 - 17, The ApiTokenService type is
missing a method used elsewhere; add a getBy (or findOne) method signature to
the ApiTokenService type so callers can use the service instead of
strapi.db.query; e.g., extend the type with getBy: (params: { where?:
Record<string, any>; populate?: string[] }) => Promise<unknown> (or a more
specific token shape), or add findOne: (where: Record<string, any>) =>
Promise<unknown>, keeping the same return type conventions as create/check/hash
so callers referencing ApiTokenService (and code that uses strapi.db.query) can
switch to service.getBy/getOne.
infra/aws/vercel/ssm.tf (1)

46-70: Consider adding tags for resource management consistency.

The KMS key includes tags (lines 17-19) but the SSM parameters don't. Adding tags = local.vercel_ssm_tags to SSM parameters would improve resource discoverability and cost tracking. This matches the existing api_token resource pattern, so it's optional.

📋 Suggested tags addition
 resource "aws_ssm_parameter" "strapi_api_token_stage" {
   count = local.create_vercel_ssm_resources ? 1 : 0

   name   = "/forge/vercel/strapi_api_token_stage"
   type   = "SecureString"
   key_id = aws_kms_key.vercel_ssm[0].arn
   value  = "manually set in AWS console"
+  tags   = local.vercel_ssm_tags

   lifecycle {
     ignore_changes = [value]
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/aws/vercel/ssm.tf` around lines 46 - 70, Add consistent tagging to the
two SSM parameter resources by adding tags = local.vercel_ssm_tags to
aws_ssm_parameter.strapi_api_token_stage and
aws_ssm_parameter.strapi_api_token_prod so they match the KMS key and the
existing api_token resource pattern; place the tags attribute alongside the
other top-level attributes (name/type/key_id/value) in each resource block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/cms/src/index.ts`:
- Around line 92-95: The current non-atomic rotation deletes the existing
managed token via strapi.db.query("admin::api-token").delete(...) before calling
createReadOnlyToken(strapi, apiTokenService, accessKey), which risks losing all
tokens if creation fails; change the flow to create-and-verify the new token
first (e.g., call createReadOnlyToken with a temporary name/marker and confirm
it exists), only then delete the old token, or wrap createReadOnlyToken in a
try/catch that on failure logs prominently and restores the old token if
deleted; reference the delete call and createReadOnlyToken when implementing the
create-then-delete or rollback logic to ensure token rotation is
atomic/defensive.

In `@infra/github/data.tf`:
- Around line 55-59: The Terraform data source data "aws_ssm_parameter"
"strapi_api_token_stage" fails during plan because the SSM parameter it reads is
created conditionally by the SSM resource in the github module (created only
when environment == "prod"); either make the lookup conditional (wrap data
"aws_ssm_parameter" "strapi_api_token_stage" with the same environment == "prod"
guard or use count/for_each to only reference it in prod) or ensure the
infra/aws/github SSM resource is applied before running terraform plan for
infra/github (and document this ordering in the README or CI workflow); update
the CI workflow to apply the github SSM module first or add documentation
explaining the dependency.

---

Nitpick comments:
In `@apps/cms/src/index.ts`:
- Around line 84-95: Add an info log when an existing API token is being
rotated: inside the branch after determining matches and isReadOnly (just before
deleting and recreating), emit a concise audit log via the application's logger
(e.g., strapi.log.info) mentioning the existingToken.id, existingToken.type and
the accessKey (or masked form) to record the rotation action, then proceed to
call strapi.db.query("admin::api-token").delete(...) and
createReadOnlyToken(strapi, apiTokenService, accessKey).
- Around line 7-17: The ApiTokenService type is missing a method used elsewhere;
add a getBy (or findOne) method signature to the ApiTokenService type so callers
can use the service instead of strapi.db.query; e.g., extend the type with
getBy: (params: { where?: Record<string, any>; populate?: string[] }) =>
Promise<unknown> (or a more specific token shape), or add findOne: (where:
Record<string, any>) => Promise<unknown>, keeping the same return type
conventions as create/check/hash so callers referencing ApiTokenService (and
code that uses strapi.db.query) can switch to service.getBy/getOne.

In `@infra/aws/vercel/ssm.tf`:
- Around line 46-70: Add consistent tagging to the two SSM parameter resources
by adding tags = local.vercel_ssm_tags to
aws_ssm_parameter.strapi_api_token_stage and
aws_ssm_parameter.strapi_api_token_prod so they match the KMS key and the
existing api_token resource pattern; place the tags attribute alongside the
other top-level attributes (name/type/key_id/value) in each resource block.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ddbab034-3229-40af-b0aa-da2273f25780

📥 Commits

Reviewing files that changed from the base of the PR and between 604f01d and 50267bc.

📒 Files selected for processing (11)
  • apps/cms/.env.example
  • apps/cms/src/index.ts
  • infra/aws/github/ssm.tf
  • infra/aws/modules/cms/main.tf
  • infra/aws/vercel/ssm.tf
  • infra/github/README.md
  • infra/github/actions.tf
  • infra/github/data.tf
  • infra/vercel/README.md
  • infra/vercel/data.tf
  • infra/vercel/main.tf

Generate STRAPI internal tokens in the CMS module as random
SecureStrings controlled by ssm_secret_version for rotation.
Point Vercel and GitHub token reads to those CMS parameters
and remove duplicate token params from vercel/github namespaces.

Made-with: Cursor
Move internal API token logic behind a single bootstrap call and
implement create-verify-promote rotation to avoid delete-first risk.
This keeps startup entrypoint small while preserving audit logs.

Made-with: Cursor
Allow github plan/apply stack roles to read and decrypt both stage
and prod CMS token parameters, and document cms-parameter dependency
for github/vercel plans.

Made-with: Cursor
@tataihono
Copy link
Contributor Author

Review feedback addressed (c25dbe0)

Handled:

  • generate automatically from random: token now generated by ephemeral.random_password + value_wo_version in CMS SSM.
  • non-atomic rotation risk: moved logic to ensureInternalApiToken() with create-verify-promote flow.
  • terraform plan missing token parameter: token source now CMS SSM path + README dependency notes for stage/prod infra apply.
  • bootstrap file bloat: bootstrap now delegates to one helper call.
  • github needs prod token access (plan): added prod CMS token/kms access.
  • github needs prod token access (apply): added prod CMS token/kms access.

Declined:

  • switch vercel SSM scope to /forge/vercel/* (plan role): kept explicit ARNs for least privilege and clear source-of-truth paths.
  • switch vercel SSM scope to /forge/vercel/* (apply role): kept explicit ARNs for least privilege and clear source-of-truth paths.

Questions / blocked:

  • none

Guard section keys using an id-property check and relax exhaustive
section rendering fallback so generated union additions do not break
Next.js production build typechecking.

Made-with: Cursor
@tataihono
Copy link
Contributor Author

Review feedback addressed (bbcbefa)

Handled:

  • [generate automatically from random]: CMS token now uses random generation + ssm_secret_version rotation.
  • [non-atomic token rotation]: moved into ensureInternalApiToken() with create-verify-promote swap flow.
  • [terraform plan missing token parameter]: switched to CMS SSM paths and documented stage/prod infra prerequisite.
  • [bootstrap should be simple]: bootstrap now delegates to one helper call.
  • [github needs prod token access]: added prod token/KMS access for github plan+apply role scopes.

Declined:

  • [use /forge/vercel/* wildcard in stack role scopes]: kept explicit ARNs for least-privilege and clearer source-of-truth paths.

Questions / blocked:

  • none

Use `Core.Strapi` for the bootstrap argument type to keep the entrypoint
signature aligned with the Strapi app contract.

Made-with: Cursor
@tataihono
Copy link
Contributor Author

Review feedback addressed (5fbf06c)

Handled:

  • [Serialize token bootstrap across CMS instances]: Added withTokenBootstrapLock using Postgres advisory lock so token create/rotate executes serially across concurrent startups.
  • [drop] (apps/cms/src/internal-api-token.ts line 25): Dropped extra helper-type verbosity while keeping strict Core.Strapi entrypoint.
  • [drop] (apps/cms/src/internal-api-token.ts line 85): Dropped redundant type indirection at service narrowing callsite.

Declined:

  • [Invalid value_wo/value_wo_version fields]: Current Terraform AWS provider in this repo supports these write-only fields; local terraform validate passes.
  • [Make prod token lookup conditional in infra/vercel]: Stack intentionally manages preview+prod env vars together; full-state plan needs both token reads.

Questions / blocked:

  • None.

Document the Strapi internal typing rule for future agents and add
an inline note at the local cast site in token bootstrap logic.

Made-with: Cursor
@tataihono
Copy link
Contributor Author

Review feedback addressed (b09c49b)

Fixed:

  • [Don't add this type just rely on Core.Strapi]: Confirmed handled. StrapiLike was removed and helper uses Core.Strapi with only minimal local narrow casts where Strapi internals are not publicly typed.

Not changed:

  • None.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/cms/src/internal-api-token.ts (1)

19-26: Avoid reading DATABASE_CLIENT directly in apps/cms runtime code.

This hard-codes env-specific branching into app logic and duplicates the database-client resolution that already lives in Strapi config. Prefer deriving capability/client information from the resolved Strapi DB object instead of process.env here.

As per coding guidelines "Do not embed env-specific branching in app logic."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cms/src/internal-api-token.ts` around lines 19 - 26, Don't read
process.env.DATABASE_CLIENT in apps/cms; instead derive DB type/capabilities
from the resolved Strapi DB object. Remove the isPostgres check and rely on the
inspected strapi.db object (use dbWithRaw, dbWithRaw.connection?.raw and/or the
DB client's config/dialect on strapi.db or strapi.db.connection) to decide
behavior—i.e., check for the presence of a raw function or the client's
dialect/config field rather than using process.env.DATABASE_CLIENT. Update the
conditional around raw and any code paths that depended on isPostgres to use
that derived capability from strapi.db.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/cms/src/internal-api-token.ts`:
- Around line 15-36: The advisory lock calls in withTokenBootstrapLock use
raw(...) twice which may pull different pooled connections so
pg_advisory_lock/pg_advisory_unlock can run on different sessions; modify
withTokenBootstrapLock to acquire a single connection/session for the lifetime
of the lock (either by wrapping the lock, run(), and unlock calls inside a
single Knex transaction or by manually acquiring a connection from the Knex pool
and releasing it after unlock) so both SELECT pg_advisory_lock(?) and SELECT
pg_advisory_unlock(?) run on the same session; ensure you still call the
provided run() callback inside that transaction/connection and release/commit in
a finally block, referencing TOKEN_BOOTSTRAP_LOCK_ID and the existing raw
function usage in withTokenBootstrapLock.

---

Nitpick comments:
In `@apps/cms/src/internal-api-token.ts`:
- Around line 19-26: Don't read process.env.DATABASE_CLIENT in apps/cms; instead
derive DB type/capabilities from the resolved Strapi DB object. Remove the
isPostgres check and rely on the inspected strapi.db object (use dbWithRaw,
dbWithRaw.connection?.raw and/or the DB client's config/dialect on strapi.db or
strapi.db.connection) to decide behavior—i.e., check for the presence of a raw
function or the client's dialect/config field rather than using
process.env.DATABASE_CLIENT. Update the conditional around raw and any code
paths that depended on isPostgres to use that derived capability from strapi.db.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3d7f5669-aad4-47b5-9f9c-9685dbf534da

📥 Commits

Reviewing files that changed from the base of the PR and between 5bf68bc and b09c49b.

📒 Files selected for processing (2)
  • apps/cms/AGENTS.md
  • apps/cms/src/internal-api-token.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/cms/AGENTS.md

Run advisory lock and unlock on the same acquired Postgres session
so concurrent CMS startups cannot bypass the token bootstrap mutex.

Made-with: Cursor
@tataihono
Copy link
Contributor Author

Review feedback addressed (aadbb82)

Fixed:

  • [Keep advisory lock on same PostgreSQL session]: Updated withTokenBootstrapLock to acquire a single pooled DB session and run lock/callback/unlock on that same session, then release in finally.

Not changed:

  • None.

@tataihono tataihono merged commit c71ea24 into main Mar 10, 2026
24 of 31 checks passed
@tataihono tataihono deleted the fix/299-strapi-internal-api-token-routing branch March 10, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cms): bootstrap internal api token and stage/prod routing

1 participant