Envoy Gateway 실전 HTTP 라우팅: Host, Path, Header, 그리고 트래픽 분할

10 min read

앞의 두 글이 "이게 대체 뭐냐"라는 기초를 잡아줬다면, 이번 글은 여러분이 일상적으로 가장 많이 수정하게 될 대상, 즉 HTTPRoute로 들어갑니다.

HTTPRoute를 들어오는 트래픽을 위한 라우팅 스크립트라고 생각해 보세요. 다만 Nginx 규칙을 쓰거나 컨트롤러 annotation을 외우는 대신, Kubernetes 네이티브 리소스를 쓴다는 점이 다릅니다.

먼저 분명히 해둘 점이 하나 있습니다. 이 글은 HTTPRoute만 다룹니다. HTTPRoute는 HTTP/HTTPS 의미 체계를 처리하기 때문입니다. gRPC라면 GRPCRoute, raw TCP라면 TCPRoute, TLS passthrough라면 TLSRoute 쪽이 맞습니다. 모든 걸 HTTPRoute에 우겨 넣지 마세요. 그러면 YAML 전체에서 "일단 되는 대로 맞춰 쓰는" 기운이 나기 시작합니다.

가장 흔한 라우팅: Host와 Path

가장 전형적인 예시는 이렇습니다:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-service
          port: 8080

의미는 다음과 같습니다:

  • Host: app.example.com일 때만 이 route에 매칭됨
  • path는 /api로 시작해야 함
  • 트래픽은 api-service:8080으로 전달됨

hostnames를 제거하면 이 route는 "경로만 보고, 도메인은 아무거나"가 됩니다.

matches는 무엇을 매칭할 수 있나

matchesHTTPRoute에서 가장 중심이 되는 개념 중 하나입니다. "어떤 조건일 때 이 규칙을 발동할 것인가"라고 생각하면 됩니다.

자주 쓰는 매칭 조건:

  • path
  • headers
  • queryParams
  • method

예시:

matches:
  - method: GET
    path:
      type: PathPrefix
      value: /api
    headers:
      - name: version
        value: v2

의미:

  • GET 요청만
  • path가 /api로 시작해야 함
  • 요청에 version: v2 header가 포함되어야 함

이 패턴은 API 버전 시험 운영이나 특정 사용자만 대상으로 한 단계적 rollout에 이상적입니다.

하나의 Route, 여러 개의 규칙

하나의 HTTPRoute 안에서 서로 다른 path를 서로 다른 서비스로 보낼 수 있습니다:

rules:
  - matches:
      - path:
          type: PathPrefix
          value: /api
    backendRefs:
      - name: api-service
        port: 8080
  - matches:
      - path:
          type: PathPrefix
          value: /admin
    backendRefs:
      - name: admin-service
        port: 8080

하나의 도메인 아래에서 하위 path를 나눌 때 아주 좋습니다. 예를 들면:

  • /api는 API 서비스로
  • /admin은 admin 백엔드로

라우팅 로직이 바로 눈에 들어옵니다. 오래된 설정 파일처럼 반쯤 읽다가 "내가 지금 뭘 보고 있지"라는 실존적 고민에 빠지지 않아도 됩니다.

Header, Method, Query를 조건으로 쓰기

HTTPRoute는 path 매칭에만 제한되지 않습니다. 요청 header, method, query param으로도 매칭할 수 있습니다.

예를 들어 version: v2가 붙은 요청만 새 서비스로 보내고 싶다면:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: versioned-route
spec:
  parentRefs:
    - name: eg
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
          headers:
            - name: version
              value: v2
      backendRefs:
        - name: api-v2
          port: 8080
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-v1
          port: 8080

이 패턴은 다음에 잘 맞습니다:

  • Canary 배포
  • 특정 고객에게만 새 버전을 먼저 체험하게 하기
  • 새 기능 수동 검증

새 도메인을 통째로 전환하는 것보다, header 기반 라우팅은 더 세밀하고 실험하기 쉽습니다.

filters: 전달 전에 요청을 가공하기

HTTPRoute는 단순히 "매칭하고 전달"만 하는 것이 아닙니다. 전달 전후로 변환도 적용할 수 있습니다. 그 역할을 하는 것이 filters입니다.

가장 흔한 사용 사례는 header 추가입니다:

rules:
  - filters:
      - type: RequestHeaderModifier
        requestHeaderModifier:
          add:
            - name: x-env
              value: prod
    backendRefs:
      - name: api-service
        port: 8080

이건 다음에 유용합니다:

  • 백엔드로 보내기 전에 tracing metadata 주입
  • ingress 계층에서 고정 header 추가
  • 요청/응답 변환

다만 filters가 강력하다고 해서 ingress 계층을 거대한 middleware 덩어리로 만들라는 뜻은 아닙니다. route에 로직을 너무 많이 쌓아 올리면 디버깅이 따로 상담이 필요할 정도로 피곤해집니다.

Weighted Traffic Splitting: 가장 흔한 Canary 패턴

기존 버전에 90%, 새 버전에 10%를 보내고 싶다면 weight를 사용합니다:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: canary-route
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "app.example.com"
  rules:
    - backendRefs:
        - name: api-v1
          port: 8080
          weight: 90
        - name: api-v2
          port: 8080
          weight: 10

이것이 기본적인 canary / traffic splitting입니다.

트래픽 실험을 하려고 굳이 전체 service mesh가 필요한 것은 아닙니다. 많은 API 서비스에서는 이것만으로도 충분하고, 설정도 훨씬 깔끔합니다.

💡 weight는 상대값이며 반드시 100이 될 필요는 없습니다. 919010과 같은 비율을 의미합니다.

timeouts: 요청이 영원히 매달리지 않게 하자

HTTPRoute는 rule 수준의 timeout 설정을 지원합니다. 이건 꽤 중요합니다. ingress 계층에 제한이 없으면, 나쁜 요청이 마치 안 나가는 손님처럼 계속 자리를 차지하게 됩니다.

rules:
  - matches:
      - path:
          type: PathPrefix
          value: /reports
    timeouts:
      request: 10s
      backendRequest: 2s
    backendRefs:
      - name: report-service
        port: 8080

두 필드의 의미:

  • request: 클라이언트 요청 전체에 대해 허용하는 최대 대기 시간
  • backendRequest: 백엔드로 보내는 단일 요청에 대한 최대 대기 시간

backendRequestrequest보다 크면 안 된다는 점도 기억하세요. 전체 여행이 10분인데 버스 한 구간을 20분 기다릴 수 있다는 말과 같아서, 우주가 혼란스러워집니다.

자주 놓치는 두 가지 뉘앙스

1. parentRefs는 특정 Listener를 지정할 수 있다

Gateway에 listener가 여러 개 있다면, 그중 하나에만 바인딩할 수 있습니다:

parentRefs:
  - name: eg
    sectionName: http

이 뜻은 이 route가 http listener에만 attach된다는 것입니다. 멀티 listener 환경에서는 모든 곳에 붙게 두는 것보다 훨씬 예측 가능합니다.

2. matches가 없으면 전부 매칭된다

rule에 matches가 없으면 기본적으로 /에 매칭되는, 사실상 catch-all 규칙이 됩니다.

편리하긴 하지만, 여러분이 더 구체적이라고 생각한 규칙들을 조용히 집어삼킬 수 있습니다. 귀찮다고 생략했다가 왜 정밀한 규칙이 안 먹는지 한 시간 동안 헤매지 마세요.

라우팅이 실제로 동작하는지 검증하기

route를 구성한 뒤에는 curl로 검증하세요:

curl -H "Host: app.example.com" http://$GATEWAY_HOST/api/users
curl -H "Host: app.example.com" -H "version: v2" http://$GATEWAY_HOST/api/users

HTTPRoute 상태를 확인하려면:

kubectl get httproute app-route -o yaml

특히 다음을 주의 깊게 보세요:

  • status.parents
  • Accepted condition

가끔 YAML은 멀쩡해 보여도 Gateway가 거부한 경우가 있습니다. 지원서를 냈는데 아무 연락이 없었다고 합격했다고 믿는 것과 비슷하죠.

한 줄 요약

HTTPRoute의 핵심은 "먼저 매칭하고, 그다음 전달한다"입니다. host, path, header, weight만 잘 다뤄도 일상적인 HTTP ingress 요구사항의 80%는 처리할 수 있습니다.

다음 단계

다음 글에서는 모든 운영 환경에서 반드시 마주치게 되는 주제를 다룹니다. HTTPS, 인증서, TLS termination, 그리고 보안입니다: 👉 TLS와 보안