像软件代码一样,测试基础设施代码是一项非常重要的任务。理想情况下,生产中应该没有未经测试的代码,尤其是当您有严格的客户服务级别协议需要满足时,即使对于基础架构来说也是如此。在本章中,我们将了解语法检查、不在机器上应用代码的测试(无操作模式)以及剧本的功能测试,这些都是 Ansible 的核心,并触发您想要在远程主机上执行的各种任务。建议您将其中一些集成到您的持续集成 ( CI )系统中,以便 Ansible 更好地测试您的行动手册。我们将关注以下几点:
- 语法检查
- 有无
--diff
检查模式 - 功能测试
作为功能测试的一部分,我们将关注以下内容:
- 关于系统结束状态的断言
- 用标签测试
- 使用
--syntax-check
选项 - 使用
ANSIBLE_KEEP_REMOTE_FILES
和ANSIBLE_DEBUG
标志
然后,我们将研究如何管理异常以及如何主动触发错误。
对于这一章,除了通常的要求之外,没有具体的要求,例如 Ansible、游民和 shell。
你可以在 https://github.com/PacktPublishing/Learning-Ansible-2.从这本书的 GitHub 资源库下载所有的文件第三版/树/主/章节 08 。
每当运行剧本时,Ansible 都会首先检查剧本文件的语法。如果遇到错误,Ansible 会报错,说有语法错误,除非您修复该错误,否则不会继续。该语法检查仅在运行ansible-playbook
命令时执行。当写一个大剧本时,或者如果你已经包含了任务文件,可能很难修复所有的错误;这可能会浪费更多的时间。为了处理这种情况,Ansible 提供了一种方法来检查您的 YAML 语法,因为您一直在继续您的剧本。对于本例,我们需要创建具有以下内容的playbooks/setup_apache.yaml
文件:
---
- hosts: all
tasks:
- name: Install Apache
yum:
name: httpd
state: present
become: True
- name: Enable Apache
service:
name: httpd
state: started
enabled: True
become: True
现在我们有了示例文件,需要用--syntax-check
参数运行它;因此,您需要调用 Ansible,如下所示:
ansible-playbook playbooks/setup_apache.yaml --syntax-check
ansible-playbook
命令检查了setup_apache.yml
剧本的 YAML 语法,显示剧本的语法是正确的。让我们看看剧本中无效语法导致的错误:
ERROR! Syntax Error while loading YAML.
did not find expected '-' indicator
The error appears to have been in '/home/fale/Learning-Ansible-2.X-Third-Edition/Ch8/playbooks/setup_apache.yaml': line 10, column 5, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: Enable Apache
service:
^ here
错误显示Enable Apache
任务有缩进错误。Ansible 还为您提供行号、列号和发现错误的文件名(即使这不能保证错误的确切位置)。这绝对应该是基本测试之一,应该作为 Ansible 配置项的一部分运行。
检查模式(也称为试运行或无操作模式)将在无操作模式下运行您的行动手册,也就是说,它不会对远程主机进行任何更改;相反,它将只显示任务运行时引入的更改。检查模式是否实际启用取决于每个模块。很少有你可能会感兴趣的命令。所有这些命令都必须在/usr/lib/python2.7/site-packages/ansible/modules
中运行,或者在您的 Ansible 模块文件夹中运行(根据您使用的操作系统以及您安装 Ansible 的方式,可能会有不同的路径)。
要计算安装中可用模块的数量,可以执行以下命令:
find . -type f | grep '.py$' | grep -v '__init__' | wc -l
对于 Ansible 2.7.2,这个命令的结果是2095
,因为 Ansible 有那么多模块。
如果您想了解其中有多少支持检查模式,可以运行以下代码:
grep -r 'supports_check_mode=True' | awk -F: '{print $1}' | sort | uniq | wc -l
使用 Ansible 2.7.2,该命令的结果为1239
。
您可能还会发现以下命令对于列出支持检查模式的所有模块非常有用:
grep -r 'supports_check_mode=True' | awk -F: '{print $1}' | sort | uniq
这有助于您测试行动手册的表现,并在生产服务器上运行之前检查是否有任何故障。只需将--check
选项传递给ansible-playbook
命令,即可在检查模式下运行剧本。让我们看看检查模式如何与setup_apache.yml
剧本一起工作,运行以下代码:
ansible-playbook --check -i ws01, playbooks/setup_apache.yaml
结果如下:
PLAY [all] ***********************************************************
TASK [Gathering Facts] ***********************************************
ok: [ws01]
TASK [Install Apache] ************************************************
changed: [ws01]
TASK [Enable Apache] *************************************************
changed: [ws01]
PLAY RECAP ***********************************************************
ws01 : ok=3 changed=2 unreachable=0 failed=0
在前一次运行中,Ansible 没有在目标主机上进行更改,而是突出显示了在实际运行期间可能发生的所有更改。从前面的运行中,您可以发现httpd
服务已经安装在目标主机上。因此,Ansible 针对该任务的退出消息是正常的:
TASK [Install Apache] ************************************************
changed: [ws01]
但是,对于第二个任务,它发现目标主机上没有运行httpd
服务:
TASK [Enable Apache] *************************************************
changed: [ws01]
当您在未启用检查模式的情况下再次运行前面的行动手册时,Ansible 将确保服务状态正在运行。
在检查模式下,您可以使用--diff
选项显示将应用于文件的更改。为了能够看到使用中的--diff
选项,我们需要创建一个playbooks/setup_and_config_apache.yaml
剧本来匹配以下内容:
- hosts: all
tasks:
- name: Install Apache
yum:
name: httpd
state: present
become: True
- name: Enable Apache
service:
name: httpd
state: started
enabled: True
become: True
- name: Ensure Apache userdirs are properly configured
template:
src: ../templates/userdir.conf
dest: /etc/httpd/conf.d/userdir.conf
become: True
如您所见,我们添加了一个任务,该任务将确保/etc/httpd/conf.d/userdir.conf
文件的某个状态。
我们还需要创建一个放置在templates/userdir.conf
中的模板文件,其内容如下(完整文件可在 GitHub 上获得):
#
# UserDir: The name of the directory that is appended onto a user's home
# directory if a ~user request is received.
#
# The path to the end user account 'public_html' directory must be
# accessible to the webserver userid. This usually means that ~userid
# must have permissions of 711, ~userid/public_html must have permissions
# of 755, and documents contained therein must be world-readable.
# Otherwise, the client will only receive a "403 Forbidden" message.
#
<IfModule mod_userdir.c>
#
# UserDir is disabled by default since it can confirm the presence
# of a username on the system (depending on home directory
# permissions).
#
UserDir enabled
...
在这个模板中,我们只修改了UserDir enabled
线,默认为UserDir disabled
。
The --diff
option doesn't work with the file
module; you will have to use the template
module only.
现在,我们可以使用以下命令测试结果:
ansible-playbook -i ws01, playbooks/setup_and_config_apache.yaml --diff --check
如您所见,我们正在使用--check
参数来确保这将是一次试运行。我们将收到以下输出:
PLAY [all] ***********************************************************
TASK [Gathering Facts] ***********************************************
ok: [ws01]
TASK [Install Apache] ************************************************
ok: [ws01]
TASK [Enable Apache] *************************************************
ok: [ws01]
TASK [Ensure Apache userdirs are properly configured] ****************
--- before: /etc/httpd/conf.d/userdir.conf
+++ after: /home/fale/.ansible/tmp/ansible-local-6756FTSbL0/tmpx9WVXs/userdir.conf
@@ -14,7 +14,7 @@
# of a username on the system (depending on home directory
# permissions).
#
- UserDir disabled
+ UserDir enabled
#
# To enable requests to /~user/ to serve the user's public_html
changed: [ws01]
PLAY RECAP ***********************************************************
ws01 : ok=4 changed=1 unreachable=0 failed=0
我们可以看到,Ansible 将远程主机的当前文件与源文件进行比较;以+
开头的一行表示文件中增加了一行,而-
表示删除了一行。
You can also use --diff
without the --check
option, which will allow Ansible to make the specified changes and show the difference between two files.
将--diff
和--check
模式结合使用是一个测试步骤,可以作为配置项测试的一部分,以确定运行过程中有多少步骤发生了变化。另一种可以一起使用这些特性的情况是部署过程的一部分,该部分检查当您在该机器上运行 Ansible 时会发生什么变化。
也有一些情况——这不应该发生,但有时会发生——你已经很长时间没有在机器上运行剧本了,你担心再次运行它会破坏一些东西。将这些选项结合起来使用,应该有助于你理解这是否只是让你担心,或者这是否是一个真正的风险。
维基百科称,功能测试是一个质量保证过程,也是一种基于被测软件组件规格的黑盒测试。通过输入函数并检查输出来测试函数;很少考虑内部程序结构。当涉及到基础设施时,功能测试和代码一样重要。
从基础设施的角度来看,关于功能测试,我们在实际机器上测试 Ansible 运行的输出。Ansible 提供了多种方法来执行剧本的功能测试;让我们来看看一些最常用的方法。
只有当您想要检查任务是否会改变主机上的任何内容时,检查模式才会起作用。当您想要检查模块的输出是否如您所期望的那样时,这将没有帮助。例如,假设您编写了一个模块来检查端口是否打开。为了测试这一点,您可能需要检查您的模块的输出,看看它是否匹配所需的输出。为了执行这样的测试,Ansible 提供了一种直接比较模块输出和期望输出的方法。
让我们通过创建包含以下内容的playbooks/assert_ls.yaml
文件来看看这是如何工作的:
---
- hosts: all
tasks:
- name: List files in /tmp
command: ls /tmp
register: list_files
- name: Check if file testfile.txt exists
assert:
that:
- "'testfile.txt' in list_files.stdout_lines"
在前面的剧本中,我们在目标主机上运行ls
命令,并将该命令的输出注册到list_files
变量中。此外,我们要求 Ansible 检查ls
命令的输出是否有预期的结果。我们使用assert
模块来实现这一点,该模块使用一些条件检查来验证任务的stdout
值是否满足用户的预期输出。让我们运行前面的剧本,看看 Ansible 会返回什么输出,使用以下命令:
ansible-playbook -i ws01, playbooks/assert_ls.yaml
由于我们没有该文件,我们将收到以下输出:
PLAY [all] ***********************************************************
TASK [Gathering Facts] ***********************************************
ok: [ws01]
TASK [List files in /tmp] ********************************************
changed: [ws01]
TASK [Check if file testfile.txt exists] *****************************
fatal: [ws01]: FAILED! => {
"assertion": "'testfile.txt' in list_files.stdout_lines",
"changed": false,
"evaluated_to": false,
"msg": "Assertion failed"
}
to retry, use: --limit @/home/fale/Learning-Ansible-2.X-Third-Edition/Ch8/playbooks/assert_ls.retry
PLAY RECAP ***********************************************************
ws01 : ok=2 changed=1 unreachable=0 failed=1
如果我们在创建预期文件后重新运行行动手册,结果将是:
PLAY [all] ***********************************************************
TASK [Gathering Facts] ***********************************************
ok: [ws01]
TASK [List files in /tmp] ********************************************
changed: [ws01]
TASK [Check if file testfile.txt exists] *****************************
ok: [ws01] => {
"changed": false,
"msg": "All assertions passed"
}
PLAY RECAP ***********************************************************
ws01 : ok=3 changed=1 unreachable=0 failed=0
这一次,任务传递了一条确认消息,因为list_files
变量中存在testfile.txt
。同样,您可以使用and
和or
运算符匹配一个变量或多个变量中的多个字符串。断言特性非常强大,在项目中编写过单元测试或集成测试的用户会非常高兴看到这个特性!
标签是一种无需运行整个剧本就能测试一堆任务的好方法。我们可以使用标签在节点上运行实际测试,以验证用户想要在剧本中的状态。我们可以将此视为在实际盒子上运行 Ansible 集成测试的另一种方式。要测试的标记方法可以在运行 Ansible 的实际机器上运行,并且它可以主要在部署期间用于测试终端系统的状态。在本节中,我们将首先了解如何使用tags
一般来说,它们的功能可能会帮助我们,不仅仅是测试,甚至是为了测试目的。
要在您的行动手册中添加标签,请使用tags
参数,后跟一个或多个用逗号或 YAML 列表分隔的标签名称。让我们在playbooks/tags_example.yaml
中创建一个简单的剧本,看看标签如何与以下内容一起工作:
- hosts: all
tasks:
- name: Ensure the file /tmp/ok exists
file:
name: /tmp/ok
state: touch
tags:
- file_present
- name: Ensure the file /tmp/ok does not exists
file:
name: /tmp/ok
state: absent
tags:
- file_absent
如果我们现在运行剧本,文件将被创建和销毁。我们可以看到它运行如下:
ansible-playbook -i ws01, playbooks/tags_example.yaml
它会给我们这个输出:
PLAY [all] ***********************************************************
TASK [Gathering Facts] ***********************************************
ok: [ws01]
TASK [Ensure the file /tmp/ok exists] ********************************
changed: [ws01]
TASK [Ensure the file /tmp/ok does not exists] ***********************
changed: [ws01]
PLAY RECAP ***********************************************************
ws01 : ok=3 changed=2 unreachable=0 failed=0
由于这不是等幂剧本,如果我们反复运行它,我们将总是看到相同的结果,因为剧本每次都会创建和删除文件。
但是我们增加了两个标签:file_present
和file_absent
。现在,您可以简单地传递file_present
标记或file_absent
标记,只执行其中一个动作,如下例所示:
ansible-playbook -i ws01, playbooks/tags_example.yaml -t file_present
感谢-t file_present
部分,只有带有file_present
标签的任务才会被执行;事实上,这将是输出:
PLAY [all] ***********************************************************
TASK [Gathering Facts] ***********************************************
ok: [ws01]
TASK [Ensure the file /tmp/ok exists] ********************************
changed: [ws01]
PLAY RECAP ***********************************************************
ws01 : ok=2 changed=1 unreachable=0 failed=0
您还可以使用标记在远程主机上执行一组任务,就像从负载平衡器中取出服务器并将其重新添加到负载平衡器中一样。
您也可以使用带有标签的--check
选项。通过这样做,您可以测试您的任务,而无需在您的主机上实际运行它们。这允许您直接测试一堆单独的任务,而不是将您的任务复制到临时行动手册中并从那里运行它。
Ansible 还提供了一种跳过剧本中一些标签的方法。如果您有一个包含多个标签(例如 10 个)的长剧本,并且您想执行除一个标签之外的所有标签,那么将 9 个标签传递给 Ansible 不是一个好主意。如果你忘记传递标签,并且ansible-run
命令失败,情况会更加困难。为了克服这种情况,Ansible 提供了一种跳过几个标签的方法,而不是传递多个标签,这些标签应该运行。它的功能非常简单,可以通过以下方式触发:
ansible-playbook -i ws01, playbooks/tags_example.yaml --skip-tags file_present
输出如下所示:
PLAY [all] ***********************************************************
TASK [Gathering Facts] ***********************************************
ok: [ws01]
TASK [Ensure the file /tmp/ok exists] ********************************
changed: [ws01]
PLAY RECAP ***********************************************************
ws01 : ok=2 changed=1 unreachable=0 failed=0
如您所见,除了带有file_present
标签的任务外,所有任务都已执行。
Ansible 允许我们使用两个非常强大的变量来帮助我们调试。
ANSIBLE_KEEP_REMOTE_FILES
变量允许我们告诉 Ansible 保留它在远程机器上创建的文件,这样我们就可以回去调试它们。
ANSIBLE_DEBUG
变量允许我们告诉 Ansible 将所有调试内容打印到 shell 中。调试输出通常有些矫枉过正,但是它可能有助于解决一些非常复杂的问题。
我们已经看到了如何在您的行动手册中发现问题。有时候,你知道一个特定的步骤可能会失败,但没关系。在这种情况下,我们应该适当地管理异常。让我们看看如何做到这一点。
有很多情况下,由于这样或那样的原因,你希望你的剧本和角色继续下去,以防一个或多个任务失败。这方面的一个典型例子是,您想要检查软件是否已安装。让我们看下面的例子来安装 Java 11,如果,也只有当,Java 8 没有安装。在roles/java/tasks/main.yaml
文件中,我们将输入以下代码:
- name: Verify if Java8 is installed
command: rpm -q java-1.8.0-openjdk
args:
warn: False
register: java
ignore_errors: True
changed_when: java is failed
- name: Ensure that Java11 is installed
yum:
name: java-11-openjdk
state: present
become: True
when: java is failed
在继续执行这个角色所需的其他部分之前,我想花一些时间介绍一下角色任务列表的各个部分,因为有很多新的东西。
在这个任务中,我们将执行一个rpm
命令:
- name: Verify if Java8 is installed
command: rpm -q java-1.8.0-openjdk
args:
warn: False
register: java
ignore_errors: True
changed_when: java is failed
该代码可以有两种可能的输出:
- 失败
- 返回 JDK 包的完整名称
因为我们只想检查包是否存在,然后继续前进,所以我们记录输出(第五条线)并忽略最终的故障(第六条线)。**
**当失败时,意味着Java8
没有安装,因此我们可以继续安装Java11
:
- name: Ensure that Java11 is installed
yum:
name: java-11-openjdk
state: present
become: True
when: java is failed
创建角色后,我们需要包含主机的hosts
文件;就我而言,它将是以下内容:
ws01
我们还需要一份剧本来应用该角色,放在playbooks/hosts/j01.fale.io.yaml
中,内容如下:
- hosts: ws01
roles:
- java
我们现在可以用以下命令执行它:
ansible-playbook playbooks/hosts/ws01.yaml
我们将得到以下结果:
PLAY [ws01] **********************************************************
TASK [Gathering Facts] ***********************************************
ok: [ws01]
TASK [java : Verify if Java8 is installed] ***************************
fatal: [ws01]: FAILED! => {"changed": true, "cmd": ["rpm", "-q", "java-1.8.0-openjdk"], "delta": "0:00:00.028358", "end": "2019-02-10 10:56:22.474350", "msg": "non-zero return code", "rc": 1, "start": "2019-02-10 10:56:22.445992", "stderr": "", "stderr_lines": [], "stdout": "package java-1.8.0-openjdk is not installed", "stdout_lines": ["package java-1.8.0-openjdk is not installed"]}
...ignoring
TASK [java : Ensure that Java11 is installed] ************************
changed: [ws01]
PLAY RECAP ***********************************************************
ws01 : ok=3 changed=2 unreachable=0 failed=0
如您所见,安装检查失败,因为机器上没有安装 Java,因此,另一个任务已经按预期执行。
有些情况下,您想要直接触发故障。这可能有多种原因,即使这样做有缺点,因为当您触发故障时,行动手册将被残酷地中断,如果您不小心,这可能会使您的机器处于不一致的状态。我看到它工作得非常好的一个情况是,当您运行一个非幂等剧本(例如,构建一个应用的较新版本)并且您需要一个变量(例如,要部署的版本/分支)集。在这种情况下,您可以在开始运行操作之前检查预期的变量是否配置正确,以确保以后一切都将按预期运行。
让我们将以下代码放入playbooks/maven_build.yaml
:
- hosts: all
tasks:
- name: Ensure the tag variable is properly set
fail: 'The version needs to be defined. To do so, please add: --extra-vars "version=$[TAG/BRANCH]"'
when: version is not defined
- name: Get last Project version
git:
repo: https://github.com/org/project.git
dest: "/tmp"
version: '{{ version }}'
- name: Maven clean install
shell: "cd /tmp/project && mvn clean install"
如您所见,我们期望用户在脚本调用命令中添加--extra-vars "version=$[TAG/BRANCH]"
。我们可以默认使用一个分支,但是这太冒险了,因为用户可能会失去焦点,忘记自己添加正确的分支名称,这将导致编译(和部署)应用的错误版本。fail
模块还允许我们指定将向用户显示的消息。
I think that the fail
task is far more useful in playbooks that are run manually since, when a playbook is automatically run, managing the exception is often better than failing.
使用fail
模块,一旦检测到问题,您就可以退出行动手册。
在本章中,我们已经看到了如何使用语法检查、有无--diff
的检查模式以及功能测试来调试 Ansible 剧本。
作为功能测试的一部分,我们已经看到了如何在系统的结束状态上执行断言,如何利用标签进行测试,以及如何使用--syntax-check
选项以及ANSIBLE_KEEP_REMOTE_FILES
和ANSIBLE_DEBUG
标志。然后,我们转向故障管理,最后,我们看到了如何故意触发故障。
在下一章中,我们将讨论多层环境以及部署方法。**