Docker・Multipass・CentOS 7・Capistranoを経て、新VPSとKamal 2へ
こんにちは、迷い家の白猫のシロです。
今回は、東方アレンジ楽曲のデータベースサイト「東方編曲録」の大きな更新について書きます。
主な内容は次のとおりです。
- Ruby 3.0.0からRuby 4.0.6へ更新
- Rails 6.1.7.10からRails 8.1.3.1へ更新
- 古いVPS上の本番環境を直接更新せず、新しいVPSを用意
- Capistrano中心のデプロイから、新VPSではKamal 2へ移行
- 新VPSの事前検証環境、Basic認証、ヘルスチェック、メンテナンス、rollbackを準備
- 直近で行った公開画面・管理画面のUI改善
この記事で扱う変更は、プライベートリポジトリで行った一連の変更をもとにしています。リポジトリは非公開のため、この記事ではコードそのものではなく、公開できる範囲の設計と運用の流れを紹介します。
先に大事なことを書くと、変更をマージした時点では、本番DNSを切り替える前の新VPSの事前検証までが完了し、本番DNSの切り替えとproduction向けKamalデプロイはまだ実施していません。本番切り替えの手順も含めて、どこまで準備できたかを記録します。
まず結論
以前は、RubyとRailsの更新を何週間もかけて少しずつ行う計画でした。実際には、更新対象を細かいコミットに分け、Dockerで実行環境を固定し、新しいVPSを並行して用意することで、最初の更新コミットから一連の変更のマージまでを1日以内に終えられました。
Gitの履歴では、最初のRuby/Rails更新コミットから一連の変更のマージまでを、約半日で終えています。
この速度を実現できた最大の要因は、開発パートナーとしてCodexを使えたことです。既存コードの調査、依存関係の更新、互換性問題の切り分け、Kamal移行の設計、UI回帰の確認を、調査・実装・テストのサイクルとして短く回せました。
ただし、これは「古い本番サーバーへその場で更新を適用した」という意味ではありません。旧本番を触らずに、新環境を作って検証し、最後にDNSを切り替えられる状態まで持っていった、という速さです。
更新前と更新後
バージョンの比較
| 項目 |
更新前 |
更新途中 |
更新後・新環境 |
| Ruby |
3.0.0 |
3.3.12 → 3.4.10 |
4.0.6 |
| Rails |
6.1.7.10 |
7.0 → 7.1 → 7.2 → 8.0 |
8.1.3.1 |
| Puma |
4.3.12 |
互換対応 |
7.2.1 |
| Rails実行環境 |
ruby:3.0.0 |
Dockerイメージを更新 |
ruby:4.0.6-bookworm |
| デプロイ |
Capistrano 3 + rbenv + Nginx |
旧環境を維持 |
Kamal 2.12 + Docker + kamal-proxy |
| DB |
既存のMySQL構成 |
変更しない |
MySQL Accessory |
Railsは一気に8.1へ書き換えるのではなく、7.0、7.1、7.2、8.0、8.1の順番に通しました。Rubyも3.3.12、3.4.10を経由しています。
この順番にしたことで、どのバージョンで互換性問題が出たのかをコミット単位で追跡できます。Rails 8向けのenumや管理画面パラメータ、Rails 8.1向けのPaperTrailなど、変更理由も分けて記録できました。
CentOS 7 + rbenv上でRuby 4.0.6を試す
現行本番は、さくらのVPS上でCapistrano、Nginx、Pumaを動かしています。ホストOSはCentOS 7で、Rubyはrbenvで管理していました。
CentOS 7はすでにEOLを迎えています。それでも、まずは既存環境のままRuby 4.0.6を入れられないか試しました。
rbenvでRuby 4.0.6をインストールするために、コンパイラ、OpenSSL、ビルド依存、RubyGems、既存Gemとの互換性などを一つずつ確認し、かなり頑張ってインストールを進めました。インストールコマンドが通るところまでは進めても、それだけで本番アプリケーションが安定して動くとは限りません。
アプリケーションのbundle、Puma、Capistrano、画像処理、アセット、既存のRailsコードまでを、Ruby 3.0.0から4.0.6へ安定して移行する必要があります。CentOS 7自体がEOLのため、ここで問題が起きた場合にOS側の更新で解決できないことも不安要素でした。
旧コンテナ側でも、Ruby 3.0.0のベースイメージはDebian Busterを前提にしており、パッケージ取得にはすでにarchive.debian.orgを使う対応が必要になっていました。
Ruby 4.0、Rails 8.1、Puma 7を古いOS・古いライブラリ環境へ直接載せ替えると、次の問題を同時に抱えることになります。
- OS、glibc、OpenSSL、Node.js、native extensionの互換性をまとめて確認する必要がある
- 失敗したときに、アプリの問題かホストOSの問題か切り分けにくい
- 本番でRubyを入れ替えるため、切り戻しの手順も複雑になる
- 作業中に旧環境まで壊すと、比較対象と復旧先を失う
Ruby 4.0.6をインストールすることと、CentOS 7上でRuby 3.0.0から4.0.6へ安定移行することは別問題です。検証を進めた結果、後者を旧VPSの上で続けるのは難しいと判断しました。
そこで、「新VPSを借りて、旧VPSと並行して準備する」方針に切り替えました。OSの更新、Ruby/Railsの更新、デプロイ方式の変更を、新しい環境でまとめて検証できるようにしたわけです。
移行方法の判断マトリクス
| 選択肢 |
初期作業 |
旧本番への影響 |
切り戻し |
今回の判断 |
| 旧VPS上で直接Ruby/Railsを更新 |
少ない |
大きい |
難しい |
不採用 |
| 旧VPS上でDockerだけ更新 |
中程度 |
OS側の制約が残る |
中程度 |
不採用 |
| 新VPSを用意して並行検証 |
新VPSのセットアップが必要 |
旧本番を変更しない |
旧VPSを残せる |
採用 |
| DBまで同時にPostgreSQLへ移行 |
非常に大きい |
データ移行リスクが大きい |
難しい |
今回は見送り |
今回の移行では、Ruby/Railsとデプロイ基盤に集中するため、DBエンジンの変更は見送りました。MySQLからPostgreSQLへの移行は、検索SQL、照合順序、インデックス、データ型などを確認する別プロジェクトに分けています。
そもそも、普段の開発環境から大変だった
今回の更新が難しかった理由は、単にRailsのメジャーバージョンを上げるだけではありませんでした。Ruby 3.0.0を使っていたため、私の通常のmacOS環境ではRuby自体をビルドできず、実質的にDockerで動かすしかない状態でした。
macOSではRuby 3.0.0を動かせない
普段使っているMacでRuby 3.0.0を直接ビルドしようとしても、現在のmacOS、Apple Silicon、古いRuby、OpenSSL、native extensionの組み合わせでビルドできない状態でした。
Rubyだけを入れれば終わりではなく、Gemのビルド、Node.js、MySQLクライアント、アセットのビルドまでそろえる必要があります。
そのため、アプリケーションの開発・RSpec・アセットの確認はDockerで行っていました。
Mac
↓
Docker Compose
├─ Ruby 3.0.0
├─ Rails 6.1
├─ MySQL
└─ Node.js / assets
Dockerに閉じ込めることで、少なくともアプリケーションの実行環境は固定できます。一方で、Capistranoのデプロイは別の問題として残りました。
Capistranoを動かすためにMultipassを使う
旧本番へのデプロイにはCapistranoを使っていました。しかし、Capistranoを実行するためのRuby環境も、古いアプリケーションに合わせる必要があります。
そこで、Mac上にMultipassでUbuntu環境を用意し、Capistranoを実行していました。
Mac
├─ Docker : アプリ開発・RSpec・ビルド
└─ Multipass : Ubuntu上でCapistranoを実行
↓
旧VPSへデプロイ
デプロイのたびにMultipassのUbuntu環境へ入り、そこからCapistranoを実行する必要がありました。アプリケーションをDockerで動かす環境と、旧本番へデプロイする環境が分かれていたことも、日常の負担になっていました。
つまり、日常的に次の4つの環境を意識していました。
| 環境 |
用途 |
Rubyの扱い |
| Mac本体 |
エディタ、Git、ブラウザ |
Ruby 3.0.0は直接動かさない |
| Docker |
アプリ開発、テスト、アセットビルド |
Ruby 3.0.0をコンテナで実行 |
| Multipass Ubuntu |
Capistranoのデプロイ |
旧環境用のRubyとGemを実行 |
| 旧VPS |
本番 |
CentOS 7 + rbenv + Ruby 3.0.0 |
この構成は動いてはいましたが、Ruby/Railsを更新する前から、開発とデプロイの経路が複数に分かれていました。
1日で進めた時系列
| 時刻(JST) |
内容 |
変更の意味 |
| 8/22 13:05 |
Rails 7.0、Ruby 3.3.12へ更新 |
古い実行環境から最初の段階へ進む |
| 8/22 14:34頃 |
Rails 7.1、秘密情報の環境変数化 |
Railsの非推奨箇所とsecrets.ymlを整理 |
| 8/22 15:03〜15:29 |
Rails 7.2、8.0、8.1へ更新 |
enum、管理画面、PaperTrailなどを順に互換対応 |
| 8/22 16:30 |
Ruby 4.0.6、Puma 7系へ更新 |
Docker・CI・Capistranoの実行環境をそろえる |
| 8/22 20:41 |
Kamal 2の依存とVPSデプロイ設定を追加 |
新VPS用のデプロイ経路を作る |
| 8/22 夜 |
GHCR、MySQL Accessory、DB dump/restore、事前検証環境を確認 |
本番に近い環境で検証する |
| 8/23 00:07 |
事前検証環境にBasic認証を追加 |
公開前の画面を保護する |
| 8/23 00:43 |
現行本番のNginxメンテナンスを準備 |
DNS切り替え時に旧VPSを安全に止める |
| 8/23 01:31 |
一連の変更をマージ |
実装と切り替え準備が完了 |
Ruby/Rails更新で行ったこと
Railsをメジャーバージョンごとに通す
更新の順番は、次のようになっています。
Rails 6.1
↓
Rails 7.0
↓
Rails 7.1
↓
Rails 7.2
↓
Rails 8.0
↓
Rails 8.1.3.1
主な互換性対応は次のとおりです。
- Rails 7.1以降の管理画面パラメータ検証に対応
- Rails 8系のenum構文へ更新
- Rails 8の管理画面ステータス名へ対応
- Rails 8.1とPaperTrail 17の組み合わせへ更新
- Ransack、Devise、Pumaなどを互換バージョンへ更新
- Ruby 3.4以降で標準ライブラリから分離された
csv、nkf、observer、mutex_mなどを明示
- Ruby 4.0でCapistrano関連の依存を整理
- Node.jsをDockerとCIへ入れ、ExecJSの実行環境を統一
- Active Storageの画像処理に備えて
image_processingとlibvipsを追加
実行環境をDockerイメージに閉じ込める
旧環境では、Capistranoがrbenv上のRuby 3.0.0を使う構成でした。新環境では、Ruby、Rails、Gem、Node.js、画像処理ライブラリ、アセットをDockerイメージに含めます。
新環境では、Ruby 4.0.6を含むDockerイメージを作成します。
本番イメージのビルド時にアセットのコンパイルまで実行するため、デプロイ先でアセットを再生成する必要がありません。新しいコンテナが起動してヘルスチェックを通過した後に、Kamal Proxyが通信先を切り替えます。
config/secrets.ymlを削除する
Railsのconfig/secrets.ymlは削除し、秘密情報を環境変数から渡す形へ変更しました。
秘密情報はリポジトリへ実値を置かず、1PasswordのSecret Referenceをデプロイ時に環境変数として解決します。
1Password
↓ op run
環境変数
↓
Kamal / Dockerコンテナ
これにより、Gitへ秘密情報をコミットせず、開発PCからKamalを実行する場合とCIから実行する場合で同じ仕組みを使えます。
CapistranoからKamal 2へ
今回の変更は、Capistranoを一度に削除したわけではありません。旧VPSの本番運用を維持したまま、新VPS用のデプロイ経路としてKamal 2を追加しました。
デプロイ方式の比較
| 項目 |
旧本番 |
新VPS |
| デプロイツール |
Capistrano 3 |
Kamal 2.12 |
| Rubyの管理 |
ホストのrbenv |
Dockerイメージ |
| Web |
Nginx → Puma Unix socket |
kamal-proxy → Puma TCP |
| イメージ管理 |
サーバー上でbundle・アセット生成 |
GHCRへGit SHAタグでpush |
| HTTPS |
既存Nginxの設定 |
kamal-proxy + Let's Encrypt |
| メンテナンス |
Nginxの静的ページとフラグファイル |
Kamal Proxyのメンテナンスモード |
| ヘルスチェック |
既存監視 |
/up、/healthz |
| rollback |
Capistranoのrelease切り戻し |
Kamalで直前SHAのイメージへ切り戻し |
| DB |
既存MySQL |
MySQL Accessory |
旧本番をCapistranoで動かし続けるのは、切り替えまでの安全網を残すためです。新VPSへのデプロイが失敗しても旧VPSは変更されていないため、DNSを戻す判断ができます。
事前検証環境と本番環境を分ける
Kamalの宛先を省略すると本番へ接続してしまう事故を防ぐため、stagingまたはproductionの指定を必須にしました。
| 宛先 |
公開範囲 |
役割 |
staging |
非公開の検証用ホスト |
新VPSの事前検証 |
production |
touhou.arrangement-chronicle.com |
DNS切り替え後の本番 |
事前検証環境では通常ページだけBasic認証を有効にしています。/upと/healthzは、Kamal Proxyと外部監視が利用するため認証対象外です。
実環境では次の結果を確認しました。
| 確認 |
結果 |
| 認証なしで事前検証環境の通常ページ |
認証が必要になることを確認 |
| 正しいBasic認証で通常ページ |
正常に表示できることを確認 |
| 間違ったパスワード |
アクセスが拒否されることを確認 |
/up |
ヘルスチェックが応答することを確認 |
/healthz |
ヘルスチェックが応答することを確認 |
stagingのイメージをproductionへ昇格する
stagingとproductionで別々にイメージをビルドすると、「事前検証環境で確認したものと本番で動くものが違う」という事故が起きます。
そこでstagingで検証したGit SHAをそのままproductionへ昇格させます。stagingで動作確認したイメージを識別し、本番では同じイメージを再ビルドせずに利用する仕組みにしました。
新VPSのセットアップと事前検証
新VPSには、XServer VPSのUbuntu Server 26.04 LTS、4GB RAM、vCPU 4コア、NVMe SSD 150GBの構成を用意しました。
Kamalを実行する前に、次のホスト設定を行います。
- Ubuntuのセキュリティ更新
- デプロイ用ユーザーと接続設定
- 不要な管理者ログインとパスワード認証の無効化
- UFWで公開ポートを必要最小限に制限
- AppArmor、時刻同期、unattended-upgradesの確認
- Docker EngineとCompose pluginの導入
- XServerのコンソール接続による復旧経路の確認
その後、Kamalのstaging設定を確認し、新VPS上でアプリケーションと関連サービスをセットアップして、事前検証用のデプロイを行います。
MySQLはアプリケーションコンテナとは別のAccessoryとして起動し、3306番ポートは外部へ公開しません。現行本番から取得したdumpを新VPSへrestoreし、検索インデックス、データベースビュー、主要画面を確認しました。
今回、事前検証環境と将来の本番環境は同じDBを使います。ただし、事前検証環境での確認は読み取り中心に限定し、db:seed_fuやカウンター再計算のようなデータ変更タスクを動作確認目的では実行しない方針にしました。
メンテナンス画面を用意する
DNSの切り替え中に、旧VPSへアクセスする利用者と新VPSへアクセスする利用者が混在する可能性があります。そのため、旧VPS側にもメンテナンス画面を用意しました。
旧VPS側はNginxの静的ページ
旧VPSはCapistrano管理のNginxなので、新VPS用のKamalから直接操作できません。旧VPSへは、Nginxのフラグファイルでメンテナンスを切り替えます。
メンテナンスページは、Railsや通常のassets配信に依存しない自己完結したHTMLにしています。Nginx設定をroot権限で反映するときは、appユーザーが書き換えられる場所のスクリプトを直接sudo実行せず、root管理下の/usr/local/sbinへ配置してから実行します。
設定反映後は、Nginxの構文チェックに成功した場合だけreloadします。失敗時はバックアップから復元するようにしました。
新VPS側はKamal Proxyのメンテナンス
新VPSのKamal側では、Proxyがメンテナンス応答を返すモードを使います。アプリコンテナが起動していない状態でも応答できるのが利点です。
事前検証環境では、次の状態を確認済みです。
| 状態 |
通常ページ |
/up |
| メンテナンス中 |
エラー応答 |
ヘルスチェックは継続 |
| メンテナンス解除後 |
正常表示 |
正常応答 |
ヘルスチェックはメンテナンス中も応答するため、Kamal Proxyがアプリの状態を確認できます。
DNS切り替えと本番切り替え
ここは、今回用意した本番切り替え手順です。変更をマージした時点では、まだこのproductionデプロイとDNS変更は実行していません。
切り替え前
- stagingで検証したGit SHAとDocker image digestを固定する
- 旧VPSでデプロイと書き込みを止める
- 旧VPSのNginxメンテナンスをONにする
- 新VPSのMySQL、Rails、Kamal Proxy、ログローテーションを確認する
/up、/healthz、トップページ、検索、一覧、詳細、画像、管理画面を確認する
- production向けの設定を最終確認する
今回の前提では、新VPSのDBは現行本番の最新状態へ復元済みで、切り替え前の追加更新もありません。そのため、切り替え時の最終dump/restoreは行わない方針です。切り替え前にデータ更新が発生した場合は、この前提を見直します。
DNSを切り替える
新VPSにはグローバルIPv6がないため、Aレコードを新VPSへ変更すると同時に、旧VPSを向いているAAAAレコードを削除します。リンクローカルIPv6をAAAAレコードへ登録しないことも確認します。
DNSのAレコードとAAAAレコードが新VPSを向いていることを確認します。反映には時間差があるので、旧VPSはすぐに停止せず、rollback判断期間はメンテナンス状態で残します。
productionへ同じイメージを昇格する
DNS切り替え後、stagingで確認したSHAをproductionへ昇格します。その後、ヘルスチェック、トップページ、検索、一覧、詳細、画像、管理画面を順番に確認します。
productionで確認するのは、デプロイが成功したかだけではありません。検索結果、一覧のページング、詳細画面、画像表示、管理画面のログイン、Rails/Puma/Kamal Proxy/MySQLのログ、エラー応答の有無まで確認します。
rollbackの考え方
新VPSで書き込みが発生した後に旧VPSへDNSを戻すと、旧VPSのDBには新VPS側の書き込みが存在しない可能性があります。そのため、rollbackは状況ごとに分けます。
| 状況 |
取るべき対応 |
| DNS切り替え前 |
旧VPSのメンテナンスを解除する |
| DNS切り替え直後・書き込みなし |
DNSを旧VPSへ戻して正常性を確認する |
| 新VPSで書き込み発生後 |
まず新VPS上でRailsイメージを直前SHAへ戻す |
| DB障害 |
新VPSをメンテナンスにし、restoreまたは切り戻しを判断する |
アプリのrollbackとDBのrollbackは別物です。Kamalでイメージを戻せても、DB migrationを自動的に戻してよいとは限りません。スキーマ変更はexpand/contract方式で、古いコードと新しいコードが一時的に混在しても動くようにします。
直近で行ったUI変更
インフラ更新だけでなく、8月中旬から公開画面と管理画面のUIも大きく変更しました。
UI改善の一覧
| 主な変更 |
利用者にとっての変化 |
技術面 |
| 共通UIシェル、レスポンシブ、検索、一覧、詳細画面を刷新 |
PC・タブレット・スマホで探しやすくなった |
CSS/JavaScript分離、ページング、キャッシュ、N+1削減 |
| 原曲・アルバム詳細の前後ナビゲーション |
一覧順に沿って次の情報へ移動できる |
共通サービス・ヘルパーを追加、CoffeeScriptをJavaScriptへ移行 |
| テーブル内無限スクロール、固定ヘッダー、モバイル操作改善 |
長い曲一覧でも表の中だけをスクロールできる |
読み込み中・失敗・再試行・完了状態を追加 |
| 詳細検索のスペース入力、表セルのフォーカス枠を修正 |
検索とキーボード操作の違和感を減らした |
Rails 8互換修正と合わせて回帰テストを追加 |
特に変わったところ
詳細画面の前後ナビゲーション
原曲詳細画面やサークルのアルバム詳細画面で、一覧順に基づく前後リンクを表示するようにしました。
「検索結果へ戻る」「一覧を探し直す」を繰り返さず、前後の原曲・アルバムを順番に見られます。
長い曲一覧をテーブル内で追加読み込み
以前の追加読み込みはページ全体が伸びる方式でした。現在は、テーブルにスクロール領域を設け、その中で追加読み込みを行います。
- ヘッダーをスクロール領域の上端に固定
- 読み込み中、失敗、再試行、全件完了を表示
- モバイル・タブレットの表示切り替えをフローティング化
- サイドバーの開閉操作を追加
- 件数カードの列数を画面幅と件数に合わせて調整
データが多いサービスでは、単に「ページを速く表示する」だけでなく、読み終わるまでの操作を止めないことも重要です。
CoffeeScriptからJavaScriptへ
Railsのバージョンアップに合わせて、残っていたCoffeeScriptをJavaScriptへ移行し、CoffeeScript依存を削除しました。UI変更と実行環境更新を別々にせず、アセットパイプラインを整理しながら進めています。
テストと確認
今回の更新では、互換性修正だけでなくテスト基盤も強化しました。
| 確認項目 |
結果 |
| RSpec |
互換性修正後のテストを通過 |
| RuboCop |
対象ファイルを確認 |
| Brakeman |
セキュリティチェックを完了 |
| SimpleCov |
branch coverage計測を追加 |
| 事前検証環境のBasic認証 |
認証なし・正しい認証・誤った認証の挙動を確認 |
| ヘルスチェック |
/up、/healthzの応答を確認 |
| Kamalメンテナンス |
メンテナンス中・解除後の表示とヘルスチェックを確認 |
| rollback |
既知のSHAへ切り戻し、再度復元して確認 |
UI側では、デスクトップ・タブレット・モバイル幅の表示、テーブル内スクロール、固定ヘッダー、追加読み込みの成功・失敗・再試行を実ブラウザで確認しています。
1日で終えられた理由
1. 本番を直接更新しなかった
旧本番を保持したまま新VPSを作ったので、作業の途中で本番を壊すリスクを抑えられました。新VPS側で失敗しても、旧環境を比較対象として残せます。
2. Ruby/Railsを段階的に更新した
Rails 6.1から8.1へ直接ジャンプせず、各メジャーバージョンを順番に通しました。コミットもバージョン更新、互換修正、テスト、デプロイ基盤に分けています。
3. 実行環境をDockerに固定した
ホストOSへRubyを入れ直すのではなく、Ruby、Rails、Node.js、Gem、アセットをイメージにまとめました。新VPSのホストOSとアプリケーションの依存関係を分離できます。
4. stagingとproductionで同じイメージを使う設計にした
stagingで確認したGit SHAをproductionへ昇格するため、検証済みのものを本番で再ビルドしません。
5. DBの大規模変更を同時に行わなかった
既存のMySQL構成を維持し、PostgreSQL化や文字コードの変更は別課題に分離しました。今回の目的をRuby/Railsと実行基盤の更新に絞れたことが大きいです。
6. Codexと対話しながら進めた
今回の約1日という短さは、Codexの影響が非常に大きいです。Ruby/Railsの更新では、変更箇所を探し、互換性の問題を見つけ、修正し、テスト結果を確認する作業を何度も繰り返します。Codexを使うことで、この調査と修正の往復を止めずに進められました。
デプロイ基盤の移行でも、Capistranoを残す範囲、新VPSへKamal 2を導入する順番、事前検証と本番切り替えの境界を整理しながら実装できました。UI変更についても、検索・一覧・詳細画面の利用者目線と、Rails更新による回帰の両方を同時に確認できました。
正直に言えば、Codexがなければ、この範囲の変更を約1日で形にすることはできなかったと思います。ただし、提案をそのまま採用したわけではなく、変更の妥当性、テスト結果、デプロイ手順、本番切り替えの判断は自分で確認しています。
まとめ
今回のポイントは、古いVPS上でRubyとRailsを無理に更新しなかったことです。
旧環境をCapistranoで残し、新VPSをKamal 2で構築することで、アプリケーションのバージョン更新とサーバー移行を同時に進めながら、切り戻し先も確保できました。
また、直近のUI改善では、検索・一覧・詳細画面を「情報を見つける」「次へ進む」「大量データを読み続ける」という利用者の操作に合わせて整理しました。UIの回帰テストとRailsの互換性テストを同じ更新の中で強化できたのもよかったです。これらを短期間で進められた背景には、Codexと対話しながら調査・実装・検証を進められたことがあります。
変更をマージした時点では本番DNS切り替え前ですが、事前検証環境、DB restore、Basic認証、ヘルスチェック、メンテナンス、rollbackまでを準備できました。本番切り替え後は、DNSの反映時間と新VPSでの書き込みを確認しながら、旧VPSを一定期間維持する予定です。