들어가며
이전 포스팅에서 집에 굴러다니는 미니PC들로 쿠버네티스 클러스터를 구축한 이야기를 했었다.
그때 설치한 버전이 v1.30.4였는데, 그 뒤로 Jenkins, Kafka, MySQL InnoDB Cluster, Redis 같은 것들을 하나씩 올리면서 “잘 돌아가는데 굳이 건드려야 하나” 싶어서 업그레이드를 계속 미뤄왔다. 근데 v1.30 지원 종료(EOL)도 되었고(on-prem은 2025년에 만료), CKA 시험 준비를 하면서 kubeadm 업그레이드를 공부하다 보니 이참에 직접 해보자 싶었다.
실제 프로덕션 워크로드가 돌아가는 환경에서 하는 거라 우려했는데, 막상 절차대로 하니까 생각보다 어렵지 않았다. 그 과정을 기록해둔다.
3줄 요약
- Kubernetes는 한 번에 1 마이너 버전씩만 업그레이드할 수 있다 (v1.30 → v1.31 OK, v1.30 → v1.32 불가.
도대체 왜 이렇게 만든건가?)- 반드시 control-plane 먼저, worker 나중에 순서를 지켜야 한다
- 업그레이드 전 etcd 백업은 필수 – 실패 시 복구할 수 있는 유일한 보험
Kubernetes 클러스터 구성
업그레이드 대상 클러스터 구성은 이렇게 생겼다:
Kubernetes Cluster v1.30.4
Control Plane (3대)
- luckys-worker0
- ingress-nginx-controller
- redis-node-0
- mysql-operator
- luckys-worker1
- ingress-nginx-controller
- dev-mysql-cluster-0
- luckys-worker2
- kafka-controller-2
- redis-node-2
Worker (3대)
luckys-worker3 – 사망- luckys-worker4
- kafka-controller-1
- prod-mysql-cluster-1
- loki (로그 수집)
- luckys-worker5
- kafka-controller-0
- prod-mysql-cluster-2
- prometheus, alertmanager
- redis-dev-master (standalone)
- luckys-worker6
- prod-mysql-cluster-0
- ingress-nginx-controller
- jenkins
- nexus, openclaw, grafana
주요 워크로드: Jenkins, Kafka, MySQL InnoDB Cluster, Redis Sentinel, Longhorn, MetalLB, Ingress-Nginx, 이외 다수 Application
OS는 Ubuntu 24.04 LTS, 컨테이너 런타임은 containerd.
사전 준비
1. 현재 버전 확인
kubeadm version
# kubeadm version: v1.30.4
kubelet --version
# Kubernetes v1.30.4
kubectl version
2. 업그레이드 가능한 버전 확인
v1.31 저장소를 추가하고 사용 가능한 버전을 확인한다:
# v1.31 저장소 추가
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring-v1.31.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' \
| sudo tee /etc/apt/sources.list.d/kubernetes-v1.31.list
# GPG 키 등록
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key \
| sudo gpg --dearmor --yes -o /etc/apt/keyrings/kubernetes-apt-keyring-v1.31.gpg
sudo apt update
sudo apt-cache madison kubeadm | head -5
참고: 기존 v1.30 저장소의 GPG 키가 만료되어
apt update시 에러가 발생할 수 있다. v1.31 저장소만 정상이면 업그레이드 진행에 문제없다.
3. etcd 백업 (필수!)
업그레이드 전 반드시 etcd를 백업한다. 문제가 생기면 이 백업으로 클러스터를 복구할 수 있다.
# 인증서 경로 확인
kubectl describe pod etcd-luckys-worker0 -n kube-system
# etcd 백업 실행
export ETCDCTL_API=3
etcdctl snapshot save /opt/etcd-backup-before-upgrade.db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key
# 백업 확인
etcdctl snapshot status /opt/etcd-backup-before-upgrade.db --write-out=table
etcdctl이 설치되어 있지 않다면
kubectl exec으로 etcd Pod 안에서 실행하거나, etcd 바이너리를 직접 설치하면 된다.
4. 노드별 워크로드 분포 확인
drain 하면 영향받는 워크로드를 미리 파악해야 한다:
# 각 노드에서 돌아가는 Pod 확인
kubectl get pods -A -o wide --field-selector spec.nodeName=luckys-worker0 | grep -v kube-system
# Taint 확인 (control-plane에 Taint가 없으면 일반 워크로드도 올라가 있을 수 있음)
kubectl describe node luckys-worker0 | grep -i taint
내 클러스터는 control-plane에 Taint가 설정되어 있지 않아서(마스터도 예외 없다) 일반 워크로드도 control-plane 노드에서 실행되고 있었다. MySQL InnoDB Cluster, Redis, Ingress 등의 분포를 확인하고 drain해도 프로덕션에 영향이 없는지 검증한 후 진행했다.
업그레이드 시 안전도
비교적 안전
- luckys-worker0 (prod 1개뿐, MySQL/Kafka 없음)
- luckys-worker2 (워크로드 적음)
우려됨
- luckys-worker1 (prod 앱 + ingress)
- luckys-worker5 (모니터링 + MySQL + Kafka)
매우 우려됨
- luckys-worker4 (워크로드 최다 + MySQL + Kafka)
- luckys-worker6 (prod 앱 많음 + MySQL + Jenkins + Ingress)
사실 다른 것도 그렇지만, Longhorn 으로 데이터가 서로 다른 노드에 동기화 되어야 하는데, upgrade로 인한 중단 시 쓰기 지연이 발생할 경우 클러스터 전체의 성능이 대폭 하락하는 문제가 있어서 이 지점이 가장 골머리 아프다.
업그레이드 순서
반드시 control-plane → worker 순서로 해야한다. kubelet은 apiserver보다 높은 버전일 수 없기 때문이다.
1. Control Plane #1 (luckys-worker0) ← 첫 번째는 kubeadm upgrade apply
2. Control Plane #2 (luckys-worker1) ← 이후는 kubeadm upgrade node
3. Control Plane #3 (luckys-worker2)
4. Worker #1 (luckys-worker4) ← 전부 kubeadm upgrade node
5. Worker #2 (luckys-worker5)
6. Worker #3 (luckys-worker6)
Control Plane 첫 번째 노드 업그레이드
첫 번째 control-plane 노드만 kubeadm upgrade apply를 사용한다. 나머지는 전부 kubeadm upgrade node를 쓴다.
Step 1: kubeadm 업그레이드
# kubeadm 패키지 잠금 해제 → 설치 → 다시 잠금
apt-mark unhold kubeadm && \
apt-get update && apt-get install -y kubeadm=1.31.14-1.1 && \
apt-mark hold kubeadm
Step 2: 업그레이드 계획 확인 및 실행
# 버전 확인
kubeadm version
# 업그레이드 계획 확인
sudo kubeadm upgrade plan
# 업그레이드 실행
sudo kubeadm upgrade apply v1.31.14
kubeadm upgrade plan은 현재 상태를 분석해서 업그레이드 가능 여부를 보여준다. 문제가 없으면 apply로 실제 업그레이드를 진행한다.
Step 3: 노드에서 Pod 퇴거 (drain)
kubeadm upgrade 후, kubelet 업그레이드 전에 drain한다.
kubectl drain luckys-worker0 --ignore-daemonsets
--ignore-daemonsets: DaemonSet Pod(모니터링, 네트워크 등)은 무시- emptyDir 사용하는 Pod 때문에 실패하면
--delete-emptydir-data추가
Step 4: kubelet & kubectl 업그레이드
# 패키지 잠금 해제 → 설치 → 다시 잠금
apt-mark unhold kubelet kubectl && \
apt-get update && apt-get install -y kubelet=1.31.14-1.1 kubectl=1.31.14-1.1 && \
apt-mark hold kubelet kubectl
# kubelet 재시작
sudo systemctl daemon-reload
sudo systemctl restart kubelet
Step 5: 노드 복귀 (uncordon)
kubectl uncordon luckys-worker0
Step 6: 업그레이드 확인
kubectl get nodes
# luckys-worker0의 VERSION이 v1.31.14로 변경되었는지 확인
나머지 Control Plane 노드 업그레이드
두 번째, 세 번째 control-plane 노드는 kubeadm upgrade node를 사용한다. apply가 아닌 점에 주의. kubeadm upgrade plan도 불필요하다.
각 노드에 SSH 접속 후:
# 1. kubeadm 업그레이드
apt-mark unhold kubeadm && \
apt-get update && apt-get install -y kubeadm=1.31.14-1.1 && \
apt-mark hold kubeadm
# 2. 노드 업그레이드
sudo kubeadm upgrade node # ← apply가 아닌 node!
# 3. drain (다른 control-plane 노드에서 실행)
kubectl drain luckys-worker1 --ignore-daemonsets
# 4. kubelet & kubectl 업그레이드
apt-mark unhold kubelet kubectl && \
apt-get update && apt-get install -y kubelet=1.31.14-1.1 kubectl=1.31.14-1.1 && \
apt-mark hold kubelet kubectl
sudo systemctl daemon-reload
sudo systemctl restart kubelet
# 5. uncordon (다른 노드에서 실행)
kubectl uncordon luckys-worker1
luckys-worker2도 동일하게 진행한다.
Worker 노드 업그레이드
Worker 노드도 거의 동일하다. kubeadm upgrade node를 사용한다.
# 1. kubeadm 업그레이드
apt-mark unhold kubeadm && \
apt-get update && apt-get install -y kubeadm=1.31.14-1.1 && \
apt-mark hold kubeadm
# 2. 노드 업그레이드
sudo kubeadm upgrade node
# 3. drain (control-plane에서 실행)
kubectl drain luckys-worker4 --ignore-daemonsets
# 4. kubelet & kubectl
apt-mark unhold kubelet kubectl && \
apt-get update && apt-get install -y kubelet=1.31.14-1.1 kubectl=1.31.14-1.1 && \
apt-mark hold kubelet kubectl
sudo systemctl daemon-reload
sudo systemctl restart kubelet
# 5. uncordon (control-plane에서 실행)
kubectl uncordon luckys-worker4
luckys-worker5, luckys-worker6도 동일하게 진행한다.
전체 업그레이드 완료 확인
kubectl get nodes -o wide
NAME STATUS ROLES VERSION OS-IMAGE
luckys-worker0 Ready control-plane v1.31.14 Ubuntu 24.04 LTS
luckys-worker1 Ready control-plane v1.31.14 Ubuntu 24.04 LTS
luckys-worker2 Ready control-plane v1.31.14 Ubuntu 24.04 LTS
luckys-worker4 Ready <none> v1.31.14 Ubuntu 24.04 LTS
luckys-worker5 Ready <none> v1.31.14 Ubuntu 24.04 LTS
luckys-worker6 Ready <none> v1.31.14 Ubuntu 24.04 LTS
주의사항 & 삽질 기록
GPG 키 만료 문제
v1.30 저장소의 GPG 키가 만료되어 apt update 시 에러가 났다:
EXPKEYSIG 234654DA9A296436 isv:kubernetes OBS Project
v1.31 저장소를 새로 추가하고 키를 등록하면 해결된다. 기존 v1.30 저장소 에러는 무시해도 된다.
1. v1.31 GPG 키 다운로드 및 등록
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring-v1.31.gpg
2. v1.31 저장소 추가
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring-v1.31.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes-v1.31.list
control-plane에 Taint가 없는 경우
일반적으로 control-plane 노드에는 NoSchedule Taint가 설정되어 있어서 일반 워크로드가 배치되지 않는다. 근데 내 클러스터처럼 Taint가 없으면 MySQL, Redis, Ingress 등이 control-plane에서도 실행된다.(Taint를 설정하기엔 비용 이슈가..)
drain 전에 반드시 해당 노드의 워크로드를 확인하고, 프로덕션 영향을 검토해야 한다:
kubectl get pods -A -o wide --field-selector spec.nodeName=<노드명> | grep -v kube-system
drain vs cordon
kubectl drain: 기존 Pod를 다른 노드로 퇴거시키고 스케줄 차단kubectl cordon: 새 Pod 스케줄만 차단, 기존 Pod는 그대로
프로덕션 영향이 걱정되면 cordon만 하고 업그레이드를 진행하는 방법도 있다. kubelet 재시작 시 잠깐 중단되지만 Pod가 다른 노드로 이동하지는 않는다.
MySQL InnoDB Cluster 고려
MySQL InnoDB Cluster는 3개 인스턴스가 서로 다른 노드에 분산되어 있어서, 한 노드를 drain해도 나머지 2개가 쿼럼을 유지한다. drain 전에 어떤 노드에 어떤 인스턴스가 있는지 확인하자:
kubectl get pods -A -o wide | grep mysql
kubectl get innodbcluster -A
한 대씩, 확인하면서
절대 여러 노드를 동시에 drain하지 말자. 특히 control-plane은 etcd 쿼럼(과반수) 유지가 필수다. 3대 중 2대가 동시에 내려가면 클러스터가 멈춘다.
한 대 업그레이드 완료 → kubectl get nodes로 Ready 확인 → 다음 노드
업그레이드 절차 요약
[사전 준비]
etcd 백업 → 저장소 추가 → 워크로드 분포 확인
[Control Plane 첫 번째 노드]
kubeadm 설치 → kubeadm upgrade apply → drain → kubelet kubectl 설치 → restart → uncordon
[Control Plane 나머지 + Worker 전체]
kubeadm 설치 → kubeadm upgrade node → drain → kubelet kubectl 설치 → restart → uncordon
[완료]
kubectl get nodes로 전체 버전 확인
마치며
막상 해보니 절차만 지키면 크게 어렵지 않아서 이걸 왜이리 미뤄왔나 싶다.
핵심은 세 가지다:
- etcd 백업: 만약을 위한 보험
- 순서 준수: control-plane 먼저, worker 나중에
- 한 대씩: 확인하고 넘어가기
CKA 시험에서도 kubeadm 업그레이드는 거의 매번 출제되는 문제라고 한다. 실제 클러스터에서 한 번 해보면 시험에서도 별 문제 없이 풀 수 있을 것 같다.
참고 링크
- Kubernetes 공식 문서 – Upgrading kubeadm clusters
- Kubernetes Version Skew Policy
- CKA 시험 공식 페이지
- 홈 서버 쿠버네티스 클러스터 구축기