본문으로 건너뛰기

CKAD 개념 노트: 멀티 컨테이너 Pod 패턴

  • kubernetes
  • ckad
  • pod
  • init-container
  • sidecar

기준: Kubernetes v1.35 (kind kindest/node:v1.35.8에서 명령어·YAML 확인)

목차

Pod 안의 container 종류

종류정의 위치언제 실행재시작probe
app containerspec.containersinit 단계가 끝난 뒤, 병렬Pod restartPolicy지원
init containerspec.initContainersapp보다 먼저, 순서대로, 끝까지실패 시 재시도 (아래 참고)불가
sidecar (native)spec.initContainers + restartPolicy: Always정의된 순서대로 시작 후 계속항상지원
ephemeral containerephemeralcontainers subresource사용자가 추가할 때안 함불가

공유하는 것

  • network: Pod IP 하나를 같이 씀. container끼리 localhost:<port>로 통신, 대신 포트가 겹치면 안 됨
  • storage: spec.volumes에 정의한 volume을 각 container가 volumeMounts로 mount. 주고받는 파일은 보통 emptyDir
  • IPC: SystemV semaphore, POSIX shared memory도 사용 가능
  • process namespace는 기본으로 분리. 필요하면 process namespace sharing을 켬

init container

Pod 시작 전에 준비 작업을 하는 container. 문서가 드는 용도는 Service가 생길 때까지 기다리기, Git repository를 volume에 clone, 설정 파일 템플릿 렌더링 같은 것들.

  • app 이미지와 별도 이미지라서 sed, dig 같은 도구를 app 이미지에 넣을 필요 없음
  • app container가 못 보는 Secret에 접근하게 할 수 있음
  • 조건이 맞을 때까지 app 시작을 막는 용도로 씀

동작 방식

  1. kubelet은 network와 storage가 준비된 뒤 init container를 spec에 적힌 순서대로 실행
  2. 각 init container는 성공(exit 0)해야 다음 것이 시작됨
  3. 전부 끝나면 app container들을 시작

실패하면:

  • Pod restartPolicy가 Never: Pod 전체가 실패 처리
  • OnFailure: 해당 init container를 재시도
  • Always: init container에는 OnFailure로 적용됨

그 밖의 규칙:

  • init이 끝나기 전 Pod phase는 Pending, condition Initialized는 false. kubectl get STATUS에는 Init:0/2 형태로 표시
  • 일반 init container는 lifecycle, livenessProbe, readinessProbe, startupProbe를 쓸 수 없음. 넣으면 API server가 Forbidden: may not be set for init containers without restartPolicy=Always로 거부
  • 다시 실행될 수 있으므로 idempotent하게 작성. emptyDir에 쓸 파일이 이미 있을 수 있음
  • 실행 중인 Pod에서 init container는 image만 바꿀 수 있고, 바꿔도 Pod가 재시작되지 않음
  • container 이름은 init·app 전체에서 유일해야 함
  • init container의 포트는 Service에 묶이지 않음

리소스 계산

  • init container들 중 가장 큰 request/limit = effective init request/limit
  • Pod의 effective request/limit = max(app·sidecar container 합, effective init 값) + pod overhead
  • 스케줄링은 이 effective 값 기준이라, init이 크면 초기화 때만 쓰는 리소스도 예약됨

sidecar

메인 앱 옆에서 로그 수집, 모니터링, 데이터 동기화 같은 보조 기능을 하는 container. 앱 코드를 고치지 않고 기능을 붙이는 게 목적입니다.

native sidecar

SidecarContainers feature gate(v1.29부터 기본 활성화)로 생긴 방식. initContainers 항목에 restartPolicy: Always를 주면 sidecar가 됩니다.

  • init container처럼 순서가 보장됨. sidecar가 started 상태가 되면 다음 init container가 시작. startupProbe가 있으면 그게 성공해야 started
  • 시작 후 Pod가 끝날 때까지 계속 실행. 종료되면 Pod restartPolicy와 상관없이 재시작
  • probe 지원. readinessProbe 결과가 Pod ready 판정에 들어감
  • Pod 종료 시: 메인 container가 완전히 끝난 뒤에야 sidecar에 TERM을 보내고, 정의된 순서의 역순으로 종료
  • Job에서 메인 container가 끝나면 sidecar가 남아 있어도 Job은 완료됨
  • image를 바꾸면 Pod가 아니라 그 container만 재시작
  • 종료 과정에서 grace period를 메인 container가 다 쓰면 sidecar는 곧바로 SIGKILL을 받을 수 있음. 이때 0이 아닌 exit code는 정상으로 봐도 됨
    • grace period: Pod 삭제 요청부터 강제 종료까지 주는 유예 시간. spec.terminationGracePeriodSeconds로 정하고 기본 30초. 이 안에 안 끝난 container는 SIGKILL로 죽음

    • container마다 따로 주는 게 아니라 Pod 전체가 타이머 하나를 나눠 씀. 메인이 SIGTERM을 받고 정리하는 동안에도 시간이 흐르니, 메인이 오래 걸릴수록 sidecar 몫이 줄어듦

    • 예: grace period 30초, 메인은 정리에 28초, 로그 수집 sidecar는 남은 로그를 보내는 데 5초가 필요한 경우

      t=0s   Pod 삭제 요청. 메인에 SIGTERM
      t=28s  메인 종료 (exit 0). 이제야 sidecar에 SIGTERM
      t=30s  기한 도달. 정리 중이던 sidecar에 SIGKILL -> exit 137 (128+9)

      메인이 30초를 다 쓰면 sidecar는 SIGTERM도 못 받고 메인과 함께 SIGKILL을 받음

    • sidecar는 메인을 돕는 container라, 메인이 정상 종료했다면 Pod 종료는 제 할 일을 한 것. 공식 문서도 이때 sidecar의 0이 아닌 exit code는 외부 도구에서 무시하라고 안내함. 모니터링이 sidecar의 137을 장애로 잡지 않게 해 둘 것

기존 방식 sidecar

containers에 app container를 하나 더 넣는 방식. container 단위 restartPolicy를 지원하지 않는 옛 버전에서도 동작하고, 지금도 유효합니다. 다만 일반 app container라서:

  • 앱보다 먼저 뜬다는 보장이 없음
  • Job에서는 문제가 됨. Pod가 Succeeded가 되려면 모든 container가 종료돼야 하는데, 계속 도는 sidecar가 있으면 끝나지 않음

시작·종료 순서가 상관없고 모든 container가 Pod 동작에 필요하다면 이 방식으로도 충분하다고 공식 문서도 설명합니다.

ambassador와 adapter

Kubernetes 블로그 2015년 글 “Patterns for Composite Containers”에서 sidecar와 함께 소개한 패턴. 현재 concepts 문서에는 따로 항목이 없고, 구현도 별도 필드 없이 container를 추가하는 방식입니다.

패턴역할예시 (블로그 기준)
sidecar메인 container 기능 확장nginx + Git 저장소를 동기화하는 container, 파일 시스템 공유
ambassador외부 연결을 대신하는 로컬 proxy앱은 localhost의 Redis에 붙고, ambassador가 읽기/쓰기를 나눠 실제 서버로 전달
adapter출력을 표준 형식으로 변환앱마다 다른 모니터링 데이터 형식을 공통 형식으로 바꿔 내보냄

ambassador는 network namespace 공유, adapter는 volume이나 localhost 공유에 기대는 구조. 셋 다 구현은 containers 또는 native sidecar로 합니다.

ephemeral container

실행 중인 Pod에 임시로 붙이는 디버깅용 container. v1.25부터 stable.

  • Pod에는 생성 후 container를 추가할 수 없지만, ephemeral container는 ephemeralcontainers subresource로 추가. 그래서 kubectl edit로는 못 넣음
  • 추가한 뒤에는 수정·삭제 불가
  • 자동 재시작 없음, 리소스 보장 없음
  • ports, livenessProbe, readinessProbe, resources 사용 불가
  • static Pod에는 사용 불가
  • 쓰는 상황: container가 crash했거나, distroless처럼 shell이 없어 kubectl exec가 안 될 때
kubectl debug -it <pod> --image=busybox:1.28 --target=<container>

--target은 지정한 container의 process namespace에 붙어서 그 프로세스를 볼 수 있게 함. container runtime이 지원해야 동작하고, 지원하지 않으면 process namespace가 분리된 채로 뜰 수 있음.

예시

init container로 설정 파일을 만들고, native sidecar가 앱 로그를 읽는 Pod.

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  initContainers:
    - name: init-config
      image: busybox:1.36
      command: ["sh", "-c", 'echo "server_name=web" > /config/app.conf']
      volumeMounts:
        - name: config
          mountPath: /config
    - name: log-shipper
      image: busybox:1.36
      restartPolicy: Always
      command: ["sh", "-c", "touch /logs/app.log; tail -F /logs/app.log"]
      volumeMounts:
        - name: logs
          mountPath: /logs
  containers:
    - name: app
      image: busybox:1.36
      command:
        [
          "sh",
          "-c",
          "while true; do cat /config/app.conf >> /logs/app.log; sleep 5; done",
        ]
      volumeMounts:
        - name: config
          mountPath: /config
        - name: logs
          mountPath: /logs
  volumes:
    - name: config
      emptyDir: {}
    - name: logs
      emptyDir: {}

실행 순서: init-config 완료 → log-shipper 시작(started) → app 시작. log-shipper는 app과 함께 계속 실행.

같은 sidecar를 기존 방식으로 쓰면 containers 아래로 옮기고 restartPolicy를 빼면 됩니다. 이 경우 log-shipper와 app은 순서 없이 함께 시작.

kubectl apply -f web.yaml
kubectl get pod web                      # STATUS: Init:0/2 → Running
kubectl logs web -c init-config          # init container 로그
kubectl logs web -c log-shipper -f       # sidecar 로그
kubectl logs web --all-containers
kubectl exec -it web -c app -- sh
kubectl describe pod web                 # Init Containers 섹션의 State/Reason

헷갈리는 것 비교

init container, native sidecar, 기존 sidecar

init containernative sidecar기존 sidecar
위치initContainersinitContainerscontainers
container restartPolicy없음Always없음
실행 기간끝날 때까지만Pod 수명 전체Pod 수명 전체
app보다 먼저 시작예 (완료까지)예 (started까지)보장 없음
probe불가지원지원
app과 데이터 교환한 방향 (volume에 써 두기)양방향양방향
Job 완료 막음아니오아니오예
종료 순서해당 없음메인 뒤, 역순순서 없음

kubectl exec vs kubectl debug

kubectl execkubectl debug (ephemeral)
사용하는 이미지기존 container 이미지새로 지정한 이미지
shell 없는 이미지불가가능
crash한 container불가가능
Pod spec 변화없음ephemeral container가 남음

시험 포인트

명령어

멀티 container Pod를 한 번에 만드는 imperative 명령어는 없음. 뼈대를 만든 뒤 YAML을 고치는 흐름.

kubectl run web --image=busybox:1.36 --dry-run=client -o yaml \
  --command -- sh -c 'sleep 3600' > web.yaml
# initContainers, 추가 container, volumes를 직접 작성

kubectl logs <pod> -c <container>
kubectl exec -it <pod> -c <container> -- sh
kubectl debug -it <pod> --image=busybox:1.28 --target=<container>

kubectl explain

kubectl explain pod.spec.initContainers
kubectl explain pod.spec.initContainers.restartPolicy  # Always = native sidecar
kubectl explain pod.spec.shareProcessNamespace         # container 간 프로세스 공유
kubectl explain pod.spec.volumes.emptyDir              # container 간 파일 공유
kubectl explain pod.spec.containers.volumeMounts

참고 문서