本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

本記事ではホスト名を一般化したプレースホルダで表記しています。例:

  • 検証したホスト名: test.example.jp
  • Pages プロジェクト: <project>.pages.dev
  • 移行を検討していた既存ホスト名: lod.example.or.jp

数値・出力・日時は実測値をそのまま載せています。

大学・財団・自治体などの組織ドメインは、DNS(Domain Name System、ドメイン名とサーバーを対応づける仕組み)が組織側の既存プロバイダで管理されていることが多く、プロジェクト単位で「ネームサーバーを移してください」とは頼みにくいものです。

この状況で「CNAMEレコード(Canonical Name、ある名前を別の名前の別名として扱うDNSレコード)を1本追加してもらうだけで、サブドメインをCloudflare Pagesに割り当てられる」という話は、以前に公式ドキュメントを読んで整理しました(外部DNS管理の組織ドメインでも、サブドメインをCloudflare Pagesに無料で割り当てられる(CNAME一本))。ただしそれは文書の読み合わせであって、実際に設定して確かめたものではありませんでした。

今回、AWS Route 53 で管理しているドメインを使って実際に試したので、その記録です。所要時間、証明書の発行元、検証方式といった、ドキュメントには書かれていない実測値が中心になります。

先に結論

  • 手順は2つだけでした。Cloudflare側でカスタムドメインを登録し、外部のDNSにCNAMEを1本追加する
  • TXTレコードの追加は不要でした。今回の登録では、所有確認はHTTPで行われました
  • 登録から証明書の有効化まで、実測で約3分でした(1回の計測)
  • 費用は無料枠のままでした
  • 設定は正しいのに、手元の環境からは10分以上「失敗している」ように見えました。原因はDNSの負のキャッシュです

最後の項目が、実際に手を動かさないと出てこない部分でした。

前提の整理: WorkersではなくPagesであること

この構成が成立するのはPagesの場合です。Workersでは対象外になります。Workers側で動かしたい場合に、Pagesを受け口だけに使う構成を別記事で試しています(外部DNSのサブドメインを保ったままWorkersへ移る)。

Workers と Pages で、外部 DNS に置いたホスト名の扱いが分かれることを示した図。左が Workers で、ブラウザが名前を引き、外部 DNS の CNAME をたどって Cloudflare の edge に届くが、ホスト名の登録が無いためエラーになり、HTTP 530 が返って本文に 1001 のコードが入る。右が Pages で、同じ経路をたどったあとカスタムドメインとして登録済みであるため HTTP 200 で配信される。

CloudflareのWorkers と Pages の compatibility matrixに、次の行があります。

Custom domains                            Workers ✅   Pages ✅
Custom subdomains                         Workers ✅   Pages ✅
Custom domains outside Cloudflare zones   Workers ❌   Pages ✅
Non-root routes                           Workers ✅   Pages ❌

同じページの本文にも注記があります。

Unlike Pages, Workers does not support any domain whose nameservers are not managed by Cloudflare.

Workersに外部DNSのホスト名を向けた場合に何が返るかは、Cloudflareのエラーコードのドキュメントで説明されています。Error 1001の「Common causes」に、まさにこの状況が挙げられています。

An external domain that is not on using Cloudflare has a CNAME record to a domain active on Cloudflare

not on using は原文ママです。)

このとき返るHTTPステータスは530で、1XXXのコードは本文に入ります。Error 530にこう書かれています。

An HTTP error 530 is returned when Cloudflare is encountering an issue resolving the origin hostname. In this case the body of the response contains an 1XXX error code.

「1014が返る」と書かれることがありますが、Error 1014は「CNAME Cross-User Banned」で、CNAMEの元も Cloudflare 上にあり、かつ別アカウントである場合のコードです。外部DNSから向けたケースとは別のものです。

