CDK Pipelines を使用した継続的インテグレーションと継続的デリバリー (CI/CD) - AWS Cloud Development Kit (AWS CDK) v2

これは AWS CDK v2 開発者ガイドです。旧版の CDK v1 は 2022 年 6 月 1 日にメンテナンスを開始し、2023 年 6 月 1 日にサポートを終了しました。

CDK Pipelines を使用した継続的インテグレーションと継続的デリバリー (CI/CD)

AWS コンストラクトライブラリの CDK Pipelines モジュールを使用して、AWS CDK アプリケーションの継続的デリバリーを設定します。CDK アプリのソースコードを AWS CodeCommit、GitHub、または AWS CodeStar にコミットすると、CDK Pipelines が自動的に新しいバージョンのビルド、テスト、デプロイを行います。

CDK Pipelines は自動更新です。アプリケーションステージまたはスタックを追加すると、パイプラインは自動的に再設定され、それらの新しいステージまたはスタックがデプロイされます。

注記

CDK Pipelines は 2 つの API をサポートしています。1 つは、CDK Pipelines 開発者プレビューで利用可能になった元の API です。もう 1 つは、プレビューフェーズで受け取った CDK 顧客からのフィードバックが組み込まれた、最新の API です。このトピックの例では、最新の API を使用しています。2 つのサポートされている API の違いの詳細については、「AWS-CDK GitHub リポジトリ」の「CDK Pipelines オリジナル API」を参照してください。

AWS 環境のブートストラップ

CDK Pipelines を使用する前に、スタックをデプロイする AWS 環境をブートストラップする必要があります。

CDK パイプラインには、少なくとも 2 つの環境が含まれます。1 つめの環境は、パイプラインがプロビジョニングされる場所です。2 つめの環境は、アプリケーションのスタックまたはステージをデプロイする場所です (ステージは関連するスタックのグループです)。これらの環境は同じにすることができますが、ベストプラクティスとしては、異なる環境でステージを分離することをおすすめします。

注記

ブートストラップによって作成されるリソースの種類と、ブートストラップスタックをカスタマイズする方法については、「AWS CDK ブートストラッピング」を参照してください。

CDK Pipelines での継続的なデプロイでは、CDK Toolkit スタックに以下を含める必要があります。

  • 1 つの Amazon Simple Storage Service (Amazon S3) バケット

  • Amazon ECR リポジトリ

  • パイプラインのさまざまな部分に必要なアクセス許可を付与する IAM ロール

CDK Toolkit は、既存のブートストラップスタックをアップグレードするか、必要に応じて新しいブートストラップスタックを作成します。

AWS CDK パイプラインをプロビジョニングできる環境をブートストラップするには、以下の例に示すように cdk bootstrap を呼び出します。npx コマンドを使用して AWS CDK Toolkit を呼び出すと、必要に応じて一時的にインストールされます。また、現在のプロジェクトにインストールされている Toolkit が存在する場合は、そのバージョンも使用します。

--cloudformation-execution-policies は、将来の CDK Pipelines デプロイを実行するポリシーの 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 コマンドを再度実行して、ブートストラップスタックを新しいバージョンに更新してみてください。

ブートストラップスタックが誤って削除されないように保護するには、終了保護を有効にする --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 ツールキットの一部のファイル名とクラス名がメインディレクトリの名前に基づいているためです。

クローンを作成したら、通常どおりにプロジェクトを初期化します。

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

アプリを作成したら、以下の 2 つのコマンドも入力します。これにより、アプリケーションの 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.json および cdk.context.json ファイルは必ずソースコントロールにコミットしてください。コンテキスト情報 (AWS アカウントから取得した特徴量フラグやキャッシュされた値など) は、プロジェクトの状態の一部です。値は環境ごとに異なる場合があり、結果として予期せぬ変更が生じる可能性があります。詳細については、「コンテキスト値と AWS CDK」を参照してください。

パイプラインを定義する

CDK Pipelines アプリケーションには、少なくとも 2 つのスタックが含まれます。1 つはパイプライン自体を表すスタックで、もう 1 つかそれ以上は、それを通してデプロイされたアプリケーションを表すスタックです。スタックはステージにグループ化することもできます。ステージを使用して、インフラストラクチャスタックのコピーをさまざまな環境にデプロイできます。ひとまずはパイプラインを検討し、後ほどにはデプロイするアプリケーションについて掘り下げていきます。

コンストラクト 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"] ) )

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")), } }

パイプラインは 1 度手動でデプロイする必要があります。その後、パイプラインはソースコードリポジトリから自身を最新の状態に保ち続けます。したがって、リポジトリ内のコードが、デプロイしたいコードであることを必ず確認してください。変更を確認して GitHub にプッシュし、デプロイします。

git add --all git commit -m "initial commit" git push cdk deploy
ヒント

最初のデプロイが完了したら、ローカル AWS アカウントには管理アクセスが不要になります。これは、アプリケーションへのすべての変更はパイプラインを介してデプロイされるためです。必要なのは GitHub にプッシュすることだけです。

アプリケーションステージ

パイプラインに一度に追加できるマルチスタック AWS アプリケーションを定義するには、Stage のサブクラスを定義します。(これは CDK Pipelines モジュールの CdkStage とは異なります。)

ステージには、アプリケーションを構成するスタックが含まれています。スタック間に依存関係がある場合、スタックはパイプラインに自動的に正しい順序で追加されます。相互に依存しないスタックは、並列にデプロイされます。stack1.addDependency(stack2) を呼び出すことで、スタック間に依存関係を追加できます。

ステージはデフォルトの env 引数を受け入れます。これは、その中のスタックのデフォルトの環境になります。(スタックには独自の環境を指定することもできます)。

アプリケーションをパイプラインに追加するには、Stage のインスタンスで addStage() を呼び出します。ステージをインスタンス化してパイプラインに複数回追加することで、DTAP またはマルチリージョンアプリケーションパイプラインのさまざまなステージを定義できます。

シンプルな Lambda 関数を含むスタックを作成し、そのスタックをステージに配置します。次に、ステージをパイプラインに追加してデプロイできるようにします。

TypeScript

Lambda 関数を含むアプリケーションスタックを保持する新しいファイル lib/my-pipeline-lambda-stack.ts を作成します。

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

Lambda 関数を含むアプリケーションスタックを保持する新しいファイル lib/my-pipeline-lambda-stack.js を作成します。

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

Lambda 関数を含むアプリケーションスタックを保持する新しいファイル my_pipeline/my_pipeline_lambda_stack.py を作成します。

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

Lambda 関数を含むアプリケーションスタックを保持する新しいファイル src/main/java/com.myorg/MyPipelineLambdaStack.java を作成します。

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#

Lambda 関数を含むアプリケーションスタックを保持する新しいファイル src/MyPipeline/MyPipelineLambdaStack.cs を作成します。

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 Pipeline にステップを追加して、実行しているデプロイを検証できます。たとえば、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 Pipelines はデプロイ後に 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 に持ち込むことができます。入力は、ソース (GitHub リポジトリなど) や別の ShellStep など、出力を持つ任意のステップにすることができます。

ソースリポジトリからファイルを取り込むのは、ファイルがテストで直接使用できる場合 (たとえば、ファイル自体が実行可能である場合) に適しています。この例では、GitHub リポジトリを (CodePipeline の一部としてインラインでインスタンス化するのではなく) source として宣言します。次に、このファイルセットをパイプラインと検証テストの両方に渡します。

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 Pipelines 自体は、コンプライアンスチェックをバイパスしようとする悪意のある開発者から保護することはできません。脅威モデルに CDK コードを記述する開発者が含まれている場合は、AWS CloudFormation 実行ロールに無効化するアクセス許可がない AWS CloudFormation Hooks (予防) や AWS Config (事後対応) などの外部コンプライアンスメカニズムが必要です。

  • 本番稼働環境の認証情報は、有効期間が短くなければなりません。ブートストラップと初期プロビジョニングの後、開発者がアカウント認証情報を持つ必要はありません。変更はパイプラインを通じてデプロイできます。そもそも認証情報を必要とせず、認証情報が漏洩する可能性を減らします。

トラブルシューティング

CDK Pipelines の使用を開始する際に、一般的に発生する問題は次のとおりです。

パイプライン: 内部障害
CREATE_FAILED  | AWS::CodePipeline::Pipeline | Pipeline/Pipeline
Internal Failure

GitHub アクセストークンを確認します。欠落しているか、リポジトリにアクセスするアクセス許可がない可能性があります。

キーポリシーに 1 つ以上の無効なプリンシパルを含むステートメントが含まれています。
CREATE_FAILED | AWS::KMS::Key | Pipeline/Pipeline/ArtifactsBucketEncryptionKey
Policy contains a statement with one or more invalid principals.

ターゲット環境の 1 つが新しいブートストラップスタックでブートストラップされていません。すべてのターゲット環境がブートストラップされていることを確認します。

スタックは 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 コンソールからスタックを削除し、デプロイを再試行します。