Incremental Static Regenerationをコンテナで運用するときの注意点

Posted on December 27, 2020  -  9 min read

こちらの記事を参考に Incremental Static Regeneration(以下、ISR)を コンテナでデプロイしたときの挙動について気になった点があったのでまとめてみました。

なお、本記事では ISR の詳細ついては言及しません。それらについては元記事を参照いただければと思います。

ISR をコンテナイメージで運用する際の注意点

Next.js の ISR では、getStaticPropsの戻り値にrevalidateオプションを付与すると、指定された秒数、クライアントにキャッシュされたコンテンツが返却されるようになります。このキャッシュはアプリケーションが動作するコンテナ内に保持されます。

以下の例であれば、最後にページを動的に作成してから 最低 10 秒間は静的なページをクライアントに返却します。

export async function getStaticProps() {
  const date = new Date();
  const current = date.toLocaleString();
  return {
    props: {
      current,
    },
    revalidate: 10, // 10秒に1回ページを最新化する
  };
}

なお、revalidate は各コンテナごとにバラバラのタイミングで実施される点に注意が必要です。ロードバランサなどで複数のコンテナにルーティングされる場合、リクエストが流れるコンテナによって異なるコンテンツが返却される可能性があります。

一つのアプリケーションで一つのコンテナしか動いていない場合にはこの問題は発生しませんが、そのようなアプリケーションはあまり多くないと思います。 また、この問題は revalidate の間隔が長くなればなるほど顕著になります。

例えば、revalidate: 300(5分)を指定したアプリケーションが「コンテナ A」、「コンテナ B」 という二つのコンテナ上で動いている例を考えます。初回のリクエストがロードバランサを経由して「コンテナ A」にルーティングされ、その 2 分 30 秒後に「コンテナ B」にルーティングされたとしましょう。 この場合、次回 revalidate のタイミングは「コンテナ A」は 2 分 30 秒後で「コンテナ B」は 5 分後になります。この期間にバックエンドでコンテンツの更新が行われた場合、最大 2 分 30 秒間、「コンテナ A」と「コンテナ B」で異なるコンテンツをクライアントに返却してしまうことになります。ロードバランサがラウンドロビンでリクエストをバランシングしている場合、ページをリロードするたびに表示内容が変わったり、ユーザによって表示されるコンテンツが異なることが想定されます。

revalidate

AWS Fargate を用いて検証

AWS が提供するコンテナオーケストレーションサービスである、AWS Fargateを用いて上記の挙動を再現してみましょう。

Dockerfile とアプリケーションは元記事のものを拝借します。

export default function Index({ current }) {
  return <div>現在時刻は{current}です。</div>;
}

export async function getStaticProps() {
  const date = new Date();
  const current = date.toLocaleString();
  return {
    props: {
      current,
    },
    revalidate: 10,
  };
}
FROM node:current-alpine AS base
WORKDIR /base
COPY package*.json ./
RUN yarn install && yarn cache clean
COPY . .

FROM base AS build
ENV NODE_ENV=production
WORKDIR /build
COPY --from=base /base ./
RUN yarn build

FROM node:current-alpine AS production
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /build/package*.json ./
COPY --from=build /build/.next ./.next
COPY --from=build /build/public ./public
RUN yarn add next && yarn cache clean

EXPOSE 3000
CMD yarn start

ユーザがアクセスしたタイミングで現在時刻が表示されるシンプルなアプリケーションです。 revalidate: 10が指定されているため、10 秒間は何度アクセスしても表示時刻が変化しないことを期待します。

なお、今回は ECS に簡単にコンテナをデプロイできるAWS Copilotを用いました。

Next.js のトップディレクトリでcopilot initコマンドを発行し、Dockerfile を指定するだけでコンテナ実行に必要な VPC、ECS クラスタ、ALB、及びアプリケーションのエンドポイント URL を作成してくれます。めちゃくちゃ簡単です。

$ copilot init
Application name: isr-test # アプリケーション名を指定

Service type: Load Balanced Web ServiceX Sorry, your reply was invalid: Value is required
Service name: isr-test # サービス名を指定
Dockerfile: ./Dockerfile # Dockerfileを指定
Ok great, we'll set up a Load Balanced Web Service named isr-test in application isr-test listening on port 3000.

✔ Created the infrastructure to manage services under application isr-test.

✔ Wrote the manifest for service isr-test at copilot/isr-test/manifest.yml
Your manifest contains configurations like your container size and port (:3000).

✔ Created ECR repositories for service isr-test.

All right, you're all set for local development.
Deploy: Yes

(省略...)

f40183c0ca23: Pushed
8107d4305b97: Pushed
920b98a6ce66: Pushed
53a4abbb293c: Pushed
a40220d40eaf: Pushed
0fcbbeeeb0d7: Pushed
4e09dfc: digest: sha256:82c4b9b191ee37e520502eb3d007f4d565f7316698fcc67fe46901e8206d408c size: 2203

 # アプリケーションのエンドポイントURLが発行される。
✔ Deployed isr-test, you can access it at http://isr-t-Publi-1MHPV70ZK9M4Z-802069023.ap-northeast-1.elb.amazonaws.com.



発行された URL にアクセスすると、想定通りにアプリケーションが動作していることがわかります。

app

デフォルトの設定だと、コンテナは一つしか動いていないため、copilot の manifest ファイルからコンテナを増やす設定を加えてみます。

copilot が作成したcopilot/<サービス名>/manifest.ymlcountから起動するコンテナの数を変更します。

# Number of tasks that should be running in your service.
count: 10

サービスのデプロイコマンドで変更をクラウドに反映します。

$ copilot svc deploy

再度、ブラウザにアクセスしてみると、ページを表示させる度に表示される内容が毎回変わってしまうことが確認できます。これは先ほど述べた revalidate のタイミングが異なるため、ルーティングされるコンテナごとに異なるコンテンツ(今回の場合現在時刻)が表示されてしまうためです。

compare

まとめ

コンテナに限らずですが、ローカルにキャッシュデータを保持する特性上 revalidate のタイミングが異なる点は意識する必要があります。コンテンツの表示に高い一貫性を求められない場合は大きな問題にはならないかもしれません。

これらの挙動が許容できない場合は、revalidate の間隔を短くする、もしくは厳密な一貫性が求められるページのみ SSR や、クライアントから API を Call するなどなどして対応する必要があります(その場合、リクエストの度にバックエンドに負荷をかけることになるため ISR 本来の旨味が少なくなります)。Vercel でデプロイする場合 CDN のキャッシュと合わせてその辺りをうまくハンドリングしているようです。

フロントエンドも SPA、JAMstack、SSR、そして ISR とアーキテクチャの選択肢が増えてきましたが、それぞれの特性をよく理解して技術選定をするのが良さそうです。