AWS - Presigned/Signedを活用したAPI負荷対策

## 序

API開発において、ユーザーにファイルをアップロードしてもらいたい、もしくはダウンロードしてもらいたい、というシチュエーションは多々あると思われる。

概ね以下のような構成になっているだろう。

```mermaid
sequenceDiagram
    actor U as User
    participant A as API
    U->>+A: PUT /object.dat
    A-->>-U: 201 Created
    U->>+A: GET /object.dat
    A-->>-U: 200 OK
```

取り扱うファイルサイズが非常に大きい、処理数が膨大である、などの場合は、処理が終わるまでAPIのスロットを占有し続けることになるし、負荷も発生し続けることになる。

Amazon S3の[Presigned URL](https://docs.aws.amazon.com/en_us/AmazonS3/latest/userguide/using-presigned-url.html)を使うことによって、この処理コストをS3に肩代わりしてもらうことができる。

## Presigned URLとは

Amazon S3では、Presigned URLを発行することができる。

* S3バケット内のオブジェクトを指すURLに、指定の有効期間だけ指定のS3アクション(`s3:PutObject` 等)を実行する許可を与えるような認可情報が付与されたもの
* 認可情報はURLの`QUERY_STRING`として付与されている
  * バケット名
    * `example`
  * オブジェクト名
    * `object.dat`
  * Presigned URL
    * `https://example.s3.ap-northeast-1.amazonaws.com/object.dat?X=Y&Y=Z&...(認可情報クエリー)...`
* 当然ながら、Presigned URLを発行できるのは発行権限を持っているIAMユーザー／ロールだけ

Presigned URLは、 **入手していれば誰でも(※後述)** 指定オブジェクトに対して認可されているアクションを実行することができる。

```text
PUT /object.dat?X=Y&Y=Z&...(認可情報クエリー)... HTTP/1.1
Host: example.s3.ap-northeast-1.amazonaws.com
Content-Type: application/octet-stream
Content-Length: 128
Content-MD5: xMpCOKC5I4INzFCab3WEmw==
Expect: 100-continue
```

## ファイルアップロードへの応用

以下のようにAPIを設計する。

1. ユーザーはAPIに対し、「アップロード申請」を実行する
2. APIはユーザーに対し、`s3:PutObject`のみを認可したPresigned URLを返す
3. ユーザーはS3 (Presigned URL)に対し、ファイルアップロードを実行する
4. S3はユーザーに対し、アップロード合否を返す

```mermaid
sequenceDiagram
    actor U as User
    participant A as API
    participant S as S3
    U->>+A: POST /uploads?key=object.dat
    A-->>-U: 200 OK with Presigned URL
    U->>+S: PUT /object.dat?X=Y&Y=Z&...
    S-->>-U: 200 OK
```

APIは、「アップロード申請」は取り回しているものの、「巨大ファイルバイナリそのもの」を取り回しているわけではないため、ユーザーリクエストに対するサーバー処理コストが少なくて済む。

### デメリットと、その対応

前述の通り、Presigned URLは **入手していれば誰でも** アクションを実行することができる。

APIのケースで言うと、「URLが漏洩すると、指定パスに誰でもどのようなファイルでもPUTすることができる」ということになる。

この問題を回避するため、「アップロード申請」時のレスポンスPresigned URLにおいては、以下を踏まえて認可情報を組み立てる。

* 有効期限を短く設定する
* オブジェクトのファイルサイズ(Content-Length)を指定する
* オブジェクトのMD5ハッシュ値(Content-MD5)を指定する
* オブジェクトのファイル種別(Content-Type)を指定する

これにより、以下のような状態を実現できる。

* Presigned URLは、期限内のみ有効である
  * →仮に漏洩したとしても、使用できるのはごく短い間だけ
* アップロードが成功するのは、指定のファイルサイズとMD5ハッシュ値を持つデータのみ
  * →仮に漏洩したとしても、指定のデータ以外のものをアップロードすることが困難
    * ※ファイルサイズに関しては、省略する方が予測困難になり良い、という見方もある
* アップロードが成功するのは、指定のファイル種別が設定されたデータのみ
  * →仮に漏洩したとしても、指定のメタデータしかアップロードできない

また、参考として以下のような問答も考えられる。
開発要件に応じて調整することになるであろう。

* 限られたユーザーにのみ「アップロード申請」を許可したい等
  * → APIとして通常通り認証認可を行う
* 有効期限が短い場合に、アップロードが完了できない等
  * →リクエストにExpectヘッダーを含めることで認可を済ませてしまう
* ファイルサイズとMD5ハッシュ値からオブジェクトを逆算できる等
  * →暗号化済みのファイルを取り扱う

## ファイルダウンロードへの応用

以下のようにAPIを設計する。
基本的にはアップロード時と同じような流れとなる。

1. ユーザーはAPIに対し、「ダウンロード申請」を実行する
2. APIはユーザーに対し、`s3:GetObject`のみを認可したPresigned URLを返す
3. ユーザーはS3 (Presigned URL)に対し、ファイルダウンロードを実行する
4. S3はユーザーに対し、オブジェクトを返す

```mermaid
sequenceDiagram
    actor U as User
    participant A as API
    participant S as S3
    U->>+A: GET /downloads?key=object.dat
    A-->>-U: 302 Found with Presigned URL
    U->>+S: GET /object.dat?X=Y&Y=Z&...
    S-->>-U: 200 OK
```

APIは、「ダウンロード申請」は取り回しているものの、「巨大ファイルバイナリそのもの」を取り回しているわけではないため、ユーザーリクエストに対するサーバー処理コストが少なくて済む。

Presigned URLのデメリット対応に関してもアップロード時と同様で、有効期限をなるべく短くするということになる。

## さらなるファイルダウンロードへの応用

### S3データ転送費用への対策

これまでの手法で、APIの負荷・処理コストを抑えた、Amazon S3経由でのアップロード／ダウンロードが可能になった。

しかし、S3からオブジェクトをダウンロードする行為により、S3からインターネットへのデータ転送費用がかかることになる。ダウンロード数が増えるにつれ、転送費用も懸念材料となる。

対策として、ユーザーアクセスとS3の間にAmazon CloudFrontを挟む。

CloudFrontはCDNであることは当然ながら、詳細は違えど似たような機能として、[Signed URL](https://docs.aws.amazon.com/en_us/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html)機能を取り扱えるため、これまでと同じような動作イメージのまま展開できる。

```mermaid
sequenceDiagram
    actor U as User
    participant A as API
    participant C as CloudFront
    participant S as S3
    U->>+A: GET /downloads?key=object.dat
    A-->>-U: 302 Found with Signed URL
    U->>+C: GET /object.dat?X=Y&Y=Z&...
        alt Cached
            C->>C: GET from Caches
        else Not Cached
            C->>+S: GET /object.dat
            S-->>-C: 200 OK
        end
    C-->>-U: 200 OK
```

### 申請受付API負荷への対策

これまでの手法で、ダウンロード処理の負荷・コストそのものはS3やCloudFrontに移譲することができた。

しかし、ダウンロード申請の受付時には、APIレベルでの認証認可処理や(Pre)signed URLの発行処理を行っているため、申請アクセス数が増えてくると申請受付APIの処理負荷すらも懸念材料となる。

対策としては、CloudFrontによる[Signed Cookies](https://docs.aws.amazon.com/en_us/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-cookies.html)を検討すると良い。
Signed CookiesはSigned URLと似た動作イメージだが、以下のような違いがある。

* 認可情報がCookieとして引き渡される
* 「指定パス以下のオブジェクト全て」という形で認可を設定することもできる

これを利用することで「ダウンロード申請」回数を減らすことができる。

```mermaid
sequenceDiagram
    actor U as User
    participant A as API
    participant C as CloudFront
    U->>+A: POST /downloads?key=foo
    A-->>-U: 200 OK with Signed Cookies for /foo/*
    U->>+C: GET /foo/object1.dat with Signed Cookies
        C-->>C: Somehow
    C-->>-U: 200 OK
    U->>+C: GET /foo/object2.dat with Signed Cookies
        C-->>C: Somehow
    C-->>-U: 200 OK
    U->>+A: POST /downloads?key=bar
    A-->>-U: 200 OK with Signed Cookies for /bar/*
    U->>+C: GET /bar/object1.dat with Signed Cookies
        C-->>C: Somehow
    C-->>-U: 200 OK
    U->>+C: GET /bar/object2.dat with Signed Cookies
        C-->>C: Somehow
    C-->>-U: 200 OK
    U->>+C: GET /bar/object3.dat with Signed Cookies
        C-->>C: Somehow
    C-->>-U: 200 OK
```

## まとめ

AWSサービスであるAmazon S3とAmazon CloudFront、及びその機能である(Pre)signed URL／Signed Cookiesを活用することで、ファイルのアップロードやダウンロード時のAPI処理負荷(処理コスト)を、セキュリティやコスト感も考慮しつつAWS側に移譲することができる。