こしあん
2025-02-12

クロスアカウントEventBridgeでEC2の状態変更通知を実現する


7{icon} {views}


Terraformで開発アカウントのEC2状態変更イベントを管理アカウントへ転送し、Lambdaでログを取得・通知する仕組みを構築。 カスタムイベントバスとIAMポリシーを組み合わせることで、安全かつ柔軟にマルチアカウントでのイベント管理を可能にしている。

はじめに

  • DOP勉強していて出てきた例。あるアカウントでEC2の状態が変更されたとき、それを別のアカウントで通知を受け取るというもの
  • ポイントはクロスアカウントのEventBridgeの連携

アーキテクチャー図

  • EventBridgeを2個作る。
  • 開発アカウントではデフォルトのイベントバスを使い、Event Ruleのみ定義する
  • 管理アカウントではカスタムのイベントバスを使い、Event Ruleも定義してLambdaに連結する

できるもの

管理側にデプロイするLambda

以下のようなLambdaを管理アカウント側にデプロイする

def lambda_handler(event, context):
    print("Received event:", event)

    detail = event.get("detail", {})
    instance_id = detail.get("instance-id", "unknown instance")
    state = detail.get("state", "unknown state")

    message = f"Instance {instance_id} changed state to {state}"
    print(message)

    return {
        "statusCode": 200,
        "body": message
    }

開発側でEC2を起動停止してみると

開発側のアカウントでEC2を起動すると以下のようなメッセージが表示される

Instance i-0d20ae48fcb7d176d changed state to running

起動したEC2を停止すると以下のようなメッセージが表示される

Instance i-0d20ae48fcb7d176d changed state to stopped

ここでLambdaに渡されるeventは以下の通り

Received event: {'version': '0', 'id': '4240000f-4957-9924-10fd-28f99e41c2be', 'detail-type': 'EC2 Instance State-change Notification', 'source': 'aws.ec2', 'account': '{develop_account_id}', 'time': '2025-02-12T04:04:36Z', 'region': 'ap-northeast-1', 'resources': ['arn:aws:ec2:ap-northeast-1:{develop_account_id}:instance/i-0d20ae48fcb7d176d'], 'detail': {'instance-id': 'i-0d20ae48fcb7d176d', 'state': 'stopped'}}

整形して表示するとこう

"Received event":{
   "version":"0",
   "id":"4240000f-4957-9924-10fd-28f99e41c2be",
   "detail-type":"EC2 Instance State-change Notification",
   "source":"aws.ec2",
   "account":"{develop_account_id}",
   "time":"2025-02-12T04:04:36Z",
   "region":"ap-northeast-1",
   "resources":[
      "arn:aws:ec2:ap-northeast-1:{develop_account_id}:instance/i-0d20ae48fcb7d176d"
   ],
   "detail":{
      "instance-id":"i-0d20ae48fcb7d176d",
      "state":"stopped"
   }
}

コード

解説(By o1)

このTerraformコードは、開発アカウントで発生したEC2の状態変更イベントを管理アカウントへ転送し、そこからLambdaを起動するマルチアカウント構成のEventBridge連携を実装しています。大きな流れとしては、(1)管理アカウントにイベントバスを作成し、(2)開発アカウント側からこのイベントバスへPutEventsできるようにIAMの設定を行い、(3)管理アカウントのEventBridgeルールで受信したイベントによってLambdaを呼び出す、という手順です。

まず管理アカウントでは、”aws_cloudwatch_event_bus”リソースで“development-events”というカスタムイベントバスを作成します。あわせて”aws_cloudwatch_event_bus_policy”により、開発アカウント(rootユーザー)からのPutEventsを許可するポリシーを付与します。これにより、イベントバスが別アカウントからのイベント受信を受け付けられるようになります。

次に、Lambda実行用のIAMロール(”aws_iam_role”)と、そのロールに基本実行ポリシーをアタッチする”aws_iam_role_policy_attachment”を用意しています。その後、”aws_lambda_function”を作り、Pythonのコードをzip化した成果物をアップロードします。このLambdaをトリガーするためのEventBridgeルール(”aws_cloudwatch_event_rule”)は、上記で作成したカスタムイベントバスを監視し、EC2インスタンスの状態が「running」「stopped」「terminated」になった際に発火するよう設定されています。また、”aws_cloudwatch_event_target”と”aws_lambda_permission”で、発火したイベントがLambdaを呼び出せるよう結び付けています。

一方の開発アカウントでは、同様に”aws_cloudwatch_event_rule”を使ってEC2インスタンスの状態変更を検知し、そのイベントを”aws_cloudwatch_event_target”で管理アカウントのイベントバスに転送する仕組みを構築します。ただし別アカウントへの転送には、”aws_iam_role”でEventBridgeがassumeできるロールを作成し、このロールに“events:PutEvents”を許可するポリシーを設定する必要があります。ターゲットの定義では”role_arn”にこのロールを指定し、”arn”に管理アカウント側のイベントバスARNを指定することで、イベントを正しく転送できるようになります。

このように両アカウントで設定を組み合わせることで、開発アカウントで起きたEC2の状態変更が管理アカウント側へイベントとして送信され、そのイベントをもとにLambdaが自動的に実行される仕組みが完成します。

Terraformの全体コード

#############################
# プロバイダーの設定
#############################

provider "aws" {
  alias   = "management"
  region  = "ap-northeast-1"
  profile = var.management_profile # 管理アカウント用の AWS CLI プロファイル
}

provider "aws" {
  alias   = "development"
  region  = "ap-northeast-1"
  profile = var.development_profile # 開発アカウント用の AWS CLI プロファイル
}

#############################
# 各アカウントの動的情報取得
#############################

data "aws_caller_identity" "management" {
  provider = aws.management
}

data "aws_caller_identity" "development" {
  provider = aws.development
}


#############################
# 管理アカウント側のリソース
#############################

# 管理アカウント側に、開発アカウントからのイベント転送先となるカスタムイベントバスを作成
resource "aws_cloudwatch_event_bus" "dev_events" {
  provider = aws.management
  name     = "development-events"
}

# イベントバスポリシー:開発アカウントのルートユーザーからの PutEvents を許可
resource "aws_cloudwatch_event_bus_policy" "allow_dev" {
  provider       = aws.management
  event_bus_name = aws_cloudwatch_event_bus.dev_events.name
  policy = jsonencode({
    Version : "2012-10-17",
    Statement : [
      {
        Sid : "AllowDevelopmentAccount",
        Effect : "Allow",
        Principal : {
          AWS : "arn:aws:iam::${data.aws_caller_identity.development.account_id}:root"
        },
        Action : "events:PutEvents",
        Resource : "arn:aws:events:ap-northeast-1:${data.aws_caller_identity.management.account_id}:event-bus/${aws_cloudwatch_event_bus.dev_events.name}"
      }
    ]
  })
}

# Lambda 用 IAM ロールの作成
resource "aws_iam_role" "lambda_exec_role" {
  provider = aws.management
  name     = "management_lambda_exec_role"
  assume_role_policy = jsonencode({
    Version : "2012-10-17",
    Statement : [
      {
        Effect : "Allow",
        Principal : { Service : "lambda.amazonaws.com" },
        Action : "sts:AssumeRole"
      }
    ]
  })
}

# Lambda に基本実行ポリシーをアタッチ
resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
  provider   = aws.management
  role       = aws_iam_role.lambda_exec_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# Lambda のコードを zip 形式にパッケージ化
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_file = "${path.module}/lambda_function.py"
  output_path = "${path.module}/.cache/lambda_function.zip"
}

# 管理アカウント側の Lambda 関数(Python)
resource "aws_lambda_function" "management_lambda" {
  provider         = aws.management
  function_name    = "ManagementTriggeredLambda"
  role             = aws_iam_role.lambda_exec_role.arn
  handler          = "lambda_function.lambda_handler"
  runtime          = "python3.12"
  timeout          = 5
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = filebase64sha256(data.archive_file.lambda_zip.output_path)
}

# 管理アカウント側:カスタムイベントバス上で EC2 の state が running となったイベントを検知する EventBridge ルール
resource "aws_cloudwatch_event_rule" "management_lambda_rule" {
  provider       = aws.management
  name           = "TriggerLambdaOnDevEC2Launch"
  event_bus_name = aws_cloudwatch_event_bus.dev_events.name
  event_pattern = jsonencode({
    "source" : ["aws.ec2"],
    "detail-type" : ["EC2 Instance State-change Notification"],
    "detail" : {
      "state" : [
        "running",
        "stopped",
        "terminated"
      ]
    }
  })
}

# 上記ルールのターゲットとして Lambda を登録
resource "aws_cloudwatch_event_target" "lambda_target" {
  provider       = aws.management
  rule           = aws_cloudwatch_event_rule.management_lambda_rule.name
  target_id      = "LambdaTarget"
  arn            = aws_lambda_function.management_lambda.arn
  event_bus_name = aws_cloudwatch_event_bus.dev_events.name
}

# EventBridge から Lambda を呼び出すための権限付与
resource "aws_lambda_permission" "allow_eventbridge" {
  provider      = aws.management
  statement_id  = "AllowExecutionFromEventBridge"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.management_lambda.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.management_lambda_rule.arn
}

#############################
# 開発アカウント側のリソース
#############################

# 開発アカウント側:EC2 の状態変更(running)を検知する EventBridge ルール
resource "aws_cloudwatch_event_rule" "dev_ec2_launch_rule" {
  provider = aws.development
  name     = "ForwardEC2LaunchToManagement"
  event_pattern = jsonencode({
    "source" : ["aws.ec2"],
    "detail-type" : ["EC2 Instance State-change Notification"],
    "detail" : {
      "state" : [
        "running",
        "stopped",
        "terminated"
      ]
    }
  })
}

# 1) イベントを転送するための IAM ロール定義
resource "aws_iam_role" "eventbridge_to_management_role" {
  provider = aws.development
  name     = "eventbridge-to-management-role"

  assume_role_policy = data.aws_iam_policy_document.assume_events_role.json
}

data "aws_iam_policy_document" "assume_events_role" {
  provider = aws.development
  statement {
    sid     = "AllowEventBridgeService"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["events.amazonaws.com"]
    }
  }
}

# 2) 作成したロールに、管理アカウントのイベントバスへの PutEvents を許可するポリシーを付与
resource "aws_iam_role_policy" "allow_put_events_to_management" {
  provider = aws.development
  name     = "allow_put_events_to_management"
  role     = aws_iam_role.eventbridge_to_management_role.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action   = ["events:PutEvents"]
        Effect   = "Allow"
        Resource = "arn:aws:events:ap-northeast-1:${data.aws_caller_identity.management.account_id}:event-bus/${aws_cloudwatch_event_bus.dev_events.name}"
      }
    ]
  })
}

# 3) aws_cloudwatch_event_target に role_arn を指定
resource "aws_cloudwatch_event_target" "forward_target" {
  provider  = aws.development
  rule      = aws_cloudwatch_event_rule.dev_ec2_launch_rule.name
  target_id = "ManagementEventBus"
  arn       = "arn:aws:events:ap-northeast-1:${data.aws_caller_identity.management.account_id}:event-bus/${aws_cloudwatch_event_bus.dev_events.name}"

  # (ここが重要) EventBridge がこのロールを引き受けて PutEvents できるようにする
  role_arn  = aws_iam_role.eventbridge_to_management_role.arn
}

所感

  • これは便利。マルチプロジェクト・プロダクトの運用で使いそう
  • EventBridgeのクロスアカウント連携が最初むずい。特にカスタムバスを定義してつなげるところ
  • EC2以外にもいろいろできるんだろうけど、いろいろ活用法はありそう


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です