第 4-2 章 Linux Ansible 进阶知识

作者: admin 分类: ARM64 Linux CICD 研发 发布时间: 2022-07-19 12:12

在前面综合案例的最后,我们将初始化配置服务器的多个任务组合到了单个 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)中定义的变量。

分析一下上面的执行流程:

  1. 解析 playbook 文件 pb1.yml
  2. 执行第一个 play
  3. 当执行到 pb1.yml 中的第二个任务时,该任务在循环中,且其作用是加载外部任务文件 pb2.yml
  4. 开始循环,每轮循环都加载、解析并执行 pb2.yml 文件中的所有任务
  5. 退出

正是因为 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 指令或 lookupfirst_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 的角色。

ansible_playbook

既然 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 打了标签。

  1. 可以在 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
  2. 可以在 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]
  3. 在静态加载文件的指令上打标签,等价于为所加载文件中所有子任务打标签。在动态加载文件的指令上打标签,不会为子任务打标签,而是为父任务自身打标签。

    • 静态加载的指令有: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 实践

标签云