Raspberry Pi 2台構成のWiFi APでSSIDが起動時に出ない問題を解決した

構成 自宅NWの冗長化目的で Raspberry Pi を2台使い、WiFiアクセスポイントを組んでいる。 ホスト名 チャンネル SSID pi-1 1 your-ssid pi-2 11 your-ssid 同一SSIDで別チャンネルにする構成は ESS(Extended Service Set) と呼ばれ、クライアントが自動的に電波の強い方に接続する。WPA2-PSKで普通に成立する冗長化構成。 ブリッジ構成で bridge interface(br0)にWiFiインタフェース(wlan0)を参加させ、hostapdで電波を飛ばしている。 問題 OS再起動後、SSIDが片方または両方スキャンに出ない。 systemctl status hostapd は active (running) エラーログも一見出ていない hostapdを手動で再起動すると出る 調査 チャンネルの干渉を疑う 最初にhostapd.confのチャンネル設定を確認した。 2.4GHz帯で実際に干渉しないチャンネルは 1、6、11 の3つだけ。元の設定は6と7だったため、ほぼ完全に干渉していた。 ch6 --|-- ch7 --|-- ← 隣接していてほぼ被る チャンネルを1と11に変更した。 # pi-1 channel=1 # pi-2 channel=11 BSSIDレベルで電波を確認 sudo iw dev wlan0 scan | grep -E "BSS |SSID|freq|signal" BSS xx:xx:xx:xx:xx:xx(on wlan0) freq: 2462 signal: -15.00 dBm SSID: your-ssid BSS xx:xx:xx:xx:xx:xx(on wlan0) freq: 2412 signal: -68.00 dBm SSID: your-ssid 両台ともビーコンは出ていた。ESSとして動作は成立している。 ...

May 6, 2026 · 2 min

自宅NWをKeepalivedとdnsmasqで作り直した話

背景 自宅のネットワーク構成が辛かった。 具体的には以下の問題があった。 ルータが1台構成で、死んだら自宅NW全滅 メンテナンスで定期的に落としたいとかあっても、コストが大きい DNS/DHCPがルータに同居していて、役割が混在している WireGuardで外部接続していたが、外部からの入り方がだるい。 特に「冗長化したい」という気持ちは何年も前からあったが、DNSとDHCPの同期という難問の前に何度も挫折してきた。 結論からいえば、DNS,DHCPの冗長化は諦めた。 諦めた上で出口のルータとDNS、DHCPは役割を分けた上で出口のルータとWIFIルータのみ冗長化を実施した。 設計方針 過去の失敗から学んだ一番の教訓はやはり「DNS/DHCPの冗長化は難しい」ということだ。 理論上可能ではあるが、私は万年初級自宅インフラエンジニアでいつまで経ってもできる目処が立たない。 それでいて、生成AIという過ぎた兵器を持ってしても、この問題は解決しなかったので、難しいというよりは不可能とした。 2台のサーバでDHCPを冗長化しようとすると、リースの同期問題が必ず発生する。同期ツールを使ったりしたが、どうしても同期しきれなかった。 そこで今回は思い切って諦めた上で役割を分離する方針にした。 ルータ(冗長化) → GWとTailscaleだけ担当 AP → hostapdのみ DNS/DHCP → 専用機1台に集約(冗長化しない) DNSとDHCPは単一障害点になるが、自宅ラボであれば許容範囲だと判断した。 てかどうしようもない。 構成 使用機材はすべてRaspberry Pi。封印していたタワーからも引っ張り出した。 ホスト 役割 ルータA (MASTER) Keepalived + Tailscale ルータB (BACKUP) Keepalived + Tailscale DNS/DHCP機 dnsmasq専用 AP x2 WIFI冗長化 ネットワークセグメントはひとつ。VIPをデフォルトゲートウェイ兼DNSとして各クライアントに配布する。 Keepalivedの設定 まずルータ2台にKeepalivedを入れる。 sudo apt install -y keepalived MASTER側の設定: vrrp_instance VI_1 { state MASTER interface lan-if virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass xxxxxxxx } virtual_ipaddress { 192.168.xx.1/24 } } BACKUP側は state BACKUP と priority 90 に変えるだけ。シンプルだ。 ...

April 18, 2026 · 2 min

ドメイン購入後のやり残し作業 - メールとDNS設定編

wasutech.dev を公開したはいいが、メール周りを何もしていなかった。放置するとなりすましに悪用される可能性があるので、Cloudflare Email Routing・SPF・DMARCを設定した。 Cloudflare Email Routing 自前のメールサーバーを建てるのはコストがかかる。Cloudflareには Email Routing という機能があり、@wasutech.dev 宛のメールを既存のGmailなどに転送できる。無料。 仕組みとしては、Cloudflareが自動でMXレコードを追加し、受信したメールを指定のアドレスへ転送する。 dig MX wasutech.dev # → isaac.mx.cloudflare.net 等が返ってくる MXレコードはドメインレベルの情報なので、転送先のメールアドレスは外部に公開されない。dig で見えるのはCloudflareのサーバーだけで、「どこに転送しているか」は誰にもわからない。 設定はCloudflareのダッシュボードから数クリックで完了する。 転送先のアクションは3種類から選べる。 アクション 動作 Forward to email 指定のメールアドレスへ転送 Drop 受信して即破棄。転送しない Send to Worker Cloudflare Workersで独自処理 今回はメールを受け取る必要がないため Drop に設定した。スパム対策にもなるし、転送先アドレスを管理する必要もない。Workers連携は受信メールをSlackに流したり、自動返信を実装したりする場合に使う。 SPF - 送信元を証明するレコード SPFとは SPF(Sender Policy Framework) は、「このドメインからメールを送信していいサーバーはどこか」を定義するDNSレコード。 なぜ必要か。メールのプロトコル(SMTP)は設計上、送信元アドレスを自由に詐称できる。つまり誰でも [email protected] を名乗ってメールを送れる。SPFはこれを受信側が検証できるようにする仕組み。 受信側のメールサーバーは、届いたメールの送信元IPアドレスを確認し、そのドメインのSPFレコードに記載されたIPと照合する。一致しなければ怪しいメールとして処理できる。 設定したレコード v=spf1 include:_spf.mx.cloudflare.net ~all 各要素の意味: 要素 意味 v=spf1 SPFバージョン1 include:_spf.mx.cloudflare.net CloudflareのメールサーバーからのSMTPを許可 ~all 上記以外は「疑わしい」扱い(ソフトフェイル) ~all は疑わしいメールを迷惑メール扱いにする。-all にすると完全拒否になるが、DMARCと組み合わせて制御するのが一般的。 確認: dig TXT wasutech.dev # v=spf1 include:_spf.mx.cloudflare.net ~all DMARC - SPFの結果を使って何をするか決めるレコード DMARCとは DMARC(Domain-based Message Authentication, Reporting, and Conformance) は、SPF(やDKIM)の検証結果に基づいて、受信側が「そのメールをどう扱うか」を指示するレコード。 ...

April 14, 2026 · 1 min

Cloudflareの全通知をDiscord Webhookに飛ばす

CloudflareのNotificationsはUIから1個ずつ設定するのがとにかくだるい。 種別も多いし、手動でDiscord Webhookを登録していくのは非現実的。 Cloudflare APIとWorkersを組み合わせて全通知を一括登録する。 構成 Cloudflare Notifications ↓ Cloudflare Worker (受け口) ↓ Discord Webhook Cloudflare NotificationsはWebhook送信に対応しているので、Workerを受け口にしてDiscordに転送する。 1. Discord Webhookを作成 通知を飛ばしたいDiscordチャンネルの設定から作成する。 チャンネル設定 → Integrations → Webhooks → New Webhook → Copy Webhook URL 2. Cloudflare Workerを作成 Cloudflareダッシュボードから Workers & Pages → Create → Hello World で作成する。 名前は cf-notify-discord など適当につけてDeploy。 エディタ画面で以下のコードに置き換える。 export default { async fetch(request, env) { if (request.method !== "POST") { return new Response("ok"); } let body; try { body = await request.json(); } catch { return new Response("invalid json", { status: 400 }); } const message = { username: "Cloudflare", embeds: [{ title: body.text?.title || "Cloudflare Notification", description: body.text?.description || JSON.stringify(body, null, 2), color: 0xF6821F, timestamp: new Date().toISOString() }] }; await fetch(env.DISCORD_WEBHOOK, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) }); return new Response("ok"); } } Discord WebhookのURLはWorkerのSettings → Variables and Secretsでシークレットとして登録する。 ...

