Amazon Web Services ブログ
AWS CDK Pipelines と AWS CodeDeploy を使用したブルー/グリーンデプロイ
お客様から Amazon Elastic Container Service (Amazon ECS) に AWS CodeDeploy を使用して ブルー/グリーン デプロイを実装するための支援がしばしば求められます。 お客様のユースケースは通常、クロスリージョンおよびクロスアカウント間でのデプロイシナリオが含まれます。 これらの要件だけでも十分に難しいのですが、さらに CodeDeploy を使用する際には特定の設計上の決定が必要となります。 具体的には CodeDeploy の設定方法、CodeDeploy リソース (アプリケーションやデプロイグループなど) の作成時期と方法、アカウントとリージョンの任意の組み合わせにデプロイできるコードの書き方が含まれます。
本日は、そうした設計上の決定について詳細に説明し、CDK Pipelines を使用してアカウントとリージョンをまたぐシナリオで Amazon ECS へサービスをデプロイする self-mutation パイプラインを実装する方法について説明します。このブログ記事では、AWS Cloud Development Kit (AWS CDK) を使用したクラウドインフラストラクチャの開発とデプロイのためのベストプラクティスに従っている Java 製の デモアプリケーション も紹介します。記事中で言及されるコードはデモアプリケーションの実装です。
パイプラインの概要
CDK Pipelines は、さまざまなデプロイエンジンでパイプラインを構築するために使用されるコンストラクトライブラリです。 クロスリージョンやクロスアカウントのパイプラインを実装する際に、開発者やインフラストラクチャエンジニアが解決する必要がある実装の詳細を抽象化しています。 例えば、クロスリージョンのシナリオでは、AWS CloudFormation はターゲットリージョンに複製されたアーティファクトを必要とします。 そのため、AWS Key Management Service (AWS KMS)キー、Amazon Simple Storage Service (Amazon S3)バケット、ポリシーをセカンダリリージョンに作成する必要があり、リージョン間でアーティファクトを移動できるようになります。 また、クロスアカウントのシナリオでは、CodeDeploy は構成ファイルの暗号化に使用される KMS キーへのアクセス権を持つクロスアカウントロールを必要とします。 これらはお客様が手作業で対処したくないものの一例です。
AWS CodeDeploy は、さまざまなシナリオでアプリケーションのデプロイを自動化するデプロイサービスです。Amazon EC2 インスタンス、オンプレミスインスタンス、 Lambda 関数、または Amazon ECS サービスにデプロイします。AWS Identity and Access Management (AWS IAM) と統合して、アプリケーションをデプロイする際のアクセス制御を実装できます。 ブルー/グリーン型のデプロイでは、Amazon CloudWatch Alarms を使ってデプロイのロールバックを自動化できます。
CDK Pipelines は、AWS CloudFormation のデプロイを自動化するように設計されました。 AWS CDK を使用すると、これらの CloudFormation デプロイでは、インスタンスやコンテナへのアプリケーションのデプロイを含めることができます。 ただし、一部のお客様は、アプリケーションのデプロイに CodeDeploy を使用することを好みます。 この AWS ブログ記事では、CloudFormation ではなく CodeDeploy を使用した CDK Pipelines のデプロイを紹介します。
設計上の考慮事項
この記事では、CodeDeploy を使用してサービスをアカウント (シングルアカウントまたはクロスアカウント) およびリージョン (シングルリージョンまたはクロスリージョン) の任意の組み合わせにデプロイするための、さまざまなユースケースに CDK Pipelines を活用することを検討します。 具体的には、以下の 4 つの問題を解決する必要があります。
CodeDeploy の設定
CodeDeploy を使ってブルー/グリーンデプロイタイプを実装するための最も一般的なオプションは、AWS::CodeDeployBlueGreen マクロを使うか、CodeDeploy コンストラクトを使うことです。この記事では後者の方法を採用します。同じ問題をカスタムリソースを使って解決しているお客様もいますが、この方法はカスタムリソースを使わない柔軟な方法です。パイプラインは実行のたびにコンテナを Amazon Elastic Container Registry (ECR) にプッシュし、タグを作成します。CodeDeploy はその情報をコンテナのデプロイに使います。
ここで推奨したいのは、AWS CDK クラウドアセンブリをスキャンし、リポジトリとタグ情報を取得するためにパイプラインアクションを作成することです。同じアクションで CodeDeploy 設定ファイルの作成を行うこともできます。CodeDeploy を設定するには、appspec.yaml、taskdef.json、imageDetail.json の 3 つの設定ファイルが必要です。 このパイプラインアクションは、CodeDeploy デプロイアクションの前に実行する必要があります。appspec.yaml
と taskdef.json
のテンプレートファイルを作成し、次のスクリプトをパイプライン内で実行することで3つの設定ファイルを作成することができます。
##
#!/bin/sh
#
# AWS CodeDeploy の設定を行うアクション
# template-appspec.yaml と template-taskdef.json を元に環境に応じた設定ファイルを作成します
#
# Account = The Account Id
# AppName = Name of the application
# StageName = Name of the stage
# Region = Name of the region (us-east-1, us-east-2)
# PipelineId = Id of the pipeline
# ServiceName = Name of the service. It will be used to define the role and the task definition name
#
# Primary output directory is codedeploy/. All the 3 files created (appspec.json, imageDetail.json and
# taskDef.json) will be located inside the codedeploy/ directory
#
##
Account=$1
Region=$2
AppName=$3
StageName=$4
PipelineId=$5
ServiceName=$6
repo_name=$(cat assembly*$PipelineId-$StageName/*.assets.json | jq -r '.dockerImages[] | .destinations[] | .repositoryName' | head -1)
tag_name=$(cat assembly*$PipelineId-$StageName/*.assets.json | jq -r '.dockerImages | to_entries[0].key')
echo ${repo_name}
echo ${tag_name}
printf '{"ImageURI":"%s"}' "$Account.dkr.ecr.$Region.amazonaws.com/${repo_name}:${tag_name}" > codedeploy/imageDetail.json
sed 's#APPLICATION#'$AppName'#g' codedeploy/template-appspec.yaml > codedeploy/appspec.yaml
sed 's#APPLICATION#'$AppName'#g' codedeploy/template-taskdef.json | sed 's#TASK_EXEC_ROLE#arn:aws:iam::'$Account':role/'$ServiceName'#g' | sed 's#fargate-task-definition#'$ServiceName'#g' > codedeploy/taskdef.json
cat codedeploy/appspec.yaml
cat codedeploy/taskdef.json
cat codedeploy/imageDetail.json
ツールチェーンの利用
効果的な戦略は、パイプラインをツールチェーンにカプセル化して、さまざまなアカウントやリージョンへのデプロイ方法を抽象化することです。これにより、パイプラインの作成方法、CodeDeploy の設定方法、および別のアカウントやリージョンへの展開方法など、実装の詳細を切り離すことができます。パイプラインを作成するには、デモアプリケーション内の Toolchain
スタックをデプロイします。これは必要に応じて別の環境を追加できるようになっています。要件によってはコンポーネントごとに異なる Stage や Wave を反映するように、パイプラインをカスタマイズする必要があります。より詳細な情報は、安全なハンズオフデプロイの自動化とそのリファレンス実装を参照してください。
Toolchain
スタックは、Java 用 CDK 全体で使われている Builder パターンに従っています。これは、1 つの文で複雑なオブジェクトを作成できる便利な機能です。
Toolchain.Builder.create(app, Constants.APP_NAME+"Toolchain")
.stackProperties(StackProps.builder()
.env(Environment.builder()
.account(Demo.TOOLCHAIN_ACCOUNT)
.region(Demo.TOOLCHAIN_REGION)
.build())
.build())
.setGitRepo(Demo.CODECOMMIT_REPO)
.setGitBranch(Demo.CODECOMMIT_BRANCH)
.addStage(
"UAT",
EcsDeploymentConfig.CANARY_10_PERCENT_5_MINUTES,
Environment.builder()
.account(Demo.SERVICE_ACCOUNT)
.region(Demo.SERVICE_REGION)
.build())
.build();
上記では、CDパイプラインが TOOLCHAIN_ACCOUNT
と TOOLCHAIN_REGION
で作成されています。 このパイプラインには、Apache Maven を使ってソースコードをビルドし、Java アーカイブ (JAR) を作成するステージが実装されています。 その後、パイプラインは JAR ファイルを含む Docker イメージを作成します。
UAT ステージでは、CANARY_10_PERCENT_5_MINUTES
のデプロイ設定を使用して、サービスを SERVICE_ACCOUNT
と SERVICE_REGION
にデプロイします。 つまり、最初に 10% のトラフィックが新しいアプリケーションに移行され、残りの 90 % が 5 分後にデプロイされます。
追加のデプロイステージを作成するには、ステージ名、CodeDeploy のデプロイ設定、そしてサービスをデプロイすべき環境が必要です。先に述べたように、このパイプラインはデフォルトで自身を変更する Self-mutation パイプラインです。たとえば、Prod
ステージを追加するには、Toolchain
オブジェクトを作成する CDK コードを更新し、この変更をコードリポジトリに送信します。パイプラインが実行され、UAT
ステージの後に Prod
ステージが追加されてパイプライン自身が更新されます。次に示すのは、新しい Prod
ステージを追加するコードです。新しいステージは UAT
環境と同じアカウントおよびリージョンにデプロイされます。
...
.addStage(
"Prod",
EcsDeploymentConfig.CANARY_10_PERCENT_5_MINUTES,
Environment.builder()
.account(Demo.SERVICE_ACCOUNT)
.region(Demo.SERVICE_REGION)
.build())
.build();
上記の文では、Prod
ステージが CodeDeploy のデプロイ構成 CANARY_10_PERCENT_5_MINUTES
を使用して、新しいバージョンのサービスをデプロイします。 つまり、最初に 10 % のトラフィックが新しいバージョンのアプリケーションに移行され、5 分後に残りのトラフィックが新しいバージョンに移行されるということです。 ビジネスアプリケーションの分離と管理のベストプラクティスについては、複数のアカウントを使用した AWS 環境の構成ホワイトペーパーを参照してください。
一部のお客様は、このアプローチに興味を持たれ、アプリケーション開発チームへの抽象化されたソリューションとして提供することを決定するかもしれません。 その場合、そのようなパイプラインを構築するコンストラクトを作成することをお勧めします。コンストラクトを使うと、さらにカスタマイズできます。 例えば、品質保証を促進するステージや、災害復旧シナリオでサービスをデプロイするステージなどです。
この実装では、ツールチェーンスタックと、各デプロイステージ用のスタックを作成します。たとえば、UAT
という単一のデプロイステージでツールチェーンが作成された場合を考えましょう。 正常に実行されると、次の画像のように DemoToolchain
および DemoService-UAT
スタックが作成されます。
CodeDeploy アプリケーションとデプロイグループ
CodeDeploy の構成には、アプリケーションとデプロイグループが必要です。ユースケースによっては、ツールチェーン (パイプライン) と同じアカウントあるいは別のアカウントでこれらを作成する必要があります。パイプラインには、ブルー/グリーンデプロイを実行する CodeDeploy デプロイアクションが含まれています。 私の推奨は、CodeDeploy アプリケーションとデプロイグループをサービススタックの一部として作成することです。 このアプローチにより、CodeDeploy アプリケーションとデプロイグループのライフサイクルを関連するサービススタックインスタンスと合わせることができます。
CodePipeline を使うと、存在しない CodeDeploy アプリケーションとデプロイグループを参照する CodeDeploy デプロイアクションを作成できます。 これにより、以下のアプローチを実装できます。
- ツールチェーンスタックは、存在しない CodeDeploy アプリケーションとデプロイグループを参照するデプロイアクションでパイプラインをデプロイします
- パイプラインが実行されると、まず関連する CodeDeploy アプリケーションとデプロイグループを作成するサービススタックがデプロイされます
- 次のパイプラインアクションでデプロイアクションが実行されます。パイプラインがデプロイアクションを実行する段階では、必要な CodeDeploy アプリケーションとデプロイグループが(前のステップで作成されたため)存在しています。
以下は、(最初は存在しない) CodeDeploy アプリケーションおよびデプロイグループを参照するパイプラインコードです。
private IEcsDeploymentGroup referenceCodeDeployDeploymentGroup(
final Environment env,
final String serviceName,
final IEcsDeploymentConfig ecsDeploymentConfig,
final String stageName) {
IEcsApplication codeDeployApp = EcsApplication.fromEcsApplicationArn(
this,
Constants.APP_NAME + "EcsCodeDeployApp-"+ stageName,
Arn.format(ArnComponents.builder()
.arnFormat(ArnFormat.COLON_RESOURCE_NAME)
.partition("aws")
.region(env.getRegion())
.service("codedeploy")
.account(env.getAccount())
.resource("application")
.resourceName(serviceName)
.build()));
IEcsDeploymentGroup deploymentGroup = EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(
this,
Constants.APP_NAME + "-EcsCodeDeployDG-"+ stageName,
EcsDeploymentGroupAttributes.builder()
.deploymentGroupName(serviceName)
.application(codeDeployApp)
.deploymentConfig(ecsDeploymentConfig)
.build());
return deploymentGroup ;
}
これを機能させるには、パイプラインで CodeDeploy デプロイアクションを作成するときと、サービススタック (Amazon ECS インフラストラクチャがデプロイされている) で CodeDeploy アプリケーションとデプロイグループを作成するときで、同じアプリケーション名とデプロイグループ名の値を使用する必要があります。 このアプローチが必要な理由はサービススタック内で作成したリソースを参照して CodeDeply デプロイアクションを設定すると循環依存エラーが発生するためです。 以下は、サービススタックコンストラクト ID を使用して CodeDeploy アプリケーションとデプロイグループに名前を付けるコードです。 パイプラインで CodeDeploy デプロイアクションを作成する際に使用したのと同じ名前をサービススタックのコンストラクト ID に設定しています。
// configure AWS CodeDeploy Application and DeploymentGroup
EcsApplication app = EcsApplication.Builder.create(this, "BlueGreenApplication")
.applicationName(id)
.build();
EcsDeploymentGroup.Builder.create(this, "BlueGreenDeploymentGroup")
.deploymentGroupName(id)
.application(app)
.service(albService.getService())
.role(createCodeDeployExecutionRole(id))
.blueGreenDeploymentConfig(EcsBlueGreenDeploymentConfig.builder()
.blueTargetGroup(albService.getTargetGroup())
.greenTargetGroup(tgGreen)
.listener(albService.getListener())
.testListener(listenerGreen)
.terminationWaitTime(Duration.minutes(15))
.build())
.deploymentConfig(deploymentConfig)
.build();
CDK Pipelines のロールと権限
CDK Pipelines は、さまざまなシナリオ (リージョンやアカウントなど) においてデプロイを実行するためのロールとアクセス許可を作成します。 CodeDeploy を別のアカウントで実行する際、CDK Pipelines はクロスアカウントのサポートスタックをデプロイします。このサポートスタックは、CodeDeploy アクションのためのパイプラインアクションロールを作成します。 このクロスアカウントのサポートスタックは JSON ファイルに定義されており、対象アカウントの AWS CDK の Assets バケットにアップロードする必要があります。 パイプラインで self-mutation 機能が有効 (デフォルト) の場合、UpdatePipeline ステージでは cdk deploy を実行し、パイプラインへの変更をデプロイします。 クロスアカウントのシナリオでは、このデプロイにはクロスアカウントのサポートスタックのデプロイ/更新も含まれます。 このため、UpdatePipeline ステージの SelfMutate アクションは、リモートアカウントで CDK の ファイル公開 と デプロイ のロールを引き受ける必要があります。
UpdatePipeline ステージを実行する AWS CodeBuild プロジェクトに関連付けられている IAM ロールには、デフォルトではこれらのアクセス許可がありません。 CDK Pipelines は、クロスアカウントスタックに必要なアクセス許可の情報は AWS CDK アプリの合成が終わった後でしか利用できないため、これらのアクセス許可を自動的に付与することはできません。その時点では、パイプラインのアクセス許可がすでにロックされています。 したがって、クロスアカウントのシナリオでは、ツールチェーンがパイプラインの UpdatePipeline ステージのアクセス許可を拡張して、ファイル公開とデプロイのロールを含める必要があります。
クロスアカウント環境では、これらのアクセス許可を手動で UpdatePipeline ステージに追加することが可能です。そのためには、Toolchain
スタックを使用して、このような実装の詳細を隠すことができます。最終的には、以下のようなメソッドを使用して、不足しているアクセス許可を追加できます。パイプライン内の各異なるステージとアカウントのマッピングについて、ターゲットアカウントがパイプラインがデプロイされているアカウントと異なるかどうかを検証します。条件を満たした場合は、UpdatePipeline ステージに対して、ターゲットアカウントの CDK ブートストラップロールを引き受ける権限を付与する必要があります (キー aws-cdk:bootstrap-role
で、タグ値はfile-publishing
または deploy
)。以下の例は、UpdatePipeline ステージにアクセス許可を追加する方法を示しています。
private void grantUpdatePipelineCrossAccoutPermissions(Map stageNameEnvironment) {
if (! stageNameEnvironment.isEmpty()) {
this.pipeline.buildPipeline();
for (String stage : stageNameEnvironment.keySet()) {
HashMap condition = new HashMap<>();
condition.put(
"iam:ResourceTag/aws-cdk:bootstrap-role",
new String[] {"file-publishing", "deploy"});
pipeline.getSelfMutationProject()
.getRole()
.addToPrincipalPolicy(PolicyStatement.Builder.create()
.actions(Arrays.asList("sts:AssumeRole"))
.effect(Effect.ALLOW)
.resources(Arrays.asList("arn:*:iam::"
+ stageNameEnvironment.get(stage).getAccount() + ":role/*"))
.conditions(new HashMap() {{
put("ForAnyValue:StringEquals", condition);
}})
.build());
}
}
}
デプロイステージ
UAT
という単一のデプロイステージがあるパイプラインを考えてみましょう。UAT
ステージではDemoService
をデプロイします。それには 4 つのアクション: DemoService-UAT (準備とデプロイ)、ConfigureBlueGreenDeploy、そしてDeployが必要です。
DemoService-UAT.Deploy アクションは ECS リソースと CodeDeploy アプリケーションとデプロイグループを作成します。ConfigureBlueGreenDeploy アクションは CDK クラウドアセンブリから ECR リポジトリやコンテナイメージのタグ情報を読み取ります。パイプラインはこの情報をデプロイアクションに送り、CodeDeploy を使ったデプロイが開始されます。
ソリューションの概要
利便性のため、これらすべての課題を解決し、リファレンス実装として使える Java アプリケーション を作成しました。 このアプリケーションのデプロイは、アカウントやリージョンでのすべてのデプロイシナリオで同じ 5 つのステップに従い、次の設計に表されているシナリオも含まれます。
結論
この投稿では、アカウントとリージョンの様々な組み合わせで CodeDeploy を使用して Amazon ECS にサービスをデプロイするパイプラインを作成する際に伴う課題を特定、説明、解決しました。 また、これらの推奨事項を実装するデモアプリケーションを紹介しました。 サンプルコードを拡張して、自動テスト、自動デプロイロールバック、障害復旧など、より複雑なシナリオを実装できます。 あなたの変革の旅がうまくいくことを願っています。
本記事は、 Luiz Decaro による “Blue/Green deployments using AWS CDK Pipelines and AWS CodeDeploy” を翻訳したものです。翻訳はソリューションアーキテクトの平川 大樹が担当しました。