Vercel Blob でデータを永続保存する方法|Runtime Cache のデータ消失対策

Vercel Runtime Cache に保存していたデータが、あるとき取得できなくなりました。

もともとは Yahoo!ショッピング API から定期取得したランキングデータを GitHub Actions で更新して、Vercel Runtime Cache に保存しておく構成にしていました。

その内容は下記の記事にまとめています。

→ Yahoo!ショッピング API でランキング広告を自作した話

前の記事でも書きましたが、ランキングを1時間ごとに更新しつつ、Runtime Cache の TTL は30日にしていました。更新に失敗しても最後に正常取得できたデータを残しておくためです。

ただ、運用を開始すると、更新用 API で Runtime Cache への保存と読み返しまで成功していたデータが、その数時間後には表示用 API から取得できない状態になりました。

表示用 API は、保存済みデータがなければその場で Yahoo!ショッピング API を呼び直す作りにはしていません。

30日の TTL にしているから大丈夫だろうと思っていましたが、この状態が実際に起きた以上、Runtime Cache だけを保存先にする構成のままにはしない方がいいかなと思いました。

そこで、残しておきたいデータは Vercel Blob に保存して、Runtime Cache は表示時に使うキャッシュとして残す構成へ変更しました。

この変更に合わせて、Runtime Cache の TTL も30日から2時間へ短くしました。長期間残しておきたいデータは Blob 側に保存する構成になったため、Runtime Cache に30日間保持させる必要がなくなったからです。

また、Runtime Cache が空になった場合は Blob から読み出し、そのデータを Runtime Cache へ再保存します。

この記事では、そのときに行った Vercel Blob の Store 作成、OIDC での接続、Blob への保存、Runtime Cache が空だった場合の読み戻しまでまとめます。


Runtime Cache だけで保存していたときの構成

前の記事で変更したあとの構成はこうでした。

GitHub Actions
毎時17分 または 本番デプロイ後
↓
更新用 API
↓
ランキングデータを取得
↓
Runtime Cache に保存

ユーザーが記事を開く
↓
表示用 API
↓
Runtime Cache を読む
↓
ランキング広告を表示

更新用 API と表示用 API を分けて、ユーザーが記事を開いたときには Yahoo!ショッピング API を呼ばないようにしています。

Runtime Cache へは getCache() を使って保存していました。

→ Vercel Docs:Runtime Cache

ランキング自体は1時間ごとに更新していましたが、TTL は30日にしていました。

これは30日間同じランキングを表示するためではなく、更新処理が一時的に失敗したときも、前回成功したデータを残しておくためでした。


保存に成功していたのに数時間後に取得できなくなった

更新時点では Runtime Cache に保存できていたことを確認しています。

ところが、その数時間後に本番の表示用 API を開くとエラーになり、Runtime Cache から値を取得できませんでした。

同じ本番 Deployment で、Function の実行先も確認しましたが、今回の確認だけでは「なぜ取得できなくなったのか」までは特定できませんでした。

Vercel の Runtime Cache には TTL を指定できますが、今回分かったのは、

30日の TTL を設定した
↓
保存直後の読み返しも成功した
↓
数時間後には get で値を取得できなかった

というところまでです。

なので、「Runtime Cache の TTL が切れた」とも、「必ず数時間で消える」とも書けません。

ただ、少なくとも自分のやりたいことにおいては、消えると困るデータを Runtime Cache に置くだけ構成では足りないということになります。


Vercel Blob に保存する構成へ変更

今回変更したあとの構成はこうです。

GitHub Actions
↓
更新用 API
↓
ランキングデータを取得
↓
Vercel Blob に保存
↓
Blob から読み返して確認
↓
Runtime Cache に保存

表示側は、

表示用 API
↓
Runtime Cache にデータがある?
├─ YES → そのまま返す
└─ NO  → Vercel Blob から読む
          ↓
        Runtime Cache に戻す
          ↓
        データを返す

という形にしました。

Vercel Blob に残っているデータを保存用として使って、Runtime Cache はこれまでどおり表示時に使います。

なので、Runtime Cache が空になっても、その場で Yahoo!ショッピング API からデータを取り直す必要はありません。

Runtime Cache が空
↓
Blob から前回成功したデータを読む
↓
Runtime Cache に戻す
↓
表示を続ける

という形です。

→ Vercel Docs:Vercel Blob


Vercel Blob の Store を作成

Vercel の Storage から、ランキングデータを保存する Blob Store を作成しました。

Vercel Blob 作成画面

今回の設定は次のとおりです。

Access:Private

Region:iad1

ランキングの JSON は一般公開しても問題はないですが、あえてする意味もないので Access は Private にしました。

Region は、アップロード時の遅延を抑えるため、利用者や Functions に近いものを選ぶよう Vercel 公式が案内しています。

実行ログでランキングデータ取得用 Function が iad1 にルーティングされていたため、Blob Store も iad1 を選びました。

そのあと、作成した Store を対象の Vercel Project へ接続します。

接続すると、Project 側には BLOB_STORE_ID などの環境変数が自動的に追加されます。

Vercel Blob で自動作成された環境変数

今回は固定の BLOB_READ_WRITE_TOKEN を追加する設定にはせず、OIDC を使う形にしています。

Vercel の Private Blob の公式ドキュメントでも、Vercel 上で動かす場合は自動でローテーションされる OIDC token の利用が案内されています。

