1 · 안정적인 이름
db-0, db-1, db-2.
재시작해도 이름이 그대로다.
Pod을 직접 만들지 않는 이유
Pod을 직접 만들면 —
컨트롤러는 **“몇 개가 어떤 모습으로 있어야 하는가”**를 선언받고, 그 상태를 계속 유지한다. 이것이 자가치유(self-healing)의 실체다.
flowchart LR
SPEC["원하는 상태<br/>replicas: 3"] --> LOOP{"컨트롤 루프"}
ACT["실제 상태<br/>Pod 2개"] --> LOOP
LOOP -->|"차이만큼 행동"| ACT2["Pod 1개 생성"]
ACT2 --> ACT
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class SPEC key
class ACT2 ok
class LOOP,ACT mute
flowchart TD
Q1{"작업이 끝나는가"}
Q1 -->|"끝난다"| Q2{"일정에 따라<br/>반복하는가"}
Q2 -->|아니오| JOB["Job"]
Q2 -->|예| CJ["CronJob"]
Q1 -->|"계속 돈다"| Q3{"노드마다<br/>하나씩 필요한가"}
Q3 -->|예| DS["DaemonSet"]
Q3 -->|아니오| Q4{"각 인스턴스가 고유한<br/>신원·저장소·순서가 필요한가"}
Q4 -->|예| STS["StatefulSet"]
Q4 -->|아니오| DEP["Deployment ★ 기본"]
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef alt fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class DEP key
class DS,STS,JOB,CJ alt
class Q1,Q2,Q3,Q4 mute
| 필요한 것 | 컨트롤러 |
|---|---|
| 상태 없는 앱을 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도 자기 것으로 센다.
flowchart LR
RS["ReplicaSet<br/>selector app=web · replicas 3"]
RS -->|"내가 만든 것"| P1["Pod A · app=web"]
RS -->|"내가 만든 것"| P2["Pod B · app=web"]
EXIST["이미 있던 Pod C<br/>app=web"] -->|"입양된다 ⚠️<br/>개수에 포함"| RS
P1 -->|"라벨을 바꾸면"| ESC["app=web-debug<br/>관리에서 벗어난다"]
ESC -.->|"부족분을 새로 만든다"| RS
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class RS key
class EXIST,ESC warn
class P1,P2 mute
# 관리에서 떼어내 디버깅하는 기법kubectl label pod web-abc-123 app=web-debug --overwrite# → ReplicaSet은 부족분을 새로 채우고, 이 Pod은 그대로 남아 조사할 수 있다flowchart LR
D["Deployment web"] --> RS1["ReplicaSet web-5d4f · v1<br/>replicas 0"]
D --> RS2["ReplicaSet web-7b9c · v2<br/>replicas 3"]
RS2 --> P1["Pod"]
RS2 --> P2["Pod"]
RS2 --> P3["Pod"]
RS1 -.->|"롤백의 재료로 남는다"| UNDO["kubectl rollout undo"]
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef old fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class D key
class RS2 ok
class RS1,UNDO old
class P1,P2,P3 mute
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.27flowchart LR
S["replicas 4<br/>maxSurge 1 · maxUnavailable 1"] --> R["살아 있는 Pod 은<br/>3 ~ 5 개 사이를 오간다"]
MS["maxSurge<br/>목표보다 더 만들 수 있는 수"] -.->|"0 이면 절대 초과하지 않는다"| R
MU["maxUnavailable<br/>없어도 되는 수"] -.->|"0 이면 용량이 절대 줄지 않는다"| R
BOTH["둘 다 0"] -->|"만들 수도 지울 수도 없다"| STUCK["롤아웃이 시작조차 안 된다 ❌"]
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class MS,MU key
class STUCK bad
class S,R,BOTH mute무중단으로 조금씩 교체한다. 기본 전략이다.
flowchart LR
A["옛 Pod 전부 삭제"] --> B["다운타임 발생 ⚠️"] --> C["새 Pod 전부 생성"]
R1["RWO 볼륨을 공유할 때"] -.-> A
R2["버전 공존이 불가능할 때"] -.-> A
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class B warn
class A,C,R1,R2 mute전부 죽이고 전부 새로 만든다. 다운타임이 있지만 RWO 볼륨을 공유하거나 버전 공존이 불가능할 때 필요하다 (13장).
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는 “돌아가는” 게 아니라 앞으로 나아간다.
flowchart LR
R1["리비전 1<br/>nginx:1.27"] --> R2["리비전 2<br/>nginx:1.28"] --> R3["리비전 3<br/>nginx:1.29"]
R3 -->|"undo --to-revision=2"| R4["리비전 4<br/>내용은 2와 같다"]
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class R4 key
class R1,R2,R3 mute
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=60sapiVersion: 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.0flowchart TB
DS["DaemonSet log-agent<br/>★ replicas 가 없다"]
DS --> N1["node01 → Pod"]
DS --> N2["node02 → Pod"]
DS --> N3["node03 → Pod"]
DS -.->|"toleration 이 없으면 안 뜬다 ❌"| CP["controlplane<br/>NoSchedule taint"]
NEW["node04 추가"] -->|"자동으로 하나 더"| DS
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class DS key
class CP bad
class NEW ok
class N1,N2,N3 mute
replicas가 없다. 개수는 노드 수가 정한다kubectl get ds -A # kube-proxy, CNI가 보인다kubectl rollout status ds/log-agentnodeSelector 나 affinity 를 쓴다tolerations가 필요하다 (7장)RollingUpdate(기본) / OnDeleteDaemonSet Pod은 기본 스케줄러가 배치하지만,
노드 리소스 부족 등의 이유로 Pending이 될 수 있다.
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 } }flowchart LR
STS["StatefulSet db"]
STS --> P0["db-0"] --> V0[("PVC data-db-0")]
STS --> P1["db-1"] --> V1[("PVC data-db-1")]
STS --> P2["db-2"] --> V2[("PVC data-db-2")]
ORD["생성 0 → 1 → 2<br/>삭제 2 → 1 → 0"] -.-> STS
DNS["db-0.db-headless.…<br/>Pod 별 안정적 DNS"] -.-> P0
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef vol fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class STS key
class V0,V1,V2 vol
class P0,P1,P2,ORD,DNS mute
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(기본) | Parallelflowchart LR
U["업데이트 시작"] --> P2["db-2 먼저"] --> P1["db-1"] --> P0["db-0 마지막"]
PART["partition: 2"] -.->|"인덱스 2 이상만 바뀐다<br/>db-1 · db-0 은 그대로"| P2
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class PART key
class U,P0,P1,P2 mute
2 → 1 → 0)partition: N 이면 N 이상 인덱스만 바뀐다 — 단계적 배포에 쓴다podManagementPolicy: Parallel 이면 순서 없이 동시에 생성/삭제 (기동이 빠르다)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는 남는다. 다시 늘리면 옛 데이터로 복귀한다.
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을 즉시 한 번 실행flowchart LR
C1["completions 없음"] --> R1["Pod 하나가 성공하면 끝"]
C2["completions 5<br/>parallelism 1"] --> R2["순차로 5번"]
C3["completions 5<br/>parallelism 5"] --> R3["5개 동시에"]
C4["parallelism 만 설정"] --> R4["워커 큐 방식<br/>하나가 성공하면 전체 완료"]
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class R1,R2,R3,R4 key
class C1,C2,C3,C4 mute
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을 찾으려면 두 단계를 내려가야 한다.
flowchart LR
CJ["CronJob nightly<br/>schedule 0 3 * * *"] -->|"일정마다 생성"| J["Job nightly-28901234"]
J -->|"생성"| P["Pod nightly-28901234-x7k2p"]
TZ["timeZone 미지정<br/>→ 기본 UTC ⚠️"] -.-> CJ
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class CJ key
class TZ warn
class J,P mute
concurrencyPolicy — 이전 Job이 아직 도는데 다음 일정이 오면?
flowchart LR
Q{"concurrencyPolicy"}
Q -->|"Allow · 기본"| A["겹쳐도 그냥 실행"]
Q -->|Forbid| B["새 Job 을 건너뛴다"]
Q -->|Replace| C["이전 것을 죽이고 새로 시작"]
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
class A mute
class B,C warn
timeZone 필드로 명시하는 게 안전하다kubectl get cronjob nightlykubectl get jobs --selector=job-name # CronJob이 만든 Job들kubectl logs job/nightly-28901234apiVersion: policy/v1kind: PodDisruptionBudgetmetadata: name: web-pdbspec: minAvailable: 2 # 또는 maxUnavailable: 1 selector: matchLabels: app: webflowchart LR
V["자발적 중단<br/>kubectl drain · 노드 업그레이드"] --> PDB{"PDB 검사<br/>minAvailable 충족?"}
PDB -->|"충족"| OK["축출 허용 ✅"]
PDB -->|"위반"| WAIT["drain 이 멈춰서 기다린다 ⏳"]
IV["비자발적 중단<br/>노드 장애 · 커널 패닉"] -.->|"막지 못한다 ❌"| GONE["그냥 죽는다"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class OK ok
class WAIT warn
class GONE bad
class V,IV,PDB mute
kubectl drain이나 노드 업그레이드 같은 “자발적 중단”에서 최소 가용 수를 지킨다drain이 멈춰서 기다린다flowchart LR
E1["컨테이너가 죽었다"] --> A1["kubelet 이 같은 노드에서 재시작 ✅"]
E2["Pod 이 삭제됐다"] --> A2["ReplicaSet 이 새 Pod 생성 ✅<br/>이름·IP 가 바뀐다"]
E3["노드가 NotReady"] --> A3["약 5분 후 축출<br/>다른 노드에 새로 만든다 ✅"]
E4["노드가 영구히 죽었다"] --> A4["위와 같다<br/>단 StatefulSet 은 자동으로 안 옮긴다 ⚠️"]
E5["앱이 살아 있는데 응답을 안 한다"] --> A5["livenessProbe 가 없으면<br/>아무 일도 안 일어난다 ❌"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
class A1,A2,A3 ok
class A4 warn
class A5 bad
마지막 줄이 중요하다. Kubernetes는 “프로세스가 살아 있는가”만 본다. “제대로 동작하는가”는 프로브를 통해 당신이 알려줘야 한다.
maxSurge / maxUnavailable 두 손잡이. 둘 다 0이면 멈춘다rollout undo는 되돌아가는 게 아니라 새 리비전을 만든다Never/OnFailure만 — 기본값 그대로 두면 거부된다concurrencyPolicy로 겹침을 제어replicas: 1 + minAvailable: 1 = drain 교착