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시간 걸린 사건이다. 표면상 성공이라 원인이 가려졌고, 추적해 보니 topologySpread → PDB → 노드 교체 가 연쇄로 묶인 구조였다.
배경
aws_eks_node_group(Terraform),updateConfig.maxUnavailable=1(직렬),force_update_version=true- 21노드를 한 대씩 cordon/drain/terminate.
force라describe-nodegroup-update에PodEvictionFailure없이 “성공” 으로 끝남 → 느린 원인이 안 드러남
증상
- 노드그룹 업그레이드만 ~4시간. 정상 노드는 교체 사이클이 ~5분인데, 막힌 노드는 연속 terminate 간격이 ~18–20분
- 그 ~15분은 drain 윈도우 가 PDB 에 막혀 timeout 후 force-terminate 되는 시간
- 4시간 중 ~2.5시간이 PDB 차단 drain 에 소모됨
원인
연쇄 사슬:
- 워크로드의 zone
topologySpread가DoNotSchedule(maxSkew 1) → 업그레이드로 evict 된 파드의 교체본이 스케줄 못 됨(Pending) - Pending 교체본이 PDB 의
disruptionsAllowed를 0 으로 묶음 - 그 0 이 다음 노드의 eviction 을 429 로 차단 (직렬이어도 노드를 건너 전염)
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로읽는 법:
- AZ 컬럼이 c→a→b 로 묶임(AZ 단위 drain)
- 연속 Terminate 간격이 노드당 사이클(정상 ~5분 / 막힘 ~18–20분)
- 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 — 스코프가 다른 두 소스를 합쳐야 그림이 완성된다.