複数のクロスアカウントでS3イベント通知を構築するCfnテンプレート

# はじめに
S3バケットにオブジェクトを配置・更新した時に、複数の別のアカウントのLambdaから当該オブジェクトを取得してゴニョゴニョしたい、といったことがありました。
一部、S3の制約に引っかかるなどして手戻りもあったので作業の記録として記事に残します。

# 本文
## S3イベント通知
S3バケットのオブジェクト配置・更新をトリガーにLambdaなどをキックしたい場合、S3のイベント通知を使用することが有効です。

S3オブジェクトのイベントタイプを指定することで、オブジェクトが作成された時・削除された時など、必要に応じてイベントを指定することができます。
また、イベント通知のフィルターを活用することで、特定のファイルに絞り込むことが可能です。
大量のファイルを扱うバケットでも、特定のファイルの特定オブジェクトイベントのみLambdaをキックする、といったことが可能です。

## 制約
ただ、この通知設定において、フィルターのプレフィックスとサフィックスを指定している、かつ同じイベントタイプを設定しようとすると、エラーとなります。

例えば、「はじめに」に書いたように、特定オブジェクトの更新に対して、Lambdaを起動したいS3通知設定を作成します。
```
event: s3:ObjectCreated:*
filter prefix: images
filter suffix: jpg
通知先: Lambda Function A
```

そのうえで、以下のように設定を追加しようとすると、エラーとなります。
```
event: s3:ObjectCreated:*
filter prefix: images
filter suffix: jpg
通知先: Lambda Function A

event: s3:ObjectCreated:*
filter prefix: images
filter suffix: jpg
通知先: Lambda Function B
```

[無効なプレフィックスやサフィックスの重複がある通知設定の例。](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/notification-how-to-filtering.html)を確認すると、

> サフィックスが重複していなければ、プレフィックスが重複していても問題ありません。

 とあります。ですのでprefixだけ指定していれば、eventが重複していても問題ないのかもしれません。
 しかし、今回はsuffixまで厳密に指定したいため、別の方法で対応を考えます。


# 構成
上記を踏まえて、以下のような構成になります。
![qiita021601.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3328969/5828675d-b9a7-1dc1-b585-f2c5157ad2e7.png)
S3バケットのイベント通知先をSNSトピックにするところがミソになります。
S3の同じ条件のイベント通知を複数のLambdaに送信したい場合、SNSトピックを経由させます。SNSトピックのサブスクリプション先をLambda関数にすることで、複数のLambdaなどのサブスクリプション先にS3イベント通知を送信することができます。

# Cfnテンプレート
今回使用したCfnテンプレートを載せておきます。
今回の構成を実現するにはバケットポリシーや、SNSトピックのポリシーなど、複数の権限管理が必要になります。
一部マスクしていますが、それらも網羅しています。

### S3バケットを配置するアカウント
```
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  SnsTopic:
    Type: 'AWS::SNS::Topic'
    Properties:
      TopicName: 'topic'
      DisplayName: 'topic'
  SnsTopicPolicy:
    Type: 'AWS::SNS::TopicPolicy'
    Properties:
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: 'Subscribe'
            Effect: Allow
            Principal:
              AWS: !Ref Principals
            Action: 'sns:Subscribe'
            Resource: !Ref SnsTopic
          - Sid: 'Publish'
            Effect: Allow
            Principal:
              Service: 's3.amazonaws.com'
            Action: 'sns:Publish'
            Resource: !Ref SnsTopic
            Condition:
              ArnLike:
                'aws:SourceArn': !Sub arn:aws:s3:::<bucket_name>
      Topics:
        - !Ref SnsTopic
  S3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: <bucket_name>
      VersioningConfiguration:
        Status: Enabled
      NotificationConfiguration:
        TopicConfigurations:
          - Event: 's3:ObjectCreated:*'
            Topic: !Ref SnsTopic
            Filter: 
              S3Key:
                Rules:
                  - Name: prefix
                    Value: prefix
                  - Name: suffix
                    Value: suffix
    DependsOn: SnsTopicPolicy
  S3BucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Ref Principals
            Action: 's3:GetObject'
            Resource: !Sub arn:aws:s3:::<bucket_name>
```

### 送信先のLambdaを管理するアカウント
```
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  LambdaRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: LambdaPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - 's3:GetObject'
                Resource: !Sub arn:aws:s3:::<bucket_name>/*
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
  Lambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: function
      Handler: index.main
      Role: !GetAtt LambdaRole.Arn
      Code:
        ZipFile: 'xxxxxxxxxxxxx'
      Runtime: python3.9
      Timeout: 300
      MemorySize: 128
  LambdaPermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName: !Ref Lambda
      Principal: 'sns.amazonaws.com'
      SourceAccount: !Ref SoureAccountID
      SourceArn: !Sub arn:aws:sns:${AWS::Region}:<source_account_id>:topic
  ExternalSnsTopicSubscription:
    Type: 'AWS::SNS::Subscription'
    Properties:
      Protocol: 'lambda'
      TopicArn: !Sub arn:aws:sns:${AWS::Region}:<source_account_id>:topic
      Endpoint: !GetAtt Lambda.Arn
```

以下の、SNSサブスクリプションは、送信先のLambdaを配置するアカウント側で実行する必要があります。
（S3バケット側で実行しようとすると、エラーになります。）
```
  ExternalSnsTopicSubscription:
    Type: 'AWS::SNS::Subscription'
    Properties:
      Protocol: 'lambda'
      TopicArn: !Sub arn:aws:sns:${AWS::Region}:<source_account_id>:topic
      Endpoint: !GetAtt Lambda.Arn
```

# おわりに
S3のイベント通知などを駆使するイベントドリブンな構成は、AWSを利用する上ではよくある構成です。
しかし、ちょっと複雑なことをやろうとするといろいろな制約に引っ掛かり、手戻りなどで思ったより時間がかかってしまいました。

どなたかに役立てば幸いです。

# Appendix
- [Amazon S3 イベント通知を作成するときに「Amazon S3 イベント通知を作成する際に、次の宛先の設定を検証できません」というエラーが表示されるのはなぜですか?](https://repost.aws/ja/knowledge-center/lambda-invoke-error-s3-bucket-permission)
- [Lambda 関数をトリガーする Amazon S3 イベント通知を作成するときに、「Configuration is ambiguously defined」(構成の定義があいまいです) というエラーが表示されるのはなぜですか?
](https://repost.aws/ja/knowledge-center/lambda-s3-event-configuration-error)
- [CloudFormationでSNSトピックへの複数のイベント通知を設定したS3バケットを作成する
](https://dev.classmethod.jp/articles/create_bucket_multiple_event_notifications_to_sns_cfn/)
- [Fanout S3 Event Notifications to Multiple Endpoints](https://aws.amazon.com/jp/blogs/compute/fanout-s3-event-notifications-to-multiple-endpoints/)