앞 글에서 쿠버네티스의 클러스터 아키텍처를 정리했습니다. 컨트롤 플레인과 워커 노드가 어떻게 역할을 나누는지는 이해했는데, 정작 "그래서 내 애플리케이션은 어떻게 올리지?"하는 궁금증이 생겼습니다.
이번 글에서는 쿠버네티스에서 앱을 실행하는 기본 단위인 Pod부터 시작해서, ReplicaSet을 거쳐 Deployment까지 왜 이런 계층이 필요한지를 단계적으로 정리합니다. Labels와 Selectors가 이 리소스들을 어떻게 연결하는지도 함께 다룹니다.
Pod — 쿠버네티스의 최소 실행 단위
Docker에서는 컨테이너가 기본 단위였는데, 쿠버네티스에서는 Pod가 그 역할을 합니다.
Pod는 하나 이상의 컨테이너를 감싸는 래퍼(wrapper)인데, 같은 Pod 안의 컨테이너들은 네트워크(IP와 포트)와 스토리지 볼륨을 공유합니다. 대부분의 경우 Pod 하나에 컨테이너 하나가 들어갑니다.
간단한 Pod 하나를 띄워보면 이렇습니다.
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
kubectl apply -f pod.yaml
kubectl get pods
kubectl describe pod nginx-pod
여기서 resources.requests와 limits를 설정한 이유가 있습니다.
requests는 스케줄러가 "이 Pod를 어느 노드에 배치할지" 판단할 때 기준으로 쓰고, limits는 런타임에 컨테이너가 쓸 수 있는 자원의 상한선입니다. CPU limits를 초과하면 스로틀링(느려짐), 메모리 limits를 초과하면 OOMKilled(강제 종료)가 발생합니다.
이 차이는 나중에 리소스 관리 글에서 다시 다룰 예정입니다.
Pod를 직접 쓰면 안 되는 이유
처음 공부할 때 "Pod를 만들면 앱이 뜨니까 이걸로 충분하지 않나?" 싶었는데, 셀프힐링이 되지 않는 문제가 있었습니다.
Pod를 직접 만들면 노드가 죽거나 Pod가 비정상 종료됐을 때 아무도 다시 띄워주지 않습니다. 쿠버네티스의 셀프힐링이 동작하려면 "이 Pod를 몇 개 유지해라"고 선언하는 상위 리소스가 필요합니다.
그래서 Pod를 직접 만드는 건 디버깅이나 일회성 테스트 정도에만 쓰고, 프로덕션 워크로드는 Deployment 같은 상위 리소스를 통해 관리합니다.
Pod 생명주기와 디버깅
Pod의 상태(phase)는 이런 흐름을 따릅니다.
Pending → Running → Succeeded / Failed
Pending이면 스케줄링 대기 중이거나 이미지를 받고 있는 것이고, Running이면 컨테이너가 실행 중인 상태입니다.
다만 kubectl get pods에서 STATUS 컬럼에 보이는 ImagePullBackOff, CrashLoopBackOff, ContainerCreating 같은 값들은 엄밀히는 phase가 아니라 컨테이너의 상태(reason) 입니다.
이 둘이 같은 컬럼에 섞여 나오기 때문에 헷갈리기 쉬운데, phase는 Pod 전체의 거시적 상태고, reason은 컨테이너 레벨의 세부 상태라고 구분해두면 트러블슈팅할 때 편합니다.
문제가 생겼을 때 쓰는 명령 몇 가지를 적어둡니다.
# Pod 내부 쉘 접속
kubectl exec -it <pod-name> -- /bin/sh
# 로그 확인
kubectl logs <pod-name>
kubectl logs <pod-name> --previous # 재시작 전 로그
# 일회용 디버그 Pod
kubectl run debug --image=curlimages/curl -it --rm -- sh
--previous 옵션은 컨테이너가 CrashLoopBackOff로 재시작됐을 때 이전 로그를 볼 수 있어서 꽤 유용합니다.
Labels와 Selectors — 리소스를 식별하고 연결
Pod에서 Deployment로 넘어가기 전에 Labels와 Selectors를 먼저 짚어야 합니다.
쿠버네티스에서 리소스 간 연결은 거의 다 이걸로 동작하기 떄문입니다.
- Label
리소스에 붙이는 키-값 쌍입니다. app: web, env: production 같은 식으로 태그를 다는 거죠. - Selector
Label을 기준으로 리소스를 골라내는 필터입니다. Service가 "app=web인 Pod에 트래픽을 보내라", Deployment가 "app=web인 Pod를 관리하라" — 이런 연결이 전부 Selector로 이루어집니다.
# 특정 Label이 있는 Pod만 조회
kubectl get pods -l app=web
# Label 추가
kubectl label pod nginx-pod env=production
# Label 삭제 (키 뒤에 - 붙임)
kubectl label pod nginx-pod env-
여기서 주의할 점이 있는데, 실행 중인 Pod에서 Label을 빼면 즉시 영향이 갑니다.
예를 들어 app=web Label을 제거하면, 그 Pod는 Service 엔드포인트에서 빠지고 ReplicaSet 관리 대상에서도 벗어납니다. ReplicaSet은 "Pod가 하나 줄었다"고 판단해서 새 Pod를 만들게 됩니다.
Annotation은 Label과 비슷하게 키-값 쌍이지만, Selector로 선택할 수 없고 단순히 정보를 저장하는 용도입니다.
빌드 커밋 해시나 Prometheus 스크래핑 설정 같은 걸 넣습니다. "이 값으로 리소스를 필터링할 일이 있나?" — 있으면 Label, 없으면 Annotation이라고 생각하면 됩니다.
ReplicaSet — Pod 수를 유지하는 컨트롤러
ReplicaSet은 "이 Pod를 n개 유지해라"라는 선언을 이행하는 컨트롤러입니다. Pod가 삭제되면 즉시 새로 만들고, 초과되면 지웁니다.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: web-rs
spec:
replicas: 3
selector:
matchLabels:
app: web # 이 Label을 가진 Pod를 관리
template:
metadata:
labels:
app: web # selector와 반드시 일치해야 함
spec:
containers:
- name: nginx
image: nginx:1.27
# Pod 하나를 강제로 지우면
kubectl delete pod <pod-name>
# 즉시 새 Pod가 생성되는 걸 확인
kubectl get pods -l app=web -w
앞 글에서 셀프힐링의 정체가 컨트롤러의 루프라고 했는데, ReplicaSet이 바로 그 대표적인 예시입니다. ReplicaSet Controller가 현재 Pod 수와 원하는 수를 비교하고, 차이가 있으면 맞춰줍니다.
그런데 ReplicaSet에는 치명적인 한계가 있습니다. 이미지를 업데이트해도 기존 Pod가 교체되지 않습니다.
template의 이미지를 nginx:1.28로 바꿔도 이미 돌고 있는 Pod는 nginx:1.27 그대로입니다. 새 이미지를 적용하려면 기존 Pod를 하나하나 수동으로 지워야 합니다.
⚠️ 주의: ReplicaSet의 selector는 생성 후 변경할 수 없습니다(immutable). selector를 바꾸려면 삭제하고 다시 만들어야 합니다.
이 문제를 해결하는 게 바로 Deployment입니다.
Deployment — 실무에서 쓰는 배포 단위
Deployment는 ReplicaSet 위에 롤링 업데이트와 롤백 기능을 얹은 리소스입니다. 이미지를 변경하면 새 ReplicaSet을 만들어서 점진적으로 Pod를 교체하고, 문제가 생기면 이전 ReplicaSet으로 되돌릴 수 있습니다.
계층 구조는 이렇습니다.
Deployment: web-app
├── ReplicaSet (현재, hash:xyz) — replicas: 3
│ ├── Pod 1
│ ├── Pod 2
│ └── Pod 3
└── ReplicaSet (이전, hash:abc) — replicas: 0
Deployment가 직접 Pod를 관리하는 게 아니라, ReplicaSet을 통해 간접 관리합니다.
업데이트가 발생하면 새 ReplicaSet을 만들고, 이전 ReplicaSet은 replicas: 0으로 유지해서 롤백에 대비합니다.
기본 매니페스트
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web-app # 불변. 생성 후 변경 불가
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 업데이트 중 초과 허용 Pod 수
maxUnavailable: 0 # 0이면 항상 3개는 유지
template:
metadata:
labels:
app: web-app # selector와 일치 필수
spec:
containers:
- name: app
image: myapp:v1
ports:
- containerPort: 8080
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
⚠️ 주의: spec.selector는 불변(immutable)입니다. 생성 후 변경할 수 없고, template.metadata.labels는 이 selector를 반드시 포함해야 합니다.
롤링 업데이트와 롤백
이미지를 변경하면 롤링 업데이트가 자동으로 진행됩니다.
# 이미지 업데이트
kubectl set image deployment/web-app app=myapp:v2
# 롤아웃 진행 상태 확인
kubectl rollout status deployment/web-app
# ReplicaSet 히스토리 확인 — 구 RS는 replicas=0
kubectl get replicasets -l app=web-app
문제가 생기면 아래의 명령어로 롤백을 진행할 수 있습니다.
# 히스토리 확인
kubectl rollout history deployment/web-app
# 직전 버전으로 롤백
kubectl rollout undo deployment/web-app
# 특정 리비전으로 롤백
kubectl rollout undo deployment/web-app --to-revision=2
rollout undo를 치면 이전 ReplicaSet의 replicas를 다시 올리고 현재 ReplicaSet을 줄이는 방식으로 동작합니다.
이전 ReplicaSet이 남아 있기 때문에 가능한 건데, 이걸 몇 개까지 보관할지가 revisionHistoryLimit(기본값 10)입니다.
너무 낮게 잡으면 오래된 버전으로 롤백이 안 되니 기본값을 유지하는 게 좋습니다.
업데이트 전략 — RollingUpdate vs Recreate
- RollingUpdate(기본값)는 이전 Pod를 하나씩 교체합니다.
maxUnavailable: 0, maxSurge: 1로 설정하면 항상 원하는 수 이상의 Pod가 떠 있으므로 무중단 배포가 됩니다.
단, 이게 진짜 "무중단"이 되려면 아래의 세 가지 조건이 갖춰져야 합니다.
- readinessProbe가 설정되어 있을 것 (새 Pod가 준비 완료됐는지 확인)
- 앱이 graceful shutdown을 처리할 것 (SIGTERM 받으면 진행 중 요청을 마무리)
- maxUnavailable이 0일 것
- Recreate는 기존 Pod를 전부 삭제하고 새로 만듭니다. 다운타임이 생기지만, 구버전과 신버전이 동시에 떠 있으면 안 되는 경우(DB 스키마 마이그레이션 등)에 씁니다.
일시 중지와 재개
여러 변경을 한꺼번에 적용하고 싶을 때 유용합니다.
# 업데이트 일시 중지
kubectl rollout pause deployment/web-app
# 변경 여러 개 적용 (아직 반영 안 됨)
kubectl set image deployment/web-app app=myapp:v3
kubectl set resources deployment/web-app -c=app --limits=cpu=1,memory=512Mi
# 재개 — 위 변경이 한 번에 롤아웃
kubectl rollout resume deployment/web-app
스케일 조정
# 수동 스케일
kubectl scale deployment/web-app --replicas=5
# 오토스케일러 연동 (HPA — 별도 글에서 다룸)
kubectl autoscale deployment/web-app --min=2 --max=10 --cpu-percent=70
Pod → ReplicaSet → Deployment 계층이 필요한 이유
처음에는 "왜 이렇게 복잡하게 관리하는걸까?" 하는 생각이 들었는데, 정리해보니 각 계층이 명확한 역할을 하고 있었습니다.
| 계층 | 역할 | 한계 |
| Pod | 컨테이너 실행의 최소 단위 | 셀프힐링 없음 |
| ReplicaSet | Pod 수 유지 (셀프힐링) | 이미지 업데이트 시 자동 교체 안 됨 |
| Deployment | 롤링 업데이트 + 롤백 | — (실무 표준) |
각 계층의 한계가 다음 계층이 존재하는 이유입니다. Pod만으로는 복구가 안 되고, ReplicaSet만으로는 업데이트가 불편하고, 그래서 Deployment가 나온 거죠.
트러블슈팅 팁 : 롤아웃이 멈췄을 때
kubectl rollout status deployment/web-app
kubectl describe deployment web-app
kubectl get pods -l app=web-app
kubectl logs <새-pod-name>
새 Pod가 Ready 상태가 안 되면 롤아웃이 진행되지 않습니다. 이미지 태그 오타, readinessProbe 실패, 리소스 부족이 흔한 원인입니다.
Pod가 ImagePullBackOff인 경우
이미지 이름이나 태그를 잘못 적었거나, 프라이빗 레지스트리 인증이 안 된 경우입니다. kubectl describe pod <name>의 Events 섹션에서 정확한 원인이 나옵니다.
정리
- Pod는 최소 실행 단위이지만 셀프힐링이 없기 때문에 직접 쓰지 않고,
- ReplicaSet이 Pod 수를 유지해주고,
- Deployment가 ReplicaSet 위에서 롤링 업데이트와 롤백을 관리합니다.
- Labels와 Selectors가 이 리소스들을 연결하는 핵심 메커니즘입니다.
다음 글에서는 StatefulSet, DaemonSet, Job, CronJob 등 용도별 워크로드 리소스를 다루겠습니다.
'DevOps > kubernetes' 카테고리의 다른 글
| 6. ConfigMap과 Secret - 설정을 이미지에서 분리 (0) | 2026.06.13 |
|---|---|
| 5. Service와 Ingress - 네트워킹 정리 (0) | 2026.06.06 |
| 4. StatefulSet 부터 CronJob 까지 (0) | 2026.05.30 |
| 2. Kubernetes 클러스터 아키텍처 (0) | 2026.05.09 |
| 1. 쿠버네티스란 무엇일까? (0) | 2026.04.25 |