自宅NW活動誌:固定IP撤去とdnsmasq移行で名前解決を建て直した話

TL;DR 電源断からの復旧で名前解決がおかしくなり、調べていったら「dnsmasqがhosts変更を未反映」「gw-a/gw-bがローカル固定IPとdns01のDNS管理で二重管理になっていた」の2つが絡んでいたという話。ついでに「自ホスト名pingが127.0.1.1を返す」件は仕様通りで実害なしと確認した。全部片付いたので、今回は「詰んだ」話ではなく「ハマったけど直した」話。 環境 セグメント: home.local(192.168.20.0/24) DNS/DHCP: dns01(dnsmasq) Keepalived構成のGW: gw-a, gw-b クライアント側ネットワーク管理: nmcli 発生した問題 電源断→起動後、あるノード(client01)から複数ノードにSSHできなくなった。調査を進める過程で、そもそもdns01自身の名前解決(dns01という名前そのもの)が引けなくなっていることが判明。/etc/hostsに手動でエントリを足しても、他ノードから引けない状態が続いた。 原因は1つではなく、「gw-a/gw-bがローカル固定IP設定+hosts記載を持っていたこと」と「dns01側のhostsエントリ消失」が絡み合っていた。 対応1:dnsmasqがhosts変更を反映していなかった 症状: /etc/hostsにエントリを書いても、他ノードからdnsmasq経由で引けない。 原因: dnsmasqは/etc/hostsを起動時に読み込むだけで、実行中のファイル変更を自動では拾わない。dhcp-hostsfileやdhcp-optsfileなどのDHCP関連設定ファイルは変更時にSIGHUPシグナルを送ることでdnsmasqに再読み込みさせられる仕様になっており、/etc/hostsについても同様に、SIGHUPを受け取るとキャッシュをクリアした上で再読み込みする挙動になっている。つまり「編集しただけ」では反映されず、シグナルを送る操作が別途必要というのがハマりどころだった。 対処: systemctl restart dnsmasq(reload = SIGHUP相当)で即解決。 学び: /etc/hostsをdnsmasq向けに編集したら、反射的に再起動する習慣をつける。 対応2:gw-a/gw-bを固定IP運用からdhcp-host予約運用に切り替え 目的: ローカル固定IP(dhcpcd.conf/nmcli static)とdns01のDNS管理が二重管理になっていたのを解消し、IP管理をdns01(dnsmasq)側に一元化する。 手順(実施順が重要) 1. dns01側を先に設定 /etc/dnsmasq.conf(またはdhcphostsファイル)に予約を追記する。 dhcp-host=<MACアドレス>,192.168.20.3 # gw-a dhcp-host=<MACアドレス>,192.168.20.4 # gw-b MACアドレスの大文字/小文字はdnsmasqでは区別されない(AA:BB:...でもaa:bb:...でも可)。区切り文字は:固定。設定後はsystemctl restart dnsmasq。 2. クライアント側(gw-a/gw-b)を動的取得に変更 nmcli管理の場合は以下の通り。 nmcli connection show # 対象接続名を確認 nmcli connection modify "<接続名>" ipv4.method auto nmcli connection modify "<接続名>" ipv4.addresses "" nmcli connection modify "<接続名>" ipv4.gateway "" nmcli connection modify "<接続名>" ipv4.dns "" nmcli connection up "<接続名>" 確実に反映させるため最終的にreboot。 ...

July 30, 2026 · 1 min

Terraform学習1:variable/outputが分離しているのは何のためか

Terraformを触り始めると、variableとoutputが真逆の役割を持っていることはすぐわかる。だが「なぜこの2つが分離しているのか」「なぜvalidationが型チェックと別枠なのか」まで理解しないと、秘匿情報をうっかりstateに残す事故につながる。今回は自分のTerraform構成(variables.tf, outputs.tf, versions.tfのbackend部分)を題材に、設計意図を整理する。 variableは「設定とロジックの分離」のための入口 variableは、値をリソース定義本体に直書きせず、外部から注入するための仕組みだ。値を変えるたびにリソース定義そのものを編集しなくて済むようにする、という目的がある。 値の入力元は主に4つある。 default(ブロック内で指定) terraform.tfvars 環境変数(TF_VAR_xxx) -varオプション(CLI実行時) defaultの有無で意味が変わる 意味 defaultあり 共通・変更頻度が低い値。省略可能(例: aws_region, project_name) defaultなし 環境固有・必須の値。指定しないとplan/apply時にエラー(例: budget_alert_email, github_org) defaultの有無は「省略可能かどうか」の表明であり、そのままその変数の性格(共通設定か、環境固有の必須値か)を表している。 validationは型チェックとは別レイヤーの検証 variableのtypeは形式のチェックしかしない。type = stringは「文字列であること」しか保証せず、「その文字列が正しい値かどうか」は見ない。 そこを埋めるのがvalidationブロックだ。値の中身・意味をチェックする。例えばSSMパラメータ名は/始まりである必要がある、EBSルートボリュームは8GiB以上必要、といった業務ルールをここに書く。 variable "image_id" { type = string description = "The ID of the machine image (AMI) to use for the server." validation { condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-" error_message = "The image_id value must be a valid AMI ID, starting with \"ami-\"." } } validationはapply前、ローカルのplan段階でエラーを弾ける。無駄なapply実行を未然に防げるのが最大の利点で、クラウド側にリクエストが飛ぶ前に「その値はそもそもおかしい」と教えてくれる。 ...

July 25, 2026 · 2 min

WireGuard 経由で UNEXT が見れない問題を解決した話

環境 ラズパイ(Linux)がルーター兼 WireGuard クライアント 配下のデバイス(タブレット等)は eth0 経由でラズパイを通してインターネットへ ラズパイは wlan0 で ISP ルーター(192.168.x.1)に接続 全トラフィックを WireGuard(wg0)経由で VPS に流す構成 VPS は国内 VPS サービス タブレット(192.168.x.x) └─ eth0 ─ ラズパイ(ルーター) ├─ wlan0 ─ ISPルーター ─ インターネット └─ wg0 ─ VPS(国内) ─ インターネット 症状 WireGuard 経由の WiFi で UNEXT が一切見れない YouTube・Amazon Prime Video は問題なし UNEXT の生配信は見れる、VOD だけ駄目 調査 tcpdump で通信を確認 タブレットが UNEXT に接続しようとしたタイミングで tcpdump を仕掛けた。 sudo tcpdump -i eth0 -n 'src <タブレットIP> or dst <タブレットIP>' 2>/dev/null UNEXT のサーバーへの SYN が2回送られているが SYN-ACK が返ってこないことを確認。接続確立できていない。 ...

June 6, 2026 · 2 min

WireGuard 経由で UNEXT が見れない問題を調査した話【調査編】

解決編はこちら → WireGuard 経由で UNEXT が見れない問題を解決した話 | 怠惰技術ブログ 概要 WireGuard 経由の WiFi で UNEXT の VOD だけ見れないという問題を調査した。症状・仮説・コマンド・結果の思考トレースを残しておく。 最初の症状整理 UNEXT の VOD が見れない(くるくるのままタイムアウト) 生配信は見れる YouTube・Prime Video は問題なし この時点での仮説: MTU の問題(大きいパケットが通らない) QUIC(UDP)の問題 DRM 認証の問題 VPS 側のブロック Step 1: MTU を疑う WireGuard はオーバーヘッドがあるので MTU が小さくなる。大きいパケットが詰まってないか確認。 ping -M do -s 1400 8.8.8.8 結果: From 192.168.x.254 icmp_seq=1 Frag needed and DF set (mtu = 1420) ping: sendmsg: Message too long 1400 バイトは通らない。 1300 バイトは通った。MTU の壁が確認できた。 TCPMSS clamping を確認 sudo iptables -t mangle -L FORWARD -n -v TCPMSS tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp flags:0x06/0x02 TCPMSS clamp to PMTU すでに設定済みだった。TCP はケアされている。 ...

June 6, 2026 · 2 min

Cloudflare Workers でサイト監視 + Discord通知を作った

外形監視をどこに任せるか迷った。ラズパイでやるか、Cloudflareに任せるか。 外から見えるサービスの監視なら せっかくなのでCloudflare Workersにする。 自宅NWそこそこ落ちたりするので・・・。 ラズパイと比較した Cloudflare Workers ラズパイ 向いてる監視 外形監視(外からの死活確認) 内部NW監視 コスト 無料 電気代のみ メンテ ほぼゼロ たまに落ちる ローカルNW確認 ❌ ✅ 最小間隔 1分 自由 今回は wasutech.dev と blog.wasutech.dev、techblog.wasutech.dev の3つを監視したい。 Cloudflare Workers 無料枠で十分な理由 公式ドキュメント - Limits によると、無料プランは以下のとおり。 Cron Triggers: 5個まで リクエスト: 1日10万回まで CPU時間: 10ms/invocation 今回のユースケースは「HTTPリクエスト投げてステータスコード確認して Discord Webhook 叩く」だけなので、CPU時間は 2〜3ms で収まる。5分間隔で3ドメイン監視しても 1日864リクエストなので余裕。 コード全文 // src/index.ts const TARGETS = [ { name: "wasutech.dev", url: "https://wasutech.dev" }, { name: "blog.wasutech.dev", url: "https://blog.wasutech.dev" }, { name: "techblog.wasutech.dev", url: "https://techblog.wasutech.dev" }, ]; export default { async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext) { const results = await Promise.allSettled( TARGETS.map((t) => check(t.name, t.url)) ); const failures = results .map((r, i) => ({ result: r, target: TARGETS[i] })) .filter(({ result }) => result.status === "rejected" || (result.status === "fulfilled" && !result.value.ok) ); if (failures.length > 0) { const lines = failures.map(({ result, target }) => { const detail = result.status === "rejected" ? (result.reason as Error).message : `HTTP ${(result.value as Response).status}`; return `🔴 ${target.name} | ${detail}`; }); await notify(env.DISCORD_WEBHOOK, lines.join("\n")); } }, }; async function check(name: string, url: string): Promise<Response> { const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(10000), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res; } async function notify(webhookUrl: string, msg: string) { await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: msg }), }); } interface Env { DISCORD_WEBHOOK: string; } # wrangler.toml name = "wasutech-monitor" main = "src/index.ts" compatibility_date = "2025-01-01" [triggers] crons = ["*/5 * * * *"] Promise.allSettled を使った理由 Promise.all だと1つが失敗した時点で残りを待たずに throw する。 Promise.allSettled なら3つ並列でチェックして、全部の結果を集めてからまとめて通知できる。 1メッセージにまとめることでDiscordがスパムにならない。 ...

April 13, 2026 · 2 min

Proxmox LXCコンテナでJupyterLab環境構築 - 試行錯誤とトラブルシューティング

はじめに Proxmox上にJupyterLabのLXC環境を構築しました。当初はGeminiに任せて試行錯誤しましたが、最終的にベストプラクティスに辿り着いたので、その過程と解決策をまとめます。 構築の基本方針 当初は「Root + グローバル環境」で構築しようとしましたが、最終的に**「専用ユーザー + 仮想環境(venv)」**による安全でクリーンな構成に落ち着きました。 最終構成 OS: Ubuntu 24.04 LTS (LXC Container) ユーザー: jupyter (非Root運用) Jupyter: JupyterLab (v4.x) 環境: /opt/jupyter/venv (OSと分離した仮想環境) 環境構築手順 1. OSの準備 Ubuntu 24.04の最小構成に必要なパッケージをインストールします。 apt update && apt upgrade -y apt install -y python3-full build-essential python3-fullが重要です。これがないと後述するPEP 668の問題に直面します。 2. 専用ユーザーとディレクトリの作成 # 専用ユーザー作成 useradd -m -s /bin/bash jupyter # Jupyter本体用のディレクトリ準備 mkdir -p /opt/jupyter chown jupyter:jupyter /opt/jupyter 3. 仮想環境の構築 jupyterユーザーとして、OSの制限を受けない独立した環境を作ります。 su - jupyter python3 -m venv /opt/jupyter/venv source /opt/jupyter/venv/bin/activate # JupyterLabとカーネルのインストール pip install jupyterlab ipykernel pandas 4. systemdによるデーモン化 /etc/systemd/system/jupyter.serviceを作成します。 ...

January 26, 2026 · 2 min