Learn AWS CDK Pipeline

1. 什么是 AWS CDK Pipeline

AWS CDK Pipeline 是一个 CDK 的功能, 它可以让我们用 CDK 来定义一个 CI/CD Pipeline. 通常 CI/CD 都需要定义 Pipeline, 定义执行的 Terminal Command 的流程, 并且需要自己管理用于 CI/CD 的资源. 是有一些前期工作的.

而 CDK Pipeline 则是用 IAC 的方式自动生成这些用于 CI/CD 的 CodePipeline, CodeBuild Project 等资源. 通常的 CI/CD 工具允许开发者在 YAML 文件中修改 CI/CD 的逻辑, 但如果要修改用于 CI/CD 的 Infrastructure 本身还需要自己再部署一次. CDK 的牛逼之处的是它还能对用于 CI/CD 的资源进行自我更新.

CDK Pipeline 的理念是, 一切的部署都是 Infrastructure as Code. 由于 CDK 对很多原生的 CloudFormation 做了增强, 很多原生 CF 做不到的事情, 例如发布一个软件的新版本, 都可以通过 CDK 来实现. 在 CDK Pipeline 中, 它用一个 CodeBuild Project 来 build artifacts, 以及生成最终的 CloudFormation Template, 然后就直接调用 AWS CloudFormation 来部署了. 之后就再也不需要用 CodeBuild 了, 实现了部署阶段的并行执行.

由于 CDK Pipeline 不是一个传统意义上的 CI/CD 系统, 它不提供 Terminal Command 的编排, 运行时的编排等, 这些工作都是在 CodeBuild 中进行, 但是 CDK Pipeline 提供了一套语法允许你定义 CodeBuild 中的编排. 然后 CDK Pipeline 会自动更新这个 CodeBuild Project 然后在里面运行.

2. 什么时候该用 CDK Pipeline

如果你的部署任务可以通过 CDK 来完成, 例如发布一个 Lambda 版本, 发布一个 ECS Task 的版本等, 那么完全可以用 CDK Pipeline.

3. CDK Pipeline 的工作原理

这里我们来看一个使用 CDK Pipeline 从 CodeCommit 中拉取代码, 部署 Lambda Function 的例子. 本例的源代码都在 GitHub 上.

首先, 我们来了解一下 CDK Pipeline 作为一个 IAC 工具是如何实现 CI/CD 的. 前面我们介绍了 CI/CD 系统包括两个部分:

  1. CI/CD 系统的基础设施, 包括 CodeBuild Project 用于提供运行环境, CodePipeline 用于提供编排, S3 Bucket 用于存放 Artifacts, Event Rule 用来定义触发规则, 以及一些 IAM Role Policy 来管理权限.

  2. CI/CD 系统的具体运行逻辑, 由一堆 Terminal Command 和 CodePipeline Stage / Action 组成.

CDK Pipeline 能自动的创建 #1, 并且允许用户自定义 #2. 由于 CDK 本身使用 TypeScript 实现的, #1 的创建是由一堆 If Else 逻辑来自动生成的. 而 #2 的自定义是通过 CodePipeline 中的 Stage Action, 以及 CodeBuild Job 中的 Command 来实现的. 基本上实现了用户在其他 CI/CD 系统中使用的任何 Terminal Command 都可以在 CDK Pipeline 中实现.

然后我们来看一个标准的用 CDK Pipeline 开发和部署的流程, 示例代码我们之后再分析:

  1. 首先你需要用 CDK Pipeline 定义一个 Pipeline Construct, 然后用 cdk deploy 命令部署一次. 该次部署会将所有的 CI/CD 所需的基础设施都部署好. 这一步是必须的, 而且要在你部署真正的 App 应用之前做. 一旦部署好, 你对 Pipeline 本身的基础设施的定义的修改都会被这个 Pipeline 自己所更新.

  2. 接下来你就可以专注于开发你的 App 了, 然后你只要 push 代码到 CodeCommit, 这个 CodePipeline 都会自动运行来部署. 当然我们这里只用到了一个 branch, 仅仅是一个示例. 生产环境中会有很多个 branch, 这个我们在本文中不做讨论.

