使用 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 Construct Library 中的 CDK 管道模組來設定 AWS CDK 應用程式的持續交付。當您將 CDK 應用程式的原始碼遞交至 AWS CodeCommitGitHub、 或 AWS CodeStar時,CDK 管道可以自動建置、測試和部署您的新版本。

CDK 管道正在自我更新。如果您新增應用程式階段或堆疊,管道會自動自行重新設定,以部署這些新的階段或堆疊。

注意

CDK 管道支援兩個 APIs。一個是 CDK 管道開發人員預覽版中提供的原始 API。另一個是現代 API,其中包含預覽階段期間從 CDK 客戶收到的意見回饋。本主題中的範例使用現代 API。如需兩個支援 APIs之間的差異詳細資訊,請參閱 aws-cdk GitHub儲存庫中的 CDK Pipelines 原始 API

引導您的 AWS 環境

您必須先引導要部署堆疊 AWS 的環境,才能使用 CDK 管道。

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,它也會使用目前專案中安裝的 Toolkit 版本。

--cloudformation-execution-policies 指定未來 CDK 管道部署將在其中執行的政策 ARN。預設AdministratorAccess政策可確保您的管道可以部署每種類型的 AWS 資源。如果您使用此政策,請確定您信任組成 AWS CDK 應用程式的所有程式碼和相依性。

大多數組織要求對自動化可以部署的資源類型進行更嚴格的控制。請洽詢組織內適當的部門,以判斷管道應使用的政策。

如果您的預設 AWS 設定檔包含必要的身分驗證組態和,您可以省略 --profile選項 AWS 區域。

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 設定檔包含必要的身分驗證組態和 ,您可以省略 --profile選項 AWS 區域。

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命令,將引導堆疊更新為新版本。

為了防止意外刪除您的引導堆疊,我們建議您提供 --termination-protection選項與 cdk bootstrap命令,以啟用終止保護。您可以在新的或現有的引導堆疊上啟用終止保護。若要進一步了解此選項,請參閱 --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 Construct Library 模組。

$ go get
重要

請務必將您的 cdk.jsoncdk.context.json 檔案遞交至來源控制。內容資訊 (例如從 AWS 您的帳戶擷取的功能旗標和快取值) 是專案狀態的一部分。在其他環境中,這些值可能不同,這可能會導致結果發生意外變更。如需詳細資訊,請參閱內容值和 AWS CDK

定義管道

您的 CDK Pipelines 應用程式將包含至少兩個堆疊:一個代表管道本身,以及一或多個代表透過它部署之應用程式的堆疊。堆疊也可以分組為階段,您可以使用這些階段將基礎設施堆疊的副本部署到不同的環境。現在,我們將考慮管道,並在稍後深入探索其將部署的應用程式。

建構CodePipeline是代表使用 AWS CodePipeline 做為其部署引擎之 CDK 管道的建構。當您在堆疊CodePipeline中執行個體化時,您可以定義管道的來源位置 (例如 GitHub 儲存庫)。您也可以定義建置應用程式的命令。

例如,以下定義管道,其來源存放在 GitHub 儲存庫中。它也包含 TypeScript CDK 應用程式的建置步驟。填寫 GitHub 儲存庫的相關資訊,如所示。

注意

根據預設,管道會使用存放在 Secrets Manager 中的個人存取字符,以名稱 對 GitHub 進行身分驗證github-token

您也需要更新管道堆疊的執行個體化,以指定 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"] ) )

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)

Stages 接受預設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 執行個體表示。您可以呼叫 或 addPost()方法,將部署前addPre()或部署後動作新增至階段。

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 Pipeline 程式庫的 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 屬性ShellStep將其他檔案 (例如完整的 shell 指令碼或其他語言的程式) 帶入 。輸入可以是具有輸出的任何步驟,包括來源 (例如 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 雲端中資訊的安全。CDK Pipelines 程式庫透過整合安全預設值和建模最佳實務,為您提供一個良好的開始。

不過,就其本質而言,需要高度存取權才能實現其預期目的的程式庫無法保證完全安全。 AWS 和您的組織外有許多攻擊媒介。

尤其請記住下列事項:

  • 請注意您依賴的軟體。審核您在管道中執行的所有第三方軟體,因為它可以變更要部署的基礎設施。

  • 使用相依性鎖定來防止意外升級。CDK Pipelines 會尊重 package-lock.jsonyarn.lock並確保您的相依性是您所預期的相依性。

  • CDK Pipelines 會在您自己的帳戶中建立的資源上執行,而這些資源的組態是由透過管道提交程式碼的開發人員所控制。因此,CDK 管道本身無法防範惡意開發人員嘗試略過合規檢查。如果您的威脅模型包含編寫 CDK 程式碼的開發人員,您應該具備外部合規機制,例如 AWS CloudFormation 執行角色沒有停用許可AWS CloudFormation 的勾點 (預防) 或 AWS Config (被動)。

  • 生產環境的登入資料應該是短期的。在引導和初始佈建之後,開發人員完全不需要擁有帳戶登入資料。變更可以透過管道部署。一開始就不需要登入資料,以減少登入資料洩漏的可能性。

故障診斷

開始使用 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 主控台刪除堆疊,然後重試部署。