グラフとチャートが表示されたデスクの2台のモニター
技術解説

git pull本番運用の落とし穴、デプロイ鍵削除で13分の復旧が数時間に

目次を見る

サーバー上でcronによるgit pullを本番デプロイに使っている、あるいはSSHのデプロイ鍵をIaC管理外で手作業運用しているチームに向けた内容です。CIが緑でレビューも通ったPRが、デプロイの仕組み側を壊してサービス全体を止めてしまう事例を手がかりに、確認すべき点を整理します。

何が起きたか

ドキュメントサイトをPHPの古いCMSから静的サイトジェネレーターに移行するPRがマージされました。CIは通り、レビューも承認済みです。

ところが本番サーバーは2分ごとのcronジョブでgit pullを実行し、mainブランチの内容を直接デプロイする構成でした。PRがマージされた瞬間、両方のサーバーが変更を取り込み、PHPアプリと一緒にロードバランサーのヘルスチェック用エンドポイントも消えました。

結果はヘルスチェック失敗、送信先ゼロ、全ユーザーに503という数分での連鎖でした。アプリのコード自体は正しく、ステージングでも問題なく動いていました。壊れたのはデプロイの配線部分です。

なぜ起きるか(原因の分解)

1つ目の原因は、本番デプロイの仕組みがコードとして管理されていなかったことです。cronによるgit pullデプロイは、Terraformなどのコード化された構成管理(IaC)の対象外に置かれがちです。誰も「ドキュメント化していない」という事実そのものに気づけません。

2つ目は、SSHのデプロイ鍵が「片付けてよいもの」に見えたことです。移行作業で古いPHPアプリ関連のファイルを削除した際、デプロイ鍵もついでに消されました。見た目は不要なクリーンアップですが、実際は本番サーバーがリポジトリを読み取るための認証情報でした。

3つ目は、revert(変更を打ち消すPR)をマージしてもサーバー側の状態は自動的に戻らないという構造です。GitHub上でコードは13分で元に戻りましたが、git pullする権限自体が失われていたため、サーバーは永遠に古い状態のままでした。エラーは次のように出ます。

ERROR: Repository not found.
fatal: Could not read from remote repository.

このエラーは権限不足のように見えますが、実際はSSHの「認証(authentication)」段階で誤った鍵が先に提示され、GitHub側がその鍵の持ち主として認識してしまう識別(identity)の問題です。SSHクライアントは~/.ssh/内の鍵を順番に提示するため、意図しない鍵がpuppetの自動化ユーザーとして認証され、リポジトリへのアクセス権がないと判定されていました。

さらに新しい鍵を作ってもGitHubから「Key is already in use」と拒否されました。GitHubのデプロイ鍵は全リポジトリを通じてグローバルに一意である必要があるためです。過去の自動化設定で別のリポジトリにすでに登録されていた可能性が高いという状況でした。

自分のプロジェクトが該当するか確認する

以下は本番サーバー側で確認できるチェックです。

# cronでgit pullが仕込まれていないか確認
crontab -l | grep -i git

# デプロイに使うSSH鍵とホスト別設定を確認
cat ~/.ssh/config

# サーバーがどのブランチ・コミットを見ているか確認
cd /var/www/site && git log -1 --oneline

# GitHub側で登録されているデプロイ鍵を確認(リポジトリのSettings > Deploy keys)

~/.ssh/configHost *StrictHostKeyChecking noだけが書かれていて、リポジトリ専用のホストエイリアスとIdentitiesOnly yesがない場合は要注意です。今回のケースでは、まさにこの専用エイリアスが「不要な設定」として削除されていました。

もう1つの確認ポイントは、デプロイ手順とロードバランサーのヘルスチェック対象が、Terraformなどのコードに残っているかどうかです。手作業のcron設定やサーバー個体のSSH鍵は、IaCのコード化からこぼれ落ちやすい部分です。

対策の手順

応急対応として、GitHub APIでリポジトリのtarballを直接取得し、S3経由でサーバーに配布する方法があります。SSH認証の問題を回避してサービスを復旧できます。

# 1. GitHub APIからtarballを取得
curl -L -H "Authorization: token $TOKEN" \
  "https://api.github.com/repos/org/docs/tarball/main" \
  -o /tmp/site.tar.gz

# 2. S3にアップロード
aws s3 cp /tmp/site.tar.gz s3://tmp-bucket/restore.tar.gz

# 3. 短い有効期限で署名付きURLを発行
URL=$(aws s3 presign s3://tmp-bucket/restore.tar.gz --expires-in 300)

# 4. サーバーで取得しWebルートに展開
curl -o /tmp/site.tar.gz "$URL"
tar xzf /tmp/site.tar.gz -C /var/www/site/ --strip-components=1

これは一時しのぎであり、恒久対策ではありません。恒久的には次の3点を見直すべきです。

  • デプロイ用SSH鍵はTerraformなどのIaCでリソースとして管理し、削除時にPlanで差分が見える状態にする
  • cronによるgit pullデプロイをCI/CDパイプライン経由の明示的なデプロイに置き換え、誰が何をデプロイしたか追跡できるようにする
  • ロードバランサーのヘルスチェックエンドポイントは、アプリ削除時に自動でCIが失敗するようテストを組む

オブザーバビリティの観点では、デプロイ後のヘルスチェック失敗率をSLI(サービスレベル指標)として監視し、変更直後のエラーバジェット消費を自動検知できるようにしておくと、今回のような503の連鎖を数分単位で検知できます。SLO(サービスレベル目標)に対する消費速度のアラートがあれば、13分の revert マージより先に異常を捉えられた可能性があります。

障害対応の仕組み化に向けて

この一件が示すのは、ビルドを通すテストと、デプロイの配線を守るテストは別物だという点です。CIがコードの正しさを保証しても、サーバー側の認証情報やcron設定までは検証していません。

手を動かせる次の一歩として、まず自分のプロジェクトの本番サーバーでcrontab -l~/.ssh/configを確認してください。git pull型デプロイが残っているなら、IaC化とCI/CD移行を検討する価値があります。

そのうえで、デプロイ鍵やヘルスチェックエンドポイントのような「地味だが落ちると全滅する」設定を、コードレビューの対象として明示的にリスト化しておくことが、次の503を防ぐ一番の近道です。

参考

💀 The Most Dangerous Deletions Don't Break the Build. They Break the Deploy. An AI-Assisted Recovery Story 🤖

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。