Edited at
iRidgeDay 7

ECS + GitLab CIでカナリアリリースみたいなことを実現する

この記事はiRidge Advent Calendar 2018 の7日目の記事です。


tl;dr;

私の運用しているシステムではGitLab CI + ansibleを利用して、リリースの度にEC2を半自動+手順書で作り変える運用をしているんですが、どうしてもリリースに時間がかかってしまうため、アプリケーションのコンテナ化 + コンテナ向けのデプロイの仕組みづくりを推進することになりました。

その中でECSとGitLab CIを用いたカナリアリリースの仕組み等を考えて検証を行ったのでこの記事ではそれについて紹介していきます。


はじめに

デプロイの全体の流れ的なところを中心に書いていきます。ECSよりGitLab CIの要素が強めです。

AWSのECSクラスタの構築の仕方等いろいろ省略してたりするので読みにくいかもしれませんが、ご容赦ください。


カナリアリリースとは

本番環境に部分的に新しいバージョンのアプリケーションをデプロイし、カナリア環境のアプリケーションに問題ないことを確認してから全体の更新を行なうリリース手法のことです。

下記にカナリアリリースのコンセプト等が書かれています。

https://cloudplatform-jp.googleblog.com/2017/04/how-release-canaries-can-save-your-bacon-CRE-life-lessons.html


作りたい仕組みの全体像

カナリアデプロイ計画.jpg


AWS側の構成

検証として、ECSクラスタ(EC2)を作っておきます。検証としてnginxのTaskを利用するのでALBとそれに紐づくTargetGroupも用意しました。

ECSへTaskをデプロイするためのツールとしてはecs-cliを利用することにしました。採用の経緯はdocker-composeのような形でECSのServiceの更新ができ、個人的に使いやすいと思ったためです。


GitLab CIで準備するJob

GitLab CIに次のbuild,canary-deploy,deployの3つのjobを定義したパイプラインを作ります。それぞれの役割は下記のような内容になっています。(商用に適用する際はアプリケーションのユニットテストやら別のjobも入ってくると思います。)

パイプラインはリポジトリでタグを作成したタイミングで生成され、build jobまでは自動実行、deploy系のjobはマニュアル実行という形を想定しています。


  • build



    • docker buildによるimageの生成、Registryへのpushを行なう。

    • 自動実行



  • canary-deploy


    • ECSのカナリア用のServiceにデプロイを行なう

    • まず、こっちのjobを実行して、カナリア用のServiceに新しいバージョンのタスクを配置

    • 実行はGitLab CI の when: manualを利用した手動実行



  • deploy


    • ECSの本番用のServiceにデプロイを行なう

    • カナリアリリースの内容が問題なかったら実行

    • 実行はGitLab CI の when: manualを利用した手動実行




ディレクトリの構成

.

├── .gitignore
├── .gitlab-ci.yml
├── Dockerfile # テスト用のnginxファイル
├── conf
│ └── test.conf # テスト用のnginx.conf
├── deploy
│ ├── docker-compose.yml
│ └── ecs-params.yml
└── html
└── index.html # nginxで表示させるindex.html


ecs-cliに渡すファイルの設定


docker-compose.yml

version: '3'

services:
canary-test:
container_name: canary-test
image: <Registryのpath>:${CI_COMMIT_TAG}
environment:
- TZ=Asia/Tokyo
ports:
- "80"
restart: always
logging:
driver: awslogs
options:
awslogs-region: ap-northeast-1
awslogs-group: /ecs/canary-test


ecs-params.yml

version: 1

task_definition:
task_execution_role: TaskExecRole
ecs_network_mode: bridge
task_size:
cpu_limit: 256
mem_limit: 512
services:
canary-test:
essential: True
repository_credentials:
credentials_parameter: "arn:aws:secretsmanager:ap-northeast-1:xxxxxxxxxxxxx:secret:ecs-canary-poc2-1ax2py"

今回はGitLabのDocker Registryを使ったので、Registryにアクセスするための情報をSecretsManagerに登録しました。


GitLab CIの設定

.gitlab-ci.ymlに下記のようにjobの定義をしました。


.gitlab-ci.yml

image: docker:latest

services:
- docker:dind

stages:
- build
- deploy

tag-build:
stage: build
only:
- tags
- triggers
before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
tags:
- docker

canary-deploy:
stage: deploy
variables:
AWS_REGION: ap-northeast-1 # AWS region
AWS_ACCOUNT_ID: xxxxxxxxxxxx # AWSのアカウントID
AWS_ACCESS_KEY_ID: $POC_AWS_ACCESS_KEY_ID # GitLab CIのSecret variableから取得
AWS_SECRET_ACCESS_KEY: $POC_AWS_SECRET_ACCESS_KEY # GitLab CIのSecret variableから取得
AWS_TARGET_GROUP_NAME: ecs-canary-test-poc # target groupの名前
AWS_TARGET_GROUP_ID: yyyyyyyyyyyyyyyy # target groupのID
AWS_ECS_CLUSTER_NAME: ecs-canary-test-poc-svc-cluster # ECSのクラスタ名
AWS_TASK_ROLE_NAME: TaskExecRole # Taskに割り当てるIAMRole名
ENVIRONMENT: poc
CONTAINER_NAME: canary-test
CONTAINER_PORT: 80
DEPLOYMENT_MAX_PARCENT: 100
DEPLOYMENT_MIN_PARCENT: 0
SERVICE_NAME: canary-release # ECSのService名
before_script: &deploy_before_script
- apk -v --update add python py-pip groff less mailcap curl
- pip install --upgrade awscli s3cmd python-magic
- aws sts get-caller-identity # 正しいIAMを読み込めているかを確認
- curl -o /usr/local/bin/ecs-cli https://s3.amazonaws.com/amazon-ecs-cli/ecs-cli-linux-amd64-latest
- chmod 755 /usr/local/bin/ecs-cli
- ecs-cli -v
script: &deploy_script
- echo ecs-cli compose -p ${SERVICE_NAME} -f deploy/docker-compose.yml --ecs-params deploy/ecs-params.yml --task-role-arn arn:aws:iam::${AWS_ACCOUNT_ID}:role/${AWS_TASK_ROLE_NAME} service up --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-1:${AWS_ACCOUNT_ID}:targetgroup/${AWS_TARGET_GROUP_NAME}/${AWS_TARGET_GROUP_ID} --container-name ${CONTAINER_NAME} --container-port ${CONTAINER_PORT} --cluster ${AWS_ECS_CLUSTER_NAME} --deployment-max-percent ${DEPLOYMENT_MAX_PARCENT} --deployment-min-healthy-percent ${DEPLOYMENT_MIN_PARCENT}
- ecs-cli compose -p ${SERVICE_NAME} -f deploy/docker-compose.yml --ecs-params deploy/ecs-params.yml --task-role-arn arn:aws:iam::${AWS_ACCOUNT_ID}:role/${AWS_TASK_ROLE_NAME} service up --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-1:${AWS_ACCOUNT_ID}:targetgroup/${AWS_TARGET_GROUP_NAME}/${AWS_TARGET_GROUP_ID} --container-name ${CONTAINER_NAME} --container-port ${CONTAINER_PORT} --cluster ${AWS_ECS_CLUSTER_NAME} --deployment-max-percent ${DEPLOYMENT_MAX_PARCENT} --deployment-min-healthy-percent ${DEPLOYMENT_MIN_PARCENT}
tags:
- docker
environment:
name: canary-poc
only:
- tags
when: manual

