使用 CDK 管线进行持续集成和交付(CI/CD) - AWS Cloud Development Kit (AWS CDK) v2

这是 AWS CDK v2 开发者指南。旧版 CDK v1 于 2022 年 6 月 1 日进入维护阶段,并于 2023 年 6 月 1 日终止支持。

使用 CDK 管线进行持续集成和交付(CI/CD)

使用 AWS 构造库中的 CDK 管线模块,配置 AWS CDK 应用程序的持续交付。当您将 CDK 应用程序的源代码提交到 AWS CodeCommit、GitHub 或 AWS CodeStar 中时,CDK 管线可以自动构建、测试和部署新版本。

CDK 管线会自行更新。如果您添加了应用程序阶段或堆栈,则该管线会自动对自身重新配置,以部署这些新阶段或堆栈。

注意

CDK 管线支持两个 API。一个是在 CDK 管线开发人员预览版中提供的原始 API。另一个是现代 API,它包含了在预览阶段收到的 CDK 客户的反馈。本主题中的示例使用该现代 API。有关两个受支持的 API 之间的区别的详细信息,请参阅 aws-cdk GitHub 存储库 中的 CDK 管线原始 API

引导 AWS 环境

在使用 CDK 管线之前,必须先引导要将堆栈部署到其中的 AWS 环境

CDK 管线至少涉及两个环境。第一个环境用于预置管线。第二个环境用于部署应用程序的堆栈或阶段(阶段是相关堆栈的组)。这些环境可能相同,但最佳实践建议是在不同的环境中将各个阶段彼此隔离开来。

注意

有关通过引导创建的资源的类型以及如何自定义引导堆栈的更多信息,请参阅AWS CDK 引导

使用 CDK 管线进行持续部署需要在 CDK Toolkit 堆栈中包含以下内容:

  • Amazon Simple Storage Service (Amazon S3) 存储桶。

  • Amazon ECR 存储库。

  • IAM 角色,用于为管线的各个部分提供所需的权限。

如有必要,CDK Toolkit 将升级现有的引导堆栈或创建一个新的引导堆栈。

要引导可以预置 AWS CDK 管线的环境,请按如下示例调用 cdk bootstrap。如有必要,通过 npx 命令调用 AWS CDK Toolkit 会临时安装它。它还将使用当前项目中安装的 Toolkit 版本(如有)。

--cloudformation-execution-policies 指定将来执行 CDK 管线部署所依据的策略的 ARN。默认 AdministratorAccess 策略可确保管线可以部署所有类型的 AWS 资源。如果您使用此策略,请确保您信任构成 AWS CDK 应用程序的所有代码和依赖关系。

大多数组织都要求对自动化可以部署哪些类型的资源进行更严格的控制。请咨询组织内的相应部门,以确定您的管线应使用的策略。

如果默认 AWS 配置文件包含必要的身份验证配置和 AWS 区域,则可以省略 --profile 选项。

macOS/Linux
npx cdk bootstrap aws://ACCOUNT-NUMBER/REGION --profile ADMIN-PROFILE \ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
Windows
npx cdk bootstrap aws://ACCOUNT-NUMBER/REGION --profile ADMIN-PROFILE ^ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess

要引导管线要将 AWS CDK 应用程序部署到的其他环境,请改用以下命令。--trust 选项指示哪个其他账户应有权将 AWS CDK 应用程序部署到此环境中。对于此选项,请指定管线的 AWS 账户 ID。

同样,如果默认 AWS 配置文件包含必要的身份验证配置和 AWS 区域,则可以省略 --profile 选项。

macOS/Linux
npx cdk bootstrap aws://ACCOUNT-NUMBER/REGION --profile ADMIN-PROFILE \ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \ --trust PIPELINE-ACCOUNT-NUMBER
Windows
npx cdk bootstrap aws://ACCOUNT-NUMBER/REGION --profile ADMIN-PROFILE ^ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess ^ --trust PIPELINE-ACCOUNT-NUMBER
提示

仅使用管理凭证引导和预置初始管线。之后,使用管线本身而非本地机器部署更改。

如果要升级已引导的旧版环境,则在创建新存储桶时,之前的 Amazon S3 存储桶将被孤立出来。使用 Amazon S3 控制台将其手动删除。

保护引导堆栈不被删除

如果引导堆栈被删除,则最初在环境中为支持 CDK 部署而预置的 AWS 资源也将被删除。这将导致管线停止工作。如果发生这种情况,没有通用的恢复解决方案。

引导环境后,请勿删除并重新创建环境的引导堆栈。而应尝试通过再次运行 cdk bootstrap 命令将引导堆栈更新到新版本。

为了防止引导堆栈被意外删除,我们建议您在 cdk bootstrap 命令中提供 --termination-protection 选项来启用终止保护。您可以在新的或现有的引导堆栈上启用终止保护。要了解有关该选项的更多信息,请参阅 --termination-protection

启用终止保护后,您可以使用 AWS CLI 或 CloudFormation 控制台进行验证。

启用终止保护
  1. 运行以下命令可在新的或现有的引导堆栈上启用终止保护:

    $ cdk bootstrap --termination-protection
  2. 使用 AWS CLI 或 CloudFormation 控制台进行验证。以下是一个使用 AWS CLI 的示例。如果您修改了引导堆栈名称,请将 CDKToolkit 替换为堆栈名称:

    $ aws cloudformation describe-stacks --stack-name CDKToolkit --query "Stacks[0].EnableTerminationProtection" true

