Step 18 — 오토스케일링: metrics-server와 HPA
학습 목표
- 오토스케일링의 세 축(HPA·VPA·Cluster Autoscaler)이 각각 무엇을 조절하는지 구분한다.
- metrics-server를 kind 클러스터에 설치하고
kubectl top이 동작하게 만든다.
- HPA(
autoscaling/v2)를 CPU 기준으로 걸고, 실제 부하로 replica가 1→6으로 늘고 다시 주는 것을 관찰한다.
behavior(scaleUp/scaleDown 정책, 안정화 윈도우)로 스케일링 속도를 제어한다.
선행 지식
- Step 05(Deployment), Step 09(resources requests/limits). HPA는 request 대비 사용률로 판단하므로 requests가 없으면 CPU HPA가 동작하지 않습니다.
소요 시간
- 40분 (부하 후 scaleDown 관찰까지 하면 +10분)
1. 오토스케일링의 세 축
| 종류 | 무엇을 조절 | 이 스텝에서 |
|---|
| HPA (Horizontal Pod Autoscaler) | 파드 개수(replica) | 실습 (CPU 기반) |
| VPA (Vertical Pod Autoscaler) | 파드의 requests/limits 크기 | 개념만 (kind 미설치) |
| Cluster Autoscaler | 노드 개수 | 개념만 (kind는 노드 고정) |
HPA는 "손님이 많으면 계산대를 늘린다", VPA는 "계산대 한 대의 처리 능력을 키운다", Cluster Autoscaler는 "매장(노드) 자체를 늘린다"에 해당합니다. 가장 흔히 쓰는 것이 HPA입니다.
2. metrics-server 설치 (공용 인프라)
HPA도 kubectl top도 메트릭 파이프라인이 있어야 동작합니다. 그 표준 구현이 metrics-server입니다. kube-system에 한 번 설치하면 클러스터 전체가 씁니다(Step 19에서도 사용).
kind 특수사항 — --kubelet-insecure-tls
metrics-server는 각 노드의 kubelet에 HTTPS로 접속해 메트릭을 긁습니다. kind의 kubelet은 자체 서명(self-signed) 인증서를 쓰는데, metrics-server 기본 설정은 이를 검증하려다 실패합니다. 그래서 kind에서는 --kubelet-insecure-tls 인자를 추가해야 합니다. (학습용 클러스터에서만. 운영에서는 제대로 된 인증서를 쓰세요.)
# 1) 공식 매니페스트를 호스트에서 받는다 (사내망에서 호스트는 pull/다운로드 가능)
curl -sL https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml -o /tmp/ms.yaml
# 2) Deployment args 에 --kubelet-insecure-tls 를 추가 (아래 한 줄을 --metric-resolution 아래에 삽입)
# - --kubelet-insecure-tls
# 3) 이미지가 사내망에서 노드로 안 받아지면: 호스트에서 pull 후 kind 로 주입
docker pull registry.k8s.io/metrics-server/metrics-server:v0.8.1
docker save registry.k8s.io/metrics-server/metrics-server:v0.8.1 -o /tmp/ms-img.tar
for n in learn-control-plane learn-worker learn-worker2; do
docker exec -i $n ctr --namespace=k8s.io images import - < /tmp/ms-img.tar
done
# 4) 적용
kubectl apply -f /tmp/ms.yaml
kubectl -n kube-system rollout status deploy/metrics-server
이 스텝의 manifests/metrics-server.yaml이 바로 --kubelet-insecure-tls를 넣은 완성본입니다(전문은 아래 실습 파일 섹션에 있습니다). 위 과정을 건너뛰고 kubectl apply -f manifests/metrics-server.yaml 해도 됩니다(이미지가 노드에 있어야 함).
왜 kind load 대신 ctr images import인가: 호스트에서 받은 이미지가 단일 아키텍처면 kind load docker-image가 --all-platforms 때문에 "content digest not found"로 실패합니다. docker save → 각 노드에서 ctr images import가 확실합니다.
동작 확인 — kubectl top
설치 후 20~30초 지나 첫 스크레이프가 쌓이면:
실제 출력
NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
learn-control-plane 156m 1% 1278Mi 8%
learn-worker 134m 1% 1337Mi 8%
learn-worker2 128m 1% 1549Mi 9%
kubectl get apiservices v1beta1.metrics.k8s.io
NAME SERVICE AVAILABLE AGE
v1beta1.metrics.k8s.io kube-system/metrics-server True 46s
AVAILABLE True가 핵심입니다. False (MissingEndpoints)면 metrics-server 파드가 아직 안 떴거나 TLS 문제입니다.
3. 실습 워크로드
HPA는 request 대비 사용률로 판단합니다. request를 작게(20m) 주면 약간의 부하로도 목표치를 넘겨 스케일업을 쉽게 관찰할 수 있습니다(manifests/workload.yaml, 전문은 아래 실습 파일 섹션 참고).
resources:
requests:
cpu: 20m # HPA 계산의 기준값 (이게 없으면 CPU HPA 불가)
memory: 24Mi
limits:
cpu: 200m
kubectl apply -f manifests/namespace.yaml
kubectl apply -f manifests/workload.yaml
4. HPA 정의 (autoscaling/v2)
manifests/hpa.yaml의 핵심 부분입니다(전문은 아래 실습 파일 섹션 참고).
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
scaleTargetRef:
kind: Deployment
name: web
minReplicas: 1
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50 # request(20m)의 50% = 평균 10m 초과 시 스케일업
autoscaling/v1은 CPU 하나만, 필드도 빈약합니다. 항상 autoscaling/v2를 쓰세요. 메모리·커스텀·복수 메트릭과 behavior를 지원합니다.
계산식: 원하는 replica = ceil(현재 replica × 현재 사용률 / 목표 사용률). 1개가 360%를 쓰면 ceil(1 × 360/50) = 8 → maxReplicas 6으로 제한 → 6.
kubectl apply -f manifests/hpa.yaml
kubectl get hpa -n step18
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web Deployment/web cpu: 15%/50% 1 6 1 20s
cpu: 15%/50% = 현재 사용률 15%, 목표 50%. 아직 여유가 있어 replica는 1.
함정: 방금 걸면 TARGETS가 <unknown>/50%로 뜹니다. metrics-server가 파드 메트릭을 처음 수집하기까지 15~30초 걸립니다. 기다리세요. 계속 <unknown>이면 파드에 requests.cpu가 없는 것입니다.
5. 부하를 걸어 스케일업 실증
nginx 정적 서빙은 요청당 CPU가 작으므로, busybox 파드 3개가 각각 10개의 무한 wget 루프를 돌려 RPS를 높입니다(manifests/load-generator.yaml).
kubectl apply -f manifests/load-generator.yaml
# 20초 간격으로 관찰
watch -n 20 'kubectl get hpa web -n step18; kubectl top pods -n step18 -l app=web'
실제 관찰 결과 (부하 투입 후):
=== t=20s === web cpu: 0%/50% 1 6 1
=== t=40s === web cpu: 360%/50% 1 6 1 web-...-6bznr 72m
=== t=60s === web cpu: 343%/50% 1 6 6 (파드 6개로 스케일업)
web-...-6bznr 67m web-...-h5tzs 71m web-...-kpjjb 73m
web-...-ksnvr 64m web-...-pt287 68m web-...-rcf2n 63m
=== t=80s === web cpu: 270%/50% 1 6 6 (파드당 부하 분산되어 하락)
CPU가 360%까지 치솟자 HPA가 1→5→6으로 늘렸고, 부하가 6개에 분산되며 파드당 사용률이 내려갔습니다.
kubectl describe hpa의 이벤트로 왜/언제 늘었는지 확인:
kubectl describe hpa web -n step18 | sed -n '/Events:/,$p'
Type Reason Age Message
Normal SuccessfulRescale 2m7s New size: 5; reason: cpu resource utilization ... above target
Normal SuccessfulRescale 112s New size: 6; reason: cpu resource utilization ... above target
6. scaleUp / scaleDown behavior
기본 HPA는 급격히 늘리고 아주 천천히(기본 5분 안정화) 줄입니다. behavior로 이를 제어합니다(hpa.yaml):
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 즉시 대응
policies:
- type: Percent
value: 100 # 15초마다 최대 2배
periodSeconds: 15
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 60 # 60초간 최댓값 유지 후
policies:
- type: Pods
value: 1 # 60초마다 1개씩만 축소 (플래핑 방지)
periodSeconds: 60
부하 제거 후 실제 축소 관찰:
kubectl delete -f manifests/load-generator.yaml
CPU 0%/50% 도달 후:
6 → 5 (New size: 5; reason: All metrics below target)
5 → 4 (60초 뒤)
4 → 3 (다시 60초 뒤)
...정책대로 60초에 1개씩 감소
축소가 즉각적이지 않은 이유: 순간적으로 부하가 빠졌다고 바로 줄이면, 부하가 다시 오를 때 또 늘리는 **플래핑(flapping)**이 생깁니다. stabilizationWindowSeconds는 이를 막는 완충 장치입니다.
7. VPA·Cluster Autoscaler (개념)
- VPA: 파드의 실제 사용량을 관찰해
requests를 자동 조정합니다. 파드를 재생성해야 반영되므로 HPA(CPU)와 동시에 같은 메트릭으로 쓰면 충돌합니다. 보통 VPA는 메모리, HPA는 CPU로 나누거나 VPA를 추천(Off) 모드로 씁니다. kind에는 미설치.
- Cluster Autoscaler: 파드가
Pending(노드 자원 부족)이면 클라우드 노드풀을 늘리고, 노드가 놀면 줄입니다. kind는 노드가 Docker 컨테이너로 고정되어 있어 동작하지 않습니다. EKS/GKE 등 클라우드에서만.
팁과 함정
- 함정 1 — requests 없으면 CPU HPA는 죽은 상태:
averageUtilization은 request 대비 비율입니다. requests.cpu가 없으면 HPA는 TARGETS를 <unknown>으로 남기고 아무것도 못 합니다.
- 함정 2 — metrics-server가 안 뜨는 3대 원인: (1) kind인데
--kubelet-insecure-tls 누락, (2) 이미지가 노드에 없음, (3) apiservice가 AVAILABLE False. kubectl get apiservices v1beta1.metrics.k8s.io와 kubectl -n kube-system logs deploy/metrics-server로 확인.
- 함정 3 — 축소가 느리다고 놀라지 말 것: 기본 scaleDown 안정화가 300초입니다. behavior를 안 건드리면 부하가 빠져도 5분간 replica가 안 줄어듭니다. 버그가 아닙니다.
- 팁 —
kubectl top은 순간값: metrics-server는 15초마다 스크레이프한 값을 보여줍니다. 부하가 막 빠져도 직전 샘플이 남아 몇 초간 높게 보일 수 있습니다(위 관찰에서 CPU가 261%로 잠깐 튄 이유).
- 팁 — HPA와 replicas 필드: HPA가 관리하는 Deployment의
spec.replicas를 kubectl scale이나 매니페스트로 직접 바꾸지 마세요. HPA와 싸웁니다. HPA가 붙으면 개수는 HPA에게 맡깁니다.
정리표
| 명령/필드 | 뜻 |
|---|
kubectl top nodes / top pods | 노드/파드 실시간 CPU·메모리 (metrics-server 필요) |
autoscaling/v2 | HPA API 버전. 항상 이걸 사용 |
averageUtilization: 50 | request의 50% 초과 시 스케일업 |
minReplicas / maxReplicas | 스케일 범위 하한/상한 |
behavior.scaleUp/scaleDown | 스케일 속도·안정화 제어 |
stabilizationWindowSeconds | 이 시간 동안 최댓값 유지 후 결정(플래핑 방지) |
kubectl describe hpa | 스케일 결정 이유가 Events에 남음 |
연습 과제
challenge.md의 4개 과제.
다음 단계
→ Step 19 — 관측성: kubectl top·events·logs·describe·debug로 클러스터에서 무슨 일이 일어나는지 읽어내는 법을 익힙니다. metrics-server는 계속 씁니다.
실습 파일
이 스텝의 파일들은 **"메트릭 파이프라인을 깔고 → 워크로드를 띄우고 → HPA를 걸고 → 부하를 넣어 스케일업/다운을 관찰한다"**는 하나의 흐름을 그대로 따라갑니다. 먼저 manifests/metrics-server.yaml을 kube-system에 한 번 설치해 kubectl top을 살린 뒤, namespace.yaml → workload.yaml → hpa.yaml 순으로 적용하고, 마지막에 load-generator.yaml을 넣었다 빼면서 replica가 1→6→1로 움직이는 것을 봅니다. 이 전 과정을 자동으로 수행하는 것이 commands.sh입니다.
manifests/metrics-server.yaml
metrics-server 공식 components.yaml에 kind용 수정 한 줄을 미리 반영해 둔 완성본입니다. 본문 2절에서 "이 과정을 건너뛰고 바로 apply해도 된다"고 한 파일이 이것입니다.
- 핵심은 Deployment의 args에 있는
--kubelet-insecure-tls 한 줄입니다. kind 노드의 kubelet은 자체 서명 인증서를 쓰기 때문에 이 인자가 없으면 metrics-server가 스크레이프에 실패하고 kubectl top이 영원히 에러를 냅니다.
--metric-resolution=15s는 15초마다 각 kubelet에서 메트릭을 긁어온다는 뜻입니다. 그래서 설치 직후 kubectl top이나 HPA TARGETS가 값을 보여주기까지 15~30초가 필요합니다(본문의 <unknown> 함정).
--kubelet-preferred-address-types=InternalIP,...와 --kubelet-use-node-status-port는 노드 주소/포트를 어떻게 찾을지 정하는 설정으로, kind처럼 DNS 이름이 안 풀리는 환경에서 InternalIP를 먼저 시도하게 해 줍니다.
- 맨 아래
APIService v1beta1.metrics.k8s.io가 집계 API 등록입니다. kubectl get apiservices v1beta1.metrics.k8s.io가 AVAILABLE True가 되어야 HPA가 메트릭을 읽을 수 있습니다.
- 이미지는
registry.k8s.io/metrics-server/metrics-server:v0.8.1이고 imagePullPolicy: IfNotPresent이므로, 폐쇄망이면 본문 2절의 docker save → ctr images import로 노드에 이미지를 먼저 넣어두어야 합니다.
- 주의: 이 리소스들은
kube-system에 설치되는 공용 인프라입니다. 실습이 끝나도 삭제하지 마세요. Step 19에서 그대로 씁니다.
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
k8s-app: metrics-server
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rbac.authorization.k8s.io/aggregate-to-edit: "true"
rbac.authorization.k8s.io/aggregate-to-view: "true"
name: system:aggregated-metrics-reader
rules:
- apiGroups:
- metrics.k8s.io
resources:
- pods
- nodes
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
k8s-app: metrics-server
name: system:metrics-server
rules:
- apiGroups:
- ""
resources:
- nodes/metrics
verbs:
- get
- apiGroups:
- ""
resources:
- pods
- nodes
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
k8s-app: metrics-server
name: metrics-server-auth-reader
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: extension-apiserver-authentication-reader
subjects:
- kind: ServiceAccount
name: metrics-server
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
k8s-app: metrics-server
name: metrics-server:system:auth-delegator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: metrics-server
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
k8s-app: metrics-server
name: system:metrics-server
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:metrics-server
subjects:
- kind: ServiceAccount
name: metrics-server
namespace: kube-system
---
apiVersion: v1
kind: Service
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
spec:
ports:
- appProtocol: https
name: https
port: 443
protocol: TCP
targetPort: https
selector:
k8s-app: metrics-server
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
spec:
selector:
matchLabels:
k8s-app: metrics-server
strategy:
rollingUpdate:
maxUnavailable: 0
template:
metadata:
labels:
k8s-app: metrics-server
spec:
containers:
- args:
- --cert-dir=/tmp
- --secure-port=10250
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --kubelet-use-node-status-port
- --metric-resolution=15s
- --kubelet-insecure-tls
image: registry.k8s.io/metrics-server/metrics-server:v0.8.1
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 3
httpGet:
path: /livez
port: https
scheme: HTTPS
periodSeconds: 10
name: metrics-server
ports:
- containerPort: 10250
name: https
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /readyz
port: https
scheme: HTTPS
initialDelaySeconds: 20
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 200Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- mountPath: /tmp
name: tmp-dir
nodeSelector:
kubernetes.io/os: linux
priorityClassName: system-cluster-critical
serviceAccountName: metrics-server
volumes:
- emptyDir: {}
name: tmp-dir
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
labels:
k8s-app: metrics-server
name: v1beta1.metrics.k8s.io
spec:
group: metrics.k8s.io
groupPriorityMinimum: 100
insecureSkipTLSVerify: true
service:
name: metrics-server
namespace: kube-system
version: v1beta1
versionPriority: 100
manifests/namespace.yaml
실습 리소스를 담을 step18 네임스페이스를 만듭니다. course: k8s-learn, step: "18" 라벨이 붙어 있어 나중에 라벨로 골라내기 쉽습니다. 정리할 때 kubectl delete ns step18 한 줄이면 워크로드·서비스·HPA·부하 생성기가 모두 함께 사라지지만, kube-system에 있는 metrics-server는 영향을 받지 않습니다. 가장 먼저 적용해야 하는 파일입니다.
apiVersion: v1
kind: Namespace
metadata:
name: step18
labels:
course: k8s-learn
step: "18"
manifests/workload.yaml
HPA가 조절할 대상 워크로드입니다(본문 3절). nginx Deployment 1개와 이를 가리키는 Service web으로 구성되어 있습니다.
- 가장 중요한 줄은
requests.cpu: 20m입니다. HPA의 averageUtilization은 request 대비 비율이므로, request가 없으면 CPU HPA는 아무것도 계산하지 못하고 TARGETS가 <unknown>에 머뭅니다.
- request를 일부러 아주 작게(
20m) 잡은 것이 실습의 트릭입니다. 목표 50%면 파드당 평균 10m만 넘겨도 스케일업이 시작되므로, 노트북 kind 클러스터에서도 replica가 늘어나는 것을 눈으로 볼 수 있습니다.
limits.cpu: 200m은 파드 하나가 쓸 수 있는 CPU 상한입니다(메모리는 requests.memory: 24Mi / limits.memory: 64Mi). CPU limit이 request(20m) 대비 10배이므로 사용률이 최대 1000%까지 찍힐 수 있고, 실제 관찰에서 360%가 나온 이유가 여기에 있습니다.
- Service
web은 포트 80을 그대로 파드로 넘깁니다. 부하 생성기가 http://web.step18.svc.cluster.local/로 때리는 대상이 바로 이 Service이고, replica가 늘면 요청이 자동으로 분산됩니다.
spec.replicas: 1로 시작하지만, HPA를 건 뒤에는 이 값을 손으로 바꾸지 마세요(본문 팁 참고 — HPA와 싸웁니다).
# HPA 실습 대상 워크로드.
# 핵심: cpu request 를 아주 작게(20m) 주었다. HPA 는 "request 대비 사용률(%)"로
# 판단하므로, request 가 작으면 약간의 부하로도 목표치를 쉽게 넘겨 스케일업을 관찰하기 쉽다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: step18
labels:
app: web
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 20m # HPA 계산의 기준값
memory: 24Mi
limits:
cpu: 200m
memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: step18
labels:
app: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80
manifests/hpa.yaml
본문 4절과 6절의 주인공입니다. autoscaling/v2 HPA로 Deployment/web을 CPU 기준으로 1~6개 사이에서 조절합니다.
scaleTargetRef가 apps/v1 Deployment web을 가리키므로 workload.yaml을 먼저 적용해야 합니다.
averageUtilization: 50 — request가 20m이니 파드당 평균 10m를 넘으면 스케일업입니다. minReplicas: 1, maxReplicas: 6이 상·하한이라, 계산식이 8을 내놓아도 6에서 잘립니다.
behavior.scaleUp은 공격적입니다. stabilizationWindowSeconds: 0이라 지연 없이 즉시 반응하고, Percent 100 / 15s(15초마다 2배)와 Pods 4 / 15s(15초마다 4개) 두 정책을 selectPolicy: Max로 묶어 둘 중 더 큰 쪽을 택합니다. 1개에서 시작하면 Percent는 +1, Pods는 +4를 허용하므로 4개 쪽이 이겨 단숨에 커집니다.
behavior.scaleDown은 보수적입니다. stabilizationWindowSeconds: 60으로 60초 동안 관측된 최댓값을 기준으로 삼아 성급한 축소를 막고, Pods 1 / 60s로 60초에 1개씩만 줄입니다. 6개에서 1개까지 내려오는 데 수 분이 걸리는 것이 정상입니다(플래핑 방지).
behavior를 아예 안 쓰면 기본 scaleDown 안정화가 300초라 5분간 replica가 그대로입니다. 버그가 아닙니다.
# autoscaling/v2 HPA. CPU 평균 사용률이 request 의 50% 를 넘으면 replica 를 늘린다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
namespace: step18
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 1
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50 # request(20m)의 50% = 평균 10m 초과 시 스케일업
behavior:
scaleUp:
# 급격한 부하에 빠르게 대응: 15초마다 최대 100%(2배) 또는 4개까지 증가
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 4
periodSeconds: 15
selectPolicy: Max
scaleDown:
# 축소는 신중하게: 60초 안정화 후, 60초마다 1개씩만 줄인다(플래핑 방지)
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 1
periodSeconds: 60
manifests/load-generator.yaml
스케일업을 실증하기 위한 부하 생성기입니다(본문 5절). 적용 후 20~40초면 HPA의 TARGETS가 치솟기 시작합니다.
- busybox 파드
replicas: 3이 각각 while [ $i -lt 10 ] 루프로 10개의 무한 wget 서브셸을 백그라운드(&)로 띄우므로 총 30개의 요청 루프가 동시에 돕니다. 마지막 wait가 있어야 메인 셸이 끝나지 않고 컨테이너가 살아 있습니다.
- 요청 대상은
http://web.step18.svc.cluster.local/ — 같은 네임스페이스의 Service DNS입니다. web Service가 먼저 떠 있어야 하므로 workload.yaml 다음에 적용합니다.
- nginx 정적 페이지는 요청당 CPU가 워낙 작아서 한두 개 루프로는 사용률이 안 오릅니다. 병렬 루프 10개 × 파드 3개라는 물량이 필요한 이유입니다.
- 부하를 끄는 방법은
kubectl delete -f manifests/load-generator.yaml입니다. 이걸 지운 뒤부터 6절의 scaleDown 관찰이 시작됩니다.
- 주의: 이 파드들은 CPU를 계속 태웁니다. 관찰이 끝나면 반드시 삭제하세요. 방치하면 노트북 팬이 계속 돌아갑니다.
# 부하 생성기. 각 파드가 nginx 서비스로 무한 요청 루프를 돌려 CPU 를 밀어올린다.
# nginx 정적 서빙은 요청당 CPU 가 작으므로, 여러 병렬 루프 + 다중 replica 로 RPS 를 높인다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: load
namespace: step18
labels:
app: load
spec:
replicas: 3
selector:
matchLabels:
app: load
template:
metadata:
labels:
app: load
spec:
containers:
- name: load
image: busybox:1.36
# 한 컨테이너 안에서 10개의 무한 wget 루프를 병렬로 돌린다.
command: ["/bin/sh", "-c"]
args:
- |
i=0
while [ $i -lt 10 ]; do
( while true; do wget -q -O /dev/null http://web.step18.svc.cluster.local/; done ) &
i=$((i+1))
done
wait
resources:
requests:
cpu: 20m
memory: 16Mi
commands.sh
위 매니페스트들을 본문 2~6절 순서 그대로 자동 실행하는 스크립트입니다. 처음 볼 때는 손으로 한 줄씩 따라 하고, 두 번째부터 이 스크립트로 재현하는 것을 권합니다.
set -euo pipefail로 중간에 하나라도 실패하면 즉시 멈추고, HERE="$(cd "$(dirname "$0")" && pwd)"로 스크립트 위치를 잡아 "$HERE/manifests/..."를 적용하므로 어느 디렉터리에서 실행해도 동작합니다.
- 맨 위의
export PATH="/opt/homebrew/bin:$PATH"는 Homebrew로 깐 kubectl을 찾기 위한 macOS(Apple Silicon) 전용 줄입니다. Linux나 다른 경로에 kubectl이 있다면 이 줄은 없어도 그만이며, 지워도 스크립트는 정상 동작합니다.
- 맨 앞의
if ! kubectl get deploy metrics-server -n kube-system는 멱등성 장치입니다. metrics-server가 이미 있으면 설치를 건너뛰므로 스크립트를 여러 번 돌려도 안전합니다.
- 설치 후
sleep 20을 두고 kubectl top nodes를 부르는 것은 첫 스크레이프(15초 주기)를 기다리기 위함이고, HPA 적용 후의 sleep 25도 같은 이유로 TARGETS가 <unknown>이 아닌 값을 갖게 하려는 것입니다.
- 부하 투입 후
for i in $(seq 1 8) 루프가 20초 간격으로 8회, 즉 약 2분 40초 동안 HPA와 kubectl top pods를 함께 찍습니다. 이 출력이 본문 5절의 t=20s / 40s / 60s 표와 대응합니다. 이어서 kubectl describe hpa ... | sed -n '/Events:/,$p'로 스케일 결정 이유를 뽑습니다.
- 부하를 지운 뒤의 두 번째 루프(30초 × 6회)에서 replica가 60초에 1개씩 줄어드는 것을 확인합니다.
- 주의: 스크립트 끝의 마지막
kubectl 명령이 kubectl delete namespace step18이라 스크립트가 끝까지 돌면 실습 리소스가 전부 지워집니다. 결과를 더 들여다보고 싶으면 그 전에 Ctrl-C 하세요. 반대로 metrics-server는 의도적으로 남겨둡니다(Step 19에서 사용).
#!/usr/bin/env bash
# Step 18 — 오토스케일링 (metrics-server + HPA)
export PATH="/opt/homebrew/bin:$PATH"
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# ── metrics-server 설치 (이미 설치돼 있으면 건너뜀) ───────────────────
if ! kubectl get deploy metrics-server -n kube-system >/dev/null 2>&1; then
echo "▶ metrics-server 설치"
# 매니페스트를 직접 받아 쓰려면(사내망 host 에서):
# curl -sL https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml -o /tmp/ms.yaml
# # /tmp/ms.yaml 의 args 에 '- --kubelet-insecure-tls' 추가
# 여기서는 그 수정을 이미 반영한 manifests/metrics-server.yaml 을 쓴다.
# 이미지가 노드에 없으면 먼저 주입:
# docker pull registry.k8s.io/metrics-server/metrics-server:v0.8.1
# docker save registry.k8s.io/metrics-server/metrics-server:v0.8.1 -o /tmp/ms-img.tar
# for n in learn-control-plane learn-worker learn-worker2; do docker exec -i $n ctr --namespace=k8s.io images import - < /tmp/ms-img.tar; done
kubectl apply -f "$HERE/manifests/metrics-server.yaml"
kubectl -n kube-system rollout status deploy/metrics-server --timeout=120s
fi
# ── top 동작 확인 ─────────────────────────────────────────────────────
sleep 20
kubectl top nodes
kubectl get apiservices v1beta1.metrics.k8s.io
# ── 워크로드 + HPA ────────────────────────────────────────────────────
kubectl apply -f "$HERE/manifests/namespace.yaml"
kubectl apply -f "$HERE/manifests/workload.yaml"
kubectl rollout status deploy/web -n step18 --timeout=60s
kubectl apply -f "$HERE/manifests/hpa.yaml"
sleep 25
kubectl get hpa -n step18 # cpu: ~15%/50%, REPLICAS 1
# ── 부하 투입 → 스케일업 관찰 ─────────────────────────────────────────
kubectl apply -f "$HERE/manifests/load-generator.yaml"
kubectl rollout status deploy/load -n step18 --timeout=60s
echo "▶ 스케일업 관찰(약 2분). Ctrl-C 로 빠져나온 뒤 아래 부하 제거로 진행."
for i in $(seq 1 8); do
kubectl get hpa web -n step18 --no-headers
kubectl top pods -n step18 -l app=web --no-headers 2>/dev/null || true
sleep 20
done
# ── 스케일 결정 이유 ─────────────────────────────────────────────────
kubectl describe hpa web -n step18 | sed -n '/Events:/,$p'
# ── 부하 제거 → 스케일다운 관찰 ──────────────────────────────────────
kubectl delete -f "$HERE/manifests/load-generator.yaml"
echo "▶ 스케일다운은 stabilizationWindow(60s) 후 60초에 1개씩 감소. 수 분 소요."
for i in $(seq 1 6); do
echo "REPLICAS=$(kubectl get deploy web -n step18 -o jsonpath='{.status.replicas}') $(kubectl get hpa web -n step18 --no-headers)"
sleep 30
done
# ── 정리 (metrics-server 는 보존!) ───────────────────────────────────
kubectl delete namespace step18
echo "metrics-server 는 다른 스텝(19)에서 쓰므로 삭제하지 않습니다."