Ansible Playbook高效自动化实战指南

Ansible Playbook

Ansible Playbook(剧本)是 Ansible 核心配置文件,采用 YAML 格式编写,用于定义一系列有序的自动化任务集合,描述 "要在哪些远程主机上执行哪些操作"。

YAML 基本规则

YAML 是一种易读的序列化格式,是 Playbook 的核心语法,核心规则如下:

  1. 缩进规则:仅允许空格缩进(禁止 Tab),通常 2 个半角空格为 1 级,缩进层级代表逻辑层级;

    全角空格是中文输入法下的(肉眼看和半角空格一样,但解析器不认),必须替换为半角空格(英文输入法下的空格)。

  2. 大小写敏感 :变量名、模块名、键名(如 hosts/Hosts)均区分大小写;

  3. 注释规则 :单行注释用 ## 后内容会被忽略),无原生多行注释(可每行加 #);

  4. 字符串规则 :字符串可加引号(单 / 双)或不加,含特殊字符(:、$、空格)时必须加引号(如 name: "web-server:8080");

  5. 核心标识:

    • 元素以 - 开头(短横线 + 空格),如 - nginx

    • 键值对用 key: value(冒号后必须加空格),如 name: nginx

  6. 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

  1. 多 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 指定远程执行任务的用户(如 rootansible
--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

*实验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`),完成安装。 > 问题: > > ![image-20251204111441330](https://i-blog.csdnimg.cn/img_convert/21f41a413d6e1bd9ae01fa8b49e16971.png) > > 解决: > > 用户现在遇到的 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:重新执行任务。

相关推荐
数据安全科普王2 小时前
从 HTTP/1.1 到 HTTP/3:协议演进如何改变 Web 性能?
网络·其他
舰长1152 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络
学***54232 小时前
如何轻松避免网络负载过大
开发语言·网络·php
weixin_395448912 小时前
main.c_cursor_0129
前端·网络·算法
CS创新实验室2 小时前
《计算机网络》深入学:路由算法与路径选择
网络·计算机网络·算法
骆驼爱记录2 小时前
Excel邮件合并嵌入图片技巧
自动化·word·excel·wps·新人首发
gantiexia3 小时前
影刀结合飞书,实现报表数据全自动化推送
自动化·飞书·rpa
zhengfei6114 小时前
AutoPentestX – Linux 自动化渗透测试和漏洞报告工具
linux·运维·自动化
wWYy.4 小时前
C++-集群聊天室(2):muduo网络库
网络·c++