목차
8 장

StatefulSet / DaemonSet / Job / CronJob

Deployment의 stateless 가정으로는 표현되지 않는 네 갈래 워크로드를 다루는 컨트롤러들을 정리합니다. StatefulSet의 정체성과 PVC 1:1, DaemonSet의 노드 단위 한 개, Job의 종료 모델, CronJob의 cron 스케줄링과 concurrencyPolicy · startingDeadlineSeconds의 안전장치까지 한 사이클로 다룹니다.

2부 (워크로드와 운영)의 첫 챕터입니다. 4장 Deployment와 ReplicaSet의 Deployment는 stateless 워크로드 위에 서 있는 컨트롤러입니다. 같은 Pod 여러 개가 서로 같다고 가정하고, 사라져도 다시 띄우면 그만이라는 단순한 모델입니다. 그러나 정체성과 디스크가 필요한 DB, 노드마다 정확히 하나씩 떠야 하는 에이전트, 한 번 실행하고 끝나야 하는 마이그레이션, 매일 도는 백업 — 이 네 가지는 Deployment로는 표현되지 않습니다. 이번 챕터에서는 그 빈 부분을 메우는 네 컨트롤러 StatefulSet, DaemonSet, Job, CronJob을 한 자리에 정리합니다.

이번 챕터의 끝에서는 클러스터의 매니페스트 디렉터리에서 kind:가 무엇이든 그 의도를 한 줄로 읽어 낼 수 있는 결정 트리가 손에 들어옵니다. 각 컨트롤러를 “왜 Deployment로는 안 되는가"의 문제부터 시작해, 매니페스트 한 장과 운영 시 주의점까지 한 사이클로 따라갑니다.

Deployment로는 표현이 안 되는 워크로드들 #

4장에서 잡은 Deployment의 머릿속 모델을 한 줄로 줄이면 이렇습니다 — **같은 Pod 템플릿으로 N 개를 항상 유지하고, 새 버전이 오면 점진적으로 교체합니다.**이 모델이 잘 맞는 워크로드는 stateless 웹 서버, API 서버, 워커 큐 컨슈머처럼 Pod가 서로 구분되지 않아도 되는 경우입니다. web-abc123-aa11 이든 web-abc123-bb22 든 같은 코드가 돌고, 어느 Pod가 죽어도 다른 Pod가 그 역할을 메우면 끝입니다.

이 모델로는 잘 풀리지 않는 네 가지 패턴이 있습니다.

  • Pod가 서로 다르다고 가정해야 하는 워크로드 — 데이터베이스 클러스터의 primary와 replica, Kafka의 broker-0 / broker-1 / broker-2처럼 각 Pod가 자기만의 정체성과 자기만의 디스크를 가져야 하는 경우입니다. Deployment가 만드는 Pod는 이름이 임의값이고 디스크도 공유되지 않습니다.
  • 노드마다 정확히 하나씩 떠야 하는 워크로드 — 로그 수집기, 노드 모니터링 에이전트, CNI (컨테이너 네트워크 인터페이스) 에이전트입니다. “replicas 개수"가 아니라 “노드 개수에 자동으로 맞추기"가 필요한데, Deployment의 replicas 필드는 그 의도를 표현하지 못합니다.
  • 한 번 실행하고 끝나야 하는 워크로드 — DB 마이그레이션, 일회성 데이터 리포트, 클러스터 셋업 스크립트입니다. Deployment는 Pod가 종료되면 다시 띄우려고 하지만, 이런 일은 끝나는 게 정상입니다.
  • 주기적으로 실행되어야 하는 워크로드 — 매일 새벽 백업, 매시 정각의 정리 작업, 매주 리포트 생성입니다. cron 같은 스케줄링이 컨트롤러 차원에 있어야 합니다.

이 네 가지를 K8s가 각각 다른 컨트롤러로 분리해 둔 것이 **StatefulSet, DaemonSet, Job, CronJob**입니다. 하나씩 살펴봅니다.

StatefulSet — 정체성과 디스크가 필요한 워크로드 #

데이터베이스를 K8s에 띄우려고 하면 Deployment가 처음 부딪히는 벽이 명확합니다. PostgreSQL primary가 죽고 새 Pod가 떠올랐을 때, 그 새 Pod는 이전 Pod의 데이터 디렉터리를 그대로 이어받아야 합니다. 이름이 임의값으로 바뀌어도 곤란하고, 다른 replica 들이 primary를 어떻게 부를지도 안정적이어야 합니다. Deployment는 이 셋 중 어느 것도 보장하지 않습니다.

