Skip to content
Published on

Ingress で TCP/UDP サービスを公開する — L4 の限界と解法

Authors

はじめに

Kubernetes で Web アプリケーションを外部に公開することは比較的直感的です。Ingress リソースを作り、ホストとパスを書けば終わりです。ところがある日、運用チームからこんな依頼が来ます。「PostgreSQL を外部の分析ツールから直接つなげるように開けてほしい」「ゲームサーバーの UDP ポートを公開してほしい」「社内 DNS をクラスターから配信してほしい」。この瞬間、私たちは Ingress の本質的な限界に直面します。

Ingress API は設計上 L7、正確には HTTP と HTTPS のための抽象です。ホスト名、URL パス、HTTP ヘッダーといった L7 情報でルーティングするのがすべてです。一方、PostgreSQL のワイヤープロトコル、ゲームの UDP パケット、DNS クエリは HTTP ではない純粋な TCP/UDP トラフィックなので、Ingress リソースで表現する方法がそもそもありません。

それでも現実には、同じ Ingress コントローラーで TCP/UDP まで公開したいという要求が多くあります。別の LoadBalancer をたくさん作りたくないからです。幸い主要なコントローラーは、Ingress API の外側、つまり ConfigMap や EntryPoint、CRD といったコントローラー固有のメカニズムで L4 公開をサポートします。本記事ではその方法を整理し、こうした回避策をいつ使い、いつ専用の L4 ソリューション(MetalLB、クラウド NLB など)を使うのが正しいかの判断基準を示します。また 2026 年の後継標準である Gateway API の TCPRoute/UDPRoute がこの問題をどう一級として扱うのかも見ていきます。

Ingress は本来 L7 である

まずはっきりさせておきましょう。Ingress リソースには TCP/UDP のためのフィールドがありません。spec をいくら眺めても ruleshttp ブロックしか持たず、ポートとホスト、パスで HTTP トラフィックをルーティングします。したがって「Ingress で TCP を公開する」という言い方は厳密には誤りで、実際には「Ingress コントローラー(その背後の nginx/Traefik/HAProxy プロセス)に L4 プロキシ設定を追加で重ねる」が正確です。

   L7 (HTTP)                       L4 (TCP/UDP)
   ---------                       ------------
   Ingress リソースで表現可能       Ingress リソースで表現不可
   host/path ルーティング           ポート単位のパススルー
   TLS termination                  TLS passthrough(SNI)
        |                                |
        +----- 同じコントローラープロセス -+
              (ConfigMap / EntryPoint / CRD で L4 を追加)

この区別を明確にしておくと、後で「なぜ host ルーティングが効かないのか」といった混乱を避けられます。L4 公開ではホスト名ルーティングは(TLS SNI を使わない限り)不可能です。

ingress-nginx の TCP/UDP services ConfigMap

ingress-nginx は L4 公開のために二つの別々の ConfigMap を持ちます。tcp-servicesudp-services です。キーは外部に公開するポート番号、値は namespace/service:port 形式のバックエンド指定です。

apiVersion: v1
kind: ConfigMap
metadata:
  name: tcp-services
  namespace: ingress-nginx
data:
  "5432": "databases/postgres:5432"
  "6379": "cache/redis:6379"

UDP も同じ方式です。

apiVersion: v1
kind: ConfigMap
metadata:
  name: udp-services
  namespace: ingress-nginx
data:
  "53": "dns/coredns:53"

これで終わりではありません。ingress-nginx コントローラー自体がこの ConfigMap を読むように引数を与える必要があり、コントローラー Pod と LoadBalancer Service で該当ポートを実際に開く必要があります。Deployment の args に次を追加します。

args:
  - /nginx-ingress-controller
  - --tcp-services-configmap=ingress-nginx/tcp-services
  - --udp-services-configmap=ingress-nginx/udp-services

そしてコントローラーを公開する Service にポートを追加します。

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  type: LoadBalancer
  ports:
    - name: http
      port: 80
      targetPort: 80
    - name: https
      port: 443
      targetPort: 443
    - name: postgres
      port: 5432
      targetPort: 5432
    - name: dns-udp
      port: 53
      protocol: UDP
      targetPort: 53

Helm でインストールした場合は、values で tcpudp のマッピングを宣言すれば、上の三つのステップをチャートが自動で処理します。

tcp:
  5432: "databases/postgres:5432"
udp:
  53: "dns/coredns:53"

Traefik の EntryPoint と IngressRouteTCP/UDP

Traefik は L4 をより一級として扱います。まず静的設定で TCP/UDP 用の EntryPoint を定義します。

entryPoints:
  postgres:
    address: ":5432"
  gamedns:
    address: ":53/udp"

次に CRD である IngressRouteTCP で TCP ルーティングを宣言します。TLS を終端せずそのまま通すには、SNI マッチングと passthrough を併用します。

apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
  name: postgres-route
  namespace: databases
spec:
  entryPoints:
    - postgres
  routes:
    - match: HostSNI(`*`)
      services:
        - name: postgres
          port: 5432

UDP は IngressRouteUDP で扱います。UDP には SNI やホストの概念がないため、マッチ条件なしで EntryPoint に来たトラフィックをバックエンドに送ります。

apiVersion: traefik.io/v1alpha1
kind: IngressRouteUDP
metadata:
  name: dns-route
  namespace: dns
spec:
  entryPoints:
    - gamedns
  routes:
    - services:
        - name: coredns
          port: 53

TLS passthrough — SNI ベースのルーティング

L4 でホスト名ルーティングを模倣したいとき、唯一の手がかりは TLS ハンドシェイクの SNI(Server Name Indication)です。SNI は暗号化前の ClientHello に平文で含まれており、プロキシは TLS を終端せずにどのドメインへ向かうのかを知ることができます。

ClientHello (SNI: db1.example.com)
        -> プロキシは SNI だけを読みルーティングを決定
        -> TLS は終端せずバックエンドまでそのまま転送(passthrough)
        -> バックエンドが直接 TLS を終端

passthrough の利点は、エンドツーエンド暗号化がバックエンドまで維持され、プロキシが証明書を保持する必要がない点です。ingress-nginx では nginx.ingress.kubernetes.io/ssl-passthrough アノテーションとコントローラーの --enable-ssl-passthrough フラグで有効化します。Traefik では上の IngressRouteTCP に tls.passthrough: true を置きます。ただし SNI を送らない純粋な TCP プロトコル(例: 平文 PostgreSQL)には適用できないという限界があります。

Gateway API の TCPRoute/UDPRoute

Ingress API が凍結された今、L4 公開の正解は Gateway API です。最初から複数プロトコルを念頭に設計され、TCP は TCPRoute、UDP は UDPRoute、TLS passthrough は TLSRoute という一級リソースを提供します。

まず Gateway の Listener に L4 プロトコルを宣言します。

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: l4-gateway
  namespace: infra
spec:
  gatewayClassName: example
  listeners:
    - name: postgres
      protocol: TCP
      port: 5432
      allowedRoutes:
        kinds:
          - kind: TCPRoute

次に TCPRoute でバックエンドをつなぎます。

apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
  name: postgres
  namespace: databases
spec:
  parentRefs:
    - name: l4-gateway
      namespace: infra
      sectionName: postgres
  rules:
    - backendRefs:
        - name: postgres
          port: 5432

UDPRoute も同じ構造で protocol: UDP の Listener につなぎます。アノテーションや ConfigMap の回避策なしに、そしてコントローラーに依存せずに表現される点が中核の利点です。ただし TCPRoute/UDPRoute は Gateway API の中で比較的遅く GA に向かう部分なので、採用前に使用する実装(例: Cilium、Istio、Envoy Gateway)の対応水準を確認する必要があります。

専用 L4 ソリューションとの選択

ここで一歩引いて考えるべきです。TCP/UDP をわざわざ Ingress コントローラーに重ねることが常に最善とは限りません。しばしばよりシンプルで堅牢な答えは専用の L4 経路です。

アプローチ適する状況欠点
Ingress コントローラー L4(ConfigMap/CRD)すでにコントローラーを運用中、ポート数個の追加コントローラー依存、ホストルーティング制約
Service type LoadBalancerサービスごとに独立 IP、最もシンプルクラウド LB コスト、IP 消費
MetalLB(オンプレ)ベアメタルで LoadBalancer を実装L2/BGP ネットワークの理解が必要
クラウド NLB(L4)高性能な純粋 L4、大量接続L7 機能なし
Gateway API TCPRoute標準化・移植性、複数プロトコル実装の対応成熟度にばらつき

判断基準はシンプルです。ポートが一つか二つで、すでに ingress-nginx や Traefik を運用しているなら、コントローラー L4 公開が運用コストを節約します。逆に、データベースのようにスループットと安定性が重要、公開する L4 サービスが多い、あるいは L7 機能がまったく不要なら、NLB や type LoadBalancer Service の方が明確でデバッグしやすいです。

実戦事例

データベースの公開

本番データベースを外部に直接開くのはセキュリティ上慎重であるべきです。公開が避けられないなら、TLS passthrough でエンドツーエンド暗号化を維持し、NetworkPolicy で送信元 IP を制限し、可能であれば読み取り専用レプリカのみを公開します。

ゲームサーバー UDP

リアルタイムゲームは UDP を使うことが多く、遅延に敏感です。UDP は SNI ルーティングができないため、ポート単位でのみ分岐します。セッション中に同じバックエンドへ行く必要がある場合、クラウド NLB のセッション維持またはクライアント IP ベースのハッシュを活用します。

DNS の配信

DNS は UDP 53 と TCP 53 の両方を扱う必要があります(大きな応答やゾーン転送は TCP)。したがって同じ 53 ポートを udp-services と tcp-services の両方に登録し、Service にも両プロトコルを宣言する必要があります。

落とし穴とチェックリスト

L4 公開でよくはまる落とし穴です。

  • ホストルーティングの期待: L4 には host の概念がありません。SNI なしでホストごとの分岐を期待すると失敗します。
  • Service ポートの漏れ: ConfigMap に登録しても、コントローラー Service にポートを開かないと外部から届きません。
  • UDP ファイアウォール: クラウドのセキュリティグループやノードのファイアウォールで UDP が既定でブロックされていることが多いです。
  • コントローラーフラグ未設定: ConfigMap だけ作って --tcp-services-configmap 引数を与えないと無視されます。
  • 送信元 IP の保持: L4 プロキシを経由するとクライアント IP が隠れることがあります。externalTrafficPolicy: Local または PROXY プロトコルを検討します。

チェックリスト:

[ ] 公開対象は本当に L7 ではない純粋な TCP/UDP か
[ ] ConfigMap/CRD にポートとバックエンドを正確に登録したか
[ ] コントローラー Service(LoadBalancer) に該当ポートを開けたか
[ ] コントローラーに必要なフラグ/EntryPoint を設定したか
[ ] ファイアウォール/セキュリティグループで TCP・UDP ポートを許可したか
[ ] NetworkPolicy で送信元を制限したか(特に DB)
[ ] 専用 L4(NLB/MetalLB) の方がシンプルではないか比較したか

おわりに

Ingress で TCP/UDP を公開することの核心は、Ingress が本来 L7 の抽象であり、L4 トラフィックはコントローラー固有のメカニズムで「おまけ」として重ねられるという事実を理解することです。ingress-nginx の tcp/udp-services ConfigMap、Traefik の EntryPoint と IngressRouteTCP/UDP、TLS passthrough の SNI ルーティングがその道具でした。

しかし本当に綺麗な未来は Gateway API です。TCPRoute、UDPRoute、TLSRoute が L4 を標準リソースとして一級化するからです。同時に、ポート一つか二つのためにコントローラーを複雑にするより、専用の L4 ソリューションの方が良い場合も多いことを忘れてはいけません。公開対象の性質、運用中のインフラ、そして今後の Gateway API 移行計画を併せて選択するのがよいでしょう。

参考資料