Ansible Playbook
Ansible Playbook(剧本)是 Ansible 核心配置文件,采用 YAML 格式编写,用于定义一系列有序的自动化任务集合,描述 "要在哪些远程主机上执行哪些操作"。
YAML 基本规则
YAML 是一种易读的序列化格式,是 Playbook 的核心语法,核心规则如下:
-
缩进规则:仅允许空格缩进(禁止 Tab),通常 2 个半角空格为 1 级,缩进层级代表逻辑层级;
全角空格是中文输入法下的(肉眼看和半角空格一样,但解析器不认),必须替换为半角空格(英文输入法下的空格)。
-
大小写敏感 :变量名、模块名、键名(如
hosts/Hosts)均区分大小写; -
注释规则 :单行注释用
#(#后内容会被忽略),无原生多行注释(可每行加#); -
字符串规则 :字符串可加引号(单 / 双)或不加,含特殊字符(
:、$、空格)时必须加引号(如name: "web-server:8080"); -
核心标识:
-
元素以
-开头(短横线 + 空格),如- nginx; -
键值对用
key: value(冒号后必须加空格),如name: nginx;
-
-
playbook文件命名:文件名后缀无强制要求,只要内容是 YAML 即可执行,
.yaml或.yml结尾(前者更规范,后者是简写,均常用)
Playbook 常用 YAML 结构
Playbook(YAML文件)
├─ Play 1(一个部署单元,以 - 开头)
│ ├─ 基础配置(hosts、remote_user、gather_facts)
│ ├─ 变量(vars)
│ ├─ 任务(tasks)→ 每个任务由「模块+参数」组成
│ ├─ 模块+参数
│ └─ 模块+参数
├─ Play 2(另一个部署单元)
│ └─ ...(同Play 1的结构)
└─ ...
Playbook 以 "Play" 为基本单元,常见结构分为 2 类:
1. 单 Play 结构(极简版)
- hosts: webservers # 目标主机组
remote_user: root # 远程执行用户
tasks: # 任务列表
- name: 安装 Nginx
yum:
name: nginx
state: present
- name: 启动 Nginx
service:
name: nginx
state: started
2. 多 Play 结构(多主机组执行不同任务)
- hosts: webservers
vars:
web_name: nginx
tasks:
- name: 安装 Nginx
yum:
name: "{{ nginx }}"
state: present
- hosts: dbservers
tasks:
- name: 安装 MySQL
yum:
name: mysql-server
state: present
3. 带变量 / 处理器的嵌套结构
以下以「部署 MySQL 服务」为例,重新编写带变量 / 处理器的嵌套结构 Playbook,保持核心逻辑一致但场景更贴近实际运维,同时补充关键注释便于理解
# 针对所有被管理主机部署MySQL服务
- hosts: all
# 1. 定义Play级变量(可复用,修改时仅需改此处)
vars:
mysql_pkg: mariadb-server # CentOS默认MySQL兼容包
mysql_port: 3306
mysql_conf_path: /etc/my.cnf.d/mysql-server.cnf
mysql_service: mariadb
# 2. 定义处理器(仅被notify触发,用于配置变更后重启服务)
handlers:
- name: 重启MySQL服务并设置开机自启
ansible.builtin.service:
name: "{{ mysql_service }}" # 引用变量
state: restarted # 重启服务
enabled: true # 开机自启
# 3. 定义核心任务列表
tasks:
# 任务1:安装MySQL软件包(引用变量)
- name: 安装 {{ mysql_pkg }} 软件包
ansible.builtin.yum:
name: "{{ mysql_pkg }}"
state: present # 确保安装(幂等:已装则不操作)
# 任务2:启动MySQL服务(首次部署先启动)
- name: 启动 {{ mysql_service }} 服务
ansible.builtin.service:
name: "{{ mysql_service }}"
state: started
# 任务3:复制自定义MySQL配置文件(修改配置后触发处理器)
- name: 复制自定义MySQL配置文件(指定端口{{ mysql_port }})
ansible.builtin.copy:
src: ./mysql-server.cnf # 本地配置文件路径
dest: "{{ mysql_conf_path }}" # 远程目标路径(引用变量)
mode: '0644' # 配置文件权限
owner: root # 属主
group: root # 属组
# 仅当配置文件发生变更时,触发处理器重启MySQL
notify: 重启MySQL服务并设置开机自启
ansible-playbook用法
| 选项 | 简写 | 核心说明 |
|---|---|---|
--check |
-C |
模拟执行(干跑):仅展示要执行的操作,不实际修改目标主机系统 |
--inventory <路径> |
-i |
指定自定义 Inventory(主机清单)文件,默认路径 /etc/ansible/hosts |
--user <用户名> |
-u |
指定远程执行任务的用户(如 root、ansible) |
--become |
-b |
提权执行(等效 sudo),普通用户执行系统操作(如装软件)时必备 |
--become-user <用户> |
无 | 指定提权后的目标用户(默认 root),如 --become-user=nginx |
--verbose |
-v |
详细输出执行日志:-v 基础详情,-vvv 最详细(排错专用),-vv 中等 |
--ask-pass |
-k |
交互式提示输入远程主机 SSH 密码(避免明文写在 Inventory 中) |
--ask-become-pass |
-K |
交互式提示输入提权密码(如 sudo 密码) |
--limit <主机/组> |
无 | 限制仅执行指定主机 / 主机组的任务,如 --limit 192.168.1.100 |
Playbook 执行流程解读
任务1: [host1, host2, host3, ... hostN] ← 同时执行
等待任务1所有主机成功,然后开始执行任务任务2
任务2: [host1, host2, host3, ... hostN] ← 同时执行
问题:如果太多主机并发,等待的时间会过长,所以就有了`--limit <主机/组>`
# 仅在host1和host2上执行Playbook(减少并发数,快速验证)
ansible-playbook task.yml -i /hosts --limit "host1,host2"
# 仅在host1和host2上执行Playbook(减少并发数,快速验证)
ansible-playbook task.yml -i /hosts --limit "host1,host2"
# 若Inventory中有子组,可直接限制组(此处仍用web_servers的子集示例)
# 先在Inventory中添加子组:
# [web_servers_subset]
# host1
# host3
# 然后执行:
ansible-playbook task.yml -i /hosts --limit web_servers_subset
#作用:检查 YAML 语法、Playbook 结构(如字段缺失、缩进错误),不执行任务,输出报错位置和原因
ansible-playbook -C -v playbook.yml
无 Role的playbook
# site-simple.yml
---
- hosts: webservers
vars:
pkg_nginx: nginx
nginx_port: 80
handlers:
- name: 重启Nginx
service:
name: nginx
state: restarted
tasks:
- name: 安装{{ pkg_nginx }}
yum:
name: "{{ pkg_nginx }}"
state: present
- name: 复制配置文件
copy:
src: ./nginx.conf
dest: /etc/nginx/nginx.conf
notify: 重启Nginx
- name: 启动Nginx
service:
name: nginx
state: started
含Role的playbook:
/ansible/
├── roles/
│ ├── web/ # web角色目录(变量仅属于web角色)
│ │ ├── vars/
│ │ │ └── main.yml # web角色的变量文件(Ansible自动识别)
│ │ └── tasks/
│ │ └── main.yml # web角色的任务文件
│ └── security/ # security角色目录(变量仅属于security角色)
│ ├── vars/
│ │ └── main.yml # security角色的变量文件(Ansible自动识别)
│ └── tasks/
│ └── main.yml # security角色的任务文件
└── site.yml # 主Playbook
mkdir -p /ansible/roles/web/vars
mkdir -p /ansible/roles/web/tasks
mkdir -p /ansible/roles/security/vars
mkdir -p /ansible/roles/security/tasks
mkdir -p /ansible/roles/security/{vars,tasks}
1. 清单文件:
# 仅定义webservers主机组(学生替换为自己的受控节点IP)
[newserver]
192.168.223.151
192.168.223.152
2. web 角色 - 变量:roles/web/vars/main.yml
# web角色仅保留核心变量(减少记忆成本)
nginx_pkg: nginx # Nginx软件包名
nginx_service: nginx # Nginx服务名
3.web 角色 - 任务:roles/web/tasks/main.yml
# web角色核心任务:安装+启动Nginx(无复杂配置,聚焦角色逻辑)
---
- name: 安装Nginx软件包
ansible.builtin.yum:
name: "{{ nginx_pkg }}"
state: present # 确保安装(幂等,多次执行不重复安装)
- name: 启动并开机自启Nginx服务
ansible.builtin.service:
name: "{{ nginx_service }}"
state: started
enabled: true
4. security 角色 - 任务:roles/security/tasks/main.yml
# security角色核心任务:防火墙放行80端口 + SELinux宽松模式
- name: 放行防火墙80端口(http服务)
ansible.builtin.firewalld:
service: http # 直接用预设的http服务(对应80端口,比写port更简单)
permanent: true # 永久生效
immediate: true # 立即生效(无需重启防火墙)
state: enabled
- name: 设置SELinux为宽松模式(临时生效,避免重启)
ansible.builtin.selinux:
policy: targeted
state: permissive # 宽松模式:不阻止操作,仅记录日志
5. 主 Playbook:site.yml
# 主Playbook仅做2件事:指定目标主机 + 引入两个角色
---
- hosts: webservers # 关联清单中的webservers主机组
remote_user: root # 远程执行用户
gather_facts: true # 自动收集Facts(firewalld/selinux模块依赖)
# 按顺序引入角色:先做安全配置,再部署Web(逻辑合理)
# 提前写好仓库挂载好,或者增加一个cnagku角色
roles:
- security # 引入security角色(执行防火墙+SELinux配置)
- web # 引入web角色(执行Nginx部署)
四、执行与验证
ansible-playbook -i /hosts /ansible/site.yml
扩展:把某个角色需要的变量单独放到文件里,那ansible-playboot如何识别哪个变量文件是哪个角色的?

当在 Playbook 的roles中直接以 "字符串形式" 引用角色(如- web)时,Ansible 会默认将该角色名称作为目录名 ,去roles/目录下查找同名目录(如roles/web/),这是最规范、最常用的方式,无需额外配置。
案例1:远程批量安装 nginx,修改端口号,并使配置生效
注意:
-
每个 Task 只能调用 1 个模块
-
description参数用于描述 yum 源的用途(比如 "CentOS BaseOS Repository"),是yum_repository模块的强制参数,没有它 Ansible 会判定参数不完整,直接终止任务并抛出Parameter 'description' is required错误。 -
yum_repository指定的file名称(比如x)和/etc/yum.repos.d/目录下已存在的.repo文件(如x.repo)重名时,Ansible 的处理逻辑非常友好 ------不会直接覆盖整个文件,而是 "精准修改 / 追加",完全不用担心原有配置丢失,
- name: 给web服务器部署nginx # Play的名字(方便看日志)
hosts: webservers # 这个Play要操作的主机(对应你的Hosts列表)
tasks: # 这个Play里的Task列表- name: 挂载光盘
mount:
path: /mnt
src: /dev/sr0
state: mounted
fstype: iso9660 # 光盘专属文件系统类型(必须加) - name: 配置仓库base
yum_repository:
file: x
name: base
description: base
state: present
enabled: yes
gpgcheck: no
baseurl: /mnt/BaseOS - name: 配置仓库app
yum_repository:
file: x
name: app
description: app
state: present
enabled: yes
gpgcheck: no
baseurl: /mnt/AppStream - name: 安装nginx
yum:
name: nginx
state: present - name: 修改nginx配置文件
lineinfile:
path: /etc/nginx/nginx.conf # 主流端口配置文件路径
regexp: "^\s*listen\s+\d+;?$" # 匹配以listen开头、后跟数字的行(如listen 80;)
line: " listen 8080;" # 替换为监听8080端口(缩进和原文件保持一致)
backup: yes # 修改前自动备份原文件(建议开启,方便回滚)
state: present - name: 修改Nginx默认欢迎页面为welcome test
copy:
content: "welcome test" # 页面要显示的内容
dest: /usr/share/nginx/html/index.html # Nginx默认首页路径
backup: yes # 备份原首页文件(生成index.html.bak.xxxxxx)
mode: '0644' # 设置文件权限,确保Nginx能读取(必需) - name: 启动nginx
service:
name: nginx
state: started
- name: 挂载光盘
- name: 给web服务器部署nginx # Play的名字(方便看日志)
实验2:纯剧本方式:部署 LNMP+Discuz(扁平任务结构)
[root@rhce ~]# cat discuz.yml
---
- name: 纯剧本部署 LNMP + Discuz 论坛(适配文件名不固定)
hosts: 192.168.223.151
gather_facts: no
become: yes
vars:
# 基础配置
mount_src: /dev/sr0
mount_path: /mnt
mount_fstype: iso9660
web_root: /var/www/html/discuz
nginx_conf_path: /etc/nginx/conf.d/discuz.conf
discuz_url: "https://gitee.com/Discuz/DiscuzX/attach_files/2335009/download"
# 数据库配置
db_root_pwd: "Redhat123!" # 学生可自行修改目标密码
db_user: "root"
db_host: "localhost"
tasks:
###########################################################################
# 步骤 0:挂载光盘(原生幂等)
###########################################################################
- name: 挂载光盘到/mnt目录
mount:
src: "{{ mount_src }}"
path: "{{ mount_path }}"
fstype: "{{ mount_fstype }}"
state: mounted
###########################################################################
# 步骤 1:安装基础软件(原生幂等)
###########################################################################
- name: 安装LNMP及基础依赖包
package:
name:
- unzip
- nginx
- php
- php-fpm
- php-mysqlnd
- mariadb-server
- python3-PyMySQL
state: present
###########################################################################
# 步骤 2:启动基础服务(原生幂等)
###########################################################################
- name: 启动并开机自启Nginx服务
systemd:
name: nginx
state: started
enabled: yes
- name: 启动并开机自启PHP-FPM服务
systemd:
name: php-fpm
state: started
enabled: yes
- name: 启动并开机自启MariaDB服务
systemd:
name: mariadb
state: started
enabled: yes
###########################################################################
# 步骤 3:创建网站根目录(原生幂等)
###########################################################################
- name: 创建Discuz网站根目录并设置权限
file:
path: "{{ web_root }}"
state: directory
owner: nginx
group: nginx
mode: 0755
###########################################################################
# 步骤 4:下载+解压Discuz(核心适配:文件名不固定,全靠find动态处理)
###########################################################################
# 第一步:检查是否已有任意Discuz开头的压缩包(不限制具体名称)
- name: 检查{{ web_root }}是否有Discuz开头的压缩包
find:
path: "{{ web_root }}"
patterns: "Discuz*.zip" # 匹配所有Discuz开头的zip包
file_type: file
register: existing_discuz_zip
# 第二步:仅当无任何Discuz压缩包时,才下载(避免重复下载/401)
- name: 下载Discuz压缩包(文件名不固定,下载到目录即可)
get_url:
url: "{{ discuz_url }}"
dest: "{{ web_root }}/" # 指向目录,让文件按URL默认名保存
validate_certs: no
timeout: 60
when: existing_discuz_zip.files | length == 0 # 无任何Discuz包时才下载
# 第三步:再次find所有Discuz压缩包(适配下载后的随机名称)
- name: 动态查找{{ web_root }}下所有Discuz开头的压缩包
find:
path: "{{ web_root }}"
patterns: "Discuz*.zip"
file_type: file
register: discuz_zip_files
# 第四步:检查是否已解压(通过Discuz核心目录判断,与文件名无关)
- name: 检查Discuz是否已解压(通过upload目录判断)
stat:
path: "{{ web_root }}/upload"
register: discuz_unzip_stat
# 第五步:解压(仅找到压缩包且未解压时执行,适配任意Discuz开头的包)
- name: 解压动态匹配到的Discuz压缩包
unarchive:
src: "{{ discuz_zip_files.files[0].path | default('') }}" # 取第一个匹配的包,兜底赋空
dest: "{{ web_root }}"
remote_src: yes
when:
- discuz_zip_files.files | length > 0 # 至少找到1个Discuz包
- not discuz_unzip_stat.stat.exists # 未解压过
ignore_errors: yes # 兜底:压缩包损坏/格式问题不中断流程
#当 {{ web_root }}/upload 目录不存在时:discuz_unzip_stat.stat.exists 为 False,经 not 取反后为 True,条件满足,解压任务会执行;
#当 {{ web_root }}/upload 目录已存在时:discuz_unzip_stat.stat.exists 为 True,经 not 取反后为 False,条件不满足,解压任务会跳过。
###########################################################################
# 步骤 5:配置Nginx虚拟主机(原生幂等)
###########################################################################
- name: 写入Discuz的Nginx配置文件
copy:
content: |
server {
listen 80;
root {{ web_root }}/upload;
include /etc/nginx/default.d/php.conf;
}
dest: "{{ nginx_conf_path }}"
backup: yes
register: nginx_conf
changed_when: nginx_conf.changed
- name: 重新加载Nginx配置(仅配置变更时执行)
systemd:
name: nginx
state: reloaded
when: nginx_conf.changed
###########################################################################
# 步骤 6:配置防火墙和SELinux(原生幂等)
###########################################################################
- name: 永久开放80端口
firewalld:
port: 80/tcp
permanent: yes
state: enabled
immediate: yes
- name: 临时关闭SELinux
command: setenforce 0
ignore_errors: yes
changed_when: no
###########################################################################
# 步骤 7:修改Discuz权限(幂等:仅目录存在时执行)
###########################################################################
- name: 检查Discuz upload目录是否存在
stat:
path: "{{ web_root }}/upload"
register: discuz_upload_stat
- name: 修改Discuz data目录权限为777
file:
path: "{{ web_root }}/upload/data"
mode: 0777
recurse: yes
state: directory
when: discuz_upload_stat.stat.exists
- name: 修改Discuz config目录权限为777
file:
path: "{{ web_root }}/upload/config"
mode: 0777
recurse: yes
state: directory
when: discuz_upload_stat.stat.exists
- name: 批量修改uc_client/uc_server目录权限为777
file:
path: "{{ web_root }}/upload/{{ item }}"
mode: 0777
recurse: yes
state: directory
loop:
- uc_client
- uc_server
when: discuz_upload_stat.stat.exists
###########################################################################
# 步骤 8:初始化数据库(原生幂等)
###########################################################################
# 步骤1:判断root是否有密码(核心:用简单命令测试)
- name: 测试root无密码能否登录
ansible.builtin.shell: 'mysql -u{{ db_user }} -S /var/lib/mysql/mysql.sock -e "show databases;" 2>/dev/null'
register: no_pwd_login
ignore_errors: yes # 有密码时登录失败,忽略错误不中断
changed_when: no # 仅判断状态,不标记"变更"
# 步骤2:仅无密码时,设置root密码(幂等:设过就不重复设)
- name: 初始化root密码(仅第一次执行)
community.mysql.mysql_user:
name: "{{ db_user }}"
host: "localhost" # 简化:只设localhost的密码,去掉冗余host
password: "{{ db_root_pwd }}"
# check_implicit_admin: yes # 无密码时允许登录的关键参数
login_unix_socket: /var/lib/mysql/mysql.sock
login_user: "{{ db_user }}"
login_host: "{{ db_host }}"
login_password: "" # 无密码时填空
state: present # 幂等核心:密码已设则跳过
when: no_pwd_login.rc == 0 # 只有无密码登录成功时才执行
# 步骤3:创建测试库(幂等:已存在则跳过,适配有/无密码登录)
- name: 创建luntan数据库(多次执行不报错)
community.mysql.mysql_db:
name: luntan
state: present # 幂等:确保库存在,不存在才创建
encoding: utf8mb4
# 动态适配密码:有密码用db_root_pwd,无密码填空
login_unix_socket: /var/lib/mysql/mysql.sock
login_user: "{{ db_user }}"
login_host: "{{ db_host }}"
login_password: "{{ db_root_pwd }}"
执行剧本 & 验证
-
执行命令:`ansible-playbook discuz-lnmp-playbook.yml`
-
验证:浏览器访问 `http://受控节点IP`,进入 Discuz 安装向导,输入剧本中定义的数据库信息(库名 `discuz_db`、用户 `discuz_user`、密码 `Discuz@123`),完成安装。
问题:
解决:
用户现在遇到的 Ansible 执行修改 MariaDB root 密码任务失败的问题,首先要解释错误原因,然后给解决方案。
首先,错误原因:Ansible 的 mysql_user/mysql_db 模块依赖 Python 的 MySQL 数据库驱动包,但受控节点上没有安装对应的包,或者 Python 解释器版本不匹配导致找不到驱动。
然后,分步骤解决:
- 先解释错误核心:
错误提示明确说需要 MySQL Python 模块,Python2.7 需要 PyMySQL/MySQL-python,Python3 需要 mysqlclient/PyMySQL;
受控节点要么没装这些包,要么 Ansible 用的 Python 解释器和安装的驱动版本不匹配。
- 解决方案步骤:
步骤 1:在受控节点安装 Python MySQL 驱动(区分 CentOS 版本,yum/dnf);
步骤 2:(可选)指定 Ansible 的 Python 解释器(避免版本不匹配);
步骤 3:重新执行任务。
