ArgoCD로 Kubernetes GitOps 배포 자동화 — Helm 연동부터 롤백까지 실전 구성

목차

ArgoCD는 Kubernetes 클러스터의 상태를 Git 레포지토리에 선언된 상태와 자동으로 일치시키는 GitOps 배포 도구다. ArgoCD GitOps 배포 자동화라는 주제가 요즘 DevOps 쪽에서 자주 나오는데, 정작 "왜 필요한가"보다 "어떻게 설치하나"에만 집중하는 글이 많다. 설치는 Helm 한 줄이면 끝난다. 진짜 문제는 그 다음이다.

프론트엔드를 하다가 백엔드로 넘어온 입장에서, Kubernetes 배포는 처음에 상당히 낯설었다. React 앱은 빌드하고 S3에 올리면 끝이었는데, 쿠버네티스는 매니페스트 파일이 수십 개고 환경별로 값이 다르고 배포 순서도 신경 써야 했다. Jenkins 파이프라인에서 kubectl apply -f를 순서대로 실행하는 스크립트를 관리하다가, staging용 ConfigMap이 production에 반영되는 사고가 터졌다. 환경변수 하나가 꼬여서 API 요청이 전부 staging 서버로 날아갔고, 어떤 커밋이 이 매니페스트를 배포했는지 추적하는 데만 한참 걸렸다. 그때 ArgoCD를 도입했다.

"kubectl apply면 충분하다"는 착각

다들 처음엔 이렇게 시작한다. CI 파이프라인 마지막 단계에 kubectl apply를 넣고, 잘 돌아가니까 이대로 괜찮다고 생각한다. 소규모 프로젝트에서는 실제로 괜찮기도 하다. 문제는 규모가 커지면서 드러난다.

명령형 배포의 한계

kubectl apply는 명령형(imperative) 배포다. "이걸 적용해라"라고 지시하는 방식이다. 반면 GitOps는 선언형(declarative)이다. "이 상태여야 한다"를 Git에 정의해두면, 도구가 알아서 현재 상태와 비교하고 맞춰준다. 차이가 미묘해 보이지만 운영에서는 결정적이다.

명령형 배포에서 자주 생기는 문제를 몇 가지 꼽으면 이렇다. 첫째, 누군가 kubectl edit으로 클러스터 리소스를 직접 수정한다. Git에는 흔적이 없다. 둘째, CI 파이프라인이 실패하면 "어디까지 적용됐는지" 파악이 어렵다. 셋째, 롤백이 kubectl rollout undo인데, 이게 Git의 어떤 커밋 상태로 돌아가는 건지 명확하지 않다.

GitOps가 해결하는 것

GitOps에서 Git 레포지토리는 Single Source of Truth다. 클러스터 상태를 알고 싶으면 Git을 보면 된다. 누가 언제 뭘 바꿨는지 git log로 전부 추적된다. 롤백은 git revert다. CI/CD 파이프라인의 권한 관리도 단순해진다. CI는 이미지를 빌드하고 레지스트리에 푸시하는 것까지만 담당하고, 클러스터에 직접 접근할 필요가 없다. ArgoCD가 Git을 감시하다가 변경을 감지하면 클러스터에 반영한다.

프론트엔드 시절에 Git 기반 워크플로우에 익숙했던 게 여기서 오히려 도움이 됐다. PR 리뷰 → 머지 → 자동 배포 흐름이 React 프로젝트의 Vercel 배포와 본질적으로 같다.

관련 추천 상품 보기 — 개발자를 위한 추천 장비와 도구를 확인해보세요. 쿠팡에서 보기 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

ArgoCD 설치 — 여기서 헤매는 사람은 없다

설치 자체는 간단하다.

# ArgoCD 네임스페이스 생성 및 설치 (2026-04 기준 v2.13.x)
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

Helm으로 설치하는 방법도 있다.

helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd -n argocd --create-namespace

초기 admin 비밀번호는 아래 명령어로 확인한다.

# 초기 비밀번호 확인
argocd admin initial-password -n argocd

설치 후 UI에 접근하려면 kubectl port-forward svc/argocd-server -n argocd 8080:443을 쓰거나, Ingress를 붙이면 된다. 여기까지는 공식 문서(출처: Argo CD Getting Started)만 따라가면 되니까 길게 안 쓴다.

Helm Chart 연동 — 생각보다 선택지가 많다

ArgoCD에서 Helm Chart를 쓰는 방식이 두 가지다. 이걸 처음에 헷갈려서 시행착오를 좀 겪었다.

방식 1: Helm Chart를 직접 참조

ArgoCD Application 매니페스트에서 Helm 레포지토리와 차트를 직접 지정하는 방식이다.

# application-helm-direct.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://charts.example.com  # Helm 레포지토리 주소
    chart: my-app-chart
    targetRevision: 1.2.3  # 차트 버전 고정
    helm:
      valueFiles:
        - values-production.yaml  # 환경별 values 파일
      parameters:
        - name: image.tag
          value: "v2.0.1"  # 이미지 태그 오버라이드
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

이 방식의 장점은 외부 Helm 레포의 차트를 바로 갖다 쓸 수 있다는 것이다. nginx-ingress나 cert-manager 같은 서드파티 차트를 배포할 때 적합하다.

방식 2: Git 레포에 Helm Chart를 포함

자체 애플리케이션이라면 이 방식이 낫다. Git 레포에 Chart.yaml, templates/, values.yaml을 넣어두고 ArgoCD가 해당 경로를 바라보게 한다.

# application-helm-git.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/my-org/k8s-manifests.git
    path: charts/my-app  # Git 레포 내 차트 경로
    targetRevision: main
    helm:
      valueFiles:
        - values-staging.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: staging

이 방식을 선택한 이유가 있다. values 파일의 변경 이력이 Git에 남는다. PR로 리뷰할 수 있다. 환경별 values 파일(values-staging.yaml, values-production.yaml)을 같은 레포에서 관리하니까 diff 보기도 편하다.

레포 구조를 어떻게 잡을 것인가

레포 구조에 대해서는 의견이 갈린다. 앱 코드와 매니페스트를 한 레포에 넣는 모노레포 방식이 있고, 매니페스트만 따로 관리하는 분리형이 있다. ArgoCD 공식 문서(출처: Argo CD Best Practices)에서는 분리형을 권장한다. 이유는 명확하다. 앱 코드 커밋이 매번 ArgoCD 동기화를 트리거하면 불필요한 배포가 생긴다. CI에서 이미지 빌드가 끝나면 매니페스트 레포의 이미지 태그만 업데이트하는 구조가 깔끔하다.

실제로 쓰고 있는 레포 구조는 이렇다.

k8s-manifests/
├── apps/
│   ├── my-app/
│   │   ├── Chart.yaml
│   │   ├── templates/
│   │   │   ├── deployment.yaml
│   │   │   ├── service.yaml
│   │   │   └── ingress.yaml
│   │   ├── values-staging.yaml
│   │   └── values-production.yaml
│   └── another-app/
│       └── ...
├── infra/
│   ├── nginx-ingress/
│   └── cert-manager/
└── argocd/
    └── applications/  # ArgoCD Application 매니페스트

apps/는 자체 앱, infra/는 서드파티 인프라 컴포넌트, argocd/는 ArgoCD Application 정의 자체를 관리한다. App of Apps 패턴이라고 부르는 구조인데, ArgoCD Application이 다른 Application들을 관리하는 형태다.

자동 동기화(Auto Sync) — 켜야 하나, 말아야 하나

여기서 반박을 하나 하고 싶다. "자동 동기화는 위험하니까 수동으로 해야 한다"는 주장이 꽤 많다. 틀린 말은 아니지만, 환경에 따라 다르다는 전제가 빠져 있다.

staging은 자동, production은 수동 — 이것도 정답은 아니다

흔히 staging은 Auto Sync, production은 Manual Sync로 가라는 조언을 본다. 처음엔 이대로 했다. 근데 production 배포할 때마다 ArgoCD UI에서 Sync 버튼을 누르는 게, 결국 Jenkins에서 kubectl apply 버튼 누르던 것과 뭐가 다른지 모르겠더라.

Auto Sync를 production에도 켰다. 대신 조건을 걸었다.

syncPolicy:
  automated:
    prune: true      # Git에서 삭제된 리소스를 클러스터에서도 삭제
    selfHeal: true   # 클러스터에서 직접 수정한 것을 Git 상태로 되돌림
  syncOptions:
    - CreateNamespace=true
    - PrunePropagationPolicy=foreground
    - ApplyOutOfSyncOnly=true  # 변경된 리소스만 적용
  retry:
    limit: 3  # 실패 시 3회 재시도
    backoff:
      duration: 5s
      factor: 2
      maxDuration: 1m

핵심은 selfHeal: true다. 누군가 kubectl edit으로 production 리소스를 직접 건드리면, ArgoCD가 Git 상태로 되돌린다. 이게 없으면 GitOps의 의미가 반감된다.

Sync Window로 배포 시간 제한

Auto Sync를 켜되, 배포 가능한 시간대를 제한할 수 있다. 새벽 2시에 자동 배포가 돌면 곤란하니까.

# AppProject에 Sync Window 설정
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: production
  namespace: argocd
spec:
  syncWindows:
    - kind: allow
      schedule: "0 9 * * 1-5"   # 평일 09:00 UTC부터
      duration: 8h               # 8시간 동안만 동기화 허용
      applications:
        - "*"
    - kind: deny
      schedule: "0 0 * * 0,6"   # 주말 전체 차단
      duration: 24h
      applications:
        - "*"

이렇게 하면 Git에 머지해도 Sync Window 밖이면 배포가 보류된다. Window가 열리면 자동으로 동기화가 시작된다.

롤백 전략 — git revert가 전부는 아니다

GitOps에서 롤백은 git revert면 된다고들 한다. 맞는 말인데 현실은 좀 더 복잡하다.

긴급 롤백이 필요한 상황

장애가 터졌을 때 git revert → PR 생성 → 리뷰 → 머지를 기다릴 수 없는 경우가 있다. ArgoCD는 이런 상황을 위해 UI에서 이전 동기화 상태로 즉시 롤백하는 기능을 제공한다. CLI로도 가능하다.

# 특정 앱의 배포 히스토리 확인
argocd app history my-app

# 특정 리비전으로 롤백
argocd app rollback my-app 3

이렇게 롤백하면 클러스터 상태는 이전으로 돌아가지만 Git은 그대로다. ArgoCD 입장에서는 "OutOfSync" 상태가 된다. 이게 의도된 동작이다. 긴급 대응 후에 Git 쪽도 git revert로 맞춰줘야 한다. 안 그러면 다음 동기화 때 다시 장애 버전으로 돌아간다.

실무에서 쓰는 롤백 절차는 이렇다.

  1. ArgoCD CLI 또는 UI로 즉시 롤백 (클러스터 복구)
  2. Auto Sync를 일시 비활성화 (argocd app set my-app --sync-policy none)
  3. 원인 파악 후 Git에서 git revert
  4. PR 리뷰 → 머지
  5. Auto Sync 재활성화

2번을 빼먹으면 3~4번 작업하는 사이에 ArgoCD가 다시 최신 Git 상태(= 장애 버전)로 동기화해버린다. 이걸 한 번 겪으면 잊지 않는다.

Deployment 전략과 ArgoCD

ArgoCD 자체가 Blue-Green이나 Canary 배포를 지원하는 건 아니다. 이건 Argo Rollouts라는 별도 프로젝트의 영역이다(출처: Argo Rollouts GitHub). ArgoCD + Argo Rollouts를 같이 쓰면 Git 기반으로 Canary 배포까지 자동화할 수 있는데, 이건 이 글의 범위를 벗어난다. 다음에 따로 쓸 생각이다.

실무에서 마주치는 문제들

공식 문서대로 따라하면 기본 동작은 된다. 문제는 운영하면서 생긴다.

Secret 관리

Kubernetes Secret을 Git에 평문으로 넣을 수는 없다. 이 문제의 해법이 여러 가지 있는데, 주로 쓰이는 조합을 하나만 정리한다.

도구 방식 ArgoCD 호환 러닝커브
Sealed Secrets 암호화된 Secret을 Git에 저장 네이티브 지원 낮음
External Secrets Operator 외부 시크릿 매니저(AWS SM, Vault 등)에서 가져옴 네이티브 지원 중간
Helm Secrets (sops) values 파일을 sops로 암호화 플러그인 필요 중간
HashiCorp Vault + CSI Vault에서 직접 마운트 별도 구성 높음

Sealed Secrets가 시작하기엔 가장 쉽다. kubeseal CLI로 암호화하면 Git에 커밋해도 안전하고, ArgoCD가 클러스터에 적용할 때 자동으로 복호화된다. 규모가 커지면 External Secrets Operator로 넘어가는 게 일반적인 경로다.

대규모 Application 관리

앱이 20~30개를 넘어가면 Application 매니페스트를 하나씩 관리하는 게 고통이다. ApplicationSet이라는 리소스가 이 문제를 해결한다. Git 레포의 디렉토리 구조를 기반으로 Application을 자동 생성해준다.

# applicationset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-apps
  namespace: argocd
spec:
  generators:
    - git:
        repoURL: https://github.com/my-org/k8s-manifests.git
        revision: main
        directories:
          - path: apps/*  # apps/ 아래 각 디렉토리가 하나의 Application
  template:
    metadata:
      name: "{{path.basename}}"
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/k8s-manifests.git
        path: "{{path}}"
        targetRevision: main
      destination:
        server: https://kubernetes.default.svc
        namespace: "{{path.basename}}"

apps/ 아래에 새 디렉토리를 만들면 ArgoCD Application이 자동으로 생성된다. 삭제하면 Application도 사라진다. 이 패턴을 쓰기 시작하면서 새 서비스 배포 구성 시간이 체감상 절반 이하로 줄었다.

Health Check 커스터마이징

ArgoCD는 리소스의 Health 상태를 자동으로 판단한다. Deployment는 replicas가 ready인지, Service는 endpoint가 있는지 본다. 문제는 CRD(Custom Resource Definition)다. ArgoCD가 모르는 리소스는 Health 상태를 판단 못 한다. 이때 resource.customizations.health 설정으로 Lua 스크립트를 넣어서 커스텀 Health Check를 정의할 수 있다.

이 부분은 아직 깊게 안 써봐서 자세히 쓰기 어렵다. CRD를 많이 쓰는 환경이라면 ArgoCD ConfigMap의 resource.customizations 섹션을 확인하는 게 좋다(출처: Argo CD Resource Health).

알림 연동과 모니터링

배포가 자동화되면 "지금 뭐가 배포됐는지"를 알 수 있는 채널이 필요하다. ArgoCD Notifications라는 기능이 내장되어 있다(v2.6부터 코어에 통합, 출처: Argo CD Notifications).

Slack 알림 설정 예시는 간단한 편이다. argocd-notifications-cm ConfigMap에 Slack 봇 토큰을 넣고, Application에 어노테이션을 추가하면 된다. 동기화 성공, 실패, Health Degraded 같은 이벤트별로 알림을 받을 수 있다. 설정 자체보다 "어떤 이벤트에 알림을 보낼 것인가"를 결정하는 게 더 중요하다. 모든 이벤트에 알림을 걸면 하루에 수십 개씩 오고, 결국 아무도 안 본다.

현재 쓰고 있는 기준은 세 가지다. 동기화 실패(on-sync-failed), Health Degraded(on-health-degraded), 그리고 production 환경의 동기화 성공(on-sync-succeeded). staging 동기화 성공은 빈도가 높아서 알림에서 뺐다.

Prometheus 메트릭도 ArgoCD가 기본 제공한다. /metrics 엔드포인트에서 argocd_app_info, argocd_app_sync_total 같은 메트릭을 수집할 수 있고, Grafana 대시보드 템플릿도 커뮤니티에 여러 개 올라와 있다.

CI 파이프라인과의 연결 — 이미지 태그 업데이트 자동화

ArgoCD만으로는 전체 파이프라인이 완성되지 않는다. CI에서 이미지를 빌드하고, 매니페스트 레포의 이미지 태그를 업데이트하는 연결 고리가 있어야 한다.

GitHub Actions에서 이 흐름을 구현하면 대략 이런 모양이 된다.

# .github/workflows/build-and-update.yaml (앱 코드 레포에 위치)
name: Build and Update Manifest
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build & Push Docker Image
        run: |
          IMAGE_TAG="${GITHUB_SHA::7}"
          docker build -t my-registry/my-app:$IMAGE_TAG .
          docker push my-registry/my-app:$IMAGE_TAG

      - name: Update manifest repo
        run: |
          # 매니페스트 레포를 클론해서 이미지 태그만 교체
          git clone https://x-access-token:${{ secrets.MANIFEST_REPO_TOKEN }}@github.com/my-org/k8s-manifests.git
          cd k8s-manifests
          # yq로 values 파일의 image.tag 업데이트
          yq -i ".image.tag = \"${GITHUB_SHA::7}\"" apps/my-app/values-staging.yaml
          git add .
          git commit -m "chore: update my-app image to ${GITHUB_SHA::7}"
          git push

이 커밋이 매니페스트 레포에 푸시되면 ArgoCD가 변경을 감지하고 동기화를 시작한다. production 배포는 staging에서 검증 후 values-production.yaml의 태그를 별도 PR로 올리는 방식이다.

ArgoCD Image Updater라는 도구를 쓰면 이 과정을 더 자동화할 수 있다. 컨테이너 레지스트리를 감시하다가 새 이미지 태그가 올라오면 자동으로 매니페스트를 업데이트한다. 다만 이건 Git 커밋을 ArgoCD가 대신 만들어주는 구조라서, 팀 내에서 "Git 커밋은 사람이 만든다"는 원칙과 충돌할 수 있다. 현재 쓰고 있지는 않다.

필요한 장비가 있다면 쿠팡에서 찾아보기 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

kubectl apply에서 ArgoCD로 넘어오면서 달라진 점

프론트에서 백엔드로 전환할 때, "상태 관리"라는 개념이 React의 Redux와 Kubernetes의 Desired State가 본질적으로 비슷하다는 걸 느꼈다. 현재 상태와 원하는 상태의 차이를 감지하고 reconcile한다는 점에서 같은 패턴이다. ArgoCD는 이 reconciliation을 Git 기반으로 자동화한 것이다.

도입 전후를 비교하면, 배포 관련 슬랙 문의가 눈에 띄게 줄었다. "이거 배포됐어?" → ArgoCD UI 보면 된다. "언제 배포된 거야?" → Git 커밋 시간 보면 된다. "롤백 좀 해주세요" → 해당 PR을 revert하면 된다. 배포 프로세스가 투명해지니까 불필요한 커뮤니케이션이 사라졌다.

ArgoCD GitOps 기반 Kubernetes 배포 자동화를 처음 구성한다면, 먼저 매니페스트 레포를 분리하고, staging에 Auto Sync를 켜고, Sealed Secrets로 시크릿을 관리하는 것부터 시작하면 된다. 이 세 가지만 해도 kubectl 스크립트 시절과는 완전히 다른 운영 경험이 된다. 다음에는 Argo Rollouts를 붙여서 Canary 배포까지 자동화하는 구성을 실험해볼 생각이다.

관련 글

Chiko IT
Chiko IT

Platform Engineer. Python, AI, Infra에 관심이 많습니다.