Kubernetes 패턴
프로덕션급 워크로드를 안정적으로 배포, 관리, 디버깅하기 위한 쿠버네티스(Kubernetes) 패턴입니다.
활성화 시점
- 쿠버네티스 매니페스트(Deployment, Service, Ingress, Jobs) 작성
- 리소스 요청/제한(request/limit), liveness/readiness 프로브 구성
- RBAC, 네임스페이스, ServiceAccount 설정
- K8s에서 구성(configuration)과 시크릿(secrets) 관리
- CrashLoopBackOff, OOMKilled, pending 상태의 파드, 이미지 풀(pull) 오류 디버깅
- HPA(Horizontal Pod Autoscaler) 또는 PodDisruptionBudget 구성
- 보안 또는 정확성을 위한 K8s YAML 리뷰
사용 시점
위의 활성화 시점과 동일합니다. 이 별칭은 저장소 스킬 형식 규칙을 충족합니다. 쿠버네티스 YAML과 워크로드를 작성하거나 리뷰하거나 디버깅할 때 이 스킬을 사용하세요.
작동 방식
이 스킬은 복사-붙여넣기가 가능한 프로덕션급 YAML 패턴과 kubectl 디버깅 명령어를 작업별로 정리하여 제공합니다:
- Deployment 템플릿 — 보안 컨텍스트, 롤링 업데이트 전략, 세 가지 프로브 유형, 리소스 제한, ConfigMap/Secret 환경 변수 주입이 포함된 완전히 구성된 프로덕션 Deployment입니다.
- 프로브 — startup vs liveness vs readiness 프로브에 대한 결정 테이블과 올바른
failureThreshold × periodSeconds계산이 포함됩니다. - 서비스 및 Ingress — ClusterIP, LoadBalancer, TLS Ingress 패턴 및 cert-manager 어노테이션입니다.
- ConfigMap 및 Secret —
envFrom, 파일 마운트, 외부 시크릿(External Secrets) 가이드입니다. - 리소스 관리 — 워크로드 유형(웹 API, JVM, worker, sidecar)별 요청 vs 제한(request vs limit) 경험법칙입니다.
- RBAC — 최소 권한 ServiceAccount → Role → RoleBinding 체인입니다.
- HPA 및 PDB — 오토스케일링 및 노드 드레인 안전 구성입니다.
- Jobs 및 CronJobs — 올바른
restartPolicy를 갖춘 일회성 및 예약 워크로드 패턴입니다. - kubectl 치트시트 — 로그, exec, 롤백, 포트-포워드, dry-run, 일반적인 오류 진단 명령어입니다.
- 안티-패턴 및 체크리스트 — 하지 말아야 할 것과 보안/안정성/관측 가능성 체크리스트입니다.
예제
아래 섹션에서 완전하고 실행 가능한 예제를 확인하세요. 빠른 참조:
| 작업 | 이동 링크 |
|---|---|
| 전체 프로덕션 Deployment YAML | 핵심 워크로드 패턴 |
| 프로브 구성 | 프로브 |
| 최소 권한 RBAC 설정 | RBAC |
| CrashLoopBackOff 디버깅 | kubectl 디버깅 치트시트 |
| 오토스케일링 | HPA |
핵심 워크로드 패턴
Deployment — 프로덕션 템플릿
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-namespace
labels:
app: my-app
version: "1.0.0"
spec:
replicas: 3
selector:
matchLabels:
app: my-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 업데이트 중 1개의 추가 파드 허용
maxUnavailable: 0 # 원하는 개수 미만으로 줄이지 않음
template:
metadata:
labels:
app: my-app
version: "1.0.0"
spec:
# 파드 수준 보안 컨텍스트
securityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 1001
# 정상 종료
terminationGracePeriodSeconds: 30
containers:
- name: my-app
image: ghcr.io/org/my-app:1.0.0 # 절대 :latest를 사용하지 마세요
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
protocol: TCP
# 리소스 요청과 제한 모두 필요합니다
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
# 컨테이너 보안 컨텍스트
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
# 프로브 (아래 프로브 섹션 참조)
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 30
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 0
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 2
# ConfigMap 및 Secret의 환경 변수
envFrom:
- configMapRef:
name: my-app-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: my-app-secrets
key: db-password
# readOnlyRootFilesystem: true 일 때 쓰기 가능한 tmp 디렉토리
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
프로브 — Liveness, Readiness, Startup
각 프로브를 사용해야 하는 시점을 이해하는 것이 중요합니다:
| 프로브 | 실패 시 동작 | 사용 목적 |
|---|---|---|
startupProbe | 느린 시작 시 컨테이너 종료 | 느리게 시작되는 앱(JVM, Python) |
livenessProbe | 컨테이너 재시작 | 데드락/중단된 프로세스 감지 |
readinessProbe | Service 엔드포인트에서 제거 | 일시적 사용 불가(DB 재연결) |
# 올바른 패턴: startupProbe가 느린 시작을 처리하고,
# 그 후 liveness/readiness가 역할을 이어받습니다
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 30 # 30 * 5s = 최대 150초 시작 시간
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 30
failureThreshold: 3 # 3 * 30s = 재시작 전 90초
readinessProbe:
httpGet:
path: /ready # 별도 엔드포인트: DB, 캐시 등 확인
port: 8080
periodSeconds: 10
failureThreshold: 2
# 잘못된 예: startupProbe 없이 initialDelaySeconds 사용
# 앱 시작에 60초가 걸리면 대신 startupProbe를 설정하세요
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 60 # 나쁨: 임의의 대기 시간, 경쟁 조건 발생
서비스와 Ingress
서비스 유형
# ClusterIP (기본값) — 내부 전용
apiVersion: v1
kind: Service
metadata:
name: my-app
namespace: my-namespace
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
protocol: TCP
type: ClusterIP
# LoadBalancer — 외부 트래픽 (클라우드 제공자)
spec:
type: LoadBalancer
ports:
- port: 443
targetPort: 8080
TLS가 포함된 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
namespace: my-namespace
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- myapp.example.com
secretName: my-app-tls
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
ConfigMap과 Secret
ConfigMap — 민감하지 않은 설정
apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config
namespace: my-namespace
data:
LOG_LEVEL: "info"
APP_ENV: "production"
MAX_CONNECTIONS: "100"
# 복잡한 설정은 파일로 마운트
app.yaml: |
server:
port: 8080
timeout: 30s
# ConfigMap을 파일로 마운트
volumes:
- name: config
configMap:
name: my-app-config
items:
- key: app.yaml
path: app.yaml
volumeMounts:
- name: config
mountPath: /etc/app
readOnly: true
Secret — 민감한 데이터
# 리터럴로 시크릿 생성 (CLI에서 실행 후 Vault/SOPS에 저장)
kubectl create secret generic my-app-secrets \
--from-literal=db-password='s3cr3t' \
--namespace=my-namespace \
--dry-run=client -o yaml | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: my-app-secrets
namespace: my-namespace
type: Opaque
# 값은 base64로 인코딩됨 (암호화되지 않음 — 실제 암호화는 Sealed Secrets 또는 ESO 사용)
data:
db-password: czNjcjN0 # 's3cr3t'의 base64
중요: 원시 Kubernetes Secret은 base64로만 인코딩되며, 클러스터에 암호화가 구성되지 않은 경우 저장 시 암호화되지 않습니다. 프로덕션 환경에서는 Sealed Secrets 또는 External Secrets Operator를 사용하세요.
리소스 요청과 제한
resources:
requests: # 스케줄러가 파드를 배치하는 데 사용
cpu: "100m" # 100 millicores = 0.1 CPU
memory: "128Mi"
limits: # 이 값을 초과하면 컨테이너 종료/제한됨
cpu: "500m"
memory: "256Mi"
경험법칙:
| 워크로드 유형 | CPU 요청 | 메모리 요청 | 참고 |
|---|---|---|---|
| 웹 API | 100–250m | 128–256Mi | 제한을 요청의 2-4배로 설정 |
| Worker/consumer | 250–500m | 256–512Mi | 예측 가능성을 위해 메모리 제한 = 요청 |
| JVM 앱 | 500m–1 | 512Mi–2Gi | JVM 오버헤드를 위해 -Xmx 위에 여유 공간 확보 |
| Sidecar | 10–50m | 32–64Mi | 최소한으로 유지 |
# 잘못된 예: 요청이나 제한이 없음 — 예측 불가능한 스케줄링, OOM 제거 발생
containers:
- name: app
image: myapp:latest
# resources: {} 누락 — 프로덕션에서 위험합니다
# 잘못된 예: 제한만 있고 요청 없음 — 요청이 제한값으로 기본 설정되어 용량 초과 예약
resources:
limits:
cpu: "2"
memory: "1Gi"
# requests 누락 — limits 값으로 기본 설정됩니다
RBAC — 역할과 ServiceAccount
최소 권한 원칙
앱이 Kubernetes API를 호출하는지 여부에 따라 두 가지 패턴:
패턴 A — 앱이 Kubernetes API를 필요로 하지 않음 (대부분의 앱)
ServiceAccount에서 토큰 자동 마운트를 비활성화합니다. Role/RoleBinding이 필요하지 않습니다.
# 토큰이 비활성화된 ServiceAccount — 가장 안전한 기본값
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-sa
namespace: my-namespace
automountServiceAccountToken: false # K8s API 토큰이 파드에 주입되지 않음
# Deployment에서 참조 — 토큰 없음, API 액세스 없음
spec:
template:
spec:
serviceAccountName: my-app-sa
automountServiceAccountToken: false # 이중 안전장치: 파드 수준에서도 설정
패턴 B — 앱이 Kubernetes API를 필요로 함 (operator, controller, config watcher)
토큰을 활성화하고 실제로 필요한 권한만 부여합니다.
# 1. ServiceAccount — 이 SA에 대해 토큰 활성화
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-sa
namespace: my-namespace
automountServiceAccountToken: true # 토큰 필요: 앱이 K8s API 호출
# 2. Role — 앱이 필요한 것만 부여 (네임스페이스 범위)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: my-app-role
namespace: my-namespace
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"] # 읽기 전용, 특정 리소스
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["my-app-secrets"] # 이름으로 특정 시크릿으로 제한
verbs: ["get"]
# 3. Role을 ServiceAccount에 바인딩
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: my-app-rolebinding
namespace: my-namespace
subjects:
- kind: ServiceAccount
name: my-app-sa
namespace: my-namespace
roleRef:
kind: Role
apiGroup: rbac.authorization.k8s.io
name: my-app-role
# 4. Deployment에서 SA 참조
spec:
template:
spec:
serviceAccountName: my-app-sa
# automountServiceAccountToken은 SA에서 기본적으로 true — 토큰이 주입됨
Horizontal Pod Autoscaler (HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
namespace: my-namespace
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2 # 고가용성을 위해 항상 최소 2개
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 평균 CPU > 70%일 때 스케일 업
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
HPA는 모든 컨테이너에
resources.requests가 설정되어 있어야 합니다 — 사용률을current / request로 계산합니다.
PodDisruptionBudget (PDB)
노드 드레인 또는 롤링 업데이트 중 너무 많은 파드가 종료되는 것을 방지:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-pdb
namespace: my-namespace
spec:
minAvailable: 2 # 또는 maxUnavailable: 1 사용
selector:
matchLabels:
app: my-app
네임스페이스와 멀티 테넌시
# 리소스 할당량(ResourceQuota)이 있는 네임스페이스 생성
kubectl create namespace my-namespace
# 네임스페이스 소비를 제한하는 ResourceQuota 적용
kubectl apply -f - <<EOF
apiVersion: v1
kind: ResourceQuota
metadata:
name: my-namespace-quota
namespace: my-namespace
spec:
hard:
requests.cpu: "4"
requests.memory: 4Gi
limits.cpu: "8"
limits.memory: 8Gi
pods: "20"
EOF
Jobs와 CronJobs
# 일회성 Job (DB 마이그레이션, 데이터 처리)
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
namespace: my-namespace
spec:
backoffLimit: 3 # 실패 시 최대 3회 재시도
ttlSecondsAfterFinished: 3600 # 1시간 후 자동 삭제
template:
spec:
restartPolicy: OnFailure # Job의 경우 Never (Always 사용 안 함)
containers:
- name: migrate
image: ghcr.io/org/my-app:1.0.0
command: ["python", "manage.py", "migrate"]
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
# CronJob — 예약된 작업
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-cleanup
namespace: my-namespace
spec:
schedule: "0 2 * * *" # 매일 02:00 UTC
concurrencyPolicy: Forbid # 중복 실행 방지
startingDeadlineSeconds: 300 # 시작 기한
successfulJobsHistoryLimit: 3 # 성공 기록 제한
failedJobsHistoryLimit: 1 # 실패 기록 제한
jobTemplate:
spec:
backoffLimit: 2
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: OnFailure
containers:
- name: cleanup
image: ghcr.io/org/cleanup:1.0.0
command: ["cleanup", "--deep"]
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "256Mi"
kubectl 디버깅 치트시트
# ----- 파드 상태 확인 -----
kubectl get pods -n <namespace> # 모든 파드와 상태
kubectl describe pod <pod-name> -n <namespace> # 상세 이벤트
# ----- 로그 보기 -----
kubectl logs <pod-name> -n <namespace> # 최신 로그
kubectl logs <pod-name> -n <namespace> --tail=100 # 마지막 100줄
kubectl logs -f <pod-name> -n <namespace> # 로그 스트리밍
kubectl logs <pod-name> -c <container-name> -n <namespace> # 특정 사이드카 로그
# ----- 파드에서 명령어 실행 -----
kubectl exec -it <pod-name> -n <namespace> -- /bin/sh
kubectl exec -it <pod-name> -n <namespace> -- curl localhost:8080/health
# ----- CrashLoopBackOff 진단 -----
kubectl logs --previous <pod-name> -n <namespace> # 충돌 이전 로그 보기
kubectl get events -n <namespace> --sort-by='.lastTimestamp' # 최근 이벤트
# ----- OOMKilled 진단 -----
kubectl describe pod <pod-name> -n <namespace> | grep -A2 'Last State'
kubectl top pod <pod-name> -n <namespace> # 파드 리소스 사용량
# ----- ImagePullBackOff 진단 -----
kubectl describe pod <pod-name> -n <namespace> | grep -A10 'Events' # 이미지 풀 상세
# 원인: 잘못된 이미지 태그, 프라이빗 레지스트리 자격 증명 없음, 레지스트리 속도 제한
# ----- Pending 파드 진단 -----
kubectl describe pod <pod-name> -n <namespace> | grep -A5 'Conditions' # 스케줄링 문제 확인
kubectl describe node # 노드 리소스 가용성
# ----- 롤백 -----
kubectl rollout undo deployment/<name> -n <namespace> # 이전 리비전으로 롤백
kubectl rollout status deployment/<name> -n <namespace> # 롤아웃 상태 확인
kubectl rollout history deployment/<name> -n <namespace> # 리비전 히스토리 보기
# ----- 네트워크 진단 -----
kubectl port-forward pod/<name> 8080:8080 -n <namespace> # 로컬에서 파드로 포트 포워드
kubectl run tmp-shell --rm -it --image=nicolaka/netshoot /bin/bash # 임시 네트워크 진단 파드
일반적인 안티-패턴
# 잘못: 최신 태그 — 롤백 불가, 예측 불가능
image: my-app:latest
# 올바름: 고정된 버전 태그
image: ghcr.io/org/my-app:v1.0.0
# 잘못: startupProbe 없이 initialDelaySeconds — 느린 시작에서 경쟁 조건 발생
livenessProbe:
initialDelaySeconds: 60
# 올바름: startupProbe를 사용하여 느린 시작 처리
# 잘못: 제한 없이 요청만 설정 — OOMKilled 위험 (파드가 제한을 초과할 수 있음)
resources:
requests:
memory: "256Mi"
# 올바름: 요청과 제한 모두 설정
# 잘못: 제한만 있고 요청 없음 — 요청이 제한과 같게 설정되어 용량 낭비
resources:
limits:
memory: "1Gi"
# 올바름: 실제 필요에 따라 요청 설정
보안 및 안정성 체크리스트
보안
-
runAsNonRoot: true로 비루트로 실행 -
allowPrivilegeEscalation: false설정 -
readOnlyRootFilesystem: true사용 가능하다면 - 불필요한 capabilities 제거 (
drop: ["ALL"]) - 이미지 태그 고정, 최신 태그 사용 금지
- 앱이 K8s API를 호출하지 않으면
automountServiceAccountToken: false - K8s API가 필요한 경우 최소 권한 Role 사용
- 민감한 데이터에 raw Secret 대신 Sealed Secrets/External Secrets Operator 사용
안정성
- 리소스 요청과 제한이 모든 컨테이너에 설정되었는지 확인
- startupProbe로 느린 시작 처리
- readinessProbe에 DB/캐시 연결 확인을 위한 별도 엔드포인트 사용
- HPA에
minReplicas: 2이상 설정 - PodDisruptionBudget(PDB) 구성
- 각 노드의 파드 수를 초과하지 않도록 PodAntiAffinity 설정 고려
관측 가능성
- readinessProbe가 트래픽을 올바르게 라우팅하는지 확인
- 애플리케이션에 구조화된 로깅(JSON) 설정
- 메트릭 수집 및 알림 구성
- 클러스터 이벤트 모니터링