🏗️ 아키텍처

Sidecar Pattern

사이드카 패턴

메인 애플리케이션 컨테이너 옆에 보조 컨테이너(사이드카)를 배치하여 로깅, 모니터링, 프록시, 보안 등의 횡단 관심사(Cross-cutting Concerns)를 분리하는 마이크로서비스 아키텍처 패턴입니다. Kubernetes의 Pod에서 하나의 Pod 안에 여러 컨테이너를 배치하는 방식으로 구현됩니다.

📖 상세 설명

패턴 원리: 사이드카 패턴은 오토바이 옆에 부착된 사이드카에서 이름을 따왔습니다. 메인 애플리케이션(오토바이)은 비즈니스 로직에만 집중하고, 부가 기능(승객)은 사이드카 컨테이너가 담당합니다. 두 컨테이너는 같은 네트워크 네임스페이스와 스토리지를 공유하므로 localhost로 통신하고 파일 시스템을 공유할 수 있습니다.

사용 시나리오: 가장 대표적인 사용 사례는 Service Mesh(Istio, Linkerd)에서 Envoy 프록시를 사이드카로 배치하는 것입니다. 모든 네트워크 트래픽이 사이드카를 통과하면서 mTLS 암호화, 트래픽 제어, 관측성(Observability)이 자동으로 적용됩니다. 로그 수집(Fluentd), 설정 동기화(Config Sync), 시크릿 주입(Vault Agent) 등도 사이드카로 구현합니다.

장점: 애플리케이션 코드 수정 없이 기능을 추가할 수 있습니다. 언어/프레임워크에 독립적이므로 폴리글랏 환경에서 일관된 인프라 기능을 제공합니다. 사이드카만 업그레이드하면 되므로 유지보수가 쉽고, 관심사 분리로 테스트와 디버깅이 용이합니다.

단점: 컨테이너 수가 늘어나 리소스 오버헤드가 발생합니다. Pod 내 컨테이너 간 시작 순서 관리가 복잡하고, 사이드카 장애가 메인 애플리케이션에 영향을 줄 수 있습니다. 네트워크 홉이 추가되어 지연 시간이 미세하게 증가합니다.

Kubernetes 1.28부터는 네이티브 사이드카 컨테이너가 지원되어 initContainer와 결합한 사이드카의 시작/종료 순서 문제가 해결되었습니다. AWS App Mesh, Google Cloud Traffic Director 등 클라우드 벤더도 사이드카 기반 서비스 메시를 제공합니다.

💻 코드 예제

# Kubernetes Sidecar Pattern 기본 예제
apiVersion: v1
kind: Pod
metadata:
  name: web-app-with-sidecar
  labels:
    app: web-app
spec:
  containers:
    # 메인 애플리케이션 컨테이너
    - name: main-app
      image: my-web-app:v1.0
      ports:
        - containerPort: 8080
      volumeMounts:
        - name: shared-logs
          mountPath: /var/log/app
        - name: shared-data
          mountPath: /data
      resources:
        requests:
          memory: "256Mi"
          cpu: "250m"
        limits:
          memory: "512Mi"
          cpu: "500m"

    # 사이드카 컨테이너 1: 로그 수집
    - name: log-collector
      image: fluentd:v1.16
      volumeMounts:
        - name: shared-logs
          mountPath: /var/log/app
          readOnly: true
        - name: fluentd-config
          mountPath: /fluentd/etc
      env:
        - name: ELASTICSEARCH_HOST
          value: "elasticsearch.logging.svc.cluster.local"
      resources:
        requests:
          memory: "64Mi"
          cpu: "50m"
        limits:
          memory: "128Mi"
          cpu: "100m"

    # 사이드카 컨테이너 2: 프록시
    - name: envoy-proxy
      image: envoyproxy/envoy:v1.28.0
      ports:
        - containerPort: 9901  # Envoy admin
        - containerPort: 10000 # Listener
      volumeMounts:
        - name: envoy-config
          mountPath: /etc/envoy
      resources:
        requests:
          memory: "64Mi"
          cpu: "50m"
        limits:
          memory: "128Mi"
          cpu: "100m"

  # Kubernetes 1.28+ 네이티브 사이드카 (restartPolicy: Always)
  initContainers:
    - name: vault-agent
      image: hashicorp/vault:1.15
      restartPolicy: Always  # 사이드카로 동작
      volumeMounts:
        - name: shared-data
          mountPath: /data
      env:
        - name: VAULT_ADDR
          value: "https://vault.default.svc.cluster.local:8200"

  volumes:
    - name: shared-logs
      emptyDir: {}
    - name: shared-data
      emptyDir: {}
    - name: fluentd-config
      configMap:
        name: fluentd-config
    - name: envoy-config
      configMap:
        name: envoy-config

---
# Service - 메인 앱 포트만 노출
apiVersion: v1
kind: Service
metadata:
  name: web-app-service
spec:
  selector:
    app: web-app
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP
# Envoy Sidecar 설정 - Service Mesh 패턴
# envoy-config.yaml

static_resources:
  listeners:
    # 인바운드 트래픽 (외부 → 앱)
    - name: inbound_listener
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 10000
      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: inbound_http
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: local_app
                http_filters:
                  # 분산 추적 (Tracing)
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

    # 아웃바운드 트래픽 (앱 → 외부)
    - name: outbound_listener
      address:
        socket_address:
          address: 127.0.0.1
          port_value: 15001
      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: outbound_http
                route_config:
                  name: outbound_route
                  virtual_hosts:
                    - name: external_services
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: external_service
                            # 재시도 정책
                            retry_policy:
                              retry_on: "5xx,connect-failure"
                              num_retries: 3
                              per_try_timeout: 2s
                            # 타임아웃
                            timeout: 30s
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

  clusters:
    # 로컬 애플리케이션
    - name: local_app
      type: STATIC
      connect_timeout: 1s
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: local_app
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: 127.0.0.1
                      port_value: 8080
      # 헬스체크
      health_checks:
        - timeout: 1s
          interval: 10s
          healthy_threshold: 2
          unhealthy_threshold: 3
          http_health_check:
            path: /health

    # 외부 서비스 (Circuit Breaker 적용)
    - name: external_service
      type: STRICT_DNS
      connect_timeout: 5s
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: external_service
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: api.external.com
                      port_value: 443
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
      # Circuit Breaker 설정
      circuit_breakers:
        thresholds:
          - priority: DEFAULT
            max_connections: 100
            max_pending_requests: 100
            max_requests: 100
            max_retries: 3

admin:
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901
# 로깅 사이드카 패턴 - Fluentd 예제
# 메인 앱이 파일로 로그를 쓰면, 사이드카가 수집하여 중앙 저장소로 전송

---
# Fluentd ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-sidecar-config
data:
  fluent.conf: |
    # 애플리케이션 로그 수집
    
      @type tail
      path /var/log/app/*.log
      pos_file /var/log/fluentd/app.log.pos
      tag app.logs
      
        @type json
        time_key timestamp
        time_format %Y-%m-%dT%H:%M:%S.%NZ
      
    

    # 액세스 로그 수집
    
      @type tail
      path /var/log/app/access.log
      pos_file /var/log/fluentd/access.log.pos
      tag app.access
      
        @type regexp
        expression /^(?[^ ]*) - (?[^ ]*) \[(?
    

    # 메타데이터 추가
    
      @type record_transformer
      
        hostname "#{Socket.gethostname}"
        pod_name "#{ENV['POD_NAME']}"
        namespace "#{ENV['POD_NAMESPACE']}"
        container_name "#{ENV['CONTAINER_NAME']}"
      
    

    # Elasticsearch로 전송
    
      @type elasticsearch
      host "#{ENV['ELASTICSEARCH_HOST']}"
      port 9200
      index_name fluentd-${tag}-%Y.%m.%d
      
        @type file
        path /var/log/fluentd/buffer
        flush_interval 5s
        chunk_limit_size 5MB
        total_limit_size 500MB
        retry_max_interval 30s
        retry_forever true
      
    

---
# 실제 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-with-logging-sidecar
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        # 메인 애플리케이션
        - name: app
          image: my-app:v1.0
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: log-volume
              mountPath: /var/log/app
          env:
            - name: LOG_PATH
              value: /var/log/app/app.log
            - name: LOG_FORMAT
              value: json

        # Fluentd 사이드카
        - name: fluentd-sidecar
          image: fluent/fluentd-kubernetes-daemonset:v1.16-debian-elasticsearch8
          volumeMounts:
            - name: log-volume
              mountPath: /var/log/app
              readOnly: true
            - name: fluentd-config
              mountPath: /fluentd/etc
            - name: fluentd-buffer
              mountPath: /var/log/fluentd
          env:
            - name: ELASTICSEARCH_HOST
              value: elasticsearch.logging.svc.cluster.local
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          resources:
            requests:
              memory: "100Mi"
              cpu: "100m"
            limits:
              memory: "200Mi"
              cpu: "200m"

      volumes:
        - name: log-volume
          emptyDir: {}
        - name: fluentd-config
          configMap:
            name: fluentd-sidecar-config
        - name: fluentd-buffer
          emptyDir: {}

🗣️ 실무에서 이렇게 말하세요

💬 서비스 메시 도입 회의에서
"Istio를 도입하면 모든 Pod에 Envoy 사이드카가 자동 주입됩니다. 애플리케이션 코드 수정 없이 mTLS, 트래픽 제어, 분산 추적이 가능해져요. 다만 사이드카당 메모리 50~100MB 정도 오버헤드가 있으니 리소스 계획에 반영하세요."
💬 로깅 아키텍처 설계 시
"각 Pod에 Fluentd 사이드카를 붙여서 로그를 수집하는 방식과 DaemonSet으로 노드당 하나씩 배포하는 방식이 있습니다. 사이드카 방식이 격리성은 좋지만 리소스 사용량이 많아요. 로그 볼륨이 크지 않다면 DaemonSet 권장합니다."
💬 Vault 시크릿 관리 논의에서
"Vault Agent를 사이드카로 배포하면 시크릿을 자동으로 갱신해서 공유 볼륨에 써줍니다. 애플리케이션은 파일만 읽으면 되니까 Vault SDK 없이도 시크릿을 사용할 수 있어요. 시크릿 갱신 시 앱 재시작도 필요 없습니다."

⚠️ 주의사항 & 베스트 프랙티스

시작 순서 미관리

사이드카가 준비되기 전에 메인 앱이 시작하면 문제가 됩니다. Kubernetes 1.28+ 네이티브 사이드카나 initContainer 활용, 앱 레벨 재시도 로직을 구현하세요.

리소스 과할당

사이드카마다 CPU/메모리를 할당하면 전체 클러스터 리소스가 부족해집니다. 프로파일링으로 실제 사용량을 측정하고 적절한 requests/limits를 설정하세요.

사이드카 장애 전파

프록시 사이드카가 죽으면 메인 앱도 통신 불가합니다. 사이드카의 liveness/readiness probe를 설정하고, 사이드카 없이도 동작하는 fallback 모드를 고려하세요.

사이드카 패턴 베스트 프랙티스

경량 사이드카 이미지 사용, 공유 볼륨으로 효율적 통신, 사이드카 버전 일괄 관리(Helm/Kustomize), 모니터링으로 사이드카 리소스 추적.

🔗 관련 용어

📚 더 배우기