LabHub
배우기 러닝패스 코스

CNPA — クラウドネイティブプラットフォームエンジニアリングアソシエイト

削除したクォータが数秒後に復活した

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

진짜 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), 버전 v1alpha1subresources.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.jsonuid(alpha 의 uid)와 namespace_exists(네임스페이스 team-alpha 가 있는지, 불리언)를 적으세요.
  2. alpha 에 세 가지를 차례로 해 보고 그때마다 metadata.generation 을 읽으세요. ① 라벨 owner=platform 추가, ② 본 리소스 주소로 {"status":{"observedGeneration":7}} merge 패치, ③ spec.pods6 으로 변경. /root/cnpa-op/generation.jsonuid, 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.jsonquota_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-alphateam-quota 를 손으로 지우고, 새 uid 로 되살아날 때까지 걸린 초를 재세요. 이어서 되살아난 쿼터의 pods99 로 패치하고 다시 6 으로 돌아오는지 보세요. /root/cnpa-op/drift.jsondeleted_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/<이름>.jsonname·uid·pods 를 남긴 뒤 finalizer 를 떼어 줍니다. 그다음 TeamSpace beta(pods: 2, cpu: "500m")를 만들고 네임스페이스가 생긴 것을 본 뒤 지우세요. /root/cnpa-op/cleanup.jsonuid(beta 의 uid), blocked_seen(삭제 직후 beta 가 deletionTimestamp 를 단 채 아직 남아 있는 것을 봤는지, 불리언), namespace_gone(끝난 뒤 team-beta 가 사라졌는지, 불리언)을 적습니다.
  7. cnpa-op.service 를 멈추고 alpha 의 spec.pods8 로 바꾸세요. 8초 기다린 뒤 alpha 의 metadata.generation·status.observedGeneration 과 쿼터의 pods 를 읽고, 서비스를 다시 시작해 둘이 맞춰질 때까지 기다리세요. /root/cnpa-op/catchup.jsongeneration, observed_while_stopped, quota_pods_while_stopped, observed_after_start, quota_pods_after_start(쿼터 값은 문자열 그대로)를 적습니다.
  8. /root/cnpa-op/report.jsoncrd_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 중 이 컨트롤러가 따르는 방식)를 적으세요.

참고

타입만 등록하면 아무 일도 일어나지 않는다

/root/cnpa-op/crd.yaml 에 CRD teamspaces.platform.labhub.io 를 작성해 적용하세요. 그룹 platform.labhub.io, scope: Cluster, kind TeamSpace(plural teamspaces), 버전 v1alpha1subresources.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.jsonuid(alpha 의 uid)와 namespace_exists(네임스페이스 team-alpha 가 있는지, 불리언)를 적으세요.

CRD 는 API 서버에 새 명사를 알려 줄 뿐입니다. 그 명사를 보고 무언가를 만드는 쪽은 아직 아무도 없습니다. 네임스페이스가 있는지는 kubectl get ns 의 종료 코드로 알 수 있습니다.

무엇이 generation 을 올리는가

alpha 에 세 가지를 차례로 해 보고 그때마다 metadata.generation 을 읽으세요. ① 라벨 owner=platform 추가, ② 본 리소스 주소로 {"status":{"observedGeneration":7}} merge 패치, ③ spec.pods6 으로 변경. /root/cnpa-op/generation.jsonuid, gen_initial(시작 값), gen_after_label, gen_after_spec(③ 뒤), status_via_main(② 직후 alpha 의 .status 값, 없으면 null)을 적습니다.

generation 은 API 서버가 spec 이 바뀔 때만 올리는 숫자입니다. 메타데이터만 바뀌면 resourceVersion 은 오르지만 generation 은 그대로입니다. status 하위 리소스를 켠 CRD 에서는 본 리소스로 보낸 status 변경을 서버가 버립니다. kubectl 이 그때 무엇이라고 출력하는지 보세요.

몇 번을 돌려도 같은 결과

/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.jsonquota_uid(team-alpha 의 team-quota uid), namespace_owner_uid(team-alpha 의 ownerReference uid), second_pass_rv_changed(한 번 더 돌렸을 때 쿼터의 resourceVersion 이 바뀌었는지, 불리언)를 적으세요.

조정은 "무엇이 바뀌었나" 가 아니라 "지금 원하는 상태와 실제 상태가 같은가" 를 봅니다. 그래서 몇 번을 돌려도 결과가 같아야 합니다. kubectl apply 는 내용이 같으면 쓰지 않습니다. status 는 kubectl patch --subresource=status 로 씁니다. 채점기는 이 스크립트를 두 번 더 실행해 아무것도 쓰지 않는지 봅니다.

한 번이 아니라 계속 돈다

/etc/systemd/system/cnpa-op.service 를 만들어 reconcile.sh 를 3초 간격으로 끝없이 실행하게 하고(Restart=always), enable 하고 시작하세요. 새 TeamSpace 를 만들면 사람이 아무것도 하지 않아도 몇 초 안에 네임스페이스와 쿼터가 생겨야 합니다.

ExecStart 에 while true; do ...; sleep 3; done 루프를 bash -c 로 넣으면 됩니다. 채점기는 임시 TeamSpace 를 하나 만들어 네임스페이스가 스스로 생기는지 보고 지웁니다.

지운 쿼터가 몇 초 뒤 되살아났다

컨트롤러가 도는 상태에서 team-alphateam-quota 를 손으로 지우고, 새 uid 로 되살아날 때까지 걸린 초를 재세요. 이어서 되살아난 쿼터의 pods99 로 패치하고 다시 6 으로 돌아오는지 보세요. /root/cnpa-op/drift.jsondeleted_uid, restored_uid, restore_seconds(정수), edited_to(99), reverted_to(돌아온 값, 정수)를 적습니다.

컨트롤러는 누가 지웠는지 모릅니다. 다음 바퀴에 원하는 상태와 실제 상태를 비교할 뿐입니다. 그래서 복원된 쿼터는 이름은 같아도 새 오브젝트입니다. 2단계에서 spec.pods 를 얼마로 바꿨는지 떠올리세요.

지우기 전에 해야 할 일이 있다

reconcile.sh 를 고쳐 finalizer platform.labhub.io/cleanup 을 다루게 하세요. 삭제 요청이 없는 TeamSpace 에는 이 finalizer 를 붙이고(다른 finalizer 는 보존), deletionTimestamp 가 찍힌 TeamSpace 는 /root/cnpa-op/archive/<이름>.jsonname·uid·pods 를 남긴 뒤 finalizer 를 떼어 줍니다. 그다음 TeamSpace beta(pods: 2, cpu: "500m")를 만들고 네임스페이스가 생긴 것을 본 뒤 지우세요. /root/cnpa-op/cleanup.jsonuid(beta 의 uid), blocked_seen(삭제 직후 beta 가 deletionTimestamp 를 단 채 아직 남아 있는 것을 봤는지, 불리언), namespace_gone(끝난 뒤 team-beta 가 사라졌는지, 불리언)을 적습니다.

finalizer 가 있는 오브젝트에 delete 를 보내면 API 서버는 지우지 않고 deletionTimestamp 만 찍습니다. 목록이 비어야 실제로 사라집니다. 네임스페이스는 ownerReference 를 따라 가비지 컬렉터가 지우므로 컨트롤러가 직접 지울 필요가 없습니다. 클러스터 밖의 기록처럼 소유 관계로 표현할 수 없는 것만 finalizer 로 정리합니다. 삭제 직후를 보려면 kubectl delete --wait=false 로 요청만 보내고 바로 읽으세요. 채점기는 임시 TeamSpace 를 하나 더 만들어 지워 봅니다.

멈춘 동안의 변경을 놓치지 않는다

cnpa-op.service 를 멈추고 alpha 의 spec.pods8 로 바꾸세요. 8초 기다린 뒤 alpha 의 metadata.generation·status.observedGeneration 과 쿼터의 pods 를 읽고, 서비스를 다시 시작해 둘이 맞춰질 때까지 기다리세요. /root/cnpa-op/catchup.jsongeneration, observed_while_stopped, quota_pods_while_stopped, observed_after_start, quota_pods_after_start(쿼터 값은 문자열 그대로)를 적습니다.

이 컨트롤러는 변경 사건을 받아 처리하지 않습니다. 매 바퀴 현재 상태를 읽을 뿐이므로 멈춘 동안 무엇이 몇 번 바뀌었는지 몰라도 됩니다. observedGeneration 이 generation 보다 작다는 것은 "아직 이 spec 을 반영하지 못했다" 는 신호이고, 사용자가 컨트롤러의 진행을 읽는 표준 방법입니다.

컨트롤러가 한 일을 값으로 남긴다

/root/cnpa-op/report.jsoncrd_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 중 이 컨트롤러가 따르는 방식)를 적으세요.

앞 단계에서 남긴 JSON 을 읽어 계산합니다. 이 컨트롤러는 변경 사건 하나하나에 반응하지 않고 매번 현재 상태 전체를 읽어 맞춥니다. 채점기는 같은 파일과 클러스터를 다시 대조합니다.