私自身、以前に別の案件で外部組織のサブドメインをWorkersに向けようとして行き詰まり、AWS CloudFrontを前段に挟む構成で回避したことがあります(外部組織管理のサブドメインを Cloudflare Workers に向ける — AWS CloudFront + WAF を前段に挟んだ記録)。その記事で書いた「サブドメインを単独のゾーンとして登録できるのはEnterpriseプランのみ」という点は、いまも変わりません。当てはまらなかったのは、そこから一般化して「Cloudflareは自分のゾーンかCloudflare for SaaSに登録したホスト名しか受け付けない」「証明書の発行にはホスト名のゾーンがCloudflare配下にあることが前提」と書いた部分です。Pagesのカスタムドメインはこれに当てはまりません。当時の対象がWorkersだったため、区別せずに書いてしまっていました。

なお、サブドメインだけをCloudflareのゾーンとして持ち込むSubdomain setupはEnterpriseプラン限定で、これは今回の話とは別の機能です。Pagesのカスタムドメインは、ゾーンを持ち込まずに済ませるための独立した仕組み、という位置づけになります。

試した構成

2つの条件で測りました。1回目に条件の不備が見つかったので、条件を変えてもう一度測っています。

親ゾーンの状態目的
1回目Cloudflareに親ゾーンがあり、その配下のサブドメインをRoute 53にNSレコード(Name Server、その名前より下をどのサーバーが答えるかを示すレコード)で委任まず動くかを見る
2回目Cloudflareにゾーンが1つも無いドメイン。Route 53のみ組織ドメインと同じ条件で確かめる

1回目の構成はこうでした。

example.jp        → ネームサーバーは Cloudflare
sub.example.jp    → NS レコードで AWS Route 53 に委任
test.sub.example.jp → ここに CNAME を置く

委任があるので、sub.example.jp 以下はRoute 53が権威サーバーになります。外部DNSの再現としては足りるように見えたのですが、後述するとおり不十分でした。

2回目は、Cloudflareのアカウントに一切登録していないドメイン(ネームサーバーはRoute 53のみ)の直下に test.example.jp を置きました。組織ドメインの状況と同じです。

割り当て先には、いずれも公開済みのPagesプロジェクトを使いました。

test.example.jp  →  <project>.pages.dev

事前にCAA(Certification Authority Authorization、証明書の発行元を制限するDNSレコード)の有無を確認しました。何も返らなければ制限はありません。

$ dig +short example.jp CAA

どちらのドメインも空でした。

手順と実測値

設定の順序を示した図。1 はサイト運営側が Cloudflare にカスタムドメインを登録し、status は initializing になる。2 は DNS 管理側が外部 DNS に CNAME を 1 本追加し、ここまでは pending のまま。3 は Cloudflare が自動で HTTP による所有確認を行い証明書を発行し、登録から約 3 分で active になる(1 回の計測)。TXT レコードの追加は不要で、検証方式は http、発行元は Google Trust Services。2 を先に行うと名前解決に失敗し、522 エラーになると公式ドキュメントに記載されている。

1. Cloudflare側でカスタムドメインを登録する

ダッシュボードからも操作できますが、記録を残しやすいのでAPIを使いました。

curl -s -X POST \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/pages/projects/$PROJECT/domains" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"test.example.jp"}'

返ってきた内容です。

{
  "result": {
    "name": "test.example.jp",
    "status": "initializing",
    "verification_data": { "status": "pending" },
    "validation_data": { "status": "initializing", "method": "http" },
    "certificate_authority": "google",
    "created_on": "2026-08-27T16:56:25.27906Z"
  },
  "success": true
}

validation_data.methodhttp になっている点が、実務上いちばん効きます。今回はDNS管理者に頼むレコードがCNAMEの1本だけで済み、検証用のTXTレコードは出てきませんでした。

証明書の発行元は google(Google Trust Services)でした。CAAレコードを設定している場合は、この発行元を許可しておく必要があります。Pagesのドキュメントには letsencrypt.orgpki.googssl.com が例示されていますが、Cloudflareが提携する認証局はcertificate authoritiesのページに一覧があり、構成は変わりえます。CAAを運用している組織では事前確認が要ります。

