5

ansible系统架构和设计

本章详细探索了关于ansible如何替你执行任务的架构和设计细节。我们将介绍inventory解析的基本概念以及数据如何发现数据,然后深入到playbook解析。我们会过一下模块的准备、传输以及执行。最后,我们将详细描述变量类型以及探寻变量可以位于何处、它们可以使用的范围以及当多处定义变量时如何确定优先级。所有这些都会涵盖到,以便为掌握ansible打好基础。

本章我们会讨论下面这些话题:

  • ansible版本及配置。
  • inventory解析及配置。
  • playbook解析。
  • 模块传输和执行。
  • 变量类型和位置。
  • 变量优先级。

ansible版本及配置

假设你已经在系统中安装了ansible。有很多关于如何安装适合你系统的ansible的文章。本书假设你使用的是ansible 1.9.x。查看你系统中安装的ansible版本号,可以通过命令ansible-playbook --version查看。

注意ansible用于执行临时的单个任务,而ansible-playbook用于执行那些使用playbook编排了很多任务的命令。

ansible的配置文件可以位于一些不同的位置,它会按顺序查找并使用第一个找到的文件。查找的顺序在1.5稍微做了调整,使用下面的顺序进行查找的:

  • ANSIBLE_CONFIG: 这是一个环境变量,即定义了该环境变量,就使用它指定的位置。
  • ansible.cfg: 当前目录下面的ansible.cfg文件。
  • ansible.cfg: 用户home目录下面的的ansible.cfg文件。
  • /etc/ansible/ansible.cfg文件。

有些安装方法可能在上面其中某个位置放一个ansible.cfg文件。找找这个文件在哪里,然后看看里边有些什么内容,了解下ansible操作是如何受这些配置参数影响的。本书假设ansible.cfg中不存在影响ansible操作的默认行为的配置。

inventory解析及数据源

在ansible中,如果没有inventory就什么也不会发生。甚至是临时的(ad hoc)localhost上执行的行为都需要inventory,即使inventory仅仅包含一个localhost。 inventory是ansible架构中最基本的构建块。当执行ansible或ansible-playbook的时候,必须引用一个inventory。inventory可以是运行ansible或ansible-playbook的同一个机器上的文件或目录。inventory的位置可以在运行时通过--inventory-file(-i)参数指定或者在ansible配置文件中指定。

inventory可以是静态的或者动态的,或者甚至是动静结合的。ansible不限于一个inventory。标准做法是在逻辑边界上进行拆分,例如staging和production, 允许工程师根据他们的staging环境验证执行不同的演奏集合(剧本不同,演出效果(play)不同嘛), 然后根据production inventory集合遵循同样精确的演奏来运行。

变量数据,例如你的inventory中如何连接到特定主机的特定细节,也可以以各种方式和inventory包含在一起, 我们也会为你探讨一些可用选项。

静态inventory

静态inventory是所有inventory选项中最基本的。 一般来说,静态inventory是以ini格式的单个文件组成。下面是一个静态inventory文件的例子,描述了单个主机: mastery.example.com。

mastery.example.com

这就是它的所有内容。 金蛋的列举了你的inventory中系统的名字。当然,这没有完全利用起来inventory所提供的完整优势。如果名字都以这种形式列举出来,所有的演奏(play)必须引用特定的主机名或者all这个特殊的组。这可能在不同的基础设施上开发剧本时就非常乏味了。至少,应该把各种不同的主机进行分组。一个很好的设计模式就是根据你系统所希望的功能进行分组安排。一开始,这可能看起来比较困难,如果你的环境是一个单独的系统,可能扮演了很多不同的角色,但是这是相当好的。一个inventory中的系统可以位于多个组中,每个组甚至可以还包含其他组。另外,当使用分组和主机进行列举的时候,还是可以列出没有分组的主机。这些没有分组的主机需要在其他组定义之前被列举出来。

下面让我们基于前面的例子构建并扩大我们的inventory, 添加一些新的主机和一些分组:

[web]
mastery.example.com

[dns]
backend.example.name

[database]
backend.example.name

[frontend:children]
web

[backend:children]
dns
database

我们这里创建了一个具有三个分组的集合,每个组一个系统,另外又创建了两个组,这两个组逻辑上对前面组进行了再次分组。 使得,非常棒;你可以有分组的分组。这里使用的语法是[groupname:children], 它可以指示ansible的inventory解析器,groupname分组是其他分组的组。这个情况下子分组的值就是其他组的名字。 这个inventory允许根据特定主机、低级别的特定角色组、或者高级别的逻辑组、或者两者组合来写演奏(play)。

通过利用一般的分组名,例如dns和database, ansible演奏可以引用这些一般的组,而不用引用组中具体的主机名了。工程师可以创建使用预生产阶段环境的主机来填充这些组的一个inventory文件,和使用生产版本的主机来填充这些分组的另外一个inventory文件。简单的在想要的环境中引用正确的inventory来执行。

inventory变量数据

inventory提供了比系统名称和分组更多的东西。关于系统的数据也可以一起传递。包括:

  • 模版中使用的特定主机数据。
  • 任务参数或条件中使用的特定分组数据。
  • 调节(tune)ansible如何和系统交互的行为参数。

变量是ansible中强大的结构,可以在各种方式下使用,不仅仅是这里描述的方式下。 几乎ansible中做的每个单独的事情都能包含一个变量引用。ansible在设置阶段能发现关于系统的数据,并不是所有的数据都能发现。使用inventory定义数据是如何扩大数据集。注意变量数据可以来自很多不同的源,并且它们之间可以覆盖的。变量优先级在本章后面会介绍。

下面我们增强我们前面的inventory,添加一些变量数据。我们将添加一些特定主机的数据以及特定分组的数据:

[web]
mastery.example.name ansible_ssh_host=192.168.10.25

[dns]
backend.example.name

[database]
backend.example.name

[frontend:children]
web

[backend:children]
dns
database

[web:vars]
http_port=88
proxy_timeout=5

[backend:vars]
ansible_ssh_port=314

[all:vars]
ansible_ssh_user=otto

上面例子中,我们给master.example.name添加了ansible_ssh_host变量,使用了一个IP地址。ansible_ssh_host是一个行为inventory参数,当操作这个主机的时候会改变ansible行为。在这个例子中,这个参数会通知ansible使用提供的IP地址来连接系统,而不是通过dns查询来连接。 还有一些其他的行为inventory参数,在本节最后会连带他们被希望使用的目的一起列出来。

我们新的inventory数据也为web和backend组提供了组级别的变量。web组定义了http_port, 这个变量可能在nginx配置文件中使用,proxy_timeout变量,这个变量可用于决定HAProxy行为。backend组使用了另外一个行为inventory参数来告诉ansible连接这个组中的主机时使用端口号314作为ssh的端口号,而不是默认的22端口号。

最后,引入了另外一个结构,提供了跨越这个inventory所有主机的变量数据,利用了内置的all分组。这个组中定义的变量会应用到这个inventory中的所有主机。在这个特殊的例子中,我们会告诉ansible在连接系统的时候用otto用户登录。这也是一种行为的改变,因为ansible默认行为是使用控制主机上执行ansible或ansible-playbook的用户相同的用户来登录的。

下面是行为inventory参数,以及他们想要修改的行为:

  • ansible_ssh_host: 这是要连接主机的名字,如果和你想要别名不同,你可以提供这个参数。
  • ansible_ssh_port: 这是ssh连接的端口号,如果不是使用22的话。
  • ansible_ssh_user: 这是默认ssh连接要使用的用户名。
  • ansible_ssh_pass: 这是ssh连接使用的密码(这是不安全的, 我们强烈推荐使用--ask-sudo-pass)。
  • ansible_connection: 这是主机的连接类型。可选值有local, smart, ssh和paramiko。 在ansible1.2之前默认是paramiko, 之后默认使用smart,smart会根据是否支持ssh特性ControlPersist来检测使用ssh是否可行。
  • ansible_ssh_private_key_file: 这是ssh使用的私匙文件。如果你使用多个keys,并且你不希望使用ssh代理的时候非常有用。
  • ansible_shell_type: 这个是目标系统的shell类型。默认情况下,命令都是以sh形式语法格式化的。当设置该变量为csh或fish的时候,会导致命令在这些目标系统上执行的时候遵循这些具体的shell语法。
  • ansible_python_interpreter: 这是目标主机的python路径。对于那些具有多个版本python、或者python不在/usr/bin/python(例如*BSD)、或/usr/bin/python版本不是2.x的python的系统来说会非常有用。我们不使用/usr/bin/env机制,因为他需要远程用户的路径设置正确,并且也假设python可执行程序是以python命名的,因为可执行程序可能使用其他名字命名,例如python26。
  • ansible_*_interpreter: 这个对于类似Ruby或Perl的那些适用,和ansible_python_interpreter的工作原理一样。这个替换运行在那些主机上的模块的位置。

动态inventory

静态inventory已经非常强大,已经满足很多情况了。 但是有时候,静态书写的主机集合泰太过笨重,不便管理。考虑这样的情况,inventory数据在另外一个系统中已经存在,例如LDAP, 一个与计算提供商,或者一个机构内部CMDB(inventory, 资产跟踪,以及数据仓库)系统。复制那些数据可能很浪费时间和精力,在按需设施的现代世界中,这些数据将很快变得陈旧或者灾难性的错误。

另外一个例子就是,当想得到的动态inventory源是当你网站增长到超过单个剧本集合的时候。多个剧本仓库可能陷入保存同一个inventory数据多个拷贝本的陷阱,或者需要创建复杂的处理来引用数据单个的副本。外部inventory可以很容易促使改变来访问存储在剧本仓库之外的通用inventory数据来简化设置。幸好,ansible不限于静态inventory文件。

动态inventory源(或plugin)就是一个可执行脚本,ansible可以在运行时调用来发现实时的inventory数据。这个脚本可以达到外部数据源内部,并返回数据,或者它可以解析本地已经存在的数据,但是它们可能不是以ansible inventory ini格式提供的。它是可能的,并且很容易开发你自己的动态inventory源,我们后面会介绍,ansible提供了一些inventory插件例子,包括但不限于下面这些:

  • OpenStack Nova
  • Rackspace Public Cloud
  • DigitalOcean
  • Linode
  • Amazon EC2
  • Google Compute Engine
  • Microsoft Azure
  • Docker
  • Vagrant

这些插件很多都需要一些等级的配置,例如EC2的用户认证或OpenStack Nova的授权终端。既然不可能为ansible配置额外参数来和inventory脚本一起传递,这些脚本的配置必须通过从已知位置读取的ini配置文件来管理,或者从用于执行ansible或ansible-playbook的shell环境变量读取的环境变量来管理。

当ansible或ansible-playbook在一个inventory源可执行文件控制,ansible将执行那个脚本使用一个单独的参数,--list。这也是ansible可以获取整个inventory的列表,以便于构建他自己的代表数据的内部对象。一旦数据构建好,ansible将执行这个脚本,为不同的数据中的主机使用不同的参数来发现变量数据。这个执行中使用的参数是--host <hostname>,它将返回那个主机特定的任何变量数据。

在第八章, 扩展Ansible,我们会开发我们自己的自定义inventory插件来演示他们如何操作的。

运行时增加的inventory

和静态inventory文件类似,需要记住的重要一点就是ansible在每个ansible或ansible-playbook执行中将只解析它的数据一次。这是对于那些使用云动态源的用户来说非常容易出错的点,一个剧本经常性的会创建一个新的云资源,并且然后会尝试使用它,就认为它就是inventory的一部分。这就会失败,因为这个资源在剧本启动的时候还不是inventory的一部分。但一切都没有丢掉!提供了一种特殊的模块,它允许剧本临时添加inventory到内存中的inventory对象中,这个模块就是add_host。

add_host模块接收两个选项,name和groups。name很明显,他定义的是ansible用于连接这个特殊系统的主机。groups选项是逗号分隔的要添加到这个新系统的组列表。传给这个模块的其他选项会编程这个主机的主机变量数据。例如,如果我们希望添加一个新的系统,name是newmastery.example.name, 将它添加到web分组, 指示ansible通过IP地址192.168.10.30来连接它,我们就可以创建类似下面的任务:

- name: add new node into runtime inventory
  add_host:
    name: newmastery.example.name
    groups: web
    ansible_ssh_host: 192.168.10.30

这个新的主机通过提供的name,或web分组名对于ansible-playbook执行的其余部分可用。然而,一旦执行完成,这个主机将不可用,除非它已经被加入到inventory源中。当然,如果这是一个创建的新云资源,下一次ansible或ansible-paybook执行的时候,来自那个云的源inventory会捡起这个新成员的。

inventory限制

正如前面提到的,每次ansible或ansible-playbook执行都会解析它一直指向整个inventory。当limit应用的时候一样适用。限制是在运行时通过使用--limit运行时参数给ansible或ansible-playbook来应用的。这个参数接收一个模式,一般是一个掩码来应用inventory。整个inventory被解析,并且每个演练提供的limit掩码会进一步限制演练限制的主机模式。

-------- 待补充 -------------

playbook解析

inventory源的整个目的就是要有系统可操作。操作来自于playbook(或者ansible临时执行的情况,简单的单个任务演练(play))。你可能已经有了playbook构建的基本理解,因此我们不会花大量时间来涵盖它,然而,我们将深入研究一些剧本如何解析的具体内容。具体来说,我们将包含以下内容:

  • 操作顺序。
  • 假设的相对路径。
  • 演练行为keys。
  • 演练和任务主机选择。
  • 演练和任务名字。

操作顺序

ansible被设计为尽可能的让人容易理解。开发人员努力在人类理解和机器效率之间做权衡。为此,ansible中几乎所有的东西都假设按上下顺序执行的;也就是文件顶部列出的操作要比文件底部列出的操作要先执行完。话虽如此,还是要有一些附加说明,还是存在一些影响操作顺序的方式。

playbook只有两种可以完成的主要操作。它能运行一个演练,也可以包含一个文件系统上的另外一个剧本。

完成这些操作的顺序就是它们在playbook文件中出现的顺序,从上而下的。重点需要注意的是,当操作按顺序执行的时候,整个剧本和任何包含的剧本在执行之前都已经完全解析了的。这就意味着任何包含剧本的文件都必须在剧本解析时存在。它们不能是前面操作生成的。在某个演练中,还有一些操作。虽然剧本是从上到下严格排序的,但某个演练操作顺序更为微妙。这里是可能的操作列表和它们将发生的顺序:

  • 变量加载。
  • 事实搜集。
  • pre_tasks执行。
  • 来自pre_tasks执行通知的处理器。
  • 角色执行。
  • 任务执行。
  • 从角色和任务执行通知的处理器。
  • post_tasks执行。
  • 从post_tasks执行通知的处理器。

下面是一个演练例子,带有这些展示的操作的大部分:

