はじめに
EC2インスタンス上でRailsアプリを稼働していた副業先のいくつかの会社で、Rakeタスクやrails runnerコマンドを定期実行するためにwheneverなどを利用していました。
Railsアプリをコンテナ化してECSで稼働する際、タスクの定期実行をどう管理するか改めて考える必要があったので、その一例を紹介します。
※実際に対応したのは2年前なので、今はもう少し良い方法もあるかもしれませんが、参考までに。
前提条件
共通する条件としては、以下のようなものがありました。
- 定期実行したいタスクだけが数個ある。
- タスク間の依存関係はなく、複雑なワークフローを組む必要はない。
- アプリがRuby on Railsで構成されており、ECS Serviceで稼働している。そこで利用されているDockerイメージを使い、Rakeタスクやrails runnerコマンドを実行したい。
- タスクの実行は15分以上かかる。
ECS Scheduled Tasksの選定理由
前提条件のもと、なるべくシンプルな構成にすることを考えました。
ざっくりと以下のような理由から、ECS Scheduled Tasksで管理することにしました。(EventBridgeのルールベースでの管理)
- 実行に15分以上かかり、サービスで利用しているDockerイメージを利用したいので、LambdaではなくECS Taskを選定。
- 複雑なワークフローはなく、キュー管理なども不要なので、Apache AirflowやAWS Step Functions、AWS Batchあたりは利用しなくても良いと判断。
対応方法
DBマイグレーションのために rails db:migrate
コマンドを実行するECSタスク定義が存在していたため、そちらを利用した上で、containerOverridesでコマンドを上書きしました。
CloudFormationのテンプレートの抜粋は以下になります。
※一部タスク名など変えてます。
AWSTemplateFormatVersion: '2010-09-09'
...中略...
Resources:
Task1:
Type: AWS::Events::Rule
Properties:
Name: !Sub "${ServiceName}-${EnvName}-task-1"
ScheduleExpression: "cron(0 9 ? * * *)"
State: ENABLED
Targets:
- Id: !Sub "${ServiceName}-${EnvName}-task-1"
RoleArn: !GetAtt ECSTaskExecutionRole.Arn
EcsParameters:
TaskDefinitionArn: !Ref TaskDefArn
TaskCount: 1
LaunchType: 'FARGATE'
NetworkConfiguration:
AwsVpcConfiguration:
AssignPublicIp: 'DISABLED'
SecurityGroups:
- Fn::ImportValue: !Sub SecurityGroup-${EnvName}
Subnets:
- Fn::ImportValue: !Sub PrivateSubnet1-${EnvName}
- Fn::ImportValue: !Sub PrivateSubnet2-${EnvName}
Arn: !ImportValue
Fn::Sub: ECSClusterArn-${EnvName}
Input: '{ "containerOverrides": [{"name": "rails_runner", "command": [ "bundle", "exec", "rails", "runner", "xxxxx"]}]}'
Task2:
Type: AWS::Events::Rule
Properties:
Name: !Sub "${ServiceName}-${EnvName}-task-2"
ScheduleExpression: "cron(0 15 * * ? *)"
State: ENABLED
Targets:
- Id: !Sub "${ServiceName}-${EnvName}-task-2"
RoleArn: !GetAtt ECSTaskExecutionRole.Arn
EcsParameters:
TaskDefinitionArn: !Ref TaskDefArn
TaskCount: 1
LaunchType: 'FARGATE'
NetworkConfiguration:
AwsVpcConfiguration:
AssignPublicIp: 'DISABLED'
SecurityGroups:
- Fn::ImportValue: !Sub SecurityGroup-${EnvName}
Subnets:
- Fn::ImportValue: !Sub PrivateSubnet1-${EnvName}
- Fn::ImportValue: !Sub PrivateSubnet2-${EnvName}
Arn: !ImportValue
Fn::Sub: ECSClusterArn-${EnvName}
Input: '{ "containerOverrides": [{"name": "rails_runner", "command": [ "bundle", "exec", "rails", "runner", "xxxxx"]}]}'
実際にスタックを作成すると、ECSクラスターの設定画面の「スケジュールされたタスク」にて、スケジュール実行されるタスク名とCron式、Activeなスケジュールかどうかなどが確認できます。
また、「スケジュールされたタスク」の実態はEventBridge側で管理されており、そちらの設定を確認するとcontainerOverridesで上書きされたパラメータを確認することもできます。
※ターゲットへの入力の定数を表示すると、ダイアログボックスが開いて確認できます。
対応方法における注意点
今回の対応方法は、共通のECSタスク定義を利用し、containerOverridesで実行コマンドを上書きするようにしています。
ただ、この方法だとLogGroupの上書きやタスクのコンテナ名の上書きができないため、すべてのタスクの実行ログが同じLogGroupに含まれてしまいます。
なので、
- 定期実行したいタスクが少なければタスク定義を分割する。
- タスクが多ければ、Rakeタスクやrails runnerのスクリプト側に各タスクを実行していることがわかるような識別子を設定し、CloudWatch Logs Insightsなどを利用して確認する。
などの追加対応をするなど、何かしら工夫が必要になります。
おわりに
個人的にはECS Scheduled Tasksは簡単に定期実行を管理することができるので便利だと感じています。
一方で、ログ管理についてはある程度工夫が必要なので、運用上何を重視するか次第で検討してもいいのではないかと思います。
また、私はまだ試していませんが、1年ほど前にAmazon EventBridge Schedulerがリリースされており、こちらを利用するとタスク実行エラー時のリトライやデッドレターキューを利用したエラー通知などがしやすそうです。
現在はルールベースではなくこちらが推奨されているようなので、どこかのタイミングで試してみたいですね。