2. 外部のDNSにCNAMEを1本追加する

Route 53側の操作です。

aws route53 change-resource-record-sets \
  --hosted-zone-id "$ZONE_ID" \
  --change-batch '{"Changes":[{"Action":"CREATE","ResourceRecordSet":{"Name":"test.example.jp.","Type":"CNAME","TTL":60,"ResourceRecords":[{"Value":"<project>.pages.dev"}]}}]}'

順序については、Pagesの公式ドキュメントに次の記載があります。

To ensure a custom domain is added successfully, you must go through the Add a custom domain process described above. Manually adding a custom CNAME record pointing to your Cloudflare Pages site - without first associating the domain (or subdomains) in the Cloudflare Pages dashboard - will result in your domain failing to resolve at the CNAME record address, and display a 522 error.

実際、1を済ませた直後の状態は pending のままで、CNAMEを入れて初めて先に進みました。既存のサイトが動いているホスト名を切り替える場合、逆順にするとその間見えなくなるので、DNS管理者への依頼は「こちらの設定が終わってから」にする必要があります。

3. 25秒おきに状態を見る

CNAMEを追加した直後から、Cloudflare側の状態・権威サーバーの応答・HTTPSの到達性を並べて記録しました。左の時刻は、CNAMEを追加した時点を0とした経過です。

[00:00] CF状態: pending / cert: pending / verify: pending | 権威CNAME: <project>.pages.dev.
[00:25] CF状態: pending / cert: pending / verify: pending | 権威CNAME: <project>.pages.dev.
[00:50] CF状態: pending / cert: pending / verify: active  | 権威CNAME: <project>.pages.dev.
[01:15] CF状態: pending / cert: pending / verify: active  | 権威CNAME: <project>.pages.dev.
[01:40] CF状態: pending / cert: pending / verify: active  | 権威CNAME: <project>.pages.dev.
[02:05] CF状態: active  / cert: active  / verify: active  | 権威CNAME: <project>.pages.dev.

所有確認が通るまでが約1分、そこから証明書が有効になるまでが約1分でした。カスタムドメインの登録(created_on の時刻)からCNAMEを追加するまでに1分弱かかっているので、登録から数えると3分弱になります。1回だけの計測なので幅はあると思いますが、桁として「数分」であることは確かめられました。

4. 到達性と証明書を確認する

このとき手元のリゾルバはまだ古い答えを返していたので、--resolve でDNSを迂回し、CloudflareのIPアドレスに直接あてています。なぜ手元から引けなかったかは次の節で扱います。

$ curl -s --resolve "test.example.jp:443:172.66.47.64" \
    -o /dev/null -w 'HTTP %{http_code} / %{size_download} bytes / TLS verify %{ssl_verify_result}\n' \
    https://test.example.jp/
HTTP 200 / 152370 bytes / TLS verify 0

ssl_verify_result0 は証明書チェーンの検証が通ったことを示します(バイト数はサイト側の内容で変わります)。証明書そのものも確認しました。

$ echo | openssl s_client -connect 172.66.47.64:443 -servername test.example.jp 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates
subject= /CN=test.example.jp
issuer= /C=US/O=Google Trust Services/CN=WE1
notBefore=Aug 27 16:00:03 2026 GMT
notAfter=Nov 25 16:59:58 2026 GMT

対象のホスト名で証明書が発行されており、有効期間は約3か月でした。notBefore が登録時刻(16:56)より前になっているのは、認証局が発行時刻をさかのぼって記載しているためのようです。更新はCloudflare側で自動的に行われる、とPagesのドキュメントには記載されています(今回は発行直後までしか見ていません)。

手順どおりでも手元から見えなかった件

上の手順を終えた時点で、私の手元からは10分以上「失敗している」ように見えていました。

