몇 년째 클러스터마다 기본으로 깔던 ingress-nginx인데, 이제 손을 떼야 할 때가 왔다. 2025년 11월 쿠버네티스 측에서 이 프로젝트를 은퇴시킨다는 공지가 나왔고, 남은 기간 동안 옮겨갈 곳을 정해야 한다. 필자도 운영 중인 클러스터가 몇 개 있어서 결국 전부 손봐야 하는 상황이라, 이참에 이전 순서를 한 번 정리해 둔다.
결론부터 말하자면, 후속으로 밀고 있는 표준은 Gateway API다. 기존 Ingress 리소스는 GatewayClass, Gateway, HTTPRoute 세 종류로 쪼개서 옮기고, 변환은 SIG-Network가 만든 ingress2gateway 도구로 초안을 뽑은 뒤 손으로 다듬는다. ingress-nginx의 애노테이션 기능(경로 재작성, 헤더 조작, HTTPS 리다이렉트 등)은 대부분 HTTPRoute의 filters 필드로 대응된다.
1. ingress-nginx 은퇴 일정부터 정리
먼저 왜 지금 움직여야 하는지부터 짚고 넘어간다. 쿠버네티스 블로그 공지에 따르면 ingress-nginx는 2026년 3월까지만 best-effort 유지보수가 이어지고, 그 뒤로는 신규 릴리스, 버그 수정, 보안 취약점 패치가 모두 중단된다. 저장소는 읽기 전용으로 바뀐다.
기존에 배포된 것이 갑자기 죽는 건 아니다. 설치 아티팩트도 당분간 남아 있고 동작도 계속한다. 다만 CVE가 새로 뜨면 아무도 고쳐주지 않는다는 뜻이라, 인터넷에 노출된 엣지 컴포넌트를 그 상태로 두는 건 곤란하다. 그래서 유지보수 종료 전에 대체재로 넘어가는 게 이 글의 목적이다.
2. Gateway API가 뭐고 Ingress와 뭐가 다른가
Gateway API는 Ingress를 대체하려고 SIG-Network에서 만든 L4/L7 라우팅 표준이다. Ingress 하나에 모든 설정을 욱여넣고 나머지는 컨트롤러별 애노테이션으로 때우던 방식을, 역할별로 리소스를 나눠 CRD로 표준화한 것이다.
핵심 리소스는 세 가지다.
- GatewayClass - 어떤 컨트롤러 구현체를 쓸지 지정하는 클래스. Ingress의
IngressClass에 대응한다. - Gateway - 실제 리스너(포트, 프로토콜, TLS 인증서)를 정의하는 리소스. 인프라 담당자가 관리하는 진입점이다.
- HTTPRoute - 호스트, 경로, 헤더로 매칭해서 백엔드 서비스로 보내는 라우팅 규칙. 애플리케이션 팀이 관리한다.
즉 Ingress 하나가 하던 일을, 리스너(Gateway)와 라우팅(HTTPRoute)으로 분리한 구조다. 매핑을 표로 정리하면 이렇게 된다.
Ingress Gateway API --------------------------------------------------- IngressClass --> GatewayClass Ingress (리스너 부분) --> Gateway Ingress (rules 부분) --> HTTPRoute spec.tls[] --> Gateway listener tls.certificateRefs rules[].host --> HTTPRoute hostnames[] rules[].http.paths[].path --> HTTPRoute rules[].matches[].path.value 애노테이션(리다이렉트/헤더) --> HTTPRoute rules[].filters[]
3. Gateway API CRD와 컨트롤러 설치
Gateway API는 CRD와 컨트롤러(데이터플레인 구현체)를 따로 설치한다. 표준 채널 CRD부터 깐다. v1.5는 2026년 2월에 나온 릴리스다. 버전 번호는 저장소 상태에 따라 달라질 수 있으니 릴리스 페이지를 확인하고 맞춘다.
$ kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.0/standard-install.yaml customresourcedefinition.apiextensions.k8s.io/gatewayclasses.gateway.networking.k8s.io created customresourcedefinition.apiextensions.k8s.io/gateways.gateway.networking.k8s.io created customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io created customresourcedefinition.apiextensions.k8s.io/referencegrants.gateway.networking.k8s.io created customresourcedefinition.apiextensions.k8s.io/grpcroutes.gateway.networking.k8s.io created
다음은 컨트롤러다. Gateway API는 스펙만 정의하고 실제 트래픽 처리는 구현체가 한다. ingress-nginx에서 넘어오는 팀이라면 nginx 계열인 NGINX Gateway Fabric이 손에 익다. Envoy 기반이 편하면 Envoy Gateway를 써도 되고, 이미 Istio를 돌리면 Istio가 Gateway API를 직접 구현하므로 추가 설치 없이 GatewayClass만 붙이면 된다. 여기서는 NGINX Gateway Fabric으로 간다.
$ helm install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric \
--create-namespace -n nginx-gateway
$ kubectl get pods -n nginx-gateway
NAME READY STATUS RESTARTS AGE
nginx-gateway-fabric-6d4b9c7f8d-abcde 2/2 Running 0 40s
설치되면 GatewayClass가 하나 생긴다. 이 이름을 Gateway에서 참조한다.
$ kubectl get gatewayclass NAME CONTROLLER ACCEPTED AGE nginx gateway.nginx.org/nginx-gateway-controller True 30s
여기서 한 번 걸리는 지점이 있다. ACCEPTED가 True가 아니면 컨트롤러가 CRD를 못 읽는 상태라, 컨트롤러보다 CRD를 먼저 깔았는지 순서를 확인한다. CRD가 없는 상태로 컨트롤러가 뜨면 GatewayClass 자체가 안 잡힌다.
4. ingress2gateway로 기존 Ingress 변환
이제 쓰던 Ingress를 HTTPRoute로 옮긴다. 손으로 다 옮겨도 되지만 규칙이 많으면 ingress2gateway로 초안을 뽑는 게 빠르다. 이 도구는 클러스터나 매니페스트 파일에서 Ingress를 읽어 Gateway와 HTTPRoute YAML로 변환해준다.
$ go install github.com/kubernetes-sigs/ingress2gateway@latest # 클러스터의 모든 네임스페이스 Ingress를 변환 $ ingress2gateway print --providers=ingress-nginx -A # 파일에서 읽어 변환 $ ingress2gateway print --providers=ingress-nginx --input-file=ingress.yaml
--providers는 필수 플래그다. ingress-nginx 외에 kong, traefik, istio, gce 등을 지원한다. 예를 들어 아래 같은 예전 Ingress가 있다고 하자.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts: ["shop.example.com"]
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port: { number: 8080 }
이걸 변환하면 Gateway 하나와 HTTPRoute 하나가 나온다. 다만 공식 문서도 명시하듯 변환 결과는 항상 검증이 필요하다. 애노테이션 기반 기능은 도구가 다 못 옮기는 경우가 있어서, 뽑은 YAML을 그대로 적용하지 말고 리다이렉트나 재작성 같은 부분을 눈으로 확인한다.
5. Gateway 리스너와 TLS 재구성
먼저 진입점인 Gateway를 정의한다. HTTP(80)와 HTTPS(443) 리스너를 두고, HTTPS 쪽에 TLS 인증서 Secret을 붙인다. ingress-nginx에서는 Ingress의 spec.tls에 넣던 인증서가, 여기서는 Gateway 리스너의 tls.certificateRefs로 올라온다.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shop-gw
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
hostname: "shop.example.com"
- name: https
protocol: HTTPS
port: 443
hostname: "shop.example.com"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: shop-tls
mode: Terminate는 게이트웨이에서 TLS를 종료한다는 뜻이고 기본값이다. 인증서 Secret은 기존 ingress-nginx에서 쓰던 kubernetes.io/tls 타입을 그대로 참조하면 되니 cert-manager 설정은 손댈 게 없다.
여기서 자주 나오는 함정 하나. HTTPS 리스너에서 인증서 Secret이 다른 네임스페이스에 있으면 그냥은 참조가 안 된다. 이때 ReferenceGrant로 크로스 네임스페이스 참조를 명시적으로 허용해줘야 한다. 같은 네임스페이스면 신경 쓸 필요 없다.
6. 경로와 헤더 라우팅을 HTTPRoute로
라우팅 규칙은 HTTPRoute로 옮긴다. parentRefs로 위에서 만든 Gateway에 붙이고, matches에 경로와 헤더 조건을 적는다. 경로 매칭 타입은 PathPrefix(접두 매칭)와 Exact(정확 매칭)를 쓴다. Ingress의 pathType: Prefix가 여기서 PathPrefix에 대응한다.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-route
spec:
parentRefs:
- name: shop-gw
hostnames:
- "shop.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api
headers:
- name: x-api-version
value: v2
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: x-forwarded-by
value: gateway
remove: ["x-debug"]
backendRefs:
- name: api-svc
port: 8080
headers 매칭은 요청 헤더 값으로 라우팅을 가른다. 위 예시는 /api로 시작하면서 x-api-version: v2 헤더가 붙은 요청만 이 규칙에 걸린다. 카나리나 버전 분기에 쓰던 애노테이션을 이걸로 대체한다.
헤더 조작은 filters로 한다. 요청 헤더는 RequestHeaderModifier, 응답 헤더는 ResponseHeaderModifier다. 각각 add(기존 값에 추가), set(덮어쓰기), remove(제거) 세 동작을 지원한다. ingress-nginx의 configuration-snippet으로 넣던 헤더 조작이 여기로 온다.
7. HTTP를 HTTPS로 리다이렉트
ingress-nginx의 ssl-redirect: "true" 애노테이션에 해당하는 부분이다. 별도 애노테이션이 아니라 HTTP 리스너에 붙은 HTTPRoute에서 RequestRedirect 필터로 처리한다.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-https-redirect
spec:
parentRefs:
- name: shop-gw
sectionName: http
hostnames:
- "shop.example.com"
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
sectionName: http로 80 포트 리스너에만 이 라우트를 붙인 게 포인트다. HTTP로 들어온 요청을 301로 HTTPS에 돌려보낸다. 실제 서비스 라우팅(HTTPRoute)은 HTTPS 리스너 쪽에 붙이면 역할이 깔끔하게 나뉜다.
8. 확인
적용한 뒤 리소스 상태부터 본다. HTTPRoute의 PARENTS가 Gateway를 제대로 물었는지, Gateway가 주소를 받았는지 확인한다.
$ kubectl get gateway shop-gw
NAME CLASS ADDRESS PROGRAMMED AGE
shop-gw nginx a1b2c3.elb... True 1m
$ kubectl get httproute shop-route
NAME HOSTNAMES AGE
shop-route ["shop.example.com"] 1m
$ kubectl describe httproute shop-route | grep -A3 Conditions
Conditions:
Type: Accepted
Status: True
Type: ResolvedRefs
Status: True
Accepted: True와 ResolvedRefs: True가 둘 다 떠야 정상이다. ResolvedRefs가 False면 backendRefs가 가리키는 서비스 이름이나 포트가 틀렸거나, 크로스 네임스페이스 참조에 ReferenceGrant가 빠진 경우다.
그다음 실제 요청을 던져본다. Gateway가 받은 주소로 붙어서 리다이렉트와 라우팅을 확인한다.
$ ADDR=$(kubectl get gateway shop-gw -o jsonpath='{.status.addresses[0].value}')
# HTTP가 HTTPS로 301 리다이렉트되는지
$ curl -sI http://shop.example.com --resolve shop.example.com:80:$ADDR | head -1
HTTP/1.1 301 Moved Permanently
# 헤더 매칭 라우팅 확인
$ curl -s https://shop.example.com/api/ping -H "x-api-version: v2" \
--resolve shop.example.com:443:$ADDR -k
301이 뜨고 v2 헤더를 붙였을 때 api-svc로 들어가면 이전이 끝난 것이다. 마지막으로 트래픽을 새 Gateway로 완전히 넘긴 걸 확인한 뒤에 예전 Ingress 리소스를 지운다. ingress-nginx 컨트롤러 자체는 롤백 여지를 위해 며칠 남겨두고, 문제 없으면 제거한다.
9. 정리
ingress-nginx는 2026년 3월 이후 보안 패치가 끊긴다. 넘어갈 표준은 Gateway API 하나로 사실상 정해졌다. 이전 순서는 CRD와 컨트롤러 설치, ingress2gateway로 초안 변환, Gateway에 TLS 재구성, HTTPRoute로 경로와 헤더 라우팅 이관, 마지막 검증이다. 변환 도구가 애노테이션을 전부 옮겨주지는 않으니 리다이렉트와 헤더 조작은 손으로 확인하는 걸 잊지 않는다. 한 번 구조를 잡아두면 리스너와 라우팅이 분리돼서 팀별 권한 나누기도 오히려 수월해진다.