30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Lambda@Edge で History API のフォールバックを複数設定する

SPA を開発する際に必須のタスクの一つとして、History APIのフォールバック(以下、単に「フォールバック」と記述する)がある。

この事象について掻い摘んで説明すると、SPA においては URL と HTML ファイルが一対一になっていないので、それに伴う対応を行うこと。

SPA ではその名の通りページは一枚しかなく、URL の管理や表示するコンテンツの切り替えは、ルーティングライブラリが行っている。
例えば、唯一のページを返す URL がhttp://example.com/である場合、その URL が返す HTML ファイルが JS ファイルを読み込み、その JS ファイルがルーティングライブラリによるプログラムを実行することで、http://example.com/foohttp://example.com/barといった URL が有効になる。

問題となるのが、いきなりhttp://example.com/fooなどの URL にアクセスされた場合。他のサイトからのリンクであったり、ページのリロードなどで、発生する。
この場合、http://example.com/fooに対応する HTML ファイルは存在しないため、404となってしまう。
そのため、http://example.com/foohttp://example.com/barへのアクセスがあった場合は、http://example.com/が対応している HTML ファイルを返すようにしないといけない。
そうすることで、必要な HTML ファイルと JS ファイルが読み込まれ、アクセスのあった URL に対応するコンテンツを表示させることが出来る。

開発環境でよく使われているwebpack-dev-serverでは、historyApiFallbackオプションを使うことで、対応できる。

devServer.historyApiFallback

本番環境ではサーバーの設定が必要。
Vueと組み合わせて使うルーティングライブラリVue Routerのドキュメントで、いくつかの例が簡単に紹介されている。

HTML5 History モード | Vue Router

この記事では、SPA の配信にCloudFrontを使う場合にどうやってフォールバックをするのか、書いていく。

Error Pages を使う?

CloudFrontにはError Pagesという設定項目があり、403404といったステータスコード毎に、レスポンスするページを設定できる。

これを使ってフォールバックを設定することが出来る。
先程の例なら、404が発生したときは/index.htmlを返すように設定すればよい。

だがこの方法だと、起点となるページが複数あるケースに対応できない。

SPA の規模によっては、起点となるページが一つだとは限らない。
そのようなときは、レスポンスしたいページは複数になる。
例えば、http://example.com/user/*にアクセスがあったときはhttp://example.com/user/index.htmlを、http://example.com/product/*にアクセスがあったときはhttp://example.com/product/index.htmlを返したいとする。

Error Pagesでは、このようなニーズに応えることは出来ない。一つのステータスコードに対して、一つのページしか設定できないから。

複数のフォールバックを設定するには、Lambda@Edgeを使う。

Lambda@Edge で URL をリライトする

Lambda@Edgeは、CloudFrontのエッジロケーションでLambdaを実行するサービス。

Lambda@Edge - AWS Lambda

これを利用して、リクエストのあった URL に応じてリライトすることで、複数のフォールバックを設定できる。

IAMの設定

Lambdaを利用するにはIAMロールが必要なので、作成する。

まず、以下の内容のIAMポリシーを作成する。
CloudWatch Logsにログを出力することになるので、その権限が必要。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}

新しくロールを作り、上記のポリシーを付与する。
そして、「信頼されたエンティティ」としてlambda.amazonaws.comedgelambda.amazonaws.comを指定する。

これでIAMの設定は完了。

関数の作成

Lambdaで関数を作成する。
Lambda@Edgeを使うには、リージョンを「バージニア北部」に、ランタイムをNode.js 8.10Node.js 6.10に設定しておく必要がある。
ロールは、先程作成したものを使う。

コードを書く

先程の例だと、以下のように書けばいい。
そんなに難しい内容ではないのだが、ポイントとしては以下。

  • 第一引数のeventの中から、リクエストされた URI を取得する
  • その URI に応じて処理を変えたり、URI を書き換えたりする
  • console.logを実行すると、その内容がCloudWatchに保存される
exports.handler = (event, context, callback) => {
  const {request} = event.Records[0].cf;

  const currentUri = request.uri;

  // ドットを含むURIは、アセットへのアクセスとみなし、リライトしない
  if (currentUri.indexOf('.') !== -1) {
    console.log(`Don't rewrite. Uri is ${currentUri}`);
    return callback(null, request);
  }

  let newUri = currentUri;
  switch (true) {
    case /^\/user/.test(currentUri):
      newUri = '/user/index.html';
      break;
    case /^\/product/.test(currentUri):
      newUri = '/product/index.html';
      break;
    default:
  }

  console.log(`Old URI: ${currentUri}`);
  console.log(`New URI: ${newUri}`);

  request.uri = newUri;

  return callback(null, request);
};

デプロイ

コードを保存したら、デプロイする。
ページ上部の「アクション」ボタンから「Lambda@Edge へのデプロイ」を選択すればよい。
今回のケースでは「CloudFront イベント」は「オリジンリクエスト」を選ぶ。

Lambda@Edge の解除

設定したLambda@Edgeの解除は、Lambdaの画面からは出来ない。
CloudFrontのディストリビューションの設定画面からBehaviorsを選び、Lambda Function Associationsを編集することで解除できる。
そうすると、Lambdaの画面から、関数を削除することも出来るようになる。