ServerlessなサービスのBlue/Greenデプロイメントの現実

Posted on
Deploy AWS

雑談

いやー春が近いですね。ウキウキのHiCustomerの小田です。最近の騒動で知ったんですが、Linuxカーネルのstableへのバグフィックスの一部はNeural networkを使って自動的に選定されてバックポートされているようです。正直まだGregの超絶な能力のみを使ってマニュアルでさばいているのかと思っていました。一般的にバグのバックポートはめちゃめちゃerror-proneな作業なため、その辺が進化してくれると今後いろんなソフトウェアが幸せになりそうです。詳細は以下にまとまっています。PatchNetの実装が見たかったんですが、ネットに情報がなさすぎて追い切れなかったです。ご存知の方は教えてください。

  • The case of the supersized shebang
    • きっかけになった騒動です。偽陽性や偽陰性を2つともゼロにはできないので、意図しないパッチが入ってしまったり、入るべきなパッチが入らなかったりする問題は常にあります。今回はそれが発生して予期せぬパッチが混入して問題になっていました。
  • Machine learning and stable kernels
    • 特徴量を何にしたのか、どういうアプローチをとったのか(記事の時点ではCNNを試しているようです)が説明されています。かなりナイーブな実装なので今後の発展に期待が持てます。また、コメントでSashal本人がデータセットのサイズや実行時のマシーンのスペックなども説明してくれています。カーネルコミッターがコメントで答えてくれるlwn.netは貴重なメディアです。

概要

本題ですが、ここ数週間つきっきりで本番環境のCloudformation化とBlue/Greenデプロイメント対応を行ってきたので、今日はその過程で得た雑多な知識をご紹介します。特に以下のAWSのサービスはBlue/Greenデプロイメントについて説明していきます。ただ、結構AWSの各サービスの基本的な使い方をわかっていないと読み進めるのが難しいかもしれません。私にもっと文才があればよかったんですが、読みにくかったらゴメンね🙏

  • API Gateway
    • インターネットからHTTPリクエストを受け付けるGateway。HiCustomerではパブリックAPIとプライベートAPIともにAPI GatewayのAPIとして実装されています。APIはSwaggerで管理され、各URLのエンドポイントにはLambdaが紐付いている。
  • DynamoDB
  • Cloudwatch Events Rules
    • HiCustomerでは、定期的に実行するバッチをCloudwatch EventsのRuleとLambdaで管理している。
  • Lambda
    • API GatewayやDynamoDBやCloudwatch eventから呼び出される関数群。HiCustomerでは100を超えるLambdaを使用している。
  • Cloudformation
    • IaC(Infrastructure as Code)を支えるAWSのサービス。AWSの各サービスのリソースの作成・更新をテンプレートで表現することができる。

背景として、弊社のプロダクトHiCustomerは1年ほど前から開発が始まり、やることとやらないことを線引し、プロダクトのMVPの検証を最優先で進めてきました。そのためアーキテクチャの一部やデプロイ周りが負債化し、バグの発生源や開発スピード向上のボトルネックになり始めていました。正式リリース以降はプロダクトの開発を止め、今まで後回しにしてきたことを消化する期間を1ヶ月ほど取らせてもらい、負債の解消を行っていました。実際には3ヶ月になっちゃったけど😰

デプロイ周りの負債を解消する上で、@hizenyと以下のようなObjectiveを立てました。今後は1日のデプロイの回数などをKPIにしていくことを検討しています。

  • 安全にデプロイできる
    • リリース前にリリースする環境の動作確認ができる
    • リリース後に以前の環境に戻せる
  • リリースした環境をビジネスの要件に沿って捨てられる
    • サービスや機能毎にデプロイをわけ、失敗したサービスや機能をすぐに捨てられるようにする
      • 不要なサービスや機能が全体の成長や進捗を阻害するような負債を防ぐことができる
      • サービスを担当するチームごとに技術選定をある程度任せられるようになり、アクティブな技術選定ができる
    • 捨てることが難しい、認証、ログ基盤、データストアなどについては共通化する

上記の崇高な目標を実現するために、以下のアプローチをとりました。以下では既にリリース済みのリソースをBlue、これからリリースするリソースをGreenとして表現します。

  • DynamoDB StreamsとLambdaのデプロイ
    • DynamoDB StreamsとLambdaの間にはEvent Source Mappingと呼ばれる中間Layerが存在します。この中間LayerがDynamoDBに変更があったときにどのLambdaを呼び出すかを決定します。リリース時はLambda Blueとは別にLambda Greenをリリースし、リリース後にこのLayerを使ってLambdaを付け替えるアプローチを取りました。
  • API GatewayのAPIのデプロイ
    • API Blueとは別にAPI Greenを別途デプロイし、デプロイ後にDomainとAPIをつなぐBase path Mappingと呼ばれる中間Layerを使ってAPI BlueとAPI Greenを付け替える方法を採用しました。
  • Cloudwatch Eventのデプロイ
    • Rule Blueとは別に、Rule Greenと関連づくLambda Greenをdisableにした状態でデプロイし、デプロイ後にenableとdisableを切り替えています。
  • Cloudformationのわけ方
    • 各サービス毎にCloudformationのスタックを分けるSOA(service-oriented architecture)と呼ばれるアプローチを取りました。さらに、各サービスは機能毎にスタックをわける(multi-layered architecture)を採用しました。
    • Blue/Greenを達成するのに必要な各サービスの中間Layer(Event Source MappingやBase path Mapping)はCloudformationの対象から外しました。
    • データストア(今回の場合はDynamoDBですが)について、サービス毎のスタック管理の対象から外しました。
    • 基本的に以下のドキュメントに沿って、各サービス・機能のライフサイクルを丁寧に考えていきました。基本が大事ですね。

できなかったことは以下になります。ECRの導入はすぐにできるんですが、手が回らなかったです。

  • API GatewayとLambdaのCanary release対応
    • Cloudformationとの相性が特別に悪いため、対応を見送りました。CloudformationはAWSの各リソースを管理するIaCのツールですが、更新時の挙動が以下の3つしかないため、Blue/Greenで必要な同じ環境をもう一つ作るような処理がコードとして表現しにくいです。
      • Update with No Interruption
      • Update with Some Interruption
      • Relacement
  • ビルドの成果物のECRでの管理
    • 完全に本番と同じにするためにはアプリのバイナリをECRで管理したいんですが、現状では本番環境と同じ環境をデプロイする場合は、毎回コンテナ内ですべてのバイナリをbuildしています。
  • 認証のCognito化
    • この辺も時間がなくてまだできていない部分です。

以下では個別の処理について、特にキレイなところではなく汚いところ積極的にお見せしようと思います。

DynamoDB StreamsとLambdaのBlue/Green

DynamoDB Streamsに対応するLambdaのデプロイは、 DynamoDB StreamとLambdaの間の中間LayerであるEvents Source Mappingの設定を更新することで行います。Events Source Mappingに設定したLambdaは、shard(streamのeventをグループ化したもの)毎に毎秒4回の頻度でeventのpollingを行います。その際に関連するEvents Source Mappingは、どこまでpollingしたかeventのシーケンス番号を保持しています。この機能を使用するとeventと取りこぼしや二重処理を防ぎながら安全にBlueのLambdaからGreenのLambdaに切り替えることができます。

この辺の説明はWEB上の公式のドキュメントやサポートの方の説明より、manが一番詳しかったです。以下はaws lambda update-event-source-mapping helpからの抜粋です。

You can update an event source mapping. This is useful if you want to change the parameters of the existing mapping without losing your posi- tion in the stream. You can change which function will receive the stream records, but to change the stream itself, you must create a new mapping.

If you disable the event source mapping, AWS Lambda stops polling. If you enable again, it will resume polling from the time it had stopped polling, so you don’t lose processing of any records. However, if you delete event source mapping and create it again, it will reset.

上記を踏まえた上で、DynamoDB StreamとLambdaのBlue/Greenデプロイは以下の手順で行います。

  • Lambda GreenをCloudformationでデプロイする。
    • Lambda Greenの挙動を検証
  • 該当のEvent Source Mappingを無効にする。
    • Event Source MappingはCloudformationで管理せずに、手動で更新する。
  • 無効にしたEvent Source MappingにLambda Greenをつける。
  • 該当のEvent Source Mappingを有効にする。

