RDAPでドメインの有効期限台帳を作る — Claude Codeで更新対象を洗い出す
ドメイン一覧にRDAPで問い合わせ、expirationイベントの日付から60日以内の更新対象を拾う台帳の作り方です。問い合わせ先の引き方、statusの読み方、Claude Codeへの任せ方まで扱います。
自社で持つドメインが増えると、更新日が誰の管理画面にあるのか分からなくなります。期限切れでWebサイトやメールが止まる事故を避けるには、全ドメインの有効期限を1枚の一覧に集め、期限が近い順に並べておくのが確実です。
その一覧は、RDAP(Registration Data Access Protocol)で作れます。RDAPは、ドメインの登録情報をJSONで返す問い合わせの仕組みです。返ってくるJSONの events に expiration というイベントがあり、その日付がドメインの有効期限になります。この記事では、ドメイン一覧をRDAPで引いて残り日数で並べるスクリプトと、結果の読み方、Claude Codeに任せる範囲を順に書きます。
RDAPの有効期限は「expiration」イベントで取る
RDAPのドメイン応答は、events という配列に出来事を持ちます。要素は eventAction(出来事の種類)と eventDate(日時)の組で、RFC 9083は両方を必須としています。
期限を表すのは、eventAction が expiration の要素です。RFC 9083はこの値を「オブジェクトが、あらかじめ決まった日時にレジストリから削除される(または削除された)こと」と定めています。同じ節に、イベントは未来の日付でもよく、その使い道の1つがオブジェクトの失効日を示すことだと書かれています。
つまり、期限日を取る処理は次の2点だけです。
eventsの中からeventActionがexpirationの要素を探す- その
eventDateを日時として読む
注意が1つあります。events の並び順は仕様で決まっていません。手元で .com と .org の応答を比べると、.com では registration が先頭で、.org では expiration が先頭でした。先頭要素や添字で読まず、eventAction で探します。
eventAction の値は「RDAP JSON Values」というIANAのレジストリに登録されています。同じ語彙でTLDをまたいで読めるので、1本のコードで全ドメインを処理できます。
台帳に持つ列と、60日以内を拾う条件
台帳の列は、問い合わせの結果から機械的に埋められるものと、人が持つものに分けます。
| 列 | 中身 | 埋める方法 |
|---|---|---|
| domain | 中身ドメイン名 | 埋める方法入力の一覧 |
| expiration | 中身期限日(UTC) | 埋める方法RDAPの expiration |
| days_left | 中身実行日からの残り日数 | 埋める方法スクリプトで計算 |
| registrar | 中身管理しているレジストラ名 | 埋める方法RDAPの registrar ロールのエンティティ |
| status | 中身登録状態の一覧 | 埋める方法RDAPの status |
| note | 中身要更新、取得失敗の理由 | 埋める方法スクリプトが記入 |
更新対象の条件は「残り日数が60日以内」とします。60日は台帳の運用上の目安で、RDAPの仕様に出てくる数字ではありません。社内の稟議や支払いの締めに合わせ、スクリプトの定数 LIMIT_DAYS で変えられるようにしておきます。期限を過ぎたドメインは残り日数が負になるので、同じ条件に自然に入ります。
レジストラ名は、entities の中で roles に registrar を含む要素の vcardArray から fn(表示名)を取ります。自動更新の有無、請求先、更新の受付方法のように、レジストラごとに違う運用情報は、台帳に備考列を足して人が持つ情報として管理します。
IANAのbootstrapで問い合わせ先を引くスクリプト
RDAPは、TLDごとに問い合わせ先のサーバーが違います。自分で一覧を持たなくても、IANAが公開する https://data.iana.org/rdap/dns.json に対応表があります。これはRFC 9224で定められた「bootstrap」の仕組みで、ドメイン名とラベル単位で右から最長一致するエントリを探し、そこに並ぶ基本URLを使います。基本URLが複数あるときは、HTTPSを先に試します。
問い合わせのURLは、基本URLに domain/<ドメイン名> を付けた形です(RFC 9082)。たとえば .com なら https://rdap.verisign.com/com/v1/domain/example.com になります。
次のスクリプトは、Python標準ライブラリだけで動きます。domains.txt に1行1ドメインで書き、# で始まる行は読み飛ばします。
#!/usr/bin/env python3
"""domains.txt の各ドメインを RDAP で引き、expiration の日付を CSV にする。"""
import csv, json, sys, time, urllib.request, urllib.error
from datetime import datetime, timezone
BOOTSTRAP = "https://data.iana.org/rdap/dns.json"
LIMIT_DAYS = 60
def load_bootstrap():
with urllib.request.urlopen(BOOTSTRAP, timeout=30) as r:
services = json.load(r)["services"]
return {t: urls for tlds, urls in services for t in tlds}
def base_url(table, domain):
labels = domain.lower().rstrip(".").split(".")
for i in range(len(labels)): # ラベル単位の最長一致
urls = table.get(".".join(labels[i:]))
if urls:
return next((u for u in urls if u.startswith("https://")), urls[0])
return None
def registrar_name(data):
for ent in data.get("entities", []):
if "registrar" in ent.get("roles", []):
for item in ent.get("vcardArray", [None, []])[1]:
if item[0] == "fn":
return item[3]
return ""
def lookup(domain, table):
row = {"domain": domain, "expiration": "", "status": "",
"registrar": "", "note": ""}
base = base_url(table, domain)
if not base:
row["note"] = "bootstrap に該当 TLD なし"
return row
req = urllib.request.Request(
base.rstrip("/") + "/domain/" + domain,
headers={"Accept": "application/rdap+json",
"User-Agent": "domain-ledger/1.0"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
data = json.load(r)
except urllib.error.HTTPError as e:
row["note"] = f"HTTP {e.code}"
return row
except Exception as e:
row["note"] = f"取得失敗: {type(e).__name__}"
return row
row["status"] = ";".join(data.get("status", []))
for ev in data.get("events", []):
if ev.get("eventAction") == "expiration":
row["expiration"] = ev["eventDate"]
row["registrar"] = registrar_name(data)
if not row["expiration"]:
row["note"] = "expiration イベントなし"
return row
def main(path):
table = load_bootstrap()
now = datetime.now(timezone.utc)
rows = []
for line in open(path, encoding="utf-8"):
domain = line.strip()
if not domain or domain.startswith("#"):
continue
row = lookup(domain, table)
if row["expiration"]:
exp = datetime.fromisoformat(row["expiration"].replace("Z", "+00:00"))
row["days_left"] = (exp - now).days
if row["days_left"] <= LIMIT_DAYS:
row["note"] = "要更新"
else:
row["days_left"] = ""
rows.append(row)
time.sleep(1) # 相手のサーバーへの負荷を抑える
rows.sort(key=lambda r: (r["days_left"] == "", r["days_left"] or 0))
w = csv.DictWriter(sys.stdout, ["domain", "expiration", "days_left",
"registrar", "status", "note"])
w.writeheader()
w.writerows(rows)
if __name__ == "__main__":
main(sys.argv[1])エラーになった行も一覧から落とさず、note に理由を書いて残しています。取得できなかったドメインが台帳から消えると、期限管理の穴がそのまま見えなくなるためです。並び順は、日付のある行を残り日数の少ない順に先頭へ、日付の無い行を末尾にしています。
実行結果の読み方と、引けない行の切り分け
実行は、一覧のファイルを渡してCSVを受け取るだけです。
python3 rdap_expiry.py domains.txt > ledger.csv
column -s, -t ledger.csv予約ドメインの example.com example.net example.org、bootstrapに載らないTLDの example.jp、存在しない名前を並べて実行した出力は、次のとおりでした(Python 3.12。days_left は実行日で変わります)。
domain,expiration,days_left,registrar,status,note
example.com,2027-08-13T04:00:00Z,297,RESERVED-Internet Assigned Numbers Authority,client delete prohibited;...,
example.net,2027-08-30T04:00:00Z,314,RESERVED-Internet Assigned Numbers Authority,client delete prohibited;...,
example.org,2027-08-30T04:00:00Z,314,ICANN,server delete prohibited;...,
example.jp,,,,,bootstrap に該当 TLD なし
nonexistent-zzq81.com,,,,,HTTP 404日付のある行は期限順に並び、取得できなかった2行は末尾にnoteつきで残っています。registrar 列は、実在のドメインならレジストラの表示名が入ります。
落とし穴
Pythonの既定のままだと、サーバーによっては403が返る
Pythonの urllib は既定で Python-urllib/3.x を User-Agent として送ります。この既定値で example.org を問い合わせるとHTTP 403が返り、User-Agent: domain-ledger/1.0 のような独自の値を付けると200で応答が返って expiration も取れます。応答本文に403の理由は書かれていません。取得失敗の行が特定のTLDに偏るときは、ヘッダーの指定を最初に疑う価値があります。
行ごとの note が空でないとき、原因は次の順に切り分けます。
取得できなかった行の切り分け
- Qnoteは「bootstrap に該当 TLD なし」か答えがはいのとき答えがいいえ
そのTLDはIANAの対応表に載っていません。上の出力では
example.jpがこれで、取得したdns.jsonにjpのエントリはありませんでした。RDAPでは引けないので、レジストラの管理画面かWHOISで期限を確かめ、台帳の備考に手入力します。自分のTLDが載っているかは、python3 -cでdns.jsonを開いて確かめられます。 - Qnoteは「HTTP 404」か答えがはいのとき答えがいいえ
サーバーがその名前を知らないという応答です。綴りの誤り、未登録、すでに削除された名前のどれかです。入力の一覧に誤記がないか見直します。
- Qnoteは「HTTP 403」などのその他のコードか答えがはいのとき答えがいいえ
ヘッダーと問い合わせ間隔を見直します。スクリプトは1件ごとに1秒空けています。数百件を並列で投げると、サーバー側に制限される要因になります。
expiration イベントなし
応答は返っているのに期限の要素が無いケースです。status の列と、レジストラの管理画面の表示を突き合わせます。
期限が近いドメインのstatusを読む
status の列は、期限日と合わせて読むと状況が分かります。RFC 9083とRFC 8056(EPPの状態をRDAPの語に対応づけた仕様)が定義する語のうち、期限まわりで目に入りやすいものを抜き出します。
| status | 意味(仕様の定義) |
|---|---|
| auto renew period | 意味(仕様の定義)登録期間が切れたあと、サーバーが自動で延長する猶予期間 |
| client hold / server hold | 意味(仕様の定義)DNSの委任情報を公開しない状態 |
| redemption period | 意味(仕様の定義)削除要求を受けたが、復元して取り消せる余地が残っている状態 |
| pending restore | 意味(仕様の定義)redemption periodを経て、復元の処理が進んでいる状態 |
| pending delete | 意味(仕様の定義)削除要求を受けたが処理が終わっていない状態。ドメインではDNSで公開されなくなっても、レジストリのデータベースから消えていない場合がある |
client transfer prohibited client delete prohibited のように prohibited が付く語は、更新や移管の操作を拒否する設定です。上の出力で .com の予約ドメインに並んでいたのも、この種の語でした。期限とは別の話で、付いているから更新できないという意味にはなりません。
台帳の見方として、期限日が過ぎているのに redemption period や client hold が出ている行は、最初に見る行です。猶予期間の長さや、猶予中の更新に追加費用がかかるかどうかは、レジストリとレジストラの案内で確かめます。
Claude Codeに任せる範囲と設定
台帳の作成そのものはスクリプトで決まるので、Claude Codeには「実行して結果を読み、人に渡す一覧を整える」ところを任せます。ドメインの一覧をHostingerのようなレジストラから取るところは、Hostinger MCPでドメインとDNSを管理する手順と組み合わせられます。
まず、実行をいちいち確認されないように、スクリプトの呼び出しだけを許可します。ルールの書き方は、Bash(npm run *) のようにコマンドと * を並べる形です。.claude/settings.json に次を置きます。
{
"permissions": {
"allow": [
"Bash(python3 rdap_expiry.py *)"
]
}
}次に、CLAUDE.mdに扱いの規約を書きます。CLAUDE.mdはセッションの開始時に読み込まれる永続的な指示で、台帳の作業に共通する決まりを毎回伝え直さずに済みます。
# ドメイン台帳
- 台帳の更新は `python3 rdap_expiry.py domains.txt > ledger.csv` で行う
- 期限日は RDAP の expiration の値だけを使い、推測で補わない
- note が空でない行は、理由ごとに分けて報告する
- domains.txt を書き換えるときは、追加・削除したドメインを先に示す実行後の確認は、Claude Codeに次の順で頼みます。
ledger.csvの行数が、domains.txtのコメントと空行を除いた行数と一致するかを数えさせるdays_leftが60以下の行を、期限が近い順に表で出させる- noteが空でない行を、前節の切り分けの分類ごとにまとめさせる
1つ目の数え直しが、取りこぼしを見つける検査になります。一覧のドメイン数と台帳の行数が合わなければ、スクリプトか入力ファイルのどちらかに欠落があります。
人が持つのは、閾値の決定、bootstrap に該当 TLD なし の行の手入力、レジストラ別の更新手順の備考です。更新の操作そのものはClaude Codeに任せず、人が行います。台帳は「いつ、誰が、何を更新するか」を決める材料で、決定は人に残します。
台帳をサポート終了日で作る方法はendoflife.date APIとClaude Codeの突合に、ドメインの漏えい確認はHave I Been Pwnedとの連携にあります。期限と同じ一覧に並べると、ドメインごとの点検が1か所で回ります。
定期実行にして更新漏れを避ける
台帳は1回作って終わりでは意味がありません。残り日数は実行日で変わるので、週に1回ほど自動で再生成します。cronなら次の1行です。
0 9 * * 1 cd /path/to/ledger && python3 rdap_expiry.py domains.txt > ledger.csv出力のCSVはGitで管理すると、期限日が前回から動いた行(更新された行)が差分として見えます。期限日が伸びていれば更新済み、動いていなければ未更新です。60日以内の行が出たら、担当者に知らせる運用を足します。
まとめ
ドメインの有効期限は、RDAPの応答の events から eventAction が expiration の要素を探して取ります。問い合わせ先はIANAのdns.jsonから引き、引けないTLDや取得失敗の行は、一覧から落とさず理由つきで残します。
Claude Codeに渡すのは、スクリプトの実行許可とCLAUDE.mdの規約、行数の突き合わせです。期限の判断と、RDAPで引けないドメインの手入力は人が持ちます。