kube-apiserver 를 컨트롤 플레인 외부에 설치해서 사용하기

Kube-apiserver 를 컨트롤플레인 외부에 설치해서 사용할 수 도 있다는 내용을 openai 의 기술블로그를 통해 영감을 얻고 직접 테스트해본 글입니다. Dedicated Node(전용 노드) 에서 kube-apiserver 를 사용하는 상황에서 유용하게 쓰일 방법입니다. 프로덕션에서 사용하기에는 적절하지 않은 테스트이지만 그럼에도 PoC (개념검증) 수준에서 실제 코드, 설정, 인증서를 수정해가면서 직접 테스트 가능하도록 작성한 글입니다.

kube-apiserver 를 컨트롤 플레인 외부에 설치해서 사용하기

Scaling Kubernetes to 7,500 nodes
We’ve scaled Kubernetes clusters to 7,500 nodes, producing a scalable infrastructure for large models like GPT-3, CLIP, and DALL·E, but also for rapid small-scale iterative research such as Scaling Laws for Neural Language Models.
💡
이 포스팅은 OpenAI 의 7,500 노드 규모의 클러스터를 운영하면서 겪은 기술적 이슈를 다룬 글을 통해 아이디어를 얻고 작성한 글이다.

Hits

개요

 

원래 kube-apiserver 는 보통 컨트롤 플레인 노드 안에서 static pod 형태로 운영된다.

static podkubernetes 가 관리하진 않지만(생성/추가/삭제 등), kubernetes 에서 관측 가능한 파드이고, 이것은 /etc/kubernetes/manifests 에 정의된 yaml 들을 파드로 kubelet 이 바라보고 생성하는 컨테이너들이다.

엄밀히 말하자면 쿠버네티스의 자원은 아니지만, 쿠버네티스가 써야 할 컴포넌트들을 주로 static pod 로 생성해서 사용한다. 대표적인 예가 kube-apiserver , kube-scheduler , kube-controller-manager 등이다. 만약 kubeadm 으로 별도의 설정 없이 클러스터를 프로비저닝하면 etcd 또한 static pod 로 프로비저닝 하게 된다.

Dedicated Node

dedicated node 란 보통 특정 서비스/애플리케이션/워크로드의 성능 극대화를 위해, 단일 서비스/애플리케이션/워크로드만을 하나의 노드(머신)에서 운영하는 케이스를 의미한다. 보통 I/O 작업이 많거나, 아니면 다른 워크로드에 의해서 영향을 받으면 안되는, 혹은 크리티컬한 워크로드를 이런형태로 사용 가능하고 그 대표적인 예가 etcd 이다. etcd 의 각종 사용사례를 보면 궁극적으로 성능 최대화, 클러스터의 안정성을 위해서 별도의 머신에서 etcd 만을 두고 운영하는 형태를 권장한다. 특히 클러스터 내 노드의 개수가 많은경우가 그렇다.

openai 의 경우는 kube-apiserver 또한 dedicated node 에서 static pod 가 아닌 별도의 systemd service 로 사용하는것으로 추측된다. 그러한 사용사례가 크게 문제가 될 것이 없는것이

첫째, kube-apiserverstateless 하다. 단지 뒷단의 백엔드에 etcd 가 있긴 하지만 kube-apiserver 자체가 상태를 들고있고, 상태에 의해서 문제가 발생하는 서비스가 아니다.

두번째, kube-apiserveretcd 와만 잘 연결되면 외부에서 kube-apiserver 에 request 를 했을 때, 그것을 처리하는데 문제가 없다.

 

위와 같은 특성으로 kube-apiserver 는 꼭 컨트롤플레인 노드에서 실행되어야 하는것이 아니다!

실제 테스트

 

여기서 kube-apiserversystemd 서비스와 같이 컨테이너가 아닌 네이티브한 프로세스 로 실행하는것으로 테스트해보았다. 이 테스트에서의 전제조건은 etcd 는 stacked 가 아닌 external 형태로 배포되어있을 때의 환경에서 테스트 하였다.  

환경

node1 : 컨트롤플레인, 192.168.201.12 / etcd 가 systemd 로 실행중

node2 : 컨트롤플레인, 192.168.201.13 / etcd 가 systemd 로 실행중

