きっかけ
最近はチーム内でレガシーなバッチ群(CakePHP2)を新基盤(CakePHP5)に移す案件をやっています。
以下、旧を legacy-batch、新を new-batch と呼びます。
今回担当するものは一斉切替じゃないので、しばらく新旧が並走するんです。
境界の注文が「どっちにも処理されない」「両方に処理される」なんてことが起きたら事故なので、ここは絶対に落とせません。
従来なら、切り替えの日付に手動で新旧バッチを回して確認するか、スクリプトである程度自動化させる方法でやりますが、AI時代なら、本番の移行スケジュールどおりに日付を1日ずつ進めて新旧のバッチを全部回すこともできなくはないので、今回はその方針を選びました。
そして、その物量を見て「これは手では終わらないな」と思ったものを、実際にAIが終わらせてくれました。
難しかった点
1. 日付を進めないと再現できない
対象のバッチ群は、注文の状態を日次で1つずつ前に進めていきます。図にするとこんな感じです。
1件の注文がステート1からステート8にたどり着くまで、数ヶ月かかります。
あるバッチが書いた行を数週間後に別のバッチが拾う形なので、その連鎖がどこかで切れていないかは、1日分だけ動かしても分かりません。結局、日付を進めて通しで回す方がシンプルですが、その分、コマンドの実行量も膨大になりました。
2. バッチの本数が多い
並走期間中に1日で叩くコマンドの数がこれです。
| 1日に実行するコマンド数 | |
|---|---|
new-batch |
11本 |
legacy-batch |
5本 |
| 合計 | 16本 |
移行期間ぶん回すと、延べ1,000回超になります。しかも「実行できました」では意味がなくて、1本ずつ対象件数が想定どおりかを見ないといけないので、手作業でやり切るのは現実的ではありませんでした。
1日の流れも、新旧が時間帯で入り混じるので少し複雑です。実行時刻に並べると、こうなります。
| 時刻 | legacy-batch |
new-batch |
|---|---|---|
| 01:30 | バッチA | バッチA → バッチB |
| 01:40 | バッチB | |
| 02:30 | バッチC | |
| 03:00 | バッチD | |
| 04:05 | バッチE | |
| 04:10 | バッチS | |
| 05:15 | バッチG | バッチG |
| 05:25 | バッチF | バッチF |
| 06:05 | バッチE-1 → E-2 → E-3 → E-4 |
同じ時刻に新旧が並ぶ枠もあれば、片方だけの枠もあります。新基盤のバッチEはE-1〜E-4の4本に分かれていて、06:05からこの順で続けて実行します。
この順番は入れ替えられません。E-2が作った行を同じ日のE-3が処理するので、1つ順序を間違えるだけで結果が変わってしまいます。なのでシミュレーションでも、本番と同じ順序を守る必要がありました。
3. 新旧の担当が日付で分かれる
どちらが処理するかは、注文の基準日で決まります。基準日が切替日より前なら legacy-batch が最後まで面倒を見て、以降は new-batch に任せる、という分け方です。
しかも後半の工程(バッチE以降)は別のタイミングで切り替わるので、境界(フェンス)が2つあります。
この前後で取りこぼしや二重処理が起きないかどうかが、今回いちばん確かめたかったところです。
AIエージェントに向いていそうだと思った理由
やる前に、これはAIに任せられるかもしれないと思った理由が3つありました。
- 画面操作がほぼない。コマンドを叩いてDBとログを見るだけで完結します。ブラウザを触らせる必要がないので、AIに任せやすい部分だと思いました
- 単調な繰り返しだけど、毎回判断が要る。「今日は何件処理されるはずか」を業務ロジックから導いて実績と突き合わせる、という作業がセットになります。これは静的なスクリプトには書ききれないですし、人がやると3日目くらいで確認が甘くなってきます
- ズレたときの調査まで地続き。件数が合わなかったら、そのままログを読んでSQLを投げて、原因の仮説まで立ててくれます。テスト実行と障害調査を行き来できるのが、スクリプトとの一番の違いでした
そこでClaude Codeに任せてみることにしました。
AIにやらせたこと
実際に任せたのは、次の3つです。日付を進めながらバッチを回すところ、実行前に対象件数を数えさせるところ、そして最後にレポートまで作らせるところ、という流れになりました。
1. 日付を偽装して1日ずつ回す
日付の注入には faketime(libfaketime)を使いました。
レガシー側にも日付を指定するオプションはあったのですが、内部のシェル呼び出しには効かず、途中で本物の日付に戻ってしまいます。それなら、プロセスごと偽装してしまったほうが確実でした。
まずは実行コマンドと本番のcron時刻・順序を、一度だけ教えます。
【プロンプト例】
新旧バッチを本番の移行スケジュールどおりに実行するシミュレーションをしたい。
- legacy-batch(CakePHP2、コンテナA):
docker exec <container> faketime '<日付> <時刻>:00' \
<path>/Console/cake <command> --action=<action>
crontab どおり 01:30 バッチA → 01:40 バッチB → 04:05 バッチE
→ 05:15 バッチG → 05:25 バッチF の順。
- new-batch(CakePHP5、コンテナB):
docker run --rm -v <repo>:/app --network <net> <image> \
faketime '<日付> <時刻>:00' bin/cake <command> --target-date=<日付>
01:30 バッチA → バッチB → 02:30 バッチC → 03:00 バッチD
→ 04:10 バッチS → 05:15 バッチG → 05:25 バッチF
→ 06:05 バッチE-1 → E-2 → E-3 → E-4 の順。
- 切替は基準日ベース。legacy-batch はフェンス日より前の基準日のみを対象にする。
- 本番の移行計画、各想定リリース日とリリース手順。
これを一度渡しておけば、以降はこの前提を守って実行してくれます。
もちろん、一回の指示で全て分かってくれることはないので、数回やりとりして、プロンプトの改善もやっておりました。
2. 実行前に「事前想定」を数えさせる
今回いちばん効いたのがこの部分で、AIにテストを任せるなら、ここは外せないと思っています。
各バッチを実行する直前に、そのバッチの抽出条件をSQLでミラーした件数をDBから取ってきて、記録させます。 そして実行後に、ログから読み取った実績と突き合わせます。
-- 例: バッチAの「事前想定」
SELECT COUNT(*) FROM orders WHERE {バッチAの抽出条件}
なぜこれが必要かというと、ログに「3件処理しました」と出ていても、それが正しい3件なのかは分からないからです。5件処理されるべき日に3件で終わっていても、ログ上は何も問題なく見えてしまいます。
これを新旧15本ぶん用意しました。効いたのは次の3つです。
- AIの「できました」を数値で検証できる。想定5件・実績3件なら不一致として記録されるので、報告を信じるかどうかで悩まなくなります
- 抽出ロジックの理解が正しいかを毎日確かめることになる。実装とSQLミラーが同じ答えを出し続ける、という相互チェックになっています
- バグの発見装置になる。
逆にいうと、これがないとAIによるテスト自動化は成り立たないと思っています。実行ログだけを見ていると、どうしても「エラーなく完走した=OK」に流れてしまいます。
日次ループの骨子は、最終的にこうなりました(擬似コード)。
acquire_lock # 二重起動の防止
for day in $(seq_dates "$SIM_START" "$SIM_END"); do
[ -f "raw/$day/.done" ] && continue # 再開可能にする
clear_logs "$day" # 前回ランのログ混在を防ぐ
for spec in "${SPECS[@]}"; do # 本番cron時刻の順に並べたリスト
expect=$(precount "$spec" "$day") # ← 事前想定
echo "$spec expect=$expect" >> "expect/$day.tsv"
run_batch "$spec" "$day" # faketimeで実行 → raw/ に保存
done
touch "raw/$day/.done"
done
全体の流れも図にしておきます。
ここでのポイントは、想定と実績がズレても止めないことです。止めてしまうとその日で打ち切りになるので、差分は記録だけして先に進ませ、あとでレポートを見て人が判断するようにしました。
3. 最後にカレンダーレポートまで作らせる
レポート化も任せました。指示は以下な感じです。
【プロンプト例】
最終日まで完走させて、結果をカレンダー式のHTMLレポートにまとめて。
できたものがこれです。
月別カレンダーの各日に、新旧それぞれの実行結果と件数が並びます。色分けは自動なので、想定と一致した日・差分が出た日・フェンスが効いて0件だった日が、ぱっと見で分かります。
日付をクリックすると、その日のバッチ一覧が出ます。1本ごとに実行時刻・事前想定と実績の対比・実行コマンド全文が並び、ログビューアでバッチの出力の中身まで追えます。
よかった点
1. AIが精度高く自走できた
前提を一度渡したあとは、見張っていなくても、1日16本を数十日ぶん、本番どおりの時刻と順序で回し続けました。 しかも1本ごとに事前想定と実績を突き合わせて、記録まで残しながらです。
「なんとなく動いたように見える」ではなく、数値で一致を示せる状態で完走できたのが大きいと思っています。
2. 期間の長いテストサイクルを何度も回せた
このテストは、1回まわすのに移行の全期間ぶんの日数が必要です。本来なら1周するだけで数日仕事でした。
それを7ラン、実行日数の合計で400日以上まわせました。
| ラン | 内容 |
|---|---|
| 1 | 最小データで移行全期間を通し |
| 2 | 本番規模データ |
| 3 | 本番規模データで全区間完走 |
| 4 | 最小データで再検証 |
| ... | ... |
繰り返せるようになると、テストの位置づけが変わってきます。データを作り直して「本番規模だとどうか」「まっさらな環境でも同じ結果になるか」「閾値を変えたら何が起きるか」と観点を足しながら、何周も確認できました。1周が数日仕事のままだったら、このうちどれか1つしか選べていないので、全部やれたことがいちばん大きいと思っています。
手順は最後にスキル(手順書)としてまとめたので、次回は日付とコンテナ名を差し替えるだけで再実行できます。
工数はこうなりました。
| 作業 | 手作業なら | 実際 |
|---|---|---|
| 日次実行と件数突合(延べ400日+分) | 約12人日 | 自動実行 |
| テストデータ準備・日付調整(7ラン分) | 約3人日 | 指示のみ |
| 実行基盤・検証スクリプトの作成 | 約3人日 | レビューのみ |
| レポート・証跡の作成 | 約2人日 | 自動生成 |
| 合計 | 約20人日 | 実働3日 |
手作業側は「1日分の実行と確認に30分」という仮定のざっくり計算です。
とはいえ現実には、20人日かけてこれをやる判断にはならないと思います。なので削減幅そのものよりも、やらずに済ませていた検証が実施できたことのほうが価値があったと感じています。
今後やりたいこと
画面からの手動操作を含むシナリオは、今回は人手でやりました。ここもブラウザ操作系のツールと組み合わせて、シナリオテストまで通しで自動化したいところです。
まとめ
バッチのテストは、AIエージェントに任せやすい領域でした。 画面操作がなく、コマンドとSQLで完結するのが効いています
同じやり方は「日付駆動で状態が進む業務フロー」全般に使えると思います。時間軸のテストは、コストが見合わないからと省略されがちだったと思うのですが、そこが変わりつつある感覚があります。同じような移行を控えている方の参考になれば嬉しいです。
