OS Upgrades


Security Patch 나 Software Upgrade 등의 유지보수 사유로 Node 를 제거해야 하는 경우, 해당 Node 에서 실행되고 있는 Pod 을 다른 Node 에 옮겨두는 방법을 택할 수 있다.

kube-controller-manager --pod-eviction-timeout=5m0s

ReplicaSet 으로 배포된 Pod 의 경우, 기본적으로 5분 뒤 다른 Node 에 재배포된다.

kubectl drain node-1

Node 내 Pod 를 다른 Node 로 옮기고 싶은 경우 drain 명령어를 통해 Pod 를 옮길 수 있다. 만약 ReplicaSet 으로 배포되지 않은 Pod 가 존재한다면 Error 가 발생한다.

kubectl uncordon node-1

Node 가 재시작 된 이후 Pod 가 해당 Node 에 Scheduling 될 수 있도록 uncordon 을 사용할 수 있고,

kubectl cordon node-2

반대로 cordon 명령어를 통해 Node 에 Pod 가 Scheduling 되는 것을 제한할 수 있다.

Kubernetes Software Versions


K8s 는 일반적인 Software 처럼 Release Version 을 가지고 있으며, 기본적으로 최근 3개의 마이너 버전을 지원한다. kube-apiserver, controller-manager, kube-scheduler, kubelet, kube-proxy, kubectl 은 모두 동일한 버전으로 출시되며, etcd 와 core-dns 는 각각 다른 프로젝트이기에 독립적인 버전을 가지고 있다.

kube-apiserver 를 기준으로 다른 Component 들의 버전이 호환될 수 있는데 이는 아래와 같다.

  • 예를 들어 kube-apiserver 가 v1.10 일 경우,
  • controller-manager 와 kube-scheduler 는 v1.9 와 v1.10 이 호환되고,
  • kubelet 과 kube-proxy 는 v1.8 과 v1.9 와 v1.10 이 호환되고,
  • kubectl 은 v1.9 와 v1.10 과 v1.11 이 호환된다.

Cluster Upgrade Process

K8s Cluster 를 업그레이드할 때 EKS 나 AKS 같은 Managed-Service 를 사용한다면 간단하게 몇 번의 클릭으로 업그레이드가 가능하지만, 그렇지 않은 경우엔 kubeadm 툴을 활용해 Cluster 를 업그레이드할 수 있다.

# k8s apt repository 리스트 파일에서
vim /etc/apt/sources.list.d/kubernetes.list

# 아래로 변경
echo -e "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /" > /etc/apt/sources.list.d/kubernetes.list

# apt 업데이트 후 kubeadm 최신 버전 확인
apt update
apt-cache madison kubeadm

apt-get install kubeadm=1.32.0-1.1

kubeadm upgrade plan v1.32.0
kubeadm upgrade apply v1.32.0
kubectl get nodes

apt-get install kubelet=1.32.0-1.1
systemctl restart kubelet
kubectl get nodes

먼저 Master Node 를 위 명령어들을 통해 업그레이드하자. kubeadm 툴 역시 업그레이드하고자 하는 버전으로 새로 설치해주어야한다. 또한, kubectl get nodes 는 기본적으로 kubelet 의 버전을 보여주기 때문에 kubelet 역시 새로운 버전으로 설치해준 뒤 확인해보자.

# 먼저 node01 에 실행중인 Pod 을 drain 하고
kubectl drain node01

# node01 으로 접속한 뒤
ssh node01

# k8s apt repository 리스트 파일에서
vim /etc/apt/sources.list.d/kubernetes.list

# 아래로 변경
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /

# apt 업데이트 후 kubeadm 최신 버전 확인
apt update
apt-cache madison kubeadm

apt-get install kubeadm=1.32.0-1.1
kubeadm upgrade node config --kubelet-version v1.32.0

apt-get install kubelet=1.32.0-1.1
systemctl restart kubelet

exit

kubectl uncordon node-1

이제 Worker Node 로 넘어간 뒤 마찬가지로 업그레이드해주면 된다.

Backup and Restore Methods


kubectl get all --all-namespaces -o yaml > all-deploy-services.yaml

Resource Configuration 자체를 yaml 파일로 저장하여 백업하는 방법이 있고,

# etcd 스냅샷 생성
export ETCDCTL_API=3
etcdctl snapshot save /tmp/snapshot.db \
  --endpoints=https://[127.0.0.1]:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/etcd-server.crt \
  --key=/etc/kubernetes/pki/etcd/etcd-server.key
etcdctl snapshot status snapshot.db
 
# etcd 복원
etcdctl snapshot restore /opt/snapshot-pre-boot.db \
  --data-dir /var/lib/etcd-from-backup
 
# kube-apiserver 정지
service kube-apiserver stop
 
# etcd 재시작
systemctl daemon-reload
service etcd restart
 
# kube-apiserver 재시작
service kube-apiserver start

또는 etcd 스냅샷을 이용해 백업하는 방법이 있다.

vi /etc/kubernetes/manifests/etcd.yaml

# 복원 이후 volume path 수정
  volumes:
  - hostPath:
      path: /var/lib/etcd-from-backup
      type: DirectoryOrCreate
    name: etcd-data

복원 이후 etcd volume path 를 수정해주자.

EKS 노드 그룹 업그레이드 지연 사례


EKS 클러스터의 worker 노드그룹(21노드) 1.33→1.34 업그레이드가 에러 없이 ~4시간 걸린 사건이다. 표면상 성공이라 원인이 가려졌고, 추적해 보니 topologySpreadPDB노드 교체 가 연쇄로 묶인 구조였다.

배경

  • aws_eks_node_group(Terraform), updateConfig.maxUnavailable=1(직렬), force_update_version=true
  • 21노드를 한 대씩 cordon/drain/terminate. forcedescribe-nodegroup-updatePodEvictionFailure 없이 “성공” 으로 끝남 → 느린 원인이 안 드러남

증상

  • 노드그룹 업그레이드만 ~4시간. 정상 노드는 교체 사이클이 ~5분인데, 막힌 노드는 연속 terminate 간격이 ~18–20분
  • 그 ~15분은 drain 윈도우 가 PDB 에 막혀 timeout 후 force-terminate 되는 시간
  • 4시간 중 ~2.5시간이 PDB 차단 drain 에 소모됨

원인

연쇄 사슬:

  1. 워크로드의 zone topologySpreadDoNotSchedule(maxSkew 1) → 업그레이드로 evict 된 파드의 교체본이 스케줄 못 됨(Pending)
  2. Pending 교체본이 PDBdisruptionsAllowed 를 0 으로 묶음
  3. 그 0 이 다음 노드의 eviction 을 429 로 차단 (직렬이어도 노드를 건너 전염)
  4. force=true 라 노드마다 ~15분 timeout 후 force-terminate 를 반복 → 누적 지연
  • 악화요인: 큰 resource requests, AZ 단위로 묶인 drain, nodeTaintsPolicy: Honor(cordon 노드를 skew 계산에서 빼 살아있는 zone 정원을 더 빡빡하게 함)

결정적 증거

ASG 타임라인 노드그룹 단위 launch/terminate 는 audit log 가 아니라 ASG describe-scaling-activities 로만 정확히 보인다.

# 노드그룹 ASG 의 노드 증설/종료를 시간순 + AZ 포함으로 표 출력
aws autoscaling describe-scaling-activities --auto-scaling-group-name "$ASG" --region <r> | jq -r '
  ["INSTANCE","ACTION","AZ","START","END","DUR","STATUS"],
  (.Activities | sort_by(.StartTime) | .[]
    | (if (.Description|test("Launching")) then "Launch" elif (.Description|test("Terminating")) then "Terminate" else "?" end) as $a
    | ((.Description | capture("(?<i>i-[0-9a-f]+)").i) // "-") as $iid
    | ((try (.Details|fromjson|.["Availability Zone"]) catch null) // "-") as $az
    | ((.StartTime[0:19]+"Z")|fromdateiso8601) as $s
    | (if .EndTime then (.EndTime[0:19]+"Z")|fromdateiso8601 else null end) as $e
    | [$iid,$a,$az,.StartTime[0:19],(.EndTime[0:19]//"-"),(if $e then "\($e-$s)s" else "running" end),.StatusCode])
  | @tsv' | column -t -s $'\t'
INSTANCE  ACTION     AZ          START                END                  DUR   STATUS
i-0aa..   Launch     ap-east-1a  2026-06-08T16:31:02  2026-06-08T16:34:10  188s  Successful  ← Scale-up 버퍼(a/b/c 동시)
i-0bb..   Launch     ap-east-1b  2026-06-08T16:31:03  2026-06-08T16:34:31  208s  Successful
i-0cc..   Launch     ap-east-1c  2026-06-08T16:31:03  2026-06-08T16:34:31  208s  Successful
i-0c1..   Terminate  ap-east-1c  2026-06-08T16:40:55  2026-06-08T16:41:40  45s   Successful  ┐ zone c부터 1:1 교체
i-0cc..   Launch     ap-east-1c  2026-06-08T16:40:55  2026-06-08T16:41:40  45s   Successful
i-0c2..   Terminate  ap-east-1c  2026-06-08T16:59:30  2026-06-08T17:00:18  48s   Successful  ← 직전과 ~19분 간격 = 막힌 노드
i-0cc..   Launch     ap-east-1c  2026-06-08T17:00:55  2026-06-08T17:01:40  45s   Successful
i-0c3..   Terminate  ap-east-1c  2026-06-08T17:18:05  2026-06-08T17:18:50  45s   Successful  ← 또 ~18분 간격
i-0cc..   Launch     ap-east-1a  2026-06-08T17:19:00  2026-06-08T17:19:45  45s   Successful
i-0a1..   Terminate  ap-east-1a  2026-06-08T18:40:12  2026-06-08T18:40:58  46s   Successful  ← zone c 끝나고 zone a로

읽는 법:

  1. AZ 컬럼이 c→a→b 로 묶임(AZ 단위 drain)
  2. 연속 Terminate 간격이 노드당 사이클(정상 ~5분 / 막힘 ~18–20분)
  3. Launch DUR 은 EC2 부팅~health(~3분).

스케줄러 메시지 사유별로 읽으면 리소스가 아니라 topology 가 주범:

0/28 nodes are available: 1 Insufficient cpu, 1 Insufficient memory,
 14 node(s) didn't match pod topology spread constraints,
 4 node(s) had untolerated taint(s), 9 node(s) were unschedulable, ...
 preemption: ... not helpful ...

→ 28노드 중 9(cordon)+4(taint)=13 은 후보 제외, 남은 중 14 가 “정원 찬 zone” 이라 거부.

FailedScheduling 집계(Logs Insights 쿼리) — total_fail == topo_fail, 즉 스케줄 실패가 100% topology:

svc                total_fail  topo_fail
a1-service         711         711
b2-service         144         144   ← 작은 서비스(200m/1Gi)도 막힘 = 리소스가 아니라 topology
c3-service         128         128

문제의 topologySpread(전 서비스 공통) — zone 제약이 hard 인 게 핵심:

- maxSkew: 1
  topologyKey: topology.kubernetes.io/zone
  whenUnsatisfiable: DoNotSchedule   # ← hard. evict 교체본을 Pending 으로 만듦
  nodeTaintsPolicy: Honor            # ← cordon 노드 제외 → brittleness 증폭
- maxSkew: 1
  topologyKey: kubernetes.io/hostname
  whenUnsatisfiable: ScheduleAnyway  # hostname 은 soft

진단 보조: kubectl get pdb -A -o wide(ALLOWED DISRUPTIONS 0 찾기), deployment 별 requests 정렬(jq 로 cpu→m·mem→Mi 정규화 후 내림차순)로 큰 워크로드 식별.

해결

  • 근본: zone 제약을 ScheduleAnyway(soft)로 완화하거나, surge 용량을 충분히 확보해 새 노드가 Ready 된 뒤 drain 되도록 한다. 한번 깨진 불균형은 다음 배포 때 수렴하거나 descheduler 로 재배치.
  • 차선: 업그레이드 시 maxUnavailable 를 키워 동시 교체량을 늘리거나, 대형 행사·업그레이드 전 topology/PDB 를 사전 점검.

교훈

  • force_update_version=true 는 에러를 없애지 느림 을 없애지 않는다 — “성공했는데 느린” 업그레이드는 PDB 차단 drain 을 의심한다.
  • eviction(429)·FailedScheduling·PDB·topology 는 별개 지표지만 한 사슬이다. 한쪽만 보면 원인을 못 찾는다.
  • 노드그룹 단위 타임라인은 ASG, 클러스터 전역 원인 분포는 CloudWatch audit — 스코프가 다른 두 소스를 합쳐야 그림이 완성된다.

References