shoc-backend/infra/cdk/deploy-dev-stack.ts
Adam Moussa 6ef8e9ba2a
feat(terraform): complete dev environment adoption (SH-300) (#99)
* feat(terraform): complete dev environment adoption

* fix(terraform): preserve dev release permissions
2026-08-31 20:18:44 -03:00

180 lines
6.3 KiB
TypeScript

import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
const ACCOUNT_ID = '396287094661';
const REGION = 'us-east-1';
const APPLICATION_NAME = 'shoc-backend';
const ENVIRONMENT_NAME = 'shoc-backend-dev';
const ENVIRONMENT_ID = 'e-hehnrqjjrt';
const ENVIRONMENT_STACK_NAME = `awseb-${ENVIRONMENT_ID}-stack`;
const REPO = 'Sea-Haven-Industries/shoc-backend';
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`;
const deployRole = new iam.Role(this, 'GithubDeployRole', {
roleName: 'githubdeploy-shoc-backend-dev',
description:
'Least-privilege GitHub OIDC deploy role for shoc-backend dev. CDK-owned; application/environment/S3 are owned by Elastic Beanstalk.',
assumedBy: new iam.FederatedPrincipal(
oidcProviderArn,
{
StringEquals: {
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
'token.actions.githubusercontent.com:sub': `repo:${REPO}:environment:dev`,
},
},
'sts:AssumeRoleWithWebIdentity',
),
});
deployRole.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
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({
effect: iam.Effect.ALLOW,
actions: [
'autoscaling:Describe*',
'ec2:Describe*',
'elasticbeanstalk:DescribeEnvironments',
'elasticbeanstalk:DescribeApplicationVersions',
'elasticbeanstalk:DescribeEvents',
'elasticloadbalancing:Describe*',
],
resources: ['*'],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['elasticbeanstalk:CreateApplicationVersion'],
resources: [
applicationArn,
`arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:applicationversion/${APPLICATION_NAME}/*`,
],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['elasticbeanstalk:UpdateEnvironment'],
resources: [environmentArn],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'cloudformation:DescribeStackEvents',
'cloudformation:DescribeStackResource',
'cloudformation:GetTemplate',
'cloudformation:DescribeStackResources',
'cloudformation:DescribeStacks',
'cloudformation:ListStackResources',
'cloudformation:CancelUpdateStack',
'cloudformation:UpdateStack',
],
resources: [
`arn:aws:cloudformation:${REGION}:${ACCOUNT_ID}:stack/${ENVIRONMENT_STACK_NAME}/*`,
],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'autoscaling:PutNotificationConfiguration',
'autoscaling:ResumeProcesses',
'autoscaling:SuspendProcesses',
],
resources: [
`arn:aws:autoscaling:${REGION}:${ACCOUNT_ID}:autoScalingGroup:*:autoScalingGroupName/${ENVIRONMENT_STACK_NAME}-*`,
],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:Delete*', 's3:Get*', 's3:Put*'],
// AWS Support case 178526484500047 confirmed that UpdateEnvironment
// reads, writes, versions, ACL-checks, and removes objects in both the
// account bucket and AWS-owned Elastic Beanstalk service buckets.
resources: ['arn:aws:s3:::elasticbeanstalk-*/*'],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
's3:GetBucket*',
's3:ListBucket',
's3:PutBucketOwnershipControls',
's3:PutBucketPolicy',
's3:PutBucketPublicAccessBlock',
],
// This is AWS Support's bucket-level UpdateEnvironment set, excluding
// CreateBucket because the workflow deploys only to an existing
// application/environment and disables bucket creation.
resources: ['arn:aws:s3:::elasticbeanstalk-*'],
}),
);
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;
}
}