Certified Kubernetes Administrator (CKA) #11 Workloads 2: DaemonSet, StatefulSet, Job, CronJob

#10 Workloads 1에서 Deployment와 ReplicaSet으로 무상태 앱을 굴리고 rolling update와 rollback을 손에 익혔습니다. 그런데 모든 워크로드가 “어느 노드든 상관없는 똑같은 Pod 여러 개"로 끝나지는 않습니다. 노드마다 정확히 하나씩 떠야 하는 에이전트가 있고, 각 Pod가 고유한 이름과 자기만의 디스크를 가져야 하는 데이터베이스가 있으며, 한 번 돌고 끝나야 하는 배치 작업이 있고, 그 작업을 매일 새벽에 자동으로 돌려야 하는 경우가 있습니다.

이번 글은 Deployment로는 풀리지 않는 네 가지 워크로드인 DaemonSet, StatefulSet, Job, CronJob을 다룹니다. 각각이 어떤 문제를 풀려고 존재하는지, Deployment와 무엇이 다른지, 그리고 시험에서 자주 나오는 YAML과 kubectl 패턴을 정리하겠습니다.

Deployment로는 왜 부족한가 #

#10에서 본 Deployment는 무상태(stateless) 앱을 위한 워크로드입니다. 똑같은 Pod를 replicas 개수만큼 띄우고, 어느 노드에 뜨든 상관없으며, Pod끼리 구별할 이유가 없습니다. 이 모델로 풀리지 않는 네 가지 요구가 이번 글의 주제입니다.

요구풀어 주는 워크로드
모든 노드에 정확히 하나씩 떠야 한다DaemonSet
각 Pod가 고유한 이름,순서,자기 디스크를 가져야 한다StatefulSet
한 번 실행해 완료까지 책임진다Job
정해진 일정에 반복 실행한다CronJob

이 네 가지를 Deployment와 비교하면 차이가 분명해집니다.

구분DeploymentDaemonSetStatefulSetJob / CronJob
목적무상태 앱노드별 에이전트상태 보존 앱배치,일회성,스케줄 작업
Pod 개수replicas로 지정노드 수에 따라 자동replicas로 지정completions로 지정
Pod 이름랜덤 해시노드별 1개안정적 서수(0,1,2…)랜덤 해시
생성,종료 순서보장 없음보장 없음순서 보장completions까지 반복
저장소보통 공유,외부보통 호스트 경로Pod마다 전용 PVC보통 없음
restartPolicyAlwaysAlwaysAlwaysOnFailure/Never

이제 각각을 살펴보겠습니다.

DaemonSet: 노드마다 하나씩 #

DaemonSet은 클러스터의 모든(또는 일부) 노드에 Pod를 정확히 하나씩 띄우는 워크로드입니다. 노드가 새로 조인하면 DaemonSet이 그 노드에도 자동으로 Pod를 추가하고, 노드가 빠지면 그 Pod도 사라집니다. replicas 필드가 없는 이유가 여기 있습니다. 개수를 사람이 정하는 것이 아니라 노드 수가 곧 Pod 수입니다.

전형적인 쓰임은 노드 단위로 동작해야 하는 인프라 에이전트입니다.

  • 로그 수집기(fluentd, fluent-bit)
  • 노드 모니터링(node-exporter)
  • 네트워크 플러그인(CNI), kube-proxy 자체도 DaemonSet으로 뜨는 경우가 많음
  • 스토리지 데몬

기본 매니페스트 #

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: node-exporter
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      containers:
        - name: node-exporter
          image: prom/node-exporter:v1.8.0
          ports:
            - containerPort: 9100

Deployment 매니페스트에서 kind를 DaemonSet으로 바꾸고 replicas만 지운 모양과 거의 같습니다. selector와 template의 라벨이 일치해야 하는 규칙도 동일합니다.

특정 노드에만 띄우기: nodeSelector #

모든 노드가 아니라 일부 노드에만 올려야 할 때는 template의 Pod spec에 nodeSelector를 둡니다. 라벨이 일치하는 노드에만 Pod가 뜹니다.

spec:
  template:
    spec:
      nodeSelector:
        disktype: ssd

control plane 노드에도 띄우기: tolerations #

기본 설정에서 control plane 노드에는 taint가 걸려 있어 일반 Pod가 배치되지 않습니다. 로그 수집기처럼 control plane 노드에도 떠야 하는 DaemonSet이라면 그 taint를 견디는 toleration을 넣어야 합니다. taint와 toleration의 자세한 동작은 #14에서 다루겠습니다.

spec:
  template:
    spec:
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule

업데이트 전략 #

DaemonSet의 기본 업데이트 전략은 RollingUpdate입니다. template을 바꾸면 노드의 Pod를 하나씩 새 버전으로 교체하며, maxUnavailable로 동시에 내릴 수 있는 노드 수를 제어합니다.

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1

또 다른 전략은 OnDelete입니다. template을 바꿔도 자동으로 교체하지 않고, 해당 Pod를 직접 삭제해야 그 자리에 새 template으로 다시 뜹니다. 교체 시점을 사람이 제어하고 싶을 때 씁니다.

# 상태 확인 (노드 수와 desired/ready 비교)
k get daemonset -n monitoring
k rollout status ds/node-exporter -n monitoring

StatefulSet: 안정 ID와 순서 #

StatefulSet은 상태를 보존해야 하는 앱을 위한 워크로드입니다. 데이터베이스, 메시지 브로커, 분산 KV 저장소처럼 각 인스턴스가 고유한 정체성과 자기만의 데이터를 가져야 하는 경우에 씁니다. Deployment가 주지 못하는 세 가지를 보장합니다.

  1. 안정적인 네트워크 ID. Pod 이름이 이름-0, 이름-1, 이름-2처럼 서수로 고정됩니다. Pod가 죽고 다시 떠도 같은 이름과 같은 DNS 이름으로 돌아옵니다.
  2. 순서 보장. 생성은 0부터 차례로(0이 Ready여야 1을 생성), 삭제와 스케일 다운은 높은 번호부터 역순으로 진행됩니다.
  3. Pod별 전용 저장소. volumeClaimTemplates로 각 Pod마다 독립된 PVC가 만들어지고, Pod가 재생성돼도 같은 번호의 PVC에 다시 연결됩니다.

headless Service가 먼저 필요하다 #

StatefulSet의 안정적인 DNS 이름은 headless Service가 있어야 동작합니다. headless Service는 clusterIP: None으로 만든 Service로, 클러스터 IP를 하나 받는 대신 각 Pod의 DNS 레코드를 직접 노출합니다. 그 결과 각 Pod가 Pod 이름.서비스이름.네임스페이스.svc.cluster.local 형태의 고유 주소를 갖습니다. Service의 종류와 동작 전반은 #18에서 다루겠습니다.

StatefulSet + headless Service 예제 #

# headless Service: clusterIP가 None
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  clusterIP: None
  selector:
    app: nginx
  ports:
    - name: web
      port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: nginx       # 위 headless Service 이름과 일치해야 함
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - name: web
              containerPort: 80
          volumeMounts:
            - name: data
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:     # Pod마다 PVC를 자동 생성
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi

이 매니페스트가 만드는 것을 정리하겠습니다.

  • Pod는 web-0, web-1, web-2로 순서대로 생성됩니다.
  • 각 Pod에는 data-web-0, data-web-1, data-web-2라는 PVC가 자동으로 붙습니다.
  • DNS 이름은 web-0.nginx.<namespace>.svc.cluster.local처럼 안정적으로 유지됩니다.
# Pod 이름이 서수로 떴는지 확인
k get pods -l app=nginx

# 자동 생성된 PVC 확인
k get pvc

# 헤드리스 Service의 엔드포인트(각 Pod IP) 확인
k get endpoints nginx

스케일과 삭제 시 주의 #

스케일 다운하면 Pod는 높은 번호부터 사라지지만, volumeClaimTemplates로 만든 PVC는 자동으로 삭제되지 않습니다. 데이터 손실을 막기 위한 의도된 동작입니다. StatefulSet을 지워도 PVC는 남으므로, 정리가 필요하면 PVC를 직접 삭제해야 합니다.

# 스케일 다운 (web-2가 먼저 사라짐, PVC는 남음)
k scale statefulset web --replicas=2

# StatefulSet만 삭제하고 Pod는 남기기 (드물게 사용)
k delete statefulset web --cascade=orphan

업데이트 전략은 기본이 RollingUpdate이며, 높은 번호 Pod부터 역순으로 교체합니다. partition 값을 두면 그 번호 이상만 갱신하는 단계적 롤아웃(canary)도 가능합니다.

Job: 완료를 목표로 도는 작업 #

Deployment와 DaemonSet은 Pod를 계속 살려 두는 워크로드입니다. 반면 Job은 정해진 횟수만큼 성공적으로 완료되면 끝나는 워크로드입니다. 데이터 마이그레이션, 배치 계산, 백업 스크립트처럼 한 번(또는 정해진 횟수) 돌고 끝나야 하는 작업에 씁니다.

핵심 필드는 네 가지입니다.

필드의미
completions성공해야 하는 총 Pod 수. 기본 1
parallelism동시에 돌릴 수 있는 Pod 수. 기본 1
backoffLimit실패 시 재시도 한도. 초과하면 Job 실패
restartPolicyOnFailure 또는 Never만 허용(Always 불가)

Deployment와 달리 Job의 Pod에는 restartPolicy: Always를 쓸 수 없습니다. 완료를 목표로 하는 작업에 무한 재시작은 의미가 맞지 않기 때문입니다.

기본 Job 예제 #

apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  completions: 4        # 총 4번 성공해야 완료
  parallelism: 2        # 한 번에 2개씩 병렬 실행
  backoffLimit: 4       # 실패 시 최대 4번까지 재시도
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: pi
          image: perl:5.34
          command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
# Job 상태 (COMPLETIONS가 4/4가 되면 완료)
k get jobs

# Job이 만든 Pod와 로그 확인
k get pods --selector=job-name=pi
k logs job/pi

# 완료된 Job 정리
k delete job pi

activeDeadlineSeconds를 두면 그 시간을 넘긴 Job을 강제로 종료시킬 수 있고, ttlSecondsAfterFinished를 두면 완료된 Job과 그 Pod를 일정 시간 뒤 자동으로 정리합니다.

CronJob: 일정에 맞춰 Job을 찍어 낸다 #

CronJob은 cron 표현식에 따라 주기적으로 Job을 생성하는 워크로드입니다. CronJob 자체는 직접 Pod를 만들지 않고, 일정마다 Job 객체를 하나씩 만들며, 그 Job이 Pod를 만들어 작업을 수행합니다. 야간 백업, 정기 리포트, 캐시 정리 같은 반복 작업에 씁니다.

핵심 필드는 다음과 같습니다.

필드의미
schedulecron 표현식(분 시 일 월 요일)
concurrencyPolicy이전 실행이 안 끝났을 때의 정책
successfulJobsHistoryLimit보관할 성공 Job 수. 기본 3
failedJobsHistoryLimit보관할 실패 Job 수. 기본 1
startingDeadlineSeconds예정 시각을 놓쳤을 때 허용하는 지연 한도

concurrencyPolicy의 값은 세 가지입니다.

  • Allow(기본). 이전 실행이 안 끝나도 새 Job을 동시에 시작합니다.
  • Forbid. 이전 실행이 끝나지 않았으면 이번 실행을 건너뜁니다.
  • Replace. 이전 실행을 취소하고 새 Job으로 교체합니다.

CronJob 예제 #

apiVersion: batch/v1
kind: CronJob
metadata:
  name: db-backup
spec:
  schedule: "0 3 * * *"           # 매일 새벽 3시
  concurrencyPolicy: Forbid       # 이전 백업이 안 끝났으면 건너뜀
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  startingDeadlineSeconds: 120
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: bitnami/postgresql:16
              command: ["/bin/sh", "-c", "pg_dump ... > /backup/dump.sql"]

schedule분 시 일 월 요일 다섯 자리입니다. 0 3 * * *는 매일 03:00, */15 * * * *는 15분마다, 0 0 * * 0은 매주 일요일 자정을 뜻합니다.

# CronJob 목록과 마지막 실행 시각
k get cronjob

# CronJob이 찍어 낸 Job들 확인
k get jobs --watch

# 즉시 한 번 수동 실행 (테스트할 때)
k create job manual-backup --from=cronjob/db-backup

CronJob을 잠시 멈추려면 spec.suspend: true로 두면 새 Job 생성이 중단됩니다. 점검 중 백업이 도는 것을 막을 때 유용합니다.

시험 포인트 #

CKA 실기에서 이 워크로드들이 나올 때 점수를 가르는 지점을 모았습니다.

  • 명령형 생성으로 시간 절약. Job과 CronJob은 kubectl create로 골격을 만든 뒤 do(--dry-run=client -o yaml)로 매니페스트를 뽑아 편집하는 편이 빠릅니다.

    k create job test --image=busybox $do -- /bin/sh -c "echo hi" > job.yaml
    k create cronjob hello --image=busybox --schedule="*/1 * * * *" $do \
      -- /bin/sh -c "date" > cron.yaml
  • DaemonSet은 kubectl create로 못 만든다. DaemonSet 전용 생성 명령이 없으므로 Deployment 매니페스트를 do로 뽑은 뒤 kind를 DaemonSet으로 바꾸고 replicas를 지우는 패턴을 외워 두면 빠릅니다.

  • restartPolicy 함정. Job과 CronJob의 Pod template에는 OnFailure 또는 Never만 허용됩니다. 기본값(Always)을 그대로 두면 매니페스트가 거부되므로 반드시 명시합니다.

  • StatefulSet은 serviceName과 headless Service가 짝. serviceName이 가리키는 headless Service(clusterIP: None)가 없으면 안정적인 DNS가 동작하지 않습니다. 둘을 한 매니페스트로 함께 제출하는 습관이 안전합니다.

  • PVC는 남는다. StatefulSet을 스케일 다운하거나 삭제해도 volumeClaimTemplates가 만든 PVC는 자동 삭제되지 않습니다. 문제에서 정리를 요구하면 PVC를 직접 지워야 합니다.

  • selector와 template 라벨 일치. 네 워크로드 모두 #10에서 본 것처럼 selector.matchLabelstemplate.metadata.labels가 일치하지 않으면 생성이 거부됩니다.

이 워크로드들을 실제 클러스터에서 직접 만들고 부숴 보는 연습이 시험장에서의 손 빠르기를 만듭니다. 더 넓은 배경이 필요하면 쿠버네티스 중급 시리즈에서 같은 리소스들을 운영 관점으로 다시 정리해 두었습니다.

정리 #

이번 글에서 잡은 것:

  • DaemonSet. 노드마다 Pod 하나. replicas 없음. nodeSelector로 대상 노드 제한, tolerations로 control plane 노드까지 확장, RollingUpdate/OnDelete 업데이트 전략
  • StatefulSet. 안정 서수 ID,생성/삭제 순서 보장,Pod별 전용 PVC. serviceName이 가리키는 headless Service(clusterIP: None)와 volumeClaimTemplates가 핵심. PVC는 자동 삭제되지 않음
  • Job. 완료를 목표로 하는 작업. completions/parallelism/backoffLimit, restartPolicy는 OnFailure/Never만 허용
  • CronJob. 일정마다 Job을 생성. schedule(cron 다섯 자리), concurrencyPolicy(Allow/Forbid/Replace), history limit, suspend로 일시 중지
  • 시험 포인트. 명령형 생성으로 골격 확보, DaemonSet은 변환으로 작성, restartPolicy 함정, StatefulSet은 headless Service와 짝, PVC 잔존 처리

다음: ConfigMap과 Secret 깊이 #

여기까지로 워크로드의 모양을 다 잡았습니다. 그런데 Pod에 설정값과 비밀 정보를 어떻게 주입할지는 아직 다루지 않았습니다.

#12 ConfigMap과 Secret 깊이에서는 설정을 코드에서 분리하는 ConfigMap, 민감 정보를 담는 Secret의 타입과 base64 인코딩, 그리고 이 둘을 환경 변수와 볼륨으로 Pod에 주입하는 방법, 값이 바뀌었을 때의 반영 동작까지 정리하겠습니다.

X