-
이코에코(Eco²) GitOps #02 Ansible 의존성 감소, Kustomize Overlays 패턴 적용이코에코(Eco²)/Kubernetes Cluster+GitOps+Service Mesh 2025. 11. 24. 16:07
🛎️ 본 포스팅은 구현이 완료된 사안만 다룹니다. 현재 이코에코 14-nodes cluster는 ap-northeast-2 리전에 배포돼 있습니다.

Terraform + Ansible로 러프하게 펼친 클러스터를 초안으로 삼아, ArgoCD App-of-Apps + Sync Wave + Helm/Kustomize 기반 GitOps로 발전하는 과정이다.1. Ansible이 너무 많은 걸 알고 있던 시절
이코에코 클러스터의 0.7.0 근처 아키텍처를 한 줄로 요약하면 이랬다. 버저닝은 이 글을 참고하면 된다.
Terraform (VPC/EC2/SSM) ↓ Ansible (거의 모든 클러스터 작업) ↓ ArgoCD (일부 앱 배포)
당시 정리했던 GitOps 1.0 문서가 이거다.
→ docs/architecture/GITOPS_ARCHITECTURE.md
여기서 Ansible 역할은 대충 이런 느낌이었다.
- Kubeadm init / join
- CNI 설치 (Calico)
- 노드 레이블/테인트
- Metrics Server / EBS CSI / AWS Load Balancer Controller
- Cert-Manager, Prometheus, Grafana, Atlantis, ArgoCD 설치
- 일부 Ingress, Namespace까지
Terraform이 AWS 인프라를 흩뿌리면, Ansible이 엮어 전체 클러스터를 구성한 다음, ArgoCD는 그 위에 올라가는 일부 애플리케이션만 쥐고 있는 구조였던 거다.
레이어 경계가 애매하면 결국 중간 레이어가 너무 많은 걸 알고 버틴다. 이 프로젝트에서도 그 역할이 Ansible이었다.1.1 40~50분짜리 부트스트랩
14 Node 기준으로 Ansible 전체 Playbook을 돌리면:
1. OS 업데이트 + 패키지 설치
2. Docker / containerd / K8s 패키지 설치
3. `kubeadm init` / `join`
4. CNI / CSI / ALB Controller / Monitoring / Atlantis / ArgoCD …
대략 40~50분 정도가 걸렸다.
중간에 apt 레포가 한 번 삐끗하거나, 어떤 노드에서 SSH 타임아웃이 나면 “멱등성 있으니까 다시 돌리면 되지”라고 위안을 삼아도 체감상 “다시 처음부터”에 가까웠다.
GitOps 1.0 플로우는 문서에 이렇게 그려져 있다.
→ 같은 문서 하단 `Phase 2: 클러스터 설정 (Ansible)` mermaid 참고
GITOPS_ARCHITECTURE.md#phase-2-클러스터-설정-ansible1.2 스펙이 조금만 바뀌어도 덜컹거리던 Ansible
Terraform로 EC2 타입이나 노드 수를 수정하면, 항상 이런 루틴을 탔다.
- `terraform output`으로 inventory를 다시 뽑고
- Ansible `hosts.ini`를 맞춰주고
- 라벨/테인트 Playbook에서 새 노드 이름을 또 맞춰주고
- 전체 Playbook을 다시 돌린다
문제는 이게 GitOps와 호응이 잘되지 않았다.
인프라 State는 Terraform 코드 + S3 Backend에 있고, 애플리케이션 State는 Git + ArgoCD가 알고 있는데 클러스터 설정(State)은 언제 돌렸는지 모를 Ansible 실행 이력에 묻혀 있다.
Git이 Single Source of Truth인 것처럼 보이지만,
실제로는 중간에 Ansible이 가공하는 구조였기에 SSOT라 부르기엔 무리가 있었다.2. ANSIBLE-TASK-CLASSIFICATION: 일단 쪼개서 보기
가장 먼저 한 일은, Ansible이 실제로 무슨 일을 얼마나 하고 있는지 쪼개 보는 거였다. 그 결과물이 `ANSIBLE-TASK-CLASSIFICATION` 문서다.
→ docs/deployment/ansible/ANSIBLE-TASK-CLASSIFICATION.md
여기서 카테고리별로 작업을 쭉 나열하고, 각 태스크마다 이런 라벨을 붙였다.
- ✅ 대안 있음: Terraform, User-Data, ArgoCD, Helm 등으로 이관 가능
- ❌ 대안 없음: 현실적으로 Ansible(혹은 Operator)이 계속 맡아야 하는 것
- 🗑️ 제거 가능: 실제로 더 이상 쓰지 않는 작업들
정리해보니까 숫자가 이렇게 나왔다.OS 레벨 설정 3 0 0 3 클러스터 초기화 2 1 0 3 CNI/네트워크 1 0 0 1 K8s Add-ons 3 0 1 4 GitOps 도구 2 0 0 2 데이터베이스 4 0 0 4 K8s 리소스 3 0 0 3 노드 관리 0 3 0 3 기타 2 0 0 2 합계 20 4 1 25
실질적으로 25개 작업 중 20개는 Terraform User-Data + ArgoCD(Helm/Kustomize) 조합으로 이관 가능으로 Ansible의 작업을 단 4개까지 축소시킬 여지가 있었다.
이쯤 되니까 목표가 자연스럽게 이렇게 바뀌었다.
1. Ansible을 부트스트랩 전용으로 가볍게 만들자.
2. 나머지는 GitOps한테 넘기자.
3. 0.7.1 – Helm Values에서 Kustomize로
Ansible을 줄이려면, 먼저 “클러스터 위에서 돌아가는 것들” 을 코드로 관리해야 했다.
0.7.1에서 한 키 포인트가 바로 Kustomize 전면 도입이다.
초기에는 Helm Values 파일이 환경별로 이렇게 늘어났다.
- `values-dev.yaml`
- `values-14nodes.yaml`
- `values-prod.yaml`
- …(이름만 다른 복붙 세트들)
Config 하나 바꾸면 Values 파일 여러 개를 동시에 손봐야 했고,
나중에 보면 “지금 진짜 기준이 되는 값이 어디냐?”를 코드로 추적해야 했다.
https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/Declarative Management of Kubernetes Objects Using Kustomize
Kustomize is a standalone tool to customize Kubernetes objects through a kustomization file. Since 1.14, kubectl also supports the management of Kubernetes objects using a kustomization file. To view resources found in a directory containing a kustomizatio
kubernetes.io
그래서 base/overlay 패턴으로 구조를 갈아엎었다.
base에 공통 사항을 기록해 두고 overlay 아래에 환경별로 필요한 값을 패치하는 방식이라 한 눈에 확인하기 편했다.k8s/ base/ api/ infra/ ... overlays/ dev/ 14nodes/ prod/
- 공통 스펙은 base/로 모으고
- 환경/스펙 차이는 overlays/{env}에서 patch로만 관리
이에 이르기까지의 흔적이 문서에 남겨져 있다.
1. docs/infrastructure/04-IaC_QUICK_START.md
2. docs/deployment/ENVIRONMENT_DEPLOYMENT_STRATEGY.md
이때부터는:
1. 리소스 정의는 전부 Git 아래 k8s/base, /k8s/overlays에 있고
2. 환경 차이는 overlay patch로만 표현되고
3. ArgoCD는 이걸 Kustomize로 렌더링해서 그대로 클러스터에 반영하는 구조가 됐다
이 과정에서 Ansible이 들고 있던 Namespace/Ingress 일부 역할이 `k8s/base`로 넘어갔고,
“이건 플랫폼이고, 이건 앱(Business Layer)이다”라는 선이 조금씩 또렷해졌다.
kustomize overlays는 이미 많은 선례가 있는 패턴이고, 오픈소스 Operator 레포에도 해당 구조를 채택하는 경우가 다수지만..
결국 개인 성향에 따라 flat한 base/dev/prod 구조로 갔다. dev/prod 기준 base의 상대 경로가 꼬이기에 한 depth를 낮췄다
4. 0.7.2 – Ingress 쪼개고, Terraform State 바깥으로 빼기
0.7.2에서는 GitOps 작업과 병렬로 운영 쪽 체력도 같이 끌어올렸다.
4.1 거대한 Ingress 한 장에서 서비스별 Ingress로
초기에는 API, ArgoCD, Grafana가 한 Ingress 안에서 path로만 흩어져 있었다.
- `/api`, `/argocd`, `/grafana` 같은 식으로 한 파일에 다 들어가 있다 보니
- 뭔가를 수정할 때마다 다른 서비스까지 스코프를 건드리게 된다
그래서 도메인/서비스별로 Ingress를 분리했다.
- `ingress-api.yaml`
- `ingress-argocd.yaml`
- `ingress-grafana.yaml`
이 구조는 Namespace 전략 문서에서 같이 언급된다.
→ docs/namespace/NAMESPACE_STRATEGY_ANALYSIS.md
결과적으로:
- API 라우팅 만지다가 ArgoCD를 날리는 사고를 줄였고
- ACM 인증서도 “와일드카드 1개 + 서비스별 Ingress” 패턴으로 단순화할 수 있었다4.2 Terraform S3 Backend: State를 로컬에서 해방
Terraform State도 처음엔 로컬에 있었다.
나 혼자 만질 땐 괜찮은데, 이걸 GitOps라 부르기엔 좀 민망한 구조였다.
그래서:
- S3 Backend + DynamoDB Lock을 붙이고
- `dev/stg/prod`별로 버킷/테이블을 나누고
- Atlantis로 PR 기반 `plan/apply`를 돌리는 구조로 바꿨다
이 부분은 GitOps 아키텍처 1.0 문서에도 플로우로 정리해놨다.
→ GITOPS_ARCHITECTURE.md#phase-1-인프라-생성-atlantis--terraform
이렇게 되면서 인프라 쪽은 비교적 건강한 PR → plan → apply → State 업데이트 플로우가 잡혔고,
슬슬 클러스터 내부(Ansible 쪽)를 본격적으로 갈아엎을 준비가 된 셈이다.5. 0.7.3 – GitOps Architecture 2.0 & App-of-Apps
0.7.3에서 바뀐 내용은 Changelog에서 이렇게 정리해놨다.
→ CHANGELOG.md v0.7.3 섹션backend/CHANGELOG.md at main · eco2-team/backend
Contribute to eco2-team/backend development by creating an account on GitHub.
github.com
핵심만 뽑으면:
- GitOps Architecture 2.0
- ArgoCD App-of-Apps 패턴 전면 도입
- Sync Wave 기반 계층 배포 (Wave 0~70)
- Helm + Kustomize 통합 관리
- Atlantis 통합, SSH 키 단일화, Terraform 워크플로우 표준화
- Ansible 역할 최소화 (부트스트랩 전용)- kubeadm init/join
- Calico CNI (초기 1회)
- ArgoCD Core 설치
- 이후 모든 리소스는 ArgoCD 관리
5.1 App-of-Apps: root-app 하나로 모든 앱을

예전에는 ArgoCD Application들이 제각각이었다.
- 인프라용 App
- 모니터링용 App
- 도메인별 API Apps …
그래서 항상 머릿속에 무엇을 먼저 Sync해야 하는가 타임라인을 들고 있어야 했다.
0.7.3에서는 Root App을 도입했다.argocd/root-app.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: root-app namespace: argocd spec: source: repoURL: https://github.com/eco2-team/backend targetRevision: develop path: argocd/apps
argocd/apps 아래에는 Wave별로 쪼갠 App들이 있다.
argocd/apps/ 00-namespaces.yaml # Wave -1 10-infrastructure.yaml # CNI, EBS CSI, ALB, Metrics... 20-platform.yaml # ExternalSecrets, Cert-Manager ... 40-monitoring.yaml # Prometheus, Grafana 60-data-clusters.yaml # PostgreSQL, Redis, RabbitMQ 80-apis-app-of-apps.yaml# Domain API들이제는 root-app을 Sync하면, 내부 Wave 순서대로 다 올라가는 구조가 됐다.
Kube-OVN 문서 펼쳐 두고 전체 플로우를 다시 그려보던 느낌이랑 비슷했다.5.2 Sync Wave: CrashLoopBackOff 탈출기
App-of-Apps로 묶고 나서 곧바로 맞닥뜨린 문제가 하나 있었다.
“ArgoCD는 착실해서, DB가 없는데도 API부터 띄운다.”
초기엔 Wave를 안 쓰고 한 번에 Sync를 눌렀더니:
PostgreSQL Cluster가 아직 `Pending/Creating` 상태인데 Auth/Scan API가 먼저 올라갔다가 DB 커넥션 실패로 CrashLoopBackOff 무한 반복
`kubectl logs` 보면서 한참 헤매다가, Sync Wave를 제대로 도입했다. Wave 계획은 별도 문서로도 뽑아놨다.
대략 구조는 이렇다.
- Wave -1: Namespaces / CRDs
- Wave 0: CNI / EBS CSI / ALB Controller / Metrics 등 인프라
- Wave 1~3: ExternalSecrets, Cert-Manager, Platform 계층
- Wave 4: Data Clusters (PostgreSQL, Redis, RabbitMQ)
- Wave 10: Application (API, Worker)
이제 root-app Sync를 한 번 걸면:
- 네임스페이스/CRD가 먼저 깔리고
- 인프라 계층이 준비된 뒤
- SSM → ExternalSecrets → Secret 경로로 값이 채워지고
- DB/MQ가 `Ready`가 된 다음
- 마지막에 API들이 뜬다
GitOps 2.0 파이프라인 문서에서는 이 플로우를 아예 그림으로 정리했다.
→ docs/archive/deployment/gitops/TERRAFORM-OPERATOR-PIPELINE.md
특히 Before (Ansible 기반) / After (Terraform + Operator 기반) 블록 비교가 참고가 많이 된다.6. Ansible Before & After

