[AEWS4] 3주차 EKS Scaling(2) - EKS 스케일링 (Pod- HPA, VPA, KEDA, CPA)

2026. 4. 6. 02:47카테고리 없음

3. EKS Scaling 소개

  • EKS에서 스케일링은 단순히 Pod 수를 늘리는 문제만이 아니라, 애플리케이션 튜닝, Pod 리소스 조정, 노드 증설, 이벤트 기반 확장까지 함께 고려해야 합니다.
  • 어떤 워크로드인지에 따라 HPA, VPA, KEDA, Cluster Autoscaler, Karpenter 같은 도구의 역할이 달라집니다.

주요 스케일링 기술은 다음과 같습니다.

  1. Application tuning 애플리케이션 자체의 병목을 줄이기 위한 프로세스 튜닝입니다.
  2. VPA Vertical Pod Autoscaler와 In-Place Pod Resource Resize를 통해 Pod 1개의 CPU/메모리 크기를 조정합니다.
  3. HPA CPU/메모리 같은 메트릭을 기준으로 Pod 수를 늘리거나 줄입니다. 요청 분산을 위해 보통 Load Balancer가 함께 필요합니다.
  4. KEDA CPU/메모리가 아니라 큐 길이, Kafka 메시지 수, Cron 스케줄 같은 이벤트를 기준으로 확장합니다.
  5. CA/CAS Cluster Autoscaler는 Pod가 더 이상 스케줄되지 못할 때 노드를 동적으로 추가/삭제합니다.
  6. AWS 제공 기타 옵션
    • Karpenter: 워크로드에 맞춰 노드를 빠르게 프로비저닝
    • Fargate: 서버리스 방식으로 컨테이너 실행
    • Auto Mode: AWS가 노드 운영 일부를 대신 관리

 

스케일링 전략 의사결정 프레임워크

 

  • 먼저 "우리 서비스가 정말 초고속 반응형 스케일링이 필요한가?"를 판단해야 합니다.
  • "트래픽 급증 시 사용자 에러 방지"라는 같은 목표라도, 실제로는 여러 접근법이 존재하며 대부분은 초고속 확장보다 예측형/복원력 중심 접근이 더 비용 효율적입니다.
1. 반응형 고속화 Karpenter + KEDA + Warm Pool 5-45초 $40K-190K 매우 높음 극소수 미션 크리티컬
2. 예측형 스케일링 CronHPA + Predictive Scaling 사전 확장 (0초) $2K-5K 낮음 패턴 있는 대부분의 서비스
3. 아키텍처 복원력 SQS/Kafka + Circuit Breaker 스케일링 지연 허용 $1K-3K 중간 비동기 처리 가능한 서비스
4. 적정 기본 용량 기본 replica 20-30% 증설 불필요 (이미 충분) $5K-15K 매우 낮음 안정적인 트래픽

 

4. Horizontal Pod Autoscaler (HPA)

HPA 소개

링크: Docs

  • Kubernetes에서 애플리케이션의 Pod 복제본 수를 자동으로 늘리고 줄이는 기능을 수평 Pod 자동 확장(HPA) 이라고 합니다.
  • HPA는 CPU 사용률, 메모리, 커스텀 메트릭 등을 관찰하여 원하는 상태에 맞게 replica 수를 조정합니다.
  • 예를 들어 CPU 목표값을 50%로 두면, 평균 CPU 사용률이 50%를 넘을 때 replica를 늘리고 다시 안정화되면 줄입니다.
  • 이 동작은 minReplicas, maxReplicas, 그리고 scale up/down 안정화 시간의 영향을 받습니다.

샘플 애플리케이션 배포

먼저 HPA 실습용 샘플 애플리케이션을 배포합니다.

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 https://k8s.io/examples/application/php-apache.yaml
kubectl exec -it deploy/php-apache -- cat /var/www/html/index.php
<?php
$x = 0.0001;
for ($i = 0; $i <= 1000000; $i++) {
        $x += sqrt($x);
}
echo "OK!";
?>

부하 발생과 모니터링

부하를 발생시키기 전에 HPA와 Pod 상태를 모니터링할 터미널을 준비합니다.

