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

やりたいこと・ゴール
背景
- API経由でファイルをS3に画像ファイルをアップロード・ダウンロード・削除したい
- API Gateway の Service Integration を使えばLambda不要らしい
イメージ
クライアント → API Gateway → S3
ゴール
- API Gateway経由でS3バケットに直接ファイル操作
- PUT(アップロード)、GET(ダウンロード)、DELETE(削除)の3操作
- JPEGなどのバイナリファイルも正しく処理
- CloudFormationでインフラをコード化
- 10MBまでのファイル制限内での動作確認

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}'

Payload sizeのクォータが10 MB
と記載あり。
参考:
- Quotas for configuring and running an HTTP API in API Gateway

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"
レスポンスなし = 正常

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制限に引っかかってファイルをアップロードできないことを確認。

後片付け
S3バケットにアップロードしたcfnのテンプレートを削除後、cfnのスタックを削除。
ステータス:DELETE_COMPLETE
cfnのスタックのステータスが上記のようになっている事を確認して素振りは終了。
このスクラップは1ヶ月前にクローズされました