Chapter 14

클러스터 유지보수

  • 14.1 OS 업그레이드
  • 14.2 Kubernetes 릴리스
  • 14.3 클러스터 업그레이드
  • 14.4 클러스터 업그레이드 데모
  • 14.5 백업과 복원

클러스터를 운영하다 보면 노드 OS 패치, 쿠버네티스 버전 업그레이드, 장애 대비 백업까지 유지보수 작업이 끊이질 않아. 노드를 안전하게 빼고 올리는 기본기부터 버전 호환 규칙, 실제 업그레이드 절차, etcd 스냅샷까지 클러스터 유지보수의 전체 흐름을 다뤄.

클러스터 노드를 유지보수 때문에 내려야 할 때, 제일 중요한 건 파드 퇴출 타임아웃(pod eviction timeout) 이야. 노드가 다운되면 쿠버네티스는 기본적으로 5분을 기다려. 그 안에 노드가 돌아오면 kubelet 프로세스가 시작되면서 파드가 다시 살아나지만, 5분이 지나면 그 노드의 파드는 죽은 걸로 간주해버려. 이 타임아웃은 kube-controller-manager에서 설정하는 값이야. 최신 쿠버네티스에서는 이게 노드 테인트와 파드 톨러레이션으로 처리되는데, 노드가 NotReady 상태가 되면 자동으로 테인트가 걸리고, 그 테인트를 견디지 못하는 파드는 유예 기간이 지나면 퇴출되는 구조야.

여기서 핵심은, 레플리카셋에 속한 파드는 다른 노드에서 다시 생성되지만, 레플리카셋 없이 단독으로 떠 있는 파드는 그냥 사라진다는 거야. 노드가 타임아웃 후에 다시 살아나도 빈 깡통으로 올라와. 이전에 있던 파드가 자동으로 돌아오지 않거든. 예를 들어 파란색 파드가 레플리카셋으로 여러 개 돌고 있으면 하나가 죽어도 다른 노드에서 새로 만들어지니까 사용자는 영향 없어. 근데 녹색 파드가 레플리카셋 없이 단 하나만 있었으면, 노드가 내려가면 그 파드는 영영 사라져.

그래서 노드 유지보수할 때 5분 안에 끝낼 자신이 있으면 그냥 빠르게 재부팅해도 되긴 해. 근데 정말 5분 안에 돌아올 거라고 확신할 수 있어? 보장할 수 없잖아. 그래서 더 안전한 방법이 있어.

바로 kubectl drain 명령이야. 이걸 쓰면 해당 노드의 모든 파드를 정상적으로 종료시키고, 다른 노드에서 다시 생성해. 엄밀히 말하면 "이동"이 아니야. 기존 노드에서 파드가 gracefully 종료되고, 다른 노드에서 새로 만들어지는 거지. 그리고 그 노드를 스케줄 불가(cordon) 상태로 표시해서, 새로운 파드가 배치되지 않게 막아줘.

kubectl drain <node-name>

이렇게 하면 파드가 안전하게 다른 노드로 옮겨간 상태에서 노드를 재부팅하거나 업그레이드할 수 있어. 노드가 다시 올라와도 여전히 스케줄 불가 상태야. 그래서 작업이 끝나면 kubectl uncordon 명령으로 스케줄 가능 상태로 풀어줘야 해.

kubectl uncordon <node-name>

주의할 점은, uncordon 한다고 해서 이전에 옮겨간 파드가 자동으로 돌아오진 않아. 그냥 "이제 이 노드에도 새 파드를 배치해도 돼"라고 표시만 하는 거야. 실제로 파드가 이 노드에 다시 오려면 다른 노드에서 파드가 삭제되거나, 새 파드가 스케줄링될 때 이 노드가 선택되어야 해.

그리고 kubectl cordon이라는 명령도 있는데, 이건 drain과 달리 기존 파드를 내쫓지 않아. 단순히 새 파드가 이 노드에 스케줄되지 않도록 표시만 하는 거야. 지금 돌아가는 파드는 그대로 두고, 앞으로의 스케줄링만 막고 싶을 때 쓰는 거지.

정리하면 노드 유지보수 흐름은 이래: drain으로 파드 빼고, 노드 작업하고, uncordon으로 스케줄 다시 열어주고. 5분 타임아웃에 기대서 급하게 재부팅하는 것보다 이 방법이 훨씬 안전해.

노드를 안전하게 다루는 법을 알았으니, 이제 쿠버네티스 버전 체계를 이해해보자. 쿠버네티스 버전 번호는 세 파트로 나뉘어: 메이저.마이너.패치. 예를 들어 1.33.0이면 메이저가 1, 마이너가 33, 패치가 0이야. kubectl get nodes 명령을 실행하면 VERSION 컬럼에서 현재 클러스터 버전을 확인할 수 있어. 마이너 버전은 몇 달에 한 번씩 새로운 기능을 담아서 나오고, 패치 버전은 중요한 버그 수정이 있을 때 더 자주 나와.

첫 번째 메이저 릴리스인 1.0은 2015년 7월에 나왔고, 그 이후로 꾸준히 마이너 버전이 올라가고 있어.

정식 릴리스가 나오기 전에 알파와 베타 단계를 거치는데, 모든 버그 수정과 개선 사항은 먼저 알파 릴리스에 적용돼. 알파 릴리스는 새 기능이 들어가긴 하지만 기본 비활성화 상태이고 버그가 있을 수 있어. 그 다음 베타 릴리스로 넘어가면 코드가 잘 테스트되고, 새 기능이 기본 활성화된 상태로 나와. 그리고 최종적으로 메인 안정(stable) 릴리스에 반영되는 거야.

한 가지 알아둘 건, 쿠버네티스 릴리스 패키지(kubernetes.tar.gz)를 다운로드해서 압축을 풀면 kube-apiserver, kube-controller-manager, kube-scheduler 같은 컨트롤 플레인 컴포넌트가 전부 같은 버전으로 들어 있어. 근데 etcd랑 CoreDNS는 별도 프로젝트라서 자체 버전 체계를 따르거든. 그래서 쿠버네티스 버전과 다를 수 있어. 각 릴리스의 릴리스 노트에 지원되는 etcd, CoreDNS 버전 정보가 나와 있으니까 그걸 참고하면 돼.

릴리스 정보는 쿠버네티스 GitHub 리포지토리의 릴리스 페이지나 공식 문서 페이지에서 확인할 수 있어.

버전 체계를 알았으니, 이제 실제 클러스터 업그레이드 전략을 알아보자. 클러스터 업그레이드에서 제일 먼저 알아야 할 건 컴포넌트 간 버전 호환 규칙이야. kube-apiserver가 컨트롤 플레인의 핵심이고 다른 모든 컴포넌트와 통신하는 녀석이라서, 이게 기준점이 돼. 다른 컴포넌트는 절대 kube-apiserver보다 높은 버전이면 안 돼. 구체적으로 보면, kube-apiserver가 버전 X일 때 kube-controller-manager와 kube-scheduler는 X 또는 X-1까지 허용되고, kubelet과 kube-proxy는 X-2까지 가능해. 예를 들어 kube-apiserver가 1.10이면, controller-manager와 scheduler는 1.10이나 1.9, kubelet과 kube-proxy는 1.10, 1.9, 1.8 중 하나면 돼. 반면에 kubectl은 좀 특이한데, X+1, X, X-1 전부 괜찮아. apiserver보다 한 단계 높은 버전도 쓸 수 있다는 거지.

이런 버전 차이(version skew)가 허용되는 덕분에 실시간으로 컴포넌트를 하나씩 순차적으로 업그레이드하는 롤링 업그레이드가 가능해.

그럼 언제 업그레이드해야 할까? 쿠버네티스는 최신 마이너 버전 3개만 지원해. 예를 들어 1.12가 최신이면 1.12, 1.11, 1.10까지 지원하고, 1.13이 나오면 1.10은 지원이 끊겨. 그래서 1.13이 나오기 전에 업그레이드하는 게 좋아.

업그레이드할 때 주의할 건, 한 번에 여러 마이너 버전을 건너뛰면 안 된다는 거야. 1.10에서 1.13으로 바로 가는 게 아니라, 1.10 -> 1.11 -> 1.12 -> 1.13 이렇게 한 단계씩 올려야 해.

업그레이드 절차는 클러스터 환경에 따라 달라. GKE 같은 관리형 쿠버네티스면 UI에서 클릭 몇 번이면 되고, kubeadm으로 설치한 클러스터면 kubeadm이 도와주고, 직접 처음부터 구축한 클러스터면 수동으로 각 컴포넌트를 업그레이드해야 해.

kubeadm 기준으로 설명하면, 업그레이드는 크게 두 단계야. 먼저 마스터(컨트롤 플레인)를 업그레이드하고, 그 다음에 워커 노드를 업그레이드해.

마스터를 업그레이드하는 동안 API 서버, 스케줄러, 컨트롤러 매니저가 잠깐 내려가. 하지만 워커 노드의 파드와 애플리케이션은 계속 돌아가니까 사용자한테 영향은 없어. 다만 이 시간 동안 kubectl로 뭘 하거나, 새 파드를 만들거나, 기존 애플리케이션을 삭제하거나 수정하는 건 안 돼. 컨트롤러 매니저도 안 돌아가니까 파드가 죽어도 자동 복구가 안 돼. 하지만 노드와 파드가 살아 있는 한 애플리케이션은 정상 동작하고 사용자는 영향받지 않아. 업그레이드가 끝나면 정상 복구되고, 이 시점에서 마스터는 새 버전이고 워커 노드는 이전 버전인 상태인데, 이건 앞서 말한 버전 호환 규칙에 의해 허용되는 구성이야.

워커 노드 업그레이드 전략은 세 가지가 있어.

첫째, 전부 한꺼번에 업그레이드하는 방법. 파드가 전부 내려가니까 다운타임이 생겨. 업그레이드가 끝나면 노드가 다시 올라오고 파드가 스케줄되면서 사용자가 다시 접속할 수 있어.

둘째, 한 노드씩 순차적으로 업그레이드하는 방법. 첫 번째 노드를 drain 해서 워크로드를 다른 노드로 옮기고, 업그레이드하고, 다시 올리고, 다음 노드로 넘어가는 식이야. 다운타임 없이 할 수 있어.

셋째, 새 버전이 설치된 새 노드를 추가하고, 워크로드를 옮긴 다음 기존 노드를 제거하는 방법. 클라우드 환경에서 노드를 쉽게 프로비저닝하고 폐기할 수 있을 때 특히 편리해.

실제 kubeadm 업그레이드 흐름을 보면, 먼저 kubeadm upgrade plan을 실행해. 이 명령은 현재 클러스터 버전, kubeadm 도구 버전, 최신 안정 버전, 모든 컨트롤 플레인 컴포넌트와 업그레이드 가능 버전을 보여줘. 그리고 kubelet은 kubeadm이 업그레이드해주지 않으니 수동으로 해야 한다고 알려줘. 마지막으로 클러스터를 업그레이드하는 명령도 알려주고.

여기서 중요한 건, 클러스터를 업그레이드하려면 kubeadm 도구 자체를 먼저 업그레이드해야 한다는 거야. kubeadm도 쿠버네티스와 같은 버전 체계를 따르거든. 그래서 순서가 이래: kubeadm 업그레이드 -> kubeadm upgrade apply -> kubelet 업그레이드.

kubeadm upgrade plan
kubeadm upgrade apply v1.12.0

kubectl get nodes를 실행했을 때 버전이 안 바뀌어 있을 수 있는데, 이 명령이 보여주는 건 API 서버 버전이 아니라 각 노드의 kubelet 버전이거든. 그래서 kubelet을 업그레이드하고 재시작해야 비로소 버전이 바뀌어 보여.

마스터 노드에도 kubelet이 있을 수 있어. kubeadm으로 배포한 클러스터에서는 마스터 노드의 kubelet이 컨트롤 플레인 컴포넌트를 파드로 실행하는 데 사용되거든. apt 같은 패키지 매니저로 kubelet 패키지를 업그레이드하고 서비스를 재시작하면 돼.

워커 노드도 마찬가지야. kubectl drain으로 해당 노드의 파드를 안전하게 빼내고, kubeadm과 kubelet 패키지를 업그레이드하고, kubeadm upgrade node로 노드 설정을 업데이트하고, kubelet을 재시작하고, kubectl uncordon으로 스케줄링을 다시 열어주면 돼. drain 할 때 노드가 예약 불가로 표시되니까, uncordon을 꼭 해줘야 해. 그리고 uncordon 했다고 파드가 바로 돌아오는 건 아니야. 스케줄 가능으로만 표시되는 거지. 이걸 모든 워커 노드에 하나씩 반복하면 클러스터 전체 업그레이드가 완료돼.

이론을 알았으니 실제로 해보자. 이 데모에서는 kubeadm을 이용해서 쿠버네티스 클러스터를 1.28에서 1.29로 업그레이드하는 전체 과정을 실제로 보여줘. 쿠버네티스 공식 문서의 Tasks > Cluster Administration > kubeadm 관리 아래에 "Upgrading kubeadm clusters" 항목이 있어. 업그레이드 경로별로(1.28->1.29, 1.27->1.28 등) 문서가 따로 있지만 절차는 거의 동일하니까, 자기 상황에 맞는 걸 골라서 따라하면 돼.

시작하기 전에 꼭 알아둘 게 하나 있어. 패키지 리포지토리가 바뀌었거든. 예전에는 apt.kubernetes.ioyum.kubernetes.io를 썼는데, 이게 deprecated 됐어. 이제는 pkgs.k8s.io를 써야 해. 그리고 마이너 버전마다 별도의 리포지토리가 있어. 1.28용, 1.29용, 1.27용 이렇게. 그래서 업그레이드 초기 단계에서 리포지토리 URL의 버전을 업그레이드할 버전(예: v1.29)으로 바꿔줘야 하는 거야.

