LabHub
시작하기
배우기 러닝패스 코스

kubespray 와 Terraform 으로 클러스터 세우기

같은 플레이북을 다시 돌리는 것이 운영이다

LabHub 에서 이어서 보기

한 줄 요약

cluster.yml 은 호스트 준비 → 런타임 → 받기 → etcd → kubelet → 컨트롤 플레인 → CNI → 애드온 순서로 도는 play 15개이고, 같은 인벤토리로 몇 번을 다시 돌려도 같은 클러스터가 되도록 만들어져 있습니다.

왜 이게 필요했나

kubeadm 으로 한 대를 세우는 일은 명령 몇 줄이면 끝납니다. 어려운 것은 그다음입니다. 석 달 뒤 누가 containerd 설정 하나를 바꿔야 할 때, 그 사람은 처음에 무엇을 어떤 순서로 했는지 알아야 하고, 노드마다 같은 결과를 내야 합니다. 셸 스크립트는 처음 한 번은 잘 돌지만 두 번째에는 이미 있는 파일과 이미 떠 있는 데몬에 부딪힙니다. kubespray 는 설치를 "현재 상태를 보고 모자란 것만 채우는" Ansible 작업으로 썼기 때문에, 설치 명령과 운영 명령이 같습니다. 설정을 바꾸든 노드를 더하든 같은 cluster.yml 을 다시 돌립니다.

어떻게 동작하나

--list-tasks 로 펼쳐 보면 v2.32.0 의 cluster.yml 은 play 15개입니다(실측 4초, 아무것도 바꾸지 않습니다). 순서에 이유가 있습니다.

#1-2   Ansible 판 확인, 인벤토리 검사(boilerplate)            — 틀린 인벤토리는 여기서 1초 만에 멈춘다
#4-5   호스트 부트스트랩, 사실(facts) 수집
#6     etcd 준비: preinstall(스왑·sysctl·패키지) → 컨테이너 런타임 → 받기(download)
#8     etcd 설치 — 기본은 컨테이너가 아니라 호스트의 systemd 서비스(etcd_deployment_type: host)
#9     kubelet 설치
#10    컨트롤 플레인 — 안에서 kubeadm init 을 부른다
#11    kubeadm 마무리, 노드 라벨·테인트, CNI(기본 calico)
#14    애드온 — CoreDNS, nodelocaldns, (켰다면) metrics-server 등
#15    클러스터 DNS 가 뜬 뒤 resolv.conf 정리

etcd 가 컨트롤 플레인보다 먼저 오는 것은 API 서버가 저장소 없이는 뜨지 않기 때문이고, CNI 가 애드온보다 먼저 오는 것은 CoreDNS 가 파드 네트워크 없이는 뜨지 않기 때문입니다. 이 순서를 알면 실패한 지점만 보고도 무엇이 이미 있고 무엇이 없는지 짐작할 수 있습니다.

로그는 끝에서부터 읽습니다. PLAY RECAP 의 한 줄이 판정입니다 — ok, changed, unreachable, failed, skipped. 저장소의 ansible.cfg 가 profile_tasks 콜백을 켜 두어 그 아래에 TASKS RECAP 이 붙는데, 머리 줄의 누적 시간이 전체 시간이고 아래 목록이 오래 걸린 작업 순서입니다. 중간의 fatal: 은 판정이 아닙니다. 예를 들어 첫 설치 때 Get currently-deployed etcd version 은 etcd 가 아직 없으니 실패하는 것이 정상이고, kubespray 는 그 결과를 ...ignoring 으로 넘깁니다.

멱등은 changed=0 이 아닙니다. Ansible 의 모듈 대부분은 "원하는 상태와 지금 상태를 비교해 다를 때만 바꾼다" 로 동작하므로 두 번째 실행에서는 대부분 ok 로 끝납니다. 그런데 command·shell 로 부르는 작업은 비교할 방법이 없어 돌 때마다 changed 를 내거나, 일부러 changed_when 으로 누르거나 둘 중 하나입니다. 그래서 두 번째 실행의 changed 목록은 "매번 바뀌는 것" 의 목록이고, 그걸 알아야 세 번째 실행에서 새로 생긴 changed 가 내 변경인지 아닌지를 가를 수 있습니다.

태그로 부분만 돌릴 수 있습니다. kubespray 문서의 태그 표에는 containerd, etcd, network, apps, coredns, metrics_server 같은 이름이 있고, --tags containerd 를 주면 그 태그가 붙은 작업과 always 태그(인벤토리 검사·facts)만 돕니다. 문서는 태그를 "무엇을 하는지 100% 확신할 때만" 쓰라고 경고합니다. 역할 사이의 의존(예: containerd 설정을 바꾸면 재시작 핸들러가 불린다)이 태그 밖에 있으면 빠지기 때문입니다.

현장에서 만나는 모습

이 코스를 만들며 medium VM(4 vCPU · 4 GiB)에서 실측한 값입니다. 깨끗한 VM 에서 cluster.yml 첫 실행은 341초(changed 132), 같은 명령의 두 번째 실행은 183초(changed 27)였습니다. 설치 중 VM 전체의 메모리 사용(MemTotal − MemAvailable)은 최대 1.52GiB 였고, Ansible 프로세스들이 그 가운데 최대 345MB 를 차지했습니다. 4 GiB 로 충분하다는 뜻입니다. 받는 것은 github.com 릴리스·dl.k8s.io·registry.k8s.io·quay.io 와 우분투 apt 저장소였고, 모두 HTTPS(443)나 HTTP(80)로만 받았습니다.

실측 중 두 번 넘어졌고, 둘 다 현장에서 그대로 만나는 모양입니다. 첫째, 루트를 아끼려고 /usr/local 을 통째로 다른 디스크에 바인드했더니 /usr/local/share/ca-certificates 가 가려져 etcd 인증서 단계가 Destination directory ... does not exist 로 멈췄습니다. 둘째, 플레이북을 HOME 이 비어 있는 환경(systemd 유닛)에서 돌렸더니 kubespray 의 kube 모듈이 --kubeconfig 없이 kubectl 을 불러 localhost:8080 으로 갔고, cluster_roles 단계가 1분을 재시도하다 멈췄습니다. 이 둘째 실패는 CNI 설치 전이었는데, 그 상태로 cluster.yml 을 다시 돌리자 이번에는 "Wait for new control plane nodes to be Ready" 가 130초를 기다리다 실패했습니다. kubeadm 이 이미 돈 노드에서는 CNI 없이 Ready 를 기다리는 작업이 먼저 오기 때문입니다. 반쯤 세워진 클러스터는 다시 돌려서 고쳐지지 않을 수 있고, 그때는 reset.yml 로 지우고 처음부터 세우는 편이 빠릅니다(reset 은 실측 78초).

다음 실습에서 할 것

플레이 순서를 먼저 펼쳐 보고, cluster.yml 을 돌려 클러스터를 세웁니다. 로그 끝에서 결과와 시간을 읽고, 같은 명령을 한 번 더 돌려 두 번째 실행이 바꾼 작업을 모읍니다. 마지막으로 containerd 설정 값 하나를 group_vars 로 바꾸고 --tags containerd 로 그 부분만 다시 적용해, 설정 파일과 데몬이 모두 바뀌었는지 확인합니다.