feat(terraform): complete dev environment adoption

This commit is contained in:
Adam Moussa 2026-08-31 14:42:19 -04:00
parent e1e547f7d0
commit e25a936da4
No known key found for this signature in database
4 changed files with 88 additions and 11 deletions

View file

@ -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

View file

@ -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;
}
}

View file

@ -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

View file

@ -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