→ Vercel Docs:Private storage


Blob への保存処理を追加

コードでは @vercel/blob を追加して、put() と get() を使いました。

import { get, put } from '@vercel/blob';
import { getCache } from '@vercel/functions';

Blob への書き込み部分はこんな感じです。

await put(
  'ランキングデータ.json',
  JSON.stringify(payload),
  {
    access: 'private',
    storeId: process.env.BLOB_STORE_ID,
    addRandomSuffix: false,
    allowOverwrite: true,
    contentType: 'application/json; charset=utf-8',
  },
);

毎回同じ json を更新したいので、

addRandomSuffix: false,
allowOverwrite: true,

にしています。

書き込んだあとには、Blob から同じデータを読み返して確認します。

const result = await get(
  'ランキングデータ.json',
  {
    access: 'private',
    storeId: process.env.BLOB_STORE_ID,
    useCache: false,
  },
);

useCache: false にしたのは、書き込んだ直後の確認で CDN に残っている古いデータではなく、保存先から最新のデータを読むためです。

Vercel の公式ドキュメントでも、Private Blob で最新の内容を取得したい場合は useCache: false でキャッシュを使わずに読む方法が案内されています。

→ Vercel Docs:Private storage

読み返した generatedAt が、今回保存したデータと一致しなければ更新成功にはしません。


Blob に保存できたら Runtime Cache にも入れる

Blob への保存と確認が終わったあと、同じデータを Runtime Cache に入れます。

ランキングデータを取得
↓
表示用データを作る
↓
Blob に保存
↓
Blob から読み返して確認
↓
Runtime Cache に保存

以前は Runtime Cache の TTL を30日にしていましたが、今回は2時間に変更しました。

ランキングは1時間ごとに更新しているので、Runtime Cache がずっと残り続ける必要はなくなったためです。

前回成功したデータを長く残す役割は Blob 側に移したので、Runtime Cache は表示時にすぐ返すためのキャッシュとして使う形にしました。

また、Blob への保存は成功したものの Runtime Cache への書き込みだけ失敗した場合は、Blob のデータを残したまま Warning を出します。

Blob 保存成功
↓
Runtime Cache の更新失敗
↓
Blob はそのまま残す
↓
次の表示時は Blob から読める

Runtime Cache の不調で、せっかく Blob へ保存できたデータまで失敗扱いにしないようにしました。


Runtime Cache が空なら Blob から読み戻す

表示用 API では、まず今までどおり Runtime Cache を読みます。

const cached = await readRuntimeCacheSnapshot();

if (cached) {
  return cached;
}

値がなければ Blob へ進みます。

const durable = await readBlobSnapshot();

if (!durable) {
  return null;
}

Blob から取得できた場合は、そのデータを Runtime Cache へ戻してから返します。

Runtime Cache
MISS
↓
Blob
HIT
↓
Runtime Cache に再保存
↓
Blob から取得したデータを返す

Runtime Cache の get() 自体がエラーになった場合も、そこで全部失敗にはせず Blob を読みにいくようにしています。

さらに、Blob からデータを取得できたあと、Runtime Cache への再保存だけが失敗した場合も、Blob から取得したデータ自体は返します。

なので表示側では、

Runtime Cache が使える
↓
Runtime Cache から返す

Runtime Cache が空または読み取りエラー
↓
Blob から返す

Blob にもデータがない
↓
エラー

という形です。


本番で Blob への保存を確認

実装後、本番へ反映して GitHub Actions の更新用 API を実行しました。

その後、Vercel の Blob Store を確認すると、

ランキングデータ.json

が作成され、本番の表示用 API を開いても保存済みデータを取得できていました。


使わない Read-Write Token を Revoke

Blob Store の画面には、Read-Write Token を Revoke する案内も表示されていました。

Vercel Blob の Revoke Token

画面の説明では、Revoke すると、

  • 接続している Project の BLOB_READ_WRITE_TOKEN が削除される
  • その固定 token を使っているコードは動かなくなる
  • 接続自体は OIDC の短時間 token で引き続き利用できる

という内容になっていました。

今回の Blob Store は最初から OIDC で接続しており、コードでも固定の BLOB_READ_WRITE_TOKEN は使っていません。

そのため、Blob を利用するために Revoke が必要なわけではありません。

今回は固定の Read-Write Token を今後使う予定もなかったため、Blob への保存と読み出しが本番で問題なく動いていることを確認したあと、使わない token は残しておく必要はないと考えて Revoke しました。

固定 token をローカルの .env.local や別サービスから使っている場合は、Revoke するとそちらも動かなくなるため、その場合は先に利用状況を確認する必要があります。


まとめ

最初は、Runtime Cache の TTL を30日に設定していたため、少なくともその期間内は保存したデータを取得できる前提で構成していました。

ただし、Vercel 公式でも Runtime Cache は一時的なキャッシュ(ephemeral cache)として位置づけられており、長期間残したいデータの永続保存先とは役割が異なります。

実際、保存と読み返しまで成功していたデータを数時間後に取得できないという事象が起きました。

原因そのものまでは特定できませんでしたが、消えると困るデータを Runtime Cache だけに置いておく構成はやめました。

今は、

Vercel Blob
データの長期保存

Runtime Cache
使用時に使うキャッシュ

という分け方にしています。

参考

← ブログ一覧に戻る