マルチリージョン・マルチアカウントで発生するAWSイベントをChatBotでまるっと通知する

はじめまして。ZOZOテクノロジーズ CTO室 兼 SRE部 のkotatsu360です。
ウィスキーと葉巻とAWSが好きです∠( ﾟдﾟ)／

# この記事は
[ZOZOテクノロジーズ #4 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/zozo_tech4) 4日目です。

昨日は[tajimaTheMemer](https://qiita.com/tajimaTheMemer)さんの[Cloud Pub/Subから別のGCPプロジェクトのCloud Runをトークン認証付きで呼び出す](https://qiita.com/tajimaTheMemer/items/1deab1a01918d6d88c6e)でした。

# マルチリージョン・マルチアカウントで発生するAWSイベントをChatBotでまるっと通知する
## 前説

先日、こんな発表をしました。
[AWS Configを用いたマルチアカウント・マルチリージョンでのリソース把握とコンプライアンス維持への取り組みについて - Speaker Deck](https://speakerdeck.com/kotatsu360/using-aws-config-for-multi-account-multi-region-resource-understanding-and-maintaining-compliance?slide=23) by kotatsu360

AWS Configを使って、マルチアカウント・マルチリージョンの品質を維持しましょう！というお話でした。実はこの資料の内、動いたときに一番**(っ'ヮ'c)ｳｩｯﾋｮｵｱｱｧｱｱｱｧ**となったのは通知の部分だったりします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62601/1ed5a01a-f175-4ed8-8a8c-f9d492b71a3f.png)

この記事は、その通知部分を詳しく取り上げるものです。

登場するサービスについては登壇資料17ページあたりにまとめております。
もし、ナニコレというものがあればそちらを御覧ください。

## 目指したところ

1. 維持コストが無い（ほっといたら壊れてるがない）
2. AWSアカウント増減に伴う手間がかからない
3. 初期構築もできれば楽

1〜3の条件がそのまま優先度になっています。1は日常、2は時々、3は一回だけ。普段、手のかからないシステムは良いシステム。
これを目標にマネージドシステムを組み合わせました。

最終的に、初期構築の手作業もChatbotを有効化する、それだけです！

# 作り方

見出しは`【リソース作成先アカウント】【対象リージョン】`の構造です。
作業自体は全てMasterアカウントです。

ステップ0のみ手動ですが、あとは`CloudFormation` と `CloudFormation Service Managed StackSets`で行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62601/44cdcaf8-b8a4-5c67-3c07-b22c68484e65.png)

```
この記事を書いているときに、ステップ2に含まれるEventBusがDefaultじゃないことに気づいたので取り消し線を入れています。。すいません。
Defaultとそれ以外については、文中ででてきますのでそちらを御覧ください。
```

## 0. 【Masterアカウント】【リージョンなし（グローバル）】準備
ChatbotとSlackをつなぐところは、OAuthによる認可が必要です。
Masterアカウントで一回だけ行います。Chatbotはグローバルなサービスなので、リージョンを気にする必要はありません。

## 1. 【Masterアカウント】【シングルリージョン】Chatbotリソースの作成
ChatbotとSlackの認可が終わったら、実際にChatbotリソースを作成します。

Chatbotの設定をする際、Webコンソールからだと存在するSNSトピックしか設定できないのですが、CloudFormationからだと存在チェックを無視して指定できます。ここではリージョンの有効化状況に関係なく全リージョンを指定しておきます。

```yaml
Resources:
  IAMRole:
    Type: 'AWS::IAM::Role'
    Properties: (略）

  ChatbotSlackChannelConfiguration:
    Type: 'AWS::Chatbot::SlackChannelConfiguration'
    Properties:
      ConfigurationName: !Ref AWS::StackName
      IamRoleArn: !GetAtt IAMRole.Arn
      SlackChannelId: #####通知したいSlackチャンネル#####
      SlackWorkspaceId: #####通知したいSlackワークスペース#####
      SnsTopicArns: # [NOTE] Chatbotが使うSNSTopicは、存在チェックされない。全リージョン列挙しておく。
        - !Sub 'arn:aws:sns:us-east-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:us-east-2:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:us-west-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:us-west-2:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:af-south-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:ap-east-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:ap-south-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:ap-northeast-2:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:ap-southeast-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:ap-southeast-2:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:ap-northeast-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:ca-central-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:eu-central-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:eu-west-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:eu-west-2:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:eu-south-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:eu-west-3:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:eu-north-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:me-south-1:${AWS::AccountId}:my-events'
        - !Sub 'arn:aws:sns:sa-east-1:${AWS::AccountId}:my-events'
```

## 2. 【Masterアカウント】【マルチリージョン】他アカウントからのイベントを受け取るEventBusを作成
次に、他アカウントからChatbotへイベントを仲介する、EventBusを作成します。作成せずとも`default`というリソースが最初から用意されているのですが、ポリシーを設定して自分の好きにコントロールしたいので、`myevent`というリソースを新たに作成しています。

なお、このEventBridgeはリージョナルなリソースなので、CloudFormation Stacksetsを用いて自分自身（Masterアカウント）の全リージョンへEventBusを作成します。

```yaml
Resources:
  EventsEventBus:
    Type: 'AWS::Events::EventBus'
    Properties:
      Name: 'myevent'

  EventsEventBusPolicy:
    Type: 'AWS::Events::EventBusPolicy'
    Properties:
      Action: 'events:PutEvents'
      Condition:
        Type: 'StringEquals'
        Key: 'aws:PrincipalOrgID'
        Value: !Ref PrincipalOrgID # 組織内からの呼び出しであれば受け付ける
      EventBusName: !Ref EventsEventBus
      Principal: '*'
      StatementId: 'my-statement'

  SNSTopic:
    Type: 'AWS::SNS::Topic'
    Properties:
      TopicName: 'my-events'   # ChatBotで指定するため固定

  SNSTopicPolicy:
    Type: 'AWS::SNS::TopicPolicy'
    Properties:
      PolicyDocument:
        Version: '2008-10-17'
        Id: '__default_policy_ID'
        Statement:
          - Sid: '__default_statement_ID' # SNS Topicが持つ基本的な権限。適当なTopicからコピーしてくる。
            (略)
          - Sid: 'events'    # EventBusからのPutを許可
            Effect: 'Allow'
            Principal:
              Service: 'events.amazonaws.com'
            Action: 'sns:Publish'
            Resource: !Ref SNSTopic
      Topics:
        - !Ref SNSTopic

  EventsRuleConfigComplianceChange: # EventBusが受け付けたイベントの内、PatternにマッチするものだけをSNSに流す
    Type: 'AWS::Events::Rule'
    Properties:
      EventBusName: 'myevent'
      EventPattern:
        source:
          - 'aws.config'
        detail-type:
          - 'Config Rules Compliance Change'
        detail:
          messageType:
            - 'ComplianceChangeNotification'
          newEvaluationResult:
            complianceType:
              - 'NON_COMPLIANT'
              - 'COMPLIANT'
      State: 'ENABLED'
      Targets:
        - Arn: !Ref SNSTopic
          Id: 'my-chatbot-sns'
```

## 3. 【Memberアカウント】【マルチリージョン】Masterアカウントへイベントを送出するEventBusの作成
最後に、MemberアカウントへEventBusを作成します。

ここではMasterアカウントからCloudFormation Stacksetsを `SERVICE_MANAGED` モードで作成します。これにより、**新規AWSアカウントに対する実行を自動化できます。**

なお、地味にハマった部分が、`EventBusName: 'default'`の部分です。
**AWSサービスが発するイベントを処理したい場合、defaultリソースに対してルールを設定する必要があります。**

最初は「全部自分で、EventBusの作成からやるぞ〜！！！」と作業した結果、待てど暮らせどイベントが来ない。。という状況になりました。

```yaml
Resources:
  EventsRuleConfigComplianceChange:
    Type: 'AWS::Events::Rule'
    Properties:
      EventBusName: 'default' # [NOTE] AWSサービスのイベントはdefaultで受ける必要がある
      EventPattern:
        source:
          - 'aws.config'
        detail-type:
          - 'Config Rules Compliance Change'
        detail:
          messageType:
            - 'ComplianceChangeNotification'
          newEvaluationResult:
            complianceType:
              - 'NON_COMPLIANT'
              - 'COMPLIANT'
      State: 'ENABLED'
      Targets:
        - Arn: !Sub 'arn:aws:events:${AWS::Region}:${MasterAccount}:event-bus/myevent'
          Id: 'myevent'
          RoleArn: !GetAtt IAMRoleCloudWatchEvents.Arn

  IAMRoleCloudWatchEvents:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: 'Allow'
          Principal:
            Service: 'events.amazonaws.com'
          Action: 'sts:AssumeRole'
      Policies:
        - PolicyDocument:
            Statement:
              - Action:
                  - 'events:PutEvents'
                Effect: 'Allow'
                Resource: !Sub 'arn:aws:events:${AWS::Region}:${MasterAccount}:event-bus/myevent'
          PolicyName: 'post-to-parent-account'
```

以上を実行することで、各Memberアカウント・各リージョンのAWS Configから発されたイベントを、回り回ってSlackへ届けることができます！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62601/1d3e1aa1-e233-dd62-8dff-0a9060eeabed.png)


# まとめ
EventBridgeが提供するEventBusを中心に、CloudFormationを駆使することで、マルチアカウント・マルチリージョンでも手作業を最大限減らした通知基盤ができました。ルールを追加することで、どんなイベントでも通知することが可能です。

なお、お気づきの方もおられるかと思いますが、、、実はリージョンが増えたときだけはCloudFormation StackSetsの設定を変更するなど、手作業が必要です。それでもアカウント作成ほど頻繁ではないのであまり問題にはなっていません。

紹介したテンプレートについて、リソース類は漏れなく記載いたしましたが一部重要でない部分は省略しています。
気になることがあればコメント等で質問ください！！

明日は[r-tezuka](https://qiita.com/r-tezuka)さんのBezier作図環境に関する記事です。お楽しみに！

# 謝辞
この構成を検討するにあたって以下の記事にはとてもお世話になりました。ありがとうございます。
[【全リージョン対応】EventBridge + SNS + Chatbotで GuardDutyの結果を Slackチャンネルに通知する | Developers.IO](https://dev.classmethod.jp/articles/guardduty-finding-chatbot-all-region/)