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'; const BUCKET_NAME = `elasticbeanstalk-${REGION}-${ACCOUNT_ID}`; export class DeployDevStack extends cdk.Stack { constructor(scope: Construct, id: string, props: cdk.StackProps = {}) { super(scope, id, props); 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 bucketArn = `arn:aws:s3:::${BUCKET_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; deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'elasticbeanstalk:DescribeEnvironments', 'elasticbeanstalk:DescribeApplicationVersions', 'elasticbeanstalk:DescribeEvents', ], 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', ], resources: [ `arn:aws:cloudformation:${REGION}:${ACCOUNT_ID}:stack/${ENVIRONMENT_STACK_NAME}/*`, ], }), ); deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'ec2:DescribeAvailabilityZones', 'ec2:DescribeImages', 'ec2:DescribeSubnets', ], resources: ['*'], }), ); deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'autoscaling:DescribeAutoScalingGroups', 'autoscaling:DescribeScalingActivities', ], resources: ['*'], }), ); 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:ListBucket', 's3:CreateBucket', 's3:PutBucketOwnershipControls', 's3:GetBucketLocation', ], resources: [bucketArn], }), ); deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['s3:GetObject', 's3:GetObjectVersion', 's3:PutObject'], resources: [`${bucketArn}/${APPLICATION_NAME}/*`], }), ); deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['s3:GetObjectAcl'], // UpdateEnvironment also checks objects in AWS-owned Elastic Beanstalk // buckets. AWS Support case 178526484500047 confirmed that the caller's // identity policy must cover the service-wide bucket namespace. resources: ['arn:aws:s3:::elasticbeanstalk-*/*'], }), ); 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', }); } }