CloudFront を使用したマルチホスティングと、CloudFront Functions を活用した動的ルートディレクトリ書き換えを試してみた。

記事タイトルとURLをコピーする

概要図:

マルチホスティング

マルチホスティングとは、一つのサーバーで複数のウェブサイトを運営できるシステムを指します。これは、各ウェブサイトを異なるサーバーで運営するよりもコストを抑えられ、一つのサーバーだけを管理すれば良いため便利です。ただし、一つのウェブサイトに訪問者が増えると、そのサーバー上の他のウェブサイトに影響が出る可能性があります。また、サーバーにメンテナンスが必要な場合、そのサーバーで運営している全てのウェブサイトが影響を受けることになります。

Server Name Indication ( SNI )

一つのサーバーで複数のウェブサイトを運営する際、そのサーバー内には各ウェブサイトに対応する複数の証明書が存在します。Server Name Indication(SNI)という技術を用いることで、ウェブブラウザは接続開始時に、どのウェブサイトにアクセスしようとしているか(ホスト名)をサーバーに伝えることができます。 サーバーはこの情報をもとに、適切な証明書を選択して提供します。この結果、一つのサーバー上で複数のウェブサイトを運営する場合でも、各ウェブサイトごとに異なる証明書を使用することが可能になります。これにより、ウェブサイトの安全性を確保しつつ、複数のウェブサイトを効率的に運営することが可能となります。

CloudFront も SNI に対応しています。訪問者がウェブブラウザから CloudFront のホストするウェブサイトのうちの 1 つにアクセスする際、CloudFront は適切なセキュリティ証明書を選びます。そして、訪問者とウェブサイト間の安全な接続を確立します

詳しくはこちらの公式ドキュメントをご覧ください:Choosing how CloudFront serves HTTPS requests - Amazon CloudFront

本記事の検証内容

1. CloudFront を使用したマルチホスティング

この記事では、私がテスト用に Route 53 で取得した2つのドメイン名を使って、複数のウェブサイトを一つのサーバーで運営する(マルチホスティング)環境を作ります。そのために、CloudFront の設定を変更します。 まず、一つのCloudFront 配信設定に対して、以下の2つのドメイン名を設定します。それぞれのドメイン名に対して、対応するセキュリティ証明書もCloudFrontに設定します。

  1. test.yamaaazon.com
  2. test.karukozaka46.click

次に、これらのドメイン名が指す元のサーバー(「オリジン」と呼ばれます)として、Application Load Balancer を設定し以下のレコードを設定します

  1. origin.yamaaazon.com

この設定により、上記の2つのドメイン名どちらからアクセスしても、結果は同じ「オリジン」のサーバーにアクセスすることになります。これは、まるで「分身の術」のように一つのサーバーが複数のウェブサイトとして振る舞うことを可能にします。

2. CloudFront Functionsを活用した動的ルートディレクトリ書き換え

「CloudFront Functions」とは、CloudFront で提供される機能の一つで、これを使うと、ユーザーからのリクエスト内容に基づいてウェブページのURLパスを書き換えたり、ユーザーを他のURLにリダイレクトしたりすることができます
今回のテストでは、オリジン(元のサーバー)となるALB(Application Load Balancer)で、各ホスト(ウェブサイト)向けに専用のパスを設定します。そして、ユーザーがアクセスしたホスト名によって、アクセスさせるルートディレクトリ(ドキュメントの保存場所)を書き換えます。 例えば、

  • ユーザーが「test.yamaaazon.com」にアクセスすると、オリジンである「origin.yamaaazon.com」のルートディレクトリ("/")にアクセスします。
  • 一方、ユーザーが「test.karukozaka46.click」にアクセスすると、「origin.yamaaazon.com」の「/karukozaka」ディレクトリにアクセスします。

まるで「変身の術」のように、ホスト名に応じてアクセスするドキュメントルートを変えることで、複数のウェブサイトを同じサーバーで運営することができます

詳しくはこちらの公式ドキュメントをご覧ください: CloudFront Functionsによるエッジでのカスタマイズ

