Skip to content

feat(ksmcompat): 하드코딩 스텁을 실제 K8s API 수집으로 교체 - #36

Merged
eightynine01 merged 1 commit into
mainfrom
feat/ksmcompat-real-api
Aug 12, 2026
Merged

feat(ksmcompat): 하드코딩 스텁을 실제 K8s API 수집으로 교체#36
eightynine01 merged 1 commit into
mainfrom
feat/ksmcompat-real-api

Conversation

@eightynine01

Copy link
Copy Markdown
Contributor

의존: #35Makefile 한 줄(chart-testcompatibility-check.sh 연결)이 겹칩니다. 내용이 동일해 3-way merge 로 해결되지만, #35 를 먼저 머지하는 편이 깔끔합니다.

ksmcompatKubernetes API 를 한 번도 호출하지 않았습니다. apiURL·token·client·lastFetch 필드를 선언해 두고 어디서도 쓰지 않았고, 방출하던 값은 전부 리터럴이었습니다:

메트릭 스텁이 내던 값 실제
kube_node_status_capacity{resource="cpu"} 항상 16 노드마다 12 / 16 / 32
kube_node_info{kubeproxy_version} 항상 "v1.28.0" k3s v1.36.2
kube_pod_info 자기 자신 1개를 조작 노드당 수십 개
kube_daemonset_status_number_ready 항상 1 11

켜면 관측 스택이 이것을 사실로 받아 저장합니다. 이 클러스터의 vmalert 룰은 kube_node_status_condition·kube_daemonset_status_number_ready·kube_deployment_*실제로 사용하므로, 진짜 kube-state-metrics 시리즈 옆에 job 라벨만 다른 거짓 시리즈가 나란히 서게 됩니다.

수집 범위를 노드 스코프로 확정

kube-state-metrics 는 단일 Deployment 로 클러스터 전체를 보고합니다. nodevitals 는 DaemonSet 이라 노드마다 한 벌 도는데, 각 파드가 전역 객체를 보고하면 같은 시리즈가 노드 수만큼 생깁니다(이 클러스터에서 11벌). 메모리도 KSM 실측 265Mi 급이 노드마다 필요해져 128Mi limit 과 양립하지 않습니다.

그래서 이 에이전트가 사실로서 말할 수 있는 것만 냅니다 — 자기 노드와 그 노드에 스케줄된 파드(서버 쪽 fieldSelector=spec.nodeName 로 좁힘). mode: cluster 는 조용히 node 로 강등하지 않고 거부합니다(helm 렌더 단계 + 에이전트 기동 시점 양쪽).

⚠️ 결과적으로 kube-state-metrics 를 은퇴시키지는 못합니다. kube_deployment_*·kube_daemonset_* 는 내지 않습니다. dcgmCompat·smartctlCompat 이 대체에 성공한 이유는 원본이 애초에 DaemonSet 이었기 때문이고, KSM 은 전제가 다릅니다. values 주석에 명시했습니다.

내는 것: kube_node_info / kube_node_status_condition / kube_node_status_capacity / kube_node_status_allocatable / kube_pod_info / kube_pod_status_phase / kube_pod_container_status_ready / _restarts_total

실패는 실패로 보이게

  • 기동 거부: 토큰 없음 / cluster 모드 / 오타 난 mode — 그대로 두면 증상이 "메트릭이 안 나온다" 하나뿐이라 원인까지 멀어집니다.
  • 스크레이프 실패: API 도달 불가·403 은 폴백 대신 아무 시리즈도 내지 않고 InvalidMetric 으로 에러 경로에 실어 보냅니다. RBAC 이 모자란 상태가 "건강한 클러스터"처럼 보이면 안 됩니다.
  • Quantity 파싱: 읽을 수 없는 값에 0 을 돌려주면 "이 노드는 용량이 0"이라는 거짓이 메트릭이 되므로 에러로 올리고 그 리소스만 건너뜁니다. KikGi 에서 이미 7% 넘게 벌어지므로 이진/십진 접두사를 구분합니다.

차트 RBAC

ksmCompat.enabled 일 때만 ServiceAccount + ClusterRole + Binding 이 렌더되고 파드가 토큰을 마운트합니다. 꺼져 있으면 아무것도 생기지 않습니다.

권한은 실제로 읽는 두 리소스로 한정했습니다 — nodes:get, pods:list. KSM 의 ClusterRole(20+ 리소스 list/watch)을 복사하지 않습니다: 읽지 않는 리소스의 열람 권한은 이 파드가 탈취됐을 때 그대로 클러스터 열람 권한이 됩니다. 회귀 가드가 secrets·configmaps·deployments·statefulsets 가 섞여 들어오는 것을 막습니다.

kube_* 표면은 core·singlePod tier 에서만 렌더되므로 SA 도 그 둘에만 붙습니다.

검증

  • go test ./... 통과. ksmcompat 테스트 전면 재작성 — httptest 로 API 를 세워 값이 API 에서 온 것인지(cpu=7, kernel=6.17.0-test 같은 특이값), fieldSelector 가 자기 노드로 좁히는지, 도달 불가/403 에서 메트릭이 0개인지, 잘못된 mode/토큰이 거부되는지 확인
  • Quantity 파서 단위 테스트(이진/십진/밀리/지수 표기 + 쓰레기값 거부)
  • make chart-lint, compatibility-check.sh §6·§7 신설(게이트·최소권한·SA 배선·cluster 거부)

알려진 무관 이슈

make chart-testtier-runtime.sh 가 macOS 로컬에서 3회 중 1회 간헐 실패합니다(단독 실행·순차 실행·최소 Makefile·CI 전부 통과). 이 PR 과 무관한 기존 flaky 이며, 실패 메시지가 "이미지 태그가 appVersion 과 다르다"로 나오지만 helm 호출이 빈 출력을 내도 같은 메시지가 나오는 구조라 원인을 가립니다. 별도 처리가 필요합니다.

ksmcompat 은 Kubernetes API 를 한 번도 호출하지 않았다. apiURL·token·client·
lastFetch 필드를 선언해 두고 어디서도 쓰지 않았고, 방출하던 값은 전부 리터럴
상수였다 — capacity cpu=16, memory=64Gi, kubeproxy_version="v1.28.0", 파드는
자기 자신 하나를 지어낸 것, cluster 모드는 특정 이름의 Deployment/DaemonSet/PVC
세 줄.

켜면 관측 스택이 이 값들을 사실로 받아 저장한다. 이 클러스터의 vmalert 룰은
kube_node_status_condition·kube_daemonset_status_number_ready·kube_deployment_*
를 실제로 쓰므로, 노드마다 제각각인 실제 용량(cpu 12/16/32) 대신 16 이 들어가고
DaemonSet ready=1(실제 11)이 들어간다. 진짜 kube-state-metrics 시리즈 옆에
job 라벨만 다른 거짓 시리즈가 나란히 서게 된다.

## 수집 범위를 노드 스코프로 확정

kube-state-metrics 는 단일 Deployment 로 클러스터 전체를 보고한다. nodevitals 는
DaemonSet 이라 노드마다 한 벌 도는데, 각 파드가 전역 객체를 보고하면 같은
시리즈가 노드 수만큼 생긴다(이 클러스터에서 11벌). 메모리도 KSM 실측 265Mi 급이
노드마다 필요해져 128Mi limit 과 양립하지 않는다.

그래서 이 에이전트가 사실로서 말할 수 있는 것만 낸다 — 자기 노드와 그 노드에
스케줄된 파드. 파드 목록은 서버 쪽 fieldSelector(spec.nodeName)로 좁힌다.
mode "cluster" 는 조용히 node 로 강등하지 않고 **거부**한다(helm 렌더 단계 +
에이전트 기동 시점 양쪽). 설정한 사람이 그 사실을 알아야 하기 때문이다.

그 결과 kube_deployment_* · kube_daemonset_* 는 내지 않으므로 **kube-state-metrics
를 은퇴시키지는 못한다**. dcgmCompat·smartctlCompat 이 대체에 성공한 이유는
원본이 애초에 DaemonSet 이었기 때문이고, KSM 은 전제가 다르다. values 주석에
그대로 적었다.

## 실패는 실패로 보이게

토큰 없음 / cluster 모드 / 오타 난 mode 는 기동 시점에 거부한다 — 그대로 두면
증상이 "메트릭이 안 나온다" 하나뿐이라 원인까지 도달하는 데 시간이 걸린다.
스크레이프 중 API 도달 불가·403 은 하드코딩 폴백 대신 아무 시리즈도 내지 않고,
InvalidMetric 으로 스크레이프 에러 경로에 실어 보낸다. RBAC 이 모자란 상태가
"건강한 클러스터" 처럼 보이면 안 된다.

Quantity 파싱도 같은 원칙이다. 읽을 수 없는 값에 0 을 돌려주면 "이 노드는 용량이
0" 이라는 거짓이 메트릭이 되므로 에러로 올리고 그 리소스만 건너뛴다. Ki 와 k 는
Gi 에서 이미 7% 넘게 벌어지므로 이진/십진 접두사를 구분한다.

## 차트

ksmCompat.enabled 일 때만 ServiceAccount + ClusterRole + Binding 이 렌더되고
파드가 토큰을 마운트한다. 꺼져 있으면 아무것도 생기지 않는다 — 다른 tier 는
/proc·/sys·/dev 와 NVML 만 읽으므로 API 자격이 필요 없다.

권한은 실제로 읽는 두 리소스로 한정했다(nodes:get, pods:list). KSM 의
ClusterRole(20+ 리소스 list/watch)을 복사하지 않는다: 읽지 않는 리소스의 열람
권한은 이 파드가 탈취됐을 때 그대로 클러스터 열람 권한이 된다. 회귀 가드가
secrets·configmaps·deployments·statefulsets 가 섞여 들어오는 것을 막는다.

kube_* 표면은 core·singlePod tier 에서만 렌더되므로 SA 도 그 둘에만 붙인다.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a58bcbf18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+e.token)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reload expiring service-account tokens before requests

On Kubernetes clusters using the default projected service-account token volume, kubelet rotates the token file while the process keeps running. Because each request keeps using the token string captured during startup, once that token expires the API starts returning 401s and the kube_* surface disappears after the cache TTL until the pod is restarted; reread the mounted token or use a token source for each API call.

Useful? React with 👍 / 👎.

snap, err := e.load()
if err != nil {
e.log.Warn("ksmcompat scrape failed — emitting no kube_* series", "node", e.node, "err", err)
ch <- prometheus.NewInvalidMetric(scrapeErrorDesc, err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make API failures visible to Prometheus scrapes

When the API or RBAC path fails, this sends an invalid metric and returns, but this app serves /metrics with promhttp.ContinueOnError, so the handler logs the gather error and can still return HTTP 200 with the other nodevitals series. In that failure mode Prometheus will not see up=0 or any kube_state_metrics_scrape_error sample; the kube_* series just vanish, so emit a real error gauge/counter or use an error mode that fails the scrape.

Useful? React with 👍 / 👎.

Comment on lines +80 to +81
case "memory", "ephemeral-storage":
return "byte"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Label hugepage resources with byte units

On Linux nodes that report hugepages-* resources, those capacity and allocatable entries fall through to the default integer unit even though kube-state-metrics exposes hugepage resources with unit="byte". Since unit is part of the series identity, existing KSM-compatible hugepage queries will not match the nodevitals series for those resources.

Useful? React with 👍 / 👎.

@eightynine01
eightynine01 merged commit 93a4e4c into main Aug 12, 2026
2 checks passed
@eightynine01
eightynine01 deleted the feat/ksmcompat-real-api branch August 12, 2026 11:06
eightynine01 added a commit that referenced this pull request Aug 12, 2026
릴리스 파이프라인은 이미 발행된 appVersion 을 skip 하므로(멱등), #36 이 착지한
뒤에도 0.9.1 이미지는 재빌드되지 않았다 — 즉 ksmcompat 실구현이 어느 발행
이미지에도 들어 있지 않다. main 의 코드와 발행 아티팩트가 어긋난 채로 두면,
0.9.1 을 받는 쪽은 ksmCompat 을 켰을 때 여전히 하드코딩 스텁을 얻는다.

Co-authored-by: phil <phil@keiailab.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants