Kubernetes 개념 한판 정리 - 아키텍처부터 실전 배포까지
왜 Kubernetes인가
Docker로 컨테이너 하나 띄우는 건 쉽다. 근데 실제 서비스는 그렇게 단순하지 않다.
- 컨테이너가 죽으면 누가 다시 띄워주지?
- 트래픽이 늘면 컨테이너를 어떻게 늘리지?
- 여러 서버에 컨테이너를 어떻게 분산하지?
- 무중단 배포는 어떻게 하지?
이런 문제를 해결하는 게 컨테이너 오케스트레이션이고, Kubernetes(K8s)가 사실상 표준이다.
| 기능 | 설명 |
|---|---|
| 자동 복구 (Self-healing) | 컨테이너 죽으면 자동 재시작 |
| 오토스케일링 | 트래픽에 따라 Pod 수 자동 조절 |
| 로드밸런싱 | 여러 Pod에 트래픽 분산 |
| 롤링 업데이트 | 무중단 배포 |
| 서비스 디스커버리 | DNS 기반 서비스 간 통신 |
Kubernetes 아키텍처
K8s 클러스터는 Control Plane과 Worker Node로 구성된다.
Kubernetes 클러스터 아키텍처 - Control Plane과 Worker Node 구성
Control Plane 구성요소
| 구성요소 | 역할 |
|---|---|
| API Server | 모든 요청의 진입점. kubectl 명령이 여기로 들어옴 |
| etcd | 클러스터 상태 저장소 (Key-Value DB) |
| Scheduler | 새 Pod를 어느 Node에 배치할지 결정 |
| Controller Manager | 원하는 상태(Desired State)를 유지. ReplicaSet, Deployment 등 관리 |
Worker Node 구성요소
| 구성요소 | 역할 |
|---|---|
| kubelet | Node의 에이전트. Pod 생성/삭제/상태 보고 |
| kube-proxy | 네트워크 프록시. Service → Pod 트래픽 라우팅 |
| Container Runtime | 실제 컨테이너 실행 (containerd, CRI-O 등) |
핵심 오브젝트
K8s에서는 모든 것이 오브젝트다. YAML로 원하는 상태(Desired State)를 선언하면, K8s가 알아서 현재 상태를 맞춰준다.
Pod - 최소 배포 단위
Pod는 K8s에서 가장 작은 배포 단위다. 하나 이상의 컨테이너를 포함한다.
| Pod 특징 | 설명 |
|---|---|
| 네트워크 | 같은 네트워크 네임스페이스 (localhost 통신) |
| 스토리지 | 같은 볼륨 공유 가능 |
| IP | Pod 단위로 고유 IP 할당 |
Pod 특징:
- Pod 내 컨테이너들은
localhost로 서로 통신 - Pod마다 고유 IP 할당 (클러스터 내부 IP)
- Pod는 일시적(Ephemeral). 죽으면 새 IP로 재생성됨
- 그래서 직접 Pod IP로 접근하면 안 됨 → Service 사용
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-app
labels:
app: my-app
env: production
spec:
containers:
- name: app
image: my-app:1.0
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
ReplicaSet - Pod 복제 관리
ReplicaSet은 지정한 수의 Pod를 항상 유지한다. Pod가 죽으면 자동으로 새로 생성.
ReplicaSet의 핵심은 Self-healing이다. replicas: 3으로 설정하면 항상 3개의 Pod를 유지한다. Pod 2가 죽으면 자동으로 새 Pod 4를 생성하여 3개를 맞춘다.
Deployment - 배포 관리
실무에서는 Pod나 ReplicaSet을 직접 만들지 않는다. Deployment를 사용한다.
Deployment가 ReplicaSet을 관리하고, ReplicaSet이 Pod를 관리하는 구조다.
Deployment는 ReplicaSet을 관리한다. 버전 업데이트(v1.0 → v1.1) 시 새 ReplicaSet을 만들고 기존 ReplicaSet의 Pod를 점진적으로 종료한다. 이것이 롤링 업데이트다.
Deployment의 장점:
- 롤링 업데이트: 무중단 배포
- 롤백: 문제 생기면 이전 버전으로 복구
- 스케일링:
kubectl scale명령으로 Pod 수 조절
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 업데이트 중 최대 추가 Pod 수
maxUnavailable: 0 # 업데이트 중 최소 가용 Pod 수 보장
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:1.0
ports:
- containerPort: 8080
Service - 네트워크 추상화
Pod는 일시적이고 IP가 바뀐다. Service는 고정된 엔드포인트를 제공한다.
Service는 selector로 매칭되는 Pod들에게 트래픽을 분산한다. Service IP(예: 10.96.0.100)는 고정이므로, Pod가 죽고 새로 생겨도 클라이언트는 같은 IP로 접근할 수 있다.
kube-proxy가 Service와 Pod 간 트래픽 라우팅을 담당한다
Service 타입:
| 타입 | 설명 | 접근 범위 |
|---|---|---|
| ClusterIP | 클러스터 내부 IP (기본값) | 클러스터 내부만 |
| NodePort | 모든 Node의 특정 포트로 노출 | 외부 접근 가능 |
| LoadBalancer | 클라우드 LB 연동 | 외부 접근 가능 |
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-service
spec:
type: ClusterIP
selector:
app: my-app
ports:
- port: 80 # Service 포트
targetPort: 8080 # Pod 포트
Ingress - HTTP 라우팅
Ingress는 외부 HTTP(S) 트래픽을 클러스터 내부 Service로 라우팅한다. 도메인 기반, 경로 기반 라우팅이 가능하다.
Ingress Controller(nginx, traefik 등)가 외부 HTTP 트래픽을 받아 도메인/경로 규칙에 따라 내부 Service로 라우팅한다.
| 요청 | 라우팅 대상 |
|---|---|
| api.example.com/* | api-service:80 |
| web.example.com/* | web-service:80 |
| example.com/api/* | api-service:80 |
| example.com/admin/* | admin-service:80 |
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- host: web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
Namespace - 논리적 분리
하나의 클러스터를 여러 팀이나 환경으로 분리할 때 사용한다.
| Namespace | 용도 | ResourceQuota 예시 |
|---|---|---|
| dev | 개발 환경 | CPU 4 cores, Memory 8Gi |
| staging | 스테이징 환경 | CPU 8 cores, Memory 16Gi |
| prod | 프로덕션 환경 | CPU 32 cores, Memory 64Gi |
# Namespace 생성
kubectl create namespace dev
# 특정 Namespace에 리소스 생성
kubectl apply -f deployment.yaml -n dev
# Namespace별 리소스 확인
kubectl get pods -n dev
kubectl get all -n prod
ConfigMap & Secret
환경별로 다른 설정값을 관리한다.
| 구분 | ConfigMap | Secret |
|---|---|---|
| 용도 | 일반 설정값 | 민감한 정보 (비밀번호, API 키) |
| 저장 방식 | 평문 | Base64 인코딩 |
| 예시 | DB 호스트, 로그 레벨 | DB 비밀번호, JWT 시크릿 |
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
DATABASE_HOST: "mysql-service"
LOG_LEVEL: "INFO"
SPRING_PROFILES_ACTIVE: "production"
---
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secret
type: Opaque
data:
DATABASE_PASSWORD: cGFzc3dvcmQxMjM= # base64 encoded
JWT_SECRET: c2VjcmV0LWtleS0xMjM0NTY=
Pod에서 사용하는 방법:
spec:
containers:
- name: app
image: my-app:1.0
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secret
# 또는 개별 환경변수로
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secret
key: DATABASE_PASSWORD
Volume & PersistentVolume
Pod는 일시적이다. 데이터를 영구 저장하려면 Volume을 사용해야 한다.
| Volume 종류 | 설명 | 수명 |
|---|---|---|
| emptyDir | Pod 내 컨테이너간 임시 공유 | Pod 삭제 시 소멸 |
| hostPath | Node의 파일시스템 마운트 | Node에 종속적 |
| PersistentVolume | 클러스터 레벨의 영구 스토리지 | 독립적 (권장) |
PV / PVC 구조
PV(PersistentVolume)는 인프라 관리자가 프로비저닝하고, PVC(PersistentVolumeClaim)는 개발자가 요청한다.
| 구성요소 | 역할 | 관리 주체 |
|---|---|---|
| Pod | volumeMounts로 PVC 연결 | 개발자 |
| PVC | 스토리지 요청 (예: 10Gi) | 개발자 |
| PV | 실제 스토리지 (AWS EBS, NFS 등) | 인프라 관리자 |
# pv.yaml - 인프라 관리자가 생성
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: standard
hostPath:
path: /data/my-pv
---
# pvc.yaml - 개발자가 생성
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: standard
---
# Pod에서 PVC 사용
spec:
containers:
- name: app
volumeMounts:
- name: data
mountPath: /app/data
volumes:
- name: data
persistentVolumeClaim:
claimName: my-pvc
오토스케일링
HPA (Horizontal Pod Autoscaler)
CPU/메모리 사용량에 따라 Pod 수를 자동 조절한다.
HPA는 CPU/메모리 사용률을 모니터링하여 Pod 수를 자동 조절한다.
| CPU 사용률 | 동작 | Pod 수 |
|---|---|---|
| 50% 이하 | Scale Down | 1개로 축소 |
| 50% 목표 | 유지 | 3개 유지 |
| 80% 이상 | Scale Up | 5개로 확장 |
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70
실전: Spring Boot 앱 배포
전체 구성을 한 번에 보자.
# 1. Namespace
apiVersion: v1
kind: Namespace
metadata:
name: my-app
---
# 2. ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: my-app
data:
SPRING_PROFILES_ACTIVE: "production"
SERVER_PORT: "8080"
---
# 3. Secret
apiVersion: v1
kind: Secret
metadata:
name: app-secret
namespace: my-app
type: Opaque
data:
DB_PASSWORD: cGFzc3dvcmQxMjM=
---
# 4. Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:1.0.0
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secret
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
---
# 5. Service
apiVersion: v1
kind: Service
metadata:
name: my-app-service
namespace: my-app
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
---
# 6. Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: my-app
spec:
ingressClassName: nginx
rules:
- host: my-app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-service
port:
number: 80
---
# 7. HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
namespace: my-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
kubectl 명령어 정리
# 리소스 조회
kubectl get pods # Pod 목록
kubectl get pods -o wide # 상세 정보 (Node, IP)
kubectl get all # 모든 리소스
kubectl get all -n my-namespace # 특정 Namespace
# 리소스 상세 정보
kubectl describe pod my-pod
kubectl describe deployment my-app
# 로그 확인
kubectl logs my-pod # Pod 로그
kubectl logs -f my-pod # 실시간 로그
kubectl logs my-pod -c my-container # 특정 컨테이너 로그
# 리소스 생성/수정/삭제
kubectl apply -f deployment.yaml # 생성 또는 수정
kubectl delete -f deployment.yaml # 삭제
kubectl delete pod my-pod # Pod 삭제
# 스케일링
kubectl scale deployment my-app --replicas=5
# 롤아웃 (배포)
kubectl rollout status deployment my-app # 배포 상태
kubectl rollout history deployment my-app # 배포 이력
kubectl rollout undo deployment my-app # 롤백
# Pod 접속
kubectl exec -it my-pod -- /bin/bash
# 포트 포워딩 (로컬 테스트)
kubectl port-forward pod/my-pod 8080:8080
kubectl port-forward svc/my-service 8080:80
자주 묻는 질문
Pod가 계속 CrashLoopBackOff 상태인데요?
컨테이너가 시작 후 바로 종료되는 상황이다.
# 로그 확인
kubectl logs my-pod --previous
# 이벤트 확인
kubectl describe pod my-pod
흔한 원인:
- 애플리케이션 에러 (설정 누락, 포트 충돌)
- 리소스 부족 (메모리 OOM)
- liveness probe 실패
ImagePullBackOff는 뭔가요?
이미지를 Pull 받지 못하는 상태다.
확인 사항:
- 이미지 이름/태그가 정확한지
- Private 레지스트리면 imagePullSecrets 설정했는지
- 네트워크 문제는 없는지
Pod가 Pending 상태에서 안 넘어가요
Scheduler가 Pod를 배치할 Node를 찾지 못한 상태다.
kubectl describe pod my-pod
# Events 섹션에서 원인 확인
흔한 원인:
- 리소스 부족 (CPU, Memory 요청량 > 가용량)
- nodeSelector가 매칭되는 Node가 없음
- PVC가 바인딩되지 않음
Service로 Pod에 접근이 안 돼요
# Endpoints 확인 (연결된 Pod IP 목록)
kubectl get endpoints my-service
# 없으면 selector가 Pod label과 일치하는지 확인
kubectl get pods --show-labels
StatefulSet은 뭔가요? Deployment랑 뭐가 다른가요?
StatefulSet은 상태를 가진 애플리케이션용이다. DB, 메시지 큐 같은 것들.
| 구분 | Deployment | StatefulSet |
|---|---|---|
| Pod 이름 | 랜덤 (my-app-abc123) | 순차 (my-app-0, my-app-1) |
| 스토리지 | 공유 또는 없음 | Pod마다 고유 PVC |
| 생성/삭제 순서 | 동시 | 순차 (0→1→2, 2→1→0) |
| 용도 | Stateless 앱 (API 서버) | Stateful 앱 (DB, Kafka) |
대부분의 Spring Boot 앱은 Deployment로 충분하다. StatefulSet은 DB 클러스터 같은 특수한 경우에만 쓴다.
로컬에서 K8s 테스트하려면 어떻게 하나요?
여러 옵션이 있다.
| 도구 | 특징 | 추천 |
|---|---|---|
| Docker Desktop | 가장 쉬움. 설정에서 K8s 활성화 | 입문자 |
| minikube | 단일 노드 클러스터. 다양한 드라이버 지원 | 학습용 |
| kind | Docker 컨테이너로 클러스터 구성. 가벼움 | CI/CD |
| k3s | 경량 K8s. ARM 지원 | 라즈베리파이, Edge |
# minikube 예시
minikube start
kubectl get nodes
minikube dashboard # 웹 대시보드
# kind 예시
kind create cluster --name my-cluster
kubectl cluster-info
Helm이 뭔가요?
K8s용 패키지 매니저다. 여러 YAML 파일을 하나의 Chart로 묶어서 관리한다.
# nginx 설치 예시
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install my-nginx bitnami/nginx
# 값 커스텀
helm install my-nginx bitnami/nginx --set replicaCount=3
ConfigMap, Secret, Deployment, Service 등을 일일이 만들 필요 없이 Chart 하나로 설치 가능. 버전 관리, 롤백도 쉽다.
정리
Kubernetes 핵심 개념:
- Pod: 최소 배포 단위, 일시적(Ephemeral)
- Deployment: Pod 배포 관리, 롤링 업데이트/롤백
- Service: Pod에 대한 고정 엔드포인트, 로드밸런싱
- Ingress: HTTP 라우팅, 도메인/경로 기반
- ConfigMap/Secret: 설정값 외부화
- PV/PVC: 영구 스토리지
- HPA: CPU/메모리 기반 오토스케일링
- Namespace: 클러스터 논리적 분리
선언형(Declarative) 방식으로 원하는 상태를 YAML에 기술하면, K8s가 알아서 맞춰주는 게 핵심이다.
참고문헌
- Kubernetes 공식 문서
- Kubernetes The Hard Way - 직접 클러스터 구축
- CNCF Kubernetes