# 터미널 1
watch -d 'kubectl get hpa,pod; echo; kubectl top pod; echo; kubectl top node'
# 터미널 2
kubectl exec -it deploy/php-apache -- top
# 터미널 3
kubectl exec -it curl -- sh -c 'for i in $(seq 1 5); do while true; do curl -s php-apache; sleep 1; done & done; wait'

 

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

CLI 한 줄로 생성할 수도 있습니다.

kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10

HPA 생성 후 상태를 확인합니다.

kubectl describe hpa
kubectl get hpa php-apache -o yaml | kubectl neat

예시 출력:

Name: php-apache
Namespace: default
Reference: Deployment/php-apache
Metrics: ( current / target )
resource cpu on pods (as a percentage of request): 0% (1m) / 50%
Min replicas: 1
Max replicas: 10
Deployment pods: 1 current / 1 desired
Conditions:
  AbleToScale   True
  ScalingActive True
  ScalingLimited False
Events: <none>

지속적으로 부하를 주면 replica가 증가하고, 부하를 중단한 뒤 일정 시간이 지나면 다시 줄어듭니다.

kubectl exec curl -- sh -c 'while true; do curl -s php-apache; sleep 0.01; done'
  • 기본적으로 scale up은 비교적 빠르게 반응합니다.
  • scale down은 기본 안정화 시간 때문에 약 5분 정도 뒤에 일어날 수 있습니다.
  • 이는 순간적인 트래픽 하락에 과민 반응하지 않도록 하기 위한 동작이며, 운영 환경에서는 워크로드 특성에 맞게 조정합니다.

HPA 사용 시 주의사항

  • HPA가 CPU 사용률을 계산하려면 Deployment에 resources.requests가 반드시 있어야 합니다.
  • metrics-server가 정상 동작하지 않으면 HPA는 CPU/메모리 기반 스케일링을 수행할 수 없습니다.
  • HPA는 평균값 기반으로 판단하므로, 일부 Pod만 과부하인 상황을 놓칠 수 있습니다.
  • CPU/메모리만으로 대응이 어려운 이벤트성 워크로드는 KEDA 같은 이벤트 기반 확장이 더 적합할 수 있습니다.

참고 링크:

5. VPA - Vertical Pod Autoscaler

VPA 소개

  • VPA(Vertical Pod Autoscaler) 는 Pod의 resources.requests 값을 자동으로 최적화하는 도구입니다.
  • HPA가 Pod 수를 늘리는 수평 확장이라면, VPA는 Pod 1개의 CPU/메모리 크기를 조정하는 수직 확장입니다.
  • VPA는 실제 리소스 사용량을 분석해 적정 요청값을 계산하고, 필요하면 Pod를 재시작하여 새 리소스 요청값을 반영합니다.

주의할 점:

  • HPA와 VPA를 CPU/메모리 기준으로 동시에 사용하면 충돌이 발생할 수 있습니다.
  • VPA는 Pod 재시작을 유발할 수 있으므로 순간적인 서비스 영향이 생길 수 있습니다.
  • updateMode: Off로 두면 권장값만 계산하고 실제 반영은 하지 않습니다.

Goldilocks

 

Goldilocks는 VPA 권장값을 보기 쉽게 보여주는 도구입니다.

  • 뜻: "너무 크지도 작지도 않은, 딱 적당한 상태"
  • VPA 권장치를 기반으로 적절한 CPU/메모리 요청값을 찾는 데 유용합니다.

참고 링크:

Goldilocks 설치

kubectl create ns javajmx-sample
kubectl label ns javajmx-sample goldilocks.fairwinds.com/enabled=true
kubectl describe ns javajmx-sample
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm upgrade --install goldilocks fairwinds-stable/goldilocks \
  --namespace goldilocks \
  --create-namespace \
  --set vpa.enabled=true
kubectl get all -n goldilocks

예시 출력:

NAME                                                       READY   STATUS    RESTARTS   AGE
pod/goldilocks-controller-6cb5d68986-qjq72                 1/1     Running   0          35s
pod/goldilocks-dashboard-fcf885596-56w6d                  1/1     Running   0          35s
pod/goldilocks-dashboard-fcf885596-m4xz7                  1/1     Running   0          35s
pod/goldilocks-vpa-admission-controller-5dd4f64f9c-fl4sw  1/1     Running   0          35s
pod/goldilocks-vpa-recommender-967dd4dcb-ls5nn            1/1     Running   0          35s

