RR DNS 를 통한 kube-apiserver 앞단의 LB 대체 테스트
이 글에서는 Round Robin DNS 를 통해 HA k8s 클러스터 앞단의 kube-apiserver를 위한 로드밸런서를 대체 할 수 있는지 테스트를 해보는 글입니다. 결론부터 이야기 하면 어느정도는 가능하다입니다. DNS 로는 Route53 을 사용하고, 상태검사등의 기능까지 사용해서 어느정도 구현이 가능합니다.

쿠버네티스 고가용성 토폴리지 (etcd)

위 문서는 쿠버네티스 공식 문서에서 고가용성이 확보된 쿠버네티스 클러스터를 구축하는 두가지 토폴로지에 대해서 서술하고 있습니다. 위 문서에서의 초점은 etcd
이며. etcd
를 컨트롤 플레인 노드(머신)과 중첩시켜서 구축할지, 아니면 외부 다른 노드(머신)에 구축할지에 대한 내용입니다.
이 문서는 etcd
를 초점으로 한 문서는 아니지만 간략하게 요약하자면.
Stacked etcd 토폴로지

Stacked
etcd 토폴로지의 경우 컨트롤 플레인 노드(머신)에 etcd 를 같이 설치하는 형태로.
kube-apiserver
와 etcd
간에 Network I/O
가 없다는 장점이 있지만. RAFT 알고리즘을 사용하는 etcd
의 특성, 그리고 그 etcd
를 컨트롤 플레인 노드에 같이 설치한다는 구성상의 특징으로 컨트롤 플레인 노드의 개수는 etcd
클러스터 멤버의 개수와 동일하며. 그에 따라서 고가용성 유지를 위한 Quorum
개수를 맞추려고 할 때 컨트롤 플레인 노드의 개수도 etcd
와 동일하게 맞춰줘야 한다는 단점이 있을 수 있습니다.
etcd
클러스터는 etcd
멤버를 3대 이상 유지했을 때 1개의 etcd
멤버가 다운되더라도 정상 동작이 가능하고, 5대로 구성하는경우 최대 2개의 멤버가 다운되더라도 정상 동작이 가능합니다.
참조 : https://etcd.io/docs/v3.5/faq/#what-is-failure-tolerance
따라서 고가용성을 유지하기 위해 etcd
를 5개를 설치하기로 선택 했을 때, 컨트롤 플레인의 개수 자체도 5개가 되어야 한다는것이 단점이 될 수 있습니다.
External etcd 토폴로지

External
etcd 토폴로지의 경우 컨트롤 플레인 노드와 별도로 etcd
를 설치해서 사용하는 형태로. kube-apiserver
와 etcd
가 물리적으로 다른 머신에 설치되어있는 경우 Network I/O
를 염두해서 사용해야 합니다. 다만 이 케이스에서는 컨트롤 플레인의 개수는 etcd
의 개수와 전혀 상관없이 구성이 가능하다는 점이 장점입니다.
컨트롤 플레인 노드는 2대, etcd
멤버는 5개로 설치하거나, 컨트롤 플레인은 3대 , etcd 멤버는 3개 등으로도 설치 가능합니다. 이렇게 컨트롤 플레인의 개수와 etcd
개수간의 디펜던시가 없는것이 장점이 될 수 있습니다. 다만 etcd
멤버 수 만큼 추가적인 노드(머신)이 필요 한 것이 단점이 될 수 있습니다.
kube-apiserver 앞단의 로드밸런서 (software LB)
앞서서 HA 쿠버네티스를 구성하는 두가지 토폴로지를 보면서 확인 가능했겠지만. 워커노드들이 kube-apiserver
에 접근 할 때 로드밸런서를 통해 접근하는것은 동일하다는 것을 볼 수 있었습니다.
kubeadm
을 통해 HA 쿠버네티스 클러스터를 구성하는 아래와 같은 문서에서도 kube-apsierver
용 로드밸런서를 생성하라는 이야기가 나와있습니다.
Creating Highly Available Clusters with kubeadm
또한 위 문서에서 소프트웨어를 통한 부하 분산에 대한 내용이 아래와 같이 링크 되어있는데요.
위 문서에서는 VIP(Virtual IP)를 통해 로드밸런싱을 제공하는 keepalived + haproxy 조합이 오래동안 사용되어왔고, 충분한 테스트를 거친 조합이라고 설명하고있습니다.

위 그림에서의 IP 주소는 이해를 돕기위해 임의로 설정한 주소입니다.
아래쪽의 각 컨트롤 플레인 노드는 각각 192.168.100.2
, 192.168.100.3
, 192.168.100.4
주소를 갖고 있다고 할 때. kube-apiserver
는 각 컨트롤 플레인 노드에서 6443(기본적으로 설정되는 포트이며 변경될 수 있음)
포트로 listen 하고 있는 상태일 것입니다.
이러한 kube-apiserver
들에 부하를 분산해서, 그리고 문제가 생겼을때 문제가 없는 서버로 트래픽을 틀어주기 위해서 HAProxy
를 통해 각 kube-apiserver
에 round-robin 형태로 트래픽을 틀어주도록 설정하고. 이 때 각 HAProxy
는 192.168.100.10
, 192.168.100.20
주소를 갖고 있다고 가정합니다.
이때 각 LB(HAProxy)
간의 HA 구성을 위해 Keepalived
를 두고. VIP
를 192.168.100.100
으로 두고. 설정을 통해 한쪽 LB를 마스터, 한쪽 LB 를 백업으로 두는 Active-Standby
조합으로 구성하면
클라이언트나 워커노드는 192.168.100.100:6443
혹은 도메인을 192.168.100.100
에 연결했다고 하고, 그 도메인이 foo.bar.com
이라고 하면 https://foo.bar.com:6443
으로 요청을 보낼 수 있습니다.
그렇게 요청을 보내면 Keepalived
를 통해 한쪽 LB
로 요청이 넘어가고. 해당 LB 는 round-robin 형태로 세 컨트롤 플레인 노드의 kube-apiserver
중 하나로 트래픽을 틀어주게 됩니다.
이것이 일반적인 Keepalived
+ HAProxy
조합의 HA 쿠버네티스 클러스터 구성입니다.
이렇게 하게 되면 kube-apiserver
, LB(HAProxy)
중 하나가 장애가 생기더라도 정상적인 kube-apiserver
, LB
로 트래픽을 틀어 줄 수 있기 때문에 고가용성이 확보되는 것입니다.
물론 뒷단의 Control Plane Node
를 Stacked etcd 토폴로지
로 했냐, External etcd 토폴로지
로 했냐에 따라서 고가용성 확보 여부가 달라질 수 있고. Keepalived
에 장애가 생겼냐에 따라서도 고가용성 확보 여부가 달라 질 수는 있습니다. 여기서는 단편적인 케이스에 대해서만 설명하였습니다.
위 구성에서 VIP(Virtual IP)
그리고 Keepalived
를 사용해서 LB(HAProxy)
에 대한 고가용성을 확보했기 때문에 두 LB(HAProxy)
중 하나는 유휴상태(Standby
) 상태로 남아있게 되는 것 단점으로 남습니다.
위 부분을 해결하기 위해서는 별도의 Keepalived
를 하나 더 추가하고. 2개의 VIP(Virtual IP)
를 구성하고. Round Robin DNS
, 그리고 Health Check
까지 겸해서 사용하거나, 두개의 LB
에 대해서 Round Robin DNS
그리고 Health Check
를 겸해서 사용하는 등의 추가적인 방법을 사용해야 할 것입니다.
Round-Robin DNS 로 해보자!
Round-robin DNS 는 DNS 요청에 대한 IP 주소들의 응답을 여러 주소를 반환하고 그와 더불어서 첫번째 레코드를 순회(round-robin)하면서 DNS 응답을 하는 방법입니다.
일반적인 상황에서 S/W , H/W LB 를 두지 않고 가장 간단하게 부하 분산을 할 수 있는 방법에 그 이점이 있습니다. 다만 고려해야할 아래와 같은 단점이 있습니다.
- DNS 쿼리 정보를 캐싱하는 주체가 다양하다. (OS, 애플리케이션, 기타 클라이언트 등등)
- DNS 쿼리 캐싱 문제로 인해 문제가 발생했을 때 바로 반영이 되지 않을 수 있다. 기존 쿼리해서 받은 IP 주소로 계속해서 애플리케이션/클라이언트 등이 호출을 하려 할 것이다.
- DNS 서버에 Health Check 기능이 존재하지 않으면 비정상적인 IP 주소를 계속 담아서 보내줄 수도 있다.
우선 1, 2번 문제를 제외하고 3번 부분을 처리하기 위해서 AWS Route53
의 다중 응답 기능 + 상태 검사 기능을 사용해서 테스트를 해보기로 하였습니다.
테스트 환경
테스트를 하고자 하는 환경 구성은 아래와 같습니다.

2개의 Control Plane Node (172.xx.xx.31
, 172.xx.xx.32
) 와 1개의 Worker Node (172.xx.xx.33
) 이 존재하고. 이 노드들은 모두 사설 대역대에 존재합니다. 이 사설망과 연결된 게이트웨이는 xx.xx.xx.xx
라는 공인 ip 주소를 갖고 있고 Route53
의 헬스체크 요청이 도달할 수 있도록 포트포워딩을 아래와 같이 하였습니다.
xx.xx.xx.xx:16443
→ 172.xx.xx.31:6443
(kube-apiserver)
xx.xx.xx.xx:26443
→ 172.xx.xx.32:6443
(kube-apiserver)
xx.xx.xx.xx
를 public_ip
라고 지칭하겠습니다.
Route53 설정

위와 같이 public_ip:16443
, public_ip:26443
에 대해서 상태검사를 생성합니다.

이후 A Record
에 위와 같이 다중값 응답 및 상태확인을 연결하여 생성합니다.
172.xx.xx.31:6443
, 172.xx.xx.32:6443
으로 떠있는 각 kube-apiserver
가 정상 상태라면 두 상태 검사도 정상이고, 이에 따라서 DNS 쿼리시 두 IP 주소를 번갈아가면서 응답해주게 됩니다.
DNS 테스트

kube-apiserver
는 static pod 로 띄워져있기때문에 ,한번 172.xx.xx.31
쪽에 있는 kube-apiserver
를 제거해보고 DNS
응답이 어떻게 나오는지 확인해봅니다.
$ mv /etc/kubernetes/manifests/kube-apiserver.yaml /root/kube-apiserver.yaml
위와 같이 kube-apiserver.yaml
을 잠시 /etc/kubernetes/manifests
경로에서 빼내줍니다.
cat /etc/kubernetes/kubelet-config.yaml | grep staticPodPath
staticPodPath: /etc/kubernetes/manifests

잠시 기다리면 kubelet
이 kube-apiserver.yaml
파일이 제거됨을 인지하고, kube-apsierver
static pod 를 제거합니다. 위 사진처럼 node1 (172.xx.xx.31)
에는 kube-apiserver
가 제거되었습니다.


이렇게 상태 검사 + Rotue53 다중 응답(Round Robin DNS)을 동작하도록 했다. 172.xx.xx.31
에서 잠깐 제거한 kube-apiserver.yaml
을 다시 아래 명령어를 통해 정상화 한다.
$ mv /root/kube-apiserver.yaml /etc/kubernetes/manifests/
kube-apiserver 인증서 재발급
이제 kube.xxx.com
과 같은 도메인으로(host header) kube-apiserver
를 요청하더라도 처리가 되도록 기존에 프로비저닝 된 kube-apiserver
인증서를 재발급 해줘야한다.
kubeadm
을 통해 수행합니다. 172.xx.xx.31
서버에서 아래와 같이 kube-apiserver
가 사용하는 기존 인증서를 확인해보자.
$ openssl x509 -text -noout -in /etc/kubernetes/ssl/apiserver.crt

kube-apsierver
에서 ssl termination 을 할 때 오류가 발생한다. kubeadm
을 통해 다시 인증서를 발급하기 위해서 우선 /etc/kubernetes/kubeadm-config.yaml
파일의 certSANs
항목에 도메인을 추가해야 한다.
$ vi /etc/kubernetes/kubeadm-config.yaml
위는 kubeadm-config.yaml
파일의 certSANS
항목의 일부이다. 해당 부분의 아무 라인에서 kube.xxx.com
과 같은 도메인을 추가해줘야 한다.
이후 기존 kube-apiserver
인증서, 키파일을 제거한다.
$ rm -rf /etc/kubernetes/ssl/{apiserver.crt,apiserver.key}
이후 kubeadm
을 통해 kube-apiserver
인증서를 업데이트된 kubeadm-config.yaml
파일 기반으로 재생성한다.
kubeadm
바이너리를 호출해서 실행하면 된다. kubespray
로 프로비저닝 한 경우 kubeadm
이 /usr/local/bin
에 설치되어있다.$ /usr/local/bin/kubeadm init phase certs apiserver --config=/etc/kubernetes/kubeadm-config.yaml
위 명령어를 수행하면 apiserver.crt
, apiserver.key
가 새롭게 /etc/kubernetes/ssl
디렉터리에 생성된다.
이후 제대로 생성이 되었는지 $ openssl x509 -text -noout -in /etc/kubernetes/ssl/apiserver.crt
명령어를 통해 확인한다.

이후 172.xx.xx.31
에서 생성된 인증서를 다른 모든 컨트롤플레인(현재 테스트 환경에서는 172.xx.xx.32
)에도 반영해준다. (기존 인증서 제거 및 172.xx.xx.31
에서 생성한 인증서 복사)
워커노드에서의 kubelet 설정
이제 172.xx.xx.33
(워커노드) 에서 본격적인 테스트를 진행해보자.
워커노드에서 $ vi /etc/kubernetes/kubelet.conf
을 통해 kubelet
이 사용할 kubeconfig
파일을 열어봅니다.
kubelet
, kube-scheduler
, kube-controller-manager
와 같은 쿠버네티스 컴포넌트들은 kubeconfig
형태의 파일을 통해 어떤 서버 주소로 접근하고, 어떤 인증서로 API 서버에 인증할지 등을 설정할 수 있습니다. 우리가 로컬 머신에서 사용하는 kubectl
의 경우도 동일한 파일 포맷을 사용합니다.
위와 같이 server
항목에 도메인(kube.xxx.com:6443) 을 입력해줍니다.
이로써 워커노드의kubelet
은 kube.xxx.com
도메인 네임을 resolve 하기 위해 DNS Query를 하게 되고. 그 응답으로 돌아오는 IP 주소를 통해 kube-apiserver
에 호출하게됩니다.
kubelet
의 동작을 자세하게 확인하기 위해서 kubelet
의 로그레벨을 잠시 올려봅니다. kubelet
은 systemd
서비스로 설치되었고. 로그는 journald
에 기록되기때문에 로그를 확인하기 위해서는
$ journalctl -xeu kubelet -f
형태로 확인 가능합니다.
로그 레벨 변경은 $ vi /etc/kubernetes/kubelet.env
을 통해 kubelet.env
파일을 수정해야합니다.
기본적으로 위와 같이 KUBE_LOG_LEVEL
은 --v=2
로 되어있습니다. 이것을 잠시 9
로 올려봅니다.
위 환경변수를 반영하기 위해서 $ systemctl restart kubelet
으로 kubelet
을 재실행합니다.
워커노드에서의 테스트
현재 환경에서는 2개의 kube-apiserver
가 있는데. 두 kube-apiserver
를 죽여봅니다. 각각의 컨트롤 플레인에서
$ mv /etc/kubernetes/manifests/kube-apiserver.yaml /root/kube-apiserver.yaml
를 통해 kube-apiserver
static pod 를 제거해봅니다.

kube-apiserver.yaml
을 제거하고. 프로세스가 떠있지 않고 6443 포트가 열려있지 않음을 확인
위와 같은 상황일때. Route53
은 kube.xxx.com
에 대한 DNS 쿼리 응답으로. kube.xxx.com
에 등록한 모든 A Record
를 반환합니다.

현재 172.xx.xx.31
, 172.xx.xx.32
두 컨트롤 플레인 노드에 떠있는 kube-apiserver
가 모두 없는 상황입니다.
이 때 워커노드의 kubelet
은 어떤지 로그를 살펴보면 ($ journalctl -xeu kubelet -f
)

노란색 박스 친 부분을 보면 round_trippers
코드 부분에서 DNS Loookup 을 진행해서 두개의 IP 주소를 받아오고. 이후 앞쪽에 있는 IP 주소에 먼저 TCP 커넥션 맺기를 시도해보고, 이후 다른 IP 주소로도 TCP 커넥션을 맺어보려고 한다. 이 round_trippers
는 DNS 쿼리 결과에 따른 IP 주소들에 대해서 성공하는 IP 주소로 DNS Query 결과를 kubelet
이 캐싱해서 해당 IP 주소로 계속해서 요청하기 위한 로직입니다.
실제 kubernetes 컴포넌트들의 클라이언트 모듈인 client-go
레포 내의 round_trippers
코드를 보면 도메인을 DNS Query 하고, IP 주소로 커넥션을 맺어보려고 시도하는 로직들이 들어있습니다.
여기서 172.xx.xx.31
서버의 kube-apiserver
를 복구하고 kubelet
로그를 다시 확인해보겠습니다.

