CNPA — 클라우드 네이티브 플랫폼 엔지니어링 어소시에이트 · 플랫폼 API 와 추상화 · 실습
지운 쿼터가 몇 초 뒤 되살아났다
목표
진짜 k3s 에서 TeamSpace 라는 플랫폼 API 에 작은 컨트롤러를 직접 붙입니다. 사용자가 TeamSpace 하나를 만들면 네임스페이스와 쿼터가 따라 생기고,
누가 손으로 고쳐도 되돌아가며, 지울 때는 정리할 일을 끝낸 뒤에 사라지는 것을 단계마다 값으로 확인합니다.
왜 중요한가
CRD 는 API 서버에 새 명사를 등록할 뿐이고, 그 명사에 의미를 주는 것은 컨트롤러입니다. 쿠버네티스의 컨트롤러는 원하는 상태(spec)와 실제 상태를
끝없이 비교해 차이를 좁히는 조정 루프이고, 오퍼레이터 패턴은 그 루프에 특정 도메인의 운영 지식을 담는 방식입니다. 플랫폼 팀이 셀프서비스 API 를
만들 때 이 구조를 따르면 저장·인가·감사·watch 는 API 서버가 주고, 팀은 조정 로직만 쓰면 됩니다.
조정이 사건이 아니라 현재 상태를 기준으로 돌기 때문에 누락된 알림이나 컨트롤러 재시작에도 결국 수렴하고, 사용자는 status 의 observedGeneration 으로
"내 변경이 반영됐는가" 를 읽습니다. 이 실습은 그 성질을 하나씩 깨 보며 확인합니다.
단계
1. /root/cnpa-op/crd.yaml 에 CRD teamspaces.platform.labhub.io 를 작성해 적용하세요. 그룹 platform.labhub.io, scope: Cluster, kind TeamSpace(plural teamspaces), 버전 v1alpha1 에 subresources.status 를 켜고, 스키마는 spec.pods(integer, 필수, 1..50)·spec.cpu(string, 기본값 "1")·status.observedGeneration(integer)·status.namespace(string) 입니다. 그다음 /root/cnpa-op/alpha.yaml 로 TeamSpace alpha(spec.pods: 4)를 만들고, /root/cnpa-op/before.json 에 uid(alpha 의 uid)와 namespace_exists(네임스페이스 team-alpha 가 있는지, 불리언)를 적으세요.
2. alpha 에 세 가지를 차례로 해 보고 그때마다 metadata.generation 을 읽으세요. ① 라벨 owner=platform 추가, ② 본 리소스 주소로 {"status":{"observedGeneration":7}} merge 패치, ③ spec.pods 를 6 으로 변경. /root/cnpa-op/generation.json 에 uid, gen_initial(시작 값), gen_after_label, gen_after_spec(③ 뒤), status_via_main(② 직후 alpha 의 .status 값, 없으면 null)을 적습니다.
3. /root/cnpa-op/reconcile.sh(실행 권한)를 작성하세요. 한 번 실행하면 모든 TeamSpace <이름> 에 대해 네임스페이스 team-<이름>(라벨 platform.labhub.io/teamspace: <이름>, 그 TeamSpace 를 가리키는 ownerReference)과 그 안의 ResourceQuota team-quota(pods 는 spec.pods, requests.cpu 는 spec.cpu)를 맞추고, status 하위 리소스에 observedGeneration(지금 generation)과 namespace 를 기록합니다. 한 번 실행한 뒤 /root/cnpa-op/reconcile.json 에 quota_uid(team-alpha 의 team-quota uid), namespace_owner_uid(team-alpha 의 ownerReference uid), second_pass_rv_changed(한 번 더 돌렸을 때 쿼터의 resourceVersion 이 바뀌었는지, 불리언)를 적으세요.
4. /etc/systemd/system/cnpa-op.service 를 만들어 reconcile.sh 를 3초 간격으로 끝없이 실행하게 하고(Restart=always), enable 하고 시작하세요. 새 TeamSpace 를 만들면 사람이 아무것도 하지 않아도 몇 초 안에 네임스페이스와 쿼터가 생겨야 합니다.
5. 컨트롤러가 도는 상태에서 team-alpha 의 team-quota 를 손으로 지우고, 새 uid 로 되살아날 때까지 걸린 초를 재세요. 이어서 되살아난 쿼터의 pods 를 99 로 패치하고 다시 6 으로 돌아오는지 보세요. /root/cnpa-op/drift.json 에 deleted_uid, restored_uid, restore_seconds(정수), edited_to(99), reverted_to(돌아온 값, 정수)를 적습니다.
6. reconcile.sh 를 고쳐 finalizer platform.labhub.io/cleanup 을 다루게 하세요. 삭제 요청이 없는 TeamSpace 에는 이 finalizer 를 붙이고(다른 finalizer 는 보존), deletionTimestamp 가 찍힌 TeamSpace 는 /root/cnpa-op/archive/<이름>.json 에 name·uid·pods 를 남긴 뒤 finalizer 를 떼어 줍니다. 그다음 TeamSpace beta(pods: 2, cpu: "500m")를 만들고 네임스페이스가 생긴 것을 본 뒤 지우세요. /root/cnpa-op/cleanup.json 에 uid(beta 의 uid), blocked_seen(삭제 직후 beta 가 deletionTimestamp 를 단 채 아직 남아 있는 것을 봤는지, 불리언), namespace_gone(끝난 뒤 team-beta 가 사라졌는지, 불리언)을 적습니다.
7. cnpa-op.service 를 멈추고 alpha 의 spec.pods 를 8 로 바꾸세요. 8초 기다린 뒤 alpha 의 metadata.generation·status.observedGeneration 과 쿼터의 pods 를 읽고, 서비스를 다시 시작해 둘이 맞춰질 때까지 기다리세요. /root/cnpa-op/catchup.json 에 generation, observed_while_stopped, quota_pods_while_stopped, observed_after_start, quota_pods_after_start(쿼터 값은 문자열 그대로)를 적습니다.
8. /root/cnpa-op/report.json 에 crd_alone_created_namespace(1단계), status_bumps_generation(status 기록이 generation 을 올렸는지 — 2단계 또는 지금 alpha 에서 확인), restore_seconds(5단계), finalizer(이름), archived(archive 디렉터리의 beta.json 에 적힌 uid), generations_behind_while_stopped(7단계의 generation 과 observed_while_stopped 의 차), trigger(level 또는 edge 중 이 컨트롤러가 따르는 방식)를 적으세요.
참고
- VM 안에 k3s 가 있고
kubectl,jq,python3,systemctl을 쓸 수 있습니다. 컨테이너 이미지는 쓰지 않습니다. - status 쓰기:
kubectl patch teamspace <이름> --subresource=status --type merge -p '{"status":{...}}'. - 흔한 실수: status 를 본 리소스 주소로 패치하는 것. status 하위 리소스가 있으면 서버가 조용히 버리고
patched (no change)만 출력합니다. - 흔한 실수: finalizer 를 붙이는 패치가 기존 목록을 통째로 덮는 것. 다른 컨트롤러의 finalizer 가 사라집니다.
- 흔한 실수: 컨트롤러가 멈춘 채로 실습을 끝내는 것. 4·5·6·7단계 채점기는 컨트롤러가 돌고 있어야 통과합니다.
- [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) · [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) · [CRD status subresource](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#status-subresource) · [Finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/) · [Owners and Dependents](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/) · [CNCF Platforms White Paper](https://tag-app-delivery.cncf.io/whitepapers/platforms/)
단계 8개
- 타입만 등록하면 아무 일도 일어나지 않는다
- 무엇이 generation 을 올리는가
- 몇 번을 돌려도 같은 결과
- 한 번이 아니라 계속 돈다
- 지운 쿼터가 몇 초 뒤 되살아났다
- 지우기 전에 해야 할 일이 있다
- 멈춘 동안의 변경을 놓치지 않는다
- 컨트롤러가 한 일을 값으로 남긴다