Ansible Playbook 进阶 #

Blocks #

什么是 Blocks? #

Blocks 允许将多个任务分组,可以统一应用条件、错误处理等属性。

yaml
---
- name: Use blocks
  hosts: webservers
  become: yes
  
  tasks:
    - name: Install and configure Nginx
      block:
        - name: Install Nginx
          apt:
            name: nginx
            state: present
        
        - name: Copy configuration
          template:
            src: nginx.conf.j2
            dest: /etc/nginx/nginx.conf
          notify: Restart Nginx
        
        - name: Start Nginx
          service:
            name: nginx
            state: started
            enabled: yes
      
      when: ansible_facts['os_family'] == "Debian"
      become: yes
      tags: nginx

Block 结构 #

yaml
tasks:
  - name: Block example
    block:
      # 主要任务
      - name: Task 1
        ...
      - name: Task 2
        ...
    
    rescue:
      # 错误处理任务(当 block 中任务失败时执行)
      - name: Rescue task 1
        ...
      - name: Rescue task 2
        ...
    
    always:
      # 总是执行的任务(无论成功或失败)
      - name: Always task
        ...

错误处理 #

yaml
---
- name: Error handling with blocks
  hosts: webservers
  become: yes
  
  tasks:
    - name: Handle deployment errors
      block:
        - name: Stop application
          service:
            name: myapp
            state: stopped
        
        - name: Update application
          git:
            repo: https://github.com/user/app.git
            dest: /var/www/app
            version: main
        
        - name: Run migrations
          command: /var/www/app/migrate.sh
          register: migration_result
          failed_when: "'ERROR' in migration_result.stderr"
        
        - name: Start application
          service:
            name: myapp
            state: started
      
      rescue:
        - name: Rollback to previous version
          command: /var/www/app/rollback.sh
        
        - name: Start application with old version
          service:
            name: myapp
            state: started
        
        - name: Send failure notification
          mail:
            to: admin@example.com
            subject: "Deployment failed on {{ inventory_hostname }}"
            body: "Deployment failed. Rollback completed."
      
      always:
        - name: Clean up temporary files
          file:
            path: /tmp/deploy_temp
            state: absent
        
        - name: Log deployment result
          lineinfile:
            path: /var/log/deploy.log
            line: "{{ ansible_date_time.iso8601 }} - Deployment {{ 'succeeded' if ansible_failed_task is undefined else 'failed' }}"
            create: yes

错误处理 #

ignore_errors #

忽略任务错误继续执行:

yaml
tasks:
  - name: This task might fail
    command: /usr/bin/some_command
    ignore_errors: yes
  
  - name: This task will still run
    debug:
      msg: "Previous task failed but we continue"

failed_when #

自定义失败条件:

yaml
tasks:
  - name: Run a script
    command: /usr/local/bin/myscript.sh
    register: script_result
  
  - name: Check script result
    debug:
      msg: "Script output: {{ script_result.stdout }}"
  
  - name: Fail if error in output
    fail:
      msg: "Script failed with error: {{ script_result.stderr }}"
    when: "'ERROR' in script_result.stderr"
  
  # 或者直接在任务中定义
  - name: Run another script
    command: /usr/local/bin/another_script.sh
    register: result
    failed_when: "'FAILURE' in result.stdout or result.rc != 0"

changed_when #

自定义变更条件:

yaml
tasks:
  - name: Run command that doesn't change anything
    command: /usr/bin/check_status.sh
    register: status_result
    changed_when: false    # 永远不会标记为 changed
  
  - name: Run command with custom change condition
    command: /usr/local/bin/update_config.sh
    register: update_result
    changed_when: "'updated' in update_result.stdout"
  
  - name: Restart service only if config changed
    service:
      name: myapp
      state: restarted
    when: update_result.changed

block/rescue/always 完整示例 #

yaml
---
- name: Complete error handling example
  hosts: databases
  become: yes
  
  tasks:
    - name: Database backup and restore
      block:
        - name: Create backup directory
          file:
            path: /backup/{{ ansible_date_time.date }}
            state: directory
        
        - name: Backup database
          command: >
            mysqldump -u root -p{{ mysql_root_password }} 
            --all-databases > /backup/{{ ansible_date_time.date }}/all.sql
          no_log: true    # 不记录敏感信息
        
        - name: Stop database
          service:
            name: mysql
            state: stopped
        
        - name: Upgrade database package
          apt:
            name: mysql-server
            state: latest
        
        - name: Start database
          service:
            name: mysql
            state: started
        
        - name: Verify database is running
          command: mysqladmin ping
          register: db_ping
          retries: 5
          delay: 10
          until: db_ping.rc == 0
      
      rescue:
        - name: Restore from backup
          command: >
            mysql -u root -p{{ mysql_root_password }} 
            < /backup/{{ ansible_date_time.date }}/all.sql
          no_log: true
        
        - name: Start database
          service:
            name: mysql
            state: started
        
        - name: Alert team
          slack:
            token: "{{ slack_token }}"
            msg: "Database upgrade failed on {{ inventory_hostname }}, rollback completed"
      
      always:
        - name: Clean up backup
          file:
            path: /backup/{{ ansible_date_time.date }}
            state: absent
          when: cleanup_backup | default(false)

