Building clusters with Kubespray and Terraform
Where Terraform meets Kubespray — templates before, wrapping in the middle, declarations after
한국어 원문으로 표시합니다.
한 줄 요약
Terraform 은 "무엇이 있어야 하는가" 를 state 로 기억하고 차이만 바꾸는 도구라서, kubespray 앞에서는 인벤토리를 만드는 일을, 뒤에서는 클러스터 위의 자원을 선언하고 drift 를 잡는 일을 맡기고, 가운데의 설치는 kubespray 에 맡기는 것이 경계입니다.
왜 이게 필요했나
kubespray 의 입력은 인벤토리인데, 인벤토리의 주소와 이름은 노드를 만든 쪽이 압니다. 클라우드에서 VM 을 Terraform 으로 만들었다면 그 출력(IP, 이름, 역할)으로 인벤토리를 만드는 것이 자연스럽고, kubespray 저장소의 contrib/terraform 에도 aws·gcp·openstack·vsphere 같은 클라우드별 예제가 들어 있습니다. 반대쪽도 같습니다. 클러스터가 선 뒤 팀마다 네임스페이스와 권한과 기본 앱을 깔아 주는 일은 반복되고, 누가 손으로 바꾼 것을 알아채야 합니다. Terraform 의 state 와 plan 은 정확히 그 일을 합니다. 문제는 가운데입니다. 쿠버네티스 설치는 수백 개의 작업이 순서를 지켜야 하는 절차이고, 이미 kubespray 가 멱등하게 해 줍니다. 그것을 Terraform 리소스로 다시 쓰는 것은 두 번 일하는 셈입니다.
어떻게 동작하나
앞: 인벤토리를 템플릿으로. templatefile() 은 파일 하나를 읽어 변수로 채웁니다. 노드 맵(name → {control_plane, worker, connection})을 변수로 두고 %{ for }·%{ if } 지시문으로 그룹을 채우면, 노드를 늘리는 일은 맵에 한 줄을 더하는 일이 됩니다. local_file 이 파일을 쓰고, state 에 그 내용이 남습니다. 그래서 누가 파일을 손으로 고치면 다음 plan 이 되돌리겠다고 나옵니다 — 이 파일의 주인이 Terraform 이 되었다는 뜻이고, 판(kube_version)처럼 여러 곳에 적히기 쉬운 값의 주인을 하나로 정하는 방법이기도 합니다.
가운데: 감싸기만 한다. terraform_data 는 아무 인프라도 만들지 않는 내장 리소스입니다. triggers_replace 에 넣은 값이 바뀌면 다시 만들어지고, 만들어질 때 local-exec 프로비저너가 명령을 실행합니다. 여기에 인벤토리·판·host_vars 파일 내용을 넣고 명령으로 ansible-playbook cluster.yml 을 두면 "선언이 바뀔 때만 kubespray 를 부른다" 가 됩니다. 실행이 실패하면 리소스가 tainted 로 남아 다음 apply 에서 다시 부릅니다.
한계도 여기서 드러납니다. 판을 1.35.8 → 1.36.4 로 바꾸면 plan 은 판 파일과 terraform_data 를 다시 만들겠다고 하고, 그러면 불리는 것은 cluster.yml 입니다. kubespray 의 업그레이드는 upgrade-cluster.yml(cordon·drain·한 칸씩)이어야 합니다. Terraform 은 "무엇이 바뀌었나" 는 알아도 "그 변화를 어떤 절차로 적용해야 하나" 는 모릅니다. 그래서 설치는 감싸되, 업그레이드·노드 제거처럼 절차가 다른 작업은 파이프라인이나 사람이 kubespray 플레이북을 골라 부르게 두는 것이 안전합니다.
뒤: 클러스터 위를 선언으로. kubernetes 프로바이더는 kubeconfig 로 API 서버에 붙어 kubernetes_namespace_v1·kubernetes_service_account_v1·kubernetes_role_v1·kubernetes_role_binding_v1 같은 리소스를 만들고, helm 프로바이더는 helm_release 로 차트를 설치합니다. helm 프로바이더 3.x 에서는 쿠버네티스 연결 설정이 kubernetes = { ... } 속성이고 값은 set = [{ name, value }] 목록입니다. 클러스터를 만드는 루트 모듈과 이 모듈을 나누는 것이 좋습니다 — 한 모듈에서 클러스터를 만들고 같은 실행에서 그 클러스터로 프로바이더를 설정하면, 첫 plan 때 아직 없는 kubeconfig 를 읽어야 해서 순서가 꼬입니다.
drift. tofu plan 은 먼저 실제 상태를 읽어 state 를 새로 하고(refresh), 선언과 비교합니다. 누가 kubectl label 로 네임스페이스 라벨을 바꾸면 plan 이 "선언대로 되돌리겠다" 고 나오고, -detailed-exitcode 가 2 를 돌려줍니다. 이 종료 코드를 정기 작업에 걸면 drift 알림이 됩니다. 다만 helm_release 는 릴리스 메타데이터(차트·값)를 비교하므로, 차트가 만든 Deployment 를 밖에서 고친 것까지 늘 잡지는 않습니다. 무엇을 어느 도구가 감시하는지 알아야 합니다.
현장에서 만나는 모습
흔한 설계는 셋으로 나뉜 저장소입니다. 노드를 만드는 Terraform(클라우드 계정별), 인벤토리를 받아 kubespray 를 부르는 파이프라인, 클러스터 위를 선언하는 Terraform(팀별). 이 실습은 이 셋을 한 VM 에 압축했습니다. 노드를 만드는 첫 층은 여기서 하지 않습니다 — 이 플랫폼에서 그 층은 VM 을 만드는 가상화 API 인데, 실습 안에서 그 API 를 부르게 하면 수강생 사이의 격리가 깨집니다. 현장에서는 클라우드 프로바이더나 vSphere·OpenStack 프로바이더가 그 자리에 옵니다.
state 도 조심할 대상입니다. 이 실습은 state 를 로컬 파일로 두지만, 팀이 쓰면 원격 백엔드와 잠금이 필요하고, state 안에는 파일 내용과 리소스 속성이 평문으로 들어가므로 비밀을 넣지 않아야 합니다.
다음 실습에서 할 것
노드 맵과 템플릿으로 인벤토리·host_vars·판 파일을 만들고 apply 해, 그 파일들의 주인이 Terraform 이 되는 것을 봅니다. terraform_data 로 kubespray 를 감싸 클러스터를 세우고, 판을 바꾸면 무엇이 다시 불리는지 plan 으로 확인합니다. 다른 루트 모듈에서 네임스페이스·RBAC·helm 릴리스를 선언하고, 밖에서 바꾼 라벨을 plan 으로 잡아 되돌립니다.