PR 머지 하나로 Kubernetes 카나리 배포를 자동화하는 ArgoCD + Argo Rollouts 파이프라인
솔직히 말하면, 처음 카나리 배포를 도입했을 때 저는 배포 스크립트를 직접 손으로 실행하고 있었습니다. kubectl set image 명령어를 터미널에 치고, Slack에 "지금 카나리 5% 올렸습니다" 메시지를 남기고, 30분 뒤에 다시 콘솔에 들어가서 메트릭 확인하고, 또 손으로 가중치를 올리고. 자동화라고 부르기 민망한 수준이었죠.
ArgoCD와 Argo Rollouts를 함께 쓰기 시작하면서 이 흐름이 완전히 바뀌었습니다. 지금은 PR을 머지하는 순간부터 카나리 트래픽 이동, Prometheus 메트릭 검증, 전체 승격 또는 자동 롤백까지 사람 손이 거의 닿지 않습니다. 이 글을 다 읽으면 Rollout, AnalysisTemplate, ArgoCD Application, GitHub Actions 워크플로, 이렇게 YAML 4개로 돌아가는 카나리 파이프라인을 직접 구성할 수 있습니다.
다만 이 글은 Kubernetes를 어느 정도 알고 Deployment와 Service를 직접 작성해본 경험이 있는 분을 전제합니다. PromQL 예시도 일부 등장하지만, Prometheus를 깊이 몰라도 전체 흐름을 따라오는 데는 무리가 없습니다.
핵심 개념
GitOps: PR 머지 = 배포 승인
GitOps의 핵심은 단순합니다. Git 리포지토리를 유일한 진실의 원천(Single Source of Truth)으로 삼고, 모든 인프라와 앱 상태를 Git에 선언해두는 것입니다. 배포하고 싶으면 Git에 반영하면 됩니다. 승인 프로세스가 필요하면 PR 리뷰로 대체됩니다.
GitOps: "운영 상태를 코드로 선언하고 Git을 통해 변경을 관리하는 방법론." 배포 스크립트를 실행하는 것이 아니라, Git 커밋이 배포 트리거가 됩니다.
이 방식의 또 다른 장점은 감사 추적(audit trail)이 자동으로 생긴다는 점입니다. 누가, 언제, 무엇을, 왜 배포했는지가 Git 히스토리에 고스란히 남습니다.
ArgoCD vs Argo Rollouts: 역할이 다릅니다
두 도구를 처음 접하면 "둘 다 Argo 아닌가요? 뭐가 다른 거죠?"라는 질문이 자연스럽게 나옵니다. 저도 처음엔 헷갈렸습니다.
| 도구 | 역할 | 한 줄 요약 |
|---|---|---|
| ArgoCD | Git → 클러스터 동기화 | 무엇을 배포할지 결정 |
| Argo Rollouts | 트래픽 점진적 이동 + 자동 승격/롤백 | 어떻게 배포할지 결정 |
ArgoCD는 Git에 변경이 생기면 클러스터를 그 상태와 맞춰줍니다. 반면 Argo Rollouts는 새 버전이 들어왔을 때 트래픽을 5% → 25% → 50%로 단계적으로 이동시키고, 각 단계에서 Prometheus 같은 메트릭을 확인해서 문제가 생기면 즉시 롤백합니다. 두 도구가 레이어를 나눠 맡는 구조입니다.
카나리 자동화 흐름 전체 그림
실제로 Git 머지부터 배포 완료까지 어떤 순서로 일이 벌어지는지 정리하면 이렇습니다.
개발자 PR 머지
│
▼
GitHub Actions: 이미지 빌드 → 이미지 태그를 설정 리포에 커밋
│
▼
ArgoCD: 설정 리포 변경 감지 → Rollout 리소스 업데이트 (sync)
│
▼
Argo Rollouts 컨트롤러: 카나리 단계 시작
├── Step 1: 트래픽 5% → 5분 대기 → AnalysisRun 실행
├── Step 2: 트래픽 25% → 10분 대기
├── Step 3: 트래픽 50% → 10분 대기
└── 모든 단계 통과 시: 컨트롤러가 자동으로 stable 승격
└── 실패 시: 즉시 0%로 복원 (자동 롤백)Rollout, AnalysisTemplate, AnalysisRun
Argo Rollouts가 제공하는 CRD(Custom Resource Definition) 세 가지를 알아두면 나머지가 쉽게 읽힙니다.
- Rollout: 기존
Deployment를 대체하는 리소스. 카나리 단계와 전략이 여기 정의됩니다.- AnalysisTemplate: "성공률 95% 이상이어야 통과"처럼 메트릭 기반 통과 기준을 정의하는 템플릿.
- AnalysisRun: AnalysisTemplate을 특정 배포 단계에서 실행한 인스턴스. 결과가 PASS/FAIL로 나옵니다.
실전 적용
Rollout YAML 기본 구조 — Deployment를 카나리 전략으로 교체하기
가장 먼저 해야 할 일은 기존 Deployment 리소스를 Rollout으로 교체하는 것입니다. apiVersion과 kind가 바뀌고, strategy 블록에 카나리 단계를 선언합니다.
# k8s/rollout.yaml (설정 리포지토리에 저장)
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: my-app
spec:
replicas: 10
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: ghcr.io/myorg/my-app:1.0.0 # GitHub Actions가 이 값을 자동으로 갱신합니다
strategy:
canary:
canaryService: my-app-canary # 카나리 전용 Service
stableService: my-app-stable # 스테이블 전용 Service
trafficRouting:
nginx:
stableIngress: my-app-ingress
steps:
- setWeight: 5 # 트래픽 5%를 카나리로 이동
- pause: {duration: 5m} # 5분 관찰
- analysis: # Prometheus 메트릭 검증
templates:
- templateName: success-rate
args:
- name: app-name
value: my-app
- setWeight: 25
- pause: {duration: 10m}
- setWeight: 50
- pause: {duration: 10m}
# 모든 steps를 통과하면 컨트롤러가 자동으로 stable로 승격합니다
# setWeight: 100을 명시하지 않아도 됩니다canaryService와 stableService는 Rollout과 별도로 미리 만들어두어야 합니다. 이 두 Service가 없으면 컨트롤러가 에러를 내뿜고 카나리가 시작되지 않습니다. 최소 구성은 이렇습니다.
# k8s/services.yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-stable
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: my-app-canary
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080두 Service의 selector는 처음엔 동일해도 됩니다. Argo Rollouts가 배포 과정에서 카나리 Pod에만 트래픽이 가도록 selector를 자동으로 조작합니다.
| 필드 | 역할 |
|---|---|
canaryService / stableService |
트래픽 분리를 위한 Service 쌍. 사전에 반드시 생성되어 있어야 합니다. |
trafficRouting.nginx |
Nginx Ingress를 통한 가중치 라우팅. Istio나 Linkerd로 교체도 가능합니다. Nginx는 Ingress Controller 레벨에서 가중치를 조절하고, Istio/Linkerd는 사이드카 메시 방식이라 세밀한 헤더 기반 라우팅이 가능하지만 운영 복잡도가 올라갑니다. |
setWeight |
카나리 Pod로 보낼 트래픽 비율(%) |
pause |
다음 단계로 넘어가기 전 대기 시간. {}(시간 없음)으로 쓰면 수동 승인 게이트가 됩니다. |
analysis |
이 단계에서 AnalysisTemplate을 실행합니다. FAIL이면 자동 롤백됩니다. |
pause: {}를 수동 승인 게이트로 쓰는 방식은 실제로 꽤 유용합니다. 주요 배포에서는 이 게이트 앞에 팀 리드가 실제로 앉아서 ArgoCD 대시보드로 메트릭을 보고 kubectl argo rollouts promote my-app 명령으로 직접 승격 결정을 내리기도 합니다.
Prometheus 분석 조건 선언 — AnalysisTemplate으로 자동 롤백 기준 설정
메트릭 분석 조건을 별도 템플릿으로 선언해두면 여러 Rollout에서 재사용할 수 있습니다. 재사용성을 위해 앱 이름은 args로 파라미터화하는 것이 실무 관행입니다.
# k8s/analysis-template.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: app-name # Rollout에서 앱 이름을 주입합니다
metrics:
- name: success-rate
interval: 1m # 1분마다 측정
successCondition: result[0] >= 0.95 # 성공률 95% 이상이어야 통과
failureLimit: 3 # 연속 3회 실패하면 AnalysisRun이 FAIL
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(http_requests_total{status!~"5.*",app="{{args.app-name}}"}[2m]))
/
sum(rate(http_requests_total{app="{{args.app-name}}"}[2m]))이렇게 하면 같은 AnalysisTemplate을 my-app, my-other-service 등 여러 Rollout에서 앱 이름만 바꿔서 재사용할 수 있습니다.
failureLimit: 3은 실무에서 중요한 설정입니다. 순간적인 트래픽 스파이크나 측정 노이즈 때문에 일시적으로 95%를 밑도는 경우가 있는데, 연속 3회를 기준으로 삼으면 오판 가능성이 크게 줄어듭니다. 저도 초기에 failureLimit: 1로 설정했다가 정상 배포인데도 롤백이 발생해서 곤혹스러웠던 적이 있었습니다. 반대로 interval을 너무 짧게 잡으면 샘플이 부족해서 통계적으로 의미 없는 결과가 나옵니다. 30초 간격으로 조회하면 샘플이 부족해 노이즈에 취약해지기 때문에, 1~2분 간격을 출발점으로 삼고 트래픽 규모에 맞게 조정하는 것이 좋습니다.
ArgoCD 자동 동기화 설정 — Git 변경을 클러스터에 즉시 반영
ArgoCD가 어떤 Git 리포지토리의 어떤 경로를 감시할지 선언합니다. 여기서 가리키는 리포는 my-app-config, 즉 애플리케이션 코드가 아닌 Kubernetes 설정 전용 리포입니다.
# argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
source:
repoURL: https://github.com/myorg/my-app-config # 설정 전용 리포지토리
targetRevision: HEAD
path: k8s/ # Rollout, Service, AnalysisTemplate이 모두 여기 있습니다
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Git에서 삭제된 리소스는 클러스터에서도 삭제
selfHeal: true # 클러스터 상태가 Git과 달라지면 자동으로 복원syncPolicy.automated를 켜두면 my-app-config 리포지토리의 k8s/ 경로에 변경이 생기는 즉시 클러스터와 동기화됩니다.
왜 리포지토리를 분리하나요? 앱 코드 리포(소스 코드, Dockerfile)와 설정 리포(Kubernetes YAML)를 나누면 배포 히스토리가 설정 리포의 Git 로그에만 깔끔하게 남습니다. 앱 기능 커밋과 배포 커밋이 섞이지 않아 롤백이나 감사 추적이 훨씬 명확해집니다.
CI에서 이미지 태그 자동 갱신 — GitHub Actions와 설정 리포 연동
CI 파이프라인에서 이미지를 빌드한 뒤, 설정 리포의 이미지 태그를 자동으로 업데이트하는 단계입니다. 이 커밋이 my-app-config 리포에 푸시되는 순간 ArgoCD가 감지하고 Rollout이 시작됩니다.
# .github/workflows/deploy.yml (앱 코드 리포에 위치)
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push image
run: |
# 레지스트리 주소를 포함한 전체 이미지 이름을 사용합니다
docker build -t ghcr.io/myorg/my-app:${{ github.sha }} .
docker push ghcr.io/myorg/my-app:${{ github.sha }}
- name: Checkout config repo
uses: actions/checkout@v4
with:
repository: myorg/my-app-config
token: ${{ secrets.CONFIG_REPO_TOKEN }}
path: config-repo
- name: Update image tag in config repo
run: |
cd config-repo
# 주의: sed로 직접 교체하는 방식은 개념 전달용 예시입니다.
# 실무에서는 Kustomize의 images 필드나 Helm values 업데이트를 권장합니다.
# sed 방식은 이미지 태그 형식이 달라지면 바로 깨질 수 있습니다.
sed -i "s|image: ghcr.io/myorg/my-app:.*|image: ghcr.io/myorg/my-app:${{ github.sha }}|" k8s/rollout.yaml
git config user.email "actions@github.com"
git config user.name "GitHub Actions"
git add k8s/rollout.yaml
git commit -m "chore: deploy ${{ github.sha }}"
# main 브랜치에 다른 커밋이 먼저 들어오면 push가 실패할 수 있습니다.
# 실무에서는 git pull --rebase 후 재시도하거나
# peter-evans/create-pull-request 액션을 활용하는 것이 안전합니다.
git push이 흐름의 핵심은 앱 코드 리포(my-app)와 설정 리포(my-app-config)를 분리하는 것입니다. 앱 코드 변경 → 이미지 빌드 → 설정 리포의 이미지 태그 업데이트 → ArgoCD 감지 → Rollout 시작. 이 분리 덕분에 배포 이력이 설정 리포에 깔끔하게 남고, ArgoCD도 설정 리포만 바라보면 됩니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 완전한 GitOps 일관성 | 배포 전략 포함 모든 것이 Git에 선언됩니다. PR 리뷰가 곧 배포 리뷰가 됩니다. |
| 메트릭 기반 자동 승격 | Prometheus, Datadog, New Relic 등을 배포 게이트로 직접 활용할 수 있습니다. |
| 자동 롤백 | AnalysisRun 실패 시 카나리 가중치를 즉시 0으로 복원합니다. 사람이 모니터링하지 않아도 됩니다. |
| 세밀한 단계 제어 | 트래픽 비율, 대기 시간, 분석 조건을 단계별로 명시적으로 선언할 수 있습니다. |
| 수동 승인 게이트 | pause: {}로 무기한 대기 → 중요한 배포에서는 사람이 직접 승격 결정을 내릴 수 있습니다. |
| ArgoCD UI 통합 | Rollout 진행 상태, 카나리 단계, AnalysisRun 결과를 대시보드에서 실시간으로 확인할 수 있습니다. |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| Deployment 마이그레이션 부담 | 기존 Deployment를 Rollout으로 교체해야 합니다. 기존 HPA, 모니터링 연동도 함께 검토해야 합니다. |
트래픽이 가장 적은 서비스 하나부터 시작해 팀이 패턴에 익숙해지면 나머지를 전환합니다. |
| 롤백이 Git에 반영되지 않음 | 자동 롤백 발생 시 클러스터 상태가 Git 상태와 달라집니다. ArgoCD가 다시 sync를 시도하면 같은 신버전으로 재배포가 시작될 수 있습니다. | 롤백 발생 시 Slack webhook으로 알림을 받고, 담당자가 설정 리포의 이미지 태그를 이전 값으로 즉시 되돌리는 프로세스를 팀 내 합의해둡니다. |
| 운영 복잡도 증가 | ArgoCD + Argo Rollouts + Ingress Controller + Prometheus를 모두 운영해야 합니다. | AnalysisTemplate 없이 트래픽 이동만 먼저 구성하고, 운영이 익숙해지면 분석 단계를 추가하는 방식으로 단계적으로 접근합니다. |
| 트래픽 라우팅 인프라 사전 필요 | Nginx Ingress, Istio, Linkerd 중 하나가 사전에 구성되어 있어야 가중치 기반 라우팅이 됩니다. | 대부분의 팀이 이미 Nginx Ingress를 쓰고 있어 기존 스택을 그대로 활용하면 됩니다. |
| DB 스키마 호환성 | 카나리와 스테이블 버전이 동시에 실행되므로 두 버전 모두 같은 DB 스키마를 읽어야 합니다. | Expand/Contract 패턴으로 선행 처리 — 새 컬럼을 먼저 추가하고, 이전 버전이 완전히 사라진 뒤에 구 컬럼을 제거합니다. |
Expand/Contract 패턴: 새 컬럼이나 테이블을 먼저 추가(Expand)하고 두 버전이 공존한 뒤, 이전 버전이 사라지면 오래된 스키마를 제거(Contract)하는 DB 마이그레이션 방식. 카나리 배포처럼 두 버전이 동시에 실행될 때 하위 호환성을 유지하기 위해 필요합니다.
실무에서 가장 흔한 실수
-
canaryService와stableService를 생성하지 않고 Rollout을 적용하는 경우. Argo Rollouts는 트래픽 분리를 위해 두 Service를 반드시 사전에 생성해두어야 합니다. 없으면 컨트롤러가 에러를 내뿜고 카나리가 시작되지 않습니다. 저도 처음 세팅할 때 이 부분을 빠뜨려서ProgressDeadlineExceeded에러를 한참 붙잡고 있었습니다. -
AnalysisTemplate의
interval을 너무 짧게 설정하는 경우. 30초 간격으로 Prometheus를 조회하면 샘플이 부족해서 통계적으로 의미 없는 결과가 나옵니다. 1~2분 간격을 출발점으로 삼고 트래픽 규모에 맞게 조정하는 것이 좋습니다. -
자동 롤백 후 Git 상태를 방치하는 경우. 롤백이 발생해도 설정 리포의 이미지 태그는 신버전을 가리킨 채로 남아 있습니다. ArgoCD가 다시 sync를 시도하면 같은 신버전으로 재배포가 시작될 수 있습니다. 롤백 발생 알림과 설정 리포 복구 절차를 미리 팀 내에 공유해두는 것이 중요합니다.
마치며
ArgoCD와 Argo Rollouts를 조합하면 Git 머지라는 단순한 행위 하나가 안전하고 자동화된 카나리 배포 전체를 구동하는 트리거가 됩니다. 처음 설정하는 데 시간이 들지만, 한 번 궤도에 오르면 주말 배포를 꺼리지 않게 되고, 새벽에 메트릭을 보며 가중치를 손으로 올리는 일 없이 시스템이 이상 감지와 롤백을 처리합니다.
지금 바로 시작해볼 수 있는 3단계:
-
Argo Rollouts 컨트롤러를 클러스터에 설치해볼 수 있습니다.
kubectl create namespace argo-rollouts로 네임스페이스를 만든 뒤, 공식 문서에서 최신 install.yaml을 확인해 적용하면 됩니다. (설치 명령어는 버전마다 달라질 수 있으니 공식 문서에서 최신 버전을 확인하는 것을 권장합니다.) kubectl 플러그인(kubectl argo rollouts)도 함께 설치하면kubectl argo rollouts get rollout my-app --watch로 카나리 진행 상황을 터미널에서 실시간으로 볼 수 있습니다. -
기존 Deployment 하나를 골라 Rollout으로 교체해볼 수 있습니다.
apiVersion,kind를 바꾸고strategy.canary.steps에setWeight: 10→pause: {duration: 2m}→ 컨트롤러 자동 승격처럼 단순한 3단계부터 시작하면 됩니다. AnalysisTemplate 없이 트래픽 이동만 먼저 확인해보는 것이 좋습니다. -
Prometheus 메트릭이 준비되어 있다면 AnalysisTemplate을 붙여볼 수 있습니다. 처음에는
successCondition: result[0] >= 0.90처럼 여유 있는 기준으로 시작하고, 운영 데이터를 보면서 점진적으로 기준을 높이면 됩니다.
참고 자료
- Argo Rollouts 공식 문서
- Argo Rollouts Analysis & Progressive Delivery 공식 가이드
- ArgoCD 공식 문서
- How to Implement Canary Deployments with ArgoCD and Argo Rollouts | oneuptime
- How to Use Argo Rollouts with ArgoCD for Progressive Delivery | oneuptime
- How to Handle Canary Analysis with ArgoCD and Prometheus | oneuptime
- Automated Promotion Pipeline with Argo CD, Argo Rollouts, and GitHub Actions | Medium
- Automating Blue-Green & Canary Deployments with Argo Rollouts | Akuity
- Progressive Delivery with Argo Rollouts: Canary with Analysis | InfraCloud
- Implementing GitOps and Canary Deployment with Argo Project and Istio | Tetrate
- Automating Canary Analysis in Spring Boot Using Prometheus + Argo Rollouts | Medium
- Argo Rollouts GitHub Repository