자세한 프로젝트 내용은 포트폴리오 — 모니 - 초보 투자자를 위한 AI 모의투자 플랫폼 에서 확인할 수 있습니다.
처음엔 Kubernetes로 가려고 했다. 매니페스트까지 다 써놨다. 근데 결국 안 썼다.
Docker는 컨테이너 하나를 어떻게 만들고 돌릴지에 대한 도구다. 이미지를 빌드하고, 컨테이너로 띄우고, 필요하면 docker-compose로 여러 컨테이너를 한 번에 묶어서 실행한다. 여기까진 익숙했다.
문제는 서비스가 11개가 되면서부터였다. 컨테이너 하나가 죽으면 누가 다시 살려주나. 트래픽이 몰리면 인스턴스를 어떻게 늘리나. 새 버전을 배포할 때 다운타임 없이 어떻게 바꿔치기하나. docker-compose는 이런 걸 직접 신경 써야 한다.
그래서 자연스럽게 Kubernetes 쪽을 보게 됐다.
Kubernetes(줄여서 K8s)는 컨테이너들을 여러 대의 서버에 걸쳐서 운영해주는 오케스트레이션 도구다. 한 줄로 말하면 "컨테이너가 죽으면 알아서 다시 띄우고, 트래픽에 맞춰 알아서 늘리고 줄여주는 시스템"이라고들 한다. 아래 항목들도 문서 보고 정리한 거라, 실제로 다 부딪혀본 건 아니다.
livenessProbe)readinessProbe)실제로 모니 프로젝트에서도 각 서비스별로 매니페스트를 다 만들어뒀었다. user-service만 봐도 이렇다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: moni
spec:
replicas: 1
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:latest
imagePullPolicy: Never
ports:
- containerPort: 8081
readinessProbe:
httpGet:
path: /actuator/health
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8081
initialDelaySeconds: 60
periodSeconds: 15apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: moni
spec:
replicas: 1
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:latest
imagePullPolicy: Never
ports:
- containerPort: 8081
readinessProbe:
httpGet:
path: /actuator/health
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8081
initialDelaySeconds: 60
periodSeconds: 15readinessProbe, livenessProbe까지 세팅해두고, 11개 서비스 전부 deployment.yaml / service.yaml을 만들었다. ingress.yaml, namespace.yaml도 있었다. 겉으로 보면 이미 K8s로 갈 준비가 다 된 상태였다.
K8s가 왜 이런 걸 다 해줄 수 있는지 이해하려면 몇 가지 기본 개념부터 봐야 한다.
deployment.yaml이 결국 이 Pod를 어떻게 띄울지 정의한 파일이다.replicas: 1이라고 써두면 Control Plane이 "Pod가 항상 1개는 떠 있어야 한다"는 걸 계속 확인하고, 죽으면 알아서 다시 띄운다.이 개념들이 서로 맞물려서 "선언한 상태를 유지해주는" 게 K8s의 핵심이다. replicas: 3이라고 써두면 실제로 3개가 떠 있도록 Control Plane이 계속 맞춰준다는데, 이게 docker-compose와 근본적으로 다른 지점이라고 한다 — Compose는 "지금 이 명령을 실행해라"에 가깝고, K8s는 "이 상태를 계속 유지해라"에 가깝다고. 다만 이것도 매니페스트 문서 보고 이해한 수준이지, 실제로 Pod가 죽었다 살아나는 걸 눈으로 본 적은 없어서 체감은 아직 모른다.
이유는 단순하다. 우리 규모에 비해 너무 오버였다.
Kubernetes는 최소 마스터 노드 + 워커 노드 여러 대로 클러스터를 구성하는 게 기본 전제다. EC2 몇 대짜리 소규모 인프라에서 이걸 그대로 가져가면, 클러스터 관리 자체에 드는 리소스와 비용이 서비스 운영 비용보다 커진다. 관리형 K8s(EKS 등)를 쓰면 컨트롤 플레인 비용부터 시작해서, 노드 그룹, 로드밸런서까지 다 따로 붙는다.
비용만 문제가 아니었다. 학습·운영 부담도 컸다.
돌이켜보면 가장 크게 다가온 실수는 이거였다. 서비스 코드는 하나도 없던 시점, 팀원들과 개발을 빠르게 시작하려고 모노레포 기준으로 Dockerfile, docker-compose, 기본 Config 서버 같은 걸 미리 세팅하던 단계가 있었다. 그때 docker-compose랑 Eureka를 건너뛰고 곧바로 Kubernetes로 가려고 했다.
근데 하다 보니 이게 크게 잘못될 것 같다는 느낌이 왔다. Eureka 없이 서비스 디스커버리를 K8s Service(DNS 기반)에 통째로 맡기려면 그만큼 K8s 쪽 구성을 처음부터 제대로 잡아야 했는데, 서비스 코드조차 없는 시점에 이 판단을 내리는 건 너무 일렀다. 그래서 접었다.
즉 Deployment, Service, Ingress, ConfigMap, Secret, HPA — 개념 하나하나가 다 붙어 있어서, 이미 Spring Cloud(Eureka, Gateway)로 서비스 디스커버리와 라우팅을 처리하고 있는 상황에서 K8s까지 얹으면 같은 역할을 하는 레이어가 두 개가 된다. Eureka가 하던 서비스 디스커버리를 K8s DNS가 대신하고, Gateway가 하던 라우팅을 Ingress가 대신하는 식으로 정리하려면 아키텍처를 통째로 다시 짜야 했다.
MVP 단계에서 검증해야 할 건 서비스 자체지, 인프라의 확장성이 아니었다. 그래서 K8s는 접었다.
k8s 사용으로 인한 유지보수/학습 비용 및 추가 금액적 부담
내부적으로 정리했던 이유도 결국 이거였다. 매니페스트는 저장소에 남겨뒀다. 지우기엔 아깝고, 나중에 트래픽이 실제로 늘어나서 필요해지면 그때 다시 꺼내 쓸 생각으로 말이다..
대신 docker-compose.yml 여러 개로 역할을 나눴다. 서비스용, 인프라용(DB, Redis, Kafka), 모니터링용(Tempo, Loki, Prometheus, Grafana)을 분리해서 관리했다. EC2 3대에 나눠 올리고, 죽으면 재시작 정책과 헬스체크로 최소한의 복구만 가져갔다.
K8s가 해주는 자동 복구, 오토스케일링, 롤링 업데이트 같은 걸 다 포기한 거냐면 그건 아니다. 오토스케일링이 필요할 만큼 트래픽이 크지 않았고, 롤링 업데이트는 CI/CD 파이프라인에서 헬스체크 후 문제 있으면 이전 이미지로 롤백하는 방식으로 대신 처리했다. 필요한 만큼만 직접 구현한 셈이다.
K8s를 검토하면서 K3s도 같이 봤었다. K3s는 Kubernetes의 경량화 버전이라고 한다. Rancher Labs에서 만들었고, 단일 바이너리로 뜨고 메모리도 훨씬 적게 먹는다고. 등록되지 않는 레거시 기능이나 클라우드 프로바이더 전용 드라이버 같은 걸 빼서 가볍게 만든 게 K3s라는데, 직접 띄워본 건 아니라서 체감 차이는 모른다.
"그럼 K3s면 되는 거 아니냐"는 생각도 들었다. 리소스 부담은 확실히 줄어든다. 근데 K3s를 쓴다고 해도 Deployment, Service, Ingress 같은 K8s 개념 자체는 그대로 다 써야 한다. 가벼워지는 건 클러스터를 띄우는 비용이지, 운영에 필요한 개념적 복잡도가 아니다.
우리가 포기한 이유는 "K8s가 무거워서"보다 "지금 규모에서 오케스트레이션 레이어 자체가 필요 없어서"에 더 가까웠다. 그래서 K3s로 낮춰서 가는 것도 답이 아니었다. 문제는 무게가 아니라 범위였다.
우리 규모에서는 오버였다고 했지만, 반대로 K8s가 진짜 필요해지는 규모의 회사들을 보면 이유가 명확해진다.
Spotify는 원래 자체 개발한 컨테이너 오케스트레이션 시스템(Helios)을 쓰고 있었다. 근데 이걸 유지보수하는 팀 규모가 작다 보니, 커뮤니티가 훨씬 큰 K8s로 옮기는 게 장기적으로 더 효율적이라고 판단해서 2018년부터 이전을 시작했다. 옮긴 뒤 가장 큰 서비스 하나가 초당 1000만 건 요청을 처리하는데, 오토스케일링 덕분에 수동으로 용량을 미리 잡아둘 필요가 줄었다고 한다.
Airbnb는 이유가 조금 다르다. 1,000명이 넘는 엔지니어가 동시에 서비스를 배포하는 상황에서, 배포 파이프라인을 수평으로 확장할 방법이 필요했다. 수백 개 서비스를 수동으로 운영하던 구조에서 K8s로 옮기면서, 설정을 표준화하고 배포 과정 자체를 통일할 수 있었다.
두 회사의 공통점은 "서비스 개수 + 엔지니어 수가 이미 관리 불가능한 규모"였다는 거다. Spotify는 자체 도구 유지보수가 부담될 만큼, Airbnb는 사람이 수천 명 규모로 배포에 관여할 만큼 커진 뒤에 K8s를 선택했다. 우리처럼 서비스 11개, 팀 6명 규모에서는 아직 이 문제 자체가 발생하지 않는다.
K8s는 서비스가 많아지고 트래픽이 커지면 분명히 필요해지는 도구다. 근데 "필요해질 것 같아서" 미리 가져가는 거랑 "지금 필요해서" 쓰는 건 다른 얘기다.
매니페스트까지 다 써놓고 결국 안 쓴 게 시간 낭비처럼 보일 수도 있는데, 오히려 그 과정에서 우리 인프라 규모에 뭐가 진짜 필요한지 명확해졌다. 지금은 Docker Compose로 충분하고, 트래픽이 실제로 늘어나는 시점이 오면 그때 다시 꺼내면 된다.
근데 솔직히 아직도 K8s는 잘 모르겠다. Pod, Service, Ingress 개념은 문서 보고 정리했고, 매니페스트로 서비스를 띄워보긴 했다. 근데 딱 껍데기만이다. 트래픽 받아본 것도 아니고, 노드가 죽어서 실제로 복구되는 걸 본 것도 아니다. 그러니 아는 척은 못 하겠다. 나중에 진짜 필요해져서 붙잡고 있게 되면 그때 제대로 부딪혀볼 생각이다.
참고 자료