당신에게는 k8s가 필요하지 않습니다

당신의 서비스에 Kubernetes가 필요할까요? 아마 아닐 겁니다.

새 프로젝트를 시작하면 인프라부터 고민하게 됩니다. 마이크로서비스, 서비스 메시, GitOps — 검색하면 K8s가 정답처럼 나옵니다. 하지만 사용자가 100명도 안 되는 서비스에 Netflix급 인프라가 필요할까요? Helm 차트 작성하는 시간에 기능 하나를 더 만드는 게 낫습니다.

K8s를 운영하다 보면 깨닫게 됩니다. 가장 많은 시간을 쓰는 건 서비스가 아니라 클러스터 자체라는 걸요. 인증서 갱신, 노드 업그레이드, RBAC 설정, 스토리지 클래스 관리, 모니터링 스택 유지. 서비스 몇 개 띄우려고 치르는 비용이 점점 목적을 넘어섭니다.

Docker Compose로도 충분히 멀리 갈 수 있습니다. 생각보다 훨씬 멀리요.

Docker Compose는 어디까지 버틸 수 있는가

로그 수집 서비스 Logtide는 Docker Compose 하나로 하루 50만 건의 로그를 처리하면서 99.8% 가용성을 유지하고 있습니다1. 서버는 Hetzner CX33 (4vCPU, 8GB RAM) 딱 한 대, 월 €12.50입니다2.

항목수치
일일 로그 처리500,000건
피크 처리량50 logs/sec
P50 응답 지연45ms
P95 응답 지연120ms
배포 시간30초
장애 복구 시간2분
월 비용€12.50

같은 워크로드를 Datadog에 맡기면 $800~1,200/월3. 관리형 K8s로 구성해도 $150~300/월입니다. 연간으로 따지면 $1,600~9,600 차이예요. 같은 금액으로 개발자 한 명이 한 달 더 제품에 집중할 수 있습니다.

Logtide의 확장 계획도 K8s가 아니라 Compose 위에서 시작합니다1.

규모구성비용
50만/일 (현재)서버 1대, Docker Compose~$14/월
500만/일서버 3대 (primary 1 + read replica 2), 수동 페일오버~$50/월
5,000만/일서버 10대+, K8s 또는 Swarm 검토 시점$500+/월

K8s가 해결하는 문제, 당신에게는 없는 문제

K8s는 좋은 도구입니다. 다만 특정 규모의 특정 문제를 풀기 위한 도구예요.

K8s가 필요한 조건Compose로 충분한 경우
5개 이상의 서비스 오케스트레이션서비스가 몇 개 안 된다
예측 불가능한 트래픽에 자동 스케일링트래픽이 예측 가능하다
멀티 리전 / 멀티 클라우드 배포단일 리전으로 충분하다
여러 팀의 독립적 배포소규모 팀이 함께 운영한다

오른쪽에 해당하는 항목이 더 많다면, K8s는 오버엔지니어링입니다.

기본 구성

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
services:
  nginx:
    image: nginx:latest
    restart: unless-stopped
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"

  db:
    image: postgres:16
    restart: unless-stopped
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres

핵심은 restart: unless-stopped입니다. 컨테이너가 죽으면 Docker 데몬이 알아서 재시작해줍니다. K8s의 파드 재시작 정책을 이 한 줄로 대체할 수 있어요.

헬스체크

컨테이너가 떠 있다고 서비스가 정상은 아닙니다. K8s의 liveness/readiness probe 같은 기능이 Compose에도 있어요.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  nginx:
    image: nginx:latest
    depends_on:
      db:
        condition: service_healthy

depends_oncondition: service_healthy를 걸면 DB가 준비된 뒤에 앱이 뜹니다. initContainers 같은 우회 없이 기동 순서를 잡을 수 있어요.

리소스 제한

K8s의 resources.limits와 같은 개념이 Compose에도 있습니다. 컨테이너 하나가 메모리를 독점하면 다른 서비스까지 같이 죽으니 걸어두는 게 좋습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
services:
  nginx:
    image: nginx:latest
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
        reservations:
          memory: 256M
          cpus: "0.5"

무중단 배포

bash 스크립트로 K8s의 롤링 업데이트를 어느 정도 대체할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash
# 새 이미지 풀
docker compose pull backend

# 레플리카를 3개로 확장 (기존 컨테이너 유지)
docker compose up -d --scale backend=3 --no-recreate

# 새 컨테이너가 healthy 상태가 될 때까지 대기
sleep 10

# 기존 컨테이너 교체 후 2개로 축소
docker compose up -d --scale backend=2

# Nginx 설정 리로드
docker compose exec nginx nginx -s reload

완벽한 블루-그린 배포는 아니지만, 대부분의 서비스에는 이 정도면 충분합니다.

시크릿 관리

K8s Secrets는 base64 인코딩이라 사실상 평문입니다. Compose의 secrets 기능이 오히려 나을 수 있어요.

1
2
3
4
5
6
7
8
9
services:
  nginx:
    image: nginx:latest
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

컨테이너 안에서 /run/secrets/db_password로 접근하면 됩니다. 환경변수와 달리 docker inspect에 노출되지 않고, .env보다 한 단계 안전하면서 K8s Secrets보다 설정이 단순합니다.

1
2
chmod 600 ./secrets/*
echo "secrets/" >> .gitignore

모니터링

K8s에서는 모니터링이 사실상 필수입니다. 클러스터 자체의 상태를 봐야 하니까요. Compose에서는 필요할 때 붙이면 됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
services:
  prometheus:
    image: prom/prometheus:latest
    restart: unless-stopped
    volumes:
      - ./config/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./data/prometheus:/prometheus
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    restart: unless-stopped
    volumes:
      - ./data/grafana:/var/lib/grafana
    ports:
      - "3000:3000"

같은 docker-compose.yml에 넣으면 docker compose up -d 한 번으로 전체 스택이 올라갑니다. kube-prometheus-stack 설치하다 반나절 날려본 적 있다면, 이 단순함이 얼마나 좋은지 아실 거예요.

백업

K8s에서 PersistentVolume 백업은 꽤 귀찮습니다. CSI 스냅샷 드라이버, VolumeSnapshot 리소스, 스토리지 클래스 설정… Compose는 호스트 디렉토리에 바인드 마운트되어 있으니 이걸로 끝입니다.

1
2
3
4
5
6
7
8
9
#!/bin/bash
# DB 덤프
docker compose exec db pg_dump -U postgres postgres | gzip > backup-$(date +%Y%m%d).sql.gz

# 파일 백업
rsync -az ./data/ remote:/backups/

# 30일 이상 된 백업 정리
find /backups -mtime +30 -delete

cron에 걸어두면 됩니다. Velero 설치하고 S3 버킷 연결하고 백업 스케줄 CRD 만들 필요 없어요.

그래도 K8s가 필요한 순간

Docker Compose에도 한계는 있습니다.

  • 자동 스케일링 — 트래픽에 따라 인스턴스를 자동으로 늘리고 줄이는 건 안 됩니다
  • 노드 페일오버 — 서버가 죽으면 서비스도 같이 죽습니다. 다중 노드 HA는 Compose의 영역이 아니에요
  • 서비스 디스커버리 — 서비스가 수십 개로 늘어나면 Compose의 DNS 기반 네트워킹만으로는 부족합니다
  • 멀티 테넌시 — 여러 팀이 독립적으로 배포하고 격리가 필요하다면 K8s의 네임스페이스가 맞습니다

다만 이런 요구사항이 실제로 생기는 시점은 대부분 생각보다 훨씬 늦습니다.

결론

기술 선택은 현재의 문제를 푸는 거지, 미래의 문제를 대비하는 게 아닙니다.

K8s를 도입하기 전에 스스로에게 물어보세요.

  • 서비스가 10개 넘는가?
  • 서버가 5대 넘는가?
  • 다중 리전이 필요한가?
  • 자동 스케일링 없이는 안 되는가?

전부 No라면, Docker Compose면 충분합니다. 남는 시간에 제품을 만드세요. 인프라는 제품이 아닙니다.