ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이코에코(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-클러스터-설정-ansible

    1.2 스펙이 조금만 바뀌어도 덜컹거리던 Ansible

    Terraform로 EC2 타입이나 노드 수를 수정하면, 항상 이런 루틴을 탔다.

    1. `terraform output`으로 inventory를 다시 뽑고
    2. Ansible `hosts.ini`를 맞춰주고
    3. 라벨/테인트 Playbook에서 새 노드 이름을 또 맞춰주고
    4. 전체 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를 한 번 걸면:
     

    1. 네임스페이스/CRD가 먼저 깔리고
    2. 인프라 계층이 준비된 뒤
    3. SSM → ExternalSecrets → Secret 경로로 값이 채워지고
    4. DB/MQ가 `Ready`가 된 다음
    5. 마지막에 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을 줄인 이유를 짧게 정리하면 세 가지 정도다.

    1. 속도 문제
      • SSH로 14개 노드를 순회하는 방식은, 개인 프로젝트/해커톤 스코프에서도 오래 버티기 힘들다.
    2. 역할 분리
      • AWS 리소스: Terraform/Atlantis
      • K8s 리소스 & 앱: ArgoCD + Helm/Kustomize
      • 클러스터 부트스트랩: 최소한의 Ansible(or User-Data)
      • 각 층의 책임이 명확해져야, “지금 문제가 나면 어디를 봐야 하는지”가 선명해진다.
    3. 설명 가능성
      • “코드 → PR → plan/apply → ArgoCD Sync Wave”라는 선이 그어져 있어야,
        나중에 새로 합류한 사람에게도 말로 설명할 수 있는 구조가 된다.

    첫 직장 회고 글들에서도 꾸준히 반복적으로 얘기를 한 사안이 레이어가 애매하면 결국 사람이 다 떠안는다.
    이코에코 인프라를 GitOps 2.0 쪽으로 리팩토링한 작업은 그때 못했던 정리를 뒤늦게 다시 하는 느낌에 가깝다.
    다음 편에서는 이 구조 위에서 Sync Wave랑 Helm/Kustomize, ExternalSecrets를 어떻게 풀었는지,
    CrashLoopBackOff 로그와 함께 조금 더 적나라하게 기록할 생각이다.
     

    번외

    어쩌다보니 기술적인 욕심이 생겨 도입하게 된 GitOps다.
    전직장 파이프라인의 구조를 기반으로 큰 틀만 잡아둔 채로 나름의 조합을 짜며 구축했기에 주도적인 느낌이 들어 오랜만에 개발이 재밌어진 기간이었다.
    지금은 많은 stack이 Helm-charts로 대체됐지만..
    오픈소스 Operator + CRD/CD 전면 도입을 시도했었다. 다음 장에 여유가 된다면 기록해 두겠다.

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango