CRD 와 오퍼레이터 · 레플리카를 늘렸는데 아무 일도 없었다 · 이론
kubectl scale 은 내 타입을 어떻게 아는가
한 줄 요약
kubectl scale 과 HPA 는 대상의 종류를 모른 채 동작한다. 둘이 아는 것은 scale 하위 리소스라는 공통 창
하나뿐이고, CRD 는 그 창을 specReplicasPath·statusReplicasPath·labelSelectorPath 세 줄로 열어 준다.
왜 이 계약이 필요했나
쿠버네티스에는 "복제본 수" 라는 개념을 가진 타입이 여럿이다. Deployment, ReplicaSet, StatefulSet,
ReplicationController, 그리고 수많은 CRD. 이들의 필드 이름은 제각각일 수 있고 실제로 그렇다. 그런데kubectl scale 은 한 줄이고 HPA 컨트롤러도 하나다. 이 도구들이 타입마다 다른 코드를 갖고 있다면 새 CRD 가
생길 때마다 kubectl 을 고쳐야 한다. 그래서 쿠버네티스는 반대로 갔다 — **도구는 고정하고, 타입이 자기를
번역해서 내놓게 한다.**
그 번역 결과가 autoscaling/v1 의 Scale 오브젝트다. 어떤 타입이든 scale 하위 리소스를 켜면GET .../shards/a/scale 이 Shard 가 아니라 Scale 을 돌려준다. 안에는 spec.replicas 한 칸,status.replicas 한 칸, 그리고 status.selector 문자열 하나뿐이다. kubectl 도 HPA 도 이 세 칸만 본다.
어떻게 동작하나
CRD 의 버전마다 subresources.scale 아래에 세 경로를 적는다.
subresources: status: {} scale: specReplicasPath: .spec.replicas statusReplicasPath: .status.replicas labelSelectorPath: .status.selector| 경로 | 누가 쓰나 | 없거나 틀리면 |
| --- | --- | --- |
| specReplicasPath | 사용자·kubectl·HPA 가 쓴다 | 필수. .spec 아래여야 한다. 값이 실제 오브젝트에 없으면 하위 리소스가 오류를 낸다 |
| statusReplicasPath | 컨트롤러가 status 에 쓴다 | 필수. .status 아래여야 한다. 오브젝트에 값이 없으면 실제 수가 0 으로 보인다 |
| labelSelectorPath | 컨트롤러가 status 에 쓰고 HPA 가 읽는다 | 선택. 없으면 HPA 가 조정을 시작하지 못한다 |
세 경로 모두 점 표기만 허용되고 배열 표기는 쓸 수 없다. labelSelectorPath 가 가리키는 자리는
구조체가 아니라 문자열 하나여야 하고, 그 안에 라벨 셀렉터를 직렬화한 형태(app=web)로 담는다.
여기서 반드시 같이 기억할 것이 있다. statusReplicasPath 가 가리키는 자리는 반드시 status 서브리소스
안이고, labelSelectorPath 도 보통 거기에 둔다(.spec 아래도 허용되지만 셀렉터는 컨트롤러가 계산해
채우는 값이라 status 가 제자리다). 그러니 status: {} 를 함께 켜지 않으면 컨트롤러가 그 값을 쓸 창 자체가 없고,
켜 두더라도 보통의 kubectl patch 로는 써지지 않는다 — --subresource=status 를 붙여야 한다. 컨트롤러가
없는 클러스터에서 kubectl scale 을 걸면 원하는 수만 5 로 바뀌고 실제 수는 0 에 머무는데, 그건 고장이
아니라 아직 아무도 그 자리를 채우지 않았다는 뜻이다.
HPA 쪽은 조금 더 재미있다. HPA 는 scaleTargetRef 에 적힌 apiVersion·kind·name 으로 대상을 찾고, 그
대상의 scale 하위 리소스를 읽는다. 이 과정이 성공했는지는 status.conditions 의 AbleToScale 에 남고,
지표를 계산해 실제로 조정할 수 있는지는 ScalingActive 에 남는다. 이 둘은 서로 다른 질문이다.
셀렉터 경로가 없으면 AbleToScale 은 True(SucceededGetScale)인데 ScalingActive 가False(InvalidSelector)가 되고, 메시지는 대상의 scale 에 셀렉터가 없다고 말한다. 화면 목록에는 HPA 가
멀쩡히 한 줄로 보인다.
현장에서 만나는 모습
첫째, 성공했다고 답하는 실패. specReplicasPath 를 .spec.replica 처럼 한 글자 틀리게 적어 두면kubectl scale 은 종료 코드 0 과 함께 scaled 를 찍는다. 그런데 오브젝트의 값은 그대로다. 같은 CRD 에kubectl get … --subresource=scale 을 물으면 그제야 "the spec replicas field ... does not exist" 가 나온다.
배포 스크립트가 종료 코드만 보고 다음 단계로 넘어가는 구조라면 이 고장은 몇 달을 산다.
둘째, 붙었는데 조정하지 않는 HPA. 셀렉터 경로를 빠뜨린 CRD 에 HPA 를 걸면 아무도 오류를 보지 않는다.
부하가 올라가도 복제본은 그대로다. 사람들은 대개 지표 파이프라인부터 의심하는데, 실제 원인은 CRD 의
세 줄 중 한 줄이 없는 것이다. 사고 회고에서 이 원인에 닿기까지 시간이 오래 걸리는 이유는 증상이
"자동 확장이 안 된다" 라서 조사 범위가 지표 쪽으로 먼저 열리기 때문이다.
셋째, 그래서 점검을 자동화한다. CRD 를 만드는 팀은 CI 에서 두 가지를 확인할 값어치가 있다 —
scale 하위 리소스를 실제로 한 번 읽어 보는 것, 그리고 자동 확장을 쓸 타입이면 셀렉터 경로가 있는지
보는 것. 정의만 읽어서는 첫 번째를 알 수 없다. 경로가 실제 오브젝트에 닿는지는 물어봐야만 나온다.
이 실습 환경의 한계
실습 파드의 클러스터에는 지표 서버(metrics-server)가 없다. 그래서 HPA 의 TARGETS 칸은 계속
알 수 없음으로 남고, 실제로 복제본 수가 부하에 따라 오르내리는 모습은 볼 수 없다. 대신 HPA 컨트롤러
자체는 돌기 때문에 "대상을 읽었는가", "왜 조정을 시작하지 못했는가" 는 조건에 그대로 기록된다 —
이 모듈이 다루는 실패는 모두 그 자리에 남는다. 또 Shard 를 돌보는 컨트롤러가 없으므로 status 는
사람이 직접 채워 컨트롤러의 자리를 흉내 낸다.
다음 실습에서 할 것
세 경로를 갖춘 CRD 를 만들어 kubectl scale 과 HPA 를 붙여 보고, 셀렉터 경로가 없는 CRD 와 경로에
오타가 있는 CRD 를 각각 만들어 어떤 침묵이 생기는지 조건 메시지로 확인한다. 마지막에는 세 CRD 를
표로 묶어 ok·nosel·broken 으로 분류하는 점검 스크립트를 만들고, 고치기 전과 후의 출력을 나란히 남긴다.