API Gateway + S3のサービス統合でファイル操作REST APIを構築
はじめに
フロントエンドからAPIを通じて画像をアップロードする機能を実装したかったのですが、
APIというとJSONペイロードでテキストデータをやり取りするイメージが強く、画像ファイルをアップロードできるのか分からなかったです。
調べてみたところ、API GatewayのService Integrationを使えば、Lambdaを介さずに直接S3バケットのオブジェクトを操作できそうでした。
本記事では、CloudFormationを使ってAPI Gateway経由でS3に画像ファイルをアップロード・ダウンロード・削除するREST APIの構築について記載します。
実際にリソースを構築した際のスクラップは下記になります。
アーキテクチャ
クライアント → API Gateway → S3 の構成です。
API Gatewayのサービス統合機能を使ってS3と統合しています。
Lambda関数を使わないシンプルな構成で、以下の操作ができます。
-
PUT: ファイルアップロード、更新
-
GET: ファイルダウンロード
-
DELETE: ファイル削除
-
参考
作成されるリソース
下記に記載した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制限によりアップロードが拒否されました。
リソースのクリーンアップ
使用後は以下の手順でリソースを削除します。
- S3バケット内のファイルをすべて削除
- CloudFormationスタックを削除
- ステータスが
DELETE_COMPLETE
になることを確認
その他のアップロード方法
WebアプリケーションからS3にファイルをアップロードする方法は、今回の方法以外にもいくつかあります。
- AWS SDK for JavaScript:クライアント側から直接S3にアップロード(API Gatewayは介さないもの)
- Pre-signed URL:バックエンドで一時的なアップロード用URLを生成し、クライアントから直接S3へアップロード
まとめ
API GatewayのService Integrationを使って、API経由のS3のオブジェクト操作をやってみました。
Lambda関数のコード管理やデプロイも必要なく、よりシンプルで実装・運用コストが低い構成だと思います。
一方で、10MBのペイロードサイズ制限があるので、大きなファイルを扱う場合はPre-signed URL(署名付きURL)など別の方法を検討する必要があると思いました。
Discussion