EventBridgeで実現するクロスリージョンイベントルーティング。バージニア -> 東京へのイベント転送を CDK でやってみた
こんにちは!製造ビジネステクノロジー部の小林です。
前回の記事「CloudFormation の失敗を Slack に通知する仕組みを AWS CDK で実装してみた」では、単一リージョン(東京)での CloudFormation 失敗通知を実装しました。
今回はその続編として、USリージョンの CloudFormation 失敗イベントを東京リージョンで受け取り、Slackに通知するマルチリージョン対応版を実装してみました!
なぜマルチリージョン対応が必要なのか?
前回の記事では、単一リージョンでのCloudFormation失敗通知を実装しました。しかし、実際の運用では以下のような課題が発生するときがあります。
複数リージョンにリソースをデプロイする場合
Amazon CloudFront の利用やサービスをグローバルに展開する場合、リソースを複数のリージョン(例:USと東京)にデプロイする必要があります。
各リージョンで個別に通知設定を行い、Slackチャンネルを分けてしまうと、運用するリージョンが増えるほど通知の管理コストが増大します。そのため、通知は一つのチャンネルに集約できたら管理は楽になるのかなと思います。

AWS Chatbotのリージョン制限
通知を一元化しようとした際、制限に直面しました。
AWS Chatbot の SlackChannelConfiguration はグローバル設定として扱われるため、同一の Slack チャンネル ID を複数のリージョンで重複して設定することができません。
これにより、「USリージョンにも同じ通知設定をデプロイする」というシンプルな方法が使えなくなりました。
解決策
この問題を解決するために、EventBridge のクロスリージョンルーティング機能を活用します。
USリージョン(イベント発生源): CloudFormationの失敗イベントを検知するルールを設定し、イベントを直接東京リージョンへ転送します。
東京リージョン(通知集約地): 転送されてきたUSのイベントを受け取り、一元管理しているSlack通知設定を通じてSlackに通知します。
この仕組みにより、デプロイは分散させつつ、通知経路の一元化を実現します。
構成図
今回実装するのは以下の構成です。

イベントフロー
- USリージョンでCloudFormationスタックのデプロイが失敗
 - デフォルトEventBusにCloudFormation失敗イベントが送信される
 - EventBridgeルールがイベントをキャッチ
 - 東京リージョンのデフォルトEventBusへクロスリージョン転送
 - 東京リージョンのEventBridgeルールがイベントを受信
 - SNSトピックがトリガーされる
 - AWS ChatbotがSNSからイベントを受け取り、Slackに通知
 
ポイント
- CloudFormationなどのAWSサービスイベントは、自動的にデフォルトEventBusに送信されます
 - カスタムEventBusへのルーティングは不要で、デフォルトEventBus同士で直接やりとりできます
 
EventBusとはなんでしょうか?今回のクロスリージョン連携を理解するために、EventBridgeを構成する基本的な要素である「EventBus」と「ルール」の役割を整理してみます。
EventBusとは?
EventBusは、イベント駆動型アーキテクチャにおける中央のルーター役です。複数のイベントソースからイベントを受信し、設定されたルールに基づいて適切なターゲットに配信します。

AWS 公式ドキュメント参照
つまり、EventBusを使用すると、イベントを複数のソースから複数の送信先やターゲットにルーティングできるようになります。
EventBridgeルールとは
EventBusに届いたイベントをフィルタリングし、対応するターゲットにルーティングする設定です。
ルールの構成要素
- イベントパターン: どのイベントを処理するか(フィルタ条件)
 - ターゲット: イベントをどこに送るか(SNS、Lambda、別のEventBusなど)
 
