【CloudFront】マルチオリジン設定時にリクエストパスがオリジンへ転送される問題を解消する

# 要約
- [Amazon CloudFront](https://aws.amazon.com/jp/cloudfront/)においてマルチオリジンを設定した場合、リクエストパスがオリジンへ転送されてしまう問題がある
- CloudFront Functionsをビューワーリクエストに設定することによって、パスの削除を行う。これにより、問題を解決する
- しかし、パスの削除によってキャッシュキーが競合する危険あり
- 同じくCloudFront Functionsによって削除したパスをクエリパラメータに追加する。これにより、問題を解決する

# マルチオリジン設定時にリクエストパスがオリジンへ転送される問題
## 問題について
[Amazon CloudFront](https://aws.amazon.com/jp/cloudfront/)では複数のオリジン(マルチオリジン)を設定することができます。
ビヘイビアにパスパターンを設定することで、「このパスにリクエストが来た時はオリジンA、あのパスにリクエストが来た時はオリジンB」といったように振り分けが可能です。

マルチオリジンを設定した際、**リクエストパスがオリジンへのリクエスト時にも転送されるという問題**が発生します。

例を示します。

オリジンサーバーとして以下の二つが存在するとします。
- `example1.com`
- `example2.com`

`サーバーのドメイン/ファイル名`へアクセスすることでファイルをダウンロードすることができます。
(例: `example1.com/sound.mp3`)

また、CloudFrontディストリビューションとして、以下が存在するとします。
- `foobar.cloudfront.net`

このディストリビューションには以下のようなビヘイビアが設定されています。
- パスパターン: `/example1/*`, オリジンサーバー: `example1.com`
  - `foobar.cloudfront.net/example1/*`へアクセスがあった場合、`example1.com`がオリジンとなる
- パスパターン: `/example2/*`, オリジンサーバー: `example2.com`
  - `foobar.cloudfront.net/example2/*`へアクセスがあった場合、`example2.com`がオリジンとなる

この時、クライアントから`https://foobar.cloudfront.net/example1/sound.mp3`へのリクエストを行います。
以下のようにリクエストが行われます。
```
クライアント(ビューワー)
|
| https://foobar.cloudfront.net/example1/sound.mp3
|
CloudFrontディストリビューション(foobar.cloudfront.net)
|
| https://example1.com/example1/sound.mp3
|                      ~~~~~~~~
オリジンサーバー(example1.com)
```

CloudFrontディストリビューションからオリジンサーバーへのリクエストURLに注目してください。
本来、オリジンサーバーに対してリクエストする際は`サーバーのドメイン/ファイル名`としたい訳です。
しかし、**クライアント→CloudFrontディストリビューションのリクエストパスが余計に転送**されてきています。

これにより`sound.mp3`を取得したいはずが、`example1/sound.mp3`に対する**意図しないリクエストが発生**してしまいます。

## 解決方法
ビューワーリクエストにCloudFront Functionsを設定することで解決します。
CloudFront Functionsにてリクエストパスを削除するという対応を行います。

例えば、以下のようなCloudFront Functionsを作成し、それぞれのビヘイビアのビューワーリクエストに設定します。
```
// example1.comに対応するビヘイビアに設定するFunction

// ここはKeyValueStoreを使って外側から変更できるようにしても良い
const pathToDelete = '/example1'

function handler(event) {
    const request = event.request;
    request.uri = request.uri.replace(pathToDelete, '/');
    return request;
}
```

```
// example2.comに対応するビヘイビアに設定するFunction

// ここはKeyValueStoreを使って外側から変更できるようにしても良い
const pathToDelete = '/example2' // example1.com版とはここが変わっているだけ

function handler(event) {
    const request = event.request;
    request.uri = request.uri.replace(pathToDelete, '/');
    return request;
}
```

ちなみに、CloudFront Functionsではなく、[Lambda@Edge](https://aws.amazon.com/jp/lambda/edge/)を利用することも可能です。
その際は、ビューワーリクエストではなくオリジンリクエストに設定します。
オリジンリクエストに設定した場合、後述の「キャッシュ競合の危険性」が発生しなくなるためです。
(今回CloudFront Functionsを利用しているのはコストが圧倒的に安いためです)

# キャッシュ競合の危険性
## 問題の内容
CloudFront Functionsを利用してパスの削除を行うと、**キャッシュが競合する危険**が生まれます。

`foobar.cloudfront.net/example1/sound.mp3`へアクセスした際、`sound.mp3`のキャッシュキーは`/sound.mp3`となります。
`/example1/sound.mp3`ではありません。なぜなら、ビューワーリクエストのCloudFront Functionsで`/example1`部分を削除してしまっているからです。

`foobar.cloudfront.net/example2/sound.mp3`へアクセスした際、`sound.mp3`のキャッシュキーは同じく`/sound.mp3`となります。

したがって、`foobar.cloudfront.net/example1/sound.mp3`と`foobar.cloudfront.net/example2/sound.mp3`のキャッシュが共通になってしまいます。

キャッシュが共通になってしまっても問題ない状況であれば良いのですが、それを避けたい場合、さらなる対処を講じる必要があります。

## 解決方法
CloudFront Functionsに処理を追加して、削除したパスをクエリパラメータとしてくっつけます。
クエリパラメータ名はなんでも良いですが、とりあえずここでは`deleted_path`にしておきます。

`foobar.cloudfront.net/example1/sound.mp3`
↓
`foobar.cloudfront.net/sound.mp3?deleted_path=/example1`
(`/example1`を削除してクエリパラメータをくっつける)

その上で、追加したクエリパラメータをキャッシュキーとして追加する必要があります。
ビヘイビアに設定してるキャッシュポリシーのキャッシュキー設定のクエリ文字列に、追加したクエリパラメータ名(ここでは`deleted_path`)を追加します。

CloudFront Functionsのコード例
```
const pathToDelete = '/example1';

function handler(event) {
    const request = event.request;
    
    // URIから指定されたパスを削除
    request.uri = request.uri.replace(pathToDelete, '/');
    
    // クエリパラメータを追加
    const queryString = request.querystring || '';
    const newQueryString = queryString 
        ? `deleted_path=${pathToDelete}&${queryString}`
        : `deleted_path=${pathToDelete}`;
    
    request.querystring = newQueryString;
    
    return request;
}
```

ちなみに、リクエスト時に既にクエリパラメータを付加している場合は`deleted_path`を一番最初のクエリパラメータとしてくっつけます。
`foobar.cloudfront.net/example1/sound.mp3?param=val`
↓
`foobar.cloudfront.net/sound.mp3?deleted_path=/example1&param=val`

なぜかというと、キャッシュ削除のことを考えるとその方が都合が良いからです。
CloudFrontにおけるキャッシュ削除時には`*`(ワイルドカード)を利用することができますが、一番最後にしか指定することができません。
`deleted_path`が最初に来ていることによって、以下のようにキャッシュを削除することができます。
`/sound.mp3?deleted_path=/example1&*`
(`/example1`配下のキャッシュを全て削除する)

# 最後に
CloudFrontは機能としてはシンプルに保ち、それ以上のことをしたければCloudFront FunctionsやLambda@Edgeで補う、という方針だとは思いますが、流石にこれは、、、という気持ちになりました。