Docker with ECS × Railsを実現させるために考えたこと(デプロイ編)

こんにちは。エンジニアの志村です。

cluex-developers.hateblo.jp

の続きとなります。 今回はタイトル通り、デプロイ辺りを執筆していければと思います。

Blue-Green Deployment

Blue-Green Deploymentはマーチン・ファウラー氏が提唱したデプロイ方式です。 blue, greenとほぼ同じ本番環境を2系統用意してデプロイを行います。このデプロイ方式ですが、

  1. ダウンタイムの極小化
  2. ロールバックが容易

という大きな2つの利点があります。

Dockerizeへのモチベーションとして前回の記事にも書いたのですが、徐々にドッグフーディング環境・本番環境で差異が出てきました。
そのような状況だと、ドッグフーディングでは正しく動作したのに本番環境では意図した挙動とは違うという場面も出てきます。
上記のようなトラブルがあった際に今までのIn place型のデプロイ方式だとロールバックに時間がかかり、ユーザーに迷惑をかけてしまう時間が長くなるという問題がありました。
Blue−Green Deploymentでは特に2.の「ロールバックが容易」という点が非常に魅力でした。
何かトラブルがあったとしてもELB設定を変えてしまえば瞬時に旧環境に戻せるという点で、デプロイに対する心理的ハードルが社内全体で下がりました。

また、ECSの仕組みにより今までのようにCapistranoによるSSHを用いたデプロイが難しかったという理由も相俟って、弊社でもBlue−Green Deploymentを用いております。

ECSによるBlue-Green Deployment

前回の記事にも書いた通り、ECSを使用した際のBlue−Green Deploymentにはいくつかやり方が存在します。

  1. ECSのMaximum healthy percent, Minimum healthy percentを調整し、徐々にタスクが切り替わるように調整する。
  2. CLB(Classic Load Balancer)× Autoscaling Groupを使用して切り替える
  3. Routes53を使用し、Standby, ActiveのELBを切り替える
  4. ALBに紐付いているTarget groupの優先順位を切り替える

ECSのMaximum healthy percent, Minimum healthy percentを調整し、徐々にタスクが切り替わるように調整する。

利点

  • ECSデフォルトの機能を用いるので、他に余計な操作をする必要が一切ない
  • タスクの増減を元にデプロイを行うのでインスタンス料金を低く抑えることが出来る

欠点

ECSの特徴が最大限発揮されたデプロイ方式だと考えます。
ECSには「タスク」という概念があります。タスクはDocker imageを起動させる際の設定、またそのリビジョンです。
タスクは

  1. 使用するdocker image
  2. コンテナ間のリンク
  3. コンテナのコマンド
  4. データボリューム

等が設定できます。 リビジョンが用意されたdocker-composeだと考えると分かりやすいかと思います。
タスクに関しては公式のドキュメントが分かりやすいです。
Amazon ECS タスク定義

そのタスクを管理するのがサービスという概念です。
サービスはざっくり言うと、ロードバランサーとの連携、上記のタスクをインスタンスにどう配置するか、最大・最小・維持タスク数をいくつにするかの取り決めです。

ECSのサービスのデプロイメントオプションに、maximum helathy percent(最大率), minimum healthy percent(最小ヘルス率)という項目があります。
この値を調整することによって、Blue-Green Deploymentを実現することが出来ます。

例えば
* maximum healthy percent: 200%
* minimum healthy percent: 100%
と設定すれば、
デプロイ時に通常のタスク数の2倍、デプロイ終了後にタスク数1倍という形になります。

仮に1インスタンス1タスクという戦略であれば、デプロイ時に2倍のインスタンス数が必要ということになります。
* maximum healthy percent: 50%
* minimum healthy percent: 100%

個人的な感触ですが、既存のタスクをkillして新しいタスクに入れ替えるのに結構な時間がかかっている印象でした。
つまりは、ロールバック時にも同じ作業をしなければならない≒ロールバックに時間がかかると予想される為、今回は見送りました。

CLB(Classic Load Balancer)× Autoscaling Groupを使用して切り替える

利点

  • 設定がほとんど必要ない。実装が楽
  • ロールバックが早い
  • ダウンタイム無し

欠点

  • SpotFleetが使用できない
  • Blue, Greenが重なるタイミングがある

恐らく現時点で最もポピュラーなBG Deploymentの方式かと思います。 実装もAutoscaling Groupを作成すればOKという簡便さです。

