API GatewayからLambdaを通さずにS3へデータを入れる処理をCDKで書いたメモ

## 概要
API GatewayにPutしたデータをS3に格納してみたメモ。

[API GatewayからLambdaを通さずに直接DynamoDBにデータを入れる処理をCDKで書いたメモ](https://qiita.com/hibohiboo/items/fb801dca48f01403e0e2)と同様、インテグレーションでマッピングする。
[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/blob/09987ded34fe472bb7524947b14857d70124074f/cdk/lib/api-gateway-proxy-to-s3-by-cdk-stack.ts)

## CDK

```ts
import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
import { AwsIntegration, Cors, MethodLoggingLevel, PassthroughBehavior, RestApi } from 'aws-cdk-lib/aws-apigateway';
import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { Bucket, EventType } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
import { LambdaDestination } from 'aws-cdk-lib/aws-s3-notifications';


export class ApiGatewayProxyToS3ByCdkStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps & { projectId: string }) {
    super(scope, id, props);

    const projectName: string = props.projectId;
    const bucket = this.createBucket(projectName);
    const restApiRole = this.createRole(bucket);
    const restApi = this.createRestApi(projectName);

    // リソースを作成する `/test`
    const test = restApi.root.addResource('test');
    test.addMethod('PUT', this.createIntegration(restApiRole, bucket), {
      requestParameters: {},
      methodResponses: [this.createOkResponse(), this.create400Response(), this.createErrorResponse()],
    });
    const lambdaParamsDefault = {
      runtime: Runtime.NODEJS_20_X,
      handler: 'index.handler',
    };

    const lambdaFunction = new Function(this, 'lambdaFunction', {
      ...lambdaParamsDefault,
      code: Code.fromInline(`exports.handler = async (event) => { console.log(JSON.stringify(event)); };`),
    });
    bucket.addEventNotification(EventType.OBJECT_CREATED_PUT, new LambdaDestination(lambdaFunction));
  }
  private createBucket(projectName: string) {
    const bucket = new Bucket(this, 'Bucket', { bucketName: `${projectName}-proxy-to-bucket`, removalPolicy: RemovalPolicy.DESTROY });
    return bucket;
  }
  private createRole(bucket: Bucket) {
    const restApiRole = new Role(this, 'Role', { assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), path: '/' });
    bucket.grantReadWrite(restApiRole);
    return restApiRole;
  }
  private createRestApi(projectName: string) {
    const restApi = new RestApi(this, 'RestApi', {
      restApiName: `${projectName}-api`,
      deployOptions: {
        stageName: 'v1',
        loggingLevel: MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
      },
    });
    return restApi;
  }
  private createIntegration(restApiRole: Role, bucket: Bucket) {
    const responseParameters = {
      'method.response.header.Access-Control-Allow-Headers': "'Content-Type,Authorization'",
      'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,POST,PUT,GET,DELETE'",
      'method.response.header.Access-Control-Allow-Origin': "'*'",
    };
    return new AwsIntegration({
      service: 's3',
      integrationHttpMethod: 'PUT',
      // アップロード先を指定する
      path: `${bucket.bucketName}/{folder}/{object}.json`,
      options: {
        credentialsRole: restApiRole,
        passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH,
        // https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-logging-variables.html
        requestParameters: {
          // リクエストのEpocTime を 統合リクエストのパスパラメータ folder にマッピングする
          'integration.request.path.folder': 'context.requestTimeEpoch',
          // API Gateway が API リクエストに割り当てる IDを 統合リクエストの object にマッピングする。
          'integration.request.path.object': 'context.requestId',
        },
        integrationResponses: [
          {
            statusCode: '202',
            responseParameters: {
              ...responseParameters,
              'method.response.header.Timestamp': 'integration.response.header.Date',
              'method.response.header.Content-Length': 'integration.response.header.Content-Length',
              'method.response.header.Content-Type': 'integration.response.header.Content-Type',
            },
          },
          {
            statusCode: '400',
            selectionPattern: '4\\d{2}',
            responseParameters,
          },
          {
            statusCode: '500',
            selectionPattern: '5\\d{2}',
            responseParameters,
          },
        ],
      },
    });
  }
  private createOkResponse() {
    return {
      statusCode: '202',
      responseParameters: {
        'method.response.header.Timestamp': true,
        'method.response.header.Content-Length': true,
        'method.response.header.Content-Type': true,
        'method.response.header.Access-Control-Allow-Headers': true,
        'method.response.header.Access-Control-Allow-Methods': true,
        'method.response.header.Access-Control-Allow-Origin': true,
      },
    };
  }
  private create400Response() {
    return {
      statusCode: '400',
      responseParameters: {
        'method.response.header.Access-Control-Allow-Headers': true,
        'method.response.header.Access-Control-Allow-Methods': true,
        'method.response.header.Access-Control-Allow-Origin': true,
      },
    };
  }
  private createErrorResponse() {
    return {
      statusCode: '500',
      responseParameters: {
        'method.response.header.Access-Control-Allow-Headers': true,
        'method.response.header.Access-Control-Allow-Methods': true,
        'method.response.header.Access-Control-Allow-Origin': true,
      },
    };
  }
}

```

## 実行

```
PUT https://hoge.execute-api.ap-northeast-1.amazonaws.com/v1/test/

{
  "name": "test",
  "age": 20
}

```
### 実行結果

戻り値として202がかえることを確認。

```
HTTP/1.1 202 Accepted
Content-Type: application/json
Content-Length: 0
Connection: close
Date: Tue, 13 Feb 2024 16:36:34 GMT
x-amzn-RequestId: 02c8b819-390e-46b6-9b73-baf9d7c32815
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type,Authorization
x-amzn-Remapped-Content-Length: 0
x-amz-apigw-id: TFUW8GiNNjMEDpw=
Access-Control-Allow-Methods: OPTIONS,POST,PUT,GET,DELETE
Timestamp: Tue, 13 Feb 2024 16:36:35 GMT
X-Amzn-Trace-Id: Root=1-65cb9a92-6000c0252286a9f4603bc198
X-Cache: Miss from cloudfront
Via: 1.1 xxxx.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: NRT51-C1
X-Amz-Cf-Id: K2eEwSteJfEjgoqryMCDHdl7qceAs4sKiM4P1K6-pmPkTSdMhkmAbA==
```


S3に格納されることを確認。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/8ade1f99-8616-effc-b9c8-4c9192753147.png)


