第六章:Playbook 进阶

掌握 Ansible Playbook 的高级特性,包括委托、异步、错误处理等。

最后更新: 2024-01-20
页面目录

Playbook 进阶

本章介绍 Ansible Playbook 的高级特性,帮助你编写更复杂、更健壮的自动化任务。

错误处理

ignore_errors

tasks:
  - name: Try to stop service (ignore error if not running)
    service:
      name: myapp
      state: stopped
    ignore_errors: yes
  
  - name: Continue with next task
    debug:
      msg: "Service stopped or not running"

failed_when

tasks:
  - name: Check application status
    command: /opt/myapp/bin/health-check
    register: health_check
    failed_when: health_check.rc not in [0, 2]
  
  # 条件判断
  - name: Fail on specific error
    command: /usr/local/bin/deploy.sh
    register: deploy_result
    failed_when: "'ERROR' in deploy_result.stdout"

changed_when

tasks:
  - name: Check if configured
    command: /opt/myapp/bin/is-configured
    register: config_status
    changed_when: config_status.rc == 0
    failed_when: config_status.rc >= 2

block 和 rescue

tasks:
  - name: Deploy application
    block:
      - name: Backup current version
        command: /opt/myapp/backup.sh
        register: backup
      
      - name: Deploy new version
        command: /opt/myapp/deploy.sh
        register: deploy
      
      - name: Verify deployment
        command: /opt/myapp/verify.sh
        register: verify
    rescue:
      - name: Rollback on failure
        command: /opt/myapp/rollback.sh
        when: backup is succeeded
      
      - name: Notify failure
        debug:
          msg: "Deployment failed, rollback executed"
    always:
      - name: Cleanup temporary files
        file:
          path: /tmp/deploy_temp
          state: absent

force_handlers

# 即使 Play 失败也执行 Handlers
- name: Deploy application
  hosts: webservers
  force_handlers: yes
  
  tasks:
    - name: Install package
      apt:
        name: custom-package
        state: present
    
    - name: This will fail
      command: /bin/false
    
  handlers:
    - name: Restart services
      service:
        name: nginx
        state: restarted

委托和本地执行

delegate_to

tasks:
  # 在本地执行任务
  - name: Get load balancer info
    uri:
      url: "http://lb.example.com/api/status"
    register: lb_status
  
  # 委托到负载均衡器
  - name: Add server to load balancer
    haproxy:
      state: enabled
      host: "{{ ansible_host }}"
      socket: /var/run/haproxy.sock
    delegate_to: lb-server
  
  # 委托到本地主机
  - name: Save facts locally
    copy:
      content: "{{ hostvars[inventory_hostname].ansible_facts | to_nice_json }}"
      dest: "/tmp/facts/{{ inventory_hostname }}.json"
    delegate_to: localhost
  
  # 委托到特定主机
  - name: Update DNS
    dnsupdate:
      record: "{{ inventory_hostname }}"
      type: A
      value: "{{ ansible_host }}"
    delegate_to: "{{ dns_server }}"

run_once

tasks:
  # 仅在第一台主机执行
  - name: Initialize database
    command: /opt/init-db.sh
    run_once: yes
  
  # 结合 delegate_to
  - name: Create deployment lock
    command: /usr/local/bin/create-lock.sh
    run_once: yes
    delegate_to: localhost
  
  # 条件 run_once
  - name: Wait for database initialization
    wait_for:
      port: 5432
      host: "{{ db_host }}"
      timeout: 300
    run_once: yes
    delegate_to: localhost
    when: inventory_hostname == groups['dbservers'][0]

异步任务和轮询

async 和 poll

tasks:
  # 启动长时间运行的任务
  - name: Long running backup
    command: /usr/local/bin/backup.sh
    async: 3600          # 最大执行时间(秒)
    poll: 0              # 不等待,立即继续
    register: backup_job
  
  # 轮询异步任务
  - name: Check backup progress
    async_status:
      jid: "{{ backup_job.ansible_job_id }}"
    register: job_result
    until: job_result.finished
    retries: 60
    delay: 60
  
  # 后台执行
  - name: Run report generation
    command: /usr/local/bin/generate-report.sh
    async: 1800
    poll: 15

