CloudFrontを介してS3からコンテンツを提供する
2022-06-20 (2022-06-27 更新)
このウェブサイトはZolaで生成しAmazon CloudFrontを介してAmazon S3から配信しています。 このブログ投稿ではこの構成でコンテンツをうまく配信するために何をしたかをお伝えします。
コンテンツ配信のためのプラン
私のウェブサイトはS3バケットのバケットにデプロイしCloudFront Distributionを介して配信するつもりでした。 このアイデア自体はいたって普通です。
Zolaはどのようにコンテンツを配置しているか
Zolaは各セクションとページのコンテンツを/{parent section path}/{section or page title}/index.html
のようなパスに配置しています(このページならば/ja/blog/0002-serving-contents-from-s3-via-cloudfront/index.html
)。
コンテンツを参照するときは、サーバ側で末尾に/index.html
が追加される前提で/index.html
を省略して/{parent section path}/{section or page title}
のようにします(このページならば/ja/blog/0002-serving-contents-from-s3-via-cloudfront
)。
残念ながらこれ(サブディレクトリにindex.html
を追加する)はCloudFront Distributionにとって簡単*なタスクではありません。
(*これは実際全然簡単ではありませんでした!)
CloudFront Functionsの導入
上記の課題に対処するためにCloudFront Functionsを使うことができます。 CloudFront Functionをまさにこのような状況に使う例がAWSのガイドにあります。 しかしこの一見簡単そうなタスクは全く簡単でないことが分かりました。 URIの仕様に注意深く対処しなければならず、分かったことは、
- URIはアンカーIDで終わるかもしれない。つまり、ハッシュが続くかもしれない(
#
)。- 最後のURIセグメントとハッシュの間に
[/]index.html
挿入しなければならない。
- 最後のURIセグメントとハッシュの間に
- アンカーIDにはドットを含む記号が入っているかもしれない(過去の投稿「アンカーIDの難点」参照)。
- 上述の例のように、単純にURIの中にドットがあるからといってファイル拡張子が指定されたとは判断できない。
- Markdownのセクションタイトルのすべての記号がそのままになるのでアンカーIDはハッシュやスラッシュを含んでしまうかもしれない。
- URIの最初のハッシュをまず見つけて実際のパスとアンカーIDを分けなければならない。 私が正しくURIの文法を理解しているとすれば、この処理は正当なはず。
- 試した限りでは、Zolaは最初のドットを見つけるとすぐに言語コードの区切りと判断するのでセクションとページのタイトルはドットを含まないはずである。
ということでアンカーIDを除くURIの最後のパスセグメントがドットを含む場合はセクションやページとは別のリソースなので
/index.html
を提供すべきでない。 - URIはクエスチョンマーク(
?
)から始まるクエリパートを含むかもしれない。- 最後のURIセグメントとクエスチョンマークの間に
[/]index.html
を挿入しなければならない。
- 最後のURIセグメントとクエスチョンマークの間に
ということで、私のアルゴリズムは、
- URIが与えられる →
uri
- 最初のオプションのハッシュ(
#
)をuri
から見つけてフラグメント(#
から始まる部分文字列もしくは空文字列)を分離する → [uri
,fragment
] - 最初のオプションのクエスチョンマーク(
?
)をuri
から見つけてクエリ(?
から始まる部分文字列もしくは空文字列)を分離する → [uri
,query
] - 最後のスラッシュ(
/
)をuri
から見つけて最後のパスセグメント(/
から始まる部分文字列)を分離する → [uri
,last path segment
] last path segment
がドット(.
)を含んでいるなら、以下をlast path segment
に追加する。"index.html"
:last path segment
が/
で終わる場合"/index.html"
: それ以外
- 新しいURIを返す =
uri
+last path segment
+query
+fragment
私が実装したhandler
関数はこちらで閲覧できます。
ところで、CloudFront FunctionsのJavaScriptエンジンはECMA v5.1をベースにしており「古いなぁ・・・」と感じるかもしれません。
CloudFront Functionsのユニットテストを行う
CloudFront Functionsのユニットテストもやっかいです。
こちらの記事が便利だと思いました。
問題はCloudFront Functionsのランタイムがmodule.exports
の記述もexport
修飾子も許さないことです。
なのでCloudFront Functionsのスクリプトから関数をエクスポートする標準的な方法がありませんでした。
上述の記事の提案する回避策はインポートしたスクリプトに内部変数や内部関数にアクセスする関数を後付けするbabel-plugin-rewire
を使うというものでした。
babel-plugin-rewire
を試してみると、使われていない内部関数が削除されてしまうというbabel-plugin-rewire
の課題に出くわしました。
ランタイムから呼び出されるhandler
関数自体はソースファイル内で呼び出されていないのでこれは問題です。
先述したとおり、module.exports
の記述もexport
修飾子も使えません。
私の回避策は別の関数handlerImpl
を追加してシンプルにhandler
からそれを呼び出すというもので、そうするとhandlerImpl
を代わりにテストできます。
特定のフォルダ内の*.js
ファイルをBabelとbabel-plugin-rewire
で処理するようにJestを設定しました。
私のjest.config.js
ファイルはこちらで閲覧できます。