CI/CD(コードの変更を自動でビルド・テストし本番環境へ届ける一連の仕組み)のパイプラインは、グリーンのチェックマークが並んでいても安心できるとは限りません。ステージング環境で全テストが通過したのに、本番で挙動が違う、障害対応が遅れる、という経験をしたインフラ担当者は少なくないはずです。
この記事は、デプロイパイプラインを運用しているSRE・DevOpsエンジニア・プラットフォームチームに向けた内容です。パイプラインのどこに穴が空きやすいかを、原因の分解から確認手順、対策まで順番に整理しました。
何が起きるか:グリーンのパイプラインが嘘をつく
典型的な事象は「ステージングでは成功したビルドが、本番では別物になっている」というものです。
たとえばステージングデプロイ時にDockerイメージをその場でビルドし、本番デプロイ時にも別途ビルドし直す構成があります。ベースイメージの「latest」タグが数時間でパッチ更新されると、同じコードでも別バイナリが本番に乗ります。
結果として、ステージングのテスト結果は本番の安全性を保証する証拠として弱くなります。障害発生時も「ステージングでは再現しない」という厄介な状況に陥りがちです。
もう一つの事象は、障害時にログを見ても失敗理由が分からないケースです。ジョブは落ちているのに、どのステップのどの処理が原因か特定できず、SREが手探りで調査する時間だけが伸びていきます。
なぜ起きるか:原因を3段階に分解する
段階1: アーティファクトの再ビルド問題
ビルド成果物(アーティファクト)を環境ごとに作り直す設計だと、依存パッケージのバージョン固定(ピン留め)が緩い場合に差分が生まれます。package.jsonの「^」や「~」指定、Dockerfileの「FROM node:20」のような緩いタグ指定が典型例です。
本来は、検証済みの同一アーティファクトをステージングから本番へ「昇格(プロモート)」させる設計が望ましいとされています。コンテナレジストリにイメージダイジェスト(SHA256ハッシュ)で参照を固定し、同じイメージIDをそのままデプロイする方式です。
段階2: パイプライン構造が複雑になりすぎている
コード変更の種類に関わらず全ジョブを毎回実行する設計は、フィードバックを遅くするだけでなく、設定の重複を生みやすくなります。条件分岐(コードの変更範囲に応じて実行ジョブを切り替える仕組み)を入れること自体は有効ですが、共有コンポーネントに対する必須テストやセキュリティスキャンまで誤ってスキップしてしまう設計ミスが起きがちです。
段階3: 失敗が「サイレント成功」として扱われる
テストが失敗してもパイプライン全体としては成功扱いになる設定は、CIツールの設定ミスとして意外と見逃されます。シェルスクリプト内で終了コードを明示的に0で返していたり、終了コードを握りつぶすラッパースクリプトが混入していたりするケースです。
この3つは独立した問題ではなく、連鎖します。ビルドが環境ごとに異なり、構造が複雑でテストがスキップされ、失敗が隠蔽される。これが重なると「グリーンだが壊れている」パイプラインが完成します。
自分のプロジェクトが該当するか確認する方法
実際に自分たちのパイプラインがこの穴にハマっていないか、次のポイントを順番に確認してみてください。
# 1. ステージングと本番で同じイメージダイジェストを使っているか確認
docker inspect --format='{{index .RepoDigests 0}}' <image_name>
# ステージングと本番それぞれで実行し、ダイジェスト値が一致するか比較する- CI設定ファイル(.gitlab-ci.yml、.github/workflows配下のyamlなど)を開き、buildジョブがステージング用・本番用で別々に定義されていないか確認する
- DockerfileのFROM行がタグ固定(例: node:20.11.1-alpine)か、可変タグ(latestや20)かを確認する
- 依存管理ファイル(package-lock.json、Pipfile.lock、go.sumなど)がリポジトリにコミットされ、CIで再生成されていないか確認する
- パイプラインのYAML内で終了コードを強制的に成功させる記述がないかを検索する
- アーティファクトの保存先(コンテナレジストリやパッケージリポジトリ)に、保持期限とクリーンアップポリシーが設定されているか確認する
# サイレント成功の疑いがあるコマンドを一括検索する例
grep -rn "true$" .github/workflows/ .gitlab-ci.yml該当する行が見つかった場合、意図した挙動かどうかをチーム内で再確認する価値があります。
対策の手順
手順1: アーティファクトを1回だけビルドして使い回す
ビルドは1回だけ実行し、生成されたイメージやバイナリにバージョン情報とビルド由来(プロベナンス)を記録します。ステージングと本番は同じダイジェストを参照する設計に変更します。
手順2: 依存バージョンをピン留めする
重要なツールチェーンと依存パッケージはメジャー・マイナー・パッチまで固定します。Dockerのベースイメージもタグではなくダイジェスト参照を使う選択肢も検討できます。
手順3: 失敗ジョブのログを構造化する
失敗理由が一目で分かるよう、ジョブごとに明確なエラーメッセージを出力させます。SREの障害対応(インシデントレスポンス)を速めるには、失敗ステップ・エラーコード・直前のコミットハッシュをログに含める設計が有効です。
手順4: 必須チェックのバイパスを防ぐ
GitHubの場合はブランチ保護ルール、GitLabの場合はマージリクエスト承認ルールで、必須ジョブの合格なしにマージできない設定になっているか確認します。条件分岐を入れる際も、共有コンポーネント変更時は必須テストが必ず走るようルールを明文化します。
手順5: 定期的にチェックリストで棚卸しする
パイプラインのアーキテクチャ、ビルド品質、テスト、セキュリティ、デプロイ準備、監視の各項目を「完了・未完了・対象外」で評価し、担当者と改善期限を割り当てる運用が提案されています。四半期に一度など、定期棚卸しのタイミングを決めておくと形骸化を防げます。
まとめ:次に確認すべきこと
CI/CDパイプラインの信頼性は、ビルドの再現性・テストの正直さ・失敗時の可視性の3点で決まります。
- まずはdocker inspectやCI設定ファイルを開き、ステージングと本番で同じアーティファクトを使っているか確認する
- grepで終了コードの強制成功パターンを検索し、サイレント成功が紛れ込んでいないか洗い出す
- 依存バージョンのピン留め状況とブランチ保護ルールを見直す
どれも数分で確認できる作業です。小さな確認から始めて、パイプラインが「見た目のグリーン」ではなく実際の安全性を示している状態に近づけていきましょう。