詳細な構成図

  1. AWS Certificate Manager (ACM)を使用して、バージニア北部リージョンでマルチドメイン証明書を作成します。
  2. CloudFront配信設定の「代替ドメイン名 (CNAME)」欄に2つのドメイン名を入力します。また、「カスタム SSL 証明書」欄にマルチドメイン証明書を設定します。その後、同じ2つのドメイン名のレコードをRoute 53で作成します。
  3. Route 53で、東京リージョンのALBのレコードを作成します。
  4. 東京リージョンのAWS Certificate Manager (ACM)を使って新たな証明書を作成し、これを東京リージョンのALBのリスナールールに関連付けます(上の構成図では省略しています)。
  5. CloudFront配信設定のオリジンに東京リージョンのALBを設定し、HTTPSで通信するように設定します。
  6. EC2をALBに関連付けます。
  7. EC2上にウェブサーバーを作成し、それぞれのホスト(ウェブサイト)向けに専用のパスを設定します。
  8. CloudFront Functionsを設定し、それをCloudFront配信設定に関連付けます。

これらのステップにより、マルチドメイン証明書の作成からウェブサーバーの設定まで、マルチホスティングに必要な一連の作業を行うことができます。

検証

これから先はサービス毎の設定をしていきます。

1. AWS Certificate Manager (ACM)を使用して、バージニア北部リージョンでマルチドメイン証明書を作成します。

このテストを実施するために作成する証明書が対象とするドメインは次の通りです。

  1. test.yamaaazon.com
  2. test.karukozaka46.click

将来的にホストが増える可能性を考慮し、全てのサブドメインをカバーできるワイルドカード証明書を選択しました。

  1. *.yamaaazon.com("yamaaazon.com" の全てのサブドメインを意味します。)
  2. *.karukozaka46.click("karukozaka46.click" の全てのサブドメインを意味します。)

これにより、新たなサブドメインが追加されたとしても、証明書の更新が不要になります。

証明書の検証方法として「DNS 検証」を選びました。この方法は、DNS サーバーにCNAMEレコードを追加し、そのレコードを通じて証明書の検証を行います。

DNS サーバー (私の場合は Route 53 のホストゾーン)にCNAMEレコードを追加すると、「発行済み」になりました。

次の手順で使うため、証明書の「識別子」をメモしておいてください。

2. CloudFront配信設定の「代替ドメイン名 (CNAME)」欄に2つのドメイン名を入力します。また、「カスタム SSL 証明書」にマルチドメイン証明書を設定します。その後、同じ2つのドメイン名のレコードをRoute 53で作成します。

CloudFront配信設定の「代替ドメイン名 (CNAME)」欄に2つのドメイン名を入力します。
また、「カスタム SSL 証明書」欄にマルチドメイン証明書を設定します。先ほどの手順でメモしておいた証明書の「識別子」を選択してください。

2つのドメイン名それぞれに対して、Route 53でレコードを作成します。その際、Route 53の「エイリアスレコード」機能を利用し、対応するCloudFront配信設定をドロップダウンリストから選択します。

  • test.yamaaazon.com

  • test.karukozaka46.click

3. Route 53で、東京リージョンのALBのレコードを作成します。

Route 53で ALB 用のレコードを作成します。その際、Route 53の「エイリアスレコード」機能を利用し、対応する ALB をドロップダウンリストから選択します。

4. 東京リージョンのAWS Certificate Manager (ACM)を使って新たな証明書を作成し、これを東京リージョンのALBのリスナールールに関連付けます(上の構成図では省略しています)。

東京リージョンのAWS Certificate Manager (ACM)を使って新たな証明書を作成します。

東京リージョンのALBのリスナールールに関連付けます。構築方法の詳細は割愛します。

エンドユーザーがオリジンのALBに直接アクセスすることを防ぐために、ALB は CloudFront からの通信のみを許可する設定にすると良いでしょう。この設定方法の詳細は、下記の記事をご覧ください。
CloudFront が AWS-managed prefix list に対応しました - サーバーワークスエンジニアブログ

5. CloudFront配信設定のオリジンに東京リージョンのALBを設定し、HTTPSで通信するように設定します。

"origin.yamaaazon.com" を入力し、このドメインに「HTTPSのみ」で通信することを許可します。

6. EC2をALBに関連付けます。

構築方法の詳細は割愛します。

7. EC2上にウェブサーバーを作成し、それぞれのホスト(ウェブサイト)向けに専用のパスを設定します。

構築方法の詳細は割愛します。
"/"にアクセスすると、画面には"hello"と表示されます。 また、"/karukozaka/karukozaka.html"にアクセスすると、"karukoza is here"と表示されます。

これは、EC2上でcurlコマンドを使用して行った確認の結果を表示した画面です。

8. CloudFront Functionsを設定し、それをCloudFront配信設定に関連付けます。

「関数コード」を記述し、「変更を保存」を押して保存します。

  • コード
