TailscaleをDockerに統合:ポート開放不要で安全なマルチクラウドネットワークを構築する

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

深夜2時のマルチクラウド接続障害

午前2時15分、PagerDutyのアラート画面が一斉に点灯しました。Hetzner VPS上で稼働するデータ同期ワーカーが、AWS EC2上のPostgreSQLクラスタへの書き込み時にconnection timed outエラーを頻発させていたのです。双方ともDocker上で稼働していました。それまでチームは構築工数を削減するため、パブリックIP経由で接続し、Security GroupでIPホワイトリストを設定する運用を選択していました。

しかし、Hetznerの定期ネットワークメンテナンスに伴いIPアドレス帯が再割り当てされたことで障害が発生しました。AWS側のホワイトリストは即座に無効化され、注文処理パイプライン全体が45分間にわたり停止する事態に陥ったのです。とはいえ、ポート5432をインターネットに全開放すれば、15分も経たないうちにShodanなどのボットにスキャンされる危険があります。かといってAWS NAT GatewayやDirect Connectを導入すると、クラスタごとに毎月少なくとも32ドルの追加コストが発生します。こうして、メッシュ型内部ネットワークの導入が不可欠となったのです。

サーバー間Dockerコンテナ接続における3つのアプローチ

コンテナクラスタがAWS、DigitalOcean、Hetzner、あるいはオフィスのオンプレミスサーバーに分散している場合、一般的に以下の3つの選択肢があります:

  • 手法1:パブリックポート開放+IPホワイトリスト(暫定対応)
    コンテナのポートをパブリックIPにマッピング(-p 5432:5432)し、UFWやSecurity GroupでIP制限をかける方法。2分で構築できますが、IP変更時のメンテナンス負荷が非常に高くなります。
  • 手法2:ホストOSにWireGuardを直接インストール(従来のインフラ構成)
    各サーバーにWireGuardをセットアップし、wg0.confを作成した上で、Docker bridgeからVPNへトラフィックを転送するiptablesを設定する方法。パフォーマンスは高いものの、OSアップデート時にDockerのネットワークスタックと競合しやすい欠点があります。
  • 手法3:Docker内でTailscale Sidecarを実行(コンテナレベルのメッシュ)
    各サービススタックに専用のTailscaleコンテナを配置します。アプリケーションコンテナはnetwork_mode: "service:..."ディレクティブを介してTailscaleのネットワークスタックに直接アタッチします。

3つのアプローチの詳細比較

評価項目 パブリックポート+ホワイトリスト ホストOS上のWireGuard Tailscale Sidecarコンテナ
セキュリティ 低(ファイアウォール設定ミスでポート露出の危険あり) 非常に高い 非常に高い(ゼロトラスト)
スケール時の保守性 5台を超えると悪夢 キー管理とNAT設定に工数がかかる Control Planeにより100%自動化
ホストサーバーへの影響 ホスト上で直接ポート開放 カーネルやiptablesへの深い介入 Docker内に完全に隔離
設定の複雑さ 極めて低い 中〜高 docker-compose.yml 1ファイルのみ

なぜホスト直接インストールよりTailscale Sidecarが優れているのか?

以前、Ubuntu 20.04から22.04へアップグレードした際、サーバーのiptablesテーブルがリセットされる事態が発生しました。Docker bridgeとwg0間の転送ルールが完全に消去され、3つのマイクロサービス間の接続がすべてダウンしました。チームは深夜にiptablesのデバッグのためだけに3時間もログを追い続けることになりました。

Sidecarパターンはこの問題を根本から解決します。ネットワークがアプリケーションと一緒にパッケージ化されるためです。コンテナを新しいVPSに移行したり、クラウドプロバイダー間を移動したりする場合でも、docker compose up -dを実行するだけで即座に通信が開通し、基盤となるホストサーバーの設定に一切依存しません。

Docker ComposeによるTailscale Sidecarの設定手順

安全に通信させる必要がある2つのサービスがあると仮定します:

  • Server A(AWS EC2):PostgreSQLデータベースを実行。
  • Server B(Hetzner VPS):Server Aからデータをクエリする必要があるWorkerサービスを実行。

ステップ1:Tailscale Auth Keyの生成

Tailscaleの管理画面にアクセスし、Settings > Keys > Generate auth keyを選択します。Reusableオプションを有効にし、タグ(例:tag:server)を付与します。タグを付与することでキーが90日で失効するのを防ぎ、事前に定義したACLに基づいて自動的に権限が付与されます。

ステップ2:Server Aでのデータベースクラスタの設定

Server A上にdocker-compose.ymlファイルを作成し、以下の内容を記述します:

services:
  tailscale-db:
    image: tailscale/tailscale:v1.64.0
    container_name: tailscale-db
    hostname: db-node
    environment:
      - TS_AUTHKEY=tskey-auth-kXXXXX-XXXXXXXXXXXX
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_EXTRA_ARGS=--accept-routes=false
    volumes:
      - ./tailscale-state:/var/lib/tailscale
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - NET_ADMIN
      - NET_RAW
    restart: unless-stopped

  postgres:
    image: postgres:15-alpine
    container_name: production-postgres
    network_mode: "service:tailscale-db"
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: SecretPassword123!
      POSTGRES_DB: app_db
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped
    depends_on:
      - tailscale-db

volumes:
  pgdata:

3つの重要な技術的ポイント:

  • ports:を定義しない:PostgreSQLコンテナはホストにポートを公開しないため、外部のあらゆるポートスキャナーから不可視となります。
  • Network Namespaceの共有:network_mode: "service:tailscale-db"の記述により、PostgreSQLはTailscaleの内部IP上で直接リッスンするようになります。
  • ステートディレクトリのマウント:./tailscale-stateボリュームはノードの識別情報(Identity)を保持します。このボリュームがないと、コンテナ再起動のたびにTailscale管理画面上に新しいノードが重複して生成されてしまいます。

ステップ3:ノードの起動と動作確認

Server Aでスタックを起動します:

docker compose up -d
docker compose logs -f tailscale-db

ログにBoot: ... Success; node is db-node (100.x.y.z)という行が表示されれば、データベースクラスタは安全なメッシュネットワークに正常に参加しています。

ステップ4:Server Bからの接続テスト

同じTailscaleネットワークに参加済みのServer Bから、直接通信経路とレイテンシを確認できます:

# MagicDNS経由で直接pingを実行。EUのデータセンター間でも通常レイテンシは20〜35ms程度
tailscale ping db-node

# WireGuardプライベートネットワーク経由でPostgreSQLのポートを確認
nc -zv db-node 5432

画面にConnection to db-node 5432 port [tcp/*] succeeded!と表示されれば成功です。Worker側の接続文字列は、postgres://admin:SecretPassword123!@db-node:5432/app_dbを指定するだけで通信可能になります。

本番環境運用のベストプラクティス

  • 最小権限の原則:cap_add: [NET_ADMIN, NET_RAW]のみを付与します。コンテナブレイクアウトのリスクを避けるため、privileged: trueの使用は控えてください。
  • MagicDNSの活用:ノードのIPアドレスが再割り当てされた際に設定ファイルの変更が発生しないよう、100.x.y.zといったIPアドレスのハードコードを避け、ホスト名db-nodeを使用します。
  • DERP Relayの監視:tailscale statusコマンドで接続がdirect(P2P WireGuard)かrelay(DERP)かを確認できます。もしリレー経由になっている場合は、最大スループットを確保するためにアウトバウンドのUDPポート41641を開放してください。
Share: