K8s 중급 #6 오토스케일링 — HPA / VPA / Cluster Autoscaler
K8s 중급 시리즈의 여섯 번째 글입니다. #5까지는 단일 Pod의 자원과 건강 신호 모델을 정리한 흐름이었습니다. #4에서 한 Pod가 얼마만큼의 자원을 요청하고 어디까지 쓸 수 있는지 정했고, #5에서 그 Pod가 살아 있는지,트래픽을 받을 준비가 됐는지,천천히 뜨고 있는지를 세 종류의 probe로 표현했습니다. 단일 Pod의 모델까지는 거기서 마무리됩니다. 그러나 운영의 부하는 단일 Pod 모델 위에서 출렁입니다 — 점심시간 트래픽이 두 배로 뛰고, 새벽엔 1/10로 줄고, 마케팅 캠페인이 도는 하루는 하루 종일 평소의 다섯 배입니다. 이 변동을 사람이 매번 kubectl scale deployment ... --replicas=...로 따라가는 운영은 오래 가지 않습니다. 이번 글에서는 이 문제를 해결하는 세 차원의 자동 조정 — HPA / VPA / Cluster Autoscaler를 한 편에 정리하겠습니다.
이번 시리즈는 K8s 중급 7편입니다.
- #1 StatefulSet / DaemonSet / Job / CronJob — Deployment가 아닌 다른 컨트롤러들
- #2 PV / PVC / StorageClass — 영속 데이터 모델
- #3 Ingress와 Ingress Controller — 외부 진입점
- #4 resources.requests / limits — Pod의 자원 요청과 상한
- #5 Health check — liveness / readiness / startup probe
- #6 오토스케일링 — HPA / VPA / Cluster Autoscaler ← 이번 글
- #7 RBAC / NetworkPolicy / ResourceQuota — 보안과 자원 정책
오토스케일링이 풀어 주는 것 #
운영 클러스터에서 부하가 변동하는 패턴은 보통 다음 셋 중 하나입니다 — 시간대별 변동(낮,밤), 이벤트성 폭증(캠페인,세일,뉴스), 그리고 워크로드 추가에 따른 누적 증가. 사람이 손으로 맞추는 운영은 보통 다음 단계를 거쳐 한계에 도달합니다.
- 처음에는
replicas를 넉넉하게 잡아 두면 충분합니다. 평소의 두 배쯤을 항상 띄워 둡니다. - 시간이 지나면 그 “넉넉한 값"이 어떤 시간대엔 부족하고 어떤 시간대엔 낭비라는 걸 알게 됩니다. 비용도 자원도 양쪽에서 새고 있습니다.
- 누군가 평일 낮,밤,주말 세 슬롯에 다른
replicas를 적용하는 스케줄을 만듭니다. 한동안은 굴러갑니다. - 캠페인이 들어오거나 외부 트래픽이 튀는 사고가 한 번 나면 사람이 새벽에 일어나
kubectl scale을 칩니다. 곧 그 일이 반복됩니다.
K8s가 이 문제를 표현하는 방식이 세 차원의 오토스케일러입니다. 각자 다른 축을 자동으로 조정합니다.
| 오토스케일러 | 무엇을 조정 | 신호 | 대상 |
|---|---|---|---|
| HPA (Horizontal Pod Autoscaler) | Pod 개수 (replicas) | CPU,메모리 사용률, custom metric | Deployment / StatefulSet |
| VPA (Vertical Pod Autoscaler) | Pod의 자원 요청,상한 (requests / limits) | 과거 CPU,메모리 사용 추세 | Deployment / StatefulSet |
| Cluster Autoscaler (CA) | 노드 개수 | Pending 상태의 Pod, 비어 있는 노드 | 클라우드의 노드 그룹(ASG / MIG / VMSS) |
세 축이 서로 보완 관계라는 점이 중요합니다. HPA가 Pod를 늘려도 노드에 여유가 없으면 새 Pod는 Pending에 멈춥니다. 그때 CA가 노드를 더 띄워 줍니다. VPA는 별개의 사이클에서 “이 워크로드는 사실 메모리를 1Gi 정도 필요로 한다"는 사실을 권장값으로 알려 줍니다. 셋이 같이 굴러갈 때 비로소 부하 변동이 사람의 개입 없이 흡수됩니다.
metrics-server라는 전제 #
오토스케일링이 굴러가려면 클러스터 안에 현재 자원 사용량을 알려 주는 컴포넌트가 있어야 합니다. K8s 본체는 그 메트릭을 직접 들고 있지 않습니다. 대신 표준화된 인터페이스(metrics.k8s.io)를 제공하고, 그 인터페이스를 채우는 컴포넌트를 클러스터에 따로 설치해 두는 모양입니다. 가장 흔한 구현이 metrics-server입니다.
metrics-server는 클러스터의 각 노드에서 kubelet의 /metrics/resource 엔드포인트를 주기적으로 긁어 노드와 Pod의 CPU,메모리 사용량을 메모리에 들고 있습니다. 그 수치를 kubectl top이나 HPA 컨트롤러가 API로 조회합니다.
kubectl top nodes
kubectl top pods -ANAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
node-1 450m 22% 1.8Gi 45%
node-2 320m 16% 1.5Gi 37%수치가 보이면 metrics-server가 살아 있는 것이고, error: Metrics API not available 같은 메시지가 나오면 설치되어 있지 않거나 죽어 있는 것입니다. 환경별 설치 방법은 다음과 같습니다.
| 환경 | metrics-server 상태 |
|---|---|
| minikube | minikube addons enable metrics-server 한 번으로 활성화 |
| kind | 직접 설치 필요 (kubectl apply -f 또는 Helm) |
| EKS | 직접 설치 필요. Helm 또는 공식 매니페스트 |
| GKE | 기본 활성화 |
| AKS | 기본 활성화 |
EKS는 운영 클러스터를 만든 직후에는 metrics-server가 빠져 있습니다. HPA,VPA를 쓰려면 가장 먼저 설치해야 하는 컴포넌트입니다. CPU,메모리 외에 큐 길이,요청 수 같은 custom metric으로 HPA를 굴리고 싶다면 metrics-server 대신(또는 같이) Prometheus와 Prometheus Adapter, KEDA 같은 컴포넌트가 그 역할을 맡습니다 — 뒤에서 다시 보겠습니다.
HPA — Pod 개수를 자동으로 조정 #
가장 자주 쓰이고 가장 먼저 도입하는 오토스케일러가 HPA입니다. replicas 필드를 사람이 적는 대신, 메트릭의 평균값을 보고 K8s가 자동으로 채워 주는 모델입니다.
HPA 매니페스트 — CPU 기준 #
가장 단순한 모양은 CPU 사용률 기준입니다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70핵심 필드를 한 줄씩 짚어 두겠습니다.
apiVersion: autoscaling/v2— HPA의 현재 stable 버전입니다.v1은 CPU 한 가지만 다룰 수 있고 단일 메트릭에 묶여 있습니다.v2부터 multi-metric, custom metric, scale up,down 비대칭 동작(behavior)이 모두 가능합니다. 새 매니페스트는 거의 항상v2를 씁니다.scaleTargetRef— 어느 워크로드의replicas를 조정할지의 대상입니다. Deployment / StatefulSet / ReplicaSet에 붙일 수 있습니다. DaemonSet은 노드 개수에 묶여 있으므로 HPA의 대상이 아닙니다.minReplicas/maxReplicas— 자동 조정의 하한과 상한입니다. 운영에서 의도치 않게 0이나 수백 개로 가는 사고를 막는 안전망입니다. 하한을 1보다 크게 두면 가용성, 상한을 합리적으로 두면 비용,자원 보호가 됩니다.metrics— 어떤 신호를 보고 결정할지의 배열입니다. 위 예시는type: Resource(CPU,메모리 같은 표준 자원)에target.type: Utilization(사용률),averageUtilization: 70(평균 70%)입니다. 모든 Pod의 CPU 사용률 평균이 70%가 되도록replicas를 맞춥니다.
minReplicas를 1이 아니라 2 이상으로 두는 이유 한 가지를 짚어 두면 — 한 Pod가 죽거나 업데이트로 종료되는 순간에도 트래픽을 받을 다른 Pod가 있어야 한다는 가용성의 요구입니다. #5에서 readiness probe로 트래픽 진입을 통제했지만, 그 통제는 같은 Pod 안의 이야기입니다. Pod 자체가 없어지는 순간에는 다른 Pod가 대신 트래픽을 받아야 합니다.
HPA 알고리즘 — 비례식 한 줄 #
HPA가 새 replicas 값을 정하는 식은 단순합니다.
desiredReplicas = ceil( currentReplicas * (currentMetricValue / targetMetricValue) )말로 풀면 — 현재 평균이 목표의 몇 배인가를 보고 그 비율로 Pod 개수를 키웁니다. 예시로 보면 명료합니다.
| currentReplicas | currentMetric (CPU 평균) | targetMetric | 계산 | 새 replicas |
|---|---|---|---|---|
| 5 | 70% | 70% | 5 × 1.0 = 5 | 5 (변화 없음) |
| 5 | 140% | 70% | 5 × 2.0 = 10 | 10 |
| 5 | 35% | 70% | 5 × 0.5 = 2.5 → ceil | 3 |
| 10 | 105% | 70% | 10 × 1.5 = 15 | 15 |
분자에 들어가는 사용률(Utilization)의 정의가 중요합니다. CPU 사용률은 Pod의 requests 대비 비율입니다. 어떤 Pod가 requests.cpu: 500m을 들고 있고 실제로 700m을 쓰고 있다면 사용률은 140%입니다.
이 정의 때문에 #4와 직결되는 한 가지 함정이 생깁니다 — 워크로드에 resources.requests가 없으면 HPA의 Utilization 메트릭은 동작하지 않습니다. 분모가 정의되지 않기 때문입니다. HPA를 도입하기 전에 대상 Deployment에 CPU,메모리 requests가 들어가 있는지부터 확인해야 합니다. 이 체크를 빼먹으면 HPA가 unknown이나 <unknown> 상태에서 멈춰 있습니다.
requests 없이도 굴리고 싶다면 target.type을 Utilization 대신 AverageValue로 두고 절댓값(예: 200m)을 적는 길이 있습니다. 사용률이 아니라 절댓값 기준으로 비교합니다. 다만 이 모양은 흔하지 않고, 운영의 표준은 requests + Utilization입니다.
multi-metric — 여러 신호를 같이 본다 #
metrics 배열에 여러 항목을 넣으면 HPA는 각 메트릭마다 desired replicas를 따로 계산하고, 그 중 가장 큰 값을 채택합니다. CPU와 메모리를 동시에 보면 다음과 같습니다.
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80CPU 기준으로는 5개면 충분한데 메모리 기준으로는 8개가 필요한 상황이라면, HPA는 8개를 채택합니다. 둘 중 더 부담스러운 쪽에 맞추는 보수적인 선택입니다.
이 모양이 자주 의미를 가지는 워크로드는 메모리 캐시를 들고 있는 서비스입니다. CPU는 한가하지만 메모리가 차오르는 패턴이 보일 때 HPA가 그 신호를 놓치지 않게 하려면 메모리도 같이 봐야 합니다.
scale up과 scale down의 비대칭 — behavior #
HPA가 한 가지 비율로 매번 매끄럽게 조정해 주지는 않습니다. 그대로 두면 운영에서 두 가지 사고가 납니다 — 부하가 잠깐 떨어졌을 때 Pod를 너무 빨리 줄여서, 다시 부하가 올라올 때 cold start로 응답이 튀는 일. 그리고 부하가 잠깐 튀었을 때 Pod를 과하게 늘려서 자원과 비용이 새는 일. 이 둘을 다루는 필드가 behavior입니다.
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 4
periodSeconds: 15
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
selectPolicy: Max세 가지를 짚어 두겠습니다.
stabilizationWindowSeconds— 결정의 안정화 윈도우입니다. scale down의 기본값이 300초(5분)인 것이 운영의 핵심 안전장치입니다. CPU가 5분간 계속 낮은 값일 때만 진짜로 줄입니다. 잠깐 떨어졌다가 다시 올라오는 신호로는 줄이지 않습니다. scale up은 보통 0초로 두어 즉각 반응하게 둡니다.policies— 한 회차에 얼마만큼 변화시킬지의 정책입니다.Percent(현재 개수의 비율)와Pods(절대 개수) 두 종류이고,periodSeconds는 그 정책의 주기입니다. 위 예시는 scale up의 경우 15초마다 “현재의 100%(곱하기 2배) 또는 +4개” 둘 중 더 큰 변화를 허용합니다.selectPolicy: Max/Min— 여러 policy 중 어느 것을 채택할지.Max는 가장 공격적인 변화,Min은 가장 보수적인 변화입니다.
비대칭의 운영 의미를 한 줄로 줄이면 — 올릴 땐 빠르게, 내릴 땐 천천히입니다. 부하가 튀어 응답 시간이 늘어나는 사고는 사용자에게 즉각 보이지만, Pod가 한두 개 더 떠 있는 비용은 짧은 시간 동안은 미미합니다. 반면 너무 빨리 줄이면 다시 늘리는 동안 cold start가 발생해 사용자에게 그대로 보입니다. 이 비대칭을 behavior로 명시적으로 굳혀 두는 패턴이 운영의 표준입니다.
behavior 자체를 적지 않으면 K8s의 합리적인 기본값(scale up 즉각, scale down 5분 안정화)이 적용됩니다. 처음 도입할 때는 기본값으로 시작해도 되고, 워크로드의 특성에 맞춰 조정해 나가는 것이 보통의 흐름입니다.
HPA 적용과 동작 확인 #
kubectl apply -f hpa-cpu.yaml
kubectl get hpa
kubectl describe hpa webNAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web 55%/70% 2 20 4 5mTARGETS 컬럼의 55%/70%이 현재 평균/목표 값입니다. 이 값이 <unknown>/70%로 보이면 metrics-server가 살아 있지 않거나 대상 워크로드에 requests가 없는 것입니다. kubectl describe hpa의 이벤트 섹션에 FailedGetResourceMetric 같은 메시지가 같이 나옵니다.
부하 테스트로 동작을 한 번 확인해 보는 명령은 다음과 같습니다.
kubectl run load-gen --rm -it --image=busybox -- /bin/sh
# 컨테이너 안에서
while true; do wget -q -O- http://web.default.svc.cluster.local; done다른 터미널에서 kubectl get hpa -w로 보면 TARGETS이 70%를 넘어가는 시점부터 REPLICAS가 비례식대로 늘어가는 모양이 보입니다. 부하를 멈추면 5분쯤 뒤부터 천천히 줄어듭니다.
Custom metric과 KEDA — CPU,메모리 너머 #
CPU,메모리만으로는 충분히 표현되지 않는 워크로드가 있습니다.
- 큐 컨슈머 — SQS,Kafka,RabbitMQ에서 메시지를 받아 처리하는 워커. 큐의 길이가 진짜 신호이지 CPU가 아닙니다. 큐가 쌓이고 있어도 워커의 CPU는 한가할 수 있습니다.
- API 게이트웨이 — 초당 요청 수(RPS)나 동시 연결 수가 자원 사용보다 직접적인 신호.
- 이벤트 기반 워크로드 — 일이 있을 때만 도는 함수형 워크로드.
이 워크로드들에 HPA의 CPU 기준만 적용하면, 부하의 진짜 변화 시점보다 한 박자 늦거나 아예 신호를 놓칩니다.
Prometheus Adapter #
HPA가 CPU,메모리 외의 메트릭을 보게 하는 첫 번째 길은 Prometheus Adapter입니다. 클러스터에 Prometheus가 설치되어 있고 워크로드들이 메트릭을 노출하고 있다면, Prometheus Adapter는 그 Prometheus의 PromQL 결과를 K8s의 custom.metrics.k8s.io API로 노출해 줍니다. HPA가 그 메트릭을 표준 메트릭처럼 쓸 수 있게 됩니다.
매니페스트의 metrics 배열에 type: Pods 또는 type: External을 적어 어떤 PromQL의 결과를 볼지 표현합니다. 깊이 들어가는 부분은 K8s 고급 트랙으로 미루지만, 모양만 보여 두면 다음과 같습니다.
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "100"각 Pod의 평균 RPS가 100이 되도록 replicas를 맞춥니다. 메트릭의 정의(http_requests_per_second)는 Prometheus Adapter의 설정 파일에서 PromQL로 적습니다.
KEDA — 이벤트 기반 0→N #
KEDA(Kubernetes Event-Driven Autoscaling)는 한 단 더 나아간 컴포넌트입니다. HPA가 못 하는 두 가지를 풀어 줍니다.
- 0 → N 스케일링 — 표준 HPA의
minReplicas는 1 이상이어야 합니다. 일이 없을 때 Pod를 완전히 0으로 줄일 수 없습니다. KEDA는 큐가 비어 있는 시간 동안 워크로드를 0으로 줄이고, 새 메시지가 도착하면 1로 띄웁니다. 비용 측면에서 큰 차이가 됩니다. - 다양한 이벤트 소스 직접 연결 — SQS, Kafka, RabbitMQ, Redis Streams, PostgreSQL, Prometheus 등 50종 넘는 소스를 빌트인으로 지원합니다. Prometheus Adapter처럼 PromQL을 짜 두지 않아도 KEDA의
ScaledObject매니페스트 한 장으로 큐 길이 기반 스케일링이 됩니다.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: sqs-worker
spec:
scaleTargetRef:
name: sqs-worker
minReplicaCount: 0
maxReplicaCount: 30
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.ap-northeast-2.amazonaws.com/.../my-queue
queueLength: "10"
awsRegion: ap-northeast-2KEDA는 내부적으로 표준 HPA를 만들어 동작합니다 — ScaledObject가 적용되면 그에 대응하는 HPA가 자동으로 생성되고, KEDA가 외부 메트릭을 K8s 메트릭 API로 노출합니다. 표준 HPA 위에 한 층의 편의를 얹은 모양이라고 보면 됩니다. 큐 컨슈머나 이벤트 워커가 많은 클러스터에서는 거의 표준 도구가 되어 가고 있습니다.
VPA — Pod의 자원 요청을 자동으로 조정 #
HPA가 “Pod 몇 개"의 차원이라면 VPA는 “Pod 하나의 크기"의 차원입니다. #4에서 사람이 사용량 데이터를 보고 requests를 정하는 사이클을 다뤘습니다. VPA는 그 일을 자동화하려는 시도입니다 — 워크로드의 과거 CPU,메모리 사용 추세를 보고 권장값을 산출하고, 정책에 따라 그 값을 적용해 Pod를 재생성합니다.
세 컴포넌트 — recommender / updater / admission-controller #
VPA는 단일 컨트롤러가 아니라 세 컴포넌트의 묶음입니다.
| 컴포넌트 | 역할 |
|---|---|
| recommender | 메트릭을 모아 권장 requests 값을 산출. VPA 객체의 status.recommendation에 기록 |
| updater | recommender의 권장값과 현재 Pod의 값이 크게 어긋나면 Pod를 evict (재생성하게 함) |
| admission-controller | 새 Pod가 만들어질 때 mutating admission webhook으로 권장값을 주입 |
세 컴포넌트가 사이클을 만듭니다 — recommender가 권장값을 계산해 두고, updater가 큰 차이를 발견해 Pod를 죽이고, 새 Pod가 만들어질 때 admission-controller가 권장값을 적용한 매니페스트로 띄웁니다. 사람의 개입 없이 requests가 워크로드의 실제 사용량에 맞춰 갱신됩니다.
VPA 매니페스트와 updatePolicy #
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: web
namespace: default
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: web
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed:
cpu: 50m
memory: 64Mi
maxAllowed:
cpu: 2
memory: 4GiupdateMode의 세 값이 운영 의사결정의 핵심입니다.
| updateMode | 동작 |
|---|---|
"Off" | 권장값을 산출만 하고 적용은 안 함. 사람이 보고 매니페스트에 반영 |
"Initial" | Pod가 새로 만들어지는 시점에만 권장값을 적용. 이미 떠 있는 Pod는 그대로 |
"Auto" (= Recreate) | 큰 차이가 보이면 Pod를 evict하고 새 권장값으로 재생성 |
Auto가 자동화의 끝처럼 보이지만 운영에서는 신중해야 합니다. VPA가 Pod를 evict 한다는 건 그 Pod가 한 번 죽고 다시 떠야 한다는 뜻입니다. StatefulSet이나 단일 replica로 떠 있는 워크로드, 또는 #5의 startup probe 시간이 긴 워크로드에서 evict가 곧장 가용성에 영향을 미칩니다.
처음 VPA를 도입할 때는 거의 항상 "Off"로 두는 것이 표준 패턴입니다. 며칠,몇 주 동안 권장값을 모으고, 그 값이 합리적이라는 걸 확인한 뒤에야 Initial이나 Auto로 옮기거나, 아니면 그 권장값을 사람이 매니페스트에 반영해 commit 합니다.
kubectl describe vpa webStatus:
Recommendation:
Container Recommendations:
Container Name: web
Lower Bound:
Cpu: 150m
Memory: 256Mi
Target:
Cpu: 350m
Memory: 512Mi
Upper Bound:
Cpu: 800m
Memory: 1GiTarget이 핵심 권장값입니다. Lower Bound와 Upper Bound는 통계적 신뢰 구간이라고 보면 됩니다. 이 권장값이 현재 매니페스트의 requests와 크게 다르다면, 사람이 그 차이를 검토해 매니페스트에 반영하는 것이 운영의 보수적인 방식입니다.
resourcePolicy의 minAllowed / maxAllowed #
위 매니페스트의 resourcePolicy에서 minAllowed와 maxAllowed는 권장값의 상,하한입니다. 이 안전망이 없으면 VPA가 한가한 시간대의 값을 보고 requests를 너무 작게 권장하거나, 일시적인 메모리 누수 패턴을 보고 너무 크게 권장하는 사고가 납니다. 운영에서는 이 두 값을 거의 항상 적어 두는 편이 좋습니다.
VPA가 설치되어 있지 않은 클러스터 #
HPA의 metrics-server와 달리 VPA는 K8s 본체에 포함되어 있지 않습니다. EKS,GKE,AKS 모두 별도 설치가 필요합니다 — 보통 공식 GitHub의 매니페스트 또는 Helm chart로 깝니다. GKE만 관리형 옵션을 제공합니다.
HPA와 VPA의 충돌 — 같은 메트릭에 둘 다 걸지 말 것 #
운영에서 자주 보는 함정 한 가지를 짚어 두겠습니다. 같은 워크로드에 CPU 기준 HPA와 CPU 기준 VPA를 동시에 걸면 진동이 발생합니다. 이유는 단순합니다.
- CPU 부하가 올라갑니다. HPA가 비례식대로 Pod 개수를 늘립니다.
- Pod가 늘어났으니 Pod당 평균 CPU 사용량이 떨어집니다.
- VPA(Auto)가 그 떨어진 사용량을 보고 “requests를 줄여야겠다"고 판단합니다. 권장값을 낮추고 Pod를 재생성합니다.
- requests가 낮아졌으니 같은 사용량을 분모로 나눠도 사용률(
Utilization)은 다시 올라갑니다. HPA가 다시 Pod를 늘립니다.
진동이 멈추지 않는 사이클입니다. 회피 패턴은 두 가지입니다.
- HPA와 VPA의 메트릭을 분리 — 예를 들어 HPA는 CPU 기준, VPA는 메모리 기준. 두 사이클이 서로의 분모,분자를 흔들지 않습니다.
- VPA는
updateMode: "Off"로 — 권장값만 산출하고 자동 적용은 하지 않음. 사람이 검토해 매니페스트로 반영. HPA는 그대로 동작.
대부분의 운영 클러스터는 두 번째 패턴을 갑니다. HPA가 동적인 부하 조정을 책임지고, VPA는 권장값 도구로 두고 사람이 분기,분기에 한 번씩 매니페스트에 반영합니다. 이 분리가 가장 안전한 출발선입니다.
Cluster Autoscaler — 노드 차원의 조정 #
HPA가 Pod를 늘려도 그 Pod가 들어갈 노드 자원이 없으면 Pod는 Pending 상태에서 멈춥니다. #4에서 본 스케줄링 가능성의 식 — 노드의 allocatable에서 이미 예약된 requests 합을 뺀 여유분이 새 Pod의 requests 이상이어야 한다는 — 이 만족되지 않는 상태입니다. 이 상황을 해결하는 것이 Cluster Autoscaler입니다.
동작 모델 #
CA의 동작은 두 방향으로 단순합니다.
- scale up —
Pending상태의 Pod가 보이면, 그 Pod의requests를 받아 줄 만한 노드가 노드 그룹에 추가되도록 클라우드 API를 호출합니다. AWS라면 ASG의 desired capacity를 키우고, GCP라면 MIG, Azure라면 VMSS를 키웁니다. - scale down — 일정 시간 동안 사용률이 낮은 노드가 있으면 그 노드 위의 Pod를 다른 노드로 옮기고 노드를 종료합니다. 옮길 수 없는 Pod(예: PV가 그 노드에만 붙어 있는 경우,
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"어노테이션이 붙은 Pod)가 있으면 그 노드는 그대로 둡니다.
CA의 결정은 메트릭이 아니라 requests와 스케줄링 가능성에 기반합니다. 실제 사용량이 한가해도 requests 합이 노드를 다 채우고 있으면 새 Pod는 Pending이 되고, CA는 노드를 추가합니다. 이 모델이 #4의 “requests는 스케줄러의 진짜 통화"라는 표현과 정확히 같은 층입니다.
클라우드 환경별 노드 그룹 #
CA는 클라우드 provider와 짝으로 굴러갑니다. 환경별 매핑은 다음과 같습니다.
| 클라우드 | 노드 그룹 추상화 | 비고 |
|---|---|---|
| AWS EKS | Auto Scaling Group (ASG) 또는 EKS managed node group | 가용영역마다 별도 ASG 권장 |
| GCP GKE | Managed Instance Group (MIG) | 기본 활성화. GKE Autopilot은 노드 자체가 추상화됨 |
| Azure AKS | Virtual Machine Scale Set (VMSS) | AKS 클러스터 옵션으로 활성화 |
| 온프레미스 | Cluster API + provider | 환경에 따라 다름 |
EKS의 경우 CA는 보통 Helm chart로 깝니다. ASG에 적절한 태그를 붙여 CA가 그 ASG를 발견,관리하도록 하는 셋업이 한 번 필요합니다. GKE는 클러스터 생성 시 옵션 한 줄로 켜집니다.
Karpenter — EKS의 더 빠른 대안 #
CA의 설계는 “ASG에 desired capacity 한 칸을 더 요청 → 그 ASG의 launch template에 따라 노드가 만들어짐 → kubelet이 클러스터에 등록"의 사이클을 거칩니다. 노드 spec이 ASG에 미리 정해져 있다는 점이 제약입니다 — Pending Pod가 큰 메모리를 요구하는데 ASG의 인스턴스 타입이 작은 노드밖에 못 만들면, 그 노드를 띄워도 Pod가 여전히 Pending에 남는 사고가 납니다.
Karpenter는 EKS 환경에서 CA의 더 빠른 대안으로 정착하고 있습니다. Karpenter의 차이는 두 가지입니다.
- Pending Pod를 보고 노드 spec을 동적으로 결정 — 미리 정의된 ASG가 아니라, Pending Pod의
requests와 toleration에 가장 잘 맞는 인스턴스 타입을 그때그때 골라 EC2 API로 직접 띄웁니다. - 빠른 프로비저닝 — ASG 한 단계를 거치지 않으므로 노드가 떠서 클러스터에 합류하기까지의 시간이 보통 더 짧습니다.
EKS 신규 클러스터에서는 CA 대신 Karpenter를 도입하는 흐름이 늘고 있습니다. GKE,AKS의 동급 도구는 아직 EKS의 Karpenter만큼 정착되지는 않았습니다.
CA가 동작하지 않는 흔한 이유 #
CA가 의도대로 안 굴러가는 패턴 몇 가지를 짚어 두겠습니다.
- 노드에 cluster-autoscaler-related 태그가 없음 — AWS의 경우 ASG에
k8s.io/cluster-autoscaler/enabled같은 태그가 있어야 CA가 그 ASG를 관리 대상으로 봅니다. - PodDisruptionBudget이 너무 엄격함 — scale down 시 Pod를 옮기려 해도 PDB가 막으면 노드를 죽일 수 없어 줄어들지 않습니다.
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"어노테이션 — 이 어노테이션이 붙은 Pod가 있는 노드는 CA가 종료하지 않습니다. 시스템 Pod나 로컬 디스크에 의존하는 Pod에 붙는 경우가 많습니다.Pending사유가 자원 부족이 아님 —nodeSelector나affinity미스매치, 또는 PV의 AZ 미스매치(#2의WaitForFirstConsumer가 풀어 주는 부분) 때문에 Pending이라면, 노드를 더 띄워도 그 Pod는 여전히 Pending입니다. CA의 책임 밖입니다.
kubectl describe pod의 이벤트 섹션과 CA Pod의 로그(kubectl logs -n kube-system -l app=cluster-autoscaler)를 같이 보면 어느 쪽 사유인지 가려집니다.
세 차원의 협업 — 부하 폭증 한 사이클 #
세 오토스케일러가 같이 굴러가는 모양을 한 시나리오로 따라가 보겠습니다 — 평소의 다섯 배 트래픽이 들어오는 마케팅 캠페인 시작 직후입니다.
t=0s 캠페인 시작. 트래픽 5x 진입.
Deployment 'web': replicas=4, requests.cpu=500m
모든 Pod의 CPU 평균 130% (목표 70% 대비)
t=15s HPA가 메트릭 수집,계산.
desired = ceil(4 * (130/70)) = 8
replicas: 4 -> 8로 변경 요청.
t=20s K8s가 새 Pod 4개를 만들려 함.
그러나 노드의 가용 CPU가 부족.
새 Pod 2개는 Running, 2개는 Pending.
t=30s CA가 Pending Pod를 발견.
ASG에 desired capacity +1 요청.
새 노드가 클라우드에서 부팅 시작.
t=120s 새 노드가 클러스터에 Ready로 합류.
Pending 상태였던 Pod 2개가 그 노드에 스케줄.
Running.
t=135s HPA가 다시 측정. 평균 80%.
desired = ceil(8 * (80/70)) = 10. replicas: 8 -> 10.
2개 더 필요 — 이번엔 새 노드의 여유 자원에 들어감.
...캠페인이 끝나고 트래픽이 평소로 돌아오면 역순으로 줄어듭니다 — HPA의 scale down 안정화 윈도우(5분) 후 Pod가 천천히 줄고, CA가 비어 가는 노드를 발견해 종료합니다. 노드 종료는 보통 사용률이 낮은 상태가 일정 시간(기본 10분쯤) 유지된 뒤에 시작되어 더 보수적입니다.
이 사이클이 사람의 개입 없이 굴러간다는 점이 핵심입니다. 그러나 굴러가는 데 필요한 전제 — 워크로드에 requests가 적혀 있고, metrics-server가 살아 있고, HPA의 behavior가 합리적이고, ASG에 CA 태그가 붙어 있고, 노드 인스턴스 타입이 워크로드에 맞는 것 — 이 모두 #4부터의 모델 위에 서 있습니다. 오토스케일링은 그 모델을 동적으로 굴리는 마지막 한 층이라고 보면 됩니다.
운영 도입 패턴 — 어디서부터 시작하나 #
세 오토스케일러를 모두 동시에 켜는 것이 좋아 보일 수 있지만, 운영의 권장 흐름은 보수적입니다.
- HPA부터 도입 — 가장 익숙하고, 사고의 위험이 가장 적습니다. 대상 워크로드에
requests가 적혀 있는지 확인하고,minReplicas≥ 2로 두고, CPU 70% 같은 표준값에서 시작합니다. 며칠,몇 주 동안 동작을 관찰하며behavior를 조정합니다. - VPA는
updateMode: "Off"로 권장값만 — Pod를 재생성하는 정책은 처음부터 켜지 않습니다. recommender의 권장값을 며칠 모아 본 뒤, 합리적이라는 판단이 서면 사람이 매니페스트에 반영합니다.Auto로 옮기는 것은 그 워크로드의 evict 영향이 적다는 확신이 들 때만 합니다. - CA는 클라우드 환경이면 거의 필수 — minikube,kind 같은 학습 환경에서는 의미가 없지만, 클라우드 클러스터에서 CA 없이 운영하면 노드의 desired capacity를 사람이 매번 따라가야 합니다. EKS는 처음부터 CA(또는 Karpenter)를 같이 설치해 두는 패턴이 표준입니다.
- Custom metric / KEDA는 워크로드 특성에 맞춰 — CPU,메모리 신호로 충분히 표현되는 워크로드에는 무리해서 Prometheus Adapter를 도입할 이유가 없습니다. 큐 컨슈머나 이벤트 워커처럼 신호의 종류가 다른 워크로드에 한해 도입합니다.
이 흐름을 한 줄로 줄이면 — HPA는 거의 모든 워크로드에 기본, VPA는 권장값 도구로 시작, CA는 클라우드면 필수, KEDA는 필요한 곳에입니다.
정리 #
이번 글에서 잡은 흐름을 정리하겠습니다.
- 세 차원의 자동 조정 — HPA(Pod 개수), VPA(Pod의 requests,limits), CA(노드 개수). 서로 보완 관계이고 동시에 굴러갑니다.
- metrics-server라는 전제 — HPA,VPA가 동작하려면 클러스터 안에 metrics-server(또는 Prometheus + Adapter, KEDA)가 설치되어 있어야 합니다. minikube는 addon 한 줄, EKS는 별도 설치 필요, GKE,AKS는 기본 활성화.
- HPA 매니페스트 —
apiVersion: autoscaling/v2.scaleTargetRef(대상 Deployment),minReplicas/maxReplicas(안전망),metrics(보는 신호). CPUUtilization은requests대비 비율이므로 #4의 requests가 전제입니다. - HPA 알고리즘 —
desired = ceil(current * (currentMetric / targetMetric)). 비례식 한 줄. multi-metric은 각 메트릭의 desired 중 가장 큰 값을 채택. - scale up,down 비대칭 —
behavior필드. scale up은 즉각, scale down은 5분 안정화 윈도우. 잠깐 떨어진 신호로 줄였다가 cold start 사고가 나는 일을 방지. - Custom metric과 KEDA — Prometheus Adapter로 PromQL 결과를 HPA에 노출. KEDA는 50종 넘는 이벤트 소스 빌트인 + 0→N 스케일링. 큐,이벤트 워크로드에 적합.
- VPA 세 컴포넌트 — recommender(권장값 산출), updater(Pod evict), admission-controller(새 Pod에 권장값 주입).
updateMode는Off(권장만) /Initial(생성 시) /Auto(재생성). 운영 시작은 거의 항상Off. - HPA,VPA 충돌 — 같은 메트릭(CPU)으로 둘 다 굴리면 진동. 회피는 두 길 — 메트릭을 분리, 또는 VPA를
Off로 두고 사람이 반영. - Cluster Autoscaler —
PendingPod가 보이면 노드 그룹(ASG / MIG / VMSS)에 노드 추가, 비어 있는 노드는 일정 시간 후 종료. 결정은requests기반. EKS의 더 빠른 대안으로 Karpenter. - 세 차원의 협업 — 부하 폭증 시 HPA가 Pod를 늘림 → 노드 자원 부족 → 새 Pod가 Pending → CA가 노드 추가. 줄어들 때 역순. VPA는 별도 사이클.
- 운영 도입 흐름 — HPA부터, VPA는
Off로 권장값만, CA는 클라우드면 거의 필수, KEDA는 필요한 워크로드에만.
이 모델까지 손에 들어오면, 운영 클러스터의 부하가 출렁일 때 사람이 매번 손으로 따라가지 않아도 되는 한 단을 갖추게 됩니다. 동시에 그 자동화가 굴러가는 데 필요한 전제 — requests의 존재, metrics-server, behavior의 합리적인 값, CA 태그 — 가 한 묶음으로 보입니다.
다음 — RBAC / NetworkPolicy / ResourceQuota #
이번 글까지의 시리즈는 워크로드를 어떻게 굴릴지의 모델을 한 사이클 따라온 흐름이었습니다. #1의 컨트롤러, #2의 영속 데이터, #3의 외부 진입점, #4의 자원 요청, #5의 건강 신호, 이번 글의 자동 조정. 이만큼이 한 워크로드를 운영 클러스터에 띄워 굴러가게 하는 모델의 한 묶음입니다.
다음 편의 주제는 시점을 한 단 옮깁니다 — 여러 사용자,여러 팀,여러 워크로드가 한 클러스터를 같이 쓰는 환경의 정책입니다. 누가 어떤 객체에 어떤 동작을 할 수 있는가의 권한 모델 RBAC, Pod끼리의 네트워크 통신을 화이트리스트로 통제하는 NetworkPolicy, 한 네임스페이스가 클러스터 자원을 얼마까지 쓸 수 있는지의 상한 ResourceQuota와 LimitRange. 이 셋이 멀티 테넌트,운영 클러스터의 표준 안전망입니다.
#7 RBAC / NetworkPolicy / ResourceQuota — 보안과 자원 정책에서는 이 세 객체의 매니페스트와 동작, 그리고 운영의 권장 패턴을 한 사이클로 따라가며 K8s 중급 시리즈를 마무리하겠습니다.