ECSでBlueGreen DeployをCDKで試す記事 part2

記事タイトルとURLをコピーする

CS1の石井です。

今回の記事は前回の「ECSでBlueGreen DeployをCDKで試す記事 part1」の記事の後編です。

こちらはパイプラインを用いてB/Gデプロイを行う実装となります。

※本記事もコードだけ欲しい人は「早速結論」の項目まで飛んでください。

記事を書いた動機

part1に続き、B/Gデプロイの話をCDKで実践したいと思っていました。 ただ、B/GデプロイはCI/CD環境との組み合わせで旨みが出るものと理解しています。そのため、記事としてはこちらが本命です。

また、パイプライン自体はCDKのpipelinesモジュールという要素を用いてパイプラインを構成します。 しかしpipelinesモジュールは既存のCodePipelineモジュールという昔からあるパイプラインを構成するモジュールと比べて少し複雑です。

ただ、コード量的にpipelinesモジュールの方が少なくなるため、私はpipelinesモジュールの方を気に入っています。 そのため、本記事でpipelinesモジュールの実装例として紹介できればと考えています。

なお、本ブログは公式のAWS blogの記事を参考にしています。 公式のブログでもマルチアカウントでB/Gデプロイを行うCDKのサンプルを記載しているのですが、若干私のやりたいことと少し外れていたので、以下の点で改造を行なっています。

全体的なコード修正

javaのコードからtypescriptで書き直しました。

ECRの変更

dockerビルドを明示的にステージを設けました。

Assetsステージで利用するCDKが用意したECRではなく、ユーザーが生成したECRにイメージをプッシュします。

ECRはパイプラインアカウントで集中的に管理したいことと、全プロジェクトが同じECRにpushされてしまうためです。

ECSのデプロイ内容の変更

ECSはL3コンストラクトでの作成ではなくpart1で使用したコードを改造してデプロイしています。

上記の修正を踏まえて図にすると以下のような構成となります。

Pipelinesモジュール名物「Selfmutation」によりいつもより多く線が伸びていますが、これは後ほどサポートスタックと一緒にコードベースに内容を記載します。

また、本記事では「パイプラインを構成するアカウント」を「パイプラインアカウント」とし、「ECSが稼働するデプロイ先のアカウント」を「ECSアカウント」と記載します。

本記事の対象者

  • CDKワークショップを終わらせている方
  • パイプライン用とECS稼働用の2アカウントがある方
  • ECSのB/Gデプロイの動きをある程度理解している方

早速結論

下記のリポジトリからgit cloneしてnpm ciを実行して使用してください。

git clone https://github.com/nov03/bgdeploy_ts.git ; cd bgdeploy_ts
npm ci

使い方:

使い方は以下の通りです。

※すでに2環境でbootstrapできている前提です。デプロイ手順のみ記載します。 アカウント間のbootstrapは「アカウントへのbootstrap」の項目を参照してください

アカウントの指定

bin/custom-demo.tsのファイルの下記の要素を更新してください。

  • TOOLCHAIN_ACCOUNT:パイプラインの配置先のアカウント番号
  • SERVICE_ACCOUNT:ECSが稼働するアカウントの番号

デプロイ

以下のコマンドでデプロイするとサポートスタックとパイプラインおよびCodeCommitがデプロイされます。

npx cdk deploy --all

先ほどのデプロイで「ecs-tutorial-repo-test」というCodeCommitが生成されたのでpushを行います。

git remote set-url origin codecommit::ap-northeast-1://ecs-tutorial-repo-test
git add .
git commit -m "first deploy"
git push --set-upstream origin main

しばらくするとパイプラインからECSアカウントに対してデプロイされ、B/Gデプロイも含めてパイプラインの全工程が完了します。

この時点ではblueのページを再度blueのページでB/Gデプロイしているだけなので、「lib/codebuild/src/main/resources/static/index.html」という静的ファイルをgreen.htmlの内容で書き換えてpushします。

