外观
项目 13:Kubernetes 集群自动化部署
约 3974 字大约 13 分钟
Kuberneteskubeadmkubeletkubectl
2026-06-06
项目目标
第 12 课完成了所有节点的系统预配置和 containerd 安装。本课继续完成 Kubernetes 集群的自动化部署:使用 Ansible 安装 kubeadm/kubelet/kubectl、初始化控制平面节点、自动加入工作节点,并部署 CNI 网络插件。
环境说明:WSL Ubuntu 作为 Ansible 控制端和 kubectl 客户端;三台 Docker 容器(privileged: true)在 WSL 内模拟 K8s 节点,通过 localhost 端口映射通信。
最终提交 deploy_k8s.yml、k8s_install、k8s_master、k8s_worker、k8s_network 四个 Role,以及 kubectl get nodes 显示所有节点 Ready 的截图。
Docker 实验边界
本课仍然要先按正文命令在 Docker 节点上实测,不要一开始跳过。实测中如果遇到 failed to mount rootfs ... overlay ... invalid argument,先回到第 12 课确认 containerd 已改为 native snapshotter 并重启。若 kubeadm init 与 worker join 已成功,但等待 Ready 阶段出现 ghcr.io/flannel-io/flannel-cni-plugin ... no unpack platforms defined、API Server 反复 connection refused、控制面 Pod 被 containerd/kubelet 反复重建,这属于 Docker/OrbStack 嵌套运行 K8s 的边界问题。此时提交故障日志、deploy_k8s.yml --syntax-check、Role 代码检查和已成功执行到的命令链;完整 Ready 集群改用虚拟机、云主机或教师预置好的 K8s 环境完成。
一、Kubernetes 组件关系
在开始自动化部署之前,需要理解 kubeadm、kubelet 和 kubectl 三者各自扮演什么角色。
| 组件 | 运行位置 | 职责 |
|---|---|---|
kubeadm | 仅在集群初始化和加入时使用 | 引导集群:init(初始化控制平面)、join(加入节点)、upgrade(版本升级) |
kubelet | 每个节点常驻运行 | 接收 Pod 定义,通过 containerd 启动和管理容器 |
kubectl | 控制端(可以是任何能访问 API Server 的机器) | 用户命令行工具,向 API Server 发送操作请求 |
kube-apiserver | 控制平面节点 | 集群入口,所有操作都通过它 |
kube-scheduler | 控制平面节点 | 决定 Pod 调度到哪个节点 |
kube-controller-manager | 控制平面节点 | 运行各种控制器(Deployment、Service、Node 等) |
kube-proxy | 每个节点 | 维护网络规则,实现 Service 的负载均衡 |
etcd | 控制平面节点 | 分布式键值存储,保存集群所有配置和状态 |
kubelet 的特殊之处在于:其他控制平面组件(API Server、Scheduler 等)都是以静态 Pod 的形式由 kubelet 管理的。kubelet 先启动,然后读取 /etc/kubernetes/manifests/ 下的 YAML 文件,启动 API Server 等控制平面组件。这意味着 kubelet 必须使用 systemd 管理而不能容器化——它是"先有蛋后有鸡"问题的解决方案。
二、k8s_install Role
k8s_install role 负责在所有节点上安装 kubeadm、kubelet 和 kubectl。这三个包通过 Kubernetes 官方 APT 仓库安装,而非系统默认的 Ubuntu 仓库。
问题:Kubernetes 官方提供的集群引导工具叫什么?
2.1 defaults/main.yml
---
# roles/k8s_install/defaults/main.yml
k8s_version: "1.30"
k8s_apt_keyring_dir: /etc/apt/keyrings
k8s_apt_keyring_file: /etc/apt/keyrings/kubernetes-apt-keyring.asc
k8s_apt_release_key_url: "https://pkgs.k8s.io/core:/stable:/v{{ k8s_version }}/deb/Release.key"
k8s_apt_repo_line: "deb [signed-by={{ k8s_apt_keyring_file }}] https://pkgs.k8s.io/core:/stable:/v{{ k8s_version }}/deb/ /"
k8s_apt_repo_filename: kubernetes
k8s_packages:
- kubeadm
- kubelet
- kubectl
k8s_hold_packages: true
kubelet_service_name: kubelet2.2 K8s APT 仓库与版本锁定
Kubernetes 的 APT 仓库与 Docker 的仓库地址不同:
| 仓库 | URL | 用途 |
|---|---|---|
| Docker | https://download.docker.com/linux/ubuntu | containerd.io |
| Kubernetes | https://pkgs.k8s.io/core:/stable:/v1.30/deb/ | kubeadm, kubelet, kubectl |
版本号 v1.30 需要与课堂环境兼容。本课在 roles/k8s_install/defaults/main.yml 中统一控制 K8s 版本和 APT 仓库地址:
# roles/k8s_install/defaults/main.yml
k8s_version: "1.30"
k8s_apt_repo_line: "deb [signed-by={{ k8s_apt_keyring_file }}] https://pkgs.k8s.io/core:/stable:/v{{ k8s_version }}/deb/ /"apt-mark hold 用于锁定软件包版本,防止 apt upgrade 自动升级 kubelet(这会导致节点版本不一致):
- name: 锁定 kubeadm/kubelet/kubectl 版本
ansible.builtin.command:
cmd: "apt-mark hold kubeadm kubelet kubectl"
changed_when: false2.3 kubelet 的特殊启动方式
kubeadm 安装的 kubelet 有一个特点:安装后它会因为没有配置文件而反复重启。这是正常行为——kubelet 需要一个 kubeadm init 或 kubeadm join 生成的 /etc/kubernetes/kubelet.conf 才能正常连接到 API Server。在安装阶段,只需确保 kubelet 已安装并 enabled 即可:
- name: 启用 kubelet 开机启动
ansible.builtin.command: systemctl enable kubelet
changed_when: false安装阶段不要强行 state: started。kubelet 需要 kubeadm init 或 kubeadm join 生成的 /etc/kubernetes/kubelet.conf,提前启动通常只会进入反复重启状态。真正启动 kubelet 的动作交给 kubeadm 完成。
三、k8s_master Role
k8s_master role 仅在控制平面节点上执行,负责 kubeadm init 和配置 kubectl。
3.1 defaults/main.yml
---
# roles/k8s_master/defaults/main.yml
k8s_pod_network_cidr: "10.244.0.0/16"
k8s_image_repository: registry.aliyuncs.com/google_containers
k8s_worker_group: k8s_workers
kubeconfig_admin_conf_path: /etc/kubernetes/admin.conf
kubeconfig_remote_dir: /root/.kube
kubeconfig_remote_path: /root/.kube/config
kubeconfig_local_path: outputs/admin.conf
kubeadm_join_script_path: /tmp/kubeadm-join.sh3.2 kubeadm init 的执行流程
Kubernetes 集群组建演示
Kubeadm init 引导控制平面,kubeadm join 接入工作节点
👑 控制平面 (Master)
kubeadm init
☸️ API Server
🖥️ 工作节点 (Worker)
kubeadm join
⚙️ Kubelet
问题:Kubernetes 中用于初始化控制平面节点的命令是什么?
3.3 kubeadm init 的参数
- name: 配置 kubeadm 国内镜像仓库参数
ansible.builtin.set_fact:
kubeadm_image_repository_arg: "--image-repository={{ k8s_image_repository }}"
when: k8s_image_repository | default('') | length > 0
- name: 初始化 Kubernetes 控制平面
ansible.builtin.command:
cmd: >
kubeadm init
--pod-network-cidr={{ k8s_pod_network_cidr }}
--apiserver-advertise-address={{ node_ip }}
{{ kubeadm_image_repository_arg | default('') }}
register: kubeadm_init
changed_when: "'initialized successfully' in kubeadm_init.stdout"| 参数 | 作用 | 示例值 |
|---|---|---|
--pod-network-cidr | Pod 网络 CIDR,CNI 插件根据这个分配 IP | 10.244.0.0/16(Flannel 默认) |
--apiserver-advertise-address | API Server 对外公告的 IP | 控制平面节点的物理 IP |
--image-repository | kubeadm 拉取控制平面、CoreDNS、kube-proxy 等镜像时使用的仓库 | registry.aliyuncs.com/google_containers |
--pod-network-cidr 必须与你选择的 CNI 插件匹配。Flannel 默认使用 10.244.0.0/16,Calico 默认使用 192.168.0.0/16。如果不一致,Pod IP 分配会出现问题。
k8s_image_repository 不是预拉取镜像,而是让 kubeadm init 在初始化过程中直接使用国内镜像仓库。这样可以避免卡在 registry.k8s.io/kube-proxy 等镜像拉取步骤。如果实验环境可以稳定访问 registry.k8s.io,也可以把 k8s_image_repository 设为空字符串,回到官方默认仓库。
不要把 APT 仓库版本直接传给 kubeadm init
k8s_version: "1.30" 用于 Kubernetes APT 仓库路径 v1.30,它不是完整语义版本。kubeadm init --kubernetes-version=v1.30 会报 doesn't match patterns for neither semantic version nor labels。本课不传 --kubernetes-version,让 kubeadm 使用当前安装版本对应的稳定版本。
3.4 配置 kubectl
kubeadm init 成功后,控制平面的 root 用户需要配置 kubectl:
- name: 创建 .kube 目录
ansible.builtin.file:
path: /root/.kube
state: directory
owner: root
group: root
mode: "0755"
- name: 复制 admin.conf 到 kubectl 配置
ansible.builtin.copy:
src: /etc/kubernetes/admin.conf
dest: /root/.kube/config
remote_src: true
owner: root
group: root
mode: "0600"同时,控制端(运行 Ansible 的机器)也需要获取 admin.conf,否则 kubectl 命令需要在 Ansible playbook 中通过 ansible.builtin.command 远程执行,很不方便。可以用 fetch 模块将 admin.conf 拉取到控制端本地:
- name: 获取 kubeconfig 到控制端
ansible.builtin.fetch:
src: /etc/kubernetes/admin.conf
dest: outputs/admin.conf
flat: true3.5 保存 join 命令
kubeadm init 的输出中包含了 worker 节点加入集群所需的命令和 token。需要用 register 保存输出,再提取 join 命令:
- name: 生成 worker 节点加入命令
ansible.builtin.command:
cmd: kubeadm token create --print-join-command
register: join_command
changed_when: false
- name: 保存 join 命令到文件
ansible.builtin.copy:
content: "{{ join_command.stdout }}"
dest: /tmp/kubeadm-join.sh
mode: "0700"然后通过 fetch 或 set_fact 将 join 命令传递给 worker role:
# 在 k8s_master role 中设置全局 fact
- name: 存储 join 命令为全局变量
ansible.builtin.set_fact:
kubeadm_join_command: "{{ join_command.stdout }}"
delegate_to: "{{ item }}"
delegate_facts: true
loop: "{{ groups[k8s_worker_group] }}"四、k8s_worker Role
k8s_worker role 使用 master 节点生成的 join 命令,将 worker 节点加入集群。
问题:Kubernetes 中用于将工作节点加入已有集群的命令是什么?
4.1 defaults/main.yml
---
# roles/k8s_worker/defaults/main.yml
kubelet_conf_path: /etc/kubernetes/kubelet.conf4.2 join 的幂等性
kubeadm join 依赖 master play 生成并分发的 kubeadm_join_command。为了让 Playbook 可重复执行,worker 先检查 /etc/kubernetes/kubelet.conf 是否存在;如果节点已经加入过集群,就跳过 join:
- name: 检查节点是否已经加入集群
ansible.builtin.stat:
path: "{{ kubelet_conf_path }}"
register: kubelet_conf
- name: 确认 master 已生成 join 命令
ansible.builtin.assert:
that:
- kubeadm_join_command is defined
- kubeadm_join_command | length > 0
fail_msg: "未获取到 kubeadm_join_command,请确认 k8s_master play 已成功执行。"
when: not kubelet_conf.stat.exists
- name: 加入 Kubernetes 集群
ansible.builtin.command:
cmd: "{{ kubeadm_join_command }}"
register: kubeadm_join
changed_when: "'This node has joined the cluster' in kubeadm_join.stdout"
when: not kubelet_conf.stat.exists4.3 验证节点加入
worker 加入后,在控制平面节点上执行 kubectl get nodes 验证:
# 在控制端(运行 Ansible 的机器)
kubectl --kubeconfig outputs/admin.conf get nodes预期输出:三个节点均显示,但状态可能是 NotReady(因为 CNI 尚未部署)。
五、k8s_network Role
Kubernetes 集群本身不包含网络插件。必须部署一个 CNI(Container Network Interface)插件,Pod 之间才能相互通信。
问题:Kubernetes 中容器网络接口的英文缩写是什么?(3 个大写字母)
5.1 defaults/main.yml
---
# roles/k8s_network/defaults/main.yml
kubeconfig_admin_conf_path: /etc/kubernetes/admin.conf
cni_manifest_url: https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
nodes_ready_retries: 60
nodes_ready_delay: 55.2 常用 CNI 插件对比
| 插件 | 网络模型 | 优势 | 适用场景 |
|---|---|---|---|
| Flannel | overlay(vxlan) | 最简单,一条命令部署 | 学习和测试环境 |
| Calico | BGP 路由 | 性能好,支持 NetworkPolicy | 生产环境,安全策略需求 |
| Cilium | eBPF | 高性能,可观测性强 | 云原生、微服务网格 |
| Weave | overlay(UDP 加密) | 自带加密 | 多集群和安全敏感场景 |
本课推荐使用 Flannel——因为它最简单,一个 kubectl apply 就能完成部署,适合课堂验证。
CNI 插件与 Overlay 网络演示
没有 CNI 插件时节点 NotReady,部署 Flannel 后实现跨节点 Pod 通信
k8s-worker1 NotReady
物理 IP: 172.28.0.3
📦
Pod A
无 IP
Missing CNI
flanneld
物理网络 (172.28.0.0/24)
Overlay 网络 (10.244.0.0/16)
k8s-worker2 NotReady
物理 IP: 172.28.0.4
📦
Pod B
无 IP
Missing CNI
flanneld
💡 网络原理: 初始化状态:kubelet 发现系统里没有配置 CNI 插件,无法为 Pod 分配 IP,因此将节点状态标记为 NotReady。
5.3 部署 Flannel
- name: 部署 Flannel CNI 插件
ansible.builtin.command:
cmd: kubectl apply -f {{ cni_manifest_url }}
environment:
KUBECONFIG: "{{ kubeconfig_admin_conf_path }}"
register: flannel_deploy
changed_when: "'created' in flannel_deploy.stdout or 'configured' in flannel_deploy.stdout"5.4 验证 CNI 部署
部署 Flannel 后,等待所有节点变为 Ready:
- name: 等待所有节点 Ready(最多 300 秒)
ansible.builtin.command:
cmd: kubectl get nodes --no-headers
environment:
KUBECONFIG: "{{ kubeconfig_admin_conf_path }}"
register: nodes_status
until: nodes_status.stdout_lines | select('search', 'NotReady') | list | length == 0
retries: "{{ nodes_ready_retries | int }}"
delay: "{{ nodes_ready_delay | int }}"
changed_when: falsenodes_ready_retries: 60 × nodes_ready_delay: 5 = 最多等待 300 秒。Docker 嵌套环境使用 native snapshotter 后会更慢,等待时间要比真实虚拟机更宽松。如果 300 秒后仍有 NotReady 节点,任务失败并输出当前节点状态。
六、deploy_k8s.yml 总入口
问题:本课 K8s 集群自动化部署的顶层 Playbook 文件名是什么?
deploy_k8s.yml 中需要明确区分 hosts 范围:
---
# Play 1: 所有节点安装 K8s 组件
- name: 安装 Kubernetes 组件
hosts: k8s_cluster
become: true
roles:
- role: k8s_install
# Play 2: 初始化控制平面
- name: 初始化控制平面节点
hosts: k8s_master
become: true
roles:
- role: k8s_master
# Play 3: 加入工作节点
- name: 加入工作节点
hosts: k8s_workers
become: true
roles:
- role: k8s_worker
# Play 4: 部署网络插件
- name: 部署 CNI 网络插件
hosts: k8s_master
become: true
roles:
- role: k8s_network四个 Play 按依赖顺序排列:必须先安装组件,才能 init;必须先 init 生成 join 命令,worker 才能 join;必须先有集群,才能部署 CNI。
七、完整工程目录
ansible-k8s/
├── deploy_k8s.yml # K8s 集群部署总入口
├── inventory.ini
├── ansible.cfg
├── group_vars/
│ └── all.yml
├── roles/
│ ├── k8s_prepare/ # 第 12 课:节点预配置
│ ├── container_runtime/ # 第 12 课:containerd
│ ├── k8s_install/ # 本课新增:kubeadm/kubelet/kubectl
│ │ ├── tasks/main.yml
│ │ └── defaults/main.yml
│ ├── k8s_master/ # 本课新增:kubeadm init
│ │ ├── tasks/main.yml
│ │ └── defaults/main.yml
│ ├── k8s_worker/ # 本课新增:kubeadm join
│ │ ├── tasks/main.yml
│ │ └── defaults/main.yml
│ └── k8s_network/ # 本课新增:CNI 部署
│ ├── tasks/main.yml
│ └── defaults/main.yml
├── docker/
└── outputs/ # 存放 admin.conf 和运行日志八、运行与验证
8.1 执行完整部署(在 WSL Ubuntu 中执行)
所有 Ansible 和 kubectl 命令在 WSL Ubuntu 终端中运行。admin.conf 被 fetch 到本地 outputs/ 目录后,通过 KUBECONFIG 环境变量让 kubectl 直接操作 Docker 内的 K8s 集群。
# 语法检查
ansible-playbook deploy_k8s.yml --syntax-check
# 预演(kubeadm 不支持完整 check 模式)
ansible-playbook deploy_k8s.yml --check --diff
# 正式部署
ansible-playbook deploy_k8s.yml | tee outputs/deploy-k8s.log8.2 验证集群状态
# 使用拉取到控制端的 kubeconfig
export KUBECONFIG=outputs/admin.conf
# 查看节点
kubectl get nodes -o wide
# 查看所有 Pod(包括系统组件)
kubectl get pods -A
# 查看节点详细状态
kubectl describe nodes预期输出:
NAME STATUS ROLES AGE VERSION
k8s-master Ready control-plane 2m v1.30.0
k8s-worker1 Ready <none> 1m v1.30.0
k8s-worker2 Ready <none> 1m v1.30.08.3 验证 Pod 网络
# 部署测试 Pod
kubectl run test-pod --image=busybox --restart=Never -- sleep 300
# 检查 Pod 状态
kubectl get pod test-pod -o wide
# 验证 Pod 间网络(进入 Pod 后 ping 另一个 Pod 的 IP)
kubectl exec -it test-pod -- ping <another-pod-ip>8.4 部署测试服务
# 快速验证集群功能:部署一个 nginx
kubectl create deployment nginx-test --image=nginx --replicas=2
kubectl expose deployment nginx-test --port=80 --type=ClusterIP
kubectl get svc nginx-test
# 验证 Service 可达性
kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -- \
curl -s -o /dev/null -w "%{http_code}" http://nginx-test九、提交物清单
| 提交物 | 要求 |
|---|---|
k8s_install Role | 完整的 tasks/main.yml 和 defaults/main.yml,含 APT 仓库添加、组件安装和版本锁定 |
k8s_master Role | 完整的 tasks/main.yml 和 defaults/main.yml,含 kubeadm init、kubectl 配置、join 命令保存与分发 |
k8s_worker Role | 完整的 tasks/main.yml 和 defaults/main.yml,含 kubeadm join,正确使用 master 分发的 join 命令 |
k8s_network Role | 完整的 tasks/main.yml 和 defaults/main.yml,含 CNI 插件部署和等待节点 Ready |
deploy_k8s.yml | 4 个 Play 按正确顺序编排,hosts 范围准确 |
kubectl get nodes 截图 | 所有节点 STATUS=Ready |
kubectl get pods -A 截图 | 所有系统 Pod Running |
| 部署日志 | 完整 ansible-playbook 输出,能看到 play recap |
十、故障排查
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
kubeadm init 报 swap 未关闭 | swapoff -a 只在当前会话生效,重启后恢复 | 检查 /etc/fstab 中 swap 行已被注释;重启节点后重新 swapoff -a |
kubeadm init 报 cgroup 不匹配 | containerd 的 SystemdCgroup 未启用 | 检查 /etc/containerd/config.toml,grep SystemdCgroup 确认为 true |
kubeadm init 报端口占用 | 之前初始化失败后残留的进程 | kubeadm reset -f 清理后重试 |
worker 加入失败 connection refused | API Server 地址不对,或防火墙未放行 6443 端口 | 检查 --apiserver-advertise-address 是否正确;检查防火墙规则 |
| join token 过期 | kubeadm 默认 token 有效期 24 小时 | 在 master 上执行 kubeadm token create --print-join-command 重新生成 |
kubeadm init 卡在 registry.k8s.io/... 镜像拉取 | 访问 Kubernetes 官方镜像仓库太慢或被网络限制 | 确认 roles/k8s_master/defaults/main.yml 中 k8s_image_repository 已设置为国内镜像仓库,例如 registry.aliyuncs.com/google_containers |
| CNI 部署后节点仍 NotReady | Flannel 镜像拉取失败或 Pod CIDR 不匹配 | kubectl describe pod -n kube-flannel 查看 Pod 状态;确认 --pod-network-cidr 与 CNI 插件一致 |
Flannel 报 no unpack platforms defined | Docker/OrbStack 嵌套容器中 containerd native snapshotter 与多架构镜像解包不兼容 | 记录 journalctl -u kubelet 和 crictl ps -a 证据;换虚拟机/云主机或教师预置集群继续完整 Ready 验收 |
kubectl 命令报 connection refused | kubeconfig 未配置或 API Server 未运行 | 检查 KUBECONFIG 环境变量;在 master 节点上执行 kubectl 确认 API Server 正常 |
Ansible 报 kubeadm_join_command is undefined | worker Play 先于 master Play 执行,或 master 初始化/token 生成失败导致 join 命令没有分发 | 检查 deploy_k8s.yml 中 Play 的顺序;确认 master Play 中生成并分发 kubeadm_join_command 的任务成功 |
| join 命令中 API Server 地址错误 | --apiserver-advertise-address 使用了默认值而非实际 IP | Docker 课堂环境中使用 Inventory 的 node_ip,在 kubeadm init 中显式指定 --apiserver-advertise-address={{ node_ip }} |
十一、进阶任务
探索任务 1:实现 kubeadm reset 回滚 Playbook
编写 reset_k8s.yml,在所有节点上执行 kubeadm reset -f,清理 /etc/kubernetes/ 和 CNI 网络配置,重置集群到"节点准备完成"状态。理解 reset 不会删除 /etc/hosts 和内核模块等系统配置,只清理 K8s 组件本身。
探索任务 2:多控制平面高可用部署
修改 Inventory 为 3 个控制平面节点 + 2 个工作节点。编写 k8s_master role 区分"第一个控制平面"和"加入的控制平面",使用 kubeadm init --control-plane-endpoint 指定负载均衡器地址。理解 etcd 集群和 API Server 高可用的原理。
探索任务 3:部署 Calico 替代 Flannel
修改 k8s_network role,使用 Calico 替代 Flannel。注意 Calico 的 Pod CIDR 默认是 192.168.0.0/16,确保 --pod-network-cidr 与此匹配。验证 Calico 的 NetworkPolicy 功能——创建一个 NetworkPolicy 限制特定 Pod 的入站流量。
