基于K8s的云原生AI基础设施:架构、部署与实践【003】-AI算力的驰骋之地

Worker Node 批量纳管:从手工部署到 Ansible 自动化

在前面的内容中,Kubernetes 控制平面已经完成初始化,Master 节点也已经具备了最基本的运行能力。但对于一个真正面向 AI 场景的集群来说,完成控制平面部署只是第一步,接下来的重点会迅速转移到 Worker Node。

这是因为,真正承载训练任务、推理服务和各类计算负载的,往往不是 Master,而是数量更多的 Worker 节点。如果这些节点仍然依赖手工逐台配置,那么随着节点数量增加,部署效率、一致性控制以及后续维护成本都会迅速失控。尤其是在 AI 集群场景下,容器运行时、内核模块、sysctl 参数、Kubernetes 三件套、镜像仓库配置以及节点接入过程,都要求节点环境具备较高的一致性。

也正因为如此,从这一节开始,部署方式将从“手工分步骤搭建 Master”进一步推进到“利用 Ansible 对 Worker Node 进行批量自动化处理”。

前面的手工部署并不是多余的,它解决的是理解 Kubernetes 集群是怎么一步一步搭起来的;而这一节的自动化,则解决的是如何让这些步骤在多节点环境中可重复、可批量、可维护地落地

下面将按照从连通性检查、Ansible 项目构建,到基础环境修正、容器运行时准备、Kubernetes 组件安装、私有镜像仓库配置以及节点加入集群的顺序,逐步完成 Worker Node 的批量纳管。

批量操作前的连通性检查

在正式执行自动化任务之前,首先要确认控制节点与目标 Worker Node 之间具备基础网络连通性。这一步虽然简单,但非常必要,因为后续无论是 SSH 免密分发,还是 Playbook 批量执行,都依赖节点可达这一前提。这里使用 fping 对目标网段进行快速探测,分别确认哪些地址不可达、哪些地址可达,从而为后续主机清单编制和故障排查提供基础信息。

# 安装 fping
sudo apt install -y fping
# 有哪些ping不通?
fping -u -g 10.x.x.0/24 2>/dev/null
# 有哪些ping能通?
fping -a -g 10.x.x.0/24 2>/dev/null

Ansible 项目初始化

要让 Ansible 能够稳定地批量管理 Worker Node,首先需要准备控制节点侧的项目结构,包括工具安装、全局配置、主机清单和 SSH 认证方式。这一步的目标,是把后续分散的节点操作固化为统一、可重复执行的自动化入口。

#1. 在控制节点安装 Ansible
# Ansible 是后续批量管理 Worker Node 的核心工具。这里以 Ubuntu 22.04.5 为例,通过官方推荐方式安装较新的 Ansible 版本。
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install ansible

# 安装完成后,建议先检查版本信息,确认 Ansible 已可正常运行。
sudo ansible --version

ansible [core 2.19.4]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3/dist-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.12.3 (main, Nov  6 2024, 18:32:19) [GCC 13.2.0] (/usr/bin/python3)
  jinja version = 3.1.2
  pyyaml version = 6.0.1 (with libyaml v0.2.5)
  
  
#2. 定义全局配置:ansible.cfg
ansible.cfg 用于统一定义 Ansible 的默认行为和运行规则,使 Playbook 执行过程更一致、可控。
在多节点场景下,这一步尤其重要,因为它决定了默认 inventory 文件、并发数、SSH 超时以及 Python 解释器选择方式。

vi ansible.cfg
[defaults]
host_key_checking = False
remote_user = xxx
inventory = ./hosts.ini
forks = 20
timeout = 30
interpreter_python = auto_silent

# 这里有几个关键点值得说明:
* 关闭 host_key_checking,可以避免第一次连接时被 yes/no 交互阻塞;
* remote_user 指定后续默认通过哪个用户登录目标节点;
* inventory 明确默认主机清单文件;
* forks 决定批量并发数,节点较多时可以适当调大;
* interpreter_python = auto_silent 可以降低 Python 路径不一致带来的问题。

# 3. 定义主机清单:hosts.ini
hosts.ini 的作用,是明确告诉 Ansible:需要管理哪些主机、这些主机属于哪些分组,以及如何连接它们。
在这里,Worker Node 被统一归入 workers 组,并进一步纳入 all_nodes 聚合组中,方便后续 Playbook 按角色或按全集群执行。
vi hosts.ini
[workers]
gpu-worker-1 ansible_host=10.x.x.1
gpu-worker-1 ansible_host=10.x.x.2
gpu-worker-1 ansible_host=10.x.x.3

[all_nodes:children]
workers
在节点规模较大时,建议保持命名和分组方式统一,这样后续不管是扩容、分批执行还是针对失败节点重试,都会更清晰。

# 4.设置 SSH 免密登录

在正式执行 Playbook 之前,还需要先打通控制节点到目标节点的 SSH 免密访问。
这是后续 Ansible 稳定批量执行的基础前提之一。

# 先检查当前控制节点是否已经存在公钥:
ls ~/.ssh/
# 若没有,执行如下命令,一直回车至结束。
ssh-keygen

# 定义变量,方便后续使用
PUBKEY="$(cat ~/.ssh/id_rsa.pub)"

# 检查变量输出
echo "$PUBKEY"

# 批量下发公钥
ansible all -i hosts.ini -m authorized_key \
  -a "user=hpcc key='${PUBKEY}'" \
  --ask-pass
  
# 测试验证ansible控制节点是否可以正常连接至ansible管理节点(由于在ansible.cfg文件里写了inventory所以这里不需要-i hosts.ini)
ansible all -m ping

# 其它
# 如果不做免密登陆可用如下方法,快速手工测试可加 --ask-pass 参数 (option)
ansible all -i hosts.ini -m ping --ask-pass

建立 Playbook 并按步骤执行

在 Kubernetes 节点真正可用之前,首先必须具备稳定可用的容器运行时。这一部分的目标,就是在所有 Worker Node 上批量安装并配置 containerd,并完成基础版本检查。

 ansible-playbook playbook/s1_container_runtime_deploy.yml -K

Playbook样例如下

---
- name: K8s Step1 - Install and configure containerd
  hosts: workers
  become: yes
  gather_facts: yes

  tasks:
    # 1. 更新 apt 缓存
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

    # 2. 安装 containerd
    - name: Install containerd package
      apt:
        name: containerd
        state: present

    # 3. 创建 /etc/containerd 目录
    - name: Ensure /etc/containerd directory exists
      file:
        path: /etc/containerd
        state: directory
        mode: '0755'

    # 4. 生成默认配置(如果不存在就生成一次)
    - name: Generate default containerd config if not exists
      shell: "containerd config default > /etc/containerd/config.toml"
      args:
        creates: /etc/containerd/config.toml

    # 5. 确保 config.toml 权限与属主正确
    - name: Ensure containerd config file ownership and permissions
      file:
        path: /etc/containerd/config.toml
        owner: root
        group: root
        mode: '0644'

    # 6. 重启并开机自启 containerd
    - name: Restart and enable containerd service
      systemd:
        name: containerd
        state: restarted
        enabled: yes
        daemon_reload: yes

    # 7. 验证 containerd 版本
    - name: Check containerd version
      command: containerd --version
      register: containerd_version
      changed_when: false
      failed_when: false   # 如果命令异常,不让整个 play 失败,先收集信息

    - name: Show containerd version
      debug:
        msg: "containerd version on {{ inventory_hostname }}: {{ containerd_version.stdout | default('command failed') }}"

    # 8. 验证 runc 版本
    - name: Check runc version
      command: runc --version
      register: runc_version
      changed_when: false
      failed_when: false

    - name: Show runc version
      debug:
        msg: "runc version on {{ inventory_hostname }}: {{ runc_version.stdout | default('command failed') }}"

批量完成 Kubernetes 节点环境准备

在容器运行时就绪之后,还需要进一步统一 Worker Node 的内核模块、网络转发参数、Swap 状态、cgroup 驱动以及 pause 镜像配置。这些内容看似分散,但实际上都是 Kubernetes 能否稳定运行的基础前提。

ansible-playbook playbook/s2_k8s_env_ready.yml -K

Playbook样例如下

---
- name: K8s Step2 - Kernel modules, sysctl, swap, cgroup & containerd tuning
  hosts: workers
  become: yes
  gather_facts: no

  tasks:
  ############################################################
  # 第一部分:加载内核模块 + 开机自启
  ############################################################
    - name: Load required kernel modules (overlay, br_netfilter, vxlan)
      modprobe:
        name: "{{ item }}"
        state: present
      loop:
        - overlay
        - br_netfilter
        - vxlan

    - name: Ensure kernel modules auto-load on boot
      copy:
        dest: /etc/modules-load.d/k8s.conf
        content: |
          overlay
          br_netfilter
          vxlan

    - name: Check loaded kernel modules
      shell: |
        for m in overlay br_netfilter vxlan; do
          lsmod | grep -q "$m" && echo "$m loaded" || echo "$m not loaded"
        done
      register: kernel_check
      changed_when: false

    - debug:
        msg: "{{ kernel_check.stdout_lines }}"

  ############################################################
  # 第二部分:sysctl 网络转发规则
  ############################################################
    - name: Configure sysctl for Kubernetes networking
      copy:
        dest: /etc/sysctl.d/k8s.conf
        content: |
          net.bridge.bridge-nf-call-iptables = 1
          net.bridge.bridge-nf-call-ip6tables = 1
          net.ipv4.ip_forward = 1

    - name: Apply sysctl settings
      command: sysctl --system
      register: sysctl_apply
      changed_when: "'Applying /etc/sysctl.d/k8s.conf' in sysctl_apply.stdout"

    - name: Verify sysctl values
      shell: |
        sysctl net.bridge.bridge-nf-call-iptables \\
               net.bridge.bridge-nf-call-ip6tables \\
               net.ipv4.ip_forward
      register: sysctl_check
      changed_when: false

    - debug:
        msg: "{{ sysctl_check.stdout_lines }}"

  ############################################################
  # 第三部分:Swap、Cgroup、Pause image 修复
  ############################################################

    # 关闭 swap(立即生效)
    - name: Disable swap temporarily
      command: swapoff -a

    # 注释 fstab 中所有 swap 行(永久生效)
    - name: Disable swap in fstab permanently
      replace:
        path: /etc/fstab
        regexp: '^([^#].*\\sswap\\s.*)$'
        replace: '#\\1'

    # 检查 swap 是否真的关闭
    - name: Check swap status
      command: free -m
      register: swap_check
      changed_when: false

    - debug:
        msg: "{{ swap_check.stdout_lines }}"

    # 修改 containerd SystemdCgroup 为 true
    - name: Enable SystemdCgroup in containerd
      replace:
        path: /etc/containerd/config.toml
        regexp: '^\\s*SystemdCgroup = false'
        replace: 'SystemdCgroup = true'

    # 修改 sandbox image 为阿里云 pause:3.10
    - name: Change containerd sandbox_image to pause:3.10
      replace:
        path: /etc/containerd/config.toml
        regexp: 'registry.k8s.io/pause:[^"]*'
        replace: 'registry.aliyuncs.com/google_containers/pause:3.10'

    # 重启 containerd
    - name: Restart containerd
      systemd:
        name: containerd
        state: restarted
        enabled: yes

    # 检查 SystemdCgroup 修改结果
    - name: Verify SystemdCgroup setting
      shell: grep 'SystemdCgroup' /etc/containerd/config.toml
      register: cgroup_check
      changed_when: false

    - debug:
        msg: "{{ cgroup_check.stdout_lines }}"

    # 检查 pause 镜像配置
    - name: Verify sandbox_image setting
      shell: grep 'pause' /etc/containerd/config.toml
      register: pause_check
      changed_when: false

    - debug:
        msg: "{{ pause_check.stdout_lines }}"

    # 查看 containerd 服务状态
    - name: Check containerd service status
      command: systemctl status containerd
      register: containerd_status
      changed_when: false

    - debug:
        msg: "{{ containerd_status.stdout_lines }}"

批量安装 Kubernetes 三大组件

在完成运行环境准备之后,下一步就是安装 Kubernetes 的三大核心组件:kubeadmkubeletkubectl。这一部分的目标,是为所有 Worker Node 提供统一版本的 Kubernetes 节点能力,并尽量避免版本漂移。

ansible-playbook playbook/s3_k8s_3components_install.yml -K

Playbook样例如下

---
- name: K8s Step3 - Install kubeadm, kubelet, kubectl (v1.33.5)
  hosts: workers
  become: yes
  gather_facts: no

  tasks:
    # 1. 安装前置依赖
    - name: Install Kubernetes APT dependencies
      apt:
        name:
          - apt-transport-https
          - ca-certificates
          - curl
          - gpg
        state: present
        update_cache: yes

    # 2. 确保 keyrings 目录存在
    - name: Ensure /etc/apt/keyrings directory exists
      file:
        path: /etc/apt/keyrings
        state: directory
        mode: '0755'

    # 3. 下载 Kubernetes v1.33 的 Release.key 并转换为 gpg keyring
    - name: Download Kubernetes v1.33 APT Release key
      get_url:
        url: <https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key>
        dest: /tmp/kubernetes-apt-release.key
        mode: '0644'

    - name: Convert Kubernetes APT key to keyring
      command: >
        gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
        /tmp/kubernetes-apt-release.key
      args:
        creates: /etc/apt/keyrings/kubernetes-apt-keyring.gpg

    # 4. 写 APT 源配置文件
    - name: Configure Kubernetes v1.33 APT repository
      copy:
        dest: /etc/apt/sources.list.d/kubernetes.list
        content: |
          deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] <https://pkgs.k8s.io/core:/stable:/v1.33/deb/> /
        mode: '0644'

    # 5. 更新 APT 索引
    - name: Update APT cache after adding Kubernetes repo
      apt:
        update_cache: yes

    # 6. 安装指定版本的 kubelet / kubeadm / kubectl
    - name: Install kubeadm, kubelet, kubectl v1.33.5-1.1
      apt:
        name:
          - kubelet=1.33.5-1.1
          - kubeadm=1.33.5-1.1
          - kubectl=1.33.5-1.1
        state: present
        allow_downgrade: yes

    # 7. 设置版本 hold,带重试逻辑,避免 apt 锁冲突导致失败
    - name: Hold kubelet, kubeadm, kubectl versions
      command: apt-mark hold kubelet kubeadm kubectl
      register: hold_cmd
      retries: 5           # 最多重试 5 次
      delay: 10            # 每次间隔 10 秒
      until: hold_cmd.rc == 0

    ##################################################
    # 检查部分
    ##################################################

    # 8. 检查 kubeadm 版本
    - name: Check kubeadm version
      command: kubeadm version -o yaml
      register: kubeadm_ver
      changed_when: false
      failed_when: false

    - name: Show kubeadm version
      debug:
        msg: "{{ kubeadm_ver.stdout | default('kubeadm version command failed') }}"

    # 9. 检查 kubelet 版本
    - name: Check kubelet version
      command: kubelet --version
      register: kubelet_ver
      changed_when: false
      failed_when: false

    - name: Show kubelet version
      debug:
        msg: "{{ kubelet_ver.stdout | default('kubelet version command failed') }}"

    # 10. 检查 kubectl 客户端版本
    - name: Check kubectl client version
      command: kubectl version --client --output=yaml
      register: kubectl_ver
      changed_when: false
      failed_when: false

    - name: Show kubectl client version
      debug:
        msg: "{{ kubectl_ver.stdout | default('kubectl version command failed') }}"

    # 11. 检查 kubelet 服务状态(未 join 前 inactive 是正常的)
    - name: Check kubelet service status
      command: systemctl status kubelet
      register: kubelet_status
      changed_when: false
      failed_when: false

    - name: Show kubelet service status (Active line)
      debug:
        msg: "{{ item }}"
      loop: "{{ kubelet_status.stdout_lines | default([]) }}"
      when: "'Active:' in item"

    # 12. 检查 APT 源是否正确配置
    - name: Verify Kubernetes APT repository line
      shell: grep -R "pkgs.k8s.io/core:/stable:/v1.33" /etc/apt/sources.list.d/ || echo 'k8s repo not found'
      register: repo_check
      changed_when: false

    - name: Show repo check result
      debug:
        msg: "{{ repo_check.stdout_lines }}"

    # 13. 检查 apt-mark hold 状态
    - name: Check held packages for kubelet/kubeadm/kubectl
      shell: apt-mark showhold | grep -E "kubelet|kubeadm|kubectl" || echo 'no hold found'
      register: hold_check
      changed_when: false

    - name: Show hold status
      debug:
        msg: "{{ hold_check.stdout_lines }}"

