フロントエンドの変更を安全にリリースする方法

フロントエンドの新しいバージョンをプッシュしたとします。ビルドは成功し、テストはすべてパス、デプロイパイプラインも「成功」と表示しています。しかし、本番環境のメトリクスを確認すると、何かがおかしい。一部のユーザーにはレイアウトが崩れて表示され、別のユーザーはボタンが動作しないと報告しています。さらに、ブラウザが新しいファイルを取得できていないため、古いバージョンを使い続けているユーザーもいます。

これがフロントエンドリリースの現実です。バックエンドサービスと違い、サーバーを再起動すれば即座にバージョンを切り替えられるわけではありません。フロントエンドのコードはユーザーのブラウザ上で動作します。すべてのユーザーに強制的にリロードさせることはできません。キャッシュの有効期限を制御することもできません。そして、何か問題が発生した場合、ロールバックボタンを押せばすぐに全員が修正を受け取れるわけでもありません。

なぜフロントエンドリリースは異なるのか

根本的な問題は、フロントエンドのアセットが数千のブラウザに分散しており、それぞれが独自のキャッシュ状態を持っていることです。新しいバージョンをデプロイすると、すぐに新しいバージョンを取得するユーザーもいれば、キャッシュの有効期限が切れるまで古いバージョンを使い続けるユーザーもいます。また、手動でリロードするまで壊れたバージョンに固定されてしまうユーザーもいます。

CDNを通じて配信される静的フロントエンドの場合、課題は自分が制御するサーバーにデプロイしているわけではないことです。ファイルをストレージバケットにアップロードし、CDNに配信を任せています。CDNはキャッシュヘッダーやエッジノードの動作によって、一部のユーザーには古いファイルを、別のユーザーには新しいファイルを配信する可能性があります。

サーバーサイドレンダリング(SSR)のフロントエンドの場合、課題は異なります。アクティブな接続を切断せずに更新する必要がある実際のサーバーを扱うことになります。フォームに入力しているユーザーやページ間を移動しているユーザーが、バージョン切り替えによってセッションを失うことがあってはなりません。

以下のフローチャートは、フロントエンドの種類とリスク許容度に基づいて適切なリリース戦略を選択するのに役立ちます。

flowchart TD A[フロントエンドの種類は?] --> B[静的] A --> C[SSR] B --> D[CDNによる段階的ロールアウト] D --> E[エラーを監視] E --> F{エラー率は低い?} F -->|はい| G[トラフィックを増やす] F -->|いいえ| H[ロールバック: ポインタを切り替え] G --> I[100%ロールアウト] C --> J{リスク許容度は?} J -->|低い| K[カナリアリリース] J -->|非常に低い| L[ブルーグリーンデプロイメント] K --> M[少数のインスタンスにデプロイ] M --> N[ヘルスを監視] N --> O{健全?} O -->|はい| P[徐々にインスタンスを追加] O -->|いいえ| Q[新しいインスタンスを削除] L --> R[グリーン環境をデプロイ] R --> S[ロードバランサを切り替え] S --> T{問題あり?} T -->|はい| U[ブルーに戻す] T -->|いいえ| V[グリーンを維持]

静的フロントエンドの段階的ロールアウト

静的フロントエンドをリリースする最も安全な方法は、新しいバージョンを表示するユーザー数を制御することです。ほとんどのCDNは重み付け分散をサポートしており、リクエストの一定割合を新しいファイルに、残りを古いファイルに送信できます。

典型的な流れは次のとおりです。

  1. 新しいバージョンをストレージバケットにアップロードします。パスは一意にし、例えば app-v2/ やコンテンツハッシュを含むファイル名を使用します。
  2. CDNを設定し、リクエストの5%を新しいファイルにルーティングします。
  3. エラー率、ページ読み込み時間、ユーザーからの報告を監視します。
  4. 問題がなければ、割合を25%、50%、そして100%へと徐々に増やします。

クライアントサイドでフィーチャーフラグを使用する方法もあります。すべてのユーザーに同じアプリケーションシェルを提供し、Cookie、URLパラメータ、ユーザーIDに基づいて新しいコンポーネントや機能を条件付きで読み込みます。これにより、CDNレベルのトラフィック分割を必要とせず、誰が何を見るかをより細かく制御できます。

段階的ロールアウトの主な利点は、いつでも停止できることです。10%の時点でエラー率が急上昇したら、割合をゼロに戻します。すでに新しいバージョンを読み込んだユーザーには問題が残るかもしれませんが、影響範囲を限定できます。

SSRフロントエンドのカナリアリリース

SSRフロントエンドの場合、段階的ロールアウトはバックエンドのカナリアデプロイメントとほぼ同じように機能します。ロードバランサの背後でアプリケーションの複数のインスタンスを実行します。新しいバージョンをリリースする際は、1つまたは2つのインスタンスにデプロイし、残りは古いバージョンのままにします。

ロードバランサは、新しいインスタンスに少量のトラフィックを送信するように設定します。ヘルスチェックが失敗したりエラー率が上昇した場合、それらのインスタンスはローテーションから外されます。ロールバックは、新しいインスタンスをシャットダウンし、すべてのトラフィックを古いインスタンスに戻すだけです。

このアプローチは、SSRアプリケーションがサーバーサイドで状態を維持するため、うまく機能します。ユーザーのセッションは特定のインスタンスに結びついているため、セッションデータをRedisやデータベースなどの共有ストレージに保存する必要があります。そうしないと、バージョン間でトラフィックを切り替えた際にユーザーがログアウトしてしまいます。

このプロセスを自動化するには、デプロイメント設定でカナリアロールアウトを定義します。以下は、Kubernetes上でArgo Rolloutsを使用した例です。

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: frontend-ssr
spec:
  replicas: 10
  strategy:
    canary:
      steps:
        - setWeight: 10
        - pause: { duration: 5m }
        - setWeight: 50
        - pause: { duration: 5m }
        - setWeight: 100
      analysis:
        templates:
          - templateName: success-rate
        startingStep: 0
        args:
          - name: service-name
            value: frontend-ssr
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  metrics:
    - name: error-rate
      successCondition: result < 0.01
      provider:
        prometheus:
          query: |
            sum(rate(http_requests_total{status=~"5.."}[5m])) /
            sum(rate(http_requests_total[5m]))

この設定では、新しいバージョンに10%のトラフィックをルーティングし、5分間待機してエラー率を監視します。その後、エラー率が1%未満に保たれている場合に限り、50%、そして100%へと進みます。

ゼロダウンタイムを実現するブルーグリーンデプロイメント

リリース中にダウンタイムを一切許容しない場合、ブルーグリーンデプロイメントは堅実な選択肢です。2つの同一環境を維持します。ブルー(現在のバージョン)とグリーン(新しいバージョン)です。すべてのトラフィックはブルーに送られます。グリーンの準備が整い、すべてのヘルスチェックに合格したら、ロードバランサを切り替えてすべてのトラフィックをグリーンに送信します。

切り替え後に問題が発生した場合は、ブルーに戻します。プロセス全体は数秒で完了し、セッション状態が適切に処理されていれば、ユーザーは中断を感じません。

欠点は、2倍のインフラストラクチャが必要になることです。小規模なチームやトラフィックの少ないアプリケーションでは過剰かもしれません。しかし、数秒のダウンタイムが金銭的損失や信頼の低下につながる高トラフィックの本番システムでは、ブルーグリーンはそのオーバーヘッドに見合う価値があります。

本当の課題: 静的フロントエンドのロールバック

ここが厄介なところです。静的フロントエンドのロールバックは技術的には単純ですが、実際には混乱を招きます。CDN上のファイルを古いバージョンに戻すことはできますが、すでに新しいファイルをダウンロードしたユーザーは自動的に古いファイルを取得しません。ブラウザキャッシュには新しいバージョンが残っており、キャッシュの有効期限が切れるまでそれが提供され続けます。

解決策は、ファイルを決して上書きしないことです。すべてのバージョンに一意のパスを割り当て、通常はコンテンツハッシュに基づいてパスを生成します。デプロイ時には、新しいファイルを古いファイルと一緒にアップロードします。CDNはアプリケーションが指し示すバージョンを提供します。ロールバックするには、ポインタを新しいバージョンから古いバージョンに変更します。すでに新しいバージョンをキャッシュしているユーザーはキャッシュがクリアされるまで使い続けますが、新しいユーザーやリフレッシュしたユーザーは古いバージョンを取得します。

このアプローチは、ロールバックが必要になる前に、ロールバックについて考えておく必要があることを意味します。パイプラインがファイルをその場で上書きするように設計されている場合、問題が発生したときに復旧が困難になります。

フロントエンドリリースの実践的チェックリスト

次のリリースをプッシュする前に、このチェックリストを確認してください。

  • すべてのファイルパスはバージョンごとに一意ですか?(コンテンツハッシュまたはバージョン管理)
  • 新しいバージョンに少量のトラフィックをルーティングできますか?
  • エラー率とページ読み込み時間をリアルタイムで監視する方法はありますか?
  • ロールバック計画は文書化されているだけでなく、実際にテストされていますか?
  • SSRの場合: セッション状態はアプリケーションサーバーの外部に保存されていますか?
  • 静的な場合: 再デプロイせずにバージョンポインタを切り替える仕組みはありますか?

最も重要なこと

段階的ロールアウトとロールバック戦略は、パイプライン構築後に後付けで追加できる機能ではありません。これらは最初からデプロイプロセスに設計される必要があります。パイプラインが1つのバージョンを1つの場所にデプロイすることしか知らない場合、カナリアリリースやブルーグリーンデプロイメントを行うのは困難です。CDNやロードバランサレベルでトラフィック管理ができなければ、問題のあるリリースの影響を限定する方法がありません。

目標はすべての問題を防ぐことではありません。問題は起こるものです。目標は、問題が発生したときにパニックにならずに対応できるようにすることです。適切に設計されたリリース戦略は、影響範囲を制御し、明確なロールバックパスを提供し、頻繁に変更をリリースする自信を与えます。それがなければ、すべてのリリースがギャンブルのように感じられます。