Skip to content
Published on

Emissary-ingress(Ambassador) 완벽 가이드: API Gateway형 Ingress의 모든 것

Authors

들어가며

쿠버네티스에 서비스를 외부로 노출할 때 가장 먼저 떠올리는 것이 Ingress입니다. 그런데 막상 운영을 해보면 단순한 경로 기반 라우팅 이상이 필요해지는 순간이 옵니다. 인증을 붙이고, 레이트 리밋을 걸고, 카나리 배포로 트래픽을 5퍼센트만 새 버전으로 흘려보내고, 헤더 기반으로 라우팅을 분기하고, gRPC와 WebSocket을 동시에 처리해야 하는 상황이 그렇습니다.

이런 요구사항이 쌓이면 결국 "Ingress가 아니라 API Gateway가 필요한 것 아닌가?"라는 질문에 도달합니다. Emissary-ingress(예전 이름 Ambassador)는 바로 그 지점에서 출발한 프로젝트입니다. CNCF의 인큐베이팅 프로젝트로, Envoy Proxy를 데이터 플레인으로 삼아 쿠버네티스 네이티브한 방식으로 API Gateway 기능을 제공합니다.

2026년 현재 인그레스 생태계는 큰 전환점을 지나고 있습니다. 네트워킹 Ingress API는 사실상 동결(frozen) 상태로, 더 이상 새로운 기능이 추가되지 않습니다. 그 후계 표준으로 Gateway API가 자리를 잡았고, 가장 많이 쓰이던 ingress-nginx는 보안 이슈와 메인테이너 부족으로 유지보수 모드에 가까운 흐름을 보이고 있습니다. 이런 상황에서 Envoy 기반의 모던한 컨트롤러를 이해해 두는 것은 점점 더 중요해지고 있습니다.

이 글에서는 Emissary-ingress의 아키텍처를 뜯어보고, 핵심 리소스인 Mapping CRD를 깊이 있게 다루며, 인증과 레이트 리밋, 트래픽 관리, 카나리 배포, Gateway API 지원, 실전 배포와 운영 튜닝, 그리고 흔히 빠지는 함정까지 한 번에 정리하겠습니다.

Emissary-ingress란 무엇인가

Emissary-ingress는 쿠버네티스를 위한 오픈소스 API Gateway이자 Ingress 컨트롤러입니다. 핵심 특징을 먼저 짚으면 다음과 같습니다.

  • Envoy Proxy를 데이터 플레인으로 사용합니다. 즉 실제 트래픽 처리는 검증된 Envoy가 담당합니다.
  • 쿠버네티스 CRD(커스텀 리소스)로 모든 설정을 선언합니다. Mapping, Host, Listener, AuthService 등이 대표적입니다.
  • 어노테이션 기반 설정이 아니라 독립적인 리소스로 라우팅을 정의하므로, 마이크로서비스 팀이 각자 자신의 Mapping을 소유하는 분산 설정(decentralized configuration)이 자연스럽습니다.
  • gRPC, WebSocket, HTTP/2, HTTP/3, TLS 종료, 인증, 레이트 리밋 같은 API Gateway 기능을 기본 제공합니다.

역사적으로 이 프로젝트는 Datawire(현 Ambassador Labs)가 만든 Ambassador에서 출발했습니다. 이후 오픈소스 코어가 CNCF에 기증되면서 Emissary-ingress로 이름이 바뀌었고, 상용 버전은 Ambassador Edge Stack(AES)이라는 이름으로 개발자 포털, 고급 레이트 리밋, OAuth/OIDC 필터, 웹 애플리케이션 방화벽 등을 추가로 제공합니다.

한눈에 보는 포지셔닝

구분단순 Ingress 컨트롤러Emissary-ingress
주 용도L7 경로 라우팅, TLS 종료API Gateway, 경로 라우팅, 트래픽 관리
설정 방식Ingress 리소스 + 어노테이션전용 CRD(Mapping 등)
데이터 플레인컨트롤러마다 다름Envoy Proxy
인증별도 구성 필요AuthService로 내장
레이트 리밋제한적RateLimitService로 내장
카나리어려움weight 기반 내장
Gateway API컨트롤러별 상이지원

아키텍처: 컨트롤 플레인과 데이터 플레인의 분리

Emissary-ingress를 제대로 이해하려면 컨트롤 플레인과 데이터 플레인의 분리를 먼저 알아야 합니다. 많은 단순 Ingress 컨트롤러는 설정 파일을 직접 렌더링해서 프록시를 재시작하는 방식을 쓰지만, Emissary는 Envoy의 xDS(동적 디스커버리) 프로토콜을 활용합니다.

                    +-----------------------------+
                    |   Kubernetes API Server      |
                    |  (Mapping, Host, Listener...) |
                    +--------------+--------------+
                                   | watch (CRD)
                                   v
              +----------------------------------------+
              |        Emissary Pod                     |
              |  +----------------+   +--------------+   |
              |  |  controller    |   |   Envoy      |   |
              |  | (CRD -> snapshot|-->| (data plane) |  |
              |  |  -> Envoy conf) |xDS|              |  |
              |  +----------------+   +------+-------+   |
              +--------------------------------|--------+
                                               | L7 traffic
                  client ---- :8080/:8443 ------+----> upstream services

흐름을 단계로 풀어 보면 이렇습니다.

  1. 사용자가 Mapping, Host 같은 CRD를 kubectl apply로 생성합니다.
  2. Emissary 컨트롤러가 쿠버네티스 API 서버를 watch 하면서 변경을 감지합니다.
  3. 컨트롤러는 모든 리소스를 모아 하나의 일관된 스냅샷으로 구성하고, 이를 Envoy 설정으로 변환합니다.
  4. 변환된 설정은 xDS(또는 V3 ADS) 프로토콜로 Envoy 데이터 플레인에 전달됩니다.
  5. Envoy는 무중단으로 새 설정을 적용하고, 실제 클라이언트 트래픽을 처리합니다.

이 구조 덕분에 설정 변경 시 프록시 프로세스를 통째로 재시작하지 않고도 라우팅 규칙을 갱신할 수 있습니다. 데이터 플레인(Envoy)은 트래픽 처리에만 집중하고, 컨트롤 플레인은 선언된 의도를 Envoy가 이해하는 형태로 번역하는 역할에 집중합니다.

핵심 리소스 계층

Emissary 3.x 기준으로 트래픽이 들어오는 경로는 다음 리소스들이 함께 구성합니다.

  • Listener: Envoy가 어떤 포트와 프로토콜로 요청을 받을지 정의합니다(예: 8080 HTTP, 8443 HTTPS).
  • Host: 도메인(hostname)과 TLS 설정, 인증서를 묶습니다. ACME 자동 발급도 여기서 다룹니다.
  • Mapping: 실제 라우팅 규칙입니다. 경로 prefix를 어떤 서비스로 보낼지 정의합니다.
  • AuthService / RateLimitService: 외부 필터 동작을 정의합니다.

이 분리 덕분에 "어떤 포트로 듣는가(Listener)", "어떤 도메인을 처리하는가(Host)", "어떤 경로를 어디로 보내는가(Mapping)"가 깔끔하게 나뉩니다.

Mapping CRD 깊이 들여다보기

Mapping은 Emissary의 심장입니다. 가장 기본적인 형태부터 봅시다.

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: quote-backend
  namespace: default
spec:
  hostname: "*"
  prefix: /backend/
  service: quote

이 Mapping은 /backend/로 시작하는 모든 요청을 quote 서비스로 보냅니다. hostname: "*"는 모든 도메인에 적용한다는 뜻입니다. 서비스 이름은 같은 네임스페이스의 쿠버네티스 Service를 가리키며, quote.namespace 형식으로 다른 네임스페이스도 지정할 수 있습니다.

prefix 와 정규식 라우팅

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: regex-route
spec:
  hostname: "*"
  prefix: "/user/[0-9]+/profile"
  prefix_regex: true
  service: user-service

prefix_regex: true를 주면 prefix를 정규식으로 해석합니다. 단, 정규식 라우팅은 성능 비용이 있으므로 꼭 필요할 때만 쓰는 편이 좋습니다.

헤더와 쿼리 파라미터 기반 라우팅

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: canary-by-header
spec:
  hostname: "*"
  prefix: /api/
  service: api-v2
  headers:
    x-api-version: "v2"

위 Mapping은 x-api-version: v2 헤더가 있는 요청만 api-v2로 보냅니다. 헤더가 없는 요청은 동일 prefix를 처리하는 다른 Mapping으로 흐릅니다. 이런 헤더 기반 분기는 점진적 마이그레이션이나 내부 테스트 트래픽 분리에 유용합니다.

경로 재작성과 호스트 재작성

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: rewrite-example
spec:
  hostname: "*"
  prefix: /legacy/
  rewrite: /v3/
  host_rewrite: internal.example.com
  service: modern-service

/legacy/foo 요청이 업스트림에는 /v3/foo로 전달되고, Host 헤더는 internal.example.com으로 바뀝니다. 레거시 경로를 유지하면서 백엔드만 교체할 때 자주 쓰는 패턴입니다.

타임아웃, 재시도, 서킷 브레이커

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: resilient-route
spec:
  hostname: "*"
  prefix: /orders/
  service: orders
  timeout_ms: 4000
  connect_timeout_ms: 1500
  retry_policy:
    retry_on: "5xx"
    num_retries: 3
  circuit_breakers:
    - max_connections: 2048
      max_pending_requests: 1024
      max_requests: 2048

Envoy의 강력한 회복 탄력성(resilience) 기능이 Mapping 필드로 그대로 노출됩니다. retry_on은 어떤 조건에서 재시도할지(5xx, gateway-error, connect-failure 등), circuit_breakers는 업스트림 과부하를 막는 한계치를 정합니다.

인증: AuthService

Emissary는 Envoy의 ext_authz(외부 인가) 필터를 통해 인증을 처리합니다. AuthService 리소스를 만들면 모든(또는 특정) 요청이 업스트림에 닿기 전에 인증 서비스로 먼저 전달됩니다.

apiVersion: getambassador.io/v3alpha1
kind: AuthService
metadata:
  name: authentication
  namespace: default
spec:
  auth_service: "auth-service:3000"
  proto: http
  path_prefix: "/extauth"
  allowed_request_headers:
    - "x-request-id"
    - "authorization"
  allowed_authorization_headers:
    - "x-user-id"
    - "x-user-role"

동작은 이렇습니다.

  1. 요청이 들어오면 Emissary가 본문 처리 전에 auth-service:3000으로 인증 요청을 보냅니다.
  2. 인증 서비스가 200 OK를 반환하면 요청은 통과하고, 응답 헤더(x-user-id 등)가 업스트림으로 전달됩니다.
  3. 인증 서비스가 401/403을 반환하면 요청은 거기서 차단됩니다.

특정 Mapping만 인증을 건너뛰게 하려면 해당 Mapping에 bypass_auth: true를 추가하면 됩니다. 상용 Edge Stack에서는 OAuth2/OIDC를 Filter 리소스로 선언적으로 붙일 수 있어, 별도 인증 서비스를 직접 구현하지 않아도 됩니다.

레이트 리밋: RateLimitService

레이트 리밋도 Envoy의 ext 필터 메커니즘을 활용합니다. 먼저 RateLimitService로 외부 레이트 리밋 서비스를 등록합니다.

apiVersion: getambassador.io/v3alpha1
kind: RateLimitService
metadata:
  name: ratelimit
  namespace: default
spec:
  service: "ratelimit-service:8081"
  protocol_version: v3
  domain: emissary

그리고 Mapping에서 어떤 차원(descriptor)으로 레이트 리밋을 적용할지 라벨로 지정합니다.

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: rate-limited-api
spec:
  hostname: "*"
  prefix: /api/
  service: api
  labels:
    emissary:
      - request_label_group:
          - remote_address:
              key: remote_address

여기서 labels 아래의 그룹이 Envoy의 rate limit descriptor로 변환되어 외부 레이트 리밋 서비스로 전달됩니다. 레이트 리밋 서비스는 descriptor별로 카운터를 관리하고, 한도를 초과하면 429를 반환하도록 구성합니다. 오픈소스 Emissary는 Envoy ratelimit 서비스(레디스 백엔드)를 그대로 쓰고, Edge Stack은 한도를 CRD로 선언하는 통합 레이트 리밋을 제공합니다.

트래픽 관리와 카나리 배포

API Gateway형 Ingress의 진가는 트래픽 관리에서 드러납니다. 같은 prefix를 처리하는 두 Mapping에 weight를 주면 가중치 기반 트래픽 분할이 됩니다.

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: app-stable
spec:
  hostname: "*"
  prefix: /app/
  service: app-v1
  weight: 90
---
apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: app-canary
spec:
  hostname: "*"
  prefix: /app/
  service: app-v2
  weight: 10

이 설정은 /app/ 트래픽의 90퍼센트를 v1으로, 10퍼센트를 v2로 보냅니다. 카나리 배포의 가장 단순한 형태입니다. 모니터링 지표가 정상이면 weight를 10에서 25, 50, 100으로 점진적으로 올리면 됩니다.

   incoming /app/ requests
            |
            v
   +-----------------+
   | Emissary route  |
   | weight split    |
   +--+-----------+--+
      | 90%       | 10%
      v           v
  app-v1       app-v2  (canary)

GitOps와 결합하면 이 weight 값을 단순히 PR로 조정하는 것만으로 점진적 롤아웃을 제어할 수 있습니다. Argo Rollouts 같은 도구와 연동하면 지표 기반 자동 승급도 가능합니다. 헤더 기반 카나리(특정 사내 사용자만 새 버전)와 weight 기반 카나리(무작위 비율)를 조합하는 것도 흔한 패턴입니다.

트래픽 섀도잉

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: shadow-traffic
spec:
  hostname: "*"
  prefix: /app/
  service: app-v2-shadow
  shadow: true

shadow: true는 실제 트래픽의 복사본을 새 버전으로 보내되, 응답은 버립니다. 즉 사용자에게는 영향을 주지 않으면서 새 버전이 실제 트래픽을 어떻게 처리하는지 관찰할 수 있습니다. 다크 런치(dark launch)에 유용합니다.

API Gateway vs 단순 Ingress: 언제 무엇을 쓰나

여기서 한 번 정리하고 가겠습니다. 모든 서비스에 API Gateway가 필요한 것은 아닙니다.

단순 Ingress로 충분한 경우는 다음과 같습니다.

  • 내부 도구나 단일 팀이 소유한 소규모 서비스를 노출할 때
  • 경로 기반 라우팅과 TLS 종료만 필요할 때
  • 인증을 애플리케이션이나 서비스 메시 레벨에서 이미 처리할 때

반면 Emissary 같은 API Gateway형 Ingress가 빛나는 경우는 이렇습니다.

  • 여러 팀이 각자 마이크로서비스를 소유하고, 라우팅 설정을 분산 관리하고 싶을 때
  • 게이트웨이 레벨에서 인증, 레이트 리밋, 변환을 일괄 적용하고 싶을 때
  • 카나리, 트래픽 섀도잉, 헤더 기반 라우팅 같은 점진적 배포가 필요할 때
  • gRPC, WebSocket, HTTP/3 등 다양한 프로토콜을 단일 진입점에서 처리할 때
요구사항추천
단순 경로 라우팅 + TLS기본 Ingress 컨트롤러
분산 라우팅 소유권Emissary Mapping
게이트웨이 레벨 인증Emissary AuthService
점진적 카나리Emissary weight 또는 Argo Rollouts
표준화된 미래 지향 APIGateway API(Emissary 지원)

개발자 포털

상용 Ambassador Edge Stack에는 개발자 포털(Dev Portal)이 포함됩니다. 이는 Mapping에 OpenAPI 스펙을 연결하면 자동으로 API 카탈로그 페이지를 생성해 주는 기능입니다.

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: catalog-api
  labels:
    docs.getambassador.io/source: "true"
spec:
  hostname: "*"
  prefix: /catalog/
  service: catalog
  docs:
    path: "/openapi.json"

docs.path에 OpenAPI(스웨거) 문서 경로를 지정하면, 포털이 이를 수집해 외부/내부 개발자가 탐색할 수 있는 문서를 자동 생성합니다. 마이크로서비스가 많아질수록 "어떤 API가 어디에 있고 어떻게 호출하는가"를 자동으로 모아주는 것은 큰 운영 이점입니다. 오픈소스 Emissary 단독으로는 포털 UI가 제공되지 않으므로, 이 기능이 필요하면 Edge Stack을 고려해야 합니다.

Gateway API: 미래 표준과의 흐름

앞서 언급했듯 2026년 현재 네트워킹 Ingress API는 동결되었고, Gateway API가 후계 표준으로 자리잡았습니다. Gateway API는 역할 기반(role-oriented) 설계가 핵심입니다.

  • GatewayClass: 인프라 제공자가 정의하는 게이트웨이 구현 종류(예: Emissary)
  • Gateway: 클러스터 운영자가 만드는 실제 진입점(리스너, 포트)
  • HTTPRoute: 애플리케이션 개발자가 만드는 라우팅 규칙

이 분리는 Emissary가 원래 추구하던 Listener/Host/Mapping 분리와 철학적으로 매우 닮아 있습니다. 그래서 Emissary는 자체 CRD와 함께 Gateway API도 지원합니다.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: emissary-gateway
spec:
  gatewayClassName: emissary
  listeners:
    - name: http
      protocol: HTTP
      port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
spec:
  parentRefs:
    - name: emissary-gateway
  hostnames:
    - "app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /app/
      backendRefs:
        - name: app
          port: 80

흐름을 보면, GatewayClass가 Emissary를 가리키고, Gateway가 포트 8080 리스너를 열며, HTTPRoute가 /app/ 경로를 app 서비스로 보냅니다. Gateway API의 backendRefs는 여러 백엔드와 weight를 지원하므로, 카나리 같은 시나리오도 표준 방식으로 표현할 수 있습니다.

  GatewayClass(emissary)
        |
        v
     Gateway  ---- listener :8080
        ^
        | parentRefs
   HTTPRoute  ---- /app/ -> Service(app)

신규 프로젝트라면 Mapping 같은 벤더 CRD 대신 Gateway API로 시작하는 것을 적극 검토할 만합니다. 표준이기 때문에 추후 컨트롤러를 바꾸더라도 라우팅 정의를 그대로 재활용할 가능성이 높습니다. 다만 Gateway API가 아직 커버하지 못하는 고급 기능(일부 Envoy 세부 설정)은 벤더 CRD나 확장 필드로 보완해야 할 수 있습니다.

실전 배포 핸즈온

Helm으로 Emissary를 설치하는 가장 일반적인 흐름을 따라가 보겠습니다.

# CRD 먼저 설치
kubectl apply -f https://app.getambassador.io/yaml/emissary/3.10.0/emissary-crds.yaml
kubectl wait --timeout=90s --for=condition=available deployment emissary-apiext -n emissary-system

# Helm 리포 추가 후 설치
helm repo add datawire https://app.getambassador.io
helm repo update
helm install emissary-ingress datawire/emissary-ingress \
  --namespace emissary \
  --create-namespace
kubectl rollout status deployment/emissary-ingress -n emissary

설치가 끝나면 기본 Listener를 정의합니다.

apiVersion: getambassador.io/v3alpha1
kind: Listener
metadata:
  name: http-listener
  namespace: emissary
spec:
  port: 8080
  protocol: HTTP
  securityModel: XFP
  hostBinding:
    namespace:
      from: ALL
---
apiVersion: getambassador.io/v3alpha1
kind: Listener
metadata:
  name: https-listener
  namespace: emissary
spec:
  port: 8443
  protocol: HTTPS
  securityModel: XFP
  hostBinding:
    namespace:
      from: ALL

그 다음 도메인과 TLS를 묶는 Host를 만듭니다. cert-manager 또는 Emissary 내장 ACME로 인증서를 자동 발급할 수 있습니다.

apiVersion: getambassador.io/v3alpha1
kind: Host
metadata:
  name: example-host
  namespace: emissary
spec:
  hostname: app.example.com
  acmeProvider:
    authority: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
  tlsSecret:
    name: app-example-com-tls

마지막으로 라우팅 Mapping을 적용하고 동작을 확인합니다.

kubectl apply -f mapping.yaml

# 외부 IP 확인
kubectl get service emissary-ingress -n emissary

# 라우팅 확인
EMISSARY_IP=$(kubectl get svc emissary-ingress -n emissary -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -i http://$EMISSARY_IP/backend/

# 진단 정보 확인 (포트포워딩)
kubectl port-forward -n emissary deploy/emissary-ingress 8877
curl http://localhost:8877/ambassador/v0/diag/?json=true | jq .

/ambassador/v0/diag/ 진단 엔드포인트는 현재 Emissary가 인식한 모든 Mapping과 클러스터 상태를 보여 주므로, 설정이 제대로 반영됐는지 확인할 때 가장 먼저 보는 곳입니다.

다른 Envoy 기반 컨트롤러와의 비교: Contour

Emissary 외에도 Envoy를 데이터 플레인으로 쓰는 대표 컨트롤러로 Contour가 있습니다. 둘 다 CNCF 프로젝트이고 Envoy 기반이지만 지향점이 다릅니다.

항목Emissary-ingressContour
메인 주체Ambassador Labs(코어는 CNCF)VMware/CNCF
핵심 CRDMapping, Host, ListenerHTTPProxy
지향점API Gateway 풀세트경량 Ingress + L7 라우팅
인증/레이트 리밋내장(AuthService 등)외부 ext_authz 연동
위임 모델네임스페이스/리소스 단위HTTPProxy include 위임
Gateway API지원적극 지원
상용 버전Edge Stack없음(순수 오픈소스)

대략적인 선택 기준은 이렇습니다. 게이트웨이 레벨에서 인증, 레이트 리밋, 개발자 포털 같은 풀세트 기능을 원하면 Emissary가 유리합니다. 반대로 가볍고 단순한 Envoy 기반 L7 라우터가 필요하고 인증은 외부에서 처리한다면 Contour가 깔끔합니다. Contour의 HTTPProxy는 include를 통한 위임 모델이 명확해서, 멀티테넌시 환경에서 경로 충돌을 막는 데 강점이 있습니다.

운영과 튜닝

프로덕션에서 Emissary를 안정적으로 운영하려면 다음을 챙기는 것이 좋습니다.

리소스와 스케일링

Envoy는 메모리 사용량이 설정 규모(라우트, 클러스터 수)에 비례해 커집니다. Mapping이 수천 개로 늘어나면 컨트롤러가 스냅샷을 재계산하는 비용도 커지므로, 적절한 CPU/메모리 요청과 한계를 설정하고 HPA로 수평 확장을 구성합니다.

# Helm values 예시 (일부)
replicaCount: 3
resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: "1"
    memory: 1Gi
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

관측성

Emissary는 Envoy의 통계를 Prometheus 포맷으로 노출합니다. 요청 수, 지연(p50/p90/p99), 업스트림 5xx 비율, 활성 커넥션 등을 대시보드로 모니터링하면 카나리 승급 판단의 근거가 됩니다. 분산 트레이싱은 TracingService 리소스로 Zipkin/Jaeger/OpenTelemetry 컬렉터에 연동할 수 있습니다.

apiVersion: getambassador.io/v3alpha1
kind: TracingService
metadata:
  name: tracing
  namespace: emissary
spec:
  service: "otel-collector:9411"
  driver: zipkin
  sampling:
    overall: 10

무중단 재배포

Emissary 자체를 업그레이드할 때는 PodDisruptionBudget과 적절한 readiness/liveness 프로브, 그리고 충분한 replica를 두어 롤링 업데이트 중에도 트래픽이 끊기지 않게 합니다. LoadBalancer 서비스의 externalTrafficPolicy: Local을 쓰면 클라이언트 IP는 보존되지만 노드 분포에 주의해야 합니다.

흔히 빠지는 함정과 트러블슈팅

실제 운영에서 자주 만나는 문제들을 정리합니다.

  • 404가 뜨는데 Mapping은 있다: 가장 흔한 원인은 Listener가 없거나, Host의 hostname과 요청의 Host 헤더가 매칭되지 않는 경우입니다. /ambassador/v0/diag/ 진단 페이지에서 해당 Mapping이 실제로 로드됐는지 먼저 확인하세요.
  • 설정이 반영되지 않는다: Emissary는 모든 CRD를 모아 일관된 스냅샷이 만들어질 때만 적용합니다. 한 리소스라도 유효성 검증에 실패하면 스냅샷 전체가 거부될 수 있으므로, 컨트롤러 로그에서 검증 오류를 확인합니다.
  • TLS 인증서가 적용 안 됨: Host의 tlsSecret이 같은 네임스페이스에 있는지, ACME 발급이 완료됐는지 Host 상태(state)를 확인합니다. ACME는 80 포트로의 HTTP-01 챌린지 도달이 가능해야 합니다.
  • gRPC가 안 됨: Mapping에 grpc: true를 빠뜨린 경우가 많습니다. HTTP/2 prior knowledge 설정과 프로토콜 매칭을 함께 점검하세요.
  • 카나리 비율이 정확하지 않게 느껴짐: weight는 통계적 분할이라 요청 수가 적으면 비율이 흔들릴 수 있습니다. 또 같은 prefix를 처리하는 Mapping의 weight 합이 100이 아니면 의도와 다르게 정규화됩니다.
  • 503 upstream connect error: 업스트림 서비스의 엔드포인트가 준비됐는지, 포트와 프로토콜(특히 TLS origination 여부)이 맞는지 확인합니다. cluster 상태를 진단 페이지에서 볼 수 있습니다.

문제 진단의 황금률은 "진단 엔드포인트 먼저, 컨트롤러 로그 다음, Envoy 통계 마지막"입니다. 이 순서로 보면 대부분의 라우팅 문제는 빠르게 좁혀집니다.

마치며

Emissary-ingress는 단순한 Ingress 컨트롤러가 아니라, Envoy의 강력함을 쿠버네티스 네이티브한 CRD로 풀어낸 API Gateway입니다. 컨트롤 플레인과 데이터 플레인의 명확한 분리, Mapping을 통한 분산 라우팅 소유권, 게이트웨이 레벨의 인증과 레이트 리밋, weight 기반 카나리와 트래픽 섀도잉, 그리고 Gateway API 지원까지, 모던한 트래픽 관리에 필요한 거의 모든 요소를 담고 있습니다.

2026년의 맥락에서 보면 방향성은 분명합니다. Ingress API는 동결되었고, Gateway API가 표준의 자리를 이어받고 있으며, 기존 컨트롤러들은 유지보수와 보안 부담을 겪고 있습니다. 이런 흐름에서 Envoy 기반의 컨트롤러를 이해하고, 신규 라우팅은 Gateway API로 표준화해 두는 전략은 미래의 마이그레이션 비용을 크게 줄여 줍니다.

단순한 노출만 필요하다면 기본 Ingress로 충분합니다. 하지만 여러 팀, 여러 마이크로서비스, 점진적 배포, 게이트웨이 레벨 정책이 필요한 순간이 오면, Emissary 같은 API Gateway형 Ingress가 답이 됩니다. 작게 시작해 진단 엔드포인트와 관측성을 갖추고, weight와 Gateway API로 점진적으로 확장해 나가길 권합니다.

참고 자료