見出し画像

Laravel on ECSでのバッチ実行基盤事例

こんにちは。エンジニアの佐々木です。
最近、映画アルゴの主人公のような密度が濃く整った髭を目指し日々手入れを行っています。

さて。以前、社内勉強会で発表した「北欧、暮らしの道具店」におけるSREについての記事を書きました。

今回は、上の記事の中のスライド内で少しだけ触れていた「コンテナ技術(ECS/Docker)への移行」について、バッチ実行まわりでの技術選定や注意した点について紹介したいと思います。

ECS/Fargate移行前の課題

「北欧、暮らしの道具店」はこれまで、AWSのOpsWorks(Chef)を使用しEC2の構成管理を行ってきました。

インフラ側の変更を反映するためには新しくプロビジョニングしたEC2を毎回用意する必要があるため、そのときには以下の様な課題を抱えていました。

- Web・APIサーバーとして動いているEC2インスタンスは、その数だけプロビジョニングを実行し新規でEC2インスタンスを作成。その後ALBのターゲットグループで入れ替え、というような作業が発生。サーバーが増えると辛さが増す
- バッチ処理は冗長化ができておらず、重複実行されたり意図しない動作となってしまわないよう、サーバーにログインしてcronを停止するなどのオペレーションが必要
- キューワーカーに関しても変更内容によってはサーバーにログインして停止するなどのオペレーションが必要

ちなみに、バッチ処理はLaravelのartisanで自作したコマンドをcronで管理し、キューワーカーはLaravelが用意しているキューサービスをsupervisorで管理していました。

これらのトイルを解消し、メンテナンスもしやすい状態にしたいと考えたときに出てきたのがECS/Fargateの採用です。

開発するときはローカル環境にてDocker、Docker Composeを使用していました。いろんなエンジニアに相談したり記事を参考にした結果、本番・ステージングもECS/Fargateに移行してしまうことで運用負担を減らしメンテナンスしやすい状態にできるだろう、と考えプロジェクトを進めました。

実際にECS/Fargateに移行するにあたって、Web・APIサーバーに関しては、ログをどうするか、監視をどうするか、ステージングの認証をどうするかなどいくつか課題はあったものの、それぞれCloudWatch Logs、mackerel-container-agent、Amazon Cognitoの採用でクリアできました。

この記事ではその辺りは割愛し、バッチの実行基盤に関して説明をさせていただきます。

バッチ実行、どうするか

バッチ実行の基盤に関してはcronを使わずともいくつかとれる選択肢があります。その中で今回採用したのが以下の2つです。

- sqsjkr
- ECS Scheduled Task

「北欧、暮らしの道具店」では基本的にsqsjkr、一部でECS Scheduled Taskを採用しました。それぞれの大きなpros、consを挙げると以下の通りになります。

sqsjkr
- pros
  - Dockerイメージのpullが発生せず、常駐のプロセスが処理してくれるため処理開始までのオーバーヘッドが少ない
- cons
  - 処理プロセスがタスク定義パラメータstopTimeoutの値以上かかる場合、デプロイ時にECSクラスターから強制停止されてしまう可能性がある
ECS Scheduled Task
- pros
  - AWS公式の方法のため、ドキュメント・事例が多い
  - タスクの実行が完了するまで基本的にコンテナが終了することはない(タイムアウトなどでの強制停止がない)
- cons
  - コンテナとはいえ起動のオーバーヘッドがある。1minかかることも
  - 毎回コンテナのpullが発生するため、NAT Gateway経由でpullした場合などはお金がかかる
  - (退役したコンテナの履歴が大量に残るので邪魔に感じるときがある)

バッチ実行で2つの基盤を採用した理由

最初はすべてsqsjkrを採用し動かしていたのですが、本番切り替え前にconsに記載した強制停止する事象を発見し、ECS Scheduled Taskを追加で採用しました。

consに記載したstopTimeoutというパラメータは、コンテナが正常に終了しなかった場合にコンテナが強制終了されるまでの待機時間です。タスク定義パラメータの stopTimeout で設定できる最大値が120であり、デフォルトは30になります。

ECSのデプロイツールではecspressoを採用しているのですが、デプロイ時、コンテナの入れ替えのため古いコンテナを終了させる必要からECSクラスターが古いほうのコンテナに対してSIGTERMを送信します。しかし、バッチの処理内容によってはSIGTERMを受けてから処理が完了するまでに数分かかるなど、120秒を超えてしまうものもあります。

この問題によって、処理が途中で終了してしまったり、そのため排他処理のレコードが残り後続のバッチがいつまでも処理開始されなかったりといった問題を起こしてしまうことになります。

そこで、デプロイが発生してもコンテナが強制停止されないECS Scheduled Taskを採用しました。

ECS Scheduled Taskはecspressoでは管理できないため、ecscheduleというデプロイツールを別途採用しました。ecscheduleはecspressoにかなり影響を受けているというだけあって、使い勝手もよく導入はすんなりできました。

ECS Scheduled Task導入時注意したこと

sqsjkr、ECS Scheduled Taskどちらも仕組み上EventBridge(CloudWatch Events)、SQSを使用しています。

EventBridgeは仕様として、同じルールを複数回トリガーしてしまうことがあります。そのため、排他処理を行わないとバッチが多重実行されてしまい障害を起こしてしまう可能性があります。

sqsjkrに関しては多重実行されないよう、DynamoDBを使った排他処理の実装がなされています。

ECS Scheduled Taskに関しては自前で排他処理を用意する必要があります。

そのため、ECS Scheduled Taskで実行されるLaravelのarisanコマンドに関しては、排他処理を行うようDynamoDBにレコードをinsert/deleteする実装を追加で行いました。

移行してみて

ECS/Fargateに移行してみて、踏んだ課題はありましたがサーバーに入るオペレーションもなくなりバッチの運用がとても楽になりました。

sqsjkr, ECS Scheduled TaskどちらもEventBridgeを使っているので、サーバー内には入らずともecschedule/Terraformなどを使ってプルリクエストで特定のバッチだけを止めるということが可能になったり、AWSコンソール上から起動/停止の操作ができるのも、運用面ではとても大きいです。

最後に

クラシコムでは日々アプリケーション、インフラを改善し、継続し続けることができるシステム作りを目指しています。今回はインフラ面での話がメインでした。

クラシコムのインフラは、先日のISUCON11でも見事優勝した面白法人カヤックの fujiwara さんと週イチでMTGさせていただき、多くのアドバイスをいただいています。この記事のECS/Fargateまわりに関してもそうで、ありがたいことにコードレビューもしていただけています。

システムを良くしていくための土壌を今も作り続けてる途中ではありますが、一緒にアプリケーション・インフラ両面ともに改善を続け、良いサービスを提供していってくれる仲間を募集しています。