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挿入しなければならない。
  • アンカーIDにはドットを含む記号が入っているかもしれない(過去の投稿「アンカーIDの難点」参照)。
    • 上述の例のように、単純にURIの中にドットがあるからといってファイル拡張子が指定されたとは判断できない。
  • Markdownのセクションタイトルのすべての記号がそのままになるのでアンカーIDはハッシュやスラッシュを含んでしまうかもしれない。
    • URIの最初のハッシュをまず見つけて実際のパスとアンカーIDを分けなければならない。 私が正しくURIの文法を理解しているとすれば、この処理は正当なはず。
  • 試した限りでは、Zolaは最初のドットを見つけるとすぐに言語コードの区切りと判断するのでセクションとページのタイトルはドットを含まないはずである。 ということでアンカーIDを除くURIの最後のパスセグメントがドットを含む場合はセクションやページとは別のリソースなので/index.htmlを提供すべきでない。
  • URIはクエスチョンマーク(?)から始まるクエリパートを含むかもしれない。
    • 最後のURIセグメントとクエスチョンマークの間に[/]index.htmlを挿入しなければならない。

ということで、私のアルゴリズムは、

  1. URIが与えられる → uri
  2. 最初のオプションのハッシュ(#)をuriから見つけてフラグメント(#から始まる部分文字列もしくは空文字列)を分離する → [uri, fragment]
  3. 最初のオプションのクエスチョンマーク(?)をuriから見つけてクエリ(?から始まる部分文字列もしくは空文字列)を分離する → [uri, query]
  4. 最後のスラッシュ(/)をuriから見つけて最後のパスセグメント(/から始まる部分文字列)を分離する → [uri, last path segment]
  5. last path segmentがドット(.)を含んでいるなら、以下をlast path segmentに追加する。
    • "index.html": last path segment/で終わる場合
    • "/index.html": それ以外
  6. 新しい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を代わりにテストできます。

function handler(event) {
  return handlerImpl(event);
}
function handlerImpl(event) {
  // actual implementation...
}

特定のフォルダ内の*.jsファイルをBabelbabel-plugin-rewireで処理するようにJestを設定しました。 私のjest.config.jsファイルはこちらで閲覧できます。