Kubernetes 완벽 가이드: 컨테이너 오케스트레이션
이 글의 핵심
Kubernetes(K8s)는 컨테이너화된 애플리케이션의 배포, 확장, 관리를 자동화하는 오픈소스 플랫폼입니다. Google이 개발하고 CNCF가 관리하며, 선언적 설정, 자가 치유, 수평 확장, 서비스 디스커버리, 롤링 업데이트를 제공합니다.
솔직히 말하면 k8s는 복잡해요. 문서 읽다 보면 “선언적이에요, 자가 치유에요” 같은 말이 쏟아지는데, 막상 운영 들어가면 CRD, 네트워크 플러그인, StorageClass, RBAC… 한 번에 머릿속이 꽉 차요. 그래서 정말 필요할 때만 쓰는 편이 낫다고 봅니다. VM 한 대에 Docker Compose로도 충분한 서비스에 굳이 클러스터 띄우면, 배보다 배꼽이 커질 때가 많거든요.
그래도 팀이 “이제 쿠버네티스 쓴다”고 하면, 개념을 아예 몰랐다가 현장에서 터지는 케이스가 하나쯤은 있어요. 제가 가장 자주 본 건 파드가 계속 재시작하는 상황이에요. 대시보드엔 CrashLoopBackOff만 반짝이고, “방금 잘 떴는데?” 싶다가 kubectl get pods 해보면 RESTARTS 칼럼이 올라가기만 하죠. 이럴 때 첫 손님은 kubectl describe pod <이름>이에요. Events 쪽에 OOMKilled, ImagePullBackOff, probe 실패 같은 힌트가 한 줄씩 박혀 있어서, 로그(kubectl logs, 필요하면 kubectl logs --previous)보다 먼저 보는 경우가 많아요. 메모리 limit이 타이트하면 조용히 죽고, liveness가 너무 공격적이면 “아직 준비 안 됐는데” 죽었다가 다시 뜨는 루프에 들어가요. 이미지 태그가 틀렸으면 pull 단계에서 멈추고요.
이게 왜 k8s 스럽냐면, 컨테이너 런타임이 “프로세스 죽으면 다시 켤게”를 해 주고, Deployment가 “원하는 개수 맞출게”를 해 주는데, 둘 다 의도를 YAML로 적어야 제대로 돌아가거든요. 그래서 Pod·Deployment·Service 같은 말이 책 머리말이 아니라, 재시작 루프 풀 때 손에 잡히는 용어가 돼요. Pod는 최소로 묶는 단위, Deployment는 “몇 개 띄울지, 어떤 이미지 쓸지, 롤링으로 갈아끼울지”를 잡는 쪽, Service는 “그 애들한테 트래픽을 어떻게 붙일지”예요. Minikube로 한 번 kubectl get 찍어보는 정도는 해볼 만해요. 로컬에서 이미지 받느라 첫 start가 느릴 수는 있어요.
예시로 Pod 하나(리소스만 살짝 잡은 버전)는 이런 느낌이에요. 실서비스에선 requests/limits 꼭 잡는 걸 권해요. 안 잡으면 이웃 Pod가 힘들어질 뿐 아니라, OOM 났을 때 원인도 더 묘해져요.
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.24
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
kubectl apply -f pod.yaml
kubectl get pods
kubectl logs nginx-pod
kubectl exec -it nginx-pod -- /bin/bash
배포는 Deployment로 묶는 경우가 대부분이에요. replica 늘리고, kubectl set image로 버전 갈아끼우고, 망하면 rollout undo—이 흐름이 익숙해지면 “재시작”이 장애인지 의도된 롤백/배포인지 감이 잡혀요.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.24
ports:
- containerPort: 80
kubectl apply -f deployment.yaml
kubectl get deployments
kubectl get pods
kubectl scale deployment nginx-deployment --replicas=5
kubectl set image deployment/nginx-deployment nginx=nginx:1.25
kubectl rollout undo deployment/nginx-deployment
Service는 “클러스터 안에서 어디로 붙냐” 문제예요. Minikube에선 minikube service ...로 한 번 띄워보면 감이 와요.
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
설정/비밀은 ConfigMap·Secret로 빼는 패턴이 흔해요. Secret은 base64인 거 알면서도 커밋에 넣지 말고, CI나 sealed-secrets 쪽 흐름을 쓰는 편이 안전하다고 봅니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
DATABASE_HOST: postgres
DATABASE_PORT: "5432"
---
apiVersion: v1
kind: Secret
metadata:
name: app-secret
type: Opaque
data:
DATABASE_PASSWORD: cGFzc3dvcmQ=
Ingress, probe, HPA, PV까지 가면 글이 아니라 책이 되니, 여기서만 짚으면: 외부 HTTP 라우팅이 필요하면 Ingress, “살아는 있는데 트래픽 받을 준비 됐냐”는 readiness, “죽었냐”는 liveness, 트래픽/CPU 보고 늘리면 HPA, 디스크 남기려면 PV/PVC. 이름만 나열이 아니라, 한 번씩 YAML 열어서 averageUtilization이나 initialDelaySeconds가 왜 박혀 있는지 보면 돼요.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nginx-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx-deployment
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
정리하자면, k8s는 “모든 팀이 당장 써야 할 만큼 쉬운” 도구는 아니에요. 대신 여러 팀, 여러 서비스, 롤백·스케일·네트워크가 한 번에 겹칠 때 가치가 나와요. 그때 파드가 계속 재시작하면 당황하지 말고 describe로 events, logs로 앱, 그다음이 리소스·프로브·이미지 순으로 의심해 보시면, 체감 난이도가 한 단계는 내려가요.