批量将 Worker Node 加入集群

当前面的基础环境、容器运行时、Kubernetes 组件和镜像仓库访问都准备完成之后,就可以正式让 Worker Node 加入集群。

ansible-playbook playbook/s5_k8s_join_workers.yml -K

Playbook样例如下

---
- name: K8s Step4 - kubeadm join workers to cluster
  hosts: workers
  become: yes
  gather_facts: no

  vars:
    # 按你现在集群的实际情况填
    kubeadm_join_command: >-
      kubeadm join 10.x.x.200:8443
      --token abcdef.xxxdef
      --discovery-token-ca-cert-hash sha256:57a047xxxe517db537204

  tasks:
    # 1. 判断该节点是否已经在集群中(简单依据:kubelet.conf 是否存在)
    - name: Check if kubelet.conf exists (joined or initialized)
      stat:
        path: /etc/kubernetes/kubelet.conf
      register: kubelet_conf

    - name: Show join status hint
      debug:
        msg: >
          kubelet.conf exists on {{ inventory_hostname }},
          this node looks already joined or initialized. Skip join.
      when: kubelet_conf.stat.exists

    # 2. 执行 kubeadm join(仅在还没 join 的节点上)
    - name: Run kubeadm join
      command: "{{ kubeadm_join_command }}"
      register: join_result
      when: not kubelet_conf.stat.exists
      # kubeadm join 只会执行一次,不需要认为 changed_when
      # 让 Ansible 正常感知 changed 状态即可

    - name: Show kubeadm join output
      debug:
        msg: "{{ join_result.stdout_lines | default(['kubeadm join not executed (node already joined).']) }}"
      when: not kubelet_conf.stat.exists

    # 3. 确保 kubelet 服务已启用并启动
    - name: Enable and start kubelet service
      systemd:
        name: kubelet
        enabled: yes
        state: started
      # 已 join 与否都可以执行一次,确保 kubelet 在跑

到这里,Worker Node 已经通过 Ansible 完成了批量化的基础环境修正、容器运行时准备、Kubernetes 组件安装、私有镜像仓库配置以及节点加入集群等关键步骤。相比逐台手工处理,这种方式不仅显著降低了重复劳动,也让节点侧配置具备了更好的一致性、可复用性和可维护性。

这一小节更重要的是为了让 Kubernetes 集群的节点纳管过程真正具备工程化能力。前面的 Master 节点部署解决的是“如何把 Kubernetes 一步一步搭起来”;而这里的 Ansible 自动化,解决的是“如何把这些步骤稳定地复制到更多节点上”。

Leave a Reply