Kubernetes 온프레미스 클러스터 업그레이드하기

들어가며

이전 포스팅에서 집에 굴러다니는 미니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로 전체 버전 확인

마치며

막상 해보니 절차만 지키면 크게 어렵지 않아서 이걸 왜이리 미뤄왔나 싶다.

핵심은 세 가지다:

  1. etcd 백업: 만약을 위한 보험
  2. 순서 준수: control-plane 먼저, worker 나중에
  3. 한 대씩: 확인하고 넘어가기

CKA 시험에서도 kubeadm 업그레이드는 거의 매번 출제되는 문제라고 한다. 실제 클러스터에서 한 번 해보면 시험에서도 별 문제 없이 풀 수 있을 것 같다.

참고 링크

답글 남기기