필터#HA×

[Kubernetes] HAproxy를 통한 마스터 노드 삼중화(HA) 구성

쿠버네티스 클러스터를 운영할 때 마스터 노드(컨트롤 플레인)가 1대뿐이라면, 그 노드가 죽는 순간 클러스터 전체를 제어할 수 없게 된다. 이런 단일 장애점(SPOF)을 없애기 위해 마스터 노드를 여러 대로 구성하고, 그 앞단에 로드밸런서를 두는 것이 일반적인 고가용성(HA) 구성이다. 이번 글에서는 HAproxy를 이용해서 마스터 노드 3대를 삼중화하는 과정을 정리해본다. 고가용성(HA) 구성의 원리 마스터 노드를 여러 대로 구성한다고 해서 모든 노드가 동시에 요청을 처리하는 건 아니다. 보통 3개의 마스터 노드가 있다면 그중 한 개만 활성화(Active) 상태이고, 나머지는 Standby 상태로 대기한다. 그러다가 Active 상태였던 노드에 문제가 생기면, 대기 중이던 다른 노드 하나가 새로운 Active로 전환되면서 서비스를 계속 유지하는 방식이다. 노드 개수는 왜 홀수여야 할까? 새로운 Active 마스터 노드를 선출하려면 마스터 노드의 개수를 반드시 홀수 개(3, 5, 7, 9...) 로 구성해야 한다. 이는 네트워크 분할(Network Partition) 같은 상황에서 클러스터가 과반수 이상의 동의를 얻어 의사결정 권한을 명확히 하기 위함이다. 짝수 개로 구성하면 표가 정확히 반으로 갈리는 상황(Splitbrain)이 발생할 수 있어서, 어느 쪽이 정상 다수인지 판단할 수 없게 된다. 그럼 여기서 말하는 "의사결정"이란 정확히 뭘까? 조금 더 구체적으로 들어가보자. 쿠버네티스의 모든 상태 정보 — 어떤 파드가 떠 있는지, 어떤 노드가 살아있는지, 디플로이먼트 설정이 뭔지 — 는 각 마스터 노드 안에 있는 etcd라는 저장소에 기록된다. kubectl apply로 뭔가를 배포하든, 노드가 하트비트를 보내든, 컨트롤러가 파드 개수를 조정하든, 결국은 전부 "etcd에 이 상태 변경을 기록해달라"는 요청으로 이어진다. 마스터가 3대면 etcd도 3벌이 각자 떠서 서로 동기화하고 있는데, 한 대에만 기록하고 끝내버리면 나머지 두 대는 그 사실을 모르는 채로 남게 된다. 그래서 etcd는 Raft라는 분산 합의 알고리즘을 사용해서, 상태 변경 요청이 들어올 때마다 "이 변경을 클러스터의 공식 기록으로 확정해도 되는가"를 매번 투표로 결정한다. 리더 역할을 맡은 etcd가 나머지 멤버들에게 "이거 기록해도 될까?"라고 묻고, 과반수가 "나도 기록했다"고 응답해야 그 변경이 확정(commit)된다. 과반수 동의를 못 받으면 그 요청은 보류되고, 클러스터 입장에서는 새로운 배포도, 파드 상태 갱신도 반영되지 않는 상태가 된다. 이 투표를 진행할 리더가 누구인지를 정하는 것도 같은 방식의 의사결정이다. 리더가 죽으면 남은 멤버들끼리 다시 투표해서 새 리더를 뽑는데, 이 재선출 과정 동안 잠깐 요청이 안 먹히는 시간이 생긴다. 뒤에서 소개할 장애 테스트에서 kubectl 명령이 한순간 타임아웃 나는 것도 바로 이 재선출 과정 때문이다. 쿼럼(quorum) 계산 — 몇 대까지 죽어도 괜찮을까? 마스터가 3대일 때 과반수(쿼럼)는 2표다. 즉 3대 중 2대만 살아있어도 투표는 성립하고, 클러스터는 정상적으로 의사결정을 계속할 수 있다. 반대로 3대 중 2대가 죽어서 1대만 남으면, 1표로는 과반수(2표)에 못 미치기 때문에 그때는 진짜로 쿼럼이 깨지고 클러스터가 쓰기 작업을 처리하지 못하는 상태에 빠진다. 정리하면 3대 구성에서는 딱 1대까지만 장애를 허용할 수 있는 셈이다. 그렇다면 "짝수인 4대로 구성하면 더 안전하지 않을까?" 하는 생각이 들 수 있는데, 계산해보면 그렇지 않다. 4대 구성의 과반수는 3표이므로, 4대 중 2대가 죽으면 역시 쿼럼이 깨진다. 결국 4대든 3대든 버틸 수 있는 장애 대수는 똑같이 1대뿐이고, 4대는 서버 한 대를 더 쓰면서도 내결함성은 그대로인 비효율적인 구성이 된다. 같은 수준의 내결함성을 얻는 데 필요한 최소 노드 수를 구하면 자연스럽게 홀수(3, 5, 7...)가 나오기 때문에, 마스터 노드는 관례적으로 홀수 개로 구성하는 것이다. 실습 아키텍처 이번 실습에서는 아래와 같이 구성한다. 마스터 노드 3개 (m1, m2, m3) — 각각 etcd를 포함한 컨트롤 플레인 역할 HAproxy 서버 1개 — 마스터 노드 앞단에서 로드밸런싱 + 리버스 프록시 역할 kubectl 명령은 HAproxy 서버의 가상 IP(VIP)를 향해 요청을 보내고, HAproxy가 이 요청을 뒤쪽의 m1 / m2 / m3 중 살아있는 노드로 분산해주는 구조다. 1. HAproxy 서버 설정 먼저 HAproxy 역할을 할 서버에 HAproxy를 설치한다. bash apt update y apt install y haproxy 설정 파일을 연다. bash vi /etc/haproxy/haproxy.cfg 기존 내용은 전부 지우고, 아래 내용으로 채워준다. frontend kubernetesmasterlb bind 0.0.0.0:6443 option tcplog mode tcp defaultbackend kubernetesmasternodes backend kubernetesmasternodes mode tcp balance roundrobin option tcpcheck option tcplog server k8smaster1 211.183.3.101:6443 check server k8smaster2 211.183.3.102:6443 check server k8smaster3 211.183.3.103:6443 check frontend: 클라이언트(kubectl 등)의 요청을 6443 포트로 받는다. 쿠버네티스 API 서버의 기본 포트가 6443이기 때문이다. backend: 실제 요청을 전달할 대상, 즉 마스터 노드 3대의 IP와 포트를 나열한다. balance roundrobin으로 요청을 순서대로 분산하고, option tcpcheck로 각 서버가 살아있는지 주기적으로 확인한다. 마스터 노드 3대를 위 IP들을 갖도록 구성해두면 된다. 삼중화가 잘 구성되어 있다면, 특정 Active 마스터 노드에 문제가 생기더라도 Standby 상태였던 마스터 노드가 자동으로 Active로 선출되어 서비스가 유지된다. 설정을 저장했으면 HAproxy를 재시작하고 부팅 시 자동 실행되도록 등록한다. bash systemctl restart haproxy systemctl enable haproxy 참고: 이 시점에는 아직 m1 m3 마스터 노드들이 준비되지 않았기 때문에, HAproxy 입장에서는 백엔드 서버들이 응답하지 않아 에러 로그가 뜰 수 있다. 마스터 노드들을 구성한 뒤 다시 확인하면 된다. 2. 첫 번째 마스터 노드(m1) 초기화 이제 m1 서버에서 클러스터를 초기화한다. 이때 핵심은 controlplaneendpoint에 HAproxy 서버의 IP를 지정해주는 것이다. 이렇게 해야 클러스터의 모든 구성원이 HAproxy를 거쳐서 API 서버에 접근하게 된다. bash kubeadm init podnetworkcidr=10.244.0.0/16 uploadcerts kubernetesversion=v1.30.3 \ ignorepreflighterrors=all \ controlplaneendpoint=211.183.3.50 초기화가 끝나면 kubectl을 사용할 수 있도록 kubeconfig를 설정해준다. bash mkdir p $HOME/.kube sudo cp i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id u):$(id g) $HOME/.kube/config 3. m2, m3를 컨트롤 플레인으로 조인 kubeadm init이 끝나면 콘솔에 조인용 명령어가 출력된다. 이 중 컨트롤 플레인 노드용 조인 명령을 복사해서 m2, m3 서버에 각각 붙여넣어 실행하면 된다. (워커 노드용 조인 명령과는 다른 명령이므로 헷갈리지 않도록 주의한다.) m2, m3에 컨트롤 플레인 조인 명령을 복붙해주면 마스터 노드 삼중화 구성이 완료된다. 4. HAproxy 서버에서 kubectl 사용하기 HAproxy 서버에서도 클러스터를 직접 관리할 수 있으면 편하다. 이를 위해서는 두 가지가 필요하다. 1. kubectl 명령이 설치되어 있어야 한다. 2. 해당 클러스터의 kubeconfig 파일(m1의 /etc/kubernetes/admin.conf에 존재)을 가져와야 한다. kubectl 설치 bash aptget install y apttransporthttps cacertificates curl mkdir p /etc/apt/keyrings curl fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key sudo gpg dearmor o /etc/apt/keyrings/kubernetesaptkeyring.gpg echo 'deb [signedby=/etc/apt/keyrings/kubernetesaptkeyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' sudo tee /etc/apt/sources.list.d/kubernetes.list aptget update aptget install y kubectl aptmark hold kubectl kubeconfig 가져오기 m1에서 admin.conf 내용을 출력해서 복사한다. bash 에서 cat /etc/kubernetes/admin.conf 내용 복사 haproxy 서버에서 kube 설정 디렉터리를 만들고, 복사한 내용을 붙여넣는다. bash 에서 mkdir /.kube vi /.kube/config 파일 생성 후 m1에서 복사한 내용을 붙여넣기 이제 HAproxy 서버에서 kubectl get nodes를 실행하면 m1, m2, m3 세 노드가 모두 정상적으로 조회되는 것을 확인할 수 있다. 5. 장애 상황 테스트 — Active 노드 다운 삼중화가 실제로 잘 동작하는지 확인해보자. m1 서버(현재 Active 역할)를 강제로 중지시킨 뒤, HAproxy 서버에서 다시 kubectl 명령을 날려본다. 바로 직후에는 아래처럼 명령이 먹히지 않는 순간이 발생한다. root@haproxy: kubectl get nodes Unable to connect to the server: net/http: TLS handshake timeout HAproxy가 죽은 노드를 헬스체크로 감지하고 백엔드에서 제외하기까지는 약간의 시간이 걸리기 때문에 나타나는 현상이다. 조금 뒤 watch 명령으로 다시 확인해보면, m1이 NotReady 상태로 바뀌고 나머지 m2, m3는 여전히 Ready 상태로 정상 동작하고 있는 것을 확인할 수 있다. bash watch kubectl get nodes 즉, Active였던 m1이 죽어도 나머지 마스터 노드(m2, m3)가 살아있는 한 클러스터 제어 자체는 계속 정상적으로 이루어진다는 걸 실습으로 확인할 수 있었다. 결국 HAproxy를 이용한 마스터 노드 삼중화의 핵심은 "요청을 받는 창구(HAproxy)는 하나로 통일하되, 실제로 그 요청을 처리하는 마스터는 여러 대를 두어 하나가 죽어도 나머지가 대신 응답하게 만드는 것"이다. 단일 장애점을 없애고 안정적으로 클러스터를 운영하려면 꼭 필요한 구성이라는 걸 다시 한번 체감했다.

Sep 08, 2026kubernetes
[Kubernetes] HAproxy를 통한 마스터 노드 삼중화(HA) 구성