"Everything about infra" 2026. 4. 2. 02:52
포스팅은 CloudNet@팀 서종호(Gasida)님이 진행하시는
AWS EKS Workshop Study 내용을 참고하여 작성합니다.

 

안녕하세요!

이번 포스팅은 파드 오토스케일링 기술인

HPA, VPA에 대해서 알아보도록 하겠습니다.

 

 

 

01. HPA (Horizontal Pod Autoscaling)

 

HPA는 언제, 어떻게 스케일 판단을 내리나? 가 핵심입니다.

단순히 "CPU가 높으면 늘린다"가 아닙니다.

 

메트릭 수집 → 목표값과 비교 → 수식으로 replica 수 계산 → 실제 반영 (4단계로 나뉘며, 반복합니다)

 

📌 메트릭은 어디서 가져올까요?

HPA Controller
    │
    ├─ metrics.k8s.io          ← CPU/Memory (metrics-server)
    ├─ custom.metrics.k8s.io   ← Prometheus Adapter 등
    └─ external.metrics.k8s.io ← 외부 시스템 (SQS 큐 길이 등)

오늘 실습 내용은 metrics.k8s.io 경로 즉, metrics-server가 수집한 CPU 사용률을 씁니다.

 

📌 스케일 판단 수식

HPA는 아래 공식으로 필요한 replica 수를 계산합니다.

desiredReplicas = ceil( currentReplicas × (currentMetricValue / desiredMetricValue) )

예시로 이해해볼까요?

상황 currentReplicas currentMetric desiredMetrics 계산 결과
과부하 1 250m (125%) 200m (100%) ceil(1 × 250/200) 2
과부하 심화 2 440m 200m ceil(2 × 440/200) 5
부하 감소 5 60m 200m ceil(5 × 60/200) 2
⚠️ currentMetricValue는 요청(request) 대비 사용량이 아니라, raw 사용량(밀리코어) 입니다.
desiredMetricValue도 마찬가지로 request 기준 % 설정 시 내부에서 request x targetUtilization%로 환산함.

 

📌 스케일링에 영향을 주는 타이밍 제어

HPA가 즉각 반응하면 오히려 불안정해집니다. 그래서 두 가지 안정화 장치가 있습니다.

scaleUp   cooldown : 기본 0초   → 빠르게 올림 (트래픽 급증 대응)
scaleDown cooldown : 기본 300초 → 천천히 내림 (flapping 방지)

그리고 HPA는 15초마다 메트릭을 체크합니다 (--horizontal-pod-autoscaler-sync-period)

 

실습에 들어가기 전에, Grafana 대쉬보드 (22128, 22251) Import를 설정합시다.

저는 values.yaml을 위와 같이 수정하여 업데이트 하였습니다.

위 EKS Control Plane 파일에서 HPA, Scaling 등 관련된 대쉬보드가 생성된 것을 확인 바랍니다.

 


 

HPA 샘플 애플리케이션 배포 

vi hpa-example.yaml

apiVersion: apps/v1
kind: Deployment
metadata: 
  name: php-apache
spec: 
  selector: 
    matchLabels: 
      run: php-apache
  template: 
    metadata: 
      labels: 
        run: php-apache
    spec: 
      containers: 
      - name: php-apache
        image: registry.k8s.io/hpa-example
        ports: 
        - containerPort: 80
        resources: 
          limits: 
            cpu: 500m
          requests: 
            cpu: 200m
---
apiVersion: v1
kind: Service
metadata: 
  name: php-apache
  labels: 
    run: php-apache
spec: 
  ports: 
  - port: 80
  selector: 
    run: php-apache

위 샘플 애플리케이션을 배포하고 확인해봅시다.

# 배포
kubectl apply -f hpa-example.yaml

# 확인
kubectl exec -it deploy/php-apache -- cat /var/www/html/index.php

# 모니터링 (터미널 2개 사용)
watch -d 'kubectl get hpa,pod;echo;kubectl top pod;echo;kubectl top node'
kubectl exec -it deploy/php-apache -- top

 

📌 배포 모니터링

노드는 t3.medium으로 배포하였고 스펙은 vCPU 2core  = 2000m 수용 가능입니다.

php-apache request: 200m  → 노드 전체의 10%
php-apache limit:   500m  → 노드 전체의 25%
현재 실제 사용:       1m   → 거의 0

 

현재 index.php 코드를 보면 아래와 같습니다.

// GET / 요청이 올 때마다 실행
for ($i = 0; $i <= 1000000; $i++) {
    $x += sqrt($x);   // 이 연산이 CPU를 태움
}

요청이 없으면 루프 자체가 돌지 않으므로 현재 정상 상태라고 볼 수 있습니다.

 

부하 발생을 위한 클라이언트용 파드 배포 및 반복 호출

# curl 파드 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: curl
spec:
  containers:
  - name: curl
    image: curlimages/curl:latest
    command: ["sleep", "3600"]
  restartPolicy: Never
EOF

 

이제 아래와 같이 curl로 반복 호출을 진행해보겠습니다.

kubectl exec curl -- sh -c 'while true; do curl -s php-apache; sleep 1; done'
kubectl exec curl -- sh -c 'while true; do curl -s php-apache; sleep 0.5; done'
kubectl exec curl -- sh -c 'while true; do curl -s php-apache; sleep 0.1; done'
kubectl exec curl -- sh -c 'while true; do curl -s php-apache; sleep 0.01; done'

 

그 전에 부하 강도를 먼저 비교하자면

명령 초당 요청 수 특징
sleep 1 ~1 RPS 워밍업, CPU 소폭 상승
sleep 0.5 ~2 RPS 체감 부하 시작
sleep 0.1 ~10 RPS HPA 스케일 아웃 트리거 가능
sleep 0.01 ~100 RPS 강한 부하, 빠른 스케일 아웃
병렬 5 worker ~5 RPS (동시) 단일 루프와 달리 응답 대기 없이 동시 발사

 

📌 HPA 정책 생성 및 부하 발생 후 파드 오토 스케일링 확인

cat <<EOF | kubectl apply -f -
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        averageUtilization: 50
        type: Utilization
EOF

 

HPA 정책 생성 확인

위 내용을 수식으로 풀면:

desiredReplicas = ceil(1 × 250/50) = ceil(5.0) = 5

 

부하가 발생한다면 아래와 같이 동작하게 될거에요.

부하 발생 → 파드 CPU 250% (request 200m의 250% = 500m, limit 찍음)
         → HPA: 목표 50% 초과 감지
         → replica 1 → 4 → (곧 5) 스케일 아웃

 

부하를 발생 시켜볼까요? (위 내용 중 반복 호출 실행)

목표 50%를 초과하여 파드가 10개까지 올라간 상태입니다.

파드 10개가 2개 노드에 분산됐는데 노드 2가 좀 더 부하를 받고 있네요. (CPU 확인)

sleep 0.01 (100 RPS) → 파드 10개로도 CPU 69% → 목표 50% 초과 유지 중
→ max 10개라서 더 이상 못 늘림

 

이제 반복 호출을 취소하면 어떻게 scaleDown 될까요?

호출이 없으니 CPU가 점차 낮아지고, HPA가 파드를 스케일 다운하는 것을 확인 가능합니다.

 


 

02. VPA (Vertical Pod Autoscaler)

 

파드를 배포할 때 resources.requests를 개발자가 설정할거에요.

resources:
  requests:
    cpu: 200m
    memory: 256Mi

근데 이 값을 처음부터 정확하게 잡기가 어렵습니다.

너무 크게 잡으면 → 노드 자원 낭비, 스케줄링 안됨.
너무 작게 잡으면 → OOMKill, CPU 스로틀링 발생함.

 

📌 VPA가 하는 일은?

실제 사용량을 장기간 관찰해서 "이 파드한테 이 정도 request가 적당해" 를 자동으로 계산하고 적용해줍니다.

 

최적값 계산 방식은 아래와 같습니다.

Recommender가 metrics-server로 파드 사용량 수집 (히스토리 누적)
        ↓
백분위수 기반으로 기준값 결정
ex) CPU 사용량 상위 90% 값 → 기준값
        ↓
안전 마진 추가 (버퍼)
기준값 × 1.15 (15% 여유)
        ↓
이 값을 request로 추천/적용

 

그래서 실제 적용 흐름도는 아래와 같습니다.

파드 running
    ↓ (수분~수시간 관찰)
VPA Recommender → recommendation 계산
    ↓
updateMode에 따라:
  Off      → kubectl describe vpa 로 추천값만 확인
  Recreate → 기존 파드 삭제 → 새 파드를 추천 request로 생성

 

📌 VPA 3가지 컴포넌트

Recommender  → 메트릭 수집, 최적값 계산 및 추천
Updater      → 추천값과 현재값 비교, 재시작 필요 파드 판단
Admission Controller → 새 파드 생성 시 추천값으로 request 자동 주입

 

📌 VPA 4가지 모드 (updateMode)

모드 동작
Off 추천값만 계산, 적용 안함 (추천값 확인용)
Initial 파드 최초 생성 시에만 적용
Recreate 추천값 벗어나면 파드 재시작해서 적용
Auto 현재는 Recreate와 동일

 

VPA의 가장 큰 특징HPA와 같이 사용을 하지 못합니다. Why?

HPA: CPU 사용률 기준으로 replica 수 조정
VPA: CPU request 값 자체를 변경
→ request가 바뀌면 HPA 스케일 기준이 흔들림
→ 서로 충돌

 


 

VPA 컨트롤러 설치 후 부하 테스트 실습

Step1. CRD + RBAC 설치

kubectl apply -f https://raw.githubusercontent.com/kubernetes/autoscaler/refs/heads/master/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes/autoscaler/refs/heads/master/vertical-pod-autoscaler/deploy/vpa-rbac.yaml

 

Step2. VPA 컴포넌트 설치

