The reconcile ran twice - lease expiry, takeover, and incident diagnosis
한국어 원문으로 표시합니다.
목표
Lease 의 다섯 필드를 직접 다루며 갱신·만료·인수를 만들어 보고, 리더 선출에 필요한 최소 권한을 맞춘 뒤, 리더가 둘일 때 실제로 무슨 일이 생기는지 필드 관리자 충돌로 증명하고 운영 점검표로 묶는다.
왜 중요한가
오퍼레이터를 두 개 이상 띄우는 이유는 가용성이다. 그런데 조정 루프는 클러스터 상태를 바꾸는 코드라 동시에 둘이 돌면 안 된다. 그래서 쿠버네티스는 아주 작은 오브젝트 하나에 '지금 누가 리더인가' 를 적어 두고, 리더가 주기적으로 갱신하며 살아 있음을 알린다. 여기서 중요한 것은 이 오브젝트에 '만료됨' 같은 필드가 없다는 사실이다. 살아 있는지는 renewTime + leaseDurationSeconds 를 지금과 견주는 계산으로 정해지고, 그 계산은 각 인스턴스가 자기 시계로 한다. 시계가 어긋나거나 API 서버 응답이 늦으면 두 인스턴스가 동시에 자기가 리더라고 믿는 구간이 생긴다. 그 구간에 두 조정 루프가 같은 자원을 만들려 하면 무슨 일이 생기는지, 그리고 그 흔적을 어디서 찾는지가 이 실습의 주제다.
단계
- 네임스페이스
op-leader를 만들고/root/op-leader/lease-widget.yaml에coordination.k8s.io/v1Leasewidget-operator를 쓰세요 —holderIdentity는widget-operator-0,leaseDurationSeconds는 15,acquireTime과renewTime은 지금 시각,leaseTransitions는 0 입니다. 적용한 뒤kubectl -n op-leader get lease widget-operator -o yaml출력을/root/op-leader/lease-initial.yaml에 저장하세요. - 리더가 하는 일을 손으로 세 번 해 보세요 —
renewTime만 지금 시각으로 갱신하는 patch 를 1초 간격으로 세 번 돌리고, 그때마다 갱신된renewTime을/root/op-leader/renew-log.txt에 한 줄씩 남깁니다(세 줄).holderIdentity와leaseTransitions는 건드리지 않습니다. - 두 개의 임차를 더 만드세요 —
/root/op-leader/lease-stale.yaml은stale-operator(holderstale-operator-0,leaseDurationSeconds15,renewTime을 10분 전으로),/root/op-leader/lease-fresh.yaml은fresh-operator(holderfresh-operator-0,leaseDurationSeconds86400,renewTime은 지금)입니다./root/op-leader/leases.tsv에<임차이름><탭><기대: EXPIRED 또는 LIVE>두 줄을 적고,/root/op-leader/expiry.sh가 각 임차의renewTime + leaseDurationSeconds를 지금과 견줘 분류한 뒤 맞으면OK …, 틀리면MISMATCH …를 표준출력에만 찍고 한 줄이라도 틀리면 0 이 아닌 코드로 끝나게 하세요. 출력을/root/op-leader/expiry.txt에 저장합니다. widget-operator임차를 다른 인스턴스가 가져갔다고 보고 한 번의 patch 로 네 값을 바꾸세요 —holderIdentity를widget-operator-1로,acquireTime과renewTime을 지금 시각으로,leaseTransitions를 1 로./root/op-leader/takeover.txt에 네 줄을 남기세요 —before-holder=,after-holder=,before-transitions=,after-transitions=.- 초 단위 시각으로 갱신을 시도해 보세요 —
renewTime을2026-01-01T00:00:00Z처럼 소수점 없는 값으로 patch 합니다. 결과를/root/op-leader/microtime-error.txt에 모으세요 — 첫 줄은patch-rc=<종료 코드>이고 그 아래에 서버가 낸 문장을 그대로 붙입니다. /root/op-leader/rbac.yaml에 세 오브젝트를 담아 적용하세요 — ServiceAccountwidget-operator, Roleleader-election(coordination.k8s.io그룹의leases에get·create·update만), 그리고 둘을 잇는 RoleBindingleader-election. 그다음kubectl auth can-i <동사> leases.coordination.k8s.io --as=system:serviceaccount:op-leader:widget-operator -n op-leader를get·create·update·delete네 번 돌려 결과를/root/op-leader/rbac-check.txt에<동사>=<yes 또는 no>네 줄로 저장하세요.- 두 인스턴스가 동시에 자기가 리더라고 믿는 상황을 재현하세요 —
/root/op-leader/child-a.yaml에 ConfigMaporder-1(네임스페이스op-leader,data.phase는A)을 쓰고kubectl apply --server-side --field-manager=widget-operator-0로 적용합니다. 그다음/root/op-leader/child-b.yaml에 같은 이름의 ConfigMap 을data.phase는B로 써서--field-manager=widget-operator-1로 적용해 보세요. 결과를/root/op-leader/split-brain.txt에 모으세요 —apply-b-rc=<종료 코드>, 서버가 낸 문장, 그리고 마지막 줄에final-phase=<지금 값>. /root/op-leader/lease-audit.sh를 만드세요 —op-leader의 모든 임차를<이름> holder=<홀더> transitions=<전환 횟수> duration=<임차 길이>한 줄씩 정렬해 표준출력에만 찍습니다(나이나 시각은 넣지 않습니다). 출력을/root/op-leader/lease-audit.txt에 저장하고,/root/op-leader/runbook.txt에 운영 규칙을 적으세요 —--leader-elect-lease-duration·--leader-elect-renew-deadline·--leader-elect-retry-period세 값의 이름이 모두 나와야 하고, 갱신 기한이 임차 길이보다 짧아야 하는 이유와 전환 횟수가 계속 늘 때 무엇을 의심할지가 들어가야 합니다.
참고
acquireTime과renewTime은 MicroTime 이라 소수점 이하 여섯 자리가 필요합니다.- 갱신은
renewTime만 바꾸고, 인수는 홀더·acquireTime·leaseTransitions를 함께 바꿉니다. - 리더 선출에 필요한 동사는
get·create·update셋뿐입니다. kubectl auth can-i --as=로 다른 주체의 권한을 대신 물어볼 수 있습니다.- 흔한 실수: 갱신할 때
leaseTransitions를 함께 올려 전환 횟수를 의미 없게 만든다. - 흔한 실수: 임차가 만료됐는지 오브젝트의 어떤 필드에서 찾으려 한다 — 계산해야 합니다.
- 참고: https://kubernetes.io/docs/concepts/architecture/leases/
- 참고: https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/lease-v1/
- 참고: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/
임차 한 장이 리더를 정한다
네임스페이스 op-leader 를 만들고 /root/op-leader/lease-widget.yaml 에 coordination.k8s.io/v1 Lease widget-operator 를 쓰세요 — holderIdentity 는 widget-operator-0, leaseDurationSeconds 는 15, acquireTime 과 renewTime 은 지금 시각, leaseTransitions 는 0 입니다. 적용한 뒤 kubectl -n op-leader get lease widget-operator -o yaml 출력을 /root/op-leader/lease-initial.yaml 에 저장하세요.
시각 두 필드는 보통의 Time 이 아니라 MicroTime 이라 소수점 이하 여섯 자리가 필요합니다(예: 2026-09-17T13:37:24.807518Z). GNU date 라면 date -u +%Y-%m-%dT%H:%M:%S.%6NZ 로 바로 만들 수 있습니다. leaseTransitions 는 '리더가 바뀐 횟수' 라서 처음에는 0 입니다.
리더는 쉬지 않고 갱신한다
리더가 하는 일을 손으로 세 번 해 보세요 — renewTime 만 지금 시각으로 갱신하는 patch 를 1초 간격으로 세 번 돌리고, 그때마다 갱신된 renewTime 을 /root/op-leader/renew-log.txt 에 한 줄씩 남깁니다(세 줄). holderIdentity 와 leaseTransitions 는 건드리지 않습니다.
갱신은 '나 아직 살아 있다' 는 신호일 뿐이라 홀더도 전환 횟수도 바뀌지 않습니다. 전환 횟수가 갱신 때마다 올라간다면 그건 리더가 매번 바뀌고 있다는 뜻이고, 운영에서는 경보를 걸 만한 상태입니다. 세 줄이 시간순으로 커지는지 확인하세요.
살아 있는지는 계산으로 정한다
두 개의 임차를 더 만드세요 — /root/op-leader/lease-stale.yaml 은 stale-operator(holder stale-operator-0, leaseDurationSeconds 15, renewTime 을 10분 전으로), /root/op-leader/lease-fresh.yaml 은 fresh-operator(holder fresh-operator-0, leaseDurationSeconds 86400, renewTime 은 지금)입니다. /root/op-leader/leases.tsv 에 <임차이름><탭><기대: EXPIRED 또는 LIVE> 두 줄을 적고, /root/op-leader/expiry.sh 가 각 임차의 renewTime + leaseDurationSeconds 를 지금과 견줘 분류한 뒤 맞으면 OK …, 틀리면 MISMATCH … 를 표준출력에만 찍고 한 줄이라도 틀리면 0 이 아닌 코드로 끝나게 하세요. 출력을 /root/op-leader/expiry.txt 에 저장합니다.
임차 오브젝트에는 '만료됨' 같은 필드가 없습니다. 각 후보가 자기 시계로 계산할 뿐입니다 — 그래서 시계가 어긋나면 같은 오브젝트를 보고도 판정이 갈립니다. 계산은 date -u -d "<시각>" +%s 로 초로 바꿔 더하면 됩니다.
인수는 전환 횟수로 드러난다
widget-operator 임차를 다른 인스턴스가 가져갔다고 보고 한 번의 patch 로 네 값을 바꾸세요 — holderIdentity 를 widget-operator-1 로, acquireTime 과 renewTime 을 지금 시각으로, leaseTransitions 를 1 로. /root/op-leader/takeover.txt 에 네 줄을 남기세요 — before-holder=, after-holder=, before-transitions=, after-transitions=.
인수와 갱신을 가르는 것이 acquireTime 과 leaseTransitions 입니다. 갱신은 renewTime 만 움직이지만 인수는 홀더가 바뀌므로 언제 잡았는지를 새로 적고 전환 횟수를 하나 올립니다. 이 숫자를 지켜보면 리더가 얼마나 자주 바뀌는지가 보입니다.
시각 형식 하나가 갱신을 막는다
초 단위 시각으로 갱신을 시도해 보세요 — renewTime 을 2026-01-01T00:00:00Z 처럼 소수점 없는 값으로 patch 합니다. 결과를 /root/op-leader/microtime-error.txt 에 모으세요 — 첫 줄은 patch-rc=<종료 코드> 이고 그 아래에 서버가 낸 문장을 그대로 붙입니다.
이 필드는 Time 이 아니라 MicroTime 입니다. 형식이 다르면 값이 아니라 파싱에서 막히는데, 오류 문장에 기대하는 형식이 그대로 적혀 있습니다. 컨트롤러를 직접 짜다가 표준 라이브러리의 기본 형식을 그대로 쓰면 이 자리에서 막혀 갱신이 통째로 실패합니다.
리더 선출에 필요한 최소 권한
/root/op-leader/rbac.yaml 에 세 오브젝트를 담아 적용하세요 — ServiceAccount widget-operator, Role leader-election(coordination.k8s.io 그룹의 leases 에 get·create·update 만), 그리고 둘을 잇는 RoleBinding leader-election. 그다음 kubectl auth can-i <동사> leases.coordination.k8s.io --as=system:serviceaccount:op-leader:widget-operator -n op-leader 를 get·create·update·delete 네 번 돌려 결과를 /root/op-leader/rbac-check.txt 에 <동사>=<yes 또는 no> 네 줄로 저장하세요.
리더 선출에 필요한 동사는 셋뿐입니다 — 읽고, 없으면 만들고, 주기적으로 갱신합니다. 지울 일은 없습니다. watch 도 필요 없는데, 후보는 감시가 아니라 주기적인 조회로 만료를 확인하기 때문입니다. 권한을 넓게 주면 실수로 남의 임차를 지우는 경로가 열립니다.
리더가 둘이면 같은 필드를 놓고 싸운다
두 인스턴스가 동시에 자기가 리더라고 믿는 상황을 재현하세요 — /root/op-leader/child-a.yaml 에 ConfigMap order-1(네임스페이스 op-leader, data.phase 는 A)을 쓰고 kubectl apply --server-side --field-manager=widget-operator-0 로 적용합니다. 그다음 /root/op-leader/child-b.yaml 에 같은 이름의 ConfigMap 을 data.phase 는 B 로 써서 --field-manager=widget-operator-1 로 적용해 보세요. 결과를 /root/op-leader/split-brain.txt 에 모으세요 — apply-b-rc=<종료 코드>, 서버가 낸 문장, 그리고 마지막 줄에 final-phase=<지금 값>.
서버사이드 적용은 필드마다 '누가 이 값을 관리하는가' 를 기록해 둡니다. 다른 관리자가 같은 필드를 다른 값으로 쓰려 하면 API 서버가 조용히 덮어쓰지 않고 충돌을 알려 줍니다. 리더 선출이 무너졌을 때 두 조정 루프가 서로의 결과를 지우는 일을 이 장치가 눈에 보이게 만들어 줍니다.
점검표와 운영 규칙으로 묶는다
/root/op-leader/lease-audit.sh 를 만드세요 — op-leader 의 모든 임차를 <이름> holder=<홀더> transitions=<전환 횟수> duration=<임차 길이> 한 줄씩 정렬해 표준출력에만 찍습니다(나이나 시각은 넣지 않습니다). 출력을 /root/op-leader/lease-audit.txt 에 저장하고, /root/op-leader/runbook.txt 에 운영 규칙을 적으세요 — --leader-elect-lease-duration·--leader-elect-renew-deadline·--leader-elect-retry-period 세 값의 이름이 모두 나와야 하고, 갱신 기한이 임차 길이보다 짧아야 하는 이유와 전환 횟수가 계속 늘 때 무엇을 의심할지가 들어가야 합니다.
점검표는 '무엇을 보면 이상한지' 를 줄여 놓은 것입니다. 홀더가 계속 같은데 전환 횟수만 늘어난다면 인수가 반복되고 있다는 뜻이고, 원인은 대개 시계 어긋남이나 API 서버 지연입니다. 세 값의 관계는 공식 문서의 기본값(15초·10초·2초)을 근거로 설명하세요.