Kubernetes 정복기: 운영 필수 기술 마스터하기 (Day 3)

문한성·2025년 11월 1일
0

K8S 정복하기

목록 보기
3/9

3-node 클러스터에서 직접 실습하며 배운 Kubernetes Operations의 모든 것


들어가며

Day 2에서 클러스터 아키텍처와 네트워킹을 이해했다면, Day 3는 실전 운영에 필요한 기술들을 익히는 날이었습니다. Secret 관리부터 Rolling Update, Health Check까지 - Production 환경에서 반드시 알아야 할 개념들을 직접 실습하며 체득했습니다.

특히 이번 Day에서는 "왜 이 기능이 필요한가?"라는 질문을 끊임없이 던지며, 단순히 명령어를 외우는 것이 아니라 설계 철학을 이해하는 데 집중했습니다.

학습 환경

  • Kubernetes: v1.31.13
  • 클러스터 구성:
    • cpu1 (172.30.1.43): Master + Worker (12 core, 7.5GB RAM)
    • cpu2 (172.30.1.34): Worker (8 core, 16GB RAM)
    • gpu1 (172.30.1.38): Worker (12 core, 16GB RAM)
  • CNI: Calico (VXLAN CrossSubnet)

1. Secret과 ConfigMap: 민감 정보는 어떻게 관리할까?

ConfigMap vs Secret: 차이가 뭘까?

처음엔 의문이었습니다. "둘 다 설정 정보 저장하는 거 아닌가? 왜 굳이 나눠놨을까?"

핵심 차이:

# ConfigMap: 일반 텍스트
data:
  app.env: |
    LOG_LEVEL=info
    MAX_CONNECTIONS=100

# Secret: base64 인코딩
data:
  password: c3VwZXJzZWNyZXQxMjM=  # echo -n 'supersecret123' | base64

Secret은 base64로 인코딩되고, etcd에 암호화되어 저장됩니다. (etcd encryption 설정 시)

Secret 사용 방법 실습

1. 환경변수로 주입:

kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password=supersecret123
# Pod에서 환경변수로 사용
env:
- name: DB_USER
  valueFrom:
    secretKeyRef:
      name: db-credentials
      key: username

검증:

$ kubectl exec secret-env-pod -- env | grep DB_USER
DB_USER=admin

2. 볼륨으로 마운트:

volumes:
- name: secret-volume
  secret:
    secretName: db-credentials

실제로 들어가보니 신기한 구조:

$ kubectl exec secret-volume-pod -- ls -la /etc/secrets
total 0
drwxrwxrwt 3 root root  120 Nov  1 10:23 .
drwxr-xr-x 1 root root 4096 Nov  1 10:23 ..
drwxr-xr-x 2 root root   80 Nov  1 10:23 ..2025_11_01_10_23_45.1234567890
lrwxrwxrwx 1 root root   32 Nov  1 10:23 ..data -> ..2025_11_01_10_23_45.1234567890
lrwxrwxrwx 1 root root   15 Nov  1 10:23 password -> ..data/password
lrwxrwxrwx 1 root root   15 Nov  1 10:23 username -> ..data/username

심볼릭 링크 구조! 이렇게 하면 Secret을 업데이트해도 파일 경로는 동일하게 유지됩니다.

배운 점

  • Secret은 단순히 "보안"만이 아니라 RBAC과 통합되어 권한 관리가 가능
  • 볼륨 마운트 시 심볼릭 링크 구조로 무중단 업데이트 가능

2. Rolling Update: 무중단 배포의 마법

maxSurge와 maxUnavailable의 비밀

문서에서 "Rolling Update는 무중단 배포를 지원합니다"라는 말은 많이 봤지만, 실제로 어떻게 동작하는지 보고 싶었습니다.

전략 설정:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 0        # 추가 Pod 생성 안 함
    maxUnavailable: 1  # 한 번에 1개씩만 교체

업데이트 실행:

$ kubectl set image deployment/nginx-rolling nginx=nginx:1.21

실시간 모니터링 결과:

# 시작 (3개 모두 Running)
nginx-rolling-56d8f-abc   1/1   Running
nginx-rolling-56d8f-def   1/1   Running
nginx-rolling-56d8f-ghi   1/1   Running

# 첫 번째 교체
nginx-rolling-56d8f-abc   1/1   Terminating     <- 종료 중
nginx-rolling-56d8f-def   1/1   Running
nginx-rolling-56d8f-ghi   1/1   Running
nginx-rolling-7c9d4-jkl   0/1   ContainerCreating  <- 생성 중

