"경로마다 다른 앱으로"를 annotation으로 때우던 시절 — Ingress와 그 한계
한 팀이 단일 도메인에서 경로별로 여러 앱을 서비스하려 했다: /api는 백엔드, /는 프론트, /admin은 관리자 앱. Ingress로 이걸 표현하려 하자 — HTTP 헤더 기반 라우팅, 호스트별 정책, TLS 종단이 필요했다. 그런데 Ingress API엔 이런 것들이 없었다. 결국 nginx.ingress.kubernetes.io/... 같은 구현체별 annotation으로 한 줄 한 줄 때웠다. 그 매니페스트는 nginx 전용이 돼 다른 Ingress Controller로 못 옮겼다.
이 글이 푸는 것은: Ingress가 L7(외부→클러스터) 진입을 어떻게 다루고, 왜 그 표현력 한계가 Gateway API로 가는 압력이 됐는가다.
Service(L4)로는 안 되는 것 — 그래서 Ingress가 필요하다
01장에서 Service는 ClusterIP/NodePort/LoadBalancer를 다뤘다. 이들은 다 L4(포트/IP) 진입이다. 클라이언트가 80번 포트로 오면 Service가 그 포트의 트래픽을 Pod로 돌린다.
그런데 실제 웹 서비스는 L7 요구가 있다. app.example.com/api는 backend Service로, app.example.com/admin은 admin Service로, app.example.com/는 frontend Service로 — 같은 80/443 포트로 들어오되 경로와 호스트 이름으로 갈라야 한다. L4 Service는 포트만 보므로 이걸 못 한다. "같은 포트, 여러 경로"를 표현할 수 없다.
이 빈자리를 Ingress가 채운다. Ingress는 L7(HTTP/HTTPS) 진입을 다룬다 — 호스트 이름, 경로, TLS. (Kubernetes docs - Ingress)
flowchart LR
U["외부 사용자<br/>https://app.com/api"] --> IC["Ingress Controller<br/>(nginx 등)"]
IC -. Ingress 규칙 .-> ING["Ingress 리소스"]
IC -->|"경로별 라우팅"| S1["Service: backend"]
IC --> S2["Service: frontend"]
Ingress는 리소스와 컨트롤러 두 부분이다
Ingress를 처음 접하면 흔히 혼란스러운 지점이 있다 — Ingress 리소스(YAML)를 만들었는데 외부 접속이 안 된다. 왜?
Ingress는 두 부분으로 동작하기 때문이다:
- Ingress 리소스(API 객체): "이 호스트/경로는 저 Service로" 규칙을 선언.
- Ingress Controller(별도 배포): nginx, HAProxy, Traefik 등이 그 규칙을 해석해 실제 L7 라우팅을 수행.
리소스만 있고 컨트롤러가 없으면 — 규칙은 etcd에 저장되지만 그것을 읽고 라우팅하는 주체가 없다. 그래서 아무 일도 안 일어난다. IngressClass로 "어떤 컨트롤러가 이 Ingress를 처리하나"를 지정한다. (Kubernetes docs - Ingress Controllers)
# Kubernetes 1.36
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: {name: web, annotations: {kubernetes.io/ingress.class: nginx}}
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend: {service: {name: backend, port: {number: 80}}}
- path: /
pathType: Prefix
backend: {service: {name: frontend, port: {number: 80}}}
tls: [{hosts: [app.example.com], secretName: tls-secret}]
Ingress의 표현력 한계 — annotation 지옥
Ingress API는 의도적으로 최소다. 기본은 호스트 + 경로(prefix/exact) 라우팅, TLS. 그런데 실제 L7 요구는 이보다 훨씬 많다:
- HTTP 헤더/메서드 기반 라우팅.
- 트래픽 가중치/미러링(canary 배포).
- 요청/응답 헤더 조작.
- 재시도/타임아웃/속도 제한.
이런 것들이 API에 없으니, 구현체(nginx 등)가 annotation으로 끼워 넣었다:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "X-Custom: yes";
이 매니페스트는 nginx 전용이다. Traefik/HAProxy로 옮기면 annotation이 다 무효. 이식성이 깨진다 — Ingress가 "표준"인데 실제로는 구현체 종속이다. 도입의 팀이 부딪힌 벽이 바로 이것.
pathType의 세 가지 — 매칭 규칙이 구현체마다 달랐던 역사
Ingress의 pathType가 경로 매칭 규칙을 정의한다. 이것이 생긴 이유가 annotation 지옥과 같은 맥락이다:
| pathType | 매칭 | 비고 |
|---|---|---|
| Exact | 정확히 일치(/api만, /api/는 X) |
엄격 |
| Prefix | 해당 경로로 시작(/api → /api, /api/v1, /api/ 매칭) |
일반적 |
| ImplementationSpecific | 구현체 마음대로(정규식 등) | nginx 등이 자기 방식으로 |
paths:
- path: /api
pathType: Prefix # /api, /api/v1 모두 매칭
ImplementationSpecific이 바로 "구현체 종속의 온상"이다. nginx에선 정규식 패턴으로, 다른 컨트롤러에선 다르게. 그래서 이 값을 쓴 매니페스트는 이식성이 깨진다. Prefix/Exact가 표준 매칭 — 이 둘로 표현 가능한 건 이 둘로 쓰는 것이 안전하다.
IngressClass — 어느 컨트롤러가 처리하나
# Kubernetes 1.36
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata: {name: nginx}
spec:
controller: k8s.io/ingress-nginx
Ingress는 ingressClassName으로 자기를 처리할 IngressClass를 지정한다. 한 클러스터에 여러 Ingress Controller(nginx, traefik)가 공존할 수 있게 해 준다. (Kubernetes docs - IngressClass)
새 Ingress가 ingressClassName을 안 적으면? 기본 IngressClass(ingressclass.kubernetes.io/is-default-class: "true")가 그것을 결정한다:
kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 30m # default
기본으로 지정된 IngressClass의 컨트롤러가 ingressClassName 없는 Ingress를 처리. 여러 컨트롤러를 쓸 땐 항상 명시하는 습관이 안전 — "어느 컨트롤러"를 매니페스트에 박아두는 것.
ingress-nginx — 가장 흔하지만 유지보수 부담이 있는 Ingress Controller
가장 널리 쓰이는 Ingress Controller는 ingress-nginx(kubernetes/ingress-nginx)인데, 이것이 바로 09장에서 다룬 "EOL/유지보수 부담"과 보안 사고(CVE-2025-1974)의 주인공이다. nginx 위에 구축됐고, 많은 annotation이 ingress-nginx 전용이다. (ingress-nginx docs)
다른 Ingress Controller: Traefik, HAProxy Ingress. 그리고 많은 Ingress Controller가 동시에 Gateway API를 지원하는 방향으로 진화 중이다.
TLS 종단 — Ingress가 인증서를 쥔다
Ingress가 외부 HTTPS를 종단(terminate)하고 내부 Service로는 HTTP로 넘기는 패턴이 흔하다:
tls:
- hosts: [app.example.com]
secretName: app-tls
TLS 인증서는 Secret(kubernetes.io/tls 타입)으로. cert-manager(03-k8s-security 영역)가 이 Secret을 자동 발급/갱신한다. Ingress + cert-manager 조합이 HTTPS 서비스의 표준이다.
TLS의 두 종단 — Terminate vs Passthrough
| 모드 | Ingress 역할 | 백엔드 연결 | 용도 |
|---|---|---|---|
| Terminate(일반) | TLS 종단, 복호화 → 백엔드엔 HTTP | 평문 | 인증서 관리 집중, 일반적 |
| Passthrough | TLS 그대로 백엔드로 (SNI만 보고 라우팅) | 암호화 유지 | 백엔드 자체 인증서/mTLS |
Terminate가 흔하지만 — 백엔드까지 평문이 흐른다. 클러스터 내부가 신뢰할 수 없다면(또는 규정 요구) Passthrough로 백엔드까지 암호화를 유지. 단 Passthrough는 Ingress가 L7(HTTP 경로) 검사를 못 함 — SNI(호스트 이름)만으로 라우팅. 이것이 09장 Gateway API의 Terminate/Passthrough/Reencrypt와 같은 축이다.
HTTP가 아닌 백엔드 프로토콜 — 또 annotation
Ingress는 기본적으로 HTTP 백엔드를 가정한다. 그런데 gRPC/HTTPS/FastCGI 백엔드라면? annotation으로:
annotations:
nginx.ingress.kubernetes.io/backend-protocol: GRPC # 또는 HTTPS, FCGI
이것도 구현체 종속 annotation이다 — 09장 Gateway API가 이것을 표준화(backendRef.protocol)한다. Ingress의 구현체 종속 문제의 또 다른 사례.
Ingress의 한계 요약 — Gateway API가 왜 필요한가
| 한계 | Ingress | Gateway API |
|---|---|---|
| 라우팅 기준 | 호스트, 경로(prefix/exact) | 호스트, 경로, 헤더, 쿼리, 메서드, 가중치 |
| 구현체 종속 | annotation(nginx 전용) | 표준 API(이식성) |
| 역할 분리 | 없음(사용자가 다 씀) | 인프라 담당/앱 담당 분리 |
| 멀티 프로토콜 | HTTP/HTTPS 중심 | TCP/UDP/gRPC도 |
| 서비스 메시 | 미지원 | GAMMA initiative로 통합 |
이 한계들이 쌓여 Gateway API가 탄생한 배경이다.
직접 확인하기
# Kubernetes 1.36 — Ingress 생성 (Ingress Controller 필요)
kubectl create ingress web --rule="app.example.com/api=backend:80" --rule="app.example.com/=frontend:80"
kubectl get ingress
확인할 것: Ingress 리소스 생성. 단, 실제 라우팅은 Ingress Controller가 있어야 동작.
kind에 ingress-nginx 설치 가능(
ingress-nginxcontroller + kind config의extraPortMappings).k8s-verify스킬 참조. 미검증 환경은 노트.
# IngressClass와 annotation의 구현체 종속성 확인
kubectl get ingressclass
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
| "Ingress만 있으면 외부 접속이 된다" | Ingress Controller 별도 배포 필수. 리소스만으론 아무것도 안 함 |
| "Ingress는 표준이라 이식성이 좋다" | annotation이 구현체 종속. 실제론 nginx 전용 매니페스트 흔함 |
| "Ingress는 L7 라우팅의 전부다" | 표현력 제한적. 헤더/가중치/canary는 annotation으로 때움 |
| "IngressClass는 Ingress 리소스다" | 별도 객체. Ingress가 참조하는 컨트롤러 지정 |
| "TLS는 Service가 종단한다" | 보통 Ingress가 종단. Service는 HTTP로 받음 |
| "Ingress를 안 쓰면 외부 접속이 안 된다" | LoadBalancer Service로도 외부 노출 가능(L4) |
| "pathType은 항상 표준이다" | ImplementationSpecific은 구현체 마음대로. 이식성 깨짐 |
요약 — 이 글의 결론
- Service(L4)로는 "같은 포트, 여러 경로"를 못 한다 — 그 빈자리를 Ingress가 L7(HTTP/HTTPS) 진입으로 채운다. 호스트/경로 라우팅, TLS 종단.
- Ingress = 리소스(API 객체) + Ingress Controller(구현체). 리소스만으론 동작 안 함. IngressClass가 처리 컨트롤러 지정.
- 표현력이 의도적으로 최소 — 헤더/가중치/canary가 API에 없어, 구현체(nginx) annotation으로 때움. 그래서 이식성이 깨짐(nginx 전용 매니페스트).
- pathType의 ImplementationSpecific이 구현체 종속의 온상. Prefix/Exact가 표준.
- ingress-nginx가 가장 흔하지만, 유지보수 부담과 보안 사고가 Gateway API 전환 압력.
- TLS 종단을 Ingress가 담당, cert-manager가 인증서 자동화(03-security). Terminate가 일반, Passthrough는 mTLS/규정 시.
- Ingress의 한계(표현력/이식성/역할분리/멀티프로토콜/메시 통합)가 Gateway API의 탄생 배경.
생각해 볼 문제
- ingress-nginx의 annotation을 잔뜩 쓴 매니페스트를 Traefik으로 옮기려 한다. 무엇이 깨지나?
- Ingress Controller 없이 Ingress 리소스만 만들면? 실제 동작은?
- canary 배포(신규 버전 10% 트래픽)를 순수 Ingress API(anno 없이)로 표현할 수 있는가?
- TLS 종단을 Ingress가 아닌 Service 쪽(SSL passthrough)으로 미루면 장단점은?
- ingress-nginx의 유지보수 우려가 "Gateway API로"라는 한 방향으로 수렴하는 기술적 이유는?
- Ingress와 Service(LoadBalancer)가 외부 노출에서 어떻게 다른가? 언제 어느 쪽을?
참고
- Kubernetes 공식 문서 - Ingress - 접근 2026-07-13
- Kubernetes 공식 문서 - Ingress Controllers - 접근 2026-07-13
- Kubernetes 공식 문서 - IngressClass - 접근 2026-07-13
- ingress-nginx docs - 접근 2026-07-13 (annotation)
- Gateway API 동기 - ingress 한계 - 접근 2026-07-13
'Tech Artifacts > K8s Networking' 카테고리의 다른 글
| K8s Networking - 10. service-mesh concepts (0) | 2026.07.14 |
|---|---|
| K8s Networking - 09. gateway-api (0) | 2026.07.14 |
| K8s Networking - 07. calico advanced-policy (0) | 2026.07.14 |
| K8s Networking - 06. network-policy (0) | 2026.07.14 |
| K8s Networking - 05. cilium ebpf (0) | 2026.07.14 |
