
ArgoCD와 GitOps: Git에 푸시하면 쿠버네티스가 알아서 배포한다
kubectl apply를 수동으로 치는 것은 FTP 시대와 다를 바 없다. Git 저장소를 단일 진실의 원천으로 삼고, ArgoCD가 쿠버네티스 클러스터를 자동으로 동기화하는 GitOps 패러다임의 모든 것을 풀어본다.

kubectl apply를 수동으로 치는 것은 FTP 시대와 다를 바 없다. Git 저장소를 단일 진실의 원천으로 삼고, ArgoCD가 쿠버네티스 클러스터를 자동으로 동기화하는 GitOps 패러다임의 모든 것을 풀어본다.
2000년대 초반, 웹사이트를 배포하는 방법은 단순했다. FTP 클라이언트를 열고, 파일을 끌어다 서버에 올린다. 누가, 언제, 무엇을 올렸는지는 아무도 몰랐다. 실수로 잘못된 파일을 올리면? 되돌리는 방법도 없었다.
20년이 지난 2026년, 쿠버네티스가 인프라의 표준이 되었다. 그런데 배포 방식을 가만히 들여다보면 — FTP 시대와 비슷한 패턴이 보인다.
# 누군가의 노트북에서...
kubectl apply -f deployment.yaml
kubectl set image deployment/my-app my-app=my-registry/my-app:v1.2.3
이것이 문제다:
수동 kubectl apply의 세계에서는 클러스터의 현재 상태가 곧 진실이다. 그런데 그 상태가 어떻게 만들어졌는지는 아무도 모른다. 클러스터에 직접 접근할 수 있는 사람 누구나 상태를 바꿀 수 있고, 그 변경은 어디에도 기록되지 않는다.
GitOps는 이 문제의 근본적인 해결책이다. 그리고 ArgoCD는 GitOps를 쿠버네티스에서 실현하는 가장 널리 쓰이는 도구다.

2017년, 쿠버네티스 모니터링 회사 Weaveworks의 CEO Alexis Richardson이 "GitOps"라는 용어를 만들었다. 블로그 포스트 "GitOps — Operations by Pull Request"에서 제안한 개념은 단순했다:
"운영 환경의 원하는 상태(desired state)를 Git에 선언적으로 정의하고, 자동화된 에이전트가 실제 상태(actual state)를 원하는 상태에 맞춰 수렴시킨다."
전통적 배포는 명령형(imperative) — "이 이미지를 이 서버에 배포해라"라는 명령이다. GitOps는 선언형(declarative) — "이 시스템은 이런 상태여야 한다"고 정의하면, 도구가 알아서 그 상태를 만든다.
OpenGitOps 프로젝트(CNCF 샌드박스)가 정의한 공식 원칙이다:
핵심은 하나: Git이 단일 진실의 원천(Single Source of Truth) 이다.
| 문제 | 전통적 배포 | GitOps |
|---|---|---|
| 감사 추적 | "누가 변경했지?" | Git 커밋 로그에 전부 기록 |
| 롤백 | "이전 상태가 뭐였지?" | git revert로 즉시 복원 |
| 환경 일관성 | 직접 접근으로 드리프트 발생 | 자동 조정으로 드리프트 차단 |
| 코드 리뷰 | 배포 스크립트를 누가 리뷰? | PR/MR로 인프라 변경도 리뷰 |
| 접근 제어 | kubectl 권한 = 배포 권한 | Git 권한 = 배포 권한 |
전통적 CI/CD는 Push 기반이다. Jenkins, GitHub Actions 같은 CI 서버가 빌드·테스트 후 직접 클러스터에 밀어 넣는다.
문제점:
GitOps의 Pull 기반은 방향이 반대다. 클러스터 안의 에이전트가 Git을 주기적으로 감시하고, 변경을 스스로 당겨온다(pull).
장점:
kubeconfig를 가져야 한다. CI가 뚫리면 운영 클러스터에 무엇이든 배포할 수 있다. Pull 기반은 클러스터 밖→안 접근이 없으므로 공격 표면이 대폭 줄어든다.2018년, Applatix(이후 Intuit에 인수)의 엔지니어들이 ArgoCD를 만들었다. 미국 최대 세무 소프트웨어 기업 Intuit은 수천 개의 마이크로서비스를 쿠버네티스에서 운영하고 있었고, 안정적 GitOps 배포 도구가 필요했다.
CNCF Graduated 프로젝트(2022년 12월 승격). Kubernetes, Prometheus, Envoy와 같은 최고 성숙도 등급이다.
ArgoCD는 쿠버네티스 클러스터 안에서 동작하는 컴포넌트들로 구성된다.
Application Controller가 핵심이다. K8s 컨트롤러 패턴을 그대로 따른다 — 관찰(Observe) → 비교(Diff) → 행동(Act) 루프를 무한 반복.
ArgoCD의 가장 기본 단위. "이 Git 경로의 매니페스트를 이 클러스터 네임스페이스에 배포하라"를 정의한다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-nextjs-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/k8s-manifests.git
targetRevision: main
path: apps/my-nextjs-app
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Git에서 삭제된 리소스를 클러스터에서도 삭제
selfHeal: true # 직접 변경을 Git 상태로 되돌림
Application들을 논리적으로 그룹화하고 접근 제어를 적용하는 경계다.
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-frontend
namespace: argocd
spec:
sourceRepos:
- 'https://github.com/my-org/frontend-*'
destinations:
- namespace: 'frontend-*'
server: https://kubernetes.default.svc
clusterResourceWhitelist: [] # 클러스터 수준 리소스 배포 불가
이렇게 하면 프론트엔드 팀은 자기 네임스페이스에만 배포할 수 있다. ClusterRole이나 PersistentVolume은 건드리지 못한다.
| 상태 종류 | 정상 | 진행 중 | 문제 |
|---|---|---|---|
| Sync Status | Synced — Git = 클러스터 | Syncing — 동기화 중 | OutOfSync — Git ≠ 클러스터 |
| Health Status | Healthy — 모든 Pod Ready | Progressing — 롤링 업데이트 중 | Degraded — CrashLoopBackOff 등 |
ArgoCD 대시보드에서 Synced + Healthy가 우리가 원하는 상태다.

OutOfSync 표시. 자동 동기화 설정 시 kubectl apply를 대신 실행. 수동 모드라면 사용자가 Sync 버튼을 누를 때까지 대기.이 루프는 무한 반복된다. GitOps 원칙 4번 "지속 조정"의 구현이다.
쿠버네티스 컨트롤러들이 리소스에 status, metadata.generation 등 필드를 자동 추가한다. 2-way diff만 하면 이런 자동 필드 때문에 항상 OutOfSync로 나온다. 3-way diff는 Git 원하는 상태 + 클러스터 실제 상태 + 마지막 적용 상태(last-applied-configuration 어노테이션) 를 비교하여 "사용자 의도적 변경"과 "시스템 자동 변경"을 구분한다.
ArgoCD Application 자체가 K8s CRD이므로, Application을 관리하는 Application을 만들 수 있다.
Root Application이 Git의 apps/ 디렉토리를 감시하고, 각 Application YAML을 자동 관리. 새 서비스 추가는 Application YAML 하나를 Git에 Push하면 끝.
App of Apps보다 강력한 ApplicationSet은 템플릿 기반으로 여러 Application을 자동 생성한다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-app-all-clusters
spec:
generators:
- clusters:
selector:
matchLabels:
env: production
template:
metadata:
name: 'my-app-{{name}}'
spec:
source:
repoURL: https://github.com/my-org/k8s-manifests.git
path: 'apps/my-app/overlays/{{metadata.labels.region}}'
destination:
server: '{{server}}'
namespace: production
env: production 레이블이 있는 모든 클러스터에 자동으로 Application 생성. 서울, 도쿄, 미국 — 클러스터를 추가하면 ApplicationSet이 감지하고 배포한다.
| 항목 | ArgoCD | Flux | Jenkins X |
|---|---|---|---|
| CNCF 등급 | Graduated | Graduated | 없음 |
| Web UI | 강력한 내장 UI | 없음 (별도 설치) | 기본 UI |
| 아키텍처 | 중앙 집중형 | 분산형 (컨트롤러 조합) | 중앙 집중형 |
| 멀티 클러스터 | 네이티브 허브-스포크 | 클러스터별 설치 | 제한적 |
| RBAC | 세밀한 자체 RBAC | K8s RBAC 활용 | 기본 수준 |
| 채택률 (2025) | ~70% | ~25% | 소수 |
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
name: CI
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build & Push Image
run: |
docker build -t ghcr.io/my-org/my-app:${{ github.sha }} .
docker push ghcr.io/my-org/my-app:${{ github.sha }}
- name: Update K8s Manifests
run: |
git clone https://github.com/my-org/k8s-manifests.git
cd k8s-manifests/apps/my-app/base
kustomize edit set image ghcr.io/my-org/my-app=ghcr.io/my-org/my-app:${{ github.sha }}
git commit -am "chore: update image to ${{ github.sha }}" && git push
CI의 역할은 여기서 끝. 매니페스트 저장소에 커밋이 Push되면 ArgoCD가 나머지를 처리한다.
argocd app create my-nextjs-app \
--repo https://github.com/my-org/k8s-manifests.git \
--path apps/my-nextjs-app/overlays/production \
--dest-server https://kubernetes.default.svc \
--dest-namespace production \
--sync-policy automated \
--auto-prune --self-heal
main에 PushOutOfSync개발자는 코드를 Push한 것 외에 아무것도 하지 않았다. kubectl을 치지 않았고, 클러스터에 접근하지도 않았다.

각 환경별 Kustomize 오버레이로 차이를 관리한다:
replicas: 1, dev-latest 이미지, 리소스 제한 낮음replicas: 2, 테스트된 이미지 태그replicas: 5, 검증된 특정 버전, 리소스 제한 높음| dev | staging | production |
|---|---|---|
| 자동 Sync | 자동 Sync + PR 승인 | 수동 Sync (automated: null) |
| Push 즉시 배포 | 이미지 태그 업데이트는 PR로 | PR 리뷰 + UI에서 Sync 클릭 |
production에서 automated: null은 Git에 변경이 있어도 자동 배포하지 않는다. OutOfSync 확인 후 팀 합의 → 수동 Sync. 이것이 안전장치다.
DB 마이그레이션이 앱보다 먼저 실행되어야 할 때:
# Wave -1: DB 마이그레이션 (먼저 실행)
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-1"
argocd.argoproj.io/hook: PreSync
# Wave 0: 앱 Deployment (마이그레이션 완료 후)
metadata:
annotations:
argocd.argoproj.io/sync-wave: "0"
Wave 숫자가 작은 리소스부터 순서대로 적용. Wave -1 성공 후 Wave 0이 시작된다.
# 프론트엔드 팀: 자기 프로젝트 앱 조회/동기화만 허용
p, role:frontend-dev, applications, get, team-frontend/*, allow
p, role:frontend-dev, applications, sync, team-frontend/*, allow
p, role:frontend-dev, applications, override, team-frontend/*, deny
# 플랫폼 팀: 모든 권한
p, role:platform-admin, applications, *, */*, allow
p, role:platform-admin, clusters, *, *, allow
GitHub, Okta, Azure AD 등과 OIDC/SAML로 연동. GitHub 팀 구조가 ArgoCD RBAC 그룹에 매핑되어, 새 팀원이 GitHub에 추가되면 ArgoCD 권한도 자동 부여.
Git에 평문 시크릿을 넣을 수 없다. 주요 대안:
이 글에서 다룬 모든 내용은 하나의 문장으로 수렴한다:
"Git 저장소의 상태가 곧 인프라의 상태여야 한다."
git revert. 클러스터 상태는 Git이 말해준다. 개발자는 코드만 Push하면 된다.ArgoCD를 시작하고 싶다면, minikube로 로컬 클러스터를 띄우고 ArgoCD를 설치한 뒤, 간단한 nginx 앱을 Git에 올려 배포해 보라. 처음 대시보드에서 Synced + Healthy 초록색 상태를 보는 순간, FTP 시대로 돌아가고 싶지 않을 것이다.