マルチクラウドネットワーク運用の頭痛の種
複数のホスティングプロバイダー、ベアメタルラック、リモートワークステーションにまたがるサーバー同士を接続する作業は、あっという間に煩雑化します。あるプロジェクトでは、私たちのインフラは2つのAWSリージョン、3台の格安Hetznerサーバー、そしてオンプレミスのテスト用クラスターに分散していました。新しいマシンを1台プロビジョニングするだけでも、何時間もの退屈な手作業が発生していたのです。SSH接続を確保するためだけに、頻繁にiptablesルールを書き換え、壊れやすいOpenVPN証明書を同期し、不調なIPsecハンドシェイクのトラブルシューティングを行い、クラウドのファイアウォールに穴を開け続ける日々でした。
ポート22をインターネットに直接公開することは、自らトラブルを招くようなものです。ボットネットによる自動化されたブルートフォース攻撃がパブリックIPに対して毎日何千回も押し寄せていました。
従来のハブ&スポーク型VPNも解決策にはなりませんでした。フランクフルトにある中央ゲートウェイを経由させることで、本来なら40msで直接通信できるはずの米国ノード間のレイテンシが190msという低速なホップへと悪化し、ギガビット回線にもかかわらず転送速度はわずか150Mbps前後に制限されてしまったのです。私たちに必要だったのは、認証・暗号化されたトンネルを通じてノード同士が直接ルーティングし合える、ポイントツーポイントのメッシュ接続でした。
TailscaleとHeadscaleのアーキテクチャを理解する
Tailscaleは、WireGuardを自動NATトラバーサル(STUN/DERP)でラップすることにより、P2P(ピアツーピア)ネットワーキングを手軽に利用できるようにしました。まさに「設定いらずで動く」ツールです。しかし、標準のTailscaleは、暗号化キーの配布やネットワークトポロジーの管理を独自のプロプライエタリな協調サーバー(coordination server)に依存しています。厳格なデータ主権のルールがあるチームや、HIPAA規制対象のペイロードを扱う組織、あるいはアカウント数に応じて膨らむSaaS利用料を抑えたい企業にとって、接続の調整を外部ベンダーに委ねることは受け入れがたい制約となります。
この問題を解決するのがHeadscaleです。Headscaleは、Go言語で書かれたTailscaleコントロールサーバーのクリーンなオープンソース再実装です。フリート全体で公式のTailscaleクライアントバイナリをそのまま使い続けながら、コントロールプレーンを自前でホストできます。
このアーキテクチャは、大きく2つのレイヤーに分かれています。
- コントロールプレーン(Headscale):公開鍵の配布、マシン登録の追跡、ネットワークACLの適用、プライベートCGNATブロック(
100.64.0.0/10など)からの静的IPアドレスの割り当てを行います。ノードは協調メタデータのやりとりのためだけにHTTPS経由でHeadscaleと通信します。 - データプレーン(WireGuard):実際のアプリケーションペイロードがHeadscaleサーバーを通過することはありません。2つのノードがコントロールプレーンを通じて鍵と検出エンドポイントを交換すると、カーネルレベルで直接WireGuardトンネルが確立されます。スループットは回線速度の上限(ワイヤレート)まで引き出されます。
Headscaleコントロールサーバーのセットアップ
パブリックIPv4アドレスを持ち、DNSレコード(例:vpn.yourdomain.com)が設定された小規模なLinuxインスタンスが1台必要になります。Headscaleはトラフィックのアクティブな転送ではなく接続の協調のみを処理するため、月額4〜6ドル程度の安価なVPS(1 vCPU、1 GB RAM)でも、500台以上の接続マシンからなるフリートを容易に管理できます。
1. Docker ComposeでHeadscaleをデプロイする
設定ファイルと永続化データ用のディレクトリを作成します:
mkdir -p /opt/headscale/config /opt/headscale/data
cd /opt/headscale
公式の設定テンプレートをダウンロードします:
wget -O /opt/headscale/config/config.yaml \
https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml
/opt/headscale/config/config.yamlを編集し、ドメイン、ポート、内部IPの割り当てを設定します:
server_url: https://vpn.yourdomain.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 0.0.0.0:50443
ip_prefixes:
- 100.64.0.0/10
- fd7a:115c:a1e0::/48
derp:
server:
enabled: true
region_id: 999
region_code: "headscale"
region_name: "Headscale 組み込み DERP"
stun_listen_addr: "0.0.0.0:3478"
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
続いて、/opt/headscale内にdocker-compose.ymlファイルを作成します:
version: "3.8"
services:
headscale:
image: headscale/headscale:latest
container_name: headscale
restart: unless-stopped
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
ports:
- "8080:8080"
- "3478:3478/udp"
command: headscale serve
サービスを起動します:
docker compose up -d
2. リバースプロキシとSSLの設定
Headscaleにはポート443での適切なTLS終端が必要です。以下は、WebSocketとHTTP/2のサポートを有効にした実戦向けNginxバーチャルホスト設定です:
server {
server_name vpn.yourdomain.com;
listen 443 ssl http2;
ssl_certificate /etc/letsencrypt/live/vpn.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vpn.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
3. ユーザーと事前認証キーの作成
Headscaleではマシンをユーザー単位でグループ分けします。次のコマンドで初期の管理用ユーザーを作成します:
docker exec -it headscale headscale users create infra-team
確認画面を手動で操作することなくノードの追加を自動化するために、48時間有効な再利用可能事前認証キー(pre-auth key)を生成します:
docker exec -it headscale headscale preauthkeys create \
--user infra-team \
--reusable \
--expiration 48h
生成された文字列を保存しておきます。このトークンは、新しいノードをメッシュに参加させるたびに指定します。
Linuxサーバーをメッシュに参加させる
参加対象のLinuxサーバー(Ubuntu、Debian、AlmaLinux、Fedoraなど)にログインし、公式のインストールスクリプトを実行します:
curl -fsSL https://tailscale.com/install.sh | sh
事前認証キーを使用し、デーモンの接続先を自前のコントロールサーバーに設定します:
sudo tailscale up \
--login-server https://vpn.yourdomain.com \
--authkey <YOUR_PREAUTH_KEY> \
--accept-routes
ホストが登録され、仮想IPが取得できたことを確認します:
tailscale ip -4
tailscale status
2台目のサーバーでも同様の手順を繰り返します。これで両ノードは内部の100.64.0.xアドレス経由で相互に通信できるようになります。パケットはUDP経由でノード間を直接行き交い、手動でのポート転送を行うことなく対称NAT(Symmetric NAT)を越えて通信が確立されます。
Kubernetesインフラを接続する
外部マシンからプライベートなクラスターAPIやサービスエンドポイントにアクセスするには、通常、大規模な踏み台(バスティオン)ホストをデプロイするか、高額なクラウドのロードバランサーを用意する必要があります。Tailscaleをクラスター内部でサブネットルーター(Subnet Router)として動作させれば、この課題をスマートに解決できます。Headscaleメッシュ上のあらゆるノードが、あたかもPodネットワーク自体に常駐しているかのように、KubernetesのClusterIP(10.96.0.0/12など)やPodネットワーク(10.244.0.0/16など)と通信できるようになります。
1. サブネットルーターのマニフェストをデプロイする
ネームスペースを作成し、認証トークンをSecretに保存します:
kubectl create namespace tailscale
kubectl create secret generic tailscale-auth \
--namespace=tailscale \
--from-literal=TS_AUTHKEY=<YOUR_PREAUTH_KEY>
ルーターPodをデプロイします。--advertise-routes引数には、使用しているクラスターの実際のサービスCIDRおよびPod CIDRが反映されていることを確認してください:
apiVersion: apps/v1
kind: Deployment
metadata:
name: tailscale-subnet-router
namespace: tailscale
spec:
replicas: 1
selector:
matchLabels:
app: tailscale-subnet-router
template:
metadata:
labels:
app: tailscale-subnet-router
spec:
containers:
- name: tailscale
image: tailscale/tailscale:latest
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
add:
- NET_ADMIN
env:
- name: TS_AUTHKEY
valueFrom:
secretKeyRef:
name: tailscale-auth
key: TS_AUTHKEY
- name: TS_LOGIN_SERVER
value: "https://vpn.yourdomain.com"
- name: TS_KUBE_SECRET
value: "tailscale-state"
- name: TS_USERSPACE
value: "false"
- name: TS_EXTRA_ARGS
value: "--advertise-routes=10.96.0.0/12,10.244.0.0/16"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tailscale-subnet-router
namespace: tailscale
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "get", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tailscale-subnet-router
namespace: tailscale
subjects:
- kind: ServiceAccount
name: default
namespace: tailscale
roleRef:
kind: Role
name: tailscale-subnet-router
apiGroup: rbac.authorization.k8s.io
2. アドバタイズされたルートの承認
Headscaleは、明示的に承認するまでルートのアドバタイズをブロックします。承認待ちのルート一覧を確認します:
docker exec -it headscale headscale routes list
Kubernetesサブネットルートに割り当てられた数値IDを確認し、有効化します:
docker exec -it headscale headscale routes enable -r <ROUTE_ID>
これで、ノートPCやリモートサーバーからcurl http://10.96.x.x:8080を実行して、Kubernetesの内部サービスに直接アクセスできるようになりました。NodePortも、パブリッククラウドの高価なロードバランサーの請求も不要です。
運用のポイントと実践的な知見
Headscaleをセルフホストすることで、脆弱になりがちな従来のVPNトポロジーを、回復力に優れた直接P2P接続へと置き換えることができます。プライベートルーティングテーブルやマシンの識別キーの完全な所有権を維持しながら、純粋なWireGuardの高速スループット(標準的なギガビット回線で1ミリ秒未満のオーバーヘッド、実効800Mbps超の転送速度)を享受できます。
より大規模なフリートに展開する前に、cronジョブを設定してdb.sqliteの毎日のバックアップを実施してください(ノード数が1,000台を超える場合はマネージドPostgreSQLへの移行を検討してください)。また、config.yamlでMagicDNSを有効にして人間が読みやすいホスト名を利用できるようにし、本番以外のノードが重要なデータベースにアクセスできないよう厳格なACLポリシーを策定しておきましょう。

