- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- Ingress는 본래 L7이다
- ingress-nginx의 TCP/UDP services ConfigMap
- Traefik의 EntryPoint와 IngressRouteTCP/UDP
- TLS passthrough — SNI 기반 라우팅
- Gateway API의 TCPRoute/UDPRoute
- 전용 L4 솔루션과의 선택
- 실전 사례
- 함정과 체크리스트
- 마치며
- 참고 자료
들어가며
Kubernetes에서 웹 애플리케이션을 외부에 노출하는 일은 비교적 직관적입니다. 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을 아무리 들여다봐도 rules는 http 블록만 가지며, 포트와 호스트, 경로로 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는 TCP/UDP 노출을 위해 두 개의 별도 ConfigMap을 둡니다. tcp-services와 udp-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에서 tcp와 udp 매핑을 선언하면 위 세 단계를 차트가 알아서 처리합니다.
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입니다. 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 이행 계획을 함께 놓고 선택하는 것이 좋습니다.
참고 자료
- Kubernetes Ingress 공식 문서: https://kubernetes.io/docs/concepts/services-networking/ingress/
- ingress-nginx TCP/UDP 노출 가이드: https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services/
- ingress-nginx SSL passthrough: https://kubernetes.github.io/ingress-nginx/user-guide/tls/
- Traefik IngressRouteTCP/UDP: https://doc.traefik.io/traefik/routing/providers/kubernetes-crd/
- Gateway API TCPRoute: https://gateway-api.sigs.k8s.io/api-types/tcproute/
- Gateway API UDPRoute: https://gateway-api.sigs.k8s.io/api-types/udproute/
- MetalLB 공식 문서: https://metallb.universe.tf/
- Kubernetes Service 공식 문서: https://kubernetes.io/docs/concepts/services-networking/service/