serverlessを使ってLambdaをデプロイする際、Lambda統合の時にバイナリデータをどうやって送るのか地味にハマったのでメモとして残しておきます。
(たぶんうちの若い子達がココ見るはず..)
serverless便利ですね。コマンド一発でデプロイから各種AWSリソースをいい感じにセットアップしてくれます。いらなくなったら同じくコマンド一発でまるっと削除してくれます。これを使わない手はないですね。
TL;DR
- serverless.ymlのcustomキー配下にapiBinaryでバイナリメディアタイプを指定する
- serverless.ymlのpluginsキー配下にserverless-apigw-binaryを追加する
- Lambdaに送られてくるリクエストはbase64エンコードされたmultipart/form-dataなのでデコードしてaws-lambda-multipart-parserでマルチパートデータをバイナリにする
custom:
...省略
apigwBinary:
types:
- multipart/form-data
...省略
plugins:
- serverless-apigw-binary
serverless.yml
serverless.ymlの記載内容をまとめると以下のようになるかと思います。
- S3バケットの設定
- Lambdaの設定
- APIGatewayの設定
service:
name: FileUploadTest
custom:
webpack:
webpackConfig: ./webpack.config.js
includeModules: true
apigwBinary:
types:
- multipart/form-data # ← バイナリメディアタイプの指定
webSiteName: jp.co.onewedge.test.fileuploadtest
s3Sync:
- bucketName: ${self:custom.webSiteName}
localDir: static # ← プロジェクトのstaticフォルダ配下のファイルをS3にアップロード
# Add the serverless-webpack plugin
plugins:
- serverless-webpack
- serverless-apigw-binary # ← APIGatewayのバイナリメディアタイプでアップロードするために必要
- serverless-s3-sync # ← S3に静的ファイルをアップロードするために必要
provider:
name: aws
runtime: nodejs12.x
region: us-east-1
endpointType: REGIONAL
apiGateway:
minimumCompressionSize: 1024
environment:
AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1
functions:
fileUpload:
handler: index.handler
events:
- http:
method: post
path: doUpload
cors: false
resources:
Resources: # ←S3バケットでHTMLを配信するための設定
StaticSite:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:custom.webSiteName}
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
StaticSiteS3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: StaticSite
PolicyDocument:
Statement:
- Sid: PublicReadGetObject
Effect: Allow
Principal: "*"
Action:
- s3:GetObject
Resource:
Fn::Join: ["", ["arn:aws:s3:::",{"Ref": "StaticSite"},"/*"]]
multipart/form-dataでバイナリデータをアップロードする場合、キモとなるのはAWSコンソールのバイナリメディアタイプとなります。serverlessを利用する場合はserverless-apigw-binaryプラグインの導入と、customキー配下apigwBinaryキーの指定となります。このtypesで指定した値がバイナリメディアタイプとなります。
custom:
...省略
apigwBinary:
types:
- multipart/form-data
...省略
plugins:
- serverless-apigw-binary
Lambda実装
Labmda側は、通常のLambda統合のリクエストでデータを取得できます。
この際、event.bodyにはbase64エンコードされたmultipart/form-dataが格納されています。
つまり、そのままでは利用できないので受け取ったevent.bodyはデコードしたあとmultipartの処理を行います。
'use strict';
import { APIGatewayProxyHandler, APIGatewayProxyEvent, Context} from 'aws-lambda';
import multipart from 'aws-lambda-multipart-parser';
import { S3 } from 'aws-sdk';
import 'source-map-support/register';
export const handler: APIGatewayProxyHandler = async (event:APIGatewayProxyEvent, _context:Context) => {
// 受け取ったevent.bodyがbase64エンコードされているのでデコード
const event2:APIGatewayProxyEvent = event;
event2.body = Buffer.from(event.body, 'base64').toString('binary');
// multipart/form-dataをパースする
const multipartBuffer = multipart.parse(event2, true);
// クライアント側で特に指定なき場合、アップロードされたファイルはfile.contentとして取り出せる
// ここではアップロードされたファイルをS3に保管します
const s3 = new S3();
const params:S3.Types.PutObjectRequest = {
Bucket:'jp.co.onewedge.test.fileuploadtest',
Key: '保存ファイル名',
Body: multipartBuffer.file.content
};
await s3.putObject(params).promise();
// ... 省略
}
デプロイ
ここまでできたらコマンドラインからいったんデプロイしましょう。
デプロイするとAPIGatewayのエンドポイントがコンソールに表示されたログの中のendpoints欄に表示されるので、メモっておきます。
$ sls deploy
...省略
Service Information
service: FileUploadTest
stage: dev
region: us-east-1
stack: FileUploadTest-dev
resources: xx
api keys:
None
endpoints:
POST - https://xxxx.execute-api.us-east-1.amazonaws.com/dev/doUpload # ←これです
functions:
fileUpload: fileUpload-dev-doUpload
layers:
None
...省略
HTML作成
HTMLを作成し、先ほどメモったAPIのエンドポイントに向けてファイルをPostするようにしてください。今回はVue.jsとAxiosで作りました。
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<form >
<input type="file" id="file" ref="file" @change="handledUploadFile" />
<button type="button" @click="onClickUpload">アップロード</button>
</form>
</div>
<script type="text/javascript">
const v = new Vue({
el: '#app',
data: function(){
return {
file:''
}
},
methods:{
handledUploadFile: function(){
this.file = this.$refs.file.files[0];
},
onClickUpload: function(){
const formData = new FormData();
formData.append('file', this.file);
axios.post('メモったエンドポイントURL',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
).then(function(data){
console.log(`SUCCESS!!:${data}`);
}).catch(function(err){
console.log(`FAILURE!!${err}`);
});
}
}
})
</script>
</body>
</html>
作成したHTMLはstaticディレクトリに配置します。
配置したら再度デプロイしましょう。今度は作成したHTMLがS3にアップロードされるはずです
$ sls deploy
だいぶ端折りましたがこんな感じで動くかと思います。