Overview
Kubernetes 클러스터를 구축하는 방법은 여러 가지가 있지만, 온프레미스 환경에서 반복 가능하고 자동화된 설치를 원한다면 Kubespray가 가장 강력한 선택지 중 하나이다.
Kubespray는 Ansible 기반의 프로비저닝 도구로, YAML 기반 인벤토리 파일만 정의하면 다양한 환경(GCP, AWS, 온프렘 등)에서 HA 구성을 포함한 쿠버네티스 클러스터를 유연하게 설치할 수 있다.
이번 포스팅에서는 다음의 내용을 중심으로 정리하였다.
- Ubuntu 24.04 기반 온프레미스 환경에서 Kubespray를 이용한 Kubernetes v1.34 클러스터 구축
- Cilium CNI, Helm, Metrics Server, Krew 등 주요 애드온 설정
- containerd insecure registry (Harbor) 설정
- Worker Node 추가 (scale.yml) 및 제거 (remove-node.yml)
- 실제 구축 시 발생한 에러와 해결 방법

시스템 구성
2026.04.07 기준이다.
| 항목 | 값 |
| OS | Ubuntu 24.04 LTS |
| Kubernetes | v1.34.3 |
| Kubespray | v2.30.0 |
| CNI | Cilium v1.19.1 |
| Container Runtime | containerd v2.2.1 |
| Cluster DNS | somaz-cluster.local |
노드 구성
| 역할 | 호스트명 | IP | CPU | Memory |
| Control Plane + etcd | k8s-control-01 | 10.10.10.17 | 4 | 24GB |
| Worker | k8s-compute-01 | 10.10.10.18 | 12 | 48GB |
| Worker | k8s-compute-02 | 10.10.10.19 | 12 | 48GB |
| Worker | k8s-compute-03 | 10.10.10.22 | 12 | 48GB |
k8s-compute-01 까지 먼저 설치한 뒤, scale.yml을 사용해서 k8s-compute-02, 03을 추가하는 방식으로 진행한다.
사전 준비
SSH 키 생성 및 복사
모든 노드에 패스워드 없이 SSH 접속이 가능해야 한다.
ssh-keygen
# /etc/hosts 에 노드 등록
sudo vi /etc/hosts
10.10.10.17 k8s-control-01
10.10.10.18 k8s-compute-01
10.10.10.19 k8s-compute-02
10.10.10.22 k8s-compute-03
# SSH 키 복사
ssh-copy-id k8s-control-01
ssh-copy-id k8s-compute-01
ssh-copy-id k8s-compute-02
# 접속 확인
ssh k8s-control-01
ssh k8s-compute-01
ssh k8s-compute-02
패키지 설치 및 Kubespray 준비
# Python 3.10 및 필수 패키지 설치
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get -y update
sudo apt install -y python3.10 python3-pip git python3.10-venv
python3.10 --version
# Kubespray 클론
git clone https://github.com/kubernetes-sigs/kubespray.git
cd kubespray
# 원하는 버전으로 체크아웃
git checkout v2.30.0
# 인벤토리 복사
cp -rfp inventory/sample inventory/somaz-cluster
# Python 가상환경 생성 및 활성화
python3.10 -m venv venv
source venv/bin/activate
# 의존성 설치
pip install -U -r requirements.txt
인벤토리 설정
inventory.ini
`inventory/somaz-cluster/inventory.ini`
처음에는 k8s-compute-01만 포함하여 설치한다. k8s-compute-02는 이후 `scale.yml` 로 추가한다.
[kube_control_plane]
k8s-control-01 ansible_host=10.10.10.17 ip=10.10.10.17 etcd_member_name=etcd1
[etcd:children]
kube_control_plane
[kube_node]
k8s-compute-01
k8s-cluster.yml
`inventory/somaz-cluster/group_vars/k8s_cluster/k8s-cluster.yml`
# CNI 플러그인 설정
kube_network_plugin: cilium
# 클러스터 도메인
cluster_name: somaz-cluster.local
# Cilium을 사용할 경우 kube_owner를 root로 설정해야 한다
# https://kubespray.io/#/docs/CNI/cilium?id=unprivileged-agent-configuration
kube_owner: root
kube_owner: root를 설정하지 않으면 Cilium에서 /opt/cni/bin 권한 에러가 발생한다. 참고: https://github.com/cilium/cilium/issues/23838
addons.yml
`inventory/somaz-cluster/group_vars/k8s_cluster/addons.yml`
# Helm 활성화
helm_enabled: true
# Metrics Server 활성화
metrics_server_enabled: true
# kubectl 플러그인 매니저 Krew 활성화
krew_enabled: true
krew_root_dir: "/usr/local/krew"
containerd.yml — Insecure Registry 설정
`inventory/somaz-cluster/group_vars/all/containerd.yml`
프라이빗 레지스트리(Harbor 등)를 HTTP로 사용하는 경우, containerd에 insecure registry 설정을 추가해야 한다. 이 설정을 kubespray inventory에 넣어두면, 노드 추가나 업그레이드 시에도 자동으로 적용된다.
# Harbor insecure registry 설정
containerd_registries_mirrors:
- prefix: harbor.example.com
mirrors:
- host: http://harbor.example.com
capabilities: ["pull", "resolve", "push"]
skip_verify: true
plain_http: true
# Harbor 인증 정보
containerd_registry_auth:
- registry: harbor.example.com
username: admin
password: your-password
- 이 설정이 적용되면 각 노드의 `/etc/containerd/certs.d/harbor.example.com/hosts.toml` 파일이 자동 생성된다.
주의사항
- 서버에 직접 `hosts.toml` 을 수정하면 kubespray 업그레이드 시 덮어쓸 위험이 있다. 반드시 inventory에서 관리하는 것을 권장한다.
- 업그레이드 후 설정이 유지되었는지 확인하려면 아래 명령어를 사용한다.
ssh somaz@<node-ip> "cat /etc/containerd/certs.d/harbor.example.com/hosts.toml"
- 만약 설정이 날아갔다면 containerd 태그만 재실행하면 복구된다.
ansible-playbook -i inventory/somaz-cluster/inventory.ini cluster.yml \
--tags containerd -b --become-user=root
클러스터 배포
Ansible 통신 확인
배포 전에 모든 노드와 Ansible 통신이 가능한지 확인한다.
# 반드시 ~/kubespray 경로에서 실행
ansible all -i inventory/somaz-cluster/inventory.ini -m ping
플레이북 실행
# 포그라운드 실행
ansible-playbook -i inventory/somaz-cluster/inventory.ini cluster.yml --become
# 백그라운드 실행 (시간이 오래 걸리므로 권장)
nohup ansible-playbook -i inventory/somaz-cluster/inventory.ini cluster.yml --become &
# 로그 확인
tail -f nohup.out
kubectl 설정
클러스터 배포 완료 후, Control Plane 노드에서 kubectl을 설정한다.
# kubeconfig 복사
mkdir ~/.kube
sudo cp /etc/kubernetes/admin.conf ~/.kube/config
sudo chown $USER:$USER ~/.kube/config
# 자동완성 및 alias 설정
echo '# kubectl completion and alias' >> ~/.bashrc
echo 'source <(kubectl completion bash)' >> ~/.bashrc
echo 'alias k=kubectl' >> ~/.bashrc
echo 'complete -F __start_kubectl k' >> ~/.bashrc
source ~/.bashrc
설치 확인
k get nodes
NAME STATUS ROLES AGE VERSION
k8s-compute-01 Ready <none> 2d4h v1.34.3
k8s-control-01 Ready control-plane 2d4h v1.34.3
k version
Client Version: v1.34.3
Kustomize Version: v5.6.0
Server Version: v1.34.3
containerd --version
containerd github.com/containerd/containerd/v2 v2.2.1
# Cilium 버전 확인
k get ds cilium -n kube-system -o=jsonpath='{.spec.template.spec.containers[0].image}'
quay.io/cilium/cilium:v1.19.1
Cilium 권한 에러 해결
Cilium을 CNI로 사용할 때 `/opt/cni/bin` 디렉토리 권한 문제로 에러가 발생할 수 있다.
증상
Pod이 Init:CrashLoopBackOff 상태에 빠지거나, Cilium agent 로그에서 권한 관련 에러가 출력된다.
해결 방법 1: 수동 권한 변경
chown -R root:root /opt/cni/bin
해결 방법 2: kubespray 설정에서 근본 해결 (권장)
`inventory/somaz-cluster/group_vars/k8s_cluster/k8s-cluster.yml`
# 기본값
# kube_owner: kube
# Cilium 사용 시 root로 변경
kube_owner: root
Kubespray v2.30.0부터는 이 부분에 Cilium 관련 주석이 공식적으로 추가되었다. # Note: cilium needs to set kube_owner to root 참고: https://github.com/cilium/cilium/issues/23838
Worker Node 추가 (Scale Up)
Step 1. 사전 준비
# /etc/hosts에 새 노드 추가
sudo vi /etc/hosts
10.10.10.19 k8s-compute-02
# SSH 키 복사
ssh-copy-id k8s-compute-02
Step 2. inventory.ini 수정
[kube_control_plane]
k8s-control-01 ansible_host=10.10.10.17 ip=10.10.10.17 etcd_member_name=etcd1
[etcd:children]
kube_control_plane
[kube_node]
k8s-compute-01
k8s-compute-02 # 새 노드 추가
[add_node]
k8s-compute-02 # scale.yml 대상 지정
Step 3. facts 수집 (중요!)
기존 노드의 facts를 먼저 수집해야 한다. 이 단계를 생략하면 ipwrap 에러가 발생한다.
ansible-playbook -i inventory/somaz-cluster/inventory.ini playbooks/facts.yml --become
이 단계를 생략하면 아래 에러가 발생한다.
AnsibleFilterError: Unrecognized type for ipwrap filter
Step 4. scale.yml 실행
# --limit add_node 으로 새 노드만 대상으로 실행
nohup ansible-playbook -i inventory/somaz-cluster/inventory.ini \
scale.yml --limit add_node --become &
# 로그 확인
tail -f nohup.out
Step 5. 검증 및 정리
k get nodes
NAME STATUS ROLES AGE VERSION
k8s-compute-01 Ready <none> 2d5h v1.34.3
k8s-compute-02 Ready <none> 1m v1.34.3
k8s-control-01 Ready control-plane 2d5h v1.34.3
완료 후 `[add_node]` 섹션을 주석 처리하거나 제거한다.
# [add_node]
# k8s-compute-02 # Already added
Worker Node 제거 (Scale Down)
노드위에 Pod 수와 PDB 영향을 확인한다.
kubectl get pods -A -o wide --field-selector spec.nodeName=k8s-compute-02 --no-headers | wc -l
kubectl get pdb -A
- StatefulSet (DB 등) 이 그 노드에 있으면 데이터 이동/PV 재할당 계획 먼저
- `replica=1` Deployment 가 있으면 옮길 곳 검토 (다른 노드 capacity)
cd ~/kubespray
source venv/bin/activate
ansible-playbook -i inventory/somaz-cluster/inventory.ini \
remove-node.yml \
-b --extra-vars='node=k8s-compute-02' \
--extra-vars reset_nodes=true
- 완료 후 `inventory.ini` 에서 해당 노드를 제거한다.
playbook 이 수행하는 것은 아래와 같다.
1. `kubectl drain k8s-compute-02 --ignore-daemonsets --delete-emptydir-data` — Pod evict
2. `kubectl delete node k8s-compute-02`
3. `reset_nodes=true` 일 때 — 그 노드의 kubelet/CNI/etcd-data/containerd 데이터 정리 (재합류 가능 상태로 초기화)
- `reset_nodes=false` 로 호출하면 cluster 측 cleanup 만 함 (노드의 디스크는 그대로).
- 노드를 곧장 다른 용도로 재사용하지 않을 거면 `true` 권장.
inventory를 정리한다.
vi ~/kubespray/inventory/somaz-cluster/inventory.ini
# [kube_node] 에서 k8s-compute-02 라인 삭제
아래와 같이 검증한다.
kubectl get nodes
# 제거된 노드가 더 이상 안 보여야 함
# (옵션) 남은 worker 에 Pod 가 자동 재스케줄됐는지
kubectl get pods -A -o wide --field-selector status.phase=Pending
# Pending 이 길게 남아있으면 — capacity/affinity 문제
Control Plane Node 추가 (HA 전환)
단일 Control Plane으로 운영 중이던 클러스터에 control-02, control-03을 추가하여 HA 구성으로 전환한다. Worker 추가와는 절차가 다르므로 주의가 필요하다.
공식 가이드: Adding control plane nodes
kubespray/docs/operations/nodes.md at master · kubernetes-sigs/kubespray
Deploy a Production Ready Kubernetes Cluster. Contribute to kubernetes-sigs/kubespray development by creating an account on GitHub.
github.com
Worker 추가와의 차이점
| 항목 | Worker 추가 | Control Plane 추가 |
| 실행 playbook | scale.yml | cluster.yml |
| --limit 옵션 | --limit add_node | 사용하지 않음 (전체 대상) |
| [add_node] 그룹 | 사용 | 사용하지 않음 |
| etcd 영향 | 없음 | member join + cert 재발급 |
| 후속 작업 | 없음 | 모든 worker의 nginx-proxy 재시작 필요 |
| 소요 시간 | 30~60분 | 60~90분 |
`cluster.yml`을 사용하는 이유는 `etcd member join`, 인증서 재발급, `nginx-proxy upstream` 갱신 등이 클러스터 전체에 걸쳐 일어나기 때문이다. `scale.yml` 은 worker 확장 전용이므로 control-plane 추가에는 사용할 수 없다.
etcd Quorum 주의사항
etcd는 합의 알고리즘(Raft) 기반으로 동작하므로 홀수 멤버 + quorum 유지가 필수다.
- 1 → 3으로 한 번에 늘리기: OK
- 1 → 2로 늘리기: ❌ split-brain 위험 (짝수)
- 3 → 5로 늘리기: OK
따라서 control-02와 control-03을 동일한 cluster.yml 실행 안에서 함께 추가해야 한다.
운영 클러스터에서 실행 중 기존 etcd가 일시적으로 재시작될 수 있다. 짧은 시간 동안 API 호출이 실패할 수 있으므로 비업무 시간대에 진행을 권장한다. 실행 전 반드시 etcd 스냅샷 백업을 수행한다.
노드 구성
| 역할 | 호스트명 | IP |
| Control Plane + etcd (기존) | k8s-control-01 | 10.10.10.17 |
| Control Plane + etcd (신규) | k8s-control-02 | 10.10.10.24 |
| Control Plane + etcd (신규) | k8s-control-03 | 10.10.10.25 |
Step 1. 사전 준비
Worker 추가와 동일하게 `/etc/hosts` 매핑과 SSH 키 등록을 진행한다.
# /etc/hosts에 새 노드 추가
sudo vi /etc/hosts
10.10.10.24 k8s-control-02
10.10.10.25 k8s-control-03
# SSH 키 복사
ssh-copy-id k8s-control-02
ssh-copy-id k8s-control-03
# 접속 확인
ssh k8s-control-02
ssh k8s-control-03
Step 2. etcd 백업 (필수)
`cluster.yml` 실행 중 기존 etcd 멤버가 재시작되므로 사전 백업은 필수다.
sudo ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/ssl/etcd/ssl/ca.pem \
--cert=/etc/ssl/etcd/ssl/admin-k8s-control-01.pem \
--key=/etc/ssl/etcd/ssl/admin-k8s-control-01-key.pem \
snapshot save /tmp/etcd-snapshot-$(date +%Y%m%d-%H%M%S).db
Step 3. inventory.ini 수정
`[kube_control_plane]` 그룹에 반드시 끝에 append 한다.
[kube_control_plane]
k8s-control-01 ansible_host=10.10.10.17 ip=10.10.10.17 etcd_member_name=etcd1
k8s-control-02 ansible_host=10.10.10.24 ip=10.10.10.24 etcd_member_name=etcd2
k8s-control-03 ansible_host=10.10.10.25 ip=10.10.10.25 etcd_member_name=etcd3
[etcd:children]
kube_control_plane
[kube_node]
k8s-compute-01
k8s-compute-02
k8s-compute-03
첫 번째 위치의 노드(k8s-control-01)는 primary로 취급되므로 앞에 끼워넣으면 클러스터가 깨질 수 있다. Kubespray 공식 문서에서도 "always append"를 명시하고 있다.
[add_node] 그룹은 worker 흐름 전용이므로 control-plane 추가에는 사용하지 않는다.
Step 4. facts 수집
Worker 추가 시와 동일한 이유(`ipwrap` 에러 방지)로 control-plane 추가에서도 facts 수집을 먼저 수행한다.
ansible-playbook -i inventory/somaz-cluster/inventory.ini playbooks/facts.yml --become
Step 5. cluster.yml 실행
`scale.yml` 이 아닌 `cluster.yml` 을 `--limit` 없이 실행한다.
nohup ansible-playbook -i inventory/somaz-cluster/inventory.ini \
cluster.yml --become &
# 로그 확인
tail -f nohup.out
- 소요 시간은 약 60~90분이다.
Step 6. 모든 Worker의 nginx-proxy 재시작 (필수)
Kubespray의 표준 구성은 각 worker에 nginx-proxy 컨테이너를 띄워 `localhost:6443` 을 control-plane 멤버들로 분배한다(별도 외부 LB 불필요). 새로 추가된 control-plane을 worker가 인식하려면 nginx-proxy 재시작이 필요하다.
# 모든 worker에서 nginx-proxy 재시작 (ansible 한 줄 버전)
ansible -i inventory/somaz-cluster/inventory.ini kube_node -b -m shell \
-a 'crictl ps | grep nginx-proxy | awk "{print \$1}" | xargs -r crictl stop'
# kubelet이 static pod를 자동 재기동함
nginx-proxy의 upstream 목록은 kubespray가 [kube_control_plane] 멤버 기준으로 템플릿 렌더하여 `/etc/nginx/nginx.conf`에 박는다. 컨테이너 재시작 = 새 멤버 인식. kubelet의 static pod 재기동이라 별도 `kubectl apply` 불필요.
Step 7. 검증
7-1. 노드 상태
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# k8s-control-01 Ready control-plane 2d6h v1.34.3
# k8s-control-02 Ready control-plane 5m v1.34.3
# k8s-control-03 Ready control-plane 5m v1.34.3
# k8s-compute-01 Ready <none> 2d6h v1.34.3
# k8s-compute-02 Ready <none> 1h v1.34.3
# k8s-compute-03 Ready <none> 1h v1.34.3
ROLES 컬럼이 <none>으로 남는다면 inventory의 [kube_control_plane] 매핑이나 `cluster.yml` 실행 로그를 점검한다.
7-2. etcd 멤버 및 Quorum
# control-01에서 etcd member list 확인
sudo ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/ssl/etcd/ssl/ca.pem \
--cert=/etc/ssl/etcd/ssl/admin-k8s-control-01.pem \
--key=/etc/ssl/etcd/ssl/admin-k8s-control-01-key.pem \
member list
# 3개 멤버(etcd1, etcd2, etcd3)가 모두 started 상태여야 함
# endpoint status 확인 (LEADER + RAFT INDEX)
sudo ETCDCTL_API=3 etcdctl \
--endpoints=https://10.10.10.17:2379,https://10.10.10.24:2379,https://10.10.10.25:2379 \
--cacert=/etc/ssl/etcd/ssl/ca.pem \
--cert=/etc/ssl/etcd/ssl/admin-k8s-control-01.pem \
--key=/etc/ssl/etcd/ssl/admin-k8s-control-01-key.pem \
endpoint status --cluster -w table
- 한 멤버가 `IS LEADER=true`, 나머지 둘은 `false`. `RAFT INDEX` 는 거의 동일해야 한다.
7-3. Control Plane Static Pod 분산 확인
kubectl get pods -n kube-system -o wide | grep -E "kube-apiserver|kube-controller|kube-scheduler|etcd"
- kube-apiserver, kube-controller-manager, kube-scheduler, etcd 각 컴포넌트가 control-01/02/03 세 노드에 모두 분산되어 있어야 한다.
7-4. nginx-proxy Upstream 검증
각 worker의 nginx-proxy가 세 control-plane endpoint 모두를 upstream으로 가지고 있는지 확인한다.
# nginx 설정의 upstream 블록 확인
ansible -i inventory/somaz-cluster/inventory.ini kube_node -b -m shell -a '
ID=$(crictl ps -q --name nginx-proxy | head -n1)
echo "--- $(hostname) ---"
crictl exec "$ID" cat /etc/nginx/nginx.conf | grep -E "server .*:6443"
'
# 기대 출력 (각 worker마다):
# --- k8s-compute-01 ---
# server 10.10.10.17:6443;
# server 10.10.10.24:6443;
# server 10.10.10.25:6443;
3줄이 모두 출력되어야 한다. 하나만 보이는 worker가 있다면 해당 노드의 nginx-proxy만 다시 재시작한다.
ssh somaz@<문제-노드> 'sudo crictl ps -q --name nginx-proxy | xargs sudo crictl stop'
7-5. localhost:6443 Health 확인
ansible -i inventory/somaz-cluster/inventory.ini kube_node -b -m shell \
-a 'curl -kfsS --max-time 3 https://127.0.0.1:6443/healthz; echo'
# 각 노드에서 ok 출력
Step 8. (선택) HA Failover 동작 확인
운영 환경에서 fault injection을 통해 HA 동작을 검증한다.
# control-01의 kube-apiserver 잠시 정지
sudo systemctl stop kubelet # 또는 /etc/kubernetes/manifests/kube-apiserver.yaml 임시 이동
# worker에서 다른 control-plane으로 자동 fallback 되는지 확인
kubectl get nodes # 정상 응답이 와야 함
# 복구
sudo systemctl start kubelet
운영 클러스터에서는 비업무 시간대에 수행하고, 의도된 fault injection임을 팀에 사전 공지한다.
주의사항 정리
| 항목 | 설명 |
| playbook | scale.yml ❌ → cluster.yml ⭕ |
| --limit 옵션 | 사용하지 않음 (전체 노드 대상) |
| inventory 순서 | 기존 노드 뒤에 append (앞 끼워넣기 금지) |
| etcd 멤버 수 | 홀수 유지 (1 → 3 한번에 추가) |
| etcd 백업 | 실행 전 반드시 스냅샷 생성 |
| facts.yml | cluster.yml 전에 먼저 실행 (ipwrap 에러 방지) |
| nginx-proxy 재시작 | 모든 worker에서 필수 (upstream 갱신) |
| 실행 시간 | 60~90분, nohup 권장 |
Control Node 제거
etcd quorum 보호가 최우선이다.
3 → 1 (한 번에 두 개 제거) 또는 3 → 2 (한 개 제거) 중 어느 케이스든
- 3 → 2: 짝수 quorum. 1 노드만 더 죽으면 cluster read-only. 단기 운영은 가능하지만 즉시 다른 member 추가 또는 3 → 1 로 추가 감축 결정 필요
- 3 → 1: 단일 SPOF 로 회귀. 의도된 다운스케일이면 OK
- 한 번에 여러 member 동시 제거 시 quorum 잃을 위험 → 반드시 한 번에 한 노드씩
1. etcd 백업 (필수)
ssh somaz@<server_ip>
sudo ETCDCTL_API=3 etcdctl snapshot save /tmp/etcd-pre-remove-$(date +%Y%m%d-%H%M%S).db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/ssl/etcd/ssl/ca.pem \
--cert=/etc/ssl/etcd/ssl/admin-k8s-control-01.pem \
--key=/etc/ssl/etcd/ssl/admin-k8s-control-01-key.pem
# 워크스테이션으로 복사
scp somaz@<server_ip>:/tmp/etcd-pre-remove-*.db ./backup/
2. etcd member 수동 제거
sudo ETCDCTL_API=3 etcdctl member list -w table \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/ssl/etcd/ssl/ca.pem \
--cert=/etc/ssl/etcd/ssl/admin-k8s-control-01.pem \
--key=/etc/ssl/etcd/ssl/admin-k8s-control-01-key.pem
# 제거 대상 (예: etcd3 = f14b6f49ec2e7af4)
sudo ETCDCTL_API=3 etcdctl member remove f14b6f49ec2e7af4 \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/ssl/etcd/ssl/ca.pem \
--cert=/etc/ssl/etcd/ssl/admin-k8s-control-01.pem \
--key=/etc/ssl/etcd/ssl/admin-k8s-control-01-key.pem
# 확인 — 2 member 만 남아야 함
sudo ETCDCTL_API=3 etcdctl member list -w table ...
- 이 시점에서 cluster 는 2-member quorum 으로 운영 중. write 가능하지만 1 member 더 잃으면 read-only.
3. remove-node.yml 실행
cd ~/kubespray
source venv/bin/activate
ansible-playbook -i inventory/somaz-cluster/inventory.ini \
remove-node.yml \
-b --extra-vars='node=k8s-control-03' \
--extra-vars reset_nodes=true
playbook 이 수행하는 것:
- `kubectl drain k8s-control-03 --ignore-daemonsets --delete-emptydir-data`
- `kubectl delete node k8s-control-03`
- 대상 노드의 kubelet / CNI / etcd-data / containerd 정리
etcd member 제거 (Step 2) 를 건너뛰고 `remove-node.yml` 만 실행하면, etcd 클러스터에 stale member 가 남아 quorum 계산이 어긋날 수 있다. 반드시 etcd 먼저 실행한다.
4. inventory 정리
vi ~/kubespray/inventory/somaz-cluster/inventory.ini
[kube_control_plane]
k8s-control-01 ansible_host=10.10.10.17 ip=10.10.10.17 etcd_member_name=etcd1
k8s-control-02 ansible_host=10.10.10.24 ip=10.10.10.24 etcd_member_name=etcd2
# k8s-control-03 라인 삭제 — etcd_member_name=etcd3 까지 같이
5. nginx-proxy 재시작 (필수)
남은 worker 의 nginx-proxy 가 옛 upstream (사라진 control-03 포함) 을 들고 있다. 재시작으로 갱신한다.
ansible -i inventory/somaz-cluster/inventory.ini kube_node -b -m shell \
-a 'crictl ps | grep nginx-proxy | awk "{print \$1}" | xargs -r crictl stop'
6. 검증
kubectl get nodes
# k8s-control-03 가 더 이상 안 보여야 함
# etcd 멤버 - 2 개로 정리됨
sudo ETCDCTL_API=3 etcdctl member list -w table ...
# nginx-proxy upstream — 남은 두 endpoint 만 나와야 함
ansible -i inventory/somaz-cluster/inventory.ini kube_node -b -m shell -a '
ID=$(crictl ps -q --name nginx-proxy | head -n1)
echo "--- $(hostname) ---"
crictl exec "$ID" cat /etc/nginx/nginx.conf | grep -E "server .*:6443"
'
# 기대: server 10.10.10.17:6443; / server 10.10.10.24:6443; 두 줄만
트러블 슈팅
etcd member 가 remove 됐는데 list 에 stale 로 남음
# Force list/health 재계산
sudo systemctl restart etcd # 남은 한 member 에서 — quorum 영향 주의
또는
# 명시적으로 health endpoint 갱신
sudo ETCDCTL_API=3 etcdctl endpoint health --cluster ...
drain 이 PDB 위반으로 멈춤
- error when evicting pods: Cannot evict pod as it would violate the pod's disruption budget.
원인
- PDB `minAvailable=N` 인데 다른 replica 가 모두 다른 노드에서 NotReady
- replica=1 + PDB 설정
해결
- 다른 노드의 같은 Pod replica health 확인 후 회복 (보통 옳은 길)
- 임시로 PDB 완화 후 재시도 (자체 책임)
- `--disable-eviction` 으로 강제 drain — Pod 직접 delete (마지막 수단)
실무 운영 팁
인벤토리 로컬 백업
Control Plane의 inventory를 로컬에 백업해두면, 서버 장애 시 복구에 유용하다.
scp -r somaz@10.10.10.17:~/kubespray/inventory/somaz-cluster \
~/my-project/kubespray/inventory-somaz-cluster
버전 확인 스크립트
클러스터 상태를 빠르게 확인할 수 있는 스크립트를 만들어두면 편리하다.
#!/bin/bash
echo "=== K8s Version ==="
kubectl get nodes -o wide
echo ""
echo "=== Kubespray Version ==="
cd ~/kubespray && git describe --tags
echo ""
echo "=== Containerd Version ==="
containerd --version
echo ""
echo "=== Cilium Version ==="
kubectl get ds cilium -n kube-system -o=jsonpath='{.spec.template.spec.containers[0].image}'
echo ""
echo ""
echo "=== Certificate Expiration ==="
sudo kubeadm certs check-expiration 2>/dev/null | head -15
주의사항 정리
| 항목 | 설명 |
| venv | kubespray의 Python venv 활성화를 반드시 확인 (`source venv/bin/activate`) |
| 경로 | 모든 ansible-playbook 명령은 `~/kubespray` 디렉토리에서 실행 |
| facts.yml | 노드 추가 시 반드시 먼저 실행 (ipwrap 에러 방지) |
| kube_owner | Cilium 사용 시 root로 설정 필수 |
| insecure registry | 서버 직접 수정 대신 inventory에서 관리 권장 |
| etcd 백업 | 업그레이드 전 반드시 스냅샷 생성 |
| StatefulSet | drain 시 데이터 확인 필요 |
| Replica | 서비스 무중단을 위해 Deployment replica 2개 이상 권장 |
마무리
Kubespray는 단순 설치 자동화를 넘어 운영 환경 수준의 쿠버네티스 클러스터 구성을 가능하게 해준다. 특히 다음과 같은 경우에 유용하다.
- 온프레미스나 사설 클라우드에서 빠른 배포가 필요할 때
- HA 클러스터, MetalLB/Ingress 구성 자동화가 필요할 때
- Terraform + Ansible 연동을 통한 IaC 기반 인프라 관리
- CI 환경에서 테스트 클러스터 생성 및 제거 반복
Ansible 기반 자동화 → 클러스터 구축 → GitOps 기반 앱 배포라는 흐름을 갖추면 운영 자동화 수준을 한 단계 끌어올릴 수 있다.
다음 포스팅에서는 Kubespray를 이용한 Kubernetes 클러스터 업그레이드 방법을 다룰 예정이다.
Reference
- Kubespray GitHub
- Kubespray Releases
- Kubespray Node Management
- Cilium CNI Issue #23838
- Kubespray Cilium Docs
Somaz | DevOps Engineer | Kubernetes & Cloud Infrastructure Specialist
'Container Orchestration > Kubernetes' 카테고리의 다른 글
| Kubernetes 1.34.x gRPC etcd Warning 버그 상세 분석 (0) | 2026.04.20 |
|---|---|
| Kubernetes 클러스터 업그레이드하기 (kubespray 2026v.) (0) | 2026.04.13 |
| Kubernetes Probe (Liveness, Readiness, Startup) (0) | 2026.04.06 |
| 누가 kubectl edit 했어? — Kubernetes Cluster Drift 감지 도구 직접 만들기 (0) | 2026.03.23 |
| Kubernetes 환경에서 Redis Redlock 구현하기: 분산 락의 완전한 이해 (0) | 2025.12.24 |