### S3トリガーで取得できるイベント
イベントログが出力されていることを確認。

```json
{
    "Records": [
        {
            "eventVersion": "2.1",
            "eventSource": "aws:s3",
            "awsRegion": "ap-northeast-1",
            "eventTime": "2024-02-13T16:36:34.598Z",
            "eventName": "ObjectCreated:Put",
            "userIdentity": {
                "principalId": "AWS:xxx:BackplaneAssumeRoleSession"
            },
            "requestParameters": {
                "sourceIPAddress": "xx.xx.xxx.xxx"
            },
            "responseElements": {
                "x-amz-request-id": "7DBKNF5FG9H7CXXA",
                "x-amz-id-2": "ehR9bsjJN9pjFSt/aoOpH646Hz++loxJYzyLzaULAYOPFji10+TYcNAPQGvrFbrEArLT1yZfWSoZn/CL08WFryJoqg58Z9Vb"
            },
            "s3": {
                "s3SchemaVersion": "1.0",
                "configurationId": "MTZjYmU1MDUtOThiZS00OGMzLWFhOTEtYmNhNzVlMmI2OTVj",
                "bucket": {
                    "name": "hoge-bucket",
                    "ownerIdentity": {
                        "principalId": "xxxxxx"
                    },
                    "arn": "arn:aws:s3:::hoge-bucket"
                },
                "object": {
                    "key": "1707842194527/02c8b819-390e-46b6-9b73-baf9d7c32815.json",
                    "size": 36,
                    "eTag": "37f2d6e6f0d7af5ca3967198d35bbde5",
                    "sequencer": "0065CB9A928F560569"
                }
            }
        }
    ]
}
```

## S3から取得する。

[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/blob/7585b69b472095d272c52ad0f6e764774d7e87d2/cdk/lib/api-gateway-proxy-to-s3-by-cdk-stack.ts)

## 参考

[x-amazon-apigateway-integration.requestParameters オブジェクト](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration-requestParameters.html)
[【AWS CDK】API Gateway で S3 をプロキシしてオブジェクトをアップロードしてみた](https://dev.classmethod.jp/articles/api-gateway-proxy-to-s3-put-object-by-cdk/)
[CDKを使って既存S3バケットのPUTイベントをトリガーにLambda関数を実行しようとしたらハマった話](https://dev.classmethod.jp/articles/cdk-s3notification-kick-lambda/)
[Lambda(node.js)からS3のオブジェクトをGetObjectCommandで取得する – AWS SDK for JavaScript v3](https://confrage.jp/lambdanode-js%E3%81%8B%E3%82%89s3%E3%81%AE%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92getobjectcommand%E3%81%A7%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B-aws-sdk-for-javascript-v3/)