1.1 Kubernetes란 무엇인가
컨테이너 하나를 실행하는 일은 어렵지 않다. docker run 한 줄이면 충분하다. 문제는 서비스가 커지면서 시작된다. 컨테이너가 수십 개로 늘고 서버가 여러 대가 되면, 어느 서버에 무엇을 배치할지, 죽은 컨테이너를 언제 되살릴지, 트래픽이 몰릴 때 몇 개를 더 띄울지 결정하는 일이 사람의 손을 벗어난다. Kubernetes는 이런 결정을 대신 내리고 실행하는 컨테이너 오케스트레이션 플랫폼이다.
오케스트레이션이 푸는 문제
컨테이너를 여러 서버에 걸쳐 운영할 때 반복해서 마주치는 문제는 크게 세 가지다.
첫째는 배치다. 서버마다 남은 CPU와 메모리가 다르고, 어떤 컨테이너는 SSD가 있는 서버에만 두어야 하며, 같은 애플리케이션의 복제본은 서로 다른 장애 도메인에 분산해야 한다. 이 조건을 사람이 표로 관리하면 서버가 추가되거나 빠질 때마다 전체 배치를 다시 계산해야 한다. Kubernetes에서는 스케줄러가 각 워크로드의 자원 요구량과 제약 조건을 읽고 배치할 노드를 자동으로 결정한다.
둘째는 장애 복구다. 프로세스는 죽고, 서버도 죽는다. 오케스트레이터가 없으면 모니터링 알람을 받은 사람이 서버에 접속해 프로세스를 다시 띄우거나 다른 서버로 옮겨야 한다. Kubernetes는 컨테이너가 죽으면 재시작하고, 노드 전체가 응답하지 않으면 그 위에서 돌던 워크로드를 다른 노드에 다시 배치한다.
셋째는 스케일이다. 트래픽에 맞춰 복제본 수를 조절하는 작업이 선언된 숫자 하나를 바꾸는 일로 줄어들고, 부하 지표에 따라 그 숫자를 자동으로 조정하는 오토스케일링(Part VIII. 스케줄링과 자원 관리)으로도 이어진다.
이 밖에도 서비스 디스커버리, 무중단 롤링 업데이트, 설정과 시크릿의 배포처럼 분산 환경마다 매번 다시 만들게 되는 기반 기능을 플랫폼 차원에서 제공한다.
Borg에서 CNCF까지
Kubernetes는 갑자기 등장한 프로젝트가 아니다. Google은 2000년대 초반부터 Borg라는 내부 클러스터 관리자로 검색과 Gmail을 포함한 대부분의 서비스를 컨테이너 기반으로 구동해 왔고, 그 후속 연구 시스템인 Omega도 운영했다. Kubernetes는 이 두 시스템에서 얻은 교훈을 반영해 처음부터 오픈소스로 새로 설계한 세 번째 시스템이다. 물려받은 것은 코드보다는 설계 경험이다. 원하는 상태를 중심에 두는 사고, label로 워크로드를 묶는 방식, Pod라는 실행 단위(Borg의 alloc에 대응) 같은 개념이 Borg 운영 십여 년에서 나왔다.
조 베다(Joe Beda), 브렌던 번스(Brendan Burns), 크레이그 매클러키(Craig McLuckie)가 시작한 이 프로젝트는 2014년 6월에 공개됐다. 2015년 7월 1.0 릴리스와 함께 Google은 Kubernetes를 Linux Foundation 산하에 신설된 CNCF(Cloud Native Computing Foundation)에 기증했고, Kubernetes는 CNCF의 첫 번째 프로젝트가 됐다. 이후 특정 회사가 아닌 재단 중립 거버넌스 아래에서 개발이 이어지고 있으며, 현재는 해마다 세 번의 마이너 버전이 릴리스된다.
선언형 API와 reconciliation
Kubernetes 전체를 관통하는 설계는 하나다. 사용자는 절차를 지시하지 않고 원하는 상태(desired state)를 선언하며, 시스템이 현재 상태(current state)를 그 선언에 끊임없이 맞춘다.
“nginx 컨테이너를 3개 띄워라"라는 명령을 순서대로 실행하는 방식이라면, 실행이 끝난 뒤 컨테이너 하나가 죽었을 때 시스템은 아무것도 하지 않는다. 명령은 이미 완료됐기 때문이다. Kubernetes에서는 “nginx 3개가 실행 중인 상태"라는 선언이 API 서버를 통해 저장되고, controller라 부르는 프로세스가 루프를 돌며 선언과 현실을 비교한다. 하나가 죽어 2개가 되면 그 차이가 감지되고, controller가 하나를 새로 만들어 3개로 되돌린다. 이 반복 과정을 reconciliation이라 부른다.
flowchart TD
U["사용자"] -->|"원하는 상태 선언"| API["API 서버"]
API --> E[("etcd<br/>선언 저장")]
C["controller"] -->|"현재 상태 관찰"| API
C --> D{"선언과 일치?"}
D -->|"일치"| C
D -->|"불일치"| A["생성·삭제·수정"]
A --> API
장애 복구가 별도 기능이 아니라 이 루프의 자연스러운 결과라는 점이 중요하다. 컨테이너가 죽는 사건과 사용자가 선언을 3에서 5로 고치는 사건은 controller 입장에서 같은 종류의 일이다. 둘 다 선언과 현실 사이의 차이일 뿐이다.
이 구조는 이 노트 전체에서 반복해서 나타난다. Deployment가 Pod 수를 유지하는 것도, Service가 트래픽을 살아 있는 Pod로만 보내는 것도, 뒤에서 만날 Operator 패턴(Part XII. 패키징과 확장)도 전부 같은 reconciliation 루프의 변주다.
Kubernetes가 하지 않는 일
공식 문서는 Kubernetes가 전통적인 PaaS(Platform as a Service)가 아니라고 선을 긋는다. 컨테이너 수준에서 동작할 뿐 그 위의 개발 워크플로 전체를 떠안지 않으며, 다음 일은 범위 밖에 둔다.
- 소스 코드를 빌드하지 않는다. 이미지 빌드와 CI/CD 파이프라인은 Jenkins, GitHub Actions, Argo CD 같은 외부 도구의 몫이다.
- 미들웨어(메시지 버스 등)나 데이터베이스 같은 애플리케이션 수준 서비스를 내장 제공하지 않는다. 그런 워크로드를 Kubernetes 위에서 실행할 수는 있지만, 플랫폼이 기본으로 주지는 않는다.
- 로깅, 모니터링, 알림 솔루션을 강제하지 않는다. 지표를 수집할 메커니즘과 일부 통합만 제공하고 선택은 사용자에게 남긴다.
이 경계는 의도된 것이다. Kubernetes는 완성된 플랫폼이라기보다 플랫폼을 만들 조립 재료에 가깝고, 실제로 많은 관리형 서비스와 사내 플랫폼이 Kubernetes를 토대로 그 위에 만들어졌다.
정리
- Kubernetes는 배치, 장애 복구, 스케일을 자동화하는 컨테이너 오케스트레이션 플랫폼으로, Google Borg와 Omega의 설계 경험 위에서 2014년 공개됐고 2015년 1.0 릴리스와 함께 CNCF에 기증됐다.
- 사용자는 원하는 상태를 선언하고, controller가 reconciliation 루프를 돌며 현재 상태를 선언에 맞춘다. 장애 복구도 이 루프의 결과다.
- 소스 빌드, CI/CD, 미들웨어 내장 제공은 범위 밖이다. Kubernetes는 그 위에 플랫폼을 쌓기 위한 토대에 가깝다.