SPA+CloudFrontマルチオリジン構成で「/」以外へのアクセスをルーティングする

# やりたいこと

AWS CloudFront ではリクエストパスのパターンに応じて、１つのドメインから複数のオリジンにアクセスを振り分けることができます。
以下のように、S3 でフロントエンドの SPA を配信し、バックエンドを API Gateway で提供するようなマルチオリジン構成を考えます。
![multi-origin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98903/4381bfb4-bd4b-c166-4786-856ed85c6b7b.png)

# SPA を CloudFront + S3 で配信する時の問題点

SPA を CloudFront + S3 で配信する時に問題になるのが、ルートパス ("/") 以外の URL に直接アクセスしたときに 403 エラーが発生するという現象です。
例えば `https://example.com/page1` という URL にアクセスしたとき、実際に S3 上に `/page1` というオブジェクトが存在しないため、403 エラーが発生します。
![４０３error.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98903/11cffdc8-8751-fde9-dea7-27e6a3e675be.jpeg)

## よくある解消法（カスタムエラーレスポンス）

よく紹介されている解消法として、CloudFront でカスタムエラーレスポンスを設定することでこの問題を回避することができます。
![custom-error-response.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98903/50458607-4c23-2aa4-f239-376a1efda5d8.jpeg)
上記の設定は「オリジンから 403 or 404 が返却されたら、代わりに `/index.html` に転送するよ」という意味になります。
すなわち `https://example.com/page1` にアクセスしても実際には `https://example.com/index.html` の資材を返却してくれるようになります。

## マルチオリジン環境でのカスタムエラーレスポンスの問題点

フロントエンドの配信だけであれば上記の対策方法で問題ないのですが、マルチオリジン構成では１つ困ったことが生じます。
それは「**カスタムエラーレスポンスはオリジンごとに設定できない**」という点です。

バックエンド側で 403 エラーが発生したとしても、CloudFront ではカスタムエラーレスポンスに従って処理を行なってしまうので、ブラウザには `200 /index.html` がレスポンスとして返却されてしまいます。
![multiorigin-problem.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98903/5e9d4feb-53dd-d97b-17ac-16e67b176bb3.png)
ブラウザ側でエラーハンドリングを実装したくても、CloudFront によってエラーレスポンスが書き換えられてしまうため、機能しません。

# 解決策

[CloudFront Functions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html) を利用します。CloudFront Functions は、CloudFront を通過するあらゆるリクエストとレスポンスに簡単な処理を挟むことができます。
今回は　AWS の公式ドキュメントでも紹介されている方法を利用します。

[Add index.html to request URLs that don’t include a file name](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example-function-add-index.html)
> This function can be useful for single page applications or statically generated websites that are hosted in an Amazon S3 bucket.

CloudFront > Functions から、以下のような Function を作成しましょう。コードは、上記 AWS 公式ドキュメントで紹介されているコードをそのまま貼り付ければ OK です。
![Function.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98903/45a2c73b-d454-7abb-c1d4-d91ab2d7a700.jpeg)

このコードでは、リクエスト URL にファイル名や拡張子を含まない場合、オリジンに対してはその直下の `/index.html` へアクセスするようにリクエストを書き換えています。

Function 作成後、Publish タブから `Publish function` を行うことで、CloudFront ディストリビューションと紐づけることが出来るようになります。
CloudFront > Distributions > {Distribution ID} > Behaviours から S3 向けのビヘイビアを編集し、Viewer request に作成した Function を紐付けましょう。
![associations.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98903/14882185-f1da-9f40-70b4-24b59111d9fe.jpeg)

ここで大事なのは、Function はカスタムエラーレスポンスと違ってディストリビューション毎ではなく**ビヘイビア毎に設定できる**点です。以上の設定により、バックエンドへのリクエストには影響を与えずに、SPA へのルートパス以外へのアクセスを正常に行えるようになりました。

# CloudFront Functions を利用する上での Tips

利用していて一番驚いた点は、[ランタイム環境](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-features.html)です。`const` や `let` も使用することができません。
> The CloudFront Functions JavaScript runtime environment is compliant with ECMAScript (ES) version 5.1 and also supports some features of ES versions 6 through 9.

その分非常に軽量・高速な処理が可能といった感じです。 Lambda@Edge でも CloudFront Functions と似たようなことが実現できていましたが、環境や制約が異なるため、Lambda@Edge から移植する際には注意が必要です。使い分けについては[こちら](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/edge-functions.html)を参照しましょう。

独自に Function をカスタマイズしたい場合は、テスト機能を活用しましょう。実際にデプロイを行う前に、リクエストやレスポンスをエミュレートしてテストすることができます。
![test-function.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98903/1655789e-539e-dfea-f16b-9a379c6d418a.jpeg)

# さいごに

CloudFront Functions を利用することで、今回紹介したリクエストパスの書き換え以外にも様々な処理を挟むことができます。公式ドキュメントでも色々な例が紹介されているので、ぜひ参考にしてください。
[Customizing at the edge with CloudFront Functions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-example-code.html)
例えば以下のようなことが簡単に実現できます。

- CORS ヘッダをリクエストに追加
- レスポンスにセキュリティヘッダを追加する