AWS CDKから既存のLambdaトリガに既存のS3を設定する

IAMロールのポリシー設定権限が無い環境において、AWS CDKから既存のS3バケットNotificationに既存のLambdaを設定しようとしたら、[このようなissue](https://github.com/aws/aws-cdk/issues/4671)やらcfnリソースの依存性の循環やらで苦労したのでメモ。

### 解決のポイント
- 依存性の循環回避にはカスタムリソースで対応
    - [API Reference/custom-resources](https://docs.aws.amazon.com/cdk/api/latest/docs/custom-resources-readme.html)
- 既存LambdaにS3パーミッションを与えるにはローレベルなAPI`lambda.CfnPermission`を使う
    - v1.18.0時点で[このように](https://github.com/aws/aws-cdk/blob/v1.18.0/packages/%40aws-cdk/aws-lambda/lib/function-base.ts#L189)なっているため、`lambda.Function.fromFunctionArn().addPermission()`でパーミッション設定できない。warningすら出ず、何が悪いか気がつけないYO。
- カスタムリソースにLambdaを実行するためのIAMロールを指定する
    - ロールを指定しないと[CDK側](https://github.com/aws/aws-cdk/blob/v1.18.0/packages/%40aws-cdk/aws-lambda/lib/function.ts#L414)でロールを作成するが、作成権限が無いためリソース生成に失敗。。。
    - ↑開発者にIAMポリシー変更できる強めの権限が与えられている環境であれば、この点は問題無し。


### 実装例

```typescript
import iam = require('@aws-cdk/aws-iam');
import lambda = require('@aws-cdk/aws-lambda');
import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/core');
import cr = require('@aws-cdk/custom-resources');

export class S3NotificationStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        const bucketName = '{s3_bucket_name}';
        const functionArn = '{lambda_function_arn}';
        const lambdaRoleArn = '{iam_role_arn_for_execute_lambda}';

        // s3 bucket
        const bucket = s3.Bucket.fromBucketName(this, 'ExistingBucket', bucketName);

        // lambda function
        const fn = lambda.Function.fromFunctionArn(
            this,
            'ExistingFunction',
            functionArn);

        // avoid circular dependency.
        const cres = new cr.AwsCustomResource(this, 'S3NotificationResource', {
            role: iam.Role.fromRoleArn(this, 'LambdaRole',
                lambdaRoleArn),
            onCreate: {
                service: 'S3',
                action: 'putBucketNotificationConfiguration',
                parameters: {
                    Bucket: bucket.bucketName,
                    NotificationConfiguration: {
                        LambdaFunctionConfigurations: [
                            {
                                Events: ['s3:ObjectCreated:*'],
                                LambdaFunctionArn: fn.functionArn,
                                Filter: {
                                    Key: {
                                        FilterRules: [
                                            { Name: 'prefix', Value: 'hoge/' },
                                            { Name: 'suffix', Value: '.jpg' }]
                                    }
                                }
                            }
                        ]
                    }
                },
                physicalResourceId: 'S3NotificationCustomResource' + Date.now().toString()
            }
        });

        // avoid this.
        // https://github.com/aws/aws-cdk/blob/v1.18.0/packages/%40aws-cdk/aws-lambda/lib/function-base.ts#L189
        const perm = new lambda.CfnPermission(this, "AllowS3Invocation", {
            action: 'lambda:InvokeFunction',
            functionName: fn.functionName,
            principal: 's3.amazonaws.com',
            sourceArn: bucket.bucketArn
        });

        // for executing following order.
        // add permission to lambda -> set s3 notification
        cres.node.addDependency(perm);
    }
}
```

### 参考
- https://stackoverflow.com/questions/58087772/aws-cdk-how-to-add-an-event-notification-to-an-existing-s3-bucket