ArgoCD ApplicationSet으로 PR Preview 환경 자동화하기
코드 리뷰를 하다 보면 "이거 로컬에서 띄워봐야 확인할 수 있는 건데…" 하는 순간이 옵니다. PR에 스크린샷 한 장 붙여놓는 것만으로는 한계가 있고, 리뷰어가 직접 브랜치를 체크아웃해서 실행해보는 건 너무 번거롭습니다.
PR이 열리면 자동으로 Kubernetes에 배포되고 고유한 URL이 생기며, PR이 병합되거나 닫히면 흔적도 없이 정리되는 임시 환경 — 솔직히 저도 처음 구축할 때 "이게 정말 자동으로 된다고?" 싶었는데, ArgoCD ApplicationSet의 Generator 조합을 이해하고 나면 생각보다 간결한 구조라는 걸 느끼게 됩니다. 이 글에서는 Pull Request Generator와 Matrix Generator를 조합해 PR 생명주기에 완전히 동기화되는 Preview 환경을 구축하는 방법을 풀어보겠습니다. 이 글을 따라하면 30분 안에 첫 Preview 환경이 뜨는 것을 확인할 수 있습니다.
전제조건: 이 글은 Kubernetes 기초(namespace, Pod, Ingress 개념)와 ArgoCD가 클러스터에 설치된 상태를 전제합니다. Helm이나 Kustomize를 한 번이라도 써보셨다면 무리 없이 따라갈 수 있습니다.
핵심 개념
PR Preview 환경이란
PR Preview 환경은 Pull Request의 생명주기와 1:1로 동기화되는 임시(ephemeral) 배포 환경입니다. 개발자가 PR을 열면 해당 코드가 실제 Kubernetes 클러스터에 배포되어 고유한 URL로 접근할 수 있고, PR이 닫히거나 병합되면 관련 리소스가 모두 자동 삭제됩니다.
이게 왜 가치 있냐면, PM이나 디자이너가 "지금 어디서 확인해볼 수 있어요?"라고 물었을 때 URL 하나만 던져줄 수 있기 때문입니다. 코드 리뷰어도 실제 동작을 눈으로 보면서 리뷰할 수 있으니 리뷰 품질이 체감될 정도로 올라갑니다.
ApplicationSet과 Generator
이 전체 구조의 엔진은 ArgoCD의 ApplicationSet 컨트롤러입니다. ApplicationSet은 "어떤 조건에서 어떤 ArgoCD Application을 만들 것인가"를 선언적으로 정의하는 리소스인데, 여기서 조건을 만들어주는 것이 바로 Generator입니다.
ApplicationSet: 하나의 템플릿으로 여러 ArgoCD Application을 동적으로 생성·삭제하는 컨트롤러. Generator가 파라미터 셋을 제공하면, 템플릿에 파라미터를 주입해 Application을 만듭니다.
Pull Request Generator
Git 호스팅 플랫폼(GitHub, GitLab, Gitea, Bitbucket)의 API를 주기적으로 폴링하여 열린 PR을 감지합니다. 감지된 각 PR에 대해 다음과 같은 템플릿 파라미터를 제공합니다:
| 파라미터 | 설명 | 예시 값 |
|---|---|---|
{{.number}} |
PR 번호 | 142 |
{{.branch}} |
소스 브랜치명 | feature/login-ui |
{{.head_sha}} |
최신 커밋 SHA | a1b2c3d4e5f6... |
{{.head_short_sha}} |
커밋 SHA (7자리) | a1b2c3d |
{{.labels}} |
PR에 붙은 라벨 목록 | ["preview","frontend"] |
PR이 닫히거나 병합되면 해당 Application이 자동으로 삭제됩니다. 이게 PR Preview의 핵심 메커니즘입니다.
폴링 vs Webhook: 기본적으로 Pull Request Generator는
requeueAfterSeconds에 설정한 주기로 GitHub API를 폴링합니다. 즉시 반응이 필요하다면 ArgoCD에 GitHub Webhook을 설정하여 PR 이벤트를 실시간으로 수신하는 방식도 있습니다. 폴링만이 유일한 방법은 아닙니다.
Matrix Generator
두 개의 자식 Generator 출력을 모든 가능한 조합(카르테시안 곱)으로 만들어줍니다. 실무에서 자주 맞닥뜨리는 상황인데, 예를 들어 PR이 3개 열려 있고 대상 클러스터가 2개라면 총 6개의 Application이 생성됩니다.
Pull Request Generator 출력: [PR-1, PR-2, PR-3]
Cluster Generator 출력: [dev-cluster, staging-cluster]
Matrix 결과: [PR-1×dev, PR-1×staging, PR-2×dev, PR-2×staging, PR-3×dev, PR-3×staging]전체 아키텍처 흐름
개발자 PR 오픈
↓
CI(GitHub Actions)가 컨테이너 이미지 빌드 & 푸시
↓
Pull Request Generator가 열린 PR 감지 (폴링 or Webhook)
↓
Matrix Generator와 조합 → Application 생성
↓
ArgoCD가 PR 전용 네임스페이스(preview-pr-142)에 배포
↓
고유 URL 할당 (pr-142.preview.example.com)
↓
PR 종료/병합 시 Application 삭제 → Finalizer가 네임스페이스 및 리소스 정리Finalizer: Kubernetes에서 리소스 삭제 전에 실행되는 정리(cleanup) 로직. ArgoCD에서는
resources-finalizer.argocd.argoproj.io를 Application에 추가하면, Application 삭제 시 해당 Application이 배포한 모든 리소스를 함께 정리합니다. 이걸 빼먹으면 PR이 닫혀도 Pod와 Service가 고아로 남게 됩니다.
실전 적용
예시 1: 단일 클러스터 기본 PR Preview
가장 기본적인 구성입니다. 저도 처음엔 이 패턴으로 시작했는데, 단일 클러스터에서 PR별로 격리된 네임스페이스에 앱을 배포하는 방식입니다. preview 라벨이 붙은 PR만 대상으로 하는 것이 비용 관리의 핵심 포인트입니다.
왜 Kustomize인가: 이 예시에서는 Kustomize를 사용합니다.
namePrefix와commonLabels로 PR별 리소스를 간단히 격리할 수 있어서, 별도의 Helm 차트 없이 기존 매니페스트를 재활용하기 좋은 방식입니다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: pr-preview
namespace: argocd
spec:
goTemplate: true
generators:
- pullRequest:
github:
owner: my-org
repo: my-app
tokenRef:
secretName: github-token
key: token
labels:
- preview
requeueAfterSeconds: 30
template:
metadata:
name: 'preview-{{.number}}'
annotations:
notifications.argoproj.io/subscribe.on-deployed.github: ""
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: previews
source:
repoURL: 'https://github.com/my-org/my-app.git'
targetRevision: '{{.head_sha}}'
path: k8s/overlays/preview
kustomize:
namePrefix: 'pr-{{.number}}-'
commonLabels:
app.kubernetes.io/instance: 'pr-{{.number}}'
destination:
server: https://kubernetes.default.svc
namespace: 'preview-pr-{{.number}}'
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true주요 설정을 정리하면 이렇습니다:
| 설정 | 역할 |
|---|---|
labels: [preview] |
preview 라벨이 붙은 PR만 환경 생성. 불필요한 비용 방지 |
requeueAfterSeconds: 30 |
30초마다 GitHub API를 폴링하여 PR 상태 확인 |
finalizers |
Application 삭제 시 배포된 모든 리소스를 함께 정리 |
goTemplate: true |
Go 템플릿 문법 사용 (.number 등 dot notation) |
targetRevision: '{{.head_sha}}' |
PR의 최신 커밋을 추적. 새 커밋 푸시 시 자동 재배포 |
CreateNamespace=true |
네임스페이스가 없으면 자동 생성 |
prune: true |
Git에서 삭제된 리소스를 클러스터에서도 정리 |
requeueAfterSeconds와 Rate Limit: 30초 폴링이면 시간당 120회 API 호출입니다. 여러 레포에 ApplicationSet을 적용하거나 토큰을 공유하는 환경에서는 GitHub API rate limit(시간당 5,000회)에 걸릴 수 있습니다. 레포가 많다면 60초 이상으로 늘리거나 Webhook 방식을 병행하는 것이 안전합니다.
k8s/overlays/preview 디렉토리에는 최소한 다음과 같은 kustomization.yaml이 필요합니다:
# k8s/overlays/preview/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patchesStrategicMerge:
- resource-limits.yaml # Preview용 리소스 제한이 구성만으로도 PR을 열고 preview 라벨을 붙이면 30초 이내에 환경이 뜨기 시작합니다.
예시 2: Matrix Generator로 멀티 클러스터 Preview
팀이 커지면 "dev 클러스터에서도 보고 싶고, staging에서도 확인하고 싶다"는 요구가 생깁니다. 저도 팀 규모가 10명을 넘어가면서 이 패턴이 필요해졌는데, 처음 설정했을 때 클러스터 라벨 하나 빠뜨려서 한참 헤맨 기억이 있습니다. Matrix Generator를 Pull Request Generator와 조합하면 됩니다.
왜 Helm인가: 멀티 클러스터 환경에서는 클러스터별로 다른 값(ingress host, image tag 등)을 주입해야 하는데, Helm의
parameters오버라이드가 이런 동적 값 주입에 더 적합합니다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: pr-preview-multi-cluster
namespace: argocd
spec:
goTemplate: true
generators:
- matrix:
generators:
- pullRequest:
github:
owner: my-org
repo: my-app
tokenRef:
secretName: github-token
key: token
labels:
- preview
requeueAfterSeconds: 30
- clusters:
selector:
matchLabels:
env: preview
template:
metadata:
name: 'preview-{{.number}}-{{.name}}'
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: previews
source:
repoURL: 'https://github.com/my-org/my-app.git'
targetRevision: '{{.head_sha}}'
path: charts/my-app
helm:
parameters:
- name: image.tag
value: 'pr-{{.number}}-{{.head_short_sha}}'
- name: ingress.host
value: 'pr-{{.number}}.{{.name}}.preview.example.com'
destination:
server: '{{.server}}'
namespace: 'preview-pr-{{.number}}'
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true여기서 clusters Generator는 ArgoCD에 등록된 클러스터 중 env: preview 라벨이 붙은 것만 선택합니다. Matrix Generator가 PR과 클러스터를 곱해주니, PR-142가 열리면 preview-142-dev-cluster와 preview-142-staging-cluster 두 개의 Application이 자동 생성됩니다.
{{.name}}은 Cluster Generator에서 온 클러스터 이름이고, {{.server}}는 해당 클러스터의 API 서버 주소입니다. 서로 다른 Generator의 파라미터를 하나의 템플릿에서 자유롭게 혼용할 수 있는 것이 Matrix Generator의 강력함입니다.
예시 3: GitHub Actions CI 파이프라인 연계
ArgoCD가 배포를 담당한다면, CI는 이미지 빌드와 PR에 Preview URL을 코멘트하는 역할을 합니다. 솔직히 이 부분에서 가장 많이 삽질했던 게 이미지 태그 컨벤션 맞추기였습니다. CI에서 빌드한 태그와 ApplicationSet 템플릿의 태그가 한 글자라도 다르면 이미지를 못 찾아서 Pod가 뜨질 않습니다.
# .github/workflows/pr-preview.yml
name: PR Preview
on:
pull_request:
types: [opened, synchronize, labeled]
jobs:
build:
if: contains(github.event.pull_request.labels.*.name, 'preview')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and Push Image
run: |
IMAGE=ghcr.io/my-org/my-app:pr-${{ github.event.number }}-$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)
docker build -t $IMAGE .
docker push $IMAGE
- name: Comment Preview URL
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `🚀 Preview 환경이 준비되고 있습니다!\n\nhttps://pr-${context.issue.number}.preview.example.com\n\n배포 완료까지 1~2분 소요됩니다.`
})이미지 태그에서 head_short_sha(7자리)를 사용하는 부분이 핵심입니다. ApplicationSet 템플릿의 image.tag 값이 pr-{{.number}}-{{.head_short_sha}}이므로, CI에서도 동일하게 SHA를 7자리로 잘라서 태그를 만들어야 합니다.
장단점 분석
솔직히 PR Preview를 도입하고 나서 팀의 리뷰 문화가 눈에 띄게 바뀌었습니다. 하지만 공짜 점심은 없더라구요. 운영하면서 느낀 장단점을 정리해봤습니다.
장점
| 항목 | 내용 |
|---|---|
| 빠른 피드백 루프 | 코드 리뷰어가 URL 하나로 기능을 직접 확인. 리뷰 품질이 체감될 정도로 올라갑니다 |
| 완전 자동화 | PR 오픈부터 환경 삭제까지 수동 개입이 전혀 없습니다 |
| 격리된 테스트 | 각 PR이 독립 네임스페이스에서 실행되어 환경 간 간섭이 없습니다 |
| 비개발자 협업 | PM, QA, 디자이너가 개발 환경 설정 없이 기능을 직접 확인할 수 있습니다 |
| GitOps 원칙 준수 | 모든 인프라 상태가 Git에 선언적으로 정의되어 추적성과 재현성이 보장됩니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 인프라 비용 증가 | PR마다 전체 스택 배포로 리소스 소비 급증 | preview 라벨 필터링, ResourceQuota 설정, TTL 기반 자동 정리 |
| 초기 설정 복잡도 | ApplicationSet, CI, DNS, Ingress, 시크릿 등 세팅할 것이 많음 | 예시 1의 기본 패턴부터 시작해 점진적으로 확장 |
| DB 상태 관리 | 스테이트풀 서비스의 시딩/마이그레이션/격리가 까다로움 | PR별 임시 DB 인스턴스 또는 공유 DB + 스키마 격리 전략 선택 |
| 네트워크 복잡성 | 와일드카드 DNS, TLS 인증서, 서비스 간 통신 설정 | ExternalDNS + cert-manager로 자동화 |
| 보안 노출 | 미완성 코드가 외부에 노출될 수 있음 | OAuth2 Proxy 경유, 내부 네트워크 제한 |
ResourceQuota: Kubernetes에서 네임스페이스별로 CPU, 메모리, Pod 수 등의 상한을 설정하는 리소스. Preview 환경에 적용하면 하나의 PR이 클러스터 리소스를 독점하는 것을 방지할 수 있습니다.
실무에서 가장 흔한 실수
-
Finalizer를 빼먹는 것 — Application에
resources-finalizer.argocd.argoproj.io를 추가하지 않으면, Application은 삭제되지만 배포된 리소스(Pod, Service, Ingress)는 고아 상태로 남습니다. 며칠 뒤kubectl get pods --all-namespaces를 때려보면 정체불명의 Pod들이 줄줄이 나오는 상황이 반드시 옵니다. -
라벨 필터 없이 모든 PR에 환경을 생성하는 것 — 처음엔 "전부 다 생성하면 편하지 않을까?" 싶지만, 활발한 저장소에서 동시에 20개 PR이 열리면 클러스터가 비명을 지릅니다.
labels필터는 선택이 아니라 필수입니다. -
이미지 태그 컨벤션 불일치 — 새벽 2시에 슬랙 알림이 와서 확인해보니 Preview 환경이 전부 CrashLoopBackOff였던 적이 있습니다. 원인은 단순했는데, CI에서
pr-142-a1b2c3d(7자리 SHA)로 빌드했는데 템플릿에서{{.head_sha}}(전체 40자리 SHA)를 참조하고 있었던 것입니다.head_sha와head_short_sha를 혼동하면 안 됩니다.
selfHeal에 대한 팁:
selfHeal: true는 Git 상태로 자동 복구하는 설정인데, Preview 환경에서 디버깅하며 리소스를 임시로 수정할 때 ArgoCD가 계속 되돌려버릴 수 있습니다. 디버깅이 잦은 팀이라면 Preview에서는selfHeal: false로 두는 것도 고려해볼 만합니다.
마치며
PR Preview 환경은 "코드 리뷰 = URL 클릭 한 번으로 확인"이라는 경험을 팀에 선물하는 가장 효과적인 DevOps 투자입니다.
저도 처음 구축했을 때 PM분이 "이제 매번 '어디서 확인해요?' 안 물어봐도 되는 거죠?" 하셨던 순간이 기억납니다. 초기 설정이 만만치 않은 건 사실이지만(앞서 단점에서도 솔직하게 언급했듯이), 한번 구축해놓으면 그 이후로는 모든 PR에서 자동으로 돌아갑니다.
지금 바로 시작해볼 수 있는 3단계:
-
GitHub Token Secret 생성 —
kubectl create secret generic github-token --from-literal=token=ghp_xxxx -n argocd로 토큰을 등록합니다. repo 읽기 권한만 있으면 충분합니다. (프로덕션 환경에서는 Personal Access Token 대신 GitHub App 인증을 사용하는 것이 보안상 권장됩니다.) -
기본 ApplicationSet 배포 — 위의 예시 1 YAML을 그대로 복사해서 org/repo 정보만 수정한 뒤
kubectl apply -f pr-preview-appset.yaml로 적용합니다.k8s/overlays/preview경로에 Kustomize 오버레이가 준비되어 있어야 합니다. -
PR에
preview라벨 붙이기 — 테스트 PR을 하나 열고preview라벨을 추가한 뒤, ArgoCD UI에서preview-{PR번호}Application이 생성되고 Sync되는 것을 확인합니다. 30초 내에 반응이 없다면 Token 권한과 ArgoCD ApplicationSet controller 로그(kubectl logs -n argocd -l app.kubernetes.io/name=argocd-applicationset-controller)를 점검해보시면 됩니다.
다음 글: Preview 환경에 공유 데이터베이스를 안전하게 연결하는 방법 — PR별 스키마 격리와 시드 데이터 자동화 전략
참고 자료
핵심 문서 (먼저 읽기)
- ArgoCD Pull Request Generator 공식 문서
- ArgoCD Matrix Generator 공식 문서
- ArgoCD Application Pruning & Resource Deletion
실전 가이드
- Create Temporary Argo CD Preview Environments Based On Pull Requests — Codefresh
- Automate CI/CD on pull requests with Argo CD ApplicationSets — Red Hat Developer
- Setting up Preview Environments for Pull Requests with Argo CD and GitHub Actions — Medium
- From PR → Preview → Production with GitHub Actions + ArgoCD — Medium
- The What and Why of Ephemeral Preview Environments on Kubernetes — Northflank
- Comprehensive Guide to Preview Environment Solutions for Kubernetes — Signadot