Docker:1つのコンテナをVPN経由にし、もう1つは直接のままにする
トンネルに専用のコンテナを与え、必要とするサービスだけにその名前空間を共有させ、そのうえで所有者に付随するもの——ポート、localhost、DNS——を考慮します。
一台のホスト、二つのワークロード、そしてそれらを分ける要件があります。商用VPNから出ていく必要があるダウンロードクライアントと、ホストの接続を使い続ける必要がある管理インターフェースです。VPNクライアントをホストに導入すると要件は即座に満たされなくなります。両方のワークロードが一つの出口を共有してしまうからです。実際に機能するパターンは、トンネルを専用のコンテナに移し、それを必要とするワークロードだけにそのコンテナのネットワーク名前空間へ参加させることです。それ以外は通常のDockerの経路を保ち、続く詳細は名前空間を誰が所有するかの帰結です。
共有された名前空間がルールを変える理由
他のコンテナの名前空間に参加するコンテナは、独自のネットワークスタックを持ちません。参加先のコンテナと同じインターフェース、同じルーティングテーブル、同じloopbackを見ます。だからこそこのパターンは有用です。ワークロードは通常どおり名前解決して接続し、下のトンネルがアプリケーションに知られずにトラフィックを捕捉します。代償は、ネットワークに関するあらゆる設定の所有者が一つになり、参加するコンテナがその所有者ではないことです。
この所有権のルールは、ほとんどの設定上の疑問に問われる前に答えます。設定がネットワークスタックに属するなら、それはサイドカー側のものです。プロセスを記述するものなら、ワークロード側に残ります。
loopback は人を驚かせる部分です。参加するコンテナの内部では、localhostはサイドカーのloopbackに解決され、自分専用のプライベートなものにはなりません。そのため、分離を期待して127.0.0.1にバインドするサービスは、いまやそのアドレスをトンネルコンテナと共有します。
名前空間を所有するコンテナでポートを公開する
ポートのマッピングは、待ち受けプロセスがもう一方のコンテナで動いていても、名前空間の所有者に属します。マッピングはサイドカー側で宣言し、ワークロードは自分のプロセス設定をそのまま保ちます。参加するサービス側のマッピングは効果を持ちません。エンジンが拒否するか、コンテナの起動方法によっては黙って無視されるかであり、だからこそ一瞬前まで到達できたインターフェースが、パターンを適用した途端に消えます。
services:
gluetun:
image: qmcgaw/gluetun
cap_add: [NET_ADMIN]
ports: ["8080:8080"]
client-vpn:
image: example/client
network_mode: "service:gluetun"
client-direct:
image: example/client
ports: ["8081:8080"]このブロックは三つのサービスではなく三つの決定として読んでください。サイドカーがトンネルと公開ポートを所有し、参加するクライアントはネットワークを一切宣言せず、直接のクライアントはデフォルトネットワーク上に独自のマッピングを保ちます。サイドカーがインターフェースを作成しルートを変更できるのはNET_ADMINのケーパビリティがあるからであり、それがこのコンテナが提供するワークロードより多くの権限を必要とする理由です。
直接のままのサービスは独自のネットワークを保つ
トンネルを通してはいけないワークロードは、デフォルトブリッジか独自のユーザー定義ネットワークを、独自の公開ポートと独自の出口とともに保ちます。二つのコンテナが名前で会話できるようにサイドカーのネットワークへ接続したくなりますが、その一ステップでトラフィックは黙ってトンネルへ移ります。互いに到達する必要があるなら、別のユーザー定義ネットワークを作り、両方をそこへ接続し、サイドカーだけを外向きの唯一の経路として残してください。
二つの問いを切り離しておいてください。どのコンテナ同士が到達できるか、そしてどれがどこから出るかです。これらを混ぜることが、この設計が再びホスト全体のVPNに戻る最も一般的な道です。
最初の一時間のための障害マトリクス
最初の試みのほとんどは四つのうちのどれかで失敗し、それぞれ原因が異なります。この順に確認してください。最初の二つがほぼすべての報告を説明するからです。
| Symptom | Likely cause |
|---|---|
| Web UI unreachable after the change | Port mapping declared on the joining container instead of the sidecar |
| Both containers show the provider's exit address | The direct service was attached to the sidecar network |
| Internal names stop resolving | Resolver settings live on the host, not in the shared namespace |
| Health check fails but the app works | The check probes localhost, which now means the sidecar |
四行目は、人がアプリケーションのバグだと誤診するものです。ワークロード自身のloopbackに対して書かれたプローブは、いまやトンネルコンテナを検査しており、失敗するか、まったく別のサービスを報告します。
一つのホスト名だけがトンネルを必要とする場合
フルトンネルは過剰なことがあります。内部名の一つは社内VPNを通す必要がある一方で、ローカルのソケットやディスプレイ接続は動き続けなければなりません。コンテナ内のフルトンネルはその経路とホスト宛てのすべてを壊すので、必要なプレフィックスだけをルーティングします。クライアントの周りの分割ルーティングラッパーがそれを行い、指定したホストをトンネル経由で送り、それ以外には以前のルートをそのまま残します。
一つのサービスに到達するだけでよく、コンテナのトラフィック全体を移そうとしていないときにこれを選びます。サイドカーとは別の道具であり、別の問いに答えます。
検証し、それから再起動に耐えるようにする
各コンテナに、どのアドレスから来ているように見えるか尋ねます。トンネル側はプロバイダーのアドレスを、直接側はホストの出口を出力するはずです。同じ答えが返るなら、直接のサービスが誤った名前空間を継承したか、ホストレベルのVPNがいまもすべてを捕捉しているかで、この二つの修正は異なります。composeを編集するたびに確認をやり直してください。サービスブロックを一つコピーするだけで問題は再発します。
後から壊れやすい点が三つあります。DNSは名前空間の所有者に従うので、リゾルバーと検索ドメインの設定はホストではなくサイドカーに置きます。ポート転送を使うVPNは再接続時に待ち受けポートを変えるので、プロバイダーの転送ポートとコンテナ側の待ち受けポートをクライアント設定かポートマッピングへ書き戻す必要があり、ふつうはプロバイダーのAPIを読む小さな更新ツールが行います。そしてlocalhostに対して書かれたプローブは、いまやワークロードではなくトンネルコンテナに問い合わせています。
パターンを書き留めてください。どのコンテナが名前空間を所有し、どのポートがそれに属するかも含めます。次の人はサービスを追加し、誤ったブロックをコピーし、この記事の最初の障害を再現するでしょう。