エンジニアを目指す日常ブログ

日々勉強したことのメモ。独学ですので間違っていたらコメント等で教えてください。

CodePipelineを利用してECS Fargateでブルーグリーンデプロイメントする

はじめに

今回の記事の目的

CodePipelineを利用してECS Fargateでブルーグリーンデプロイメント(Blue-Green Deployment)する。

まず、ビルド、デプロイについて単体で理解し、それらの設定をCodePipelineに組み込んでいった。かなり苦労したので、エラーの内容も合わせて紹介する。

順々にやっていったことで20000字の超大作になってしまったが、チュートリアルの通りにやるだけだとなかなか理解できないので、本記事の流れで理解していくのがおすすめ。

主に参考にしたチュートリアル
https://pages.awscloud.com/rs/112-TZM-766/images/AWS_CICD_ECS_Handson.pdf

前提条件

以下の記事で作成したFargateを利用する。 ALB経由でインターネットからアクセス可能なコンテナを立ち上げた。 tomiko0404.hatenablog.com

また、コードコミットとの連携は以下の記事の通り完了している。 tomiko0404.hatenablog.com

【参考】CodePipelineを使用せずイメージだけ作り直した場合の動作確認

dockerイメージをECRにプッシュ

アプリを変更し、dockerイメージをビルドし直したら、ECRにpushする。 ECR上でもlatestが最新となった。

この状態でALBにHTTP接続してみてもアプリは変更前(旧バージョン)の状態であった。

Webアプリは旧バージョン
Webアプリは旧バージョン

最終的に用意する必要があるファイル

元々作っていたプロジェクトフォルダ直下に、以下3ファイルを追加する必要がある。詳細は実施内容の中で記述する。

  • buildspec.yml
  • appspec.yml
  • taskdef.json

これらのファイルはCodeCommit上のリポジトリに存在しないと意味が無いので注意。 作成したり修正したりするたびに、pushする必要がある。

buildspec.yml

buildspec.ymlとは、CodeBuildがビルド時に実行する処理を記載するファイル。

チュートリアルを通してCodeBuildを理解する #reinvent | DevelopersIO

appspec.yml

appspec.ymlとは、CodeDeployを使ってデプロイする際の条件を記載したもの。

taskdef.json

taskdef.jsonとは、「タスク定義」を記載したもの。

CodeBuildとCodeDeployの理解

CodePipelineは、ソースコードのコミット→ビルド→デプロイの3ステップを自動で行ってくれるものである。

そもそもビルド、デプロイの概念を理解したいため、ビルド(Code Build)、デプロイ(Code Deploy)のステップをそれぞれ手動実行で利用してみる。

CodeBuildを利用してみる(手動)

IAMポリシー作成

「CodeBuildが」「ECRに」アクセスするためのIAMポリシーを作成する。

公式サイト( CodeBuild のDocker サンプル - AWS CodeBuild ) からのコピペで実施しようとしたがうまくいかなかった(ECRログインで権限エラー)ので、とりあえずフルアクセス権限にした。

フルアクセス権限作成

IAMのマネジメントコンソール→「ロールを作成」→「AWSサービス」→「CodeBuild」

ユースケースの選択
ユースケースの選択

Administrator Accessのポリシーをアタッチする。

Administrator Accessのポリシーをアタッチ
Administrator Accessのポリシーをアタッチ

最終的に以下のようになっていた。CodeBuildでIAMロールを設定すると自動で必要なポリシーもつくと思われる。

最終的なIAMロール
最終的なIAMロール

【参考】公式サイトの内容

公式サイトでは、ポリシーに以下JSONを追加するようにとのことだった。うまくいかなかった原因は不明。

{
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:CompleteLayerUpload",
        "ecr:GetAuthorizationToken",
        "ecr:InitiateLayerUpload",
        "ecr:PutImage",
        "ecr:UploadLayerPart"
      ],
      "Resource": "*",
      "Effect": "Allow"
    },

buildspec.yml 作成

プロジェクトフォルダ直下(Dockerfileと同じ場所)に作成した。 公式サイト(CodeBuild のDocker サンプル - AWS CodeBuild)からのコピペ。

これを見ると、実行しているコマンドは手動でECRにプッシュしていた時と同じことをしているとわかる。(一部環境変数になっているが、後で設定する。)

