개념
GitOps에서 배포는 커밋이다
GitOps는 원하는 운영 상태를 Git 저장소에 선언하고, 클러스터 안의 에이전트가 그 선언에 클러스터를 맞추는 방식이다. Argo CD가 대표적인
도구다. 사람이나 CI가 클러스터에 직접 kubectl apply 하지 않는다. 배포 저장소(환경별 매니페스트·헬름 values)에 PR을 올리고, 병합되면 Argo CD가
그 차이를 감지해 동기화한다.
앱 저장소 PR → CI(빌드·테스트·이미지 푸시) → 배포 저장소에 이미지 태그 변경 PR
→ 리뷰·검증 → 병합 → Argo CD 가 차이 감지 → 동기화 → 헬스 판정
→ (점진 배포면) 카나리 단계별 분석 → 전체 전환 또는 중단QA에게 이 흐름은 좋은 소식이다. 모든 배포가 PR로 남고, 모든 상태가 선언과 비교 가능 하기 때문이다. 모듈 38 레슨 5의 "판정은 근거와 함께 기록으로 남는다"가 Git 이력으로 자연스럽게 생긴다. 대신 검증을 심을 지점이 CI 한 곳이 아니라 흐름 전체에 흩어진다.
| 지점 | 질문 | 이 레슨 |
|---|---|---|
| 배포 저장소 PR | 이 매니페스트 변경이 안전한가 | 매니페스트 검증 |
| 동기화 직후 | 선언대로 적용됐는가, 리소스가 건강한가 | 동기화·헬스 판정 |
| 동기화 완료 후 | 실제로 동작하는가 | PostSync 훅 스모크 |
| 점진 배포 중 | 새 버전이 운영 트래픽에서 기준을 지키는가 | 분석 게이트 |
| 배포 이후 | 누군가 클러스터를 직접 바꾸지 않았는가 | 드리프트 감시 |
| 문제 발생 시 | 되돌리기가 동작하는가 | 롤백 검증 |
배포 저장소 PR — 매니페스트도 코드다
배포 저장소의 PR은 이미지 태그 한 줄처럼 보여도 운영을 바꾼다. PR 단계에서 결정적으로 검사할 것들이 있다.
| 검사 | 잡는 것 |
|---|---|
| 렌더링 검사 | 헬름·Kustomize 렌더링이 실패하지 않는가, 렌더링 결과 diff |
| 스키마 검사 | 렌더링된 매니페스트가 대상 Kubernetes 버전의 API 스키마와 CRD 스키마를 지키는가 |
| 제거 예정 API | 고객 클러스터 버전에서 사라진 API를 쓰지 않는가 (레슨 2) |
| 정책 검사 | 리소스 요청·한도 누락, 특권 컨테이너, latest 태그, GPU 요청 없는 GPU 노드 톨러레이션 |
| 환경 간 차이 | 스테이징과 운영 values의 의도하지 않은 차이 |
| 이미지 출처 | 태그가 CI가 검증을 통과시킨 이미지 다이제스트와 일치하는가 |
마지막 줄이 GitOps에서 QA가 가장 먼저 챙길 연결이다. 배포 저장소 PR의 이미지가 모듈 38 레슨 4의 회귀·릴리스 전 층을 통과한 그 이미지인가. 태그는 덮어쓸 수 있으므로 다이제스트로 확인한다. 게이트 결정 기록(모듈 38 레슨 5)의 후보 식별자와 배포 PR의 다이제스트가 일치하지 않으면 PR을 막는다. 이 한 줄이 "검증한 것과 배포한 것이 다르다"는 사고를 막는다.
동기화 상태와 헬스 상태는 다른 질문이다
Argo CD는 애플리케이션마다 두 상태를 따로 보여준다. 이 둘을 섞으면 판정이 틀린다.
| 동기화 상태 (sync status) | 헬스 상태 (health status) | |
|---|---|---|
| 질문 | 클러스터가 Git 선언과 같은가 | 리소스가 건강하게 동작하는가 |
| 값 | Synced / OutOfSync | Healthy / Progressing / Degraded / Suspended / Missing / Unknown |
| 오해 | Synced = 성공 | Healthy = 기능 정상 |
Synced 는 "선언한 대로 적용했다"일 뿐이다. 선언 자체가 틀렸거나 파드가 계속 재시작해도 Synced일 수 있다. Healthy 는 리소스 유형별 헬스
판정이 통과했다는 뜻이다. Deployment라면 원하는 레플리카가 준비됐다는 뜻이지, API가 올바른 답을 준다는 뜻은 아니다.
애플리케이션 전체의 헬스는 직계 하위 리소스 중 가장 나쁜 상태 로 정해진다. 우선순위는 Healthy, Suspended, Progressing, Missing, Degraded, Unknown 순으로 뒤로 갈수록 나쁘다. 그래서 판정 기준은 이렇게 쓴다.
배포 판정 1단계 (결정적)
- sync.status == Synced 이고 sync.revision == 배포 PR 병합 커밋
- health.status == Healthy
- 두 조건이 동기화 시작 후 N분 안에 동시에 성립
- Progressing 체류 N분 초과는 실패 (Progressing 에 영원히 멈추는 경우)sync.revision 확인은 레슨 2의 observedGeneration 확인과 같은 역할이다. 이전 커밋 기준으로 Synced·Healthy였던 상태를 보고 통과하는 것을
막는다.
커스텀 리소스는 헬스 판정을 직접 가르쳐야 한다
Argo CD는 Deployment·StatefulSet·Service·Ingress·Job·PVC 같은 표준 리소스의 헬스 판정을 내장하고 있다. 플랫폼이 만든 커스텀 리소스 (학습 작업, 추론 엔드포인트, GPU 노드 풀)는 기본적으로 헬스를 모른다. 판정 규칙이 없으면 그 리소스는 헬스 집계에 제대로 반영되지 않고, 애플리케이션은 커스텀 리소스가 실패해도 Healthy로 보일 수 있다.
Argo CD는 argocd-cm 설정에 리소스 종류별 Lua 헬스 체크를 정의하게 해 준다. QA가 볼 것은 Lua 문법이 아니라 판정 규칙이 레슨 2의 기준과
같은가 다.
-- resource.customizations.health.serving.example.com_InferenceEndpoint
hs = { status = "Progressing", message = "waiting for status" }
if obj.status ~= nil and obj.status.observedGeneration == obj.metadata.generation then
for _, c in ipairs(obj.status.conditions or {}) do
if c.type == "Ready" and c.status == "True" then
hs.status = "Healthy"; hs.message = c.message
elseif c.type == "Ready" and c.status == "False" and c.reason == "ModelLoadFailed" then
hs.status = "Degraded"; hs.message = c.message
end
end
end
return hs이 헬스 체크 자체도 시험 대상이다. observedGeneration을 확인하지 않는 헬스 체크를 쓰면 GitOps 흐름 전체가 레슨 2의 거짓 통과를 물려받는다.
커스텀 헬스 체크마다 Healthy·Progressing·Degraded를 만드는 status 예시로 단위 시험 을 만든다. ignore-healthcheck 주석으로 헬스 집계에서
빠진 리소스 목록도 검토 대상이다. 빠질 이유가 없는 리소스가 빠져 있으면 배포가 조용히 Healthy가 된다.
PostSync 훅 — 동기화 뒤의 스모크 테스트
Argo CD 동기화는 PreSync, Sync, PostSync 단계로 진행되고, 실패 시 SyncFail 훅이 돈다. PostSync 훅은 Sync 단계가 성공하고 모든 리소스가 Healthy가 된 뒤에 실행 되고, 훅이 실패하면 동기화 전체가 실패로 표시된다. 이 성질 덕분에 배포 직후 스모크 테스트를 동기화 결과에 묶을 수 있다.
apiVersion: batch/v1
kind: Job
metadata:
name: smoke-inference-api
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 0
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: Never
containers:
- name: smoke
image: registry.internal/qa/smoke-runner@sha256:...
args: ["--suite", "inference-smoke", "--criteria-version", "infer-2026.08.4",
"--record-to", "s3://qa-evidence/deploy/"]설계 포인트를 짚자.
backoffLimit: 0— 스모크가 실패했는데 재시도로 통과하면 플래키를 숨긴다(모듈 38 레슨 6). 재시도가 필요하면 스모크 안에서 시도 기록과 함께activeDeadlineSeconds— 멈춘 스모크가 동기화를 무한정 붙잡지 않게- 삭제 정책
BeforeHookCreation— 실패한 Job이 남아 있어야 로그를 볼 수 있다. 성공 즉시 지우는HookSucceeded는 증거를 없앤다. 판정 레코드를 외부에 따로 남긴다면 괜찮다 - 이미지는 다이제스트로 고정 — 스모크 러너가 조용히 바뀌면 판정 기준이 바뀐다
- 판정 레코드를 모듈 38 레슨 4 형식으로 외부에 남긴다 — 운영 배포의 판정 근거
스모크의 범위는 짧고 결정적 이어야 한다. 몇 분 안에 끝나고, 운영 데이터를 바꾸지 않으며(또는 전용 테스트 테넌트만), 실패하면 명백히 배포 문제인 것만. 긴 검증은 분석 게이트나 배포 후 모니터링으로 보낸다.
Argo Rollouts 분석 게이트 — 카나리 단계마다 지표로 판정
전체 트래픽을 한 번에 새 버전으로 넘기지 않고 조금씩 옮기는 점진 배포(카나리)에서는 각 단계에서 계속 갈지 를 판정해야 한다. Argo Rollouts는 이 판정을 AnalysisTemplate으로 선언한다.
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: inference-canary-slo
spec:
args:
- name: service
metrics:
- name: error-rate
interval: 2m
count: 5
successCondition: "result[0] < 0.01"
failureCondition: "result[0] >= 0.02"
failureLimit: 1
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{service="{{args.service}}",version="canary",code=~"5.."}[2m]))
/ sum(rate(http_requests_total{service="{{args.service}}",version="canary"}[2m]))
- name: ttft-p95
interval: 2m
count: 5
successCondition: "result[0] < 0.8"
failureLimit: 1
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
histogram_quantile(0.95, sum by (le) (rate(ttft_seconds_bucket{service="{{args.service}}",version="canary"}[2m])))# Rollout 의 카나리 단계
strategy:
canary:
steps:
- setWeight: 5
- analysis: { templates: [ { templateName: inference-canary-slo } ], args: [ { name: service, value: llm-a } ] }
- setWeight: 25
- analysis: { templates: [ { templateName: inference-canary-slo } ], args: [ { name: service, value: llm-a } ] }
- setWeight: 100분석은 세 결과를 낸다. 성공 이면 다음 단계로, 실패 면 롤아웃을 즉시 중단하고 안정 버전으로 트래픽을 되돌리며, 판정 불가(inconclusive) 면 멈추고 사람의 결정을 기다린다. 성공 조건도 실패 조건도 만족하지 않는 측정이 판정 불가가 된다. 위 예에서 오류율이 1%와 2% 사이면 성공도 실패도 아니다. 모듈 38 레슨 3의 판정 불가(abstain)와 같은 설계다.
분석 게이트에서 QA가 챙길 함정은 다음과 같다.
| 함정 | 증상 | 대응 |
|---|---|---|
| 트래픽이 너무 적다 | 5% 단계에서 요청 수십 건, 오류율 판정이 우연에 좌우 | 최소 요청 수 조건을 성공 조건에 포함, 부족하면 판정 불가로 |
| 데이터 없음 | 쿼리 결과가 비어 있어 조건 평가 오류 | 빈 결과를 성공으로 처리하지 않는지 확인. 연속 오류 한도 설정 |
| 기준선 없는 절대값 | 원래 오류율이 1.5%인 서비스에 1% 기준 | 안정 버전과 비교 하는 쿼리로 |
| 측정 창이 짧다 | 메모리 누수·캐시 워밍 이후 문제를 못 봄 | 분석 기간을 알려진 결함 발현 시간 이상으로 |
| 사용자 영향 지표가 아니다 | CPU 사용률로 판정 | 모듈 14 레슨 7의 SLI(오류율·지연)로 |
| 분석 템플릿 변경이 리뷰되지 않는다 | 조건을 느슨하게 바꿔 통과 | 템플릿을 게이트 정책처럼 소유권·완화 승인 |
"데이터 없음"은 실제 사고로 자주 이어진다. 카나리 파드에 지표 라벨이 빠져 쿼리가 비었는데 그 결과가 성공으로 평가되면, 아무것도 측정하지 않은 채 100%까지 진행된다. 분석 템플릿을 새로 만들거나 바꿀 때 "지표가 비어 있는 카나리"를 일부러 만들어 결과를 확인한다.
드리프트 감시와 자동 복구
누군가 장애 대응 중 클러스터에서 직접 설정을 바꾸면 클러스터가 Git 선언과 달라진다(OutOfSync). 자동 동기화와 자가 치유(self-heal)를 켜면 Argo CD가 되돌리고, 끄면 차이만 표시한다.
QA 관점에서 드리프트는 두 가지 검증 질문이다.
- 운영 환경이 검증한 선언과 같은가. OutOfSync로 오래 남은 애플리케이션은 "검증한 것과 운영 중인 것이 다르다"는 뜻이다. 게이트 요약에 OutOfSync 애플리케이션 수와 체류 시간을 올린다
- 자가 치유가 의도한 대로 동작하는가. 스테이징에서 하위 리소스를 직접 수정하고 되돌려지는 시간, 의도적으로 무시하도록 설정한 필드 (예: HPA가 바꾸는 replicas)가 되돌려지지 않는지 확인한다
롤백 검증
GitOps에서 롤백은 대개 배포 저장소의 되돌림 커밋 이다. 그래서 롤백도 배포와 같은 흐름을 탄다. 검증할 것은 되돌림 자체보다 되돌린 뒤의 상태 다.
| 기준 | 이유 |
|---|---|
| 되돌림 커밋 후 Synced·Healthy 도달 시간 | 장애 중에는 이 시간이 곧 복구 시간 |
| 새 버전이 만든 데이터·스키마와 이전 버전 호환 | 이전 버전이 새 스키마를 못 읽으면 롤백이 또 다른 장애 |
| 새 버전이 만든 커스텀 리소스 필드 | 이전 오퍼레이터가 모르는 필드를 만나면 어떻게 되는가 |
| 카나리 중단 후 안정 버전 트래픽 100% 복귀 | 분석 실패 시 자동 중단이 실제로 트래픽을 옮기는가 |
| PostSync 스모크가 롤백에도 돈다 | 롤백도 배포다 |
모듈 38 레슨 4의 릴리스 전 층 "롤백 리허설"이 GitOps에서는 배포 저장소에 되돌림 PR을 실제로 올리고 병합하는 리허설 이 된다.
실무에서 왜 필요한가
자격요건의 "GitOps(ArgoCD 등) 기반 배포 흐름에 참여해 본 경험"은 Argo CD 화면을 본 경험이 아니라, 이 흐름의 어디에 어떤 검증을 심을 수 있는지 아는 것을 요구한다. GitOps 조직에서 QA가 배포 흐름을 모르면 검증이 CI에서 끝나고, 배포 이후는 SRE의 영역으로 남는다. 그러면 "검증한 이미지와 배포한 이미지가 같은가", "카나리 분석이 아무것도 측정하지 않은 채 통과하지 않았는가" 같은 질문을 아무도 하지 않는다.
반대로 QA가 배포 흐름을 알면 모듈 38의 게이트가 배포 자체와 연결 된다. 게이트 결정의 후보 다이제스트가 배포 PR에서 확인되고, 스모크와 분석 결과가 판정 레코드로 남고, 롤백까지 같은 기록 사슬에 들어간다. 채용 공고의 "배포는 게이트를 지나며, 판정은 근거와 함께 기록으로 남습니다"가 GitOps 위에서 구현되는 방식이다.