この記事はAkatsuki Advent Calendar 2018の12日目の記事です。 前回は hayamaruさんの、知覚メカニズムと網膜投影でした。
はじめに
継続的デリバリーという文脈で、素早く、安全にデプロイする手法について、近年多くの記事を見かけます。
例えば、カナリアのおかげで命拾い : CRE が現場で学んだこと でカナリアリリースについて詳しく触れられています。このアイデアは「たとえテスト環境で十分な検証をしたとしても、本番環境で問題が発生しないということを保証するのは難しいので、少しづつトラフィックを移動して問題があったらすぐにロールバックをしよう」というものです。
上記記事中に紹介されているKayentaといったツールを使ってカナリア分析を行い、自動的にカナリアリリースするというのも良いですが、人間による承認を元にデプロイするならもう少しコストの低い方法で良さそうです。
この記事では、CircleCIのワークフローを使って、承認を元にGoogle App Engineのトラフィックを移動するという形で、カナリアリリースを実現します。
サンプルアプリケーション
この記事のサンプルコードはcsouls/gae_go111-deploy_sampleに置いています。
また、サンプルプロジェクト名をproject-name
としているので、適宜読み替えてください。
(このコードは、Google Cloud Platformのリポジトリで公開されているGAEのサンプル、go11x/warmup からコピーして修正したものです。)
このアプリケーションは、初期処理が完了した後に、どれだけ時間が経過しているか?というのを表示するための実装と、ウォームアップリクエストを受け付けるための実装がされています。
GAEでのデプロイ
Google App Engineへのデプロイはとても簡単です。 Cloud SDK をインストールして、GCPコンソールから新しいプロジェクトを作り、以下のコマンドを実行したあとにデプロイするリージョンや確認に応答するだけで、アプリケーションのデプロイができてしまいます。
gcloud app deploy --project=project-name
結果
You are creating an app for project [project-name]. WARNING: Creating an App Engine application for a project is irreversible and the region cannot be changed. More information about regions is at <https://cloud.google.com/appengine/docs/locations>. Please choose the region where you want your App Engine application located: [1] asia-east2 (supports standard and flexible) [2] asia-northeast1 (supports standard and flexible) [3] asia-south1 (supports standard and flexible) [4] australia-southeast1 (supports standard and flexible) [5] europe-west (supports standard and flexible) [6] europe-west2 (supports standard and flexible) [7] europe-west3 (supports standard and flexible) [8] northamerica-northeast1 (supports standard and flexible) [9] southamerica-east1 (supports standard and flexible) [10] us-central (supports standard and flexible) [11] us-east1 (supports standard and flexible) [12] us-east4 (supports standard and flexible) [13] us-west2 (supports standard and flexible) [14] cancel Please enter your numeric choice: 2 Creating App Engine application in project [project-name] and region [asia-northeast1]....done. Services to deploy: descriptor: [/Users/y.tanaka/src/sandbox/gae_go111_deploy_sample/app.yaml] source: [/Users/y.tanaka/src/sandbox/gae_go111_deploy_sample] target project: [project-name] target service: [default] target version: [20181212t155052] target url: [http://project-name.appspot.com] Do you want to continue (Y/n)? y ... deploy log ... Deployed service [default] to [http://project-name.appspot.com]
デプロイの中身
gcloud app deploy
を実行することで何が行われているかは、コマンドを実行したあとの標準出力に表示されています。
Services to deploy: descriptor: [/Users/y.tanaka/src/sandbox/gae_go111_deploy_sample/app.yaml] source: [/Users/y.tanaka/src/sandbox/gae_go111_deploy_sample] target project: [project-name] target service: [default] target version: [20181212t160030] target url: [http://project-name.appspot.com]
この例では、gae_go111_deploy_sample/app.yaml
という App Engineの設定を元に、gae_go111_deploy_sample
をソースとして、project-name
というプロジェクトのdefault
というサービスの、バージョン20181212t160030
を作成し、そのバージョンでサービスのトラフィックを100%処理する設定にした上で、http://project-name.appspot.com というURLで公開する、という内容となっています。
gcloud app deploy
に、--no-promote
というフラグを指定すると、「新しいバージョンは作るが、トラフィックは移動しない」という動作になります。
デプロイのデフォルト動作
gcloud app browse --project=project-name
を実行してブラウザでアクセスしてみると、実装の通りインスタンス起動からの経過時間が表示されます。表示されている時間は約50msということなので、このインスタンスに対する最初のアクセスであることが分かります。
再度アクセスすると、初回アクセスからの経過時間が表示され、同じインスタンスで要求を受けていることが分かります。
GAEのコンソール、Logging -> ログから、Stackdriver Logging に自動的に記録されたアクセスログが確認できます。レイテンシの内容を確認すると、初回アクセスの応答は50msではなく、775msかかっているようです。体験としてもこれくらい待たされています。
また、トレースがサンプリングされている場合は、レイテンシをクリックすることでトレースの詳細を確認することができます。
どうやら、サービスのデプロイにより新しいプロセスが起動され、loading requestが発生しているようです。
このサービスではapp.yamlに以下の記載をしているので、warmup requestsが有効になっていますが、単純に gcloud app deploy
をしても、warmupが実行されない様です。
inbound_services: - warmup handlers: - url: /_ah/warmup script: auto login: admin
本番環境でデプロイの運用をする場合は、この動作についても何らかの対処が必要でしょう。
CircleCIの設定
デフォルトの動作を踏まえ、warmupを有効にする形で GAE & CircleCIでのカナリアリリースを設定していきます。
サービスアカウントの設定
この手順 に従い、circleci/gcp-cli@1.0.1を使う前提で、サービスアカウントの設定をします。気をつけることは以下です。
- GAE コンソールから、App Engine Admin API を有効にする必要があります。
- GAEでデプロイをさせるサービスアカウントには、「App Engine デプロイ担当者」「App Engine サービス管理者」「Cloud Build サービス アカウント」「ストレージのオブジェクト管理者」の権限が必要です。
- CircleCIのプロジェクト設定 -> BUILD SETTINGS -> Environment Variables -> Add Variable から、下表の環境変数を設定します。
環境変数 | 内容 |
---|---|
GCLOUD_SERVICE_KEY | base64 <service-account-json>.json の結果 |
GOOGLE_PROJECT_ID | GCPプロジェクトのID |
GOOGLE_COMPUTE_ZONE | 動作させるZone。空白を設定すると、Zone指定なし。※後続の項で説明するcircleci/gcp-cliを使う場合、環境変数の定義は必ずしておかなければいけない。 |
gcp-cli/initialize
stepのパラメータで環境変数名を指定することができるので、一つのリポジトリに対して複数の環境があっても大丈夫です。
CircleCIの設定ファイル
リポジトリ直下の.circleci/config.yml
に、以下の様に記述します。
version: 2.1 orbs: gcp-cli: circleci/gcp-cli@1.0.1 executors: golang: docker: - image: circleci/golang:1.11 gcloud: docker: - image: google/cloud-sdk:latest jobs: test: executor: golang steps: - checkout - restore_cache: keys: - go-mod-v1-{{ checksum "go.sum" }} - run: name: build command: go build - run: name: test command: go test deploy: executor: gcloud steps: - checkout - gcp-cli/initialize - run: gcloud app deploy --no-promote --version $CIRCLE_SHA1 --quiet set-traffic: parameters: before-traffic: type: string default: "0.99" after-traffic: type: string default: "0.01" executor: gcloud steps: - checkout - gcp-cli/initialize - run: | BEFORE_VERSION="$(gcloud app versions list --service=${GAE_SERVICE} --filter='traffic_split>0.5' --format='value(id)')" gcloud app services set-traffic --splits ${BEFORE_VERSION}=<< parameters.before-traffic >>,${CIRCLE_SHA1}=<< parameters.after-traffic >> --split-by=random --quiet promote: executor: gcloud steps: - checkout - gcp-cli/initialize - run: gcloud app services set-traffic --splits ${CIRCLE_SHA1}=1 --split-by=random --quiet --migrate workflows: version: 2 test_and_deploy: jobs: - test - deploy: requires: - test filters: branches: only: - master - hold-canary: type: approval requires: - deploy - set-traffic: name: canary requires: - hold-canary before-traffic: "0.01" after-traffic: "0.95" - hold-promote: type: approval requires: - canary - promote: requires: - hold-promote
以下、ポイントを解説します。
orbs
orb とは、CircleCI 2.1から追加された、設定をパッケージとして共有可能にするものです。
今回はGCP CLIを使うので、その初期設定でcircleci/gcp-cli@1.0.1
を利用しています。
orbs: gcp-cli: circleci/gcp-cli@1.0.1 ...snip... jobs: steps: - gcp-cli/initialize
deploy ジョブ
新しいバージョンを作るジョブです。
$CIRCLE_SHA1
をバージョン名として指定しています。これで、CircleCIから操作するバージョンを特定するのが簡単になります。
deploy: executor: gcloud steps: - checkout - gcp-cli/initialize - run: gcloud app deploy --no-promote --version $CIRCLE_SHA1 --quiet
set-traffic ジョブ
トラフィックの分割をするジョブです。 トラフィックの50%以上を割り当てられているバージョンを前のバージョンだと判断しています。
set-traffic: parameters: before-traffic: type: string default: "0.99" after-traffic: type: string default: "0.01" executor: gcloud steps: - checkout - gcp-cli/initialize - run: | BEFORE_VERSION="$(gcloud app versions list --service=${GAE_SERVICE} --filter='traffic_split>0.5' --format='value(id)')" gcloud app services set-traffic --splits ${BEFORE_VERSION}=<< parameters.before-traffic >>,${CIRCLE_SHA1}=<< parameters.after-traffic >> --split-by=random --quiet
promote ジョブ
トラフィックを新しいバージョンに100%移行するジョブです。
--migrate
オプションを付けることで、トラフィックを徐々に移行してくれます。
また、Warmup処理を実行してくれます。
promote: executor: gcloud steps: - checkout - gcp-cli/initialize - run: gcloud app services set-traffic --splits ${CIRCLE_SHA1}=1 --split-by=random --quiet --migrate
ワークフロー
test -> deploy -> hold-canary -> set-traffic(canary) -> hold-promote -> promoteの順に並ぶように、requiresで設定しています。
また、deployはmasterブランチでのみ実行されるので、それ以外のブランチではtestだけが実行されます。
hold-canary, hold-promote では type: approval
で設定しているため、ここで人間の承認が入るという設定になっています。
workflows: version: 2 test_and_deploy: jobs: - test - deploy: requires: - test filters: branches: only: - master - hold-canary: type: approval requires: - deploy - set-traffic: name: canary requires: - hold-canary before-traffic: "0.01" after-traffic: "0.95" - hold-promote: type: approval requires: - canary - promote: requires: - hold-promote
結果
masterブランチにpushしたあとに、deployジョブ(新バージョンの作成)が完了した状態です。
type: approval
のワークフロー定義により、hold-canary
で止まっていることが分かります。
hold-canaryを承認し、canaryジョブまで実行された結果です。
ワークフローの承認者がアイコンで表示されていること、次のhold-promote
で止まっていることが分かります。このジョブの前に、承認待ちのワークフローへのリンクをSlackへ通知するのも良いでしょう。
トラフィックの割当も新バージョンに1%, 旧バージョンに99%となっています。 この状態でアプリケーションエラーが発生していないか、新バージョンのレイテンシやその他メトリクスに悪化したものがないか、を確認することができます。
承認する際は、このようなダイアログが表示されるので、間違えてクリックしてしまっても大丈夫です。
--migrate
オプションを付けたことで、トラフィックが徐々に移行されていることが分かります。
Stackdriver loggingからログをみても、warmup リクエストが処理されており、2回目の応答は4ms程度で返せていることが分かります。
開発者の数が50名を超えるような大規模なプロジェクトになると、冒頭で紹介したKayentaのようなツールが必要になると思いますが、小規模であればこのCircleCIのワークフローを使った人間による承認でも十分なことが多いと思います。
設定にコストをかけずに、GAEのカナリアリリースを実現する手段として、CircleCIと組み合わせるアイデアを覚えておくと役に立つことがあるかもしれません。