들어가며
이 글에서는 kubeadm 기반 Kubernetes 업그레이드를 실습합니다.
목표는 “명령어 나열”보다는, 버전 스큐(Version Skew) 규칙을 지키면서 컨트롤 플레인과 워커 노드를 순서대로 올리는 흐름을 손에 익히는 것입니다.
실습 환경은 로컬에서 재현 가능한 Vagrant 기반(rockylinux + containerd)으로 구성했고, 초기 상태는 Kubernetes v1.32.11입니다. 이후 글에서는 kubeadm 방식으로 v1.33 → v1.34까지 단계적으로 업그레이드해보려 합니다.
전체 흐름 살펴보기
사전 준비(환경 안정화)
- Rocky Linux 10 vagrant 이미지에서 SWAP off 및 swap 파티션 삭제(재부팅 후에도 유지되도록)
- 가상머신 외부 통신(ping 8.8.8.8) 실패 시 라우팅 정리(enp0s9 never-default)
- k8s-ctr / k8s-w1 / k8s-w2 모두 동일 기준으로 정리
모니터링 스택 구축
- kube-prometheus-stack 설치(프로메테우스/그라파나 NodePort 노출)
- 그라파나 대시보드(예: 15661, 15757) import로 기본 관측 화면 구성
샘플 워크로드 배포 및 반복 호출로 기준선 확보
- webpod(traefik/whoami) + curl-pod(netshoot) 배포
- curl 반복 호출로 서비스 연속성 기준선 확보
- watch kubectl get node/pod, crictl ps, kubectl top node 등으로 작업 중 영향도 관측
ETCD 백업(스냅샷)
- etcdctl / etcdutl 설치 후 환경변수 구성
- etcdctl snapshot save로 etcd snapshot 저장 및 상태 확인
Flannel CNI 업그레이드
- 데몬셋 재기동 시간을 줄이기 위해 노드별 이미지 선다운로드
- helm으로 v0.27.3 → v0.27.4 업그레이드 후 DS 롤아웃/통신 영향 확인
[k8s-ctr] Rocky Linux 마이너 업그레이드
- v10.0 → v10.1 업데이트 후 노드 reboot
- 단일 ControlPlane 환경 특성상 재부팅 구간 API 단절/메트릭 수집 공백 확인
[k8s-ctr] Kubernetes 업그레이드(1.32 → 1.33 → 1.34)
- kubeadm upgrade plan으로 대상 버전/변경 컴포넌트 확인
- kubeadm upgrade apply로 컨트롤 플레인 업그레이드
- 이후 kubelet/kubectl 업그레이드 + kubelet 재시작
- 1.34 업그레이드 시 etcd 업그레이드 포함(3.6.5-0), pause 이미지(3.10.1) 반영 절차 포함
워커 노드 업그레이드 및 운영 절차 검증
- 업그레이드 전 파드 배치/STS/PV·PVC 여부 확인
- kubectl drain 시 DaemonSet/emptyDir/PDB 제약 확인 후 처리
- 워커별 kubeadm upgrade node → kubelet/kubectl 업그레이드 → kubelet 재시작
- kubectl uncordon으로 정상화 후 파드 재배치 확인
노드 제거(reset) 후 재-join 및 실습 환경 정리
- Control에서 drain → delete node
- 대상 노드에서 kubeadm reset -f 후 CNI/iptables/잔여 디렉터리 정리
- 필요 시 재-join 검증
- 최종적으로 vagrant destroy -f && rm -rf .vagrant로 실습 환경 삭제
- 이번 실습은 “사전 안정화 → 관측 환경 구축 → 네트워크/OS/쿠버네티스 업그레이드 → 워커 운영 절차(드레인/복구) → 노드 초기화/재조인 → 정리” 순서로 진행했습니다.
K8S Upgrade by kubeadm
이번 장에서는 kubeadm 기반 업그레이드를 진행하기 전에, “왜 이런 순서로 해야 하는지(규칙)”와 “실습 환경은 어떻게 구성했는지”를 먼저 정리합니다.
실제 업그레이드 명령(컨트롤 플레인/워커 노드별 단계)은 다음 장에서 본격적으로 들어갑니다.
K8S Version Skew Policy
쿠버네티스 업그레이드는 구성요소 간 허용되는 버전 차이(Version Skew)를 지켜야 합니다.
- 릴리즈 주기
- 대략 1년에 3개의 마이너 버전이 나오고,
- 일반적으로 최근 3개 마이너 버전의 release branch(패치)만 유지됩니다.
- 구성요소별 버전 스큐(요약)
- kube-apiserver(HA): 신규 버전 + 직전 마이너 혼재 가능 (예: 1.32 + 1.31)
- kubelet
- apiserver보다 새 버전 불가
- apiserver보다 최대 3개 마이너까지 구버전 허용
- 예) apiserver 1.32 → kubelet 1.32/1.31/1.30/1.29 가능
- 예) apiserver(HA) 1.32+1.31 혼재 시 → kubelet은 1.31/1.30/1.29 (1.32는 아직 1.31 apiserver가 남아있어서 “앞서가면” 안됨)
- kube-controller-manager / kube-scheduler / cloud-controller-manager
- apiserver보다 새 버전 불가
- apiserver보다 최대 1개 마이너 구버전 허용
- HA에서 apiserver 혼재 중이면 하위(apiserver 최소 버전) 기준으로 맞추는 게 안전
- kube-proxy
- apiserver보다 새 버전 불가
- apiserver보다 최대 3개 마이너 구버전 허용
- kubelet과도 ±3 마이너 범위
- kubectl
- apiserver 기준 ±1 마이너 범위
결론적으로, “업그레이드 순서”는 스큐 정책이 강제합니다.
그래서 보통 Control Plane 먼저, 그 다음 Worker 순으로 진행합니다.
업그레이드 방식 소개 (In-place vs Blue-Green)
- In-place(Incremental)
- Version Skew를 활용해서 Control Plane을 점진적으로 올리고,
허용 스큐 범위 내에서 Worker 업그레이드를 잠시 미루는 전략 - 장점: 인프라 추가 비용 적음 / 운영 구조 변화 최소
- 단점: 단계별 반복 작업(1.32→1.33→1.34…)이 길어질 수 있음
- Version Skew를 활용해서 Control Plane을 점진적으로 올리고,
- Blue-Green
- “기존 클러스터(Blue)” + “신규 클러스터(Green)”를 병렬로 만들고 트래픽을 전환
- 장점: 롤백/전환이 상대적으로 쉬움
- 단점: 비용/운영 복잡도 증가, 외부 연동(인증/OIDC/DNS/CI/CD 등) 고려 많음
업그레이드 전 체크 포인트
실제로 문제가 터지는 지점은 보통 “kubeadm”이 아니라 주변부입니다.
- Addon/App 호환성
- CNI/CSI, CoreDNS, kube-proxy 등과
- 애플리케이션이 사용하는 API 리소스 버전(deprecations 포함) 호환 확인
- OS / CRI 호환성
- OS(Kernel) ↔ containerd/runc ↔ kubelet/kubeadm 연쇄 호환 체크
- ETCD
- 업그레이드 전 백업 필수
- 업그레이드해도 ETCD 데이터 포맷은 하위 호환 유지(그래도 백업은 해야 함)
- 가용성(무중단)
- Worker drain 시 서비스 영향 최소화를 위해
- PDB, replica 수, topology 분산 등 “퇴거(Evict)” 시나리오 점검
온프레미스 환경에서 In-place 계획 잡는 흐름
예시 환경(가정): k8s 1.28, CP 3대, DP 3대, HW LB, 목표는 1.32
큰 흐름은 아래처럼 잡습니다.
- 호환성 검토 (Kernel / containerd / CNI / App API)
- 업그레이드 방식 결정 (in-place vs blue-green)
- 계획 수립 (버전 단위로 CP→DP 반복)
- 사전 준비 (모니터링, ETCD 백업, CNI 등 선조치)
- CP 순차 업그레이드 (버전 단위 반복)
- DP 순차 업그레이드 (drain/upgrade/uncordon)
- 동작 점검 (중간/최종)
이번 실습 환경: Vagrant로 재현 가능한 kubeadm 클러스터
이번 글의 실습은 “현업과 동일한 HA 구성”보다는, 업그레이드 흐름에 집중한 구성입니다.
- 구성: 1 Control Plane + 2 Worker
- 네트워크: 192.168.10.100(ctr), 192.168.10.101(w1), 192.168.10.102(w2)
- 프로비저닝 스크립트 역할
- init_cfg.sh : 공통 초기 세팅(시간대/NTP, 방화벽/SELinux, swap 제거, 커널 파라미터, containerd/kubeadm 설치 등)
- k8s-ctr.sh : kubeadm init + Flannel/Helm/편의도구 설치
- k8s-w.sh : kubeadm join
스크립트는 실습에서 “설치/초기화 출력이 보이도록” 구성되어 있어,
어떤 단계에서 무엇이 설치되고 변경되는지 추적하기 편합니다.
실습 버전(초기/목표)
실습 초기 버전: Kubernetes v1.32.11
| Rocky Linux | 10.0-1.6 | kernel 6.12 |
| containerd | v2.1.5 | k8s 1.32~1.35 지원 |
| runc | v1.3.3 | (필요 시 추가 확인) |
| kubelet/kubeadm/kubectl | v1.32.11 | |
| helm | v3.18.6 | k8s 1.30~1.33 |
| flannel cni | v0.27.3 | k8s 1.28+ |
목표 버전: Kubernetes v1.34.11 (+ 도전: 1.35)
| Rocky Linux | 10.1-1.4 | kernel 6.12 |
| kubelet/kubeadm/kubectl | v1.34.x | |
| helm | v3.19.x | k8s 1.31~1.34 |
| flannel cni | v0.28.0 | 업그레이드 문서 참고 |
실습 환경 배포
mkdir k8s-upgrade
cd k8s-upgrade
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-upgrade/Vagrantfile
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-upgrade/init_cfg.sh
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-upgrade/k8s-ctr.sh
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-upgrade/k8s-w.sh
vagrant up
vagrant status
업그레이드 절차
이 글에서는 “큰 절차”까지만 잡고, 다음 장에서 버전별로 실제 수행합니다.
- 사전 준비 & etcd 백업
- CNI 업그레이드
- OS 업그레이드
- containerd 업그레이드
- kubeadm/kubelet/kubectl 업그레이드 (마이너 버전 단위 반복)
- admin용 kubeconfig 갱신
- worker 노드 drain → 업그레이드 → uncordon (마이너 버전 단위 반복)
사전 준비
이번 실습은 “업그레이드”보다 먼저, 실패 지점(네트워크/노드/etcd)에서 바로 원인 확인할 수 있게 바닥을 깔아두는 단계입니다.
즉, (1) 노드 기본 이슈 제거 → (2) 관측/샘플 워크로드 준비 → (3) ETCD 백업 순서로 갑니다.
(TS) Rocky Linux 10 Vagrant 이미지: SWAP off 이슈 정리
Rocky 10 이미지에서 swapoff만으로는 재부팅 후 swap 파티션이 다시 살아나는 케이스가 있어, 실습 스크립트(init_cfg.sh)에 swap 파티션 삭제까지 포함했습니다.
init_cfg.sh 반영 내용
echo "[TASK 3] Disable and turn off SWAP & Delete swap partitions"
swapoff -a
sed -i '/swap/d' /etc/fstab
sfdisk --delete /dev/sda 2
partprobe /dev/sda >/dev/null 2>&1
확인/재현/복구 포인트
- swapon --show, lsblk, free -h로 swap 상태 확인
- swap 파티션이 남아있다면:
- 즉시 비활성화: swapoff /dev/sda2
- 완전 삭제: fdisk /dev/sda로 2번 파티션 삭제 후 partprobe /dev/sda
- k8s-ctr, k8s-w1도 동일하게 swap 파티션 삭제해두기
잔여 작업 메모: 삭제된 파티션을 활용하고 싶다면(캐시/임시용), 현재 파티션 번호 순서상 “합치기”는 어렵고 별도 파티션/마운트로 쓰는 식이 현실적.
(TS) VM 외부 통신(ping 8.8.8.8) 실패 시: default route 제거
실습 NIC(enp0s9) 쪽에 gateway가 잡히면서 라우팅이 꼬이는 케이스를 막기 위해, init_cfg.sh에 아래를 반영했습니다.
echo "[TASK 6] Delete default routing - enp0s9 NIC"
nmcli connection modify enp0s9 ipv4.never-default yes
nmcli connection up enp0s9
확인:
- /etc/NetworkManager/system-connections/enp0s9.nmconnection
- never-default=true 들어갔는지
- 불필요한 gateway=...가 제거됐는지
모니터링: kube-prometheus-stack 설치
업그레이드 중 “진짜 끊겼는지/잠깐 흔들린 건지”를 체감하려고 Prometheus/Grafana를 NodePort로 노출합니다.
핵심 구성
- Prometheus NodePort: 30001
- Grafana NodePort: 30002 (admin / prom-operator)
- scrape/evaluation interval: 20s
- kubeProxy 모니터링은 끔(실습 단순화)
추가:
- Grafana Dashboard Import: 15661, 15757
- 데이터소스는 Prometheus 선택 후 Import
4) 샘플 애플리케이션 배포 + 반복 호출 준비
업그레이드 중 영향도 확인을 위해,
- 서비스(webpod)를 계속 호출하는 curl-pod
- 서비스 endpoint가 바뀌는 걸 체감 가능한 replica 2개짜리 간단 앱
을 올립니다.
주의(중요): podAntiAffinity의 selector 값은 실제 라벨(app=webpod) 과 일치해야 동작합니다. (원문 예시의 sample-app는 webpod로 맞추는 게 자연스럽습니다.)
동작 확인
kubectl get deploy,svc,ep webpod -owide
# 반복 호출(둘 중 하나)
kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---"; sleep 1; done'
SVCIP=$(kubectl get svc webpod -o jsonpath='{.spec.clusterIP}')
while true; do curl -s $SVCIP | grep Hostname; sleep 1; done
업그레이드 중 “계속 켜둘” 모니터링 명령(터미널 여러 개)
핵심은 아래 3개만 살아 있어도 됩니다.
- 반복 호출(서비스 체감)
- 노드 상태 변화
- 시스템 파드 변화
# (1) 서비스 반복 호출
SVCIP=$(kubectl get svc webpod -o jsonpath='{.spec.clusterIP}')
while true; do curl -s $SVCIP | grep Hostname; sleep 1; done
# (2) 노드
watch -d kubectl get node
# (3) 전체 파드
watch -d kubectl get pod -A -owide
(옵션) kubectl top node, 각 노드 crictl ps watch는 실습 성향에 따라 추가.
ETCD 백업(snapshot)
업그레이드 실습에서 “최소 안전장치”는 etcd snapshot 1개입니다.
요약 흐름
- etcd 컨테이너 버전 확인
- 동일 버전의 etcdctl/etcdutl 설치
- ETCDCTL_* 환경변수 설정 후 상태 확인
- snapshot 저장 + 결과 확인
백업 예시
mkdir /backup
etcdctl snapshot save /backup/etcd-snapshot-$(date +%F).db
etcdutl snapshot status /backup/etcd-snapshot-YYYY-MM-DD.db
Flannel CNI 업그레이드 (v0.27.3 → v0.27.4)
Kubernetes 본 업그레이드(1.32→1.33…) 전에, CNI를 먼저 작은 단위로 올려서
- 데몬셋 롤링 중 서비스 영향이 어떤지,
- “실습 모니터링/반복 호출”이 제대로 경보 역할을 하는지
를 먼저 확인합니다.
고려사항
- Flannel은 DaemonSet이라 노드별로 순차 재기동됨
- 재기동 순간에 네트워크 흔들림이 생길 수 있으니
- 이미지 사전 pull
- 반복 호출 + kube-flannel 파드 watch
를 같이 걸어두는 게 핵심
이미지 사전 다운로드(전 노드)
업그레이드 시 필요한 이미지(Flannel 본체 + CNI plugin) 를 미리 pull 해서, 재기동 시간을 줄입니다.
crictl pull ghcr.io/flannel-io/flannel:v0.27.4
crictl pull ghcr.io/flannel-io/flannel-cni-plugin:v1.8.0-flannel1
Helm으로 업그레이드
작업 전 확인
kubectl get ds -n kube-flannel -owide
kubectl get pod -n kube-flannel -o yaml | grep -i image: | sort | uniq
values 파일
podCidr: "10.244.0.0/16"
flannel:
cniBinDir: "/opt/cni/bin"
cniConfDir: "/etc/cni/net.d"
args:
- "--ip-masq"
- "--kube-subnet-mgr"
- "--iface=enp0s9"
backend: "vxlan"
image:
tag: v0.27.4
업그레이드 + 롤아웃 확인
helm upgrade flannel flannel/flannel -n kube-flannel -f flannel.yaml --version 0.27.4
kubectl -n kube-flannel rollout status ds/kube-flannel-ds
업그레이드 후 확인(이미지/파드)
kubectl get ds -n kube-flannel -owide
kubectl get pod -n kube-flannel -o yaml | grep -i image: | sort | uniq
crictl ps
[k8s-ctr] Rocky Linux OS 마이너 버전 Upgrade (10.0 → 10.1)
Kubernetes 업그레이드 전에 OS(커널)부터 먼저 올려두는 단계입니다. 단, ControlPlane 노드가 단일(혹은 HA 미구성)이라면 재부팅 동안 API가 끊기는 건 정상입니다.
고려 사항
- ControlPlane가 HA(3대 이상)라면:
1대 재부팅 시에도 API / etcd quorum / LB 헬스체크가 문제 없는지 먼저 확인 - 단일 파드가 특정 노드에 “박혀” 있을 때(예: nodeName 지정): 재부팅 시 영향 큼
- 실습에서 Prometheus/Grafana는 PV/PVC를 안 쓰는 형태라면 재기동 영향은 상대적으로 작지만, 메트릭 공백은 발생할 수 있음
[k8s-ctr] 작업 전 정보 확인 (reboot 영향도 파악)
# k8s-ctr에 올라간 파드 확인
kubectl get pod -A -owide |grep k8s-ctr
# (선택) CoreDNS 분산/집중 시나리오 생각해보기
# - 만약 coredns가 1개고 그게 k8s-ctr에만 떠있으면, 재부팅 동안 DNS 영향 가능
kubectl scale deployment -n kube-system coredns --replicas 1
kubectl get pod -n kube-system -owide | grep coredns
# 원복
kubectl scale deployment -n kube-system coredns --replicas 2
kubectl get pod -n kube-system -owide | grep coredns
[k8s-ctr] Rocky Linux 10.0 → 10.1 업그레이드 & 재부팅
재부팅 동안에도 서비스 흐름을 보려고, 호출은 k8s-w1에서 돌려두는 방식을 사용합니다(단일 CP reboot 대비).
# 모니터링(권장): [k8s-w1]에서 서비스 반복 호출
SVCIP=<IP 직접 입력>
while true; do curl -s $SVCIP | grep Hostname; sleep 1; done
# 현재 OS/커널 확인
rpm -aq | grep release
uname -r
containerd가 같이 올라가는 걸 피하고 싶으면 versionlock으로 고정
rpm -q containerd.io
dnf install -y 'dnf-command(versionlock)'
dnf versionlock add containerd.io
dnf versionlock list
업데이트 & 재부팅
dnf -y update
reboot
부팅 구간 관찰
ping 192.168.10.100
부팅 후 확인
rpm -aq | grep release # rocky-release-10.1 ...
uname -r # 6.12.0-124 ...
kubectl get pod -A -owide |grep k8s-ctr
[k8s-ctr] kubeadm / kubelet / kubectl 업그레이드 (v1.32.11 → v1.33.7)
이 단계는 ControlPlane 컴포넌트(Static Pod)와 애드온(CoreDNS, kube-proxy) 이 먼저 올라가고,
노드 버전 표시는 kubelet 기준이라 “당장 노드 버전이 안 바뀌어 보이는 상태”가 정상입니다.
1) 고려 사항
순서 고정
- kubeadm 업그레이드
- kubeadm upgrade apply
- kubelet/kubectl 업그레이드
- kubelet 재시작
containerd는 재시작하지 않음(소켓/CRI/API 동일, 런타임 유지)
- 단, pause 이미지 정책을 바꾸거나 런타임 설정 변경이 있으면 containerd 재시작 고려
kubeadm 신규 버전 설치 & kubeadm upgrade plan
repo를 v1.33로 변경
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.33/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.33/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
dnf makecache
설치 가능 버전 확인 후 kubeadm만 먼저 업그레이드
dnf list --showduplicates kubeadm --disableexcludes=kubernetes
dnf install -y --disableexcludes=kubernetes kubeadm-1.33.7-150500.1.1
kubeadm version -o yaml
kubeadm upgrade plan
upgrade plan에서 확인 포인트
- Target: v1.33.7
- kube-proxy / CoreDNS가 같이 올라갈 예정(모든 노드 영향 가능)
kubeadm upgrade apply v1.33.7 실행
모니터링(권장)
# [k8s-w1] 서비스 반복 호출(권장)
SVCIP=<IP 직접 입력>
while true; do curl -s $SVCIP | grep Hostname; sleep 1; done
# 다른 터미널
watch -d kubectl get node
watch -d kubectl get pod -n kube-system
watch -d "etcdctl member list -w table"
이미지 사전 다운로드(시간 단축)
kubeadm config images pull
# [k8s-w1/w2] kube-proxy/coredns는 all node에 pull 권장
crictl pull registry.k8s.io/kube-proxy:v1.33.7
crictl pull registry.k8s.io/coredns/coredns:v1.12.0
업그레이드 실행
kubeadm upgrade apply v1.33.7
# y 입력
업그레이드 직후 상태 확인 (노드 버전이 안 바뀌는 이유)
kubectl get node -owide
kubectl describe node k8s-ctr | grep 'Kubelet Version:'
# 이미지 확인
crictl images
# static pod manifest 이미지 확인
ls -l /etc/kubernetes/manifests/
cat /etc/kubernetes/manifests/*.yaml | grep -i image:
정상 해석:
- control-plane static pod 이미지: v1.33.7로 바뀜
- 노드 VERSION은 kubelet 기준이라 아직 v1.32.11로 보일 수 있음
kubelet / kubectl 업그레이드 & kubelet 재시작
dnf list --showduplicates kubelet --disableexcludes=kubernetes
dnf list --showduplicates kubectl --disableexcludes=kubernetes
dnf install -y --disableexcludes=kubernetes \
kubelet-1.33.7-150500.1.1 kubectl-1.33.7-150500.1.1
kubectl version --client=true
kubelet --version
systemctl daemon-reload
systemctl restart kubelet
재시작 직후 확인
kubectl get nodes -o wide
kubectl describe node k8s-ctr | grep 'Kubelet Version:'
Prometheus 알람: KubeVersionMismatch
ControlPlane은 올라갔는데 Worker kubelet이 아직이면, 한동안 아래 알람이 뜨는 게 자연스럽습니다.
- 의미: 클러스터 내 Kubernetes 컴포넌트 minor 버전이 2개 이상 공존
- 룰은 patch를 잘라서 minor 기준으로 비교하도록 되어 있음(label_replace)
정리:
- 지금 상태: k8s-ctr만 kubelet이 1.33, k8s-w1/w2는 1.32 → 알람 발생 가능
- 워커 노드 업그레이드까지 끝내고 15분 정도 지나면 정상으로 돌아오는지 확인하면 됩니다.
[k8s-ctr] kubeadm / kubelet / kubectl 업그레이드 (v1.33.7 → v1.34.3)
이번 단계는 1.33 → 1.34 업그레이드이며, 5장(1.32→1.33)과 달리 etcd 업그레이드(3.5 → 3.6)가 포함되어 시간이 조금 더 걸립니다.
kubeadm 신규 버전 설치 & kubeadm upgrade plan (kubelet/kubectl은 아직)
# repo 수정 : 1.33 -> 1.34
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.34/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.34/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
dnf makecache
# 설치 가능 버전 확인
dnf list --showduplicates kubeadm --disableexcludes=kubernetes
# kubeadm만 먼저 설치
dnf install -y --disableexcludes=kubernetes kubeadm-1.34.3-150500.1.1
which kubeadm && kubeadm version -o yaml
# 업그레이드 계획 확인
kubeadm upgrade plan
plan에서 확인 포인트
- Target: v1.34.3
- CoreDNS: v1.12.0 → v1.12.1
- etcd: 3.5.24-0 → 3.6.5-0 (여기서 1.32→1.33 대비 체감 시간 증가)
kubeadm upgrade apply 실행 (etcd 업그레이드 포함)
- 업그레이드 중 영향도 관찰용 모니터링
# [k8s-w1] 서비스 반복 호출(권장)
SVCIP=<IP 직접 입력>
while true; do curl -s $SVCIP | grep Hostname; sleep 1; done
# 다른 터미널들
watch -d kubectl get node
watch -d kubectl get pod -n kube-system
watch -d "etcdctl member list -w table"
이미지 사전 다운로드 (시간 단축)
kubeadm config images pull
# [k8s-w1/w2] all node pull 권장(특히 kube-proxy/coredns/pause)
crictl pull registry.k8s.io/kube-proxy:v1.34.3
crictl pull registry.k8s.io/coredns/coredns:v1.12.1
crictl pull registry.k8s.io/pause:3.10.1
crictl images | egrep 'kube-proxy|coredns|pause'
업그레이드 실행(대화형 생략)
kubeadm upgrade apply v1.34.3 --yes
로그에서 Preparing for "etcd" upgrade → Component "etcd" upgraded successfully! 구간이 추가로 보이는 게 정상입니다.
업그레이드 직후 상태 확인 (노드 VERSION이 아직 1.33.7인 이유)
ControlPlane static pod는 1.34.3으로 올라가도, kubectl get node의 VERSION은 kubelet 기준이라 kubelet을 올리기 전까지는 예전 버전으로 보입니다.
kubectl get node -owide
kubectl describe node k8s-ctr | grep 'Kubelet Version:'
# 이미지 / manifest 반영 확인
crictl images | egrep 'kube-apiserver|controller-manager|scheduler|etcd|coredns|pause|kube-proxy'
ls -l /etc/kubernetes/manifests/
cat /etc/kubernetes/manifests/*.yaml | grep -i image:
kubelet / kubectl 업그레이드 + kubelet 재시작
dnf list --showduplicates kubelet --disableexcludes=kubernetes
dnf list --showduplicates kubectl --disableexcludes=kubernetes
dnf install -y --disableexcludes=kubernetes \
kubelet-1.34.3-150500.1.1 kubectl-1.34.3-150500.1.1
kubectl version --client=true
kubelet --version
systemctl daemon-reload
systemctl restart kubelet
systemctl daemon-reload vs daemon-reexec
- daemon-reload: unit 파일 변경 사항을 다시 읽음(일반적으로 이거면 충분)
- daemon-reexec: systemd 프로세스 자체를 재실행(드물게 필요, systemd 바이너리/환경 문제일 때)
업그레이드 반영 확인
kubectl get nodes -o wide
kubectl describe node k8s-ctr | grep 'Kubelet Version:'
admin kubeconfig 갱신
ls -l ~/.kube/config
ls -l /etc/kubernetes/admin.conf
yes | cp /etc/kubernetes/admin.conf ~/.kube/config ; echo
chown $(id -u):$(id -g) ~/.kube/config
kubectl config rename-context "kubernetes-admin@kubernetes" "HomeLab"
kubens default
(TS) pause(3.10 → 3.10.1) 신규 이미지 적용
핵심은 “새로 만들어지는 파드부터” 적용된다는 점입니다(기존 파드는 유지).
- kubelet이 실제로 사용하는 pause 확인은 ps의 --pod-infra-container-image가 제일 직관적입니다.
- kubeadm 환경에서는 /var/lib/kubelet/kubeadm-flags.env를 수정하는 방식이 깔끔합니다.
# kubeadm이 보는 pause 이미지
kubeadm config images list | grep pause
# 로컬 이미지 확인
crictl images | grep pause
# kubelet 실행 인자에서 현재 적용중인 pause 확인
ps -ef | grep -i pause | grep kubelet | grep pod-infra
kubelet 플래그 변경(권장 포인트)
cat /var/lib/kubelet/kubeadm-flags.env
# ... --pod-infra-container-image=registry.k8s.io/pause:3.10
vi /var/lib/kubelet/kubeadm-flags.env
# ... --pod-infra-container-image=registry.k8s.io/pause:3.10.1
systemctl restart kubelet
systemctl status kubelet --no-pager
참고: /etc/containerd/config.toml의 sandbox(pause)도 바꿨다면 containerd 재시작이 필요하지만, kubelet이 --pod-infra-container-image로 이미지를 지정하는 구성이라면 kubelet 플래그가 우선인 경우가 많습니다. (즉, “kubelet 플래그 수정 + kubelet 재시작 + 파드 재생성”이 실무적으로 가장 확실)
적용 확인(파드 재생성 후)
kubectl delete pod curl-pod
# 다시 생성(예시)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: curl-pod
labels:
app: curl
spec:
nodeName: k8s-ctr
containers:
- name: curl
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
ctr -n k8s.io containers list | grep pause
Worker 노드 업그레이드 (v1.32.11 → v1.33.7 → v1.34.3)
ControlPlane이 1.34로 올라간 상태에서, 워커는 드레인 → 노드 패키지 업그레이드 → kubelet 재시작 → 언코든 순으로 정리합니다.
이번 실습에서는 워커가 1.32라서 1.33을 한 번 거쳐 1.34로 올리는 흐름으로 진행했습니다.
사전 점검 & PDB 주의사항
# 노드별 주요 파드 위치 확인(특히 stateful/모니터링)
kubectl get pod -A -owide | egrep 'k8s-w1|k8s-w2'
# sts 확인
kubectl get sts -A
# pv/pvc 확인
kubectl get pv,pvc -A
PDB 관련 포인트 (중요)
- webpod가 replicas=2인데 maxUnavailable: 0 PDB를 걸면, drain 시 단 1개도 eviction 불가라서 drain이 막히는 게 정상입니다.
- “업그레이드/드레인 중에도 서비스 유지” 목적이면 보통 아래 중 하나로 운영합니다.
- minAvailable: 1 (replicas 2일 때 가장 흔함)
- 또는 maxUnavailable: 1
예시(PDB를 유지하고 drain을 허용하고 싶다면)
cat <<EOF | kubectl apply -f -
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: webpod
namespace: default
spec:
minAvailable: 1
selector:
matchLabels:
app: webpod
EOF
kubectl get pdb
워커 1대 drain 시도 & DaemonSet / emptyDir 케이스 정리
워커를 drain하면 흔히 아래 2개에 걸립니다.
- DaemonSet: flannel, kube-proxy, node-exporter 등 → --ignore-daemonsets
- emptyDir(local storage): alertmanager가 emptyDir 쓰는 경우 등 → --delete-emptydir-data
→ 이 옵션 쓰면 해당 파드 데이터는 날아갈 수 있음
# cordon/drain (실무형)
kubectl drain k8s-w2 --ignore-daemonsets --delete-emptydir-data
만약 webpod eviction이 PDB로 막히면(PDB 정책 위반), 위에서 말한 것처럼 PDB를 조정하거나(권장) 이번 실습처럼 PDB를 제거해야 drain이 진행됩니다.
드레인 결과 확인
kubectl get node
kubectl describe node k8s-w2 | egrep 'Taints|Unschedulable'
kubectl get pod -A -owide | grep k8s-w2
[k8s-w2] kubeadm/kubelet/kubectl 업그레이드 (1.33 → 1.34)
아래는 워커 노드에서 수행(vagrant ssh k8s-w2)
(1) v1.33 repo → kubeadm 업그레이드 → kubeadm upgrade node → kubelet/kubectl 업그레이드
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.33/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.33/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
dnf makecache
dnf install -y --disableexcludes=kubernetes kubeadm-1.33.7-150500.1.1
kubeadm version -o yaml
kubeadm upgrade node
dnf install -y --disableexcludes=kubernetes \
kubelet-1.33.7-150500.1.1 kubectl-1.33.7-150500.1.1
kubelet --version
kubectl version --client=true
systemctl daemon-reload
systemctl restart kubelet
systemctl status kubelet --no-pager
crictl ps
(2) v1.34 repo → kubeadm 업그레이드 → kubeadm upgrade node → kubelet/kubectl 업그레이드
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.34/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.34/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
dnf makecache
dnf install -y --disableexcludes=kubernetes kubeadm-1.34.3-150500.1.1
kubeadm version -o yaml
kubeadm upgrade node
dnf install -y --disableexcludes=kubernetes \
kubelet-1.34.3-150500.1.1 kubectl-1.34.3-150500.1.1
kubelet --version
kubectl version --client=true
systemctl daemon-reload
systemctl restart kubelet
systemctl status kubelet --no-pager
crictl ps
[k8s-ctr] uncordon → 파드 재배치 확인
워커 업그레이드가 끝났으면 스케줄링을 다시 열고, 파드가 정상 배치되는지 확인합니다.
# [k8s-ctr]
kubectl uncordon k8s-w2
kubectl get node -owide
# 파드 배치 확인(예시)
kubectl scale deployment webpod --replicas 1
kubectl get pod -owide
kubectl scale deployment webpod --replicas 2
kubectl get pod -owide
나머지 워커(k8s-w1)도 동일하게 반복
- 실습 흐름상 w1에 Prometheus/Grafana가 있었으니 w2부터 진행한 판단이 좋습니다.
- w1도 drain → (1.33) → (1.34) → uncordon 순서로 동일합니다.
# [k8s-ctr]
kubectl drain k8s-w1 --ignore-daemonsets --delete-emptydir-data
# [k8s-w1] 위의 1.33 -> 1.34 업그레이드 절차 동일 수행
# [k8s-ctr]
kubectl uncordon k8s-w1
kubectl get node -owide
kubectl get pod -A -owide
워커 노드 1개 제거(reset) → 다시 join 해보기
“워커 노드를 클러스터에서 제거 → 노드 로컬 상태 초기화 → 동일 노드명으로 재-조인” 흐름입니다.
(장점: 장애/드리프트/실습 초기화 상황에서 가장 확실하게 원복 가능)
kubeadm reset workflow (내부 동작 요약)
https://kubernetes.io/docs/reference/setup-tools/kubeadm/implementation-details/#kubeadm-reset-workflow-internal-design
Implementation details
FEATURE STATE: Kubernetes v1.10 [stable] kubeadm init and kubeadm join together provide a nice user experience for creating a bare Kubernetes cluster from scratch, that aligns with the best-practices. However, it might not be obvious how kubeadm does that.
kubernetes.io
- kubeadm reset은 노드에 남아있는 kubeadm/kubelet 관련 상태를 최선으로 정리합니다.
- 다만, 문서에도 명시된 것처럼 아래는 자동 정리되지 않습니다(수동 처리 필요):
- IPVS / iptables / nftables rules 정리 ❌
- CNI 설정(/etc/cni/net.d 등) 정리 ❌
- ~/.kube/ (사용자 홈) 정리 ❌
- containerd 이미지(캐시) 정리 ❌ (필요 시 수동)
[k8s-ctr] Control Plane에서 노드 제거
워커 노드를 안전하게 빼기 위해 먼저 drain 후 노드 오브젝트를 삭제합니다.
# node drain
kubectl drain k8s-w1 --ignore-daemonsets --delete-emptydir-data
# node delete : API 서버에서 노드 객체 제거
kubectl delete node k8s-w1
# 노드 상태 확인
kubectl get node
# NAME STATUS ROLES AGE VERSION
# k8s-ctr Ready control-plane ... v1.34.3
# k8s-w2 Ready <none> ... v1.34.3
여기까지는 클러스터 관점에서 노드 제거 완료
다음 단계부터는 제거된 노드(k8s-w1) 로컬에서 reset/정리가 필요합니다.
3) [k8s-w1] 제거 대상 노드에서 kubeadm reset 실행
vagrant ssh k8s-w1
# kubeadm reset 실행
kubeadm reset -f
예시 출력(정상)
[reset] Stopping the kubelet service
[reset] Unmounting mounted directories in "/var/lib/kubelet"
[reset] Deleting contents of directories: [/etc/kubernetes/manifests /var/lib/kubelet /etc/kubernetes/pki]
...
The reset process does not perform cleanup of CNI plugin configuration,
network filtering rules and kubeconfig files.
리셋 후 남은 디렉터리 확인(보통 tmp 잔재가 남을 수 있음)
tree /etc/kubernetes/
tree /var/lib/kubelet/
tree /etc/cni
reset 이후 수동 정리: CNI / kube 파일 / kubelet 디렉터리 / 네트워크 룰
CNI 설정/상태 정리
# CNI 설정 제거
rm -rf /etc/cni/net.d
# kubeadm/kubelet 관련 디렉터리 제거(잔재 제거)
rm -rf /etc/kubernetes/
rm -rf /var/lib/kubelet/
(참고) flannel 사용 시 인터페이스가 남는 경우가 있어 선택적으로 정리할 수 있습니다.
# (옵션) 남아있는 CNI/Flannel 인터페이스 확인
ip link | egrep 'cni0|flannel.1' || true
# (옵션) 인터페이스 삭제(랩 환경에서만 권장)
ip link delete cni0 2>/dev/null || true
ip link delete flannel.1 2>/dev/null || true
iptables 정리 (랩 환경 기준)
iptables -t nat -S
iptables -t filter -S
iptables -F
iptables -t nat -F
iptables -t mangle -F
iptables -X
Rocky/RHEL 계열은 환경에 따라 nftables backend를 사용할 수 있어, iptables flush로 “완전 정리”가 안 되는 경우도 있습니다.
깔끔한 초기화가 필요하고 랩 환경이라면 아래도 참고
# (옵션/주의) nftables 룰셋 확인/초기화
nft list ruleset
# nft flush ruleset
kubeconfig 정리(사용자 홈)
kubeadm reset은 ~/.kube를 안 지웁니다.
rm -rf ~/.kube
5) containerd / kubelet 서비스 상태 확인
join 전에 반드시 런타임(containerd)이 살아있어야 합니다.
systemctl status containerd --no-pager
systemctl status kubelet --no-pager
만약 앞 단계에서 서비스를 stop/disable 했다면, join 전에 다시 켭니다.
# join 전에 필수
systemctl enable --now containerd
systemctl enable --now kubelet
일반적으로는 containerd는 유지(재사용)하고, kubelet은 kubeadm join 시점에 정상 기동되도록 준비만 해두면 됩니다.
[k8s-ctr] Join 명령 재발급 → [k8s-w1] 다시 Join
(control plane) join 커맨드 생성
# [k8s-ctr]
kubeadm token create --print-join-command
출력 예시(환경마다 다름)
kubeadm join 192.168.10.100:6443 --token <...> \
--discovery-token-ca-cert-hash sha256:<...>
(worker) join 실행
# [k8s-w1]
kubeadm join 192.168.10.100:6443 --token <...> \
--discovery-token-ca-cert-hash sha256:<...>
containerd 소켓을 명시해야 하는 구성이라면(환경에 따라) --cri-socket 옵션도 함께 사용합니다.
예: --cri-socket unix:///run/containerd/containerd.sock
Join 후 검증 (노드 Ready / DS 파드 정상 / 스케줄링 확인)
# [k8s-ctr]
kubectl get node -owide
kubectl get pod -A -owide | grep k8s-w1
# flannel / kube-proxy 등 데몬셋 정상 확인
kubectl -n kube-flannel get pod -owide
kubectl -n kube-system get pod -owide | egrep 'kube-proxy|coredns'
드물게 join 직후 NotReady라면 대부분은
- CNI 잔재(/etc/cni/net.d, 인터페이스)
- iptables/nft 잔재
- containerd 미기동
중 하나가 원인인 경우가 많습니다.
가상 머신 삭제 (Vagrant 정리)
VM과 로컬 상태를 같이 정리합니다.
vagrant destroy -f
rm -rf .vagrant
마무리하며
이번 실습은 사전 준비부터 CNI 업그레이드, OS 마이너 업그레이드, Kubernetes 컴포넌트(kubeadm/kubelet/kubectl) 업그레이드, 워커 노드 drain/uncordon, 마지막으로 노드 reset 후 재-join까지 한 흐름으로 정리해 보았습니다.
특히 작업 내내 curl 반복 호출과 watch kubectl get node/pod, crictl ps, 그리고 Prometheus/Grafana를 함께 확인하면서, 서비스 영향이 실제로 있었는지를 기준으로 상태를 검증했습니다. 단일 ControlPlane 환경에서는 재부팅이나 kubelet 재시작 구간에 API 단절이 발생할 수밖에 없어서, 해당 구간을 전제로 모니터링을 걸어두는 방식이 중요했습니다.
또한 업그레이드 과정에서 발생하는 버전 불일치(예: KubeVersionMismatch)나 drain 과정에서의 제약(DaemonSet, emptyDir, PDB)은 ‘오류’라기보다는 클러스터가 의도대로 보호 동작하는 과정이라는 점도 함께 확인할 수 있었습니다. 마지막으로 kubeadm reset은 단순 초기화가 아니라 CNI 설정, iptables 규칙, kubeconfig 잔재 등이 남을 수 있으므로, 문서에 명시된 범위까지 포함해서 정리해야 안전하게 재-join이 가능하다는 점을 실습으로 남겼습니다.