タグ: 環境変数

  • 設定を足しただけでは、変わらなかった

    設定を足しただけでは、変わらなかった

    問い合わせフォームの送信先を設定して、保存しました。画面を開き直しても、フォームは「準備中」のままでした。

    設定は間違っていません。読まれるタイミングが違っただけでした。

    この記事で扱うこと

    問い合わせの受け取りを外部サービスに任せるまでの判断と、その設定でつまずいた3点です。環境変数がいつ読まれるか、保存したら見られなくなる値のこと、そして「正しく弾けているか」の確かめ方を書きます。

    なぜ自分でメールを送らないのか

    このサイトには、メールを送る手段がありません。足すこと自体はできますが、そこから先が長い。

    迷惑メール扱いされないためには、そのドメインから送ってよいという宣言をDNSに置く必要があります。SPFとDKIMと呼ばれるものです。

    SPFは1ドメインに1本しか置けない

    すでに何かの設定が入っている状態で足すと、書き方を誤った瞬間にメールが届かなくなります。

    しかも壊れたことは、こちらからは分かりません。誰かに「返信が来ない」と言われるまで、正常に見え続けます。

    問い合わせを1本受け取るために、この重さを引き受けるのは割に合わないと判断しました。受け取りだけ外に出すことにします。

    求めているのは、転送先1つだけ

    ボット対策も、エラーの出し分けも、入力の確認も、すべてサイト側に置いてあります。外のサービスに任せているのは「受け取って、メールで転送する」という一点です。

    この形にしておくと、乗り換えの費用が小さくなります。別のサービスに移すときに変えるのは送信先のURL1つで、コードには触りません。

    最初から小さく繋いでおく

    外部サービスの機能を使い込むほど、そのサービスの形に合わせた作りになります。便利ですが、離れにくくもなります。

    「何を任せないか」を先に決めておくほうが、あとから選び直せます。

    つまずき1:環境変数は、ビルドのときに読まれる

    送信先のURLは、コードに書かずに環境変数として置いています。保存して、サイトを開き直しました。変わりません。

    思っていたこと 実際
    設定を保存すれば、次のアクセスから反映される 作り直すまで、前の状態のまま

    このサイトはページを先に作ってしまう構成なので、環境変数を読むのはページを作るときです。あとから設定を足しても、すでに作られたページは古い状態を抱えたままになります。

    配信サービスの画面にも「変更を反映するには新しいデプロイが必要です」と出ていました。読んでいませんでした。保存した時点で終わったつもりになっていたからです。

    つまずき2:保存すると、見られなくなる値がある

    環境変数を保存するとき、種別を2つから選べました。

    種別 あとから値を見られるか
    設定値 見られる
    秘密 見られない

    今回は「秘密」を選びました。訪問者に配るファイルへ混ざってほしくない値だからです。

    ただしこれは、自分でも二度と確認できないという意味でもあります。今回は発行元の画面でいつでも見られるURLなので困りませんが、そうでない値なら、保存する前に控える必要がありました。

    つまずき3:相手側の防御が、こちらの正常な送信を弾く

    受け取り側のサービスにも、独自のボット対策が用意されています。画像の文字を読ませるあれです。

    ブラウザ前提の仕組みは、サーバー間では通らない

    この対策は「人がブラウザで操作している」ことを前提にしています。

    今回はサイトのサーバーから相手のサーバーへ送る形なので、有効になっているとこちらの正常な送信まで弾かれます。

    今回は既定で無効になっていたので何もせずに済みましたが、有効な状態で繋いでいたら「設定は合っているのに届かない」で長く悩むところでした。二重に掛けた対策が、互いを打ち消すことがあります。

    正しく弾けているかは、画面では分からない

    繋いだあと、4つ試しました。

    送ったもの 期待 結果
    普通に書いて送る 通る 通った
    開いた直後に送る 断る 断られた
    アドレスの形が違う 断る 断られた
    見えない欄を埋めて送る 捨てる 「送信しました」と出た

    最後のものは、わざとそうしています。機械に「その欄を避ければ通る」と分からせないためです。

    そのぶん、画面を見ているだけでは、弾けたのか通ったのかが区別できません。確認するには受信箱を見て、届いていないことを確かめる必要があります。実際に届いていたのは4件のうち1件だけでした。

    「何も起きないこと」を確かめる手順を用意しておく

    正しく動いた証拠が「画面に出ない」「メールが来ない」という形をしている場合、確認の手順を別に決めておかないと、確認そのものを忘れます。

    今回の学び

    まず、設定を保存した時点では、まだ何も変わっていないことがあるということ。読まれるのがページを作るときなら、作り直すまでは古いままです。保存できたことと、効いていることは別でした。

    次に、隠すと、自分からも見えなくなること。訪問者に見せたくない値を隠すのは正しい判断ですが、その値を次に必要とするのは自分かもしれません。保存する前に控えるかどうかを決めておく必要がありました。

    そして、対策は二重に掛けると打ち消し合うことがあること。自分の側で弾いているのに相手の側でも弾いていると、通したいものが通りません。どこで何を見ているかを1回並べて書き出すのが、結局早い道でした。

  • 動かしていなかったから、漏れていなかっただけ

    動かしていなかったから、漏れていなかっただけ

    問い合わせフォームにボット対策を入れようとして、先に別のことが分かりました。

    いまの作りでは、ブラウザ側にどんな対策を入れても効きません。

    この記事で扱うこと

    送信先のURLが配信されるJavaScriptに入る作りになっていた話と、その直し方です。あわせて、画像の文字を読ませる方式を選ばなかった理由も書きます。

    先頭の6文字が、置き場所を決めていた

    このサイトはNext.jsで作っています。環境変数の名前には、決まりがあります。

    名前 どこから見えるか
    CONTACT_ENDPOINT サーバーだけ
    NEXT_PUBLIC_CONTACT_ENDPOINT ブラウザにも配られる

    名前の先頭に NEXT_PUBLIC_ が付いていると、その値は組み立てのときにJavaScriptの中へ埋め込まれます。訪問者に配られるファイルの一部になる、ということです。

    フォームはブラウザの中で動く部品なので、送信先を知る必要がありました。だから素直に NEXT_PUBLIC_ を付けていました。付けなければ読めないので、それ以外に選びようが無いように見えていました。

    鍵の掛かっていない扉が、隣に並ぶ

    この形だと、送信先のURLはフォームを開かなくても分かります。配られたファイルを開けば書いてあります。

    フォームを通さずに送れる

    見えない入力欄を足しても、送信までの時間を測っても、それらはフォームを使った場合にだけ効きます。

    送信先を直接叩く相手には、そもそも通り道が違うので関係がありません。鍵を掛けた扉の横に、鍵の掛かっていない扉が並んでいる状態でした。

    動かしていなかったから、漏れていなかった

    ここで確認したところ、本番の環境変数は設定されていませんでした。

    フォームは、送信先が無いときは「お問い合わせ先は準備中です」と出す作りにしてあります。つまり公開されてからずっと、フォームは表示されておらず、URLもどこにも出ていませんでした。

    安全だったのではなく、使っていなかっただけ

    結果として何も漏れていませんが、設定を入れた瞬間に漏れる形のまま置いてありました。

    この種の問題は「まだ起きていない」と「起きない」の区別が付きにくく、動かし始めた日にまとめて効いてきます。

    ついでに言うと、フォームが出ていなかったこと自体も問題でした。問い合わせ先として案内していた場所が、ずっと準備中だったということになります。

    自分のサイトに窓口を作る

    直し方は、間に1枚挟むことでした。

    前 後
    送信先 ブラウザ → 外部サービス ブラウザ → 自分のサイト → 外部サービス
    URLの在り処 配られるJavaScript サーバーだけが持つ
    判定の場所 ブラウザ(迂回できる) サーバー(迂回できない)

    ブラウザから見えるのは /api/contact だけになります。ここを直接叩くことはできますが、叩いた先で何を見ているかは分かりません。

    画像の文字を読ませる方式は、入れなかった

    ボット対策というと、ゆがんだ文字や、信号機の写真を選ばせるものが思い浮かびます。今回は入れていません。

    読みにくい文字は、機械より先に人が脱落する

    問い合わせは、そもそも件数が多くありません。1日に何百件も来るものではない。

    それに対して、1件取りこぼす損のほうが大きいと考えました。迷惑な送信を100件止める価値より、相談しようとした人が1人あきらめる損のほうが重い、という判断です。

    人がやらないことを、3つ見る

    代わりに置いたのは、訪問者の手間が一切増えない3つです。

    見ること 人がやらない理由
    見えない欄が埋まっている 画面に出ていないので、触りようがない
    開いてすぐ送っている 3秒で文章は書けない
    短時間に何度も送っている 同じ相談を10分に4回はしない

    どれも、正しく使っている人には何も起きません。止めるために何かをさせるのではなく、やっていないことを確認する形にしています。

    見えない欄の名前を、慎重に決めた

    ひとつだけ、注意が必要な部分があります。見えない欄の名前です。

    この欄を name や url のような一般的な名前にすると、ブラウザやパスワード管理ソフトの自動入力が、画面に出ていなくても勝手に埋めることがあります。

    人の送信が、黙って捨てられる

    この欄が埋まっている送信は、機械とみなして捨てます。しかも成功したように見せて捨てます。理由を返すと、次からその欄を避ければ通ると分かってしまうからです。

    つまり自動入力に埋められた人は、送信したつもりで、何も届いていない状態になります。これがこの仕組みの一番の失敗です。

    自動入力が狙わない名前にしたうえで、キーボードの移動先からも読み上げの対象からも外しています。

    回数の数え方には、限界がある

    正直に書いておくと、短時間の回数を数える部分は完全ではありません。

    このサイトのサーバー側の処理は、必要なときに起動して、しばらくすると消えます。数えた記録は共有されず、消えるときに一緒に無くなります。厳密には数えられません。

    それでも、続けて叩いてくる相手は同じ実体に当たりやすいので、一番困る「短時間に何百件」は止まります。正確さが要るようになったら、外に記録を置くことになります。いまの流量でそこまで持つ必要は無いと判断しました。

    今回の学び

    まず、名前の先頭6文字が、置き場所を決めていたということ。NEXT_PUBLIC_ は便利な省略ではなく、「これは配る」という宣言でした。付けなければ読めないから付ける、では順番が逆でした。

    次に、ブラウザ側の判定は、通り道が1本のときにだけ効くこと。もう1本、鍵の掛からない道が並んでいれば、そちらが使われます。対策を足す前に、道が何本あるかを数える必要がありました。

    そして、止めるために手間を増やさないこと。問い合わせのように件数が少ないものでは、止めた数より取りこぼした数のほうが重くなります。人がやらないことを確認するだけなら、正しく使う人には何も起きませんでした。

  • ローカルのサイトを本番公開するまでにハマった話

    ローカルのサイトを本番公開するまでにハマった話

    ローカルのパソコンで作ったWebサイトを、世界中から見られる状態にする——いわゆる「本番公開」の作業をしました。手順書に沿えば1時間で終わるはずが、実際には丸一日かかっています。今回はその全過程を、途中で出てくる用語をひとつずつ説明しながら記録します。

    想定している読者は、HTML・CSS・JavaScriptは書けるけれど、サーバーやDNSやSSLはこれから、という方です。私自身がその立場で、今回いくつも初めてのことに出会いました。

    そもそも何をしようとしているのか

    今回のサイトはヘッドレス構成というものです。まずここから説明します。

    従来のWordPressサイトは、記事の管理も、見た目の表示も、ぜんぶWordPressがやります。訪問者のブラウザはWordPressに直接アクセスし、WordPressがHTMLを組み立てて返す。1つのソフトが全部を担当する形です。

    ヘッドレス構成では、この役割を2つに分けます。

    WordPress … 記事を書く・保存する(管理画面とデータベース)
    Next.js   … 見た目を作る・訪問者に表示する

    「ヘッドレス」は「頭がない」という意味で、この場合の”頭”は表示部分を指します。WordPressから表示機能を取り除き、データを保管して受け渡すことに専念させる、というイメージです。

    2つに分けると、置き場所も2つ必要になります。

    WordPress → お名前.comのレンタルサーバー(cms.kisaku.site)
    Next.js   → Vercel(kisaku.site)

    Vercelは、Next.jsで作ったサイトを公開するためのサービスです。GitHubにコードを置いておくと、それを自動で取ってきて、ビルドして、公開してくれます。

    ビルドという言葉も説明しておきます。開発中に書いているコードは人間が読みやすい形になっていて、そのままではブラウザで動きません。これを実際に動く形に変換する作業がビルドです。Next.jsの場合は、この段階でWordPressからデータを取ってきて、ページの形に組み立てるところまで行います。

    前回までの記事で、WordPress側の移行は終わっています。今回はNext.js側をVercelに載せ、最後に kisaku.site というドメインを新しいサイトに向けるところまでが目標です。

    デプロイ前に見つけた不具合

    Vercelに繋ぐ前に、設定ファイルを確認していて問題を見つけました。next.config.mjs の画像に関する設定です。

    {
      protocol: "https",
      hostname: "cms.kisaku.site",
      pathname: "/wp-content/uploads/**",   // ← ここが間違っていた
    }

    これが何の設定かというと、Next.jsの画像表示機能が使う「許可リスト」です。

    Next.jsには Image というコンポーネントがあり、画像を自動で最適化して表示してくれます。ただし、どこの画像でも無条件に扱うわけではありません。あらかじめ許可したホスト名とパスの画像しか受け付けない仕組みになっています。悪意のあるサイトの画像を勝手に処理させられるのを防ぐためです。

    ところが前回の移行作業で、このサイトのWordPressは /wp/ というフォルダの中にあることが判明していました。画像の実際のURLはこうなります。

    https://cms.kisaku.site/wp/wp-content/uploads/2026/07/image.png
                            ~~~~ ← これが入る

    許可リストのほうには /wp が入っていないので、一致しません。このままだとブログ記事の画像が全部表示されないことになります。

    直すのは1行です。

    pathname: "/wp/wp-content/uploads/**",

    この間違いは、私が以前「WordPressはルートフォルダにある」と誤解していた頃の設定が残っていたものでした。ひとつの誤解が、気付かないうちに別のファイルにも影響していたわけです。誤解が判明した時点で「この前提を使っている場所は他にないか」を探すべきでした。

    失敗1: 自動生成されるファイルをGitに入れていなかった

    最初のデプロイは20秒で失敗しました。原因はGraphQLの型定義ファイルがリポジトリに入っていなかったことです。

    順に説明します。GraphQLは、必要なデータを指定して取り出すための問い合わせ言語です。「記事のタイトルと本文と日付だけください」というように、欲しい項目を書いて送ると、その形で返ってきます。

    このプロジェクトでは GraphQL Codegen というツールを使っています。書いたGraphQLの問い合わせを読み取って、TypeScriptの型定義を自動生成してくれるものです。型定義があると、エディタが「このデータにはtitleという項目がありますよ」と教えてくれたり、存在しない項目を書いたときに警告してくれたりします。

    これは npm run codegen というコマンドで生成されます。自動生成されるものなので、私は .gitignore(Gitで管理しないファイルを指定するリスト)に入れていました。

    ところがVercelは、ビルド時に npm run build しか実行しません。codegenは走らないので、型定義ファイルが存在しないままビルドに入り、そこで落ちるという流れでした。

    対処は2つ考えられます。

    案A: ビルドコマンドを "npm run codegen && npm run build" に変える
    案B: 生成されたファイルをGitにコミットしてしまう

    一般論では案Aのほうがきれいです。自動生成物をGitに入れると、差分が読みにくくなり、複数人で開発するときに衝突の原因にもなります。

    それでも案Bを選びました。理由は、codegenがWordPressのGraphQLに接続してデータの構造を取りに行くからです。案Aにすると、ビルドが成功するかどうかが本番のWordPressが生きているかどうかに左右されます。

    案A: WordPressが落ちている → codegen失敗 → ビルド失敗 → デプロイできない
    案B: WordPressが落ちている → ビルドは成功する(表示するデータは無いが)

    ビルドは、なるべく外部のサービスに依存しないほうが壊れにくい。 そしてこの判断は、直後に正しかったと証明されます。まさにWordPressへの接続が失敗し続ける事態に陥り、案Aを選んでいたら原因調査のためのデプロイすらできなかったからです。

    教科書的な正解と、その場の正解は一致しないことがあります。大事なのはどちらを選ぶかより、選んだ理由を説明できることだと思っています。

    失敗2: ビルドは成功。でもデータが1件も出ない

    2回目のデプロイは43秒で成功し、サイトも表示されました。ところが中身を見ると、こうなっています。

    実績       : まだ実績が登録されていません。
    スキル     : まだスキルが登録されていません。
    ブログ     : まだ記事がありません。
    プロフィール: (WordPress管理画面で作成すると、ここに表示されます)

    WordPress側には投稿7件・実績4件・スキル14件・画像10件が入っています。それが1件も出てきません。

    そして厄介なのが、ビルドが「成功」と表示されていることでした。データ取得に失敗しているのに、エラーとして表面化していません。理由はコードにありました。

    const [profileResult, projectsResult, skillsResult, postsResult] =
      await Promise.allSettled([
        getProfilePage(), getProjects(), getSkills(), getPosts(),
      ]);
    
    const projects =
      projectsResult.status === "fulfilled" ? projectsResult.value : [];

    Promise.allSettled を説明します。JavaScriptで複数の非同期処理(時間のかかる処理)をまとめて実行する方法は主に2つあります。

    Promise.all        … 1つでも失敗したら、その時点で全体が失敗になる
    Promise.allSettled … 全部の結果を待って、成功・失敗を個別に返す(例外を投げない)

    allSettled を選んだのには理由がありました。たとえば「プロフィールページをまだ作っていない」という場合でも、それ以外の部分は表示されてほしいからです。1箇所の欠損でページ全体が真っ白になるのは避けたい。設計としては妥当な判断です。

    ただ今回は、4つ全部が失敗しました。それでも設計どおりに空の配列へ置き換えられ、「まだ登録されていません」という平和な画面が出てしまいます。

    想定していた「一部が欠ける」ではなく「全部が落ちる」が起きたとき、この防御は問題を隠す方向に働きました。

    ここから学べることを一般化すると、こうなります。「壊さない」と「気付ける」は別の要求であり、両立できる。 次の2行があれば、調査は最初の5分で終わっていました。

    if (projectsResult.status === "rejected") {
      console.error("[getProjects] failed:", projectsResult.reason);
    }

    エラーを飲み込む設計自体が悪いのではありません。飲み込んだ痕跡を残さなかったことが問題でした。

    失敗3: 犯人を推測して、2回外した

    接続先の設定を疑いました。GraphQLの接続先はこう書かれています。

    const endpoint =
      process.env.NEXT_PUBLIC_WORDPRESS_API_URL ?? "http://localhost:8080/graphql";

    process.env.○○ は環境変数を読み取る書き方です。環境変数は、コードの外側から値を渡す仕組みで、「開発中はローカルのWordPress、本番は本番のWordPress」のように環境ごとに変わる値を扱うのに使います。

    ?? はnull合体演算子といって、「左側が空だったら右側を使う」という意味です。つまりこのコードは、環境変数が読めなければ黙ってローカル用のURLにフォールバック(代替)するという動きをします。Vercel上にローカルのWordPressは存在しないので、全部失敗する——筋は通っています。

    Vercelの管理画面を見ると、環境変数が Sensitive(機密)という設定で登録されていました。これは値を秘匿するための設定で、登録後は管理画面からも中身が見えなくなります。

    ここで違和感を持ちました。この環境変数には NEXT_PUBLIC_ という接頭辞が付いています。Next.jsではこの接頭辞に特別な意味があり、付いた値はビルド時にコードへ直接埋め込まれます。

    【ビルド前】process.env.NEXT_PUBLIC_WORDPRESS_API_URL
    【ビルド後】"https://cms.kisaku.site/wp/graphql"   ← 値そのものに置き換わる

    埋め込まれたコードはブラウザに配信されるので、この値は原理的に秘密にできません。だから「公開してよい値にだけ NEXT_PUBLIC_ を付ける」というルールがあり、パスワードやAPIキーに付けてはいけないのもこのためです。

    公開前提の値をSensitiveにするのは矛盾している——そう考えて、いったん削除し、Sensitiveなしで登録し直し、再デプロイしました。

    結果は変化なし。 データは相変わらず空のままでした。

    この時点で、私は原因を2回外しています。しかも困ったことに、Sensitiveだと私自身も値を確認できないので、「URLが間違っているのか」「値が届いていないのか」の区別すらついていませんでした。

    観測できない対象を推測で潰しにいっても、当たったか外れたかが分からない。 やるべきだったのは、値を推測することではなく、実際に何が起きているかを表示させることでした。

    決め手: エラーを表示させるためだけのコードを書く

    方針を変えました。エラーが握りつぶされているなら、握りつぶさずにそのまま表示する経路を一時的に作ればいい、という発想です。

    診断専用のURLを1つ追加しました。アクセスすると、次の4つをまとめて返します。

    1. ビルド時に埋め込まれた環境変数の実際の値
    2. GraphQLクライアントが使っているURL
    3. 素のfetchで接続した結果(エラーの中身まで)
    4. アプリと同じ経路で実行した結果

    コードの要点はここです。

    try {
      const r = await fetch(endpoint, { method: "POST", /* ... */ });
      result.rawStatus = r.status;
    } catch (e) {
      result.rawError = {
        name: e.name,
        message: e.message,
        cause: String(e.cause),   // ← これが決定的だった
      };
    }

    e.cause を取っているのが重要な点です。Node.js(サーバー側でJavaScriptを動かす仕組み)の fetch は、通信の失敗をすべて同じメッセージに丸めてしまいます。

    TypeError: fetch failed

    名前解決に失敗しても、接続を拒否されても、証明書が一致しなくても、タイムアウトしても、これだけです。本当の原因は cause というプロパティの中に入っています。

    これはエラーのラッピング(包装)と呼ばれる設計です。低レベルの細かいエラーを、上位の統一されたエラーで包む。使う側が実装の違いを気にしなくて済む利点がありますが、原因を調べるときは包みを開ける必要があります。

    デプロイして、この診断URLを叩いた結果がこれでした。

    {
      "envRaw": "https://cms.kisaku.site/wp/graphql",
      "clientEndpoint": "https://cms.kisaku.site/wp/graphql",
      "rawError": {
        "message": "fetch failed",
        "cause": "Error [ERR_TLS_CERT_ALTNAME_INVALID]:
          Hostname/IP does not match certificate's altnames:
          Host: cms.kisaku.site is not in the cert's altnames:
          DNS:*.gmoserver.jp, DNS:gmoserver.jp"
      }
    }

    SSL証明書の不一致でした。 そして環境変数は正しく届いていたことも同時に分かりました。私が疑って作り直したSensitiveの件は、完全に的外れだったわけです。

    なぜブラウザでは繋がり、サーバーからは繋がらないのか

    この問題が見つけにくかった理由を説明します。ブラウザからは正常にアクセスできていたのです。鍵マークも付き、GraphQLもデータを返していました。

    理解するには、HTTPS通信の最初の手順を知る必要があります。

    HTTPSの「S」はSecure(安全)の意味で、通信を暗号化します。ただしその前に、相手が本物かどうかを確認する手続きがあります。これをTLSハンドシェイク(TLSは暗号化の規格名、ハンドシェイクは握手)といいます。

    1. こちらが「cms.kisaku.site と話したい」と伝える
    2. サーバーが、そのホスト名に対応する証明書を返す
    3. こちらが「証明書に書かれた名前」と「アクセス先の名前」が一致するか確認する
    4. 一致すれば暗号化通信を開始、しなければ接続を中断

    証明書は、身分証明書のようなものです。「このサーバーは確かに cms.kisaku.site です」と第三者機関が保証したデータで、そこには対象のホスト名が書かれています。

    今回失敗したのは3の段階でした。返ってきた証明書には *.gmoserver.jp としか書かれておらず、cms.kisaku.site が含まれていない。だからNode.jsは「名乗っている相手が違う」と判断して接続を打ち切りました。

    ここで重要なのは、この確認はデータを送る前に行われるということです。実際、Vercelのログには「外部への通信が1本もない」と表示されていました。リクエストを送って失敗したのではなく、送る前の握手で決裂していたのです。

    そしてヘッドレス構成では、この違いが致命的になります。

    従来のWordPress: ブラウザ ──────────────→ WordPress
    今回の構成      : ブラウザ → Next.js(Vercel) → WordPress
                                    ↑
                        ここの通信は、ブラウザからは見えない

    従来の構成なら、ブラウザで確認すれば十分でした。しかしヘッドレスではサーバーがサーバーを呼ぶ経路が主役です。私はずっとブラウザから確認して「サーバー側は正常だ」と判断していましたが、その経路は実際に使われる経路ではなかったのです。

    「自分の環境から動く」は「どこからでも動く」ではない。 検証は、実際に通信する経路で行わないと意味がありません。

    証明書はその後サーバー側で正しく適用され、コードを一切変えずにデータが表示されるようになりました。診断用のURLは役目を終えたので削除しています。

    危なかった話: DNSの設定画面を間違えるところだった

    サイトが動いたので、最後の工程です。kisaku.site というドメインを、新しいサイトに向けます。

    DNSを説明します。インターネット上のサーバーは、本来 216.198.79.1 のような数字(IPアドレス)で識別されます。しかし人間が覚えるのは大変なので、kisaku.site のような名前と数字を対応づける仕組みがあり、これがDNS(ドメインネームシステム)です。電話帳のようなものだと考えてください。

    Vercelにドメインを追加すると、設定すべき内容が表示されます。

    kisaku.site       A      216.198.79.1
    www.kisaku.site   CNAME  e2e46a9cb91ef14a.vercel-dns-017.com

    Aレコードは「この名前は、このIPアドレスです」という対応づけ。CNAMEレコードは「この名前は、別の名前の別名です」という指定です。

    さて、これをどこで設定するか。お名前.comの管理画面には「DNSレコード設定」という、いかにもそれらしいメニューがありました。開いてみると、入力欄が並んだ画面が出てきます。

    ところが、その画面の既存レコード一覧が空でした。いま動いているはずの設定が、1件も表示されません。

    ここで手を止めて、確認しました。

    kisaku.site のネームサーバー: dns01.gmoserver.jp / dns02.gmoserver.jp

    ネームサーバーは、そのドメインの答えを実際に持っているサーバーのことです。DNSには2つの階層があります。

    【上位】ネームサーバー … 「このドメインの答えは、どのサーバーが持っているか」
    【下位】レコード       … 「そのサーバーが持っている、実際の答え」

    つまり答えを持っているのはレンタルサーバー側で、私が開いていたのはお名前.comが提供する別のDNSサービスの画面でした。名前がそっくりなので紛らわしいのですが、まったく別物です。

    そして画面の下には「レコードの登録とあわせてネームサーバーも変更する」というチェックボックスがありました。もしこれを有効にして保存していたら、上位の階層が書き換わります。

    変更前: kisaku.site → gmoserverのDNS(3件のレコードが登録済み)
    変更後: kisaku.site → 別サービスのDNS(レコードは空)

    参照先が空のほうに切り替わるので、ぶら下がるすべての名前が一斉に消えます。

    kisaku.site      → 消える
    www.kisaku.site  → 消える
    cms.kisaku.site  → 消える  ← WordPressごと到達不能に

    cms が落ちればGraphQLも止まるので、新サイトもデータを取得できなくなります。「cms のレコードには絶対に触らない」と決めて作業していたのに、1行も触らないまま巻き添えにするところでした。

    「レコードを触らなければ安全」は成り立ちません。 上位を切り替えれば下位は丸ごと入れ替わります。作業前には、個々の行を見る前に「いま自分はどのゾーンを編集しようとしているのか」を確認する必要があります。

    気付けたのは、一覧が空だったことに違和感を持ったからでした。「あるはずのものが無い」は、たいてい自分が別の場所を見ている合図です。

    正しい場所で切り替える

    本来の作業場所は、レンタルサーバーのコントロールパネルでした。開くと、期待どおりのレコードが並んでいます。

    (なし) 標準 A     ← ルートドメイン(kisaku.site そのもの)
    www     標準 A
    ftp / pop / smtp / imap  標準 A   ← メール関連
    (なし) MX 10              ← メールの宛先
    (なし) TXT               ← 送信元の正当性を示す設定

    ルートドメインの行を編集すると「別サーバーを利用する」という選択肢があり、IPアドレスを指定できました。ここにVercelの 216.198.79.1 を入れます。

    画面には注意書きもありました。「ドメインのホスト名なしはAレコードのみ設定できます」。

    これはDNSの仕様上の制約です。理由を説明します。CNAMEは「この名前は別名です」という宣言なので、そのホスト名に他のレコードを共存させられません。ところがルートドメイン(www などが付かない、ドメインそのもの)には、DNSの仕組み上どうしても必要なレコードがあります。

    NS  … このドメインを管理するネームサーバー(必須)
    SOA … ゾーンの管理情報(必須)
    MX  … メールの宛先(メールを使うなら必要)

    これらと共存できないため、ルートドメインにCNAMEは置けないのです。VercelがルートにはAレコード、www にはCNAMEを指定してきたのは、この事情を踏まえたものでした。

    www にCNAMEを使うのは、接続先のIPが変わっても自動で追従できるからです。Aレコードで直接IPを書くと、相手のインフラ変更のたびに手で直す必要が出ます。

    失敗4: 末尾のドット1文字でエラー

    www の設定で、保存に失敗しました。

    「指定先」の入力内容が正しくありません。
    ・ドットの連続やドットとハイフンを連続することはできません。

    原因は末尾のドットでした。Vercelが表示していた値は e2e46a9cb91ef14a.vercel-dns-017.com. と、最後にドットが付いています。

    これはDNSの正式な記法で、「完全修飾名なので、これ以上ドメイン名を補完しない」 という意味を持ちます。ドットが無いと、システムによっては自動的に自分のドメイン名を後ろに付け足してしまうことがあり、それを防ぐための表記です。

    ところがこの入力欄は、ドットを受け付けない実装でした。外して入れ直したら通りました。

    情けないのは、私が事前に「入力欄によってはドットでエラーになるので、その場合は外してください」と自分で書いていたことです。注意点を知っていることと、実行時にそれを適用することは別だと痛感しました。

    反映を待つ ── 「変わらない」の正しい読み方

    DNSを変更した直後に確認すると、まだ古い値が返ってきます。ここで「保存できていないのでは」と疑って設定を触り直すと、事態が混乱します。

    DNSの答えにはTTL(Time To Live)という有効期限が付いています。今回は600秒でした。世界中のDNSサーバーは、一度取得した答えをこの秒数のあいだ保存(キャッシュ)し、その間は問い合わせ直しません。毎回問い合わせていたら世界中のDNSが処理しきれないためです。

    つまり変更直後に古い値が返るのは正常です。問題は「保存されていない」のか「キャッシュを見ているだけ」なのかをどう区別するかでした。

    ここで役に立ったのがTTLの残り秒数です。実際に観測した値を並べます。

    kisaku.site  A 133.130.64.184  ttl600   ← 取得したばかりのキャッシュ
    kisaku.site  A 133.130.64.184  ttl218   ← 382秒前に取得された答えを見ている
    kisaku.site  A 216.198.79.1    ttl600   ← 期限切れ後に取り直した結果

    TTLはキャッシュの残り寿命なので、600から218に減っていれば「これは382秒前の答えだ」と分かります。いま見ている情報がいつ時点のものかを読み取れるわけです。ゼロになるのを待って問い合わせ直せば、確実に現在の状態が得られます。

    なおブラウザで確認する場合は、パソコンとブラウザ自身のキャッシュも重なります。シークレットウィンドウで開くか、別のネットワークから見るのが手軽です。「反映されない」の多くは、どこかの層にキャッシュが挟まっているだけです。

    最終的にこうなりました。

    kisaku.site       → 216.198.79.1                          Vercel
    www.kisaku.site   → CNAME e2e46a9cb91ef14a.vercel-dns...  Vercel
    cms.kisaku.site   → 133.130.64.184                        無事
    

    守りたかった cms は最後まで無傷でした。https://www.kisaku.site/ で新サイトが表示され、SSL証明書も自動で発行されて、公開完了です。

    今回の学び

    技術的な収穫は、fetch failed の cause を必ず見ることでした。表面のメッセージだけ読んでいる限り、永遠に原因にたどり着けません。包まれたエラーは、包みを開けないと中身が分からない。

    進め方の話としては、推測から観測に切り替えるのが遅かったという反省です。今回、原因を2回外しました。どちらも「たぶんこれだろう」で設定をいじり、結果が変わらず、当たったのか外れたのかも判然としない、という時間の使い方です。

    最終的に効いたのは、エラーを表示させるためだけのコードを書くという、一見遠回りな手段でした。書くのに10分もかかっていません。推測で1回試すコストと、確実に答えを得るコストは、思っているほど差がない。

    そして、DNSのように間違えると広範囲が止まる作業では、操作の前に「いま自分はどの階層を見ているか」を確認する習慣が要る、ということ。レコードを1行も触らずにサイト全体を落としかけた今回は、それを実感する良い機会になりました。

    次回は、この作業を通して出てきた用語や仕組みを、もう少し体系立てて解説します。

    契約前に知っておきたかったこと

    ドメインとサーバーを同じ会社で契約すると、DNSの設定画面が2か所に増えます。

    ドメインを買った側の管理画面にもDNSの設定欄があり、レンタルサーバー側にもあります。そして実際に参照されるのは片方だけです。

    厄介なのは、参照されないほうに設定してもエラーが出ないことです。保存は成功し、画面上も正しく見えます。ただ、何も起きません。

    この1点を知っているだけで、丸一日の調査が要らなくなります。

    お名前.comでドメインを取得する