CRD 와 오퍼레이터 · CRD 작성과 배포 · 이론
스키마·서브리소스·버전 — CRD 의 세 축
한 줄 요약
CRD 작성은 필드 목록을 적는 일이 아니라, 검증을 어디까지 API 서버에 떠넘길 수 있는지를 설계하는 일이다.
왜 이게 필요했나
컨트롤러 코드에 if spec.Replicas < 1 { return error } 같은 검사가 쌓이기 시작하면 두 가지 문제가 생긴다. 첫째, 그 검사가 도는 시점이 너무 늦다. 잘못된 오브젝트는 이미 etcd 에 저장됐고, 사용자는 kubectl apply 가 성공했다고 믿는다. 둘째, 그 규칙이 어디에도 문서화되지 않는다. 사용자는 컨트롤러 로그를 읽어야 무엇이 잘못됐는지 안다.
스키마로 옮기면 정반대가 된다. kubectl apply 가 그 자리에서 실패하고, 오류 메시지가 어떤 필드가 왜 안 되는지 알려 주고, 심지어 열거형이면 허용되는 값 목록까지 같이 알려 준다. 그리고 kubectl explain webservice.spec 이 그대로 문서가 된다.
어떻게 동작하나
CRD 를 지탱하는 축은 셋이다.
1) structural schema. 쿠버네티스 1.16 이후 모든 CRD 는 모든 필드의 타입이 OpenAPI v3 로 명시돼야 한다. 이 조건이 갖춰져야 pruning, 기본값 주입, 서버사이드 apply, CEL 검증이 동작한다. 여기서 세 가지를 명확히 구분해야 한다.
| 분류 | 의미 | 언제 쓰나 |
| --- | --- | --- |
| required | 비어 있으면 거부 | 합리적 기본값이 없는 핵심 식별자 |
| default | 비면 API 서버가 채움 | 대부분의 사람이 같은 값을 쓸 필드 |
| 아무것도 안 붙임 | 비어도 허용, 채우지 않음 | 진짜 선택적 기능 |
기본값을 줄 수 있는 필드를 굳이 required 로 만들면 사용자가 매번 같은 값을 적는 보일러플레이트가 생기고, 나중에 선택 필드로 낮추기도 어려워진다. 반대로 이미지처럼 잘못된 기본값이 위험한 필드는 명시적 거부가 낫다.
pruning 은 기본 동작이다. 스키마에 없는 필드는 저장 전에 잘려 나간다. 오타 필드가 조용히 사라지는 것이 처음에는 당황스럽지만, 이것이 "스키마가 곧 계약"을 강제하는 장치다. 임의의 키-값을 받아야 하는 자리에는 x-kubernetes-preserve-unknown-fields: true 로 예외를 열되, 그 자리만 최소로 열어야 한다. 남발하면 structural schema 의 이점을 통째로 잃는다.
2) 서브리소스. status 서브리소스를 켜면 spec 과 status 가 서로 다른 엔드포인트가 된다. 사용자는 spec 만, 컨트롤러는 status 만 쓴다. 여기서 결정적인 성질 하나가 따라온다 — status 를 써도 metadata.generation 이 올라가지 않는다. 덕분에 컨트롤러는 "사용자가 spec 을 바꾼 것"과 "내가 방금 status 를 쓴 것"을 구분할 수 있고, 이것이 무한 reconcile 을 막는 토대가 된다.
scale 서브리소스는 specReplicasPath, statusReplicasPath, labelSelectorPath 세 경로를 알려 주는 것만으로 kubectl scale 과 HPA 를 내 타입에 붙여 준다. 셀렉터 경로가 필요한 이유는 HPA 가 그 셀렉터로 파드를 세기 때문이다.
3) 버전. 한 CRD 는 여러 버전을 동시에 제공할 수 있고, 각 버전은 두 플래그를 갖는다.
served: 이 버전으로 요청을 받을 것인가storage: 이 버전으로 etcd 에 저장할 것인가 — 정확히 하나만 true
저장은 하나의 표현으로만 이루어지므로, 모든 버전은 서로 무손실 변환이 가능해야 한다. 그리고 status.storedVersions 는 "지금까지 이 CRD 로 저장된 적이 있는 버전들"을 기록한다. 저장 버전을 바꾼 뒤 기존 오브젝트를 재저장하지 않으면 이 목록에 옛 버전이 남고, 그 상태에서 옛 스키마를 지우면 저장된 오브젝트를 읽을 수 없게 된다. 버전 제거 사고의 대부분이 이 재저장 단계를 건너뛴 데서 나온다.
참고로 이 실습 환경에는 웹훅 엔드포인트를 서빙할 수단이 없어서 conversion webhook 은 다루지 않는다. 대신 strategy: None 상태에서 여러 버전을 함께 제공하며 저장 버전 규칙을 확인한다.
현장에서 만나는 모습
첫째, CRD 이름 규칙에서 막히는 첫날. metadata.name 은 반드시 <복수형>.<그룹> 이어야 한다. webservices 라고만 적으면 API 서버가 거부한다. 픽스처로 준 고장난 CRD 가 정확히 그 케이스다. 이 규칙이 있는 이유는 CRD 자체가 클러스터 스코프이고 이름이 곧 전역 유일 키이기 때문이다.
둘째, 커스텀 컬럼이 운영 품질을 바꾼다. kubectl get webservices 를 쳤을 때 NAME 과 AGE 만 나오면 아무도 그 명령을 쓰지 않는다. 운영자가 사고 대응 중에 알고 싶은 두세 개를 컬럼으로 올려 두면 그 명령 하나가 대시보드가 된다.
셋째, 열거형은 문서다. enum 을 넣으면 거부 메시지에 허용 목록이 함께 나온다. 사용자는 위키를 뒤지지 않고 오류 메시지만 읽어도 답을 안다. 검증을 스키마로 옮기는 진짜 이득은 거부 자체가 아니라 이 안내다.
다음 실습에서 할 것
두 개의 실습이 이어진다. 첫 실습에서는 apps.labhub.io/v1 의 WebService 를 처음부터 작성한다 — 이름 규칙, 스키마, 커스텀 컬럼, status 와 scale 서브리소스, 그리고 v1alpha1 과 v1 두 버전. 두 번째 실습에서는 일부러 규칙을 어긴 리소스를 던져 API 서버가 어떤 문구로 거부하는지 모으고, 기본값 주입과 pruning 을 눈으로 확인한 뒤, CEL 규칙으로 필드 간 제약까지 스키마에 넣고 검증 매트릭스를 만든다.