现代 IT 人一定要知道的 Ansible系列教程:Roles详解 #
Ansible Playbook 提供了一个可重复、可重用、简单的配置管理和多机部署系统,非常适合部署复杂的应用程序。如果您需要多次使用 Ansible 执行任务,请编写一个 playbook 并将其置于源代码控制之下。然后,您可以使用 playbook 推出新配置或确认远程系统的配置。
Playbook 语法 #
Playbook 以 YAML 格式表示,语法最少。如果您不熟悉 YAML,请查看我们的 YAML 语法概述,并考虑为您的文本编辑器安装一个附加组件(请参阅其他工具和程序),以帮助您在 playbook 中编写干净的 YAML 语法。
Playbook 由有序列表中的一个或多个“任务”组成。运行一个或多个任务。每个任务调用一个 Ansible 模块。
执行 #
playbook 按从上到下的顺序运行。在每个playbook中,任务也按从上到下的顺序运行。可以编排多计算机部署,在 Web 服务器上运行一个任务,然后在数据库服务器上运行另一部任务,然后在网络基础结构上运行第三任务,依此类推。至少,每个任务都定义了两件事:
- 要定位的受管节点,使用模式
- 至少要执行一个任务
在此示例中,第一个以 Web 服务器为目标;第二个以数据库服务器为目标
---
- name: 更新web服务器
hosts: webservers
remote_user: root
tasks:
- name: 安装apache最新版本
ansible.builtin.yum:
name: httpd
state: latest
- name: 编写apache配置文件
ansible.builtin.template:
src: /srv/httpd.j2
dest: /etc/httpd.conf
- name: 更新数据库服务器
hosts: databases
remote_user: root
tasks:
- name: postgresql是最新版本
ansible.builtin.yum:
name: postgresql
state: latest
- name: 启动postgresql
ansible.builtin.service:
name: postgresql
state: started
playbook 可以包含的不仅仅是主机行和任务。例如,上面的配置中为每个任务设置了一个 remote_user
。
这是 SSH 连接的用户帐户。您可以在 playbook、play 或任务级别添加其他 playbook 关键字,以影响 Ansible 的行为方式。
Playbook 关键字可以控制连接插件、是否使用权限提升、如何处理错误等。
为了支持各种环境,Ansible 允许您在 Ansible 配置或清单中将其中许多参数设置为命令行标志。了解这些数据源的优先规则将有助于您扩展 Ansible 生态系统。
任务执行 #
默认情况下,Ansible 会针对主机模式匹配的所有计算机按顺序执行每个任务,一次执行一个任务。
每个任务都执行一个具有特定参数的模块。当一个任务在所有目标计算机上执行完毕后,Ansible 会继续执行下一个任务。
您可以使用策略来更改此默认行为。在每次任务中,Ansible 都会将相同的任务指令应用于所有主机。
如果主机上的任务失败,Ansible 会将该主机从轮换中移除,以执行 playbook 的其余部分。
当运行 playbook 时,Ansible 会返回有关连接、所有 play 和任务 name
的行、每个任务在每台计算机上是成功还是失败以及每个任务是否在每台计算机上进行了更改的信息。
在 playbook 执行的底部,Ansible 提供了目标节点及其执行方式的摘要。一般故障和致命的“无法访问”通信尝试在计数中保持分开。
期望状态和幂等性 #
大多数 Ansible 模块会检查是否已经达到所需的最终状态,如果已达到该状态,则在不执行任何操作的情况下退出,因此重复任务不会改变最终状态。以这种方式运行的模块通常称为幂等性
。
无论运行一次还是多次运行剧本,结果都应该是相同的。但是,并非所有 playbook
和模块都以这种方式运行。如果不确定,请在沙盒环境中测试 playbook,然后再在生产环境中多次运行它们。
运行 playbook #
要运行 playbook,请使用 ansible-playbook 命令。
ansible-playbook playbook.yml -f 10
在运行 playbook 时使用该 --verbose
标志可查看成功模块和不成功模块的详细输出。
检查模式下运行 playbook #
Ansible 的检查模式允许您在不对系统进行任何更改的情况下执行 playbook。您可以使用检查模式来测试 playbook,然后再在生产环境中实施它们。
若要在检查模式下运行 playbook,可以将 -C
or --check
标志传递给 ansible-playbook
命令:
ansible-playbook --check playbook.yaml
执行此命令将正常运行 playbook,但 Ansible 不会实施任何修改,只会提供有关其所做的更改的报告。此报告包含文件修改、命令执行和模块调用等详细信息。
检查模式提供了一种安全实用的方法来检查 playbook 的功能,而不会冒着意外更改系统的风险。此外,它还是对未按预期运行的 playbook 进行故障排除的宝贵工具。
Ansible-Pull #
如果你想反转 Ansible 的架构,以便节点签入到一个中心位置,而不是将配置推送给它们,我们可以这样做。
这是一个 ansible-pull
小脚本,它将从 git 中检出配置指令的存储库,然后针对该内容运行 ansible-playbook
。
假设您对结账地点进行负载均衡, ansible-pull
基本上可以无限扩展。
运行 ansible-pull --help
查看详细的说明
验证 playbook #
您可能需要验证 playbook,以便在运行它们之前捕获语法错误和其他问题。ansible-playbook 命令提供了多个验证选项,包括 --check
--diff
--list-hosts
--list-tasks
和 --syntax-check
。用于验证 playbook 的工具介绍了用于验证和测试 playbook 的其他工具。
在执行 playbook 之前,您可以使用 ansible-lint 获取特定于 Ansible 的详细信息。
例如,如果在此页面顶部附近调用 verify-apache.yml
的 playbook 上运行 ansible-lint
,则应获得以下结果:
$ ansible-lint verify-apache.yml
[403] Package installs should not use latest
verify-apache.yml:8
Task/Handler: ensure apache is at the latest version
ansible-lint 默认规则页面描述了每个错误。对于 [403]
,建议的解决方法是 state: present
在 playbook 中更改为 state: latest
。
Roles #
在 ansible
中角色允许我们根据已知的文件结构自动加载相关的变量、文件、任务、处理程序和其他 Ansible 工件。将内容分组到角色后,我们可以轻松地重复使用它们并与其他用户共享它们。
角色目录结构 #
Ansible 角色具有定义的目录结构,其中包含 8 个主要标准目录。我们必须在每个角色中至少包含其中一个目录。我们可以省略该角色不使用的任何目录。例如:
# playbooks
site.yml
webservers.yml
fooservers.yml
roles/
common/ # 该层次结构表示一个“角色”
tasks/ #
main.yml # 如果有必要,任务文件可以包括较小的文件
handlers/ #
main.yml
templates/ # 与模板资源一起使用的文件
ntp.conf.j2 # 模板以.j2结尾
files/ #
bar.txt # 用于复制资源的文件
foo.sh # 与脚本资源一起使用的脚本文件
vars/ #
main.yml # 与此角色关联的变量
defaults/ #
main.yml # 此角色的默认低优先级变量
meta/ #
main.yml # 角色依赖关系
library/ # 角色还可以包括自定义模块
module_utils/ # 角色还可以包括自定义的module_utils
lookup_plugins/ # 或者其他类型的插件,比如本例中的查找
webtier/ # 与上面的“common”相同的结构,用于webtier角色
monitoring/
fooapp/
默认情况下,Ansible 将在角色的每个目录中查找 main.yml
相关内容(以及 main.yaml
和 main
):
tasks/main.yml
- 角色执行的主要任务列表。handlers/main.yml
- 处理程序,可以在此角色内部或外部使用。library/my_module.py
- 模块,可以在此角色中使用(有关更多信息,请参阅在角色中嵌入模块和插件)。defaults/main.yml
- 角色的默认变量。这些变量在所有可用变量中具有最低的优先级,并且可以很容易地被任何其他变量(包括库存变量)覆盖。vars/main.yml
- 角色的其他变量。files/main.yml
- 角色部署的文件。emplates/main.yml
- 角色部署的模板。meta/main.yml
- 角色的元数据,包括角色依赖关系和可选的 Galaxy 元数据,例如支持的平台。
可以在某些目录中添加其他 YAML
文件。例如,我们可以将特定于平台的任务放在单独的文件中,并在 tasks/main.yml
文件中引用它们:
# roles/example/tasks/main.yml
- name: Install the correct web server for RHEL
import_tasks: redhat.yml
when: ansible_facts['os_family']|lower == 'redhat'
- name: Install the correct web server for Debian
import_tasks: debian.yml
when: ansible_facts['os_family']|lower == 'debian'
# roles/example/tasks/redhat.yml
- name: Install web server
ansible.builtin.yum:
name: "httpd"
state: present
# roles/example/tasks/debian.yml
- name: Install web server
ansible.builtin.apt:
name: "apache2"
state: present
角色还可以在名为 library
的目录中包含模块和其他插件类型。
存储和查找角色 #
默认情况下,Ansible 在以下位置查找角色:
- 如果我们使用了集合,可以在集合中查找
- 在名为
roles/
的目录中,相对于 playbook 文件 - 在配置的roles_path中。默认搜索路径为
~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
。 - 在
playbook
文件所在的目录中
如果我们将角色存储在其他位置,请设置 roles_path 配置选项,以便 Ansible 可以找到我们的角色。
将共享角色签入单个位置可使其更易于在多个 playbook 中使用。有关管理中的设置的详细信息,请参阅配置 ansible.cfg
Ansible。
或者,我们可以使用完全限定的路径调用角色:
---
- hosts: webservers
roles:
- role: '/path/to/my/roles/common'
使用角色 #
我们可以通过三种方式使用角色:
- 在
playbook
的roles
选项中,推荐使用这种方式。 - 在
task
使用include_role
, 我们可以使用 在playbook
的tasks
部分的任何位置动态重用角色include_role
- 在
task
使用import_role
, 我们可以使用 在playbook tasks
部分的任何位置静态重用角色import_role
。
在 playbook 使用角色 #
使用金典方式是针对给定 roles
进行选择:
---
- hosts: webservers
roles:
- common
- webservers
当我们在playbook
级别使用该 roles
选项时,对于每个角色 x
:
- 如果
roles/x/tasks/main.yml
存在,则 Ansible 会将该文件中的任务添加到playbook中。 - 如果
roles/x/handlers/main.yml
存在,则 Ansible 会将该文件中的处理程序添加到playbook中。 - 如果
roles/x/vars/main.yml
存在,Ansible 会将该文件中的变量添加到playbook中。 - 如果
roles/x/defaults/main.yml
存在,则 Ansible 会将该文件中的变量添加到playbook中。 - 如果
roles/x/meta/main.yml
存在,Ansible 会将该文件中的任何角色依赖项添加到角色列表中。 - 任何副本、脚本、模板或包含任务(在角色中)都可以引用
roles/x/{files,templates,tasks}/
中的文件(dir 取决于任务),而不必相对或绝对地路径它们。
我们可以将其他关键字传递给该 roles
选项:
---
- hosts: webservers
roles:
- common
- role: foo_app_instance
vars:
dir: '/opt/a'
app_port: 5000
tags: typeA
- role: foo_app_instance
vars:
dir: '/opt/b'
app_port: 5001
tags: typeB
当我们向该 role
选项添加标签时,Ansible 会将该标签应用于角色中的所有任务。
在 playbook roles:
的部分中使用 vars:
时,变量将添加到剧本变量中,使它们可用于角色之前和之后剧本中的所有任务。此行为可以通过DEFAULT_PRIVATE_ROLE_VARS
更改。
动态重用角色 #
可以使用 include_role
在 tasks
段中的任何位置动态重用角色。
虽然在 roles
中添加的角色在playbook
的任何其他任务之前运行,但包含的角色则按定义的顺序运行。如果任务 include_role
之前有其他任务,则其他任务将首先运行。
要包含角色,请执行以下操作:
---
- hosts: webservers
tasks:
- name: Print a message
ansible.builtin.debug:
msg: "this task runs before the example role"
- name: Include the example role
include_role:
name: example
- name: Print a message
ansible.builtin.debug:
msg: "this task runs after the example role"
在包含角色时,我们可以传递其他关键字,包括变量和标签:
---
- hosts: webservers
tasks:
- name: Include the foo_app_instance role
include_role:
name: foo_app_instance
vars:
dir: '/opt/a'
app_port: 5000
tags: typeA
...
当我们向 include_role
任务添加标签时,Ansible 仅将标签应用于包含本身。
这意味着,如果角色中的选定任务本身具有与 include
语句相同的标记,则只能传递 --tags
以运行这些任务。
我们可以有条件地包含角色:
---
- hosts: webservers
tasks:
- name: Include the some_role role
include_role:
name: some_role
when: "ansible_facts['os_family'] == 'RedHat'"
导入角色 #
我们可以使用 import_role
在重用部分的 tasks
任意位置静态重用角色。该行为与使用 roles
关键字相同。例如:
---
- hosts: webservers
tasks:
- name: Print a message
ansible.builtin.debug:
msg: "before we run our role"
- name: Import the example role
import_role:
name: example
- name: Print a message
ansible.builtin.debug:
msg: "after we ran our role"
导入角色时,我们可以传递其他关键字,包括变量和标签:
---
- hosts: webservers
tasks:
- name: Import the foo_app_instance role
import_role:
name: foo_app_instance
vars:
dir: '/opt/a'
app_port: 5000
...
当我们向 import_role
语句添加标签时,Ansible 会将该标签应用于角色中的所有任务。有关详细信息,请参阅标签继承:为多个任务添加标签。
角色参数验证 #
从版本 2.11 开始,我们可以选择基于参数规范启用角色参数验证。此规范在 meta/argument_specs.yml
文件(或 .yaml
文件扩展名)中定义。
定义此参数规范后,将在角色执行开始时插入一个新任务,该任务将根据规范验证为角色提供的参数。如果参数验证失败,则角色将无法执行。
角色参数规范必须在角色 meta/argument_specs.yml
文件的顶级 argument_specs
块中定义。所有字段均为小写。
示例:
# roles/myapp/meta/argument_specs.yml
---
argument_specs:
# roles/myapp/tasks/main.yml entry point
main:
short_description: Main entry point for the myapp role
description:
- This is the main entrypoint for the C(myapp) role.
- Here we can describe what this entrypoint does in lengthy words.
- Every new list item is a new paragraph. You can have multiple sentences
per paragraph.
author: Daniel Ziegenberg
options:
myapp_int:
type: "int"
required: false
default: 42
description:
- "The integer value, defaulting to 42."
- "This is a second paragraph."
myapp_str:
type: "str"
required: true
description: "The string value"
myapp_list:
type: "list"
elements: "str"
required: true
description: "A list of string values."
version_added: 1.3.0
myapp_list_with_dicts:
type: "list"
elements: "dict"
required: false
default:
- myapp_food_kind: "meat"
myapp_food_boiling_required: true
myapp_food_preparation_time: 60
- myapp_food_kind: "fruits"
myapp_food_preparation_time: 5
description: "A list of dicts with a defined structure and with default a value."
options:
myapp_food_kind:
type: "str"
choices:
- "vegetables"
- "fruits"
- "grains"
- "meat"
required: false
myapp_food_boiling_required:
type: "bool"
required: false
default: false
description: "Whether the kind of food requires boiling before consumption."
myapp_food_preparation_time:
type: int
required: true
description: "Time to prepare a dish in minutes."
myapp_dict_with_suboptions:
type: "dict"
required: false
default:
myapp_host: "bar.foo"
myapp_exclude_host: true
myapp_path: "/etc/myapp"
description: "A dict with a defined structure and default values."
options:
myapp_host:
type: "str"
choices:
- "foo.bar"
- "bar.foo"
- "ansible.foo.bar"
required: true
description: "A string value with a limited list of allowed choices."
myapp_exclude_host:
type: "bool"
required: true
description: "A boolean value."
myapp_path:
type: "path"
required: true
description: "A path value."
original_name:
type: list
elements: "str"
required: false
description: "An optional list of string values."
# roles/myapp/tasks/alternate.yml entry point
alternate:
short_description: Alternate entry point for the myapp role
version_added: 1.2.0
options:
myapp_int:
type: "int"
required: false
default: 1024
description: "The integer value, defaulting to 1024."
传递不同的参数 #
Ansible 在一次play中只执行每个角色一次,即使我们多次定义它,除非每个定义在角色上定义的参数不同。
如果在每个角色定义中传递不同的参数,则 Ansible 会多次运行该角色。提供不同的变量值与传递不同的角色参数不同。
我们必须为此行为使用关键字 roles
,因为 import_role
并且 include_role
不接受角色参数。
---
- hosts: webservers
roles:
- { role: foo, message: "first" }
- { role: foo, message: "second" }
在这些示例中,Ansible 运行 foo
两次,因为每个角色定义都有不同的参数。
使用 allow_duplicates: true
llow_duplicates: true
添加到角色 meta/main.yml
的文件中:
# playbook.yml
---
- hosts: webservers
roles:
- foo
- foo
# roles/foo/meta/main.yml
---
allow_duplicates: true
在此示例中,Ansible 运行 foo
了两次,因为我们已明确启用它来执行此操作。
使用角色依赖关系 #
角色依赖关系允许我们在使用角色时自动拉入其他角色。
角色依赖关系是先决条件,而不是真正的依赖关系。这些角色没有父/子关系。Ansible 加载所有列出的角色,运行首先列出的 dependencies
角色,然后运行列出这些角色的角色。
角色依赖项存储在角色目录内的 meta/main.yml
文件中。此文件应包含要在指定角色之前插入的角色和参数的列表。例如:
# roles/myapp/meta/main.yml
---
dependencies:
- role: common
vars:
some_parameter: 3
- role: apache
vars:
apache_port: 80
- role: postgres
vars:
dbname: blarg
other_parameter: 12
Ansible 始终在列出这些角色的角色之前执行 中 dependencies
列出的角色。当我们使用关键字时, roles
Ansible 会递归地执行此模式。例如,如果在 下列出 role foo
roles:
,则 role 在其 meta/main.yml 文件中列出 role,而 role 在其 meta/main.yml dependencies
中列出 role foo
bar
dependencies
bar
baz
,则 bar
,然后 foo
baz
。
Ansible 将重复的角色依赖关系视为重复角色,例如: Ansible 只执行一次角色依赖关系,即使定义了多次, roles:
除非每个定义在角色上定义的参数、标签或 when 子句不同。如果一个角色中的两个角色都将第三个角色列为依赖项,则 Ansible 只会运行该角色依赖项一次,除非我们传递不同的参数、标签、when 子句或在要多次运行的角色 allow_duplicates: true
中使用。
例如,命名 car
的角色依赖于命名 wheel
如下的角色:
---
dependencies:
- role: wheel
n: 1
- role: wheel
n: 2
- role: wheel
n: 3
- role: wheel
n: 4
角色 wheel
取决于两个角色: tire
和 brake
.然后, meta/main.yml
for 将包含以下内容:
---
dependencies:
- role: tire
- role: brake
meta/main.yml
for tire
和 brake
将包含以下内容:
---
allow_duplicates: true
生成的执行顺序如下:
tire(n=1)
brake(n=1)
wheel(n=1)
tire(n=2)
brake(n=2)
wheel(n=2)
...
car
若要与角色依赖项一起使用 allow_duplicates: true
,必须为 下 dependencies
列出的角色指定它,而不是为列出它的角色指定它。
在上面的示例中, allow_duplicates: true
出现在 tire
和 brake
角色中 meta/main.yml
。该 wheel
角色不需要 allow_duplicates: true
,因为 定义的 car
每个实例都使用不同的参数值。
在角色中嵌入模块和插件 #
如果我们编写自定义模块或插件,我们可能希望将其作为角色的一部分进行分发。
例如,如果我们编写了一个模块来帮助配置公司的内部软件,并且希望组织中的其他人使用此模块,但又不想告诉每个人如何配置其 Ansible 库路径,则可以将该模块包含在我们的internal_config
角色中。
要向角色添加模块或插件:除了角色的“tasks”和“handlers”结构外,添加一个名为“library”的目录,然后将该模块直接包含在“library”目录中。
假设你有这个:
roles/
my_custom_modules/
library/
module1
module2
该模块将可用于角色本身,以及在此角色之后调用的任何角色,如下所示:
---
- hosts: webservers
roles:
- my_custom_modules
- some_other_role_using_my_custom_modules
- yet_another_role_using_my_custom_modules
如有必要,我们还可以在角色中嵌入模块,以修改 Ansible 核心发行版中的模块。例如,通过复制模块并将副本嵌入角色,可以在生产版本中发布特定模块之前使用该模块的开发版本。请谨慎使用此方法,因为核心组件中的 API 签名可能会更改,并且不能保证此解决方法有效。
可以使用相同的机制在角色中嵌入和分发插件,使用相同的架构。例如,对于过滤器插件:
roles/
my_custom_filter/
filter_plugins
filter1
filter2
然后,这些过滤器可以在 Jinja
模板中以 my_custom_filter
之后调用的任何角色使用。