Share
Embed
claudeをサブドライバーに<br>システム移行した体験
オンラインくじ型のECサービス拡大のため、システム移行しました。
作業は claude code に任せ、**人は判断と、違和感を出す役**に回りました。
運転席に座るのは人のまま、**claude は交代でハンドルを握るサブドライバー**。
ハンドルは渡すが、**行き先と経路は人が決める**。
Naka Sho
August 17, 2026
Transcript
33 slides
-
はじめに 本日話すこと オンラインくじ型のECサービス拡大のため、システム移行しました。 作業は claude code に任せ、人は判断と、違和感を出す役に回りました。 運転席に座るのは人のまま、claude は交代でハンドルを握るサブドライバー。 ハンドルは渡すが、行き先と経路は人が決める。
今日話す範囲 フォーカスするのは移行当日(8/7 20:56 〜 8/8 09:45)。 前段の local / dev / test / stg 環境の構築は、ベースは人が手で作っています。 claude / codex に任せたのは、そこで出たコーナーケースの修正だけです。 2 -
抱えていた問題 抱えていた問題 1. 環境問題 a. local/dev/test/stg環境がなかった prod環境しかないので、本番一発勝負 b. コード管理が適切に行われていない git管理されていないので、サーバー上でvimやsftpで修正
c. WordPressとStreamlitの2つに分かれた管理画面 レンタルサーバーとStreamlit Cloudの2つ 2. アプリ問題 a. パフォーマンス問題 平均topページ 900ms 詰まるガチャ つまり、継続的な開発ができない、ユーザー満足度が低い 5 -
抱えていた問題 抱えていた問題の解消後のイメージ 旧 右にするためにやったこと local / dev / test /
stg 環境を作成 GitHub にコードを上げて管理 1つのサーバーに管理画面を同居 DB を分離してパフォーマンス改善 新 6 -
抱えていた問題 移行スケジュール 日程 7/10 - 7/17 7/17 - 7/24 7/24
- 8/7 8/8 内容 local環境構築 dev/test環境構築 stg/prod環境構築 移行 確認 現行環境(共用レンタルサーバー)の再現 テスト環境の確認、動作確認 stg/本番の確認、ツールなどの調整 移行当日 スピード感出して移行する 7 -
環境問題 local/dev/test/stg環境 ブランチ運用はGitHubフローに従う。mainにマージしたものはそのままprodへ反映して良いというシンプルな運用。 local — 各開発者が個別に使う環境。ローカルなのでどんな確認でも何でもできる。 devXX — 各開発者が個別に使う環境。日々の開発と自分のブランチの動作確認のベースになる。 test
— mainの状態を確認する環境。特定の人には紐付かない共用環境。 stg — 本番DBに接続している環境。本番のデータでなければ確認できないときに使う。扱いに気をつける。 prod — 本番環境。tagを指定してリリースする。 8 -
環境問題 ブランチ運用はGitHubフロー Git-Flow main GitHub Flow main develop feature/A feature/A
feature/B feature/B release feature/C hotfix Git-Flow — develop / release / hotfix と経路が分かれる。1本ずつは easy だが、 「今どこにいて、次はどこへ出すのか」を毎回考えることになる GitHub Flow — featureを切ってmainにマージするだけ。経路が1本 案件の直列は関係ない — どの案件も同じmainから切って並行に進む。 始めた順ではなく、mainにマージできた案件から順に出る リリースの工程を減らせたのは、その手前でブランチの経路を1本にしたから ※ 規模が小さいから選んだのではない。「mainのこの状態を本番へ」を1本の線で言えるようにしたかった。 9 -
環境問題 【問題】このリリース、タグは何番になる? v1.0.0 の状態から、次の5件をこの順でmainへマージした。semver(vX.Y.Z)で運用するなら、最後はどれ? ① 表示崩れ ② 検索機能 ③ 決済障害
④ 内部リファクタ ⑤ 付与ルール変更 バグ修正 機能追加 緊急対応 挙動は変わらない 挙動が変わる A B C D 1.0.1 1.0.1 1.0.1 1.0.1 1.0.2 1.1.0 1.1.0 1.1.0 1.0.3 1.1.0-hotfix 1.1.1 1.1.1-hotfix 1.0.4 1.1.1 1.1.2 1.1.2 1.0.5 1.1.2 1.2.0 2.0.0 ※ ③は緊急対応だから patch? でも②の機能がもう載っているから minor? ※ ⑤は破壊的変更? APIは変わっていないが、ユーザーから見た挙動は変わる。 10 -
環境問題 【解】どれも正解 どれも正解。ただ、その解に行くまでのプロセスが複雑 「これは minor か patch か」を毎回人が判断し、レビューで揉め、人によって答えが変わる 判断の根拠はチーム内の合意しかない。半年後に見返しても、なぜその番号なのか分からない semverは、外部に公開するAPIやライブラリの互換性を伝えるための規約。
自社サービスの本番デプロイに、互換性を伝える相手はいない その判断、本当に必要? 11 -
環境問題 GitHubフローとTAG採番 (マージで⾃動実⾏) GitHub Actions ⼈がやること feature ブランチ Pull Request
main にマージ タグを採番 v310 prod 戻すとき git checkout v310 リリースノートを タグに埋め込む Chat通知 デプロイしてください (タグのcheckoutだけ) post-checkout deploy.log に追記 git checkout v306 フック 通知 戻すと Chat v306 → v310 / v310 → v306 タグは手で打たない — mainへのマージごとに自動採番され、リリースノートもタグに入る 番号はそのコミットまでのコミット数。連番にはならず飛ぶが、大小関係が新旧の順序を表す prodはタグのcheckoutだけ — 戻すのも前のタグをcheckoutするだけ。前進も後退も同じ経路で deploy.log と通知に残る 12 -
環境問題 TAG採番の実装 mainへのマージごとに GitHub Actions( .github/workflows/tag-release.yml )がタグを打つ。 tag="v$(git rev-list --count
"$GITHUB_SHA")" 番号はそのコミットまでのコミット数 — 1回のpushに複数コミットが入ればその分飛ぶ。 番号自体に意味はないので、飛んでも問題なし。大小関係だけが新旧の順序を表す コミットから一意に決まる — build時間が長くてrunが並走しても、番号の取り合いにならない。 祖先タグの探索も二重付与のガードも要らない リリースノートはGitHubに生成させて( releases/generate-notes )、注釈付きタグの本文に埋める 条件 — force pushしないこと。commitの改ざんをしないこと。 ※ mainをforce pushで巻き戻すと番号も戻り、既存タグと衝突してrunが落ちる。 13 -
環境問題 リリースはシンプルに ⼀般 — 1⼯程ずつは easy。でも合わさると複雑 main merge どのタグ? major
/ minor / hotfix ⼿動でタグ付け draft pre-release release latest を付け替え 今回 — やりたいことが、そのまま1本の線になる main merge commit数で タグが発⾏される タグでリリース ▪ 人がやること ▪ 自動 シンプルは工程がすくないではない。やりたいことが簡潔なのか?理解しやすいのか?がシンプルだと思う。 一つ一つシンプルでも合わさればそれはシンプルではない 14 -
環境問題 Jenkinsからリリース / サーバー上でtag切り替え ジョブ deploy-hypes stg-release-hypes release-hypes 対象 指定できるref
やること test / devXX ブランチ・タグ git checkout <ref> stg ブランチ・タグ SSHして git checkout <ref> prod タグだけ SSHして git checkout <tag> リリース方法はjenkinsからと、サーバー上で作業と2通り。Jenkinsはサーバー作業を肩代わりしてくれているだけ 本番でやるのは git だけ。 git fetch -p --tags → git checkout <tag> のみで、dockerには一切触らない (prodとstgが同じVPSでnginxを共有していて、片方のために作り直すともう一方まで巻き込む) サーバーに入って切り替えることもできる。docker imageを変える場合はJenkinsからできないので、サーバーで直に作業する 15 -
環境問題 通知の実装 ① タグ発行 ② デプロイ タイミング 実装 webhookの置き場所 mainへマージした直後
GitHub Actions の最終ステップ GitHub Secrets prodでcheckoutした瞬間 gitの post-checkout フック prodのローカルgit設定 ※ gitの post-checkout フックを使っているので、Jenkinsからのリリースでも通知が飛ぶ 16 -
環境問題 local環境 1. 現行サーバーにsshして apache version, DBのバージョンを確認し、ローカルにコードをすべて落とす 2. docker-compose.yml で同一バージョンを落として動作確認する
問題 backupファイルが多すぎてどれが本物かわからない 現行サーバー側の既存の修正が先行する 現行で動いているものをとりあえず落として動かす。移行後、問題あれば現行サーバー側から取得する ※ ここから stg / prod までの環境構築は、ベースを人が手で作業している。claude / codex を使ったのは、詰まったコーナーケースの修正だ け。 17 -
環境問題 dev, test, stg, prod環境 1. docker-compose.dev.yml , docker-compose.prod.yml でそれぞれインフラ用に配置する
問題 DBサーバーは? 本番のみDBサーバー用意する。dev環境は予算の関係でDB分けない MariaDB だったので、マネージドDBが使えなかった サーバーインストールは? 柔軟性を重視し、dockerで管理する。オーバーヘッドはほぼない 別問題で apache だとパフォーマンスに問題あり apache → nginx + php-fpm に構成変更 Cloudflareの導入 18 -
環境問題 CloudflareをPRO化 問題 メンテナンス時のDNSの切り替えの伝播 VPNによる社内と一般の接続制限 不正攻撃に対する対策 対策 メンテナンス画面を Cloudflare Snippets
でエッジから返す(DNSを触らない) 切り替え・解除はルールのON/OFFで即時。伝播待ちゼロ、 no-store で古い503も残らない 社内/一般の出し分けを Snippet Rule の式(VPN固定IP)で判定 「一般=メンテ画面 / VPN=新オリジン」を同時に成立させ、本番ホスト名のまま確認できる 攻撃対策を Cloudflare 側へ集約 管理画面への国外アクセス遮断、REST API への不正リクエスト遮断 ログイン試行のレート制限、Super Bot Fight Mode によるボット対策 22 -
移行 移行手順を作成 前提条件 環境・デプロイフローを構築済み(local / dev / test / stg
/ prod、タグでのリリースと通知) Cloudflare 導入済み VPN 設定済み 1Password 利用可能 手順 — Cloudflare Snippets で、一般 / VPN の向き先を切り替えていく 段階 移行前 移行中 〃 移行後 一般 旧環境 メンテナンス メンテナンス 新環境 VPN メンテナンス メンテナンス 新環境 新環境 同時にやること — 画像ファイル / DB dump を新環境へ移行 機能テスト、一通りの正常系を確認 — 23 -
移行 移行リハーサル 手順は記載したので、claude code で実行する。 リハーサルでメンテナンスにするわけには行かないので、 VPN経由の場合のメンテナンスと新環境サーバーの切り替えを実施させる。 そうすることで、一般も同じことができるはず。 操作させるために 1Password
に以下を登録する sshキー(移行前、移行後の鍵を登録して操作) Cloudflare APIキー(DNSの切り替え、メンテナンスの切り替え) テストユーザー情報(会員サイトでログインをさせる) 鍵を渡すと、指示が「何をするか」だけで済む。 ログイン方法も切り替え手順も毎回説明しなくてよくなり、 DNSの伝播・画面表示・ログの記録という結果の確認まで自分で取りにいくようになる。 24 -
移行 移行当日 シナリオ 1. 最終リハーサル (待機) 2. データ移行 3. 社内確認
4. 公開 時刻 20:56 – 01:00 01:00 – 02:00 02:00 – 02:54 02:54 – 05:38 05:38 – 09:45 入力 24 – 6 26 34 操作 709 – 422 292 238 確認 本番と同じ手順を通しで実行し、手順書の間違いを潰す Cloudflare が 02:00 に自動でメンテナンスへ切り替わるのを待つ 停止確認 → cron 停止 → 画像 → DB → RENAME で切替 VPN 経由で新本番を検証。出てきた不具合を片端から潰す 公開してログの監視 操作の 68%(709回)が移行前のリハーサルに費やされている。移行本体の 422 回に対し、人が指示したのは 6 回。 25 -
移行 1. 最終リハーサル 時刻 20:56 22:53 23:28 ユーザー入力 移行リハーサルを最後に行いたいで す。旧環境を読み取りで新環境に画
像・DB を挿入してください。DB は 新しいスキーマ名で、完了後に DB 名をリネームで入れ替えてくださ い。 今日のリハのスキーマは削除してく ださい MAINTENANCE_START を本番の一 般側でも設定できますか?8/8 2:00 に自動でメンテナンスページに切り 替わるように Cloudflare に事前に 設定できますか? Claude の操作 画像 rsync 1.80GB → DB ダンプ 259 秒 → 新スキーマへ投入 35 分 14 秒 RENAME TABLE でスワップ 384ms / 94ms、合計 44 分 74 テーブル中 67 が行数完全一致、差分 7 件はすべて稼働中の書き込みで説明 WordPress が再起動なしで新 DB を掴むこと、全導線 200 を確認 指定された 1 つだけ削除 もう一方は残し、「取り消せない操作は明示されたものに限定しました」と申告 22:55、言われて初めて 2 つ目を削除 開始時刻を判定式に埋めた Cloudflare Snippet を事前投入 旧環境側には一切触らず、変更はエッジのルールのみ 結果 — 02:00 に人が何かを押す必要がなくなった 26 -
移行 2. データ移行 時刻 02:00 02:06 02:54 ユーザー入力 時間になったのでメンテナンスに なったのを確認しました。一般ユー
ザのアクセスがとまっていることの 確認を進めてください cron 止めて、画像、DB の移行を進 めてください。細かい手順は以下を 確認して(バックログのリンク) 社内確認をするので VPN を新環境 に向けてください(cron も起動して ください) Claude の操作 停止を 3 層で証明。① Cloudflare が 503 ② オリジン通過 0 件 ③ 旧本番 DB の書き込み系 5 テーブルを 145 秒監視して増分 0 crontab 6 ジョブを退避して削除、 DISABLE_WP_CRON 投入。画像 rsync は全 8 パス rc=0 / 転送 0 件 (前夜のリハで同期済み) 事故1 sed のクォートが shell に食われ、 wp-config.php が PHP8 で落ちる形に。直前のバックアッ プから 1 分で復元し Python で書き直し ダンプ完了 172 秒 / 1.5GB。投入前に 74 テーブル・除外 15 テーブルの混入 0 件を検証 インポート完了 34 分 44 秒 → RENAME TABLE でスワップ 380ms / 97ms。退避側にも VIEW を作り、 切り戻せる状態を維持 74 テーブル / 81,215,834 行が移行元と完全一致。ドリフト 0、超過 0 Cloudflare のルールを切替。VPN → 新環境、一般 → メンテ画面。新環境側 nginx のログに実 IP で記録 されることまで確認。ここまで 49 分 27 -
移行 3. 社内確認 データが入り終わってからが長い。移行本体の 3 倍の時間を、新環境で初めて表面化した不具合の切り分けに使った。 時刻 ユーザー入力 03:06 課金ができない
03:36 差分がない状態ということですか? 03:39 アイテム画像が出ない (実ユーザーからの報告) インデックス当てたら早くなる? 02:59 04:05 PR #75 / #74 の改修を本番に入れて いるので、旧環境側の修正を取り入 れてください Claude の操作 事故2(03:11) 旧環境版 functions.php で上書きし関数が消滅、トップが 6 分間 500。「差分 0 行」 を「上書きしても失うものがない」と誤読 revert して 94 関数を突き合わせ、両版に固有だった 6 関数ずつを手で合成(03:29) 犯人はメンテページ自身。 /charge はサーバーが Cloudflare 経由で自サイトを叩くため、自分の 503 を食っていた → ルール修正で 200 「一致しているのはコードだけです」と訂正。test環境に TURNSTILE_SECRET の配信漏れ=会員登録 が必ず失敗する状態だったことを自己申告 移行ではなく2 日前の最適化 PR が原因。添付の guid に旧サイト構成の URL が残存。添付 117 件 / ア イテム 567 件を修正 退避 DB(4,163 万行)で先に実測してから本番へ。4,128 万行の全走査 6.3 秒 → 0.147 秒。 LOCK=NONE で無停止、実行中もトップは 200(04:13 完了) 28 -
移行 4. 公開 時刻 ユーザー入力 Claude の操作 05:38 では一般公開を進めてください 05:41
1. エラーが発生していないか 2. 旧 環境側に人が流れていないか 今一般ユーザのアクセス数は秒速ど れくらいですか? 実顧客の問い合わせ 「10万円振り込んだが反映されな い」 監視終了で DNS の A レコードを 旧環境 → 新環境へ、同時にメンテナンスルールを無効化。「メンテのまま」も 「旧本番へ流入」も起きないよう連続実施。05:45 に全導線 200 公開後の 5xx 0 件(500 は 10 件あるがすべて 03:14 の自分の事故のもの)。旧 DB は 02:03 以降 1 行も書 き込まれていないことを 2 回測って確認し、 .htaccess で旧本番を 503 に固定 公開の瞬間に 0.02 → 4.4 req/秒(約 60 倍)。負荷試験の限界 31.6 req/秒に対し 7 倍の余裕。待ってい た利用者が戻ってきた 読み取りだけで原因を特定し、課題に起票して補填まで実行 → 次ページ 05:47 08:42 09:45 ありがとうございました 29 -
移行 42秒の穴 旧環境の最終書き込み 01:59:26 / 失われた入金確定 02:00:08 銀行振込のポイント付与は、決済代行からの webhook 受信が唯一の起点。
メンテナンスの 503 に当たって消えた 翌朝 08:42、実顧客から「深夜 2 時少し前に 10 万円振り込んだが反映されない」 補填は SQL の直 INSERT ではなく、webhook ハンドラと同じ関数を wp-cli から実行(冪等チェック込み)。 568pt → 106,568pt 09:33、移行後はじめて本物の webhook が着弾。経路が生きていることを確認 止めるのは自分たちのトラフィックだけじゃない。 「メンテ明けに未着 webhook を拾う」を手順に入れる。 30 -
移行 claudeはなにをやったのかをまとめる 移行本体は 6 回しか喋らない リハーサルは 手順書のテスト 自己申告が いちばんの価値 先回りさせない
02:00 から 02:54 のあいだ、人間が入力したのはたった 6 回です。「確認して」「cron 止めて移行して」「VPN を向けて」 ——それだけで 422 回の操作が走りました。 そうできたのは前日までに全部決めてあったからです。当日に判断が残っていたら、この密度では回りません。 5 時間前のリハで見つかったのは、性能の問題ではなく手順書の間違い 4 件でした。3 日前に廃止した .env を読む手 順、終了コードを見ない rsync、止まっていない書き込み。 本番の実測値がリハとほぼ一致したことが、手順が正しく機能した証拠になりました。 この夜、Claude は自分から何度も報告しています。「トップページを 6 分間 500 にしました、原因は私の判断ミスです」 「一致しているのはコードだけです」「その Warning は私のプローブです」「www が 503 と報告しましたが私の計測ミス でした」。 作業量より、作業ログの正直さのほうが効きます。深夜に一人で判断するとき、頼れるのは相手の報告だけなので。 「バックアップ DB を削除して」に対し、Claude は指定された 1 つだけを消し、もう一方は「取り消せない操作は明示さ れたものに限定しました」と残しました。 親切な先回りが最悪の結果になるのが移行作業です。不可逆な操作は明示されたぶんだけ——この線引きを最初に合意して おく価値があります。 31 -
まとめ 抱えていた問題を解消 1. 環境問題 a. local/dev/test/stg環境がなかった prod環境しかないので、本番一発勝負 → local, dev,
test, stg, prod 作成 b. コード管理が適切に行われていない git管理されていない → GitHub移行、GitHubフローで開発 c. WordPressとStreamlitの2つに分かれた管理画面 レンタルサーバーとStreamlit Cloudの2つ → 同一サーバーに移行 2. アプリ問題 a. パフォーマンス問題 平均topページ 900ms → 450ms (CDN配信・nginx/php-fpm分離・インデックス追加の合算) 詰まるガチャ → 新規システムに切り替え中 32 -
まとめ claudeをサブドライバーにシステム移行した体験 AIに正直な作業ログを吐かせて、人が違和感で検算する 冒頭に挙げた「体裁は整っているが内容が空疎」は、成果物ではなく過程が見えないことから生まれる。 作業量ではなく、作業ログの正直さが効いた。 1. 細かい手順は不要 — 人が手順を理解していること前提で操作をさせる 2.
リハーサル必須 — 1Password に sshキー・APIキーを持たせ、作業結果を自走でコメント投稿させる 3. VPN必須 — 移行前・移行中・移行後の確認画面を、一般 / VPN で出し分ける 33