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.files0.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:重新执行任务。

相关推荐
火山上的企鹅5 小时前
Codex实战:APP远程升级服务搭建(三)后台管理页面(APK 上传、版本管理、多应用页签)
服务器·网络·数据库·oracle·qgc
caimouse6 小时前
Reactos 第 9 章 设备驱动 — 9.5 一组PnP设备驱动模块的实例
网络·windows
袁小皮皮不皮6 小时前
3.HCIP OSPF补充知识(优化版)
服务器·网络·数据库·网络协议·智能路由器
虾壳云官方6 小时前
OpenClaw 2.7.9 Windows 一键部署教程:零基础也能搭建 AI 自动化助手
运维·人工智能·windows·自动化·openclaw·openclaw一键部署
志栋智能7 小时前
超自动化巡检:知识沉淀与团队协作的新载体
大数据·运维·网络·数据库·人工智能·自动化
酣大智7 小时前
策略路由PBR--企业双出口实验
网络·智能路由器·策略路由·pbr
袁小皮皮不皮7 小时前
1.HCIP BFD 学习笔记(优化版)
服务器·网络·笔记·网络协议·学习·智能路由器·ip
梁辰兴8 小时前
计算机网络基础:数据加密模型
网络·计算机网络·计算机·数据加密·计算机网络基础·梁辰兴·数据加密模型
fofantasy8 小时前
NSK LH12AN 微型导轨技术手册
运维·网络·数据库·经验分享·规格说明书
网络系统管理8 小时前
第八届江苏技能状元大赛“信息通信网络运行管理”项目技术文件
网络