デプロイの順序がパイプラインよりも重要な理由
アプリケーションの新しいバージョンが準備できました。パイプラインはグリーンです。チームが見守る中、あなたはデプロイボタンを押します。数分後、ログにエラーが現れ始めます。ユーザーから購入が完了できないと報告が入ります。データベースチームは、スキーマ変更がアプリケーションの起動後、つまり起動前に適用されるべきだったと言います。
このシナリオは珍しくありません。なぜなら、デプロイとは単一のアプリケーションが単一の処理を行うことではほとんどないからです。それは、アプリケーションが依存する他のすべてのものとどのように接続するかという問題です。それらの接続と、それらがもたらすリスクを理解することが、スムーズなデプロイと本番インシデントを分けるものです。
依存関係はコードだけではない
現代のアプリケーションが単独で動作することはほとんどありません。データベースから読み取り、決済処理のためにAPIを呼び出し、画像リサイズにサードパーティライブラリを使用し、通知送信のためにメッセージキューに依存します。これらのそれぞれが依存関係であり、新しいバージョンをデプロイするときにそれぞれが壊れる可能性があります。
最も一般的な依存関係はデータベースです。アプリケーションはPostgreSQLやMySQLにユーザーデータを保存します。新しいバージョンをデプロイすると、そのバージョンは古いバージョンとは異なる方法でデータの読み書きを行います。古いバージョンがユーザーの住所を1つのカラムに保存していたのに対し、新しいバージョンはそれを番地、市区町村、郵便番号に分割するかもしれません。新しいバージョンがデータベーススキーマの更新前に起動すると、既存のデータを読み取れずに失敗します。これは破壊的変更です。古いバージョンと新しいバージョンは互いに互換性がありません。
依存関係には、他チームや外部サービスのAPIも含まれます。アプリケーションが決済APIを呼び出すとします。新しいバージョンは異なるJSON形式のレスポンスを期待しています。そのAPIがまだ更新されていない場合、アプリケーションは解析できないデータを受け取り、トランザクションは失敗します。問題は、そのAPIの変更を常に自分たちで制御できるとは限らないことです。別のチームには独自のデプロイスケジュールがあり、あなたのアプリケーションが特定のレスポンス形式に依存していることを知らないかもしれません。
サードパーティのライブラリやパッケージも依存関係です。ライブラリをバージョン1.0から2.0に更新すると、関数名が変更されたり、シグネチャが変わったりする可能性があります。アプリケーションが古い関数名を呼び出し続けると、実行時にエラーが発生します。これが、成熟したチームが依存関係を requirements.txt、package.json、go.mod などのファイルで明確に追跡し、本番で使用する前に新しいライブラリバージョンをテストする理由です。
隠れたリスク:デプロイ順序
依存関係は、デプロイの順序に直接影響します。アプリケーションAがスキーマ変更を必要とするデータベースに依存している場合、最初にデータベースを更新し、次にアプリケーションAをデプロイする必要があります。アプリケーションBがアプリケーションCのAPIを呼び出す場合、最初にアプリケーションCをデプロイし、次にアプリケーションBをデプロイする必要があります。この順序が重要なのは、逆にすると、新しくデプロイされたアプリケーションがまだ利用できないデータやサービスを探すからです。結果として、エラー、リクエストの失敗、またはアプリケーションの完全な停止が発生します。
次のシーケンス図は、正しいデプロイ順序と、それが逆転した場合に何が起こるかを示しています。
リスクは依存関係の数に比例して増大します。同期を取る必要のあるサービスが増えれば増えるほど、そのうちの1つが同期されていない可能性が高まります。これをうまく処理するチームは、デプロイ前にすべての依存関係をマッピングし、正しい順序を確認し、何かが一致しない場合に備えてロールバック計画を準備します。また、後方互換性(新しいバージョンが古いバージョンでも動作するようにする)などの手法を使用して、破壊的変更のリスクを低減します。
破壊的変更は常に明白とは限らない
破壊的変更は劇的である必要はありません。微妙な場合もあります。例えば、アプリケーションの新しいバージョンが内部APIへのリクエストに余分なフィールドを送信し始めるかもしれません。受信側のサービスは未知のフィールドを無視するため、すべてが正常に見えます。しかし数週間後、その受信側のサービスが更新され、その余分なフィールドが存在することを期待するようになります。一部の環境でまだ実行されている古いバージョンのアプリケーションは動作しなくなります。障害は遅延して発生し、根本原因を追跡するのは困難です。
このため、依存関係のマッピングは一度限りの活動ではありません。システムの進化に合わせて更新する必要があります。新しい依存関係が追加されたり、既存の依存関係が変更されたりするたびに、デプロイ順序とリスクプロファイルも変化します。
依存関係リスクを低減する方法
依存関係が関わる場合にデプロイをより安全にするための実用的な手順があります。
まず、依存関係を文書化します。これは誰も読まない長い文書を書くという意味ではありません。アプリケーションが何に依存し、何がそれに依存しているかについて、明確で機械可読な記録を持つことを意味します。依存関係グラフ、サービスカタログ、あるいはリポジトリ内のシンプルなREADMEなどのツールが役立ちます。
次に、ユニットテストだけでなく、統合テストを実施します。すべての外部サービスをモックするユニットテストでは、実際の依存関係の振る舞いによって引き起こされる問題を発見できません。実際のデータベース、API、メッセージキューに対して実行する統合テストは、問題が本番に到達する前に表面化させます。
第三に、フィーチャーフラグやバージョン管理されたAPIを使用して後方互換性を維持します。新しいバージョンが古いクライアントからのリクエストにも対応できる場合、デプロイ順序の柔軟性が高まります。新しいバージョンを先にデプロイし、動作を確認してから、依存するサービスを更新できます。
第四に、ロールバックを練習します。依存関係の不一致が原因でデプロイが失敗した場合に、何が起こる必要があるかを正確に把握します。データベーススキーマの変更を元に戻せますか?アプリケーションを古いAPIバージョンに戻せますか?テスト済みのロールバック計画があれば、デプロイ中のプレッシャーが軽減されます。
例えば、以下のシンプルな順次デプロイスクリプトは、正しい順序を強制し、失敗時に停止します。
#!/bin/bash
# deploy.sh - 正しいデプロイ順序を強制する
set -e # エラーが発生したら終了
echo "Step 1: データベーススキーマをデプロイ"
./deploy_database.sh || { echo "データベースのデプロイに失敗しました。中止します。"; exit 1; }
echo "Step 2: バックエンドAPIをデプロイ"
./deploy_api.sh || { echo "APIのデプロイに失敗しました。データベースをロールバック中..."; ./rollback_database.sh; exit 1; }
echo "Step 3: フロントエンドをデプロイ"
./deploy_frontend.sh || { echo "フロントエンドのデプロイに失敗しました。APIとデータベースをロールバック中..."; ./rollback_api.sh; ./rollback_database.sh; exit 1; }
echo "すべてのデプロイが正常に完了しました。"
次のデプロイ前の実用的チェックリスト
デプロイする前に、この短いチェックリストを確認してください。
- アプリケーションが使用するすべての依存関係(データベース、API、ライブラリ)をリストアップしましたか?
- 各依存関係の正しいデプロイ順序を把握していますか?
- 新しいバージョンを、それらの依存関係の実際のバージョンに対してテストしましたか?
- 壊れる可能性のある各依存関係に対するロールバック計画はありますか?
- 依存関係を所有するすべてのチームにデプロイ順序を伝えましたか?
このチェックリストは網羅的ではありませんが、最も一般的な障害ポイントをカバーしています。5つの質問すべてに「はい」と答えられれば、ほとんどのチームよりもはるかに良い立場にあります。
まとめ
デプロイとは、単にコードをプッシュすることではありません。アプリケーションと、それが触れるすべてのものとの間の関係を管理することです。依存関係は、デプロイの順序、失敗のリスク、そして復旧の難しさを決定します。依存関係を理解し、それに備えるチームは、インシデントが少なく、復旧が速く、すべてのリリースに自信を持つことができます。パイプラインは重要ですが、パイプラインを火災訓練に変えないようにするのは依存関係マップです。