---
- hosts: localhost
  gather_facts: false
  
  vars:
    - a_var: derp
  pre_tasks:
    - name: pretask
      debug: msg="a pre task"
      changed_when: true
      notify: say hi
  roles:
    - role: simple
      derp: newvar
  tasks:
    - name: task
      debug: msg="a task"
      changed_when: true
      notify: say hi
  post_tasks:
    - name: posttask
      debug: msg="a post task"
      changed_when: true
      notify: say hi

不管这些块的顺序在一个play中按什么顺序列举出来,这就是它们将会被处理的顺序。处理器是特殊例子。处理器就是可以由其他能导致改变的任务触发的任务。由一个工具模块meta,它可以用于在那个点上触发处理器处理。

- meta: flush_handlers

这个将指示ansible在该点处,在继续play中的下一个任务或下一个操作块之前,处理任意挂起的处理程序。理解顺序和能够使用flush_handlers影响顺序是在需要编排复杂的行为的时候,具有的另外一个关键技能, 在那个地方,类似服务启动对于顺序非常敏感的。

考虑服务首次展示的情景。play需要具有这样的任务,修改配置文件、并指示这些文件发生改变时,应该重启服务。这个play也应该表识服务应该运行。这个play第一次发生,配置文件会改变,服务会从非运行状态转换为运行状态。然后,处理器会触发,它会导致服务立即重启。 这对于这个服务的任何消费者来说都是扰乱性的。最好在最最后一个任务之前刷新handlers,确保服务正在运行。这样的话,重新启动将在初始启动之前发生,因此服务将启动一次并保持不掉线(stay up)。

相对路径假设

当ansible解析剧本的时候,做了一些关于在剧本语句引用的项目是相对路径的假设。大多数情况下,例如包含的变量文件、任务文件、剧本文件、要拷贝的文件、要展示的模版、要执行的脚本等等一些东西的路径都是相对于引用他们的文件所在的目录的。让我们用个例子探讨下剧本以及目录列表,来展示这些东西都在什么地方。

# 目录结构
.
|----- a_vars_file.yaml
|----- mastery-hosts
|----- relative.yaml
+----- tasks
         |------  a.yaml
         +------  b.yaml

_vars_file.yaml内容如下:

---
something: "better than nothing"

relative.yaml内容如下:

---
- name: relative path play
  hosts: 127.0.0.1
  gather_facts: false
  
  vars_files:
    - a_vars_file.yaml
  
  tasks:
    - name: who ami I
      debug:
        msg: "I am mastery task"
      
    - name: var from file
      debug: var=something
      
    - include: tasks/a.yaml

tasks/a.yaml内容如下:

---
- name: where am I
  debug:
    msg: "I am task a"
- include: b.yaml

tasks/b.yaml内容:

---
- name: who am I
  debug:
    msg: "I am task b"

mastery-hosts内容:

localhost

然后运行剧本,执行下面命令:

$ ansible-playbook -i mastery-hosts relative.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: /pathto/tasks/a.yaml
statically imported: /pathto/tasks/b.yaml

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

PLAY [relative path play] ******************************************************************************************************************************************************
META: ran handlers

TASK [who am I] ****************************************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/relative.yaml:10
ok: [localhost] => {
    "msg": "I am mastery task"
}

TASK [var from file] ***********************************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/relative.yaml:14
ok: [localhost] => {
    "something": "better than nothing"
}

TASK [where am I] **************************************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/tasks/a.yaml:2
ok: [localhost] => {
    "msg": "I am task a"
}

TASK [who am I] ****************************************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/tasks/b.yaml:2
ok: [localhost] => {
    "msg": "I am task b"
}
META: ran handlers
META: ran handlers

PLAY RECAP *********************************************************************************************************************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0   

我们可以清楚的看到路径的相对地址引用,以及它们如何相对于引用它们的文件的。在使用角色时,还有一些额外的相对路径假设,我们会在后续的章节中介绍。

演练行为keys

当ansible解析一个play的时候,有一些keys会查找这个play的各种行为定义。这些keys在同一级别写出,类似hosts: key。

下面是一些我们可以使用的keys:

  • any_errors_fatal: 这是一个布尔值,用于指示ansible将任何失败作为致命错误,防止进一步的任务尝试。这个可以改变ansible行为:默认情况下,ansible会在所有任务完成或所有主机失败才退出。
  • connection: 这个是字符串key, 定义用于定义用于给定play的连接系统的类型。这个key通常选择使用local, 指示ansible在本地做所有操作,但是带有来自inventory的系统上下文。
  • gather_facts: 这个布尔key控制ansible是否执行操作阶段的事实收集,运行在主机上一个特殊的任务会发现关于系统的各种事实。当你确认你不需要任何发现的数据的时候,你可以跳过搜集事实,这样在大型的环境中可以很明显的节约时间。
  • max_fail_percentage: 数字类型的key, 类似于any_errors_fatal,但是更加细粒度的。允许你定义你的的主机在多少百分比失败的情况下整个操作会被停下来。
  • no_log: 这是一个boolean类型的key, 控制ansible是否将给定命令或从任务接收到的结果打日志。如果任务很重要或者返回与机密有关的时候就非常有用。这个key也可以直接应用到一个任务上。
  • port: 这是一个数字key, 定义ssh(或另外一个远程连接插件)应该使用的连接端口号,除非在inventory数据中配置过。
  • remote_user: 这是一个字符串key, 定义使用什么用户登录远程系统。默认使用启动ansible-playbook的用户。
  • serial: 这个key接收一个数字值,控制ansible在移动到play的下一个任务之前需要在多少个系统上执行任务。 这个key是极端改变操作的正常顺序,默认情况下,在一个play过程中,跨越所有系统执行完任务才会移动到下一个任务。这个key在你需要滚动更新的场景下会非常有用,后面章节会详细陈述。
  • sudo: 这个是一个boolean值,配置在远程主机上执行任务的时候是否需要sudo用户。这个key也可以定义在特定任务级别。二级key, sudo_user可以用于配置sudo要切换到什么用户(而不是root)。
  • su: 和sudo非常类似,这个key用户使用su代替sudo. 这个key也有一个组合su_user, 配置su到的用户名(instead of root)。

上面的很多key在本书中的剧本中会有大量使用。

演练和任务的主机选择

大多数play首先要定义的事情就是这个play的主机模式。这就是用于从inventory对象中选择出要运行任务的主机列表。通常都非常简单: 一个主机模式包含一个或多个块,指示用于选择的主机、分组、通配符模式、或正则表达式。这些块以分号分隔,通配符就是一个星号,正则表达式以波浪线开头:

hostname:groupname:*.example:~(web|db)\.example\.com 

高级使用可能包含组索引选择或甚至是组中的一个选区:

Webservers[0]:webservers[2:4]

每一块被视为一个包含块,也就是,第一个模式匹配到的主机会添加到下一个模式匹配的主机里,以此类推。然而,可以通过控制字符串来改变这个行为。使用&符号可以实现交集选择(在两个模式中都存在的主机)。使用感叹号可以实现排除选择(所有在前面模式中匹配,但不在后面模式匹配出现的主机列表)。

Webservers:&dbservers
Webservers:!dbservers
  • 使用:实现并集选择。
  • 使用&符号实现交集选择。
  • 使用!实现排除选择。

一旦ansible解析模式后,如果有的话,它就应用限制。限制以limit或失败主机数来实现。在play进行的过程中,结果会保存起来,并且可以通过play_hosts变量来访问它们。随着每个任务的执行完成,这个数据就作为参考,额外的限制可能应用起来处理串形操作。随着遇到失败的情况,即连接失败或执行任务失败,失败的主机被放到限制列表中,这样下一个任务进行的时候,这些主机就会绕过。 如果在某个时刻,主机选择routine被限制到零的时候,play执行就会停止,并报错。 这里需要注意的是,play如果配置了max_fail_percentage或any_error_fatal参数,剧本执行会在条件满足的时候立即停止。

演练和任务名字

养成良好习惯,给你的play和task都打上标签,虽然这不是必须的。 这些名字会展示在ansible-playbook的命令行输出上, 如果ansible-playbook直接到日志文件,也会附加到日志文件中。任务名也将派上用场,指导ansible-playbook在特定任务中启动,并引用处理器。

在命名play和task的时候,需要注意两点:

  • play和task的名字必须唯一。
  • 需要注意play和task名称中可以使用什么样的变量名。

命名play和task唯一性是最佳实践,有助于快速识别有问题任务可能位于剧本、角色、任务文件、处理器等等层次的什么地方。当通知一个处理器或启动一个特定任务的时候,唯一性很重要。当任务名有重复的时候,ansible的行为可能是不确定的,至少是不明显。

将唯一性作为一个目标,很多playbook的作者将寻找满足这个限制的变量。这种策略可能会很好,但是作者需要注意它们引用变量数据的来源。可变数据可以来自各种地方(在本章后面会介绍), 赋值给变量的值可以在各种不同时间定义。为了play和任务的名字,需要记住的重要点就是,变量的值只能在playbook解析时间确定,才能正确的解析和呈现。如果引用变量的数据是通过任务或其他操作发现的,这个变量字符串在输出中会现实为未解析的。让我们看一个使用变量作为play和任务名的剧本例子。

---
- name: play with a {{ var_name  }}
  hosts: localhost
  gather_facts: false

  vars:
      - var_name: not-master

  tasks:
      - name: set a variable
        set_fact:
            task_var_name: "defined variable"

      - name: task with a {{ task_var_name  }}
        debug:
            msg: "I am mastery task"

- name: second play with a {{ task_var_name }}
  host: localhost
  gather_facts: false

  tasks:
      - name: task with a {{ runtime_var_name }}
        debug:
            msg: "I am another mastery task"

执行剧本,输出如下:

ansible-playbook -i mastery-hosts another.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

PLAYBOOK: another.yaml *********************************************************************************************************************************************************
2 plays in another.yaml

PLAY [play with a not-master] **************************************************************************************************************************************************
META: ran handlers

TASK [set a variable] **********************************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/another.yaml:10
ok: [localhost] => {"ansible_facts": {"task_var_name": "defined variable"}, "changed": false}

TASK [task with a defined variable] ********************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/another.yaml:14
ok: [localhost] => {
    "msg": "I am mastery task"
}
META: ran handlers
META: ran handlers

PLAY [second play with a {{ task_var_name }}] **********************************************************************************************************************************
META: ran handlers

TASK [task with a {{ runtime_var_name }}] **************************************************************************************************************************************
task path: /Users/apple/Sites/workspace/k8s-cluster/ansibledemo/another.yaml:23
ok: [localhost] => {
    "msg": "I am another mastery task"
}
META: ran handlers
META: ran handlers

PLAY RECAP *********************************************************************************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0

乍一看,我们可能期望至少var_name, task_var_name能正确呈现。但是我们可以清楚的看到task_var_name在使用之前定义的。然而,根据我们前面了解的知识,剧本在执行之前完全解析了.因此正如我们上面看到的,只有var_name是正确呈现,因为它是以静态play变量定义的。

模块传输及执行

一旦playbook被解析后,主机就确定了,ansible就准备执行任务。任务由名字(可选的,但是不要省略它)、模块参数、以及任务控制关键词组成。下一章会涵盖任务控制关键词的细节,因此我们这里只关心模块引用和参数。

模块引用

每个任务都有一个模块引用。它会告诉ansible要做的工作是什么。ansible的设计很容易让我们自定义模块与剧本相伴而生。这些自定义模块可以是全新功能,或者可以替换ansible自己原有模块。当ansible解析任务,并发现任务使用的模块名,它就会在一系列目录中查找这个任务请求的模块。在哪里查找同样依赖任务的位置,无论实在角色中还是不在。

如果任务在角色中,ansible会首先在角色所在的目录树中查找模块。如果没有找到,ansible会在主剧本中的library目录中查找。如果还是没有找到,ansible最后会在配置的类库目录查找,ansible最终在配置的类库路径查找,一般默认在/usr/share/ansible下面。这个类库的目录可以在ansible.cfg中配置或者通过环境变量ANSIBLE_LIBRARY来设置。

这种设计,让模块绑定到roles和剧本上了,允许添加功能,或者非常容易的快速修复问题。

模块参数

模块参数并不总是必须的;模块的help输出会指出那些模块需要而那些模块不需要。模块文档可以通过ansible-doc命令来访问。

参数可以使用Jinja2来模版化,它能在执行时间进行解析,允许在之前任务中发现的数据用于后续任务;这是一个非常强大的设计。

参数可以使用key=value的格式、或者以一种复杂格式提供,对于yaml来说非常原生。下面是用两种格式传入模块的参数展示例子。

- name: add a keypair to nova
     nova_keypair: login_password={{ pass }} login_tenant_name=admin
                   name=admin-key
- name: add a keypair to nova
     nova_keypair: login_password: "{{ pass }}" login_tenant_name: admin
                   name: admin-key

上面两种形式的结果都一样;但是,如果你想给模块传递复杂的参数,需要使用复杂格式。一些模块期望传入对象列表或数据的哈希。复杂格式就可以这样做。两种格式对于很多模块来说都可以接受,复杂格式的参数传入在本书中有着大量的应用。

模块传输和执行

一旦找到模块,ansible必须以某种方式执行这个模块。模块如何传输和执行依赖于几个因素,然而通常的过程是定位本地文件系统中的模块文件,将其读取到内存,然后加入传给模块的参数。最后,ansible核心的模版模块代码加入,完成内存中的文件对象。接下来发生的就真实依赖于连接方法和运行时选项了(例如交托模块代码给远程系统来审阅)。

  • 本地定位模块文件,将其读到内存。
  • 加入传给模块的参数。
  • 加入ansible核心中的模版模块,补充完整内存中的文件对象。
  • 连接方法和运行时选项。

默认的连接方式是smart, 通常分解为ssh连接方法。使用默认的配置,ansible会打开一个到远程主机的ssh连接,创建一个临时目录,并关闭连接。

然后ansible会打开另外一个ssh连接将内存中的任务对象(本地模块文件、任务模块参数、以及ansible模版代码的结果)写到我们刚创建的临时目录里,并关闭连接。

最后,ansible为了执行这个模块,打开第三个连接,并删除临时目录和它的内容。模块结果以json格式从stdou捕获,ansible会恰当的解析并处理它。如果任务有async控制,ansible会在模块完成之前关闭第三个连接,然后ssh在规定的时间内回到主机检查任务的状态,直到模块完成或达到规定的超时时间。

任务执行

从上面描述来看,每个任务、每个主机至少需要三个SSH连接。在一个具有少量任务的小舰队来说,这可能不是一个问题;然而,随着任务集的增长和规模的增大,创建和销毁ssh连接所需要的时间必然大幅增加。谢天谢地,有两种方法可以缓解这种情况。

第一个是ssh的特性,ControlPersist, 它提供一种机制在第一次连接到远程主机的时候创建持久socket连接,这样在后续连接中可复用,以绕过创建连接所需的一些握手。这样可以大大减少ansible花费在开启新连接上的时间。ansible自动利用这个特性,如果运行ansbile的主机平台支持它的话。要检查平台是否支持此功能,请检查main ssh看是否支持ControlPersist。

第二个可利用的性能提升是ansible的一个特性,叫做pipeling。Pipelining对于基于ssh的连接方法可用,可以在ansible配置文件中使用ssh_connection选项进行配置:

[ssh_connection]
pipelining=True

这个设置会改变模块如何被传输。不是使用一个打开的ssh连接来创建目录,另外一个ssh连接将组成模块写出,第三个ssh进行执行和清理,而是ansible打开远程主机的一个ssh连接,并启动python解析器。然后,通过这个活动的连接,ansible将组装的模块代码管道进去来执行。这样将连接从3个减少到1个, 它确实可以如此。默认情况下,pipelining是禁用的。
利用上面两种性能调优的组合,即便规模不断增大,你的剧本也可以很友好,并且速度很快。然而,请记住,ansible一次性拥有的主机地址最多只能是配置文件中设置的fork数。fork是ansible分隔出来与远程计算机通信的worker数量。默认是5个fork, 一次性最多有5个主机。如果规模增大了, 可以在配置文件中调整forks参数的值,或者执行ansible或ansible-playbook时使用-forks参数。

变量类型及位置

变量是ansible设计中的关键组件。变量允许动态play内容、可以在不同inventory之间复用play。在非常基本的ansible使用之上的就是利用变量。理解不同变量类型以及他们可能位于的位置,以及了解如何访问外部数据或提示用户产生数据,是掌握ansible的关键。

变量类型

在深入到变量的优先级之前,我们首先必须理解ansible可用的各种不同的变量类型以及变量子类型,它们的位置,以及在什么地方使用是有效的。

第一个重要的变量类型是inventory变量。这些变量是ansible通过inventory的方式获取的。这些变量可以定义为特定于单个主机的host_vars或适用于整个组的group_vars.这些变量可以直接写入到inventory文件中,通过动态inventory插件传递,或者从host_vars/<host>或group_vars/<group>目录加载。

这些变量可以用于定义ansible处理主机时的行为,或者与这些主机上运行的应用相关的特定网站的数据。变量是否来自于host_vars或group_vars, 他将被赋予给主机的hostvars, 并且可以从剧本和模版文件访问到。访问主机自有变量可以直接通过引用名字的方式达到,例如{{ foobar }}, 访问另外主机的变量可以通过访问hostvars来完成。例如,要访问来自examplehost的foobar: {{ hostvars'examplehost' }}。折线变量都是全局作用域的。

第二种主要变量类型是角色变量。这些变量是特定角色的,可以被角色任务利用,并且作用域仅仅在定义它们的角色之内。这些变量通常作为role default提供的,意思是为变量提供了默认值,但是当应用角色的时候可以很容易覆盖。当角色被引用的时候,可能同时提供变量数据,要么覆盖角色默认值,要么完全新创建的数据。我们会在后面章节详细介绍role。这些变量可以应用给角色中的所有主机,它们可以直接访问,就像主机主机的hostvars一样。

第三种主要的变量类型是play变量。这些变量是在play的控制key中定义的,要么直接通过vars key,要么通过vars_files key从外部文件获取源。另外,play可以和用户交互来获取变量,通过vars_prompt。这些变量可以在play作用域内、play的任意任务或包含的任务内使用。这些变量应用到play里边的所有主机, 可以像hostvars一样的引用它们。

第四种类型是任务变量。任务变量在执行任务或play的事实收集阶段发现的变量。这些变量是特定主机的,并被添加到主机的hostvars中,因此可以如此使用,也就意味着在它们发现或定义了指向什么之后,就具有全局作用域。这种类型的变量可以通过gather_facts和fact模块,通过register任务key从任务返回数据产生, 或者直接由任务通过使用set_fact或者add_host模块定义。数据也可以使用pause模块的prompt参数从操作员的交互中获取和注册结果:

- name: get the operators name
     pause:
       prompt: "Please enter your name"
     register: opname

fact模块不改变状态但返回数据的模块。

最后一种变量类型是外部变量,或者extra-vars类型。这些变量是在执行ansible-playbook命令时通过命令行--extra-vars提供的。变量数据可以以key=value对的形式、引用的JSON数据,或者yaml格式,里边放着变量数据的文件的引用三种方式来提供的。

--extra-vars "foo=bar owner=fred"
--extra-vars '{"services": ["nova-api", "nova-conductor"]}'
--extra-vars @/path/to/data.yaml

外部变量可视为全局变量。可以应用到每个主机,并且在整个剧本中都有效。

访问外部数据

角色变量、play变量、以及任务变量都可以来自外部源。ansible提供了一种从控制机器(就是运行ansible-playbook的机器)访问和计算数据的机制。这个机制叫做lookup插件,ansible自带了几种。这些插件可以用于通过读取文来查找或访问数据。产生并本地存储密码在ansible主机,以便将来使用,计算环境变量,从可执行中管道数据进来,访问redis或etcd系统的数据,模版文件的呈现数据,查询dnstxt记录等等。语法如下:

lookup('<plugin_name>', 'plugin_argment')

例如,在debug任务中使用来自etcd的mastery值:

- name: show data from etcd
  debug: msg="{{ lookup('etcd', 'mastery') }}"

当应用lookup的任务执行的时候,就会计算它们,这样就可以使用动态数据发现。要在多个任务之间复用特定lookup, 每次都重新计算,剧本变量可以使用lookup值定义。每次剧本比阿亮被引用,lookup就会执行,每次也就提供不同的值了。

变量优先级

前面一节,我们知道一些可以在各种地方定义的主要变量类型。这就导致一个非常重要的问题,假设同名变量在多个位置出现的时候会发生什么事情呢? ansible加载数据的时候有一个优先级,因此他有一套顺序以及定义那个变量会获胜的。变量值的覆盖在ansible中是一个高级用法,因此在尝试这样场景之前需要对这样的场景完全理解就非常重要了。

优先级顺序

ansible优先级顺序定义如下:

  • 外部变量(命令行过来的)优先级最高。
  • inventory中定义的连接变量。
  • 大多数其他变量。
  • inventory中定义的其他变量。
  • 发现的关于系统的事实。
  • 角色默认值。

这个列表是个很好的开端,然而事情稍微有点微妙,我们接下来探讨一下。

外部变量

extra-vars, 由命令行提供的,当然会重写其他的所有同名变量了。不管这些变量在哪里定义的,即便它是明确的在play中使用set_fact定义的,命令行中提供的变量的值会优先使用。

连接变量

接下来就是连接变量,就是前面我们列出来的行为变量。这些变量会影响ansible如何连接并在远程机器上执行任务。这些变量例如有ansible_ssh_user, ansible_ssh_host,以及其他一些前面我们所描述的一些行为inventory参数。ansible文档陈述这些都是来自inventory, 然而,它们可以被任务重写,例如set_fact。set_fact模块关于ansible_ssh_user的变量会覆盖来自inventory源的值。inventory中的变量也有一些优先级顺序。特定主机的定义会覆盖组定义中的变量,子组定义中的会覆盖父组中定义的变量。这样就可以给组设置大多数变量,而特定主机可以有自己不同的值。当某个主机属于多个组时,如果多个组都定义了相同的变量,这个时候行为就不太好定义,因此强烈不推荐这样做。

大多数其他变量

大多数其他变量块是一个很大的源组。包括:

  • 命令行开关。
  • play变量。
  • Task变量。
  • 角色变量(非默认值)。

这些变量集合也可以互相覆盖,具有最后提供的优先的规则。角色变量在这个集合中是在roles的vars/main.yaml中提供的,变量在赋予给角色或角色依赖时定义。在这个例子中,我们提供一个变量名字为role_var, 此时我们赋值给角色:

- role: examlpel_role
  role_var: var_value_here

这里重要的微妙之处是在角色赋值时间提供的定义会覆盖在角色的vars/main.yaml中定义的。也要记住最后提供的规则;如果在角色example_role中,role_var变量通过任务重新定义了,那个点定义的会获胜。

其他ivnentory变量

紧接着稍低优先级的变量是在inventory中保存的变量。这些变量可以在inventory数据中定义,但是不会改变ansible的行为。连接变量的规则在这里也适用。

关于系统的发现事实

发现事实变量就是在我们搜集事实过程中获取的变量。完整的变量列表依赖与主机的平台以及我们可以执行的用于现实系统信息的外部软的,它们可能安装在上述的主机上。除了角色默认之外,这是优先级最低的变量,最有可能被覆盖。

角色默认值

角色具有定义在它们里边的默认值变量。这些是角色中使用的合理缺省值,合适角色应用的定制目标。这就使得角色更具重用性,灵活性、以及使得角色应用的环境和条件更具调节性。

合并散列

前面我们聚焦在变量互相覆盖的优先级顺序上。ansible默认行为是任何变量名覆盖定义都会完全掩盖之前那个变量的定义。然而,这个行为可以通过一种类型变量来修改,这个就是hash。hash变量是key/value的数据集合.每个key的value可以是不同类型,甚至还可以是hash,来构造更加复杂的结构。

有一些复杂的场景中,更希望替换hash的其中一位或添加到一个原有hash中,而不是完整的替换hash。要解锁这个能力,需要在配置中做修改。配置入口是hash_behavior, 它可以是replace或merge。使用merge的时候就会告诉ansible去合并或混合两个出现的hash, 使用重写场景,而不是默认的替换。默认替换会使用新数据完全替换旧数据。

总结

ansible的设计目标是简单易用,它自身的架构非常强大。本章中,我们涵盖了ansible关键的设计和架构概念, 例如版本和配置、剧本解析、模块传输和执行、变量类型和位置、变量优先级。

术语及词汇

  • playbook: 这里特指ansible任务剧本,形象比喻了ansible的任务编排。

导航


老将廉颇
878 声望297 粉丝