#Kubernetes#CI/CD#git#チーム開発

デプロイのたびに互いの成果を消していた — 2セッションで3回やった巻き戻し事故


同じマイクロサービス群を、2つの作業系が並行して触っていた。片方は音声パイプライン、もう片方は会話品質。担当範囲は分かれていて、触るファイルもほぼ重ならない。

それでも3回、お互いの成果を本番から消した

1回目: フロントエンドの5コミットが消えた

フロントの修正を1つ入れるために、main からブランチを切ってビルドし、デプロイした。

ユーザーから報告が来た。

レコードがなくなり、以前消したはずの画面が復活しています。

撤去したはずの画面が戻り、追加したはずの機能が消えていた。調べると、稼働していたイメージは main から焼かれていなかった

$ grep -l "virtualpv:1.0.407" ~/kaniko-builds/*.yaml | xargs grep -- --branch
  --branch feat/character-list-preview

そのブランチは main より5コミット進んでいた。main 基点で焼いた時点で、その5件が本番から消えた。

このリポジトリでは main が稼働の正本ではなかった。 未マージのブランチを数えると、54件・12件・6件…と積み上がっていた。「main は最新」という一般的な前提が、この環境では成り立っていない。

直前のタグへ kubectl set image で戻して復旧し、正しい基点に cherry-pick して焼き直した。

2回目: バックエンドの17版が消えた

1回目の教訓として、こういう手順を決めた。

# 焼く前に、稼働版の基点ブランチに対して自分が取りこぼしていないか
git log --oneline HEAD..<稼働版の基>

そして次のビルドで、私はこれを実行した上で事故を起こした。

バックエンドを焼く前、稼働Podに入って自分の変更が入っていることを確認した。

>>> inspect.signature(judge_transcript).parameters['max_inserted'].default
1     # 私が入れた値。稼働版は私の変更を含んでいる

これで安心して main 基点で焼いた。結果、もう一方の17版が消えた。会話ログの記録が止まり、60分間ぶん失われた。

確かめたのは「稼働イメージが私の変更を含んでいるか」だった。確かめるべきは「私のビルドが稼働イメージを含んでいるか」だ。この2つは別の命題なのに、同じものとして扱っていた。

差分を取れば一目で分かる状態だった。

$ git log --oneline <自分のHEAD>..<稼働版の基> | wc -l
20

焼く前にこれを1回実行していれば防げた。手順は決めていたのに、リポジトリが変わったら実行していなかった。fvではマニフェストの --branch まで辿ったのに、バックエンドではPod内の値を1つ見て済ませている。

3回目: 手順を守った側が被害を受けた

しばらくして、逆方向で起きた。

私がバックエンドを直してデプロイした。そのあと焼き直しの監視に移り、main へ push するのを忘れた

もう一方は正しい手順を踏んでビルドした。自分のブランチを稼働版の基点と突き合わせ、取りこぼしがないことを確認している。そのとき main にも相手のブランチにも私の変更は無かったので、確認は通過した。そして私の変更が消えた。

確認する側がどれだけ正しくても、相手が push していなければ防げない。

ブランチ確認では足りなかった

3回目のあと、もう一方が別のリポジトリで4回目を寸前で止めた。使ったのは、稼働Podのファイルと自分のブランチの sha256 突合だった。

pipeline.py     一致
brain_talk.py   ⚠️差分  稼働=1a02…  自分=341e…
app.py          ⚠️差分  稼働=8a41…  自分=4110…

差分の中身は、私が入れた配信の復帰対策3件だった。フルビルドしていれば全部落ちていた。

このとき本人は基点ブランチを取り違えていた(feature ブランチだと思っていたが、実際は main から焼かれていた)。git log HEAD..<基点> は基点が分かっている前提の手順なので、思い込みがあると素通りする。sha256 突合はブランチ名を知らなくても成立する。

もう一つ分かったのは、ファイルの存在確認では足りないことだ。私は2回目の事故のあと「相手の conversation_log.py があるか」を確認するようにしたが、今回問題になった2ファイルは存在するが中身が違う状態だった。存在チェックでは検出できない。

結論: 2つで足りる

3回の事故から抽出すると、必要なのは2つだけだった。

① デプロイ直後に main へ push する。

これが本体。両者がこれを守れば、main が常に稼働と一致するので、②は自然に通る。3回目の事故は、これを私が破った結果だった。

② 焼く前に、稼働Podと自分のブランチを sha256 で突き合わせる。

これは保険。①が守られていない相手がいても、これで止まる。

kubectl exec -n <ns> deploy/<dep> -- sh -c \
  'cd /app && find apps/services -name "*.py" | sort | xargs sha256sum' > /tmp/pod.txt
(cd <repo> && find apps/services -name "*.py" | sort | xargs sha256sum) > /tmp/br.txt
diff /tmp/pod.txt /tmp/br.txt   # 差分が自分の変更分だけかを見る

②だけでは3回目が防げず、①だけでは1回目が防げない。両方が要る。

付随して踏んだもの

kubectl rollout status の完了を信じてはいけない。 「successfully rolled out」と出た直後に、新旧2つのPodが並んでいた。

exista-voicepipe-<rs-old>-lpqtx   Running  ...:0.7.14
exista-voicepipe-<rs-new>-cv92x  Running  ...:0.7.15

数秒後に収束したが、その間に検証していれば「効くPodと効かないPod」に振り分けられて、再現しない挙動に悩むことになる。デプロイ後はPod数を直接数える。

似た事故を、私は生成バッチでも踏んでいる。200本のクリップを生成している最中にPodが入れ替わり、前半と後半が別の設定で作られた素材ができあがった。ログを見て「新しいゲートが1回も発動していない」ことに気づくまで、原因が分からなかった。

ビルド定義を使い回さない。 過去のマニフェストを sed でタグだけ書き換えて使っていたが、--branch の書き換えを忘れて1回目の事故を起こしている。用途ごとに分けて残すほうがいい。実際、もう一方は「基点タグは稼働中のタグにすること」という注意書き込みで定義を残すようにした。

タグの上書きに注意。 過去に && の連鎖が切れてタグの bump が飛び、古いタグに新しい中身を push した事故があった。そのタグへのロールバックは、もう当時の挙動に戻らない。ロールバック先を選ぶとき、この履歴を知らないと詰む。

一般化できること

この事故は「並行作業だから起きた」ように見えるが、単独でも起きると思う。

1回目の直接の原因は、「main が稼働の正本である」という一般的な前提が、この環境では成り立っていなかったことだった。デプロイ用のブランチが正本で、mainは古い。この状態は、リリースフローが整っていない環境ではよくある。

そして単独作業でも、過去の自分が別ブランチで焼いていれば同じことが起きる。1か月前の自分は他人と変わらない。

確認すべきは「自分が何を足したか」ではなく「いま動いているものを、自分のビルドが含んでいるか」だ。 前者は覚えているが、後者は覚えていない。だから機械的に突き合わせる。


シリーズ: 拡散TTSから実用ボイスを量産する

キャプション1行から声を設計し、学習コーパスを製造して、役割別の実用ボイスを量産するまでの記録です。この記事は第4部 運用にあたります。

← 前: 登録経路が4つ、管理画面が0 → 次: 測れていないものを閾値で追い込むと、必ず失敗する

シリーズ全18本
  1. 音質で選んだTTSが、会話には遅すぎた
  2. 声をガチャで引く
  3. 「ナレーターっぽい声」を24候補から機械に選ばせる
  4. 品質ゲートを厳しくするほど、棒読みが生き残る
  5. 学習後に話速は変えられない
  6. 生成するたび「録音場所」が変わるTTS
  7. クリップ1本の粗さが、スタイル全体を嗄れさせる
  8. AIが「こんにちわー」と伸ばす癖は、どこで身についたのか
  9. 「少々」が「しょも」になる — 許可文字リストが日本語を削っていた
  10. ハルシネーション対策のコードが、ハルシネーションの時だけ動かなかった
  11. 品質ゲートが許した「3文字」が、モデルの口癖になった
  12. 治せる欠陥で候補を落としていた
  13. 文字起こしでは見つからない欠陥がある
  14. 70分かけた学習素材が、ネットワークの一瞬の瞬きで消えた
  15. “ja” を “JP” と書いて喃語モデルができるまで
  16. 登録経路が4つ、管理画面が0 17. デプロイのたびに互いの成果を消していた ← いまここ
  17. 測れていないものを閾値で追い込むと、必ず失敗する

知見の元になったノートは 拡散TTSから実用ボイスを量産する製造パイプライン にまとめてあります。

← 記事一覧へ