こんにちは。SREチームの山根(@fumikony)です。
このブログでも東口(@hgsgtk)が何度か紹介している即時資金調達できる金融サービス「YELL BANK(エールバンク)」のインフラまわりに関わっています。
今回は、YELL BANKのデプロイメントパイプラインを構築したときの工夫などを紹介します。
インフラ構成
まず大まかなインフラ構成について説明します。上の図は構成図です。
YELL BANKではGo言語でAPIサーバを開発しており、ビルドしたコンテナをECSのFargateモードを使って動かしています。
コンテナレジストリにはAWSのECRを使っています。
また複数AWSアカウント構成をとっていて、本番・ステージング・開発の各環境ごとに個別のAWSアカウントを用意しています。
今回のデプロイ機構ではこれらの環境のうち本番環境(prd)およびステージング環境(stg)を対象としました。
GitリポジトリにはGitHub、CIにはCircleCIを使っています。
デプロイ
ECSデプロイツールにはkayac/ecspressoを採用しました。
GitHub上のmasterブランチへのマージをトリガーとして、CircleCI内からecspressoを実行することでECSへのデプロイを実現しています。
以下、このデプロイ機構について説明していきます。
ecspressoについて
ecspressoはGo言語製のECSデプロイツールです。
今回ecspressoを採用した理由は主に2つあります。
1つは、aws ecs describe-task-definition
で出力されるjsonを、そのままecspressoで使うことができるという点です。これによって、まずECSのマネジメントコンソール上で試行錯誤してタスク定義を作り、それをそのままecspressoで使うという事ができます。これはECSの経験が浅い身としてはとても便利でした。
もう1つはテンプレート機能の存在です。これは、タスク定義のjson内に
{{ env `FOO` `bar` }}
という記述があると、その部分を環境変数FOO
で置き換えるというものです。bar
はFOO
がなかった場合のデフォルト値です。また、
{{ must_env `FOO` }}
という記述の場合はFOO
が無いとエラーになります。
この機能を使って、タスク定義内部で以下のように記述しました。
taskdef.json
{ "taskDefinition": { "containerDefinitions": [ { "name": "example", "image": "{{must_env `AWS_ACCOUNT_ID`}}.dkr.ecr.ap-northeast-1.amazonaws.com/basebank/example:{{env `D_TAG` `latest`}}",
これによって2つのことが実現できました。
- 環境変数
D_TAG
によって、dockerイメージのタグをデプロイ実行時に指定します(省略時はlatest
)。 - 環境変数
AWS_ACCOUNT_ID
によって環境ごとの差を吸収します。こうすることで、prdとstgのタスク定義を一括で管理できます。
なお、これらの機能はecspressoのREADMEで説明されているので、あわせて参照いただければと思います。
CircleCI設定
いきなりですが、今回作成した.circleci/config.yml
です。縦に長いですがご容赦ください。
.circleci/config.yml
# https://circleci.com/orbs/registry/orb/circleci/slack # https://circleci.com/docs/2.0/env-vars/#using-bash_env-to-set-environment-variables version: 2.1 orbs: slack: circleci/slack@2.0.0 commands: install_awscli: steps: - run: | sudo apt-get install python-pip sudo pip install awscli setenv_docker_tag: steps: - run: echo "export DOCKER_TAG=${CIRCLE_SHA1:0:8}" >> $BASH_ENV setenv_stg: steps: - run: echo "export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX" >> $BASH_ENV - run: echo "export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY_STG" >> $BASH_ENV - run: echo "export ECR_ENDPOINT='yyyyyyyyyyyy.dkr.ecr.ap-northeast-1.amazonaws.com'" >> $BASH_ENV setenv_prd: steps: - run: echo "export AWS_ACCESS_KEY_ID=YYYYYYYYYYYYYYYYYYYY" >> $BASH_ENV - run: echo "export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY_PRD" >> $BASH_ENV - run: echo "export ECR_ENDPOINT='zzzzzzzzzzzz.dkr.ecr.ap-northeast-1.amazonaws.com'" >> $BASH_ENV jobs: test: docker: - image: circleci/golang steps: - checkout # (略) build: ## (B) docker: - image: circleci/golang steps: - slack/notify: message: "begin build" - checkout - setup_remote_docker - install_awscli - run: name: docker build command: | make build - setenv_docker_tag - setenv_stg - run: name: stg docker tag & docker push command: | $(aws ecr get-login --no-include-email --region ap-northeast-1) docker tag "basebank/example" "${ECR_ENDPOINT}/basebank/example:latest" docker push "${ECR_ENDPOINT}/basebank/example:latest" docker tag "basebank/example" "${ECR_ENDPOINT}/basebank/example:${DOCKER_TAG}" docker push "${ECR_ENDPOINT}/basebank/example:${DOCKER_TAG}" - setenv_prd - run: name: prd docker tag & docker push command: | $(aws ecr get-login --no-include-email --region ap-northeast-1) docker tag "basebank/example" "${ECR_ENDPOINT}/basebank/example:latest" docker push "${ECR_ENDPOINT}/basebank/example:latest" docker tag "basebank/example" "${ECR_ENDPOINT}/basebank/example:${DOCKER_TAG}" docker push "${ECR_ENDPOINT}/basebank/example:${DOCKER_TAG}" deploy-stg: ## (C) docker: - image: circleci/golang steps: - slack/notify: message: "begin deploy-stg" - checkout - setup_remote_docker - install_awscli - setenv_docker_tag - setenv_stg - deploy: name: deploy-stg command: | make deploy-stg D_TAG=$DOCKER_TAG - slack/status deploy-prd: ## (C) docker: - image: circleci/golang steps: - slack/notify: message: "begin deploy-prd" - checkout - setup_remote_docker - install_awscli - setenv_docker_tag - setenv_prd - deploy: name: deploy-prd command: | make deploy-prd D_TAG=$DOCKER_TAG - slack/status workflows: ## (A) version: 2 test-build-deploy: jobs: - test - build: requires: - test filters: branches: only: master - deploy-stg: requires: - build filters: branches: only: master - slack/approval-notification: message: 'prdにデプロイするにはApproveが必要です' mentions: '<!here>' requires: - deploy-stg filters: branches: only: master - approve-deploy-prd: type: approval requires: - deploy-stg filters: branches: only: master - deploy-prd: requires: - approve-deploy-prd filters: branches: only: master
以下、工夫したポイントを解説していきます。
(A): workflows
デプロイの流れがtest-build-deploy
というワークフローに定義されています。
GitHubのmasterブランチにfeatureブランチがマージされると、以下のような流れでデプロイが進んでいきます。
- テスト
- ビルド
- stgへのデプロイ
- Manual Approval
- prdへのデプロイ
Manual ApprovalというのはCircleCIの機能で、ワークフローの次のjobに行く前に手動による承認を要求するというものです。
これをワークフローに挟み込んでおくことで、stgでの確認を終えるまでprdへのデプロイを待たせることができます。
また、ワークフロー内にslack/approval-notification
というものがありますが、これはCircleCIのSlack Orbを利用したもので、以下のようなSlack通知を簡単に出すことができます。(@
が重なっているのは、Orbが@here
に対応していないところに強引に入れたため)
(B): build
このjobでは、docker build、docker tag、docker pushを行っています。
この際、stgで動作確認したイメージそのものをprdにもデプロイしたいので、同一job内でstgとprd両方のECRに対してdocker pushしています。
また、dockerのtagとしては、gitのコミットハッシュの先頭8桁を使うようにしました。こうしておくことで、どのコミットからビルドされたイメージなのかわかります。
(C): deploy-stg, deploy-prd
各環境へのデプロイを実行するjobです。
具体的なデプロイコマンドはMakefileにラップして、例えばstgであれば make deploy-stg D_TAG=$DOCKER_TAG
のようにしています。この D_TAG
というのはecspressoに渡すための環境変数です。
Makefileのターゲットとしては以下のようになっています。
deploy-stg: bin/ecspresso AWS_ACCOUNT_ID=yyyyyyyyyyyy \ ./bin/ecspresso deploy --config=config/stg.yml deploy-prd: bin/ecspresso AWS_ACCOUNT_ID=zzzzzzzzzzzz \ ./bin/ecspresso deploy --config=config/prd.yml
Makefileにラップしておくことで、いざというときには手元からもデプロイしやすくなっています。
おわりに
CircleCIとecspressoを用いて、ECSへのデプロイメントパイプラインを構築しました。
しばらく使ってみるうちにいくつか気になるところも出てきたので、引き続き改良していこうと思っています。
また、今回ひととおりのECSデプロイを組んでみて、ECS自体にも慣れることができました。
次の機会があればCodePipelineによるBlueGreenデプロイメントを使うパターンなども試してみて、今回作ったものとの比較できればと考えています。そのときにはまた本ブログで紹介しますので、お楽しみに。