上記の切り替えは以下のスクリプトで行っています。Events Source Mappingは変更が反映されるまでにタイムラグがあるので気をつけてください。かならず状態が更新されるのを確認してから次の処理を行うのがベストだと思います。

#!/bin/bash

[[ -n $1 ]] || { echo "USAGE: $0 UUID FunctionArn"; exit 1; }
[[ -n $2 ]] || { echo "USAGE: $0 UUID FunctionArn"; exit 1; }

set -xe

UUID="$1"
FN="$2"

wait_until() {
  local count=0
  while [[ $(aws lambda get-event-source-mapping --uuid $UUID --query 'State' --output text) != $1 ]]; do
    [[ $count -ge 10 ]] && { echo "ERROR: reach maximum attempts"; exit 1; }
    count=$((count+1))
    sleep 2
  done
}

aws lambda update-event-source-mapping --uuid "$UUID" --no-enabled
wait_until "Disabled"
aws lambda update-event-source-mapping --uuid "$UUID" --function-name "$FN"
wait_until "Disabled"
aws lambda update-event-source-mapping --uuid "$UUID" --enabled
wait_until "Enabled"

API GatewayのAPIのBlue/Green

API GatewayへのAPIのデプロイは、本番用のドメインとAPIの間のBase path Mappingを更新することで行います。Base path Mappingはドメインと特定のパスにどのAPIを関連付けるかを管理します。サポートの方の説明によると、特定のパスに紐付けるAPIを変更すると即時に切り替わるらしいです。今回はこの機能を使用しました。

ちなみに公式に詳細なドキュメントがないため、公開できる範囲でもう少し技術的な詳細を教えてもらえないかサポートの方に確認したんですが、詳細は教えられないとのことでした。切り替えに伴うタイムラグなど細かい点はAWSがいい具合にやってくれるのを信じる以外にないです。Configure Base Path Mapping of an API with a Custom Domain Name as its Host Name

上記を踏まえた上で、APIのデプロイは以下の手順で行います。

  • API Blueとは別にAPI GreenをCloudformationで別途デプロイ
    • API Greenの動作をAWSから割り当てられたデフォルトのドメインを使用し検証
  • 本番のドメインとAPI Blueが紐づけているBase path Mappingを更新し、本番のドメインとAPI Greenを紐付ける
    • Base path MappingはCloudformationで管理せずに、手動で更新する。

Base path Mappingの切り替えは以下のスクリプトで行っています。

#!/bin/bash

[[ -n $1 ]] || { echo "USAGE: $0 restApiId api-example.hicustomer.jp"; exit 1; }
[[ -n $2 ]] || { echo "USAGE: $0 restApiId api-example.hicustomer.jp"; exit 1; }

set -xe

ID="$1"
DN="$2"
BP=v0

aws apigateway get-base-path-mapping --domain-name "$DN" --base-path "$BP"
aws apigateway update-base-path-mapping \
  --domain-name "$DN" \
  --base-path "$BP" \
  --patch-operations op='replace',path='/restapiId',value="$ID"

Cloudwatch EventsのRuleのBlue/Green

Cloudwatch Eventsは新しくデプロイするRule Greenを無効にしてデプロイすることでBlue/Greenを達成しています。以下の流れになります。

  • CloudformationでRule Greenをデプロイする。
    • Rule Greenの挙動を確認する。
  • Rule Blueをdisableにし、Rule Blueに対応するRule Greenをenableにする。
    • エッジケースでの二重起動を許せるなら、GreenをenableにしてからBlueをdisableにしたほうがいい場合もあります。

以上の切り替えの部分を行うスクリプトは以下になります。

#!/bin/bash

[[ -n $1 ]] || { echo "USAGE: $0 blue green"; exit 1; }
[[ -n $2 ]] || { echo "USAGE: $0 blue green"; exit 1; }

set -x

BLUE="$1"
GREEN="$2"

blue=($(
  aws events list-rules \
    --query "Rules[?contains(Name, \`${BLUE}-lambda-batch\`) == \`true\`][ Name ][]" \
    --output text
))
green=($(
  aws events list-rules \
    --query "Rules[?contains(Name, \`${GREEN}-lambda-batch\`) == \`true\`][ Name ][]" \
    --output text
))

for i in ${blue[*]}; do aws events disable-rule --name $i; done
for i in ${green[*]}; do aws events enable-rule --name $i; done

Blue/Greenにおけるデータの扱い

インフラがBlue/Greenになったからといって、そこに流れるデータはどちらの環境で処理しても正常に扱えるとは限りません。例えば、ナイーブなケースでGreenのデプロイ時にデータストアにカラムAを追加したとします。その後問題があってBlueにrollbackすると新しいカラムAはBlueのコードで処理できないため障害が発生します。Blue/Greenにおけるデータの扱いで面倒なのは、Blueがrollbackするケースを考慮するとBlueもGreenも最新のデータ(Greenで登録・更新したデータ)を扱えなければならない点です。この辺をやりきるためには、コードレビューを徹底するか、コードの静的解析などをCIでシステマチックにチェックする以外ない気がします。弊社もどこまでできるかまだ検討段階です。

ちなみに、Blue/Greenを提唱したMartin Fowlerの記事では、データの扱いについて以下のような記載になっています。

Databases can often be a challenge with this technique, particularly when you need to change the schema to support a new version of the software. The trick is to separate the deployment of schema changes from application upgrades. So first apply a database refactoring to change the schema to support both the new and old version of the application, deploy that, check everything is working fine so you have a rollback point, then deploy the new version of the application. (And when the upgrade has bedded down remove the database support for the old version.)

また、AWSが出しているWhitepaperによると以下のような記載になっています。ちなみにこのWhitepaperではServerlessではなく、EC2などを使った従来のケースでのBlue/Greenの方法について説明していますが、個人的な経験をいうとElastic Beanstalkがめちゃめちゃ楽です。はじめからボタン一つ押すだけでBlue/Greenが実現できます。スタートアップは初めElastic Beanstalkで全部構築して、でかくなったら考えるがベストではないかと思っています。

Both the blue and green environments need up-to-date data:

  • The green environment needs up-to-date data access because it’s becoming the new production environment.
  • The blue environment needs up-to-date data in the event of a rollback, when production is then either shifted back or kept on the blue environment.

弊社はRDBはほぼ使っていませんが、RDBの場合はもっと面倒です。まずスキーマの更新とコードの更新のデプロイの粒度をわける必要があります。これをやらないとまた場合によってかなり深刻の障害を引き起こす可能性があります。複数のインスタンス(Blue01とBlue02)をデプロイする環境で、Blue01でアプリをロードしスキーマ定義を読み込んだ後で、Blue02がスキーマ定義を更新すると、Blue01がその後の処理で落ちる可能性(キャッシュしているスキーマ定義でBlue02が更新したスキーマ定義が異なるから)もあります。ちなみにWhitepaperでは以下のような記載になっています。

  • The schema is changed first, before the blue/green code deployment. Database updates must be backward compatible, so the old version of the application can still interact with the data.
  • The schema is changed last, after the blue/green code deployment. Code changes in the new version of the application must be backward compatible with the old schema.

カラムを追加するような処理では最初のアプローチをとり、カラムを削除するような処理では最後のアプローチをとる必要があります。うーんめんどくさい。デプロイの制約ががっつりアプリ開発に入ってくるので、アプリケーションプログラマの立場としてはまじ面倒です。

最後に

上記以外はCloudformationでかなりキレイに管理されているのですが、やはり現実はめんどなもので、Blue/Greenを本気でやろうと思うと今のCloudformationの作りではmappingポイントを管理外にせざるおえない気がします。このへんはAWSのSAの方とも相談を続けて行きたいです。あと、デプロイの刷新によってKPIにどの程度変化があったかも今後公開していきたいと思います。

明後日(36)の以下のイベントで弊社の@hizenyが登壇します。弊社のインフラのBlue/Green対応についてもっと突っ込んで聞きたい方はイベントで捕まえてみてください。ちょっとでも弊社に興味をもった方も@hizenyに絡んでみてください。弊社の一番のナイスガイなので楽しくお話ができると思います😎

最後に、かなりキレイになったフロントエンドの環境でVue.jsとTypeScriptでアプリを書きたいエンジニアを募集しています。気になった方はぜひお話だけでも応募お願いします🙏