LabHub
배우기 러닝패스 코스

CCA — Cilium Certified Associate

Gateway API Enabled, but No GatewayClass Appears

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

VM 안의 진짜 k3s + Cilium 1.20.1 에서 Gateway API 를 틀린 순서로 켜 보고 바로잡은 뒤, Gateway·HTTPRoute 로 경로·헤더·가중치 분할과 네임스페이스 경계(ReferenceGrant)를 실제 요청으로 확인합니다. 마지막에 요청을 실제로 받는 것이 노드의 cilium-envoy 라는 것을 서비스 목록에서 찾아냅니다.

왜 중요한가

Ingress 는 경로와 호스트 정도만 표준으로 정하고 나머지를 구현별 어노테이션에 맡겼습니다. Gateway API 는 역할을 나눕니다 — 인프라 담당은 GatewayClass·Gateway 를, 앱 팀은 HTTPRoute 를 소유하고, 헤더 매칭·가중치 분할 같은 기능이 스펙 안에 들어 있으며, 다른 네임스페이스를 참조하려면 받는 쪽이 ReferenceGrant 로 허락해야 합니다.

Cilium 은 사이드카 없이 이 API 를 구현합니다. L7 처리는 노드마다 하나씩 도는 envoy 가 맡고, eBPF 가 게이트웨이 주소로 온 트래픽을 그 envoy 로 넘깁니다. 이 구조를 모르면 "게이트웨이 파드가 어디 있지?" 를 찾다가 시간을 씁니다.

그리고 켜는 순서가 중요합니다. 공식 문서는 CRD 를 먼저 설치하라고 합니다. 이 실습에서 순서를 뒤집으면 무엇이 비는지, 재시작만으로 왜 채워지지 않는지를 직접 봅니다.

환경 준비에 약 5분이 걸립니다. Gateway API CRD 는 github raw 에서 받습니다. 세션이 끝나면 /root/cca-gateway 의 파일은 사라집니다.

단계

  1. cilium CLI 로 Gateway API 기능을 켜세요(cilium upgrade --version 1.20.1 --reuse-values --set gatewayAPI.enabled=true). 그다음 공식 문서 순서대로 cilium-operator Deployment 와 cilium DaemonSet 을 재시작합니다. Gateway API CRD 는 아직 설치하지 않습니다. 그 순간을 /root/cca-gateway/no-crd.json 에 기록합니다 — enable_gateway_api(cilium-config 의 enable-gateway-api 문자열), gatewayclass_api(API 에 gatewayclasses 자원이 있으면 true, 없으면 false), operator_log(새 오퍼레이터가 CRD 가 없다고 남긴 로그 한 줄 그대로).
  2. Gateway API v1.6.1 표준 CRD 7개(gatewayclasses gateways httproutes referencegrants grpcroutes backendtlspolicies tlsroutes)를 kubectl apply --server-side -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.6.1/config/crd/standard/gateway.networking.k8s.io_<자원>.yaml 로 설치하세요. 그 뒤 cilium-operator 를 다시 재시작하고, 새 오퍼레이터 파드가 뜬 뒤 20초 이상 기다렸다가 GatewayClass 개수를 셉니다. /root/cca-gateway/crd-late.jsoncrds(설치된 CRD 전체 이름 7개 목록), operator_started(재시작한 오퍼레이터 파드의 status.startTime), gatewayclasses(그때 개수), recorded_at(개수를 센 순간의 UTC 시각, date -u +%Y-%m-%dT%H:%M:%SZ) 를 기록합니다. Cilium 설정은 아직 다시 올리지 않습니다.
  3. CRD 가 있는 상태에서 1단계의 cilium upgrade 명령을 한 번 더 실행해 GatewayClass cilium 이 생기고 Accepted=True 가 되게 하세요. /root/cca-gateway/gatewayclass.jsonname, controller(spec.controllerName), accepted(Accepted 조건의 status), managed_by(라벨 app.kubernetes.io/managed-by 값) 를 기록합니다.
  4. kubectl apply -f /opt/fixtures/cca-gateway/backends.json 으로 백엔드(cca-gw 의 store-v1·store-v2·admin, cca-shop 의 payments)를 띄우세요. cca-gw 네임스페이스에 Gateway shop-gw 를 만듭니다 — gatewayClassName cilium, 리스너 하나(name http, protocol HTTP, port 80). Programmed=True 가 되면 /root/cca-gateway/gateway.jsonaddress(status.addresses 의 값), programmed, service(자동 생성된 서비스 이름), service_type, proxy_backend(에이전트 cilium-dbg service list 에서 그 주소:80 LoadBalancer 프런트엔드의 백엔드 ip:port), root_code(노드에서 curl http://<주소>/ 의 HTTP 코드, 숫자), server(그 응답의 server 헤더 값) 를 기록합니다.
  5. cca-gw 에 HTTPRoute store 를 만드세요. parentRefs 는 shop-gw 하나, 규칙은 둘입니다 — (1) path PathPrefix /store 이면서 헤더 x-canary: yes 인 요청은 store-v2:8080, (2) path PathPrefix /store 인 나머지는 store-v1:8080. 노드에서 게이트웨이 주소로 /store/list, 헤더를 붙인 /store/list, /storefront 를 요청해 /root/cca-gateway/routes.jsonstore(첫 요청의 응답 JSON 의 app), store_canary(둘째의 app), storefront(셋째의 HTTP 코드, 숫자) 를 기록합니다.
  6. cca-gw 에 HTTPRoute split 을 만드세요. parentRefs shop-gw, 규칙 하나 — path PathPrefix /checkout, backendRefs store-v1:8080 weight 80 과 store-v2:8080 weight 20. 라우트가 반영된 뒤 게이트웨이 주소의 /checkout100번 요청해 응답의 app 을 세고 /root/cca-gateway/split.jsonpath, requests(보낸 수), counts({store-v1: n, store-v2: m}) 를 기록합니다. 두 버전 모두 관측돼야 합니다.
  7. cca-gw 에 HTTPRoute pay 를 만드세요 — parentRefs shop-gw, path PathPrefix /pay, backendRefs 는 namespace cca-shoppayments:8080. 먼저 허락 없이 적용해 route 의 ResolvedRefs 조건(status·reason)과 /pay 의 HTTP 코드를 관측합니다. 그다음 cca-shop 에 ReferenceGrant 를 만들어 cca-gw 의 HTTPRoute 가 Service payments 만 참조하도록 허락하고, 다시 관측합니다. /root/cca-gateway/refgrant.jsonbefore·after 각각 resolved_refs, reason, code(숫자) 를 기록합니다.
  8. /root/cca-gateway/report.txt키=값 일곱 줄을 씁니다 — gatewayclass_owner(GatewayClass cilium 의 managed-by 라벨), l7_proxy_daemonset(kube-system 에서 게이트웨이 요청을 실제로 처리하는 DaemonSet 이름), gateway_frontend(주소:80), gateway_proxy_backend(4단계와 같은 방법으로 지금 읽은 백엔드), envoy_config(cca-gw 에 자동 생성된 CiliumEnvoyConfig 이름), split_v2_count(split.json 의 store-v2 수), cross_namespace_without_grant(허락 없을 때의 HTTP 코드). 값은 실제 상태·기록과 일치해야 합니다.

참고

CRD 없이 게이트웨이 기능부터 켠 날

cilium CLI 로 Gateway API 기능을 켜세요(cilium upgrade --version 1.20.1 --reuse-values --set gatewayAPI.enabled=true). 그다음 공식 문서 순서대로 cilium-operator Deployment 와 cilium DaemonSet 을 재시작합니다. Gateway API CRD 는 아직 설치하지 않습니다. 그 순간을 /root/cca-gateway/no-crd.json 에 기록합니다 — enable_gateway_api(cilium-config 의 enable-gateway-api 문자열), gatewayclass_api(API 에 gatewayclasses 자원이 있으면 true, 없으면 false), operator_log(새 오퍼레이터가 CRD 가 없다고 남긴 로그 한 줄 그대로).

기능 스위치는 ConfigMap 을 바꿀 뿐이고, 오퍼레이터는 시작할 때 Gateway API 자원이 있는지 점검합니다. 재시작한 오퍼레이터의 로그에서 level=error 줄을 찾아보세요. 자원 유무는 kubectl api-resources --api-group=gateway.networking.k8s.io 로 확인합니다. 파일은 jq -n 의 --arg/--argjson 으로 만들면 따옴표가 섞인 로그 줄도 안전하게 담깁니다.

CRD 를 뒤늦게 깔고 재시작했는데도 비어 있다

Gateway API v1.6.1 표준 CRD 7개(gatewayclasses gateways httproutes referencegrants grpcroutes backendtlspolicies tlsroutes)를 kubectl apply --server-side -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.6.1/config/crd/standard/gateway.networking.k8s.io_<자원>.yaml 로 설치하세요. 그 뒤 cilium-operator 를 다시 재시작하고, 새 오퍼레이터 파드가 뜬 뒤 20초 이상 기다렸다가 GatewayClass 개수를 셉니다. /root/cca-gateway/crd-late.jsoncrds(설치된 CRD 전체 이름 7개 목록), operator_started(재시작한 오퍼레이터 파드의 status.startTime), gatewayclasses(그때 개수), recorded_at(개수를 센 순간의 UTC 시각, date -u +%Y-%m-%dT%H:%M:%SZ) 를 기록합니다. Cilium 설정은 아직 다시 올리지 않습니다.

오퍼레이터가 CRD 를 알아보는 것과 GatewayClass 객체가 생기는 것은 같은 일이 아닐 수 있습니다. 개수가 0 이라면 그 GatewayClass 를 원래 누가 만드는지 궁금해질 것입니다 — 다음 단계에서 라벨로 확인합니다. 파드가 둘 보이면 creationTimestamp 가 가장 늦은 것이 새 파드입니다. 채점기는 파일 수정 시각이 아니라 recorded_at 을 재시작 시각·나중에 생길 GatewayClass 의 생성 시각과 비교하므로, 기록한 순간의 시각을 적으세요.

GatewayClass 는 차트가 만든다

CRD 가 있는 상태에서 1단계의 cilium upgrade 명령을 한 번 더 실행해 GatewayClass cilium 이 생기고 Accepted=True 가 되게 하세요. /root/cca-gateway/gatewayclass.jsonname, controller(spec.controllerName), accepted(Accepted 조건의 status), managed_by(라벨 app.kubernetes.io/managed-by 값) 를 기록합니다.

GatewayClass 의 라벨과 어노테이션에 누가 이 객체를 소유하는지 적혀 있습니다. Helm 차트는 렌더링할 때 클러스터에 어떤 API 가 있는지 보고 템플릿을 넣거나 뺄 수 있습니다. 조건이 True 가 될 때까지 몇 초 간격으로 다시 읽으세요.

게이트웨이 주소 뒤에 서 있는 것은 envoy 다

kubectl apply -f /opt/fixtures/cca-gateway/backends.json 으로 백엔드(cca-gw 의 store-v1·store-v2·admin, cca-shop 의 payments)를 띄우세요. cca-gw 네임스페이스에 Gateway shop-gw 를 만듭니다 — gatewayClassName cilium, 리스너 하나(name http, protocol HTTP, port 80). Programmed=True 가 되면 /root/cca-gateway/gateway.jsonaddress(status.addresses 의 값), programmed, service(자동 생성된 서비스 이름), service_type, proxy_backend(에이전트 cilium-dbg service list 에서 그 주소:80 LoadBalancer 프런트엔드의 백엔드 ip:port), root_code(노드에서 curl http://<주소>/ 의 HTTP 코드, 숫자), server(그 응답의 server 헤더 값) 를 기록합니다.

Gateway 를 만들면 컨트롤러가 LoadBalancer 서비스를 만들고, 이 k3s 에서는 servicelb 가 노드 IP 를 줍니다. 그 서비스의 프런트엔드를 에이전트의 서비스 목록(-o json)에서 찾아 백엔드가 파드 IP 인지 노드 로컬 주소인지 보세요. 경로 규칙이 하나도 없을 때 누가 어떤 코드로 답하는지도 확인합니다. 주소가 막 붙은 직후에는 연결이 잠시 실패할 수 있으니 폴링하세요.

경로가 같아도 헤더가 다르면 다른 버전으로

cca-gw 에 HTTPRoute store 를 만드세요. parentRefs 는 shop-gw 하나, 규칙은 둘입니다 — (1) path PathPrefix /store 이면서 헤더 x-canary: yes 인 요청은 store-v2:8080, (2) path PathPrefix /store 인 나머지는 store-v1:8080. 노드에서 게이트웨이 주소로 /store/list, 헤더를 붙인 /store/list, /storefront 를 요청해 /root/cca-gateway/routes.jsonstore(첫 요청의 응답 JSON 의 app), store_canary(둘째의 app), storefront(셋째의 HTTP 코드, 숫자) 를 기록합니다.

한 규칙의 matches 항목 하나 안에 path 와 headers 를 함께 두면 둘 다 만족해야 합니다. 더 구체적인 조건(헤더가 있는 쪽)이 우선하도록 Gateway API 가 정해 두었습니다. PathPrefix 는 글자 단위가 아니라 / 로 나뉜 경로 요소 단위로 비교합니다. 백엔드는 자기 이름을 JSON 으로 돌려줍니다.

20% 만 새 버전으로 흘린다는 약속을 세어 본다

cca-gw 에 HTTPRoute split 을 만드세요. parentRefs shop-gw, 규칙 하나 — path PathPrefix /checkout, backendRefs store-v1:8080 weight 80 과 store-v2:8080 weight 20. 라우트가 반영된 뒤 게이트웨이 주소의 /checkout100번 요청해 응답의 app 을 세고 /root/cca-gateway/split.jsonpath, requests(보낸 수), counts({store-v1: n, store-v2: m}) 를 기록합니다. 두 버전 모두 관측돼야 합니다.

가중치는 요청 하나하나를 비율에 따라 나누는 것이지 정확히 다섯 번째마다 보내는 규칙이 아닙니다. 그래서 표본의 수는 80/20 근처에서 흔들립니다. 라우트를 만든 직후에는 envoy 설정이 퍼지는 동안 404 가 섞일 수 있으니, 두 버전이 한 번씩 보인 뒤에 세기 시작하세요. 채점기는 기록을 가중치와 대조하고, 스스로도 표본을 다시 뽑습니다.

옆 네임스페이스의 결제 서비스는 허락을 받아야 붙는다

cca-gw 에 HTTPRoute pay 를 만드세요 — parentRefs shop-gw, path PathPrefix /pay, backendRefs 는 namespace cca-shoppayments:8080. 먼저 허락 없이 적용해 route 의 ResolvedRefs 조건(status·reason)과 /pay 의 HTTP 코드를 관측합니다. 그다음 cca-shop 에 ReferenceGrant 를 만들어 cca-gw 의 HTTPRoute 가 Service payments 만 참조하도록 허락하고, 다시 관측합니다. /root/cca-gateway/refgrant.jsonbefore·after 각각 resolved_refs, reason, code(숫자) 를 기록합니다.

ReferenceGrant 는 참조를 받는 쪽 네임스페이스에 둡니다. from 에는 참조하는 자원의 group·kind·namespace, to 에는 참조되는 자원의 group·kind (그리고 좁히려면 name)을 적습니다. 코어 API 의 group 은 빈 문자열입니다. 조건은 status.parents[0].conditions 에서 type 으로 골라 읽으세요.

개통 보고서: 누가 무엇을 만들고 누가 요청을 받았나

/root/cca-gateway/report.txt키=값 일곱 줄을 씁니다 — gatewayclass_owner(GatewayClass cilium 의 managed-by 라벨), l7_proxy_daemonset(kube-system 에서 게이트웨이 요청을 실제로 처리하는 DaemonSet 이름), gateway_frontend(주소:80), gateway_proxy_backend(4단계와 같은 방법으로 지금 읽은 백엔드), envoy_config(cca-gw 에 자동 생성된 CiliumEnvoyConfig 이름), split_v2_count(split.json 의 store-v2 수), cross_namespace_without_grant(허락 없을 때의 HTTP 코드). 값은 실제 상태·기록과 일치해야 합니다.

사이드카 없는 Cilium 에서 L7 은 노드마다 하나씩 도는 envoy 가 맡습니다. kubectl -n kube-system get dskubectl get ciliumenvoyconfig -A 로 게이트웨이가 남긴 흔적을 찾고, 서비스 목록의 127.0.0.1 백엔드와 연결해 보세요.