Building a Reconcile Loop in Shell
한국어 원문으로 표시합니다.
목표
CR 의 명세를 읽어 하위 리소스를 맞추고 결과를 status 에 되쓰는 조정 루프를 셸 스크립트로 직접 구현하고, 두 번째 실행이 아무것도 바꾸지 않는다는 것을 오브젝트 버전으로 증명합니다.
왜 중요한가
이 실습에서 만드는 것은 컨트롤러 바이너리가 아니라 리컨사일 함수의 본질 입니다. 실제 컨트롤러에서도 리컨사일에 전달되는 것은 오브젝트가 아니라 키뿐이고, 무엇이 바뀌었는지는 알려 주지 않습니다. 그래서 리컨사일은 언제나 "지금 원하는 상태는 무엇이고 실제는 무엇인가"에서 다시 시작합니다. 이 설계가 강제하는 것이 멱등성입니다. 이미 맞는 상태에서 다시 쓰면 새 watch 이벤트가 생기고, 그 이벤트가 다시 리컨사일을 불러 자기 자신을 무한히 트리거합니다. 그래서 멱등성의 진짜 판정 기준은 "오류가 안 났다"가 아니라 "하위 오브젝트의 resourceVersion 이 그대로다" 입니다. 또 하나 중요한 구분은 에러와 재큐입니다. 의존 대상이 아직 준비되지 않은 것은 실패가 아니므로 에러로 반환하면 안 됩니다. 에러는 지수 백오프를 쌓고 로그를 오염시키며, 그 객체의 재시도 간격이 길어져 진짜 문제가 생겼을 때 반응이 느려집니다. 마지막으로 파이널라이저는 "삭제 = 즉시 사라짐"이라는 직관을 깨는 장치입니다. 삭제 요청은 삭제 시각을 찍을 뿐이고, 리컨사일이 한 번 더 불린 그 순간이 외부 자원을 정리할 마지막 기회입니다.
단계
시작 전 준비: 실습 파드는 실습마다 새로 뜨므로 앞 실습의 클러스터 상태는 남아 있지 않습니다. kubectl get crd webservices.apps.labhub.io 가 비어 있으면 CRD 를 다시 작성해 적용하고 kubectl create ns crd-lab 도 하세요. 이 실습에는 subresources.status 와 properties.status 아래 replicas·observedGeneration·conditions 정의가 반드시 있어야 하며, spec 에는 image(required), replicas(default 1), tier(default dev) 가 필요합니다.
- 먼저
crd-lab에 WebServicecheckout을 만드세요(spec.image: nginx:1.27,spec.replicas: 3,spec.tier: dev). 그다음/root/op/reconcile/reconcile.sh를 만들고chmod +x하세요. 이 스크립트는kubectl get으로 CR 의 spec(desired)과 하위 ConfigMapcheckout-desired(actual)를 읽어/root/op/reconcile/out/observe.json에{"desired": {...}, "actual": {...}}형태로 저장해야 하며,desired.image는 CR 의spec.image와 정확히 같아야 합니다. - 첫 실행의 판단을
/root/op/reconcile/out/decision-1.json에 저장하세요.action은create,reason은 왜 그렇게 판단했는지 적은 문자열,target은 무엇에 대한 판단인지(예:configmap/checkout-desired)를 담습니다. - 스크립트가 판단대로 실행하게 하세요.
crd-lab에 ConfigMapcheckout-desired를 만들되data.image는 CR 의spec.image값이어야 하고,metadata.ownerReferences[0].uid는checkout의 실제 uid,metadata.labels에app.kubernetes.io/managed-by: webservice-controller가 있어야 합니다. 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.json에action이noop으로 들어가야 합니다.- 스크립트가 status 를 되쓰게 하세요.
checkout의status.conditions에type: Ready,status: "True"조건을 넣고,status.observedGeneration을metadata.generation과 같게,status.replicas를spec.replicas와 같게 만드세요. 스크립트 안에는--subresource=status문자열이 실제로 들어 있어야 합니다. /root/op/reconcile/out/requeue.json을 만드세요.action은requeue,after_seconds는 0 보다 큰 정수,is_error는false입니다. 그리고/root/op/reconcile/out/backoff-note.txt에 두 가지를 각각 한 문장 이상으로 적으세요 — 재시도 간격이 지수적으로 늘어나는 백오프가 왜 필요한지, 그리고 "아직 준비 안 됨"을 전부 에러로 처리하면 에러 로그가 폭주하고 한 객체가 큐를 굶기게 되는 문제.crd-lab에 WebServiceephemeral을 만들고metadata.finalizers에webservice.labhub.io/cleanup을 넣으세요. 3번과 같은 방식으로 자식 ConfigMapephemeral-desired도 만듭니다. 그다음kubectl delete webservice ephemeral -n crd-lab --wait=false로 삭제를 요청하고, 그 직후의 오브젝트 전체를/root/op/reconcile/out/terminating.json으로 저장하세요(이 파일에는metadata.deletionTimestamp와metadata.finalizers가 모두 보여야 합니다). 그다음 자식 ConfigMapephemeral-desired를 지우고 무엇을 정리했는지/root/op/reconcile/out/cleanup.txt에 적은 뒤, 파이널라이저를 제거해ephemeral이 실제로 사라지게 하세요.crd-lab에 WebServicebilling과search를 추가로 만들고(각각 image 와 replicas 를 갖게), 리컨사일러로checkout·billing·search세 개를 모두 한 바퀴 돌린 뒤/root/op/reconcile/out/reconcile-report.json을 만드세요.items는 각 원소가name과action(create/update/noop/requeue중 하나)을 가진 배열이고,summary.noop에 아무것도 하지 않은 건수를,converged에true를 담습니다. 세 CR 모두status.observedGeneration이metadata.generation과 같아야 합니다.
참고
- 실습 파드는 실습마다 새로 뜨므로 앞 실습의 클러스터 상태는 남아 있지 않습니다. 그래도 선언을 파일로 남겨 두면 어느 파드에서든 같은 상태를 다시 세울 수 있습니다 — 이것이 선언형의 실질적 이점입니다.
- 파이널라이저 제거:
kubectl patch webservice ephemeral -n crd-lab --type=merge -p '{"metadata":{"finalizers":null}}' - 값이 같은 내용으로
kubectl apply하면 서버가 변경 없음으로 처리해resourceVersion이 오르지 않습니다. 다만 매번 달라지는 값(타임스탬프 등)을 annotation 에 넣으면 그 성질이 깨집니다. - status 는 patch 로 씁니다:
kubectl patch webservice checkout -n crd-lab --subresource=status --type=merge -p '{"status":{...}}' - 흔한 실수 1: 4번에서 스크립트가 매번
kubectl replace나kubectl create --dry-run뒤 강제 적용을 하는 것. 내용이 같아도 버전이 올라가 멱등성 판정에 실패합니다. - 흔한 실수 2: 6번에서
is_error를true로 적는 것. 의존 대상이 아직 없는 것은 오류가 아니라 대기 상황입니다. - 흔한 실수 3: 7번에서 삭제 명령이 끝날 때까지 기다리는 것. 파이널라이저가 붙어 있으면 명령이 반환되지 않으므로 기다리지 않는 옵션을 써야 삭제 중 상태를 관찰할 수 있습니다.
원하는 상태와 실제 상태 읽기
먼저 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 와 정확히 같아야 합니다.
리컨사일의 첫 단계는 판단이 아니라 읽기입니다. CR 의 spec 이 desired, 하위 오브젝트가 actual 입니다. 아직 하위 오브젝트가 없다면 actual 은 비어 있는 것이 정상이며, 스크립트에는 실행 권한이 필요합니다.
비교해서 조정 판단 내리기
첫 실행의 판단을 /root/op/reconcile/out/decision-1.json 에 저장하세요. action 은 create, reason 은 왜 그렇게 판단했는지 적은 문자열, target 은 무엇에 대한 판단인지(예: configmap/checkout-desired)를 담습니다.
판단은 action 하나로 끝나면 안 됩니다. 왜 그렇게 판단했는지와 무엇에 대한 판단인지를 함께 남겨야 나중에 로그만 보고 추적할 수 있습니다.
판단대로 하위 리소스 만들기
스크립트가 판단대로 실행하게 하세요. crd-lab 에 ConfigMap checkout-desired 를 만들되 data.image 는 CR 의 spec.image 값이어야 하고, metadata.ownerReferences[0].uid 는 checkout 의 실제 uid, metadata.labels 에 app.kubernetes.io/managed-by: webservice-controller 가 있어야 합니다.
값은 손으로 베끼지 말고 CR 에서 읽어 넣으세요. 부모와의 연결과, 내가 만든 것임을 나타내는 표식 라벨이 둘 다 필요합니다.
두 번째 실행이 아무것도 바꾸지 않게 하기
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.json 에 action 이 noop 으로 들어가야 합니다.
멱등성의 증거는 로그가 아니라 오브젝트의 버전입니다. 두 번째 실행 전후로 하위 오브젝트의 resourceVersion 을 각각 기록해 비교하세요. 내용이 같으면 서버가 다시 쓰지 않습니다.
조정 결과를 status 에 되쓰기
스크립트가 status 를 되쓰게 하세요. checkout 의 status.conditions 에 type: Ready, status: "True" 조건을 넣고, status.observedGeneration 을 metadata.generation 과 같게, status.replicas 를 spec.replicas 와 같게 만드세요. 스크립트 안에는 --subresource=status 문자열이 실제로 들어 있어야 합니다.
status 는 spec 과 다른 경로로 씁니다. 그 경로를 지정하는 문자열이 스크립트 안에 실제로 있어야 합니다. 처리한 세대 번호와 관측한 복제본 수도 함께 맞추세요.
재큐 판단과 백오프 정리하기
/root/op/reconcile/out/requeue.json 을 만드세요. action 은 requeue, after_seconds 는 0 보다 큰 정수, is_error 는 false 입니다. 그리고 /root/op/reconcile/out/backoff-note.txt 에 두 가지를 각각 한 문장 이상으로 적으세요 — 재시도 간격이 지수적으로 늘어나는 백오프가 왜 필요한지, 그리고 "아직 준비 안 됨"을 전부 에러로 처리하면 에러 로그가 폭주하고 한 객체가 큐를 굶기게 되는 문제.
의존 대상이 아직 없는 것은 실패가 아닙니다. 언제 다시 볼지를 초 단위로 적고, 이것이 에러가 아님을 명시하세요. 메모에는 재시도 간격이 늘어나는 이유와, 전부 에러로 처리했을 때 생기는 문제를 각각 적어야 합니다.
파이널라이저로 정리 후 삭제하기
crd-lab 에 WebService ephemeral 을 만들고 metadata.finalizers 에 webservice.labhub.io/cleanup 을 넣으세요. 3번과 같은 방식으로 자식 ConfigMap ephemeral-desired 도 만듭니다. 그다음 kubectl delete webservice ephemeral -n crd-lab --wait=false 로 삭제를 요청하고, 그 직후의 오브젝트 전체를 /root/op/reconcile/out/terminating.json 으로 저장하세요(이 파일에는 metadata.deletionTimestamp 와 metadata.finalizers 가 모두 보여야 합니다). 그다음 자식 ConfigMap ephemeral-desired 를 지우고 무엇을 정리했는지 /root/op/reconcile/out/cleanup.txt 에 적은 뒤, 파이널라이저를 제거해 ephemeral 이 실제로 사라지게 하세요.
삭제 요청은 즉시 삭제가 아닙니다. 삭제 시각이 찍힌 직후의 모습을 먼저 저장하고, 정리 작업을 한 뒤 파이널라이저를 떼야 오브젝트가 사라집니다. 기다리지 않는 삭제 옵션이 있습니다.
여러 CR 을 한 바퀴 돌고 보고서 만들기
crd-lab 에 WebService billing 과 search 를 추가로 만들고(각각 image 와 replicas 를 갖게), 리컨사일러로 checkout·billing·search 세 개를 모두 한 바퀴 돌린 뒤 /root/op/reconcile/out/reconcile-report.json 을 만드세요. items 는 각 원소가 name 과 action(create/update/noop/requeue 중 하나)을 가진 배열이고, summary.noop 에 아무것도 하지 않은 건수를, converged 에 true 를 담습니다. 세 CR 모두 status.observedGeneration 이 metadata.generation 과 같아야 합니다.
리컨사일러는 특정 CR 전용이 아닙니다. 여러 대상을 돌면서 각각의 판단을 모으고, 전부 원하는 상태에 도달했는지 한 줄로 요약하세요. 모든 대상의 세대 번호가 맞아야 수렴한 것입니다.