이번 실습에서는 Kubespray를 사용해서 고가용성(HA) Kubernetes 클러스터를 구축한다. VirtualBox와 Vagrant로 가상 환경을 만들고, HAProxy를 통해 API 서버 로드밸런싱을 구현하는 구조다.
| 노드명 | 역할 | CPU | RAM | IP 주소 | 초기화 스크립트 |
|---|---|---|---|---|---|
| admin-lb | Kubespray 실행, API LB | 2 | 1GB | 192.168.10.10 | admin-lb.sh |
| k8s-node1 | Control Plane | 4 | 2GB | 192.168.10.11 | init-cfg.sh |
| k8s-node2 | Control Plane | 4 | 2GB | 192.168.10.12 | init-cfg.sh |
| k8s-node3 | Control Plane | 4 | 2GB | 192.168.10.13 | init-cfg.sh |
| k8s-node4 | Worker Node | 4 | 2GB | 192.168.10.14 | init-cfg.sh |
| k8s-node5 | Worker Node | 4 | 2GB | 192.168.10.15 | init-cfg.sh |

HAProxy 로드밸런서
Control Plane HA 구성
Worker Node
먼저 실습용 디렉터리를 만들고 필요한 파일들을 다운로드한다.
# 실습용 디렉터리 생성
mkdir k8s-ha-kubespary
cd k8s-ha-kubespary
# 필요한 파일들 다운로드
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-ha-kubespary/Vagrantfile
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-ha-kubespary/admin-lb.sh
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-ha-kubespary/init_cfg.sh
# 실습 환경 배포 (약 10분 소요)
vagrant up
# 배포 상태 확인
vagrant status
작업 설명:
vagrant up: Vagrantfile을 읽어서 6개의 가상머신을 생성하고 초기화한다admin-lb.sh 스크립트로 초기화된다init_cfg.sh 스크립트로 초기화된다vagrant ssh admin-lb
HAProxy는 Kubernetes API 서버 앞단에서 로드밸런서 역할을 수행한다.
# HAProxy 설정 파일 확인
cat /etc/haproxy/haproxy.cfg
핵심 설정 내용:
# Frontend: 6443 포트로 들어오는 요청을 받는다
frontend k8s-api
bind *:6443
mode tcp
option tcplog
default_backend k8s-api-backend
# Backend: 3개의 Control Plane에 Round Robin으로 분산한다
backend k8s-api-backend
mode tcp
option tcp-check
option log-health-checks
timeout client 3h
timeout server 3h
balance roundrobin
server k8s-node1 192.168.10.11:6443 check check-ssl verify none inter 10000
server k8s-node2 192.168.10.12:6443 check check-ssl verify none inter 10000
server k8s-node3 192.168.10.13:6443 check check-ssl verify none inter 10000
작업 설명:
mode tcp: L4 레벨에서 동작한다 (HTTPS 암호화 트래픽을 그대로 전달)balance roundrobin: 순차적으로 요청을 분산한다check-ssl verify none: SSL 인증서 검증 없이 헬스체크를 수행한다inter 10000: 10초마다 헬스체크를 수행한다# 서비스 상태 확인
systemctl status haproxy.service --no-pager
# 포트 리스닝 확인
ss -tnlp | grep haproxy
출력 예시:
LISTEN 0 3000 0.0.0.0:6443 0.0.0.0:* # k8s api loadbalancer
LISTEN 0 3000 0.0.0.0:9000 0.0.0.0:* # haproxy stats dashboard
LISTEN 0 3000 0.0.0.0:8405 0.0.0.0:* # metrics exporter
웹 브라우저에서 http://192.168.10.10:9000/haproxy_stats로 접속하면 HAProxy의 실시간 상태를 확인할 수 있다. 현재는 Kubernetes가 아직 설치되지 않아서 backend 서버들이 모두 DOWN 상태로 표시된다.
admin-lb 노드는 NFS 서버도 함께 실행한다. 이는 나중에 PersistentVolume으로 사용할 수 있다.
# NFS 서비스 상태 확인
systemctl status nfs-server --no-pager
# 공유 디렉터리 확인
tree /srv/nfs/share/
# Export 설정 확인
cat /etc/exports
# 출력: /srv/nfs/share *(rw,async,no_root_squash,no_subtree_check)
# Export 적용
exportfs -rav
admin-lb에서 모든 노드에 passwordless SSH 접속이 가능한지 확인한다.
# /etc/hosts 파일 확인
cat /etc/hosts
# 모든 노드 통신 확인
for i in {1..5}; do
echo ">> k8s-node$i <<"
ssh k8s-node$i hostname
echo
done
# Kubespray 작업 디렉터리 확인
cd /root/kubespray/
tree /root/kubespray/ -L 2
# Ansible 설정 파일 확인
cat ansible.cfg
# Inventory 파일 확인
cat /root/kubespray/inventory/mycluster/inventory.ini
Kubespray는 Ansible을 사용해서 Kubernetes를 배포한다. 먼저 inventory 파일을 확인한다.
cd /root/kubespray/
# Inventory 파일 내용 확인
cat /root/kubespray/inventory/mycluster/inventory.ini
Inventory 구조:
[kube_control_plane]
k8s-node1 ansible_host=192.168.10.11 ip=192.168.10.11 etcd_member_name=etcd1
k8s-node2 ansible_host=192.168.10.12 ip=192.168.10.12 etcd_member_name=etcd2
k8s-node3 ansible_host=192.168.10.13 ip=192.168.10.13 etcd_member_name=etcd3
[etcd:children]
kube_control_plane
[kube_node]
k8s-node4 ansible_host=192.168.10.14 ip=192.168.10.14
작업 설명:
[kube_control_plane]: Control Plane 역할을 할 노드들을 정의한다[etcd:children]: etcd는 Control Plane 노드에 함께 설치된다[kube_node]: Worker 노드를 정의한다etcd_member_name: etcd 클러스터 멤버 이름을 지정한다# Inventory 그래프 형태로 확인
ansible-inventory -i /root/kubespray/inventory/mycluster/inventory.ini --graph
출력:
@all:
|--@ungrouped:
|--@etcd:
| |--@kube_control_plane:
| | |--k8s-node1
| | |--k8s-node2
| | |--k8s-node3
|--@kube_node:
| |--k8s-node4
배포 전에 클러스터 설정을 수정한다.
# 1. 클러스터 기본 설정 변경
sed -i 's|kube_owner: kube|kube_owner: root|g' inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
sed -i 's|kube_network_plugin: calico|kube_network_plugin: flannel|g' inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
sed -i 's|kube_proxy_mode: ipvs|kube_proxy_mode: iptables|g' inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
sed -i 's|enable_nodelocaldns: true|enable_nodelocaldns: false|g' inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
# 2. CoreDNS autoscaler 비활성화
echo "enable_dns_autoscaler: false" >> inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
# 3. Flannel 네트워크 인터페이스 지정
echo "flannel_interface: enp0s9" >> inventory/mycluster/group_vars/k8s_cluster/k8s-net-flannel.yml
# 설정 확인
grep -iE 'kube_owner|kube_network_plugin:|kube_proxy_mode|enable_nodelocaldns:' \
inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
설정 설명:
kube_owner: root: Kubernetes 바이너리 소유자를 root로 설정한다kube_network_plugin: flannel: CNI로 Flannel을 사용한다kube_proxy_mode: iptables: kube-proxy를 iptables 모드로 실행한다enable_nodelocaldns: false: NodeLocal DNS를 비활성화한다flannel_interface: enp0s9: Flannel이 사용할 네트워크 인터페이스를 지정한다# Metrics Server 활성화
sed -i 's|metrics_server_enabled: false|metrics_server_enabled: true|g' \
inventory/mycluster/group_vars/k8s_cluster/addons.yml
# 리소스 요청량 설정
echo "metrics_server_requests_cpu: 25m" >> inventory/mycluster/group_vars/k8s_cluster/addons.yml
echo "metrics_server_requests_memory: 16Mi" >> inventory/mycluster/group_vars/k8s_cluster/addons.yml
# 설정 확인
grep -iE 'metrics_server_enabled:' inventory/mycluster/group_vars/k8s_cluster/addons.yml
작업 설명:
kubectl top 명령어를 사용하기 위해 필요하다이제 본격적으로 Kubernetes 클러스터를 배포한다. 약 8분 정도 소요된다.
# 배포 전 Task 목록 확인 (선택사항)
ansible-playbook -i inventory/mycluster/inventory.ini -v cluster.yml --list-tasks
# Kubernetes 1.32.9 버전으로 배포 시작
ANSIBLE_FORCE_COLOR=true ansible-playbook \
-i inventory/mycluster/inventory.ini \
-v cluster.yml \
-e kube_version="1.32.9" | tee kubespray_install.log
작업 설명:
ansible-playbook: Ansible 플레이북을 실행한다-i inventory/mycluster/inventory.ini: 인벤토리 파일을 지정한다-v: verbose 모드로 실행한다cluster.yml: Kubespray의 메인 플레이북이다-e kube_version="1.32.9": Kubernetes 버전을 지정한다tee kubespray_install.log: 설치 로그를 파일로 저장한다Kubespray는 다음과 같은 작업들을 자동으로 수행한다:
사전 준비
컨테이너 런타임 설치
Kubernetes 바이너리 다운로드
/tmp/releases에 캐시된다etcd 클러스터 구성
Control Plane 구성
Worker Node 조인
CNI 설치
애드온 설치
admin-lb 노드에서 kubectl을 사용하기 위해 kubeconfig 파일을 복사한다.
# .kube 디렉터리 생성
mkdir /root/.kube
# Control Plane 노드에서 kubeconfig 복사
scp k8s-node1:/root/.kube/config /root/.kube/
# API Server 주소 확인
cat /root/.kube/config | grep server
# 출력: server: https://127.0.0.1:6443
작업 설명:
# localhost를 k8s-node1 IP로 변경
sed -i 's/127.0.0.1/192.168.10.11/g' /root/.kube/config
# 노드 목록 확인
kubectl get node -owide
출력 예시:
NAME STATUS ROLES AGE VERSION INTERNAL-IP
k8s-node1 Ready control-plane 3m37s v1.32.9 192.168.10.11
k8s-node2 Ready control-plane 3m31s v1.32.9 192.168.10.12
k8s-node3 Ready control-plane 3m29s v1.32.9 192.168.10.13
k8s-node4 Ready <none> 3m3s v1.32.9 192.168.10.14
Control Plane 노드에는 기본적으로 Taint가 설정되어 있어서 일반 Pod가 스케줄링되지 않는다.
# 노드별 Taint 확인
kubectl describe node | grep -E 'Name:|Taints'
출력:
Name: k8s-node1
Taints: node-role.kubernetes.io/control-plane:NoSchedule
Name: k8s-node2
Taints: node-role.kubernetes.io/control-plane:NoSchedule
Name: k8s-node3
Taints: node-role.kubernetes.io/control-plane:NoSchedule
Name: k8s-node4
Taints: <none>
작업 설명:
NoSchedule Taint: 일반 Pod는 이 노드에 스케줄링되지 않는다각 노드에 할당된 Pod IP 대역을 확인한다.
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
출력:
k8s-node1 10.233.64.0/24
k8s-node2 10.233.65.0/24
k8s-node3 10.233.66.0/24
k8s-node4 10.233.67.0/24
작업 설명:
kubectl get pod -A
주요 Pod 목록:
kube-system/kube-apiserver-*: API Server (Control Plane 노드에만)kube-system/kube-controller-manager-*: Controller Manager (Control Plane 노드에만)kube-system/kube-scheduler-*: Scheduler (Control Plane 노드에만)kube-system/etcd-*: etcd (Control Plane 노드에만)kube-system/kube-flannel-*: Flannel CNI (모든 노드)kube-system/kube-proxy-*: kube-proxy (모든 노드)kube-system/coredns-*: CoreDNSkube-system/metrics-server-*: Metrics Serverssh k8s-node1 etcdctl.sh member list -w table
출력:
+------------------+---------+-------+----------------------------+----------------------------+------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
+------------------+---------+-------+----------------------------+----------------------------+------------+
| 8b0ca30665374b0 | started | etcd3 | https://192.168.10.13:2380 | https://192.168.10.13:2379 | false |
| 2106626b12a4099f | started | etcd2 | https://192.168.10.12:2380 | https://192.168.10.12:2379 | false |
| c6702130d82d740f | started | etcd1 | https://192.168.10.11:2380 | https://192.168.10.11:2379 | false |
+------------------+---------+-------+----------------------------+----------------------------+------------+
작업 설명:
PEER ADDRS: etcd 클러스터 내부 통신에 사용하는 주소 (2380 포트)CLIENT ADDRS: API Server가 연결하는 주소 (2379 포트)IS LEARNER: false는 정식 투표 멤버임을 의미한다for i in {1..3}; do
echo ">> k8s-node$i <<"
ssh k8s-node$i etcdctl.sh endpoint status -w table
echo
done
출력 예시:
>> k8s-node1
+----------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+----------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| 127.0.0.1:2379 | c6702130d82d740f | 3.5.25 | 8.3 MB | true | false | 4 | 2834 | 2834 | |
+----------------+------------------+---------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
작업 설명:
IS LEADER: true: 이 노드가 현재 etcd 리더다RAFT TERM: Raft 합의 알고리즘의 Term 번호DB SIZE: etcd 데이터베이스 크기# 현재 세션에 적용
source <(kubectl completion bash)
alias k=kubectl
alias kc=kubecolor
complete -F __start_kubectl k
# 영구 적용
echo 'source <(kubectl completion bash)' >> /etc/profile
echo 'alias k=kubectl' >> /etc/profile
echo 'alias kc=kubecolor' >> /etc/profile
echo 'complete -F __start_kubectl k' >> /etc/profile
작업 설명:
kubectl completion bash: kubectl 명령어 자동완성을 활성화한다alias k=kubectl: kubectl을 k로 축약한다alias kc=kubecolor: 색상 출력을 지원하는 kubecolor를 사용한다complete -F __start_kubectl k: k 별칭에도 자동완성을 적용한다k9s는 터미널 기반 Kubernetes 대시보드다.
k9s
주요 단축키:
0: 전체 네임스페이스 보기:pod: Pod 목록 보기:node: 노드 목록 보기/: 검색l: 로그 보기d: 삭제?: 도움말# Ansible Inventory 확인
ansible-inventory -i inventory/mycluster/inventory.ini --graph
ansible-inventory -i inventory/mycluster/inventory.ini --list
# Kubernetes 클러스터 배포
ansible-playbook -i inventory/mycluster/inventory.ini -v cluster.yml -e kube_version="1.32.9"
# 배포 Task 목록만 확인
ansible-playbook -i inventory/mycluster/inventory.ini cluster.yml --list-tasks
# 노드 목록 확인
kubectl get nodes -owide
# 노드 상세 정보 확인
kubectl describe node <node-name>
# 전체 Pod 확인
kubectl get pod -A
# Pod CIDR 확인
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
# etcd 멤버 목록
ssh k8s-node1 etcdctl.sh member list -w table
# etcd 상태 확인
ssh k8s-node1 etcdctl.sh endpoint status -w table
# etcd 헬스체크
ssh k8s-node1 etcdctl.sh endpoint health -w table
# HAProxy 상태 확인
systemctl status haproxy
# HAProxy 설정 확인
cat /etc/haproxy/haproxy.cfg
# 리스닝 포트 확인
ss -tnlp | grep haproxy
# HAProxy 로그 확인
journalctl -u haproxy.service

이제 고가용성 Kubernetes 클러스터가 완성되었다. 주요 특징을 정리하면: