会社が空中分解した日でも、バグ修正の依頼は来る
入社して 6 か月目。会社の事情で組織が空中分解し、私たちのチームだけがぽつんと独立することになったその日。オフィスは混乱していて、チーム長は「もう技術のことはよく分からないから、全部お前がやれ」と言い、インフラ管理の全権を私に渡した。
そんな混乱のさなかでも電話は鳴った。顧客だった。「すみません、ログインページでエラーが出るんですが。早く直してください。」
頭が真っ白になった。以前ならローカルで直して git push さえすれば、他部署のサーバーにあった Jenkins や GitLab が勝手に本番サーバーへデプロイしてくれたはずだ。だが、そのつながりは切れていた。
もう私を助けてくれる「デプロイロボット」はいない。自分の手でこのコードを本番サーバーまで運ばなければならない。途方に暮れた。だがそのとき、ここ数か月、体当たりで覚えてきた知識が頭の中をよぎった。
「待て、落ち着け。原理が分かっていれば手動でもできる。ひとつずつ安全に運ぼう。」

Git と GitLab: コードの保管庫
まずは切れた橋を架け直さなければならなかった。最初に必要だったのは「Git」と「GitLab」だった。
- Git: 自分のコードを保存し、バージョン (履歴) を管理してくれるツールだ。(タイムマシン)
- GitLab: Git リポジトリの機能だけでなく、デプロイ自動化 (CI/CD) ツールまでひとつに統合されたプラットフォームだ。
「普通は GitHub をよく使うんじゃないですか?」 その通りだ。一般的には GitHub、GitHub Actions を含む、が最もよく使われ、伝統的な強者である Jenkins を別に構築して使うことも多い。
だが私たちは外部アクセスが制限された閉鎖的な社内ネットワーク環境にいたため、自分たちのサーバーに直接インストールできる GitLab Self-Hosted が最善の選択だった。道具は違っても、「コードを保存し、自動でデプロイする」という本質は同じだ。
私は徹夜でサーバーに GitLab を入れ、コードを移した。
「ふう、これでまた push すればデプロイされるはずだよな?」
胸を躍らせながらコードを修正し、git push origin master を打った。GitLab のリポジトリには問題なくコードが上がった。だが本番サーバーは微動だにしなかった。いくら待っても機能は更新されなかった。
手動デプロイの悪戦苦闘: 失われた工程を探す
「あれ? なんでデプロイされないんだ?」 聞ける先輩も、頼れるチーム長もいない。私は自分で答えを見つけるしかなかった。
よく考えると不思議だった。GitLab はただの「コードを保存する図書館」にすぎない。図書館に新しい本が入ったからといって、それが自動的に書店、本番サーバー、の棚に並ぶわけではない。以前はその間で誰かが配送してくれていたのだ。
「じゃあ、私が直接運ばなきゃいけないのか?」
私は手動デプロイの工程をひとつずつ踏み始めた。思っていたよりずっと複雑で面倒だった。
第 1 段階: テストとビルド (Test & Build) – 不良品を弾く
最初にやるべきことは、自分のパソコンにある Java コードが正常かを検証し、実行可能な状態にすることだった。単にファイルをコピーすればいいわけではない。自分が修正したコードが既存ロジックを壊していないか、テスト、文法エラーがないか、ビルド、を確認しなければならない。
私はターミナルを開いてコマンドを打った。./gradlew test clean build
緑色のチェックとともに、すべてのテストが通過したというメッセージが出た。ひと安心だ。もしここでエラーが出ていたらどうなるか。それは「不良品」だ。サーバーに上げてはいけないコードである。この工程は、コードが最低限の資格を満たしているかを調べる「最初の安全装置」だった。
第 2 段階: イメージ生成 (Dockerize) – 環境を丸ごと封じ込める
次はこの jar ファイルをサーバーへ送らなければならない。ファイルだけ送ればいいだろうか。いや、「Works on My Machine」シリーズで痛感した。自分のパソコン、Java 17、とサーバー、Java 8、では環境が違うかもしれない。だから私たちは「環境ごと丸ごと凍らせて」送ることにした。それが「Docker」だ。
私はプロジェクトルートに Dockerfile を作り、慣れた手つきで書き上げた。
FROM openjdk:17-alpine
COPY build/libs/my-app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
そしてイメージを焼いた。docker build -t my-app:v1 .
これで私のコードは、どのサーバーに投げても同じように動く不滅の「コンテナイメージ」になった。Docker は環境差によるエラーを防ぐ「二つ目の安全装置」だった。
第 3 段階: 運搬 (Push & Pull) – 共用倉庫を使う
イメージの準備はできた。次はこれを本番サーバーへ運ばなければならない。幸い、「Docker Hub」は社内サーバーではなく、インターネット上の外部サービス、SaaS、だった。会社が空中分解して社内網にあった GitLab との接続は切れたが、外部ネットワークへ通じる道はまだ開いていたのだ。
私は震える手で、そのイメージを共用物流倉庫である Docker Hub へ打ち上げた。
- 自分のパソコンからアップロード:
docker push my-id/my-app:v1
しばらくするとアップロード完了のメッセージが出た。次は反対側、本番サーバーへ接続する番だ。サーバールームへ走るのではない。「SSH (Secure Shell)」を使って安全にリモート接続する。
ssh user@production-server
接続した黒い画面、CLI、の上で、さきほど上げておいたイメージを引き下ろす。docker pull my-id/my-app:v1
第 4 段階: 配備 (Run) – 入れ替える
ついに最後の段階だ。サーバーではすでに旧バージョンが動いている。これを止めて新しいバージョンを立ち上げなければならない。Linux のコマンドが指先から流れ出た。
- 既存コンテナの停止と削除:
docker stop my-app && docker rm my-app - 新しいコンテナの起動:
docker run -d --name my-app -p 80:8080 my-id/my-app:v1
Enter を押した瞬間、長いハッシュ値が表示され、docker logs -f my-app でログを確認すると見慣れた Spring のロゴが出た。顧客から再び連絡が来た。「あれ? もうちゃんと動いてますね。ありがとうございます!」

だが、結局「人」はミスをする
最初の手動デプロイは成功だった。私は自信満々になった。「なんだ、一人でもいけるじゃないか?」 だがその慢心は長く続かなかった。繰り返される手動作業の中で、人間の限界はすぐに露わになった。
危機 1. 一本橋で出会ったバグ (Git Branch の不在)
数日後、顧客 A の要望で「ログイン機能」を修正していたところ、突然、顧客 B から「決済機能」に致命的なバグが出たので今すぐ直してほしいという連絡が来た。
私は慌てた。Git は使っていたが、一人開発だからという言い訳で、すべての作業を master ブランチひとつでやっていたからだ。ログイン機能を直しているうちに、コードはすでにめちゃくちゃに混ざっていた。
「え? 今この状態で決済バグだけ直してデプロイしたら、未完成のログインコードまで一緒に上がるんじゃないか?」
結局私は原始的な方法で、作業中のコードを一時ファイルに退避し、git reset でコードを巻き戻すしかなかった。その過程で、誤っていくつかのコードを消してしまう惨事まで起きた。
危機 2. 私には仲間がいない (Test Code の不在)
紆余曲折の末に決済バグを修正してデプロイした。やっと一息つこうとしたその瞬間、今度は「会員登録ができない」という連絡が来た。調べてみると、決済ロジックを直すときに共通で使っているユーザー検証関数に手を入れてしまったのが原因だった。
「ああ… 誰かが私のコードを見てくれていたら、Code Review、こんなミスはしなかったのに。」
だが私にはコードを見てくれる先輩も同僚もいなかった。私は一人だった。実際、会員登録機能はあまりにも慌ただしく作ったせいで、Unit Test も書いていなかった。もしあのとき面倒でもテストコードを作っておけば、決済ロジックを触って会員登録を壊した瞬間に、機械が赤信号を灯してくれただろう。
危機 3. 「サーバーが落ちました!」 (Docker Tag の不在)
そして数日後、決定打が来た。急いで機能を追加してデプロイしたのだが、サーバーは起動した瞬間にビジネスロジックのエラーで落ちてしまった。テストを省略したことが原因だった。
顧客から電話が鳴りやまなかった。「早く前のバージョンにロールバックしてください!」 私は震える手でサーバーのターミナルを開いた。だが、ロールバックする方法がなかった。
私は毎回面倒だと言って、イメージ名を my-app:latest だけで上書きしていた。v1 や v2 のようなバージョンタグを別につけていなかったので、前バージョンの正常なイメージはもう消えていた。結局、止まったサーバーを後にしてローカルで再びコードを直し、最初からビルドし直すしかなかった。イメージを作り、本番サーバーへ運んでコンテナを再起動するまでに 10 分かかり、そのサーバーダウンの経緯を書いた顛末書まで提出することになった。
おわりに: この一連の工程の名前が「パイプライン」だった
地獄のような 10 分間のダウンタイムと引き換えに顛末書を書きながら、私はここ数日自分が経験したことを振り返った。
- Git Branch: コードを安全に隔離し、管理するための出発点。
- Test & Code Review: 不良品が次の段階へ進めないよう食い止めるふるい。
- Build & Dockerize: コードを実行可能な環境ごと梱包する工程。
- Docker Tag & Push: 万一の事態に備えて、バージョンを記録して保存する工程。
- Deploy: 安全なイメージをサーバーで実行する最後の段階。
私はこれまで、この工程を断片的な知識としてしか理解していなかった。面倒だから省いたこともあったし、知らなくて飛ばしたこともあった。だが実務で起きた事故は、これらの段階のどれひとつ欠けてもいけない「必須の安全装置」だということを証明してくれた。
そして気づいた。私があれほど探し回っていた「自動化」の正体が何だったのか。それは単にコマンドを代わりに打ってくれる機械ではなかった。この 1 から 5 までの工程を、水が流れるように途切れず、しかも人のミスなしに 自動で流していく道だった。
それこそが CI/CD パイプラインだった。
「チーム長、もう分かりました。私たちに必要なのは、この安全装置が自動で回る『パイプライン』です。」
次回はこの設計図をもとに、自分だけのデプロイロボット、CI/CD パイプライン を実際に構築してみよう。



