[AWS CloudFormation] S3イベント通知先にLambdaを設定（循環依存回避）

# 循環依存の発生

以下のようにS3バケットに通知設定を行い、
オブジェクトが生成されたことをトリガにLambdaを実行しようとした場合、
互いのリソースを参照しており循環依存でデプロイエラーとなる

```yaml

  # S3バケット
  testBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${RootStackName}-test-bucket
      NotificationConfiguration:  # 通知設定
        LambdaConfigurations:
          - Event: 's3:ObjectCreated:*'
            #　** Lambda関数ARN参照 **
            Function: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${RootStackName}-OnDetectFunction'

  # S3にオブジェクトが生成されたことをトリガに実行されるLambda
  OnDetectFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: handlers
      FunctionName: !Sub ${RootStackName}-OnDetectFunction
      Policies:
        - S3ReadPolicy:
            BucketName: !Ref testBucket # ** S3バケット名を参照 **

  # Lambda実行権限をS3に追加
  TriggerLambdaPermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName: !GetAtt OnDetectFunction.Arn
      Principal: 's3.amazonaws.com'
```

# 循環依存の回避

[このAWSの回答](https://aws.amazon.com/jp/blogs/mt/resolving-circular-dependency-in-provisioning-of-amazon-s3-buckets-with-aws-lambda-event-notifications/)を参考にする
方法としては、一旦依存関係のない状態（通知設定をしていない状態）でS3バケットを定義し、
カスタムリソースを使用して、LambdaでS3バケットに通知設定を行う
以下が実際に記述したもの

```yaml
# S3バケット
testBucket:
  Type: AWS::S3::Bucket
  Properties:
    BucketName: !Sub ${RootStackName}-test-bucket
    # NotificationConfiguration削除

# S3にオブジェクトが生成されたことをトリガに実行されるLambda
OnDetectFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: handlers
    FunctionName: !Sub ${RootStackName}-OnDetectFunction
    Policies:
      - S3ReadPolicy:
          BucketName: !Ref testBucket

# Lambda実行権限をS3に追加
TriggerLambdaPermission:
  Type: 'AWS::Lambda::Permission'
  Properties:
    Action: 'lambda:InvokeFunction'
    FunctionName: !GetAtt OnDetectFunction.Arn
    Principal: 's3.amazonaws.com'

# S3バケットにイベント通知設定を追加するためのロール
ApplyNotificationFunctionRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Principal:
            Service: lambda.amazonaws.com
          Action: sts:AssumeRole
    ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
    Path: /
    Policies:
      - PolicyName: S3BucketNotificationPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Sid: AllowBucketNotification
              Effect: Allow
              Action: s3:PutBucketNotification
              Resource:
                - !Sub 'arn:aws:s3:::${testBucket}'
                - !Sub 'arn:aws:s3:::${testBucket}/*'

# カスタムリソース生成時に実行するLambda
ApplyBucketNotificationFunction:
  Type: AWS::Serverless::Function
  Properties:
    FunctionName: !Sub ${RootStackName}-ApplyBucketNotificationFunction
    Role: !GetAtt 'ApplyNotificationFunctionRole.Arn'
    InlineCode: |
      const AWS = require('aws-sdk');
      const S3 = new AWS.S3();
      const cfnResponse = require('cfn-response');

      exports.handler = async (event, context) => {
        try {
          if (event.RequestType !== 'Create') {
            return await cfnResponse.send(event, context, cfnResponse.SUCCESS);
          }
          const params = {
            Bucket: event.ResourceProperties.S3Bucket,
            NotificationConfiguration: {
              LambdaFunctionConfigurations: [
                {
                  Events: ['s3:ObjectCreated:*'],
                  LambdaFunctionArn: event.ResourceProperties.FunctionARN
                },
              ],
            },
          };
          await S3.putBucketNotificationConfiguration(params).promise();
          return await cfnResponse.send(event, context, cfnResponse.SUCCESS, {}, event.PhysicalResourceId);

        } catch (err) {
          return await cfnResponse.send(event, context, cfnResponse.FAILED, {});
        }
      };

# カスタムリソース
ApplyNotification:
  Type: Custom::ApplyNotification
  Properties:
    ServiceToken: !GetAtt 'ApplyBucketNotificationFunction.Arn'  # カスタムリソース生成時に実行するLambda
    # 以下はカスタムリソース生成時に実行するLambdaに渡すパラメータ
    S3Bucket: !Ref testBucket  # イベント通知設定を追加するS3バケット名
    FunctionARN: !GetAtt OnDetectFunction.Arn  # イベント通知送信先Lambda

```

注意事項

- CloudFormationに応答を返すための[cfn-responseモジュール](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html)は、インライン実装にしか対応していない
- `cfnResponse.send`で送信している[カスタムリソースの応答オブジェクト](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html)が正しくCloudFormationに届かない場合、リソース生成中の状態でハングアップしてしまう（応答を返さないパスがないことを確認する、`cfnResponse.send`を`await`で待たない場合もアウト）
- リソース生成中の状態でハングアップしてしまったリソースの削除にも1時間程度かかる