본문 바로가기
삵
Cloud Computing & MSA

ingress-nginx 은퇴, Gateway API로 이전하기

냉냉장고를사다줘·2026년 9월 28일·조회 1

몇 년째 클러스터마다 기본으로 깔던 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로 경로와 헤더 라우팅 이관, 마지막 검증이다. 변환 도구가 애노테이션을 전부 옮겨주지는 않으니 리다이렉트와 헤더 조작은 손으로 확인하는 걸 잊지 않는다. 한 번 구조를 잡아두면 리스너와 라우팅이 분리돼서 팀별 권한 나누기도 오히려 수월해진다.

자주 묻는 질문

ingress-nginx는 언제까지 쓸 수 있나

2026년 3월까지 best-effort 유지보수가 이어지고, 그 뒤로는 신규 릴리스와 버그 수정, 보안 취약점 패치가 모두 중단된다. 저장소는 읽기 전용으로 바뀐다. 기존 배포는 계속 동작하지만 새 CVE가 나와도 고쳐지지 않으므로, 인터넷에 노출된 환경이면 종료 전에 옮기는 것을 권한다.

Ingress를 지우지 않고 Gateway API와 병행해도 되나

된다. Gateway API는 별도 CRD와 컨트롤러로 동작하므로 기존 ingress-nginx와 동시에 존재할 수 있다. 도메인별로 하나씩 HTTPRoute로 옮기고 검증한 뒤, 트래픽이 새 Gateway로 넘어간 것을 확인하고 나서 예전 Ingress를 제거하는 점진적 이전이 안전하다.

ingress2gateway가 애노테이션까지 전부 변환해주나

아니다. 호스트, 경로, TLS, 백엔드 같은 기본 매핑은 잘 옮기지만 컨트롤러별 애노테이션은 완전히 대응되지 않는 경우가 있다. 공식 문서도 변환 결과는 항상 테스트와 검증이 필요하다고 명시한다. HTTPS 리다이렉트, 경로 재작성, 헤더 조작 부분은 뽑은 YAML을 눈으로 확인하고 손봐야 한다.

어떤 Gateway API 컨트롤러를 골라야 하나

ingress-nginx에서 넘어온다면 nginx 계열인 NGINX Gateway Fabric이 익숙하다. Envoy 기반이 편하면 Envoy Gateway, 이미 Istio를 운영 중이면 Istio가 Gateway API를 직접 구현하므로 GatewayClass만 붙이면 된다. 어느 쪽이든 Gateway API 표준 리소스(GatewayClass, Gateway, HTTPRoute)는 동일하게 쓴다.

HTTPRoute가 Accepted True인데 트래픽이 안 간다

ResolvedRefs 상태를 확인한다. False면 backendRefs가 가리키는 서비스 이름이나 포트가 틀렸거나, 인증서 Secret 또는 백엔드가 다른 네임스페이스에 있어 ReferenceGrant가 필요한 경우다. kubectl describe httproute로 Conditions를 보면 원인이 나온다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.