GitOps 와 ArgoCD · 드리프트와 self-heal · 이론
리컨실 루프 — 되돌리는 힘과 지우는 힘
한 줄 요약
selfHeal 은 클러스터를 저장소 쪽으로 되돌리는 힘이고 prune 은 저장소에 없는 것을 지우는 힘인데, 둘은 방향이 다르고 위험도 다르다.
왜 이게 필요했나
gitops-manifest 실습에서 여러분은 손으로 드리프트를 만들었다. kubectl scale 로 replicas 를 5로 올렸고, kubectl diff 가 종료 코드 1로 그것을 알려 줬고, 다시 kubectl apply 를 쳐서 3으로 되돌렸다. 여기서 질문 하나만 하면 GitOps 의 마지막 조각이 나온다 — 그 apply 를 누가 치는가?
사람이 친다면 그것은 GitOps 가 아니라 그냥 잘 정리된 배포 스크립트다. 사람은 잊고, 휴가를 가고, 다른 장애에 붙잡힌다. 네 번째 원칙 "자가 치유"가 요구하는 것은 그 apply 를 루프가 치게 만드는 것이다. 사람의 결심 대신 주기가 그 일을 한다.
어떻게 동작하나
ArgoCD 의 Application Controller 는 informer 로 리소스 변화를 지켜보면서, 각 Application 마다 이 순환을 돈다.
반복: 1. repo-server 에 원하는 상태(렌더된 매니페스트)를 요청 2. 대상 클러스터에서 실제 상태를 조회 3. 정규화한 뒤 둘을 비교 (3-way diff) 4. Sync 상태 갱신 → Synced / OutOfSync 5. Health 상태 갱신 → Healthy / Progressing / Degraded / Missing 6. 자동 동기화가 켜져 있으면 동기화 실행 7. 다음 주기까지 대기 (기본 180초)여기서 반드시 구분해야 할 두 축이 나온다. Sync 상태는 "저장소와 같은가" 이고 Health 상태는 "잘 돌고 있는가" 다. Synced 인데 Degraded 일 수 있다 — 저장소가 시킨 대로 배포했지만 이미지가 안 받아져 파드가 CrashLoopBackOff 인 경우다. 반대로 OutOfSync 인데 Healthy 일 수도 있다 — 누가 손으로 늘려 놓은 파드가 멀쩡히 도는 경우다. 두 축을 섞어 보면 장애 원인을 잘못 짚는다.
3단계의 비교에는 정규화가 끼어 있다. 쿠버네티스가 자동으로 채우는 값들(resourceVersion, uid, generation, creationTimestamp, managedFields, 대부분의 status)을 양쪽에서 걷어낸 뒤 비교한다. 이 과정이 없으면 아무것도 안 바뀌었는데도 매번 차이가 있다고 보고된다.
selfHeal 이 하는 일
selfHeal: true 는 4단계에서 OutOfSync 가 나왔을 때 6단계로 넘어가게 하는 스위치다. 그래서 kubectl scale 이나 kubectl edit 로 한 수정은 다음 리컨실에서 사라진다. 즉시 사라지지 않는다는 점이 중요하다 — 기본 주기가 있으므로 "고쳤는데 한동안 잘 되다가 갑자기 원래대로 돌아갔다"처럼 느껴진다. 이 시차 때문에 원인을 엉뚱한 곳에서 찾는 일이 흔하다.
selfHeal 을 끄면 ArgoCD 는 차이를 화면에 띄우기만 하는 대시보드가 된다. 실무에서 프로덕션에 selfHeal 을 켜는 판단은 "긴급 수동 개입을 허용할 것인가" 라는 조직의 질문이지 기술적 취향이 아니다. 켜면 응급 처치가 조용히 되돌려지고, 끄면 드리프트가 쌓인다. 대부분의 팀은 켜고, 대신 응급 처치는 곧바로 커밋으로 승격시키는 규율을 함께 만든다.
prune 이 하는 일 — 그리고 왜 무서운가
prune: true 는 방향이 반대다. 판단 기준은 이렇다.
1. 저장소를 렌더해 "있어야 할 오브젝트" 목록을 만든다2. 클러스터에서 이 앱이 소유한 오브젝트를 찾는다 (argocd.argoproj.io/tracking-id 어노테이션 = 앱:그룹/종류:네임스페이스/이름)3. 2에 있는데 1에 없는 것 = 삭제 대상무서운 지점이 3번에 있다. source.path 를 빈 디렉터리로 잘못 바꾼 커밋 하나면 1번 목록이 0개가 되고, 그 앱이 관리하던 모든 오브젝트가 삭제 대상이 된다. 리뷰어가 한 글자 오타를 놓치는 것으로 충분하다. 그래서 방어 장치들이 있다.
| 장치 | 하는 일 |
| --- | --- |
| allowEmpty: false | 렌더 결과가 비면 동기화를 거부 |
| PruneLast=true | 다른 리소스를 다 맞춘 뒤 마지막에 삭제 |
| Prune=false (리소스 어노테이션) | 그 오브젝트만 삭제 대상에서 제외 |
| orphanedResources.warn | 관리 대상 밖의 오브젝트를 경고로만 알림 |
selfHeal 은 최악의 경우 "내 수동 변경이 사라진다"이고 prune 은 최악의 경우 "데이터가 사라진다"이다. 위험의 급이 다르므로 PVC 같은 상태 보관 리소스에는 Prune=false 를 붙여 두는 편이 안전하다.
되돌리면 안 되는 필드
리컨실 루프는 성실해서, 다른 컨트롤러가 정당하게 소유한 필드까지 되돌린다. HPA 가 spec.replicas 를 8로 올리면 ArgoCD 가 3으로 내리고, HPA 가 다시 8로 올린다. 이 무한 루프의 해법은 스위치를 끄는 것이 아니라 소유권을 명시하는 것이다.
spec: ignoreDifferences: - group: apps kind: Deployment jsonPointers: - /spec/replicasServerSideApply=true 도 같은 문제를 다른 층위에서 다룬다. 필드 소유권을 API 서버가 추적하게 해서, 내가 선언하지 않은 필드를 남이 관리하는 상황을 정상으로 취급한다.
현장에서 만나는 모습
첫째, 이 실습 환경에는 ArgoCD 컨트롤러가 없다. 파드는 특권 없이 뜨고 그 안의 kwok 클러스터는 etcd·apiserver·controller-manager·scheduler 만 진짜로 돌린다. 그래서 CRD 를 등록하고 Application 을 만들 수는 있어도 그것이 스스로 Synced 로 바뀌지는 않는다. 이 모듈에 실습이 없는 이유가 이것이고, 이것을 숨기지 않는 것이 이 커리큘럼의 방침이다. 대신 여러분은 이미 같은 일을 손으로 했다 — gitops-manifest 8단계에서 만든 sync.sh 가 정확히 3~6단계를 한 번 도는 스크립트다. 컨트롤러는 그것을 잊지 않고 반복하는 존재일 뿐이다.
둘째, "왜 되돌아갔지"의 정체. 운영 중 가장 자주 오는 문의가 이것이다. 설정을 고쳤는데 몇 분 뒤 원래대로 돌아가 있다. 답은 언제나 같다 — selfHeal 이 켜져 있고, 그 변경이 저장소에 없기 때문이다. 이때 화를 낼 대상은 도구가 아니라 프로세스다. 저장소를 거치지 않은 변경은 애초에 존재하지 않는 변경으로 취급하기로 팀이 합의했기 때문이다.
셋째, 자동 동기화의 범위를 환경별로 나눈다. 흔한 절충은 dev 에는 prune 과 selfHeal 을 모두 켜고, 프로덕션에는 selfHeal 만 켜거나 아예 수동 동기화로 두는 것이다. 실수로 병합된 삭제가 즉시 프로덕션에 반영되는 사고를 막기 위해서다. 어느 쪽을 고르든 그 선택을 Application YAML 에 적어 저장소에 커밋하는 것 자체가 GitOps 다.
다음 확인에서 볼 것
이 모듈에는 실습이 없다. 대신 지금까지 만든 것을 다시 열어 보라 — /root/gitops/out/drift-diff.txt 는 컨트롤러가 4단계에서 계산하는 것과 같은 종류의 결과이고, sync.sh 는 6단계를 한 번 수행하는 스크립트이며, Application 의 syncPolicy 는 그 루프를 어떻게 돌릴지 적어 둔 설정이다. 이어지는 퀴즈에서 각 스위치가 정확히 무엇을 되돌리고 무엇을 지우는지 확인한다.