今回の実装の流れ(全体像)
この基本を踏まえると、今回のクロスリージョン構成における各リージョンの役割は以下のようになります。
USリージョン: CloudFormation失敗イベントをキャッチ → 東京EventBusに転送
東京リージョン: CloudFormation失敗イベントをキャッチ → SNS経由でSlackに通知
プロジェクト構成
今回のCDKプロジェクトは、以下のようなディレクトリ構造になっています。
cloudformation-notification/
  ├── bin/
  │   └── cloudformation-notification.ts    # メイン(TokyoStack & VirginiaStackをインスタンス化)
  ├── lib/
  │   ├── constructs/
  │   │   ├── events/
  │   │   │   └── index.ts                  # EventBridgeルール作成
  │   │   └── slack/
  │   │       └── index.ts                  # SNS + Slack通知設定
  │   ├── tokyo-stack.ts                    # 東京リージョンスタック
  │   └── virginia-stack.ts                 # USリージョンスタック
  ├── test/
  │   └── cloudformation-notification.test.ts
  ...
やってみた
1. Slackの準備とAWS Chatbotの事前設定
こちらについては前回の記事を参考に設定します。
2. CDKの実装
それでは、実装を見ていきましょう。
まず、CloudFormation失敗イベントをキャッチするEventBridgeルールを共通化します。
import * as events from "aws-cdk-lib/aws-events";
import { Construct } from "constructs";
export class EventsConstruct extends Construct {
  public readonly rule: events.Rule;
  constructor(scope: Construct, id: string) {
    super(scope, id);
    // EventBridge ルールを作成
    const rule = new events.Rule(this, "Rule", {
      eventPattern: {
        source: ["aws.cloudformation"],
        detailType: ["CloudFormation Stack Status Change"],
        detail: {
          "status-details": {
            status: [
              "CREATE_FAILED",
              "DELETE_FAILED",
              "IMPORT_ROLLBACK_COMPLETE",
              "IMPORT_ROLLBACK_FAILED",
              "ROLLBACK_COMPLETE",
              "ROLLBACK_FAILED",
              "UPDATE_FAILED",
              "UPDATE_ROLLBACK_COMPLETE",
              "UPDATE_ROLLBACK_FAILED",
            ],
          },
        },
      },
    });
    this.rule = rule;
  }
}
このConstructは、CloudFormationの失敗ステータスをキャッチするEventBridgeルールを作成します。東京とUSの両方のスタックで再利用できるようになっています。
次に、Slack通知の設定を行うConstructを作成します。
import * as cdk from 'aws-cdk-lib';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as chatbot from 'aws-cdk-lib/aws-chatbot';
import { Construct } from 'constructs';
export interface SlackConstructProps {
  rule: events.Rule;
}
export class SlackConstruct extends Construct {
  constructor(scope: Construct, id: string, props: SlackConstructProps) {
    super(scope, id);
    const { rule } = props;
    // SNSトピックの作成
    const topic = new sns.Topic(this, 'Topic');
    // Chatbotの設定
    new chatbot.SlackChannelConfiguration(this, 'SlackChannel', {
      slackChannelConfigurationName: this.node.path.replace(
      /[^a-zA-Z0-9-_]/g,
      '-'
      ),
      slackWorkspaceId: 'TXXXXXXXXX', // ワークスペースIDを指定
      slackChannelId: 'CXXXXXXXXX',   // チャンネルIDを指定
      notificationTopics: [topic],
    });
    /**
     * EventBridge ルールのターゲットとして SNS トピックを設定
     */
    rule.addTarget(new targets.SnsTopic(topic));
  }
}
このConstructは、EventBridgeルールをSNS経由でSlackに通知する設定を行います。
ポイント
- slackWorkspaceIdとslackChannelIdは、先ほど取得した値に置き換えてください
 - rule.addTarget()で、EventBridgeルールとSNSトピックを接続しています
 
東京リージョンのスタック(TokyoStack)
東京リージョンでは、イベントを受信してSlackに通知します。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { EventsConstruct } from './constructs/events';
import { SlackConstruct } from './constructs/slack';
export class TokyoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, {
      ...props,
      env: {
        region: 'ap-northeast-1', // 東京リージョン
      },
    });
    // EventBridge設定
    const eventsConstruct = new EventsConstruct(this, 'Events');
    // Slack通知設定
    new SlackConstruct(this, 'Slack', {
    rule: eventsConstruct.rule,
    });
  }
}
東京スタックでは、EventsConstructとSlackConstructを組み合わせて、イベント受信からSlack通知までを実現しています。
USリージョンのスタック(VirginiaStack)
USリージョンでは、イベントを東京リージョンに転送します。
import * as cdk from 'aws-cdk-lib';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import { Construct } from 'constructs';
import { EventsConstruct } from './constructs/events';
export class VirginiaStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, {
      ...props,
      env: {
        region: 'us-east-1', // USリージョン
      },
    });
    // EventBridge設定(EventsConstructを使用)
    const eventsConstruct = new EventsConstruct(this, 'Events');
    // 東京リージョンのEventBridgeにイベントを転送
    const tokyoEventBus = events.EventBus.fromEventBusArn(
      this,
      'TokyoEventBus',
      this.formatArn({
        service: 'events',
        resource: 'event-bus',
        resourceName: 'default',
        region: 'ap-northeast-1',
      })
    );
    // VirginiaスタックのEventBridgeルールに、東京リージョンのEventBusをターゲットとして追加
    eventsConstruct.rule.addTarget(
      new targets.EventBus(tokyoEventBus)
    );
    // 動作確認用にS3バケットを作成(意図的に失敗させる)
    // new cdk.aws_s3.Bucket(this, 'TestBucket', {
    //   bucketName: 'aws', // 既に存在するバケット名を指定
    //   removalPolicy: cdk.RemovalPolicy.DESTROY,
    // });
  }
}
ポイント
- EventBus.fromEventBusArn(): 東京リージョンのデフォルトEventBusを参照
 - rule.addTarget(): クロスリージョン転送の設定
 - テスト用バケット: 意図的に失敗するバケット名を設定(動作確認用)
 
メインアプリケーション
最後に、両方のスタックをデプロイするメインアプリケーションを作成します。
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { TokyoStack } from '../lib/tokyo-stack';
import { VirginiaStack } from '../lib/virginia-stack';
const app = new cdk.App();
new TokyoStack(app, 'CloudformationNotificationTokyoStack', {
  env: {
    region: 'ap-northeast-1', // 東京リージョン
  },
});
new VirginiaStack(app, 'CloudformationNotificationVirginiaStack', {
  env: {
    region: 'us-east-1', // USリージョン
  },
});
シンプルに両方のスタックをインスタンス化するだけです。
デプロイ
CDKスタックをデプロイします。
## TokyoStackとVirginiaStackの両方をデプロイ
cdk deploy --all --require-approval never
動作確認
デプロイが完了したら、まずコンソールで構築したリソースを確認してみます。
東京リージョン

USリージョン

USリージョンではSlackが作成されていないことが確認できますね。
次に動作確認を行います。
USリージョンのスタック(VirginiaStack)には、意図的に失敗するS3バケットが含まれています。このスタックを再デプロイすると、CloudFormationが失敗し、その通知が東京経由でSlackに届くはずです。
スタックのデプロイが失敗すると...

CloudFormationのイベントで上記のように表示されます。
すると、Slackチャンネルに以下のような通知が届きます。

通知内容には以下が含まれています。
- スタック名
 - 失敗したリージョン(us-east-1)
 
USリージョンのCloudFormation失敗が、東京のSlackチャンネルに正常に通知されました。
おわりに
今回は、USリージョンで発生した CloudFormation の失敗イベントを、東京リージョンの Slack へ通知する仕組みを実装しました。
これにより、複数リージョンにまたがるデプロイ状況を、 Slack チャンネルを増やすことなく 1箇所に集約して監視できるという、当初の課題をようやく解決することができました。
この記事がどなたかの参考になれば幸いです!






