Skip to content

Files

Latest commit

869c669 · Dec 29, 2021

History

History
1153 lines (848 loc) · 43.6 KB

File metadata and controls

1153 lines (848 loc) · 43.6 KB

二、自动化简单任务

如前一章所述,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)

所以,我们做了如下工作:

  1. 我们可以参与。
  2. 我们指示 Ansible 在所有主机上运行。
  3. 我们指定了我们的清单(也称为主机列表)。
  4. 我们指定了想要运行的模块(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参数是可选的。yumservice模块有自己的一组参数。几乎所有模块都有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选项运行剧本时会发生什么。

行动手册中的变量

有时候,剧本中的setget变量很重要。

通常,您需要自动化多个类似的操作。在这些情况下,您将希望创建一个可以用不同变量调用的剧本,以确保代码的可重用性。

变量非常重要的另一种情况是,当您有多个数据中心时,某些值将是特定于数据中心的。一个常见的例子是域名系统服务器。让我们分析下面的简单代码,它将向我们介绍设置和获取变量的简单方法:

- 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, viewvalueszfill.

创建 Ansible 的用户

当你创建一台机器(或从任何一家托管公司租赁一台机器)时,它只会带着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

在运行它之前,让我们稍微看一下它。我们使用了三个从未见过的不同模块(userauthorized_keylineinfile)。

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

EPEL 是 Enterprise Linux 最重要的存储库,它包含很多附加包。它也是一个安全的存储库,因为在 EPEL 没有包会与基础存储库中的包冲突。

要在 RHEL/CentOS 7 中启用 EPEL,只需安装epel-release包即可。为此,我们将使用以下内容:

- name: Ensure EPEL is enabled 
  yum: 
    name: epel-release 
    state: present 
  become: True 

如您所见,我们已经使用了yum模块,正如我们在本章的第一个示例中所做的那样,指定了包的名称,并且我们希望它存在。

为 SELinux 安装 Python 绑定

由于 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状态时,我们说它将安装最后一个可用的版本。那么presentlatest有什么区别呢?present如果没有安装软件包会安装最新版本,而如果已经安装了软件包(无论版本如何),则不做任何更改继续前进。latest如果没有安装软件包,会安装最新版本,如果已经安装了软件包,会检查是否有更新的版本,如果有,Ansible 会更新软件包。

确保安装、配置并运行了 NTP

为了确保存在 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,我们需要一个对所有服务器都相同的模板,以及一个使用该模板的任务。

我发现在每台服务器上添加一个 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 服务器。我们将这两个阶段分开,这样我们就可以在每台机器之间共享第一个阶段,并将第二个阶段仅应用于 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 让HTTPHTTPS流量通过。

让我们运行以下脚本:

$ 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 模板

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 模块,例如useryumservice、FirewalID、lineinfile 和模板模块。在这一章的最后,我们集中讨论了模板。

在下一章中,我们将讨论库存,这样我们就可以轻松管理多台机器。