TT Lab
开始
学习 学习路径 课程

用 Kubespray 与 Terraform 搭建集群

Terraform 生成 inventory、调用 Kubespray,再在其上部署

在 TT Lab 中继续学习

目标

在同一台 VM 内,让 OpenTofu 把 kubespray inventory 生成为模板,包装 kubespray 的执行来搭建集群, 并用 kubernetes 和 helm provider 在搭建好的集群上声明命名空间、RBAC 和应用。通过 plan 捕捉在外部发生的变化并将其改回去。

为什么重要

现场集群的生命周期通常分为三层——创建节点的层(云、虚拟化)、在节点之上安装 Kubernetes 的层(kubespray)、在集群之上安装 团队的空间、权限和应用的层。Terraform 擅长第一层和第三层,而第二层通常交给 Ansible。本实验把这条界线 在一台 VM 内接起来:什么放在 Terraform 的 state 中,什么交给 kubespray 的幂等性,声明与实际不一致时由谁来发现。 创建节点的第一层不在这台 VM 上做——因为让实验调用这个平台的虚拟化 API 会破坏隔离。包括安装在内,要等待约 20 分钟。

步骤

  1. 在 /root/ks/tf/cluster/main.tf 中用 hashicorp/local provider 声明三个 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 必须通过。
  2. 在 /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。
  3. 在 /root/ks/tf/cluster/main.tf 中加入 terraform_data "kubespray",让它只在 inventory、版本、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。
  4. 在 /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 提出要更改或重新创建的资源地址排序后的数组)。
  5. 在 /root/ks/tf/apps/main.tf 中,把 hashicorp/kubernetes(3.2.1)和 hashicorp/helm(3.3.0)provider 配置为使用 /root/.kube/config,并声明命名空间 team-a(标签 owner=platform)、ServiceAccount deployer、可以创建和修改 deployments 而对 pods、services 只读的 Role deployer 及其 RoleBinding,以及把本地 Chart /opt/ks/charts/hello 以 2 个副本安装到 team-a 的 helm_release "hello",然后执行 tofu apply。deployer 必须能在 team-a 中创建 deployments,并且不能删除节点,hello 的两个 Pod 必须是 Ready。
  6. 用 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(提出要更改的资源地址排序后的数组)。暂时不要应用。
  7. 在 /root/ks/tf/apps 中用 tofu apply 把差异改回去。完成后,team-a 的 owner 标签必须是 platform,并且 tofu plan -detailed-exitcode 必须是 0。而且 /root/ks/tf/cluster 的 plan 也必须是 0(集群一侧的声明也保持不变)。

参考

把 inventory 生成为模板

在 /root/ks/tf/cluster/main.tf 中用 hashicorp/local provider 声明三个 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 } 指令进行循环,而波浪号(tilde)会吞掉换行。把节点的角色放在变量(映射)中,增加节点时只需在映射中加一行,inventory 和 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",让它只在 inventory、版本、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)provider 配置为使用 /root/.kube/config,并声明命名空间 team-a(标签 owner=platform)、ServiceAccount deployer、可以创建和修改 deployments 而对 pods、services 只读的 Role deployer 及其 RoleBinding,以及把本地 Chart /opt/ks/charts/hello 以 2 个副本安装到 team-a 的 helm_release "hello",然后执行 tofu apply。deployer 必须能在 team-a 中创建 deployments,并且不能删除节点,hello 的两个 Pod 必须是 Ready。

之所以要把搭建集群的根模块和在其上部署的根模块分开,是有原因的——如果在一个模块中创建集群,并在同一次 apply 中用该集群来设置 provider,首次 plan 时就必须读取尚不存在的 kubeconfig。在 helm provider 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 改回去,还是因为外部的修改才是对的而去修改声明,要由人来判断——这次认为声明是对的,所以改回去。