Certified Kubernetes Application Developer (CKAD) #3 Multi-container 패턴: Init container, sidecar, ambassador, adapter

#2 Pod와 컨테이너 라이프사이클에서 단일 컨테이너 Pod의 생애 주기와 재시작 동작을 익혔습니다. 그러나 실무와 시험에서는 하나의 Pod 안에 컨테이너가 여럿 들어가는 경우가 자주 나옵니다. 이번 글에서는 이렇게 여러 컨테이너가 한 Pod에 모일 때의 대표적인 협업 패턴을 정리하겠습니다.

쿠버네티스의 Pod 모델에서 Pod 안의 컨테이너들은 같은 네트워크 네임스페이스와 같은 스토리지 볼륨을 공유합니다. 즉 컨테이너끼리 localhost로 통신하고, 같은 디렉터리에 파일을 주고받을 수 있습니다. 이 공유 성질 덕분에 init container, sidecar, ambassador, adapter라는 네 가지 패턴이 성립합니다.

Init container: 메인 컨테이너 전에 끝나야 하는 사전 작업 #

init container는 메인 컨테이너가 시작되기 전에 실행되고, 끝까지 완료되어야 하는 컨테이너입니다. 여러 개를 두면 정의된 순서대로 하나씩 실행되며, 앞 컨테이너가 성공으로 끝나야 다음 컨테이너가 시작됩니다. 모든 init container가 완료된 뒤에야 일반 컨테이너가 기동합니다.

전형적인 쓰임은 다음과 같습니다.

  • 메인 앱이 의존하는 서비스(데이터베이스, 백엔드 Service)가 준비될 때까지 대기
  • 앱 기동 전 데이터베이스 스키마 마이그레이션 실행
  • 설정 파일이나 정적 자산을 공유 볼륨에 미리 내려받기
  • 권한이 필요한 초기화를 메인 컨테이너와 분리해서 수행

init container가 실패하면 kubelet이 restartPolicy에 따라 재시도합니다. restartPolicyNever가 아니라면 성공할 때까지 반복 재시작하므로, Pod는 init 단계에서 멈춘 채 Init:Error 또는 Init:CrashLoopBackOff 상태로 남습니다. init container의 로그를 따로 확인하려면 k logs myapp -c wait-for-db처럼 컨테이너 이름을 지정하고, 진행 단계는 k describe pod myapp으로 확인합니다.

다음은 메인 컨테이너 전에 백엔드 Service가 뜰 때까지 대기하는 init container 예제입니다.

apiVersion: v1
kind: Pod
metadata:
  name: myapp
spec:
  initContainers:
    - name: wait-for-db
      image: busybox:1.36
      command:
        - sh
        - -c
        - "until nslookup db-service; do echo waiting for db; sleep 2; done"
  containers:
    - name: app
      image: nginx:1.27
      ports:
        - containerPort: 80

initContainerscontainers와 같은 spec 레벨에 두는 별도 필드입니다. 시험에서는 k run myapp --image=nginx:1.27 $do > myapp.yaml로 Pod 뼈대를 만든 뒤 이 필드를 직접 추가하는 흐름이 빠릅니다.

Sidecar: 메인과 함께 도는 보조 컨테이너 #

sidecar는 메인 컨테이너와 같은 Pod 안에서 나란히 계속 실행되는 보조 컨테이너입니다. init container가 메인 전에 끝나고 사라지는 것과 달리, sidecar는 메인이 도는 내내 함께 동작합니다. 대표적인 쓰임은 로그 수집, 메트릭 노출, 프록시, 설정 동기화입니다.

가장 흔한 형태는 로그 수집 sidecar입니다. 메인 컨테이너가 공유 볼륨에 로그 파일을 쓰면, sidecar가 같은 볼륨을 읽어 표준 출력으로 흘려보내거나 외부 수집기로 보냅니다. 두 컨테이너가 같은 파일을 공유하는 핵심은 뒤에서 다룰 emptyDir volume입니다.

1.28+ 네이티브 sidecar #

쿠버네티스 1.28부터 sidecar를 restartPolicyAlways인 init container로 선언하는 네이티브 방식이 도입되었습니다(1.29에서 기본 활성화). 이렇게 정의된 init container는 일반 init container처럼 메인 컨테이너보다 먼저 시작되지만, 종료되지 않고 메인과 함께 계속 실행되며 Pod 종료 시 메인 다음에 정리됩니다. 즉 sidecar가 메인보다 먼저 준비되어야 하는 경우에 적합합니다. 시험 버전이 1.28 이상이라면 이 형태도 알아 두는 편이 안전합니다.

apiVersion: v1
kind: Pod
metadata:
  name: native-sidecar
spec:
  initContainers:
    - name: log-agent
      image: busybox:1.36
      restartPolicy: Always   # 이 줄이 init container를 sidecar로 만든다
      command: ["sh", "-c", "tail -F /var/log/app/access.log"]
      volumeMounts:
        - name: logs
          mountPath: /var/log/app
  containers:
    - name: app
      image: nginx:1.27
      volumeMounts:
        - name: logs
          mountPath: /var/log/app
  volumes:
    - name: logs
      emptyDir: {}

Ambassador: 외부 연결을 로컬 프록시로 추상화 #

ambassador 패턴은 메인 컨테이너가 외부 서비스와 직접 연결하지 않고, 같은 Pod 안의 프록시 컨테이너를 거쳐 localhost로만 통신하게 만드는 구조입니다. 메인 앱은 항상 localhost:포트만 바라보고, 실제 라우팅과 재시도, 샤딩, 인증 같은 복잡한 연결 로직은 ambassador 컨테이너가 담당합니다.

쓰임은 메인 앱의 연결 코드를 단순하게 유지하는 것입니다. 예를 들어 데이터베이스 읽기/쓰기 분리나 환경별 엔드포인트 전환을 앱 코드 변경 없이 ambassador 설정만으로 처리할 수 있습니다. 앱 입장에서는 외부가 늘 한곳에 있는 것처럼 보입니다.

Adapter: 출력 포맷을 표준화 #

adapter 패턴은 ambassador와 방향이 반대입니다. ambassador가 나가는 연결을 추상화한다면, adapter는 메인 컨테이너의 출력을 외부 시스템이 기대하는 표준 포맷으로 변환합니다. 메인 앱은 자기 방식대로 로그나 메트릭을 내보내고, adapter 컨테이너가 그것을 모니터링 시스템이 읽을 수 있는 형태로 가공합니다.

대표적인 쓰임은 메트릭 노출입니다. 앱이 자체 형식으로 상태를 기록하면, adapter가 그것을 Prometheus가 수집할 수 있는 형식으로 변환해 노출합니다. 앱마다 제각각인 출력을 클러스터 표준 하나로 맞추는 것이 adapter의 역할입니다.

컨테이너 간 공유 #

이 네 가지 패턴이 동작하는 바탕은 Pod 안 컨테이너들이 자원을 공유한다는 점입니다. 실기에서 직접 쓰는 공유 수단은 두 가지입니다.

emptyDir volume 공유 #

emptyDir는 Pod가 노드에 스케줄될 때 생성되어 Pod가 사라질 때까지 존재하는 임시 볼륨입니다. 같은 Pod의 여러 컨테이너가 이 볼륨을 각자 volumeMounts로 마운트하면 같은 디렉터리를 공유합니다. 한 컨테이너가 쓴 파일을 다른 컨테이너가 즉시 읽을 수 있어, 로그 수집 sidecar와 init container의 파일 전달이 모두 여기에 기댑니다.

shared process namespace #

spec.shareProcessNamespacetrue로 두면 Pod 안 컨테이너들이 프로세스 네임스페이스까지 공유해, 한 컨테이너에서 다른 컨테이너의 프로세스를 보거나 신호를 보낼 수 있습니다. 디버깅이나 프로세스 관리용 sidecar에서 쓰입니다.

YAML 예제 #

init container와 메인 컨테이너 #

다음은 init container가 공유 볼륨에 정적 페이지를 내려놓고, 메인 nginx가 그 볼륨을 서빙하는 예제입니다.

apiVersion: v1
kind: Pod
metadata:
  name: web-with-init
spec:
  initContainers:
    - name: fetch-content
      image: busybox:1.36
      command:
        - sh
        - -c
        - "echo '<h1>ready</h1>' > /usr/share/nginx/html/index.html"
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
  containers:
    - name: web
      image: nginx:1.27
      ports:
        - containerPort: 80
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
  volumes:
    - name: html
      emptyDir: {}

sidecar로 로그 공유 #

다음은 메인 컨테이너가 emptyDir에 로그를 쓰고, sidecar가 같은 볼륨을 읽어 표준 출력으로 흘려보내는 예제입니다.

apiVersion: v1
kind: Pod
metadata:
  name: app-with-logging
spec:
  containers:
    - name: app
      image: busybox:1.36
      command:
        - sh
        - -c
        - "while true; do echo \"$(date) request\" >> /var/log/app/access.log; sleep 3; done"
      volumeMounts:
        - name: logs
          mountPath: /var/log/app
    - name: log-shipper
      image: busybox:1.36
      command:
        - sh
        - -c
        - "tail -F /var/log/app/access.log"
      volumeMounts:
        - name: logs
          mountPath: /var/log/app
  volumes:
    - name: logs
      emptyDir: {}

두 컨테이너가 같은 logs 볼륨을 마운트한 덕분에, app이 남긴 로그를 log-shipper가 그대로 읽어 출력합니다. sidecar 로그는 k logs app-with-logging -c log-shipper처럼 컨테이너 이름으로 확인합니다.

시험 포인트 #

  • init container는 containers가 아니라 initContainers 에 둡니다. 같은 spec 레벨의 별도 필드인 점을 헷갈리지 않습니다.
  • init container는 순서대로, 성공할 때까지 실행됩니다. Pod가 Init:0/2 같은 상태에 멈춰 있으면 어느 init container에서 막혔는지 k describe pod로 확인합니다.
  • 멀티 컨테이너 Pod의 로그는 반드시 -c 컨테이너이름을 붙여야 원하는 컨테이너를 봅니다. 이름을 빼면 어떤 컨테이너 로그인지 모호합니다.
  • sidecar와 init container의 파일 공유는 emptyDir, 통신 공유는 localhost 라는 두 축을 기억합니다.
  • 1.28 이상에서는 sidecar를 restartPolicy: Always인 init container로 선언하는 네이티브 방식이 있습니다. 문제 지문의 버전을 확인합니다.
  • 컨테이너 안에서 명령을 실행해 동작을 확인할 때도 컨테이너를 지정합니다.
# 특정 컨테이너 안에서 명령 실행
k exec app-with-logging -c app -- cat /var/log/app/access.log

정리 #

이번 글에서 잡은 것:

  • init container. 메인 전에 순서대로 실행되고 성공해야 다음으로 넘어가는 사전 작업용 컨테이너. 대기, 마이그레이션, 자산 내려받기에 사용
  • sidecar. 메인과 함께 계속 도는 보조 컨테이너. 로그 수집과 프록시가 대표 쓰임. 1.28+ 네이티브 sidecar는 restartPolicy: Always인 init container
  • ambassador. 나가는 외부 연결을 로컬 프록시로 추상화. 앱은 localhost만 바라봄
  • adapter. 메인의 출력을 외부 시스템 표준 포맷으로 변환
  • 공유 수단. emptyDir volume으로 파일 공유, shareProcessNamespace로 프로세스 네임스페이스 공유
  • 로그 확인. 멀티 컨테이너 Pod는 k logs pod -c 컨테이너이름

다음: 컨테이너 이미지 #

Pod 안에 컨테이너를 어떻게 배치하는지 익혔습니다. 그러면 그 컨테이너가 담고 있는 이미지는 어떻게 만드는지가 다음 질문입니다.

#4 컨테이너 이미지: Dockerfile, 멀티스테이지, 시험에서 직접 빌드에서는 Dockerfile의 핵심 명령, 이미지 크기를 줄이는 멀티스테이지 빌드, 그리고 CKAD에서 직접 이미지를 빌드하고 태그를 붙여 레지스트리에 올리는 작업까지 손으로 따라 하며 정리하겠습니다.

X