TT Lab
시작하기
배우기 러닝패스 코스

CGOA — GitOps 인증 어소시에이트

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

TT Lab 에서 이어서 보기

한 줄 요약

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 과 클러스터가 얼마나 다른가"를 아무것도 바꾸지 않고 계속 관찰할 수 있습니다. 자동 동기화를 끈 채 운영하는 조직이 많은 이유가 바로 이것입니다 — 감시는 항상, 적용은 사람이.

Refresh 버튼 한 번이 하는 세 가지

UI 의 Refresh 는 적용 버튼이 아니라 비교를 새로 계산하는 버튼이다. 본문이 적은 세 단계를 순서대로 옮겼다.

  1. repo-server 가 매니페스트를 다시 만든다최신 매니페스트를 다시 렌더해 달라고 요청한다. 캐시를 무효화하는 hard refresh 라면 클론부터 다시 한다.
  2. 대상 클러스터의 live 상태를 다시 읽는다Git 쪽만 새로 계산하는 것이 아니다. 클러스터에 지금 있는 오브젝트도 다시 읽는다.
  3. 둘을 비교해 상태 표시만 고친다Sync Status(Synced 또는 OutOfSync)와 Health Status 를 갱신한다. 여기서 끝이고 클러스터는 바뀌지 않는다.

여기서 구분할 것 적용은 sync 의 일이다. sync 는 사람이 누르거나 syncPolicy.automated 가 켜져 있을 때만 일어난다. 그래서 Refresh 후에도 OutOfSync 로 남아 있는 것은 고장이 아니라 정상이다.

잠깐, 예측해 보세요 자동 동기화를 끄고 운영하는 팀이 Refresh 로 주기적으로 상태를 갱신하기만 한다. 이 습관으로 얻는 것과 얻지 못하는 것은 각각 무엇일까?

설명 확인 · 채점 없는 자가 점검

얻는 것은 아무것도 바꾸지 않고 Git 과 클러스터가 얼마나 다른지 계속 관찰하는 일이다. 감시는 항상, 적용은 사람이 한다는 운영 방식이다. 얻지 못하는 것은 차이의 해소이다. 클러스터를 맞추는 일은 sync 만 한다.

근거 문서

어떻게 동작하나

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 wave 는 argocd.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-policy 는 HookSucceeded, 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 와 필드 소유권이 왜 필요한지가 이 장면에 다 들어 있습니다.

경로 오타 하나가 대량 삭제가 되는 길

본문이 prune 의 위험으로 든 사례다. prune: true 가 켜진 앱에서 source.path 를 잘못 바꾸면 어떤 순서로 일이 벌어지는지 옮겼다.

  1. source.path 를 오타로 빈 디렉터리로 바꾼 커밋경로를 잘못 바꾼 커밋 하나가 출발점이다.
  2. 렌더 결과가 0 개가 된다그 경로에서 만들어 낼 리소스가 하나도 없으므로 Git 에 원하는 상태가 비어 있는 것으로 계산된다.
  3. 그 앱이 관리하던 리소스가 prune 대상이 된다대상은 클러스터에 있는데 Git 에 없는 것 전부가 아니라, Argo CD 가 자기 것이라고 표시해 둔 것 중 Git 에 없는 것이다. 기본 표시 방식은 tracking-id 주석이다.

여기서 구분할 것 방어 장치는 셋이고 막는 지점이 다르다. allowEmpty: false 는 빈 렌더 결과 자체를 거부한다. PruneLast=true 는 다른 리소스 동기화가 끝난 뒤 마지막에 prune 하도록 순서를 바꾼다. 리소스에 붙이는 sync-options Prune=false 는 그 리소스 하나를 prune 에서 뺀다.

잠깐, 예측해 보세요 prune: true 인 앱의 네임스페이스에 누군가 kubectl 로 직접 만든 ConfigMap 이 있다. 이 앱의 추적 표시는 없고 Git 에도 없다. 경로 오타 커밋이 반영되면 이 ConfigMap 도 지워질까?

설명 확인 · 채점 없는 자가 점검

추적 표시가 정말 없다면 prune 대상이 아니다. 판별 기준은 클러스터에 있는데 Git 에 없는 것 전부가 아니라 Argo CD 가 자기 것이라고 표시해 둔 것 중 Git 에 없는 것이기 때문이다.

근거 문서

다음 실습에서 할 것

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