Version Skew(バージョンスキュー)とは、連携するコンポーネント間で稼働するバージョンが食い違う状態です。Webアプリでは、利用者のブラウザが読み込んだ資産と、その後のリクエストを処理するサーバーのデプロイ版が食い違っている状態です。デプロイ直後にチャンクの404やフォーム送信の失敗が散発する場合は、この食い違いが原因の一つとして考えられます。本記事では、発生する層の切り分け、Next.jsのdeploymentIdをローカルビルドで実測した結果、Vercel・Netlify・AWS Amplifyの適用条件の違い、そしてサーバー間のスキューを規定するKubernetesのポリシーまでを、実装の判断に使える形で整理します。
まとめ:Version Skew対策で最初に決める3点
Version Skewが発生し得る前提で、固定する範囲、互換性を保つ範囲、保護期間を先に決めます。
- バージョン固定と後方互換の役割分担:固定(version locking)は移行期間を稼ぐ手段で、互換性の設計を代替しません。両方を併用します。
- 固定の担い手はフレームワークとホスティングの2つ:Next.jsの
deploymentIdは資産URLや遷移リクエストに識別子を付け、不一致時の全画面リロードにも使われます。識別子に応じて旧デプロイへ振り分ける仕組みは、ホスティング側で用意します。片方だけでは成立しません。 - 保護される期間の上限:Vercelの最大有効期間は既定で1日、AWS Amplifyは静的が1週間・SSRが直近8デプロイです。保護範囲を外れた古いクライアントではエラーが起こり得るため、未保存データに配慮したリロード導線を用意します。
Version Skewの定義と、発生する3つの層
同じ「バージョンの食い違い」でも、起きる層によって打ち手がまったく変わります。まず層を分けて捉えます。
クライアント資産・API・サーバー間という3層の区別
1つ目はクライアント資産の層です。利用者が開いたページのJavaScriptやCSSは、読み込んだ時点のデプロイのものです。その後に新しいデプロイが公開され、古いファイル名のチャンクが配信元から消えると、画面遷移の途中で読み込みが失敗します。
2つ目はクライアントとサーバーの契約の層です。古い画面が古い形式のリクエストを送るのに対し、サーバーは新しい形式を前提に応答します。必須フィールドの追加やパスの改名は、この層で即座に失敗を生みます。
3つ目はサーバー同士の層です。ローリング更新の最中は新旧のプロセスが同時に動きます。Kubernetesのようにコンポーネント間の許容差が明文化されている基盤もあり、後半で扱います。1つ目と2つ目はプラットフォームの機能である程度吸収できますが、3つ目は運用計画の問題です。
典型的な症状と、切り分けに使うログの3点
Netlifyの公式ドキュメントは、スキューが表面化する形を「静的資産の404」と「サーバー側呼び出しの失敗」の2つに整理しています。前者は画面遷移で追加のバンドルを取りに行ったときに、初回読み込み時の識別子で要求したファイルが存在せず失敗するものです。後者はナビゲーションやフォーム送信が、更新後のコードに依存した応答を受け取って予期しないエラーになるものです。
切り分けでは次の3点を突き合わせます。第一に、失敗したリクエストのパスにビルド識別子やハッシュが含まれているかどうかです。第二に、エラーの発生時刻がデプロイ完了時刻の直後に集中しているかどうかです。第三に、同じ利用者のセッション開始時刻が、そのデプロイより前かどうかです。この3点は調査の手掛かりですが、揃ってもスキューとは断定できず、揃わなくても除外できません。クライアントと応答側のデプロイ識別子、要求した資産の所在、APIの入出力形式を照合して原因を確かめます。
原子的なデプロイが成立しない構造と、スキューが伸びる条件
スキューは実装のミスではなく、分散したシステムの構造から生まれます。原因を2つに分けます。
ローリング更新とキャッシュ滞留という2つの遅延源
すべてのサーバーとすべてのクライアントを同時に切り替える方法は、現実には取れません。無停止で出すために更新を少しずつ進めれば、進行中は新旧が併存します。ローリングアップデートやカナリアのような段階的な公開方式は、可用性と引き換えにスキューの窓を意図的に作っている、と捉えると整理しやすくなります。
もう1つの遅延源がキャッシュです。CDNのエッジやブラウザに古い資産が残っている間は、サーバーを切り替えても利用者の手元は旧版のままです。Vercelの公式ドキュメントは、クローラーへの配慮としてGooglebotとBingbotからのリクエストについては最大有効期間を自動的に60日へ調整すると明記しています。文書のクロールとレンダリングの間に遅れがあるためで、機械が相手でも滞留は起きるという例です。
長時間セッションとモバイルアプリという例外的な長期スキュー
通常は次の全画面遷移で解消します。問題はそれが起きない使われ方です。監視ダッシュボード、オンライン試験、長い入力フォームのように、タブを開いたまま数時間経過するアプリでは、スキューの窓が運用上の上限を超えます。Vercelはこの用途を想定し、ライブの試験、音声や映像のリアルタイム通信、複数ステップのワークフローを具体例として挙げています。
ネイティブアプリの自動更新が無効な端末も同じ構造です。こちらは旧クライアントが長期間残り得るため、APIの後方互換やAPIバージョニングを基本に、旧版サーバーへの振り分けと更新要求の運用を検討します。
対策の2軸:バージョン固定と後方互換の役割分担
打ち手は2つの軸に分かれます。バージョン固定は、クライアントが読み込んだデプロイへ以降のリクエストを送り届ける方法です。Vercelはこれを version locking と呼び、プラットフォームとフレームワークの層で実装しています。後方互換は、新しいサーバーが古いクライアントの要求を受け付けられるようにコードとスキーマを設計する方法です。
固定は強力ですが、有効期間の上限があり、削除されたデプロイには適用されません。つまり固定が稼ぐのは時間だけです。移行期間中に互換性を保つ設計がなければ、期限が切れた瞬間に同じ失敗が起きます。逆に後方互換だけでは、資産ファイルが消える種類の失敗は防げません。ゼロダウンタイムデプロイを成立させるには、両方を組み合わせる必要があります。
Next.jsのdeploymentIdによる資産URL固定(16.3.5での実測)
ここからは具体的な挙動です。Next.jsのdeploymentIdは、公式ドキュメントで「バージョンスキュー対策とローリングデプロイ時のキャッシュバスティングに使う識別子」と説明されている設定です。実際に何が出力に現れるのかを、手元のビルドで確認しました。
実測手順と、未設定との差分14箇所
Next.js 16.3.5とReact 19.3.0を使い、app/page.jsは<main>skew test</main>を返し、app/layout.jsはhtml・body要素でchildrenを包む構成で、next.config.jsに識別子を設定してビルドしました。
// next.config.js
module.exports = { deploymentId: 'dpl-test-0001' };
ビルド後、事前生成されたHTML(.next/server/app/index.html)を確認したところ、スクリプトの参照がすべて識別子付きになっていました。
$ grep -o 'src="[^"]*dpl=[^"]*"' .next/server/app/index.html | head -3
src="/_next/static/chunks/3fdu7ur7z1_qc.js?dpl=dpl-test-0001"
src="/_next/static/chunks/27t_qfc-3_lzs.js?dpl=dpl-test-0001"
src="/_next/static/chunks/turbopack-1o9yxctiv349e.js?dpl=dpl-test-0001"
$ grep -o 'dpl=dpl-test-0001' .next/server/app/index.html | wc -l
14
$ grep -o 'data-dpl-id="[^"]*"' .next/server/app/index.html
data-dpl-id="dpl-test-0001"
同じアプリから設定だけを外して再ビルドすると、dpl=の出現は0箇所、data-dpl-id属性も付きませんでした。さらに設定を書かずに環境変数だけで渡した場合も、識別子付きの参照は14箇所で同数でした。
$ NEXT_DEPLOYMENT_ID=env-dpl-0002 npx next build
$ grep -o 'dpl=env-dpl-0002' .next/server/app/index.html | wc -l
14
公式ドキュメントの記述どおり、設定ファイルと環境変数のどちらでも指定でき、両方ある場合はnext.config.jsの値が優先されます。Next.jsが行うのは、静的資産URLへの?dpl=付与、クライアント遷移リクエストへのx-deployment-idヘッダ付与、応答へのx-nextjs-deployment-idヘッダ付与、html要素へのdata-dpl-id属性の注入、そして'use cache'のキャッシュキーへの反映です。
deploymentIdの制約:旧版への振り分けと独自fetchの識別子
1つ目の穴は振り分けです。公式ドキュメントは「Next.jsは受信リクエストの?dpl=クエリパラメータを読まない」と明記しています。このパラメータはキャッシュバスティングのためのもので、経路の決定には使われません。デプロイ単位で振り分ける仕組みは、ホスティングかCDN側で用意する必要があります。識別子を設定しただけの自前環境では、不一致を検知して全画面リロードに落ちる挙動になります。
2つ目の穴は自前のfetchです。フレームワークが管理する遷移やプリフェッチには識別子が付きますが、クライアントコンポーネントから書いたfetchには自動では付きません。Netlifyのドキュメントは、この場合のリクエストは常に現行デプロイで処理されると説明し、ヘッダを自分で付ける回避策を示しています。
fetch('/path', {
headers: {
'x-deployment-id': process.env.NEXT_DEPLOYMENT_ID
},
});
Vercelでも考え方は同じで、非対応フレームワークの場合はVERCEL_SKEW_PROTECTION_ENABLEDが1のときにVERCEL_DEPLOYMENT_IDの値を、dplクエリパラメータ・x-deployment-idヘッダ・__vdplクッキーのいずれかで渡す実装が案内されています。
ホスティング別のSkew Protection比較(Vercel・Netlify・AWS Amplify)
振り分けを担うのはホスティングです。3社の条件は次のように異なります。
| 項目 | Vercel | Netlify | AWS Amplify Hosting |
|---|---|---|---|
| 利用条件 | ProとEnterpriseチーム | 全プラン・本番のみ・環境変数で有効化 | 追加費用なし |
| 既定の状態 | 2024年11月19日以降に作成した対応FWのプロジェクトは有効 | Next.jsは無効(Astroは自動) | 無効(ブランチ単位で有効化) |
| 保護される期間 | 既定は作成から1日・保持ポリシーの上限まで延長可 | デプロイを保持する限り | 静的は1週間・SSRは直近8デプロイ |
| 設定の単位 | プロジェクト | プロジェクト(環境変数) | ブランチ |
| 期限切れの挙動 | 404を返す | 保持終了後の挙動は要検証 | 保護範囲外の挙動は要検証 |
Vercelの適用条件と最大有効期間の既定値
Vercelの制限事項には「Skew ProtectionはProとEnterpriseチームのすべてのデプロイ環境で利用できる」と書かれています。無料のHobbyは対象外です。対応フレームワークはNext.js・SvelteKit・Qwik・Astro・Nuxtです。必要なアダプタや設定はフレームワークごとに異なり、Next.jsは14.1.4以降なら追加設定が不要、13.4.7から14.1.3ではexperimental.useDeploymentIdを設定し、Server Actionsも保護する場合はexperimental.useDeploymentIdServerActionsも有効にします。Astroは@astrojs/vercelの9.0.0以降が必要で、アダプタ側にskewProtection: trueを書きます。SvelteKitは@sveltejs/adapter-vercelの5.2.0以降、Qwikは1.5.3以降が条件です。
最大有効期間(Maximum Age)の既定はデプロイ作成から1日で、プロジェクトの保持ポリシーの上限まで引き上げられます。期限を過ぎたデプロイや削除済みのデプロイを識別子で要求すると404が返る点は、設計上の落とし穴です。長時間セッションを守りたい場合は、ルーティングミドルウェアで__vdplクッキーを設定し、全画面遷移も含めて同じデプロイへ固定する方法が案内されています。固定はクッキーを削除するまで続くため、業務セッション終了時に削除してから最新デプロイへ遷移させます。
// middleware.ts(Vercel公式の例)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const deploymentId = process.env.VERCEL_DEPLOYMENT_ID;
if (deploymentId && !request.cookies.get('__vdpl')) {
response.cookies.set('__vdpl', deploymentId, {
path: '/',
httpOnly: true,
sameSite: 'strict',
});
}
return response;
}
ローカルでビルドしてvercel deploy --prebuiltで出す運用は長く対象外でしたが、2026年1月28日の変更で、next.config.jsにカスタムの識別子を設定して利用できるようになりました。現行仕様ではNext.jsは16.2.0-canary.15より新しい版、Vercel CLIは50.3.3より新しい版が必要です。この識別子はroutes-manifest.jsonに書き出され、振り分けに使われます。
Netlifyの有効化フラグと対応バージョン
NetlifyのNext.js向けSkew Protectionはオプトインです。環境変数NETLIFY_NEXT_SKEW_PROTECTIONにtrueを設定して再デプロイすると有効になります。Next.jsが14.1.4より前の場合は、前述のexperimental.useDeploymentIdを併せて指定します。Astro本体は5.15.0以降、Netlifyアダプタは6.6.0以降でSkew Protectionに対応し、自動的に有効になります。アクションやサーバーアイランド、プリフェッチが保護対象に入ります。コマンドラインからのデプロイで使う場合は、Netlify CLI 23.11.0以降が条件です。注意点として、Netlifyの公式ドキュメントはSkew Protectionが本番コンテキストでのみ有効だと明記しており、ブランチデプロイとDeploy Previewへのリクエストでは迂回され、そのコンテキストの最新デプロイが配信されます。
AWS Amplifyのブランチ単位設定と保持範囲
Amplify Hostingのスキュー保護はブランチ単位で、追加費用はかかりません。既定の_dplクッキーは同じドメインのタブ間で共有されるため、新旧版を別タブで開く場合はX-Amplify-Dplヘッダーによる固定を検討します。保護される範囲は、静的アプリケーションが1週間分のデプロイ、SSRアプリケーションが直近8デプロイまでと明記されています。WEB_DYNAMICプラットフォーム(Next.js 11以前)のアプリは対象外です。コンソールのブランチ設定から有効化するほか、APIではCreateBranchとUpdateBranchのenableSkewProtectionで操作します。有効化後のデプロイだけが保護対象になるため、設定後の再デプロイが必要です。なお有効化するとCDNのキャッシュ設定が更新されるため、直後の初回デプロイは最大10分ほどかかるとされています。
プラットフォーム機能で埋まらない範囲のキャッシュとAPI設計
固定機能を入れても、キャッシュ設計とAPIの互換性は自分たちの責任範囲として残ります。
HTMLとハッシュ付き資産で分けるCache-Controlの方針
ファイル名にハッシュを含む資産は、内容が変われば名前も変わるため長期キャッシュと相性がよく、逆にHTMLは保存済みのものを使う前に更新確認をさせたい対象です。後者にはno-cacheを指定します。これは保存済みHTMLの再検証を求める指示であり、すでに読み込まれたページや古い資産を消す設定ではありません。
# HTMLは毎回検証させる
Cache-Control: no-cache
# ハッシュ付きの静的資産は長期キャッシュ
Cache-Control: public, max-age=31536000, immutable
キャッシュ層ごとの制御の考え方はCDNとは?仕組み・キャッシュ制御からサービスの選び方までで整理しています。
拡張と縮約の2段階で出すAPIの後方互換
APIの変更は、旧クライアントが残っている期間を見込んで2段階に割ります。第1段階では追加だけを行い、新しいフィールドは任意扱いにして、新旧どちらの形式でも受け付けます。第2段階は、旧クライアントが十分に入れ替わったことを計測で確認してから、古い経路を落とします。必須フィールドの追加やフィールドの改名を1回のリリースで行うのが、最も典型的な失敗です。
この期間の見積もりは、固定機能の有効期間と揃えると管理しやすくなります。Vercelの既定の1日はデプロイ作成時点から数える保護期間であり、APIの互換性維持期間とは別です。旧形式の利用実績と、ロールバックで旧版が戻る期間を確認して廃止時期を決めます。バージョンの切り方そのものはバージョニングとは?セマンティック/APIバージョニングの意味と運用設計を参照してください。
サーバー間のversion skew:Kubernetesの許容差ポリシー
「version skew」という語は、クラスタ内のコンポーネント間でも使われます。Kubernetesは公式に許容差を定義しており、ローリング更新の計画を立てる際の基準になります。主な規定は次のとおりです。
- kube-apiserver:高可用性構成では、最も新しいインスタンスと最も古いインスタンスが1マイナーバージョン以内であることが必要です。
- kubelet:kube-apiserverより新しくてはならず、最大で3マイナーバージョンまで古いものが許容されます(1.25より前のkubeletは2マイナーまで)。
- kube-proxy:kube-apiserverより新しくできず、最大3マイナー古い版まで許容されます。同居するkubeletとの差は前後3マイナー以内です。ただし、1.25未満のkube-proxyはいずれも最大2マイナー差です。
- kube-controller-manager・kube-scheduler・cloud-controller-manager:kube-apiserverより新しくてはならず、同じマイナーバージョンが期待されますが、無停止更新のため1マイナー古いものまで許容されます。
- kubectl:kube-apiserverの前後1マイナーバージョン以内がサポート範囲です。
この差が示すのは、分散システムでは「全部同時に上げる」ではなく「どこまでのズレを許すか」を先に決める設計だという点です。Webアプリのスキュー対策も同じで、許容する窓の広さを決めてから、固定と互換の手当てを配分します。
よくある質問
Skew Protectionを有効にすれば、APIの後方互換は不要になりますか?
不要にはなりません。固定には有効期間の上限があり、Vercelでは既定で1日、AWS AmplifyではSSRの場合に直近8デプロイまでです。期限を過ぎたクライアントや、削除されたデプロイを要求するクライアントは保護の対象外になります。固定は移行期間を稼ぐ手段と捉え、互換性の設計は別に行ってください。
Vercelの無料プランでもSkew Protectionは使えますか?
使えません。公式ドキュメントの制限事項では、ProとEnterpriseのチームが対象と記載されています。無料のHobbyプランで運用する場合は、キャッシュ制御とAPIの後方互換、そして不一致検知時のリロード導線で対応することになります。
自分で書いたfetchにもデプロイ識別子は自動で付きますか?
付きません。フレームワークが管理する遷移・プリフェッチ・静的資産の取得には付きますが、クライアントコンポーネントから呼ぶfetchは対象外です。Netlifyの例のようにx-deployment-idヘッダを自分で付けるか、Vercelの案内どおりdplクエリパラメータやクッキーで渡す必要があります。
deploymentIdを設定すれば、Vercel以外の環境でもスキューは解消しますか?
解消しません。Next.jsは受信リクエストの?dpl=を読まず、経路の決定には使わないと公式に明記されています。デプロイ単位で振り分ける仕組みはホスティングかCDNの側に必要です。自前構成では、不一致を検知した際に全画面リロードで最新へ揃える挙動になります。
ダッシュボードのように長時間開いたままのアプリはどう扱いますか?
通常の設定では、次の全画面遷移で最新デプロイに揃います。リロードが業務を壊す用途では、Vercelの__vdplクッキーのようにセッション単位でデプロイを固定する方法があります。最大有効期間はセッション開始ではなくデプロイ作成から数えます。セッション開始時点のデプロイ経過時間と想定セッション長を合計して設定し、その間のデプロイ保持も確保してください。固定したデプロイが保持ポリシーで削除されると、クッキーがあっても振り分けはできなくなります。
関連記事
- ゼロダウンタイムデプロイとは?無停止を壊す発生源と設定順序・計測方法:スキュー対策を含めた無停止リリース全体の設計手順をまとめています。
- ローリングアップデートとは?仕組みとKubernetes・ECSの設定値・採用判断:新旧が併存する期間を作る更新方式そのものの設定値を扱っています。
- カナリアリリースとは?段階リリースの仕組み・他方式との違い・実装と採用判断:段階公開でスキューの窓を意図的に管理する場合の判断材料になります。
- CDNとは?仕組み・キャッシュ制御からサービスの選び方まで実装目線で解説:古い資産が残る原因側のキャッシュ制御を詳しく説明しています。
- Next.jsとは?16系App Routerで最小アプリを動かす手順と採用判断:本記事で実測に使ったNext.jsの基本構成を確認できます。