콘텐츠로 이동
Study NoteCKA

Ingress와 Gateway API

HTTP 트래픽을 클러스터 안으로

Service는 L4(전송 계층 — IP와 포트만 본다)에서 동작한다. 호스트 이름이나 URL 경로로 트래픽을 나누려면 L7(애플리케이션 계층 — HTTP 호스트·경로·헤더까지 본다)이 필요하다.

NodePort · LoadBalancer 만으로는 호스트 이름이나 경로로 나눌 수 없어 LB 하나 뒤에서 나눠 주는 L7 이 필요하다는 대비

LB 하나 뒤에서 호스트/경로로 여러 서비스에 나눠주는 L7 계층이 필요하다. 그것이 Ingress이고, 그 후속이 Gateway API다.

시작하기 전에의 축으로 보면 — Deployment는 선언만 하면 내장 컨트롤러가 실제 상태로 만들어 주지만, Ingress를 조정하는 컨트롤러는 쿠버네티스에 내장되어 있지 않다. 직접 하나 골라 설치해야 “선언 → 실제 상태” 루프가 돌기 시작한다. 이 장의 함정 대부분이 여기서 나온다.

Ingress 리소스는 규칙일 뿐이고 실제 트래픽은 LoadBalancer Service 뒤의 Ingress 컨트롤러 Pod 이 읽어서 Service 로 넘긴다는 구조
  • Ingress 리소스는 규칙일 뿐이다. 아무것도 하지 않는다
  • 실제 라우팅은 Ingress 컨트롤러 Pod(nginx 등)이 한다
  • 컨트롤러가 없으면 Ingress를 만들어도 아무 일도 안 일어난다
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
spec:
ingressClassName: nginx
rules:
- host: shop.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port: { number: 8080 }
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port: { number: 80 }
터미널 창
kubectl create ingress web --class=nginx \
--rule="shop.example.com/api*=api-svc:8080" --rule="shop.example.com/*=web-svc:80"

시험 중에는 공식 Ingress 문서의 예시 YAML을 복사해 이름·경로·서비스·포트만 바꾸는 게 가장 빠르다.

pathType Prefix 의 /api 가 /api 와 /api/v1 에는 맞고 /apidocs 에는 맞지 않는 경로 요소 단위 접두사라는 대비
pathType매칭
Prefix경로 요소 단위 접두사. /api 는 /api, /api/v1 에 매칭. /apidocs 는 안 된다
Exact완전히 일치해야 한다
ImplementationSpecific컨트롤러가 알아서 (nginx는 정규식을 허용)
  • pathType은 필수 필드다. 빠뜨리면 생성이 거부된다
  • 여러 규칙이 매칭되면 가장 긴 경로가 이긴다
터미널 창
kubectl get ingress
kubectl describe ingress web # Rules와 Events, 백엔드 상태를 본다

IngressClass — 어느 컨트롤러가 처리하나

섹션 제목: “IngressClass — 어느 컨트롤러가 처리하나”
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
spec:
controller: k8s.io/ingress-nginx
ingressClassName 도 기본 클래스도 없으면 어떤 컨트롤러도 Ingress 를 집어가지 않아 ADDRESS 가 비어 있게 되는 갈래
터미널 창
kubectl get ingressclass
  • Ingress에 spec.ingressClassName으로 지정한다
  • 기본 클래스가 지정되어 있으면 생략 가능하다
  • 옛날 방식인 kubernetes.io/ingress.class 애노테이션은 deprecated다

시험 중 복사 원본은 공식 Ingress 문서의 TLS 절이다 — 라우팅 규칙을 가져온 그 페이지에서 스크롤만 내리면 spec.tls 예시가 있다.

터미널 창
kubectl create secret tls web-tls --cert=./tls.crt --key=./tls.key
spec:
ingressClassName: nginx
tls:
- hosts:
- shop.example.com
secretName: web-tls
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80
Ingress 컨트롤러가 TLS 를 종료하고 백엔드로는 평문 HTTP 로 넘기며 인증서 Secret 은 Ingress 와 같은 네임스페이스에 있어야 한다는 구조
  • Secret은 kubernetes.io/tls 타입이어야 하고 Ingress와 같은 네임스페이스에 있어야 한다
  • TLS는 컨트롤러에서 종료(terminate)되고, 백엔드로는 평문으로 간다
  • 종료란 암호화를 진입점에서 풀어 준다는 뜻이다 — 인증서를 백엔드마다 두지 않고 컨트롤러 한 곳에서 관리하려는 설계다
spec:
defaultBackend: # 어느 규칙에도 안 맞으면 여기로
service:
name: fallback-svc
port:
number: 80

애노테이션은 컨트롤러마다 다르다 — 표준이 아니다. 개별 애노테이션을 외울 필요는 없다 — 아래처럼 생겼다는 것과, 표준 밖이라는 사실이 포인트다.

metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: 50m
재작성 · 타임아웃 · 카나리 같은 실무 기능이 전부 컨트롤러별 애노테이션이라 컨트롤러를 바꾸면 다시 써야 하고 그것이 Gateway API 가 나온 이유라는 흐름

바로 이것이 Ingress의 한계다. 재작성·타임아웃·카나리·헤더 조작 — 실무에 필요한 거의 모든 것이 표준 밖의 애노테이션이라 컨트롤러를 바꾸면 전부 다시 써야 한다.

Ingress의 문제를 세 방향에서 푼다.

1 · 역할 분리

인프라 담당자와 앱 개발자가 다른 리소스를 만진다.

2 · 표현력

헤더 매칭·가중치 분배·재작성이 표준 필드다. 애노테이션이 아니다.

3 · 프로토콜 확장

HTTP뿐 아니라 gRPC·TCP·TLS를 같은 모델로.

Gateway API는 CRD(CustomResourceDefinition — 쿠버네티스에 새 리소스 종류를 추가하는 확장, CRD)로 제공된다. 클러스터에 기본 탑재가 아니다 — 직접 설치해야 한다. 이 점이 Ingress와 가장 큰 실무적 차이다.

터미널 창
# Standard 채널 CRD 설치
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
kubectl get crd | grep gateway
kubectl api-resources | grep gateway.networking
GatewayClass 는 인프라 제공자, Gateway 는 클러스터 운영자, HTTPRoute 는 앱 개발자가 맡고 allowedRoutes 로 운영자가 붙을 수 있는 네임스페이스를 통제하는 역할 분리
리소스누가 만드나무엇을 정하나
GatewayClass인프라 제공자어떤 구현체를 쓸 것인가 (IngressClass에 대응)
Gateway클러스터 운영자어떤 포트·프로토콜·호스트를 열 것인가
HTTPRoute앱 개발자그 안에서 어떻게 라우팅할 것인가

이 세 리소스는 전부 선언이다 — 정책을 적어 둘 뿐 트래픽을 직접 만지지 않는다. 선언을 읽어 실제 트래픽을 제어하는 것은 따로 설치하는 Gateway 컨트롤러(구현체 — NGINX Gateway Fabric, Istio, Cilium 같은)다. “Ingress = 리소스 + 컨트롤러”의 축이 리소스만 셋으로 늘어난 채 그대로 성립한다.

GatewayClass — 선언이지 컨트롤러가 아니다

섹션 제목: “GatewayClass — 선언이지 컨트롤러가 아니다”
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata: { name: nginx }
spec:
controllerName: gateway.nginx.org/nginx-gateway-controller # 이 클래스를 맡을 구현체의 식별자

GatewayClass는 “이 클래스는 어느 구현체가 맡는다”를 적어 두는 API 오브젝트일 뿐, 그 자체가 프록시를 띄우지 않는다. controllerName이 가리키는 구현체가 설치되어 있어야 클래스가 Accepted되고 Gateway가 실체를 얻는다 — IngressClass와 Ingress 컨트롤러의 관계가 한 층 위로 복제된 것이다.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata: { name: prod-gateway, namespace: infra }
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces: { from: All } # All | Same | Selector
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs: [{ name: web-tls }]
allowedRoutes:
namespaces:
from: Selector
selector: { matchLabels: { team: shop } }

allowedRoutes가 핵심이다. 운영자가 “어느 네임스페이스가 이 Gateway에 붙을 수 있는지”를 통제한다. certificateRefs의 kind와 group을 생략하면 같은 네임스페이스의 core Secret을 뜻한다. 다른 네임스페이스의 인증서를 참조하려면 그쪽의 ReferenceGrant가 추가로 허용해야 한다.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
namespace: shop
spec:
parentRefs:
- name: prod-gateway
namespace: infra # 다른 네임스페이스의 Gateway에 붙는다
sectionName: http # 특정 listener에만 붙일 때 — Gateway의 listeners[].name
hostnames:
- "shop.example.com"
rules:
- matches:
- path: { type: PathPrefix, value: /api }
headers:
- name: x-version
value: "v2"
backendRefs:
- name: api-v2
port: 8080
- matches:
- path: { type: PathPrefix, value: / }
backendRefs:
- name: web-svc
port: 80

가중치 분배와 필터 — 애노테이션 없이

섹션 제목: “가중치 분배와 필터 — 애노테이션 없이”
rules:
- backendRefs:
- name: web-v1
port: 80
weight: 90 # 카나리 배포가 표준 필드다
- name: web-v2
port: 80
weight: 10
- matches:
- path: { type: PathPrefix, value: /old }
filters:
- type: RequestRedirect
requestRedirect:
statusCode: 301
path: { type: ReplacePrefixMatch, replacePrefixMatch: /new }
- type: RequestHeaderModifier
requestHeaderModifier:
add: [{ name: x-source, value: gateway }]
backendRefs:
- name: web-svc
port: 80
HTTPRoute 한 rule 이 weight 로 트래픽을 90 대 10 으로 나누고 filters 로 리다이렉트 · 헤더 수정 · URL 재작성을 거는 두 축

weight가 곧 카나리 배포다 — 새 버전에 트래픽 일부만 먼저 흘려보고, 문제가 없으면 비중을 올리는 방식이다. Ingress에서 컨트롤러별 애노테이션이었던 것들이 전부 스펙 안에 있다.

네임스페이스를 넘을 때 — ReferenceGrant

섹션 제목: “네임스페이스를 넘을 때 — ReferenceGrant”

HTTPRoute가 다른 네임스페이스의 Service를 백엔드로 쓰려면 그쪽의 허가가 필요하다.

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-shop-routes
namespace: backend # 참조 "당하는" 쪽에 만든다
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: shop
to:
- group: ""
kind: Service
다른 네임스페이스의 Service 를 backendRef 로 쓰려면 그 네임스페이스에 ReferenceGrant 가 있어야 하고 없으면 ResolvedRefs 가 False 가 된다는 관계
  • 참조당하는 쪽이 허가한다 — 몰래 트래픽을 뺏어가지 못하게 하는 설계
  • TLS 인증서 Secret을 다른 네임스페이스에서 참조할 때도 필요하다
리소스프로토콜상태
HTTPRouteHTTP/HTTPSStandard 채널, v1
GRPCRoutegRPCStandard 채널 (v1.4에서 승격)
TCPRoute / UDPRoute원시 TCP/UDPv1.6에서 GA로 승격
TLSRouteTLS 패스스루Experimental 채널
  • 설치 매니페스트가 Standard / Experimental 두 채널로 나뉜다
  • 실습·시험에서는 Standard를 쓰면 된다

깊게 팔 것은 HTTPRoute 하나다. GRPCRoute는 gRPC(HTTP/2 위에서 도는 원격 호출 프로토콜) 트래픽용으로 HTTPRoute와 문법이 거의 같고, TCPRoute·UDPRoute와 TLSRoute(TLS를 종료하지 않고 암호화된 채 백엔드까지 통과시키는 패스스루)는 이름과 용도만 알아두면 된다.

IngressGateway API
클래스 지정IngressClassGatewayClass
트래픽을 실제로 제어Ingress 컨트롤러Gateway 컨트롤러(구현체)
진입점Ingress 리소스에 섞임Gateway로 분리
라우팅 규칙rulesHTTPRoute
재작성·헤더 조작컨트롤러 애노테이션표준 필드(filters)
트래픽 분할애노테이션weight
네임스페이스 교차불가ReferenceGrant
설치컨트롤러만CRD + 컨트롤러

Ingress는 없어지지 않는다. 동결(frozen)은 제거 예정이라는 뜻이 아니라 새 기능이 더 추가되지 않는다는 뜻이고, 기존 기능은 계속 지원된다. 시험에서도 둘 다 나올 수 있으니 양쪽 문법을 알아야 한다.

기존 Ingress를 Gateway API로 옮길 때는 Ingress 한 장에 섞여 있던 필드를 Gateway와 HTTPRoute 두 곳으로 나눠 옮긴다. 진입점에 관한 것(포트·TLS)은 Gateway로, 라우팅에 관한 것(호스트·경로·백엔드)은 HTTPRoute로 간다.

Ingress 필드옮기는 자리
spec.ingressClassNameGateway의 spec.gatewayClassName
spec.tls[].secretNameGateway의 listeners[].tls.certificateRefs[].name. listener는 protocol: HTTPS, port: 443, tls.mode: Terminate
spec.tls[].hostsGateway의 listeners[].hostname
rules[].hostHTTPRoute의 hostnames
paths[].path와 pathType: PrefixHTTPRoute의 rules[].matches[].path, type: PathPrefix
pathType: Exacttype: Exact
backend.service.name·port.numberHTTPRoute의 rules[].backendRefs[].name·port
spec.defaultBackend대응 필드가 없다. 경로 /를 PathPrefix로 받는 규칙을 직접 둔다
컨트롤러 애노테이션filters 같은 표준 필드, 또는 구현체의 확장

HTTPRoute는 parentRefs로 Gateway에 붙는다. 이때 HTTPRoute의 hostnames가 Gateway listener의 hostname과 맞아야 하며, 맞지 않는 호스트의 규칙은 listener가 무시한다 (공식 전환 안내). TLS Secret은 Gateway와 같은 네임스페이스에 있어야 하므로, Gateway는 보통 기존 Ingress와 같은 네임스페이스에 만든다.

실제 Ingress 한 장을 옮겨 보는 연습은 실전 과제에 있다.

터미널 창
# Ingress
kubectl get ingress
kubectl describe ingress web # Rules / Events / ADDRESS
kubectl get ingressclass
kubectl get pods -n ingress-nginx # 컨트롤러가 살아 있는가
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx
# Gateway API
kubectl get gatewayclass
kubectl get gateway -A
kubectl describe gateway prod-gateway -n infra # ★ status.conditions
kubectl get httproute -A
kubectl describe httproute shop -n shop # ★ parents[].conditions

Gateway API는 status.conditions에 답이 다 있다. 여기서 데이터 플레인(data plane)은 실제 트래픽이 지나가는 프록시 — nginx 같은 구현체 — 를 말한다. Gateway·HTTPRoute는 선언일 뿐이고, 그 선언이 프록시 설정에 반영됐는지를 conditions가 알려준다 — 시작하기 전에의 “선언과 실제 상태의 간극”을 리소스가 스스로 보고하는 셈이다.

status.conditions 의 Accepted · Programmed · ResolvedRefs 가 각각 설정 오류 · 데이터 플레인 미반영 · 백엔드 참조 실패를 가리키는 진단표
Condition뜻
Accepted: FalseGateway/Route 설정 자체가 잘못됐다
Programmed: False설정은 맞지만 데이터 플레인에 반영되지 않았다
ResolvedRefs: False백엔드 Service를 못 찾는다 또는 ReferenceGrant 없음
  • Ingress 리소스는 규칙일 뿐, 일은 Ingress 컨트롤러 Pod이 한다
  • pathType은 필수이고 Prefix는 경로 요소 단위다
  • ingressClassName이 없고 기본 클래스도 없으면 아무 일도 안 일어난다
  • Ingress의 고급 기능은 전부 컨트롤러별 애노테이션 — 이식성이 없다
  • Gateway API = GatewayClass(제공자) + Gateway(운영자) + HTTPRoute(개발자)
  • CRD라서 직접 설치해야 한다. Standard 채널을 쓴다
  • 재작성·헤더·가중치가 표준 필드, 네임스페이스 교차는 ReferenceGrant
  • 진단은 status.conditions — Accepted / Programmed / ResolvedRefs