샘플 앱 배포와 권장치 확인

kubectl apply -f https://raw.githubusercontent.com/aws-observability/aws-o11y-recipes/main/sandbox/javajmx/example/sample-javajmx-app.yaml
kubectl get vpa -n javajmx-sample

예시 출력:

NAME                       MODE   CPU   MEM         PROVIDED   AGE
goldilocks-tomcat-example  Off    15m   126805489   True       6m49s

초기에는 VPA 컴포넌트가 완전하지 않으면 PROVIDED=False로 보일 수 있습니다. 이 경우 VPA CRD/RBAC/컨트롤러 구성을 별도로 확인해야 합니다.

 

순정 VPA 설치

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
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./hack/vpa-up.sh
kubectl get pods -n kube-system | grep vpa
kubectl get crd | grep autoscaling

예시 출력:

vpa-admission-controller-5879bdc979-k5jvg   1/1   Running   0   35s
vpa-recommender-59c7769c67-g8wrb            1/1   Running   0   39s
vpa-updater-64888c8777-q7cf5                1/1   Running   0   41s

VPA 핵심 컴포넌트는 다음과 같습니다.

컴포넌트역할

vpa-recommender 파드의 리소스 사용량을 분석해 적정 requests 값을 계산
vpa-updater 현재 값과 권장값 차이가 크면 파드를 재시작해 반영
vpa-admission-controller 새 파드 생성 시 권장값을 자동 적용

VPA 동작 확인

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

예시 출력:

NAME         MODE   CPU    MEM    PROVIDED   AGE
hamster-vpa  Auto   587m   250Mi  True       36s
kubectl describe pod | grep Requests: -A2
kubectl get events --sort-by=".metadata.creationTimestamp" | grep VPA

예시 이벤트:

Normal  EvictedByVPA  pod/hamster-7996dbb57-t8kvx               Pod was evicted by VPA Updater to apply resource recommendation.
Normal  EvictedPod    verticalpodautoscaler/hamster-vpa         VPA Updater evicted Pod hamster-7996dbb57-t8kvx to apply resource recommendation.

참고:

  • UpdateMode "Auto"는 deprecated 경고가 표시될 수 있습니다.
  • 최신 버전에서는 Recreate, Initial, InPlaceOrRecreate 같은 명시적인 모드를 사용하는 편이 좋습니다.

6. KEDA - Kubernetes Event-driven Autoscaling

KEDA 소개

 

 

KEDA는 특정 이벤트를 기반으로 Pod를 자동으로 스케일링하는 도구입니다.

  • HPA는 보통 CPU/메모리 같은 리소스 메트릭을 기준으로 동작합니다.
  • KEDA는 Kafka 메시지 수, SQS 큐 길이, Cron 스케줄 등 외부 이벤트를 기준으로 스케일링할 수 있습니다.
  • 이벤트가 없을 때 replica를 0까지 줄일 수 있어 비용 효율이 좋습니다.
  • 내부적으로는 HPA를 생성해서 동작하며, 사용자는 ScaledObject를 선언하면 됩니다.

KEDA 주요 컴포넌트:

컴포넌트역할

keda-operator 이벤트를 감지하고 Deployment를 0↔N으로 스케일
keda-operator-metrics-apiserver 외부 이벤트를 HPA가 읽을 수 있는 메트릭으로 변환
keda-admission-webhooks ScaledObject 설정 검증

metrics API 확인

먼저 metrics-server API가 정상 노출되는지 확인합니다.

kubectl get --raw "/apis/metrics.k8s.io" | jq

예시 출력:

{
  "kind": "APIGroup",
  "apiVersion": "v1",
  "name": "metrics.k8s.io",
  "versions": [
    {
      "groupVersion": "metrics.k8s.io/v1beta1",
      "version": "v1beta1"
    }
  ],
  "preferredVersion": {
    "groupVersion": "metrics.k8s.io/v1beta1",
    "version": "v1beta1"
  }
}

참고:

# CPU/Memory 메트릭은 기존 metrics-server가 담당하고,
# KEDA metrics-server는 외부 이벤트 소스 메트릭을 노출합니다.
# https://keda.sh/docs/2.16/operate/metrics-server/

KEDA 설치

helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda \
  --version 2.16.0 \
  --namespace keda \
  --create-namespace \
  -f keda-values.yaml
kubectl get crd | grep keda
kubectl get all -n keda
kubectl get validatingwebhookconfigurations keda-admission-webhooks -o yaml | kubectl neat
kubectl get podmonitor,servicemonitors -n keda
kubectl get apiservice v1beta1.external.metrics.k8s.io -o yaml

확인 포인트:

  • scaledobjects.keda.sh, scaledjobs.keda.sh 같은 CRD가 생성되어야 합니다.
  • keda-operator, keda-operator-metrics-apiserver, keda-admission-webhooks Pod가 모두 Running이어야 합니다.
  • v1beta1.external.metrics.k8s.io APIService 상태가 Available=True여야 합니다.

 

Cron 기반 ScaledObject 실습

먼저 keda 네임스페이스에 샘플 애플리케이션을 배포합니다.

kubectl apply -f https://k8s.io/examples/application/php-apache.yaml -n keda
kubectl get pod -n keda

이제 Cron 기반 ScaledObject를 생성합니다.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: php-apache-cron-scaled
spec:
  minReplicaCount: 0
  maxReplicaCount: 2
  pollingInterval: 30
  cooldownPeriod: 300
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  triggers:
    - type: cron
      metadata:
        timezone: Asia/Seoul
        start: "0,15,30,45 * * * *"
        end: "5,20,35,50 * * * *"
        desiredReplicas: "1"
kubectl apply -f keda-cron.yaml -n keda
watch -d 'kubectl get ScaledObject,hpa,pod -n keda'

ScaledObject 상태를 실시간으로 보면 활성화 구간에서 ACTIVE=True로 바뀌고, KEDA가 자동으로 HPA를 생성하는 것을 볼 수 있습니다.

kubectl get ScaledObject -n keda -w
NAME                     SCALETARGETKIND      SCALETARGETNAME   MIN   MAX   READY   ACTIVE    FALLBACK   PAUSED    TRIGGERS   AUTHENTICATIONS   AGE
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    Unknown   False      Unknown                                17s
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    Unknown   False      Unknown                                30s
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    True      False      Unknown                                30s
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    True      False      Unknown                                60s
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    False     False      Unknown                                90s


kubectl get ScaledObject,hpa,pod -n keda
NAME                                          SCALETARGETKIND      SCALETARGETNAME   MIN   MAX   READY   ACTIVE   FALLBACK   PAUSED    TRIGGERS   AUTHENTICATIONS   AGE
scaledobject.keda.sh/php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    False    False      Unknown                 


kubectl get hpa -o jsonpath="{.items[0].spec}" -n keda | jq
{
  "maxReplicas": 2,
  "metrics": [
    {
      "external": {
        "metric": {
          "name": "s0-cron-Asia-Seoul-00,15,30,45xxxx-05,20,35,50xxxx",
          "selector": {
            "matchLabels": {
              "scaledobject.keda.sh/name": "php-apache-cron-scaled"
            }
          }
        },
        "target": {
          "averageValue": "1",
          "type": "AverageValue"
        }
      },
      "type": "External"
    }
  ],
  "minReplicas": 1,
  "scaleTargetRef": {
    "apiVersion": "apps/v1",
    "kind": "Deployment",
    "name": "php-apache"
  }
}

예시 출력:

NAME                     SCALETARGETKIND        SCALETARGETNAME   MIN   MAX   READY   ACTIVE   FALLBACK   PAUSED   TRIGGERS   AUTHENTICATIONS   AGE
php-apache-cron-scaled   apps/v1.Deployment     php-apache        0     2     True    True     False      Unknown  cron       Unknown           30s
{
  "maxReplicas": 2,
  "metrics": [
    {
      "external": {
        "metric": {
          "name": "s0-cron-Asia-Seoul-00,15,30,45xxxx-05,20,35,50xxxx",
          "selector": {
            "matchLabels": {
              "scaledobject.keda.sh/name": "php-apache-cron-scaled"
            }
          }
        },
        "target": {
          "averageValue": "1",
          "type": "AverageValue"
        }
      },
      "type": "External"
    }
  ],
  "minReplicas": 1,
  "scaleTargetRef": {
    "apiVersion": "apps/v1",
    "kind": "Deployment",
    "name": "php-apache"
  }
}

정리:

  • 사용자는 ScaledObject만 선언하지만, 실제 스케일링은 KEDA가 만든 HPA가 수행합니다.
  • HPA와 달리 외부 이벤트를 직접 트리거로 사용할 수 있습니다.
  • idle 상태에서는 replica를 0까지 내릴 수 있다는 점이 큰 차이입니다.

 

정리 후 삭제:

kubectl delete scaledobject php-apache-cron-scaled -n keda
kubectl delete deploy,svc php-apache -n keda
helm uninstall keda -n keda

7 Cluster Proportional Autoscaler (CPA)

설치

helm upgrade --install cluster-proportional-autoscaler \
  cluster-proportional-autoscaler/cluster-proportional-autoscaler

테스트용 Deployment를 하나 준비합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          resources:
            limits:
              cpu: "100m"
              memory: "64Mi"
            requests:
              cpu: "100m"
              memory: "64Mi"
          ports:
            - containerPort: 80
kubectl apply -f cpa-nginx.yaml

CPA 값은 다음처럼 정의할 수 있습니다.

config:
  ladder:
    nodesToReplicas:
      - [1, 1]
      - [2, 2]
      - [3, 3]
      - [4, 3]
      - [5, 5]
options:
  namespace: default
  target: "deployment/nginx-deployment"
helm upgrade --install cluster-proportional-autoscaler \
  cluster-proportional-autoscaler/cluster-proportional-autoscaler \
  -f cpa-values.yaml \
  --namespace kube-system
kubectl describe cm cluster-proportional-autoscaler -n kube-system

노드 수를 늘려서 Deployment replica 변화도 확인할 수 있습니다.

export ASG_NAME=$(aws autoscaling describe-auto-scaling-groups \
  --query "AutoScalingGroups[].AutoScalingGroupName" \
  --output text | tr '\t' '\n' | grep "myeks")

aws autoscaling update-auto-scaling-group \
  --auto-scaling-group-name "${ASG_NAME}" \
  --min-size 5 \
  --desired-capacity 5 \
  --max-size 5
aws autoscaling describe-auto-scaling-groups \
  --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize, DesiredCapacity]" \
  --output table
kubectl get pods -o wide
NAME                                READY   STATUS    RESTARTS   AGE     IP              NODE                                                NOMINATED NODE   READINESS GATES
hamster-7996dbb57-tcgjq             1/1     Running   0          41m     192.168.21.13   ip-192-168-21-237.ap-northeast-2.compute.internal   <none>           <none>
hamster-7996dbb57-z58wf             1/1     Running   0          42m     192.168.12.81   ip-192-168-14-65.ap-northeast-2.compute.internal    <none>           <none>
nginx-deployment-57f959d4d7-cvkvd   1/1     Running   0          6m21s   192.168.14.4    ip-192-168-14-236.ap-northeast-2.compute.internal   <none>           <none>
nginx-deployment-57f959d4d7-k8wsp   1/1     Running   0          4m31s   192.168.22.55   ip-192-168-21-237.ap-northeast-2.compute.internal   <none>           <none>
nginx-deployment-57f959d4d7-wtfkj   1/1     Running   0          4m31s   192.168.14.92   ip-192-168-14-65.ap-northeast-2.compute.internal    <none>           <none>

예시 결과:

  • 노드 수 변화에 맞춰 nginx-deployment replica가 3개로 늘어났습니다.
  • 각 Pod가 서로 다른 노드에 분산된 것도 함께 확인할 수 있습니다.

정리 후 삭제:

helm uninstall cluster-proportional-autoscaler -n kube-system
kubectl delete -f cpa-nginx.yaml

참고:

  • CPU/Memory 기반 정책도 구성할 수 있습니다.
coresToReplicas:
  - [1, 1]
  - [64, 3]
  - [512, 5]
  - [1024, 7]
  - [2048, 10]
  - [4096, 15]