컨트롤 플레인 컴포넌트
- 3.1 etcd 기초
- 3.2 etcd in Kubernetes
- 3.3 kube-apiserver
- 3.4 kube-controller-manager
- 3.5 kube-scheduler
- 3.6 kubelet
- 3.7 kube-proxy
앞 챕터에서 클러스터의 큰 그림을 잡았으니, 이제 각 컴포넌트를 하나씩 뜯어보자. etcd부터 시작해서 apiserver, controller manager, scheduler, kubelet, kube-proxy까지. 각각이 뭘 하고, 어떻게 설치하고, 어디서 설정을 확인하는지를 다뤄.
etcd는 간단하고 안전하며 빠른 분산형 키-값 저장소야. Kubernetes에서 클러스터의 모든 상태 정보를 저장하는 핵심 컴포넌트인데, 먼저 키-값 저장소가 뭔지부터 알아보자.
전통적인 관계형 데이터베이스(SQL)는 행과 열로 데이터를 저장해. 테이블에 사람 정보를 넣는다고 하면, 새로운 종류의 정보(급여, 성적 등)를 추가할 때마다 열을 추가해야 하는데, 그러면 테이블 전체에 영향을 미치고 해당 안 되는 행에는 빈 셀이 잔뜩 생겨. 스키마가 엄격해서 SQL로 복잡한 쿼리를 잘 할 수 있지만, 유연성이 떨어지고 정형 데이터에 가장 적합해.
문서 저장소(Document Store)는 정보를 문서(보통 JSON) 형태로 저장해. 각 개인이 하나의 문서를 갖고, 한 문서를 변경해도 다른 문서에 영향 없어. 직장인은 급여 항목이 있는 파일을, 학생은 성적만 있는 파일을 가질 수 있지. 스키마 정의가 필요 없고 유연하지만, 여러 테이블을 조인하는 복잡한 쿼리에는 제한이 있어. 반정형 데이터에 적합해.
키-값 저장소는 가장 단순해. 키에 값을 저장하는 거야. name: John, age: 45, salary: 5000 이런 식이지. 값으로 프로퍼티 집합이나 JSON 문서 같은 복잡한 데이터도 넣을 수 있어. 키도 user:unknown 같은 형태로 쓸 수 있고. 스키마가 필요 없고 복잡한 쿼리는 지원 안 하지만, 성능이 매우 빠르고 유연해서 간단한 빠른 조회에 최적이야. etcd가 바로 이런 키-값 저장소 중 하나인데, 분산되어 있고 신뢰할 수 있다는 특징이 있어. 분산과 신뢰성이 뭘 의미하는지는 이 과정의 뒷부분(고가용성 섹션)에서 자세히 다뤄.
설치는 간단해. GitHub 릴리스 페이지에서 운영체제에 맞는 바이너리를 다운로드하고, 압축 풀고, etcd 실행 파일을 실행하면 돼. etcd를 실행하면 기본적으로 포트 2379에서 수신 대기하는 서비스가 시작돼. 이게 가장 단순한 형태고, 실제로는 시스템 서비스나 Kubernetes 클러스터의 파드로 실행하는 게 이상적이야.
etcd와 함께 제공되는 기본 CLI 클라이언트가 etcdctl이야. 이걸로 키-값 쌍을 저장하고 조회할 수 있어.
etcdctl put key1 value1
etcdctl get key1
etcd 버전에 따라 명령이 달라지니까 주의해야 해. 버전 역사를 보면, 첫 버전(0.1)이 2013년 8월에 나왔고, 공식 안정 버전인 2.0이 2015년 2월에 출시됐어. 이때 Raft 합의 알고리즘이 재설계되어 초당 1000개 이상의 쓰기를 지원하게 됐지. 2017년 1월에 버전 3.0이 나오면서 API가 크게 바뀌었어.
버전 2에서는 etcdctl set, etcdctl get, etcdctl rm 명령을 썼는데, 버전 3부터는 etcdctl put, etcdctl get, etcdctl del로 바뀌었어. 트랜잭션도 v3에서 새로 지원됐고. 온라인에서 이전 API 기반 문서를 볼 수 있으니까 헷갈리지 마.
etcdctl version
이 명령으로 etcd 버전과 API 버전(2 또는 3)을 확인할 수 있어. 2018년 11월에 etcd가 CNCF 인큐베이터 프로젝트가 됐고, 2020년 11월에 CNCF 졸업 프로젝트가 됐어. 2021년 6월에는 버전 3.5가 출시됐지.
그럼 이 etcd가 Kubernetes에서는 구체적으로 어떻게 쓰이냐면, 노드, 파드, 컨피그, 시크릿, 계정, 역할, 역할 바인딩 등등 클러스터에 관한 모든 정보가 여기 들어가. kubectl get 명령을 실행할 때 보이는 모든 정보가 etcd 서버에서 나오는 거야. 노드 추가, 파드 배포, 레플리카 세트 변경 같은 클러스터에 대한 모든 변경 사항은 etcd 서버에 업데이트된 후에야 "완료"로 간주돼.
클러스터를 어떻게 설정하느냐에 따라 etcd 배포 방식이 달라져. 크게 두 가지가 있어.
처음부터(scratch) 설정하는 경우, etcd 바이너리를 직접 다운로드해서 설치하고, 마스터 노드에서 etcd를 서비스로 구성해야 해. 서비스에 전달되는 옵션이 많은데, 대부분은 인증서 관련이야(이 과정 뒷부분의 TLS 인증서 섹션에서 자세히 다룸). 나머지는 etcd를 클러스터로 구성하는 방법에 관한 거고(고가용성 섹션에서 다룸). 지금 주목할 건 advertised-client-urls 옵션이야. etcd가 수신하는 주소인데, 서버 IP와 기본 포트 2379로 구성돼. kube-apiserver가 etcd에 접속할 때 이 URL을 사용해.
kubeadm으로 설정하는 경우, kubeadm이 etcd 서버를 kube-system 네임스페이스에 파드로 배포해. 이 파드 안에서 etcdctl을 사용해서 etcd 데이터베이스를 탐색할 수 있어.
Kubernetes가 저장한 모든 키를 나열하려면 이렇게 해:
etcdctl get / --prefix --keys-only
Kubernetes는 특정 디렉터리 구조에 데이터를 저장하는데, 루트 디렉터리가 /registry이고, 그 아래에 minions(nodes), pods, replicasets, deployments 같은 다양한 쿠버네티스 구성이 있어.
고가용성(HA) 환경에서는 여러 마스터 노드에 etcd 인스턴스가 분산돼. 이 경우 etcd 서비스 구성에서 initial-cluster 옵션을 설정해서 etcd 인스턴스들이 서로를 알 수 있게 해야 해. 이 옵션에 etcd 서비스의 여러 인스턴스를 지정하는 거야. 고가용성에 대해서는 이 과정의 뒷부분에서 훨씬 더 자세히 다뤄.
etcd가 데이터 저장소라면, 그 데이터에 접근하는 관문은 kube-apiserver야. Kubernetes의 기본 관리 컴포넌트이고, 클러스터에서 일어나는 모든 변경의 중심에 있어. etcd 데이터 저장소와 직접 통신하는 유일한 컴포넌트이기도 해. 스케줄러, 컨트롤러 매니저, kubelet 같은 다른 컴포넌트는 전부 API 서버를 통해서 클러스터를 업데이트해.
kubectl 명령을 실행하면 실제로 kube-apiserver에 요청이 가는 거야. API 서버는 먼저 요청을 인증하고 유효성을 검사한 다음, etcd에서 데이터를 가져와서 응답해. kubectl 대신 POST 요청을 보내서 API를 직접 호출할 수도 있어.
파드 생성을 예로 들어볼게. 파드 생성 요청이 들어오면 이런 흐름이야:
- API 서버가 요청을 인증하고 유효성을 검사해
- 파드 오브젝트를 생성하되, 아직 노드에 할당하지 않아
- etcd 서버의 정보를 업데이트해
- 사용자에게 파드가 생성되었다고 알려줘
- 스케줄러가 API 서버를 지속적으로 모니터링하다가, 노드가 할당되지 않은 새 파드를 발견해
- 스케줄러가 적합한 노드를 식별해서 API 서버에 알려줘
- API 서버가 etcd를 업데이트해
- API 서버가 해당 워커 노드의 kubelet에 정보를 전달해
- kubelet이 노드에 파드를 생성하고 컨테이너 런타임 엔진에 이미지 배포를 지시해
- 완료되면 kubelet이 상태를 API 서버에 보고하고, API 서버가 etcd를 업데이트해
변경이 요청될 때마다 이런 비슷한 패턴이 반복돼. 요약하면, kube-apiserver는 요청을 인증/검증하고, etcd에서 데이터를 읽고 쓰는 역할을 해.
kubeadm으로 클러스터를 부트스트랩하면 이런 걸 알 필요 없지만, 직접 설정하는 경우에는 kube-apiserver 바이너리를 Kubernetes 릴리스 페이지에서 다운로드해서 마스터 노드에서 서비스로 실행해야 해. 매개변수가 정말 많아. 다양한 컴포넌트 간 통신을 위한 인증서 설정, 다른 컴포넌트의 위치 정보, 인증/권한 부여/암호화 관련 옵션 등이 있어. 인증서에 대해서는 이 과정 뒷부분의 TLS 인증서 섹션에서 자세히 다뤄. etcd 서버 옵션은 etcd 서버의 위치를 지정하는 건데, 이게 kube-apiserver가 etcd에 연결하는 방식이야.
기존 클러스터에서 kube-apiserver 옵션을 보려면, kubeadm으로 설정한 경우 kube-system 네임스페이스에 파드로 배포되어 있으니까 /etc/kubernetes/manifests 폴더의 파드 정의 파일에서 확인할 수 있어. kubeadm이 아닌 경우에는 /etc/systemd/system/kube-apiserver.service에서 서비스 설정을 볼 수 있고. 마스터 노드에서 프로세스를 나열하고 kube-apiserver를 검색해서 실행 중인 프로세스와 옵션을 확인할 수도 있어.
다음은 컨트롤러야. 컨트롤러는 시스템 내 다양한 구성 요소의 상태를 지속적으로 모니터링하고, 전체 시스템을 원하는 상태(desired state)로 만들기 위해 동작하는 프로세스야. 이 모든 컨트롤러가 Kube Controller Manager라는 하나의 프로세스에 패키징되어 있어. Kube Controller Manager를 설치하면 다른 컨트롤러도 함께 설치되는 거지.
노드 컨트롤러를 예로 들어볼게. 노드 컨트롤러는 kube-apiserver를 통해 5초마다 노드 상태를 확인해. 노드로부터 하트비트 수신을 중단하면 40초간 기다린 후 해당 노드를 "연결 불가(unreachable)"로 표시해. 그 후 5분간 다시 연결할 수 있는 시간을 줘. 5분이 지나도 복구가 안 되면, 해당 노드에 할당된 파드를 제거하고 정상 노드에 프로비저닝해(파드가 레플리카 세트의 일부인 경우).
레플리케이션 컨트롤러는 레플리카 세트의 상태를 모니터링하고, 세트 내에서 원하는 수의 파드가 항상 사용 가능한지 확인하는 역할이야. 파드가 죽으면 다른 파드를 만들어.
이 두 가지는 예시일 뿐이고, Kubernetes에는 훨씬 더 많은 컨트롤러가 있어. 배포, 서비스, 네임스페이스, 퍼시스턴트 볼륨 등 지금까지 본 Kubernetes 개념들에 내장된 인텔리전스가 바로 이 다양한 컨트롤러를 통해 구현되는 거야. Kubernetes의 많은 것들을 뒷받침하는 두뇌 같은 역할이지.
Kube Controller Manager를 설치하려면 Kubernetes 릴리스 페이지에서 다운로드해서 서비스로 실행하면 돼. 실행할 때 여러 옵션이 있는데, 노드 컨트롤러의 모니터 기간(node-monitor-period), 유예 기간(node-monitor-grace-period), 퇴거 타임아웃(pod-eviction-timeout) 같은 기본 설정을 커스터마이즈할 수 있어. controllers 옵션으로 활성화할 컨트롤러를 지정할 수도 있어. 기본적으로 모든 컨트롤러가 활성화되어 있지만, 일부만 켜도록 선택할 수 있지. 컨트롤러가 작동하지 않거나 존재하지 않는 것 같다면 이 옵션을 확인해봐.
기존 클러스터에서 Kube Controller Manager 옵션을 보려면, kubeadm으로 설정한 경우 kube-system 네임스페이스에 파드로 배포되어 있으니까 /etc/kubernetes/manifests 폴더의 파드 정의 파일에서 확인할 수 있어. kubeadm이 아닌 설정에서는 서비스 디렉터리에 있는 kube-controller-manager 서비스를 확인하면 되고. 마스터 노드에서 프로세스를 나열하고 kube-controller-manager를 검색해서 실행 중인 프로세스와 옵션을 확인할 수도 있어.
이제 스케줄러 차례야. kube-scheduler는 파드를 어느 노드에 배치할지 결정만 하는 역할이야. 실제로 노드에 파드를 배치하는 건 kubelet이 하는 일이거든. 이 차이를 꼭 기억해.
그럼 스케줄러가 왜 필요하냐면, 파드마다 리소스 요구 사항이 다를 수 있고, 노드마다 용량이 다르잖아. 올바른 파드가 올바른 노드에 배치되도록 보장해야 하니까. 특정 애플리케이션 전용 노드를 만들 수도 있고.
스케줄러는 두 단계를 거쳐 파드에 가장 적합한 노드를 찾아. 첫 번째 단계는 필터링이야. 파드의 프로파일에 맞지 않는 노드를 걸러내. 예를 들어 파드가 요청한 CPU와 메모리 리소스가 충분하지 않은 노드는 제외되지. 두 번째 단계는 순위 매기기야. 남은 노드들을 우선순위 함수로 0~10점 척도로 점수를 매겨. 예를 들어 파드를 배치한 후 남는 리소스가 더 많은 노드에 더 높은 점수를 줘. 그래서 점수가 높은 노드가 선택되는 거야.
이건 높은 수준의 개요이고, 실제로는 훨씬 더 다양한 기준이 있어. 리소스 요구 사항과 제한, 테인트(taint)와 톨러레이션(toleration), 노드 셀렉터, 어피니티(affinity) 규칙 같은 주제가 있는데, 이 강좌에서 스케줄링 전용 섹션을 마련해서 각각을 자세히 다뤄. 게다가 이런 기능들은 커스터마이즈할 수 있고, 직접 스케줄러를 작성할 수도 있어.
스케줄러를 설치하려면 Kubernetes 릴리스 페이지에서 kube-scheduler 바이너리를 다운로드하고, 압축 풀고, 서비스로 실행하면 돼. 서비스로 실행할 때 스케줄러 구성 파일을 지정해.
기존 클러스터에서 kube-scheduler 옵션을 보려면, kubeadm으로 설정한 경우 kube-system 네임스페이스에 파드로 배포되어 있으니까 /etc/kubernetes/manifests 폴더의 파드 정의 파일에서 확인할 수 있어. 마스터 노드에서 프로세스를 나열하고 kube-scheduler를 검색해서 실행 중인 프로세스와 옵션을 확인할 수도 있어.
마스터 쪽 컴포넌트는 다 봤고, 이제 워커 노드 쪽이야. kubelet은 워커 노드의 선장 같은 존재야. 노드에서 일어나는 모든 활동을 주도하거든.
구체적으로 kubelet이 하는 일은 이래. 먼저 노드를 Kubernetes 클러스터에 등록해. 그 다음 노드에서 컨테이너나 파드를 로드하라는 지시를 받으면, 컨테이너 런타임 엔진(Docker 등)에 필요한 이미지를 가져와 인스턴스를 실행하도록 요청해. 그리고 파드와 그 안의 컨테이너 상태를 계속 모니터링하면서 정기적으로 kube-apiserver에 보고해. 마스터 노드와의 유일한 연락 창구이기도 하고, 스케줄러의 지시에 따라 컨테이너를 적재하거나 하역하는 것도 kubelet이야.
다른 컴포넌트와의 중요한 차이점이 하나 있어. kubeadm을 사용해서 클러스터를 배포하더라도 kubelet은 자동으로 배포되지 않아. 항상 워커 노드에 kubelet을 수동으로 설치해야 해. 설치 프로그램을 다운로드하고, 압축 풀고, 서비스로 실행하면 돼.
워커 노드에서 프로세스를 나열하고 kubelet을 검색하면 실행 중인 kubelet 프로세스와 옵션을 확인할 수 있어. 이 과정의 뒷부분에서 kubelet을 구성하는 방법, 인증서를 생성하는 방법, TLS 부트스트랩하는 방법에 대해 자세히 다뤄.
마지막으로 kube-proxy. Kubernetes 클러스터에서 모든 파드는 다른 모든 파드에 도달할 수 있어. 파드 네트워킹 솔루션을 배포해서 클러스터의 모든 노드에 걸친 내부 가상 네트워크(파드 네트워크)를 만들기 때문이야.
근데 파드 IP는 항상 동일하게 유지된다는 보장이 없어. 그래서 서비스를 만들어서 안정적인 접근점을 제공하는 거야. 예를 들어 웹 앱 파드가 데이터베이스 파드에 접근해야 한다면, 데이터베이스 파드를 직접 IP로 접근하는 대신 서비스를 만들어서 서비스 이름(예: db-service)으로 접근하는 게 좋아. 서비스는 자체 IP를 갖고, 트래픽을 백엔드 파드로 전달해줘.
그런데 서비스는 실제로 존재하는 게 아니야. 파드처럼 컨테이너가 아니고, 인터페이스나 수신 프로세스도 없어. Kubernetes 메모리에만 존재하는 가상 구성 요소야. 그런데도 클러스터 전체의 모든 노드에서 접근할 수 있어야 하잖아. 이걸 가능하게 하는 게 바로 kube-proxy야.
kube-proxy는 Kubernetes 클러스터의 각 노드에서 실행되는 프로세스야. 새로운 서비스가 생성되는 걸 감시하다가, 서비스가 만들어지면 각 노드에 적절한 규칙을 생성해서 해당 서비스로 향하는 트래픽을 백엔드 파드로 전달해. 이걸 하는 방법 중 하나가 iptables 규칙을 사용하는 거야. 예를 들어 서비스의 IP(10.96.0.1)로 향하는 트래픽을 실제 파드의 IP(10.32.0.15)로 포워딩하는 iptables 규칙을 클러스터의 각 노드에 만들어.
kube-proxy를 설치하려면 Kubernetes 릴리스 페이지에서 바이너리를 다운로드해서 서비스로 실행하면 돼. kubeadm 도구를 사용하면 각 노드에 kube-proxy를 데몬셋(DaemonSet)으로 배포해. 데몬셋은 클러스터의 각 노드에 항상 단일 파드가 배포되도록 보장하는 거야. 데몬셋에 대해서는 이 강좌의 별도 강의에서 다뤄.
네트워킹, 서비스, kube-proxy, 파드 네트워킹에 대해서는 이 과정 뒷부분의 네트워킹 전용 섹션에서 훨씬 더 자세히 다뤄.
정리
3장 읽고 기억할 거 세 가지:
- etcd가 모든 걸 기억해. 클러스터의 모든 상태가 etcd에 저장되고, 변경은 etcd에 기록된 후에야 완료로 간주돼. apiserver만이 etcd와 직접 통신해
- 파드 생성의 흐름: apiserver → etcd → scheduler → kubelet. apiserver가 허브 역할을 하고, 각 컴포넌트는 자기 역할만 수행한 뒤 apiserver를 통해 결과를 전달해
- kubelet만 수동 설치가 필요해. kubeadm이 대부분의 컴포넌트를 자동 배포하지만, kubelet은 항상 워커 노드에 직접 설치해야 해