はじめに
「ECS のタスクを EventBridge で定期実行したい」
やりたいことは明確なのに、コンソールを開くと手が止まる。EventBridge に「タスク」という項目が見当たらない。あるのは「ルール」と「スケジュール」だ。
それもそのはずで、EventBridge に「タスク」という資源は存在しない。API にもコンソールにもない。それでも人が「EventBridge のタスク」と言うのは、EventBridge から起動される ECS 側のタスクを指しているから。
しかも ECS 側の「タスク」も一枚岩ではない。タスク定義・タスク・サービスの3つが、日常会話では全部「タスク」に圧縮される。
つまり詰まる原因は手順ではなく、登場人物の名前が両サービスでねじれていることにある。
この記事では、両サイドの登場人物を並べて、その言い換えを不要にする。
全体図:往路と復路
まず全体像から。EventBridge と ECS の関係は一方通行ではなく、往復している。
-
往路:EventBridge が ECS の
RunTaskAPI を代理で呼び、タスクを起こす - 復路:ECS がタスクの状態変化をイベントとして EventBridge に流す
この2つは独立した仕組みだ。片方だけ使うこともできる。
そして実際、「往路だけ組んで復路を組んでいない」構成はよくある。定時に起動する設定は入れたが、その結果を見る設定は入れていない、という状態。
👉 その構成がどうなるかというと、バッチが黙って死ぬ。定時に起動を試みて失敗しても、誰にも何も届かない。なぜそうなるかは第7節で回収する。
ECS 側の登場人物
ECS 側には4つ。
| 語 | 何か |
|---|---|
| クラスター | タスクを動かす場所の論理的なまとまり |
| タスク定義 | 何をどう動かすかの設計図 |
| タスク | 設計図から起こされた実行中の実体 |
| サービス | 指定した数のタスクを維持し続ける常駐管理者 |
タスク定義はイミュータブルで、登録するたびリビジョン番号が増えていく。webserver:5 は、ファミリ名 webserver とリビジョン 5 の組み合わせ。番号を省略すると最新の ACTIVE リビジョンが使われる。
❌ タスク定義とタスクは同じもの — 設計図と実体の関係。タスク定義そのものは何も動かさない。1つのタスク定義から何個でもタスクを起こせる。
サービスは「タスクを3つ動かし続けろ」と指示しておくと、落ちたぶんを立て直し続ける常駐の管理者。Web サーバーのような常駐プロセス向けの仕組みだ。
❌ サービスがないとタスクは動かない — RunTask を呼べば単発でタスクは起きる。サービスは「維持し続ける」役であって、起動の必須条件ではない。むしろバッチはサービスを使わない。1回走って終わるものを「維持」されても困る。
👉 RunTask でタスクを起こすとき、group を省略すると タスク定義のファミリ名が入る。family:<ファミリ名> という形になる。第7節でこのフィールドがまた出てくる。
EventBridge 側の登場人物
EventBridge 側には5つ。
| 語 | 何か |
|---|---|
| イベントバス | イベントが流れてくる通り道 |
| ルール | バス上のイベントを条件で拾い、ターゲットに配る仕分け係 |
| イベントパターン | ルールが「どのイベントを拾うか」を書いた JSON の条件式 |
| ターゲット | ルールが発火したときに呼ぶ相手 |
| スケジュール | EventBridge Scheduler が持つ、時刻で発火する独立した資源 |
ECS は自分の状態変化を、何もしなくても既定のイベントバスへ流している。こちら側でバスに流すための設定は要らない。サービスによっては EventBridge へ流すことを明示的に有効化する必要があるものもあるが、ECS はその必要がない。
ルールはそのバスの上に置く仕分け係。イベントパターンという JSON の条件式にマッチしたイベントを、登録されたターゲットに配る。
スケジュール(EventBridge Scheduler)はバスを経由しない。時刻が来たら直接ターゲットを呼ぶ、独立した資源だ。図でバスと繋がっていないのはそのため。
❌ EventBridge に「タスク」がある — ない。時間で何かを起こしたいなら Scheduler のスケジュール、イベントを拾って何かを起こしたいなら Rules のルール。この2つが「EventBridge のタスク」と呼ばれているものの正体。
❌ ルールが処理を実行している — ルールは仕分けしかしない。実際に処理するのは常にターゲット側のサービス。「ルールを作ったのに動かない」の多くは、ルール自体は正しく発火していて、その後のターゲット呼び出しで失敗している。ルールの発火とターゲットの成功は別々に見る必要がある。
往路:EventBridge が ECS タスクを起こす
ターゲットに ECS を選ぶというのは、「ECS の RunTask API をあなたの代わりに叩く」という設定を書くことである。
だからそこには RunTask に渡すぶんの情報が要る。それが EcsParameters と呼ばれるまとまりで、中身はこうなっている。
- 使うタスク定義の ARN
- キャパシティプロバイダ戦略、または起動タイプ(
launchType)- 両方は指定できない。どちらも省くとクラスターの既定戦略が使われる
-
awsvpcネットワークモード用のネットワーク設定(サブネットとセキュリティグループ)-
launchTypeがFARGATEの場合は必須
-
👉 EventBridge の設定でサブネットやセキュリティグループを聞かれるのは、EventBridge がネットワークを気にしているからではない。RunTask がそれを要求するから、代理で呼ぶ側が持たされているだけだ。
❌ こうして起動したタスクもサービスが面倒を見てくれる — 見ない。単発の RunTask なのでサービス配下ではない。落ちても再起動されないし、望みの数を維持する仕組みも働かない。「ECS ならサービスが立て直してくれる」という常識はここでは効かない。
❌ ターゲットの設定が通った=タスクが動く — RunTask の呼び出しは非同期だ。API が受理されても、そこから先で失敗しうる。イメージが引けない、サブネットに外への経路がない、ロールが足りない。
「呼んだ」「起動した」「正常終了した」は全部別の事実である。 これがこの記事の背骨なので覚えておいてほしい。
👉 では往路の呼び出しが失敗したことはどこで分かるか。ECS 側には何も残らない。タスクが1つも生まれていないのだから当然だ。
EventBridge はターゲット呼び出しの失敗を CloudWatch メトリクスに出し、DLQ(デッドレターキュー)を設定していればそこにイベントを送る。DLQ のメッセージには ERROR_CODE が付き、NO_PERMISSIONS や FAILED_TO_ASSUME_ROLE といった値を取る。
しかも権限不足や存在しないターゲットのような「リトライしても無駄なエラー」は、リトライされずに直接 DLQ へ送られる。次の節のロール設定を間違えたとき、最初に見るべき場所はここになる。
3つのロールが出てくる
往路には性格の違うロールが3つ登場する。ここが最大の山だ。
| ロール | 誰が引き受けるか | 何のため |
|---|---|---|
EventBridge のロール(ecsEventsRole) |
EventBridge(events.amazonaws.com) |
あなたの代わりに ecs:RunTask を呼ぶ |
タスク実行ロール(ecsTaskExecutionRole) |
ECS / Fargate 基盤(ecs-tasks.amazonaws.com) |
イメージを引く、ログを送る、シークレットを読む |
| タスクロール | タスクの中で動くアプリ(ecs-tasks.amazonaws.com) |
アプリが S3 や DynamoDB を触る |
❌ タスク実行ロールとタスクロールは同じもの — 前者はタスクを立ち上げる側の権限、後者は立ち上がった中身が使う権限。切り分けは簡単で、イメージが引けない・ログが出ないなら前者、アプリが AccessDenied を吐くなら後者を疑う。
EventBridge ロール用の AWS 管理ポリシー AmazonEC2ContainerServiceEventsRole の中身は3つしかない。
ecs:RunTask-
iam:PassRole(条件iam:PassedToServiceがecs-tasks.amazonaws.com) -
ecs:TagResource(条件ecs:CreateActionがRunTask)
なぜ iam:PassRole が要るのか
EventBridge が RunTask を呼ぶとき、「このタスクにはこのタスク実行ロールとこのタスクロールを付けてくれ」と指定する。
これは自分が使わないロールを他人に渡す行為だ。もし誰でも自由にロールを渡せるなら、RunTask の権限しか持たない人が、管理者権限のタスクロールを指定してタスクを起動できてしまう。だから「そのロールを渡してよいか」が別の権限として切り出されている。それが iam:PassRole。
👉 落とし穴はここ。上の管理ポリシーを使わずに自分でポリシーを書くなら、タスク実行ロールとタスクロールそれぞれの ARN を iam:PassRole の Resource に並べる必要がある。
これが抜けていると、ecs:RunTask は許可されているのにタスクが起動しない、という分かりにくい失敗になる。ECS のコンソールを見てもタスクは1つもいない。前節のとおり、痕跡は EventBridge 側の DLQ の ERROR_CODE に残る。
復路:ECS が EventBridge に流す
ここまでが往路。今度は逆向きの話。
ECS は自分の状態変化を勝手に EventBridge へ流している。そのための設定は要らない。必要なのは、流れてきているものを拾うルールを書くことだけだ。
ECS が送るイベントは8種類ある。
- Container instance state change
- Task state change
- Deployment state change
- Hook state change
- Service action
- Container instance health change
- Daemon deployment state change
- Daemon service action
バッチの死活を見たいなら Task state change を拾う。source は aws.ecs、detail-type は "ECS Task State Change"。
実物の JSON は 200 行近くあるので、要点だけ抜き出すとこうなる。
{
"detail-type": "ECS Task State Change",
"source": "aws.ecs",
"detail": {
"clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/example-cluster",
"taskArn": "arn:aws:ecs:us-east-1:123456789012:task/example-cluster/a1173316d40a45dea9",
"taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/webserver:5",
"group": "family:webserver",
"lastStatus": "RUNNING",
"desiredStatus": "RUNNING",
"containers": [
{ "name": "web", "lastStatus": "RUNNING" }
]
}
}
読み方の要点。
-
lastStatusは今の状態、desiredStatusはあるべき状態。この2つが揃うまで、同じタスクについて何度もイベントが飛ぶ。1タスク=1イベントではない -
groupがfamily:webserverなので、groupを指定せずにRunTaskで起こされたタスクだと分かる(第3節) - タスクが停止したときのイベントには
stopCodeとstoppedReasonが乗る -
detailの中のversionはリソースの版数で、重複配信されたイベントの見分けに使える。イベント本体側のversionは常に0なので混同しないこと - タスクのヘルスステータスはこのイベントに含まれない。必要なら
DescribeTasksを叩く
stopCode に入る値は6つ。
| stopCode | 意味 |
|---|---|
TaskFailedToStart |
起動そのものに失敗した |
EssentialContainerExited |
essential なコンテナが終了した |
UserInitiated |
人が StopTask した |
ServiceSchedulerInitiated |
サービスのスケジューラが停止させた |
SpotInterruption |
Fargate Spot が中断された |
TerminationNotice |
終了通知を受けた |
❌ RunTask が成功したからバッチも成功している — 第2節と第5節の伏線がここで回収される。
往路だけを組んだ構成では、次のどれが起きても何も通知されない。
-
TaskFailedToStartで1秒も動かなかった -
EssentialContainerExitedかつexitCodeが1で異常終了した -
SpotInterruptionで処理の途中で消えた
どれも ECS の中では正しくイベントが飛んでいる。拾うルールが無いだけだ。
👉 拾い方の方針としては、ECS Task State Change を lastStatus が STOPPED のものに絞り、そこからさらに stopCode や containers[].exitCode で異常だけを取り出して SNS などに流す。これで「定時に動いて正常に終わった」以外を検知できるようになる。
なぜ cron が2つあるのか
EventBridge で時刻起動を設定できる場所は2つある。そして片方は AWS 自身が legacy と宣言している。
EventBridge ユーザーガイドのスケジュール式ルールのページは、タイトルからして "Creating a scheduled rule (legacy)" であり、本文に "Scheduled rules are a legacy feature of EventBridge." と書かれている。コンソールのナビゲーションでも Scheduler > Scheduled rule (legacy) の位置に格納されている。そして "We recommend that you use Scheduler to invoke targets on a schedule." と明言されている。
両者を並べるとこうなる。
| スケジュール式ルール(legacy) | EventBridge Scheduler | |
|---|---|---|
| 位置づけ | イベントバスの上に載るルール | バスから独立した専用サービス |
| 資源の名前 | ルール | スケジュール |
| 使えるバス | 既定のイベントバスのみ | バスを使わない |
| タイムゾーン | UTC 固定 | 任意のタイムゾーンを指定できる |
| 最小粒度 | 1分 | 1分 |
| 単発実行 | できない(rate / cron の繰り返しのみ) | できる |
| 実行時刻のばらし | なし | フレキシブルタイムウィンドウあり |
| リトライ・DLQ | ターゲットごとに設定 | スケジュール自体に設定 |
👉 リトライと DLQ はどちらにもある。既定値も同じ(最大185回 / 最大24時間)。違うのは設定する場所だけで、ルールでは「ターゲットの追加設定」として、Scheduler では「スケジュールの設定」として書く。Scheduler が優れているのはここではなく、タイムゾーン・単発実行・タイムウィンドウ・スケーラビリティの側だ。
なお Scheduler では ECS の RunTask は templated target として用意されていて、EcsParameters を取る。第5節で書いた内容がそのまま当てはまる。
新規に定期バッチを組むなら Scheduler を選ぶ、が現時点の素直な答えになる。既にスケジュール式ルールで動いているものが明日壊れるわけではないが、新しく増やす積極的な理由は乏しい。
❌ Scheduler は Rules の上位互換だからルールはもう要らない — legacy なのは Rules のスケジュール式の部分だけだ。
イベントパターンで拾うルールは現役であり、S3 にファイルが置かれたら起動する、といったイベント駆動はルールの仕事。Scheduler は時刻でしか発火しない。そして第7節の復路——ECS が流すイベントを拾う——も、ルールでしかできない。
用語対応表
最後に、混同しやすい言い方を一覧で回収しておく。
| よく言われる言い方 | 正確には |
|---|---|
| EventBridge のタスク | Scheduler の「スケジュール」、または Rules の「ルール+ECS ターゲット」 |
| EventBridge で ECS を実行する | EventBridge が RunTask を代理で呼ぶ |
| ECS のタスクを登録する | タスク定義を登録する(新しいリビジョンが増える) |
| タスクを常時起動する | サービスを作ってタスク数を維持させる |
| バッチ用のサービスを作る | サービスは要らない。単発の RunTask で足りる |
| ロールを設定する | 3つある。EventBridge のロール / タスク実行ロール / タスクロール |
| EventBridge の cron | スケジュール式ルール(legacy)と EventBridge Scheduler の2つがある。新規は Scheduler |
まとめ
- EventBridge に「タスク」は無い。時刻で起こすなら Scheduler のスケジュール、イベントで起こすなら Rules のルール
- ECS の「タスク」は3層。タスク定義(設計図)/ タスク(実体)/ サービス(維持係)
- 往路は EventBridge が
RunTaskを代理で呼ぶ仕組み。だからEcsParametersが要るし、iam:PassRoleが要る - 復路は ECS が勝手に流している。拾うルールを書かなければ、失敗は黙って流れていく
- 「呼んだ」「起動した」「正常終了した」は別の事実。往路だけでは最初のひとつしか保証されない
- 定期バッチを新しく組むなら EventBridge Scheduler。ただしイベント駆動と復路は今もルールの仕事