async_status

tasks:
  - name: Start report generation
    command: /usr/local/bin/generate-report.sh
    async: 3600
    poll: 0
    register: report_job
  
  - name: Wait for report to complete
    async_status:
      jid: "{{ report_job.ansible_job_id }}"
    register: result
    until: result.finished
    retries: 720
    delay: 5
  
  - name: Download report
    fetch:
      src: "/tmp/reports/report.pdf"
      dest: "./reports/{{ inventory_hostname }}.pdf"
    when: result.finished

等待条件和等待模块

wait_for

tasks:
  # 等待端口可用
  - name: Wait for database to be ready
    wait_for:
      port: 5432
      host: "{{ db_host | default('localhost') }}"
      delay: 5
      timeout: 60
  
  # 等待文件存在
  - name: Wait for config file
    wait_for:
      path: /etc/myapp/config.yml
      state: present
      delay: 5
      timeout: 60
  
  # 等待文件包含内容
  - name: Wait for service to initialize
    wait_for:
      path: /var/log/myapp/ready
      search_regex: "Service started"
      timeout: 120
  
  # 等待连接建立
  - name: Wait for server to accept connections
    wait_for:
      host: "{{ inventory_hostname }}"
      port: "{{ app_port }}"
      state: started
      timeout: 300
  
  # 等待进程消失
  - name: Wait for old process to stop
    wait_for:
      path: /var/run/myapp.pid
      state: absent
      timeout: 60

wait_for_connection

tasks:
  # 等待主机连接可用
  - name: Wait for server to boot
    wait_for_connection:
      delay: 10
      timeout: 300
  
  # 等待后执行配置
  - name: Wait and then configure
    wait_for_connection:
      timeout: 300
    register: connection_result
  
  - name: Install packages after boot
    apt:
      name: nginx
      state: present
    when: connection_result is successful

查找和过滤器

lookup 插件

tasks:
  # 读取文件
  - name: Read SSH public key
    debug:
      msg: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
  
  # 读取环境变量
  - name: Get environment variable
    debug:
      msg: "{{ lookup('env', 'HOME') }}"
  
  # 读取模板
  - name: Template lookup
    debug:
      msg: "{{ lookup('template', 'vars.j2') }}"
  
  # 读取INI文件
  - name: Read INI values
    debug:
      msg: "{{ lookup('ini', 'value section=section1 file=~/test.ini') }}"
  
  # 读取 AWS SSM 参数
  - name: Get SSM parameter
    debug:
      msg: "{{ lookup('aws_ssm', '/path/to/parameter') }}"
  
  # 读取密码
  - name: Generate random password
    debug:
      msg: "{{ lookup('password', '/dev/null length=20 chars=ascii_letters,digits') }}"
  
  # 多值查找
  - name: Read multiple files
    debug:
      msg: "{{ item }}"
    loop:
      - "{{ lookup('file', 'file1.txt') }}"
      - "{{ lookup('file', 'file2.txt') }}"

Jinja2 过滤器

