Ambassador Pattern
앰배서더 패턴 / 대사 패턴
원격 서비스에 대한 프록시 역할을 하는 헬퍼 서비스를 사이드카로 배포하는 패턴입니다. 애플리케이션과 같은 Pod 또는 컨테이너 내에서 실행되며, 로깅, 모니터링, 인증, 재시도, Circuit Breaker 등 교차 관심사(Cross-Cutting Concerns)를 애플리케이션 코드에서 분리합니다. Istio, Linkerd 같은 Service Mesh의 핵심 구현 원리입니다.
앰배서더 패턴 / 대사 패턴
원격 서비스에 대한 프록시 역할을 하는 헬퍼 서비스를 사이드카로 배포하는 패턴입니다. 애플리케이션과 같은 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)과 보안을 일관되게 제공합니다.
일시적 실패에 대한 자동 재시도, 지수 백오프, 요청별 타임아웃 설정. 애플리케이션 코드 수정 불필요.
연속 실패 시 회로 차단하여 장애 전파 방지. 반개방 상태에서 점진적 복구 시도.
서비스 간 암호화 통신 자동화. 인증서 발급/갱신/검증을 투명하게 처리.
분산 트레이싱, 메트릭 수집, 접근 로깅. Jaeger, Prometheus 연동.
# 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-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"
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-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로 설정 오류를 사전에 점검할 수 있습니다.