172.xx.xx.31
에 떠있는 kube-apiserver
로의 TCP 커넥션 맺기가 성공했다는 로그를 확인 할 수 있습니다. 이러한 로그가 생긴 시점 이후로는 kube.xxx.com
도메인으로 kube-apiserver
접근 요청을 하더라도 클라이언트 내부적으로 172.xx.xx.31
로 계속해서 요청을 보내게 됩니다.
추가적으로172.xx.xx.32
의 kube-apiserver
를 복구하더라도 더이상 DNS Query 를 하지 않고 계속해서 기존에 정상적으로 호출했던 172.xx.xx.31
쪽으로 요청을 하는것도 확인 할 수 있었습니다.
그럼 잘 동작하는거 아닌가요? kube-apiserver 앞단에 로드밸런서 이제 걷어내도 되나요?
위 테스트를 간단하게 했을때는 kube-apiserver
의 일부가 장애가 발생했더라도, 정상적인 kube-apiserver
로 쿠버네티스 컴포넌트들(kubelet 포함한 kube-scheduler, kube-controller-manager 등)이 Round Robin DNS 를 통해서 정상적으로 동작하는 것 처럼 보입니다.
여기서 중요한 점은, kubelet
을 포함한 쿠버네티스 컴포넌트들은 아까 kubelet
이 사용했던 kubernetes/client-go
를 사용하는데. 이 클라이언트 코드는 DNS
쿼리를 하고, 반환된 IP 주소에 TCP 커넥션을 맺어보고 성공한 IP:Port
조합으로 계속해서 요청을 보내는 코드입니다.
따라서 172.xx.xx.31
, 172.xx.xx.32
에 존재하는 두개의 kube-apiserver
가 모두 장애가 발생했다가, 한쪽이 복구되었을 때, 복구된 한 쪽으로 kubelet
같은 쿠버네티스 컴포넌트들, 그리고 클라이언트들이 잘 호출 할 수 있겠지만, 클라이언트 코드쪽의 DNS 캐싱정책으로 인해서, 장애가 있었던 나머지 한쪽이 복구가 되더라도 트래픽이 Load Balancing 되는 것이 아니라, 여전히 한쪽으로만 흘러가게 되는 현상을 겪을 수 밖에 없습니다.
이는 Round Robin DNS
자체의 문제점으로, 불특정 다수의 다양한, 수많은 클라이언트가 동일 도메인으로 접속하는 경우에 대해서는 어느정도의 Load Balancing 이 가능하지만. 소수의 일부 클라이언트가 지속해서 접속해야하는 케이스에서는 적절한 Load Balancing 을 기대하긴 어렵습니다.
Wrapping up
Round Robin DNS 를 통해 kube-apiserver
앞단의 로드밸런서를 대체 할 수 있는지 테스트해보았는데 결론적으로는 반은 되고 반은 안되는 구성이라고 볼 수 있습니다.
kube-apiserver
의 앞단 LB 를 대체 할 수 있는 부분은 일부 kube-apiserver
장애시 장애에 대한 복구(fail over)는 어느정도 가능하지만 대체 할 수 없는 부분이 일부 존재해 정말 진정한 Load Balancing
을 수행하기는 어렵다는 부분이 있습니다.
일반적인 LB의 경우 들어온 요청을 자신이 관리하는 뒷단의 백엔드 서버들로 로드밸런싱 정책에 맞춰 부하를 분산해줍니다.
하지만 지금과 같은 설정 케이스(RR-DNS)는 별도의 서버가 트래픽을 중계(proxy)해서 처리하는 형태가 아닌. 각각의 클라이언트들이 Domain 에 대한 IP 주소를 Resolve하고. 그 IP 주소가 여러대의 백엔드 서버(머신)주소 일 때 진정한 부하 분산이 되는 케이스라고 볼 수 있습니다.
여기서 클라이언트의 DNS Query 캐싱 동작으로 인해, 여러 클라이언트들이 여러 IP주소의 백엔드로 호출하고 있다가, 장애가 발생해서 일부 백엔드로 호출하고, 다시 모든 백엔드 서버가 정상이 되어서 여러 IP 주소의 백엔드로 호출하는 (부하분산) 형태가 아닌점이 문제가 됩니다.
대부분의 클라이언트들은 한번 DNS Query 를 하고 정상 동작중이라면 계속해서 동일한 IP주소의 백엔드에 요청을 보낸다는 점이 문제가 됩니다. 따라서 장애 발생 후 복구가 되었더라도, 장애 발생시 정상적으로 처리했던 백엔드에만 클라이언트가 요청을 하게 되는것입니다.
이렇게 되면 일부 서버에만 부하가 몰리게 됩니다.
물론 kubelet
같은 것들을 임의로 재시작해서(kubelet 프로세스의 재시작에 의미가 있는 건 아니고 DNS 캐싱을 초기화 하는 부분에 의미가 있음) Round Robin DNS 를 통해 어느정도의 강제적인 로드밸런싱을 수행 할 수도 있겠지만 그렇게 하는것이 과연 H/W, S/W LB 를 kube-apiserver
앞단에 두는것보다 얼마나 더 장점이 있을지를 고려해보고, 사용 자체는 해볼 수 있을 것으로 판단은 됩니다.