# 새 Pod Ready
nginx-rolling-56d8f-def   1/1   Running
nginx-rolling-56d8f-ghi   1/1   Running
nginx-rolling-7c9d4-jkl   1/1   Running         <- Ready!

# 두 번째 교체
nginx-rolling-56d8f-def   1/1   Terminating
nginx-rolling-56d8f-ghi   1/1   Running
nginx-rolling-7c9d4-jkl   1/1   Running
nginx-rolling-7c9d4-mno   0/1   ContainerCreating

놀라운 점:

  • 정확히 한 번에 하나씩만 교체됨
  • 새 Pod가 Running이 된 후에야 다음 Pod 종료 시작
  • 서비스 중단 없이 완벽하게 업데이트!

배운 점

maxSurge=0, maxUnavailable=1
→ 총 Pod 수는 항상 3개 유지
→ 하나씩 차근차근 교체

maxSurge=1, maxUnavailable=1 (기본값)
→ 총 Pod 수는 3~4개 (최대 4개까지 가능)
→ 더 빠른 업데이트, 약간의 리소스 오버헤드

3. PV/PVC: 스토리지 추상화의 필요성

HostPath의 함정

처음엔 간단하게 생각했습니다. "각 노드에 같은 경로로 PV 만들면 되겠지?"

현실은...

gpu1 노드에서 데이터 생성:

$ kubectl exec pvc-test-pod -- sh -c "echo 'Data from gpu1' > /data/test.txt"
$ kubectl exec pvc-test-pod -- cat /data/test.txt
Data from gpu1

Pod를 cpu1 노드로 이동:

$ kubectl delete pod pvc-test-pod
$ kubectl apply -f pvc-test-pod.yaml  # nodeSelector: cpu1
$ kubectl exec pvc-test-pod -- cat /data/test.txt
cat: can't open '/data/test.txt': No such file or directory

아하! HostPath는 노드 로컬 스토리지였습니다. 다른 노드에선 접근 불가!

Ceph가 뭐길래?

"그럼 여러 노드에서 같은 데이터를 쓰려면 어떻게 하지?"

해답: 분산 스토리지 시스템

┌─────────────────────────────────────┐
│  Kubernetes Cluster                 │
│  ┌─────┐  ┌─────┐  ┌─────┐         │
│  │ cpu1│  │ cpu2│  │ gpu1│         │
│  └──┬──┘  └──┬──┘  └──┬──┘         │
│     │        │        │             │
│     └────────┼────────┘             │
│              │                      │
│         ┌────▼─────┐                │
│         │   Ceph   │ ← 네트워크 스토리지
│         │  Cluster │                │
│         └──────────┘                │
│  (cpu1, cpu2, gpu1의 디스크를       │
│   통합하여 하나의 스토리지로 제공)   │
└─────────────────────────────────────┘

Ceph:

  • 여러 노드의 SSD/HDD를 하나의 스토리지 풀로 통합
  • 데이터 복제 (Replication)로 안정성 보장
  • ReadWriteMany (RWX) 지원 - 여러 Pod가 동시 접근 가능

AWS EFS, GCP Persistent Disk 등도 같은 원리!

PV vs PVC: 왜 나눴을까?

"PV만 있으면 되는 거 아냐? PVC는 왜 필요하지?"

설계 철학:

PV (PersistentVolume)
  ├─ 클러스터 레벨 리소스
  ├─ 관리자가 프로비저닝
  └─ 실제 스토리지 백엔드 정의

PVC (PersistentVolumeClaim)
  ├─ 네임스페이스 레벨 리소스
  ├─ 개발자가 요청
  └─ 필요한 용량/접근모드만 명시

비유:

  • PV = 아파트 (실제 부동산)
  • PVC = 임대 계약서 (사용 권한)

장점:
1. 추상화: 개발자는 스토리지 구현 몰라도 됨
2. 격리: 네임스페이스별 권한 관리
3. 동적 프로비저닝: StorageClass로 자동 생성 가능

배운 점

  • HostPath는 개발/테스트용, Production에선 Ceph/NFS 필수
  • PV/PVC 분리는 관심사의 분리 (Separation of Concerns)

4. QoS: 리소스 압박 시 누구를 살릴 것인가?

처음엔 이해 안 됐던 QoS

"limits 넘으면 OOMKilled되는데, QoS는 또 뭐지?"

핵심 차이:

OOMKilled (개별 Pod)
  ├─ Pod가 자신의 limits를 초과할 때
  ├─ 언제든 발생 가능
  └─ 해당 Pod만 종료

