K8s Init·Sidecar 컨테이너 패턴

멀티 컨테이너 패턴의 필요성

Kubernetes Pod는 하나 이상의 컨테이너를 포함할 수 있습니다. 단일 컨테이너로 모든 책임을 처리하면 이미지가 비대해지고, 관심사 분리가 불가능합니다. Init Container는 메인 컨테이너 시작 전 초기화 작업을, Sidecar Container는 메인 컨테이너와 함께 실행되며 보조 기능을 담당합니다. 이 두 패턴을 조합하면 마이크로서비스의 횡단 관심사를 깔끔하게 분리할 수 있습니다.

Init Container 기본 개념

Init Container는 Pod의 spec.initContainers에 정의하며, 메인 컨테이너보다 먼저 순차적으로 실행됩니다. 모든 Init Container가 성공해야 메인 컨테이너가 시작됩니다.

apiVersion: v1
kind: Pod
metadata:
  name: app-with-init
spec:
  initContainers:
    - name: wait-for-db
      image: busybox:1.36
      command:
        - sh
        - -c
        - |
          until nc -z postgres-svc 5432; do
            echo "DB 대기 중..."
            sleep 2
          done
          echo "DB 연결 확인 완료"

    - name: run-migrations
      image: myapp:latest
      command: ["npx", "typeorm", "migration:run"]
      env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: url

  containers:
    - name: app
      image: myapp:latest
      ports:
        - containerPort: 3000

이 예시에서 두 Init Container는 순서대로 실행됩니다:

  • wait-for-db: PostgreSQL이 준비될 때까지 대기
  • run-migrations: DB 마이그레이션 실행

둘 다 성공한 후에야 메인 app 컨테이너가 시작됩니다.

Init Container 실전 패턴

패턴 1: 설정 파일 다운로드

Vault, S3, Git 등에서 설정 파일을 가져와 emptyDir 볼륨에 저장합니다:

initContainers:
  - name: fetch-config
    image: vault:1.15
    command:
      - sh
      - -c
      - |
        vault kv get -format=json secret/myapp/config 
          | jq -r '.data.data' > /config/app-config.json
    env:
      - name: VAULT_ADDR
        value: "http://vault.infra:8200"
      - name: VAULT_TOKEN
        valueFrom:
          secretKeyRef:
            name: vault-token
            key: token
    volumeMounts:
      - name: config-volume
        mountPath: /config

containers:
  - name: app
    image: myapp:latest
    volumeMounts:
      - name: config-volume
        mountPath: /app/config
        readOnly: true

volumes:
  - name: config-volume
    emptyDir: {}

패턴 2: 스키마 검증

DB 스키마가 앱 버전과 호환되는지 사전 검증합니다:

initContainers:
  - name: schema-check
    image: myapp:latest
    command:
      - sh
      - -c
      - |
        PENDING=$(npx typeorm migration:show | grep -c "\[ \]")
        if [ "$PENDING" -gt 0 ]; then
          echo "미적용 마이그레이션 ${PENDING}개 발견"
          exit 1  # Pod 시작 차단
        fi
        echo "스키마 정합성 확인 완료"

패턴 3: 의존 서비스 헬스체크

Redis, Kafka 등 의존 서비스가 모두 준비될 때까지 대기합니다:

initContainers:
  - name: wait-for-dependencies
    image: busybox:1.36
    command:
      - sh
      - -c
      - |
        services="redis-svc:6379 kafka-svc:9092 elasticsearch-svc:9200"
        for svc in $services; do
          host=$(echo $svc | cut -d: -f1)
          port=$(echo $svc | cut -d: -f2)
          until nc -z $host $port; do
            echo "$host:$port 대기 중..."
            sleep 3
          done
          echo "$host:$port 준비 완료"
        done

K8s 1.28+ 네이티브 Sidecar

Kubernetes 1.28부터 네이티브 사이드카가 정식 지원됩니다. 기존에는 사이드카를 일반 컨테이너로 정의했지만, 이제 initContainersrestartPolicy: Always를 지정하면 Init 단계에서 시작되어 Pod 전체 수명 동안 실행됩니다:

apiVersion: v1
kind: Pod
metadata:
  name: app-with-native-sidecar
spec:
  initContainers:
    # 네이티브 사이드카 — Pod 수명 동안 계속 실행
    - name: log-collector
      image: fluent/fluent-bit:2.2
      restartPolicy: Always   # 핵심! 이것이 네이티브 사이드카
      volumeMounts:
        - name: log-volume
          mountPath: /var/log/app
      resources:
        requests:
          cpu: 50m
          memory: 64Mi
        limits:
          cpu: 100m
          memory: 128Mi

    # 일반 Init Container — 완료 후 종료
    - name: run-migrations
      image: myapp:latest
      command: ["npx", "typeorm", "migration:run"]

  containers:
    - name: app
      image: myapp:latest
      volumeMounts:
        - name: log-volume
          mountPath: /var/log/app

  volumes:
    - name: log-volume
      emptyDir: {}

네이티브 사이드카의 장점:

기존 방식 네이티브 사이드카 (1.28+)
메인 컨테이너와 동시 시작 Init 단계에서 먼저 시작 — 의존성 보장
Job 완료 후에도 사이드카 남아있음 메인 컨테이너 종료 시 자동 정리
종료 순서 제어 불가 메인 → 사이드카 순서로 graceful shutdown
preStop 훅 워크어라운드 필요 라이프사이클 자동 관리

Sidecar 실전 패턴

패턴 1: Envoy 프록시 사이드카

서비스 메시 없이도 Envoy를 사이드카로 배치하여 mTLS, 리트라이, 서킷 브레이커를 구현합니다:

initContainers:
  - name: envoy-proxy
    image: envoyproxy/envoy:v1.29
    restartPolicy: Always
    ports:
      - containerPort: 9901  # admin
    volumeMounts:
      - name: envoy-config
        mountPath: /etc/envoy
    resources:
      requests:
        cpu: 100m
        memory: 128Mi

containers:
  - name: app
    image: myapp:latest
    env:
      - name: HTTP_PROXY
        value: "http://localhost:10000"  # Envoy를 통해 아웃바운드

volumes:
  - name: envoy-config
    configMap:
      name: envoy-sidecar-config

패턴 2: CloudSQL Auth Proxy

GCP Cloud SQL에 접근할 때 가장 흔히 사용하는 사이드카 패턴입니다:

initContainers:
  - name: cloud-sql-proxy
    image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.8
    restartPolicy: Always
    args:
      - "--structured-logs"
      - "--port=5432"
      - "myproject:us-central1:mydb"
    securityContext:
      runAsNonRoot: true
    resources:
      requests:
        cpu: 50m
        memory: 64Mi

containers:
  - name: app
    image: myapp:latest
    env:
      - name: DATABASE_HOST
        value: "localhost"    # 사이드카가 로컬에서 프록시
      - name: DATABASE_PORT
        value: "5432"

패턴 3: 로그 수집 + 메트릭 내보내기

앱이 파일로 로그를 쓰면, Fluent Bit 사이드카가 수집하여 구조화된 JSON 로그를 Elasticsearch로 전송합니다:

initContainers:
  - name: fluent-bit
    image: fluent/fluent-bit:2.2
    restartPolicy: Always
    volumeMounts:
      - name: log-volume
        mountPath: /var/log/app
      - name: fluent-config
        mountPath: /fluent-bit/etc
    env:
      - name: ES_HOST
        value: "elasticsearch.logging:9200"

containers:
  - name: app
    image: myapp:latest
    volumeMounts:
      - name: log-volume
        mountPath: /var/log/app

volumes:
  - name: log-volume
    emptyDir: {}
  - name: fluent-config
    configMap:
      name: fluent-bit-sidecar-config

리소스 관리 전략

사이드카는 메인 컨테이너의 리소스를 잠식할 수 있으므로, ResourceQuota/LimitRange와 함께 세밀하게 관리해야 합니다:

# Pod 전체 리소스 = 메인 + 모든 사이드카 합산
# HPA는 Pod 단위로 스케일하므로 사이드카 리소스도 고려

# 리소스 최적화 예시
initContainers:
  - name: envoy-proxy
    restartPolicy: Always
    resources:
      requests:
        cpu: 50m       # 사이드카는 최소한으로
        memory: 64Mi
      limits:
        cpu: 200m      # 버스트 허용
        memory: 128Mi

containers:
  - name: app
    resources:
      requests:
        cpu: 250m
        memory: 512Mi
      limits:
        cpu: 1000m
        memory: 1Gi

주의사항:

  • HPA 계산: Pod의 CPU/메모리 사용량에 사이드카 리소스가 포함됨. targetAverageUtilization 설정 시 이를 고려
  • QoS 클래스: 모든 컨테이너(사이드카 포함)에 requests=limits를 설정해야 Guaranteed QoS 확보
  • Init Container 리소스: Init Container의 리소스는 메인 컨테이너와 별도로 계산됨. 둘 중 더 큰 값이 Pod의 effective request

보안 강화: SecurityContext

사이드카도 최소 권한 원칙을 적용해야 합니다:

initContainers:
  - name: log-collector
    restartPolicy: Always
    securityContext:
      runAsNonRoot: true
      runAsUser: 65534          # nobody
      readOnlyRootFilesystem: true
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]
    volumeMounts:
      - name: log-volume
        mountPath: /var/log/app
        readOnly: true          # 읽기 전용으로 마운트
      - name: tmp
        mountPath: /tmp         # 쓰기 필요 시 별도 emptyDir

트러블슈팅 가이드

증상 원인 해결
Pod가 Init:CrashLoopBackOff Init Container 스크립트 오류 kubectl logs pod -c init-container-name
Pod가 Init:0/2 에서 멈춤 첫 번째 Init이 완료되지 않음 의존 서비스 상태 확인, timeout 추가
Job이 완료되지 않음 기존 방식 사이드카가 종료 안 됨 네이티브 사이드카(restartPolicy: Always) 전환
사이드카 OOMKilled 메모리 limit 부족 리소스 모니터링 후 limit 조정
emptyDir 공간 부족 로그/임시파일 누적 sizeLimit 설정 및 로테이션 추가
# 디버깅 명령어
kubectl describe pod app-with-init          # Init 상태 확인
kubectl logs app-with-init -c wait-for-db   # Init Container 로그
kubectl logs app-with-init -c log-collector  # 사이드카 로그
kubectl get pod app-with-init -o jsonpath='{.status.initContainerStatuses}'

정리

Init Container와 Sidecar는 단일 책임 원칙을 Pod 레벨로 확장하는 패턴입니다. Init으로 사전 조건을 보장하고, Sidecar로 횡단 관심사를 분리하면 메인 애플리케이션은 비즈니스 로직에만 집중할 수 있습니다. K8s 1.28의 네이티브 사이드카는 Job 환경에서의 종료 문제를 해결하고, 시작 순서를 보장하여 프로덕션 안정성을 크게 높입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux