쿠버네티스 오케스트레이션 핵심 구조
출처: https://www.youtube.com/watch?v=PziYflu8cB8 · Fireship · 2:06 · 2020-09-24
요지
- 쿠버네티스는 컨테이너화된 애플리케이션의 배포, 확장, 복구를 자동화하는 오케스트레이션 시스템이다.
- 쿠버네티스 클러스터는 전체 상태를 관리하는 컨트롤 플레인과 실제 워크로드를 실행하는 노드로 구성된다.
- 파드는 쿠버네티스가 배포하고 관리하는 가장 작은 단위이며, 하나 이상의 밀접하게 연관된 컨테이너를 담는다.
- 개발자는 YAML 객체로 원하는 상태를 선언하고, 쿠버네티스는 실제 상태가 그 상태와 일치하도록 지속적으로 조정한다.
- 레플리카와 수평 확장을 이용하면 부하 변화나 장애에도 애플리케이션의 가용성을 유지할 수 있다.
개요
컨테이너 하나를 직접 실행하는 일은 어렵지 않다. 그러나 여러 서버에서 수십 또는 수백 개의 컨테이너를 운영하려면 배치, 네트워킹, 확장, 장애 복구, 비밀 정보와 영구 저장소 관리까지 해결해야 한다. 쿠버네티스는 이런 운영 작업을 자동화한다.
영상은 오케스트라의 연주자를 컨테이너에, 지휘자를 쿠버네티스에 비유한다. 트래픽이 급증하면 컨테이너를 늘리고, 서버나 컨테이너가 실패하면 대체 인스턴스를 준비하여 애플리케이션이 계속 동작하도록 조율하는 것이 쿠버네티스의 역할이다.
배경 / 사전 지식
컨테이너는 애플리케이션과 실행에 필요한 의존성을 격리된 단위로 묶는다. Docker는 컨테이너 이미지를 만들고 컨테이너를 실행하는 대표적인 도구다. 이미지는 실행 환경을 담은 불변 템플릿이고, 컨테이너는 그 이미지를 실행한 인스턴스다.
컨테이너 수가 늘어나면 어느 서버에 무엇을 실행할지, 실패한 컨테이너를 어떻게 다시 띄울지, 트래픽을 어느 인스턴스로 보낼지 결정해야 한다. 이러한 다수의 컨테이너를 여러 머신에 걸쳐 배치하고 관리하는 일을 컨테이너 오케스트레이션이라고 한다.
쿠버네티스에서 선언적 구성도 중요하다. 사용자가 실행 절차를 일일이 명령하는 대신 “nginx 파드가 3개 존재해야 한다”와 같은 목표 상태를 정의하면, 시스템이 현재 상태와 목표 상태의 차이를 찾아 스스로 조정한다.
핵심 개념
클러스터와 컨트롤 플레인
클러스터는 쿠버네티스가 관리하는 전체 시스템이다. 컨트롤 플레인은 클러스터의 두뇌로서 사용자의 요청을 받고, 워크로드를 어디에 배치할지 결정하며, 실제 상태가 선언된 상태와 일치하는지 감시한다.
API 서버는 kubectl 같은 외부 도구와 내부 구성 요소가 클러스터를 조작하는 관문이다. 클러스터의 구성과 상태 정보는 분산 키-값 저장소인 etcd에 보관된다.
노드와 kubelet
노드는 실제 컨테이너 워크로드가 실행되는 물리 머신 또는 가상 머신이다. 각 노드의 kubelet은 컨트롤 플레인과 통신하면서 자신에게 할당된 파드가 올바르게 실행되고 있는지 확인한다.
파드
파드는 쿠버네티스의 최소 배포 단위다. 일반적으로 파드 하나에 주 애플리케이션 컨테이너 하나를 두지만, 네트워크와 저장 공간을 긴밀히 공유해야 하는 보조 컨테이너를 함께 둘 수도 있다. 같은 파드의 컨테이너들은 동일한 네트워크 네임스페이스를 공유하므로 localhost로 서로 통신할 수 있다.
디플로이먼트와 레플리카셋
디플로이먼트는 애플리케이션의 배포 상태와 업데이트 방식을 선언하는 상위 객체다. 디플로이먼트가 관리하는 레플리카셋은 지정된 수의 동일한 파드가 계속 실행되도록 보장한다. 파드 하나가 실패하면 레플리카셋이 새 파드를 생성해 부족한 수를 채운다.
원하는 상태
YAML의 spec에는 컨테이너 이미지, 복제본 수, 포트, 볼륨 등 원하는 상태를 기술한다. 쿠버네티스의 컨트롤러는 실제 상태를 관찰하고 원하는 상태와 비교하는 조정 루프를 반복한다.
작동 원리
- 개발자가 디플로이먼트 같은 쿠버네티스 객체를 YAML로 정의한다.
kubectl apply가 YAML을 API 서버에 전달한다.- API 서버는 요청을 검증하고 클러스터의 원하는 상태를 저장한다.
- 스케줄러가 자원 여유와 제약 조건을 고려해 새 파드를 실행할 노드를 선택한다.
- 선택된 노드의 kubelet이 컨테이너 런타임을 통해 이미지를 내려받고 컨테이너를 실행한다.
- 컨트롤러는 파드 수와 상태를 계속 관찰한다. 파드가 사라지거나 장애가 발생하면 새로운 파드를 만들어 선언된 복제본 수를 복구한다.
- 부하가 증가하면 복제본이나 노드를 수평으로 늘릴 수 있다. 부하가 감소하면 불필요한 인스턴스를 줄여 자원 사용을 조절한다.
이 과정에서 서비스 디스커버리와 네트워킹, 시크릿 전달, 영구 볼륨 연결 같은 운영 기능도 쿠버네티스 객체를 통해 구성할 수 있다.
코드 예시
다음 예시는 nginx 파드 세 개를 유지하는 디플로이먼트다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
위 내용을 nginx-deployment.yaml로 저장한 뒤 접근 가능한 쿠버네티스 클러스터에서 실행한다.
kubectl apply -f nginx-deployment.yaml
kubectl get deployments
kubectl get pods -l app=nginx
replicas: 3은 동일한 역할의 파드 세 개를 유지하겠다는 선언이다. 파드 하나를 삭제하더라도 컨트롤러가 차이를 감지해 새 파드를 생성한다. resources는 스케줄링에 필요한 최소 자원과 컨테이너가 사용할 수 있는 상한을 정의한다.
함정·실수
- 파드를 서버처럼 다루고 내부 데이터를 영구 보존하면 안 된다. 파드는 언제든 교체될 수 있으므로 지속되어야 하는 데이터는 PersistentVolume 같은 별도 저장소에 둔다.
latest태그에 의존하면 배포된 이미지 버전을 재현하기 어렵다. 명시적인 버전이나 이미지 다이제스트를 사용한다.- 리소스 요청과 제한을 생략하면 스케줄러가 적절한 노드를 선택하기 어렵고, 한 워크로드가 노드 자원을 독점할 수 있다.
- 복제본 수만 늘린다고 외부 트래픽이 자동으로 분산되는 것은 아니다. 안정적인 접근점과 로드 밸런싱을 위해 Service 또는 Ingress/Gateway 구성이 필요하다.
- 파드가 실행 중이라는 사실만으로 애플리케이션이 정상이라고 판단하면 안 된다. 시작, 준비, 생존 상태를 검사하는 프로브를 목적에 맞게 설정한다.
- 시크릿을 일반 YAML이나 Git 저장소에 평문으로 커밋하지 않는다. 접근 제어와 암호화, 외부 시크릿 관리 도구를 함께 고려한다.
베스트 프랙티스
- 디플로이먼트, 서비스, 설정을 선언형 YAML로 관리하고 Git에서 변경 이력을 추적한다.
- 컨테이너 이미지 버전을 고정하고, 롤링 업데이트와 롤백 절차를 검증한다.
- CPU와 메모리의 요청량 및 제한량을 실제 측정값에 근거해 설정한다.
- readiness probe로 트래픽을 받을 준비가 되었는지 확인하고, liveness probe는 복구가 필요한 교착 상태를 판별하는 데 제한적으로 사용한다.
- 애플리케이션 복제본 확장과 클러스터 노드 확장을 구분한다. 전자는 파드 수를, 후자는 실행 가능한 머신의 용량을 늘린다.
- 고가용성이 필요하면 파드를 여러 노드와 장애 영역에 분산하고, PodDisruptionBudget 및 적절한 배치 규칙을 사용한다.
- 로그, 메트릭, 이벤트를 중앙에서 관찰할 수 있도록 구성하여 자동 복구가 반복되는 근본 원인까지 추적한다.
참고
(영상 내 명시 없음)