:bulb: 운영 중인 Kubernetes 클러스터에 노드가 부족할 때, Kubespray의 scale.yml 플레이북으로 워커 노드를 추가하는 표준 절차와 실전 트러블슈팅을 정리한다.


[01] 환경

항목
Kubespray inventory/mycluster/ 기반
Kubernetes v1.28.0
CRI containerd 1.7.22
CNI Calico
OS Ubuntu 22.04 (Jammy)
Ansible Python venv 환경

1-1. 기존 클러스터 구성

Role Hostname IP
control-plane + etcd k8s-master 192.168.1.91
worker k8s-worker1 192.168.1.92
worker k8s-worker2 192.168.1.93
worker (special HW) node-a 192.168.1.111
worker (special HW) node-b 192.168.1.113

여기에 k8s-worker3 (192.168.1.94)를 추가한다.


[02] 전체 흐름

1
2
3
4
5
6
7
8
9
[1] 인벤토리에 노드 추가
  ↓
[2] SSH 공개키 + NOPASSWD sudo 설정
  ↓
[3] Ansible 연결 테스트 (ping)
  ↓
[4] scale.yml 실행
  ↓
[5] kubectl get nodes로 검증

[03] Step 1 — 인벤토리에 노드 추가

inventory/mycluster/inventory.yamlall.hostschildren.kube_node.hosts 두 곳 모두에 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
all:
  hosts:
    k8s-master:
      ansible_host: 192.168.1.91
    k8s-worker1:
      ansible_host: 192.168.1.92
    k8s-worker2:
      ansible_host: 192.168.1.93
    k8s-worker3:                 # ← 추가
      ansible_host: 192.168.1.94 # ← 추가

  children:
    kube_node:
      hosts:
        k8s-worker1:
        k8s-worker2:
        k8s-worker3:              # ← 추가

:warning: hosts에만 추가하고 kube_node.hosts에 빠뜨리면, 호스트 정의는 있지만 그룹에 소속되지 않아 scale.yml 대상에서 제외된다.


[04] Step 2 — SSH 공개키 + NOPASSWD sudo

Kubespray는 become: yes로 sudo를 사용하므로, 두 가지 모두 설정해야 한다.

4-1. SSH 공개키 등록

1
ssh-copy-id user@192.168.1.94

확인:

1
2
ssh 192.168.1.94 'hostname'
# 패스워드 묻지 않고 hostname 출력되면 OK

4-2. NOPASSWD sudo 등록

새 서버에서:

1
2
3
ssh 192.168.1.94 \
  "echo 'user ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/user && \
   sudo chmod 440 /etc/sudoers.d/user"

확인:

1
2
ssh 192.168.1.94 'sudo -n whoami'
# → root 출력되면 OK

:bulb: sudo -n은 패스워드 프롬프트를 띄우지 않는 옵션이다. a password is required 메시지가 뜨면 NOPASSWD 설정이 안 된 것이다.


[05] Step 3 — Ansible 연결 테스트

플레이북 실행 전에 반드시 ping으로 기본 연결성을 확인한다.

1
2
cd ~/kubespray/kubespray
../venv/bin/ansible -i inventory/mycluster/inventory.yaml k8s-worker3 -m ping

정상 응답:

1
2
3
4
5
6
7
k8s-worker3 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

[06] Step 4 — scale.yml 실행

1
2
3
4
5
6
7
LOG=~/kubespray/logs/scale-$(date +%Y%m%d-%H%M%S).log
../venv/bin/ansible-playbook \
  -i inventory/mycluster/inventory.yaml \
  scale.yml \
  -b \
  --limit=k8s-worker3 \
  > "$LOG" 2>&1 &
옵션 설명
-b become (sudo)
--limit=k8s-worker3 새 노드에만 플레이북 적용 → 기존 워커에 부하/변경 없음
> "$LOG" 2>&1 & 백그라운드 실행, 로그 파일 저장

진행 모니터링:

1
tail -f $LOG

[07] 트러블슈팅 — APT 버전 404 에러

7-1. 에러 메시지

1
2
3
4
5
6
TASK [kubernetes/preinstall : Install packages requirements] ***
fatal: [k8s-worker3]: FAILED! =>
  "msg": "'/usr/bin/apt-get -y ...
    install 'apt-transport-https=2.4.13' ...' failed:
    E: Failed to fetch .../apt-transport-https_2.4.13_all.deb
    404 Not Found"

