대규모 쿠버네티스 플랫폼에서 GitOps 다시 생각하기

|MSA & Architecture|21분 읽기

GitOps의 달콤한 거짓말

클러스터가 50개를 넘어서기 시작하면서, GitOps 운영에서 예전엔 보이지 않던 문제들이 하나둘 튀어나오기 시작했습니다. "Git은 유일한 진실의 원천(Single Source of Truth)"이라는 말을 믿고 설계했던 시스템이, 규모가 커지자 묘하게 삐걱거리더라고요.

처음엔 도구 설정 문제라고 생각했습니다. 폴링 주기를 조정하고, 저장소를 쪼개고, 컨트롤러 리소스를 늘려봤죠. 잠깐은 나아지는 것 같았는데, 클러스터가 더 늘어나면 같은 증상이 다시 올라왔습니다. 그때 든 생각이 이거였어요. 이건 튜닝으로 풀 문제가 아니라, 전제 자체가 틀린 거 아닐까?

클라우드 네이티브 생태계에서 수년간 반복되어온 이 간단한 명제가 실제로는 얼마나 복잡한 현실을 감추고 있는지, 그리고 대규모 환경에서는 왜 다른 접근이 필요한지 정리해보려고 합니다.

상태, 의도, 현실 - 용어부터 정리하자

쿠버네티스 논의에서 '상태'라는 단어가 도처에 등장하지만, 사실 그 안에는 완전히 다른 세 가지 개념이 뒤섞여 있습니다. 이 셋을 구분하지 못하면 GitOps 논쟁 자체가 엉뚱한 데서 맴돌게 됩니다.

실제 상태 - 지금 벌어지는 현실

첫 번째는 클러스터의 실제 런타임 상태입니다. 바로 이 순간 어떤 Pod가 떠 있고, 어떤 컨테이너가 healthy한지, IP는 뭔지, 어떤 노드에 부하가 몰리는지 같은 것들이죠. 이 상태는 가만히 있는 법이 없습니다. 초 단위로 변합니다.

마지막으로 알려진 상태 - 운영 메모리

두 번째는 클러스터의 운영 메모리입니다. 쿠버네티스에서는 etcd에 저장되는 정보죠. 컨트롤러들이 끊임없이 읽고 쓰는, 클러스터가 지금 세상을 어떻게 이해하고 있는지를 담은 객체와 메타데이터입니다.

실질적으로 보면 etcd가 클러스터의 진짜 상태 저장소입니다. 운영하다 보면 결국 모든 디버깅이 여기로 수렴하더라고요.

원하는 상태 - 우리의 의도

세 번째는 원하는 상태, 즉 우리가 바라는 시스템의 모습입니다. YAML 매니페스트, Helm 차트, 커스텀 설정 같은 걸로 표현되는 것들이죠. 이 파일들은 지금 일어나는 일이 아니라, 우리가 일어나길 바라는 일을 적어둔 겁니다.

바로 여기서 Git이 등장합니다. Git은 이 매니페스트를 저장하고 의도를 버전 관리하는 자리에 들어옵니다. 문제는 이 셋을 한 덩어리로 묶어 "Git이 상태다"라고 말하는 순간 시작됩니다.

Git이 진정한 상태 저장소가 아닌 이유

진짜 상태 저장소라면 상태가 계속 기록되고 갱신되는 시스템이어야 합니다. 빈번한 읽기/쓰기를 처리하고, 런타임 정보를 담고, 시스템의 현재 모습을 알고 있어야 하죠. 데이터베이스나 분산 키-값 저장소, 쿠버네티스의 etcd 같은 것들 말입니다.

Git이 정말 상태 저장소라면, 컨트롤러들이 런타임 정보를 끊임없이 저장소에 되써야 합니다. 이른바 양방향 GitOps처럼요.

예를 들면 이런 것들이 Git에 기록돼야 할 겁니다:

  • 복제본 수가 변경되었습니다
  • Pod가 생성되었습니다
  • Pod에 IP 주소 10.1.2.3이 할당되었습니다
  • Pod가 CrashLoopBackOff 상태에 진입했습니다

