HAQM Web Services ブログ

Amplify Hosting がスキュー保護を発表

Web アプリケーションの開発とデプロイにおける一般的な課題は、クライアントとサーバーリソース間のバージョンのずれです。2025 年 3 月 13 日、AWS Amplify Hosting にデプロイされたアプリケーション向けのデプロイメントスキュー保護機能(更新期間中の新旧バージョン混在時のシステム安定性を確保する機能)を発表できることを嬉しく思います。この機能は、アプリケーションのデプロイ中にエンドユーザーがシームレスな体験を得られるよう支援します。

課題

現代の Web アプリケーションは、多数の静的アセットとサーバーサイドコンポーネントからなる複雑なシステムであり、これらすべてが連携して動作する必要があります。1 時間に複数回のデプロイが一般的となっている世界では、バージョンの互換性が重要な懸念事項となります。新しいデプロイが行われると、ユーザーのブラウザにキャッシュされた古いバージョンのアプリケーションが、更新されたデプロイメントからリソースを取得しようとします。これにより、404 エラーや機能の破損が発生する可能性があります。

この課題は、クライアントとサーバーのバージョンのずれによってさらに複雑になり、それは 2 つの一般的なシナリオで現れます。1 つ目に、ユーザーがブラウザタブを長時間開いたままにすることが多く、アプリケーションの古いバージョンを実行しながら、更新されたバックエンドサービスとの対話を試みるケースがあります。2 つ目に、モバイルアプリケーションでは、自動更新を無効にしているユーザーが古いバージョンを無期限に使用し続ける可能性があり、バックエンドサービスが複数のクライアントバージョンと同時に互換性を維持する必要があります。

これらのバージョン管理の課題は、適切に対処しなければユーザー体験とアプリケーションの信頼性に大きな影響を与える可能性があります。以下のシナリオを考えてみましょう。

  1. ユーザーがアプリケーション(バージョン A)を読み込む
  2. 新しいバージョン(バージョン B)をデプロイする
  3. ユーザーのキャッシュされた JavaScript が、バージョン A にのみ存在していたアセットを読み込もうとする
  4. 結果:機能が壊れ、ユーザー体験が悪化する

スキュー保護の仕組み

Amplify Hosting は複数のクライアントセッション間でリクエスト解決を賢く調整し、意図したデプロイメントバージョンへの正確なルーティングを確保します。

1. スマートアセットルーティング

リクエストを受け取ると、Amplify Hosting は以下の処理を行います。

  • リクエストの元となったデプロイメントバージョンを識別します
  • リクエストを識別されたアセットのバージョンにルーティングして解決します
  • ハードリフレッシュは常にユーザーセッションで最新のデプロイメントアセットを提供します

2. 一貫したバージョンの提供

システムは以下を確認します。

  • 単一のユーザーセッションからのすべてのアセットが同じデプロイメントから提供されます
  • 新しいユーザーセッションは常に最新バージョンを取得します
  • 既存のセッションは、リフレッシュされるまで元のバージョンで動作し続けます

スキュー保護の利点

スキュー保護がもたらす利点は以下の通りです。

  • 設定が不要:人気のあるフレームワークですぐに使用可能です
  • 信頼性の高いデプロイメント:デプロイメントのずれによる 404 エラーを排除します
  • パフォーマンス最適化:レスポンスタイムへの影響を最小限に抑制します

ベストプラクティス

スキュー保護はほとんどのシナリオを自動的に処理しますが、以下を推奨します。

  1. アトミックデプロイメントを行う
  2. ステージング環境でデプロイメントプロセスをテストする

スキュー保護の有効化

スキュー保護は各 Amplify アプリごとに有効化する必要があります。これはすべての Amplify Hosting アプリのブランチレベルで行われます。詳しくは Amplify Hosting のドキュメントを参照ください。

1. ブランチを有効にするには、App settings をクリックし、次に Branch settings をクリックします。次に、有効にしたいブランチを選択し、Action タブをクリックします。

Screenshot of the branches tab in Amplify Console settings

図 1 – Amplify Hosting ブランチ設定

2. スキュー保護を有効にするには、アプリケーションを一度デプロイする必要があります。

注意:スキュー保護は、従来の SSRv1/WEB_DYNAMIC アプリケーションを使用しているお客様は利用できません。

価格:この機能に追加コストはなく、Amplify Hosting が利用できるすべてのリージョンで利用可能です。

チュートリアル

始めるには、以下の手順に従って Next.js アプリケーションを作成し、スキュー保護を有効にしてください。

前提条件

始める前に、以下がインストールされていることを確認してください。

  • Node.js (v18.x 以降)
  • npm または npx (v10.x 以降)
  • Git (v2.39.5 以降)

Next.js アプリの作成

スキュー保護の動作を確認するために Next.js アプリを作成しましょう。

1. TypeScriptTailwind CSS を使用した新しい Next.js 15 アプリを作成します。

$ npx create-next-app@latest skew-protection-demo --typescript --tailwind --eslint
$ cd skew-protection-demo

2. デプロイメント ID を持つフィンガープリントされたアセットをリストする SkewProtectionDemo コンポーネントを作成します。以下のコードを使用してコンポーネントを作成してください。

注意:Amplify はNext.js アプリのフィンガープリントされたアセットに、UUID に設定された dpl クエリパラメータを自動的にタグ付けします。この UUID は、他のフレームワークでも AWS_AMPLIFY_DEPLOYMENT_ID 環境変数を通じてビルド中に利用可能です。

// app/components/SkewProtectionDemo.tsx

"use client";

import { useState, useEffect } from "react";
import DeploymentTester from "./DeploymentTester";

interface Asset {
  type: string;
  url: string;
  dpl: string;
}

export default function SkewProtectionDemo() {
  const [fingerprintedAssets, setFingerprintedAssets] = useState<Asset[]>([]);
  const [deploymentId, setDeploymentId] = useState<string>("Unknown");

  // Detect all assets with dpl parameters on initial load
  useEffect(() => {
    detectFingerprintedAssets();
  }, []);

  // Function to detect all assets with dpl parameters
  const detectFingerprintedAssets = () => {
    // Find all assets with dpl parameter (CSS and JS)
    const allElements = [
      ...Array.from(document.querySelectorAll('link[rel="stylesheet"]')),
      ...Array.from(document.querySelectorAll("script[src]"))
    ];
    
    const assets = allElements
      .map(element => {
        const url = element.getAttribute(element.tagName.toLowerCase() === "link" ? "href" : "src");
        if (!url || !url.includes("?dpl=")) return null;

        // Extract the dpl parameter
        let dplParam = "unknown";
        try {
          const urlObj = new URL(url, window.location.origin);
          dplParam = urlObj.searchParams.get("dpl") || "unknown";
        } catch (e) {
          console.error("Error parsing URL:", e);
        }

        return {
          type: element.tagName.toLowerCase() === "link" ? "css" : "js",
          url: url,
          dpl: dplParam,
        };
      })
      .filter(asset => asset !== null);

    setFingerprintedAssets(assets);
    
    // Set deployment ID if assets were found
    if (assets.length > 0) {
      setDeploymentId(assets[0]?.dpl || "Unknown");
    }
  };

  // Function to format URL to highlight the dpl parameter
  const formatUrl = (url: string) => {
    if (!url.includes("?dpl=")) return url;
    
    const [baseUrl, params] = url.split("?");
    const dplParam = params.split("&").find(p => p.startsWith("dpl="));
    
    if (!dplParam) return url;
    
    const otherParams = params.split("&").filter(p => !p.startsWith("dpl=")).join("&");
    
    return (
      <>
        <span className="text-gray-400">{baseUrl}?</span>
        {otherParams && <span className="text-gray-400">{otherParams}&</span>}
        <span className="text-yellow-300 font-bold">{dplParam}</span>
      </>
    );
  };

  return (
    <main className="min-h-screen p-6 bg-white">
      <div className="w-full max-w-2xl mx-auto">
        <h1 className="text-2xl font-bold text-gray-900 mb-6">
          Amplify Skew Protection Demo
        </h1>

        <div className="grid grid-cols-1 gap-4 mb-6">
          <div className="flex items-center p-4 bg-white text-gray-800 rounded-md border border-gray-200 hover:bg-gray-50 transition-colors">
            <div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg mr-3">
              <span className="text-lg">🚀</span>
            </div>
            <div>
              <p className="font-medium">Zero-Downtime Deployments</p>
              <p className="text-xs text-gray-600">Assets and API routes remain accessible during deployments using deployment ID-based routing</p>
            </div>
          </div>
          
          <div className="flex items-center p-4 bg-white text-gray-800 rounded-md border border-gray-200 hover:bg-gray-50 transition-colors">
            <div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg mr-3">
              <span className="text-lg">⚡️</span>
            </div>
            <div>
              <p className="font-medium">Built-in Next.js Support</p>
              <p className="text-xs text-gray-600">Automatic asset fingerprinting and deployment ID injection for Next.js applications</p>
            </div>
          </div>
          
          <div className="flex items-center p-4 bg-white text-gray-800 rounded-md border border-gray-200 hover:bg-gray-50 transition-colors">
            <div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg mr-3">
              <span className="text-lg">🔒</span>
            </div>
            <div>
              <p className="font-medium">Advanced Security</p>
              <p className="text-xs text-gray-600">Protect against compromised builds by calling the delete-job API to remove affected deployments</p>
            </div>
          </div>
        </div>

        <div className="bg-gradient-to-r from-blue-900 to-purple-900 p-4 rounded-md mb-6">
          <h2 className="text-sm font-medium text-blue-200 mb-1">
            Current Deployment ID
          </h2>
          <div className="p-2 bg-black bg-opacity-30 rounded-md font-mono text-lg text-center text-yellow-300">
            {deploymentId}
          </div>
        </div>

        {fingerprintedAssets.length > 0 ? (
          <>
            <h2 className="text-xl font-bold text-gray-900 mb-6">
              Fingerprinted Assets
            </h2>
            <div className="border border-gray-200 rounded-md overflow-hidden mb-6 bg-white">
              <div className="max-h-48 overflow-y-auto">
                <table className="min-w-full divide-y divide-gray-200">
                  <thead className="bg-gray-50">
                    <tr>
                      <th className="px-3 py-2 text-left text-xs font-medium text-gray-900 uppercase tracking-wider w-16">
                        Type
                      </th>
                      <th className="px-3 py-2 text-left text-xs font-medium text-gray-900 uppercase tracking-wider">
                        URL
                      </th>
                    </tr>
                  </thead>
                  <tbody className="divide-y divide-gray-200">
                    {fingerprintedAssets.map((asset, index) => (
                      <tr key={index} className="bg-white hover:bg-gray-50">
                        <td className="px-3 py-2 text-sm text-gray-900">
                          <span
                            className={`inline-block px-2 py-0.5 rounded-full text-xs ${
                              asset.type === "css"
                                ? "bg-blue-100 text-blue-800"
                                : "bg-yellow-100 text-yellow-800"
                            }`}
                          >
                            {asset.type}
                          </span>
                        </td>
                        <td className="px-3 py-2 text-xs font-mono break-all text-gray-900">
                          {formatUrl(asset.url)}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            </div>
            
            <h2 className="text-xl font-bold text-gray-900 mb-6">
              Test Deployment Routing
            </h2>
            <DeploymentTester />
          </>
        ) : (
          <div className="p-6 text-center text-gray-600 border border-gray-200 rounded-md">
            <p>No fingerprinted assets detected</p>
            <p className="text-sm mt-2">
              Deploy to Amplify Hosting to see skew protection in action
            </p>
          </div>
        )}
      </div>
    </main>
  );
}

3. 次に、各リクエストに X-Amplify-Dpl ヘッダーを送信して API リクエストがデプロイメントの一貫性を維持する方法を示す DeploymentTester コンポーネントを作成します。これにより、Amplify は正しい API バージョンにルーティングできます。以下のコードを使用してコンポーネントを作成してください。

// app/components/DeploymentTester.tsx

'use client';

import { useState } from 'react';

interface ApiResponse {
  message: string;
  timestamp: string;
  version: string;
  deploymentId: string;
}

export default function DeploymentTester() {
  const [testInProgress, setTestInProgress] = useState(false);
  const [testOutput, setTestOutput] = useState<ApiResponse | null>(null);
  const [callCount, setCallCount] = useState(0);

  const runApiTest = async () => {
    setTestInProgress(true);
    setCallCount(prev => prev + 1);
    
    try {
      const response = await fetch('/api/skew-protection', {
        headers: {
        // Amplify provides the deployment ID as an environment variable during build time
          'X-Amplify-Dpl': process.env.AWS_AMPLIFY_DEPLOYMENT_ID || '',
        }
      });
      
      if (!response.ok) {
        throw new Error(`API returned ${response.status}`);
      }
      const data = await response.json();
      setTestOutput(data);
    } catch (error) {
      console.error("API call failed", error);
      setTestOutput({
        message: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
        timestamp: new Date().toISOString(),
        version: 'error',
        deploymentId: 'error'
      });
    } finally {
      setTestInProgress(false);
    }
  };

  return (
    <div className="border border-gray-200 rounded-md overflow-hidden bg-white">
      <div className="bg-gray-50 px-4 py-3 flex justify-between items-center border-b border-gray-200">
        <span className="font-medium text-gray-800">Test Deployment Routing</span>
        <button
          onClick={runApiTest}
          disabled={testInProgress}
          className={`px-3 py-1 rounded text-sm ${
            testInProgress
              ? 'bg-gray-200 text-gray-500 cursor-not-allowed'
              : 'bg-blue-600 hover:bg-blue-700 text-white'
          }`}
        >
          {testInProgress ? "Testing..." : "Test Route"}
        </button>
      </div>
      
      <div className="p-4">
        {testOutput ? (
          <div className="p-3 bg-gray-50 rounded border border-gray-200 font-mono text-sm">
            <div className="text-green-600 mb-2">{testOutput.message}</div>
            <div className="text-gray-600 text-xs space-y-1">
              <div>API Version: <span className="text-blue-600 font-medium">{testOutput.version}</span></div>
              <div>Deployment ID: <span className="text-purple-600 font-medium">{testOutput.deploymentId}</span></div>
              <div>Call #: {callCount}</div>
              <div>Time: {new Date(testOutput.timestamp).toLocaleTimeString()}</div>
            </div>
          </div>
        ) : testInProgress ? (
          <div className="p-3 bg-gray-50 rounded border border-gray-200 text-sm text-gray-600">
            Testing deployment routing...
          </div>
        ) : (
          <div className="p-3 bg-gray-50 rounded border border-gray-200 text-sm text-gray-600">
            Click &quot;Test Route&quot; to verify how requests are routed to the correct deployment version
          </div>
        )}
      </div>
    </div>
  );
} 

4. 次に、X-Amplify-Dpl ヘッダーを使用してリクエストがどのデプロイメントから来ているかを識別する API ルートを作成し、Amplify がデプロイメント中にバージョンの一貫性を維持するために API リクエストをルーティングする方法をシミュレートします。以下のコードを使用して API ルートを作成してください。

// app/api/skew-protection/route.ts
import { NextResponse } from 'next/server';
import { type NextRequest } from 'next/server';

// This version identifier can be changed between deployments to demonstrate skew protection
const CURRENT_API_VERSION = "v2.0";

export async function GET(request: NextRequest) {
  // Get the deployment ID from the X-Amplify-Dpl header
  // This is how Amplify routes API requests to the correct deployment version
  const deploymentId = request.headers.get('x-amplify-dpl') || '';
  
  // Determine which version to serve based on deployment ID
  const apiVersion = CURRENT_API_VERSION;
  const message = `Hello from API ${apiVersion}! 🚀`;
  
  // Return the response with deployment information
  return NextResponse.json({
    message,
    version: apiVersion,
    deploymentId: deploymentId || 'none',
    timestamp: new Date().toISOString()
  });
} 

5. クライアントコードからアクセスできるようにするため、Amplify デプロイメントの環境変数を追加します。

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  env: {
    AWS_AMPLIFY_DEPLOYMENT_ID: process.env.AWS_AMPLIFY_DEPLOYMENT_ID || '',
  }
};
export default nextConfig;

6. 変更を GitHub リポジトリにプッシュします。

  • 新しい GitHub リポジトリを作成します
  • 変更を Git ブランチに追加してコミットします
  • リモートオリジンを追加し、変更をアップストリームにプッシュします
git add .
git commit -m "initial commit"
git remote add origin http://github.com/<OWNER>/amplify-skew-protection-demo.git
git push -u origin main

アプリケーションを Amplify Hosting にデプロイする

以下の手順に従って、新しく構築したアプリケーションを AWS Amplify Hosting にデプロイしてください。

  1. AWS Amplify コンソールにサインイン
  2. Create new app を選択し、リポジトリソースとして GitHub を選択します
  3. Amplify が GitHub アカウントにアクセスすることを許可します
  4. 作成したリポジトリとブランチを選択します
  5. App settings を確認し、Next を選択します
  6. 全体の設定を確認し、Save and deploy を選択します

スキュー保護を有効にする

Amplify コンソールで、App settings に移動し、次に Branch settings に移動します。Branch を選択し、アクションドロップダウンメニューから Enable skew protection を選択します。

Screenshot of the branch settings in the AWS Console

図 2 – Amplify Hosting ブランチ設定

次に、デプロイメントページに移動し、アプリケーションを再デプロイします。アプリケーションに対してスキュー保護が有効になると、AWS Amplify はその CDN キャッシュ構成を更新する必要があります。したがって、スキュー保護を有効にした後の最初のデプロイメントには最大 10 分かかることを想定してください。

Screenshot of the AWS Amplify Console. Shows a deployed app

図 3 – Amplify Hosting アプリのデプロイメント

デプロイされた Next.js アプリにアクセスする

Amplify コンソールの Overview タブに移動し、ブラウザで Amplify が生成したデフォルトの URL を開きます。これで、アプリのフィンガープリントされたアセットのリストとデプロイメント ID が表示されるはずです。

Screenshot of Amplify Hosting settings

図 4 – Amplify Hosting アプリ設定 – ブランチレベル

Screenshot of the deployed app to see skew protection demo

図 5 – デモアプリのホームページ

スキュー保護のテスト

Next.js アプリケーションを Amplify にデプロイすると、各デプロイメントには一意のデプロイメント ID が割り当てられます。この ID は、バージョンの一貫性を確保するために、静的アセット(JS, CSS)と API ルートに自動的に挿入されます。実際の動作を見てみましょう。

  1. アセットフィンガープリント:各静的アセット URL (JavaScript ファイルや CSS ファイルなど)には、現在のデプロイメント ID を示す「?dpl=」パラメータが自動的に付加されます。例えば「main.js?dpl=abc123」のような形式です。これにより、ブラウザは常にアセットの正しいバージョンを取得できます。
  2. API ルーティング:Test Route ボタンは、Amplify がどのようにして API リクエストをルーティングするかを示しています。クリックすると、/api/skew-protection エンドポイントにリクエストを送信します。リクエストは現在のデプロイメント ID に一致する X-Amplify-Dpl ヘッダーを使用するため、正しい API バージョンへのルーティングが保証されます。

これは、デプロイメント中であっても、ユーザーがバージョンの不一致を体験せず、各ユーザーのセッションが最初に読み込んだバージョンと一貫性を保つことを意味します。これにより、クライアントとサーバーのバージョンが一致しない場合に発生する可能性のあるバグを防止します。

自分で試してみよう

  1. 現在のブラウザタブを開いたままにして、Test Route をクリックして、 API バージョンとデプロイメント ID が一致することを確認します。
  2. api/skew-protection/route.ts で異なる CURRENT_API_VERSION を持つ新しいバージョンをデプロイします。
  3. 新しいシークレットウィンドウでアプリケーションを開きます。
    1. 動作を比較します。
      • 元のタブは古いバージョンを維持します
      • 新しいシークレットウィンドウは新しいバージョンを表示します
      • 各タブのアセットと API コールは、それぞれのバージョンと一貫して一致します
      • 両方のウィンドウで Test Route を繰り返しクリックしてみてください。それぞれが一貫して対応するバージョンにルーティングされ、複数のバージョンが同時に稼働している場合でも Amplify がセッションの一貫性を維持する方法を示しています

Screenshot of the demo app comparing both versions

図 6 – スキュー保護の動作の比較

これは、デプロイメント中にアプリケーションの複数のバージョンが実行されている場合でも、Amplify が各ユーザーセッションのバージョンの一貫性をどのように維持するかを示しています。

おめでとうございます。Amplify Hosting 上の Next.js アプリケーションデプロイメントでスキュー保護を正常に作成して検証しました。

クリーンアップ

App settings に移動し、次に General settings に進み、Delete app を選択して、AWS Amplify アプリを削除します。

次のステップ

  1. アプリケーションのスキュー保護を有効にしましょう
  2. この機能についてもっと学ぶには、ドキュメントをお読みください!

本記事は Amplify Hosting Announces Skew Protection Support を翻訳したものです。翻訳は Solutions Architect の都築が担当しました。

著者について

Matt Headshot

Matt Auerbach, Senior Product Manager, Amplify Hosting

Matt Auerbach is a NYC-based Product Manager on the AWS Amplify Team. He educates developers regarding products and offerings, and acts as the primary point of contact for assistance and feedback. Matt is a mild-mannered programmer who enjoys using technology to solve problems and making people’s lives easier. B night, however…well he does pretty much the same thing. You can find Matt on X @mauerbac. He previously worked at Twitch, Optimizely and Twilio.

Jay Author

Jay Raval, Solutions Architect, Amplify Hosting

Jay Raval is a Solutions Architect on the AWS Amplify team. He’s passionate about solving complex customer problems in the front-end, web and mobile domain and addresses real-world architecture problems for development using front-end technologies and AWS. In his free time, he enjoys traveling and sports. You can find Jay on X @_Jay_Raval_