2
4

CloudFront Functions を使ってトラフィックの流量制限を行う

Posted at

はじめに

Web アプリケーションを運用しているときに、特定のタイミングでアクセスが急増することがあります。例えば、音楽のチケット販売サイトで、人気のミュージシャンのライブイベントが販売開始ににあるタイミングなどがあります。対処方法は色々考えられますが、一つの選択肢として以下の対応が可能です。

  • 既にログイン済みのユーザーは、購入処理を進める
  • ログインしていないユーザーのアクセスを一時的に停止する
  • (チケットを販売開始するときには、早い者勝ちではなくて、抽選方式にする方が、平等に機会提供ができて望ましいという側面もあります。)

上記の方式を、CloudFront Functions で実現が可能です。この記事では、この作成手順を紹介していきます。

概要図

CloudFront Functions で、ログイン済みのユーザー or 新規のユーザーを判断します。判断の方法は、以下の通りです。

  • 特定の Cookie が有る場合 : ログイン済みユーザー
  • 特定の Cookie が無い場合 : ログインしていない新規ユーザー

image-20240831172410295.png

特定の Cookie が無い場合は、Sorry ページを Hosting している S3 上の Sorry ページを表示します。

Cookie はユーザー側でいくらでも作れますが、この抜け道を知っているユーザーは一部に限られるはずで、大多数のユーザーをブロックできる効果を期待できます。

それでは環境の構築方法を紹介します。

オリジンの準備 : S3 で React をホスト

適当なサンプルアプリケーションを React で作成して、S3 に格納して動かします。手順の詳細は本題ではないので、説明せずにコマンドの羅列にとどめます。

cd ~/temp/reactdir/
npx create-react-app cloudfront-functions-traffic-control
npm start

App.js のサンプルプログラム例

import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  const [cookieValue, setCookieValue] = useState('');

  useEffect(() => {
    // コンポーネントマウント時に既存のCookieを読み取る
    const existingCookie = document.cookie.split('; ').find(row => row.startsWith('sampleCookie='));
    if (existingCookie) {
      setCookieValue(existingCookie.split('=')[1]);
    }
  }, []);

  const issueCookie = () => {
    // 現在の日時を日本時間の文字列として取得
    const value = new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' });

    // Cookieを設定
    document.cookie = `sampleCookie=${encodeURIComponent(value)}; path=/; max-age=3600`;

    // 状態を更新して新しいCookie値を反映
    setCookieValue(value);
  };

  const deleteCookie = () => {
    document.cookie = 'sampleCookie=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
    setCookieValue('');
  };

  return (
    <div className="App">
      <header className="App-header">
        <h1>CloudFront Functions 用のサンプルアプリ</h1>
        <div className="button-container">
          <button className="btn btn-issue" onClick={issueCookie}>Issue Cookie</button>
          <button className="btn btn-delete" onClick={deleteCookie}>Delete Cookie</button>
        </div>
        <p className="cookie-value">Current Cookie Value: {cookieValue || 'No cookie set'}</p>
      </header>
    </div>
  );
}

export default App;

こんな感じで、Cookie を発行や削除するボタンが置いてあるだけのシンプルなアプリです。発行する Cookie の名前は sampleCookie としています。実際には、利用している Web アプリの認証に関する Cookie 、みたいな位置づけで理解していただいて大丈夫です。

image-20240831163648378.png

ビルドします。

npm run build

バケットを作成して、ビルド結果を格納。 (いまさらですが、mb コマンドは、make bucket の略らしき点に気が付きました。)

aws s3 mb s3://cloudfront-functions-traffic-control01/
aws s3 sync build/ s3://cloudfront-functions-traffic-control01/

オリジンの準備 : Sorry ページ

同様に、Sorry ページ用の S3 を用意します。

npx create-react-app cloudfront-functions-traffic-control-sorry

App.js のソースコードです。いい感じな Sorry ページを用意します。

import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>Sorry, We're Currently Unavailable</h1>
        <p>
          ご不便をおかけして申し訳ございません。
        </p>
        <p>
          現在、メンテナンスまたは技術的な問題により、一時的にウェブサイトをご利用いただけない状況です。
        </p>
        <p>
          お手数ですが、しばらく時間をおいてから再度アクセスをお試しください。
        </p>
      </header>
    </div>
  );
}

export default App;

image-20240831172751094.png

ブラウザからアクセスするときの URI は、/sorry/hogehoge としたいので、package.json の homepage に /sorry と指定します。

{
  "name": "cloudfront-functions-traffic-control-sorry",
  "version": "0.1.0",
  "homepage": "/sorry",

ビルドします。

npm run build

バケットを作成して、ビルド結果を格納

  • 格納先は、バケットに /sorry/ と一段階 Path を深くして格納する。CloudFront の Path Pattern を一致させるため。
aws s3 mb s3://cloudfront-functions-traffic-control01-sorry/
aws s3 sync build/ s3://cloudfront-functions-traffic-control01-sorry/sorry/

CloudFront Distribution の作成

ここからが本題です。まずは、CloudFront Distribution を作成して、S3 と連携します。

image-20240831165125548.png

以下のパラメータを指定します。画像は長いですが、あまり関係ないので飛ばして大丈夫です。

  • 名前の指定
  • OAC を利用
  • CloudFront Function は後で指定する
  • Default root object を指定

image-20240831165616845.png

Distribution が作成されました。Policy の Copy しましょう。

image-20240831165755797.png

コピーした Policy を S3 Bucket の Permissions に指定します。

image-20240831170113836.png

作成した CloudFront の URL を開くと、React で作成したサンプルアプリケーションが表示されます。

image-20240831170257507.png

CloudFront で Sorry 用の Origin と Path を追加

CloudFront に、Sorry 用の Origin を追加します。

image-20240831174244725.png

Sorry 用の Origin を指定します。

image-20240831174455066.png

コピーした Policy を S3 Bucket に設定します。

image-20240831174620462.png

Create behavior を押して、新たな Path pattern を定義します。

image-20240831190609952.png

以下のパラメータで指定します。

image-20240831191013689.png

以下の URL にアクセスすることで、sorry ページを出力できました。

image-20240831192841653.png

CloudFront Functions を作成

CloudFront Functions を作成します。これが、CloudFront のエッジロケーションに展開されます。

image-20240831202836928.png

適当な名前を入れて作成します。

image-20240831202902128.png

CloudFront Functions は、JavaScript で記載をします。以下にサンプルソースコードを記載します。

動作のポイントを以下に記載します。

  • originAcceptingTraffictrue の場合、全てのアクセスを許可する
  • originAcceptingTrafficfalse の場合、流量制限として新規アクセスのユーザーのみ、Sorry ページを表示する
  • 具体的には、ブラウザから送信される Cookie を確認して、sampleCookie が無ければ、新規ユーザーとして判断する
  • 新規ユーザーの場合は、リクエストの uri を /sorry/index.html に書き換えることで、Sorry ページの表示を強制する

originAcceptingTraffic は、まずは true でデプロイを行います。

function handler(event) {
    // event を logging
    console.log("event : " + JSON.stringify(event));

    // フラグをハードコードします。このフラグは、CloudFront Functions の更新によって変更されます。
    var originAcceptingTraffic = true;

    var request = event.request;

    // Cookieの有無を確認
    var hasCookie = !!event.request.cookies.sampleCookie;

    console.log("hasCookie : " + hasCookie);

    if (!originAcceptingTraffic && !hasCookie) {
        // オリジンがトラフィックを受け付けておらず、新規ユーザー(Cookieなし)の場合は、uri を Sorryページに書き換える
        console.log("New connection was internally rewritten to the Sorry Page because it doesn't have sampleCookie.");
        request.uri = "/sorry/index.html";
    }

    // それ以外の場合は通常のリクエストを続行
    return request;
}

保存

image-20240831213432601.png

以下でテストが可能です。Cookie を入れることも可能です。

image-20240831212029399.png

Publish を押します。

image-20240831212427912.png

Function と Distribution を紐づける

作成した CloudFront Function と Distribution を紐づけます。

Add association を押します。

image-20240831212634807.png

紐づけをします。

image-20240831212704969.png

実行確認

アクセスすると、普通に Web ページを表示できます。Delete Cookie を押して、Cookie を持っていない状態にします。

image-20240831213043332.png

originAcceptingTraffic を false にして Save Changes を押します。

image-20240831213535355.png

Publish function を押します。

image-20240831213618906.png

想定通り、Sorry ページが表示されます。URL はトップページですが、内部で /sorry/index.html に書き換えられていることがわかります。

image-20240831213644016.png

今度は、originAcceptingTraffictrue にしたあとに、Cookie を発行しておきます。これは、ログイン済みの既存ユーザーという扱いになります。

image-20240831213847598.png

Developer Tools で実際の Cookie が有ることを確認できます。

image-20240831213921522.png

その後、originAcceptingTrafficfalse に変更して、Publish をします。

image-20240831213959810.png

Cookie を持っているので、正常に表示できます。では、Delete Cookie を押してブラウザを更新してみると、、、

image-20240831214104578.png

Sorry ぺージが表示されました。想定どおりです!

image-20240831214127979.png

検証してみてわかった Tips

  • この記事の環境の場合、CloudFront Functions をアップデートしたときに、反映まで約 7 秒ほどの短時間で反映された。結構はやい。

参考 URL

https://aws.amazon.com/jp/blogs/news/visitor-prioritization-by-cloudfront-functions/
https://aws.amazon.com/jp/blogs/news/visitor-prioritization-by-cloudfront-functions-part2/

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4