目次
- この記事の立ち位置
- 失敗1:自動デプロイが2か月以上止まっていた
- 失敗2:紛らわしい設定ファイルで本番が停止した
- 失敗3:イメージ取得の失敗を握りつぶして旧版で起動した
- 失敗4:設定ファイルの差し替えが反映されなかった
- 失敗5:ヘルスチェックが35時間100%失敗し続けた
- 失敗6:検証を切ったコマンドが壊れた証明書を隠した
- 失敗7:ビルドが既存の成果物を消した
- 失敗8:配信経路の切替が監視の宛先を覆い隠した
- 失敗9:監視自身が計測失敗を正常値に化かした
- 9件に共通する構造
- 補足:ローカルの監視はローカルと一緒に死ぬ
- 「成功」の定義を変える
- 確認に使っているコマンド
- 反映時刻を出す仕組みを作る
- 段取りの順番
- それでも残る問題
- まとめ
この記事の立ち位置
デプロイの手順書は多い。しかし「手順どおりに実行して成功したのに、本番が変わっていなかった」記録は少ない。
編集部は複数のサービスを運用しており、この形の失敗を9件経験した。いずれもコマンドは正常終了し、画面にはエラーが出ず、多くは監視も鳴らなかった。
この記事はその記録である。各件について、何が起きたか、どう気づいたか、どう確認すべきだったかを書く。
先に結論を書く。「デプロイが成功した」は「反映された」を意味しない。 この2つは別の量であり、別に確認する必要がある。
失敗1:自動デプロイが2か月以上止まっていた
何が起きたか
定期実行で自動デプロイを回していた。登録は残っており、時刻になれば起動していた。
しかしスクリプトの実行権限が失われており、毎回「Permission denied」で終了していた。
デプロイの記録を見ると、反映を示す行は過去に数件あるだけで、それ以降は一度も記録されていない。本番で動いているコミットは、2か月以上前のものだった。
なぜ気づかなかったか
定期実行そのものは「起動している」。エラーは実行環境のログに出るが、誰も見ていない。
そして本番は動いていた。 古いままだが、壊れてはいない。利用者からの連絡もない。
どう気づいたか
別の作業で本番のコミットを確認したときに、想定より古いことに気づいた。自動的には検知されなかった。
対策
「最後に反映された時刻とコミット」を本番から取得できるようにする。 これがあれば、当日に気づける。
定期実行の設定はcrontabの仕様に従うが、登録されていることと、実行が成功していることは別である。
失敗2:紛らわしい設定ファイルで本番が停止した
何が起きたか
本番の環境変数を読み込む際、名前の似たファイルを指定した。
片方は63バイトの雛形、もう片方が1,461バイトの本物だった。誤って雛形のほうを指定した結果、データベースの接続情報が空になり、再起動が繰り返される状態になった。
実測で約1分間、APIが停止した。
なぜ起きたか
ファイル名が似ていた。一方は本番用の名前がついているように見えるが、中身は雛形だった。
対策
設定ファイルを指定する前に、サイズか中身を確認する。 63バイトと1,461バイトなら、一覧を見るだけで区別できる。
より本質的には、雛形と実体を同じディレクトリに置かないことである。
失敗3:イメージ取得の失敗を握りつぶして旧版で起動した
何が起きたか
コンテナのイメージを取得してから起動する手順だった。取得に失敗しても、手元にある古いイメージで起動が成功する。
見た目には正常に終了する。コマンドは0で終わる。
どう気づいたか
戻せるようにイメージの識別子を控える運用をしていて、そこで食い違いに気づいた。
確認のため、配信されているファイルのハッシュを全件突き合わせた。180ファイル中、差分は2件だけだった。逆に言えば、178ファイルは同じで、その2件が変更の本体だった。目視では絶対に分からない。
対策
取得と起動を別々に扱い、取得が失敗したら止める。 コンテナレジストリの仕様上、取得の失敗は明確なエラーを返すので、それを握りつぶさない。
加えて、起動しているイメージの識別子を記録する。 期待する識別子と一致するかで判定できる。
失敗4:設定ファイルの差し替えが反映されなかった
この件が、7件のうち最も気づきにくかった。
何が起きたか
配信サーバーの設定ファイルを、コンテナに単一ファイルとしてマウントしていた。設定を更新する際、新しいファイルを作って既存のものと入れ替えた。
入れ替えても、コンテナの中では古い設定のままだった。
なぜか
単一ファイルのマウントは、ファイルの実体を指している。ファイルを移動して置き換えると、同じ名前でも別の実体になる。マウントは元の実体を指し続けるため、新しい内容が見えない。
詳細はbind mountの仕様にある。
最も厄介な点
設定の検証コマンドも、再読み込みコマンドも、両方「成功」を返した。
検証コマンドは、コンテナの中から見えているファイル(=古いほう)を検証して「正常」と答える。再読み込みも、同じファイルを読み直して「成功」と答える。
2つの成功表示を見て、反映されたと判断した。
どう気づいたか
新しいコンテナで同じ手順を試したところ、そちらでは反映された。この差から実体の固定に行き着いた。
対策
実効設定を出力させて確認する。 配信サーバーの設定は、現在読み込まれている内容を丸ごと出力する方法がある。検証の成功ではなく、出力された中身を見る。
手順としては、中身を同じ実体に上書きするか、コンテナを作り直す。
失敗5:ヘルスチェックが35時間100%失敗し続けた
何が起きたか
コンテナの健全性チェックを、ホスト名を使って自分自身に接続する形で書いていた。
コンテナ内での名前解決がIPv6のアドレスを優先し、サービスはそちらで待ち受けていなかった。 接続が拒否され、チェックは必ず失敗する。
連続失敗の回数は4,464回。30秒間隔なので、起動から37時間、一度も成功していない。
サービス自体は正常だった
ここが重要である。利用者からの接続は問題なく処理されていた。 失敗していたのは自己チェックだけである。
なぜ35時間放置されたか
健全性の表示が異常でも、サービスが動いていれば実害が出ない。表示を見た人は「何か変だが動いている」と判断して先に進む。
対策
ホスト名ではなくアドレスを直接書く。 名前解決の優先順位はRFC 6724で定義されており、環境によって結果が変わる。
もう1つ重要な点として、同種の設定が5か所にあった。 1か所直して終わりにすると、残りが同じ状態で残る。ヘルスチェックの書き方を見直す際は、全数を確認する。
失敗6:検証を切ったコマンドが壊れた証明書を隠した
何が起きたか
配信元の疎通を確認するとき、証明書の検証を無効にする指定を付けていた。正常な応答が返り、中身も本物のJSONだった。
しかし検証を有効にすると、自己署名の証明書だというエラーが出た。
実際の配信経路では検証が行われるため、そちらではエラーになっていた。
なぜ検証を切っていたか
開発中に一度切って、そのまま確認手順に残っていた。切った理由は誰も覚えていなかった。
対策
確認は必ず検証を有効にした状態で行う。 コマンドの仕様上、検証を無効にする指定は明示的に付けるものなので、確認手順から外せば済む。
一般化すると、「普段の本番が動いている」ことは、検証の代わりにならない。 経路が違えば結果が違う。
失敗7:ビルドが既存の成果物を消した
何が起きたか
1つの出力先に、2つのアプリケーションを配置する構成だった。片方をビルドすると、出力先を空にしてから書き出す仕様のため、もう片方が消える。
結果として、会員向けの画面一式が約13時間、本番から消えていた。
切り分けが難しかった理由
入口のURLは、配信基盤のキャッシュからHTTP 200を返し続けた。 その下のログイン画面だけが404になる。
この状態は「一部のページだけ壊れている」ように見え、原因を配信の設定やアプリケーションの不具合に求めてしまう。
監視は鳴っていた
正直に書くと、監視は15分ごとに正しく異常を通知していた。 それでも13時間続いた。
検知できることと、対応されることは別である。
対策
複数の成果物を1つの出力先にまとめる構成では、片方のビルドで他方が消えないことを確認する。 具体的には、ビルド後に両方のファイルが存在することを検査する。
失敗8:配信経路の切替が監視の宛先を覆い隠した
何が起きたか
配信を新しい経路へ切り替える作業で、ルートの範囲を広げた。広げた瞬間、新しい経路が既存のパスを覆い隠した。
切替の前後で全パスを突き合わせたところ、26件中15件(58%)が現行と食い違っていた。 うち4件は本物の不具合だった。
最悪だったもの
覆い隠されたパスの1つが、死活監視の宛先そのものだった。
新しい経路が固定の応答を返すため、監視は200を受け取り続ける。 配信元が完全に落ちていても、監視は正常と判定する。
監視が「バックエンドを一切見ていない」状態になっていたが、画面上は何も変わらない。
どう気づいたか
ルートを設定しない複製を別のURLに立て、全パスを新旧で突き合わせた。
注意点として、突き合わせでは「差に見えて差でない」ものが11件あった。配信基盤が自動で挿入するスクリプトなどである。正規化しないと、本物の差が埋もれる。
対策
ルートを広げる前に、覆い隠されるパスの一覧を出す。 ルートの指定はパターン一致なので、意図より広く当たることがある。
そして、監視の宛先が新しい経路を通っていないかを確認する。 監視だけは、経路を迂回して配信元に直接届く形にしておくのが安全である。
失敗9:監視自身が計測失敗を正常値に化かした
最後の1件は、監視そのものの話である。
何が起きたか
データベース層のエラーを数える監視を作った。リモートのサーバーでスクリプトを実行し、結果を集計する形である。
実行の仕組みの都合で、集計部分が一度も実行されない状態になっていた。それでも監視は「OK: 5xxなし」と報告し続けた。
なぜ0と報告されたか
集計結果が取れなかったとき、値が空になる。空の場合に0として扱う書き方をしていたため、「計測できなかった」が「0件だった」に変換されていた。
監視を作った当日に踏んだ
正直に書くと、この監視を新設したその日に、その監視自身がこの罠を踏んでいた。 作った本人が、動いていると思い込んでいた。
対策
値が無いときに既定値へ落とす書き方を、監視コードで使わない。
代わりに、処理が最後まで到達したことを示す印を明示的に出力させる。 その印が無ければ、値が何であれ「計測失敗」として扱う。
これは編集部が繰り返し踏んでいる形で、0は「無い」と「測れていない」の両方を意味する。 区別する仕掛けを測定の側に持たせる必要がある。
9件に共通する構造
並べると、同じ形が見える。
| 件 | 何が「成功」と表示されたか | 実際に起きていたこと |
|---|---|---|
| 1 | 定期実行が起動した | スクリプトが毎回失敗していた |
| 2 | 設定ファイルを読み込んだ | 雛形を読んでいた |
| 3 | 起動コマンドが完了した | 古いイメージで起動していた |
| 4 | 設定の検証と再読み込み | 古い設定を検証していた |
| 5 | サービスは応答していた | 自己チェックは全滅していた |
| 6 | 疎通の確認が成功した | 検証を切っていた |
| 7 | ビルドが完了した | 別の成果物を消していた |
| 8 | 監視が 200 を受け取った | 新しい経路が宛先を覆い隠していた |
| 9 | 監視が「5xx なし」と報告した | 集計が一度も実行されていなかった |
いずれも、確認している対象が、確かめたい対象とずれている。
定期実行の起動は、スクリプトの成功ではない。コマンドの終了は、意図した中身での起動ではない。検証の成功は、正しいファイルを検証したことを意味しない。
なぜこのずれが生まれるか
確認が安いほうへ流れるためである。 終了コードを見るのは一瞬で済む。実際に配信されているファイルのハッシュを取るのは手間がかかる。
安い確認を繰り返していると、確認しているつもりで何も確認していない状態になる。
補足:ローカルの監視はローカルと一緒に死ぬ
9件とは別に、記録しておくべき形がある。監視する側が、監視される側と同じ場所にいる場合である。
何が起きたか
編集部は多数の定期実行を1台の環境で回している。この環境が停止した際、43本のタイマーが2時間分まるごと飛んだ。 停止していた間の実行は、再開後に埋め合わされない。
厄介なのは、失敗のログすら残らないことである。実行されなかったのだから、失敗の記録も出ない。翌朝、起動の境界を確認して初めて分かった。
別の機会には、環境が異常終了ではなく正常終了した。このときは84本のタイマーと4本の接続が全て停止した。復旧に5時間かかったが、その理由は再起動に管理者権限が必要だったことである。
どう気づいたか
2件目は、外部に置いた独立の監視が検知した。 同じ環境の中にある監視は、一緒に止まっているので鳴らない。
対策
外部から死活を見る経路を1本だけ持つ。 全部を外に出す必要はない。「この環境が生きているか」だけを、別の場所から確認する。
もう1つ、管理者権限なしで復旧できる経路を事前に用意する。 権限が必要な手順は、必要になった時に人を待つことになる。
一般化すると
監視の独立性を確認する。 監視される対象と同じ電源、同じネットワーク、同じホストにある監視は、対象と一緒に沈黙する。
これは本記事の9件と同じ構造である。確認している対象と、確かめたい対象がずれている。 ここでは「確認する仕組みそのものが、確認したい対象の一部だった」という形をとる。
「成功」の定義を変える
対策の中心は、確認の対象を変えることである。
実行の成功ではなく、状態の変化を見る
| 見ていたもの | 代わりに見るもの |
|---|---|
| コマンドの終了コード | 動いているイメージの識別子 |
| デプロイのログ | 配信されているファイルのハッシュ |
| 設定の検証が通ったこと | 実効設定の出力 |
| 定期実行が起動したこと | 成果物の件数・最終更新時刻 |
| 疎通の確認が成功したこと | 検証を有効にした状態での応答 |
「変化がないこと」も検知する
9件のうち、失敗1は何も変わらなかったことが問題だった。監視は「壊れていないか」を見るが、「更新されているか」は見ていない。
定期的に更新されるべきものには、最終更新からの経過時間を監視の対象に入れる。
成功のマーカーを明示的に出す
これは編集部が別の場面で学んだことだが、共通して効く。
「エラーが出なかった」を成功と読まない。 処理が最後まで到達したことを示す印を、明示的に出力させる。
編集部には、監視スクリプトの一部が実行されないまま「問題なし」と報告し続けていた例がある。検査が走らなければエラーも出ないので、沈黙が成功に見える。
確認に使っているコマンド
実際に使っている確認の形を挙げる。環境によって異なるので、考え方を読み取ってほしい。
動いているイメージを確認する
起動中のコンテナが、どのイメージの、どの版で動いているかを出す。期待する識別子と照合する。
Dockerの仕様上、イメージは内容から計算された識別子を持つため、これが一致すれば中身は同じである。
配信されているファイルを確認する
本番のURLから実際に取得し、手元の成果物とハッシュを比べる。代表的な数ファイルでよい。
失敗3では、180ファイル中2ファイルだけが違っていた。全数を比べたから分かったが、変更したファイルが分かっていれば、そこだけ見れば足りる。
実効設定を確認する
配信サーバーや実行環境に、現在読み込んでいる設定を丸ごと出力させる。 検証コマンドの成功ではなく、出力された中身を見る。
最終更新を確認する
本番に、反映した時刻とコミットの識別子を返すURLを用意する。詳細は次節で書く。
検証を有効にして疎通を確認する
証明書の検証を無効にする指定を、確認手順から外す。
反映時刻を出す仕組みを作る
9件のうち複数は、これ1つで当日に気づけた。 最も費用対効果が高い対策である。
作り方
デプロイの最後に、コミットの識別子と時刻をファイルへ書き出す。 そのファイルを返すURLを1本用意する。
それだけである。フレームワークも監視ツールも要らない。
何が分かるようになるか
本番がいつのコードで動いているかが、外から見える。
失敗1では、これがあれば「最終反映が2か月前」と一目で分かった。失敗3でも、識別子が変わっていないことで気づけた。
監視に組み込む
さらに、最終反映からの経過時間が想定を超えたら通知する。 毎日デプロイする運用なら、3日経過で異常である。
気をつけること
このファイル自体が、古い成果物に含まれたまま配信される可能性がある。失敗3のような状況では、古いイメージに古いファイルが入っている。
したがって、デプロイの実行時に書き出し、成果物に含めるのではなく、起動時に環境から読んで返すほうが確実である。
段取りの順番
これから整えるなら、次の順序を勧める。
1. 反映時刻を出すURLを作る
最も安く、最も効く。1時間で作れる。
2. 確認手順から「検証を切る指定」を外す
無料で、すぐできる。
3. 設定ファイルの一覧を確認する
雛形と実体が同じ場所にあるなら分ける。サイズを見れば区別できる。
4. ヘルスチェックの宛先をアドレスに変える
ホスト名を使っている箇所を全部探す。1か所直して終わりにしない。
5. デプロイの各段で、失敗したら止める
取得と起動を分ける。パイプを使っている場合は、途中の失敗が全体の失敗になる設定を入れる。
6. 成果物の存在を検査する
デプロイ後に、あるべきファイルが全部あるかを確認する。失敗7はこれで防げる。
7. 最後に、監視の宛先と対応の時間を決める
失敗7では監視が正しく鳴っていた。鳴っても動かなければ同じである。
それでも残る問題
正直に書いておく。
確認の手間は増える
ここに挙げた確認を全部やると、デプロイのたびに時間がかかる。自動化しない限り続かない。
編集部では、デプロイのスクリプトに確認を組み込み、失敗したら中止する形にしている。手で実行する確認は、実行されなくなる。
新しい形の失敗は防げない
9件はいずれも、起きた後に対策を入れた。 事前に予測できたものはない。
対策として意味があるのは、個別の手当てより、「成功」の定義を状態の確認に変えるという一般則のほうである。
対応する人を決めないと意味がない
失敗7が13時間続いたのは、検知の問題ではなく対応の問題だった。誰が、何分以内に見るかを決めていなければ、監視を増やしても同じことが起きる。
確認を増やすほど、確認の故障が増える
これは皮肉だが避けられない。確認の仕組み自体が、失敗8と失敗9の対象になった。
失敗8では、配信経路の変更が監視の宛先を覆い隠した。失敗9では、監視のコードが計測失敗を正常値に変換していた。補足に書いた例では、監視が対象と同じ場所にあって一緒に止まった。
確認を足すときは、その確認がどう壊れるかを一度考える。 具体的には次の3つを確かめる。
対象に届いているか。 検査の対象が0件のとき、合格として扱っていないか。
同じ系にいないか。 対象と同じ電源・ネットワーク・プロセスにある確認は、一緒に沈黙する。
失敗と0を区別しているか。 値が取れなかったときに既定値へ落とす書き方は、測定失敗を正常値に変える。
それでも確認を減らさない
確認が壊れうるからといって、確認をやめる理由にはならない。壊れた確認は、無い確認より少しだけましである。 少なくとも、壊れたことに後から気づける。
本記事の9件のうち、自動的に検知されたのは失敗7だけだった。残る8件は、別の作業をしていた人が偶然気づいた。確認の仕組みが足りていない状態からの改善なので、まずは足す。
まとめ
デプロイが「成功」しながら本番が変わっていなかった失敗を9件記録した。
自動デプロイが2か月以上止まっていた。 実行権限が失われ、毎回エラーで終了していた。本番は古いまま動いていたので誰も気づかなかった。
紛らわしい設定ファイルで本番が約1分停止した。 63バイトの雛形と1,461バイトの実体が同じ場所にあった。
イメージ取得の失敗を握りつぶして旧版で起動した。 配信ファイルを全件照合して、180ファイル中2件の差分で気づいた。
設定ファイルの差し替えが反映されなかった。 単一ファイルのマウントは実体を指すため、移動による差し替えが効かない。検証も再読み込みも「成功」を返していた。
ヘルスチェックが35時間100%失敗し続けた。 名前解決がIPv6を優先し、サービスはそちらで待ち受けていなかった。連続失敗4,464回。サービス自体は正常だった。
検証を切ったコマンドが壊れた証明書を隠した。 実際の配信経路では検証が行われるためエラーになっていた。
ビルドが既存の成果物を消した。 会員向け画面が13時間消えていた。監視は正しく鳴っていたが対応されなかった。
9件に共通するのは、確認している対象が確かめたい対象とずれていたことである。確認が安いほうへ流れると、確認しているつもりで何も確認していない状態になる。
明日から試せる3つ
1つ目は、本番に「最後に反映した時刻とコミット」を返すURLを1本作ることである。1時間で作れて、今回の9件のうち複数はこれだけで当日に気づけた。
2つ目は、確認手順から証明書の検証を切る指定を外すことである。無料で、すぐできる。
3つ目は、いま動いているコンテナのイメージ識別子を控えて、期待するものと照合することである。一致しなければ、デプロイは反映されていない。
編集部では、AI導入の支援に加えて、こうした運用の確認手順の設計も行っている。動いているように見えるが確かめ方が分からない、という段階の相談については、サービス案内を参照してほしい。
参考にした一次資料
- Docker のドキュメント — イメージと識別子の考え方
- Docker Compose — 複数コンテナの起動手順
- Dockerfile のリファレンス — ヘルスチェックの書き方
- bind mount の仕様 — 失敗4の原因
- nginx のドキュメント — 実効設定の出力
- GitHub Container Registry — イメージ取得の失敗の扱い
- crontab の仕様 — 定期実行の登録
- curl のマニュアル — 検証を無効にする指定
- RFC 6724 — アドレス選択の優先順位。失敗5の背景
- Cloudflare Workers のルート設定 — 配信経路の切り替え
- Cloudflare Pages — 静的成果物の配信
- Cloudflare のキャッシュ — 失敗7で入口が200を返し続けた理由
- リアルタイムログ — 実行と例外の確認
よくある質問
デプロイコマンドが成功すれば反映されていますか。
限りません。編集部の記録では、イメージの取得に失敗しても手元にある古いイメージで起動が成功し、コマンド全体は正常終了していました。反映の確認は、コマンドの終了コードではなく、実際に動いているものの中身で行ってください。起動しているイメージのダイジェスト、配信されているファイルのハッシュ、実効設定の出力などです。
設定ファイルを差し替えたのに反映されません。
コンテナに単一ファイルをマウントしている場合、ファイルの移動による差し替えは反映されません。マウントは実体を指しているため、新しいファイルに置き換わっても参照先が変わらないためです。編集部の例では、設定の検証コマンドも再読み込みコマンドも両方「成功」を返しながら、古い設定で動き続けていました。中身を同じ実体に上書きするか、コンテナを作り直してください。
ヘルスチェックが失敗しているのにサービスは正常です。
名前解決の経路を疑ってください。編集部の例では、コンテナ内でホスト名を解決するとIPv6のアドレスが優先され、サービスがそちらで待ち受けていなかったため接続が拒否されていました。サービス自体は正常で、35時間にわたって全てのチェックが失敗し続けていました。ホスト名ではなくアドレスを直接指定すると解決します。
自動デプロイが動いているかをどう確認すればよいですか。
実行の記録ではなく、成果物の変化で確認してください。編集部の例では、定期実行の登録は残っていたものの実行権限が失われており、毎回エラーで終了していました。本番のコミットは2か月以上前のまま止まっていましたが、誰も気づきませんでした。「最後に反映された時刻」を出す仕組みを用意してください。
証明書の確認は curl でできますか。
検証を無効にする指定を付けていると確認になりません。編集部の例では、検証を無効にしたコマンドでは正常な応答が返り疎通できているように見えましたが、検証を有効にすると証明書の不備でエラーになりました。実際の配信経路では検証が行われるため、そちらではエラーになっていました。確認は必ず検証を有効にした状態で行ってください。
何から手を付けるべきですか。
「最後に本番へ反映された時刻とコミット」を表示する仕組みを1つ作ることです。編集部の失敗のうち複数は、これがあれば当日に気づけました。作るのに必要なのは、デプロイ時にコミットのハッシュと時刻をファイルへ書き出し、それを返すURLを1本用意することだけです。