하지만 이런 일은 전혀 일어나지 않습니다. Git은 런타임 변경을 기록하지 않아요. 배포가 실패했는지, Pod가 죽어가는지 알지도 못하고요. 새벽에 장애가 터졌을 때 Git 히스토리를 들여다본다고 답이 나오지 않는 이유가 여기 있습니다.

Git은 사람이나 파이프라인이 작성한 정적 선언을 저장하는 시스템입니다. 운영 메모리로는 기능할 수 없어요.

Git의 실제 역할과 한계

Git이 실제로 하는 일은 훨씬 소박합니다. 의도를 기록하는 시스템이죠. 누가 언제 설정을 바꿨고, 그 이유는 무엇인지를 남깁니다. 이것만으로도 충분히 가치 있지만, 운영 상태를 저장하는 것과는 결이 완전히 다릅니다.

게다가 그렇게 자주 인용되는 "Git이 진실의 원천이다"라는 말도, 조금만 뜯어보면 앞뒤가 맞지 않는 구석이 있습니다. 흔한 패턴에서 Git은 의도를 기록하는 자리에 있고, 정작 중요한 종속성 대부분은 간접 참조로만 걸려 있거든요.

Helm 차트는 다른 차트를 참조하고, 차트는 컨테이너 이미지를 참조하죠. 아주 단순한 매니페스트를 떠올려보세요:

image: my-app:latest
# 또는
image: my-app:3.0.1

매니페스트는 "이 애플리케이션을 배포하겠다"는 의도를 적어두지만, 실제 아티팩트는 다른 곳, 보통 컨테이너 레지스트리에 있습니다. 해당 태그 뒤의 이미지가 다시 빌드돼서 푸시되면, Git에서는 한 글자도 바뀌지 않았는데 클러스터는 전혀 다른 이미지를 가져올 수 있어요. latest 태그를 운영에 물려본 사람이라면 한 번쯤 이걸로 데어봤을 겁니다.

실제 아티팩트, 즉 진짜로 실행되는 부분은 Git 바깥에 존재합니다. 이 사실 하나가 "Git = 진실"이라는 명제를 처음부터 흔듭니다.

대규모에서 Git이 한계를 드러내는 지점

소규모 환경에서는 전통적인 GitOps가 정말 잘 돌아갑니다. 클러스터 몇 개에 애플리케이션 몇 개라면 어디서도 티가 안 나요. 그런데 플랫폼 규모가 커지면 이야기가 달라집니다.

흔한 플랫폼 환경을 상상해보죠:

  • 50개 클러스터
  • 클러스터당 5개의 플랫폼 애플리케이션
  • 총 250개의 GitOps로 제어되는 배포

각 GitOps 컨트롤러는 주기적으로(기본값은 3분마다) 소스의 변경을 확인합니다. 소스가 Git이면, 이는 각 컨트롤러가 저장소와 끊임없이 대화한다는 뜻이죠.

폴링 한 번에도 여러 단계가 딸려옵니다:

  • 인증
  • 연결 설정
  • 저장소 동기화
  • 커밋 그래프 비교

아무것도 바뀌지 않았어도 이 과정은 매번 반복됩니다. 변한 게 없다는 사실을 확인하기 위해 매번 같은 일을 하는 거죠.

최신 GitOps 도구들은 shallow clone, sparse checkout, 저장소 파티셔닝 같은 기법으로 이 오버헤드를 줄이려고 합니다. 일부 팀은 웹훅을 써서 폴링 자체를 피하려는 실험도 하죠. 이런 최적화가 도움이 안 되는 건 아닙니다. 다만 Git 프로토콜이 태생적으로 "수다스럽다"는 사실 자체는 바꾸지 못해요.

수백 개의 컨트롤러가 동시에 돌면, Git 서버에는 본질적으로 불필요한 부하가 쌓입니다. 그리고 이 부하는 어느 임계점을 넘으면 성능 문제로 모습을 바꿔서 나타나기 시작합니다. 처음엔 동기화가 가끔 느려지는 정도지만, 나중엔 배포 지연이나 타임아웃으로 번지죠.

OCI 아티팩트를 배포 계층으로 활용하기

OCI 레지스트리는 전혀 다른 결의 문제를 풀어온 시스템입니다. 바로 대규모 아티팩트 배포죠. 컨테이너 레지스트리는 애초에 수천 개의 노드를 동시에 상대하도록 만들어졌습니다. 높은 동시성과 글로벌 분산을 비상 상황이 아니라 평상시 조건으로 처리해요.

구성이 OCI 아티팩트로 패키징되면 상호작용이 한결 단순해집니다:

  1. CI 파이프라인이 매니페스트와 메타데이터를 담은 아티팩트를 빌드
  2. 그 아티팩트를 OCI 레지스트리로 푸시
  3. GitOps 컨트롤러는 아티팩트 다이제스트가 바뀌었는지만 확인
  4. 바뀌었으면 해당 아티팩트를 가져와 적용

핵심 차이는, 컨트롤러가 더 이상 저장소 히스토리를 협상하지 않는다는 점입니다. "이 다이제스트가 그대로인가?" 한 줄만 물어보면 끝이에요. 커밋 그래프를 비교할 일이 없습니다.

이 단순함 덕분에 OCI는 배포 메커니즘으로서 훨씬 효율적입니다.

다만 OCI에도 분명한 선이 있습니다. OCI 아티팩트는 불변 패키지예요. 배포에는 더없이 적합하지만, 구성이 만들어지고 다듬어지는 자리는 아닙니다. 여기서 구성을 고치려 들면 곧장 어색해져요. OCI는 배포 계층으로 이해하는 것이 가장 정확합니다.

구성 데이터 계층의 필요성

Git이 의도를 저장하고 OCI가 아티팩트를 배포한다면, 빈자리가 하나 남습니다. 실제 구성 값은 대체 어디에 있나요?

여기서 ConfigHub 같은 구성 관리 시스템이 들어옵니다. ConfigHub는 구성을 여러 저장소에 흩어진 파일 더미가 아니라, 구조화된 데이터로 다룹니다. 이게 생각보다 큰 차이를 만들어요.

이 접근은 의도적으로 WET(Write Everything Twice) 원칙을 택합니다. 사람의 작업을 굳이 중복시키자는 게 아니라, 기계가 읽을 명확한 구성을 만들어두자는 쪽이죠. DRY를 신앙처럼 떠받들다 보면, 추상화에 추상화가 겹쳐서 정작 "이 클러스터에 실제로 어떤 값이 들어가는가"를 아무도 답하지 못하는 상황이 옵니다. WET는 그 반대편에 서 있습니다.

명시적 구성의 장점

  • 배포 전에 최종 결과를 검증할 수 있습니다
  • 구성이 어떻게 짜였는지 이해하기가 쉬워집니다
  • 디버깅이 훨씬 단순해집니다

이렇게 하면 진짜 WYSIWYG 모델에 가까워집니다. 화면에서 보는 그대로가 실제 배포되는 내용이 되는 거죠. 템플릿을 머릿속으로 렌더링해가며 디버깅해본 사람이라면, 이게 왜 중요한지 바로 와닿을 겁니다.

ConfigHub는 여러 시스템의 구성을 한자리로 모읍니다:

  • Git 저장소
  • Vault 같은 시크릿 스토어
  • 데이터베이스
  • 배포 메타데이터

그리고 이 입력값들을 검증된 구성 표현으로 변환합니다. 흩어진 진실들을 하나의 질의 가능한 형태로 정리하는 셈이죠.

규제 압력도 고려해야 할 요소

확장성만이 플랫폼들이 아키텍처를 다시 들여다보는 이유는 아닙니다. 규제도 점점 무게 있는 동인으로 올라오고 있어요.

**EU 사이버 복원력법(CRA)**은 디지털 구성요소를 포함한 제품에 엄격한 보안 요구사항을 들고 들어왔습니다. 그 요구사항 상당수가 2027년까지 의무화될 예정이죠.

EU에 소프트웨어나 SaaS를 제공하는 조직은 다음을 갖춰야 합니다:

  • 소프트웨어 공급망 투명성
  • 취약점 관리
  • 소프트웨어 자재명세서(SBOM)

CRA는 시장 원칙을 따르기 때문에, EU에서 소프트웨어를 파는 기업이라면 본사가 어디에 있든 이 규정 안으로 들어옵니다. 한국 회사라고 비껴갈 수 있는 이야기가 아니에요.

Git만으로는 공급망 추적성을 온전히 보장하기 어렵습니다. 커밋 병합이나 히스토리 재작성 같은 작업이 감사 추적의 일부를 소리 없이 지워버릴 수 있거든요. "누가 봐도 안전한 히스토리"를 증명해야 하는 상황에서, force push 한 번에 흔들리는 추적성은 그 자체로 리스크입니다.

반면 OCI 아티팩트는 여러 요소를 하나의 불변 아티팩트로 묶어줍니다:

  • 배포 매니페스트
  • 컨테이너 이미지
  • SBOM
  • 서명

이렇게 묶이면 각 아티팩트가 곧 배포의 검증 가능한 스냅샷이 됩니다. 규제 대응이 운영 부담이 아니라 아키텍처 산출물로 자연스럽게 떨어지는 거죠.

현실적인 GitOps 아키텍처

오해하면 안 되는 게, 목표는 아키텍처에서 Git을 걷어내는 게 아닙니다. Git은 협업과 버전 관리 측면에서 여전히 대체하기 어려운 도구예요. 다만 그 역할을 제 자리에 두는 게 핵심입니다.

대규모를 견디는 GitOps 아키텍처는 보통 책임을 쪼갭니다:

구성요소 역할 특징
Git 의도 기록 개발자와 플랫폼 팀의 변경사항 추적
OCI 아티팩트 배포 불변 배포 아티팩트, 대규모 확장 가능
ConfigHub 구성 집계 구성 구조에 대한 쿼리 가능한 진실의 원천
Kubernetes/etcd 운영 상태 클러스터의 실제 운영 상태 저장

각 구성요소가 자기가 가장 잘하는 일만 맡습니다. Git에 운영 상태를 떠넘기지 않고, OCI에 구성 편집을 시키지 않고, etcd를 형상 관리 도구로 착각하지 않는 거죠.

이렇게 역할이 갈리면, 파일 기반 저장소로는 답하기 곤란하던 질문들에 비로소 답할 수 있게 됩니다:

  • 유럽 전역의 모든 클러스터에서 스테이징 환경에 떠 있는 애플리케이션은 뭔가?
  • securityContext가 빠진 워크로드는 어떤 건가?
  • 취약한 이미지를 참조하는 배포는 어디인가?

평소엔 굳이 안 물어봐도 되는 질문 같지만, 사고나 보안 이벤트가 터진 순간엔 이야기가 달라집니다. 그때 시스템의 정확한 상태를 몇 분 안에 짚어낼 수 있느냐 없느냐가, 장애 대응의 질을 가릅니다. 여러 저장소를 grep으로 뒤지고 있을 시간은 없거든요.

마치며

"Git은 유일한 진실의 원천"이라는 말은 GitOps 초창기엔 꽤 쓸모 있는 사고 모델이었습니다. 머릿속을 단순하게 정리해줬으니까요. 그런데 플랫폼이 커지면서, 이 단순한 명제가 가렸던 복잡함이 하나씩 드러나고 있습니다.

Git은 강력한 도구입니다. 다만 인프라의 운영 메모리 역할을 하라고 만들어진 물건은 아니에요. 이걸 상태 저장소처럼 다루는 순간, 혼란과 확장성 문제가 따라옵니다. 클러스터 50개 언저리에서 제가 겪은 게 정확히 그거였고요.

규모가 바뀌면 게임의 룰도 바뀝니다. 같은 도구라도 작은 환경에서 옳던 가정이 큰 환경에선 그대로 함정이 되곤 하죠. 향후 10년을 버틸 플랫폼을 짓는다면, 이제는 Git을 데이터베이스 취급하는 습관을 내려놓고, 구성 파일을 진짜 다뤄야 할 핵심 데이터로 보기 시작할 때라고 생각합니다.

#GitOps#쿠버네티스#OCI#클라우드네이티브#플랫폼엔지니어링