「見えない」の原因がリゾルバ側にあることを示した図。権威サーバーである Route 53 は正しい CNAME を返している。そこから 2 つの経路が分かれ、公開リゾルバ(8.8.8.8、1.1.1.1、9.9.9.9)は解決できたが、手元のリゾルバは NXDOMAIN を残り 518 秒保持しており解決できない。同じ瞬間に、同じ名前が、引く先によって違う答えを返している。

Cloudflare側は active になっていて、Route 53の権威サーバーに直接聞けばCNAMEも返ります。それなのに curl は接続できませんでした。引く先を変えて確かめると、答えが割れていました。

$ dig +short test.example.jp @8.8.8.8
<project>.pages.dev.
172.66.47.64
172.66.44.192

$ dig +short test.example.jp @1.1.1.1
<project>.pages.dev.
172.66.44.192
172.66.47.64

$ dig +short test.example.jp @9.9.9.9
<project>.pages.dev.
172.66.47.64
172.66.44.192

$ dig +short test.example.jp
(当時は何も返らなかった。負のキャッシュが切れた現在は正常に返る)

手元のリゾルバに聞くと、こうなっていました。

$ dig test.example.jp +noall +comment +authority
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 58493
;; AUTHORITY SECTION:
example.jp.  518  IN  SOA  ns-888.awsdns-47.net. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

原因は、作業を始める前に「まだ設定されていないこと」を確認しようとして一度引いていたことでした。そのとき返ってきた NXDOMAIN(そのような名前は存在しない、という答え)がキャッシュされ、レコードを作ったあとも残っていました。

この寿命はRFC 2308で決まっています。

When the authoritative server creates this record its TTL is taken from the minimum of the SOA.MINIMUM field and SOA's TTL.

つまりSOA(Start of Authority、ゾーンの管理情報をまとめたレコード)自身のTTL(Time To Live、キャッシュを保持してよい秒数)と、SOAのMINIMUMフィールドの小さいほうが使われます。

上の応答でMINIMUMは末尾の86400です。SOAレコード自身のTTLは、この出力からは読み取れません。 TTLの位置にある518は手元のリゾルバに残っている残り秒数ですし、RDATAの3番目にある900はretryフィールドで、TTLとは別のものです。元のTTLは権威サーバーに直接聞くと分かります。

$ dig +noall +answer example.jp SOA @1.1.1.1
example.jp.  900  IN  SOA  ns-888.awsdns-47.net. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

Route 53が作るSOAレコードのTTLは既定で900秒なので、min(900, 86400) = 900秒が上限になり、観測時点では518秒残っていました。

「肯定的な答えのTTLだけ短くしておけば切り替えは速い」と考えがちですが、まだ存在しない名前を先に引いてしまうと、この負のキャッシュのほうが効いてきます。

実務上の含意

負のキャッシュが効くかどうかを決めるのは、新規か切り替えかではなく、レコードを作る前にその名前を引いたかどうかです。今回のように「まだ無いこと」を先に確かめると、新規ホスト名ではNXDOMAINが残ります。既存のホスト名を切り替える場合はNXDOMAINにはなりませんが、名前はあってもそのタイプのレコードが無いときに返る応答(AレコードだけがあるところにCNAMEやAAAAを引いた場合など)も、RFC 2308では同じように負のキャッシュの対象です。

ただし、切り替えでより大きく効いてくるのは既存レコードのTTLのほうです。移行を検討していた別のホスト名では、既存のAレコードのTTLが10800秒(3時間)でした。

$ dig +noall +answer lod.example.or.jp
lod.example.or.jp.  10800  IN  A  198.51.100.10

この状態で切り替えると、最大3時間は古いサーバーを見続ける利用者が出ます。切り替えの数日前にTTLを短くしておいてもらうのが定石です。DNS管理者への依頼を1回にまとめたい気持ちは分かりますが、TTLの短縮と切り替えは分けて頼むほうが結果的に楽になります。

そして、切り替え当日に「見えない」という連絡を受けたときは、まず引く先を変えて確かめることになります。公開リゾルバに直接聞いて正しいCNAMEが返るなら、少なくとも権威サーバー側の設定は済んでいる、という切り分けができます(Cloudflare側の証明書が active かどうかは別に確認します)。

$ dig +short test.example.jp @1.1.1.1

1回目の計測にあった条件の不備と、そのやり直し

ここが今回いちばん危なかった部分です。

1回目に使ったサブドメインは、親ゾーンがCloudflareのアカウントにありました。カスタムドメインを登録したときの応答に、その親ゾーンを指す zone_tag が入っていることに気づきました。

{ "zone_tag": "<親ゾーンのゾーンID>" }

組織ドメインのケース(Cloudflareにゾーンが1つも無い)とは、この点が異なります。

名前解決そのものについては、1回目でも確かめられていたと考えています。委任があるため、その下の名前を親ゾーン側が答えることはありません。実際、公開リゾルバが返したのはRoute 53に置いたCNAMEでしたし、CNAMEを追加する前に返ってきたNXDOMAINも、AUTHORITYセクションはRoute 53のSOAでした。所有確認のHTTPリクエストが通ったのも、CNAMEを追加したあとです。

問題は、Cloudflare側が登録をどう扱ったかのほうでした。カスタムドメインの受理、検証方式の選択(method: http)、証明書の発行までを、親ゾーンがある前提で処理していた可能性が残ります。TXTが不要だったことも、数分で active になったことも、いずれもこのCloudflare側の挙動です。つまり実測値の主要部分が、そのまま「ゾーンが無い環境でも同じ」とは言えない状態でした。

そこで、Cloudflareのアカウントに一切登録していないドメイン(Route 53のみ)で、同じ手順をもう一度実行しました。登録時の応答から違いが出ます。

{
  "name": "test.example.jp",
  "status": "initializing",
  "validation_data": { "method": "http" },
  "certificate_authority": "google"
}

zone_tag のフィールドがありません。そのうえで、CNAMEを1本追加すると、同じように通りました。

[7] CF: active / active | 解決: 172.66.47.198 | HTTPS: 200
$ echo | openssl s_client -connect 172.66.47.198:443 -servername test.example.jp 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates
subject= /CN=test.example.jp
issuer= /C=US/O=Google Trust Services/CN=WE1
notBefore=Aug 27 16:56:21 2026 GMT
notAfter=Nov 25 17:56:17 2026 GMT

Cloudflareにゾーンが1つも無いドメインでも、登録・所有確認・証明書の発行・配信のすべてが同じように動きました。公式ドキュメントの記述とも一致します。

If you are deploying to a subdomain, it is not necessary for your site to be a Cloudflare zone.

なお同じ箇所の直後に、apexドメインについては別の条件が書かれています。今回の話はサブドメインに限った内容です。

To deploy your Pages project to a custom apex domain, that custom domain must be a zone on the Cloudflare account you have created your Pages project on.

まとめ

  • 外部DNSにCNAMEを1本追加するだけで、サブドメインをCloudflare Pagesに割り当てられました。無料枠のままです
  • 今回の登録では所有確認はHTTPで行われ、TXTレコードは不要でした。証明書はGoogle Trust Servicesから発行されました
  • 登録から証明書の有効化まで約3分でした(1回の計測)
  • 順序は「Cloudflare側の登録 → DNSのCNAME」です。逆順は名前解決に失敗し、公式ドキュメントによれば522エラーになります
  • 設定前に名前を引くと、NXDOMAINが負のキャッシュとして残ります。手元から見えないときは、公開リゾルバに直接聞いて切り分けます
  • 既存ホスト名の切り替えでは、現行レコードのTTLがそのまま遅れになります。TTLの短縮は別の依頼として先に済ませておくのが安全です
  • この構成が使えるのはPagesの場合です。Workersは対象外です
  • 検証環境を作るときは、親ゾーンがCloudflareのアカウントにあるかどうかを先に確認してください。委任していても、Cloudflare側の処理は分岐しうるので条件が揃いません

参考