ArgoCD ApplicationSet으로 멀티 환경 배포 자동화하기

ArgoCD ApplicationSet으로 멀티 환경 배포 자동화하기
ArgoCD를 처음 도입했을 때는 환경마다 Application 리소스를 따로 만들어 썼다. dev, staging, prod 세 환경에 마이크로서비스 12개. 단순 계산으로 36개. 여기에 신규 환경이라도 하나 늘면 또 12개를 복사 붙여넣기. 처음엔 견딜만 했는데, 팀이 커지면서 이 반복 작업이 진짜 골치 아파졌다.
ApplicationSet은 이 문제를 푸는 도구다. 한 번 정의해두면 generator가 알아서 N개의 Application을 만들어준다. 이번 글에서는 우리 팀이 실제 운영하면서 정착시킨 패턴을 정리해본다. 참고로 최근 ArgoCD 3.3에서 ApplicationSet의 shallow clone 옵션이 추가돼서 큰 모노레포에서도 generator 동작이 훨씬 빨라졌으니, 가능하면 3.3 이상으로 올리는 걸 추천한다.
Generator 종류와 선택 기준
ApplicationSet generator는 종류가 꽤 많은데, 실무에서 자주 쓰는 건 결국 4가지 정도다.
- List generator: 가장 단순. 환경/리전 이름을 배열로 박아두고 쓴다. 정적이라 명시적이지만, 환경이 늘 때마다 매니페스트를 수정해야 한다.
- Cluster generator: ArgoCD에 등록된 cluster secret을 읽어서 자동으로 Application을 만든다. cluster label로 필터링 가능. 멀티 클러스터 환경이면 이게 정답에 가깝다.
- Git generator: Git 레포의 디렉터리나 파일을 스캔해서 발견되는 만큼 Application을 만든다. "환경 디렉터리만 추가하면 자동 배포"라는 GitOps의 이상에 가장 근접.
- Matrix generator: 위 generator 두 개를 곱한다. "환경 4개 × 서비스 12개 = Application 48개" 같은 조합 폭발을 한 줄로 표현.
처음엔 List로 시작하다가, 환경이 늘면 Git이나 Cluster로 옮겨가는 경로가 가장 자연스럽다.
우리 팀이 정착시킨 구조
먼저 디렉터리 구조부터 보자.
gitops/
├── apps/
│ ├── api-gateway/
│ │ ├── base/
│ │ │ └── kustomization.yaml
│ │ └── overlays/
│ │ ├── dev/
│ │ ├── staging/
│ │ └── prod/
│ ├── user-service/
│ └── ...
└── applicationsets/
└── all-apps.yaml
핵심은 apps/<서비스>/overlays/<환경> 패턴이다. Git generator가 이 경로를 스캔해서 환경별 Application을 자동 생성한다.
ApplicationSet 매니페스트는 이렇게 생겼다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: all-apps
namespace: argocd
spec:
generators:
- matrix:
generators:
- git:
repoURL: https://github.com/my-org/gitops
revision: main
directories:
- path: apps/*/overlays/*
- list:
elements:
- cluster: dev-cluster
envFilter: dev
- cluster: staging-cluster
envFilter: staging
- cluster: prod-cluster
envFilter: prod
template:
metadata:
name: '{{path.basename}}-{{path[1]}}'
spec:
project: default
source:
repoURL: https://github.com/my-org/gitops
targetRevision: main
path: '{{path}}'
destination:
server: '{{cluster}}'
namespace: '{{path[1]}}'
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
path[1]이 서비스명, path.basename이 환경명을 가리킨다. Application 이름은 dev-api-gateway 같은 형태로 떨어진다.
운영 중에 한 번씩 발 묶이는 지점
이 패턴을 1년 가까이 굴리면서 몇 번 데인 적이 있다.
Matrix generator로 묶었을 때 prune이 무서워진다. ApplicationSet은 generator 결과에 없는 Application을 자동으로 지운다. dev 환경 overlay 디렉터리를 잠깐 잘못된 브랜치에 올렸다가 main에 머지 안 한 상태로 두면, generator는 그 dev overlay가 없다고 인식해서 prod까지 prune 후보로 만들 수 있다. 우리는 이걸 막으려고 spec.syncPolicy.preserveResourcesOnDeletion: true를 prod 환경엔 적용했다. 의도된 삭제까지 막아버리는 단점은 있다.
Git generator의 polling 주기가 의외로 느리다. 기본 3분. CI가 git push 후 즉시 배포되길 기대했는데, 처음엔 "왜 안 되지?"하며 한참을 찾아 헤맸다. webhook을 ArgoCD에 걸어두면 즉시 트리거된다. webhook 없이 polling만 쓰면 PR 머지 후 평균 1~2분 대기 시간이 깔린다는 점은 인지하고 있어야 한다.
dev/staging/prod 한 ApplicationSet에 묶으면 권한 분리가 까다롭다. ApplicationSet 자체가 어드민 권한을 갖는 셈이라, 누군가 ApplicationSet 매니페스트를 수정하면 prod까지 영향을 준다. 그래서 환경별로 ApplicationSet을 쪼개고, prod용 ApplicationSet은 별도 GitOps 레포(보호 브랜치 + CODEOWNERS)에 두는 패턴으로 옮겨가는 중이다.
최근에 도움이 된 변화
ArgoCD 3.3부터 Git generator에 shallow clone 옵션이 들어왔다. 큰 모노레포에서 디렉터리 스캔할 때 전체 히스토리를 fetch하던 게 사라져서, 우리 케이스에선 generator 한 사이클이 90초에서 12초로 줄었다. 같은 버전에 안전 삭제 관련 옵션들도 들어와서, prod prune 사고를 줄이려는 사람들에겐 의미가 있다.
마무리
ApplicationSet은 환경이 3개 이상으로 늘어나는 순간 본전을 뽑기 시작한다. 처음엔 List generator로 천천히 시작해서, 패턴이 잡히면 Git generator나 Matrix로 옮겨가는 흐름이 안전하다. prod 환경의 prune 동작은 반드시 한 번 시뮬레이션 해보는 걸 추천한다. 우리도 staging에서 일부러 디렉터리 지웠다 복구하는 훈련을 한 번 거치고 나서야 prod에 적용했다.
혹시 다른 generator 패턴 쓰시는 분 있으면 댓글로 공유 부탁드린다. 특히 SCM provider generator(GitHub/GitLab 조직 단위로 레포 스캔)는 우리도 아직 안 써봐서 궁금하다.