우리 플랫폼의 배포는 전부 ArgoCD 위에서 돈다. ArgoCD를 깐 건 내가 아니지만, GitOps도 ArgoCD도 큰 그림은 대부분 알고 매일 그 위에서 배포를 다뤄왔다. 다만 "OutOfSync면 이게 장애인가?" 같은 질문에 원리로 답하기보다 익숙함으로 넘어가는 지점이 남아 있어서, 언젠가는 기본부터 딥하게 다시 파보고 싶었다. 그러던 차에 두 공용 클러스터의 ArgoCD 버전을 우연히 같이 확인했는데, 한쪽은 `v2.12.3`, 다른 쪽은 `v3.3.4`. 메이저가 통째로 한 칸 차이였다. "같은 ArgoCD인 줄 알았는데 사실 두 개였구나"라는 가벼운 충격이, 미뤄두던 그 공부를 처음부터 다시 시작하게 만든 출발점이었다. 이 글은 그 과정에서 정리한 GitOps의 본질, 컴포넌트 구조, 그리고 가장 많이 헷갈리는 Sync와 Health라는 두 축에 대한 기록이다.
Intro
- ArgoCD의 본질은 한 문장이다: Git(원하는 상태)과 클러스터(실제 상태)를 계속 비교(diff)해서 같아지도록 맞추는(sync) 루프. 나머지 기능은 전부 이 루프의 변주다.
- 파드가 7종이나 뜨는 게 처음엔 과해 보였는데, 데이터 평면은 둘뿐이었다.
application-controller(diff+sync 엔진)와repo-server(매니페스트 렌더). 나머지(server/redis/dex/applicationset/notifications)는 부가 기능이라, UI 서버가 죽어도 배포 자체는 계속 돈다. - 가장 크게 바로잡은 오해: Sync(Git 일치도)와 Health(런타임 건강)는 직교하는 두 축이다.
OutOfSync ≠ 장애,Synced ≠ 정상. 이 두 줄을 모르면 상태판을 영영 못 읽는다. - 두 클러스터의 버전 격차(v2 vs v3)는 단순 패치가 아니라 메이저 업그레이드 거리였다. "우리 ArgoCD"라는 단일 실체는 없고, 클러스터마다 따로 봐야 한다는 걸 그때 알았다.
- 운영 깊이로 더 판 것들: 웹 터미널(exec) 기능과 그 보안 함의, UI 없이 CLI/kubectl로 제어하기, Settings 메뉴가 실제로 어느 k8s 리소스에 저장되는지. 이걸 알면 GitOps로 코드화가 된다.
이 글에 등장하는 환경은 검증(alpha)과 베타(beta) 두 공용 클러스터다. 운영 사고 분석이 아니라, 남이 깔아둔 도구를 내가 제대로 이해하려고 파헤친 학습 기록으로 읽으면 된다. 사내 식별자(레지스트리 호스트, 서비스명 등)는 전부 placeholder로 치환했다.
출발점 - "같은 ArgoCD"라는 착각
두 공용 클러스터를 같이 점검하다가 버전을 나란히 찍어봤다.
검증(alpha) 공용 클러스터 : quay.io/argoproj/argocd:v2.12.3
베타(beta) 공용 클러스터 : <internal-registry>/argoproj/argocd:v3.3.4
두 가지가 동시에 눈에 들어왔다. 첫째, 버전이 메이저 단위로 다르다. v2.12와 v3.3은 패치 차이가 아니라 한 세대 차이다. 둘째, 이미지를 끌어오는 출처도 다르다. 한쪽은 외부 공식 레지스트리(quay.io)에서 직접 받고, 다른 쪽은 사내 미러 레지스트리(<internal-registry>)를 경유한다. 사이드카로 붙는 dex와 redis도 베타 쪽이 한 단계씩 최신이었다.
그동안 나는 "ArgoCD는 그냥 ArgoCD지"라고 뭉뚱그려 생각했다. 그런데 클러스터마다 버전도, 이미지 경로도, 심지어 올라간 앱 구성도 달랐다. 상태판을 볼 때 "이 클러스터의 이 ArgoCD"로 좁혀서 보지 않으면 안 된다는 걸 그때 깨달았다.
그래서 마음먹었다. 큰 그림은 알아도 클러스터마다 다른 이 도구를, 이번엔 원리부터 제대로 짚자.
ArgoCD가 푸는 문제 - GitOps라는 방향 전환
먼저, ArgoCD가 뭐 하는 물건인가
한 문장으로 줄이면 이렇다.
Git 저장소의 선언적 매니페스트를 "원하는 상태"로 보고, 클러스터의 "실제 상태"가 그것과 같아지도록 계속 맞추는(reconcile) 쿠버네티스용 GitOps CD 컨트롤러.
압축된 문장이라, 처음 보는 입장에서 풀어 쓰면 이렇다.
- 어디서 도나 : 별도 외부 서버가 아니라 쿠버네티스 클러스터 안에 상주하는 컨트롤러다. 한 번 설치되면 계속 떠 있으면서 일한다.
- 무엇을 단위로 다루나 :
Application이라는 단위다. "이 Git repo의 이 경로(매니페스트)를, 이 클러스터의 이 네임스페이스에 배포해라"라는 선언 하나가 Application 하나다. ArgoCD는 이 Application들을 죽 늘어놓고 관리한다. - 사용자가 실제로 보는 것 : 웹 대시보드에 Application이 카드처럼 뜨고, 각각에 대해 "Git이랑 같은가(Sync)"와 "잘 떠 있나(Health)"가 색으로 표시된다. 바로 이 두 축이 이 글의 핵심이라 4장에서 따로 판다.
- 어떻게 시작하나 : 배포 소스가 될 Git repo와 배포 대상 클러스터를 등록하고, Application을 정의해 주면, 그 다음부터는 컨트롤러가 알아서 Git을 당겨와 맞춘다.
핵심 동작은 단 하나, diff → sync → 반복이다. Git에 선언된 상태와 클러스터의 실제 상태를 끊임없이 비교(diff)하고, 다르면 같아지도록 적용(sync)한다. 나머지 기능은 전부 이 루프의 변주다.
왜 이게 방향 전환인가
이게 왜 의미가 있는지는 전통적인 배포 방식과 비교하면 선명해진다.
| 전통 방식 (push CD) | GitOps (pull CD, ArgoCD) |
|---|---|
CI 파이프라인이 클러스터에 kubectl apply를 밀어넣음 |
클러스터 안의 컨트롤러가 Git을 당겨와 스스로 적용 |
| 클러스터 자격증명을 CI에 노출해야 함 | CI는 Git에 머지만 하면 됨, 클러스터 크레덴셜 외부 노출 없음 |
| 실제 상태가 코드와 어긋나도(drift) 모름 | drift를 상시 감지해 OutOfSync로 표면화 |
| "지금 뭐가 떠 있나"의 단일 진실원천이 없음 | Git이 곧 단일 진실원천(SSOT), 롤백은 git revert |
내 입장에서 가장 와닿은 변화는 이거였다. 배포가 "파이프라인이 클러스터에 apply"가 아니라 "Git에 머지 → 컨트롤러가 알아서 pull"로 바뀐다. 백엔드 개발자에게 이건 곧 PR 머지가 배포 트리거라는 뜻이고, 롤백은 git revert 한 줄로 끝난다. 누가 클러스터에서 직접 kubectl edit로 뭔가를 바꿔놔도, ArgoCD가 "Git이랑 다른데?"라고 잡아낸다(self-heal을 켜면 자동 원복까지).
구조 - 왜 파드가 7종이나 뜨는가
처음 argocd 네임스페이스를 봤을 때 파드가 일곱 종류나 떠 있어서 "CD 도구 하나가 왜 이렇게 무겁지?" 했다. 각자 역할을 정리하고 나서야 납득이 됐다.

| 컴포넌트 | 역할 | 없으면 |
|---|---|---|
| application-controller | 핵심 reconcile 엔진. desired와 live의 diff를 계산하고 sync(apply)를 실행 | 배포 자체가 멈춤 |
| repo-server | Git을 clone한 뒤 helm/kustomize를 렌더링해 순수 manifest로 변환. CPU/메모리를 많이 씀 | manifest 생성 불가 |
| server | REST/gRPC API + Web UI + RBAC 진입점. CLI도 여기로 붙음 | UI/CLI 조작 불가 (배포는 계속 돎) |
| redis | 렌더 결과와 diff 결과를 담는 휘발성 캐시 | 느려질 뿐 데이터 유실은 아님 |
| dex-server | 외부 IdP(OIDC/SAML/LDAP)와 SSO를 중개 | 내장 로컬 계정만 가능 |
| applicationset-controller | 템플릿 + generator로 Application을 대량 자동 생성 | 수동으로 만들어야 함 |
| notifications-controller | sync/health 상태 변화를 Slack/웹훅으로 통지 | 알림만 끊김 |
여기서 가장 중요한 통찰은 이거였다. 실제 배포를 책임지는 데이터 평면은 application-controller와 repo-server 둘뿐이고, 나머지는 부가 기능이다. 그래서 server(UI) 파드가 죽어도 배포는 멀쩡히 계속 돈다. "UI가 안 떠요 = 배포가 멈췄어요"가 아니라는 것. 이 구분 하나로 장애 대응의 우선순위가 달라진다.
참고로 내가 본 두 클러스터는 양쪽 다 모든 컴포넌트가 단일 인스턴스(비HA)였다. redis도 단일이고 컨트롤러 샤딩도 없었다. 앱이 더 늘면 이 부분이 손볼 1순위라는 메모를 남겨뒀다.
핵심 개념 - Sync와 Health는 다른 축이다
원리로 다시 짚으면서 가장 또렷해진 지점이 여기다. Application의 상태는 완전히 독립된 두 차원으로 표시되는데, 바쁠 때는 이 둘을 뭉뚱그려 "초록색이면 좋은 거, 아니면 나쁜 거" 정도로만 보고 넘기기 쉽다.

Sync는 "Git과 같은가"를 본다. Synced는 실제 상태가 Git과 일치한다는 뜻이고, OutOfSync는 차이가 있다는 뜻이다. 여기서 핵심은, 자동 sync가 꺼져 있으면 OutOfSync여도 사람이 sync 버튼을 누를 때까지 그냥 그대로 머문다는 것. Git이 한 발 앞서 있을 뿐 클러스터는 멀쩡할 수 있다.
Health는 "잘 돌고 있는가"를 본다. Healthy/Progressing(롤아웃 진행 중)/Degraded(죽음)/Missing(아직 생성 안 됨)/Suspended(의도적 일시중지).
두 축이 직교한다는 걸 받아들이면, 헷갈리던 조합들이 한순간에 정리된다.
OutOfSync+Healthy= Git이 앞섰을 뿐 서비스는 멀쩡. 가장 흔한 "정상" 케이스다. 실제로 두 클러스터를 훑어보니, OutOfSync로 떠 있는 앱들 대부분이 이 경우였다. 자동 sync를 안 켜둔 앱이 사람의 수동 sync를 기다리고 있을 뿐, 트래픽은 정상이었다.Synced+Degraded= Git대로 깔리긴 했는데 앱이 죽음. 이게 진짜 문제다.Suspended= 누가 의도적으로 멈춰둔 것. 장애가 아니다.
이건 예전에 Argo Rollouts를 파면서 얻었던 교훈("Degraded ≠ 장애, stable이 살아있으면 서비스는 정상")과 정확히 같은 결의 깨달음이었다. 상태 라벨 하나만 보고 놀라지 말고, 축을 분리해서 읽어라.
reconcile은 기본 3분 주기 polling에 Git webhook(즉시 반영)이 더해져 돌아간다. 컨트롤러가 repo-server에 "이 repo의 이 경로를 렌더해줘"라고 요청하고, 돌아온 순수 manifest를 클러스터의 live 상태와 비교해서, 차이가 있고 자동 sync면 적용하는 흐름이다.
운영 깊이로 더 판 것들
원리를 잡고 나니, 그동안 UI에서 그냥 지나치던 메뉴와 기능들이 비로소 눈에 들어왔다.
웹 터미널(exec) - 편리함과 위험은 한 몸이다
UI에서 Application의 Pod를 클릭하면 컨테이너에 바로 셸로 붙는 기능이 있다(v2.4+ 코어 내장, 플러그인 아님). 사실상 kubectl exec를 웹에서 하는 것이다. 그런데 내가 본 두 클러스터는 양쪽 다 이 기능이 꺼져 있었다. 확인해보니 활성화에는 세 가지가 모두 필요했다.
argocd-cm에exec.enabled: "true"스위치argocd-rbac-cm에 exec 권한 부여 (별도 액션이라 일반 application 권한만으론 안 됨)argocd-server재기동
기본값이 전부 꺼짐인 건 의도된 보안 기본값이다. 웹 로그인만으로 운영 파드 내부에 들어갈 수 있게 되는 거라, 가장 위험한 액션이기 때문이다. 두 클러스터의 기본 RBAC 정책이 readonly로 잡혀 있던 것도 같은 맥락의 안전한 상태였다. 켤 거라면 특정 admin 역할에만 좁게 주고, exec 세션 추적을 위해 server 로그를 별도 수집하는 게 맞다(터미널 세션은 API audit 로그에 직접 안 남을 수 있다).
UI 없이 제어하기 - CLI와 kubectl
UI는 보기 편할 뿐, 모든 조작은 argocd CLI나 kubectl로 똑같이 된다. 자동화/CI에선 오히려 이쪽이 기본이다.
# argocd CLI (argocd-server로 붙음, 서버 RBAC 적용)
argocd app list # 전체 앱 + sync/health 한눈에
argocd app diff <app> # desired vs live 차이
argocd app sync <app> # 수동 sync(배포 반영)
argocd app history <app> # 배포 이력
argocd app rollback <app> <revision> # 특정 revision으로 롤백
# kubectl (CRD 직접, server를 안 거침)
kubectl -n argocd get applications.argoproj.io
kubectl -n argocd get app <app> -o yaml
여기서 놓치기 쉬운 함정이 하나 있다. kubectl 직접 조작은 argocd-server의 RBAC를 우회하고 k8s RBAC만 적용된다. 편하다고 평소에 kubectl로 막 만지면 감사/권한 일관성이 깨진다. 평상시는 UI/CLI로, kubectl은 server가 죽었거나 디버깅할 때의 비상용으로 한정하는 게 안전하다.
Settings 메뉴 - 실제로는 전부 k8s 리소스다
UI 좌측 Settings 하위 메뉴들이 그동안 막연했는데, 각 메뉴가 실제로 어느 k8s 리소스에 저장되는지를 알고 나니 전부 GitOps로 코드화할 수 있는 대상으로 보였다.
| 메뉴 | 무엇을 하나 | 저장되는 곳 | 주로 이렇게 쓴다 |
|---|---|---|---|
| Repositories | 배포 소스가 될 Git/Helm/OCI repo 등록 + 인증 | Secret |
사내 git을 토큰/SSH로 연결. 제일 자주 건드림 |
| Repository certificates and known hosts | 사설 TLS 인증서, SSH known_hosts 등록 | ConfigMap 2종 | 사내 자체 CA / SSH git 연동 시 필수. 안 넣으면 repo 연결이 인증서/호스트 검증에서 막힘 |
| GnuPG Keys | 커밋 서명 검증용 공개키 등록 | ConfigMap | 서명된 커밋만 sync 허용(공급망 보안). AppProject의 signatureKeys와 짝 |
| Clusters | 배포 대상 클러스터 등록 | Secret |
in-cluster(자기 자신) 기본 + 외부 클러스터 추가 = 멀티클러스터 배포의 핵심 |
| Projects | AppProject 관리: repo/cluster/namespace 화이트리스트, RBAC | AppProject CRD |
운영에선 default를 안 쓰고 팀/환경별로 쪼개 권한을 좁힘 |
| Accounts | 로컬 계정 관리, API 토큰 발급 | ConfigMap + Secret | CI 봇 계정과 토큰 발급용. 내가 본 클러스터에도 CI/운영용 봇 계정 몇 개가 이걸로 만들어져 있었다 |
| Appearance | UI 테마 등 개인화 | 사용자/브라우저 단위 | 기능/보안 영향 없음 |
특히 certificates와 known_hosts는 사내 자체 CA를 쓰거나 SSH로 git에 붙는 환경(즉, 사내망 대부분)에서 안 넣으면 repo 연결 자체가 안 된다는 걸 알아두면 디버깅이 빨라진다. 그리고 이 대부분이 ConfigMap/Secret/CRD라서, UI에서 클릭으로 설정하기보다 매니페스트로 관리하면 재현·감사·롤백이 된다. 내가 본 클러스터도 ArgoCD가 자기 자신을 Application으로 관리(self-managed)하면서 이 설정들을 Git에서 끌고 있었다.
auto-sync는 FE만, BE는 수동 sync로 가져가는 이유
배포를 어디까지 컨트롤러에 맡길지는 결국 자동화해도 안전한 영역과 그렇지 않은 영역을 가르는 일이다. 나는 그 선을 FE와 BE 사이에 그었다.
출발점이 되는 인식 하나. ArgoCD의 auto-sync는 BE 배포에서 생길 문제를 풀어주는 기능이 아니다. auto-sync는 "Git에 머지되면 사람 손 없이 바로 적용한다"는 자동화 편의일 뿐, 부팅 시 도는 DB 마이그레이션이 실 스키마를 건드리거나 외부 연계가 어긋나는 문제를 대신 막아주지 않는다. 오히려 그런 위험이 있는 영역에선 자동화를 빼고 사람이 sync 타이밍을 쥐는 것 자체가 가드레일이 된다. 그래서 BE 배포는 OutOfSync로 띄워두고 수동으로 sync한다.
영역을 가르는 실제 기준은 이렇다.
- FE(React, CSR) : 빌드 결과물이 정적 번들이라 stateless하고 부수효과가 없다. 잘못 나가도 이전 번들로 즉시 롤백하면 끝이다. 자동화해도 안전한 영역이라 auto-sync를 켠다.
- BE(Spring Boot) : 부팅 시 도는 DB 스키마 마이그레이션(Flyway/Liquibase) 같은 비가역 부수효과가 붙고, 외부 연계 시스템과의 타이밍 조율이 필요하며, 부팅이 길어 rollout 위험 창도 크다. 롤백도 DB 상태까지 얽혀 비싸다. 자동화를 빼고 사람을 게이트로 끼우는 게 안전한 영역이라 수동 sync로 둔다.
여기서 진짜 판별 축은 'FE냐 BE냐'가 아니라 'stateless + 부수효과 없음 + 즉시 롤백'이냐 'stateful 결합 + 마이그레이션 + 조율 필요'냐다. FE/BE는 그 축의 흔한 프록시일 뿐이고, 수동 sync는 위험한 쪽에 두른 가장 단순한 가드레일이다.
가드레일에도 층위가 있다. 내가 쓰는 수동 sync는 사람이 게이트하는 형태이고, 한 단계 더 가면 self-heal과 auto-prune을 끄고 DB 마이그레이션을 PreSync hook Job으로 분리하고 sync window로 배포 시간대를 제한하는 기능적 가드레일을 얹어 BE도 auto-sync로 옮길 수 있다. 다만 3장에서 봤듯 지금 환경은 단일 인스턴스·비HA라, 휴먼 게이트라는 단순한 선택이 합리적 디폴트였다.
보강 로드맵 / 결론
원리를 한 바퀴 돌고 나니, 막연하던 상태판이 읽히기 시작했다. 그리고 다음에 더 팔 것들이 분명해졌다.
- 버전 정렬: 두 클러스터의 v2 → v3 격차는 그 자체가 운영 리스크다. 메이저 업그레이드라 RBAC 기본 정책 변화, applicationset 동작 변경 등 breaking change를 먼저 검토하고 정렬 계획을 세워야 한다.
- HA 전환: 양쪽 다 단일 인스턴스라, 앱 수가 늘면 application-controller 샤딩 + redis-ha를 검토해야 한다. 단일 컨트롤러는 reconcile 정지의 단일 장애점이다.
- 관측성:
argocd_app_info(sync/health gauge)와 reconcile 지연 메트릭을 Prometheus로 잡고, "Application phase가 Healthy가 아닌 상태로 N분 지속"을 1차 경보선으로 두는 게 정석이다. - 공급망 보안: GnuPG 서명 커밋 검증 + AppProject signatureKeys로 "서명된 커밋만 배포"를 강제하는 것도 언젠가 적용해볼 후보다.
결국 ArgoCD는 한 문장으로 환원된다. "Git이 원하는 상태, 클러스터가 실제 상태, 컨트롤러가 둘을 맞춘다." 여기에 Sync와 Health라는 두 축만 얹으면, 도구의 90%는 읽힌다. 큰 그림으로만 알던 도구를, 이제는 컴포넌트 단위로 "지금 무슨 일이 벌어지고 있는지"까지 설명할 수 있는 상태로 넘어왔다. 그거면 이번 학습의 목적은 달성한 셈이다.
다음 글에서는 같은 결로, Argo Rollouts(Canary/Blue-Green 점진 배포)를 원리부터 정리한 기록을 풀어볼 생각이다. ArgoCD가 "무엇을 언제 배포하나(Git=SSOT)"를 푼다면, Rollouts는 "한 워크로드를 어떻게 점진적으로 배포하나"를 푸는, 층위가 다른 도구다.
'DevOps, SRE > GitOps' 카테고리의 다른 글
| Argo Rollouts Deep Dive (0) | 2026.06.11 |
|---|---|
| GitOps with ArgoCD (0) | 2025.05.13 |