最初の実ユーザーが来た瞬間に手動アップデートが破綻する理由
あなたはラップトップでバグを修正した。修正したファイルをSCPでサーバーにアップロードし、アプリケーションを再起動した。バグは消えた。シンプルだ。
では、そのアプリケーションを100人が使っている状況を想像してほしい。もう好きなときに再起動はできない——ユーザーがセッションの途中で強制切断されるからだ。トラフィックをさばくためにアプリケーションが3台のサーバーで動いているとしよう。同じ修正ファイルを3台すべてに、1台ずつアップロードしなければならない。1台でも忘れると、一部のユーザーには壊れたバージョンが表示され続ける。さらに悪いことに、同じリクエストの中で新旧のコードが混ざってエラーが発生するかもしれない。
ここがソフトウェアデリバリーの本当の難しさの始まりだ。パイプラインやツールの問題ではない。アプリケーションは絶えず変化し、手動のプロセスでは追いつけなくなるという単純な事実が原因なのだ。
変更の本当の発生源
アプリケーションは最初のリリースで完成しない。進化し続ける。変更はあらゆる方向からやってくる。
- 実ユーザーが特定の機能を使い始めて初めて表面化するバグ
- 次のリリースに向けて計画された新機能
- サーバーが増大するトラフィックに耐えられず必要になる設定変更
- 即座に適用しなければならないセキュリティパッチ
これらの変更はすべて、アプリケーションが動作している場所に届けられなければならない。そして、ユーザーがアプリケーションを使い続ける限り、繰り返し届ける必要がある。
ビルドの一貫性の落とし穴
チームで日常的に起こるシナリオを紹介しよう。ラップトップでバグを修正した。ローカル環境では完璧に動く。ファイルをサーバーにアップロードして再起動すると……アプリケーションがクラッシュした。
何が起きたのか? ラップトップとサーバーでライブラリのバージョンが違うのかもしれない。サーバーにしかない設定ファイルの更新を忘れたのかもしれない。異なるコンパイル設定でコードをビルドしたのかもしれない。修正は手元のマシンでは動いたが、サーバー環境は微妙に異なっていたのだ。
この時点で、なぜ本番環境がローカルと違う動作をするのかをデバッグすることになる。問題を修正して再アップロードし、今度はうまくいくことを願う。しかし、手動変更のたびに同じ不確実性がつきまとう。
これは不注意の問題ではない。手動プロセスを繰り返すことの根本的な信頼性の低さの問題だ。手動でビルドするたびに、何かが少しだけ異なる結果になる可能性がわずかながら存在する。たった1つの違いが、動いているアプリケーションを壊す原因になる。
テストの盲点
手動テストにも同じ問題がある。変更を手動でテストするとき、前回行ったすべての手順を覚えていなければならない。ログインフローは確認したか? この小さな変更の影響を受けるかもしれないあの機能は検証したか? 先月壊れたエッジケースはテストしたか?
更新頻度が高くなればなるほど、テストのステップを飛ばす可能性が高くなる。そして1つでも飛ばすと、バグが本番に紛れ込む。怠けているからではない。人間の記憶は、何十もの手順を毎回完璧に繰り返すようにはできていないのだ。
マルチサーバーの悪夢
3台のサーバーのシナリオに戻ろう。同じファイルを全サーバーにアップロードできたとしても、今度はタイミングの問題が発生する。サーバー2にアップロードしている間、サーバー1はすでに新しいコードで動いており、サーバー3はまだ古いコードのままである。ユーザーは負荷に応じて異なるサーバーにルーティングされるため、同じセッションの中で異なるバージョンのアプリケーションを目にする可能性がある。
実際の手動アップデートは次のようになる。
# 各サーバーに1台ずつ手動アップロード
scp app.jar user@server1.example.com:/opt/app/
ssh user@server1.example.com 'systemctl restart app'
scp app.jar user@server2.example.com:/opt/app/
ssh user@server2.example.com 'systemctl restart app'
scp app.jar user@server3.example.com:/opt/app/
ssh user@server3.example.com 'systemctl restart app'
サーバーが10台になったり、どのサーバーを更新したか忘れたりすることを想像してほしい。単純なスクリプトのループでリスクを減らせる。
# スクリプト化されたループ——エラーの余地が少ない
for server in server1 server2 server3; do
scp app.jar "user@$server.example.com:/opt/app/"
ssh "user@$server.example.com" 'systemctl restart app'
done
この小さなスクリプトだけで、サーバーをスキップしたり間違った順序で再起動したりする可能性はなくなる。ただし、スクリプトを実行することを覚えていて、正しいファイルがローカルにあることが前提だ。
この不整合は、再現がほぼ不可能なバグを生み出す。ユーザーが問題を報告するが、ログを確認する頃には全サーバーが同じバージョンになっている。問題は消えているが、なぜ消えたのかわからない。そして次の手動アップデートで再発する。
一貫性が譲れなくなる理由
ここでパターンが明確になる。手動プロセスには一貫性がない。ビルド、テスト、デプロイを手動で行うたびに、わずかなバラつきが生じる可能性がある。ビルドプロセスの1つのバラつき、1つのテストのスキップ、1台のサーバーの見落とし——これらのどれかが問題を引き起こす。そして頻繁に更新する場合、少なくとも1つのバラつきが発生する確率はほぼ確実になる。
これは怠けたいからでも、自動化のために自動化したいからでもない。手動プロセスでは、稼働中のアプリケーションに必要な一貫性を提供できないという認識の問題だ。ユーザーは、あなたが長い一日を終えて1つのシナリオをテストし忘れたことなど気にしない。彼らが気にするのは、アプリケーションが動くかどうかだけだ。
一貫性とは次のことを意味する。
- すべてのビルドが、実際に変更されたコード以外は同じ結果を生成する
- すべてのテスト実行が、同じシナリオを同じ方法でカバーする
- すべてのデプロイが、すべてのサーバーで同じ手順に従う
実践的な転換
これこそが、チームがビルド、テスト、デプロイのプロセスを標準化する方法を模索し始める瞬間だ。ブログ記事でCI/CDについて読んだからではない。一貫性のない手動アップデートの痛みを経験したからだ。忘れられた設定ファイルが原因で深夜にデバッグする羽目になった。サーバーを1台見落としたことでユーザーからの苦情に対処した。
転換が起こるのは、手動プロセスが単に遅いだけでなく、繰り返し行う作業には信頼性が低いと気づいたときだ。そしてアプリケーションには、修正すべきバグ、追加すべき機能、変更すべき設定がある限り、常に繰り返しのアップデートが必要になる。
簡単な一貫性チェック
自動化を始める前に、次の基本が整っているか確認してほしい。
- どのマシンでも、アプリケーションのまったく同じバージョンをゼロから再ビルドできるか?
- 現在のバージョンと前のバージョンの間で、どのファイルが変更されたかを正確に把握しているか?
- すべてのサーバーが現在同じバージョンを実行していることを確認できるか?
- 他の誰かが実行できる、ステップバイステップのデプロイ手順書があるか?
これらのいずれかが「いいえ」なら、そこから始めよう。ツールやパイプラインはその後だ。一貫性が最優先である。
チームにとっての意味
次に手動デプロイを行うときは、すべての手順に注意を払ってほしい。何気なく行っている小さな判断——どのファイルを最初にアップロードするか、どのサーバーを最後に更新するか、どのテストを実行するか——に気づくこと。それらの小さな判断の中に、不整合が潜んでいる。
目標は、すべての手動作業を一夜でなくすことではない。手動プロセスには限界があると認識することだ。1台のサーバーと1人の開発者ならうまくいく。しかし、複数のサーバー、複数の環境、週に複数回のアップデートがあると破綻する。その破綻は失敗ではなく、デリバリープロセスがアプリケーションとともに進化する必要があるというシグナルなのだ。