Kubernetes 핵심 개념 완전 정리 — Pod부터 Ingress까지 실전 가이드
이 글은 K3s 클러스터로 ASAT(청각 재활 훈련 시스템)를 이중화 운영한 실제 경험을 바탕으로 작성했습니다.
목차
- Kubernetes 기본 개념
- Pod의 생명주기 관리
- Deployment 사용 및 관리
- Service와 Ingress로 네트워킹 구성
- 지속적인 스토리지 관리
- 실전 사례: K3s로 ASAT 이중화 운영
1. Kubernetes 기본 개념
왜 Kubernetes가 필요한가?
Docker로 컨테이너를 띄우는 건 쉽습니다. 하지만 10개 이상의 서비스를 운영하면 이런 문제가 생깁니다:
- 컨테이너가 죽으면 누가 다시 띄우나?
- 트래픽이 늘면 어떻게 스케일 아웃?
- 여러 서버에 컨테이너를 어떻게 분배?
- 무중단 배포는?
Kubernetes는 이 모든 걸 선언적으로 해결합니다.
핵심 아키텍처
┌─────────────────────────────────────────────┐
│ Control Plane │
│ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ API │ │ Scheduler│ │ Controller │ │
│ │ Server │ │ │ │ Manager │ │
│ └──────────┘ └──────────┘ └─────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ etcd │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
↕ ↕
┌────────────────┐ ┌────────────────┐
│ Worker Node │ │ Worker Node │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │ kubelet │ │ │ │ kubelet │ │
│ │ kube- │ │ │ │ kube- │ │
│ │ proxy │ │ │ │ proxy │ │
│ ├──────────┤ │ │ ├──────────┤ │
│ │ Pod Pod │ │ │ │ Pod Pod │ │
│ └──────────┘ │ │ └──────────┘ │
└────────────────┘ └────────────────┘
구성 요소 역할
| 구성 요소 | 역할 | 비유 |
|---|---|---|
| API Server | 모든 요청의 진입점 | 회사의 접수 데스크 |
| etcd | 클러스터 상태 저장소 | 회사의 데이터베이스 |
| Scheduler | Pod를 어떤 노드에 배치할지 결정 | 인사 배치 담당자 |
| Controller Manager | 원하는 상태(desired state)를 유지 | 현장 관리자 |
| kubelet | 각 노드에서 Pod를 실행/관리 | 현장 작업자 |
| kube-proxy | 네트워크 규칙 관리, 서비스 라우팅 | 네트워크 관리자 |
Docker Compose vs Kubernetes
| Docker Compose | Kubernetes | |
|---|---|---|
| 범위 | 단일 서버 | 다중 서버 클러스터 |
| 자가 복구 | restart 정책만 | Pod 자동 재시작 + 자동 스케줄링 |
| 스케일링 | 수동 | kubectl scale 한 줄 |
| 무중단 배포 | 직접 구현 | Rolling Update 기본 제공 |
| 서비스 디스커버리 | Docker DNS | Service + DNS 자동 등록 |
2. Pod의 생명주기 관리
Pod란?
Kubernetes의 최소 배포 단위입니다. 하나 이상의 컨테이너를 묶어서 같은 네트워크/스토리지를 공유합니다.
# 가장 단순한 Pod 정의
apiVersion: v1
kind: Pod
metadata:
name: my-app
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:1.0
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Pod 생명주기
Pending → Running → Succeeded/Failed
↓
CrashLoopBackOff (반복 실패 시)
| 상태 | 의미 |
|---|---|
| Pending | 노드 배정 대기 또는 이미지 다운로드 중 |
| Running | 최소 1개 컨테이너 실행 중 |
| Succeeded | 모든 컨테이너 정상 종료 (Job 등) |
| Failed | 컨테이너가 비정상 종료 |
| CrashLoopBackOff | 반복 실패 → 재시작 간격 증가 |
Health Check (프로브)
spec:
containers:
- name: app
livenessProbe: # 살아있는지 확인 → 실패하면 재시작
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe: # 트래픽 받을 준비 됐는지 → 실패하면 Service에서 제외
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
startupProbe: # 시작 완료 확인 → 실패하면 liveness/readiness 시작 안 함
httpGet:
path: /actuator/health
port: 8080
failureThreshold: 30
periodSeconds: 10
실전 팁: Spring Boot 앱은 시작이 느리므로 startupProbe를 꼭 설정하세요. 없으면 livenessProbe가 앱 시작 전에 실패 판정 → 무한 재시작.
Multi-Container Pod 패턴
# Sidecar 패턴: 메인 앱 + 로그 수집기
spec:
containers:
- name: app
image: my-app:1.0
- name: log-collector # Sidecar
image: fluentd:latest
volumeMounts:
- name: log-volume
mountPath: /var/log/app
| 패턴 | 용도 | 예시 |
|---|---|---|
| Sidecar | 보조 기능 추가 | 로그 수집, 프록시 |
| Ambassador | 외부 통신 프록시 | DB 연결 풀 |
| Init Container | 초기화 작업 | DB 마이그레이션, 설정 다운로드 |
3. Deployment 사용 및 관리
Deployment란?
Pod의 선언적 관리자입니다. “이 앱을 3개 실행해라”라고 선언하면 Kubernetes가 알아서 유지합니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: asat-backend
namespace: asat
spec:
replicas: 2 # Pod 2개 유지
selector:
matchLabels:
app: asat-backend
strategy:
type: RollingUpdate # 무중단 배포 전략
rollingUpdate:
maxSurge: 1 # 최대 1개 추가 Pod 허용
maxUnavailable: 0 # 기존 Pod 0개 이하로 내려가지 않음
template:
metadata:
labels:
app: asat-backend
spec:
containers:
- name: backend
image: asat-backend:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
배포 전략
Rolling Update (기본값)
v1 v1 v1 ← 기존 3개
v1 v1 v1 v2 ← 새 Pod 1개 추가
v1 v1 v2 ← 기존 1개 제거
v1 v2 v2 ← 반복
v2 v2 v2 ← 완료
- 장점: 무중단, 점진적 전환
- 단점: 잠시 동안 v1과 v2가 공존
Recreate
v1 v1 v1 ← 전부 종료
(다운타임)
v2 v2 v2 ← 전부 시작
- 장점: 버전 혼재 없음
- 단점: 다운타임 발생
롤백
# 배포 히스토리 확인
kubectl rollout history deployment/asat-backend
# 이전 버전으로 롤백
kubectl rollout undo deployment/asat-backend
# 특정 버전으로 롤백
kubectl rollout undo deployment/asat-backend --to-revision=3
# 배포 상태 확인
kubectl rollout status deployment/asat-backend
HPA (Horizontal Pod Autoscaler)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: asat-backend-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: asat-backend
minReplicas: 2
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU 70% 넘으면 스케일 아웃
4. Service와 Ingress로 네트워킹 구성
Service란?
Pod는 생성/삭제될 때마다 IP가 바뀝니다. Service는 고정된 엔드포인트를 제공하여 Pod 집합에 접근합니다.
Service 유형
ClusterIP (기본값)
클러스터 내부에서만 접근 가능:
apiVersion: v1
kind: Service
metadata:
name: asat-backend-svc
spec:
type: ClusterIP
selector:
app: asat-backend # 이 라벨의 Pod로 트래픽 전달
ports:
- port: 80 # Service 포트
targetPort: 8080 # Pod 포트
다른 Pod → asat-backend-svc:80 → Pod(8080)
NodePort
모든 노드의 특정 포트로 외부 노출:
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # 모든 노드의 30080 포트로 접근
외부 → <노드IP>:30080 → Service → Pod
LoadBalancer
클라우드 환경에서 외부 로드밸런서 자동 생성:
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
Ingress
L7(HTTP) 레벨 라우팅. 도메인/경로 기반으로 여러 Service에 분배:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: asat-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: asat.lemuel.co.kr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: asat-frontend-svc
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: asat-backend-svc
port:
number: 80
asat.lemuel.co.kr/ → frontend Service → frontend Pod
asat.lemuel.co.kr/api → backend Service → backend Pod
Service vs Ingress 비교
| Service (NodePort) | Ingress | |
|---|---|---|
| 레이어 | L4 (TCP/UDP) | L7 (HTTP) |
| 도메인 라우팅 | 불가 | 가능 |
| SSL 종료 | 별도 구현 | 지원 |
| 경로 기반 분배 | 불가 | 가능 |
| 포트 | 노드당 1개 | 80/443 공유 |
5. 지속적인 스토리지 관리
문제: Pod는 일시적이다
Pod가 재시작되면 컨테이너 내부 파일이 전부 사라집니다. DB 데이터, 업로드 파일 등을 유지하려면 외부 스토리지가 필요합니다.
PV (Persistent Volume)
클러스터 관리자가 생성하는 스토리지 리소스:
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce # 하나의 노드에서만 읽기/쓰기
hostPath:
path: /data/postgres # 노드의 로컬 경로 (개발용)
persistentVolumeReclaimPolicy: Retain # PVC 삭제 후에도 데이터 보존
PVC (Persistent Volume Claim)
사용자(개발자)가 요청하는 스토리지 신청서:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi # 5GB 요청 → 10GB PV에 바인딩
Pod에서 사용
spec:
containers:
- name: postgres
image: postgres:17
volumeMounts:
- name: db-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: db-storage
persistentVolumeClaim:
claimName: postgres-pvc
Access Modes
| 모드 | 의미 | 약자 |
|---|---|---|
| ReadWriteOnce | 하나의 노드에서 읽기/쓰기 | RWO |
| ReadOnlyMany | 여러 노드에서 읽기 전용 | ROX |
| ReadWriteMany | 여러 노드에서 읽기/쓰기 | RWX |
StorageClass (동적 프로비저닝)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: rancher.io/local-path # K3s 기본 provisioner
reclaimPolicy: Retain
PVC가 생성되면 자동으로 PV를 생성합니다. 매번 수동으로 PV를 만들 필요가 없습니다.
6. 실전 사례: K3s로 ASAT 이중화 운영
환경
- 마스터: 르무엘 (i7-6500U, 32GB)
- 워커: 루이스 (i7-8565U, 16GB)
- 서비스: ASAT 청각 재활 훈련 시스템
K3s 설치
# 마스터 노드 (르무엘)
curl -sfL https://get.k3s.io | sh -
# 워커 노드 (루이스) — 토큰으로 마스터에 연결
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.219.101:6443 \
K3S_TOKEN=$(cat /var/lib/rancher/k3s/server/node-token) sh -
Deployment 구성
# backend 2개, frontend 2개 — 양쪽 노드에 분배
apiVersion: apps/v1
kind: Deployment
metadata:
name: asat-backend
namespace: asat
spec:
replicas: 2
template:
spec:
containers:
- name: backend
image: asat-backend:latest
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
affinity:
podAntiAffinity: # 같은 노드에 배치하지 않음 → 이중화
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: asat-backend
topologyKey: kubernetes.io/hostname
트래픽 흐름
인터넷 → Cloudflare Tunnel → nginx (르무엘 호스트)
→ upstream (Pod IP 직접 지정)
→ 르무엘 Pod (192.168.x.x:8080)
→ 루이스 Pod (192.168.y.y:8080)
NodePort 대신 nginx upstream + Pod IP 직접 지정 방식을 사용합니다. K3s kube-proxy의 iptables 라우팅이 홈서버 환경에서 불안정했기 때문입니다.
Pod IP 자동 갱신 스크립트
Pod가 재시작되면 IP가 바뀌므로 nginx upstream을 자동 갱신합니다:
#!/bin/bash
# /opt/scripts/update-k8s-nginx.sh (cron 1분마다)
NEW_IPS=$(kubectl get pods -n asat -l app=asat-backend \
-o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}')
# nginx upstream 업데이트
# ... IP 변경 감지 시 nginx reload
운영 현황
$ kubectl get pods -n asat -o wide
NAME READY NODE IP
asat-backend-xxx-abc 1/1 lemuel 10.42.0.15
asat-backend-xxx-def 1/1 louise 10.42.1.8
asat-frontend-xxx-ghi 1/1 lemuel 10.42.0.16
asat-frontend-xxx-jkl 1/1 louise 10.42.1.9
- Backend 2개: 르무엘 1, 루이스 1
- Frontend 2개: 르무엘 1, 루이스 1
- 한쪽 서버가 죽어도 나머지에서 서비스 유지
교훈
- K3s는 경량 Kubernetes — 홈서버에서도 충분히 운영 가능
- NodePort보다 nginx upstream — 홈서버 환경에서 더 안정적
- podAntiAffinity — 이중화의 핵심, 같은 노드에 배치 방지
- startupProbe 필수 — Spring Boot 앱은 시작이 느려서 livenessProbe가 먼저 실패할 수 있음
- 실무에서는 Ingress Controller — nginx Ingress Controller가 표준, 홈서버에서는 호스트 nginx로 대체