tasks:
  # 默认值
  - debug:
      msg: "{{ my_var | default('default_value') }}"
  
  # 类型转换
  - debug:
      msg: "{{ '123' | int }}"
      msg: "{{ 123 | string }}"
      msg: "{{ list | list }}"
  
  # 字符串过滤器
  - debug:
      msg: "{{ name | upper }}"
      msg: "{{ name | lower }}"
      msg: "{{ path | basename }}"
      msg: "{{ path | dirname }}"
      msg: "{{ 'hello world' | capitalize }}"
      msg: "{{ 'hello world' | title }}"
  
  # 列表过滤器
  - debug:
      msg: "{{ [1,2,3] | sum }}"
      msg: "{{ [1,2,3,4,5] | min }}"
      msg: "{{ [1,2,3,4,5] | max }}"
      msg: "{{ [3,1,2] | sort }}"
      msg: "{{ list1 | union(list2) }}"
      msg: "{{ list1 | intersect(list2) }}"
  
  # 字典过滤器
  - debug:
      msg: "{{ dict | keys }}"
      msg: "{{ dict | values }}"
      msg: "{{ dict | items }}"
      msg: "{{ dict | combine(other_dict) }}"
  
  # JSON 过滤器
  - debug:
      msg: "{{ python_dict | to_json }}"
      msg: "{{ python_dict | to_nice_json }}"
      msg: "{{ json_string | from_json }}"
  
  # YAML 过滤器
  - debug:
      msg: "{{ data | to_yaml }}"
      msg: "{{ yaml_string | from_yaml }}"
  
  # 路径过滤器
  - debug:
      msg: "{{ path | expanduser }}"
      msg: "{{ path | realpath }}"
  
  # UUID 生成
  - debug:
      msg: "{{ inventory_hostname | hash('sha1') }}"
      msg: "{{ 128 | random }}"
      msg: "{{ 50 | random(start=1, end=100) }}"

条件测试

tests 操作符

tasks:
  # 定义变量
  - set_fact:
      my_list: [1, 2, 3, 4, 5]
      my_dict: {a: 1, b: 2}
  
  # 包含测试
  - debug:
      msg: "Contains 3"
    when: "3 in my_list"
  
  # 定义测试
  - debug:
      msg: "Variable is defined"
    when: my_var is defined
  
  - debug:
      msg: "Variable is not defined"
    when: my_var is not defined
  
  # truthy/falsy 测试
  - debug:
      msg: "Value is truthy"
    when: my_value is truthy
  
  # 比较测试
  - debug:
      msg: "Greater"
    when: my_num|int > 10
  
  # 字符串匹配
  - debug:
      msg: "Matches"
    when: my_string is match("^prefix.*")

敏感数据处理

no_log

tasks:
  # 隐藏任务输出
  - name: Set database password
    set_fact:
      db_password: "{{ lookup('password', '/dev/null length=32') }}"
    no_log: yes
  
  - name: Configure database
    mysql_user:
      name: app
      password: "{{ db_password }}"
      priv: 'app.*:ALL'
    no_log: yes
  
  # 整个 Play 隐藏
  - name: Security sensitive play
    hosts: databases
    no_log: yes
    tasks:
      - name: Set secrets
        command: /opt/set-secrets.sh

环境变量

tasks:
  - name: Use API with environment
    uri:
      url: "https://api.example.com/data"
      headers:
        Authorization: "Bearer {{ api_token }}"
    environment:
      HTTPS_PROXY: "{{ proxy_url }}"
      HTTP_PROXY: "{{ proxy_url }}"

动态主机模式

主机组操作

tasks:
  - name: Get all webservers
    debug:
      msg: "{{ groups['webservers'] }}"
  
  - name: Get first database server
    debug:
      msg: "{{ groups['dbservers'][0] }}"
  
  - name: Get all hostnames
    debug:
      msg: "{{ inventory_hostname }}"
  
  - name: Check if in group
    debug:
      msg: "In production"
    when: "'production' in group_names"
  
  - name: Loop over all hosts in group
    debug:
      msg: "{{ item }}"
    loop: "{{ groups['webservers'] }}"

hostvars

tasks:
  - name: Get fact from another host
    debug:
      msg: "{{ hostvars['db1']['ansible_host'] }}"
  
  - name: Get all facts from group
    debug:
      msg: "{{ hostvars[item]['ansible_default_ipv4'] }}"
    loop: "{{ groups['all'] }}"
  
  - name: Check if host is reachable
    debug:
      msg: "{{ item }} is reachable"
    loop: "{{ groups['webservers'] }}"
    when: hostvars[item].ansible_connection != 'none'

下一步

现在你已经掌握了 Playbook 的进阶特性。接下来让我们学习 Handlers 处理器。

👉 Handlers 处理器