1. playbook基本语法
Ansible的playbook文件格式为YAML语法,所以对于编写剧本的初学者,建议先对YAML语法结构有一定的了解,否则在运行playbook的时候会经常碰到语法错误。关于YAML的语法的详细介绍信息可以通过https://yaml.org/spec/1.2/spec.html网站进行了解。
安装部署Nginx服务的剧本编写案例:
[root@m01 ~]# cat nginx.yaml
---
- hosts: all
tasks:
- name: Install Nginx Package
yum: name=nginx state=present
- nane: Copy Nginx.conf
copy: src=./nginx.conf dest=/etc/nginx/nginx.conf mode=0644
对于以上playbook剧本代码信息,这里进行简单说明解释:
- 第1行表示该文件注释说明,YAML文件中通常使用三个短横线表示注释,也可以使用#。
- 第2行定义该playbook剧本管理的目标主机,all表示针对所有的主机,这个位置定义支持Ansible Ad-Hoc模式的所有参数。
- 第3行定义该playbook所有的任务集合信息,比如次剧本代码中定义了两个任务。
- 第4行定义一个任务的名称,非必须,建议根据实际任务命名。
- 第5行定义一个任务的具体操作动作,比如这里使用yum实现nginx软件包的安装。
- 第6到7行表示使用copy模块,将本地的nginx配置文件推送分发给其他所有被管理的主机,并且修改设置文件权限为644。
编写剧本主要需要注意两点规范:第一就是剧本内容组成规范;第二就是剧本编写语法规范。
1.1 playbook内容组成规范
Ansible的playbook由最基本的两个部分组成——hosts定义剧本所管理的主机信息,tasks定义所管理的主机需要执行的任务信息。
定义剧本的host部分可以有多种方式,常见的方式有以下几种:
方式一:定义所管理的主机IP地址
- hosts: 192.168.9.5
tasks:
---- 任务内容先省略 ----
方式二:定义所管理的主机名称信息
- hosts: backup_host
tasks:
---- 任务内容先省略 ----
方式三:定义所管理的主机组信息
- hosts: rsync_server
tasks:
---- 任务内容先省略 ----
- hosts: rsync_client
tasks:
---- 任务内容先省略 ----
方式四:定义所管理的多个主机信息
- hosts: 192.168.9.5, backup_host
tasks:
---- 任务内容先省略 ----
方式五:定义所管理所有主机信息
- hosts: all
tasks:
---- 任务内容先省略 ----
企业可根据自身需求,对以上常用方式进行自行扩展。以上定义剧本所管理的主机信息的几种方法有一个最重要的前提,即所管理的主机在Ansible主机清单文件中必须有相应定义,即默认/etc/ansible/hosts文件中必须有定义,否则剧本将不能直接管理相应主机。
定义剧本的tasks部分也可以有多种方式,常见的方式有以下几种:
方式一:采用变量格式设置任务信息
tasks:
- name: make sure apache is running
service: name=httpd state=running
当需要传入的参数列表过长时,可以将其分隔到多行
tasks:
- name: copy ansible inventory file to client
copy: src=/etc/ansible/hosts dest=/etc/ansible/hosts
owner=root group=root mode=0644
方式二:采用字典格式设置任务信息
tasks:
- name: copy ansible inventory file to client
copy:
src: /etc/ansible/hosts
dest: /etc/ansible/hosts
owner: root
group: root
mode: 0644
1.2 playbook编写语法规范
(1)注意剧本编写缩进规范
在编写剧本时,需要注意不同行信息之间有时需要有缩进关系,一般将两个空格作为一个缩进。
- hosts: oldboy
task:
- name: exec scripts
script: /server/scripts/oldboy.sh
(2)主机剧本编写字典规范
在编写剧本时,有时需要定义变量信息或设置模块参数的配置信息,可以采用字典格式进行设置,字典配置信息格式为:
key: value
key和value之间用冒号加空格进行分割
具体编写的剧本样例:
- hosts: oldboy
tasks:
- name: create file
file:
path: /oldboy/oldboy.txt
state: directory
mode: 644
owner: oldboy
group: oldboy
(3)主机剧本编写列表规范
在编写剧本时,剧本中定义的有些信息可能会重复出现,并且缩进关系一致,以及他们表达的意思也比较相近,这样不同行的信息就构成了列表,列表信息格式为:
- list01
- list02
- list03
短横线和列表信息中间有空格
2. playbook执行方式
剧本编写完成之后,需要进行运行,才能完成剧本的主机管理功能。在Ansible程序中,加载使用模块信息时,可以使用Ansible命令,加载执行剧本文件时,可以使用ansible-playbook命令。
ansible-playbook oldboy.yml
说明:可以使用相对路径加载剧本文件,也可以使用绝对路径加载剧本文件。
查看剧本执行时输出的详细信息:
ansible-playbook oldboy.yml --verbose
查看剧本执行时会影响哪些主机信息:
ansible-playbook oldboy.yml --list-hosts
执行playbook时指定加载的主机清单文件:
ansible-playbook oldboy.yml -i /etc/ansible/hosts
执行playbook时检查剧本语法是否正确:
ansible-playbook oldboy.yml --syntax-check
执行playbook时只是模拟执行,不会影响主机的配置:
ansible-playbook oldboy.yml -c
3. playbook的输出
剧本在执行过程中,会产生相应输出,根据输出的信息可以掌握剧本是否完整执行、每个执行过程是否正确,以及根据输出的错误提示信息,可以排查剧本编写中的逻辑问题。
剧本在执行时,任务中的每个Action会调用一个模块,然后在模块中检查当前系统状态并决定是否需要重新执行。
- 如果本地执行了,那么Action会得到返回值changed。
- 如果不需要执行,那么Action会得到返回值ok。
模块的执行状态的具体判断规则由各个模块自己决定和实现。例如,copy模块的判断方法是比较文件的checksum,copy模块的代码如下:
checksum_src = module.sha1(src)
...
checksum_dest = module.sha1(dest)
...
if checksum_src != checksum_dest or os.path.islink(b_dest):
...
changed = True
else:
changed = False
下面以一个copy文件的任务为例,展示在执行任务状态到底有什么不同的行为:
- hosts: oldboy
tasks:
- name: copy the /etc/hosts
copy: src=/etc/hosts dest=/etc/hosts
第一次执行,执行结果如下所示:
[root@m01 ~]# ansible-playbook copy_hosts.yaml
PLAY [oldboy] **************************************************************************
TASK [Gathering Facts] *****************************************************************
ok: [192.168.9.6]
ok: [192.168.9.5]
TASK [copy the /etc/hosts] *************************************************************
changed: [192.168.9.5]
changed: [192.168.9.6]
PLAY RECAP *****************************************************************************
192.168.9.5 : ok=2 changed=1 unreachable=0 failed=0
192.168.9.6 : ok=2 changed=1 unreachable=0 failed=0
第二次执行,执行结果如下所示:
[root@m01 ~]# ansible-playbook copy_hosts.yaml
PLAY [oldboy] **************************************************************************
TASK [Gathering Facts] *****************************************************************
ok: [192.168.9.6]
ok: [192.168.9.5]
TASK [copy the /etc/hosts] *************************************************************
ok: [192.168.9.6]
ok: [192.168.9.5]
PLAY RECAP *****************************************************************************
192.168.9.5 : ok=2 changed=0 unreachable=0 failed=0
192.168.9.6 : ok=2 changed=0 unreachable=0 failed=0
由于第一次执行copy_hosts.yaml时,已经复制过文件,因此Ansible会根据文件的状态避免重复复制。
接着更改192.168.9.5主机的/etc/hosts再执行,发现只有192.168.9.5的主机状态是changed,另外一台远程主机的状态是ok:
[root@m01 ~]# ansible-playbook copy_hosts.yaml
PLAY [oldboy] **************************************************************************
TASK [Gathering Facts] *****************************************************************
ok: [192.168.9.6]
ok: [192.168.9.5]
TASK [copy the /etc/hosts] *************************************************************
ok: [192.168.9.6]
changed: [192.168.9.5]
PLAY RECAP *****************************************************************************
192.168.9.5 : ok=2 changed=1 unreachable=0 failed=0
192.168.9.6 : ok=2 changed=0 unreachable=0 failed=0
通过以上执行剧本输出的信息,可以将剧本执行过程输出的信息总结为三个部分,具体说明参见下表。
4. playbook扩展配置
4.1 playbook设置变量功能
在剧本中可以通过设置变量信息,实现相应参数的配置功能,在某些场景下,可以简化对剧本的修改调整。在playbook中,常用的几种变量设置方法如下:
1)在playbook中用户自定义的变量。
2)用户无须定义,Ansible会在执行playbook之前去管理主机上收集关于远程主机系统的信息的变量。
3)在文件模板中,可以直接使用上述两种变量。
4)把任务的运行结果作为一个变量来使用,这个叫作注册变量。
5)为了使playbook更灵活,通用性更强,允许用户在执行playbook时传入变量的值,这个时候就需要用到额外变量。
(1)在playbook中用户自定义的变量
用户可以在playbook中,通过vars关键字自定义变量,之后再用{{}}调用即可。
- playbook中定义和变量的方法
例如:下面的例子中,用户定义变量为http_port,其值为80。在tasks下的firewalld中,可通过{{ http_port }}调用该变量。
- hosts: web
vars:
http_port: 80
remote_user: root
tasks:
- name: insert firewalld rule for httpd
firewalld: port={{ http_port }}/tcp permanent=true state=enabled imme-diate=yes
- 将变量配置在单独文件中
当变量较多的时候,或者变量需要在多个playbook中重用的时候,可以把变量放到一个单独的文件中,之后通过关键字“var_files”可将该变量引用到playbook中。使用变量的方法和在文件中定义变量的方法相同:
- hosts: web
vars_files:
- vars/server_vars.yml
remote_user: root
tasks:
- name: insert firewalld rule for httpd
firewalld: port={{ http_port }}/tcp permanent=true state=enabled imme-diate=yes
变量文件/vars/server_vars.yml的内容为:
http_port: 80
- 定义和使用复杂的变量
在某些场景中需要使用的变量的值不是简单的字符串或者数字,而是一个对象。对象的定义语法如下,格式为YAML的字典格式:
foo:
field1: one
field2: two
访问复杂变量中的子属性,可以利用中括号或者点号:
foo['field1']
foo.field1
(2)远程主机的系统变量(Facts)
Ansible会通过模块“setup”来搜集主机的系统信息,这些搜集到的系统信息称为Facts。每个playbook在执行前都会默认执行setup模块,所以这些Facts信息可以直接以变量的形式使用。
可以通过在命令行中调用setup模块命令,查看所有可以调用的Facts变量信息:
ansible all -m setup -u root
在剧本中调用收集到的Facts变量信息:
- hosts: all
user: root
tasks:
- name: print system info
debug: msg={{ ansible_os_family }}
- name: install git on Debian linux
apt: name=git state=installed
when: ansible_os_family == "Debian"
- name: install git on RedHat linux
yum: name=git state=installed
when: ansible_os_family == "RedHat"
- 使用复杂的Facts变量
一般在系统中搜集到如下信息时,复杂的、多层级的Facts变量是如何进行调取的呢?
"ansible_eth0": {
"active": true,
"device": "eth0",
"ipv4": {
"address": "10.0.0.200",
"broadcast": "10.0.0.255",
"netmask": "255.255.255.0",
"network": "10.0.0.0"
},
}
...
可以通过下面的两种方式访问复杂变量中的子属性:
- 中括号调用
{{ ansible_eth0["ipv4"]["address"] }}
- 点号调用
{{ ansible_eth0.ipv4.address }}
- 关闭Facts
搜集Facts信息会消耗额外的时间,如果不需要Facts信息,则可以在playbook中,通过关键字gather_facts来控制是否搜集远程系统的信息。如果不搜集系统信息,那么上面的Facts变量就不能在该playbook中使用了:
- hosts: oldboy
gather_facts: no
通过setup模块搜集主机信息时,会发现很多可以作为剧本的facts变量信息,以下为企业中常用的Facts变量信息说明。
(3)文件模板中使用的变量
template模块在Ansible中十分常用,而它在使用中并没有显式地指定template文件中的值,所以有时候用户会对template文件中的变量感到困惑,所以这里强调以下它的变量的使用。
- template中变量的定义
在playbook中定义的变量,可以直接在template中使用,同时Facts变量可以直接在template中使用,当然在Inventory中定义的Hosts和Group变量也是如此。所有在playbook中可以访问的变量,都可以在template文件中使用。
下面的playbook脚本中使用了template模块来复制文件index.html.j2,并且替换index.html.j2中的变量为playbook中定义的变量值。
- hosts: web
vars:
http_port: 80
defined_name: "Hello My name is oldboy"
remote_user: root
tasks:
- name: write the default index.html file
template: src=templates/index.html.j2 dest=/var/www/html/index.html
- template中变量的使用
在上面的剧本举例中,index.html.j2模板文件直接使用了以下变量信息:
系统定义变量:{{ ansible_hostname }} {{ ansible_default_ipv4.address }}
用户定义变量:{{ defined_name }}
index.html.j2文件的内容如下:
<html>
<title>Demo</title>
<boby>
<div class="""block" style="height: 99%;">
<div class="centered">
<h1>#46 Demo {{ defined_name }}</h1>
<p>Served by {{ ansible_hostname }} {{{ ansible_default_ipv4.address }}}.</p>
</div>
</div>
</body>
</html>
(4)运行结果注册变量
把任务的执行结果当作一个变量的值也是可以的。这个时候就需要用到“注册变量”,即把执行结果注册到一个变量中,待后面的任务使用。把执行结果注册到变量中的关键字时register,使用方法如下:
- hosts: web
tasks:
- shell: ls
register: result
ignore_errors: True
- shell: echo "{{ result.stdout }}"
when: result.rc == 5
- debug: msg=""{{ result.stdout }}
注册变量经常和debug模块一起使用,这样可以得到更多的关于执行错误的信息,以帮助用户调试剧本内容。
(5)用命令行传递变量信息
为了使playbook更灵活,通用性更强,允许用户在执行的时候传入指定变量的值,此时就需要用到“额外变量”。
- 定义命令变量
在oldboy.yml文件中,hosts和user都定义为变量,它们需要从命令行传递变量值。如果在命令行中不传入值,那么执行playbook是会报错的:
- hosts: '{{ hosts }}'
remote_user: '{{ user }}'
tasks:
- ...
当然也可以直接在playbook中定义变量信息。例如下面的剧本,如果在命令行中传入新的值,那么会覆盖playbook中的值,未在命令行中的传入值也不会报错:
- hosts: localhost
remote_user: root
vars:
test_name: "Value in playbook file"
tasks:
- debug: msg=""{{ test_name }}"
- 使用命令行变量
ansible-playbook oldboy.yml --extra-vars "hosts=web user=root"
还可以用JSON格式传递参数:
ansible-playbook oldboy.yml --extra-vars "{'hosts':'web', 'user':'root'}"
4.2 playbook逻辑控制语句
在playbook中也可以设置一些逻辑控制语句(类似于Shell脚本中的逻辑语句信息),使剧本配置方式更加灵活多样,在剧本中常用的逻辑语句的参数如下:
- when:条件判断语句,类似编程语言中的if。
- loop:循环语句,类似编程语言中while。
- block:把几个任务组成一个代码块,以便针对一组操作的异常进行处理。
(1)条件判断语句when
- when的基本用法
有时候很可能需满足特定条件才执行某一个特定的步骤,例如在某一个特定版本的系统中安装软件包,或者只在磁盘空间不足的文件系统上执行清理操作。这些操作在playbook中用when语句实现。
若远程主机为Debian Linux系统,则立刻关闭主机系统:
tasks:
- name: "shutdown Debian system"
command: /sbin/shutdown -t now
when: ansible_os_family == "Debian"
进行判断的方式有多种。
1)简单方式:
command: echo oldboy
when: ansible_os_family == "centos"
说明:当指定条件满足时,执行模块动作
2)取反方式:
command: echo oldboy
when: ansible_os_family != "centos"
说明:当指定条件不满足时,执行模块动作
3)多个条件:
command: echo oldboy
when: ansible_os_family == " centos" and ansible_hosts == "web01"
说明:当多个条件同时满足时,执行模块动作
command: echo oldboy
when: ansible_os_family == "centos" or ansible_hosts == "web01"
说明:当多个条件其中之一满足时,执行模块动作
(2)逻辑循环语句loop
- 标准循环
为了保持简洁,重复的任务可以用以下简写方式:
- name: add server users
user: name={{ item }} state=present group=oldboy
with_item:
- testuser1
- testuser2
如果在变量文件中或者“vars”区域定义了一组列表变量somelist,也可以进行如下配置:
vars:
somelist: ["testuser1", "testuser2"]
tasks:
- name: add server user
user: name={{ item }} state=present groups=oldboy
with_items: "{{ somelist }}"
“with_item”用于迭代的list类型变量,不仅支持简单的字符串列表,也可以支持哈希列表,那么可以用以下方式来引用子项:
- name: add server user
user: name={{ item.name }} state=present groups={{ item.groups }}
with_items:
- { name: 'testuser1', groups: 'test1' }
- { name: 'testuser2', groups: 'test2' }
注意:如果同时使用when和with_items,那么when声明会针对每个条目单独判断一次。
- 嵌套循环
循环也可以嵌套,用[]访问内存和外层的循环:
- name: give users access to multiple databases
mysql_user: name={{ item[0] }} priv={{ item[1] }}.*:ALL append_privs=yes password=foo
with_nested:
- [ 'alice', 'bob' ]
- [ 'clientdb', 'employeedb', 'providerd' ]
或者用点号(.)访问内存和外层的变量:
- name: give users access to multiple databases
mysql_user: name={{ item.0 }} priv={{ item.1 }}.*:ALL append_privs=yes password=foo
with_nested:
- [ 'alice', 'bob' ]
- [ 'clientdb', 'employeedb', 'providerd' ]
4.3 playbook调试功能配置
编写剧本时,可以加入一些调试功能,以便在剧本执行报错时进行调试修改,常用的调试功能有以下几种:
- ignore_errors:忽略剧本执行过程中的报错信息。
- tags:给剧本打标签。
(1)剧本执行错误忽略功能
在执行剧本时,由于Ansible具有串行执行特性,即一个任务执行成功,才会执行下一个任务,如果一个剧本中的某任务执行失败了,就会停止剧本的执行。在引入剧本执行报错忽略功能后,可以先忽略有些可能有错误的任务,确保剧本中的其他任务执行完毕,之后再研究出现错误的任务。
实现忽略错误的剧本信息为:
tasks:
- name: install software
shell: yum install -y rsync
- name: create user
shell: useradd oldboy
ignore_errors: yes
- name: boot server
shell: systemctl start rsyncd
(2)剧本标签功能
在某些场景中编写剧本任务的步骤会非常烦琐复杂,在进行测试时,某一个任务很可能出现问题,从而需要对剧本进行调试,而剧本调试完毕后,重新测试时,又会反复执行已经成功执行的任务,影响剧本的调试效率。实际上,可以利用剧本标签功能只执行某个剧本任务。
添加标签功能的剧本信息如下:
tasks:
- name: create file info
file: path:/tmp/this_is_{{ ansible_hostname }}_file state=touch
when: (ansible_hostname == "nfs01") or (ansible_hostname == "backup")
tags: t1
- name: install httpd
yum: name=httpd state=installed
when: (ansible_all_ipv4_addresses == ["192.168.9.5","192.168.9.6"])
tags: t2
以上剧本包含了两个任务信息,分别对它们做了标记,可以利用Ansible执行剧本的命令参数,以进行如下操作。
执行指定标签任务的命令:
ansible-playbook test_tags.yml -t t1
跳过指定标签任务的命令:
ansible-playbook test_tags.yml --skip-tags t2
4.4 playbook触发功能
(1)什么是剧本触发功能(handlers)
每个主流的编程语言都有Event机制,而handlers就是playbook的Event。
handlers里面的每一个触发器信息都是对模块的一次调用。而handlers与任务不同,任务会默认地按照定义顺序执行,而handlers则不会,它需要在任务中调用,才有可能得到执行。
任务表中的任务都是有状态的:changed或者ok。在Ansible中,只有在任务的执行状态为changed时,才会执行该任务调用的handler。这也是handler与普通的Event机制不同的地方。
(2)剧本触发功能应用场景
如果在任务中修改了Apache的配置文件,那么需要重启Apache。如果还安装了Apache的插件,那么还需要重启Apache。像这样的应用场景,重启Apache就可以设计成一个handler。
一个handler最多只执行一次,并且是在所有的任务都执行完之后再执行。如果有多个任务调用(notify)同一个handler,那么只执行一次。
在下面的例子中Apache重启只执行一次:
- hosts: lb
remote_user: root
vars:
random_number1: "{{ 10000| random }}"
random_number2: "{{ 10000000| random }}"
tasks:
- name: copy the /etc/hosts to /tmp/hosts.{{ random_number1 }}
copy: src=/etc/hosts dest=/tmp/hosts.{{ random_number1 }}
notify:
- call in every action
- name: copy the /etc/hosts to /tmp/hosts.{{ random_number2 }}
copy: src=/etc/hosts dest=/tmp/hosts.{{ random_number2 }}
notify:
- call in every action
handlers:
- name: call in every action
debug: msg=""call in every action, but execute only one time"
只有是changed状态的任务才会触发handler的执行。
下面的剧本执行了两次,执行结果是不同的。
- 第一次执行时:
任务的状态都是changed,会触发两次handler。 - 第二次执行时:
第一个任务的状态是ok,因而不会触发handlers “call by /tmp/hosts”;
第二个任务的状态是changed,触发了handler “call by /tmp/hosts random_number”。
测试代码如下:
- hosts: lb
remote_user: root
vars:
random_number: "{{ 10000| random }}"
tasks:
- name: copy the /etc/hosts to /tmp/hosts
copy: src=/etc/hosts dest=/tmp/hosts
notify:
- call by /tmp/hosts
- name: copy the /etc/hosts to /tmp/hosts.{{ random_number }}
copy: src=/etc/hosts dest=/tmp/hosts.{{ random_number }}
notify:
- call by /tmp/hosts.random_number
handlers:
- name: call by /tmp/hosts
debug: msg=""call first time"
- name: call by /tmp/hosts.random_number
debug: msg=""call by /tmp/hosts.random_number"
(3)按定义的顺序执行触发功能
handler是按照定义的顺序执行的,而不是按照所安装的任务中调用的顺序执行的。下面的例子定义的顺序是1>2>3,调用的顺序是3>2>1,实际执行顺序是1>2>3。
- hosts: lb
remote_user: root
gather_facts: no
vars:
random_number1: "{{ 10000| random }}"
random_number2: "{{ 10000000| random }}"
tasks:
- name: copy the /etc/hosts to /tmp/hosts.{{ random_number1 }}
copy: src=/etc/hosts dest=/tmp/hosts.{{ random_number1 }}
notify:
- define the 3nd handler
- name: copy the /etc/hosts to /tmp/hosts.{{ random_number2 }}
copy: src=/etc/hosts dest=/tmp/hosts.{{ random_number2 }}
notify:
- define the 2nd handler
- define the 1nd handler
handlers:
- name: define the 1nd handler
debug: msg""" define the 1nd handler""
- name: define the 2nd handler
debug: msg""" define the 2nd handler""
- name: define the 3nd handler
debug: msg""" define the 3nd handler""
4.5 playbook整合
在编写多个剧本信息时,有时会实现自动化批量管理,需要执行多个剧本,这时可以将需要执行的多个剧本的信息进行整合,省去利用ansible-playbook命令逐个加载执行剧本的低效工作。实现剧本整合的方式常见的有两种。
1. 只定义单个剧本任务信息
在整合多个剧本信息时,可以只在每个剧本中定义具体的任务信息,而无须定义hosts管理的主机信息,在汇总剧本中灵活调用整合多个剧本,并定义需要执行任务的hosts主机信息,具体的剧本配置信息如下:
- hosts: all
remote_user: root
tasks:
- include_tasks: f1.yml
- include_tasks: f2.yml
2. 直接将编写好的剧本进行整合
在整合剧本信息时,比较简单的方式就是找到相应剧本,直接利用import_playbook参数进行整合,在执行时会按照整合加载的顺序,执行每一个剧本,具体剧本配置信息如下:
- import_playbook: base.yml
- import_playbook: rsync.yml
- import_playbook: nfs.yml