Ansible 自动化 Nginx 集群部署实战:动态 Inventory、滚动发布与 Role 工程化
- Ansible 简介与核心原理
Ansible 是红帽推出的 IT 自动化运维引擎,能够完成批量服务器初始化、应用部署、配置管理、持续发布等工作。 核心原理:控制节点通过 SSH 协议连接远端主机,临时传输 Python 模块到被控主机本地执行,执行完毕后自动清理临时文件;整个过程不需要常驻后台进程,执行模式简单安全,天然适合云服务器批量运维。
- Ansible 核心组件
- Inventory(主机清单) 维护所有被控服务器列表、IP、分组、登录变量,用来区分负载均衡节点、Web 业务节点,支持多环境(测试 / 生产)隔离。
- Module(模块) Ansible 最小执行单元,系统内置大量原生模块,实现安装软件、创建目录、下发文件、启停服务等操作,无需手写 Shell 脚本。
- Playbook(剧本) 使用 YAML 语言编写的任务文件,把多条运维任务编排成一套完整流程,实现可重复执行、版本可控的自动化部署。
- Role(角色) 对 Playbook 进行封装复用的组件,把任务、模板、配置、触发器拆分管理,相同服务可以直接复用一套 Role,适合集群化部署。
- Handler(触发器) 只有配置文件发生变更时才会触发执行,例如修改 Nginx 配置后重载服务,避免无意义的重复重启。
- Template(Jinja2 模板) 支持动态渲染配置文件,可读取主机清单变量自动生成 Nginx upstream 后端列表,新增 Web 节点时无需手动修改负载均衡配置。
本文基于腾讯云 Ubuntu 24.04 CVM,搭建 Ansible 自动化 Nginx 集群,架构包含 1 台管控机、1 台负载均衡节点、3 台 Web 后端节点。
一、环境规划
| 主机名 | 角色 | 内网IP | 公网IP | 系统 | 配置 |
|---|---|---|---|---|---|
| ansible | Ansible跳板机 | 10.10.3.16 | 1.13.186.149 | Ubuntu24.04 | 2C2G |
| lb1 | Nginx负载均衡 | 10.10.3.12 | 175.27.143.43 | Ubuntu24.04 | 2C2G |
| web01 | Web后端 | 10.10.3.17 | 无 | Ubuntu24.04 | 2C2G |
| web02 | Web后端 | 10.10.3.2 | 无 | Ubuntu24.04 | 2C2G |
| web03 | Web后端 | 10.10.3.10 | 无 | Ubuntu24.04 | 2C2G |
环境说明:全部节点部署在腾讯云相同 VPC 与子网,内网互通
二、部署Ansible控制端
1、安装Ansible
登录ansible主机,修改主机名并安装 Ansible
bash
sudo hostnamectl set-hostname ansible
sudo apt update
sudo apt install -y ansible
验证:
bash
ansible --version
2、配置免密登录
在 ansible 服务器生成一套专门管理其他节点的 SSH 密钥:
ssh-keygen -t ed25519 -f ~/.ssh/ansible_ed25519 -N ""
查看公钥:
bash
cat ~/.ssh/ansible_ed25519.pub
把这段公钥 配置到 lb、web1、web2、web3 的 ubuntu 用户中,可以用腾讯云【云服务器】------【自动化助手】批量执行以下命令:
bash
PUBKEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpLiQbXlYlS9SY/edcN6iETPSocckghqX4aQ+X1IWdf ubuntu@ansible'
mkdir -p /home/ubuntu/.ssh
grep -qxF "$PUBKEY" /home/ubuntu/.ssh/authorized_keys 2>/dev/null ||
echo "$PUBKEY" >> /home/ubuntu/.ssh/authorized_keys
chown -R ubuntu:ubuntu /home/ubuntu/.ssh
chmod 700 /home/ubuntu/.ssh
chmod 600 /home/ubuntu/.ssh/authorized_keys

选中要执行的主机点击【执行命令】

3、测试免密登录
在 ansible 服务器批量测试:
bash
for ip in 10.10.3.12 10.10.3.17 10.10.3.2 10.10.3.10; do
ssh -o StrictHostKeyChecking=accept-new \
-i ~/.ssh/ansible_ed25519 ubuntu@$ip hostname
done
成功返回各台主机名,则说明 SSH 免密和安全组配置正确
4、创建项目目录
bash
sudo mkdir -p /data/ansible
sudo chown -R ubuntu:ubuntu /data/ansible
cd /data/ansible
mkdir -p inventories/{test,prod}/{group_vars,host_vars}
mkdir -p playbooks roles files templates vault
当前结构:

目录结构说明
ansible.cfg :项目独立配置文件
inventories :多环境服务器清单,维护主机与变量
playbooks :自动化任务执行剧本
roles: 可复用的部署角色组件
files :直接分发的静态资源文件
templates: jinja2动态配置模板
vault :ansible‑vault加密存放敏感凭证
5、创建生产环境主机清单
inventories/prod/hosts.ini
ini
[lb] #负载均衡服务器组
lb1 ansible_host=10.10.3.12
[web] #Web 服务器组
web1 ansible_host=10.10.3.17
web2 ansible_host=10.10.3.2
web3 ansible_host=10.10.3.10
[nginx:children] #包含 lb 和 web 的父组
lb
web
6、创建公共变量
inventories/prod/group_vars/all.yml
yml
ansible_user: ubuntu
ansible_ssh_private_key_file: /home/ubuntu/.ssh/ansible_ed25519
ansible_python_interpreter: /usr/bin/python3
这样就不需要在每台主机后面重复配置登录用户和私钥
7、创建Ansible配置
/data/ansible/ansible.cfg
bash
[defaults]
inventory = ./inventories/prod/hosts.ini
roles_path = ./roles
host_key_checking = True
retry_files_enabled = False
forks = 20
timeout = 10
[ssh_connection]
ssh_args = -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/home/ubuntu/.ssh/known_hosts
pipelining = True
检查配置是否生效
bash
ansible --version
确认输出中包含: /data/ansible/ansible.cfg

