Notice
Recent Posts
Recent Comments
Link
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

근묵자흑

Kubernetes Pattern: Stateless Service 본문

k8s/kubernetes-pattern

Kubernetes Pattern: Stateless Service

Luuuuu 2025. 10. 18. 18:24

들어가며

Kubernetes는 서버가 갑자기 다운되었을 때, 자동으로 복구되고 사용자는 서비스 중단을 느끼지 못하도록 설계되었습니다.

(Kubernetes는 self-healing과 high availability를 통해 서버 장애 시에도 사용자는 서비스 중단을 느끼지 못합니다.)

이번장은 그것을 어떻게 가능하게 구현하는지 Stateless Service 패턴에 대해 얘기해보겠습니다.

클라우드 네이티브 환경에서 애플리케이션은 언제든지 죽고, 다시 살아날 수 있어야 합니다.

이것이 바로 Stateless Service 패턴의 핵심입니다.

이 글에서는 Kubernetes Patterns 11장 Stateless Service를 실제 실습 및 테스트한 결과와 함께 어떻게 활용할 수 있는지 알아보겠습니다.

Stateless Service 패턴이란?

정의

Stateless Service는 인스턴스 간 상호작용에서 내부적으로 어떤 상태도 유지하지 않는 서비스입니다. 각 요청은 완전히 독립적으로 처리되며, 과거의 요청이나 세션 정보에 의존하지 않습니다.

Kubernetes 환경에서는 이러한 Stateless 애플리케이션을 여러 개의 동일한 Pod으로 실행하여, 다음과 같은 이점을 얻을 수 있습니다:

  • 고가용성: 하나의 Pod이 죽어도 다른 Pod이 즉시 트래픽을 처리
  • 확장성: 필요에 따라 Pod 개수를 쉽게 늘리거나 줄임
  • 자동 복구: 장애 발생 시 자동으로 새 Pod 생성
  • 무중단 배포: 롤링 업데이트로 서비스 중단 없이 배포

핵심 특징: I.R.E

Stateless Service의 모든 인스턴스(Pod)는 다음 세 가지 특징을 가집니다:

1. Identical (동일성)

모든 Pod이 기능적으로 완전히 동일합니다.

  • 같은 컨테이너 이미지 사용
  • 동일한 설정과 환경 변수
  • 같은 코드 실행

2. Replaceable (교체 가능성)

어떤 Pod이든 즉시 삭제하고 교체할 수 있습니다.

  • 특정 Pod에 대한 의존성 없음
  • 언제든지 새로운 Pod으로 대체 가능
  • 사용자는 Pod 교체를 인지하지 못함

3. Ephemeral (일시성)

Pod의 생명주기는 짧고 언제든 사라질 수 있습니다.

  • Pod 내부에 중요한 상태 저장 금지
  • 영구 데이터는 외부 스토리지나 DB에 저장
  • Pod은 "소모품"처럼 취급

12-Factor App과의 관계

Stateless Service 패턴은 12-Factor App 원칙을 따릅니다:

  • VI. Processes: 앱을 하나 이상의 무상태 프로세스로 실행
  • VIII. Concurrency: 프로세스 모델을 통해 확장
  • IX. Disposability: 빠른 시작과 graceful shutdown

Stateless Service 실습

이제 실제로 Stateless Service를 구현하고 테스트해보겠습니다.
우리는 간단한 랜덤 숫자를 반환하는 REST API 서비스를 사용할 것입니다.

환경 준비 (참고)

# 테스트 환경
- Minikube
- 이미지: k8spatterns/random-generator:1.0

실습 1: ReplicaSet으로 Pod 복제하기

3개의 동일한 Pod을 생성하는 ReplicaSet을 만들어보겠습니다.

# replicaset.yml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: random-generator
spec:
  replicas: 3  # 3개의 복제본 유지
  selector:
    matchLabels:
      app: random-generator
  template:
    metadata:
      labels:
        app: random-generator
    spec:
      containers:
      - image: k8spatterns/random-generator:1.0
        name: random-generator
        ports:
        - containerPort: 8080
          protocol: TCP

실행:

kubectl apply -f replicaset.yml
kubectl get pods -l app=random-generator

결과:

NAME                     READY   STATUS    RESTARTS   AGE
random-generator-8948h   1/1     Running   0          9s
random-generator-bnw54   1/1     Running   0          9s
random-generator-xbjkx   1/1     Running   0          9s

확인 포인트:

  • 3개의 Pod이 모두 Running 상태
  • 모든 Pod이 동일한 이름 패턴 (random-generator-xxxxx)
  • ReplicaSet이 DESIRED=CURRENT=READY=3으로 일치
kubectl get rs random-generator
NAME               DESIRED   CURRENT   READY   AGE
random-generator   3         3         3       17s

실습 2: Self-Healing 확인하기

Stateless Service의 가장 강력한 기능 중 하나는 Self-Healing입니다.
Pod 하나를 강제로 삭제하고, ReplicaSet이 어떻게 수행되는지 확인하겠습니다.

실행:

# 첫 번째 Pod 삭제
kubectl get pods -l app=random-generator -o name | head -1 | xargs kubectl delete

# 즉시 확인
kubectl get pods -l app=random-generator -w

결과:

NAME                     READY   STATUS    RESTARTS   AGE
random-generator-bnw54   1/1     Running   0          77s
random-generator-p96nl   1/1     Running   0          11s   ← 새로 생성됨!
random-generator-xbjkx   1/1     Running   0          77s

무슨 일이 일어났나?

  1. random-generator-8948h Pod이 삭제됨
  2. ReplicaSet Controller가 변화를 감지
  3. 선언된 상태(replicas: 3)와 현재 상태(2개)가 불일치함을 발견
  4. 즉시 새로운 Pod random-generator-p96nl 생성
  5. 다시 3개의 Pod이 유지됨

이것이 바로 Kubernetes의 선언적 관리(Declarative Management)입니다.
우리는 "3개의 Pod을 유지하라"고 선언했고, Kubernetes는 이 상태를 지속적으로 유지합니다.

실습 3: Service로 로드밸런싱 구현하기

이제 3개의 Pod에 트래픽을 분산하는 Service를 만들어보겠습니다.

# service.yml
apiVersion: v1
kind: Service
metadata:
  name: random-generator
spec:
  selector:
    app: random-generator  # ReplicaSet과 동일한 label
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  type: ClusterIP

실행:

kubectl apply -f service.yml
kubectl get svc random-generator
kubectl get endpoints random-generator

결과:

NAME               TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
random-generator   ClusterIP   None         <none>        8080/TCP   1m

NAME               ENDPOINTS
random-generator   10.1.0.38:8080,10.1.0.34:8080,10.1.0.37:8080

Service가 3개의 Pod IP를 Endpoint로 자동 등록한걸 확인할 수 있습니다.

로드밸런싱 테스트:

# 5번 요청을 보내봅시다
for i in {1..5}; do
  kubectl run test-curl-$i --image=curlimages/curl --rm -i --restart=Never --quiet \
    -- -s http://random-generator:8080
done

결과:

Request 1: {"random":1962907337,"id":"516fb2c2-6992-4cb3...","version":"1.0"}
Request 2: {"random":-61271774,"id":"d850fc5c-0913-4108...","version":"1.0"}
Request 3: {"random":328143425,"id":"35996d4b-a1d8-4dd6...","version":"1.0"}
Request 4: {"random":-1387919917,"id":"516fb2c2-6992-4cb3...","version":"1.0"}
Request 5: {"random":-2102010644,"id":"d850fc5c-0913-4108...","version":"1.0"}

분석:

  • Request 1, 4: 같은 Pod (516fb2c2...)
  • Request 2, 5: 같은 Pod (d850fc5c...)
  • Request 3: 다른 Pod (35996d4b...)

3개의 서로 다른 Pod에 요청이 분산되었습니다!
이것이 Kubernetes의 자동 로드밸런싱입니다.

실습 4: PersistentVolume으로 스토리지 공유하기

여러 Pod이 외부 스토리지를 공유할 수 있습니다.

# pv-and-pvc.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: example
spec:
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 10Mi
  storageClassName: standard
  hostPath:
    path: /tmp/example
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: random-generator-log
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: standard
  resources:
    requests:
      storage: 10Mi
  volumeName: example

replicaset-with-pv.yml (볼륨 마운트 추가)

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: random-generator
spec:
  replicas: 3
  selector:
    matchLabels:
      app: random-generator
  template:
    metadata:
      labels:
        app: random-generator
    spec:
      containers:
      - image: k8spatterns/random-generator:1.0
        name: random-generator
        env:
        - name: POD_ID
          valueFrom:
            fieldRef:
              fieldPath: metadata.uid
        - name: LOG_FILE
          value: /tmp/logs/random-$(POD_ID).log
        volumeMounts:
        - mountPath: /tmp/logs
          name: log-volume
      volumes:
      - name: log-volume
        persistentVolumeClaim:
          claimName: random-generator-log

실행:

# PV/PVC 생성
kubectl apply -f pv-and-pvc.yml

# ReplicaSet 업데이트
kubectl apply -f replicaset-with-pv.yml

# Pod 재시작
kubectl delete pod -l app=random-generator

확인:

kubectl get pv,pvc

결과:

NAME                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM
persistentvolume/example   10Mi       RWO            Retain           Bound    default/random-generator-log

NAME                                         STATUS   VOLUME    CAPACITY   ACCESS MODES
persistentvolumeclaim/random-generator-log   Bound    example   10Mi       RWO

PVC가 성공적으로 Bound 되었습니다

Pod에서 확인:

POD_NAME=$(kubectl get pods -l app=random-generator -o name | head -1)
kubectl describe $POD_NAME | grep -A 5 "Volumes:"
Volumes:
  log-volume:
    Type:       PersistentVolumeClaim
    ClaimName:  random-generator-log
    ReadOnly:   false

핵심 포인트:

  • Pod은 Stateless이지만, 외부 스토리지는 사용 가능
  • 모든 Pod이 동일한 볼륨을 공유
  • 각 Pod은 POD_ID 환경 변수로 고유한 로그 파일 생성
  • Pod이 재시작되어도 데이터는 보존됨

동작 원리

Service 로드밸런싱 메커니즘

Service가 어떻게 트래픽을 분산하는가?

  1. Service 생성 시:
    • Service는 selector로 Pod들을 찾음
    • 일치하는 Pod들의 IP를 Endpoints에 자동 등록
  2. kube-proxy 동작:
    • 각 노드의 kube-proxy가 iptables 규칙 생성
    • Service IP로 오는 트래픽을 Pod IP로 NAT
    • 기본적으로 랜덤 방식으로 분산
  3. DNS 등록:
    • CoreDNS가 random-generator.default.svc.cluster.local 등록
    • 클러스터 내에서 서비스 이름으로 접근 가능

언제 Stateless Service 패턴을 사용해야 할까?

적합한 사용 사례

  1. REST API 서버
    • 예: User API, Product API, Order API - 각 요청이 독립적 - 세션 정보는 Redis/DB에 저장
  2. 웹 애플리케이션 프론트엔드
    • 예: React, Vue, Angular SPA - 정적 파일 서빙 - API 호출만 수행
  3. 마이크로서비스
    • 예: Payment Service, Notification Service - 서비스 간 독립성 - 수평 확장 필요
  4. 배치 작업 워커
    • 예: 이미지 처리, 데이터 변환 - 작업 큐에서 작업 가져옴 - 병렬 처리
  5. 프록시/게이트웨이
    • 예: Nginx, Envoy, API Gateway - 요청 라우팅 - 부하 분산

부적합한 사용 사례

  1. 데이터베이스
    • MySQL, PostgreSQL, MongoDB → StatefulSet 사용
  2. 메시지 큐
    • RabbitMQ, Kafka → StatefulSet 사용 (순서 보장 필요)
  3. 분산 캐시
    • Redis Cluster, Memcached → StatefulSet + Headless Service
  4. 싱글톤 서비스
    • Cron Job, Leader Election → Singleton Service 패턴 사용
  5. 세션 기반 애플리케이션
    • 메모리에 세션 저장하는 레거시 앱 → Session Affinity 또는 외부 세션 스토어 필요

Best Practices

1. 무상태 설계 원칙

# 나쁜 예: 메모리에 상태 저장
class UserService:
    def __init__(self):
        self.sessions = {}  # 메모리에 세션 저장

    def login(self, user_id):
        self.sessions[user_id] = {...}

# 좋은 예: 외부 저장소 사용
class UserService:
    def __init__(self, redis_client):
        self.redis = redis_client

    def login(self, user_id):
        self.redis.set(f"session:{user_id}", {...})

2. Health Checks 설정

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  template:
    spec:
      containers:
      - name: api
        image: myapi:1.0
        # Readiness Probe: 트래픽 받을 준비 확인
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 3

        # Liveness Probe: 애플리케이션 살아있는지 확인
        livenessProbe:
          httpGet:
            path: /health/live
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10

        # Startup Probe: 초기 시작 확인 (느린 시작 대비)
        startupProbe:
          httpGet:
            path: /health/startup
            port: 8080
          failureThreshold: 30
          periodSeconds: 10

3. 리소스 관리

resources:
  requests:
    memory: "128Mi"   # 최소 보장
    cpu: "250m"       # 0.25 core
  limits:
    memory: "256Mi"   # 최대 사용량
    cpu: "500m"       # 0.5 core

리소스 설정 가이드:

  • requests: 스케줄링 기준, Pod이 반드시 확보할 리소스
  • limits: 최대 사용량, 초과 시 Throttling(CPU) 또는 OOMKilled(Memory)
  • CPU는 압축 가능(compressible), Memory는 비압축(incompressible)

4. Horizontal Pod Autoscaler (HPA)

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # 5분간 안정화
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60

5. Rolling Update 전략

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # 최대 1개 추가 Pod 생성 가능
      maxUnavailable: 1  # 최대 1개 Pod 중단 가능
  template:
    spec:
      containers:
      - name: api
        image: myapi:2.0
        # Graceful Shutdown
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 15"]

Rolling Update 동작:

  1. 새 버전 Pod 1개 생성 (maxSurge: 1)
  2. 새 Pod이 Ready 상태 확인
  3. 기존 Pod 1개 종료 (maxUnavailable: 1)
  4. 모든 Pod이 교체될 때까지 반복

6. Pod Disruption Budget (PDB)

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-server-pdb
spec:
  minAvailable: 2  # 최소 2개는 항상 실행 중이어야 함
  selector:
    matchLabels:
      app: api-server

PDB는 자발적 중단(voluntary disruption) 시 최소 가용성을 보장합니다:

  • 노드 드레인(drain)
  • 클러스터 업그레이드
  • 수동 Pod 삭제

7. Anti-Affinity 설정

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  template:
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - api-server
              topologyKey: kubernetes.io/hostname

이렇게 하면 Pod들이 서로 다른 노드에 분산 배치되어 단일 노드 장애에도 서비스 가능합니다.


트러블슈팅

문제 1: Pod이 계속 재시작됨

증상:

kubectl get pods
NAME                     READY   STATUS             RESTARTS   AGE
api-server-abc123        0/1     CrashLoopBackOff   5          3m

원인 및 해결:

# 로그 확인
kubectl logs api-server-abc123

# 이전 컨테이너 로그 확인
kubectl logs api-server-abc123 --previous

# Pod 이벤트 확인
kubectl describe pod api-server-abc123

일반적인 원인:

  1. 애플리케이션 에러 → 코드 수정
  2. Health Check 실패 → Probe 설정 조정
  3. 리소스 부족 (OOMKilled) → Memory Limit 증가
  4. 환경 변수 누락 → ConfigMap/Secret 확인

문제 2: Service로 접근 불가

확인 절차:

# 1. Pod이 실행 중인지 확인
kubectl get pods -l app=api-server

# 2. Service의 Selector가 맞는지 확인
kubectl describe svc api-server

# 3. Endpoints가 생성되었는지 확인
kubectl get endpoints api-server

# 4. Pod의 라벨 확인
kubectl get pods --show-labels

해결 방법:

# Service의 selector와 Pod의 labels가 일치해야 함
Service:
  selector:
    app: api-server    # ← 이것과

Pod:
  labels:
    app: api-server    # ← 이것이 일치

문제 3: HPA가 스케일하지 않음

확인:

kubectl get hpa
kubectl describe hpa api-server-hpa

# Metrics Server 확인
kubectl top nodes
kubectl top pods

일반적인 원인:

  • Metrics Server 미설치
    • resources.requests 미설정
  • 메트릭 수집 지연

좀 더 들어가서: VPA (Vertical Pod Autoscaler)

VPA란?

Vertical Pod Autoscaler (VPA)는 Pod의 CPU와 Memory 리소스를 자동으로 조정하는 Kubernetes 컴포넌트입니다.

HPA가 Pod의 개수를 조정한다면, VPA는 각 Pod의 크기를 조정합니다.

VPA의 3가지 핵심 기능

1. 리소스 자동 조정

VPA는 실제 사용량을 분석하여 최적의 CPU/Memory 값을 자동으로 적용합니다.

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: vpa-test-app-auto
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: my-app
  updatePolicy:
    updateMode: "Auto"  # 자동으로 리소스 조정

2. 과다/과소 할당 감지

실제 테스트 결과:

# 의도적으로 매우 작은 리소스 설정
resources:
  requests:
    memory: "10Mi"    # ← 너무 작음!
    cpu: "10m"

# VPA 추천값
kubectl get vpa
NAME        MODE   CPU   MEM     PROVIDED
my-app-vpa  Off    25m   250Mi   True

결과:

  • 현재 설정: Memory 10Mi → OOMKilled 발생!
  • VPA 추천: Memory 250Mi → 25배 증가 필요

이것이 바로 과소 할당 감지입니다.

3. 최적 값 추천

VPA는 4가지 추천값을 제공합니다:

recommendation:
  containerRecommendations:
  - containerName: app
    lowerBound:      # 최소 필요량
      cpu: 25m
      memory: 250Mi
    target:          # 권장값 (이 값 사용 권장)
      cpu: 25m
      memory: 250Mi
    upperBound:      # 피크 시 안전 마진 포함
      cpu: 25m
      memory: 1Gi
    uncappedTarget:  # 제한 없는 이상적 값
      cpu: 25m
      memory: 250Mi

VPA 실습

1. VPA 설치

# VPA 저장소 클론
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler

# VPA 설치
./hack/vpa-up.sh

# 설치 확인
kubectl get pods -n kube-system | grep vpa

결과:

vpa-admission-controller   1/1     Running
vpa-recommender            1/1     Running
vpa-updater                1/1     Running

2. VPA 추천 모드 테스트

# vpa-recommender-only.yml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-app-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: my-app
  updatePolicy:
    updateMode: "Off"  # 추천만 제공, 자동 적용 안 함
  resourcePolicy:
    containerPolicies:
    - containerName: app
      minAllowed:
        cpu: 10m
        memory: 10Mi
      maxAllowed:
        cpu: 1000m
        memory: 1Gi

적용 및 확인:

kubectl apply -f vpa-recommender-only.yml

# 추천값 확인 (수 분 후)
kubectl describe vpa my-app-vpa

3. VPA Auto 모드 (자동 적용)

# vpa-auto-mode.yml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-app-vpa-auto
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: my-app
  updatePolicy:
    updateMode: "Auto"  # 자동으로 적용!
  resourcePolicy:
    containerPolicies:
    - containerName: app
      controlledResources: ["cpu", "memory"]

동작 과정:

  1. VPA Recommender가 메트릭 수집 및 분석
  2. 현재 리소스와 추천값 비교
  3. 차이가 크면 VPA Updater가 Pod을 evict
  4. 새 Pod이 생성될 때 VPA Admission Controller가 추천값 주입

VPA vs HPA 비교

