📄

【AWS】CloudFrontでWebアプリの画面とAPIのキャッシュ取ってみた

2023/11/03に公開

概要

会社で AWS を触ることになり、基本から学んでいこうと思ったため備忘録として記事を書き始めました。
今回は CloudFront を利用して S3+APIGateway+Lambda で構築した Web アプリの画面と API のキャッシュを取ってみます。
もし理解が違うよというところ等ありましたら優しく教えて頂けると幸いです 🙇‍♀️

CloudFront とは

公式ドキュメント引用。

CloudFront とは、html・css・js・画像・json ファイル等のキャッシュを取りユーザーにファイルを速く届けるためのサービスです。
下記の画像をご覧ください。

上記のようにオリジンと呼ばれるファイルが配信されるサーバーがあります。
このサーバーから CloudFront は以下のようにキャッシュを取り、ユーザーにファイルを速く届けています。

  1. 開発者が CloudFront でディストリビューションと呼ばれるキャッシュの取り方を設定する物を作成する。
  2. 設定したディストリビューションが全国のエッジロケーションに反映される。
    エッジロケーションとは全国に散らばったサーバーで、ユーザーが一番近いエッジロケーションにアクセスすることでオリジンにアクセスするより速くファイルを取得することができる。
  3. ユーザー 1 が一番近いエッジロケーションにアクセスする。ディストリビューション作成時に払い出された URL にアクセスすると、自動で一番近いエッジロケーションにリクエストを送ってくれる。
  4. ユーザー 1 のリクエストで指定したファイルがエッジロケーションになかった場合、オリジンにリクエストがとぶ。オリジンからエッジロケーションに対してファイルを返す。この時、ファイルが格納されていなかったらエッジロケーションにファイルが格納される。これでキャッシュが取れたことになる。
  5. エッジロケーションからユーザーに対してファイルが返される。
  6. ユーザー 2 がユーザー 1 と同じファイルをリクエストするために一番近いエッジロケーションにアクセスする。この時、先ほどキャッシュされたファイルがエッジロケーションに格納されているので、ユーザー 2 はキャッシュされたファイルを取得することができ、より速くファイルを取得することができる。以降別ユーザーがアクセスした場合も、ユーザー 2 と同じように離れたオリジンにアクセスするよりも速くファイルを取得することができる。

CloudFront で Web アプリの画面と API のキャッシュを取る

では、S3+APIGateway+Lambda で構築した Web アプリの画面と API のキャッシュを CloudFront で取得するハンズオンを行ってみます 💃
ここでは、以下のような構成で Web アプリケーションを作成します。

黒い連番の数字で書かれているのは CloudFront で Web アプリの画面と API のキャッシュを取る手順です。
以下手順が各数字と対応します。

  1. ユーザーが画面用ディストリビューションから払い出された URL を通してエッジロケーションにアクセスする。
  2. キャッシュが取れていなかった場合エッジロケーションが S3 にアクセスし、画面用ファイル(html、css、js 等)を取得してエッジロケーションにキャッシュを取ってユーザーにファイルを配信する。
  3. ユーザーが API 用ディストリビューションから払い出された URL を通してエッジロケーションにアクセスする。
  4. キャッシュが取れていなかった場合エッジロケーションが API Gateway にアクセスし、API Gateway が指定されたパスに応じて Lambda 関数を実行する。
  5. クエリパラメーターの値によってレスポンスが変わる Lambda 関数が実行され、API のレスポンスファイル(json)を API Gateway を通じてエッジロケーションに返す。
    この時 エッジロケーションにキャッシュを取ってユーザーにファイルを配信する。
  6. リクエストヘッダーの値によってレスポンスが変わる Lambda 関数が実行され 5 と同じ手順でユーザーにレスポンスが返される。
  7. Cookie の値によってレスポンスが変わる Lambda 関数が実行され 5 と同じ手順でユーザーにレスポンスが返される。

では、上記 Web アプリを作成していきます。

  1. APIGateway + Lambda で API を作成する

まず、クエリパラメーターの値によってレスポンスが変わる Lambda 関数getMessageFromQueryParamを作成します。
マネジメントコンソールで AWS Lambda を開き、関数の作成を押下してください。
関数の作成画面で以下項目を入力して関数の作成ボタンを押下してください。

*1

コードソースに以下を入力して Deploy を押下してください。

*2

export const handler = async (event) => {
  const queryParam = event.queryStringParameters.queryParam;

  const response = {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
    body: JSON.stringify({
      message: `クエリパラメーターは${queryParam}です`,
    }),
  };
  return response;
};

次にリクエストヘッダーの値によってレスポンスが変わる Lambda 関数getMessageFromReqHeader、Cookie の値によってレスポンスが変わる Lambda 関数getMessageFromCookieを作成します。
上記文章の中で米印がついた数字の箇所を各々下の対応する数字の入力項目と読み替えてください。

  • getMessageFromReqHeader 関数

*1

関数名を getMessageFromReqHeader に変える

*2

export const handler = async (event) => {
  const header = event.headers;

  const response = {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Headers": "*",
      "Access-Control-Allow-Origin": "*",
    },
    body: JSON.stringify({
      message: `ヘッダーは${header["x-header"]}です`,
    }),
  };
  return response;
};
  • getMessageFromCookie 関数

*1

関数名を getMessageFromCookie に変える

*2

export const handler = async (event) => {
  const cookie = event.multiValueHeaders.cookies[0];

  const response = {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Headers": "*",
      "Access-Control-Allow-Origin": "*",
    },
    body: JSON.stringify({
      message: `クッキーは${cookie}です`,
    }),
  };
  return response;
};

次に、API Gateway で上記 Lambda 関数を実行する REST API を作成します。
マネジメントコンソールで API Gateway を開き、API を作成を押下してください。
API タイプを選択で REST API の構築ボタンを押下してください。
REST API を作成画面で以下項目を入力して API を作成ボタンを押下してください。

リソース画面でリソースを作成ボタン押下してリソースを作成画面に遷移してください。

リソースを作成画面で以下項目を入力し、リソースを作成ボタンを押下してください。

*1

リソース画面で/query-param*2 を押下し、青色の表示にさせた後、メソッドを作成ボタンを押下してください。

メソッドを作成画面で以下項目を入力し、メソッドを作成ボタンを押下してください。

*3

次に Lambda 関数getMessageFromReqHeadergetMessageFromCookieをこの API に紐付けます。
上記文章の中で米印がついた数字の箇所を各々下の対応する数字の入力項目と読み替えてください。

  • header

*1

リソース名を header に変える
CORS(クロスリソースオリジン共有)にチェックを入れる

*2

パスは/header

*3

Lambda 関数はgetMessageFromReqHeader

  • cookie

*1

リソース名を cookie に変える
CORS(クロスリソースオリジン共有)にチェックを入れる

*2

パスは/cookie

*3

Lambda 関数はgetMessageFromCookie

次に、リソース画面で/headerパスを選択し、CORS を有効にするボタンを押下してください。

CORS を有効にする画面で以下項目を入力し、保存ボタンを押下してください。

上記 CORS を有効にする処理は/cookieパスに対しても同様に行ってください。
最後にリソース画面で API をデプロイを押下して API をデプロイしてください。

  1. CloudFront で API のキャッシュを取る

CloudFront で API のキャッシュを取る設定を行います。
マネジメントコンソールで CloudFront を開き、ディストリビューションを作成ボタンを押下してください。
ディストリビューションを作成画面で以下のように入力し、ディストリビューションを作成ボタンを押下、ディストリビューションを作成してください。

キャッシュポリシーは Create cache policy を押下してキャッシュポリシーを作成画面を開き、以下項目を入力してポリシーを作成後、cache-api-policyポリシーを指定してください。

これで API のキャッシュを取る準備が整いました。
API はこの後実装する画面から叩いてキャッシュの挙動を見るので、先に S3 に画面用ファイルを格納して Web 上に公開します。

  1. S3 に画面用ファイルを格納する

次に、画面用ファイル(html、css、js)を作成して S3 に格納します。
画面は Nextjs で作成して画面用ファイルを出力させます。

以下コマンド実行して Next アプリケーションを作成してください。
途中の質問には以下のように答えてください。

$ npx create-next-app@13
...
Need to install the following packages:
  create-next-app
