목차
- 왜 선언적 GitOps로 넘어가는가
- 기존 접근의 한계: Jenkins만으로 부족했던 이유
- Helm으로 ArgoCD 설치하기
- GitHub 프라이빗 레포 연동
- Application CRD와 자동 Sync 정책
- 검증: 드리프트가 실제로 막히는가
- 한계와 주의점
- 지금 해볼 것과 개인 의견
ArgoCD GitOps 설정은 Git 저장소를 유일한 진실 공급원(Source of Truth)으로 두고, 클러스터 상태를 거기에 맞춰 자동으로 동기화하는 체계를 세우는 일이다. 단순히 컨트롤러 하나 띄우는 작업이 아니라, 배포라는 행위를 "누가 kubectl apply를 실행했는가"에서 "Git 커밋이 병합되었는가"로 옮기는 관점 전환에 가깝다.
프론트엔드에서 백엔드로 넘어온 지 2년차, 처음 떠안은 과제 중 하나가 바로 이 파이프라인 정리였다. React 프로젝트에서 Vercel이 알아서 해주던 "PR 머지 → 자동 배포"의 매끄러움을 쿠버네티스에서도 재현하고 싶었다. 그런데 팀에 남아 있던 건 Jenkins Freestyle 잡 7개와, 누가 언제 수정했는지 아무도 모르는 deploy.sh 스크립트 한 뭉치였다.
왜 선언적 GitOps로 넘어가는가
게다가, 배포 파이프라인을 새로 짤 때 가장 먼저 세워야 하는 질문은 "지금 클러스터에 실제로 떠 있는 것이 무엇인가"에 답할 수 있는가다. 기존 Jenkins 기반 구조에서는 이 답이 늘 흐릿했다. 스크립트가 kubectl set image를 직접 때렸고, 긴급 패치가 터지면 엔지니어가 수동으로 edit을 걸었다. Git에 있는 매니페스트와 클러스터 실제 상태가 어긋나는 "드리프트"는 시간 문제였다.
게다가, GitOps는 이 문제를 정의부터 다르게 잡는다. CNCF의 OpenGitOps 원칙(v1.0.0) 중 첫 항목이 "선언적(Declarative)"이고, 두 번째가 "버전 관리되고 불변(Versioned and Immutable)"이다. (출처: OpenGitOps Principles v1.0.0) 즉 클러스터가 떠 있어야 할 상태 자체를 YAML로 선언하고, Git이 그 기록을 책임지고, 컨트롤러가 간극을 계속 조정하는 구조다.
물론, ArgoCD는 Argo 프로젝트군의 CD 컴포넌트이고, 2022년 CNCF Graduated 프로젝트로 올라선 이후 꾸준히 업데이트되어 작성 시점(2026년 4월 22일) 기준 v2.13 계열이 안정 릴리스다. (출처: argoproj/argo-cd GitHub Releases)
전환자 시각에서 본 비교 지점
따라서, 프론트엔드에서는 "빌드 산출물을 어디에 배포하는가"가 핵심이었다. CDN이든 S3든 배포 대상이 문자열 하나로 명료했다. 백엔드, 특히 k8s로 오면 배포 대상이 "클러스터의 원하는 상태"라는 추상 개념으로 바뀐다. ArgoCD가 이 추상을 Git이라는 익숙한 도구로 다시 끌어오는 방식이 전환자 입장에서는 가장 직관적이었다. 프론트에서 vercel.json을 읽듯, 백엔드에서 Application.yaml을 읽는 셈이다.
기존 접근의 한계: Jenkins만으로 부족했던 이유
특히, Jenkins에서 kubectl 명령을 때려도 배포는 된다. 동작한다. 운영 규모가 커질수록 아래 세 가지가 발목을 잡을 뿐이다.
첫째, 권한이 Jenkins 에이전트에 집중된다. 사람마다 kubeconfig를 나눠 갖지 않고, Jenkins 노드가 클러스터 전권을 쥔 상태에서 모든 배포가 일어난다. 감사 로그는 Jenkins 빌드 히스토리에만 남고, k8s 이벤트와 매핑이 안 된다.
한편, 둘째, 클러스터 상태의 "현재"를 묻는 도구가 따로 필요하다. kubectl diff를 매번 돌려야 하고, 그것도 네임스페이스별로 일일이 확인해야 한다. 스크립트로 감싸다 보면 스크립트의 스크립트가 생긴다.
그런데, 셋째, 롤백이 "이전 파이프라인을 다시 돌린다"는 형태라 느리다. Git revert 한 번으로 끝나지 않고, 빌드 아티팩트 버전부터 되짚어야 한다.
이처럼, :::tip Jenkins를 지우자는 뜻이 아니다. CI는 Jenkins나 GitHub Actions로 그대로 두고, CD만 ArgoCD로 분리하는 구성이 현실적이다. 이 분리가 GitOps에서 말하는 "빌드 파이프라인 ≠ 배포 파이프라인"이다. :::
Helm으로 ArgoCD 설치하기
차트 추가와 기본 설치
공식 Helm 차트(argo/argo-cd)로 설치했다. 2026-04-22 기준 차트 7.7.x 버전이 앱 v2.13 계열을 가져온다.
# Helm 레포 등록
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
# 전용 네임스페이스에 설치
kubectl create namespace argocd
helm install argocd argo/argo-cd \
--namespace argocd \
--version 7.7.14 \
--values values.yaml
values.yaml은 처음부터 크게 잡지 말고, 딱 세 가지만 건드리는 게 낫다. 도메인, Ingress 설정, 그리고 reconciliation 타임아웃이다.
# values.yaml (발췌)
global:
domain: argocd.example.internal
server:
ingress:
enabled: true
ingressClassName: nginx
annotations:
# TLS는 cert-manager에 위임
cert-manager.io/cluster-issuer: letsencrypt-prod
configs:
params:
server.insecure: "false"
cm:
# 타임아웃을 넉넉히 잡으면 대형 차트에서 안전하다
timeout.reconciliation: 180s
초기 admin 비밀번호 바꾸기
그래서, 설치 직후 admin 비밀번호는 argocd-initial-admin-secret에 평문으로 들어 있다. 운영 클러스터라면 즉시 바꾸고, Secret 자체는 삭제해야 한다. (출처: ArgoCD 공식 Getting Started v2.13)
# 초기 비밀번호 꺼내기
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
# CLI로 로그인 후 변경
argocd login argocd.example.internal
argocd account update-password
# 원본 Secret 삭제
kubectl -n argocd delete secret argocd-initial-admin-secret
UI 접근과 SSO 준비
Ingress가 뜨면 브라우저에서 바로 접속된다. 팀 규모가 커지면 로컬 admin 하나로는 금방 한계가 온다. Dex 연동으로 GitHub OAuth를 붙이거나, OIDC로 기존 IdP와 묶는 방향을 빠르게 준비해두는 편이 낫다. 초기에 admin만 돌려쓰다가 나중에 감사 로그 뒤집어쓰는 경우가 실제로 자주 관찰된다.
GitHub 프라이빗 레포 연동
실제로, 공개 레포라면 URL만 넣어도 된다. 실무에서는 거의 전부 프라이빗이다. 인증 수단은 크게 세 가지다.
| 방식 | 장점 | 단점 | 추천 상황 |
|---|---|---|---|
| Deploy Key (SSH) | 레포 단위 격리, 만료 없음 | 레포마다 키 생성 필요 | 레포 수 10개 이하 |
| GitHub App | Org 단위 권한, 감사 로그 | 초기 설정 복잡 | 레포 많고 Org 관리 필요 |
| PAT (Personal Access Token) | 가장 간단 | 개인 계정에 묶임, 만료 관리 필요 | 단기 테스트용 |
또한, 소규모 팀이면 Deploy Key, 중규모 이상이면 GitHub App으로 가는 흐름이 자연스럽다. PAT은 개인 계정 이슈가 곧 장애로 번지니 운영에 두지 말자.
특히, Deploy Key 등록은 Repository CRD로 처리하는 게 깔끔하다.
apiVersion: v1
kind: Secret
metadata:
name: repo-k8s-manifests
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: git@github.com:my-org/k8s-manifests.git
sshPrivateKey: |
-----BEGIN OPENSSH PRIVATE KEY-----
# Deploy Key 개인키
-----END OPENSSH PRIVATE KEY-----
주의할 점은 라벨이다. argocd.argoproj.io/secret-type: repository가 붙어야 ArgoCD가 이 Secret을 레포로 인식한다. 이 라벨을 빼먹어서 UI에 레포가 안 뜨는 시행착오를 한 번 겪었다. (여담이지만 공식 문서에 분명히 적혀 있는데, 설치 직후에는 꼭 이런 작은 필드를 놓친다.)
Application CRD와 자동 Sync 정책
또한, 여기가 사실상 ArgoCD GitOps 설정의 본체다. Application 하나가 "어떤 Git 경로를, 어떤 클러스터의 어떤 네임스페이스에, 어떻게 동기화할지"를 선언한다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-api
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: git@github.com:my-org/k8s-manifests.git
targetRevision: main
path: apps/payment-api/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: payment
syncPolicy:
automated:
prune: true # Git에서 지운 리소스는 클러스터에서도 삭제
selfHeal: true # 수동 변경을 자동 복구
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- ServerSideApply=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
prune과 selfHeal의 차이
즉, 둘을 혼동하기 쉽다. prune은 Git에서 사라진 리소스를 클러스터에서도 지우는 동작이다. selfHeal은 클러스터에서 누가 손으로 바꾼 값을 Git 기준으로 되돌리는 동작이다. 방향이 다르다.
처음에는 selfHeal을 꺼둬야 한다는 의견이 있고, 켜야 한다는 의견도 있다. 운영 환경이라면 켜는 쪽이 드리프트 방어에 확실히 유리하게 보인다. 다만 급한 장애 대응 중에 핫픽스를 클러스터에 직접 꽂아야 할 때 selfHeal이 켜져 있으면 변경이 수 초 만에 되돌아간다. 이 타이밍을 한 번 겪고 나서는 네임스페이스별로 정책을 나누는 방식으로 바꿨다. prod는 selfHeal true, staging은 false로 둔다.
재시도와 백오프
retry 블록은 의외로 자주 무시된다. 초기 배포가 네트워크 이유로 실패하거나, 의존 리소스가 아직 뜨지 않았을 때 지수 백오프로 재시도해주는 옵션이다. 기본값은 재시도 없음이라, 한 번 실패하면 수동 Sync를 눌러야 한다.
ServerSideApply를 켜는 이유
그런데, ArgoCD 기본은 client-side apply다. HPA가 replicas 필드를 동적으로 조정하거나, Istio가 VirtualService에 주석을 다는 환경에서는 필드 소유권이 꼬인다. ServerSideApply=true를 syncOption에 넣으면 필드 관리자가 명시되어 충돌 감지가 훨씬 선명해진다.
검증: 드리프트가 실제로 막히는가
구성을 마친 뒤 가장 먼저 확인한 건 "selfHeal이 약속한 일을 정말 하는가"였다. 간단하게 재현해봤다.
# Deployment의 replicas를 손으로 바꿈
kubectl -n payment scale deploy/payment-api --replicas=5
# ArgoCD가 감지하는지 확인
argocd app get payment-api
# STATUS: OutOfSync → 수 초 뒤 Synced로 복구
# replicas: 5 → 3 (Git 기준)
체감상 10초 안에 원복된다. reconciliation timeout을 180초로 잡았는데도 변경 감지 자체는 훨씬 빠른 편이다. Application Controller가 3분마다 전체 리소스를 비교하는 주기와 별개로, 리소스 이벤트를 실시간으로 받아 처리하기 때문으로 보인다.
ConfigMap을 kubectl edit으로 고친 경우도 마찬가지로 복구된다. ServerSideApply를 켜두면 필드 소유권이 명확해져서 "Synced이긴 한데 값이 다른" 애매한 상태가 사라진다. ServerSideApply 없이 테스트했을 때는 간헐적으로 OutOfSync 깜빡임이 남았다.
한계와 주의점
실제로, 완벽한 도구는 없다. ArgoCD도 몇 가지 지점에서는 부가 장치가 필요하다.
첫째, Secret 관리가 과제로 남는다. Git에 평문 Secret을 올릴 수는 없으니, Sealed Secrets, External Secrets Operator, SOPS 중 하나와 결합해야 한다. 팀에서는 External Secrets Operator + AWS Secrets Manager 조합을 쓰고 있다. 초기 진입 비용은 있지만 시크릿 로테이션이 자동화되는 이점이 크다.
물론, 둘째, 수십 개 Application을 수동으로 만들면 금방 지친다. ApplicationSet이라는 상위 CRD가 이 문제를 푼다. Git 디렉터리, 클러스터 목록, PR 등을 기반으로 Application을 자동 생성해주는데, 이 부분은 아직 깊게 안 파봤다. 현재 30개 남짓 Application을 직접 YAML로 관리 중인 상태로 운영하고 있고, 50개를 넘어가면 ApplicationSet 도입을 검토할 생각이다.
그 외에도, 셋째, 롤백 전략이 Git 중심이라 "지금 당장 3분 안에 되돌려야 한다"는 상황에서는 Git revert + PR + 승인 흐름이 번거롭게 느껴질 수 있다. 긴급 롤백용 argocd app rollback 명령이 따로 있긴 하다. 이건 Git과 클러스터 상태가 어긋나는 행위라 사내 규칙을 정해두고 써야 한다.
지금 해볼 것과 개인 의견
- 스테이징 클러스터에 Helm으로 ArgoCD v2.13을 띄우고 admin 비밀번호부터 바꿔라.
- 기존 매니페스트 레포 하나를 Deploy Key로 연결해서 Application 한 개만 먼저 Sync 걸어봐라.
- selfHeal은 staging에서 먼저 켜고, replicas를 수동으로 바꿔 되돌아오는 시간을 측정해봐라.
한편, 개인적으로는 소규모 팀에서도 CI와 CD를 분리하는 첫 도구로 ArgoCD를 먼저 도입하는 편이 다른 어떤 리팩토링보다 배포 체감 품질을 크게 올려준다고 본다.
관련 글
- ArgoCD로 Kubernetes GitOps 배포 자동화 — Helm 연동부터 롤백까지 실전 구성 – kubectl apply 스크립트를 ArgoCD GitOps로 전환하면서 겪은 일들을 정리했다. Helm Chart 연동, 자동 동기화 설…
- EKS 비용 최적화 실전: Karpenter, Spot, 리소스 요청 3단계 비교 – EKS 월 청구서가 세 배 가까이 뛴 뒤 Karpenter 전환, Spot 혼합, 리소스 요청 튜닝을 3단계로 적용해 약 40% 줄인 기록…
- OpenSSL 4.0.0 업그레이드 회고 — 올렸다가 롤백하고 배운 것들 – OpenSSL 4.0.0이 나오자마자 스테이징에 올려봤다. 결과는 롤백이었다. 프론트엔드 출신 백엔드 개발자가 시스템 라이브러리 메이저 업…