RailsアプリをECSにのせてecs-cliでデプロイとかをする
RailsのECS移行事例なんて既に山ほどあるので、特に書くつもりは無かったのですが、実際にやってみると
- 時代が進んで、より便利なものが出てきている
- デプロイどうするのよ、となったときに各自が最強のECSデプロイツールを作っていて、参考にならない
といった体験をしたので、最近やったECS移行の話を書くことにしました。
社内Qiitaに書いたポエムです pic.twitter.com/yDlWGhhkF1
— wata (@wata727_) 2017年8月31日
もちろん、この記事も古くなると何の役にも立たないと思うので、古くなったら、みなさん頑張って調べてください。
ECS移行で考えるべきこと
まず前提として、移行対象はシンプルなRailsアプリで、WebサーバとWorkerからなります。デプロイはCapistranoなどのいわゆる「Push型」で行っていたものとします。Railsに限定していますが、PHPとかでも大体同じようなことが言えると思います。
ECSになると、デプロイの仕方が変わりますし、そもそもSSHができない環境になるので、今まで普通にできていたことができなくなります。アプリケーション特有の事情も含むとキリが無いのですが、ざっと以下のようなことを考え直さないといけないはずです。
- デプロイをどうするか
db:migrate
をどうするか- cronをどうするか
- ログをどうするか
なぜECSにするのかとか、RailsアプリのDockerizeとか、その辺の話はしません。
デプロイをどうするか
まず考えなくてはいけないのは、やはりデプロイでしょう。マネージメントコンソールからでもデプロイはできますが、ボタンのポチポチを複数回する必要があり、手間がかかるので、何らかのツールを使って自動化したいところです。
残念ながら、この記事を書いてる時点で「これ!」というデプロイツールは存在しません。みんな自分たち専用のデプロイツールを作っているので、ここの選定はとても苦労すると思います(した)
個人的な要求としては
- Task DefinitionなどのECS独自概念をなるべく意識したくない。docker-composeみたいな既に知ってる概念をそのまま使いたい
- コンテナのメモリやCPU設定、イメージなどは設定ファイルに書いてVCSで管理したい。変更したらその設定を元に新しいコンテナが起動できて、既存のコンテナと切り替えられるようにしたい
heroku run
っぽく任意のコマンドを叩けるCLIが欲しい
といったものがあります。つまりdocker-composeが欲しかったので、「ECS docker-compose」とググったところ、ecs-cliが出てきたので、これを採用しました。
これで、docker-compose.yml
があればecs-cli compose service up
などすれば
docker-compose.yml
から適切なTask Definitionを作成- ECSのServiceを作成したTask Definitionで更新
という一連の流れをやってくれます。大体docker-compose up
と同じ振る舞いをするので、事前にローカルである程度確認できるというのが嬉しいポイントです。
ALBとのポートマッピングを考える場合には、最初のecs-cli compose service up
時に以下のような小細工をする必要がありますが、最初だけで大丈夫です。
$ ecs-cli compose service up --target-group-arn hogehoge --container-name app --container-port 3000 --role ecsServiceRole
対象のALBとかECSのクラスタとか、クラスタを構成するインスタンスとかは、CloudFormationとかTerraformで作ると良いでしょう。ecs-cliでもクラスタが作れますが、ALBは現時点でサポートされてないので、別のツールと組み合わせる必要があります。
後はJenkinsなどで、デプロイするトリガーを引いたら、docker build
-> ECRにpush -> ecs-cli compose service up
とすればデプロイのフローは完成です。簡単。タグ名とかはdocker-compose.yml
が環境変数をサポートしてるので、ecs-cli compose service up
の際に環境変数で与えてあげれば、docker-compose.yml
を書き換えることなく、デプロイするイメージのタグを変えられます。
version: '2' services: app: image: 1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:${VERSION} environment: - RAILS_ENV
$ VERSION=v1 ecs-cli compose service up
ちなみにスケールアウトもコマンド一発でできます。便利。
$ ecs-cli compose service scale 10
db:migrate
をどうするか
他に考えなくてはいけない問題として、db:migrate
のようなワンオフのジョブをどうするか、というのがあります。Capistranoだと、リーダーを決めて、デプロイ時にそこでコマンドを叩くようにすれば良いのですが、ECSにはそのような仕組みはありません。
ecs-cliには、heroku run
っぽくコマンドを実行する機能があるので、これを使います。デプロイとは別にdb:migrate
をする必要がありますが、これは仕方ないでしょう。
$ ecs-cli compose run oneoff "bundle exec rake db:migrate"
これでdocker-compose.yml
からTask Definitionを作成して、COMMAND
を上書きしてコンテナを起動してくれます。コンテナはWebサーバ用のTask Definitionを使っても良いと思いますが、色々調整ができるように、専用のTask Definitionを作って、共通部分をextendsするようにしたほうが良いでしょう。
version: '2' services: base: image: 1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:${VERSION} environment: - RAILS_ENV
version: '2' services: oneoff: extends: file: base.yml service: base mem_reservation: 512m
ただ残念なことに、ecs-cli compose run
はジョブを登録するだけで、ジョブの出力は流れてこないので、流して見たいならばawslogsなどのツールを使わないといけません。
ここはもうちょいなんとかしたい…
cronをどうするか
後は、今までcrontabを使っていた仕組みをどう置き換えるかという問題があります。これもCapistranoだとリーダーを決めて、デプロイ時に特定のサーバのcrontabを更新していたと思うのですが、ECSにはそのような仕組みはありません。
ちょっと前までは、resque-schedulerなどを使って、代用していたらしいですが、最近ではScheduled Tasksという便利な機能があるので、それを使いましょう。
Scheduled Tasksの裏側はCloudWatch Eventsなのですが、ECSのクラスタやTask Definitionが指定できます。これを使うと、特定の時間にTask Definitionを元にコンテナを起動することができます。便利です。
ただ、ecs-cliからはScheduled Tasksを操作するコマンドが無いという問題があります。Issueを立てて、軽く実装もしてみたのですが、結局、cronのジョブはWheneverのconfig/schedule.rb
みたいな形でVCSで管理したいよね、となって挫折しました。
ecs-cli compose create
でcron用のTask Definitionは作れるので、デプロイの度に最新のアプリケーションのイメージを参照するTask Definitionを作成するところまではできます。後はそのTask Definitionを指定して、Scheduled Tasksを都度作成するだけです。
新しい機能だったこともあり、他に使えそうなツールが無かったので、泣く泣く新しいGemを作りました。
Wheneverのようにcronジョブのタスク一覧をファイルにまとめて管理することをしたかったのですが、どうせならconfig/schedule.rb
がそのまま動いたら便利でしょと思って作りました。が、案の定、めちゃめちゃ大変だったので、とても後悔しています。
Elastic Wheneverでは、対象のTask Definitionのリビジョンを省略すると、最新のリビジョンを参照することができます。つまり
- ecs-cliで
docker-compose.yml
をベースにcronを動かすためのTask Definitionを作る - Elastic Wheneverで
config/schedule.rb
をベースにecs-cliで作られた最新のTask Definitionを参照するSchduled Tasksを作成する
ということができます。以下のような感じです。
$ ecs-cli compose create $ elastic_whenever -i myapp --set "cluster=ecs-test&task_definition=myapp&container=myapp"
ログをどうするか
awslogs
でCloudWatch Logsがサクッと使えるので、これを使うと簡単でしょう。
version: '2' services: app: image: 1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:${VERSION} environment: - RAILS_ENV logging: driver: awslogs options: awslogs-region: ap-northeast-1 awslogs-group: myapp
終わりに
他にも色々ありますが、書き出すとキリが無いのでこんなところです。絶対新しいツール作らないぞ、と思ってやり始めたはずなのに、結局作ってしまいました。誰か、みんなが使えるスタンダードを頑張って作ってください。
ecs-cliでECS専用のフィールドをdocker-compose.yml
に書けるようにしようよ、という動きがあるようなので、これに期待してもいいかもしれません。docker-compose.yml
の設定がそのまま動く、というメリットとの釣り合いをどうやって取るのかはわかりませんが…