Feedforce Developer Blog

フィードフォース開発者ブログ

Next.js の Static HTML Export で生成したファイルを Lambda@Edge を使わずに CloudFront + S3 にデプロイする

こんにちは、id:daido1976 です。

今回は Next.js の Static HTML Export で生成したファイルを Lambda@Edge を使わずに CloudFront + S3 にデプロイする方法を紹介します。

前提

Next.js

Next.js のアプリで SSR を行う場合は Vercel やその他の Node.js 実行環境のあるサーバにデプロイする必要がありますが、そうでない場合は Static HTML Export 機能を使って静的ページを出力し、任意の Web サーバにデプロイすることもできます。

Static HTML Export 機能では通常 page/about.tsx というファイルから /about.html が生成されますが、これをそのまま S3 にアップロードして CloudFront 経由で /about にアクセスしても上手く動きません。

Next.js 側で生成されるファイルを /about.html から /about/index.html に変える trailingSlash: true という設定があるのでこれで一件落着と思いきや、CloudFront + S3 側でも課題がありました…。

CloudFront + S3

Next.js の Static HTML Export 機能(& trailingSlash: true)でビルドされた /index.html/about/index.html のように、ルートとサブディレクトリにインデックスドキュメントを含むファイル群をデプロイする際には以下の方法が考えられますが、それぞれに問題があります。

  1. CloudFront の Default Root Objectオリジンアクセスアイデンティティ を使う
    • ルートの index.html は表示できるがサブディレクトリの index.html が表示できない
  2. S3 の 静的ウェブサイトホスティング 機能を有効にし、インデックスドキュメント を設定する
    • ルート、サブディレクトリともに index.html 表示できるが、S3への直接アクセスが禁止できない

👆 の解決策として、以下の記事のように Lambda@Edge などを使って「末尾が "/" で終わっているアクセスのパスに index.html を付与する」というものが多いです。

dev.classmethod.jp

が、管理するリソースはできるだけ増やしたくないのと、CloudFront の Behavior を使ってマルチオリジン構成にしている時にはパス書き換えのロジックが複雑になってしまうので、できれば Lambda@Edge を使わず CloudFront + S3 だけでやる方法を見つけたいと考えていました。

結論としては、2. S3 の 静的ウェブサイトホスティング 機能を有効にし、インデックスドキュメント を設定する の方法を使いながら S3 への直接アクセスを禁止することができたので、その方法をご紹介します。

やり方

以下の AWS ナレッジセンターの記事を参考にしました。

CloudFront を使用して、Amazon S3 でホストされた静的ウェブサイトを公開するにはどうすればよいですか?#アクセスが Referer ヘッダーで制限されたオリジンとして、ウェブサイトのエンドポイントを使用する

ざっくりしたアーキテクチャ図は以下になります。

f:id:daido1976:20201021094707p:plain

1. S3 側での静的ウェブサイトホスティングの設定

S3 の静的ウェブサイトを有効にして、インデックスドキュメントを index.html にします。

以下のエンドポイントは後ほど CloudFront の Origin Domain Name に設定する必要があるので控えておきましょう。

f:id:daido1976:20201020084312p:plain

2. CloudFront の Origin Domain Name の設定

CloudFront の Origins and Origin Groups タブから Origin の Edit を押下し、

f:id:daido1976:20201020084835p:plain

Origin Domain Name を S3 の Origin ではなく、1 で取得した S3 の静的ウェブサイトのエンドポイント(ドメインの指定なので http:// は不要)にします。*1

f:id:daido1976:20201020085837p:plain

ここまでは通常の静的ウェブサイトホスティングの設定と同じです。

3. Referer を使って CloudFront から S3 への直接アクセスを禁止する

[ヘッダー名] の [Origin カスタムヘッダー] に、[Referer] と入力します。[値] には、オリジンに転送するカスタムヘッダーを入力します (S3 バケット)。オリジンへのアクセスを制限するために、ランダムな値または他の人は知らない秘密の値を入力することができます。

ドキュメントに従ってそのまま Origin の編集画面でカスタムヘッダを設定します。

「ランダムな値または他の人は知らない秘密の値を入力することができます」とある通り、この Referer の値は URL ではなく適当な値でも構いません。

f:id:daido1976:20201020090510p:plain

加えて、S3 コンソールから上記のカスタム Referer ヘッダーがリクエストに含まれることを条件として、s3:GetObject を許可するバケットポリシーを追加します。

バケットポリシーの例#特定の HTTP Referer へのアクセスの制限 - Amazon Simple Storage Service

f:id:daido1976:20201016135940p:plain

これで Referer ヘッダーに指定の値が含まれていないアクセスを禁止することができます。*2

4. Next.js 側で各ページのファイル名を index.html にする設定

最後に Next.js の設定で trailingSlash: true を指定して、各ページのファイル名を index.html にします。

https://nextjs.org/docs/api-reference/next.config.js/exportPathMap#adding-a-trailing-slash

参考

*1:S3 の Origin は {bucket}.s3.amazonaws.com のような形式で、S3 の静的ウェブサイトのエンドポイントは {bucket}.s3-website-{region}.amazonaws.com のような形式です。間違えやすいので注意。

*2:実際は curl などでこの Referer の値をヘッダにセットすると CloudFront 経由でなくともアクセスできるのですが、今回は一般ユーザからの URL 直打ちによるアクセスが禁止できれば良いので、この方法で問題ないと判断しました。