01. Introducing Kubernetes
Overview of Kubernetes Architecture (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-1)
기존에는 소프트웨어가 커다란 덩어리였지만 최근에는 독립적으로 작동하는 작은 마이크로서비스(microservice)로 나뉘고 있다. 이들은 독립적으로 동작하기 때문에, 개발하고 배포하거나 스케일링을 따로 해줄 수 있다는 장점이 있으며, 이 장점은 빠르게 변화하는 소프트웨어의 요구사항을 반영하기에 적합하다.
마이크로서비스를 여러 개 띄우기 시작하면 이들을 효율적으로 관리하는 방법이 필요하다.
쿠버네티스(Kubernetes)는 하드웨어 인프라를 추상화하여 데이터 센터 (혹은 소프트웨어가 운영되는 서버나 클라우드)를 커다란 하나의 리소스로 바라볼 수 있게 해준다.
1.1 쿠버네티스가 필요한 이유
1.1.1 하나의 거대한 앱에서 마이크로서비스로
소프트웨어가 거대한 하나의 앱이면 오랜 기간 유지 보수를 거치면서 지나치게 복잡해지고, 의존성이 늘어나기 마련이다.
서버에 부하가 늘어나게 되면 스케일링을 해야한다.
스케일링의 종류
- Vertical scaling (scaling up): CPU/메모리 추가 등, 코드를 수정할 필요는 없지만 비싸다.
- Horizontal scaling (scaling out): 서버 증설 등, vertical scaling 에 비하면 싼 편이지만, 코드를 수정해야 하기도 하며, 때로는 불가능한 경우도 있다 (ex. DB)
만약 앱이 하나의 거대한 덩어리라면, 일부가 스케일링이 불가능하면 전체도 스케일링 할 수 없다. 한편 이를 작은 마이크로서비스로 나누면?
마이크로서비스는 독립적인 프로세스로 동작하며, 다른 마이크로서비스와 API를 이용하여 통신한다. 독립적이기 때문에 해당 마이크로서비스가 하려는 일에 적합한 언어를 사용하여 서비스를 만들면 된다. 또 각 마이크로서비스 개별적으로 개발하고 배포할 수 있다. 다른 마이크로서비스에 변경 사항이 생기더라도 API만 유지된다면 의존성을 크게 고려하지 않아도 된다.
따라서 마이크로서비스는 서비스별로 스케일링해주면 되며, 부하가 심한 마이크로서비스만 스케일링 해주면 되고, 나머지 부분은 그대로 두면 된다!
마이크로서비스의 단점
마이크로서비스로 애플리케이션을 나누게 되면 각 서비스끼리 통신을 하게 될텐데 스케일링을 많이 하면 할수록 통신을 더 많이 해야한다. 복잡도가 증가하게 된다. 복잡한 시스템을 효율적으로, 에러 없이 운영하는 것은 어렵다.
디버깅할 때 골치아프다. 문제가 여러 마이크로서비스에 걸쳐있을 수도 있다.
마이크로서비스 마다 요구하는 라이브러리가 다를 수 있고, 같은 라이브러리를 사용하지만 버전이 다를 수도 있다. 의존성을 관리하기 어려워 질 수도 있다.
1.1.2 앱에 일관된 환경 제공
개발 환경과 운영/배포 환경은 다르다!
개발 환경과 배포 환경을 동일하게 만들어서 같은 운영체제, 라이브러리, 시스템 설정, 네트워크 환경 등을 똑같이 할 수 있다면 배포 환경에서만 생기는 문제들 때문에 골치 아파야 할 일은 줄어들 것이다.1
1.1.3 지속적인 배포: DevOps and NoOps
DevOps: 애플리케이션 개발, 배포와 운영을 담당
개발자가 애플리케이션 배포와 운영에 참여하게 되면서 사용자의 필요나 이슈를 잘 파악할 수 있게 되고, 운영에 있어서 발생하는 어려움을 더 잘 이해하게 된다.
NoOps: 개발자가 하드웨어 인프라에 대한 지식 없이, 또 운영 팀의 도움 없이 서비스를 배포
1.2 컨테이너 기술 소개
쿠버네티스에서는 리눅스의 컨테이너 기술을 사용하여 애플리케이션의 동작을 분리한다.
1.2.1 컨테이너란?
마이크로서비스를 운영하려면 각 서비스별로 독립적인 환경을 제공해야 한다. 이를 하기 위해서는 각 마이크로서비스를 가상 머신(virtual machine)에 올리면 된다.
하지만 VM은 너무 시스템 자원을 많이 소모하고, 각 VM마다 설정을 따로 해야하니 피곤하다.
대안으로 컨테이너 기술을 사용한다. 여러 개의 서비스가 하나의 머신에서 작동하면서도 각 서비스에게 독립적인 환경을 제공할 수 있게 되며, VM에 비해 훨씬 가볍다.
컨테이너에서 돌아가는 프로세스는 호스트(컨테이너를 실행한) 머신의 운영체제 아래에서 돌아간다. (다른 프로세스들과 마찬가지!)
VM과의 차이점
VM은 독자적인 운영체제를 사용하기 때문에 시스템 프로세스가 필요하고 이는 추가적인 리소스의 소모로 이어진다. (윈도우 VM에 RAM 4GB씩 준다고 생각하면 몇 개를 띄울 수 있을지…)
반면 컨테이너는 호스트 머신에서 돌아가는 프로세스이기 때문에 추가적인 시스템 프로세스가 필요 없어서 애플리케이션이 필요한 리소스만 소모하게 된다. VM에 비해 훨씬 가볍기 때문에 한 머신에서 여러 컨테이너를 돌릴 수 있게 된다.
VM을 사용하게 되면 hypervisor 가 하드웨어 자원을 가상 자원(virtual resource)으로 나누어 각 VM안의 OS가 사용할 수 있게 해준다. VM 안에서 돌아가는 애플리케이션은 VM의 OS에 system call 을 하게 되고, VM의 커널은 hypervisor를 통해 호스트의 CPU에서 명령을 수행하게 된다.
컨테이너는 호스트의 OS 커널에서 돌아가기 때문에 가상화 작업이 불필요하다. 결국 시스템 자원을 VM에 비해 적게 소모하므로 실행도 빠르다.
컨테이너의 동작 원리
- Linux Namespace
각 프로세스가 독립적으로 시스템을 바라보게 되어 독립적인 환경을 제공한다. 같은 namespace 에 있는 자원만 확인할 수 있으므로, 여러 namespace 를 만들고 자원을 할당하면 프로세스 입장에서는 시스템 내에서 독립적으로 돌아가는 것 처럼 보일 것이다.
- Linux Control Groups (cgroups)
각 프로세스가 사용할 수 있는 자원에 제한을 걸어둔다.
1.2.2 Docker (도커)
애플리케이션을 패키징하고, 배포하고 실행하는 플랫폼이다.
VM 이미지가 아니라 용량이 더 작은 컨테이너 이미지를 사용한다.
컨테이너 이미지는 여러 layer 로 구성되어 있어서 이를 공유하거나 재사용 할 수 있다. 같은 layer 가 필요한 다른 서비스에서 그대로 가져다 쓸 수 있게 된다.
(실제로 Dockerfile 명령 줄 별로 이미지 layer 를 구성하던 것 같다. Hash 를 떠두고 만들어 둔 이미지가 있으면 새로 만들지 않고 그대로 가져와서 쓰던 기억이 있다.)
공유하는 대신 이미지 layer 는 read-only 이다. 컨테이너가 실행되면 writable layer 가 layer 의 맨 위에 생겨서 해당 layer 에 필요한 작업을 하게 된다.
도커의 3가지 주요 개념
이미지(Images): 애플리케이션과 애플리케이션이 동작하는 환경을 패키징한 것이다. ‘환경’에는 파일시스템이나, 실행 파일들의 경로와 같은 메타데이터도 들어간다.
레지스트리(Registries): 도커 이미지의 저장소라고 생각하면 쉽다. 이미지를 만들어서 저장소에 push 할 수 있고, 직접 만든 혹은 만들어진 공식 이미지들을 pull 해서 사용할 수 있다.
컨테이너(Containers): 이미지로부터 만들어진 컨테이너로 호스트에서 돌아가는 하나의 프로세스이지만, 리눅스 컨테이너 기술이 적용되어 있어 독립적인 프로세스처럼 돌아간다.
컨테이너 이미지의 한계
모든 컨테이너는 호스트의 머신에서 돌아가는 프로세스이기 때문에 해당 컨테이너가 요구하는 커널 버전이나 운영체제, 아키텍쳐 (x86)가 있다면 해당 조건이 만족 되어야 컨테이너를 실행할 수 있다. M1 맥에서 도커 지원을 위해 추가 개발이 필요한 이유이다.
VM은 자체적으로 OS를 가지고 있기 때문에 VM을 사용하게 되면 이런 문제가 없다.
1.2.3 rkt (rock-it)
도커와 같은 컨테이너 엔진이다.
보안, 결합성, 표준을 지키는 것에 조금 더 중점을 둔다.
1.3 쿠버네티스 소개
마이크로서비스를 스케일링 하고 컨테이너가 많아지면 운영이 어려워진다! 이를 효율적으로 할 수 있게 해줄 도구가 필요하다.
1.3.1 쿠버네티스의 기원
구글에서 Borg, Omega 와 같은 서비스 관리 시스템을 자체 개발하여 사용하다가 2014년에 쿠버네티스를 공개했다.
1.3.2 High-level에서 바라본 쿠버네티스
쿠버네티스는 컨테이너화 된 애플리케이션을 쉽게 배포하고 운영하게 해주는 소프트웨어이다.
수많은 노드에서 돌아가는 애플리케이션이 마치 한 컴퓨터에서 돌아가는 것처럼 운영해준다.
클러스터를 위한 운영체제로 생각할 수 있다.
개발자의 입장에서는 인프라 관련된 기능들을 애플리케이션에 덜 넣어도 된다. (서비스가 준비되었는지 알아낸다거나, 스케일링, 로드 밸런싱, 에러로부터 회복, leader election 등) 개발자는 이제 애플리케이션의 기능에 더욱 집중할 수 있다. 인프라와 통합하는데 개발 리소스를 투입하지 않아도 된다.
쿠버네티스를 사용하면 사람이 직접 컨테이너와 작업을 스케쥴링 하는 것보다 자원을 효율적으로 사용할 수 있게 된다.
1.3.3 쿠버네티스 클러스터 구조
쿠버네티스 시스템에는 마스터 노드(master node)와 워커 노드(worker node)가 있다.
- 마스터 노드: Kubernetes Control Plane 이 실행되고, 쿠버네티스 시스템을 관리
- 워커 노드: 배포한 애플리케이션이 동작하는 노드
Kubernetes Control Plane
클러스터를 제어하고 동작하게 한다.
Kubernetes API 서버: Control Plane 내부의 각 부분이 서로 통신할 때 사용하는 API 서버이다. 워커 노드에서 API를 이용하기도 한다.
스케쥴러: 애플리케이션 배포시 워커 노드에 서비스를 할당한다.
Controller Manager: 클러스터 수준의 기능을 담당한다. 컴포넌트를 복제하거나 워커 노드 개수를 관리하거나, 노드 에러를 처리하는 등 작업을 담당한다.
etcd: 클러스터의 설정을 저장하는 persistent 분산 데이터 스토어이다.
Worker Nodes
- Container runtime: 컨테이너를 실행해준다.
- Kubelet: API 서버와 통신하고 노드의 컨테이너를 관리한다.
- Kubernetes Service Proxy (kube-proxy): 애플리케이션 컴포넌트 간 네트워크 로드 밸런싱을 담당한다.
1.3.4 쿠버네티스에서 애플리케이션 실행
우선 컨테이너 이미지를 만들어서 이미지 레지스트리에 등록해둬야 한다.
그리고 Kubernetes API 서버에 애플리케이션의 구성 명세를 알려줘야 한다.
명세에는 애플리케이션의 컴포넌트들에 대한 정보, 각 컴포넌트들의 관계, 같은 노드에 실행되어야 하는 컴포넌트들, 컴포넌트의 복제본 개수, 외부로 공개할 컴포넌트 등에 대한 여러 정보가 들어있다.
API 서버가 애플리케이션의 구성 명세를 처리하면, 스케줄러는 컨테이너들을 적절하게 노드에 할당한다. Kubelet은 Container Runtime 에게 필요한 이미지를 가져와 실행하라고 한다.
컴포넌트의 복제본 개수(replica)를 정해두면 이 값을 지키도록 쿠버네티스가 자동으로 동작한다. 만약 개수가 적으면 더 실행하고, 많으면 줄인다. 또 들어오는 요청이 많거나, CPU 부하가 심할 때와 같이 상황에 맞게 자연스럽게 스케일링도 해준다. 이렇게 하나의 클러스터 내에서 컨테이너를 옮겨다니며 리소스를 효율적으로 쓰기도 하고 관리를 편하게 해준다.
kube-proxy 의 경우 하나의 고정된 IP 주소로 들어오는 요청에 대해 로드 밸런싱을 수행하여 클러스터 내의 적절한 컨테이너에게 작업을 넘겨준다.
1.3.5 쿠버네티스 사용의 장점
개발자들은 클러스터를 구성하는 서버에 대해서 잘 몰라도 된다. 추상화로 인해 모든 노드/클러스터가 커다란 서버가 된다.
물론 서버에 GPU가 필요하거나 추가적인 제약 조건이 따른다면 이를 잘 조절해 줘야 한다. (AWS GPU 인스턴스, DB 인스턴스 등)
쿠버네티스를 사용하게 되면 애플리케이션과 인프라가 분리된다! 무엇보다 개발 환경과 배포 환경을 유사하게 설정할 수 있어서 편해진다.
쿠버네티스가 하드웨어 리소스를 효율적으로 사용할 수 있도록 도와준다. 컴포넌트들을 관리하며 스케쥴링을 해주고 오류 발생시 자동으로 처리해주고 (노드 재시작 등), 부하가 심한 경우 자동으로 스케일링을 제공한다.
새로운 버전의 애플리케이션을 배포할 때 연속적인 배포를 할 수 있게 된다. 중간에 서비스를 중단하지 않아도 된다.
물론 컨테이너를 씀으로 인해 발생하는 새로운 문제를 얻겠지만, 개인적으로 장점이 더 크다고 생각한다. ↩︎