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 练习题

基础练习

  1. YAML 语法

    • 编写一个包含各种数据类型的 YAML 文件
    • 修复给定的 YAML 语法错误
    • 将 JSON 数据转换为 YAML 格式
  2. 简单 Playbook

    • 编写安装和配置 Nginx 的 Playbook
    • 添加变量和模板支持
    • 实现基本的错误处理
  3. 条件和循环

    • 使用条件语句处理不同操作系统
    • 实现循环安装多个软件包
    • 结合条件和循环创建用户

进阶练习

  1. 复杂 Playbook

    • 编写多 Play Playbook 部署 Web 应用
    • 实现环境特定的配置
    • 添加部署验证和回滚机制
  2. 变量管理

    • 设计多层次的变量结构
    • 使用 Ansible Vault 保护敏感信息
    • 实现动态变量生成
  3. 错误处理

    • 实现复杂的错误处理逻辑
    • 使用块和救援处理部署失败
    • 添加监控和告警机制

实战练习

  1. 完整应用部署

    • 设计完整的应用部署 Playbook
    • 包含数据库、缓存、Web 服务器配置
    • 实现零停机部署
  2. 基础设施管理

    • 编写系统初始化 Playbook
    • 实现安全加固配置
    • 添加监控和日志配置
  3. CI/CD 集成

    • 将 Playbook 集成到 CI/CD 流水线
    • 实现自动化测试和部署
    • 添加部署审批和回滚功能

下一章第6章:变量和模板详解

返回目录Ansible 自动化运维教程