LabHub

CRD 와 오퍼레이터 · 오퍼레이터 패턴과 리컨사일 루프 · 실습

셸로 리컨사일 루프 만들기

LabHub 에서 이어서 보기

목표

CR 의 명세를 읽어 하위 리소스를 맞추고 결과를 status 에 되쓰는 조정 루프를 셸 스크립트로 직접 구현하고, 두 번째 실행이 아무것도 바꾸지 않는다는 것을 오브젝트 버전으로 증명합니다.

왜 중요한가

이 실습에서 만드는 것은 컨트롤러 바이너리가 아니라 리컨사일 함수의 본질 입니다. 실제 컨트롤러에서도 리컨사일에 전달되는 것은 오브젝트가 아니라 키뿐이고, 무엇이 바뀌었는지는 알려 주지 않습니다. 그래서 리컨사일은 언제나 "지금 원하는 상태는 무엇이고 실제는 무엇인가"에서 다시 시작합니다. 이 설계가 강제하는 것이 멱등성입니다. 이미 맞는 상태에서 다시 쓰면 새 watch 이벤트가 생기고, 그 이벤트가 다시 리컨사일을 불러 자기 자신을 무한히 트리거합니다. 그래서 멱등성의 진짜 판정 기준은 "오류가 안 났다"가 아니라 "하위 오브젝트의 resourceVersion 이 그대로다" 입니다. 또 하나 중요한 구분은 에러와 재큐입니다. 의존 대상이 아직 준비되지 않은 것은 실패가 아니므로 에러로 반환하면 안 됩니다. 에러는 지수 백오프를 쌓고 로그를 오염시키며, 그 객체의 재시도 간격이 길어져 진짜 문제가 생겼을 때 반응이 느려집니다. 마지막으로 파이널라이저는 "삭제 = 즉시 사라짐"이라는 직관을 깨는 장치입니다. 삭제 요청은 삭제 시각을 찍을 뿐이고, 리컨사일이 한 번 더 불린 그 순간이 외부 자원을 정리할 마지막 기회입니다.

단계

시작 전 준비: 실습 파드는 실습마다 새로 뜨므로 앞 실습의 클러스터 상태는 남아 있지 않습니다. kubectl get crd webservices.apps.labhub.io 가 비어 있으면 CRD 를 다시 작성해 적용하고 kubectl create ns crd-lab 도 하세요. 이 실습에는 subresources.statusproperties.status 아래 replicas·observedGeneration·conditions 정의가 반드시 있어야 하며, spec 에는 image(required), replicas(default 1), tier(default dev) 가 필요합니다.

1. 먼저 crd-lab 에 WebService checkout 을 만드세요(spec.image: nginx:1.27, spec.replicas: 3, spec.tier: dev). 그다음 /root/op/reconcile/reconcile.sh 를 만들고 chmod +x 하세요. 이 스크립트는 kubectl get 으로 CR 의 spec(desired)과 하위 ConfigMap checkout-desired(actual)를 읽어 /root/op/reconcile/out/observe.json{"desired": {...}, "actual": {...}} 형태로 저장해야 하며, desired.image 는 CR 의 spec.image 와 정확히 같아야 합니다.
2. 첫 실행의 판단을 /root/op/reconcile/out/decision-1.json 에 저장하세요. actioncreate, reason 은 왜 그렇게 판단했는지 적은 문자열, target 은 무엇에 대한 판단인지(예: configmap/checkout-desired)를 담습니다.
3. 스크립트가 판단대로 실행하게 하세요. crd-lab 에 ConfigMap checkout-desired 를 만들되 data.image 는 CR 의 spec.image 값이어야 하고, metadata.ownerReferences[0].uidcheckout 의 실제 uid, metadata.labelsapp.kubernetes.io/managed-by: webservice-controller 가 있어야 합니다.
4. kubectl get cm checkout-desired -n crd-lab -o jsonpath='{.metadata.resourceVersion}' 값을 /root/op/reconcile/out/rv-before.txt 에 저장하고, 스크립트를 한 번 더 실행한 뒤 같은 값을 /root/op/reconcile/out/rv-after.txt 에 저장하세요. 두 값이 같아야 하고, 두 번째 판단은 /root/op/reconcile/out/decision-2.jsonactionnoop 으로 들어가야 합니다.
5. 스크립트가 status 를 되쓰게 하세요. checkoutstatus.conditionstype: Ready, status: "True" 조건을 넣고, status.observedGenerationmetadata.generation 과 같게, status.replicasspec.replicas 와 같게 만드세요. 스크립트 안에는 --subresource=status 문자열이 실제로 들어 있어야 합니다.
6. /root/op/reconcile/out/requeue.json 을 만드세요. actionrequeue, after_seconds 는 0 보다 큰 정수, is_errorfalse 입니다. 그리고 /root/op/reconcile/out/backoff-note.txt 에 두 가지를 각각 한 문장 이상으로 적으세요 — 재시도 간격이 지수적으로 늘어나는 백오프가 왜 필요한지, 그리고 "아직 준비 안 됨"을 전부 에러로 처리하면 에러 로그가 폭주하고 한 객체가 큐를 굶기게 되는 문제.
7. crd-lab 에 WebService ephemeral 을 만들고 metadata.finalizerswebservice.labhub.io/cleanup 을 넣으세요. 3번과 같은 방식으로 자식 ConfigMap ephemeral-desired 도 만듭니다. 그다음 kubectl delete webservice ephemeral -n crd-lab --wait=false 로 삭제를 요청하고, 그 직후의 오브젝트 전체를 /root/op/reconcile/out/terminating.json 으로 저장하세요(이 파일에는 metadata.deletionTimestampmetadata.finalizers 가 모두 보여야 합니다). 그다음 자식 ConfigMap ephemeral-desired 를 지우고 무엇을 정리했는지 /root/op/reconcile/out/cleanup.txt 에 적은 뒤, 파이널라이저를 제거해 ephemeral 이 실제로 사라지게 하세요.
8. crd-lab 에 WebService billingsearch 를 추가로 만들고(각각 image 와 replicas 를 갖게), 리컨사일러로 checkout·billing·search 세 개를 모두 한 바퀴 돌린 뒤 /root/op/reconcile/out/reconcile-report.json 을 만드세요. items 는 각 원소가 nameaction(create/update/noop/requeue 중 하나)을 가진 배열이고, summary.noop 에 아무것도 하지 않은 건수를, convergedtrue 를 담습니다. 세 CR 모두 status.observedGenerationmetadata.generation 과 같아야 합니다.

참고

단계 8개

  1. 원하는 상태와 실제 상태 읽기
  2. 비교해서 조정 판단 내리기
  3. 판단대로 하위 리소스 만들기
  4. 두 번째 실행이 아무것도 바꾸지 않게 하기
  5. 조정 결과를 status 에 되쓰기
  6. 재큐 판단과 백오프 정리하기
  7. 파이널라이저로 정리 후 삭제하기
  8. 여러 CR 을 한 바퀴 돌고 보고서 만들기