一次一步,从 inventory 开始 — Kubespray 升级的规则
한국어 원문으로 표시합니다.
한 줄 요약
kubespray 의 업그레이드는 인벤토리의 판을 한 칸 올리고 upgrade-cluster.yml 을 돌리는 일이며, 노드마다 cordon → drain → 올리기 → uncordon 을 하므로 노드 한 대짜리 클러스터에서는 그 동안이 곧 중단 시간입니다.
왜 이게 필요했나
쿠버네티스 프로젝트는 최근 세 마이너 판만 패치합니다. 그러니 1년에 두세 번은 올려야 하고, 올리지 않고 버티면 한 번에 여러 칸을 건너야 하는데 그건 허락되지 않습니다. 쿠버네티스의 버전 차이 정책(Version Skew Policy)은 kube-apiserver 가 한 번에 한 마이너씩만 올라가야 한다고 정하고, kubeadm 업그레이드 문서도 마이너 판을 건너뛰지 말라고 합니다. kubespray 는 여기에 자기 규칙을 하나 더 얹습니다 — kubespray 태그도 한 칸씩 올리라는 것입니다. 태그마다 역할의 기본값과 받을 파일 목록이 바뀌기 때문에, 오래된 태그에서 최신 태그로 곧장 뛰면 무엇이 깨질지 아무도 시험하지 않았습니다.
어떻게 동작하나
판은 인벤토리에서 올립니다. 업그레이드 문서의 요지는 이렇습니다. 인벤토리에 kube_version 을 적어 두었다면 upgrade-cluster.yml 전에 그 값을 고치거나 -e kube_version=... 로 새 판을 넘기라고, 그러지 않으면 "인벤토리에 적힌 같은 판에 그대로 머문다" 고. 두 방법 중 파일을 고치는 쪽이 낫습니다. -e 로만 올리면 클러스터는 새 판인데 인벤토리는 옛 판이라, 다음에 누가 그 인벤토리로 cluster.yml 을 돌리는 순간 어긋난 상태에서 시작합니다.
upgrade-cluster.yml 은 순서를 지킵니다. 이 플레이북은 이미 있는 클러스터에만 쓰고, 컨트롤 플레인과 etcd 를 먼저, 워커를 나중에 올립니다. 노드마다 pre-upgrade 역할이 cordon 과 drain 을 하고, kubeadm upgrade 로 정적 파드 매니페스트를 새 판으로 바꾸고, kubelet 을 올린 뒤, post-upgrade 역할이 uncordon 합니다. 한 번에 올리는 노드 수는 serial(기본 20%)로 정하고, 문서는 upgrade_node_confirm·upgrade_node_pause_seconds 로 노드마다 멈춰 확인하는 방법도 알려 줍니다. 노드를 나눠 올리려면 먼저 facts.yml 을 제한 없이 돌린 뒤 --limit "kube_control_plane:etcd" 로 컨트롤 플레인부터 올리라고 합니다.
모든 것이 따라 오르지는 않습니다. kubespray 는 etcd·CoreDNS·pause 이미지의 판을 쿠버네티스 마이너 판별 표(etcd_supported_versions, coredns_supported_versions 등)로 고릅니다. 두 마이너가 표에서 같은 값을 가리키면 그 구성요소는 그대로 남습니다. 이번 실측이 그랬습니다 — 1.35.8 과 1.36.4 모두 etcd 3.6.14 였고, 반대로 CoreDNS 는 표가 가리키는 판이 달라 1.12.4 에서 1.14.2 로 바뀌었습니다. "쿠버네티스를 올렸으니 etcd 도 올랐겠지" 는 확인 전에는 가정일 뿐입니다.
그다음 칸은 다음 태그에서 옵니다. 태그가 받아 주는 가장 높은 판은 체크섬 목록의 첫 키이고, v2.32.0 에서는 1.36.4 입니다. 이 판까지 왔으면 이 kubespray 로는 더 올라갈 수 없습니다. 문서의 "Multiple upgrades" 절처럼 kubespray 를 다음 태그로 올리고(그 태그의 requirements.txt 로 Ansible 도 다시 깔고) upgrade-cluster.yml 을 돌리는 것이 다음 칸입니다.
현장에서 만나는 모습
이 코스를 만들며 medium VM 에서 1.35.8 → 1.36.4 를 실측했습니다. upgrade-cluster.yml 전체가 368초였고, 그 가운데 kubeadm 이 첫 컨트롤 플레인을 올리는 작업이 94초, 드레인이 16초였습니다. 메모리는 최대 1.56GiB 까지 올라가 설치 때(최대 1.36GiB)보다 조금 높았지만 4GiB 안에서 넉넉했습니다. 노드 UID 는 그대로였고, 드레인된 web 파드는 uncordon 뒤 새 UID 로 다시 떴습니다. 즉 한 대짜리 클러스터에서 드레인은 "옮긴다" 가 아니라 "멈췄다 다시 띄운다" 입니다.
현장에서 자주 보는 사고 둘. 첫째, PodDisruptionBudget 이 minAvailable 로 모든 레플리카를 묶어 두면 드레인이 끝나지 않습니다. pre-upgrade 역할의 기본값은 drain_timeout: 360s 로 세 번(drain_retries: 3) 시도한 뒤 실패하고, upgrade_node_uncordon_after_drain_failure: true 라 노드를 다시 uncordon 한 채 플레이북을 멈춥니다. 노드는 옛 판 그대로입니다. 둘째, 드레인은 지났는데 그 뒤 단계에서 멈추면 노드가 cordon 된 채 남습니다. 다시 돌리지 않고 퇴근하면 다음 날 스케줄이 막힌 노드가 발견됩니다. 그래서 업그레이드 뒤 점검표 맨 위에 "unschedulable 인 노드가 없는가" 가 있습니다.
다음 실습에서 할 것
1.35.8 으로 세우고 워크로드를 띄운 뒤 올리기 전 사진을 찍습니다. 인벤토리의 판을 1.36.4 로 바꾸고 upgrade-cluster.yml 을 돌린 다음, 같은 사진을 다시 찍어 노드·워크로드·etcd·CoreDNS 가 각각 어떻게 됐는지 비교합니다. 로그에서 cordon·drain·uncordon 을 읽고, 이 kubespray 로 다음 칸이 가능한지 판단합니다.