mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-04 09:02:10 +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/`.
|
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
|
## CI integration
|
||||||
|
|
||||||
`npm run synth` is the deterministic local/CI validation. After synth, inspect
|
`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`:
|
`AWS::IAM::Role`:
|
||||||
|
|
||||||
- Trust policy `StringEquals` matches the exact audience and subject above.
|
- 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
|
- The inline policy contains no `Resource: "*"` mutation action and no service
|
||||||
outside `elasticbeanstalk` / `s3` / `cloudformation` / `ec2` /
|
outside `elasticbeanstalk` / `s3` / `cloudformation` / `ec2` /
|
||||||
`elasticloadbalancing` / `autoscaling`. CloudFormation discovery and
|
`elasticloadbalancing` / `autoscaling`. CloudFormation discovery and
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,27 @@ export class DeployDevStack extends cdk.Stack {
|
||||||
constructor(scope: Construct, id: string, props: cdk.StackProps = {}) {
|
constructor(scope: Construct, id: string, props: cdk.StackProps = {}) {
|
||||||
super(scope, id, props);
|
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 applicationArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:application/${APPLICATION_NAME}`;
|
||||||
const environmentArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:environment/${APPLICATION_NAME}/${ENVIRONMENT_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`;
|
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;
|
const cfnRole = deployRole.node.defaultChild as iam.CfnRole;
|
||||||
cfnRole.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN;
|
cfnRole.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN;
|
||||||
cfnRole.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
|
cfnRole.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
|
||||||
|
cfnRole.cfnOptions.condition = manageGithubDeployRoleCondition;
|
||||||
|
|
||||||
deployRole.addToPolicy(
|
deployRole.addToPolicy(
|
||||||
new iam.PolicyStatement({
|
new iam.PolicyStatement({
|
||||||
|
|
@ -134,10 +156,25 @@ export class DeployDevStack extends cdk.Stack {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
new cdk.CfnOutput(this, 'GithubDeployRoleArn', {
|
const defaultPolicy = deployRole.node.findChild(
|
||||||
value: deployRole.roleArn,
|
'DefaultPolicy',
|
||||||
description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.',
|
) as iam.Policy;
|
||||||
exportName: 'shoc-backend-deploy-dev-role-arn',
|
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
|
creates, deletes, replacements, and managed resource types outside the
|
||||||
approved environment-owned boundary.
|
approved environment-owned boundary.
|
||||||
4. Apply the no-op import only after review.
|
4. Apply the no-op import only after review.
|
||||||
5. Change the environment root to `adoption_complete=true` in a reviewed code
|
5. Change the environment root to `adoption_complete=true` and
|
||||||
change, then review the controlled in-place role and policy update:
|
`manage_eb_settings=true` in a reviewed code change, then review the
|
||||||
|
controlled in-place role metadata, secret metadata, and Elastic Beanstalk
|
||||||
|
update:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Dev example. Omit any address that is not updating.
|
# 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_instance_profile.runtime \
|
||||||
--allow-update-address module.environment.aws_iam_role.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.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
|
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
|
The reviewed `adoption_complete=true` change updates ownership tags on IAM
|
||||||
roles, instance profiles, and app-config secrets, and narrows the dev role to
|
roles, instance profiles, and app-config secrets. Dev retains the proven GitHub
|
||||||
the staging-style S3 bucket and application prefix. Elastic Beanstalk
|
Elastic Beanstalk release policy until application CD is migrated in a separate
|
||||||
environment tags remain at their imported values. EB accepts an added
|
reviewed change; infrastructure adoption must not silently break the current
|
||||||
`ManagedBy` tag request but can fail the asynchronous service-managed
|
manual release path. Elastic Beanstalk environment tags remain at their imported
|
||||||
CloudFormation propagation after Terraform reports success. Terraform still
|
values. Terraform manages the declared EB settings. Secret values remain
|
||||||
manages the declared EB settings. Deploy-role descriptions and immutable
|
out-of-band even after the secret shell receives `ManagedBy=terraform`.
|
||||||
`HcpTerraformWorkspace` tags remain unchanged. Read-only AWS APIs retain
|
Deploy-role descriptions and immutable `HcpTerraformWorkspace` tags remain
|
||||||
`Resource = "*"` only where AWS does not support resource-level permissions.
|
unchanged. Read-only AWS APIs retain `Resource = "*"` only where AWS does not
|
||||||
|
support resource-level permissions.
|
||||||
|
|
||||||
## POC retained identifiers
|
## POC retained identifiers
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ module "environment" {
|
||||||
aws_account_id = local.aws_account_id
|
aws_account_id = local.aws_account_id
|
||||||
aws_region = local.aws_region
|
aws_region = local.aws_region
|
||||||
environment = "dev"
|
environment = "dev"
|
||||||
adoption_complete = false
|
adoption_complete = true
|
||||||
manage_eb_settings = false
|
manage_eb_settings = true
|
||||||
eb_application_name = local.eb_application_name
|
eb_application_name = local.eb_application_name
|
||||||
eb_environment_name = local.eb_environment_name
|
eb_environment_name = local.eb_environment_name
|
||||||
eb_environment_id = local.eb_environment_id
|
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_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"
|
environment_stack_name = "awseb-${var.eb_environment_id}-stack"
|
||||||
eb_bucket_name = "elasticbeanstalk-${var.aws_region}-${var.aws_account_id}"
|
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}-*"
|
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" {
|
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
|
name = var.app_config_secret_name
|
||||||
description = var.metadata_before_adoption.app_config_description
|
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
|
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" {
|
variable "legacy_dev_s3_policy" {
|
||||||
type = bool
|
type = bool
|
||||||
default = false
|
description = "Retain the proven GitHub Elastic Beanstalk release policy until application CD is migrated separately."
|
||||||
|
default = false
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "hosted_zone_id" {
|
variable "hosted_zone_id" {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue