운영 클러스터 상태가 Git 저장소와 달라졌지만 누가 언제 수동으로 바꿨는지 찾기 어렵습니다. 배포 이력이 명령어 기록에만 남으면 롤백과 감사가 모두 불안정해집니다. GitOps와 Argo CD는 Git을 기준 상태로 삼아 클러스터 변경을 추적하고 동기화합니다.
GitOps와 ArgoCD
배포 파이프라인이 복잡해질수록 "지금 프로덕션에 떠 있는 버전이 정확히 무엇인가"라는 질문에 답하기 어려워집니다. CI/CD에서 누군가 kubectl 명령을 잘못 실행했거나, 긴급 패치를 적용했거나, 설정을 직접 수정했다면 Git의 코드와 실제 클러스터 상태가 달라집니다. GitOps는 이 문제를 원천 차단하는 운영 패러다임입니다. Git 리포지토리의 선언적 매니페스트가 클러스터의 유일한 진실이 되고, 클러스터를 변경하는 유일한 방법은 Git 커밋입니다. ArgoCD는 이 원칙을 자동화하는 도구로, 60초마다 Git 상태를 폴링해서 클러스터와 차이가 생기면 자동으로 동기화합니다. 롤백은 git revert 한 줄이고, 배포 이력은 곧 커밋 이력입니다. 이 모듈을 마치면 ArgoCD를 설치하고 Application을 정의해서 Git 커밋만으로 배포가 자동화되는 파이프라인을 구성할 수 있습니다.
- 1GitOps 4대 원칙: 선언적, 버전 관리, 자동 적용, 지속적 조정
- 2ArgoCD 설치 및 Application 정의 (source + destination + syncPolicy)
- 3자동 동기화, prune, selfHeal 설정으로 드리프트 자동 교정
- 4ApplicationSet으로 멀티환경/멀티클러스터 배포 자동화
- 5배포 전략: Sync Waves와 Resource Hooks로 순서 제어
- 6TroubleCase: Sync 실패 — 매니페스트 오류와 RBAC 권한 문제
kubectl create namespace argocd && kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yamlkubectl wait --for=condition=available --timeout=300s deployment/argocd-server -n argocdcurl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 && chmod +x argocd && mv argocd /usr/local/bin/kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -dkubectl port-forward svc/argocd-server -n argocd 8080:443GitOps 4대 원칙
GitOps 운영 흐름:
개발자 Git 리포지토리 ArgoCD Kubernetes
│ │ │ │
│── git commit ─────────▶│ │ │
│ (image tag 변경) │ │ │
│ │── 폴링 (60초마다) ──▶│ │
│ │ │── 차이 감지 │
│ │ │── kubectl apply ─▶│
│ │ │ │
│ │ (누군가 직접 kubectl edit 실행) │
│ │ │◀── 드리프트 감지 │
│ │ │── selfHeal ──────▶│
│ │ │ (Git 상태로 복원)│
4대 원칙:
- 선언적(Declarative): 시스템 상태를 명령어가 아닌 YAML로 선언
- 버전 관리(Versioned): 모든 변경은 Git 커밋으로 기록 — 누가, 언제, 무엇을 변경했는지 추적 가능
- 자동 적용(Automatically Applied): 승인된 변경(Git 커밋)은 자동으로 클러스터에 적용
- 지속적 조정(Continuously Reconciled): ArgoCD가 주기적으로 Git 상태와 클러스터 상태를 비교하고 차이를 교정
GitOps vs 기존 Push 기반 CD — 보안과 운영 관점 차이
기존 CI/CD는 Jenkins나 GitHub Actions가 kubectl 명령을 직접 실행합니다. 이 방식에서 CI 서버는 클러스터 접근 권한(KUBECONFIG)을 보유해야 합니다. CI 서버가 해킹되면 클러스터 전체가 위험에 노출됩니다.

기존 Push 방식 (취약점 존재):
CI 서버 ──[KUBECONFIG 보유]──▶ kubectl apply → Kubernetes
(CI 서버 탈취 시 클러스터 무방비)
GitOps Pull 방식 (ArgoCD):
Git ◀── ArgoCD (클러스터 내부에서 Git 폴링)
ArgoCD는 클러스터 내부 서비스어카운트로 동작
외부에서 클러스터 접근 권한 불필요
ArgoCD는 클러스터 내부에서 실행되며 Git을 폴링합니다. 외부 시스템이 클러스터에 접근하는 것이 아니라, 클러스터가 Git에서 상태를 가져오는 Pull 방식입니다. CI/CD 서버에 클러스터 자격증명을 저장할 필요가 없어 공격 표면이 줄어듭니다.
또한 Git 리포지토리 접근 권한만 있으면 배포 이력 확인, 롤백, 특정 버전 재배포가 가능합니다. 별도의 배포 도구 권한 관리가 필요 없습니다.
ArgoCD Application 정의
기본 Application YAML
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-service
namespace: argocd # Application 객체는 argocd 네임스페이스에 생성
labels:
environment: production
team: backend
spec:
project: default
# 소스: Git 리포지토리에서 읽을 매니페스트
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main # 브랜치, 태그, 커밋 SHA 모두 가능
path: apps/payment/production # 리포지토리 내 경로
# 목적지: 어느 클러스터, 어느 네임스페이스에 배포할지
destination:
server: https://kubernetes.default.svc # 현재 클러스터
namespace: production
# 동기화 정책
syncPolicy:
automated:
prune: true # Git에서 삭제된 리소스 자동 제거
selfHeal: true # 수동 변경 감지 시 자동 복원
syncOptions:
- CreateNamespace=true # 네임스페이스 없으면 자동 생성
- PrunePropagationPolicy=foreground # 종속 리소스 먼저 삭제
- RespectIgnoreDifferences=true
retry:
limit: 5 # 실패 시 재시도 횟수
backoff:
duration: 5s
factor: 2
maxDuration: 3m
# 비교 시 무시할 필드 (Kubernetes 자동 추가 필드)
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # HPA가 레플리카를 변경해도 OutOfSync로 표시 안 함
Helm Chart Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: nginx-ingress
namespace: argocd
spec:
project: default
source:
repoURL: https://kubernetes.github.io/ingress-nginx
chart: ingress-nginx
targetRevision: 4.8.3 # Chart 버전 고정
helm:
releaseName: ingress-nginx
values: | # 인라인 values (또는 valuesFiles 사용)
controller:
replicaCount: 2
service:
type: LoadBalancer
metrics:
enabled: true
destination:
server: https://kubernetes.default.svc
namespace: ingress-nginx
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
ArgoCD CLI로 Application 관리
# CLI 로그인
argocd login localhost:8080 \
--username admin \
--password $(kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d) \
--insecure
# Application 목록
argocd app list
# 특정 Application 상태 확인
argocd app get payment-service
# 수동 동기화 (즉시 반영)
argocd app sync payment-service
# 특정 커밋으로 동기화 (rollback)
argocd app sync payment-service --revision abc1234
# Git과 클러스터 차이 확인
argocd app diff payment-service
# 롤백 (이전 성공 배포로)
argocd app rollback payment-service
# Application 삭제 (리소스도 함께 삭제)
argocd app delete payment-service --cascade
ApplicationSet으로 멀티환경 배포
# 환경 목록 기반 ApplicationSet
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: payment-service-all-envs
namespace: argocd
spec:
generators:
# List 생성기: 환경 목록을 직접 정의
- list:
elements:
- environment: dev
cluster: https://dev-cluster.example.com
namespace: payment-dev
replicaCount: "1"
imageTag: latest
- environment: staging
cluster: https://staging-cluster.example.com
namespace: payment-staging
replicaCount: "2"
imageTag: "{{ .values.stagingTag }}"
- environment: production
cluster: https://prod-cluster.example.com
namespace: payment-production
replicaCount: "5"
imageTag: "1.2.3"
template:
metadata:
name: "payment-{{ environment }}" # 각 환경별 Application 이름
labels:
environment: "{{ environment }}"
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: "apps/payment/{{ environment }}"
helm:
parameters:
- name: replicaCount
value: "{{ replicaCount }}"
- name: image.tag
value: "{{ imageTag }}"
destination:
server: "{{ cluster }}"
namespace: "{{ namespace }}"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
# ApplicationSet 적용 후 생성된 Application 확인
kubectl apply -f applicationset.yaml -n argocd
argocd app list | grep payment
# NAME CLUSTER NAMESPACE
# payment-dev https://dev-cluster.example.com payment-dev
# payment-staging https://staging-cluster.example.com payment-staging
# payment-production https://prod-cluster.example.com payment-production
- NAME—조회 대상 리소스 이름이 예상한 대상과 일치하는지 확인합니다.
- STATUS/READY—Running, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
- RESTARTS/EVENTS—재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.
Git 디렉토리 기반 ApplicationSet
# apps/ 디렉토리의 각 서브디렉토리를 자동으로 Application으로 생성
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: all-apps
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/myorg/k8s-manifests.git
revision: main
directories:
- path: "apps/*/production" # apps/payment/production, apps/order/production 등
template:
metadata:
name: "{{ path.basenameNormalized }}"
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: "{{ path }}"
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
Sync Waves와 Resource Hooks
배포 순서 제어가 필요할 때 사용합니다. 예를 들어 DB 마이그레이션을 앱 배포 전에 실행해야 하는 경우입니다.
# DB 마이그레이션 Job (wave: -1 → 앱보다 먼저 실행)
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
annotations:
argocd.argoproj.io/hook: Sync # Sync 단계에서 실행
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "-1" # 음수일수록 먼저 실행
spec:
template:
spec:
containers:
- name: migration
image: myapp:latest
command: ["python", "manage.py", "migrate"]
restartPolicy: Never
---
# 앱 Deployment (wave: 0 — 마이그레이션 성공 후 배포)
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
# ...
---
# 스모크 테스트 Job (wave: 1 → 앱 배포 후 실행)
apiVersion: batch/v1
kind: Job
metadata:
name: smoke-test
annotations:
argocd.argoproj.io/hook: PostSync # 배포 완료 후 실행
argocd.argoproj.io/hook-delete-policy: HookSucceeded
argocd.argoproj.io/sync-wave: "1"
spec:
template:
spec:
containers:
- name: test
image: curlimages/curl
command:
- sh
- -c
- "curl -f http://payment:8080/health || exit 1"
restartPolicy: Never
트러블슈팅
ArgoCD UI에서 Application이 SyncFailed 상태이고 에러 메시지가 failed to apply resource 또는 invalid spec으로 표시됩니다.
1단계: ArgoCD UI와 CLI에서 에러 상세 확인
# CLI에서 구체적인 에러 메시지 확인
argocd app get payment-service --show-operation
# 출력 예시:
# OPERATION: sync
# PHASE: Failed
# MESSAGE: one or more objects failed to apply, reason:
# Deployment.apps "payment" is invalid:
# spec.template.spec.containers[0].resources.requests:
# Invalid value: "2000m": must be less than or equal to cpu limit
# 전체 동기화 상세
argocd app sync payment-service --dry-run 2>&1
2단계: 매니페스트 로컬 검증
# Git에서 매니페스트 가져와서 로컬 검증
git clone https://github.com/myorg/k8s-manifests.git
cd k8s-manifests/apps/payment/production
# kubectl dry-run으로 유효성 확인
kubectl apply -f . --dry-run=server -n production
# YAML 문법 검증 (kubeval 사용)
kubeval deployment.yaml
# 일반 오류 패턴:
# - resource requests > limits (CPU/메모리)
# - 필수 필드 누락 (selector.matchLabels)
# - API 버전 오류 (deprecated API 사용)
# - 잘못된 imagePullPolicy 값
# deprecated API 확인
kubectl api-resources | grep <resource>
pluto detect-files -d . --output wide # pluto CLI 도구
3단계: RBAC 권한 문제 진단
# ArgoCD 서비스어카운트 확인
kubectl get sa -n argocd
# ArgoCD가 배포 대상 네임스페이스에 접근 권한이 있는지 확인
kubectl auth can-i create deployment \
--as=system:serviceaccount:argocd:argocd-application-controller \
-n production
# kubectl auth can-i list로 필요 권한 전체 확인
kubectl auth can-i '*' '*' \
--as=system:serviceaccount:argocd:argocd-application-controller \
-n production
# 출력: "no" 라면 ClusterRole 또는 RoleBinding 추가 필요
# ClusterRole 확인
kubectl get clusterrolebinding | grep argocd
kubectl describe clusterrolebinding argocd-application-controller
4단계: RBAC 권한 추가
# ArgoCD가 특정 네임스페이스에 배포할 수 있도록 권한 부여
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: argocd-deploy
namespace: production # 배포 대상 네임스페이스
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: admin # 또는 필요한 최소 권한으로 커스텀 Role
subjects:
- kind: ServiceAccount
name: argocd-application-controller
namespace: argocd
5단계: 개인 리포지토리 접근 오류
# Git 리포지토리 연결 상태 확인
argocd repo list
# STATUS TYPE NAME REPO
# Failed git https://github.com/myorg/private-manifests.git
# 리포지토리 자격증명 추가 (SSH 키)
argocd repo add git@github.com:myorg/private-manifests.git \
--ssh-private-key-path ~/.ssh/id_rsa
# 리포지토리 자격증명 추가 (토큰)
argocd repo add https://github.com/myorg/private-manifests.git \
--username myuser \
--password ghp_xxxxxxxxxxxx
# 연결 테스트
argocd repo get https://github.com/myorg/private-manifests.git
흔한 실수: OutOfSync가 되는 자동 추가 필드들
# Kubernetes가 자동으로 추가하는 필드들이 OutOfSync를 유발할 때
# ignoreDifferences로 무시 설정
kubectl apply -f - << 'EOF'
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-service
namespace: argocd
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # HPA가 변경하는 레플리카 수
- group: ""
kind: ConfigMap
jsonPointers:
- /data/last-applied # 자동 추가 어노테이션
- group: admissionregistration.k8s.io
kind: MutatingWebhookConfiguration
jsonPointers:
- /webhooks/0/clientConfig/caBundle # 자동 주입 CA 번들
EOF
시나리오: 프로덕션 롤백 — 잘못된 배포를 3분 안에 이전 버전으로 복구
배포 직후 에러율이 급등했습니다. 기존 CI/CD라면 이전 이미지 태그를 찾아 파이프라인을 다시 실행해야 합니다. GitOps + ArgoCD라면 Git 히스토리에서 이전 커밋을 찾아 revert하거나 ArgoCD 롤백 명령 하나로 복구됩니다.
# 방법 1: ArgoCD CLI 롤백 (가장 빠름 — 이전 성공 배포로 즉시 복원)
argocd app history payment-service
# ID DATE REVISION
# 42 2024-01-15 14:23:45 +0000 UTC main (abc1234) ← 정상
# 43 2024-01-15 15:10:02 +0000 UTC main (def5678) ← 문제 배포
argocd app rollback payment-service 42
# 즉시 이전 배포(커밋 abc1234) 상태로 복원
# 방법 2: Git revert (GitOps 원칙에 부합 — 변경 이력 보존)
git revert def5678 --no-edit
git push origin main
# ArgoCD가 60초 내 자동 감지하여 revert된 상태로 동기화
# 방법 3: 특정 커밋으로 수동 동기화
argocd app sync payment-service --revision abc1234
# 롤백 후 상태 확인
argocd app get payment-service
# 상태: Synced, Health: Healthy 확인
# 파드 교체 완료 확인
kubectl rollout status deployment/payment -n production
kubectl get pods -n production -l app=payment
Git 커밋 로그가 배포 로그가 됩니다. git log --oneline apps/payment/production/ 명령만으로 누가 언제 무엇을 배포했는지 전체 이력을 확인할 수 있습니다. 별도의 배포 로그 시스템이 필요 없습니다.
핵심 요약
| 개념 | 설명 | 실무 적용 |
|---|---|---|
| Application | ArgoCD 배포 단위 (source + destination) | 서비스당 하나 또는 환경당 하나 |
| ApplicationSet | Application 템플릿 (멀티환경/클러스터) | 동일 앱의 dev/staging/prod 자동화 |
| Sync | Git → 클러스터 상태 적용 | automated + selfHeal로 완전 자동화 |
| Prune | Git 삭제 리소스 → 클러스터에서도 삭제 | 활성화 필수 (기본값은 false) |
| Sync Wave | 리소스 배포 순서 제어 | DB 마이그레이션 → 앱 배포 → 스모크 테스트 |
리포지토리 구조 권장 패턴:
k8s-manifests/
├── apps/
│ ├── payment/
│ │ ├── base/ # 공통 매니페스트
│ │ ├── dev/ # dev 오버레이 (kustomize)
│ │ ├── staging/
│ │ └── production/
│ └── order/
└── infrastructure/
├── cert-manager/
├── ingress-nginx/
└── monitoring/
다음 단계
- Kustomize와 ArgoCD 연동으로 환경별 설정 오버레이
- ArgoCD Image Updater로 이미지 태그 자동 업데이트
- Argo Rollouts로 Blue/Green, 카나리 배포 고급 전략
- ArgoCD Notifications로 Slack/PagerDuty 배포 알림 연동