deploy:
stage: deploy
variables:
AWS_REGION: ap-northeast-1 # AWS region
AWS_ACCOUNT_ID: xxxxxxxxxxxx # AWSのアカウントID
AWS_ACCESS_KEY_ID: $POC_AWS_ACCESS_KEY_ID # GitLab CIのSecret variableから取得
AWS_SECRET_ACCESS_KEY: $POC_AWS_SECRET_ACCESS_KEY # GitLab CIのSecret variableから取得
AWS_TARGET_GROUP_NAME: ecs-canary-test-poc # target groupの名前
AWS_TARGET_GROUP_ID: yyyyyyyyyyyyyyyy # target groupのID
AWS_ECS_CLUSTER_NAME: ecs-canary-test-poc-svc-cluster # ECSのクラスタ名
AWS_TASK_ROLE_NAME: TaskExecRole # Taskに割り当てるIAMRole名
ENVIRONMENT: poc
CONTAINER_NAME: canary-test
CONTAINER_PORT: 80
DEPLOYMENT_MAX_PARCENT: 200
DEPLOYMENT_MIN_PARCENT: 100
SERVICE_NAME: release # ECSのService名
before_script: *deploy_before_script
script: *deploy_script
tags:
- docker
environment:
name: poc
only:
- tags
when: manual


個人的なポイントとしては、GitLab CIのEnvironmentsを利用している点です。deploy系のjobにそれぞれenvironmentを定義しておくことで、カナリア用にデプロイしたアプリケーションに問題があった場合、1つ前のバージョンに戻すことが容易になります。

https://docs.gitlab.com/ce/ci/environments.html

上記を設定して、リポジトリにタグを作成すると下記のようなパイプラインが作成されます。

デプロイするときは、canary-deployのjobを実行し様子を見て問題ないことが確認できたらdeployのjobを実行していく想定です。

※ 初回デプロイ時はTaskが1つしか作成されないので手動でスケールさせる必要があります。2回目以降はその時定義されているTask数を維持したまま更新する挙動になっています。

スクリーンショット 2018-12-06 19.44.38.png


更新をしてみる

nginxのv1のコンテナがデプロイされた状況から、カナリアデプロイ -> 通常デプロイ の順で更新をしてみようと思います。ALBにアクセスすると下記のようなページが表示される状況です。

スクリーンショット 2018-12-06 19.58.43.png

ECSは下記のような状態で、canary用のServiceは1 Task,通常用のServiceは3 Taskという状況で更新してみます。

スクリーンショット 2018-12-06 20.00.58.png


  1. nginxで表示させるindex.html内のアプリケーションバージョンをv2に変更し、pushします。

  2. バージョン2.0.0のタグを作成します。するとタグの内容をデプロイするパイプラインが構築されます。

    スクリーンショット 2018-12-06 20.07.57.png



  3. canary-deployをマニュアル実行して、カナリア用のサービスを2.0.0に更新します。


    • 完了後にALBにアクセスすると、4回に1回(タスクが全部で4つあるので)は下記のような新しいバージョンが表示されるようになります。

    スクリーンショット 2018-12-06 20.12.31.png




  4. deployをマニュアル実行して、全体のバージョンを2.0.0に更新します。


    • 完了後にALBにアクセスすると、新しいバージョンのみが表示される様になります。




ロールバックをしてみる

ロールバックには先程書いたように、GitLab CIのEnvironmentsを使います。CI/CD > Environmentsに移動すると下記のようなページが表示されます。

スクリーンショット 2018-12-06 20.20.11.png

Environmentの詳細に移動すると下記のようにデプロイの履歴が表示されます。

この一覧の戻したいバージョンの右側にあるRollbackボタンを押すと、そのバージョンをデプロイするためのjobが実行され、特定のバージョンに戻すことができます。

スクリーンショット 2018-12-06 20.22.34.png


まとめ

ECSのServiceを分離して、それぞれにデプロイするjobをGitLab CIで準備することで、カナリアリリースっぽいこと実現することができました。GitLab CIの仕組みをうまく活用することで、ロールバック等も比較的容易に行えそうかなと思っています。今回は同一のALBのTargetGroupを利用してしまいましたが、分離しておくことでカナリア部分のみの応答状況をCloudWatchで見れそうなので、そういった細かい点の見直しをしていきたいです。

今後はカナリアリリース後、エラーレートとかを見て問題なければ自動でリリースを続ける仕組みとか、新しいバージョンの割合を徐々に増やしていくような仕組みがあったらいいなーとか考えています。以上です。