April 12, 2026 · 3 min

歴史地図アプリを雑にk3sへデプロイした

歴史地図アプリの構成 React + TypeScript + Vite + MapLibre GL のSPA。歴史的国境データ(GeoJSON)を表示するアプリ。 データはpublic/data/以下にGeoJSONを置く構成で、.gitignoreに含めているためリポジトリには入っていない。 ちなみに、生成するスクリプトはあるが、GEMINIを利用しないといけない。 しかし、APIキーのレート制限が入ってしまったので、ローカルで生成済みのデータを持ち込むことにした。 インフラ構成 自宅のProxmox上にLXCコンテナとしてk3sクラスタを構築している。マスター1台+ノード1台の最小構成。 外部公開はNginx Proxy Manager(NPM)でポートフォワーディングしており、DuckDNSのドメインにSSL終端している。 インターネット ↓ Nginx Proxy Manager(SSL終端) ↓ k3s NodePort ↓ Pod 問題:データファイルをどう持ち込むか public/data/がgitignoreされているため、コンテナ内でgit cloneしてもデータが存在しない。 選択肢はいくつかあったが、今回はk3sのhostPathボリュームでマウントする方針にした。 だるいファイル転送 データファイルをk3sノードに転送するのが一番面倒だった。 Proxmoxのファイルアップロード → UIの制限でNG ngrok経由 → Tailscale環境のためlocalhostの名前解決失敗 結局TailscaleのIPでProxmoxホストに転送 → pct pushでLXCコンテナへ # ProxmoxホストからLXCへ pct push <CTID> /path/to/data.tar.gz /tmp/data.tar.gz # k3sマスターで解凍 mkdir -p /opt/history-map-data tar -xzf /tmp/data.tar.gz -C /opt/history-map-data 融通の効かないViteとふわふわClaude君の罠 npm run previewはデフォルトで許可ホストを制限する。Nginx Proxy Manager経由でアクセスするとBlocked requestが出る。 環境変数で全許可とかできたらよかったけど、結論だけ言うとできなかった。少なくともClaude君の指示では何をどうしても駄目だったので、最終的にvite.config.tsをデプロイ時に動的に書き換えることで回避した。 cat > /app/vite.config.ts << 'EOF' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], preview: { allowedHosts: ['your-domain.example.com'], }, }) EOF 最終的なYAML apiVersion: apps/v1 kind: Deployment metadata: name: history-map spec: replicas: 1 selector: matchLabels: app: history-map template: metadata: labels: app: history-map spec: nodeName: k3s-master containers: - name: history-map image: node:20-alpine workingDir: /app command: ["sh", "-c"] args: - | apk add --no-cache git git clone https://github.com/wasuken/history-map-app.git /app --depth=1 cat > /app/vite.config.ts << 'EOF' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], preview: { allowedHosts: ['your-domain.example.com'], }, }) EOF mkdir -p /app/public/data cp -r /data/historical /app/public/data/historical cp -r /data/modern /app/public/data/modern cp /data/translation-cache.json /app/public/data/translation-cache.json npm install npx vite build npm run preview -- --host 0.0.0.0 --port 3000 ports: - containerPort: 3000 volumeMounts: - name: map-data mountPath: /data volumes: - name: map-data hostPath: path: /opt/history-map-data type: Directory --- apiVersion: v1 kind: Service metadata: name: history-map-service spec: selector: app: history-map ports: - port: 80 targetPort: 3000 nodePort: 30080 type: NodePort nodeName: k3s-masterを指定しているのはhostPathがPodの動くノード上に存在する必要があるため。ProxmoxのNPM(Nginx Proxy Manager)から このNodePortに向けてプロキシを設定している。 ...

February 25, 2026 · 2 min