异步任务 #

异步执行 #

长时间运行的任务可以使用异步模式:

yaml
tasks:
  # 同步执行(默认,会等待完成)
  - name: Run long task synchronously
    command: /usr/bin/long_running_task.sh
    # 会阻塞直到完成

  # 异步执行
  - name: Run long task asynchronously
    command: /usr/bin/long_running_task.sh
    async: 3600        # 最大运行时间(秒)
    poll: 0            # 不轮询检查状态
    register: long_task

  # 继续执行其他任务
  - name: Do other work
    debug:
      msg: "Doing other work while long task runs..."

  # 稍后检查异步任务状态
  - name: Check async task status
    async_status:
      jid: "{{ long_task.ansible_job_id }}"
    register: job_result
    until: job_result.finished
    retries: 60
    delay: 60

轮询检查 #

yaml
tasks:
  - name: Run task with polling
    command: /usr/bin/long_task.sh
    async: 300         # 5分钟超时
    poll: 10           # 每10秒检查一次
    register: task_result

  - name: Check result
    debug:
      msg: "Task completed with output: {{ task_result.stdout }}"

并行执行多个异步任务 #

yaml
tasks:
  - name: Start multiple async tasks
    command: "/usr/bin/process_item.sh {{ item }}"
    async: 600
    poll: 0
    loop:
      - item1
      - item2
      - item3
    register: async_tasks

  - name: Wait for all tasks to complete
    async_status:
      jid: "{{ item.ansible_job_id }}"
    register: job_result
    until: job_result.finished
    retries: 60
    delay: 10
    loop: "{{ async_tasks.results }}"

委托 #

delegate_to #

在特定主机上执行任务:

yaml
---
- name: Delegation example
  hosts: webservers
  
  tasks:
    # 在本地执行
    - name: Run task on localhost
      command: /usr/local/bin/local_script.sh
      delegate_to: localhost
    
    # 在特定主机执行
    - name: Run task on specific host
      command: /usr/local/bin/check_lb.sh
      delegate_to: loadbalancer1
    
    # 在主机本身执行(默认行为)
    - name: Normal task
      command: hostname

delegate_facts #

委托收集 Facts:

yaml
tasks:
  - name: Gather facts from another host
    setup:
    delegate_to: "{{ item }}"
    delegate_facts: True
    loop:
      - web1
      - web2

local_action #

在本地执行任务的简写:

yaml
tasks:
  # 使用 delegate_to
  - name: Run on localhost
    command: /usr/local/bin/local_script.sh
    delegate_to: localhost

  # 使用 local_action(等价)
  - name: Run on localhost
    local_action: command /usr/local/bin/local_script.sh

实际应用场景 #

yaml
---
- name: Deployment with delegation
  hosts: webservers
  serial: 1    # 逐个主机执行
  
  tasks:
    - name: Remove from load balancer
      haproxy:
        backend: web_servers
        host: "{{ inventory_hostname }}"
        state: disabled
      delegate_to: loadbalancer
    
    - name: Update application
      git:
        repo: https://github.com/user/app.git
        dest: /var/www/app
        version: main
    
    - name: Restart application
      service:
        name: myapp
        state: restarted
    
    - name: Wait for application to start
      wait_for:
        port: 8080
        delay: 5
    
    - name: Add back to load balancer
      haproxy:
        backend: web_servers
        host: "{{ inventory_hostname }}"
        state: enabled
      delegate_to: loadbalancer

执行策略 #

策略类型 #

text
┌─────────────────────────────────────────────────────────────┐
│                      执行策略类型                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  linear(默认)              free                           │
│  ─────────────              ──────                          │
│  逐个主机执行任务            主机独立执行                    │
│  等待所有主机完成            不等待其他主机                  │
│  适合大多数场景              适合独立任务                    │
│                                                              │
│  host_pinned                                               │
│  ────────────                                              │
│  固定主机顺序                                               │
│  按主机顺序执行完整 Play                                    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

