1 · 안정적인 이름
db-0, db-1, db-2.
재시작해도 이름이 그대로다.
Pod을 직접 만들지 않는 이유
Pod을 직접 만들면 —
컨트롤러는 “몇 개가 어떤 모습으로 있어야 하는가”를 선언받고, 그 상태를 계속 유지한다. 이것이 자가치유(self-healing)의 실체다.
| 필요한 것 | 컨트롤러 |
|---|---|
| 상태 없는 앱을 N개 | Deployment |
| 모든(또는 일부) 노드에 하나씩 | DaemonSet |
| 고유한 이름·저장소·순서가 필요한 앱 | StatefulSet |
| 한 번 실행하고 끝나는 작업 | Job |
| 일정에 따라 반복되는 작업 | CronJob |
| Pod 개수만 유지 (직접 쓸 일은 거의 없다) | ReplicaSet |
기본은 Deployment다. 나머지는 “왜 Deployment로는 안 되는가”에 답이 있을 때 쓴다.
apiVersion: apps/v1kind: ReplicaSetmetadata: name: web-rsspec: replicas: 3 selector: # 어떤 Pod을 내 것으로 볼 것인가 matchLabels: app: web template: # 부족하면 이 틀로 만든다 metadata: labels: app: web # selector와 반드시 일치해야 한다 spec: containers: - name: nginx image: nginx:1.27셀렉터에 맞기만 하면 자기가 안 만든 Pod도 자기 것으로 센다.
# 관리에서 떼어내 디버깅하는 기법kubectl label pod web-abc-123 app=web-debug --overwrite# → ReplicaSet은 부족분을 새로 채우고, 이 Pod은 그대로 남아 조사할 수 있다kubectl create deploy web --image=nginx:1.27 --replicas=3 --dry-run=client -o yaml > web.yamlapiVersion: apps/v1kind: Deploymentmetadata: name: webspec: replicas: 3 revisionHistoryLimit: 10 # 보관할 옛 ReplicaSet 개수 (기본 10) selector: matchLabels: app: web strategy: type: RollingUpdate # RollingUpdate(기본) | Recreate rollingUpdate: maxSurge: 25% # 목표보다 몇 개 더 만들 수 있나 maxUnavailable: 25% # 몇 개까지 없어도 되나 template: metadata: labels: app: web spec: containers: - name: nginx image: nginx:1.27무중단으로 조금씩 교체한다. 기본 전략이다.
전부 죽이고 전부 새로 만든다. 다운타임이 있지만 RWO(ReadWriteOnce — 한 번에 한 노드만 붙는) 볼륨을 공유하거나 버전 공존이 불가능할 때 필요하다 (Pod별 저장소와 업데이트).
kubectl set image deploy/web nginx=nginx:1.28kubectl rollout status deploy/web # 완료될 때까지 지켜본다kubectl rollout history deploy/webkubectl rollout history deploy/web --revision=2kubectl rollout undo deploy/web # 직전으로kubectl rollout undo deploy/web --to-revision=2kubectl rollout restart deploy/web # 템플릿 변경 없이 전부 재생성kubectl rollout pause deploy/webkubectl rollout resume deploy/webrollout restart 는 템플릿에 타임스탬프 애노테이션을 넣어 롤아웃을 유발한다
→ ConfigMap을 바꾼 뒤 반영하는 표준 방법이다pause 는 여러 변경을 모아서 한 번에 롤아웃할 때 쓴다kubectl rollout history deploy/web# REVISION CHANGE-CAUSE# 1 <none># 2 nginx 1.28로 업그레이드CHANGE-CAUSE는 애노테이션 kubernetes.io/change-cause 를 읽은 것이다.
kubectl annotate deploy/web kubernetes.io/change-cause="nginx 1.28로 업그레이드"undo는 “돌아가는” 게 아니라 앞으로 나아간다.
revisionHistoryLimit을 0으로 두면 롤백이 불가능해진다. 기본 10을 그대로 두자.
kubectl scale deploy web --replicas=5kubectl scale deploy web --replicas=5 --current-replicas=3 # 조건부kubectl scale --replicas=5 -f web.yamlkubectl scale statefulset db --replicas=3replicas: 0 도 유효하다 — 삭제하지 않고 멈추는 방법# 롤아웃/스케일이 끝날 때까지 기다리기 — 검산에 유용kubectl wait --for=condition=available deploy/web --timeout=60skubectl wait --for=condition=ready pod -l app=web --timeout=60sDeployment는 “몇 개”는 보장하지만 “어디에”는 스케줄러에 맡긴다. 로그 수집기·모니터링 에이전트처럼 모든 노드에 정확히 하나씩 있어야 하는 것은 개수가 replicas가 아니라 노드 수를 따라가야 한다 — 그 컨트롤러가 DaemonSet이다.
apiVersion: apps/v1kind: DaemonSetmetadata: name: log-agentspec: selector: matchLabels: app: log-agent template: metadata: labels: app: log-agent spec: tolerations: # 컨트롤 플레인에도 놓으려면 필요 - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule containers: - name: agent image: fluent-bit:3.0replicas가 없다. 개수는 노드 수가 정한다DaemonSet은 kubectl create 단축 명령이 없다 — 시험장에서는 공식
DaemonSet 페이지의
YAML 뼈대를 복사해 오거나, create deploy로 뽑은 YAML에서 kind를 바꾸고 replicas·strategy를 지운다.
kubectl get ds -A # kube-proxy, CNI가 보인다kubectl rollout status ds/log-agentnodeSelector 나 affinity 를 쓴다tolerations가 필요하다 (스케줄링)RollingUpdate(기본) / OnDeleteDaemonSet Pod은 기본 스케줄러가 배치하지만,
노드 리소스 부족 등의 이유로 Pending이 될 수 있다.
Deployment의 Pod은 서로 바꿔치기 가능한 복제본이다 — 이름은 무작위고, 죽으면 다른 이름·다른 저장소로 새로 뜬다. DB 복제처럼 “내가 몇 번 인스턴스인지”와 “내 데이터 디스크가 어느 것인지”가 중요한 앱은 이 무작위성 때문에 Deployment로는 안 된다 — 각 Pod에 고정된 신원을 주는 컨트롤러가 StatefulSet이다.
apiVersion: apps/v1kind: StatefulSetmetadata: { name: db }spec: serviceName: db-headless # 반드시 headless Service를 가리킨다 replicas: 3 selector: { matchLabels: { app: db } } template: metadata: labels: { app: db } spec: containers: - name: postgres image: postgres:17 volumeMounts: - name: data mountPath: /var/lib/postgresql/data volumeClaimTemplates: # Pod마다 PVC를 하나씩 만든다 - metadata: { name: data } spec: accessModes: ["ReadWriteOnce"] storageClassName: standard resources: { requests: { storage: 10Gi } }1 · 안정적인 이름
db-0, db-1, db-2.
재시작해도 이름이 그대로다.
2 · 안정적인 저장소
data-db-0, data-db-1 … PVC가 Pod 이름에 묶인다.
db-0이 죽었다 살아나면 같은 PVC를 다시 붙인다.
3 · 순서 보장
생성은 0 → 1 → 2, 삭제는 2 → 1 → 0.
앞 Pod이 Ready가 되어야 다음이 시작한다.
kubectl get pvc# data-db-0 Bound pvc-xxx 10Gi RWO# data-db-1 Bound pvc-yyy 10Gi RWOapiVersion: v1kind: Servicemetadata: name: db-headlessspec: clusterIP: None # ← headless selector: app: db ports: - port: 5432clusterIP: None이면 Service IP를 만들지 않고 DNS가 Pod IP들을 직접 반환한다.
db-0.db-headless.default.svc.cluster.local → 10.244.1.5db-1.db-headless.default.svc.cluster.local → 10.244.2.7그래서 “1번 레플리카에만 연결” 같은 것이 가능하다. DB 복제에서 primary/replica를 구분해 붙일 때 이 이름을 쓴다.
spec: updateStrategy: type: RollingUpdate rollingUpdate: partition: 2 # 인덱스 2 이상만 업데이트 (카나리) podManagementPolicy: OrderedReady # OrderedReady(기본) | Parallel2 → 1 → 0)partition: N 이면 N 이상 인덱스만 바뀐다 — 단계적 배포에 쓴다podManagementPolicy: Parallel 이면 순서 없이 동시에 생성/삭제 (기동이 빠르다)여기서는 “업데이트가 큰 번호부터 역순”이라는 것만 기억하면 된다.
partition·podManagementPolicy까지 깊게 팔 필요는 없다 — 필드 이름과 용도만 알아두자.
kubectl scale sts db --replicas=5 # 3 → 5: db-3, db-4가 순서대로 추가kubectl scale sts db --replicas=2 # 5 → 2: db-4, db-3, db-2가 역순으로 삭제축소해도 PVC는 남는다. 다시 늘리면 옛 데이터로 복귀한다.
Deployment는 끝나는 작업에 쓸 수 없다 — Pod 템플릿에 restartPolicy: Always만 허용되니,
작업이 성공해 exit 0으로 끝나도 계속 되살린다.
“몇 번 성공하면 완료”라는 개념을 아는 컨트롤러가 따로 필요하다 — 그게 Job이다.
apiVersion: batch/v1kind: Jobmetadata: name: importspec: completions: 5 # 총 몇 번 성공해야 하는가 parallelism: 2 # 동시에 몇 개까지 backoffLimit: 4 # 실패 재시도 횟수 (기본 6) activeDeadlineSeconds: 300 # 전체 제한 시간 — 넘으면 중단 ttlSecondsAfterFinished: 100 # 끝나고 100초 뒤 Job과 Pod을 자동 삭제 template: spec: restartPolicy: OnFailure # Never 또는 OnFailure만 가능 containers: - name: worker image: busybox:1.36 command: ["sh", "-c", "echo processing; sleep 5"]kubectl create job import --image=busybox -- echo hikubectl create job manual --from=cronjob/nightly # CronJob을 즉시 한 번 실행completionMode: Indexed 를 쓰면 각 Pod에 인덱스(0, 1, 2 …) 가 붙는다.
JOB_COMPLETION_INDEX 환경변수와 Pod 이름 접미사로 들어온다 — 데이터를 나눠 처리할 때 쓴다.
깊게 팔 필요는 없다 — 이런 모드가 있다는 것만 알아두자.
apiVersion: batch/v1kind: CronJobmetadata: name: nightlyspec: schedule: "0 3 * * *" # 분 시 일 월 요일 timeZone: "Asia/Seoul" # 없으면 컨트롤러의 시간대(보통 UTC) concurrencyPolicy: Forbid # Allow(기본) | Forbid | Replace startingDeadlineSeconds: 120 # 이 시간 안에 못 시작하면 건너뛴다 successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 1 suspend: false # true면 새 Job을 만들지 않는다 jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: backup image: busybox:1.36 command: ["sh", "-c", "echo backup"]kubectl create cronjob nightly --image=busybox --schedule="0 3 * * *" -- echo backupkubectl patch cronjob nightly -p '{"spec":{"suspend":true}}'3단 소유 사슬이다. Pod을 찾으려면 두 단계를 내려가야 한다.
concurrencyPolicy — 이전 Job이 아직 도는데 다음 일정이 오면?
timeZone 필드로 명시하는 게 안전하다kubectl get cronjob nightlykubectl get jobs --selector=job-name # CronJob이 만든 Job들kubectl logs job/nightly-28901234컨트롤러가 죽은 Pod을 다시 만들어 주긴 하지만, 새 Pod이 뜨기까지는 시간이 걸린다.
노드 업그레이드로 kubectl drain을 돌릴 때(노드 유지보수) 한 앱의 Pod들이 한꺼번에 축출되면
그 공백 동안 서비스가 순간적으로 비어 버릴 수 있다 — “동시에 죽어도 되는 수의 예산”을
선언해 두는 것이 PodDisruptionBudget(PDB)이다.
apiVersion: policy/v1kind: PodDisruptionBudgetmetadata: name: web-pdbspec: minAvailable: 2 # 또는 maxUnavailable: 1 selector: matchLabels: app: webkubectl drain이나 노드 업그레이드 같은 “자발적 중단”에서 최소 가용 수를 지킨다drain이 멈춰서 기다린다마지막 줄이 중요하다. Kubernetes는 “프로세스가 살아 있는가”만 본다. “제대로 동작하는가”는 프로브를 통해 당신이 알려줘야 한다.
maxSurge / maxUnavailable 두 손잡이. 둘 다 0은 API가 거부한다rollout undo는 되돌아가는 게 아니라 새 리비전을 만든다Never/OnFailure만 — 기본값 그대로 두면 거부된다concurrencyPolicy로 겹침을 제어replicas: 1 + minAvailable: 1 = drain 교착