Notice
Recent Posts
Recent Comments
Link
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

근묵자흑

Kubernetes Pattern: Adapter 본문

k8s/kubernetes-pattern

Kubernetes Pattern: Adapter

Luuuuu 2025. 11. 29. 20:03

현대의 분산 시스템 환경에서는 다양한 기술 스택이 공존합니다:

  • 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이 해결하는 문제

  1. 이기종 시스템 통합: 서로 다른 형식을 표준화
  2. 코드 수정 최소화: 기존 애플리케이션 변경 불필요
  3. 관심사 분리: 비즈니스 로직과 통합 로직 분리
  4. 유연성: 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