エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

AWS Fargateのデプロイパイプライン(Gitlab > S3 > CodePipeline)を構築してみた

こんにちは、エムスリーエンジニアの園田です。

この記事はAWS FargateでElixirのコンテンツ配信システムを動かしてみた (実装編) - エムスリーテックブログの続きです。

エムスリーでは医療・ヘルスケアサイト向けのコンテンツ配信システムであるChuoiというサービスを運用しています。先日のポストで、ElasticBeanstalkからFargateに運用を切り替えたことについて書きました。

www.m3tech.blog

今回は前回に引き続きその実装編で、CodePipeline を利用したデプロイパイプラインの構築について書きます。

まずは構成のおさらいです。

f:id:ryoheisonoda:20180713112117p:plain

デプロイ周りは以下のような構成です。

f:id:ryoheisonoda:20180713115557p:plain

社内 Gitlab からの CI/CD パイプライン構築

先日の記事で述べたとおり、弊社ではソース管理にオンプレの Gitlab を使っており、 CodeBuild や CodePipeline のトリガとして利用できません。 そのため、Gitlab からは S3 へソースのアップロードのみを行い、以降の処理は CodePipeline で実行するという構成にしました。

デプロイパイプラインを構築した際の大まかな手順は以下の通りです。

  1. ビルドスクリプト (buildspec.yml) の実装
  2. ソース・中間成果物格納バケットの作成
  3. パイプライン用の IAM ロール作成
  4. CodePipeline の作成
    1. Source ステージの設定
    2. Build ステージの設定 (CodeBuild プロジェクトの作成)
    3. Deploy ステージの設定
  5. Gitlab CI のデプロイジョブを設定

ファイル・ディレクトリ構成

前回の記事にも記載しましたが、再掲します。 前回と違い、デプロイパイプラインと直接関係のないファイル類は省略しています。

.
├── dockerfiles/
│   ├── app/
│   ├── log/
│   ├── web/
│   ├── archive-and-upload-src.sh
│   ├── build-docker-images.sh
│   └── push-to-ecr.sh
├── .gitlab-ci.yml
└── buildspec.yml

buildspec.yml の作成

CodeBuild で実行するジョブを定義する buildspec.yml を実装します。
buildspec.yml では以下のことを行います。

  1. Dockerイメージのビルド
  2. Dockerイメージを ECR に push
  3. imagedefinitions.json の生成

buildspec.yml にすべての処理を書くと非常に煩雑になるため、各処理を実行するためのスクリプトは buildspec.yml とは別のファイルとしています。

Dockerイメージのビルド

ファイル・ディレクトリ構成にある dockerfiles/build-docker-images.sh に実装します。内容は以下の通りです。 環境変数MIX_ENVSECRET_KEY_BASEは CodeBuild プロジェクトのビルド変数として設定されているものが渡されます。
SECRET_KEY_BASEParameter Storeに暗号化されて格納しており、ビルド実行時に復号化されて環境変数としてプロセスに渡されます。

echo Build started on `date`
echo Building the Docker image...

docker build -t "app:latest" \
  -f dockerfiles/app/Dockerfile  \
  --build-arg MIX_ENV=${MIX_ENV} \
  --build-arg SECRET_KEY_BASE=${SECRET_KEY_BASE} \
  ./

docker build -t "web:latest" ./dockerfiles/web
docker build -t "log:latest" ./dockerfiles/log

echo Build completed on `date`

Dockerイメージを ECR に push

つづいて dockerfiles/push-to-ecr.sh の中身です。AWS_REGIONは CodeBuild のビルトイン変数なので指定する必要はありませんが、 AWS_ACCOUNT_IDは CodeBuild のビルトイン変数ではないため、ビルド変数としてプロジェクトに設定する必要があります。

ECR_REPO_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"

echo Logging in to Amazon ECR...
$(aws ecr get-login --region ${AWS_REGION} --no-include-email)

echo Pushing the Docker images...

IMAGE_TAG=$(get_image_tag)

for repo in app web log; do
    docker tag "${repo}:latest" "${ECR_REPO_URI}/${repo}:latest"
    docker tag "${ECR_REPO_URI}/${repo}:latest" "${ECR_REPO_URI}/${repo}:${IMAGE_TAG}"
    docker push "${ECR_REPO_URI}/${repo}:${IMAGE_TAG}"
    docker push "${ECR_REPO_URI}/${repo}:latest"
done

上のサンプルのIMAGE_TAGを取得する関数get_image_tagは以下のような実装になっています。

get_image_tag() {
  if [ -e .git-commit-hash ]; then
    cat .git-commit-hash
  elif [ ! -z "$CODEBUILD_RESOLVED_SOURCE_VERSION" ]; then
    echo ${CODEBUILD_RESOLVED_SOURCE_VERSION}
  else
    echo latest
  fi
}

ソースのルートに.git-commit-hashファイルがあればその内容を、 なければ CodeBuild のビルトイン変数であるCODEBUILD_RESOLVED_SOURCE_VERSIONを、それもなければlatestを返しています。

後述しますが、.git-commit-hashファイルは Gitlab CI から S3 にソースをプッシュする際に生成しています。 なお、 CodeCommit や Github をソースリポジトリとする場合、コミットハッシュは環境変数CODEBUILD_RESOLVED_SOURCE_VERSIONから取得可能です。 今回は Gitlab を利用するので、環境変数からは取得できないため、ファイルに書き出しています。

imagedefinitions.json の生成

以下の内容を dockerfiles/push-to-ecr.sh の末尾に追記します。

echo Writing image definitions file...
cat <<__JSON__ > imagedefinitions.json
[
  {"name": "app", "imageUri": "${ECR_REPO_URI}/app:${IMAGE_TAG}"},
  {"name": "web", "imageUri": "${ECR_REPO_URI}/web:${IMAGE_TAG}"},
  {"name": "log", "imageUri": "${ECR_REPO_URI}/log:${IMAGE_TAG}"}
]
__JSON__

CodePipeline のデプロイプロバイダにECSを利用する場合、このimagedefinitions.jsonが必要になります。
コンテナ名とイメージの対応を JSON で渡すことで、 CodePipeline が以下のことをやってくれます。

  1. imagedefinitions.jsonの通りにタスク定義のコンテナイメージを変更し、新しいタスク定義のリビジョンを作成 (コンテナイメージ以外のパラメータはそのまま)
  2. 新しいタスク定義で起動するようにECSサービスを更新

なお、タスク定義の新しいリビジョンは、 現在サービスに適用されているタスク定義から 作成されます。 サービスに適用されていない最新のタスク定義があったとしても、そちらは利用されないのでご注意ください。

buildspec.yml

ビルドとECRプッシュを別ファイルに切り出したので、 buildspec.yml は非常にシンプルです。

version: 0.2
phases:
  build:
    commands:
      - dockerfiles/build-docker-images.sh
  post_build:
    commands:
      - dockerfiles/push-to-ecr.sh
artifacts:
  files:
    - imagedefinitions.json

ポイントは、imagedefinitions.jsonartifactsとして保存することです。これをしないと CodePipeline がECSにデプロイできません。

ここまでで AWS リソースを作成するための準備が整いました。次からは AWS の各リソースを作成していきます。

作成するAWSリソースの一覧

マネジメントコンソールでパイプラインを作成する場合に必要なリソースの一覧です。 マネジメントコンソールで作成する場合は CloudWatch Event Rule など一部のリソースが強制的に自動生成されるため、 Terraform で作成する場合と顔ぶれが異なるので注意が必要です。

  • ソースおよびパイプラインの中間成果物を格納するバケット
  • パイプライン用の IAM ロール (面倒なのでまとめて1つ)
  • Parameter Store の SECRET_KEY_BASE パラメータ
  • CodePipeline のパイプライン
  • CodeBuild のプロジェクト

以下はマネジメントコンソール利用の場合に自動生成されるリソースです。 Terraform で作成する場合は以下のリソースも Terraform のテンプレートに含めます。

  • ソースの更新を検知してパイプラインを起動する CloudWatch Event Rule
  • 上記 CloudWatch Event Rule の IAM ロール

