ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이코에코(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의 책임을 최소한의 부트스트랩 작업으로 축소할 수 있는 여지가 충분했습니다.

    이 시점에서 목표가 명확해졌습니다.

    1. Ansible을 부트스트랩 전용 도구로 경량화
    2. 클러스터 내부 리소스 관리는 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로만 관리

    이 전환 이후의 구조는 다음과 같습니다.

    1. 모든 리소스 정의가 Git의 workloads/ 디렉토리에 존재
    2. 환경 차이는 overlay patch로만 표현
    3. 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: true

    clusters/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를 한 번 실행하면 다음 순서로 배포됩니다.

    1. 네임스페이스와 CRD가 먼저 생성
    2. SSM → ExternalSecrets → Secret 경로로 설정값 주입
    3. 데이터베이스와 메시지 브로커가 Ready 상태 도달
    4. 마지막으로 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 역할을 축소한 이유는 세 가지로 정리할 수 있습니다.

    1. 속도: SSH로 25개 노드를 순회하는 방식은 확장에 한계가 있습니다.
    2. 역할 분리: AWS 리소스는 Terraform, K8s 리소스는 ArgoCD, 부트스트랩은 Ansible로 책임을 명확히 구분합니다.
    3. 설명 가능성: "코드 → 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으로 늘려"라고 요청하면 다음 과정이 진행됩니다.

    1. workloads/domains/scan/deployment.yaml 수정
    2. Git commit & push
    3. 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 디버깅 경험을 더 상세히 다룰 예정입니다.

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer, Full-time: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) BE/AI(Harness)/Infra/FE 24-node E2E 고도화 및 운영, 2600만원 소모: 2025.12 - 2026.02
🪂 넥슨 AI 엔지니어(서류합 -> 과제합 -> 면접 탈락), 무신사 AI-Native(전환형 인턴, 진행 X) 채용 프로세스: 2026.01.31 - 2026.03.05
🪂 GEODE/REODE 개발, Agentic Loop-based 자율 수행 하네스 + 도메인 특화 DAG(Plug-In), AI R&D Freelance @Pinxlab : 2026.03 - 2026.05
🪂 FuriosaAI SWE, Agent System (서류합 -> 면접 탈락), 42dot AI Engineer, Gleo팀(서류합->코테합->면접 탈락): 2026.04 - 2026.07
🪂 LG AI Research AI Data Engineer Intern(서류합, 전형 참여중), FuriosaAI SWE, Compiler[AX](서류합, 전형 참여중): 2026.07

Designed by Mango