Docker with ECS × Railsを実現させるために考えたこと(導入編)

こんにちは。エンジニアの志村です。
今回から私は「Docker on Rails with ECSを実現させるために考えたこと」と題して、実際にDockerをProductionで運用する際にハマったポイント、また考慮すべき点に関して、数記事に渡って執筆していこうかなと考えております。 弊社では開発環境はDocker + compose、その他はItamae × EC2の構成でしたが、現在ドッグフーディング・本番環境をDocker with ECSに移行しております。

ProductionをDockerで運用しようと思った背景

今回インフラ環境を見直した背景として、

  • プロビジョニングツールの管理つらい
    →緊急で直接サーバ内で作業をした際にプロビジョニングツールとサーバ側の差異が発生。
  • OSにインストールしているライブラリのアップデートが完全手動になっている。
    →自動化したい
  • Productionとドッグフーディング環境の差異
    ドッグフーディング環境で新規ミドルウェアや設定の実験を繰り返していたため

といった理由が挙げられます。
プロビジョニングツール、本番環境とドッグフーディング環境の差異に関してはルールを決めて厳密に管理、またライブラリのアップデートも定期的に新規インスタンス自動起動するスクリプトを作成すれば問題ないとは思います。
しかし、規模が大きくなったタイミングで導入するのはかなりのコストがかかる、またなるべく上記のようなことを考えずに実装に集中したいという思いから今回のインフラ刷新に踏み切りました。

ProductionをDockerで運用する際に考慮した点

開発環境は既にDockerizeしておりましたが、実際にProductionをDockerにて運用する為には様々な障壁にぶつかりました。

デプロイ

Blue-Green Deployment

恐らく、ECSを使用する際に最も悩むのがデプロイの方式をどのようにするかだと思います。
今まででの環境であればCapistranoを叩いてgitをpullして〜のようにデプロイ出来ましたが、コンテナの場合そのようなデプロイ方法は難しいです。
ECSのデプロイ方法として真っ先に挙げられるのがBlue-Green Deploymentです。
BG Deploymentについては割愛しますが、

  1. ロールバックが容易
  2. デプロイ時のダウンタイムを極力抑えることが出来る

のような特徴があげられます。 ロールバックの容易さ、ダウンタイム極小化の観点から弊社でもBG Deployment方式にしました。

BG Deploymentは良いことずくめに見えますが、デプロイの戦略を立てる上では中々苦労した部分です。
ECSを使用した際のBG Deploymentには以下の3種類が考えられるかと思います。

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

結論から言うと弊社は3を選択しました。 選んだ理由やそれぞれのメリット・デメリットに関しては別の記事で書きたいと思います。

スクリプト

今までの環境であれば、Capistranoという素晴らしいツールが存在するのでそこまでデプロイ環境で悩むことも無かったですが、DockerになるとCapistrano単体でのデプロイ環境の構築は中々難しいと感じました。
デプロイの手順としては下記の通りになります。

  1. Task definitionの更新
  2. ECSのServiceをupdate
  3. TaskがRUNNINGになっているか確認
  4. インスタンスの状況を見て、ALBのTarget groupの優先順位を切り替える

現在では様々なECS向けのデプロイツールが出ておりますが、上述している通りALBのTarget groupの優先順位を切り替えるという作業が発生する為、社内専用のデプロイツールを作成する方針にしました。
この辺りも詳しくは別の記事で書いていきたいと思います。

CI

現在、CircleCIをはじめとして様々なCIサービスが出ていますね。
弊社でもCircleCIを使用し、Rubocop -> scss-lint -> RSpec -> capistranoという流れでデプロイを行っておりました。
今回の移行にあたって、Jenkinsへシフトさせました。
Jenkins移行の理由として、カスタマイズ性が一番の理由です。

弊社では、ドッグフーディング環境においてはテスト有りデプロイ、テスト無しデプロイの2種類が存在します。
cap dogfooding deploy 〜のように手動デプロイをし、テストを通す前からディレクターに動作を確認して貰う手順をとっております。
勿論テストを通して完璧な状態で見てもらうのが一番ですが、確認を早め早めにすることによって、実装の最終段階でディレクターの意図と違う!というような不一致を極力減らすことが出来ます。
テストを通してからデプロイだとその回数が減ってしまい、リリーススピードが結果的に落ちてしまうのでテスト無しデプロイを可能にする環境を構築したいという背景がありました。
CircleCIでこのような環境を作成するのは結構な手間だったため、今回はJenkinsのパラメータ付きビルドで処理することにしました。

マイグレーション

RailsではDBスキーマの変更、データのクレンジングにActiveRecord::Migrationを使用します。
一定規模になると ActiveRecord::Migrationを使わない例が多いですが、弊社ではロールバックが容易なのとRubyで記述出来るという点で未だに使用しております。
以前の環境であればcapistranoがよしなにやってくれたのですが、今回の環境の場合、稼働用のTask definitionで実行するのは難しいと判断しました。 そこでbatch, migration専用のインスタンスクラスタを作成し、デプロイスクリプトを走らせた際にまずmigrationを当該クラスタで実行させるようにしました。
こちらはデプロイスクリプトにバッチ専用のコマンドを実装しました。

バッチ

上記のマイグレーションに使用しているインスタンスクラスタと併用しております。
cronによるバッチ処理+sidekiqをこのインスタンスで管理しています。

環境変数の扱い

環境変数はEC2 Systems Manager・EC2のインスタンスメタデータ・S3と様々な方法があるかと思います。
今回は管理の手軽さ、実装の容易さというメリットから、S3を選択しました。
具体的にはビルド前にS3からenvファイルをダウンロード→ビルド→デプロイ
という流れになります。

また環境によってnginxのconfやfluentdのconfの中身を変えたいという場合がありますが、
これに関してはenvsubstコマンドを使用し、ビルド時にconfの中身を動的に変えるようにしています。

assets周り

弊社ではwebpackを使用し、assetsをビルドしております。
webpackにてビルド→asset_syncを使用し、S3にアップロード→CloudfrontのInvalidationを走らせる
というフローでデプロイを行っておりました。
デプロイフローが複雑になる + Herokuも推奨していないという点から今回はこのasset_syncを使用するのをやめ、Webサーバから直接配信→サイト前段にCloudfrontを張ってキャッシュをさせるようにしました。

devcenter.heroku.com

ログ周り

fluentdコンテナを立て、Kinesis, BigQueryにログを流すように変更しました。
厳密なログの記録が必要ないサービスに関しては、Cloudwatchにそのまま流して記録しております。

監視

zabbix-agentコンテナを立て、そこからzabbixにデータを流すようにしています。
勿論zabbix, grafana自体もDockerizeしております。

https://hub.docker.com/r/monitoringartist/dockbix-agent-xxl-limited/

非常に使い勝手が良いので、上記のイメージを使用しております。
インスタンス起動時に勝手に登録してくれてとても使い勝手が良いのですが、インスタンス消えた時にホスト情報をどう削除するか未だに悩んでおります…

以上になります。
次回からは項目ごとに具体的な事例と共に執筆していきたいと考えています。