CRD 와 오퍼레이터 · CRD 작성과 배포 · 실습
WebService 타입 정의하고 API 에 등록하기
목표
apps.labhub.io/v1 그룹에 WebService 라는 새 리소스 타입을 정의해 API 서버에 등록하고, 스키마·커스텀 컬럼·서브리소스·복수 버전까지 갖춘 실제로 쓸 만한 CRD 를 완성합니다.
왜 중요한가
CRD 를 작성한다는 것은 "필드를 나열한다"가 아니라 검증 책임을 어디에 둘지 결정한다는 뜻입니다. 스키마에 minimum: 1 을 적으면 그 규칙은 apply 시점에 API 서버가 강제하고, 오류 메시지가 사용자에게 직접 갑니다. 반대로 컨트롤러 코드에 넣으면 잘못된 오브젝트가 이미 저장된 뒤에야 로그에서 발견됩니다. 서브리소스도 편의 기능이 아닙니다. status 를 별도 경로로 나누면 status 를 써도 metadata.generation 이 올라가지 않아서, 컨트롤러가 "사용자가 spec 을 바꾼 것"과 "내가 방금 status 를 쓴 것"을 구분할 수 있게 됩니다. 이 구분이 없으면 컨트롤러가 자기 status 쓰기에 다시 반응하는 무한 루프에 빠집니다. 마지막으로 버전은 처음부터 두 개로 시작해 보는 것이 좋습니다. 저장 버전이 정확히 하나여야 한다는 규칙과 status.storedVersions 의 존재를 몸으로 익혀 두면, 나중에 옛 버전을 지우다 데이터를 못 읽게 만드는 사고를 피할 수 있습니다.
단계
1. /root/crd/crd.yaml 을 만드세요. apiVersion: apiextensions.k8s.io/v1, kind: CustomResourceDefinition, metadata.name: webservices.apps.labhub.io, spec.group: apps.labhub.io 여야 합니다.
2. 같은 파일의 spec.names 에 plural: webservices, singular: webservice, kind: WebService, listKind: WebServiceList, shortNames: [ws], categories: [labhub] 을 넣고 spec.scope: Namespaced 로 하세요.
3. spec.versions 에 name: v1 을 두고 schema.openAPIV3Schema 를 쓰세요. 최상위 type: object, properties.spec.type: object, properties.spec.required: [image] 이며 properties.spec.properties 아래에 image(type string), replicas(type integer, minimum: 1, maximum: 10, default: 1), tier(type string, enum: [dev, stage, prod], default: dev) 세 필드를 정의하세요.
4. kubectl apply -f /root/crd/crd.yaml 로 적용하고 Established 조건이 True 인지 확인한 뒤, kubectl api-resources --api-group=apps.labhub.io 의 출력을 /root/crd/out/api-resources.txt 로 저장하세요.
5. v1 버전에 additionalPrinterColumns 를 추가하세요. Image(type string, jsonPath .spec.image), Replicas(type integer, jsonPath .spec.replicas), Tier(type string, jsonPath .spec.tier), Age(type date, jsonPath .metadata.creationTimestamp) 네 컬럼입니다.
6. v1 버전에 subresources.status: {} 와 subresources.scale 을 켜세요. scale 은 specReplicasPath: .spec.replicas, statusReplicasPath: .status.replicas, labelSelectorPath: .status.selector 입니다. 동시에 스키마의 properties.status 를 type: object 로 정의하고 그 아래 replicas(integer), selector(string), observedGeneration(integer), conditions(type array, items 는 type object 이며 type·status·reason·message 는 string, lastTransitionTime 은 string) 을 넣으세요. 스키마에 없는 status 필드는 잘려 나가 저장되지 않습니다.
7. spec.versions 에 name: v1alpha1 을 추가하세요. v1alpha1 은 served: true, storage: false, v1 은 served: true, storage: true 입니다. v1alpha1 에도 스키마가 있어야 하므로 v1 의 스키마를 그대로 복사하세요. 재적용 후 kubectl get crd webservices.apps.labhub.io -o jsonpath='{.status.storedVersions}' 에 v1 이 있는지 확인하세요.
8. kubectl create ns crd-lab 으로 네임스페이스를 만들고 /opt/lab/fixtures/crd/sample-cr.yaml 을 적용해 sample 을 만드세요(spec.image 는 태그까지 포함해야 합니다). 그다음 kubectl get webservice -n crd-lab 출력을 /root/crd/out/get-ws.txt 로, kubectl get --raw /apis/apps.labhub.io/v1/namespaces/crd-lab/webservices/sample/status 출력을 /root/crd/out/status.json 으로 저장하세요.
참고
/opt/lab/fixtures/crd/broken-crd.yaml을 그대로 적용해 보면 이름 규칙과 필수 필드에 대한 API 서버의 거부 문구를 볼 수 있습니다. 이 파일은 고치는 대상이 아니라 오류를 구경하는 용도입니다.kubectl explain webservice.spec으로 방금 등록한 스키마가 문서처럼 보이는지 확인해 보세요.- 흔한 실수 1:
metadata.name을webservices로만 적는 것. CRD 이름은 반드시<복수형>.<그룹>입니다. - 흔한 실수 2: 6번에서 서브리소스만 켜고 스키마의
properties.status를 빠뜨리는 것. status 를 patch 해도 pruning 으로 전부 잘려 나갑니다. - 흔한 실수 3: 7번에서 두 버전 모두
storage: true로 두는 것. 저장 버전은 정확히 하나여야 하며 그렇지 않으면 CRD 적용 자체가 거부됩니다.
단계 8개
- CRD 뼈대와 이름 규칙 맞추기
- 이름·스코프·짧은 이름 정하기
- OpenAPI v3 스키마로 필드 못 박기
- 적용하고 API 목록에 나타났는지 확인하기
- kubectl get 에 보일 컬럼 붙이기
- status 와 scale 서브리소스 켜기
- 두 버전 제공하고 저장 버전 하나 유지하기
- 첫 커스텀 리소스 만들고 조회하기