第 4-2 章 Linux Ansible 进阶知识
在前面综合案例的最后,我们将初始化配置服务器的多个任务组合到了单个 playbook 中,这种组织方式的可读性和可维护性都很差,整个 playbook 看上去也非常凌乱。
如果我们将每一个功能块单独维护在一个 playbook 中,然后通过目录树的方式组织这些 playbook,最后通过 python 的编程逻辑统一执行这些 playbook。这种方式看起来有点像编程中的框架思想。不错,确实很像,在 ansible 中我们叫这种框架为 Role。
在学习 Role 之前需要打一下基础,下面梳理一下脉络。
4.1、Include 和 Import 指令
前面我们学习了 play、task、variable、handler等,为了实现某些功能把它们放到一个 playbook 当中,现在我们打算分门别类组织它们。
将各类文件分类存放后,最终需要在某个入口文件去汇集引入这些外部文件。加载这些外部文件通常可以使用 include 指令、include_xxx 指令和 import_xxx 指令,其中 xxx 表示内容类型。
在早期 Ansible 版本,组织文件的方式均使用 include 指令,但随着版本的更迭,Ansible 对这方面做了更为细致的区分。虽然目前仍然支持 include,但早已纳入废弃的计划,所以现在不要再使用 include 指令。
对于 playbook 中的 play 或 task,应该使用 include_xxx 或 import_xxx 指令:
- include_tasks 和 import_tasks 用于引入外部任务文件;
- import_playbook 用于引入 playbook 文件;
- include 可用于引入几乎所有内容文件,但建议不要使用它;
对于 handler,因为它本身也是 task,所以它也能使用 include_tasks、import_tasks 来引入,但是这并不是想象中那么简单,后文再细说。
对于 variable,使用 include_vars(这是核心模块提供的功能)或其它组织方式(如 vars_files),没有对应的 import_vars。
对于后文要介绍的 Role,使用 include_role 或 import_role 或 roles 指令。
既然某类内容文件既可以使用 include_xxx 引入,也可以使用 import_xxx 引入,那么就有必要去搞清楚它们有什么区别。本文最后我会详细解释它们,现在我先把结论写在这:
- include_xxx 指令是在遇到它的时候才加载文件并解析执行,所以它是动态解析的;
- import_xxx 是在解析 playbook 的时候解析的,也就是说在执行 playbook 之前就已经解析好了,所以它也称为静态加载。
4.2、组织 task
在此前的所有示例中,一直都是将所有任务编写在单个 playbook 文件中。但 Ansible 允许将任务分离到不同的文件中,然后去引入外部任务文件。
用示例来解释会非常简单。假设,两个 playbook 文件 pb1.yml 和 pb2.yml。
pb1.yml 文件内容如下:
---
- name: play1
hosts: localhost
gather_facts: false
tasks:
- name: task1 in play1
debug:
msg: "task1 in play1"
# - include_tasks: pb2.yml
- import_tasks: pb2.yml
pb2.yml 文件内容如下:
- name: task2 in play1
debug:
msg: "task2 in play1"
- name: task3 in play1
debug:
msg: "task3 in play1"
执行 pb1.yml:
$ ansible-playbook pb1.yml
上面是在 pb1.yml 文件中通过 import_tasks 引入了额外的任务文件 pb2.yml,对于此处来说,将 import_tasks 替换成 include_tasks 也能正确工作,不会有任何影响。
但如果是在循环中(比如 loop),则只能使用 include_tasks 而不能再使用 import_tasks。
4.2.1、在循环中 include 文件
修改 pb1.yml 和 pb2.yml 文件内容:
pb1.yml 内容如下,注意该文件中的 include_tasks 指令:
---
- name: play1
hosts: localhost
gather_facts: false
tasks:
- name: task1 in play1
debug:
msg: "task1 in play1"
- name: include two times
include_tasks: pb2.yml
loop:
- ONE
- TWO
pb2.yml 内容如下,注意该文件中的变量引用:
- name: task2 in play1
debug:
msg: "task2 in {{item}}"
执行 pb1.yml 文件,观察执行结果:
$ ansible-playbook pb1.yml
TASK [task1 in play1] ************************
ok: [localhost] => {
"msg": "task1 in play1"
}
TASK [include two times] *********************
included: /root/ansible/pb2.yml for localhost
included: /root/ansible/pb2.yml for localhost
TASK [task2 in play1] ************************
ok: [localhost] => {
"msg": "task2 in ONE"
}
TASK [task2 in play1] ************************
ok: [localhost] => {
"msg": "task2 in TWO"
}
上面是在 loop 循环中加载两次 pb2.yml 文件,该文件中的任务被执行了两次,并且在 pb2.yml 中能够引用外部文件(pb1.yml)中定义的变量。
分析一下上面的执行流程:
- 解析 playbook 文件 pb1.yml
- 执行第一个 play
- 当执行到 pb1.yml 中的第二个任务时,该任务在循环中,且其作用是加载外部任务文件 pb2.yml
- 开始循环,每轮循环都加载、解析并执行 pb2.yml 文件中的所有任务
- 退出
正是因为 include_tasks 指令是在遇到它的时候才进行加载解析以及执行,所以在 pb2.yml 中才能使用变量。
如果将上面 loop 循环中的 include_tasks 换成 import_tasks 呢?语法会报错,下面可以找到答案。
4.3、组织 handler
handler 其本质也是 task,所以也可以使用 include_tasks 或 import_tasks 来加载外部任务文件。但是它们引入 handler 任务文件的方式有很大的差别。
先看 include_tasks 引入 handler 任务文件的示例:
pb1.yml 的内容:
---
- name: play1
hosts: localhost
gather_facts: false
handlers:
- name: h1
include_tasks: handler1.yml
tasks:
- name: task1 in play1
debug:
msg: "task1 in play1"
changed_when: true
notify:
- h1
注意在 tasks 的任务中加了一个指令 changed_when: true
,它用来强制指定它所在任务的 changed 状态,如果条件为真,则 changed=1,否则 changed=0。使用这个指令是因为 debug 模块默认不会引起 changed=1 行为,所以只能使用该指令来强制其状态为 changed=1。
当 Ansible 监控到了 changed=1,notify 指令会生效,它会去触发对应的 handler,它触发的 handler 的名称是 handler1,其作用是使用 include_tasks 指令引入 handler1.yml 文件。
下面是 handler1.yml 文件的内容:
---
- name: h11
debug:
msg: "task h11"
注意两个名称,一个是 notify 触发 handler 的任务名称(“h1”),一个是引入文件中任务的名称(“h11”),它们是两个任务。
再来看 import_tasks 引入 handler 文件的示例,注意观察名称的不同点。
如下是 pb1.yml 文件的内容:
---
- name: play1
hosts: localhost
gather_facts: false
handlers:
- name: h2
import_tasks: handler2.yml
tasks:
- name: task1 in play1
debug:
msg: "task1 in play1"
changed_when: true
notify:
- h22
下面是使用 import_tasks 引入的 handler2.yml 文件的内容:
---
- name: h22
debug:
msg: "task h22"
在引入 handler 任务文件的时候,include_tasks 和 import_tasks 的区别表现在:
- 使用 include_tasks 时,notify 指令触发的 handler 名称是 include_tasks 任务本身的名称
- 使用 import_tasks 时,notify 指令触发的 handler 名称是 import_tasks 所引入文件内的任务名称
将上面的两个示例合在一起,或许要更清晰一点:
---
- name: play1
hosts: localhost
gather_facts: false
handlers:
- name: h1
include_tasks: handler1.yml
- name: h2
import_tasks: handler2.yml
tasks:
- name: task1 in play1
debug:
msg: "task1 in play1"
changed_when: true
notify:
- h1 # 注意h1和h22名称的不同
- h22
其实分析一下就很容易理解为什么 notify 触发的名称要不同:
- include_tasks 是在遇到这个指令的时候才引入文件的,所以 notify 不可能去触发外部 handler 文件里的名称(h11),外部 handler 文件中的名称在其引入之前根本就不存在。
- import_tasks 是在解析 playbook 的时候引入的,换句话说,在执行 play 之前就已经把外部 handler 文件的内容引入并替换在 handler 的位置处,而原来的名称(h2)则被覆盖了。
最后,不要忘了 import_tasks 或 include_tasks 自身也是任务,既然是任务,就能使用 task 层次的指令。例如下面的示例:
handlers:
- name: h1
include_tasks: handler.yml
vars:
my_var: my_value
when: my_var == "my_value"
但这两个指令对 task 层次指令的处理方式不同,相关细节仍然保留到后文统一解释。
4.4、组织变量
引入保存了变量的文件有两种方式:include_vars 和 vars_files。此外,还可以在命令行中使用 -e 或 --extra-vars 选项来引入。
4.4.1、vars_files
先介绍 vars_files
,它是一个 play 级别的指令,可用于在解析 playbook 的阶段引入一个或多个保存了变量的外部文件。
例如,pb.yml 文件如下:
---
- name: play1
hosts: localhost
gather_facts: false
vars_files:
- varfile1.yml
- varfile2.yml
tasks:
- debug:
msg: "var in varfile1: {{var1}}"
- debug:
msg: "var in varfile2: {{var2}}"
pb.yml 文件通过 vars_files 引入了两个变量文件,变量文件的写法要求遵守 YAML 或 JSON 格式。下面是这两个文件的内容:
# 下面是varfile1.yml文件的内容
---
var1: "value1"
var11: "value11"
# 下面是varfile2.yml文件的内容
---
var2: "value2"
var22: "value22"
需要说明的是,vars_files
指令是 play 级别的指令,且是在解析 playbook 的时候加载并解析的,所以所引入变量的变量是 play 范围内可用的,其它 play 不可使用这些变量。
4.4.2、include_vars
include_vars
指令也可用于引入外部变量文件,它和 vars_files
不同。一方面,include_vars
是模块提供的功能,它是一个实实在在的任务,所以在这个任务执行之后才会创建变量。另一方面,既然 include_vars
是一个任务,它就可以被一些 task 级别的指令控制,如 when
指令。
例如:
---
- name: play1
hosts: localhost
gather_facts: false
tasks:
- name: include vars from files
include_vars: varfile1.yml
when: 3 > 2
- debug:
msg: "var in varfile1: {{var1}}"
上面示例中引入变量文件的方式是直接指定文件名 include_vars: varfile1.yml
,也可以明确使用 file
参数来指定路径。
- name: include vars from files
include_vars:
file: varfile1.yml
如果想要引入多个文件,可以使用循环的方式。例如:
- name: include two var files
include_vars:
file: "{{item}}"
loop:
- varfile1.yml
- varfile2.yml
- 注意:include_vars 在引入文件的时候要求文件已经存在,如果有多个可能的文件但不确定文件是否存在,可以使用
with_first_found
指令或lookup
的first_found
插件。
例如:
tasks:
- name: include vars from files
include_vars:
file: "{{item}}"
with_first_found:
- varfile1.yml
- varfile2.yml
- default.yml
# 等价于:
tasks:
- name: include vars from files
include_vars:
file: "{{ lookup('first_found',any_files) }}"
vars:
any_files:
- varfile1.yml
- varfile2.yml
- default.yml
此外,include_vars 还能从目录中导入多个文件,默认会递归到子目录中。例如:
- name: Include all files in vars/all
include_vars:
dir: vars/all
4.4.3、–extra-vars 选项
ansible-playbook 命令的 -e 选项或 --extra-vars 选项也可以用来定义变量或引入变量文件。
# 定义单个变量
$ ansible-playbook -e 'var1="value1"' xxx.yml
# 定义多个变量
$ ansible-playbook -e 'var1="value1" var2="value2"' xxx.yml
# 引入单个变量文件
$ ansible-playbook -e '@varfile1.yml' xxx.yml
# 引入多个变量文件
$ ansible-playbook -e '@varfile1.yml' -e '@varfile2.yml' xxx.yml
因为是通过选项的方式来定义变量的,所以它所定义的变量是全局的,对所有 play 都有效。
通常来说不建议使用 -e 选项,因为这对用户来说是不透明也不友好的,要求用户记住要定义哪些变量。
4.5、组织 playbook 文件
import_playbook 指令可用于引入 playbook 文件,它是一个 play 级别的指令,其本质是引入外部文件中的一个或多个 play。
例如,pb.yml 是入口 playbook 文件,此文件中引入了其它 playbook 文件,其内容如下:
---
# 引入其它playbook文件
- import_playbook: pb1.yml
- import_playbook: pb2.yml
# 文件本身的play
- name: play in self
hosts: localhost
gather_facts: false
tasks:
- debug: 'msg="file pb.yml"'
pb1.yml 文件是一个完整的 playbook,它可以包含一个或多个 play,其内容如下:
---
- name: play in pb1.yml
hosts: localhost
gather_facts: false
tasks:
- debug: 'msg="imported file: pb1.yml"'
pb2.yml 文件也是一个完整的 playbook,其内容如下:
---
- name: play in pb2.yml
hosts: localhost
gather_facts: false
tasks:
- debug: 'msg="imported file: pb2.yml"'
4.6、更为规范的组织方式:Role
我们上面自己组织 playbook 其实也很不错,针对一些小项目够用。当然这里介绍的 Role 不过是一种更规范的组织方式,另外 Role 背后的逻辑可以由 python 组织,所以更加灵活多变,我们只学一些简单适用的技巧即可。
4.6.1、Role 文件结构
Role 可以组织任务、变量、handler 以及其它一些内容,所以一个完整的 Role 里包含的目录和文件可能较多,手动去创建所有这些目录和文件是一件比较烦人的事,好在可以使用 ansible-galaxy init ROLE_NAME 命令来快速创建一个符合 Role 文件组织规范的框架。关于ansible galaxy,我稍后会简单介绍一下它。
例如,下面创建了一个名为 first_role 的 Role:
$ ansible-galaxy init first_role
$ tree
.
└── first_role
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── README.md
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
可以使用 ansible-galaxy init --help 查看更多选项。比如,使用 --init-path 选项指定创建的 Role 路径:
$ ansible-galaxy init --init-path /etc/ansible/roles first_role
可以看到,这里面已经包含了不少目录和文件,这些目录的含义稍后我会一一解释,不过从部分文件名中,大概能看出一个 Role 包含了任务、变量、handler 等。这些目录或目录里的文件允许不存在,在没有使用到相关文件的时候并不强制这些文件或目录存在。
不难发现大多数文件命名为 main.yml,这是 Role 自动加载的主配置文件,如果换成别的名字,需要使用 include_xxx 或 import_xxx 去加载。另外,这些目录下可能还包含其它 yml 文件,比如 tasks 目录下有多个任务文件,需要在 main.yml 文件中使用 include_xxx 或 import_xxx 加载其它外部文件。
因为有可能同时会有多个 Role,比如创建一个 Nginx 的 Role,再创建一个 MySQL 的 Role,还创建一个 Haproxy 的 Role,所以为了组织多个 Role,通常会将每个 Role 放在一个称为 roles 的目录下。即:
$ tree -L 2
.
└── roles
├── first_role
└── second_role
有了 Role 之后,就可以将 Role 当作一个不可分割的任务整体来对待,一个 Role 相当于是一个完整的功能。但在此需要明确一个层次上的概念,Role 只是用于组织一个或多个任务,原来在 play 级别中使用 tasks 指令来定义任务,现在使用 roles 指令来引入 Role 中定义的任务。当然,roles 指令和 tasks 指令并不冲突,它们可以共存。
通过下面的图,应能帮助理解 Role 的角色。
既然 Role 是一个完整的任务体系,拥有 Role 之后就可以去使用它,或者也可以分发给别人使用,但是一个 Role 仅仅只是目录而已,如何去使用这个 Role 呢?
所以,还需要提供一个被 ansible-playbook 执行的入口 playbook 文件(就像 main() 函数一样),在这个入口文件中引入一个或多个 roles 目录下的 Role。入口文件的名称可以随意,比如 www.yml、site.yml、main.yml 等,但注意它们和 roles 目录要在同一个目录下。
.
├── enter.yml
└── roles
├── first_role/
└── second_role/
上面和 roles 同目录的 enter.yml 文件内容如下,此文件中使用 roles 指令引入了 roles 目录内的两个 Role。
---
- name: play with role
hosts: nginx
gather_facts: false
roles:
- first_role
- second_role
如果遵循了 Role 规范,入口文件中可以直接使用 Role 名称来引入 roles 目录下的 Role(正如上面的示例),也可以指定 Role 的路径来引入。
4.6.2、定义 Role 的 task
Role 的任务定义在 roles/xxx/tasks/main.yml 文件中,main.yml 是该 Role 任务的入口,在执行 Role 的时候会自动执行 main.yml 中的任务。可以直接将所有任务定义在此文件中,也可以定义在其它文件中,然后在 main.yml 文件中去引入。
以 first_role 这个 Role 为例,例如,直接将任务定义在 main.yml 文件中:
---
- name: task in main.yml
debug:
msg: "task in main.yml"
或者,将任务定义在 roles/xxx/tasks/ 目录下的其它文件中,如 mytask.yml:
---
- name: task in main.yml
debug:
msg: "task in main.yml"
然后在 roles/xxx/tasks/main.yml 中通过 include_tasks 或 import_tasks 引入它:
---
- include_tasks: mytask.yml
# 或者
#- import_tasks: mytask.yml
Role 的任务文件定义好后,然后在 Role 的入口文件(即 roles 同目录下的 playbook 文件)enter.yml 中引入这个 Role:
---
- name: play1
hosts: localhost
gather_facts: false
roles:
- first_role
执行它:
$ ansible-playbook enter.yml
4.6.3、定义 Role 的 handler
handler 和 task 类似,它定义在 roles/xxx/handlers/main.yml 中,当 Role 的 task 触发了对应的 handler,会自动来此文件中寻找。
仍然要说的是,可以将 handler 定义在其它文件中,然后在 roles/xxx/handlers/main.yml 使用 include_tasks 或 import_tasks 指令来引入,而且前面也提到过这两者在 handler 上的区别和注意事项。
例如,roles/first_role/handlers/main.yml 中定义了如下简单的 handler:
---
- name: handler for test
debug:
msg: "a simple handler for test"
在 roles/first_role/tasks/main.yml 中通过 notify 触发该 Handler:
---
- name: task in main.yml
debug:
msg: "task in main.yml"
changed_when: true
notify: handler for test
然后执行:
$ ansible-playbook enter.yml
4.6.4、定义 Role 的变量
Role 中有两个地方可以定义变量:
- roles/xxx/vars/main.yml
- roles/xxx/defaults/main.yml
从目录名称中可以看出,defaults/main.yml 中用于定义 Role 的默认变量,那么显然,vars/main.yml 用于定义其他变量。
这两个文件之间的区别在于,defaults/main.yml 中定义的变量优先级低于 vars/main.yml 中定义的变量,事实上, defaults/main.yml 中的变量优先级几乎是最低的,基本上其它任何地定义的变量都可以覆盖它。
4.6.5、Role 用到的外部文件和模板文件
有时候需要将 Ansible 端的文件拷贝到远程节点上,比如拷贝本地已经写好的 MySQL 配置文件 my.cnf 到多个远程节点上,拷贝本地写好的脚本文件到多个远程节点执行,等等。
这时候在进行拷贝的模块中可以指定这些文件的绝对路径。但在 Role 中,可以将这些文件放在 roles/xxx/files/ 或 roles/xxx/templates/ 目录下,遵守了这个 Role 规范,就可以在模块中直接指定文件名称,而不用加上路径前缀(当然,加上也不会错)。
例如,Role 中有一个 copy 模块的任务,想要拷贝 roles/first_role/files/my.cnf 到目标节点的 /etc/my.cnf,则:
- name: copy file
copy:
src: my.cnf # 直接指定文件名my.cnf即可
dest: /etc/my.cnf
这些模块知道去 roles/xxx/files/ 目录或 roles/xxx/templates/ 下搜索对应文件的原因,在于这些模块的代码内部定义了文件搜索路径,不同的模块搜索路径不同,且可能不止一个搜索路径。
例如对于 Role 中的 template 模块任务(template 模块目前尚未介绍,之后遇到的时候再解释,或者各位可自搜其用法,现在将其当作 copy 模块即可),如果其参数 src=my.cnf,则依次搜索如下路径:
roles/first_role/templates/my.cnf
roles/first_role/my.cnf
roles/first_role/tasks/templates/my.cnf
roles/first_role/tasks/my.cnf
templates/my.cnf
my.cnf
一般来说,需要考虑源文件存放位置的模块包括 copy、script、template 模块,前两个模块以及其它可能的模块,一般会先搜索 roles/xxx/files/ 目录,但不会搜索 templates 目录,而 template 模块则会先搜索 templates 目录而不会搜索 files 目录。
换句话说,除了 template 模块外,其它模块使用到的文件很可能都应该存放于 roles/xxx/files/ 目录。如果不确定某个模块的搜索路径,测试一番即可,或者直接看报错信息中给出的路径搜索过程。
4.6.6、Role 中定义的模块和插件
对于绝大多数需求,使用 Ansible 已经提供的模块和插件就能解决问题,但有时候确实有些需求需要自己去写模块或插件,Ansible 也支持用户自定义的模块和插件。
对于 Role 来说,如果这个 Role 需要额外使用自己编写的模块或插件,则模块放在 roles/xxx/librarys/ 目录下,而插件放在各自对应类型的目录下:
roles/xxx/action_plugins/
roles/xxx/lookup_plugins/
roles/xxx/callback_plugins/
roles/xxx/connection_plugins/
roles/xxx/filter_plugins/
roles/xxx/strategy_plugins/
roles/xxx/cache_plugins/
roles/xxx/test_plugins/
roles/xxx/shell_plugins/
一般情况用不上自定义模块或插件,目前各位了解即可。
4.6.7、定义 Role 的依赖关系
当我们定义多个 Role 实现一个项目需求时,这些 Role 之间或多或少具有依赖关系,换句话说,有的 Role 必须先执行,只有它执行完了后,依赖它的 Role 才能接着执行。
按照 Role 规范,被依赖的先行任务都定义在 roles/xxx/meta/main.yml 文件中。
例如:
---
dependencies:
- second_role
- third_role
注意,Role 的 dependencies 指令只能指定被依赖的 Role,不能直接指定被依赖的任务。例如,下面是错误的依赖定义:
---
dependencies:
- debug: msg="check it"
当真正开始执行 Role 的时候,会先检查是否有依赖任务,如果有,则先执行依赖任务,依赖任务执行完后再开始执行普通任务。
4.6.8、动手写一个 Role
了解完 Role 各个目录和文件的意义后,可以开始动手写一个 Role 来体验一番。
就以 first_role 为例,这个 Role 没有具体的功能,全部都是 debug 模块的调试信息,所以这个 Role 非常简单,这个 Role 唯一的意义是:学会写最简单的 Role 并看懂执行流程。
首先在 defaults/main.yml 中定义一个变量 default_var。
---
default_var: default_value
然后在 vars/main.yml 中定义两个变量 my_var 和 default_var:
---
my_var: my_value
default_var: overrided_default_value
显然 vars/main.yml 中的 default_var 会覆盖 defaults/main.yml 中的 default_var。
定义完变量之后,就可以在 task、handler 甚至 template 模板文件中使用这些变量。当然,在实际编写 Role 的时候,一般不可能预先知道要定义哪些变量,通常都是在编写 task 的过程中来变量文件中添加变量的。
然后是 tasks/main.yml 文件,在此文件中定义了一个使用变量的任务,并引入了一个外部 task 文件 t.yml。内容如下:
---
- name: task1
debug:
msg: "task in my_var: {{my_var}}"
- name: include t.yml
import_tasks: t.yml
在 t.yml 中定义了一个任务,且通过 notify 触发一个 handler,其内容为:
---
- name: task in t.yml
debug:
msg: "default_var: {{default_var}}"
changed_when: true
notify: "go to handler"
然后去 handlers/main.yml 中定义对应的 handler 即可,其内容为:
---
- name: go to handler
debug:
msg: "new_var: {{new_var}}"
这个 Role 就这么简单,因为没有定义依赖关系,也没有拷贝文件,所以 roles/first_role/{meta,files,templates} 这三个目录都可以删掉。
写好 Role 后,再提供一个被 ansible-playbook 命令执行的入口 playbook 文件,然后在此 playbook 文件中去加载对应的 Role 并执行。例如,这个入口文件名为 enter.yml,其内容如下:
---
- name: play1
hosts: localhost
gather_facts: false
roles:
- role: first_role
vars:
new_var: new_value
最后执行该入口文件:
$ ansible-playbook enter.yml
PLAY [play1] ****************************************
TASK [first_role : task1] ***************************
ok: [localhost] => {
"msg": "task in my_var: my_value"
}
TASK [first_role : task in t.yml] *******************
changed: [localhost] => {
"msg": "default_var: overrided_default_value"
}
RUNNING HANDLER [first_role : go to handler] ********
ok: [localhost] => {
"msg": "new_var: new_value"
}
4.7、使用 Role
写好 Role 之后就是使用 Role,即在一个入口 playbook 文件中去加载 Role。加载 Role 的方式有多种:
- roles 指令:play 级别的指令,在 playbook 解析阶段加载对应文件,这是传统的引入 Role 的方式。
- import_role 指令:task 级别的指令,在 playbook 解析阶段加载对应文件。
- include_role 指令:task 级别的指令,在遇到该指令的时候才加载 Role 对应文件。
例如前面使用的是 roles,如下:
---
- name: play1
hosts: localhost
gather_facts: false
roles:
- first_role
上面通过 roles 指令来定义要解析和执行的 Role,可以同时指定多个 Role,且也可以加上 role:参数,例如:
roles:
- first_role
- role: seconde_role
- role: third_role
也可以使用 include_role 和 import_role 来引入 Role,但需注意,这两个指令是 tasks 级别的,也正因为它们是 task 级别,使得它们可以和其它 task 共存。
例如:
---
- hosts: localhost
gather_facts: false
tasks:
- debug:
msg: "before first role"
- import_role:
name: first_role
- include_role:
name: second_role
- debug:
msg: "after second role"
这三种引入 Role 的方式都可以为对应的 Role 传递参数,例如:
---
- hosts: localhost
gather_facts: false
roles:
- role: first_role
varvar: "valuevalue"
vars:
var1: value1
tasks:
- import_role:
name: second_role
vars:
var1: value1
- include_role:
name: third_role
vars:
var1: value1
有时候需要让某个 Role 按需执行,比如对于目标节点是 CentOS 7 时执行 Role7 而不执行 Role6,目标节点是 CentOS 6 时执行 Role6 而不是 Role7,这可以使用 when 指令来控制。
例如:
---
- hosts: localhost
gather_facts: false
roles:
# 下面是等价的,分别采用YAML和Json语法书写
- role: first_role
when: xxx
- {role: ffirst_role, when: xxx}
tasks:
- import_role:
name: second_role
when: xxx
- include_role:
name: third_role
when: xxx
注意,在 roles、import_role 和 include_role 三种方式中,when 指令的层次。
4.8、查看任务和打标签 tags
有时候 Role 文件比较多,相互调用有些复杂,通过查看文件想知道执行流程有点困难,可以使用命令 ansible-playbook --list-tasks 查看。
[root@controller ~]# ansible-playbook --list-tasks sshd_config.yml
playbook: sshd_config.yml
play #1 (new): modify sshd_config TAGS: []
tasks:
backup sshd config TAGS: []
disable root login TAGS: []
disable password auth TAGS: []
从结果中还看到 play 和 task 的后面都带有 TAGS: [],它是标签。当在 play 或 task 级别使用 tags 指令后就表示为此 play 或 task 打了标签。
-
可以在 task 级别为单个任务打一个或多个标签,多个任务可以打同一个标签名。
- name: yum install ntp yum: name: ntp state: present tags: - initialize - pkginstall - ntp - name: started ntpd service: name: ntpd state: started tags: - ntp - initialize
当任务具有了标签之后,就可以在 ansible-playbook 命令行中使用 --tags 来指定只有带有某标记的任务才执行,也可以使用 --skip-tags 选项明确指定不要执行某个任务。
# 只执行第一个任务 $ ansible-playbook test.yml --tags "pkginstall" # 两个任务都执行 $ ansible-playbook test.yml --tags "ntp,initialize" # 第一个任务不执行 $ ansible-playbook test.yml --skip-tags "pkginstall"
如果想要确定 tag 筛选之后会执行哪些任务,加上 --list-tasks 即可:
$ ansible-playbook test.yml --tags "ntp" --list-tasks
-
可以在 play 级别打标签,这等价于对 play 中的所有任务都打上标签。
- name: play1 hosts: localhost gather_facts: false tags: - tag1 - tag2 pre_tasks: - debug: "msg='pre_task1'" - debug: "msg='pre_task2'" tasks: - debug: "msg='task1'" - debug: "msg='task2'"
这会为 4 个任务都打 tag1 和 tag2 标签。
$ ansible-playbook a.yml --list-tasks playbook: a.yml play #1 (localhost): play1 TAGS: [tag1,tag2] tasks: debug TAGS: [tag1, tag2] debug TAGS: [tag1, tag2] debug TAGS: [tag1, tag2] debug TAGS: [tag1, tag2]
-
在静态加载文件的指令上打标签,等价于为所加载文件中所有子任务打标签。在动态加载文件的指令上打标签,不会为子任务打标签,而是为父任务自身打标签。
-
静态加载的指令有:roles、include、import_tasks、import_role
-
动态加载的指令只有include_xxx,包括include_tasks、include_role
import_playbook 和 include_playbook 因为本身就是 play 级别或高于 play 级别,所以不能为这两个指令打标签。
例如,在 b.yml 文件中有两个任务:
--- - debug: "msg='task1 in b.yml'" - debug: "msg='task2 in b.yml'"
在 c.yml 中也有两个任务:
--- - debug: "msg='task1 in c.yml'" - debug: "msg='task2 in c.yml'"
然后在 a.yml 中分别使用 import_tasks 指令引入 b.yml,使用 include_tasks 指令引入 c.yml,同时为这两个指令打标签:
- name: play1 hosts: localhost gather_facts: false tasks: - import_tasks: b.yml tags: [tag1,tag2] - include_tasks: c.yml tags: [tag3,tag4]
这会为 b.yml 中的两个任务打上 tag1 和 tag2 标签,还会为 a.yml 中的 include_tasks 任务自身打上标签 tag3 和 tag4。
$ ansible-playbook a.yml --list-tasks playbook: a.yml play #1 (localhost): play1 TAGS: [] tasks: debug TAGS: [tag1, tag2] debug TAGS: [tag1, tag2] include_tasks TAGS: [tag3, tag4]
-
4.9、Ansible Galaxy
很多时候我们想要实现的 Ansible 部署需求其实别人已经写好了,所以我们自己不用再动手写(甚至不应该自己写),直接去网上找别人已经写好的轮子即可。
Ansible Galaxy(https://galaxy.ansible.com/)是一个 Ansible 官方的 Role 仓库,世界各地的人都在里面分享自己写好的 Role,我们可以直接去 Galaxy 上搜索是否有自己想要的 Role,如果有符合自己心意的,直接安装便可。当然,我们也可以将写好的 Role 分享出去给别人使用。
Ansible 提供了一个 ansible-galaxy 命令行工具,可以快速创建、安装、管理由该工具维护的 Role。它常用的命令有:
# 安装Role:
ansible-galaxy install username.role_name
# 移除Role:
ansible-galaxy remove username.role_name
# 列出已安装的Role:
ansible-galaxy list
# 查看Role信息:
ansible-galaxy info username.role_name
# 搜索Role:
ansible-galaxy search role_name
# 创建Role
ansible-galaxy init role_name
# 此外还有:'delete','import', 'setup', 'login'
# 它们都用于管理galaxy.ansible.com个人账户或里面的Role
# 无视它们
例如,前面已经用该命令快速创建过一个 Role,免去了手动创建 Role 的一堆目录和文件。
$ ansible-galaxy init --init-path /etc/ansible/roles first_role
当从 Galaxy 中搜索到了 Role 之后,可以直接使用 ansible-galaxy install author.rolename 来安装,之所以要加上作者名 author,是因为不同的人可能会上传名称相同的 Role。
默认情况下,ansible-galaxy install安装Role的位置顺序是:
~/.ansible/roles
/usr/share/ansible/roles
/etc/ansible/roles
可以使用 -p 或 --roles-path 选项指定安装路径。
$ ansible-galaxy install -p roles/ chusiang.helloworld
安装完成后,就可以直接使用这个 Role。例如,创建一个 enter.yml 文件,并在此文件中引入该 Role,其内容如下:
---
- name: role from galaxy
hosts: localhost
gather_facts: false
roles:
- role: chusiang.helloworld
然后执行:
$ ansible-playbook enter.yml
虽然 Ansible Galaxy 中有大量的 Role,但有时候我们也会在 Github 上搜索 Role,而且 Galaxy 仓库上的 Role 大多也都在 Github 上。ansible-galaxy install 也可以直接从 git 上下载安装 Role。
例如,上面 ”helloworld” Role 存放在https://github.com/chusiang/helloworld.ansible.role,直接从 github 上安装它:
$ ansible-galaxy install -p roles/ git+https://github.com/chusiang/helloworld.ansible.role.git
4.10、综合案例
参见 LNMP 实践