如何在远程主机中执行多个任务?
很显然,我们可以将众多 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
#!/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 Pluginsjsonfile – JSON formatted files
使用 Cache 插件即可,比如我们想缓存到文件中:
ANSIBLE_CACHE_PLUGIN=jsonfile ANSIBLE_CACHE_PLUGIN_CONNECTION=/cache/saving/path ansible-playbook -i inventory.txt zabbix-agent.yaml