「Ansible」- 使用 Playbook 脚本

  CREATED BY JENKINSBOT

如何在远程主机中执行多个任务?

很显然,我们可以将众多 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脚本中挨个命令执行的方式。

使用 Playbook 脚本(快速开始)

第一步、编写 Playbook 脚本

---
- 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

第二步、执行 Playbook 脚本

使用 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)

使用 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)”;

name 为动作名;
yum /
template /
service 为模块名;

指定任务列表,对于 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

回调通知(handlers)

问题:在执行多个不同的任务后,我们可能需要执行某些特定操作。比如,在某个任务中修改 Apache 配置文件,在另个任务中安装 Apache 插件,这两个任务都需要重启 Apache 服务。

方案:像上面的这样场景,重启 Apache 就可以设计成“回调通知”(是软件编程的概念),即执行某些操作后触发某个事件。在Ansible中,通过使用 handlers 指令实现。

简单 handler 示例

---
- 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中的动作是按照定义的顺序执行的,与收到的通知顺序是无关的。

逻辑控制(when、with_xxx、block)

在 Playbook 中,也可以逻辑控制语句:
when – 类似于编程语言中的 if 关键字
with_x – 类似于编程语言中的 while 关键字
block – 类似于编程语言中的代码块。可以把几个任务组成一个代码块,以便于针对一组操作的异常进行处理等操作。

when

使用条件判断语句”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' }

with_xxx

使用循环(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

使用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用于异常处理。

脚本复用(include vs role)

重用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)

通过为任务使用 tags 属性,可以实现执行部分任务。

第一步、使用 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

第二步、使用 tags 属性

对于如下的执行方式:

#!/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 / role

在 include 中使用标签:

- include: foo.yml
  tags: [web, foo]

在 role 中使用标签:

roles:
  - { role : webserver, port : 5000 , tags: [ 'web', 'foo' ]}

Playbook vs Play

---
# 安装 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"

如何为特定主机执行 Playbook 脚本?

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"

如何缓存 Facts 信息?

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笔记(13):tags的用法
Why is ansible notify not working?