5.1 Playbook 概述
5.1.1 什么是 Playbook
Playbook 是 Ansible 的核心功能,它是一个 YAML 格式的文件,用于定义一系列有序的任务,这些任务将在指定的主机上执行。Playbook 提供了一种声明式的方式来描述系统的期望状态。
Playbook 的特点: - 使用 YAML 语法,易于阅读和编写 - 支持复杂的逻辑控制(条件、循环、错误处理) - 可重复执行,具有幂等性 - 支持变量、模板和角色 - 可以组织成复杂的自动化工作流
5.1.2 Playbook vs Ad-hoc 命令
特性 | Ad-hoc 命令 | Playbook |
---|---|---|
复杂度 | 简单任务 | 复杂工作流 |
可重用性 | 低 | 高 |
版本控制 | 困难 | 容易 |
文档化 | 差 | 好 |
错误处理 | 基本 | 高级 |
条件逻辑 | 有限 | 丰富 |
5.2 YAML 基础
5.2.1 YAML 语法规则
# YAML 基础语法示例
---
# 文档开始标记
# 注释以 # 开头
# 键值对
key: value
name: "John Doe"
age: 30
# 字符串(可以不用引号)
simple_string: Hello World
quoted_string: "Hello World"
multiline_string: |
这是一个
多行字符串
保持换行符
folded_string: >
这是一个
折叠字符串
会合并为一行
# 列表
fruits:
- apple
- banana
- orange
# 或者内联格式
colors: [red, green, blue]
# 字典
person:
name: John
age: 30
address:
street: 123 Main St
city: New York
zip: 10001
# 或者内联格式
coordinates: {x: 10, y: 20}
# 布尔值
enabled: true
disabled: false
yes_value: yes
no_value: no
# 空值
empty_value: null
# 或者
empty_value: ~
# 数字
integer: 42
float: 3.14
scientific: 1.2e+3
# 多文档(用 --- 分隔)
---
document: 1
---
document: 2
5.2.2 YAML 常见错误
# 错误示例和修正
# 错误:缩进不一致
# tasks:
# - name: task 1
# command: echo "hello"
# - name: task 2 # 缩进错误
# command: echo "world"
# 正确:
tasks:
- name: task 1
command: echo "hello"
- name: task 2
command: echo "world"
# 错误:混用空格和制表符
# 应该只使用空格,不使用制表符
# 错误:特殊字符未转义
# message: John's car # 单引号会导致解析错误
# 正确:
message: "John's car"
# 或者
message: 'John\'s car'
# 错误:列表项缩进错误
# items:
# - item1 # 应该缩进
# - item2
# 正确:
items:
- item1
- item2
5.3 Playbook 基本结构
5.3.1 最简单的 Playbook
# simple-playbook.yml
---
- name: 我的第一个 Playbook
hosts: localhost
tasks:
- name: 显示消息
debug:
msg: "Hello, Ansible!"
5.3.2 完整的 Playbook 结构
# complete-playbook.yml
---
# Play 1
- name: Web 服务器配置
hosts: webservers
become: yes
gather_facts: yes
connection: ssh
remote_user: ansible
# Play 级别变量
vars:
http_port: 80
max_clients: 200
app_name: myapp
# 变量文件
vars_files:
- vars/common.yml
- "vars/{{ environment }}.yml"
# 变量提示
vars_prompt:
- name: db_password
prompt: "请输入数据库密码"
private: yes
# 环境变量
environment:
PATH: "{{ ansible_env.PATH }}:/usr/local/bin"
JAVA_HOME: /usr/lib/jvm/java-8-openjdk
# 前置任务
pre_tasks:
- name: 更新包缓存
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: 检查系统要求
assert:
that:
- ansible_memtotal_mb >= 1024
- ansible_processor_vcpus >= 1
fail_msg: "系统不满足最低要求"
# 角色
roles:
- common
- nginx
- { role: mysql, mysql_root_password: "{{ db_password }}" }
# 主要任务
tasks:
- name: 安装应用程序包
package:
name:
- "{{ app_name }}"
- python3-pip
state: present
notify: restart application
- name: 配置应用程序
template:
src: app.conf.j2
dest: "/etc/{{ app_name }}/app.conf"
owner: root
group: root
mode: '0644'
notify: restart application
- name: 启动应用程序服务
service:
name: "{{ app_name }}"
state: started
enabled: yes
# 处理器
handlers:
- name: restart application
service:
name: "{{ app_name }}"
state: restarted
- name: reload nginx
service:
name: nginx
state: reloaded
# 后置任务
post_tasks:
- name: 验证应用程序状态
uri:
url: "http://{{ inventory_hostname }}:{{ http_port }}/health"
status_code: 200
retries: 3
delay: 10
- name: 发送通知
mail:
to: admin@example.com
subject: "部署完成"
body: "{{ app_name }} 已在 {{ inventory_hostname }} 上成功部署"
# Play 2
- name: 数据库服务器配置
hosts: databases
become: yes
vars:
mysql_root_password: "{{ vault_mysql_root_password }}"
tasks:
- name: 安装 MySQL
package:
name: mysql-server
state: present
- name: 启动 MySQL
service:
name: mysql
state: started
enabled: yes
5.4 任务(Tasks)
5.4.1 任务基本语法
# tasks-basic.yml
---
- name: 任务基础示例
hosts: all
tasks:
# 最简单的任务
- name: 显示消息
debug:
msg: "这是一个简单任务"
# 带参数的任务
- name: 创建目录
file:
path: /opt/myapp
state: directory
mode: '0755'
owner: root
group: root
# 使用变量的任务
- name: 安装软件包
package:
name: "{{ item }}"
state: present
loop:
- nginx
- vim
- git
# 条件任务
- name: 仅在 Ubuntu 上执行
apt:
name: ubuntu-specific-package
state: present
when: ansible_distribution == "Ubuntu"
# 注册变量
- name: 检查服务状态
command: systemctl is-active nginx
register: nginx_status
failed_when: false
changed_when: false
# 使用注册变量
- name: 显示服务状态
debug:
msg: "Nginx 状态: {{ nginx_status.stdout }}"
# 委托任务
- name: 在控制节点执行
command: echo "在控制节点执行"
delegate_to: localhost
# 本地操作
- name: 本地文件操作
local_action:
module: file
path: /tmp/local-file
state: touch
# 运行一次
- name: 只运行一次的任务
debug:
msg: "这个任务只在第一个主机上运行"
run_once: true
5.4.2 任务控制
# task-control.yml
---
- name: 任务控制示例
hosts: all
tasks:
# 忽略错误
- name: 可能失败的任务
command: /bin/false
ignore_errors: yes
# 自定义失败条件
- name: 检查磁盘空间
shell: df -h / | tail -1 | awk '{print $5}' | sed 's/%//'
register: disk_usage
failed_when: disk_usage.stdout|int > 90
# 自定义变更条件
- name: 检查配置文件
shell: grep "server_name" /etc/nginx/nginx.conf
register: config_check
changed_when: false # 永远不报告为变更
# 重试机制
- name: 下载文件(带重试)
get_url:
url: https://example.com/file.tar.gz
dest: /tmp/file.tar.gz
register: download_result
until: download_result is succeeded
retries: 3
delay: 5
# 标签
- name: 安装基础包
package:
name: "{{ item }}"
state: present
loop:
- curl
- wget
- vim
tags:
- packages
- basic
# 跳过任务
- name: 开发环境专用任务
debug:
msg: "这是开发环境任务"
when: environment == "development"
tags:
- development
- never # 默认跳过,除非明确指定
5.4.3 任务包含和导入
# main-playbook.yml
---
- name: 主 Playbook
hosts: all
tasks:
# 包含任务文件(动态)
- name: 包含通用任务
include_tasks: tasks/common.yml
# 条件包含
- name: 包含 Ubuntu 特定任务
include_tasks: tasks/ubuntu.yml
when: ansible_distribution == "Ubuntu"
# 循环包含
- name: 为每个应用包含任务
include_tasks: tasks/deploy-app.yml
vars:
app_name: "{{ item }}"
loop:
- web-app
- api-app
- worker-app
# 导入任务文件(静态)
- name: 导入安全配置任务
import_tasks: tasks/security.yml
# 包含 Playbook
- name: 包含数据库配置
include: playbooks/database.yml
vars:
db_name: myapp
db_user: app_user
# tasks/common.yml
---
- name: 更新包缓存
package:
update_cache: yes
when: ansible_pkg_mgr in ['apt', 'yum', 'dnf']
- name: 安装基础工具
package:
name:
- curl
- wget
- vim
- git
state: present
- name: 创建应用目录
file:
path: /opt/apps
state: directory
mode: '0755'
# tasks/ubuntu.yml
---
- name: 添加 Ubuntu 特定 PPA
apt_repository:
repo: ppa:nginx/stable
state: present
- name: 安装 Ubuntu 特定包
apt:
name:
- software-properties-common
- apt-transport-https
state: present
# tasks/deploy-app.yml
---
- name: "创建 {{ app_name }} 目录"
file:
path: "/opt/apps/{{ app_name }}"
state: directory
mode: '0755'
- name: "下载 {{ app_name }}"
get_url:
url: "https://releases.example.com/{{ app_name }}-latest.tar.gz"
dest: "/tmp/{{ app_name }}-latest.tar.gz"
- name: "解压 {{ app_name }}"
unarchive:
src: "/tmp/{{ app_name }}-latest.tar.gz"
dest: "/opt/apps/{{ app_name }}"
remote_src: yes
5.5 变量使用
5.5.1 变量定义和引用
# variables.yml
---
- name: 变量使用示例
hosts: all
vars:
# 简单变量
app_name: myapp
app_version: "1.2.3"
debug_mode: true
# 字典变量
database:
host: localhost
port: 3306
name: "{{ app_name }}"
user: app_user
# 列表变量
required_packages:
- nginx
- python3
- python3-pip
# 复杂数据结构
environments:
development:
debug: true
database_host: dev-db.example.com
production:
debug: false
database_host: prod-db.example.com
tasks:
# 基本变量引用
- name: "显示应用名称"
debug:
msg: "应用名称: {{ app_name }}"
# 字典变量引用
- name: 显示数据库配置
debug:
msg: "数据库: {{ database.host }}:{{ database.port }}/{{ database.name }}"
# 列表变量引用
- name: 安装必需包
package:
name: "{{ required_packages }}"
state: present
# 复杂变量引用
- name: 显示环境配置
debug:
msg: "调试模式: {{ environments[environment].debug }}"
vars:
environment: development
# 变量默认值
- name: 使用默认值
debug:
msg: "端口: {{ custom_port | default(8080) }}"
# 条件变量
- name: 设置条件变量
set_fact:
service_state: "{{ 'started' if debug_mode else 'stopped' }}"
# 注册变量
- name: 获取系统信息
command: uname -a
register: system_info
- name: 显示系统信息
debug:
msg: "系统: {{ system_info.stdout }}"
# 事实变量
- name: 显示主机信息
debug:
msg: |
主机名: {{ ansible_hostname }}
IP 地址: {{ ansible_default_ipv4.address }}
操作系统: {{ ansible_distribution }} {{ ansible_distribution_version }}
内存: {{ ansible_memtotal_mb }} MB
5.5.2 变量文件和加密
# vars/common.yml
---
app_name: myapp
app_version: "1.2.3"
app_port: 8080
# 数据库配置
database:
host: "{{ db_host | default('localhost') }}"
port: 3306
name: "{{ app_name }}"
# 服务配置
services:
- name: nginx
port: 80
enabled: true
- name: redis
port: 6379
enabled: true
# vars/production.yml
---
environment: production
debug_mode: false
db_host: prod-db.example.com
redis_host: prod-redis.example.com
# 生产环境特定配置
max_connections: 1000
worker_processes: 4
# vars/development.yml
---
environment: development
debug_mode: true
db_host: dev-db.example.com
redis_host: dev-redis.example.com
# 开发环境特定配置
max_connections: 100
worker_processes: 1
# 创建加密变量文件
ansible-vault create vars/secrets.yml
# 编辑加密文件
ansible-vault edit vars/secrets.yml
# 查看加密文件
ansible-vault view vars/secrets.yml
# 加密现有文件
ansible-vault encrypt vars/passwords.yml
# 解密文件
ansible-vault decrypt vars/passwords.yml
# vars/secrets.yml (加密前)
---
db_password: "super_secret_password"
api_key: "abc123def456"
ssl_private_key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
-----END PRIVATE KEY-----
# 使用加密变量的 Playbook
---
- name: 使用加密变量
hosts: databases
vars_files:
- vars/common.yml
- vars/secrets.yml
tasks:
- name: 配置数据库
mysql_user:
name: "{{ database.user }}"
password: "{{ db_password }}"
priv: "{{ database.name }}.*:ALL"
state: present
# 运行使用加密变量的 Playbook
ansible-playbook playbook.yml --ask-vault-pass
# 或使用密码文件
echo "vault_password" > .vault_pass
ansible-playbook playbook.yml --vault-password-file .vault_pass
# 或使用环境变量
export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
ansible-playbook playbook.yml
5.6 条件控制
5.6.1 基本条件语句
# conditionals.yml
---
- name: 条件控制示例
hosts: all
vars:
environment: production
install_debug_tools: false
required_memory_mb: 2048
tasks:
# 简单条件
- name: 仅在生产环境执行
debug:
msg: "这是生产环境"
when: environment == "production"
# 多条件(AND)
- name: 生产环境且内存充足时执行
debug:
msg: "生产环境,内存充足"
when:
- environment == "production"
- ansible_memtotal_mb >= required_memory_mb
# 多条件(OR)
- name: 开发或测试环境执行
debug:
msg: "非生产环境"
when: environment == "development" or environment == "testing"
# 复杂条件
- name: 复杂条件判断
debug:
msg: "满足复杂条件"
when: >
(environment == "production" and ansible_memtotal_mb >= 4096) or
(environment == "development" and install_debug_tools)
# 基于事实的条件
- name: 仅在 Ubuntu 18.04+ 执行
debug:
msg: "Ubuntu 18.04 或更高版本"
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version is version('18.04', '>=')
# 基于变量存在性的条件
- name: 变量已定义时执行
debug:
msg: "自定义端口: {{ custom_port }}"
when: custom_port is defined
# 基于变量值的条件
- name: 变量不为空时执行
debug:
msg: "应用名称: {{ app_name }}"
when: app_name is defined and app_name != ""
# 基于列表成员的条件
- name: 主机在 Web 服务器组中
debug:
msg: "这是 Web 服务器"
when: "'webservers' in group_names"
# 基于注册变量的条件
- name: 检查服务状态
command: systemctl is-active nginx
register: nginx_status
failed_when: false
changed_when: false
- name: 服务未运行时启动
service:
name: nginx
state: started
when: nginx_status.stdout != "active"
# 基于文件存在性的条件
- name: 检查配置文件
stat:
path: /etc/myapp/config.yml
register: config_file
- name: 配置文件不存在时创建
template:
src: config.yml.j2
dest: /etc/myapp/config.yml
when: not config_file.stat.exists
5.6.2 条件导入和包含
# conditional-includes.yml
---
- name: 条件导入和包含
hosts: all
tasks:
# 条件包含任务
- name: 包含 Ubuntu 特定任务
include_tasks: tasks/ubuntu.yml
when: ansible_distribution == "Ubuntu"
- name: 包含 CentOS 特定任务
include_tasks: tasks/centos.yml
when: ansible_distribution == "CentOS"
# 条件包含变量
- name: 包含环境特定变量
include_vars: "vars/{{ environment }}.yml"
when: environment is defined
# 条件导入 Playbook
- name: 导入数据库配置
import_playbook: database.yml
when: "'databases' in group_names"
# 基于条件的角色应用
- name: 应用 Web 服务器角色
include_role:
name: webserver
when: "'webservers' in group_names"
- name: 应用数据库角色
include_role:
name: database
vars:
mysql_root_password: "{{ vault_mysql_password }}"
when: "'databases' in group_names"
5.7 循环控制
5.7.1 基本循环
# loops.yml
---
- name: 循环控制示例
hosts: all
vars:
packages:
- nginx
- vim
- git
- curl
users:
- name: alice
groups: [sudo, developers]
shell: /bin/bash
- name: bob
groups: [operators]
shell: /bin/zsh
- name: charlie
groups: [developers]
shell: /bin/bash
databases:
- name: app_db
user: app_user
password: app_pass
- name: log_db
user: log_user
password: log_pass
tasks:
# 简单列表循环
- name: 安装软件包
package:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
# 字典循环
- name: 创建用户
user:
name: "{{ item.name }}"
groups: "{{ item.groups | join(',') }}"
shell: "{{ item.shell }}"
state: present
loop: "{{ users }}"
# 数字范围循环
- name: 创建测试文件
file:
path: "/tmp/test{{ item }}.txt"
state: touch
loop: "{{ range(1, 6) | list }}" # 1 到 5
# 字符串列表循环
- name: 创建目录
file:
path: "/opt/{{ item }}"
state: directory
loop:
- app1
- app2
- app3
# 嵌套循环
- name: 创建用户和数据库组合
debug:
msg: "用户 {{ item.0.name }} 访问数据库 {{ item.1.name }}"
loop: "{{ users | product(databases) | list }}"
# 条件循环
- name: 仅为开发者创建 SSH 密钥
user:
name: "{{ item.name }}"
generate_ssh_key: yes
ssh_key_bits: 2048
loop: "{{ users }}"
when: "'developers' in item.groups"
# 循环注册变量
- name: 检查多个服务状态
command: "systemctl is-active {{ item }}"
register: service_status
failed_when: false
changed_when: false
loop:
- nginx
- mysql
- redis
- name: 显示服务状态
debug:
msg: "{{ item.item }}: {{ item.stdout }}"
loop: "{{ service_status.results }}"
5.7.2 高级循环
# advanced-loops.yml
---
- name: 高级循环示例
hosts: all
vars:
server_configs:
web1:
ip: 192.168.1.10
role: webserver
ports: [80, 443]
web2:
ip: 192.168.1.11
role: webserver
ports: [80, 443]
db1:
ip: 192.168.1.20
role: database
ports: [3306]
tasks:
# 字典循环(dict2items)
- name: 配置服务器
debug:
msg: "配置 {{ item.key }}: IP={{ item.value.ip }}, 角色={{ item.value.role }}"
loop: "{{ server_configs | dict2items }}"
# 嵌套字典循环
- name: 配置防火墙端口
debug:
msg: "服务器 {{ item.0.key }} 开放端口 {{ item.1 }}"
loop: "{{ server_configs | dict2items | subelements('value.ports') }}"
# 文件查找循环
- name: 查找日志文件
find:
paths: /var/log
patterns: "*.log"
age: "1d"
register: log_files
- name: 处理日志文件
debug:
msg: "处理日志文件: {{ item.path }}"
loop: "{{ log_files.files }}"
# 循环控制
- name: 循环控制示例
debug:
msg: "处理项目 {{ item }} (索引: {{ ansible_loop.index }})"
loop:
- item1
- item2
- item3
- item4
loop_control:
index_var: my_idx
label: "{{ item }}" # 简化输出
pause: 2 # 每次循环暂停 2 秒
# 并行循环
- name: 并行下载文件
get_url:
url: "https://example.com/{{ item }}"
dest: "/tmp/{{ item }}"
loop:
- file1.tar.gz
- file2.tar.gz
- file3.tar.gz
async: 300
poll: 0
register: download_jobs
- name: 等待下载完成
async_status:
jid: "{{ item.ansible_job_id }}"
register: download_result
until: download_result.finished
retries: 30
delay: 10
loop: "{{ download_jobs.results }}"
5.7.3 循环过滤和转换
# loop-filters.yml
---
- name: 循环过滤和转换
hosts: all
vars:
all_packages:
- name: nginx
category: web
required: true
- name: apache2
category: web
required: false
- name: mysql-server
category: database
required: true
- name: postgresql
category: database
required: false
- name: redis
category: cache
required: true
tasks:
# 过滤循环
- name: 仅安装必需的包
package:
name: "{{ item.name }}"
state: present
loop: "{{ all_packages | selectattr('required', 'equalto', true) | list }}"
# 按类别过滤
- name: 安装 Web 服务器包
package:
name: "{{ item.name }}"
state: present
loop: "{{ all_packages | selectattr('category', 'equalto', 'web') | list }}"
when: "'webservers' in group_names"
# 提取特定属性
- name: 显示包名列表
debug:
msg: "所有包: {{ all_packages | map(attribute='name') | list }}"
# 组合过滤器
- name: 安装必需的数据库包
package:
name: "{{ item }}"
state: present
loop: >
{{
all_packages
| selectattr('category', 'equalto', 'database')
| selectattr('required', 'equalto', true)
| map(attribute='name')
| list
}}
when: "'databases' in group_names"
# 唯一值循环
- name: 获取所有类别
debug:
msg: "包类别: {{ all_packages | map(attribute='category') | unique | list }}"
# 分组循环
- name: 按类别分组显示
debug:
msg: "{{ item.key }}: {{ item.value | map(attribute='name') | list }}"
loop: "{{ all_packages | groupby('category') }}"
5.8 错误处理
5.8.1 基本错误处理
# error-handling.yml
---
- name: 错误处理示例
hosts: all
tasks:
# 忽略错误
- name: 尝试停止可能不存在的服务
service:
name: non-existent-service
state: stopped
ignore_errors: yes
# 自定义失败条件
- name: 检查磁盘空间
shell: df -h / | tail -1 | awk '{print $5}' | sed 's/%//'
register: disk_usage
failed_when: disk_usage.stdout | int > 95
# 永不失败
- name: 收集信息(永不失败)
shell: ps aux | grep nginx
register: nginx_processes
failed_when: false
# 复杂失败条件
- name: 检查服务状态
shell: systemctl status nginx
register: nginx_status
failed_when:
- nginx_status.rc != 0
- "'inactive' in nginx_status.stdout"
# 重试机制
- name: 下载文件(带重试)
get_url:
url: https://example.com/important-file.tar.gz
dest: /tmp/important-file.tar.gz
timeout: 30
register: download_result
until: download_result is succeeded
retries: 5
delay: 10
# 条件重试
- name: 等待服务启动
uri:
url: http://localhost:8080/health
status_code: 200
register: health_check
until: health_check.status == 200
retries: 30
delay: 5
when: start_service | default(true)
5.8.2 块和救援
# block-rescue.yml
---
- name: 块和救援示例
hosts: all
tasks:
# 基本块结构
- name: 应用程序部署
block:
- name: 停止应用程序
service:
name: myapp
state: stopped
- name: 备份当前版本
command: cp -r /opt/myapp /opt/myapp.backup.{{ ansible_date_time.epoch }}
- name: 下载新版本
get_url:
url: "https://releases.example.com/myapp-{{ app_version }}.tar.gz"
dest: /tmp/myapp-{{ app_version }}.tar.gz
- name: 解压新版本
unarchive:
src: /tmp/myapp-{{ app_version }}.tar.gz
dest: /opt/
remote_src: yes
- name: 更新符号链接
file:
src: /opt/myapp-{{ app_version }}
dest: /opt/myapp
state: link
force: yes
- name: 启动应用程序
service:
name: myapp
state: started
- name: 验证部署
uri:
url: http://localhost:8080/health
status_code: 200
retries: 5
delay: 10
rescue:
- name: 记录部署失败
debug:
msg: "部署失败,开始回滚..."
- name: 停止失败的应用程序
service:
name: myapp
state: stopped
ignore_errors: yes
- name: 恢复备份
shell: |
if [ -d /opt/myapp.backup.{{ ansible_date_time.epoch }} ]; then
rm -rf /opt/myapp
mv /opt/myapp.backup.{{ ansible_date_time.epoch }} /opt/myapp
fi
- name: 启动回滚版本
service:
name: myapp
state: started
- name: 发送失败通知
mail:
to: admin@example.com
subject: "部署失败 - {{ inventory_hostname }}"
body: "应用程序部署失败,已回滚到之前版本"
delegate_to: localhost
- name: 标记部署失败
fail:
msg: "部署失败,已回滚"
always:
- name: 清理临时文件
file:
path: /tmp/myapp-{{ app_version }}.tar.gz
state: absent
- name: 记录部署尝试
lineinfile:
path: /var/log/deployments.log
line: "{{ ansible_date_time.iso8601 }} - 尝试部署版本 {{ app_version }}"
create: yes
vars:
app_version: "1.2.3"
# 嵌套块
- name: 数据库维护
block:
- name: 数据库备份
block:
- name: 停止应用程序连接
service:
name: myapp
state: stopped
- name: 创建数据库备份
mysql_db:
name: myapp
state: dump
target: /backup/myapp-{{ ansible_date_time.date }}.sql
rescue:
- name: 备份失败处理
debug:
msg: "数据库备份失败"
- name: 数据库优化
mysql_db:
name: myapp
state: import
target: /scripts/optimize.sql
rescue:
- name: 维护失败处理
debug:
msg: "数据库维护失败"
always:
- name: 重启应用程序
service:
name: myapp
state: started
when: "'databases' in group_names"
5.9 处理器(Handlers)
5.9.1 基本处理器
# handlers.yml
---
- name: 处理器示例
hosts: all
become: yes
tasks:
- name: 安装 Nginx
package:
name: nginx
state: present
notify: start nginx
- name: 配置 Nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
backup: yes
notify:
- validate nginx config
- restart nginx
- name: 配置虚拟主机
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ item.name }}"
loop:
- { name: "example.com", port: 80 }
- { name: "api.example.com", port: 8080 }
notify: reload nginx
- name: 启用虚拟主机
file:
src: "/etc/nginx/sites-available/{{ item.name }}"
dest: "/etc/nginx/sites-enabled/{{ item.name }}"
state: link
loop:
- { name: "example.com" }
- { name: "api.example.com" }
notify: reload nginx
- name: 配置防火墙
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: [80, 443, 8080]
notify: restart ufw
handlers:
- name: start nginx
service:
name: nginx
state: started
enabled: yes
- name: restart nginx
service:
name: nginx
state: restarted
- name: reload nginx
service:
name: nginx
state: reloaded
- name: validate nginx config
command: nginx -t
register: nginx_syntax
failed_when: nginx_syntax.rc != 0
- name: restart ufw
service:
name: ufw
state: restarted
5.9.2 高级处理器
# advanced-handlers.yml
---
- name: 高级处理器示例
hosts: all
become: yes
tasks:
- name: 更新应用程序配置
template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: restart application stack
- name: 更新数据库配置
template:
src: database.conf.j2
dest: /etc/mysql/conf.d/myapp.cnf
notify:
- restart mysql
- wait for mysql
- restart application stack
- name: 更新缓存配置
template:
src: redis.conf.j2
dest: /etc/redis/redis.conf
notify: restart redis
# 强制执行处理器
- name: 强制重启服务
debug:
msg: "强制重启所有服务"
changed_when: true
notify: restart application stack
when: force_restart | default(false)
# 条件处理器
- name: 开发环境配置
template:
src: debug.conf.j2
dest: /etc/myapp/debug.conf
notify: restart application stack
when: environment == "development"
handlers:
# 基本处理器
- name: restart mysql
service:
name: mysql
state: restarted
- name: restart redis
service:
name: redis
state: restarted
# 等待处理器
- name: wait for mysql
wait_for:
port: 3306
host: localhost
delay: 5
timeout: 30
# 复合处理器
- name: restart application stack
block:
- name: 停止应用程序
service:
name: myapp
state: stopped
- name: 等待进程完全停止
wait_for:
path: /var/run/myapp.pid
state: absent
timeout: 30
- name: 清理临时文件
file:
path: /tmp/myapp-*
state: absent
- name: 启动应用程序
service:
name: myapp
state: started
- name: 等待应用程序就绪
uri:
url: http://localhost:8080/health
status_code: 200
retries: 10
delay: 5
- name: 发送重启通知
mail:
to: admin@example.com
subject: "应用程序已重启 - {{ inventory_hostname }}"
body: "应用程序栈已在 {{ ansible_date_time.iso8601 }} 重启"
delegate_to: localhost
# 条件处理器
- name: restart nginx
service:
name: nginx
state: restarted
when: ansible_service_mgr == "systemd"
# 包含处理器
- name: restart web services
include_tasks: handlers/web-restart.yml
# handlers/web-restart.yml
---
- name: 停止 Web 服务
service:
name: "{{ item }}"
state: stopped
loop:
- nginx
- php-fpm
- name: 启动 Web 服务
service:
name: "{{ item }}"
state: started
loop:
- php-fpm
- nginx
- name: 验证 Web 服务
uri:
url: http://localhost
status_code: 200
retries: 5
delay: 2
5.10 本章总结
本章详细介绍了 Playbook 编写的基础知识,主要内容包括:
- Playbook 概述:Playbook 的特点和与 Ad-hoc 命令的区别
- YAML 基础:YAML 语法规则和常见错误
- Playbook 结构:从简单到复杂的 Playbook 结构
- 任务控制:任务的基本语法、控制选项和包含机制
- 变量使用:变量定义、引用、文件和加密
- 条件控制:各种条件语句和条件执行
- 循环控制:基本循环、高级循环和循环过滤
- 错误处理:错误处理机制、块和救援
- 处理器:基本和高级处理器的使用
掌握这些基础知识是编写高效、可维护的 Ansible Playbook 的关键。
5.11 练习题
基础练习
YAML 语法
- 编写一个包含各种数据类型的 YAML 文件
- 修复给定的 YAML 语法错误
- 将 JSON 数据转换为 YAML 格式
简单 Playbook
- 编写安装和配置 Nginx 的 Playbook
- 添加变量和模板支持
- 实现基本的错误处理
条件和循环
- 使用条件语句处理不同操作系统
- 实现循环安装多个软件包
- 结合条件和循环创建用户
进阶练习
复杂 Playbook
- 编写多 Play Playbook 部署 Web 应用
- 实现环境特定的配置
- 添加部署验证和回滚机制
变量管理
- 设计多层次的变量结构
- 使用 Ansible Vault 保护敏感信息
- 实现动态变量生成
错误处理
- 实现复杂的错误处理逻辑
- 使用块和救援处理部署失败
- 添加监控和告警机制
实战练习
完整应用部署
- 设计完整的应用部署 Playbook
- 包含数据库、缓存、Web 服务器配置
- 实现零停机部署
基础设施管理
- 编写系统初始化 Playbook
- 实现安全加固配置
- 添加监控和日志配置
CI/CD 集成
- 将 Playbook 集成到 CI/CD 流水线
- 实现自动化测试和部署
- 添加部署审批和回滚功能
下一章:第6章:变量和模板详解
返回目录:Ansible 自动化运维教程