LambdaでHTTPSリクエストで画像をアップロードするエンドポイントを作ってみた

クライアントからHTTPSリクエストでサーバーに画像をアップロードして、いろいろ処理してレスポンスを返す、というのをAWS Lambdaでやってみたかったので調べてみました。[Lambdaのチュートリアル](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/getting-started.html)をなぞってたつもりがうまく動かなかったりしていろいろ罠があったりしましたが、なんとか目指していたものができることが確認できたので、その記録です。

## やりたいこと

* HTTPS POSTリクエストで画像をアップロードしてLambdaで受け取り、S3にアップロードしたい。

cURL的には以下のような感じです。

```
curl -H "Content-Type: image/png" --data-binary "@test.png" -X POST https://XXXXX.execute-api.ap-northeast-1.amazonaws.com/test/myresource
```

※S3にアップロードするだけLambdaを使わなくても直接アップロードすることが可能ですが、アップロードする以外にもいろいろと処理を追加していく予定なので最初からLambdaを通す方法を考えていました。あと、アップロードの完了およびその後の後処理まで完了した段階でHTTPSレスポンスを返したかったりもします。


## システム構成

AWS Lambda自体にはHTTP(S)リクエストで処理を呼び出す機能はありません。これを行うためにはAmazon API Gatewayを使ってエンドポイントを作り、エンドポイントとLambda関数を結びつける設定をする必要があります。この設定の[チュートリアル](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/getting-started.html)が用意されているので、まず一読することをおすすめします。


## S3バケットを作る

画像のアップロード先として使用するS3バケットを作っておいてください。


## Lambda関数を作る

それではまず、HTTPSリクエストが来ることを想定して、リクエストボディに設定されたデータをS3にアップロードするという、簡単なLambda関数を作ります。

```js:index.js
const AWS = require('aws-sdk')
const s3 = new AWS.S3()

const handler = (event, context, callback) => {

  // リクエストボディに設定された画像データはBase64エンコードされているので、デコードする
  let requestBody = Buffer.from(event.body, 'base64')

  s3.putObject({
    Body: requestBody,
    Bucket: 'my-bucket', // 先ほど作成したS3バケット名に合わせる
    ContentType: 'image/png',
    Key: 'test.png'
  }).promise()
    // S3へのアップロードが完了したら完了レスポンス
    .then((result) => {
      callback(null, {
        body: JSON.stringify(result),
        statusCode: 200
      })
    })
    // S3へのアップロードに失敗したら、エラーレスポンス
    .catch((err) => {
      callback(err, {
        body: JSON.stringify(err),
        statusCode: 500
      })
    })
}

module.exports = { handler }
```

特に外部ライブラリを使っていないので、ブラウザ上で直接コードを編集してLambda関数をすぐに作成できます。

Lambdaに先ほど作成したS3バケットにアクセスする権限を付けておかないと処理に失敗します。とりあえずS3FullAccessのような権限を付けるようにしてください(業務用の本番環境で運用する場合などはもっときちんと必要最低限の権限だけ付けるようにしたほうがいいですけど)。


## API Gatewayを設定する

次に、上記Lambda関数を呼び出すためのエンドポイントをAPI Gatewayで作成します。Lambda関数を作る時に一緒に作ることもできますが、どういう設定をすれば正しく動くのかを把握するために最初は手動でそれぞれ作成することをおすすめします。

[Lambda関数を公開するためのAPIを作成する](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/getting-started.html)ドキュメントを参照しつつ、APIの作成、リソースの作成、メソッドの作成を行ってください。メソッドはなんでもいいのですが、今回やりたいことはリクエストボディでデータを送ることなので、POSTが良いと思います。

このチュートリアルには書いていませんが、メソッドの作成をする際に**Lambda プロキシ統合の使用にチェックを入れてください。**(チェックが入っているかいないかによって、Lambda関数でコールバックすべきデータの形式が変わります。上記のLambdaの例はLambdaプロキシ統合を使用する前提の形式です。)

※ API Gatewayではリクエストに認証を要求させることで第三者からの不正アクセスから保護することができますが、テスト用にとりあえず試すだけなら認証なしで作るのが問題が起きにくくて良いです。認証なしで作ったエンドポイントは不要になったらさっさと削除しましょう。

そして、ここがチュートリアルには含まれていない大事なポイントです。左のメニューにあるバイナリサポートをクリックしてください。
![test.png](https://qiita-image-store.s3.amazonaws.com/0/56668/ddaf232c-5cce-f93f-6e9d-6f5bfce1a602.png)

そして編集ボタンをクリックして、`image/png`のバイナリメディアタイプを追加します。
![test2.PNG](https://qiita-image-store.s3.amazonaws.com/0/56668/56da8b1b-04f0-b45b-cb02-57d5583e44b7.png)

これで、HTTPSリクエストのヘッダーに`Content-Type: image/png`が設定されていると**リクエストボディの内容がBase64エンコードされてLambdaに渡される**ようになります。これを設定しないとHTTPSリクエストのボディがバイナリデータのときに正しくLambda関数に渡されず、壊れたデータがS3にアップロードされてしまいます。
なお、バイナリメディアタイプの設定をしていなくても、テキストデータなら正しく処理されます。

ここまでできたらAPI GatewayでAPIをデプロイします。


## アップロードテスト

それでは実際にHTTPS POSTリクエストをしてみましょう。最後のURLの部分は実際にデプロイされているAPI GatewayのURLに書き換えてください。

```
curl -H "Content-Type: image/png" --data-binary "@test.png" -X POST https://XXXXX.execute-api.ap-northeast-1.amazonaws.com/test/myresource
```

S3のバケットにtest.pngの内容がアップロードされていれば成功です！