【AWS CDK】API Gateway で S3 をプロキシしてオブジェクトの一覧を取得してみた

はじめに

テントの中から失礼します、CX事業本部のてんとタカハシです!

API Gateway では、S3 をプロキシして、オブジェクトのダウンロードやアップロード、削除など様々なことができます。単純な CRUD 操作であれば、Lambda を実装することなく実現可能なので結構便利です。

本記事では、API Gateway で S3 をプロキシして、Bucket に格納されているオブジェクトの一覧をする REST API を CDK で作成してみます。

下記のチュートリアルをガッツリ参考していますので、細かい説明についてはこちらをご参照ください。

他の記事で、オブジェクトのダウンロード、アップロード、削除にもチャレンジしています。

本記事及び、上記の記事にて試した機能を全て搭載した REST API のソースコードを下記リポジトリに置いています。

GitHub - iam326/api-gateway-proxy-to-s3-by-cdk

環境

環境は下記の通りです。

$ cdk --version
1.102.0 (build a75d52f)

$ yarn --version
1.22.10

$ node --version
v14.7.0

実装

  • オブジェクトを格納するための S3 Bucket を作成する
  • API Gateway で REST API を作成する
  • 上記 API にリソース/users/{userId}/filesを作成する
  • 上記リソースに GET メソッドを作成して、S3 をプロキシする

lib/api-gateway-proxy-to-s3-by-cdk-stack.ts

import * as cdk from '@aws-cdk/core';
import * as apigateway from '@aws-cdk/aws-apigateway';
import * as iam from '@aws-cdk/aws-iam';
import * as s3 from '@aws-cdk/aws-s3';

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

    const projectName: string = this.node.tryGetContext('projectName');

    // ★ S3
    
    const bucket = new s3.Bucket(this, 'Bucket', {
      bucketName: `${projectName}-bucket`,
    });

    // ★ API Gateway
    
    const restApiRole = new iam.Role(this, 'Role', {
      assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
      path: '/',
    });
    bucket.grantReadWrite(restApiRole);

    const restApi = new apigateway.RestApi(this, 'RestApi', {
      restApiName: `${projectName}-api`,
      deployOptions: {
        stageName: 'v1',
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
      },
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: ['POST', 'OPTIONS', 'PUT', 'DELETE'],
        statusCode: 200,
      },
    });

    // リソースを作成する `/users/{userId}/files`
    const users = restApi.root.addResource('users');
    const userId = users.addResource('{userId}');
    const files = userId.addResource('files');

    // オブジェクトの一覧を取得するための GET メソッドを作成する
    files.addMethod(
      'GET',
      new apigateway.AwsIntegration({
        service: 's3',
        integrationHttpMethod: 'GET',
        // 一覧取得の対象 Bucket を指定する
        path: bucket.bucketName,
        options: {
          credentialsRole: restApiRole,
          passthroughBehavior: apigateway.PassthroughBehavior.WHEN_NO_TEMPLATES,
          integrationResponses: [
            {
              statusCode: '200',
              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',
                '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': "'*'",
              },
            },
            {
              statusCode: '400',
              selectionPattern: '4\\d{2}',
              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': "'*'",
              },              
            },
            {
              statusCode: '500',
              selectionPattern: '5\\d{2}',
              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': "'*'",
              },              
            },
          ],
          requestTemplates: {            
            // メソッドリクエストのパスパラメータ userId の末尾に '/' を付けて、統合リクエストの querystring.prefix として指定する
            // 加えて、統合リクエストの querystring.delimiter として '/' を指定する
            // この prefix と delimiter は、S3 API の ListObjects に指定できるオプション
            'application/json': `#set($context.requestOverride.querystring.prefix = "$input.params('userId')/")
#set($context.requestOverride.querystring.delimiter = "/")`,
          },
        },
      }),
      {
        methodResponses: [
          {
            statusCode: '200',
            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,              
            },
          },
          {
            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,              
            },            
          },
          {
            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,              
            },            
          },
        ],
      }
    );
  }
}

動作確認

テキトーなテキストファイルを作成して、直接 S3 にアップロードします。その後、API Gateway で作成した REST API にリクエストして、オブジェクトの一覧を取得します。すると XML が返ってきます。

$ cat sample.txt
hello, world

$ aws s3 cp sample.txt s3://<BUCKET_NAME>/sample-user/sample1.txt
upload: ./sample.txt to s3://<BUCKET_NAME>/sample-user/sample1.txt

$ aws s3 cp sample.txt s3://<BUCKET_NAME>/sample-user/sample2.txt
upload: ./sample.txt to s3://<BUCKET_NAME>/sample-user/sample2.txt

$ aws s3 cp sample.txt s3://<BUCKET_NAME>/sample-user/sample3.txt
upload: ./sample.txt to s3://<BUCKET_NAME>/sample-user/sample3.txt

$ curl https://<DOMAIN_NAME>/v1/users/sample-user/files
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">...</ListBucketResult>

この XML を整形すると下記になります。JSON とかの方が扱いやすいのですが、残念ながら? XML で結果が返ってきてしまうので、呼び出し元にて JSON へ変換してあげる必要がありそうです。

<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name><BUCKET_NAME></Name>
  <Prefix>sample-user/</Prefix>
  <Marker></Marker>
  <MaxKeys>1000</MaxKeys>
  <Delimiter>/</Delimiter>
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>sample-user/sample1.txt</Key>
    <LastModified>2021-05-20T18:01:15.000Z</LastModified>
    <ETag>"22c3683b094136c3398391ae71b20f04"</ETag>
    <Size>13</Size>
    <Owner>
      <ID>...</ID>
      <DisplayName>...</DisplayName>
    </Owner>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
  <Contents>
    <Key>sample-user/sample2.txt</Key>
    <LastModified>2021-05-20T18:01:19.000Z</LastModified>
    <ETag>"22c3683b094136c3398391ae71b20f04"</ETag>
    <Size>13</Size>
    <Owner>
      <ID>...</ID>
      <DisplayName>...</DisplayName>
    </Owner>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
  <Contents>
    <Key>sample-user/sample3.txt</Key>
    <LastModified>2021-05-20T18:01:22.000Z</LastModified>
    <ETag>"22c3683b094136c3398391ae71b20f04"</ETag>
    <Size>13</Size>
    <Owner>
      <ID>...</ID>
      <DisplayName>...</DisplayName>
    </Owner>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
</ListBucketResult>

おわりに

本記事では、API Gateway で S3 をプロキシして、Bucket に格納されているオブジェクトの一覧を取得してみました。結果が XML で返ってくるのは少し残念でしたが、Lambda で実装することなく、オブジェクトの一覧が取得できるのは便利そうですね。

今回は以上になります。最後まで読んで頂きありがとうございました!