UNPKG

@aws-amplify/amplify-category-api

Version:
397 lines (347 loc) 12 kB
import * as path from 'path'; import * as codebuild from 'aws-cdk-lib/aws-codebuild'; import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'; import * as codepipelineactions from 'aws-cdk-lib/aws-codepipeline-actions'; import * as ecr from 'aws-cdk-lib/aws-ecr'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as cdk from 'aws-cdk-lib'; import * as custom from 'aws-cdk-lib/custom-resources'; import { Construct } from 'constructs'; import * as fs from 'fs-extra'; import { DEPLOYMENT_MECHANISM } from './base-api-stack'; import { getGitHubOwnerRepoFromPath } from './utils/github'; type PipelineAwaiterProps = { pipeline: codepipeline.Pipeline; artifactBucketName?: string; artifactKey?: string; deploymentMechanism: DEPLOYMENT_MECHANISM; }; export type GitHubSourceActionInfo = { path: string; tokenSecretArn: string; }; const lambdaRuntimeNodeVersion = new lambda.Runtime('nodejs22.x', lambda.RuntimeFamily.NODEJS, { supportsInlineCode: true }); const lambdasDir = path.resolve(__dirname, '../../../resources/awscloudformation/lambdas'); class PipelineAwaiter extends Construct { constructor(scope: Construct, id: string, props: PipelineAwaiterProps) { const { pipeline, artifactBucketName, artifactKey, deploymentMechanism } = props; const { pipelineArn, pipelineName } = pipeline; const pipelineOnEventCodeFilePath = path.join(lambdasDir, 'pipeline-on-event.js'); const onEventHandlerCode = fs.readFileSync(pipelineOnEventCodeFilePath, 'utf8'); const onEventHandler = new lambda.Function(scope, `${id}CustomEventHandler`, { runtime: lambdaRuntimeNodeVersion, handler: 'index.handler', code: lambda.Code.fromInline(onEventHandlerCode), timeout: cdk.Duration.seconds(15), }); const pipelineCodeFilePath = path.join(lambdasDir, 'pipeline.js'); const isCompleteHandlerCode = fs.readFileSync(pipelineCodeFilePath, 'utf8'); const isCompleteHandler = new lambda.Function(scope, `${id}CustomCompleteHandler`, { runtime: lambdaRuntimeNodeVersion, handler: 'index.handler', timeout: cdk.Duration.seconds(15), code: lambda.Code.fromInline(isCompleteHandlerCode), }); isCompleteHandler.addToRolePolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['codepipeline:GetPipeline', 'codepipeline:ListPipelineExecutions'], resources: [pipelineArn], }), ); isCompleteHandler.addToRolePolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['cloudformation:DescribeStacks'], resources: [cdk.Stack.of(scope).stackId], }), ); const myProvider = new custom.Provider(scope, `${id}MyProvider`, { onEventHandler, isCompleteHandler, queryInterval: cdk.Duration.seconds(10), }); new cdk.CustomResource(scope, `Deployment${id}`, { serviceToken: myProvider.serviceToken, properties: { artifactBucketName, artifactKey, pipelineName, deploymentMechanism, }, }); super(scope, id); } } export class PipelineWithAwaiter extends Construct { pipelineName: string; constructor( scope: Construct, id: string, { skipWait = false, bucket, s3SourceActionKey, service, deploymentMechanism, gitHubSourceActionInfo, containersInfo, desiredCount, envName, }: { skipWait?: boolean; bucket: s3.IBucket; s3SourceActionKey?: string; deploymentMechanism: DEPLOYMENT_MECHANISM; gitHubSourceActionInfo?: GitHubSourceActionInfo; service: ecs.CfnService; containersInfo: { container: ecs.ContainerDefinition; repository: ecr.IRepository; }[]; desiredCount: number; envName: string; }, ) { super(scope, id); const sourceOutput = new codepipeline.Artifact('SourceArtifact'); const buildOutput = new codepipeline.Artifact('BuildArtifact'); const codeBuildProject = new codebuild.PipelineProject(scope, `${id}CodeBuildProject`, { environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_4_0, // See: https://docs.aws.amazon.com/codebuild/latest/userguide/troubleshooting.html#troubleshooting-cannot-connect-to-docker-daemon privileged: true, }, }); if (gitHubSourceActionInfo && gitHubSourceActionInfo.tokenSecretArn) { codeBuildProject.addToRolePolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'secretsmanager:GetRandomPassword', 'secretsmanager:GetResourcePolicy', 'secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret', 'secretsmanager:ListSecretVersionIds', ], resources: [gitHubSourceActionInfo.tokenSecretArn], }), ); } codeBuildProject.role.addToPrincipalPolicy( new iam.PolicyStatement({ resources: ['*'], actions: [ 'ecr:GetAuthorizationToken', 'ecr:BatchGetImage', 'ecr:GetDownloadUrlForLayer', 'ecr:InitiateLayerUpload', 'ecr:BatchCheckLayerAvailability', 'ecr:UploadLayerPart', 'ecr:CompleteLayerUpload', 'ecr:PutImage', ], effect: iam.Effect.ALLOW, }), ); const prebuildStages = createPreBuildStages(scope, { bucket, s3SourceActionKey, gitHubSourceActionInfo, roleName: 'UpdateSource', sourceOutput, }); const environmentVariables = containersInfo.reduce( (acc, c) => { acc[`${c.container.containerName}_REPOSITORY_URI`] = { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: c.repository.repositoryUri, }; return acc; }, { AWS_ACCOUNT_ID: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: cdk.Aws.ACCOUNT_ID, }, } as Record<string, codebuild.BuildEnvironmentVariable>, ); // TODO: Remove this custom role if we upgrade the CDK version to >= 2.132.0 // since this ecs:TagResource permission will be present in the default deploy role policy generated by CDK const ecsDeployActionRole = new iam.Role(scope, 'EcsDeployActionRole', { assumedBy: new iam.AccountRootPrincipal(), }); ecsDeployActionRole.addToPolicy( new iam.PolicyStatement({ actions: ['ecs:TagResource'], effect: iam.Effect.ALLOW, resources: ['*'], }), ); const ecsDeployAction = new codepipelineactions.EcsDeployAction({ actionName: 'Deploy', service: new (class extends Construct implements ecs.IBaseService { cluster = { clusterName: service.cluster, env: {}, } as ecs.ICluster; serviceArn = cdk.Fn.ref(service.attrServiceArn); serviceName = service.serviceName; stack = cdk.Stack.of(this); env = {} as any; node = service.node; public applyRemovalPolicy(policy: cdk.RemovalPolicy): void { // TODO: This is added for CDK upgrade. Modify the behavior if required. } })(this, 'tmpService'), input: buildOutput, role: ecsDeployActionRole, }); const stagesWithDeploy = ([] as codepipeline.StageOptions[]).concat(prebuildStages, [ { stageName: 'Build', actions: [ new codepipelineactions.CodeBuildAction({ actionName: 'Build', type: codepipelineactions.CodeBuildActionType.BUILD, project: codeBuildProject, input: sourceOutput, outputs: [buildOutput], environmentVariables, }), ], }, { stageName: 'Predeploy', actions: [ new codepipelineactions.LambdaInvokeAction({ actionName: 'Predeploy', lambda: (() => { const preDeployCodeFilePath = path.join(lambdasDir, 'predeploy.js'); const lambdaHandlerCode = fs.readFileSync(preDeployCodeFilePath, 'utf8'); const action = new lambda.Function(scope, 'PreDeployLambda', { code: lambda.Code.fromInline(lambdaHandlerCode), handler: 'index.handler', runtime: lambdaRuntimeNodeVersion, environment: { DESIRED_COUNT: `${desiredCount}`, CLUSTER_NAME: service.cluster, SERVICE_NAME: service.serviceName, }, timeout: cdk.Duration.seconds(15), }); action.addToRolePolicy( new iam.PolicyStatement({ actions: ['ecs:UpdateService'], effect: iam.Effect.ALLOW, resources: [cdk.Fn.ref(service.logicalId)], }), ); return action; })(), inputs: [], outputs: [], }), ], }, { stageName: 'Deploy', actions: [ecsDeployAction], }, ]); this.pipelineName = `${envName}-${service.serviceName}`; const pipeline = new codepipeline.Pipeline(scope, `${id}Pipeline`, { pipelineName: this.pipelineName, crossAccountKeys: false, artifactBucket: bucket, stages: stagesWithDeploy, }); pipeline.node.addDependency(service); if (!skipWait) { new PipelineAwaiter(scope, 'Awaiter', { pipeline, artifactBucketName: bucket.bucketName, artifactKey: s3SourceActionKey, deploymentMechanism, }); } new cdk.CfnOutput(scope, 'PipelineName', { value: this.pipelineName }); } getPipelineName(): string { return this.pipelineName; } } function createPreBuildStages( scope: Construct, { bucket, s3SourceActionKey, gitHubSourceActionInfo, sourceOutput, roleName, }: { bucket: s3.IBucket; s3SourceActionKey: string; gitHubSourceActionInfo?: GitHubSourceActionInfo; sourceOutput: codepipeline.Artifact; roleName: string; }, ) { const stages: codepipeline.StageOptions[] = []; const stage = { stageName: 'Source', actions: [], }; stages.push(stage); if (gitHubSourceActionInfo && gitHubSourceActionInfo.path) { const { path, tokenSecretArn } = gitHubSourceActionInfo; const { owner, repo, branch } = getGitHubOwnerRepoFromPath(path); const preBuildOutput = new codepipeline.Artifact('PreBuildArtifact'); stage.actions = [ new codepipelineactions.GitHubSourceAction({ actionName: 'Source', oauthToken: cdk.SecretValue.secretsManager(tokenSecretArn), owner, repo, branch, output: preBuildOutput, }), ]; stages.push({ stageName: 'PreBuild', actions: [ new codepipelineactions.LambdaInvokeAction({ actionName: 'PreBuild', lambda: new lambda.Function(scope, 'PreBuildLambda', { code: lambda.S3Code.fromBucket(bucket, 'codepipeline-action-buildspec-generator-lambda.zip'), handler: 'index.handler', runtime: lambdaRuntimeNodeVersion, timeout: cdk.Duration.seconds(15), }), inputs: [preBuildOutput], outputs: [sourceOutput], }), ], }); } else { stage.actions = [ new codepipelineactions.S3SourceAction({ actionName: 'Source', bucket, bucketKey: s3SourceActionKey, output: sourceOutput, }), ]; } return stages; } export type ContainerStackProps = { deploymentBucket: string; containerPort: number; awaiterZipPath: string; gitHubPath?: string; gitHubTokenSecretsManagerArn: string; };