【Node.js】dns.resolveCnameでCNAMEレコードを解決!エラーとトラブルシューティング

2025-05-27

簡単に言うと、あるドメイン名が別のドメイン名への「別名(エイリアス)」として登録されている場合に、その元のドメイン名を調べてくれます。

CNAMEレコードとは?

CNAMEレコードは、あるドメイン名(エイリアス)が別のドメイン名(正規名またはカノニカル名)を指し示すために使われるDNSレコードの一種です。例えば、www.example.comexample.comのCNAMEレコードとして設定されている場合、www.example.comへのアクセスはexample.comへとリダイレクトされます。

dns.resolveCname()の仕組み

dns.resolveCname()メソッドは、以下のような形式で利用します。

const dns = require('dns');

dns.resolveCname(hostname, callback);
  • callback: 非同期処理の結果を受け取るためのコールバック関数です。この関数は通常、function(err, addresses)という2つの引数を受け取ります。
    • err: エラーが発生した場合にエラーオブジェクトが入ります。
    • addresses: ホスト名に対応するCNAMEアドレスの配列が文字列として格納されます。複数のCNAMEレコードがある場合は、それらすべてが配列に入ります。
  • hostname: CNAMEレコードを解決したいホスト名(ドメイン名)を指定します。

使用例

example.comというホスト名のCNAMEレコードを解決する例を考えてみましょう。

const dns = require('dns');

dns.resolveCname('example.com', (err, addresses) => {
  if (err) {
    console.error('CNAME解決エラー:', err);
    return;
  }
  console.log('CNAMEアドレス:', addresses);
});

dns.resolveCname('www.google.com', (err, addresses) => {
  if (err) {
    console.error('CNAME解決エラー (www.google.com):', err);
    return;
  }
  console.log('www.google.com の CNAMEアドレス:', addresses);
});

このコードを実行すると、example.comwww.google.comのCNAMEレコードが存在すれば、その結果がaddresses配列として出力されます。もしCNAMEレコードが存在しない場合や、エラーが発生した場合は、errオブジェクトに情報が格納されます。

  • DNSのキャッシュやネットワークの状態によって、結果が異なる場合があります。
  • CNAMEレコードが存在しない場合は、addresses配列が空になるか、エラーが返されることがあります。
  • dns.resolveCname()は非同期処理です。結果はコールバック関数で受け取る必要があります。


一般的なエラーとその原因

dns.resolveCname()は、エラーが発生した場合、コールバック関数のerr引数にErrorオブジェクトを返します。このErrorオブジェクトには、エラーの種類を示すcodeプロパティが含まれています。よく遭遇するエラーコードは以下の通りです。

  1. dns.NODATA (or ENODATA):

    • 原因: 指定されたホスト名に対してCNAMEレコードが存在しない場合に発生します。CNAMEレコードは、特定のドメインが別のドメインのエイリアスである場合にのみ存在するため、一般的なドメイン名(例: IPアドレスに直接紐付けられているドメイン)にはCNAMEレコードがないことがあります。
    • : example.comにはCNAMEレコードがなく、Aレコードのみが存在する場合、dns.resolveCname('example.com', ...)NODATAを返す可能性があります。
    • トラブルシューティング: これは必ずしも「エラー」ではなく、CNAMEレコードが存在しないというDNSの応答を意味します。対象のドメインがCNAMEレコードを持つことを想定している場合は、DNS設定を確認する必要があります。
  2. dns.NOTFOUND (or ENOTFOUND / NXDOMAIN):

    • 原因: 指定されたホスト名が見つからない、つまり存在しないドメイン名である場合に発生します。タイプミスや、ドメインが登録されていない場合に起こります。
    • トラブルシューティング: ホスト名が正しいスペルであることを確認し、そのドメインが実際に存在し、DNSに登録されていることを確認します。dignslookupのようなツールを使って、外部からドメインの存在を確認するのも有効です。
  3. dns.SERVFAIL (or ESERVFAIL):

    • 原因: DNSサーバー自体が一時的に利用できないか、リクエストを処理できない場合に発生します。DNSサーバーの過負荷、設定ミス、またはネットワークの問題が考えられます。
    • トラブルシューティング: DNSサーバーの設定を確認し、必要であれば別のDNSサーバー(例: Google Public DNS 8.8.8.8 や Cloudflare DNS 1.1.1.1)を使用するようにNode.jsのdns.setServers()で設定してみます。また、一定時間待ってから再試行することも有効です。大量のCNAME解決を試行している場合、DNSサーバーがリクエストを拒否することがあります。
  4. dns.TIMEOUT:

    • 原因: DNSサーバーからの応答がタイムアウトした場合に発生します。ネットワークの遅延、DNSサーバーの応答がない、またはリクエストがブロックされている可能性があります。
    • トラブルシューティング: ネットワーク接続を確認します。DNSサーバーの設定を確認し、別のサーバーを試します。場合によっては、より長いタイムアウトを設定する必要があるかもしれません(ただし、dns.resolveCnameには直接タイムアウトオプションはありませんが、DNSサーバーの応答が遅いことが原因であれば、その根本原因に対処する必要があります)。
  5. dns.FORMERR (or EFORMERR):

    • 原因: DNSサーバーが不正な形式のクエリを受信したか、不正な形式の応答を返した場合に発生します。これは通常、Node.jsのDNSモジュールの内部的な問題か、DNSサーバーの異常な動作を示唆します。
    • トラブルシューティング: 稀なケースですが、Node.jsのバージョンを更新したり、DNSサーバーの安定性を確認したりすることが考えられます。
  6. dns.BADNAME (or EBADNAME):

    • 原因: ホスト名の形式が不正な場合に発生します。例えば、無効な文字が含まれている、長すぎる、といったケースです。
    • トラブルシューティング: 指定したhostname文字列が有効なドメイン名の形式であることを確認します。
  1. DNSサーバーの確認と変更: Node.jsはOSが設定しているDNSサーバーを使用しますが、dns.setServers()を使って明示的にDNSサーバーを指定できます。これは、特定のDNSサーバーに問題がある場合に有効なトラブルシューティング手段です。

    const dns = require('dns');
    
    // Google Public DNS を使用するように設定
    dns.setServers(['8.8.8.8', '8.8.4.4']);
    
    dns.resolveCname('www.example.com', (err, addresses) => {
      if (err) {
        console.error('エラー:', err);
        return;
      }
      console.log('CNAMEアドレス:', addresses);
    });
    
  2. OSレベルでのDNS解決の確認: Node.jsの外部で、コマンドラインツール(dignslookup)を使って対象のホスト名に対するCNAMEレコードを直接クエリしてみます。これにより、問題がNode.jsアプリケーションにあるのか、それともシステム全体のDNS解決に問題があるのかを切り分けられます。

    • Linux/macOS:
      dig CNAME www.example.com
      
    • Windows:
      nslookup -type=CNAME www.example.com
      

    これらのツールで期待通りの結果が得られない場合、問題はNode.jsではなく、ローカルのDNS設定やネットワーク構成にある可能性が高いです。

  3. DNSキャッシュの問題: DNSレコードが変更されたにもかかわらず、dns.resolveCname()が古い情報を返すことがあります。これは、OSやDNSサーバーのキャッシュが原因である可能性があります。DNSレコードのTTL(Time To Live)が切れるまで待つか、OSのDNSキャッシュをクリアすることを試みます。

    • macOS: sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
    • Windows: ipconfig /flushdns
    • Linux: ディストリビューションや設定によりますが、通常はDNSリゾルバサービスを再起動します。
  4. 大量のCNAME解決に対する考慮: 非常に大量のCNAME解決を同時に行うと、DNSサーバーに負担をかけたり、Node.jsの内部スレッドプール(libuv)をブロックしたりして、パフォーマンス問題やSERVFAILなどのエラーを引き起こす可能性があります。このような場合は、解決するリクエストをキューイングして同時実行数を制限する(例: async.queueのようなライブラリを使用する)ことを検討してください。



基本的な使い方:CNAMEレコードの解決

最も基本的な使い方です。特定のホスト名に対してCNAMEレコードを解決し、結果をコンソールに出力します。

const dns = require('dns');

// 解決したいホスト名(CNAMEレコードを持つ可能性のあるもの)
// 例: www.example.com は example.com のCNAMEになっていることが多い
const hostnameToResolve = 'www.google.com';

dns.resolveCname(hostnameToResolve, (err, addresses) => {
  if (err) {
    // エラーが発生した場合
    console.error(`CNAME解決エラー for ${hostnameToResolve}:`, err);
    console.error(`エラーコード: ${err.code}`);
    return;
  }

  // CNAMEレコードが見つかった場合
  if (addresses.length > 0) {
    console.log(`'${hostnameToResolve}' の CNAME レコード:`);
    addresses.forEach(address => {
      console.log(`- ${address}`);
    });
  } else {
    // CNAMEレコードが存在しない場合(NODATAの場合など)
    console.log(`'${hostnameToResolve}' には CNAME レコードが見つかりませんでした。`);
  }
});

// CNAMEレコードを持たない可能性が高いドメインの例
// (これにより NODATA エラーが発生する可能性が高い)
dns.resolveCname('example.com', (err, addresses) => {
  if (err) {
    console.error(`CNAME解決エラー for example.com:`, err);
    console.error(`エラーコード: ${err.code}`); // 通常は NODATA
    return;
  }
  if (addresses.length > 0) {
    console.log(`'example.com' の CNAME レコード:`);
    addresses.forEach(address => {
      console.log(`- ${address}`);
    });
  } else {
    console.log(`'example.com' には CNAME レコードが見つかりませんでした。`);
  }
});

解説:

  • errが存在する場合、エラーの種類(err.code)を確認し、適切なメッセージを出力します。
  • errが存在しない場合(nullの場合)、addresses配列には見つかったCNAMEレコードが格納されます。複数のCNAMEレコードがある場合もあります。
  • 第2引数はコールバック関数です。この関数はerr(エラーオブジェクト)とaddresses(CNAMEアドレスの配列)を受け取ります。
  • dns.resolveCname()の第1引数に解決したいホスト名を文字列で渡します。
  • require('dns') でDNSモジュールをロードします。

Promise を使った実装 (async/awaitと組み合わせ)

Node.jsのdnsモジュールはデフォルトでコールバックベースですが、util.promisifyを使うことでPromiseベースに変換し、async/await構文でよりクリーンに記述できます。

const dns = require('dns');
const util = require('util');

// dns.resolveCname を Promise ベースの関数に変換
const resolveCnamePromise = util.promisify(dns.resolveCname);

async function checkCname(hostname) {
  try {
    const addresses = await resolveCnamePromise(hostname);
    if (addresses.length > 0) {
      console.log(`[${hostname}] の CNAME レコード:`);
      addresses.forEach(address => {
        console.log(`  -> ${address}`);
      });
      return addresses;
    } else {
      console.log(`[${hostname}] には CNAME レコードがありませんでした。`);
      return [];
    }
  } catch (err) {
    console.error(`[${hostname}] の CNAME 解決エラー: ${err.message} (コード: ${err.code})`);
    return null; // エラーが発生した場合は null を返す
  }
}

// いくつかのドメインをチェック
(async () => {
  console.log('--- CNAMEチェック開始 ---');
  await checkCname('www.cloudflare.com'); // CNAMEを持つ例
  await checkCname('cloudflare.com');     // CNAMEを持たない可能性が高い例
  await checkCname('nonexistent-domain-12345.com'); // 存在しないドメインの例
  console.log('--- CNAMEチェック終了 ---');
})();

解説:

  • 即時実行関数式(async () => { ... })();を使って、awaitを使用できるコンテキストを作成しています。
  • try...catchブロックでエラーを捕捉し、エラーコードとメッセージを出力しています。
  • async/await構文を使うことで、非同期処理を同期処理のように記述でき、可読性が向上します。
  • util.promisify(dns.resolveCname)を使って、コールバック形式のresolveCname関数をPromiseを返す関数に変換します。

