(@aws-cdk/pipelines) 应用程序源代码的构建阶段

(@aws-cdk/pipelines) Build stage for application source code

我完成了 CDK Pipelines: Continuous delivery for AWS CDK applications tutorial, which gave an overview of creating a self-mutating CDK pipeline with the new CodePipeline API

本教程创建了一个 CodePipeline,每次将更改推送到 master 分支时,CDK 源代码都会自动从 GitHub 存储库中检索。 CDK 代码定义了一个 lambda,其中包含与 CDK 一起定义的打字稿处理程序。

对于我的用例,我想定义一个自变异 CodePipeline,每当我推送到包含我的应用程序源代码的第二个存储库时,它也会被触发。第二个存储库还将包含一个构建规范,该构建规范使用我的应用程序生成一个 Docker 图像并将该图像上传到 ECR。然后,新映像将在我的管道的应用程序阶段部署到 Fargate 集群。

我在 PublishAssets 阶段之后创建了一个 ApplicationBuild 阶段,其中包括一个 CodeBuild 项目。 CodeBuild 项目从我的存储库中读取并构建/上传图像到 ECR;但是,我需要一种方法将此 CodeBuild link 部署到管道中。我不清楚如何使用新的 cdk CodePipeline API.

如果有人遇到同样的问题,我可以使用我在问题中提到的教程的 legacy CdkPipeline API following the archived version 来破解一个解决方案。

这是一个最小的可行管道堆栈,其中包括...

  1. CDK 流水线源操作(在“源”阶段)
  2. 应用程序源操作(在“源”阶段)
  3. 一个 CDK 构建操作(在“Build”阶段)+ 自变异管道(“UpdatePipeline”阶段)
  4. 应用程序构建操作(在“构建”阶段)

lib/cdkpipelines-demo-pipeline-stack.ts

import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as core from '@aws-cdk/core';
import {Construct, SecretValue, Stack, StackProps} from '@aws-cdk/core';
import {CdkPipeline, SimpleSynthAction} from "@aws-cdk/pipelines";
import * as iam from "@aws-cdk/aws-iam";
import * as ecr from "@aws-cdk/aws-ecr";
import * as codebuild from "@aws-cdk/aws-codebuild";

/**
 * The stack that defines the application pipeline
 */
export class CdkpipelinesDemoPipelineStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props);

        const sourceArtifact = new codepipeline.Artifact();
        const cloudAssemblyArtifact = new codepipeline.Artifact();

        const pipeline = new CdkPipeline(this, 'Pipeline', {
            // The pipeline name
            pipelineName: 'MyServicePipeline',
            cloudAssemblyArtifact,

            // Where the source can be found
            sourceAction: new codepipeline_actions.GitHubSourceAction({
                actionName: 'GitHub',
                output: sourceArtifact,
                oauthToken: SecretValue.secretsManager('github-token'),
                owner: 'OWNER',
                repo: 'REPO',
            }),

            // How it will be built and synthesized
            synthAction: SimpleSynthAction.standardNpmSynth({
                sourceArtifact,
                cloudAssemblyArtifact,

                // We need a build step to compile the TypeScript Lambda
                buildCommand: 'npm run build'
            }),
        });
        const pipelineRole = pipeline.codePipeline.role;

        // Add application source action
        const appSourceArtifact = new codepipeline.Artifact();
        const appSourceAction = this.createAppSourceAction(appSourceArtifact);
        const sourceStage = pipeline.stage("Source");
        sourceStage.addAction(appSourceAction);

        // Add application build action
        const codeBuildServiceRole = this.createCodeBuildServiceRole(this, pipelineRole);
        const repository = this.createApplicationRepository(this, codeBuildServiceRole);
        const pipelineProject = this.createCodeBuildPipelineProject(
            this, codeBuildServiceRole, repository, 'REGION', 'ACCOUNT_ID');
        const appBuildOutput = new codepipeline.Artifact();
        const appBuildAction = this.createAppCodeBuildAction(
            this, appSourceArtifact, appBuildOutput, pipelineProject, codeBuildServiceRole);
        const buildStage = pipeline.stage("Build");
        buildStage.addAction(appBuildAction);

        // This is where we add the application stages...
    }

    createAppSourceAction(appSourceArtifact: codepipeline.Artifact): codepipeline_actions.GitHubSourceAction {
        return new codepipeline_actions.GitHubSourceAction({
            actionName: 'GitHub-App-Source',
            output: appSourceArtifact,
            oauthToken: SecretValue.secretsManager('github-token'),
            owner: 'SOURCE-OWNER',
            repo: 'SOURCE-REPO',
        });
    }

    createCodeBuildServiceRole(scope: core.Construct, pipelineRole: iam.IRole): iam.Role {
        const role = new iam.Role(scope, 'CodeBuildServiceRole', {
            assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
        });
        role.assumeRolePolicy?.addStatements(new iam.PolicyStatement({
            sid: "PipelineAssumeCodeBuildServiceRole",
            effect: iam.Effect.ALLOW,
            actions: ["sts:AssumeRole"],
            principals: [pipelineRole]
        }));

        // Required policies to create an AWS CodeBuild service role
        role.addToPolicy(new iam.PolicyStatement({
            sid: "CloudWatchLogsPolicy",
            effect: iam.Effect.ALLOW,
            actions: [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            resources: ["*"]
        }));
        role.addToPolicy(new iam.PolicyStatement({
            sid: "CodeCommitPolicy",
            effect: iam.Effect.ALLOW,
            actions: ["codecommit:GitPull"],
            resources: ["*"]
        }));
        role.addToPolicy(new iam.PolicyStatement({
            sid: "S3GetObjectPolicy",
            effect: iam.Effect.ALLOW,
            actions: [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            resources: ["*"]
        }));
        role.addToPolicy(new iam.PolicyStatement({
            sid: "S3PutObjectPolicy",
            effect: iam.Effect.ALLOW,
            actions: [
                "s3:PutObject"
            ],
            resources: ["*"]
        }));
        role.addToPolicy(new iam.PolicyStatement({
            sid: "S3BucketIdentity",
            effect: iam.Effect.ALLOW,
            actions: [
                "s3:GetBucketAcl",
                "s3:GetBucketLocation"
            ],
            resources: ["*"]
        }));

        // This statement allows CodeBuild to upload Docker images to Amazon ECR repositories.
        // source: https://docs.aws.amazon.com/codebuild/latest/userguide/sample-docker.html#sample-docker-running
        role.addToPolicy(new iam.PolicyStatement({
            sid: "ECRUploadPolicy",
            effect: iam.Effect.ALLOW,
            actions: [
                "ecr:BatchCheckLayerAvailability",
                "ecr:CompleteLayerUpload",
                "ecr:GetAuthorizationToken",
                "ecr:InitiateLayerUpload",
                "ecr:PutImage",
                "ecr:UploadLayerPart"
            ],
            resources: ["*"]
        }));

        return role;
    }

    createApplicationRepository(scope: core.Construct, codeBuildServiceRole: iam.Role): ecr.Repository {
        const repository = new ecr.Repository(scope, 'Repository', {
            repositoryName: 'cdkpipelines-demo-image-repository'
        });
        repository.grantPullPush(codeBuildServiceRole);
        return repository;
    }

    createCodeBuildPipelineProject(scope: core.Construct,
                                   codeBuildServiceRole: iam.Role,
                                   repository: ecr.Repository,
                                   region: string,
                                   accountId: string): codebuild.PipelineProject {
        return new codebuild.PipelineProject(scope, 'BuildProject', {
            buildSpec: codebuild.BuildSpec.fromSourceFilename("buildspec.yml"),
            environment: {
                buildImage: codebuild.LinuxBuildImage.fromCodeBuildImageId("aws/codebuild/standard:4.0"),
                privileged: true,
                computeType: codebuild.ComputeType.SMALL,
                environmentVariables: {
                    AWS_DEFAULT_REGION: {value: region},
                    AWS_ACCOUNT_ID: {value: accountId},
                    IMAGE_REPO_NAME: {value: repository.repositoryName},
                    IMAGE_TAG: {value: "latest"},
                }
            },
            role: codeBuildServiceRole
        });
    }

    createAppCodeBuildAction(scope: core.Construct,
                             input: codepipeline.Artifact,
                             output: codepipeline.Artifact,
                             pipelineProject: codebuild.PipelineProject,
                             serviceRole: iam.Role) {
        return new codepipeline_actions.CodeBuildAction({
            actionName: "App-Build",
            checkSecretsInPlainTextEnvVariables: false,
            input: input,
            outputs: [output],
            project: pipelineProject,
            role: serviceRole,
            type: codepipeline_actions.CodeBuildActionType.BUILD,
        })
    }
}