git clone https://github.com/kubernetes/autoscaler.git
cd ~/autoscaler/vertical-pod-autoscaler/
./hack/vpa-up.sh

vpa-up.sh가 Recommender, Updater, Admission Controller 3개를 한번에 설치해줍니다.

 

Step3. 설치 확인

kubectl get pod -n kube-system | grep vpa
kubectl get crd | grep autoscaling
kubectl get mutatingwebhookconfigurations vpa-webhook-config

 

Step 4. VPA 샘플 배포

cat examples/hamster.yaml
kubectl apply -f examples/hamster.yaml && kubectl get vpa -w

 

📌 결과 확인

원래 설정 값은 cpu request: 100m 입니다. 하지만 VPA 추천 값은 아래와 같습니다.

Lower Bound: 268m ← 최소한 이 정도는 있어야 함
Target: 587m ← 이걸로 설정해라 (실제 적용값)
Upper Bound: 1000m ← 이 이상은 필요 없음

그래서 실제 동작 (Event)을 보면,

vpa-updater가 파드 3번 evict함
→ 재시작하면서 request를 100m → 587m 으로 적용

 

kube-ops-view에서 본 동작:

파드가 삭제되네요. VPA가 request 값을 바꾸려면 실행 중인 파드를 수정할 수 없습니다.

Kubernetes에서 실행 중인 파드의 resources.requests는 변경 불가입니다. 그래서 아래와 같이 동작합니다.

기존 파드 (cpu request: 100m) 실행 중
    ↓
VPA Updater: "587m으로 바꿔야 해, 근데 수정 불가"
    ↓
기존 파드 강제 evict (삭제)
    ↓
Deployment가 새 파드 생성 시도
    ↓
VPA Admission Controller가 가로채서
새 파드 spec에 cpu request: 587m 주입
    ↓
새 파드가 587m으로 뜸

 

실제로 아래와 같이 VPA에 의해 기존 파드 삭제되고 신규 파드가 생성되는 것도 확인이 가능합니다.

 


 

In-Place Pod Resource Resize (CPU) 적용

간단하게 말씀드리자면, 기존 VPA 방식과 In-Place의 다른 점은 아래와 같습니다.

기존: request 변경 → 파드 삭제 → 재시작
In-Place: request 변경 → 파드 살아있는 채로 즉시 적용

즉, 서비스 중단 없이 리소스 조정이 가능하단 얘기네요.

여기서 주의할 점은 In-Place Resize는 K8s 1.27 + alpha, 1.33 stable 이상에 지원합니다.

 

📌 테스트 파드 배포 (기존 hamster yaml 변경)

cat <<'EOF' | kubectl apply -f -
apiVersion: "autoscaling.k8s.io/v1"
kind: VerticalPodAutoscaler
metadata:
  name: hamster-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hamster
  resourcePolicy:
    containerPolicies:
      - containerName: '*'
        minAllowed:
          cpu: 100m
          memory: 50Mi
        maxAllowed:
          cpu: 1
          memory: 500Mi
        controlledResources: ["cpu", "memory"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hamster
spec:
  selector:
    matchLabels:
      app: hamster
  replicas: 2
  template:
    metadata:
      labels:
        app: hamster
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
      containers:
        - name: hamster
          image: registry.k8s.io/ubuntu-slim:0.14
          resources:
            requests:
              cpu: 100m
              memory: 50Mi
          resizePolicy:
          - resourceName: cpu
            restartPolicy: NotRequired
          - resourceName: memory
            restartPolicy: RestartContainer
          command: ["/bin/sh"]
          args:
            - "-c"
            - "while true; do timeout 0.5s yes >/dev/null; sleep 0.5s; done"
EOF

 

 

여기서 resizePolicy를 추가하여 파드의 중단 없이 In-Place의 동작을 확인해보겠습니다.

VPA와 In-place를 같이 사용하니까 적용이 안되네요 (VPA 방식으로 동작합니다)

 

하지만 VPA를 사용하지 않고 In-Place를 독립적으로 테스트 해보면 이야기가 달라집니다.

# 테스트 파드 배포
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: inplace-test
spec:
  containers:
  - name: stress
    image: nginx
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 500m
        memory: 256Mi
EOF

# 현재 request 확인
kubectl get pod inplace-test -o jsonpath='{.spec.containers[0].resources}' | jq .

 

이제 배포를 했으니, request를 확인해본 후에 업데이트를 해볼까요?

kubectl patch pod inplace-test --subresource resize --type merge -p '{"spec":{"containers":[{"name":"stress","resources":{"requests":{"cpu":"300m","memory":"128Mi"},"limits":{"cpu":"500m","memory":"256Mi"}}}]}}'

request가 업데이트 되면서 값이 변경이 되었음에도 AGE (파드가 떠있는 시간)은 그대로입니다.

즉, In-Place가 동작하여서 파드의 중단 없이 잘 적용이 된 것을 확인해보았습니다.

 


긴 글 읽어주셔서 감사합니다.

 

다음 포스팅에서는 KEDA, CPA로 찾아뵙도록 하겠습니다.