dns.resolveCname()は直接のCNAMEレコードを返しますが、そのCNAMEがさらに別のCNAMEを指している「CNAMEチェーン」が存在する場合があります。この例では、CNAMEチェーンを再帰的に解決する方法を示します。

const dns = require('dns');
const util = require('util');

const resolveCnamePromise = util.promisify(dns.resolveCname);

/**
 * 指定されたホスト名から始まるCNAMEチェーンを再帰的に解決します。
 * @param {string} hostname 解決を開始するホスト名
 * @param {string[]} [chain=[]] 解決されたCNAMEのパス(内部使用)
 * @returns {Promise<string[]>} CNAMEチェーンの配列(最上位から最終的な正規名まで)
 */
async function resolveCnameChain(hostname, chain = []) {
  try {
    const addresses = await resolveCnamePromise(hostname);

    if (addresses.length > 0) {
      // 最初に見つかったCNAMEアドレスを次のターゲットとする
      const nextTarget = addresses[0];
      chain.push(hostname); // 現在のホスト名をチェーンに追加

      console.log(`[${hostname}] -> CNAME: ${nextTarget}`);

      // 次のターゲットが既にチェーンに存在するかチェック(無限ループ防止)
      if (chain.includes(nextTarget)) {
        console.warn(`警告: CNAME ループを検出しました。停止します。`);
        chain.push(nextTarget); // ループの終点を追加
        return chain;
      }

      // 次のターゲットを再帰的に解決
      return await resolveCnameChain(nextTarget, chain);
    } else {
      // CNAMEレコードが見つからない場合、それが最終的なホスト名(またはA/AAAAレコードを持つもの)
      // そのホスト名がチェーンにない場合のみ追加(最初の呼び出しでCNAMEなしの場合など)
      if (!chain.includes(hostname)) {
          chain.push(hostname);
      }
      return chain;
    }
  } catch (err) {
    if (err.code === 'ENODATA' || err.code === 'ENOTFOUND') {
      // CNAMEレコードが存在しないか、ドメインが見つからない場合
      // そのホスト名が最終的な解決点となる
      if (!chain.includes(hostname)) {
          chain.push(hostname);
      }
      return chain;
    } else {
      // その他のDNSエラー
      console.error(`CNAMEチェーン解決中にエラーが発生しました: ${hostname}, ${err.message} (コード: ${err.code})`);
      throw err; // エラーを上位に伝播させる
    }
  }
}

// 例: CNAMEチェーンを持つ可能性のあるドメイン
(async () => {
  console.log('\n--- CNAMEチェーン解決テスト ---');

  // 実際のCNAMEチェーンを持つドメインを使用してください
  // 例: CDNやサブドメインのセットアップなどで見られます
  // 注意: 以下のドメインはCNAMEチェーンのテスト用としています。
  //      実際にCNAMEチェーンがあるかはその時点のDNS設定によります。
  const testDomains = [
    'www.example.com', // CNAME -> example.com (が多い)
    'docs.microsoft.com', // 複数のCNAMEを辿る可能性
    'nonexistent-cname-chain-test.com' // 存在しないドメイン
  ];

  for (const domain of testDomains) {
    try {
      const chain = await resolveCnameChain(domain);
      console.log(`'${domain}' の CNAME チェーン:`);
      console.log(chain.join(' -> '));
    } catch (error) {
      console.error(`'${domain}' のチェーン解決失敗:`, error.message);
    }
    console.log('------------------------------');
  }
})();

解説:

  • 実際のDNS設定によっては、CNAMEチェーンが見られない場合もあります。
  • ENODATAENOTFOUNDのエラーは、CNAMEチェーンの末端に到達したことを意味する場合があるため、そこで解決を停止し、現在のチェーンを返します。
  • chain配列は、解決されたCNAMEのパスを記録し、無限ループを検出するために使用されます。
  • resolveCnameChain関数は再帰的にCNAMEを解決していきます。


dns.resolve() メソッドの使用

dns.resolve() は、指定されたホスト名に対して特定のレコードタイプを解決するための汎用的なメソッドです。CNAMEレコードのみを解決したい場合は、'CNAME' タイプを指定してdns.resolve()を使用できます。

dns.resolveCname() と dns.resolve(hostname, 'CNAME') の違い

理論的には同じ結果を返しますが、dns.resolveCname() はCNAMEレコード専用に設計されているため、より簡潔で意図が明確になります。内部的には同じようなロジックが使用されている可能性が高いです。


const dns = require('dns');

const hostname = 'www.google.com';

// dns.resolve() を使って CNAME レコードを解決
dns.resolve(hostname, 'CNAME', (err, addresses) => {
  if (err) {
    console.error(`dns.resolve(${hostname}, 'CNAME') エラー:`, err);
    return;
  }
  console.log(`dns.resolve(${hostname}, 'CNAME') 結果:`, addresses);
});

// 比較のために dns.resolveCname() も実行
dns.resolveCname(hostname, (err, addresses) => {
  if (err) {
    console.error(`dns.resolveCname(${hostname}) エラー:`, err);
    return;
  }
  console.log(`dns.resolveCname(${hostname}) 結果:`, addresses);
});

この2つの呼び出しは通常、同じ結果を返します。

dns.lookup() メソッドの使用 (IPアドレス解決が主な目的の場合)

dns.lookup() は、DNSを使用してホスト名をIPアドレスに解決する最も基本的なメソッドです。これは主にネットワーク接続のためにIPアドレスが必要な場合に使用されます。dns.lookup() はCNAMEチェーンを透過的に解決し、最終的なIPアドレスを返しますが、途中のCNAMEレコード自体は提供しません。

特徴

  • dns.resolve() シリーズとは異なり、ネットワーク操作よりもOSレベルのDNSキャッシュやhostsファイルの影響を受けやすいです。
  • オペレーティングシステムのDNSリゾルバ設定の影響を強く受けます。
  • dns.resolveCname() とは異なり、CNAMEレコードの文字列自体は返しません。 最終的なIPアドレスを返します。


const dns = require('dns');

const hostname = 'www.google.com';

dns.lookup(hostname, (err, address, family) => {
  if (err) {
    console.error(`dns.lookup(${hostname}) エラー:`, err);
    return;
  }
  console.log(`dns.lookup(${hostname}) 結果:`);
  console.log(`  IPアドレス: ${address}`);
  console.log(`  IPファミリー: IPv${family}`); // 4 (IPv4) または 6 (IPv6)
});

もしwww.google.comがCNAMEを介してIPアドレスに解決される場合でも、dns.lookup()は最終的なIPアドレスのみを提供し、CNAMEレコードの存在は知らされません。

Node.jsの組み込みdnsモジュールは基本的な機能を提供しますが、より高度なDNS操作(例: DNSSECの検証、任意のレコードタイプのクエリ、特定のDNSサーバーへの直接クエリ制御など)が必要な場合、コミュニティ製の外部ライブラリを検討することができます。

例として、dns-packetnative-dns-clientといったライブラリがありますが、これらは通常、dns.resolveCname()のような高レベルの抽象化ではなく、より低レベルのDNSプロトコル操作を提供します。

例 (概念)

// これは架空のコードであり、実際のライブラリの使用法とは異なる場合があります。
// 特定のライブラリのドキュメントを参照してください。
/*
const { Client } = require('native-dns-client');

const client = new Client();

async function resolveCnameWithExternalLib(hostname) {
  try {
    const response = await client.query({
      questions: [{
        name: hostname,
        type: 'CNAME'
      }]
    });

    const cnameRecords = response.answers
      .filter(a => a.type === 'CNAME')
      .map(a => a.data);

    console.log(`外部ライブラリで ${hostname} の CNAME:`, cnameRecords);
  } catch (err) {
    console.error(`外部ライブラリでの CNAME 解決エラー:`, err);
  }
}

resolveCnameWithExternalLib('www.example.com');
*/

利点

  • 非標準のDNSレコードタイプやDNSSECなどの高度な機能。
  • より詳細なDNSクエリの制御(例: リトライ設定、特定のサーバーへの直接接続)。
  • 多くの場合は、dns.resolveCname()で十分な機能が提供されます。
  • 学習コストが高い。
  • 外部依存性が増える。