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