ブログ一覧に戻る
インフラDNSCloud RunCloudflare運用

ダウンタイムなしでサブドメインを変更する 4 段階アプローチ

はじめに

app.example.comcloud.example.com に変えたい」「v2.example.comapi.example.com に統合したい」— サブドメイン変更は、製品名のブランディングが固まってきた時期によく発生する地味なタスクです。

ところがこのタスク、素朴に「DNS と Cloud Run mapping と env を全部一気に書き換える」とやると、ほぼ確実につまずきます。本記事では、niyase で実際に直面した app.niyase.comcloud.niyase.com の移行を題材に、ダウンタイム 0 で完遂するための 4 段階アプローチ をまとめます。

「単純な改名」が難しくなる 6 つの理由

サブドメイン変更を Big Bang で実行した時、典型的につまずくポイントは次のとおりです。

#つまずく箇所原因
1DNS 伝播遅延TTL 300 秒のレコードでも、世界中の resolver / クライアントキャッシュには 5〜15 分のラグが出る。新旧 IP が混在する時間が必ず存在
2SSL 証明書発行待ちCloud Run / App Engine の domain mapping は Google が Let's Encrypt 証明書を発行する。これに 5〜60 分 かかる。発行完了前にアクセスすると NET::ERR_CERT_AUTHORITY_INVALID
3環境 deploy ラグenv を更新してから Cloud Run の新 revision が rollout し終わるまで 1〜3 分。フロントは新 URL を期待しているのにバック側は旧 env のままという捻れが発生
4CORS リジェクトAPI の許可オリジン (allowlist) 更新前に、新 URL のフロントから叩くとブラウザが弾く。コンソールで Access to fetch ... has been blocked by CORS
5Firebase Auth ドメイン未許可Firebase の Authorized domains に新 URL を追加し忘れるとログインが失敗。エラーは auth/unauthorized-domain
6外部リンク・ブックマーク死亡過去にメール・Slack・ドキュメント・お客様の env に渡した旧 URL が即時 404。SEO ロスとブランド毀損

これらが 同時多発 するため、何が原因で動かないのか切り分けに丸 1 日を費やす、というのが典型パターンです。

4 段階アプローチ — A 追加 → B 切替 → C 301 → D 廃止

全体像

Phaseやること旧 URL新 URL切り戻し
A. 追加新 URL の Cloud Run mapping + DNS を 追加 (旧と並存)✅ 動作✅ 動作即時 (追加削除だけ)
B. 切替env / コード / docs を 全部新 URL に書き換え✅ 動作✅ 動作env で revert
C. 301化旧 URL を 301 redirect で新 URL に転送→ 転送✅ 動作Page Rule 削除で revert
D. 廃止2〜4 週間運用後、旧 mapping を 完全削除❌ 終了✅ 動作(時間置きなのでリスク低)

ポイントは 「全段階で旧と新の両方が動いている時間を作る」 こと。これにより、どの段階で問題が起きても 常に切り戻し可能 な状態を維持します。

Phase A: 追加 — リスクゼロのスタート

新 URL の Cloud Run domain mappingDNS CNAME を、旧と並存で追加します。

ポイント:

  • 既存の app.* は触らない。何も壊れない
  • 同じ Cloud Run service を 2 ホストにバインドする (Cloud Run は 1 service が複数ホストに mapping 可能)
  • SSL 発行に 5〜60 分かかるので、A の完了確認は curl -sI で 200 が返ってから

Phase A は 追加するだけ なので、いつでもキャンセル可能。SSL 発行を待っている間に他の作業を進められます。

Phase B: 切替 — コードと env を新 URL に

新 URL が完全に立ち上がってから、コード / env / docs を一括で新 URL に書き換えます。

切り替え対象の典型:

  • NEXT_PUBLIC_*_URL 等の 環境変数 (各 Cloud Run の env settings)
  • ハードコードされた fallback URL (process.env.URL ?? "https://app.example.com""https://app.example.com" 部分)
  • apps/api 内で env を経由していない 直書き URL (リダイレクト先、招待リンクのテンプレなど)
  • Firebase Auth Authorized domains に新 URL を追加 (旧はまだ残す)
  • CORS allowlist に新 URL を追加 (旧はまだ残す)
  • docs / README / 規約 等のドキュメンテーション

このタイミングで 旧 URL も動き続けている ことが命綱です。env の rollout 中に何か不整合が出ても、ユーザーは旧 URL で引き続き使えます。

Phase C: 301化 — 外部リンクの誘導

旧 URL を 301 Permanent Redirect で新 URL に転送する設定を入れます。

Cloudflare の場合は Page Rule で 1 行:

Match:       app.example.com/*
Action:      Forwarding URL — 301 Permanent Redirect
Destination: https://cloud.example.com/$1

これで:

  • 過去にメール / Slack / 印刷物・名刺で配った 旧 URL のリンク が新 URL に自動誘導される
  • ブックマークしていたユーザーも、意識せずに新 URL へ移行できる
  • 検索エンジンが SEO 評価を 新 URL に承継 する (301 の意味)

C を入れた瞬間から「移行完了」と外部にはアナウンスできる状態になります。

Phase D: 廃止 — 時間を置いてクリーンアップ

C で 301 を入れてから 2〜4 週間 運用して、エラーログ・サポート問い合わせを観察します。問題が出なければ:

  • 旧 URL の Cloud Run mapping を削除
  • 旧 URL の DNS レコードを削除
  • Cloudflare Page Rule (301) も削除

ここまでで、構成図上から旧 URL の痕跡が完全になくなります。

D を急がない理由は、忘れていた外部依存 (誰かが社内 Wiki に貼ってた絶対 URL とか、メールテンプレに焼き込まれていた URL とか) が後から出てくるからです。「削除は最後」が運用の鉄則 です。

実装: Cloud Run + Cloudflare の冪等スクリプト

niyase は domain mapping と DNS を 冪等な bash スクリプト で管理しています。同じスクリプトを何度実行しても結果が同じになる構造です。

setup-domain-mappings.sh (抜粋)

SERVICES=(niyase-api niyase-cloud niyase-cloud niyase-admin)
HOSTS=(
  "api${SUFFIX}.${DOMAIN}"
  "app${SUFFIX}.${DOMAIN}"        # 既存維持 (Phase D で削除)
  "cloud${SUFFIX}.${DOMAIN}"      # ← Phase A で追加
  "admin${SUFFIX}.${DOMAIN}"
)

for i in "${!SERVICES[@]}"; do
  service="${SERVICES[$i]}"
  host="${HOSTS[$i]}"
  if gcloud beta run domain-mappings describe --domain="${host}" \
       --region="${REGION}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
    echo "  (already mapped, skip)"        # ← 冪等
    continue
  fi
  gcloud beta run domain-mappings create \
    --service="${service}" --domain="${host}" \
    --region="${REGION}" --project="${PROJECT_ID}"
done

setup-cloudflare-dns.sh (抜粋)

SUBDOMAINS=("api${SUFFIX}" "app${SUFFIX}" "cloud${SUFFIX}" "admin${SUFFIX}")

for sub in "${SUBDOMAINS[@]}"; do
  EXISTING=$(curl -sS "${AUTH[@]}" \
    "${API}/zones/${ZONE_ID}/dns_records?type=CNAME&name=${sub}.${ZONE_NAME}" \
    | jq -r '.result[0].id // empty')

  PAYLOAD=$(jq -n --arg name "${sub}" --arg content "ghs.googlehosted.com" \
    '{type:"CNAME", name:$name, content:$content, ttl:300, proxied:false}')

  if [[ -n "${EXISTING}" ]]; then
    # 既存ある → PUT で update (冪等)
    curl -sS -X PUT "${AUTH[@]}" "${API}/zones/${ZONE_ID}/dns_records/${EXISTING}" -d "${PAYLOAD}"
  else
    # 無ければ POST で create
    curl -sS -X POST "${AUTH[@]}" "${API}/zones/${ZONE_ID}/dns_records" -d "${PAYLOAD}"
  fi
done

サブドメイン変更時は、配列に 1 行追加するだけ。スクリプト再実行で安全に反映されます。

チェックリスト

各 Phase で見るべきもの:

Phase A 完了の判定

  • dig CNAME cloud.example.com +shortghs.googlehosted.com
  • curl -sI https://cloud.example.com/HTTP/2 200
  • app.example.com も引き続き 200 を返す

Phase B 完了の判定

  • git grep "https://app.example.com" apps/ → ヒット 0 (env 経由化済 or 置換済)
  • 各 Cloud Run service の env が新 URL になっている
  • Firebase Auth → Authorized domains に新 URL が含まれている
  • CORS allowlist に新 URL が含まれている
  • 新 URL で onboarding 一通り通る (ログイン → スペース作成 → 招待 → サブスク登録)

Phase C 完了の判定

  • curl -sI https://app.example.com/fooHTTP/2 301 + Location: https://cloud.example.com/foo
  • 旧 URL のブックマークがブラウザで自動的に新 URL に飛ぶ

Phase D 完了の判定

  • 2〜4 週間、エラーログに旧 URL 関連のエラーが出ていない
  • サポート問い合わせに旧 URL 関連が出ていない
  • 旧 mapping / DNS / Page Rule を削除
  • docs から旧 URL の記述が消えている

まとめ — 「サブドメインを変える」の本質

サブドメイン変更は、技術的には DNS と mapping を 1 行書き換えるだけのタスクに見えます。実際には、

  • DNS / SSL / env / CORS / Auth / 外部リンク の 6 系統が連動している
  • そのどれかが時間差で更新されると、その時間差の間は 必ずユーザー影響 が出る
  • 「同時に更新する」ことは現実的に不可能 (TTL ・ SSL 発行・ rollout の時定数が全て違う)

→ 解決策は 「全段階で旧と新の両方が動いている」状態を意図的に作る ことです。これが 4 段階アプローチ (A 追加 → B 切替 → C 301 → D 廃止) の本質です。

サブドメインの改名は、それ自体は地味なタスクですが、ブランドの整理・整合性の確保・SEO 資産の継承 という観点では重要な仕事です。冪等なスクリプトと段階的な手順をテンプレ化しておけば、次回以降は 数時間で安全に完遂 できるようになります。

niyase でも app.niyase.comcloud.niyase.com の移行を、まさにこの 4 段階アプローチで実施予定です。実施後の振り返り記事も別途公開する予定なので、同じ課題に直面している方の参考になれば幸いです。