🏗️ 아키텍처

Ambassador Pattern

앰배서더 패턴 / 대사 패턴

원격 서비스에 대한 프록시 역할을 하는 헬퍼 서비스를 사이드카로 배포하는 패턴입니다. 애플리케이션과 같은 Pod 또는 컨테이너 내에서 실행되며, 로깅, 모니터링, 인증, 재시도, Circuit Breaker 등 교차 관심사(Cross-Cutting Concerns)를 애플리케이션 코드에서 분리합니다. Istio, Linkerd 같은 Service Mesh의 핵심 구현 원리입니다.

📖 상세 설명

Ambassador Pattern은 마이크로서비스 아키텍처에서 서비스가 외부 리소스(다른 서비스, 데이터베이스, 메시지 큐 등)와 통신할 때 발생하는 교차 관심사를 처리하기 위한 프록시 패턴입니다. "대사(Ambassador)"라는 이름처럼 서비스를 대신하여 외부와의 통신을 중재하고 관리합니다.

이 패턴은 2005년 Microsoft의 Cloud Design Patterns에서 처음 체계화되었으며, Kubernetes와 컨테이너 오케스트레이션의 발전과 함께 급격히 보편화되었습니다. Sidecar Pattern의 특수한 형태로, 프록시 기능에 특화되어 있습니다. 애플리케이션 컨테이너 옆에서 함께 실행되면서 모든 아웃바운드 트래픽을 가로채 처리합니다.

Ambassador의 핵심 역할은 네트워킹 관련 복잡성을 추상화하는 것입니다. 재시도 로직, 타임아웃 설정, Circuit Breaker, TLS 종료, 로드 밸런싱, 서비스 디스커버리 등을 애플리케이션 코드 변경 없이 적용할 수 있습니다. 특히 레거시 애플리케이션에 현대적인 네트워킹 기능을 추가할 때 유용합니다.

실무에서 Ambassador Pattern은 Envoy Proxy를 기반으로 한 Istio, Linkerd 같은 Service Mesh의 데이터 플레인을 구성합니다. AWS App Mesh, Consul Connect도 동일한 원리를 사용합니다. 각 서비스마다 사이드카 프록시가 배포되어 서비스 간 통신의 관찰성(Observability)과 보안을 일관되게 제공합니다.

Ambassador가 처리하는 교차 관심사

재시도 & 타임아웃

일시적 실패에 대한 자동 재시도, 지수 백오프, 요청별 타임아웃 설정. 애플리케이션 코드 수정 불필요.

Circuit Breaker

연속 실패 시 회로 차단하여 장애 전파 방지. 반개방 상태에서 점진적 복구 시도.

mTLS (상호 TLS)

서비스 간 암호화 통신 자동화. 인증서 발급/갱신/검증을 투명하게 처리.

관찰성 (Observability)

분산 트레이싱, 메트릭 수집, 접근 로깅. Jaeger, Prometheus 연동.

💻 코드 예제

Kubernetes - Envoy Sidecar Ambassador

# ambassador-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      # 메인 애플리케이션 컨테이너
      - name: order-service
        image: myapp/order-service:v1.2
        ports:
        - containerPort: 8080
        env:
        - name: PAYMENT_SERVICE_URL
          value: "http://localhost:9000"  # Ambassador를 통해 접근
        - name: INVENTORY_SERVICE_URL
          value: "http://localhost:9001"

      # Ambassador (Envoy Sidecar)
      - name: envoy-ambassador
        image: envoyproxy/envoy:v1.28-latest
        ports:
        - containerPort: 9000  # Payment service proxy
        - containerPort: 9001  # Inventory service proxy
        - containerPort: 9901  # Envoy admin
        volumeMounts:
        - name: envoy-config
          mountPath: /etc/envoy
        args:
        - "-c"
        - "/etc/envoy/envoy.yaml"
        - "--service-cluster"
        - "order-service"

      volumes:
      - name: envoy-config
        configMap:
          name: envoy-ambassador-config

Envoy Ambassador 설정

# envoy-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: envoy-ambassador-config
data:
  envoy.yaml: |
    static_resources:
      listeners:
      # Payment Service 프록시
      - name: payment_proxy
        address:
          socket_address:
            address: 0.0.0.0
            port_value: 9000
        filter_chains:
        - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: payment_http
              route_config:
                name: payment_route
                virtual_hosts:
                - name: payment_backend
                  domains: ["*"]
                  routes:
                  - match:
                      prefix: "/"
                    route:
                      cluster: payment_cluster
                      timeout: 5s
                      retry_policy:
                        retry_on: "5xx,reset,connect-failure"
                        num_retries: 3
                        per_try_timeout: 2s
              http_filters:
              - name: envoy.filters.http.router
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

      clusters:
      - name: payment_cluster
        connect_timeout: 1s
        type: STRICT_DNS
        lb_policy: ROUND_ROBIN
        load_assignment:
          cluster_name: payment_cluster
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: payment-service.default.svc.cluster.local
                    port_value: 80
        # Circuit Breaker 설정
        circuit_breakers:
          thresholds:
          - max_connections: 100
            max_pending_requests: 100
            max_requests: 100
            max_retries: 3
        # 헬스 체크
        health_checks:
        - timeout: 1s
          interval: 5s
          unhealthy_threshold: 3
          healthy_threshold: 2
          http_health_check:
            path: "/health"

Go - 간단한 Ambassador 구현

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
    "time"
    "sync/atomic"
)

// CircuitBreaker 상태
type CircuitBreaker struct {
    failures     int32
    threshold    int32
    state        int32 // 0: closed, 1: open, 2: half-open
    lastFailure  time.Time
    resetTimeout time.Duration
}

func NewCircuitBreaker(threshold int32, resetTimeout time.Duration) *CircuitBreaker {
    return &CircuitBreaker{
        threshold:    threshold,
        resetTimeout: resetTimeout,
    }
}

func (cb *CircuitBreaker) AllowRequest() bool {
    state := atomic.LoadInt32(&cb.state)

    if state == 0 { // closed
        return true
    }
    if state == 1 { // open
        if time.Since(cb.lastFailure) > cb.resetTimeout {
            atomic.StoreInt32(&cb.state, 2) // half-open
            return true
        }
        return false
    }
    return true // half-open: 한 번 시도 허용
}

func (cb *CircuitBreaker) RecordSuccess() {
    atomic.StoreInt32(&cb.failures, 0)
    atomic.StoreInt32(&cb.state, 0) // closed
}

func (cb *CircuitBreaker) RecordFailure() {
    failures := atomic.AddInt32(&cb.failures, 1)
    cb.lastFailure = time.Now()

    if failures >= cb.threshold {
        atomic.StoreInt32(&cb.state, 1) // open
        log.Printf("🔴 Circuit OPEN: %d consecutive failures", failures)
    }
}

// Ambassador Proxy
type Ambassador struct {
    target         *url.URL
    proxy          *httputil.ReverseProxy
    circuitBreaker *CircuitBreaker
    retryCount     int
    timeout        time.Duration
}

func NewAmbassador(targetURL string, retries int, timeout time.Duration) *Ambassador {
    target, _ := url.Parse(targetURL)
    proxy := httputil.NewSingleHostReverseProxy(target)

    // Transport 커스터마이징 (타임아웃, 연결 풀)
    proxy.Transport = &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    }

    return &Ambassador{
        target:         target,
        proxy:          proxy,
        circuitBreaker: NewCircuitBreaker(5, 30*time.Second),
        retryCount:     retries,
        timeout:        timeout,
    }
}

func (a *Ambassador) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    start := time.Now()

    // Circuit Breaker 체크
    if !a.circuitBreaker.AllowRequest() {
        log.Printf("🚫 Circuit open, rejecting request: %s", r.URL.Path)
        http.Error(w, "Service temporarily unavailable", http.StatusServiceUnavailable)
        return
    }

    // 재시도 로직
    var lastErr error
    for attempt := 0; attempt <= a.retryCount; attempt++ {
        if attempt > 0 {
            backoff := time.Duration(attempt*attempt) * 100 * time.Millisecond
            time.Sleep(backoff)
            log.Printf("🔄 Retry attempt %d for %s", attempt, r.URL.Path)
        }

        // 요청 복제 (재시도를 위해)
        reqCopy := r.Clone(r.Context())

        // 타임아웃 적용
        client := &http.Client{Timeout: a.timeout}
        resp, err := client.Do(reqCopy)

        if err != nil {
            lastErr = err
            a.circuitBreaker.RecordFailure()
            continue
        }

        if resp.StatusCode >= 500 {
            lastErr = fmt.Errorf("server error: %d", resp.StatusCode)
            a.circuitBreaker.RecordFailure()
            resp.Body.Close()
            continue
        }

        // 성공
        a.circuitBreaker.RecordSuccess()

        // 응답 복사
        for k, v := range resp.Header {
            w.Header()[k] = v
        }
        w.WriteHeader(resp.StatusCode)
        io.Copy(w, resp.Body)
        resp.Body.Close()

        log.Printf("✅ %s %s -> %d (%v)", r.Method, r.URL.Path, resp.StatusCode, time.Since(start))
        return
    }

    log.Printf("❌ All retries failed for %s: %v", r.URL.Path, lastErr)
    http.Error(w, "Service unavailable after retries", http.StatusBadGateway)
}

func main() {
    // 각 외부 서비스에 대한 Ambassador
    paymentAmbassador := NewAmbassador(
        "http://payment-service:8080",
        3,              // 3회 재시도
        5*time.Second,  // 5초 타임아웃
    )

    inventoryAmbassador := NewAmbassador(
        "http://inventory-service:8080",
        2,
        3*time.Second,
    )

    // 포트별 프록시
    go func() {
        log.Println("🚀 Payment Ambassador on :9000")
        http.ListenAndServe(":9000", paymentAmbassador)
    }()

    log.Println("🚀 Inventory Ambassador on :9001")
    http.ListenAndServe(":9001", inventoryAmbassador)
}

Istio Service Mesh - Ambassador 자동 주입

# istio-virtual-service.yaml
# Istio가 Envoy sidecar를 자동으로 주입
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: payment-service
spec:
  hosts:
  - payment-service
  http:
  - route:
    - destination:
        host: payment-service
        port:
          number: 80
    timeout: 5s
    retries:
      attempts: 3
      perTryTimeout: 2s
      retryOn: "5xx,reset,connect-failure,retriable-4xx"
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: payment-service
spec:
  host: payment-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        h2UpgradePolicy: UPGRADE
        http1MaxPendingRequests: 100
        http2MaxRequests: 100
    outlierDetection:  # Circuit Breaker
      consecutive5xxErrors: 5
      interval: 10s
      baseEjectionTime: 30s
      maxEjectionPercent: 50

📊 관련 패턴 비교

특성 Ambassador Sidecar API Gateway
배포 위치 서비스와 같은 Pod 서비스와 같은 Pod 중앙 집중형
주요 역할 아웃바운드 프록시 기능 확장 전반 인바운드 프록시
트래픽 방향 → 외부 서비스 양방향 외부 → 내부
예시 Envoy, linkerd-proxy 로그 수집기, 설정 동기화 Kong, NGINX Ingress
장애 영향 해당 서비스만 해당 서비스만 전체 시스템

선택 기준: 서비스 간 통신(East-West)에는 Ambassador/Service Mesh, 외부 클라이언트 진입점(North-South)에는 API Gateway를 사용합니다. 둘을 함께 사용하는 것이 일반적입니다.

🗣️ 실무 대화 예시

💬 아키텍처 설계 회의에서

"레거시 결제 서비스에 재시도 로직을 추가해야 하는데, 코드 수정이 어렵습니다. Ambassador 패턴으로 Envoy sidecar를 붙이면 애플리케이션 변경 없이 재시도, Circuit Breaker, mTLS까지 적용할 수 있어요. Istio 도입하면 선언적으로 관리됩니다."

💬 기술 면접에서

"Ambassador 패턴은 Sidecar의 특수한 형태로, 외부 서비스에 대한 프록시 역할을 합니다. Service Mesh에서 각 Pod에 주입되는 Envoy가 대표적인 예입니다. 교차 관심사를 인프라 레이어로 분리해서 개발자가 비즈니스 로직에 집중할 수 있게 해줍니다."

💬 장애 대응 리뷰에서

"이번 결제 서비스 장애가 주문 서비스로 전파된 건 Circuit Breaker가 없었기 때문입니다. Ambassador에 outlierDetection 설정을 추가하면, 연속 5xx 오류 발생 시 해당 엔드포인트를 자동으로 격리합니다."

⚠️ 주의사항

리소스 오버헤드: 모든 Pod에 sidecar가 추가되면 메모리/CPU 사용량이 증가합니다. Envoy는 Pod당 약 50-100MB 메모리를 사용합니다. 소규모 서비스에서는 과한 수 있습니다.

레이턴시 증가: 모든 요청이 Ambassador를 거치므로 약간의 지연이 추가됩니다. 일반적으로 1ms 미만이지만, 지연에 민감한 시스템에서는 고려가 필요합니다.

복잡성 증가: 디버깅 시 Ambassador 설정까지 확인해야 합니다. 로그와 메트릭을 제대로 수집하지 않으면 문제 원인 파악이 어려워집니다.

권장 사항: Service Mesh 도입 전 1-2개 서비스에 Ambassador를 먼저 적용해 효과와 오버헤드를 측정하세요. Istio의 경우 istioctl analyze로 설정 오류를 사전에 점검할 수 있습니다.

🔗 관련 용어

📚 더 배우기