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