深夜3時、ピッチリと冷房が効いたNOCルーム。突然、監視モニターの一角が真っ赤に染まった。
「おい、APIの死活監視が全滅だ。上流のクラウドサービスか?」
若手エンジニアが青い顔をして振り向く。しかし、私は慌てず騒がせず、手元マグカップの冷めたブラックコーヒーを一口啜ってこう言った。
「落ち着け。まずはトランスポート層を見る前に、すべての通信の羅針盤である『あいつ』を疑うんだ。――そう、DNSだ」
Web APIの設計やインフラ運用において、エンドポイントの名前解決トラブルは日常茶飯事だ。だが、多くのエンジニアは「繋がらない」と聞くと、すぐに ping を打ったり、HTTPのステータスコードだけに気を取られたりする。
本当に見るべきなのは、DNSサーバーが返してくる「言葉」の裏側、すなわちDNS応答メッセージのヘッダーフィールド(FlagsとRCODE)だ。今回は、百戦錬磨の現場で私たちがどのようにDNSのパケットを読み解き、障害の根源を特定しているのか、その実戦的なノウハウを伝授しよう。
—
1. RFCが定めた「DNS応答」の裏側とパケットの現実
DNSは、私たちが普段意識することなく使っているが、裏側ではUDP(またはTCP)のポート53番を使い、極めて厳格なプロトコル仕様(RFC 1035など)に基づいてやり取りされている。
DNSのクエリ(要求)に対し、フルリゾルバや権威DNSサーバーが返すレスポンスメッセージは、大別して以下のセクションで構成されている。
1. Header(ヘッダー): トランザクションID、各種フラグ(Flags)、セクションごとのレコード数
2. Question(質問): 問い合わせたドメイン名、クエリタイプ、クラス
3. Answer(応答): 問い合わせに対する実際のレコード(AレコードやCNAMEなど)
4. Authority(権威): 権威を持つネームサーバーの情報
5. Additional(追加): 関連する追加情報(EDNS0のバッファサイズやGlueレコードなど)
この中で、トラブルシューティングの成否を握るのが、Headerセクションに刻まれた「Flags」と「RCODE(Response Code)」だ。ここには、DNSサーバーがそのリクエストをどう解釈し、なぜその結果になったのかという「言い訳」のすべてが詰まっている。
—
2. dig コマンドでヘッダーの深層を覗き見る
実務において、名前解決のデバッグツールとして nslookup を使っているなら、今すぐ卒業して dig(Domain Information Groper)を相棒にしよう。情報の解像度が圧倒的に違う。
例えば、存在しないドメインをあえて dig で叩いてみよう。
$ dig +noall +answer +comments @8.8.8.8 non-existent-api.example.com A
このコマンドを実行した際、出力されるヘッダー部分に注目してほしい。
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 45123
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
この数行の中に、障害の切り分けに必要な情報の9割が詰まっている。ここから読み解くべき重要パラメーターを解説しよう。
ステータスコード(RCODE)の読み方
ヘッダーの status: [RCODE] 部分は、DNSサーバーからの通信結果の「通知表」だ。代表的なものを頭に叩き込んでおいてほしい。
NOERROR: 正常終了。おめでとう、データはそこにある。ただし、Answerセクションが空(ANSWER: 0)の場合は、ドメインは存在するが該当するレコードタイプ(AやAAAAなど)が存在しない「NODATA」状態を意味する。NXDOMAIN(Non-Existent Domain): 「そんなドメインは宇宙のどこにも存在しない」という権威サーバーからの冷徹な返答。タイポ、設定漏れ、あるいはドメインの失効を疑え。SERVFAIL(Server Failure): 権威サーバー側でのダウン、ゾーンファイルの構文エラー、あるいはDNSSECの検証失敗など、サーバー側で何らかの致命的な障害が発生している状態。自社管理のゾーンであれば即座に権威DNSのログを確認すべきだ。REFUSED: ポリシーやアクセス制限(ACL)によって、DNSサーバーがリクエストを拒絶した状態。転送設定(Forwarding)の不備や、ゾーン転送の権限エラーでよく遭遇する。
フラグ(Flags)の深層
flags: qr rd ra の部分も極めて重要だ。現場でよく見るフラグの意味を整理する。
qr(Query Response): これが立っていれば「応答パケット」であることを示す。aa(Authoritative Answer): この応答を返したサーバーが、そのゾーンの「権威(マスター/スレーブ)」自身であることを示す。キャッシュサーバーからの応答であればこのフラグは落ちている。tc(Truncated): 応答メッセージがUDPの制限サイズ(一般的に512バイト、EDNS0有効時はそれ以上)を超えたため、途中で切り捨てられたことを示す。これが立っている場合、通常は自動的にTCPでの再接続にフォールバックするが、ファイアウォールがTCP 53番をブロックしていると名前解決がランダムに失敗する「嫌な障害」を引き起こす。rd(Recursion Desired): クライアントが「再帰的問い合わせ(代わりに調べてきてくれ)」を要求しているフラグ。ra(Recursion Available): 問い合わせ先のDNSサーバーが「再帰的問い合わせに対応しているよ」と返しているフラグ。パブリックDNS(8.8.8.8や1.1.1.1)には必ず立っているが、社内ニッチなフォワーダーサーバーでこれが立っていない場合、外部への名前解決が一切できなくなる。
—
3. 実務で遭遇する「迷宮入り」トラブルと解決シナリオ
ここで、過去に私がNOCで対応した実際のインシデントをベースにした、典型的なトラブルシューティングのシナリオを紹介しよう。
シナリオ:APIクライアントからの「突然のSERVFAIL」
あるマイクロサービス基盤で、特定の外部決済APIへの接続が断続的に失敗するというアラートが上がった。アプリケーションログには getaddrinfo ESERVFAIL が並んでいる。
1. 初動:パケットとRCODEの確認
まずはローカルのフルリゾルバ(BINDやCoreDNSなど)をバイパスして、直接外部の権威サーバー、あるいはパケットキャプチャ(tcpdump)で状態を確認する。
# パブリックDNSを直接指定して、どのRCODEが返っているか確認する
$ dig @1.1.1.1 api.payment-gateway.example.com A +noall +comments
出力結果のステータスが SERVFAIL になっていることを確認。さらに、デバッグ用の詳細出力を得る。
$ dig @1.1.1.1 api.payment-gateway.example.com +trace
2. 原因究明:DNSSECとEDNS0の闇
+trace コマンドでルートサーバーから順に追っていくと、決済APIの親ドメイン(example.com)の権威DNSサーバーの応答において、特定のゾーンで署名検証エラー(DNSSEC Validation Failure)が起きていることが判明した。
相手方のインフラチームが、キーロールオーバー(暗号鍵の更新)の際に古いDSレコードを親ゾーンに登録し忘れた、あるいはキャッシュサーバー側とのEDNS0バッファサイズ(UDPパケットサイズ)の不整合によるパケットドロップが原因だった。
3. 一時回避策と恒久対策
- 一時回避: アプリケーション側のコンテナが参照している
/etc/resolv.confのネームサーバーを、厳格なDNSSEC検証を行うパブリックDNSから、検証を一時的に緩和・スキップする社内フォワーダーへ切り替える。 - 恒久対策: 相手方(API提供元)のインフラ担当者に
SERVFAILが発生している旨と、dig +traceの出力を添えてチケットを起票。DNSSECの署名整合性を修正してもらった。
—
4. アプリケーションコード(Python / Node.js)でのDNSエラーハンドリング
インフラエンジニアだけでなく、Web APIを設計・実装するアプリケーションエンジニアも、コードレベルでDNSの挙動を意識する必要がある。単に「接続エラー」として一括りにキャッチするのではなく、DNS特有のエラーをハンドリングできるようにしよう。
以下は、Pythonの dnspython ライブラリを使用して、明示的にRCODEやフラグを検証し、障害時に適切なログとメトリクスを出力する実装例だ。
import dns.exception
import dns.message
import dns.query
import dns.rdatatype
def check_api_dns_health(domain_name: str, nameserver_ip: str) -> bool:
"""指定されたドメインのDNSレコードをチェックし、
RCODEやフラグを検証してヘルスステータスを返す。
"""
try:
# クエリメッセージの作成(Aレコードを要求)
q = dns.message.make_query(domain_name, dns.rdatatype.A)
# タイムアウトを2秒に設定し、UDPで問い合わせを実行
# 実務ではTCPフォールバックの考慮も必要
response = dns.query.udp(q, nameserver_ip, timeout=2.0)
# ヘッダーのRCODE(応答コード)をチェック
rcode = response.rcode()
rcode_str = dns.rcode.to_text(rcode)
print(f"[INFO] Query for {domain_name} to {nameserver_ip} -> RCODE: {rcode_str}")
if rcode == dns.rcode.NOERROR:
# 正常終了だが、Answerセクションが空(NODATA)の可能性をチェック
if len(response.answer) == 0:
print(
f"[WARN] NODATA: Domain exists, but no A record for {domain_name}"
)
return False
# 正常にレコードが存在する
return True
elif rcode == dns.rcode.NXDOMAIN:
print(f"[ERROR] NXDOMAIN: Domain {domain_name} does not exist.")
# TODO: アラート発報やメトリクス加算の処理をここに記述
return False
elif rcode == dns.rcode.SERVFAIL:
print(
f"[ERROR] SERVFAIL: Nameserver internal failure or DNSSEC error."
)
# TODO: アップストリームの障害としてハンドリング
return False
else:
print(f"[ERROR] Unexpected RCODE received: {rcode_str}")
return False
dns.exception.Timeout:
print(
f"[FATAL] DNS query timed out against nameserver {nameserver_ip}."
)
# ネットワーク層(ファイアウォール、ルーティング)の障害を疑う
return False
except Exception as e:
print(f"[CRITICAL] An unexpected error occurred: {e}")
return False
# 実行例のシミュレーション
if __name__ == "__main__":
target_domain = "api.example.com"
resolver = "8.8.8.8"
is_healthy = check_api_dns_health(target_domain, resolver)
if not is_healthy:
print("-> DNS Health Check FAILED. Investigate headers and network paths.")
このように、コード側でも単なる例外キャッチではなく、SERVFAIL なのか NXDOMAIN なのかを識別できるようにしておくことで、障害時の自動復旧ロジック(リトライすべきか、即座にフェイルオーバーすべきかの判断)が劇的に洗練される。
—
5. シニアエンジニアからの実務Tips:詰まったときのデバッグチェックリスト
最後に、現場であなたがDNSトラブルの荒海に放り出されたときのために、私の頭の中にある「トラブルシューティング・チェックリスト」を授けよう。
1. まずはキャッシュを疑え
ローカルOSのキャッシュ(nscdやsystemd-resolved)、あるいはアプリケーションランタイム(JavaのJVM DNSキャッシュなど)が古い NXDOMAIN や古いIPアドレスを掴んでいるケースがあまりに多い。dig を直叩きして「生」の応答を確認するのが鉄則だ。
2. -x や +trace を使いこなせ
フルリゾルバの挙動がおかしい時は dig +trace でルートから順にパケットの足取りを追う。誰も教えてくれない「どこで権威が途切れているか」を教えてくれる。
3. EDNS0とUDP/TCP 53番のファイアウォールを確認せよ
応答パケットの tc フラグが見えたら、ネットワークのレイヤー3/4(セキュリティグループやFW)でTCP 53番がブロックされていないか即座に確認しろ。これが隠れたパケットロスやレイテンシー増大の元凶だ。
4. 構造化ログにRCODEを残せ
APIのゲートウェイやリバースプロキシ(NginxやEnvoyなど)のアクセスログ・エラーログには、可能な限りDNS解決時のエラーコード(あるいはそれに準ずる詳細)を出力させよ。夜中のインシデント対応で「なぜ繋がらなかったのか」の証拠がそこにある。
DNSは、目に見えない。だからこそ、パケットが語るヘッダーの微細なサイン(FlagsとRCODE)を読み解く能力が、インフラエンジニア、そしてWeb APIを支えるすべてのエンジニアの「腕の見所」となる。
さあ、コンソールを開き、コマンドの海へ飛び出そう。パケットは決して嘘をつかない。
コメント