GitOps와 Argo CD 기반 선언적 배포 아키텍처

· 약 13분 읽기 · 조회 2 #GitOps#Argo CD#Kubernetes#배포 자동화#CI/CD

운영 클러스터에 무엇이 떠 있어야 하는지를 Git 저장소 하나에 적어 두고, 클러스터 안의 에이전트가 그 상태를 계속 맞추게 하는 방식이 GitOps다. 이 글은 OpenGitOps 4원칙에서 출발해 저장소 분리, Argo CD의 Application·AppProject 설정, 동기화 정책, 이미지 갱신·비밀값·멀티 클러스터·롤백까지 실제로 구성할 때 정해야 할 것을 순서대로 정리한다.

항해사가 탁자 위 해도의 항로를 짚으며 창밖 항구를 보고 키를 맞추는 조타실

한눈에 보기

구성요소 맡는 일 정하는 것
앱 저장소 소스와 Dockerfile 빌드·테스트 규칙
CI 파이프라인 이미지 빌드, 레지스트리 push, 설정 저장소에 태그 커밋 클러스터 접근 권한을 갖지 않는다
설정 저장소 환경별 매니페스트(Kustomize, Helm values) 운영의 원하는 상태(desired state)
Argo CD 설정 저장소를 당겨 와 실제 상태와 비교·동기화 동기화 정책, 접근 범위
클러스터 실제 상태(live state) 사람이 직접 바꾸지 않는다

CI가 이미지를 올리고 설정 저장소에 태그를 커밋하면 클러스터 안의 Argo CD가 이를 당겨 와 리소스를 동기화하고, 롤백은 git revert로 하는 흐름

OpenGitOps 4원칙

CNCF 산하 OpenGitOps 프로젝트는 GitOps를 네 가지 원칙으로 정의한다.

  1. 선언적(Declarative): 시스템의 원하는 상태를 절차가 아니라 결과로 적는다. "Pod 3개를 띄워라"가 아니라 replicas: 3이다.
  2. 버전 관리·불변(Versioned and Immutable): 원하는 상태는 이력이 남는 저장소에 두고 덮어쓰지 않는다. 누가 언제 무엇을 바꿨는지가 커밋으로 남는다.
  3. 자동으로 당겨 옴(Pulled Automatically): 에이전트가 저장소에서 원하는 상태를 스스로 가져온다.
  4. 지속적 조정(Continuously Reconciled): 에이전트가 실제 상태를 계속 관찰하고 원하는 상태와 다르면 맞춘다.

네 가지 중 하나라도 빠지면 GitOps라기보다 "Git에 YAML을 보관하는 CI 배포"에 가깝다. 특히 4번이 없으면 누군가 kubectl edit으로 바꾼 값이 저장소와 어긋난 채 남는다.

Push 방식과 Pull 방식

전통적인 CI/CD는 파이프라인이 마지막 단계에서 kubectl apply나 helm upgrade를 실행한다(push). GitOps는 클러스터 안의 Argo CD가 저장소를 보고 스스로 적용한다(pull).

항목 Push (CI가 배포) Pull (Argo CD가 배포)
클러스터 자격 증명 CI 서버가 보관 클러스터 안에만 존재
드리프트 감지 없음, 다음 배포 때 덮어씀 계속 비교해 표시·복구
배포 이력 파이프라인 로그 Git 커밋
방화벽 CI → 클러스터 인바운드 필요 클러스터 → Git 아웃바운드만

Pull 방식의 가장 큰 이점은 CI가 운영 클러스터 권한을 갖지 않는다는 점이다. CI가 털려도 공격자가 얻는 것은 설정 저장소 쓰기 권한이고, 그 변경은 리뷰와 커밋 이력을 거친다. 폐쇄망에서 저장소와 레지스트리를 내부에 두는 구성은 망분리 환경의 Maven·npm 의존성 공급망 보안에서 다뤘다.

앱 저장소와 설정 저장소를 나누는 이유

소스 코드와 배포 매니페스트를 한 저장소에 두면 처음엔 편하지만 세 가지가 꼬인다.

  • 이미지 태그를 바꾸는 커밋이 다시 CI를 돌려 빌드가 반복된다.
  • 레플리카 수 하나 바꾸는 운영 변경에도 앱 저장소 쓰기 권한이 필요하다.
  • 운영 롤백(git revert)이 소스 이력과 섞인다.

그래서 보통 설정 저장소를 따로 두고 환경을 디렉터리로 나눈다.

deploy-config/
├── base/order-api/          # Deployment, Service, HPA
└── overlays/
    ├── dev/order-api/       # kustomization.yaml: 이미지 태그, replicas
    ├── stg/order-api/
    └── prod/order-api/

Application과 AppProject

Argo CD에서 배포 단위는 Application이다. "이 저장소의 이 경로를 저 클러스터의 이 네임스페이스에 맞춰라"를 적는다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: order-api-prod
  namespace: argocd
spec:
  project: commerce
  source:
    repoURL: https://git.example.com/platform/deploy-config.git
    targetRevision: main
    path: overlays/prod/order-api
  destination:
    server: https://kubernetes.default.svc
    namespace: order
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
    retry:
      limit: 3
      backoff:
        duration: 10s
        factor: 2
        maxDuration: 2m

AppProject는 Application이 어디서 가져와 어디에 배포할 수 있는지 울타리를 친다. 팀마다 프로젝트를 나누면 한 팀의 Application이 다른 팀 네임스페이스나 클러스터 범위 리소스를 건드리지 못한다.

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: commerce
  namespace: argocd
spec:
  sourceRepos:
    - https://git.example.com/platform/deploy-config.git
  destinations:
    - server: https://kubernetes.default.svc
      namespace: order
  clusterResourceWhitelist: []   # Namespace, ClusterRole 같은 클러스터 범위 리소스 금지

동기화 정책과 드리프트

Argo CD는 주기적으로(기본값은 argocd-cm의 timeout.reconciliation, 3분 안팎) 설정 저장소를 확인하고, Git 웹훅을 걸면 커밋 직후 바로 반영한다. 비교 결과는 Synced/OutOfSync로 표시된다.

설정 켜면 끄면
automated 저장소가 바뀌면 자동 동기화 사람이 Sync 버튼을 눌러야 함
prune 저장소에서 지운 리소스를 클러스터에서도 삭제 고아 리소스가 남음
selfHeal 클러스터에서 직접 바꾼 값을 저장소 값으로 되돌림 OutOfSync 표시만 함

처음 도입할 때는 automated만 켜고 prune·selfHeal은 끈 채 OutOfSync 목록을 관찰하는 것이 안전하다. HPA가 바꾸는 replicas처럼 정상적으로 클러스터에서 변하는 필드는 ignoreDifferences로 비교에서 빼야 selfHeal과 HPA가 서로 값을 뒤집는 일이 없다.

Sync Wave와 Hook으로 순서 정하기

한 Application 안의 리소스는 기본적으로 종류 순서대로 함께 적용된다. 순서가 필요하면 어노테이션을 붙인다.

metadata:
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
  • argocd.argoproj.io/sync-wave: "-1": 낮은 숫자부터 적용하고, 앞 wave가 Healthy가 된 뒤 다음 wave로 넘어간다. ConfigMap·Secret을 -1, 앱을 0에 둔다.
  • PreSync Hook: DB 스키마 마이그레이션 Job처럼 본 배포 전에 끝나야 하는 작업. 실패하면 동기화가 멈춘다.
  • PostSync Hook: 스모크 테스트, 배포 알림.
  • SyncFail Hook: 동기화가 실패했을 때 정리 작업.

마이그레이션은 이전 버전 앱과도 호환되게 써야 한다. 앱은 git revert로 되돌아가도 스키마는 되돌아가지 않기 때문이다.

이미지 갱신 흐름

CI가 클러스터를 건드리지 않으므로, 새 이미지를 배포하는 방법은 설정 저장소의 태그를 바꾸는 커밋이다.

# CI 마지막 단계: 설정 저장소에 새 태그를 커밋
git clone https://git.example.com/platform/deploy-config.git && cd deploy-config/overlays/prod/order-api
kustomize edit set image registry.example.com/order-api=registry.example.com/order-api:1.4.2
git commit -am "deploy: order-api 1.4.2" && git push

dev·stg는 CI가 바로 main에 push하고, prod는 같은 변경을 PR로 올려 승인 후 병합하게 하면 승인 이력이 커밋에 남는다. 태그는 latest 대신 버전이나 커밋 SHA를 쓴다. latest는 저장소 내용이 그대로라 Argo CD가 변경을 알 수 없다. 레지스트리를 감시해 태그를 대신 커밋해 주는 Argo CD Image Updater도 있지만, 별도 컴포넌트라 쓰기 전에 지원 범위를 확인한다.

비밀값 다루기

설정 저장소에 평문 Secret을 넣으면 Git 이력에 영원히 남는다. 두 가지 방식이 많이 쓰인다.

  • Sealed Secrets: kubeseal로 클러스터 공개키를 써서 암호화한 SealedSecret을 저장소에 커밋한다. 클러스터 안의 컨트롤러만 복호화할 수 있다. 클러스터를 새로 만들면 키를 백업·복원해야 한다.
  • External Secrets Operator: 저장소에는 "Vault의 이 경로를 Secret으로 만들어라"는 ExternalSecret만 두고, 값은 Vault·AWS Secrets Manager 같은 외부 저장소에서 가져온다. 값 교체가 Git 커밋 없이 일어난다.

어느 쪽이든 Argo CD UI에서 Secret 값이 보이지 않도록 RBAC을 같이 정한다.

멀티 클러스터와 ApplicationSet

클러스터가 dev·stg·prod에 리전까지 늘어나면 Application을 손으로 복사하기 어렵다. ApplicationSet은 생성기(generator)로 Application을 찍어 낸다.

  • List 생성기: 클러스터 이름과 주소를 목록으로 적는다.
  • Cluster 생성기: Argo CD에 등록된 클러스터 중 env=prod 같은 레이블이 붙은 곳마다 하나씩 만든다.
  • Git 생성기: 설정 저장소의 디렉터리마다 하나씩 만든다. 디렉터리를 추가하면 배포 대상이 생긴다.

새 리전 클러스터에 레이블만 붙이면 같은 앱 묶음이 자동으로 깔린다. 반대로 레이블을 잘못 붙이면 운영 앱이 엉뚱한 클러스터에 배포되므로, 레이블 변경도 리뷰 대상에 넣는다.

롤백은 git revert로

Argo CD UI에도 History 화면에서 이전 리비전으로 돌리는 Rollback 기능이 있다. 하지만 자동 동기화가 켜진 Application에는 쓸 수 없고, 써도 저장소는 그대로라 다음 동기화 때 다시 새 버전이 깔린다. GitOps에서 롤백은 설정 저장소에서 문제 커밋을 git revert하는 것이다. 이력이 남고, 리뷰를 거치고, 다른 환경과 같은 경로로 반영된다. 노드 단위 롤백과 백업을 다룬 공공 SI 환경의 CI/CD 배포·백업·롤백 자동화와 비교하면, GitOps는 롤백 대상이 서버가 아니라 커밋이라는 점이 다르다.

정리

무엇을 누가 바꾸나 없으면 생기는 일
설정 저장소 분리 CI(태그), 운영자(PR) 태그 커밋이 빌드를 반복 호출
AppProject 플랫폼 팀 한 팀 Application이 다른 팀 리소스 변경
selfHeal·prune 플랫폼 팀 수동 변경과 고아 리소스가 쌓임
PreSync Hook 앱 팀 스키마 없이 새 버전이 먼저 뜸
Sealed/External Secrets 보안 담당 평문 비밀값이 Git 이력에 남음

자주 하는 실수

  • latest 태그로 배포한다. 저장소가 바뀌지 않으니 Argo CD가 새 이미지를 모르고, 롤백할 버전도 특정할 수 없다.
  • 급할 때 kubectl edit으로 고친다. selfHeal이 켜져 있으면 몇 분 뒤 되돌아가고, 꺼져 있으면 저장소와 어긋난 채 잊힌다.
  • HPA가 관리하는 replicas를 그대로 비교한다. selfHeal과 HPA가 값을 번갈아 덮어써 Pod 수가 흔들린다.
  • default 프로젝트로 운영한다. 모든 저장소와 클러스터가 허용돼 실수 하나가 전 클러스터로 번진다.
  • 마이그레이션을 되돌릴 수 있다고 가정한다. git revert는 매니페스트만 되돌린다.

점검표

  • 앱 저장소와 설정 저장소가 분리돼 있고, CI는 클러스터 자격 증명이 없다.
  • 이미지 태그가 버전 또는 커밋 SHA다.
  • 운영 Application이 default가 아닌 AppProject에 속한다.
  • AppProject의 sourceRepos·destinations가 필요한 범위만 허용한다.
  • prune·selfHeal 설정을 환경별로 의도해서 정했다.
  • HPA 대상 필드가 ignoreDifferences에 있다.
  • 마이그레이션 Job이 PreSync Hook이고 이전 버전과 호환된다.
  • 저장소에 평문 Secret이 없다(git log -p로 확인).
  • prod 태그 변경이 PR 승인을 거친다.
  • 롤백 절차가 git revert 기준으로 문서화돼 있다.

GitOps와 Argo CD의 핵심은 운영을 바꾸는 길을 설정 저장소의 커밋 하나로 좁히고, 클러스터 안의 에이전트가 그 커밋과 실제 상태를 계속 맞추게 하는 것이다.

댓글 0개

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

댓글 쓰기

← 목록으로 돌아가기