Lambda@Edge で静的サイトの URL を正規化する
2020-04-17Hugo でビルドしたブログやサイトを AWS 環境 (CloudFront + S3) で運用している方も多いと思います。今回は、Hugo のみならず静的サイトを AWS 環境で運用する際に利用したい Lambda@Edge についての話です。
目次
前提
まず、今回の話は下記のような構成で静的サイトを 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 + S3 と S3 の Static website hosting それぞれで運用した場合のインデックスドキュメントの挙動は次のようになります。
CloudFront + S3
http://example.com
またはhttp://example.com/
へのアクセス
index.html
の内容が表示されるhttp://example.com/index.html
へのアクセス
index.html
の内容が表示されるhttp://example.com/posts/sample_1
またはhttp://example.com/posts/sample_1/
へのアクセス
403 Access Deniedhttp://example.com/posts/sample_1/index.html
へのアクセス
posts/sample_1/index.html
の内容が表示される
S3 の Static website hosting
http://example.com
またはhttp://example.com/
へのアクセス
index.html
の内容が表示されるhttp://example.com/index.html
へのアクセス
index.html
の内容が表示されるhttp://example.com/posts/sample_1
またはhttp://example.com/posts/sample_1/
へのアクセス
posts/sample_1/index.html
の内容が表示されるhttp://example.com/posts/sample_1/index.html
へのアクセス
posts/sample_1/index.html
の内容が表示される
つまり、 CloudFront + S3 で静的サイトを運用する場合、対象のバケットの直下にある index.html
のみ省略が可能で、それ以降の階層にアクセスするためには明示的に index.html
にアクセスする必要があるということです。
静的サイトの URL の正規化
これが今回の一番のポイントです。
上で説明したとおり、個別のページには /index.html
を付与する必要があります。しかし、トップページには付与する必要はありませんし、付与してもアクセス可能です。つまり、トップページに対しては http://example.com
と http://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 つです。
- Viewer Request : クライアントから CloudFront へのリクエスト
- Viewer Response : CloudFront からクライアントへのレスポンス
- Origin Request : CloudFront から Origin へのリクエスト
- Origin Response : Origin から CloudFront へのレスポンス
たとえば、 Basic 認証を設定したい場合、 Viewer Request をトリガーにして認証情報をチェックする Lambda 関数を実行し、正しければ Origin へのリクエストを継続し、正しくなければクライアントへ認証失敗のレスポンスを返す といったことが可能になります。
Lambda@Edge を使用することで、クライアントからのリクエストを Origin に到達するまでにチェックすることが可能になります。なので、上の例と同じく Viewer Request をトリガーにしてクライアントからのリクエストをよしなに処理する Lambda を実行することで、 URL の正規化をやろうというのが今回の話です。(ここまで長くなりました)
Lambda@Edge で URL の正規化をする
Viewer Request をトリガーにすることで、クライアントからのリクエスト情報にアクセスすることができます。リクエストの URL とかヘッダーとかです。
実行される Lambda ではリクエストの URL をチェックして、 index.html
ありなのかなしなのか、 /
で終わっているかどうか、などをチェックし、必要であればクライアントにリダイレクトさせるなどの処理を実行します。その処理をうまいことやって、 URL の正規化を実現します。
(ここでの) 正規化の定義
一言で 正規化 と言っても、どういった形に落とし込むかは色々あると思います。なので、今回は次のような条件を満たすように正規化を考えることにします。
- 各ページには
/
終わりでのアクセスに統一 /
無し、および/index.html
終わりのリクエストは/
に 302 リダイレクト
これによって、次のリクエストはすべて https://michimani.net/post/development-lambda-edge-for-hugo-hosted-aws/
に統一され、アクセス解析でもこの URL へのアクセスとして計算されることになります。
https://michimani.net/post/development-lambda-edge-for-hugo-hosted-aws/
https://michimani.net/post/development-lambda-edge-for-hugo-hosted-aws
https://michimani.net/post/development-lambda-edge-for-hugo-hosted-aws/index.html
Hugo でアクセスが想定される URL
Hugo でビルドしたブログへのアクセスとして想定される URL を確認しておきます。
/page/*
: 記事一覧ページのページング/posts/*
または/post/*
: 各記事/about/
: about ページ/categories/*
: カテゴリ別ページ/tags/*
: タグ別ページ/archives/*
: アーカイブページ
これらのページ対して正規化の処理を実行するようにします。
Lambda の処理
お待たせしました。やっと Lambda 本体にたどり着きました。
やっていることは次の 2 つです。
/
無し、およびindex.html
終わりのリクエストは/
終わりの URL に 302 リダイレクト/
終わりのリクエストは/index.html
へのリクエストとして改ざんして Origin へ
まず一つ目ですが、正規表現で条件にマッチする場合は、クライアントにリダイレクトさせるような値を返却しています。
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