Defining a WebService Type and Registering It With the API
한국어 원문으로 표시합니다.
목표
apps.labhub.io/v1 그룹에 WebService 라는 새 리소스 타입을 정의해 API 서버에 등록하고, 스키마·커스텀 컬럼·서브리소스·복수 버전까지 갖춘 실제로 쓸 만한 CRD 를 완성합니다.
왜 중요한가
CRD 를 작성한다는 것은 "필드를 나열한다"가 아니라 검증 책임을 어디에 둘지 결정한다는 뜻입니다. 스키마에 minimum: 1 을 적으면 그 규칙은 apply 시점에 API 서버가 강제하고, 오류 메시지가 사용자에게 직접 갑니다. 반대로 컨트롤러 코드에 넣으면 잘못된 오브젝트가 이미 저장된 뒤에야 로그에서 발견됩니다. 서브리소스도 편의 기능이 아닙니다. status 를 별도 경로로 나누면 status 를 써도 metadata.generation 이 올라가지 않아서, 컨트롤러가 "사용자가 spec 을 바꾼 것"과 "내가 방금 status 를 쓴 것"을 구분할 수 있게 됩니다. 이 구분이 없으면 컨트롤러가 자기 status 쓰기에 다시 반응하는 무한 루프에 빠집니다. 마지막으로 버전은 처음부터 두 개로 시작해 보는 것이 좋습니다. 저장 버전이 정확히 하나여야 한다는 규칙과 status.storedVersions 의 존재를 몸으로 익혀 두면, 나중에 옛 버전을 지우다 데이터를 못 읽게 만드는 사고를 피할 수 있습니다.
단계
/root/crd/crd.yaml을 만드세요.apiVersion: apiextensions.k8s.io/v1,kind: CustomResourceDefinition,metadata.name: webservices.apps.labhub.io,spec.group: apps.labhub.io여야 합니다.- 같은 파일의
spec.names에plural: webservices,singular: webservice,kind: WebService,listKind: WebServiceList,shortNames: [ws],categories: [labhub]을 넣고spec.scope: Namespaced로 하세요. 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) 세 필드를 정의하세요.kubectl apply -f /root/crd/crd.yaml로 적용하고Established조건이True인지 확인한 뒤,kubectl api-resources --api-group=apps.labhub.io의 출력을/root/crd/out/api-resources.txt로 저장하세요.- v1 버전에
additionalPrinterColumns를 추가하세요.Image(type string, jsonPath.spec.image),Replicas(type integer, jsonPath.spec.replicas),Tier(type string, jsonPath.spec.tier),Age(typedate, jsonPath.metadata.creationTimestamp) 네 컬럼입니다. - 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 필드는 잘려 나가 저장되지 않습니다. 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이 있는지 확인하세요.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 적용 자체가 거부됩니다.
CRD 뼈대와 이름 규칙 맞추기
/root/crd/crd.yaml 을 만드세요. apiVersion: apiextensions.k8s.io/v1, kind: CustomResourceDefinition, metadata.name: webservices.apps.labhub.io, spec.group: apps.labhub.io 여야 합니다.
CRD 는 apiextensions.k8s.io/v1 그룹의 오브젝트입니다. metadata.name 은 자유롭게 지을 수 없고 복수형과 그룹을 점으로 이은 형태여야 합니다. 픽스처의 고장난 CRD 를 적용해 보면 어떤 문구로 거부하는지 알 수 있습니다.
이름·스코프·짧은 이름 정하기
같은 파일의 spec.names 에 plural: webservices, singular: webservice, kind: WebService, listKind: WebServiceList, shortNames: [ws], categories: [labhub] 을 넣고 spec.scope: Namespaced 로 하세요.
spec.names 에는 복수형·단수형·kind·listKind 가 각각 따로 들어갑니다. kind 는 파스칼 표기, listKind 는 kind 뒤에 List 를 붙입니다. shortNames 와 categories 는 배열입니다.
OpenAPI v3 스키마로 필드 못 박기
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) 세 필드를 정의하세요.
required 는 spec 오브젝트 안에 배열로 들어갑니다. 숫자 필드는 minimum/maximum/default 를, 문자열 필드는 enum 을 쓸 수 있습니다. 기본값은 검증을 통과한 값이어야 한다는 점에 주의하세요.
적용하고 API 목록에 나타났는지 확인하기
kubectl apply -f /root/crd/crd.yaml 로 적용하고 Established 조건이 True 인지 확인한 뒤, kubectl api-resources --api-group=apps.labhub.io 의 출력을 /root/crd/out/api-resources.txt 로 저장하세요.
적용 직후 바로 쓸 수 있는 것은 아닙니다. CRD 의 status 조건 중 하나가 True 가 되어야 API 서버가 그 타입을 받아들입니다. 새 타입이 실제로 등록됐는지는 API 리소스 목록으로 확인합니다.
kubectl get 에 보일 컬럼 붙이기
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) 네 컬럼입니다.
버전마다 additionalPrinterColumns 를 답니다. 각 컬럼은 name, type, jsonPath 세 가지가 필요하고, 시각 컬럼은 type 을 date 로 둬야 상대 시간으로 표시됩니다.
status 와 scale 서브리소스 켜기
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 필드는 잘려 나가 저장되지 않습니다.
서브리소스를 켜는 것만으로는 부족합니다. 스키마에 status 필드를 정의하지 않으면 pruning 때문에 써도 저장되지 않습니다. scale 은 세 개의 경로를 알려 줘야 하고, 그중 하나는 HPA 가 파드를 세는 데 씁니다.
두 버전 제공하고 저장 버전 하나 유지하기
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 이 있는지 확인하세요.
apiextensions/v1 에서는 모든 버전이 각자 스키마를 가져야 합니다. served 와 storage 는 다른 뜻이고, storage 가 true 인 버전은 정확히 하나여야 합니다. 적용 후 CRD 의 status 에 저장 버전이 기록되는지 보세요.
첫 커스텀 리소스 만들고 조회하기
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 으로 저장하세요.
네임스페이스를 먼저 만들어야 합니다. 커스텀 컬럼이 실제로 보이는지는 평범한 조회 출력을 저장해 확인하고, status 는 일반 조회가 아니라 서브리소스 경로로 따로 읽어 보세요. raw API 경로를 직접 때리는 방법이 있습니다.