查看主机分组
bash
ansible-inventory --graph
预期结果类似:

8、测试连接
bash
#测试所有服务器:返回ping:pong
ansible all -m ping
#测试不同分组:返回ping:pong
ansible lb -m ping
ansible web -m ping
#测试命令执行:成功返回主机名
ansible all -a "hostname"
三、编写服务器初始化 Playbook
编写第一个 Playbook,统一初始化 4 台业务服务器,包括主机名、时区和基础工具
1、验证sudo权限
在 /data/ansible 下执行:
bash
ansible all -b -a "whoami"
全部返回 root,说明 Ansible 可以使用 sudo。
2、创建初始化 Playbook
playbooks/init.yml
yml
cat > playbooks/init.yml <<'EOF'
---
- name: 初始化服务器
hosts: all
become: true
serial: 2
tasks:
- name: 设置主机名
ansible.builtin.hostname:
name: "{{ inventory_hostname }}"
- name: 设置时区
community.general.timezone:
name: Asia/Shanghai
- name: 更新软件索引
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: 安装基础运维工具
ansible.builtin.apt:
name:
- curl
- wget
- vim
- git
- unzip
- jq
- lsof
- net-tools
state: present
EOF
其中:
become: true:通过 sudo 获取 root 权限serial: 2:每批处理两台服务器inventory_hostname:使用 Inventory 中的lb1、web1~web3设置主机名
3、执行Playbook
(1)检查语法
bash
ansible-playbook playbooks/init.yml --syntax-check
(2)预演变更
bash
ansible-playbook playbooks/init.yml --check --diff
预演不会真正修改服务器

预演结果正常,且没有失败,说明 Playbook 可以正常执行
(3)正式执行
bash
ansible-playbook playbooks/init.yml
执行完成后验证:
bash
ansible all -a "hostname"
ansible all -a "timedatectl show -p Timezone --value"

四、使用 Role 批量部署 Web 节点 Nginx
这一步我们创建 nginx_web Role,在 web1~web3 批量安装 Nginx 并发布带有节点标识的页面。
1、创建Role 目录结构
bash
cd /data/ansible
ansible-galaxy role init roles/nginx_web
2、 编写 Nginx 安装与配置任务
roles/nginx_web/tasks/main.yml
yml
---
- name: 安装 Nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: 创建网站根目录
ansible.builtin.file:
path: /data/www
state: directory
owner: www-data
group: www-data
mode: "0755"
- name: 下发首页
ansible.builtin.template:
src: index.html.j2
dest: /data/www/index.html
owner: www-data
group: www-data
mode: "0644"
- name: 下发 Nginx 配置
ansible.builtin.template:
src: default.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
notify: 重载 Nginx
- name: 启动并设置 Nginx 开机自启
ansible.builtin.service:
name: nginx
state: started
enabled: true
3、创建首页模板
每台 Web 节点显示自己的主机名和 IP,方便后续验证负载均衡效果:
roles/nginx_web/templates/index.html.j2
html
<!DOCTYPE html>
<html>
<head><title>{{ inventory_hostname }}</title></head>
<body>
<h1>Served by {{ inventory_hostname }}</h1>
<p>IP: {{ ansible_host }}</p>
<p>Time: {{ ansible_date_time.iso8601 }}</p>
</body>
</html>
4、创建NGINX配置模版
roles/nginx_web/templates/default.conf.j2
nginx
server {
listen 80;
listen [::]:80;
server_name _;
root /data/www;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
5、创建Handler
roles/nginx_web/handlers/main.yml
yaml
---
- name: 检查 Nginx 配置
ansible.builtin.command: nginx -t
changed_when: false
listen: 重载 Nginx
- name: 重载 Nginx
ansible.builtin.service:
name: nginx
state: reloaded
listen: 重载 Nginx
6、编写 Playbook 入口
playbooks/deploy_web.yml
yml
---
- name: 部署 Web 节点 Nginx
hosts: web
become: true
roles:
- nginx_web
7、执行部署
bash
ansible-playbook playbooks/deploy_web.yml --syntax-check
ansible-playbook playbooks/deploy_web.yml --check --diff
注意:执行
--check预演模式时,Ansible 判断 Nginx"将被安装",但实际上没有安装,所以执行服务管理时找不到nginx而报错,这不是 Playbook 配置错误
直接正式执行即可:
bash
ansible-playbook playbooks/deploy_web.yml
8、验证NGINX状态
bash
ansible web -a "systemctl is-active nginx" #应全部返回:active
ansible web -a "curl -s http://localhost" #三台服务器应分别显示 web1、web2、web3
五、部署 Nginx 负载均衡节点
在 lb1 安装 Nginx,将请求转发到 web1~web3。
1、检查后端连通性
先确认 lb1 能访问三台 Web 节点:
bash
ansible lb -a "curl -s --connect-timeout 3 http://10.10.3.17"
ansible lb -a "curl -s --connect-timeout 3 http://10.10.3.2"
ansible lb -a "curl -s --connect-timeout 3 http://10.10.3.10"
如果访问失败,需要检查 Web 服务器安全组:TCP 80 应允许来自 lb1 的内网 IP 10.10.3.12
2、创建Role
bash
cd /data/ansible
ansible-galaxy role init roles/nginx_lb
3、编写部署任务
roles/nginx_lb/tasks/main.yml
yml
---
- name: 安装 Nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: 下发负载均衡配置
ansible.builtin.template:
src: default.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
notify: 重载 Nginx
- name: 启动并设置 Nginx 开机自启
ansible.builtin.service:
name: nginx
state: started
enabled: true
4、创建负载均衡配置模板
roles/nginx_lb/templates/default.conf.j2
nginx
upstream web_backend {
{% for host in groups['web'] %}
server {{ hostvars[host].ansible_host }}:80;
{% endfor %}
}
server {
listen 80;
listen [::]:80;
server_name _;
location / {
proxy_pass http://web_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 3s;
proxy_read_timeout 30s;
}
}
模板会根据 Inventory 中的 web 分组自动生成:
nginx
upstream web_backend {
server 10.10.3.17:80;
server 10.10.3.2:80;
server 10.10.3.10:80;
}
以后增加 Web 节点时,只需要更新 Inventory,无需手动修改 Nginx 配置。
5、创建 Handler
roles/nginx_lb/handlers/main.yml
yml
---
- name: 检查 Nginx 配置
ansible.builtin.command: nginx -t
changed_when: false
listen: 重载 Nginx
- name: 重载 Nginx
ansible.builtin.service:
name: nginx
state: reloaded
listen: 重载 Nginx
6、创建 Playbook
playbooks/deploy_lb.yml
yml
---
- name: 部署 Nginx 负载均衡
hosts: lb
become: true
roles:
- nginx_lb
7、检查并执行
bash
ansible-playbook playbooks/deploy_lb.yml --syntax-check
ansible-playbook playbooks/deploy_web.yml --check --diff
ansible-playbook playbooks/deploy_lb.yml
成功结果应满足:unreachable=0、failed=0
8、验证负载均衡
连续访问 lb1:可以轮流返回三台web页面
bash
for i in {1..6}; do
curl -s http://10.10.3.12 | grep "Served by"
done

也可以通过 lb1 的公网 IP 访问:http://175.27.143.43(需要确保 lb1 安全组已开放 TCP 80)
通过以上测试,说明负载均衡部署成功。请求按照 Nginx 默认的轮询策略 依次分发到:web1 → web2 → web3
六、实现 Web 集群滚动发布与健康检查
本次按 web1 → web2 → web3 逐台发布。每台发布成功并通过健康检查后,才继续下一台;失败则回滚并停止发布。
1、创建Web组变量
inventories/prod/group_vars/web.yml
---
release_version: "v2.0.0"
release_message: "Ansible rolling deployment"
2、修改首页模板
修改roles/nginx_web/templates/index.html.j2为以下内容
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{{ inventory_hostname }}</title>
</head>
<body>
<h1>Served by {{ inventory_hostname }}</h1>
<p>IP: {{ ansible_host }}</p>
<p>Version: {{ release_version }}</p>
<p>Message: {{ release_message }}</p>
</body>
</html>
3、创建滚动发布 Playbook
playbooks/rolling_deploy_web.yml
yml
---
- name: 滚动发布 Web 集群
hosts: web
become: true # 在远程 Web 节点上使用特权提升(通常为 root)
serial: 1 # 每次只发布一台节点,实现逐台滚动发布
any_errors_fatal: true # 任意节点发布失败时,停止整个 Play 后续节点的执行
tasks:
- name: 检查当前首页是否存在
ansible.builtin.stat:
path: /data/www/index.html
register: current_index
- name: 发布并执行健康检查
block:
- name: 备份当前首页
ansible.builtin.copy:
src: /data/www/index.html
dest: /data/www/index.html.rollback
remote_src: true
owner: www-data
group: www-data
mode: "0644"
when: current_index.stat.exists
- name: 发布新首页
ansible.builtin.template:
src: ../roles/nginx_web/templates/index.html.j2
dest: /data/www/index.html
owner: www-data
group: www-data
mode: "0644"
- name: 检查本机 Nginx 服务
ansible.builtin.uri:
url: http://127.0.0.1/
return_content: true
status_code: 200
register: local_health
retries: 3 # 健康检查失败时最多重试 3 次
delay: 2 # 每次重试间隔 2 秒
until:
- local_health.status == 200
- release_version in local_health.content
- name: 从负载均衡节点检查当前 Web 节点
ansible.builtin.uri:
url: "http://{{ hostvars[inventory_hostname].ansible_host }}/"
return_content: true
status_code: 200
delegate_to: lb1
become: false
register: lb_health
retries: 3
delay: 2
until:
- lb_health.status == 200
- release_version in lb_health.content
- name: 显示当前节点发布结果
ansible.builtin.debug:
msg: "{{ inventory_hostname }} 发布 {{ release_version }} 成功"
rescue:
- name: 回滚旧首页
ansible.builtin.copy:
src: /data/www/index.html.rollback
dest: /data/www/index.html
remote_src: true
owner: www-data
group: www-data
mode: "0644"
when: current_index.stat.exists
- name: 发布失败并停止后续节点
ansible.builtin.fail:
msg: "{{ inventory_hostname }} 发布失败,已执行回滚"
4、执行滚动发布
bash
ansible-playbook playbooks/rolling_deploy_web.yml --syntax-check
ansible-playbook playbooks/rolling_deploy_web.yml
执行顺序会分为三个批次:web1 发布并检查成功→ web2 发布并检查成功→ web3 发布并检查成功
5、验证
通过访问负载均衡节点,看三台节点是否升级到了v2.0.0
bash
for i in {1..3}; do
curl -s http://10.10.3.12 |
grep -E "Served by|Version"
done

从上面结果可以验证滚动发布和健康检查部署完成!
6、模拟发布失败并验证自动回滚
模拟发布失败的核心思路是:正常发布新版本,但故意设置一个永远无法满足的健康检查条件,让 Ansible 认为发布失败,从而触发回滚。
(1) 将版本升级到 v2.0.1
bash
sed -i 's/v2.0.0/v2.0.1/' inventories/prod/group_vars/web.yml
(2)故意修改健康检查条件
修改Playbook中健康检查部分代码为以下:临时改成检查一个不存在的版本v9.9.9
yml
until:
- lb_health.status == 200
- "'v9.9.9' in lb_health.content"
(3)执行滚动发布
bash
ansible-playbook playbooks/rolling_deploy_web.yml

从日志可以看到web1 发布失败后成功回滚;web2、web3 没有继续发布;滚动发布和失败停止机制正常。
七、滚动发布时自动摘除和恢复节点
前面的滚动发布虽然逐台执行,但节点发布期间仍在负载均衡池中。
生产环境通常采用:从负载均衡摘除节点→ 发布并检查→ 恢复节点→ 继续下一台
1、修改负载均衡模版
编辑roles/nginx_lb/templates/default.conf.j2,修改为:
nginx
upstream web_backend {
{% for host in groups['web'] %}
{% if host != (maintenance_host | default('')) %} #maintenance_host 表示当前正在维护、需要临时摘除的节点。
server {{ hostvars[host].ansible_host }}:80;
{% endif %}
{% endfor %}
}
server {
listen 80;
listen [::]:80;
server_name _;
location / {
proxy_pass http://web_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
2、修改Web组变量
修改inventories/prod/group_vars/web.yml中版本为v3.0.0
yml
---
release_version: "v3.0.0"
release_message: "Ansible rolling deployment"
3、创建新的发布Playbook
playbooks/rolling_delop_lb_web.yml
yml
---
- name: 带负载均衡摘除的滚动发布
hosts: web
become: true
serial: 1
any_errors_fatal: true
tasks:
- name: 检查当前首页
ansible.builtin.stat:
path: /data/www/index.html
register: current_index
- name: 执行滚动发布
block:
- name: 从负载均衡摘除当前节点
ansible.builtin.template:
src: ../roles/nginx_lb/templates/default.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
vars:
maintenance_host: "{{ inventory_hostname }}"
delegate_to: lb1
- name: 检查负载均衡配置
ansible.builtin.command: nginx -t
changed_when: false
delegate_to: lb1
- name: 重载负载均衡
ansible.builtin.service:
name: nginx
state: reloaded
delegate_to: lb1
- name: 备份当前首页
ansible.builtin.copy:
src: /data/www/index.html
dest: /data/www/index.html.rollback
remote_src: true
owner: www-data
group: www-data
mode: "0644"
when: current_index.stat.exists
- name: 发布新首页
ansible.builtin.template:
src: ../roles/nginx_web/templates/index.html.j2
dest: /data/www/index.html
owner: www-data
group: www-data
mode: "0644"
- name: 检查新版本
ansible.builtin.uri:
url: http://127.0.0.1/
return_content: true
status_code: 200
register: health_result
retries: 3
delay: 2
until:
- health_result.status == 200
- release_version in health_result.content
- name: 将当前节点恢复到负载均衡
ansible.builtin.template:
src: ../roles/nginx_lb/templates/default.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
vars:
maintenance_host: ""
delegate_to: lb1
- name: 检查并重载负载均衡
ansible.builtin.shell: nginx -t && systemctl reload nginx
changed_when: true
delegate_to: lb1
- name: 显示发布结果
ansible.builtin.debug:
msg: "{{ inventory_hostname }} 发布成功并已恢复流量"
rescue:
- name: 回滚旧首页
ansible.builtin.copy:
src: /data/www/index.html.rollback
dest: /data/www/index.html
remote_src: true
owner: www-data
group: www-data
mode: "0644"
when: current_index.stat.exists
- name: 检查回滚后的服务
ansible.builtin.uri:
url: http://127.0.0.1/
status_code: 200
- name: 将回滚后的节点恢复到负载均衡
ansible.builtin.template:
src: ../roles/nginx_lb/templates/default.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
vars:
maintenance_host: ""
delegate_to: lb1
- name: 检查并重载负载均衡
ansible.builtin.shell: nginx -t && systemctl reload nginx
changed_when: true
delegate_to: lb1
- name: 停止后续发布
ansible.builtin.fail:
msg: "{{ inventory_hostname }} 发布失败,已回滚并恢复流量"
4、执行发布
bash
ansible-playbook playbooks/rolling_delop_lb_web.yml --syntax-check
ansible-playbook playbooks/rolling_delop_lb_web.yml

可以看到执行逻辑如下:
摘除 web1 → 发布 web1 → 健康检查 → 恢复 web1
摘除 web2 → 发布 web2 → 健康检查 → 恢复 web2
摘除 web3 → 发布 web3 → 健康检查 → 恢复 web3
发布中的节点不会继续接收用户请求,而其他节点可以持续提供服务。
八、腾讯云动态 Inventory 管理主机
目前服务器 IP 手动写在 hosts.ini 中,服务器数量较多或频繁扩缩容时难以维护。动态 Inventory 将通过标签识别环境和服务器角色,避免手动维护 IP。
1、给腾讯云CVM添加标签
在腾讯云CVM控制台,给 5 台 CVM 添加以下标签:
| CVM | Environment | Role | ManagedBy |
|---|---|---|---|
| ansible | prod | control | ansible |
| lb1 | prod | lb | ansible |
| web1 | prod | web | ansible |
| web2 | prod | web | ansible |
| web3 | prod | web | ansible |
2、创建只读CAM子用户
该账号仅用于让 Ansible 查询腾讯云 CVM 和标签信息,不允许创建、修改或删除云资源。
(1)创建子用户
进入腾讯云控制台:【访问管理 CAM】------【用户】------【用户列表】------【新建用户】------【自定义创建】
创建以下用户以及权限:
| 用户名 | 访问方式 | 授予的只读权限 |
|---|---|---|
ansible-inventory |
编程访问 | QcloudCVMReadOnlyAccess 、QcloudTagReadOnlyAccess |
(2)保存访问密钥
创建完成后会生成SecretId和SecretKey,请立即妥善保存,SecretKey 通常只完整显示一次。
(3)记录地域信息
我的 CVM 位于南京三区,对应地域通常是:ap-nanjing ,后面查询 CVM 时将使用这个地域。
3、安装腾讯云 SDK 并验证 CAM 权限
(1)创建Python虚拟环境
在 Ansible 控制服务器执行:
bash
sudo apt install -y python3-venv
cd /data/ansible
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install tencentcloud-sdk-python
验证 SDK:
bash
.venv/bin/python -c "import tencentcloud; print('腾讯云 SDK 安装成功')"
(2)配置腾讯云 CAM 凭据
创建凭据文件
bash
mkdir -p ~/.config/tencentcloud
chmod 700 ~/.config/tencentcloud
umask 077
安全输入凭据:
bash
read -rsp "SecretId: " SID
echo
read -rsp "SecretKey: " SKEY
echo
输入时终端不会显示内容,输入完成后按回车即可。
将凭据写入配置文件:
bash
printf 'export TENCENTCLOUD_SECRET_ID=%q\n' "$SID" \
> ~/.config/tencentcloud/ansible.env
printf 'export TENCENTCLOUD_SECRET_KEY=%q\n' "$SKEY" \
>> ~/.config/tencentcloud/ansible.env
设置文件权限并清理临时变量:
bash
chmod 600 ~/.config/tencentcloud/ansible.env
unset SID SKEY
配置自动加载,将加载逻辑写入 .bashrc:
bash
grep -q 'tencentcloud/ansible.env' ~/.bashrc || cat >> ~/.bashrc <<'EOF'
# Tencent Cloud credentials for Ansible
if [ -f "$HOME/.config/tencentcloud/ansible.env" ]; then
source "$HOME/.config/tencentcloud/ansible.env"
fi
EOF
立即加载
bash
source ~/.bashrc
(3)验证配置
检查环境变量是否已加载,但不要直接输出密钥内容:
bash
test -n "$TENCENTCLOUD_SECRET_ID" \
&& echo "SecretId 已加载" || echo "SecretId 未加载"
test -n "$TENCENTCLOUD_SECRET_KEY" \
&& echo "SecretKey 已加载" || echo "SecretKey 未加载"
检查文件权限:
bash
ls -l ~/.config/tencentcloud/ansible.env #预期权限为:-rw------- ubuntu ubuntu
(4)验证腾讯云 API
bash
cd /data/ansible
.venv/bin/python scripts/test_tencentcloud.py
如果能够列出 CVM 实例,说明凭据已正确加载。以后重新登录服务器时,.bashrc 会自动加载凭据,无需再次手动输入。

4、创建腾讯云动态 Inventory
自动读取腾讯云 CVM,并根据 Role 标签生成 control、lb、web 分组
(1)创建动态 Inventory 脚本
bash
cd /data/ansible
mkdir -p inventories/dynamic
创建脚本:inventories/dynamic/tencent_cvm.py
python
#!/data/ansible/.venv/bin/python
import json
import os
import sys
from tencentcloud.common import credential
from tencentcloud.cvm.v20170312 import cvm_client, models
# 腾讯云地域及实例标签筛选条件
REGION = "ap-nanjing"
ENVIRONMENT = "prod"
MANAGED_BY = "ansible"
def get_instances():
"""分页获取指定地域下的全部 CVM 实例。"""
# 从环境变量读取 CAM API 凭据
cred = credential.Credential(
os.environ["TENCENTCLOUD_SECRET_ID"],
os.environ["TENCENTCLOUD_SECRET_KEY"]
)
# 创建 CVM API 客户端
client = cvm_client.CvmClient(cred, REGION)
instances = []
offset = 0
limit = 100
# 腾讯云 API 分页查询,每次最多获取 100 台
while True:
request = models.DescribeInstancesRequest()
request.Offset = offset
request.Limit = limit
response = client.DescribeInstances(request)
instances.extend(response.InstanceSet)
# 已获取全部实例时退出循环
if len(instances) >= response.TotalCount:
break
offset += limit
return instances
def build_inventory():
"""根据腾讯云 CVM 标签生成 Ansible 动态 Inventory。"""
# 保存每台主机的连接变量及自定义变量
inventory = {
"_meta": {
"hostvars": {}
}
}
for instance in get_instances():
# 将标签列表转换为字典
# 示例:{"env": "prod", "ManagedBy": "ansible", "Role": "web"}
tags = {
tag.Key: tag.Value
for tag in (instance.Tags or [])
}
# 只纳管 env=prod 的实例
if tags.get("env") != ENVIRONMENT:
continue
# 只纳管 ManagedBy=ansible 的实例
if tags.get("ManagedBy") != MANAGED_BY:
continue
role = tags.get("Role")
private_ips = instance.PrivateIpAddresses or []
# 没有角色标签或内网 IP 时跳过
if not role or not private_ips:
continue
# 使用腾讯云实例名称作为 Ansible 主机名
hostname = instance.InstanceName
# 根据 Role 标签生成主机组,如 web、lb、control
group_name = role.lower()
# 创建主机组并添加当前主机
inventory.setdefault(group_name, {"hosts": []})
inventory[group_name]["hosts"].append(hostname)
# 设置主机连接参数和腾讯云信息
inventory["_meta"]["hostvars"][hostname] = {
"ansible_host": private_ips[0],
"ansible_user": "ubuntu",
"ansible_ssh_private_key_file":
"/home/ubuntu/.ssh/ansible_ed25519",
"ansible_python_interpreter": "/usr/bin/python3",
# 自定义变量,可在 Playbook 中引用
"tencent_instance_id": instance.InstanceId,
"tencent_region": REGION,
"tencent_role": role,
"tencent_environment": tags.get("env")
}
# 对各主机组中的主机名排序,保证执行顺序稳定
for group_data in inventory.values():
if isinstance(group_data, dict) and "hosts" in group_data:
group_data["hosts"].sort()
return inventory
def main():
"""处理 Ansible 动态 Inventory 参数并输出 JSON。"""
# 主机变量已放入 _meta.hostvars,因此 --host 返回空字典
if len(sys.argv) == 2 and sys.argv[1] == "--host":
print(json.dumps({}))
return
# 默认及 --list 模式返回完整 Inventory
print(
json.dumps(
build_inventory(),
ensure_ascii=False,
indent=2
)
)
if __name__ == "__main__":
main()
保存后验证:
bash
chmod 750 inventories/dynamic/tencent_cvm.py
inventories/dynamic/tencent_cvm.py --list
ansible-inventory \
-i inventories/dynamic/tencent_cvm.py \
--graph

从上面的执行效果可以看到,动态 Inventory 已生成成功
(2)验证动态Inventory
测试业务节点连接
bash
ansible 'lb:web' -i inventories/dynamic/tencent_cvm.py -m ping

(3)将原有静态 Inventory 切换为动态 Inventory
现在我们将动态 Inventory 脚本直接放到环境目录,统一管理生产主机清单、组变量和主机变量。
最终目录结构:
bash
inventories/prod/
├── tencent_cvm.py #从腾讯云动态获取生产服务器
├── group_vars/ #保存生产环境的组变量
├── host_vars/ #保存单台主机的专属变量
└── hosts.ini.bak #原静态主机清单备份
- 移动动态脚本
bash
cd /data/ansible
mv inventories/dynamic/tencent_cvm.py inventories/prod/
chmod 750 inventories/prod/tencent_cvm.py
- 备份静态 Inventory
bash
mv inventories/prod/hosts.ini inventories/prod/hosts.ini.bak
- 修改
ansible.cfg
将默认 Inventory 改为:
ini
[defaults]
inventory = ./inventories/prod/tencent_cvm.py
roles_path = ./roles
host_key_checking = True
retry_files_enabled = False
forks = 20
timeout = 10
[ssh_connection]
ssh_args = -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/home/ubuntu/.ssh/known_hosts
pipelining = True
- 验证配置
bash
ansible-inventory --graph
ansible 'lb:web' -m ping
ansible-inventory --host web1

可以验证现有 Playbook 仍能识别目标主机:
bash
ansible-playbook playbooks/rolling_deploy_lb_web.yml --list-hosts

动态 Inventory 切换完成,后续新增服务器,只需配置对应腾讯云标签,无需修改 hosts.ini
九、使用 Tags、Limit 和变量实现灰度发布
本章通过修改首页样式和发布版本,演示如何使用 Ansible 实现灰度发布。
发布目标:将首页升级为新的页面样式,并将版本从 v3.0.0 发布到 v4.0.0,先发布 web1 进行验证,验证成功后,再发布 web2 和 web3。
1、修改首页模版
将首页修改为新的页面样式:roles/nginx_web/templates/index.html.j2
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ inventory_hostname }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
background: linear-gradient(135deg, #1f2937, #111827);
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
color: #e5e7eb;
padding: 20px;
}
.card {
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 40px 48px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
text-align: center;
min-width: 360px;
}
.host-title {
font-size: 28px;
font-weight: 600;
color: #38bdf8;
margin-bottom: 24px;
}
.item {
font-size: 16px;
margin: 12px 0;
}
.label {
color: #9ca3af;
}
.value {
color: #f9fafb;
font-weight: 500;
}
.version-tag {
display: inline-block;
margin-top: 16px;
padding: 6px 16px;
background-color: #0ea5e9;
border-radius: 999px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="card">
<div class="host-title">{{ inventory_hostname }}</div>
<div class="item">
<span class="label">节点 IP:</span>
<span class="value">{{ ansible_host }}</span>
</div>
<div class="item">
<span class="label">发布版本:</span>
<span class="version-tag">{{ release_version }}</span>
</div>
<div class="item">
<span class="label">备注信息:</span>
<span class="value">{{ release_message }}</span>
</div>
</div>
</body>
</html>
模板中的变量会在发布时由 Ansible 自动替换:
bash
{{ inventory_hostname }} → 当前主机名
{{ ansible_host }} → 当前主机 IP
{{ release_version }} → 发布版本
{{ release_message }} → 发布说明
2、修改发布版本
修改版本变量文件inventories/prod/group_vars/web.yml,内容修改为:
ini
---
release_version: "v4.0.0"
release_message: "Ansible rolling deployment"
3、使用 Tags 区分任务
在 Playbook 中为任务添加标签:
| Tags | 作用 |
|---|---|
config |
检查 Nginx 配置 |
deploy |
执行完整发布流程 |
verify |
执行健康检查和结果验证 |
修改playbooks/rolling_deploy_lb_web.yml代码:
yml
---
- name: 带负载均衡摘除的滚动发布
hosts: web
order: sorted
serial: 1
become: true
any_errors_fatal: true
tasks:
- name: 检查当前首页
ansible.builtin.stat:
path: /data/www/index.html
register: current_index
tags:
- deploy
- name: 执行滚动发布
block:
- name: 从负载均衡摘除当前节点
ansible.builtin.template:
src: ../roles/nginx_lb/templates/default.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
vars:
maintenance_host: "{{ inventory_hostname }}"
delegate_to: lb1
tags:
- deploy
- name: 检查摘除后的 Nginx 配置
ansible.builtin.command:
cmd: nginx -t
changed_when: false
delegate_to: lb1
become: true
tags:
- config
- deploy
- name: 重载摘除节点后的负载均衡配置
ansible.builtin.service:
name: nginx
state: reloaded
delegate_to: lb1
become: true
tags:
- deploy
- name: 备份当前首页
ansible.builtin.copy:
src: /data/www/index.html
dest: /data/www/index.html.rollback
remote_src: true
owner: www-data
group: www-data
mode: "0644"
when: current_index.stat.exists
tags:
- deploy
- name: 发布新首页
ansible.builtin.template:
src: ../roles/nginx_web/templates/index.html.j2
dest: /data/www/index.html
owner: www-data
group: www-data
mode: "0644"
tags:
- deploy
- name: 检查当前 Web 节点健康状态
ansible.builtin.uri:
url: http://127.0.0.1/
return_content: true
status_code: 200
register: local_health
retries: 3
delay: 2
until:
- local_health.status == 200
- release_version in local_health.content
tags:
- verify
- deploy
- name: 从负载均衡节点检查当前 Web 节点
ansible.builtin.uri:
url: "http://{{ hostvars[inventory_hostname].ansible_host }}/"
return_content: true
status_code: 200
register: lb_health
retries: 3
delay: 2
until:
- lb_health.status == 200
- release_version in lb_health.content
delegate_to: lb1
become: false
tags:
- verify
- deploy
- name: 将当前节点恢复到负载均衡
ansible.builtin.template:
src: ../roles/nginx_lb/templates/default.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
vars:
maintenance_host: ""
delegate_to: lb1
become: true
tags:
- deploy
- name: 检查恢复后的负载均衡配置
ansible.builtin.command:
cmd: nginx -t
changed_when: false
delegate_to: lb1
become: true
tags:
- config
- deploy
- name: 重载恢复后的负载均衡配置
ansible.builtin.service:
name: nginx
state: reloaded
delegate_to: lb1
become: true
tags:
- deploy
- name: 显示发布结果
ansible.builtin.debug:
msg: "{{ inventory_hostname }} 发布 {{ release_version }} 成功并已恢复流量"
tags:
- verify
- deploy
rescue:
- name: 回滚旧首页
ansible.builtin.copy:
src: /data/www/index.html.rollback
dest: /data/www/index.html
remote_src: true
owner: www-data
group: www-data
mode: "0644"
when: current_index.stat.exists
tags:
- deploy
- name: 检查回滚后的 Web 服务
ansible.builtin.uri:
url: http://127.0.0.1/
return_content: true
status_code: 200
register: rollback_health
retries: 3
delay: 2
until: rollback_health.status == 200
tags:
- deploy
- name: 将回滚后的节点恢复到负载均衡
ansible.builtin.template:
src: ../roles/nginx_lb/templates/default.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
vars:
maintenance_host: ""
delegate_to: lb1
become: true
tags:
- deploy
- name: 检查回滚后的负载均衡配置
ansible.builtin.command:
cmd: nginx -t
changed_when: false
delegate_to: lb1
become: true
tags:
- config
- deploy
- name: 重载回滚后的负载均衡配置
ansible.builtin.service:
name: nginx
state: reloaded
delegate_to: lb1
become: true
tags:
- deploy
- name: 停止后续发布
ansible.builtin.fail:
msg: "{{ inventory_hostname }} 发布失败,已回滚并恢复流量"
tags:
- deploy
修改好后查看标签状态
bash
#查看所有标签
ansible-playbook playbooks/rolling_deploy_lb_web.yml --list-tags
#查看指定标签对应的任务:config、deploy、verify
ansible-playbook playbooks/rolling_deploy_lb_web.yml --tags config --list-tasks
ansible-playbook playbooks/rolling_deploy_lb_web.yml --tags deploy --list-tasks
ansible-playbook playbooks/rolling_deploy_lb_web.yml --tags verify --list-tasks
4、使用limit限制目标主机
--limit 用于限制本次 Playbook 操作的服务器范围。
只操作 web1:
bash
ansible-playbook playbooks/rolling_deploy_lb_web.yml --limit web1 --tags deploy
操作 web2 和 web3:
bash
ansible-playbook playbooks/rolling_deploy_lb_web.yml --limit 'web2:web3' --tags deploy
操作 web 组中除 web3 以外的主机:
bash
ansible-playbook playbooks/rolling_deploy_lb_web.yml --limit 'web:!web3' --tags deploy
查看本次命令会匹配哪些主机:
bash
ansible-playbook playbooks/rolling_deploy_lb_web.yml --limit web1 --list-hosts
5、灰度发布web1
先检查 Playbook 语法:
bash
ansible-playbook playbooks/rolling_deploy_lb_web.yml --syntax-check
确认目标主机:
bash
ansible-playbook playbooks/rolling_deploy_lb_web.yml --limit web1 --list-hosts
执行灰度发布:
bash
ansible-playbook playbooks/rolling_deploy_lb_web.yml --limit web1 --tags deploy
6、验证web1
直接访问web1:
bash
curl http://10.10.3.17
可以看到版本更新为了v4.0.0
7、发布剩余节点
确认 web1 正常后,发布 web2 和 web3:
bash
ansible-playbook playbooks/rolling_deploy_lb_web.yml --limit 'web2:web3' --tags deploy
由于playbook中配置了:order: sorted和serial: 1,发布执行顺序为 web2 → web3,如果某个节点发布失败,Playbook 会执行回滚,并停止后续发布。
8、验证全部节点
可以执行健康检查Tags:
bash
ansible-playbook playbooks/rolling_deploy_lb_web.yml --tags verify

通过负载均衡节点访问:三个节点都返回最新版页面和版本号

十、Role 工程化变量管理
为什么要提取变量?
在最初的 Playbook 中,网站目录、Nginx 服务名、文件权限等配置可能直接写在任务和模板里。
ini
path: /data/www
owner: www-data
group: www-data
mode: "0755"
这种写法存在以下问题:
- 配置分散在多个文件中;
- 修改目录或端口时需要查找多处;
- 不同环境难以复用同一套 Role;
- 容易出现修改遗漏或配置不一致。
因此,需要将固定配置提取为变量,实现:任务逻辑与配置分离,整套执行代码无需重复改动,仅通过变量即可适配开发、测试、生产等不同环境,既避免了漏改、错改的问题,也让配置集中清晰、维护更简单,大幅提升 Role 的通用性、规范性和可维护性,是 Ansible 工程化标准化的核心基础。
1、Role目录结构

其中:
defaults/main.yml:Role 默认变量;tasks/main.yml:Role 执行任务;handlers/main.yml:配置变化后的处理任务;templates/:带变量的Jinja2 模板文件,运行时自动填充内容;vars/main.yml:Role 变量,优先级较高,一般不放环境配置。
2、定义Role默认变量
编辑roles/nginx_web/defaults/main.yml,内容如下:
nginx
---
# 网站根目录
web_root: /data/www
# Nginx 服务名称
nginx_service_name: nginx
# Nginx 网站配置文件
nginx_web_config: /etc/nginx/sites-available/default
# 网站文件所属用户和用户组
web_owner: www-data
web_group: www-data
# 网站目录和文件权限
web_dir_mode: "0755"
web_file_mode: "0644"
# Nginx 监听端口和域名
nginx_listen_port: 80
nginx_server_name: "_"
这些变量表示 Role 的默认配置,如果外部没有提供同名变量,Role 就使用这里的默认值。
3、修改tasks任务文件,引用变量
编辑roles/nginx_web/tasks/main.yml,将原本的内容:

改成如下:
yml
---
- name: 安装 Nginx
ansible.builtin.apt:
name: "{{ nginx_service_name }}"
state: present
update_cache: true
- name: 创建网站根目录
ansible.builtin.file:
path: "{{ web_root }}"
state: directory
owner: "{{ web_owner }}"
group: "{{ web_group }}"
mode: "{{ web_dir_mode }}"
- name: 下发首页
ansible.builtin.template:
src: index.html.j2
dest: "{{ web_root }}/index.html"
owner: "{{ web_owner }}"
group: "{{ web_group }}"
mode: "{{ web_file_mode }}"
- name: 下发 Nginx 配置
ansible.builtin.template:
src: default.conf.j2
dest: "{{ nginx_web_config }}"
owner: root
group: root
mode: "{{ web_file_mode }}"
notify: 重载 Nginx
- name: 启动并设置 Nginx 开机自启
ansible.builtin.service:
name: "{{ nginx_service_name }}"
state: started
enabled: true
任务文件只负责描述执行逻辑,不再直接写死具体配置。
4、修改 Nginx 配置模板
同样,修改roles/nginx_web/templates/default.conf.j2中参数为变量:
jinja2
server {
listen {{ nginx_listen_port }};
listen [::]:{{ nginx_listen_port }};
server_name {{ nginx_server_name }};
root {{ web_root }};
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
模板变量会在执行时根据Role默认变量自动替换:
jinja2
{{ nginx_listen_port }} → 80
{{ nginx_server_name }} → _
{{ web_root }} → /data/www
5、配置环境变量
inventories/prod/group_vars/web.yml
当前可以只保存发布相关变量:
yml
---
release_version: "v4.0.0"
release_message: "Ansible rolling deployment"
Role 会自动从 defaults/main.yml 获取其他默认变量。
如果需要针对当前环境修改配置,也可以在这里定义同名变量,例如修改网站目录:
yml
---
release_version: "v4.0.0"
release_message: "Ansible rolling deployment"
web_root: /data/www/html
nginx_listen_port: 80
nginx_server_name: "_"
则当前 prod 环境中的 Web 节点会使用:
bash
/data/www/html
而其他没有覆盖该变量的环境,仍然使用 Role 默认值。
变量优先级说明
Ansible 会根据约定自动加载变量:
【Role defaults】 < 【group_vars / host_vars】 < 【Playbook vars】 < 【命令行 -e】
6、检查并执行Role
检查 Playbook 语法:
bash
ansible-playbook playbooks/deploy_web.yml \
--syntax-check
预演变更:
bash
ansible-playbook playbooks/deploy_web.yml \
--check --diff
正式执行:
bash
ansible-playbook playbooks/deploy_web.yml
7、验证部署结果
bash
ansible web -a "ls -ld /data/www"
ansible web -a "curl -s http://localhost"
ansible web -b -a "nginx -t"
通过本次实践,我们从零搭建了 Ansible 控制端,并完成了腾讯云 CVM 服务器的自动化管理。整个过程涵盖了 SSH 免密登录、Inventory 主机清单、服务器初始化、Role 模块化、Nginx Web 集群部署、负载均衡、滚动发布、健康检查、失败回滚以及动态 Inventory 等内容。
在此基础上,我们还使用 Tags、Limit 和变量覆盖实现了可控的灰度发布,并通过提取 Role 变量,将任务逻辑与环境配置分离,使同一套 Role 能够复用于不同环境。