version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...          
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG      
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG

CodeBuildプロジェクトを作成する

マネジメントコンソールの「CodeBuild」→ 「ビルドプロジェクトを作成する」。

公式サイト:
ビルドプロジェクトの作成 (コンソール) - AWS CodeBuild
に従って実施。

  • 環境イメージは「マネージド型イメージ」。
  • オペレーティングシステムは「Ubuntu」、ランタイムは「Standard」、イメージは「aws/codebuild/standard:4.0」、イメージのバージョンは最新のものとした。(公式サイト( CodeBuild のDocker サンプル - AWS CodeBuild ) より)
  • 特権付与(Privileged)にチェックする
  • ソースの設定
    • プロバイダはCodeCommit
    • リポジトリは自分で作成したCodeCommitのリポジトリ
    • リファレンスタイプはブランチ。ブランチは「master」とした。
  • ビルドバッジは無効にした。
  • サービスロールは、先ほど作成した「ECRにアクセスできるロール(adminをつけたロール)」を設定する。
  • ビルド仕様は「buildspecファイルを使用する」を選択。オプションのBuildspec名は記載不要。デフォルトでbuildspec.ymlを利用するようになっているため。
  • アーティファクトは「なし」とした。
  • 環境変数の設定。buildspecファイルに記載した環境変数を、ECRに手動でpushするときのコマンドと見比べながら以下のように設定。
パラメータ
AWS_DEFAULT_REGION ap-northeast-1
AWS_ACCOUNT_ID [AWSアカウント(数字12桁)]
IMAGE_TAG latest
IMAGE_REPO_NAME ay-s-react-practice-6(ECRのリポジトリ名)

参考:CodeBuildプロジェクト作成画面
参考:CodeBuildプロジェクト作成画面

ビルドの実行

「ビルドを開始」で手動ビルド(=CodeCommitからECRへのpush)を実施する。 何とか成功。

失敗するときはビルドプロジェクトに設定したIAMロールに、ECRへのアクセス権限があるか要確認。

成功
成功

ECR上に新たなイメージがビルドされていることを確認した。 ECR上に新たなイメージがビルドされていることを確認した。

これでビルドは完了。

CodeDeployを利用してみる(手動)

以下記事で作成したサービスを、CodeDeployを使ってデプロイする。
ECS(Fargate)でコンテナを起動その2(ALBを利用したルーティング&CodeDeployの下準備) - エンジニアを目指す日常ブログ

前提として必要な設定

前回の記事で完了しているが、事前に必要な設定として以下のようなものがある。

  • Fargateサービスの作成時にALBが設定されていること。
  • CodeDeploy用のIAMロールが作成されていること。
  • Fargateサービスの作成時に、デプロイ方法をBlue/Greenデプロイメントと設定していること。
  • Fargateサービスの作成時に、「CodeDeploy のサービスロール」にCodeDeploy用IAMロールを設定していること。

公式サイト「CodeDeploy のBlue/Greenデプロイメントを実行する」以降の手順を実施する。

AWS CodeDeploy による AWS Fargate と Amazon ECS でのBlue/Greenデプロイメントの実装 | Amazon Web Services ブログ

タスク定義の新バージョンを作成する

タスク定義から、現行バージョンのタスク定義にチェックを入れ「新しいリビジョンの作成」をクリック。Nameタグのみ、値を少し変えて作成完了する。

タスク定義の新バージョンを作成
タスク定義の新バージョンを作成

サービスの更新を手動で実行

ECSコンソールの「サービス」→立ち上げているサービスにチェック→「更新」ボタンをクリック。

サービスの更新
サービスの更新

サービスの設定画面
  • リビジョンは2(latest)に変更する。

サービスの設定画面
サービスの設定画面

  • デプロイメントの設定は変更なし。 デプロイメントの設定は変更なし

  • ネットワークの設定も変更なし。 ネットワークの設定も変更なし

  • オートスケーリングも設定なしのまま。

  • サービスの更新を実行する。

デプロイ開始

CodeDeployによるBlue/Greenデプロイメントが開始した。

サービスの更新完了
サービスの更新完了

デプロイに失敗した・・・・・・・

デプロイ失敗
デプロイ失敗

