앞 글에서 Pod → ReplicaSet → Deployment로 이어지는 워크로드 기초를 다뤘습니다.
Deployment가 실무 표준이라는 건 알겠는데, 문제는 모든 앱이 Deployment에 딱 맞는 건 아니라는 점입니다. 데이터베이스처럼 상태를 가진 앱은? 모든 노드에 하나씩 띄워야 하는 에이전트는? 한 번 돌고 끝나는 배치 작업은? 이런 경우에는 어떻게 해야할까요?
이번 글에서는 용도별로 나뉘는 워크로드 리소스들과, 컨테이너의 상태를 어떻게 판단하는지(Probe), 그리고 한 Pod 안에 여러 컨테이너를 두는 패턴(Init/Sidecar)까지 정리합니다.
StatefulSet — 상태가 필요한 앱을 위한 리소스
Deployment로 데이터베이스를 띄우면 어떤 문제가 생길까요? Pod가 재시작되면 이름과 IP가 바뀌고, 이전에 쓰던 디스크와의 연결도 끊어집니다. 분산 시스템에서 "redis-0이 마스터고 redis-1, redis-2가 슬레이브"라는 구조가 필요한데, Pod 이름이 매번 바뀌면 이걸 유지할 수 없죠.
StatefulSet은 이 문제를 세 가지로 해결합니다.
- 안정적인 네트워크 ID
Pod 이름이 redis-0, redis-1, redis-2처럼 고정됩니다. 재시작돼도 같은 이름을 받습니다. Headless Service와 함께 쓰면 redis-0.redis-headless.default.svc.cluster.local 같은 DNS로 특정 Pod에 직접 접근할 수 있습니다. - 안정적인 스토리지
volumeClaimTemplates로 Pod마다 별도의 PVC가 자동 생성됩니다. Pod가 죽었다 다시 뜨면 같은 PVC에 다시 연결되므로 데이터가 보존됩니다. - 순서 보장
기본적으로 0번부터 순서대로 생성하고, 역순으로 삭제합니다. 이전 Pod가 Ready 상태가 되어야 다음 Pod가 만들어집니다.
# Headless Service (ClusterIP: None)
apiVersion: v1
kind: Service
metadata:
name: redis-headless
spec:
clusterIP: None
selector:
app: redis
ports:
- port: 6379
---
# StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: redis-headless # Headless Service와 연결
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7.4
ports:
- containerPort: 6379
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources:
requests:
storage: 10Gi
# Pod가 순서대로 생성되는 걸 확인
kubectl get pods -l app=redis -w
# redis-0 Running
# redis-1 Running
# redis-2 Running
# Pod마다 별도 PVC가 생성됨
kubectl get pvc
# data-redis-0, data-redis-1, data-redis-2
⚠️ 주의: StatefulSet을 삭제해도 PVC는 자동으로 삭제되지 않습니다. 데이터 보호를 위한 설계인데, 완전히 정리하려면 PVC를 별도로 삭제해야 합니다. 삭제를 깜빡하면 스토리지 비용이 계속 나가니 주의가 필요합니다.
Deployment vs StatefulSet 선택 기준
간단하게 정리하면 이렇습니다. 앱이 상태를 내부에 저장하지 않으면(웹 서버, API 서버) Deployment, 상태를 저장하고 Pod 간 구분이 필요하면(DB, 메시지 큐, 분산 캐시) StatefulSet입니다.
DaemonSet — 모든 노드에 하나씩
DaemonSet은 클러스터의 모든 노드(또는 선택한 노드)에 Pod를 하나씩 배포합니다. 새 노드가 추가되면 자동으로 Pod가 생기고, 노드가 제거되면 함께 사라집니다.
노드마다 반드시 하나씩 돌아야 하는 인프라 에이전트같은 앱을 실행할 때 사용할 수 있습니다. 재밌는 건 kube-proxy 자체도 DaemonSet으로 배포된다는 점입니다.
대표적인 사용 사례는 이렇습니다.
- 로그 수집 에이전트 (Fluent Bit, Filebeat)
- 모니터링 에이전트 (Prometheus Node Exporter, Datadog Agent)
- 네트워크 플러그인 (Calico, Cilium)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: logging
spec:
selector:
matchLabels:
app: fluent-bit
template:
metadata:
labels:
app: fluent-bit
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule # 컨트롤 플레인 노드에도 배포
containers:
- name: fluent-bit
image: fluent/fluent-bit:3.2
resources:
requests:
cpu: "50m"
memory: "50Mi"
limits:
cpu: "200m"
memory: "200Mi"
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
컨트롤 플레인 노드에는 기본적으로 Taint가 걸려 있어서 일반 Pod가 배치되지 않습니다.
로그 수집처럼 모든 노드에서 돌아야 하는 DaemonSet은 Toleration을 추가해야 합니다.
# 모든 노드에 하나씩 배포됐는지 확인
kubectl get daemonset -n logging
# DESIRED == CURRENT == READY == 노드 수
Job — 한 번 돌고 끝나는 작업
Deployment는 Pod를 계속 Running 상태로 유지하려고 합니다.
그런데 DB 마이그레이션이나 배치 데이터 처리는 한 번 실행하고 끝나야 하는 작업이죠.
Job은 Pod가 성공적으로 종료(exit 0)되면 완료로 간주합니다.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
backoffLimit: 3 # 실패 시 최대 재시도 횟수
activeDeadlineSeconds: 300 # 전체 작업 타임아웃
template:
spec:
restartPolicy: OnFailure # Job에서는 Always 사용 불가
containers:
- name: migration
image: myapp:v2
command: ["python", "manage.py", "migrate"]
kubectl apply -f job.yaml
kubectl get job db-migration # COMPLETIONS: 0/1 → 1/1
kubectl logs -l job-name=db-migration
여러 데이터를 동시에 처리하고 싶으면 parallelism과 completions를 조합합니다.
completions: 10, parallelism: 3이면 최대 3개의 Pod가 동시에 돌면서 총 10번 완료할 때까지 실행됩니다.
⚠️ 주의: activeDeadlineSeconds를 설정하지 않으면 실패한 Pod가 무한 재시도에 빠질 수 있습니다. 배치 작업에는 반드시 타임아웃을 걸어둡니다.
완료된 Job과 Pod는 자동 삭제되지 않아 로그를 확인할 수 있지만, 쌓이면 관리가 번거롭습니다. ttlSecondsAfterFinished: 600을 설정하면 완료 10분 후 자동으로 정리됩니다.
CronJob — 스케줄 기반 반복 Job
CronJob은 유닉스 cron 표현식으로 Job을 반복 생성합니다. 계층은 CronJob → Job → Pod 순서입니다.
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
spec:
schedule: "0 2 * * *" # 매일 새벽 2시
timeZone: "Asia/Seoul" # v1.27 GA. 이전엔 UTC 기준으로 계산해야 했음
concurrencyPolicy: Forbid # 이전 Job이 아직 돌고 있으면 새 Job 생성 안 함
startingDeadlineSeconds: 300 # 5분 안에 시작 못하면 스킵
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 1800
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: backup-tool:v1
command: ["/bin/sh", "-c", "backup.sh"]
concurrencyPolicy는 중요한 설정입니다. 기본값인 Allow는 이전 Job이 안 끝나도 새 Job을 만듭니다.
백업 스크립트가 30분 넘게 걸리는데 1시간마다 돌린다면 Job이 겹칠 수 있기 때문에, 이런 경우 Forbid로 설정하면 이전 것이 끝날 때까지 새 것은 스킵됩니다.
# 테스트용 수동 실행
kubectl create job --from=cronjob/db-backup manual-backup
# CronJob 일시 중지 (배포 등 특정 기간)
kubectl patch cronjob db-backup -p '{"spec":{"suspend":true}}'
⚠️ 주의: CronJob은 지정 시각에 정확히 실행되지 않을 수 있습니다. 컨트롤러 재시작이나 네트워크 지연으로 수 초~수 분 늦어질 수 있어서, 작업은 멱등성(idempotent)이 있게 설계해야 합니다.
Probe — 컨테이너 헬스 체크
지금까지 워크로드 리소스를 다뤘는데, 이 리소스들이 Pod를 관리할 때 "이 컨테이너가 정상인지"를 어떻게 판단할까요? 컨테이너가 Running이라고 해서 반드시 정상인 건 아닙니다. 앱이 데드락에 빠졌거나, DB 연결이 끊어졌거나, 아직 시작이 안 끝났을 수 있습니다.
쿠버네티스는 세 가지 Probe를 제공합니다.
- livenessProbe — "살아 있는가?" 실패하면 컨테이너를 재시작합니다.
- readinessProbe — "트래픽을 받을 준비가 됐는가?" 실패하면 Service 엔드포인트에서 빼서 트래픽을 차단합니다. 컨테이너를 재시작하진 않습니다.
- startupProbe — "시작이 끝났는가?" 이게 성공할 때까지 liveness와 readiness를 비활성화합니다.
containers:
- name: app
image: myapp:v1
ports:
- containerPort: 8080
startupProbe: # 시작 완료 대기 (최대 300초)
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10
livenessProbe: # 살아 있는지 감시
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
failureThreshold: 3
readinessProbe: # 트래픽을 받을 준비가 됐는지
httpGet:
path: /ready
port: 8080
periodSeconds: 5
failureThreshold: 3
처음에 /healthz와 /ready를 왜 분리하는지 헷갈렸습니다. 생각해보면 DB 연결이 끊어졌을 때, 앱 자체는 살아 있으니 재시작할 필요가 없고(livenessProbe 성공), 대신 요청을 처리할 수 없으니 트래픽만 차단하면 됩니다(readinessProbe 실패).
⚠️ 주의: livenessProbe에서 외부 의존성(DB, 외부 API)을 체크하면 안 됩니다. 외부가 죽었다고 앱을 재시작해봤자 외부가 복구되지 않고, 오히려 모든 Pod가 동시에 재시작되면서 장애가 확산됩니다. 외부 의존성은 readinessProbe에서 체크합니다.
⚠️ 주의: JVM이나 대용량 데이터 로딩 같이 시작이 느린 앱에 startupProbe 없이 livenessProbe만 설정하면 "아직 뜨는 중인데 죽었다고 판단 → 재시작 → 또 안 떠서 재시작" 무한 루프에 빠집니다.
Init Container & Sidecar — 멀티 컨테이너 패턴
Pod에 컨테이너가 하나만 들어가는 건 아닙니다.
메인 앱 시작 전에 사전 작업을 하거나, 메인 앱 옆에서 보조 기능을 수행하는 컨테이너를 넣을 수 있습니다.
Init Container — 시작 전 준비
Init Container는 메인 컨테이너보다 먼저 실행되고, 성공하면 종료됩니다. 여러 개를 정의하면 순서대로 하나씩 실행됩니다.
spec:
initContainers:
- name: wait-for-db
image: busybox
command: ['sh', '-c', 'until nc -z db-svc 5432; do sleep 2; done']
- name: run-migrations
image: myapp-migrations:v1
command: ['python', 'manage.py', 'migrate']
containers:
- name: app
image: myapp:v1
DB가 준비될 때까지 기다리고, 마이그레이션을 실행하고, 그 다음에야 메인 앱이 뜹니다.
네이티브 Sidecar
사이드카는 메인 앱과 함께 계속 돌면서 보조 기능(로그 수집, 프록시 등)을 수행합니다.
v1.33부터 initContainers에 restartPolicy: Always를 설정하는 방식으로 공식 지원됩니다.
spec:
initContainers:
- name: log-collector
image: fluentbit:latest
restartPolicy: Always # 사이드카의 핵심. 종료 안 하고 계속 실행
volumeMounts:
- name: logs
mountPath: /var/log/app
containers:
- name: app
image: myapp:v1
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}
네이티브 사이드카가 나오기 전에는 일반 containers에 사이드카를 넣었는데, 이러면 Job/CronJob에서 문제가 생겼습니다.
메인 컨테이너가 끝나도 사이드카가 살아 있어서 Job이 완료 처리가 안 되는 거죠.
네이티브 사이드카는 Job 완료 판정에서 제외되어 이 문제가 해결됐습니다.
워크로드 선택 가이드
어떤 리소스를 써야 할지 한눈에 보이도록 정리했습니다.
| 리소스 | 언제 쓰는가 | 핵심 특징 |
| Deployment | Stateless 앱 (웹 서버, API) | 롤링 업데이트, 롤백 |
| StatefulSet | Stateful 앱 (DB, 메시지 큐) | 고정 이름, 안정적 스토리지 |
| DaemonSet | 노드당 1개 (로그, 모니터링) | 노드 추가 시 자동 배포 |
| Job | 일회성 배치 작업 | 완료 후 종료 |
| CronJob | 정기 반복 작업 | cron 스케줄로 Job 생성 |
정리
Deployment가 모든 걸 커버할 수 없기 때문에, 상황에 따라 알맞는 서비스를 사용할 수 있습니다.
- 상태가 필요하면 StatefulSet, 노드당 하나면 DaemonSet,
- 한 번 돌고 끝이면 Job/CronJob.
- Probe로 컨테이너 상태를 감시하되 liveness에는 외부 의존성을 넣지 않는 게 핵심이고,
- 멀티 컨테이너가 필요하면 Init Container와 네이티브 Sidecar를 활용합니다.
다음 글에서는 이 Pod들이 서로, 그리고 외부와 어떻게 통신하는지 — Service, Ingress, NetworkPolicy를 다루겠습니다.
'DevOps > kubernetes' 카테고리의 다른 글
| 6. ConfigMap과 Secret - 설정을 이미지에서 분리 (0) | 2026.06.13 |
|---|---|
| 5. Service와 Ingress - 네트워킹 정리 (0) | 2026.06.06 |
| 3. Pod에서 Deployment 까지 (0) | 2026.05.16 |
| 2. Kubernetes 클러스터 아키텍처 (0) | 2026.05.09 |
| 1. 쿠버네티스란 무엇일까? (0) | 2026.04.25 |