初始化项目

创建一个新的空 GitHub 项目,然后将其克隆到工作站的 my-pipeline 目录中。(我们在本主题中的代码示例使用 GitHub。您也可以使用 AWS CodeStar 或 AWS CodeCommit。)

git clone GITHUB-CLONE-URL my-pipeline cd my-pipeline
注意

您可以使用 my-pipeline 之外的名称命名应用程序主目录。但是,如果这样做,则必须在本主题的后面部分调整文件和类名。这是因为 AWS CDK Toolkit 的某些文件和类名以主目录的名称为依据。

克隆后,照常初始化项目。

TypeScript
$ cdk init app --language typescript
JavaScript
$ cdk init app --language javascript
Python
$ cdk init app --language python

创建应用程序后,还应输入以下两条命令。它们会激活应用程序的 Python 虚拟环境并安装 AWS CDK 核心依赖关系。

$ source .venv/bin/activate # On Windows, run `.\venv\Scripts\activate` instead $ python -m pip install -r requirements.txt
Java
$ cdk init app --language java

如果您使用的是 IDE,现在可以打开或导入项目了。例如,在 Eclipse 中,依次选择文件 > 导入 > Maven > 现有的 Maven 项目。确保将项目设置设为使用 Java 8(1.8)。

C#
$ cdk init app --language csharp

如果您使用的是 Visual Studio,请在 src 目录中打开解决方案文件。

Go
$ cdk init app --language go

创建应用程序后,还应输入以下命令,以安装应用程序所需的 AWS 构造库模块。

$ go get
重要

请务必提交 cdk.jsoncdk.context.json 文件以进行源代码控制。上下文信息(例如从 AWS 账户中检索的功能标志和缓存值)是项目状态的一部分。在其他环境中,这些值可能会有所不同,这可能会导致结果发生意外变化。有关更多信息,请参阅 上下文值和 AWS CDK

定义管线

CDK 管线应用程序将包含至少两个堆栈:一个代表管线本身,剩下的一个或多个堆栈代表通过管线部署的应用程序。堆栈也可以分为几个阶段,您可以使用这些阶段将基础设施堆栈的副本部署到不同的环境中。现在,我们将探讨管线,稍后再深入研究它将部署的应用程序。

构造 CodePipeline 是表示将 AWS CodePipeline 用作其部署引擎的 CDK 管线的构造。在堆栈中实例化 CodePipeline 时,可以定义管线的源位置(例如 GitHub 存储库)。您还可以定义用于构建应用程序的命令。

例如,以下命令会定义一个管线,其源存储在 GitHub 存储库中。它还包含 TypeScript CDK 应用程序的构建步骤。在指示的地方填写有关 GitHub 存储库的信息。

注意

管线默认使用存储在 Secrets Manager 中名称 github-token 下的个人访问令牌对 GitHub 进行身份验证。

您还需要更新管线堆栈的实例化以指定 AWS 账户和区域。

TypeScript

lib/my-pipeline-stack.ts 中(如果项目文件夹名称不为 my-pipeline,可能有所不同):

import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines'; export class MyPipelineStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); } }

bin/my-pipeline.ts 中(如果项目文件夹名称不为 my-pipeline,可能有所不同):

#!/usr/bin/env node import * as cdk from 'aws-cdk-lib'; import { MyPipelineStack } from '../lib/my-pipeline-stack'; const app = new cdk.App(); new MyPipelineStack(app, 'MyPipelineStack', { env: { account: '111111111111', region: 'eu-west-1', } }); app.synth();
JavaScript

lib/my-pipeline-stack.js 中(如果项目文件夹名称不为 my-pipeline,可能有所不同):

const cdk = require('aws-cdk-lib'); const { CodePipeline, CodePipelineSource, ShellStep } = require('aws-cdk-lib/pipelines'); class MyPipelineStack extends cdk.Stack { constructor(scope, id, props) { super(scope, id, props); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); } } module.exports = { MyPipelineStack }

bin/my-pipeline.js 中(如果项目文件夹名称不为 my-pipeline,可能有所不同):

#!/usr/bin/env node const cdk = require('aws-cdk-lib'); const { MyPipelineStack } = require('../lib/my-pipeline-stack'); const app = new cdk.App(); new MyPipelineStack(app, 'MyPipelineStack', { env: { account: '111111111111', region: 'eu-west-1', } }); app.synth();
Python

my-pipeline/my-pipeline-stack.py 中(如果项目文件夹名称不为 my-pipeline,可能有所不同):

import aws_cdk as cdk from constructs import Construct from aws_cdk.pipelines import CodePipeline, CodePipelineSource, ShellStep class MyPipelineStack(cdk.Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) pipeline = CodePipeline(self, "Pipeline", pipeline_name="MyPipeline", synth=ShellStep("Synth", input=CodePipelineSource.git_hub("OWNER/REPO", "main"), commands=["npm install -g aws-cdk", "python -m pip install -r requirements.txt", "cdk synth"] ) )

In app.py:

#!/usr/bin/env python3 import aws_cdk as cdk from my_pipeline.my_pipeline_stack import MyPipelineStack app = cdk.App() MyPipelineStack(app, "MyPipelineStack", env=cdk.Environment(account="111111111111", region="eu-west-1") ) app.synth()
Java

src/main/java/com/myorg/MyPipelineStack.java 中(如果项目文件夹名称不为 my-pipeline,可能有所不同):

