[CloudNeta] EKS 워크샵 스터디 (3) - EKS Scaling Part 2 - Pod 스케일링 (HPA, VPA, KEDA, CPA)

이번 게시글에서는 EKS 워크샵 스터디 제 3주차 내용을 작성합니다.

이 글은 3부로 나누어집니다.

  1. 3주차 - EKS Scaling Part 1 - 실습 환경 구성과 관리형 노드그룹
  2. 3주차 - EKS Scaling Part 2 - Pod 스케일링 (HPA, VPA, KEDA, CPA) (현재 보고계신 글)
  3. 3주차 - EKS Scaling Part 3 - Node 스케일링 (CAS, Karpenter, Fargate)

Scaling Up 소개

Kubernetes Patterns 2nd edition 에는 다양한 스케일 업 배포기법이 나옵니다.

AWS는 Cluster Autoscaler로 아래 요소를 제공합니다.

이를 그림으로 도식화하면 아래와 같습니다(출처):

나에게 맞는 배포방안은 뭐지?

이를 생각하기 위해선, 설계중인 워크로드에 정말 초고속 반응형 스케일링이 필요한지 생각합니다. 필요한 방법이라면 어떻게든 도입해야하지만, 그렇지않으면 고스란히 기술부채입니다. 대부분의 워크로드에서는 아래 접근법 2-4가 더 비용효율적 입니다.

접근법 핵심 전략 E2E 스케일링 시간 월 추가 비용 (28개 클러스터) 복잡도 적합한 워크로드
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 매우 낮음 안정적인 트래픽
graph TB
    START[트래픽 급증 시
사용자 에러 발생] --> Q1{트래픽 패턴이
예측 가능한가?} Q1 -->|Yes| PRED[접근법 2: 예측형 스케일링
CronHPA + Predictive Scaling] Q1 -->|No| Q2{요청을 즉시
처리해야 하는가?} Q2 -->|대기 가능| ARCH[접근법 3: 아키텍처적 복원력
큐 기반 버퍼링 + Rate Limiting] Q2 -->|즉시 처리 필수| Q3{기본 용량을
늘릴 수 있는가?} Q3 -->|Yes| BASE[접근법 4: 적정 기본 용량
피크 70-80%로 기본 운영] Q3 -->|비용 제약| REACTIVE[접근법 1: 반응형 스케일링 고속화
Karpenter + KEDA + Warm Pool] PRED --> COMBINE[실무: 2-3개 접근법 조합 적용] ARCH --> COMBINE BASE --> COMBINE REACTIVE --> COMBINE style PRED fill:#059669,stroke:#232f3e,stroke-width:2px style ARCH fill:#3b82f6,stroke:#232f3e,stroke-width:2px style BASE fill:#8b5cf6,stroke:#232f3e,stroke-width:2px style REACTIVE fill:#f59e0b,stroke:#232f3e,stroke-width:2px style COMBINE fill:#1f2937,color:#fff,stroke:#232f3e,stroke-width:2px

살펴보기 - EKS Best Practices Guide

Cluster Autoscaling - Karpenter

Reliability - Applications

Scalability - Kubernetes Scaling Theory

Scalability - Node and Workload Efficiency

HPA - Horizontal Pod Autoscaler

HPA란?

실습

실습 앱을 배포해서 확인해봅니다.

php-apache 앱 배포해보기

아래와 같이 배포하고, 잘 동작하나 확인해봅시다.

# 배포해보고
$ k apply -f hpa-test.yaml
deployment.apps/php-apache created
service/php-apache created

# 잘 나온지도 봅니다.
$ k exec -ti deploy/php-apache -- cat /var/www/html/index.php
<?php
$x = 0.0001;
for ($i = 0; $i <= 1000000; $i++) {
        $x += sqrt($x);
}
echo "OK!";
?>

cURL 파드를 배포하고, 부하를 발생해서 CPU가 증가하는지 살펴봅시다.

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

# 단발성으로 테스트하면?
$ kubectl exec -it curl -- curl php-apache

# 순간적으로 튀는 게 보입니다
 k exec -ti deploy/php-apache -- top
top - 06:03:36 up 14:58,  0 users,  load average: 0.44, 0.21, 0.13
Tasks:   7 total,   1 running,   6 sleeping,   0 stopped,   0 zombie
%Cpu(s):  4.3 us,  0.7 sy,  0.0 ni, 94.8 id,  0.0 wa,  0.0 hi,  0.0 si,  0.2 st
KiB Mem:   3926448 total,  3666240 used,   260208 free,      844 buffers
KiB Swap:        0 total,        0 used,        0 free.  2714120 cached Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
     10 www-data  20   0  166348  10824   5472 S   2.7  0.3   0:00.08 apache2
      1 root      20   0  166268  19132  13860 S   0.0  0.5   0:00.09 apache2
      9 www-data  20   0  166348  10824   5472 S   0.0  0.3   0:00.08 apache2
     11 www-data  20   0  166292   7456   2172 S   0.0  0.2   0:00.00 apache2
     12 www-data  20   0  166292   7456   2172 S   0.0  0.2   0:00.00 apache2
     13 www-data  20   0  166292   7456   2172 S   0.0  0.2   0:00.00 apache2
     20 root      20   0   21920   2464   2068 R   0.0  0.1   0:00.05 top

# 그러면 이제 여러 워커로 동시에 호출해보죠
$ 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'

# 많이 발생해서 부하가 올라가는 건 보입니다.
top - 06:05:14 up 15:00,  0 users,  load average: 9.78, 2.75, 0.99
Tasks: 152 total, 151 running,   1 sleeping,   0 stopped,   0 zombie
%Cpu(s): 33.8 us,  0.8 sy,  0.0 ni, 65.2 id,  0.0 wa,  0.0 hi,  0.1 si,  0.0 st
KiB Mem:   3926448 total,  3807496 used,   118952 free,      844 buffers
KiB Swap:        0 total,        0 used,        0 free.  2714140 cached Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    118 www-data  20   0  166324  10584   5248 R   1.0  0.3   0:00.12 apache2
...
    121 www-data  20   0  166324  10584   5248 R   0.5  0.3   0:00.06 apache2
    123 www-data  20   0  166324  10584   5248 R   0.5  0.3   0:00.05 apache2

HPA 정책 생성 후 테스트해보기

아래와 같이 생성 후 hpa를 확인해봅시다.

$ k apply -f hpa-policy.yaml 
horizontalpodautoscaler.autoscaling/php-apache created

$ k describe hpa
Name:                                                  php-apache
Namespace:                                             default
Labels:                                                <none>
Annotations:                                           <none>
CreationTimestamp:                                     Thu, 02 Apr 2026 15:08:11 +0900
Reference:                                             Deployment/php-apache
Metrics:                                               ( current / target )
  resource cpu on pods  (as a percentage of request):  3% (6m) / 50%
Min replicas:                                          1
Max replicas:                                          10
Deployment pods:                                       1 current / 1 desired
Conditions:
  Type            Status  Reason              Message
  ----            ------  ------              -------
  AbleToScale     True    ReadyForNewScale    recommended size matches current size
  ScalingActive   True    ValidMetricFound    the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)
  ScalingLimited  False   DesiredWithinRange  the desired count is within the acceptable range
Events:           <none>

이후 테스트를 위해 부하를 줘봅시다:

$ k exec curl -- sh -c 'while true; do curl -s php-apache; sleep 0.01; done'

# 파드도 성공적으로 늘어나있군요
$ k get pods
NAME                          READY   STATUS    RESTARTS   AGE
awscli-pod-847bcdb4db-b76p2   1/1     Running   0          12h
awscli-pod-847bcdb4db-htkws   1/1     Running   0          12h
curl                          1/1     Running   0          16m
php-apache-7ff755b4cb-2hjt6   1/1     Running   0          2m41s
php-apache-7ff755b4cb-cxdft   1/1     Running   0          3m41s
php-apache-7ff755b4cb-jb5ph   1/1     Running   0          3m41s
php-apache-7ff755b4cb-mqv8x   1/1     Running   0          2m41s
php-apache-7ff755b4cb-nzfl5   1/1     Running   0          20m
php-apache-7ff755b4cb-qw5zq   1/1     Running   0          3m27s
php-apache-7ff755b4cb-rdfcp   1/1     Running   0          116s
php-apache-7ff755b4cb-wmzdn   1/1     Running   0          3m41s
php-apache-7ff755b4cb-xlssp   1/1     Running   0          2m41s
php-apache-7ff755b4cb-z8v2d   1/1     Running   0          2m41s

# 오른것도, 내려간것도 확인완료입니다.
Events:
  Type    Reason             Age    From                       Message
  ----    ------             ----   ----                       -------
  Normal  SuccessfulRescale  17m   horizontal-pod-autoscaler  New size: 4; reason: cpu resource utilization (percentage of request) above target
  Normal  SuccessfulRescale  17m   horizontal-pod-autoscaler  New size: 5; reason: cpu resource utilization (percentage of request) above target
  Normal  SuccessfulRescale  16m   horizontal-pod-autoscaler  New size: 9; reason: cpu resource utilization (percentage of request) above target
  Normal  SuccessfulRescale  15m   horizontal-pod-autoscaler  New size: 10; reason: cpu resource utilization (percentage of request) above target
  Normal  SuccessfulRescale  16s   horizontal-pod-autoscaler  New size: 2; reason: All metrics below target
  Normal  SuccessfulRescale  1s    horizontal-pod-autoscaler  New size: 1; reason: All metrics below target

HPA 부하 테스트 - CPU Utilization/Threshold 및 Replica 수 변화

HPA Status Condition - AbleToScale, ScalingActive, ScalingLimited 상태 확인

HPA의 주요 제약사항

  1. 평균의 함정: 특정 파드만 100% 시 probe 실패 후 restart 발생
    → 나머지 파드 부하 후 반복
    ⇒ 다수 파드 종료로 과도한 증설 발생, 기간 동안 장애가 일어날 수 있습니다.
  2. 증설까지의 시간: 스파이크성 트래픽에는 대응이 어렵습니다
  3. 파드 App 초기화 동작 시 CPU/Mem 튀는 현상에도 대응이 필요합니다 ⇒ Kube Startup CPU Boost 기법 활용 가능!

VPA - Vertical Pod Autoscaler

VPA에 대해 알아봅시다.

VPA란?

pod resources.request을 최대한 최적값으로 수정합니다 ⇒ 악분님 블로그에서 기본 소개

HPA vs VPA 비교

HPA는 Pod 를 늘리고(수평), VPA는 Pod 리소스 크기(CPU/Memory request/limit)를 키웁니다(수직).

HPA VPA
확장 방향 수평 — Pod 수 증감 수직 — Pod 리소스 크기 증감
예시 1 Pod(1 CPU) → 3 Pod(1 CPU씩) 1 Pod(1 CPU) → 1 Pod(2 CPU)
Pod 재시작 없음 (새 Pod 추가) 있음 (Pod Evict 후 새로 생성, 다운타임 발생)
적합한 워크로드 Stateless 웹 서비스 리소스 요구량이 가변적인 앱

VPA 제한사항과 주의점

제한사항 설명 대응
Pod 재시작 필요 실행 중인 Pod의 리소스를 in-place 변경 불가. Evict 후 재생성 PodDisruptionBudget 설정 필수
JVM 힙 사이즈 불일치 JAVA_OPTS에 힙 크기를 고정하면 VPA가 메모리를 변경해도 JVM은 기존 힙 사용 → 리소스 낭비 힙 크기를 비율(%)로 설정하거나 동적으로 계산
StatefulSet 주의 순차적 재시작으로 데이터 손실 위험 Initial 모드만 사용 권장
Metrics Server 의존 Metrics Server 장애 시 권장사항 업데이트 중단 Metrics Server HA 구성
권장사항 계산 시간 최소 24시간 데이터 필요. 트래픽 패턴 변화 반영에 시간 소요 충분한 관찰 기간 확보

VPA와 HPA 공존 전략

VPA와 HPA를 동일 메트릭(CPU 등)으로 동시에 사용하면 충돌이 발생합니다. 공존이 필요한 경우 아래 패턴을 사용합니다.

패턴 구성 설명
패턴 1 (권장) VPA Off + HPA VPA는 권장사항 조회 용도로만, 실제 스케일링은 HPA
패턴 2 (권장) VPA Memory + HPA CPU 메트릭을 분리하여 충돌 방지
패턴 3 (고급) VPA + HPA + Custom Metrics 큐 기반 워크로드 등 특수한 경우
In-Place Pod Vertical Scaling

Kubernetes 1.35(2025.12)에서 GA가 되었습니다. Pod를 재시작하지 않고 CPU/Memory request/limit을 변경할 수 있어, VPA의 가장 큰 단점인 Pod 재시작 문제가 해결됩니다. VPA도 이를 활용하는 InPlaceOrRecreate 업데이트 모드가 beta로 제공되고 있습니다.

참고:

참고사항 - KRR

Feature 🛠️ Robusta KRR 🚀 Kubernetes VPA 🌐
Resource Recommendations 💡 ✅ CPU/Memory requests and limits ✅ CPU/Memory requests and limits
Installation Location 🌍 ✅ Not required to be installed inside the cluster, can be used on your own device, connected to a cluster ❌ Must be installed inside the cluster
Workload Configuration 🔧 ✅ No need to configure a VPA object for each workload ❌ Requires VPA object configuration for each workload
Immediate Results ⚡ ✅ Gets results immediately (given Prometheus is running) ❌ Requires time to gather data and provide recommendations
Reporting 📊 ✅ Detailed CLI Report, web UI in Robusta.dev ❌ Not supported
Extensibility 🔧 ✅ Add your own strategies with few lines of Python ⚠️ Limited extensibility
Custom Metrics 📏 🔄 Support in future versions ❌ Not supported
Custom Resources 🎛️ 🔄 Support in future versions (e.g., GPU) ❌ Not supported
Explainability 📖 🔄 Support in future versions (Robusta will send you additional graphs) ❌ Not supported
Autoscaling 🔀 🔄 Support in future versions ✅ Automatic application of recommendations

KEDA - Kubernetes Event-driven Autoscaling

KEDA란?

KEDA(Kubernetes Event-Driven Autoscaling)는 CNCF 졸업 프로젝트로, HPA를 대체하는 게 아니라 확장합니다. KEDA가 외부 이벤트 소스에서 메트릭을 가져와 Kubernetes Metrics API로 노출하면, 그 뒤는 기존 HPA가 스케일링을 수행합니다.

동작 흐름

flowchart TD
    A[ScaledObject 생성] --> B[Admission Webhook 검증]
    B --> C[KEDA가 자동으로 HPA 리소스 생성]
    C --> D[HPA가 KEDA Metrics Server를 참조]
    D --> E[KEDA Metrics Server가\n외부 이벤트 소스에서 메트릭 수집]
    E --> F{이벤트 존재?}
    F -- Yes --> G[HPA가 메트릭 기반으로\nPod 스케일 아웃]
    F -- No --> H[keda-operator가\nreplicas=0으로 스케일 다운]
    H -- 이벤트 재발생 --> I[keda-operator가\nreplicas=1로 활성화]
    I --> D

    style A fill:#4a9eff,color:#fff
    style F fill:#ffb347,color:#000
    style G fill:#77dd77,color:#000
    style H fill:#ff6b6b,color:#fff
ScaledObject를 사용하면 KEDA가 HPA를 자동 생성합니다. 별도로 HPA를 수동 생성하면 충돌이 발생합니다.

3개 컴포넌트

KEDA를 설치하면 3개의 컴포넌트가 배포됩니다.

kubectl get pod -n keda
NAME                                               READY   STATUS    RESTARTS      AGE
keda-operator-6bdffdc78-5rqnp                      1/1     Running   1 (11m ago)   11m
keda-operator-metrics-apiserver-74d844d769-2vrcq   1/1     Running   0             11m
keda-admission-webhooks-86cffccbf5-kmb7v           1/1     Running   0             11m
컴포넌트 역할
keda-operator Deployment를 감시하며 이벤트가 없으면 0으로 스케일 다운, 이벤트가 발생하면 활성화
keda-operator-metrics-apiserver 외부 이벤트 데이터(큐 길이, 스트림 lag 등)를 Kubernetes Metrics API로 노출 → HPA가 이 메트릭을 기반으로 스케일링
keda-admission-webhooks 리소스 변경을 검증하여 잘못된 설정 방지 (예: 같은 대상에 ScaledObject 중복 생성 차단)

ScaledObject — KEDA의 핵심 리소스

ScaledObject는 "어떤 Deployment를, 어떤 이벤트 소스 기준으로, 몇 개까지 스케일할 것인가"를 정의하는 CRD입니다.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: my-app-scaledobject
spec:
  scaleTargetRef:
    name: my-app-deployment # 스케일링 대상 Deployment
  minReplicaCount: 0 # 이벤트 없으면 0까지 축소 가능
  maxReplicaCount: 10
  triggers: # 이벤트 소스 (복수 가능)
    - type: <scaler-type>
      metadata: ...

70개 이상의 Scaler 지원

KEDA의 핵심은 Scaler(트리거)입니다. 70개 이상의 이벤트 소스를 지원하며, 클라우드 벤더에 종속되지 않습니다.

카테고리 예시
메시지 큐 Apache Kafka, RabbitMQ, AWS SQS, Azure Service Bus, NATS
클라우드 AWS CloudWatch/DynamoDB/Kinesis, Azure Event Hubs, GCP Pub/Sub
데이터베이스 PostgreSQL, MySQL, MongoDB, Redis
모니터링 Prometheus, Datadog, New Relic
스케줄 Cron
기타 HTTP request rate, Kubernetes 리소스, 외부 API

Scaler 예시 (1) — Kafka topic lag

Kafka consumer group의 lag을 기준으로 스케일링합니다.

triggers:
  - type: kafka
    metadata:
      bootstrapServers: kafka.svc:9092
      consumerGroup: my-group
      topic: test-topic
      lagThreshold: "5" # lag이 평균 5 이상이면 스케일 아웃
      activationLagThreshold: "3" # lag이 3 이상이면 스케일러 활성화
      offsetResetPolicy: latest

Scaler 예시 (2) — AWS SQS 큐 길이

SQS 큐에 쌓인 메시지 수를 기준으로 스케일링합니다. EKS 환경에서 가장 흔한 패턴 중 하나입니다.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: aws-sqs-queue-scaledobject
spec:
  scaleTargetRef:
    name: my-worker
  minReplicaCount: 0 # 큐가 비면 0으로
  maxReplicaCount: 10
  triggers:
    - type: aws-sqs-queue
      authenticationRef:
        name: keda-trigger-auth-aws-credentials
      metadata:
        queueURL: https://sqs.ap-northeast-2.amazonaws.com/123456789012/my-queue
        queueLength: "5" # 메시지 5개당 Pod 1개
        awsRegion: ap-northeast-2
        identityOwner: operator # EKS 노드 IAM 역할 사용

identityOwner: operator 사용 시 노드의 IAM 역할에 sqs:GetQueueAttributes 권한이 필요합니다. 별도 인증이 필요하면 TriggerAuthentication을 사용합니다.

apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: keda-trigger-auth-aws-credentials
spec:
  secretTargetRef:
    - parameter: awsAccessKeyID
      name: aws-secret
      key: AWS_ACCESS_KEY_ID
    - parameter: awsSecretAccessKey
      name: aws-secret
      key: AWS_SECRET_ACCESS_KEY

Scaler 예시 (3) — Cron 스케줄 기반

업무 시간에만 Pod를 띄우고, 야간/주말에는 0으로 내리는 비용 절감 패턴입니다. - Cron Scaler 문서

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: office-hours-scaledobject
spec:
  scaleTargetRef:
    name: my-app-deployment
  minReplicaCount: 0
  maxReplicaCount: 5
  triggers:
    - type: cron
      metadata:
        timezone: Asia/Seoul
        start: 0 9 * * MON-FRI # 평일 09:00 시작
        end: 0 18 * * MON-FRI # 평일 18:00 종료
        desiredReplicas: "3" # 업무 시간에는 3개 유지

Cron과 CPU 트리거를 조합하면 더 유연해집니다. 업무 시간에는 최소 1개를 보장하되, 부하에 따라 추가 확장합니다.

triggers:
  - type: cron
    metadata:
      timezone: Asia/Seoul
      start: 0 9 * * MON-FRI
      end: 0 18 * * MON-FRI
      desiredReplicas: "1"
  - type: cpu
    metadata:
      value: "80"
HPA vs KEDA 선택 기준

  • CPU/Memory 기반 스케일링이면 → HPA로 충분
  • 큐 길이, 스트림 lag, cron 스케줄, DB 쿼리 결과 등 외부 이벤트 기반이면 → KEDA
  • KEDA는 0 → 1 스케일링(scale-to-zero)이 가능하다는 점도 HPA와의 차이
  • Node 오토스케일링(CAS/Karpenter)과 조합하면, Pod가 0 → N으로 늘어날 때 노드도 자동 확장되어 비용 최적화 가능

참고 자료

실습

특정 이벤트 기반의 파드 오토 스케일링을 테스트 해봅시다. 먼저 KEDA 대시보드 JSON을 다운로드 받고 import 합시다.

이어서 설치 전, 기본 메트릭 서버에서 제공하는 API를 확인합니다.

$ k get --raw "/apis/metrics.k8s.io" -v=6 | jq
I0402 16:20:12.552961  519022 cmd.go:527] kubectl command headers turned on
I0402 16:20:12.615132  519022 loader.go:402] Config loaded from file:  /home/l4in/.kube/config
I0402 16:20:12.615328  519022 envvar.go:172] "Feature gate default state" feature="InOrderInformers" enabled=true
I0402 16:20:12.615359  519022 envvar.go:172] "Feature gate default state" feature="InformerResourceVersion" enabled=false
I0402 16:20:12.615388  519022 envvar.go:172] "Feature gate default state" feature="WatchListClient" enabled=false
I0402 16:20:12.615414  519022 envvar.go:172] "Feature gate default state" feature="ClientsAllowCBOR" enabled=false
I0402 16:20:12.615439  519022 envvar.go:172] "Feature gate default state" feature="ClientsPreferCBOR" enabled=false
I0402 16:20:13.302832  519022 round_trippers.go:632] "Response" verb="GET" url="https://A17A8A7F84B06E0B18298D9D26F3D875.sk1.ap-northeast-2.eks.amazonaws.com/apis/metrics.k8s.io" status="200 OK" milliseconds=687
{
  "kind": "APIGroup",
  "apiVersion": "v1",
  "name": "metrics.k8s.io",
  "versions": [
    {
      "groupVersion": "metrics.k8s.io/v1beta1",
      "version": "v1beta1"
    }
  ],
  "preferredVersion": {
    "groupVersion": "metrics.k8s.io/v1beta1",
    "version": "v1beta1"
  }
}

KEDA 구성하기

KEDA를 설치하고, 아래와 같은 value를 포함하여 helm chart로 프로비저닝 해봅시다.

$ helm repo add kedacore https://kedacore.github.io/charts
"kedacore" has been added to your repositories

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "kedacore" chart repository
...Successfully got an update from the "eks" chart repository
...Successfully got an update from the "prometheus-community" chart repository
...Successfully got an update from the "geek-cookbook" chart repository
Update Complete. ⎈Happy Helming!⎈

$ helm install keda kedacore/keda --version 2.16.0 --namespace keda --create-namespace -f keda-values.yaml
NAME: keda
LAST DEPLOYED: Thu Apr  2 16:31:24 2026
NAMESPACE: keda
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
:::^.     .::::^:     :::::::::::::::    .:::::::::.                   .^.
7???~   .^7????~.     7??????????????.   :?????????77!^.              .7?7.
7???~  ^7???7~.       ~!!!!!!!!!!!!!!.   :????!!!!7????7~.           .7???7.
7???~^7????~.                            :????:    :~7???7.         :7?????7.
7???7????!.           ::::::::::::.      :????:      .7???!        :7??77???7.
7????????7:           7???????????~      :????:       :????:      :???7?5????7.
7????!~????^          !77777777777^      :????:       :????:     ^???7?#P7????7.
7???~  ^????~                            :????:      :7???!     ^???7J#@J7?????7.
7???~   :7???!.                          :????:   .:~7???!.    ~???7Y&@#7777????7.
7???~    .7???7:      !!!!!!!!!!!!!!!    :????7!!77????7^     ~??775@@@GJJYJ?????7.
7???~     .!????^     7?????????????7.   :?????????7!~:      !????G@@@@@@@@5??????7:
::::.       :::::     :::::::::::::::    .::::::::..        .::::JGGGB@@@&7:::::::::
                                                                      ?@@#~
                                                                      P@B^
                                                                    :&G:
                                                                    !5.
                                                                    .Kubernetes Event-driven Autoscaling (KEDA) - Application autoscaling made simple.

