Ansible 自动化部署 K8s 三节点集群(完整教程)

用 Ansible 把「装系统环境 → 装运行时 → 装 K8s 组件 → 拉镜像」全流程脚本化,告别手动敲命令。本文所有 Playbook 均来自本人真实实验环境,可直接复用。

0. 项目概览

目标:用 Ansible 自动化部署一套三节点 K8s 集群(1 master + 2 node),容器运行时用 Docker(经 cri-dockerd 适配),网络用 Calico,镜像走 Harbor 私有仓库 + 阿里云加速。

架构拓扑

主机 角色 IP
workstation Ansible 控制机 192.168.211.201
k8s-master 集群主节点 192.168.211.202
k8s-node01 工作节点 192.168.211.203
k8s-node02 工作节点 192.168.211.204
hb.reg.com Harbor 私有仓库 192.168.211.101

技术栈:Ansible + Docker(cri-dockerd) + Calico + Harbor + 阿里云镜像源。

1. 目录结构一览

复制代码
/home/devops/ansible/
├── ansible.cfg              # Ansible 全局配置
├── inventory                # 主机清单
├── hosts.yml                # 生成 /etc/hosts
├── set_hostname.yml         # 设置主机名
├── swapoff.yml              # 关闭 swap(含避坑重点)
├── selinux.yml              # 关防火墙 + SELinux
├── max_limits.yml           # 修改最大连接数
├── ip_forward.yml           # 开启路由转发
├── yum_repository.yml       # 添加 K8s/Docker 阿里云源
├── yum_basic.yml            # 装基础软件
├── yum_docker.yml           # 装 Docker
├── docker_daemon.yml        # 配置并启动 Docker
├── yum_k8s.yml              # 装 kubelet/kubeadm/kubectl
├── install_cri_docker.yml   # 装 cri-dockerd
├── pull_images.yml          # 从 Harbor 拉 K8s 组件镜像
├── copy_calico.yml          # 拷贝并加载 Calico 离线镜像
├── templates/
│   ├── hosts.j2             # hosts 文件模板
│   └── docker_daemon.j2     # Docker 配置模板
└── app/                     # 离线安装包
    ├── calico-images_v3.32.1.tar.gz
    └── cri-dockerd-0.4.4.amd64.tgz

2. 基础配置:inventory + ansible.cfg

inventory(主机清单) ------按角色分组,为后续 hosts: xxx 提供目标:

复制代码
[k8s_master]
192.168.211.202 hostname=k8s-master
​
[k8s_nodes]
192.168.211.203 hostname=k8s-node01
192.168.211.204 hostname=k8s-node02
​
[k8s_cluster:children]   # 子分组:集群 = master + nodes
k8s_master
k8s_nodes

ansible.cfg------指定清单、免密登录用户:

复制代码
[defaults]
inventory=/home/devops/ansible/inventory
remote_user=root              # 用 root 免密登录目标机
host_key_checking = False     # 跳过首次 SSH 指纹确认

前置:控制机已 ssh-copy-id 把公钥分发到三台节点,实现 root 免密。

3. 第一阶段:系统初始化(三台都做)

3.1 set_hostname.yml ------ 设置主机名

ansible.builtin.hostname 模块给每台机器改名:

复制代码
- name: set hostname
  hosts: k8s_master
  tasks:
    - ansible.builtin.hostname:
        name: k8s-master
- name: node01
  hosts: 192.168.211.203
  tasks:
    - ansible.builtin.hostname:
        name: k8s-node01
# ... node02 同理

进阶写法:inventory 里已定义 hostname 变量,可直接 name: "{``{ hostname }}" 遍历,避免写死(文件里这段被注释保留,供参考)。

3.2 hosts.yml ------ 生成 /etc/hosts

template 模块 + Jinja2 模板,自动把清单里的机器写进 /etc/hosts:

复制代码
- name: create hosts
  hosts: all
  tasks:
    - ansible.builtin.template:
        src: /home/devops/ansible/templates/hosts.j2
        dest: /etc/hosts

hosts.j2 模板(动态遍历所有主机 + 固定写死 Harbor):

复制代码
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
{% for item in groups.all %}
{{ hostvars[item]['inventory_hostname'] }} {{ hostvars[item]['hostname'] }}
{% endfor %}
192.168.211.101 hb.reg.com

3.3 swapoff.yml ------ 关闭 swap(★ 本篇避坑重点)

K8s 要求关闭 swap,swapoff -a 只是临时生效,重启后 fstab 会重新挂载,所以要永久注释 fstab 里的 swap 行

复制代码
- name: 关闭交换分区
  hosts: all
  tasks:
    - name: swapoff -a
      ansible.builtin.command: swapoff -a
    - name: 永久关闭swap分区
      ansible.builtin.lineinfile:
        path: /etc/fstab
        regexp: '^([^#].*\sswap\s.*)$'   # 匹配未注释的 swap 行,并整行捕获
        line: '#\1'                       # 用 # + 捕获组内容替换
        backup: yes                       # 改前自动备份
        backrefs: yes                     # ★ 关键:开启反向引用
    - name: mount -a
      ansible.builtin.command: mount -a

这里我踩过两次坑,总结成问题档案:

次数 line 写法 结果 根因
第 1 次 line: '#1' fstab 变成垃圾 #1 缺反斜杠,1 被当普通字符
第 2 次 line: '#\1' fstab 变成垃圾 #\1 有反斜杠但backrefs: yes

三个必记知识点

  1. line: '#\1' 里的 \1 表示「regexp 第一个捕获组的内容」,但只有 backrefs: yes 才会解析它 ,否则 \1 被当字面字符原样输出。

  2. regexp\s 反斜杠,必须用单引号包裹,双引号会被 YAML 转义。

  3. backup: yes 会生成 fstab.<INODE>.<时间戳>~ 备份,写坏了用它恢复。

更省心的替代写法(K8s 场景够用):state: absent 直接把 swap 行删掉,不保留记录、不踩 backrefs 的坑。

3.4 selinux.yml ------ 关防火墙 + SELinux

复制代码
- name: 关闭防火墙和selinux
  hosts: all
  tasks:
    - name: Stop service firewall
      ansible.builtin.service:
        name: firewalld
        state: stopped
        enabled: no
    - name: 临时关闭 SELinux
      ansible.builtin.command: setenforce 0
      changed_when: false                       # 避免误报 changed
      when: ansible_selinux.status == "enabled"
    - name: SELinux 改为 permissive
      ansible.builtin.lineinfile:
        path: /etc/selinux/config
        regexp: '^SELINUX='
        line: SELINUX=permissive

亮点:setenforce 0 这类命令每次跑都会报 changed,用 changed_when: false 让它「改了也不报变化」,保持幂等。

3.5 max_limits.yml ------ 修改最大连接数

blockinfile 向 limits.conf 追加一个块(可重复执行、不会重复插入):

复制代码
- name: 修改linux最大连接数
  hosts: all
  tasks:
    - ansible.builtin.blockinfile:
        path: /etc/security/limits.conf
        block: |
          *      soft    nofile      655350
          *      hard    nofile      655350
          *      soft    nproc       655350
          *      hard    nproc       655350
          *      soft    memlock     unlimited
          *      hard    memlock     unlimited

3.6 ip_forward.yml ------ 开启路由转发

复制代码
- ansible.builtin.blockinfile:
    path: /etc/sysctl.conf
    block: net.ipv4.ip_forward=1
- ansible.builtin.command: sysctl -p

文件里还保留了注释掉的 ansible.posix.sysctl 模块写法(更规范、自动 reload),需要 ansible-galaxy collection install ansible.posix

4. 第二阶段:装运行时 + K8s 组件

4.1 yum_repository.yml ------ 加国内源

yum_repository 模块添加阿里云 K8s 源和 Docker 源(替代手写 .repo 文件):

复制代码
- name: 添加k8s源
  ansible.builtin.yum_repository:
    name: Kubernetes
    baseurl: https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/rpm/
    gpgcheck: yes
    gpgkey: https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/rpm/repodata/repomd.xml.key
- name: 添加docker源
  ansible.builtin.yum_repository:
    name: Docker
    baseurl: https://mirrors.aliyun.com/docker-ce/linux/rhel/9/x86_64/stable/
    gpgcheck: no

4.2 yum_basic.yml ------ 装基础软件

dnf 模块批量装:net-tools、nfs-utils、vim、wget、curl、bash-completion、device-mapper-persistent-data、psmisc、tree、ipvsadm、conntrack-tools 等(K8s 部署依赖项一次装齐)。

4.3 yum_docker.yml ------ 装 Docker

复制代码
- name: Install docker
  ansible.builtin.dnf:
    name:
      - docker-ce
      - docker-ce-cli
      - containerd.io
      - docker-buildx-plugin
      - docker-compose-plugin
    state: present

4.4 docker_daemon.yml ------ 配置并启动 Docker

template 生成 daemon.json,notify 触发 handler 重启 Docker:

复制代码
- name: Template daemon.json
  ansible.builtin.template:
    src: templates/docker_daemon.j2
    dest: /etc/docker/daemon.json
  notify: restart docker      # 配置变了自动重启
- name: 启动docker
  ansible.builtin.service:
    name: docker
    state: started
    enabled: yes
handlers:
  - name: restart docker
    ansible.builtin.service:
      name: docker
      state: restarted

docker_daemon.j2 模板(关键项:cgroupdriver、数据目录、镜像加速、私有仓库):

复制代码
{
  "data-root": "/data/docker",
  "exec-opts": ["native.cgroupdriver=systemd"],   # 与 kubelet 的 cgroup 驱动一致
  "log-driver": "json-file",
  "log-opts": { "max-size": "100m", "max-file": "50" },
  "insecure-registries": ["hb.reg.com"],
  "registry-mirrors": [
    "https://docker.m.daocloud.io", "https://docker.1ms.run",
    "https://dockerproxy.net", "https://proxy.vvvv.ee"
  ]
}

细节观察exec-optsnative.cgroupdriver=systemd 是重点------K8s 官方要求 kubelet 与容器运行时的 cgroup 驱动一致,都应是 systemd。另外 insecure-registries 的规范写法是 hb.reg.comhttp://hb.reg.com(不带 https://),带 https:// 会被 Docker 忽略,仅当 Harbor 是自签 HTTP 时才需要这一项。

4.5 yum_k8s.yml ------ 装 kubelet/kubeadm/kubectl

复制代码
- name: install kubelet kubeadm kubectl
  ansible.builtin.yum:
    name: [kubectl, kubeadm, kubelet]
    state: present
- name: enable kubelet
  ansible.builtin.service:
    name: kubelet
    state: started
    enabled: yes

4.6 install_cri_docker.yml ------ 装 cri-dockerd(为什么需要它)

背景 :K8s 1.24 起移除了 dockershim,Docker 不能再直接作为容器运行时,需要 cri-dockerd 这个「翻译官」把 K8s 的 CRI 调用转给 Docker。

步骤:解压离线包 → 拷贝二进制到 /usr/bin → 写 systemd 服务单元 → 启动。

复制代码
- name: 解压压缩包
  ansible.builtin.unarchive:
    src: /home/devops/ansible/app/cri-dockerd-0.4.4.amd64.tgz
    dest: /tmp/
- name: 拷贝文件
  ansible.builtin.copy:
    src: /tmp/cri-dockerd/cri-dockerd
    dest: /usr/bin/cri-dockerd
    remote_src: yes
- name: 创建cri-docker服务   # 用 copy 的 content 参数直接内联服务单元
  ansible.builtin.copy:
    dest: /usr/lib/systemd/system/cri-docker.service
    content: |
      [Unit]
      Description=CRI Interface for Docker Application Container Engine
      After=network-online.target docker.service
      Requires=cri-docker.socket
      [Service]
      ExecStart=/usr/bin/cri-dockerd --container-runtime-endpoint fd:// \
        --network-plugin=cni \
        --pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.10
      [Install]
      WantedBy=multi-user.target
# 另有 cri-docker.socket 单元 + daemon-reload + 启动,略

关键点--pod-infra-container-image 指向的 pause 镜像要用国内源,否则 kubeadm 起 Pod 时拉 pause 失败。

5. 第三阶段:镜像准备(内网加速的关键)

5.1 pull_images.yml ------ 从 Harbor 预拉镜像

kubeadm init 时默认去 registry.k8s.io 拉镜像,内网根本拉不动。所以提前从私有 Harbor 拉好,kubeadm 检测到本地已有镜像就跳过网络拉取。

复制代码
- name: k8s-master pull images
  hosts: k8s_master
  tasks:
    - name: pull kube-apiserver:v1.36.3
      ansible.builtin.command: docker pull hb.reg.com/k8s/kube-apiserver:v1.36.3
    # controller-manager / scheduler / kube-proxy / coredns / pause / etcd 同理
- name: k8s-node pull images
  hosts: k8s_nodes
  tasks:
    - name: pull kube-proxy:v1.36.3
      ansible.builtin.command: docker pull hb.reg.com/k8s/kube-proxy:v1.36.3
    # coredns / pause 同理

细节观察 :master 的 etcd 任务名写的是 pull etcd:3.6.11-0,但实际命令拉的是 etcd:3.6.8-0------task 名称与命令不一致,属于典型的「改名忘同步」小笔误,不影响执行,但排查时容易误导。写 Playbook 时 task name 最好和实际动作保持一致。

5.2 copy_calico.yml ------ 离线加载 Calico 镜像

内网装 Calico 也一样,先把镜像打成 tar 包拷过去,再 docker load

复制代码
- name: copy
  ansible.builtin.copy:
    src: /home/devops/ansible/app/calico-images_v3.32.1.tar.gz
    dest: /root/calico-images_v3.32.1.tar.gz
- name: load -i
  ansible.builtin.shell: docker load -i /root/calico-images_v3.32.1.tar.gz
  register: load_result
  changed_when: "'Loaded image' in load_result.stdout"   # 只有真的加载了才报 changed

幂等技巧docker load 重复加载相同镜像不会报错但也无输出,用 changed_when 判断 stdout 里是否出现 Loaded image,只在真正加载时才标记为「已变更」。

6. 手动收尾:kubeadm init + join + Calico

环境就绪后,剩余三步手动完成(也可再封装成 Playbook):

复制代码
# master 上初始化(指定阿里云镜像仓库 + Calico 网段)
kubeadm init --pod-network-cidr=10.244.0.0/16 \
  --image-repository=registry.aliyuncs.com/google_containers

# node 上用 init 输出的命令加入
kubeadm join 192.168.211.202:6443 --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

# master 上部署 Calico 网络(镜像已离线 load 过)
kubectl apply -f calico.yaml

7. 一键执行顺序(完整流程)

复制代码
cd /home/devops/ansible
ansible-playbook set_hostname.yml      # 1 主机名
ansible-playbook hosts.yml             # 2 hosts 解析
ansible-playbook swapoff.yml           # 3 关 swap
ansible-playbook selinux.yml           # 4 防火墙/SELinux
ansible-playbook max_limits.yml        # 5 连接数
ansible-playbook ip_forward.yml        # 6 路由转发
ansible-playbook yum_repository.yml    # 7 加源
ansible-playbook yum_basic.yml         # 8 基础软件
ansible-playbook yum_docker.yml        # 9 装 Docker
ansible-playbook docker_daemon.yml     # 10 配置 Docker
ansible-playbook yum_k8s.yml           # 11 装 K8s 组件
ansible-playbook install_cri_docker.yml# 12 装 cri-dockerd
ansible-playbook pull_images.yml       # 13 拉 K8s 镜像
ansible-playbook copy_calico.yml       # 14 加载 Calico 镜像
# 15 kubeadm init + join + apply calico(手动)

8. 避坑总结

  1. lineinfile 注释 swap 三要素regexp 捕获组 + line: '#\1' + backrefs: yes,缺一不可;写坏就找带 ~ 的备份恢复。

  2. cgroup 驱动要一致 :Docker 的 exec-opts 和 kubelet 都用 systemd,否则节点起不来。

  3. 内网拉镜像两条路 :要么 --image-repository 指定国内源,要么像本项目一样提前 docker pull 到本地 / 走 Harbor。

  4. pause 镜像别忽略 :cri-dockerd 的 --pod-infra-container-image 也要指国内源,否则 Pod 起不来。

  5. task name 与命令保持一致:避免 etcd 版本号那种「名字 3.6.11、命令 3.6.8」的误导。

9. 总结

这套 Playbook 的价值在于:把 K8s 部署中「繁琐、重复、易错」的环境准备部分一次性脚本化,通过 Ansible 的幂等性,同一个 playbook 可以反复执行、批量套用到任意数量的节点。配合 inventory 分组 + 变量 + 模板,就是运维自动化「一次编写、处处复用」的完整实践。

相关推荐
云川之下2 小时前
【k8s】Docker inspect 命令
docker·容器·kubernetes
游戏开发爱好者82 小时前
App Store 上传 IPA 自动化,.p8 密钥认证与 CI/CD 接入实战
android·运维·ci/cd·小程序·uni-app·自动化·iphone
微硬创新2 小时前
大点数模拟量采集落地:耐达讯自动化32路0-20mA适配PROFINET总线实践解析
运维·人工智能·网络协议·自动化·信息与通信
猫吃了源码3 小时前
CentOS7使用Kubeadm安装部署K8s(Kubernetes)最新稳定版本1.26.x
云原生·容器·kubernetes
张忠琳4 小时前
【k3s】AutoK3s v0.9.3 —— Part 7 辅助模块超深度逐行分析
云原生·容器·kubernetes·k3s·autok3s
张忠琳13 小时前
【k3s】AutoK3s v0.9.3 Part 1 入口与 CLI 命令模块 — 超深度逐行分析之三
云原生·容器·kubernetes·k3s·autok3s
科技绘图14 小时前
快鲸GEO vs 传统AI搜索优化:全链路自动化与高效内容生产在转化闭环上的对比
数据库·人工智能·自动化
Elastic 中国社区官方博客17 小时前
Elasticsearch:使用 AI Agent 来创建 workflow
大数据·运维·人工智能·elasticsearch·搜索引擎·自动化·全文检索