node3 : etcd 가 systemd 로 실행중

node4 : 아무것도 없는 노드 192.168.201.15

목표 구성

node1 에서 static pod 로 실행중인 kube-apiservernode4 에서 프로세스로 띄워보고, node4kube-apiserver 에 요청을 했을 때 잘 처리되는지 확인한다.  

컨트롤 플레인에서 kube-apiserver static pod 제거

우선 kubelet은 /etc/kubernetes/manifests 에 있는 yaml 파일을 static pod 로 띄우게 된다.

[root@node1 manifests]# pwd
/etc/kubernetes/manifests
[root@node1 manifests]# tree .
.
├── kube-apiserver.yaml
├── kube-controller-manager.yaml
└── kube-scheduler.yaml

위와 같이 총 3개의 파드가 static pod 로 띄워져있는데, static pod 를 제거하는 방법은, 단순하게 해당 디렉터리에서 yaml 을 제거하는것이다. 따라서 mv kube-apiserver.yaml ~/ 을 통해 파일을 삭제하진 않고 제거해보았다.  

이후 ps -ef | grep kube 를 통해 kube-apiserver 프로세스(컨테이너) 가 있는지 확인해본다.

kube-controller-manager 및 kube-scheduelr 만 존재하는 상태

이후, kubeadm 으로 생성되었던 apiserver 인증서에 node4ip 주소를 SAN (subject alternative name) 으로 넣어줘야 하기 때문에 /etc/kubernetes/kubeadm-config.yaml 파일을 수정한다.

192.168.201.15 를 certSANs 에 추가

이후에 /etc/kuberentes/pki , /etc/kubernetes/ssl (이건 kubespray 썼을 경우 인증서 경로) 내의 apiserver.crt , apiserver.key 를 삭제한다.  

이후

$ kubeadm init phase certs apiserver --config=/etc/kubernetes/kubeadm-config.yaml 을 통해 apiserver 인증서를 새로 생성한다.

apiserver.crt , apiserver.key 인증서를 node4 머신에 옮겨주면 되는데, 테스트 차원에서 sa.pub / sa.key 같은 인증서들도 필요하기때문에 통째로 node4 머신에 옮겨준다.

$ scp /etc/kubernetes/ssl/* hayden@192.168.201.15:/home/hayden

이후 etcd 인증서 또한 옮겨주어야 한다. kubespray 를 사용해 etcd 를 프로비저닝 한 케이스에서 etcd 인증서는 /etc/ssl/etcd/ssl 경로에 들어가있다. 여기서 원칙적으로는 node4 가 사용할 별도 인증서를 넘겨주는게 맞겠지만, 테스트 차원에서 /etc/ssl/etcd/ssl 경로에 있는 ca.pem , node-node2.pem , node-node2-key.pem 인증서를 node4 머신에 옮겨준다.

$ scp /etc/ssl/etcd/ssl/{ca.pem,node-node2.pem,node-node2-key.pem} hayden@192.168.201.15:/home/hayden

이후 node4 머신에서 kube-apiserver 바이너리를 다운로드 받는다.

Download Kubernetes 해당 링크에서 찾을 수 있다.

$ wget https://dl.k8s.io/v1.29.1/bin/linux/arm64/kube-apiserver

(테스트중인 환경의 아키텍처에 맞게 주의해서 다운로드)

이후 node4 머신에서 kube-apiserver 를 인자와 함께 아래와 같이 실행한다. (각자 상황에 따라 ip 주소 등이 다를 수 있음 주의)

$ ./kube-apiserver --advertise-address=192.168.201.15 --allow-privileged=true \
  --anonymous-auth=True --apiserver-count=2 --authorization-mode=Node,RBAC --bind-address=0.0.0.0 \
  --client-ca-file=./ca.crt --default-not-ready-toleration-seconds=300 --default-unreachable-toleration-seconds=300 \
  --enable-admission-plugins=NodeRestriction --enable-aggregator-routing=False --enable-bootstrap-token-auth=true \
  --endpoint-reconciler-type=lease --etcd-cafile=./ca.pem --etcd-certfile=./node-node2.pem \
  --etcd-compaction-interval=5m0s --etcd-keyfile=./node-node2-key.pem \
  --etcd-servers=https://192.168.201.12:2379,https://192.168.201.13:2379,https://192.168.201.14:2379 \
  --kubelet-client-certificate=./apiserver-kubelet-client.crt --kubelet-client-key=./apiserver-kubelet-client.key \
  --kubelet-preferred-address-types=InternalDNS,InternalIP,Hostname,ExternalDNS,ExternalIP --profiling=False \
  --proxy-client-cert-file=./front-proxy-client.crt --proxy-client-key-file=./front-proxy-client.key --secure-port=6443 \
  --requestheader-username-headers=X-Remote-User --requestheader-group-headers=X-Remote-Group \
  --requestheader-extra-headers-prefix=X-Remote-Extra- --service-account-issuer=https://kubernetes.default.svc.cluster.local \
  --service-account-key-file=./sa.pub --service-account-lookup=True --service-account-signing-key-file=./sa.key \
  --storage-backend=etcd3 --tls-cert-file=./apiserver.crt --tls-private-key-file=./apiserver.key

kube-apiserver 는 정말 많은 인자/옵션들이 있는데 이것을 직접 알아내서 확인한것이 아니라, 아까 node1 에서 본 kube-apiserver.yml static pod 명세에서 찾아서 넣으면 된다. 여기서 수정해야 할 값들은

--client-ca-file , --etcd-certifle 과 같은 인증서 옵션들이다. (나중에 더 자세히 작성)

이렇게하고 node4 에서 kube-apsierver 를 실행하면 kube-apiserver 가 동작하기 시작한다.

이것을 테스트하기 위해 다시 node1 으로 넘어가서 kubectl 이 사용할 config 파일을 수정해본다.

$ vim /root/.kube/config

원래는 node1 이 자기자신 static pod 가 당연하게도 localhost로 뜨기때문에, localhost:6443 으로 호출하도록 되어있었다. 이것을 node4 주소로 변경

이렇게 하게 되면 앞으로 node1 머신에서의 kubectl 호출은 node4 (192.168.201.15) 에 있는 kube-apiserver 에 호출을 하게 된다.  

$ curl -k "https://192.168.201.15:6443/readyz?verbose"

위와 같이 node4 에 떠있는 kube-apiserver 의 상태를 조회해볼 수 있고.

curl 로 kube-apiserver 호출해서 상태 확인

$ kubectl get --raw='/readyz?verbose' 을 통해 kubectl 로 동일한 결과를 확인 할 수 있다.

kubectl 로 kube-apiserver 상태 확인

$ kubectl get po -A 와 같은 일반적인 요청도 잘 처리 된다.

kubectl 테스트 결과

0:00
/0:36

위는 node1 , 아래는 node4 이다. 이렇게 컨트롤 플레인 외부에 kube-apiserver 를 둘 수 있다 ! (node4 는 쿠버네티스와 전혀 관계 없는 머신이다)

Wrapping up

이렇게 K8s 의 Control Plane 외부에서 kube-apiserver 를 실행하고 정상 동작하는것을 확인해봤는데 당연히 프로덕션에서 이러한 구조를 사용하기 위해서는 kube-apiserversystemd 서비스로 래핑해서 사용하거나 혹은 kubelet + static pods 조합으로 사용해야 할 것이다.

만약 kube-apiserver 를 이러한 dedicated node 에서 systemd 서비스등으로 설치해서 사용하는 경우 쿠버네티스 클러스터의 자체적인 로깅/모니터링/관리는 포기하게 되므로 트레이드 오프를 잘 판단해서 사용해야 한다.

OpenAI의 경우 거대한 규모의 K8s 클러스터를 운영하고있고, 그에 따른 kube-apiserver 의 부하도 엄청난 상황에서 kube-apiserver를 위한 전용(dedicated)노드에서 실행하여 조금 더 안정적으로 쿠버네티스 클러스터를 운영하려고 했던 케이스로 보인다.

일반적인 유즈케이스에서는 이렇게 사용할 필요는 없겠지만 K8s Control Plane 의 일부 컴포넌트를 이런식으로 인증서 설정만 잘 하면 클러스터 외부에 설치해서 사용 할 수 있다는 아이디어를 얻을 수 있는 계기가 되었다.