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-pocshoc-frontend-new-devshoc-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:
module.environment_owned.aws_s3_bucket.sitemodule.environment_owned.aws_s3_bucket_public_access_block.sitemodule.environment_owned.aws_s3_bucket_ownership_controls.sitemodule.environment_owned.aws_s3_bucket_server_side_encryption_configuration.sitemodule.environment_owned.aws_s3_bucket_versioning.sitemodule.environment_owned.aws_s3_bucket_policy.sitemodule.environment_owned.aws_cloudfront_distribution.sitemodule.environment_owned.aws_cloudfront_origin_access_control.sitemodule.environment_owned.aws_cloudfront_function.spa_rewritemodule.environment_owned.aws_route53_record.site_amodule.environment_owned.aws_route53_record.site_aaaamodule.environment_owned.aws_iam_role.github_deploymodule.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
CDKToolkitresources 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 = falseEnvironment,ManagedBy=cdk, andProject=shoc-frontendtags- the S3-only
aws-cdk:auto-delete-objects=truetag - 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:
- Confirm the deployment workflow for the target environment is paused while PR validation remains active.
- Confirm no CloudFormation/CDK update or content deployment can race the adoption.
- Confirm the HCP workspace is in the
seahaven-external-devproject with auto-apply off. - Confirm the org-baseline plan/apply roles and deploy-role permissions boundary exist with exact workspace trust.
- Attach the root's deterministic boundary through the approved legacy-owner procedure. It is mandatory before the import plan.
- Verify account
396287094661, regionus-east-1, all import IDs, current tags, the devStringEqualstrust prerequisite, role description, boundary, policies, distribution configuration, OAC configuration, function code, DNS targets, and bucket settings using read-only queries. - 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.
- 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-pocterraform/live/devterraform/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
- Deploy only the temporary shared stack with
tfPocPhase=zoneand record its name servers. - Apply the separately approved parent-zone NS delegation and verify it publicly.
- Use
tfPocPhase=environmentto add the certificate and environment stack. Do not attempt certificate creation before delegation. - 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.
- 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.
- Deploy the real SPA through GitHub environment
tf-poc, built withVITE_API_URL=https://api.tf-poc.seahaven.com/api. - 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. - Populate HCP variables. Re-run read-only inventory and compare all declared metadata.
- Produce the import plan, export JSON, and pass the zero-change import gate.
- Review and apply only the imports. Require an immediate second no-op plan.
- Prepare and inspect retention for all transferred resources and the auto-delete custom resource. Do not detach yet.
- 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. - 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.sitemodule.environment_owned.aws_s3_bucket_policy.sitemodule.environment_owned.aws_cloudfront_distribution.sitemodule.environment_owned.aws_cloudfront_function.spa_rewritemodule.environment_owned.aws_iam_role.github_deploymodule.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:
- Keep releases paused.
- Complete and verify boundary, retention, and exact-metadata prerequisites.
- Run and review the zero-change import plan.
- Apply imports and require a second no-op plan.
- Prepare and verify the retention template only after tf-poc evidence is accepted. Do not detach yet.
- Set
adoption_complete=true, allow only the exact updating addresses from the controlled list, apply after review, and require another no-op plan. - Detach CloudFormation with the reviewed retention template.
- Verify IDs, object versions, DNS, TLS, API connectivity, content deployment, invalidation, rollback, deploy identity, and a final no-op plan.
- 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
IMPORTchange 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.