[Kubernetes] Cluster: Kubespray를 이용해 클러스터 구성하기 - 6.0.2. 노드 관리 - remove-node.yml
서종호(가시다)님의 On-Premise K8s Hands-on Study 5주차 학습 내용을 기반으로 합니다.
TL;DR
이번 글에서는 Kubespray의 노드 제거 플레이북 remove-node.yml의 전체 흐름을 분석한다.
- 목적: 특정 노드를 안전하게 클러스터에서 제거
- 플레이 구성: 검증 → 확인 → 팩트 수집 → 노드 초기화(drain/etcd/reset) → 클러스터에서 삭제
- 핵심 특징: Graceful 제거 (Drain → etcd 제거 → Reset → Delete)
- 안전 장치: 노드 지정 검증, 사용자 확인 프롬프트
remove-node.yml 전체 흐름
remove-node.yml은 Kubespray의 노드 제거 플레이북으로, 특정 노드를 안전하게 클러스터에서 제거한다.

1. 노드 지정 검증 (assert)
↓
2. 공통 작업 (boilerplate.yml)
↓
3. 사용자 확인 (pause)
↓
4. 팩트 수집 (internal_facts.yml)
↓
5. 노드 초기화
├── pre_remove (drain, cordon)
├── remove-etcd-node (etcd 멤버인 경우)
└── reset (kubeadm reset)
↓
6. 클러스터에서 삭제 (kubectl delete node)
단계별 그룹핑
위 흐름을 목적별로 그룹핑하면 다음과 같다:
| 단계 | 목적 | 플레이/태스크 |
|---|---|---|
| 사전 검증 | 노드 명시적 지정 확인 | assert |
| 공통 설정 | 변수, 핸들러 로딩 | boilerplate |
| 사용자 확인 | 실수 방지 | pause + fail |
| 정보 수집 | 노드 상태 파악 | internal_facts |
| Graceful 제거 | Pod 이동, 스케줄링 중지 | pre_remove (drain, cordon) |
| etcd 제거 | etcd 클러스터 정합성 유지 | remove-etcd-node |
| 노드 초기화 | kubeadm reset, 파일 삭제 | reset |
| 메타데이터 삭제 | K8s 리소스 정리 | post-remove (kubectl delete node) |
플레이북 구조
전체 코드
remove-node.yml 전체 코드 (클릭하여 펼치기)
---
- name: Validate nodes for removal
hosts: localhost
gather_facts: false
become: false
tasks:
- name: Assert that nodes are specified for removal
assert:
that:
- node is defined
- node | length > 0
msg: "No nodes specified for removal. The `node` variable must be set explicitly."
- name: Common tasks for every playbooks
import_playbook: boilerplate.yml
- name: Confirm node removal
hosts: "{{ node | default('this_is_unreachable') }}"
gather_facts: false
tasks:
- name: Confirm Execution
pause:
prompt: "Are you sure you want to delete nodes state? Type 'yes' to delete nodes."
register: pause_result
run_once: true
when:
- not (skip_confirmation | default(false) | bool)
- name: Fail if user does not confirm deletion
fail:
msg: "Delete nodes confirmation failed"
when: pause_result.user_input | default('yes') != 'yes'
- name: Gather facts
import_playbook: internal_facts.yml
when: reset_nodes | default(True) | bool
- name: Reset node
hosts: "{{ node | default('this_is_unreachable') }}"
gather_facts: false
environment: "{{ proxy_disable_env }}"
pre_tasks:
- name: Gather information about installed services
service_facts:
when: reset_nodes | default(True) | bool
roles:
- { role: kubespray_defaults, when: reset_nodes | default(True) | bool }
- { role: remove_node/pre_remove, tags: pre-remove }
- role: remove-node/remove-etcd-node
when: "'etcd' in group_names"
- { role: reset, tags: reset, when: reset_nodes | default(True) | bool }
# Currently cannot remove first control plane node or first etcd node
- name: Post node removal
hosts: "{{ node | default('this_is_unreachable') }}"
gather_facts: false
environment: "{{ proxy_disable_env }}"
roles:
- { role: kubespray_defaults, when: reset_nodes | default(True) | bool }
- { role: remove-node/post-remove, tags: post-remove }
플레이별 역할
| 순서 | 플레이 | 대상 호스트 | 주요 역할 |
|---|---|---|---|
| 1 | Validate nodes | localhost |
노드 지정 검증 |
| 2 | Common tasks | - | 공통 설정, 변수 검증 |
| 3 | Confirm removal | `` | 사용자 확인 |
| 4 | Gather facts | - | 시스템 정보 수집 |
| 5 | Reset node | `` | drain, etcd 제거, kubeadm reset |
| 6 | Post removal | `` | kubectl delete node |
reset_nodes 변수
remove-node.yml에서 가장 중요한 변수 중 하나인 reset_nodes는 노드 자체를 초기화할 것인지 결정한다.
| 값 | 의미 | 수행 작업 |
|---|---|---|
true (기본값) |
노드 초기화 O | reset 롤 실행 |
false |
노드 초기화 X | 클러스터에서만 제거 (drain → delete node) |
이 변수에 따라 팩트 수집, 서비스 정보 수집, reset 롤 실행 여부가 결정된다. 자세한 사용 사례는 실행 방법 - 노드 초기화 없이 클러스터에서만 제거 섹션을 참고한다.
단계별 분석
1. 노드 지정 검증
제거할 노드를 지정했는지 검증한다.
- name: Validate nodes for removal
hosts: localhost
gather_facts: false
become: false
tasks:
- name: Assert that nodes are specified for removal
assert:
that:
- node is defined
- node | length > 0
msg: "No nodes specified for removal. The `node` variable must be set explicitly."
| 요소 | 설명 |
|---|---|
hosts: localhost |
로컬에서 검증 실행 |
become: false |
root 권한 불필요 |
assert |
that 조건이 모두 참이어야 통과, 하나라도 거짓이면 msg와 함께 즉시 실패 |
node is defined |
node 변수가 정의되어 있는지 확인 |
node \| length > 0 |
node 변수가 빈 문자열이 아닌지 확인 |
안전 장치: node 변수가 정의되지 않았거나 빈 값이면 플레이북이 즉시 실패한다.
# 올바른 실행
ansible-playbook remove-node.yml -e node=k8s-node5
# 실패하는 실행
ansible-playbook remove-node.yml # assert 실패
2. 공통 작업 (boilerplate)
- name: Common tasks for every playbooks
import_playbook: boilerplate.yml
모든 Kubespray 플레이북의 공통 시작점으로, 변수와 핸들러를 로딩한다.
3. 사용자 확인
노드 제거는 클러스터에 영향을 주는 중요한 작업이므로, 실수 방지를 위해 사용자 확인을 받는다.
- name: Confirm node removal
hosts: "{{ node | default('this_is_unreachable') }}"
tasks:
- name: Confirm Execution
pause:
prompt: "Are you sure you want to delete nodes state? Type 'yes' to delete nodes."
register: pause_result
run_once: true
when:
- not (skip_confirmation | default(false) | bool)
- name: Fail if user does not confirm deletion
fail:
msg: "Delete nodes confirmation failed"
when: pause_result.user_input | default('yes') != 'yes'
| 요소 | 설명 |
|---|---|
pause |
사용자 입력 대기 |
run_once: true |
여러 노드 지정해도 한 번만 확인. 확인 대상은 “삭제 의도”이지 “각 노드별 삭제”가 아니므로, 반복 확인은 불필요 |
skip_confirmation |
true로 설정 시 확인 건너뛰기 |
동적 호스트 지정
hosts: "{{ node | default('this_is_unreachable') }}"
| 요소 | 설명 |
|---|---|
| `` | 사용자가 -e node=<노드명>으로 지정 |
default('this_is_unreachable') |
미지정 시 존재하지 않는 호스트로 설정 |
Fail-safe: node 변수가 정의되지 않으면 this_is_unreachable이라는 존재하지 않는 호스트를 대상으로 하여 아무 작업도 수행하지 않는다.
자동화 지원
# 인터랙티브 실행 (프롬프트 표시)
ansible-playbook remove-node.yml -e node=k8s-node5
# 자동화 실행 (프롬프트 건너뛰기)
ansible-playbook remove-node.yml -e node=k8s-node5 -e skip_confirmation=true
4. 팩트 수집
reset_nodes=true일 때만 팩트를 수집한다.
- name: Gather facts
import_playbook: internal_facts.yml
when: reset_nodes | default(True) | bool
왜 reset_nodes=true일 때만 필요한가?: reset 롤에서 kubeadm reset, 서비스 중지, CNI 플러그인 삭제 등을 수행하려면 노드에 어떤 컴포넌트가 설치되어 있는지 알아야 한다. 반면 reset_nodes=false일 때는 drain과 delete node가 Control Plane에서 kubectl로 실행되므로, 노드의 상세 시스템 정보가 필요 없다.
5. 노드 초기화 (Reset node)
- name: Reset node
hosts: "{{ node | default('this_is_unreachable') }}"
pre_tasks:
- name: Gather information about installed services
service_facts:
when: reset_nodes | default(True) | bool
roles:
- { role: kubespray_defaults, when: reset_nodes | default(True) | bool }
- { role: remove_node/pre_remove, tags: pre-remove }
- role: remove-node/remove-etcd-node
when: "'etcd' in group_names"
- { role: reset, tags: reset, when: reset_nodes | default(True) | bool }
pre_tasks
pre_tasks는 Ansible 플레이북에서 roles가 실행되기 전에 먼저 수행되는 태스크다. 여기서는 service_facts 모듈을 사용해 노드에 설치된 서비스 정보를 수집한다.
pre_tasks:
- name: Gather information about installed services
service_facts:
when: reset_nodes | default(True) | bool
| 요소 | 설명 |
|---|---|
pre_tasks |
roles 실행 전 먼저 수행되는 태스크 블록 |
service_facts |
시스템에 설치된 서비스 목록과 상태 수집 |
when: reset_nodes |
노드 초기화 시에만 실행 |
서비스 정보는 이후 reset 롤에서 kubelet, containerd 등의 서비스를 중지하고 삭제할 때, 해당 서비스가 실제로 설치되어 있는지 확인하기 위해 필요하다.
pre_tasks에 대한 자세한 설명은 Ansible 플레이북 특수 섹션을 참고한다.
5.1 pre_remove 롤
노드 제거 전 워크로드를 안전하게 이동시킨다.
# roles/remove_node/pre_remove/defaults/main.yml
allow_ungraceful_removal: false
drain_grace_period: 300
drain_timeout: 360s
drain_retries: 3
drain_retry_delay_seconds: 10
| 변수 | 기본값 | 설명 |
|---|---|---|
allow_ungraceful_removal |
false |
drain 실패 시 강제 제거 허용 여부 |
drain_grace_period |
300 |
Pod 종료 유예 시간 (초) |
drain_timeout |
360s |
drain 타임아웃 |
drain_retries |
3 |
drain 재시도 횟수 |
drain_retry_delay_seconds |
10 |
재시도 간격 (초) |
주요 태스크:
- kubectl drain: DaemonSet 제외하고 모든 Pod 축출
- kubectl cordon: 새 Pod 스케줄링 방지
- kubelet 중지: 노드의 kubelet 서비스 중지
5.2 remove-etcd-node 롤 (조건부)
- role: remove-node/remove-etcd-node
when: "'etcd' in group_names"
| 조건 | 설명 |
|---|---|
'etcd' in group_names |
해당 노드가 etcd 그룹에 속해 있는 경우에만 실행 |
etcd 클러스터에서 멤버를 안전하게 제거한다. Worker 노드 제거 시에는 이 롤이 실행되지 않는다.
5.3 reset 롤
kubeadm reset을 수행하고 노드의 K8s 관련 파일을 정리한다.
6. 클러스터에서 삭제 (Post removal)
- name: Post node removal
hosts: "{{ node | default('this_is_unreachable') }}"
roles:
- { role: kubespray_defaults, when: reset_nodes | default(True) | bool }
- { role: remove-node/post-remove, tags: post-remove }
post-remove 롤
Kubernetes 클러스터에서 노드 메타데이터를 삭제한다.
- name: Remove-node | Delete node
command: "{{ kubectl }} delete node {{ kube_override_hostname | default(inventory_hostname) }}"
delegate_to: "{{ groups['kube_control_plane'] | first }}"
retries: "{{ delete_node_retries }}"
delay: "{{ delete_node_delay_seconds }}"
register: result
until: result is not failed
| 요소 | 설명 |
|---|---|
kubectl delete node |
K8s API에서 노드 리소스 삭제 |
delegate_to |
Control Plane에서 실행 |
retries / until |
실패 시 재시도 |
제한사항
플레이북 주석에 명시된 제한사항을 확인한다. 첫 번째 Control Plane 노드와 첫 번째 etcd 노드는 이 플레이북으로 제거할 수 없다.
# Currently cannot remove first control plane node or first etcd node
| 제거 불가 노드 | 이유 |
|---|---|
| 첫 번째 Control Plane | 클러스터 초기화 시 기준 노드 |
| 첫 번째 etcd 노드 | etcd 클러스터의 리더 역할 가능 |
첫 번째 Control Plane 노드
- Kubespray에서 클러스터 초기화 시 첫 번째 Control Plane이
kubeadm init을 실행하는 기준 노드 - Kubespray의 많은 태스크들이
groups['kube_control_plane'] | first로 첫 번째 Control Plane에서 실행됨 (kubectl 명령, 인증서 생성 등) - 이 노드가 없어지면 Kubespray가 의존하는 많은 작업들이 실패
첫 번째 etcd 노드
- etcd 클러스터 구성 시 첫 번째 노드가 초기 클러스터를 생성
- etcd는 Raft 합의 알고리즘을 사용하므로, 리더 노드를 갑자기 제거하면 클러스터 안정성에 문제
- Kubespray가 etcd 관련 작업 시 첫 번째 노드를 참조
이 노드들을 제거하려면 수동 작업이 필요하다.
안전한 제거를 위한 설계
remove-node.yml의 주요 설계 특징은 다음과 같다. 이러한 특징들이 노드를 “우아하게” 제거한다는 느낌을 준다:
1. 엄격한 검증
- name: Assert that nodes are specified for removal
assert:
that:
- node is defined
- node | length > 0
msg: "No nodes specified for removal. The `node` variable must be set explicitly."
- 명시적 노드 지정 강제:
-e node=<노드명>없이는 실행 불가 - 실수 방지: 전체 클러스터에 대한 의도치 않은 실행 차단
2. 사용자 확인
- name: Confirm Execution
pause:
prompt: "Are you sure you want to delete nodes state? Type 'yes' to delete nodes."
- 인터랙티브 확인: “yes” 입력 필수
- 자동화 지원:
-e skip_confirmation=true로 CI/CD에서 사용 가능
3. 안전한 제거 순서
1. Drain (Pod 이동)
↓
2. Cordon (스케줄링 중지)
↓
3. etcd 멤버 제거 (해당 시)
↓
4. kubeadm reset
↓
5. kubectl delete node
- Graceful Shutdown: Pod들이 다른 노드로 안전하게 이동
- PDB 준수: PodDisruptionBudget 정책을 존중
- etcd 정합성: etcd 멤버 제거 후 노드 초기화
Kubespray를 사용하지 않더라도, 노드를 수동으로 제거할 때 이 순서를 참고하면 좋다.
4. 재시도 로직
drain_retries: 3
drain_retry_delay_seconds: 10
- 일시적 실패 대응: drain 실패 시 자동 재시도
- 타임아웃 설정: 무한 대기 방지
5. 조건부 실행
- { role: reset, tags: reset, when: reset_nodes | default(True) | bool }
- 선택적 초기화:
reset_nodes: false로 노드 초기화 건너뛰기 가능 - etcd 조건부: etcd 멤버인 경우에만 etcd 제거 실행
reset_nodes=false 사용 사례
앞서 설명한 reset_nodes 변수의 기본값은 true다. 따라서 명시적으로 -e reset_nodes=false를 지정하지 않으면 항상 노드 초기화가 수행된다. 그렇다면 언제 굳이 false로 지정할까?
왜 reset_nodes=false를 사용하는가?
다음과 같은 상황에서는 reset_nodes=false가 유용하다:
| 상황 | 설명 |
|---|---|
| 노드 접근 불가 | 노드가 다운되었거나 네트워크 단절로 SSH 접속이 불가능한 경우. kubeadm reset을 실행할 수 없으므로 클러스터 메타데이터만 정리 |
| 노드 폐기 | 노드를 재사용하지 않고 폐기할 예정인 경우. 초기화 작업이 불필요 |
| 물리적 제거 후 정리 | 노드가 이미 물리적으로 제거된 후, 클러스터에 남아있는 메타데이터만 삭제 |
| 빠른 제거 필요 | 긴급 상황에서 kubeadm reset 없이 빠르게 클러스터에서 제거해야 하는 경우 |
reset_nodes=false일 때 수행되는 작업
reset_nodes=false로 설정하면 일부 작업이 스킵된다:
| 작업 | reset_nodes=true | reset_nodes=false |
|---|---|---|
| 노드 지정 검증 (assert) | O | O |
| 공통 작업 (boilerplate) | O | O |
| 사용자 확인 (pause) | O | O |
| 팩트 수집 (internal_facts) | O | X |
| 서비스 정보 수집 (service_facts) | O | X |
| pre_remove (drain, cordon) | O | O |
| remove-etcd-node (etcd 멤버인 경우) | O | O |
| reset (kubeadm reset) | O | X |
| post-remove (kubectl delete node) | O | O |
핵심 차이: reset_nodes=false는 노드 자체를 초기화하지 않고, 클러스터 관점에서의 제거만 수행한다.
reset_nodes=true: drain → etcd 제거 → kubeadm reset → kubectl delete node
reset_nodes=false: drain → etcd 제거 → (X) → kubectl delete node
주의사항
reset_nodes=false로 제거한 노드를 나중에 다시 클러스터에 추가하려면, 수동으로 kubeadm reset을 실행해야 한다. 노드에 남아있는 K8s 관련 파일과 설정이 충돌을 일으킬 수 있기 때문이다.
# 노드에서 수동으로 실행
kubeadm reset -f
rm -rf /etc/cni/net.d
rm -rf ~/.kube
실행 방법
기본 실행
ansible-playbook -i inventory/mycluster/inventory.ini remove-node.yml -e node=k8s-node5
여러 노드 제거
ansible-playbook remove-node.yml -e "node=k8s-node5,k8s-node6"
자동화 (확인 건너뛰기)
ansible-playbook remove-node.yml -e node=k8s-node5 -e skip_confirmation=true
노드 초기화 없이 클러스터에서만 제거
ansible-playbook remove-node.yml -e node=k8s-node5 -e reset_nodes=false
정리
이번 글에서 remove-node.yml의 전체 흐름을 파악했다. 핵심은 엄격한 검증과 Graceful한 제거 과정이다.
주요 변수
| 변수 | 기본값 | 설명 |
|---|---|---|
node |
(필수) | 제거할 노드 이름 |
skip_confirmation |
false |
확인 프롬프트 건너뛰기 |
reset_nodes |
true |
노드 초기화 수행 여부 |
allow_ungraceful_removal |
false |
drain 실패 시 강제 제거 |
drain_timeout |
360s |
drain 타임아웃 |
drain_retries |
3 |
drain 재시도 횟수 |
scale.yml과의 비교
scale.yml이 노드를 추가한다면, remove-node.yml은 노드를 제거한다.
| 항목 | scale.yml | remove-node.yml |
|---|---|---|
| 목적 | 기존 클러스터에 노드 추가 | 기존 클러스터에서 노드 제거 |
| 주요 대상 | kube_node |
-e node=<노드명> |
| etcd 영향 | etcd 클러스터 변경 안 함 | etcd 멤버 제거 (해당 시) |
| kubeadm 명령 | kubeadm join |
kubeadm reset |
| 사용자 확인 | 없음 | pause 프롬프트 |
Kubespray와 kubeadm
remove-node.yml 분석에서도 확인할 수 있듯이, Kubespray는 결국 kubeadm을 사용한다.
| Kubespray가 하는 일 | 실제 동작 |
|---|---|
| 노드 초기화 | kubeadm reset |
| 노드 조인 | kubeadm join |
| 클러스터 초기화 | kubeadm init |
주요 특징
| 특징 | 설명 |
|---|---|
| 명시적 노드 지정 | -e node=<노드명> 필수 |
| 사용자 확인 | “yes” 입력 또는 skip_confirmation |
| Graceful 제거 | drain → etcd 제거 → reset → delete |
| 재시도 로직 | drain/delete 실패 시 자동 재시도 |
| 조건부 실행 | etcd 멤버, reset_nodes 등 조건에 따라 선택적 실행 |
댓글남기기