下面是一个 CDK Pipeline 的示例代码, 请仔细阅读里面的注释部分:

  1#!/usr/bin/env python3
  2
  3import aws_cdk as cdk
  4from aws_cdk.pipelines import (
  5    CodePipeline,
  6    CodePipelineSource,
  7    CodeBuildStep,
  8    ManualApprovalStep,
  9)
 10import aws_cdk.aws_iam as iam
 11import aws_cdk.aws_lambda as lambda_
 12import aws_cdk.aws_codecommit as codecommit
 13from constructs import Construct
 14
 15
 16# 定义了你的 Application Stack, 这里我们是一个 Lambda Function.
 17class MyLambdaStack(cdk.Stack):
 18    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
 19        super().__init__(scope, construct_id, **kwargs)
 20
 21        self.my_lambda_function = lambda_.Function(
 22            self,
 23            "LambdaFunction",
 24            function_name="learn_cdk_pipeline-my_lambda_function",
 25            runtime=lambda_.Runtime.NODEJS_18_X,
 26            handler="index.handler",
 27            # 这是一个 POC, 所以为了方便起见我们直接 Hard code 源代码
 28            code=lambda_.InlineCode("exports.handler = _ => 'Hello, CDK';"),
 29        )
 30
 31
 32# 将 Application Stack 包装成一个 Pipeline 中的 Stage
 33# 将 Application Stack 包装成一个 Pipeline 中的 Stage
 34# 这个 Stage 需要用 MyPipelineStack.CodePipeline.CodeBuildStep 里面创建的
 35# CloudFormation Template asset 来作为输入, 不过这个 CDK Pipeline 会帮你 handle
 36class MyPipelineAppStage(cdk.Stage):
 37    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
 38        super().__init__(scope, construct_id, **kwargs)
 39
 40        self.my_lambda_stack = MyLambdaStack(
 41            self,
 42            "LambdaStack",
 43            stack_name="learn-cdk-pipeline-my-lambda-stack",
 44        )
 45
 46
 47# 这就是 CDK Pipeline 的 Stack
 48class MyPipelineStack(cdk.Stack):
 49    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
 50        super().__init__(scope, construct_id, **kwargs)
 51
 52        # 这个 aws_cdk.pipelines.CodePipeline 跟 aws_cdk.aws_codepipeline.Pipeline 不同
 53        # 它是一个 CDK 团队开发的 Construct 里面包含了 Pipeline 运行所需的 Infrastructure
 54        self.pipeline = CodePipeline(
 55            self,
 56            "Pipeline",
 57            pipeline_name="learn-cdk-pipeline-my-pipeline",
 58            # 这一步最为关键, 它定义了构建部署所需的全部 CloudFormation 的逻辑
 59            # 为了灵活性, 我们一般在 CodeBuild 中来运行这些逻辑
 60            synth=CodeBuildStep(
 61                "Synth",
 62                project_name="learn-cdk-pipeline-my-pipeline-synth",
 63                # 这里定义了从哪里拉取源代码
 64                input=CodePipelineSource.code_commit(
 65                    codecommit.Repository.from_repository_name(
 66                        self,
 67                        "Repo",
 68                        repository_name="learn_cdk_codepipeline-project",
 69                    ),
 70                    branch="main",
 71                ),
 72                # 这里定义了 CodeBuild 的 IAM Role 需要的额外 Permission
 73                role_policy_statements=[
 74                    iam.PolicyStatement(
 75                        effect=iam.Effect.ALLOW,
 76                        actions=["*"],
 77                        resources=["*"],
 78                    )
 79                ],
 80                # 这里定义了 CodeBuild 的构建逻辑, 其中最关键的是 cdk synth
 81                # 你可以在它之前或者之后添加任意的构建逻辑
 82                install_commands=[
 83                    "npm install -g aws-cdk",
 84                    "python -m pip install -r requirements.txt",
 85                ],
 86                commands=[
 87                    "cdk synth",
 88                ],
 89                # 这个步骤会是会将 cdk 生成的 CloudFormation 打包成 Artifacts 的
 90                # 默认是 cdk.out 目录, 你也可以修改这个路径
 91                # primary_output_directory="/path/to/cdk.out",
 92            ),
 93        )
 94
 95        # 这一步定义了 Application Deployment 的 Stage 的逻辑, 其内容是我们之前用
 96        # Stage 包装好的 AppStack
 97        # 注意, 这里的 my_pipeline_app_stage_deployment 返回值不是 Stage 本身
 98        # 而是一个 StageDeployment 对象, 它有 add_pre, add_post 方法.
 99        self.my_pipeline_app_stage_deployment = self.pipeline.add_stage(
100            MyPipelineAppStage(
101                self,
102                "MyPipelineAppStage",
103                stage_name="Deploy-Lambda-Function",
104            )
105        )
106
107        # 你可以通过 add_pre, add_post 方法来添加任意的构建逻辑
108        # 我推荐使用 CodeBuildStep 来添加构建逻辑, 而不要用 ShellStep,
109        # 因为 ShellStep 本身也会被包装成 CodeBuildStep 来执行
110        # 注意每当你定义一个 CodeBuildStep 就会让 CodePipeline 创建一个新的 CodeBuild Project
111        # 这些 CodeBuild Project 的 Build job run 之间相互独立
112        # 如果你需要传递数据的话, 你需要用 CodePipeline 的 Artifact 来传递
113        self.my_pipeline_app_stage_deployment.add_pre(
114            CodeBuildStep(
115                "PreAppStageStep",
116                project_name="learn-cdk-pipeline-my-pipeline-pre-app-stage-step",
117                commands=[
118                    "echo 'PreAppStageStep'",
119                    "env",
120                ],
121            )
122        )
123        self.my_pipeline_app_stage_deployment.add_post(
124            ManualApprovalStep("Approval"),
125        )
126
127
128app = cdk.App()
129MyPipelineStack(app, "MyPipelineStack", stack_name="my-pipeline-stack")
130app.synth()

最后来看一下 CDK Pipeline 中关于 #1 的基础设施部分 (被自动创建的部分) 包含哪些内容:

这个是 CDK 的 Metadata, 不用管.

  • CDKMetadata (AWS::CDK::Metadata)

这个是 CodePipeline 以及它的 Service Role 的定义.

  • Pipeline9850B417 (AWS::CodePipeline::Pipeline)

  • PipelineRoleB27FAA37 (AWS::IAM::Role)

  • PipelineRoleDefaultPolicy7BDC1ABB (AWS::IAM::Policy)

这个是第一个 CodeBuild Project, 用来构建最终所需的 CloudFormation template. 这个 CodeBuild Project 最为重要, 你所需要的 CI/CD 的逻辑, 权限都在这里被定义. 跟其他 CI/CD 不同的事

  • PipelineBuildSynthCdkBuildProject6BEFA8E6 (AWS::CodeBuild::Project)

  • PipelineBuildSynthCdkBuildProjectRole231EEA2A (AWS::IAM::Role)

  • PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C (AWS::IAM::Policy)

这个是第二个 CodeBuild Project, 用来更新这个 CodePipeline 本身.

  • PipelineUpdatePipelineSelfMutationDAA41400 (AWS::CodeBuild::Project)

  • PipelineUpdatePipelineSelfMutationRole57E559E8 (AWS::IAM::Role)

  • PipelineUpdatePipelineSelfMutationRoleDefaultPolicyA225DA4E (AWS::IAM::Policy)

这是一个用来给 Pipeline Role 来 Assume 的 Role, 使得 Pipeline 能调用 CodeBuild 的 API 来运行 Build Job Run.

  • PipelineCodeBuildActionRole226DB0CB (AWS::IAM::Role)

  • PipelineCodeBuildActionRoleDefaultPolicy1D62A6FE (AWS::IAM::Policy)

这是一个用来给 AWS Account 来 Assume, 允许从 CodeCommit check out 源代码的 Role:

  • PipelineSourcelearncdkcodepipelineprojectCodePipelineActionRole3D2ABC00 (AWS::IAM::Role)

  • PipelineSourcelearncdkcodepipelineprojectCodePipelineActionRoleDefaultPolicy9C23C694 (AWS::IAM::Policy)

这是一个用来给 CodePipeline Approval Action 的 Role, 我研究不深, 还不是很清楚:

  • PipelineDeployLambdaFunctionApprovalCodePipelineActionRoleFA32B3F2 (AWS::IAM::Role)

这是 CodePipeline 用来监控 CodeCommit 的 Event Rule:

  • RepoMyPipelineStackPipeline61E383C6mainEventRule0B20E01C (AWS::Events::Rule)

  • PipelineEventsRole96280D9B (AWS::IAM::Role)

  • PipelineEventsRoleDefaultPolicy62809D8F (AWS::IAM::Policy)

这是用来保存 Artifacts 的 S3 Bucket:

  • PipelineArtifactsBucketAEA9A052 (AWS::S3::Bucket)

  • PipelineArtifactsBucketPolicyF53CCC52 (AWS::S3::BucketPolicy)

4. Multi Branch 情况下如何使用 CDK Pipeline

  1. 由于一个 CodePipeline 只能监控一个 Branch, 所以如果你想要为许多名字不可预测的 Branch 来创建 CDK Pipeline, 那么我们在本地第一次运行 cdk deploy "*" 这个动作就需要在每次创建新的符合规则的 Branch 时进行. 我们可以为 CodeCommit 创建一个 Event Rule, 然后监控创建和删除特定的 Branch 的事件, 然后用 Lambda Function 来执行 cdk deploy "*" 命令.

  2. 由于每个 Branch 所对应的 CDK Pipeline 都会创建 20 多个的资源, 很容易让 Bucket, CodeProject, CodePipeline 的数量爆炸. 所以在 Branch 被删除后, 如何清理掉 App Stack 以及 Pipeline Stack 就需要小心设计. 这个 Lambda 需要先删除 App Stack, 然后再删除 Pipeline Stack.

Reference