GitOps는 "Git 저장소에 커밋된 것이 곧 클러스터의 상태"라는 원칙이다. ArgoCD는 그 원칙을 실제로 강제하는 pull 기반 컨트롤러로, Git과 클러스터가 어긋나면 감지하고 되돌린다.
kubectl apply를 손으로 때리던 시절엔 "지금 프로덕션에 뭐가 떠 있는지"를 아무도 정확히 몰랐다. GitOps로 넘어오면 그 질문의 답이 항상 하나다 — main 브랜치. 이 글은 그 전환을 실무 관점에서, 드리프트 감지·App of Apps·시크릿·롤백까지 파고든다.
몇 년 전 새벽 2시에 장애 대응을 하다가 이런 대화를 나눈 적이 있다. "이거 누가 언제 바꿨어요?" "저는 안 건드렸는데요." 아무도 모르는 변경이 프로덕션에 살아 있었다. kubectl로 급하게 패치한 게 그대로 눌러앉아 있었던 거다. 그날 이후로 나는 클러스터에 사람이 직접 손대는 걸 극도로 싫어하게 됐다. GitOps는 그 트라우마를 해결해준 방법론이다.
GitOps가 정확히 뭔가?
GitOps는 시스템의 원하는 상태(desired state)를 Git에 선언형으로 저장하고, 그 Git 상태를 자동으로 클러스터에 반영·유지하는 운영 방식이다. Weaveworks가 2017년에 이름 붙였고, 지금은 CNCF의 OpenGitOps 프로젝트가 4대 원칙으로 정리해두었다. 핵심은 딱 네 가지다.
- 선언형(Declarative) — 명령이 아니라 "결과 상태"를 기술한다. Deployment YAML은 "이렇게 돼 있어야 한다"이지 "이걸 해라"가 아니다.
- 버전 관리·불변(Versioned & Immutable) — 상태는 Git에 저장되고, 모든 변경은 커밋으로 남는다. 히스토리 전체가 감사 로그다.
- 자동 pull(Pulled automatically) — 에이전트가 클러스터 안에서 Git을 당겨온다. 외부에서 밀어넣지 않는다.
- 지속 조정(Continuously reconciled) — 실제 상태를 계속 관찰해서 원하는 상태로 수렴시킨다.
이 마지막 항목이 CI/CD 파이프라인이 kubectl apply를 한 번 실행하고 끝나는 것과 결정적으로 다른 지점이다. GitOps 컨트롤러는 배포 후에도 계속 지켜본다.
push 배포와 pull 배포는 뭐가 다른가?
가장 큰 차이는 제어권이 어디 있느냐다. 전통적 CI 배포(push)는 파이프라인이 클러스터 자격증명을 들고 밖에서 안으로 밀어넣는다. GitOps(pull)는 클러스터 안의 에이전트가 밖의 Git을 당겨온다. 사소해 보이지만 보안·감사·복구 측면에서 완전히 다른 세계다.
| 항목 | Push (CI가 kubectl apply) | Pull (GitOps / ArgoCD) |
|---|---|---|
| 클러스터 자격증명 | CI 시스템에 kubeconfig 노출 | 클러스터 밖으로 안 나감 |
| 드리프트 감지 | 없음(배포 후 방치) | 지속 감지·자동 교정 |
| 현재 상태의 진실원본 | 클러스터(직접 조회해야 앎) | Git(커밋 로그) |
| 롤백 | 역방향 파이프라인 재실행 | git revert 또는 이전 리비전 sync |
| 감사 추적 | CI 로그에 흩어짐 | 커밋=변경 이력 |
개인적으로 pull 모델의 가장 큰 실익은 "CI에 프로덕션 kubeconfig를 안 넣어도 된다"는 것이다. CI 러너가 털리면 클러스터도 같이 털리던 구조가 사라진다.
ArgoCD는 어떻게 동작하나?
ArgoCD는 클러스터에 상주하는 Kubernetes 네이티브 컨트롤러다. Application이라는 커스텀 리소스(CRD)를 정의하면, ArgoCD가 그 안에 적힌 Git 저장소·경로·대상 클러스터를 보고 실제 리소스와 비교(diff)한 뒤 동기화한다. 내부적으로 세 개의 축이 돈다.
- repo-server — Git을 clone해서 Helm/Kustomize를 렌더링하고 최종 manifest를 만든다.
- application-controller — 렌더된 desired state와 라이브 상태를 비교, sync를 수행한다.
- api-server / Web UI — 상태 조회, 수동 sync, RBAC, SSO를 담당한다.
기본 리컨실리에이션 주기는 3분(180초)이다. 즉 Git에 푸시해도 최대 3분까지 반영이 늦을 수 있다. 즉시 반영하고 싶으면 Git webhook을 걸어 ArgoCD에 알림을 쏘면 된다. 이건 문서를 대충 읽으면 놓치는 부분인데, webhook 없이 "왜 배포가 3분이나 걸리지?" 하고 헤매는 사람 많이 봤다.
가장 단순한 Application 하나 만들어보기
Application CRD는 결국 "어느 Git의 어느 경로를, 어느 클러스터의 어느 네임스페이스에 맞춰라"를 적는 YAML이다. 최소 형태는 이렇게 생겼다.
# app-guestbook.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: guestbook
namespace: argocd # Application 리소스 자체는 argocd 네임스페이스에 둔다
spec:
project: default
source:
repoURL: https://github.com/myorg/deploy-repo.git
targetRevision: main # 브랜치/태그/커밋SHA 모두 가능
path: apps/guestbook # 이 경로의 manifest를 배포
destination:
server: https://kubernetes.default.svc
namespace: guestbook
syncPolicy:
automated:
prune: true # Git에서 지운 리소스는 클러스터에서도 삭제
selfHeal: true # 클러스터가 드리프트되면 자동 복구
syncOptions:
- CreateNamespace=true # 대상 네임스페이스 없으면 생성
여기서 prune과 selfHeal가 GitOps의 심장이다. 이 둘을 켜야 비로소 "Git이 진실원본"이 강제된다. 끄면 그냥 예쁜 배포 대시보드일 뿐이다.
드리프트 감지와 self-heal은 실제로 어떻게 동작하나?
드리프트(drift)는 Git에 적힌 상태와 클러스터 실제 상태가 어긋난 것이다. 누군가 kubectl edit로 replicas를 3에서 10으로 바꿨다고 하자. ArgoCD는 이걸 OutOfSync로 표시한다. selfHeal: true면 다음 리컨실 때 다시 3으로 되돌린다. Git에 없는 변경은 그냥 소멸하는 거다.
이게 실무에서 양날의 검이다. 장점은 명확하다 — 아무도 몰래 프로덕션을 바꿀 수 없다. 단점도 명확하다 — 진짜 급한 핫픽스를 kubectl로 때리면 self-heal이 그걸 3분 안에 지워버린다. 나도 이걸로 한 번 데였다. 긴급 스케일업을 손으로 했는데 트래픽 폭주 중에 replicas가 원래 값으로 되돌아가서 파드가 죽어나갔다. 교훈: self-heal 환경에서 긴급 변경은 반드시 Git에 커밋하거나, 임시로 auto-sync를 꺼라.
ignoreDifferences로 /spec/replicas를 무시 처리해야 한다.# HPA와 충돌 방지: replicas 차이를 무시
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
App of Apps 패턴 — 앱을 앱으로 관리하기
App of Apps는 여러 Application을 관리하는 상위 Application 하나를 두는 패턴이다. 클러스터를 처음 세팅할 때 앱이 30개, 50개가 되면 Application YAML을 하나씩 kubectl apply 하는 게 지옥이다. 루트 앱 하나만 심으면 나머지가 줄줄이 딸려오게 만드는 게 핵심.
# root-app.yaml — 이 앱이 다른 Application들을 배포한다
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/deploy-repo.git
targetRevision: main
path: bootstrap/apps # 이 폴더 안에 Application YAML들이 있음
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
bootstrap/apps 폴더에 guestbook, redis, monitoring 각각의 Application YAML을 넣어두면, root 앱을 sync하는 순간 자식 앱들이 생성되고 각자 자기 소스를 sync한다. 신규 클러스터를 재해 복구용으로 다시 세울 때 root 앱 하나만 apply하면 전체가 복원되는 게 이 패턴의 진짜 가치다.
규모가 더 커지면 App of Apps 대신 ApplicationSet을 쓴다. Git 디렉터리나 클러스터 목록을 제너레이터로 돌려 Application을 자동 생성하는 상위 리소스라, 멀티 클러스터·멀티 환경에서 반복을 확 줄여준다. 개인적으로 "환경별로 똑같은 앱을 dev/staging/prod에 뿌린다" 같은 케이스는 ApplicationSet의 list/matrix 제너레이터가 App of Apps보다 훨씬 깔끔했다.
시크릿은 어떻게 다루나? (Git에 평문 금지)
GitOps의 최대 난제가 시크릿이다. 모든 걸 Git에 넣는데 DB 비밀번호를 평문으로 커밋할 순 없다. 정답은 암호화된 형태로 커밋하고, 클러스터 안에서만 복호화하는 것. 실무에서 자리잡은 선택지는 크게 셋이다.
| 방식 | 저장 위치 | 특징 | 언제 |
|---|---|---|---|
| Sealed Secrets | 암호문을 Git에 | 클러스터 컨트롤러가 복호화, 외부 의존 없음 | 단순·소규모 |
| External Secrets Operator | 참조만 Git에 | Vault/AWS SM/GCP SM에서 실값 동기화 | 중앙 시크릿 저장소가 이미 있을 때 |
| SOPS + age/KMS | 암호문을 Git에 | 파일 단위 암호화, 플러그인 필요 | Kustomize/Helm과 섞어 쓸 때 |
Sealed Secrets는 이렇게 쓴다. 로컬에서 일반 Secret을 만들고 kubeseal로 봉인하면, 봉인된 결과물은 오직 그 클러스터의 컨트롤러 개인키로만 풀린다. 그래서 Git에 올려도 안전하다.
# 1) 평문 Secret 생성 (클러스터에 apply하지 않고 파일로만)
kubectl create secret generic db-cred \
--from-literal=password='s3cr3t' \
--dry-run=client -o yaml > secret.yaml
# 2) kubeseal로 봉인 -> 이 결과를 Git에 커밋
kubeseal --format yaml \
--controller-namespace kube-system \
--controller-name sealed-secrets \
< secret.yaml > sealed-secret.yaml
주의할 함정 하나. Sealed Secrets의 봉인 키를 잃어버리면 기존 봉인 시크릿을 전부 다시 만들어야 한다. 클러스터를 날리고 재구축할 때 이 키 백업을 안 해두면 재해 복구 시나리오가 무너진다. 나는 이 컨트롤러 키만큼은 별도 안전 저장소에 따로 백업한다. 여기서만큼은 "전부 Git" 원칙에 예외를 둔다.
롤백은 어떻게 하나?
GitOps에서 롤백은 두 갈래다. Git 되돌리기가 정석, ArgoCD 리비전 롤백은 임시방편이다. 원칙적으로는 git revert로 나쁜 커밋을 되돌리고 푸시하면, ArgoCD가 이전 상태로 sync하면서 자연히 롤백된다. Git 히스토리에 "롤백했다"는 사실까지 남으니 감사 관점에서 가장 깨끗하다.
# 정석: Git에서 되돌리기 (히스토리에 남음)
git revert <bad-commit-sha>
git push origin main
# -> ArgoCD가 감지 후 자동 sync (auto-sync면), 아니면 수동 sync
# 급할 때: ArgoCD가 기록한 이전 배포 리비전으로 즉시 롤백
argocd app history guestbook
argocd app rollback guestbook 42 # history의 ID 지정
단, argocd app rollback은 auto-sync가 켜져 있으면 곧바로 Git 상태로 다시 튕겨온다. 그래서 급하게 리비전 롤백을 할 땐 먼저 auto-sync를 잠깐 꺼야 한다. 이 순서를 모르면 "롤백했는데 왜 또 원상복구되지?" 하고 당황한다 — 나도 처음엔 그랬다.
실전 도입 순서 — 어디서부터?
한 번에 전부 GitOps로 옮기려다 지친 팀을 여럿 봤다. 내가 권하는 순서는 이렇다.
- 저장소를 app repo(코드)와 config repo(manifest)로 분리한다. 이미지 태그 변경이 애플리케이션 커밋 히스토리를 오염시키지 않는다.
- 중요도 낮은 앱 하나를 auto-sync 없이 먼저 붙여 diff만 관찰한다. 감을 잡는 단계.
- 익숙해지면
prune·selfHeal을 켜서 진짜 GitOps로 전환한다. - 앱이 늘면 App of Apps → ApplicationSet 순으로 확장한다.
- 시크릿 전략을 초기에 확정한다. 나중에 바꾸면 이미 커밋된 시크릿 마이그레이션이 고통스럽다.
언제 GitOps를 쓰지 말아야 하나?
솔직히 GitOps가 항상 정답은 아니다. Kubernetes가 없는 환경(순수 VM, 서버리스만 쓰는 팀)에서는 ArgoCD의 이점이 대부분 사라진다. 앱이 두세 개뿐이고 배포가 주 1회도 안 되는 소규모라면, ArgoCD 운영 부담이 이득을 넘어설 수 있다. 컨트롤러 자체도 관리 대상이니까. 또 극단적으로 잦은 실험성 배포(하루 수백 번, 브랜치마다 프리뷰)는 순수 GitOps보다 별도 프리뷰 자동화가 더 맞을 때가 있다. 도구는 문제에 맞춰 고르는 거지, 유행이라 도입하는 게 아니다.
자주 묻는 질문
GitOps와 CI/CD는 다른 건가요?
겹치지만 다릅니다. CI/CD는 빌드·테스트·배포를 자동화하는 넓은 개념이고, GitOps는 그중 CD(배포) 단계를 "Git을 진실원본으로 삼는 pull 방식"으로 구현하는 구체적 방법론입니다. GitOps는 CI를 대체하지 않고, CD 부분을 맡습니다.
ArgoCD와 Flux 중 뭘 골라야 하나요?
둘 다 CNCF 졸업 프로젝트로 성숙합니다. ArgoCD는 강력한 Web UI와 멀티테넌시·SSO가 강점이라 UI로 상태를 보고 싶은 팀에 좋고, Flux는 UI 없이 GitOps toolkit 컴포넌트를 조합하는 경량·모듈형이라 자동화·코드 중심 팀에 맞습니다. UI가 중요하면 ArgoCD, 순수 선언형·경량이 좋으면 Flux를 권합니다.
Git에 시크릿을 평문으로 넣어도 되나요?
절대 안 됩니다. 프라이빗 레포라도 안 됩니다. Sealed Secrets나 SOPS로 암호화해 커밋하거나, External Secrets Operator로 Vault·클라우드 시크릿 매니저의 참조만 커밋하세요. 실제 값은 클러스터 안에서만 존재해야 합니다.
ArgoCD가 Git 변경을 반영하는 데 얼마나 걸리나요?
기본 폴링 주기가 3분(180초)이라 최대 그만큼 걸릴 수 있습니다. 즉시 반영하려면 Git 저장소에 webhook을 설정해 ArgoCD API 서버로 이벤트를 보내면 됩니다. 그러면 푸시 직후 sync가 트리거됩니다.
self-heal을 켜면 kubectl로 아무것도 못 바꾸나요?
바꿀 수는 있지만 유지되지 않습니다. selfHeal이 켜진 리소스는 다음 리컨실 주기에 Git 상태로 되돌아갑니다. 긴급 변경이 필요하면 Git에 커밋해서 정식 경로로 반영하거나, 해당 앱의 auto-sync를 일시적으로 비활성화한 뒤 수동 조치하세요.
모노레포와 멀티레포 중 뭐가 나은가요?
정답은 없지만, 최소한 애플리케이션 코드 레포와 배포 manifest 레포는 분리하는 걸 권합니다. 배포용 config는 단일 모노레포로 두고 폴더로 환경을 나누는 방식이 App of Apps/ApplicationSet과 잘 맞고 감사도 쉽습니다. 팀이 많고 권한 분리가 중요하면 레포를 더 쪼개세요.
스테이징과 프로덕션 환경 차이는 어떻게 관리하나요?
Kustomize의 base/overlays나 Helm values 파일로 공통 base를 두고 환경별 차이만 오버레이하는 방식이 표준입니다. YAML을 통째로 복붙해 두 벌 유지하면 반드시 어긋납니다. 환경 간 diff를 최소화하는 게 드리프트와 사고를 줄이는 핵심입니다.

댓글 0