mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +00:00
feat(terraform): complete dev environment adoption (SH-300) (#99)
* feat(terraform): complete dev environment adoption * fix(terraform): preserve dev release permissions
This commit is contained in:
parent
e1e547f7d0
commit
6ef8e9ba2a
6 changed files with 104 additions and 23 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,8 +94,10 @@ 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 metadata, secret metadata, and Elastic Beanstalk
|
||||
update:
|
||||
|
||||
```bash
|
||||
# Dev example. Omit any address that is not updating.
|
||||
|
|
@ -103,22 +105,32 @@ tf-poc rehearsal has completed both phases and therefore pins
|
|||
--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
|
||||
--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
|
||||
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.
|
||||
roles, instance profiles, and app-config secrets. Dev retains the proven GitHub
|
||||
Elastic Beanstalk release policy until application CD is migrated in a separate
|
||||
reviewed change; infrastructure adoption must not silently break the current
|
||||
manual release path. 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.
|
||||
|
||||
## POC retained identifiers
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ locals {
|
|||
environment_arn = "arn:aws:elasticbeanstalk:${var.aws_region}:${var.aws_account_id}:environment/${var.eb_application_name}/${var.eb_environment_name}"
|
||||
environment_stack_name = "awseb-${var.eb_environment_id}-stack"
|
||||
eb_bucket_name = "elasticbeanstalk-${var.aws_region}-${var.aws_account_id}"
|
||||
use_legacy_s3_policy = !var.adoption_complete && var.legacy_dev_s3_policy
|
||||
use_legacy_s3_policy = var.legacy_dev_s3_policy
|
||||
app_config_secret_pattern = "arn:aws:secretsmanager:${var.aws_region}:${var.aws_account_id}:secret:${var.app_config_secret_name}-*"
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +141,8 @@ resource "aws_iam_instance_profile" "runtime" {
|
|||
}
|
||||
|
||||
resource "aws_secretsmanager_secret" "app_config" {
|
||||
# Terraform owns the secret shell and metadata only. Values remain out of
|
||||
# band and must never be declared in this resource or its callers.
|
||||
name = var.app_config_secret_name
|
||||
description = var.metadata_before_adoption.app_config_description
|
||||
tags = var.adoption_complete ? merge(var.metadata_before_adoption.app_config_tags, { ManagedBy = "terraform" }) : var.metadata_before_adoption.app_config_tags
|
||||
|
|
|
|||
|
|
@ -190,8 +190,9 @@ variable "github_deploy_policy_name" {
|
|||
}
|
||||
|
||||
variable "legacy_dev_s3_policy" {
|
||||
type = bool
|
||||
default = false
|
||||
type = bool
|
||||
description = "Retain the proven GitHub Elastic Beanstalk release policy until application CD is migrated separately."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "hosted_zone_id" {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue