From e25a936da487a5e36bbe537c656cfaac54b25a8d Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 31 Aug 2026 14:42:19 -0400 Subject: [PATCH] feat(terraform): complete dev environment adoption --- infra/cdk/README.md | 29 +++++++++++++++++++++ infra/cdk/deploy-dev-stack.ts | 47 +++++++++++++++++++++++++++++++---- terraform/live/README.md | 19 +++++++++++--- terraform/live/dev/main.tf | 4 +-- 4 files changed, 88 insertions(+), 11 deletions(-) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 8c77048..5ab4d24 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -187,6 +187,32 @@ npm run deploy # deploy the stack (requires AWS) All commands run from `infra/cdk/`. +## Terraform ownership transfer + +`ManageGithubDeployRole` deliberately has no default. Every CDK deployment must +state the intended ownership phase: + +```bash +# Before the controlled Terraform apply: install Retain on the role and policy. +npx cdk deploy shoc-backend-deploy-dev \ + --parameters shoc-backend-deploy-dev:ManageGithubDeployRole=true + +# After Terraform succeeds and live verification passes: relinquish ownership. +npx cdk deploy shoc-backend-deploy-dev \ + --parameters shoc-backend-deploy-dev:ManageGithubDeployRole=false +``` + +Both deployments must use the same reviewed SHA. The first keeps the role and +generated inline policy under CloudFormation while adding retention metadata. +The second removes both resources from CloudFormation ownership while retaining +them live for Terraform. After the second deployment succeeds, +`ManageGithubDeployRole=true` must never be used again. + +Omitting the parameter fails closed before deployment. If the `true` deployment +rolls back, inspect the stack resources and live role/policy before retrying; +retained resources can outlive a failed update and must not be cleaned up +automatically. + ## CI integration `npm run synth` is the deterministic local/CI validation. After synth, inspect @@ -194,6 +220,9 @@ All commands run from `infra/cdk/`. `AWS::IAM::Role`: - Trust policy `StringEquals` matches the exact audience and subject above. +- The role, generated `AWS::IAM::Policy`, and role ARN output share the + `ManageGithubDeployRoleCondition`; both resources use `DeletionPolicy` and + `UpdateReplacePolicy` set to `Retain`. - The inline policy contains no `Resource: "*"` mutation action and no service outside `elasticbeanstalk` / `s3` / `cloudformation` / `ec2` / `elasticloadbalancing` / `autoscaling`. CloudFormation discovery and diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 6ec101d..87bad48 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -14,6 +14,27 @@ export class DeployDevStack extends cdk.Stack { constructor(scope: Construct, id: string, props: cdk.StackProps = {}) { super(scope, id, props); + const manageGithubDeployRole = new cdk.CfnParameter( + this, + 'ManageGithubDeployRole', + { + type: 'String', + allowedValues: ['true', 'false'], + description: + 'Set true only before Terraform adoption. After ownership transfer, always reuse false.', + }, + ); + const manageGithubDeployRoleCondition = new cdk.CfnCondition( + this, + 'ManageGithubDeployRoleCondition', + { + expression: cdk.Fn.conditionEquals( + manageGithubDeployRole.valueAsString, + 'true', + ), + }, + ); + const applicationArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:application/${APPLICATION_NAME}`; const environmentArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:environment/${APPLICATION_NAME}/${ENVIRONMENT_NAME}`; const oidcProviderArn = `arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`; @@ -38,6 +59,7 @@ export class DeployDevStack extends cdk.Stack { const cfnRole = deployRole.node.defaultChild as iam.CfnRole; cfnRole.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN; cfnRole.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN; + cfnRole.cfnOptions.condition = manageGithubDeployRoleCondition; deployRole.addToPolicy( new iam.PolicyStatement({ @@ -134,10 +156,25 @@ export class DeployDevStack extends cdk.Stack { }), ); - new cdk.CfnOutput(this, 'GithubDeployRoleArn', { - value: deployRole.roleArn, - description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.', - exportName: 'shoc-backend-deploy-dev-role-arn', - }); + const defaultPolicy = deployRole.node.findChild( + 'DefaultPolicy', + ) as iam.Policy; + defaultPolicy.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN); + const cfnDefaultPolicy = defaultPolicy.node.defaultChild as iam.CfnPolicy; + cfnDefaultPolicy.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN; + cfnDefaultPolicy.cfnOptions.updateReplacePolicy = + cdk.CfnDeletionPolicy.RETAIN; + cfnDefaultPolicy.cfnOptions.condition = manageGithubDeployRoleCondition; + + const githubDeployRoleArn = new cdk.CfnOutput( + this, + 'GithubDeployRoleArn', + { + value: deployRole.roleArn, + description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.', + exportName: 'shoc-backend-deploy-dev-role-arn', + }, + ); + githubDeployRoleArn.condition = manageGithubDeployRoleCondition; } } diff --git a/terraform/live/README.md b/terraform/live/README.md index e7379c8..0a03f2d 100644 --- a/terraform/live/README.md +++ b/terraform/live/README.md @@ -94,8 +94,9 @@ tf-poc rehearsal has completed both phases and therefore pins 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: +5. Change the environment root to `adoption_complete=true` and + `manage_eb_settings=true` in a reviewed code change, then review the + controlled in-place role, policy, secret, and Elastic Beanstalk update: ```bash # Dev example. Omit any address that is not updating. @@ -104,11 +105,21 @@ tf-poc rehearsal has completed both phases and therefore pins --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 + --allow-update-address module.environment.aws_secretsmanager_secret.app_config \ + --allow-update-address module.environment.aws_elastic_beanstalk_environment.this ``` 6. Apply only when every update address is named on the command line and the - plan contains no create, delete, or replacement action. + plan contains no create, delete, or replacement action. The dev direct ALB + alias remains pinned during this phase and must not update. + +The same reviewed change prepares the legacy dev CDK stack for ownership +transfer. Before the Terraform apply, deploy `shoc-backend-deploy-dev` with +`ManageGithubDeployRole=true` so both the role and generated inline-policy +resource carry `Retain`. After Terraform succeeds and live verification passes, +deploy the same reviewed SHA with `ManageGithubDeployRole=false`. This removes +both resources from CloudFormation ownership without deleting them. Never use +`ManageGithubDeployRole=true` again after that transfer. The reviewed `adoption_complete=true` change updates ownership tags on IAM roles, instance profiles, and app-config secrets, and narrows the dev role to diff --git a/terraform/live/dev/main.tf b/terraform/live/dev/main.tf index 6391581..64f2aeb 100644 --- a/terraform/live/dev/main.tf +++ b/terraform/live/dev/main.tf @@ -26,8 +26,8 @@ module "environment" { aws_account_id = local.aws_account_id aws_region = local.aws_region environment = "dev" - adoption_complete = false - manage_eb_settings = false + adoption_complete = true + manage_eb_settings = true eb_application_name = local.eb_application_name eb_environment_name = local.eb_environment_name eb_environment_id = local.eb_environment_id