CNPA — Cloud Native Platform Engineering Associate
You Learn the Contract by Being Rejected
한국어 원문으로 표시합니다.
한 줄 요약
CRD 의 값어치는 API 서버의 검증·기본값·RBAC·감사를 빌려 오는 데 있습니다. 무엇이든 받아 주는 클러스터에서는 그중 무엇도 확인할 수 없습니다.
왜 거절당해 봐야 하나
앞 모듈에서 CRD 로 플랫폼 API 를 만들었습니다. 그런데 그 실습이 도는 곳에는 API 서버의 능력이 없어서, 무엇을 넣어도 받아 주었습니다.
CRD 는 "API 서버가 이미 가진 능력을 내 타입에 빌려 준다" 는 물건입니다. 그 능력이란 이것입니다.
검증 스키마에 안 맞으면 거절한다
기본값 빠진 값을 저장 시점에 채운다
RBAC 다른 자원과 똑같이 권한을 건다
감사 누가 언제 무엇을 바꿨는지 남는다
watch 컨트롤러가 변화를 구독한다
문서 kubectl explain 이 스키마를 읽어 준다
직접 API 를 만들면 이 여섯을 전부 다시 만들어야 합니다. 그런데 받아 주기만 하는 클러스터에서는 그중 무엇도 확인할 수 없었습니다.
조용히 지워지는 필드
구조적 스키마는 기본이 잘라내기입니다. properties 에 없는 필드는 저절로 지워집니다 — 막는 설정을 켠 것이 아닙니다. replicas 를 replica 로 잘못 쓰면 그 값이 사라지고 아무도 알려 주지 않습니다.
additionalProperties: false 를 쓰려다 물리는 자리이기도 합니다. properties 와 함께 쓸 수 없습니다(Forbidden: mutual exclusive). 쓸 필요가 없어서 막아 둔 것입니다.
반대로 x-kubernetes-preserve-unknown-fields: true 를 넣으면 잘라내기도 검증도 통째로 꺼집니다. 편하다고 넣는 순간 모든 담장이 사라집니다.
기본값은 누가 채우느냐가 다르다
스키마의 default 저장 시점에 채워져 kubectl get -o yaml 에 바로 보인다
컨트롤러가 채움 한참 뒤에 나타난다. 그 사이에는 비어 있다
개발자가 확인했을 때 값이 있어야 무슨 일이 일어날지 압니다. 골든 패스는 여기서 만들어집니다.
스키마를 바꿀 때 무엇이 깨지나
CRD 는 배포하고 나면 이미 저장된 오브젝트가 있습니다. 스키마를 바꾸는 것은 저장된 데이터를 다시 해석하는 일 이라 규칙이 있습니다.
| 변경 | 안전한가 | 이유 |
|---|---|---|
| 선택 필드 추가 | ✅ | 옛 오브젝트는 그 필드가 비어 있을 뿐 |
| 필수 필드 추가 | ❌ | 옛 오브젝트가 검증에 걸려 수정도 못 하게 된다 |
| 필드 삭제 | ⚠️ | 값이 잘려 나간다. 되돌릴 수 없다 |
| 타입 변경(string→int) | ❌ | 저장된 값을 못 읽는다 |
| enum 값 추가 | ✅ | |
| enum 값 삭제 | ❌ | 그 값을 쓰던 오브젝트가 무효가 된다 |
필수 필드를 더해야 한다면 새 버전 을 만듭니다(v1alpha1 → v1beta1).
storage: true 인 버전은 하나뿐이고, 나머지는 변환(conversion)을 거쳐 제공됩니다.
변환 웹훅이 없으면 None 전략이라 필드가 그대로 통과하므로, 구조가 다르면
웹훅을 세워야 합니다.
검증을 어디까지 스키마로 할 수 있나
OpenAPI 스키마로 되는 것과 안 되는 것이 갈립니다.
properties:
replicas:
type: integer
minimum: 1
maximum: 100
default: 3
tier:
type: string
enum: [bronze, silver, gold]
name:
type: string
pattern: '^[a-z][a-z0-9-]{2,30}$'
여기까지는 스키마로 됩니다. 필드 사이의 관계 는 안 됩니다 — "tier 가 gold 면 replicas 가 5 이상" 같은 것입니다. 예전에는 웹훅이 필요했지만, 지금은 CEL 검증 규칙으로 CRD 안에서 표현할 수 있습니다.
x-kubernetes-validations:
- rule: "self.tier != 'gold' || self.replicas >= 5"
message: "gold 등급은 복제본이 5개 이상이어야 합니다"
- rule: "self.name == oldSelf.name" # 불변 필드
message: "name 은 만든 뒤에 바꿀 수 없습니다"
웹훅보다 나은 점이 분명합니다. 별도 배포가 없고, 웹훅이 죽어서 API 가 멈추는 일이 없으며, 오류 메시지를 스키마와 같은 자리에 둡니다. 웹훅은 CEL 로 표현할 수 없을 때만 씁니다.
상태를 어디에 두나
status 는 spec 과 분리해 서브리소스로 둡니다.
subresources:
status: {}
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.replicas
이렇게 하면 사용자가 status 를 못 고치고 컨트롤러가 spec 을 못 고칩니다. 권한이
자연스럽게 갈립니다. scale 서브리소스를 두면 kubectl scale 과 HPA 가 그대로
동작합니다 — 커스텀 리소스에 HPA 를 붙이는 방법이 이것입니다.
실무에서 진짜 중요한 것
오타는 오류가 아니라 침묵으로 돌아옵니다. 구조적 스키마는 기본이 잘라내기라, replicas 를 replica 로 쓰면 값이 조용히 사라집니다. 플랫폼 API 를 낼 때는 kubectl get -o yaml 로 저장된 결과를 되읽어 보라고 안내문에 적어 두는 편이 낫습니다.
x-kubernetes-preserve-unknown-fields: true 는 마지막 수단입니다. 편하다고 넣는 순간 잘라내기도 검증도 통째로 꺼져서, 그 타입에 세워 둔 담장이 전부 사라집니다.
기본값은 컨트롤러가 아니라 스키마에 둡니다. 스키마의 default 는 저장 시점에 채워져 개발자가 바로 확인할 수 있지만, 컨트롤러가 채우면 그 사이에는 비어 있습니다. 골든 패스는 이 차이에서 만들어집니다.
다음 실습에서 이것들을 진짜 API 서버 위에서 직접 거절당해 가며 확인합니다.