LabHub

CGOA — GitOps 인증 어소시에이트 · 조정 루프와 드리프트 · 이론

refresh 는 계산이고 sync 는 적용이다

LabHub 에서 이어서 보기

한 줄 요약

refresh 는 다시 렌더하고 diff 를 계산할 뿐 아무것도 적용하지 않습니다. sync 만 클러스터를 건드립니다. 이 한 문장을 놓치면 Argo CD 의 절반이 이해되지 않습니다.

왜 이게 필요했나

UI 에서 Refresh 버튼을 눌렀는데 앱이 여전히 OutOfSync 로 남아 있으면 대부분 "고장 났나?" 라고 생각합니다. 정상입니다. refresh 가 하는 일은 이겁니다.

1. repo-server 에 최신 매니페스트를 다시 만들어 달라고 요청한다(캐시 무효화 여부에 따라 hard refresh 면 클론부터 다시).
2. 대상 클러스터에서 live 상태를 다시 읽는다.
3. 둘을 비교해 Sync Status(Synced / OutOfSync)와 Health Status 를 갱신한다.

여기서 끝입니다. 적용은 sync 의 일이고, sync 는 사람이 누르거나 syncPolicy.automated 가 켜져 있을 때만 일어납니다. 이 분리 덕분에 "지금 Git 과 클러스터가 얼마나 다른가"를 아무것도 바꾸지 않고 계속 관찰할 수 있습니다. 자동 동기화를 끈 채 운영하는 조직이 많은 이유가 바로 이것입니다 — 감시는 항상, 적용은 사람이.

어떻게 동작하나

3-way diff — 왜 세 상태인가

Argo CD 의 diff 는 두 상태가 아니라 세 상태를 봅니다.

| 상태 | 어디서 오나 | 무엇을 답하나 |
| --- | --- | --- |
| Desired | Git 을 렌더한 결과 | 우리가 원하는 것 |
| Live | 클러스터의 현재 오브젝트 | 지금 실제인 것 |
| Last-applied | 오브젝트의 last-applied-configuration 주석 | 우리가 이전에 관리하겠다고 선언한 것 |

두 상태만 비교하면 치명적인 오판이 생깁니다. 예를 들어 HPA 가 replicas 를 5 로 올려 놨고 Git 에는 replicas 필드 자체가 없다고 합시다. Desired 와 Live 만 비교하면 "Live 에만 있는 필드니까 지워야겠다"가 됩니다. 세 번째 상태를 보면 답이 달라집니다 — last-applied 에도 replicas 가 없으므로 애초에 우리가 관리한 적 없는 필드 이고, 남의 컨트롤러가 소유한 값이니 건드리면 안 됩니다.

정규화(normalization)도 여기 붙습니다. metadata.resourceVersion, uid, generation, creationTimestamp, managedFields, 그리고 대부분의 status 는 diff 에서 제거합니다. 쿠버네티스가 자동으로 채우는 기본값들 — Service 의 clusterIP, 이미지 태그가 latest 일 때의 imagePullPolicy: Always — 도 무시합니다. 이 정규화가 없으면 모든 앱이 영원히 OutOfSync 로 보입니다.

selfHeal 이 진짜로 뜻하는 것

syncPolicy.automated.selfHeal: true 는 "드리프트를 자동으로 되돌린다"입니다. 뒤집으면 이런 뜻입니다.

> 장애 대응 중 손으로 고친 것이 되돌아온다.

새벽에 파드가 죽어서 급히 kubectl scale 로 replicas 를 늘렸다면, 다음 조정 루프에서 Git 값으로 되돌아갑니다. 이게 버그가 아니라 설계입니다. selfHeal 을 켠 조직은 "긴급 수정도 커밋으로 한다"는 규율을 함께 받아들인 것입니다. 급할 때를 위한 탈출구가 필요하면 자동 동기화를 잠시 비활성화하거나, 해당 필드를 ignoreDifferences 에 넣어 관리 대상에서 빼는 게 정석입니다.

웨이브와 훅 — 순서를 만드는 두 장치

선언형은 순서를 표현하지 못합니다. 그래서 두 장치가 얹힙니다.

Sync waveargocd.argoproj.io/sync-wave 주석의 숫자로 리소스를 그룹 짓고 낮은 번호부터 적용합니다. 핵심은 현재 웨이브의 모든 리소스가 Healthy 가 될 때까지 다음 웨이브로 넘어가지 않는다 는 점입니다. 그래서 웨이브를 잘못 나누면 배포가 그 자리에서 멈춥니다 — 예를 들어 절대 Healthy 가 되지 않는 리소스(들어오는 트래픽이 없어 준비되지 않는 Job 등)를 앞 웨이브에 두면 뒤가 영원히 안 옵니다. 같은 웨이브 안에서는 리소스 종류별 기본 순서(Namespace → NetworkPolicy → ResourceQuota → LimitRange → ServiceAccount → Secret/ConfigMap → RBAC → CRD → PV/PVC → Service → 워크로드 → Ingress)가 적용됩니다.

Hook 은 단계(phase) 자체를 나눕니다. PreSync → Sync → PostSync 이고, 실패하면 SyncFail 이 돕니다. 훅은 보통 Job 이고, 주석 argocd.argoproj.io/hook: PreSync 로 지정합니다. 삭제 정책 argocd.argoproj.io/hook-delete-policyHookSucceeded, HookFailed, BeforeHookCreation 세 값이 있고 기본값은 BeforeHookCreation 입니다. 즉 훅 리소스는 성공해도 남아 있다가 다음 sync 에서 새로 만들기 직전에 지워집니다. 실패한 마이그레이션 Job 의 로그를 사후에 볼 수 있는 이유가 이 기본값 덕분입니다.

재시도와 백오프

sync 가 실패하면 지수 백오프로 재시도합니다. duration: 5s, factor: 2, maxDuration: 3m 이면 대기 시간은 5s → 10s → 20s → 40s → 80s 로 늘고 3 분에서 상한에 걸립니다. 재시도가 트리거되는 상황은 리소스 적용 실패, Health Check 타임아웃, 훅 Job 실패, 일시적 네트워크 오류입니다.

prune 의 위험

prune: true 는 Git 에서 사라진 리소스를 클러스터에서도 지웁니다. 판별 기준은 "클러스터에 있는데 Git 에 없는 것" 전부가 아니라, Argo CD 가 자기 것이라고 표시해 둔 것 중 Git 에 없는 것입니다. 표시 방식이 리소스 추적(resource tracking)이고 기본은 주석 방식입니다.

argocd.argoproj.io/tracking-id: APP_NAME:GROUP/KIND:NAMESPACE/NAME예) checkout-prod:apps/Deployment:cgoa-prod/prod-checkout

레거시 방식은 라벨 app.kubernetes.io/instance 를 씁니다. 이 라벨은 Helm 등 다른 도구도 쓰기 때문에 소유권 판정이 충돌할 수 있어 주석 방식이 권장됩니다.

prune 이 무서운 이유는 경로를 잘못 바꾼 커밋 하나가 곧 대량 삭제 이기 때문입니다. source.path 를 오타로 빈 디렉터리로 바꾸면 렌더 결과가 0 개가 되고, 그 앱이 관리하던 모든 리소스가 prune 대상이 됩니다. 방어 장치는 allowEmpty: false(빈 렌더 결과를 거부), PruneLast=true(다른 리소스 동기화가 끝난 뒤 마지막에 prune), 그리고 개별 리소스의 argocd.argoproj.io/sync-options: Prune=false 입니다.

현장에서 만나는 모습

저자의 홈랩에서 이 감각이 필요했던 순간은 GPU 노드 확장 때였습니다. GPU Feature Discovery 가 새 노드에 라벨을 자동으로 답니다 — RTX 3090 24GB, 5090 32GB, 4070 Laptop 8GB 두 대. 그런데 파드가 nvidia.com/gpu: 1 만 요청하면 32GB 가 필요한 학습이 8GB 노트북 GPU 에 얹힐 수 있습니다. 쿠버네티스에게는 둘 다 "GPU 한 개"이기 때문입니다.

그래서 의미 기반 라벨을 손으로 얹었습니다(gpu.homelab/tier=xlarge, vram=32g 같은 식으로). 여기서 GitOps 관점의 교훈이 나옵니다 — 컨트롤러가 붙이는 라벨과 사람이 선언하는 라벨이 같은 오브젝트에 공존 합니다. 이 노드 오브젝트를 GitOps 로 관리한다면 GFD 가 붙인 라벨은 반드시 ignoreDifferences 로 빼야 합니다. 그러지 않으면 조정 루프와 컨트롤러가 서로의 필드를 지우며 싸웁니다. 3-way diff 와 필드 소유권이 왜 필요한지가 이 장면에 다 들어 있습니다.

다음 실습에서 할 것

실제 클러스터에 매니페스트를 올린 뒤 손으로 replicas 를 바꿔 드리프트를 만들고, kubectl diff 로 그 차이를 파일에 남긴 다음 되돌립니다(사람이 하는 self-heal). 이어서 sync wave 주석이 붙은 매니페스트 세 개와 PreSync 훅 Job 을 파일로 작성하고, 마지막에는 클러스터를 뒤져 prune 대상이 무엇인지 직접 판별해 봅니다.