Skip to content
Published on

ingress-nginx 딥다이브 — 아키텍처, 어노테이션, 템플릿의 모든 것

Authors

들어가며

쿠버네티스에서 외부 트래픽을 클러스터 안의 서비스로 라우팅하는 가장 흔한 방법은 Ingress 리소스이고, 그 Ingress를 실제로 구현하는 컨트롤러 중 압도적으로 많이 쓰이는 것이 ingress-nginx입니다. 사내 플랫폼 팀에서 한 번이라도 인그레스를 다뤄봤다면 거의 확실히 만나게 되는 컴포넌트입니다.

그런데 막상 운영을 시작하면 혼란스러운 지점이 많습니다. "nginx ingress"라고 검색하면 두 개의 서로 다른 프로젝트가 나오고, 어노테이션을 붙였는데 동작하지 않으며, rewrite-target 하나 때문에 404가 쏟아지고, snippet 어노테이션을 쓰다가 보안 점검에서 지적을 받습니다. 게다가 2026년 현재 ingress-nginx는 유지보수 모드로 전환되었고, Ingress API 자체가 동결(frozen) 상태이며 Gateway API가 후계 표준으로 자리잡고 있습니다.

이 글에서는 ingress-nginx의 내부 구조를 해부하고, 실무에서 반드시 알아야 할 어노테이션을 카탈로그 형태로 정리하며, 전역 튜닝과 보안 위험, 그리고 앞으로의 전환 전략까지 한 번에 다룹니다.

ingress-nginx vs NGINX Inc. 컨트롤러 — 먼저 구분하자

가장 먼저 짚어야 할 혼란의 근원은 "nginx 기반 인그레스 컨트롤러"가 사실 두 개라는 점입니다. 이름이 비슷해서 자료를 섞어 보다가 동작하지 않는 어노테이션을 붙이는 일이 매우 흔합니다.

구분ingress-nginx (커뮤니티)NGINX Ingress Controller (F5/NGINX Inc.)
주체쿠버네티스 프로젝트(SIG)F5 NGINX (상용 벤더)
저장소kubernetes/ingress-nginxnginxinc/kubernetes-ingress
어노테이션 prefixnginx.ingress.kubernetes.ionginx.org / nginx.com
설정 확장Lua + 템플릿, snippetVirtualServer/VirtualServerRoute CRD
라이선스Apache 2.0, 전부 오픈소스OSS 버전 + 상용 NGINX Plus
동적 설정Lua 기반 동적 reload 회피Plus는 API 기반, OSS는 reload

핵심은 어노테이션 prefix입니다. 인터넷 자료를 따라 했는데 동작하지 않는다면, 십중팔구 다른 프로젝트의 어노테이션을 붙인 것입니다. 이 글은 전적으로 커뮤니티 ingress-nginx(prefix nginx.ingress.kubernetes.io)를 다룹니다.

내부 아키텍처 해부

ingress-nginx 파드는 한 컨테이너 안에서 두 가지 역할을 동시에 수행합니다. 하나는 컨트롤러 프로세스이고, 다른 하나는 실제 트래픽을 처리하는 nginx 프로세스입니다.

            ┌──────────────────────────────────────────────┐
            │           ingress-nginx Pod                  │
            │                                              │
  K8s API   │   ┌────────────┐         ┌───────────────┐   │
  ─────────▶│   │ Controller │  watch  │  nginx master │   │
  (watch    │   │  (Go)      │────────▶│  + workers    │   │
  Ingress,  │   │            │ reload/ │               │   │
  Service,  │   │ Lua sync   │ Lua API │  Lua modules  │   │
  Endpoints)│   └────────────┘         └───────┬───────┘   │
            │                                  │           │
            └──────────────────────────────────┼───────────┘
                              ┌────────────────▼────────────────┐
                              │  Upstream Pods (Endpoints)       │
                              └──────────────────────────────────┘

