2025년 11월 3일
Job/CronJob, Network Policy, Resource Quotas, CRD, Helm Chart, etcd Backup, Cluster Upgrade까지!
Day 4에서 Ingress, HPA, RBAC, StatefulSet, DaemonSet 등 고급 패턴을 마스터했습니다. 이제 Day 5에서는 Production 환경에서 클러스터를 안전하고 효율적으로 운영하는 방법을 학습했습니다.
오늘 배운 것:
1. Job & CronJob으로 배치 작업 자동화
2. Network Policy로 Zero Trust 네트워크 구현
3. Resource Quotas로 팀별 리소스 관리
4. CRD와 Operator Pattern 이해
5. Helm Chart로 애플리케이션 패키징 (Terraform + Terragrunt 패턴!)
6. etcd 백업 주기와 실무 전략
7. Cluster 업그레이드 절차와 Best Practices
🤔 내가 이해한 것:
apiVersion: batch/v1
kind: Job
metadata:
name: pi-calculation
spec:
template:
spec:
containers:
- name: pi
image: perl:5.34
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
backoffLimit: 4
실행 결과:
$ kubectl apply -f job-pi.yaml
job.batch/pi-calculation created
$ kubectl get jobs
NAME COMPLETIONS DURATION AGE
pi-calculation 1/1 8s 45s
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-calculation-abc123 0/1 Completed 0 50s
$ kubectl logs pi-calculation-abc123 | head -3
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117067982148086513282306647093844609550582231725359408128481117450284102701938521105559644622948954930381964428810975665933446128475648233786783165271201909145648566923460348610454326648213393607260249141273724587006606315588174881520920962829254091715364367892590360011330530548820466521384146951941511609433057270365759591953092186117381932611793105118548074462379962749567351885752724891227938183011949129833673362440656643086021394946395224737190702179860943702770539217176293176752384674818467669405132000568127145263560827785771342757789609173637178721468440901224953430146549585371050792279689258923542019956112129021960864034418159813629774771309960518707211349999998372978049951059731732816096318595024459455346908302642522308253344685035261931188171010003137838752886587533208381420617177669147303598253490428755468731159562863882353787593751957781857780532171226806613001927876611195909216420199
2000자리 π 값 출력 성공!
apiVersion: batch/v1
kind: Job
metadata:
name: parallel-job
spec:
completions: 10 # 총 10번 성공해야 함
parallelism: 3 # 동시에 3개씩 실행
template:
spec:
containers:
- name: worker
image: busybox:1.28
command: ["/bin/sh", "-c", "echo 'Processing task' && sleep 5 && echo 'Task completed'"]
restartPolicy: Never
실시간 관찰 (2초마다):
# t=0초 - 3개 동시 시작!
$ kubectl get job parallel-job && kubectl get pods -l job-name=parallel-job
NAME COMPLETIONS DURATION AGE
parallel-job 0/10 3s 3s
NAME READY STATUS RESTARTS AGE
parallel-job-abc12 1/1 Running 0 3s
parallel-job-def34 1/1 Running 0 3s
parallel-job-ghi56 1/1 Running 0 3s
# t=8초 - 첫 3개 완료, 다음 3개 시작!
$ kubectl get job parallel-job && kubectl get pods -l job-name=parallel-job
NAME COMPLETIONS DURATION AGE
parallel-job 3/10 11s 11s
NAME READY STATUS RESTARTS AGE
parallel-job-abc12 0/1 Completed 0 11s
parallel-job-def34 0/1 Completed 0 11s
parallel-job-ghi56 0/1 Completed 0 11s
parallel-job-jkl78 1/1 Running 0 3s
parallel-job-mno90 1/1 Running 0 3s
parallel-job-pqr12 1/1 Running 0 3s
# t=38초 - 모두 완료!
$ kubectl get job parallel-job
NAME COMPLETIONS DURATION AGE
parallel-job 10/10 38s 38s
결과: 10개 작업을 3개씩 동시 실행하여 38초 만에 완료! (순차 실행 시 50초 소요)
apiVersion: batch/v1
kind: CronJob
metadata:
name: backup-job
spec:
schedule: "*/1 * * * *" # 매 1분마다 (테스트용)
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: busybox:1.28
command:
- /bin/sh
- -c
- |
echo "[$(date)] Starting backup..."
echo "Backing up data..."
sleep 3
echo "[$(date)] Backup completed!"
restartPolicy: OnFailure
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
3분 후 확인:
$ kubectl get cronjob
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
backup-job */1 * * * * False 0 45s 3m
$ kubectl get jobs -l job-name=backup-job
NAME COMPLETIONS DURATION AGE
backup-job-29369409 1/1 5s 3m
backup-job-29369410 1/1 5s 2m
backup-job-29369411 1/1 5s 1m
$ kubectl logs backup-job-29369411-abc12
[Wed Nov 3 05:42:00 UTC 2025] Starting backup...
Backing up data...
[Wed Nov 3 05:42:03 UTC 2025] Backup completed!
자동으로 매 1분마다 Job 생성 및 실행!
답변: 아니요, 일반적으로 20-30%의 중요 리소스에만 적용합니다:
답변: 네, 반드시 Zero Trust를 적용해야 합니다!
시나리오:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: database-policy
namespace: production
spec:
podSelector:
matchLabels:
app: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 5432
테스트 결과:
# Test Pod에서 접근 시도 (차단되어야 함)
$ kubectl exec -n production test-pod -- nc -zv database 5432
nc: database (10.244.5.225): Operation timed out ❌ 차단 성공!
# Backend Pod에서 접근 시도 (허용되어야 함)
$ kubectl exec -n production backend -- nc -zv database 5432
database (10.244.5.225:5432) open ✅ 허용 성공!
Network Policy가 정확히 작동!
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "10"
services: "5"
apiVersion: v1
kind: LimitRange
metadata:
name: dev-limits
namespace: dev
spec:
limits:
- max:
cpu: "2"
memory: 4Gi
min:
cpu: 100m
memory: 128Mi
default:
cpu: 500m # 기본 limit
memory: 1Gi
defaultRequest:
cpu: 200m # 기본 request
memory: 512Mi
type: Container
1. LimitRange 위반 테스트:
$ kubectl run test-large -n dev --image=nginx \
--limits=cpu=6 # max는 2 CPU인데 6 요청
Error from server (Forbidden): pods "test-large" is forbidden:
maximum cpu usage per Container is 2, but limit is 6
✅ LimitRange가 먼저 차단!
2. ResourceQuota 위반 테스트:
# 이미 dev namespace에 CPU request 2.2 core 사용 중
$ kubectl run test2 -n dev --image=nginx \
--requests=cpu=2 # 총 4.2 core가 되어 quota(4) 초과
Error from server (Forbidden): pods "test2" is forbidden:
exceeded quota: dev-quota, requested: requests.cpu=2,
used: requests.cpu=2200m, limited: requests.cpu=4
✅ ResourceQuota가 차단!
$ kubectl run test-default -n dev --image=nginx
$ kubectl get pod test-default -n dev -o yaml | grep -A 10 resources:
resources:
limits:
cpu: 500m # ← LimitRange의 default 자동 적용!
memory: 1Gi
requests:
cpu: 200m # ← defaultRequest 자동 적용!
memory: 512Mi
리소스를 명시하지 않아도 자동으로 설정됨!
이 질문이 가장 중요했습니다!
답변: CRD 단독으로는 아무것도 하지 않습니다. CRD는 단지 데이터 구조 정의일 뿐입니다.
진짜 힘은 Operator Pattern:
CRD (데이터 구조) + Operator (Controller) = 자동화!
apiVersion: apps/v1
kind: Deployment # ← 이것도 CRD입니다! (Built-in)
metadata:
name: nginx
spec:
replicas: 3
Deployment를 생성하면:
1. etcd에 Deployment 정보 저장 (CRD 역할)
2. Deployment Controller가 감지 (Operator 역할)
3. ReplicaSet 자동 생성
4. ReplicaSet Controller가 감지
5. Pod 3개 자동 생성
6. Pod가 죽으면 자동으로 재생성! ← 이게 Operator의 힘!
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.mycompany.com
spec:
group: mycompany.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
engine:
type: string
enum: ["postgres", "mysql", "mongodb"]
version:
type: string
storage:
type: string
required:
- engine
- version
scope: Namespaced
names:
plural: databases
singular: database
kind: Database
shortNames:
- db
Database 생성:
apiVersion: mycompany.com/v1
kind: Database
metadata:
name: production-db
spec:
engine: postgres
version: "15"
storage: 100Gi
$ kubectl apply -f database.yaml
database.mycompany.com/production-db created
$ kubectl get databases
NAME AGE
production-db 10s
dev-db 5s
$ kubectl get db # shortName 동작!
NAME AGE
production-db 15s
dev-db 10s
스키마 검증 테스트 (enum 위반):
$ kubectl apply -f database-oracle.yaml # engine: oracle
Error: Unsupported value: "oracle": supported values: "postgres", "mysql", "mongodb"
✅ OpenAPI 스키마 검증 작동!
완벽한 이해입니다!
| Terraform | Helm |
|---|---|
| Terraform 모듈 | Helm Chart |
| tfvars | values.yaml |
| terragrunt.hcl | values-dev.yaml, values-prod.yaml |
| terraform apply | helm install |
| terraform plan | helm template |
$ helm create my-webapp
Creating my-webapp
$ tree my-webapp/
my-webapp/
├── Chart.yaml
├── values.yaml
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ └── _helpers.tpl
└── charts/
values.yaml 수정:
replicaCount: 3
image:
repository: nginx
tag: "1.21"
설치:
$ helm install myapp ./my-webapp
$ kubectl get pods -l app.kubernetes.io/name=my-webapp
NAME READY STATUS RESTARTS AGE
my-webapp-6c8b4d9f7b-abc12 1/1 Running 0 30s
my-webapp-6c8b4d9f7b-def34 1/1 Running 0 30s
my-webapp-6c8b4d9f7b-ghi56 1/1 Running 0 30s
업그레이드 (replicas 변경):
$ helm upgrade myapp ./my-webapp --set replicaCount=5
$ kubectl get pods -l app.kubernetes.io/name=my-webapp
NAME READY STATUS RESTARTS AGE
my-webapp-6c8b4d9f7b-abc12 1/1 Running 0 2m
my-webapp-6c8b4d9f7b-def34 1/1 Running 0 2m
my-webapp-6c8b4d9f7b-ghi56 1/1 Running 0 2m
my-webapp-6c8b4d9f7b-jkl78 1/1 Running 0 5s
my-webapp-6c8b4d9f7b-mno90 1/1 Running 0 5s
롤백:
$ helm rollback myapp 1
$ kubectl get pods -l app.kubernetes.io/name=my-webapp
NAME READY STATUS RESTARTS AGE
my-webapp-6c8b4d9f7b-abc12 1/1 Running 0 3m
my-webapp-6c8b4d9f7b-def34 1/1 Running 0 3m
my-webapp-6c8b4d9f7b-ghi56 1/1 Running 0 3m
✅ 다시 3개로 롤백!
values-dev.yaml (개발 환경):
architecture: standalone # 단일 인스턴스
auth:
postgresPassword: "dev-password-123"
username: "myapp"
password: "myapp-dev-123"
database: "myapp_dev"
primary:
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
persistence:
enabled: true
storageClass: "local-path"
size: 5Gi
backup:
enabled: false
metrics:
enabled: false
values-prod.yaml (프로덕션 환경):
architecture: replication # HA 구성
auth:
existingSecret: "postgres-prod-secret"
primary:
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
persistence:
enabled: true
storageClass: "fast-ssd"
size: 100Gi
podAntiAffinityPreset: hard
readReplicas:
replicaCount: 2
resources:
requests:
memory: "2Gi"
cpu: "1000m"
backup:
enabled: true
cronjob:
schedule: "0 2 * * *"
metrics:
enabled: true
serviceMonitor:
enabled: true
pgpool:
enabled: true
numInitChildren: 32
maxPool: 4
배포:
# 개발 환경
helm install postgres-dev bitnami/postgresql -f values-dev.yaml -n dev
# 프로덕션 환경
helm install postgres-prod bitnami/postgresql -f values-prod.yaml -n prod
결과:
완전히 Terraform + Terragrunt 패턴!
| 환경 | 자동 백업 주기 | RTO | RPO | 보관 정책 |
|---|---|---|---|---|
| 대기업/금융 | 매 1시간 | 1시간 | 1시간 | 7년 (규정 준수) |
| 중견기업 | 매 6시간 | 4시간 | 6시간 | 3개월 |
| 스타트업 | 매일 1회 | 12시간 | 24시간 | 1개월 |
가장 일반적인 패턴 (중견기업 표준):
✅ 매 6시간: 자동 백업 (S3 Standard-IA) - 48시간 보관
✅ 매일 새벽 2시: 전체 백업 (S3 Glacier) - 7일 보관
✅ 매주 일요일: 주간 백업 - 4주 보관
✅ 매월 1일: 월간 백업 - 12개월 보관
✅ 배포 직전: 수동 백업 필수!
$ kubectl exec -n kube-system etcd-cpu1 -- sh -c "ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /var/lib/etcd/backup.db"
Snapshot saved at /var/lib/etcd/backup.db
$ kubectl exec -n kube-system etcd-cpu1 -- sh -c "ETCDCTL_API=3 etcdctl \
--write-out=table \
snapshot status /var/lib/etcd/backup.db"
+---------+----------+------------+------------+
| HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
+---------+----------+------------+------------+
| ad8760b | 1030374 | 1760 | 7.6 MB |
+---------+----------+------------+------------+
백업 성공! 1760개 키, 7.6MB
etcd 손실 = 클러스터 전체 손실!
etcd에 저장되는 데이터:
$ kubectl version --short
Client Version: v1.31.13
Kustomize Version: v5.4.2
Server Version: v1.31.13
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION
cpu1 Ready control-plane 6d21h v1.31.13
cpu2 Ready <none> 5d16h v1.31.13
gpu1 Ready <none> 5d16h v1.31.13
$ sudo kubeadm upgrade plan
[upgrade/versions] Cluster version: 1.31.13
[upgrade/versions] kubeadm version: v1.31.13
[upgrade/versions] Target version: v1.31.13
[upgrade/versions] Latest version in the v1.31 series: v1.31.13
이미 최신 버전이라 실제 업그레이드는 불가능! 대신 시뮬레이션과 이론 학습을 진행했습니다.
한 번에 한 마이너 버전씩
✅ 1.30 → 1.31 → 1.32 (순차)
❌ 1.30 → 1.32 (건너뛰기 불가)
업그레이드 순서
1) etcd 백업 (필수!)
2) Control Plane 업그레이드 (kubeadm)
3) Control Plane kubelet 업그레이드
4) Worker Node 순차 업그레이드 (Rolling)
다운타임
$ ./upgrade-simulation.sh
======================================
Kubernetes Cluster Upgrade Simulation
======================================
[Step 1/10] 업그레이드 전 체크리스트
현재 클러스터 버전 확인:
Client Version: v1.31.13
Kustomize Version: v5.4.2
모든 노드 상태 확인:
NAME STATUS ROLES AGE VERSION
cpu1 Ready control-plane 6d21h v1.31.13
cpu2 Ready <none> 5d16h v1.31.13
gpu1 Ready <none> 5d16h v1.31.13
✅ Step 1 완료
[Step 2/10] etcd 백업
kubectl exec -n kube-system etcd-cpu1 -- sh -c "ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /var/lib/etcd/pre-upgrade-backup.db"
✅ Step 2 완료 (시뮬레이션)
[Step 3/10] 모든 리소스 백업
명령어: kubectl get all -A -o yaml > /tmp/all-resources-backup.yaml
백업 완료: /tmp/all-resources-backup.yaml (396K)
✅ Step 3 완료
[Step 4/10] API Deprecation 확인
Deprecated API 사용: 3개
⚠️ 업그레이드 전 Deprecated API 수정 필요!
[Step 5/10] Control Plane 업그레이드 (시뮬레이션)
[Step 6/10] Control Plane kubelet 업그레이드 (시뮬레이션)
[Step 7/10] Worker Node 1 (cpu2) 업그레이드 (시뮬레이션)
[Step 8/10] Worker Node 2 (gpu1) 업그레이드 (시뮬레이션)
[Step 9/10] 업그레이드 검증
노드 버전 확인:
NAME STATUS ROLES AGE VERSION
cpu1 Ready control-plane 6d21h v1.31.13
cpu2 Ready <none> 5d16h v1.31.13
gpu1 Ready <none> 5d16h v1.31.13
[Step 10/10] 최종 확인
테스트 워크로드 배포:
pod/upgrade-test created
NAME READY STATUS RESTARTS AGE NODE
upgrade-test 1/1 Running 0 3s gpu1
✅ Step 10 완료
======================================
업그레이드 시뮬레이션 완료!
======================================
Kubernetes는 매 버전마다 API를 Deprecate 시킵니다.
주요 Deprecation 히스토리:
확인 방법:
$ kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis
apiserver_requested_deprecated_apis{group="",removed_release="",resource="componentstatuses",subresource="",version="v1"} 1
Blue-Green Cluster (대기업):
클러스터 2개 운영 → 트래픽 점진 전환 → 다운타임 Zero
장점: 빠른 롤백, 안전
단점: 2배 비용
Rolling Upgrade (중소기업):
노드 순차 업그레이드 → 1-2분 다운타임
장점: 추가 비용 없음
단점: Control Plane 업그레이드 시 짧은 중단
| 환경 | 주기 | 이유 |
|---|---|---|
| 프로덕션 | 6개월 | 안정성 우선 (최소 2개 패치 버전 대기) |
| 스테이징 | 3개월 | 프로덕션 사전 검증 |
| 개발 | 즉시 | 최신 기능 테스트 |
Kubernetes 버전 지원 정책:
Kubernetes는 최근 3개 마이너 버전만 지원
현재: v1.32 (최신)
v1.32: 지원 ✅
v1.31: 지원 ✅
v1.30: 지원 ✅
v1.29: 지원 종료 ❌ (보안 패치 없음)
결론: 최소 1년에 1-2회 업그레이드 필수!
문제: nginx 이미지에 nc (netcat) 명령어 없음
error: exec: "nc": executable file not found in $PATH
해결: busybox:1.28 이미지로 test Pod 생성
kubectl run test-pod -n production --image=busybox:1.28 \
--labels="app=test" --command -- sleep 3600
내 착각: ResourceQuota가 먼저 체크할 줄 알았음
실제: LimitRange → ResourceQuota 순서
문제: "CRD가 왜 필요한지 모르겠다"
해결: Operator Pattern 이해로 해결
Day 5에서 Production-Ready 운영을 마스터했습니다! Day 6에서는 Monitoring & Logging으로 클러스터 가시성을 확보할 예정입니다:
Production 클러스터의 가시성을 완벽하게 확보합시다!
노드 구성:
버전: