CodePipelineからAWS FargateにサイドカーパターンのFireLensコンテナも含めBlue/Greenデプロイするパイプラインを作成するCloudFormationテンプレート

2021.09.19

Fargateに素のNginx(Webサーバ)とFireLens(Fluent Bit)込みの検証環境を構築するCloudFormationのテンプレートを以前紹介しました。アプリコンテナ(Webサーバ)とFluent Bitのデプロイ作業を簡略化するためにCodePipelineでCI/CDパイプラインを作成し連携させてます。デプロイ方式はプレースホルダを用いたBlue/Green デプロイメントを採用します。

前回の環境おさらい

前回アプリコンテナは素のNginxコンテナを利用していました。FireLens用のFluent Bitのイメージは自前の設定ファイルを読み込ませる都合、ECRにビルドした設定ファイル込みのイメージをpushしたものを利用していました。

今回はプレースホルダを利用してBlue/Greenデプロイをするためappspec.ymltaskdef.jsonが登場します。CodeBuildでイメージをビルドするためにbuildspec.ymlが必要になります。

今回使用するテンプレート

以下に置いてあります。

事前準備

CI/CDのCodePipelineを設定する前に準備が必要です。新登場する各種ファイル(appspec.yml, taskdef.json, buildspec.yml)を保存するレポジトリが必要になります。

CodeCommitとECR作成

前回作成したECRと同じ名前のイメージレポジトリ作成するため前回のは一度削除してください。

rainコマンドで以下のパラメータで作成した環境を用います。

rain deploy ./1-CodeSeries.yml sample3-code-stack --params \
	ProjectName=sample3,\
	Environment=dev,\
	CCRepositoryName1=webappservice,\
	ECRRepositoryName1=custom-logrouter-firelens,\
	ECRRepositoryName2=webapp

CodeCommitにソースコードと各種ファイルをPush

検証で使用したコードは以下に置いてあります。

taskdef.jsonのアカウントIDは環境に合わせて修正してください。

taskdef.json抜粋

	],
	"family": "sample3-dev-webapp-taskdefinition",
	"taskRoleArn": "arn:aws:iam::012346789012:role/sample3-dev-webapp-ECSTaskRole",
	"executionRoleArn": "arn:aws:iam::0123456789012:role/sample3-dev-webapp-ECSTaskExecutionRole"
}

その他全般ではsample3-dev-とプレフィックを付けている箇所が多数あります。構築時のrainコマンド例通りの指定した場合は修正不要です。必要に応じてCloudFormationのパラメータで指定しているProjectName, Environmentに合わせて適時修正してください。

CodeBuildの設定

Webアプリをビルドするためのbuildspec.ymlと、FireLensをビルドするためのbuildspec.ymlを用意しました。

.
├── README.md
├── appspec.yml
├── firelens
│   ├── Dockerfile
│   ├── buildspec.yml
│   └── extra.conf
├── taskdef.json
└── webapp
    ├── Dockerfile
    ├── buildspec.yml
    ├── go.mod
    ├── go.sum
    ├── handler
    │   └── handler.go
    └── main.go

WebアプリとFireLensではビルドする内容に違いがあるかもと思い分けて検証をはじめました。ほぼ同じ内容に落ち着きました。docker buildする対象のDockerfileのパスの指定が異なるだけです。

buildspec.yml

version: 0.2
env:
  variables:
    DOCKER_BUILDKIT: "1"
phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
      - REPOSITORY_URI=${ACCOUNT_ID}.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/${REPOSITORY_NAME}
      - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8)

  build:
    commands:
      - echo Build started on `date`
      - docker build -t $REPOSITORY_NAME:$IMAGE_TAG ./webapp
      - docker tag $REPOSITORY_NAME:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG

  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - printf '{"ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json

artifacts:
  files: imageDetail.json

${ACCOUNT_ID}と、${REPOSITORY_NAME}は環境変数を使っています。パイプライン作成のテンプレートで環境変数を渡しています。ポイントはCodeBuildに対する環境変数の設定ではなく、CodePipelineに対して環境変数を渡すところです。

環境変数の設定はCodePipelineのパイプラインに対して設定します。

CodeDeployの設定

Blue/Greenデプロイするためのappspec.ymlは1個です。CodePipelineの都合、Webアプリ、FireLens同時にビルドを行います。その後、同時にデプロイする方法しか取れませんでした。検討した他のパターンは最下部の「おわりに」で取り上げています。

