GitHub creates the immutable Elastic Beanstalk version; HCP Terraform is the only UpdateEnvironment caller via a guarded version_label run. |
||
|---|---|---|
| .. | ||
| dev | ||
| modules | ||
| staging | ||
| tf-poc | ||
| README.md | ||
Backend environment adoption
These roots adopt environment-owned infrastructure without taking ownership of shared or Elastic Beanstalk-generated infrastructure.
Ownership
dev/andstaging/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:
- Create a temporary
OptionSettingsJSON file containing the exactenvironmentsecretsARN/key references configured in that root and the five non-secret ordinary environment settings. - Create a temporary
OptionsToRemoveJSON file naming the old raw secret-valued keys inaws:elasticbeanstalk:application:environment. - Run
aws elasticbeanstalk update-environmentfor exactlyshoc-backend-devorshoc-backend-stagingwith--option-settings file://...and--options-to-remove file://.... Include the provider-normalized sortedSubnetsandELBSubnetsvalues in this same approved update if live ordering differs. - Delete both files, wait for the replacement environment to become Ready and
healthy, and run
scripts/smoke-elastic-beanstalk.shagainst the exact API. - 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-boundarytoshoc-backend-devshoc-backend-staging-runtime-boundarytoshoc-backend-stagingshoc-backend-dev-deploy-boundarytogithubdeploy-shoc-backend-devshoc-backend-staging-deploy-boundarytogithubdeploy-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-devgithubdeploy-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.
-
Create the HCP workspace and configure dynamic credentials.
-
Run the declarative imports.
-
Export the HCP plan as JSON and run:
python scripts/check-terraform-import-plan.py plan.json --environment devThe 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.
-
Apply the no-op import only after review.
-
Change the environment root to
adoption_complete=trueandmanage_eb_settings=truein a reviewed code change, then review the controlled in-place role metadata, secret metadata, and Elastic Beanstalk 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_secretsmanager_secret.app_config \ --allow-update-address module.environment.aws_elastic_beanstalk_environment.this -
Apply only when every update address is named on the command line and the plan contains no create, delete, or replacement action. The dev direct ALB alias remains pinned during this phase and must not update.
The dev deploy role and generated inline policy completed their retained CloudFormation-to-Terraform transfer before the legacy backend CDK source was removed. Do not reintroduce that ownership path.
The reviewed adoption_complete=true change updates ownership tags on IAM
roles, instance profiles, and app-config secrets. Elastic Beanstalk
environment tags remain at their imported values. Terraform manages the
declared EB settings. Secret values remain out-of-band even after the secret
shell receives ManagedBy=terraform. Deploy-role descriptions and immutable
HcpTerraformWorkspace tags remain unchanged. Read-only AWS APIs retain
Resource = "*" only where AWS does not support resource-level permissions.
The measured self-contained .NET/EF bundle is approximately 199.5 MB and separate builds are not byte-identical. Each deploy job therefore validates the exact bundle it uploads; bundle bytes never enter Terraform plans or state.
Dev application CD
GitHub compiles, validates, and uploads the bundle, then creates the immutable
Elastic Beanstalk application version. HCP Terraform is the only caller of
UpdateEnvironment, by setting version_label on
module.environment.aws_elastic_beanstalk_environment.this. GitHub then
health-checks, smokes, and requests one guarded Terraform rollback. Terraform
does not manage aws_elastic_beanstalk_application_version; retained versions
are the rollback inventory.
release_version_label is a nullable root and module variable. Null VCS plans
leave the live version unchanged. Application-CD runs pass the immutable
<full-sha>-<run-id>-<attempt> label only as a run-specific
TF_VAR_release_version_label HCL string. Do not set this variable on the
workspace, in a variable set, or in terraform.tfvars. Do not upload a new
configuration version on application releases; create-run reuses the
workspace's last applied VCS config. Global auto-apply stays off. GitHub
applies only after plan-output counts are 0/1/0 and
scripts/check-terraform-release-plan.py accepts a version-only plan JSON.
Staging keeps today's direct Elastic Beanstalk deploy path until staging adoption.
Credentials and enablement
Store a dedicated HCP team token only as the GitHub dev environment secret
TF_API_TOKEN. Scope it to workspace shoc-backend-dev. Plan JSON download
requires workspace admin on that one workspace. Do not grant project admin,
workspace create/move/delete, or staging access. Rotate at least every 90 days.
Repository variable TERRAFORM_APP_CD_ENABLED starts unset/false so pushes to
dev do not deploy. workflow_dispatch on dev still runs a release for the
first manual proof. Set the variable to true only after that proof confirms
the exact version, a version-only plan, apply, efbundle, Ready/Green, smokes,
and a retained previous version.
This change is the allowed exception that mixes deployable application CD with the Terraform variable that application CD needs. Later PRs must not mix deployable application changes with Terraform or CDK changes.
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 environmentshoc-backend-dev(e-hehnrqjjrt); .NET 8 AL20233.11.3;api.dev.seahaven.com. - Staging: workspace
shoc-backend-staging; EB environmentshoc-backend-staging(e-6c9m4vb62z); .NET 8 AL20233.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.