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

# はじめに

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

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



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



# 概要図

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

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

![image-20240831172410295.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/cc49a9de-4228-8ea2-a59f-7a413cfadfb4.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` のサンプルプログラム例

```react
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/17b698bb-4f63-7f92-08b2-35c5c48f41c5.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 ページを用意します。

```react
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/cd4a0231-96d9-db5f-7a18-93e21e2b213a.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/988d388e-3ec2-a7fd-efd3-e0ce0476d3f4.png)



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

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

![image-20240831165616845.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/89c3689a-2627-b646-4f66-846d96c4ebdb.png)



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

![image-20240831165755797.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/ec2cb208-f3c7-40d5-b2ef-505fe62be89b.png)



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

![image-20240831170113836.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/13b82568-642f-b2dd-64aa-2dea32542375.png)



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

https://d2c78fvlo67l5x.cloudfront.net/

![image-20240831170257507.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/4bf1fddc-d154-4b0f-a589-d258d9407004.png)



# CloudFront で Sorry 用の Origin と Path を追加

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

![image-20240831174244725.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/9376d37e-216b-f206-57a1-099c98fd88de.png)



Sorry 用の Origin を指定します。

![image-20240831174455066.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/094fd958-55ce-a1df-0187-dcf6c844f017.png)



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

![image-20240831174620462.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/4e979225-34b1-d516-8828-80442c65712e.png)



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

![image-20240831190609952.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/89e48acd-4aee-6ca4-595a-3833336ceab4.png)



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

![image-20240831191013689.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/71d1c705-836d-0251-d387-131e1bfda732.png)



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

https://d2c78fvlo67l5x.cloudfront.net/sorry/index.html

![image-20240831192841653.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/4408add1-62b3-30fd-f09c-1423a3f62d48.png)



# CloudFront Functions を作成

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

![image-20240831202836928.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/6a573808-3c8e-8194-0364-2255ce65563e.png)



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

![image-20240831202902128.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/7ce504c0-b52c-a053-a99e-2d34dd93d8de.png)



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

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

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

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

```javascript
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/fa1689fd-b5e7-047b-93fa-c8402e16e5fe.png)



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

![image-20240831212029399.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/5887d8f5-77ec-334f-e545-02b561d664ec.png)



Publish を押します。

![image-20240831212427912.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/a1bf357c-333e-3b36-26af-1552cc9ddd0b.png)



# Function と Distribution を紐づける

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

Add association を押します。

![image-20240831212634807.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/74a2a45d-101e-4b39-5902-67bfb72ef246.png)



紐づけをします。

![image-20240831212704969.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/235eea80-978f-f541-8a66-b8b14485920c.png)



# 実行確認

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

![image-20240831213043332.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/415e1741-9ad8-20af-d58e-2b5a22fd3a81.png)



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

![image-20240831213535355.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/fc074e3e-dac8-80e4-7a2e-17199728cb22.png)



Publish function を押します。

![image-20240831213618906.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/3ef6b4fb-e793-9f54-e8b4-b5d3d7a65933.png)



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

https://d2c78fvlo67l5x.cloudfront.net/

![image-20240831213644016.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/b4acd03e-f949-4b51-3e84-0f89e174c72c.png)



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

![image-20240831213847598.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/8bd59701-a7e2-e846-a20e-65110189fb47.png)



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

![image-20240831213921522.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/78757a17-e4eb-29d6-ad63-46fc9232b59b.png)



その後、`originAcceptingTraffic` を `false` に変更して、Publish をします。

![image-20240831213959810.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/8c0a8942-1b77-bc4f-ca03-4d4c3ef56a69.png)



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

![image-20240831214104578.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/6166b2ab-b1ec-d2b9-a443-1d9c953d082b.png)



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

![image-20240831214127979.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/3994ff17-34c6-20bc-8086-1a59d18db2ff.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/