つまり、Webアプリを修正すると変更のないFireLensも再度ビルドしてデプロイされます。逆もまた然りでFluent Bitの更新をかけると、アプリも再ビルド・再デプロイが行われます。

appsepc.ymlの設定内容ではコンテナ名を変更していた場合に修正が必要です。

appspec.yml

version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: "<TASK_DEFINITION>"
        LoadBalancerInfo:
          ContainerName: "webapp"
          ContainerPort: 80
        PlatformVersion: "1.4.0"

CodePipeline作成

テンプレート内で後々作成するデプロイグループ名を事前にsample3-dev-webservice-deploy-groupと指定しています。変更する場合は必要に応じてテンプレートを修正してください。

3-CodePipeline.yml

                DeploymentGroupName: "sample3-dev-webservice-deploy-group"

rainコマンドで以下のパラメータで作成した環境を用います。

rain deploy ./3-CodePipeline.yml sample3-pipeline-stack --params \
	ProjectName=sample3,\
	Environment=dev,\
	AppName1=webapp,\
	AppName2=logrouter,\
	PipelineName=webservice,\
	S3BucketName1=artifact,\
	CodeSeriesStackName=sample3-code-stack,\
	AppECRName=sample3-webapp,\
	FireLensECRName=sample3-custom-logrouter-firelens,\
	CodeCommitRepositoryName=sample3-webappservice

パイプラインが作成されると初回は自動実行されます。初回は確実に失敗します。

Deployフェーズで失敗します。もしDeployフェーズ以外のエラーの場合、環境依存でテンプレートの修正が必要になるかと思われます。sample3-devと固定で入力している箇所が疑わしいです。

デプロイグループの手動作成

初回は確実に失敗する理由はCloudFormationがデプロイグループの作成に対応していなく必要なリソースが足りていません。テンプレートからすべて作成できませんでした。

以下リンクのLambdaでカスタムリソースでデプロイグループを作成していた意味を理解できました。

デプロイグループ以外の設定はCodePipelineテンプレートで作成してあります。テンプレート内で決め打ちしてデプロイグループ名を指定しています。今回はsample3-dev-webservice-deploy-groupの名前でデプロイグループを手動作成します。事前にデプロイグループ名を変更している場合は置き換えて読み進めてください。

手動設定箇所

CodeDepolyからアプリーケーションを開きます。

デプロイグループの作成します。

デプロイグループ名は事前に設定した名前を入力してください。サービスロール、他はテンプレートからリソースを作成済みです。統一感ある名前のリソースを次々に選択していくだけです。

本番稼働リスナーポートは80番、テストリスナーポートは8080番を想定して作成しています。

デプロイ設定はお好みで設定してください。ここではBlue/Greenデプロイ時、待機時間10分を設けた設定としました。また、古いタスクは1時間15分間待機し、すぐに切り戻せる設定としています。(本当は15分のつもりだったのですが、この後デプロイしたときに1時間15分と表示されて気づきました)

デプロイグループの手動作成は完了です。サービスロールも含め必要なリソースはテンプレートで作成されています。設定は選択するだけですので難しくはないと思います。似た名前のリソースが先にあったときは注意してください。

パイプラインテスト

テストのため手動で変更をリリースします。

Deployフェーズが実行中になりました。

指定した10分間トラフィックを新しいタスクへルーティングするか否かの待機中です。

Blue/Greenデプロイしている感がでてきました。実際にWebブラウザからアクセスして表示を確かめてみましょう。

アクセステスト

ELBのDNS名に対してWebブラウザからアクセスします。

Blue(本番環境: 既存タスク)

Nginxのデフォルトページが表示されます。既存環境は素のNginxコンテナをデプロイしているため期待通りです。

Green(テスト環境: 新タスク)

テスト環境はポート番号が8080でルーティングされるよう設定しています。別の表示のWebページを確認できました。新タスクで起動している自前の設定を入れたWebコンテナの表示です。

ここまでの流れ

まずCodeCommitのレポジトリにGoのソースコードがあります。ソースコードをCodeBuildでビルドします。でき上がったバイナリファイルを別のイメージにコピーし、要はマルチステージビルドを行い最終的なできあがったイメージをECRにpushしています。CodeDeployがECRにある新しいイメージをFargateへBlue/Greenデプロイしました。テストリスナーポートを(8080)通じて新しいイメージのコンテナへアクセスできているのが今の状態です。

本番環境へ切り替える

トラフィックの再ルーティングを押して、切り替えます。

すぐに切り替わりました。

80番ポートでアクセスすると、自前の設定入りのWebコンテナの表示に切り替わっています。

8080番ポートは変化なし。Nginxのデフォルトページが表示され、アクセス先のタスクが入れ替わると想定していたのですが違いました。気になったので後ほどターゲットグループの動きを確認します。

待機時間が長いので早々に元のタスク終了します。

Blue/Greenデプロイ完了です。

パイプラインも完了しています。

Blue/Greenの状態とターゲットグループ関係

デプロイ完了後8080番ポートへアクセスすると80番ポートと同じ表示がされたので設定を確認しました。リスナーの設定をみると808080も同じターゲットグループに向いています。であれば、同じアプリコンテナにアクセスしているのだから当然同じ表示になりますね。

改めてBlue/Greenデプロイを行いリスナーとターゲットグループの変化を確認します。まず、切り替え前のこの状態で確認します。

リスナーを確認すると...

次にトラフィックを切り替え後のこの状態で確認します。

リスナーをみると同じターゲットグループになっています。

最後にBlue/Greenデプロイが完了したこの状態で確認します。

リスナーをみるとトラフィック切り替え後の設定と変化なしです。

CodeCommitへのPushからの実行テスト

Webアプリの表示名を編集してからCodeCommitにPushすることでパイプラインが実行され、Blue/Greenデプロイができるか確認します。

Hello, 網走こんにちは、あばしりに変更します。

main.go

package handler

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

func HomePage(c echo.Context) error {
	return c.String(http.StatusOK, "こんにちは、あばしり")
}

func HealthCheck(c echo.Context) error {
	return c.String(http.StatusOK, "Health Check OK")
}

お行儀が悪いですがmainブランチにPushします。

$ git push origin main

mainブランチの変更検知する設定はテンプレートでCloudWatch Eventsの設定を仕込んであります。

パイプラインの様子

mainブランチの変更をCloudWatch Eventsで検知しパイプラインが走りはじめました。

また切り替えの状態まで来ました。

80番ポートは以下の表示

8080番ポートは以下のひらがな表示

トラフィックの再ルーティングをクリックすると、80番ポートの表示はひらがな表示に切り替わりました。

CodeCommitの特定ブランチの変更をトリガーにしたBlue/Greenデプロイに成功を確認できました。

まとめ

  • CloudFormationではCodeDeployのデプロイグループの作成がサポートされていないため、すべてテンプレート化はできない
  • マルチレポ構成であればCodePipelineは相性が良い
    • モノレポ構成はCodePipelineからCodeBuild、CodeDeployを走らせる都合、全ビルド・全デプロイになるので相性が悪い
  • GitHub Flowのような固定ブランチ名ならCodePipelineでトリガーに使えるので相性が良い
    • feature/*のようにブランチ名が固定されていないものをトリガーにできない
      • CodePipelineからソースレポジトリのブランチの指定が固定ブランチのため

おわりに

Fargateの検証環境と、Fluent Bitの設定検証用途でCI/CDがたびたび必要になったためテンプレートにまとめました。と同時にCodePipelineでモノレポのような構成(アプリコンテナと、FireLensコンテナ)の場合、どうビルドして、どのようにデプロイするのか検証しました。この場合レポジトリにあるソースコードを全ビルド・全デプロイの構成をとるしかありませんでした。上手いことできないかと試してみたのですが簡単な手法は思いつきませんでした。

例えばマルチレポの様な構成でアプリ用のレポジトリ、Fluent Bit用のレポジトリと分けたとします。FireLensでサイドカーとしてFluent Bitが起動する都合、同じタスク内で共にコンテナとして起動します。双方のレポジトリから別々のパイプラインでデプロイしたくても、CodeDeployの仕様でECSのサービスにつき1つのデプロイグループしか関連付けられません。デプロイ方法が課題でこのパターンは断念しました。

デプロイグループ作成時のエラー

The ECS Service sample3-dev-service is already associated with the sample3-dev-webservice-deploy-group deployment group. An ECS Service can be associated with only one deployment group at a time.