Heroku で動いている Rails アプリを ECS Fargate に移行する

この記事は **SmartHR Advent Calendar 2019** 7日目の記事です。

最近 Heroku から ECS Fargate に移行するプロジェクトを担当しました。そちらが一段落したので、移行する上で検討したこと、どのように実現したかを共有します。

---
SmartHR では、いくつかの機能ごとにチームが分かれており、リポジトリもインフラもチームごとに分けて開発が進められています。私のチームで開発しているアプリケーションはもともと Heroku の上で動いていたのですが、最近 ECS Fargate に移行するプロジェクトを完了しました。

この会社に来るまで、 Heroku を使ったことは無かったのですが、Heroku の印象として、インフラ周りの運用は全てお任せして開発に集中できるという観点では最高だなというところです。
ただ、 Heroku のレールから外れたことをしようとすると、多くをマネージドサービスでカバーしてくれているがゆえ細かいところまでは手が届かず運用していくのが難しくなってしまうことがあります。

Heroku では add-on 含めるとざっとあげるだけでもこれだけのことを管理してくれています。

- ネットワーク
- ロードバランシング
- ファイアウォール
- メトリクス監視
- アラート
- ログ
- CI
- コンテナイメージのビルド
- コンテナイメージの管理
- デプロイ
- ロールバック
- メンテナンスモード
- デバッグコンソール
- DNS
- SSL

この度、Heroku では実現が難しい要件が出てきたこともあり、脱 Heroku プロジェクトがスタートしたのですが、上記の機能をどう置き換えていくかを検討していく必要がありました。

#### 目指した姿
移行していく上で、目指したのが

**メンバーの開発効率を下げないためにも現状の Heroku での運用フローにできるだけ近づけたい**
弊社ではインフラ専門のチームというのは存在せず、アプリケーションを動かすために足りないものはすべて開発メンバーでなんとかするスタイルです。そのため、なるべく運用に労力を割かず開発に集中できるようにしたいと思っていました。Heroku での運用の感覚に近くなるようにしておくことで、メンバーの運用へのオンボーディングをスムーズに進めたい思いがありました。

**なるべくマネージドなサービスを使用する**
こちらも同様で、なるべくサーバー管理などに運用コストをかけたくないという思いからです。ECS on EC2 ではなく Fargate を選定したのはこの理由が大きいです。

#### 移行前の構成について
Heroku では以下のような組み合わせで Rails アプリケーションを動かしていました。

- web dyno: `puma`
- worker dyno: `sidekiq`

## コンテナイメージの創造
まず手掛けたのがコンテナイメージを作ってローカルでアプリケーションを動かすというところです。
Heroku には Heroku buildpack というものがあり、サーバーに必要なパッケージの buildpack を組み合わせるといい感じでコンテナビルド時にパッケージを注入してくれます。
buildpack を使っているところは apt-get で入れて上げるなど、地道に Dockerfile を書いていくしかないです。
Dockerfile のベースイメージとして、Heroku Stack で使用している OS のバージョンと同じものを使うようにすることに注意です。OSバージョンが変わってしまうと思わぬバグが生まれることがあります。
`Heroku-18 Stack` であれば `Ubuntu 18.04` をベースイメージとして選びましょう
https://devcenter.heroku.com/articles/stack

Heroku での過去の build log を見てみると、どのような手順でビルドしていっているかが丸わかりなのでそのようにビルドが実行されるように Dockerfile を書いていけばOKです。

## Heroku CI からの脱却
Heroku から移行するのであればもはや Heroku CI を使う必要は無いと思うのでこれも移行です。
今回は、弊社の他のプロジェクトでも使用していた CircleCI を使うのがノウハウもたまっていてよさそうということで CircleCI を使うことにしました。
検討時、CircleCI でテストからビルド、ECS へのデプロイまでをやってしまおうかと思っていたのですが、ECS のデプロイ方法を調べていくと、CodeDeploy、CodePipeline との親和性が高いということを知ったので、CircleCI ではテスト、ビルドまで、デプロイは CodePipeline、CodeDeploy と、それぞれの強いところをいいとこ取りする形にしました。

## データベース移行
もともとアプリケーションは Heroku private space の中で動いており、VPC Peering で AWS 環境と繋がっている状態だったため、まずは RDS にデータを移行し、 Heroku で動いているアプリケーションから RDS に繋ぎに行くようにしました
その後アプリケーションを AWS に移行するというステップを踏みました。

## デプロイ周り
デプロイには先にも述べたとおり、ECS のデプロイとの親和性が高いという理由から、 CodePipeline、CodeBuild、CodeDeploy を使用することにしました。それぞれの役割は以下のとおりです。

- CodePipeline
    - デプロイワークフロー全体を管理
    - ステージング環境には、本番環境へのデプロイを承認するためのステージも作成
- CodeBuild
    - Database migration するための ECS タスクを起動するジョブ
    - ECS worker タスクを起動するジョブ
- CodeDeploy
    - ECS web タスクを起動

## ECS のデプロイツール
docker-compose と同じ書きっぷりでタスク定義が表現できる ecs-cli を採用しようとしていましたが、使ってみたところ、docker-compose と同じように表現できるのは一部で、大部分は ecs-cli 独自の書き方となっていて、あまりメリットがなさそうなのと、 ecs-cli 自体が v2 のリリースが予定されており、まだ安定してなさそうなので見送りました。
他にもよさそうなツールは見つからなかったので、一旦、生のタスク定義で管理するようにしています。CodeDeploy を使ってデプロイするのであれば生のタスク定義が必要というのもあります。今後よさげなデプロイツールが出てきたときにツールを使うことも検討します。

## メンテナンスモード
Heroku には、アプリケーションをメンテナンス状態にしてアクセスさせたくない場合などにメンテナンス用の静的ページを表示させておくメンテナンスモードというものがあり、ワンコマンドで切り替えることができます。
メンテナンスモードは [ALB のルール機能](https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/listener-update-rules.html) を使って実現することにしました。ALB では複数ターゲットをルールとして指定することができ、優先度の高いものにルーティングするようになっています。メンテナンス時用の HTML を返すターゲットを登録しておき、平時はアプリケーションへのルーティングの優先度を上げておき、メンテナンス時はメンテナンス用 HTML の優先度を上げることで、10秒くらいでルーティングが切り替わるようにしています。
ターゲットに登録できる HTML は1024文字が制限となっているので注意です。

## 監視
コンテナメトリクスや、リクエストステータスなどのメトリクスは CloudWatch を使って監視しています。ECS の Container Insight を使うと起動しているコンテナ台数や、コンテナ単位でのリソース使用量をモニタリングできるので便利。この辺はあまり不自由なく実現することができています。むしろ Heroku よりも多岐にわたる監視ができるのでいい感じです。
アプリケーションメトリクスは Heroku add on として使用していた Scout を引き続き使用しています。

## ヘルスチェック
ALB からのヘルスチェックにレスポンスできるエンドポイントを生やすために [health_check](https://github.com/ianheggie/health_check) という gem を入れました。そして ALB からのヘルスチェックのアクセスはログを出力したくないので、特定のエンドポイントへのアクセスログを無視するように設定することができるログフォーマッタの [lograge](https://github.com/roidrage/lograge) という gem も一緒に入れました。

## ログ
Heroku では add on の Papertrail を使っていました。
移行後は CloudWatch Logs を使えばいいかなーと考えていたのですが、使ってみると検索などの操作感が Papertrail に比べて劣るので操作に慣れている Papertrail 使いたいなとなり、Papertrail を使うことにしました。
また、CloudWatch Logs を使う場合、ログローテートで定期的に S3 にアーカイブを保存するなどの処理を自前で実装する必要があるのですが、Papertrail を使っていれば定期的に S3 にアーカイブを保存してくれるのでそこも決め手の一つです。
Papertrail にログを送るために、アプリケーションのコンテナに fluentd コンテナをサイドカーとしてくっつけてデプロイしています。ECS の **firelens** を使うことでアプリケーションの標準出力に吐かれたログが fluentd コンテナで動いている fluentd の source として読み込まれ、`fluent.conf` に定義した処理を実行させることができます。
fluentd から Papertrail にログを送るための [fluent-plugin-papertrail](https://github.com/solarwinds/fluent-plugin-papertrail) をインストールする必要があったため fluentd コンテナイメージは自前でビルドしたものを使っています。また、Papertrail にログを送信する際に、ログに不正な文字コードが含まれていると送信エラーになってログが送信できなくなることが発生したため、不正な文字コードを安全な文字に置換するための [fluent-plugin-string-scrub](https://github.com/kataring/fluent-plugin-string-scrub) もイメージに含めました。

Papertrail にログを送る別の方法として、[remote_syslog2](https://github.com/papertrail/remote_syslog2)  などの gem を使い、アプリケーションの処理とログの送信を同期的に送信することもできそうでしたが、Papertrail 自身が落ちていたり Papertrail サーバーの名前解決がうまく行かなくなるとログが送れなくなってこちらのアプリケーションも落ちる、みたいなことになるのでさすがにまずいので見送りました。

## デバッグコンソール
ここが今回の移行を通して最大の関門でした。
理想としてはサーバーに入ったりコマンドを叩くことなく運用したいですが、現実的には、不整合が起きたデータを修正したり、ワンタイムスクリプトを流して結果を確認する、みたいな作業が発生することがあるのでコンソールを用意しました。

最終的には AWS Systems Manager の Session Manager 機能を使ってデバッグコンソールの機能を実現しました。
参考： [[AWS ECS]Fargateのcontainerにシェルで入りたい(sshd無しで！)](https://qiita.com/pocari/items/3f3d77c80893f9f1e132)
あらかじめ Session Manager で接続するための agent を含むコンテナイメージをビルドしておき、ローカルでアクティベーションID の発行をし、それをもとにタスクを起動するシェルスクリプトを実行します。タスクの中でそのアクティベーションを使って agent を起動し、インタラクティブコンソールに接続できる、といった形です。
ここはもっといい感じにしていきたい部分のひとつです。少なくとも自前のシェルスクリプトは書かなくていいようにしたい。

## ChatOps
Heroku では slack コマンドでステージングで動いているアプリケーションを本番環境にデプロイすることができるという Bot があります。これは非常に便利だったので、AWS でも同じような操作感でデプロイしたいなと考えておりました。
しかし最終的にここは独自の bot アプリを Lambda で実装しました。
[ykarakita/code-pipeline-slack-approver](https://github.com/ykarakita/code-pipeline-slack-approver
)
CodePipeline の動きとしては、でステージングへのデプロイが完了したら、CodePipeline の Approve アクションに移り、承認されると、本番環境にデプロイされるようになっています。ここの承認アクションを slack から実行できるようにしています。AWS 公式で同様の機能がリリースされることに期待！


## まとめ
ここまで見てきたように Heroku から Fargate に移行するには検討すべきことが多々ありました。Heroku でやっていけるプロダクトなのであれば Heroku の肩に乗っかるのが賢明かなという印象です。
しかし AWS に移行したことによって、柔軟なリソースの増減やインフラレベルでの細かいメトリクスの取得など、細かいところまでいじれるようになったことのほか、どんどんリリースされる AWS のサービスについても取り入れていくということができるようになりました。Lift and Shift の精神で、今回はまず AWS に移行したので、今後可能な部分はよりクラウドのマネージドな部分に乗っかることができれば、より運用の負担は減っていくのではないかと思っています。