AWS

Abuse通知のリージョン間転送をServerless Framework+CloudFormation構文で構築する

koma

こんにちは!
結婚式にて、某ゲーム音楽を流しまくっていたのを参列していたオタク友達にバレて恥ずかしかったこまっちゃんです。

今回は、AWS人生を続ける上で重要なAbuse通知をIaCで攻略していきたいと思います。

Abuse通知って?

Abuse通知は、AWS リソースが不審なアクティビティに使用されている疑いがある場合に届きます。基本的に、通知を確認し次第の対応が必要です!

通知に対して 24 時間以内に応答がない場合、AWS は、お客様のリソースをブロックするか、お客様の AWS アカウントを停止することがあります。

最終的なアーキテクチャ

You must create an EventBridge rule for each Region that you want to receive AWS Health events for. If you don’t create a rule, you won’t receive events.

の通り、EventBridgeの送信ルールはマルチリージョンに展開する必要があります。

一方、EventBridgeはリージョン間転送が可能なので、受信ルールは東京(ap-norheast-1)に展開し、slack通知に用いるLambdaも同じく東京に作ります。

Serverless Frameworkを用いて構築

では、さっそく実装していきましょう。以下の順番で解説していきます。
本記事の本題はイベントのリージョン間転送なので、①はスキップいただいてもOKです!

  1. Lambdaの実装
  2. EventBridge 受信側ルールの実装
  3. EventBridge 送信側ルールの実装
  4. デプロイ

serverless framework用ディレクトリの構成は以下の形です。

serverless
 ├── api # Lambda
 │    ├── functions.yml
 │    ├── iam.yml
 │    └── serverless.yml
 ├── eventbridge-receiver # 受信側の設定
 │    ├── eventbridge.yml
 │    ├── iam.yml
 │    └── serverless.yml
 └── eventbridge-sender  # 送信側の設定
      ├── eventbridge.yml
      └── serverless.yml

1. Lambdaの実装

Q
全体像
service: aws-health-to-backlog-api

frameworkVersion: "^3.33.0"

useDotenv: true

custom:
  prune:
    automatic: true
    number: 3
  stage: ${env:STAGE}
  prefix: aws-health-${self:custom.stage}
  go:
    baseDir: ../
    binDir: .bin
    cgo: 0
    cmd: GOARCH=amd64 GOOS=linux go build -tags lambda.norpc
    monorepo: false
    supportedRuntimes: ["provided.al2023"]
    buildProvidedRuntimeAsBootstrap: true

plugins:
  - serverless-prune-plugin
  - serverless-plugin-split-stacks
  - serverless-go-plugin
  - serverless-function-outputs

provider:
  # General settings
  name: aws
  stage: ${self:custom.stage}
  region: ap-northeast-1
  stackName: ${self:custom.prefix}-cfs-api-${self:custom.project}
  deploymentMethod: direct
  # General function settings
  runtime: provided.al2023
  memorySize: 128
  timeout: 900
  environment:
    STAGE: ${self:custom.stage}
    SLACK_WEBHOOK_URL: ${env:SLACK_WEBHOOK_URL}
  logRetentionInDays: 90
  # IAM permissions
  iam:
    role: LambdaRole

functions:
  - ${file(./functions.yml)}

resources:
  - ${file(./iam.yml)}

Lambdaを定義

NoticeAbuseToSlack:
  handler: ../handler/slack/main.go
  name: ${self:custom.prefix}-lmd-slack-NotifyAbuseAlert
  package:
    individually: true
    patterns:
      - "!./../../handler/**"
      - ./../../handler/slack/**

Lambdaに付与するIAM Role

Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: ${self:custom.prefix}-rol-lmd-${self:custom.project}
      Description: Role allows logging and Lambda actions for Lambda
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: ${self:custom.prefix}-pol-lmd-${self:custom.project}
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - "Fn::Join":
                      - ":"
                      - - "arn:aws:logs"
                        - Ref: "AWS::Region"
                        - Ref: "AWS::AccountId"
                        - "log-group:/aws/lambda/*:*:*"
              - Effect: Allow
                Action:
                  - lambda:*
                Resource: "*"
  • LambdaはGo言語で書いていたので、プラグイン「serverless-go-plugin」を使用してzip化まで実行しています。
  • EventBridgeの受信側で使用するため、プラグイン「serverless-function-outputs」を使用して、Lambdaの名称やArnをOutputに出力しています。

2. EventBridge 受信側ルールの実装

Q
全体像
service: aws-health-eventbridge-receiver

frameworkVersion: "^3.33.0"

useDotenv: true

custom:
  prune:
    automatic: true
    number: 3
  stage: ${env:STAGE}
  region: ap-northeast-1
  prefix: health-${self:custom.stage}
  lambdaAbuseToSlack: ${cf:${self:custom.prefix}-cfs-api-abuse.NoticeAbuseToSlackLambdaFunctionName}
  lambdaAbuseToSlackArn: ${cf:${self:custom.prefix}-cfs-api-abuse.NoticeAbuseToSlackLambdaFunctionArn}

plugins:
  - serverless-prune-plugin
  - serverless-plugin-split-stacks

provider:
  # General settings
  name: aws
  stage: ${env:STAGE}
  region: ${self:custom.region}
  stackName: ${self:custom.prefix}-cfs-evb-receiver-abuse
  deploymentMethod: direct

resources:
  - ${file(./eventbridge.yml)}
  - ${file(./iam.yml)}

  - Outputs:
      AbuseEventReceiverBusArn:
        Value: !GetAtt AbuseEventReceiverBus.Arn
      EventBridgeCrossRegionIAMroleArn:
        Value: !GetAtt EventBridgeCrossRegionIAMrole.Arn

EventBridge Rule(CloudFormation記法)

Resources:
  AbuseEventReceiverBus:
    Type: AWS::Events::EventBus
    Properties:
      Name: ${self:custom.prefix}-evb-${self:custom.abuse}Bus

  AbuseEventReceiverRule:
    Type: AWS::Events::Rule
    Properties:
      Name: ${self:custom.prefix}-evb-abuseReceiverRule
      Description: "Receive ABUSE events from sender EventBridge"
      EventBusName: !Ref AbuseEventReceiverBus
      EventPattern:
        source:
          - aws.health
        detail-type:
          - AWS Health Abuse Event
        detail:
          eventTypeCategory:
            - issue
          service:
            - ABUSE
      State: ENABLED
      Targets:
        - Id: LambdaAbuseToSlack
          Arn: ${self:custom.lambdaAbuseToSlackArn}

  ResourceBasePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: ${self:custom.lambdaAbuseToSlack}
      Principal: events.amazonaws.com
      SourceArn: !GetAtt AbuseEventReceiverRule.Arn

クロスリージョンでイベントを通知するためのIAM rule(CloudFormation記法)

Resources:
  EventBridgeCrossRegionIAMrole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: ${self:custom.prefix}-rol-eventbridge-abuse
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub events.amazonaws.com
            Action: "sts:AssumeRole"
      Path: /
      Policies:
        - PolicyName: ${self:custom.prefix}-pol-eventbridge-abuse
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "events:PutEvents"
                Resource: !GetAtt AbuseEventReceiverBus.Arn

それぞれ解説していきます!

イベント受信用のカスタムイベントバス

東京リージョンには「送信用」「受信用」のEventBridgeが存在するため、両方ともdefaultバスになってしまわないよう、受信側はカスタムイベントバスを使用しています。

本来は、AWSサービスからの発出されるイベントはデフォルトバスを使用する形で使い分けるため、送信側はデフォルトバスで対応します。
(参考:default event bus vs custom event bus | AWS re:Post

AbuseEventReceiverBus:
  Type: AWS::Events::EventBus
  Properties:
    Name: ${self:custom.prefix}-evb-${self:custom.abuse}Bus

EventBridge 受信用ルール

イベントを受信するためのEventBridgeルールを作成します。

AbuseEventReceiverRule:
  Type: AWS::Events::Rule
  Properties:
    Name: ${self:custom.prefix}-evb-abuseReceiverRule
    Description: "Receive ABUSE events from sender EventBridge"
    EventBusName: !Ref AbuseEventReceiverBus
    EventPattern:
      source:
        - aws.health
      detail-type:
        - AWS Health Abuse Event
      detail:
        eventTypeCategory:
          - issue
        service:
          - ABUSE
    State: ENABLED
    Targets:
      - Id: LambdaAbuseToSlack
        Arn: ${self:custom.lambdaAbuseToSlackArn}
  • EventBusNameカスタムイベントバスを指定します。
  • EventPattern:Abuseイベントのみ通知するように絞り込んでいます。
    EventBridgeのマネジメントコンソールで試していただくと、わかりやすいのでおすすめです(下図)。
  • Targets:slackに通知するためのLambdaのarnをターゲットに設定します。
  • Id:任意の文字列でOKです。
abuse rule
Event Pattern コンソール画面

Lambdaへのリソースベースのパーミッション

コンソール上で作成する場合は自動で作成されるのですが、IaCの場合は手動で作成する必要があります。このパーミッションがないとLambdaの呼び出しをすることができません。

ResourceBasePermission:
  Type: AWS::Lambda::Permission
  Properties:
    Action: lambda:InvokeFunction
    FunctionName: ${self:custom.lambdaAbuseToSlack}
    Principal: events.amazonaws.com
    SourceArn: !GetAtt AbuseEventReceiverRule.Arn

リージョン間転送(クロスリージョン)のためのIAM Role

送信側のEventBridgeイベントルールがターゲットを指定する際に必要な、IAMロールとIAMポリシーを作成します。

送信側のEventBridgeに対して付与することで、sts:AssumeRoleの信頼ポリシーと、受信側EventBridgeにイベントを送信することができるようになります。

EventBridgeCrossRegionIAMrole:
  Type: "AWS::IAM::Role"
  Properties:
    RoleName: ${self:custom.prefix}-rol-eventbridge-abuse
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Principal:
            Service: !Sub events.amazonaws.com
          Action: "sts:AssumeRole"
    Path: /
    Policies:
      - PolicyName: ${self:custom.prefix}-pol-eventbridge-abuse
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action:
                - "events:PutEvents"
              Resource: !GetAtt AbuseEventReceiverBus.Arn

3. EventBridge 送信側ルールの実装

Q
全体像
service: aws-health-eventbridge-sender

frameworkVersion: "^3.33.0"

useDotenv: true

custom:
  prune:
    automatic: true
    number: 3
  stage: ${env:STAGE}
  region: ${opt:region}
  prefix: aws-health-${self:custom.stage}
  abuseEventReceiverBusArn: ${cf(ap-northeast-1):${self:custom.prefix}-cfs-evb-receiver-${self:custom.project}.AbuseEventReceiverBusArn}
  eventBridgeCrossRegionIAMroleArn: ${cf(ap-northeast-1):${self:custom.prefix}-cfs-evb-receiver-abuse.EventBridgeCrossRegionIAMroleArn}

plugins:
  - serverless-prune-plugin
  - serverless-plugin-split-stacks

provider:
  # General settings
  name: aws
  stage: ${self:custom.stage}
  region: ${self:custom.region}
  stackName: ${self:custom.prefix}-cfs-evb-sender-abuse
  deploymentMethod: direct

resources:
  - ${file(./eventbridge.yml)}

EventBridge Rule(CloudFormation記法)

Resources:
  AbuseEventSenderRule:
    Type: AWS::Events::Rule
    Properties:
      Name: ${self:custom.prefix}-evb-abuseSenderRule
      Description: "Send ABUSE events to receiver EventBridge"
      EventPattern:
        source:
          - aws.health
        detail-type:
          - AWS Health Abuse Event
        detail:
          eventTypeCategory:
            - issue
          service:
            - ABUSE
      State: ENABLED
      Targets:
        - Id: AbuseEventReceiverBus
          Arn: ${self:custom.abuseEventReceiverBusArn}
          RoleArn: ${self:custom.eventBridgeCrossRegionIAMroleArn}

受信側EventBridgeが出力した値を使用しつつルールを実装していきます。

EventBridge 送信用ルール

AbuseEventSenderRule:
  Type: AWS::Events::Rule
  Properties:
    Name: ${self:custom.prefix}-evb-abuseSenderRule
    Description: "Send ABUSE events to receiver EventBridge"
    EventPattern:
      source:
        - aws.health
      detail-type:
        - AWS Health Abuse Event
      detail:
        eventTypeCategory:
          - issue
        service:
          - ABUSE
    State: ENABLED
    Targets:
      - Id: AbuseEventReceiverBus
        Arn: ${self:custom.abuseEventReceiverBusArn}
        RoleArn: ${self:custom.eventBridgeCrossRegionIAMroleArn}
  • EventName(省略):defaultバスに受信されます。
  • EventPattern:受信側と同じです。受け口を広くする意味合いでは、送信側はもう少し緩くしてもいいかもしれません(Abuse以外の通知も受け取るようにできます)。
  • Targets:受信側のカスタムイベントバスをターゲットにしており、2にて作成したIAM Roleによってクロスリージョンでの転送を実現しています。
  • Id:任意の文字列でOKです。

4. デプロイ

以上でリソースの準備は終了です!最後にデプロイしていきます。

cd serverless/api && \
npx serverless@3.38.0 deploy --verbose --stage $STAGE --region $AWS_DEFAULT_REGION && \

cd ../eventbridge-receiver && \
npx serverless@3.38.0 deploy --verbose --stage $STAGE --region $AWS_DEFAULT_REGION && \

AWS_REGIONS=("us-east-1" "us-east-2" "us-west-1" "us-west-2" "ap-south-1" "ap-northeast-3" "ap-northeast-2" "ap-northeast-1" "ap-southeast-2" "ap-southeast-1" "ca-central-1" "eu-central-1" "eu-west-1" "eu-west-2" "eu-west-3" "eu-north-1" "sa-east-1")

# deploy to multi regions
cd ../eventbridge-sender && \
for region in "${AWS_REGIONS[@]}"; do
  npx serverless@3.38.0 deploy --verbose --stage $STAGE --region $region
done

serverless frameworkは単体でマルチリージョンへのデプロイをサポートしていません。
そこで、やむを得ずデフォルトで有効化されているリージョンごとにforループでデプロイする形式をとっています。

デプロイする際は順番が非常に重要です。依存性を考慮し、Lambda → 受信側EventBridgeルール → 送信側EventBridgeルール の順にデプロイしてくださいね!

さいごに

いかがでしたでしょうか?
Abuse通知はめったにお目にかかれるものではありませんが、いざ出会ってしまえば此処で会ったが百年目。アカウントの命がかかっているので、いち早く気が付きたいところです。

ぜひ素敵なAWSライフのために、IaCでのデプロイに挑戦してみてください!

AUTHOR
koma
koma
エンジニア
元々製薬業界で働いており、スクールを経てDWSに入社。主にgolangを使用したバックエンド業務に携わっているが、触ったことのない技術にも楽しく挑戦していきたいと考えている。趣味はスキューバダイビング。
記事URLをコピーしました