很显然,我们可以将众多 ansible 命令放在 Shell 脚本中执行,以实现批量部署操作。比如:
#!/bin/sh ansible host-01 -m ping ansbile host-01 -m copy -a "src=/etc/hosts dest=/tmp/hosts" ansible host-01 -m shell -a "/sbin/reboot"
但是,如果我们的需求更加复杂呢?比如需要根据远程主机的环境、当前状态、发行版版本等等结果来执行不同的命令呢?很显然在 Shell 脚本中使用 ansible 命令不是最佳方案。那我们应该怎么办呢?
Ansible 提供了脚本化的功能,将任务编写到脚本中,运行该脚本以执行多个任务,这种脚本被称为 Playbook。使用 Playbook 描述 Ansible 要执行的系列操作,脚本为YAML文件,以yml或yaml为后缀。它替代在Shell脚本中挨个命令执行的方式。
---
- hosts: web
  vars:
    http_port: 80
    max_clients: 200
  remote_user: root
  # 任务列表
  tasks:
    - name: ensure apache is at the latest version
      yum: pkg=httpd state=latest
    - name: Write the configuration file
      template: src=templates/httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
      notify:
        - restart apache
    - name: Write the default index.html file
      template: src=templates/index.html.j2 dest=/var/www/html/index.html
    - name: ensure a pache is running
      service: name=httpd state=started
  # 回调处理
  handlers:
    - name: restart apache
      service: name=httpd state=restarted
使用 ansible-playbook 命令执行 Playbook 脚本:
#!/bin/sh # 执行 Playbook 的基本方法。 ansible-playbook deploy.yml # 查看输出的细节 ansible-playbook playbook.yml --verbose # 查看该脚本影响哪些主机( hosts ) ansible-playbook playbook.yml --list-hosts # 并行执行脚本 ansible-playbook playbook.yml -f 10
查看更多命令帮助:
#!/bin/sh man ansible-playbook ansible-playbook -h
使用 YAML 语法编写 Playbook 脚本,这里不介绍 YAML 的语法,已经有很多优秀文章。
通常 Playbook 由三部分组成:
在哪些机器上以哪个用户执行执行:相关的指令有 hosts、user 等等;
 hosts 指定主机组名;
 vars 定义参数,可以后面的参数中引用;
 remote_user 定义执行的用户; 
 指定主机与用户,对于开始的部分(hosts、user、vars):
 1)定义要操作的主机及用户;
 2)同时定义两个变量;
 3)还可以使用 become、become_method、become_user 来以其他用户执行(需要 --ask-become-pass 指定密码); 
执行哪些任务:使用 tasks 指令;
 tasks 指定要执行的任务,每个任务都称为“动作(Action)”;
 
指定任务列表,对于 tasks 部分,基本语法如下:
tasks:
  - name: ensure apache is at the latest version
    yum: pkg=httpd state=latest
# 但是 name 可选,因此:
tasks:
  - yum: pkg=httpd state=latest
 1)依次执行,如果在 tasks 中的动作发生错误,则 Playbook 会终止执行。需要调整 Playbook 以重新执行;
 2)每个动作都是对模块的一次调用,只是参数和变量不同;参数可以 key=value 形式传入,参数可以写在多行中,或者以 YMAL 形式传入;
 3)建议为每个动作都加上 name 指令,以指示动作的内容;否则显示当前动作内容,在命令中难以辨识。
 4)对于每个动作,所调用的模块会先检查是否需要执行:如果执行成功,则返回“changed”;如果不需要执行,则返回“ok”。检查判断由模块负责实现。 
---
- hosts: webservers
  user: root
  vars:
    http_port: 80
    max_clients	: 200
  tasks:
    # 模块参数写在单行
    - name: ensure apache is at the latest version
      yum: pkg=httpd state=latest
    # 模块参数写在多行
    - name: write the apache config file
      template: src=/srv/httpd.j2
                  dest=/etc/httpd.conf
      notify:
        - restart apache
    # 模块参数以 YAML 形式传入
    - name: ensure apache is running
      service:
        name: httpd
        state: started
问题:在执行多个不同的任务后,我们可能需要执行某些特定操作。比如,在某个任务中修改 Apache 配置文件,在另个任务中安装 Apache 插件,这两个任务都需要重启 Apache 服务。
方案:像上面的这样场景,重启 Apache 就可以设计成“回调通知”(是软件编程的概念),即执行某些操作后触发某个事件。在Ansible中,通过使用 handlers 指令实现。
---
- hosts: web
  # 任务列表
  tasks:
    - name: Write the configuration file
      template: src=templates/httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
      # 发出通知
      notify:
        - restart-apache
  # 回调处理
  handlers:
    - name: restart-apache
      service: name=httpd state=restarted
但多次触发只执行一次,并按照声明的顺序执行。
 需要使用 notify 指令调用:
 在 handlers 中的动作需要在 tasks 中调用,即使用 notify 指令; 
 需要任务真正执行(changed):
 只有在 tasks 中的动作真正执行后(changed),才会执行 handlers 中的动作; 
 对应 handlers 只会执行一次:
 在多次通知(notify)下,特定 handlers 只会执行一次.因为 handler 是在 tasks 执行结束后才开始执行的。就是说,在 tasks 中的多个动作可以通知 handler 中的同一个动作,但是 handler 里的这个动作只会执行一次。 
 依照定义顺序执行:
 鉴于此,在handler中的动作是按照定义的顺序执行的,与收到的通知顺序是无关的。 
 在 Playbook 中,也可以逻辑控制语句:
 when - 类似于编程语言中的 if 关键字
 with_x - 类似于编程语言中的 while 关键字
 block - 类似于编程语言中的代码块。可以把几个任务组成一个代码块,以便于针对一组操作的异常进行处理等操作。 
使用条件判断语句”when“:
tasks :
  - name: "shutdown Debian flavored systems "
    when: ansible_os_family == "Debian"
    command: /sbin/shutdown -t now
也可以更具动作的执行结果来执行任务:
tasks:
  - command: /bin/false
    register: result
    ignore_errors: True
  - command: /bin/something
    when: result|failed
  - command: /bin/something
    when: result|success
  - command: /bin/something
    when: result|skipped
远程主机中的系统变量也可以作为when的条件,用"|int"还可以转换返回值的类型:
- hosts: web
  tasks:
    - debug: msg="only on Red Hat 7 , derivatives , and later"
      when : ansible_os_family == "RedHat" and ansible_lsb.major_release|int >= 6
还可以使用条件表达式:
vars:
  epic: true
tasks:
  - shell: echo "epic !"
    when: epic
  - shell: echo "Tnot epic"
    when: not epic
  - shell: echo "epic is defined"
    when: epic is defined
  - shell: echo "epic is not defined"
    when: epic is not defined
数值表达式:
tasks :
  - command: echo {{ item }}
    with_items: [ 0, 2 , 4 , 6, 8 , 10 ]
    when: item > 5
    # 注意,当when和with_items一起使用时,when是针对每个条目进行判断的。
与include一起用:
- include: tasks/sometasks.yml when: "reticulating splines ’ in output"
与role一起使用:
- hosts: webservers
  roles :
    - { role: debian_stock_config, when: ansible_os_family == ‘Debian‘ }
使用循环(Loop):
vars:
  somelist: ["testuserl", "testuser2"]
tasks:
  - name: "批量添加用户"
    user: name={{ item }} state=present groups=wheel
    with_items:
      - testuser0
      - testuser1
  - name: "从变量中获取要添加的用户"
    user: name={{ item }} state=present groups=wheel
    with_items: "{{ somelist }}"
  # with_items不仅支持列表,还支持字典
  - name: "演示一个更加复杂的循环参数"
    user: name={{ item.name }} state=present groups={{ item.groups }}
    with_items:
      - { name: ‘testuser0‘, groups: ‘root‘ }
      - { name: ‘testuserl‘, groups: ‘wheel‘ }
嵌套循环:
---
- hosts: all
  tasks:
    - name: "嵌套循环"
      debug: msg="first {{ item.0 }} {{ item.1 }}"
      with_nested:
        - [‘alice‘, ‘bob‘]
        - [‘db0‘, ‘db1‘, ‘db2‘]
    - name: "嵌套循环的另一种取值形式"
      debug: msg="first {{ item[‘0‘] }} {{ item[‘1‘] }}"
      with_nested:
        - [‘alice‘, ‘bob‘]
        - [‘db0‘, ‘db1‘, ‘db2‘]
