1

用角色合成可重用Ansible内容

对于很多项目来说,简单单独的ansible剧本就满足了。随着时间的变化以及项目的增长,就需要添加额外剧本、变量文件、任务文件的分隔。组织中的其他项目可能需要复用一些内容,其他项目加入目录树或者有些内容需要在多个项目间拷贝。随着场景的复杂度和大小的增长,急切希望有松散组织的少量剧本、任务文件、以及变量文件。创建这样的层次结构可能是令人生畏的,为什么ansible的很多使用开始简单,一旦分散的文件变得笨拙并难以维护的麻烦出现,就只能发展成良好组织的形式。迁移也变得困难,可能需要重写剧本的重要部分,就进一步延迟重组工作。

本章,我们就介绍下ansible中可组合、可复用、组织良好内容的最佳实践。本章中的经验将帮助开发人员设计在项目增长良好的ansible内容,避免后续进行困难的重新设计工作。下面是概述内容:

  • 任务、处理器、变量以及包含概念的剧本。
  • 角色。
  • 利用角色设计顶层剧本。
  • 跨项目共享角色。

任务、处理器、变量以及包含概念的剧本

理解如何有效组织ansible项目结构的第一步就是掌握包含文件的概念。实际上包含文件就是允许以特定话题来定义内容,并且可以被项目中的其他文件引用一次或多次。这个包含特性支持DRY的概念(Don't Repeat Yourself)。

包含任务

任务文件是定义一个或多个任务的yaml文件。这些任务不是直接绑定到任何特定的剧情或剧本上的;它们纯粹就是任务列表。这些文件可以由剧本或其他任务文件通过include操作符引用。include操作符接收一个任务文件的路径,我们在第一章中已经看到,路径可以是从引用它的文件所在目录的相对路径。

将任务分解成多个独立的文件,我们就可以对其引用多次或者在多个剧本中引用它们。如果我们希望修改其中一个任务的话,我们就只需要修改任务所在的单独文件,无需关心有多少地方引用过它。

---
- name: include a task file
  debug:
    msg: "I am the main task"

- include: more-tasks.yaml
给引入的任务传递变量

有时候,我们会分隔很多任务文件,但是又希望这些任务文件根据变量不同而产生不同的行为。include操作符允许我们在引入的时候定义并覆盖变量数据。定义的作用域就只在包含的任务文件里边。

下面我们创建一个任务文件,可以接收一个pathname和filename变量,任务会创建传入的路径,并创建这个文件。

备注: 书上使用path和file,执行剧本的时候老是报错,但是找不到原因,改成pathname, filename就可以了。
---
- name: create leading path
  file:
    path: "{{ pathname }}"
    state: directory
    
- name: touch the file
  file:
    path: "{{ pathname + '/' + filename }}"
    state: touch

然后定义一个剧本:

---
- name: touch files
  hosts: localhost
  gather_facts: false
  tasks:
      - include: tasks/files.yaml
        pathname: /tmp/foo
        filename: herd

执行剧本结果如下:

ansible-playbook touchfiles.yaml -vv
ansible-playbook 2.6.2
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/usr/share/my_modules']
  ansible python module location = /usr/local/lib/python2.7/site-packages/ansible
  executable location = /usr/local/bin/ansible-playbook
  python version = 2.7.15 (default, Jul 23 2018, 21:27:06) [GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)]
Using /etc/ansible/ansible.cfg as config file
statically imported: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/tasks/files.yaml
statically imported: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/tasks/files.yaml

PLAYBOOK: touchfiles.yaml ******************************************************************************************************************************************************
1 plays in touchfiles.yaml

PLAY [touch files] *************************************************************************************************************************************************************
META: ran handlers

TASK [create leading path] *****************************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/tasks/files.yaml:2
changed: [localhost] => {"changed": true, "gid": 0, "group": "wheel", "mode": "0755", "owner": "apple", "path": "/tmp/foo", "size": 64, "state": "directory", "uid": 501}

TASK [touch the file] **********************************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/tasks/files.yaml:7
changed: [localhost] => {"changed": true, "dest": "/tmp/foo/herd", "gid": 0, "group": "wheel", "mode": "0644", "owner": "apple", "size": 0, "state": "file", "uid": 501}

