Terraform 生成 inventory、调用 Kubespray,再在其上部署
한국어 원문으로 표시합니다.
목표
같은 VM 안에서 OpenTofu 가 kubespray 인벤토리를 템플릿으로 만들고, kubespray 실행을 감싸 클러스터를 세우고, 세운 클러스터에 kubernetes·helm 프로바이더로 네임스페이스·RBAC·애플리케이션을 선언합니다. 밖에서 바뀐 것을 plan 으로 잡아 되돌립니다.
왜 중요한가
현장의 클러스터 수명 주기는 보통 세 층입니다 — 노드를 만드는 층(클라우드·가상화), 노드 위에 쿠버네티스를 까는 층(kubespray), 클러스터 위에 팀의 공간과 권한과 앱을 까는 층. Terraform 은 첫 층과 셋째 층에 강하고, 둘째 층은 Ansible 에 맡기는 것이 일반적입니다. 이 실습은 그 경계를 한 VM 안에서 이어 붙여 봅니다: 무엇을 Terraform 의 state 로 두고, 무엇을 kubespray 의 멱등성에 맡기고, 선언과 실제가 어긋났을 때 누가 그것을 알아채는가. 노드를 만드는 첫 층은 이 VM 에서 하지 않습니다 — 이 플랫폼의 가상화 API 를 실습에서 부르게 하면 격리가 깨지기 때문입니다. 설치를 포함해 약 20분을 기다립니다.
단계
/root/ks/tf/cluster/main.tf에hashicorp/local프로바이더로 세 가지local_file을 선언하세요 —inventory(/root/ks/inventory/lab/inventory.ini을/root/ks/tf/cluster/inventory.tftpl템플릿과nodes변수로),host_vars(노드마다/root/ks/inventory/lab/host_vars/<노드>.yml에ansible_connection),version(/root/ks/inventory/lab/group_vars/k8s_cluster/zz-terraform.yml에kube_version: <변수>).kube_version변수의 기본값은 1.35.8,nodes의 기본값은 node1 한 대(컨트롤 플레인·워커 겸, local 연결)입니다.tofu validate가 통과해야 합니다./root/ks/tf/cluster에서tofu apply로 세 파일을 만드세요. 만들어진/root/ks/inventory/lab/inventory.ini이 state 의local_file.inventory내용과 같아야 하고,ansible-inventory로 읽었을 때 node1 이 kube_control_plane·etcd·kube_node 에 있으며 kube_version 이 1.35.8 으로 풀려야 합니다./root/ks/tf/cluster/main.tf에terraform_data "kubespray"를 더해, 인벤토리·판·host_vars 파일 내용이 바뀔 때만 다시 만들어지게(triggers_replace) 하고, 만들어질 때local-exec로/opt/ks/kubespray에서ansible-playbook -i /root/ks/inventory/lab/inventory.ini cluster.yml을 돌려 출력을/root/ks/logs/tf-cluster.log에 남기게 하세요(HOME=/root 를 넘깁니다).tofu apply로 설치까지 끝나면(약 7분) state 에terraform_data.kubespray가 있고, 로그의 PLAY RECAP 이failed=0, node1 이 Ready 여야 합니다./root/ks/tf/cluster에서tofu plan -detailed-exitcode를 그대로 한 번,-var kube_version=1.36.4를 붙여 한 번 돌려 보세요(적용하지 않습니다)./root/ks/tf/plan.json에steady_exit(첫 plan 의 종료 코드),bump_exit(두 번째의 종료 코드),bump_replaces(두 번째 plan 이 바꾸거나 다시 만들겠다는 리소스 주소의 정렬 배열)를 적으세요./root/ks/tf/apps/main.tf에hashicorp/kubernetes(3.2.1)·hashicorp/helm(3.3.0) 프로바이더를/root/.kube/config로 설정하고, 네임스페이스team-a(라벨owner=platform), 서비스어카운트deployer, deployments 를 만들고 고칠 수 있고 pods·services 는 읽기만 하는 Roledeployer와 그 RoleBinding, 그리고 로컬 차트/opt/ks/charts/hello를team-a에 레플리카 2 로 까는helm_release "hello"를 선언해tofu apply하세요.deployer는 team-a 에서 deployments 를 만들 수 있고 노드는 지울 수 없어야 하며, hello 의 파드 두 개가 Ready 여야 합니다.kubectl label ns team-a owner=someone-else --overwrite로 네임스페이스 라벨을 밖에서 바꾼 뒤,/root/ks/tf/apps에서tofu plan -detailed-exitcode로 차이를 잡아 출력을/root/ks/tf/drift-plan.txt에 남기세요./root/ks/tf/drift.json에exit_code(그 plan 의 종료 코드),drifted(바꾸겠다고 나온 리소스 주소의 정렬 배열)를 적습니다. 아직 적용하지 않습니다./root/ks/tf/apps에서tofu apply로 차이를 되돌리세요. 끝나면 team-a 의 owner 라벨이 platform 이고,tofu plan -detailed-exitcode가 0 이어야 합니다. 그리고/root/ks/tf/cluster의 plan 도 0 이어야 합니다(클러스터 쪽 선언도 그대로).
참고
- OpenTofu 1.12.6 이
/usr/local/bin/tofu(그리고terraform)에, 프로바이더(local 2.9.1·kubernetes 3.2.1·helm 3.3.0)가 플러그인 캐시/opt/ks/tf-plugin-cache에 미리 받아져 있습니다. 설정은TF_CLI_CONFIG_FILE=/root/.tofurc입니다. - kubespray v2.32.0 이
/opt/ks/kubespray에, 인벤토리 디렉터리/root/ks/inventory/lab의 group_vars 가 준비돼 있습니다. inventory.ini·host_vars·판 파일은 Terraform 이 만듭니다. - 흔한 실수: 클러스터를 만드는 모듈과 그 위에 배포하는 모듈을 하나로 합치는 것. 첫 plan 에서 아직 없는 kubeconfig 를 읽게 됩니다.
- 흔한 실수: Terraform 이 만든 inventory.ini 를 손으로 고치는 것. 다음 apply 가 되돌리고, 내용이 바뀌었으니 kubespray 도 다시 부릅니다.
- 문서: OpenTofu — templatefile · OpenTofu — terraform_data · OpenTofu — plan -detailed-exitcode · Kubernetes provider · Helm provider
인벤토리를 템플릿으로 만든다
/root/ks/tf/cluster/main.tf 에 hashicorp/local 프로바이더로 세 가지 local_file 을 선언하세요 — inventory(/root/ks/inventory/lab/inventory.ini 을 /root/ks/tf/cluster/inventory.tftpl 템플릿과 nodes 변수로), host_vars(노드마다 /root/ks/inventory/lab/host_vars/<노드>.yml 에 ansible_connection), version(/root/ks/inventory/lab/group_vars/k8s_cluster/zz-terraform.yml 에 kube_version: <변수>). kube_version 변수의 기본값은 1.35.8, nodes 의 기본값은 node1 한 대(컨트롤 플레인·워커 겸, local 연결)입니다. tofu validate 가 통과해야 합니다.
templatefile() 은 %{ for } … %{ endfor } 지시문으로 반복하고, ~ 는 줄바꿈을 먹습니다. 노드의 역할을 변수(맵)에 두면 노드를 늘릴 때 맵에 한 줄을 더하는 것으로 인벤토리·host_vars 가 함께 바뀝니다. 조리법이 k8s-cluster.yml 에 적어 두던 판 줄은 지워 두었습니다 — 판의 주인을 Terraform 하나로 두기 위해서입니다. OpenTofu 는 terraform 이라는 이름으로도 부를 수 있습니다.
파일만 먼저 apply
/root/ks/tf/cluster 에서 tofu apply 로 세 파일을 만드세요. 만들어진 /root/ks/inventory/lab/inventory.ini 이 state 의 local_file.inventory 내용과 같아야 하고, ansible-inventory 로 읽었을 때 node1 이 kube_control_plane·etcd·kube_node 에 있으며 kube_version 이 1.35.8 으로 풀려야 합니다.
state 에는 Terraform 이 만든 파일의 내용이 그대로 들어 있습니다. 누가 inventory.ini 를 손으로 고치면 다음 plan 이 그것을 되돌리겠다고 나옵니다 — 이 파일의 주인이 이제 Terraform 이라는 뜻입니다. tofu state show local_file.inventory 로 내용을 볼 수 있습니다.
terraform_data 로 kubespray 를 부른다
/root/ks/tf/cluster/main.tf 에 terraform_data "kubespray" 를 더해, 인벤토리·판·host_vars 파일 내용이 바뀔 때만 다시 만들어지게(triggers_replace) 하고, 만들어질 때 local-exec 로 /opt/ks/kubespray 에서 ansible-playbook -i /root/ks/inventory/lab/inventory.ini cluster.yml 을 돌려 출력을 /root/ks/logs/tf-cluster.log 에 남기게 하세요(HOME=/root 를 넘깁니다). tofu apply 로 설치까지 끝나면(약 7분) state 에 terraform_data.kubespray 가 있고, 로그의 PLAY RECAP 이 failed=0, node1 이 Ready 여야 합니다.
local-exec 는 Terraform 을 돌리는 곳(이 VM)에서 명령을 실행합니다. 명령이 실패하면 리소스가 tainted 로 남아 다음 apply 가 다시 부릅니다. kubespray 의 kube 모듈은 HOME 이 비면 kubeconfig 를 못 찾으므로 environment 로 넘깁니다. apply 가 7분 동안 터미널을 붙잡으니 systemd-run 이나 tmux 로 띄우는 편이 안전합니다.
바뀐 것이 없으면 아무것도 안 한다 — 판을 바꾸면?
/root/ks/tf/cluster 에서 tofu plan -detailed-exitcode 를 그대로 한 번, -var kube_version=1.36.4 를 붙여 한 번 돌려 보세요(적용하지 않습니다). /root/ks/tf/plan.json 에 steady_exit(첫 plan 의 종료 코드), bump_exit(두 번째의 종료 코드), bump_replaces(두 번째 plan 이 바꾸거나 다시 만들겠다는 리소스 주소의 정렬 배열)를 적으세요.
-detailed-exitcode 는 바뀔 것이 없으면 0, 있으면 2 를 돌려줍니다. 판을 바꾸면 무엇이 다시 만들어지는지 보세요 — terraform_data 가 다시 만들어지면 cluster.yml 이 다시 불립니다. kubespray 의 업그레이드는 cluster.yml 이 아니라 upgrade-cluster.yml 입니다. Terraform 은 그 차이를 모릅니다. tofu plan -json 의 resource_changes 가 주소와 동작(update·replace)을 알려 줍니다.
세운 클러스터에 선언으로 배포한다
/root/ks/tf/apps/main.tf 에 hashicorp/kubernetes(3.2.1)·hashicorp/helm(3.3.0) 프로바이더를 /root/.kube/config 로 설정하고, 네임스페이스 team-a(라벨 owner=platform), 서비스어카운트 deployer, deployments 를 만들고 고칠 수 있고 pods·services 는 읽기만 하는 Role deployer 와 그 RoleBinding, 그리고 로컬 차트 /opt/ks/charts/hello 를 team-a 에 레플리카 2 로 까는 helm_release "hello" 를 선언해 tofu apply 하세요. deployer 는 team-a 에서 deployments 를 만들 수 있고 노드는 지울 수 없어야 하며, hello 의 파드 두 개가 Ready 여야 합니다.
클러스터를 세우는 루트 모듈과 그 위에 배포하는 루트 모듈을 나누는 이유가 있습니다 — 한 모듈에서 클러스터를 만들고 같은 apply 에서 그 클러스터로 프로바이더를 설정하면, 첫 plan 때 아직 없는 kubeconfig 를 읽어야 합니다. helm 프로바이더 3.x 에서는 kubernetes 설정이 블록이 아니라 kubernetes = {{ ... }} 속성이고 set 도 목록 속성입니다. 권한은 kubectl auth can-i ... --as=system:serviceaccount:team-a:deployer 로 확인합니다.
누가 밖에서 바꿨다
kubectl label ns team-a owner=someone-else --overwrite 로 네임스페이스 라벨을 밖에서 바꾼 뒤, /root/ks/tf/apps 에서 tofu plan -detailed-exitcode 로 차이를 잡아 출력을 /root/ks/tf/drift-plan.txt 에 남기세요. /root/ks/tf/drift.json 에 exit_code(그 plan 의 종료 코드), drifted(바꾸겠다고 나온 리소스 주소의 정렬 배열)를 적습니다. 아직 적용하지 않습니다.
plan 은 먼저 실제 상태를 읽어(refresh) state 와 비교하고, 그다음 선언과 비교합니다. 밖에서 바뀐 것이 선언과 다르면 선언대로 되돌리겠다는 계획이 나옵니다. 이것이 drift 감지이고, 정기적으로 plan 을 돌려 종료 코드 2 를 알림으로 거는 것이 흔한 운영 방식입니다.
선언으로 되돌린다
/root/ks/tf/apps 에서 tofu apply 로 차이를 되돌리세요. 끝나면 team-a 의 owner 라벨이 platform 이고, tofu plan -detailed-exitcode 가 0 이어야 합니다. 그리고 /root/ks/tf/cluster 의 plan 도 0 이어야 합니다(클러스터 쪽 선언도 그대로).
apply 는 plan 과 같은 비교를 한 번 더 하고 차이를 선언대로 맞춥니다. drift 를 되돌릴지, 아니면 밖에서 바꾼 것이 맞아 선언을 고칠지는 사람이 판단할 몫입니다 — 이번에는 선언이 맞다고 보고 되돌립니다.