Get started by deploying Scaled Objects to your cluster:
    - Information about Scaled Objects : https://keda.sh/docs/latest/concepts/
    - Samples: https://github.com/kedacore/samples

Get information about the deployed ScaledObjects:
  kubectl get scaledobject [--namespace <namespace>]

Get details about a deployed ScaledObject:
  kubectl describe scaledobject <scaled-object-name> [--namespace <namespace>]

Get information about the deployed ScaledObjects:
  kubectl get triggerauthentication [--namespace <namespace>]

Get details about a deployed ScaledObject:
  kubectl describe triggerauthentication <trigger-authentication-name> [--namespace <namespace>]

Get an overview of the Horizontal Pod Autoscalers (HPA) that KEDA is using behind the scenes:
  kubectl get hpa [--all-namespaces] [--namespace <namespace>]

Learn more about KEDA:
- Documentation: https://keda.sh/
- Support: https://keda.sh/support/
- File an issue: https://github.com/kedacore/keda/issues/new/choose

$ kubectl get crd | grep keda
cloudeventsources.eventing.keda.sh              2026-04-02T07:31:26Z
clustercloudeventsources.eventing.keda.sh       2026-04-02T07:31:26Z
clustertriggerauthentications.keda.sh           2026-04-02T07:31:26Z
scaledjobs.keda.sh                              2026-04-02T07:31:26Z
scaledobjects.keda.sh                           2026-04-02T07:31:26Z
triggerauthentications.keda.sh                  2026-04-02T07:31:26Z

$ kubectl get all -n keda
NAME                                                   READY   STATUS    RESTARTS      AGE
pod/keda-admission-webhooks-56b6b85c57-gc8sp           1/1     Running   0             45s
pod/keda-operator-5c65c9c598-np4h4                     1/1     Running   1 (36s ago)   45s
pod/keda-operator-metrics-apiserver-554cb87754-vskdq   1/1     Running   0             45s

NAME                                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)             AGE
service/keda-admission-webhooks           ClusterIP   10.100.185.53    <none>        443/TCP,8020/TCP    46s
service/keda-operator                     ClusterIP   10.100.244.184   <none>        9666/TCP,8080/TCP   46s
service/keda-operator-metrics-apiserver   ClusterIP   10.100.208.233   <none>        443/TCP,9022/TCP    46s

NAME                                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/keda-admission-webhooks           1/1     1            1           45s
deployment.apps/keda-operator                     1/1     1            1           45s
deployment.apps/keda-operator-metrics-apiserver   1/1     1            1           45s

NAME                                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/keda-admission-webhooks-56b6b85c57           1         1         1       45s
replicaset.apps/keda-operator-5c65c9c598                     1         1         1       45s
replicaset.apps/keda-operator-metrics-apiserver-554cb87754   1         1         1       45s

배포는 되었으니 메트릭 서버에 KEDA 메트릭이 오는지 살펴봅시다.

$ k get pod -n keda -l app=keda-operator-metrics-apiserver
NAME                                               READY   STATUS    RESTARTS   AGE
keda-operator-metrics-apiserver-554cb87754-vskdq   1/1     Running   0          3m31s

$ k get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq
{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "external.metrics.k8s.io/v1beta1",
  "resources": [
    {
      "name": "externalmetrics",
      "singularName": "",
      "namespaced": true,
      "kind": "ExternalMetricValueList",
      "verbs": [
        "get"
      ]
    }
  ]
}

Deployment 생성 후 스케일 여부를 모니터링 해보기

php-apache 앱을 배포해보고 ScaledObject 정책을 생성합니다.

배포를 진행합니다.

$ k apply -f keda-cron.yaml -n keda
scaledobject.keda.sh/php-apache-cron-scaled created

# 설정상 15분마다 cron으로 구동되었음을 볼 수 있습니다.
$ k describe scaledobject php-apache-cron-scaled -n keda
Events:
  Type     Reason                    Age                 From           Message
  ----     ------                    ----                ----           -------
  Warning  ScaledObjectCheckFailed   29m (x12 over 29m)  keda-operator  ScaledObject doesn't have correct scaleTargetRef specification
  Warning  ScaledObjectCheckFailed   18m (x18 over 29m)  keda-operator  Target resource doesn't exist
  Normal   KEDAScalersStarted        7m24s               keda-operator  Scaler cron is built.
  Normal   KEDAScalersStarted        7m24s               keda-operator  Started scalers watch
  Normal   KEDAScaleTargetActivated  3m24s               keda-operator  Scaled apps/v1.Deployment keda/php-apache from 0 to 1, triggered by cronScaler

$ k 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"
  }
}

앞서 작성한 그라파나로 살펴보면 이런식으로 확인이 가능합니다.

KEDA Cron ScaledObject Grafana 대시보드 - 15분 주기 replica 변화 확인

CPA - Cluster Proportional Autoscaler

CPA란?

노드 수(또는 코어 수)에 비례하여 특정 애플리케이션의 Pod 수를 자동 조정하는 오토스케일러입니다. - Github Workshop

HPA/KEDA와 달리 메트릭을 보는 게 아니라, 클러스터 규모 자체를 기준으로 삼습니다.

대표적인 사용 사례는 CoreDNS입니다. 노드가 늘어나면 DNS 쿼리도 비례해서 증가하므로, CoreDNS Pod도 함께 늘려야 합니다.

스케일링 정책 — Ladder

nodesToReplicas로 "노드 N개일 때 Pod M개" 매핑 테이블을 정의합니다.

config:
  ladder:
    nodesToReplicas:
      - [1, 1] # 노드 1개 → Pod 1개
      - [2, 2] # 노드 2개 → Pod 2개
      - [3, 3] # 노드 3개 → Pod 3개
      - [4, 3] # 노드 4개 → Pod 3개 (유지)
      - [5, 5] # 노드 5개 → Pod 5개

노드 수 대신 CPU 코어 수 기반 정책도 가능합니다.

config:
  ladder:
    coresToReplicas:
      - [1, 1]
      - [64, 3]
      - [512, 5]
      - [1024, 7]
      - [2048, 10]
      - [4096, 15]

실습

CPA 설치

helm repo add cluster-proportional-autoscaler https://kubernetes-sigs.github.io/cluster-proportional-autoscaler
helm upgrade --install cluster-proportional-autoscaler cluster-proportional-autoscaler/cluster-proportional-autoscaler

테스트용 nginx 배포

cat <<EOF > cpa-nginx.yaml
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
EOF
kubectl apply -f cpa-nginx.yaml

CPA 규칙 적용

cat <<EOF > cpa-values.yaml
config:
  ladder:
    nodesToReplicas:
      - [1, 1]
      - [2, 2]
      - [3, 3]
      - [4, 3]
      - [5, 5]
options:
  namespace: default
  target: "deployment/nginx-deployment"
EOF

helm upgrade --install cluster-proportional-autoscaler \
  -f cpa-values.yaml \
  cluster-proportional-autoscaler/cluster-proportional-autoscaler

노드 수 변경 → Pod 수 자동 조정 확인

# 모니터링
watch -d kubectl get pod

# 노드 5개로 증가 → nginx Pod가 5개로 늘어나는지 확인
export ASG_NAME=$(aws autoscaling describe-auto-scaling-groups \
  --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].AutoScalingGroupName" \
  --output text)
aws autoscaling update-auto-scaling-group \
  --auto-scaling-group-name ${ASG_NAME} \
  --min-size 5 --desired-capacity 5 --max-size 5

# 노드 4개로 축소 → nginx Pod가 3개로 줄어나는지 확인
aws autoscaling update-auto-scaling-group \
  --auto-scaling-group-name ${ASG_NAME} \
  --min-size 4 --desired-capacity 4 --max-size 4

정리

helm uninstall cluster-proportional-autoscaler && kubectl delete -f cpa-nginx.yaml
참고 자료