一、Playbook介绍
Playbook是Ansible的配置,部署,编排语言。他们可以被描述为一个需要希望远程主机执行命令的方案,或者一组IT程序运行的命令集合。
当执行一些简单的改动时ansible命令是非常有用的,然而它真的作用在于它的脚本能力。当对一台机器做环境初始化的时候往往需要不止做一件事情,这时使用playbook会更加适合。通过playbook你可以一次在多台机器执行多个指令。通过这种预先设计的配置保持了机器的配置统一,并很简单的执行日常任务。
Playbook还开创了很多特性,它可以允许你传输某个命令的状态到后面的指令,如你可以从一台机器的文件中抓取内容并附为变量,然后在另一台机器中使用,这使得你可以实现一些复杂的部署机制,这是ansible命令无法实现的。
在如右的连接中: ansible-examples repository,有一些整套的playbooks,它们阐明了上述的这些技巧。建议你在另一个标签页中打开它看看,配合本章节一起看。
YAML介绍
Ansible使用标准的YAML解析器,使用YAML文件语法即可书写playbook。
YAML是一个可读性高的用来表达资料序列的格式,YAML参考了其他多种语言,包括:XML、C语言、Python、Perl以及电子邮件格式RFC2822等。Clark Evans在2001首次发表了这种语言。
YAML Ain’t Makup Language,即YAML不是XML。不过,在开发这种语言时,YAML的意思是:Yet Another Makrup Language(仍是一种标记语言),其特性:YAML的可读性好、YAML和脚本原因的交互性好、YAML有一个一致的信息模型、YAML易于实现、YAML可以基于流来处理、YAML表达能力强,扩展性好。更多的内容及规范参见www.yaml.org。
Playbook组成结构
1 2 3 4 5 6 7 8 9 |
Inventory Modules Ad Hot Commands Playbooks Variables #变量元素,可传递给Tasks/Templates使用; Tasks #任务元素,即调用模块完成任务; Templates #模板元素,可根据变量动态生成配置文件; Hadlers #处理器元素,通常指在某事件满足时触发的操作; Roles #角色元素 |
执行一个Playbook
使用Playbook时通过ansible-playbook命令使用,它的参数和ansible命令类似,如参数-k(–ask-pass) 和-K (–ask-sudo) 来询问ssh密码和sudo密码,-u指定用户,这些指令也可以通过规定的单元写在playbook里。ansible-playbook的简单使用方法:
1 |
$ ansible-playbook /etc/ansible/roles/sites.yml |
也可以并行执行playbook,这里的示例是并行的运行playbook,并行的级别是10个进程:
1 |
$ ansible-playbook /etc/ansible/roles/sites.yml -f 10 |
以下是一个简单的playbook例子,其中仅包含一个 play(play可以有多个):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
--- - hosts: test vars: http_port: 80 max_clients: 200 remote_user: root tasks: - name: ensure apache is at the latest version yum: name=httpd state=latest - name: write the apache config file template: src=/srv/httpd.j2 dest=/etc/httpd.conf notify: - restart apache - name: ensure apache is running service: name=httpd state=started handlers: - name: restart apache service: name=httpd state=restarted |
下面,我们将分别讲解playbook语言的多个特性。
二、Playbook基础组件
- 主机(hosts)和用户(users)
Playbook中的每一个paly的目的都是为了让某个或某些主机以某个指定的用户的身份执行任务,hosts用于指定要执行任务的主机,其可以是一个或多个由冒号分隔主机组,这和前面章节ansible命令提到的的hosts使用一样的语法。remote_user则用于指定远程主机上的执行任务的用户,如上面示例中:
1 2 |
- hosts: test remote_user: root |
不过,remote_user也可用于各task中:
1 2 3 4 5 6 |
- hosts: test remote_user: root tasks: - name: test connection ping: remote_user: yourname |
也可以通过指定其通过sudo的方式在远程主机上执行任务,其可用于play全局或某task。此外,甚至可以在sudo时使用sudo_user指定sudo时切换的用户。
1 2 3 4 |
- hosts: test remote_user: root sudo: yes sudo_user: yourname |
如果你需要在使用sudo时指定密码,可在运行ansible-playbook命令时加上选项 --ask-sudo-pass
(-K)。如果使用sudo时,playbook疑似被挂起,可能是在sudo prompt处被卡住,这时可执行Control-C杀死卡住的任务,再重新运行一次。
- Tasks列表
Play的主体部分是task列表,task列表中的各任务按次序逐个在hosts中指定的主机上执行,即在所有主机上完成第一个任务后再开始第二个任务。在运行playbook 时(从上到下执行),如果一个host执行task失败,这个host将会从整个playbook的rotation中移除,如果发生执行失败的情况,请修正playbook 中的错误,然后重新执行即可。
Task的目的是使用指定的参数执行模块,而在模块参数中可以使用变量,模块执行时幂等的,这意味着多次执行是安全的,因为其结果一致。
对于command module和shell module,重复执行playbook,实际上是重复运行同样的命令。如果执行的命令类似于‘chmod’ 或者‘setsebool’这种命令,这没有任何问题,也可以使用一个叫做‘creates’的flag使得这两个module变得具有”幂等”特性 (不是必要的)。
每一个task必须有一个名称name,这样在运行playbook时,从其输出的任务执行信息中可以很好的辨别出是属于哪一个task的。如果没有定义name,‘action’的值将会用作输出信息中标记特定的task。
定义一个task,以前有一种格式: “action: module options” (可能在一些老的playbooks中还能见到),现在推荐使用更常见的格式:”module: options”,本文档使用的就是这种格式。
1 2 3 4 5 6 7 8 |
- hosts: test vars: http_port: 80 max_clients: 200 remote_user: root tasks: - name: ensure apache is at the latest version yum: name=httpd state=latest |
意思是,针对test组执行一个task使用root用户执行。而这个task任务是“ensure apache is at the latest version”,然后调用yum模块进行httpd的检测,这里的模块参数含义都跟前面在介绍常用模块时是一样的,所以详情可以看Ansible第三篇:常用模块介绍。
在众多模块中,只有command和shell模块仅需要给定一个列表而无需使用”key-value”格式,例如:
1 2 3 |
tasks: - name: disable selinux command: /sbin/setenforce 0 |
在使用command和shell模块时,我们需要关心返回码信息,如果有一条命令,它的成功执行的返回码不是0(ansible可能会终止运行),我们可以使用如下方式替代,强制返回成功:
1 2 3 |
tasks: - name: run this command and ignore the result shell: /usr/bin/somecommand || /bin/true |
或者使用ignore_error来忽略错误信息:
1 2 3 4 |
tasks: - name: run this command and ignore the result shell: /usr/bin/somecommand ignore_errors: True |
- 变量(variables)和模板(template)
在Playbook中可以直接定义变量(当然,前面我们也在inventory中添加变量),如下所示:
1 2 3 |
- hosts: test vars: - package: httpd |
其中vars是固定格式,而package就是变量名,httpd就是变量值。
但在定义变量名时有一些规范,也就是合法的变量名。在使用变量之前最好先知道什么是合法的变量名,变量名可以为字母,数字以及下划线。变量始终应该以字母开头,“foo_port”是个合法的变量名。”foo5”也是, “foo-port”, “foo port”,“foo.port” 和“12”则不是合法的变量名。
对于定义好的变量可以在task或者template中使用,但一般playbook中定义的变量都是给模板使用的,Ansible使用jijia2模板框架。这个后面会详解。现在只需要知道变量可以在task和template中使用即可。如下在task中调用变量:
1 2 3 |
tasks: - name: ensure apache is at the latest version yum: name={{ package }} state=latest |
在task或template中引用变量都是使用双花括号中间引入变量名即可,如:{{ VAR_NAME }}。
另外需要说明,在playbook中调用的变量不仅仅是在playbook中自定义的变量,也可以是Ansible中所定义的所有变量。比如说在Ansible中有一个setup模块,就是用来获取客户端所有信息的,如内存、CPU、IP、主机名、磁盘等等信息,这些信息都是key-value格式的。每当我们执行playbook时首先会需要执行setup模块,如下示例:
1 2 |
TASK [setup] ******************************************************************* ok: [10.0.60.143] |
这意味着什么呢?也就是说当我执行playbook时,其实是可以在task或template中去引用setup模块输出的变量。有哪些变量可用,你可以执行ansible命令看看输出结果:
1 2 3 4 5 6 7 8 9 10 |
$ ansible test -m setup -k 10.0.60.143 | SUCCESS => { "ansible_facts": { "ansible_all_ipv4_addresses": [ "10.0.60.143" ], "ansible_all_ipv6_addresses": [ "fe80::21d:d8ff:feb7:1e2b" ], .......忽略........ |
其中变量”ansible_all_ipv4_addresses”和”ansible_all_ipv6_addresses”都可以在task或者template中使用双花括号去引用的。
- Handlers
Handlers其实挺好理解的,就是在发生改变时执行提前定义好的操作。
上面我们曾提到过,模块具有”幂等”性,所以当远端系统被人改动时,可以重放playbook达到恢复的目的。playbook本身可以识别这种改动,并且有一个基本的event system(事件系统),可以响应这种改动。
(当发生改动时)’notify’actions会在playbook的每一个task结束时被触发,而且即使有多个不同的task通知改动的发生,’notify’ actions只会被触发一次。
这里有一个例子,当一个文件的内容被改动时,重启两个services:
1 2 3 4 |
- name: write the apache config file copy: src=/srv/httpd.conf dest=/etc/httpd/conf/httpd.conf notify: - restart httpd |
“notify”下列出的即是handlers。
Handlers也是一些task的列表,通过名字来引用,它们和一般的task并没有什么区别。Handlers是由通知者进行notify,如果没有被 notify,handlers不会执行。不管有多少个通知者进行了notify,等到play中的所有task执行完成之后,handlers也只会被执行一次。
这里是一个handlers的示例:
1 2 3 |
handlers: - name: restart apache service: name=apache state=restarted |
下面给一个安装服务然后使用Handlers的例子:
1 2 3 4 5 6 7 8 9 |
- hosts: test remote_user: root tasks: - name: ensure apache is at the latest version yum: name=httpd state=latest - name: write the apache config file copy: src=/srv/httpd.conf dest=/etc/httpd/conf/httpd.conf - name: ensure apache is running service: name=httpd state=started |
这个play就做了三个task,第一是检测httpd是否是最新版,如果不是就进行安装;第二就是从服务器copy一份定义好的配置文件到目标主机;第三就是启动httpd服务器。
首选需要在ansible服务器上提供一个/srv/httpd.conf配置文件,不然执行时会报错哦。然后使用ansible-playbook执行。
1 |
$ ansible-playbook apache.yml |
这里就不贴结果了,应该都没有什么问题。如果有报错看看报错信息自行解决,一般会报什么selinux或要安装libselinux-python,在客户端执行,或者指定定义到playbook中:
1 2 |
$ setenforce 0 $ yum install libselinux-python |
那么此时你更改了配置文件,是不是需要reload httpd或者restart httpd后你的配置文件才能生效。但是上面的这份配置就算你改完配置文件再次执行playbook,配置文件虽然复制过去了,但由于ansible幂等性配置是无法生效的。而Handlers就是为了解决此类问题而生的,我们可以给copy文件的那个task定义一个handlers,一旦有配置文件改动就调用提前定义的handlers进行处理。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- hosts: test remote_user: root tasks: - name: ensure apache is at the latest version yum: name=httpd state=latest - name: write the apache config file copy: src=/srv/httpd.conf dest=/etc/httpd/conf/httpd.conf notify: - restart httpd - name: ensure apache is running service: name=httpd state=started handlers: - name: restart httpd service: name=httpd state=restarted |
这个时候再次修改配置文件,然后执行playbook,当执行到第二个任务后检测到定义了notify,当整个play的task都执行完毕后,就会找handlers,匹配到名称后就会执行对应的模块。
注意:notify定义的restart httpd必须与handlers定义的name名称相同,不然就会报:not found in any of the known handlers
- Register
经常在playbook中,存储某个命令的结果在变量中,以备日后访问是很有用的。而‘register’就是用来决定把结果存储在哪个变量中,结果参数可以用在模版中,动作条目,或者when语句。像下面这个例子:
1 2 3 4 5 6 7 8 9 10 |
- name: test play hosts: all tasks: - shell: cat /etc/motd register: motd_contents - shell: echo "motd contains the word hi" when: motd_contents.stdout.find('hi') != -1 |
就像上面展示的那样,这个注册后的参数的内容为字符串’stdout’是可以访问。这个注册了以后的结果,如果像上面展示的,可以转化为一个list(或者已经是一个list),就可以在任务中的”with_items”中使用,“stdout_lines”在对象中已经可以访问了,当然如果你喜欢也可以调用“home_dirs.stdout.split()” , 也可以用其它字段切割:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- name: registered variable usage as a with_items list hosts: all tasks: - name: retrieve the list of home directories command: ls /home register: home_dirs - name: add home dirs to the backup spooler file: path=/mnt/bkspool/{{ item }} src=/home/{{ item }} state=link with_items: home_dirs.stdout_lines # same as with_items: home_dirs.stdout.split() |
提示与技巧
在playbook执行输出信息的底部,可以找到关于托管节点的信息。
1 2 |
PLAY RECAP ********************************************************************* 10.0.60.143 : ok=5 changed=2 unreachable=0 failed=0 |
其中:
ok:表示执行成功但没有做过任何变动的任务。
changed:表示执行成功但做过变动的任务。
unreachable:表示无法到达的任务。
failed:表示失败的任务。
如果你想看到执行成功的模块的输出信息,使用–verbose即可(否则只有执行失败的才会有输出信息)。
1 |
$ ansible-playbook playbook.yml --list-hosts |
如果安装了cowsay软件包,ansible playbook的输出已经进行了广泛的升级,可以尝试一下!
在执行一个playbook之前,想看看这个playbook 的执行会影响到哪些 hosts,你可以这样做:
1 |
$ ansible-playbook playbook.yml --list-hosts |
三、Playbook其他特性
- 条件测试
如果需要根据变量、facts(setup)或此前任务的执行结果来作为某task执行与否的前提时要用到条件测试。在Playbook中条件测试使用when子句。在task后添加when子句即可使用条件测试:when子句支持jinjia2表达式或语法,例如:
1 2 3 4 |
tasks: - name: "shutdown Debian" command: /sbin/shutdown -h now when: ansible_os_family == "Debian" |
或者多条件判断
1 2 3 4 5 6 |
tasks: - name: "shut down CentOS 6 systems" command: /sbin/shutdown -t now when: - ansible_distribution == "CentOS" - ansible_distribution_major_version == "6" |
意思就是如果对方的系统是CentOS并且版本是6就进行关机处理,ansible_os_family这个变量是从facts中取的。
也可以使用组条件判断:
1 2 3 4 5 |
tasks: - name: "shut down CentOS 6 and Debian 7 systems" command: /sbin/shutdown -t now when: (ansible_distribution == "CentOS" and ansible_distribution_major_version == "6") or (ansible_distribution == "Debian" and ansible_distribution_major_version == "7") |
另外也可以自定义变量,当值为某某时执行什么动作,如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
- hosts: all vars: exist: "True" tasks: - name: creaet file command: touch /tmp/test.txt when: exist | match("True") - name: delete file command: rm -rf /tmp/test.txt when: exist | match("False") |
意思是,当exist变量值为True时就创建文件,当值为False时就删除文件。
- 迭代
当有需要重复性执行的任务时,可以使用迭代机制。其使用格式为将需要迭代的内容定义为item变量引用,并通过with_items语句指明迭代的元素列表即可。例如:
1 2 3 4 5 6 7 |
tasks: - name: "Install Packages" yum: name={{ item }} state=latest with_items: - httpd - mysql-server - php |
事实上,with_items中可以使用元素还可为hashes,例如添加用户和组:
1 2 3 4 5 6 |
tasks: - name: "Add users" user: name={{ item.name }} state=present groups={{ item.groups }} with_items: - { name:'test1', groups:'wheel'} - { name:'test2', groups:'root'} |
其中引用变量时前缀item变量是固定的,而item后跟的键名就是在with_items中定义的字典键名。
迭代功能在做批量处理时非常有用,比如系统初始化时一般要安装很多初始化的包就可以使用迭代了。
关于循环的更多高级功能请看Ansible中文官方:http://ansible.com.cn/docs/playbooks_loops.html。
- tags
在一个playbook中,我们一般会定义很多个task,如果我们只想执行其中的某一个task或多个task时就可以使用tags标签功能了,格式如下:
1 2 3 4 5 |
tasks: - name: "Copy hosts file" copy: src=/etc/hosts dest=/etc/hosts tags: - hosts |
为复制hosts文件定义了一个tags,tags为hosts。那么在执行此playbook时可通过ansible-playbook命令使用–tags选项能实现仅运行指定的tasks而非所有的,如下:
1 |
$ ansible-playbook playbook.yml --tags="hosts" |
事实上,不光可以为单个或多个task指定同一个tags。playbook还提供了一个特殊的tags为always。作用就是当使用always当tags的task时,无论执行哪一个tags时,定义有always的tags都会执行。
1 2 3 4 5 |
tasks: - name: "Copy hosts file" copy: src=/etc/hosts dest=/etc/hosts tags: - always |
总结
当你了解了Playbook的以上基础知识后基本可以使用playbook来编排一些自动化任务了。上面介绍了playbook的Hos、Users、Tasks、Variables、Template等基本组成元素的使用,以及条件测试、迭代、tags等特性。下一章节就介绍playbook中非常重要的一个概念roles。另外对于playbook的template元素,不光可以简单地调用变量,并且支持各种操作符。在使用playbook时,模板也是一个不可或缺的功能。
http://docs.ansible.com/ansible/latest/playbooks_conditionals.html