TerraformでAmazon EventBridgeを組むときは、ルール(aws_cloudwatch_event_rule)とターゲット(aws_cloudwatch_event_target)が別リソースに分かれ、呼び出し先の権限もさらに別リソースで書く構成になります。本記事では、カスタムバスのイベントでLambdaを呼ぶ最小構成のHCLから、再試行とDLQ、aws_scheduler_scheduleでの定時実行、事故になりやすい書き漏れまでを扱います。EventBridgeそのものの仕組みはAmazon EventBridgeとは?仕組み・料金とCLI・Pythonでの実装手順を参照してください。引数の仕様はAWSプロバイダ6.69.1(2026年10月15日公開)時点のドキュメントに基づきます。
まとめ:Terraform EventBridgeはルールとターゲットと権限の3点を揃えて書く
イベントで動かす処理は、ルール・ターゲット・呼び出し先の権限の3リソースを1組で書きます。Lambdaならaws_lambda_permissionのsource_arnにルールのARNを渡し、そのルールからの呼び出しだけを許可します。カスタムバスを使う場合は、ルールだけでなくターゲット側にもevent_bus_nameを書いてください。
定時実行は、デフォルトバス限定でUTC固定のスケジュールルールではなく、タイムゾーンを指定できるaws_scheduler_scheduleで書くのが現行の推奨です。再試行は既定で24時間・185回なので、業務の締め時刻に合わせて縮め、取りこぼしはDLQで受けます。2026年9月に追加された新しいCustom Event Busは、AWSプロバイダ6.69.1の時点で対応リソースがまだありません。
検証環境の準備とEventBridge関連リソースのAWSプロバイダ対応範囲
AWSプロバイダ6系とTerraform 1.16系で揃える検証環境の前提条件
本記事のHCLは、Terraform 1.16系(2026年10月時点の最新は1.16.6)とAWSプロバイダ6系を前提にしています。プロバイダの最新はv6.69.1のリリースノートで確認できます。6系から各リソースにregion引数が入り、1つのプロバイダ設定のまま別リージョンのルールを書けるようになりました。
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
認証情報とstateの置き場所の準備はTerraformでAWSを構築する手順|aws 6系の認証とS3バックエンド・エラー対処にまとめています。
aws_cloudwatch_event_で始まるリソース群と名前が旧称のままの理由
EventBridgeは旧名称のCloudWatch Eventsから改称されましたが、Terraformのリソース名はaws_cloudwatch_event_のままです。aws_cloudwatch_event_ruleのドキュメントにも「機能は同一」と注記されています。検索するときは「eventbridge」ではなく「cloudwatch_event」で探すのが近道です。
- カスタムイベントバス:
aws_cloudwatch_event_bus - ルール:
aws_cloudwatch_event_rule - ルールのターゲット:
aws_cloudwatch_event_target - バスのリソースポリシー:
aws_cloudwatch_event_bus_policy - 定時実行:
aws_scheduler_schedule(別サービス扱い)
アーカイブやAPI送信先もaws_cloudwatch_event_archiveなどの同じ接頭辞で揃っています。
カスタムバスのイベントでLambdaを呼ぶHCLを書いてapplyで動かす
カスタムバス・ルール・ターゲット・Lambda権限をつなぐHCLの全体像
注文サービスがordersバスへ送るOrderCreatedのうち、支払い済みのものだけを通知用Lambdaへ渡す例です。Lambda関数本体(aws_lambda_function.notify)は別ファイルで定義済みとします。
resource "aws_cloudwatch_event_bus" "orders" {
name = "orders"
}
resource "aws_cloudwatch_event_rule" "paid" {
name = "orders-paid"
event_bus_name = aws_cloudwatch_event_bus.orders.name
state = "ENABLED"
event_pattern = jsonencode({
"source" = ["app.orders"]
"detail-type" = ["OrderCreated"]
"detail" = { "status" = ["PAID"] }
})
}
resource "aws_cloudwatch_event_target" "paid_notify" {
rule = aws_cloudwatch_event_rule.paid.name
event_bus_name = aws_cloudwatch_event_bus.orders.name
target_id = "notify-lambda"
arn = aws_lambda_function.notify.arn
}
resource "aws_lambda_permission" "from_paid_rule" {
statement_id = "AllowFromOrdersPaidRule"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.notify.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.paid.arn
}
event_patternはjsonencodeで書くと、HCLの書式チェックが効き、JSON文字列の引用符のエスケープも要りません。パターンの長さは既定で2,048字、上限緩和の申請で4,096字までです。演算子の書き方はEventBridge イベントパターンの書き方:演算子一覧とtest-event-patternでの検証で扱っています。
is_enabledは非推奨になり、stateで有効・無効を指定します。注意したいのは、state = "ENABLED"ではCloudTrail経由で届くイベントが対象外になる点です。CloudTrailの管理イベントまで拾いたいルールはENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTSを指定します。この値はスケジュール式とは併用できません。
aws_lambda_permissionのsource_arnをルールARNで絞る
source_arnを省くと、events.amazonaws.comからの呼び出しがどのルール経由でも通るポリシーになります。同じアカウントの別チームが作ったルールからも呼べてしまうため、ルールのARNで絞るのが基本です。
カスタムバス上のルールのARNはrule/orders/orders-paidのようにバス名を挟む形式で、手書きすると間違えやすい箇所です。aws_cloudwatch_event_rule.paid.arnを参照すれば形式を意識せずに済みます。statement_idは関数内で一意にするため、ルール名を含めておきます。
terraform applyの後にput-eventsでテストイベントを送る確認手順
適用後は、パターンに合うイベントと合わないイベントを1件ずつ送って、届き分けを確かめます。
cat > entries.json <<'JSON'
[
{"Source": "app.orders", "DetailType": "OrderCreated",
"Detail": "{\"status\": \"PAID\", \"orderId\": \"A-1001\"}", "EventBusName": "orders"},
{"Source": "app.orders", "DetailType": "OrderCreated",
"Detail": "{\"status\": \"PENDING\", \"orderId\": \"A-1002\"}", "EventBusName": "orders"}
]
JSON
terraform apply
aws events put-events --entries file://entries.json
aws logs tail /aws/lambda/notify --since 5m
put-eventsの応答でFailedEntryCountが0なら、バスへの受け付けまでは成功です。LambdaのログにA-1001だけが出ていれば、パターンと権限の両方が意図どおりに動いています。何も出ない場合は、ルールのメトリクスを見ます。TriggeredRulesが増えていなければパターンの不一致、FailedInvocationsが増えていれば呼び出し先の権限不足を疑ってください。
ターゲットごとに異なる権限の持たせ方とrole_arnの要否
リソースポリシーで足りるターゲットとrole_arnが必須のターゲット
EventBridgeが呼び出し先へ届ける権限は、ターゲットの種類で持たせ方が変わります。公式のリソースベースのポリシーの説明と、aws_cloudwatch_event_targetのドキュメントのrole_arnの記載を突き合わせると、次のように整理できます。
| ターゲット | 権限の持たせ方 | 書くリソース |
|---|---|---|
| Lambda | リソースポリシーか実行ロール | aws_lambda_permission |
| SNS | リソースポリシーか実行ロール | aws_sns_topic_policy |
| SQS | リソースポリシーか実行ロール | aws_sqs_queue_policy |
| CloudWatch Logs | リソースポリシーのみ | ロググループのポリシー |
| Kinesis Data Streams | 実行ロール必須 | aws_iam_role |
| Step Functions・ECS | 実行ロール必須 | aws_iam_role |
| 別アカウント・別リージョンのバス | 実行ロール必須 | aws_iam_role |
Lambda・SNS・SQSは、role_arnを付けた場合はそのロールの権限で、付けない場合は呼び出し先のリソースポリシーで判定されます。実務では、呼び出し先1つに複数ルールがぶら下がるならリソースポリシー、1つのロールで複数の呼び出し先をまとめて管理したいなら実行ロール、という分け方が見通しのよい選択です。ロールの信頼ポリシーの書き方はAWS IAMとは?仕組み・ユーザー/ロール/ポリシーの違いと権限設計を参照してください。
CloudWatch Logsをターゲットにするときにrole_arnを付けない理由
公式ドキュメントは、CloudWatch LogsのターゲットではRoleArnを指定しないよう明記しています。ロールではなく、ロググループ側のリソースポリシーでevents.amazonaws.comからの書き込みを許可する仕組みだからです。
このポリシーはコンソールなら自動で作られますが、Terraformでは作られません。aws_cloudwatch_log_resource_policyでlogs:CreateLogStreamとlogs:PutLogEventsを許可し忘れると、applyは成功してもログが出ません。
再試行ポリシーとDLQをaws_cloudwatch_event_targetに書き足す
既定の24時間・185回を業務の締め時刻に合わせて縮めるretry_policy
EventBridgeの再試行の説明によると、配信に失敗したイベントは指数バックオフとジッタを挟みながら、既定で24時間・最大185回まで再送されます。使い切ったイベントは削除され、それ以上は追いません。
既定値はRetryPolicyのAPIリファレンスが示す上限値(60〜86,400秒、0〜185回)そのものです。たとえば注文の通知が1時間以上遅れて届くと業務上の意味がないなら、次のように縮めて早めにDLQへ落とします。
resource "aws_cloudwatch_event_target" "paid_notify" {
rule = aws_cloudwatch_event_rule.paid.name
event_bus_name = aws_cloudwatch_event_bus.orders.name
target_id = "notify-lambda"
arn = aws_lambda_function.notify.arn
retry_policy {
maximum_event_age_in_seconds = 3600
maximum_retry_attempts = 10
}
dead_letter_config {
arn = aws_sqs_queue.paid_dlq.arn
}
}
Lambda関数の中で起きた失敗は、EventBridgeではなくLambda側の非同期呼び出しの再試行で扱われます。両者の役割分担とDLQの置き場所はEventBridgeとSQS・Lambdaの非同期構成|キューポリシー・部分バッチ応答・DLQの置き場所で詳しく比べています。
DLQは標準キュー限定・同一リージョンで、キューポリシーは手書き
DLQの公式説明には、Terraformで書くときに効く制約が3つあります。FIFOキューは使えず標準キューのみであること、キューはルールと同じリージョンに置くこと、そしてAPI経由(Terraformもこちら)で作る場合はキューへの送信許可を手動で付けること、の3点です。
resource "aws_sqs_queue" "paid_dlq" {
name = "orders-paid-dlq"
message_retention_seconds = 1209600
}
data "aws_iam_policy_document" "paid_dlq" {
statement {
actions = ["sqs:SendMessage"]
resources = [aws_sqs_queue.paid_dlq.arn]
principals {
type = "Service"
identifiers = ["events.amazonaws.com"]
}
condition {
test = "ArnEquals"
variable = "aws:SourceArn"
values = [aws_cloudwatch_event_rule.paid.arn]
}
}
}
resource "aws_sqs_queue_policy" "paid_dlq" {
queue_url = aws_sqs_queue.paid_dlq.id
policy = data.aws_iam_policy_document.paid_dlq.json
}
権限の欠落やターゲットが存在しないといったエラーは、再試行されずに直接DLQへ送られます。DLQのメッセージにはERROR_CODEなどの属性が付くため、NO_PERMISSIONSが並んでいればTerraform側の権限の書き漏れと判断できる状態です。キューポリシー自体が無いと、DLQへの送信にも失敗し、メトリクスInvocationsFailedToBeSentToDLQだけが増えます。
定時実行はaws_scheduler_scheduleへ寄せるスケジュールルールとの差
schedule_expressionのルールはデフォルトバス限定かつUTC固定
aws_cloudwatch_event_ruleにもschedule_expressionはありますが、使えるのはデフォルトバスだけです。AWSはスケジュールされたルールの説明でこの機能を「レガシー」と位置づけ、EventBridge Schedulerを勧めています。スケジュールはUTC+0で評価されるため、日本時間の毎朝9時はcron(0 0 * * ? *)と書く必要があります。
aws_scheduler_scheduleで日本時間のcronを書くHCL
Schedulerではschedule_expression_timezoneで評価するタイムゾーンを指定でき、flexible_time_windowとtarget.role_arnが必須です(aws_scheduler_scheduleのドキュメント)。日本時間の毎日2時にバッチ用Lambdaを起動する例です。
data "aws_caller_identity" "current" {}
data "aws_iam_policy_document" "scheduler_assume" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["scheduler.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "aws:SourceAccount"
values = [data.aws_caller_identity.current.account_id]
}
}
}
resource "aws_iam_role" "scheduler" {
name = "nightly-batch-scheduler"
assume_role_policy = data.aws_iam_policy_document.scheduler_assume.json
}
resource "aws_iam_role_policy" "scheduler_invoke" {
role = aws_iam_role.scheduler.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{ Effect = "Allow", Action = "lambda:InvokeFunction", Resource = aws_lambda_function.batch.arn }]
})
}
resource "aws_scheduler_schedule" "nightly_batch" {
name = "nightly-batch"
schedule_expression = "cron(0 2 * * ? *)"
schedule_expression_timezone = "Asia/Tokyo"
flexible_time_window {
mode = "OFF"
}
target {
arn = aws_lambda_function.batch.arn
role_arn = aws_iam_role.scheduler.arn
retry_policy {
maximum_retry_attempts = 3
}
}
}
ルールのときと違い、Lambda側にaws_lambda_permissionは要りません。Schedulerは実行ロールを引き受けて呼び出すためです。信頼ポリシーのaws:SourceAccountは、Schedulerの実行ロールの説明が本番環境で推奨している「混乱した代理」対策の条件にあたります。nameとgroup_nameは変更すると作り直しになるので、命名は最初に決めてください。cron式の評価規則や手動実行の方法はEventBridge SchedulerのAPI|cron式・手動実行・上限と料金の実装ガイドで扱っています。
Terraform運用の落とし穴:event_bus_name漏れとポリシー上書き
ターゲット側のevent_bus_name書き漏れでdefaultを探しに行く挙動
カスタムバスで最も多い書き漏れが、ターゲット側のevent_bus_nameです。aws_cloudwatch_event_targetはこの引数を省くとデフォルトバスを使う仕様なので、ルールにはevent_bus_nameを書き、ターゲットに書き忘れると、デフォルトバス上に同名のルールを探してしまいます。
結果は、ルールが見つからないというapply時のエラーです。原因がバス名の不一致だと気づきにくいので、ルールとターゲットの両方でaws_cloudwatch_event_bus.orders.nameを参照する書き方に揃えてください。
bus_policyとevent_permissionの併用で他方の権限が消える事故
別アカウントからのイベントを受けるとき、バスのポリシーを書くリソースはaws_cloudwatch_event_bus_policyとaws_cloudwatch_event_permissionの2つがあります。aws_cloudwatch_event_bus_policyのドキュメントは、両者は互換性がなく、権限を上書きすると明記しています。
併用すると、後から適用したほうが先のステートメントを消し、次のplanで消えた側の差分が出ます。新規に書くなら、ポリシー文書全体をaws_iam_policy_documentで組み立ててaws_cloudwatch_event_bus_policy1本で管理する形を選ぶのが安全です。送信側に必要なIAMロールや受信側ルールのaccount指定は、EventBridge イベントバスの使い分け:クロスアカウント・リージョン送信のポリシー設定と公式のアカウント間送受信の説明を参照してください。
既存ルールをimportブロックで取り込むときのIDはバス名とルール名
コンソールで作った既存のルールをTerraform管理へ移すときは、importブロックを使います。IDの形式がリソースごとに違う点に注意が要ります。
import {
to = aws_cloudwatch_event_rule.paid
id = "orders/orders-paid"
}
import {
to = aws_cloudwatch_event_target.paid_notify
id = "orders/orders-paid/notify-lambda"
}
ルールは「バス名/ルール名」、ターゲットは「バス名/ルール名/ターゲットID」で、デフォルトバスならバス名を省けます。ターゲットIDはtarget_idを省くとランダムに振られるため、新規作成時から明示しておくと後の取り込みや差し替えが楽です。importブロックの書き方と-generate-config-outの使い方はterraform importで既存リソースをコード化する手順にまとめています。
EventBridgeをTerraformで管理すべき場面とコンソールで足りる場面
Terraform管理を採用する条件:複数環境・権限のレビュー・クロスアカウント
Terraformで書く価値が大きいのは、同じルール構成を開発・検証・本番へ複製する場合と、権限をレビューの対象にしたい場合です。EventBridgeの構成は権限やキューポリシーに分散し、コンソールで作ると自動で付いた権限が記録に残りません。送信側のロールと受信側のバスポリシーを対で持つクロスアカウント構成も、コードで揃える効果がはっきり出ます。ルールが1〜2本で権限も単純なら、Terraform化の手間に見合う効果は小さくなります。
Terraformに載せないほうがよい場面と新Custom Event Busの扱い
見送る判断をはっきりさせておきたいのは2つの場面です。1つ目として、2026年9月に追加された新しいCustom Event Busを使いたい場合が挙げられます。AWSプロバイダ6.69.1の時点で対応リソースは無く、追加要望のissue #50154と、aws_eventbridgev2_event_busなどを追加するプルリクエストが2026年10月13日時点でオープンのままです。リリースされるまでは、新しいバスだけを別の手段で作り、Classicのバスとルールは従来どおりTerraformで書く分担になります。
もう1つは、検証用に一時的に作るルールです。数日で消すルールまでstateに入れると、削除漏れと差分確認の手間が増えます。試行錯誤はCLIで済ませ、運用中のバスとルールだけを載せる線引きが現実的です。イベント駆動の構成をTerraformで設計から組みたい、既存のコンソール構成をコード管理へ移したいといった相談は、AWSを含むインフラ構築の受託で受け付けています。
よくある質問
TerraformでEventBridgeを書くときによく出る疑問に、公式ドキュメントの記載をもとに答えます。
aws_cloudwatch_event_ruleとaws_scheduler_scheduleはどちらを使えばよいですか?
イベントに反応して動かすならaws_cloudwatch_event_rule、時刻で動かすならaws_scheduler_scheduleです。Schedulerはタイムゾーン指定と1回限りの実行に対応しています。既存のスケジュールルールは動いている限りそのままでよく、新しく書く定時実行と、タイムゾーンの換算ミスが起きた箇所からSchedulerへ寄せるのが無理のない移行です。
1つのルールに複数のターゲットを付けられますか?
付けられます。aws_cloudwatch_event_targetを複数書き、同じruleとevent_bus_nameを指定し、変更するのはtarget_idだけです。再試行とDLQはターゲットごとに持てるので、通知は1時間で諦め、会計連携は24時間粘る、という差も付けられます。for_eachで一覧から生成すれば、追加は1行の変更です。
event_patternはjsonencodeとヒアドキュメントのどちらで書くべきですか?
jsonencodeを勧めます。HCLの構文として検査されるため、カンマの過不足が分かるのはplanの時点です。ヒアドキュメントで書いたJSONは文字列のまま渡り、誤りはAPI呼び出し時のエラーで初めて分かります。変数や他リソースの属性をパターンに埋め込むときも、jsonencodeなら引用符の付け忘れが起きません。detail-typeのようにハイフンを含むキーは引用符で囲めばそのまま書けます。
別アカウントのイベントバスをターゲットにするときは何が必要ですか?
送信側では、ターゲットにrole_arnを指定し、そのロールに受信側バスへのevents:PutEventsを許可します。別アカウント・別リージョンのバスがターゲットのとき、role_arnは必須です。受信側では、aws_cloudwatch_event_bus_policyで送信側アカウントからのPutEventsを許可します。両アカウントのTerraformを別々に管理するなら、バスのARNを出力値として受け渡すと手書きのARNを減らせます。
terraform destroyでEventBridgeのルールが消えないのはなぜですか?
よくある原因は、AWSサービスが作成した管理ルールを取り込んでいる場合です。aws_cloudwatch_event_ruleとaws_cloudwatch_event_targetにはforce_destroy引数があり、AWSが作った管理ルールを削除するときにtrueを指定します。既定値はfalseなので、指定しないまま管理ルールを取り込むと削除できません。どのルールが管理ルールかは、aws events list-rules --event-bus-nameの出力にあるManagedByで見分けられます。
関連記事
- Amazon EventBridgeとは?仕組み・料金とCLI・Pythonでの実装手順、新Custom Event Busとの違いを解説【2026年版】:バス・ルール・ターゲットの全体像と新旧バスの比較。
- EventBridge SchedulerのAPI|cron式・手動実行・上限と料金の実装ガイド:Schedulerのcron式とタイムゾーンの評価規則。
- EventBridge イベントバスの使い分け:クロスアカウント・リージョン送信のポリシー設定:アカウントをまたぐ送受信の権限設計。
- EventBridge 入力トランスフォーマーの使い方:InputTemplateでSNS・Slack通知を整形する:ターゲットのinput_transformerに書く変換テンプレート。
- terraform lifecycleの使い分け|ignore_changes・depends_on・movedの判断:ルールやターゲットの名前変更をmovedで扱うときの判断。