レガシーからモダンへ:エックスサーバーから Cloudflare/AWS への無停止移行プロセス

|
Migration Xserver Cloudflare AWS WordPress DNS ゼロダウンタイム

エックスサーバーから Cloudflare Pages / AWS へのWordPress・メール・SSL無停止移行の完全手順。DNS切り替え・ロールバック計画・移行後検証まで一気通貫で解説。

はじめに:「切り替え日にサービスが止まる」移行の何が問題か

レガシー環境からモダン環境への移行で最もよくある失敗は「切り替え日に止まる」だ。原因の大半は次の3つに集約される。

移行失敗の典型パターン:

  ① DNS 切り替え直後の「伝播待ち」を甘く見た
    → DNS の TTL を事前に短縮していなかった
    → 世界中のリゾルバが古い IP をキャッシュしたまま
    → 一部のユーザーには旧環境、一部には新環境が見える「中間状態」が
      最大 48 時間続いた

  ② メール送信設定を後回しにした
    → Web サーバーは新環境に移行済み
    → コンタクトフォームの送信先が旧サーバーのメール設定のまま
    → 移行後 2 日間、問い合わせメールがどこにも届いていなかった

  ③ ロールバック手順を用意していなかった
    → 新環境で予期しない問題が発生
    → 旧環境に戻そうとしたが、DNS を元に戻せば良いのか
      SSL 証明書を再発行すれば良いのか、判断できなかった
    → 障害対応中に判断を迷い、停止時間が延びた

この記事では、エックスサーバーで運用している WordPress サイト + メール + SSL を、Cloudflare Pages + AWS SES + Cloudflare DNS へ移行した実際のプロセスを解説する。「ゼロダウンタイム」の実現には技術より先に 「並走設計」という考え方が必要だ。

なぜ今この移行をするのか — 技術選定の背景

「なぜ Xserver のままではダメなのか」という問いに答えることが、移行設計の出発点だ。

技術選定の判断軸:

  旧環境(Xserver 共有サーバー)の限界:
    ・PHP/MySQL 依存 → スケールできない、コールドスタートの概念がない
    ・サーバー共有 → 他テナントの影響を受ける(ノイジーネイバー問題)
    ・デプロイが FTP/手動 → CI/CD が組みにくい
    ・監視の粒度が荒い → エラーが起きても原因追跡が難しい
    ・メール送信の信頼性が低い → DMARC 対応が不十分

  新環境(Cloudflare + AWS)の優位性:
    ・Cloudflare Pages → エッジキャッシュ、コールドスタートゼロ
    ・AWS SES → DKIM/SPF/DMARC 完全対応、バウンス管理
    ・Cloudflare DNS → DNSSEC、DDoS 軽減、ルーティング制御
    ・全構成を Terraform でコード化 → 変更履歴が git log に残る

コスト面でも、Xserver の月額料金(約1,000〜3,000円)と比較して、Cloudflare Pages 無料枠 + AWS SES の従量課金(1,000通で0.10 USD)は小〜中規模サイトであれば大幅なコスト削減になる。


Part 1:移行対象の整理と「並走設計」の考え方

移行対象の4層

移行対象の全体像:

  旧環境(エックスサーバー)      新環境
  ──────────────────────    ──────────────────────────
  WordPress(PHP + MySQL)  → 静的 HTML(Cloudflare Pages)
  メール(Xserver SMTP)    → AWS SES + Route53
  SSL(Let's Encrypt / 自動)→ Cloudflare SSL(自動)
  DNS(独自ドメイン管理)   → Cloudflare DNS

  ※ WordPress を静的化できない場合は
    Xserver → Cloudflare Tunnel 経由の AWS EC2/Fargate も選択肢

「並走設計」とは何か

移行の本質は「スイッチを切り替える」ことではなく、**「新旧両環境を同時に動かしながら、段階的にトラフィックを移す」**ことだ。

並走設計のフロー:

  フェーズ1: 新環境の構築(旧環境は触らない)
    ・新環境をゼロから構築
    ・DNS はまだ旧環境を向いている
    ・新環境の URL(pages.dev 等)で動作確認

  フェーズ2: DNS の並走準備
    ・旧環境の DNS レコードの TTL を短縮(3600s → 300s)
    ・300 秒 = TTL 伝播待ちが最大 5 分になる
    ・この状態を TTL 変更後 24 時間維持する

  フェーズ3: DNS 切り替え
    ・ネームサーバーを Cloudflare に変更
    ・既存 TTL(300s)が切れるまでの 5 分で旧→新へ移行
    ・この間は「旧環境が生きたまま」が重要

  フェーズ4: 切り替え後の確認(24 時間)
    ・新環境の死活確認
    ・メール送受信の確認
    ・旧環境は停止しない(ロールバック用に 24〜48 時間は維持)

  フェーズ5: 旧環境の停止
    ・全ユーザーが新環境に移行したことを確認後
    ・旧環境(Xserver)の契約を終了

リスク評価マトリクス

移行を計画する際、何が失敗すると何が起きるかを事前に書き出すことが重要だ。「なぜその手順が必要か」は、このリスク評価から逆算される。

リスク項目 影響度 発生確率 対応策
DNS 伝播の遅延 高(ユーザーが見えない) TTL 事前短縮(24h前)
メール不達 高(問い合わせ消失) SES 並走期間中の確認
SSL 証明書エラー 高(HTTPS 不可) Cloudflare 自動発行確認
旧 WordPress URL の404 中(SEO・UX低下) Cloudflare Redirect Rules 事前設定
ロールバック不能 最高(停止が長期化) TTL 300s維持・旧環境 48h並走

Part 2:フェーズ 1 — 新環境の構築

WordPress の静的化

WordPress のコンテンツを静的 HTML に変換する。プラグイン「Simply Static」または「WP2Static」を使う。

# Simply Static の出力ディレクトリ構成(例)
output/
├── index.html
├── about/
│   └── index.html
├── contact/
│   └── index.html
├── wp-content/
│   ├── uploads/
│   └── themes/your-theme/
│       ├── style.css
│       └── *.js
└── sitemap.xml

静的化の前に確認すべきチェックリスト:

静的化の事前確認:

  ✅ お問い合わせフォーム: 静的 HTML では PHP が動かない
     → Cloudflare Pages + Hono の API エンドポイントに置き換える
     → AWS SES でメール送信

  ✅ 会員機能・ログイン機能: 静的化不可
     → 移行スコープ外にするか、別途 SPA + API 構成に再設計

  ✅ WordPress 検索: 静的 HTML では機能しない
     → Algolia/Pagefind 等の静的サイト向け検索に置き換える

  ✅ 画像の URL: WordPress の /wp-content/uploads/ を参照している
     → Simply Static が自動的に相対パスに変換するが確認が必要

Cloudflare Pages へのデプロイ

# 静的ファイルを Cloudflare Pages にデプロイ
npx wrangler pages deploy output/ --project-name your-project

# デプロイ後の確認 URL(本番ドメインを向ける前)
# https://abc123.your-project.pages.dev

この時点では *.pages.dev の URL で新環境が動いている。旧環境には影響がない。

コンタクトフォームの置き換え(Hono + SES)

WordPress のコンタクトフォームは、静的サイト化後に動かなくなる。Hono で API エンドポイントを作り、SES でメールを送信する構成に置き換える。

// src/index.ts(Cloudflare Pages Functions として動作)
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { AwsClient } from 'aws4fetch'

type Bindings = {
  AWS_ACCESS_KEY_ID:     string
  AWS_SECRET_ACCESS_KEY: string
  ADMIN_EMAIL:           string
  FROM_EMAIL:            string
}

const app = new Hono<{ Bindings: Bindings }>()

app.use('/api/*', cors())

const contactSchema = z.object({
  name:    z.string().min(1).max(100),
  email:   z.string().email(),
  message: z.string().min(10).max(2000),
})

app.post('/api/contact', zValidator('json', contactSchema), async (c) => {
  const { name, email, message } = c.req.valid('json')

  const aws = new AwsClient({
    accessKeyId:     c.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: c.env.AWS_SECRET_ACCESS_KEY,
    region:          'ap-northeast-1',
    service:         'ses',
  })

  const payload = {
    Content: {
      Simple: {
        Subject: { Data: `【お問い合わせ】${name}様より`, Charset: 'UTF-8' },
        Body: {
          Text: {
            Data: `名前: ${name}\nメール: ${email}\n\n${message}`,
            Charset: 'UTF-8',
          },
        },
      },
    },
    Destination:      { ToAddresses: [c.env.ADMIN_EMAIL] },
    FromEmailAddress: c.env.FROM_EMAIL,
    ReplyToAddresses: [email],
  }

  const response = await aws.fetch(
    'https://email.ap-northeast-1.amazonaws.com/v2/email/outbound-emails',
    {
      method:  'POST',
      headers: { 'Content-Type': 'application/json' },
      body:    JSON.stringify(payload),
    }
  )

  if (!response.ok) {
    return c.json({ error: 'メール送信に失敗しました' }, 500)
  }

  return c.json({ ok: true })
})

export default app

Part 3:Terraform による DNS・メール設定のコード化

手動で Cloudflare や AWS のコンソールを操作すると「何をいつ変更したか」が記録に残らない。移行手順を Terraform でコード化することで、変更履歴が git log に残り、ロールバックも git revert で対応できる。

ディレクトリ構成

terraform/
├── main.tf
├── variables.tf
├── cloudflare_dns.tf     # DNS レコード
├── cloudflare_pages.tf   # Cloudflare Pages 設定
├── aws_ses.tf            # SES + DKIM/SPF/DMARC
├── aws_iam.tf            # SES 送信用 IAM
└── terraform.tfvars      # 変数(git に含めない)

Cloudflare DNS レコードの Terraform 化

# cloudflare_dns.tf

terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }
}

variable "cloudflare_zone_id" {}
variable "domain"             {}

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# A / CNAME レコード(Cloudflare Pages へのルーティング)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

# Cloudflare Pages へのカスタムドメイン接続
# apex ドメインは CNAME が使えないため Cloudflare の CNAME flattening を活用
resource "cloudflare_record" "apex" {
  zone_id = var.cloudflare_zone_id
  name    = var.domain
  type    = "CNAME"
  content = "your-project.pages.dev"
  proxied = true  # Cloudflare プロキシを通す(CDN + DDoS 保護)
  ttl     = 1     # proxied = true のとき TTL は自動(1 = Auto)

  comment = "Cloudflare Pages - apex domain"
}

resource "cloudflare_record" "www" {
  zone_id = var.cloudflare_zone_id
  name    = "www"
  type    = "CNAME"
  content = var.domain
  proxied = true
  ttl     = 1

  comment = "www → apex redirect via Cloudflare"
}

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# MX レコード(AWS SES インバウンド)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

resource "cloudflare_record" "mx" {
  zone_id  = var.cloudflare_zone_id
  name     = var.domain
  type     = "MX"
  content  = "inbound-smtp.ap-northeast-1.amazonaws.com"
  priority = 10
  proxied  = false  # MX は必ず proxied = false(プロキシ不可)
  ttl      = 300    # 移行期間中は 300s を維持

  comment = "AWS SES inbound MX"
}

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# SPF レコード
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

resource "cloudflare_record" "spf" {
  zone_id = var.cloudflare_zone_id
  name    = var.domain
  type    = "TXT"
  content = "\"v=spf1 include:amazonses.com ~all\""
  proxied = false
  ttl     = 300

  comment = "SPF for AWS SES"
}

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# DMARC レコード(段階的適用)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

variable "dmarc_policy" {
  type        = string
  default     = "none"   # none → quarantine → reject の順で強化
  description = "DMARC policy: none | quarantine | reject"
}

resource "cloudflare_record" "dmarc" {
  zone_id = var.cloudflare_zone_id
  name    = "_dmarc"
  type    = "TXT"
  content = "\"v=DMARC1; p=${var.dmarc_policy}; rua=mailto:dmarc-reports@${var.domain}; pct=100\""
  proxied = false
  ttl     = 300

  comment = "DMARC policy (current: ${var.dmarc_policy})"
}

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# URL リダイレクト(旧 WordPress URL → 新 URL)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

resource "cloudflare_ruleset" "redirect_old_urls" {
  zone_id = var.cloudflare_zone_id
  name    = "WordPress URL redirect"
  kind    = "zone"
  phase   = "http_request_dynamic_redirect"

  rules {
    action = "redirect"
    action_parameters {
      from_value {
        status_code = 301
        target_url {
          # /2024/01/article-title/ → /blog/article-title/(静的サイトの URL 構造に合わせる)
          expression = "regex_replace(http.request.uri.path, \"^/[0-9]{4}/[0-9]{2}/(.+)/$\", \"/blog/${1}/\")"
        }
        preserve_query_string = false
      }
    }
    expression  = "http.request.uri.path matches \"^/[0-9]{4}/[0-9]{2}/\""
    description = "Redirect WordPress date-based URLs to new structure"
    enabled     = true
  }
}

AWS SES + DKIM の Terraform 化

# aws_ses.tf

variable "aws_region" { default = "ap-northeast-1" }
variable "ses_from_email" {}

# SES ドメイン ID の作成
resource "aws_sesv2_email_identity" "domain" {
  email_identity = var.domain

  dkim_signing_attributes {
    next_signing_key_length = "RSA_2048_BIT"
  }
}

# DKIM レコードを Cloudflare DNS に自動追加
# SES が生成する3つの CNAME レコードを Terraform で自動登録
resource "cloudflare_record" "dkim" {
  count = 3

  zone_id = var.cloudflare_zone_id
  name    = "${aws_sesv2_email_identity.domain.dkim_signing_attributes[0].tokens[count.index]}._domainkey"
  type    = "CNAME"
  content = "${aws_sesv2_email_identity.domain.dkim_signing_attributes[0].tokens[count.index]}.dkim.amazonses.com"
  proxied = false
  ttl     = 300

  comment = "SES DKIM record ${count.index + 1}/3"

  depends_on = [aws_sesv2_email_identity.domain]
}

# MAIL FROM ドメイン(バウンス処理の信頼性向上)
resource "aws_sesv2_email_identity_mail_from_attributes" "domain" {
  email_identity         = aws_sesv2_email_identity.domain.email_identity
  mail_from_domain       = "mail.${var.domain}"
  behavior_on_mx_failure = "USE_DEFAULT_VALUE"
}

resource "cloudflare_record" "mail_from_mx" {
  zone_id  = var.cloudflare_zone_id
  name     = "mail"
  type     = "MX"
  content  = "feedback-smtp.ap-northeast-1.amazonses.com"
  priority = 10
  proxied  = false
  ttl      = 300

  comment = "SES MAIL FROM MX"
}

resource "cloudflare_record" "mail_from_spf" {
  zone_id = var.cloudflare_zone_id
  name    = "mail"
  type    = "TXT"
  content = "\"v=spf1 include:amazonses.com ~all\""
  proxied = false
  ttl     = 300

  comment = "SES MAIL FROM SPF"
}

# SNS バウンス・苦情通知(SES アカウント停止を防ぐ)
resource "aws_sns_topic" "ses_notifications" {
  name = "ses-bounce-complaint-notifications"
}

resource "aws_sesv2_configuration_set" "main" {
  configuration_set_name = "main"
}

resource "aws_sesv2_configuration_set_event_destination" "bounce" {
  configuration_set_name = aws_sesv2_configuration_set.main.configuration_set_name
  event_destination_name = "bounce-and-complaint"

  event_destination {
    sns_destination {
      topic_arn = aws_sns_topic.ses_notifications.arn
    }
    enabled = true
    matching_event_types = [
      "BOUNCE",
      "COMPLAINT",
    ]
  }
}

Terraform apply の実行順序

移行で Terraform を使う際、適用順序を間違えると DNS が壊れる。正しい順序を示す。

# ① 変数ファイルの準備(git に含めない)
cat > terraform.tfvars << 'EOF'
cloudflare_zone_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
domain             = "your-domain.com"
ses_from_email     = "noreply@your-domain.com"
dmarc_policy       = "none"   # 最初は監視モードから開始
EOF

# ② まず SES のみ apply(DNS レコードはまだ変更しない)
terraform apply -target=aws_sesv2_email_identity.domain

# SES が生成した DKIM トークンを確認
terraform output ses_dkim_tokens

# ③ DKIM レコードを DNS に追加(この時点では A/MX は変更しない)
terraform apply -target=cloudflare_record.dkim

# ④ DKIM 検証を確認(SES コンソールで "Verified" になるまで待つ)
aws sesv2 get-email-identity \
  --email-identity your-domain.com \
  --query 'DkimAttributes.Status' \
  --output text
# → SUCCESS

# ⑤ SPF + DMARC を追加
terraform apply -target=cloudflare_record.spf
terraform apply -target=cloudflare_record.dmarc

# ⑥ 全リソースを apply(A/CNAME、MX 切り替えを含む)
# ← この時点でトラフィックが新環境に切り替わる
terraform apply

Part 4:フェーズ 2 — DNS の TTL 短縮(切り替え 24 時間前)

最も重要なフェーズだ。ここを怠ると DNS 伝播に 48 時間かかる。

現在の TTL 確認

# 現在の DNS レコードと TTL を確認
dig A your-domain.com +short
# → 203.0.113.1(Xserver のIP)

dig A your-domain.com
# → ;; ANSWER SECTION:
# → your-domain.com. 3600 IN A 203.0.113.1
#                   ↑ TTL が 3600秒(1時間)= 変更後 1 時間は古いキャッシュが残る

# MX レコード(メール)の確認
dig MX your-domain.com +short
# → 10 sv12345.xserver.jp.

TTL の短縮(切り替え 24 時間前に実施)

# Xserver のドメイン管理画面、または使用中の DNS 管理画面で変更

# 変更前(Xserver 管理画面での設定値):
# A レコード: 3600 秒(1時間)
# MX レコード: 3600 秒(1時間)

# 変更後:
# A レコード: 300 秒(5分)
# MX レコード: 300 秒(5分)

# 変更後、300秒(5分)待ってから再確認
sleep 300
dig A your-domain.com
# → your-domain.com. 300 IN A 203.0.113.1
#                    ↑ TTL が 300 秒になっていることを確認

なぜ 24 時間前に実施するか:TTL を 3600 秒から 300 秒に変更しても、既存のキャッシュが期限切れになるまでは古い値(3600 秒 TTL)が使われる。最大 1 時間待てばキャッシュが更新されるが、安全マージンとして 24 時間前に実施する。

TTL を下げすぎないこと — よくある落とし穴

TTL 設定の注意点:

  × TTL = 60 秒(1分)に設定する誘惑
    → リゾルバへの DNS クエリが 60 倍に増加
    → Cloudflare の DNS レート制限に引っかかる可能性
    → 300 秒(5分)で十分: ロールバックが 5 分以内に効く

  × TTL = 0 秒に設定しようとする
    → RFC 2308 では TTL=0 は「キャッシュしない」だが
      多くのリゾルバが最低 5〜60 秒はキャッシュする
    → 実質的な効果が読めない

  ✅ 推奨: 300 秒(5分)
    → 切り替え後 5 分でほぼ全リゾルバが新 IP を学習
    → DNS クエリ量は現実的な範囲内
    → ロールバック判断猶予: 5 分

Part 5:フェーズ 3 — ネームサーバーの切り替え(本番)

切り替え前の最終チェックリスト

# ① 新環境の動作確認(pages.dev URL で確認)
curl -I https://abc123.your-project.pages.dev
# → HTTP/2 200

# ② コンタクトフォームの送信テスト(新環境の API エンドポイントで)
curl -X POST https://abc123.your-project.pages.dev/api/contact \
  -H "Content-Type: application/json" \
  -d '{"name":"テスト","email":"test@example.com","message":"テスト送信です(10文字以上)"}'
# → {"ok":true}

# ③ 送信先メールの受信確認(管理者メールボックスを確認)

# ④ SES が本番モードか確認(サンドボックスでないか)
aws sesv2 get-account --query 'ProductionAccessEnabled' --output text
# → true

# ⑤ DKIM の検証状態を確認
aws sesv2 get-email-identity \
  --email-identity your-domain.com \
  --query 'DkimAttributes' \
  --output table
# → Status: SUCCESS, SigningEnabled: true

# ⑥ 旧環境 DNS の TTL が 300 秒になっていることを再確認
dig A your-domain.com | grep -E "IN\s+A"
# → your-domain.com. 298 IN A 203.0.113.1(カウントダウン中)

# ⑦ ロールバック手順を手元に用意する(後述)

ネームサーバーの切り替え手順

Cloudflare への切り替え手順:

  1. Cloudflare ダッシュボードにドメインを追加
     → "Add a Site" でドメインを入力
     → Free プランを選択
     → Cloudflare が現在の DNS レコードを自動スキャンして取り込む

  2. スキャンされた DNS レコードを確認・修正
     A レコード:  your-domain.com → 新しいIPまたはCNAME(Cloudflare Pages)
     MX レコード: SES の MX レコードに変更
     TXT レコード: SPF、DKIM、DMARC を追加

  3. Cloudflare が指定するネームサーバーを確認
     → ns1.cloudflare.com
     → ns2.cloudflare.com(実際の値は割り当てによって異なる)

  4. ドメインレジストラの管理画面でネームサーバーを変更
     (Xserver のドメイン管理 or お名前.com、Google Domains 等)
     旧: sv12345.xserver.jp / sv12345.xserver.jp
     新: ns1.cloudflare.com / ns2.cloudflare.com

  5. 伝播確認(5〜30 分待つ)

切り替え後の即時確認コマンド

# ネームサーバーが Cloudflare に切り替わったか確認
dig NS your-domain.com +short
# → ns1.cloudflare.com.
# → ns2.cloudflare.com.

# A レコードが Cloudflare の IP を向いているか確認
dig A your-domain.com +short
# → 104.21.xxx.xxx(Cloudflare のエッジ IP)

# HTTPS の確認
curl -I https://your-domain.com
# → HTTP/2 200
# → server: cloudflare

# SSL 証明書の確認
echo | openssl s_client -connect your-domain.com:443 2>/dev/null | openssl x509 -noout -issuer -dates
# → issuer=C=US, O=Let's Encrypt または Cloudflare
# → notAfter=...(有効期限)

# メール設定(MX レコード)の確認
dig MX your-domain.com +short
# → 10 inbound-smtp.ap-northeast-1.amazonaws.com.(SES)

# DMARC レコードの確認
dig TXT _dmarc.your-domain.com +short
# → "v=DMARC1; p=none; rua=mailto:dmarc-reports@your-domain.com; pct=100"

Part 6:フェーズ 4 — 切り替え後 24 時間の監視

確認すべき項目と確認コマンド

DNS 切り替え後の 24 時間は「世界中のリゾルバが新しい IP を学習する期間」だ。この間、一部のユーザーには旧環境が見えている可能性がある。

# 複数のグローバルな DNS サーバーから名前解決されているか確認
# (Google DNS, Cloudflare DNS, OpenDNS それぞれで確認)
dig A your-domain.com @8.8.8.8 +short      # Google DNS
dig A your-domain.com @1.1.1.1 +short      # Cloudflare DNS
dig A your-domain.com @208.67.222.222 +short # OpenDNS

# 全て Cloudflare の IP(104.21.xxx.xxx / 172.67.xxx.xxx)になっていれば OK
# まだ旧 IP が残っているリゾルバには旧環境が見えている

# 海外からの名前解決の確認(無料ツール)
# https://dnschecker.org/ でグローバルの伝播状況を確認

リアルタイムトラブルシューティング — 「メールが届かない」

切り替え後に最も多い問題が「コンタクトフォームから送ったはずのメールが届かない」だ。原因ごとの調査手順を示す。

# ━━━ ステップ1: SES の送信ログを確認 ━━━

# CloudWatch Logs で SES の送信イベントを確認
aws logs filter-log-events \
  --log-group-name /aws/ses/emails \
  --start-time $(date -d '1 hour ago' +%s000) \
  --filter-pattern "BOUNCE OR COMPLAINT OR DELIVERY" \
  --output table

# ━━━ ステップ2: API エンドポイントが正しく動作しているか ━━━