cp lib/codebuild/src/main/resources/static/green.html lib/codebuild/src/main/resources/static/index.html
git commit -m "green deploy"
git push origin main

このpushにより再度パイプラインが実行され、B/GデプロイのステップでALBに対してテストリスナーと本番リスナーでB/Gコンテナが稼働しているところが確認できます。

※B/Gデプロイの詳細な動きはpart1のパイプラインがない構成で動きを確認してください。

本記事の構成

本記事は上記の結論部分で流したコードの内容でポイントとなる部分をフォーカスして記載します。

ECSとB/Gデプロイの細かい動きなどは本記事では記載しません。

  1. パイプラインアカウントとECSアカウントの信頼関係作成
  2. コードとデプロイまでの全体像の紹介
  3. パイプラインスタックとサポートスタックについて
  4. ECSへのデプロイ内容について
  5. まとめ

アカウントへのbootstrap

まずはローカルの端末からcdk deployが実行できる必要があります。

ワークショップを完了していると少なくとも1つのAWSアカウントにbootstrapが実行されていると思います。

今回は複数アカウントでの挙動の確認のため、もう一つのアカウントにもbootstrapを実行します。

※本来はECSアカウントと信頼関係を結んでおくのはパイプラインアカウントのみで良いのですが、 今回はサポートスタックという要素があるため、ローカル端末とのbootstrapも必要となります。

# ローカル端末とECSアカウントでbootstrapを実行
npx cdk boostrap --profile ecsprofile

# パイプラインアカウントをECSアカウントが信頼
npx cdk bootstrap --profile ecsprofile \
    --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \
    --trust パイプラインアカウント \
    aws://ECSアカウントの番号/ap-northeast-1

なお、本記事ではパイプラインアカウントをaws credentialのdefaultのプロファイルをパイプラインアカウントとしてデプロイします。

コードとデプロイまでの全体像の紹介

コードは下記のgitにあります、下記コマンドでcloneしてください

git clone https://github.com/nov03/bgdeploy_ts.git ; cd bgdeploy_ts
npm ci

クローンしたgitの中身は以下となります。

ishiinobuaki@ishiinokinoMBP8 bgdeploy_ts % tree bin
bin
└── custom-demo.ts

0 directories, 1 file
ishiinobuaki@ishiinokinoMBP8 bgdeploy_ts % tree lib
lib
├── codebuild
│   ├── Dockerfile
│   ├── build.gradle
│   ├── build.sh
│   ├── gradle
│   │   └── wrapper
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradlew
│   ├── gradlew.bat
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       └── example
│           │           └── mywebapp
│           │               └── MyWebAppApplication.java
│           └── resources
│               └── static
│                   ├── blue.html
│                   ├── green.html
│                   └── index.html
├── codedeploy
│   ├── codedeploy_configuration.sh
│   ├── template-appspec.yaml
│   └── template-taskdef.json
├── codedeploystep.ts
├── constants.ts
├── service.ts
└── toolchain.ts

12 directories, 18 files
ishiinobuaki@ishiinokinoMBP8 bgdeploy_ts % 

それぞれディレクトリごとに役割を記載します。 ※CodebuildとCodedeployディレクトリの内容に関しては「ECSへのデプロイ内容について」の項目で記載します。

ディレクトリ名 ファイル名 役割
bin custom-demo.ts CDKのアプリケーションのエントリーポイント。メインのパイプラインやリソースの構築の設定が含まれる。
lib constants.ts アプリケーション全体で使用される共通の定数を定義する。
lib toolchain.ts CDKのToolchainスタックの定義。パイプラインの生成、ステージの追加、ECRの設定などが含まれる。
lib service.ts AWSのリソース(VPC、セキュリティグループ、ECSなど)を作成・管理するCDKスタックの定義。Blue/GreenデプロイをサポートするCodeDeployの設定も含まれる。
lib codedeploystep.ts AWS CodePipelineを使用してECSサービスへのデプロイを行うステップの定義。特定のデプロイメントグループにデプロイするためのCodePipelineアクションを生成するロジックを含む。
custom-demo.ts:

AWS CDKのエントリーポイントとして機能します。

このクラスでは、パイプラインとサービスのアカウント情報やリージョン情報を設定しています。 Toolchainクラスのインスタンスを生成して、そのパイプラインを実際に構築します。

constants.ts:

アプリケーション名など、アプリケーション全体で使用される定数を提供するモジュールです。

パイプラインアカウント、ECSアカウント共通で使う定数を使いたければ要素を追記します。

toolchain.ts:

Toolchainクラスは、パイプラインを生成する主要なクラスであり、パイプラインアカウントにデプロイされます。

ソースコードの取得、ビルド、デプロイステージの定義など、パイプラインの各ステージの動作を定義します。 ECRリポジトリの構築、パイプラインのステージの追加、CodeDeployの設定、ロールの作成やポリシーのアタッチなど、多くの関連リソースの定義や設定がこのファイル内で行われます。

service.ts

AWSのリソース(VPC、セキュリティグループ、ロードバランサ、ECSクラスタ、タスク定義など)を作成するCDKスタックを定義しており、ECSアカウントにデプロイされます。

ServiceProps というインターフェースで、このスタックのプロパティを定義しています。 主にVPC、ALB、セキュリティグループ、ECSクラスタ、ECRリポジトリからのコンテナイメージを使用してECSサービスを構築するロジックが含まれています。 最後に、Blue/GreenデプロイをサポートするCodeDeployのアプリケーションとデプロイグループを作成しています。

codedeploystep.ts

AWS CodePipelineを使用してECSサービスへのデプロイを行うためのステップを定義しています。 CodeDeployStep は、特定のデプロイメントグループにデプロイするためのCodePipelineアクションを生成します。 produceAction メソッドは、指定されたステージにデプロイアクションを追加する役割があります。

用語整理

toolchain

ソフトウェアの開発やビルドプロセスを支援する一連のツールを指します。これにはコンパイラ、リンカー、アセンブラ、ライブラリ、ビルドツールなどが含まれることが多いです。これらのツールは連鎖的に動作し、ソースコードから実行可能なプログラムやライブラリを生成するのに必要な手順を一貫して実行します。

gradle

javaのビルドツールであり、元のAWSのCDKブログから改造をしたかった最大の理由はこれをパイプラインのステージに組み込みたかったからです。

本記事ではhtmlファイルをwarにまとめるだけで使用しており、本筋ではないのであまりgradleに関しては言及しません。

サポートスタックについて

本項ではサポートスタックに関して記載します。

今回のパイプラインの構成はECSアカウントのCodeDeployを実行する必要があります。 そのためにはパイプラインアカウントからECSアカウントのCodeDeployアプリケーションを参照・実行可能なロールをパイプラインに設定する必要があります。

ただし、このロールはECSアカウントに存在する必要があり、パイプラインアカウントからassume roleを許可する必要があります。

CDKはステージ上に別アカウントのロールを参照するコードがあればサポートスタックという要素でパイプラインスタックのデプロイよりも前にECSアカウントにデプロイが開始されます。

コードでは下記の部分に当たります。

// lib/toolchain.ts
    private referenceCodeDeployDeploymentGroup(
        env: cdk.Environment, 
        serviceName: string, 
        ecsDeploymentConfig: codedeploy.IEcsDeploymentConfig,
        stageName: string
      ): codedeploy.IEcsDeploymentGroup {
    
        const codeDeployApp = EcsApplication.fromEcsApplicationArn(
            this,
            `EcsCodeDeployApp-${stageName}`,
            cdk.Arn.format({
                arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
                partition: "aws",
                region: env.region,
                service: "codedeploy",
                account: env.account,
                resource: "application",
                resourceName: serviceName
            })
        );
    
        const deploymentGroup = EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(
            this,
            `-EcsCodeDeployDG-${stageName}`,
            {
                deploymentGroupName: serviceName,
                application: codeDeployApp,
                deploymentConfig: ecsDeploymentConfig
            }
        );
    
        return deploymentGroup;
      }

