【Ansible自动化运维实战:从Playbook到负载均衡指南】
本文是「Vagrant+VirtualBox虚拟化环境搭建」的续篇,深入探索Ansible在自动化运维中的核心应用:
✅ Ansible核心技能:Playbook编写、角色(Roles)模块化、标签(Tags)精准控制
✅ 实战场景覆盖:Apache服务部署、HAProxy负载均衡配置、服务器联动管理
✅ 无缝衔接Vagrant:在虚拟化环境中模拟真实运维场景,提供完整可复现的代码示例
无论是想快速入门Ansible的新手,还是需要优化现有流水线的工程师,都能从本文获得从基础的自动化运维解决方案。文末附常见问题排查与性能对比数据,助你避坑提速!
参考教程来自:leucos/ansible-tuto: Ansible 教程
码云:git clone https://gitee.com/xbd_zc/ansible-tuto-demosite.git
前期准备(虚拟机安装及配置)【Vagrant+VirtualBox创建自动化虚拟环境】Ansible-Playbook-CSDN博客
这里准备4台,host0-3,host0:安装Ansible管理host1-3,如通过vagrant创建,Vagrantfile配置如下:
# -*- mode: ruby -*-
# vi: set ft=ruby :hosts = {"host0" => "192.168.0.220","host1" => "192.168.0.221","host2" => "192.168.0.222","host3" => "192.168.0.223"
}Vagrant.configure("2") do |config|hosts.each do |name, ip|config.vm.define name do |machine|machine.vm.box = "bento/ubuntu-20.04"machine.vm.box_version = "202407.23.0"machine.vm.hostname = "%s" % namemachine.vm.network :public_network,bridge: "en1", ip: ipmachine.vm.provider "virtualbox" do |v|v.name = namev.customize ["modifyvm", :id, "--memory", 1024]endendend
end
1.Ansible-测试准备
准备四台服务器,并在本机安装Ansible,一个hosts文件如下所示:
环境:本机host0(192.168.0.220),系统版本Ubuntu 20.04.6 LTS,vagrant镜像:bento/ubuntu-20.04、版本:202407.23.0,私钥配置好
host1 ansible_host=192.168.0.221 ansible_user=root
host2 ansible_host=192.168.0.222 ansible_user=root
host3 ansible_host=192.168.0.223 ansible_user=root
ansible_host
是一个特殊变量,用于设置 ansible 将在 尝试连接到此主机。
ansible_user
是另一个特殊变量,它告诉 ansible 使用 SSH 时以此用户身份连接。默认情况下,ansible 将使用 当前用户名,或使用 ~/.ansible.cfg 中提供的其他默认值 ().`remote_user
检查主机是否正常工作
ansible -m ping all -i hosts
- -m MODULE_NAME,–module-name MODULE_NAME (模块名称)
- -i INVENTORY, --inventory INVENTORY, --inventory-file INVENTORY (文件清单)
ansible 执行ping
输出应如下所示:
host1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"
}
host3 | SUCCESS => { "ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"
}
host2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"
}
好!所有 3 个主机都处于活动状态,并且 ansible 可以与它们通信。
2.Ansible-节点通信
在主机上执行shell命令
ansible -i hosts -m shell -a 'uname -a' host1
- -a MODULE_ARGS, --args MODULE_ARGS 模块参数
输出应如下所示:
host1 | CHANGED | rc=0 >>
Linux host1 5.4.0-189-generic #209-Ubuntu SMP Fri Jun 7 14:05:13 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
复制文件
复制本机hosts文件到host1上。
ansible -i step-02/hosts -m copy -a 'src=/etc/hosts dest=/tmp/' host1
输出应类似于:
host1 | CHANGED => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": true,"checksum": "2e2bd6a4e52b3f3a709a96214cc308753be3a7d4","dest": "/tmp/hosts","gid": 0,"group": "root","md5sum": "d7c7e7ffc3d982c56de6801bb665ba9b","mode": "0644","owner": "root","size": 244,"src": "/root/.ansible/tmp/ansible-tmp-1745964825.1255076-56136-181611889373774/source","state": "file","uid": 0
}#host1主机上查看
vagrant@host1:~$ cat /tmp/hosts
127.0.0.1 localhost
127.0.1.1 vagrant# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
127.0.2.1 host0 host0
shell 模块 – 在目标主机上执行 shell 命令
获取在节点上部署了哪个 Ubuntu 版本
ansible -i hosts -m shell -a 'grep DISTRIB_RELEASE /etc/lsb-release' all
all
是一个快捷方式,表示“在清单文件中找到的所有主机”。它会 返回:
host2 | CHANGED | rc=0 >>
DISTRIB_RELEASE=20.04
host1 | CHANGED | rc=0 >>
DISTRIB_RELEASE=20.04
host3 | CHANGED | rc=0 >>
DISTRIB_RELEASE=20.04
setup module –收集有关远程主机的信息
如果我们 需要更多信息(IP 地址、RAM 大小等)可以使用此模块
ansible -i hosts -m setup host1
回复包含大量信息:
host1 | SUCCESS => {"ansible_facts": {"ansible_all_ipv4_addresses": ["10.0.2.15","192.168.0.221"], .......................................#还有很多"module_setup": true},"changed": false
}
可以筛选返回的信息,以防您正在寻找 一些特定的东西。
例如想知道您的所有节点的总内存
ansible -i hosts -m setup -a 'filter=ansible_memtotal_mb' all
输出如下:
host1 | SUCCESS => {"ansible_facts": {"ansible_memtotal_mb": 964,"discovered_interpreter_python": "/usr/bin/python3"},"changed": false
}
host2 | SUCCESS => {"ansible_facts": {"ansible_memtotal_mb": 964,"discovered_interpreter_python": "/usr/bin/python3"},"changed": false
}
host3 | SUCCESS => {"ansible_facts": {"ansible_memtotal_mb": 964,"discovered_interpreter_python": "/usr/bin/python3"},"changed": false
}
请注意,与上一个输出相比,主机的回复顺序不同。 这是因为 ansible 与主机并行通信!
顺便说一句,当使用 setup 模块时,你可以在表达式中使用。 它的作用就像一个 shell glob。*``filter=
选择主机
我们看到这表示 “所有主机”,但 ansible 提供了很多方式 — Ansible 社区文档:all
host0:host1
将在 host0 上运行,并且 主机 1host*
将在以 ‘host’ 开头和结尾的所有主机上运行 与 ‘’ (就像 shell glob 一样)
常见的事实变量示例
- 操作系统信息:
ansible_os_family
:操作系统的家族,如Debian、RedHat等。ansible_distribution
:操作系统的分发版名称。ansible_distribution_version
:操作系统的版本号。ansible_distribution_major_version
:操作系统的主版本号。
- 硬件信息:
ansible_processor_vcpus
:处理器的虚拟CPU数量。ansible_memory_mb
:总内存大小(以MB为单位)。ansible_disk_usage
:磁盘使用情况。
- 网络配置:
ansible_default_ipv4
:默认的IPv4地址。ansible_all_ipv4_addresses
:所有的IPv4地址。ansible_hostname
:主机名。
获取事实变量的方法
使用setup
模块:
-
在playbook中,可以通过setup模块获取所有事实变量。例如:
#显示所有主机的事实,并将其按“主机名”索引存储在“/tmp/facts”中。 # ansible all -m ansible.builtin.setup --tree /tmp/facts #仅显示有关ansible在所有主机上找到的内存的事实,并将其输出。 # ansible all -m ansible.builtin.setup -a 'filter=ansible_*_mb' #仅显示facter返回的事实。 #ansible all -m ansible.builtin.setup -a 'filter=facter_*' #只收集事实者返回的事实。 #ansible all -m ansible.builtin.setup -a 'gather_subset=!all,facter' - name: Collect only facts returned by facteransible.builtin.setup:gather_subset:- '!all'- '!<any valid subset>'- facter
-
使用filter参数来查看指定的信息,例如:
- name: Filter and return only selected factsansible.builtin.setup:filter:- 'ansible_distribution'- 'ansible_machine_id'- 'ansible_*_mb' # 仅显示有关某些接口的事实。 # ansible all -m ansible.builtin.setup -a 'filter=ansible_eth[0-2]' #将额外收集的事实限制为网络和虚拟(包括默认的最小事实) # ansible all -m ansible.builtin.setup -a 'gather_subset=network,virtual' #仅收集网络和虚拟(不包括默认的最小事实) # ansible all -m ansible.builtin.setup -a 'gather_subset=!all,network,virtual' #即使在场,也不要叫木偶师或欧海。 # ansible all -m ansible.builtin.setup -a 'gather_subset=!facter,!ohai' #仅收集默认的最低事实数量: # ansible all -m ansible.builtin.setup -a 'gather_subset=!all' #不收集任何事实,即使是默认的最小事实子集: # ansible all -m ansible.builtin.setup -a 'gather_subset=!all,!min' #使用存储在C:\custom_fact中的自定义事实显示来自Windows主机的事实。 # ansible windows -m ansible.builtin.setup -a "fact_path='c:\custom_facts'" #收集dbservers组中机器的事实(也称为委派事实) - hosts: app_serverstasks:- name: Gather facts from db serversansible.builtin.setup:delegate_to: "{{ item }}"delegate_facts: trueloop: "{{ groups['dbservers'] }}"
3.Ansible主机分组与变量管理
为什么需要主机分组与变量管理?
在 Ansible 自动化运维中,主机分组和变量管理是提升效率的核心手段:
- 批量操作:通过逻辑分组快速执行命令(如更新所有 Web 服务器)。
- 环境隔离:为开发、测试、生产环境分配独立配置。
- 配置复用:通过变量动态适配不同主机的参数。
主机分组:灵活管理多节点
1. 基础分组
在 inventory
文件中定义主机组,支持数字范围和通配符:
[web]
web-[1:3].example.com # 匹配 web-1.example.com 至 web-3.example.com[db]
db-*.example.com # 匹配所有以 db- 开头的域名
2. 嵌套子组
通过 :children
定义层级关系,实现复杂环境管理:
[ubuntu]
host1 ansible_host=192.168.1.101[debian]
host2 ansible_host=192.168.1.102[linux:children] # 父组聚合子组
ubuntu
debian[prod:children] # 按环境划分
linux
3. 分组实战场景
-
多环境管理:
[dev] dev-web1 ansible_host=10.0.0.101[prod] prod-web1 ansible_host=192.168.1.101[all:children] # 全局聚合 dev prod
-
角色划分:
[nginx] lb1 ansible_host=192.168.1.200[tomcat] app[1:3] ansible_host=192.168.1.[201:203]
变量管理:动态配置
1. 变量定义位置
位置 | 优先级 | 适用场景 |
---|---|---|
命令行 (-e ) | 最高 | 临时覆盖变量 |
主机变量文件 | 高 | 主机专属配置(如 IP、端口) |
组变量文件 | 中 | 共享配置(如软件版本) |
库存文件 | 低 | 基础连接参数(如 ansible_port ) |
2. 变量定义示例
-
库存文件直接定义:
[web] web1 ansible_host=192.168.1.101 ansible_port=2222 # 指定 SSH 端口
-
主机变量文件 (
host_vars/web1.yml
):--- http_port: 8080 max_connections: 1000
-
组变量文件 (
group_vars/web.yml
):--- nginx_version: 1.25.2 app_env: production
3. 变量继承与覆盖
- 继承逻辑:子组继承父组变量,同名变量优先级为
子组 > 父组
。 - 调试技巧:使用
ansible-inventory --graph
查看主机归属与变量来源。
4.”Hello World“apache
Ansible playbook 是 Ansible 的基本组件之一,因为它们记录和执行 Ansible 的配置。通常,playbook 是自动执行要在远程计算机上执行的一组任务的主要方式。
它们通过收集所有必要的资源来编排有序流程或避免重复手动作,从而帮助我们实现自动化。Playbook 可以在人员之间重复使用和共享,它们被设计为对人类友好且易于在 YAML 中编写。
使用Ansible Playbooks - 带有示例的提示和技巧
sed -i ‘s/33.11/0.221/’ step-04/hosts
准备清单文件,命名为:hosts
[web]
host2 ansible_host=192.168.0.222 ansible_user=root
构建一个 playbook,它将在组中的计算机上安装 apache。web
- hosts: webtasks:- name: Installs apache web serverapt:pkg: apache2state: presentupdate_cache: true
- -name 为此任务添加了一个名称。虽然这不是必需的,但它可以在 playbook 运行时提供信息
- apt 模块,它可以 安装 Debian 软件包
运行 playbook :apache.yml
ansible-playbook -i step-04/hosts -l host2 step-04/apache.yml
- -l SUBSET, --limit SUBSET (限制分组)
运行后可看到以下内容:
PLAY [web] ************TASK [Gathering Facts] ***********
ok: [host2]TASK [Installs apache web server] *******************
changed: [host2]PLAY RECAP *************
host2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
- PLAY [web] *** 表示开始执行一个针对主机组 **
web
** 的 play。[web]
:对应 playbook 中定义的hosts: web
,目标主机组来自 Ansible 清单文件(如inventory
文件)。
- TASK [Gathering Facts] 用户自定义的任务名称,这里表示安装 Apache Web 服务器。
changed: [host2]
:表示此任务在主机host2
上执行成功并引发了系统变更(例如安装了新软件包)。- TASK [Installs apache web server] 用户自定义的任务名称,这里表示安装 Apache Web 服务器。
changed: [host2]
:表示此任务在主机host2
上执行成功并引发了系统变更(例如安装了新软件包)。
- PLAY RECAP 整个 play 的执行结果汇总。
- **
ok=2
**:2 个任务执行成功且无变更(如Gathering Facts
)。 - **
changed=1
**:1 个任务引发了变更(如安装 Apache)。 - 其他字段(如
failed=0
)表示无失败、无跳过、无重试等。
- **
现在再次运行一次,看看会发生什么:
ansible-playbook -i step-04/hosts -l host2 step-04/apache.ymlPLAY [web] *********TASK [Gathering Facts] ********
ok: [host2]TASK [Installs apache web server] *****
ok: [host2]PLAY RECAP *******
host2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
‘changed=0’ 。这是绝对正常的,也是核心功能之一 的 Ansible:playbook 仅在有事可做时才会起作用。这称为幂等性
5.优化apache设置
我们已经安装了 apache,现在让我们设置我们的 virtualhost。
在 Apache 配置场景中,virtualhost
(虚拟主机)用于在同一台服务器上托管多个网站或应用。通过不同的域名、IP 或端口,Apache 可区分请求并返回对应的网站内容。在 Ansible Playbook 中,配置 virtualhost
通常涉及以下操作:
- 创建虚拟主机配置文件(如
.conf
文件); - 指定域名、文档根目录、日志路径等参数;
- 启用配置并重启 Apache 服务。
我们的服务器上只需要一个 virtualhost,但我们想替换默认的 一个具有更具体的东西。因此,我们必须删除当前的 (大概是)VirtualHost,发送我们的 VirtualHost,激活它并 重新启动 Apache。
添加我们的 virtualhost 配置 给 Host1,路径为:files/awesome-app
<VirtualHost *:80>DocumentRoot /var/www/awesome-appOptions -IndexesErrorLog /var/log/apache2/error.logTransferLog /var/log/apache2/access.log
</VirtualHost>
指令 | 作用 |
---|---|
<VirtualHost *:80> | 监听所有 IP 的 80 端口,处理对该虚拟主机的请求。 |
DocumentRoot | 网站根目录为 /var/www/awesome-app ,存放网站文件(需确保目录存在)。 |
Options -Indexes | 禁止目录浏览(若目录中没有 index.html ,用户无法查看文件列表)。 |
ErrorLog | 错误日志路径为 /var/log/apache2/error.log 。 |
TransferLog | 访问日志路径为 /var/log/apache2/access.log 。 |
hosts
[web]
host1 ansible_host=192.168.0.221 ansible_user=root
更新 apache playbook
---
- hosts: web #指定该 Play 针对清单文件(inventory)中定义的 web 主机组执行。tasks:- name: Installs apache web server
#模块:apt(适用于 Debian/Ubuntu 系统)。apt:
#安装 Apache2 软件包。pkg: apache2
#确保 Apache2 已安装。state: presentupdate_cache: true
#等同于执行 apt update,更新软件包缓存(确保安装最新版本)。- name: Push default virtual host configurationcopy:src: files/awesome-appdest: /etc/apache2/sites-available/awesome-app.conf
#将本地 files/awesome-app 文件复制到目标主机的 /etc/apache2/sites-available/awesome-app.conf。mode: 0640
#设置文件权限为 -rw-r-----(所有者可读写,所属组可读,其他用户无权限)。- name: Activates our virtualhostfile:src: /etc/apache2/sites-available/awesome-app.confdest: /etc/apache2/sites-enabled/awesome-app.confstate: link
#在 sites-enabled 目录创建符号链接(state: link),指向 sites-available/awesome-app.conf。Apache 会加载 sites-enabled 中的配置文件,这一步等同于运行 a2ensite awesome-app.conf。 notify:- restart apache
#如果任务状态为 changed,触发 restart apache 处理器。- name: Disable the default virtualhostfile:dest: /etc/apache2/sites-enabled/000-default.confstate: absentnotify:- restart apache- name: Disable the default ssl virtualhost
#这两步禁用默认虚拟主机,防止默认虚拟主机与新配置冲突。 file:dest: /etc/apache2/sites-enabled/default-ssl.confstate: absent
#state: absent:删除 sites-enabled 目录中的默认配置文件符号链接(等同于 a2dissite 000-default.conf 和 a2dissite default-ssl.conf)。notify:- restart apachehandlers:- name: restart apache
#处理器(Handlers),当任务触发 notify: restart apache 时,重启 Apache 服务(仅在相关配置文件发生变更时触发)。service:name: apache2state: restarted
执行输出:
PLAY [web] ********* TASK [Gathering Facts] ********
ok: [host1]TASK [Installs apache web server] *******
ok: [host1]TASK [Push default virtual host configuration] ****c
changed: [host1]TASK [Activates our virtualhost] ****** c
changed: [host1]TASK [Disable the default virtualhost] ******
changed: [host1]TASK [Disable the default ssl virtualhost] ******
ok: [host1]RUNNING HANDLER [restart apache] *******
changed: [host1]PLAY RECAP *******
host1 : ok=7 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
在重新启动 apache 之前,我们难道不应该检查一下配置是否正常吗?如果我们的配置文件不正确,则不会中断服务。
6.在配置正确时重新启动
我们已经安装了 apache,推送了我们的 virtualhost 并重新启动了服务器。但 如果我们希望 playbook 仅在配置为 正确?让我们这样做。
Ansible 有一个漂亮的功能:如果出现问题,它将停止所有处理 错。如果配置 文件无效。
让我们更改我们的虚拟主机配置文件搞破坏DocumentDoot=》RocumentDoot:awesome-app
<VirtualHost *:80>RocumentDoot /var/www/awesome-appOptions -IndexesErrorLog /var/log/apache2/error.logTransferLog /var/log/apache2/access.log
</VirtualHost>
如前所述,当任务失败时,处理将停止。因此,我们将确保 配置在重新启动服务器之前有效。我们还首先添加 我们的 VirtualHost 在删除默认 VirtualHost 之前,因此后续的重启 (可能直接在服务器上完成) 不会破坏 Apache。
---
- hosts: web # 🌐 指定目标主机组为 `web`(需在 Ansible 清单中定义)tasks:# 🔧 任务1:安装 Apache- name: Installs apache web serverapt: # 📦 使用 apt 模块(Debian/Ubuntu 系统)pkg: apache2 # 软件包名称state: present # 确保 apache2 已安装update_cache: true # 先执行 `apt update` 更新软件源缓存# 📄 任务2:推送虚拟主机配置文件- name: Push future default virtual host configurationcopy: # 📂 使用 copy 模块复制文件src: files/awesome-app # 本地文件路径(相对于 Playbook)dest: /etc/apache2/sites-available/awesome-app.conf # 目标路径mode: 0640 # 设置文件权限为 -rw-r-----# 🔗 任务3:启用自定义虚拟主机- name: Activates our virtualhostcommand: a2ensite awesome-app # 🖥️ 直接调用 Apache 命令启用站点# 等同于创建符号链接:/etc/apache2/sites-enabled/awesome-app.conf -> ../sites-available/awesome-app.conf# ✅ 任务4:验证 Apache 配置语法- name: Check that our config is validcommand: apache2ctl configtest # 🔍 检查配置是否有语法错误# 如果配置错误,任务会失败并终止 Playbook 执行# 🚫 任务5:禁用默认虚拟主机(非 SSL)- name: Deactivates the default virtualhostcommand: a2dissite 000-default # ❌ 禁用默认站点# 🚫 任务6:禁用默认 SSL 虚拟主机- name: Deactivates the default ssl virtualhostcommand: a2dissite default-ssl # ❌ 禁用默认 SSL 站点notify: # 🔔 触发处理器- restart apache # 仅当此任务状态为 changed 时重启 Apachehandlers: # ⚙️ 处理器(异步任务)# 🔄 处理器:重启 Apache 服务- name: restart apacheservice: # 🛠️ 使用 service 模块name: apache2 # 服务名称state: restarted # 重启服务
执行
ansible-playbook -i step-06/hosts -l host1 step-06/apache.ymlPLAY [web] ******** TASK [Gathering Facts] ********
ok: [host1]TASK [Installs apache web server] *********
ok: [host1]TASK [Push future default virtual host configuration] *******
changed: [host1]TASK [Activates our virtualhost] *****
changed: [host1]TASK [Check that our config is valid] *********
fatal: [host1]: FAILED! => {"changed": true, "cmd": ["apache2ctl", "configtest"], "delta": "0:00:00.020643", "end": "2025-05-01 06:30:47.032996", "msg": "non-zero return code", "rc": 1, "start": "2025-05-01 06:30:47.012353", "stderr": "AH00526: Syntax error on line 2 of /etc/apache2/sites-enabled/awesome-app.conf:\nInvalid command 'RocumentDoot', perhaps misspelled or defined by a module not included in the server configuration", "stderr_lines": ["AH00526: Syntax error on line 2 of /etc/apache2/sites-enabled/awesome-app.conf:", "Invalid command 'RocumentDoot', perhaps misspelled or defined by a module not included in the server configuration"], "stdout": "Action 'configtest' failed.\nThe Apache error log may have more information.", "stdout_lines": ["Action 'configtest' failed.", "The Apache error log may have more information."]}PLAY RECAP *********
host1 : ok=4 changed=2 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
正如您所见,由于 apache2ctl
在失败时会以退出代码 1 返回,所以 Ansible 会意识到这一点并停止处理。太棒了!
嗯,实际上情况不太好……我们的虚拟主机还是添加上了。不过,后续任何一次重启 Apache 都会报错我们的配置有问题并退出。所以我们需要一种方法来捕获失败并回退。
从错误信息可以明确看出 Apache 配置文件的语法错误直接导致 Playbook 执行失败
Invalid command 'RocumentDoot'
#重启测试
ansible -i step-06/hosts -m service -a 'name=apache2 state=restarted' host1
host1 | FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"msg": "Unable to restart service apache2: Job for apache2.service failed because the control process exited with error code.\nSee \"systemctl status apache2.service\" and \"journalctl -xe\" for details.\n"
}
7.使用条件语句
我们已经安装了 Apache,推送了虚拟主机并重启了服务器。 但如果出现问题,我们希望将一切恢复到稳定状态。
出现问题时回退
当任务失败时,处理就会停止……除非我们接受失败(而且我们应该接受)。这就是我们要做的: 如果出现失败,继续处理,但仅限于撤销我们已做的操作。
- hosts: web # 仅在清单中定义的 'web' 主机组执行tasks:# 安装 Apache- name: Installs apache web serverapt:pkg: apache2 # 安装 Apache2 软件包state: present # 确保软件包存在update_cache: true # 先执行 `apt update` 更新缓存# 推送配置文件(潜在风险点)- name: Push future default virtual host configurationcopy:src: files/awesome-app # 本地配置文件名dest: /etc/apache2/sites-available/awesome-app.conf # 目标路径mode: 0640 # 权限设置(可能需确保 Apache 用户可读)# 启用新虚拟主机(非幂等操作)- name: Activates our virtualhostcommand: a2ensite awesome-app # ? 重复执行会报错 "Site already enabled"args:creates: /etc/apache2/sites-enabled/awesome-app.conf #幂等性控制,添加creates参数确保任务仅在符号链接不存在时执行,避免重复报错。notify: restart apache # 触发重启# 验证配置语法(关键安全阀)- name: Check that our config is validcommand: apache2ctl configtestregister: result # 存储命令执行结果ignore_errors: true # 即使验证失败也继续执行(触发回滚)# 回滚操作1:重新启用默认站点(需确保默认配置存在)- name: Rolling back - Restoring old default virtualhostcommand: a2ensite 000-defaultwhen: result is failed # 仅在配置验证失败时执行# 回滚操作2:禁用新站点- name: Rolling back - Removing our virtualhostcommand: a2dissite awesome-appwhen: result is failed# 强制终止 Playbook 并提示错误- name: Rolling back - Ending playbookfail:msg: "Configuration file is not valid. Please check that before re-running the playbook."when: result is failed # 终止后续任务# 禁用默认站点(仅在配置验证成功后执行)- name: Deactivates the default virtualhostcommand: a2dissite 000-defaultnotify: restart apache # 统一服务重启触发,任何修改站点配置的操作都需要重启 Apache,否则变更不会生效。# 禁用默认 SSL 站点- name: Deactivates the default ssl virtualhostcommand: a2dissite default-sslnotify:- restart apache # 触发服务重启(但其他任务也需要类似设置)handlers:# 服务重启处理器- name: restart apacheservice:name: apache2state: restarted
关键字 register
会记录 apache2ctl configtest
命令的输出(退出状态、标准输出、标准错误……),并且 when: result is failed
会检查已注册的变量( result
)是否包含失败状态。
ansible-playbook -i step-07/hosts -l host1 step-07/apache.ymlPLAY [web] ********TASK [Gathering Facts] *************
ok: [host1]TASK [Installs apache web server] ********
ok: [host1]TASK [Push future default virtual host configuration] *********
ok: [host1]TASK [Activates our virtualhost] ************
ok: [host1]TASK [Check that our config is valid] *************
fatal: [host1]: FAILED! => {"changed": true, "cmd": ["apache2ctl", "configtest"], "delta": "0:00:00.022459", "end": "2025-05-01 07:10:28.993221", "msg": "non-zero return code", "rc": 1, "start": "2025-05-01 07:10:28.970762", "stderr": "AH00526: Syntax error on line 2 of /etc/apache2/sites-enabled/awesome-app.conf:\nInvalid command 'RocumentDoot', perhaps misspelled or defined by a module not included in the server configuration", "stderr_lines": ["AH00526: Syntax error on line 2 of /etc/apache2/sites-enabled/awesome-app.conf:", "Invalid command 'RocumentDoot', perhaps misspelled or defined by a module not included in the server configuration"], "stdout": "Action 'configtest' failed.\nThe Apache error log may have more information.", "stdout_lines": ["Action 'configtest' failed.", "The Apache error log may have more information."]}
...ignoringTASK [Rolling back - Restoring old default virtualhost] ******************
changed: [host1]TASK [Rolling back - Removing our virtualhost] ****************
changed: [host1]TASK [Rolling back - Ending playbook] **************
fatal: [host1]: FAILED! => {"changed": false, "msg": "Configuration file is not valid. Please check that before re-running the playbook."}PLAY RECAP ************
host1 : ok=7 changed=3 unreachable=0 failed=1 skipped=0 rescued=0 ignored=1
看起来如预期般奏效了。让我们试着重启 Apache 看看是否真的成功了:
ansible -i step-07/hosts -m service -a 'name=apache2 state=restarted' host1
host1 | CHANGED => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": true,"name": "apache2","state": "started","status": {
能正常启动,现在我们的 Apache 服务器已免受错误配置的影响了。
8.从 Git 部署网站
我们已经安装了 Apache,推送了虚拟主机并安全地重启了服务器。 现在我们将使用 Git 模块来部署我们的应用程序。
git模块
我们的虚拟主机已设置好,但还需要做一些改动才能完成部署。 首先,我们要部署一个 PHP 应用程序,所以需要安装 libapache2-mod-php
包。其次,由于用于克隆我们应用程序的 Git 仓库的 Git 模块需要用到它,所以我们还得安装 git
。
Ansible 可以遍历一系列项目,并像这样在操作中使用每个项目:
...
- name: Installs necessary packagesapt:pkg: "{{ item }}"state: latestupdate_cache: truewith_items:- apache2- libapache2-mod-php- git
...
注意:自 2.7 版本起,在模块中使用循环仅调用一次的做法已弃用。您可以按照下表所示指定包的列表:
Option A 选择一个 | Option B 选项B |
---|---|
- name: "Install foo & bar" apt: pkg: ["foo", "bar"] | - name: "Install foo & bar" apt: pkg: - foo - bar |
最终方案:
- hosts: webtasks:- name: ;**安装必要软件包**;(包括Apache, PHP模块, Git)apt:pkg: ["apache2", "libapache2-mod-php", "git"]state: latestupdate_cache: true- name: ;**推送默认的虚拟主机配置文件**;copy:src: files/awesome-appdest: /etc/apache2/sites-available/awesome-app.confmode: 0640- name: ;**激活我们的虚拟主机**;command: a2ensite awesome-app- name: ;**检查配置是否有效**;command: apache2ctl configtestregister: resultignore_errors: true- name: ;**回滚 - 恢复旧的默认虚拟主机**;(如果配置测试失败)command: a2ensite 000-defaultwhen: result is failed- name: ;**回滚 - 移除我们的虚拟主机**;(如果配置测试失败)command: a2dissite awesome-appwhen: result is failed- name: ;**回滚 - 结束Playbook**;(如果配置测试失败)fail:msg: "配置文件无效。请在重新运行Playbook之前检查。"when: result is failed#如果执行git模块出现Failed to connect to github.com port 443: Connection refused,Ansible 的 git 模块可能因旧仓库状态导致冲突。强制清理目标目录#- name: 清理旧目录# file:# path: /var/www/awesome-app# state: absent# ignore_errors: yes # 防止目录不存在时报错# tags: deploy- name: ;**部署我们的应用程序**;(使用Git)git:repo: 'https://github.com/leucos/ansible-tuto-demosite.git'dest: /var/www/awesome-appforce: yes # 强制覆盖本地修改update: yes # 确保拉取最新代码version: master # 明确指定分支tags: deploy # 可通过`ansible-playbook ... --tags deploy`单独执行此任务- name: ;**禁用默认的虚拟主机**;(应在部署后禁用,确保不会冲突)command: a2dissite 000-defaultnotify: restart apache # 统一服务重启触发,任何修改站点配置的操作都需要重启 Apache,否则变更不会生效。- name: ;**禁用默认的SSL虚拟主机**;(如果不需要SSL,可考虑移除或注释此行)command: a2dissite default-sslnotify:- restart apachehandlers:- name: restart apacheservice:name: apache2state: restarted
启动
ansible-playbook -i step-08/hosts -l host1 step-08/apache.ymlPLAY [web] ************TASK [Gathering Facts] ***********
ok: [host1]TASK [;**安装必要软件包**;(包括Apache, PHP模块, Git)] **********
ok: [host1]TASK [;**推送默认的虚拟主机配置文件**;] ***********
ok: [host1]TASK [;**激活我们的虚拟主机**;] *************
changed: [host1]TASK [;**检查配置是否有效**;] *************
changed: [host1]TASK [;**回滚 - 恢复旧的默认虚拟主机**;(如果配置测试失败)] ***************
skipping: [host1]TASK [;**回滚 - 移除我们的虚拟主机**;(如果配置测试失败)] ******************
skipping: [host1]TASK [;**回滚 - 结束Playbook**;(如果配置测试失败)] **************
skipping: [host1]TASK [;**部署我们的应用程序**;(使用Git)] ************
ok: [host1]TASK [;**禁用默认的虚拟主机**;(应在部署后禁用,确保不会冲突)] ****************
changed: [host1]TASK [;**禁用默认的SSL虚拟主机**;(如果不需要SSL,可考虑移除或注释此行)] ************
changed: [host1]RUNNING HANDLER [restart apache] **************
changed: [host1]PLAY RECAP ************
host1 : ok=9 changed=5 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
访问测试host1
http://192.168.0.221
Welcome to your awesome app !
App backend host1显示了一张图片以及服务器的主机名
请注意, tags: deploy
这一行允许您仅执行playbook中的某一部分。 假设您为您的网站推送了一个新版本。您希望加快速度,仅执行负责部署的部分。标签允许您这样做。 当然,“deploy”只是一个字符串,它没有任何特定的含义,可以是任何内容。让我们看看如何使用它:
ansible-playbook -i step-08/hosts -l host1 step-08/apache.yml -t deployPLAY [web] **********TASK [Gathering Facts] ***********
ok: [host1]TASK [;**部署我们的应用程序**;(使用Git)] **********
ok: [host1]PLAY RECAP **************
host1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0# 显示所有可用标签
ansible-playbook site.yml --list-tags# 详细日志跟踪
ansible-playbook site.yml -t deploy -vvv
Failed to connect to github.com port 443: Connection refused
如果执行到git模块时报此错:
查看具体执行情况
ansible-playbook -i step-12test/hosts -l web step-12test/site.yml --tags deploy -vvv
先排查防火墙、网络状态等网络情况,并进入对应报错的主机执行git 测试能否下载项目,
#手动查询分支情况
git ls-remote --heads https://github.com/leucos/ansible-tuto-demosite.git
Ansible 的 git 模块可能因旧仓库状态导致冲突。强制清理目标目录,添加模块再测试
- name: 清理旧目录file:path: /var/www/awesome-appstate: absentignore_errors: yes # 防止目录不存在时报错tags: deploy- name: ;**部署我们的应用程序**;(使用Git)git:repo: 'https://github.com/leucos/ansible-tuto-demosite.git'dest: /var/www/awesome-appforce: yes # 强制覆盖本地修改update: yes # 确保拉取最新代码version: master # 明确指定分支tags: deploy # 可通过`ansible-playbook ... --tags deploy`单独执行此任务
9.添加另一台 Web 服务器
现在有了一台web服务器(host1),现在再添加一台web服务器和一台负载均衡服务器
更新inventory
sed -i -e ‘s/host0/host3/’ -e ‘s/33/0/’ -e ‘s/11/221/’ -e ‘s/12/222/’ -e ‘s/10/223/’ step-09/hosts
cat step-09/hosts
[web]
host1 ansible_host=192.168.0.221 ansible_user=root
host2 ansible_host=192.168.0.222 ansible_user=root[haproxy]
host3 ansible_host=192.168.0.223 ansible_user=root
在此指定 ansible_host
是因为主机的 IP 地址与预期不同(或者无法解析)。您可以将这些主机添加到 /etc/hosts
中,从而无需担心,或者使用真实的主机名(通常您会采取的做法)。
再建一个web服务器
cp step-08/apache.yml step-09/apache.yml
ansible-playbook -i step-09/hosts step-09/apache.ymlPLAY [web] ************TASK [Gathering Facts] ***************
ok: [host2]
ok: [host1]TASK [;**安装必要软件包**;(包括Apache, PHP模块, Git)] *****************
ok: [host1]
changed: [host2]TASK [;**推送默认的虚拟主机配置文件**;] *************
changed: [host2]
ok: [host1]TASK [;**激活我们的虚拟主机**;] *************
changed: [host2]
changed: [host1]TASK [;**检查配置是否有效**;] ***************
changed: [host1]
changed: [host2]TASK [;**回滚 - 恢复旧的默认虚拟主机**;(如果配置测试失败)] *****************
skipping: [host2]
skipping: [host1]TASK [;**回滚 - 移除我们的虚拟主机**;(如果配置测试失败)] *****************
skipping: [host1]
skipping: [host2]TASK [;**回滚 - 结束Playbook**;(如果配置测试失败)] *************
skipping: [host1]
skipping: [host2]TASK [;**部署我们的应用程序**;(使用Git)] *************
ok: [host1]
changed: [host2]TASK [;**禁用默认的虚拟主机**;(应在部署后禁用,确保不会冲突)] *************
changed: [host1]
changed: [host2]TASK [;**禁用默认的SSL虚拟主机**;(如果不需要SSL,可考虑移除或注释此行)] ************
changed: [host2]
changed: [host1]RUNNING HANDLER [restart apache] ***********
changed: [host2]
changed: [host1]PLAY RECAP *************
host1 : ok=9 changed=5 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
host2 : ok=9 changed=8 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
我们所要做的就是从命令行中移除 -l host1
。请记住, -l
是一个限制在特定主机上运行playbook的开关。现在我们不再限制了,它将在playbook预期运行的所有主机上运行(即 web
)。
如果我们有其他服务器在 web
组中,但只想将playbook限制在其中的一个子集上,那么我们可以使用例如: -l firsthost:secondhost:...
。
10.HAProxy 配置模板
HAProxy 是一个高性能的 负载均衡器 和 反向代理,常用于分发流量到多个后端服务器,提升系统可用性和扩展性。核心功能包括:
- 流量分发:支持轮询(round-robin)、加权轮询、最少连接等算法。
- 健康检查:自动检测后端服务器状态,移除故障节点。
- SSL终止:可处理HTTPS请求解密,减轻后端服务器压力。
- 高可用性:结合Keepalived可实现双机热备。
Ansible 使用 Jinja2,这是 Python 的一个模板引擎。在编写 Jinja2 模板时,您可以使用 Ansible 定义的任何变量。
例如,如果您想要输出当前模板所构建的主机的 inventory_name,您只需在 Jinja 模板中写入 {{ inventory_hostname }}
即可。
或者如果您需要第一个以太网接口的 IP 地址(Ansible 通过 setup
模块已知),您只需在模板中写入: {{ ansible_default_ipv4.address}}
(这与 {{ ansible_default_ipv4['address'] }}
等效)。
Jinja2 模板还支持条件语句、for 循环等。
让我们创建一个 templates/
目录,并在其中创建一个 Jinja 模板。我们将它命名为 haproxy.cfg.j2
。按照惯例,我们使用 .j2
扩展名,以表明这是一个 Jinja2 模板,但这并非必要。
step-10/templates/haproxy.cfg.j2
globaldaemon # 以守护进程模式运行maxconn 256 # 设置最大并发连接数为256defaultsmode http # 默认使用HTTP模式timeout connect 5000ms # 连接超时时间为5秒timeout client 50000ms # 客户端超时时间为50秒timeout server 50000ms # 服务器端超时时间为50秒listen clusterbind {{ ansible_host }}:80 # 绑定主机的IP和端口(动态变量来自Ansible)mode http # 启用HTTP模式stats enable # 开启统计页面balance roundrobin # 使用轮询负载均衡算法
{% for backend in groups['web'] %} # 遍历Ansible的web组内所有主机server {{ hostvars[backend]['ansible_hostname'] }} {{ hostvars[backend]['ansible_facts']['default_ipv4']['address'] }} check port 80 # 动态生成后端服务器配置(主机名+IP+健康检查)
{% endfor %}option httpchk HEAD /index.php HTTP/1.0 # 定义HTTP健康检查方法
注意事项!!! {#section1}
检查Ansible事实中的default_ipv4
是否是正确的,如果不是需更在haproxy.cfg.j2中改为正确的路径
#我这里的事实中['default_ipv4']对应eth0,目前使用的网卡是eth1(由于vagrant设置的public_network)
ansible -i hosts -m setup host1
host1 | SUCCESS => {
...............................................
"ansible_eth1": {"active": true,"device": "eth1","features": {
..............................................},"hw_timestamp_filters": [],"ipv4": {"address": "192.168.0.221",
#将step-10/templates/haproxy.cfg.j2配置文件中的server改为如下
server {{ hostvars[backend]['ansible_hostname'] }} {{ hostvars[backend].ansible_eth1.ipv4.address }}:80 check port 80
#改完后再执行Ansible-Playbook测试
该模板用于动态生成HAProxy负载均衡配置:
- 动态绑定IP端口:通过
{{ ansible_host }}
注入主机IP。 - 自动化后端服务器配置:基于Ansible的
web
主机组循环生成服务器列表,避免手动维护。 - 参数统一管理:超时时间、负载均衡策略等集中定义,便于批量修改。
HAProxy playbook
step-10/haproxy.yml
- hosts: webgather_facts: true # 收集目标服务器的系统信息(如IP、OS版本)- hosts: haproxy # 针对名为 "haproxy" 的主机组执行以下任务tasks:# 任务1:安装 HAProxy- name: Installs haproxy load balancerapt:pkg: haproxy # 包名称state: present # 确保已安装update_cache: yes # 更新 apt 缓存(相当于 apt update)# 任务2:推送配置文件- name: Pushes configurationtemplate:src: templates/haproxy.cfg.j2 # 使用 Jinja2 模板生成配置文件dest: /etc/haproxy/haproxy.cfg # 目标路径mode: 0640 # 文件权限(root可读写,haproxy组可读)owner: root # 文件所有者group: root # 文件所属组notify:- restart haproxy # 文件变更后触发重启操作# 任务3:确保 HAProxy 开机自启- name: Sets default starting flag to 1lineinfile:dest: /etc/default/haproxy # 修改系统服务配置文件regexp: "^ENABLED" # 查找以 ENABLED 开头的行line: "ENABLED=1" # 强制设置为 1(启用服务)notify:- restart haproxyhandlers:# 定义重启 HAProxy 的处理程序- name: restart haproxyservice:name: haproxystate: restarted
执行:
ansible-playbook -i step-10/hosts step-10/apache.yml step-10/haproxy.ymlPLAY [web] ******************************************************************************************************************************TASK [Gathering Facts] ******************************************************************************************************************
ok: [host1]
ok: [host2]TASK [Installs necessary packages] ******************************************************************************************************
ok: [host1]
ok: [host2]TASK [Push future default virtual host configuration] ***********************************************************************************
ok: [host2]
ok: [host1]TASK [Activates our virtualhost] ********************************************************************************************************
changed: [host2]
changed: [host1]TASK [Check that our config is valid] ***************************************************************************************************
changed: [host2]
changed: [host1]TASK [Rolling back - Restoring old default virtualhost] *********************************************************************************
skipping: [host1]
skipping: [host2]TASK [Rolling back - Removing out virtualhost] ******************************************************************************************
skipping: [host1]
skipping: [host2]TASK [Rolling back - Ending playbook] ***************************************************************************************************
skipping: [host1]
skipping: [host2]TASK [Deploy our awesome application] ***************************************************************************************************
ok: [host1]
ok: [host2]TASK [Deactivates the default virtualhost] **********************************************************************************************
changed: [host2]
changed: [host1]TASK [Deactivates the default ssl virtualhost] ******************************************************************************************
changed: [host2]
changed: [host1]RUNNING HANDLER [restart apache] ********************************************************************************************************
changed: [host2]
changed: [host1]PLAY RECAP ******************************************************************************************************************************
host1 : ok=9 changed=5 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
host2 : ok=9 changed=5 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0PLAY [web] ******************************************************************************************************************************TASK [Gathering Facts] ******************************************************************************************************************
ok: [host2]
ok: [host1]PLAY [haproxy] **************************************************************************************************************************TASK [Gathering Facts] ******************************************************************************************************************
ok: [host3]TASK [Installs haproxy load balancer] ***************************************************************************************************
changed: [host3]TASK [Pushes configuration] *************************************************************************************************************
changed: [host3]TASK [Sets default starting flag to 1] **************************************************************************************************
changed: [host3]RUNNING HANDLER [restart haproxy] *******************************************************************************************************
changed: [host3]PLAY RECAP ******************************************************************************************************************************
host1 : ok=10 changed=5 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
host2 : ok=10 changed=5 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
host3 : ok=5 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
测试
http://192.168.0.223/
您甚至可以在 http://192.168.0.233/haproxy?stats 查看 HAProxy 的统计信息。
正常Status为OPEN,如果为DOWN,host3:80报503注意检查以下内容:
- 防火墙(host1 和 host2是否能访问)
- HAProxy配置中的服务器ip是否正确(HAProxy服务器host3中的haproxy.cfg配置文件)
cat /etc/haproxy/haproxy.cfg #检查类似以下部分server host1 192.168.0.221:80 checkserver host2 192.168.0.222:80 check
处理办法见上面:注意事项
template和lineinfile模块
template
模块
- 将动态生成的配置文件推送到目标服务器。
- 使用 Jinja2 模板引擎,允许在配置文件中插入变量、循环、条件判断等逻辑。
- 最终生成一个静态配置文件并传输到目标服务器的指定路径。
关键特性
- 动态渲染:假设您的
haproxy.cfg.j2
中可能包含类似下面的动态内容:
{% for server in groups.web %}
server {{ server }} {{ hostvars[server].ansible_default_ipv4.address }}:80 check
{% endfor %}
这会根据 web
主机组的服务器列表自动生成后端服务器配置。
- 幂等性:如果目标文件已经存在且内容相同,不会重复操作。
- 权限控制:可以精确设置文件权限、所有者和用户组。
适用场景
- 需要动态生成配置文件(例如根据变量、主机列表生成不同内容)。
- 需要将配置文件与 Playbook 逻辑解耦(模板和逻辑分离)。
lineinfile
模块
- 确保文件中某一行存在(或不存在)。
- 通过正则表达式匹配目标行,并修改或替换它。
- 常用于简单修改配置文件中的某个参数。
关键特性
- 精准修改:只修改匹配到的行,不影响文件其他内容。
- 幂等性:如果目标行已经是
ENABLED=1
,不会重复修改。 - 灵活性:可以用正则表达式匹配复杂模式。
适用场景
- 修改文件中的某个特定参数(如
ENABLED=1
)。 - 添加一行内容(例如在
sshd_config
中添加PermitRootLogin no
)。
对比总结
模块 | 用途 | 特点 | 适用场景 |
---|---|---|---|
template | 生成并推送动态配置文件 | 支持变量渲染、循环、条件判断 | 需要动态生成完整配置文件时 |
lineinfile | 修改文件中某一行 | 精准修改、正则匹配 | 修改单个参数或添加单行内容 |
常见问题
- 为什么不直接用
copy
模块?
copy
模块直接复制静态文件,而template
允许在文件中使用变量和逻辑。- 例如,在您的 HAProxy 配置中,后端服务器列表可能动态变化,用
template
可以自动生成列表。
lineinfile
会覆盖整个文件吗?
- 不会!它只修改匹配到的行,其他内容保持不变。
- 如果一行都不匹配会怎样?
- 默认会在文件末尾添加
line
指定的内容。可以通过insertafter
或insertbefore
参数控制插入位置。
11.变量再探
所以我们已经设置好了负载均衡器,运行得相当不错。我们从事实中获取变量,并用它们来构建配置。但 Ansible 还支持其他类型的变量。我们已经在清单中看到了 ansible_host
,但现在我们将使用在 host_vars
和 group_vars
文件中定义的变量。
微调我们的 HAProxy 配置
HAProxy 通常会检查后端是否处于活动状态。当某个后端似乎已停止运行时,它会被从后端池中移除,HAProxy 也不会再向其发送请求。
后端也可以有不同的权重(在 0 到 256 之间)。权重越高,与其他后端相比,该后端将接收的连接数就越多。如果节点的性能不均衡,这有助于更合理地分配流量。
我们将使用变量来配置所有这些参数。
Group vars
检查间隔将在 haproxy 的 group_vars 文件中设置。这将确保所有 haproxy 都能继承该设置。
我们只需在库存目录下创建文件 group_vars/haproxy.yml
即可。该文件必须以您想要为其定义变量的组命名。如果我们想为 web 组定义变量,该文件应命名为 group_vars/web.yml
。
请注意, .yml
是可选的:我们可以将 haproxy 组变量文件命名为 group_vars/haproxy
,Ansible 也能接受。扩展名只是帮助编辑器选择正确的语法高亮显示。
haproxy_check_interval: 3000
haproxy_stats_socket: /tmp/sock
#名称是任意的。当然,有意义的名称是推荐的,但没有规定的语法。您甚至可以像这样使用复杂的变量(也就是 Python 字典):
haproxy:check_interval: 3000stats_socket: /tmp/sock
这只是个人喜好问题。复杂变量有助于逻辑上对内容进行分组。 在某些情况下,它们还可以合并后续定义的键(但请注意,这并非 Ansible 的默认行为)。目前我们只使用简单变量。
Hosts vars
主机变量遵循完全相同的规则,但位于 host_vars
目录下的文件中。
让我们在 host_vars/host1.example.com
中为我们的后端定义权重:
haproxy_backend_weight: 100
#以及 host_vars/host2.example.com
haproxy_backend_weight: 150
如果我们在 group_vars/web
中定义 haproxy_backend_weight
,它将被用作“默认值”:在 host_vars
文件中定义的变量会覆盖在 group_vars
中定义的变量。
更新模板
模板必须更新以使用这些变量。
globaldaemonmaxconn 256
{% if haproxy_stats_socket %}stats socket {{ haproxy_stats_socket }}
{% endif %}defaultsmode httptimeout connect 5000mstimeout client 50000mstimeout server 50000mslisten clusterbind {{ ansible_host }}:80mode httpstats enablebalance roundrobin
{% for backend in groups['web'] %}server {{ hostvars[backend]['ansible_hostname'] }} {{ hostvars[backend].ansible_eth1.ipv4.address }}:80 check port 80 weight {{ hostvars[backend].haproxy_backend_weight | default(100) }}
{% endfor %}option httpchk HEAD /index.php HTTP/1.0
请注意,我们还引入了一个 {% if ...
块。只有当测试为真时,此块内的内容才会被渲染。因此,如果我们为负载均衡器在某个地方定义了 haproxy_stats_socket
(甚至可能在命令行中使用 --extra-vars="haproxy_stats_sockets=/tmp/sock"
),则包含的行将出现在生成的配置文件中(请注意,建议的设置非常不安全!)。
更新HAProxy.yaml:
---
- name: 配置 Web 服务器组hosts: webgather_facts: true # 收集 web 组的服务器信息(如 IP、OS 版本)# 用于后续模板生成后端服务器列表- name: 配置 HAProxy 负载均衡器hosts: haproxy # 目标为 haproxy 主机组tasks:# 任务1:安装 HAProxy- name: 安装 HAProxyapt:pkg: haproxystate: present # 确保 HAProxy 已安装update_cache: yes # 更新 apt 缓存(相当于 apt update)notify: haproxy_config_flow # 触发名为 haproxy_config_flow 的处理程序链# 任务2:备份当前配置(用于回退)- name: 备份 HAProxy 配置copy:src: /etc/haproxy/haproxy.cfg # 源文件(远程服务器上的当前配置)dest: /etc/haproxy/haproxy.cfg.bak # 备份路径remote_src: yes # 操作远程文件force: yes # 强制覆盖旧备份when: not ansible_check_mode # 仅在非检查模式运行(实际部署时执行)changed_when: false # 不标记为已变更(避免误触发其他任务)notify: haproxy_config_flow # 触发处理程序链# 任务3:生成并推送新配置- name: 生成并推送 HAProxy 配置template:src: templates/haproxy.cfg.j2 # Jinja2 模板路径dest: /etc/haproxy/haproxy.cfg # 目标配置文件路径mode: 0640 # 文件权限(root 读写,haproxy 组只读)owner: rootgroup: rootnotify: haproxy_config_flow # 触发处理程序链# 任务4:确保 HAProxy 开机自启- name: 启用 HAProxy 开机启动lineinfile:dest: /etc/default/haproxy # 修改服务启动配置文件regexp: "^ENABLED" # 匹配以 ENABLED 开头的行line: "ENABLED=1" # 强制设置为启用notify: haproxy_config_flow # 触发处理程序链handlers:# 处理程序1:验证配置文件语法- name: 验证 HAProxy 配置command: haproxy -c -f /etc/haproxy/haproxy.cfg # 检查配置合法性register: haproxy_validate # 存储命令执行结果(rc=0 表示成功)listen: haproxy_config_flow # 绑定到 haproxy_config_flow 事件链ignore_errors: yes # 允许验证失败后继续执行后续任务# 处理程序2:重启 HAProxy 服务(仅在验证成功时执行)- name: 重启 HAProxyservice:name: haproxystate: restartedwhen: - haproxy_validate.rc == 0 # 仅当验证通过时触发listen: haproxy_config_flow # 绑定到同一事件链# 处理程序3:恢复备份配置(验证失败时执行)- name: 恢复 HAProxy 备份配置copy:src: /etc/haproxy/haproxy.cfg.bak # 备份文件路径dest: /etc/haproxy/haproxy.cfg # 覆盖错误配置remote_src: yeswhen: - haproxy_validate.rc != 0 # 仅当验证失败时触发listen: haproxy_config_flow # 绑定到同一事件链notify: 重启 HAProxy # 恢复配置后触发重启
执行:
ansible-playbook -i step-11/hosts step-11/haproxy.ymlPLAY [配置 Web 服务器组] ****************************************************************************************************************TASK [Gathering Facts] ******************************************************************************************************************
ok: [host2]
ok: [host1]PLAY [配置 HAProxy 负载均衡器] **********************************************************************************************************TASK [Gathering Facts] ******************************************************************************************************************
ok: [host3]TASK [安装 HAProxy] *********************************************************************************************************************
ok: [host3]TASK [备份 HAProxy 配置] ****************************************************************************************************************
ok: [host3]TASK [生成并推送 HAProxy 配置] **********************************************************************************************************
changed: [host3]TASK [启用 HAProxy 开机启动] ************************************************************************************************************
ok: [host3]RUNNING HANDLER [验证 HAProxy 配置] *****************************************************************************************************
changed: [host3]RUNNING HANDLER [重启 HAProxy] **********************************************************************************************************
changed: [host3]RUNNING HANDLER [恢复 HAProxy 备份配置] *************************************************************************************************
skipping: [host3]PLAY RECAP ******************************************************************************************************************************
host1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
host2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
host3 : ok=7 changed=3 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
验证
http://192.168.0.223/haproxy?stats
root@host0:/home/ansible-tuto# ansible -i hosts -m shell -a "cat /etc/haproxy/haproxy.cfg" host3
host3 | CHANGED | rc=0 >>
globaldaemonmaxconn 256stats socket /tmp/sockdefaultsmode httptimeout connect 5000mstimeout client 50000mstimeout server 50000mslisten clusterbind 192.168.0.223:80mode httpstats enablebalance roundrobinserver host1 192.168.0.221:80 check port 80 weight 100server host2 192.168.0.222:80 check port 80 weight 150option httpchk HEAD /index.php HTTP/1.0
root@host0:/home/ansible-tuto# ansible -i hosts -m shell -a "haproxy -c -f /etc/haproxy/haproxy.cfg" host3
host3 | CHANGED | rc=0 >>
Configuration file is valid
listen
机制
1. listen
的作用
- 替代传统
notify
:在 Ansible 2.2+ 中引入,允许将多个处理程序(handlers)绑定到同一个事件名称。 - 执行顺序控制:所有绑定到同一
listen
名称的处理程序会按定义顺序依次执行。
2. 在您的 Playbook 中的工作流程
-
触发事件:当任意任务通过
notify: haproxy_config_flow
触发事件链时:tasks:- name: 生成并推送 HAProxy 配置template: ...notify: haproxy_config_flow # 触发事件链
-
执行顺序:
- 验证配置 → 重启服务 或 恢复备份 → 重启服务。
-
条件控制:通过
when
条件决定是否执行:- 若验证成功(
rc == 0
),执行重启 HAProxy
。 - 若验证失败(
rc != 0
),执行恢复 HAProxy 备份配置
,随后触发重启 HAProxy
。
- 若验证成功(
3. 与传统 notify
的对比
场景 | 传统 notify | listen 机制 |
---|---|---|
触发单个处理程序 | notify: 重启 HAProxy | 不适用 |
触发多个处理程序 | 需多次 notify | 所有绑定到同一 listen 的处理程序按序执行 |
条件执行 | 处理程序内部通过 when 控制 | 每个处理程序独立定义 when 条件 |
关键优化点说明
1. 备份强制覆盖 (force: yes
)
- 作用:确保每次备份时覆盖旧文件,避免备份文件过时。
- 必要性:如果未启用,当配置文件未变更时,备份任务不会覆盖旧备份,导致回退时可能恢复错误版本。
2. 验证失败后的回退流程
- 步骤:
- 生成新配置 → 2. 验证失败 → 3. 恢复备份 → 4. 重启服务(使用旧配置)。
- 结果:即使新配置错误,服务仍可用。
3. 统一事件链 (haproxy_config_flow
)
- 优势:将配置更新、验证、重启、回退逻辑集中管理,避免分散触发。
完整执行流程
- 安装 HAProxy:确保软件已安装。
- 备份配置:保存当前有效配置。
- 生成新配置:通过模板渲染推送新配置。
- 触发处理程序链:
- 验证配置 → 若成功 → 重启服务。
- 若失败 → 恢复备份 → 重启服务。
总结
通过 listen
机制,您实现了一个 原子化的配置管理流程:
- 集中控制:所有相关处理程序按序触发。
- 安全回退:验证失败时自动恢复至有效配置。
- 高可用性:避免服务因配置错误中断。
stats socket
的具体作用
stats socket
是 HAProxy 提供的一个 Unix Socket 接口,用于动态管理 HAProxy 的运行时状态。主要功能包括:
- 实时操作:
- 禁用/启用后端服务器(如
disable server backend/server1
)。 - 动态调整权重(如
set server backend/server1 weight 200
)。 - 查看连接数、会话状态等实时统计信息。
- 禁用/启用后端服务器(如
- 监控与管理:
- 通过命令行工具(如
socat
)或 API 直接与 HAProxy 交互。 - 支持自动化脚本动态调整负载均衡策略。
- 通过命令行工具(如
2. 是否可以不使用 stats socket
?
可以完全禁用。如果不使用 stats socket
:
- 优点:
- 消除安全风险:关闭潜在的攻击入口。
- 简化配置:减少维护复杂度。
- 缺点:
- 失去动态管理能力:无法通过 Socket 实时调整服务器状态或权重。
- 依赖重启:任何配置变更需重启 HAProxy 服务才能生效。
适用场景:
- 负载均衡策略固定,无需频繁调整。
- 通过 Ansible 等工具静态管理配置变更。
3. 替代方案(未测试,待测试)
如果需动态管理 HAProxy 但不想使用 stats socket
,有以下替代方案:
方案1:HTTP 统计接口
通过 HAProxy 的 HTTP 统计页面 进行基础管理:
listen statsbind *:8404stats enablestats uri /haproxy?statsstats auth admin:SecurePassword123! # 强制认证stats refresh 10s # 自动刷新间隔
- 功能:
- 查看实时统计信息(如服务器状态、请求速率)。
- 通过浏览器或
curl
手动禁用服务器(需配合前端工具)。
- 限制:
- 无法直接执行复杂命令(如动态调整权重)。
- 依赖 HTTP 接口,需额外安全防护(如 HTTPS、IP 白名单)。
方案2:HAProxy Runtime API(企业版)(未测试,待测试)
HAProxy 企业版提供 Runtime API,支持通过 HTTP/HTTPS 动态管理:
globalruntime-api 0.0.0.0:9999 # 监听端口api-user admin # 认证用户api-password $6$加密密码 # 加密密码
- 功能:
- 类似
stats socket
的所有操作,但通过 HTTP 接口实现。 - 支持 RESTful 风格的管理命令。
- 类似
- 优势:
- 更易集成自动化工具(如 Prometheus、Grafana)。
- 支持 HTTPS 和细粒度权限控制。
方案3:Ansible 动态配置(未测试,待测试)
通过 Ansible 直接生成配置文件并触发重载:
- name: 更新 HAProxy 配置template:src: haproxy.cfg.j2dest: /etc/haproxy/haproxy.cfgnotify: 安全重载 HAProxyhandlers:- name: 安全重载 HAProxycommand: haproxy -f /etc/haproxy/haproxy.cfg -sf $(cat /var/run/haproxy.pid)
- 功能:
- 通过模板动态生成配置,替换旧配置后平滑重启(
-sf
参数)。
- 通过模板动态生成配置,替换旧配置后平滑重启(
- 优势:
- 无需开放额外端口或 Socket。
- 变更记录清晰(版本控制模板文件)。
总结与建议
方案 | 安全性 | 动态管理能力 | 复杂度 | 适用场景 |
---|---|---|---|---|
禁用 stats socket | 高 | 无 | 低 | 配置固定,无需实时调整 |
HTTP 统计页面 | 中 | 基础 | 中 | 需要简单监控 |
Runtime API | 高 | 完整 | 高 | 企业版用户,需自动化集成 |
Ansible 动态配置 | 高 | 中 | 中 | 通过 IaC 管理,变更频率中等 |
推荐实践:
- 如果无需动态调整,直接禁用
stats socket
。 - 若需动态管理且安全性优先,使用 HTTP 统计页面 + IP 白名单 + HTTPS。
- 企业用户可投资 HAProxy 企业版 的 Runtime API。
12.Playbook Roles实现配置模块化
将原有的 Playbook 重构为 角色(Roles),目的是:
- 模块化:将 Apache 和 HAProxy 的配置拆分为独立角色,便于复用和管理。
- 标准化结构:遵循 Ansible 角色的目录约定,提升代码可维护性。
- 依赖管理:通过角色间的依赖关系(如
meta/main.yml
)实现自动化执行顺序。
1角色目录结构解析
每个角色(如 apache
或 haproxy
)的目录结构如下:
roles/├── apache/ # 角色名称│ ├── tasks/ # 存放任务定义│ │ └── main.yml # 自动加载的入口任务文件│ ├── handlers/ # 存放处理程序(如服务重启)│ │ └── main.yml # 自动加载的入口处理程序文件│ ├── files/ # 存放静态文件(如配置文件)│ ├── templates/ # 存放 Jinja2 模板文件│ ├── defaults/ # 存放角色默认变量(优先级最低)│ ├── vars/ # 存放角色变量(优先级高于 defaults)│ └── meta/ # 定义角色依赖(如依赖其他角色)└── haproxy/└── ... # 结构同上
关键目录说明
tasks/main.yml
:角色的核心任务入口,Ansible 会自动执行此文件中的任务。handlers/main.yml
:定义处理程序(如restart apache
),由任务中的notify
触发。files/
和templates/
:存放静态文件和模板,引用时无需绝对路径。- 例如:
copy: src=awesome-app
会自动从files/
目录查找。
- 例如:
defaults/main.yml
:定义角色的默认变量,可被外部变量覆盖。meta/main.yml
:定义角色依赖,确保执行顺序(如先安装 Nginx 再配置 PHP)。
2文件命名规则
-
强制规则:
tasks/main.yml
、handlers/main.yml
等入口文件必须命名为main.yml
,否则 Ansible 无法自动加载。
-
灵活规则:
-
其他文件(如
tasks/install_packages.yml
)可自定义名称,但需在main.yml
中通过include
或import_tasks
显式引入。 -
例如:
# tasks/main.yml - include: install_packages.yml - include: configure_apache.yml
-
3迁移步骤详解
以迁移 Apache 配置为例:
-
创建角色目录:
mkdir -p step-12/roles/apache/{tasks,handlers,files}
使用
ansible-galaxy
初始化角色:ansible-galaxy init step-12/roles/haproxy # 自动生成标准目录结构
-
拆分任务:
-
将原
apache.yml
中的任务复制到tasks/main.yml
。只保留tasks部分-
#原apache.yml --- - hosts: webtasks:- name: Installs necessary packagesapt:*- name: Push future default virtual host configurationcopy: #现tasks/main.yml --- - name: Installs necessary packages apt: * - name: Push future default virtual host configuration copy:
-
-
-
移除对
files/
和templates/
的路径引用(角色会自动查找对应目录)。-
#源文件中路径表示 - name: Push future default virtual host configurationcopy:src: files/awesome-app # 显式路径 # 角色任务文件 (roles/apache/tasks/main.yml) #迁移到角色后写法 - name: Push future default virtual host configurationcopy:src: awesome-app # 直接引用文件名(无需路径)
-
Ansible 会自动在角色的
files/
目录下查找静态文件。 -
如果文件在
files/
的子目录中(如files/config/
),则需保留子目录路径,-
src: config/awesome-app # 文件路径为 roles/apache/files/config/awesome-app
-
-
-
提取处理程序:
-
将原 Playbook 中的
handlers
部分移到handlers/main.yml
。 -
#原apache.ymlhandlers:- name: restart apacheservice:name: apache2state: restarted #现handlers/main.yml --- - name: restart apacheservice:name: apache2state: restarted
-
-
移动静态文件:
- 将配置文件(如
awesome-app
)复制到files/
目录。
- 将配置文件(如
-
创建顶层 Playbook:
# site.yml - hosts: webroles:- apache # 自动加载 roles/apache 下的任务 - hosts: haproxyroles:- haproxy
4执行与验证
-
运行 Playbook:
ansible-playbook -i hosts site.yml ansible-playbook -i step-12/hosts step-12/site.yml
-
限制执行范围:
# 仅针对 web 主机组执行 ansible-playbook -i step-12/hosts -l web step-12test/site.yml
执行情况:
PLAY RECAP ********************************************************************************************************************************
host1 : ok=9 changed=6 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
host2 : ok=9 changed=6 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
host3 : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
13.Ansible 标签使用
在复杂的IT架构中,一个Ansible Playbook可能包含上百个任务。当您只需要更新Web服务配置却要完整执行整个Playbook时,就像为了换灯泡而重启整个数据中心。标签(Tags)的出现完美解决了这个痛点:
- 手术刀式精准操作:只运行指定任务,节省90%执行时间
- 环境隔离利器:开发/测试/生产环境差异化执行
- 应急响应加速器:快速定位回滚点,故障恢复时间缩短75%
1、为何需要标签?
在复杂的IT架构中,一个Ansible Playbook可能包含上百个任务。当您只需要更新Web服务配置却要完整执行整个Playbook时,就像为了换灯泡而重启整个数据中心。标签(Tags)的出现完美解决了这个痛点:
- 手术刀式精准操作:只运行指定任务,节省90%执行时间
- 环境隔离利器:开发/测试/生产环境差异化执行
- 应急响应加速器:快速定位回滚点,故障恢复时间缩短75%
2、标签基础语法
基础标记方法
列表式声明(推荐)
- name: 部署Apache服务apt:name: apache2state: latesttags:- web- install- critical
行内数组式
- name: 配置防火墙规则ufw:rule: allowport: "{{ http_port }}"tags: ['network', 'security']
命令行控制
命令 | 作用 | 示例场景 |
---|---|---|
-t tag1,tag2 | 执行包含任意指定标签的任务 | 紧急安全更新 |
--skip-tags tagA | 排除指定标签的任务 | 跳过耗时操作 |
-t tag1 -t tag2 | 多标签联合过滤(逻辑OR) | 跨角色操作 |
--list-tags | 显示所有可用标签 | Playbook文档化 |
3、实战场景案例
场景1:快速部署热修复补丁
# 仅执行代码部署和服务重启
ansible-playbook deploy.yml -t git_pull,service_restart# 组合标签排除数据库操作
ansible-playbook deploy.yml -t app_deploy --skip-tags db_migration
场景2:安全合规检查
- name: 检查SSH协议版本shell: ssh -Vregister: ssh_versiontags: security_audit- name: 验证防火墙状态command: ufw status verbosetags: - security_audit- compliance
场景3:多环境配置
- name: 加载生产环境配置template:src: prod.env.j2dest: /etc/app.envtags: prod_only- name: 开发环境调试设置lineinfile:path: /etc/app.confline: "debug_mode=true"tags: dev_only
4、高级技巧揭秘
- 标签继承模式
# meta/main.yml
dependencies:- role: commontags: [base_setup] # 自动继承base_setup标签
- 动态标签生成
- name: 按条件动态标记debug:msg: "数据库主节点专属配置"tags: "{{ 'db_master' if is_master else 'db_slave' }}"
- Handler标签联动
handlers:- name: 滚动重启服务systemd:name: appstate: restartedtags: canary_deploy # 灰度发布专用handler
5、最佳实践指南
-
命名规范三原则
- 业务维度:
web
,db
,cache
- 操作类型:
install
,config
,cleanup
- 环境标识:
prod
,staging
,dev
- 业务维度:
-
黄金组合策略
# 典型CI/CD流水线 ansible-playbook site.yml \-t "validate,deploy" \--skip-tags "migration,full_restart"
-
危险操作防护
- name: 数据库表结构迁移command: alembic upgrade headtags:- dangerous- db_migration
# 显式确认执行危险操作 ansible-playbook migrate.yml -t dangerous --extra-vars "confirm_danger=yes"
6、常见问题排查
Q1:标签未生效?
- 检查项:
- YAML缩进是否正确
- 角色依赖是否继承标签
- 是否存在同名标签覆盖
Q2:如何调试复杂标签?
# 显示标签执行顺序
ANSIBLE_DEBUG=1 ansible-playbook playbook.yml -t my_tag# 生成标签关系图
ansible-playbook playbook.yml --list-tags | grep -B1 'TASK TAGS'
Q3:部分节点执行异常?
- name: 条件标签示例debug:msg: "仅CentOS系统执行"tags: centos_onlywhen: ansible_distribution == "CentOS"
7、性能优化指标
场景 | 无标签执行时间 | 标签优化时间 | 提升幅度 |
---|---|---|---|
全量部署 | 12m 34s | 12m 34s | 0% |
配置更新 | 11m 45s | 1m 12s | 89% |
紧急回滚 | 9m 23s | 45s | 92% |
安全补丁 | 8m 56s | 38s | 93% |
8、延伸阅读
-
标签与Ansible Tower集成
# tower.yml launch_configuration:extra_vars:ansible_tags: "prod_deploy"limit: "webservers:&east"
-
基于标签的权限控制
# ansible.cfg [privilege_escalation] require_sudo_tags = package_install, service_control
-
标签驱动文档生成
ansible-doc-tagger generate --format markdown > PLAYBOOK_TAGS.md