컨테이너를 잘 다루는 것과, 컨테이너로 만든 시스템을 잘 운영하는 것은 완전히 다른 이야기다.
docker run은 쉽다. docker-compose up도 쉽다. 그런데 운영 환경에 올리는 순간 사람이 손으로는 도저히 감당이 안 되는 문제들이 갑자기 쏟아진다.
이 글은 그 문제들이 정확히 뭐고, 오케스트레이터(Docker Swarm, Kubernetes)가 그걸 어떻게 풀어주는지 를 다룬다. 도구의 기능 나열이 아니라, 왜 이게 없으면 운영을 못하는가 를 짚는 게 목적이다.- why 오케스트레이터
서버 5대, 서비스 10개, 컨테이너 30개 정도.
오케스트레이터 없이 docker 명령어만으로 운영한다고 상상해보자.
이게 어떻게 망가지는지 하나씩 본다.
새 컨테이너를 띄울 때마다 결정해야 한다.
Server-1은 메모리 여유가 8GB 있고, Server-2는 4GB. Server-3은 CPU가 놀고 있고, Server-4는 이미 다른 서비스가 빡빡하게 돌고 있다... 이번에 띄울 컨테이너는 메모리가 좀 필요하니까 Server-1에 띄울까?
이걸 매번 사람이 판단한다. 서버 수가 5대 정도면 어떻게든 되지만, 20대, 50대가 되면 머리에 안 들어온다. 결과적으로:
# Swarm
docker service create --name web --replicas 3 nginx
# Kubernetes
kubectl apply -f web-deployment.yaml
띄우라고 말만 한다. 어디에 띄울지는 오케스트레이터가 자원 상황을 보고 결정한다. 메모리·CPU 여유, 라벨 제약(SSD 노드에만, 같은 앱끼리는 다른 노드에)까지 고려해서. 사람은 "5개 띄워줘"만 신경 쓰면 된다.
이게 스케줄링(scheduling) 이다.
운영 중인 컨테이너가 죽는 이유는 많다. OOM, 코드 버그, 외부 의존성 일시 장애, 호스트 하드웨어 문제...
오케스트레이터 없이는 이 흐름이다:
[알림 울림]
↓
SSH로 서버 들어감
↓
어느 컨테이너가 죽었는지 확인
↓
로그 보고 원인 파악
↓
docker run으로 다시 띄움 (정확한 옵션 기억해서)
↓
다시 잠
그리고 호스트 자체가 죽으면 더 끔찍하다. 그 호스트에 있던 모든 컨테이너를 다른 호스트로 옮겨야 한다. 어디로? 어떻게?
선언만 해두면 된다 — "이 앱은 항상 3개가 떠 있어야 한다."
replicas: 3
그러면 시스템이 끊임없이 현재 상태 와 원하는 상태 를 비교한다.
Desired: 3개 Actual: 3개 ✓ OK
──── 컨테이너 1개 죽음 ────
Desired: 3개 Actual: 2개 ✗
↓
"1개 더 띄워야 함"
↓
자동으로 새 컨테이너 생성
↓
Desired: 3개 Actual: 3개 ✓ OK
──── 호스트 1대 죽음, 거기 있던 2개 사라짐 ────
Desired: 3개 Actual: 1개 ✗
↓
살아있는 다른 노드에
2개 자동 재배치
↓
Desired: 3개 Actual: 3개 ✓
새벽 3시 알림이 안 온다. 시스템이 알아서 회복한다. 이게 자가 치유(self-healing) 다.
마이크로서비스라면 서비스끼리 서로를 호출한다. payment-service가 order-service를 호출하려고 한다. 그런데 order-service는...
Pod의 IP는 계속 바뀐다. 그런데 payment-service는 매번 어디로 요청을 보내야 하는지 어떻게 아는가?
오케스트레이터 없이 풀려면:
오케스트레이터에 "서비스(Service)" 라는 추상화가 내장돼 있다.
# K8s 예시
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector: { app: order-service }
ports: [{port: 80}]
이제 payment-service는 order-service라는 이름으로만 부르면 된다.
// payment-service 코드
restTemplate.getForObject("http://order-service/orders/123", Order.class);
내부에서 무슨 일이 일어나나:
Pod 위치가 바뀌어도 클라이언트 코드는 한 줄도 안 바뀐다. 이게 서비스 디스커버리 + 로드밸런싱 이다.
새 버전을 배포해야 한다. 옵션은 두 가지.
옵션 A: 한 번에 다 바꾸기
옵션 B: 한 대씩 직접 바꾸기
선언만 해둔다 — "한 번에 1개씩 교체하고, 새 거 ready 되면 다음 거 교체해."
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
흐름:
v1 v1 v1 v1 v1 (배포 시작)
↓
v1 v1 v1 v1 v1 v2 (v2 1개 추가)
↓ (v2 ready 됨)
v1 v1 v1 v1 v2 (v1 1개 제거)
↓
v1 v1 v1 v2 v2 ... 반복 ...
↓
v2 v2 v2 v2 v2 (완료, 다운타임 0)
그리고 새 버전이 ready 못 받으면 자동으로 멈춘다. 롤백 명령 한 줄로 이전 버전으로 돌아갈 수 있다.
kubectl rollout undo deployment/order-service
새벽 SSH 노동이 한 줄짜리 선언으로 바뀐다. 이게 롤링 업데이트(rolling update) 다.
평소 3개 인스턴스로 충분한 서비스가 있다. 그런데 이벤트가 시작되면 10배 트래픽이 몰린다.
선택지는?
A. 평소에도 많이 띄워두기
B. 이벤트 직전에 사람이 늘리기
C. 트래픽 보고 자동으로 늘리고 줄이기
CPU 사용률, 메모리, 커스텀 메트릭(요청 수, 큐 길이 등) 기반으로 자동 스케일링이 내장돼 있다.
# K8s HPA 예시
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
minReplicas: 3
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
CPU 평균이 70% 넘으면 자동으로 Pod을 늘리고, 떨어지면 줄인다. 이벤트가 끝나면 다시 3개로 줄어든다. 사람이 안 끼어들어도 된다.
이게 오토스케일링 이다.
각 문제를 다시 보면 패턴이 보인다.
| 문제 | 사람이 풀어야 하던 것 | 오케스트레이터의 해결 |
|---|---|---|
| 어디에 띄울지 | 매번 사람이 결정 | 스케줄러가 자동 배치 |
| 죽으면 살리기 | SSH로 재시작 | 자동 재시작/재배치 |
| 서비스 위치 추적 | 설정 파일에 IP 박기 | 서비스 디스커버리 |
| 무중단 배포 | 한 대씩 수동 교체 | 자동 롤링 업데이트 |
| 트래픽 변동 | 미리 띄우거나 장애 | 자동 스케일링 |
공통점은 하나다.
"원하는 상태"만 선언하면, 시스템이 끊임없이 현재 상태와 비교하면서 자동으로 맞춰준다.
사람이 매번 명령을 내리는 게 아니라, "이렇게 되어 있어야 한다"고 한 번 선언하면 끝이다. 그 뒤로는 시스템이 알아서 desired state를 유지한다.
이게 오케스트레이터의 본질이다.
위 5가지 문제를 둘 다 풀어준다. 그럼 뭐가 다른가?
Docker가 직접 만든 오케스트레이터. Docker 명령어를 그대로 확장한 형태다.
docker service create --name web --replicas 3 nginx
docker service scale web=5
docker service update --image nginx:1.25 web
docker run을 알면 학습 곡선이 거의 없다. 작은 클러스터, 작은 팀에 잘 맞는다. 위 5가지 문제는 이걸로 충분히 풀린다.
Google이 내부 시스템(Borg)의 경험을 일반화한 플랫폼. Swarm과 결정적으로 다른 두 가지:
(1) 추상화의 풍부함
service 하나로 표현 어려운 워크로드가 많다 — 노드마다 하나씩 떠야 하는 로그 수집기, 일회성 배치 작업, 순서 보장이 필요한 stateful 시스템, 주기적으로 도는 크론 작업...DaemonSet, Job, StatefulSet, CronJob 같은 별도 리소스로 가진다.(2) 확장 가능한 플랫폼
Swarm은 단순함을 무기로 했지만, 결국 이 생태계 격차를 따라잡지 못했다. CNCF 표준이 되고 AWS/GCP/Azure가 모두 매니지드 K8s를 제공하면서 사실상 표준의 자리는 K8s가 가져갔다.
이 글에서 하고 싶었던 건 하나다.
컨테이너를 잘 띄울 줄 안다고 끝이 아니다.
여러 컨테이너가 여러 호스트에서 동시에 살아 움직이는 시스템을 만들려면, 사람이 매번 결정하던 일들을 시스템이 자동으로 처리하게 만들어야 한다.오케스트레이터가 푸는 건 결국 그 모든 자동화의 묶음이다.
처음 K8s를 봤을 때 왜 이렇게 복잡한 게 표준이 됐지? 싶었는데, 위 5가지 문제를 하나씩 직접 만나보고 나서야 아, 이걸 자동으로 풀어주는 시스템이 없으면 정말 안 되겠구나 가 느껴졌다. 도구가 복잡한 게 아니라, 풀어야 하는 문제 자체가 복잡한 거였다.