APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

シェルスクリプトでAppEngineへ簡単・安全にデプロイする

始めに

先進サービス開発事業部の山岡です。

私がバックエンドを開発しているNEIGHBORSはAppEngine(Go)の上に構築されています。
デプロイやトラフィック切り替えの際にはSDKに付属しているappcfg.pygcloudを使用することになりますが、極々簡単なスクリプトを組むことで地味ながら安全・便利になるので紹介したいと思います。

ちなみに、本当はCircleCIを使ったCI/CDの仕組みが作ってありますが諸事情により一時的に使っていない状態であるため各々の端末からデプロイしています。
再び使うようになった暁にはそちらの仕組みも紹介したいと思います。

デプロイスクリプト

手元でのデプロイを行う場合、うっかり作業中のブランチでデプロイしてしまったりする事故が想定されます。
またGitHub上でMergeしたことに満足してしまいgit pullを忘れローカルの古いものをデプロイしてしまったりといったこともあり得ます(実際一度やりかけました……)。
こういった人間がやらかしがちな単純ミスを回避するには機械的な対策が有効であるため、以下2点の安全装置を組み込みました。

  • 現在のブランチが master でない場合は中断
  • ローカルとリモートで master の最新ハッシュを比較し一致しない場合は中断

サンプル

#!/bin/bash
set -eu

BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ $BRANCH != "master" ] ; then # 現在のブランチが master でない場合は中断
    echo "branch is not master"
    exit 1
fi

git fetch
HASH=$(git rev-parse --short master)
REMOTE_HASH=$(git rev-parse --short origin/master) # GitHub側 master の最新コミットハッシュを取得
APPPATH="$(git rev-parse --show-toplevel)/src/example-application/"

if [ $HASH = $REMOTE_HASH ] ; then # ローカルとリモートの最新ハッシュが一致する場合はデプロイ実行
    appcfg.py -A example-application-production \
        --version $HASH \
        update $APPPATH \
        -E EXAMPLE_BUCKET_NAME:example-application.example \
        -E EXAMPLE_ENDPOINT:https://example.com/endpoint
else
    echo "mismatch remote master hash"
    exit 1
fi

トラフィック切り替えスクリプト

十分にテストを重ねたとしてもリリース時のトラフィック切り替えの瞬間は少し緊張するものです。
もし何かが起こったらすぐに切り戻して復旧しなければなりません。
幸いAppEngineはアプリケーションのバージョニングとトラフィック切り替えが非常に簡単に行える仕組みになっているため、万一の際にもコマンド一発で切り戻せます。
しかしその場合は当然ながら切り替え前のバージョンが何であったかを知っていなければなりませんが、毎回切り替えの度にチェックするのは非効率ですし大抵事故が起きた場合に限って確認するのを忘れていたなんてことになりがちです。
やはりここでも人手を排するアプローチが有効でしょう。

そこで切り替えの実行前に現在のトラフィック割り当て状況を表示し、切り替え後にも同様のものを表示するスクリプトを組みました。
こうすれば万一問題が発生してもどのバージョンからどのバージョンに切り替えたのかが明白であるため迅速確実な切り戻しが可能です。
またやや蛇足ですがgcloud app versions listのコマンドはバージョンのアルファベット順でソートされてしまい見辛いためsortを使って日付順となるようにしました。

サンプル

#!/bin/bash
set -eu

PROJECT_ID="example-application-production"
HASH=$(git rev-parse --short master)

# 切り替え前情報(トラフィック割り当てのあるバージョンのみ)を日付順で表示
CURRENT_TRAFFIC=$(gcloud --project $PROJECT_ID app versions list)
echo "Current traffic"
echo "$CURRENT_TRAFFIC" | head -n 1 && echo "$CURRENT_TRAFFIC" | tail -n +2 | grep -v 0.00 | sort -r -k 4
echo -en "\n"

gcloud --project $PROJECT_ID -q app services set-traffic default --splits $HASH=1
gcloud --project $PROJECT_ID app versions list | sort -r -k 4 # 切り替え後の情報を日付順で表示

実行例

$ ./scripts/serve.sh
Current traffic
SERVICE  VERSION      TRAFFIC_SPLIT  LAST_DEPLOYED              SERVING_STATUS
default  4444444      1.00           2018-03-28T19:04:44+09:00  SERVING

Setting the following traffic allocations:
 - example-application-production/default/5555555: 1.0
Any other versions on the specified services will receive zero traffic.
Setting traffic split for service [default]...done.
SERVICE  VERSION      TRAFFIC_SPLIT  LAST_DEPLOYED              SERVING_STATUS
default  5555555      1.00           2018-03-28T19:04:44+09:00  SERVING
default  4444444      0.00           2018-01-16T15:03:35+09:00  SERVING
default  3333333      0.00           2017-09-12T14:14:55+09:00  SERVING
default  2222222      0.00           2017-08-22T01:34:26+09:00  SERVING
default  1111111      0.00           2017-08-16T14:46:09+09:00  SERVING

万一切り替え後に問題が起きた場合はCurrent trafficのところに表示されているバージョンを指定して切り戻しコマンドを実行します。

$ gcloud app services set-traffic default --splits 4444444=1 --project example-application-production

最後に

検証環境へのデプロイのようなもう少し緩くて問題無い用途であればここまでする必要は無くalias等で独自コマンドを作ってしまう方が手軽です。
私は弊社エンジニアの鈴木がOSSとして開発しているjigを使ってコマンドを定義していますので、これについてはまた別の記事で紹介したいと思います。

また、これらのスクリプトはAppEngineのアプリケーションが一つであることが前提であるため、バッチ処理用サービスが別にある等といった場合についてはもう少し工夫が必要になるでしょう。