`playbook`是`ansible`实现批量自动化最重要的手段。在其中可以使用变量、引用、循环等功能,相比`ad-hoc`而言,其功能要强大的多。
### yaml简单示例
`ansible`的`playbook`采用`yaml`语法。以下是一个`yaml`格式的文件:
```
---
# Members in Bob's family
name: Bob
age: 30
gender: Male
wife:
name: Alice
age: 27
gender: Female
children:
- name: Jim
age: 6
gender: Male
- name: Lucy
age: 3
gender: Female
```
### ansible-playbook命令说明及playbook书写简单示例
以下是一个简单的`playbook`示例。该示例执行两个任务,第一个任务是执行一个`/bin/date`命令,第二个任务是复制`/etc/fstab`文件到目标主机上的`/tmp`下,它们分别使用了`ansible`的`command`模块和`copy`模块。
```
cat /tmp/test.yaml
---
- hosts: centos7
tasks:
- name: execute date cmd
command: /bin/date
- name: copy fstab to /tmp
copy: src=/etc/fstab dest=/tmp
```
书写好`playbook`后,使用`ansible-playbook`命令来执行。`ansible-playbook`命令的选项和`ansible`命令选项绝大部分都相同。但也有其特有的选项。以下是截取出来的帮助信息。
```
ansible-playbook --help
Usage: ansible-playbook playbook.yml
Options:
-e EXTRA_VARS,--extra-vars=EXTRA_VARS # 设置额外的变量,格式为key/value。-e "key=KEY",
# 如果是文件方式传入变量,则-e "@param_file"
--flush-cache # 清空收集到的fact信息缓存
--force-handlers # 即使task执行失败,也强制执行handlers
--list-tags # 列出所有可获取到的tags
--list-tasks # 列出所有将要被执行的tasks
-t TAGS,--tags=TAGS # 以tag的方式显式匹配要执行哪些tag中的任务
--skip-tags=SKIP_TAGS # 以tag的方式忽略某些要执行的任务。被此处匹配的tag中的任务都不会执行
--start-at-task=START_AT_TASK # 从此task开始执行playbook
--step # one-step-at-a-time:在每一个任务执行前都进行交互式确认
--syntax-check # 检查playbook语法
```
有以下几点需要注意下:
1. 默认情况下,`ansible-playbook`和`ansible`是一样的,都是同步阻塞模式,需要先在所有主机上执行完一个任务,才会继续下一个任务;
2. 在执行前会自动收集fact信息;
3. 从显示结果中可以判断出任务是否真的执行了,抑或者是因为幂等性而没有执行。
4. 每一个play都包含数个task,且都有响应信息play recap。
### playbook的内容
#### hosts和remoter_user
对于`playbook`中的每一个`play`,使用`hosts`选项可以定义要执行这些任务的主机或主机组,还可以使用`remote_user`指定在远程主机上执行任务的用户,实际上`remote_user`是`ssh`连接到被控主机上的用户,自然而然执行命令的身份也将是此用户。
例如:
```
---
- hosts: centos6,centos7,192.168.100.59
remote_user: root
tasks: XXXXXX
```
虽然在hosts处可以使用","分隔主机或主机组,但官方手册上并没有介绍该方法。除此之外,有以下几种指定主机和主机组的方式:
- all或*,表示inventory中的所有主机。
- :,取并集。例如"host1:host2:group1"表示2台主机加一个主机组。
- :&,取交集。例如"group1:&group2"表示两个主机组中都有的主机。
- :!,排除。例如"group1:!host1"表示group1中排除host1主机的剩余主机。
- 通配符,例如"web*.baidu.com"。
- 数字范围,例如"web[0-5].baidu.com"。
- 字母范围,例如"web[a-d].baidu.com"。
- 正则表达式以"~"开头。例如"~web\d\.baidu\.com"。
此外,在`ansible`命令行或`ansible-playbook`命令行中,可以使用`-l`选项来限定执行任务的主机。例如:
```
ansile centos -l host[1:5] -m ping
```
表示`centos`主机组中只有`host1`到`host5`才执行`ping`模块。
还可以在某个task上单独定义执行该task的身份,这将覆盖全局的定义。
```
---
- hosts: centos6,centos7,192.168.100.59
remote_user: root
tasks:
- name: run a command
shell: /bin/date
- name: copy a file to /tmp
copy: src=/etc/fstab dest=/tmp
remote_user: myuser
也支持权限升级的方式。
---
- hosts: centos6,centos7,192.168.100.59
remote_user: yourname
tasks:
- name: run a command
shell: /bin/date
- name: copy a file to /tmp
copy: src=/etc/fstab dest=/tmp
become: yes
become_method: sudo
become_user: root # 此项默认值就是为root,所以可省
```
从上面的示例可以看出`remote_user`实际上并不是执行任务的绝对身份,它只是`ssh`连接过去的身份,只不过没有指定`become`的时候,它正好就用此身份来运行任务。
#### task list
1. 特性
每个play都包含一个hosts和一个tasks,hosts定义的是inventory中待控制的主机,tasks下定义的是一系列task任务列表,比如调用各个模块。这些task按顺序一次执行一个,直到所有被筛选出来的主机都执行了这个task之后才会移动到下一个task上进行同样的操作。
需要注意的是,虽然只有被筛选出来的主机会执行对应的task,但是所有主机(此处的所有主机表示的是,hosts选项所指定的那些主机)都会收到相同的task指令,所有主机收到指令后,ansible主控端会筛选某些主机,并通过ssh在远程执行任务。也就是说,如果查看ansible-playbook -vvvv的信息,将会发现临时任务文件会通过sftp发送到所有的被控主机上,但是只有一部分被筛选(如果进行了筛选)的主机才会ssh过去并远程执行命令。
当某一台被控主机执行某个任务出错或失败时,它将会被移除出任务轮询列表。也就是说,对于某主机来说,某任务执行失败,后续的所有任务都不会再去执行。当然,这不会影响其他的主机执行任务(除非主机的任务之间有依赖关系)。
最重要的是,ansible中的task是**幂等性**的,多次执行不会影响那些成功执行过的任务。另外幂等性还表现在执行失败后如果修正了playbook再次执行,将不会影响那些原本已经执行成功的任务,即使是不同主机也不会影响。仅这方面而言,ansible对于排错来说是极其友好的。当然,某些特殊的模块或者特殊定义的task并不一定总是幂等的,例如最简单的,执行一个command或者shell模块的命令,它会重复执行。但也有办法使其变得幂等,以command和shell模块为例,它们有两个选项:creates和removes,它们分别表示被控主机上指定的文件存在(不存在)时就不执行命令。
2. 定义task的细节
(1). 可以为每个task加上name项,也可以多个task依赖于一个name。
例如下面的两个例子。从两个示例中可以看出,两个task其实都是属于一个name的,第二个task无需再使用name命名。
示例一:
```
tasks:
- name: do something to initialize mariadb
file: path=/mydata/data state=directory owner=mysql group=mysql mode=0755
- shell: /usr/bin/mysql_install_db --datadir=/mydata/data --user=mysql creates=/mydata/data/ibdata1
```
示例二:
```
tasks:
- name: echo var passed by nginx
shell: echo "{{ hi_var }}"
register: var_result
- debug: msg="{{ var_result.stdout }}"
```
实际上,name只是一种描述性语句,它可以定义在任何地方。例如,定义在play的顶端。
```
---
- name: start a play
hosts: localhost
tasks:
```
(2). 既然是task,那么必然会有其要执行的一个或多个任务,其本质是加载并执行ansible对应的模块。在playbook中,每调用的一个模块都称为一个action。
例如,定义一个确保服务是启动状态的task,有以下三种方法传递模块参数:
```
tasks:
- name: be sure the sshd is running
service: name=sshd state=started # 方法一: 定义为key=value,直接传递参数给模块
service: # 方法二: 定义为key: value方式
name: sshd
state: started
service: # 方法三: 使用args内置关键字,然后定义为key: value方式
args:
name: sshd
state: started
```
但要注意,ping模块、command和shell模块是不需要key=value格式的。对于ping命令,可以直接省略key=value。对于command和shell,只需要给定命令路径和要接上去的选项或参数即可,且无法使用上面的方法二。例如下面定义的是一个ntpdate命令,只需给定它的参数即可。
```
tasks:
- name: execute command ntpdate
shell: /usr/sbin/ntpdate ntp1.aliyun.com
- name: ping host
ping:
```
对于command或shell模块来说,有时候要考虑命令的返回状态码。如果要忽略非0状态码继续执行任务,可以使用以下两种方式:
```
tasks:
- name: ignore non_zero return code
shell: /usr/sbin/ntpdate ntp1.aliyun.com || /bin/true
```
或者
```
tasks:
- name: another way to ignore the non_zero return code
shell: /usr/sbin/ntpdate ntp1.aliyun.com
ignore_errors: true
```
(3). 如果action的key=value太多,导致内容太长,可以在上一行的缩进级别基础上继续缩进表示续行。
例如,下面的owner比src多缩进了4个空格。
```
tasks:
- name: Copy ansible inventory file to client
copy: src=/etc/fstab dest=/tmp
owner=root group=root mode=0644
```
(4).在action的value部分,可以引用已经定义的变量,可以是已定义好的自定义的变量,也可以是内置变量。变量相关内容见后文。
(5).使用include指令,可以将其他的playbook文件包含到此playbook文件中。
### notify和handler
ansible中几乎所有的模块都具有幂等性,这意味着被控主机的状态是否发生改变是能被捕捉的,即每个任务的changed=true或changed=false。ansible在捕捉到changed=true时,可以触发notify组件(如果定义了该组件)。
notify是一个组件,并非一个模块,它可以直接定义action,其主要目的是调用handler。例如:
```
tasks:
- name: copy template file to remote host
template: src=/etc/ansible/nginx.conf.j2 dest=/etc/nginx/nginx.conf
notify:
- restart nginx
- test web page
copy: src=nginx/index.html.j2 dest=/usr/share/nginx/html/index.html
notify:
- restart nginx
```
这表示当执行template模块的任务时,如果捕捉到changed=true,那么就会触发notify,如果分发的index.html改变了,那么也重启nginx(当然这是没必要的)。notify下定义了两个待调用的handler。handler主要用于重启服务或者触发系统重启,除此之外很少使用handler。以下是这两个handler的内容:
```
handlers:
- name: restart nginx
service: name=nginx state=restarted
- name: test web page
shell: curl -I http://192.168.100.10/index.html | grep 200 || /bin/false
```
handler的定义和tasks的定义完全一样,唯一需要限定的是handler中task的name必须和notify中定义的名称相同。
注意,notify是在执行完一个play中所有task后被触发的,在一个play中也只会被触发一次。意味着如果一个play中有多个task出现了changed=true,它也只会触发一次。例如上面的示例中,向nginx复制配置文件和复制index.html时如果都发生了改变,都会触发重启apache操作。但是只会在执行完play后重启一次,以避免多余的重启。
### 标签tag
可以为playbook中的每个任务都打上标签,标签的主要作用是可以在ansible-playbook中设置只执行哪些被打上tag的任务或忽略被打上tag的任务。
```
tasks:
- name: make sure apache is running
service: name=httpd state=started
tags: apache
- name: make sure mysql is running
service: name=mysqld state=started
tags: mysql
```
以下是ansible-playbook命令关于tag的选项。
```
--list-tags # list all available tags
-t TAGS, --tags=TAGS # only run plays and tasks tagged with these values
--skip-tags=SKIP_TAGS # only run plays and tasks whose tags do not match these values
```
### include和roles
如果将所有的play都写在一个playbook中,很容易导致这个playbook文件变得臃肿庞大,且不易读。因此,可以将多个不同任务分别写在不同的playbook中,然后使用include将其包含进去即可。而role则是整合playbook的方式。无论是include还是role,其目的都是分割大playbook以及复用某些细化的play甚至是task。
#### include
可以将task列表和handlers独立写在其他的文件中,然后在某个playbook文件中使用include来包含它们。除此之外,还可以写独立的playbook文件,使用include来包含这个文件。
也即是说,include可以导入两种文件:导入task、导入playbook。
1. 一种是任务列表式的文件(没有tasks或handlers指令),它只能在tasks或handlers指令的子选项处使用include包含。这种方式可以传递变量到被包含的文件中。
假设某个task列表文件/yaml/a.yaml内容如下:
```
---
- name: execute ntpdate
shell: /usr/sbin/ntpdate ntp1.aliyun.com
```
在同目录/yaml下有一个名为test.yaml的playbook(除了role,playbook中所有相对路径都是基于playbook的),在此playbook中使用include来包含它,如果使用相对路径将会包含同目录下的文件。
```
---
- hosts: centos7
tasks:
- include: a.yaml
```
可以在include的时候传递变量给对应的文件,这样在被包含的文件中就可以引用该变量的值。
```
---
- hosts: centos7
tasks:
- include: a.yaml sayhi="hello world"
```
或者
```
---
- hosts: centos7
tasks:
- include: a.yaml
vars:
sayhi: "hello world"
```
然后可以在被包含的文件a.yaml中使用该变量。例如:
```
---
- name: execute ntpdate
shell: /usr/sbin/ntpdate ntp1.aliyun.com
- name: say hi to world
debug: msg="{{ sayhi }}"
```
2. 另一种是include整个playbook文件,即include的动作是加载一个或多个play,所以写在顶级列表的层次。
```
- name: this is a play at the top level of a file
hosts: all
remote_user: root
tasks:
- name: say hi
tags: foo
shell: echo "hi..."
- include: load_balancers.yml sayhi="hello world"
- include: webservers.yml
- include: dbservers.yml
any other operations
```
需要说明的是,在ansible 2.4版本中,添加了includes和imports两种导入的方式,它们对静态和动态导入支持的更细化,而ansible 2.3及以前的include语句已经废弃,但仍可用。
#### roles
roles意为角色,主要用于封装playbook实现复用性。在ansible中,roles通过文件的组织结构来展现。
对于一个role,它的文件组织结构如下图所示:
![ansible role_1](http://ot8956ufo.bkt.clouddn.com/ansible_role_1.png)
首先需要有一个roles目录。同时,在roles目录所在目录中,还要有一个playbook文件,此处为nginx.yml,nginx.yml文件是ansible-playbook需要执行的文件,在此文件中定义了角色,当执行到角色时,将会到roles中对应的角色目录中寻找相关文件。
roles目录中的子目录是即是各个role。例如,此处只有一个名为nginx的role,在role目录中,有几个固定名称的目录(如果没有则忽略)。在这些目录中,还要有一些固定名称的文件,除了固定名称的文件,其他的文件可以随意命名。以下是各个目录的含义:
- tasks目录:存放task列表。若role要生效,此目录必须要有一个主task文件main.yml,在main.yml中可以使用include包含同目录(即tasks)中的其他文件。
- handlers目录:存放handlers的目录,若要生效,则文件必须名为main.yml文件。
- files目录:在task中执行copy或script模块时,如果使用的是相对路径,则会到此目录中寻找对应的文件。
- templates目录:在task中执行template模块时,如果使用的是相对路径,则会到此目录中寻找对应的模块文件。
- vars目录:定义专属于该role的变量,如果要有var文件,则必须为main.yml文件。
- defaults目录:定义角色默认变量,角色默认变量的优先级最低,会被任意其他层次的同名变量覆盖。如果要有defaults文件,则必须为main.yml文件。
- meta目录:用于定义角色依赖,如果要有角色依赖关系,则文件必须为main.yml。
所以,相对完整的role的文件组织结构如下图:
![ansible role_2](http://ot8956ufo.bkt.clouddn.com/ansible_role_2.png)
如果是多个role,则在roles同级目录下定义多个入站(作用类似于C语言的main函数)文件(如上面的nginx.yml),并在roles目录下创建对应的role目录即可。
![ansible role_3](http://ot8956ufo.bkt.clouddn.com/ansible_role_3.png)
当然,如果不是使用相对路径,那么role的文件结构就无所谓了,但是roles功能开发出来,就是为了解决文件混乱和playbook臃肿问题的。所以如果可以,尽量使用推荐的role文件结构。
另外,如果role中出现的task、var、handler等和单独定义的对象同名冲突了,则优先执行role中的内容。
以下是nginx role的入站文件nginx.yml的内容。
```
---
- hosts: centos7
roles:
- nginx
```