serverlessを使ってLambdaにmultipart/form-dataでバイナリデータをアップロードする

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でマルチパートデータをバイナリにする

```serverless.yml
custom:
  ...省略
  apigwBinary:
    types:
      - multipart/form-data
...省略
plugins:
  - serverless-apigw-binary
```

## serverless.yml
serverless.ymlの記載内容をまとめると以下のようになるかと思います。

- S3バケットの設定
- Lambdaの設定
- APIGatewayの設定

```serverless.yml
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で指定した値がバイナリメディアタイプとなります。

```serverless.yml
custom:
  ...省略
  apigwBinary:
    types:
      - multipart/form-data
...省略
plugins:
  - serverless-apigw-binary
```

![apigateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140599/90f369fe-5d74-876e-c229-5e61d77434d7.png)

## Lambda実装
Labmda側は、通常のLambda統合のリクエストでデータを取得できます。
この際、**event.bodyにはbase64エンコードされたmultipart/form-dataが格納されています。**
つまり、そのままでは利用できないので受け取ったevent.bodyはデコードしたあとmultipartの処理を行います。


```index.ts
'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欄に表示されるので、メモっておきます。

```bash
$ 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で作りました。

``` index.html
<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にアップロードされるはずです

```bash
$ sls deploy
```

だいぶ端折りましたがこんな感じで動くかと思います。