🐶

【AWS CDKハマりポイント01】EventBridgeイベントバスのリソースベースポリシーのstatementは書き方が一味違う

2025/02/10に公開

こんにちは。
インフラエンジニアののぐさんです🐶

日頃AWS CDKを触って開発しており格闘中で、ハマって学習した内容を記事に残していこうと思います。

何がしたかったのか

  • クロスアカウントで、複数ソースアカウントA, BのEventBridgeRuleからターゲットアカウントのEventBridgeイベントバスに対してPutEventsしたかった
  • ターゲットアカウントのイベントバスがイベントを受け取るためには、ターゲットアカウントのイベントバスのリソースベースポリシーに、ソースアカウントA, BのEventBridgeRuleからのPutEventsを許可する必要があった
  • 上記を、CDKで実現する必要があった

先に結論:どんな感じのコードになるのか

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { CfnEventBusPolicy } from "aws-cdk-lib/aws-events";

export class EventBusResouceBasePolicyStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const sourceAccountIds = [
      "012345678901", // ソースアカウントA
      "012345678902", // ソースアカウントB
    ];
    const targetAccountId = "012345678903";

    sourceAccountIds.forEach(sourceAccountId => {
      const eventBusPolicyStatement = {
        Effect: 'Allow',
        Principal: { AWS: `arn:aws:iam::${sourceAccountId}:root` },
        Action: 'events:PutEvents',
        Resource: `arn:aws:events:ap-northeast-1:${targetAccountId}:event-bus/default`,
        Condition: {
          ArnEquals: {
            'aws:SourceArn': `arn:aws:events:ap-northeast-1:${sourceAccountId}:rule/eventBridgeRule`,
          },
        },
      }

      new CfnEventBusPolicy(this, `EventBusPolicyStatementFor${sourceAccountId}`, {
        statementId: `Allow PutEvents`,
        eventBusName: 'default',
        statement: eventBusPolicyStatement
      });
    });
  }
}

何にハマったのか

  1. CDKでEventBridgeイベントバスのリソースベースポリシーを記述する際、ポリシー中のstatementの型はany
  2. CDKでEventBridgeイベントバスのリソースベースポリシーを記述する際、statementの1つのオブジェクトは1つのConstruct

詳しく解説していきます。

1.CDKでEventBridgeイベントバスのリソースベースポリシーを記述する際、ポリシー中のstatementの型はany

CfnEventBusPolicyクラスのstatementの型を確認してみると、anyでした。

すなわち、下記の条件を満たしたJSONっぽいオブジェクトを生成してそれをstatementに引き渡す必要があります。

  1. Effect, Principalなど、実際にイベントバスポリシー内で指定可能なKeyを使用しており、必須のKeyが揃っていること
  2. PrincipalResourcesなどで指定するアカウントID、ARNが、実際に存在するものであること。存在しないIDやARNを指定した場合、エラーが発生します。

https://dev.classmethod.jp/articles/resource-base-policy-invalid-principal-error/

ふつう、リソースベースポリシーにはiam.PolicyStatement型などを指定することが多いですが、EventBridgeイベントバスのリソースベースポリシーのstatementはany型のため指定できず、コーディング時点で静的エラーが発生してしまいます。

CDKの公式ドキュメントには下記のような記述がありました。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_events.CfnEventBusPolicy.html

statement?

Type: any (optional)

A JSON string that describes the permission policy statement.

You can include a Policy parameter in the request instead of using the StatementId , Action , Principal , or Condition parameters.

See also: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbuspolicy.html#cfn-events-eventbuspolicy-statement

2.CDKでEventBridgeイベントバスのリソースベースポリシーを記述する際、statementの1つのオブジェクトは1つのConstruct

私が最終的に作りたかったのは、下記のようなリソースベースポリシーです。

statementキーに配列が指定されていて、配列の中にソースアカウントごとのオブジェクトが入っているイメージですね。

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "Allow PutEvents",
    "Effect": "Allow",
    "Principal": {
      "AWS": "arn:aws:iam::012345678901:root"
    },
    "Action": "events:PutEvents",
    "Resource": "arn:aws:events:ap-northeast-1:012345678903:event-bus/default",
    "Condition": {
      "ArnEquals": {
        "aws:SourceArn": "arn:aws:events:ap-northeast-1:012345678901:rule/eventBridgeRule"
      }
    }
  }, {
    "Sid": "Allow PutEvents",
    "Effect": "Allow",
    "Principal": {
      "AWS": "arn:aws:iam::012345678902:root"
    },
    "Action": "events:PutEvents",
    "Resource": "arn:aws:events:ap-northeast-1:012345678903:event-bus/default",
    "Condition": {
      "ArnEquals": {
        "aws:SourceArn": "arn:aws:events:ap-northeast-1:012345678902:rule/eventBridgeRule"
      }
    }
  }]
}

