Traefik Gateway API — 포트 불일치

Kubernetes7분조회 –

Traefik 은 들어온 요청을 규칙에 따라 뒤쪽 서비스로 넘겨 주는 리버스 프록시입니다. 그걸 Gateway API 컨트롤러로 올리려 했는데 설치부터 순탄하지 않았습니다. 파드가 CrashLoopBackOff 로 돌고, 겨우 띄워 놓으니 이번엔 요청이 404 로 떨어지고, 그다음에는 Gateway 가 Accepted 로 안 넘어갔습니다. 증상이 셋 다 달라 보여서 따로따로 파고들었습니다.

진입점을 새로 만들 일이 생긴 김에 Gateway API 로 가 봤습니다. 쿠버네티스에서 외부 트래픽을 받는 자리는 오랫동안 Ingress 였고, Ingress 스펙이 표현할 수 있는 건 호스트와 경로로 나누는 HTTP 라우팅까지입니다. 그 밖의 것들은 전부 벤더별 어노테이션으로 채우고, 컨트롤러를 바꾸면 그걸 다시 써야 합니다.

Gateway API 는 그 어노테이션에 흩어져 있던 것들을 표준 리소스로 끌어올린 스펙입니다. Gateway 가 진입점을, HTTPRoute 가 라우팅 규칙을 맡는 식으로 오브젝트가 나뉩니다.

컨테이너 내 80번 포트 바인딩 실패

차트 기본값 그대로 설치했는데 파드가 아예 안 떴습니다.

traefik-xxx   0/1   CrashLoopBackOff   4   2m25s
ERR Command error: error while building entryPoint web:
building listener: error opening listener: listen tcp :80: bind: permission denied

ports.web.port: 80 으로 설정하면 Traefik 이 컨테이너 안에서 직접 80번에 바인딩을 시도합니다. 1024 미만은 privileged port 라 root 권한 없이는 열 수 없습니다.

port(내부 바인딩)와 exposedPort(서비스 외부 노출)를 나눠야 합니다.

# 잘못된 설정
ports:
  web:
    port: 80          # 컨테이너 안에서 80으로 직접 바인딩 시도 → 실패

# 올바른 설정
ports:
  web:
    port: 8000        # 컨테이너 내부 바인딩 포트 (non-privileged)
    exposedPort: 80   # Service가 외부에 노출하는 포트

바인딩은 8000 으로 하고 밖에서는 80 으로 받는 것이라, 두 값이 따로라는 걸 모르면 port 하나만 고쳐 놓고 왜 안 되는지 찾게 됩니다.

Gateway listener port = Traefik entrypoint port

포트를 나눠 주니 파드는 떴지만, 이번엔 요청이 404 로 돌아옵니다.

curl http://whoami.example.com
→ 404 page not found

entrypoint 는 Traefik 이 요청을 받는 입구에 이름을 붙여 둔 것입니다. 이름마다 포트가 하나씩 달려 있고, 대시보드를 열면 그 목록이 그대로 보입니다.

Traefik 대시보드의 Entrypoints 목록. TRAEFIK 8080, WEB 8000, WEB-MTLS 8443, WEB-REDIRECT 80 처럼 이름마다 포트가 붙어 있다
Traefik 대시보드의 entrypoint 목록 — 이름마다 포트가 하나
(출처: Traefik 공식 문서, MIT)

Traefik 의 Gateway API provider 는 Gateway 리소스의 port 와 여기 붙은 entrypoint 포트가 일치해야 HTTPRoute 를 그 리스너에 붙여 줍니다.

Gateway listener port:   80     ← 설정한 값
Traefik entrypoint port: 8000   ← 실제 내부 포트
→ 매칭 실패 → 404

앞에서 내부 포트를 8000으로 바꿨으니 Gateway 쪽도 8000으로 맞춰야 합니다.

# 잘못된 설정
spec:
  listeners:
    - port: 80    # Traefik entrypoint 8000과 불일치

# 올바른 설정
spec:
  listeners:
    - port: 8000  # Traefik entrypoint와 동일하게

외부에서 보이는 포트는 exposedPort 와 로드밸런서가 담당합니다. Gateway listener 에 적는 값은 외부 노출과는 별개입니다.

websecure entrypoint의 TLS 기대

Gateway 가 Accepted 되지 않습니다.

kubectl describe gateway test-gateway -n traefik
→ Message: Cannot find entryPoint for Gateway:
  no matching entryPoint for port 8443 and protocol "HTTP"
→ Accepted: False

차트 기본값에서 websecure entrypoint(8443)에는 TLS 가 붙어 있습니다. 여기에 protocol: HTTP 로 붙으려 하면 맞는 entrypoint 가 없다고 나옵니다.

로드밸런서가 TLS 를 종료하고 plain HTTP 를 뒤로 보내는 구성이지만 Traefik 은 TLS 를 기대하니 충돌합니다.

Service 의 443 포트가 web entrypoint 로 가도록 targetPort 를 바꿨고, 포트 인덱스는 환경마다 다르니 몇 번째가 443 인지 먼저 확인해야 합니다.

kubectl get svc traefik -n traefik -o jsonpath='{.spec.ports}'

kubectl patch svc traefik -n traefik --type='json' -p='[
  {"op": "replace", "path": "/spec/ports/1/targetPort", "value": 8000}
]'

Gateway 에서도 websecure 리스너를 빼고 web 하나만 남겼습니다.

spec:
  listeners:
    - name: web
      protocol: HTTP
      port: 8000
      allowedRoutes:
        namespaces:
          from: All

Gateway 쪽 port 를 entrypoint 와 같은 8000 으로 맞췄습니다. 외부에서 들어오는 포트는 Service 의 exposedPort 가 따로 맡습니다.

Helm chart v40의 ports.websecure.tls 제거

Error: values don't meet the specifications of the schema(s):
traefik:
- ports.websecure: Additional property tls is not allowed

문서에 나오는 대로 적었는데도 스키마에서 막혔습니다. Traefik Helm chart v40.x 에서 ports.websecure.tls 필드가 아예 빠졌기 때문이라, 값을 지우면 넘어갑니다. 이 구성에서는 위처럼 web entrypoint 하나로 통일했습니다.

로드밸런서는 IP가 아닌 hostname 발급

NCloud 의 로드밸런서(NPLB)를 쓰는 환경이라, EXTERNAL-IP 칸에 IP 가 찍힐 줄 알았는데 xxx.lb.naverncp.com 형태의 hostname 이 나왔습니다.

NCloud Global DNS 에는 로드밸런서를 가리키는 전용 레코드 타입(A(LB VPC))이 있습니다. 일반 A 레코드와 달리 값으로 IP 가 아니라 이 hostname 을 받습니다. 여기에 넣어 두면 실제 IP 가 바뀌어도 NCloud 가 알아서 따라갑니다.

kubectl get svc -n traefik traefik
# EXTERNAL-IP: traefik-traefik-xxx.lb.naverncp.com

IP 가 아니라 hostname 이 찍히니, DNS 에 넣을 값은 이 hostname 입니다.

Namespace가 Terminating에서 멈춤

포트를 다 맞추고 나서 정리하려니, 이번엔 테스트 네임스페이스가 안 지워지고 멈췄습니다.

kubectl get namespace gateway-test
NAME           STATUS        AGE
gateway-test   Terminating   16m

metrics.k8s.io/v1beta1 API 그룹 discovery 가 실패해서 네임스페이스 컨트롤러가 리소스 정리가 끝났는지 확인하지 못하고 멈춘 상태였습니다.

spec.finalizers 를 빈 배열로 만들어 finalize 서브리소스에 직접 넣습니다.

kubectl get namespace gateway-test -o json | \
  python3 -c "import sys,json; d=json.load(sys.stdin); d['spec']['finalizers']=[]; print(json.dumps(d))" | \
  kubectl replace --raw /api/v1/namespaces/gateway-test/finalize -f -

남은 리소스 확인을 건너뛰고 지우는 방식이라 정리가 덜 된 리소스가 있으면 그대로 고아로 남으니, discovery 가 깨진 게 확실할 때만 씁니다.

세 증상이 전부 포트에서 갈림

CrashLoopBackOff 는 컨테이너 안에서 80 에 바인딩하려다 났습니다. 404 는 Gateway listener 포트와 Traefik entrypoint 포트가 안 맞아서였습니다. Accepted 실패는 HTTPS 를 기대하는 8443 에 HTTP 로 붙으려다 났습니다. 증상은 셋 다 달랐지만 막힌 자리는 매번 포트였습니다. 나머지 셋(차트 스키마, NPLB hostname, Namespace 정리)은 포트와는 상관없이 따로 걸린 것들입니다.

Traefik 으로 Gateway API 를 올릴 때는 포트가 세 군데에 각각 적힌다는 걸 먼저 정리해 두면 덜 헤맵니다. 컨테이너 안에서 실제로 바인딩하는 port, Service 가 외부에 노출하는 exposedPort, 그리고 Gateway listener 의 port 셋이고, 가운데 것만 외부용이고 나머지 둘은 서로 같아야 합니다.

참고

  1. 불러오는 중