# Cloudflare Workers のログを確認(Cloudflare Dashboard → Workers → Logs)
# または直接テスト
curl -v -X POST https://your-domain.com/api/contact \
  -H "Content-Type: application/json" \
  -d '{"name":"debug","email":"debug@example.com","message":"debug message here"}' \
  2>&1 | grep -E "< HTTP|{\"ok\"|error"

# ━━━ ステップ3: SES の送信制限・バウンス率を確認 ━━━

aws sesv2 get-account --output json | jq '{
  ProductionAccess: .ProductionAccessEnabled,
  SendingEnabled: .SendingEnabled,
  BounceRate: .SuppressionAttributes,
  DailyQuota: .SendQuota
}'

# ━━━ ステップ4: 特定のメールアドレスが抑制リストに入っていないか ━━━

aws sesv2 list-suppressed-destinations \
  --filter Reasons=BOUNCE \
  --output table

# 抑制リストから削除する場合
aws sesv2 delete-suppressed-destination \
  --email-address problematic@example.com

Cloudflare Analytics でのトラフィック確認

# Cloudflare Analytics で確認するポイント:
# ・リクエスト数が切り替え前と同程度か
# ・エラー率(4xx/5xx)が急増していないか
# ・オリジンエラー率が 0% 付近か

# Cloudflare API でリクエスト統計を取得
curl -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/analytics/dashboard?since=-60" \
  -H "Authorization: Bearer ${CF_API_TOKEN}" \
  -H "Content-Type: application/json" | jq '.result.totals | {requests, threats, pageviews}'

旧環境(Xserver)を「生かしたまま」にする理由

切り替え後 24〜48 時間は旧環境を停止しない理由:

  理由①: DNS 伝播が完了していないリゾルバへの対応
    → 旧 IP をキャッシュしているリゾルバからは旧環境にアクセスが来る
    → 旧環境を停止すると、そのユーザーは 503 を見ることになる

  理由②: ロールバックの選択肢を残す
    → 新環境で予期しない問題が発覚したとき
    → DNS を元に戻せば 5 分以内(TTL 300秒)で旧環境に戻せる

  理由③: メール送受信の並走確認
    → MX レコードの切り替えも DNS 伝播待ちが発生する
    → 切り替え期間中は旧環境のメールサーバーも受信できる状態にしておく

Part 7:ロールバック手順

問題が発覚したときのロールバックは 「DNS を旧 IP に戻す」 だけでよい。Cloudflare のダッシュボードで 1 分以内に操作できる。

Terraform でロールバックする場合(推奨)

# git の履歴からロールバック前の状態に戻す
git log --oneline terraform/
# → a1b2c3d feat: switch DNS to Cloudflare Pages
# → e4f5g6h feat: add DKIM and SPF records
# → i7j8k9l feat: initial Xserver DNS configuration ← ここに戻す

# 特定のコミットのファイルに戻す
git checkout i7j8k9l -- terraform/cloudflare_dns.tf

# ロールバック用の設定を apply
terraform plan   # 変更内容を確認
terraform apply  # 旧 IP に戻す

# ロールバック後の確認
dig A your-domain.com @8.8.8.8 +short
# → 203.0.113.1(旧 Xserver の IP)

手動でロールバックする場合

# ロールバック手順(Cloudflare DNS の管理画面で実施)

# 1. Cloudflare DNS の A レコードを旧 IP に変更
#    your-domain.com A 203.0.113.1(旧 Xserver の IP)

# 2. TTL が 300 秒なので、5 分で伝播する

# 3. 確認
dig A your-domain.com @8.8.8.8 +short
# → 203.0.113.1(旧 IP に戻っていること)

curl -I https://your-domain.com
# → server: Apache(Xserver のサーバーヘッダー)

# 4. 旧環境のコンタクトフォームが動作するか確認
# (旧環境の WordPress + PHP のフォームに戻る)

ロールバック後は、新環境で発覚した問題を修正してから再度切り替えを実施する。


Part 8:移行後の「後処理」チェックリスト

移行が完了したら旧環境を停止する前に確認する。