데모 환경은 노드가 2개야. 컨트롤 플레인 하나, 워커 노드 하나. kubectl get nodes로 확인할 수 있고, cat /etc/*release*로 보면 Ubuntu 20.04 기반이야. 노드가 2개든 3개든 10개든 절차는 똑같아.

먼저 패키지 리포지토리를 변경해야 해. 공식 문서에서 명령 두 개를 복사해. 하나는 리포지토리 정의 파일을 새 걸로 교체하는 거고, 다른 하나는 공용 서명 키를 다운로드하는 거야. 둘 다 URL에 있는 버전 부분을 v1.29로 바꿔줘야 해. 이 두 명령을 컨트롤 플레인 노드와 워커 노드 모두에서 실행해. 노드가 여러 개면 전부 다 해줘야 하고.

그 다음 업그레이드할 구체적인 패치 버전을 확인해. apt update 하고 apt-cache madison kubeadm을 실행하면 사용 가능한 버전 목록이 나와. 여기서 1.29.3-1.1 같은 최신 패치 버전을 골라.

이제 본격적인 업그레이드야. 컨트롤 플레인 노드부터 시작해. 먼저 kubeadm 도구 자체를 업그레이드해.

apt-mark unhold kubeadm
apt-get update && apt-get install -y kubeadm=1.29.3-1.1
apt-mark hold kubeadm

kubeadm version으로 확인하면 1.29.3이 보여야 해. 그 다음 sudo kubeadm upgrade plan을 실행해. 이건 예행연습 같은 건데, 현재 클러스터 버전, 업그레이드 가능 버전, 호환성 문제가 없는지 확인해줘. 출력을 보면 kubeadm이 자동으로 업그레이드하는 컴포넌트가 뭐고, 수동으로 해야 하는 게 뭔지(kubelet) 알려줘. 1.28의 최신 패치로 갈 수도 있고, 1.29로 점프할 수도 있다는 걸 보여주는데, 우리는 1.29.3으로 갈 거야.

문제없으면 sudo kubeadm upgrade apply v1.29.3을 실행해. "upgrade successful"이 뜨면 컨트롤 플레인 컴포넌트 업그레이드가 된 거야. 근데 kubectl get nodes 해보면 여전히 1.28.0으로 나와. 왜냐면 이 명령의 VERSION 컬럼은 kubelet 버전을 가져오는 거거든. kubelet은 아직 업그레이드 안 했으니까.

kubelet을 업그레이드하려면 먼저 컨트롤 플레인 노드를 drain 해야 해. 이 노드 위에서도 DNS 파드나 컨트롤 플레인 컴포넌트 파드가 돌고 있으니까. kubelet 프로세스를 건드리면 파드가 날아갈 수 있어서, 먼저 빼내는 거야.

kubectl drain controlplane --ignore-daemonsets

그 다음 kubelet과 kubectl 패키지를 업그레이드하고 kubelet을 재시작해.

apt-mark unhold kubelet kubectl
apt-get update && apt-get install -y kubelet=1.29.3-1.1 kubectl=1.29.3-1.1
apt-mark hold kubelet kubectl
sudo systemctl daemon-reload
sudo systemctl restart kubelet

이제 kubectl get nodes 하면 컨트롤 플레인 노드가 1.29.3으로 보여. 근데 SchedulingDisabled 상태니까 uncordon 해줘야 해.

kubectl uncordon controlplane

컨트롤 플레인 노드가 여러 개면 나머지도 같은 식으로 하되, 처음 한 대에서만 kubeadm upgrade apply를 쓰고 나머지에서는 kubeadm upgrade node를 써. 각 노드를 drain하고, kubeadm과 kubelet을 업그레이드하고, kubelet 재시작하고, uncordon 하는 흐름은 동일해.

워커 노드 차례야. 워커 노드에 SSH로 접속해서 kubeadm을 업그레이드하고 sudo kubeadm upgrade node를 실행해. apply가 아니라 node인 거 주의해. 그 다음 컨트롤 플레인 노드에서 해당 워커를 drain 해.

kubectl drain node01 --ignore-daemonsets

다시 워커 노드에서 kubelet과 kubectl을 업그레이드하고 kubelet을 재시작해. 그리고 컨트롤 플레인에서 uncordon.

kubectl uncordon node01

kubectl get nodes 하면 두 노드 모두 1.29.3으로 나와야 해. 워커 노드가 여러 개면 첫 번째 워커 업그레이드가 끝난 후 다음 워커에서 똑같이 반복하면 돼. kubeadm 업그레이드 -> kubeadm upgrade node -> drain -> kubelet 업그레이드 -> kubelet 재시작 -> uncordon. 이걸 모든 워커 노드에 하나씩 해서 전체 클러스터가 새 버전으로 올라갈 때까지 반복하면 끝이야.

업그레이드까지 했으면, 이제 만약의 사태에 대비한 백업을 알아보자. 쿠버네티스 클러스터에서 백업을 고려해야 할 대상은 크게 세 가지야: 리소스 설정(definition files), etcd 클러스터, 그리고 영구 스토리지(Persistent Storage). 이 중에서 리소스 설정과 etcd 백업이 핵심이야.

리소스 설정 백업부터 보자. 제일 좋은 건 선언적 방식으로 모든 리소스를 정의 파일로 만들어서 관리하는 거야. 단일 애플리케이션에 필요한 모든 오브젝트를 하나의 폴더에 정의 파일 형태로 저장하면 나중에 쉽게 재사용하거나 다른 사람들과 공유할 수 있거든. 이 파일들을 GitHub 같은 소스 코드 저장소에 넣어두면 팀에서 유지관리할 수 있고, 클러스터를 통째로 잃어도 kubectl apply -f 한 방이면 복구할 수 있어.

근데 현실적으로 모든 팀원이 항상 정의 파일을 만들어서 작업하진 않잖아. 누군가 kubectl create 같은 명령형 방식으로 네임스페이스나 시크릿이나 컨피그맵을 만들고 기록을 안 남겼을 수도 있어. 그래서 더 확실한 방법은 kube-apiserver를 직접 쿼리해서 클러스터에 있는 모든 리소스 설정을 뽑아내는 거야.

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

이렇게 하면 모든 네임스페이스의 파드, 디플로이먼트, 서비스 등을 yaml로 뽑을 수 있어. 다만 이건 일부 리소스 그룹만 커버하는 거고, Role이나 PV, CRD 같은 다른 리소스 타입도 별도로 고려해야 해. 이런 작업을 직접 스크립트로 짜는 대신 Velero(이전 이름 Heptio Ark) 같은 도구를 쓸 수도 있어. Kubernetes API를 통해 클러스터 리소스를 체계적으로 백업해줘.

이제 etcd 백업이야. etcd는 클러스터의 모든 상태 정보를 저장하는 곳이잖아. 클러스터 자체, 노드 정보, 클러스터 내에서 생성된 모든 리소스 정보가 전부 여기 들어 있어. 그래서 리소스를 하나하나 백업하는 대신 etcd 서버 자체를 백업하면 클러스터 전체 상태를 보존할 수 있어.

etcd를 구성할 때 --data-dir로 데이터가 저장되는 디렉터리를 지정했을 텐데, 이 디렉터리를 백업 도구로 백업할 수도 있어. 하지만 etcd에는 내장 스냅샷 기능이 있어서 이걸 쓰는 게 더 깔끔해.

ETCDCTL_API=3 etcdctl snapshot save snapshot.db

이렇게 하면 현재 디렉터리에 스냅샷 파일이 생겨. 다른 경로에 저장하고 싶으면 전체 경로를 지정하면 돼. 백업 상태는 etcdctl snapshot status 명령으로 확인할 수 있고.

복원할 때는 이래. etcd에 kube-apiserver가 의존하니까, 먼저 kube-apiserver 서비스를 중지시켜. 그 다음 스냅샷 복원 명령을 실행해.

ETCDCTL_API=3 etcdctl snapshot restore snapshot.db --data-dir /var/lib/etcd-from-backup

이 명령은 새 데이터 디렉터리를 만들면서 새 클러스터 설정을 초기화해. 왜 새 디렉터리를 만드냐면, 복원된 멤버가 실수로 기존 클러스터에 합류하는 걸 방지하기 위해서야. 복원 후에는 etcd 설정 파일에서 --data-dir을 이 새 디렉터리 경로(/var/lib/etcd-from-backup)로 바꿔주고, 서비스 데몬을 리로드한 다음 etcd 서비스를 재시작하고, 마지막으로 kube-apiserver를 다시 시작하면 클러스터가 원래 상태로 돌아와.

etcdctl 명령을 쓸 때 꼭 주의할 게 하나 있어. 인증을 위한 인증서 파일을 항상 지정해야 해. etcd 클러스터의 엔드포인트, CA 인증서, 서버 인증서, 키 파일을 옵션으로 넘겨줘야 하거든.

두 가지 백업 방식 중에 뭘 쓸지는 환경에 따라 달라. 관리형 쿠버네티스(EKS, GKE 같은)를 쓰면 etcd에 직접 접근이 안 될 수 있어. 그런 경우에는 kube-apiserver를 쿼리하는 방식이나 Velero 같은 도구를 쓰는 게 맞아. 직접 구축한 클러스터라면 etcd 스냅샷이 가장 확실한 방법이고.


정리

14장 읽고 기억할 거 세 가지:

  1. 노드 유지보수는 drain -> 작업 -> uncordon 흐름이 정석이고, 레플리카셋 없는 단독 파드는 drain 없이 노드가 내려가면 영영 사라진다
  2. 클러스터 업그레이드는 마이너 버전 한 단계씩, 컨트롤 플레인 먼저 하고 워커 노드를 순차적으로 하며, kubectl get nodes의 VERSION은 kubelet 버전이라 kubelet까지 업그레이드해야 반영된다
  3. 백업은 리소스 설정(kubectl/Velero)etcd 스냅샷 두 가지가 핵심이고, 관리형 클러스터면 API 쿼리 방식, 자체 구축이면 etcd 스냅샷이 적합하다