본문으로 건너뛰기
고급약 40분

GitOps·Argo CD 배포 흐름에서 QA — 동기화·상태 판정, 훅 스모크, 점진 배포 분석 게이트

GitOps에서는 배포가 Git 커밋이고, Argo CD가 클러스터를 그 커밋에 맞춘다. QA가 이 흐름에 검증을 심는 지점 — 매니페스트 PR 검증, 동기화 상태와 헬스 상태의 구분, 커스텀 헬스 체크, PostSync 훅 스모크 테스트, Argo Rollouts 분석 템플릿으로 카나리 단계마다 지표 판정, 환경 승격과 드리프트 감시, 롤백 검증 — 을 다룬다.

  • #GitOps
  • #ArgoCD
  • #ArgoRollouts
  • #카나리배포
  • #배포게이트
  • #헬스체크

개념

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 / OutOfSyncHealthy / 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 위에서 구현되는 방식이다.

실습 과제

과제 1 — 상태 판정 (10분)

아래 Argo CD 애플리케이션 상태 각각을 배포 성공·실패·판정 보류로 판정하고 이유를 적는다.

A. Synced(revision=병합 커밋) / Healthy / PostSync 훅 성공
B. Synced(revision=이전 커밋) / Healthy
C. Synced(revision=병합 커밋) / Progressing 22분 (기준 10분)
D. Synced(revision=병합 커밋) / Healthy / PostSync 훅 실패
E. OutOfSync / Healthy, 커스텀 리소스 InferenceEndpoint 는 ignore-healthcheck 주석

과제 2 — 커스텀 헬스 체크 시험 (20분)

본문의 Lua 헬스 체크에 대해 Healthy·Progressing·Degraded와 경계(observedGeneration 불일치, conditions 없음, Ready 조건의 reason이 목록에 없음)를 만드는 status 예시 여섯 개와 기대 결과를 표로 쓴다. 규칙의 빈틈이 보이면 수정안을 제시한다.

과제 3 — 분석 템플릿 검토 (20분)

본문의 inference-canary-slo 템플릿을 검토한다. 최소 요청 수 조건, 안정 버전과의 비교, 빈 결과 처리 측면에서 수정안을 쓰고, 수정안이 정말 빈 결과를 막는지 확인할 시험 방법을 적는다.

과제 4 (선택) — 이미지 출처 연결 (15분)

모듈 38 레슨 5의 게이트 결정 기록과 배포 저장소 PR을 잇는 검사를 설계한다. 어떤 값을 어디서 읽어 무엇과 비교하고, 불일치 시 어떻게 막는지 의사 코드로 쓴다.

자가 체크리스트

  • GitOps 배포 흐름에서 검증을 심을 여섯 지점을 말할 수 있다
  • 배포 저장소 PR에서 결정적으로 검사할 항목과 이미지 다이제스트 확인의 의미를 안다
  • 동기화 상태와 헬스 상태의 차이, 애플리케이션 헬스 집계 방식을 설명할 수 있다
  • sync revision을 확인해야 하는 이유를 안다
  • 커스텀 리소스에 헬스 체크가 필요한 이유와, 헬스 체크 자체를 시험하는 법을 안다
  • PostSync 훅으로 스모크 테스트를 동기화 결과에 묶고, 재시도·기한·삭제 정책을 설계할 수 있다
  • AnalysisTemplate의 성공·실패·판정 불가 결과와 카나리 단계에서의 동작을 설명할 수 있다
  • 분석 게이트의 함정(적은 트래픽·빈 데이터·절대값 기준·짧은 창)을 알아보고 대응할 수 있다
  • 드리프트와 자가 치유를 검증 질문으로 바꿀 수 있다
  • GitOps 롤백에서 되돌린 뒤의 상태를 검증 기준으로 쓸 수 있다

흔한 실수

Synced를 배포 성공으로 읽는다. 선언대로 적용됐을 뿐, 동작한다는 뜻이 아니다.

이전 리비전의 Healthy를 본다. sync revision이 병합 커밋인지 확인한다.

커스텀 리소스의 헬스 체크를 만들지 않는다. 가장 중요한 리소스가 헬스 집계에서 빠진다.

스모크 훅에 재시도를 건다. 플래키가 동기화 성공 뒤에 숨는다.

분석 쿼리가 비었을 때를 시험하지 않는다. 아무것도 측정하지 않고 100%까지 간다.

롤백을 되돌림 커밋으로 끝난 것으로 본다. 새 스키마·새 필드를 이전 버전이 읽을 수 있는지가 핵심이다.

참고 자료

낯선 말 풀이

이 레슨에 나온 약어입니다. 본문에서도 처음 나올 때 점선으로 표시해 두었습니다 (제목과 링크 글자는 그대로 둡니다).

CI — 지속적 통합
코드를 올릴 때마다 자동으로 합쳐서 빌드하고 시험까지 돌리는 방식.
PR — 풀 리퀘스트
"내가 고친 코드를 본체에 합쳐 달라"고 올리는 요청. 합치기 전에 사람들이 보고 의견을 단다.
QA — 품질보증
만든 것이 쓸 만한지 확인하고, 애초에 덜 망가지게 만드는 방법을 정하는 일. "테스트"는 그 일의 일부다.
API — 응용 프로그램 인터페이스
프로그램끼리 주고받는 창구. 사람이 화면 버튼을 누르는 대신, 프로그램이 정해진 주소로 요청을 보내 답을 받는다.
컨테이너 — 컨테이너
이 커리큘럼에서 두 가지로 쓰인다. ① 프로그램과 그것을 돌리는 데 필요한 것들을 한 상자에 담아, 어느 컴퓨터에서나 똑같이 돌아가게 만든 것 ② 화면에서 여러 요소를 하나로 묶는 바깥 틀. 서버·배포 이야기면 ①, 화면·접근성 이야기면 ②다.
GPU — 그래픽처리장치
비슷한 계산을 한꺼번에 많이 하는 데 특화된 부품. 그림과 기계학습에 쓰인다.
테넌트 — 입주자
한 시스템을 여러 고객사가 나눠 쓸 때, 그 고객사 하나하나를 가리키는 말. 건물의 입주자에 해당한다.
CPU — 중앙처리장치
계산을 담당하는 부품. 사람으로 치면 머리다.
SLI — 서비스 수준 지표
실제로 재는 숫자 — 응답 시간, 성공률 같은 것.
SRE — 사이트 신뢰성 엔지니어링
서비스가 안 죽게 만드는 일을 개발처럼 코드로 하는 직무와 방식.
SLO — 서비스 수준 목표
내부적으로 지키려는 목표치. 고객과의 약속보다 조금 더 엄격하게 잡는다.

용어집 전체 보기