Kubespray と Terraform でクラスターを構築する
インベントリは役割の割り当て表、group_vars はインストール内容
한국어 원문으로 표시합니다.
한 줄 요약
kubespray 는 쿠버네티스를 설치하는 Ansible 플레이북 묶음이고, 사람이 쓰는 것은 인벤토리(누가 무엇을 맡는가)와 group_vars(무엇을 설치하는가) 두 가지뿐입니다.
왜 이게 필요했나
kubeadm 은 노드 한 대 위에서 컨트롤 플레인을 조립하는 도구입니다. 서버가 열 대라면 누군가 열 대에 런타임을 깔고, 커널 설정을 맞추고, 첫 대에서 kubeadm init 을 하고, 나머지에서 순서대로 join 을 해야 합니다. 이 일을 셸 스크립트로 쓰기 시작하면 금방 "두 번 돌리면 망가지는 스크립트" 가 됩니다. kubespray 는 그 반복을 Ansible 역할(role)로 옮긴 것입니다. 공식 비교 문서는 kubespray 가 "OS 쪽의 일반적인 구성 관리" 를 맡고, 클러스터 수명 주기에 대한 지식은 v2.3 부터 안에서 kubeadm 을 불러 빌려 온다고 설명합니다. 즉 kubespray 로 세운 컨트롤 플레인도 속은 kubeadm 이 만든 정적 파드이고, 차이는 그 앞뒤의 호스트 준비·etcd·CNI·애드온을 누가 해 주느냐입니다.
어떻게 동작하나
인벤토리는 역할 배정표입니다. kubespray 문서가 정한 그룹은 셋입니다.
kube_control_plane API 서버·스케줄러·컨트롤러 매니저가 뜨는 노드
kube_node 파드가 올라가는 노드(워커)
etcd etcd 멤버. 장애 대비에는 3대 이상, 반드시 홀수
k8s_cluster kube_node + kube_control_plane (+ calico_rr) — kubespray 가 실행 중에 만든다
k8s_cluster 에는 함정이 하나 있습니다. v2.32.0 의 샘플 inventory.ini 는 이 그룹을 적지 않고, boilerplate.yml 의 dynamic_groups 역할이 플레이북 안에서 group_by 로 만듭니다. 그래서 플레이북 밖의 도구는 이 그룹을 모릅니다. 이 VM 에서 실측하면 group_vars/k8s_cluster/k8s-cluster.yml 에 kube_version 을 적어도 ansible-inventory --host node1 은 그 값을 null 로 보여 주고, ansible ... -m debug 로 물으면 group_vars/all 의 값이 이깁니다. 설치 자체는 문제없이 되지만, 설치 전에 값을 확인하려던 도구가 거짓말을 합니다. 이 코스는 그래서 [k8s_cluster:children] 에 kube_control_plane 과 kube_node 를 적어 둡니다 — 옛 샘플이 하던 방식이고, kubespray 가 실행 중에 같은 그룹을 다시 만들어도 결과는 같습니다.
한 노드가 kube_control_plane 과 kube_node 에 모두 있으면 컨트롤 플레인이 일도 합니다. etcd 를 따로 두지 않으려면 [etcd:children] 으로 kube_control_plane 을 통째로 넣습니다(stacked etcd). 이 코스는 VM 한 대라 세 그룹에 같은 노드 하나를 넣습니다.
그룹 이름은 철자까지 계약입니다. 플레이북 맨 앞의 boilerplate.yml 이 dynamic_groups 역할로 옛 이름(kube-master, kube-node)만 새 이름으로 옮겨 주고, 그 밖의 이름은 모릅니다. 그다음 validate_inventory 역할이 인벤토리를 검사하는데, 실측해 보면 [masters] 로 적었을 때 "kube_control_plane 이 비었으면 멈춘다" 는 검사는 오히려 통과합니다. 없는 그룹을 groups.get() 이 None 으로 돌려주고, None 은 빈 목록과 다르기 때문입니다. 실패는 바로 다음 검사인 "etcd 수가 짝수면 멈춘다" 에서 object of type 'dict' has no attribute 'kube_control_plane' 로 납니다. 오류가 그룹 이름을 말해 주지 않으니, 설치를 돌리기 전에 ansible-inventory --graph 로 트리를 눈으로 보는 것이 가장 싼 확인입니다.
group_vars 는 설치 내용입니다. 샘플은 두 갈래로 나뉩니다.
group_vars/all/*.yml etcd 를 포함한 모든 노드 — 프록시, 오프라인 저장소, etcd 설정
group_vars/k8s_cluster/*.yml 클러스터 노드 — kube_version, container_manager, kube_network_plugin, 대역, 애드온
group_vars/kube_control_plane.yml 컨트롤 플레인만
kubespray 문서의 변수 층 표는 짧습니다. 인벤토리의 group_vars 가 가장 많이 쓰이고, host_vars 는 노드별 예외이며, extra vars(-e)는 언제나 이긴다. 그리고 -e 는 kubespray 가 사용자에게 약속하지 않은 내부 변수를 덮을 때 쓰라고 적어 둡니다. Ansible 규칙상 같은 이름이 group_vars/all 과 group_vars/k8s_cluster 에 모두 있으면 더 구체적인 자식 그룹(k8s_cluster)이 이깁니다. 그래서 누군가 all.yml 에 판 번호를 적어 두면, 그 줄은 아무 일도 하지 않으면서 다음 사람을 속입니다.
ansible-core 2.19(Ansible 12)부터는 조건식이 반드시 불리언이어야 합니다. -e key=value 는 언제나 문자열을 넘기므로, v2.32.0 릴리스 노트는 불리언을 -e '{"drain_nodes": true}' 처럼 JSON 으로 넘기라고 적었습니다.
판은 체크섬이 정합니다. kube_version 의 기본값은 roles/kubespray_defaults/vars/main/checksums.yml 에 있는 kubelet 체크섬 목록의 첫 키이고, 받아 주는 최소 판은 마지막 키입니다. v2.32.0 에서는 기본이 1.36.4, 최소가 1.34.0 입니다. 체크섬이 없는 판은 적어도 설치되지 않습니다. 그리고 kube_version 을 인벤토리에 적지 않으면 kubespray 를 새 태그로 올리는 순간 쿠버네티스도 같이 올라갑니다. 이 코스는 1.35.8 을 적어 두고, 업그레이드 모듈에서 1.36.4 로 한 칸 올립니다.
현장에서 만나는 모습
첫 번째는 디렉터리 함정입니다. kubespray 저장소의 ansible.cfg 는 inventory_ignore_extensions 에 .ini 를 넣어 두었습니다. 그래서 -i inventory/mycluster 처럼 디렉터리를 넘기면 inventory.ini 를 건너뛰고 "No inventory was parsed" 경고만 남긴 채 호스트 0개로 돕니다. 이 VM 에서 실측한 결과이고, 공식 문서의 예시가 늘 -i inventory/mycluster/inventory.ini 로 파일을 가리키는 이유이기도 합니다.
두 번째는 연결 방식을 어디에 적느냐입니다. 노드가 한 대일 때는 node1 ansible_connection=local 처럼 인벤토리 줄에 붙여도 똑같이 돕니다. 그런데 노드를 늘리는 날 그 줄을 복사하면 새 노드도 local 로 연결되어, 제어 노드 자신에게 두 번 설치하는 사고가 납니다. 연결 정보를 host_vars/<노드>.yml 로 빼 두면 인벤토리는 역할 배정표로만 남고, 노드를 더할 때는 그룹에 이름 한 줄과 host_vars 파일 하나(ansible_host, ansible_user)를 더하면 됩니다. 이 코스의 모든 실습이 이 모양을 씁니다.
세 번째는 제어 노드의 Ansible 판입니다. kubespray 는 태그마다 requirements.txt 로 Ansible 을 고정하고, v2.32.0 은 ansible==12.3.0(ansible-core 2.19)을 요구합니다. 문서는 가상환경에 그대로 깔라고 권하고, 맞지 않는 판이면 ansible_version.yml 이 첫 작업에서 멈춥니다. 시스템 패키지의 Ansible 로 돌리다 막히는 일이 흔합니다.
다음 실습에서 할 것
샘플을 복사해 노드 한 대를 세 그룹에 넣고, 연결 방식을 host_vars 로 뺍니다. 디렉터리와 파일을 넘겼을 때 호스트 수가 어떻게 다른지 세어 보고, 판을 group_vars 에 고정한 뒤 all·k8s_cluster·-e 중 누가 이기는지 확인합니다. 마지막으로 그룹 이름을 틀린 인벤토리가 어디서 어떤 말로 멈추는지 보고, 내 인벤토리가 boilerplate 검사를 통과하는지 확인합니다.