ansible基础-playbooks
1. playbooks介绍
如果说ansible的modules是工具,inventory配置文件是原材料,那么playbook就是一封说明书,这里会记录任务是如何如何执行的,当然如果你愿意,这里也可以定义一些变量、连接参数等等。
playbook可以由单个或者多个play组成。
单个play示例:
---
- hosts: webservers
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
上面的示例中所有的任务作用于webservers所包含的主机,通过root用户连接到目的主机,对apache服务进行了安装、配置、启动等操作,当配置文件有更改时,会触发hanlders里的重启apache操作,vars里定义的“http_port”和 “ max_clients”将会在模版文件“/srv/httpd.j2”中遵循Jinja2语法被使用到。
playbooks是使用yaml语法格式,所以看起来比较通俗易懂。通过上面的示例可以看出一个play可以包含如下内容:
- hosts:主机组,后面定义的task将作用于该主机组的所有主机
- vars:变量定义,在后面的task中可以引用
- remote-user:连接参数,例如remote-user,become,become-user等等,这些参数将会覆盖ansible.cfg配置文件里的参数
- tasks:任务,可以看作很多modules的集合,这些modules可以使用vars定义的变量
- handlers:触发才会执行的task,很多情况下,当其他task被执行并且状态有改变后,我们希望会触发一些任务,那些被触发的任务可以写在这里
一个playbooks也可以编写多个play,示例如下:
---
- hosts: webservers
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
- hosts: databases
remote_user: root
tasks:
- name: ensure postgresql is at the latest version
yum:
name: postgresql
state: latest
- name: ensure that postgresql is started
service:
name: postgresql
state: started
上面的示例中,第一个play通过root用户连接到webservers主机组,进行了apache服务的安装和配置操作;第二个play通过root用户链接到databases主机组,进行了数据库的安装和启动操作。
2.可重复利用的playbooks
上一章节中我们说到,一个playbooks可以放置多个play,一个play里面可以有多个tasks(modules),但是,当要管理的资源越来越多时,我们发现将所有play都写在一个yml文件里会很臃肿,不好维护。
此时我们可以通过“import_playbook”方法引用其他的playbooks文件;
此时我们可以通过“import_playbook”方法引用其他的playbooks文件;使用“import_tasks”、“include_tasks”、“import_role”、“include_role”、“roles”引用其他的tasks文件。
import_playbook
比较简单,直接上示例,文件main.yml:
- import_playbook: webservers.yml
- import_playbook: databases.yml
上述示例中使用import_playbook将webservers.yml和databases.yml文件里的play引用到main.yml,和直接将两个文件里的内容直接粘过来是一样的效果,执行顺序自然也会按照play定义的顺序执行。
import_tasks和include_tasks
可以参考笔者之前写的文章 ansible中include_tasks和import_tasks
import_role和include_role
ansible2.3引入了include_role,ansible 2.4版本后,新增了import_role,通过这两个方法可以在tasks里面导入role,示例如下:
---
- hosts: webservers
tasks:
- debug:
msg: "before we run our role"
- import_role:
name: example
- include_role:
name: example
- debug:
msg: "after we ran our role"
从上面的示例可以看出,在tasks中使用import_role和include_role方法导入了role example,role里面的task会按顺序执行。
当然我们也可以引用的同时定义变量:
---
- hosts: webservers
roles:
- common
- role: foo_app_instance
vars:
dir: '/opt/a'
app_port: 5000
- role: foo_app_instance
vars:
dir: '/opt/b'
app_port: 5001
也可以给role打tag:
---
- hosts: webservers
tasks:
- import_role:
name: foo
tags:
- bar
- baz
使用条件语句(后面有详细写when语句用法):
---
- hosts: webservers
tasks:
- include_role:
name: some_role
when: "ansible_os_family == 'RedHat'"
roles
除了使用import_role和include_role导入role,我们也可以直接使用roles方法来导入,示例如下:
--- - hosts: webservers roles: - common - webservers ###OR - hosts: webservers roles: - role: '/path/to/my/roles/common'
和import和include方法相比,roles方法是仅仅可以导入role类型的playbook,而上述两个方法可以在其他tasks中穿插一些role类型的playbook。
在生产中,roles是比较常用的所以后面的章节会有对roles的单独讲解,这里就不在展开了。
通过以上的总结,我们可以看出,在ansible里,如果我们想复用其他文件的playbooks,可以使用include、import、roles三种方法,据我所知也只有这三种方法。
2.1 动态和静态
至此,我们知道了可以使用import*和include*导入其他的playbooks,那么这两者的区别是什么呢?
在ansible 2.4版本中引入了dynamic和static的概念,在这之前只能使用include来导入其他的tasks文件,现在include也能用,但官方在考虑在未来版本废弃掉。
静态指所有import*的方法,动态指include*的方法。
关于动态和静态的两点区别,总结如下:
- import_tasks(Static)方法会在playbooks解析阶段将父task变量和子task变量全部读取并加载
- include_tasks(Dynamic)方法则是在执行play之前才会加载自己变量
- include_tasks方法调用的文件名称可以加变量
- import_tasks方法调用的文件名称不可以有变量
具体介绍可以参考笔者之前写的文章ansible中include_tasks和import_tasks
3.playbooks变量
3.1定义变量
ansible中可以定义变量的地方可以有很多,在这里主要写下playbooks里面的变量定义,其他部分的变量会在后续的“ansible基础-变量”详细阐述。
变量的定义通常使用YAML语法格式,示例如下:
---
vars:
field1: one
字典变量:
---
foo:
field1: one
field2: two
play中定义全局变量
---
- hosts: webservers
vars:
http_port: 80
上面示例中 http_port参数可以在这个play中的tasks、playbooks、roles中引用。
tasks中定义变量
当然,我们也可以在某个task中定义局部变量,这个变量只能在本task内使用,示例如下:
---
- hosts: node1
gather_facts: false
tasks:
- name: Use var debug
vars:
- name: weimeng
- age: 26
debug:
var: name,age
include和import中定义变量
include_role定义变量只在被引用的role中生效:
- hosts: node1
gather_facts: false
tasks:
- include_role:
name: role_A
vars:
age: 24
include_tasks定义变量只在被引用的task中生效:
tasks:
- import_tasks: wordpress.yml
vars:
wp_user: timmy
- import_tasks: wordpress.yml
vars:
wp_user: alice
- import_tasks: wordpress.yml
vars:
wp_user: bob
roles中定义变量
playbook引用role也可以直接定义变量,示例如下:
---
- hosts: webservers
roles:
- role: bar
tags: ["foo"]
# using YAML shorthand, this is equivalent to the above
- { role: foo, tags: ["bar", "baz"] }
注册变量
在playbook中,我们可以将一个task的执行结果注册为一个变量,供另外一个task使用。例如:
---
- hosts: webservers
roles:
- role: bar
tags: ["foo"]
# using YAML shorthand, this is equivalent to the above
- { role: foo, tags: ["bar", "baz"] }
将一个task的结果注册为一个变量,然后通过这个变量判断另外一个task是否执行,这是注册变量很常用的方式。
通过命令行定义变量
在我们执行playbook时可以在命令行中指定自定义变量,例如:
ansible-playbook release.yml --extra-vars "version=1.23.45 other_variable=foo”
在同一个scope内,如果与其他地方的变量冲突,命令行指定的参数优先级最高。
3.2引用变量文件
上面介绍了在playbook中如何定义变量,那么变量能否和task一样定义在单独的yml文件内,然后使用类似于include_tasks的语句引用过来呢? 答案是肯定的。
在playbook内引用变量文件使用的是vars_files:语句,示例如下:
---
- hosts: all
remote_user: root
vars:
favcolor: blue
vars_files:
- /vars/external_vars.yml
tasks:
- name: this is just a placeholder
command: /bin/echo foo
在变量文件/vars/external_vars.yml中,我们只需要使用YAML语法格式进行变量定义即可。
3.3 facts
除了我们自定义的变量,ansible还支持另外一种变量,这个变量类似于puppet的facter,ansible叫做fact。
ansible的fact会根据目的主机的系统信息生成一个json格式的变量集合,我们在play中可以直接引用。例如比较常用的变量:ip地址、主机名、操作系统类型等等。
puppet的facter依赖ruby的一个安装包,通过ruby程序收集系统信息,而ansible的fact是通过python程序收集。
我们可以通过setup模块来获取目的主机的fact信息:
ansible hostname -m setup
fact会在playbook执行之前收集信息,默认是打开的,我们也可以通过指定gather_fact参数为false/no/False关闭fact。在没有配置fact cache的情况下,如果关闭fact,playbook的执行速度会有一个显著的提升,示例如下:
---
- hosts: whatever
gather_facts: no
3.4变量的使用
前面我们介绍了下变量的定义/引用方式和fact变量,那么在playbook中我们如何使用这些变量呢?
变量通常会在模版、条件判断语句、新的变量定义等处能用到。
使用变量的方法很简单,只需要将变量写在两个大括号内并且前后都有空格即可,同时我们必须将这个大括号用双引号引起来,如果变量穿插在字符串内使用,双引号也要将字符串部分引起来。
示例如下:
- hosts: app_servers
vars:
app_path: "{{ base_path }}/22"
如果一个变量定义比较复杂,例如列表、字典或fact(json格式),我们可以通过如下方式访问:
列表变量访问:
{{ foo[0] }}
字典变量访问:
{{ foo[name] }}
或
{{ foo.name }}
json格式访问变量访问:
{{ansible_eth0["ipv4"]["address"] }}
或
{{ansible_eth0.ipv4.address }}
这里说一个小技巧,在我们排错过程中很多情况我们要debug一些变量。此时,可以使用debug模块输出变量。
debug模块有两种使用方式,vars和msg :
---
- hosts: node1
gather_facts: false
vars:
- name: weimeng
- age: 26
tasks:
- name: Use var debug
debug:
var: name,age
- name: Use msg debug
debug:
msg: "my name is {{ name }},and my age is {{ age }}"
输入如下:
➜ lab-ansible ansible-playbook playbooks/task_vars.yml
[WARNING]: Found variable using reserved name: name
PLAY [node1] *******************************************************************
TASK [Use var debug] ***********************************************************
ok: [node1] => {
"name,age": "(u'weimeng', 26)"
}
TASK [Use msg debug] ***********************************************************
ok: [node1] => {
"msg": "my name is weimeng,and my age is 26"
}
PLAY RECAP *********************************************************************
node1 : ok=2 changed=0 unreachable=0 failed=0
通过对比我们可以看出,“vars”适用于直接debug变量,而“msg”可以掺杂一些字符串,我们可以根据实际情况来选择使用。
本章节主要介绍了playbook的相关变量。ansible变量的知识点还是很多的,所以我计划在后边会单独介绍ansible的变量,这里就点到为止。
4. 条件语句
ansible条件语句不是很多,比较常用的就是when语句和循环语句。
当满足一定的条件时,我们想要跳过某个task,这时候when语句出场了。当when语句的参数为true时,才会执行这个task,否则反之。
yum模块的name可以以列表的形式指定多个安装包,但是很多其他模块是不支持列表的,例如file的path,copy的src,等等;或者说我们想迭代的将一个列表元素传递给某个模块处理,如果有多少个元素写多个task就很麻烦。此时我们可以使用ansible的循环语句loop(ansible 2.5以后),在2.5版本之前可以使用with_,loop类似于旧版本的with_list语句。
4.1 when语句
ansible的when语句用于判断是否执行这个task,例如
tasks:
- name: "shut down Debian flavored systems"
command: /sbin/shutdown -t now
when: ansible_os_family == "Debian"
# note that Ansible facts and vars like ansible_os_family can be used
# directly in conditionals without double curly braces
示例中如果系统的类型是“Debian”才会执行/sbin/shutdown -t now命令。
条件语句也可以使用“and”和“or”:
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")
条件也可以写成列表的形式,这种形式和and语句起到一样的效果:
tasks:
- name: "shut down CentOS 6 systems"
command: /sbin/shutdown -t now
when:
- ansible_distribution == "CentOS"
- ansible_distribution_major_version == "6"
register变量条件语句
通过对某个task的执行结果是否成功,决定另外一个task是否要执行:
tasks:
- command: /bin/false
register: result
ignore_errors: True
- command: /bin/something
when: result is failed
# In older versions of ansible use ``success``, now both are valid but succeeded uses the correct tense.
- command: /bin/something_else
when: result is succeeded
- command: /bin/still/something_else
when: result is skipped
变量是否被定义语句:
tasks:
- shell: echo "I've got '{{ foo }}' and am not afraid to use it!"
when: foo is defined
- fail: msg="Bailing out. this play requires 'bar'"
when: bar is undefined
4.2 循环语句
上面说到loop类似于旧版本的with_list语句,也就是说loop会将列表的元素逐个传递给上面的module,从而达到重复执行的目的。
最简单的形式:
---
tasks:
- command: echo { item }
loop: [ 0, 2, 4, 6, 8, 10 ]
loop与when结合使用:
---
tasks:
- command: echo { item }
loop: [ 0, 2, 4, 6, 8, 10 ]
when: item > 5
通常loop语句会结合各式各样的filter去使用,例如“ loop: “{ { [\'alice\', \'bob\'] |product([\'clientdb\', \'employeedb\', \'providerdb\'])|list }}””,这个例子和with_nested语句起到一样的效果。也就是说旧版本的with_ + lookup() 所能实现的,新版本的loop+filter同样能实现。
5. 执行顺序
一般playbook里的task执行顺序和python一样,由上至下,定义的顺序即执行的顺序。同样的,使用include*和import*导入playbook或tasks也会安照导入顺序执行。
5.1 per_tasks和post_tasks
当playbook中有使用roles导入task和自定义tasks时,我们会发现ansible总会先执行roles导入的task,然后执行自定义的tasks,例如:
- hosts: localhost
gather_facts: no
vars:
- ff: 1
- gg: 2
tasks:
- debug:
var: ff
roles:
- role: role_B
输出结果:
➜ lab-ansible ansible-playbook playbooks/roles_vars.yml
PLAY [localhost] ***************************************************************
TASK [role_B : debug] **********************************************************
ok: [localhost] => {
"a": 2
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"ff": 1
}
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0
从上面示例发现,虽然我们将tasks定义在了前面,但是tasks任务还是在roles任务之后执行。此时我们可以使用pre_task和post_task来强制指定执行顺序,例如:
---
- hosts: localhost
gather_facts: no
vars:
- ff: 1
- gg: 2
pre_tasks:
- import_role:
name: role_A
vars:
age: 23
roles:
- role: role_B
tasks:
- debug:
var: ff
post_tasks:
- debug:
var: gg
总结下playbook里任务的执行顺序:
-
使用“pre_tasks:”定义的任务
-
使用“roles:”引用的任务
-
使用“tasks:”自定义的任务
-
使用“post_tasks”定义的任务
5.2 handlers
在部署应用时,通常的步骤是安装软件包==>更改配置文件==>初始化数据库==>启动(重启)服务;升级的步骤一般是:升级软件包==>更改配置文件==>初始化数据库==>重启服务。我们发现不管是新部署还是升级,最后一步都是要重新加载程序的,也就是说当我们升级了软件或者更改了配置文件都需要重启一下应用。
为了实现触发服务重启,ansible使用handlers方法定义重启的动作,handlers并不是每次执行playbook都会触发,而是某些指定资源状态改变时才会触发指定的handlers(这里使用“资源”一词借鉴于puppet)。
示例如下:
- name: template configuration file
template:
src: template.j2
dest: /etc/foo.conf
notify:
- restart memcached
- restart apache
上面的示例中,当/etc/foo.conf文件内容有改动时(返回changed),会触发重启memcached和apache服务,如果文件内容没有变化时(返回ok),则不会触发handlers。
ansible执行过程中并不会立即触发handlers动作,而是以play为单位,一个play执行完后最后才会触发handlers。
这样设计也是很合理的,试想在一个play内,如果触发一次就执行一次handlers,那么除了最后一次的重启,前面触发的重启都是无用功。
另外需要注意的一点是,handlers触发的执行顺序是按照定义顺序执行,而不是按照notify指定的顺序执行。
当然如果我们想要立即触发,也是可以的,在play定义“- meta: flush_handlers”即可。
另外需要注意的一点是,handlers触发的执行顺序是按照定义顺序执行,而不是按照notify指定的顺序执行。
6.参考链接
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_intro.html#
- https://docs.ansible.com/ansible/2.6/user_guide/playbooks_variables.html
- https://docs.ansible.com/ansible/2.6/user_guide/playbooks_reuse.html
- https://docs.ansible.com/ansible/2.6/user_guide/playbooks_templating.html
- https://docs.ansible.com/ansible/2.6/user_guide/playbooks_conditionals.html
- https://docs.ansible.com/ansible/2.6/user_guide/playbooks_loops.html
欢迎大家关注我的公众号: