外观
项目 12:Kubernetes 节点准备自动化
约 3922 字大约 13 分钟
Kuberneteskubeadmcontainerdsysctl
2026-06-06
项目目标
前面 11 课完成了从 Shell 脚本到 Ansible Roles 的演进。本课进入新阶段:使用 Ansible 为 Kubernetes 集群自动完成所有节点的系统预配置——包括关闭 swap、加载内核模块、设置 sysctl 参数、配置时间同步和 containerd。
环境说明:控制端为 WSL Ubuntu,所有 K8s 节点通过 Docker 容器(privileged: true)在 WSL 内模拟,Ansible 通过 localhost 端口映射连接各容器。
最终提交 k8s_prepare 和 container_runtime 两个 Role、Inventory 分组、以及所有节点的检查截图。
一、Kubernetes 节点部署要准备什么
Kubeadm 是 Kubernetes 官方提供的集群引导工具,负责安装 kubelet、初始化控制平面和加入工作节点。但在运行 kubeadm 之前,每一台节点都必须完成一系列系统级预配置。这些配置不是可有可无的——漏掉任何一项,kubelet 都无法正常启动或 Pod 网络必然异常。
Kubernetes 节点环境准备流程
Kubelet 启动前的系统级前置依赖自动检查与配置演示
💻 Worker Node (Ubuntu 22.04)
⏳
禁用 Swap 分区
系统目前启用了交换分区
⏳
加载内核模块
overlay, br_netfilter 模块未加载
⏳
配置 Sysctl 参数
bridge-nf-call-iptables 等参数未设置
⏳
时间同步服务
系统时间未同步
⏳
Container Runtime
未安装 containerd
☸️
Kubelet 状态:Waiting for prerequisites...
Ansible k8s_prepare 任务输出
本课聚焦于 A-F(节点准备),第 13 课完成 G-H(集群部署)。两个课时合起来就是一套完整的"Kubernetes 一键部署"方案。
1.1 为什么要关闭 swap
Kubernetes 从 1.8 版本开始要求关闭 swap,原因是:
| 问题 | 说明 |
|---|---|
| 调度准确性 | kubelet 假设节点有 内存总量 - 已分配内存 的可用内存;swap 让部分内存被换出到磁盘,但调度器无法感知 |
| 性能不可预测 | 容器进程被 swap 后,延迟从微秒级跳变到毫秒级,影响 QoS |
| kubelet 拒绝启动 | 如果检测到 swap 未关闭,kubelet 的 --fail-swap-on=true(默认)会直接报错退出 |
关闭 swap 需要两步:临时关闭(swapoff -a)+ 永久关闭(注释 /etc/fstab 中的 swap 行)。只做一步是不够的——临时关闭在重启后恢复,只改 fstab 不立即 swapoff 则当前内核仍在使用 swap。
1.2 内核模块与 sysctl 的网络依赖
Kubernetes Pod 网络(尤其是基于 vxlan 或 bridge 的 CNI 插件)要求内核加载特定模块:
| 模块 | 作用 | 不加载的后果 |
|---|---|---|
overlay | overlayfs 联合文件系统,容器镜像层依赖此模块 | containerd 存储驱动无法工作 |
br_netfilter | 让 iptables 能过滤桥接网络的流量 | Pod 间的网络策略(NetworkPolicy)失效,跨节点通信异常 |
br_netfilter 加载后,还需要通过 sysctl 启用桥接流量的 iptables 处理:
cat <<EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system| 参数 | 作用 | 不配置的后果 |
|---|---|---|
net.bridge.bridge-nf-call-iptables | 桥接流量经过 iptables 处理 | kube-proxy 的 iptables 规则对 Pod 流量无效 |
net.ipv4.ip_forward | 允许 IP 转发 | Pod 无法访问外部网络,甚至无法跨节点通信 |
这三行 sysctl 配置看起来简单,但它们是 Kubernetes 网络正常工作的最低保证。
二、集群拓扑规划
本课假设一个最小集群:1 个控制平面节点 + 2 个工作节点。在生产环境中,控制平面节点应该是 3 个(高可用)。课堂练习可以使用 1 主 2 从。
问题:本课 Kubernetes 集群拓扑中,控制平面节点的 Inventory 组名是什么?
集群角色划分:
| 角色 | Inventory 组 | 数量 | 职责 |
|---|---|---|---|
| 控制平面 | k8s_master | 1 | 运行 API Server、Scheduler、Controller Manager、etcd |
| 工作节点 | k8s_workers | 2 | 运行用户 Pod、kube-proxy |
| 全部节点 | k8s_cluster:children | 3 | 节点准备工作在所有节点上执行 |
Ansible Inventory 拓扑与 Role 映射演示
理解 k8s_cluster:children 分组如何将基础准备工作应用到所有节点
📄 inventory.ini
[k8s_master]
k8s-master node_ip=172.28.0.2
[k8s_workers]
k8s-worker1 node_ip=172.28.0.3
k8s-worker2 node_ip=172.28.0.4
[k8s_cluster:children]
k8s_master
k8s_workers
📄 site.yml
- hosts: k8s_cluster
roles:
- k8s_prepare
- container_runtime
🌐 集群节点拓扑
k8s_master
👑
k8s-master
172.28.0.2
k8s_workers
🖥️
k8s-worker1
172.28.0.3
🖥️
k8s-worker2
172.28.0.4
2.1 Docker 模拟集群(WSL 内运行)
三台 K8s 节点全部通过 Docker 容器在 WSL Ubuntu 内模拟。由于 K8s 节点需要更多 Linux 内核能力(加载模块、修改 sysctl、操作 ip_forward 等),Docker Compose 中必须使用 privileged: true。
# docker-compose.yml 关键配置
services:
k8s-master:
privileged: true
hostname: k8s-master
# ... 端口映射、网络
k8s-worker1:
privileged: true
hostname: k8s-worker1
k8s-worker2:
privileged: true
hostname: k8s-worker2privileged 模式的取舍
privileged: true 赋予容器几乎所有宿主机内核能力,包括加载内核模块。这只适合 Docker 课堂模拟——在真实 K8s 集群中,内核模块是在宿主机操作系统层面加载的。
三、k8s_prepare Role
k8s_prepare role 负责所有节点共有的系统预配置。
问题:Linux 中用于临时关闭所有 swap 的命令是什么?
3.1 defaults/main.yml
先为 k8s_prepare 准备默认变量。主机名默认使用 Inventory 中的主机名,例如 k8s-master、k8s-worker1、k8s-worker2。如果以后真实服务器的 Inventory 名和系统主机名不同,只需要在 host_vars/<主机名>.yml 中覆盖 k8s_node_hostname。
---
# roles/k8s_prepare/defaults/main.yml
# 节点主机名:默认使用 inventory.ini 中的主机名
k8s_node_hostname: "{{ inventory_hostname }}"
# K8s 集群主机组名,用于生成 /etc/hosts
k8s_cluster_group: k8s_cluster
# hosts 文件路径。Docker 容器中该文件常是特殊挂载文件
k8s_hosts_path: /etc/hosts
# 时间同步服务
k8s_time_sync_package: chrony
k8s_time_sync_service: chrony3.2 tasks 分解
k8s_prepare 的 tasks 按依赖关系排列:
| 序号 | Task | 模块 | 说明 |
|---|---|---|---|
| 1 | 设置主机名 | hostname | 使用 k8s_node_hostname,默认等于 Inventory 主机名 |
| 2 | 配置 /etc/hosts | lineinfile | 所有节点的 IP 和主机名映射 |
| 3 | 检查 swap 状态 | command: swapon --show --noheadings | 没有 swap 时不误报失败 |
| 4 | 关闭 swap(临时) | command: swapoff -a | 有活跃 swap 时才执行 |
| 5 | 关闭 swap(永久) | replace | 注释 /etc/fstab 中的 swap 行 |
| 6 | 检查内核能力 | command + stat | 兼容模块已内建的容器环境 |
| 7 | 加载内核模块 | modprobe | overlay, br_netfilter |
| 8 | 持久化内核模块 | copy | 写入 /etc/modules-load.d/k8s.conf |
| 9 | 配置 sysctl | copy | 写入 /etc/sysctl.d/k8s.conf 并执行 sysctl --system |
| 10 | 安装 chrony | apt | 使用 k8s_time_sync_package 安装时间同步服务 |
3.3 hosts 文件的重要性
在 K8s 集群中,每个节点的 /etc/hosts 必须包含所有节点的 IP 和主机名。这是因为:
- kubeadm 初始化时使用主机名作为节点标识
- 控制平面 API Server 的 TLS 证书绑定到主机名
- 如果节点间 DNS 不可用,hosts 文件是唯一的主机名解析来源
Ansible 的 lineinfile 模块可以确保 hosts 文件中只存在最新的映射(不会重复追加):
- name: 确保所有节点都在 /etc/hosts 中
ansible.builtin.lineinfile:
path: "{{ k8s_hosts_path }}"
regexp: "^{{ hostvars[item].node_ip }}\\s+{{ hostvars[item].k8s_node_hostname | default(item) }}"
line: "{{ hostvars[item].node_ip }} {{ hostvars[item].k8s_node_hostname | default(item) }}"
unsafe_writes: true
loop: "{{ groups[k8s_cluster_group] }}"Docker 容器里的 /etc/hosts 通常是特殊挂载文件,unsafe_writes: true 可以避免 Ansible 原子替换文件时出现 Device or resource busy。
3.4 时间同步的重要性
Kubernetes 集群中的时间同步经常被初学者忽略,但它直接影响:
| 组件 | 对时间的依赖 |
|---|---|
| etcd | 使用 Raft 共识算法,节点间时间偏差过大会导致心跳超时 |
| kubelet | 证书有效期校验依赖系统时间 |
| 审计日志 | 审计事件的时间戳基于系统时钟 |
| Prometheus 监控 | 指标采集的时间戳比对 |
推荐使用 chrony 而非 ntpd——chrony 在时钟漂移较大和网络不稳定时恢复更快:
sudo apt install -y chrony
sudo systemctl enable --now chrony
chronyc sources -v # 验证已连接到 NTP 服务器四、container_runtime Role
container_runtime role 负责在所有节点上安装和配置 containerd。containerd 是 Kubernetes 社区推荐的容器运行时,从 K8s 1.24 开始它替代 Docker 成为默认选择。
4.1 defaults/main.yml
container_runtime 也需要 defaults。这里把包名、仓库地址、配置文件路径和课堂 Docker 嵌套环境使用的 snapshotter 都放到默认变量中,后续如果切换到真实虚拟机或云主机,可以把 containerd_snapshotter 改回 overlayfs。
---
# roles/container_runtime/defaults/main.yml
# 安装 Docker APT 仓库前需要的基础包
container_runtime_prereq_packages:
- apt-transport-https
- ca-certificates
- curl
- gnupg
# Docker 官方 APT 仓库用于安装 containerd.io
docker_apt_key_url: https://download.docker.com/linux/ubuntu/gpg
docker_apt_repo: "deb https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
# containerd 包、服务和配置路径
containerd_package: containerd.io
containerd_service_name: containerd
containerd_config_path: /etc/containerd/config.toml
# kubelet 推荐使用 systemd cgroup 驱动
containerd_systemd_cgroup: true
# Docker/OrbStack 嵌套实验环境使用 native;真实虚拟机/物理机通常可改为 overlayfs
containerd_snapshotter: native4.2 containerd 配置要求
问题:Kubernetes 1.24 开始官方推荐的容器运行时名称是什么?
containerd 的安装分为三步:
其中第三步"配置 SystemdCgroup"是最容易出错的一环。containerd 默认使用 cgroupfs 作为 cgroup 驱动,但 kubelet 推荐使用 systemd。两者不一致时,kubelet 会报警:
[WARNING] CGroup v1: detected "cgroupfs" as cgroup driver, kubelet uses "systemd"解决方案是修改 /etc/containerd/config.toml:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true4.3 containerd 配置自动化
可以用 Ansible 的 command 模块生成默认配置,再用 replace 模块修改 SystemdCgroup 参数:
- name: 生成 containerd 默认配置
ansible.builtin.command:
cmd: containerd config default
register: containerd_config
changed_when: false
- name: 写入 containerd 配置文件
ansible.builtin.copy:
content: "{{ containerd_config.stdout }}"
dest: "{{ containerd_config_path }}"
owner: root
group: root
mode: "0644"
notify: restart containerd
- name: 启用 SystemdCgroup
ansible.builtin.replace:
path: "{{ containerd_config_path }}"
regexp: 'SystemdCgroup = false'
replace: 'SystemdCgroup = true'
notify: restart containerd
- name: 配置 containerd snapshotter
ansible.builtin.replace:
path: "{{ containerd_config_path }}"
regexp: "snapshotter = ['\"]overlayfs['\"]"
replace: "snapshotter = '{{ containerd_snapshotter }}'"
notify: restart containerdcontainerd config default 会输出当前版本的完整默认配置(约 500+ 行)。这比手动编写 config.toml 更安全——你只会修改关键的 SystemdCgroup 一行,其余配置与 containerd 版本精确匹配。
本课程的节点运行在 Docker 容器里,containerd 默认的 overlayfs snapshotter 容易触发嵌套 overlay 挂载错误,所以这里额外改成 native snapshotter。真实物理机或虚拟机生产环境通常仍使用 overlayfs,课堂 Docker 实验优先保证 kubelet 能启动 Pod sandbox。
4.4 handlers/main.yml
上面的 copy 和 replace 任务都写了 notify: restart containerd。Ansible 不会立刻执行 handler,而是在当前 Play 的任务完成后统一触发一次,这样即使配置文件被多个任务修改,containerd 也只需要重启一次。
notify 的值必须和 handler 的 name 完全一致:
---
# roles/container_runtime/handlers/main.yml
- name: restart containerd
ansible.builtin.service:
name: "{{ containerd_service_name }}"
state: restarted五、Ansible Roles 工程结构
本课在已有 Roles 工程基础上扩展,目录结构如下:
ansible-k8s/
├── site.yml # K8s 部署总入口
├── inventory.ini # 含 k8s_master 和 k8s_workers 分组
├── ansible.cfg
├── keys/
│ ├── ops_ed25519
│ └── ops_ed25519.pub
├── group_vars/
│ └── all.yml
├── roles/
│ ├── k8s_prepare/ # 本课新增:节点系统预配置
│ │ ├── tasks/main.yml
│ │ └── defaults/main.yml
│ ├── container_runtime/ # 本课新增:containerd 安装配置
│ │ ├── tasks/main.yml
│ │ ├── handlers/main.yml
│ │ └── defaults/main.yml
│ ├── common/ # 复用第 11 课
│ ├── security/ # 复用第 11 课
│ └── firewall/ # 复用第 11 课(可选)
└── docker/
├── Dockerfile
└── docker-compose.yml5.1 site.yml 编排
问题:Ansible Roles 工程中用于统一调度所有角色的入口文件名是什么?
执行顺序必须是 k8s_prepare → container_runtime,因为 containerd 依赖内核模块(overlay)和 sysctl 参数(ip_forward)已配置完毕。
六、运行与验证
6.1 启动 Docker 集群(在 WSL Ubuntu 中执行)
所有命令在 WSL Ubuntu 终端内运行。K8s 节点准备需要 systemd、cgroup 和内核模块能力,不能直接沿用普通 sshd -D 容器。下面的 Dockerfile 使用 /sbin/init 作为 PID 1,Compose 额外挂载 /sys/fs/cgroup,让 systemctl、containerd 和后续 kubelet 命令有可运行的基础。
mkdir -p keys
test -f keys/ops_ed25519 || ssh-keygen -t ed25519 -f keys/ops_ed25519 -N "" -q
chmod 600 keys/ops_ed25519在 docker/Dockerfile 中写入:
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
systemd \
systemd-sysv \
dbus \
openssh-server \
python3 \
python3-apt \
sudo \
kmod \
iproute2 \
procps \
ca-certificates \
curl \
gnupg \
apt-transport-https \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /var/run/sshd && systemctl enable ssh
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]docker/docker-compose.yml 使用下方隐藏答案内容。注意 node_ip 使用容器网络中的固定地址,ansible_host=127.0.0.1 只用于控制端通过端口映射登录容器,两者不能混用。
问题:Docker Compose 中用于构建并后台启动多容器环境的命令是什么?
cd docker
docker compose up -d --build
cd ..6.2 确认连通性
ansible all -m ping -i inventory.ini预期输出:三台节点均返回 SUCCESS。
6.3 执行节点准备
# 预演
ansible-playbook site.yml --check --diff
# 正式执行
ansible-playbook site.yml | tee outputs/k8s-prepare.log6.4 验证 swap 已关闭
ansible all -m command -a "swapon --show" -i inventory.ini预期输出:空(无活跃 swap)。
6.5 验证内核模块
ansible all -m shell -a "grep -w overlay /proc/filesystems && sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables" -i inventory.ini预期输出:能看到 overlay,并且 bridge netfilter 两个值均为 1。在 OrbStack/Docker 这类容器实验环境中,内核能力可能已内建,不一定会出现在 lsmod 输出里。
6.6 验证 sysctl 参数
ansible all -m command -a "sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward" -i inventory.ini预期输出:三个值均为 1。
6.7 验证 containerd
ansible all -m command -a "containerd --version" -i inventory.ini
ansible all -m command -a "systemctl is-active containerd" -i inventory.ini
# 确认 SystemdCgroup 已启用
ansible all -m command -a "grep SystemdCgroup /etc/containerd/config.toml" -i inventory.ini
# 确认 Docker 嵌套实验使用 native snapshotter
ansible all -m command -a "grep \"snapshotter = 'native'\" /etc/containerd/config.toml" -i inventory.ini预期输出:SystemdCgroup = true,并能看到 snapshotter = 'native'。
6.8 验证时间同步
ansible all -m shell -a "chronyc tracking | grep 'System time'" -i inventory.ini预期输出:System time : 0.0 或接近 0 的微小偏移。
七、提交物清单
| 提交物 | 要求 |
|---|---|
k8s_prepare Role | 完整的 tasks/main.yml 和 defaults/main.yml,覆盖主机名、hosts、swap、内核能力、sysctl、chrony 等步骤,每个 task 有清晰的 name |
container_runtime Role | 完整的 tasks/main.yml、handlers/main.yml 和 defaults/main.yml,containerd 配置中 SystemdCgroup=true,Docker 嵌套实验使用 native snapshotter |
inventory.ini | 划分 k8s_master 和 k8s_workers 组,k8s_cluster:children 包含两个子组 |
| 检查截图 | swap 关闭、内核模块、sysctl 参数、containerd 状态、时间同步各一张截图 |
| 运行日志 | ansible-playbook site.yml 完整输出 |
八、故障排查
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
modprobe: FATAL | 容器共享宿主机内核,但 /lib/modules 中没有对应模块文件 | 先确认 overlay 和 bridge sysctl 是否已可用;已内建时无需强制依赖 lsmod |
systemctl 在容器中无法使用 | Docker 容器默认没有 systemd | 容器内用 service 命令或直接启动进程;或使用支持 systemd 的基础镜像 |
| containerd 安装失败 | APT 源未包含 containerd.io | 确认 Docker 官方 GPG 密钥和 APT 仓库已添加 |
| kubelet 报 cgroup 驱动不匹配 | SystemdCgroup 未设为 true | 检查 /etc/containerd/config.toml 中对应参数 |
Pod sandbox 报 failed to mount rootfs ... overlay ... invalid argument | Docker 容器内再次使用 overlayfs,触发嵌套 overlay 限制 | 将 containerd 的 snapshotter = 'overlayfs' 改为 snapshotter = 'native' 并重启 containerd |
swapoff -a 在容器中无效 | 容器没有活跃 swap,或未获得 swap 控制权限 | 先执行 swapon --show --noheadings;有输出时再执行 swapoff -a |
sysctl 只读文件系统 | 容器内核参数与宿主机共享 | --sysctl 参数在 docker run 时设置;或 privileged: true 允许容器修改 |
chronyc 命令不存在 | chrony 未安装 | apt install -y chrony;如果容器不支持 NTP,跳过时间同步验证 |
九、进阶任务
探索任务 1:使用 Ansible Facts 动态配置 hosts
不写死 hosts 文件内容,而是遍历 groups['k8s_cluster'] 中的每个主机,用 Inventory 中的 node_ip 和主机名生成 hosts 条目。这一步让 Docker 端口映射地址(127.0.0.1)和集群内部通信地址(172.28.0.x)各司其职,不会互相污染。
探索任务 2:编写预检 Playbook
编写一个独立的 preflight.yml,在节点准备完成后、kubeadm 执行前检查以下条件:
- swap 已关闭(
swapon --show无输出) - 所需内核能力已可用
- sysctl 参数已生效
- containerd 正在运行
- 时间偏差 < 1 秒
使用 assert 模块逐项判断,失败时给出明确的错误提示。
探索任务 3:配置 containerd 镜像加速
在 container_runtime role 中增加镜像加速配置,写入 /etc/containerd/certs.d/docker.io/hosts.toml。参考第 7 课的 containerd hosts.toml 格式,让节点可以拉取国内镜像源。