linear 策略 #

yaml
---
- name: Linear strategy (default)
  hosts: webservers
  strategy: linear    # 默认策略
  
  tasks:
    - name: Task 1
      debug:
        msg: "Task 1 on {{ inventory_hostname }}"
    
    - name: Task 2
      debug:
        msg: "Task 2 on {{ inventory_hostname }}"

# 执行顺序:
# web1: Task 1 -> web2: Task 1 -> web3: Task 1
# web1: Task 2 -> web2: Task 2 -> web3: Task 2

free 策略 #

yaml
---
- name: Free strategy
  hosts: webservers
  strategy: free    # 自由策略
  
  tasks:
    - name: Task 1
      debug:
        msg: "Task 1 on {{ inventory_hostname }}"
    
    - name: Task 2
      debug:
        msg: "Task 2 on {{ inventory_hostname }}"

# 执行顺序:
# 每个主机独立执行,不等待其他主机
# web1: Task 1 -> Task 2
# web2: Task 1 -> Task 2
# web3: Task 1 -> Task 2
# 可能同时进行

serial 控制 #

控制每次处理的主机数量:

yaml
---
- name: Serial execution
  hosts: webservers
  serial: 2    # 每次处理 2 台主机
  
  tasks:
    - name: Update application
      debug:
        msg: "Updating {{ inventory_hostname }}"
    
    - name: Restart service
      debug:
        msg: "Restarting on {{ inventory_hostname }}"

# 执行顺序:
# 批次 1: web1, web2
# 批次 2: web3, web4
# 批次 3: web5

百分比控制 #

yaml
---
- name: Percentage serial
  hosts: webservers
  serial: "25%"    # 每次处理 25% 的主机
  
  tasks:
    - name: Update
      debug:
        msg: "Updating {{ inventory_hostname }}"

最大失败比例 #

yaml
---
- name: Control failure threshold
  hosts: webservers
  max_fail_percentage: 25    # 超过 25% 失败则停止
  
  tasks:
    - name: Critical task
      command: /usr/bin/critical_operation.sh

循环进阶 #

until 循环 #

等待条件满足:

yaml
tasks:
  - name: Wait for service to be ready
    uri:
      url: http://localhost:8080/health
      return_content: yes
    register: health_check
    until: "'healthy' in health_check.content"
    retries: 10
    delay: 5

  - name: Wait for file to exist
    stat:
      path: /tmp/ready
    register: file_check
    until: file_check.stat.exists
    retries: 20
    delay: 3

循环控制 #

yaml
tasks:
  - name: Loop with control
    debug:
      msg: "{{ item }}"
    loop:
      - a
      - b
      - c
    loop_control:
      pause: 3              # 每次循环暂停 3 秒
      index_var: index      # 索引变量
      label: "{{ item }}"   # 输出标签

  - name: Loop with index
    debug:
      msg: "Item {{ index }} is {{ item }}"
    loop:
      - first
      - second
      - third
    loop_control:
      index_var: index

嵌套循环 #

yaml
tasks:
  - name: Nested loops
    debug:
      msg: "User {{ item[0] }} has permission {{ item[1] }}"
    loop: "{{ users | product(permissions) | list }}"
    vars:
      users:
        - alice
        - bob
      permissions:
        - read
        - write
        - execute

条件进阶 #

组合条件 #

yaml
tasks:
  - name: Complex conditions
    debug:
      msg: "Complex condition met"
    when: >
      (ansible_facts['distribution'] == 'Ubuntu' and 
       ansible_facts['distribution_version'] == '20.04') or
      (ansible_facts['distribution'] == 'CentOS' and 
       ansible_facts['distribution_major_version'] == '8')

  - name: Multiple conditions (AND)
    debug:
      msg: "All conditions met"
    when:
      - condition1
      - condition2
      - condition3

  - name: Multiple conditions (OR)
    debug:
      msg: "At least one condition met"
    when: condition1 or condition2 or condition3

条件测试 #

yaml
tasks:
  - name: Test if variable is defined
    debug:
      msg: "Variable is defined"
    when: my_var is defined

  - name: Test if variable is undefined
    debug:
      msg: "Variable is undefined"
    when: my_var is undefined

  - name: Test if variable is string
    debug:
      msg: "Variable is a string"
    when: my_var is string

  - name: Test if variable is number
    debug:
      msg: "Variable is a number"
    when: my_var is number

  - name: Test if variable is boolean
    debug:
      msg: "Variable is a boolean"
    when: my_var is boolean

  - name: Test if list contains item
    debug:
      msg: "List contains item"
    when: item in my_list

  - name: Test if string matches regex
    debug:
      msg: "String matches pattern"
    when: my_string is match("^start.*end$")

  - name: Test if file exists
    debug:
      msg: "File exists"
    when: my_file is exists

  - name: Test if directory exists
    debug:
      msg: "Directory exists"
    when: my_dir is directory

  - name: Test if path is file
    debug:
      msg: "Path is a file"
    when: my_path is file

检查模式 #

检查模式 #

bash
# 检查模式(模拟执行)
ansible-playbook site.yml --check

# 检查模式 + 差异显示
ansible-playbook site.yml --check --diff

任务级别的检查模式控制 #

yaml
tasks:
  # 总是在检查模式下运行
  - name: Always run in check mode
    command: /usr/bin/check_script.sh
    check_mode: no    # 即使在检查模式也实际执行
  
  # 从不在检查模式下运行
  - name: Never run in check mode
    command: /usr/bin/actual_script.sh
    check_mode: yes   # 在检查模式跳过
  
  # 模拟变更
  - name: Simulate change
    command: /usr/bin/simulate.sh
    diff: yes

完整示例 #

yaml
---
- name: Advanced deployment playbook
  hosts: webservers
  become: yes
  strategy: free
  serial: "25%"
  max_fail_percentage: 30
  
  vars:
    app_name: myapp
    app_port: 8080
    health_check_retries: 10
    health_check_delay: 5
  
  tasks:
    - name: Remove from load balancer
      block:
        - name: Disable in HAProxy
          haproxy:
            backend: web_servers
            host: "{{ inventory_hostname }}"
            state: disabled
          delegate_to: loadbalancer
          notify: Wait for connections to drain
      rescue:
        - name: Log load balancer error
          debug:
            msg: "Failed to remove from load balancer, continuing anyway"
          ignore_errors: yes
    
    - name: Deploy application
      block:
        - name: Create backup
          archive:
            path: /var/www/{{ app_name }}
            dest: /backup/{{ app_name }}_{{ ansible_date_time.iso8601 }}.tar.gz
          when: ansible_facts['mounts'] | selectattr('mount', 'equalto', '/var/www') | list | length > 0
        
        - name: Pull latest code
          git:
            repo: "{{ app_repo }}"
            dest: /var/www/{{ app_name }}
            version: "{{ app_version | default('main') }}"
          register: git_result
          async: 300
          poll: 10
        
        - name: Install dependencies
          command: npm install --production
          args:
            chdir: /var/www/{{ app_name }}
          when: git_result.changed
        
        - name: Run database migrations
          command: npm run migrate
          args:
            chdir: /var/www/{{ app_name }}
          register: migration_result
          changed_when: "'Migration completed' in migration_result.stdout"
          when: git_result.changed
        
        - name: Restart application
          systemd:
            name: "{{ app_name }}"
            state: restarted
            daemon_reload: yes
          when: git_result.changed
        
        - name: Wait for application to start
          uri:
            url: "http://localhost:{{ app_port }}/health"
            return_content: yes
          register: health_check
          until: "'healthy' in health_check.content"
          retries: "{{ health_check_retries }}"
          delay: "{{ health_check_delay }}"
      
      rescue:
        - name: Rollback to previous version
          debug:
            msg: "Rolling back..."
        
        - name: Restore from backup
          unarchive:
            src: /backup/{{ app_name }}_{{ ansible_date_time.iso8601 }}.tar.gz
            dest: /var/www/
            remote_src: yes
        
        - name: Restart application
          systemd:
            name: "{{ app_name }}"
            state: restarted
        
        - name: Alert on failure
          slack:
            token: "{{ slack_token }}"
            msg: "Deployment failed on {{ inventory_hostname }}"
          delegate_to: localhost
          ignore_errors: yes
      
      always:
        - name: Clean up old backups
          shell: find /backup -name "{{ app_name }}_*.tar.gz" -mtime +7 -delete
          changed_when: false
    
    - name: Add back to load balancer
      haproxy:
        backend: web_servers
        host: "{{ inventory_hostname }}"
        state: enabled
      delegate_to: loadbalancer
  
  handlers:
    - name: Wait for connections to drain
      pause:
        seconds: 30

下一步 #

现在你已经掌握了 Playbook 的高级技巧,接下来学习 变量管理 深入了解 Ansible 的变量系统!

最后更新:2026-03-29