QoS Eviction (노드 전체)
  ├─ 노드 전체 메모리 부족 시
  ├─ 여러 Pod 중 누구를 죽일지 결정
  └─ 우선순위: BestEffort > Burstable > Guaranteed

QoS 클래스 실습

1. Guaranteed: 최고 우선순위

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "100m"      # requests = limits
    memory: "128Mi"

2. Burstable: 중간 우선순위

resources:
  requests:
    memory: "128Mi"
  limits:
    memory: "256Mi"  # limits > requests

3. BestEffort: 최하위 우선순위

resources: {}  # 아무것도 지정 안 함

검증:

$ kubectl get pod qos-guaranteed -o jsonpath='{.status.qosClass}'
Guaranteed

$ kubectl get pod qos-burstable -o jsonpath='{.status.qosClass}'
Burstable

실용적 사용 사례 (핵심!)

처음엔 "다 필요한 Pod인데 왜 죽여?"라고 생각했지만, 실제 사례를 들으니 보험 같은 개념이라는 걸 깨달았습니다.

시나리오 1: 클라우드 비용 최적화

# 핵심 API 서버 - Guaranteed
api-server:
  resources:
    requests: {memory: 2Gi}
    limits: {memory: 2Gi}

# 로그 수집기 - Burstable
log-collector:
  resources:
    requests: {memory: 256Mi}
    limits: {memory: 1Gi}

# 통계 분석 - BestEffort
analytics:
  resources: {}  # 평소엔 여유 리소스 사용, 압박 시 희생

시나리오 2: 피크 타임 대응

  • 평소: 모든 서비스 정상 동작
  • 트래픽 급증:
    • Guaranteed (API) → 절대 보호
    • BestEffort (통계) → 자동 종료
    • Burstable (로그) → 상황에 따라

시나리오 3: 스팟 인스턴스 활용

  • 저렴한 스팟 인스턴스에는 BestEffort Pod 배치
  • 인스턴스 종료되어도 핵심 서비스 무사

배운 점

  • QoS는 "Pod 죽이는 기능"이 아니라 리소스 압박 시 우선순위 보험
  • "Replica 줄여도 되는 서비스"에 낮은 QoS 부여
  • 멀티테넌트 클러스터에서 특히 중요

5. Health Check: Kubernetes가 애플리케이션 상태를 아는 법

Liveness vs Readiness: 헷갈리는 두 Probe

Liveness Probe

  • "살아있니?"
  • 실패 시 → Pod 재시작
  • 데드락, 무한루프 같은 상황 복구

Readiness Probe

  • "트래픽 받을 준비 됐니?"
  • 실패 시 → Service Endpoints에서 제외
  • 초기화, DB 연결 대기 등

Liveness Probe 실습

시나리오: 30초 후 파일 삭제

livenessProbe:
  exec:
    command:
      - cat
      - /tmp/healthy
  initialDelaySeconds: 5
  periodSeconds: 5

# Container command
command:
  - sh
  - -c
  - |
    touch /tmp/healthy
    sleep 30
    rm -f /tmp/healthy  # 30초 후 삭제!
    sleep 600

결과:

$ kubectl get pod liveness-test -w
NAME            READY   STATUS    RESTARTS   AGE
liveness-test   1/1     Running   0          10s
liveness-test   1/1     Running   0          30s
liveness-test   1/1     Running   1          40s  <- 재시작!

정확히 30초 후 재시작! Liveness Probe가 실패를 감지하고 자동 복구했습니다.

Readiness Probe 실습

시나리오: /ready 파일 없으면 트래픽 차단

readinessProbe:
  httpGet:
    path: /ready
    port: 80
  initialDelaySeconds: 5
  periodSeconds: 5

배포 직후:

$ kubectl get pod -n production
NAME                          READY   STATUS    RESTARTS   AGE
production-app-7c9d4-abc      0/1     Running   0          10s  <- 0/1!
production-app-7c9d4-def      0/1     Running   0          10s

Endpoints 확인:

$ kubectl get endpoints production-app -n production
NAME             ENDPOINTS   AGE
production-app   <none>      30s  <- 비어있음!

/ready 파일 생성:

$ kubectl exec -n production production-app-7c9d4-abc -- \
  sh -c "echo 'ready' > /usr/share/nginx/html/ready"

$ kubectl get pod -n production
production-app-7c9d4-abc      1/1     Running   0          1m  <- 1/1!

$ kubectl get endpoints production-app -n production
production-app   10.244.5.224:80,10.244.102.153:80,10.244.184.91:80

완벽하게 동작! Readiness가 통과되자 Endpoints에 자동 등록되었습니다.

Startup Probe: 느린 애플리케이션을 위한 배려

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30    # 30번 실패까지 허용
  periodSeconds: 10       # 10초마다 체크
  # → 최대 300초(5분) 대기

Java Spring Boot처럼 초기화가 오래 걸리는 앱에 유용합니다.

배운 점

  • Liveness: "죽은 Pod" 재시작
  • Readiness: "준비 안 된 Pod" 트래픽 차단
  • Startup: "느린 Pod" 보호
  • 세 가지를 함께 사용해야 완벽한 Health Check!

6. 실전 시나리오: Production-Ready 애플리케이션 배포

배운 모든 것을 하나로

Day 3의 모든 개념을 통합한 Production 애플리케이션을 배포했습니다.

아키텍처:

production namespace
├── ConfigMap (nginx.conf + app.env)
├── Secret (DB_PASSWORD, API_KEY)
├── Deployment (3 replicas)
│   ├── Resource requests/limits (QoS: Burstable)
│   ├── Liveness Probe (/health)
│   ├── Readiness Probe (/ready)
│   ├── Rolling Update (maxSurge: 1, maxUnavailable: 1)
│   └── ConfigMap/Secret mount
├── Service (ClusterIP)
└── Service (NodePort 30080)

Deployment YAML (핵심 부분)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: production-app
  namespace: production
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  template:
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        # QoS: Burstable
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "200m"
            memory: "256Mi"
        # Health Checks
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
        # Secret 환경변수
        env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: DB_PASSWORD
        # ConfigMap 마운트
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
      volumes:
      - name: nginx-config
        configMap:
          name: app-config

배포 검증

1. Pod 분산 배포 확인:

$ kubectl get pods -n production -o wide
NAME                              NODE   IP               READY
production-app-849b867f78-9vkkp   cpu1   10.244.184.91    1/1
production-app-849b867f78-b2fjx   gpu1   10.244.5.224     1/1
production-app-849b867f78-kkdjv   cpu2   10.244.102.153   1/1

완벽하게 3개 노드에 분산!

2. QoS 클래스 확인:

$ kubectl get pod production-app-849b867f78-9vkkp -n production \
  -o jsonpath='{.status.qosClass}'
Burstable

3. 내부 접근 테스트:

$ kubectl run test-v1 --rm -i --restart=Never -n production \
  --image=busybox:1.28 -- wget -qO- http://production-app

Hello from Production App v1.0

4. 외부 접근 테스트 (NodePort):

$ curl http://172.30.1.43:30080
Hello from Production App v1.0

Rolling Update 실전 (v1.0 → v2.0)

ConfigMap 업데이트:

$ kubectl apply -f app-config-v2.yaml
configmap/app-config configured

Deployment restart로 Rolling Update 트리거:

$ kubectl rollout restart deployment production-app -n production
deployment.apps/production-app restarted

$ kubectl rollout status deployment production-app -n production
deployment "production-app" successfully rolled out

업데이트 확인:

$ kubectl get pods -n production
NAME                              READY   STATUS    AGE
production-app-849b867f78-9vkkp   1/1     Running   34s  <- 모두 새로 생성!
production-app-849b867f78-b2fjx   1/1     Running   34s
production-app-849b867f78-kkdjv   1/1     Running   23s

$ curl http://172.30.1.43:30080
Hello from Production App v2.0 - UPDATED!

무중단 배포 성공! 서비스 중단 없이 v2.0으로 업데이트되었습니다.

배운 점

  • 모든 Best Practice를 한 번에 적용하는 게 Production-Ready
  • ConfigMap/Secret 분리로 설정 관리 용이
  • Health Check + Rolling Update = 무중단 배포의 핵심
  • NodePort로 외부 접근 가능 (Ingress 전 단계)

삽질 포인트

1. ConfigMap 업데이트했는데 Pod가 안 바뀌어요!

문제:

$ kubectl apply -f new-configmap.yaml
configmap/app-config configured

$ curl http://app
Hello from v1.0  <- 여전히 v1.0!

원인:

  • ConfigMap/Secret을 업데이트해도 기존 Pod는 자동으로 재시작되지 않음
  • 환경변수로 주입한 경우: Pod 재시작 필수
  • 볼륨 마운트: 심볼릭 링크로 업데이트되지만 애플리케이션이 리로드해야 함

해결:

$ kubectl rollout restart deployment production-app -n production

2. PVC가 Pending 상태로 멈춰요!

문제:

$ kubectl get pvc
NAME           STATUS    VOLUME   CAPACITY   STORAGECLASS
pvc-hostpath   Pending

원인:

  • PV와 PVC의 accessModes가 불일치
  • PV의 용량이 PVC의 요청보다 작음
  • StorageClass를 쓰는데 Provisioner가 없음

해결:

$ kubectl describe pvc pvc-hostpath
Events:
  Warning  ProvisioningFailed  no persistent volumes available

PV의 accessModes와 capacity를 확인하고 매칭시키세요!

3. QoS를 Guaranteed로 하고 싶은데 Burstable이 돼요!

문제:

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "200m"    # requests와 다름!
    memory: "128Mi"

원인:

  • Guaranteed는 모든 컨테이너모든 리소스(CPU, Memory)에서 requests = limits 필요
  • 하나라도 다르면 Burstable

해결:

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "100m"     # requests와 동일!
    memory: "128Mi"

4. Readiness Probe 실패인데 Pod가 안 죽어요!

이건 정상입니다!

  • Liveness → Pod 재시작
  • Readiness → Service Endpoints에서만 제외

Readiness 실패로 Pod를 죽이고 싶다면 Liveness Probe도 함께 설정하세요.


핵심 개념 정리

ConfigMap vs Secret

항목ConfigMapSecret
용도일반 설정민감 정보
인코딩없음base64
etcd 암호화선택권장
RBAC가능가능
예시nginx.conf, app.envpassword, API key

Rolling Update 전략

maxSurge=1, maxUnavailable=0
  → 새 Pod 먼저 생성 후 기존 Pod 종료
  → 리소스 오버헤드 있지만 가장 안전

maxSurge=0, maxUnavailable=1
  → 기존 Pod 종료 후 새 Pod 생성
  → 리소스 절약, 약간의 Capacity 감소

maxSurge=1, maxUnavailable=1
  → 균형잡힌 기본값

PV/PVC 관계

PV (관리자)
  ├─ hostPath: /mnt/data
  ├─ capacity: 1Gi
  └─ accessModes: ReadWriteOnce

      ⬇ Binding (1:1)

PVC (개발자)
  ├─ requests: 500Mi
  └─ accessModes: ReadWriteOnce

      ⬇ Mount

Pod
  └─ volumeMounts: /data

QoS 우선순위

리소스 압박 시 Eviction 순서:
1. BestEffort (requests/limits 없음)
2. Burstable (requests < limits)
3. Guaranteed (requests = limits) ← 최후까지 보호

Health Check 조합

# 가장 권장하는 조합
livenessProbe:   # 데드락 복구
  httpGet: /health
  initialDelaySeconds: 30

readinessProbe:  # 초기화 대기
  httpGet: /ready
  initialDelaySeconds: 5

startupProbe:    # 느린 시작 허용
  httpGet: /health
  failureThreshold: 30
  periodSeconds: 10

다음 계획 (Day 4)

Day 3에서 개별 기능들을 익혔다면, Day 4는 고급 패턴실전 시나리오를 다룰 예정입니다:

  1. Ingress: HTTP 라우팅, 도메인 기반 라우팅, TLS 종료
  2. HPA (Horizontal Pod Autoscaler): CPU 기반 자동 스케일링
  3. RBAC: 역할 기반 접근 제어, ServiceAccount
  4. StatefulSet: Stateful 애플리케이션 (DB, 메시지큐)
  5. DaemonSet: 모든 노드에 Pod 배포 (로그 수집, 모니터링)
  6. Monitoring: metrics-server, kube-ops-view로 시각화

특히 HPA와 Ingress는 Production 환경에서 거의 필수이기 때문에, Day 4의 하이라이트가 될 것 같습니다.


마무리

Day 3를 돌아보니, 단순히 "어떻게 하는가"를 넘어 "왜 이렇게 설계되었는가"를 이해하는 데 집중했던 것 같습니다.

특히 기억에 남는 것들:

  • Secret의 심볼릭 링크 구조
  • Rolling Update의 실시간 Pod 교체 과정
  • HostPath vs Ceph의 명확한 차이
  • QoS가 단순한 "Pod 죽이기"가 아니라 리소스 압박 시 보험이라는 깨달음
  • ConfigMap 업데이트 후 Pod를 수동으로 재시작해야 한다는 함정

3-node 클러스터에서 직접 실습하며, 문서로만 봤을 때는 몰랐던 실제 동작을 눈으로 확인할 수 있었던 게 가장 큰 수확입니다.

Day 4에서는 더 복잡한 시나리오와 Production 환경에서의 Best Practice를 익혀보겠습니다!

profile
기록하고 공유하려고 노력하는 DevOps 엔지니어

0개의 댓글