금요일 오후 4시 30분에 kubectl apply -f deployment.yaml을 잘못 실행한 적이 있다.
Staging이 아니라 Production 클러스터에. 5명짜리 팀에서 혼자 backend를 담당하고 있던 때라, 그 순간이 지금도 생생하다. 슬랙에 사용자 오류 알람이 쏟아지기 시작했고, 나는 30분 동안 롤백하느라 진땀을 뺐다. 그때부터 “사람이 직접 kubectl을 치는 방식은 한계가 있다”는 걸 뼈저리게 느꼈다.
그래서 ArgoCD를 도입했다. ArgoCD 2.9 버전부터 써봤고, 지금은 2.10.x를 운영 중이다. 솔직히 처음엔 “그냥 GitHub Actions로 kubectl apply 자동화하면 되는 거 아닌가?” 싶었는데, 두 방식을 몇 달 동안 병행해보고 나서 생각이 완전히 바뀌었다.
ArgoCD가 GitHub Actions 기반 배포와 다른 이유
두 방식의 차이는 구조에 있다. GitOps의 핵심은 “Git이 단일 진실의 원천(source of truth)이 된다”는 거다. 즉, 클러스터에 어떤 상태가 배포되어 있어야 하는지를 Git repository가 기록하고, 그 상태를 지속적으로 동기화하는 역할을 별도의 도구가 맡는다.
GitHub Actions 방식은 push가 trigger가 되어 CI가 클러스터로 명령을 “밀어 넣는” 구조다. 외부에서 클러스터로 접근하는 방식이라 권한 관리도 복잡해진다. 반면 ArgoCD는 클러스터 안에서 실행되면서 Git repo를 주기적으로 바라보다가, 실제 클러스터 상태와 Git의 원하는 상태가 달라지면 자동으로 sync한다. Push 방식 vs Pull 방식의 차이인데, 운영하다 보면 이게 생각보다 크게 다가온다.
실제로 겪은 차이가 있다. GitHub Actions 기반일 때는 누군가 클러스터에 직접 들어가서 뭔가를 수동으로 바꿔도 알 방법이 없었다. ArgoCD는 그 즉시 “OutOfSync” 상태로 표시해준다. 팀원 한 명이 긴급 패치한다고 직접 kubectl로 환경변수를 바꿔놨던 게, ArgoCD 도입 후에 처음으로 발견됐다. 그 패치가 무려 3개월 동안 Git에 반영이 안 된 채로 있었다는 것도 그때 알았다.
실제 설치: 빠른데 함정이 있다
설치 자체는 간단하다. ArgoCD namespace를 만들고 공식 manifest를 apply하면 된다.
kubectl create namespace argocd
kubectl apply -n argocd -f \
https://raw.githubusercontent.com/argoproj/argo-cd/v2.10.0/manifests/install.yaml
문제는 그다음이다.
기본 설치를 하면 argocd-server가 ClusterIP로 뜬다. 로컬에서 접근하려면 port-forward를 써야 하고, 실제 운영 환경에서는 LoadBalancer나 Ingress를 붙여야 한다. 나는 이걸 몰라서 “왜 접속이 안 되지?” 하고 10분을 날렸다 — 공식 문서에도 적혀 있는 내용인데, 처음엔 설치에 급해서 그 부분을 그냥 지나쳤다.
admin 초기 비밀번호는 자동 생성된 secret에서 가져와야 한다:
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
로그인하고 나서 반드시 비밀번호를 바꿔야 한다. 처음에 테스트한다고 그냥 놔뒀다가, 프로덕션 전환할 때 초기 secret을 삭제하지 않아서 나중에 보안 검토에서 지적받은 적이 있다. 사소한 것 같아도 이런 게 쌓이면 나중에 골치아파진다.
설치 후 첫 번째 Application을 연결하는 방법은 두 가지다 — UI에서 하거나, YAML로 하거나. 나는 처음에 UI로 해보고 감을 잡은 다음 YAML 방식으로 전환했다. YAML 방식이 관리하기 편하고, 무엇보다 Application 설정 자체를 Git으로 관리할 수 있다는 점에서 GitOps 철학에 더 맞는다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-api-service
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/k8s-manifests
targetRevision: HEAD
path: apps/api-service
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Git에서 삭제된 리소스는 클러스터에서도 삭제
selfHeal: true # 클러스터 상태가 Git과 달라지면 자동으로 되돌림
syncOptions:
- CreateNamespace=true
prune: true와 selfHeal: true는 조심해야 한다. 특히 prune — Git에서 실수로 파일을 지웠다가 클러스터에서도 Deployment가 삭제되는 상황을 경험하고 싶지 않으면, 처음엔 이걸 꺼둔 채로 시작하길 권한다. 내가 딱 그 실수를 했다. 다행히 Staging이었지만.
Sync Policy 설정의 현실
자동 sync를 켤지 말지는 팀 상황에 따라 다르다 — 라고 말하고 싶지만, 솔직히 나는 Production에서 자동 sync를 꺼놓는다. Staging은 완전 자동, Production은 수동 승인 후 sync.
이유는 간단하다. main branch에 merge됐다고 해서 “Production에 바로 올려도 되는 상태”라는 보장이 없다. 우리 팀은 Staging에서 최소 30분 이상 돌아가는 걸 확인한 다음에 Production sync를 트리거한다. ArgoCD UI에서 “SYNC” 버튼 한 번 클릭이면 끝이라 충분히 편하다.
그런데 여기서 주목할 건 — syncPolicy를 자동으로 안 걸어도 ArgoCD는 여전히 drift를 감지해서 OutOfSync 상태를 보여준다. 자동 수정은 안 하지만, 클러스터 상태가 Git과 달라졌다는 걸 UI와 알람으로 알려준다. 이것만으로도 충분히 가치 있다.
ArgoCD의 Sync Wave 기능도 써보면 돌아오기 어렵다. 예를 들어 Database migration job을 먼저 실행하고 그다음에 API server를 배포하기” rel=”nofollow sponsored” target=”_blank”>배포하고 싶을 때, annotation 하나로 순서를 정할 수 있다:
# migration job
metadata:
annotations:
argocd.argoproj.io/sync-wave: "0" # 먼저 실행
---
# api server deployment
metadata:
annotations:
argocd.argoproj.io/sync-wave: "1" # 그다음 실행
이걸 알기 전에는 init container로 억지로 순서를 맞추고 있었는데, Sync Wave가 훨씬 깔끔하다. 다만 wave 사이의 대기 시간이 기본값으로는 짧아서, Health check가 느린 리소스가 있으면 직접 timeout 설정을 건드려야 한다.
운영하면서 마주친 실제 문제들
비공개 repo 연결할 때의 함정
처음에 SSH key 설정 방법을 몰라서 HTTPS + personal access token으로 연결했다. 그런데 이 방법은 token이 만료되거나 rotate될 때마다 ArgoCD 설정을 직접 업데이트해야 해서 번거롭다. 결국 SSH 방식으로 전환했고, ArgoCD UI의 Settings > Repositories에서 SSH key를 등록하면 깔끔하게 해결된다.
App of Apps 패턴은 선택이 아니다
클러스터에 올려야 하는 Application이 5개를 넘어가면, App of Apps 패턴이 거의 필수가 된다. ArgoCD Application 리소스들을 관리하는 상위 Application을 하나 만드는 방식이다. 이렇게 하면 새 서비스를 추가할 때 ArgoCD UI에서 직접 뭔가를 하지 않아도, Git에 Application YAML 파일 하나를 추가하기만 하면 된다.
그런데 이 패턴을 처음 적용했을 때 헷갈린 부분이 있었다. 상위 App이 하위 App들을 sync할 때, 하위 App 각각의 sync 설정이 독립적으로 동작한다. 이걸 이해 못하고 상위 App만 sync하면 다 될 줄 알았다가 한참 삽질했다.
리소스 삭제가 안 된다고 당황하지 말 것
ArgoCD로 관리되는 리소스를 직접 kubectl delete로 지우면, ArgoCD가 다시 만들어버린다 (selfHeal이 켜져 있는 경우). 이걸 모르고 “왜 삭제가 안 되지?” 하고 세 번 시도한 뒤에야 깨달았다. 리소스를 제거하려면 Git에서 manifest를 지우고 sync해야 한다. 당연한 얘기지만, GitOps를 처음 도입할 때는 이 사고방식의 전환이 생각보다 쉽지 않다. 팀원들한테도 이 부분을 설명하는 데 시간이 좀 걸렸다.
Helm을 쓴다면 — ArgoCD의 Helm 지원은 꽤 쓸 만하다. 그런데 values를 environment별로 다르게 가져가고 싶어서 values file을 별도 repo에 두는 방식을 시도했다가 multiple source 설정이 생각보다 복잡해서 결국 포기하고, values file을 manifest repo에 같이 두는 방식으로 단순하게 해결했다. ArgoCD 2.6부터 multiple source를 지원하기 시작했는데, 나한테는 아직 복잡도 대비 이득이 크지 않다.
팀에 도입할 만한가
나는 “그렇다”고 말하겠다. 조건 없이.
우리 팀 기준을 말하면 — 5명, Kubernetes 클러스터 2개 (Staging, Production), 서비스 12개다. 이 규모에서 ArgoCD 도입 후 배포 관련 사고가 눈에 띄게 줄었다. 정확히는, “누가 뭘 언제 배포했는지 모르는 상황”이 사라진 게 가장 크다. ArgoCD UI의 sync history를 보면 언제 어떤 Git commit이 배포됐는지 바로 보인다.
롤백도 편하다. 이전 commit으로 Application의 targetRevision을 바꾸고 sync하면 된다. kubectl rollout undo보다 명확하다 — 어떤 상태로 돌아가는지 Git에서 눈으로 확인할 수 있으니까.
단점도 있다. 학습 곡선이 있고, App of Apps 패턴이나 멀티 클러스터 설정으로 가면 복잡도가 상당히 올라간다. ArgoCD 자체도 결국 클러스터에 올라가는 서비스라, 고가용성 설정을 안 하면 ArgoCD가 장애나는 순간 배포 파이프라인 전체가 막힌다 — 물론 이미 배포된 서비스들은 계속 돌아가지만.
100명 이상 규모에서도 이 방식이 최선인지는 솔직히 잘 모르겠다. Flux CD를 써본 사람들 얘기를 들어보면 그쪽도 나름의 장점이 있다고 하는데, 직접 충분히 써보지 않아서 비교하기 어렵다.
그래도 내 권고는 명확하다: Kubernetes를 쓰고 있다면 ArgoCD를 도입해라. 처음 설정에 반나절 정도 투자하면, 이후 배포 스트레스가 확실히 줄어든다. 금요일 오후에도 배포할 수 있게 됐다 — 뭐, 금요일 배포 자체를 피하는 게 더 현명하긴 하지만.