Container Orchestration/Kubernetes

Kubernetes 클러스터 구축하기(kubespray 2026v.)

Somaz 2026. 4. 8. 00:00
728x90
반응형

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 이 수행하는 것: 

  1. `kubectl drain k8s-control-03 --ignore-daemonsets --delete-emptydir-data` 
  2. `kubectl delete node k8s-control-03` 
  3. 대상 노드의 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 설정 

 

 

해결

  1. 다른 노드의 같은 Pod replica health 확인 후 회복 (보통 옳은 길) 
  2. 임시로 PDB 완화 후 재시도 (자체 책임) 
  3. `--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

 

 

 

 

 

Somaz | DevOps Engineer | Kubernetes & Cloud Infrastructure Specialist

728x90
반응형