k3d 실습으로 분산 클러스터의 고가용성(HA) 이해하기
k3s는 경량화된 Kubernetes 배포판으로, 테스트 목적으로 간단한 클러스터를 구축할때 주로 사용하고 etcd 대신 SQLite를 사용할 수 있다. 용어상 이해를 돕기 위해 첨언하자면 Control Plane을 Server 노드, Worker Node를 Agent 노드라고 부른다.
k3d는 이 k3s을 Docker 위에서 클러스터 형태로 구동시켜주는 도구로, 우리는 이를 통해 간단하게 쿠버네티스 클러스터가 분산 환경에서 어떻게 고가용성을 유지하는지 알아볼 수 있다.

Docker 위에서 동작하는 컨테이너 단위로 실행되는 k3d는 하드웨어 분산이 일어나지는 않는다.
k3s를 직접 설치해서 etcd를 연동하고 HA를 구성하는게 낫긴 한데, 아무래도 귀찮다.
다만 논리적인 HA 시뮬레이션으로, 서버/에이전트 컨테이너를 죽였을 때 클러스터 상태가 어떻게 되는지 확인해서 분산 환경의 고가용성에 대해 이해할 수 있다.
1. k3d 설치 및 확인
(Windows OS 기준) Git bash로 리눅스 스타일 설치

k3d 버전 표시가 잘 나타남을 확인
2. 기본 클러스터 구성
server 3개에 agents 2개로 구성하겠삼
k3d cluster create test-k3d-cluster --servers 3 --agents 2 --api-port 6443 --k3s-arg "--disable=traefik@server:\*" --waitk3d kubeconfig get test-k3d-cluster | sed 's|host.docker.internal|127.0.0.1|g' > ~/.kube/config
클러스터 상태 확인: kubectl get nodes -o wide

kubectl 오류 - couldn't get current server API group list
A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
- kubectl 명령어가 자꾸 이전에 쓰던 쿠버 config 가져와서 연결못하는거
k3d kubeconfig get test-k3d-cluster의clusters.cluster.server가host.docekr.internal을 resolve하고 있는데, k3d API 서버가 실제 리스닝하고 있지 않음- 직접 config를 수정 -
k3d kubeconfig get test-k3d-cluster | sed 's|host.docker.internal|127.0.0.1|g' > ~/.kube/config
3. 서버(Control Plane)의 장애 상황을 상정한 실험 진행
etcd의 quorum이 유지되는지 확인하는 과정이 중요하다.
복습 차원에서 ‘분산 환경에서 노드를 3개 유지해야하는 이유’에 대해서 관심 있으면 따로 찾아보라한 적이 있을거다.
뗏목 정족수(quorum) 대충 이런 키워드로 분산 환경에서 의사 결정하는 투표 방식에 대해서 찾아볼 수 있다.
1단계: 서버를 하나 제거한다.
여전히 클러스터 정족수를 만족하기 때문에, 모든 기능이 정상 작동

→ 서버 3대중에서 2개가 살아있기 때문에 투표상의 과반수를 확보
etcd에 상태 변경을 기록하는 작업도 잘 작동함을 확인하기 위해서 Pod를 생성해보겠다.

예시로 저번에 만들어본 적 있는 nginx Pod를 생성해보겠음

2단계: 서버를 2개 제거한다.
서버 노드가 1개 밖에 남지 않는다. etcd의 정족수가 과반수를 얻지 못하고 클러스터 마비
- 3개 중 2개 노드를 삭제 (kubectl delete node)
- 컨트롤 플레인 노드 1개만 남았지만, Pod는 잘 돌아가고 kubectl 명령도 정상 작동하는 상황
실제 k3s를 하드웨어 수준에서 분산하면 Control Plain 기능 일부가 마비되는 수준이란다.
- 정확히는 etcd에 새로운 상태 변경을 기록할 수 없음

근데 난 왜 server-2 하나만 남겨놔도 조회나 deployment 생성까지 다 잘되는지 모르겠다.
kubectl delete node하면 리소스를 지운건 아니고 단순히 객체만 지운다.docker stop k3d-test-k3d-cluster-server-0컨테이너를 중지시키는게 이 상황에서 더 적절하다.
내가 알던거랑 실험 결과랑 다른거 같아서 찾아봤는데, etcd 정족수는 쓰기 연산에만 영향을 주고 전체 클러스터 불능과 동일하지 않다고 한다.
- etcd는 N개의 노드 중 과반수(majority)가 살아있어야 쓰기(write) 가능
- 3개 중 2개가 살아 있어야 쓰기(리소스 생성, 상태 변경, Deployment 조정 등) 가능
- 정족수 미만이면 쓰기 작업은 실패해도 읽기는 가능할 수 있음 (로컬 캐시 + stale read)
그리고 k3s의 특이한 점이 바로 '컨트롤 플레인 + etcd가 한 노드에 묶인다'는 점이다.
k3d-test-k3d-cluster-server-2 하나만 살아 있다고 가정한다면, 해당 노드가 etcd, controller-manager, scheduler, kubelet, coredns 전부 돌려서 축소된 클러스터로 동작한다.
- 즉, 남은 1개 노드가 단일 노드 클러스터처럼 자기 역할을 다 하므로 문제 없음
3단계: 서버를 모두 제거한다.
서버를 3개 모두 제거하면 quorum 정족수를 채우지 못하고 클러스터가 마비된 상태는 동일하다.