~省略~

        const deployStep = new CodeDeployStep(
            `codeDeploy`,
            configureCodeDeployStep.primaryOutput!,
            this.referenceCodeDeployDeploymentGroup(env, "crossAccountEcsBGDeployApp", ecsDeploymentConfig, stageName),
            stageName
        );

        // stageDeploymentにdeployStepを追加したタイミングでサポートスタックが生成される
        stageDeployment.addPost(configureCodeDeployStep, deployStep);

さらに、このサポートスタックはselfmutationの範囲内の対象となる必要があります。 相手のCodeDeployのデプロイの実行内容をCDK上で変更した場合、IAM Policyのactionもそれに波及して変更する必要があります。

今回のコードで言うとbin/custom-demo.tsにdeploymentConfigにデプロイ設定を記載できます。 初期だとカナリアデプロイになってますが、ALL_AT_ONCEなどに変更するとCodeDeployの実行ロールもALL_AT_ONCEを実行する権限が必要となり、カナリアデプロイの権限は不要となります。

そのため必然的にSelfmutationの範囲内に収まる必要があり、selfmutationの実行ロールにもECSアカウントに対するデプロイ権限が必要になってきます。

CDK上だと以下のコードがサポートスタックにもselfmutationを実行可能にする内容が記載されています。

/// lib/toolcahin.ts
〜中略〜
        // configureCodeDeployStep と deployStep を dockerBuildStep の後に追加
        deployStep.addStepDependency(configureCodeDeployStep);
        stageDeployment.addPost(configureCodeDeployStep, deployStep);

        // selfMutationProject.roleにポリシーをアタッチするためパイプラインを初期化
        this.pipeline.buildPipeline();

        
        const crossAccountEnvironments = new Map<string, Environment>();
        const pipelineAccount = this.account; // パイプラインのアカウントを取得
        for (const stage of this.toolchainProperty.stages) {
            if (pipelineAccount !== stage.env.account) { // パイプラインのアカウントとステージのアカウントが異なる場合
                crossAccountEnvironments.set(stage.name, stage.env);
            }
        }
        this.grantUpdatePipelineCrossAccountPermissions(crossAccountEnvironments);

    private grantUpdatePipelineCrossAccountPermissions(stageNameEnvironment: Map<string, Environment>) {
        if (stageNameEnvironment.size > 0) {
            for (const [stage, env] of stageNameEnvironment.entries()) {
                const condition = {
                    "ForAnyValue:StringEquals": {
                        "iam:ResourceTag/aws-cdk:bootstrap-role": ["file-publishing", "deploy"]
                    }
                };

                this.pipeline.selfMutationProject.role?.addToPrincipalPolicy(new PolicyStatement({
                    actions: ["sts:AssumeRole"],
                    effect: Effect.ALLOW,
                    resources: [`arn:*:iam::${env.account}:role/*`],
                    conditions: condition
                }));
            }
        }
    }

直感的に何をやっているのか見えづらいのですが、grantUpdatePipelineCrossAccountPermissionsに注目してください。

this.pipeline.selfMutationProject.roleでselfmutationで実行するCodebuildの実行ロールに対し、ECSアカウントのCDKデプロイ用ロールとパブリッシュ用ロールをassume roleできるようにしています。 これによりselfmutationのロールがECSアカウントのサポートスタックも含めてcdk deployができるようになるという構図です。

図にすると以下のようなイメージです

余談ですが、このサポートスタックの存在を知らないころはECSアカウントにCodeDeployの実行ロール名をハードコーディングし、パイプラインアカウントに信頼関係を直接記載していました。

サポートスタックの概念を使えば、パイプラインアカウントでECSアカウントでハードコードしたロール名を指定しなくとも、サポートスタックのロールを参照するようになり、パイプラインアカウントからの信頼関係も明示的に指定する必要がなくなりました。

※サポートスタックを使わずに、CodeDeployの実行ロールをパイプラインで直接指定してしまった例として、前回私が記載したEC2に対するCodeDeployの記事が該当します

ECSへのデプロイ内容について

本項ではECSへデプロイを行うまでの内容を記載します。

ECSへのデプロイはDockerビルドし、ビルドした内容をCodeDeployでデプロイを行う必要があります。

本記事では下記のCodeBuildのステップをデプロイのステージに差し込んでいます。

  • DockerbuildというステップでDockerイメージを生成
  • ConfigureBlueGreenDeploy というステップでCodeDeployをするためのファイルを生成

Dockerbuild

まずはDockerBuildのステージから内容を確認します。

関係するファイルとコードは以下です。

lib/toolcahin.ts

        const dockerBuildStep = new pipelines.CodeBuildStep('DockerBuildStep', {
            buildEnvironment: {
              buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
              privileged: true,
            },
            role: buildRole,
            input: this.pipeline.synth.primaryOutput,
            commands: [
              "ls -l; pwd",
              "cd codebuild",
              'chmod +x build.sh',
              './build.sh',  
            ],
            
            primaryOutputDirectory: 'codebuild/',  // 出力ディレクトリを指定します。
            env: {
              'AWS_REGION_NAME': 'ap-northeast-1',
              'ECR_REPOSITORY_NAME': 'ecs-tutorial',
            }
          });

ファイルの関係性は以下です。

ディレクトリ名 ファイル名 役割
lib/codebuild Dockerfile gradleが生成したwarファイルをコピーして起動するイメージを生成。
lib/codebuild build.gradle Gradle ビルドツールの設定と依存関係を定義。
lib/codebuild build.sh ビルドプロセスを実行するためのシェルスクリプト。詳細は後述。
lib/codebuild/gradle/wrapper gradle-wrapper.jar Gradle プロジェクトのバージョンを固定するためのラッパーツール。
lib/codebuild/gradle/wrapper gradle-wrapper.properties Gradle ラッパーの設定ファイル
lib/codebuild/src/.../mywebapp MyWebAppApplication.java アプリケーションのメインJavaクラス。
lib/codebuild/src/.../static blue.html, green.html, index.html warファイルでデプロイされる静的ファイル。index.htmlだけデプロイされ、blue.htmlとgreen.htmlはサンプルファイルです。

Dockerbuildでのbuild.shの操作はシンプルに以下の流れです。

  1. ソースコードをビルド
  2. ビルドした生成物をDockerImageにする
  3. ECRにpushする
  4. ECSタスクで使用するimageDetail.jsonを出力する

図にすると以下となります。

もしデプロイしたいコードがjava以外であれば別言語のソースファイルとbuild.shにそのプログラムのビルド手順を記載するような使い方となります。

余談ですが、pipelinesモジュールのshellstepというクラスででCodeBuildを実装している都合上「buildspec.yml」が使えません。(※CDKの仕様です)

そのためシェルで頑張っているのですが、いずれbuildspec.ymlが使えるようになったら嬉しいなぁと思います。

ConfigureBlueGreenDeploy

このステップは主にタスク定義ファイルとappspecを生成するステップです。

関連するコードとファイルは以下となります。

// lib/toolchain.ts

    private referenceCodeDeployDeploymentGroup(
        env: cdk.Environment, 
        serviceName: string, 
        ecsDeploymentConfig: codedeploy.IEcsDeploymentConfig,
        stageName: string
      ): codedeploy.IEcsDeploymentGroup {
    
        const codeDeployApp = EcsApplication.fromEcsApplicationArn(
            this,
            `EcsCodeDeployApp-${stageName}`,
            cdk.Arn.format({
                arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
                partition: "aws",
                region: env.region,
                service: "codedeploy",
                account: env.account,
                resource: "application",
                resourceName: serviceName
            })
        );
    
        const deploymentGroup = EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(
            this,
            `-EcsCodeDeployDG-${stageName}`,
            {
                deploymentGroupName: serviceName,
                application: codeDeployApp,
                deploymentConfig: ecsDeploymentConfig
            }
        );
    
        return deploymentGroup;
      }

〜中略〜

        const configureCodeDeployStep = new pipelines.ShellStep("ConfigureBlueGreenDeploy", {
            input: this.pipeline.cloudAssemblyFileSet,
            additionalInputs: {
              'dockerOutput': dockerBuildStep.primaryOutput!
            },
            primaryOutputDirectory: 'codedeploy',
            commands: [
              "ls -l",
              "pwd",
              "ls -l dockerOutput/",
              "ls -l codebuild/",
              "cp dockerOutput/imageDetail.json codedeploy/",
              "cd codedeploy",
              "chmod a+x codedeploy_configuration.sh",
              "./codedeploy_configuration.sh"
            ],
            env: {
              'TASK_EXEC_ROLE': `tutorialEcsExecutionRole`,
              'APPLICATION': 'crossAccountEcsBGDeployApp',
              'FARGATE_TASK_DEFINITION': 'crossAccountEcsBGDeployDef'
            }
        });
        const deployStep = new CodeDeployStep(
            `codeDeploy`,
            configureCodeDeployStep.primaryOutput!,
            this.referenceCodeDeployDeploymentGroup(env, "crossAccountEcsBGDeployApp", ecsDeploymentConfig, stageName),
            stageName
        );
      
        // dockerBuildStep をステージに追加
        stageDeployment.addPre(dockerBuildStep);

        // configureCodeDeployStep と deployStep を dockerBuildStep の後に追加
        deployStep.addStepDependency(configureCodeDeployStep);
        stageDeployment.addPost(configureCodeDeployStep, deployStep);

ディレクトリ名 ファイル名 役割
lib/codedeploy codedeploy_configuration.sh 同ディレクトリのtemplate-taskdef.jsonとtemplate-appspec.yamlを加工するためのシェルスクリプト。
lib/codedeploy template-appspec.yaml CodeDeployが参照するデプロイ指示ファイル。
lib/codedeploy template-taskdef.json ECSのタスクを定義するためのテンプレートファイル。

このステップもそこまで複雑なことは実施しておらず、基本的にappspecとタスク定義ファイルの文字列置換に留まります。

codedeploy_configuration.shの操作はシンプルに以下の流れです。

  1. jqをyumでインストール
  2. タスク定義ファイルのテンプレートからタスク実行ロールとECSのアプリケーション名、タスク定義ファイル名を置換
  3. taskdef.jsonとしてアーティファクトに保存
  4. appspecのテンプレートからECSのアプリケーション名を置換
  5. appspec.ymlとしてアーティファクトに保存

図にすると以下のイメージとなります。

以上でpipelinesモジュールの話から逸れましたがECSへのデプロイまでの準備段階のステップの紹介となりました。

まとめ

pipelinesモジュールはやはりCodePipelineモジュールと比べると複雑だと思います。 CDKの公式サンプルでもpipelinesを用いたサンプルはどんどん公開されていくと思っており、サンプルが充実すればpipelinesモジュールの敷居も低くなると思います。

今の所、複雑なことをしていない構成、かつマルチアカウントでパイプラインを伸ばしてデプロイするような用途であればマッチしているんじゃないかなぁと思います。

また、ECSのタスク定義ファイルは少し独特な感触があります。初心者の頃は理解が進みませんでした。

私は下記の本で基本を習得したのですが、画面が変わってしまった2023/10でもまだ現役の本だと思います。

ECS入門したい方はぜひ読んでみてください。

https://www.amazon.co.jp/AWS%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E8%A8%AD%E8%A8%88%E3%83%BB%E6%A7%8B%E7%AF%89-%E6%9C%AC%E6%A0%BC-%E5%85%A5%E9%96%80-%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BE%E9%87%8E%E6%9D%91%E7%B7%8F%E5%90%88%E7%A0%94%E7%A9%B6%E6%89%80/dp/4815607656