package com.myorg; import java.util.Arrays; import software.constructs.Construct; import software.amazon.awscdk.Stack; import software.amazon.awscdk.StackProps; import software.amazon.awscdk.pipelines.CodePipeline; import software.amazon.awscdk.pipelines.CodePipelineSource; import software.amazon.awscdk.pipelines.ShellStep; public class MyPipelineStack extends Stack { public MyPipelineStack(final Construct scope, final String id) { this(scope, id, null); } public MyPipelineStack(final Construct scope, final String id, final StackProps props) { super(scope, id, props); CodePipeline pipeline = CodePipeline.Builder.create(this, "pipeline") .pipelineName("MyPipeline") .synth(ShellStep.Builder.create("Synth") .input(CodePipelineSource.gitHub("OWNER/REPO", "main")) .commands(Arrays.asList("npm install -g aws-cdk", "cdk synth")) .build()) .build(); } }

src/main/java/com/myorg/MyPipelineApp.java 中(如果项目文件夹名称不为 my-pipeline,可能有所不同):

package com.myorg; import software.amazon.awscdk.App; import software.amazon.awscdk.Environment; import software.amazon.awscdk.StackProps; public class MyPipelineApp { public static void main(final String[] args) { App app = new App(); new MyPipelineStack(app, "PipelineStack", StackProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build()); app.synth(); } }
C#

src/MyPipeline/MyPipelineStack.cs 中(如果项目文件夹名称不为 my-pipeline,可能有所不同):

using Amazon.CDK; using Amazon.CDK.Pipelines; namespace MyPipeline { public class MyPipelineStack : Stack { internal MyPipelineStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) { var pipeline = new CodePipeline(this, "pipeline", new CodePipelineProps { PipelineName = "MyPipeline", Synth = new ShellStep("Synth", new ShellStepProps { Input = CodePipelineSource.GitHub("OWNER/REPO", "main"), Commands = new string[] { "npm install -g aws-cdk", "cdk synth" } }) }); } } }

src/MyPipeline/Program.cs 中(如果项目文件夹名称不为 my-pipeline,可能有所不同):

using Amazon.CDK; namespace MyPipeline { sealed class Program { public static void Main(string[] args) { var app = new App(); new MyPipelineStack(app, "MyPipelineStack", new StackProps { Env = new Amazon.CDK.Environment { Account = "111111111111", Region = "eu-west-1" } }); app.Synth(); } } }
Go
package main import ( "github.com/aws/aws-cdk-go/awscdk/v2" codebuild "github.com/aws/aws-cdk-go/awscdk/v2/awscodebuild" ssm "github.com/aws/aws-cdk-go/awscdk/v2/awsssm" pipeline "github.com/aws/aws-cdk-go/awscdk/v2/pipelines" "github.com/aws/constructs-go/constructs/v10" "github.com/aws/jsii-runtime-go" "os" ) // my CDK Stack with resources func NewCdkStack(scope constructs.Construct, id *string, props *awscdk.StackProps) awscdk.Stack { stack := awscdk.NewStack(scope, id, props) // create an example ssm parameter _ = ssm.NewStringParameter(stack, jsii.String("ssm-test-param"), &ssm.StringParameterProps{ ParameterName: jsii.String("/testparam"), Description: jsii.String("ssm parameter for demo"), StringValue: jsii.String("my test param"), }) return stack } // my CDK Application func NewCdkApplication(scope constructs.Construct, id *string, props *awscdk.StageProps) awscdk.Stage { stage := awscdk.NewStage(scope, id, props) _ = NewCdkStack(stage, jsii.String("cdk-stack"), &awscdk.StackProps{Env: props.Env}) return stage } // my CDK Pipeline func NewCdkPipeline(scope constructs.Construct, id *string, props *awscdk.StackProps) awscdk.Stack { stack := awscdk.NewStack(scope, id, props) // GitHub repo with owner and repository name githubRepo := pipeline.CodePipelineSource_GitHub(jsii.String("owner/repo"), jsii.String("main"), &pipeline.GitHubSourceOptions{ Authentication: awscdk.SecretValue_SecretsManager(jsii.String("my-github-token"), nil), }) // self mutating pipeline myPipeline := pipeline.NewCodePipeline(stack, jsii.String("cdkPipeline"), &pipeline.CodePipelineProps{ PipelineName: jsii.String("CdkPipeline"), // self mutation true - pipeline changes itself before application deployment SelfMutation: jsii.Bool(true), CodeBuildDefaults: &pipeline.CodeBuildOptions{ BuildEnvironment: &codebuild.BuildEnvironment{ // image version 6.0 recommended for newer go version BuildImage: codebuild.LinuxBuildImage_FromCodeBuildImageId(jsii.String("aws/codebuild/standard:6.0")), }, }, Synth: pipeline.NewCodeBuildStep(jsii.String("Synth"), &pipeline.CodeBuildStepProps{ Input: githubRepo, Commands: &[]*string{ jsii.String("npm install -g aws-cdk"), jsii.String("cdk synth"), }, }), }) // deployment of actual CDK application myPipeline.AddStage(NewCdkApplication(stack, jsii.String("MyApplication"), &awscdk.StageProps{ Env: targetAccountEnv(), }), &pipeline.AddStageOpts{ Post: &[]pipeline.Step{ pipeline.NewCodeBuildStep(jsii.String("Manual Steps"), &pipeline.CodeBuildStepProps{ Commands: &[]*string{ jsii.String("echo \"My CDK App deployed, manual steps go here ... \""), }, }), }, }) return stack } // main app func main() { defer jsii.Close() app := awscdk.NewApp(nil) // call CDK Pipeline NewCdkPipeline(app, jsii.String("CdkPipelineStack"), &awscdk.StackProps{ Env: pipelineEnv(), }) app.Synth(nil) } // env determines the AWS environment (account+region) in which our stack is to // be deployed. For more information see: https://docs.aws.amazon.com/cdk/latest/guide/environments.html func pipelineEnv() *awscdk.Environment { return &awscdk.Environment{ Account: jsii.String(os.Getenv("CDK_DEFAULT_ACCOUNT")), Region: jsii.String(os.Getenv("CDK_DEFAULT_REGION")), } } func targetAccountEnv() *awscdk.Environment { return &awscdk.Environment{ Account: jsii.String(os.Getenv("CDK_DEFAULT_ACCOUNT")), Region: jsii.String(os.Getenv("CDK_DEFAULT_REGION")), } }

您必须手动部署管线一次。之后,管线会通过源代码存储库自行保持最新状态。因此,请确保存储库中的代码是您要部署的代码。签入更改并推送到 GitHub,然后部署:

git add --all git commit -m "initial commit" git push cdk deploy
提示

现在您已经完成了初始部署,您的本地 AWS 账户不再需要管理权限。这是因为对应用程序的所有更改都将通过管线进行部署。您所需要做的就是推送到 GitHub。

应用程序阶段

要定义可以一次性添加到管线中的多堆栈 AWS 应用程序,请定义 Stage 的子类。(这与 CDK 管线模块 CdkStage 中的不同。)

阶段包含组成应用程序的堆栈。如果堆栈之间存在依赖关系,则堆栈将按正确的顺序自动添加到管线中。相互不依赖的堆栈将并行部署。您可以通过调用 stack1.addDependency(stack2) 在堆栈之间添加依赖关系。

阶段接受默认 env 参数,该参数将成为阶段中的堆栈的默认环境。(堆栈仍然可以指定自己的环境。)

通过调用 addStage() 并使用 Stage 实例,可将应用程序添加到管线中。可以多次实例化一个阶段并将其添加到管线中,以定义 DTAP 或多区域应用程序管线的不同阶段。

我们将创建一个包含简单 Lambda 函数的堆栈,并将该堆栈放在一个阶段中。然后,我们将向管线中添加阶段,以便可以部署管线。

TypeScript

创建新文件 lib/my-pipeline-lambda-stack.ts,以存放包含 Lambda 函数的应用程序堆栈。

import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { Function, InlineCode, Runtime } from 'aws-cdk-lib/aws-lambda'; export class MyLambdaStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); new Function(this, 'LambdaFunction', { runtime: Runtime.NODEJS_18_X, handler: 'index.handler', code: new InlineCode('exports.handler = _ => "Hello, CDK";') }); } }

创建新文件 lib/my-pipeline-app-stage.ts 以容纳阶段。

import * as cdk from 'aws-cdk-lib'; import { Construct } from "constructs"; import { MyLambdaStack } from './my-pipeline-lambda-stack'; export class MyPipelineAppStage extends cdk.Stage { constructor(scope: Construct, id: string, props?: cdk.StageProps) { super(scope, id, props); const lambdaStack = new MyLambdaStack(this, 'LambdaStack'); } }

编辑 lib/my-pipeline-stack.ts 以将阶段添加到管线中。

import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines'; import { MyPipelineAppStage } from './my-pipeline-app-stage'; export class MyPipelineStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); pipeline.addStage(new MyPipelineAppStage(this, "test", { env: { account: "111111111111", region: "eu-west-1" } })); } }
JavaScript

创建新文件 lib/my-pipeline-lambda-stack.js,以存放包含 Lambda 函数的应用程序堆栈。

const cdk = require('aws-cdk-lib'); const { Function, InlineCode, Runtime } = require('aws-cdk-lib/aws-lambda'); class MyLambdaStack extends cdk.Stack { constructor(scope, id, props) { super(scope, id, props); new Function(this, 'LambdaFunction', { runtime: Runtime.NODEJS_18_X, handler: 'index.handler', code: new InlineCode('exports.handler = _ => "Hello, CDK";') }); } } module.exports = { MyLambdaStack }

创建新文件 lib/my-pipeline-app-stage.js 以容纳阶段。

const cdk = require('aws-cdk-lib'); const { MyLambdaStack } = require('./my-pipeline-lambda-stack'); class MyPipelineAppStage extends cdk.Stage { constructor(scope, id, props) { super(scope, id, props); const lambdaStack = new MyLambdaStack(this, 'LambdaStack'); } } module.exports = { MyPipelineAppStage };

编辑 lib/my-pipeline-stack.ts 以将阶段添加到管线中。

const cdk = require('aws-cdk-lib'); const { CodePipeline, CodePipelineSource, ShellStep } = require('aws-cdk-lib/pipelines'); const { MyPipelineAppStage } = require('./my-pipeline-app-stage'); class MyPipelineStack extends cdk.Stack { constructor(scope, id, props) { super(scope, id, props); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); pipeline.addStage(new MyPipelineAppStage(this, "test", { env: { account: "111111111111", region: "eu-west-1" } })); } } module.exports = { MyPipelineStack }
Python

创建新文件 my_pipeline/my_pipeline_lambda_stack.py,以存放包含 Lambda 函数的应用程序堆栈。

import aws_cdk as cdk from constructs import Construct from aws_cdk.aws_lambda import Function, InlineCode, Runtime class MyLambdaStack(cdk.Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) Function(self, "LambdaFunction", runtime=Runtime.NODEJS_18_X, handler="index.handler", code=InlineCode("exports.handler = _ => 'Hello, CDK';") )

创建新文件 my_pipeline/my_pipeline_app_stage.py 以容纳阶段。

import aws_cdk as cdk from constructs import Construct from my_pipeline.my_pipeline_lambda_stack import MyLambdaStack class MyPipelineAppStage(cdk.Stage): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) lambdaStack = MyLambdaStack(self, "LambdaStack")

编辑 my_pipeline/my-pipeline-stack.py 以将阶段添加到管线中。

import aws_cdk as cdk from constructs import Construct from aws_cdk.pipelines import CodePipeline, CodePipelineSource, ShellStep from my_pipeline.my_pipeline_app_stage import MyPipelineAppStage class MyPipelineStack(cdk.Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) pipeline = CodePipeline(self, "Pipeline", pipeline_name="MyPipeline", synth=ShellStep("Synth", input=CodePipelineSource.git_hub("OWNER/REPO", "main"), commands=["npm install -g aws-cdk", "python -m pip install -r requirements.txt", "cdk synth"])) pipeline.add_stage(MyPipelineAppStage(self, "test", env=cdk.Environment(account="111111111111", region="eu-west-1")))
Java

创建新文件 src/main/java/com.myorg/MyPipelineLambdaStack.java,以存放包含 Lambda 函数的应用程序堆栈。

package com.myorg; import software.constructs.Construct; import software.amazon.awscdk.Stack; import software.amazon.awscdk.StackProps; import software.amazon.awscdk.services.lambda.Function; import software.amazon.awscdk.services.lambda.Runtime; import software.amazon.awscdk.services.lambda.InlineCode; public class MyPipelineLambdaStack extends Stack { public MyPipelineLambdaStack(final Construct scope, final String id) { this(scope, id, null); } public MyPipelineLambdaStack(final Construct scope, final String id, final StackProps props) { super(scope, id, props); Function.Builder.create(this, "LambdaFunction") .runtime(Runtime.NODEJS_18_X) .handler("index.handler") .code(new InlineCode("exports.handler = _ => 'Hello, CDK';")) .build(); } }

创建新文件 src/main/java/com.myorg/MyPipelineAppStage.java 以容纳阶段。

package com.myorg; import software.constructs.Construct; import software.amazon.awscdk.Stack; import software.amazon.awscdk.Stage; import software.amazon.awscdk.StageProps; public class MyPipelineAppStage extends Stage { public MyPipelineAppStage(final Construct scope, final String id) { this(scope, id, null); } public MyPipelineAppStage(final Construct scope, final String id, final StageProps props) { super(scope, id, props); Stack lambdaStack = new MyPipelineLambdaStack(this, "LambdaStack"); } }

编辑 src/main/java/com.myorg/MyPipelineStack.java 以将阶段添加到管线中。

package com.myorg; import java.util.Arrays; import software.constructs.Construct; import software.amazon.awscdk.Environment; import software.amazon.awscdk.Stack; import software.amazon.awscdk.StackProps; import software.amazon.awscdk.StageProps; import software.amazon.awscdk.pipelines.CodePipeline; import software.amazon.awscdk.pipelines.CodePipelineSource; import software.amazon.awscdk.pipelines.ShellStep; public class MyPipelineStack extends Stack { public MyPipelineStack(final Construct scope, final String id) { this(scope, id, null); } public MyPipelineStack(final Construct scope, final String id, final StackProps props) { super(scope, id, props); final CodePipeline pipeline = CodePipeline.Builder.create(this, "pipeline") .pipelineName("MyPipeline") .synth(ShellStep.Builder.create("Synth") .input(CodePipelineSource.gitHub("OWNER/REPO", "main")) .commands(Arrays.asList("npm install -g aws-cdk", "cdk synth")) .build()) .build(); pipeline.addStage(new MyPipelineAppStage(this, "test", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build())); } }
C#

创建新文件 src/MyPipeline/MyPipelineLambdaStack.cs,以存放包含 Lambda 函数的应用程序堆栈。

using Amazon.CDK; using Constructs; using Amazon.CDK.AWS.Lambda; namespace MyPipeline { class MyPipelineLambdaStack : Stack { public MyPipelineLambdaStack(Construct scope, string id, StackProps props=null) : base(scope, id, props) { new Function(this, "LambdaFunction", new FunctionProps { Runtime = Runtime.NODEJS_18_X, Handler = "index.handler", Code = new InlineCode("exports.handler = _ => 'Hello, CDK';") }); } } }

创建新文件 src/MyPipeline/MyPipelineAppStage.cs 以容纳阶段。

using Amazon.CDK; using Constructs; namespace MyPipeline { class MyPipelineAppStage : Stage { public MyPipelineAppStage(Construct scope, string id, StageProps props=null) : base(scope, id, props) { Stack lambdaStack = new MyPipelineLambdaStack(this, "LambdaStack"); } } }

编辑 src/MyPipeline/MyPipelineStack.cs 以将阶段添加到管线中。

using Amazon.CDK; using Constructs; using Amazon.CDK.Pipelines; namespace MyPipeline { public class MyPipelineStack : Stack { internal MyPipelineStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) { var pipeline = new CodePipeline(this, "pipeline", new CodePipelineProps { PipelineName = "MyPipeline", Synth = new ShellStep("Synth", new ShellStepProps { Input = CodePipelineSource.GitHub("OWNER/REPO", "main"), Commands = new string[] { "npm install -g aws-cdk", "cdk synth" } }) }); pipeline.AddStage(new MyPipelineAppStage(this, "test", new StageProps { Env = new Environment { Account = "111111111111", Region = "eu-west-1" } })); } } }

addStage() 添加的每个应用程序阶段都会导致添加一个相应的管线阶段,该阶段由 addStage()调用返回的 StageDeployment 实例表示。您可以通过调用其 addPre()addPost() 方法向阶段添加部署前或部署后操作。

TypeScript
// import { ManualApprovalStep } from 'aws-cdk-lib/pipelines'; const testingStage = pipeline.addStage(new MyPipelineAppStage(this, 'testing', { env: { account: '111111111111', region: 'eu-west-1' } })); testingStage.addPost(new ManualApprovalStep('approval'));
JavaScript
// const { ManualApprovalStep } = require('aws-cdk-lib/pipelines'); const testingStage = pipeline.addStage(new MyPipelineAppStage(this, 'testing', { env: { account: '111111111111', region: 'eu-west-1' } })); testingStage.addPost(new ManualApprovalStep('approval'));
Python
# from aws_cdk.pipelines import ManualApprovalStep testing_stage = pipeline.add_stage(MyPipelineAppStage(self, "testing", env=cdk.Environment(account="111111111111", region="eu-west-1"))) testing_stage.add_post(ManualApprovalStep('approval'))
Java
// import software.amazon.awscdk.pipelines.StageDeployment; // import software.amazon.awscdk.pipelines.ManualApprovalStep; StageDeployment testingStage = pipeline.addStage(new MyPipelineAppStage(this, "test", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build())); testingStage.addPost(new ManualApprovalStep("approval"));
C#
var testingStage = pipeline.AddStage(new MyPipelineAppStage(this, "test", new StageProps { Env = new Environment { Account = "111111111111", Region = "eu-west-1" } })); testingStage.AddPost(new ManualApprovalStep("approval"));

您可以向 Wave 添加阶段以并行部署它们,例如,将阶段部署到多个账户或区域时。

TypeScript
const wave = pipeline.addWave('wave'); wave.addStage(new MyApplicationStage(this, 'MyAppEU', { env: { account: '111111111111', region: 'eu-west-1' } })); wave.addStage(new MyApplicationStage(this, 'MyAppUS', { env: { account: '111111111111', region: 'us-west-1' } }));
JavaScript
const wave = pipeline.addWave('wave'); wave.addStage(new MyApplicationStage(this, 'MyAppEU', { env: { account: '111111111111', region: 'eu-west-1' } })); wave.addStage(new MyApplicationStage(this, 'MyAppUS', { env: { account: '111111111111', region: 'us-west-1' } }));
Python
wave = pipeline.add_wave("wave") wave.add_stage(MyApplicationStage(self, "MyAppEU", env=cdk.Environment(account="111111111111", region="eu-west-1"))) wave.add_stage(MyApplicationStage(self, "MyAppUS", env=cdk.Environment(account="111111111111", region="us-west-1")))
Java
// import software.amazon.awscdk.pipelines.Wave; final Wave wave = pipeline.addWave("wave"); wave.addStage(new MyPipelineAppStage(this, "MyAppEU", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build())); wave.addStage(new MyPipelineAppStage(this, "MyAppUS", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("us-west-1") .build()) .build()));
C#
var wave = pipeline.AddWave("wave"); wave.AddStage(new MyPipelineAppStage(this, "MyAppEU", new StageProps { Env = new Environment { Account = "111111111111", Region = "eu-west-1" } })); wave.AddStage(new MyPipelineAppStage(this, "MyAppUS", new StageProps { Env = new Environment { Account = "111111111111", Region = "us-west-1" } }));

测试部署

您可以向 CDK 管线添加步骤,以验证正在执行的部署。例如,您可以使用 CDK 管线库的 ShellStep 执行以下任务:

  • 尝试访问由 Lambda 函数支持的新部署的 Amazon API Gateway

  • 通过发出 AWS CLI 命令检查已部署资源的设置

添加验证操作的最简单格式如下所示:

TypeScript
// stage was returned by pipeline.addStage stage.addPost(new ShellStep("validate", { commands: ['../tests/validate.sh'], }));
JavaScript
// stage was returned by pipeline.addStage stage.addPost(new ShellStep("validate", { commands: ['../tests/validate.sh'], }));
Python
# stage was returned by pipeline.add_stage stage.add_post(ShellStep("validate", commands=[''../tests/validate.sh''] ))
Java
// stage was returned by pipeline.addStage stage.addPost(ShellStep.Builder.create("validate") .commands(Arrays.asList("'../tests/validate.sh'")) .build());
C#
// stage was returned by pipeline.addStage stage.AddPost(new ShellStep("validate", new ShellStepProps { Commands = new string[] { "'../tests/validate.sh'" } }));

许多 AWS CloudFormation 部署都会导致生成名称不可预测的资源。因此,CDK 管线提供了一种在部署后读取 AWS CloudFormation 输出的方法。这使得将负载均衡器生成的 URL 传递给测试操作成为可能。

要使用输出,请公开您感兴趣的 CfnOutput 对象。然后,将其传递到步骤的 envFromCfnOutputs 属性中,使其可作为该步骤中的环境变量。

TypeScript
// given a stack lbStack that exposes a load balancer construct as loadBalancer this.loadBalancerAddress = new cdk.CfnOutput(lbStack, 'LbAddress', { value: `https://${lbStack.loadBalancer.loadBalancerDnsName}/` }); // pass the load balancer address to a shell step stage.addPost(new ShellStep("lbaddr", { envFromCfnOutputs: {lb_addr: lbStack.loadBalancerAddress}, commands: ['echo $lb_addr'] }));
JavaScript
// given a stack lbStack that exposes a load balancer construct as loadBalancer this.loadBalancerAddress = new cdk.CfnOutput(lbStack, 'LbAddress', { value: `https://${lbStack.loadBalancer.loadBalancerDnsName}/` }); // pass the load balancer address to a shell step stage.addPost(new ShellStep("lbaddr", { envFromCfnOutputs: {lb_addr: lbStack.loadBalancerAddress}, commands: ['echo $lb_addr'] }));
Python
# given a stack lb_stack that exposes a load balancer construct as load_balancer self.load_balancer_address = cdk.CfnOutput(lb_stack, "LbAddress", value=f"https://{lb_stack.load_balancer.load_balancer_dns_name}/") # pass the load balancer address to a shell step stage.add_post(ShellStep("lbaddr", env_from_cfn_outputs={"lb_addr": lb_stack.load_balancer_address} commands=["echo $lb_addr"]))
Java
// given a stack lbStack that exposes a load balancer construct as loadBalancer loadBalancerAddress = CfnOutput.Builder.create(lbStack, "LbAddress") .value(String.format("https://%s/", lbStack.loadBalancer.loadBalancerDnsName)) .build(); stage.addPost(ShellStep.Builder.create("lbaddr") .envFromCfnOutputs( // Map.of requires Java 9 or later java.util.Map.of("lbAddr", loadBalancerAddress)) .commands(Arrays.asList("echo $lbAddr")) .build());
C#
// given a stack lbStack that exposes a load balancer construct as loadBalancer loadBalancerAddress = new CfnOutput(lbStack, "LbAddress", new CfnOutputProps { Value = string.Format("https://{0}/", lbStack.loadBalancer.LoadBalancerDnsName) }); stage.AddPost(new ShellStep("lbaddr", new ShellStepProps { EnvFromCfnOutputs = new Dictionary<string, CfnOutput> { { "lbAddr", loadBalancerAddress } }, Commands = new string[] { "echo $lbAddr" } }));

您可以直接在 ShellStep 中编写简单的验证测试,但是当测试代码超过几行时,这种方法会变得笨拙。对于更复杂的测试,您可以通过 inputs 属性将其他文件(例如完整的 shell 脚本或其他语言的程序)引入到 ShellStep 中。输入可以是任何有输出的步骤,包括源(例如 GitHub 存储库)或其他 ShellStep

如果文件可以直接用于测试(例如,如果它们本身是可执行文件),则可以从源存储库中引入这些文件。在本例中,我们将 GitHub 存储库声明为 source(而不是将其作为 CodePipeline 的一部分以内联方式实例化)。然后,我们将这个文件集同时传递给管线和验证测试。

TypeScript
const source = CodePipelineSource.gitHub('OWNER/REPO', 'main'); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: source, commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); const stage = pipeline.addStage(new MyPipelineAppStage(this, 'test', { env: { account: '111111111111', region: 'eu-west-1' } })); stage.addPost(new ShellStep('validate', { input: source, commands: ['sh ../tests/validate.sh'] }));
JavaScript
const source = CodePipelineSource.gitHub('OWNER/REPO', 'main'); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: source, commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); const stage = pipeline.addStage(new MyPipelineAppStage(this, 'test', { env: { account: '111111111111', region: 'eu-west-1' } })); stage.addPost(new ShellStep('validate', { input: source, commands: ['sh ../tests/validate.sh'] }));
Python
source = CodePipelineSource.git_hub("OWNER/REPO", "main") pipeline = CodePipeline(self, "Pipeline", pipeline_name="MyPipeline", synth=ShellStep("Synth", input=source, commands=["npm install -g aws-cdk", "python -m pip install -r requirements.txt", "cdk synth"])) stage = pipeline.add_stage(MyApplicationStage(self, "test", env=cdk.Environment(account="111111111111", region="eu-west-1"))) stage.add_post(ShellStep("validate", input=source, commands=["sh ../tests/validate.sh"], ))
Java
final CodePipelineSource source = CodePipelineSource.gitHub("OWNER/REPO", "main"); final CodePipeline pipeline = CodePipeline.Builder.create(this, "pipeline") .pipelineName("MyPipeline") .synth(ShellStep.Builder.create("Synth") .input(source) .commands(Arrays.asList("npm install -g aws-cdk", "cdk synth")) .build()) .build(); final StageDeployment stage = pipeline.addStage(new MyPipelineAppStage(this, "test", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build())); stage.addPost(ShellStep.Builder.create("validate") .input(source) .commands(Arrays.asList("sh ../tests/validate.sh")) .build());
C#
var source = CodePipelineSource.GitHub("OWNER/REPO", "main"); var pipeline = new CodePipeline(this, "pipeline", new CodePipelineProps { PipelineName = "MyPipeline", Synth = new ShellStep("Synth", new ShellStepProps { Input = source, Commands = new string[] { "npm install -g aws-cdk", "cdk synth" } }) }); var stage = pipeline.AddStage(new MyPipelineAppStage(this, "test", new StageProps { Env = new Environment { Account = "111111111111", Region = "eu-west-1" } })); stage.AddPost(new ShellStep("validate", new ShellStepProps { Input = source, Commands = new string[] { "sh ../tests/validate.sh" } }));

如果需要编译测试(在合成中完成),则可以从合成步骤中获取其他文件。

TypeScript
const synthStep = new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'], }); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: synthStep }); const stage = pipeline.addStage(new MyPipelineAppStage(this, 'test', { env: { account: '111111111111', region: 'eu-west-1' } })); // run a script that was transpiled from TypeScript during synthesis stage.addPost(new ShellStep('validate', { input: synthStep, commands: ['node tests/validate.js'] }));
JavaScript
const synthStep = new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'], }); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: synthStep }); const stage = pipeline.addStage(new MyPipelineAppStage(this, "test", { env: { account: "111111111111", region: "eu-west-1" } })); // run a script that was transpiled from TypeScript during synthesis stage.addPost(new ShellStep('validate', { input: synthStep, commands: ['node tests/validate.js'] }));
Python
synth_step = ShellStep("Synth", input=CodePipelineSource.git_hub("OWNER/REPO", "main"), commands=["npm install -g aws-cdk", "python -m pip install -r requirements.txt", "cdk synth"]) pipeline = CodePipeline(self, "Pipeline", pipeline_name="MyPipeline", synth=synth_step) stage = pipeline.add_stage(MyApplicationStage(self, "test", env=cdk.Environment(account="111111111111", region="eu-west-1"))) # run a script that was compiled during synthesis stage.add_post(ShellStep("validate", input=synth_step, commands=["node test/validate.js"], ))
Java
final ShellStep synth = ShellStep.Builder.create("Synth") .input(CodePipelineSource.gitHub("OWNER/REPO", "main")) .commands(Arrays.asList("npm install -g aws-cdk", "cdk synth")) .build(); final CodePipeline pipeline = CodePipeline.Builder.create(this, "pipeline") .pipelineName("MyPipeline") .synth(synth) .build(); final StageDeployment stage = pipeline.addStage(new MyPipelineAppStage(this, "test", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build())); stage.addPost(ShellStep.Builder.create("validate") .input(synth) .commands(Arrays.asList("node ./tests/validate.js")) .build());
C#
var synth = new ShellStep("Synth", new ShellStepProps { Input = CodePipelineSource.GitHub("OWNER/REPO", "main"), Commands = new string[] { "npm install -g aws-cdk", "cdk synth" } }); var pipeline = new CodePipeline(this, "pipeline", new CodePipelineProps { PipelineName = "MyPipeline", Synth = synth }); var stage = pipeline.AddStage(new MyPipelineAppStage(this, "test", new StageProps { Env = new Environment { Account = "111111111111", Region = "eu-west-1" } })); stage.AddPost(new ShellStep("validate", new ShellStepProps { Input = synth, Commands = new string[] { "node ./tests/validate.js" } }));

安全说明

任何形式的持续交付都有固有的安全风险。根据 AWS 责任共担模式,您应对 AWS Cloud 中信息的安全负责。CDK 管线库通过整合安全的默认值并建模最佳实践,助您先人一步。

但是,就其本质而言,需要高级别访问权限才能实现其预期目的的库无法保证完全安全。AWS 和您的组织之外有许多攻击向量。

请特别注意以下事项:

  • 请注意您所依赖的软件。审查您在管线中运行的所有第三方软件,因为它们可能会更改已部署的基础设施。

  • 使用依赖关系锁定防止意外升级。CDK 管线支持 package-lock.jsonyarn.lock,以确保您的依赖关系符合自己的期望。

  • CDK 管线在您自己的账户中创建的资源上运行,这些资源的配置由开发人员通过管线提交代码进行控制。因此,CDK 管线本身无法防范试图绕过合规性检查的恶意开发人员。如果您的威胁模型涵盖编写 CDK 代码的开发人员,则应设置外部合规性机制(例如 AWS CloudFormation 钩子(预防型)或 AWS Config(被动型),AWS CloudFormation 执行角色无权禁用这些机制。

  • 用于生产环境的凭证应短期有效。在引导和初始预置之后,开发人员就完全无需拥有账户凭证。可以通过管线部署更改。通过一开始就杜绝对凭证的需求,从而降低凭证泄露的可能性。

故障排除

在开始使用 CDK 管线时,您通常会遇到以下问题。

管线:内部故障
CREATE_FAILED  | AWS::CodePipeline::Pipeline | Pipeline/Pipeline
Internal Failure

检查 GitHub 访问令牌。它可能丢失了,或者可能没有访问存储库的权限。

密钥:策略包含一条语句,涉及一个或多个无效主体。
CREATE_FAILED | AWS::KMS::Key | Pipeline/Pipeline/ArtifactsBucketEncryptionKey
Policy contains a statement with one or more invalid principals.

其中一个目标环境尚未使用新的引导堆栈进行引导。确保所有目标环境都已引导。

堆栈处于 ROLLBACK_COMPLETE 状态,无法更新。
Stack STACK_NAME is in ROLLBACK_COMPLETE state and can not be updated. (Service:
AmazonCloudFormation; Status Code: 400; Error Code: ValidationError; Request
ID: ...)

堆栈在之前的部署中失败,并且处于不可重试的状态。从 AWS CloudFormation 控制台中删除堆栈,然后重试部署。