function handler(event) {

    // クライアント IP アドレスに関する変数定義
    var clientIp = event.viewer.ip;
        // test.karukozaka46.click に接続を許可するクライアント IP アドレス
        var test_karukozaka_allowIps = ['106.156.155.231', '111.111.111.111'];
        var test_yamaaazon_allowIps = ['106.156.155.231', '222.222.222.222'];

    // host ヘッダに関する変数定義
    var request = event.request;
    var headers = request.headers;
    var host = headers.host ? headers.host.value : "";
    
    // URI に関する変数定義
    var Uri = request.uri;
    var newUri;

    // host ヘッダが test.karukozaka46.click の場合
    if (host === "test.karukozaka46.click") {

        // クライアント IP アドレス判定
        if (test_karukozaka_allowIps.includes(clientIp)) {

            // host ヘッダが一致した場合、URIを書き換え
            // test.karukozaka46.click/karukozaka.html にアクセスすると、
            // test.karukozaka46.click/karukozaka/karukozaka.html にアクセスする。オリジンの /karukozaka/ 配下にアクセスする。
            newUri = request.uri.replace(/^\//, "/karukozaka/");
            request.uri = newUri;
            return request;
        
        }
    }
    
    // host ヘッダが test.yamaaazon.com の場合
    if (host === "test.yamaaazon.com") {

        // IP アドレス判定
        if (test_yamaaazon_allowIps.includes(clientIp)) {

            // /karukozaka 配下のコンテンツにアクセスしないようにリダイレクトする。/karukozaka 以外の複数のパスも定義出来るようにリストにする。
            var redirectPaths = ["/karukozaka", "/kagurazaka"];
            // リストの中にあるパスへの接続を / にリダイレクトする。
            for (var j = 0; j < redirectPaths.length; j++) {
                if (request.uri.startsWith(redirectPaths[j])) {
                    return {
                        statusCode: 302,
                        statusDescription: 'Found',
                        headers: {
                            'location': { value: '/' }
                        },
                    }
                }
            }
            // それ以外のリクエストはそのまま処理する。
            return request;
        }
    }

    // 上に定義した host ヘッダ以外の場合、403 を返す処理。
    return {
        statusCode: 403, //302
        statusDescription: 'Forbidden', //found
        headers: {
            'cloudfront-functions': { value: 'generated-by-CloudFront-Functions' },
            //'location': { value: 'https://aws.amazon.com/cloudfront/' }
        },
            body: 'Access denied for your IP address.'
    };
}

以下は、コードの概要です。
まず、各ウェブサイトが許可する接続元のパブリック IP アドレスがそれぞれ異なる場合でも対応できるように、あらかじめ IP アドレスのリストを配列として作成します。そして、接続が許可されているIPアドレスかどうかをこの配列を使って判断します。
具体的には、「test.yamaaazon.com」の許可 IP アドレスは配列「test_yamaaazon_allowIps」に、また「test.karukozaka46.click」の許可 IP アドレスは配列「test_karukozaka_allowIps」にそれぞれ定義します。
このコードを使用する理由は、私自身のパブリック IP アドレスからの接続だけをCloudFrontで許可したかったからです。開発環境向けのコードです。

  1. 最初に、ホストヘッダを確認し、それが "test.karukozaka46.click" または "test.yamaaazon.com" のいずれかと一致するかをチェックします。
  2. もしホストヘッダが "test.karukozaka46.click" と一致し、さらにクライアントの IP アドレスが許可リスト「test_karukozaka_allowIps」に含まれている場合、リクエストの URI を変更します。具体的には、"/karukozaka.html" へのアクセスが "/karukozaka/karukozaka.html" へのアクセスに変わります。
  3. 一方、ホストヘッダが "test.yamaaazon.com" と一致し、クライアントの IP アドレスが許可リスト「test_yamaaazon_allowIps」に含まれている場合、リクエストの URI は変更されず、そのままとなります。ただし、"/karukozaka" や "/kagurazaka" など特定のパスへのアクセスはルート("/")にリダイレクトされます。
  4. 上記の条件に一致しないリクエストは、403 エラー(アクセス禁止)を返します。その際、応答ヘッダには 'cloudfront-functions' というフィールドが含まれ、その値は 'generated-by-CloudFront-Functions' と表示されます。また、応答ボディには 'Access denied for your IP address.' というメッセージが表示されます。

作成した関数を、CloudFront の配信設定の中の「ビヘイビア」セクションに関連付けます。

「関数を発行」ボタンをクリックすることで、作成した関数が実際に動作するように設定します。

【補足1】CloudFront Functions の制約

主な制約です。

  • コードサイズ: 関数のコードサイズは最大1MBでなければなりません。
  • 実行時間: 関数の実行時間は最大1msです。
    • 公式の資料に「サブミリ秒」って書いてあるのが結構ツボでした。マラソンタイムのサブスリー( 3 時間切り)みたいですね。
  • 最大メモリ: 2 MB
  • 関数コードと含まれるライブラリの最大サイズ: 10 KB

詳細は公式ドキュメントも参照ください。

CloudFront Functions と Lambda@Edge の選択 - Amazon CloudFront

【補足2】関数のテスト

「テスト」タブでは 関数をテスト実行できます。
実行の際にリクエスト IP アドレス、HTTPメソッド、URLパス、ヘッダ、 Cookie、クエリ文字列などを指定できます。

テスト結果には、ステータスコード、出力、「コンピューティング使用率」が出ます。

「コンピューティング使用率」に関するマネジメントコンソールの中にある説明文です。

コンピューティング使用率は、関数の実行にかかった時間 (最大許容時間に対するパーセンテージ) です。例えば、35 の値は、関数が最大許容時間の 35% で完了したことを意味します。

関数が最大許容時間を継続的に超えている場合、CloudFront は関数のスロットリングを行います。次のリストは、計算使用率の値に基づいて関数のスロットリングが行われる可能性を説明しています。

コンピューティング使用率の値:

1 ~ 50 — この機能は最大許容時間を快適に下回っており、スロットリングなしで実行されます。

51 ~ 70 — 関数は最大許容時間に近づいています。関数コードを最適化することを検討してください。

71 ~ 100 — 関数が最大許容時間に非常に近いか、それを超えています。ディストリビューションに関連付けた場合、CloudFront は、この関数をスロットリングする可能性があります。

動作確認

「test.yamaaazon.com」にアクセスすると、オリジンサーバーである「origin.yamaaazon.com」のルートパス("/")の内容が表示されます。

「test.karukozaka46.click/karukozaka.html」にアクセスすると、オリジンサーバーである「origin.yamaaazon.com」の"/karukozaka/karukozaka.html" の内容が表示されます。

「test.yamaaazon.com/karukozaka/karukozaka.html」にアクセスすると、オリジンサーバーである「origin.yamaaazon.com」のルートパス("/")にリダイレクトされます。

接続が許可されていない パブリック IP アドレスからサイトにアクセスすると、403 エラー(アクセス禁止)が返ってきました。

その他

本記事で取り上げた構成を応用した、異なる構成も考えられます。
以下にその一例を示します。まだ具体的な検証は行っていませんので、案として示します。

(1) 特定の外部サービスにリダイレクトするリバースプロキシとしてWEBサーバーを運用する方法です。これにより、特定のパスごとに異なる外部サービスにユーザーをリダイレクトさせることが可能になります。この設定では、プライベートサブネットに配置したWEBサーバーが、パブリックサブネットのNATゲートウェイを経由して外部サービスと通信します。NATゲートウェイのIPアドレスを送信元のパブリックIPアドレスとして固定することで、外部サービス側でIPアドレスに基づいたアクセス制限を設けることもできます。

(2) CloudFront に複数のCNAME(異なるホスト名)を設定し、それぞれ異なるEC2に対応させるという方法です。CloudFrontは各リクエストのHostヘッダー(どのホスト名がリクエストされたか)をオリジン(ALB)に転送します。そして、ALB(Application Load Balancer)はそのHostヘッダーを元にリクエストを適切なEC2にルーティングします。この方法の利点は、パフォーマンスの高いマネージドサービス(CloudFrontやALB)を一つにまとめて効率的に管理できる一方で、それぞれのウェブサイトに合わせてEC2を個別に設定できる点です。

まとめ

CloudFront を使用したマルチホスティングと、CloudFront Functions を活用した動的ルートディレクトリ書き換えを試してみました。
サーバーを集約してコストダウンしたいケースなどに使えそうです。
検討する際には本記事(巻物)を参考にしてみてください。

※2024/3/7 15:50「補足~」と「その他」を更新(追加)しました。

余談

雪が降ったので、三ッ峠山に登りました。
富士山や南アルプスの美しい景色を楽しむことができました。さらに、私の故郷にある赤石岳や悪沢岳も見ることができて、とても嬉しかったです。ニンニン。

山本 哲也 (記事一覧)

カスタマーサクセス部のエンジニア(一応)

好きなサービス:ECS、ALB

趣味:トレラン、登山(たまに)