AWS FargateとDocker Composeで簡単分散RSpec

AWS Fargate早く東京に来てくれという願いをこめて、東京で1つでも事例を増やそうと記事を書いていたら公開する前にAWS Fargateが東京に来ることが先日発表されました!めでたいです。アリネ事業部の平田です。

今日はARINEで使っていく(かもしれない) AWS Fargate を使ったRSpecの実行環境の話と、Docker Compose使っているならFargateいいかもしれませんよ、という話をします。

背景

アリネ事業部では、なりたい自分がきっと見つかる美容メディア ARINE を運用しています。

ARINEのサーバサイドはRubyで書かれており、ウェブアプリケーションフレームワークはRuby on Railsを採用し、テストにはRSpecを使っています。
テストは徐々に増えており現在テストが1000件ほどで、テストにかかる時間も徐々に長くなり、完走するのに10分以上かかるようになってきました。(遅いテストが多い問題はあります)

Pull Requestをマージする前にJenkinsでテストを実行して全てのテストが成功することを確認しているため、テストに10分もかかるとストレスフルです。
テストはJenkinsで実行しているのでslaveを増やすなどもっと簡単に対応する方法がありますが、せっかく普段からDocker+ECSを使っていますし、テストだけならリージョンが違っても特に問題ないので、AWS Fargate東京にやってくるのに備えてFargateを使ってみました。

全体的な流れ

今回用意したのは非常に簡易な仕組みです。

Pull RequestなどによりJenkinsのジョブが実行されると、まずテスト用のイメージをビルドしてECRにプッシュします。

ECRにイメージをプッシュしたら、Fargateのクラスタにコンテナを適当なセット数デプロイしてテストを開始します。

この際、テストを実行するリビジョンのハッシュと現在のタイムスタンプをつなげた文字列を環境変数を経由してアプリケーションに渡しておきます。
これが、ワーカーが実行するテストを特定するキーになります。

アプリケーションコンテナは直接RSpecを起動するのではなく、テストを駆動するためのワーカーを起動します。ワーカーは、起動するとまずDynamoDBに先程受け取ったキーをItemのキーとして条件付き書き込みを試みます。まだキーが存在しない場合のみ書き込みが成功し、書き込みに成功したワーカーがmasterになります。masterといっても役割はテスト対象のファイル名をすべてSQSのキューに積むだけです。slaveはSQSのキューからメッセージを受け取るのを待機します。

それ以降はmaster含むすべてのコンテナがSQSからメッセージ(テスト対象のファイル)を受け取り、テストを実行していきます。実行結果のレポートはそれぞれユニークな名前をつけて保存しておき、すべてのテストが終了したあとにワーカーが結果を集約してS3に送ります。
SQSからメッセージを受け取れない状態が暫く続くと、テストが終了したと判断してワーカーは終了します。

JenkinsはECSのタスクの状態を監視し、テストの終了を待ち受けます。テストが終了したら、ワーカーごとにまとめられて送られてきたテストの結果をS3から取得して更に集約して、JUnit形式に変換してレポートとして保存したり、失敗したテストのスタックトレースなどはJenkinsのコンソールログでも見れるように標準出力にも出力します。

やっていることはたったこれだけで非常に簡易な仕組みですが、DynamoDBやSQS、ECSといったAWSに用意されているサービスのおかげで用意するものが少なく簡単に実現できていると思います。

ポイント

特に今回この仕組みが割と簡単に作れたのは、 ecs-cli compose コマンドの存在が大きいです。

ecs-cli composeコマンドを使うと、docker compose 用に用意していた設定ファイルを割とそのまま使って、望みの構成のコンテナ群を望みの数だけECSクラスターにデプロイできます。

ecs-cli compose はdocker-compose.ymlの内容を大体理解していい感じにECSのタスク定義を作成してくれます。今回でいうともともと開発環境やテストを実行する環境用としてapplication, mysql, redisがセットになったdocker-compose.ymlが既にあったため、これを少しECS用に編集すればよかったので楽でした。

Fargate で使う場合には、下記あたりが違い(注意点)になるかと思います。

  • dependsは使えない(無視される)ので注意
  • コンテナ間の通信にlinkによる名前解決は使えない
  • ログの設定はしておくほうがいい
  • リソースの設定もしておいたほうがいい
  • ホストのボリュームをマウントはできない

dependsは使えない

dependsは無視されるので、コンテナ間に依存関係がある場合には注意が必要な可能性があります。ただ、今回はアプリケーションのコンテナがmysql, redisコンテナに依存することを宣言しており、かつ「dependsはサポートしていないので無視するよ」という WARNINIG のログは出力されていましたが、実際には特に問題は起きませんでした。(ローカルではdependsなしで起動すると、mysqlの起動があとになったりして落ちることがある)

linkは使えない

Fargateを使う場合、ネットワークモードは awsvpc を使うことになりますが、 awsvpc モードの場合 link 機能を使うことはできません。このことはドキュメントにも書いてあります。ではどうすればいいかというと、

containers that belong to the same task can communicate over the localhost interface. A task can only have one elastic network interface associated with it at a given time.

と書いてあるとおり、localhostで通信することができます。大体docker-compose.ymlに環境変数としてデータベースのホストを指定したりしていると思いますが、そのときにlinkを使って名前を指定している場合は、そこをlocalhostや127.0.0.1や0.0.0.0に書き換えます。(localhostだとIPv6を使ってしまい通信できない可能性があります)

