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: started2. 多 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: present3. 带变量 / 处理器的嵌套结构
以下以「部署 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.1522. 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: true4. 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实验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 }}"执行剧本 & 验证
1. 执行命令:`ansible-playbook discuz-lnmp-playbook.yml`
2. 验证:浏览器访问 `http://受控节点IP`,进入 Discuz 安装向导,输入剧本中定义的数据库信息(库名 `discuz_db`、用户 `discuz_user`、密码 `Discuz@123`),完成安装。
问题:
解决:
用户现在遇到的 Ansible 执行修改 MariaDB root 密码任务失败的问题,首先要解释错误原因,然后给解决方案。
首先,错误原因:Ansible 的 mysql_user/mysql_db 模块依赖 Python 的 MySQL 数据库驱动包,但受控节点上没有安装对应的包,或者 Python 解释器版本不匹配导致找不到驱动。
然后,分步骤解决:
1. 先解释错误核心:
- 错误提示明确说需要 MySQL Python 模块,Python2.7 需要 PyMySQL/MySQL-python,Python3 需要 mysqlclient/PyMySQL;
- 受控节点要么没装这些包,要么 Ansible 用的 Python 解释器和安装的驱动版本不匹配。2. 解决方案步骤:
步骤 1:在受控节点安装 Python MySQL 驱动(区分 CentOS 版本,yum/dnf);
步骤 2:(可选)指定 Ansible 的 Python 解释器(避免版本不匹配);
步骤 3:重新执行任务。