如前一章所述,Ansible 既可用于创建和管理整个基础架构,也可集成到已经运行的基础架构中。
在本章中,我们将涵盖以下主题:
- 亚姆
- 使用行动手册
- Ansible 速度
- 行动手册中的变量
- 创建 Ansible 用户
- 配置基本服务器
- 安装和配置 web 服务器
- 发布网站
- Jinja2 模板
首先,我们将讨论 YAML 不是标记语言 ( YAML ),一种在 Ansible 中广泛使用的人类可读的数据序列化语言。
你可以在 https://github.com/PacktPublishing/Learning-Ansible-2.从这本书的 GitHub 资源库下载所有的文件第三版/树/主/章节 02 。
像许多其他数据序列化语言(如 JSON)一样,YAML 有非常少的基本概念:
- 声明
- 列表
- 关联数组
声明非常类似于任何其他语言中的变量,如下所示:
name: 'This is the name'
要创建列表,我们必须使用-
:
- 'item1'
- 'item2'
- 'item3'
YAML 用缩进来从逻辑上区分父母和孩子。因此,如果我们想要创建关联数组(也称为对象),我们只需要添加一个缩进:
item:
name: TheName
location: TheLocation
显然,我们可以将它们混合在一起,如下所示:
people:
- name: Albert
number: +1000000000
country: USA
- name: David
number: +44000000000
country: UK
这些是 YAML 的基础。YAML 可以做得更多,但就目前而言,这就足够了。
正如我们在上一章中看到的,可以使用 Ansible 来自动化您可能已经每天执行的简单任务。
让我们从检查远程机器是否可达开始;换句话说,让我们从敲击机器开始。最简单的方法是运行以下命令:
$ ansible all -i HOST, -m ping
在这里,HOST
是一个 IP 地址,完全限定域名 ( FQDN ),或者你可以 SSH 访问的机器的别名(你可以使用游民主机,正如我们在上一章看到的)。
After HOST
, the comma is mandatory, because otherwise, it would not be seen as a list, but as a string.
在本例中,我们针对系统中的虚拟机执行了该操作:
$ ansible all -i test01.fale.io, -m ping
因此,您应该会收到这样的消息:
test01.fale.io | SUCCESS => {
"changed": false,
"ping": "pong"
}
现在,让我们看看我们做了什么,为什么。让我们从 Ansible 帮助开始。要查询它,我们可以使用以下命令:
$ ansible --help
为了便于阅读,我们删除了所有与我们没有使用的选项相关的输出:
Usage: ansible <host-pattern> [options]
Options:
-i INVENTORY, --inventory=INVENTORY, --inventory-file=INVENTORY
specify inventory host path or comma separated host
list. --inventory-file is deprecated
-m MODULE_NAME, --module-name=MODULE_NAME
module name to execute (default=command)
所以,我们做了如下工作:
- 我们可以参与。
- 我们指示 Ansible 在所有主机上运行。
- 我们指定了我们的清单(也称为主机列表)。
- 我们指定了想要运行的模块(
ping
)。
现在我们可以 ping 服务器了,让我们试试echo hello ansible!
,如下命令所示:
$ ansible all -i test01.fale.io, -m shell -a '/bin/echo hello ansible!'
因此,您应该会收到这样的消息:
test01.fale.io | CHANGED | rc=0 >>
hello ansible!
在这个例子中,我们使用了一个额外的选项。让我们查看 Ansible 帮助,了解它的功能:
Usage: ansible <host-pattern> [options]
Options:
-a MODULE_ARGS, --args=MODULE_ARGS
module arguments
从上下文和名称中可以猜到,args
选项允许您向模块传递额外的参数。有些模块(如ping
)不支持任何参数,而另一些模块(如shell
)则需要参数。
行动手册是 Ansible 的核心功能之一,告诉 Ansible 要执行什么。它们就像 Ansible 的待办事项列表,其中包含任务列表;每个任务内部链接到一段称为模块的代码。剧本是简单的、人类可读的 YAML 文件,而模块是一段可以用任何语言编写的代码,条件是它的输出是 JSON 格式。您可以在行动手册中列出多个任务,这些任务将由 Ansible 连续执行。你可以把剧本想象成木偶中的清单,盐中的州,或者厨师中的烹饪书;它们允许您输入要在远程系统上执行的任务或命令列表。
行动手册可以包含远程主机、用户变量、任务、处理程序等列表。您也可以通过行动手册覆盖大多数配置设置。让我们开始看看剧本的结构。
我们现在要考虑的行动手册的目的是确保httpd
包已安装,服务已启用****启动。这是setup_apache.yaml
文件的内容:
- hosts: all
remote_user: vagrant
tasks:
- name: Ensure the HTTPd package is installed
yum:
name: httpd
state: present
become: True
- name: Ensure the HTTPd service is enabled and running
service:
name: httpd
state: started
enabled: True
become: True
setup_apache.yaml
文件是剧本的一个例子。该文件由以下三个主要部分组成:
hosts
:这列出了我们要针对其运行任务的主机或主机组。“主机”字段是必需的。Ansible 使用它来确定列出的任务将针对哪些主机。如果提供的是主机组而不是主机,Ansible 将尝试根据清单文件查找属于它的主机。如果不匹配,Ansible 将跳过该主机组的所有任务。--list-hosts
选项以及行动手册(ansible-playbook <playbook> --list-hosts
)将告诉您行动手册将与哪些主机竞争。remote_user
:这是 Ansible 的配置参数之一(以tom' - remote_user
为例),告知 Ansible 在登录系统时使用特定用户(本例中为tom
)。tasks
:最后来说任务。所有行动手册都应该包含任务。任务是您想要执行的操作的列表。一个tasks
字段包含任务的名称(即用户关于任务的帮助文本)、应该执行的模块以及模块所需的参数。让我们看看行动手册中列出的单个任务,如前面的代码片段所示。
All examples in the book would be executed on CentOS, but the same set of examples with a few changes would work on other distributions as well.
在前一种情况下,有两个任务。name
参数表示任务正在做什么,而present
主要是为了提高可读性,我们将在剧本运行期间看到这一点。name
参数是可选的。yum
和service
模块有自己的一组参数。几乎所有模块都有name
参数(也有例外,如debug
模块),该参数指示动作在哪个组件上执行。让我们看看其他参数:
state
参数保存了yum
模块中的最新值,表示应该已经安装了httpd
包。要执行的命令大致翻译为yum install httpd
。- 在
service
模块的场景中,带有 started 值的state
参数表示应该启动httpd
服务,大致翻译为/etc/init.d/httpd
启动。在这个模块中,我们还有enabled
参数,它定义了服务是否应该在启动时启动。 become: True
参数表示任务应该通过sudo
访问来执行。如果sudo
用户的文件不允许用户运行特定的命令,那么剧本将在运行时失败。
You might have questions about why there is no package module that figures out the architecture internally and runs the yum
, apt
, or any other package options depending on the architecture of the system. Ansible populates the package manager value into a variable named ansible_pkg_manager
.
In general, we need to remember that the number of packages that have a common name across different operating systems are a small subset of the number of packages that are actually present. For example, the httpd
package is called httpd
in Red Hat systems, but is called apache2
in Debian-based systems. We also need to remember that every package manager has its own set of options that make it powerful; as a result, it makes more sense to use explicit package manager names so that the full set of options are available to the end user writing the playbook.
现在,是时候了(是的,终于!)来运行剧本。为了指示 Ansible 执行剧本而不是模块,我们将不得不使用不同的命令(ansible-playbooks
),其语法与我们已经看到的ansible
命令非常相似:
$ ansible-playbook -i HOST, setup_apache.yaml
如您所见,除了主机模式(在行动手册中指定)和模块选项(已被行动手册名称所取代)之外,没有任何变化。因此,要在我的机器上执行这个命令,确切的命令如下:
$ ansible-playbook -i test01.fale.io, setup_apache.yaml
结果如下:
PLAY [all] ***********************************************************
TASK [Gathering Facts] ***********************************************
ok: [test01.fale.io]
TASK [Ensure the HTTPd package is installed] *************************
changed: [test01.fale.io]
TASK [Ensure the HTTPd service is enabled and running] ***************
changed: [test01.fale.io]
PLAY RECAP ***********************************************************
test01.fale.io : ok=3 changed=2 unreachable=0 failed=0
哇哦!这个例子奏效了。现在让我们检查一下httpd
包是否已经安装,并且正在机器上启动运行。要检查是否安装了 HTTPd,最简单的方法就是询问rpm
:
$ rpm -qa | grep httpd
如果一切正常,您应该会得到如下输出:
httpd-tools-2.4.6-80.el7.centos.1.x86_64
httpd-2.4.6-80.el7.centos.1.x86_64
要查看服务的状态,我们可以询问systemd
:
$ systemctl status httpd
预期结果如下所示:
httpd.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2018-12-04 15:11:03 UTC; 29min ago
Docs: man:httpd(8)
man:apachectl(8)
Main PID: 604 (httpd)
Status: "Total requests: 0; Current requests/sec: 0; Current traffic: 0 B/sec"
CGroup: /system.slice/httpd.service
├─604 /usr/sbin/httpd -DFOREGROUND
├─624 /usr/sbin/httpd -DFOREGROUND
├─626 /usr/sbin/httpd -DFOREGROUND
├─627 /usr/sbin/httpd -DFOREGROUND
├─628 /usr/sbin/httpd -DFOREGROUND
└─629 /usr/sbin/httpd -DFOREGROUND
根据剧本,最终状态已经达到。让我们简要了解一下在行动手册运行期间到底发生了什么:
PLAY [all] ***********************************************************
这条线告诉我们,剧本将从这里开始,并在all
主机上执行:
TASK [Gathering Facts] ***********************************************
ok: [test01.fale.io]
TASK
行显示任务的名称(在本例中为setup
)及其对每台主机的影响。有时候,人们会被setup
的任务弄糊涂。其实看剧本就没有setup
任务。这是因为 Ansible 在执行我们要求它执行的任务之前,会尝试连接到机器,并收集关于它的信息,这些信息在以后可能会有用。如您所见,该任务导致绿色ok
状态,因此它成功了,并且服务器上没有任何更改:
TASK [Ensure the HTTPd package is installed] *************************
changed: [test01.fale.io]
TASK [Ensure the HTTPd service is enabled and running] ***************
changed: [test01.fale.io]
这两个任务的状态都是黄色,拼changed
。这意味着这些任务已经执行并成功,但它们实际上改变了机器上的某些东西:
PLAY RECAP ***********************************************************
test01.fale.io : ok=3 changed=2 unreachable=0 failed=0
最后几行是剧本的概述。让我们现在重新运行任务,并在两个任务实际运行后查看输出:
PLAY [all] ***********************************************************
TASK [Gathering Facts] ***********************************************
ok: [test01.fale.io]
TASK [Ensure the HTTPd package is installed] *************************
ok: [test01.fale.io]
TASK [Ensure the HTTPd service is enabled and running] ***************
ok: [test01.fale.io]
PLAY RECAP ***********************************************************
test01.fale.io : ok=3 changed=0 unreachable=0 failed=0
如您所料,这两个任务给出了ok
的输出,这意味着在运行任务之前已经满足了期望的状态。重要的是要记住,许多任务,如收集事实任务,获得关于系统特定组件的信息,不一定改变系统上的任何东西;因此,这些任务没有显示之前更改的输出。
第一次和第二次运行中的PLAY RECAP
部分如下所示。在第一次运行期间,您将看到以下输出:
PLAY RECAP ***********************************************************
test01.fale.io : ok=3 changed=2 unreachable=0 failed=0
在第二次运行期间,您将看到以下输出:
PLAY RECAP ***********************************************************
test01.fale.io : ok=3 changed=0 unreachable=0 failed=0
如你所见,不同的是第一个任务的输出显示changed=2
,这意味着由于两个任务,系统状态改变了两次。查看这个输出是非常有用的,因为如果一个系统已经达到了它想要的状态,然后你在上面运行剧本,预期的输出应该是changed=0
。
如果你在这个阶段想到了等幂这个词,你绝对是对的,值得表扬!幂等性是配置管理的关键原则之一。维基百科将幂等性定义为一种运算,如果对任何值应用两次,得到的结果就像应用一次一样。你在童年时遇到的最早的例子是对数字1
的乘法运算,每次都是1*1=1
。
大多数配置管理工具都采用了这一原则,并将其应用于基础架构。在大型基础架构中,强烈建议监视或跟踪基础架构中已更改任务的数量,并在发现异常时提醒相关任务;这通常适用于任何配置管理工具。在理想状态下,您应该看到变化的唯一时间是您以任何形式引入新变化的时候创建、删除、更新或删除 ( CRUD )操作各种系统组件。如果你想知道如何用 Ansible 做到这一点,继续读这本书,你最终会找到答案!
我们继续。您也可以将前面的任务编写如下,但是当任务从最终用户的角度运行时,它们是非常可读的(我们将这个文件称为setup_apache_no_com.yaml
):
---
- hosts: all
remote_user: vagrant
tasks:
- yum:
name: httpd
state: present
become: True
- service:
name: httpd
state: started
enabled: True
become: True
让我们再次运行剧本,找出输出中的任何差异:
$ ansible-playbook -i test01.fale.io, setup_apache_no_com.yaml
输出如下:
PLAY [all] ***********************************************************
TASK [Gathering Facts] ***********************************************
ok: [test01.fale.io]
TASK [yum] ***********************************************************
ok: [test01.fale.io]
TASK [service] *******************************************************
ok: [test01.fale.io]
PLAY RECAP ***********************************************************
test01.fale.io : ok=3 changed=0 unreachable=0 failed=0
如您所见,不同之处在于可读性。只要有可能,建议尽可能地保持任务的简单性( KISS 原则:保持简单,愚蠢)以考虑到你的脚本的长期可维护性。
现在,我们已经了解了如何编写基本的行动手册并在主机上运行它,让我们看看在运行行动手册时可以帮助您的其他选项。
任何人首先选择的选项之一是调试选项。要了解运行剧本时发生了什么,可以使用 verbose ( -v
)选项来运行。每增加一个v
将为最终用户提供更多的调试输出。
让我们看一个使用这些选项调试一个简单的ping
命令(ansible all -i test01.fale.io, -m ping
)的例子:
-v
选项提供默认输出:
Using /etc/ansible/ansible.cfg as config file
test01.fale.io | SUCCESS => {
"changed": false,
"ping": "pong"
}
-vv
选项添加了更多关于 Ansible 环境和处理程序的信息:
ansible 2.7.2
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/home/fale/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /bin/ansible
python version = 2.7.15 (default, Oct 15 2018, 15:24:06) [GCC 8.1.1 20180712 (Red Hat 8.1.1-5)]
Using /etc/ansible/ansible.cfg as config file
META: ran handlers
test01.fale.io | SUCCESS => {
"changed": false,
"ping": "pong"
}
META: ran handlers
META: ran handlers
-vvv
选项增加了更多信息。例如,它显示了 Ansible 用来在远程主机上创建临时文件并远程运行脚本的ssh
命令。GitHub 上有完整的脚本。
ansible 2.7.2
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/home/fale/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /bin/ansible
python version = 2.7.15 (default, Oct 15 2018, 15:24:06) [GCC 8.1.1 20180712 (Red Hat 8.1.1-5)]
Using /etc/ansible/ansible.cfg as config file
Parsed test01.fale.io, inventory source with host_list plugin
META: ran handlers
<test01.fale.io> ESTABLISH SSH CONNECTION FOR USER: None
<test01.fale.io> SSH: EXEC ssh -C -o ControlMaster=auto -o
...
现在,我们了解了当您使用详细的- vvv
选项运行剧本时会发生什么。
有时候,剧本中的set
和get
变量很重要。
通常,您需要自动化多个类似的操作。在这些情况下,您将希望创建一个可以用不同变量调用的剧本,以确保代码的可重用性。
变量非常重要的另一种情况是,当您有多个数据中心时,某些值将是特定于数据中心的。一个常见的例子是域名系统服务器。让我们分析下面的简单代码,它将向我们介绍设置和获取变量的简单方法:
- hosts: all
remote_user: vagrant
tasks:
- name: Set variable 'name'
set_fact:
name: Test machine
- name: Print variable 'name'
debug:
msg: '{{ name }}'
让我们以通常的方式运行它:
$ ansible-playbook -i test01.fale.io, variables.yaml
您应该会看到以下结果:
PLAY [all] *********************************************************
TASK [Gathering Facts] *********************************************
ok: [test01.fale.io]
TASK [Set variable 'name'] *****************************************
ok: [test01.fale.io]
TASK [Print variable 'name'] ***************************************
ok: [test01.fale.io] => {
"msg": "Test machine"
}
PLAY RECAP *********************************************************
test01.fale.io : ok=3 changed=0 unreachable=0 failed=0
如果我们分析刚刚执行的代码,应该会非常清楚发生了什么。我们设置一个变量(在 Ansible 中称为facts
),然后用debug
函数打印出来。
Variables should always be between quotes when you use this expanded version of YAML.
Ansible 允许您以多种不同的方式设置变量——也就是说,要么通过传递变量文件,在剧本中声明它,使用-e / --extra-vars
将它传递给ansible-playbook
命令,要么通过在清单文件中声明它(我们将在下一章中对此进行深入讨论)。
现在是时候开始使用 Ansible 在设置阶段获得的一些元数据了。让我们从 Ansible 收集的数据开始。为此,我们将执行以下代码:
$ ansible all -i HOST, -m setup
在我们的具体例子中,这意味着执行以下代码:
$ ansible all -i test01.fale.io, -m setup
我们显然可以用剧本做同样的事情,但是这种方式更快。同样,对于setup
的情况,您只需要在开发过程中查看输出,以确保为您的目标使用正确的变量名。
输出会是这样的。GitHub 上有完整的代码输出。
test01.fale.io | SUCCESS => {
"ansible_facts": {
"ansible_all_ipv4_addresses": [
"192.168.121.190"
],
"ansible_all_ipv6_addresses": [
"fe80::5054:ff:fe93:f113"
],
"ansible_apparmor": {
"status": "disabled"
},
"ansible_architecture": "x86_64",
"ansible_bios_date": "04/01/2014",
"ansible_bios_version": "?-20180531_142017-buildhw-08.phx2.fedoraproject.org-1.fc28",
...
正如你从这个巨大的选项列表中看到的,你可以获得大量的信息,并且你可以将它们用作任何其他变量。让我们打印操作系统名称和版本。为此,我们可以创建一个名为setup_variables.yaml
的新剧本,其内容如下:
- hosts: all
remote_user: vagrant
tasks:
- name: Print OS and version
debug:
msg: '{{ ansible_distribution }} {{ ansible_distribution_version }}'
使用以下代码运行它:
$ ansible-playbook -i test01.fale.io, setup_variables.yaml
这将为我们提供以下输出:
PLAY [all] *********************************************************
TASK [Gathering Facts] *********************************************
ok: [test01.fale.io]
TASK [Print OS and version] ****************************************
ok: [test01.fale.io] => {
"msg": "CentOS 7.5.1804"
}
PLAY RECAP *********************************************************
test01.fale.io : ok=2 changed=0 unreachable=0 failed=0
如您所见,它按照预期打印了操作系统名称和版本。除了前面看到的方法,还可以使用命令行参数传递变量。事实上,如果我们查看 Ansible 帮助,我们会注意到以下内容:
Usage: ansible <host-pattern> [options]
Options:
-e EXTRA_VARS, --extra-vars=EXTRA_VARS
set additional variables as key=value or YAML/JSON, if
filename prepend with @
ansible-playbook
命令中也有相同的行。让我们制作一个名为cli_variables.yaml
的小剧本,内容如下:
---
- hosts: all
remote_user: vagrant
tasks:
- name: Print variable 'name'
debug:
msg: '{{ name }}'
使用以下命令执行:
$ ansible-playbook -i test01.fale.io, cli_variables.yaml -e 'name=test01'
我们将收到以下信息:
[WARNING]: Found variable using reserved name: name
PLAY [all] *********************************************************
TASK [Gathering Facts] *********************************************
ok: [test01.fale.io]
TASK [Print variable 'name'] ***************************************
ok: [test01.fale.io] => {
"msg": "test01"
}
PLAY RECAP *********************************************************
test01.fale.io : ok=2 changed=0 unreachable=0 failed=0
如果我们忘记添加额外的参数来指定变量,我们会按如下方式执行:
$ ansible-playbook -i test01.fale.io, cli_variables.yaml
我们会收到以下输出:
PLAY [all] *********************************************************
TASK [Gathering Facts] *********************************************
ok: [test01.fale.io]
TASK [Print variable 'name'] ***************************************
fatal: [test01.fale.io]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'name' is undefined\n\nThe error appears to have been in '/home/fale/Learning-Ansible-2.X-Third-Edition/Ch2/cli_variables.yaml': line 5, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n tasks:\n - name: Print variable 'name'\n ^ here\n"}
to retry, use: --limit @/home/fale/Learning-Ansible-2.X-Third-Edition/Ch2/cli_variables.retry
PLAY RECAP *********************************************************
test01.fale.io : ok=1 changed=0 unreachable=0 failed=1
既然我们已经学习了行动手册的基础知识,让我们使用它们从头开始创建一个 web 服务器。为此,让我们从头开始,创建一个 Ansible 用户,然后从那里继续前进。
As you will notice in the previous example, a WARNING popped up, informing us that we were re-declaring a reserved variable (name). The full list of reserved variables (as of Ansible 2.7) are as follows: add
, append
, as_integer_ratio
, bit_length
, capitalize
, center
, clear
, conjugate
, copy
, count
, decode
, denominator
, difference
, difference_update
, discard
, encode
, endswith
, expandtabs
, extend
, find
, format
, fromhex
, fromkeys
, get
, has_key
, hex
, imag
, index
, insert
, intersection
, intersection_update
, isalnum
, isalpha
, isdecimal
, isdigit
, isdisjoint
, is_integer
, islower
, isnumeric
, isspace
, issubset
, issuperset
, istitle
, isupper
, items
, iteritems
, iterkeys
, itervalues
, join
, keys
, ljust
, lower
, lstrip
, numerator
, partition
, pop
, popitem
, real
, remove
, replace
, reverse
, rfind
, rindex
, rjust
, rpartition
, rsplit
, rstrip
, setdefault
, sort
, split
, splitlines
, startswith
, strip
, swapcase
, symmetric_difference
, symmetric_difference_update
, title
, translate
, union
, update
, upper
, values
, viewitems
, viewkeys
, viewvalues
, zfill
.
当你创建一台机器(或从任何一家托管公司租赁一台机器)时,它只会带着root
用户或其他用户(如vagrant
)到达。让我们开始创建一个行动手册,确保创建了一个 Ansible 用户,可以使用 SSH 密钥访问该用户,并且能够代表其他用户(sudo
)执行操作,而不需要密码。我们通常称这个剧本为firstrun.yaml
,因为我们一创建新机器就执行它,但是在那之后,我们不使用它,因为我们出于安全原因禁用了默认用户。我们的脚本如下所示:
---
- hosts: all
user: vagrant
tasks:
- name: Ensure ansible user exists
user:
name: ansible
state: present
comment: Ansible
become: True
- name: Ensure ansible user accepts the SSH key
authorized_key:
user: ansible
key: https://github.com/fale.keys
state: present
become: True
- name: Ensure the ansible user is sudoer with no password required
lineinfile:
dest: /etc/sudoers
state: present
regexp: '^ansible ALL\='
line: 'ansible ALL=(ALL) NOPASSWD:ALL'
validate: 'visudo -cf %s'
become: True
在运行它之前,让我们稍微看一下它。我们使用了三个从未见过的不同模块(user
、authorized_key
、lineinfile
)。
user
模块,顾名思义,允许我们确保用户在场(或缺席)。
authorized_key
模块允许我们确保某个 SSH 密钥可以作为特定用户登录到该机器上。该模块不会替换已经为该用户启用的所有 SSH 密钥,而只是添加(或删除)指定的密钥。如果你想改变这个行为,你可以使用exclusive
选项,它允许你删除所有在这个步骤中没有指定的 SSH 密钥。
lineinfile
模块允许我们更改文件的内容。它的工作方式与 sed (一个流编辑器)非常相似,在这里您指定将用于匹配该行的正则表达式,然后指定将用于替换匹配行的新行。如果没有匹配的行,该行将被添加到文件的末尾。
现在让我们用下面的代码运行它:
$ ansible-playbook -i test01.fale.io, firstrun.yaml
这将给我们以下结果:
PLAY [all] *********************************************************
TASK [Gathering Facts] *********************************************
ok: [test01.fale.io]
TASK [Ensure ansible user exists] **********************************
changed: [test01.fale.io]
TASK [Ensure ansible user accepts the SSH key] *********************
changed: [test01.fale.io]
TASK [Ensure the ansible user is sudoer with no password required] *
changed: [test01.fale.io]
PLAY RECAP *********************************************************
test01.fale.io : ok=4 changed=3 unreachable=0 failed=0
在我们为 Ansible 创建了具有必要权限的用户之后,我们可以继续对操作系统进行一些其他的小更改。为了更清楚,我们将看到每个动作是如何执行的,然后我们将查看整个行动手册。
EPEL 是 Enterprise Linux 最重要的存储库,它包含很多附加包。它也是一个安全的存储库,因为在 EPEL 没有包会与基础存储库中的包冲突。
要在 RHEL/CentOS 7 中启用 EPEL,只需安装epel-release
包即可。为此,我们将使用以下内容:
- name: Ensure EPEL is enabled
yum:
name: epel-release
state: present
become: True
如您所见,我们已经使用了yum
模块,正如我们在本章的第一个示例中所做的那样,指定了包的名称,并且我们希望它存在。
由于 Ansible 是用 Python 编写的,主要使用 Python 绑定在操作系统上操作,所以我们需要为 SELinux 安装 Python 绑定:
- name: Ensure libselinux-python is present
yum:
name: libselinux-python
state: present
become: True
- name: Ensure libsemanage-python is present
yum:
name: libsemanage-python
state: present
become: True
This could be written in a shorter way, using a cycle, but we'll see how to do this in the next chapter.
要升级所有已安装的软件包,我们需要再次使用yum
模块,但参数不同;事实上,我们将使用以下内容:
- name: Ensure we have last version of every package
yum:
name: "*"
state: latest
become: True
如您所见,我们已经将*
指定为软件包名称(这代表匹配所有已安装软件包的通配符),并且state
参数是latest
。这将把所有已安装的软件包升级到最新版本。
你可能还记得,当我们谈到present
状态时,我们说它将安装最后一个可用的版本。那么present
和latest
有什么区别呢?present
如果没有安装软件包会安装最新版本,而如果已经安装了软件包(无论版本如何),则不做任何更改继续前进。latest
如果没有安装软件包,会安装最新版本,如果已经安装了软件包,会检查是否有更新的版本,如果有,Ansible 会更新软件包。
为了确保存在 NTP,我们使用yum
模块:
- name: Ensure NTP is installed
yum:
name: ntp
state: present
become: True
既然知道安装了 NTP,就要保证服务器使用的是我们想要的timezone
。为此,我们将在/etc/localtime
中创建一个符号链接,指向需要的zoneinfo
文件:
- name: Ensure the timezone is set to UTC
file:
src: /usr/share/zoneinfo/GMT
dest: /etc/localtime
state: link
become: True
如您所见,我们使用了file
模块,指定它需要是一个链接(state: link
)。
要完成 NTP 配置,我们需要启动ntpd
服务,并确保它将在每次后续启动时运行:
- name: Ensure the NTP service is running and enabled
service:
name: ntpd
state: started
enabled: True
become: True
可以想象,第一步是确保安装 FirewallD:
- name: Ensure FirewallD is installed
yum:
name: firewalld
state: present
become: True
由于我们希望确保在启用防火墙时不会丢失 SSH 连接,因此我们将确保 SSH 流量始终可以通过:
- name: Ensure SSH can pass the firewall
firewalld:
service: ssh
state: enabled
permanent: True
immediate: True
become: True
为此,我们使用了firewalld
模块。该模块将采用与firewall-cmd
控制台非常相似的参数。您必须指定将被授权通过防火墙的服务,您是否希望此规则立即应用,以及您是否希望此规则是永久的,以便在重新启动后规则仍然存在。
You can specify the service name (such as ssh
) using the service
parameter, or you can specify the port (such as 22/tcp
) using the port
parameter.
现在我们已经安装了 FirewallD,并且我们确信我们的 SSH 连接将继续存在,我们可以像启用任何其他服务一样启用它:
- name: Ensure FirewallD is running
service:
name: firewalld
state: started
enabled: True
become: True
要添加 MOTD,我们需要一个对所有服务器都相同的模板,以及一个使用该模板的任务。
我发现在每台服务器上添加一个 MOTD 非常有用。如果您使用 Ansible,它会更有用,因为您可以使用它来警告您的用户,对系统的更改可能会被 Ansible 覆盖。我常用的模板叫motd
,有这样的内容:
This system is managed by Ansible
Any change done on this system could be overwritten by Ansible
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
Hostname: {{ inventory_hostname }}
eth0 address: {{ ansible_eth0.ipv4.address }}
All connections are monitored and recorded
Disconnect IMMEDIATELY if you are not an authorized user
这是一个jinja2
模板,它允许我们使用行动手册中设置的每个变量。这也允许我们对条件句和循环使用复杂的语法,我们将在本章后面看到。要从 Ansible 中的模板填充文件,我们需要使用以下内容:
- name: Ensure the MOTD file is present and updated
template:
src: motd
dest: /etc/motd
owner: root
group: root
mode: 0644
become: True
template
模块允许我们指定一个将由jinja2
解释的本地文件(src
),该操作的输出将保存在远程机器上的特定路径(dest
)中,将由特定用户(owner
)和组(group
)拥有,并将具有特定的访问模式(mode
)。
为了简单起见,我发现将机器的主机名设置为有意义的值很有用。为此,我们可以使用一个非常简单的 Ansible 模块hostname
:
- name: Ensure the hostname is the same of the inventory
hostname:
name: "{{ inventory_hostname }}"
become: True
将所有内容放在一起,我们现在有以下行动手册(为简单起见,称为common_tasks.yaml
):
---
- hosts: all
remote_user: ansible
tasks:
- name: Ensure EPEL is enabled
yum:
name: epel-release
state: present
become: True
- name: Ensure libselinux-python is present
yum:
name: libselinux-python
state: present
become: True
...
由于这个playbook
相当复杂,我们可以运行以下内容:
$ ansible-playbook common_tasks.yaml --list-tasks
这要求 Ansible 以较短的形式打印所有任务,以便我们可以快速查看playbook
执行的任务。输出应该如下所示:
playbook: common_tasks.yaml
play #1 (all): all TAGS: []
tasks:
Ensure EPEL is enabled TAGS: []
Ensure libselinux-python is present TAGS: []
Ensure libsemanage-python is present TAGS: []
Ensure we have last version of every package TAGS: []
Ensure NTP is installed TAGS: []
Ensure the timezone is set to UTC TAGS: []
Ensure the NTP service is running and enabled TAGS: []
Ensure FirewallD is installed TAGS: []
Ensure FirewallD is running TAGS: []
Ensure SSH can pass the firewall TAGS: []
Ensure the MOTD file is present and updated TAGS: []
Ensure the hostname is the same of the inventory TAGS: []
我们现在可以使用以下内容运行playbook
:
$ ansible-playbook -i test01.fale.io, common_tasks.yaml
我们将收到以下输出。GitHub 上有完整的代码输出。
PLAY [all] ***************************************************
TASK [Gathering Facts] ***************************************
ok: [test01.fale.io]
TASK [Ensure EPEL is enabled] ********************************
changed: [test01.fale.io]
TASK [Ensure libselinux-python is present] *******************
ok: [test01.fale.io]
TASK [Ensure libsemanage-python is present] ******************
changed: [test01.fale.io]
TASK [Ensure we have last version of every package] **********
changed: [test01.fale.io]
...
现在我们已经对操作系统进行了一些一般性的更改,让我们继续实际创建一个 web 服务器。我们将这两个阶段分开,这样我们就可以在每台机器之间共享第一个阶段,并将第二个阶段仅应用于 web 服务器。
对于第二阶段,我们将创建名为webserver.yaml
的新行动手册,内容如下:
---
- hosts: all
remote_user: ansible
tasks:
- name: Ensure the HTTPd package is installed
yum:
name: httpd
state: present
become: True
- name: Ensure the HTTPd service is enabled and running
service:
name: httpd
state: started
enabled: True
become: True
- name: Ensure HTTP can pass the firewall
firewalld:
service: http
state: enabled
permanent: True
immediate: True
become: True
- name: Ensure HTTPS can pass the firewall
firewalld:
service: https
state: enabled
permanent: True
immediate: True
become: True
可以看到,前两个任务与本章开头示例中的任务相同,后两个任务用于指示 FirewallD 让HTTP
和HTTPS
流量通过。
让我们运行以下脚本:
$ ansible-playbook -i test01.fale.io, webserver.yaml
这将导致以下结果:
PLAY [all] *************************************************
TASK [Gathering Facts] *************************************
ok: [test01.fale.io]
TASK [Ensure the HTTPd package is installed] ***************
ok: [test01.fale.io]
TASK [Ensure the HTTPd service is enabled and running] *****
ok: [test01.fale.io]
TASK [Ensure HTTP can pass the firewall] *******************
changed: [test01.fale.io]
TASK [Ensure HTTPS can pass the firewall] ******************
changed: [test01.fale.io]
PLAY RECAP *************************************************
test01.fale.io : ok=5 changed=2 unreachable=0 failed=0
现在我们有了一个网络服务器,让我们发布一个小的,单页的,静态的网站。
由于我们的网站将是一个简单的单页网站,我们可以使用一个简单的 Ansible 任务轻松创建和发布它。为了让这个页面更有趣一点,我们将从一个模板创建它,该模板将由 Ansible 用一点关于机器的数据填充。发布它的脚本将被称为deploy_website.yaml
,并将有以下内容:
---
- hosts: all
remote_user: ansible
tasks:
- name: Ensure the website is present and updated
template:
src: index.html.j2
dest: /var/www/html/index.html
owner: root
group: root
mode: 0644
become: True
让我们从一个我们称之为index.html.j2
的简单模板开始:
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>
现在,我们可以通过运行以下命令来测试我们的网站部署:
$ ansible-playbook -i test01.fale.io, deploy_website.yaml
我们应该会收到以下输出:
PLAY [all] ***********************************************
TASK [Gathering Facts] ***********************************
ok: [test01.fale.io]
TASK [Ensure the website is present and updated] *********
changed: [test01.fale.io]
PLAY RECAP ***********************************************
test01.fale.io : ok=2 changed=1 unreachable=0 failed=0
如果你现在在浏览器中进入你的 IP/FQDN 测试机,你会发现 **Hello World!**页。
Jinja2 是 Python 广泛使用且功能齐全的模板引擎。让我们看一些语法,这将有助于我们使用 Ansible。这一段不是官方文档的替代,但它的目标是教你一些在与 Ansible 一起使用时会发现非常有用的组件。
正如我们所看到的,我们可以简单地通过使用{{ VARIABLE_NAME }}
语法来打印变量内容。如果我们想打印一个数组的元素,我们可以使用{{ ARRAY_NAME['KEY'] }}
,如果我们想打印一个对象的属性,我们可以使用{{ OBJECT_NAME.PROPERTY_NAME }}
。
因此,我们可以通过以下方式改进之前的静态页面:
<html>
<body>
<h1>Hello World!</h1>
<p>This page was created on {{ ansible_date_time.date }}.</p>
</body>
</html>
有时,我们可能想稍微改变一个字符串的样式,而不需要为它编写特定的代码;例如,我们可能想要大写一些文本。为此,我们可以使用 Jinja2 的过滤器之一,如{{ VARIABLE_NAME | capitalize }}
。Jinja2 有很多过滤器,你可以在http://jinja.pocoo.org/docs/dev/templates/#builtin-filters找到完整的列表。
在模板引擎中,您可能经常会发现一件有用的事情,那就是根据字符串的内容(或存在)打印不同字符串的可能性。因此,我们可以通过以下方式改进静态网页:
<html>
<body>
<h1>Hello World!</h1>
<p>This page was created on {{ ansible_date_time.date }}.</p>
{% if ansible_eth0.active == True %}
<p>eth0 address {{ ansible_eth0.ipv4.address }}.</p>
{% endif %}
</body>
</html>
如您所见,如果连接是active
,我们增加了打印eth0
连接的主 IPv4 地址的功能。有了条件句,我们也可以使用测试。
For a full list of builtin tests, please refer to http://jinja.pocoo.org/docs/dev/templates/#builtin-tests.
因此,为了获得相同的结果,我们也可以写下以下内容:
<html>
<body>
<h1>Hello World!</h1>
<p>This page was created on {{ ansible_date_time.date }}.</p>
{% if ansible_eth0.active is equalto True %}
<p>eth0 address {{ ansible_eth0.ipv4.address }}.</p>
{% endif %}
</body>
</html>
有很多不同的测试将真正帮助你创建易于阅读、有效的模板。
jinja2
模板系统还提供了创建周期的功能。让我们在页面上添加一个功能,打印每个设备的主 IPv4 网络地址,而不仅仅是eth0
。然后,我们将获得以下代码:
<html>
<body>
<h1>Hello World!</h1>
<p>This page was created on {{ ansible_date_time.date }}.</p>
<p>This machine can be reached on the following IP addresses</p>
<ul>
{% for address in ansible_all_ipv4_addresses %}
<li>{{ address }}</li>
{% endfor %}
</ul>
</body>
</html>
如您所见,如果您已经了解 Python,那么循环的语法是很熟悉的。
Jinja2 模板上的这几页不能替代官方文档。事实上,Jinja2 模板比我们在这里看到的要强大得多。这里的目标是为您提供 Ansible 中最常用的基本 Jinja2 模板。
在这一章中,我们开始关注 YAML,并了解了什么是行动手册,它是如何工作的,以及如何使用它来创建一个 web 服务器(以及静态网站的部署)。我们还看到了多个 Ansible 模块,例如user
、yum
、service
、FirewalID、lineinfile 和模板模块。在这一章的最后,我们集中讨论了模板。
在下一章中,我们将讨论库存,这样我们就可以轻松管理多台机器。