失敗したので、ロールバックしてくれている模様。つまり、今リリースしている資材には影響を与えていない。

ロールバック
ロールバック

デプロイ失敗のログ確認

CodeDeployのコンソール画面にはログが出てこなかったが、ECSコンソール画面の「サービス」→「ログ」のタブからログを確認することができた。

また、「タスク」タブで「Stopped」を選ぶと、起動しようとして停止を繰り返すタスクがたくさん存在していることがわかる。停止を繰り返して60分たったので、デプロイがタイムアウトし、失敗となった。

デプロイ失敗の原因

Dockerfileの中身にミスがあった。CodeBuildの失敗時に色々試していた時に、コンテナのWORKDIRを変更してしまっていた。その結果/appフォルダ内にあるpackage.jsonが見つからずエラーとなっていた。

FROM node:14.15.4-slim

RUN apt-get update && apt-get install -y locales \
    && locale-gen en_US.UTF-8 \
    && localedef -i en_US -f UTF-8 en_US.UTF-8 \
    && echo "export LC_ALL=en_US.UTF-8" >> ~/.bashrc \
    && ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

COPY ./app /app
WORKDIR /app
RUN npm install
ENV PORT 80
CMD ["npm", "start" ] 

WORKDIR / ←←これが原因だったため、削除

RUN apt-get install -y \
    wget \
    curl \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

Dockerfileを修正し、CodeCommitにpush→CodeBuildを手動実行→サービスの更新を再実行した。

サイトを参考にした。
CodeDeployを使用したデプロイがタイムアウトとなり失敗する - あとらすの備忘録

デプロイに成功

デプロイに成功した。このまま1時間待機して問題が無ければ元のタスクが終了する。

デプロイステータス
デプロイステータス

手動で「元のタスクセットを終了」することもできた。

元のタスクセット終了後
元のタスクセット終了後

タスクの起動状況を見ると以下のように遷移。一時的に2つのタスクが起動し、デプロイ終了すると1つになる。

タスクの起動状況
タスクの起動状況

ALBにアクセスすると、修正版のWebアプリが表示された。Deploy画面のStep2まで完了した時点で、ALBの向き先は新バージョンのタスクに変更されていた(新バージョンのアプリを閲覧できた)。

修正版のWebアプリ
修正版のWebアプリ

CodePipelineを使った自動デプロイ(この記事の本番)

本構成のCodePipelineの役割

  • CodeCommit上のソースコードの変更を検知して、パイプラインを開始してくれる。
  • パイプラインが開始すると、CodeBuildでdockerイメージ化&ECRへのイメージプッシュを実施(そのようにbuildspec.ymlを記載している)してくれる。
  • 続いて、パイプラインでCodeDeployを動かしてFargateのサービスを更新してくれる。

パイプラインの作成

パイプラインを作成する。AWSマネジメントコンソールの「CodePipeline」→「新規のパイプラインを作成する」。

サービスロール

  • サービスロールは新規作成する。
    CodePipeline:パイプラインの設定
    CodePipeline:パイプラインの設定

ソースプロバイダ

  • ソースプロバイダーは「CodeCommit」
  • プロジェクトのリポジトリとブランチ名を設定する。
    CodePipeline:ソースステージの設定
    CodePipeline:ソースステージの設定

ビルドステージ

  • ビルドステージを追加する。
  • プロバイダは「AWS CodeBuild」を選択する。
  • プロジェクト名は先ほど作ったBuildプロジェクトを指定した。
    CodePipeline:ビルドステージの追加
    CodePipeline:ビルドステージの追加

デプロイステージ

  • デプロイステージを追加する。
  • デプロイプロバイダーは「Amazon ECS(ブルー/グリーン)」を選択。
  • AWS CodeDeployアプリケーション名を選択する。アプリケーションとは、CodeDeployでのデプロイの設定をまとめたもの。
    ※【利用中のECSサービスの頭に「AppECS-[クラスタ名]」が追記されたアプリケーション名】が選べるようになっていた。これは先ほど手動でデプロイしたときに作成されたもの。
  • AWS CodeDeployデプロイグループを選択する。デプロイグループとは、デプロイする単位をグループ化したもの。
    ※【利用中のECSサービスの頭に「DgpECS-[クラスタ名]」が追記されたデプロイグループ名】が選べるようになっていた。これも先ほど手動でデプロイしたときに作成されたもの。以下のような内容が設定されていた。

デプロイグループに設定されている内容
デプロイグループに設定されている内容

CodePipeline:デプロイステージの追加
CodePipeline:デプロイステージの追加

ひとまずパイプラインの作成を完了した。

デプロイステージの編集

taskdef.jsonファイルを作成する

先ほど(タスク定義の新バージョンを作成する)で作成したタスク定義をJSON形式でコピーしてきて、プロジェクトフォルダ直下に新規作成したtaskdef.jsonファイルに貼り付けた。

タスク定義(JSON)
タスク定義(JSON

その後、

 "image": "イメージのタグ名",

となっている部分を

 "image": "<IMAGE1_NAME>",

と書き換える。

appspec.ymlファイルを作成する

プロジェクトフォルダ直下にappspec.ymlを作成し、公式ドキュメントに従い、以下の通り記載する。

公式ドキュメント:
チュートリアル: Amazon ECR ソースと、ECS と CodeDeploy 間のデプロイを含むパイプラインを作成する - AWS CodePipeline

コンテナ名は、taskdef.jsonで定義したタスクの中で作成しているコンテナ名である必要がある。 appspec.yml

version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "ay-s-ReactPrc6-forALB-vpc16-container"
          ContainerPort: 80

デプロイステージのアーティファクトを設定する

アーティファクトとは、CodePipeline上で前のステージから次のステージに引き渡すもの。(例:SorceからBuildeへ)

デプロイステージのアクションを編集する。

pipelineの「編集」→「ステージを編集する」→「Deploy」の編集ボタンをクリックする。

  • 入力アーティファクトを「Source Artifact」と「Build Artifact」とする。
  • Amazon ECS タスク定義を「SourceArtifact」の「taskdef.json」とする。
  • AWS CodeDeploy App Spec ファイルを「SourceArtifact」の「appspec.yml」とする。
  • 入力アーティファクトを持つイメージの詳細を「Build Artifact」とする。
  • タスク定義のプレースホルダー文字を「IMAGE1_NAME」とする。

デプロイアクションの編集
デプロイアクションの編集

imageDetail.jsonを作成する

イメージファイルのURLを指定するためのimageDetail.jsonを作成する。 imageDetail.jsonは、ビルドステージで自動作成させるため、buildspec.ymlにファイル作成のための処理を追記する。

※今回はBlue/Green デプロイのためimageDetail.jsonだが、ローリングデプロイの場合は imagedefinitions.jsonとなり、フォーマットも異なるので注意。(原因②artifactのファイル名も参照)

まず、buildspec.ymlのpost_build:の処理の中に以下を追記。

      - printf '{"ImageURI":"%s"}' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > imageDetail.json

イメージURLをimageDetail.jsonに書き込んでいる。

また、ファイル末尾に以下を追記。

artifacts:
    files: imageDetail.json

全文は以下となる。
buildspec.yml

version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...          
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG      
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - echo Writing image definitions file....
      - printf '{"ImageURI":"%s"}' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > imageDetail.json

artifacts:
    files: imageDetail.json

Build後、ArtifactファイルはS3に保存されているため、ダウンロード可能。

ビルドの実行履歴
ビルドの実行履歴

中身は以下のように記載されている。

imageDetail.json

{"ImageURI":"XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/ay-s-react-practice-6:latest"}

※XXXXXXXはアカウントID

設定ファイルをコードコミットにpush

appspec.ymlbuildspec.ymltaskdef.jsonを下記の通り配置できたので、コードコミットにpushする。

設定ファイルの配置
設定ファイルの配置

$ git add .
$ git commit -m "設定ファイルを作成"
$ git push origin_codecommit master

コードコミットの設定:
tomiko0404.hatenablog.com

動作確認

ソースコードを変更して、CodeCommitにプッシュ。

※実際は、下記のとおり問題にぶつかり続けたので、各種設定ファイルの変更→コミットをかなりの回数実施しています。

$ git add .
$ git commit -m "ソースコードを変更"
$ git push origin_codecommit master

やっとできた!

コードパイプライン実行画面
コードパイプライン実行画面

Deployが進行中になっているのは、1時間の切り戻し待機中(Blue/Greenデプロイ)のため。

置き換えタスクセットがトラフィックを処理していればOK。

デプロイ詳細
デプロイ詳細

ブラウザから確認

ALBのDNS名にアクセスすると、変更が反映されていた。

ブラウザから確認
ブラウザから確認

DNS名の確認方法:
ECS(Fargate)でコンテナを起動その2(ALBを利用したルーティング&CodeDeployの下準備) - エンジニアを目指す日常ブログ

エラー対処のご紹介

ハマったエラーの紹介。
※ここまでの章は、ハマったエラー原因は解消した状態での結果を載せている。

エラー#1:BuildArtifactへのアクセス拒否

デプロイステージで以下のエラーが発生。

You are missing permissions to access input artifact: BuildArtifact.

原因と解決策

buildspec.yml内でBuildステージのアーティファクトを作成できていなかったことが問題。imageDetail.jsonを作成するの手順を実施することで解消。

エラー#2 BuildArtifactの読み込み時にエラー

なんとかBuildArtifactを作れたが、以下エラーが連発。

Exception while trying to read the image artifact file from: BuildArtifact.

原因①イメージ名の環境変数間違い

環境変数$IMAGE_REPO_NAMEと自分でCodeBuild上で設定していたが、チュートリアルに沿って$REPOSITORY_URIにしてしまっていた。

原因②artifactのファイル名

imagedefinition.jsonではなく、imageDetail.jsonでないとだめ。

  • ECS標準デプロイの場合:imagedefinitions.json
  • Blue/Greenデプロイの場合:imageDetail.json

原因③artifactファイルの中身のパラメータ名違い

チュートリアルがECS標準デプロイ前提だったので、中身の書き方もそのまま間違えていた。

imagedefinitions.jsonの場合(誤)

[
  {
    "name": "sample-app",
    "imageUri": "11111EXAMPLE.dkr.ecr.us-west-2.amazonaws.com/ecs-repo:latest"
  }
]

imageDetail.jsonの場合(正)

{
"ImageURI": "ACCOUNTID.dkr.ecr.us-west-2.amazonaws.com/dk-image-repo@sha256:example3"
}

artifactファイルの書き方
イメージ定義ファイルのリファレンス - AWS CodePipeline

エラー#3:コンテナ名まちがい

デプロイステージで以下のエラーが発生。

The ECS service cannot be updated due to an unexpected error: The container ay-s-ReactPrc6-forALB-vpc16-container-codepipeline does not exist in the task definition. (Service: AmazonECS; Status Code: 400; Error Code: InvalidParameterException; Request ID: XXXXXXXX; Proxy: null). Check your ECS service status.

原因と解決策

appspec.ymlファイルの中でContainerName:を設定する際、勝手に名前を付けてしまっていた。エラーが出たため、タスク定義の中で設定されているコンテナ名に修正した。

エラー#4

デプロイを開始したが、その後タスクが起動と停止を繰り返す現象。

以下の状態で止まり、60分でタイムアウトする。

デプロイ失敗
デプロイ失敗

さらに、今回はログが出る前にタスクがStopしている模様。

サービスのログ
サービスのログ

ただし、タスクのステータス画面に怪しげな文言が出ていたので確認。

タスクのステータス画面
タスクのステータス画面

STOPPED (CannotPullContainerError: inspect image has been retried 1 time(s): failed to resolve ref "docker.io/library/ay-s-react-practice-6:latest": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: ...)

原因と解決策

ECSのタスク定義画面から、コンテナの設定を見てみると、「イメージ」にちゃんとしたURLが入っていない。

ECSのタスク定義確認
ECSのタスク定義確認

imageDetail.jsonの中身が

{"ImageURI":"ay-s-react-practice-6:latest"}

となっており、"imageUri"がURIになっていないことが原因と思われた。

そこで、imageDetail.jsonを作っているbuildspec.ymlを修正した。

誤:元の記載)
- printf '{"ImageURI":"%s"}' $IMAGE_REPO_NAME:$IMAGE_TAG > imageDetail.json

正:修正後)
  - printf '{"ImageURI":"%s"}' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > imageDetail.json

おわりに

土日をつぶして2週間かかったが、CodePipelineでソースコードをデプロイすることができた。

その他参考資料

docs.aws.amazon.com qiita.com