0. Contents


1. Overview

Ambassador 패턴 — 핵심 아이디어메인 앱은 외부 서비스를 써야 한다(바뀌는 주소, 샤딩, 재시도, 형식 변환...)같은 Pod 안에 Ambassador 사이드카 컨테이너를 붙인다(localhost에서 듣고 → 바깥세상을 처리)메인 컨테이너(비즈니스 로직만)localhost와만 통신Ambassador 컨테이너(바깥으로 가는 스마트 프록시)환경마다 교체 가능앱은 언제나 단순한 로컬 엔드포인트 하나만 본다
Ambassador 패턴의 핵심: 앱은 localhost 하나만 보고, 바깥은 Ambassador가 맡는다

컨테이너화된 서비스의 외부 의존

  • 컨테이너화된 서비스는 혼자 살지 않는다
  • 데이터베이스, 캐시, 외부 API — 바깥의 다른 서비스에 연락해야 일을 할 수 있다
  • 문제는 이 “바깥에 연락하는 일"이 생각보다 복잡하다는 것이다

Ambassador 패턴: 복잡함을 메인 앱에서 걷어내기

  • Ambassador 패턴은 이 복잡함을 메인 앱에서 걷어낸다
  • 같은 Pod 안에 외부 통신을 전담하는 앰배서더 컨테이너를 붙인다
  • 메인 앱은 localhost로만 통신한다
  • 외국에 파견된 대사(Ambassador)가 현지의 복잡한 사정을 본국 대신 처리하듯
    • 앱은 대사에게 말만 하면 된다
    • 바깥세상과의 협상은 대사가 전담한다

Adapter와 반대 방향

  • 앞 장의 Adapter: 외부 → 내부로 들어오는 요청을 표준화
  • Ambassador: 정확히 반대 방향인 내부 → 외부로 나가는 요청을 추상화

2. Problem

2.1 외부 접근이 어려운 네 가지 이유

안정적인 외부 접근을 막는 요인

  • 외부 서비스에 안정적으로 접근하기 어려운 이유는 크게 네 가지다
이유구체적인 모습
① 동적으로 변하는 주소어제: cache → 10.0.0.5
오늘: cache → 10.0.0.17 (서버 재시작, 스케일링…)
② 로드 밸런싱 필요DB 서버가 3대인데, 요청을 누가 어떻게 분산하나?
③ 불안정한 프로토콜HTTP는 실패한다. 재시도/타임아웃/차단 로직은 누가 짜나?
④ 어려운 데이터 형식레거시 시스템의 특이한 포맷, 변환은 누가 하나?

클라우드에서는 일상

  • 클라우드 환경에서 이 네 가지는 예외 상황이 아니라 일상이다
    • 서버는 죽었다 살아나며 IP가 바뀐다
    • 서비스는 여러 인스턴스로 흩어져 있다
    • 네트워크는 언제든 실패한다

2.2 책임이 두 개가 되는 순간

이상적인 컨테이너의 조건

  • 단일 목적(single-purposed) 이어야 한다
  • 다양한 맥락에서 재사용 가능해야 한다

네 가지 문제를 메인 앱이 직접 처리할 때: 주문 처리 앱의 코드

  • ✅ 주문 처리 로직 ← 원래 하려던 일
  • ❌ 서비스 디스커버리 라이브러리 ← 어? 이건 왜
  • ❌ 샤드 라우팅 계산 ← 이것도…
  • ❌ 재시도 / 서킷브레이커 ← 이것도…
  • ❌ 데이터 형식 변환 ← 이것도…

책임이 둘 이상일 때 잃는 것

  • “주문 처리 앱"이 “주문 처리 + 네트워크 잡일 앱"이 된다
  • 책임이 두 개 이상이 되는 순간 두 가지를 잃는다

재사용성 상실

  • Consul 클라이언트가 내장된 앱은 Consul 없는 환경에서 못 쓴다
  • 다른 프로젝트에서 이 앱을 가져다 쓰려면 쓸모없는 짐까지 함께 짊어져야 한다

변경 비용 폭증

  • 디스커버리 방식이 바뀌면 앱 전체를 수정하고 재빌드해야 한다
    • 비즈니스 로직은 그대로인데도

패턴의 목표: 두 요구를 동시에 만족

  • 외부 접근용 라이브러리를 앱 컨테이너에 넣고 싶지 않다
  • 환경에 따라 접근 방식을 갈아끼우고 싶다
  • 이 두 요구를 동시에 만족시키는 것이 Ambassador 패턴의 목표다

3. Solution — Ambassador 패턴

3.1 동작 구조

기술적 기반: 네트워크 네임스페이스 공유

  • 같은 Pod 안의 컨테이너들은 네트워크 네임스페이스를 공유한다
  • 이것이 패턴의 기술적 기반이다
  • 한집에 사는 룸메이트끼리 방문만 두드리면 되듯, 같은 Pod 안에서는 localhost로 서로 통신할 수 있다
Pod메인 앱(비즈니스 로직)요청 보내는 곳localhost:9009Ambassador(etcd 클라이언트 등)listen :9009복잡한 일 처리9009분산 접근원격 클러스터etcdetcdetcd
앱은 localhost:9009에 던지고, 원격 클러스터와의 통신은 Ambassador가 맡는다

컨테이너별 단일 책임

  • 메인 앱은 그냥 localhost:9009에 던진다
  • 주소 추적·샤딩·재시도 같은 복잡한 일은 전부 앰배서더가 맡는다
  • 각 컨테이너의 단일 책임이 명확하다
컨테이너역할모르는 것
Main App비즈니스 로직 수행, localhost로 요청외부 서비스의 주소, 개수, 프로토콜
Ambassador외부 서비스와의 실제 통신 전담메인 앱의 비즈니스 로직

복잡함의 위치 이동

  • 메인 앱 입장에서는 바로 옆에 서비스 하나가 있는 것처럼 보인다
  • 실제로는 앰배서더가 뒤에서 멀리 있는 분산 클러스터와 통신하고 있다
  • 복잡함이 사라진 게 아니라, 위치가 옮겨진 것이다

3.2 대표 시나리오 세 가지

책이 제시하는 활용 사례

  • 책이 제시하는 세 가지 활용 사례를 보면 이 패턴이 커버하는 범위가 보인다

① 캐시 샤딩

  • 개발 환경에서는 캐시 1개면 된다
  • 운영 환경에서는 데이터가 여러 샤드에 쪼개져 있다
  • "user123"은 어느 샤드에?를 계산하는 라우팅 로직을 앰배서더가 전담한다
개발App캐시 1개간단운영App샤드1?샤드2?샤드3?복잡 → Ambassador 담당"user123"은 어느 샤드에?
캐시 샤딩: 개발은 캐시 1개, 운영은 샤드 라우팅을 Ambassador가 맡는다

② 클라이언트 사이드 서비스 디스커버리

  • 레지스트리(주소록 서버)에 “결제 서비스 지금 어디 있어?“를 물어보고 찾아가는 방식
  • 이 조회 코드를 앱에 넣는 대신 앰배서더가 대신 조회한다

③ 회복탄력성(Resiliency)

  • HTTP 같은 신뢰할 수 없는 프로토콜을 쓸 때 필요한 보호 장치들을 앰배서더에 몰아넣는다
장치역할
Timeout“N초 기다려도 응답 없으면 끊는다”
Retry“실패하면 N번까지 다시 시도한다”
Circuit Breaker“계속 실패하면 당분간 아예 요청을 차단한다” — 죽은 서비스에 요청을 퍼붓다 내 앱까지 함께 죽는 연쇄 장애를 막는 두꺼비집

3.3 환경별 교체 — etcd에서 memcached로

패턴의 진가: 교체

  • 이 패턴의 진가는 교체에서 드러난다
  • 운영 환경에서는 원격 분산 저장소(etcd)
  • 개발 환경에서는 로컬 인메모리 캐시(memcached)
  • 앰배서더만 갈아끼우면 된다
운영 환경 (Figure 18-1)Applocalhostetcd client Ambassador개발 환경 (Figure 18-2)Applocalhostmemcached Ambassadoretcd 클러스터(원격/분산)(로컬에서 끝)↑ App은 두 경우 모두 완전히 동일. 한 글자도 안 바뀐다.
앰배서더만 갈아끼우고 App은 그대로 둔다

카트리지 교체 구조

  • 게임기 본체는 그대로 두고 카트리지만 바꿔 끼우는 구조다
  • 개발할 땐 무거운 etcd 클러스터 없이 가볍게 테스트한다
  • 배포할 땐 앰배서더 이미지만 교체한다

3.4 Example 18-1 YAML 분석

책의 예제: 로깅 앰배서더

  • REST 서비스가 응답을 반환하기 전, 생성된 데이터를 고정 URL http://localhost:9009로 던진다
  • 앰배서더가 이 포트에서 받아 처리한다
apiVersion: v1
kind: Pod
metadata:
  name: random-generator
  labels:
    app: random-generator
spec:
  containers:
  # ① 메인 앱 컨테이너 — 랜덤 숫자 REST 서비스, 8080 포트
  - image: k8spatterns/random-generator:1.0
    name: main
    env:
    - name: LOG_URL
      value: http://localhost:9009   # 로그 목적지를 환경변수로 주입
    ports:
    - containerPort: 8080            # 외부 공개 포트
      protocol: TCP

  # ② 앰배서더 컨테이너 — 9009에서 대기, 로그 처리 전담
  - image: k8spatterns/random-generator-log-ambassador
    name: ambassador                 # localhost에서만 listen = Pod 내부 전용

설계 포인트 ①: 환경변수로 목적지 주입

  • 앱 코드에 주소가 하드코딩되어 있지 않다
  • LOG_URL만 바꾸면 목적지를 바꿀 수 있다
  • 앱은 “환경변수의 URL로 던진다"는 것만 안다

설계 포인트 ②: 9009 포트는 밖에 안 보인다

  • 앰배서더는 server.listen(9009, "localhost")로 localhost에서만 듣는다 (3.5의 코드)
  • 그래서 9009는 같은 Pod 안의 컨테이너만 닿을 수 있는 비공개 창구다
    • 바깥에서는 로그 처리 경로가 존재하는지조차 알 수 없다
  • 메인 컨테이너의 8080은 모든 인터페이스(0.0.0.0)에서 듣기 때문에 Pod 밖에서도 닿는다
  • containerPort는 정보 표시용이라 접근 여부를 정하지 않는다
    • 선언하지 않아도 0.0.0.0에서 듣는 포트는 Pod IP로 닿는다
    • 선언했다고 localhost에서만 듣는 포트가 열리지도 않는다
외부 사용자Podmainlisten 0.0.0.0:8080ambassadorlisten localhost:9009:8080 ✅ (닿음):9009 ❌(안 닿음)localhost:9009 ✅(Pod 내부 전용)
8080은 모든 인터페이스에서 듣고, 9009는 localhost에서만 들어 Pod 안에서만 닿는다

설계 포인트 ③: 교체는 YAML 한 줄

  • 콘솔 출력 대신 Elasticsearch로 보내고 싶다면, 앰배서더의 image: 한 줄만 바꾼다
  • 메인 앱은 코드도, 이미지도, 재빌드도 필요 없다
    • REST 서비스 입장에서는 로그 데이터가 그 뒤에 어떻게 되든 알 바 아니기 때문이다

3.5 앰배서더의 실체 — log_ambassador.js

image: 한 줄 뒤의 실제 프로그램

  • YAML의 image: 한 줄 뒤에는 당연히 실제 프로그램이 있다
  • 공식 예제 저장소의 image/ 디렉토리에 앰배서더의 소스가 그대로 공개되어 있다

예제 디렉토리 구성: structural/Ambassador/

  • pod.yml ← Example 18-1
  • image/
    • Dockerfile ← 이미지 빌드 레시피
    • log_ambassador.js ← 앰배서더 본체 (Node.js)

본체: 단순한 Node.js HTTP 서버

  • 본체는 놀랍도록 단순한 Node.js HTTP 서버다
// log_ambassador.js — 핵심만 발췌
server = http.createServer(function (req, res) {
  if (req.method == 'POST') {
    // ① 메인 앱이 POST로 던진 로그 데이터를 수신
    req.on('end', function () {
      var resp = JSON.parse(body);
      // ② 콘솔에 출력 — 실전에서는 이 부분만
      //    로깅 인프라 전송 코드로 바꾸면 된다
      console.log(">>> ID: " + resp.id +
                  " -- Duration: " + resp.duration +
                  " -- Random: " + resp.random);
    });
  }
  res.writeHead(200);
  res.end();
});

// ③ YAML의 LOG_URL과 정확히 맞물리는 지점
server.listen(9009, "localhost");
# Dockerfile — 4줄로 포장 완료
FROM node
EXPOSE 9009
COPY log_ambassador.js /opt
CMD node /opt/log_ambassador.js

코드에서 Pod까지의 흐름

  • JS 코드 작성 → Dockerfile로 이미지 빌드 → 레지스트리 업로드 → YAML이 이미지를 지정 → Pod 안에서 실행
  • YAML에 앰배서더가 딱 두 줄이었던 이유
    • 복잡한 작업이 이미 이미지로 포장돼 있다
    • YAML은 그 완성품을 가리키기만 하면 된다
복잡한 작업은 이미지로 포장1. JS 코드 작성log_ambassador.js · Node.js HTTP 서버2. Dockerfile로 이미지 빌드FROM node · EXPOSE 9009 · COPY · CMD3. 레지스트리 업로드4. YAML이 이미지를 지정image: k8spatterns/random-generator-log-ambassador5. Pod 안에서 실행확인: kubectl logs -f random-generator -c ambassador↑ YAML은 완성품을 가리키기만 한다
복잡한 작업은 이미지에 포장되고, YAML은 완성품을 가리키기만 한다

동작 확인

  • 실행 후 kubectl logs -f random-generator -c ambassador로 앰배서더가 실제로 데이터를 받는 모습을 확인할 수 있다

4. Discussion

4.1 사이드카와 무엇이 다른가

Sidecar의 특수화

  • Ambassador는 Sidecar 패턴의 특수화다
  • 결정적인 차이는 기능을 더해주지 않는다는 점이다
일반 SidecarSidecarApp앱에 새 능력을 추가 (파일 동기화, TLS...)"나를 업그레이드해주는 코치"AmbassadorAppAmbassador외부외부 통신을 대신함"나 대신 바깥에 심부름 다녀오는 비서"
Sidecar는 능력을 더하고, Ambassador는 바깥 통신을 대신한다

smart proxy: 중계 역할

  • 앰배서더는 앱을 더 강하게 만들지 않는다
  • 앱이 원래 하려던 “바깥에 연락하기"를 더 똑똑하게 중계할 뿐이다
  • 그래서 책은 이것을 smart proxy라 부르고, Proxy 패턴이라는 별칭으로도 불린다

4.2 레거시 앱의 무수술 현대화

레거시에서 빛나는 패턴

  • 이 패턴이 특히 빛나는 곳은 레거시다
  • 건드리면 터질 것 같은 10년 된 앱
    • 코드를 짠 사람은 퇴사했다
    • 빌드하는 법은 아무도 모른다
  • 코드를 한 줄도 안 고치고 옆에 앰배서더만 붙인다
레거시 앱(그대로 둠)Ambassador외부요청 계측(모니터링)통신 기록(로깅)새 서버로 안내(라우팅)재시도/차단(회복탄력성)
레거시 앱은 그대로 두고, 네트워킹 기능은 Ambassador가 붙인다

무수술 이식

  • 현대적 네트워킹 기능이 무수술로 이식된다
  • 서비스 메시(Istio 등)의 사이드카 프록시도 같은 원리를 쓴다
    • 다만 메시의 Envoy는 나가는 트래픽뿐 아니라 들어오는 트래픽도 맡는다
    • Ambassador의 아이디어를 양방향으로 넓힌 셈이다

4.3 Sidecar 패턴 3형제 완결

3형제 구도의 완성

  • 17장에서 예고한 3형제 구도가 이 장으로 완성된다
  • Adapter와 Ambassador는 정확히 거울상이다
Sidecar 패턴(범용 — Chapter 16)Adapter 패턴 (Chapter 17)방향: 외부 → 내부 (Reverse Proxy)목적: 이질적인 내부를 표준 인터페이스로 노출예시: Prometheus exporter, 로그 정규화기Ambassador 패턴 (Chapter 18)방향: 내부 → 외부 (Forward Proxy)목적: 외부 서비스 연결을 내부에서 추상화예시: DB/캐시 프록시, 서비스 디스커버리, 서킷브레이커← 이번 장
Adapter와 Ambassador는 방향만 다른 거울상이다
구분SidecarAdapterAmbassador
역할보조 기능 일반형식 변환 특화외부 연결 특화
방향내부 지원외부→내부 (Reverse)내부→외부 (Forward)
누구를 위해 숨기나—외부 시스템을 위해 내부를 숨김내부 앱을 위해 외부를 숨김
예시로그 수집, 모니터링Prometheus 변환DB 연결, 캐시 샤딩, 재시도

대칭적인 이점

  • 앱은 비즈니스 로직에 집중하며 단일 목적을 유지한다
  • 재사용의 혜택은 양방향으로 발생한다
    • 잘 만든 Redis 앰배서더 하나를 주문 앱, 결제 앱, 리뷰 앱 어디에나 조합할 수 있다
    • 앱 블록과 앰배서더 블록을 각각 한 번만 만들면 조합은 공짜다
앰배서더 하나 → 여러 앱Redis 앰배서더주문 앱결제 앱리뷰 앱앱 하나 → 여러 앰배서더Appetcd client Ambassador(운영)memcached Ambassador(개발)
앱 블록과 앰배서더 블록을 한 번씩만 만들면 조합은 공짜다

Ambassador 패턴의 핵심은 “메인 앱이 바깥세상을 전혀 몰라도 된다는 것” 이다. 앱은 localhost의 고정 주소로 말만 하고, 주소 추적, 분산, 재시도, 변환 — 바깥과의 모든 협상은 대사가 전담한다. Adapter가 안을 숨겨 밖을 편하게 했다면, Ambassador는 밖을 숨겨 안을 편하게 한다.