フロントエンドWebのCI/CDがバックエンドのCI/CDと異なる理由

チームがCI/CDパイプラインを構築し始めるとき、最初に思い浮かぶのは通常バックエンドです。再起動するサーバー、実行するデータベースマイグレーション、正しいレスポンスを確認するAPIがあります。バックエンドはチームが管理するインフラ上で動作します。OS、ランタイムバージョン、利用可能なメモリ、サーバーが最後に再起動された時刻を把握できます。何かが壊れたらSSHでログインし、ログを確認し、プロセスを再起動します。

フロントエンドWebはそうはいきません。アプリケーションがデプロイされると、JavaScript、HTML、CSSはユーザーのブラウザに送信されます。そのブラウザはChrome、Firefox、Safari、あるいはまったく別のものかもしれません。ユーザーはWindowsノートPC、Mac、Androidスマートフォンを使っているかもしれません。インターネット接続は高速か、あるいは非常に遅いかもしれません。チームはこれらの変数をまったく制御できません。制御できるのは、出荷するコードだけです。つまり、コードが正しく、軽量で、可能な限り多くの環境と互換性があることです。

この違いにより、フロントエンドパイプラインの設計方法が根本的に変わります。バックエンドデプロイで機能する戦略は、フロントエンドコードに適用すると、しばしば失敗したり、新たな問題を引き起こしたりします。

静的アセット vs サーバーサイドレンダリング

フロントエンドWebのデプロイでは、通常2種類のアセットが関わります。静的アセットは、誰がリクエストしても変わらないファイルです。HTML、CSS、JavaScript、画像、フォントなどです。これらのファイルはCDNに保存し、ブラウザでキャッシュし、ユーザーに最も近いサーバーから配信できます。デプロイは簡単です。ストレージバケットやCDNにアップロードすれば、ユーザーがページをリフレッシュしたときに最新バージョンを取得できます。

複雑さはキャッシュに起因します。新しいバージョンをデプロイした後でも、ブラウザやCDNが古いファイルを配信し続ける可能性があります。1時間前にサイトを訪れたユーザーは、キャッシュヘッダーやサービスワーカーの動作によって、数時間または数日間古いUIを見続ける可能性があります。これはバックエンドチームがほとんど対処しない、フロントエンド固有の問題です。バックエンドAPIの変更はサーバー上で即座に反映されます。フロントエンドの変更は、ブラウザが古いファイルを保持することを決定したため、ユーザーから見えないことがあります。

もう1つのタイプのフロントエンドアセットは、サーバーサイドレンダリング(SSR)コンテンツです。SSRでは、ページはサーバー上で構築され、準備されたHTMLとしてブラウザに送信されます。このアプローチは、初期ページ読み込みが速い、SEOに優れている、ユーザーごとに動的コンテンツが変わるアプリケーションで一般的です。SSRは、少なくとも初期レンダリングにおいて、フロントエンドコードが制御下のサーバーで実行されることを意味します。しかし、ページをハイドレートするJavaScriptは依然としてユーザーのブラウザで実行されるため、互換性の問題は残ります。SSRは別のデプロイターゲットを追加します。HTMLを生成するサーバープロセスを管理し、スケーリングを処理し、エラーを監視する必要があります。

デプロイ後にCDNに最新ファイルを強制的に取得させるには、キャッシュを無効化します。例えば、AWS CloudFrontの場合:

aws cloudfront create-invalidation \
  --distribution-id E1234567890ABC \
  --paths "/*"

このコマンドは、CDNにキャッシュされたすべてのファイルを破棄し、オリジンから新しいコピーを要求するよう指示します。この手順を踏まないと、デプロイが成功した後でも、ユーザーは数時間にわたって古いアセットを見続ける可能性があります。

API依存関係の問題

フロントエンドアプリケーションが単独で動作することはほとんどありません。ほぼすべての最新Webアプリは、APIからデータを取得し、ユーザー入力を送信し、認証を処理します。これにより、フロントエンドとバックエンドの間に密結合が生じ、パイプライン設計が見た目以上に難しくなります。

新しいフロントエンドバージョンをリリースするときは、バックエンドAPIがまだ一致していることを確認する必要があります。バックエンドがフロントエンドチームに知らせずにレスポンス構造を変更した場合、ユーザーは壊れたページや欠落したデータを目にすることになります。フロントエンドがバックエンドにまだ存在しないエンドポイントを呼び出し始めると、アプリケーションはエラーになります。

つまり、フロントエンドパイプラインは「ビルド成功」で終わるわけにはいきません。パイプラインは、リリースしようとしているフロントエンドバージョンが、現在本番環境で動作しているAPIと互換性があることも検証する必要があります。チームは、どのAPIバージョンが稼働しているか、破壊的変更が導入されたかどうか、フロントエンドとバックエンドが異なるタイミングでリリースされる場合の移行期間をどのように処理するかを把握する必要があります。

実際には、これはしばしばバージョン管理されたAPI、フィーチャーフラグ、またはフロントエンドとバックエンドのリリースサイクル間の慎重な調整につながります。一部のチームは、フロントエンドパイプラインがデプロイを許可する前に、既知のAPI仕様に対してテストを実行するコントラクトテストアプローチを採用しています。他のチームは、フロントエンドでカナリアリリースやブルーグリーンデプロイメントを使用して、非互換性を早期に発見します。

フロントエンドのテストは異なる

フロントエンドのユニットテストは、バックエンドとは異なる形状を持ちます。バックエンドのユニットテストは通常、関数やエンドポイントを呼び出し、レスポンスをチェックします。フロントエンドのユニットテストは、ユーザーインタラクション、ブラウザの動作、視覚的な出力を考慮する必要があります。

ユニットテストを個々の関数やクラスのテストと同一視する業界の習慣は、フロントエンドでは誤解を招きます。ユニットテストは、関連するエントリポイントからの1つの意味のある振る舞いを検証する必要があります。フロントエンドの場合、そのエントリポイントは多くの場合、ユーザーアクションです。クリック、フォーム送信、ナビゲーション、または目に見える状態変化です。テストは、内部メソッドが正しい引数で呼び出されたことを証明するのではなく、システムがそのインタラクションに正しく応答することを証明する必要があります。

つまり、内部コードをリファクタリングしたからといってテストスイートが壊れてはいけません。コンポーネントの内部状態管理方法を変更しても、ユーザーに同じ結果が表示されるのであれば、テストはパスするべきです。実装の詳細に密結合したテストはメンテナンスの負担となり、実際の安全性を追加することなくパイプラインを遅くします。

フロントエンドの場合、関連するテスト環境にはブラウザランタイムが含まれる場合があります。テスト対象の動作にブラウザAPI、レイアウト計算、またはランタイム機能が必要な場合、エミュレーターやシミュレーターはユニットテストの有効な実行環境となり得ます。物理デバイスは、カメラ、センサー、ネットワークの変動、現実世界のパフォーマンスなど、ハードウェアに依存するシナリオに依然として必要です。

ビルドステップは単なるコンパイルではない

バックエンドのCI/CDでは、ビルドステップは通常、コードをコンパイルし、デプロイ可能なアーティファクトにパッケージ化することを意味します。フロントエンドの場合、ビルドステップはさらに多くのことを行います。JavaScriptモジュールをバンドルし、コードを最小化し、画像を最適化し、ソースマップを生成し、環境変数を注入し、異なるブラウザやデバイス向けに複数のファイルバージョンを生成します。

ビルド出力は単一のアーティファクトではありません。これは、ファイル名にキャッシュ無効化用のハッシュが付いたファイルのコレクションです。これらのハッシュはキャッシュ管理にとって重要です。新しいバージョンをデプロイすると、変更されたファイルのみが新しいハッシュを取得します。変更されなかったファイルは古いハッシュを保持するため、ブラウザキャッシュはそれらを引き続き提供します。これにより、すでにサイトを訪れたユーザーのダウンロードサイズが削減されます。

しかし、キャッシュ無効化は、パイプラインが正しく処理した場合にのみ機能します。ビルドプロセスが変更されたファイルに一意のハッシュを生成しない場合、またはデプロイプロセスがHTMLの参照を新しいハッシュに更新しない場合、ユーザーは古いファイルと新しいファイルが混在した状態になります。結果として、一部のブラウザや特定のナビゲーションパターンでのみ発生する、デバッグが困難なUIの破損が発生します。

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

  • ビルドステップが変更されたファイルに対して一意のキャッシュ無効化ハッシュを生成することを確認する。
  • モックやステージングAPIだけでなく、本番環境で動作している正確なAPIバージョンに対してフロントエンドをテストする。
  • 内部関数のユニットテストだけでなく、実際のユーザーインタラクションをシミュレートするブラウザベースのテストを含める。
  • 鮮度とパフォーマンスのバランスが取れたキャッシュヘッダーポリシーを設定する。HTMLには短いキャッシュ時間、ハッシュ化されたアセットには長いキャッシュ時間。
  • リリース前に意図しないUI変更を検出するために、簡単なビジュアルリグレッションチェックを実行する。
  • デプロイプロセスが、サービスワーカーファイルを含むすべての新しいアセットハッシュへの参照を更新することを確認する。

具体的な要点

フロントエンドのCI/CDは、バックエンドのCI/CDの簡略版ではありません。それ自体に制約があります。制御不能なユーザー環境、リリースを隠したり壊したりする可能性のあるキャッシュ動作、バックエンドAPIへの密結合、そしてコードをコンパイルする以上のことを行うビルドステップです。フロントエンドのデプロイを「単にファイルをアップロードするだけ」と扱うと、診断が困難なユーザー体験の破綻を招きます。コードが他人のブラウザで、他人のネットワーク上で実行され、ファーストインプレッションを成功させるチャンスは一度しかないという現実に基づいて、フロントエンドパイプラインを構築してください。