7-2. 원인

Ansible fact cache가 오래된 패키지 버전을 기억하고 있었다.

ansible.cfg에 fact caching 설정이 있다:

1
2
3
4
[defaults]
fact_caching = jsonfile
fact_caching_connection = /tmp
fact_caching_timeout = 86400

이전에 수집된 apt-transport-https=2.4.13 버전 정보가 캐시에 남아 있었지만, Ubuntu 저장소에서는 이미 2.4.14로 교체되어 404 Not Found가 발생했다.

1
2
3
4
# 새 서버에서 확인하면 2.4.14만 존재
apt-cache madison apt-transport-https | head -3
# apt-transport-https | 2.4.14 | ... jammy-updates
# apt-transport-https |  2.4.5 | ... jammy

7-3. 해결

1
2
3
4
5
# 제어 노드에서 — fact cache 삭제
rm -rf /tmp/k8s-worker3

# 새 서버에서 — APT 메타데이터 강제 갱신
ssh 192.168.1.94 'sudo apt-get update'

이후 동일한 scale.yml 명령을 재실행하면 한 번에 완료된다.

:bulb: 핵심 교훈: “한 번 성공한 절차가 왜 갑자기 실패하지?”의 답은 대부분 캐시가 본 세상이 지금 세상과 다르기 때문이다. Ansible fact cache, APT metadata, Docker image digest 모두 마찬가지다.


[08] Step 5 — 결과 검증

8-1. Ansible PLAY RECAP

1
2
PLAY RECAP ************************************************************
k8s-worker3 : ok=490  changed=77  unreachable=0  failed=0  skipped=772

failed=0 확인.

8-2. Kubernetes 노드 상태

1
kubectl get nodes -o wide
1
2
3
4
5
6
7
NAME          STATUS   ROLES           AGE    VERSION   INTERNAL-IP
k8s-master    Ready    control-plane   132d   v1.28.0   192.168.1.91
k8s-worker1   Ready    <none>          132d   v1.28.0   192.168.1.92
k8s-worker2   Ready    <none>          132d   v1.28.0   192.168.1.93
k8s-worker3   Ready    <none>          42s    v1.28.0   192.168.1.94  ← 신규
node-a        Ready    <none>          23h    v1.28.0   192.168.1.111
node-b        Ready    <none>          23h    v1.28.0   192.168.1.113

새 노드가 Ready로 정상 조인되었다. Calico CNI도 배포되어 Pod 스케줄링이 가능하다.

8-3. (선택) 테스트 Pod 스케줄링

1
2
3
4
5
6
7
8
kubectl run test-nginx --image=nginx:alpine \
  --overrides='{"spec":{"nodeSelector":{"kubernetes.io/hostname":"k8s-worker3"}}}'

kubectl get pod test-nginx -o wide
# NODE가 k8s-worker3인지 확인

# 테스트 후 정리
kubectl delete pod test-nginx

[09] 참고 명령어 모음

1
2
3
4
5
6
7
8
9
10
11
12
13
# 제어 노드에서
../venv/bin/ansible -i inventory/mycluster/inventory.yaml <node> -m ping
../venv/bin/ansible-playbook -i inventory/mycluster/inventory.yaml scale.yml -b --limit=<node>
rm -rf /tmp/<node>                  # fact cache 삭제

# 대상 서버에서
sudo apt-get update                 # APT 메타데이터 갱신
sudo -n whoami                      # NOPASSWD sudo 확인

# 마스터에서
kubectl get nodes -o wide
kubectl describe node <node>
kubectl get pods -A -o wide --field-selector spec.nodeName=<node>

[10] 정리

단계 작업 핵심 포인트
1 인벤토리 등록 hosts + kube_node.hosts 두 곳 모두
2 SSH / sudo ssh-copy-id + NOPASSWD 필수
3 연결 테스트 ansible -m ping으로 사전 확인
4 scale.yml --limit=<node>로 새 노드에만 적용
5 검증 PLAY RECAP + kubectl get nodes 2단 확인
(트러블) APT 404 fact cache 삭제 + apt-get update