Kubernetes Custom Resource
CRD는 API에 타입을 하나 늘리고, 컨트롤러가 그 선언에 의미를 부여한다
Custom Resource는 Kubernetes API에 우리만의 타입을 쓸 수 있게 해준다.
Intro
Kubernetes API는 Pod, Service, Deployment 같은 정해진 타입의 집합이다.
그런데 클러스터를 실제로 운영하다 보면 더 높은 자유도가 필요할 때가 있다.
Label, Annotation 으로 몸 비틀어 해결하기도 힘든 순간이 온다.
그 타입을 클러스터에 심는 개념이 커스텀 리소스(Custom Resource)이고, 그 정의를 담는 object가 CRD(CustomResourceDefinition)다.
CRD는 이제 특수 기능이 아니다. Kubernetes 문서도 “many core Kubernetes functions are now built using custom resources, making Kubernetes more modular”라고 적는다.
VM을 object로 다루는 KubeVirt, AI 배치 작업을 묶어 스케줄링하는 Volcano, 여러 클러스터를 하나의 허브에서 관리하는 OCM(Open Cluster Management), 그리고 cert-manager, Prometheus Operator, Crossplane까지 전부 CRD 를 사용한다.
이 글은 CRD를 왜 쓰는지, 어떻게 쓰는지, Kubernetes가 그것을 어떻게 인식하는지 등을 본다.
Why a Custom Resource
먼저 Resource 부터
Kubernetes API는 타입이 있는 object의 집합이다.
Pod, Service, Deployment, ConfigMap. 각 타입마다 REST 엔드포인트가 있고, kubectl get pods는 그 엔드포인트를 두드리는 것이다.
실제로 각 타입별 엔드포인트는 이렇게 생겼다. 특정 namespace의 리소스를 조회하는 경로만 추리면 다음과 같다.
GET /api/v1/namespaces/{namespace}/pods
GET /api/v1/namespaces/{namespace}/services
GET /apis/apps/v1/namespaces/{namespace}/deployments
GET /api/v1/namespaces/{namespace}/configmaps
Pod, Service, ConfigMap은 초기부터 있던 core group 이고, deployments 는 나중에 나와서 apps 라는 그룹에 속한다.
이 타입들은 API server에 기본 내장돼 있다. 커스텀 리소스란 기본 설치에는 없고, 특정 클러스터에 나중에 더해진 타입을 말한다.
그런데 왜 굳이 내 개념을 하나의 타입으로 만들까?
설정 파일 하나로 두거나, 자체 DB를 가진 별도 서비스로 짜도 될 텐데.
이유는 Kubernetes API object가 되는 순간 생태계에 묻어갈 수 있다.
declarative reconcile, kubectl, declarative apply, watch, RBAC, Label Selector 등 기능들이 공짜로 붙는다.
What People Build With It
이미 다른 글에서 다뤘던 OCM, KubeVirt, Volcano, Argo 등도 CRD 를 다룬다.
모두 원래 Kubernetes Object 가 아니던 것을 First-Class API Object 로 만들고, Reconcile Controller 를 적용한다.
KubeVirt는 가상 머신을 object로 끌어들인다. VirtualMachine과 VirtualMachineInstance 타입을 열고, virt-controller가 VirtualMachine을 watch하다가 그에 맞는 인스턴스와 virt-launcher Pod을 만든다. VM이 죽으면 원하는 상태와 어긋나므로 다시 만든다. Kubernetes가 원래 몰랐던 VM이 Pod과 나란히 kubectl로 다뤄지는 object가 된다.
Volcano는 배치 작업의 스케줄링을 object로 다룬다. 기본 스케줄러는 Pod을 하나씩 배치하는데, 분산 학습처럼 N개의 Pod이 동시에 떠야 의미가 있는 작업에는 이게 약점이 된다. 절반만 뜨고 자원이 부족하면 그대로 교착이다. Volcano는 PodGroup으로 gang scheduling(전부 아니면 전무로 함께 배치)을, Queue로 공정 배분을, Job으로 그 위의 배치 작업을 선언하게 하고, 자체 컨트롤러와 스케줄러가 이를 현실로 옮긴다.
OCM(Open Cluster Management)은 한 단계 위를 다룬다. 대상이 클러스터 한 대 전체다. 허브 클러스터에서 ManagedCluster가 등록된 스포크 클러스터 하나를 나타내고, ManifestWork가 그 스포크에 배포할 리소스 묶음을, Placement가 조건으로 대상 클러스터를 고른다. 클러스터의 함대가 허브 위 object의 집합이 된다.
셋의 공통점이 정확히 이 글의 논지다.
How to Use It
실제로 쓰는 순서는 세 단계다.
먼저 CRD Manifest 를 쓴다.
group, version, kind, 복수형과 단수형 이름, scope(Namespaced인지 Cluster인지), 그리고 필드를 기술하는 OpenAPI v3 스키마를 담아 apply한다.
이 한 번으로 그 타입이 클러스터의 정식 리소스가 된다.
물론 CRD를 심고 object를 만들었다고 무언가가 저절로 동작하지는 않는다.
CRD만 있는 상태에서 그 object는 그냥 typed된 저장소다.
API server가 스키마를 검증하고 etcd에 넣어 주는게 전부고, 그 선언을 읽고 무언가를 하는 주체는 아직 없다.
커스텀 리소스를 커스텀 컨트롤러와 결합할 때, 비로소 진짜 declarative API가 된다.
즉 CRD는 절반이다. 어떠한 명사를 만들 뿐이고, 동사는 컨트롤러가 채운다.
컨트롤러가 하는 일은 하나의 루프다. 원하는 상태(desired state)를 읽고, 현재 상태(current state)와 비교하고, 차이를 좁히는 행동을 하고, 다시 관찰한다. 이 루프를 reconcile이라 부른다.
커스텀 리소스와 컨트롤러를 이렇게 묶은 것을 오퍼레이터(Operator)라 부른다.
여기서 control plane이 원래 하던 일과 겹친다는 게 보인다.
Deployment가 ReplicaSet을 맞추고 ReplicaSet이 Pod을 맞추는 것도 같은 루프다.
How Kubernetes Recognizes It
그러면 apply 한 번에 API server 안에서는 무슨 일이 일어나는가.
API server는 CRD를 읽고 /apis/<group>/<version>/<plural> 경로를 새로 연다.
이 경로가 discovery에 등록되고, 그래서 kubectl이 그 타입의 존재를 찾아낸다.
그 순간부터 그 타입의 object를 만들고 조회할 수 있다.
참고로, 여기서 말하는 discovery는 API server가 “내가 어떤 타입을, 어떤 경로로, 어떤 동사(get, list, watch 등)로 다루는지”를 스스로 나열해 주는 목록이다.
kubectl은 뜰 때 이 목록을 받아 pods가 /api/v1 아래 namespaced 리소스라는 걸 알아낸다.
CRD를 apply하면 새 타입이 이 목록에 실리므로, kubectl을 다시 빌드하지 않아도 곧바로 인식된다.
kubectl api-resources로 확인 가능
암튼 kubectl은 둘을 구분하지 않는다. kubectl get pods와 kubectl get <some-crd>는 같은 창구를 두드린다.
커스텀 리소스는 내장 object와 마찬가지로 etcd에 들어간다.
CRD가 하는 일은 이 창구에 타입 하나를 등록하는 것뿐이고, 나머지는 코어 API server가 원래 갖고 있던 기능을 그대로 빌려 쓴다.
그래서 타입을 정의하는 순간, 별도 서버를 짜지 않아도 다음이 그냥 따라온다.
| 기능 | 내용 |
|---|---|
| CRUD와 kubectl | kubectl get, create, apply가 즉시 동작 |
| Watch | 변경을 스트림으로 구독, 컨트롤러가 이걸로 돈다 |
| 인증과 권한 | 코어 API server의 authn, RBAC를 그대로 재사용 |
| Admission webhook | 생성, 수정, 삭제 시 검증과 기본값 주입 |
| Finalizer | 삭제 전 정리 작업을 걸어 둠 |
| 라벨과 셀렉터 | 기본 object와 동일한 라벨 규약, discovery |
/status, /scale | 상태 보고와 스케일을 subresource로 분리 |
프로그래밍 없이 CRD 하나로 “Kubernetes 시민권을 가진 새 타입”을 얻는 셈이다. 이게 CRD의 가장 큰 매력이다.
Where It Bites
그러나 공짜로 얻는 것이 있으면 잃는 것들도 있는 법.
가장 근본적인 함정은 앞서 본 그대로다. CRD는 데이터일 뿐이다.
선언과 현실의 간극을 메우는 컨트롤러는 여전히 누군가 짜야 하고, 그 컨트롤러는 사실상 분산 시스템 하나다.
reconcile 루프의 버그, 무한 재시도, 두 컨트롤러가 같은 리소스를 두고 다투는 상황은 그대로 프로덕션 장애가 된다. 오퍼레이터를 쓴다는 건 컨트롤러를 운영한다는 뜻이다.
따라서 본인이 CRD 를 만들고 싶다면 그 운영 부담까지 그대로 떠안아야 한다.
저장 비용도 무시하면 안 된다. 커스텀 리소스는 ConfigMap과 똑같이 etcd 공간을 먹는다. 문서는 명확히 경고한다.
Avoid using a Custom Resource as data storage for application, end user, or monitoring data: architecture designs that store application data within the Kubernetes API typically represent a design that is too closely coupled.
object 하나가 수 kB를 넘거나, 수천 개를 넘거나, 초당 수십 번씩 접근하는 용도라면 CRD는 맞는 그릇이 아니다.
근데 사실 이 외에도 많다…
- RBAC: 새 타입에는
cluster-admin과 와일드카드 롤을 빼면 아무도 접근 권한이 없다. 명시적으로 grant해야 한다. - versioning: 스키마를
v1alpha1에서v1으로 올리면, 저장된 데이터를 새 버전으로 옮기고 conversion webhook을 붙이고storedVersions를 정리해야 한다. - 트랜잭션 없음: 여러 CR을 원자적으로 바꿀 수 없다. 각 object는 따로 수렴한다.
- 검증: 스키마로 많은 걸 잡지만, 다른 object를 참조하는 교차 검증이나 상태 기반 검증은 여전히 admission webhook이 필요하다.
그리고 이 versioning 문제는 배포 도구와 만나면 더 고약해진다.
대표적인 게 Helm이다. Helm 3은 차트의 crds/ 디렉터리에 둔 CRD를 helm install 때 한 번 설치하는데, 이미 같은 CRD가 있으면 경고만 남기고 건너뛴다. 그리고 결정적으로 helm upgrade는 CRD를 아예 건드리지 않는다.
There is no support at this time for upgrading or deleting CRDs using Helm. This was an explicit decision after much community discussion due to the danger for unintentional data loss.
즉 차트 버전을 올려도 CRD 스키마는 옛날 그대로 남는다. 새 필드를 추가한 CRD를 반영하려면 사람이 직접 kubectl apply로 CRD를 갱신하거나, CRD를 별도 차트로 떼어 관리해야 한다.
References
- Kubernetes — Custom Resources
https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ - Kubernetes — CustomResourceDefinitions
https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/ - Kubernetes — Operator pattern
https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ - Kubernetes — API Aggregation Layer
https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/ - Kubernetes — CRD Versioning
https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/ - KubeVirt — User Guide
https://kubevirt.io/user-guide/ - Volcano — Documentation
https://volcano.sh/en/docs/ - Open Cluster Management — Documentation
https://open-cluster-management.io/ - The Kubebuilder Book
https://book.kubebuilder.io/