Kubespray 기반 K8s에서 워커 노드 삭제 & 재추가



대상: Kubespray로 구축/운영 중인 워커 노드(control-plane, etcd 제외)
전제: 운영 중 클러스터, 다운타임 최소화 목표


체크리스트

  1. 사전 점검: 노드 명/파드/스토리지 확인 → 백업/변경창 확보
  2. 드레인 & 제거: kubectl cordon/drainkubectl delete node → (필요 시) CNI 잔여 리소스 정리
  3. Kubespray 정리: remove-node.yml (온라인/오프라인 분기)
  4. 인벤토리 정리: 제거 노드 정보 삭제
  5. 노드 재추가: 신규 VM 준비 → 인벤토리에 추가 → facts.ymlscale.yml --limit=<새노드>
  6. 검증 & 레이블/테인트 복원

0. 변경 전 점검

# 노드/파드 분포
kubectl get nodes -o wide
kubectl get pods -A -o wide --field-selector spec.nodeName=<노드명>

# 로컬 PV / emptyDir 존재 여부와 PDB 확인
kubectl get pv,pvc -A
kubectl get pdb -A

kubectl drainPDB를 존중하여 안전하게 파드를 축출. PDB 때문에 드레인이 막히면, 일시적으로 스케일 아웃/인, PDB 완화 등으로 처리.

PDB (PodDisruptionBudget) 는 쿠버네티스에서 파드 중단 예산이라고 부르는 리소스


📌 정의

  • PDB는 자발적인 중단(eviction) 상황에서 동시에 내려갈 수 있는 파드의 수를 제한하는 정책.
  • 예를 들어 kubectl drain, 노드 업그레이드, 수동 롤링 업데이트 같은 관리자/운영자의 작업이 여기에 해당됨.

📌 왜 필요할까?

어떤 워크로드(특히 ReplicaSet/Deployment, StatefulSet)는 항상 일정 개수 이상의 파드가 살아 있어야 서비스가 끊기지 않음이 보장.

  • 예: DB 프록시, 웹 API 서버, 인그레스 컨트롤러 등.

PDB를 설정해 두면 운영자가 노드를 내릴 때 쿠버네티스가:

  1. PDB 조건을 확인하고,
  2. 동시에 너무 많은 파드가 내려가지 않도록 제어.

📌 사용 예시

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 2       # 항상 최소 2개는 살아 있어야 함
  selector:
    matchLabels:
      app: my-api

위 설정의 의미:

  • app=my-api 라벨을 가진 파드가 총 3개라면, kubectl drain 같은 작업 시 동시에 1개까지만 내려갈 수 있고 최소 2개는 살아 있어야 함.

📌 핵심 요약

  • 대상: 자발적 중단(노드 드레인, 롤링 업데이트 등).
  • 보장: 워크로드의 가용성을 유지하면서 안전하게 파드를 재배치.
  • 설정 단위: minAvailable(최소 유지 수) 또는 maxUnavailable(최대 허용 중단 수).

👉 정리하면,
PDB는 “내 서비스 파드가 최소 몇 개는 항상 살아 있어야 한다” 라는 업타임 안전장치


1. 워커 노드 드레인 & K8s 오브젝트 제거

# 신규 스케줄 방지
kubectl cordon <노드명>

# 안전 축출(DS 무시, emptyDir 데이터 삭제 동의, 고아 파드 강제 시 --force)
kubectl drain <노드명> --ignore-daemonsets --delete-emptydir-data

# 노드 오브젝트 제거
kubectl delete node <노드명>

기본 절차: 드레인 → 노드 삭제.

CNI(예: Calico) 잔여 리소스 정리 (선택)

드물게 노드 오브젝트 삭제 후에도 CNI의 노드/아이피 할당이 남는 경우가 있습니다. Calico 사용 시:

# calicoctl이 구성되어 있다면
calicoctl get nodes -o wide
calicoctl delete node <노드ID or 노드명>

⚠️ 주의: 워커 노드에는 보통 :6443(apiserver)이 없습니다. 프로세스 수동 kill -9로 정리하지 마세요. 꼭 필요하면 systemctl stop kubelet 정도만 일시 사용하고, 정리는 Kubespray로 일원화하는 게 안전합니다.


2. Kubespray로 노드 정리(remove-node.yml)

온라인(SSH 가능) 노드: 원격 접속으로 패키지/네트워크를 깨끗하게 정리.

cd kubespray/
# (권장) facts 캐시 갱신
ansible-playbook -i inventory/<cluster>/hosts.yaml --become --become-user=root playbooks/facts.yml

# 노드 정리(해당 노드로 한정)
ansible-playbook -i inventory/<cluster>/hosts.yaml --become --become-user=root \
  remove-node.yml -e node=<노드명>

오프라인(SSH 불가) 노드: Kubespray가 접근할 수 없으므로 강제 제거 플래그를 사용.

ansible-playbook -i inventory/<cluster>/hosts.yaml --become --become-user=root \
  remove-node.yml -e node=<노드명> -e reset_nodes=false -e allow_ungraceful_removal=true

오프라인 노드 제거 시 reset_nodes=false + allow_ungraceful_removal=true추가해야 실패를 피합니다.


3. 인벤토리에서 항목 삭제

노드가 정상 제거되었으면 Ansible 인벤토리에서 해당 노드 항목을 지움. (예: inventory/<cluster>/hosts.yaml 또는 inventory.ini)


4. 대체/신규 워커 노드 준비

신규 VM(또는 베어메탈)을 준비하고 다음을 만족시켜야 함.

  • (권장) SSH 무비번 접속 가능(컨트롤 노드 → 신규 노드)
  • sudo 권한 부여, 시간 동기화, 네트워크/방화벽 포트 허용
  • (재사용 IP일 때) known_hosts 충돌 제거: ssh-keygen -R <노드IP> ssh-keygen -R <노드호스트명>
  • swap off(Kubespray가 처리하지만, 미리 꺼두면 더 깔끔) sudo swapoff -a sudo sed -ri 's/^([^#].*\sswap\s)/#\1/' /etc/fstab

5. 인벤토리에 신규 노드 추가 (예시)

YAML(권장)

all:
  hosts:
    worker-new:
      ansible_host: 10.10.10.25
      ip: 10.10.10.25
      access_ip: 10.10.10.25
      ansible_user: ubuntu
  children:
    kube_control_plane:
      hosts:
        cp-1: {}
        cp-2: {}
    kube_node:
      hosts:
        worker-1: {}
        worker-2: {}
        worker-new: {}
    etcd:
      hosts:
        cp-1: {}
        cp-2: {}
    k8s_cluster:
      children:
        kube_control_plane: {}
        kube_node: {}

INI(사용 중이라면)

[all]
worker-new ansible_host=10.10.10.25 ip=10.10.10.25 access_ip=10.10.10.25 ansible_user=ubuntu

[kube_control_plane]
cp-1
cp-2

[kube_node]
worker-1
worker-2
worker-new

[etcd]
cp-1
cp-2

[k8s_cluster:children]
kube_control_plane
kube_node

6. facts 갱신 후, 스케일 인(노드 추가)

Kubespray는 --limit로 특정 호스트만 처리할 수 있는데, 그 전에 facts 캐시를 전체 갱신하는 것이 안전.

# 1) facts 캐시 갱신(전체)
ansible-playbook -i inventory/<cluster>/hosts.yaml --become --become-user=root playbooks/facts.yml

# 2) 새 노드에 한정하여 설치/조인
ansible-playbook -i inventory/<cluster>/hosts.yaml --become --become-user=root \
  scale.yml --limit=worker-new

7. 사후 검증 & 라벨/테인트 복원

# 노드 조인/상태
kubectl get nodes -o wide
kubectl describe node worker-new

# (필요 시) 과거 라벨/테인트 복원
kubectl label node worker-new node-role.kubernetes.io/worker='' env=prod
kubectl taint nodes worker-new node.kubernetes.io/purpose=ingress:NoSchedule

8. 장애/에러 트러블슈팅 팁

  • PDB 때문에 drain 실패: 일시 스케일업·PDB 일시 완화·--disable-eviction은 최후의 수단.
  • 노드가 오프라인: remove-node.yml -e reset_nodes=false -e allow_ungraceful_removal=true로 강제 정리.
  • Calico 잔여 엔트리: calicoctl delete node <ID>로 정리.
  • facts 캐시 꼬임/부분 작업 불안: playbooks/facts.yml--limit 작업 재시도.
  • 추가 실패 시 롤백: 해당 노드만 reset.yml --limit=<노드>로 초기화 후 재시도(전체 리셋 아님, 타겟만 한정).

9. 왜 “프로세스 강제 종료”를 권하지 않나?

  • 워커에는 보통 kube-apiserver(:6443)가 없습니다. 임의 kill -9더러운 상태를 남김.
  • Kubespray의 remove-node.yml은 kubelet/런타임/네트워크를 정상 순서로 정리.
    오프라인 노드처럼 접근 불가한 상황만 특수 플래그로 처리.

10. 명령 모음 (복붙용)

# A) 드레인 & 삭제
kubectl cordon <node>
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data
kubectl delete node <node>

# B) (선택) Calico 잔여 정리
calicoctl get nodes -o wide
calicoctl delete node <node>

# C) Kubespray 정리 (온라인)
ansible-playbook -i inventory/<cluster>/hosts.yaml -b playbooks/facts.yml
ansible-playbook -i inventory/<cluster>/hosts.yaml -b remove-node.yml -e node=<node>

#    Kubespray 정리 (오프라인)
ansible-playbook -i inventory/<cluster>/hosts.yaml -b remove-node.yml \
  -e node=<node> -e reset_nodes=false -e allow_ungraceful_removal=true

# D) 신규 노드 추가
ansible-playbook -i inventory/<cluster>/hosts.yaml -b playbooks/facts.yml
ansible-playbook -i inventory/<cluster>/hosts.yaml -b scale.yml --limit=<new-node>

# E) 검증 & 복원
kubectl get nodes -o wide
kubectl label node <new-node> env=prod
kubectl taint nodes <new-node> node.kubernetes.io/purpose=ingress:NoSchedule
ⓒ 2025 엉뚱한 녀석의 블로그 [quirky guy's Blog]. 본문 및 이미지를 무단 복제·배포할 수 없습니다. 공유 시 반드시 원문 링크를 명시해 주세요.
ⓒ 2025 엉뚱한 녀석의 블로그 [quirky guy's Blog]. All rights reserved. Unauthorized copying or redistribution of the text and images is prohibited. When sharing, please include the original source link.

🛠 마지막 수정일: 2025.09.18