CloudFrontとLambda@Edgeでアクセスするドメインごとにオリジンを切り替えてみた

CloudFrontの1つのディストリビューションで「同じパスに対して、アクセスしたドメイン(Hostヘッダ)ごとにオリジンを切り替える」という動作が、Lambda@Edgeとの連携で実現可能です。実際に試して挙動を確認してみました。
2021.07.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

清水です。AWSのCDNサービスであるAmazon CloudFront、マルチオリジン構成の場合ではパスパターンによって振り分けられるBehavior単位で使用するオリジンサーバを決める方法が基本です。(オリジンフェイルオーバのためオリジングループを使用するケースもあります。)先日ふと、Behavior(パスパターン)は同じで異なるオリジンを(フェイルオーバー用のオリジングループを使うケースでなく)使うことはできないか、ということを考えてみました。CloudFront単体では難しいのですが、Lambda@Edgeを使うことで実現が可能です。Lambda@Edgeのユースケース「コンテンツベースの動的オリジンの選択」として紹介されています。リクエストの場所やその他プロパティによって、S3オリジンのリージョンやカスタムオリジンのドメイン名を変えるという具合ですね。

今回はこれらの派生形として、CloudFrontへのリクエスト時のドメイン名、Hostヘッダの内容によってオリジンを切り替える、という構成を試してみました。イメージとしては以下となります。

リクエスト時のHostヘッダがred.example.comであれば、red.example.com用に準備したオリジンred-origin.example.comからレスポンスが返ります。またリクエスト時のHostヘッダがblue.example.comであればblue.example.com用に準備したオリジンblue-origin.example.comからレスポンスが返る、となります。

このように、CloudFrontで2つのドメインと、それに対応する2つのオリジンを使用する、というケースを考えた場合、以下のようにCloudFrontディストリビューションをわけてしまったほうが構成はシンプルになると思います。

例えばアクセスログの出力場所はCloudFrontディストリビューションごとに設定するので、2つのドメインのアクセスログが混在する、といったこともありません。またCloudFrontの利用料金は基本的にはデータ転送量とリクエスト数で決まりディストリビューションの数によらないため、1つにまとめることでの料金的メリットもありません。(ただし、例外として専用IPカスタムSSLを使用するケースではディストリビューションごとに利用料金が発生するため、この1つにまとめる構成で利用料金の削減ができるかもしれません。)その他、Behavior(パスパターン)ルールなどが複雑などの場合に、管理を容易にするため2つ目のディストリビューションを作成せず1つにまとめたい、といったケースでしょうか。(ただしLambda@Edgeを使用する分、こちらで多少なりとも構成が複雑になる感は否めません。)

ということで、個人的にはこの「CloudFrontディストリビューションを1つにまとめ、Hostヘッダの内容によってオリジンを切り替える」構成についてはそこまでメリットがないのかな、と考えるのですが、Lambda@Edgeを使うことでこんな構成も取れるんだ、ということの確認として実際に試してみたのでまとめておこうと思います。

オリジン環境の準備

まずはオリジン環境として、ドメイン名red-origin.example.comblue-origin.example.comでアクセスできる環境を準備しておきました。それぞれアクセスすると以下のように、赤色と青色のページが表示されます。なおCloudFrontからオリジンサーバへの通信は簡略化のためHTTPとしました。(HTTPSは設定していません。)

CloudFrontディストリビューションの作成

続いてCloudFrontディストリビューションを作成します。Origin domainとしてred-origin.example.comを入力します。

Behaviorはデフォルト(Default (*))のみ使用します。Cache keyとOrigin requestの設定では、Cache policyとしてCachingDisabledを、Origin request policyとしてAllViewerを選択しています。検証目的のためキャッシュは無効とし、リクエスト時にもすべての情報をオリジンに転送する動作とします。キャッシュを利用するような設定を行う場合でも、Hostヘッダによるキャッシュの出しわけ(Cache KeyとしてHostヘッダの利用)は必要になるかと考えます。(HostヘッダによりCloudFront側でコンテンツを出し分ける、という挙動のため。)

Function associationsはのちほど設定します。ここではいったん設定せずにCloudFrontディストリビューションを作成します。

Alternate domain name (CNAME)でred.example.comblue.example.com、そしてgreen.example.comの3つを指定します。(green.example.comは先に示した図には記載していませんでしたが、「指定以外のドメインでアクセスした場合」の挙動確認用としてのちほど使用します。)またCustom SSL certificateで*.example.comに対応したACM証明書を選択しました。

CloudFrontディストリビューションを作成後、red.example.comblue.example.comgreen.example.com、3つそれぞれのレコードにCloudFrontディストリビューションのドメイン名d12345678.cloudfront.netを登録して名前解決できるようにしておきます。(今回はDNSにRoute 53を利用しているため、ALISレコードで登録しました。)

実際にブラウザで各ドメインにアクセスしてみます。現状はオリジンがred-origin.example.comのみであるため、いずれのドメインでアクセスしてもred-origin.example.comの内容が表示されます。

Hostヘッダでオリジンを切り替えるLambda@Edge関数

続いて今回の構成の肝となる、Hostヘッダの内容によってオリジンを切り替えるLambda@Edge関数を設定していきます。まず関数のコードは以下となります。

index.js

'use strict';

exports.handler = (event, context, callback) => {
    console.log(JSON.stringify(event));  // 動作確認用

    const request = event.Records[0].cf.request;

    if (request.headers['host']) {
        const hostHeader = request.headers.host[0].value;
        console.log('Host Header:' + hostHeader);  // 動作確認用

        if (hostHeader === 'blue.example.com') {
            const domainName = 'blue-origin.example.com';
            request.origin.custom.domainName = domainName;
            console.log('Origin Changed:' + domainName );
        }
    }

    callback(null, request);
};

CloudFrontデベロッパーガイドの関数の例を参考に、Node.jsで記しました。

オリジンリクエストの内容は以下を参考にします。

Hostヘッダの値はRecords[0].cf.request.host[0].valueに格納されているので、それを参照します。この値がblue.example.comであった場合は、オリジン(カスタムオリジン)のドメイン名を示すRecords[0].cf.request.origin.custom.domainNameの値をblue-origin.example.comに書き換えます。request.origin.customの他の要素(ポート番号やプロトコル、タイムアウト指定)なども変更可能なようですが、今回はCloudFrontディストリビューションに設定してある値をそのまま使うことにしました。

このコードの内容を、Lambda@Edge Functionとして設定していきます。今回はLambdaのマネジメントコンソールから設定を行いました。リージョンがUS East (N. Virginia) us-east-1となっていることを確認します。Create functionから進み、function名などを設定します。RuntimeはNode.js 14.xを選択しました。そのほかはデフォルトのまま進めています。

Function作成後、Code sourceのindex.jsに先ほどのコードを貼り付け、[Deploy]ボタンを押します。

続いてIAMロールまわりの設定を行います。Configurationの項目のPermissions、Execution roleでこのFunctionに紐づいているIAMロールを確認します。このIAMロールにアタッチされているマネージドポリシーについて、操作許可対象がus-east-1リージョンに限定しているのでこれを任意のリージョンで許可するよう変更します。またlogs:CreateLogStreamlogs:PutLogEventsの許可対象のResouce ARNについて、出力されるロググループ名に合うように書き換えておきます。

変更前

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:us-east-1:123456789012:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/ChangeOriginDomainByHostHeader:*"
            ]
        }
    ]
}

変更後

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

続いて信頼関係True relationshipsでedgelambda.amazonaws.comがあるかを確認します。ない場合は追加します。

以上2つのIAMロールの設定までできたら、右上の[Actions]から「Deploy to Lambda@Edge」を選択します。

現れたダイアログでCloudFrontトリガの設定を行います。Distributionで先ほど作成したCloudFrontディストリビューションを選択します。Cache behaviorは*とします(Default (*)のBehaviorに適用する想定です。)CloudFront eventではOrigin requestを選択します。Confirm deploy to Lambda@Edgeのチェックボックスにチェックを付け、[Deploy]ボタンを押します。

新しくVersionが発行され、CloudFrontディストリビューションに紐付きます。

CloudFront側でも確認しましょう。(上記Lambdaのマネジメントコンソールからリンクを辿ると、CloudFrontの旧マネジメントコンソールに遷移しました。今回は記念(?)にここだけ、この旧コンソール画面のまま進めます。)該当のディストリビューションのBehavior、パスパターンDefault (*)の内容を確認してみると以下のように、Origin Requestに対するEdge Functionが紐付いていることがわかります。

以上でLambda@Edgeの設定は完了です。

Hostヘッダの内容でオリジンが切り替わることの確認

CloudFrontディストリビューションならびにLambda@Edgeの設定が完了しました。CloudFrontディストリビューションのDeployが完了したことを確認したら、実際にアクセスして挙動を確認してみます。

まずはred.example.comにアクセスします。こちらは先ほどと変わらず赤色のページ、red-origin.example.comからのレスポンスですね。

続いてblue.example.comにアクセスします。Hostヘッダがblue.example.comの場合はblue-origin.example.comをオリジンに使用する設定のため、こちらは青色のページが表示されました。blue-origin.example.comからのレスポンスですね。

最後に、冒頭から考慮していなかったgreen.example.comというHostヘッダでアクセスした場合です。CloudFrontのデフォルトの設定としてはオリジンにred-origin.example.comを使用、Lambda@EdgeのFunctionコードでHostヘッダの値が'blue.example.com'の場合にblue-origin.example.comを使用するようにしていました。ということは、green.example.comでのアクセスの場合はデフォルトのオリジンred-origin.example.comからのレスポンスとなる、ということになります。アクセスした結果も、下記のように赤色のページが表示されました。

まとめ

Amazon CloudFrontにてLambda@Edgeを使い、1つのディストリビューションでHostヘッダの内容によってオリジンを切り替える構成を試してみました。冒頭にも述べたとおり、個人的にはこのようなケースでは2つディストリビューションを用意してアクセスするドメインと使用するオリジンごとにディストリビューションを分けてしまうほうがシンプルになるかなと考えています。とはいえ、何らかの事情にて1つの今回の構成が役に立つこともあるかもしれません。何より今回、Lambda@Edgeの使用を通してCloudFrontのリクエストに対して(そしてレスポンスにも)色々な処理ができることも実感できました。2021年に入りリリースされたCloudFront Functionsとあわせて、ユースケースや実現可能なことなどを抑えておきたいと思いました。