S3+CloudFrontによる静的サイトにLambda@Edgeを設定(1)

S3+CloudFrontといった構成で、静的Webサイトを構築した場合、通常のWebサーバーなら簡単に利用できる、URL(URI)の書き換えやBasic認証といった機能が、そのままでは利用できません。今回は、Lambda@Edgeを使って、URLの書き換えを試してみました。


S3には、静的なWebサイトをホスティングする機能があります。個別にWebサーバーを用意しなくて済む便利な機能です。そのままでは、https通信をサポートしませんが、CloudFrontを併用することで、https通信を可能にできます。このため、静的Webサイトには、S3+CloudFrontの構成がよく利用されています。

S3+CloudFrontの構成

このブログも、Hexoという静的サイトジェネレーターによる出力を、S3+CloudFrontでホスティングしています。

S3+CloudFrontで、構成によって可能なこと・不可能なこと

S3とCloudFrontを組み合わせるには、次の2つの構成があります。

静的WebサイトのためのS3+CloudFrontの構成
構成 機能設定 CloudFrontでのオリジン設定
1 S3側で「静的Webサイトホスティング」機能を使用する {bucket}.s3-website-{region}.amazonaws.com
※「静的Webサイトホスティング」専用のドメイン名を入力
2 S3側で「静的Webサイトホスティング」機能を使用せず、
CloudFront側の各種機能を使用する
{bucket}.s3.amazonaws.com
※S3バケットの一覧から選択

S3もCloudFrontもhttpに対応したサービスではありますが、Webサーバーとして使うには、指定がなくても”index.html”といったトップページを表示したり、サイト固有のエラーページを返したりする機能が必要です。

これらの「Webサーバーとして」の機能は、S3の「静的Webサイトホスティング」機能を有効にすると、S3側で提供されます(構成1)。このとき、CloudFrontのオリジン設定で、S3バケットの一覧から選択すると、「静的Webサイトホスティング」機能が利用できず、正常に動作しません。S3バケットを選択せず、「静的Webサイトホスティング」専用のドメイン名を入力する必要があります。

専用のドメイン名は設定画面に表示

構成1の場合は、CloudFrontを経由せず、S3に直接アクセスすることを拒否できません。たとえば、CloudFront側でアクセス制御を実装しても、S3に直接アクセスされては意味がありません。

S3の「静的Webサイトホスティング」機能を有効にせず、「Webサーバーとして」の機能を、CloudFront側で提供させることも可能です(構成2) 。この場合は、CloudFrontのオリジン設定で、S3バケットの一覧から選択します。「OAI」(Origin Access Identity)を使って、CloudFrontを経由しない、S3への直接アクセスを拒否することができます。

これで一件落着‥‥残念ながら、いかないのです。

というのは、構成2の場合、CloudFrontの「Default Root Object」機能で、サイトのトップページ(ルート)として”index.html”を表示できますが、サブディレクトリに関しては、それができません。”http://~/“⇒”http://~/index.html”は可能ですが、”https://~/sub/“→”https://~/sub/index.html”ができないのです。

構成1と2での利用できる機能の違い
利用できる機能 構成1 構成2
“http://~/“→”http://~/index.html”
“http://~/sub/“→”http://~/sub/index.html” ×
サイト固有のエラーページ
CloudFrontを経由しない、S3への直接アクセスの禁止 ×

「なぜ?」といいたくなりますが、そういう仕様のようです。

そこで、構成2の不足機能を補うために、Lambda@Edgeを設定してみました。URL(URI)を書き換えることで、”http://~/sub/“→”http://~/sub/index.html”を可能にします。

なお、下記の記事などを参考にしました。
できた!S3 オリジンへの直接アクセス制限と、インデックスドキュメント機能を共存させる方法

Lambda@Edgeの注意点

Lambda@Edgeを使うと、CloudFrontに追加の処理を実行させることができます。通常のLambdaに比べて、以下の点に注意する必要があります。

  1. 実行ロール(IAMロール)
  2. トリガー
  3. Lambda@Edgeの独自制限(ランタイムやタイムアウトなど)

実行ロール(IAMロール)は、通常のLambdaとは少し違います。実際の作業手順には、テンプレートから作成するのが簡単で、オススメです。

トリガーは、以下の4つがあります。ビューワーは利用者のブラウザー、オリジンはキャッシュ元のサーバー(当記事ではS3)のことです。どのトリガーを使うかで、できることが変わってきます。

CloudFront がビューワーからリクエストを受信したとき (ビューワーリクエスト)
CloudFront がリクエストをオリジンに転送する前 (オリジンリクエスト)
CloudFront がオリジンからレスポンスを受信したとき (オリジンレスポンス)
CloudFront がビューワーにレスポンスを返す前 (ビューワーレスポンス)

Lambda@Edge を使用したエッジでのコンテンツのカスタマイズ

4つのトリガーがある

ランタイムは、Lambda@Edgeでは、2020年1月現在、Node.js 8.x/10.x、Python 3.7に対応しています。通常のLambda関数で利用可能なNode.js 12.xは、まだサポートされていません。

nodejs8.10、nodejs10.x、または python3.7 ランタイムプロパティを使用して関数を作成する必要があります。

Lambda 関数の設定と実行環境

また、タイムアウトは、通常のLambda関数は最大900秒になりましたが、Lambda@Edgeでは下記のように、より短くなっています。

エンティティ オリジンのリクエストおよびレスポンスイベントの制限 ビューワーのリクエストおよびレスポンスイベントの制限
関数タイムアウト 30秒 5秒

Lambda@Edge の制限

その他の制限についても、上記に記載されています。

Lambda@Edgeの設定

CloudFrontはグローバルサービスですので、Lambda@Edgeも「バージニア北部(us-east-1)」リージョンで設定します。

「バージニア北部」リージョンにする

Lambda@Edgeのコード
1
2
3
4
5
6
7
8
'use strict';
exports.handler = async (event, context) => {
const request = event.Records[0].cf.request;
const oldUri = request.uri;
const newUri = oldUri.replace(/\/$/, '\/index.html');
request.uri = newUri;
return request;
};

上記のコードにより、URL(URI)の最後が / の場合に、これを /index.html に置き換えています。
ランタイムは Node.js 10.x としました。

実行ロールは、テンプレートから作成し、そのまま利用します。「AWSポリシーテンプレートから新しいロールを作成」⇒「基本的な Lambda@Edge のアクセス権限 (CloudFront トリガーの場合)」を選択しました。
「基本的な Lambda@Edge のアクセス権限」を選択

バージョン:$LATESTという状態では、コードを修正できますが、Lambda@Edgeはこの状態に対応していないため、トリガーを設定する前に、「アクション」⇒「新しいバージョンを発行」と操作して、バージョン:1やバージョン:2などを作成します。
「新しいバージョンを発行」を実施

作成されたバージョンに対し、トリガーとして「オリジンリクエスト」を設定しました。
トリガーを設定

なお、設定後、デプロイの完了には、数十分かかりました。

このように設定することで、S3の「静的Webサイトホスティング」を有効にしなくても、”https://~/sub/“にアクセスできるようになりました。次回は、Basic認証を設定します。