- Published on
Argo CD 앱 OutOfSync 반복되는 원인 6가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Argo CD를 운영하다 보면 Synced로 맞춰놨는데도 얼마 지나지 않아 다시 OutOfSync로 돌아가는 “반복 루프”를 겪습니다. 이때 중요한 건 “누가(어떤 컨트롤러/웹훅/사람이) 무엇을(어떤 필드) 언제(Apply 직후/몇 분 후) 바꾸는가”를 빠르게 특정하는 것입니다.
이 글은 OutOfSync 반복을 만드는 대표 원인 6가지를 증상, 진단 포인트, 해결책으로 정리합니다. 또한 재현과 확인을 위해 argocd, kubectl, diff/로그 확인 중심으로 접근합니다.
OutOfSync 반복을 먼저 분해하는 관찰법
OutOfSync는 한 줄로 말하면 “Git에 있는 desired state”와 “클러스터 live state”가 다르다는 뜻입니다. 반복된다는 건 다음 중 하나입니다.
- 동기화 직후 또는 몇 분 후 live state가 자동으로 바뀐다(컨트롤러/웹훅/클라우드 통합).
- Git 쪽 매니페스트가 자동으로 바뀐다(Helm values 생성, Kustomize patch 생성, 파이프라인이 커밋).
- Argo CD가 “차이를 계산하는 방식” 때문에 계속 다르다고 판단한다(정렬, 기본값, 무시 규칙 부재).
가장 먼저 아래 3가지를 확인하면 원인 범위가 급격히 좁혀집니다.
- 어떤 리소스가 OutOfSync인가
- Argo CD UI에서
APP→RESOURCE를 눌러 “Diff가 발생한 리소스”를 특정합니다. - CLI로도 확인합니다.
argocd app get my-app
argocd app diff my-app
- 어떤 필드가 바뀌는가
- Diff 화면에서 바뀌는 필드 경로를 메모합니다. 예:
spec.replicas,metadata.annotations,spec.template.spec.containers[0].image등
- 누가 바꾸는가(ManagedFields/이벤트/웹훅)
managedFields를 보면 마지막으로 필드를 수정한 매니저가 보입니다.
kubectl get deploy my-deploy -n my-ns -o yaml | sed -n '1,220p'
# 또는 jq로 manager만 뽑기
kubectl get deploy my-deploy -n my-ns -o json \
| jq -r '.metadata.managedFields[] | [.manager, .operation, .time] | @tsv' \
| sort -u
- 이벤트도 같이 봅니다.
kubectl get events -n my-ns --sort-by=.lastTimestamp | tail -n 50
이제부터는 “원인 6가지”를 하나씩 대입해보면 됩니다.
원인 1) Admission Webhook이 필드를 자동 변형한다
대표 증상
- 동기화 직후 곧바로 OutOfSync로 변함
- Diff에
metadata.annotations나spec.template.metadata.annotations가 자주 등장 - Istio/Linkerd 사이드카, OPA Gatekeeper/Kyverno 정책, 보안/이미지 정책 웹훅이 개입
예: 사이드카 인젝션이 spec.template.metadata.annotations를 바꾸거나, 보안 정책이 securityContext를 강제합니다.
진단 포인트
- 해당 네임스페이스/리소스에 적용되는 MutatingWebhookConfiguration 확인
kubectl get mutatingwebhookconfiguration
kubectl describe mutatingwebhookconfiguration my-webhook
managedFields의manager가kube-apiserver혹은 특정 웹훅 컨트롤러로 표시되고, 바뀌는 필드가 웹훅이 다루는 영역이면 거의 확정입니다.
해결책
- 가능한 경우: Git 매니페스트에 웹훅이 강제하는 값을 “처음부터” 명시해서 드리프트를 제거
- 불가한 경우: Argo CD에서 해당 필드를 diff에서 제외
argocd-cm에 resource.customizations를 추가합니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
resource.customizations: |
apps/Deployment:
ignoreDifferences: |
jsonPointers:
- /spec/template/metadata/annotations
주의: 무시 규칙은 “진짜로 의미 없는 변형”에만 적용하세요. 보안 정책처럼 중요 필드를 무시하면 사고로 이어질 수 있습니다.
원인 2) HPA/VPA/클러스터 오토스케일러가 replicas나 requests를 계속 바꾼다
대표 증상
- Diff에
spec.replicas가 계속 뜸 - 또는
resources.requests/resources.limits가 계속 바뀜(VPA) - 트래픽이 있는 시간대에만 OutOfSync가 자주 발생
진단 포인트
- HPA/VPA 존재 여부 확인
kubectl get hpa -n my-ns
kubectl get vpa -n my-ns
- 디플로이먼트의
spec.replicas가 Git과 다르고, 이벤트에Scaled up replica set같은 메시지가 보이면 HPA 가능성이 큽니다.
해결책
- Git에서
spec.replicas를 관리하지 않도록 Argo CD에서 해당 필드를 무시
data:
resource.customizations: |
apps/Deployment:
ignoreDifferences: |
jsonPointers:
- /spec/replicas
- 또는 아예 아키텍처적으로 분리: “스케일은 HPA가, 나머지는 GitOps가” 책임을 갖도록 합의합니다.
원인 3) External Secrets/CSI Driver/ServiceAccount 토큰 등 외부 컨트롤러가 Secret을 재작성한다
대표 증상
- Diff 대상이
Secret또는ConfigMap data가 계속 바뀌거나,metadata.annotations에 리프레시 타임스탬프가 찍힘- 동기화 후 일정 주기(예: 1분, 5분, 1시간)로 OutOfSync 재발
진단 포인트
managedFields.manager가external-secrets,secrets-store-csi-driver,vault등으로 나타남- 해당 Secret이 Argo CD가 직접 관리해야 하는 대상인지, 외부 소스의 “출력물”인지 구분이 필요
해결책
- 원칙: 외부 컨트롤러가 생성/갱신하는 Secret은 Argo CD가 “소유”하지 않게 설계
- 방법 1: Argo CD 앱 소스에서 그 Secret을 제외(템플릿에서 생성하지 않기)
- 방법 2: 꼭 포함해야 한다면
ignoreDifferences로data를 무시(권장도는 낮음)
data:
resource.customizations: |
v1/Secret:
ignoreDifferences: |
jsonPointers:
- /data
운영 팁: AWS STS/IRSA, 네트워크 정책/IPv6 등 외부 요인으로 시크릿 동기화가 실패하면서 “반쯤 갱신된 상태”가 반복될 때도 있습니다. 이런 케이스는 클러스터/네트워크 레벨 진단이 필요할 수 있는데, 비슷한 접근으로 원인을 좁히는 글로는 EKS Pod에서 IPv6로만 STS 403 뜰 때 해결도 참고할 만합니다.
원인 4) Helm/Kustomize 렌더링 결과가 비결정적이거나(정렬/타임스탬프) 환경별 값이 섞인다
대표 증상
- Git에는 변화가 없는데도 diff가 조금씩 달라짐
- 리스트/맵 순서 차이, 자동 생성 어노테이션, 랜덤 문자열이 포함된 템플릿이 원인
- 예: Helm 차트에서
now같은 함수를 쓰거나, 파이프라인이buildTimestamp를 매번 바꿈
진단 포인트
- Argo CD가 실제로 렌더링한 매니페스트를 확인
argocd app manifests my-app | sed -n '1,200p'
- 로컬에서도 동일한 커밋으로 렌더링을 재현
helm template my-release ./chart -f values.yaml > rendered.yaml
# Kustomize
kustomize build . > rendered.yaml
같은 입력인데 출력이 매번 달라지면 “비결정적 렌더링”입니다.
해결책
- 타임스탬프/랜덤/환경 의존 값을 템플릿에서 제거하고, 필요하면 CI에서 명시적으로 주입
- 리스트/맵 정렬로 인해 diff가 발생한다면, Argo CD 버전업 또는 차트/쿠스텀 리소스의 정렬을 안정화
- 이미지 태그를
latest처럼 떠다니는 값으로 쓰지 말고, 불변 태그(커밋 SHA 등)로 고정
원인 5) Server-side apply, field ownership 충돌로 Apply 후에도 다른 매니저가 되돌린다
대표 증상
- 동기화는 성공으로 보이는데 특정 필드만 계속 원복
managedFields에 여러 매니저가 같은 필드를 소유- 예: 사람이
kubectl edit로 수정하거나, 다른 CD 도구가 병행 적용
진단 포인트
managedFields에서 같은 경로를 누가 소유하는지 확인(완벽히 보려면kubectl get -o json과 함께 분석)- 동일 리소스를 다른 파이프라인/툴이 동시에 관리하고 있지 않은지 확인
해결책
- “하나의 리소스는 하나의 소유자” 원칙으로 정리
- 불가피하게 공존해야 한다면:
- Argo CD가 관리할 필드만 남기고 나머지는 분리(리소스 분할, CRD 분리)
- 사람의 수동 변경을 금지하고, Git으로만 변경되게 프로세스 고정
운영 팁: 이런 종류의 루프는 시스템 서비스의 재시작 루프처럼 “원인 추적”이 핵심입니다. 로그/소유자/이벤트를 축으로 좁혀가는 접근은 systemd 서비스 재시작 루프 진단 - 로그·유닛·쉘에서 설명하는 방법론과도 유사합니다.
원인 6) Argo CD diff 설정 미흡: status/동적 필드/CRD 스펙 변화로 계속 다르다고 판단
대표 증상
- 실제로는 서비스가 정상인데, UI에서만 OutOfSync가 반복
- Diff에
status나 컨트롤러가 채우는 동적 필드가 포함 - CRD(예:
Ingress,Certificate,ApplicationSet등)에서 컨트롤러가 기본값을 채워 넣어 Git과 달라짐
진단 포인트
- Diff에 “운영 중에 컨트롤러가 채우는 값”이 보이는지 확인
- 예:
status하위 - 예:
metadata.annotations중kubectl.kubernetes.io/last-applied-configuration - 예: Ingress 컨트롤러가 추가하는 어노테이션/필드
- 예:
해결책
- 리소스 종류별로 필요한 만큼만
ignoreDifferences를 정교하게 적용 - 특히 CRD는 컨트롤러가 기본값을 주입하는 경우가 많으므로, JSON Pointer로 정확히 지정하는 편이 안전합니다.
예: Ingress의 특정 어노테이션만 무시
data:
resource.customizations: |
networking.k8s.io/Ingress:
ignoreDifferences: |
jsonPointers:
- /metadata/annotations/some.ingress.controller~1generated
JSON Pointer에서 /는 ~1로 이스케이프해야 합니다.
빠른 체크리스트: 10분 안에 루프를 끝내는 순서
argocd app diff로 “변하는 리소스 1개”를 먼저 고른다(동시에 여러 개면 가장 상단/핵심부터)- Diff에서 변하는 필드 경로를 적는다
managedFields로 변경 주체(manager)를 확인한다- 주체가 컨트롤러라면: 그 컨트롤러의 의도(스케일/시크릿/인젝션)를 존중하고 Argo CD 소유 범위를 조정한다
- 주체가 사람/다른 CD라면: 소유권을 정리한다
- 마지막 수단으로
ignoreDifferences를 최소 범위로 적용한다
재현 가능한 진단 스니펫 모음
특정 리소스만 골라 diff 보기
argocd app diff my-app --resource apps:Deployment:my-ns:my-deploy
라이브 리소스에서 마지막 변경자 빠르게 보기
kubectl get deploy my-deploy -n my-ns -o json \
| jq -r '.metadata.managedFields | sort_by(.time) | last | {manager, operation, time}'
Argo CD가 보고 있는 상태 확인(컨트롤러 관점)
argocd app get my-app -o json | jq '.status.sync, .status.resources[] | select(.status=="OutOfSync") | {kind, namespace, name, message}'
마무리
OutOfSync 반복은 Argo CD 자체의 문제가 아니라, 대개 “클러스터는 살아 움직이는데(GitOps 외부 컨트롤러/정책/스케일러) Git은 정적 상태를 가정한다”는 충돌에서 발생합니다. 반복 루프를 끊는 가장 현실적인 방법은 다음 두 가지 중 하나입니다.
- Git이 정말로 관리해야 하는 값만 남기고, 나머지는 컨트롤러의 책임으로 인정한다(필드/리소스 경계 재설계)
- 어쩔 수 없이 공존해야 하면,
ignoreDifferences를 최소 범위로 정교하게 적용한다
원인을 6가지로 분류해두면 다음에 같은 증상이 나타나도 “Diff 필드”와 “manager”만으로 빠르게 결론에 도달할 수 있습니다.