StatefulSet이 풀어 주는 것은 다음 셋입니다.

  • 안정적인 Pod 이름 — Pod가 <name>-0, <name>-1, <name>-2 식으로 인덱스가 붙은 이름을 받습니다. Pod가 재시작되어도 같은 인덱스를 유지합니다. web-0이 죽고 다시 떠도 다시 web-0입니다.
  • Pod 마다 1:1 영속 볼륨volumeClaimTemplates로 적은 PVC가 각 Pod 마다 자동으로 만들어집니다. web-0data-web-0 PVC를, web-1data-web-1 PVC를 갖고, 그 매핑이 Pod의 생애주기를 넘어 유지됩니다. PV / PVC 모델 자체는 9장 PV / PVC / StorageClass에서 깊게 다룹니다.
  • 순차적인 라이프사이클 — 기본적으로 Pod는 0번부터 순서대로 만들어지고, 종료는 역순 (N-1번부터)으로 진행됩니다. 롤링 업데이트도 같은 순서를 따릅니다. primary가 먼저 떠야 replica가 붙을 수 있는 토폴로지에 맞춘 모델입니다.

Headless Service와 짝을 이룬다 #

StatefulSet은 보통 headless Service와 짝으로 만듭니다. Pod 마다 안정적인 DNS 이름이 필요하기 때문입니다. headless Service의 개념 자체는 5장 Service의 §“Service 타입 한 표"에서 이미 한 줄로 짚어 두었습니다.

web-headless.yaml
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  clusterIP: None
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80

핵심은 clusterIP: None 한 줄입니다. 이 Service는 자기 가상 IP를 갖지 않고, 대신 Pod 마다 개별 DNS 레코드를 만들어 줍니다. 클러스터 내부에서 다음 이름으로 각 Pod를 직접 부를 수 있습니다.

StatefulSet Pod의 DNS
web-0.web.default.svc.cluster.local
web-1.web.default.svc.cluster.local
web-2.web.default.svc.cluster.local

<pod>.<headless-service>.<namespace>.svc.cluster.local 형태입니다. 일반 ClusterIP Service가 “여러 Pod 앞단의 가상 IP” 라면, headless Service는 “각 Pod의 안정적인 이름표 발급기"라고 보면 됩니다.

StatefulSet 매니페스트 #

위 headless Service와 같이 적용되는 StatefulSet 매니페스트입니다.

web-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: web
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
          volumeMounts:
            - name: data
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi

Deployment와 다른 부분이 셋 있습니다.

  • spec.serviceName: web — 위에서 만든 headless Service의 이름을 가리킵니다. StatefulSet이 Pod의 DNS 레코드를 어디에 등록할지 알려 주는 필드입니다.
  • spec.volumeClaimTemplates — Pod 마다 PVC를 자동으로 만들어 내는 템플릿입니다. 위 매니페스트는 data-web-0, data-web-1, data-web-2 세 개의 PVC를 만들고, 각 Pod의 /usr/share/nginx/html에 마운트합니다. 이 PVC가 실제로 어떤 디스크에 연결되는지는 StorageClass의 동적 프로비저닝이 결정하며, 이 모든 흐름은 9장의 본 주제입니다.
  • replicas와 Pod 이름 — Deployment와 같은 replicas: 3 이지만 만들어지는 Pod 이름은 web-0, web-1, web-2로 고정됩니다. ReplicaSet 중간 객체도 없습니다.
StatefulSet 적용 후
kubectl get pods,pvc -l app=web
출력 예시
NAME        READY   STATUS    RESTARTS   AGE
pod/web-0   1/1     Running   0          1m
pod/web-1   1/1     Running   0          50s
pod/web-2   1/1     Running   0          40s

NAME                               STATUS   VOLUME   CAPACITY   AGE
persistentvolumeclaim/data-web-0   Bound    pvc-...  1Gi        1m
persistentvolumeclaim/data-web-1   Bound    pvc-...  1Gi        50s
persistentvolumeclaim/data-web-2   Bound    pvc-...  1Gi        40s

Pod가 0, 1, 2 순서로 시간 차를 두고 떠 있고, PVC도 Pod 별로 각각 만들어진 게 보입니다.

운영 시 한 가지 주의 — 스케일다운 시 PVC는 남는다 #

StatefulSet을 replicas: 3에서 replicas: 1로 줄이면, Pod web-1, web-2는 종료되지만 PVC data-web-1, data-web-2는 그대로 남습니다. 의도한 동작입니다 — 데이터를 실수로 날리지 않게 하려는 안전장치입니다. 다시 replicas: 3으로 키우면 새로 떠오른 web-1, web-2는 그 PVC를 다시 마운트해 이전 데이터를 그대로 봅니다.

PVC까지 정리하려면 명시적으로 지워야 합니다.

PVC까지 정리
kubectl delete pvc data-web-1 data-web-2

이 안전장치 덕에 운영 사고로 StatefulSet의 replicas를 잘못 줄여도 데이터는 살아 있습니다. K8s 1.27부터는 spec.persistentVolumeClaimRetentionPolicy로 이 동작을 바꿀 수 있지만, 데이터 보존 측면에서는 기본값 그대로 두는 편이 안전합니다.

운영 환경에서 DB 같은 상태성 워크로드를 K8s 위에 직접 띄우는 패턴은 18장 CRD와 Operator 패턴에서 Operator 모델 (예: CloudNativePG, Zalando Postgres Operator)로 한 번 더 다룹니다. StatefulSet 한 장만으로는 백업 · 페일오버 · 복구까지 운영하기 어렵기 때문에, 보통 그 위에 도메인 컨트롤러를 한 층 더 얹는 모양입니다.

DaemonSet — 노드마다 정확히 하나씩 #

운영 클러스터에는 “각 노드의 상태를 그 노드 안에서 들여다봐야 하는” 워크로드가 있습니다. 노드의 컨테이너 로그를 모아서 중앙으로 보내는 Fluent Bit, 노드의 CPU · 메모리 · 디스크를 측정해 Prometheus에 노출하는 Node Exporter, Pod 사이의 네트워크를 구성해 주는 CNI 에이전트 (Calico, Cilium 등)입니다. 이런 워크로드의 공통점은 노드 개수만큼만 떠 있어야 한다는 점입니다.

Deployment의 replicas: N으로는 이 의도를 표현할 수 없습니다. 노드 수가 늘어나거나 줄어들 때마다 사람이 N을 손으로 맞춰야 하고, 한 노드에 같은 Pod 두 개가 뜨거나 어떤 노드에는 아예 안 뜨는 상황도 막을 수 없습니다.

DaemonSet이 풀어 주는 것은 단순합니다 — 클러스터의 각 노드에 자기 Pod를 정확히 하나씩 띄웁니다. 새 노드가 클러스터에 합류하면 그 노드에도 자동으로 한 개를 띄우고, 노드가 빠지면 그 노드의 Pod도 같이 사라집니다.

DaemonSet 매니페스트 #

replicas 필드가 없는 게 가장 큰 차이입니다.

node-exporter-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: node-exporter
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      hostNetwork: true
      containers:
        - name: node-exporter
          image: prom/node-exporter:v1.8.2
          args:
            - --path.rootfs=/host
          ports:
            - containerPort: 9100
              hostPort: 9100
          volumeMounts:
            - name: rootfs
              mountPath: /host
              readOnly: true
      volumes:
        - name: rootfs
          hostPath:
            path: /

Deployment와 같은 selector + template 구조이지만 replicas는 없습니다. 개수는 노드 수가 결정합니다. hostNetwork: truehostPath 볼륨은 DaemonSet 워크로드에서 자주 보이는 패턴입니다 — 노드의 네트워크 인터페이스로 직접 Pod를 노출하거나, 노드의 파일시스템을 직접 들여다봐야 하는 워크로드가 많기 때문입니다.

DaemonSet 확인
kubectl get ds -n monitoring
kubectl get pods -n monitoring -o wide
출력 예시
NAME            DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
node-exporter   3         3         3       3            3           <none>          2m

NAME                  READY   STATUS    RESTARTS   AGE   IP           NODE
node-exporter-7xk2p   1/1     Running   0          2m    10.0.0.11    node-1
node-exporter-9mn4v   1/1     Running   0          2m    10.0.0.12    node-2
node-exporter-bc8qr   1/1     Running   0          2m    10.0.0.13    node-3

DESIRED 3이 노드 수에 따라 자동으로 정해진 값이라는 점이 핵심입니다. 노드를 한 대 더 추가하면 DESIRED 4로 바뀌고 새 Pod가 그 노드에 자동으로 뜹니다.

일부 노드에만 띄우기 — nodeSelector / tolerations #

기본 DaemonSet은 모든 워커 노드에 Pod를 띄웁니다. 다만 운영에서는 일부 노드에만 띄우고 싶은 경우가 흔합니다 — GPU가 달린 노드에만 GPU 모니터를 띄우거나, 컨트롤플레인 노드에는 워크로드를 안 올리거나 하는 경우입니다.

nodeSelector로 노드 라벨에 매칭되는 노드에만 한정할 수 있습니다.

GPU 노드에만 띄우기 — 발췌
spec:
  template:
    spec:
      nodeSelector:
        hardware: gpu

반대로, taint가 붙은 노드 (예: 컨트롤플레인)에도 띄우려면 tolerations를 적습니다.

컨트롤플레인 노드에도 띄우기 — 발췌
spec:
  template:
    spec:
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule

실제로 클러스터의 kube-system 네임스페이스에 떠 있는 kube-proxy가 DaemonSet입니다. 컨트롤플레인 노드를 포함한 모든 노드에 떠야 하기 때문에 위와 같은 toleration을 들고 있습니다. kubectl get ds -n kube-system으로 한 번 확인해 보시면 좋습니다.

노드가 cordon / drain 되면 #

운영 중 노드를 점검할 때 흔히 쓰는 명령이 kubectl cordonkubectl drain입니다. cordon은 새 Pod의 스케줄링만 막고, drain은 노드 위의 Pod를 다른 노드로 옮깁니다. DaemonSet Pod는 drain의 기본 동작에서 옮겨지지 않습니다 — 노드마다 한 개씩 떠 있는 게 본분이라 다른 노드로 옮길 의미가 없기 때문입니다. drain 명령이 DaemonSet Pod 때문에 멈추면 --ignore-daemonsets 플래그를 함께 주는 게 표준 패턴입니다.

노드 점검 — DaemonSet 무시
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data

노드 업그레이드 흐름의 안전한 사용 패턴은 30장 업그레이드 전략에서 PodDisruptionBudget · terminationGracePeriodSeconds와 함께 본격적으로 다룹니다.

Job — 한 번 실행하고 끝나는 일 #

DB 스키마 마이그레이션, 일회성 데이터 정합성 검사, 새 클러스터 초기 셋업 스크립트입니다. 이런 일은 끝나면 끝입니다. 그런데 Deployment 매니페스트로 마이그레이션 컨테이너를 띄우면 어떻게 될까요. 컨테이너가 정상 종료 (exit 0) 하는 순간 Deployment는 “왜 죽었지?” 하면서 다시 띄웁니다. 마이그레이션이 무한 반복되는 사고가 됩니다.

Job은 이 시나리오를 위한 컨트롤러입니다. Pod가 성공적으로 종료되는 것을 정상으로 본다는 점에서 Deployment와 정반대 모델입니다.

Job 매니페스트 #

db-migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 4
  activeDeadlineSeconds: 600
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: migrator
          image: myapp/migrator:1.4.0
          command: ["./migrate.sh"]
          env:
            - name: DB_HOST
              value: postgres.default.svc.cluster.local

apiVersionbatch/v1 인 점이 새롭습니다. Deployment 계열은 apps/v1 이었지만 Job / CronJob은 별도 그룹입니다. 핵심 필드를 한 줄씩 짚어 둡니다.

  • completions: 1 — Pod가 성공으로 종료되어야 하는 횟수입니다. 위 예시는 1번이면 끝입니다. 큰 데이터를 N 조각으로 나눠 처리할 때는 N으로 둡니다.
  • parallelism: 1 — 동시에 떠 있는 Pod의 개수입니다. completions: 10, parallelism: 3으로 두면 10개를 처리하되 한 번에 3개씩 병렬로 돌립니다.
  • backoffLimit: 4 — Pod가 실패했을 때 재시도 횟수의 상한입니다. 기본값은 6입니다. 이 횟수를 넘기면 Job 자체가 Failed로 마감됩니다.
  • activeDeadlineSeconds: 600 — Job 전체의 시간 상한입니다. 600초 안에 끝나지 않으면 Pod를 강제 종료합니다. 무한 루프에 빠진 마이그레이션을 자르는 안전장치입니다.

restartPolicy의 제약 #

Pod의 restartPolicy는 보통 Always, OnFailure, Never 셋이 있지만, Job의 Pod 템플릿에서는 Always가 허용되지 않습니다. 매니페스트에 Always를 적으면 apiserver가 거부합니다.

이유는 단순합니다. Always는 Pod가 어떤 식으로 끝나든 (성공이든 실패든) 다시 띄우라는 뜻인데, Job은 종료를 기대하는 워크로드입니다. Always를 허용하면 성공해도 다시 띄우게 되어 Job의 의미가 사라집니다. 그래서 OnFailure (실패할 때만 재시도)나 Never (절대 재시도 안 함, 새 Pod로 다시 만듦) 둘 중 하나만 쓸 수 있습니다.

둘의 차이는 미묘합니다 — OnFailure는 같은 Pod 안에서 컨테이너만 다시 시작하고, Never는 그 Pod 자체를 실패로 마크하고 새 Pod를 다시 만듭니다. 로그를 보존해 디버깅하고 싶다면 Never가, 빠른 재시도를 원하면 OnFailure가 보통의 선택입니다.

Job 동작 확인 #

Job 만들기와 진행 확인
kubectl apply -f db-migration-job.yaml
kubectl get jobs
kubectl get pods --selector=job-name=db-migration
출력 예시 — 진행 중
NAME           COMPLETIONS   DURATION   AGE
db-migration   0/1           20s        20s

NAME                  READY   STATUS    RESTARTS   AGE
db-migration-xkz2p    1/1     Running   0          20s
출력 예시 — 완료 후
NAME           COMPLETIONS   DURATION   AGE
db-migration   1/1           45s        2m

NAME                  READY   STATUS      RESTARTS   AGE
db-migration-xkz2p    0/1     Completed   0          2m

COMPLETIONS 1/1이 찍히고 Pod가 Completed로 마감된 게 정상 종료의 모양입니다. 로그는 kubectl logs db-migration-xkz2p로 마이그레이션 출력을 그대로 받아 볼 수 있습니다. Job은 kubectl delete job db-migration으로 명시적으로 정리하지 않으면 클러스터에 남아 있습니다 — 이력으로 두고 보고 싶다면 그대로, 정리하고 싶다면 ttlSecondsAfterFinished를 추가해 자동 정리하게 할 수도 있습니다.

CronJob — 주기 실행 #

매일 새벽 3시에 DB 백업, 매시 정각에 임시 파일 정리, 매주 월요일 아침에 통계 리포트 생성입니다. 이런 패턴이 CronJob입니다. 모델은 단순합니다 — cron 표현식에 따라 정해진 시간마다 Job 객체를 만들어 냅니다. Job의 위에 cron 스케줄러 한 층을 더 얹은 모양입니다.

CronJob 매니페스트 #

db-backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: db-backup
spec:
  schedule: "0 3 * * *"
  timeZone: "Asia/Seoul"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  startingDeadlineSeconds: 300
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 1800
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: myapp/backup:2.1.0
              command: ["/usr/local/bin/backup.sh"]
              env:
                - name: S3_BUCKET
                  value: my-backups

CronJob 매니페스트의 핵심은 두 층입니다 — 바깥쪽 spec의 스케줄링 필드와, 안쪽 jobTemplate의 Job 정의입니다. 안쪽 jobTemplate은 위에서 본 Job 매니페스트의 spec과 똑같이 생겼습니다.

바깥쪽의 핵심 필드를 짚어 둡니다.

  • schedule: "0 3 * * *" — 표준 cron 표현식 5필드입니다. 순서대로 분 시 일 월 요일입니다. 이 예시는 매일 오전 3시 정각입니다. */15 * * * * (15분마다), 0 9 * * 1-5 (평일 오전 9시) 같은 일반 cron 문법을 그대로 씁니다.
  • timeZone: "Asia/Seoul" — 1.27부터 안정화된 필드입니다. 이전에는 CronJob의 시각이 컨트롤플레인 컴포넌트의 타임존을 따라가서 UTC로 해석되는 게 흔했고, “왜 새벽 3시 백업이 12시에 도냐” 같은 사고가 잦았습니다. 이 필드를 명시해 두면 그 모호함이 사라집니다.
  • concurrencyPolicy — 이전 회차의 Job이 아직 안 끝났는데 새 회차 시간이 왔을 때의 정책입니다. 기본값은 Allow입니다.
  • successfulJobsHistoryLimit / failedJobsHistoryLimit — 성공 · 실패한 Job 객체를 몇 개까지 클러스터에 남겨 둘지 정합니다. 기본은 각각 3과 1입니다. 너무 크게 두면 etcd에 Job이 누적됩니다.
  • startingDeadlineSeconds: 300 — 예정 시각 후 이 초 안에 시작되지 못하면 그 회차는 건너뜁니다. 컨트롤플레인이 잠시 멈췄다가 회복됐을 때, 밀린 회차들을 한꺼번에 다 띄우는 사고를 막는 안전장치입니다.

concurrencyPolicy 세 가지 #

기본값 Allow를 그대로 두면 운영 사고가 나기 쉽습니다. 세 옵션의 동작이 분명히 다릅니다.

정책동작
Allow (기본)이전 회차의 Job이 안 끝나도 새 회차의 Job을 추가로 만듭니다. 동시에 여러 개 떠 있을 수 있습니다
Forbid이전 회차가 안 끝나면 이번 회차는 건너뜁니다
Replace이전 회차의 Job을 죽이고 새 회차로 대체합니다

DB 백업처럼 같은 데이터에 동시에 두 개가 손대면 안 되는 워크로드는 Forbid가 정답입니다. 이전 백업이 30분이 걸리고 스케줄이 매시 정각이라면, Allow로 두면 매시 새 백업이 추가로 떠서 누적되는 사고가 납니다. “최신 회차만 살아 있으면 된다” 같은 워크로드 (예: 캐시 워밍)는 Replace가 맞습니다.

startingDeadlineSeconds가 없을 때의 위험 #

CronJob의 미묘한 함정 하나가 startingDeadlineSeconds입니다. 이 필드가 없거나 너무 크게 잡혀 있고 컨트롤플레인이 한참 멈춰 있다가 회복되면, 밀린 회차를 한꺼번에 다 띄우려는 시도가 일어날 수 있습니다. 매분 도는 CronJob이 한 시간 멈춰 있다가 깨어나면 Job 60개를 동시에 만드는 식입니다.

운영 클러스터의 CronJob에는 startingDeadlineSeconds를 합리적인 값 (예: 300초)으로 거의 항상 적어 두는 게 안전합니다. 회차가 그 안에 시작되지 못했다면 그 회차는 그냥 건너뛰는 것이, 깨어났을 때 한꺼번에 60개를 돌리는 것보다 거의 모든 경우에 낫습니다.

CronJob 동작 확인 #

CronJob과 그 밑의 Job, Pod
kubectl get cronjob,jobs,pods
출력 예시 — 한 번 돈 후
NAME                      SCHEDULE      TIMEZONE      LAST SCHEDULE   AGE
cronjob.batch/db-backup   0 3 * * *     Asia/Seoul    8h              2d

NAME                            COMPLETIONS   DURATION   AGE
job.batch/db-backup-29345400    1/1           14m        8h
job.batch/db-backup-29346840    1/1           13m        20m

NAME                                  READY   STATUS      RESTARTS   AGE
pod/db-backup-29346840-7kxqr          0/1     Completed   0          20m

세 단의 모양이 보입니다 — CronJob 한 개가 있고, 그 아래로 회차마다 Job 객체가 만들어지고, 각 Job 아래에 Pod가 한 번씩 떴다가 Completed로 마감됩니다. Job이 끝나도 successfulJobsHistoryLimit 개수만큼은 객체가 남아 있어 사후 디버깅에 쓸 수 있습니다.

언제 어느 컨트롤러를 쓰나 #

1부의 Deployment까지 더해 다섯 컨트롤러를 한 표로 정리합니다.

컨트롤러적합한 워크로드Pod 식별자종료 모델
Deploymentstateless 웹 · API 서버, 워커 컨슈머임의값 (web-abc-aa11)죽으면 다시 띄움
StatefulSetDB, 메시지 큐 브로커, 분산 캐시web-0, web-1 (고정)죽으면 같은 인덱스로 다시 띄움
DaemonSet노드 에이전트, 로그 수집기, CNI노드별 한 개죽으면 다시 띄움
JobDB 마이그레이션, 일회성 배치임의값성공으로 끝나면 끝
CronJob주기 백업, 정리, 리포트회차마다 Job매 회차가 Job의 종료 모델

머릿속 결정 트리는 단순합니다.

  • Pod 끼리 서로 같아도 되는가? — 아니면 StatefulSet이고, 맞으면 다음 질문으로 갑니다.
  • 노드마다 정확히 한 개 떠야 하는가? — 그렇다면 DaemonSet이고, 아니면 다음 질문으로 갑니다.
  • 한 번 실행하고 끝나야 하는가? — 그렇다면 주기 실행이면 CronJob, 일회성이면 Job, 아니면 Deployment입니다.

이 네 컨트롤러를 알고 나면 클러스터의 매니페스트 디렉터리에 kind:가 무엇이든 그 의도를 한 줄로 읽어 낼 수 있습니다.

연습문제 #

  1. 위 본문대로 web-headless.yaml (headless Service)과 web-statefulset.yamlreplicas: 3으로 적용한 뒤, kubectl get pods,pvc -l app=web으로 Pod와 PVC가 어떻게 짝지어지는지 확인합니다. 그 다음 kubectl delete pod web-1으로 가운데 Pod를 강제 삭제하고, 새로 떠오른 Pod의 이름과 마운트된 PVC가 동일한지 기록합니다. 4장 Deployment의 self-healing과 어떤 점이 다른지 한 단락으로 메모합니다.
  2. DB 마이그레이션 Job을 일부러 실패시켜 봅시다 — command: ["false"] 같은 식으로 exit 1을 내게 두고 backoffLimit: 2로 설정한 뒤 kubectl apply 합니다. kubectl get pods --selector=job-name=db-migration 출력을 시간 순서대로 기록해 재시도가 어떻게 일어나는지, 최종적으로 Job이 Failed로 마감되는 데 몇 번의 실패가 필요한지 §“Job 매니페스트"의 backoffLimit 설명과 맞춰 메모합니다.
  3. CronJob 매니페스트의 schedule*/1 * * * * (매분)으로, concurrencyPolicyAllow / Forbid / Replace 셋으로 차례차례 바꾸며 activeDeadlineSeconds: 90처럼 1분보다 긴 시간으로 잡고 적용해 보세요. 매 경우의 kubectl get jobs 출력에서 동시에 떠 있는 Job 개수가 어떻게 다른지 표로 정리하고, DB 백업 같은 워크로드에서 어느 정책이 안전한지를 §“concurrencyPolicy 세 가지"와 맞춰 자신의 표현으로 한 단락으로 정리합니다.

한 줄 요약: Deployment의 stateless 가정으로는 표현되지 않는 네 갈래 워크로드를 위해 K8s는 StatefulSet (정체성과 PVC 1:1), DaemonSet (노드 단위 한 개), Job (성공 종료를 정상으로 보는 모델), CronJob (Job 위 cron 스케줄러) 네 컨트롤러를 분리해 두었다. 결정 트리는 “Pod 끼리 같은가 / 노드 단위인가 / 종료를 기대하는가” 세 질문으로 갈라진다.

다음 챕터 #

이번 챕터에서 StatefulSet의 volumeClaimTemplates가 PVC를 자동으로 만든다고 한 줄로 짚고 넘어갔지만, 그 PVC가 진짜로 어떤 디스크에 어떻게 연결되는지는 다루지 않았습니다. 운영 클러스터에서는 그 한 줄 뒤에 PV (PersistentVolume), PVC (PersistentVolumeClaim), StorageClass의 삼각관계가 있습니다 — Pod의 생애주기와 디스크의 생애주기가 어떻게 분리되는가, 디스크가 동적으로 어떻게 만들어지는가, accessModes (ReadWriteOnce, ReadOnlyMany, ReadWriteMany)의 차이는 무엇인가, reclaimPolicy가 PVC가 사라졌을 때 디스크를 어떻게 처리하는가입니다.

9장 PV / PVC / StorageClass에서는 이 세 객체의 관계를 정리하고, StatefulSet의 volumeClaimTemplates가 그 위에서 진짜 무엇을 만들어 내는지를 한 사이클로 따라갑니다.

X