0. Contents
1. Overview
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로 서로 통신할 수 있다
앱은 localhost:9009에 던지고, 원격 클러스터와의 통신은 Ambassador가 맡는다
컨테이너별 단일 책임
- 메인 앱은 그냥
localhost:9009에 던진다 - 주소 추적·샤딩·재시도 같은 복잡한 일은 전부 앰배서더가 맡는다
- 각 컨테이너의 단일 책임이 명확하다
| 컨테이너 | 역할 | 모르는 것 |
|---|---|---|
| Main App | 비즈니스 로직 수행, localhost로 요청 | 외부 서비스의 주소, 개수, 프로토콜 |
| Ambassador | 외부 서비스와의 실제 통신 전담 | 메인 앱의 비즈니스 로직 |
복잡함의 위치 이동
- 메인 앱 입장에서는 바로 옆에 서비스 하나가 있는 것처럼 보인다
- 실제로는 앰배서더가 뒤에서 멀리 있는 분산 클러스터와 통신하고 있다
- 복잡함이 사라진 게 아니라, 위치가 옮겨진 것이다
3.2 대표 시나리오 세 가지
책이 제시하는 활용 사례
- 책이 제시하는 세 가지 활용 사례를 보면 이 패턴이 커버하는 범위가 보인다
① 캐시 샤딩
- 개발 환경에서는 캐시 1개면 된다
- 운영 환경에서는 데이터가 여러 샤드에 쪼개져 있다
"user123"은 어느 샤드에?를 계산하는 라우팅 로직을 앰배서더가 전담한다
캐시 샤딩: 개발은 캐시 1개, 운영은 샤드 라우팅을 Ambassador가 맡는다
② 클라이언트 사이드 서비스 디스커버리
- 레지스트리(주소록 서버)에 “결제 서비스 지금 어디 있어?“를 물어보고 찾아가는 방식
- 이 조회 코드를 앱에 넣는 대신 앰배서더가 대신 조회한다
③ 회복탄력성(Resiliency)
- HTTP 같은 신뢰할 수 없는 프로토콜을 쓸 때 필요한 보호 장치들을 앰배서더에 몰아넣는다
| 장치 | 역할 |
|---|---|
| Timeout | “N초 기다려도 응답 없으면 끊는다” |
| Retry | “실패하면 N번까지 다시 시도한다” |
| Circuit Breaker | “계속 실패하면 당분간 아예 요청을 차단한다” — 죽은 서비스에 요청을 퍼붓다 내 앱까지 함께 죽는 연쇄 장애를 막는 두꺼비집 |
3.3 환경별 교체 — etcd에서 memcached로
패턴의 진가: 교체
- 이 패턴의 진가는 교체에서 드러난다
- 운영 환경에서는 원격 분산 저장소(etcd)
- 개발 환경에서는 로컬 인메모리 캐시(memcached)
- 앰배서더만 갈아끼우면 된다
앰배서더만 갈아끼우고 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에서만 듣는 포트가 열리지도 않는다
8080은 모든 인터페이스에서 듣고, 9009는 localhost에서만 들어 Pod 안에서만 닿는다
설계 포인트 ③: 교체는 YAML 한 줄
- 콘솔 출력 대신 Elasticsearch로 보내고 싶다면, 앰배서더의
image:한 줄만 바꾼다 - 메인 앱은 코드도, 이미지도, 재빌드도 필요 없다
- REST 서비스 입장에서는 로그 데이터가 그 뒤에 어떻게 되든 알 바 아니기 때문이다
3.5 앰배서더의 실체 — log_ambassador.js
image: 한 줄 뒤의 실제 프로그램
- YAML의
image:한 줄 뒤에는 당연히 실제 프로그램이 있다 - 공식 예제 저장소의
image/디렉토리에 앰배서더의 소스가 그대로 공개되어 있다
예제 디렉토리 구성: structural/Ambassador/
pod.yml← Example 18-1image/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은 그 완성품을 가리키기만 하면 된다
복잡한 작업은 이미지에 포장되고, YAML은 완성품을 가리키기만 한다
동작 확인
- 실행 후
kubectl logs -f random-generator -c ambassador로 앰배서더가 실제로 데이터를 받는 모습을 확인할 수 있다
4. Discussion
4.1 사이드카와 무엇이 다른가
Sidecar의 특수화
- Ambassador는 Sidecar 패턴의 특수화다
- 결정적인 차이는 기능을 더해주지 않는다는 점이다
Sidecar는 능력을 더하고, Ambassador는 바깥 통신을 대신한다
smart proxy: 중계 역할
- 앰배서더는 앱을 더 강하게 만들지 않는다
- 앱이 원래 하려던 “바깥에 연락하기"를 더 똑똑하게 중계할 뿐이다
- 그래서 책은 이것을 smart proxy라 부르고, Proxy 패턴이라는 별칭으로도 불린다
4.2 레거시 앱의 무수술 현대화
레거시에서 빛나는 패턴
- 이 패턴이 특히 빛나는 곳은 레거시다
- 건드리면 터질 것 같은 10년 된 앱
- 코드를 짠 사람은 퇴사했다
- 빌드하는 법은 아무도 모른다
- 코드를 한 줄도 안 고치고 옆에 앰배서더만 붙인다
레거시 앱은 그대로 두고, 네트워킹 기능은 Ambassador가 붙인다
무수술 이식
- 현대적 네트워킹 기능이 무수술로 이식된다
- 서비스 메시(Istio 등)의 사이드카 프록시도 같은 원리를 쓴다
- 다만 메시의 Envoy는 나가는 트래픽뿐 아니라 들어오는 트래픽도 맡는다
- Ambassador의 아이디어를 양방향으로 넓힌 셈이다
4.3 Sidecar 패턴 3형제 완결
3형제 구도의 완성
- 17장에서 예고한 3형제 구도가 이 장으로 완성된다
- Adapter와 Ambassador는 정확히 거울상이다
Adapter와 Ambassador는 방향만 다른 거울상이다
| 구분 | Sidecar | Adapter | Ambassador |
|---|---|---|---|
| 역할 | 보조 기능 일반 | 형식 변환 특화 | 외부 연결 특화 |
| 방향 | 내부 지원 | 외부→내부 (Reverse) | 내부→외부 (Forward) |
| 누구를 위해 숨기나 | — | 외부 시스템을 위해 내부를 숨김 | 내부 앱을 위해 외부를 숨김 |
| 예시 | 로그 수집, 모니터링 | Prometheus 변환 | DB 연결, 캐시 샤딩, 재시도 |
대칭적인 이점
- 앱은 비즈니스 로직에 집중하며 단일 목적을 유지한다
- 재사용의 혜택은 양방향으로 발생한다
- 잘 만든 Redis 앰배서더 하나를 주문 앱, 결제 앱, 리뷰 앱 어디에나 조합할 수 있다
- 앱 블록과 앰배서더 블록을 각각 한 번만 만들면 조합은 공짜다
앱 블록과 앰배서더 블록을 한 번씩만 만들면 조합은 공짜다
Ambassador 패턴의 핵심은 “메인 앱이 바깥세상을 전혀 몰라도 된다는 것” 이다. 앱은 localhost의 고정 주소로 말만 하고, 주소 추적, 분산, 재시도, 변환 — 바깥과의 모든 협상은 대사가 전담한다. Adapter가 안을 숨겨 밖을 편하게 했다면, Ambassador는 밖을 숨겨 안을 편하게 한다.
