michimani.net

Lambda@Edge で静的サイトの URL を正規化する

2020-04-17

Hugo でビルドしたブログやサイトを AWS 環境 (CloudFront + S3) で運用している方も多いと思います。今回は、Hugo のみならず静的サイトを AWS 環境で運用する際に利用したい Lambda@Edge についての話です。

目次

前提

まず、今回の話は下記のような構成で静的サイトを AWS 環境で運用していることを前提とします。

Static site on AWS

S3 にコンテンツを配置し、前段に CloudFront を用意します。 CloudFront から S3 へのアクセスには Origin Access Identity を使用し、クライアントから S3 バケット内のオブジェクトへは直接アクセスできないようになっています。

AWS 環境で静的サイトを運用する際の注意点

AWS 環境で静的サイトを運用する方法としては、上記のように S3 の前段に CloudFront を置く方法と、 S3 の Static website hosting 機能を使用する方法があり、この 2 つの方法では インデックスドキュメントの挙動が異なります

インデックスドキュメントの挙動

例えば、次のような構造を持つサイトがあるとします。(仮にホスト名を http://example.com としておきます)

├── error.html
├── index.html
└── posts
    ├── sample_1
    │   └── index.html
    └── sample_2
        └── index.html

このサイトを CloudFront + S3S3 の Static website hosting それぞれで運用した場合のインデックスドキュメントの挙動は次のようになります。

CloudFront + S3

S3 の Static website hosting

つまり、 CloudFront + S3 で静的サイトを運用する場合、対象のバケットの直下にある index.html のみ省略が可能で、それ以降の階層にアクセスするためには明示的に index.html にアクセスする必要があるということです。

静的サイトの URL の正規化

これが今回の一番のポイントです。

上で説明したとおり、個別のページには /index.html を付与する必要があります。しかし、トップページには付与する必要はありませんし、付与してもアクセス可能です。つまり、トップページに対しては http://example.comhttp://example.com/index.html という 2 つの URL を持つことになります。

これらの見た目は同一ページですが、 Google Analytics などのアクセス解析などを利用する際には別ページとして扱われます。なので、 index.html ありかなしかは統一されていたほうがよいです。

じゃあどっちに統一するかという話ですが、トップページに関してはドメインのみでアクセスできたほうがスッキリするので index.html が無いほうがいいですよね。合わせて個別ページも / でアクセスできるようにしたいところですが、上で説明したように深い階層にアクセスする際には index.html を付ける必要があります。

これを解決するのが、 CloudFront の Lambda@Edge という機能です。

Lambda@Edge とは

Lambda@Edge は、Amazon CloudFrontの機能で、アプリケーションのユーザーに近いロケーションでコードを実行できるため、パフォーマンスが向上し、待ち時間が短縮されます。

(中略)

Lambda@Edge を使用すると、サーバー管理を何も行わなくても、ウェブアプリケーションをグローバルに分散させ、パフォーマンスを向上させることができます。Lambda@Edge は、Amazon CloudFront コンテンツ配信ネットワーク (CDN) によって生成されたイベントに対応してコードを実行します。

つまりどういうことかというと、 CloudFront に発生するイベント をトリガーに Lambda 関数を実行できるということです。 CloudFront に発生するイベント とは、次の 4 つです。

たとえば、 Basic 認証を設定したい場合、 Viewer Request をトリガーにして認証情報をチェックする Lambda 関数を実行し、正しければ Origin へのリクエストを継続し、正しくなければクライアントへ認証失敗のレスポンスを返す といったことが可能になります。

Lambda@Edge を使用することで、クライアントからのリクエストを Origin に到達するまでにチェックすることが可能になります。なので、上の例と同じく Viewer Request をトリガーにしてクライアントからのリクエストをよしなに処理する Lambda を実行することで、 URL の正規化をやろうというのが今回の話です。(ここまで長くなりました)

Lambda@Edge で URL の正規化をする

Viewer Request をトリガーにすることで、クライアントからのリクエスト情報にアクセスすることができます。リクエストの URL とかヘッダーとかです。
実行される Lambda ではリクエストの URL をチェックして、 index.html ありなのかなしなのか、 / で終わっているかどうか、などをチェックし、必要であればクライアントにリダイレクトさせるなどの処理を実行します。その処理をうまいことやって、 URL の正規化を実現します。

(ここでの) 正規化の定義

一言で 正規化 と言っても、どういった形に落とし込むかは色々あると思います。なので、今回は次のような条件を満たすように正規化を考えることにします。

これによって、次のリクエストはすべて https://michimani.net/post/development-lambda-edge-for-hugo-hosted-aws/ に統一され、アクセス解析でもこの URL へのアクセスとして計算されることになります。

Hugo でアクセスが想定される URL

Hugo でビルドしたブログへのアクセスとして想定される URL を確認しておきます。

これらのページ対して正規化の処理を実行するようにします。

Lambda の処理

お待たせしました。やっと Lambda 本体にたどり着きました。

やっていることは次の 2 つです。

まず一つ目ですが、正規表現で条件にマッチする場合は、クライアントにリダイレクトさせるような値を返却しています。

const redirectUrl = host + requestUri.replace('/index.html', '') + '/';
const response = {
    status: '302',
    statusDescription: 'Found',
    headers: {
        location: [{
            key: 'Location',
            value: redirectUrl,
        }],
  },
}
// return response to redirect (for viewer)
callback(null, response);

こうすることで、クライアントのリクエストは Origin まで到達せず、 CloudFront からレスポンスが返ることになります。

二つ目に関しては、 / 終わりでリクエストされた URL を /index.html へのリクエストという風に request.uri を更新しています。

// Replace the received URI with the URI that includes the index page
request.uri = actualUri;

// Return to CloudFront (for origin)
callback(null, request);

これにより、 / という Viewer Request が /index.html という Origin Request に変わり、 Origin は /index.html ならあるよ ということでレスポンスを返してくれます。クライアントからすれば、 / のリクエストで /index.html が返ってきた感じです。

この Lambda が実行されるのは CloudFront にキャッシュが存在しない場合のみなので、料金的にもあまり心配はいりません。

まとめ

AWS 上で静的サイトを運用するときに Lambda@Edge で URL を正規化する方法について書きました。
最後に紹介した Lambda 関数は Hugo を想定したものになっていますが、他の静的サイトジェネレータでビルドしたサイトでも同様の方法で URL の正規化はできます。

AWS で静的サイトを運用する場合は、インデックスドキュメントの挙動に注意して、 URL の正規化についても頭に入れておいたほうが良さそうです。Lambda@Edge のユースケースごとの実装サンプルについて公式ドキュメントにいくつか用意されているので、こちらもすごく参考になります。


comments powered by Disqus