ArgoCD App of Apps 패턴으로 GitOps 설계하기: 멀티 클러스터·멀티 환경 레포 구조 실전

클러스터가 10개로 늘어나는 순간, 수동 배포는 채무가 된다
우리 팀이 단일 Kubernetes 클러스터에 하나의 ArgoCD로 운영하던 시절, 배포는 어렵지 않았습니다. dev, staging, prod 네임스페이스를 나누고 ArgoCD Application을 손으로 만들면 됐습니다. 하지만 서비스가 성장하면서 리전이 늘었고, 보안 정책상 퍼블릭 클라우드 클러스터와 온프레미스 클러스터를 분리해야 했으며, SRE 팀과 개발팀의 배포 권한을 격리해야 하는 요구가 생겼습니다. 클러스터가 7개를 넘어섰을 때 수작업으로 만든 ArgoCD Application 오브젝트는 150개가 넘었고, 누가 언제 무엇을 바꿨는지 추적이 불가능해졌습니다.
이것이 App of Apps 패턴과 ApplicationSet이 필요해지는 이유입니다. 이 글은 멀티 클러스터·멀티 환경에서 실제로 동작하는 GitOps 레포 구조를 만드는 방법을 구체적인 YAML과 함께 다룹니다.
1. GitOps 원칙: Git이 단일 진실 공급원이 되는 이유
GitOps는 인프라와 애플리케이션의 바람직한 상태(desired state)를 Git 저장소에 선언적으로 정의하고, 자동화된 프로세스가 그 상태를 클러스터에 지속적으로 일치시키는 방식을 의미합니다. OpenGitOps에서 정의한 네 가지 원칙은 다음과 같습니다.
첫째, 선언적(Declarative): 시스템의 바람직한 상태가 선언적으로 표현되어야 합니다. 둘째, 버전 관리(Versioned and Immutable): 원하는 상태는 Git이라는 불변 버전 관리 시스템에 저장됩니다. 셋째, 자동 인출(Pulled Automatically): 소프트웨어 에이전트가 Git에서 변경을 감지하고 자동으로 적용합니다. 넷째, 지속적 조정(Continuously Reconciled): 에이전트는 클러스터의 실제 상태(actual state)를 지속적으로 원하는 상태와 비교합니다.
GitOps가 강력한 실질적 이유는 감사(audit) 가능성과 복구 속도에 있습니다. 장애가 발생했을 때 "어느 배포가 원인이었는가"를 Git 로그에서 바로 확인할 수 있습니다. Kubernetes 오토스케일링 실전 운영에서 다룬 것처럼, 클러스터 운영의 안정성은 관측 가능하고 재현 가능한 설계에서 옵니다.
2. ArgoCD 아키텍처: Application Controller, Repo Server, API Server
Application Controller는 ArgoCD의 심장입니다. Git의 desired state와 클러스터의 actual state를 지속적으로 비교하는 제어 루프가 이 컴포넌트에서 실행됩니다. 각 Application에 대해 정해진 주기(기본 3분)마다 클러스터를 쿼리하고, 드리프트가 감지되면 sync 또는 알림을 트리거합니다.
Repo Server는 Git 저장소를 클론하고 Helm, Kustomize, Jsonnet 등의 템플릿을 렌더링하는 역할을 합니다. Helm values 파일이 많거나 차트가 무거울 경우 Repo Server의 CPU와 메모리가 병목이 됩니다.
API Server는 UI, CLI, CI/CD 파이프라인이 통신하는 엔드포인트입니다. RBAC 검증, SSO 연동, Webhook 수신이 여기서 처리됩니다.
멀티 클러스터에서 ArgoCD 자체는 단일 "허브" 클러스터에 설치하고, 대상 클러스터들을 원격(remote)으로 등록하는 허브-스포크(hub-spoke) 구조가 일반적입니다.
3. App of Apps 패턴 개념: 부트스트래핑 vs 동적 생성
App of Apps 패턴의 아이디어는 단순합니다. ArgoCD의 Application은 Git 경로에 있는 매니페스트를 클러스터에 적용합니다. 그 매니페스트 자체가 또 다른 ArgoCD Application YAML일 수 있습니다.
ArgoCD 공식 클러스터 부트스트래핑 가이드는 이 패턴을 두 가지 목적으로 구분합니다.
부트스트래핑(Bootstrapping): 새 클러스터를 완전히 자동화된 방식으로 초기화할 때 사용합니다. 루트 Application 하나만 수동으로 생성하면, 그 안에서 모니터링 스택, 인그레스 컨트롤러, cert-manager, 네임스페이스, 기타 인프라 컴포넌트들이 선언된 순서대로 자동으로 생성됩니다.
동적 생성(Dynamic Generation): ApplicationSet이 이 역할을 합니다. 클러스터 목록, 환경 목록, 디렉터리 목록처럼 변경되는 입력을 기반으로 Application을 자동으로 생성·수정·삭제합니다.
4. ApplicationSet으로 환경별·클러스터별 앱 자동 생성
ApplicationSet은 ArgoCD v2.3부터 기본 포함된 컨트롤러로, 제너레이터(generator)를 통해 여러 Application을 하나의 템플릿에서 생성합니다.
# applicationsets/payment-service.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: payment-service
namespace: argocd
spec:
goTemplate: true
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
role: workload
- list:
elements:
- env: dev
namespace: payment-dev
valuesFile: values-dev.yaml
syncPolicy: automated
- env: staging
namespace: payment-staging
valuesFile: values-staging.yaml
syncPolicy: automated
- env: prod
namespace: payment-prod
valuesFile: values-prod.yaml
syncPolicy: manual
template:
metadata:
name: "payment-service-{{.env}}-{{.name}}"
spec:
project: "payment-{{.env}}"
source:
repoURL: https://github.com/myorg/helm-charts.git
targetRevision: HEAD
path: charts/payment-service
helm:
valueFiles:
- "../../environments/{{.name}}/{{.env}}/{{.valuesFile}}"
destination:
server: "{{.server}}"
namespace: "{{.namespace}}"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
clusters 제너레이터는 ArgoCD에 등록된 클러스터 목록을 동적으로 읽습니다. goTemplate: true는 ArgoCD v2.6부터 권장되는 설정입니다.
5. 레포 구조 3가지 패턴: mono / poly / matrix
| 패턴 | 구성 | 장점 | 단점 |
|---|---|---|---|
| Mono-repo | 모든 서비스의 차트, 값 파일, ArgoCD 정의가 하나의 저장소 | 변경 추적 단일화 | 저장소 규모 증가 |
| Poly-repo | 서비스별 저장소 + 별도 GitOps config 저장소 | 서비스 팀 자율성 | 크로스 서비스 배포 추적 복잡 |
| Matrix(Hybrid) | 플랫폼 레포 + 앱별 config 레포 조합 | 팀별 소유권 명확 | 레포 간 의존성 관리 필요 |
gitops-root/
├── bootstrap/ # 루트 App of Apps
│ ├── root-app.yaml # ArgoCD에 수동 등록하는 유일한 오브젝트
│ └── applicationsets/ # 모든 ApplicationSet 정의
├── charts/ # 내부 Helm 차트
│ ├── payment-service/
│ └── order-service/
├── environments/ # 클러스터별·환경별 values
│ ├── cluster-kr-prod/
│ │ ├── prod/
│ │ └── staging/
│ └── cluster-us-prod/
└── platform/ # 인프라 컴포넌트 매니페스트
├── monitoring/
├── ingress/
└── cert-manager/
핵심 원칙은 bootstrap/root-app.yaml 하나만 손으로 ArgoCD에 등록한다는 것입니다. Helm 차트 values 모범 사례에서 다루는 것처럼, environments/ 디렉터리는 클러스터와 환경의 조합마다 오버라이드 값만 담습니다.
6. 시크릿 관리: SOPS, ExternalSecrets, sealed-secrets 비교
GitOps에서 가장 민감한 문제는 시크릿입니다. 세 가지 주류 접근법을 비교합니다.
| 방식 | Git 내 시크릿 값 | 외부 의존성 | 키 로테이션 | 멀티 클러스터 |
|---|---|---|---|---|
| SOPS | 암호화된 값 | KMS 또는 PGP | KMS 재암호화 필요 | KMS 공유 가능 |
| ESO | 없음 (참조만) | 외부 시크릿 스토어 | 외부 스토어에서 처리 | 스토어 정책으로 격리 |
| Sealed Secrets | 비대칭 암호화 값 | 없음 | 복잡 | 클러스터별 키 분리 |
멀티 클러스터 환경에서는 ESO + AWS Secrets Manager 조합이 가장 확장성이 좋습니다.
7. Sync Wave와 배포 순서 제어
ArgoCD 선언적 설정 가이드에 따르면, 리소스에 argocd.argoproj.io/sync-wave 어노테이션을 달면 ArgoCD가 낮은 wave 번호 순서대로 리소스를 배포합니다.
# 인프라 컴포넌트는 음수 wave로 먼저 생성
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager
annotations:
argocd.argoproj.io/sync-wave: "-10"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cert-manager
annotations:
argocd.argoproj.io/sync-wave: "-5"
spec:
project: platform
source:
repoURL: https://charts.jetstack.io
chart: cert-manager
targetRevision: v1.14.5
destination:
server: https://kubernetes.default.svc
namespace: cert-manager
Wave 번호는 음수를 쓸 수 있습니다. 인프라 컴포넌트는 음수(-10, -5), 기본 애플리케이션은 0, 의존성이 필요한 리소스(Ingress, HPA)는 양수(5, 10)로 잡으면 직관적입니다.
8. RBAC 설계: AppProject로 권한 격리
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: payment-prod
namespace: argocd
spec:
description: "Payment 서비스 프로덕션 배포 프로젝트"
sourceRepos:
- https://github.com/myorg/helm-charts.git
- https://github.com/myorg/payment-config.git
destinations:
- server: https://kr-prod-cluster.example.com
namespace: payment-prod
- server: https://us-prod-cluster.example.com
namespace: payment-prod
clusterResourceWhitelist:
- group: ''
kind: Namespace
namespaceResourceWhitelist:
- group: apps
kind: Deployment
- group: ''
kind: Service
- group: networking.k8s.io
kind: Ingress
- group: autoscaling
kind: HorizontalPodAutoscaler
roles:
- name: payment-deployer
policies:
- p, proj:payment-prod:payment-deployer, applications, sync, payment-prod/*, allow
- p, proj:payment-prod:payment-deployer, applications, get, payment-prod/*, allow
groups:
- payment-team
namespaceResourceWhitelist를 명시하는 이유는 중요합니다. 애플리케이션 팀이 실수로 또는 의도적으로 ClusterRole, ClusterRoleBinding 같은 클러스터 전역 리소스를 배포하지 못하도록 막습니다.
9. 드리프트 감지와 셀프힐링 운영
ArgoCD의 가장 강력한 기능 중 하나는 드리프트 감지입니다. selfHeal: true를 설정하면 드리프트를 감지하는 즉시 자동으로 Git의 상태로 되돌립니다.
그러나 셀프힐링이 항상 바람직한 것은 아닙니다. 긴급 장애 대응 중 kubectl scale deployment payment-api --replicas=20으로 임시 대응을 했는데 ArgoCD가 바로 원래 replica 수로 되돌리면 오히려 장애를 키웁니다. 이런 상황을 위해 ignoreDifferences 설정을 추가합니다.
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
- group: autoscaling
kind: HorizontalPodAutoscaler
jqPathExpressions:
- .spec.metrics
드리프트 알림을 Slack으로 보내는 것도 중요합니다. argocd-notifications 컨트롤러를 설치하면 OutOfSync, Sync Failed, Degraded 이벤트를 Slack, PagerDuty로 전송할 수 있습니다.
10. 실전 트러블슈팅: OutOfSync, 컴파일 에러, 권한
OutOfSync가 해소되지 않는 경우: argocd app get {앱이름} 또는 UI의 Sync Result 탭에서 실패한 리소스와 에러 메시지를 확인합니다. 흔한 원인은 Webhook 없이 ArgoCD가 Git 변경을 3분 주기로만 폴링하는 경우입니다.
Helm 렌더링 에러: Repo Server 로그에서 실제 Helm 에러를 확인합니다. helm template 명령을 로컬에서 직접 실행해 재현합니다.
권한 오류(forbidden): AppProject의 namespaceResourceWhitelist에 해당 리소스 종류가 허용되어 있는지 확인합니다.
ApplicationSet 변경 영향: preserveResourcesOnDeletion: true를 설정하면 ApplicationSet 삭제 시 Application이 고아(orphan)로 남아 삭제되지 않습니다. 대규모 클러스터 마이그레이션 시 이 설정이 안전망이 됩니다.
결론
ArgoCD App of Apps와 ApplicationSet을 제대로 설계하면 수백 개의 Application을 Git 하나로 일관되게 관리할 수 있습니다.
- 루트 Application 하나만 수동으로 생성한다: 나머지 모든 Application과 ApplicationSet은 Git에서 선언적으로 관리되어야 합니다.
- AppProject를 팀 단위 또는 서비스 도메인 단위로 설계한다:
default프로젝트에 모든 Application을 넣으면 권한 격리가 불가능합니다. - 프로덕션 Sync는 수동 승인으로 막아둔다.
- HPA가 관리하는 replica와 ArgoCD가 관리하는 리소스가 충돌하지 않도록
ignoreDifferences를 설정한다. - Sync Wave를 인프라(-10), 플랫폼(-5), 애플리케이션(0), 의존 리소스(+5) 네 단계로 체계화한다.
GitOps는 도구를 설치하는 것이 아니라 팀의 운영 철학을 Git으로 구현하는 일입니다.