Ok to proceed? (y) y
✔ What is your project named? … cf-front
✔ Would you like to use TypeScript with this project? … Yes
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … No
✔ Would you like to use `src/` directory with this project? … Yes
✔ Would you like to use experimental `app/` directory with this project? … No
✔ What import alias would you like configured? … @/*
...

cf-front の src 配下のディレクトリ構造を以下の様に修正してください。

src
├── pages
|   ├── _app.tsx
|   ├── _document.tsx
|   └── index.tsx
├── styles
|   └── Home.module.css
├── next.config.js
└── package.json
...

内部の各ファイルを下の様に修正してください。

_app.tsx

import type { AppProps } from "next/app";

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

_document.tsx

import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="ja">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

index.tsx

import { ChangeEvent, useState } from "react";
import styles from "../styles/Home.module.css";

type Response = {
  message: string;
};

export default function Home() {
  const [queryParama, setQueryParam] = useState("");
  const [header, setHeader] = useState("");
  const [cookie, setCookie] = useState("");
  const [message, setMessage] = useState("");

  const changeQueryParam = (event: ChangeEvent<HTMLInputElement>) => {
    setQueryParam(event.target.value);
  };

  const changeHeader = (event: ChangeEvent<HTMLInputElement>) => {
    setHeader(event.target.value);
  };

  const changeCookie = (event: ChangeEvent<HTMLInputElement>) => {
    setCookie(event.target.value);
  };

  const getMessageFromQueryParam = () => {
    fetch(
      `https://[API Gatewayに紐づいているディストリビューションドメイン名]/v1/query-param?queryParam=${queryParama}`
    )
      .then((response) => response.json())
      .then((response: Response) => {
        setMessage(response.message);
      });
  };

  const getMessageFromReqHeader = () => {
    fetch(
      "https://[API Gatewayに紐づいているディストリビューションドメイン名]/v1/header",
      {
        headers: {
          "x-header": header,
        },
      }
    )
      .then((response) => response.json())
      .then((response: Response) => {
        setMessage(response.message);
      });
  };

  const getMessageFromCookie = () => {
    fetch(
      "https://[API Gatewayに紐づいているディストリビューションドメイン名]/v1/cookie",
      {
        headers: {
          Cookies: `cookie=${cookie};`,
        },
      }
    )
      .then((response) => response.json())
      .then((response: Response) => {
        setMessage(response.message);
      });
  };

  return (
    <div className={styles.home}>
      <div>取得したメッセージ:{message}</div>
      <div>
        <label>クエリパラメーター:</label>
        <input onChange={changeQueryParam} />
      </div>
      <div>
        <label>ヘッダー:</label>
        <input onChange={changeHeader} />
      </div>
      <div>
        <label>クッキー:</label>
        <input onChange={changeCookie} />
      </div>
      <div className={styles.button} onClick={getMessageFromQueryParam}>
        getMessageFromQueryParam
      </div>
      <div className={styles.button} onClick={getMessageFromReqHeader}>
        getMessageFromReqHeader
      </div>
      <div className={styles.button} onClick={getMessageFromCookie}>
        getMessageFromCookie
      </div>
    </div>
  );
}

Home.module.css

.home > * {
  margin-bottom: 10px;
}

.button {
  background-color: blue;
  color: white;
  width: 300px;
  text-align: center;
  padding: 10px;
}

next.config.js

...
const nextConfig = {
  reactStrictMode: true,
  trailingSlash: true,
}
...

package.json

{
  ...
  "scripts": {
    "dev": "next dev",
    "build": "next build && next export",
    "start": "next start",
    "lint": "next lint"
 }
 ...
}

修正が終わったら、ルートディレクトリで以下コマンドを実行して S3 に格納する画面用ファイルを作成してください。
out ディレクトリが作成され、中に画面用のファイルが出力されています。

$ npm run build

ファイルが出来上がったので S3 に画面用ファイルを格納します。
マネジメントコンソールで S3 を開き、バケットを作成を押下してください。
バケットを作成画面で以下を入力し、バケットを作成ボタンを押下してバケットを作成してください。

バケットを作成後、できあがったバケットcf-frontを押下し、以下 cf-front 画面に遷移してください。
アップロードボタンを押下して、out ディレクトリ内部のファイルをバケットに格納します。

ファイルを追加とフォルダを追加ボタンを使用して全ての out ディレクトリ配下のファイル・フォルダをアップロードし、アップロードボタンを押下してください。

次に、バケット > cf-front > プロパティタブ > 静的ウェブサイトホスティングで静的ウェブサイトホスティングを編集画面を開き、以下のように入力して変更を保存ボタンを押下してください。

これで S3 に画面用ファイルを格納する作業は終わりです。

  1. CloudFront で画面用ファイルのキャッシュを取る

CloudFront で画面用ファイルのキャッシュを取る設定を行います。
マネジメントコンソールで CloudFront を開き、ディストリビューションを作成ボタンを押下してください。
ディストリビューションを作成画面で以下のように入力し、ディストリビューションを作成ボタンを押下、ディストリビューションを作成してください。

これで画面のキャッシュを取る準備が整いました。
ディストリビューションドメイン名をブラウザに打ち込んでください。
こちらの画面が表示されるはずです。

上記で開かれている開発者ツールの Network タブの Headers タブで取得した html のリクエスト・レスポンス情報が閲覧できます。
html が返ってきたレスポンス情報を見てみると、X-Cache というレスポンスヘッダーがあります。
もしエッジロケーションでキャッシュが取得できた場合、ここの値がHit from cloudfrontになり、取得できなかった場合Miss from cloudfrontになります。
今回は初めてのリクエストだったため、キャッシュは保存されておらず、Miss from cloudfrontが表示されています。
Timing タブを開くと、リクエストからレスポンスが返ってくるまでに要した時間が閲覧できます。

今回は上記のように 98.61ms かかっています。
もう一度更新をかけてリクエストを送ります。

Network タブの Headers タブを開くと X-Cache の値がHit from cloudfrontになり、画面用ファイルのキャッシュが取れていることが分かります。
Timing タブを開きます。

リクエストからレスポンスが返ってくるまでに要した時間が 63.84ms と短くなっていることが分かります。
これでキャッシュが取れてユーザーに画面を素早く配信することができました。
次に API のキャッシュが取れているか確認してみます。
画面上の*1 クエリパラメーターという入力欄にクエリパラメーターを入力し、*2getMessageFromQueryParamボタンを押下することでクエリパラメーターの値を変えて API を叩くことができます。
1 を入力してボタンを押下してみてください。

API から取得したメッセージを見てみると、クエリパラメーターの値は 1 ですという表示がされています。
このメッセージの 1 という値は、設定したクエリパラメーターの値によって変わるようになっており、今回の場合 1 を設定したため 1 という値が API から帰ってきています。
初めてのリクエストだったためキャッシュは保存されておらず返ってきたレスポンスヘッダーのX-CacheMiss from cloudfrontが表示されています。
ここでクエリパラメーターの値を 2 に変えてもう一度ボタンを押下しリクエストを送ってみます。

API から取得したメッセージを見てみると、クエリパラメーターの値は 1 ですという表示がされています。
クエリパラメーターの値を変えてリクエストを送ったのにメッセージが変わっていません 🤨
X-Cacheを見てみると、Hit from cloudfrontという表示がされており、前回のクエリパラメーターの値は 1 ですというメッセージがキャッシュされて返されてきてしまっていることが分かります。
この現象は、CloudFront がデフォルトではリクエストされた URL のパスが同じかどうかでキャッシュを返すかオリジンからのレスポンスを返すか決めており、クエリパラメーター・ヘッダー・Cookie の値が変わってもキャッシュが返ってしまうことに起因しています。
よって、queryParamというクエリパラメーターの値が変わったらもう一度オリジンにリクエストを送ってそのレスポンスを返すように CloudFront に設定を行います。
マネジメントコンソールで CloudFront を開き、ポリシー > カスタムポリシー > cache-api-policy を選択、編集ボタンを押下してください。

キャッシュポリシーを編集画面のキャッシュキー設定で*3 クエリ文字列にqueryParamを追加し、変更を保存ボタンを押下してください。
このキャッシュキーを設定することで、設定した値が変更された時には CloudFront が新しくオリジンにリクエストを送り直すようになります。

では、もう一度先ほどの画面でリクエストを送り直します。

取得したメッセージが変わりました。
X-Cacheヘッダーを見てみると、Miss from cloudfrontと記載されており、CloudFront 上のキャッシュを利用せずオリジンにリクエストを飛ばした上でレスポンスを取得したことが分かります。
また、Timing タブを開くとレスポンスに 97.12ms かかっていることが分かります。

もう何度かリクエストを送ってみます。

X-Cacheヘッダーを見てみると、Hit from cloudfrontと記載されており、CloudFront 上のキャッシュを利用してレスポンスを取得したことが分かります。

また、Timing タブを開くとレスポンスが 14.39ms と短くなっていることが分かります。

このキャッシュキーは少なければ少ない程オリジンにリクエストを送る回数が減るので、少ない方が良いです。
しかし、表示が変わらなくなるのはおかしいため、如何に設定するキャッシュキーを少なくしつつ表示を適切に変えることができるかでキャッシュのパフォーマンスチューニングを行うことができます。
このキャッシュキーの設定を変えてキャッシュを取り直すかの判断する実験は上記文章の中で米印がついた数字の箇所を各々下の対応する数字の入力項目と読み替えることでヘッダー、Cookie に関しても行うことができるので、余裕があったら試してみてください 😌

  • ヘッダー

*1 ヘッダーに入力してください。

*2 getMessageFromReqHeaderを押下してください。

*3 ヘッダーで次のヘッダーを含めるを選択し、カスタムを追加でx-headerという値を追加してください。

  • Cookie

*1 クッキーに入力してください。

*2 getMessageFromCookieを押下してください。

*3 cookie で指定された cookie を含めるを選択し、項目を追加でcookieという値を追加してください。

終わりに

ハンズオンお疲れ様でした。
まだまだ CloudFront には面白い機能がたくさんあるので、また色々試してみたいです。
ここまで読んでいただき本当にありがとうございます 🙇‍♀️

参照

https://zenn.dev/hamo/articles/0a96c4d27097bd
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html

Discussion