-
이코에코(Eco²) GitOps #01 AWS + IaC 기반 K8s 클러스터 구축기이코에코(Eco²)/Kubernetes Cluster+GitOps+Service Mesh 2025. 11. 12. 22:42
🛎️ 본 포스팅은 구현이 완료된 사안만 다룹니다. 현재 이코에코 14-nodes cluster는 ap-northeast-2 리전에 배포돼 있습니다.

🎯 프로젝트 배경
이번 해커톤 팀은 전 직장 입사 동기 겸 마지막 팀 선임 분이 소개해 주셨다.
서울에서 몇 안되게 챙겨주시는 분이다. 같은 팀이면 불같은 분이지만 이만큼 친절한 분도 잘 없다.
최근 서탈이 쌓이고 있어서 새로 업데이트된 프젝이 하나 있으면 좋을 거 같았다.
뭐라도 하자에 가깝지만.. 개인적으론 라심코의 경험을 사이드 프젝에서 얼마나 끌어낼 수 있을지 궁금하기도 했다.
Cursor + Claude 4.5 Sonnet의 조합의 생산성이 말이 안 된다 싶기도 해서 '이걸로 어디까지 될까..?'가 궁금하기도 했다.
13일간 태운 토큰량이다. 5.9억개 토큰 대부분이 Terraform/Ansible 기반 인프라/클러스터 구성에 소모됐다.
한달이 지난 시점에서 초기 클러스터를 돌아보면 초안에 가까운 상태라 미숙한 부분이 많다.
이후 GitOps -> Istio -> Observability를 거치며 순차적으로 고도화되니 천천히 따라와주면 감사하겠다.
👥 팀 구성
- AI 개발자 1명 (방산)
- 프론트엔드 개발자 2명 (현업)
- 디자이너 1명 (취준생)
- 백엔드 개발자 1명 (전 라쿠텐 CNP 개발, 현 백수)
다들 열심히 의견을 내주시고 생각보다 편안한 분위기에서 이어졌기 때문에 개발 체감은 상당히 좋다.
LLM은 폐기물 이미지 분류엔 GPT-5.0, 채팅엔 GPT-4o-mini를 사용할 예정이다.
Elasticsearch에 관련 문서를 넣어 RAG로 주입시키는 방향으로 가지 않을까 싶었지만
AI 파트 분께서 '이미지 분류와 채팅 기능은 JSON 주입으로도 충분합니다.'라고 하셔 그 의견에 따르기로 했다.
📋 공공기관 주체 해커톤 특성
명칭은 해커톤이지만 기획서에 깃허브 주소를 올려야 하고, 미리 어느 정도 개발이 진행돼야 하는 걸로 보아
사실상 해커톤이 아니라 공모전에 가깝다고 생각한다. 공공기관 주체인 곳은 보통 이런 식으로 진행되는 듯싶다.
작년에 Dream이라는 사이드프젝을 만들었던 적이 있다.
당시 하나의 프로젝트로 참여 중인 해커톤과 공공기관에 동시에 출품했었다. 그때 이와 비슷한 절차를 경험했던 지라 낯설진 않았다.
🎯 왜 Kubernetes + GitOps인가?
기존 경험
전 직장에선 K8s + GitOps 환경을 채택하고 있었다.
덕에 일본, 인도, 미국, 싱가폴 등 각종 지사 엔지니어들이 한데 섞인 팀임에도 Git 코드를 SSOT 삼아 재택 혼합 개발이 가능했다.
한국지사엔 Cloud 팀이 전무했으니.. 풀재택이었어도 무방했을 환경이다.
주 2회 재택에서 주3회로 재택으로 늘리자는 의견도 있었지만 받아들여지지 않았다.당시엔 스토리지 서버 개발이었기에 파이프라인을 직접 만지는 일은 없었다.
개발용 소규모 클러스터를 세팅하고 Confluence에 기능 로직 디자인이 승인 나면commit → review → merge사이클이었기에 파이프라인의 존재 자체를 인식하고 있지 않았으면 몰랐을 수 있다.CI 파이프라인


한 번은 서버 개발 외에도 빌드될 도커 이미지 사이즈를 최적화하는 작업을 수행한 적이 있다.
이때 경량 OS 환경을 찾는 것도 일이었지만 CI 파이프라인을 파악하는 게 우선이었다.
로빈의 CI 파이프라인 구조에 대한 안내는 따로 없었기에 온갖 스크립트를 헤맸다.
당시에는 여기에 Jenkins와 Artifactory, Helm-chart가 존재한다는 것만 알았다.
Claude와 코드 기반 복기를 통해 알아낸 CI 파이프라인을 요약하면 아래와 같다.- GitHub Push
- Jenkins hook으로 이미지를 빌드
- 테스트
- Helm으로 패키징
- Sanity 테스트
- Artifactory로 업로드
파이프라인의 절차는 따르되, 이코에코앱의 스택(FastAPI + LLM + K8s + AWS)에 맞게 스택은 경량화했다.
Jenkins는 GitHub Actions로, Helm은 Kustomize로, Aritifactory는 GHCR로 대체해서 구성했다.
자세한 과정은 GitOps 포스팅에 남기겠다.CD 파이프라인

CI 파이프라인을 걸쳐 컨테이너 이미지로 빌드가 되면 CD 파이프라인을 탄다.
ArgoCD와 같은 오픈소스 대신 Kubernetes Operator로 선언적 배포를 수행하는 걸 확인할 수 있었다.
Reconcile loop로 선언적인 상태를 유지하도록 별도의 Operator 로직들을 구현해 둔 상태였다. (go로 작성되어 있다.)
Reconcile loop의 로직 진행 과정은 아래와 같다.- Deployment 체크: robin-master Deployment가 없으면 생성
- 이미지 업데이트 감지: 이미지가 변경되면 자동 업그레이드
- Image Puller: 새 이미지를 모든 노드에 미리 Pull
- DaemonSet 관리: robin-worker DaemonSet 생성 및 업데이트
- Requeue 메커니즘: 조건이 충족될 때까지 재시도
경량 Operator를 구현한 사례를 함께 살펴보면 이해가 편할 듯싶다.
https://kingofbackend.tistory.com/298[Kubernetes] Controller 와 Operator, 어디까지 까봤니?
틀린 부분이 있을 수 있습니다. 만약 있다면 지적해주세요 :) 쿠버네티스에서 ReplicaSet 을 통해 3개의 파드를 배포했다고 생각해봅시다. 이때 하나의 파드를 삭제하게 되면 곧 다시 파드가 생성
kingofbackend.tistory.com
Reconcile loop를 중심으로 ConfigMap, StatefulSet, ReplicaSet, Service account 리소스를 관리한다.
학습에는 매우 유용한 사례지만, 아무래도 현재의 app 서비스에 적용하기에는 부채가 크다.
이코에코는 전반적인 CD 파이프라인은 유지하되, Custom K8s Operator 대신 ArgoCD를 적용했다.
이코에코의 CI/CD 파이프라인을 포함한 GitOps 구축은 별도의 포스팅으로 기록해 둘 예정이다.
GitHub에 방문해서 살펴봐도 좋다.
🏗️ 이코에코 인프라 전략
오버 엔지니어링 회피

Rakuten CNP(Robin)는 1&1과 라쿠텐 모바일을 받치는 대규모 클라우드 프로덕션이기에 FastAPI + EC2 기반의 사이드 프젝으로 사용하기엔 무거운 스택이 많다.
CD는 자체 Kubernetes Operator로 구현된 걸로 모자라 배포 전략까지.. 이코에코는 MVP에 가까운 앱이다. CNP 스택들을 그대로 따오는 건 명백한 오5555버 엔지니어링이다.선택한 아키텍처
Terraform (AWS 인프라, Atlantis 배포) → Ansible (Kubernetes 클러스터) → GitHub Actions (CI: 빌드/테스트) → ArgoCD (CD: Kustomize 기반 배포)핵심 결정사항:
- ✅ Terraform: AWS 인프라 (EC2, VPC, ACM 등) 프로비저닝
- ✅ Ansible: Kubernetes 클러스터 구성 (kubeadm, CNI, Add-ons)
- ✅ GitHub Actions: 컨테이너 이미지 빌드 (각 마이크로서비스)
- ✅ ArgoCD + Kustomize: 선언적 배포 (7개 API + 2개 Worker)
- ✅ Atlantis: Terraform GitOps 자동화 (PR 기반 인프라 변경)

DevOps/Network/CNO(VM) 팀 등 회사 차원의 리소스가 없는 상태에선 선택의 폭이 넓지 않다.
개인용 macbook air가 끝인 상태에선 클라우드 컴퓨팅 리소스를 가져오는 것 외엔 별다른 대안이 없다.
맨바닥에 던져진 스타트업이라 가정하고 스택들을 선정했다.클라우드 플랫폼으론 AWS를 택했다.
최근 클라우드 발 장애가 터졌다지만(그러게 누가 엔지니어 막 자르라고 했나.) 여전히 가장 든든한 플랫폼이다.
제품군의 폭과 레퍼런스가 압도적이다. 인스턴스뿐 아니라 LB부터 도메인 호스팅, VPC, CM 등 제공하는 툴의 폭이 넓다.
그만큼 AWS의 의존도가 높아지는 건 단점이라 할 수 있겠다. 홀몸인 시점에선 구축/운영 난이도를 낮추는 게 우선이라 판단했다.
인프라/클러스터 구축엔 Terraform/Ansible을 활용했다.
Claude의 생산성을 극도로 끌어올리고 싶어 선언적 코드만으로 인프라/클러스터를 구축하고자 했다.
Terraform의 경우 학습 곡선이 낮고, 변경사항도 명확하게 디버깅이 가능해 Claude와 페어로 작업하기 적합했다.
순수히 코드 기반 인프라라 GitOps 파이프라인에 인프라를 그대로 실어 올릴 수 있는 점도 선택한 이유 중 하나다.
(돌아보니 이건.. 위험한 접근일 수 있겠다는 생각이다. 클러스터가 아닌 인프라가 코드 변경마다 바뀌는 건 리스크가 너무 크다. 25.12.16)

한 가지 고민되는 건.. Ansible은 절차적인 언어라 선언적 구조와 결합하기엔 제약사항이 많다.- CI에 Ansible Playbook을 태우면 파이프라인이 너무 무거워진다.
- 어플리케이션 수정은 CD에서 반영되는데 인프라 변경사항은 CI에서 반영되는 게 옳은 구존가..?
- 그렇다고 CD에 배치하기도 애매한 게 Ansible은 ArgoCD에서 상태 추적이 불가하다..
현재 이코에코 GitOps와 네임스페이스를 정비하며 고민 중인 제1 사안이 Ansible 퇴출이다.
부트스트랩엔 Ansible의 멱등성이 유용한 건 사실이다. playbook이 중단된 지점부터 이어서 진행할 수 있는 게 크다.
그렇지만 앞서 고민했듯 선언형 GitOps에 결합시키기엔 Ansible은 많은 병목이 따른다.
아마 부트스트랩 용도로 사용하고 폐기하지 않을까 싶다. 이와 관련된 의사결정들은 GitOps 포스팅에서 풀겠다.
🏛️ 14-Node 클러스터 구성
최종 아키텍처

역할 개수 용도 인스턴스 타입 Master 1 Control Plane t3a.large (2 vCPU, 8GB) API Nodes 7 auth, my, scan, character, location, info, chat t3a.medium (2 vCPU, 4GB) Celery Worker Nodes 2 storage-worker, ai-worker t3a.large(2 vCPU, 8GB) Infra Nodes 4 postgreSQL, Redis, RabbitMQ, Monitoring(Prometheus + Grafana + ArgoCD) t3a.medium (2 vCPU, 4GB) 총 리소스:
- vCPU: 30개
- Memory: 74GB
- 월 비용: ~$864 (시간당 $1.20)
4-Tier 아키텍처 적용
노드 구성을 4-Tier 아키텍처로 설계했다.- Tier 1 (Presentation) → ALB/Ingress
- Tier 2 (Business Logic) → API Nodes (7개), Celery Workers
- Tier 3 (Integration) → RabbitMQ
- Tier 4 (Data) → PostgreSQL, Redis
- Tier 0 (Observability) → Monitoring
각 API는 독립 네임스페이스로 격리했다. 도메인별 장애 격리와 NetworkPolicy 기반 접근 제어가 목적이다.
만약의 상황을 대비해 MQ Layer(Celery, RabbitMQ)를 포함시켰다.
AI 분의 개발 진척을 살펴봐야 알겠지만, Async가 필요한 상황이라면 즉시 도입할 수 있도록 추가해 둔 상태다.
레이어별 격리 전략Layer 네임스페이스 할당된 노드 NetworkPolicy Tier 1 kube-system에 포함, ALB Controller master node IMDS, In-cluster API, DNS, AWS API Egress 허용 Tier 2 각 API별 1:1 매칭 각 API별 1:1 할당 Data/Messaging 접근만 허용 Tier 3 messaging Infra node API Layer에서만 접근 허용 Tier 4 data Data node API Layer에서만 접근 허용 Tier 0 monitoring, atlantis Monitoring Node 모든 Layer의 메트릭을 수집 핵심 원칙
- 상위 계층은 단일 하위 계층에만 의존
- API 간 직접 통신 차단 (Zero Trust)
- Ingress/Egress 정책 면에서 default로 deny, 허용할 포트만 명시적으로 allow
- Data Layer는 Business Logic에서만 접근 가능
리소스 격리 고려사항초기엔 단일 api 네임스페이스에 7개 API를 몰아 배치했다. 빠른 개발엔 유리했지만 몇 가지 문제가 있었다:
- ❌ 특정 API의 메모리 누수 시 다른 API도 영향받음
- ❌ 도메인별 리소스 사용량 추적 불가
- ❌ NetworkPolicy 적용 불가 (모두 같은 네임스페이스)
도메인별 네임스페이스 분리로 이를 해결했다. 대신 복잡도가 확 올라갔다.
ALB의 경우 IMDS·Kubernetes API·AWS API에 접근하지 못해 다운되는 일도 발생했다.
결국엔 에러가 발생했던 지점의 egress만 허용해 ALB가 다운되던 문제를 해결했다.
우선 Network Policy로 접근 제어를 걸어놨지만 CNI 중심 운영으로 변경할 의향도 있다.
현재는 Kustomize의 yaml로 관리 중이다. 관련된 내용은 GitOps 포스팅에서 살펴보자.
배포 레이어(Kubernetes Manifests, Kustomize, ArgoCD, Ansible)를 전부 수정해야 했다. (Claude가 고생이 많았다.)
네임스페이스 리팩토링 과정은 별도의 포스팅으로 남길 예정이다.vCPU Limit 이슈


