Step 25 — 정답과 해설 (solutions)

정답 매니페스트는 manifests/ 에 그대로 있습니다(전문은 강의 페이지의 실습 파일 섹션에서 볼 수 있습니다). 이 문서는 왜 그렇게 썼는지, 그리고 실습에서 자주 막히는 지점을 해설합니다. 먼저 problems.md 를 스스로 풀어본 뒤 읽으세요.

한 번에 적용하기

kubectl apply -f manifests/00-namespace.yaml
kubectl apply -f manifests/01-config.yaml
kubectl apply -f manifests/10-datastore.yaml
kubectl apply -f manifests/20-backend.yaml
kubectl apply -f manifests/30-frontend.yaml
kubectl apply -f manifests/40-hpa.yaml     # metrics-server 있을 때
kubectl apply -f manifests/41-ingress.yaml # ingress-nginx 있을 때

M2. ConfigMap / Secret — manifests/01-config.yaml

핵심은 민감/비민감 분리입니다. DB 이름·배너·백엔드 URL 은 ConfigMap, 비밀번호만 Secret.

apiVersion: v1
kind: Secret
metadata: { name: db-secret, namespace: step25 }
type: Opaque
stringData:
  POSTGRES_PASSWORD: "s3cr3t-p@ss"   # stringData: 평문으로 쓰면 apiserver 가 base64 로 저장
  • stringData vs data: data 는 직접 base64 를 넣어야 하고, stringData 는 평문을 쓰면 저장 시 인코딩됩니다. 실습·가독성은 stringData 가 편합니다.
  • 주입은 valueFrom.configMapKeyRef / valueFrom.secretKeyRef 로 키 단위. 통째로 주입하려면 envFrom 도 가능하지만, 키 단위가 명시적이라 안전합니다.

M3. datastore — manifests/10-datastore.yaml

kind: Service           # 헤드리스: clusterIP: None → postgres-0.postgres DNS
spec: { clusterIP: None, selector: {app: shop, tier: datastore}, ports: [{port: 5432}] }
---
kind: StatefulSet
spec:
  serviceName: postgres            # 헤드리스 서비스 이름과 일치해야 함
  volumeClaimTemplates:            # 파드마다 PVC 자동 생성 → 영속
    - metadata: { name: data }
      spec: { accessModes: [ReadWriteOnce], resources: { requests: { storage: 1Gi } } }

자주 막히는 곳:

  • PGDATA 하위 디렉터리: PV 를 /var/lib/postgresql/data 에 마운트하면 루트에 lost+found 가 생겨 initdb 가 "directory not empty" 로 실패할 수 있습니다. PGDATA=/var/lib/postgresql/data/pgdata 로 피합니다.
  • serviceName 불일치: StatefulSet 의 serviceName 은 헤드리스 Service 이름과 같아야 안정 DNS 가 생깁니다.
  • 프로브는 exec: postgres 는 HTTP 가 없으므로 pg_isready -U $POSTGRES_USER -d $POSTGRES_DB 를 exec 프로브로.

M4. backend — manifests/20-backend.yaml

containers:
- name: netexec
  image: registry.k8s.io/e2e-test-images/agnhost:2.47
  args: ["netexec", "--http-port=8080"]
  readinessProbe: { httpGet: { path: /healthz, port: 8080 } }
  resources: { requests: { cpu: 25m, memory: 32Mi }, limits: { cpu: 200m, memory: 128Mi } }
  • requests.cpu 필수: HPA 는 요청량 대비 사용률을 계산하므로 requests 가 없으면 CPU HPA 가 동작하지 않습니다.
  • maxUnavailable: 0: 무중단 롤링을 위해 백엔드도 동일하게 설정.

M5. frontend — manifests/30-frontend.yaml ← 가장 많이 실패하는 지점

문제에서 경고한 대로, 순진하게 짜면 CrashLoopBackOff 에 빠집니다. 실제로 두 번 실패했습니다.

실패 1 — liveness 조기 발동

  • livenessProbe.initialDelaySeconds: 10 인데 hello-kubernetes 는 기동에 ~13초. liveness 가 먼저 실패해 컨테이너를 죽임 → 재시작 반복.
  • 진단: describe Events 에 Liveness probe failed: ... connection refused + Killing.

실패 2 — OOMKilled

  • limits.memory: 128Mi 로는 Node.js 런타임이 부족 → lastState.terminated.reason: OOMKilled, exitCode: 137.
  • 진단: kubectl get pod <p> -o jsonpath='{.status.containerStatuses[0].lastState}'.

정답 — startupProbe + 넉넉한 메모리

startupProbe:                    # 느린 기동을 감싼다 (period 3s × failure 20 = 최대 60초)
  httpGet: { path: /, port: 8080 }
  periodSeconds: 3
  failureThreshold: 20
livenessProbe:  { httpGet: { path: /, port: 8080 }, periodSeconds: 10 }  # startup 성공 후에만 작동
readinessProbe: { httpGet: { path: /, port: 8080 }, periodSeconds: 5 }
resources:
  requests: { cpu: 25m, memory: 128Mi }
  limits:   { cpu: 200m, memory: 256Mi }   # 128Mi 는 OOM — 256Mi 로

교훈: 느리게 뜨는 앱에는 startupProbe, 런타임에 맞는 메모리 limit. Step 08·Step 09 의 핵심이 여기 모입니다.

M8. 영속성 — 채점 방법

kubectl exec -n step25 postgres-0 -- psql -U appuser -d shopdb -c "CREATE TABLE IF NOT EXISTS orders(id serial, item text);"
kubectl exec -n step25 postgres-0 -- psql -U appuser -d shopdb -c "INSERT INTO orders(item) VALUES ('shoes'),('hat');"
kubectl delete pod postgres-0 -n step25
kubectl wait --for=condition=Ready pod/postgres-0 -n step25 --timeout=90s
kubectl exec -n step25 postgres-0 -- psql -U appuser -d shopdb -c "SELECT count(*) FROM orders;"   # 2 이면 통과

emptyDir 로 만들었다면 여기서 0 이 나옵니다. 그게 StatefulSet+PVC 를 쓰는 이유입니다.

M10. HPA — manifests/40-hpa.yaml

kind: HorizontalPodAutoscaler
spec:
  scaleTargetRef: { kind: Deployment, name: backend }
  minReplicas: 2
  maxReplicas: 6
  metrics:
  - type: Resource
    resource: { name: cpu, target: { type: Utilization, averageUtilization: 60 } }
  • TARGETS<unknown> → metrics-server 부재. kubectl scale deploy/backend --replicas=4 로 대체하고 그 사실을 명시하면 통과.
  • 부하 테스트로 스케일업을 관찰(강의 본문 25-6 에 실측: cpu 312% 에서 2→6).

M11. 무중단 롤링 업데이트

무중단의 열쇠는 전략 설정입니다.

strategy:
  type: RollingUpdate
  rollingUpdate: { maxUnavailable: 0, maxSurge: 1 }   # 새 파드 Ready 후에만 옛 파드 제거
  • maxUnavailable: 0 이면 항상 원하는 수 이상이 Ready 상태를 유지 → 요청이 끊기지 않음.
  • 함정: ConfigMap 만 바꾸면 파드는 그대로입니다. 롤아웃을 유발(kubectl set env, 또는 kubectl rollout restart deploy/frontend)해야 새 값이 반영됩니다.

채점 자동 점검 스니펫

echo -n "M3 PVC Bound: "; kubectl get pvc data-postgres-0 -n step25 -o jsonpath='{.status.phase}'; echo
echo -n "M4 backend ready: "; kubectl get deploy backend -n step25 -o jsonpath='{.status.readyReplicas}'; echo
echo -n "M5 frontend ready: "; kubectl get deploy frontend -n step25 -o jsonpath='{.status.readyReplicas}'; echo
echo -n "M6 통신: "; kubectl exec -n step25 $(kubectl get pod -n step25 -l tier=frontend -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://backend.step25.svc.cluster.local:8080/hostname; echo
echo -n "M9 외부: "; curl -s -o /dev/null -w "%{http_code}" http://localhost:30080/; echo
echo    "M10 HPA:"; kubectl get hpa backend -n step25

흔한 실수 총정리

증상원인해결
frontend CrashLoopmemory 128Mi OOM / liveness 조기 발동limit 256Mi + startupProbe
Endpoints <none>셀렉터↔레이블 불일치, 파드 not Ready레이블 맞추고 프로브 점검
postgres 기동 실패PGDATA 미설정(lost+found)PGDATA=.../pgdata
HPA <unknown>metrics-server 없음 / requests.cpu 없음설치 대기 or 수동 scale / requests 추가
ConfigMap 바꿔도 반영 안 됨파드 자동 갱신 안 함kubectl rollout restart
롤아웃 중 502maxUnavailable>0 + 프로브 부실maxUnavailable=0 + readinessProbe

수고했습니다. → 코스 처음으로 · 다음은 강의 본문이후 학습 로드맵을 따라가세요.