"경로마다 다른 앱으로"를 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는 두 부분으로 동작하기 때문이다:

  1. Ingress 리소스(API 객체): "이 호스트/경로는 저 Service로" 규칙을 선언.
  2. 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-nginx controller + 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의 탄생 배경.

생각해 볼 문제

  1. ingress-nginx의 annotation을 잔뜩 쓴 매니페스트를 Traefik으로 옮기려 한다. 무엇이 깨지나?
  2. Ingress Controller 없이 Ingress 리소스만 만들면? 실제 동작은?
  3. canary 배포(신규 버전 10% 트래픽)를 순수 Ingress API(anno 없이)로 표현할 수 있는가?
  4. TLS 종단을 Ingress가 아닌 Service 쪽(SSL passthrough)으로 미루면 장단점은?
  5. ingress-nginx의 유지보수 우려가 "Gateway API로"라는 한 방향으로 수렴하는 기술적 이유는?
  6. Ingress와 Service(LoadBalancer)가 외부 노출에서 어떻게 다른가? 언제 어느 쪽을?

참고