AWS CloudFront FunctionsでSPAのルーティング処理

緊急事態宣言で暇を持て余していた2021年のGWに、AWSからナイスな機能がリリースされました。
その名も「CloudFront Functions」。CloudFrontでクライアントからのアクセス時にJavaScriptで書かれた処理をかますことができる機能です。
今回は、このCloudFront Functionsを使って、SPA特有の「すべてのアクセスをindex.htmlに向ける」処理を書いてみましたのでご紹介します。

# CloudFront Functionsとは？

https://aws.amazon.com/jp/about-aws/whats-new/2021/05/cloudfront-functions/
https://aws.amazon.com/jp/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/
詳しくは以上のAWS公式サイトをご覧いただければと思いますが、要約すると「CloudFrontがリクエストをOriginに転送する前に、1ms以下の実行時間の比較的軽い処理を挟むことができる」という機能です。
同じような機能としてすでにLambda@Edgeが存在しますが、こちらはより軽量版といったところでしょうか。
実行時間が最大1msとかなり厳しく、できる処理はURL・ヘッダーの書き換えだったりとか、リダイレクトとかに限られますが、その代わり高速にレスポンスが返せます。
料金は、100万リクエスト当たり$0.10とのことです。Lambda@Edgeよりも安いですね。

# index.htmlへのルーティングをCloudFront Functionsで実装してみる
React/Vue/Angular等で作られたシングルページアプリケーション（SPA）は、ルーティングの処理をブラウザで実行されるJSで行うため、サーバー側でルーティングを行う必要がありません。すべての（静的ファイルへのリクエストを除く）リクエストは、index.htmlに向けられる必要があります。でないと、`/hoge`のようなサブページへのルーティングが404になってしまいます。

## 下準備

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/c8015950-8cca-965b-a09a-30cde96a2a13.png)
まず、`/hoge`というサブページを持つAngularアプリケーションを用意しました。
私がAngularの使い手というだけなので、これはVueでもReactでも何でもいいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/f1a06a3a-0ecc-5ce6-1042-db01c86f9dd0.png)
用意したSPAアプリケーションを、CloudFront+S3の形でアップロードして公開します。
ここまでで、`<ドメイン>/`直下、つまりTOPページは正しく表示されます。
しかし、アドレスバーに直接`<ドメイン>/hoge`と入れて、サブページを表示しようとすると、`Access Denied`のエラーが返ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/3e075b89-c72e-e143-100e-50826246851e.png)
これは、S3が**hogeという名前のファイルを返そうとしている**ためです。そのようなファイル実体は存在しないので、S3は404を返し、[こちら](https://qiita.com/sakuraya/items/add2cb7ced954215fb03#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%8C%E5%AD%98%E5%9C%A8%E3%81%99%E3%82%8B%E3%81%8B%E3%81%A9%E3%81%86%E3%81%8B)に書かれているようにCloudFrontは403を返します。
なので、`/hoge`のようなファイル実体が存在しないパスにルーティングしようとした際は、JSでルーティング処理を行わせるために、index.htmlを返す必要があるというわけです。
ということで、ここでCloudFront Functionsの出番です。

## CloudFront Functionsの設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/eb273d11-d724-d7c1-c101-f25f7617f80a.png)
CloudFrontの左側メニューの中に「Functions」が追加されていますので、クリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/6b52e6ab-252b-3157-2871-84d4b7a649b1.png)
「Create Function」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/7ac1ae0f-8deb-3595-52da-be81b0723073.png)
適切な名称を入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/12298456-4339-2fac-50d4-ce9233d4179f.png)
手順としては4段階、ステージは2つに分かれています。
まず「Build」。コードを書いて保存すると、「Development」ステージに反映されます。この段階では本番反映ではありません。

```javascript
var indexPage = "index.html"

function handler(event) {
    console.log(event);
    var request = event.request;
    var currentUri = request.uri;
    
    var doReplace = request.method === "GET" && currentUri.indexOf(".") === -1;
    
    if(doReplace) {
        var indexPath = `/${indexPage}`;
        request.uri = indexPath;
    }
    
    return request;
}
```

以上のコードを入力して「Save」してください。
やっていることは、リクエストメソッドがGETかつ、URIにドットが入っていない（＝拡張子がない）場合、index.htmlを返すように変更するというものです。
`console.log()`を含めていますが、ここで取れたログは`us-east1`リージョンのCloudWatch Logsに保存されます。
何やら懐かしいスタイルのJSコードを書いていますが、今のところES5相当の文法にしか対応していないようです。なんでやねん。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/17e643ab-3b3a-9e4a-f4ed-cf66039c0c18.png)
次に「Test」フェーズで、保存されたコードをテストできます。
今回は「Viewer Request」のフェーズで処理を挟みますので、「Event Type」をそのように選択します。
HTTPメソッド、URI、クエリパラメータ、リクエストヘッダー、Cookie、クライアントIPなどを変更できるようです。
「Compute utilization」という値は、最大実行時間の1msを100とした時の、実際の実行時間の値だそうです。最大でも70～80ぐらいに抑えておいたほうがよさそう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/079ce28a-4297-1863-3dbc-ad9ca66855e9.png)
このように、InputのURIに`/hoge`を入力して、OutputのURIが`/index.html`になっていればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/e90ad2aa-ed77-7c7f-4760-bc3bb236413f.png)
次に「Publish」をして、この段階で「Live」ステージにもコードがデプロイされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/f80199d1-3adc-02b0-d3a1-7d0c510cdf63.png)
最後に「Associate」で、このFunctionを紐づけたいCloudFrontディストリビューションを設定します。
先ほども書いたように、「Viewer Request」のフェーズで処理を挟むので、「Event Type」は「Viewer Request」を選んでください。
ディストリビューションの選択時、自動補完が出てきますが、ディストリビューションIDしか出てこないので、事前に紐づけたいディストリビューションのIDをメモっておいたほうがいいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188418/e504302e-7c64-b117-6a4d-f2a20709dc30.png)
設定が完了すると、`/hoge`をアドレスバーに直入力しても、正常にサブページが表示されるようになります。

# CloudFront Functionsで出来ないこと
CloudFront Functionsは、Lambda@Edgeで出来ることをもっとそぎ落として軽量高速化した機能であるため、今のところ以下のようなことはできません。

* コード内で別のAWSリソースへのアクセスはできない
* 最大実行時間が1msを超えるような重い処理はできない
* 「Origin Request」「Origin Response」イベントタイプの処理はできない

このような処理を行いたい場合は、Lambda@Edgeを採用するとよいでしょう。

# まとめ

従来、SPAのindex.htmlへのルーティング処理をCloudFrontで行う場合、「Custom Error Response」でオリジンが403を返した時に、ドメイン直下（index.html）を返すように設定するというような、若干ハッキーな手法が多く用いられてきました。その後、Lambda@Edgeが出てきて一応解消できるようにはなりましたが、デプロイまでが結構複雑なので二の足を踏んでいた方も多いと思います。今回のCloudFront Functionsの登場で、よりシンプルに同様の処理を実装できるようになりました。ありがとうAWSさん。