Serverless な構成での画像アップロードはどうするのがいいの？



## TL;DR
署名付き URL を発行し、その URL を使いフロントから S3 に直接アップロードしてもらうのがオススメです。

**フロー**
1. 認証が済んでいるユーザに対して、署名付き URL を発行
2. 署名付き URL を使い S3 にアップロード

**概要図**
![flow.png](https://qiita-image-store.s3.amazonaws.com/0/52677/faf4f822-a9ca-39c4-7468-58b192fd51d1.png)


**ソース**

```python:署名付きURLを発行
import boto3
from botocore.config import Config

# 署名付き URL を発行
def get_signature_url(bucket, key, image_size):
    s3_cli = boto3.client('s3', config=Config(signature_version='s3v4'), region_name='ap-northeast-1')
    return s3_cli.generate_presigned_url(
        ClientMethod='put_object',
        Params={'Bucket': bucket, 'Key': key, 'ContentLength': image_size},
        ExpiresIn=300,
        HttpMethod='PUT'
    )

# パラメータ設定
bucket = 'piyopiyo-bucket'    # 任意の S3バケット名
key = 'path/to/hoge.png'      # 任意の S3オブジェクトキー名
image_size = 1000             # 対象ファイルのサイズ

# 実行
signature_url = get_signature_url(bucket, key, image_size)
print(signature_url)
```

```sh:署名付きURLを使いS３にアップロード
curl "署名付きURL" --upload-file /path/to/hoge.png
```

## 画像ファイルのアップロードを検証した経緯
現在、開発している [ブログサービス（ALIS）](https://alis.to) では下記のように直接 Lambda を通して画像ファイルをアップロードしています。
![simple.png](https://qiita-image-store.s3.amazonaws.com/0/52677/7dd58195-2edc-6e90-88ca-040acbcdf130.png)


ただ、上記構成の場合、実装はシンプルですが [Lambda の payload は制限（６MB）](https://docs.aws.amazon.com/lambda/latest/dg/limits.html)があるため、6MB より大きなのデータを扱う事ができません（正確には画像ファイルをアップロードする際は Base64 エンコードをしている都合上 4.5MB 程度が限界）。より大きな画像ファイルを扱いたい要件がでてきたため、Lambda を通さないパターンでの検証を行いました。


## 気をつけたこと
- ユーザがアップロードできるオブジェクトキーを制限できること
- アップロードできるファイルサイズを制限できること

これらはソースコードに記載してある通りになりますが [generate_presigned_url](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.generate_presigned_url) の ```Params``` で対応できました。
また、今回の場合、S3 へは put_object で処理しているため、 [Request Headers に設定できるもの](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html#RESTObjectPUT-requests-request-headers) であれば制限等をかけれるかと思います（試してはしないです、、）。


## ハマッたところ

[generate_presigned_url](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.generate_presigned_url) を利用した際に、 ```ContentLength``` を指定してもファイルサイズの制限が効かない事象が発生しました。 原因は署名バージョンの指定ミスで、東京リージョンの場合、デフォルトは **署名バージョン2** であり、
指定すべき **署名バージョン4** が設定されていなかったためです。

このため、東京リージョンを利用する場合は、下記のように Config にて署名バージョンを明示的に設定する必要がありました。

```python
s3_cli = boto3.client('s3', config=Config(signature_version='s3v4'), region_name='ap-northeast-1')
```



## その他のファイルアップロード方法


上記以外にも、[generate_presigned_url](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.generate_presigned_url) を利用する代わりに [generate_presigned_post] (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.generate_presigned_post) を利用する方法があります。
この方法の場合、[POST Policy](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html#sigv4-PolicyConditions) が利用できるため、 ```content-length-range``` と言った、PUT のリクエスト（generate_presigned_url を利用したケース）では実現できない制限の付与が可能です。

## まとめ
generate_presigned_url を利用するか generate_presigned_post を利用するかは、制限したい内容で決めるのが良いかもしれません。特に制限したいことで違いがなければ、フロントの実装がシンプルになる generate_presigned_url がおすすめです。