컨트롤러 프로세스의 역할

컨트롤러는 Go로 작성되었으며 쿠버네티스 API 서버를 watch합니다. Ingress, Service, Endpoints(또는 EndpointSlice), Secret, ConfigMap의 변화를 감지하고, 이를 nginx가 이해할 수 있는 설정 모델로 변환합니다. 변환된 모델은 두 경로로 적용됩니다.

  1. 구조적 변경(새 호스트, 새 path, TLS 변경 등)은 nginx.conf 템플릿을 다시 렌더링하고 nginx를 reload합니다.
  2. 단순한 엔드포인트 변경(파드 스케일 인/아웃 등)은 reload 없이 Lua를 통해 동적으로 upstream을 갱신합니다.

Lua와 동적 설정 — reload를 피하는 핵심

전통적인 nginx 운영에서 가장 큰 문제는 upstream이 바뀔 때마다 reload가 필요하다는 점이었습니다. 파드가 자주 뜨고 내려가는 쿠버네티스 환경에서는 reload가 폭주하고, reload 중에는 연결이 끊기거나 메모리 사용량이 치솟습니다.

ingress-nginx는 이를 OpenResty 기반의 Lua로 해결합니다. upstream의 엔드포인트 목록을 Lua 공유 메모리에 저장하고, 엔드포인트가 바뀌면 nginx를 reload하지 않고 Lua 데이터만 갱신합니다. 부하 분산 결정도 balancer_by_lua 단계에서 동적으로 이뤄집니다. 그 결과 일상적인 스케일링에서는 reload가 거의 발생하지 않습니다.

reload가 필요한 경우는 다음과 같습니다.

[ reload 발생 ]                    [ reload 불필요(Lua 동적) ]
- 새로운 server/location 추가       - Pod 스케일 인/아웃
- TLS 인증서/Secret 변경            - Endpoint IP 변경
- ConfigMap 전역 옵션 변경          - 단순 가중치 갱신(canary 일부)
- snippet 어노테이션 변경

핵심 어노테이션 카탈로그

ingress-nginx의 진짜 힘은 어노테이션에 있습니다. 자주 쓰는 어노테이션을 범주별로 정리합니다. 모든 어노테이션 prefix는 nginx.ingress.kubernetes.io 입니다.

어노테이션용도대표 값
rewrite-target경로 재작성캡처 그룹으로 재작성
ssl-redirectHTTP를 HTTPS로 리다이렉트"true"
force-ssl-redirectTLS 미설정에도 강제 리다이렉트"true"
backend-protocol백엔드 프로토콜 지정HTTPS, GRPC
proxy-body-size요청 본문 최대 크기50m
proxy-read-timeout백엔드 응답 읽기 타임아웃(초)"60"
canary카나리 인그레스 활성화"true"
canary-weight카나리 트래픽 비율"20"
affinity세션 어피니티 방식cookie
whitelist-source-range소스 IP 화이트리스트10.0.0.0/8

rewrite-target과 정규식 경로

가장 자주 실수하는 어노테이션입니다. 경로 일부를 잘라내고 백엔드로 전달하려면 정규식 캡처 그룹과 함께 써야 합니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-rewrite
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /svc(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: app-svc
                port:
                  number: 80

여기서 /svc/foo로 들어온 요청은 캡처 그룹 두 번째((.*))인 foo만 백엔드로 전달됩니다. rewrite-target의 달러 표기는 nginx 정규식 캡처를 가리키며, 반드시 코드 펜스 안에서만 사용해야 MDX 빌드가 깨지지 않습니다.

TLS 리다이렉트와 백엔드 프로토콜

metadata:
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"

backend-protocol을 HTTPS로 두면 컨트롤러가 백엔드로 HTTPS로 연결합니다. gRPC 서비스라면 GRPC로 지정해야 HTTP/2 기반 gRPC 프록시가 정상 동작합니다.

카나리 배포

ingress-nginx는 동일 호스트/경로에 대해 메인 인그레스와 카나리 인그레스를 동시에 두고 트래픽을 분할합니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "20"
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-v2
                port:
                  number: 80

weight 외에도 헤더/쿠키 기반 분기가 가능합니다.

metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "x-canary"
    nginx.ingress.kubernetes.io/canary-by-header-value: "always"

ConfigMap 전역 튜닝

어노테이션이 인그레스 단위 설정이라면, ConfigMap은 컨트롤러 전체에 적용되는 전역 설정입니다. 컨트롤러 배포 시 지정한 ConfigMap(보통 ingress-nginx-controller)을 통해 nginx의 http 블록 수준 옵션을 제어합니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  use-gzip: "true"
  gzip-level: "5"
  proxy-body-size: "20m"
  keep-alive: "75"
  keep-alive-requests: "1000"
  upstream-keepalive-connections: "320"
  ssl-protocols: "TLSv1.2 TLSv1.3"
  enable-real-ip: "true"
  use-forwarded-headers: "true"

ConfigMap 변경은 거의 항상 nginx reload를 유발하므로, 빈번하게 바꾸기보다는 GitOps로 신중하게 관리하는 것이 좋습니다.

snippet 어노테이션과 보안 위험

ingress-nginx의 가장 강력하면서도 가장 위험한 기능이 snippet 어노테이션입니다. 임의의 nginx 설정 조각을 인그레스에 주입할 수 있습니다.

metadata:
  annotations:
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "X-Custom: value";

문제는 이 snippet이 컨트롤러의 nginx.conf에 그대로 렌더링된다는 점입니다. 인그레스 리소스를 만들 수 있는 권한만 있으면, snippet을 통해 컨트롤러 프로세스의 권한으로 임의 설정을 주입할 수 있습니다. 멀티테넌트 클러스터에서 이는 권한 상승 경로가 됩니다.

실제로 2025년에 보고된 일련의 CVE(이른바 IngressNightmare 계열)는 admission webhook 처리 과정과 snippet 주입을 악용해 컨트롤러 권한으로 코드 실행이 가능함을 보여줬습니다. 그 결과 ingress-nginx는 기본값을 보수적으로 바꾸었습니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  allow-snippet-annotations: "false"
  annotations-risk-level: "Critical"

최신 버전에서는 allow-snippet-annotations의 기본값이 false입니다. snippet이 꼭 필요하다면 신뢰할 수 있는 운영자만 인그레스를 만들 수 있도록 RBAC를 강하게 잠그고, annotations-risk-level로 허용 범위를 제한해야 합니다.

유지보수 모드와 Gateway API 권고

2026년 시점에서 가장 중요한 맥락은 ingress-nginx와 Ingress API 자체의 미래입니다.

  • Ingress API는 동결(frozen) 상태입니다. 더 이상 새로운 기능이 추가되지 않으며, 표현력의 한계(L7 라우팅, 트래픽 분할, 헤더 매칭 등을 어노테이션으로 우회해야 하는 문제)가 그대로 남아 있습니다.
  • 어노테이션과 snippet에 의존하는 구조는 표준화되지 않고 보안 표면을 넓힙니다.
  • 후계 표준은 Gateway API입니다. Gateway, GatewayClass, HTTPRoute 같은 1급 CRD로 라우팅을 표현하며, 역할 분리(인프라 운영자 vs 앱 개발자)와 트래픽 분할을 표준 스펙으로 제공합니다.

신규 프로젝트라면 Gateway API 기반 구현체(예: Envoy Gateway, Contour, NGINX Gateway Fabric 등)를 우선 검토하는 것이 권장됩니다. 기존 ingress-nginx 사용처는 당장 버릴 필요는 없지만, snippet 의존을 줄이고 어노테이션을 표준 기능 위주로 정리하면서 점진적 전환을 계획하는 것이 좋습니다.

[ 라우팅 표준 변천 ]

  Ingress API (frozen)        Gateway API (후계 표준)
  ───────────────────         ────────────────────────
  단일 Ingress 리소스    ──▶   GatewayClass / Gateway
  어노테이션으로 확장          HTTPRoute / TLSRoute
  벤더별 비표준 동작           역할 기반 분리(RBAC 친화)

IngressClass와 다중 컨트롤러

한 클러스터에 여러 ingress-nginx 컨트롤러를 두는 경우는 흔합니다. 외부용과 내부용을 분리하거나, 팀별로 컨트롤러를 나누는 식입니다. 이때 어떤 인그레스가 어떤 컨트롤러에 의해 처리될지는 IngressClass로 결정됩니다.

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: nginx-internal
spec:
  controller: k8s.io/ingress-nginx

인그레스에서는 spec.ingressClassName으로 사용할 클래스를 지정합니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: internal-app
spec:
  ingressClassName: nginx-internal
  rules:
    - host: internal.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: internal-svc
                port:
                  number: 80

과거에는 kubernetes.io/ingress.class 어노테이션으로 클래스를 지정했지만, 현재는 deprecated이며 spec.ingressClassName 필드를 사용해야 합니다. 컨트롤러는 자신이 담당하는 IngressClass의 인그레스만 처리하므로, 다중 컨트롤러 환경에서 ingressClassName 누락은 "어떤 컨트롤러도 처리하지 않는" 인그레스를 만드는 흔한 실수입니다.

기본 IngressClass를 지정할 수도 있습니다. annotation으로 default를 표시하면, ingressClassName을 생략한 인그레스가 그 클래스로 처리됩니다. 다만 명시성을 위해 가급적 모든 인그레스에 ingressClassName을 적는 것을 권장합니다.

pathType와 경로 매칭

Ingress 스펙의 pathType은 경로 매칭 방식을 결정하며, 의외로 동작 차이가 큽니다.

pathType의미
Exact경로가 정확히 일치해야 함
Prefix경로 세그먼트 단위 접두 일치
ImplementationSpecific컨트롤러 구현에 위임(정규식 등)

Prefix는 세그먼트 단위로 동작합니다. /foo라는 Prefix 규칙은 /foo/foo/bar에는 매칭되지만 /foobar에는 매칭되지 않습니다. 정규식이나 rewrite를 쓰려면 ImplementationSpecific과 use-regex 어노테이션을 조합해야 합니다. 같은 호스트에 여러 path가 있으면 ingress-nginx는 더 구체적인(긴) 경로를 우선합니다.

기본 백엔드와 커스텀 에러 페이지

매칭되는 인그레스가 없는 요청은 기본 백엔드(default backend)로 갑니다. 또한 백엔드가 에러를 반환할 때 사용자 친화적인 페이지를 보여주려면 custom-http-errors와 별도의 에러 페이지 서비스를 연결할 수 있습니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  custom-http-errors: "404,503"
  proxy-intercept-errors: "true"

custom-http-errors에 지정한 상태코드가 발생하면, 컨트롤러는 default-backend로 요청을 보내 커스텀 에러 페이지를 받아옵니다. 브랜딩된 점검 페이지나 일관된 에러 UX가 필요한 서비스에 유용합니다.

트러블슈팅

운영에서 자주 마주치는 증상과 점검 포인트입니다.

# 컨트롤러가 렌더링한 실제 nginx.conf 확인
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- cat /etc/nginx/nginx.conf

# 인그레스가 어떤 백엔드로 매핑됐는지 확인
kubectl describe ingress app-rewrite

# 컨트롤러 로그(reload, 설정 오류 추적)
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=200
증상흔한 원인점검
404 Not Foundrewrite-target 정규식/path 불일치use-regex와 path 캡처 그룹 확인
어노테이션 무시됨다른 프로젝트 prefix 사용nginx.ingress.kubernetes.io 인지 확인
413 Payload Too Largeproxy-body-size 미설정어노테이션 또는 ConfigMap 조정
snippet 적용 안 됨allow-snippet-annotations falseRBAC/리스크 레벨 정책 확인
reload 폭주ConfigMap/Secret 빈번 변경GitOps로 변경 빈도 관리

대부분의 문제는 "어떤 설정이 실제 nginx.conf로 렌더링됐는가"를 직접 확인하면 빠르게 좁혀집니다. 어노테이션이 무시되는 것처럼 보일 때는 prefix와 ingressClassName부터 의심하세요.

설치와 배포 토폴로지

ingress-nginx는 보통 Helm으로 설치하며, 배포 토폴로지가 트래픽 진입 방식과 외부 IP 보존에 큰 영향을 줍니다.

helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace \
  --set controller.service.type=LoadBalancer \
  --set controller.replicaCount=3

서비스 타입과 트래픽 정책은 다음과 같이 구분됩니다.

토폴로지특징외부 IP 보존
Service LoadBalancer클라우드 LB가 앞단externalTrafficPolicy로 제어
Service NodePort노드 포트로 직접 노출보통 SNAT 발생
hostNetwork DaemonSet노드 네트워크 직접 사용원본 IP 보존 용이

externalTrafficPolicy를 Local로 두면 노드 간 추가 홉이 없어져 클라이언트 원본 IP가 보존되지만, 해당 노드에 컨트롤러 파드가 있어야 트래픽을 받습니다. Cluster로 두면 어느 노드든 받지만 SNAT로 원본 IP가 가려질 수 있습니다. IP 기반 정책(화이트리스트, 로깅)이 필요하면 이 선택이 중요합니다.

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app.kubernetes.io/name: ingress-nginx
  ports:
    - name: https
      port: 443
      targetPort: https

핵심 요약

이 글에서 다룬 내용을 한 번에 정리하면 다음과 같습니다.

  • ingress-nginx(커뮤니티)와 NGINX Inc. 컨트롤러는 다른 프로젝트다. 어노테이션 prefix(nginx.ingress.kubernetes.io)로 구분하라.
  • 한 파드 안의 컨트롤러 프로세스가 Go로 API를 watch하고, nginx가 실제 트래픽을 처리하며, Lua가 동적 설정으로 reload를 회피한다.
  • rewrite-target/use-regex의 정규식 캡처, ssl-redirect, backend-protocol, canary가 핵심 어노테이션이다.
  • ConfigMap은 전역 기본값, 어노테이션은 인그레스별 오버라이드다.
  • snippet은 강력하지만 위험하므로 기본 비활성, RBAC와 정책 엔진으로 통제하라.
  • IngressClass로 다중 컨트롤러를 구분하고, pathType과 배포 토폴로지(externalTrafficPolicy)를 이해하라.
  • Ingress API는 동결, ingress-nginx는 유지보수 모드. 신규는 Gateway API로 설계하라.

이 뼈대를 잡아두면 대부분의 실무 상황에서 빠르게 원인을 좁히고 올바른 결정을 내릴 수 있습니다.

마치며

ingress-nginx는 컨트롤러 프로세스와 nginx, 그리고 Lua 기반 동적 설정이 맞물려 동작하는 정교한 시스템입니다. 어노테이션 카탈로그를 이해하고 ConfigMap으로 전역 동작을 잡으면 대부분의 요구사항을 충족할 수 있습니다. 다만 snippet은 강력한 만큼 보안 위험이 크므로 기본적으로 비활성화하고 RBAC로 통제해야 합니다.

무엇보다 2026년의 큰 그림은 분명합니다. Ingress API는 동결되었고 ingress-nginx는 유지보수 모드입니다. 지금 운영 중인 인그레스를 안정적으로 유지하되, 신규 라우팅 요구는 Gateway API로 설계하는 이중 전략이 현실적입니다.

참고 자료