IT/CI CD

ArgoCD ApplicationSet PR Generator로 PR별 preview 환경 만들기

gfrog 2026. 5. 5. 06:15
반응형

PR 올라올 때마다 리뷰어한테 "로컬에서 띄워서 봐줘"라고 말하는 게 한두 번이지, 매번 그러기 좀 그렇다. 우리 팀은 PR 하나당 stage 환경에 임시로 배포해서 QA가 직접 클릭해보고 댓글 다는 흐름을 원했는데, 그래서 결국 ArgoCD ApplicationSet의 Pull Request generator를 붙였다.

처음엔 "그냥 GitHub Actions로 helm install 돌리면 되는 거 아냐?"라고 생각했는데, 막상 정리되고 나니 GitOps의 일관성이라는 게 꽤 크게 다가왔다. PR 닫으면 알아서 지워주고, 상태도 ArgoCD UI에 그대로 보이고. 이번 글에서는 셋업 과정과 실제로 굴려보면서 부딪힌 몇 가지를 정리한다.

PR Generator가 하는 일

ApplicationSet은 한 번에 여러 Application을 generator로 찍어내는 CRD다. 그중에서 PR generator는 GitHub/GitLab/Bitbucket의 API를 폴링해서 열려 있는 PR 목록을 가져온다. PR 하나당 Application이 하나 만들어지고, PR이 머지되거나 닫히면 Application이 사라진다. 사라질 때 들어 있던 리소스(Deployment, Service, Ingress 등)도 같이 정리된다. 단순한 발상인데, 이게 GitOps라는 단일 진실 공급원에 자연스럽게 녹아드는 게 핵심이다.

기본 폴링 주기는 30분이라 좀 답답하다. requeueAfterSeconds를 60~120 정도로 줄여서 쓴다. 너무 짧게 하면 GitHub API rate limit에 닿을 수 있으니 주의.

최소 구성 예시

GitHub repo에서 열린 PR 중 preview 라벨이 붙은 것만 골라 띄우는 형태로 구성했다. 모든 PR을 띄우면 클러스터가 금방 너덜너덜해진다.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: web-pr-preview
  namespace: argocd
spec:
  generators:
    - pullRequest:
        github:
          owner: ourorg
          repo: web
          tokenRef:
            secretName: github-token
            key: token
          labels:
            - preview
        requeueAfterSeconds: 90
  template:
    metadata:
      name: 'web-pr-{{number}}'
    spec:
      project: preview
      source:
        repoURL: https://github.com/ourorg/web.git
        targetRevision: '{{head_sha}}'
        path: deploy/helm
        helm:
          valueFiles:
            - values.yaml
            - values-preview.yaml
          parameters:
            - name: image.tag
              value: 'pr-{{number}}-{{head_sha_short}}'
            - name: ingress.host
              value: 'pr-{{number}}.preview.example.dev'
      destination:
        server: https://kubernetes.default.svc
        namespace: 'preview-pr-{{number}}'
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true

여기서 중요한 건 targetRevision: '{{head_sha}}'다. 브랜치 이름({{branch}})을 쓰면 force-push 같은 상황에서 ArgoCD가 한 번 더 sync를 돌려야 하는데, sha를 직접 박으면 PR에 새 커밋이 올라올 때마다 새로 Application spec이 갱신돼서 깔끔하게 동작한다.

이미지 태그는 PR이 생성될 때 GitHub Actions에서 pr-<번호>-<sha7> 형식으로 빌드해 ECR에 푸시해두는 컨벤션을 잡았다. ApplicationSet은 그 태그가 존재한다고 가정하고 동작한다. 둘이 묶이지 않으면 sync는 됐는데 ImagePullBackOff에 걸려서 reviewer가 "왜 안 떠?" 하고 물어보는 상황이 생긴다.

굴려보면서 만난 함정들

ResourceQuota를 꼭 걸자. preview 네임스페이스에 quota 안 걸어두면, 어느 PR이 메모리 4Gi짜리 sidecar를 잘못 박아두고 머지 안 한 채로 휴가 가버리면 클러스터 노드가 한쪽으로 쏠린다. 우리는 namespace 템플릿 차원에서 LimitRange + ResourceQuota를 같이 발사하는 별도 Helm chart를 만들고, ApplicationSet의 syncOptionsApplyOutOfSyncOnly=true를 줘서 quota 변경 안 하면 매번 재적용 안 하도록 했다.

Ingress 호스트 충돌. 처음에 PR 번호로 호스트를 만들었더니 stale DNS 캐시랑 cert-manager의 ACME challenge가 맞물려서 PR 다시 열 때 인증서가 늦게 발급되는 케이스가 있었다. 그래서 와일드카드 인증서(*.preview.example.dev)를 미리 발급받아 cert-manager에서 캐싱하도록 바꿨다. PR마다 새로 challenge 안 돌아도 되니 띄우는 시간이 1~2분 빨라졌다.

cleanup 타이밍. PR 닫고 나서 Application이 즉시 사라지는 건 좋은데, finalizer 처리 안 하면 PVC가 남기도 한다. 우리 팀은 preview에 PVC 거의 안 쓰는 정책이지만, 혹시 쓰는 PR이 있다면 argocd.argoproj.io/sync-options: PrunePropagationPolicy=foreground를 application.yaml 어노테이션으로 박아두는 걸 추천한다.

RBAC 분리. preview AppProject를 따로 만들어서 destinations를 preview-* 네임스페이스로만 제한하고, clusterResourceWhitelist를 비워뒀다. PR로 들어온 매니페스트가 잘못해서 ClusterRole 뿌리거나 다른 네임스페이스 건드리는 걸 막는다. 코드 리뷰 안 거친 매니페스트가 클러스터 단위로 권한 갖는 건 상상만 해도 식은땀.

마무리

지금까지 한 달쯤 굴려보고 있는데, 리뷰어가 "PR-1234 한번 봐주세요"라고 하면 슬랙 링크 하나로 끝나는 흐름이 자리 잡았다. PR Generator 자체는 단순한데, 주변 RBAC/quota/이미지 빌드 파이프라인을 같이 정리하지 않으면 금방 지저분해진다는 걸 배웠다.

다음에는 preview 환경에서 production DB를 어떻게 안전하게 격리할지(read-replica + 데이터 마스킹) 고민한 걸 써보려고 한다. 혹시 비슷한 셋업 굴리는 분들 중에 cleanup 자동화 더 깔끔하게 한 사례 있으면 댓글로 알려주시면 좋겠다.

반응형