본문 바로가기
프로그래밍/Kubernetes

HPA는 어떻게 메트릭을 가져와서 Pod의 Scaling을 하는걸까?

by Hwan2 2026. 3. 4.
반응형

쿠버네티스에서 HPA를 쓰다 보면 이런 지점에서 한 번 멈춥니다.

  • HPA YAML에는 metrics-server 주소가 없는데 도대체 어디로 메트릭을 요청하는 거지?
  • kubectl top / /apis/metrics.k8s.io/... 는 어떻게 동작하는 거지?
  • metrics-server는 모든 Pod에 붙는 건가, 아니면 다른 원리가 있나?
  • 노드가 1,000개면 15초마다 그걸 다 커버할 수 있나?
  • 그 과정에서 나오는 Aggregation layer 는 정확히 뭐고, 왜 kube-apiserver “안”에 있나?
  • 마지막으로, 이런 확장 방식이 CRD/CR 이랑은 뭐가 다른가?

이 글은 위 질문을 한 흐름으로 묶어서 “HPA → Metrics API → aggregation → metrics-server → kubelet” 까지 연결한 뒤, 마지막에 “같은 확장 이야기인 CRD/CR”까지 자연스럽게 정리합니다.

  • HPA 동작 원리 / 어떤 API에서 메트릭을 가져오는지:
  • HPA sync 주기(기본 15초):
  • Resource Metrics Pipeline(메트릭 흐름 전체 그림, kubelet /metrics/resource 등):
  • metrics-server README(15초 수집, 노드당 리소스 효율, 5,000노드 스케일 목표):
  • Aggregation layer 개념(“apiserver in-process”, APIService가 path claim, proxy):
  • Aggregation layer 설정(APIService의 spec.service, caBundle, --enable-aggregator-routing):
  • Custom Resources(CRD/CR는 저장/조회, 컨트롤러 결합 시 선언적 API):

(복사용 링크 모음)


1. HPA는 “어디로” 메트릭을 요청하는가?

HPA는 “지속적인 데몬”이 아니라 컨트롤 루프로 동작합니다. 그리고 이 루프는 기본적으로 15초 간격으로 실행됩니다. (정확히는 kube-controller-manager 플래그 --horizontal-pod-autoscaler-sync-period 기본값이 15초)

1.1 HPA가 15초마다 하는 일 (핵심 흐름)

공식 문서 흐름을 그대로 요약하면:

  1. HPA 정의(HorizontalPodAutoscaler)를 읽고
  2. scaleTargetRef 가 가리키는 타겟(Deployment 등)을 찾고
  3. 그 타겟의 .spec.selector 라벨로 “대상 Pod들”을 결정한 뒤
  4. 메트릭 스펙에 맞는 API로부터 메트릭을 가져와
  5. 원하는 replica 수를 계산해
  6. 타겟의 scale 서브리소스를 업데이트합니다.

즉, 네가 HPA YAML에서 metrics-server: http://... 같은 걸 안 적는 이유는:

  • HPA가 “특정 Pod/서비스 주소”를 호출하도록 설계된 게 아니라
  • 정해진 메트릭 API 그룹(예: metrics.k8s.io)로 질의하도록 설계되어 있기 때문입니다.

1.2 CPU/Memory(Resource) 기반이면 어디서 가져오나?

HPA의 Resource(cpu/memory) 타입은 보통 resource metrics API에서 가져옵니다. 그리고 그 대표 API가 바로:

  • metrics.k8s.io (Metrics API)

공식 문서도 “HPA는 보통 aggregated APIs(metrics.k8s.io, custom.metrics.k8s.io, external.metrics.k8s.io)에서 메트릭을 가져오며, metrics.k8s.io는 보통 Metrics Server 애드온이 제공한다”고 못 박습니다.


2. “Metrics API(metrics.k8s.io)”는 누가 제공하나?

여기서부터가 “HPA ↔ metrics-server가 어떻게 연결되나”의 본체입니다.

2.1 Metrics API는 “코어 API”가 아니라 “확장 API”다

/apis/metrics.k8s.io/v1beta1/... 는 kube-apiserver가 원래부터 가지고 있는 core API가 아니라, 확장 API(aggregated API) 형태로 붙는 API입니다.

그래서 이 API를 쓰려면 다음이 필요합니다:

  • API aggregation layer 활성화
  • metrics.k8s.io 를 “누가 제공하는지” 등록(APIService)
  • 실제로 Metrics API를 구현하는 서버(대표가 metrics-server)

2.2 이걸 치면 전 노드가 나오는 이유

kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes | head | jq
 

이건 “metrics-server가 kubelet에서 긁어온 데이터를 Metrics API 응답 형태로 가공해서” 내주는 결과를 “클라이언트가 요청한 것”이에요.

 

중요:
이 “큰 JSON 리스트”를 metrics-server가 매번 그대로 받아오는 게 아니라, metrics-server는 kubelet에서 원천 데이터를 수집하고, 너/ HPA / kubectl 이 요청할 때 API 형태로 응답을 생성하는 구조입니다.


3. kube-apiserver는 어떻게 metrics-server를 “알아서” 찾아가나?

여기서 등장하는 게 Aggregation layer 입니다.

3.1 Aggregation layer는 어디에 있나?

Aggregation layer는 별도 프로세스가 아니라, **kube-apiserver 프로세스 내부(in-process)**에서 동작합니다.

그리고 이 레이어는 기본적으로 “아무 일도 안 하다가”,

  • 어떤 확장 API가 등록되면(APIService 오브젝트),
  • 그 API 경로를 “claim”하고,
  • 해당 경로로 들어오는 요청을 뒤쪽 서버로 프록시합니다.

3.2 “라우팅 테이블” 역할을 하는 게 APIService

Aggregation layer가 “어디로 프록시할지”를 알기 위해 보는 리소스가 APIService 입니다.

공식 문서의 예시는 딱 이렇게 생겼습니다:

  • spec.service.namespace/name = 확장 API 서버가 떠 있는 Service
  • spec.service.port = 포트(기본 443)
  • spec.caBundle = 그 서버의 TLS 인증서를 검증할 CA

즉, 개념적으로 이런 매핑이 생깁니다.

  • /apis/metrics.k8s.io/v1beta1/* 로 들어온 요청은
  • kube-system/metrics-server Service로 프록시

그래서 HPA가 metrics-server 주소를 몰라도 됩니다.
HPA는 그냥 metrics.k8s.io 로 요청하고, kube-apiserver가 APIService 기반으로 라우팅해 주는 구조예요.

3.3 (운영 디테일) 인증/인가 흐름이 왜 “apiserver 중심”이냐?

Aggregation 구조는 보안 흐름도 표준화합니다.

  • kube-apiserver가 먼저 사용자 인증/인가를 수행한 뒤
  • 확장 apiserver로 프록시하고
  • 확장 apiserver는 “이 요청이 진짜 kube-apiserver가 보낸 프록시 요청인지”를 mTLS/헤더 규칙으로 검증합니다.

이게 aggregation을 kube-apiserver “밖”이 아니라 “안”에 둔 가장 실무적인 이점 중 하나입니다:
클러스터의 모든 API가 단일 관문(인증/인가/감사/디스커버리) 아래로 들어옵니다.


4. metrics-server는 15초마다 “뭘” 긁는가? (그리고 1,000노드는 커버 되나?)

네가 가진 metrics-server Pod describe에 --metric-resolution=15s 가 있었죠. 이건 “수집 템포”를 강하게 시사합니다(환경/배포마다 조정 가능). 그리고 metrics-server 프로젝트도 “15초마다 수집”을 주요 특징으로 명시합니다.

4.1 metrics-server가 긁는 대상은 “Pod”가 아니라 “각 노드 kubelet”

resource metrics pipeline 문서 기준으로:

  • kubelet은 노드/컨테이너 리소스 메트릭을 제공하며
  • 메트릭은 kubelet의 /metrics/resource (및 일부 /stats) 엔드포인트에서 접근 가능하고
  • metrics-server는 각 노드 kubelet을 HTTP로 쿼리해서 메트릭을 수집/집계합니다.

즉 “모든 Pod 메트릭”을 얻는 방식은:

  • Pod마다 붙어서 수집이 아니라
  • 노드마다 kubelet을 수집하고
  • kubelet 응답 안에 “그 노드 위 Pod/컨테이너들” 메트릭이 포함되므로
  • 노드들을 합치면 “클러스터 전체 Pod 메트릭”이 되는 구조입니다.

4.2 metrics-server는 내부 캐시도 가진다 (HPA를 위해 필요한 동작)

공식 문서에 “metrics-server는 Kubernetes API로 노드/Pod를 추적하고, 각 노드를 HTTP로 쿼리하며, Pod 메타데이터 뷰와 pod health 캐시를 유지한다”는 설명이 있습니다. (HPA 질의에서 label selector 매칭을 위해 필요)

이 말은 실무적으로:

  • HPA가 selector로 “대상 Pod 집합”을 만들 때
  • metrics-server도 그 selector에 맞는 Pod를 찾아서 메트릭을 응답해야 하니
  • Pod 메타데이터를 어느 정도는 계속 보고/캐시한다는 뜻입니다.

4.3 1,000 노드면 “커버 되나?” — 공식 가이드는 “가능” 쪽이지만, 전제 조건이 있다

metrics-server README가 제시하는 목표/가이드는 꽤 명확합니다:

  • 15초마다 수집
  • 노드당 CPU 1 millicore, 메모리 2MB 수준의 효율
  • 최대 5,000 노드 클러스터까지 스케일 지원 목표

이 “가이드 숫자”를 그대로 단순 계산하면 (가정/근사치입니다):

  • 1,000 노드 → CPU 약 1 core, Memory 약 2GB 규모

다만 이 값은 “항상 보장”이 아니라 README가 제공하는 운영 가이드/목표치에 가깝고, 실제 병목은 다음에서 갈립니다(여긴 환경 의존):

  • kubelet 응답 지연/타임아웃
  • 네트워크(SG/NACL, 노드-파드 라우팅)
  • TLS 검증/인증서 체인 문제
  • metrics-server 리소스 제한(Request/Limits)

4.4 “그럼 15초마다 노드 1000개를 다 호출?” → 보통 yes (kubelet을)

정확히 말하면:

  • 너가 본 .../apis/metrics.k8s.io/.../nodes 같은 “큰 JSON 리스트”를 15초마다 받는 게 아니라
  • metrics-server가 각 노드 kubelet로 주기적으로 요청을 날립니다.

개념적으로 부하를 감으로 보면:

  • 1,000 노드 / 15초 ≈ 초당 ~66회 kubelet scrape (평균치)
  • 순간 동시성/지연/재시도에 따라 체감은 달라질 수 있음

이건 “정확 수치”라기보다, 노드 수 × 수집 주기가 부하의 본질이라는 점을 감 잡기 위한 계산입니다.


5. “kube-proxy가 반드시 있어야 한다”는 말의 정확한 위치

너가 이전에 정리한 내용 중 가장 자주 오해되는 부분이 여기입니다.

공식 aggregation layer 설정 문서에는:

  • “API 서버가 떠 있는 호스트에서 kube-proxy를 안 돌린다면, --enable-aggregator-routing=true 를 보장해야 한다”는 문장이 있습니다.

즉 결론은:

  • 항상 kube-proxy가 ‘반드시’ 필요한 건 아니다
  • 하지만 “apiserver가 Service로 프록시 트래픽을 라우팅하는 방식”에 따라
    • kube-proxy가 필요할 수도 있고
    • 또는 apiserver 플래그로 라우팅을 보장해야 할 수도 있습니다.

관리형(Kops/EKS/GKE 등)에서는 이 부분을 플랫폼이 이미 맞춰둔 경우가 많고, 너처럼 metrics.k8s.io 호출이 정상이라면 “현재는 충족되어 있다”는 관찰이 가능합니다.


6. 여기서 자연스럽게 이어지는 “CRD/CR” — 둘 다 ‘확장’이지만, 방식이 다르다

여기까지가 “확장 API 서버(aggregated apiserver)” 방식이었습니다.
쿠버네티스 API 확장에는 큰 축이 두 개가 있는데, Kubernetes 공식 문서가 딱 이렇게 정리합니다:

  • CRD: 간단하고, “프로그래밍 없이” 추가 가능
  • API Aggregation: 프로그래밍이 필요하지만, API 동작/스토리지 등 더 많은 제어 가능

6.1 CRD는 “새 Kind를 kube-apiserver가 인식”하게 만드는 것

CRD(CustomResourceDefinition)는:

  • kube-apiserver가 새로운 리소스 타입을 “저장/서빙”할 수 있게 만드는 방식입니다.
  • 즉, “새 종류의 오브젝트를 인식”한다는 네 표현이 정확합니다.

6.2 CRD와 CR의 차이

  • CRD: 타입(스키마/Kind) 정의
  • CR(Custom Resource): 그 타입의 인스턴스(실제 오브젝트)

비유하면:

  • CRD = 테이블 스키마
  • CR = 테이블의 한 row

6.3 “컨트롤러가 없는 CRD/CR” vs “컨트롤러가 필요한 CRD/CR”의 기준

공식 문서의 핵심 문장 하나로 정리됩니다:

  • 커스텀 리소스만으로는 구조화 데이터를 저장/조회할 수 있고,
  • 커스텀 컨트롤러와 결합해야 진짜 선언적 API가 된다.

따라서 “컨트롤러가 필요 없는 CRD/CR”의 대표 목적은:

  • 클러스터에 “정의된 형태의 데이터”를 저장하고(스키마 검증, RBAC, kubectl UX)
  • 사람이 보거나, CI/CD 같은 외부 소비자가 읽는 형태

반대로 “컨트롤러가 필요한 CRD/CR”은:

  • CR을 만들면 실제로 Deployment/Service/Secret 등을 생성/조정하고
  • status를 업데이트하며
  • 원하는 상태를 계속 맞추는(reconcile) 자동화가 필요할 때입니다.

6.4 metrics-server는 왜 CRD가 아니라 aggregated apiserver인가?

이건 “공식 문장 1개로 딱 잘라 말하는” 것보다는, 위 사실들로부터 자연스럽게 추론할 수 있어요:

  • metrics-server는 kubelet에서 주기적으로 수집 + 집계 + 캐시한 결과를 API로 내줍니다.
  • 이런 성격은 “etcd에 CRUD로 저장되는 오브젝트”라기보다, “실시간/근실시간 계산 결과를 제공하는 API”에 가깝습니다.
  • 그리고 Kubernetes 문서도 CRD와 API aggregation을 “서로 다른 방식”으로 분리해 설명합니다.

그래서 metrics-server는 API aggregation(확장 API 서버) 패턴의 대표 예시가 됩니다.


7. 한 장으로 다시 정리 (요청 흐름 + 수집 흐름)

아래 두 개의 루프가 겹쳐져서 “HPA가 스케일링한다”가 됩니다.

7.1 HPA 요청 루프(컨트롤 플레인)

  • HPA 컨트롤러(= kube-controller-manager 내부)가 기본 15초마다
  • scaleTargetRef → .spec.selector 로 Pod 집합을 만들고
  • metrics.k8s.io 같은 aggregated API에서 메트릭을 가져와 스케일을 결정

7.2 metrics-server 수집 루프(데이터 파이프라인)

  • metrics-server가 kubelet의 /metrics/resource 등에서 메트릭을 수집/집계하고
  • 내부 캐시/메타데이터 뷰를 유지하며
  • Metrics API로 응답을 제공

7.3 “라우팅”을 담당하는 중간 계층(aggregation layer)

  • aggregation layer는 kube-apiserver 내부에서 동작하고
  • APIService가 path를 claim하면 그 요청을 Service로 프록시합니다.

8. 네 클러스터에서 바로 확인하는 커맨드 (체크리스트)

8.1 metrics.k8s.io가 어디로 라우팅되는지 (APIService 확인)

 
kubectl get apiservice | grep metrics
kubectl describe apiservice v1beta1.metrics.k8s.io
kubectl get apiservice v1beta1.metrics.k8s.io -o yaml
 

여기서 spec.service.*, caBundle, status.conditions(Available=True/False)가 핵심입니다.

8.2 metrics-server가 kubelet 스크랩을 실패하는지(타임아웃/인증서)

 
kubectl -n kube-system logs deploy/metrics-server --tail=200 | egrep -i "timeout|error|failed|x509"
 

8.3 Metrics API를 “개별 단위로” 조회(큰 응답 피하기)

 
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes/<nodeName>" | jq .
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/<ns>/pods/<podName>" | jq .
 

예시 형태는 공식 문서에도 나옵니다.

반응형

댓글


스킨편집 -> html 편집에서