ログの設定

Fargateではログの設定をしておかないと、当然ですがホストに入ってログの確認とかはできないのでなにか問題が起きたときに苦労します。 Fargate の場合 awslogs しか使えないので、素直に awslogs を設定しましょう。このときCloudWatchLogsで送り先に指定したグループを作っておかないと、音もなくProvisionに失敗します。(コンテナがグループを作れるようにしておけば回避できるかもしれない)

リソースの設定

mem_limitなどの話です。タスク定義が作成される際、mem_limitを何も指定していない場合デフォルトではコンテナのメモリは512MBのハードリミットで作成されるようです。ここに気付かず試しているときにタスク定義側のメモリ制限をいくら増やしてもコンテナがOOMしてしまってハマりました。必要な場合はmem_limitをもっと大きくしておく必要があります。

ホストのボリュームはマウントできない

これまたFargateでは当然ですが、ホストのボリュームをマウントすることはできません。ローカルで起動する際にはmysqlでinnodbの設定を変更するためにmy.cnfをマウントして渡したりしていましたが、これはFargateできないので、commandに起動時の引数として渡して設定するようにしました。

 

と、実際に今回書き換えた点を中心にいくつか挙げましたが、このあたりに気をつけて docker-compose.yml を書き換えるのと、もう1つ ecs-params.yml を用意します。ecs-params.yml はタスク定義やVPCの設定などを記述するものです。

こちらは単にRoleやVPCの設定を書いてやれば良いだけなので問題はないでしょう。

たったこれだけ(!)でインスタンスを用意することなく、面倒なECSのタスク定義(!)を管理することもなくコンテナをデプロイできてしまいます。(VPC/Subnet/SecurityGroup/ECS Clusterの作成等は当然必要です)

今回のように同じセットでたくさん起動したい場合、

ecs-cli compose scale x

で起動するセット数を指定してやると指定したセットだけタスクを実行できるので、これで起動するコンテナのセット数を制御することにより、テストの並列数を制御しています。

効果はというと

これを使ってどの程度テストを高速に回せるでしょうか。

それぞれのステージでかかる時間は、

  • イメージのビルド・プッシュ
    • 変更次第だが通常10数秒程度
  • タスクの起動
    • PROVISIONING→PENDINGになるまで 10秒~4分
    • PENDING→RUNNINGまで 約90秒
  • テストの実行
    • 並列数次第で12分(1並列)~2分弱(8並列)

でした。

PROVISIONINGの時間が結構まちまちで、そこが長いとそこに引っ張られてしまいますが、そこが速く終われば全体で3~4分でテストが終えられました。

PENDING→RUNNINGにかかる時間というのはほぼイメージのPullにかかる時間なはずなので、イメージが軽い場合にはもっと速く終わるはずですが ARINE のアプリのコンテナは重いのでかなり時間がかかってしまっています。Fargate ではイメージのPullが始まってから終了するまでが課金対象の時間になるので、イメージを軽くするモチベーションがより高まります。

テストの実行は並列数次第ですが、ここは今キューへの積み方が非常に雑なのでまだまだ改善の余地はあります。

ちなみに料金ですが、このくらいの時間しか起動しないのであれば EC2 インスタンスでもそんなにお金はかからないので、あんまりお金的にとても良いということはないと思いますが、モデル的に計算だけしてみます。

今回は vCPU x 1, Memory x 2G で実行しています。

料金は https://aws.amazon.com/jp/fargate/pricing/ から引用すると

> vCPU あたりの料金は 1 秒あたり 0.00001406 USD (1 時間あたり 0.0506 USD) で、1 GB あたりのメモリは 1 秒あたり 0.00000353 USD (1 時間あたり 0.0127 USD) です。

このケースでは5並列で実行した際、 平均的にはPull~テスト完了まで4分半かかるので、

$0.00001406 * (4 * 60 + 30) * 5 = $0.01898
$0.00000353 * 2 * (4 * 60 + 30 ) * 5 = $0.009531
$0.01898 + $0.009531 = 0.028511 ≒ 3円

です。ARINEの場合1日に30回くらいJenkinsでのテストが走っているので1日に100円くらいはかかりそうですので、月だとFargateの料金のみで2000円くらいですね。他にもSQSやDynamoDBやdata transferの料金がかかります。 spot fleet を使ったりする場合と比較して料金的にメリットは恐らくあまりないと思います。

また、今回はFargateの検証を兼ねてFargateを使った環境を構築しましたが、CIでdockerを実行する環境としては AWS CodeBuild があり、こちらを使うほうが自然です。AWS CodeBuildでもdocker-composeを使うこともできますし、vCPUを増やして parallel_spec を使って並列実行するなどするだけでも十分な速さでテストできるので、CI等の環境としてはそちらのほうが優れていると思います。

まとめ

今回はAWS Fargateを使ったRSpecの実行環境について書きました。他の解決策と比較して今回のRSpecの並列実行は必ずしも良い方法とはいえませんが、Fargateを使ってみるお題としてはちょうどいい題材でした。我々のようにKubernetesを運用するのはオーバースペックな場合、いろんなタスクをコンテナ化してホイホイ実行する環境としてFargateはかなり便利だと感じました。

東京に来るのが待ち遠しいですね!