ALBを今回使用したいと考えていましたが、コンソールを見てもALBが使用できない雰囲気ではありました。 しかしながら、現在では下記のように使用できるようです。

dev.classmethod.jp

これで課題は1つクリアしましたが、SpotFleetがAutoscaling Groupに登録出来ない為、今回この方式は断念しました。
ロールバックを容易かつスピーディにするにはインスタンスの数が単純に倍必要となります。SpotFleetの使用は予算の都合上、必要でした。 またこの方式だとヘルスチェックの観点から、Blue, Greenが重なるタイミングがどうしても出てきます。
マイグレーション・モデルの変更をした際に、旧稼働系・新稼働系が混ざるとエラーを引き起こし兼ねません。この観点からも今回は見送りました。

Route53を使用し、Standby, ActiveのELBを切り替える

利点

  • ダウンタイムほぼ無し
  • シンプル

欠点

一番古典的かつ確実方式です。 Route53のWeightを利用し、Blue→GreenのELBに徐々に切り替える方式です。
実装がシンプルではありますが、一番の問題点はDNSキャッシュです。
キャッシュサーバによってはTTLを無視して一定期間キャッシュするものもあるらしいので、実際にデプロイが完全に完了したかを確認するのが難しくなります。
上記のようなキャッシュのコントロールの難しさから、今回この方式は見送りました。

ALBに紐付いているTarget groupの優先順位を切り替える

利点

  • ダウンタイムほぼ無し
  • ロールバックが早い
  • メンテナンスにすぐ切り替えられる

欠点

  • 実装が面倒

結論から言うとこの方式を採用しました。
いくらか利点はありますが、特にロールバックが容易な点が大きいかと思います。
ALBにはパスベースルーティング、ホストベースルーティングという機能があります。
パスベースルーティングは下記にある通り、パスによってどのターゲットグループにトラフィックを流すかを決定できます。

tartget.png

例えば、

IF: /target1/*, THEN: target1
IF: /target2/*, THEN: target2

のように設定すると、 host/target1/の際にはtarget1に紐付いたインスタンスに、
host/target2/の際にはtarget2に紐付いたインスタンストラフィックを分岐することが出来ます。
また、このルーティングでは優先順位というものがあります。
同じパスであっても優先順位が高い(数字が小さい)方にトラフィックが流すということが可能になります。この特徴を活かし、

優先順位 パス ターゲットグループ
1 * target-blue
2 * target-green
last Requests otherwise not routed target-maintenance

上記のように設定しました。
こうすると通常、blueにトラフィックが流れます。
デプロイ時にこの優先順位を切り替えると、green系にトラフィックが変わります。
ロールバック時も同じように優先順位を変更するだけで、一瞬で切り替わります。
メンテナンスページを出したい場合、他のルーティングルールを削除すればOKです。

上述の通り、ALBの特徴を使用すれば一瞬でロールバックが可能になり、さらにメンテナンスページまで容易に出すことが出来ます。

欠点としてはこの部分の実装が面倒だという点です。
今回、harmonikというデプロイツールを用意しました。

harmonik

事情によりGithub等に公開はしておりません…(早くしたい)
harmonikは今回のBG Deploymentを使用するにあたって新しく作成したデプロイツール(CLI)です。
主に下記のような機能があります

  1. デプロイ
  2. 単発のタスク起動
  3. ロールバック
  4. ステータス表示

1.は他のECS向けデプロイツールとほぼ同じです。

ALBの優先順位からデプロイ先を決定
↓
task definitionを更新
↓
serviceを更新
↓
taskがRUNNING状態かどうか判別
↓
target groupのヘルスチェックがhealthyかどうか判別
↓
ALBの優先順位切り替え

上記のようにしてBG Deploymentを実現しております。

2.はマイグレーション等の単発のタスクに使用します。
CIでは、単発のタスク→Blue or Greenにデプロイ
という流れをとっております。

3.は単純で、ALBの優先順位を変更してロールバックさせているだけです

4.は稼働系のステータス(クラスタ名・インスタンス数・デプロイ履歴等)を表示させるものです。

以上になります。
この実装により、Capistranoとほぼ同じような感覚で簡単にデプロイをすることが可能になりました。
Dockerの運用に切り替えてからサーバのメンテナンス等に時間を割かなくて済むようになったので非常に運用が楽になりました。

※まだ運用から日が浅く、間違えている箇所があるかもしれません。
その際はお手数ですがコメント頂けますと幸いです。