ソース・中間成果物格納バケットの作成

CodePipeline で S3 をトリガとする場合、以下のバケットが必要です。

  • パイプラインのトリガとなるソースのバケット
  • パイプラインの中間成果物を格納するバケット

Gitlab からソースをアップロードする S3 バケットを作成します。注意点は 1 点だけで、バージョニングを有効にするだけです。
バージョニングが無効な場合、 CodePipeline 実行時に エラーになります。作成時ではないので注意が必要です。

パイプラインの中間成果物を格納するバケットは、何も指定しなければ CodePipeline が自動生成しますが、Terraform フレンドリではないので作成したほうがいいです。
今回の場合はソース格納バケットと共用にしました。

パイプライン用の IAM ロール作成

パイプラインを構成する上で必要なIAM ロールは3つあります。

  • ソースの変更を検知して CodePipeline を起動する IAM ロール (codepipeline:StartPipelineExecution)
  • CodeBuild のビルド実行 IAM ロール (成果物の取得・保存と ECR, Parameter Store に対するアクセス)
  • CodePipeline 自体の IAM ロール (成果物の取得・保存と各ステージの実行権限)

最初のロールは、マネジメントコンソールで CodePipeline を構築する際に自動で作成されるため、今は作成しませんが、 Terraform で作成する場合は定義を実装する必要があります。

管理が煩雑にならないように、必要な権限を 1 つの IAM ロールにまとめました。
1 つのロールに複数の役割を持たせるのはアンチパターンですが、デリバリプロセス全体のロールということで、1 つにまとめても問題ないと判断しました。 すべての権限をまとめた IAM ロールのポリシーは以下のようになります。

まずは AssumeRolePolicy です。CodeBuild, CodePipeline の信頼関係を持ちます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": [
          "codebuild.amazonaws.com",
          "codepipeline.amazonaws.com"
        ]
      }
    }
  ]
}

続いて CodeBuild のための IAM ロールポリシーです。 ECR に対するアクセスと、パラメータの参照、ソース取得、成果物の格納、ログの出力の権限が必要です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::${SOURCE_ARTIFACT_BUCKET}",
        "arn:aws:s3:::${SOURCE_ARTIFACT_BUCKET}/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:PutLogEvents",
        "logs:CreateLogStream",
        "logs:CreateLogGroup",
        "ecr:GetAuthorizationToken"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "ssm:GetParameters",
      "Resource": "arn:aws:codepipeline:ap-northeast-1:${AWS_ACCOUNT_ID}:parameter/SECRET_KEY_BASE"
    },
    {
      "Effect": "Allow",
      "Action": "ecr:*",
      "Resource": "arn:aws:codepipeline:ap-northeast-1:${AWS_ACCOUNT_ID}:repository/*"
    }
  ]
}

Parameter Store に KMS を利用している場合は、上記に加え KMS による復号化権限も必要です。

CodePipeline のための IAM ロールポリシーです。CodeBuild, CodeDeploy の実行等の権限が必要です。 Deploy プロバイダに ECS を利用するため、ECS に関する権限が必要です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::${SOURCE_ARTIFACT_BUCKET}",
        "arn:aws:s3:::${SOURCE_ARTIFACT_BUCKET}/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iam:PassRole",
        "elasticloadbalancing:*",
        "ecs:*",
        "codedeploy:*",
        "codebuild:*",
        "cloudwatch:*"
      ],
      "Resource": "*"
    }
  ]
}

これらのインラインポリシー *1 を持った IAM ロールを作成して利用します。

Parameter Store の SECRET_KEY_BASE パラメータ

CodeBuild でビルド変数として利用する Parameter Store のパラメータは事前に設定しておく必要があります。
平文のパラメータであれば Terraform のテンプレートなどに含めれば問題ないですが、シークレットパラメータは Terraform 管理外にして CLI で事前に登録しておくのが楽です。 *2

aws ssm put-parameter \
  --name "SECRET_KEY_BASE" --value "xxx......" \
  --type "SecureString" --overwrite

CodePipeline のパイプライン

CodePipeline を作成することで、あわせて以下のリソースが作成されます。

  • ソースバケットを監視してパイプラインを起動するCloudWatch Event Rule
  • CodeBuild プロジェクト

マネジメントコンソールで CodePipeline を作成する際に必要なパラメータは以下の通りです。

  • ソースとなる S3 バケットと S3 オブジェクトのキー
  • CodeBuild のビルド変数
  • CodeBuild の IAM ロール (前段で作成済み)
  • ECS クラスター名と ECS サービス名 (前回の記事で作成済み)

パイプラインの作成

マネジメントコンソールで CodePipeline の作成を行うと、まずはじめにパイプライン名称の入力を求められます。 パイプライン名称を入力して次のステップボタンを押したら Source ステージの設定に続きます。

f:id:ryoheisonoda:20180731102729p:plain

Source ステージの設定

ソースを取得するバケットとオブジェクトのキーを入力します。これによりソースを監視する CloudWatch Event Rule と IAM ロールが自動生成されます。*3

以下を入力して次のステップボタンを押します。

  • ソースプロバイダ: Amazon S3
  • Amazon S3 の場所: s3://${ソースバケット名}/${オブジェクトキー}
  • 変更検出オプション: 変更が発生したときに Amazon CloudWatch イベントを使用してパイプラインを自動的に開始する (推奨)

f:id:ryoheisonoda:20180731102947p:plain f:id:ryoheisonoda:20180731102950p:plain

Build ステージの設定と CodeBuild プロジェクトの作成

続いてビルドステージを設定します。ビルドステージでは CodeBuild プロジェクトの作成を行います。

  • ビルドプロバイダ: AWS CodeBuild
  • 新しいビルドプロジェクトを作成
    • プロジェクト名: CodeBuild のプロジェクト名称
    • 説明: 任意の説明
    • 環境イメージ: AWS CodeBuild マネージド型イメージの使用
    • OS: Ubuntu
    • ランタイム: Docker
    • バージョン: aws/codebuild/docker:17.09.0
    • ビルド仕様: ソースコードのルートディレクトリの buildspec.yml を使用
  • キャッシュ: キャッシュなし *4
  • CodeBuild サービスロール: 前段で作成した IAM ロール
  • VPC:
    • VPC ID: 非 VPC
  • アドバンスト
    • ビルドタイムアウト: 任意のタイムアウト時間
    • 特権付与: チェックなし (ランタイムがDockerの場合、自動でONになる)
    • コンピューティング: 任意のリソース (build.standard.largeなど)
  • 環境変数
    • MIX_ENV: 環境に応じた値 (develop, staging, productionなど)
    • SECRET_KEY_BASE: TypeParameter Storeに設定し、値に Parameter Store の パラメータ名 を入力
    • AWS_ACCOUNT_ID: AWSのアカウントID (Build ステージで ECR リポジトリURI 特定に利用する)

上記を入力したらビルドプロジェクトの保存ボタンを押して CodeBuild プロジェクトを作成したら、 次のステップボタンを押してデプロイステージの設定に移ります。

f:id:ryoheisonoda:20180731103543p:plain f:id:ryoheisonoda:20180731103548p:plain f:id:ryoheisonoda:20180731103554p:plain f:id:ryoheisonoda:20180731103558p:plain

Deploy ステージの設定

最後にデプロイステージを設定します。

  • デプロイプロバイダ: Amazon ECS
  • クラスター名: 前回作成した ECS クラスター名
  • サービス名: 前回作成した ECS サービス名
  • イメージのファイル名: 空欄のまま

イメージのファイル名は、ECSデプロイプロバイダの場合、デフォルトでimagedefinitions.jsonを参照するようになります。 ファイル名を変更する場合は CodeBuild の buildspec.yml で artifacts に定義したファイル名を入力してください。

f:id:ryoheisonoda:20180731104007p:plain

動作確認

Gitlab にジョブを作成する前に、 CodePipeline の動作確認を行います。

アプリケーションコードを zip 圧縮して S3 の指定したパスにアップロードしてみます。

