LabHub

CGOA — GitOps 인증 어소시에이트 · 저장소 구조와 환경 승격 · 이론

저장소를 어떻게 쪼갤 것인가 — 그리고 승격을 한 줄 PR 로 만드는 법

LabHub 에서 이어서 보기

한 줄 요약

GitOps 의 첫 설계 결정은 도구 선택이 아니라 저장소 경계 입니다. 앱 소스와 배포 설정을 나눌 것인가, 팀별로 저장소를 나눌 것인가, 환경을 브랜치로 나눌 것인가 디렉터리로 나눌 것인가 — 이 네 질문의 답이 이후 모든 운영 비용을 결정합니다.

왜 이게 필요했나

앱 저장소 안에 k8s/ 디렉터리를 두고 매니페스트를 함께 관리하는 게 처음에는 편합니다. 그런데 곧 이런 일이 생깁니다.

이미지 태그를 올리는 커밋이 앱 저장소에 들어가면, 그 커밋이 다시 CI 를 돌립니다. CI 는 새 이미지를 만들고, 그 태그를 또 커밋합니다. 무한 루프 입니다. 우회하려고 [skip ci] 를 붙이기 시작하면 그때부터 파이프라인이 지저분해집니다.

두 번째 문제는 권한입니다. 앱 코드에 대한 리뷰 권한과 프로덕션 배포 설정에 대한 리뷰 권한은 같은 사람 집합이 아닙니다. 같은 저장소에 있으면 CODEOWNERS 로 억지로 나눠야 하고, 실수 한 번이면 뚫립니다.

세 번째는 변경 주기입니다. 앱 코드는 하루에 열 번 바뀌고 배포 설정은 한 달에 한 번 바뀝니다. 이력이 섞이면 "이 replicas 값이 언제부터 3 이었지?" 를 찾는 데 커밋 수백 개를 헤집어야 합니다.

그래서 실무의 기본형은 앱 저장소(코드 + Dockerfile + CI)설정 저장소(매니페스트 + kustomize/Helm values) 를 나누는 것입니다. 시험에서는 이걸 separation of concerns 로 묻습니다.

어떻게 동작하나

설정 저장소를 다시 몇 개로 둘지가 모노레포 대 폴리레포 논쟁입니다.

| | 모노레포(설정 하나) | 폴리레포(팀/앱마다) |
| --- | --- | --- |
| 일관성 | 공통 베이스를 한 곳에서 강제 | 저장소마다 표준이 갈림 |
| RBAC | 디렉터리 단위라 세밀한 제어가 어려움 | 저장소 권한으로 깔끔하게 분리 |
| 원자적 변경 | 여러 앱을 한 PR 로 함께 바꿀 수 있음 | 저장소를 넘나드는 조율이 필요 |
| 에이전트 부하 | 저장소 하나를 여러 앱이 동시에 폴링 | 분산됨 |
| 적합 규모 | 팀 하나~여럿, 앱 수십 개 | 조직 여러 개, 앱 수백 개 |

환경 분리도 세 가지 방식이 있습니다.

승격을 한 줄로 만들기

좋은 구조의 판별 기준은 하나입니다. "stage 에서 잘 돌던 것을 prod 로 올리는 PR 의 diff 가 몇 줄인가?"

베이스와 오버레이를 제대로 나눴다면 답은 한 줄입니다.

# apps/checkout/overlays/prod/kustomization.yamlimages:  - name: ghcr.io/labhub/checkout    newTag: 1.5.0        # ← 1.4.0 에서 이 줄만 바뀐다

이 한 줄짜리 diff 가 리뷰어에게 주는 정보는 대단히 큽니다. "prod 로 가는 변경은 이미지 태그뿐이고 나머지 설정은 stage 와 동일하다"가 눈으로 증명되기 때문입니다. 반대로 승격 PR 에 replicas, 리소스, 환경 변수가 함께 바뀌어 있다면 그건 승격이 아니라 새 배포입니다.

여기서 태그는 불변 이어야 합니다. latest 나 브랜치명 태그를 쓰면 Git 커밋은 그대로인데 실제 실행 이미지가 달라집니다. 원하는 상태를 Git 이 완전히 결정하지 못하게 되므로 두 번째 원칙이 깨집니다.

app-of-apps

앱이 늘어나면 Argo CD Application 리소스 자체도 수십 개가 됩니다. 이걸 손으로 만들면 "배포 도구를 배포하는 절차"가 다시 수동이 됩니다. app-of-apps 는 이 재귀를 닫는 패턴입니다.

루트 Application 하나가 bootstrap/ 디렉터리를 가리키고, 그 디렉터리 안에는 자식 Application 매니페스트들이 들어 있습니다. 루트를 동기화하면 자식 Application 들이 생기고, 자식들이 각자 자기 앱을 동기화합니다. 새 앱 추가는 bootstrap/ 에 파일 하나를 커밋하는 일이 됩니다.

비슷한 목적의 ApplicationSet 과는 결이 다릅니다. app-of-apps 는 자식을 파일로 명시 하고, ApplicationSet 은 제너레이터(디렉터리 스캔, 클러스터 목록, PR 목록)로 자식을 계산 합니다. 앱 목록이 명시적이길 원하면 app-of-apps, 목록이 자주 바뀌고 규칙으로 표현되면 ApplicationSet 입니다.

현장에서 만나는 모습

저자의 홈랩에서 Argo CD 는 10.0.0.201, Gitea 는 10.0.0.200 입니다. 같은 클러스터 안의 Gitea 를 Argo CD 가 당겨 옵니다. 이 구성의 재미있는 함정은 부트스트랩 순서입니다 — Gitea 가 죽으면 Argo CD 는 원하는 상태를 읽을 수 없고, Gitea 자신의 매니페스트도 그 Gitea 안에 있습니다. 그래서 GitOps 로 관리하는 영역과 손으로 부트스트랩하는 영역의 경계를 어디에 그을지가 실제 설계 문제로 등장합니다.

또 하나. 이 클러스터의 Gateway API 는 CRD v1.6.1 이 필요했습니다. v1.2 로는 tlsroutesreferencegrants 가 v1 이 아니어서 Cilium 게이트웨이 컨트롤러가 기동을 거부했습니다. CRD 버전 같은 클러스터 전제 조건은 앱 오버레이가 아니라 플랫폼 계층 저장소 에 두고 sync wave 를 앞당겨야 한다는 걸 이런 사고가 가르쳐 줍니다.

다음 실습에서 할 것

/root/cgoa-repo/ 아래에 apps/checkout/baseoverlays/dev|stage|prod 를 직접 만들고, kubectl kustomize 로 환경별 렌더 결과가 어떻게 갈리는지 확인합니다. 그다음 Application 매니페스트와 app-of-apps 루트, AppProject 까지 파일로 작성합니다. 이어지는 실습에서는 같은 구조를 실제 클러스터에 kubectl apply -k 로 올려 봅니다.