shoc-frontend-new/terraform/README.md

16 KiB

Frontend Terraform adoption runbook

This tree adopts the existing Sea Haven SHOC frontend hosting resources without recreating them. It implements the local configuration and plan-safety tooling only. Creating these files, formatting them, initializing with -backend=false, and validating them does not authorize an AWS, HCP Terraform, GitHub, CloudFormation, DNS, or deployment mutation.

The rollout order is tf-poc, dev, then staging. Production and tf-poc teardown are separate follow-up changes.

Fixed targets

  • AWS account: 396287094661
  • AWS region: us-east-1
  • HCP organization: seahaven
  • HCP project: seahaven-external-dev
  • Workspaces:
    • shoc-frontend-new-tf-poc
    • shoc-frontend-new-dev
    • shoc-frontend-new-staging
  • HCP auto-apply: off for all three workspaces
  • tf-poc site: frontend-tf-poc.seahaven.com
  • tf-poc API build value: https://api.tf-poc.seahaven.com/api
  • tf-poc bucket: seahaven-shoc-frontend-tf-poc
  • tf-poc deploy role: githubdeploy-shoc-frontend-new-tf-poc
  • tf-poc GitHub environment: tf-poc

The cloud blocks identify the organization, project, and workspace. Auto-apply is an HCP workspace setting and must be verified operationally before connecting VCS or starting a run.

Ownership boundary

live/modules/environment-owned owns exactly these 13 addresses:

  1. module.environment_owned.aws_s3_bucket.site
  2. module.environment_owned.aws_s3_bucket_public_access_block.site
  3. module.environment_owned.aws_s3_bucket_ownership_controls.site
  4. module.environment_owned.aws_s3_bucket_server_side_encryption_configuration.site
  5. module.environment_owned.aws_s3_bucket_versioning.site
  6. module.environment_owned.aws_s3_bucket_policy.site
  7. module.environment_owned.aws_cloudfront_distribution.site
  8. module.environment_owned.aws_cloudfront_origin_access_control.site
  9. module.environment_owned.aws_cloudfront_function.spa_rewrite
  10. module.environment_owned.aws_route53_record.site_a
  11. module.environment_owned.aws_route53_record.site_aaaa
  12. module.environment_owned.aws_iam_role.github_deploy
  13. module.environment_owned.aws_iam_role_policy.github_deploy

Every managed resource has prevent_destroy = true.

live/modules/environment-inventory is data-only. It resolves and checks the caller account, provider region, public hosted zone, ACM certificate, account GitHub OIDC provider, and AWS managed Managed-CachingOptimized CloudFront cache policy.

The following remain outside state:

  • public hosted zones and ACM certificates
  • the account-global GitHub OIDC provider
  • the AWS managed CloudFront cache policy
  • CDKToolkit resources and CDK metadata
  • S3 auto-delete custom resources, provider Lambda, provider role, and log group
  • hosted-zone and ACM validation internals
  • CloudFront service-generated resources

Exact live inventory

Dev

  • Bucket and all bucket subresources: seahaven-shoc-frontend-dev
  • Distribution: E2CWLM1AFB964P
  • OAC: E30VSIK87N8H64
  • OAC name: shocfrontenddevDistributionOrigin1S3OriginAccessControlDFC82620
  • OAC description: the API empty value, modeled as ""
  • Distribution origin ID: shocfrontenddevDistributionOrigin10CCD0EE1
  • Function: us-east-1shocfrontenddevSpaRewrite58674DB8
  • A import ID: Z07671212N75U4YLPWZR8_dev.seahaven.com_A
  • AAAA import ID: Z07671212N75U4YLPWZR8_dev.seahaven.com_AAAA
  • Deploy role: githubdeploy-shoc-frontend-new-dev
  • Inline policy import ID: githubdeploy-shoc-frontend-new-dev:GithubDeployRoleDefaultPolicyE8F540D1
  • Hosted zone: Z07671212N75U4YLPWZR8
  • Stack: shoc-frontend-dev

Staging

  • Bucket and all bucket subresources: seahaven-shoc-frontend-staging
  • Distribution: E2JDVEZ6EGD49J
  • OAC: E1PF5R6QQNBZAI
  • OAC name: shocfrontendstagingDistributOrigin1S3OriginAccessControl82B1C17D
  • OAC description: the API empty value, modeled as ""
  • Distribution origin ID: shocfrontendstagingDistributionOrigin16E4628FC
  • Function: us-east-1shocfrontendstagingSpaRewriteE9C0CBDA
  • A import ID: Z02602739VQWBWCAGXP4_staging.seahaven.com_A
  • AAAA import ID: Z02602739VQWBWCAGXP4_staging.seahaven.com_AAAA
  • Deploy role: githubdeploy-shoc-frontend-new-staging
  • Inline policy import ID: githubdeploy-shoc-frontend-new-staging:GithubDeployRoleDefaultPolicyE8F540D1
  • Hosted zone: Z02602739VQWBWCAGXP4
  • Stack: shoc-frontend-staging

Both live roots inventory the shared certificate:

arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00

The live roots preserve the observed pre-adoption configuration:

  • adoption_complete = false
  • Environment, ManagedBy=cdk, and Project=shoc-frontend tags
  • the S3-only aws-cdk:auto-delete-objects=true tag
  • the deploy-role-only HcpTerraformWorkspace=shoc-frontend-new-<environment> manager tag
  • current OAC names, empty descriptions, origin IDs, comments, protocols, cache policy, certificate, trust subjects, role descriptions, and legacy deploy policy
  • the exact legacy bucket-policy grant for the S3 auto-delete helper
  • the mandatory deterministic permissions boundary arn:aws:iam::396287094661:policy/shoc-frontend-new-<environment>-deploy-boundary

The approved legacy-owner prerequisite narrows the dev role's exact subject from StringLike to StringEquals before import. All roots therefore use StringEquals in both modes. The HCP workspace manager tag remains on the role in both modes and is never added to S3 or CloudFront resources.

If a prerequisite changes any live metadata before import, update the matching root to the newly observed exact value and prove a zero-change import plan. Do not approve that drift through the controlled-update checker.

Prerequisites

Before any remote plan:

  1. Confirm the deployment workflow for the target environment is paused while PR validation remains active.
  2. Confirm no CloudFormation/CDK update or content deployment can race the adoption.
  3. Confirm the HCP workspace is in the seahaven-external-dev project with auto-apply off.
  4. Confirm the org-baseline plan/apply roles and deploy-role permissions boundary exist with exact workspace trust.
  5. Attach the root's deterministic boundary through the approved legacy-owner procedure. It is mandatory before the import plan.
  6. Verify account 396287094661, region us-east-1, all import IDs, current tags, the dev StringEquals trust prerequisite, role description, boundary, policies, distribution configuration, OAC configuration, function code, DNS targets, and bucket settings using read-only queries.
  7. Confirm the creator stack has retention semantics for all 13 transferred resources and the S3 auto-delete custom resource. A synthesized template and reviewed change set are required before mutation.
  8. Capture a complete object-version inventory and smoke-test baseline.

Do not remove or replace the S3 auto-delete custom resource casually. Deleting it while its handler is active can empty the versioned bucket. Retain it during ownership transfer and prove the tf-poc path before touching dev.

HCP variables

Configure dynamic AWS credentials in each workspace. Use the exact org-baseline role ARNs for that workspace:

  • environment variable TFC_AWS_PROVIDER_AUTH=true
  • environment variable TFC_AWS_PLAN_ROLE_ARN
  • environment variable TFC_AWS_APPLY_ROLE_ARN
  • Terraform variable adoption_complete=false

Do not store AWS access keys. Mark sensitive values sensitive even when they are not credentials. VCS working directories are:

  • terraform/live/tf-poc
  • terraform/live/dev
  • terraform/live/staging

tf-poc also requires every variable in terraform.tfvars.example. Populate them only from creator outputs and read-only verification. The check in the tf-poc root blocks planning while a value is empty or starts with REPLACE_WITH_.

Local validation

From the repository root:

terraform fmt -check -recursive terraform
python scripts/test-terraform-import-plan-check.py

For every root:

terraform -chdir=terraform/live/tf-poc init -backend=false
terraform -chdir=terraform/live/tf-poc validate
terraform -chdir=terraform/live/dev init -backend=false
terraform -chdir=terraform/live/dev validate
terraform -chdir=terraform/live/staging init -backend=false
terraform -chdir=terraform/live/staging validate

Initialization without the backend may download providers and write lockfiles, but it must not contact HCP state or plan against AWS.

tf-poc flow

  1. Deploy only the temporary shared stack with tfPocPhase=zone and record its name servers.
  2. Apply the separately approved parent-zone NS delegation and verify it publicly.
  3. Use tfPocPhase=environment to add the certificate and environment stack. Do not attempt certificate creation before delegation.
  4. Record creator outputs for the distribution, OAC ID/name, function, zone, certificate, origin ID, role, inline policy, bucket auto-delete helper role, and DNS import IDs.
  5. With the frontend tf-poc HCP role gate still false, set the five org-baseline tf-poc identifiers from those outputs and deploy the reviewed boundary update. Confirm the creator role's existing boundary now permits only its bucket operations and exact distribution invalidation.
  6. Deploy the real SPA through GitHub environment tf-poc, built with VITE_API_URL=https://api.tf-poc.seahaven.com/api.
  7. Pass HTTPS page load, /login, extensionless SPA fallback, asset-reference integrity, expected/forbidden API URL scan, cache headers, invalidation completion, API CORS/preflight connectivity, and index rollback.
  8. Populate HCP variables. Re-run read-only inventory and compare all declared metadata.
  9. Produce the import plan, export JSON, and pass the zero-change import gate.
  10. Review and apply only the imports. Require an immediate second no-op plan.
  11. Prepare and inspect retention for all transferred resources and the auto-delete custom resource. Do not detach yet.
  12. Set only tf-poc adoption_complete=true. Run the controlled-update gate with the exact addresses below, apply after review, and require a no-op plan. This removes the bucket policy grant while the CDK auto-delete helper role still exists.
  13. Detach the creator stack with the reviewed retention template. Verify identifiers, every object version, DNS, HTTPS/API smoke checks, deploy-role assumption, and a final no-op plan.

Stop on a missing output, placeholder, nonzero import action, unexpected address, replacement, inventory mismatch, retention mismatch, or smoke failure.

Import plan safety

Create a saved plan using the approved remote workflow, then export its JSON:

terraform show -json path\to\saved.plan > path\to\plan.json
python scripts/check-terraform-import-plan.py path\to\plan.json --environment tf-poc

Use dev or staging for the corresponding root. Import mode requires:

  • exactly the canonical 13 addresses and AWS types
  • valid import metadata for every resource
  • exact known import IDs for dev and staging
  • populated, non-placeholder creator IDs for tf-poc
  • zero create, update, delete, or replace actions

After import apply, export the immediate refresh plan and use the distinct post-import mode. It requires all 13 resources to be no-op and rejects any remaining import metadata:

python scripts/check-terraform-import-plan.py path\to\post-import-plan.json `
  --environment tf-poc `
  --post-import-no-op

The exact controlled-adoption addresses are:

  • module.environment_owned.aws_s3_bucket.site
  • module.environment_owned.aws_s3_bucket_policy.site
  • module.environment_owned.aws_cloudfront_distribution.site
  • module.environment_owned.aws_cloudfront_function.spa_rewrite
  • module.environment_owned.aws_iam_role.github_deploy
  • module.environment_owned.aws_iam_role_policy.github_deploy

The bucket-policy update removes only the retained auto-delete helper grant. The IAM role update changes ownership tags while preserving the exact StringEquals subject, boundary, and HCP manager tag.

Run controlled mode by repeating the exact option:

python scripts/check-terraform-import-plan.py path\to\plan.json `
  --environment tf-poc `
  --allow-update-address module.environment_owned.aws_s3_bucket.site `
  --allow-update-address module.environment_owned.aws_s3_bucket_policy.site `
  --allow-update-address module.environment_owned.aws_cloudfront_distribution.site `
  --allow-update-address module.environment_owned.aws_cloudfront_function.spa_rewrite `
  --allow-update-address module.environment_owned.aws_iam_role.github_deploy `
  --allow-update-address module.environment_owned.aws_iam_role_policy.github_deploy

Controlled mode permits only in-place updates to the addresses explicitly listed on that invocation. It rejects create, delete, replace, import metadata, unapproved addresses, and unused allowlist entries. OAC and Route 53 must remain unchanged.

If an ownership-tagged resource does not actually update because its final tags are already present, omit that address from both the plan expectation and the command. Never leave an unused allowlist entry.

Dev and staging flow

Run one live environment at a time.

For dev:

  1. Keep releases paused.
  2. Complete and verify boundary, retention, and exact-metadata prerequisites.
  3. Run and review the zero-change import plan.
  4. Apply imports and require a second no-op plan.
  5. Prepare and verify the retention template only after tf-poc evidence is accepted. Do not detach yet.
  6. Set adoption_complete=true, allow only the exact updating addresses from the controlled list, apply after review, and require another no-op plan.
  7. Detach CloudFormation with the reviewed retention template.
  8. Verify IDs, object versions, DNS, TLS, API connectivity, content deployment, invalidation, rollback, deploy identity, and a final no-op plan.
  9. Re-enable dev release only after explicit approval.

Observe dev for the agreed window. Then repeat the full sequence for staging. Staging termination protection requires a separately reviewed disable immediately before retained stack deletion. Do not carry approval from dev into staging.

Evidence

Retain for each phase:

  • HCP run URL and workspace settings showing auto-apply off
  • saved plan JSON and checker output
  • state list containing exactly the 13 managed addresses
  • read-only inventory before and after each mutation
  • synthesized CloudFormation template, reviewed change set, and stack events
  • object-version inventory
  • exact DNS, certificate, distribution, OAC, function, role, and policy IDs
  • deployment, invalidation, smoke, and rollback output
  • post-action no-op plan
  • phase close-out with completed work, validation, risks, deviations, and remaining work

Rollback

  • Before import apply: discard the run and correct configuration.
  • After import but before the controlled ownership update: remove only the imported Terraform state addresses under a separately reviewed state operation. CloudFormation remains authoritative.
  • After the controlled ownership update but before detachment: do not simply remove Terraform state or redeploy CloudFormation. Either complete the reviewed retained detachment or explicitly restore the exact pre-adoption policy and tags under a separate rollback approval.
  • After detachment: Terraform remains authoritative. Restore content from the versioned bucket and release manifests. Do not recreate the legacy stack over retained resources.
  • Re-establishing CloudFormation ownership requires a reviewed CloudFormation IMPORT change set. An ordinary create/update is not a rollback.

Any replacement, destroy, cross-environment ID, missing import, broad policy change, or failed smoke check is a hard stop.