移行後の後処理:

  DNS / SSL:
  ✅ HSTS(HTTP Strict Transport Security)が有効になっているか
     curl -I https://your-domain.com | grep -i strict
     → strict-transport-security: max-age=...

  ✅ www リダイレクトが機能しているか
     curl -I http://www.your-domain.com
     → 301 Moved Permanently → https://your-domain.com

  ✅ HTTP → HTTPS リダイレクトが機能しているか
     curl -I http://your-domain.com
     → 301 → https://...

  WordPress / SEO:
  ✅ Google Search Console にサイトマップを再送信
     → https://your-domain.com/sitemap.xml

  ✅ 旧 WordPress の permalink 構造が維持されているか
     → 旧: /2024/01/article-title/
     → 新: /2024/01/article-title/(同じ URL で応答するか確認)
     → リダイレクトが必要な場合は Cloudflare の Redirect Rules で設定

  メール:
  ✅ info@your-domain.com に送信して受信確認
  ✅ 旧サーバーのメールアドレスで受信していたメールが SES 経由になっているか
  ✅ バウンス / 苦情通知の SNS が設定されているか

  セキュリティ:
  ✅ DMARC レポートを受信しているか(移行後 1 週間で確認)
     dig TXT _dmarc.your-domain.com +short
  ✅ DMARC ポリシーを none → quarantine → reject へ段階的に強化
     # terraform.tfvars の dmarc_policy を変更して apply するだけ
     dmarc_policy = "quarantine"
     terraform apply -target=cloudflare_record.dmarc

  旧環境の停止:
  ✅ Xserver のアクセスログで新しいリクエストが来ていないことを確認(48 時間後)
  ✅ Xserver の MySQL データベースをエクスポートしてバックアップを保存
  ✅ WordPress のファイル一式を tar.gz でバックアップ
  ✅ Xserver の契約を停止(または WordPress 停止のみにして契約は維持)

Conclusion:「並走設計」こそが無停止移行の本質

ゼロダウンタイム移行の核心は、技術の高度さではなく**「切り替えを一瞬で行うための準備を丁寧に積み重ねること」**だ。そしてその準備を Terraform でコード化することで、変更履歴が証跡として残り、ロールバックも git revert + terraform apply の 2 コマンドで完結する。

この移行で実現したリスク低減:

  TTL 短縮(24 時間前):
    伝播待ち 48 時間 → 5 分へ短縮

  並走期間(24〜48 時間):
    問題発覚時のロールバック所要時間: 5 分以内

  フォーム → API 化:
    PHP 依存の排除、SES による信頼性向上

  Cloudflare DNS への集約:
    SSL 自動更新・DDoS 軽減・DNS 管理の一元化

  Terraform による IaC 化:
    変更履歴が git log に残る
    ロールバックが git checkout + terraform apply で完結
    DMARC ポリシー強化が terraform.tfvars 1 行の変更で完結

移行プロジェクトにおいて「ゼロダウンタイム」は目標ではなく設計の制約条件として扱うべきだ。「止めない前提」で設計すると、TTL 操作・並走期間・ロールバック手順が自然に要件定義に入ってくる。それが、移行後に「なぜか止まった」「なぜか届かなかった」という後出しの問題を根本から排除する設計思想だ。

移行フェーズ 所要時間 リスク低減の仕組み Terraform 対応
新環境構築・テスト 数日〜1週間 本番 DNS は旧環境のまま pages.dev URL で確認
TTL 短縮 作業 5 分 + 待ち 1〜24 時間 切り替え後の伝播待ちを最大 5 分に限定 ttl = 300 に変更して apply
ネームサーバー切り替え 作業 5 分 Cloudflare に全 DNS を移管 terraform apply
切り替え後確認 24〜48 時間 旧環境は生かしたまま。問題あれば即ロールバック git revert + terraform apply
DMARC 強化 段階的(週次) none → quarantine → reject dmarc_policy 変数 1 行変更
旧環境停止 作業 5 分 全リゾルバの伝播確認後

この記事をシェア

Twitter / X LinkedIn
記事一覧に戻る
🤖
Cloud Assistant
IBM Watson powered

こんにちは!クラウドエンジニアのポートフォリオサイトへようこそ。AWS構成・副業サービス・お仕事のご相談など、何でも聞いてください 👋