-
이코에코(Eco²) GitOps #02: Ansible 의존성 감소, Kustomize Overlays 패턴 적용이코에코(Eco²)/Kubernetes Cluster+GitOps+Service Mesh 2025. 11. 24. 16:07
Ansible에서 GitOps 2.0으로: 선언적 클러스터 관리의 여정
Terraform과 Ansible로 초기 구축한 클러스터를 ArgoCD App-of-Apps와 Sync Wave 기반 GitOps 아키텍처로 발전시킨 과정을 정리합니다. 이 글에서는 40분이 넘던 부트스트랩 시간을 25분대로 단축하고, 궁극적으로 Agent-Driven Development의 기반을 마련하기까지의 기술적 결정들을 다룹니다. 버전 히스토리는 CHANGELOG.md를 참고하시면 됩니다.
1. Ansible이 과도한 책임을 지던 구조
v0.7.0 시점의 아키텍처는 다음과 같은 형태였습니다.
Terraform (VPC/EC2/SSM) ↓ Ansible (클러스터 구성 전반) ↓ ArgoCD (일부 애플리케이션 배포)당시 Ansible이 담당하던 작업 목록을 정리하면 다음과 같습니다.
카테고리 작업 내용 OS 레벨 apt 패키지 설치, swap 비활성화, sysctl 설정 컨테이너 런타임 Docker, containerd 설치 및 설정 Kubernetes 설치 kubeadm, kubelet, kubectl 패키지 클러스터 초기화 kubeadm init, worker join 노드 설정 라벨, 테인트, Provider ID 설정 CNI Calico VXLAN 설치 Add-ons Metrics Server, EBS CSI Driver, ALB Controller 플랫폼 Cert-Manager, Prometheus, Grafana GitOps 도구 Atlantis, ArgoCD 설치 K8s 리소스 일부 Namespace, Ingress 문제는 이 구조가 GitOps 원칙과 충돌한다는 점이었습니다.
- 인프라 State: Terraform 코드와 S3 Backend에 저장
- 애플리케이션 State: Git 저장소와 ArgoCD가 관리
- 클러스터 설정 State: Ansible 실행 이력에 암묵적으로 존재
Git이 Single Source of Truth(SSOT)인 것처럼 보였지만, 실제로는 Ansible이 중간에서 상태를 가공하는 구조였기 때문에 진정한 의미의 SSOT라고 부르기 어려웠습니다.
1.1 40~50분이 소요되는 부트스트랩
14개 노드 기준으로 전체 Playbook을 실행하면 다음 단계를 거칩니다.
# ansible/site.yml 실행 순서 1. SSH 연결 대기 및 cloud-init 완료 확인 2. common role: OS 기본 설정 3. docker role: Docker/containerd 설치 4. kubernetes role: kubeadm/kubelet/kubectl 설치 5. 02-master-init.yml: kubeadm init 실행 6. 03-worker-join.yml: 모든 Worker 노드 조인 7. 03-1-set-provider-id.yml: ALB Controller용 Provider ID 설정 8. 04-cni-install.yml: Calico VXLAN 배포 9. argocd role: ArgoCD Core 설치전체 소요 시간은 약 40~50분이었습니다. apt 저장소 응답이 느리거나 SSH 타임아웃이 발생하면 "멱등성이 보장되니 다시 실행하면 된다"고 생각했지만, 체감상으로는 처음부터 다시 시작하는 것과 다르지 않았습니다.
1.2 인프라 변경 시 발생하는 불편함
Terraform으로 EC2 인스턴스 타입이나 노드 수를 변경하면 다음 과정을 거쳐야 했습니다.
1. terraform output으로 새 inventory 정보 추출 2. ansible/inventory/hosts 파일 수정 3. playbooks/tasks/fix-node-labels.yml에서 노드 이름 매칭 수정 4. 전체 Playbook 재실행레이어 간 경계가 불명확하면 결국 중간 레이어가 과도한 책임을 지게 됩니다. 이 프로젝트에서 그 역할을 Ansible이 담당하고 있었습니다.
2. Ansible 작업 분류: 이관 가능성 분석
가장 먼저 수행한 작업은 Ansible이 실제로 어떤 일을 얼마나 하고 있는지 분류하는 것이었습니다.
각 작업에 다음 레이블을 부여했습니다.
- ✅ 이관 가능: Terraform User-Data, ArgoCD, Helm 등으로 대체 가능
- ❌ Ansible 유지: 현실적으로 Ansible이 계속 담당해야 하는 작업
카테고리 ✅ 이관 가능 ❌ Ansible 유지 합계 OS 레벨 설정 3 0 3 클러스터 초기화 1 2 3 CNI/네트워크 0 1 1 K8s Add-ons 4 0 4 GitOps 도구 1 1 2 데이터베이스 4 0 4 K8s 리소스 3 0 3 노드 관리 0 3 3 기타 2 0 2 합계 18 7 25 분석 결과, 25개 작업 중 18개가 Terraform User-Data와 ArgoCD 조합으로 이관 가능했습니다. Ansible의 책임을 최소한의 부트스트랩 작업으로 축소할 수 있는 여지가 충분했습니다.
이 시점에서 목표가 명확해졌습니다.
- Ansible을 부트스트랩 전용 도구로 경량화
- 클러스터 내부 리소스 관리는 GitOps로 이관
3. v0.7.1: Helm Values에서 Kustomize base/overlay로 전환
Ansible의 책임을 줄이려면, 먼저 클러스터 내부에서 동작하는 리소스들을 코드로 관리해야 했습니다.
초기에는 Helm Values 파일이 환경별로 다음과 같이 산재해 있었습니다.
values-dev.yaml values-14nodes.yaml values-prod.yaml설정 하나를 변경하면 여러 Values 파일을 동시에 수정해야 했고, "현재 기준이 되는 값이 어느 파일에 있는가"를 항상 추적해야 했습니다.
base/overlay 패턴 도입
Kustomize의 base/overlay 패턴을 도입하여 구조를 재정립했습니다.
workloads/ ├── domains/ # base: 공통 정의 │ ├── auth/ │ │ ├── deployment.yaml │ │ ├── service.yaml │ │ └── kustomization.yaml │ ├── character/ │ └── ... └── domains/dev/ # overlay: 환경별 패치 ├── auth/ │ ├── kustomization.yaml │ └── replicas-patch.yaml └── ...- 공통 스펙은
workloads/domains/{service}/에 정의 - 환경별 차이는 overlay에서 patch로만 관리
이 전환 이후의 구조는 다음과 같습니다.
- 모든 리소스 정의가 Git의
workloads/디렉토리에 존재 - 환경 차이는 overlay patch로만 표현
- ArgoCD가 Kustomize로 렌더링하여 클러스터에 반영
Ansible이 담당하던 Namespace와 Ingress 일부 역할이
workloads/로 이관되었고, "플랫폼 계층"과 "애플리케이션 계층"의 경계가 명확해지기 시작했습니다.
4. v0.7.2: Ingress 분리와 Terraform State 원격화
4.1 서비스별 Ingress 분리
초기에는 API, ArgoCD, Grafana가 하나의 Ingress 파일에서 path로 구분되어 있었습니다.
# 하나의 ingress.yaml에 모든 서비스가 혼재 paths: - /api/* - /argocd/* - /grafana/*이 구조의 문제점은 하나의 서비스 라우팅을 수정할 때 다른 서비스까지 스코프에 포함된다는 점이었습니다.
서비스별로 Ingress를 분리했습니다.
workloads/routing/ ├── ingress-api.yaml ├── ingress-argocd.yaml ├── ingress-grafana.yaml └── ingress-kiali.yaml이 변경으로 다음 효과를 얻었습니다.
- API 라우팅 수정 시 ArgoCD에 영향을 미치는 사고 방지
- ACM 인증서 관리 단순화 (와일드카드 인증서 1개 + 서비스별 Ingress)
4.2 Terraform S3 Backend 도입
Terraform State가 로컬에 저장되어 있었습니다. 개인 작업 시에는 문제가 없었지만, GitOps 관점에서는 적절하지 않은 구조였습니다.
# terraform/backend.tf terraform { backend "s3" { bucket = "eco2-terraform-state" key = "dev/terraform.tfstate" region = "ap-northeast-2" dynamodb_table = "terraform-locks" encrypt = true } }S3 Backend와 DynamoDB Lock을 적용하고, Atlantis를 통해 PR 기반
plan/apply워크플로우를 구성했습니다.인프라 영역에서 PR → plan → apply → State 업데이트 흐름이 확립되면서, 클러스터 내부 구성을 본격적으로 재정비할 준비가 완료되었습니다.
5. v0.7.3: GitOps 2.0과 App-of-Apps 패턴
v0.7.3의 핵심 변경 사항은 다음과 같습니다.
- ArgoCD App-of-Apps 패턴 전면 도입
- Sync Wave 기반 계층적 배포 (Wave -1 ~ 70)
- Ansible 역할의 최소화 (부트스트랩 전용)
5.1 App-of-Apps: 단일 진입점으로 전체 클러스터 관리
기존에는 ArgoCD Application들이 독립적으로 존재했습니다. 배포 시 "어떤 Application을 먼저 Sync해야 하는가"를 항상 기억하고 있어야 했습니다.
Root Application을 도입하여 단일 진입점을 만들었습니다.
# clusters/dev/root-app.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: dev-root namespace: argocd spec: source: repoURL: https://github.com/eco2-team/backend.git targetRevision: develop path: clusters/dev/apps directory: recurse: true syncPolicy: automated: prune: true selfHeal: trueclusters/dev/apps/디렉토리에는 Wave별로 구분된 Application들이 위치합니다.clusters/dev/apps/ ├── 00-crds.yaml # Wave -1: CRDs ├── 02-namespaces.yaml # Wave 0: Namespaces ├── 10-secrets-operator.yaml # Wave 1: External Secrets ├── 24-postgresql.yaml # Wave 2: 데이터베이스 ├── 28-redis-cluster.yaml # Wave 2: 캐시 ├── 31-rabbitmq-cluster.yaml # Wave 2: 메시지 브로커 ├── 40-apis-appset.yaml # Wave 3: API 서비스 ├── 41-workers-appset.yaml # Wave 3: Worker 서비스 └── ...이제 root-app 하나만 Sync하면 내부 Wave 순서에 따라 전체 클러스터가 구성됩니다.
5.2 Sync Wave: 의존성 기반 순차 배포
App-of-Apps 패턴을 적용한 직후 마주한 문제가 있었습니다.
ArgoCD는 리소스 간 의존성을 고려하지 않고 모든 Application을 동시에 배포하려 합니다.
Wave 설정 없이 Sync를 실행하면 다음 상황이 발생했습니다.
- PostgreSQL Cluster가 아직
Creating상태 - Auth/Scan API가 먼저 배포됨
- DB 커넥션 실패로 CrashLoopBackOff 반복
Sync Wave를 적용하여 배포 순서를 제어했습니다.
# clusters/dev/apps/24-postgresql.yaml metadata: annotations: argocd.argoproj.io/sync-wave: "2" # clusters/dev/apps/40-apis-appset.yaml metadata: annotations: argocd.argoproj.io/sync-wave: "3"최종 배포 순서는 다음과 같습니다.
Wave 리소스 설명 -1 CRDs Operator CRDs 사전 배포 0 Namespaces, RBAC 네임스페이스 및 권한 설정 1 ExternalSecrets, Cert-Manager SSM → Secret 주입 경로 확보 2 PostgreSQL, Redis, RabbitMQ 데이터 계층 Ready 대기 3 API, Worker 애플리케이션 배포 이 구조로 root-app Sync를 한 번 실행하면 다음 순서로 배포됩니다.
- 네임스페이스와 CRD가 먼저 생성
- SSM → ExternalSecrets → Secret 경로로 설정값 주입
- 데이터베이스와 메시지 브로커가 Ready 상태 도달
- 마지막으로 API 서비스 배포
6. Before & After: Ansible 역할 변화
6.1 Before: Ansible 중심 구조
Terraform (AWS 리소스 생성) ↓ Ansible ├── OS 설정 (apt, swap, sysctl) ├── Docker/containerd 설치 ├── K8s 패키지 설치 ├── kubeadm init / worker join ├── Provider ID 설정 ├── Labels/Taints 설정 ├── Calico CNI ├── Metrics Server, EBS CSI, ALB Controller ├── Prometheus, Grafana ├── Atlantis, ArgoCD └── 일부 Ingress/Namespace- 구축 시간: 40~50분
- 수동 개입: SSH/inventory 관리, 실패 시 재실행
- Drift 감지: 불가능
6.2 After: GitOps 2.0
Terraform (AWS 리소스 생성) ↓ Ansible (최소 부트스트랩) ├── kubeadm init / worker join ├── Calico CNI (초기 1회) ├── ArgoCD Core 설치 └── (완료) ↓ ArgoCD Root-App (App-of-Apps) ├── Wave -1: CRDs ├── Wave 0: Namespaces, RBAC ├── Wave 1: ExternalSecrets, Cert-Manager ├── Wave 2: PostgreSQL, Redis, RabbitMQ └── Wave 3: APIs, Workers현재 Ansible site.yml의 구조는 다음과 같이 단순화되었습니다.
# ansible/site.yml - name: Prerequisites - OS 설정 roles: [common] - name: Docker 설치 roles: [docker] - name: Kubernetes 패키지 설치 roles: [kubernetes] - name: Master 초기화 tasks: [02-master-init.yml] - import_playbook: playbooks/03-worker-join.yml - import_playbook: playbooks/04-cni-install.yml - name: ArgoCD 설치 roles: [argocd] # 이후 모든 리소스는 ArgoCD가 관리항목 Before After 구축 시간 40~50분 25~35분 수동 개입 SSH, inventory 관리 최소화 Node 추가 Ansible 재실행 Terraform apply + ArgoCD selfHeal 설정 Drift 감지 불가 ArgoCD 자동 복구 SSOT 부분 지원 Git = Source of Truth
7. 선언적 클러스터와 Agent-Driven Development
Ansible 역할을 축소한 이유는 세 가지로 정리할 수 있습니다.
- 속도: SSH로 25개 노드를 순회하는 방식은 확장에 한계가 있습니다.
- 역할 분리: AWS 리소스는 Terraform, K8s 리소스는 ArgoCD, 부트스트랩은 Ansible로 책임을 명확히 구분합니다.
- 설명 가능성: "코드 → PR → plan/apply → Sync Wave" 흐름이 명확해야 다른 사람에게 설명할 수 있습니다.
그러나 이 작업의 더 큰 의미는 Agent-Driven Development의 기반을 마련했다는 점입니다.
7.1 클러스터 상태의 코드화
GitOps 2.0으로 전환하면서 클러스터의 모든 상태가 코드로 표현되었습니다.
clusters/dev/ ├── root-app.yaml # 진입점 └── apps/ ├── 00-crds.yaml # CRD 정의 ├── 02-namespaces.yaml # 네임스페이스 ├── 24-postgresql.yaml # 데이터베이스 ├── 40-apis-appset.yaml # API 서비스 └── ... # 전체 클러스터 상태ArgoCD의
selfHeal: true설정으로 Desired State(Git) = Actual State(Cluster) 동기화가 자동으로 유지됩니다.7.2 AI 에이전트가 이해할 수 있는 구조
이 선언적 구조 위에서 AI 에이전트(Claude, Cursor 등)가 코드베이스를 읽는 것만으로 클러스터 상태를 파악할 수 있게 되었습니다.
# CLAUDE.md - 에이전트용 컨텍스트 ## 클러스터 아키텍처 - 25-Node Self-managed K8s (Terraform + Ansible) - ArgoCD App-of-Apps (clusters/dev/root-app.yaml) - Sync Wave 기반 계층 배포 ## 디렉토리 구조 - apps/: 도메인 서비스 소스코드 - workloads/: Kubernetes 매니페스트 - clusters/: ArgoCD Applications예를 들어 에이전트에게 "scan API의 replica를 3으로 늘려"라고 요청하면 다음 과정이 진행됩니다.
workloads/domains/scan/deployment.yaml수정- Git commit & push
- ArgoCD 자동 Sync로 클러스터 반영
코드 변경이 곧 클러스터 변경이 되는 구조이기 때문에, 에이전트가 인프라 영역까지 다룰 수 있는 기반이 마련된 것입니다.
7.3 Skills 기반 컨텍스트 전달
현재
.claude/skills/디렉토리에 도메인별 가이드를 축적하고 있습니다..claude/skills/ ├── function-calling-agent/ # Function Calling 구현 패턴 ├── chat-agent-flow/ # LangGraph 흐름 디버깅 └── ...에이전트가 새로운 기능을 구현할 때 이 Skills를 참조하면, 프로젝트의 컨벤션을 따르면서 클러스터 배포까지 일관되게 처리할 수 있습니다.
정리
Ansible을 싫어해서 역할을 줄인 것이 아닙니다. 레이어 간 경계가 불명확하면 결국 사람이 모든 복잡성을 감당해야 합니다.
이코에코 인프라를 GitOps 2.0으로 리팩토링한 작업의 성과는 다음과 같습니다.
- 부트스트랩 시간을 40분에서 25분으로 단축
- Git = Single Source of Truth 확립
- 선언적 클러스터 관리 → Agent-Driven Development 기반 마련
클러스터가 코드로 완전히 표현되면서, AI 에이전트가 "코드베이스 이해 = 클러스터 이해"가 되는 구조가 만들어졌습니다.
다음 글에서는 이 구조 위에서 Sync Wave 설정, ExternalSecrets 연동, CrashLoopBackOff 디버깅 경험을 더 상세히 다룰 예정입니다.
'이코에코(Eco²) > Kubernetes Cluster+GitOps+Service Mesh' 카테고리의 다른 글
이코에코(Eco²) Service Mesh #1: Istio Sidecar 마이그레이션 (0) 2025.12.08 이코에코(Eco²) GitOps #06 - Namespace · RBAC · NetworkPolicy를 한 뿌리에서 (3) 2025.11.25 이코에코(Eco²) GitOps #05 Sync Wave (0) 2025.11.25 이코에코(Eco²) GitOps #04: Operator(Controller) 기반 클러스터 인프라 구성 자동화 (0) 2025.11.24 이코에코(Eco²) GitOps #03 네트워크 트러블슈팅 (0) 2025.11.24