이런 오류가 뜰 수도 있다
$ kubectl get nodes
Error from server (ServiceUnavailable): the server is currently unable to handle the request (get nodes)
- 재시작해도 계속
cluster-server-0의 상태가 NotReady인 문제 (failed to find remote peer in cluster) - 클러스터 재생성:
k3d cluster list: 내 클러스터 리소스 이름 확인 →k3d cluster delete <name> - raft 클러스터 멤버들을 서로 찾지 못하는건데, etcd quorum이 깨져서 컨트롤 플레인이 고장남
4. 에이전트(Worker Node)의 장애 상황을 상정한 실험 진행
죽인 서버를 재시작해서 복구할 수 있다.
docker start k3d-test-k3d-cluster-server-1
실험 환경

현재 상태는 nginx Pod를 5개 정도 생성하니 agent-0,1와 server-0,1,2 골고루 스케줄링된 걸 알 수 있다.
실험1 - 노드 삭제시 클러스터 변화
agent 노드를 지우고, controller-manager가 노드 상태를 업데이트하도록 약 40초 정도 기다린다.
kubectl delete node k3d-test-k3d-cluster-agent-0
재접속 시도나 통신 문제에 대해서 kubelet은 API 서버로 주기적으로 전송한다. 이걸 node-controller가 감시하면서 노드의 상태를 변화시킨다.
- kubelet:
node-status-update-frequency(default=10s) - node-controller:
--node-monitor-grace-period(default=40s)
반영될때까지 기다리면 다음과 같다.

기존 agent-0에 있던 Pod들이 Terminating. 즉, 삭제되고 있다.
실험2 - Pod Rescheduling
Pod Rescheduling: 컨트롤 플레인에서 해당 Deployment이 원하는 Pod수보다 적은걸 감지하고, 다른 노드에 다시 Pod를 생성한다.
- 하지만 Deployment 없이 단일 Pod로 생성한 nginx같은 경우는 지워지고 다시 살아나지 않음
CoreDNS, metrics-server, local-path-provisioner 같은 시스템 Pod는 기본적으로 서버 노드에 스케줄된다

그래서 Deployment, DaemonSet, StatefulSet 등의 리소스를 통해 관리하도록 만들어서 재스케줄링을 실험해보자.
kubectl create deployment nginx-deploy --image=nginxkubectl scale deployment nginx-deploy --replicas=5

위 스크린샷 결과에서는 get nodes 했을때 agent-0 없는걸 확인 수 있다.
사진 찍기전에 이미 삭제하고 온건데, 반영하는데 시간 걸려서 아직 agent-0에 속한 pod가 있는걸로 나온다.
시간 좀 지나니까 사라지고, kube API가 감지해서 server-1 노드에 다시 스케줄링했다. (2p5sr → jmjn2)
agent-1 노드도 지우고 나니까 거기 있던 Pod가 server-2 노드에 새로 스케줄링됐다. (5ngt1 → 7g654)
5. 결론? 요약?
서버 노드(Control Plane)에서 장애 발생시, 최소 3개의 노드가 있어야 분산 환경에서 고가용성을 유지할 수 있다.
N개 중에서 N+1/2 개(과반수)의 동의를 얻어서 새로운 리더를 선출하기에, 3개 노드일때 1개가 장애 발생해도 가용성을 잃지 않고 복구시킬 수 있다.
- 상세한 내용이 궁금하다면 뗏목 정족수(Raft consensus quorum)에 대해서 검색해보삼
- zookeeper 쓰던 apche kafka가 사용해서 유명한데, k8s 내부에서 사용하는 etcd도 이거 씀

에이전트 노드(Worker Node)에 대해서 kubectl get nodes가 정상 작동함을 확인할 수 있다.
- 클러스터는 살아있고, 에이전트들이 삭제됨에 따라 Pod들이 생존한 다른 노드로 스케줄링된다.
지금은 컨트롤 플레인(Server 노드)들이 에이전트의 Worker Node 역할도 같이 하도록 해서 스케줄링되는거다. 그러면 얘네가 컨트롤 플레인 역할만 하도록 제한하면???
kubectl get pod -A를 통해 Pod를 조회해보면 모두 STATUS가 Pending 상태로 멈추게 될거여- Worker Node들이 모두 동작하지 않으니 Pod 스케줄링이 안되는겨