TASK [create leading path] *****************************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/tasks/files.yaml:2
ok: [localhost] => {"changed": false, "gid": 0, "group": "wheel", "mode": "0755", "owner": "apple", "path": "/tmp/foo", "size": 96, "state": "directory", "uid": 501}

TASK [touch the file] **********************************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/tasks/files.yaml:7
changed: [localhost] => {"changed": true, "dest": "/tmp/foo/cerd", "gid": 0, "group": "wheel", "mode": "0644", "owner": "apple", "size": 0, "state": "file", "uid": 501}
META: ran handlers
META: ran handlers

PLAY RECAP *********************************************************************************************************************************************************************
localhost                  : ok=4    changed=3    unreachable=0    failed=0   
给引入的任务传递复杂数据

当我们想给包含的任务文件传入复杂的数据,例如列表或hash,我们在引入任务文件的时候可以使用另外一种语法。

---
- name: create leading path
  file:
      path: "{{ item.value.path }}"
      state: directory
  with_dict: "{{ files }}"

- name: touch the file
  file:
      path: "{{ item.value.path + '/' + item.key }}"
      state: touch
  with_dict: "{{ files }}"

注意上面with_dict所提供的变量,使用引号包围起来。 如果直接写with_dict: files,会报"msg": "with_dict expects a dict", ansible版本2.6.x。对于1.9之前版本的应该可以。

参考: https://github.com/ansible/an...

然后重新修改touchfiles.yaml文件内容:

---
- name: touch files
  hosts: localhost
  gather_facts: false
  tasks:
      - include: tasks/files2.yaml
        vars:
          files:
            herp:
              path: /tmp/foo
            derp:
              path: /tmp/foo
注意: 在使用yaml语法给包含语句提供变量数据的时候,变量可以直接列举出来,使用不使用顶层的vars关键词都可以。 但是使用vars非常有用,如果变量的名字和ansible的控制参数重名的话,使用它就不会造成冲突。这也是前面我改path -> pathname, file -> filename的原因。
条件性任务包含

类似于向引入文件传入数据,我们也可以将条件传入包含的文件中。这是通过给include绑定一个when语句.这个条件不会导致ansible计算测试来决定文件是否被包含进去;而是,他指示ansible在包含的文件(以及前面所说的任何可能包含的其他文件)中给里边的每个任务添加条件。

不能条件性的包含一个文件。文件总是包含进去的;然而,任务条件可以应用给它们的每个任务。

需要记住的重要一点就是,所有主机都将评估所有包含的任务。没有办法影响ansible为某些主机不包含一个文件。 大多数情况下,条件可以被应用到包含层级里边的每个任务,这样包含的任务可以跳过。一种基于主机事实的包含任务可以利用group_by行为产检来根据主机事实创建动态组。然后,你可以给那个组它们自己的剧情来包含特定的任务。这点可以留给读者自己练习。

一种基于主机事实的包含任务可以利用group_by行为产检来根据主机事实创建动态组。然后,你可以给那个组它们自己的剧情来包含特定的任务。这点可以留给读者自己练习。
给引入的任务打标签

当包含任务文件的时候,可以给文件里边的所有任务打标签。tags就是用来定义一个或多个标签,并将它们应用到包含层级中的所有任务的。在包含时间点上打标签的能力可以让任务文件自己对任务该如何被打标签是无主见的, 并且可以允许任务集可以多次包含,但是每次都可以传递不同的数据和标签。

标签可以在include语句或者在剧情(play)自身定义,如果在剧情中定义,可以涵盖这个剧情里边的所有包含文件,包括其他非包含文件中的任务。

打标签之后,我们可以选择让哪个标签运行,通过ansible-playbook的--tags命令行参数来指定标签名。

ansible-playbook -i mastery-hosts includer.yaml -vv --tags second

另外,我们可以使用--skip-tags跳过某些标签的任务来运行。

包含处理器

处理器本质上是任务。它们是通过其他任务通知的方式触发的潜在任务集。像这样的话,处理器任务同样可以像一般任务一样被包含进去。include指令在handlers块里边也是合法的。

和任务包含不同的是,变量数据不能在包含处理器任务的时候一起传入。但是,还是可以给处理器包含绑定条件的,这样可以应用给包含文件里边的每一个处理器。

---
- name: touch files
  hosts: localhost
  gather_facts: false
  tasks:
    - name: a task
      debug:
      msg: "I am a changing task"
      changed_when: true
      notify: a handler
  handlers:
    - include: handlers.yaml
      when: foo | default('true') | bool
---
- name: a handler
  debug:
    msg: "handling a thing"

我们可以分别执行如下命令:

# 因为foo为true, 因此handler会执行
ansible-playbook -i mastery-hosts includer.yaml -vv


# 下面执行的时候,我们使用了外部变量foo, 定义为false, 因此handler就跳过去了。
ansible-playbook -i mastery-hosts includer.yaml -vv -e foo=false

包含变量

变量数据也可以被分离到可加载文件里边。这样就允许在多个剧情或剧本,以及项目目录之外的包含变量数据(比如密码数据)之间共享变量。变量文件是简单的yaml格式文件,以key-value对的形式出现。和任务包含文件不同,变量包含文件不能再包含其他更多文件。

变量可以以三种不同的方式被包含进来:

  • 通过vars_files
  • 通过include_vars
  • 通过--extra-vars(-e)
vars_files

vars_files是一个剧情指令。它定义了要读取变量数据的文件列表。这些文件在剧情自己解析的时候就会读取出来并解析的。和包含任务和处理器一样,路径是相对引用文件的相对路径。

---
- name: vars
  hosts: localhost
  gather_facts: false

  vars_files:
    - variables.yaml
   
  tasks:
    - name: a task
      debug:
        msg: "I am a {{ name }}"
name: derp
动态vars_files包含
---
- name: vars
  hosts: localhost
  gather_facts: false

  vars_files:
    - "{{ varfile }}"
   
  tasks:
    - name: a task
      debug:
        msg: "I am a {{ name }}"

然后可以通过ansible-playbook命令的-e参数传入动态的变量包含文件名。

ansible-playbook -i mastery-hosts includer.yaml -vv -e varfile=variables.yaml

另外,变量值需要在执行时间定义,因此要加载的文件在执行时间必须存在。即便如果引用的文件在剧本中位置可能是4个剧情后面出现的,这个引用的文件自己是由第一个剧情产生的,除非这个文件在执行时间存在,否则ansible-playbook就会报错。

include_vars

从文件中包含变量数据的第二种方法是使用include_vars模块。这个模块会以一个任务的行为加载变量,并且为每个主机做这些。和其他的很多模块不同,include_vars模块是在ansible主机本地执行的;因此,所有路径依然是相对于剧情文件自身的相对路径。因为变量加载是以任务的形式完成的,文件名字中的变量计算就是任务执行的时候发生的。文件名中的变量数据可以是特定主机的、也可以由前面任务定义的。另外,文件自身在执行时也不是必须存在的,它也可以由前面任务生成。这是一个非常强大和灵活的概念,如果恰当使用可以导致非常动态的剧本。

动态剧本实现可使用include_vars模块。
---
- name: vars
  hosts: localhost
  gather_facts: false
   
  tasks:
    - name: load variables
      include_vars: "{{ varfile }}"
    - name: a task
      debug:
        msg: "I am a {{ name }}"

剧本的执行和之前的保持一致,但是输出和前面的迭代稍有不同。

和其他任务一样,在单个任务中可以循环加载多个文件。 当我们使用with_first_found遍历列表,直到找到第一个文件为止就非常有效了。

---
- name: vars
  hosts: localhost
  gather_facts: true
   
  tasks:
    - name: load variables
      include_vars: "{{ item }}"
      with_first_found:
        - "{{ ansible_distribution }}.yaml"
        - "{{ ansible_os_family }}.yaml"
        -  variables.yaml
    - name: a task
      debug:
        msg: "I am a {{ name }}"

这次运行效果和前面基本一样,区别在于这次执行了主机信息搜集,另外我们执行的时候,没有传入额外的varfile参数了。

同时我们看到加载的文件名是variables.yaml, 因为其他两个文件不存在。实际上,这种实现通常用在给特定操作系统的主机加载变量的情况中。

各种不同的操作系统相关的变量可以保存在不同的变量文件中。通过利用变量ansible_distribution,由事实收集产生的,那么将ansible_distribution作为变量文件名的一部分,就可以使用with_first_found参数来加载它们了。同时可以建一个默认的不实用任何变量数据的变量集合文件,作为故障保护措施。

extra-vars

最后一种从文件中加载变量的方法是使用ansible-playbook的--extra-vars(-e)参数。通常来说,这个参数接收key=value格式的值;然而,如果提供文件名的话,可以使用@符号放文件名前面,ansible会读取整个文件并加载变量。

注意: 使用extra-vars加载变量文件的时候,ansible-playbook执行的时候这个变量文件必须存在。

包含剧本

剧本文件可以包含其他整个剧本文件。这种架构对于将一些独立的剧本打包到一起组成更大的,更加复杂的剧本就非常有用了。剧本包含比任务包含要更基础些。在包含剧本的时候,不能执行变量替换,不能应用条件,不能应用标签。要包含的剧本文件在执行时间也必须存在。

角色

对变量、任务、处理器和剧本的包含有了功能性理解,我们就可以转向更高级的角色主题。角色超越了对一些剧本以及分开文件的引用的基本结构。角色为完全独立或相互依赖的变量、任务、文件、模版以及模块集合提供了一个框架。每个角色通常限于一个特定的主题或想要的最终效果,所有达成结果的必要步骤,要么位于角色自身,要么位于依赖的角色列表中。

角色本身不是剧本。没有办法直接执行一个角色。角色没有设置它会应用于什么主机。顶级剧本是将你资产中的主机绑定到应该应用这些主机的角色上的胶水。

角色结构

角色在文件系统中有一个结构化的布局。这个结构在于提供自动化围绕,包括任务、处理程序、变量、模版和角色依赖。该结构还允许在角色里边的任意地方轻松的引用文件和模版。

角色都位于剧本档案的子目录中,即roles目录。当然,这个是通过配置中的roles_path来配置的,但是这里我们就使用默认值。 每个角色自身都是一个目录树。 角色名就是位于roles目录下面的子目录的名字。每个角色都可以有一些特殊意义的子目录,在角色应用给主机集合的时候会被处理成特殊意义的。

一个角色可以包含所有这些元素或者少数的这些元素。没有的元素会被忽略掉。一些角色存在的价值就是为跨项目提供一些通用的处理器。其他角色以单独的依赖点存在,而依赖点又依赖一些其他角色。

任务

任务是角色的主要角色。如果roles/<role_name>/tasks/main.yaml存在的话,那里边的所有任务以及他包含的所有其他文件就会被嵌入到剧情中,并被执行。

处理器

类似于任务,如果存在的话,处理器会自动从roles/<role_name>/handlers/main.yaml中加载。这些处理器可以被角色中的任何任务引用,或者列表中依赖这个角色的其他角色的任务引用。

变量

角色中可以定义两种类型的变量。这些都是角色变量,可以从roles/<role_name>/vars/main.yaml中加载,还有一种就是角色默认值,位于roles/<role_name>/defaults/main.yaml。两者区别在于优先级。角色默认值优先级最低。字面上讲就是,变量的任何其他定义都比角色默认值优先级高。角色默认值可以认为是实际数据的占位,引用什么变量开发者可能感兴趣的是使用特定站点值来定义。

另一方面,角色变量具有较高的优先级。角色变量可以被覆盖,但一般来说,当一个角色中同一个数据集被引用多次时会使用。 如果使用站点本地值来重定义数据集,那么这些变量应该在角色默认值中列出来,而不是在角色变量中。

模块

角色可以包含自定义模块。虽然ansible项目非常善于审查和接受新提交的模块,但是在某些情况下,向上游提交自定义模块可能并不可取,甚至无效的。这种情况下,将模块交付给角色可能是更好的选择。模块位于roles/<role_name>/library/下面,可以在角色或后续的角色中的所有任务中使用。这个地方提供的模块会覆盖文件系统中的其他任何地方的同名模块,这也是一种在上游还没有接受和使用新版发布之前,分散的给核心模块添加功能的方式。

依赖

角色可以表达对另外角色的依赖。通常做法是,角色集都依赖一个共同的角色,这样就依赖任意任务、处理器、模块等等了。可能依赖的这些角色只需要在通用角色中定义一次。当ansible为主机集合处理一个角色的时候,他首先查找在roles/<role_name>/meta/main.yaml中列举的所有依赖。

如果定义了任何依赖的话,在开始最初的角色任务之前,这些依赖角色会被处理,它们里边的任务也会执行(当然在检查完列举的所有依赖之后),直到所有依赖都完成。

文件和模版

任务和处理器模块可以引用相对位于roles/<role_name>/files/下面的文件。文件名可以不用任何前缀,那么会在files目录取得源码。也允许使用相对前缀,为了访问在files子目录的文件。类似template, copy和script模块可以利用这个。

类似的,模版是template模块使用的,可以从templates目录中引用模版文件。

- name: configure hrep
  template:
    src: herp/derp.j2
    dest: /etc/herp/derp.j2
整合在一起

为了演示角色结构看起来的样子,下面是一个demo角色的目录结构:

roles/demo
├── defaults
│   └── main.yaml
├── files
│   └── foo
├── handlers
│   └── main.yaml
├── library
│   └── samplemod.py
├── meta
│   └── main.yaml
├── tasks
│   └── main.yaml
├── templates
│   └── bar.j2
└── vars
    └── main.yaml

在创建角色的时候,这里的每一个目录或文件都不是必须的,只有存在的文件才会被处理。

角色依赖

前面我们提到,角色可以依赖其他角色。这些关系叫做依赖关系,它们是在meta/main.yaml中描述的。 这个文件期望又一个顶级的数据hash, 使用dependencies键;里边的数据是一个角色列表。

---
dependencies:
  - role: common
  - role: apache

这个例子中,ansible会首先完全处理common角色(以及它可能表达的任何依赖),然后继续apache角色,最后开始角色自己的任务。

依赖可以通过不带任何前缀的名字来引用,如果它们在同一目录下存在,或者在配置的roles_path中存在。否则,需要提供可以定位角色的完整路径。

当表达依赖的时候,能够给依赖传递数据。数据可以是变量、标签、甚至是条件。

角色依赖变量

列举依赖的时候传递变量会覆盖defaults/main.yaml或vars/main.yaml中匹配的变量值。这点对于使用通用角色,例如apache,作为依赖时就非常有用,可以提供特定应用的数据,例如需要开启的端口号,或者启用什么apache模块。变量可以表达为角色列表的额外key。

dependencies:
  - role: common
    simple_var_a: True
    simple_var_b: False
  - role: apache
    complex_var:
      key1: value1
      key2: value2
    short_list:
      - 8080
      - 8081

当提供依赖变量时,两个名字是保留的,它们不能用于角色变量: tags, when。这两个分别用于传入标签和条件的。

标签

标签可以应用到依赖角色中发现的所有任务上。和前面包含任务文件中传入标签类似。标签可以是列表,可以是单个项目。

---
dependencies:
  - role: common
    simple_var_a: True
    simple_var_b: False
    tags: common_demo
  - role: apache
    complex_var:
      key1: value1
      key2: value2
    short_list:
      - 8080
      - 8081
    tags:
      - apache_demo
      - 8080
      - 8081
角色依赖条件

在条件中不能防止依赖角色的处理,但是给依赖应用条件可以跳过依赖角色层级中的所有任务。这是任务包含中使用条件的镜像功能。

dependencies:
  - role: common
    simple_var_a: True
    simple_var_b: False
    tags: common_demo
  - role: apache
    complex_var:
      key1: value1
      key2: value2
    short_list:
      - 8080
      - 8081
    tags:
      - apache_demo
      - 8080
      - 8081
    when: backend_server == 'apache'

角色应用

角色不是剧情。它们不处理任何关于角色应该运行在那些主机、使用什么连接、是否串形操作、或者其他剧情行为的选项。角色必须在剧本中的剧情里边应用,在剧情中可以表达这些选项。
在剧情中应用角色,可以使用roles操作符。这个操作符期望应用给主机的所有角色。有点类似前面说的角色依赖,当描述了应用的角色,数据也可以一起传入,例如变量、标签、条件等等。语法完全一样。

---
- hosts: localhost
  gather_facts: false
  roles:
    - role: simple
      derp: newval
    - role: second_role
      othervar: value
    - role: third_role
    - role: another_role
混合角色和任务

剧情使用角色但不限于角色。这些剧情可以有它自己的任务,也可以有其他的任务块: pre_tasks和post_tasks。这些任务的执行不依赖它们在剧情中出现的顺序;而是遵照一种严格的顺序。

    1. 进行变量加载
    1. 事实搜集
    1. pre_tasks执行
    1. 由pre_tasks中通知的处理器执行
    1. 角色执行
    1. 任务执行
    1. 角色或任务通知的处理器执行
    1. post_tasks执行
    1. 由post_tasks通知的处理器执行

剧情的处理器可以在多个点被刷新。首先是pre_tasks执行,这个过程可能会刷新特定的处理器,然后是角色和任务,注意角色先执行,任务后执行(不论它们谁先出现谁后出现)。角色和任务执行的过程中可能再次刷新处理器。 然后执行post_tasks, 最后由post_tasks刷新的处理器会执行。

另外,任何地方都可以使用meta: flush_handlers调用来刷新处理器。

虽然执行顺序不一定是它们在剧情中出现的顺序,但是我们最好让它们以执行顺序出现,这样我们的剧情就会比较容易看懂,执行的时候不会感觉困惑了。

角色共享

使用角色的一个有点就是有能力在剧情、剧本、整个项目空间、甚至跨组织之间共享角色。角色设计成独立的(或明确的依赖角色),以便它们可以存在于应用角色的剧本的项目空间之外。角色可以安装到ansible主机的共享路径下面,或者可以通过源码控制来分发。

ansible galaxy

ansible galaxy是一个查找和共享ansible角色的社区。
另外ansible-galaxy工具可以从网站上连接并安装角色。默认会安装到/etc/ansible/roles目录下面,当然如果配置了roles_path的话,就安装到你配置的目录下面。

  • 安装角色: ansible-galaxy install -p installpath role-name
  • 列出安装角色: ansible-galaxy list -p installpath
  • 查看安装角色信息: ansible-galaxy info -p installpath role-name
  • 删除角色: ansible-galaxy remove -p installpath role-name
  • 创建角色: ansible-galaxy init role-name

安装角色比较灵活,可以直接提供名字,也可以通过git地址,还可以直接是tar包,还可以配置单独的yaml同时安装多个角色包。

---
- src: <name or url>
  path: <optional install path>
  version: <optional version>
  name: <optional name override>
  scm: <optional defined source control mechanism>
- src: <name or url>
  path: <optional install path>
  version: <optional version>
  name: <optional name override>
  scm: <optional defined source control mechanism>
- src: <name or url>
  path: <optional install path>
  version: <optional version>
  name: <optional name override>
  scm: <optional defined source control mechanism>

然后执行安装命令,通过--roles-file(-r)选项来安装:

ansible-galaxy install -r install-roles.yaml

总结

Ansible provides the capability to divide content logically into separate files. This capability helps project developers to not repeat the same code over and over again. Roles within Ansible take this capability a step further and wrap some magic around the paths to the content. Roles are tunable, reusable, portable, and shareable blocks of functionality. Ansible Galaxy exists as a community hub for developers to find, rate, and share roles. The ansible-galaxy command-line tool provides a method
of interacting with the Ansible Galaxy site or other role-sharing mechanisms. These capabilities and tools help with the organization and utilization of common code.
In the next chapter, we'll cover different deployment and upgrade strategies and the Ansible features useful for each strategy.

目录


老将廉颇
878 声望297 粉丝