なので当初は、CDKコードも下記のように書いていました。

感覚的には、配列オブジェクトを作って、それをstatementに引き渡す感じですね。

	// 失敗パターン🥺
	const statementArray: any[] = [];
    sourceAccountIds.forEach(sourceAccountId => {
      const eventBusPolicyStatement = {
        Effect: 'Allow',
        Principal: { AWS: `arn:aws:iam::${sourceAccountId}:root` },
        Action: 'events:PutEvents',
        Resource: `arn:aws:events:ap-northeast-1:${targetAccountId}:event-bus/default`,
        Condition: {
          ArnEquals: {
            'aws:SourceArn': `arn:aws:events:ap-northeast-1:${sourceAccountId}:rule/eventBridgeRule`,
          },
        },
      }

      statementArray.push(eventBusPolicyStatement);
    })

    new CfnEventBusPolicy(this, `EventBusPolicyStatement`, {
      statementId: `Allow PutEvents`,
      eventBusName: 'default',
      statement: statementArray,
    });

ところが、実際にデプロイしようとすると下記エラーが発生。

どうやら、statementには指定できるのはanyな中身のオブジェクトに限り、配列は指定できないのですね。

EventBusResouceBasePolicyStack | 21:56:33 | CREATE_FAILED        | AWS::Events::EventBusPolicy | EventBusPolicyStatement Property validation failure: [Value of property {/Statement} does not match type {Map}]
❌  EventBusResouceBasePolicyStack failed: Error: The stack named EventBusResouceBasePolicyStack failed creation, it may need to be manually deleted from the AWS console: ROLLBACK_COMPLETE: Property validation failure: [Value of property {/Statement} does not match type {Map}]

上記の使用についてドキュメントを色々と探ってみましたが、公式で解決方法を提示しているところは見つかりませんでした。(もし見つかった方、ご存知の方いたら教えてください)

試行錯誤のうえで成功したのが、冒頭の「先に結論:どんな感じのコードになるのか」に示した通り。

もう一度お示しすると、コードは下記の感じですね。

    const sourceAccountIds = [
      "012345678901", // ソースアカウントA
      "012345678902", // ソースアカウントB
    ];
    const targetAccountId = "012345678903";

    sourceAccountIds.forEach(sourceAccountId => {
      const eventBusPolicyStatement = {
        Effect: 'Allow',
        Principal: { AWS: `arn:aws:iam::${sourceAccountId}:root` },
        Action: 'events:PutEvents',
        Resource: `arn:aws:events:ap-northeast-1:${targetAccountId}:event-bus/default`,
        Condition: {
          ArnEquals: {
            'aws:SourceArn': `arn:aws:events:ap-northeast-1:${sourceAccountId}:rule/eventBridgeRule`,
          },
        },
      }

      new CfnEventBusPolicy(this, `EventBusPolicyStatementFor${sourceAccountId}`, {
        statementId: `Allow PutEvents`,
        eventBusName: 'default',
        statement: eventBusPolicyStatement
      });
    });

statementごとにnew CfnEventBusPolicyすることが正解だったようです。

このコードの書き方によって先述の期待するポリシーを記述できましたが、こんなのわからんて、、、🥺

statementごとに生成するクラスならば、CfnEventBusPolicyっていうクラス名にも問題がある気がしますよね。。。

まとめ

かなりスポット的な記事だったため、この記事を求めている方々は少なさそうですね。

でもだからこそ、同じようにカユイ状況に追い込まれた方に役立てれば幸いです!

ちなみに

従来はクロスアカウントでイベントを送信するためにイベントバスを経由する必要がありましたが、送信先アカウントのターゲットに直接イベントを送信できるアップデートがありました。
(※ただし、送信先ターゲットは、リソースベースポリシーを設定できるリソースであることが条件です)

私が対応したときは、すでにイベントバス経由でのクロスアカウントPutEventsを想定して送信元アカウントのIAMロールなどを作成しており、送信先アカウントのイベントバスのリソースベースポリシーのみ未設定の状態だったため、アップデートの内容は使用することなく、従来通りの方式で実装しました。

https://aws.amazon.com/jp/blogs/compute/introducing-cross-account-targets-for-amazon-eventbridge-event-buses/

https://dev.classmethod.jp/articles/eventbridge-cross-account-direct-delivery-update/

Discussion