RailsアプリをECSにのせてecs-cliでデプロイとかをする

RailsのECS移行事例なんて既に山ほどあるので、特に書くつもりは無かったのですが、実際にやってみると

  • 時代が進んで、より便利なものが出てきている
  • デプロイどうするのよ、となったときに各自が最強のECSデプロイツールを作っていて、参考にならない

といった体験をしたので、最近やったECS移行の話を書くことにしました。

もちろん、この記事も古くなると何の役にも立たないと思うので、古くなったら、みなさん頑張って調べてください。

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などすれば

  1. docker-compose.ymlから適切なTask Definitionを作成
  2. 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のリビジョンを省略すると、最新のリビジョンを参照することができます。つまり

  1. ecs-clidocker-compose.ymlをベースにcronを動かすためのTask Definitionを作る
  2. 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の設定がそのまま動く、というメリットとの釣り合いをどうやって取るのかはわかりませんが…