Closed6

AWS API Gateway + S3統合でLambdaを使わないファイル操作API構築

enumura1enumura1

やりたいこと・ゴール

背景

  • API経由でファイルをS3に画像ファイルをアップロード・ダウンロード・削除したい
  • API Gateway の Service Integration を使えばLambda不要らしい
イメージ
クライアント → API Gateway → S3

ゴール

  • API Gateway経由でS3バケットに直接ファイル操作
  • PUT(アップロード)、GET(ダウンロード)、DELETE(削除)の3操作
  • JPEGなどのバイナリファイルも正しく処理
  • CloudFormationでインフラをコード化
  • 10MBまでのファイル制限内での動作確認
enumura1enumura1

CloudFormationテンプレート

▼下記のテンプレートで作成されるもの

  • S3
    • バケット - 画像ファイル保存用
  • IAM
    • ロール - API Gateway用のS3アクセス権限
  • API Gateway
    • REST API - ファイル操作用API
    • APIリソース - /files, /files/{filename}パス
    • APIメソッド - PUT, GET, DELETE(各3個)
    • デプロイメント - API公開設定
    • ステージ - サンプルテスト環境
cfnテンプレート(yaml)
main.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
        - application/octet-stream

  # リソース /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}'
enumura1enumura1

curlコマンドで画像ファイルを操作

  • PUT: アップロード成功(レスポンスなし)
  • GET: ダウンロード成功(進捗表示あり、149kB転送)
  • DELETE: 削除成功(レスポンスなし)

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

curl -X PUT \
  "APIのエンドポイント/test/files/sample-image.jpg" \
  -H "Content-Type: image/jpeg" \
  --data-binary @sample-image.jpg

レスポンスなし = 正常

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

curl -X GET \
  "APIのエンドポイント/test/files/sample-image.jpg" \
  -o downloaded-test.jpg
結果
curl -X GET \
  "APIのエンドポイント/test/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

149kBのファイルが正常ダウンロード完了

3. ファイル削除(DELETE)

curl -X DELETE \
  "APIのエンドポイント/test/files/sample-image.jpg"

レスポンスなし = 正常

enumura1enumura1

30MBファイルでのテスト

Payload のクォータ10 MB 制限で失敗する想定。試しにやってみる。

curl -X PUT \
  "APIのエンドポイント/test/files/sample-large-file.json" \
  -H "Content-Type: application/json" \
  --data-binary @sample-large-file.json -v

送信されたファイルサイズ
約34MB(34,104,782バイト)と計測。

Content-Length: 34104782

結果のレスポンスは下記の通り(かなり省略)。

< HTTP/2 413 
< content-type: text; charset=utf-8
< content-length: 44

HTTP content length exceeded 10485760 bytes.

API Gatewayの10MB制限に引っかかってファイルをアップロードできないことを確認。

enumura1enumura1

後片付け

S3バケットにアップロードしたcfnのテンプレートを削除後、cfnのスタックを削除。

ステータス:DELETE_COMPLETE

cfnのスタックのステータスが上記のようになっている事を確認して素振りは終了。

このスクラップは1ヶ月前にクローズされました