6.1 Before – Ansible이 다 하던 구조
Terraform ↓ Ansible ├ OS 설정 ├ Docker/K8s 설치 ├ Master init / Worker join ├ Provider ID 설정 ├ Labels/Taints 설정 ├ CNI 설치 ├ EBS CSI, ALB Controller ├ Monitoring (Prometheus, Grafana) ├ Atlantis, ArgoCD └ 일부 Ingress/Namespace
이 구조는 위에 링크한 GitOps 1.0 문서의 “도구별 역할 구분” 표에서도 그대로 드러난다.
→ GITOPS_ARCHITECTURE.md#2-ansible-cluster-configuration-management
- 구축 시간: 40~50분
- 수동 개입: SSH/inventory 관리 + 실패 시 재실행
- Drift 감지: 거의 안 됨6.2 After – GitOps Architecture 2.0을 향해서
0.7.3~0.7.4 시점의 목표 구조는 `TERRAFORM-OPERATOR-PIPELINE` 문서에 이렇게 정리해놨다.
→ TERRAFORM-OPERATOR-PIPELINE.md#아키텍처-비교
요약하면:Terraform ├ EC2 + User-Data (OS/K8s 설치로 점진 이관) └ (최소) Ansible 부트스트랩 ├ kubeadm init/join ├ Calico CNI 1회 설치 └ ArgoCD Core 설치 ArgoCD Root-App (App-of-Apps) ├ Wave -1: Namespaces / CRDs ├ Wave 0: CNI, CSI, ALB, Metrics ├ Wave 1~4: Platform, Monitoring, Data └ Wave 10: Applications
같은 문서에 있는 비교표를 그대로 가져오면:구축 시간 40~50분 25~35분 수동 개입 SSH, inventory 관리 0 (3-5분 간격 완전 자동, WebHook 도입 X) Node 추가 Ansible 재실행 Terraform apply 설정 Drift 감지 불가 Operator/ArgoCD가 자동 복구 GitOps 부분 지원 완전 지원
실제 체감도 비슷했다.
- 예전엔 “오늘은 클러스터만 올리고, 내일 애플리케이션 배포하자” 느낌이었다면
- 지금은 “Terraform apply + root-app Sync 한 번이면, 25~30분 안에 전체 스택이 갖춰지는” 수준까지는 끌어올렸다.7. 정리: Ansible을 싫어해서 줄인 건 아니다
Ansible을 줄인 이유를 짧게 정리하면 세 가지 정도다.
- 속도 문제
- SSH로 14개 노드를 순회하는 방식은, 개인 프로젝트/해커톤 스코프에서도 오래 버티기 힘들다.
- 역할 분리
- AWS 리소스: Terraform/Atlantis
- K8s 리소스 & 앱: ArgoCD + Helm/Kustomize
- 클러스터 부트스트랩: 최소한의 Ansible(or User-Data)
- 각 층의 책임이 명확해져야, “지금 문제가 나면 어디를 봐야 하는지”가 선명해진다.
- 설명 가능성
- “코드 → PR → plan/apply → ArgoCD Sync Wave”라는 선이 그어져 있어야,
나중에 새로 합류한 사람에게도 말로 설명할 수 있는 구조가 된다.
- “코드 → PR → plan/apply → ArgoCD Sync Wave”라는 선이 그어져 있어야,
첫 직장 회고 글들에서도 꾸준히 반복적으로 얘기를 한 사안이 레이어가 애매하면 결국 사람이 다 떠안는다.
이코에코 인프라를 GitOps 2.0 쪽으로 리팩토링한 작업은 그때 못했던 정리를 뒤늦게 다시 하는 느낌에 가깝다.
다음 편에서는 이 구조 위에서 Sync Wave랑 Helm/Kustomize, ExternalSecrets를 어떻게 풀었는지,
CrashLoopBackOff 로그와 함께 조금 더 적나라하게 기록할 생각이다.
번외
어쩌다보니 기술적인 욕심이 생겨 도입하게 된 GitOps다.
전직장 파이프라인의 구조를 기반으로 큰 틀만 잡아둔 채로 나름의 조합을 짜며 구축했기에 주도적인 느낌이 들어 오랜만에 개발이 재밌어진 기간이었다.
지금은 많은 stack이 Helm-charts로 대체됐지만..
오픈소스 Operator + CRD/CD 전면 도입을 시도했었다. 다음 장에 여유가 된다면 기록해 두겠다.'이코에코(Eco²) > Kubernetes Cluster+GitOps+Service Mesh' 카테고리의 다른 글
이코에코(Eco²) GitOps #06 - Namespace · RBAC · NetworkPolicy를 한 뿌리에서 (3) 2025.11.25 이코에코(Eco²) GitOps #05 Sync Wave (0) 2025.11.25 이코에코(Eco²) GitOps #04 Operator와 Helm-charts를 오가며 겪은 시행착오들 (0) 2025.11.24 이코에코(Eco²) GitOps #03 네트워크 안정화 (0) 2025.11.24 이코에코(Eco²) GitOps #01 AWS + IaC 기반 K8s 클러스터 구축기 (0) 2025.11.12