특징 VPA HPA
조정 대상 Pod 리소스 (CPU/Memory) Pod 개수
방향 Vertical (수직) Horizontal (수평)
Pod 재시작 ✅ 필요 ❌ 불필요
적용 시점 분 단위 초 단위
적합한 경우 리소스 최적화 부하 분산
비용 영향 개별 Pod 비용 감소 Pod 수 증가

VPA의 장단점

장점

  1. 자동 리소스 최적화
    • 수동: 개발자가 추측으로 설정 → 비효율
    • VPA: 실제 사용량 기반 최적값 → 효율적
  2. 비용 절감
    • 과다 할당: 1000Mi → 실제 사용 250Mi
    • VPA 적용: 300Mi (여유 포함)
    • 절감: 700Mi × Pod 수 × 시간당 요금
  3. OOMKilled 예방
    • 메모리 부족 미리 감지
    • 자동으로 리소스 증가

단점 및 제한사항

  1. Pod 재시작 필요
    • VPA는 리소스 변경을 위해 Pod을 재시작합니다! → 서비스 중단 가능성 → PodDisruptionBudget 필수
  2. HPA와 동시 사용 불가 (CPU/Memory 기준)
    • ❌ 불가: - VPA: CPU requests 조정 - HPA: CPU 사용률 기준 스케일링 → 충돌 발생!
    • ✅ 가능: - VPA: CPU/Memory requests 조정 - HPA: Custom Metrics (RPS, Queue) 기준
  3. 초기 데이터 필요
    • 최소 8시간의 메트릭 데이터 필요
    • 새 앱은 추천값 불안정

VPA 사용 권장 사례

적합한 경우

  • Stateless 애플리케이션
  • 재시작 가능한 워커
  • 리소스 사용량이 변동적인 경우
  • 리소스 설정이 어려운 새 애플리케이션

부적합한 경우

  • Stateful 애플리케이션 (DB, 메시지 큐)
  • 재시작에 민감한 서비스 (WebSocket)
  • HPA를 CPU/Memory 기준으로 사용 중

실무 적용 팁

1. 단계적 적용

# 1단계: Off 모드로 시작 (추천만)
updateMode: "Off"
→ 추천값 확인 후 수동 적용

# 2단계: Initial 모드 (새 Pod만)
updateMode: "Initial"
→ 새 Pod 생성 시에만 적용

# 3단계: Auto 모드 (안정화 후)
updateMode: "Auto"
→ 실시간 자동 조정

2. PodDisruptionBudget 설정

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-app-pdb
spec:
  minAvailable: 1  # VPA가 Pod 재시작 시에도 최소 1개 유지
  selector:
    matchLabels:
      app: my-app

3. 리소스 정책 설정

resourcePolicy:
  containerPolicies:
  - containerName: app
    minAllowed:
      cpu: 100m        # 최소값 제한
      memory: 128Mi
    maxAllowed:
      cpu: 2000m       # 최대값 제한
      memory: 4Gi
    controlledResources: ["cpu", "memory"]

테스트 결과 요약

  1. 과소 할당 정확히 감지
    • 설정: 10Mi → 실패 (OOMKilled)
    • VPA 추천: 250Mi → 성공!
  2. 추천값의 정확성
    • Spring Boot 앱의 실제 필요량을 정확히 파악
    • 25배 메모리 증가 필요성 식별
  3. 자동 업데이트 동작
    • VPA Updater가 Pod 자동 evict
    • 새 Pod에 추천 리소스 적용

Stateless vs Stateful 비교

특징 Stateless Service Stateful Service
상태 저장 ❌ 내부 상태 없음 ✅ 내부 상태 유지
Pod 교체 ✅ 자유롭게 가능 ⚠️ 신중히 처리
확장성 ✅ 쉬운 수평 확장 ⚠️ 복잡한 확장
사용 리소스 ReplicaSet/Deployment StatefulSet
네트워크 ID 불안정 (IP 변경됨) 안정적 (고정 DNS)
스토리지 선택적 (PV) 필수 (PVC per Pod)
배포 순서 병렬 배포 순차 배포
예시 REST API, 웹서버 DB, 메시지큐

참고 자료