循环哈希表:
---
- hosts: all
  vars:
    users:
      alice:
        name: Alice
        tel: 1234567890
      bob:
        name: Bob
        tel: 0987654321
  tasks:
    - name: "嵌套循环的另一种取值形式"
      debug: msg="User {{ item.key }} Name {{ item.value.name }} Tel {{ item.value.tel }}"
      with_dict: "{{ users }}"
对文件列表使用循环。可以使用with_fileglob可以以非递归的方式来模拟匹配单个目录中的文件:
tasks :
  # first ensure our target directory exists
  - file : dest=/etc/fooapp state=directory
  # copy each file over that matches the given pattern
  - copy : src={{ item}} dest=/etc/fooapp/ owner=root mode=600
    with_fileglob:
      - /playbooks/files/fooapp/*
使用block创建块,来包含一段动作,然后根据条件执行一段语句:
tasks :
  - block :
      - yum: name={{ item }} state=installed
        with_items:
          - httpd
          - memcached
      - template: src=templates/src.j2 dest=/etc/foo.conf
      - service: name=bar state=started enabled=True
    rescue :
      - debug: msg= "I caught an error"
      - command: /bin/false
      - debug : msg="I also 口ever execute :-("
    always:
      - debug: msg="this a l ways executes"
    when: ansible_distribution == "CentOS"
    become: true
    become_user: root
组装成块后,处理异常会更加方便。示例中的rescue和always用于异常处理。
重用Playbook,解决重复编写Playbook的问题:
include - 重用单个Playbook脚本,使用起来简单 、直接。 role - 重用实现特定功能的Playbook文件夹,使用方法稍复杂、功能强大。Ansible还为role创建了一个共享平台 Ansible Galaxy, role是Ansible最为推荐的重用和分享Playbook 的方式。
使用include语句。下面是tasks/firewall_httpd_default.yml文件的内容:
---
# possibly saved as tasks/firewall_httpd_default.yml
  - name : insert firewalld rule for httpd
    firewalld : port=80/tcp permarent=true state=enabled immediate=yes
使用include引用上述文件:
tasks: - include: tasks/firewall_httpd_default.yml
在被引入的文件中传入参数:
---
# possibly saved as tasks/firewall_httpd_default.yml
  - name : insert firewalld rule for httpd
    firewalld : port={{ port }}/tcp permarent=true state=enabled immediate=yes
上述文件中定义了一个引入文件,使用该引入文件需要传入一个port参数。那如何传入参数呢?如下:
tasks: - include : tasks/firewall.yml port=80 - include: tasks/firewall.yml port=3260 - include : tasks/firewall.yml port=423
也可以传入字典参数:
tasks:
  - include : tasks/firewall.yml
    vars:
      wp_user: timmy
      ssh_keys :
        - keys/one . txt
        - keys/two.txt
或者也可以简写成一条:
tasks:
  - { include : wordpress.yml, wp_user: timmy, ssh_keys: [‘keys/one.txt‘, ‘keys/two.txt‘]}
当然,如果参数已经在Playbook中定义了,就不需要手动传入。
关于include要注意的事情:
在Ansible 1.9及之前的版本中,不能调用include里面的 handler 的,不过基于 Ansible 2.0+则可以调用 include 里面的 handler。所以在使用的时候要注意所安装的 Ansible 版本。 Ansible 允许全局(或者叫 Plays )加 include。然而这种使用方式并不推荐,因为它不支持嵌入 include,而且很多 Playbook 的参数也无法使用。 越来越强大而不稳定的 include - 为了使 include 功能更加强大,在每个新出的 Ansible 中都会添加一些新的功能。例如,在2.0 中添加了 include 动态名字的YAML,然而这样的用法有很多的限制,不够成熟,可能在更新版本的 Ansible 申又被去掉了,学习和维护成本很高。所以在需要使用更灵活的重用机制时,建议用下面介绍的 role。
使用role语句。它类似于编程语言中的“ Package”,可以重用一组文件,形成完整的功能。例如,安装和配置Apache时,既需要用tasks实现软件包的安装和模板的复制,也需要httpd.conf和index.html的模板文件,以及handler实现重启功能。这些文件都可以放在一个role里面,以供不同的Playbook 文件重用。
提倡在Ansible Playbook中使用 role,并且提供了一个分享 role 的平台 Ansible Galaxy。在 Galaxy 上可以找到别人写好的 role
如何定义一个role?通过遵循特定的目录结构来实现对 role 的定义。下面的目录结构定义了一个role(名字为 myrole):
roles/
    common/               # this hierarchy represents a "role"
        tasks/            #
            main.yml      #  <-- tasks file can include smaller files if warranted
        handlers/         #
            main.yml      #  <-- handlers file
        templates/        #  <-- files for use with the template resource
            ntp.conf.j2   #  <------- templates end in .j2
        files/            #
            bar.txt       #  <-- files for use with the copy resource
            foo.sh        #  <-- script files for use with the script resource
        vars/             #
            main.yml      #  <-- variables associated with this role
        defaults/         #
            main.yml      #  <-- default lower priority variables for this role
        meta/             #
            main.yml      #  <-- role dependencies
        library/          # roles can also include custom modules
        module_utils/     # roles can also include custom module_utils
        lookup_plugins/   # or other types of plugins, like lookup in this case
在Playbook文件site.yaml中调用了它:
---
- hosts: webservers
  roles:
    - myrole
在role中,不需要包含上述的所有目录,根据需要加入相应的目录即可:
如果 roles 文件 role/<role name>/tasks/main.yml 存在,则文件中列出的任务都将被添加到 Play 中。 如果文件 roles/<role name>/handlers/main.yml 存在,则文件中列出 handler 都将被添加到 Play 中。 如果文件 role/<role name>/vars/main.yml 存在,则文件中列出的变量都将被添加到 Play 中。 如果文件 role/<role name>/defaults/main.yml 存在,则文件中列出的变量都会被添加到 Play 中。 如果文件 role/<role name>/meta/main.yml 存在,则文件中列出的所有依赖的 role 都将被添加到Play 中。 此外,下面的文件不需要绝对或者是相对路径,等同于放在同一个目录下,可以直接使用即可。c opy 或者 scrip t 使用 roles/<role name>/files/下的文件;templae使用 roles/<role name>/temp l ates 下的文件;include 使用 roles/<role name>/tasks 下的文件;
在写 role 的时候,一般都要包含 role 人口文件 roles/x/tasks/main.yml 。其他的文件和目录可以根据需求选择是否加入。
如何定义一个带有参数的role?直接定义即可。对于目录结构如下的role:
main.yml
roles/
  myrole/
    tasks/
      main.yml
其中,main.yml的内容如下,使用{{ var_name }}定义的变量就可以了:
- name: use param
  debug: msg="{{ param }}"
如何使用这个带有参数的role呢?如下:
---
- hosts : webservers
  roles:
    - { role: myrole, param: ‘Call some_role for the 1st time‘ }
      { role: myrole, param: ‘Call some role for the 2nd time‘ }
或者:
---
- hosts: webservers
  roles:
    - role: myrole
      param: ‘Call xxxxxxxxxx‘
    - role: myrole
      param: ‘other‘
如何为role指定默认参数?对于如下的目录结构:
main.yml
roles/
  myrole/
    tasks/
      main.yml
  defaults/
    main.yml
在 roles/myrole/defaults/main.yrnl 中,使用YAML字典定义语法定义的 param 的值如下:
param: "I am the default value"
如上定义了 param 参数的默认值。
role 也可以与 when 一起使用:
---
- hosts: webservers
  roles:
    - { role : myrole, when: "ansible_os_family == ‘RedHat‘" }
    - role : my role
      when: "ansible os family == ‘RedHat‘"
如果roles和tasks同时出现,则优先级为:pre_tasks > role > tasks > post_tasks
通过为任务使用 tags 属性,可以实现执行部分任务。
例如,文件 example.yml 标记了两个标签 packages 和 configuration:
tasks:
  - yum: name={{ item }} state=installed
    with_items:
      - httpd
    tags:
      - packages
  - name: copy httpd.conf
    template: src=templates/httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
    tags:
      - configuration
  - name: copy index.html
    template: src=templates/index.html.j2 dest=/var/www/html/index.html
    tags:
      - configuration
对于如下的执行方式:
#!/bin/sh # 如果不加任何 tag 参数,那么会执行所有标签对应的任务 ansible-playbook example.yml # 利用关键字 tags 指定需要执行的部分任务 ansible-playbook example.yml --tags "packages" # 利用关键字skip-tags指定不执行对应的任务 ansible-playbook example.yml --skip-tags "configuration"
 如果把标签的名字定义为 always,并且没有明确指定不执行 always 标签,那么 always 标签所对应的任务就始终会被执行
 命令行中利用"--tags tagged"来执行所有标记了标签的任务,无论标记的标签的名字是什么
 命令行中利用"--tags untagged"来执行所有没有标签的任务
 命令行中利用"--tags all"来执行所有任务 
在 include 中使用标签:
- include: foo.yml tags: [web, foo]
在 role 中使用标签:
roles:
  - { role : webserver, port : 5000 , tags: [ ‘web‘, ‘foo‘ ]}
---
# 安装 apache 的 Play
- hosts: web
  remote user: root
  tasks:
   - name: ensure apache is at the latest version
     yum: pkg=httpd state=latest
# 安装 MySQL Server 的 Play
- hosts: lb
  remote user: root
  tasks:
   - name: ensure mysqld is at the latest version
     yum: pkg=mariadb state=latest
如上示例是某个 Playbook 脚本,单个 Playbook 通常就是可以被 Ansible 执行的 YAML 文件。上面的 Playbook 分别对两组主机进行了不同的操作,对每组主机的操作就称为一个 Play 。
 Setting the Environment (and Working With Proxies)
 Yum module fails when installing via URL if a yum http_proxy is required #18979 
可以在 tasks 中设置,可以在 Play 中设置,还可以使用变量:
- hosts: all
  remote_user: root
  # 在 play 中设置
  environment:
    http_proxy: http://proxy.example.com:8080
  # here we make a variable named "proxy_env" that is a dictionary
  vars:
    proxy_env:
      http_proxy: http://proxy.example.com:8080
  tasks:
    # 直接定义
    - name: Install cobbler
      package:
        name: cobbler
        state: present
      environment:
        http_proxy: http://proxy.example.com:8080
    # 使用变量
    - name: Install cobbler
      package:
        name: cobbler
        state: present
      environment: "{{ proxy_env }}"
某些语言环境的管理工具(NVM)需要使用环境变量,可以通过 environment 进行设置,参考 Setting the Environment (and Working With Proxies) 示例。
Ansible: Conditionally define variables in vars file if a certain condition is met
当条件不同时,我们需要为变量赋予不同参数:
test:
  var1: "{% if my_group_var %}value{% else %}other_value{% endif %}"
  var2: "{{‘value‘ if (my_group_var) else ‘other_value‘}}"
还可以根据条件引入不同的文件:
- include_vars: test_environment_vars.yml
  when: global_platform == "test"
- include_vars: staging_environment_vars.yml
  when: global_platform == "staging"
- include_vars: prod_environment_vars.yml
  when:
    - global_platform != "test"
    - global_platform != "staging"
Override hosts variable of Ansible playbook from the command line
在 Playbook 中的 hosts 参数用于指定目标主机,因此可以这样:
- hosts: "{{ variable_host | default(‘web‘) }}"
然后在命令行中使用 variable_host 参数传入主机名称(或者主机组名):
ansible-playbook server.yml --extra-vars "variable_host=x.x.x.x"
 Cache Plugins
 jsonfile – JSON formatted files 
使用 Cache 插件即可,比如我们想缓存到文件中:
ANSIBLE_CACHE_PLUGIN=jsonfile ANSIBLE_CACHE_PLUGIN_CONNECTION=/cache/saving/path ansible-playbook -i inventory.txt zabbix-agent.yaml
「Ansible」- yum / yum_repository
 「Ansible」- 使用变量
 「Ansible」- 使用 主机清单 管理主机
 ansible笔记(13):tags的用法
 Why is ansible notify not working? 
「Ansible」- 使用 Playbook 脚本 @20210207
原文:https://www.cnblogs.com/k4nz/p/14384303.html