🖼️

API Gateway + S3のサービス統合でファイル操作REST APIを構築

に公開

はじめに

フロントエンドからAPIを通じて画像をアップロードする機能を実装したかったのですが、
APIというとJSONペイロードでテキストデータをやり取りするイメージが強く、画像ファイルをアップロードできるのか分からなかったです。

調べてみたところ、API GatewayのService Integrationを使えば、Lambdaを介さずに直接S3バケットのオブジェクトを操作できそうでした。

本記事では、CloudFormationを使ってAPI Gateway経由でS3に画像ファイルをアップロード・ダウンロード・削除するREST APIの構築について記載します。

実際にリソースを構築した際のスクラップは下記になります。
https://zenn.dev/enumura/scraps/5b072f8cee04dd

アーキテクチャ

クライアント → API Gateway → S3 の構成です。
API Gatewayのサービス統合機能を使ってS3と統合しています。

Lambda関数を使わないシンプルな構成で、以下の操作ができます。

作成されるリソース

下記に記載したcfnテンプレートで作成されるものは次の通りです。

S3

  • バケット: 画像ファイル保存用(バージョニング有効)

IAM

  • ロール: API GatewayからS3へのアクセス権限

API Gateway

  • REST API: ファイル操作用API
  • リソース:
    • /files : 親パス
    • /files/{filename} : ファイル操作用パス({filename}は実際のファイル名)
  • メソッド: PUT、GET、DELETE
  • デプロイメント・ステージ: API公開設定

バイナリメディアタイプの設定

今回は、画像ファイルを処理するためBinaryMediaTypesの設定をしておきます。

BinaryMediaTypes:
  - image/jpeg
  - image/png
  - image/*
  - application/pdf

CloudFormationテンプレート

以下のテンプレートで必要なAWSリソースを一括作成します。

cfnテンプレート(yaml)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'テスト用:API Gateway経由でS3操作を行うためのリソース一式'

# パラメータ
Parameters:
  BucketPrefix:
    Type: String
    Default: hoge-handle-img-api
    Description: S3バケット名のプレフィックス
    
  ApiStageName:
    Type: String
    Default: hogetest
    Description: API Gatewayのステージ名

Resources:
  # S3バケット
  TestFileS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${BucketPrefix}-${AWS::AccountId}-${AWS::Region}'
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  # IAMロール(API GatewayがS3にアクセスするため)
  ApiGatewayS3Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-ApiGateway-S3-Role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: apigateway.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: S3AccessPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:PutObjectAcl
                  - s3:GetObject
                  - s3:DeleteObject
                Resource: !Sub '${TestFileS3Bucket.Arn}/*'

  # API Gateway REST API
  TestFileRestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub '${AWS::StackName}-FileAPI'
      Description: 'S3ファイル操作用のテストAPI'
      BinaryMediaTypes:  # 重要:バイナリファイル対応
        - image/jpeg
        - image/png
        - image/*
        - application/pdf

  # リソース /files
  FilesResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref TestFileRestApi
      ParentId: !GetAtt TestFileRestApi.RootResourceId
      PathPart: files

  # リソース /files/{filename}
  FileNameResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref TestFileRestApi
      ParentId: !Ref FilesResource
      PathPart: '{filename}'

  # PUT メソッド(アップロード)
  PutFileMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref TestFileRestApi
      ResourceId: !Ref FileNameResource
      HttpMethod: PUT
      AuthorizationType: NONE
      RequestParameters:
        method.request.path.filename: true
        method.request.header.Content-Type: false
      Integration:
        Type: AWS
        IntegrationHttpMethod: PUT
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:s3:path/{bucket}/{key}'
        Credentials: !GetAtt ApiGatewayS3Role.Arn
        RequestParameters:
          integration.request.path.bucket: !Sub "'${TestFileS3Bucket}'"
          integration.request.path.key: method.request.path.filename
          integration.request.header.Content-Type: method.request.header.Content-Type
        IntegrationResponses:
          - StatusCode: 200
      MethodResponses:
        - StatusCode: 200

  # GET メソッド(ダウンロード)
  GetFileMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref TestFileRestApi
      ResourceId: !Ref FileNameResource
      HttpMethod: GET
      AuthorizationType: NONE
      RequestParameters:
        method.request.path.filename: true
      Integration:
        Type: AWS
        IntegrationHttpMethod: GET
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:s3:path/{bucket}/{key}'
        Credentials: !GetAtt ApiGatewayS3Role.Arn
        RequestParameters:
          integration.request.path.bucket: !Sub "'${TestFileS3Bucket}'"
          integration.request.path.key: method.request.path.filename
        IntegrationResponses:
          - StatusCode: 200
            ResponseParameters:
              method.response.header.Content-Type: integration.response.header.Content-Type
      MethodResponses:
        - StatusCode: 200
          ResponseParameters:
            method.response.header.Content-Type: false

  # DELETE メソッド(削除)
  DeleteFileMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref TestFileRestApi
      ResourceId: !Ref FileNameResource
      HttpMethod: DELETE
      AuthorizationType: NONE
      RequestParameters:
        method.request.path.filename: true
      Integration:
        Type: AWS
        IntegrationHttpMethod: DELETE
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:s3:path/{bucket}/{key}'
        Credentials: !GetAtt ApiGatewayS3Role.Arn
        RequestParameters:
          integration.request.path.bucket: !Sub "'${TestFileS3Bucket}'"
          integration.request.path.key: method.request.path.filename
        IntegrationResponses:
          - StatusCode: 204
      MethodResponses:
        - StatusCode: 204

  # デプロイメント
  ApiDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn: 
      - PutFileMethod
      - GetFileMethod
      - DeleteFileMethod
    Properties:
      RestApiId: !Ref TestFileRestApi
      Description: 'テスト用デプロイメント'

  # ステージ
  ApiStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      RestApiId: !Ref TestFileRestApi
      DeploymentId: !Ref ApiDeployment
      StageName: !Ref ApiStageName
      Description: 'テスト用ステージ'
      MethodSettings:
        - ResourcePath: '/*'
          HttpMethod: '*'
          LoggingLevel: INFO
          DataTraceEnabled: true

# 出力
Outputs:
  S3BucketName:
    Description: '作成されたS3バケット名'
    Value: !Ref TestFileS3Bucket
    Export:
      Name: !Sub '${AWS::StackName}-S3Bucket'

  ApiGatewayURL:
    Description: 'API GatewayのベースURL'
    Value: !Sub 'https://${TestFileRestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiStageName}'
    Export:
      Name: !Sub '${AWS::StackName}-ApiURL'

  FileUploadURL:
    Description: 'ファイル操作用URL'
    Value: !Sub 'https://${TestFileRestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiStageName}/files/{filename}'

S3統合URIの設定

API GatewayからS3を直接呼び出すためのURIを指定します。

Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:s3:path/{bucket}/{key}'

動作確認

1. 画像ファイルのアップロード(PUT)

curl -X PUT \
  "https://[API_ID].execute-api.[REGION].amazonaws.com/hogetest/files/sample-image.jpg" \
  -H "Content-Type: image/jpeg" \
  --data-binary @sample-image.jpg

2. ファイルのダウンロード(GET)

curl -X GET \
  "https://[API_ID].execute-api.[REGION].amazonaws.com/hogetest/files/sample-image.jpg" \
  -o downloaded-test.jpg

実行結果

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  149k  100  149k    0     0   527k      0 --:--:-- --:--:-- --:--:--  528k

3. ファイルの削除(DELETE)

curl -X DELETE \
  "https://[API_ID].execute-api.[REGION].amazonaws.com/hogetest/files/sample-image.jpg"

ファイルサイズ制限の検証

API Gatewayにはペイロードサイズ10MBの制限があるようです。

下記は30MBのファイルでテストした結果です。

curl -X PUT \
  "https://[API_ID].execute-api.[REGION].amazonaws.com/hogetest/files/sample-large-file.json" \
  -H "Content-Type: application/json" \
  --data-binary @sample-large-file.json -v
送信ファイルサイズ
Content-Length: 34104782
エラーレスポンス
< HTTP/2 413 
< content-type: text; charset=utf-8
< content-length: 44

HTTP content length exceeded 10485760 bytes.

予想通り、10MB制限によりアップロードが拒否されました。

リソースのクリーンアップ

使用後は以下の手順でリソースを削除します。

  1. S3バケット内のファイルをすべて削除
  2. CloudFormationスタックを削除
  3. ステータスが DELETE_COMPLETE になることを確認

その他のアップロード方法

WebアプリケーションからS3にファイルをアップロードする方法は、今回の方法以外にもいくつかあります。

まとめ

API GatewayのService Integrationを使って、API経由のS3のオブジェクト操作をやってみました。

Lambda関数のコード管理やデプロイも必要なく、よりシンプルで実装・運用コストが低い構成だと思います。

一方で、10MBのペイロードサイズ制限があるので、大きなファイルを扱う場合はPre-signed URL(署名付きURL)など別の方法を検討する必要があると思いました。

Discussion