처음 4-nodes 클러스터 당시는 EC2의 vCPU limit에 걸리지 않았기에 인지하지 못했으나,
14-nodes로 확장하면서 30개가량의 vCPU가 필요해지자 vCPU limit이 16개로 설정됐었다.
aws cli로 vCPU limit 확장 요청을 보냈지만 생각보다 오래 걸려 긴급 메시지를 추가했다.
하루쯤 지나니 32개로 확장해 주셨더라. Anjali님께 감사할 따름이다.비용 vs 아키텍처
개인 프로젝트론 부담스러운 비용이다. 하지만 학습 관점에선 투자 가치가 있다고 본다.
얻은 것:
✅ 프로덕션급 멀티 노드 클러스터 구축 경험
✅ 4-Tier 아키텍처 실전 적용
✅ NetworkPolicy 기반 Zero Trust 네트워킹
✅ Node Affinity & 리소스 격리 전략
✅ 도메인별 독립 배포 파이프라인
잃은 것:
❌ 한 달 생활비의 상당 부분
비용이 과도해지면 다시 4-nodes로 줄일 예정이다. 클러스터 재구축은 Terraform + Ansible로 20분이면 되니 부담은 없다.
🎯 Terraform과 Ansible 역할 분리
먼저 Terraform과 Ansible의 역할을 명확히 분리하는 것이 좋다.
그렇지 않으면 tf와 yaml 스파게티에 빠진다. 스크립트 지옥은 덤이다. 나름의 시행착오 끝에 내린 결론이다.📐 Layer 0: Terraform (Infrastructure)
역할: AWS 인프라 생성
Terraform이 관리하는 리소스:
# terraform/main.tf resource "aws_vpc" "main" { ... } resource "aws_subnet" "public" { ... } resource "aws_security_group" "master" { ... } resource "aws_instance" "master" { ... } # 14개 EC2 resource "aws_acm_certificate" "main" { ... } resource "aws_route53_zone" "main" { ... } resource "aws_cloudfront_distribution" "cdn" { ... }생성되는 리소스 (총 ~60개):
- VPC, Subnets (5개), Internet Gateway, Route Tables
- Security Groups (Master, API, Worker, Infra, ALB)
- EC2 Instances (14개)
- EBS Volumes (각 30GB GP3, 암호화)
- IAM Roles (ALB Controller, EBS CSI Driver)
- Route53 Records (*. growbin.app)
- ACM Certificate (SSL/TLS) ← AWS ACM 사용!
- CloudFront Distribution
- S3 Buckets (이미지 저장소, Terraform State)
실행 시간: 15-20분
Terraform Output → Ansible Inventory
# Terraform 실행 후 terraform output -raw ansible_inventory > ../ansible/inventory/hosts.ini # 14개 노드 IP가 자동으로 Ansible Inventory에 주입됨
📐 Layer 1: Ansible (Cluster Configuration)
역할: Kubernetes 클러스터 구성
Ansible이 관리하는 설정:
# ansible/site.yml 1. OS 설정 (containerd, 커널 튜닝, Swap 비활성화) 2. Kubernetes 설치 (kubeadm, kubelet, kubectl v1.28) 3. Master 초기화 (kubeadm init) 4. Worker Join (13개 노드 Join - API 7 + Worker 2 + Infra 4) 5. Provider ID 주입 (ALB Controller 필수!) 6. CNI 설치 (Calico VXLAN) 7. Add-ons (Cert-Manager, Metrics Server, EBS CSI Driver) 8. Node 라벨링 + Taints (domain, phase, tier) 9. Cert-Manager (ACM 사용, Let's Encrypt 미사용!) 10. ALB Controller (Helm) 11. Monitoring (Prometheus + Grafana) 12. Atlantis (Terraform GitOps) 13. ArgoCD (Kubernetes CD)실행 시간: 25-40분
🔑 Phase 1: 클러스터 초기화

Step 1-4: 기본 구성
# ansible/site.yml - name: Prerequisites - OS 설정 hosts: k8s_cluster roles: - common # Swap 비활성화, 커널 파라미터, 방화벽 - name: Docker 설치 hosts: k8s_cluster roles: - docker # containerd 설치 - name: Kubernetes 패키지 설치 hosts: k8s_cluster roles: - kubernetes # kubeadm, kubelet, kubectl - name: Master 초기화 hosts: masters tasks: - import_tasks: playbooks/02-master-init.yml - name: Workers 조인 (13개 노드) hosts: workers,api_nodes,infra_nodes tasks: - import_tasks: playbooks/03-worker-join.yml중요: Worker Join이 실패하면 여기서 즉시 중단된다.
13개 노드 중 하나라도 Join에 실패하면 다음 단계로 진행하지 않는다.
🕸️ Phase 2: 네트워크 + Node 관리

Step 5: Provider ID 주입 ⚠️ 필수!
# ansible/playbooks/03-1-set-provider-id.yml - name: AWS CLI로 Worker 노드 정보 수집 shell: | aws ec2 describe-instances \ --filters "Name=tag:Name,Values=k8s-api-*,k8s-worker-*,k8s-*" \ "Name=instance-state-name,Values=running" \ --query 'Reservations[*].Instances[*].{ Name:Tags[?Key==`Name`].Value|[0], InstanceId:InstanceId, AZ:Placement.AvailabilityZone }' - name: Worker 노드별 Provider ID 설정 command: > kubectl patch node {{ item.Name }} -p '{"spec":{"providerID":"aws:///{{ item.AZ }}/{{ item.InstanceId }}"}}' loop: "{{ worker_instances }}"Provider ID 형식:
aws:///ap-northeast-2a/i-0123456789abcdef필수 이유:
- ALB Controller가 Node를 Target Group에 등록할 때 사용
- 누락 시:
failed to register targets: instance not found에러 - ALB Controller 설치 전 필수 스텝
실행 순서:
1. Worker Join 완료 확인 2. 각 Node의 AWS Instance ID 조회 3. kubectl patch node로 Provider ID 주입 4. ALB Controller 설치 (Step 10)Step 6: CNI 설치 (Calico VXLAN)
처음엔 Flannel로 구성했다가 파드가 재시작되거나 veth가 충돌하는 등 각종 오류가 발생했다.
그래서 그나마 사용 경험이 있는 Calico를 택해서 구성했다.
현재 노드의 수가 14개이기에 복잡도가 올라가는 건 원치 않아서 BGP는 껐다.
이외로는 Cilium, Kube-OVN이 있는데 단일 클러스터 환경에서 굳이 eBPF 기반 고속 통신 혹은 OVN은 원하지 않았다. 클러스터 내부에서 파드들이 서로 식별만 되면 OK였다.Calico VXLAN 선택 이유
VXLAN 모드: L2 패킷을 L3로 감싸 Overlay 네트워킹 - UDP 4789 포트 사용 - VNI(Virtual Network Identifier) 기반 격리 - tcpdump로 디버깅 시 L2 프레임 확인 가능 ✅ IPIP 모드 (선택하지 않음): - L3 패킷을 두 번 덧씌우는 형태 - 디버깅 시 직관적이지 않음 ❌개인적으로 IPIP보다 VXLAN을 더 선호하는 이유:
IPIP도 선택지에 있었지만 L3 패킷을 두 번 감싼 형태기에 선호하지 않는다.
혹시라도 tcpdump로 디버깅이 필요한 상황에서 단순히 터널링 인터페이스만 찍으면 L2 프레임이 나오는 VXLAN을 더 선호하는 편이다.
IPIP는 VNI가 필요 없기 때문에 페이로드 크기가 작아 효율적인 패킷 전송은 가능할 순 있다.
네트워크에서 병목이 발생한다면 IPIP vs eBPF(높은 복잡도를 감안해서라도) 중 하나를 선택하지 않을까 싶다.Pod-to-Pod 통신 예시
Auth Pod (192.168.1.10, Node 10.0.1.20) → VXLAN 캡슐화 (UDP 4789) → Calico CNI (VXLAN Tunnel) → Scan Pod (192.168.3.10, Node 10.0.2.20)참고: VPC CIDR
10.0.0.0/16, Pod Network CIDR192.168.0.0/16
Step 7-8: Add-ons & Node Labels
# Step 7: Add-ons - Cert-Manager v1.13.0 - Metrics Server - EBS CSI Driver (gp3 StorageClass) # Step 8: Node Labels & Taints - name: Label API Nodes shell: | kubectl label node {{ item.node }} \ workload=api \ domain={{ item.domain }} \ phase={{ item.phase }} \ --overwrite loop: - { node: "k8s-api-auth", domain: "auth", phase: "1" } - { node: "k8s-api-my", domain: "my", phase: "1" } - { node: "k8s-api-scan", domain: "scan", phase: "2" } # ... (14개 노드 전체)
🚨 Taints 추가 배경
초기 구축 땐 Taints가 포함되지 않았다.
그러다 각종 API 및 모니터링 파드들이 제멋대로 노드에 배치되는 걸 보고 추가한 설정이다.문제 상황
PostgreSQL 노드에 auth-api 파드가 배치됨! Redis 노드에 monitoring 파드가 배치됨!원인
NodeSelector만으로는 역방향 제어 불가! - NodeSelector: "이 파드는 특정 노드에 배치해" - Taints: "이 노드는 특정 파드만 받아" ← 필요!해결: Taints + Tolerations 조합
# ansible/playbooks/label-nodes.yml (Step 8) - name: Taint Infrastructure Nodes shell: | kubectl taint node {{ item }} \ node-role.kubernetes.io/infrastructure=true:NoSchedule \ --overwrite loop: - k8s-postgresql - k8s-redis - k8s-rabbitmq - k8s-monitoring적용 결과:
- ✅ API 파드는 인프라 노드에 배치 불가
- ✅ 인프라 파드만 Toleration으로 배치 가능
# k8s/infra/postgresql/deployment.yaml tolerations: - key: "node-role.kubernetes.io/infrastructure" operator: "Equal" value: "true" effect: "NoSchedule"
🔐 Phase 3: 인프라 컴포넌트

여기선 클러스터/네트워크 외 모니터링, CD 컴포넌트 등 유틸성 인프라 스택을 정비한다.
ALB에 ACM을 붙여주는 걸 포함해 모니터링 및 GitOps 툴들을 넣어준다.Step 9: ACM 사용
# ansible/playbooks/06-cert-manager-issuer.yml # ⚠️ Let's Encrypt ClusterIssuer 제거됨! # 이유: ALB에서 AWS ACM 인증서를 사용하므로 불필요 # Cert-manager는 Kubernetes 내부 인증서 관리용으로만 유지 - name: Cert-Manager 설치 command: kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml - name: Cert-Manager 준비 완료 확인 debug: msg: - "✅ Cert-Manager 설치 완료" - "📝 SSL/TLS는 AWS ACM으로 처리됨 (ALB에서 사용)" - "🔧 Cert-manager는 내부 인증서 관리용으로 대기 중"SSL/TLS 인증서 전략
Terraform → ACM Certificate 생성 (*.growbin.app) → Output: acm_certificate_arn → Ansible → ALB Ingress에 ACM ARN 주입# k8s/ingress/14-nodes-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: alb.ingress.kubernetes.io/certificate-arn: "{{ acm_certificate_arn }}" alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]' alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06 spec: rules: - host: api.growbin.app http: paths: - path: /api/v1/auth pathType: Prefix backend: service: name: auth-service port: number: 8000Cert-Manager vs AWS ACM 비교
자동 갱신 ✅ 90일마다 ✅ 자동 와일드카드 ✅ ✅ 설정 복잡도 중간 (ClusterIssuer 필요) 낮음 (Terraform) ALB 통합 복잡 (Secret 동기화) ✅ 네이티브 관리 포인트 Kubernetes AWS 장애 격리 Pod 장애 시 영향 AWS 관리 선택 이유 - ✅ ALB 네이티브 통합
결론: AWS ACM 사용으로 안정성 확보 + 설정 단순화
이전 사이드프로젝트들에선 Nginx와 Cert-Manager 혹은 Let's encrypt 조합을 자주 사용했다.
그러나 ALB L7으로 단일 진입점을 구성하고 K8s Ingress로 패스 기반 라우팅을 수행하면서 별도의 리버스 프록시는 채택하지 않았다.
그러나 auth 서버를 구현하면서 서버간 의존성 vs 도메인 외 공통 모듈 사용을 고민하는 지점이 되자 nginx의 authorization 함수가 그리워지기 시작했다..
공모전이 끝나면 Nginx Controller 혹은 서비스메쉬로 auth와 다른 도메인들과의 의존성을 줄여나갈 듯 싶다.
ACM의 경우 별다른 갱신도 필요없을 뿐더러 Terraform으로 코드 기반 관리를 할 수 있으니 보다 간편하다.
웬만하면 서비스 메시를 도입하고 싶지만 엔드포인트 개발이 계속 밀리기도 하고 무엇보다 리소스 비용이..
시간적 / 금전적 여유가 생기면 그 때 도입해 보자.
Step 10: ALB Controller
# ansible/playbooks/07-alb-controller.yml - name: Add AWS Load Balancer Controller Helm repo command: helm repo add eks https://aws.github.io/eks-charts - name: Install AWS Load Balancer Controller command: > helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=ecoeco-k8s --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller중요: Provider ID가 주입되지 않으면 ALB Controller가 Target Group에 Node를 등록하지 못한다.
ALB Traffic Flow사용자 (HTTPS 443) ↓ DNS Route53 (api.growbin.app) ↓ CNAME ALB (ACM 인증서로 SSL/TLS 종료) ↓ Listener Rule Target Group (target-type: instance, NodePort 목적지) ↓ AWS Load Balancer Controller TargetGroupBinding (serviceRef=auth-api, port=80) ↓ kube-proxy (iptables: NodePort → ClusterIP) Service (ClusterIP) ↓ Endpoints Pod (192.168.x.x:8000)
1. Ingress/Service 해석ubuntu@k8s-master:~$ kubectl get ingress -A NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE argocd argocd-ingress alb argocd.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h atlantis atlantis-ingress alb atlantis.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h auth auth-ingress alb api.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h auth health-ingress alb api.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h character character-ingress alb api.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h chat chat-ingress alb api.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h info info-ingress alb api.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h location location-ingress alb api.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h monitoring grafana-ingress alb grafana.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h monitoring prometheus-ingress alb prometheus.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h my my-ingress alb api.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h scan scan-ingress alb api.growbin.app k8s-ecoecomain-f37ee763b5-2088518262.ap-northeast-2.elb.amazonaws.com 80 13h
AWS Load Balancer Controller는 Ingress와 매칭되는 Service를 감시한다.
현재 클러스터는 api.growbin.app에 경로 /api/v1/* 형태로 Ingress를 등록했다.
2. TargetGroupBinding 생성ubuntu@k8s-master:~$ kubectl get targetgroupbinding -A NAMESPACE NAME SERVICE-NAME SERVICE-PORT TARGET-TYPE AGE argocd k8s-argocd-argocdse-f08bf3bc19 argocd-server 80 instance 3h10m ubuntu@k8s-master:~$ kubectl get targetgroupbinding -A -o yaml apiVersion: v1 items: - apiVersion: elbv2.k8s.aws/v1beta1 kind: TargetGroupBinding metadata: annotations: elbv2.k8s.aws/checkpoint: V2B3j7oi3EFdHOkc-JJwuDqSJiQyVo84sU6qXwdb3FQ/W1p8Lu4td8jDyA4xykcm7y95lajw__SD6Qr5AHZWkEg elbv2.k8s.aws/checkpoint-timestamp: "1763186894" creationTimestamp: "2025-11-15T06:08:13Z" finalizers: - elbv2.k8s.aws/resources generation: 1 labels: ingress.k8s.aws/stack: ecoeco-main name: k8s-argocd-argocdse-f08bf3bc19 namespace: argocd resourceVersion: "191275" uid: 2778b75b-e1d0-4724-8f2f-ce7d2bbdfaf0 spec: ipAddressType: ipv4 networking: ingress: - from: - securityGroup: groupID: sg-049c773912fbee03f ports: - port: 30080 protocol: TCP serviceRef: name: argocd-server port: 80 targetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-2:721622471953:targetgroup/k8s-argocd-argocdse-f08bf3bc19/67e1bd7fe8d9fd8f targetGroupProtocol: HTTP targetType: instance vpcID: vpc-08049a4dd788790fa status: observedGeneration: 1 kind: List metadata: resourceVersion: ""
컨트롤러는 Ingress 애노테이션(alb.ingress.kubernetes.io/target-type=instance)을 읽고
대응하는 TargetGroupBinding CR을 만들면서 serviceRef.name, serviceRef.port, networking.ingress 정보를 채운다.
target-type이 instance로, Pod가 아니라 노드가 등록된다. Service는 NodePort로 노출해 인스턴스와 동일한 IP를 공유한다.
Calico를 활용해 내부 클러스터망을 Overay로 구성했기에 Pod CIDR이 VPC 밖에 배치된다.
이 경우 ALB가 직접 Pod에 도달할 수 없으므로, 노드(NodePort)를 등록한 뒤 kube-proxy를 통해 Pod로 라우팅하는 구조로 구성했다.
3. 노드 등록
Target Group에는 k8s-api-auth, k8s-api-my 등 Ingress가 배포된 VPC의 모든 워커 노드가 NodePort와 함께 등록된다.
ALB는 해당 노드의 30080/30443 같은 NodePort로 트래픽을 전달하고,
kube-proxy가 iptables 규칙으로 ClusterIP → Pod(예: 192.168.x.x:8000)로 라우팅 한다.
4. 최종 흐름- 노드 → Pod
- 노드가 VXLAN 터널을 통해 Pod(192.168.x.x)와 통신
- kube-proxy가 iptables 규칙으로 ClusterIP → Pod IP를 매핑
- 외부 → Pod
- 외부 트래픽은 반드시 노드 주소(10.0.x.x) 로 들어와야 하므로, ALB가 Pod IP를 바로 타겟팅 불가
- ALB Target Group을 target-type: instance로 설정하고 NodePort를 통해 진입되도록 구성
- Pod → Pod (외부 노드)
- Pod가 다른 노드의 Pod와 통신할 때도 Calico가 VXLAN 캡슐화를 사용
- 소스 Pod IP는 192.168.x.x, 대상 Pod IP도 192.168.x.x Overlay 상 동일 서브넷 (L2)
- 실제로는 각 노드의 VXLAN 인터페이스(vxlan.calico)를 통해 패킷이 인캡/디캡되어 전달
- Pod -> Pod (동일 노드)
- 노드 내부 브릿지/루프백을 통해 즉시 전달 (로컬 L2 스위칭)
- src pod eth0 -> host calico-veth -> host dest veth -> dest pod eth0 순으로 전달
- 노드 내부 브릿지/루프백을 통해 즉시 전달 (로컬 L2 스위칭)
- Pod → 노드/외부
- Pod에서 외부나 노드로 나갈 때 Calico가 SNAT(기본 설정)로 Pod IP를 노드 IP(10.0.x.x)로 변환
- Pod가 인터넷이나 AWS 서비스(예: S3, STS)에 접근할 때는 외부에서 보기엔 노드의 프라이빗 IP만 노출
Step 11: Monitoring (Prometheus + Grafana)
# Prometheus Operator + Grafana 설치 helm install prometheus prometheus-community/kube-prometheus-stack \ -n monitoring \ --create-namespace
배포 방식- Helm으로 배포
- 모니터링 외에도 DB, Atlantis, ArgoCD 등 외부 패키지에 의존하는 컴포넌트는 Helm을 사용
- 이외 직접 작성하는 컴포넌트는 Kustomize를 사용 (namespace, Network Policy, API 등)
모니터링 대상
- 14개 노드 리소스 (CPU, Memory, Disk)
- 7개 API + 2개 Worker 파드
- 4개 인프라 파드 (PostgreSQL, Redis, RabbitMQ)
Step 12: Atlantis
Atlantis의 실제 역할
Atlantis = Terraform GitOps 자동화 도구 사용 시점: - ❌ 초기 클러스터 구축 (auto-rebuild.sh 사용) - ✅ 운영 중 인프라 변경 (PR 기반 자동화) 동작 흐름: 1. terraform/main.tf 수정 2. Pull Request 생성 3. Atlantis가 자동으로 terraform plan 실행 4. PR 코멘트에 Plan 결과 표시 5. 리뷰어 승인 후 atlantis apply 6. terraform apply 자동 실행 7. PR 자동 Merge
역할 분리:- 초기 구축:
./scripts/cluster/auto-rebuild.sh(Terraform + Ansible) - 운영 중 변경: Atlantis (PR 기반 자동화)
이점:
terraform apply수동 실행 최소화- 인프라 변경 리뷰 프로세스 개선
- Terraform State Lock 자동 관리
- 실수로 인한 인프라 삭제 방지
Step 13: ArgoCD (Kubernetes CD)
# ansible/roles/argocd/tasks/main.yml - name: Install ArgoCD command: > kubectl create namespace argocd kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml - name: Get ArgoCD initial password shell: kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -dArgoCD 구성:
- App of apps 패턴으로 7개 API + 2개 Worker 자동 배포
- Kustomize Base + Overlays (auth, scan, character, ...)
- Sync Wave로 배포 순서 제어 (Phase 1 → 2 → 3 → 4)
- Monitoring, DB 등 외부 의존 컴포넌트는 Helm-charts로 배포
🚀 전체 배포 흐름
자동 배포 스크립트
# ./scripts/cluster/auto-rebuild.sh #!/bin/bash # 1. Terraform Apply (15-20분) cd terraform terraform apply -auto-approve # 2. Ansible Inventory 생성 terraform output -raw ansible_inventory > ../ansible/inventory/hosts.ini # 3. EC2 부팅 대기 sleep 120 # 4. Ansible 실행 (25-40분) cd ../ansible ansible-playbook -i inventory/hosts.ini site.yml # 5. ArgoCD ApplicationSet 배포 (현재는 App of Apps 패턴으로 전환 중) kubectl apply -f ../argocd/applications/ecoeco-appset-kustomize.yaml # 6. 상태 확인 kubectl get nodes kubectl get applications -n argocd
총 소요 시간: 40-60분
초기 클러스터를 구성했을 땐 스크립트의 의존성이 높았다.
로컬에서 terraform/ansible로 클러스터를 구축, 점검, 삭제를 반복하니 시간 낭비와 피로가 함께 몰려와서.. 재구성이 필요했다.
Terraform은 AWS 인프라만 관리, Ansible에서 클러스터 구축을 온전히 수행하도록 수정했다.
클러스터에 ArgoCD와 Atlantis를 포함시켜 Git main branch merge 한 번으로 실제 클러스터에 인프라 변경사항이 반영되도록 수정했다. 인프라/클러스터 정비 및 자동화는 차후 GitOps 포스팅에 보다 상세히 기록해 두겠다.
💰 비용 정보
월간 비용 (24시간 Full 가동 기준)
Master: 1x t3.large ($0.0832/h) = $60.74/월 API: 7x t3a.medium ($0.0376/h) = $194.62/월 Workers: 2x t3a.large ($0.0752/h) = $109.79/월 Infra: 4x t3a.medium ($0.0376/h) = $109.79/월 총 시간당 비용: ~$1.20 총 월간 비용 (730h): ~$876실제 개발 비용 (하루 8시간 가동)
월 240시간 가동: ~$288 주말 포함 하루 12시간: ~$432참고: 개발 완료 후
terraform destroy로 즉시 삭제 가능!
📊 구축 결과
최종 클러스터 상태

1. 클러스터 노드 목록 (14nodes)ubuntu@k8s-master:~$ kubectl get nodes -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP k8s-api-auth Ready api 11h v1.28.4 10.0.1.16 <none> k8s-api-character Ready api 11h v1.28.4 10.0.1.237 <none> k8s-api-chat Ready api 11h v1.28.4 10.0.1.28 <none> k8s-api-info Ready api 11h v1.28.4 10.0.3.101 <none> k8s-api-location Ready api 11h v1.28.4 10.0.2.238 <none> k8s-api-my Ready api 11h v1.28.4 10.0.2.48 <none> k8s-api-scan Ready api 11h v1.28.4 10.0.3.159 <none> k8s-master Ready control-plane 11h v1.28.4 10.0.1.77 <none> k8s-monitoring Ready infrastructure 11h v1.28.4 10.0.2.245 <none> k8s-postgresql Ready infrastructure 11h v1.28.4 10.0.1.88 <none> k8s-rabbitmq Ready infrastructure 11h v1.28.4 10.0.2.14 <none> k8s-redis Ready infrastructure 11h v1.28.4 10.0.2.225 <none> k8s-worker-ai Ready worker 11h v1.28.4 10.0.1.87 <none> k8s-worker-storage Ready worker 11h v1.28.4 10.0.3.12 <none>
2. Ingressubuntu@k8s-master:~$ kubectl get ingress -A NAMESPACE NAME CLASS HOSTS PORTS AGE argocd argocd-ingress alb argocd.growbin.app 80 10h atlantis atlantis-ingress alb atlantis.growbin.app 80 10h auth auth-ingress alb api.growbin.app 80 10h auth health-ingress alb api.growbin.app 80 10h character character-ingress alb api.growbin.app 80 10h chat chat-ingress alb api.growbin.app 80 10h info info-ingress alb api.growbin.app 80 10h location location-ingress alb api.growbin.app 80 10h monitoring grafana-ingress alb grafana.growbin.app 80 10h monitoring prometheus-ingress alb prometheus.growbin.app 80 10h my my-ingress alb api.growbin.app 80 10h scan scan-ingress alb api.growbin.app 80 10h
3. namespaceubuntu@k8s-master:~$ kubectl get namespace NAME STATUS AGE argocd Active 11h atlantis Active 11h auth Active 11h cert-manager Active 11h character Active 11h chat Active 11h data Active 11h default Active 11h info Active 11h kube-node-lease Active 11h kube-public Active 11h kube-system Active 11h location Active 11h messaging Active 11h monitoring Active 11h my Active 11h rabbitmq-system Active 10h scan Active 11h
4. podsubuntu@k8s-master:~/workspace/backend$ kubectl get pods -A -o wide NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES argocd argocd-application-controller-0 1/1 Running 0 2d16h 192.168.235.199 k8s-master <none> <none> argocd argocd-applicationset-controller-6677f4579b-v5ml5 1/1 Running 0 21h 192.168.235.220 k8s-master <none> <none> ... argocd argocd-server-76485c5999-dnjpc 1/1 Running 0 4h21m 192.168.235.221 k8s-master <none> <none> auth auth-api-6f7d55bdf9-q5pjp 1/1 Running 0 4h58m 192.168.141.21 k8s-api-auth <none> <none> character character-api-6d7d6f6684-hbrbh 0/1 CreateContainerConfigError 0 5h30m 192.168.237.72 k8s-api-character <none> <none> chat chat-api-56d47ddf56-fcxkq 0/1 CreateContainerConfigError 0 5h30m 192.168.38.200 k8s-api-chat <none> <none> data-system dev-postgres-operator-6748bd649-2pm6p 1/1 Running 0 2d16h 192.168.235.203 k8s-master <none> <none> default node-debugger-k8s-redis-jkvg6 0/1 Completed 0 2d5h 10.0.2.44 k8s-redis <none> <none> default test-pull 0/1 CrashLoopBackOff 270 (4m17s ago) 22h 192.168.249.209 k8s-worker-storage <none> <none> grafana dev-grafana-b7b674795-wk2kt 1/1 Running 0 2d10h 192.168.134.201 k8s-monitoring <none> <none> info info-api-595cc978f6-g8v2v 0/1 CreateContainerConfigError 0 5h30m 192.168.6.200 k8s-api-info <none> <none> kube-system calico-kube-controllers-787f445f84-m65dm 1/1 Running 0 2d17h 192.168.249.194 k8s-worker-storage <none> <none> ... kube-system calico-node-z2nwg 1/1 Running 0 2d17h 10.0.3.129 k8s-worker-storage <none> <none> kube-system coredns-5dd5756b68-b9rsc 1/1 Running 0 2d17h 192.168.213.193 k8s-worker-ai <none> <none> kube-system coredns-5dd5756b68-jpzmd 1/1 Running 0 2d17h 192.168.213.194 k8s-worker-ai <none> <none> kube-system dev-aws-load-balancer-controller-6b866d894c-lzc5q 1/1 Running 0 4h13m 192.168.235.222 k8s-master <none> <none> kube-system ebs-csi-controller-7c9b66955d-jtddh 6/6 Running 0 2d5h 192.168.213.203 k8s-worker-ai <none> <none> ... kube-system ebs-csi-node-xvl4l 3/3 Running 0 2d5h 192.168.183.5 k8s-api-location <none> <none> kube-system etcd-k8s-master 1/1 Running 0 2d17h 10.0.1.205 k8s-master <none> <none> kube-system kube-apiserver-k8s-master 1/1 Running 0 2d17h 10.0.1.205 k8s-master <none> <none> kube-system kube-controller-manager-k8s-master 1/1 Running 0 2d17h 10.0.1.205 k8s-master <none> <none> kube-system kube-proxy-2bf56 1/1 Running 0 2d11h 10.0.1.134 k8s-api-auth <none> <none> ... kube-system kube-proxy-wkbdm 1/1 Running 0 2d11h 10.0.1.224 k8s-api-chat <none> <none> kube-system kube-scheduler-k8s-master 1/1 Running 0 2d17h 10.0.1.205 k8s-master <none> <none> location location-api-69c5455fb6-zp5gg 0/1 CreateContainerConfigError 0 5h30m 192.168.183.8 k8s-api-location <none> <none> my my-api-56c877d484-hzgbl 0/1 CreateContainerConfigError 0 5h30m 192.168.44.200 k8s-api-my <none> <none> platform-system dev-external-dns-66454745c6-l5zzp 1/1 Running 0 2d16h 192.168.235.206 k8s-master <none> <none> ... platform-system dev-external-secrets-webhook-645666bb64-jgn5q 1/1 Running 0 2d16h 192.168.213.200 k8s-worker-ai <none> <none> postgres dev-postgresql-0 1/1 Running 0 5h20m 192.168.80.81 k8s-postgresql <none> <none> prometheus alertmanager-kube-prometheus-stack-alertmanager-0 2/2 Running 0 2d16h 192.168.134.196 k8s-monitoring <none> <none> prometheus dev-kube-prometheus-stack-kube-state-metrics-75fb6d4b7b-cxjpr 1/1 Running 0 2d16h 192.168.213.202 k8s-worker-ai <none> <none> ... prometheus dev-kube-prometheus-stack-prometheus-node-exporter-x64rv 1/1 Running 0 2d16h 10.0.1.201 k8s-api-character <none> <none> prometheus kube-prometheus-stack-admission-create-kq4qg 0/1 Completed 0 2d10h 192.168.249.205 k8s-worker-storage <none> <none> prometheus kube-prometheus-stack-operator-55b4697db-q2x7d 1/1 Running 0 2d16h 192.168.134.195 k8s-monitoring <none> <none> prometheus prometheus-kube-prometheus-stack-prometheus-0 2/2 Running 0 2d16h 192.168.134.197 k8s-monitoring <none> <none> rabbitmq-system operator-5f979fb8b5-dplrv 0/1 ImagePullBackOff 0 2d3h 192.168.235.212 k8s-master <none> <none> redis dev-redis-node-0 2/2 Running 0 5h28m 192.168.37.157 k8s-redis <none> <none> redis dev-redis-node-1 2/2 Running 0 5h28m 192.168.37.158 k8s-redis <none> <none> scan scan-api-bcb97d958-6vf9s 0/1 CreateContainerConfigError 0 5h30m 192.168.118.136 k8s-api-scan <none> <none>
14개 노드 모두 Ready 상태, Calico, Monitoring, MQ, DB 등 각종 kube-system, 인프라 파드들이 정상 동작 중이다.
아직 api namespace pod들이 ImagePullBackOff인 점은 양해 바란다.
API 서버 개발은 진행 전이라 GHCR에 빌드된 도커 이미지가 비어있는 상태다
현재 AWS에 떠있는 클러스터의 버전은 14-nodes + namespace refactor까지 반영된 v0.7.2이다
클러스터 네임스페이스 리팩토링과 관련된 자세한 내용은 네임스페이스 리팩토링편에 작성해 두겠다.
Terrafrom + Operator 기반 선언형 GitOps 및 클러스터 관리는 v0.7.3에 적용될 예정이었으나..
Ansible을 완전 제거하는 과정에서 클러스터 부트스트랩이 엉켜 현재는 v0.7.2로 롤백한 상태다. (Thanks to GitHub)
예선 마감일이 얼마 남지 않아서 MVP용 API 개발부터 마무리한 이후에 GitOps 재정비를 시도할 예정이다.
Claude Sonnet 4.5와 함께 Operator 패턴 및 kubebuilder를 참고해서 설계 문서들만 쌓아둔 상태다.
Claude가 좋긴 한데 Cursor Max 옵션을 기준으로 input 토큰이 200k를 넘기면 비용이 2배로 청구되는게 부담이 크다.
그렇다고 Max 옵션을 끄면서 컨텍스트와 입력량을 줄여버리면 체감 성능이 반토막이 아니라 1/n 토막이 난다.
현재는 GPT 5.1 Codex Max 옵션을 사용 중이다. Claude가 헤집어 놓은 클러스터를 깔끔히 수습해 줘서 만족한다.
GPT 5.1 Codex가 Claude에 비해 편한 건 제시한 작업이 끝날 때까지 사용자에게 의사결정을 되묻지 않고 행동을 쭉쭉 이어간다.
정말 개발형 자율주행에 가깝다. 물론 점검은 필수다. 그렇지 않으면 헤집어 두는 경우도 있어서 점검 문서를 남기며 작업한다.
당분간 서류 지원은 멈추려 한다. 가끔 괜찮은 공고에 찔러보는 정도로 끝낼 듯 싶다. 지금은 프로젝트에 신경 쓰는 게 맞다.
🎯 회고: 프로덕션 경험의 가치
학습 곡선
이전까지 Terraform/Ansible을 직접 다뤄본 경험은 없다. 그 간극은 Claude와 Codex가 메워줬다.
K8s 클러스터 측면에서 CNP를 바라본 건 Rakuten CNP 네트워크 팀에 있을 때..?를 제외하면 없었다.
이전까진 클러스터에 들어가는 스토리지 서버를 C로 개발하는 시간이 많았다.트러블슈팅 과정
GitHub를 확인하면 Eco² 의사결정 및 트러블슈팅 과정을 md로 남겨뒀으니 확인하길 바란다.
K8s 클러스터 구축 과정을 조금이나마 엿볼 수 있을 거다.개발 기간
클러스터를 구축하는 기간은 일주일 정도 소요했다.
GitOps 일주일, 리팩토링+안정화엔 이틀이 소요됐다. (진행 중이다.)
🔮 반영되지 않은 피쳐
기존엔 이코에코에 서비스 메시를 추가할 생각이었다.
그렇지만 Istio는 리소스를 꽤 많이 잡아먹는다.
vCPU가 2.0 정도로 medium-small 인스턴스에선 다운이 나거나 리소스 여유가 크지 않았다.
현 스펙도 비용적으로 부담이기에 Istio 도입은 무리다. 차후 여유가 된다면 로깅은 붙여볼 생각이다.
현재 구성으로는 하루 8시간 구동 시 달에 40만 원 정도가 청구된다. 24시간 가동 시 월 120만 원이다..
파산하기 직전이지만.. 학습 및 복기 용도로는 이만한 구성이 없으니 투자라고 여긴다.
📚 다음 포스팅 예고

다음 포스팅은 선언형 GitOps 구축 과정 혹은 이코에코 API 개발 과정을 다룰 예정이다.
클러스터 구축은 구현 위주로 진행해서 그런지, 크게 막히는 부분은 없었지만.. 선언형 GitOps 구축엔 정말 많은 시행착오를 겪었다.
syncwave에 kustomize+helm을 안정적으로 적용하려면 리팩토링이 필수여서 작업이 꽤 오래 걸렸다.
Claude 4.5와 GPT 5.1 Codex를 하루종일 끼고 살면서도 2-3주정도 걸렸다.
그 과정에서 토큰값으로만 200만원 가까이를 태우다보니.. AI 회의론이 드는 이유도 조금 알 듯 싶다..
다루는 코드의 레인지가 늘어나는 만큼 한 번 삽을 잘못 뜨면 수정해야할 범위가 넓어진다.
'SW에서 괜히 의존성과 패턴에 집착하는 게 아니다.'라는 생각이 많이 드는 게,,,
영향을 받는 레인지가 모호하거나, 미흡한 상태로 삽을 푼다면 클로드의 의사결정 2-3번만에 클러스터가 망가질 수 있다.
언젠가 전직장 선임님이 '플랫폼 서버에 관심 많으시면 쿠버네티스 컨트롤러 패턴 한 번 공부해 보세요.'라는 말에 쟁여둔 쿠버네티스 패턴 서적들이 몇 권 있다.
부트스트랩 혹은 클로드 작업이 길어질 때면 한 두장씩 넘겨보곤 했다.
지옥의 GitOps Sync-wave 리팩토링이 어느정도 끝난 시점부턴 부트스트랩 과정이 사실상 생략돼버려서 그런 여유를 누릴 시간이 많이 없긴 하다.
API 배포가 자꾸 미뤄지는 상황에서 참.. 할 말이 아니긴 하나, LLM의 생산성 향상은 쓰면서도 놀라는 지점이 많다.
불과 1년 전만 하더라도 파이프라인 + API 서버를 홀로 다 짜는 건 물리적으로 무리였으나,
이제는 갈아넣을 몸과 의지, 토큰만 있다면 가능은 하다는 게 참.. 무시무시한 상황이다.
다음 포스팅은 프로젝트가 끝나면 이어쓸 듯 싶다.
클러스터 구축이 막 끝났을 시점엔 '시간이 좀 남네' 싶었는데 참.. 나이브했다.
GitOps Sync-wave를 잡고자 Helm / Kustomzie 구조를 두세번 갈아 엎으면서 역량 부족을 여실히 느꼈다.
나름대로 어느정도 정리가 됐긴 하나,, 2-3주가 훅 지나가버려서 여유 시간이 꽤 많이 증발했다ㅜ
이제서야 Auth 하나 띄우는 중이니.. 바쁘게 올려야한다. 안그럼 터진다.. 아무튼 예정 포스팅 목록은 아래와 같다.
GitOps- dev / prod 환경 분리
- RBAC / SA / Namespace / Ingress 등 클러스터 인프라 및 정책 구성
- CNI 트러블슈팅
- GitHub Actions CI 파이프라인: API/Kustomize/Helm lint test, 도커 이미지 빌드
- ArgoCD ApplicationSet -> App of Apps 리팩토링
- Sync-wave 배포 순서 제어
- ArgoCD + Kustomize/Helm CD 파이프라인 개발 및 리팩토링
- Helm + Kustomize 기반 Kubernetes 배포 구성 설계
- Helm charts와 Kustomize 오버레이를 활용한 Kubernetes 매니페스트 모듈화
- Operator + CR + CRD 기반 리소스 배포/관리
MVP API (auth, my, location, info, character)
- 이코에코 비 AI 피처 개발
- Sync 피처들로 Data Store에 직접 접근하는 구조로 구현
- Redis 기반 Cache-aside 패턴 우선 적용
📎 참고 링크
- GitHub: https://github.com/eco2-team/backend
- ArgoCD Dashboard: https://argocd.dev.growbin.app
- API Docs(Auth): https://api.dev.growbin.app/api/v1/auth/docs
'이코에코(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 #02 Ansible 의존성 감소, Kustomize Overlays 패턴 적용 (0) 2025.11.24