nun_game0

AIが書いたコードをそのまま本番に出して痛い目を見た話

AI本番障害

先に言っておくと、これは完全に自分のミスの話。AIが悪いという話ではない。

金曜の夜、締め切りに追われて判断力が落ちている時にやらかした。

何が起きたか

社内ツールの改修で、設定ファイルのバリデーション関数をAIに書かせた。出力を見て「良さそう」と思い、テストデータ1パターンだけ通して本番にデプロイした。金曜の21時。早く終わらせて帰りたかった。

翌週月曜の朝、Slackの通知が止まらない。設定ファイルの更新が全て失敗しているというアラート。バリデーション関数が、正しい設定ファイルまで弾いていた。

朝9時の時点で、社内の設定変更が約30件溜まっていた。全部バリデーションエラー。担当チームから「設定が反映されない」「週末に入れた変更が消えた?」という問い合わせがSlackに複数来ていて、出社して画面を開いた瞬間に血の気が引いた。

原因

AIが書いたバリデーションのロジックが、仕様を部分的にしか満たしていなかった。

具体的には、オプショナルなフィールドが未指定の場合を「不正」と判定していた。仕様上はオプショナルだけど、AIに渡したプロンプトにその情報を含めていなかった。

設定ファイルのスキーマには15個のフィールドがあり、そのうち6個がオプショナル。自分がAIに渡したのは「設定ファイルをバリデーションする関数を書いて」という雑な指示と、必須フィールドの一覧だけ。オプショナルフィールドの存在を伝えなかったから、AIはそれらを「未知のフィールド」として扱い、存在しない場合にエラーを返すロジックを生成した。

つまり、AIは渡された情報の範囲で正しい実装をしている。雑なプロンプトを投げた自分が悪い。

障害の原因分析

なぜレビューが甘くなったか

金曜の夜で早く帰りたかった、というのが正直なところ。AIが出したコードが「動いている」ことを確認しただけで満足してしまった。

普段なら他人のコードをレビューする時に仕様との照合をやるのに、AIが書いたコードだと「AIが書いたから大丈夫だろう」という油断が生まれる。自動化バイアスと呼ばれるやつ。自分はこれにまんまとハマった。

振り返ると、自分のレビューにはいくつか穴があったが、一番痛かったのは既存テストを回さなかったこと。

既存のテストスイートを通していなかった。 このツールにはもともとユニットテストがあった。そのテストには、オプショナルフィールドを省略したケースがちゃんと含まれていた。「バリデーション関数を追加しただけだから」と既存テストの実行を省略したが、たった1回 go test ./... を叩いていれば、この障害は起きていなかった。既にある資産を使わなかった。これが一番悔しい。

他にも穴はあった。テストで使った設定ファイルが全フィールド揃ったサンプルデータ1パターンだけだったこと。AIの出力をざっと眺めただけで、各フィールドのバリデーション条件を仕様書と1つずつ照合しなかったこと。コードの構造が綺麗だったので、中身も正しいだろうと思い込んだ。どれも初歩的なミスだけど、疲れと焦りで全部すっ飛ばした。

修正と対応

社内ツールだったのが不幸中の幸い。ユーザー向けサービスだったら始末書では済まなかった。

修正自体は10分。オプショナルフィールドの判定を追加するだけ。原因特定より、影響確認と関係者への「すみません」連絡の方がよほど時間がかかった。

対応タイムラインはこう。

  • 09:00 アラート検知、Slackで問い合わせを確認
  • 09:15 原因特定(バリデーション関数のオプショナルフィールド判定)
  • 09:25 修正コードをデプロイ
  • 09:30 滞留していた設定変更30件が正常処理されたことを確認
  • 10:00 関係者への障害報告とポストモーテムのドラフト完了

障害時間は約60時間(金曜21時のデプロイから月曜朝9時25分の修正デプロイまで)。社内ツールだったので週末は利用者がおらず、実質的な影響は月曜朝の1時間半程度だった。それでも30件の設定変更が詰まっていたのだから、週末挟んでなかったらもっと被害は大きかった。

対策チェックリスト

学んだこと

一番の教訓は「既存のテストを回せ」。これに尽きる。10分で書けるコードのために、既にある検証の仕組みを省略して障害を起こした。新しいプラクティスを導入する前に、今あるものを使い切れという話。

あとは2つ。

AIは仕様の全体像を知らない。 自分がコンテキストを渡し忘れたらそのまま穴になる。AIにテストを書かせても、同じコンテキスト不足を引き継ぐ。テストケースの設計だけは仕様書を見て人間がやる方が安全だった。

金曜の夜にデプロイするな。 これはAI関係ない。でも今回、疲れと焦りでレビューが雑になったのは事実で、もし翌日すぐ対応できる時間帯にデプロイしていれば、被害は数分で済んでいた。

この失敗を受けて導入したレビュー体制

ポストモーテムの結果、チームでいくつかルールを変えた。

CIが通るまでデプロイをブロックするようにした。 一番効いたのがこれ。もともとCIにテストは組み込まれていたが、ローカルで動作確認したらCIの結果を待たずにデプロイできる状態だった。これを物理的にブロックした。仕組みで防げるものは仕組みで防ぐ。今回の障害はこれだけで防げていた。

PRに「AIが生成したコードか」を明記するルールにした。 AIコードの場合、レビュアーは追加で以下を確認する。

  • AIに渡したコンテキストは十分か
  • 仕様書とコードの各条件分岐が1対1で対応しているか
  • 境界値・異常系のテストケースが網羅されているか
  • AIが「知らないはず」の暗黙の仕様がないか

正直、チェックリストがどこまで機能するかは分からない。でも「AIコードは追加で確認が必要」という意識をチームに共有できたのは良かった。

AIへの丸投げをやめた。 AIと対話しながらコードを書くスタイルに変えた。途中で「オプショナルフィールドはある?」と聞ける。丸投げして結果だけ受け取るのとでは、仕様の抜け漏れへの気づきやすさが全然違う。

その後、変わったこと

この障害以降、AIにコードを書かせる時のやり方がだいぶ変わった。

プロンプトに仕様を丸ごと渡すようになった。 「バリデーション書いて」ではなく、仕様書のURLか全文を含める。手間はかかるが、出力の精度が段違い。今回の障害も、オプショナルフィールドの仕様を渡していれば起きなかった。

AIの出力を「知らない人のPR」として読むようになった。 自分が指示を出したから自分のコードだ、という感覚があると、レビューが甘くなる。知らない人がPull Requestを出してきたと思って読む。そうすると仕様との照合を省略しなくなる。

テストコードだけは自分でケースを洗い出すようになった。 AIにテストを書かせるのは良い。ただし「どのケースをテストするか」は人間が決める。AIが書いたテストは、AIが書いたコードと同じ盲点を持っている。テストが通ったから安心、は危ない。

金曜の夜にデプロイしなくなった。 社内ツールでも。段階デプロイも導入して、ステージングで1日動かしてから本番に出すようにした。

結局のところ

AIが書いたコードを本番に出す判断をしたのは自分。障害の責任はAIではなく自分にある。

今回一番身に染みたのは、「AIが速くコードを書いてくれること」と「そのコードが正しく動くこと」は全く別の話だということ。速く書けるようになった分、レビューと検証に時間を使わないと、結局障害対応で倍の時間を持っていかれる。

あと、既存のテストを回していれば防げたという事実が地味にきつい。新しいツールやプラクティスの前に、今あるものを使い切る。当たり前のことを当たり前にやる。疲れている金曜の夜でもそれができる仕組みを作る。自分にとってはそれが一番の収穫だった。

関連記事

SharePost

他の記事