왜 다중 클러스터에서 배포 관리가 무너지는가

클러스터가 dev, staging, prod-us, prod-eu로 늘어나면 각 클러스터마다 Application 리소스를 따로 만들어 관리하게 된다. 이때 두 가지 문제가 반복된다. 첫째, 동일한 앱을 클러스터마다 복붙하면서 값 하나가 어긋나는 드리프트가 생긴다. 둘째, 새 서비스를 추가할 때 어느 클러스터에 무엇이 배포됐는지 전체를 추적할 단일 진실 원천(single source of truth)이 없다. ArgoCD의 App-of-Apps 패턴과 ApplicationSet은 이 문제를 "Application을 정의하는 상위 Application" 구조로 풀어낸다.

App-of-Apps의 핵심 구조

루트 Application 하나가 Git 저장소의 특정 디렉터리를 바라보고, 그 안에 자식 Application 매니페스트들을 담는다. 루트를 sync하면 자식 Application들이 생성되고, 각 자식이 실제 워크로드를 각 클러스터로 배포한다. 즉 사람이 관리하는 대상은 루트 하나뿐이다.

# root-app.yaml — 이 하나만 수동으로 apply 한다
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://git.example.com/platform/gitops.git
    targetRevision: main
    path: apps            # 자식 Application 매니페스트가 모인 디렉터리
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

ApplicationSet으로 클러스터별 팬아웃

자식 Application을 클러스터 수만큼 손으로 쓰는 건 여전히 복붙이다. ApplicationSet의 generator를 쓰면 등록된 클러스터 목록을 순회하며 Application을 자동 생성한다. 아래는 특정 라벨을 가진 클러스터에만 배포하는 예시다.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: payment-svc
  namespace: argocd
spec:
  generators:
    - clusters:
        selector:
          matchLabels:
            env: prod          # prod 라벨 클러스터만 대상
  template:
    metadata:
      name: 'payment-{{name}}'
    spec:
      project: default
      source:
        repoURL: https://git.example.com/platform/gitops.git
        targetRevision: main
        path: charts/payment
        helm:
          valueFiles:
            - 'values-{{metadata.labels.region}}.yaml'  # 리전별 값 분리
      destination:
        server: '{{server}}'
        namespace: payment
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

클러스터를 새로 등록하고 env=prod 라벨만 붙이면 payment-svc가 자동으로 그 클러스터에 배포된다. 클러스터를 빼면 자식 Application도 사라진다.

클러스터 등록과 자격 증명

ApplicationSet의 cluster generator는 argocd에 등록된 클러스터 Secret을 기준으로 동작한다. 등록 시점에 라벨을 함께 부여하는 것이 핵심이다.

# 클러스터 등록 후 라벨 부여
argocd cluster add prod-eu-context --name prod-eu
kubectl label secret -n argocd \
  cluster-prod-eu env=prod region=eu

단일 Application vs App-of-Apps vs ApplicationSet

방식관리 단위클러스터 확장적합 상황
개별 Application앱×클러스터 전부수동 복제1~2개 클러스터
App-of-Apps루트 1개자식 파일 추가앱 목록을 Git으로 통제
ApplicationSet템플릿 1개라벨만 부여클러스터 수가 유동적

실무에서 걸리는 주의점

  • prune와 selfHeal의 위험. 루트에 automated prune을 켠 상태에서 실수로 자식 매니페스트를 삭제하면 해당 워크로드가 전 클러스터에서 사라진다. prod 대상 루트는 prune을 신중히 검토하고, 리뷰 없는 main 병합을 막아야 한다.
  • 동시 롤아웃 폭발 반경. 하나의 커밋이 모든 prod 클러스터에 즉시 반영된다. ApplicationSet의 strategy: RollingSync로 클러스터를 단계적으로 배포하고, 앞 단계 실패 시 멈추도록 구성하는 편이 안전하다.
  • 값 파일 존재 여부. values-{{region}}.yaml이 없는 리전이 섞이면 sync가 실패한다. 라벨과 값 파일의 정합성을 CI에서 미리 검증하는 게 좋다.
  • 권한 경계. 다중 클러스터를 한 ArgoCD가 관리하면 그 컨트롤러가 모든 클러스터의 admin 자격을 쥔다. AppProject로 배포 가능한 대상 클러스터·네임스페이스·리소스 종류를 제한해 폭발 반경을 좁혀야 한다.

정리

App-of-Apps는 "배포 목록"을 Git 하나로 통제하고, ApplicationSet은 "클러스터 확장"을 라벨 기반으로 자동화한다. 둘을 함께 쓰면 루트 Application 한 개와 라벨 규칙만으로 다중 클러스터 상태를 선언적으로 관리할 수 있다. 다만 자동화가 강해질수록 한 번의 실수가 미치는 범위도 넓어지므로, prune 정책·단계적 롤아웃·AppProject 권한 제한을 함께 설계해야 실전에서 버틴다.