zip -r src.zip .dockerignore ./dockerfiles buildspec.yml \
  mix.exs mix.lock ./config ./lib ./priv ./rel \
  package.json tsconfig.json webpack.config.js yarn.lock ./ts/main

できあがった src.zip をマネジメントコンソールからアップロードしました。 CodePipeline が起動して ECS へのデプロイが完了することを確認します。

以上で CodePipeline による ECS へのデプロイパイプライン構築は完了です。

Gitlab CI のデプロイジョブを設定

デプロイ IAM ユーザの作成

Gitlab に設定する IAM ユーザを作成します。 設定するポリシーは CodePipeline で監視しているバケットとオブジェクトに対するs3:PutObject権限のみです。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::${SOURCE_ARTIFACT_BUCKET}/${SOURCE_OBJECT_KEY}"
    }
  ]
}

Gitlab プロジェクトに IAM 認証情報を設定

Gitlab プロジェクトに AWS の認証情報を設定します。メニューから Settings > CI / CD > Variables を選択して、 作成した IAM ユーザのAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYを指定します。

アップロードスクリプトの実装

ファイル・ディレクトリ構成にある dockerfiles/archive-and-upload-src.sh にデプロイのスクリプトを実装しました。 やってることはシンプルで、これだけです。

  1. Git のコミットハッシュをファイルに出力
  2. テストコードなどを除いたファイルを zip 圧縮
  3. 圧縮したファイルを S3 にアップロード
SOURCES="
.dockerignore ./dockerfiles buildspec.yml mix.exs mix.lock
package.json tsconfig.json webpack.config.js yarn.lock ./ts/main
./config ./lib ./priv ./rel
"

rm -f src.zip
git rev-parse HEAD | cut -c 1-8 > .git-commit-hash
zip -r src.zip .git-commit-hash $SOURCES

aws s3api put-object \
  --bucket $S3_BUCKET --key ${S3_OBJECT_KEY} \
  --body src.zip

Gitlab CI のジョブを作成

.gitlab-ci.ymlにデプロイのマニュアルジョブを追加します。

trigger_pipeline:
  image: python
  variables:
    AWS_REGION: ap-northeast-1
    S3_BUCKET: hogehoge-source-dev
    S3_OBJECT_KEY: hogehoge-src.zip
  before_script:
    - apt-get update && apt-get install -y zip git
    - pip install --upgrade awscli
    - aws configure set default.region ${AWS_REGION}
  script:
    - dockerfiles/archive-and-upload-src.sh
  when: manual

デプロイパイプラインの実行

Gitlab からマニュアルジョブを実行し、CodePipeline が正常終了すれば成功です。

次回は前回構築した Fargate 構成、および今回の CodePipeline を含めた Terraform テンプレートの説明をしたいと思います。

エンジニア募集中

エムスリーでは AWS の新サービスを試したいエンジニアを募集しています。 AWS に限らず、Firebase や GCP なども積極的に利活用しています。興味が湧いた方はぜひ Tech Talk やカジュアル面談にお越しください。 お問い合わせは以下フォームより、お待ちしております。

jobs.m3.com

*1:ユーザ定義ポリシーを作成してそれをロールにアタッチするというのがベストプラクティスのようですが、私はそれを実践する気はありません。 作成したポリシーを複数ロールで利用することがほぼありえないのと、 Terraform などを利用する場合、インラインポリシーでも十分に管理可能であるからです。 ユーザ定義ポリシーは依存関係の複雑化を促し、削除可能なリソースなのかどうかの判断を迷わせるので、私は Disposable なインラインポリシーを好んで使っています。

*2:シークレットパラメータの Infrastructure as Code 上での扱いに関してはいろんな方法があると思いますが、 私は手運用にしてドキュメントに運用手順を記載する方法を取っています。 なぜなら、シークレットをコード化するためにシークレット情報の暗号化など複雑な仕組みを構築しても、たいていの場合それは有効に機能せず、 結局は手作業で登録や更新を行うことになることが多いからです。扱うシークレットが少ないのであればなおさらです。

*3:CodePipeline の作成が完了したタイミングで作成されます。

*4:キャッシュを利用する場合はキャッシュを保存する S3 バケットに対する権限が必要