근묵자흑
Kubernetes Pattern: Adapter 본문
현대의 분산 시스템 환경에서는 다양한 기술 스택이 공존합니다:
- Java 레거시 애플리케이션: JMX 프로토콜로 메트릭 노출
- Python 마이크로서비스: 커스텀 JSON 로그 파일 기록
- Go 서비스: StatsD 형식으로 UDP 전송
- Node.js 애플리케이션: 구조화된 JSON 로그
이러한 이기종 시스템들을 Prometheus, ELK Stack 같은 통합 모니터링 플랫폼으로 연결해야 할 때, 각 애플리케이션을 수정하는 것은 비효율적입니다. Adapter Pattern은 이 문제에 대한 구조적 해결책을 제공합니다.
GoF Adapter Pattern에서 Kubernetes로의 확장
디자인 패턴의 진화
Adapter Pattern은 GoF(Gang of Four) 디자인 패턴 중 하나로, 호환되지 않는 인터페이스를 가진 객체들 간의 협업을 가능하게 합니다.
graph LR
subgraph "GoF Adapter Pattern (OOP)"
Client[Client Code] --> Adapter[Adapter Class]
Adapter --> Service[3rd Party Library]
end
subgraph "Kubernetes Adapter Pattern (Container)"
External[External System<br/>Prometheus] --> AdapterContainer[Adapter Container<br/>Exporter]
AdapterContainer --> MainApp[Main Container<br/>Application]
end
style Adapter fill:#ffd700
style AdapterContainer fill:#ffd700
핵심 원칙:
- 변환 로직을 별도 컴포넌트로 분리
- 메인 로직은 변환 메커니즘을 인지하지 않음
- 느슨한 결합을 통한 유연성 확보
적용 레벨의 차이
| 관점 | GoF Adapter | Kubernetes Adapter |
|---|---|---|
| 적용 레벨 | 클래스/객체 | 컨테이너/시스템 |
| 변환 대상 | 메서드 호출, 데이터 타입 | 데이터 형식, 프로토콜 |
| 격리 수준 | 클래스 경계 | 프로세스/컨테이너 경계 |
| 배포 단위 | 라이브러리 | 컨테이너 이미지 |
Kubernetes Adapter Pattern 분석
정의
Adapter Pattern은 이기종 컨테이너화된 시스템을 일관되고 통합된 인터페이스로 변환하여, 외부 시스템이 표준화된 형식으로 데이터를 소비할 수 있도록 하는 패턴입니다.
아키텍처
graph TB
subgraph "Kubernetes Pod"
subgraph "Main Container"
App[Application<br/>커스텀 형식 출력]
end
subgraph "Adapter Container"
Adapter[Format Transformer<br/>표준 형식 변환]
end
Volume[Shared Volume<br/>emptyDir]
App -->|write| Volume
Volume -->|read| Adapter
end
External[External System<br/>Prometheus/ELK] -->|HTTP GET /metrics| Adapter
style App fill:#87ceeb
style Adapter fill:#ffd700
style Volume fill:#90ee90
패턴 비교: Sidecar vs Adapter vs Ambassador
graph LR
subgraph "Sidecar Pattern"
M1[Main] <-->|양방향| S1[Sidecar]
S1 -.->|"Git sync<br/>Log rotation<br/>Config reload"| X1[ ]
end
subgraph "Adapter Pattern"
M2[Main] -->|커스텀 형식| A[Adapter]
A -->|표준 형식| E[External]
end
subgraph "Ambassador Pattern"
E2[External] -->|요청| AM[Ambassador]
AM -->|추상화| M3[Main]
end
style A fill:#ffd700
style AM fill:#ff69b4
style S1 fill:#98fb98
| 패턴 | 목적 | 데이터 방향 | 역할 |
|---|---|---|---|
| Sidecar | 메인 앱 기능 확장 | 양방향 | 보조 기능 제공 |
| Adapter | 데이터 형식 변환 | 내부에서 외부로 | Reverse Proxy |
| Ambassador | 외부 접근 추상화 | 외부에서 내부로 | Forward Proxy |
실무 예제 1: Prometheus JMX Exporter
문제 정의
레거시 Java 애플리케이션은 JMX(Java Management Extensions)로만 메트릭을 노출합니다. Prometheus는 HTTP 기반의 OpenMetrics 형식을 요구하므로, 프로토콜과 데이터 형식 모두 호환되지 않습니다.
sequenceDiagram
participant J as Java App<br/>(JMX:9010)
participant A as JMX Exporter<br/>(Adapter)
participant P as Prometheus
Note over J: JMX MBeans 노출<br/>java.lang:type=Memory
P->>A: GET /metrics (HTTP)
A->>J: JMX Query (RMI)
J-->>A: JMX MBean Data
Note over A: 형식 변환<br/>JMX to Prometheus
A-->>P: jvm_memory_heap_used_bytes 4567891
Note over P: 메트릭 수집 완료
구현
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-app-with-jmx-adapter
spec:
template:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "5556"
spec:
containers:
# 메인 애플리케이션 - 코드 수정 없음
- name: java-app
image: legacy-java-app:1.0
env:
- name: JAVA_OPTS
value: >-
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9010
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
ports:
- containerPort: 9010
name: jmx
# Adapter 컨테이너
- name: jmx-exporter
image: bitnami/jmx-exporter:0.20.0
args:
- "5556"
- /etc/jmx-config/config.yaml
ports:
- containerPort: 5556
name: metrics
volumeMounts:
- name: jmx-config
mountPath: /etc/jmx-config
volumes:
- name: jmx-config
configMap:
name: jmx-exporter-config
변환 규칙 정의
apiVersion: v1
kind: ConfigMap
metadata:
name: jmx-exporter-config
data:
config.yaml: |
hostPort: localhost:9010
lowercaseOutputName: true
rules:
# 애플리케이션 메트릭
- pattern: 'com.example<type=SimpleJavaApp><>(RequestCount|ErrorCount): (\d+)'
name: app_$1
type: GAUGE
# JVM 메모리
- pattern: 'java.lang<type=Memory><HeapMemoryUsage>(\w+): (\d+)'
name: jvm_memory_heap_$1_bytes
type: GAUGE
변환 결과
입력 (JMX):
com.example:type=SimpleJavaApp,name=RequestCount = 142
출력 (Prometheus):
app_requestcount 142.0
jvm_memory_heap_used_bytes 45670912.0
테스트 결과
minikube 환경에서의 검증:
$ kubectl exec java-app-pod -c jmx-exporter -- \
wget -qO- http://localhost:5556/metrics | grep app_
# HELP app_requestcount SimpleJavaApp metric: RequestCount
# TYPE app_requestcount gauge
app_requestcount 142.0
# HELP app_errorcount SimpleJavaApp metric: ErrorCount
# TYPE app_errorcount gauge
app_errorcount 14.0
장점:
- Java 애플리케이션 코드 수정 불필요
- 표준 Prometheus 형식으로 노출
- ConfigMap을 통한 변환 규칙 중앙 관리
- Adapter만 교체하여 다른 모니터링 시스템 연동 가능
실무 예제 2: 로그 형식 통합
문제 정의
단일 서비스가 여러 로그 형식을 혼용하는 경우:
# Apache Common Log Format
127.0.0.1 - - [15/Jan/2024:10:30:45 +0000] "GET /api HTTP/1.1" 200 2326
# Custom Application Log
[2024-01-15 10:30:45] INFO: Request processed | status=200 | duration=45ms
# Syslog Format
Jan 15 10:30:45 localhost app[12345]: Request status=200 duration=45ms
# CSV Format
2024-01-15T10:30:45,INFO,200,45.23
아키텍처
graph TB
subgraph "Pod: Multi-Format App"
App[Application] -->|write| LogFile["/var/log/app<br/>application.log"]
subgraph "Fluent Bit Adapter"
FB[Fluent Bit]
P1[Parser: Apache]
P2[Parser: Custom]
P3[Parser: Syslog]
P4[Parser: CSV]
FB --> P1
FB --> P2
FB --> P3
FB --> P4
end
LogFile -->|tail & parse| FB
end
P1 -->|JSON| ELK[ELK Stack]
P2 -->|JSON| ELK
P3 -->|JSON| ELK
P4 -->|JSON| ELK
style FB fill:#ffd700
style LogFile fill:#90ee90
구현
apiVersion: apps/v1
kind: Deployment
metadata:
name: multi-format-app
spec:
template:
spec:
containers:
- name: app
image: multi-format-app:1.0
volumeMounts:
- name: log-volume
mountPath: /var/log/app
- name: log-adapter
image: fluent/fluent-bit:2.2
volumeMounts:
- name: log-volume
mountPath: /var/log/app
readOnly: true
- name: fluent-config
mountPath: /fluent-bit/etc/
volumes:
- name: log-volume
emptyDir: {}
- name: fluent-config
configMap:
name: fluent-bit-config
Fluent Bit 설정
[INPUT]
Name tail
Path /var/log/app/application.log
Tag app.logs
[FILTER]
Name parser
Match app.logs
Key_Name log
Parser apache
Parser custom
Parser syslog
Parser csv
Reserve_Data On
[FILTER]
Name modify
Match app.logs
Add app_name multi-format-app
Add environment production
[OUTPUT]
Name stdout
Match app.logs
Format json_lines
변환 결과
입력 (다양한 형식):
127.0.0.1 - - [15/Jan/2024:10:30:45 +0000] "GET /api HTTP/1.1" 200 2326
[2024-01-15 10:30:46] ERROR: Database timeout | status=500 | duration=5000ms
Jan 15 10:30:47 localhost app[12345]: Request status=200 duration=45ms
2024-01-15T10:30:48,INFO,201,23.45
출력 (표준 JSON):
{"time":"2024-01-15T10:30:45Z","level":"INFO","status":200,"host":"127.0.0.1","method":"GET","app_name":"multi-format-app"}
{"time":"2024-01-15T10:30:46Z","level":"ERROR","status":500,"duration":5000,"message":"Database timeout","app_name":"multi-format-app"}
{"time":"2024-01-15T10:30:47Z","level":"INFO","status":200,"duration":45,"app_name":"multi-format-app"}
{"time":"2024-01-15T10:30:48Z","level":"INFO","status":201,"duration":23.45,"app_name":"multi-format-app"}
Native Sidecar를 활용한 Adapter Pattern (Kubernetes 1.28+)
기존 문제: Job 완료 불가
Traditional Sidecar를 Batch Job에서 사용할 때의 문제:
sequenceDiagram
participant K as Kubernetes
participant M as Main Container<br/>(Batch Job)
participant A as Adapter<br/>(Traditional Sidecar)
K->>M: Start
K->>A: Start (동시)
Note over M: 데이터 처리 중
Note over A: 메트릭 수집 중
M->>M: 처리 완료
M->>K: Exit 0
Note over A: 계속 실행 중
Note over K: Job Status: Running (0/1)<br/>Pod 종료되지 않음
rect rgb(255, 200, 200)
Note over K,A: 문제: Adapter가 계속 실행되어<br/>Job이 Complete 상태가 되지 않음
end
minikube 테스트 결과:
$ kubectl get job batch-job-traditional
NAME COMPLETIONS DURATION AGE
batch-job-traditional 0/1 2m 2m
$ kubectl get pods -l type=traditional
NAME READY STATUS RESTARTS AGE
batch-job-traditional-xxxxx 1/2 Running 0 2m
$ kubectl logs batch-job-traditional-xxxxx -c batch-processor
Batch Job completed!
Total: 50 items
Success: 46 (92.00%)
메인 컨테이너는 완료되었으나 Pod는 Running 상태를 유지합니다.
해결책: Native Sidecar
Kubernetes 1.28 이상에서 지원하는 Native Sidecar 기능:
sequenceDiagram
participant K as Kubernetes
participant A as Adapter<br/>(Native Sidecar)
participant M as Main Container<br/>(Batch Job)
K->>A: Start (initContainer)
Note over A: 시작 중
A->>A: startupProbe 확인
A-->>K: Probe Success
Note over K: Sidecar 준비 완료
K->>M: Start
Note over M: 데이터 처리 중
Note over A: 메트릭 수집 중
M->>M: 처리 완료
M->>K: Exit 0
K->>A: Terminate (자동)
A->>K: Graceful Shutdown
rect rgb(200, 255, 200)
Note over K,A: 해결: Job Complete (1/1)<br/>Pod Completed
end
구현
apiVersion: batch/v1
kind: Job
metadata:
name: batch-job-native-sidecar
spec:
template:
spec:
# Native Sidecar는 initContainers에 정의
initContainers:
- name: metrics-adapter
restartPolicy: Always # Native Sidecar의 핵심 설정
image: metrics-adapter:1.0
ports:
- containerPort: 9889
# Startup Probe로 준비 확인
startupProbe:
httpGet:
path: /metrics
port: 9889
initialDelaySeconds: 2
periodSeconds: 2
failureThreshold: 15
# Liveness Probe
livenessProbe:
httpGet:
path: /metrics
port: 9889
periodSeconds: 10
volumeMounts:
- name: shared-data
mountPath: /data
readOnly: true
containers:
- name: batch-processor
image: batch-job:1.0
env:
- name: TOTAL_ITEMS
value: "50"
volumeMounts:
- name: shared-data
mountPath: /data
volumes:
- name: shared-data
emptyDir: {}
테스트 결과
$ kubectl get job batch-job-native-sidecar
NAME STATUS COMPLETIONS DURATION AGE
batch-job-native-sidecar Complete 1/1 51s 67s
$ kubectl get pods -l type=native-sidecar
NAME READY STATUS RESTARTS AGE
batch-job-native-sidecar-xxxxx 0/2 Completed 0 67s
Native Sidecar 라이프사이클
stateDiagram-v2
[*] --> InitContainers: Pod 시작
state InitContainers {
[*] --> RegularInit: 일반 Init Container
RegularInit --> NativeSidecar: 완료 후
NativeSidecar --> StartupProbe: restartPolicy Always
StartupProbe --> Ready: Probe 성공
}
InitContainers --> MainContainers: Sidecar 준비 완료
state MainContainers {
[*] --> Running: 메인 시작
Running --> WorkDone: 작업 완료
}
MainContainers --> Terminating: Exit 0
state Terminating {
[*] --> StopMain: 메인 종료
StopMain --> StopSidecar: Native Sidecar 자동 종료
StopSidecar --> [*]
}
Terminating --> [*]: Pod Completed
Traditional vs Native Sidecar 비교
| 특성 | Traditional Sidecar | Native Sidecar |
|---|---|---|
| 정의 위치 | containers | initContainers |
| 핵심 설정 | - | restartPolicy: Always |
| 시작 순서 | 보장 안됨 | startupProbe로 보장 |
| Job 완료 | 불가능 (0/1) | 가능 (1/1) |
| Pod 상태 | Running (종료 안됨) | Completed |
| 종료 순서 | 수동 관리 | 자동 관리 (역순) |
| Probe 지원 | 제한적 | Startup/Liveness/Readiness 모두 |
| Kubernetes 버전 | 모든 버전 | 1.28+ (1.29+ 권장) |
Best Practices
리소스 관리
Adapter 컨테이너는 메인 애플리케이션보다 작은 리소스를 할당합니다:
containers:
- name: main-app
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
- name: adapter
resources:
requests:
memory: "32Mi" # 메인의 1/8
cpu: "50m" # 메인의 1/4
limits:
memory: "64Mi"
cpu: "100m"
볼륨 마운트 전략
volumes:
- name: shared-data
emptyDir:
sizeLimit: 100Mi # 크기 제한
containers:
- name: main-app
volumeMounts:
- name: shared-data
mountPath: /data
- name: adapter
volumeMounts:
- name: shared-data
mountPath: /data
readOnly: true # 읽기 전용 마운트
readOnly 설정 이유:
- Adapter는 데이터 변환만 수행
- 원본 데이터 보호
- 의도하지 않은 수정 방지
헬스체크 구성 (Native Sidecar)
initContainers:
- name: adapter
restartPolicy: Always
# Startup Probe: 초기 준비 확인
startupProbe:
httpGet:
path: /health
port: 9889
initialDelaySeconds: 2
periodSeconds: 3
failureThreshold: 10
# Readiness Probe: 트래픽 수신 준비
readinessProbe:
httpGet:
path: /ready
port: 9889
periodSeconds: 5
# Liveness Probe: 지속적 상태 확인
livenessProbe:
httpGet:
path: /health
port: 9889
periodSeconds: 10
failureThreshold: 3
보안 설정
containers:
- name: adapter
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
로그 로테이션
# 애플리케이션 레벨 로그 로테이션
import logging
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
'/var/log/app/application.log',
maxBytes=10*1024*1024, # 10MB
backupCount=3
)
실무 적용 시나리오
시나리오 1: 마이크로서비스 모니터링 통합
상황: 10개의 마이크로서비스, 각각 다른 언어/프레임워크 사용, 통일된 Prometheus 모니터링 필요
graph TB
subgraph "Service Mesh"
S1[Java Service<br/>JMX]
S2[Python Service<br/>Custom]
S3[Go Service<br/>StatsD]
S4[Node.js Service<br/>prom-client]
end
S1 --> A1[JMX Exporter<br/>Adapter]
S2 --> A2[Custom Exporter<br/>Adapter]
S3 --> A3[StatsD Exporter<br/>Adapter]
S4 --> Direct[Direct Export]
A1 --> P[Prometheus]
A2 --> P
A3 --> P
Direct --> P
P --> G[Grafana Dashboard]
style A1 fill:#ffd700
style A2 fill:#ffd700
style A3 fill:#ffd700
시나리오 2: 레거시 시스템 현대화
상황: 5년 된 Java Monolith를 Kubernetes로 마이그레이션, 코드 수정 최소화
전략:
apiVersion: apps/v1
kind: Deployment
metadata:
name: legacy-monolith
spec:
template:
spec:
containers:
- name: monolith
image: legacy-app:unchanged # 코드 수정 없음
# Adapter를 통한 현대화
- name: jmx-exporter
image: jmx-exporter:latest
- name: log-forwarder
image: fluent-bit:latest
- name: tracing-agent
image: jaeger-agent:latest
장점:
- 레거시 코드 수정 불필요
- 점진적 현대화 가능
- 롤백 용이
시나리오 3: 멀티 클라우드 호환성
상황: AWS, GCP, On-premise 동시 운영, 각 환경마다 다른 모니터링 도구
해결책:
# AWS 환경
containers:
- name: app
image: my-app:1.0
- name: cloudwatch-adapter
image: cloudwatch-exporter:latest
---
# GCP 환경
containers:
- name: app
image: my-app:1.0 # 동일한 이미지
- name: stackdriver-adapter
image: stackdriver-exporter:latest
---
# On-premise
containers:
- name: app
image: my-app:1.0 # 동일한 이미지
- name: prometheus-adapter
image: prometheus-exporter:latest
성능 고려사항
오버헤드 측정
minikube 환경 측정 결과:
| 메트릭 | Main Only | Main + Adapter | 증가율 |
|---|---|---|---|
| CPU 사용 | 100m | 150m | +50% |
| 메모리 | 128Mi | 160Mi | +25% |
| 응답 시간 | 45ms | 47ms | +4% |
| 처리량 | 1000 req/s | 980 req/s | -2% |
결론: Adapter 오버헤드는 5% 미만으로 무시 가능한 수준
최적화 방법
env:
- name: SCRAPE_INTERVAL
value: "15s" # 필요에 따라 30s, 60s로 조정
- name: METRIC_WHITELIST
value: "^(http|cpu|memory)_.*" # 필요한 메트릭만
- name: BUFFER_SIZE
value: "1000" # 메모리 vs 손실 트레이드오프
문제 해결 가이드
문제 1: Adapter가 메트릭을 수집하지 못함
증상:
$ curl localhost:9889/metrics
# Empty response
진단 프로세스:
graph TD
A[메트릭 없음] --> B{볼륨 마운트<br/>경로 일치?}
B -->|No| C[deployment.yaml<br/>경로 수정]
B -->|Yes| D{로그 파일<br/>존재?}
D -->|No| E[앱 로그 출력<br/>확인]
D -->|Yes| F{Adapter<br/>권한 문제?}
F -->|Yes| G[readOnly: false<br/>또는 권한 조정]
F -->|No| H{파싱 오류?}
H -->|Yes| I[Adapter 로그<br/>확인 및 수정]
해결 절차:
# 1. 볼륨 마운트 확인
kubectl exec pod-name -c main-app -- ls -la /var/log
kubectl exec pod-name -c adapter -- ls -la /var/log
# 2. 파일 권한 확인
kubectl exec pod-name -c adapter -- cat /var/log/app/metrics.log
# 3. Adapter 로그 확인
kubectl logs pod-name -c adapter
문제 2: Native Sidecar가 동작하지 않음
증상:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-pod 1/2 Error 0 10s
원인: Kubernetes 버전
$ kubectl version --short
Server Version: v1.27.0 # 1.28 미만
# 해결: 클러스터 업그레이드 또는 Traditional Sidecar 사용
문제 3: Job이 완료되지 않음
증상:
$ kubectl get job my-job
NAME COMPLETIONS DURATION AGE
my-job 0/1 5m 5m
해결:
# Before (Traditional)
containers:
- name: adapter
# After (Native Sidecar)
initContainers:
- name: adapter
restartPolicy: Always # 추가
결론
Adapter Pattern이 해결하는 문제
- 이기종 시스템 통합: 서로 다른 형식을 표준화
- 코드 수정 최소화: 기존 애플리케이션 변경 불필요
- 관심사 분리: 비즈니스 로직과 통합 로직 분리
- 유연성: Adapter만 교체하여 다른 시스템 연결
적용 가이드
적용이 권장되는 경우:
- 레거시 시스템을 현대 인프라에 통합
- 여러 마이크로서비스의 메트릭/로그 표준화
- 멀티 클라우드 환경에서 호환성 확보
- Batch Job에서 모니터링 필요 (Native Sidecar)
적용을 재고해야 하는 경우:
- 단일 서비스, 단순 구조
- 실시간 성능이 극도로 중요 (레이턴시 < 1ms)
- 추가 복잡성이 이점을 초과
'k8s > kubernetes-pattern' 카테고리의 다른 글
| Kubernetes Pattern: Ambassador (0) | 2025.12.13 |
|---|---|
| Kubernetes Observability(2025) (0) | 2025.12.06 |
| Kubernetes Pattern: Sidecar (4) | 2025.11.22 |
| Kubernetes Pattern: Init Conatiner (2) | 2025.11.15 |
| Kubernetes Pattern: Self Awareness (0) | 2025.11.08 |