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の画面から、関数を削除することも出来るようになる。

フロントエンドエンジニアがお世話になるであろう AWS のサービス群

備忘録として、記録していく。
ひとつひとつのサービスについて詳しく説明したりはしない。あくまでも概要や役割と、サービス間の関係について紹介するのみ。
そもそも AWS について詳しくない。フロントエンドエンジニアが必要に駆られて触っている、という感じなのでまだまだ初心者である。

S3

ストレージサービス。

純粋にストレージとして使うことも出来るし、コンテンツ配信のためにファイルを置くことも出来る。
単純にアセット(画像やJSファイルなど)を置いてもいいし、ホスティング機能を使ってS3をウェブサーバとして使うことも出来る。

他の AWS のサービスと連携して、ファイルの保存先として使うことも多い。
ログファイルもそうだし、AWS が提供するメールサービスのメールの受信先として、S3を設定することも出来る。

ファイルは、「バケット」という単位で管理する。
バケットに保存されているそれぞれのファイルのことを、「オブジェクト」と呼ぶ。

S3では、かなり細かくアクセス権を設定できる。コンテンツ配信に使うのでなければ、非公開にしておくべき。
詳細は以下を参照。
S3で誤ったデータの公開を防ぐパブリックアクセス設定機能が追加されました | DevelopersIO

CloudFront

CDNサービス。

「ディストリビューション」という単位で、設定を管理する。
ディストリビューション単位で、配信するコンテンツを設定する。

CloudFrontでは、大本のコンテンツを配信しているサーバを「オリジンサーバ」、ユーザーへの配信を行うサーバを「エッジサーバ」と呼ぶ。「エッジサーバ」は「エッジロケーション」と呼ばれることもある。
AWS再入門 Amazon CloudFront編 | DevelopersIO

CloudFrontを使うにはまず、ディストリビューションを作成する。色々と設定を行うが、そのときにオリジンサーバの指定も行う。
ディストリビューションが作成されると、xxxxx.cloudfront.netというドメインが生成される。
このドメインにアクセスすると、「オリジンサーバ」と同じ内容のコンテンツが「エッジサーバ」から配信される。

オリジンサーバには、先程紹介したS3を指定することも出来る。
S3に置いてあるコンテンツをCloudFront経由で配信する。この方法を使えば、SSL証明書を使ってHTTPS化することも出来る。

オリジンサーバにS3のコンテンツを指定したい場合、バケットそのものを指定するパターンと、S3のホスティングサービスが出力したURLを指定するパターンがあり、それぞれに特徴が異なるので留意する。
CloudFront + S3 で静的サイトを運用する際の注意点 - Qiita

オリジンサーバとしてバケットを指定する場合、バケットやオブジェクトに直接アクセスすることを禁止できるので、セキュリティ的にはこちらのほうが望ましいと思われる。
具体的にはまず、バケットのアクセス権の設定で、外部からアクセスできないようにする。
次に、ディストリビューションを作成するときの設定で、Restrict Bucket AccessYesに、Yes, Update Bucket Policyを有効にする。
こうすると、ディストリビューションからはバケットにアクセスできるようになり、そのためのバケット側の設定も自動的に行われる。
[CloudFront + S3]特定バケットに特定ディストリビューションのみからアクセスできるよう設定する | DevelopersIO

注意点としては、この方法でコンテンツを配信してもすぐには有効にならないこと。
ステータスコード307のリダイレクトが発生してしまい、正しく表示されない。
解消されるのを1時間ほど待つ必要がある。
Cloudfront,S3で307リダイレクトに苦しめられた - パパエンジニアのアウトプット帳

もうひとつの注意点としては、エラーページの設定。
ディストリビューション毎にエラーページを設定することができ、表示させるページをステータスコード毎に設定する。404が発生したときはこのページ、500が発生したときはこのページ、といった具合に。
だがS3は、コンテンツが見つからなかったときに何故か403を返すので、それに合わせた設定をする必要がある。

IAM

AWS の各種サービスを利用するための権限の設定。

「ユーザー」「ロール」「ポリシー」といった単位で管理する。

例えば、ルート権限(に相当するもの)で AWS にログインするのではなく、必要な機能へのアクセス権のみを持つユーザーを作成して、それでログインすることでセキュリティを高める。

また、CIサービスで AWS の操作(S3へのデプロイなど)を行うときも、適切な権限を設定してユーザーを作成し、そのユーザーのアクセスキーをCIサービスに登録する必要がある。

Route 53

DNSサービス。

ドメインの取得や管理を行う。もちろん、他社で取得したドメインも使える。
サブドメインの作成なども、ここで行う。

「ホストゾーン」という単位で管理する。

CloudFrontの設定画面でAlternate Domain Names(CNAMEs)にドメインを入力すると、そのドメインでコンテンツを配信できる。

ACM

SSL証明書の発行や管理を行う。

証明書を登録すると、CloudFrontCustom SSL Certificateで選択肢として表示されるので、それを選ぶとコンテンツをHTTPS化できる。

Lambda@Edge

Lambdaは、サーバーレスでコードを実行するためのサービス。
そしてLambda@Edgeは、CloudFrontのエッジサーバでLambdaを実行するサービス。
このサービスを使うことで、CloudFrontで配信しているコンテンツにアクセスがあった際に、任意の処理を差し込むことが出来る。

具合的には、ベーシック認証を設定したり、URLのリライトを行ったりすることが出来る。
Amazon CloudFrontとAWS Lambda@EdgeでSPAのBasic認証をやってみる | DevelopersIO
できた!S3 オリジンへの直接アクセス制限と、インデックスドキュメント機能を共存させる方法 | DevelopersIO

Lambda@Edgeを利用するにはまず、そのためのIAMロールを作成する必要がある。
必要なアクセス権を付与する他、「信頼されたエンティティ」にlambda.amazonaws.comedgelambda.amazonaws.comを設定する。

Lambda@Edgeを利用できるリージョンは今日現在では「バージニア北部」のみ、ランタイムはNode.js v6.10Node.js v8.10を利用できる

先程作成したロールを使ってLmabdaで関数を作成し、 「アクション」から「Lambda@Edge へのデプロイ」を選択すればよい。
[アップデート] Lambda@Edge が超簡単にデプロイ出来るようになったよ! | DevelopersIO