K8s 중급 #1 StatefulSet / DaemonSet / Job / CronJob — Deployment가 아닌 다른 컨트롤러들
K8s 중급 시리즈의 첫 글입니다. 기초 시리즈에서 본 Deployment는 “같은 Pod 여러 개를 떠 있게 유지한다"는 한 가지 패턴에 충실한 컨트롤러입니다. 그러나 운영 클러스터에는 Deployment가 다루지 못하는 워크로드가 분명히 있습니다. 이번 글에서는 그 네 갈래 역할을 각각 담당하는 컨트롤러 StatefulSet, DaemonSet, Job, CronJob을 한 편에 정리하겠습니다. 각 컨트롤러를 “왜 Deployment로는 안 되는가"의 문제부터 시작해, 매니페스트 한 장과 운영 시 주의점까지 한 사이클로 따라가겠습니다.
이번 시리즈는 K8s 중급 7편입니다.
- #1 StatefulSet / DaemonSet / Job / CronJob — Deployment가 아닌 다른 컨트롤러들 ← 이번 글
- #2 PV / PVC / StorageClass — 영속 데이터 모델
- #3 Ingress와 Ingress Controller — 외부 진입점
- #4 resources.requests / limits — Pod의 자원 요청과 상한
- #5 Health check — liveness / readiness / startup probe
- #6 오토스케일링 — HPA / VPA / Cluster Autoscaler
- #7 RBAC / NetworkPolicy / ResourceQuota — 보안과 자원 정책
kubectl apply 가 의도와 다른 에러를 뱉어, 원인을 클러스터 단에서 거꾸로 짚어 가게 됩니다. 매니페스트를 적용하기 전에 utilrepo 의 YAML 검증기 에 한 번 붙여 넣으면 구문 에러를 줄,열 번호로 짚어 줍니다. utilrepo 는 브라우저에서 동작하는 가벼운 웹 유틸리티 모음으로, 비밀 정보가 외부로 나가지 않고 --- 로 묶인 다중 문서 매니페스트와 탭,스페이스 혼용 같은 자주 만나는 함정까지 함께 잡아 줍니다.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-0은data-web-0PVC를,web-1은data-web-1PVC를 갖고, 그 매핑이 Pod의 생애주기를 넘어 유지됩니다. PV / PVC 모델 자체는 #2에서 깊게 다루겠습니다. - 순차적인 라이프사이클 — 기본적으로 Pod는 0번부터 순서대로 만들어지고, 종료는 역순(N-1번부터)으로 진행됩니다. 롤링 업데이트도 같은 순서를 따릅니다. primary가 먼저 떠야 replica가 붙을 수 있는 토폴로지에 맞춘 모델입니다.
Headless Service와 짝을 이룬다 #
StatefulSet은 보통 headless Service와 짝으로 만듭니다. Pod마다 안정적인 DNS 이름이 필요하기 때문입니다.
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를 직접 부를 수 있습니다.
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 매니페스트입니다.
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: 1GiDeployment와 다른 부분이 셋 있습니다.
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의 동적 프로비저닝이 결정하며, 이 모든 흐름은 #2의 본 주제입니다.replicas와 Pod 이름 — Deployment와 같은replicas: 3이지만 만들어지는 Pod 이름은web-0,web-1,web-2로 고정됩니다. ReplicaSet 중간 객체도 없습니다.
kubectl get pods,pvc -l app=webNAME 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 40sPod가 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까지 정리하려면 명시적으로 지워야 합니다.
kubectl delete pvc data-web-1 data-web-2이 안전장치 덕에 운영 사고로 StatefulSet의 replicas를 잘못 줄여도 데이터는 살아 있습니다. K8s 1.27부터는 spec.persistentVolumeClaimRetentionPolicy로 이 동작을 바꿀 수 있지만, 데이터 보존 측면에서는 기본값 그대로 두는 편이 안전합니다.
DaemonSet — 노드마다 정확히 하나씩 #
운영 클러스터에는 “각 노드의 상태를 그 노드 안에서 들여다봐야 하는” 워크로드가 있습니다. 노드의 컨테이너 로그를 모아서 중앙으로 보내는 Fluent Bit, 노드의 CPU,메모리,디스크를 측정해 Prometheus에 노출하는 Node Exporter, Pod 사이의 네트워크를 구성해 주는 CNI 에이전트(Calico, Cilium 등). 이런 워크로드의 공통점은 노드 개수만큼만 떠 있어야 한다는 점입니다.
Deployment의 replicas: N으로는 이 의도를 표현할 수 없습니다. 노드 수가 늘어나거나 줄어들 때마다 사람이 N을 손으로 맞춰야 하고, 한 노드에 같은 Pod 두 개가 뜨거나 어떤 노드에는 아예 안 뜨는 상황도 막을 수 없습니다.
DaemonSet이 풀어 주는 것은 단순합니다 — 클러스터의 각 노드에 자기 Pod를 정확히 하나씩 띄운다. 새 노드가 클러스터에 합류하면 그 노드에도 자동으로 한 개를 띄우고, 노드가 빠지면 그 노드의 Pod도 같이 사라집니다.
DaemonSet 매니페스트 #
replicas 필드가 없는 게 가장 큰 차이입니다.
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: true와 hostPath 볼륨은 DaemonSet 워크로드에서 자주 보이는 패턴입니다 — 노드의 네트워크 인터페이스로 직접 Pod를 노출하거나, 노드의 파일시스템을 직접 들여다봐야 하는 워크로드가 많기 때문입니다.
kubectl get ds -n monitoring
kubectl get pods -n monitoring -o wideNAME 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-3DESIRED 3이 노드 수에 따라 자동으로 정해진 값이라는 점이 핵심입니다. 노드를 한 대 더 추가하면 DESIRED 4로 바뀌고 새 Pod가 그 노드에 자동으로 뜹니다.
일부 노드에만 띄우기 — nodeSelector / tolerations #
기본 DaemonSet은 모든 워커 노드에 Pod를 띄웁니다. 다만 운영에서는 일부 노드에만 띄우고 싶은 경우가 흔합니다 — GPU가 달린 노드에만 GPU 모니터를 띄우거나, 컨트롤플레인 노드에는 워크로드를 안 올리거나.
nodeSelector로 노드 라벨에 매칭되는 노드에만 한정할 수 있습니다.
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 cordon과 kubectl drain입니다. cordon은 새 Pod의 스케줄링만 막고, drain은 노드 위의 Pod를 다른 노드로 옮깁니다. DaemonSet Pod는 drain의 기본 동작에서 옮겨지지 않습니다 — 노드마다 한 개씩 떠 있는 게 본분이라 다른 노드로 옮길 의미가 없기 때문입니다. drain 명령이 DaemonSet Pod 때문에 멈추면 --ignore-daemonsets 플래그를 함께 주는 게 표준 패턴입니다.
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-dataJob — 한 번 실행하고 끝나는 일 #
DB 스키마 마이그레이션, 일회성 데이터 정합성 검사, 새 클러스터 초기 셋업 스크립트. 이런 일은 끝나면 끝입니다. 그런데 Deployment 매니페스트로 마이그레이션 컨테이너를 띄우면 어떻게 될까요. 컨테이너가 정상 종료(exit 0)하는 순간 Deployment는 “왜 죽었지?” 하면서 다시 띄웁니다. 마이그레이션이 무한 반복되는 사고가 됩니다.
Job은 이 시나리오를 위한 컨트롤러입니다. Pod가 성공적으로 종료되는 것을 정상으로 본다는 점에서 Deployment와 정반대 모델입니다.
Job 매니페스트 #
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.localapiVersion이 batch/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 동작 확인 #
kubectl apply -f db-migration-job.yaml
kubectl get jobs
kubectl get pods --selector=job-name=db-migrationNAME COMPLETIONS DURATION AGE
db-migration 0/1 20s 20s
NAME READY STATUS RESTARTS AGE
db-migration-xkz2p 1/1 Running 0 20sNAME COMPLETIONS DURATION AGE
db-migration 1/1 45s 2m
NAME READY STATUS RESTARTS AGE
db-migration-xkz2p 0/1 Completed 0 2mCOMPLETIONS 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 매니페스트 #
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-backupsCronJob 매니페스트의 핵심은 두 층입니다 — 바깥쪽 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 동작 확인 #
kubectl get cronjob,jobs,podsNAME 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 개수만큼은 객체가 남아 있어 사후 디버깅에 쓸 수 있습니다.
언제 어느 컨트롤러를 쓰나 #
기초 시리즈의 Deployment까지 더해 다섯 컨트롤러를 한 표로 정리하겠습니다.
| 컨트롤러 | 적합한 워크로드 | Pod 식별자 | 종료 모델 |
|---|---|---|---|
| Deployment | stateless 웹,API 서버, 워커 컨슈머 | 임의값 (web-abc-aa11) | 죽으면 다시 띄움 |
| StatefulSet | DB, 메시지 큐 브로커, 분산 캐시 | web-0, web-1(고정) | 죽으면 같은 인덱스로 다시 띄움 |
| DaemonSet | 노드 에이전트, 로그 수집기, CNI | 노드별 한 개 | 죽으면 다시 띄움 |
| Job | DB 마이그레이션, 일회성 배치 | 임의값 | 성공으로 끝나면 끝 |
| CronJob | 주기 백업, 정리, 리포트 | 회차마다 Job | 매 회차가 Job의 종료 모델 |
머릿속 결정 트리는 단순합니다.
- Pod 끼리 서로 같아도 되는가? — 아니면 StatefulSet이고, 맞으면 다음 질문으로 갑니다.
- 노드마다 정확히 한 개 떠야 하는가? — 그렇다면 DaemonSet이고, 아니면 다음 질문으로 갑니다.
- 한 번 실행하고 끝나야 하는가? — 그렇다면 주기 실행이면 CronJob, 일회성이면 Job, 아니면 Deployment입니다.
이 네 컨트롤러를 알고 나면 클러스터의 매니페스트 디렉터리에 kind:가 무엇이든 그 의도를 한 줄로 읽을 수 있습니다.
정리 #
이번 글에서 잡은 흐름을 정리합니다.
- Deployment는 stateless 가정 위에 서 있다 — Pod가 서로 같다고 보고, 죽으면 다시 띄우는 단순한 모델. 정체성,노드 단위,일회성,주기성 워크로드는 다른 컨트롤러가 필요.
- StatefulSet —
serviceName(headless Service)과volumeClaimTemplates가 핵심. Pod 이름이<name>-0,<name>-1로 안정적, 각 Pod가 자기 PVC를 갖는다. 스케일다운 시 PVC는 남는다. - DaemonSet —
replicas가 없습니다. 노드 수에 자동으로 맞춰 한 개씩 띄웁니다.nodeSelector/tolerations로 일부 노드에만 적용할 수 있고,kube-proxy가 대표 사례입니다. - Job —
apiVersion: batch/v1.completions,parallelism,backoffLimit,activeDeadlineSeconds가 동작을 결정.restartPolicy는OnFailure/Never만 허용. - CronJob — Job 위에 cron 스케줄러 한 층.
schedule5필드,timeZone(1.27+),concurrencyPolicy(Allow/Forbid/Replace),startingDeadlineSeconds로 밀린 회차 폭주를 막는다. - 다섯 컨트롤러의 결정 트리 — Pod의 정체성,노드 단위 여부,종료 기대 여부의 세 질문으로 갈라집니다.
다음 — PV / PVC / StorageClass #
이번 글에서 StatefulSet의 volumeClaimTemplates가 PVC를 자동으로 만든다고 한 줄로 짚고 넘어갔지만, 그 PVC가 진짜로 어떤 디스크에 어떻게 연결되는지는 다루지 않았습니다. 운영 클러스터에서는 그 한 줄 뒤에 PV(PersistentVolume), PVC(PersistentVolumeClaim), StorageClass의 삼각관계가 있습니다 — Pod의 생애주기와 디스크의 생애주기가 어떻게 분리되는가, 디스크가 동적으로 어떻게 만들어지는가, accessModes(ReadWriteOnce, ReadOnlyMany, ReadWriteMany)의 차이는 무엇인가, reclaimPolicy가 PVC가 사라졌을 때 디스크를 어떻게 처리하는가.
#2 PV / PVC / StorageClass — 영속 데이터 모델에서는 이 세 객체의 관계를 정리하고, StatefulSet의 volumeClaimTemplates가 그 위에서 진짜 무엇을 만들어 내는지를 한 사이클로 따라가겠습니다.