shoc-backend/terraform/live
Adam Moussa a25e59a63e fix: update required_version from >=1.7.0 to >=1.9.0
The deploy-boundary check interpolates `var.aws_account_id` and `var.environment`. Terraform only allows other variables inside `validation` from 1.9.0+.

CI already runs against `1.9.8` so `versions.tf` setting version as `>=1.7.0` is a breaking finding
2026-08-30 19:25:08 -04:00
..
dev fix: update required_version from >=1.7.0 to >=1.9.0 2026-08-30 19:25:08 -04:00
modules feat(terraform): add safe backend environment adoption 2026-08-30 16:34:38 -04:00
staging fix: update required_version from >=1.7.0 to >=1.9.0 2026-08-30 19:25:08 -04:00
tf-poc fix: update required_version from >=1.7.0 to >=1.9.0 2026-08-30 19:25:08 -04:00
README.md feat(terraform): add safe backend environment adoption 2026-08-30 16:34:38 -04:00

Backend environment adoption

These roots adopt environment-owned infrastructure without taking ownership of shared or Elastic Beanstalk-generated infrastructure.

Ownership

  • dev/ and staging/ import the existing EB environment, runtime role/profile/policies, deploy role/policy, app-config secret metadata, and API record.
  • tf-poc/ manages the retained rehearsal environment after its completed CloudFormation-to-Terraform transfer, excluding the live-only webhook and Dynamo policies. It also owns the child zone and DNS-validated ACM certificate.
  • modules/environment-inventory/ reads and pins only shared resources.
  • Org-baseline CloudFormation owns the narrowly scoped HCP Terraform plan/apply roles.

The shared shoc-backend Elastic Beanstalk application and shoc-sqlserver-shared RDS instance, VPC, subnets, EB service role, shared certificate, shared RDS security group, and EB-generated SG/ALB/ASG/CloudFormation resources must never enter an environment state. The shoc_tf_poc SQL catalog is out of band.

Secret values are not Terraform resources, variables, outputs, or managed EB settings. Terraform manages the app-config secret shell and maps approved JSON keys through aws:elasticbeanstalk:application:environmentsecrets using secret-arn:json-key references. Ordinary application environment settings are limited to non-secret ASP.NET and webhook configuration. The pinned .NET 8 AL2023 platform 3.11.3 supports Secrets Manager JSON-key extraction.

Mandatory live secret migration

Before importing dev or staging, perform a separately approved production mutation from a trusted local session:

  1. Create a temporary OptionSettings JSON file containing the exact environmentsecrets ARN/key references configured in that root and the five non-secret ordinary environment settings.
  2. Create a temporary OptionsToRemove JSON file naming the old raw secret-valued keys in aws:elasticbeanstalk:application:environment.
  3. Run aws elasticbeanstalk update-environment for exactly shoc-backend-dev or shoc-backend-staging with --option-settings file://... and --options-to-remove file://.... Include the provider-normalized sorted Subnets and ELBSubnets values in this same approved update if live ordering differs.
  4. Delete both files, wait for the replacement environment to become Ready and healthy, and run scripts/smoke-elastic-beanstalk.sh against the exact API.
  5. Verify the raw ordinary secret settings are absent before generating the first Terraform plan.

This migration is not performed by Terraform. The first import plan remains zero-change only after the migration succeeds.

Mandatory role-boundary attachment

After the org baseline creates the dedicated boundary policies, perform a separately approved production IAM mutation that attaches:

  • shoc-backend-dev-runtime-boundary to shoc-backend-dev
  • shoc-backend-staging-runtime-boundary to shoc-backend-staging
  • shoc-backend-dev-deploy-boundary to githubdeploy-shoc-backend-dev
  • shoc-backend-staging-deploy-boundary to githubdeploy-shoc-backend-staging

Attach all four boundaries before the SCP and HCP PutRolePolicy exceptions become effective. In the same approved pre-import phase, add the immutable HcpTerraformWorkspace tag to each GitHub deploy role:

  • githubdeploy-shoc-backend-dev: shoc-backend-dev
  • githubdeploy-shoc-backend-staging: shoc-backend-staging

Verify each exact runtime and deploy boundary ARN and workspace tag from GetRole before importing. Terraform requires the boundaries to be present during adoption_complete=false, so the first import remains zero-change. Terraform does not perform this pre-import mutation.

Two-phase adoption

Each dev/staging root pins adoption_complete=false in reviewed code until its initial import is proven. It is not an HCP workspace variable. The retained tf-poc rehearsal has completed both phases and therefore pins adoption_complete=true.

  1. Create the HCP workspace and configure dynamic credentials.

  2. Run the declarative imports.

  3. Export the HCP plan as JSON and run:

    python scripts/check-terraform-import-plan.py plan.json --environment dev
    

    The first plan must be a no-op after import. The guard rejects updates, creates, deletes, replacements, and managed resource types outside the approved environment-owned boundary.

  4. Apply the no-op import only after review.

  5. Change the environment root to adoption_complete=true in a reviewed code change, then review the controlled in-place role and policy update:

    # Dev example. Omit any address that is not updating.
    python scripts/check-terraform-import-plan.py plan.json --environment dev \
      --allow-update-address module.environment.aws_iam_instance_profile.runtime \
      --allow-update-address module.environment.aws_iam_role.runtime \
      --allow-update-address module.environment.aws_iam_role.github_deploy \
      --allow-update-address module.environment.aws_iam_role_policy.github_deploy \
      --allow-update-address module.environment.aws_secretsmanager_secret.app_config
    
  6. Apply only when every update address is named on the command line and the plan contains no create, delete, or replacement action.

The reviewed adoption_complete=true change updates ownership tags on IAM roles, instance profiles, and app-config secrets, and narrows the dev role to the staging-style S3 bucket and application prefix. Elastic Beanstalk environment tags remain at their imported values. EB accepts an added ManagedBy tag request but can fail the asynchronous service-managed CloudFormation propagation after Terraform reports success. Terraform still manages the declared EB settings. Deploy-role descriptions and immutable HcpTerraformWorkspace tags remain unchanged. Read-only AWS APIs retain Resource = "*" only where AWS does not support resource-level permissions.

POC retained identifiers

The tf-poc HCP workspace stores the exact retained environment ID, app-config secret ARN, child-zone ID, and certificate ARN declared in tf-poc/variables.tf. The declarative import blocks consumed those identifiers during the completed transfer. Do not guess or replace them, and do not put credentials or secret values in HCP variables.

ACM DNS validation remains part of the Terraform-owned certificate resource; its generated validation record is not a separate ownership target. The public delegation of tf-poc.seahaven.com from seahaven.com remains outside this Terraform state.

Pinned live identities

  • Dev: workspace shoc-backend-dev; EB environment shoc-backend-dev (e-hehnrqjjrt); .NET 8 AL2023 3.11.3; api.dev.seahaven.com.
  • Staging: workspace shoc-backend-staging; EB environment shoc-backend-staging (e-6c9m4vb62z); .NET 8 AL2023 3.11.3; api.staging.seahaven.com.

The environment roots are intentionally not general-purpose modules. Exact identifiers make accidental cross-environment reuse fail review and planning.

Safety invariants

  • Auto-apply remains off.
  • Org baseline owns final HCP plan/apply permissions and manager tags.
  • Every imported Terraform resource has prevent_destroy.
  • The tf-poc CloudFormation creator path was removed after its no-op import, controlled update, and retained-resource ownership transfer completed.