Ansible-学习手册-全-

Ansible 学习手册(全)

原文:zh.annas-archive.org/md5/9B9E8543F5B9586A00B5C40E5C135DD5

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Ansible 已经迅速从一个小型的开源编排工具发展成为一款完整的编排和配置管理工具,由红帽公司拥有。在本书中,您将学习如何使用核心 Ansible 模块编写 playbook,部署从基本的 LAMP 堆栈到完整的高可用公共云基础架构。

通过本书,您将学会以下内容:

  • 编写自己的 playbook 来配置运行 CentOS 7、Ubuntu 17.04 和 Windows Server 的服务器

  • 定义了一个高可用的云基础架构代码,使得很容易将您的基础架构配置与您自己的代码一起分发

  • 部署和配置 Ansible Tower 和 Ansible AWX

  • 使用社区贡献的角色,并学习如何贡献自己的角色

  • 通过几个用例,了解如何在日常角色和项目中使用 Ansible

通过本书,您应该对如何将 Ansible 集成到日常角色中有一个很好的想法,比如系统管理员、开发人员和 DevOps 从业者。

这本书适合谁

这本书非常适合想要将他们当前的工作流程转变为可重复使用的 playbook 的系统管理员、开发人员和 DevOps 从业者。不需要先前对 Ansible 的了解。

本书涵盖的内容

第一章,Ansible 简介,讨论了 Ansible 开发的问题,作者是谁,并谈到了红帽公司在收购 Ansible 后的参与情况。

第二章,安装和运行 Ansible,讨论了我们将如何在 macOS 和 Linux 上安装 Ansible,然后介绍其背景。我们还将讨论为什么没有本地的 Windows 安装程序,并介绍在 Windows 10 专业版的 Ubuntu shell 上安装 Ansible。

第三章,Ansible 命令,解释了在开始编写和执行更高级的 playbook 之前,我们将先了解 Ansible 命令。在这里,我们将介绍组成 Ansible 的一组命令的用法。

第四章,部署 LAMP 堆栈,讨论了使用随 Ansible 提供的各种核心模块部署完整的 LAMP 堆栈。我们将针对本地运行的 CentOS 7 虚拟机进行操作。

第五章,部署 WordPress,解释了我们在上一章部署的 LAMP 堆栈作为基础。我们将使用 Ansible 来下载、安装和配置 WordPress。

第六章,面向多个发行版,解释了我们将如何调整 playbook,使其可以针对 Ubuntu 17.04 和 CentOS 7 服务器运行。前两章的最终 playbook 已经编写成针对 CentOS 7 虚拟机。

第七章,核心网络模块,解释了我们将如何查看随 Ansible 一起提供的核心网络模块。由于这些模块的要求,我们只会涉及这些模块提供的功能。

第八章,迁移到云,讨论了我们将如何从使用本地虚拟机转移到使用 Ansible 在 DigitalOcean 中启动 Droplet,然后我们将使用前几章的 playbook 来安装和配置 LAMP 堆栈和 WordPress。

第九章,构建云网络,讨论了在 DigitalOcean 中启动服务器后。我们将转移到 Amazon Web Services,然后启动实例。我们需要为它们创建一个网络来进行托管。

第十章,高可用云部署,继续我们的 Amazon Web Services 部署。我们将开始将服务部署到上一章中创建的网络中,到本章结束时,我们将留下一个高可用的 WordPress 安装。

第十一章,构建 VMware 部署,讨论了允许您与构成典型 VMware 安装的各种组件进行交互的核心模块。

第十二章,Ansible Windows 模块,介绍了不断增长的核心 Ansible 模块集,支持并与基于 Windows 的服务器交互。

第十三章,使用 Ansible 和 OpenSCAP 加固您的服务器,解释了如何使用 Ansible 安装和执行 OpenSCAP。我们还将研究如何使用 Ansible 解决在 OpenSCAP 扫描期间发现的任何问题。

第十四章,部署 WPScan 和 OWASP ZAP,解释了创建一个部署和运行两个安全工具 OWASP ZAP 和 WPScan 的 playbook。然后,使用前几章的 playbook 启动 WordPress 安装以运行它们。

第十五章,介绍 Ansible Tower 和 Ansible AWX,介绍了两个与 Ansible 相关的图形界面,商业版的 Ansible Tower 和开源版的 Ansible AWX。

第十六章,Ansible Galaxy,讨论了 Ansible Galaxy,这是一个社区贡献角色的在线存储库。在本章中,我们将发现一些最好的可用角色,如何使用它们,以及如何创建自己的角色并将其托管在 Ansible Galaxy 上。

第十七章,使用 Ansible 的下一步,教我们如何将 Ansible 集成到我们的日常工作流程中,从与协作服务的交互到使用内置调试器解决 playbook 的故障。我们还将看一些我如何使用 Ansible 的真实例子。

为了充分利用本书

为了充分利用本书,我假设你:

  • 有一些在 Linux 和 macOS 上使用命令行的经验

  • 对如何在 Linux 服务器上安装和配置服务有基本的了解

  • 对服务和语言(如 Git、YAML 和虚拟化)有工作知识

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

您可以按照以下步骤下载代码文件:

  1. www.packtpub.com上登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载和勘误”。

  4. 在搜索框中输入书名,并按照屏幕上的说明操作。

一旦文件下载完成,请确保您使用最新版本的解压缩或提取文件夹:

  • Windows 的 WinRAR/7-Zip

  • Mac 的 Zipeg/iZip/UnRarX

  • Linux 的 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learn-Ansible。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/上找到!看看它们!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/LearnAnsible_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:“这将创建一个密钥并将其存储在您的user目录中的.ssh文件夹中。”

代码块设置如下:

  config.vm.provider "virtualbox" do |v|
    v.memory = "2024"
    v.cpus = "2"
  end

任何命令行输入或输出都将按如下方式编写:

$ sudo -H apt-get install python-pip
$ sudo -H pip install ansible

Bold:表示一个新术语、一个重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中以这种方式出现。这是一个例子:“要做到这一点,打开控制面板,然后点击程序和功能,然后点击打开或关闭 Windows 功能。”

警告或重要说明会出现在这样的形式中。提示和技巧会出现在这样的形式中。

第一章:Ansible 简介

在我们的第一章中,我们将研究 Ansible 出现之前的技术世界,以便了解为什么需要 Ansible。

在我们开始讨论 Ansible 之前,让我们快速讨论一下旧世界。我从 90 年代末开始就一直在使用服务器,大多数是用来提供网页服务的,而当时的情况已经完全不同了。为了让你了解我是如何操作我的早期服务器的,这里是我运行服务器的前几年的简要概述。

和当时的大多数人一样,我最初使用的是共享托管账户,在那时,我对服务器端的任何事情都几乎没有控制权。当时运行的网站已经超出了共享托管的范围。我转移到了专用服务器——我以为我可以在这里展示未来的系统管理员能力,但我错了。

我得到的服务器是 Cobalt RaQ 3,一个 1U 服务器设备,我认为它领先于当时的技术。然而,我没有 root 级别的访问权限,对于我需要做的一切,我都必须使用基于 Web 的控制面板。最终,我获得了一定级别的访问权限,可以使用 SSH 或 Telnet 访问服务器(我知道,那是早期),并开始通过在 Web 控制面板中进行更改并查看服务器上的配置文件来自学成为系统管理员。

过了一段时间,我换了服务器,这次我选择放弃任何基于 Web 的控制面板,只使用我在 Cobalt RaQ 上学到的知识来配置我的第一个真正的Linux, Apache, MySQL, PHP (LAMP)服务器,使用我做的笔记。我创建了自己的运行手册,包括一行命令来安装和配置我需要的软件,以及大量的涂鸦来帮助我解决问题并保持服务器运行。

当我为另一个项目获得了第二台服务器后,我意识到这可能是一个好时机,可以打字记录我的笔记,这样在需要部署服务器时就可以复制粘贴,我很高兴我这样做了,因为就在我的第一台服务器失败后不久,我的主机道歉并用一台更新的操作系统替换了它,配置也更高。

于是,我打开了我的 Microsoft Word 文件,里面有我做的笔记,然后逐条复制粘贴每条指令,根据我需要安装的内容和升级后的操作系统进行调整。几个小时后,我的服务器恢复正常运行,我的数据也被恢复了。

我学到的一个重要教训之一是,备份永远不嫌多,另一个是不要使用 Microsoft Word 来存储这些类型的笔记;命令并不在乎你的笔记是否都是用漂亮的格式和 Courier 字体编写的。它在乎的是使用正确的语法,而 Word 已经自动更正和格式化为打印格式。

所以,我在服务器上复制了历史文件,并将我的笔记转录成纯文本。这些笔记成为了接下来几年的基础,因为我开始对其中的部分内容进行脚本化,主要是那些不需要用户输入的部分。

这些命令片段、一行命令和脚本都是通过 Red Hat Linux 6 进行调整的,一直到 CentOS 3 和 4。

当我改变了角色,停止了从 Web 主机那里获取服务,并开始为 Web 主机工作时,事情变得复杂起来。突然间,我开始为可能有不同需求的客户构建服务器——没有一个服务器是相同的。

从这里开始,我开始使用 Kickstart 脚本、PXE 引导服务器、镜像服务器上的 gold master、虚拟机和开始提示正在构建的系统的信息的 bash 脚本。我也从只需要担心维护自己的服务器转变为需要登录数百个不同的物理和虚拟服务器,从属于我工作的公司的服务器到客户机器。

在接下来的几年里,我的单个文本文件迅速变成了一个复杂的笔记、脚本、预编译二进制文件和信息电子表格的集合,如果我诚实地说,这些东西只有我自己能理解。

虽然我已经开始使用 bash 脚本和串联命令来自动化我的日常工作的很多部分,但我发现我的日子仍然充满了手动运行所有这些任务,以及处理客户报告的问题和查询的服务台工作。

我的故事可能是许多人的典型,而使用的操作系统可能被认为是相当古老的。现在,使用 GUI 作为入口并转向命令行,同时保留常用命令的草稿本,是我听说过的一个很常见的过程。

我们将涵盖以下主题:

  • 谁在背后支持 Ansible

  • Ansible 与其他工具的区别

  • Ansible 解决的问题

Ansible 的故事

让我们快速看一下谁写了 Ansible,以及 Ansible 的含义。

这个术语

在讨论 Ansible 的起源之前,我们应该快速讨论一下名称的起源。术语 Ansible 是由科幻小说作家乌苏拉·勒·格恩创造的;它首次出现在她 1966 年首次出版的小说《洛坎农的世界》中。在故事的背景下,Ansible是一个虚构的设备,能够比光速更快地发送和接收消息。

1974 年,乌苏拉·勒·格恩的小说《被放逐者:一个模棱两可的乌托邦》出版;这本书通过探索(虚构的)数学理论的细节,展示了 Ansible 技术的发展,使得这样的设备成为可能。

这个术语后来被这个类型的其他一些著名作者使用,用来描述能够在星际距离上传递消息的通信设备。

这个软件

Ansible 软件最初是由 Michael DeHaan 开发的,他也是《Cobbler》的作者,该软件是在 DeHaan 为红帽公司工作时开发的。

Cobbler 是一个 Linux 安装服务器,允许您在网络中快速部署服务器;它可以帮助进行 DNS、DHCP、软件包更新和分发、虚拟机部署、物理主机的电源管理,以及新部署的服务器(无论是物理的还是虚拟的)交接给配置管理系统。

DeHaan 离开了红帽公司,为 Puppet 等公司工作,这是一个很好的选择,因为 Cobbler 的许多用户使用它来交给 Puppet 服务器管理一旦它们被配置。

离开 Puppet 几年后,DeHaan 在 2012 年 2 月 23 日对 Ansible 项目进行了第一次公开提交。最初的 README 文件给出了一个非常简单的描述,为 Ansible 最终将成为的基础奠定了基础:

"Ansible 是一个超级简单的 Python API,用于通过 SSH 执行'远程任务'。与我共同编写的 Func 一样,它希望避免使用 SSH 并拥有自己的守护程序基础设施,Ansible 希望成为完全不同和更简化,但仍然能够随着时间的推移更加模块化地增长。"

自第一次提交以来,在撰写本文时,已经有 3000 名贡献者在 38 个分支和 195 个发布中进行了超过 35,000 次提交。

2013 年,该项目发展壮大,Ansible, Inc.成立,为依赖该项目管理他们的教练和服务器的 Ansible 用户提供商业支持,无论是物理的、虚拟的还是托管在公共云上的。

Ansible, Inc.的成立,获得了 600 万美元的 A 轮融资,推出了商业版的 Ansible Tower,作为一个基于 Web 的前端,最终用户可以在那里消费基于角色的 Ansible 服务。

然后,在 2015 年 10 月,红帽宣布他们将以 1.5 亿美元收购 Ansible。在宣布中,当时担任红帽管理副总裁的 Joe Fitzgerald 被引述说:

“Ansible 是 IT 自动化和 DevOps 领域的领军者,有助于红帽在创造无摩擦的 IT 目标上迈出重要一步。”

在本书的过程中,您会发现原始 README 文件中的声明和 Red Hat 在收购 Ansible 时的声明仍然成立。

在我们开始动手安装 Ansible 之前,我们应该先了解一些围绕它的核心概念。

Ansible 与其他工具

如果您比较第一个提交中的设计原则和当前版本,您会注意到虽然有一些增加和调整,但核心原则基本保持不变:

  • 无代理:一切都应该由 SSH 守护程序、Windows 机器的 WinRM 协议或 API 调用来管理——不应该依赖于自定义代理或需要在目标主机上打开或交互的其他端口

  • 最小化:您应该能够管理新的远程机器,而无需安装任何新软件,因为每台 Linux 主机通常都会在最小安装的一部分中安装至少 SSH 和 Python

  • 描述性:您应该能够用机器和人都能读懂的语言描述您的基础架构、堆栈或任务

  • 简单:设置过程和学习曲线应该简单且直观

  • 易于使用:这应该是最容易使用的 IT 自动化系统

其中一些原则使 Ansible 与其他工具有很大不同。让我们来看看 Ansible 和 Puppet、Chef 等其他工具之间最基本的区别。

声明式与命令式

当我开始使用 Ansible 时,我已经实施了 Puppet 来帮助管理我管理的机器上的堆栈。随着配置变得越来越复杂,Puppet 代码变得非常复杂。这时我开始寻找一些解决我面临问题的替代方案。

Puppet 使用自定义的声明性语言来描述配置。然后,Puppet 将这个配置打包成一个清单,然后运行在每台服务器上的代理程序应用这个清单。

使用声明性语言意味着 Puppet、Chef 和其他配置工具(如 CFEngine)都使用最终一致性的原则运行,这意味着最终,在代理程序运行几次后,您的期望配置将就位。

另一方面,Ansible 是一种命令式语言,这意味着你不仅要定义所需结果的最终状态,并让工具决定如何达到这个状态,还要定义任务执行的顺序,以达到你所定义的状态。

我倾向于使用的例子如下。我们有一个配置,需要将以下状态应用到服务器上:

  1. 创建一个名为Team的组

  2. 创建一个名为Alice的用户并将她添加到Team

  3. 创建一个名为Bob的用户并将他添加到Team

  4. 给用户Alice提升的特权

这可能看起来很简单;然而,当你使用声明性语言执行这些任务时,你可能会发现,例如,以下情况发生:

  • 运行 1:任务按以下顺序执行:2、1、3 和 4。这意味着在第一次运行时,由于名为Team的组不存在,添加用户Alice失败,这意味着Alice从未获得提升的特权。然而,组Team被添加,用户Bob被添加。

  • 运行 2:同样,任务按照以下顺序执行:2、1、3 和 4。因为在运行 1 期间创建了Team组,所以现在创建了用户Alice,并且她也被赋予了提升的特权。由于Team组和用户Bob已经存在,它们保持不变。

  • 运行 3:任务按照运行 1 和 2 的相同顺序执行;然而,由于已经达到了期望的配置,因此没有进行任何更改。

每次运行都会继续,直到配置或主机本身发生变化,例如,如果Bob真的惹恼了Alice,她使用她的提升的特权从主机中删除用户Bob。当代理下次运行时,Bob将被重新创建,因为这仍然是我们期望的配置,不管Alice认为Bob应该有什么访问权限。

如果我们使用命令式语言运行相同的任务,那么应该发生以下情况:

  • 运行 1:任务按照我们定义的顺序执行,这意味着首先创建组,然后创建两个用户,最后应用Alice的提升特权

  • 运行 2:同样,任务按照顺序执行,并进行检查以确保我们的期望配置已经就位

正如您所看到的,这两种方式都可以达到我们的最终配置,并且它们也强制执行我们的期望状态。使用声明性语言的工具可以声明依赖关系,这意味着我们可以简单地消除运行任务时遇到的问题。

然而,这个例子只有四个步骤;当您有几百个步骤在公共云平台上启动服务器,然后安装需要几个先决条件的软件时会发生什么?

这是我在开始使用 Ansible 之前发现自己处于的位置。Puppet 在强制执行我期望的最终配置方面做得很好;然而,在达到那里时,我发现自己不得不担心将大量逻辑构建到我的清单中,以达到我期望的状态。

令人讨厌的是,每次成功运行都需要大约 40 分钟才能完成。但由于我遇到了依赖问题,我不得不从头开始处理每次失败和更改,以确保我实际上是在解决问题,而不是因为事情开始变得一致而不得不重新开始——这不是在截止日期时想要的。

配置与编排

Ansible 与其他常常被比较的工具之间的另一个关键区别是,这些工具的大部分起源于被设计为部署和监控配置状态的系统。

它们通常需要在每个主机上安装代理,该代理会发现有关其安装主机的一些信息,然后回调到一个中央服务器,基本上说“嗨,我是服务器 XYZ,我可以请你给我配置吗?”然后服务器决定服务器的配置是什么样的,并将其发送给代理,然后代理应用它。通常,这种交换每 15 到 30 分钟发生一次——如果您需要强制执行服务器上的配置,这是很好的。

然而,Ansible 的设计方式使其能够充当编排工具;例如,您可以运行它在 VMware 环境中启动服务器,一旦服务器启动,它就可以连接到您新启动的机器并安装 LAMP 堆栈。然后,它永远不必再次连接到该主机,这意味着我们剩下的只是服务器、LAMP 堆栈,除了可能在文件中添加一些注释以表明 Ansible 添加了一些配置行之外,没有其他东西,但这应该是 Ansible 用于配置主机的唯一迹象。

基础设施即代码

在我们完成本章并继续安装 Ansible 之前,让我们快速讨论基础设施即代码,首先通过查看一些实际的代码来了解。以下 bash 脚本使用yum软件包管理器安装了几个 RPM 包:

#!/bin/sh
LIST_OF_APPS="dstat lsof mailx rsync tree vim-enhanced git whois iptables-services"
yum install -y $LIST_OF_APPS

以下是一个 Puppet 类,执行与之前的 bash 脚本相同的任务:

class common::apps {
  package{
    [
      'dstat',
      'lsof',
      'mailx',
      'rsync',
      'tree',
      'vim-enhanced',
      'git',
      'whois',
      'iptables-services',
    ]:
    ensure => installed,
  }
}

接下来,我们使用 SaltStack 执行相同的任务:

common.packages:
  pkg.installed:
    - pkgs:
      - dstat
      - lsof
      - mailx
      - rsync
      - tree
      - vim-enhanced
      - git
      - whois
      - iptables-services

最后,我们再次执行相同的任务,这次使用 Ansible:

- name: install packages we need
  yum:
    name: "{{ item }}"
    state: "latest"
  with_items:
    - dstat
    - lsof
    - mailx
    - rsync
    - tree
    - vim-enhanced
    - git
    - whois
    - iptables-services

即使不详细介绍,您也应该能够了解这三个示例各自在做什么。这三个示例虽然不严格属于基础设施,但都是基础设施即代码的有效示例。

在这里,您以与开发人员管理其应用程序源代码完全相同的方式管理管理基础设施的代码。您使用源代码控制,在一个中心可用的存储库中存储它,与同行合作,分支并使用拉取请求检查您的更改,并在可能的情况下编写和执行单元测试,以确保对基础设施的更改在部署到生产环境之前是成功的和无错误的。这应尽可能自动化。在提到的任务中的任何手动干预都应被视为潜在的故障点,您应该努力自动化任务。

这种基础设施管理方法有一些优势,其中之一是作为系统管理员,您正在使用与开发人员同样的流程和工具,这意味着适用于他们的任何程序也适用于您。这使工作体验更加一致,同时让您接触到以前可能没有接触或使用过的工具。

其次,更重要的是,它允许您分享您的工作。在采用这种方法之前,这种工作似乎对其他人来说是系统管理员专有的黑暗艺术。在公开进行这项工作可以让您的同行审查和评论您的配置,同时也可以让您做同样的事情来审查他们的配置。此外,您可以分享您的工作,以便其他人可以将其中的元素纳入他们自己的项目中。

摘要

在完成本章之前,我想结束一下我的个人经历。正如本章其他地方提到的,我从我的脚本和运行簿集合转移到了 Puppet,这很棒,直到我的需求不再局限于管理服务器配置和维护我管理的服务器的状态。

我需要开始在公共云中管理基础设施。当使用 Puppet 时,这个要求很快开始让我感到沮丧。当时,Puppet 对我需要用于基础设施的 API 的覆盖范围不足。我相信现在它的覆盖范围要好得多,但我也发现自己不得不在我的清单中构建太多的逻辑,涉及每个任务执行的顺序。

大约在 2014 年 12 月左右,我决定看看 Ansible。我知道这是因为我写了一篇名为First Steps With Ansible的博客文章,从那时起,我想我再也没有回头看过。自那时起,我已经向我的同事和客户介绍了 Ansible,并为 Packt 写了之前的书籍。

在本章中,我们回顾了我个人与 Ansible 以及与之相比的其他工具的历史,并讨论了这些工具之间的区别以及 Ansible 的起源。

在下一章中,我们将介绍如何安装 Ansible 并针对本地虚拟机运行我们的第一个 playbook。

进一步阅读

在本章中,我们提到了 Puppet 和 SaltStack:

  • Puppet 是一个运行服务器/代理配置的配置管理工具。它有两种版本——开源版本和由 Puppet 公司支持的企业版本。它是一个声明性系统,与 Ruby 密切相关。有关 Puppet 的更多信息,请参见puppet.com/

  • SaltStack 是另一个配置管理工具。它具有极高的可扩展性,虽然与 Ansible 共享设计方法,但它的工作方式类似于 Puppet,采用了服务器/代理的方式。您可以在saltstack.com/找到更多关于 SaltStack 的信息。

  • 我还提到了我的博客,您可以在media-glass.es/找到。

第二章:安装和运行 Ansible

现在我们对 Ansible 的背景有了一些了解,我们将开始安装它,并且一旦安装完成,我们将对运行 CentOS 7 的测试虚拟机运行我们的第一组 playbooks。

本章将涵盖以下主题:

  • 如何在 macOS 和 Linux 上安装 Ansible

  • 在 Windows 10 专业版上使用 Linux 子系统运行 Ansible

  • 启动一个测试虚拟机

  • playbooks 简介

技术要求

在本章中,我们将安装 Ansible,因此您需要一台能够运行它的机器。我将在本章的下一部分详细介绍这些要求。我们还将使用 Vagrant 在本地启动一个虚拟机。有一节介绍了安装 Vagrant 以及下载一个大小约为 400 MB 的 CentOS 7 Vagrant box。

您可以在本书附带的 GitHub 存储库中找到所有 playbooks 的完整版本github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter02

您还可以在作者的存储库中找到代码包:github.com/russmckendrick/learn-ansible-fundamentals-of-ansible-2x

安装 Ansible

让我们直接开始安装 Ansible。在本书中,我将假设您正在运行 macOS High Sierra 的 macOS 主机机器,或者安装了 Ubuntu 18.04 LTS 的 Linux 机器。虽然我们将涵盖在 Windows 10 专业版上使用 Linux 子系统运行 Ansible,但本书不支持使用 Windows 作为主机机器。

在 macOS 上安装

您可以在 macOS High Sierra 主机上以几种不同的方式安装 Ansible。我将涵盖它们两种。

由于我们正在讨论两种不同的安装方式,我建议在选择自己机器上的安装方法之前,先阅读本节以及最后的优缺点部分。

Homebrew

第一种安装方法是使用一个叫做 Homebrew 的软件包。

Homebrew 是 macOS 的软件包管理器。它可以用来安装命令行工具和桌面软件包。它自称为macOS 的缺失软件包管理器,通常是我在干净安装或获得新电脑后安装的第一个工具之一。您可以在brew.sh/找到更多关于该项目的信息。

要使用 Homebrew 安装 Ansible,您首先需要安装 Homebrew。要做到这一点,请运行以下命令:

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

在安装过程的每个步骤,安装程序都会告诉您它将要做什么,并提示您提供任何它需要的额外信息,以便完成安装。

安装完成后,或者如果您已经安装了 Homebrew,请运行以下命令来更新软件包列表,并检查您的 Homebrew 安装是否最佳:

$ brew update
$ brew doctor

根据您的安装时间或上次使用的时间,您可能会看到与以下截图不同的输出:

接下来,我们可以通过运行以下命令来检查 Homebrew 为 Ansible 提供了哪些软件包:

$ brew search ansible

如您在以下截图中看到的结果,搜索返回了几个软件包:

我们只需要 Ansible 软件包。您可以通过运行以下命令了解更多关于该软件包的信息:

$ brew info ansible

您可以在以下截图中看到命令的结果:

如您所见,该命令返回将要安装的软件包的版本信息,以及有关在哪里可以查看安装软件包的公式代码的详细信息。在我们的情况下,您可以在github.com/Homebrew/homebrew-core/blob/master/Formula/ansible.rb上查看公式的详细信息。

要使用 Homebrew 安装 Ansible,我们只需运行以下命令:

$ brew install ansible

这将下载并安装所有依赖项,然后安装 Ansible 软件包本身。根据您的计算机上已安装的依赖项数量,这可能需要几分钟。安装完成后,您应该看到类似以下屏幕截图的内容:

如您从前面的屏幕截图中所见,Homebrew 在输出中非常详细,为您提供了它正在做什么以及如何使用它安装的软件包的详细信息。

pip 方法

第二种方法pip是一种更传统的安装和配置 Python 软件包的方法。

pip是 Python 软件的软件包管理器。这是pip install packages的递归缩写。这是从Python Package Index (PyPI)安装软件包的良好前端。您可以在pypi.python.org/pypi/上找到索引。

根据您在计算机上安装了什么,您可能需要安装pip。要做到这一点,请运行以下命令:

$ easy_install pip

这将使用 macOS 默认附带的easy_install安装程序安装pip。安装完成后,您可以通过运行以下命令安装 Ansible:

$ sudo -H pip install ansible

由于我们使用了sudo命令,因此系统会提示您输入密码,就像 Homebrew 一样。此命令将下载并安装运行 Ansible 所需的所有先决条件。虽然它与 Homebrew 一样详细,但其输出包含有关其所做的工作的信息,而不是下一步该做什么的提示:

正如您所看到的,许多要求已经得到满足。

优缺点

因此,现在我们已经介绍了在 macOS 上安装 Ansible 的一些不同方法,哪种方法最好?嗯,这没有真正的答案,因为这取决于个人偏好。这两种方法都将安装最新版本的 Ansible。但是,Homebrew 往往会比当前版本晚一两周。

如果您已经使用 Homebrew 安装了许多软件包,那么您已经习惯于运行以下命令:

$ brew update
$ brew upgrade

偶尔更新已安装的软件包到最新版本。如果您已经这样做了,那么使用 Homebrew 来管理您的 Ansible 安装就是有意义的。

如果您不是 Homebrew 用户,并且想要确保立即安装最新版本,则使用pip命令安装 Ansible。升级到最新版本的 Ansible 就像运行以下命令一样简单:

$ sudo -H pip install ansible --upgrade --ignore-installed setuptools

我发现我需要使用--ignore-installed setuptools标志,因为 macOS 管理的版本与 Ansible 更新的版本存在问题和冲突。我没有发现这会引起任何问题。

如果需要的话,您可以使用 Homebrew 和pip安装旧版本的 Ansible。要使用 Homebrew 进行此操作,只需运行以下命令来删除当前版本:

$ brew uninstall ansible

然后,您可以通过运行以下命令安装软件包的早期版本:

$ brew install ansible@2.0

或者,要安装更早的版本,您可以使用以下命令:

$ brew install ansible@1.9

要了解要安装的软件包的确切版本的详细信息,您可以运行以下两个命令中的一个:

$ brew info ansible@2.0
$ brew info ansible@1.9

虽然这将安装一个早期版本,但您在安装哪个版本方面没有太多选择。如果您确实需要一个确切的版本,可以使用pip命令进行安装。例如,要安装 Ansible 2.3.1.0,您需要运行:

$ sudo -H pip install ansible==2.3.1.0 --ignore-installed setuptools

你永远不应该需要这样做。但是,我发现在某些情况下,我不得不降级来帮助调试由升级到较新版本引入的怪癖

正如前面提到的,我大部分时间都是在 macOS 机器前度过的,所以我使用哪种方法呢?主要是使用 Homebrew,因为我安装了几个其他工具。但是,如果我需要回滚到以前的版本,我会使用pip,然后问题解决后再返回到 Homebrew。

在 Linux 上安装

在 Ubuntu 18.04 上安装 Ansible 有几种不同的方法。然而,我这里只会介绍其中一种。虽然 Ubuntu 有可用的软件包可以使用apt安装,但它们往往很快就会过时,并且通常落后于当前版本。

高级打包工具APT)是 Debian 系统的软件包管理器,包括 Ubuntu。它用于管理.deb文件。

因此,我们将使用pip。首先要做的是安装pip,可以通过运行以下命令来完成:

$ sudo -H apt-get install python-pip

一旦安装了pip,安装 Ansible 的说明与在 macOS 上安装相同。只需运行以下命令:

$ sudo -H pip install ansible

这将下载并安装 Ansible 及其要求,如下截图所示:

安装完成后,您可以使用以下命令升级它:

$ sudo -H pip install ansible --upgrade

请注意,这一次我们不需要忽略任何内容,因为默认安装不应该有任何问题。此外,降级 Ansible 是相同的命令:

$ sudo -H pip install ansible==2.3.1.0 --ignore-installed setuptools

上述命令应该适用于大多数 Linux 发行版,如 CentOS、Red Hat Enterprise Linux、Debian 和 Linux Mint。

在 Windows 10 专业版上安装

我们要介绍的最后一个平台是 Windows 10 专业版;嗯,有点像。没有支持的方法可以在 Windows 机器上本地运行 Ansible 控制器。因此,我们将使用 Windows 的 Linux 子系统。

这是一个功能,在撰写本文时,它处于测试版,只适用于 Windows 10 专业版用户。要启用它,首先需要启用开发人员模式。要做到这一点,打开 Windows 10 设置应用,然后切换到开发人员模式,可以在更新和安全下找到,然后点击开发人员。

启用开发人员模式后,您将能够启用 shell。要做到这一点,打开控制面板,然后点击程序和功能,然后点击打开或关闭 Windows 功能。在功能列表中,您应该看到列出了 Windows 子系统的 Linux(Beta)。选中它旁边的框,然后点击确定。您将被提示重新启动计算机。重新启动后,点击开始菜单,然后键入bash。这将触发安装过程。您应该看到类似以下截图的内容:

下载后,它将提取并安装子系统。您将被问到一些问题。根据需要,整个过程将需要 5 到 10 分钟。安装完成后,您现在应该在 Windows 机器上运行一个完整的 Ubuntu 16.04 系统。您可以通过运行以下命令来检查:

$ cat /etc/*release

以下截图显示了上述命令的输出:

从这里,您可以运行以下命令来安装 Ansible:

$ sudo -H apt-get install python-pip
$ sudo -H pip install ansible

以下截图显示了上述命令的输出:

如您所见,一切都像在运行 Ubuntu 机器一样工作,使您能够以完全相同的方式运行和维护您的 Ansible 安装。

Windows 子系统 LinuxWSL)并不是在虚拟机上运行。它是完全嵌入到 Windows 10 专业版中的本机 Linux 体验。它针对需要作为工具链一部分运行 Linux 工具的开发人员。虽然对 Linux 命令的整体支持非常出色,但我建议阅读由微软撰写和维护的 FAQ,以了解子系统的限制和任何怪癖。FAQ 可以在docs.microsoft.com/en-us/windows/wsl/faq/找到。

正如前面提到的,虽然这是在 Windows 机器上运行 Ansible 控制节点的一种可行方式,但我们将在未来的章节中涵盖的一些其他工具可能无法在 Windows 上运行。因此,虽然您可以按照 Ubuntu 的说明进行操作,但某些部分可能无法正常工作。在可能的情况下,我会添加一条说明,说明它可能无法在基于 Windows 的系统上运行。

启动虚拟机

为了启动一个虚拟机来运行我们的第一组 Ansible 命令,我们将使用 Vagrant。

请注意,如果您正在运行 WSL,这些说明可能不起作用。

Vagrant 是由 HashiCorp 开发的虚拟机管理器。它可以管理本地和远程虚拟机,并支持诸如 VirtualBox、VMware 和 Hyper-V 之类的 hypervisors。

要在 macOS 上安装 Vagrant,我们可以使用 Homebrew 和 cask。要安装 cask,运行以下命令:

$ brew install cask

VirtualBox 是面向基于 x86 的计算机的开源 hypervisor。它目前由 Oracle 开发,并支持软件和硬件虚拟化。

默认情况下,Vagrant 使用 VirtualBox。安装了 cask 后,您可以通过运行以下命令来使用 VirtualBox 和 Vagrant:

$ brew cask install virtualbox vagrant

要在 Ubuntu 上安装,可以运行以下命令:

$ sudo apt-get install virtualbox vagrant

接下来,如果您还没有,我们需要为您的用户生成一个私钥和公钥。要做到这一点,运行以下命令,但如果您已经有一个密钥,可以跳过这部分:

$ ssh-keygen -t rsa -C "youremail@example.com"

这将创建一个密钥并将其存储在您的用户目录中的.ssh文件夹中。我们将使用此密钥注入到我们的 Vagrant 管理的 CentOS 7 虚拟机中。要启动虚拟机或 box(正如 Vagrant 所称),我们需要一个Vagrantfile。这是 Vagrant 用来创建和启动 box 的配置。

我们将使用的Vagrantfile如下所示。您还可以在本书附带的代码示例的Chapter02文件夹中找到副本,也可以在 GitHub 存储库中找到,地址为github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter02

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
BOX_NAME    = "centos/7"
BOX_IP      = "192.168.50.4"
DOMAIN      = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY  = '~/.ssh/id_rsa.pub'

Vagrant.configure(API_VERSION) do |config|
  config.vm.box = BOX_NAME
  config.vm.network "private_network", ip: BOX_IP
  config.vm.host_name = BOX_IP + '.' + DOMAIN
  config.ssh.insert_key = false
  config.ssh.private_key_path = [PRIVATE_KEY,
  "~/.vagrant.d/insecure_private_key"]
  config.vm.provision "file", source: PUBLIC_KEY, destination:
  "~/.ssh/authorized_keys"

  config.vm.provider "virtualbox" do |v|
    v.memory = "2024"
    v.cpus = "2"
  end

  config.vm.provider "vmware_fusion" do |v|
    v.vmx["memsize"] = "2024"
    v.vmx["numvcpus"] = "2"
  end

end

从上面的文件中可以看出,有一些事情正在进行。首先,在顶部部分,我们正在为以下内容定义一些变量:

  • API_VERSION:这是要使用的 Vagrant API 版本。这应该保持为2

  • BOX_NAME:这是我们想要使用的基本镜像。在我们的情况下,这是官方的 CentOS 7 Vagrant box 镜像,可以在app.vagrantup.com/centos/boxes/7找到。

  • BOX_IP:这是我们要启动的机器的私有 IP 地址。通常情况下,您不应该需要硬编码 IP 地址,但在我们的情况下,我们将需要一个可解析的主机名和一个固定的 IP 地址,以便在本章的下一节中的示例中使用。

  • DOMAIN:这是用于配置机器主机名的域名。我们使用nip.io/服务。这提供了免费的通配符 DNS 条目。这意味着我们的域名192.168.50.4.nip.io将解析为192.168.50.4

  • PRIVATE_KEY:这是您的私钥的路径。一旦启动虚拟机,将用它来 SSH 进入虚拟机。

  • PUBLIC_KEY:这是你的公钥的路径。当机器正在启动时,这将被注入到主机中,这意味着我们可以使用我们的私钥访问它。

下一节将采用前面的值并配置 Vagrant 框。然后我们定义了仅适用于Vagrantfile支持的两个提供者的设置。正如你所看到的,该文件将使用 VirtualBox 或者,如果你已经安装了它,VMware Fusion 来启动一个框。

有关 Vagrant 的 VMware 提供者插件的更多信息,请访问www.vagrantup.com/vmware/。请注意,Vagrant 的这一部分需要许可证,需要收费,并且需要在主机机器上安装 VMware Fusion 或 Workstation。

现在我们有了Vagrantfile,我们只需要运行以下命令来启动 Vagrant 框:

$ vagrant up

如果你没有传递提供者,它将默认使用 VirtualBox。如果你像我一样有 VMware 插件,你可以运行以下命令:

$ vagrant up --provider=vmware_fusion

下载适当的框文件并配置虚拟机需要几分钟的时间:

正如你从终端输出中所看到的,启动过程非常详细,并且在每个阶段都会收到有用的反馈。

一旦 Vagrant 框被启动,你可以通过运行以下命令来检查与它的连接。这将以 Vagrant 用户的身份登录到 Vagrant 框,并检查主机名和内核的详细信息:

$ vagrant ssh
$ hostname
$ uname -a
$ exit

你的终端应该看起来像这样:

正如你所看到的,因为我们已经告诉 Vagrant 在访问该框时使用哪个私钥,我们已经直接进入了该框,并且可以运行命令而没有问题。然而,在下一节中,我们将不会使用vagrant ssh命令,这就是为什么我们需要将我们的公钥注入到主机中。相反,我们将直接从我们的主机机器通过 SSH 连接到该机器。为了测试这一点,你应该能够运行以下命令:

$ ssh vagrant@192.168.50.4.nip.io

你应该被要求通过输入yes来建立主机的真实性。一旦你登录,你就可以运行以下命令:

$ hostname
$ uname -a
$ exit

你的终端应该看起来像这样:

正如你所看到的,我们已经使用 Vagrant 用户解析并连接到192.168.50.4.nip.io,并且已经使用我们的私钥进行了身份验证。在我们进入下一节并尝试第一次运行 Ansible 之前,我们应该讨论一下 Vagrant provisioners。

毫无疑问,你可能已经查看了 Vagrant 网站,网址是vagrantup.com/,并且可能已经注意到 Vagrant 实际上支持 Ansible。如果我们使用 Ansible provisioner,那么 Vagrant 将动态创建一个主机清单,并在启动过程中针对该框运行我们的 playbook。在我们看这个之前,我认为我们理解主机清单的工作方式是很重要的,所以我们将在下一章中看一下 Ansible provisioner。

然而,在那之前,让我们来看一些基本的 playbook 以及我们如何使用 Ansible 与我们的 Vagrant 框进行交互。

playbook 简介

在 IT 中,playbook 通常是发生某事时由某人运行的一组指令;有点模糊,我知道,但请跟着我。这些范围从构建和配置新的服务器实例,到如何部署代码更新以及如何在出现问题时处理问题。

在传统意义上,playbook 通常是用户遵循的一组脚本或指令,虽然它们旨在引入系统的一致性和一致性,但即使怀着最好的意图,这几乎从来没有发生过。

这就是 Ansible 的用武之地。使用 Ansible playbook,您基本上是在说应用这些更改和命令对这些主机集合,而不是让某人登录并手动开始运行操作手册。

在运行 playbook 之前,让我们讨论如何向 Ansible 提供要定位的主机列表。为此,我们将使用setup命令。这只是简单地连接到一个主机,然后尽可能多地获取有关主机的信息。

主机清单

要提供主机列表,我们需要提供一个清单列表。这是以 hosts 文件的形式。

在其最简单的形式中,我们的 hosts 文件可以包含一行:

192.168.50.4.nip.io ansible_user=vagrant

这告诉 Ansible 的是,我们要联系的主机是192.168.50.4.nip.io,并且要使用用户名vagrant。如果我们没有提供用户名,它将退回到您作为 Ansible 控制主机登录的用户,就像在我的情况下一样——用户russ,这个用户在 Vagrant 框中不存在。在存储库的Chapter02文件夹中有一个名为hosts-simple的 hosts 文件的副本,与我们用来启动 Vagrant 框的Vagrantfile一起。

运行setup命令,我们需要从存储hosts-simple的相同文件夹中运行以下命令:

$ ansible -i hosts-simple 192.168.50.4.nip.io -m setup

您应该看到一些类似以下的输出:

正如您从前面的屏幕截图中所看到的,Ansible 很快就找到了我们的 Vagrant 框的大量信息。从屏幕截图中,您可以看到机器上配置的两个 IP 地址,以及 IPv6 地址。它记录了时间和日期,如果您滚动查看自己的输出,您将看到返回了大量详细的主机信息。

回到我们运行的命令:

$ ansible -i hosts-simple 192.168.50.4.nip.io -m setup

正如您所看到的,我们正在使用-i标志加载hosts-simple文件。我们也可以使用--inventory=hosts-simple,这样就加载了我们的清单文件。命令的下一部分是要目标主机。在我们的情况下,这是192.168.50.4.nip.io。命令的最后一部分-m告诉 Ansible 使用setup模块。我们也可以使用--module-name=setup

这意味着,如果我们没有使用简写,完整的命令将是:

$ ansible --inventory=hosts-simple 192.168.50.4.nip.io --module-name=setup

如前所述,hosts-simple文件是我们可以得到的最基本的。以下是一个更常见的主机清单文件:

box ansible_host=192.168.50.4.nip.io

[boxes]
box

[boxes:vars]
ansible_connection=ssh
ansible_user=vagrant
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

在与hosts-simple文件相同的文件夹中有一个名为hosts的文件的副本。正如您所看到的,有很多事情要做,所以让我们快速地从上到下地进行一下工作。

第一行定义了我们的单个主机。与简单示例不同,我们将称呼我们的hosts box并使用ansible_host,因此我们正在向 Ansible 提供它可以 SSH 到的详细信息。这意味着我们现在可以在引用192.168.50.4.nip.io时使用名称 box。这意味着我们的命令现在看起来像这样:

$ ansible -i hosts box -m setup

文件中的下一步是创建一个名为boxes的主机组,在该组中,我们添加了我们的单个主机box。这意味着我们也可以运行:

$ ansible -i hosts boxes -m setup

如果我们的组中有不止一个主机,那么前面的命令将循环遍历所有这些主机。hosts文件的最后一部分为boxes组中的所有主机设置了一些常见的配置选项。在这种情况下,我们告诉 Ansible 该组中的所有主机都在使用 SSH,用户是vagrant,应该使用~/.ssh/id_rsa的私钥,还告诉不要在连接时检查主机密钥。

我们将在后面的章节中重新访问清单主机文件。从现在开始,我们将使用hosts文件来定位boxes组。

Playbooks

在上一节中,运行ansible命令允许我们调用单个模块。在本节中,我们将看看如何调用多个模块。以下 playbook 称为playbook.yml。它调用了我们在上一节中调用的setup模块,然后使用debug模块将消息打印到屏幕上:

---

- hosts: boxes
  gather_facts: true
  become: yes
  become_method: sudo

  tasks:
    - debug:
        msg: "I am connecting to {{ ansible_nodename }} which is running {{ ansible_distribution }} {{ ansible_distribution_version }}"

在我们开始分解配置之前,让我们看一下运行 playbook 的结果。为此,请使用以下命令:

$ ansible-playbook -i hosts playbook01.yml

这将连接到我们的 Vagrant box,在系统上收集信息,然后以消息的形式返回我们想要的信息:

您将注意到 playbook 的第一件事是它是用YAML编写的,这是一个递归缩写,代表YAML 不是标记语言。YAML 旨在成为一个可供所有编程语言使用的人类可读的数据序列化标准。它通常用于帮助定义配置。

在 YAML 中缩进非常重要,因为它用于嵌套和定义文件的区域。让我们更详细地看一下我们的 playbook:

---

尽管这些行看起来可能不多,但它们被用作文档分隔符,因为 Ansible 将所有 YAML 文件编译成单个文件;稍后会详细介绍。对于 Ansible 来说,知道一个文档何时结束,另一个文档何时开始是很重要的。

接下来,我们有 playbook 的配置。正如您所看到的,这是缩进开始发挥作用的地方:

- hosts: boxes
  gather_facts: true
  become: yes
  become_method: sudo
  tasks:

-告诉 Ansible 这是一个部分的开始。然后使用键值对。这些是:

  • hosts: 这告诉 Ansible 在 playbook 中目标主机或主机组。这必须在主机清单中定义,就像我们在上一节中介绍的那样。

  • gather_facts: 这告诉 Ansible 在首次连接到主机时运行setup模块。然后在运行的其余时间内,此信息对 playbook 可用。

  • become: 这是因为我们连接到主机时作为基本用户存在的。在这种情况下,Vagrant 用户。Ansible 可能没有足够的访问权限来执行我们告诉它的一些命令,因此这指示 Ansible 以 root 用户的身份执行其所有命令。

  • become_method: 这告诉 Ansible 如何成为 root 用户;在我们的情况下,Vagrant 配置了无密码的sudo,所以我们使用sudo

  • tasks: 这些是我们可以告诉 Ansible 在连接到目标主机时运行的任务。

从这里开始,您会注意到我们再次移动了缩进。这定义了配置的另一部分。这次是为了任务:

    - debug:
        msg: "I am connecting to {{ ansible_nodename }} which is running {{ ansible_distribution }} {{ ansible_distribution_version }}"

正如我们已经看到的,我们正在运行的唯一任务是debug模块。此模块允许我们在运行 playbook 时显示输出。

您可能已经注意到花括号之间的信息是来自setup模块的键。在这里,我们告诉 Ansible 在使用键的任何地方替换每个键的值——我们将在我们的 playbook 中经常使用这个。我们还将定义自己的键值,以便在 playbook 运行中使用。

让我们通过添加另一个任务来扩展我们的 playbook。以下内容可以在playbook02.yml中找到:

---

- hosts: boxes
  gather_facts: true
  become: yes
  become_method: sudo

  tasks:
    - debug:
        msg: "I am connecting to {{ ansible_nodename }} which is running {{ ansible_distribution }} {{ ansible_distribution_version }}"
    - yum:
        name: "*"
        state: "latest"

正如您所看到的,我们添加了第二个调用yum模块的任务。该模块旨在帮助我们与 CentOS 和其他基于 Red Hat 的操作系统使用的软件包管理器yum进行交互。我们在这里设置了两个关键值:

  • name: 这是一个通配符。它告诉 Ansible 使用所有安装的软件包,而不仅仅是单个命名的软件包。例如,我们可以在这里只使用 HTTPD 来仅针对 Apache。

  • state: 在这里,我们告诉 Ansible 确保我们在名称键中定义的软件包是latest版本。由于我们已经命名了所有安装的软件包,这将更新我们安装的所有内容。

使用以下命令运行 playbook:

$ ansible-playbook -i hosts playbook02.yml

这将给我们以下结果:

yum任务在主机box上被标记为changed。这意味着软件包已经更新。再次运行相同的命令会显示以下内容:

正如你所看到的,yum任务现在在我们的主机上显示为ok。这是因为当前没有需要更新的软件包。

在我们完成对 playbooks 的快速查看之前,让我们做一些更有趣的事情。下面的 playbook,名为playbook03.yml,将安装、配置和启动 NTP 服务到我们的 Vagrant box。它还向我们的 playbook 添加了一些新的部分,并使用了一个模板:

---

- hosts: boxes
  gather_facts: true
  become: yes
  become_method: sudo

  vars:
    ntp_servers:
      - "0.centos.pool.ntp.org"
      - "1.centos.pool.ntp.org"
      - "2.centos.pool.ntp.org"
      - "3.centos.pool.ntp.org"

  handlers:
    - name: "restart ntp"
      service:
        name: "ntpd"
        state: "restarted"

  tasks:
    - debug:
        msg: "I am connecting to {{ ansible_nodename }} which is
        running {{ ansible_distribution }}
        {{ ansible_distribution_version }}"
    - yum:
        name: "*"
        state: "latest"
    - yum:
        name: "{{ item }}"
        state: "installed"
      with_items:
        - "ntp"
        - "ntpdate"
    - template:
        src: "./ntp.conf.j2"
        dest: "/etc/ntp.conf"
      notify: "restart ntp"

在我们通过 playbook 的添加之前,让我们运行它,以了解你从 Ansible 那里得到的反馈:

$ ansible-playbook -i hosts playbook03.yml

以下截图显示了前面命令的输出:

这一次,我们有三个changed任务。再次运行 playbook 会显示以下内容:

正如预期的那样,因为我们没有改变 playbook 或 Vagrant box 上的任何东西,所以没有任何变化,Ansible 报告一切都是ok

让我们回到我们的 playbook 并讨论这些添加。你会注意到我们添加了两个新的部分,varshandlers,以及两个新的任务,第二个任务使用了yum模块,最后一个任务使用了template模块。

vars部分允许我们配置自己的键值对。在这种情况下,我们提供了一个 NTP 服务器列表,稍后将在 playbook 中使用:

  vars:
    ntp_servers:
      - "0.centos.pool.ntp.org"
      - "1.centos.pool.ntp.org"
      - "2.centos.pool.ntp.org"
      - "3.centos.pool.ntp.org"

正如你所看到的,我们实际上为相同的键提供了四个不同的值。这些将用于template任务。我们也可以这样写:

  vars:
    ntp_servers: [ "0.centos.pool.ntp.org", "1.centos.pool.ntp.org",
    "2.centos.pool.ntp.org", "3.centos.pool.ntp.org" ]

然而,这有点难以阅读。新的下一部分是handlers。处理程序是分配了一个名称的任务,并且根据任务的变化在 playbook 运行结束时调用:

  handlers:
    - name: "restart ntp"
      service:
        name: "ntpd"
        state: "restarted"

在我们的情况下,restart ntp处理程序使用service模块来重新启动ntpd。接下来,我们有两个新任务,首先是一个安装 NTP 服务和ntpdate软件包的任务,使用yum

   - yum:
      name: "{{ item }}"
      state: "installed"
     with_items:
      - "ntp"
      - "ntpdate"

因为我们正在安装两个软件包,我们需要一种方法来为yum模块提供两个不同的软件包名称,这样我们就不必为每个软件包安装编写两个不同的任务。为了实现这一点,我们使用了with_items命令,作为任务部分的一部分。请注意,这是yum模块的附加部分,并不是模块的一部分——你可以通过缩进来判断。

with_items命令允许你为任务提供一个变量或项目列表。无论{{ item }}在哪里使用,它都将被with_items值的内容替换。

playbook 的最后一个添加是以下任务:

   - template:
      src: "./ntp.conf.j2"
      dest: "/etc/ntp.conf"
     notify: "restart ntp"

这个任务使用了template模块。从我们的 Ansible 控制器读取一个模板文件,处理它并上传处理后的模板到主机。一旦上传,我们告诉 Ansible,如果配置文件有任何更改,就通知restart ntp处理程序。

在这种情况下,模板文件是与 playbooks 相同文件夹中的ntp.conf.j2文件,如src选项中定义的。这个文件看起来是这样的:

# {{ ansible_managed }}
driftfile /var/lib/ntp/drift
restrict default nomodify notrap nopeer noquery
restrict 127.0.0.1 
restrict ::1
{% for item in ntp_servers %}
server {{ item }} iburst
{% endfor %}
includefile /etc/ntp/crypto/pw
keys /etc/ntp/keys
disable monitor

文件的大部分是标准的 NTP 配置文件,还添加了一些 Ansible 部分。第一个添加是第一行:

# {{ ansible_managed }}

如果没有这一行,每次我们运行 Ansible 时,文件都会被上传,这将被视为一次变更,并且restart ntp处理程序将被调用,这意味着即使没有任何变化,NTP 也会被重新启动。

接下来的部分循环遍历了我们在 playbook 的vars部分中定义的ntp_servers值:

{% for item in ntp_servers %}
server {{ item }} iburst
{% endfor %}

对于每个值,添加一行包含服务器、然后是值,然后是iburst。您可以通过 SSH 连接到 Vagrant 机器并打开/etc/ntp.conf来查看此输出:

$ vagrant ssh
$ cat /etc/ntp.conf

以下截图显示了前述命令的输出:

从完全呈现的文件的前述截图中可以看出,我们在第一行上有注释,指出该文件由 Ansible 管理,还有四行包含要使用的 NTP 服务器的内容。

最后,您可以通过运行以下命令检查 NTP 是否按预期运行:

$ vagrant ssh
$ sudo systemctl status ntpd

以下截图显示了前述命令的输出:

从前述输出中可以看出,NTP 已加载并按预期运行。让我们通过运行以下命令删除 Vagrant 框架并启动一个新的框架:

$ vagrant destroy

然后通过运行以下两个命令之一再次启动该框:

$ vagrant up $ vagrant up --provider=vmware_fusion

一旦框架启动运行,我们可以使用以下命令运行最终的 playbook:

$ ansible-playbook -i hosts playbook03.yml

一两分钟后,您应该会收到 playbook 运行的结果。您应该会看到五个changed和六个ok

第二次运行只会显示五个ok

我们第一次运行时得到六个ok,第二次运行时得到五个ok的原因是自第一次运行以来没有发生任何变化。因此,重启 NTP 的处理程序从未被通知,因此重新启动服务的任务从未执行。

完成示例 playbook 后,您可以使用以下命令终止正在运行的框架:

$ vagrant destroy

我们将在下一章中再次使用该框。

总结

在本章中,我们通过本地安装 Ansible,然后使用 Vagrant 启动虚拟机进行了第一步。我们了解了基本的主机清单文件,并使用 Ansible 命令针对我们的虚拟机执行了单个任务。

然后,我们查看了 playbooks,首先是一个基本的 playbook,返回了有关我们目标的一些信息,然后进展到一个更新所有已安装的操作系统包并安装和配置 NTP 服务的 playbook。

在下一章中,我们将看看其他可以使用的 Ansible 命令。

问题

  1. 使用pip安装 Ansible 的命令是什么?

  2. 真或假:在使用 Homebrew 时,您可以选择要安装或回滚到哪个版本的 Ansible。

  3. 真或假:Windows 子系统运行在虚拟机中。

  4. 列出三个 Vagrant 支持的 hypervisors。

  5. 说明主机清单是什么。

  6. 真或假:YAML 文件中的缩进对于它们的执行非常重要,而不仅仅是装饰性的。

  7. 更新最终的 playbook 以安装您选择的服务,并通知处理程序以其默认配置启动服务。

进一步阅读

在这一章中,我们使用了以下 Ansible 模块,你可以在以下链接中找到每个模块的更多信息:

第三章:Ansible 命令

在继续编写和执行更高级的 playbook 之前,我们将看一下内置的 Ansible 命令。在这里,我们将介绍组成 Ansible 的一组命令的用法。在本章末尾,我们还将安装一些第三方工具,其中一个是清单图形化工具,它可以让我们可视化我们的主机,另一个允许你记录你的 playbook 运行。

本章将涵盖以下主题:

  • 内置命令:

  • ansible

  • ansible-config

  • ansible-console

  • ansible-doc

  • ansible-inventory

  • ansible-vault

  • 第三方命令:

  • ansible-inventory-grapher

  • ara

技术要求

我们将重复使用上一章中启动的 Vagrant box;如果你没有跟着做,请参考上一章关于如何安装 Ansible 和 Vagrant 的说明。本章中有一些 playbook 示例;你可以在github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter03找到完整的示例。

内置命令

当我们安装 Ansible 时,安装了几个不同的命令。这些是:

  • ansible

  • ansible-config

  • ansible-console

  • ansible-doc

  • ansible-inventory

  • ansible-vault

在后续章节中,我们将涵盖一些命令,比如ansible-galaxyansible-playbookansible-pull,所以在本章中我不会详细介绍这些命令。让我们从列表顶部开始,使用一个我们已经使用过的命令。

Ansible

现在,你可能会认为ansible是我们在整本书中将经常使用的最常见的命令,但事实并非如此。

ansible命令实际上只用于针对单个或一组主机运行临时命令。在上一章中,我们创建了一个目标为单个本地虚拟机的主机清单文件。在本章中,让我们来看看如何针对在 DigitalOcean 上运行的四个不同主机进行操作;我的主机文件如下所示:

ansible01 ansible_host=46.101.92.240
ansible02 ansible_host=159.65.63.218
ansible03 ansible_host=159.65.63.217
ansible04 ansible_host=138.68.145.116

[london]
ansible01
ansible02

[nyc]
ansible03
ansible04

[digitalocean:children]
london
nyc

[digitalocean:vars]
ansible_connection=ssh
ansible_user=root
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

正如你所看到的,我有四个主机,ansible01 > ansible04。我的前两个主机在一个名为london的组中,我的后两个主机在一个名为nyc的组中。然后我将这两个组合并创建了一个名为digitalocean的组,并且我使用这个组来应用一些基本配置,基于我启动的主机。

使用ping模块,我可以通过运行以下命令检查与主机的连接:

$ ansible -i hosts london -m ping
$ ansible -i hosts nyc -m ping

从这些结果中可以看出,所有四个主机都返回了pong

我也可以通过以下方式一次性针对所有四个主机进行操作:

$ ansible -i hosts all -m ping

现在我们可以通过 Ansible 访问我们的主机,可以使用一些临时命令来操作它们;让我们从一些基本的开始:

$ ansible -i hosts london -a "ping -c 3 google.com"

这个命令将连接到london主机并运行ping -c 3 google.com命令;这将从主机 ping google.com并返回结果:

我们也可以使用ansible命令运行单个模块;我们在上一章中使用setup模块做过这个。不过,一个更好的例子是更新所有已安装的软件包:

$ ansible -i hosts nyc -m yum -a "name=* state=latest"

在上一个示例中,我们使用yum模块来更新nyc组中所有已安装的软件包:

从屏幕截图中可以看出,运行 Ansible 时的输出非常详细,并且有反馈告诉我们在临时执行期间它做了什么。让我们再次针对我们的所有主机运行命令,但这次只针对一个单独的软件包,比如kpartx

$ ansible -i hosts all -m yum -a "name=kpartx state=latest"

终端输出可以让你更好地了解每个主机在执行命令时返回的信息:

如您所见,nyc组中的两个主机虽然返回了SUCCESS状态,但没有显示任何更改;london组中的两个主机再次显示了SUCCESS状态,但显示了更改。

那么为什么要这样做,以及我们运行的两个命令之间有什么区别呢?

首先,让我们看看两个命令:

$ ansible -i hosts london -a "ping -c 3 google.com"
$ ansible -i hosts london -m yum -a "name=* state=latest"

虽然第一个命令似乎没有运行模块,但实际上是有的。ansible命令的默认模块称为raw,它只是在每个目标主机上运行原始命令。命令的-a部分是将参数传递给模块。raw模块碰巧接受原始命令,这正是我们在第二个命令中所做的。

您可能已经注意到,语法与我们向ansible命令传递命令以及在 YAML playbook 中使用时略有不同。我们在这里所做的就是直接向模块传递键值对。

那么为什么要这样使用 Ansible 呢?嗯,它非常适合以极其受控的方式直接针对非 Ansible 管理的主机运行命令。Ansible 只是通过 SSH 连接,运行命令,并告诉您结果。只是要小心,因为很容易变得过于自信,运行以下命令:

$ ansible -i hosts all -a "reboot now"

如果 Ansible 有权限执行该命令,那么它会执行。运行上一个命令将重新启动主机清单文件中的所有服务器:

请注意,所有主机的状态都是UNREACHABLE,因为reboot命令在SUCCESS状态返回之前终止了我们的 SSH 会话。但是,您可以看到每个主机都已通过运行uptime命令进行了重启:

$ ansible -i hosts all -a "uptime" 

以下截图显示了上述命令的输出:

正如前面提到的,使用 Ansible 管理主机时要非常小心使用临时命令。

ansible-config 命令

ansible-config命令用于管理 Ansible 配置文件。老实说,Ansible 默认提供了一些相当合理的默认值,因此在这些默认值之外没有太多需要配置的地方。您可以通过运行以下命令查看当前配置:

$ ansible-config dump

如您从以下输出中所见,所有绿色文本都是默认配置,橙色文本中的任何配置都是更改后的值:

运行以下命令将列出 Ansible 中的每个配置选项的详细信息,包括选项的功能、当前状态、引入时间、类型等等:

$ ansible-config list

以下截图显示了上述命令的输出:

如果您有一个配置文件,比如在~/.ansible.cfg,那么您可以使用-c--config标志加载它:

$ ansible-config --config="~/.ansible.cfg" view

上一个命令将显示配置文件。

ansible-console 命令

Ansible 有自己的内置控制台。就个人而言,我几乎没有使用过。要启动控制台,我们只需要运行以下命令之一:

$ ansible-console -i hosts
$ ansible-console -i hosts london
$ ansible-console -i hosts nyc

前三个命令中的第一个命令针对所有主机,而接下来的两个命令只针对指定的组:

从终端输出中可以看出,您被要求输入 Ansible Vault 密码。在这里只需输入任何内容,因为我们没有任何受 Ansible Vault 保护的内容;稍后在本章中会详细介绍。连接后,您可以看到我连接到了london组,其中有两个主机。从这里,您只需输入模块名称,比如ping

或者使用raw模块,输入raw uptime

您还可以使用与运行ansible命令时相同的语法来传递键值对,例如yum name=kpartx state=latest

要离开控制台,只需输入exit即可返回到常规 shell。

ansible-doc 命令

ansible-doc命令有一个功能——为 Ansible 提供文档。它主要涵盖了核心 Ansible 模块,您可以通过运行以下命令找到完整的列表:

$ ansible-doc --list

要获取有关模块的信息,只需运行命令,然后是模块名称,例如:

$ ansible-doc raw

如您从以下输出所见,文档非常详细:

如果您只想查看如何在 playbook 中使用示例,那么可以使用以下命令:

$ ansible-doc --snippet raw

这将让您了解 playbook 应该包含的内容,如您从raw模块的以下输出所见:

ansible-doc命令的内容与可以在 Ansible 网站上找到的文档相同,但如果您想快速检查模块所需的语法,它就很有用。

ansible-inventory命令

使用ansible-inventory命令可以提供主机清单文件的详细信息。如果您想了解主机是如何分组的,这可能会很有用。例如,运行以下命令:

$ ansible-inventory -i hosts --graph

这为您提供了对主机组的逻辑概述。以下是我们在本章开头使用ansible命令的主机清单文件:

如您所见,它显示了分组,从 all 开始,然后是主机主分组,然后是子分组,最后是主机本身。

如果要查看单个主机的配置,可以使用:

$ ansible-inventory -i hosts --host=ansible01 

以下屏幕截图显示了前面命令的输出:

您可能已经注意到,它显示了主机从我们为所有 DigitalOcean 主机设置的配置中继承的配置信息。您可以通过运行以下命令查看每个主机和组的所有信息:

$ ansible-inventory -i hosts --list

如果您有一个庞大或复杂的主机清单文件,并且只想获取有关单个主机的信息,或者如果您已经接管了主机清单并想更好地了解清单的结构,那么这个命令就很有用。我们将在本章后面看一下一个第三方工具,它提供更多的显示选项。

Ansible Vault

在 Ansible 中,可以从文件中加载变量。我们将在下一章中更详细地讨论这个问题。这些文件可以包含诸如密码和 API 密钥之类的敏感信息。例如:

secret: "mypassword"
secret-api-key: "myprivateapikey" 

如您所见,我们有两个敏感的信息片段以明文形式可见。这在文件在我们本地机器上时是可以的,但是如果我们想要将文件检入源代码控制以与同事共享呢?即使存储库是私有的,我们也不应该以明文形式存储这种类型的信息。

Ansible 引入了 Vault 来帮助解决这个问题。使用 Vault,我们可以加密文件,然后在执行 Ansible 时,可以在内存中解密文件并读取内容。

要加密文件,我们需要运行以下命令,提供一个密码,以便在提示时用于解密文件:

$ ansible-vault encrypt secrets.yml

以下屏幕截图显示了前面命令的输出:

如您从输出中所见,将要求您确认密码。一旦加密,您的文件将如下所示:

$ANSIBLE_VAULT;1.1;AES256
32643164646266353962363635363831366431316264366261616238333237383063313035343062
6431336434356661646336393061626130373233373161660a363532316138633061643430353235
32343466613038663333383835633831363436343363613933626332383565663562366163393866
6532393661633762310a393935373533666230383063376639373831383965303461636433356365
64326162613637336630363733303732343065373233333263613538656361396163376165353237
30393265616630366134383830626335646338343739353638313264336638363338356136636637
623236653139386534613236623434626131

如您所见,详细信息使用文本进行编码。这确保我们的secrets.yml文件在源代码控制中仍然可以正常工作。您可以通过运行以下命令查看文件的内容:

$ ansible-vault view secrets.yml

这将要求您输入密码并将文件内容打印到屏幕上:

您可以通过运行以下命令在磁盘上解密文件:

$ ansible-vault decrypt secrets.yml

在使用此命令时,请记住不要将解密后的文件检入您的源代码控制系统!

自 Ansible 2.4 以来,现在可以加密文件中的单个变量。让我们向我们的文件添加更多变量:

username: russmckendrick
password: "mypassword"
secretapikey: "myprivateapikey" 
packages:
   - httpd
   - php
   - mariadb

如果我们不必一直查看或解密文件来检查变量名和文件的整体内容,那将是很好的。

通过运行以下命令来加密密码内容:

$ ansible-vault encrypt_string 'mypassword' --name 'password'

这将加密mypassword字符串并给它一个名为password的变量名:

然后我们可以复制并粘贴输出到我们的文件中,再次为secret-api-key重复这个过程,最终得到以下结果:

username: "russmckendrick"
password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          30646136653066633833363837613162623765386561356334386463366338313164633737386534
          6536663537383830323636653235633662353933616331660a313962626530303961383234323736
          36393433313530343266383239663738626235393164356135336564626661303564343039303436
          6662653961303764630a346639663964373137366666383630323535663536623763303339323062
          3662
secretapikey: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          63613932313933336532303237373732386337663662656337623962313638313338333763396232
          3463303765303530323133323064346539653234343933330a656537646262633765353766323737
          32303633323166643664323133303336393161663838386632346336626535303466303863346239
          3764633164613862350a363830336633356233626631636266303632663335346234373034376235
          3836
packages:
  - "httpd"
  - "php"
  - "mariadb"

如您所见,这样阅读起来更容易,而且与整个文件加密一样安全。还有一个关于 Ansible Vault 的最后一件事,那就是您也可以从文件中读取密码;例如,我一直在使用password作为密码对我的 Vault 进行编码。让我们把它放在一个文件中,然后用它来解锁我们的 Vault:

$ echo "password" > /tmp/vault-file

如您在以下的playbook.yml文件中所见,我们正在读取secrets.yml文件,然后使用debug模块输出内容:

---

- hosts: localhost

  vars_files:
    - secrets.yml

  tasks:
    - debug:
        msg: "The username is {{ username }} and password is {{ password }}, also the API key is {{ secretapikey }}"
    - debug:
        msg: "I am going to install {{ packages }}"

使用以下命令运行playbook.yml文件:

$ ansible-playbook playbook.yml

这导致终端输出中显示的错误消息:

如您所见,它抱怨在我们的文件中发现了 Vault 加密数据,但我们没有提供解锁它的密码。运行以下命令将读取/tmp/vault-file的内容并解密数据:

$ ansible-playbook --vault-id /tmp/vault-file playbook.yml

从以下的 playbook 运行中可以看到,输出现在是我们所期望的:

如果您更喜欢被提示输入密码,您也可以使用:

$ ansible-playbook --vault-id @prompt playbook.yml

您可以在附带存储库的Chapter03文件夹中找到playbook.ymlsecrets.yml的副本。

第三方命令

在结束查看 Ansible 命令之前,有几个不同的第三方命令我想要介绍,其中第一个是ansible-inventory-grapher

ansible-inventory-grapher 命令

ansible-inventory-grapher命令由 Will Thames 使用 Graphviz 库来可视化您的主机清单。我们需要做的第一件事就是安装 Graphviz。要在 macOS 上使用 Homebrew 安装它,运行以下命令:

$ brew install graphviz

或者,在 Ubuntu 上安装 Graphviz,使用:

$ sudo apt-get install graphviz

安装完成后,您可以使用pip安装ansible-inventory-grapher

$ sudo install ansible-inventory-grapher

现在我们已经安装了所有内容,可以使用本章早些时候使用的hosts文件生成图形:

ansible01 ansible_host=46.101.92.240
ansible02 ansible_host=159.65.63.218
ansible03 ansible_host=159.65.63.217
ansible04 ansible_host=138.68.145.116

[london]
ansible01
ansible02

[nyc]
ansible03
ansible04

[digitalocean:children]
london
nyc

[digitalocean:vars]
ansible_connection=ssh
ansible_user=root
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

我们可以运行以下命令来生成原始图形文件:

$ ansible-inventory-grapher -i hosts digitalocean

这将生成以下输出:

digraph "digitalocean" {
 rankdir=TB;

 "all" [shape=record label=<
 <table border="0" cellborder="0">
 <tr><td><b><font face="Times New Roman, Bold" point-size="16">
       all</font></b></td></tr>
 </table>
 >]
 "ansible01" [shape=record style=rounded label=<
 <table border="0" cellborder="0">
 <tr><td><b><font face="Times New Roman, Bold" point-size="16">
       ansible01</font></b></td></tr>
 <hr/><tr><td><font face="Times New Roman, Bold"
      point-size="14">ansible_connection<br/>ansible_host<br/>
      ansible_private_key_file<br/>ansible_user<br/>
      host_key_checking<br/></font></td></tr></table>
 >]
 "ansible02" [shape=record style=rounded label=<
 <table border="0" cellborder="0">
 <tr><td><b><font face="Times New Roman, Bold" point-size="16">
       ansible02</font></b></td></tr>
 <hr/><tr><td><font face="Times New Roman, Bold"
      point-size="14">ansible_connection<br/>ansible_host<br/>
      ansible_private_key_file<br/>ansible_user<br/>
      host_key_checking<br/></font></td></tr></table>
 >]
 "ansible03" [shape=record style=rounded label=<
 <table border="0" cellborder="0">
 <tr><td><b><font face="Times New Roman, Bold" point-size="16">
       ansible03</font></b></td></tr>
 <hr/><tr><td><font face="Times New Roman, Bold"
      point-size="14">ansible_connection<br/>ansible_host<br/>
      ansible_private_key_file<br/>ansible_user<br/>
      host_key_checking<br/></font></td></tr></table>
 >]
 "ansible04" [shape=record style=rounded label=<
 <table border="0" cellborder="0">
 <tr><td><b><font face="Times New Roman, Bold" point-size="16">
        ansible04</font></b></td></tr>
 <hr/><tr><td><font face="Times New Roman, Bold"
      point-size="14">ansible_connection<br/>ansible_host<br/>
      ansible_private_key_file<br/>ansible_user<br/>
      host_key_checking<br/></font></td></tr></table>
 >]
 "digitalocean" [shape=record label=<
 <table border="0" cellborder="0">
 <tr><td><b><font face="Times New Roman, Bold" point-size="16">
       digitalocean</font></b></td></tr>
 </table>
 >]
 "london" [shape=record label=<
 <table border="0" cellborder="0">
 <tr><td><b><font face="Times New Roman, Bold" point-size="16">
        london</font></b></td></tr>
 </table>
 >]
 "nyc" [shape=record label=<
 <table border="0" cellborder="0">
 <tr><td><b><font face="Times New Roman, Bold" point-size="16">
       nyc</font></b></td></tr>
 </table>
 >]

 "all" -> "digitalocean";
 "digitalocean" -> "london";
 "digitalocean" -> "nyc";
 "london" -> "ansible01";
 "london" -> "ansible02";
 "nyc" -> "ansible03";
 "nyc" -> "ansible04";
}

这是图形的原始输出。如您所见,它类似于 HTML。我们可以使用作为 Graphviz 的一部分的dot命令来渲染它。dot命令从图形中创建分层图。要做到这一点,运行:

$ ansible-inventory-grapher -i hosts digitalocean | dot -Tpng > hosts.png

这将生成一个名为hosts.png的 PNG 文件,其中包含您可以在这里看到的主机清单文件的可视化:

我们将在后面的章节中使用这个工具,以了解我们的清单文件在生成时是什么样子的。

Ansible Run Analysis

ARA是一个递归缩写,代表Ansible Run Analysis,记录 Ansible。这是一个用 Python 编写的工具,记录您的 playbook 运行并在直观的 web 界面中显示结果。在 macOS 上安装它,我不得不使用以下命令:

$ sudo pip install ara --ignore-installed pyparsing

要在 Ubuntu 上安装,我可以只使用这个:

$ sudo pip install ara

安装完成后,您应该能够运行以下命令来配置您的环境以记录您的 Ansible playbook 运行:

$ export ara_location=$(python -c "import os,ara; print(os.path.dirname(ara.__file__))")
$ export ANSIBLE_CALLBACK_PLUGINS=$ara_location/plugins/callbacks
$ export ANSIBLE_ACTION_PLUGINS=$ara_location/plugins/actions
$ export ANSIBLE_LIBRARY=$ara_location/plugins/modules

当您配置好环境后,可以运行 playbook。例如,让我们使用本章中 Ansible Vault 部分的 playbook 重新运行:

$ ansible-playbook --vault-id @prompt playbook.yml

一旦 playbook 被执行,运行以下命令将启动 ARA web 服务器:

$ ara-manage runserver

打开浏览器并转到前一个命令输出中提到的 URL,http://127.0.0.1:9191/,将给您显示 playbook 运行的结果:

正如您所看到的,我已经运行了四次 playbook,其中一次执行失败。单击元素将显示更多详细信息:

同样,我们将在以后的章节中更详细地使用 ARA;我们在这里只是简单介绍了基础知识。

摘要

在本章中,我们简要介绍了一些作为标准 Ansible 安装的一部分提供的支持工具,以及一些有用的第三方工具,这些工具旨在与 Ansible 一起使用。我们将在以后的章节中使用这些命令,以及我们故意忽略的一些命令。

在我们的下一章中,我们将开始编写一个更复杂的 playbook,在我们的本地 Vagrant 框中安装一个基本的 LAMP 堆栈。

问题

  1. 在本章中,我们介绍的提供有关主机清单信息的命令中,哪些是默认与 Ansible 一起提供的?

  2. 真或假:使用 Ansible Vault 加密字符串的变量文件将在低于 2.4 版本的 Ansible 中起作用。

  3. 您将运行哪个命令来获取如何调用yum模块作为任务的示例?

  4. 解释为什么您希望针对清单中的主机运行单个模块。

  5. 使用您自己的主机清单文件,生成显示内容的图表。

进一步阅读

您可以在本章末尾涵盖的两个第三方工具的项目页面中找到以下 URL:

第四章:部署 LAMP Stack

在本章中,我们将使用 Ansible 随附的各种核心模块来部署完整的 LAMP stack。我们将针对我们在 第二章 部署的 CentOS 7 Vagrant box 进行操作,安装和运行 Ansible

我们将讨论以下内容:

  • Playbook 布局—Playbook 应该如何结构化

  • Linux—准备 Linux 服务器

  • Apache—安装和配置 Apache

  • MariaDB—安装和配置 MariaDB

  • PHP—安装和配置 PHP

在我们开始编写 Playbook 之前,我们应该讨论一下我们将在本章中使用的结构,然后快速讨论一下我们需要的内容。

技术要求

我们将再次使用在之前章节中启动的 CentOS 7 Vagrant box。由于我们将在虚拟机上安装 LAMP stack 的所有元素,您的 Vagrant box 需要能够从互联网下载软件包;总共需要下载大约 500 MB 的软件包和配置。

您可以在 github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter04/lamp 找到完整的 Playbook 副本。

Playbook 结构

在之前的章节中,我们运行的 Playbook 通常尽可能基本。它们都在一个单独的文件中,伴随着一个主机清单文件。在本章中,由于我们将大大扩展 Playbook 的工作量,因此我们将使用 Ansible 建议的目录结构。

如您从以下布局中所见,有几个文件夹和文件:

让我们开始创建结构并在创建时讨论每个项目。我们需要创建的第一个文件夹是我们的顶层文件夹。这个文件夹将包含我们的 Playbook 文件夹和文件:

$ mkdir lamp
$ cd lamp

我们要创建的下一个文件夹叫做 group_vars。这将包含我们的 Playbook 中使用的变量文件。现在,我们将创建一个名为 common.yml 的单个变量文件:

$ mkdir group_vars
$ touch group_vars/common.yml

接下来,我们将创建两个文件:我们的主机清单文件,我们将命名为 production,以及我们的主 Playbook,通常称为 site.yml

$ touch production
**$ touch site.yml** 

我们要手动创建的最后一个文件夹叫做 roles。在这里,我们将使用 ansible-galaxy 命令创建一个名为 common 的角色。为此,我们使用以下命令:

$ mkdir roles $ ansible-galaxy init roles/common

正如您可能已经从本节开头的初始结构中注意到的那样,common 角色本身有几个文件和文件夹;当我们运行 ansible-galaxy init 命令时,所有这些都会为我们创建。我们将在下一节讨论这些文件的作用,届时我们将使用 common 角色来配置我们的基本 Linux 服务器。

除了默认的 Ansible 结构之外,唯一的其他文件是我们的 Vagrantfile。它包含以下内容:

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
BOX_NAME    = "centos/7"
BOX_IP      = "192.168.50.4"
DOMAIN      = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY  = '~/.ssh/id_rsa.pub'

Vagrant.configure(API_VERSION) do |config|
  config.vm.box = BOX_NAME
  config.vm.network "private_network", ip: BOX_IP
  config.vm.host_name = BOX_IP + '.' + DOMAIN
  config.ssh.insert_key = false
  config.ssh.private_key_path = [PRIVATE_KEY,
  "~/.vagrant.d/insecure_private_key"]
  config.vm.provision "file", source: PUBLIC_KEY,
  destination: "~/.ssh/authorized_keys"

  config.vm.provider "virtualbox" do |v|
    v.memory = "2024"
    v.cpus = "2"
  end

  config.vm.provider "vmware_fusion" do |v|
    v.vmx["memsize"] = "2024"
    v.vmx["numvcpus"] = "2"
  end

end

虽然我们将在本节和接下来的几节中逐个处理每个文件,但完整的 Playbook 副本可在附带的 GitHub 存储库中找到。

LAMP stack

LAMP stack 是用来描述一体化的网站和数据库服务器的术语。通常,组件包括:

  • Linux:底层操作系统;在我们的情况下,我们将使用 CentOS 7。

  • Apache:该堆栈的网站服务器元素。

  • MariaDB:该堆栈的数据库组件;通常是基于 MySQL 的。由于 CentOS 7 预装了 MariaDB,我们将使用它而不是 PHP。

  • PHP:网站服务器用于生成内容的动态语言。

还有一个常见的 LAMP stack 变体叫做 LEMP;它用 NGINX 替换 ApacheNGINX 的发音是 engine-x,因此用 E 而不是 N

我们将着手创建角色来处理这些组件;它们是:

  • common:这个角色将准备我们的 CentOS 服务器,安装我们需要的任何支持软件包和服务

  • apache:这个角色将安装 Apache web 服务器,并配置一个默认的虚拟主机

  • mariadb:这个角色不仅会安装 MariaDB,还会保护安装并创建一个默认的数据库和用户

  • php:这个角色将安装 PHP,一组常见的 PHP 模块,还有 Composer,这是一个用于 PHP 的包管理器

让我们开始编写 common 角色,准备好基础知识。

常见

在本章的前一部分中,我们使用ansible-galaxy init命令创建了common角色。这将创建几个文件夹和文件;在我们开始编辑它们之前,让我们快速讨论一下它们各自的用途:

我们只关心顶层;main.yml文件只是每个角色部分调用的默认 YAML 文件:

  • README.md:这是用于在像 GitHub 这样的服务中检入角色时创建有关角色的任何文档的文件。每当有人浏览 common 文件夹时,该文件将与文件夹列表一起显示。

  • default:这是角色的默认变量存储位置。这些变量可以被vars文件夹中具有相同名称的任何变量覆盖。

  • files:这个文件夹包含我们可能希望使用copy模块复制到目标主机的任何静态文件。

  • handlers:处理程序是在执行 playbook 后执行的任务;通常,handlers用于在配置文件更改时重新启动服务。

  • meta:这包含有关角色的信息,如果角色要发布到 Ansible Galaxy,则会使用。

  • tasks:这是大部分工作发生的地方。

  • templates:这个文件夹包含template模块使用的 Jinja2 模板。

  • tests:用于存储模块的任何测试。

  • vars:您可以使用此处定义的变量覆盖default文件夹中定义的任何变量;此处定义的变量也可以被从group_vars文件夹和 playbook 的顶层加载的任何变量覆盖。

让我们开始添加一些任务。

更新软件包

首先,让我们通过在roles/common/tasks/main.yml文件的开头添加以下内容来更新我们的服务器:

- name: update all of the installed packages
  yum:
    name: "*"
    state: "latest"
    update_cache: "yes"

您会注意到,与我们上次运行yum更新所有已安装软件包时有所不同,我们现在正在使用name键开始任务,这将在 playbook 运行时打印出我们分配给名称键的值的内容,这将让我们更好地了解 playbook 运行过程中发生了什么。

安装常用软件包

现在我们已经更新了安装的软件包,让我们安装我们想要在所有我们将启动的 Linux 服务器上安装的软件包:

- name: install the common packages
  yum:
    name: "{{ item }}"
    state: "installed"
  with_items: "{{ common_packages }}"

正如你所看到的,我们再次使用yum模块,并为任务添加了一个描述性名称。我们不是在任务中提供软件包的列表,而是使用一个名为common_packages的变量,在roles/common/defaults/main.yml文件中定义如下:

common_packages:
  - "ntp"
  - "ntpdate"
  - "vim-enhanced"
  - "git"
  - "unzip"
  - "policycoreutils-python"
  - "epel-release"
  - "https://centos7.iuscommunity.org/ius-release.rpm"

正如你所看到的,我们正在安装ntpntpdate;我们很快将配置ntp。接下来,我们安装vim-enhancedgit,因为它们在服务器上安装后总是有用。然后,我们安装policycoreutils-python包,稍后会详细介绍,最后安装并启用两个额外的yum仓库,EPEL 和 IUS。

企业 Linux 的额外软件包EPEL)是一个特别感兴趣的小组,他们维护了一系列不属于 Red Hat Enterprise Linux 核心的软件包。EPEL 软件包通常基于它们的 Fedora 对应软件包,并且已经打包,因此它们永远不会与核心 Enterprise Linux 发行版中的软件包发生冲突或替换。

CentOS 7 附带一个名为epel-release的软件包,它启用了 EPEL 存储库。但是,IUS 没有发布包,因此在这里,我们不是使用核心 CentOS 存储库的软件包,而是提供了启用了 IUS 存储库的 RPM 文件的完整 URL,该文件适用于 CentOS 7。

IUS 社区项目是为红帽企业 Linux 和兼容操作系统(如 CentOS)提供 RPM 的集合,旨在提供与上游稳定一致的软件包,因此IUS。他们提供 Apache、PHP 和 MariaDB 的软件包,这些都是最新版本。IUS 提供的软件包遵循SafeRepo 计划中制定的规则,这意味着它们是可信的。

配置 NTP

接下来,我们从templates文件夹中复制ntp.conf文件,添加 NTP 服务器列表,然后告诉 Ansible 每当配置文件更改时重新启动 NTP:

- name: copy the ntp.conf to /etc/ntp.conf
  template:
    src: "ntp.conf.j2"
    dest: "/etc/ntp.conf"
  notify: "restart ntp"

模板文件可以在roles/common/templates/ntp.conf.j2中找到:

# {{ ansible_managed }}
driftfile /var/lib/ntp/drift
restrict default nomodify notrap nopeer noquery
restrict 127.0.0.1 
restrict ::1
{% for item in ntp_servers %}
server {{ item }} iburst
{% endfor %}
includefile /etc/ntp/crypto/pw
keys /etc/ntp/keys
disable monitor

如您所见,我们正在使用ntp_servers变量;这个变量存储在roles/common/defaults/main.yml文件中:

ntp_servers:
  - "0.centos.pool.ntp.org"
  - "1.centos.pool.ntp.org"
  - "2.centos.pool.ntp.org"
  - "3.centos.pool.ntp.org"

最后,以下任务已添加到roles/common/handlers/main.yml中:

- name: "restart ntp"
  service:
    name: "ntpd"
    state: "restarted"

虽然我们在这里通知了处理程序,但 NTP 将不会在 playbook 运行的最后重新启动,以及我们通知的任何其他任务。

创建用户

常见角色的最后一部分是添加一个名为lamp的用户,并将我们的公钥添加到该用户。在我们查看任务之前,让我们先看一下我们将要使用的变量,这些变量在roles/common/defaults/main.yml中定义:

users:
  - { name: "lamp", group: "lamp", state: "present", key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" }

如您所见,我们提供了三条信息:

  • name:这是我们要创建的用户的名称

  • group:这是我们要将用户添加到的组

  • state:如果我们希望用户存在或不存在

  • key:在这里,我们使用 Ansible 查找任务来读取~/.ssh/id_rsa.pub文件的内容,并将其用作值

用于创建用户的roles/common/tasks/main.yml文件中的任务分为三部分;第一部分使用group模块创建组:

- name: add group for our users
  group:
    name: "{{ item.group }}"
    state: "{{ item.state }}"
  with_items: "{{ users }}"

如您所见,我们使用with_items加载users变量,因为该变量包含三个不同的项目,这里只使用了两个。我们可以只命名它们,所以这里我们使用item.groupitem.state

任务的第二部分使用user模块创建用户,如您所见:

- name: add users to our group
  user: 
    name: "{{ item.name }}"
    group: "{{ item.group }}"
    comment: "{{ item.name }}"
    state: "{{ item.state }}"
  with_items: "{{ users }}"

任务的最后一部分使用authorized_key模块将用户的公钥添加到授权密钥文件中:

- name: add keys to our users
  authorized_key:
    user: "{{ item.name }}"
    key: "{{ item.key }}"
  with_items: "{{ users }}"

如您所见,这次我们使用了item.nameitem.key变量。该模块在用户的主目录中创建一个名为.ssh/authorized_keys的文件,该目录由item.name定义,然后将item.key的内容放在其中,使私钥的持有者可以访问我们刚刚创建的用户。

运行角色

首先,让我们通过运行以下命令之一来启动 CentOS 7 Vagrant box:

$ vagrant up
$ vagrant up --provider=vmware_fusion

现在我们有了服务器,我们需要更新主机清单;在production文件中输入以下内容:

box ansible_host=192.168.50.4.nip.io

[boxes]
box

[boxes:vars]
ansible_connection=ssh
ansible_user=vagrant
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

最后,我们需要一个执行我们角色的东西。将以下内容添加到site.yml文件中:

---

- hosts: boxes
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/common

现在我们的 playbook 文件准备好了,我们可以通过运行以下命令来针对我们的 Vagrant box 运行它:

$ ansible-playbook -i production site.yml

几分钟后,您应该看到类似以下输出:

PLAY [boxes] ***************************************************************************************

TASK [Gathering Facts] *****************************************************************************
ok: [box]

TASK [roles/common : update all of the installed packages] *****************************************
changed: [box]

TASK [roles/common : install the common packages] **************************************************
changed: [box] => (item=[u'ntp', u'ntpdate', u'vim-enhanced', u'git', u'unzip', u'policycoreutils-python', u'epel-release', u'https://centos7.iuscommunity.org/ius-release.rpm'])

TASK [roles/common : copy the ntp.conf to /etc/ntp.conf] *******************************************
changed: [box]

TASK [roles/common : add group for our users] ******************************************************
changed: [box] => (item={u'state': u'present', u'group': u'lamp', u'name': u'lamp', u'key': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russmckendrick@me.com'})

TASK [roles/common : add users to our group] *******************************************************
changed: [box] => (item={u'state': u'present', u'group': u'lamp', u'name': u'lamp', u'key': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russmckendrick@me.com'})

TASK [roles/common : add keys to our users] ********************************************************
changed: [box] => (item={u'state': u'present', u'group': u'lamp', u'name': u'lamp', u'key': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russmckendrick@me.com'})

RUNNING HANDLER [roles/common : restart ntp] *******************************************************
changed: [box]

PLAY RECAP *****************************************************************************************
box : ok=8 changed=7 unreachable=0 failed=0

如您所见,一切都已按预期安装和配置。重新运行 playbook 会得到以下结果:

PLAY [boxes] ***************************************************************************************

TASK [Gathering Facts] *****************************************************************************
ok: [box]

TASK [roles/common : update all of the installed packages] *****************************************
ok: [box]

TASK [roles/common : install the common packages] **************************************************
ok: [box] => (item=[u'ntp', u'ntpdate', u'vim-enhanced', u'git', u'unzip', u'policycoreutils-python', u'epel-release', u'https://centos7.iuscommunity.org/ius-release.rpm'])

TASK [roles/common : copy the ntp.conf to /etc/ntp.conf] *******************************************
ok: [box]

TASK [roles/common : add group for our users] ******************************************************
ok: [box] => (item={u'state': u'present', u'group': u'lamp', u'name': u'lamp', u'key': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russmckendrick@me.com'})

TASK [roles/common : add users to our group] *******************************************************
ok: [box] => (item={u'state': u'present', u'group': u'lamp', u'name': u'lamp', u'key': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russmckendrick@me.com'})

TASK [roles/common : add keys to our users] ********************************************************
ok: [box] => (item={u'state': u'present', u'group': u'lamp', u'name': u'lamp', u'key': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russmckendrick@me.com'})

PLAY RECAP *****************************************************************************************
box : ok=7    changed=0    unreachable=0    failed=0

如您所见,我们跳过了重新启动 NTP 的任务,也没有其他要安装的附加软件包或更新,也没有对我们创建的用户或组的任何更改。现在我们已经更新和安装了基本软件包,并配置了基本操作系统,我们准备安装 Apache。

Apache

目前,我们没有 Apache 的角色,所以让我们使用以下命令创建一个:

$ ansible-galaxy init roles/apache

与以前一样,这将为我们的 Apache 角色创建基本的框架。

安装 Apache

我们要添加的第一个任务是安装基本的 Apache 软件包。在roles/apache/tasks/main.yml中,添加以下内容:

- name: install the apache packages
  yum:
    name: "{{ item }}"
    state: "installed"
  with_items: "{{ apache_packages }}"

正如你可能已经猜到的那样,apache_packages的默认值可以在roles/apache/defaults/main.yml中找到:

apache_packages:
  - "httpd24u"
  - "httpd24u-filesystem"
  - "httpd24u-tools"
  - "httpd24u-mod_ssl"
  - "openssl"
  - "openssl-libs"

这将从 IUS 安装最新的 Apache 2.4 软件包,以及我们需要的一些支持工具。安装完成后,我们现在需要配置 Apache。

配置 Apache

也许你已经想知道为什么我们在上一节创建了一个名为lamp的用户;我们将为这个用户托管我们的网站。准备好用户托管我们网站的第一步是将用户添加到apache_group中。为此,我们需要运行以下任务:

- name: Add user to apache group
  user:
    name: "{{ item.name }}"
    groups: "{{ apache_group }}"
    append: yes
  with_items: "{{ users }}"

这里有两件事需要指出。第一是我们正在使用上一个角色中的users变量,在 playbook 运行中仍然可以使用,第二是我们在roles/apache/defaults/main.yml中添加了一个名为apache_group的变量:

apache_group: "apache"

既然我们的用户在apache_group中,让我们创建将成为我们网站文档根目录的内容:

- name: create the document root for our website
  file:
    dest: "{{ document_root }}"
    state: "directory"
    mode: "0755"
    owner: "{{ users.0.name }}"
    group: "{{ apache_group }}"

正如你所看到的,这使用了一些新变量,以及访问旧变量的新方法。让我们先解决users.0.name,因为我们已经将用户定义为列表。在 playbook 运行期间可能会添加多个用户,因为我们只想创建一个文档根目录并将其分配给一个虚拟主机,我们使用列表中的第一个用户,该用户在user变量下注册,这就是0的用处。

document_root变量也是使用这个原则构建的;这是roles/apache/defaults/main.yml文件中的两个变量,将帮助构成完整的文档根目录:

web_root: "web"
document_root: "/home/{{ users.0.name }}/{{ web_root }}"

这将使我们的文档根目录在 Vagrant box 上的路径为/home/lamp/web/,假设我们没有覆盖主要 playbook 中的任何变量名。

我们还需要更改 lamp 用户的主目录权限,以允许我们执行脚本;为此,调用以下任务:

- name: set the permissions on the user folder
  file:
    dest: "/home/{{ users.0.name }}/"
    state: "directory"
    mode: "0755"
    owner: "{{ users.0.name }}"

接下来,我们需要放置我们的 Apache 虚拟主机;这将在我们在浏览器中输入主机名时提供我们的网页。为此,我们将使用存储在roles/apache/templates/vhost.conf.j2中的模板文件,该文件使用我们已经定义的变量以及另外两个变量:

# {{ ansible_managed }}
<VirtualHost *:80>
  ServerName {{ ansible_nodename }}
  DocumentRoot {{ document_root }}
  DirectoryIndex {{ index_file }}
  <Directory {{ document_root }}>
    AllowOverride All
    Require all granted
  </Directory>
</VirtualHost>

roles/apache/defaults/main.yml中的index_file变量如下所示:

index_file: index.html

还有ansible_nodename变量;这是从主机机器收集的变量之一,当setup模块首次运行时。部署模板的任务如下:

- name: copy the vhost.conf to /etc/httpd/conf.d/
  template:
    src: "vhost.conf.j2"
    dest: "/etc/httpd/conf.d/vhost.conf"
  notify: "restart httpd"

重启 Apache 的任务可以在roles/apache/handlers/main.yml中找到,如下所示:

- name: "restart httpd"
  service:
    name: "httpd"
    state: "restarted"

既然我们已经安装和配置了 Apache,我们需要允许 Apache 使用存储在/home/中的网站根目录。为此,我们需要调整 SELinux 权限。

配置 SELinux

在上一节安装的软件包之一是policycoreutils-python。这允许我们使用 Python 配置 SELinux,因此也可以使用 Ansible。

安全增强型 LinuxSELinux)是由红帽和美国国家安全局开发的。它提供了在内核级别支持访问控制安全策略的机制。这些包括美国国防部使用的强制访问控制。

默认情况下,我们使用的 Vagrant box 启用了 SELinux。我们可以不简单地停止 SELinux,而是允许 Apache 在其默认的/var/www/之外运行。为此,我们需要将以下内容添加到我们的角色中:

- name: set the selinux allowing httpd_t to be permissive
  selinux_permissive:
    name: httpd_t
    permissive: true

现在 Apache 被允许从我们的用户目录中提供内容,我们可以添加一个index.html文件,这样我们就有了除了默认的 Apache 页面之外的东西来提供。

复制 HTML 文件

最后一个任务是将index.html文件复制到我们的网站根目录,这样我们就有了新安装的 Apache 服务器可以提供的内容。执行此任务使用template模块:

- name: copy the test HTML page to the document root
  template:
    src: "index.html.j2"
    dest: "{{ document_root }}/index.html"
    mode: "0644"
    owner: "{{ users.0.name }}"
    group: "{{ apache_group }}"
  when: html_deploy == true

如你所见,我们正在加载一个名为 index.html.j2 的模板,其中包含以下内容:

<!--{{ ansible_managed }}-->
<!doctype html>
<title>{{ html_heading }}</title>
<style>
  body { text-align: center; padding: 150px; }
  h1 { font-size: 50px; }
  body { font: 20px Helvetica, sans-serif; color: #333; }
  article { display: block; text-align: left; width: 650px;
   margin: 0 auto; }
</style>
<article>
    <h1>{{ html_heading }}</h1>
    <div>
        <p>{{ html_body }}</p>
    </div>
</article>

我们在模板中使用了两个变量;这两个变量可以在 roles/apache/defaults/main.yml 文件中找到,还有一个变量:

html_deploy: true
html_heading: "Success !!!"
html_body: |
  This HTML page has been deployed using Ansible to
   <b>{{ ansible_nodename }}</b>.<br>
  The user is <b>{{ users.0.name }}</b> who is in the
   <b>{{ apache_group }}</b> group.<br>
  The weboot is <b>{{ document_root }}</b>, the default index file is
   <b>{{ index_file }}</b>.<br>

作为任务的一部分,我们有以下一行:

when: html_deploy == true

这意味着只有当 html_deploy 等于 true 时,任务才会被执行。如果是其他任何值,那么任务将被跳过。我们将在本章后面讨论这一点,但现在,我们希望页面被部署,所以我们将保持在 apache/defaults/main.yml 文件中定义的默认值。

在运行角色之前要指出的最后一件事是 html_body 变量。如你所见,变量的内容分布在三行上。在变量名后使用 | 字符来实现这一点;这有助于使你的变量文件可读,同时也允许你开始将诸如密钥或证书之类的项目作为变量进行分发,同时还允许你使用 vault 进行编码。

运行角色

现在安装和配置 Apache 的角色已经完成,我们可以将其添加到我们的 playbook 中:

---

- hosts: boxes
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/common
    - roles/apache

在上一节的 playbook 之后,我们可以简单地重新运行以下命令:

$ ansible-playbook -i production site.yml

这将在执行 apache 角色之前通过通用角色工作。我在这里截断了 playbook 运行中通用角色的输出:

PLAY [boxes] ***************************************************************************************

TASK [Gathering Facts] *****************************************************************************
ok: [box]

TASK [roles/common : update all of the installed packages] *****************************************
ok: [box]

TASK [roles/common : install the common packages] **************************************************
ok: [box]

TASK [roles/common : copy the ntp.conf to /etc/ntp.conf] *******************************************
ok: [box]

TASK [roles/common : add group for our users] ******************************************************
ok: [box]
TASK [roles/common : add users to our group] *******************************************************
ok: [box]

TASK [roles/common : add keys to our users] ********************************************************
ok: [box]

TASK [roles/apache : install the apache packages] **************************************************
changed: [box] => (item=[u'httpd24u', u'httpd24u-filesystem', u'httpd24u-tools', u'httpd24u-mod_ssl', u'openssl', u'openssl-libs'])

TASK [roles/apache : Add user to apache group] *****************************************************
changed: [box] => (item={u'state': u'present', u'group': u'lamp', u'name': u'lamp', u'key': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russmckendrick@me.com'})

TASK [roles/apache : create the document root for our website] *************************************
changed: [box]

TASK [roles/apache : set the permissions on the user folder] ***************************************
changed: [box]

TASK [roles/apache : copy the vhost.conf to /etc/httpd/conf.d/] ************************************
changed: [box]

TASK [roles/apache : set the selinux allowing httpd_t to be permissive] ****************************
changed: [box]

TASK [roles/apache : copy the test HTML page to the document root] *********************************
changed: [box]

RUNNING HANDLER [roles/apache : restart httpd] *****************************************************
changed: [box]

PLAY RECAP *****************************************************************************************
box : ok=15 changed=8 unreachable=0 failed=0

在浏览器中打开 http://192.168.50.4.nip.io/ 应该会给我们一个看起来像以下截图的页面:

如你所见,模板已经捕捉到我们定义的所有变量;页面的源代码如下:

<!--Ansible managed-->
<!doctype html>
<title>Success !!!</title>
<style>
  body { text-align: center; padding: 150px; }
  h1 { font-size: 50px; }
  body { font: 20px Helvetica, sans-serif; color: #333; }
  article { display: block; text-align: left; width: 650px;
    margin: 0 auto; }
</style>
<article>
  <h1>Success !!!</h1>
  <div>
    <p>This HTML page has been deployed using Ansible to
    <b>192.168.50.4.nip.io</b>.<br>
    The user is <b>lamp</b> who is in the <b>apache</b> group.<br>
    The weboot is <b>/home/lamp/web</b>, the default index file is
    <b>index.html</b>.<br></p>
  </div>
</article>

如果我们重新运行 playbook,我们应该会看到以下结果:

PLAY RECAP *****************************************************************************************
box : ok=14 changed=0 unreachable=0 failed=0

如你所见,有 14 个任务是 ok,没有发生任何 changed

MariaDB

接下来,我们将安装和配置 MariaDB,我们 LAMP 堆栈的数据库组件。

MariaDB 是 MySQL 的一个分支。它的开发由一些 MySQL 的原始开发人员领导;他们在 Oracle 收购 MySQL 后对 MySQL 的许可证引发了一些担忧后创建了这个分支。

第一步是创建我们将需要的角色文件;同样,我们将使用 ansible-galaxy init 命令来引导角色文件:

$ ansible-galaxy init roles/mariadb

安装 MariaDB

由于我们在 playbook 中使用了 IUS 仓库来安装其他软件包,所以从那里安装最新版本的 MariaDB 是有道理的。然而,我们首先需要解决一个冲突。

作为基本的 Vagrant 盒子安装,邮件服务器 Postfix 被安装了。Postfix 需要 mariadb-libs 软件包作为依赖,但安装这个软件包会导致与我们想要安装的后续版本软件包发生冲突。解决这个问题的方法是移除 mariadb-libs 软件包,然后安装我们需要的软件包,以及在卸载 mariadb-libs 时被移除的 Postfix。

角色中的第一个任务,我们需要添加到 roles/mariadb/tasks/mail.yml,看起来像这样:

- name: remove the packages so that they can be replaced
  yum:
    name: "{{ item }}"
    state: "absent"
  with_items: "{{ mariadb_packages_remove }}"

正如你可能已经怀疑的那样,mariadb_packages_removeroles/mariadb/defaults/main.yml 文件中被定义:

mariadb_packages_remove:
  - "mariadb-libs.x86_64"

如你所见,我们正在使用完整的软件包名称。我们需要这样做,因为如果我们简单地使用 mariadb-libs,那么新安装的软件包将在每次 playbook 运行时被移除。这是不好的,因为这个任务也会卸载我们接下来要安装的所有 MariaDB 软件包,如果我们有一个正在运行的数据库,那将是一场灾难!

为了安装 MariaDB 的后续版本,我们需要添加以下任务:

- name: install the mariadb packages
  yum:
    name: "{{ item }}"
    state: "installed"
  with_items: "{{ mariadb_packages }}"

mariadb_packages 变量,同样可以在 defaults 文件夹中找到,看起来像这样:

mariadb_packages:
  - "mariadb101u"
  - "mariadb101u-server"
  - "mariadb101u-config"
  - "mariadb101u-common"
  - "mariadb101u-libs"
  - "MySQL-python"
  - "postfix"

我们正在安装 MariaDB 的软件包,以及上一个任务中被移除的 Postfix。我们还安装了 MySQL-python 软件包,这将允许 Ansible 与我们的 MariaDB 安装进行交互。

默认情况下,MariaDB 在安装过程中不会启动。通常,我们会使用处理程序在 playbook 运行的最后启动服务,正如我们从前面的部分学到的,处理程序在 playbook 执行的最后运行。如果我们不需要与 MariaDB 服务交互来配置它,这不会是一个问题。为了解决这个问题,我们需要将以下任务添加到我们的角色中:

- name: start mariadb
  service:
    name: "mariadb"
    state: "started"
    enabled: "yes"

这确保了 MariaDB 正在运行,并配置了服务以在启动时启动。

配置 MariaDB

现在 MariaDB 已安装并运行,我们开始配置它。我们默认安装的 MariaDB 没有定义根密码,所以这应该是我们设置的第一件事。我们可以使用mysql_user模块来做到这一点:

- name: change mysql root password
  mysql_user:
    name: "{{ mariadb_root_username }}" 
    host: "{{ item }}" 
    password: "{{ mariadb_root_password }}"
    check_implicit_admin: "yes"
    priv: "*.*:ALL,GRANT"
  with_items: "{{ mariadb_hosts }}"

正如你所看到的,我们使用了一些不同的变量;这些在roles/mariadb/defaults/main.yml中定义为:

mariadb_root_username: "root"
mariadb_root_password: "Pa55W0rd123"
mariadb_hosts:
  - "127.0.0.1"
  - "::1"
  - "{{ ansible_nodename }}"
  - "%"
  - "localhost"

mariadb_hosts中主机的顺序很重要;如果localhost不是最后一个更改的主机,那么 Ansible 将会给出一个关于无法连接到 MariaDB 的错误消息。这是因为我们利用了 MariaDB 没有默认 root 密码的事实来实际设置 root 密码。

现在,一旦我们配置了 root 用户的密码,我们仍然希望能够连接到 MySQL。我喜欢在根用户文件夹下设置一个~/.my.cnf文件。这可以在 Ansible 中完成如下:

- name: set up .my.cnf file
  template:
    src: "my.cnf.j2"
    dest: "~/.my.cnf"

模板文件可以在lamp/roles/mariadb/templates/my.cnf.j2中找到;它包含以下内容:

# {{ ansible_managed }}
[client]
password='{{ mariadb_root_password }}'

一旦放置好,这意味着系统根用户——不要与我们刚刚在 MariaDB 中设置的 root 用户混淆——将直接访问 MariaDB 而无需提供密码。接下来,我们可以删除默认创建的匿名用户。同样,我们将使用mysql_user模块来完成这个操作:

- name: delete anonymous MySQL user
  mysql_user:
    user: ""
    host: "{{ item }}"
    state: "absent"
  with_items: "{{ mariadb_hosts }}"

最后,还创建了一个test数据库。由于我们将创建自己的数据库,让我们也将其删除,这次使用mysql_db模块:

- name: remove the MySQL test database
  mysql_db:
    db: "test" 
    state: "absent"

这些配置任务相当于运行mysql_secure_installation命令。

导入示例数据库

现在我们的 MariaDB 安装已经完成,我们应该对其进行一些操作。GitHub 上有一些示例数据库可用。让我们来看看导入 datacharmer 提供的 employee 数据库。我们将使用一个稍微修改过的 SQL 转储文件版本,但稍后在本节中会详细介绍。

我们将在 playbook 的这一部分使用嵌套变量;这些可以在mariadb/defaults/main.yml中找到:

mariadb_sample_database:
  create_database: true
  source_url: "https://github.com/russmckendrick/test_db/archive/master.zip"
  path: "/tmp/test_db-master"
  db_name: "employees"
  db_user: "employees"
  db_password: "employees"
  dump_files:
    - "employees.sql"
    - "load_departments.dump"
    - "load_employees.dump"
    - "load_dept_emp.dump"
    - "load_dept_manager.dump"
    - "load_titles.dump"
    - "load_salaries1.dump"
    - "load_salaries2.dump"
    - "load_salaries3.dump"
    - "show_elapsed.sql"

当我们调用这些变量时,它们需要以mariadb_sample_database为前缀。例如,每当我们需要使用db_name变量时,我们将需要使用mariadb_sample_database.db_name。就像我们在上一节中复制 HTML 文件时一样,我们将为每个任务添加一个使用when的条件,这意味着如果需要,它们可以被跳过。

我们需要做的第一件事是从 GitHub 下载转储文件的副本并解压缩它们。为此,我们将使用unarchive模块:

- name: download and unarchive the sample database data
  unarchive:
    src: "{{ mariadb_sample_database.source_url }}"
    dest: "/tmp"
    remote_src: "yes"
  when: mariadb_sample_database.create_database == true

我们正在从远程位置获取文件,即 URLmariadb_sample_database.source_url,并在/tmp中解压缩它。由于我们将remote_src设置为yes,Ansible 知道它必须从远程源下载文件。如果我们没有提供完整的 URL,它将尝试从控制主机复制文件。

接下来的两个任务使用mysql_dbmysql_user模块来创建数据库和一个可以访问它的用户:

- name: create the sample database
  mysql_db:
    db: "{{ mariadb_sample_database.db_name }}" 
    state: "present"
  when: mariadb_sample_database.create_database == true

- name: create the user for the sample database
  mysql_user:
    name: "{{ mariadb_sample_database.db_user }}"
    password: "{{ mariadb_sample_database.db_password }}"
    priv: "{{ mariadb_sample_database.db_name }}.*:ALL"
    state: "present"
  with_items: "{{ mariadb_hosts }}"
  when: mariadb_sample_database.create_database == true

playbook 的最后部分将 MySQL 转储文件导入数据库;然而,在导入文件之前,我们应该首先检查转储文件是否已经被导入。如果我们每次运行 playbook 时不执行此检查,转储文件将被导入。为了检查数据是否已经被导入,我们将使用stat模块;这将检查文件的存在并收集有关它的信息。

如果我们已经导入了数据,/var/lib/mysql/employees文件夹中将会有一个名为employees.frm的文件,因此让我们检查一下:

- name: check to see if we need to import the sample database dumps
  stat:
    path: "/var/lib/mysql/{{ mariadb_sample_database.db_name }}/{{ mariadb_sample_database.db_name }}.frm"
  register: db_imported
  when: mariadb_sample_database.create_database == true

现在我们知道是否需要导入数据库转储,我们可以继续进行最后的任务,如果满足以下条件,将导入mariadb_sample_database.dump_files中列出的数据库转储:

  • 变量db_imported是否已定义?如果没有,则我们将跳过导入示例数据库,并且应该跳过此任务。

  • db_imported.stat.exists是否等于false?如果是,则文件不存在,我们应该导入数据。

该任务本身使用mysql_db模块来导入数据:

- name: import the sample database
  mysql_db:
    name: "{{ mariadb_sample_database.db_name }}"
    state: "import"
    target: "{{ mariadb_sample_database.path }}/{{ item }}"
  with_items: "{{ mariadb_sample_database.dump_files }}"
  when: db_imported is defined and db_imported.stat.exists == false

这完成了将示例数据库导入到我们的 MariaDB 安装中;现在让我们运行 playbook 并调用角色。

运行角色

现在我们已经编写了我们的角色,我们可以将其添加到我们的 playbook 中:

---

- hosts: boxes
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/common
    - roles/apache
    - roles/mariadb

同样,我们可以使用以下命令重新运行 playbook:

$ ansible-playbook -i production site.yml

在继续进行 MariaDB 等效之前,这将通过常见和 Apache 角色进行操作。这个 playbook 输出开始于 MariaDB 角色开始之前:

TASK [roles/apache : set the selinux allowing httpd_t to be permissive] ***************************************************************************************************
ok: [box]

TASK [roles/apache : copy the test HTML page to the document root] ********************************************************************************************************
ok: [box]

TASK [roles/mariadb : remove the packages so that they can be replaced] ***************************************************************************************************
changed: [box] => (item=[u'mariadb-libs.x86_64'])

TASK [roles/mariadb : install the mariadb packages] ***********************************************************************************************************************
changed: [box] => (item=[u'mariadb101u', u'mariadb101u-server', u'mariadb101u-config', u'mariadb101u-common', u'mariadb101u-libs', u'MySQL-python', u'postfix'])

TASK [roles/mariadb : start mariadb] **************************************************************************************************************************************
changed: [box]

TASK [roles/mariadb : change mysql root password] *************************************************************************************************************************
changed: [box] => (item=127.0.0.1)
changed: [box] => (item=::1)
changed: [box] => (item=192.168.50.4.nip.io)
changed: [box] => (item=%)
changed: [box] => (item=localhost)

TASK [roles/mariadb : set up .my.cnf file] ********************************************************************************************************************************
changed: [box]

TASK [roles/mariadb : delete anonymous MySQL user] ************************************************************************************************************************
ok: [box] => (item=127.0.0.1)
ok: [box] => (item=::1)
changed: [box] => (item=192.168.50.4.nip.io)
ok: [box] => (item=%)
changed: [box] => (item=localhost)

TASK [roles/mariadb : remove the MySQL test database] *********************************************************************************************************************
changed: [box]

TASK [roles/mariadb : download and unarchive the sample database data] ****************************************************************************************************
changed: [box]

TASK [roles/mariadb : create the sample database] *************************************************************************************************************************
changed: [box]

TASK [roles/mariadb : create the user for the sample database] ************************************************************************************************************
changed: [box] => (item=127.0.0.1)
ok: [box] => (item=::1)
ok: [box] => (item=192.168.50.4.nip.io)
ok: [box] => (item=%)
ok: [box] => (item=localhost)

TASK [roles/mariadb : check to see if we need to import the sample database dumps] ****************************************************************************************
ok: [box]

TASK [roles/mariadb : import the sample database] *************************************************************************************************************************
changed: [box] => (item=employees.sql)
changed: [box] => (item=load_departments.dump)
changed: [box] => (item=load_employees.dump)
changed: [box] => (item=load_dept_emp.dump)
changed: [box] => (item=load_dept_manager.dump)
changed: [box] => (item=load_titles.dump)
changed: [box] => (item=load_salaries1.dump)
changed: [box] => (item=load_salaries2.dump)
changed: [box] => (item=load_salaries3.dump)
changed: [box] => (item=show_elapsed.sql)

PLAY RECAP ****************************************************************************************************************************************************************
box : ok=26 changed=11 unreachable=0 failed=0

如果我们重新运行 playbook,playbook 运行的最后部分将返回以下内容:

TASK [roles/mariadb : download and unarchive the sample database data] ****************************************************************************************************
ok: [box]

TASK [roles/mariadb : create the sample database] *************************************************************************************************************************
ok: [box]

TASK [roles/mariadb : create the user for the sample database] ************************************************************************************************************
ok: [box] => (item=127.0.0.1)
ok: [box] => (item=::1)
ok: [box] => (item=192.168.50.4.nip.io)
ok: [box] => (item=%)
ok: [box] => (item=localhost)

TASK [roles/mariadb : check to see if we need to import the sample database dumps] ****************************************************************************************
ok: [box]

TASK [roles/mariadb : import the sample database] *************************************************************************************************************************
skipping: [box] => (item=employees.sql)
skipping: [box] => (item=load_departments.dump)
skipping: [box] => (item=load_employees.dump)
skipping: [box] => (item=load_dept_emp.dump)
skipping: [box] => (item=load_dept_manager.dump)
skipping: [box] => (item=load_titles.dump)
skipping: [box] => (item=load_salaries1.dump)
skipping: [box] => (item=load_salaries2.dump)
skipping: [box] => (item=load_salaries3.dump)
skipping: [box] => (item=show_elapsed.sql)

PLAY RECAP ****************************************************************************************************************************************************************
box : ok=25 changed=0 unreachable=0 failed=0

如你所见,我们设置的检查以防止重新导入数据库转储的工作效果如预期。我们可以使用 Sequel Pro 或 MySQL Workbench 等工具测试我们的 MariaDB 安装;只需使用以下主机和凭据连接:

  • 主机:192.168.50.4.nip.io

  • 端口:3306

  • 用户名:root

  • 密码:Pa55W0rd123

以下截图来自 Sequel Pro,显示了我们导入到employees 数据库中的employees表:

现在我们已经安装、配置了 MariaDB,并导入了一些示例数据,让我们来看看创建一个安装 PHP 的角色,这是我们 LAMP 堆栈的最后一个组件。

PHP

我们正在组合的堆栈的最后一个元素是 PHP。与其他三个元素一样,我们需要使用ansible-galaxy init命令创建一个角色:

$ ansible-galaxy init roles/php

与堆栈的其他部分一样,我们将使用 IUS 存储库中的软件包;这将允许我们安装 PHP 的最新版本,即 7.2 版本。

安装 PHP

与堆栈的前三部分一样,我们将从安装软件包开始。与以前一样,我们在roles/php/default/main.yml中定义了一个变量,列出了我们需要的所有软件包:

php_packages:
  - "php72u"
  - "php72u-bcmath"
  - "php72u-cli"
  - "php72u-common"
  - "php72u-dba"
  - "php72u-fpm"
  - "php72u-fpm-httpd"
  - "php72u-gd"
  - "php72u-intl"
  - "php72u-json"
  - "php72u-mbstring"
  - "php72u-mysqlnd"
  - "php72u-odbc"
  - "php72u-pdo"
  - "php72u-process"
  - "php72u-snmp"
  - "php72u-soap"
  - "php72u-xml"
  - "php72u-xmlrpc"

这是在php/roles/tasks/main.yml中使用 YUM 模块安装的:

- name: install the php packages
  yum:
    name: "{{ item }}"
    state: "installed"
  with_items: "{{ php_packages }}"
  notify:
    - "restart php-fpm"
    - "restart httpd"

从这个任务中可以看出,我们正在通知两个不同的处理程序,一个是 Apache,另一个是 PHP-FPM。你可能会想:为什么我们需要通知 Apache?

FastCGI 进程管理器FPM)是一个 PHP FastCGI 实现,它有助于使繁忙的 PHP 网站运行更高效。它还增加了使用不同用户和组 ID 启动 PHP 工作者的能力,可以使用不同的php.ini文件在不同的端口上监听,从而允许您创建处理负载的 PHP 工作者池。

由于我们正在安装php72u-fpm软件包,我们需要配置 Apache 以使用php72u-fpm-httpd软件包中设置的配置;如果不这样做,Apache 将不会加载配置,这会指示它如何与 PHP-FPM 交互。

PHP-FPM 的处理程序可以在roles/php/handlers/main.yml中找到,其中包含以下内容:

- name: "restart php-fpm"
  service:
    name: "php-fpm"
    state: "restarted"
    enabled: "yes"

这就是 PHP 安装和配置的全部内容;现在我们应该有一个可用的 PHP 安装,并且我们可以使用 phpinfo 文件进行测试。

phpinfo 文件

与 Apache 安装一样,我们可以添加选项来上传一个测试文件,这里是一个简单的 PHP 文件,调用php_info函数。这会显示关于我们的 PHP 安装的信息。上传此文件的任务如下所示:

- name: copy the test PHP page to the document root
  copy:
    src: "info.php"
    dest: "{{ document_root }}/info.php"
    mode: "0755"
    owner: "{{ users.0.name }}"
    group: "{{ apache_group }}"
  when: php_info == true

如你所见,只有在roles/php/default/main.yml中设置以下内容时才会被调用:

php_info: true

我们从我们的 Ansible 控制器复制到主机的文件可以在roles/php/files/info.php中找到,它包含以下三行:

<?php
  phpinfo();
?>

虽然这表明 PHP 已安装并运行,但并不是很有趣,因此在运行 playbook 之前,让我们添加一些将我们的 LAMP 堆栈所有元素联系在一起的步骤。

Adminer

playbook 的最后一个任务将是安装一个名为 Adminer 的 PHP 脚本;这提供了一个用于与数据库交互和管理的基于 PHP 的界面。安装 Adminer 有三个步骤,所有这些步骤都使用roles/php/defaults/main.yml中的嵌套变量:

adminer:
  install: true
  path: "/usr/share/adminer"
  download: "https://github.com/vrana/adminer/releases/download/v4.6.2/adminer-4.6.2-mysql.php"

如您所见,我们再次使用嵌套变量,这次是告诉我们的 playbook 安装工具的位置,应该安装在哪里,以及可以从哪里下载。roles/php/tasks/main.yml中的第一个任务是创建我们将安装 Adminer 的目录:

- name: create the document root for adminer
  file:
    dest: "{{ adminer.path }}"
    state: "directory"
    mode: "0755"
  when: adminer.install == true

现在我们在 Vagrant 盒子上有了一个安装 Adminer 的地方,我们应该下载它。这一次,由于我们不是在下载存档,我们使用get_url模块:

- name: download adminer
  get_url:
    url: "{{ adminer.download }}"
    dest: "{{ adminer.path }}/index.php"
    mode: 0755
  when: adminer.install == true

如您所见,我们正在从 GitHub 下载adminer-4.6.2-mysql.php文件,并将其保存到/usr/share/adminer/index.php,那么我们如何访问它呢?任务的最后一部分使用模板模块将额外的 Apache 配置文件上传到/etc/httpd/conf.d/adminer.conf

- name: copy the vhost.conf to /etc/httpd/conf.d/
  template:
    src: "adminer.conf.j2"
    dest: "/etc/httpd/conf.d/adminer.conf"
  when: adminer.install == true
  notify: "restart httpd"

adminer.conf.j2 模板应放置在 roles/php/templates,如下所示:

# {{ ansible_managed }}
Alias /adminer "{{ adminer.path }}"
  <Directory "{{ adminer.path }}">
    DirectoryIndex index.php
    AllowOverride All
    Require all granted
  </Directory>

如您所见,它正在创建一个名为/adminer的别名,然后指向/usr/share/adminer/中的index.php。由于我们正在添加到 Apache 配置文件,因此我们还通知restart httpd处理程序,以便 Apache 重新启动,从而获取我们更新的配置。

运行角色

现在我们的 LAMP 堆栈的最后一个元素的角色已经完成,我们可以将其添加到我们的 playbook 中。现在它应该看起来像下面这样:

---

- hosts: boxes
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/common
    - roles/apache
    - roles/mariadb
    - roles/php

使用以下命令运行它:

$ ansible-playbook -i production site.yml

这将在我们的 Vagrant 盒子上部署 PHP;此输出作为调用 PHP 角色:

TASK [roles/php : install the php packages] ********************************************************
changed: [box] => (item=[u'php72u', u'php72u-bcmath', u'php72u-cli', u'php72u-common', u'php72u-dba', u'php72u-fpm', u'php72u-fpm-httpd', u'php72u-gd', u'php72u-intl', u'php72u-json', u'php72u-mbstring', u'php72u-mysqlnd', u'php72u-odbc', u'php72u-pdo', u'php72u-process', u'php72u-snmp', u'php72u-soap', u'php72u-xml', u'php72u-xmlrpc'])

TASK [roles/php : copy the test PHP page to the document root] *************************************
changed: [box]

TASK [roles/php : create the document root for adminer] ********************************************
changed: [box]

TASK [roles/php : download adminer] ****************************************************************
changed: [box]

TASK [roles/php : copy the vhost.conf to /etc/httpd/conf.d/] ***************************************
changed: [box]

RUNNING HANDLER [roles/common : restart ntp] *******************************************************
changed: [box]

RUNNING HANDLER [roles/apache : restart httpd] *****************************************************
changed: [box]

RUNNING HANDLER [roles/php : restart php-fpm] ******************************************************
changed: [box]

PLAY RECAP *****************************************************************************************
box : ok=34 changed=32 unreachable=0 failed=0

安装完成后,您应该能够访问以下 URL:

  • http://192.168.50.4.nip.io/info.php

  • http://192.168.50.4.nip.io/adminer/

当您访问第一个链接时,您应该看到类似以下页面的内容:

在第二个链接中,一旦使用用户名root和密码Pa55W0rd123登录,您应该能够看到employees数据库:

使用 Adminer,我们有一个 PHP 脚本访问我们的 MariaDB 数据库;页面是由我们的 Linux Vagrant 盒子上的 Apache 提供的。

覆盖变量

在我们完成之前,我们应该快速讨论一下如何覆盖我们一直在设置的默认变量。为此,将以下行添加到group_vars/common.yml文件中:

html_body: |
  This HTML page has been deployed using Ansible to <b>{{ ansible_nodename }}</b>.<br>
  The user is <b>{{ users.0.name }}</b> who is in the <b>{{ apache_group }}</b> group.<br>
  The weboot is <b>{{ document_root }}</b>, the default index file is <b>{{ index_file }}</b>.<br>
  You can access a <a href="/info.php">PHP Info file</a> or <a href="/adminer/">Adminer</a>.

然后重新运行 playbook。一旦 playbook 完成,打开http://192.168.50.4.nip.io/将显示以下页面:

如您所见,默认的index.html页面已更新为包含指向我们的 phpinfo 页面和 Adminer 的链接。我们配置为默认的任何变量都可以以这种方式被覆盖。

摘要

在本章中,我们已经通过编写一个 playbook,在我们的 CentOS 7 Vagrant 盒子上安装了一个 LAMP 堆栈。我们创建了四个角色,每个角色对应堆栈的一个元素,并在每个角色中构建了一些逻辑,可以覆盖以部署其他元素,如测试 HTML 和 PHP 页面,并且还内置了创建一个包含超过 40,000 条记录的测试数据库的选项。

到目前为止,我们已经安装了一些非常基本的软件包。在下一章中,我们将编写一个安装、配置和维护 WordPress 安装的 playbook。

问题

  1. 您会使用哪个 Ansible 模块来下载和解压缩 zip 文件?

  2. 真或假:在roles/rolename/default/文件夹中找到的变量会覆盖所有其他相同变量的引用。

  3. 解释一下你会如何向我们的 playbook 中添加第二个用户

  4. 真或假:你只能从一个任务中调用一个处理程序。

  5. 更新最终的 playbook 以添加第二个虚拟主机,它提供不同的默认 HTML 页面。

进一步阅读

你可以在以下 URL 找到本章中涵盖的第三方工具的项目页面:

第五章:部署 WordPress

在上一章中,我们致力于构建一个安装和配置基本 LAMP 堆栈的 playbook。在本章中,我们将在那里使用的技术基础上构建一个安装 LEMP 堆栈和 WordPress 的 playbook。

我们将涵盖以下主题:

  • 准备我们的初始 playbook

  • 下载并安装 WordPress CLI

  • 安装和配置 WordPress

  • 登录到您的 WordPress 安装

在开始之前,我们应该快速了解一下 WordPress 是什么。很可能在过去的 48 小时内,您已经访问过由 WordPress 提供支持的网站。它是一个由 PHP 和 MySQL 提供支持的开源内容管理系统CMS),根据 BuiltWith 提供的 CMS 使用统计数据,它被约 19,545,516 个网站使用。

技术要求

在前几章中启动的 CentOS 7 Vagrant box 的新副本将被使用。这意味着软件包需要重新下载,以及 WordPress。您可以在github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter05/lemp找到 playbook 的完整副本。

预安装任务

如前一章所述,LEMP 堆栈由以下元素组成:

  • Linux:在我们的情况下,这将再次是 CentOS 7

  • NGINX:如果您记得,它的发音是engine-x,并且在我们的堆栈中替代了 Apache

  • MariaDB:正如我们所看到的,这将是数据库组件

  • PHP:我们将再次使用 PHP 7.2

在安装 WordPress 之前,我们需要安装和配置这些组件。此外,由于这个 playbook 最终将被执行在公开可用的云服务器上,我们需要考虑一些关于 NGINX 配置的最佳实践。

让我们从设置 playbook 的初始结构开始:

$ mkdir lemp lemp/group_vars
$ touch lemp/group_vars/common.yml lemp/production lemp/site.yml lemp/Vagrantfile lemp/.gitignore
$ cd lemp

既然我们有了基本布局,我们需要在Vagrantfile.gitignore文件中放一些内容。Vagrantfile包含以下内容,与前几章类似:

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
BOX_NAME    = "centos/7"
BOX_IP      = "192.168.50.5"
DOMAIN      = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY  = '~/.ssh/id_rsa.pub'

Vagrant.configure(API_VERSION) do |config|
  config.vm.box = BOX_NAME
  config.vm.network "private_network", ip: BOX_IP
  config.vm.host_name = BOX_IP + '.' + DOMAIN
  config.ssh.insert_key = false
  config.ssh.private_key_path = [PRIVATE_KEY,
  "~/.vagrant.d/insecure_private_key"]
  config.vm.provision "file", source: PUBLIC_KEY, destination:
  "~/.ssh/authorized_keys"

  config.vm.provider "virtualbox" do |v|
    v.memory = "2024"
    v.cpus = "2"
  end

  config.vm.provider "vmware_fusion" do |v|
    v.vmx["memsize"] = "2024"
    v.vmx["numvcpus"] = "2"
  end

end

正如您可能已经注意到的那样,我们为这个 Vagrant box 使用了不同的 IP 地址;.gitignore文件应该包含一行:

.vagrant

现在我们已经配置好了基本内容,我们可以开始编写 playbook 来部署和配置我们的初始软件堆栈。

stack-install 命令

我们将从使用ansible-galaxy init创建一个名为stack-install的角色开始:

$ ansible-galaxy init roles/stack-install

这将安装我们的初始软件堆栈。安装完成后,我们将交给第二个角色,然后配置软件堆栈,然后第三个角色开始安装 WordPress。

那么我们需要哪些软件包呢?WordPress 有以下要求:

  • PHP 7.2 或更高版本

  • MariaDB 10.0 或更高版本,或者 MySQL 5.6 或更高版本

  • 带有mod_rewrite模块的 NGINX 或 Apache

  • HTTPS 支持

我们知道从上一章,IUS 仓库可以提供 PHP 7.2 和 MariaDB 10.1,所以我们将使用它作为这些软件包的来源,但 NGINX 呢?EPEL 仓库中有 NGINX 软件包。但是,我们将使用主要的 NGINX 仓库,以便获取最新和最好的版本。

启用仓库

让我们通过启用我们安装软件堆栈所需的三个仓库来开始我们的 playbook,然后,一旦这些仓库被启用,我们应该执行yum update来确保基本操作系统是最新的。

roles/stack-install/defaults/main.yml文件需要以下内容才能实现这一点。首先,我们有启用 EPEL 和 IUS 的 RPM 软件包的位置:

repo_packages:
  - "epel-release"
  - "https://centos7.iuscommunity.org/ius-release.rpm"

之后,我们有以下嵌套变量,其中包含我们使用yum_repository模块创建 NGINX 仓库的.repo文件所需的所有信息:

nginx_repo:
  name: "nginx"
  description: "The mainline NGINX repo"
  baseurl: "http://nginx.org/packages/mainline/centos/7/$basearch/"
  gpgcheck: "no"
  enabled: "yes"

现在我们已经有了默认设置,我们可以将任务添加到roles/stack-install/tasks/main.yml文件中;具体如下,第一个任务已经很熟悉,因为它只是安装我们的两个软件包:

- name: install the repo packages
  yum:
    name: "{{ item }}"
    state: "installed"
  with_items: "{{ repo_packages }}"

接下来的任务是在/etc/yum.repos.d/中创建一个名为nginx.repo的存储库文件:

- name: add the NGINX mainline repo
  yum_repository:
    name: "{{ nginx_repo.name }}"
    description: "{{ nginx_repo.description }}"
    baseurl: "{{ nginx_repo.baseurl }}"
    gpgcheck: "{{ nginx_repo.gpgcheck }}"
    enabled: "{{ nginx_repo.enabled }}"

从以下终端输出可以看出,文件的内容指向了 NGINX 存储库,我们可以通过运行以下命令获取有关 NGINX 软件包的更多信息:

$ yum info nginx

以下截图显示了前面命令的输出:

下面的任务也应该看起来很熟悉,因为我们在上一章中使用它来更新已安装的软件包:

- name: update all of the installed packages
  yum:
    name: "*"
    state: "latest"
    update_cache: "yes"

现在我们已经设置好了源存储库并更新了已安装的软件包,我们可以继续进行其余的软件包安装。

安装软件包

我们将创建四个软件包列表;这些在roles/stack-install/defaults/main.yml文件中。与上一章一样,我们首先需要卸载预安装的 MariaDB 软件包,因此我们的第一个列表包括要删除的软件包:

packages_remove:
  - "mariadb-libs.x86_64"

接下来,我们需要安装一些软件包,以允许 Ansible 与诸如 SELinux 和 MariaDB 之类的服务进行交互,以及安装 Postfix 软件包,我们知道上次已经删除了:

system_packages:
  - "postfix"
  - "MySQL-python"
  - "policycoreutils-python"

然后,我们有组成核心软件堆栈的所有软件包:

stack_packages:
  - "nginx"
  - "mariadb101u"
  - "mariadb101u-server"
  - "mariadb101u-config"
  - "mariadb101u-common"
  - "mariadb101u-libs"
  - "php72u"
  - "php72u-bcmath"
  - "php72u-cli"
  - "php72u-common"
  - "php72u-dba"
  - "php72u-fpm"
  - "php72u-fpm-nginx"
  - "php72u-gd"
  - "php72u-intl"
  - "php72u-json"
  - "php72u-mbstring"
  - "php72u-mysqlnd"
  - "php72u-process"
  - "php72u-snmp"
  - "php72u-soap"
  - "php72u-xml"
  - "php72u-xmlrpc"

最后,我们还有一些不错的功能:

extra_packages:
  - "vim-enhanced"
  - "git"
  - "unzip"

删除软件包然后安装它们的任务应该放在roles/stack-install/tasks/main.yml文件中,从删除软件包的任务开始:

- name: remove the packages so that they can be replaced
  yum:
    name: "{{ item }}"
    state: "absent"
  with_items: "{{ packages_remove }}"

然后,我们可以使用以下任务一次性安装所有软件包:

- name: install the stack packages
  yum:
    name: "{{ item }}"
    state: "installed"
  with_items: "{{ system_packages + stack_packages + extra_packages }}"

请注意,我们正在将剩下的三个软件包列表合并为一个变量。我们这样做是为了尽量减少重复使用yum任务。这也允许我们在剧本的其他地方覆盖,比如只覆盖extra_packages,而不必重复整个堆栈其他部分所需的软件包列表。

stack-config 角色

接下来的角色将配置我们刚刚安装的软件堆栈,所以让我们创建这个角色:

$ ansible-galaxy init roles/stack-config

现在我们已经有了角色所需的文件,我们可以开始计划需要配置的内容。我们需要做以下事情:

  • 为我们的 WordPress 创建一个用户

  • 按照 WordPress Codex 上的最佳实践配置 NGINX

  • 将 PHP-FPM 配置为以 WordPress 用户身份运行

  • 为 SELinux 进行初始配置

让我们从创建 WordPress 用户开始。

WordPress 系统用户

WordPress 系统用户的默认设置,应该放在roles/stack-config/defaults/main.yml中,如下所示:

wordpress_system:
  user: "wordpress"
  group: "php-fpm"
  comment: "wordpress system user"
  home: "/var/www/wordpress"
  state: "present"

我们将这称为系统用户,因为我们将在本章后面创建一个 WordPress 用户。这个用户的详细信息也将在 Ansible 中定义,所以我们不想混淆两个不同的用户。

使用这些变量的任务应该在roles/stack-config/tasks/main.yml中,看起来像这样:

- name: add the wordpress user
  user: 
    name: "{{ wordpress_system.user }}"
    group: "{{ wordpress_system.group }}"
    comment: "{{ wordpress_system.comment }}"
    home: "{{ wordpress_system.home }}"
    state: "{{ wordpress_system.state }}"

如你所见,这次我们没有向用户添加密钥,因为我们不想登录到用户帐户来开始操作文件和其他操作。这应该全部在 WordPress 内部完成,或者通过使用 Ansible 完成。

NGINX 配置

我们将使用几个模板文件来配置我们的 NGINX。第一个模板名为roles/stack-config/templates/nginx-nginx.conf.j2,它将替换软件包安装部署的主要 NGINX 配置:

# {{ ansible_managed }}
user nginx;
worker_processes {{ ansible_processor_count }};
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log /var/log/nginx/access.log main;
    sendfile on;
    keepalive_timeout 65;
    client_max_body_size 20m;
    include /etc/nginx/conf.d/*.conf;
}

文件本身的内容基本上与初始文件相同,只是我们正在更新worker_processes,以便它使用 Ansible 运行setup模块时检测到的处理器数量,而不是硬编码的值。

部署配置文件的任务就像你期望的那样,应该放在roles/stack-config/tasks/main.yml中:

- name: copy the nginx.conf to /etc/nginx/
  template:
    src: "nginx-nginx.conf.j2"
    dest: "/etc/nginx/nginx.conf"
  notify: "restart nginx"

如您所见,我们正在通知restart nginx处理程序,它存储在以下roles/stack-config/handlers/main.yml文件中:

- name: "restart nginx"
  service:
    name: "nginx"
    state: "restarted"
    enabled: "yes"

接下来,我们有默认站点模板,roles/stack-config/templates/nginx-confd-default.conf.j2

# {{ ansible_managed }}

upstream {{ php.upstream }} {
        server {{ php.ip }}:{{ php.port }};
}

server {
    listen 80;
  server_name {{ ansible_nodename }};
  root {{ wordpress_system.home }};
  index index.php index.html index.htm;

    include global/restrictions.conf;
    include global/wordpress_shared.conf;

}

为了帮助识别模板文件将放置在目标主机上的位置,我将它们命名,以便文件名中包含完整路径。在这种情况下,文件名是nginx-confd-default.conf.j2,它将部署到/etc/nginx/conf.d/..

我们要部署的下两个模板将进入一个不存在的文件夹。因此,我们首先需要创建目标文件夹。为此,我们需要将以下内容添加到roles/stack-config/tasks/main.yml中:

- name: create the global directory in /etc/nginx/
  file:
    dest: "/etc/nginx/global/"
    state: "directory"
    mode: "0644"

然后,以下命令将文件复制到global文件夹中:

- name: copy the restrictions.conf to /etc/nginx/global/
  copy:
    src: "nginx-global-restrictions.conf"
    dest: "/etc/nginx/global/restrictions.conf"
  notify: "restart nginx"

- name: copy the wordpress_shared.conf to /etc/nginx/global/
  template:
    src: "nginx-global-wordpress_shared.conf.j2"
    dest: "/etc/nginx/global/wordpress_shared.conf"
  notify: "restart nginx"

由于我们在nginx-global-restrictions.conf文件中没有进行任何替换,所以我们在这里使用copy模块而不是template;文件存储在roles/stack-config/files/中,内容如下:

   # Do not log robots.txt
        location = /robots.txt {
            log_not_found off;
            access_log off;
        }

    # If no favicon exists return a 204 (no content error)
        location ~* /favicon\.ico$ {
            try_files $uri =204;
            expires max;
            log_not_found off;
            access_log off;
        }

  # Deny access to htaccess files
        location ~ /\. {
            deny all;
        }

  # Deny access to some bits wordpress leaves hanging around 
        location ~* /(wp-config.php|readme.html|license.txt|nginx.conf) {
            deny all;
        }

    # Deny access to .php files in the /wp-content/ directory (including sub-folders)
        location ~* ^/wp-content/.*.(php|phps)$ {
            deny all;
        }

    # Allow only internal access to .php files inside wp-includes directory
        location ~* ^/wp-includes/.*\.(php|phps)$ {
            internal;
        }

    # Deny access to specific files in the /wp-content/ directory (including sub-folders)
        location ~* ^/wp-content/.*.(txt|md|exe)$ {
            deny all;
        }

    # hide content of sensitive files
        location ~* \\.(conf|engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\\.php)?|xtmpl)\$|^(\\..*|Entries.*|Repository|Root|Tag|Template)\$|\\.php_ {
            deny all;
        }

    # don't allow other executable file types
        location ~* \\.(pl|cgi|py|sh|lua)\$ {
            deny all;
        }

    # hide the wordfence firewall
        location ~ ^/\.user\.ini {
            deny all;
        }

由于我们将php.upstream设置为变量,我们使用template模块来确保我们的配置包含正确的值,文件roles/stack-config/templates/nginx-global-wordpress_shared.conf.j2包含以下内容:

    # http://wiki.nginx.org/WordPress
    # This is cool because no php is touched for static content. 
    # Include the "?$args" part so non-default permalinks doesn't break when using query string
        location / {
            try_files $uri $uri/ /index.php?$args;
        }

        # Set the X-Frame-Options
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Xss-Protection "1; mode=block" always;
        add_header X-Content-Type-Options "nosniff" always;

    # Do not log + cache images, css, js, etc
        location ~* \.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
            expires max;
            log_not_found off;
            access_log off;
         # Send the all shebang in one fell swoop
            tcp_nodelay off;
        # Set the OS file cache
            open_file_cache max=1000 inactive=120s;
            open_file_cache_valid 45s;
            open_file_cache_min_uses 2;
            open_file_cache_errors off;
        }

    # Handle .php files
        location ~ \.php$ {
            try_files $uri =404;
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            include /etc/nginx/fastcgi_params;
            fastcgi_connect_timeout 180s;
            fastcgi_send_timeout 180s;
            fastcgi_read_timeout 180s;
            fastcgi_intercept_errors on;
            fastcgi_max_temp_file_size 0;
            fastcgi_pass {{ php.upstream }};
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_index index.php;
        }

    # Rewrite rules for WordPress SEO by Yoast
        rewrite ^/sitemap_index\.xml$ /index.php?sitemap=1 last;
        rewrite ^/([^/]+?)-sitemap([0-9]+)?\.xml$ /index.php?sitemap=$1&sitemap_n=$2 last;

    # Add trailing slash to */wp-admin requests
        rewrite /wp-admin$ $scheme://$host$uri/ permanent;

NGINX 配置的最后一部分是复制 WordPress 站点的主配置。roles/stack-config/tasks/main.yml中的任务如下所示:

- name: copy the default.conf to /etc/nginx/conf.d/
  template:
    src: "nginx-confd-default.conf.j2"
    dest: "/etc/nginx/conf.d/default.conf"
  notify: "restart nginx"

由于我们设置了一些变量,比如路径和域名,我们有以下模板文件:

# {{ ansible_managed }}

upstream php {
        server {{ php.ip }}:{{ php.port }};
}

server {
    listen 80;
  server_name {{ ansible_nodename }};
  root {{ wordpress_system.home }};
  index index.php;
  include global/restrictions.conf;
  include global/wordpress_shared.conf;
}

如您所见,我们正在使用一些尚未定义的变量,php.ipphp.port。我们将在接下来看如何配置 PHP-FPM。

PHP 和 PHP-FPM 配置

正如我们在上一节中看到的,roles/stack-config/defaults/main.yml中为 PHP 定义了一些变量,它们是:

php:
  ip: "127.0.0.1"
  port: "9000"
  upstream: "php"
  ini:
    - { regexp: '^;date.timezone =', replace: 'date.timezone = Europe/London' }
    - { regexp: '^expose_php = On', replace: 'expose_php = Off' }
    - { regexp: '^upload_max_filesize = 2M', replace: 'upload_max_filesize = 20M' }

第一个配置任务是部署 PHP-FPM 配置;模板如下所示:

; {{ ansible_managed }}

[{{ wordpress_system.user }}]
user = {{ wordpress_system.user }}
group = {{ wordpress_system.group }}
listen = {{ php.ip }}:{{ php.port }}
listen.allowed_clients = {{ php.ip }}
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
php_admin_value[error_log] = /var/log/php-fpm/{{ wordpress_system.user }}-error.log
php_admin_flag[log_errors] = on
php_value[session.save_handler] = files
php_value[session.save_path] = /var/lib/php/fpm/session
php_value[soap.wsdl_cache_dir] = /var/lib/php/fpm/wsdlcache

如您所见,我们在这个文件中进行了一些替换。从方括号之间开始,我们定义了 PHP-FPM 池名称;我们使用wordpress_system.user的内容。接下来,我们有我们希望池运行的用户和组;在这里,我们使用wordpress_system.userwordpress_system.group。最后,我们通过使用php.ipphp.port变量来设置我们希望 PHP-FPM 池监听的 IP 地址和端口。

部署模板的roles/stack-config/tasks/main.yml任务如下所示:

- name: copy the www.conf to /etc/php-fpm.d/
  template:
    src: "php-fpmd-www.conf.j2"
    dest: "/etc/php-fpm.d/www.conf"
  notify: "restart php-fpm"

roles/stack-config/handlers/main.yml中重新启动 PHP-FPM 的处理程序只是:

- name: "restart php-fpm"
  service:
    name: "php-fpm"
    state: "restarted"
    enabled: "yes"

roles/stack-config/tasks/main.yml中的下一个任务使用lineinfile模块:

- name: configure php.ini
  lineinfile: 
    dest: "/etc/php.ini"
    regexp: "{{ item.regexp }}"
    line: "{{ item.replace }}"
    backup: "yes"
    backrefs: "yes"
  with_items: "{{ php.ini }}"
  notify: "restart php-fpm"

我们在这里做的是获取php.ini的内容,并通过查找regexp键来循环遍历它。一旦找到值,我们就用replace键的内容替换它。如果文件有更改,我们首先进行backup,以防万一。此外,我们使用backrefs来确保如果文件中没有匹配的正则表达式,它将保持不变;如果我们不使用它们,那么每次运行 playbook 时都会调用restart php-fpm处理程序,而我们不希望在没有理由的情况下重新启动 PHP-FPM。

启动 NGINX 和 PHP-FPM

现在我们已经安装和配置了我们的堆栈,我们需要启动两个服务,而不是等到 playbook 运行结束。如果现在不这样做,我们即将安装 WordPress 的角色将失败。roles/stack-config/tasks/main.yml中的两个任务是:

- name: start php-fpm
  service:
    name: "php-fpm"
    state: "started"

- name: start nginx
  service:
    name: "nginx"
    state: "started"

MariaDB 配置

MariaDB 配置将与上一章的配置非常相似,减去一些步骤,所以我不打算详细介绍。

该角色在roles/stack-config/defaults/main.yml中的默认变量为:

mariadb:
  bind: "127.0.0.1"
  server_config: "/etc/my.cnf.d/mariadb-server.cnf"
  username: "root"
  password: "Pa55W0rd123"
  hosts:
    - "127.0.0.1"
    - "::1"
    - "{{ ansible_nodename }}"
    - "localhost"

正如你所看到的,我们现在正在使用嵌套变量,并且已经在roles/stack-config/tasks/main.yml的任务的第一部分中删除了主机通配符%的根访问权限,将 MariaDB 绑定到本地主机:

- name: configure the mariadb bind address
  lineinfile: 
    dest: "{{ mariadb.server_config }}"
    regexp: "#bind-address=0.0.0.0"
    line: "bind-address={{ mariadb.bind }}"
    backup: "yes"
    backrefs: "yes"

从那里,我们开始 MariaDB,设置根密码,配置~/.my.cnf文件,然后删除匿名用户和测试数据库:

- name: start mariadb
  service:
    name: "mariadb"
    state: "started"
    enabled: "yes"

- name: change mysql root password
  mysql_user:
    name: "{{ mariadb.username }}" 
    host: "{{ item }}" 
    password: "{{ mariadb.password }}"
    check_implicit_admin: "yes"
    priv: "*.*:ALL,GRANT"
  with_items: "{{ mariadb.hosts }}"

- name: set up .my.cnf file
  template:
    src: "my.cnf.j2"
    dest: "~/.my.cnf"

- name: delete anonymous MySQL user
  mysql_user:
    user: ""
    host: "{{ item }}"
    state: "absent"
  with_items: "{{ mariadb.hosts }}"

- name: remove the MySQL test database
  mysql_db:
    db: "test" 
    state: "absent"

.my.cnf文件的模板,可以在roles/stack-config/templates/my.cnf.j2中找到,现在如下所示:

# {{ ansible_managed }}
[client]
password='{{ mariadb.password }}'

这意味着我们将不需要在每个与数据库相关的任务中传递根用户名和密码,从我们复制.my.cnf文件的地方开始。

SELinux 配置

角色的最后一个任务是将 SELinux 中的 HTTP 设置为宽松模式;为了做到这一点,我们在roles/stack-config/defaults/main.yml中有以下变量:

selinux:
  http_permissive: true

roles/stack-config/tasks/main.yml中的任务有一个条件,如果selinux.http_permissive等于true,则运行:

- name: set the selinux allowing httpd_t to be permissive is required
  selinux_permissive:
    name: httpd_t
    permissive: true
  when: selinux.http_permissive == true

我们将在后面的章节更多地关注 SELinux;目前,我们只允许所有的 HTTP 请求。

WordPress 安装任务

现在我们已经完成了准备目标 Vagrant 盒子的角色,我们可以继续进行实际的 WordPress 安装;这将分为几个不同的部分,首先是下载wp_cli和设置数据库。

在我们继续之前,我们应该创建角色:

$ ansible-galaxy init roles/wordpress

WordPress CLI 安装

WordPress CLIWP-CLI)是一个用于管理 WordPress 安装的命令行工具;我们将在整个角色中使用它,所以我们角色应该首先下载它。为了做到这一点,我们需要在roles/wordpress/defaults/main.yml中下载以下变量:

wp_cli:
  download: "https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar"
  path: "/usr/local/bin/wp"

正如你可能从这两个变量中了解到的,我们将从wp_cli.download下载文件,并将其复制到wp_cli.path。在roles/wordpress/tasks/main.yml中执行此操作的任务如下:

- name: download wp-cli
  get_url:
    url: "{{ wp_cli.download }}"
    dest: "{{ wp_cli.path }}"

- name: update permissions of wp-cli to allow anyone to execute it
  file:
    path: "{{ wp_cli.path }}"
    mode: "0755"

正如你所看到的,我们正在下载.phar文件,移动它,然后设置权限,以便任何登录到服务器的人都可以执行它——这很重要,因为我们将以wordpress用户的身份运行许多安装命令。

创建 WordPress 数据库

角色的下一部分是创建我们的 WordPress 安装将使用的数据库;与本章其他任务一样,它使用了一个可以在roles/wordpress/defaults/main.yml中找到的嵌套变量:

wp_database:
  name: "wordpress"
  username: "wordpress"
  password: "W04DPr3S5"

roles/wordpress/tasks/main.yml中创建数据库和用户的任务如下:

- name: create the wordpress database
  mysql_db:
    db: "{{ wp_database.name }}" 
    state: "present"

- name: create the user for the wordpress database
  mysql_user:
    name: "{{ wp_database.username }}"
    password: "{{ wp_database.password }}"
    priv: "{{ wp_database.name }}.*:ALL"
    state: "present"
  with_items: "{{ mariadb.hosts }}"

请注意我们正在使用前一个角色中的mariadb.hosts变量。现在我们已经创建了数据库,我们可以开始下载和安装 WordPress 了。

下载、配置和安装 WordPress

现在我们已经准备好安装 WordPress,我们可以开始了,首先在roles/wordpress/defaults/main.yml中设置一些默认变量:

wordpress:
  domain: "http://{{ ansible_nodename }}/"
  title: "WordPress installed by Ansible"
  username: "ansible"
  password: "password"
  email: "test@example.com"
  theme: "sydney"
  plugins:
    - "jetpack"
    - "wp-super-cache"
    - "wordpress-seo"
    - "wordfence"
    - "nginx-helper"

现在我们有了变量,如果需要,我们可以开始下载:

- name: are the wordpress files already there?
  stat:
    path: "{{ wordpress_system.home }}/index.php"
  register: wp_installed

- name: download wordpresss
  shell: "{{ wp_cli.path }} core download"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  when: wp_installed.stat.exists == False

正如你所看到的,第一个任务使用stat模块来检查系统用户的主目录(也是 webroot)中是否存在index.php。第二个任务使用shell模块来执行wp core download命令。

在继续下一个任务之前,我们应该处理一些参数。这些是:

  • argschdir:您可以使用argsshell模块传递额外的参数。在这里,我们传递chdir,它指示 Ansible 在运行我们提供的shell命令之前切换到我们指定的目录。

  • become_user:我们希望以哪个用户的身份运行命令。如果我们不使用这个,命令将以 root 用户的身份运行。

  • become:这指示 Ansible 以定义的用户身份执行任务。

剧本中的下一个任务设置了用户主目录的正确权限:

- name: set the correct permissions on the homedir
  file:
    path: "{{ wordpress_system.home }}"
    mode: "0755"
  when: wp_installed.stat.exists == False

现在 WordPress 已经下载,我们可以开始安装。首先,我们需要检查是否已经完成了这一步:

- name: is wordpress already configured?
  stat:
    path: "{{ wordpress_system.home }}/wp-config.php"
  register: wp_configured

如果没有wp-config.php文件,那么将执行以下任务:

- name: configure wordpress
  shell: "{{ wp_cli.path }} core config --dbhost={{ mariadb.bind }} --dbname={{ wp_database.name }} --dbuser={{ wp_database.username }} --dbpass={{ wp_database.password }}"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  when: wp_configured.stat.exists == False

现在我们已经创建了wp-config.php文件,并且数据库凭据已经就位,我们可以安装 WordPress 了。首先,我们需要检查 WordPress 是否已经安装:

- name: do we need to install wordpress?
  shell: "{{ wp_cli.path }} core is-installed"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  ignore_errors: yes
  register: wp_installed

正如你从ignore_errors选项的存在可以看出,如果 WordPress 未安装,这个命令将给我们一个错误。然后我们利用这一点来注册结果,正如你从下面的任务中可以看到的:

- name: install wordpress if needed
  shell: "{{ wp_cli.path }} core install --url='{{ wordpress.domain }}' --title='{{ wordpress.title }}' --admin_user={{ wordpress.username }} --admin_password={{ wordpress.password }} --admin_email={{ wordpress.email }}"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  when: wp_installed.rc == 1

现在我们已经安装了一个基本的 WordPress 网站,我们可以继续安装插件和主题文件。

WordPress 插件和主题安装

我们 WordPress 安装的最后一部分是下载和安装我们在wordpress.pluginswordpress.theme变量中定义的插件和主题文件。

让我们从安装插件的任务开始,这样我们就不会重新运行安装插件的任务。当需要时,我们将在任务中构建一些逻辑。首先,我们运行一个任务来查看所有插件是否已经安装:

- name: do we need to install the plugins?
  shell: "{{ wp_cli.path }} plugin is-installed {{ item }}"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  with_items: "{{ wordpress.plugins }}"
  ignore_errors: yes
  register: wp_plugin_installed

如果插件未安装,则此任务应该失败,这就是为什么我们在其中使用ignore_errors的原因。正如你所看到的,我们正在注册整个任务的结果,因为如果你记得,我们正在安装几个插件,作为wp_plugin_installed。接下来的两个任务获取wp_plugin_installed的结果,并使用setfact模块设置一个事实:

- name: set a fact if we don't need to install the plugins
  set_fact:
    wp_plugin_installed_skip: true
  when: wp_plugin_installed.failed is undefined

- name: set a fact if we need to install the plugins
  set_fact:
    wp_plugin_installed_skip: false
  when: wp_plugin_installed.failed is defined

正如你所看到的,我们将wp_theme_installed_skip设置为truefalse:如果事实设置为false,那么接下来的任务将循环安装插件:

- name: install the plugins if we need to or ignore if not
  shell: "{{ wp_cli.path }} plugin install {{ item }} --activate"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  with_items: "{{ wordpress.plugins }}"
  when: wp_plugin_installed_skip == false

如果我们将另一个插件添加到列表中,但保留其他插件不变,它将显示一个错误,导致插件被安装。我们将使用相同的逻辑来判断我们是否需要安装我们定义为wordpress.theme的主题文件:

- name: do we need to install the theme?
  shell: "{{ wp_cli.path }} theme is-installed {{ wordpress.theme }}"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  ignore_errors: yes
  register: wp_theme_installed

- name: set a fact if we don't need to install the theme
  set_fact:
    wp_theme_installed_skip: true
  when: wp_theme_installed.failed == false

- name: set a fact if we need to install the theme
  set_fact:
    wp_theme_installed_skip: false
  when: wp_theme_installed.failed == true

- name: install the theme if we need to or ignore if not
  shell: "{{ wp_cli.path }} theme install {{ wordpress.theme }} --activate"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  when: wp_theme_installed_skip == false

现在我们已经安装了插件和主题,可以尝试运行我们的 playbook 了。

运行 WordPress playbook

要运行 playbook 并安装 WordPress,我们需要一些东西,首先是名为production的清单文件:

box1 ansible_host=192.168.50.5.nip.io

[wordpress]
box1

[wordpress:vars]
ansible_connection=ssh
ansible_user=vagrant
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

正如你所看到的,它考虑了我们在本章开头定义的 Vagrant box 的更新后 IP 地址。另外,我们需要 playbook 本身;site.yml应该如下所示:

---

- hosts: wordpress
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/stack-install
    - roles/stack-config
    - roles/wordpress

现在,通过运行以下两个命令之一来启动 Vagrant box:

$ vagrant up
$ vagrant up --provider=vmware_fusion

一旦你的 Vagrant box 启动并运行,我们可以使用以下命令开始 playbook 运行:

$ ansible-playbook -i production site.yml

当首次执行 playbook 时,你应该看到类似以下结果的内容:

PLAY [wordpress] ***********************************************************************************

TASK [Gathering Facts] *****************************************************************************
ok: [box1]

TASK [roles/stack-install : install the repo packages] *********************************************
changed: [box1] => (item=[u'epel-release', u'https://centos7.iuscommunity.org/ius-release.rpm'])

TASK [roles/stack-install : add the NGINX mainline repo] *******************************************
changed: [box1]

TASK [roles/stack-install : update all of the installed packages] **********************************
changed: [box1]

TASK [roles/stack-install : remove the packages so that they can be replaced] **********************
changed: [box1] => (item=[u'mariadb-libs.x86_64'])

TASK [roles/stack-install : install the stack packages] ********************************************
changed: [box1] => (item=[u'postfix', u'MySQL-python', u'policycoreutils-python', u'nginx', u'mariadb101u', u'mariadb101u-server', u'mariadb101u-config', u'mariadb101u-common', u'mariadb101u-libs', u'php72u', u'php72u-bcmath', u'php72u-cli', u'php72u-common', u'php72u-dba', u'php72u-fpm', u'php72u-fpm-nginx', u'php72u-gd', u'php72u-intl', u'php72u-json', u'php72u-mbstring', u'php72u-mysqlnd', u'php72u-process', u'php72u-snmp', u'php72u-soap', u'php72u-xml', u'php72u-xmlrpc', u'vim-enhanced', u'git', u'unzip'])

TASK [roles/stack-config : add the wordpress user] *************************************************
changed: [box1]

TASK [roles/stack-config : copy the nginx.conf to /etc/nginx/] *************************************
changed: [box1]

TASK [roles/stack-config : create the global directory in /etc/nginx/] *****************************
changed: [box1]

TASK [roles/stack-config : copy the restrictions.conf to /etc/nginx/global/] ***********************
changed: [box1]

TASK [roles/stack-config : copy the wordpress_shared.conf to /etc/nginx/global/] *******************
changed: [box1]

TASK [roles/stack-config : copy the default.conf to /etc/nginx/conf.d/] ****************************
changed: [box1]

TASK [roles/stack-config : copy the www.conf to /etc/php-fpm.d/] ***********************************
changed: [box1]

TASK [roles/stack-config : configure php.ini] ******************************************************
changed: [box1] => (item={u'regexp': u'^;date.timezone =', u'replace': u'date.timezone = Europe/London'})
changed: [box1] => (item={u'regexp': u'^expose_php = On', u'replace': u'expose_php = Off'})
changed: [box1] => (item={u'regexp': u'^upload_max_filesize = 2M', u'replace': u'upload_max_filesize = 20M'})

TASK [roles/stack-config : start php-fpm] **********************************************************
changed: [box1]

TASK [roles/stack-config : start nginx] ************************************************************
changed: [box1]

TASK [roles/stack-config : configure the mariadb bind address] *************************************
changed: [box1]

TASK [roles/stack-config : start mariadb] **********************************************************
changed: [box1]

TASK [roles/stack-config : change mysql root password] *********************************************
changed: [box1] => (item=127.0.0.1)
changed: [box1] => (item=::1)
changed: [box1] => (item=192.168.50.5.nip.io)
changed: [box1] => (item=localhost)

TASK [roles/stack-config : set up .my.cnf file] ****************************************************
changed: [box1]

TASK [roles/stack-config : delete anonymous MySQL user] ********************************************
ok: [box1] => (item=127.0.0.1)
ok: [box1] => (item=::1)
changed: [box1] => (item=192.168.50.5.nip.io)
changed: [box1] => (item=localhost)

TASK [roles/stack-config : remove the MySQL test database] *****************************************
changed: [box1]

TASK [roles/stack-config : set the selinux allowing httpd_t to be permissive is required] **********
changed: [box1]

TASK [roles/wordpress : download wp-cli] ***********************************************************
changed: [box1]

TASK [roles/wordpress : update permissions of wp-cli to allow anyone to execute it] ****************
changed: [box1]

TASK [roles/wordpress : create the wordpress database] *********************************************
changed: [box1]

TASK [roles/wordpress : create the user for the wordpress database] ********************************
changed: [box1] => (item=127.0.0.1)
ok: [box1] => (item=::1)
ok: [box1] => (item=192.168.50.5.nip.io)
ok: [box1] => (item=localhost)

TASK [roles/wordpress : are the wordpress files already there?] ************************************
ok: [box1]

TASK [roles/wordpress : download wordpresss] *******************************************************
changed: [box1]

TASK [roles/wordpress : set the correct permissions on the homedir] ********************************
changed: [box1]

TASK [roles/wordpress : is wordpress already configured?] ******************************************
ok: [box1]

TASK [roles/wordpress : configure wordpress] *******************************************************
changed: [box1]

TASK [roles/wordpress : do we need to install wordpress?] ******************************************
fatal: [box1]: FAILED! => {"changed": true, "cmd": "/usr/local/bin/wp core is-installed", "delta": "0:00:00.364987", "end": "2018-03-04 20:22:16.659411", "msg": "non-zero return code", "rc": 1, "start": "2018-03-04 20:22:16.294424", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring

TASK [roles/wordpress : install wordpress if needed] ***********************************************
changed: [box1]

TASK [roles/wordpress : do we need to install the plugins?] ****************************************
failed: [box1] (item=jetpack) => {"changed": true, "cmd": "/usr/local/bin/wp plugin is-installed jetpack", "delta": "0:00:01.366121", "end": "2018-03-04 20:22:20.175418", "item": "jetpack", "msg": "non-zero return code", "rc": 1, "start": "2018-03-04 20:22:18.809297", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
failed: [box1] (item=wp-super-cache) => {"changed": true, "cmd": "/usr/local/bin/wp plugin is-installed wp-super-cache", "delta": "0:00:00.380384", "end": "2018-03-04 20:22:21.035274", "item": "wp-super-cache", "msg": "non-zero return code", "rc": 1, "start": "2018-03-04 20:22:20.654890", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
failed: [box1] (item=wordpress-seo) => {"changed": true, "cmd": "/usr/local/bin/wp plugin is-installed wordpress-seo", "delta": "0:00:00.354021", "end": "2018-03-04 20:22:21.852955", "item": "wordpress-seo", "msg": "non-zero return code", "rc": 1, "start": "2018-03-04 20:22:21.498934", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
failed: [box1] (item=wordfence) => {"changed": true, "cmd": "/usr/local/bin/wp plugin is-installed wordfence", "delta": "0:00:00.357012", "end": "2018-03-04 20:22:22.673549", "item": "wordfence", "msg": "non-zero return code", "rc": 1, "start": "2018-03-04 20:22:22.316537", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
failed: [box1] (item=nginx-helper) => {"changed": true, "cmd": "/usr/local/bin/wp plugin is-installed nginx-helper", "delta": "0:00:00.346194", "end": "2018-03-04 20:22:23.389176", "item": "nginx-helper", "msg": "non-zero return code", "rc": 1, "start": "2018-03-04 20:22:23.042982", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring

TASK [roles/wordpress : set a fact if we don't need to install the plugins] ************************
skipping: [box1]

TASK [roles/wordpress : set a fact if we need to install the plugins] ******************************
ok: [box1]

TASK [roles/wordpress : install the plugins if we need to or ignore if not] ************************
changed: [box1] => (item=jetpack)
changed: [box1] => (item=wp-super-cache)
changed: [box1] => (item=wordpress-seo)
changed: [box1] => (item=wordfence)
changed: [box1] => (item=nginx-helper)

TASK [roles/wordpress : do we need to install the theme?] ******************************************
fatal: [box1]: FAILED! => {"changed": true, "cmd": "/usr/local/bin/wp theme is-installed sydney", "delta": "0:00:01.451018", "end": "2018-03-04 20:23:02.227557", "msg": "non-zero return code", "rc": 1, "start": "2018-03-04 20:23:00.776539", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring

TASK [roles/wordpress : set a fact if we don't need to install the theme] **************************
skipping: [box1]

TASK [roles/wordpress : set a fact if we need to install the theme] ********************************
ok: [box1]

TASK [roles/wordpress : install the theme if we need to or ignore if not] **************************
changed: [box1]

RUNNING HANDLER [roles/stack-config : restart nginx] ***********************************************
changed: [box1]

RUNNING HANDLER [roles/stack-config : restart php-fpm] *********************************************
changed: [box1]

PLAY RECAP *****************************************************************************************
box1 : ok=42 changed=37 unreachable=0 failed=0

正如你在 playbook 中所看到的,我们对检查是否需要安装 WordPress 以及插件和主题检查都有致命错误,因为我们在任务中已经考虑到了这些情况,playbook 正常运行并安装了软件堆栈、WordPress、插件和主题。

重新运行 playbook 会给我们之前出错的部分带来以下结果:

TASK [roles/wordpress : do we need to install wordpress?] ******************************************
changed: [box1]

TASK [roles/wordpress : install wordpress if needed] ***********************************************
skipping: [box1]

TASK [roles/wordpress : do we need to install the plugins?] ****************************************
changed: [box1] => (item=jetpack)
changed: [box1] => (item=wp-super-cache)
changed: [box1] => (item=wordpress-seo)
changed: [box1] => (item=wordfence)
changed: [box1] => (item=nginx-helper)

TASK [roles/wordpress : set a fact if we don't need to install the plugins] ************************
ok: [box1]

TASK [roles/wordpress : set a fact if we need to install the plugins] ******************************
skipping: [box1]

TASK [roles/wordpress : install the plugins if we need to or ignore if not] ************************
skipping: [box1] => (item=jetpack)
skipping: [box1] => (item=wp-super-cache)
skipping: [box1] => (item=wordpress-seo)
skipping: [box1] => (item=wordfence)
skipping: [box1] => (item=nginx-helper)

TASK [roles/wordpress : do we need to install the theme?] ******************************************
changed: [box1]

TASK [roles/wordpress : set a fact if we don't need to install the theme] **************************
ok: [box1]

TASK [roles/wordpress : set a fact if we need to install the theme] ********************************
skipping: [box1]

TASK [roles/wordpress : install the theme if we need to or ignore if not] **************************
skipping: [box1]

PLAY RECAP *****************************************************************************************
box1 : ok=34 changed=3 unreachable=0 failed=0

现在 WordPress 已经安装,我们应该能够通过浏览器访问http://192.168.50.5.nip.io/。正如你在这里所看到的,我们定义的主题正在运行,而不是 WordPress 默认主题:

另外,如果你去http://192.168.50.5.nip.io/wp-admin/,你应该能够使用我们定义的用户名和密码登录 WordPress:

  • 用户名:ansible

  • 密码:密码

登录后,你应该会看到一些关于我们在 playbook 运行期间安装的插件需要配置的消息:

随意尝试使用 WordPress 安装;另外,你可以通过运行以下命令来删除 Vagrant box:

$ vagrant destroy

然后启动一个新的副本,并使用本节开头的命令重新部署它。

总结

在本章中,我们已经重复使用了我们在上一章中介绍的许多原则,并开始部署一个完整的应用程序。好处在于这个过程既可重复又只需一个命令。

到目前为止,我们一直在针对 CentOS 7 Vagrant box。如果我们对 Ubuntu Vagrant box 运行我们的 playbook,playbook 将会出错。在下一章中,我们将看看如何使用相同的 playbook 来针对多个操作系统。

问题

  1. setup模块执行期间收集的哪个事实可以告诉我们的 playbook 目标主机有多少处理器?

  2. 使用lineinfile模块中的backref是否确保如果正则表达式不匹配则不会应用任何更改。

  3. 解释为什么我们希望在 playbook 中构建逻辑来检查 WordPress 是否已经安装。

  4. 我们使用哪个模块来定义作为 playbook 运行一部分的变量?

  5. 我们传递给shell模块的哪个参数可以让我们想要运行的命令在我们选择的目录中执行?

  6. 真或假:将 MariaDB 设置为绑定到127.0.0.1将允许我们从外部访问它。

  7. 将您的 WordPress 网站主题更改为您选择的主题;请参阅wordpress.org/themes/以获取一些选项。

进一步阅读

您可以在以下链接找到有关本章涵盖的技术的更多信息:

我们安装的插件的项目页面可以在以下位置找到:

第六章:针对多个发行版

正如上一章末尾提到的,到目前为止,我们一直在针对单个操作系统使用我们的 playbook。如果我们只打算针对 CentOS 7 主机运行我们的 playbook,那是很好的,但情况可能并非总是如此。

在本章中,我们将看看如何调整我们的 WordPress 安装 playbook 以针对 Ubuntu 17.04 服务器实例。

在本章中,我们将:

  • 查看并实施操作系统相关的核心模块

  • 讨论并应用针对多个发行版的最佳实践

  • 看看如何使用 Ansible 清单来针对多个主机

技术要求

在本章中,我们将启动两个 Vagrant 盒子,所以你需要安装 Vagrant 并且能够访问互联网;这些盒子本身大约每个下载 300 到 500MB。

如果你要跟着做,适应我们的角色,你需要从上一章复制lemp文件夹并将其命名为lemp-multi。如果你不跟着做,你可以在github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter06/lemp-multi找到lemp-multi的完整版本。

启动多个 Vagrant 盒子

在我们开始查看我们需要对 Ansible playbook 进行的更改之前,我们应该看看我们将如何同时启动两个运行不同操作系统的 Vagrant 盒子。可以从单个Vagrantfile启动两个 Vagrant 盒子;我们将使用以下一个:

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
DOMAIN      = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY  = '~/.ssh/id_rsa.pub'
CENTOS_IP   = '192.168.50.6'
CENTOS_BOX  = 'centos/7'
UBUNTU_IP   = '192.168.50.7'
UBUNTU_BOX  = 'generic/ubuntu1704'

Vagrant.configure(API_VERSION) do |config|

  config.vm.define "centos" do |centos|
      centos.vm.box = CENTOS_BOX
      centos.vm.network "private_network", ip: CENTOS_IP
      centos.vm.host_name = CENTOS_IP + '.' + DOMAIN
      centos.ssh.insert_key = false
      centos.ssh.private_key_path = [PRIVATE_KEY,
      "~/.vagrant.d/insecure_private_key"]
      centos.vm.provision "file", source: PUBLIC_KEY, destination:
      "~/.ssh/authorized_keys"

      centos.vm.provider "virtualbox" do |v|
        v.memory = "2024"
        v.cpus = "2"
      end

      centos.vm.provider "vmware_fusion" do |v|
        v.vmx["memsize"] = "2024"
        v.vmx["numvcpus"] = "2"
      end
  end

  config.vm.define "ubuntu" do |ubuntu|
      ubuntu.vm.box = UBUNTU_BOX
      ubuntu.vm.network "private_network", ip: UBUNTU_IP
      ubuntu.vm.host_name = UBUNTU_IP + '.' + DOMAIN
      ubuntu.ssh.insert_key = false
      ubuntu.ssh.private_key_path = [PRIVATE_KEY,
      "~/.vagrant.d/insecure_private_key"]
      ubuntu.vm.provision "file", source: PUBLIC_KEY, destination:
      "~/.ssh/authorized_keys"

      ubuntu.vm.provider "virtualbox" do |v|
        v.memory = "2024"
        v.cpus = "2"
      end

      ubuntu.vm.provider "vmware_fusion" do |v|
        v.vmx["memsize"] = "2024"
        v.vmx["numvcpus"] = "2"
      end
  end

end

正如你所看到的,我们定义了两个不同的盒子,一个叫做centos,另一个叫做ubuntu,你应该用之前复制的lemp文件夹中的Vagrantfile替换它。

我们可以使用一个命令启动两台机器;要使用 VirtualBox,我们应该运行:

$ vagrant up 

或者要使用 VMware,我们可以运行:

$ vagrant up --provider=vmware_fusion

正如你从这里的终端输出中看到的,这启动了两个盒子:

Bringing machine 'centos' up with 'vmware_fusion' provider...
Bringing machine 'ubuntu' up with 'vmware_fusion' provider...
==> centos: Cloning VMware VM: 'centos/7'. This can take some time...
==> centos: Checking if box 'centos/7' is up to date...
==> centos: Verifying vmnet devices are healthy...
==> centos: Preparing network adapters...
==> centos: Starting the VMware VM...
==> centos: Waiting for the VM to receive an address...
==> centos: Forwarding ports...
 centos: -- 22 => 2222
==> centos: Waiting for machine to boot. This may take a few minutes...
 centos: SSH address: 127.0.0.1:2222
 centos: SSH username: vagrant
 centos: SSH auth method: private key
==> centos: Machine booted and ready!
==> centos: Setting hostname...
==> centos: Configuring network adapters within the VM...
 centos: SSH address: 127.0.0.1:2222
 centos: SSH username: vagrant
 centos: SSH auth method: private key
==> centos: Rsyncing folder: /Users/russ/lemp/ => /vagrant
==> centos: Running provisioner: file...
==> ubuntu: Cloning VMware VM: 'generic/ubuntu1704'. This can take some time...
==> ubuntu: Checking if box 'generic/ubuntu1704' is up to date...
==> ubuntu: Verifying vmnet devices are healthy...
==> ubuntu: Preparing network adapters...
==> ubuntu: Starting the VMware VM...
==> ubuntu: Waiting for the VM to receive an address...
==> ubuntu: Forwarding ports...
 ubuntu: -- 22 => 2222
==> ubuntu: Waiting for machine to boot. This may take a few minutes...
 ubuntu: SSH address: 127.0.0.1:2222
 ubuntu: SSH username: vagrant
 ubuntu: SSH auth method: private key
==> ubuntu: Machine booted and ready!
==> ubuntu: Setting hostname...
==> ubuntu: Configuring network adapters within the VM...
==> ubuntu: Running provisioner: file...

一旦盒子启动并运行,你可以使用机器名称 SSH 连接到它们:

$ vagrant ssh centos
$ vagrant ssh ubuntu

现在我们有两个运行在两个不同操作系统上的盒子,我们可以讨论我们需要对 playbook 进行的更改。首先,让我们看看对Vagrantfile的更改将如何影响我们的主机清单文件,正如你可以从这个文件中看到的那样:

centos ansible_host=192.168.50.6.nip.io 
ubuntu ansible_host=192.168.50.7.nip.io

[wordpress]
centos
ubuntu

[wordpress:vars]
ansible_connection=ssh
ansible_user=vagrant
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

现在我们有两个主机,一个叫做centos,另一个叫做ubuntu,我们将它们放在一个名为wordpress的组中,我们在那里设置一些公共变量。你应该更新你的production文件,因为我们将在下一节中使用它。

多操作系统考虑

查看在三个角色stack-installstack-configwordpress中使用的每个核心 Ansible 模块,我们使用了一些在我们新引入的 Ubuntu 盒子上不起作用的模块。让我们快速地逐个进行,并看看在针对两个非常不同的操作系统时需要考虑什么:

  • yumyum模块是 Red Hat 系机器(如 CentOS)使用的包管理器,而 Ubuntu 基于 Debian,使用apt。我们需要拆分出使用yum模块的 playbook 的部分,以使用apt模块代替。

  • yum_repository:如前所述,我们将需要使用一个apt等效模块,即apt_repository

  • useruser模块在两个操作系统上基本上是一样的,因为我们没有给我们的用户提升的特权。除了确保正确的组可用之外,我们没有任何特殊的考虑。

  • templatefilecopylineinfile:这四个模块都将按预期工作;我们需要考虑的唯一问题是检查我们是否将文件复制到了盒子上的正确位置。

  • service:服务模块在两个操作系统上应该是一样的,所以我们应该没问题。

  • mysql_usermysql_db:正如你所期望的,一旦 MySQL 安装并启动,这两个都将在两个操作系统上工作。

  • selinux_permissive:SELinux 主要用于基于 Red Hat 的操作系统,因此我们需要找到替代方案。

  • get_urlstatshellset_fact:这些应该在我们的目标操作系统上都能一致工作。

现在我们知道了在 Ubuntu 上运行与在 CentOS 上运行时需要审查现有 playbook 的哪些部分,我们可以开始让我们的角色在这两个操作系统上都能工作。

调整角色

那么我们如何在我们的角色中构建逻辑,只在不同的操作系统上执行角色的某些部分,而且我们也知道软件包名称会不同?我们如何为每个操作系统定义不同的变量集?

操作系统家族

我们在之前的章节中已经看过setup模块;这是一个收集有关我们目标主机的事实的模块。其中一个事实就是ansible_os_family;这告诉我们我们正在运行的操作系统类型。让我们在我们的两个主机上检查一下:

$ ansible -i production centos -m setup | grep ansible_os_family
$ ansible -i production ubuntu -m setup | grep ansible_os_family

正如你从以下终端输出中所看到的,CentOS 主机返回了 Red Hat,这是预期的。然而,Ubuntu 主机没有返回任何信息:

让我们看看为什么会这样。首先,我们可以重新运行命令,但这次去掉grep,这样我们就可以看到完整的输出:

$ ansible -i production ubuntu -m setup

这应该给你类似以下的结果:

哦,我们出现了一个错误。为什么它报告没有安装 Python?运行以下命令将 SSH 到该服务器:

$ vagrant ssh ubuntu

使用 SSH 登录后,运行which python将显示 Python 二进制文件的路径。正如你所看到的,由于没有返回路径,所以没有安装。那 Python 3 呢?运行which python3确实返回了一个二进制文件:

通过运行exit来关闭我们的 SSH 会话。

我们应该怎么办?由于我们运行的 Ansible 版本晚于 2.2,我们可以告诉 Ansible 使用/usr/bin/python3而不是默认的/usr/bin/python。为此,我们需要更新我们的主机清单文件,以便只有 Ubuntu 主机添加ansible_python_interpreter变量以及更新后的路径。

有几种方法可以实现这一点;然而,现在,让我们只更新production主机清单文件中的以下行:

ubuntu ansible_host=192.168.50.7.nip.io

因此,它的读法如下:

ubuntu ansible_host=192.168.50.7.nip.io ansible_python_interpreter=/usr/bin/python3

更新后,我们应该能够运行以下命令:

$ ansible -i production wordpress -m setup | grep ansible_os_family 

以下截图显示了上述命令的输出:

正如你所看到的,我们正在针对wordpress主机组,其中包含我们的两个主机,并且预期地,CentOS 主机返回RedHat,而 Ubuntu 主机现在返回Debian。现在我们已经有了一种识别每个主机上使用的操作系统的方法,我们可以开始调整角色。

stack-install 角色

正如你可能已经猜到的,这个角色的大部分内容只是调用yum相关模块的任务,我们已经提到这将会改变。

我们要查看的角色的第一部分是roles/stack-install/tasks/main.yml文件的内容。目前,该文件包含使用yumyum_repository模块安装我们期望的仓库和软件包的任务。

我们需要更新文件,但首先,将现有内容另存为名为roles/stack-install/tasks/install-centos.yml的文件。一旦你复制了内容,更新roles/stack-install/tasks/main.yml,使其包含这些内容:

---

- name: include the operating system specific variables
  include_vars: "{{ ansible_os_family }}.yml"

- name: install the stack on centos
  import_tasks: install-centos.yml
  when: ansible_os_family == 'RedHat'

- name: install the stack on ubuntu
  import_tasks: install-ubuntu.yml
  when: ansible_os_family == 'Debian'

正如你所看到的,我们正在使用ansible_os_family变量来包含变量和不同的任务。

该任务将包括以下文件之一,具体取决于任务在哪个操作系统上执行:

  • roles/stack-install/vars/RedHat.yml

  • roles/stack-install/vars/Debian.yml

然后它将包含以下两个文件中的一个,这些文件包含了操作系统的任务:

  • install-centos.yml

  • install-ubuntu.yml

我们已经知道install-centos.yml包含了我们的main.yml文件的旧内容;由于软件包名称和仓库 URL 也将发生变化,我们应该将roles/stack-install/default/main.yml的内容移动到roles/stack-install/vars/RedHat.yml,并将roles/stack-install/default/main.yml留空。

现在我们已经定义了角色的 CentOS 部分,我们可以看一下 Ubuntu 部分,从roles/stack-install/vars/Debian.yml的内容开始:

---

repo_packages:
  - "deb [arch=amd64,i386] http://mirror.sax.uk.as61049.net/mariadb/repo/10.1/ubuntu {{ ansible_distribution_release }} main"
  - "deb http://nginx.org/packages/mainline/ubuntu/ {{ ansible_distribution_release }} nginx"
  - "deb-src http://nginx.org/packages/mainline/ubuntu/ {{ ansible_distribution_release }} nginx"

repo_keys:
  - { key_server: "keyserver.ubuntu.com", key: "0xF1656F24C74CD1D8" }

repo_keys_url:
  - "http://nginx.org/keys/nginx_signing.key"

system_packages:
  - "software-properties-common"
  - "python3-mysqldb"
  - "acl"

stack_packages:
  - "nginx"
  - "mariadb-server"
  - "php7.0"
  - "php7.0-cli"
  - "php7.0-fpm"
  - "php7.0-gd"
  - "php7.0-json"
  - "php7.0-mbstring"
  - "php7.0-mysqlnd"
  - "php7.0-soap"
  - "php7.0-xml"
  - "php7.0-xmlrpc"

extra_packages:
  - "vim"
  - "git"
  - "unzip"

正如你所看到的,虽然我们保留了system_packagesstack_packagesextra_packages变量,但其中包含了不同的软件包名称。在repo_packages中也有类似的情况,我们更新了 URL,因为 CentOS 仓库将无法在 Ubuntu 上使用。最后,我们引入了两个新变量,repo_keysrepo_keys_urls;我们很快将看到这些变量的用途。

我们需要处理的最后一个文件是roles/stack-install/tasks/install-ubuntu.yml。和install-centos.yml一样,这个文件包含了我们需要添加的额外仓库和安装软件包的任务。

首先,我们需要安装一些我们继续进行其余任务所需的工具;这些工具已经在system_packages变量中定义,所以我们只需要添加以下任务:

- name: update cache and install the system packages
  apt:
    name: "{{ item }}"
    update_cache: "yes"
  with_items: "{{ system_packages }}"

现在我们已经安装了基本的先决条件,我们可以为我们将要添加的仓库添加密钥:

- name: add the apt keys from a key server
  apt_key:
    keyserver: "{{ item.key_server }}"
    id: "{{ item.key }}"
  with_items: "{{ repo_keys }}"

- name: add the apt keys from a URL
  apt_key:
    url: "{{ item }}"
    state: present
  with_items: "{{ repo_keys_url }}"

第一个任务从官方 Ubuntu 密钥存储中添加密钥,第二个任务从 URL 下载密钥。在我们的情况下,我们为官方 MariaDB 仓库添加了一个密钥,为 NGINX 主线仓库添加了一个密钥;如果没有这些密钥,我们将无法添加仓库,会出现关于不受信任的错误。

添加仓库的任务如下;它循环遍历repo_packages变量中的仓库 URL:

- name: install the repo packages
  apt_repository:
    repo: "{{ item }}"
    state: "present"
    update_cache: "yes"
  with_items: "{{ repo_packages }}"

playbook 的最后一部分安装了剩余的软件包:

- name: install the stack packages
  apt:
    name: "{{ item }}"
    state: "installed"
  with_items: "{{ stack_packages + extra_packages }}"

现在我们已经更新了stack-install角色,我们需要对stack-config角色做同样的操作。

stack-config 角色

我们在这个角色中使用的大部分模块在我们的目标操作系统上都能正常工作,所以在这个角色中,我们只需要调整配置文件的路径等内容。我不会列出整个roles/stack-config/tasks/main.yml文件的内容,我只会强调需要进行的更改,从文件顶部开始:

- name: include the operating system specific variables
  include_vars: "{{ ansible_os_family }}.yml"

这将加载包含我们稍后在角色中需要使用的路径的变量;roles/stack-config/vars/RedHat.yml的内容是:

---

php_fpm_path: "/etc/php-fpm.d/www.conf"
php_ini_path: /etc/php.ini
php_service_name: "php-fpm"

roles/stack-config/vars/Debian.yml的内容是:

php_fpm_path: "/etc/php/7.0/fpm/pool.d/www.conf"
php_ini_path: "/etc/php/7.0/fpm/php.ini"
php_service_name: "php7.0-fpm"

正如你所看到的,我们需要进行的大部分更改是关于 PHP 配置文件的位置。在获取这些文件之前,我们需要在我们的roles/stack-config/tasks/main.yml文件中重新创建 WordPress 用户。因为在 Ubuntu 上,PHP-FPM 默认运行在不同的组下,所以没有创建 PHP-FPM 组,让我们创建一个,确保在add the wordpress user任务之前添加这些任务:

- name: add the wordpress group
  group: 
    name: "{{ wordpress_system.group }}"
    state: "{{ wordpress_system.state }}"

接下来,在 Ubuntu 上没有创建/var/www/文件夹,所以我们需要创建这个文件夹:

- name: create the global directory in /etc/nginx/
  file:
    dest: "/var/www/"
    state: "directory"
    mode: "0755"

在 CentOS 服务器上,组和文件夹已经存在,所以这些任务应该只显示ok。一旦它们被创建,用户将在两个服务器上都没有错误地创建,而且add the wordpress user任务也没有变化。

所有部署 NGINX 配置的任务都可以在不进行任何更改的情况下工作,所以我们可以继续进行 PHP 配置:

- name: copy the www.conf to /etc/php-fpm.d/
  template:
    src: "php-fpmd-www.conf.j2"
    dest: "{{ php_fpm_path }}"
  notify: "restart php-fpm"

- name: configure php.ini
  lineinfile: 
    dest: "{{ php_ini_path }}"
    regexp: "{{ item.regexp }}"
    line: "{{ item.replace }}"
    backup: "yes"
    backrefs: "yes"
  with_items: "{{ php.ini }}"
  notify: "restart php-fpm"

正如你所看到的,这两个任务都已经更新,包含了当前 playbook 目标操作系统相关的路径。

restart php-fpm 处理程序也已更新,因为两个操作系统上的 PHP-FPM 服务具有不同的名称;此任务应替换roles/stack-config/handlers/main.yml中的现有任务:

- name: "restart php-fpm"
  service:
    name: "{{ php_service_name }}"
    state: "restarted"
    enabled: "yes"

同样,在roles/stack-config/tasks/main.yml中,启动 PHP-FPM 的任务应根据此任务进行更新:

- name: start php-fpm
  service:
    name: "{{ php_service_name }}"
    state: "started"

接下来的两个更改是使以下任务仅在 CentOS 框上运行:

- name: configure the mariadb bind address
  lineinfile: 
    dest: "{{ mariadb.server_config }}"
    regexp: "#bind-address=0.0.0.0"
    line: "bind-address={{ mariadb.bind }}"
    backup: "yes"
    backrefs: "yes"
  when: ansible_os_family == 'RedHat'

这是因为 Ubuntu 上 MariaDB 的默认配置不包含bind-address,所以我们跳过它;下一个和最后一个任务如下:

- name: set the selinux allowing httpd_t to be permissive is required
  selinux_permissive:
    name: httpd_t
    permissive: true
  when: selinux.http_permissive == true and ansible_os_family == 'RedHat'

我们在 Ubuntu 框上跳过这一步,因为 SELinux 未安装并且与 Ubuntu 不兼容。

wordpress 角色

wordpress 角色有一些小的更改;第一个更改是更新roles/wordpress/defaults/main.yml

wordpress:
  domain: "http://{{ wordpress_domain }}/"
  title: "WordPress installed by Ansible on {{ os_family }}"

正如您所看到的,我们已将wordpress.domain更新为包含wordpress_domain变量,而wordpress.title现在包含os_family变量;我们通过在roles/wordpress/tasks/main.yml文件中添加以下任务来设置这两个变量:

- name: set a fact for the wordpress domain
  set_fact:
    wordpress_domain: "{{ ansible_ssh_host }}"
    os_family: "{{ ansible_os_family }}"

我们在这里这样做的原因是 Vagrant 没有正确设置我们的 Ubuntu 框的主机名为完全合格的域名,例如192.168.50.7.nip.io,因此我们使用在production清单主机文件中定义的我们正在 SSH 连接的主机。这个角色的其余部分保持不变。

运行 playbook

我们的site.yml文件没有任何更改,这意味着我们只需要运行以下命令来启动 playbook 运行:

$ ansible-playbook -i production site.yml

这将通过 playbook 运行,给出以下输出;请注意,我已经删除了 playbook 输出的一些部分:

PLAY [wordpress]

TASK [Gathering Facts]
ok: [centos]
ok: [ubuntu]

TASK [roles/stack-install : include the operating system specific variables] 
ok: [centos]
ok: [ubuntu]

TASK [roles/stack-install : install the repo packages] 
skipping: [ubuntu] => (item=[])
changed: [centos] => (item=[u'epel-release', u'https://centos7.iuscommunity.org/ius-release.rpm'])

TASK [roles/stack-install : add the NGINX mainline repo] 
skipping: [ubuntu]
changed: [centos]

TASK [roles/stack-install : update all of the installed packages] 
skipping: [ubuntu]
changed: [centos]

TASK [roles/stack-install : remove the packages so that they can be replaced] 
skipping: [ubuntu]
changed: [centos] => (item=[u'mariadb-libs.x86_64'])

TASK [roles/stack-install : install the stack packages] 
skipping: [ubuntu] => (item=[])
changed: [centos] => (item=[u'postfix', u'MySQL-python', u'policycoreutils-python', u'nginx', u'mariadb101u', u'mariadb101u-server', u'mariadb101u-config', u'mariadb101u-common', u'mariadb101u-libs', u'php72u', u'php72u-bcmath', u'php72u-cli', u'php72u-common', u'php72u-dba', u'php72u-fpm', u'php72u-fpm-nginx', u'php72u-gd', u'php72u-intl', u'php72u-json', u'php72u-mbstring', u'php72u-mysqlnd', u'php72u-process', u'php72u-snmp', u'php72u-soap', u'php72u-xml', u'php72u-xmlrpc', u'vim-enhanced', u'git', u'unzip'])

TASK [roles/stack-install : update cache and install the system packages] 
skipping: [centos] => (item=[])
changed: [ubuntu] => (item=[u'software-properties-common', u'python3-mysqldb', u'acl'])

TASK [roles/stack-install : add the apt keys from a key server] 
skipping: [centos]
changed: [ubuntu] => (item={u'key_server': u'keyserver.ubuntu.com', u'key': u'0xF1656F24C74CD1D8'})

TASK [roles/stack-install : add the apt keys from a URL] 
skipping: [centos]
changed: [ubuntu] => (item=http://nginx.org/keys/nginx_signing.key)

TASK [roles/stack-install : install the repo packages] 
skipping: [centos] => (item=epel-release)
skipping: [centos] => (item=https://centos7.iuscommunity.org/ius-release.rpm)
changed: [ubuntu] => (item=deb [arch=amd64,i386] http://mirror.sax.uk.as61049.net/mariadb/repo/10.1/ubuntu zesty main)
changed: [ubuntu] => (item=deb http://nginx.org/packages/mainline/ubuntu/ zesty nginx)
changed: [ubuntu] => (item=deb-src http://nginx.org/packages/mainline/ubuntu/ zesty nginx)

TASK [roles/stack-install : install the stack packages] 
skipping: [centos] => (item=[])
changed: [ubuntu] => (item=[u'nginx', u'mariadb-server', u'php7.0', u'php7.0-cli', u'php7.0-fpm', u'php7.0-gd', u'php7.0-json', u'php7.0-mbstring', u'php7.0-mysqlnd', u'php7.0-soap', u'php7.0-xml', u'php7.0-xmlrpc', u'vim', u'git', u'unzip'])

TASK [roles/stack-config : include the operating system specific variables] 
ok: [centos]
ok: [ubuntu]

TASK [roles/stack-config : add the wordpress group] 
ok: [centos]

TASK [roles/stack-config : create the global directory in /etc/nginx/] 
changed: [ubuntu]
ok: [centos]

TASK [roles/stack-config : add the wordpress user] 
changed: [centos]
changed: [ubuntu]

TASK [roles/stack-config : copy the nginx.conf to /etc/nginx/] 
changed: [ubuntu]
changed: [centos]

TASK [roles/stack-config : create the global directory in /etc/nginx/] 
changed: [ubuntu]
changed: [centos]

TASK [roles/stack-config : copy the restrictions.conf to /etc/nginx/global/] 
changed: [ubuntu]
changed: [centos]

TASK [roles/stack-config : copy the wordpress_shared.conf to /etc/nginx/global/] 
changed: [ubuntu]
changed: [centos]

TASK [roles/stack-config : copy the default.conf to /etc/nginx/conf.d/] 
changed: [ubuntu]
changed: [centos]

TASK [roles/stack-config : copy the www.conf to /etc/php-fpm.d/] 
changed: [ubuntu]
changed: [centos]

TASK [roles/stack-config : configure php.ini] 
changed: [ubuntu] => (item={u'regexp': u'^;date.timezone =', u'replace': u'date.timezone = Europe/London'})
changed: [centos] => (item={u'regexp': u'^;date.timezone =', u'replace': u'date.timezone = Europe/London'})
ok: [ubuntu] => (item={u'regexp': u'^expose_php = On', u'replace': u'expose_php = Off'})
changed: [centos] => (item={u'regexp': u'^expose_php = On', u'replace': u'expose_php = Off'})
changed: [ubuntu] => (item={u'regexp': u'^upload_max_filesize = 2M', u'replace': u'upload_max_filesize = 20M'})
changed: [centos] => (item={u'regexp': u'^upload_max_filesize = 2M', u'replace': u'upload_max_filesize = 20M'})

TASK [roles/stack-config : start php-fpm] 
changed: [ubuntu]
changed: [centos]

TASK [roles/stack-config : start nginx] 
changed: [ubuntu]
changed: [centos]

TASK [roles/stack-config : configure the mariadb bind address] 
skipping: [ubuntu]
changed: [centos]

TASK [roles/stack-config : start mariadb] 
ok: [ubuntu]
changed: [centos]

TASK [roles/stack-config : change mysql root password] 
changed: [centos] => (item=127.0.0.1)
changed: [ubuntu] => (item=127.0.0.1)
changed: [centos] => (item=::1)
changed: [ubuntu] => (item=::1)
changed: [ubuntu] => (item=192)
changed: [centos] => (item=192.168.50.6.nip.io)
changed: [ubuntu] => (item=localhost)
changed: [centos] => (item=localhost)

TASK [roles/stack-config : set up .my.cnf file] 
changed: [ubuntu]
changed: [centos]

TASK [roles/stack-config : delete anonymous MySQL user] 
ok: [ubuntu] => (item=127.0.0.1)
ok: [centos] => (item=127.0.0.1)
ok: [ubuntu] => (item=::1)
ok: [centos] => (item=::1)
ok: [ubuntu] => (item=192)
changed: [centos] => (item=192.168.50.6.nip.io)
ok: [ubuntu] => (item=localhost)
changed: [centos] => (item=localhost)

TASK [roles/stack-config : remove the MySQL test database] 
ok: [ubuntu]
changed: [centos]

TASK [roles/stack-config : set the selinux allowing httpd_t to be permissive is required] 
skipping: [ubuntu]
changed: [centos]

TASK [roles/wordpress : set a fact for the wordpress domain] 
ok: [centos]
ok: [ubuntu]

TASK [roles/wordpress : download wp-cli] 
changed: [ubuntu]
changed: [centos]

TASK [roles/wordpress : update permissions of wp-cli to allow anyone to execute it] 
changed: [ubuntu]
changed: [centos]

TASK [roles/wordpress : create the wordpress database] 
changed: [ubuntu]
changed: [centos]

TASK [roles/wordpress : create the user for the wordpress database] 
changed: [ubuntu] => (item=127.0.0.1)
changed: [centos] => (item=127.0.0.1)
ok: [ubuntu] => (item=::1)
ok: [centos] => (item=::1)
ok: [ubuntu] => (item=192)
ok: [centos] => (item=192.168.50.6.nip.io)
ok: [ubuntu] => (item=localhost)
ok: [centos] => (item=localhost)

TASK [roles/wordpress : are the wordpress files already there?] 
ok: [ubuntu]
ok: [centos]

TASK [roles/wordpress : download wordpresss] 
changed: [ubuntu]
changed: [centos]

TASK [roles/wordpress : set the correct permissions on the homedir] 
ok: [ubuntu]
changed: [centos]

TASK [roles/wordpress : is wordpress already configured?] 
ok: [centos]
ok: [ubuntu]

TASK [roles/wordpress : configure wordpress] 
changed: [ubuntu]
changed: [centos]

TASK [roles/wordpress : do we need to install wordpress?] 
fatal: [ubuntu]: FAILED! => 
...ignoring
fatal: [centos]: FAILED! => 
...ignoring

TASK [roles/wordpress : install wordpress if needed] 
changed: [ubuntu]
changed: [centos]

TASK [roles/wordpress : do we need to install the plugins?] 
failed: [ubuntu] (item=jetpack) => 
failed: [ubuntu] (item=wp-super-cache) => 
failed: [ubuntu] (item=wordpress-seo) => 
failed: [centos] (item=jetpack) => 
failed: [ubuntu] (item=wordfence) => 
failed: [centos] (item=wp-super-cache) => 
failed: [ubuntu] (item=nginx-helper) => 
failed: [centos] (item=wordpress-seo) => 
failed: [centos] (item=wordfence) => 
failed: [centos] (item=nginx-helper) =>

TASK [roles/wordpress : set a fact if we don't need to install the plugins] 
skipping: [centos]
skipping: [ubuntu]

TASK [roles/wordpress : set a fact if we need to install the plugins] 
ok: [centos]
ok: [ubuntu]

TASK [roles/wordpress : install the plugins if we need to or ignore if not] 
changed: [centos] => (item=jetpack)
changed: [ubuntu] => (item=jetpack)
changed: [ubuntu] => (item=wp-super-cache)
changed: [centos] => (item=wp-super-cache)
changed: [ubuntu] => (item=wordpress-seo)
changed: [centos] => (item=wordpress-seo)
changed: [ubuntu] => (item=wordfence)
changed: [centos] => (item=wordfence)
changed: [ubuntu] => (item=nginx-helper)
changed: [centos] => (item=nginx-helper)

TASK [roles/wordpress : do we need to install the theme?] 
fatal: [centos]: FAILED! => 
fatal: [ubuntu]: FAILED! =>

TASK [roles/wordpress : set a fact if we don't need to install the theme] 
skipping: [centos]
skipping: [ubuntu]

TASK [roles/wordpress : set a fact if we need to install the theme] 
ok: [centos]
ok: [ubuntu]

TASK [roles/wordpress : install the theme if we need to or ignore if not] 
changed: [centos]
changed: [ubuntu]

RUNNING HANDLER [roles/stack-config : restart nginx] 
changed: [ubuntu]
changed: [centos]

RUNNING HANDLER [roles/stack-config : restart php-fpm] 
changed: [ubuntu]
changed: [centos]

PLAY RECAP 
centos : ok=47 changed=37 unreachable=0 failed=0
ubuntu : ok=45 changed=33 unreachable=0 failed=0

一旦 playbook 完成,您应该能够在浏览器中访问http://192.168.50.6.nip.io/,并且您应该看到 WordPress 显示已安装在基于 Red Hat 的操作系统上:

访问http://192.168.50.7.nip.io/将显示相同的主题,但它应该说明它正在运行 Debian-based 操作系统,就像这个截图中一样:

您可以尝试重新运行 playbook,以查看返回的结果,并且您还可以通过运行以下命令删除 Vagrant 框:

$ vagrant destroy

您将被问及是否要逐个删除每台机器;只需对两个提示都回答“是”。

摘要

在本章中,我们已经调整了我们的 WordPress 安装 playbook,以针对多个操作系统。我们通过使用 Ansible 的内置审计模块来确定 playbook 正在针对哪个操作系统,并仅运行适用于目标操作系统的任务来实现这一点。

在下一章中,我们将开始查看一些处理网络的核心 Ansible 模块。

问题

  1. 真或假:我们需要仔细检查 playbook 中的每个任务,以确保它在两个操作系统上都能正常工作。

  2. 哪个配置选项允许我们定义 Python 的路径,Ansible 将使用?

  3. 解释为什么我们需要对配置和与 PHP-FPM 服务交互的任务进行更改。

  4. 真或假:每个操作系统的软件包名称完全对应。

  5. 更新 playbook,以便在每个不同的主机上安装不同的主题。

进一步阅读

您可以在www.ubuntu.com找到有关 Ubuntu 操作系统的更多信息。

第七章:核心网络模块

在本章中,我们将介绍随 Ansible 一起提供的核心网络模块。由于这些模块的要求,我们只会简要介绍这些模块提供的功能,并提供一些用例和示例。

本章将涵盖以下主题:

  • 核心网络模块

  • 与服务器本地防火墙交互

  • 与网络设备交互

技术要求

在本章中,我们将启动一个运行软件防火墙的 Vagrant 虚拟机。您需要安装 Vagrant 并访问互联网;Vagrant 虚拟机大小约为 400MB。我们将在本章中使用的完整版本 playbook 可以在github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter07/vyos上找到。

制造商和设备支持

到目前为止,我们一直在查看与服务器交互的模块。在我们的情况下,它们都在本地运行。在后面的章节中,我们将与远程托管的服务器进行通信。在开始与远程服务器交互之前,我们应该先了解核心网络模块。

这些模块都被设计用来与各种网络设备交互和管理配置,从传统的顶部交换机和完全虚拟化的网络基础设施到防火墙和负载均衡器。Ansible 支持许多非常不同的设备,从开源虚拟设备到根据配置可能成本超过 50 万美元的解决方案。

模块

我在这里列出了每个设备和操作系统。对于每个设备,都有一个加粗显示的简称。每个简称都是模块的前缀。例如,在第一个设备中,有一个名为a10_server的模块,用于使用 aXAPIv2 API 管理服务器负载均衡器SLB)对象。

A10 Networks

A10模块支持 A10 Networks AX、SoftAX、Thunder 和 vThunder 设备。这些都是应用交付平台,提供负载均衡。除其他功能外,这几个模块允许您在物理和虚拟设备上管理负载平衡和虚拟主机。

思科应用中心基础设施(ACI)

50 多个ACI模块用于管理思科的 ACI 的所有方面,这是可以预期的,因为这是思科的下一代 API 驱动的网络堆栈。

思科 AireOS

两个AireOS模块允许您与运行 AireOS 的思科无线局域网控制器进行交互。其中一个模块允许您直接在设备上运行命令,另一个用于管理配置。

Apstra 操作系统(AOS)

大约十几个AOS模块都标记为已弃用,因为它们不支持 AOS 2.1 或更高版本。这些模块将在即将发布的 Ansible 版本 2.9 之前被替换,确切地说。

Aruba 移动控制器

只有两个Aruba模块。这些允许您管理惠普的 Aruba 移动控制器的配置并执行命令。

思科自适应安全设备(ASA)

有三个ASA模块,您可以管理访问列表,并运行命令和管理物理和虚拟的思科 ASA 设备的配置。

Avi Networks

在撰写本文时,有 65 个Avi模块,允许您与 Avi 应用服务平台的所有方面进行交互,包括负载均衡和Web 应用防火墙WAF)功能。

Big Switch Networks

有三个 Big Switch Network 模块。其中一个Big Cloud FabricBCF)允许您创建和删除 BCF 交换机。另外两个模块允许您创建Big Monitoring FabricBig Mon)服务链和策略。

Citrix Netscaler

目前有一个已弃用的Netscaler模块。它将在 Ansible 2.8 中被移除。这给了您足够的时间转移到新模块。单一模块已被 14 个其他模块取代,这些模块允许您管理负载均衡器和 Citrix 安全设备中更多的功能。

华为 CloudEngine(CE)

有 65 多个CE模块,可以让您管理华为的这些强大交换机的所有方面,包括 BGP、访问控制列表、MTU、静态路由、VXLAN,甚至 SNMP 配置。

Arista CloudVision(CV)

有一个单一模块,可以让您使用配置文件配置 Arista CV服务器端口。

Lenovo CNOS

有 15 多个模块,可以让您管理运行联想CNOS操作系统的设备;它们允许您管理从 BGP 和端口聚合到 VLAG、VLAN,甚至工厂重置设备的所有内容。

Cumulus Linux(CL)

在八个CL中,有七个已被弃用,取而代之的是一个模块,使用网络命令行实用程序NCLU)与您的 Cumulus Linux 设备进行通信。

Dell 操作系统 10(DellOS10)

DellOS10有三个模块,可以让您在运行戴尔网络操作系统的设备上执行命令,管理配置并收集信息。还有Dell 操作系统 6DellOS6)和Dell 操作系统 9DellOS9)的模块。

Ubiquiti EdgeOS

有适用于EdgeOS的模块,可以让您管理配置,执行临时命令,并收集运行 EdgeOS 的设备(如 Ubiquiti EdgeRouter)的信息。

联想企业网络操作系统(ENOS)

有三个模块适用于联想ENOS。与其他设备一样,这些模块允许您收集信息,执行命令并管理配置。

Arista EOS

有 16 个模块,可以让您管理运行EOS的设备。这些模块允许您配置接口、VLAN、VRF、用户、链路聚合、静态路由,甚至日志记录。还有一个模块,可以让您从每个设备中收集信息。

F5 BIG-IP

有 65 个模块,所有这些模块都以BIG-IP为前缀,可以让您管理 F5 BIG-IP 应用交付控制器的所有方面。

FortiGate FortiManager

有一个单一模块,可以让您使用FortiManagerfmgr)添加、编辑、删除和执行脚本,针对您的 FortiGate 设备。

FortiGate FortiOS

作为核心网络模块的一部分,有三个模块可以让您管理 FortiGate FortiOS设备上的地址、配置和 IPv4 策略对象。

illumos

illumos是 OpenSolaris 操作系统的一个分支。它具有几个强大的网络功能,使其成为部署自建路由器或防火墙的理想选择。使用了三个前缀:dladmflowadmipadm。这些模块允许您管理接口、NetFlow 和隧道。此外,由于 illumos 是 OpenSolaris 的一个分支,您的 playbook 应该适用于基于 OpenSolaris 的操作系统。

Cisco IOS 和 IOS XR

有大约 25 个模块,可以让您管理您的 Cisco IOSIOS XR设备。使用它们,您可以收集设备信息,以及配置用户、接口、日志记录、横幅等。

Brocade IronWare

有三个通常的模块,可以帮助您管理您的 Brocade IronWare设备;您可以配置、运行临时命令和收集信息。

Juniper Junos

有 20 个模块,可以让您在 playbooks 中与运行Junos的 Juniper 设备进行交互。这些模块包括标准命令、配置和信息收集模块,以及允许您安装软件包并将文件复制到设备的模块。

Nokia NetAct

有一个单一模块,可以让您上传并应用您的 Nokia NetAct驱动的核心和无线电网络。

Pluribus Networks Netvisor OS

有超过十个模块允许您管理您的Pluribus NetworksPN)Netvisor OS 设备,从创建集群和路由器到在白盒交换机上运行命令。

Cisco Network Services Orchestrator (NSO)

有少数模块允许您与您的 Cisco NSO管理的设备进行交互。您可以执行 NSO 操作,从您的安装中查询数据,并在服务同步和配置方面验证您的配置。

Nokia Nuage Networks Virtualized Services Platform (VSP)

有一个单一模块允许您管理您的 Nokia Nuage Networks VSP 上的企业。

Cisco NX-OS (NXOS)

可以想象,有很多模块用于管理运行 Cisco NXOS的设备——超过 70 个。其中一些正在被弃用。有这么多模块,您可以获得这个强大网络操作系统所有功能的广泛覆盖。

Mellanox ONYX

有超过十几个模块允许您与 Mellanox 的交换机操作系统ONYX进行交互。您可以管理 BGP、L2 和 L3 接口,以及 LDAP。

Ordnance

有两个模块用于Ordnance Router as a Service;它们允许您应用配置更改并收集信息。

Open vSwitch (OVS)

有三个模块允许您管理OVS虚拟交换机上的桥接、端口和数据库。

Palo Alto Networks PAN-OS

有超过 20 个模块可以让您配置、管理和审计运行 PAN-OS(panos)的 Palo Alto Networks 设备。目前有一些模块正在被弃用;它们将在 Ansible 2.5 中停止作为核心模块分发。

Radware

最近推出的少量模块允许您通过vDirect服务器管理您的 Radware 设备。

Nokia Networks Service Router Operating System (SROS)

有三个模块允许您对 Nokia Networks 的SROS设备运行命令、配置和回滚更改。

VyOS

有十几个模块允许您管理VyOS开源 Linux 路由器和防火墙的大多数方面。我们将在下一节中看一下 VyOS。

系统

还有一些通用的net模块,允许您管理基于 Linux 的网络设备上的接口、Layer2 和 Layer3 配置、NETCONF、路由,以及 LLDP 服务。

与网络设备交互

正如在本章开头已经提到的,我们将使用 Vagrant 启动一个网络设备,然后运行一个 playbook 来应用基本配置。我们将要启动的设备是 VyOS。虽然设备将是完整的 VyOS 安装,但我们将只应用一个测试配置,以便让您了解我们在上一节提到的模块如何使用。

在附带本标题的 GitHub 存储库中有这个 playbook 的完整副本。

启动网络设备

为了做到这一点,我们将使用一个 VyOS Vagrant box。如果您在跟随,我们首先需要创建一个名为vyos的文件夹。这将保存我们的 playbook 和Vagrantfile。要创建所需的文件夹结构和空白文件,运行以下命令:

$ mkdir vyos vyos/group_vars vyos/roles
$ ansible-galaxy init vyos/roles/vyos-firewall
$ touch vyos/Vagrantfile
$ touch vyos/production
$ touch vyos/site.yml
$ touch vyos/group_vars/common.yml
$ touch vyos/roles/vyos-firewall/templates/firewall.j2 

将以下代码复制到我们创建的空白Vagrantfile中:

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
BOX_NAME    = "russmckendrick/vyos"
BOX_IP      = "192.168.50.10"
DOMAIN      = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY  = '~/.ssh/id_rsa.pub'

Vagrant.configure(API_VERSION) do |config|
  config.vm.box = BOX_NAME
  config.vm.network "private_network", ip: BOX_IP
  config.vm.host_name = BOX_IP + '.' + DOMAIN
  config.ssh.insert_key = false
  config.ssh.private_key_path = [PRIVATE_KEY, "~/.vagrant.d/insecure_private_key"]
  config.vm.provision "file", source: PUBLIC_KEY, destination: "~/.ssh/authorized_keys"

  config.vm.provider "virtualbox" do |v|
    v.memory = "2024"
    v.cpus = "2"
  end

  config.vm.provider "vmware_fusion" do |v|
    v.vmx["memsize"] = "2024"
    v.vmx["numvcpus"] = "2"
  end

end

正如您所看到的,Vagrantfile看起来与我们在以前章节中使用的并没有太大不同。现在让我们来看一下vyos_firewall角色。在执行和编写角色时有一些不同之处,我们在启动之前应该讨论一下。

VyOS 角色

在我们进入任务之前,让我们先看一下我们将要使用的变量。首先是roles/vyos-firewall/defaults/main.yml的内容:

---

motd_asciiart: |
  -----------------------------

  VyOS Ansible Managed Firewall 

  -----------------------------

vyos_nameservers:
  - 8.8.8.8
  - 8.8.4.4

在这里,我们只设置了两个关键值。第一个motd_asciiart是一个多行横幅,每当我们登录到 VyOS 设备时都会显示。我们使用|来声明关键字后,将变量设置为多行。下一个关键字vyos_nameservers是要使用的 DNS 解析器列表。在这里,我们使用 Google 的公共 DNS 解析器。

playbook 中还使用了一些其他变量;这些可以在group_vars/common.yml中找到,如下所示:

---

vyos:
  host: "192.168.50.10.nip.io"
  username: "vagrant"
  backup: "yes"
  inside:
    interface: "172.16.20.1/24"
    subnet: "172.16.20.0/24"

whitelist_ips:
  - 172.16.20.2

rules:
    - { action: 'set', source_address: '0.0.0.0/0', source_port: '80', destination_port: '80', destination_address: '172.16.20.11', protocol: 'tcp', description: 'NAT port 80 to 172.16.10.11', rule_number: '10' }
    - { action: 'set', source_address: '0.0.0.0/0', source_port: '443', destination_port: '443', destination_address: '172.16.20.11', protocol: 'tcp', description: 'NAT port 443 to 172.16.10.11', rule_number: '20' }
    - { action: 'set', source_address: '123.123.123.123/32', source_port: '222', destination_port: '22', destination_address: '172.16.20.11', protocol: 'tcp', description: 'NAT port 443 to 172.16.10.11', rule_number: '30' }

正如你所看到的,这些是可能根据我们的 playbook 运行位置而改变的大部分变量。首先,我们在一个名为vyos的嵌套变量中设置了我们设备的详细信息和基本配置。你可能已经注意到,我们在这里传递了我们 VyOS 设备的 IP 地址和用户名的详细信息,而不是我们的主机清单文件。

实际上,我们的主机清单文件名为production,应该只包含以下代码行:

localhost

这意味着当我们的 playbook 被执行时,它不会针对我们的 VyOS 设备执行。相反,playbook 将针对我们的 Ansible 控制器,并且模块将会针对 VyOS 设备。这种方法在所有核心网络模块中都很常见。正如我们已经讨论过的,Ansible 是一个无代理平台;默认情况下只需要 SSH 或 WinRM 连接。

然而,并非每个网络设备都具有 SSH 或 WinRM 访问权限;有些可能只有基于 Web 的 API,而其他一些可能使用专有的访问方法。另外,像 VyOS 这样的设备可能看起来具有 SSH 访问权限;但是,你实际上是在一个专门设计仅运行少量防火墙命令的自定义 shell 中进行 SSH。因此,大多数核心网络模块都会将它们的连接和通信管理远离主机清单文件。

group_vars/common.yml文件中的其余变量设置了一些基本防火墙规则,我们很快会看到。

可以在roles/vyos-firewall/tasks/main.yml中找到该角色的任务,它包含四个部分。首先,我们使用vyos_config模块来设置主机名。看一下这段代码:

- name: set the hostname correctly
  vyos_config:
    provider:
      host: "{{ vyos.host }}"
      username: "{{ vyos.username }}"
    lines:
      - "set system host-name {{ vyos.host }}"

正如你所看到的,我们使用provider选项传递了 VyOS 设备的详细信息;然后我们传递了一个vyos命令来设置主机名。vyos_config模块还接受模板文件,我们将在下一步中使用它来完全配置我们的设备。

下一个任务使用vyos_system模块配置 DNS 解析器。看一下这段代码:

- name: configure name servers
  vyos_system:
    provider:
      host: "{{ vyos.host }}"
      username: "{{ vyos.username }}"
    name_server: "{{ item }}"
  with_items: "{{ vyos_nameservers }}"

接下来,我们将使用vyos_banner模块设置每日消息MOTD)。看一下这段代码:

- name: configure the motd
  vyos_banner:
    provider:
      host: "{{ vyos.host }}"
      username: "{{ vyos.username }}"
    banner: "post-login"
    state: "present"
    text: "{{ motd_asciiart }}"

最后,我们将使用以下任务应用我们的主防火墙配置:

- name: backup and load from file
  vyos_config:
    provider:
      host: "{{ vyos.host }}"
      username: "{{ vyos.username }}"
    src: "firewall.j2"
    backup: "{{ vyos.backup }}"
    save: "yes"

与其使用lines提供命令,这次我们使用src来提供模板文件的名称。我们还指示模块备份当前配置;这将存储在roles/vyos-firewall/backup文件夹中,在 playbook 运行时创建。

模板可以在roles/vyos-firewall/templates/firewall.j2中找到。该模板包含以下代码:

set firewall all-ping 'enable'
set firewall broadcast-ping 'disable'
set firewall ipv6-receive-redirects 'disable'
set firewall ipv6-src-route 'disable'
set firewall ip-src-route 'disable'
set firewall log-martians 'enable'
set firewall receive-redirects 'disable'
set firewall send-redirects 'enable'
set firewall source-validation 'disable'
set firewall state-policy established action 'accept'
set firewall state-policy related action 'accept'
set firewall syn-cookies 'enable'
set firewall name OUTSIDE-IN default-action 'drop'
set firewall name OUTSIDE-IN description 'deny traffic from internet'
{% for item in whitelist_ips %}
set firewall group address-group SSH-ACCESS address {{ item }}
{% endfor %}
set firewall name OUTSIDE-LOCAL rule 310 source group address-group SSH-ACCESS
set firewall name OUTSIDE-LOCAL default-action 'drop'
set firewall name OUTSIDE-LOCAL rule 310 action 'accept'
set firewall name OUTSIDE-LOCAL rule 310 destination port '22'
set firewall name OUTSIDE-LOCAL rule 310 protocol 'tcp'
set firewall name OUTSIDE-LOCAL rule 900 action 'accept'
set firewall name OUTSIDE-LOCAL rule 900 description 'allow icmp'
set firewall name OUTSIDE-LOCAL rule 900 protocol 'icmp'
set firewall receive-redirects 'disable'
set firewall send-redirects 'enable'
set firewall source-validation 'disable'
set firewall state-policy established action 'accept'
set firewall state-policy related action 'accept'
set firewall syn-cookies 'enable'
set interfaces ethernet eth0 firewall in name 'OUTSIDE-IN'
set interfaces ethernet eth0 firewall local name 'OUTSIDE-LOCAL'
set interfaces ethernet eth1 address '{{ vyos.inside.interface }}'
set interfaces ethernet eth1 description 'INSIDE'
set interfaces ethernet eth1 duplex 'auto'
set interfaces ethernet eth1 speed 'auto'
set nat source rule 100 outbound-interface 'eth0'
set nat source rule 100 source address '{{ vyos.inside.subnet }}'
set nat source rule 100 translation address 'masquerade'
{% for item in rules if item.action == "set" %}
{{ item.action }} nat destination rule {{ item.rule_number }} description '{{ item.description }}'
{{ item.action }} nat destination rule {{ item.rule_number }} destination port '{{ item.source_port }}'
{{ item.action }} nat destination rule {{ item.rule_number }} translation port '{{ item.destination_port }}'
{{ item.action }} nat destination rule {{ item.rule_number }} inbound-interface 'eth0'
{{ item.action }} nat destination rule {{ item.rule_number }} protocol '{{ item.protocol }}'
{{ item.action }} nat destination rule {{ item.rule_number }} translation address '{{ item.destination_address }}'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} action 'accept'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} source address '{{ item.source_address }}'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} destination address '{{ item.destination_address }}'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} destination port '{{ item.destination_port }}'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} protocol '{{ item.protocol }}'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} state new 'enable'
{% endfor %}
{% for item in rules if item.action == "delete" %}
{{ item.action }} nat destination rule {{ item.rule_number }}
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }}
{% endfor %}

模板中有很多命令,其中大部分只是在设备上应用一些基本设置。我们感兴趣的是三个for循环。第一个循环如下:

{% for item in whitelist_ips %}
set firewall group address-group SSH-ACCESS address {{ item }}
{% endfor %}

这将简单地循环遍历我们在whitelist_ips变量中提供的每个 IP 地址,类似于我们在之前的 playbook 中使用with_items的方式。下一个循环更好地演示了这一点,它从firewall变量中获取变量,并创建 NAT 和防火墙规则。看一下这段代码:

{% for item in rules if item.action == "set" %}
{{ item.action }} nat destination rule {{ item.rule_number }} description '{{ item.description }}'
{{ item.action }} nat destination rule {{ item.rule_number }} destination port '{{ item.source_port }}'
{{ item.action }} nat destination rule {{ item.rule_number }} translation port '{{ item.destination_port }}'
{{ item.action }} nat destination rule {{ item.rule_number }} inbound-interface 'eth0'
{{ item.action }} nat destination rule {{ item.rule_number }} protocol '{{ item.protocol }}'
{{ item.action }} nat destination rule {{ item.rule_number }} translation address '{{ item.destination_address }}'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} action 'accept'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} source address '{{ item.source_address }}'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} destination address '{{ item.destination_address }}'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} destination port '{{ item.destination_port }}'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} protocol '{{ item.protocol }}'
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }} state new 'enable'
{% endfor %}

正如你所看到的,只有在我们将变量中的action设置为set时,才会包含该规则;最后一个循环处理任何将action设置为delete的规则,如下所示:

{% for item in rules if item.action == "delete" %}
{{ item.action }} nat destination rule {{ item.rule_number }}
{{ item.action }} firewall name OUTSIDE-IN rule {{ item.rule_number }}
{% endfor %}

如果您一直在跟进,那么除了site.yml文件之外,我们最初创建的所有文件中应该都包含内容。这个文件应该包含以下代码:

---

- hosts: localhost
  connection: local
  gather_facts: false

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/vyos-firewall

现在我们已经将 playbook 的所有部分准备好,我们可以启动 VyOS Vagrant box 并运行 playbook。

运行 playbook

要启动 Vagrant box,请确保您在我们在本章节开始时创建的vyos文件夹中,并运行以下两个命令中的一个来使用您选择的 hypervisor 启动 box:

$ vagrant up
$ vagrant up --provider=vmware_fusion

一旦您的 Vagrant box 启动,您可以使用以下命令运行 playbook:

$ ansible-playbook -i production site.yml

此 playbook 运行的输出应该类似于以下内容:

PLAY [localhost] ***********************************************************************************

TASK [roles/vyos-firewall : set the hostname correctly] ********************************************
changed: [localhost]

TASK [roles/vyos-firewall : configure name servers] ************************************************
changed: [localhost] => (item=8.8.8.8)
changed: [localhost] => (item=8.8.4.4)

TASK [roles/vyos-firewall : configure the motd] ****************************************************
changed: [localhost]

TASK [roles/vyos-firewall : backup and load from file] *********************************************
changed: [localhost]

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

完成后,您应该能够通过运行以下代码 SSH 到您的 VyOS 设备:

$ vagrant ssh

您应该能够看到登录横幅已更新为我们定义的横幅,如下截图所示:

在登录状态下,您可以通过运行以下命令查看 VyOS 配置:

$ show config

您应该能够在 playbook 运行中看到我们所做的所有更改,如下截图所示:

要停止查看配置,请按Q。您可以输入exit来离开 SSH 会话。您可以通过运行以下命令删除 VyOS Vagrant box:

$ vagrant destroy

正如本章节开头提到的,这个练习并不是关于使用 Ansible 配置一个完全功能的 VyOS 安装;相反,它提供了一个实际的例子,说明您可以如何使用 Ansible 模块配置网络设备,这些模块既能产生变化,又能使用模板应用配置。

总结

在本章中,我们简要介绍了作为 Ansible 核心模块一部分提供的各种网络模块。我们还将配置应用到虚拟的 VyOS 设备上,以了解网络模块与我们在之前章节中介绍的模块有何不同。

在下一章中,我们将学习如何使用 Ansible 启动基于云的服务器实例,然后将一些 playbooks 应用到它们上。

问题

  1. 真或假:您必须在模板中的for循环中使用with_items

  2. 哪个字符用于将您的变量分成多行?

  3. 真或假:在使用 VyOS 模块时,我们不需要在主机清单文件中传递设备的详细信息。

  4. 您能否将 VyOS 配置回滚到您存储的最早备份?

进一步阅读

每个设备和技术的详细信息,目前都由核心网络模块支持,都可以在以下链接中找到:

第八章:转移到云端

在本章中,我们将从使用本地虚拟机转移到使用 Ansible 在公共云提供商中启动实例。在本章中,我们将使用 DigitalOcean,我们选择这个提供商是因为它允许我们简单地启动虚拟机并与其交互,而不需要太多的配置开销。

然后,我们将研究如何调整我们的 WordPress playbook,以便与新启动的实例进行交互。

在本章中,我们将涵盖以下主题:

  • DigitalOcean 的简要介绍

  • 在 DigitalOcean 中启动实例

  • 如何在本地和远程之间切换运行 Ansible,以便我们可以部署 WordPress

技术要求

在本章中,我们将在公共云中启动实例,因此如果您正在跟随操作,您将需要一个 DigitalOcean 账户。与其他章节一样,playbook 的完整版本可以在github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter08Chapter08文件夹中找到。

与 DigitalOcean 交互

DigitalOcean 成立于 2011 年,从一个典型的虚拟专用服务器主机发展成为一个拥有全球数据中心的开发者友好的云服务提供商。Netcraft 指出,2012 年 12 月,DigitalOcean 托管了大约 100 个面向 Web 的服务器;到 2018 年 3 月,这个数字超过了 400,000 个,使 DigitalOcean 成为第三大面向 Web 的实例主机。

除了价格之外,DigitalOcean 之所以受到开发者的欢迎,还在于其性能;DigitalOcean 是最早提供全固态硬盘(SSD)实例存储的托管公司之一。它有易于使用的基于 Web 的控制面板,可以从命令行界面启动实例,还有强大的 API,允许您从应用程序内启动实例(DigitalOcean 称之为 Droplets),以及诸如 Ansible 之类的工具。

您可以在www.digitalocean.com/注册账户。注册后,在进行其他操作之前,我建议您首先在您的账户上配置双因素认证。

双因素认证(2FA)或多因素认证(MFA)为您的账户增加了额外的认证级别。通常,这是通过向与您的账户关联的设备发送短信验证码来实现的,或者将账户链接到第三方认证应用程序(如 Google 或 Microsoft Authenticator)来实现,该应用程序运行在您的智能手机上。与这些服务相关的账户通常需要您输入一个每 30 秒轮换一次的六位数字。

您可以通过转到 DigitalOcean 控制面板中的设置,然后点击左侧菜单中的安全来配置 2FA;一旦进入,按照屏幕上的说明启用您账户上的 2FA。

生成个人访问令牌

为了使我们的 playbook 能够在我们的 DigitalOcean 账户中启动 Droplet,我们需要生成一个个人访问令牌以与 DigitalOcean API 进行交互。要做到这一点,请点击 DigitalOcean 基于 Web 的控制面板顶部菜单中的 API 链接。

点击“生成新令牌”按钮将打开以下对话框:

如您所见,我已将我的令牌命名为Ansible,以便轻松识别。点击“生成令牌”按钮将创建一个令牌;它只会显示一次,所以请确保您记下来。

任何拥有您个人访问令牌副本的人都可以在您的 DigitalOcean 账户中启动资源;请确保将其保存在安全的地方,不要在任何地方发布您的令牌。

我已经在以下截图中模糊处理了我的令牌,但这应该让您了解在生成个人访问令牌后会看到什么:

现在我们有了令牌,但在开始 playbook 之前,我们还需要配置另一件事。

安装 dopy

我们将使用的一个模块需要一个名为dopy的 Python 模块;它是 DigitalOcean API 的包装器,可以使用以下pip命令进行安装:

$ sudo pip install dopy

安装了dopy之后,我们可以开始编写 playbook。

启动 Droplet

根据我们之前编写的 playbooks,您可以通过运行以下命令来创建骨架结构:

$ mkdir digitalocean digitalocean/group_vars digitalocean/roles
$ ansible-galaxy init digitalocean/roles/droplet
$ touch digitalocean/production digitalocean/site.yml digitalocean/group_vars/common.yml

我们需要完成两个任务来启动我们的 Droplet;首先,我们需要确保我们的公共 SSH 密钥的副本已上传到 DigitalOcean,以便我们可以在第二个任务期间将其注入到我们启动的 Droplet 中。

在我们继续查看启动 Droplet 的角色之前,我们应该弄清楚 playbook 需要访问 DigitalOcean API 的个人访问令牌要做什么。为此,我们将使用 Ansible Vault 对令牌进行编码;运行以下命令,确保用您自己的令牌替换encrypt_string的内容:

ansible-vault \
 encrypt_string 'pLgVbM2hswiLFWbemyD4Nru3a2yYwAKm2xbL6WmPBtzqvnMTrVTXYuabWbp7vArQ' \
 --name 'do_token'

本章中使用的令牌是随机生成的;请确保您用自己的替换它们。

以下截图显示了上述命令的输出:

如您所见,这返回了加密令牌,因此将加密令牌放入group_vars/common.yml文件中。在我们填充变量的同时,让我们看看roles/droplet/defaults/main.yml的内容应该是什么样的:

---
# defaults file for digitalocean/roles/droplet

key:
  name: "Ansible"
  path: "~/.ssh/id_rsa.pub"

droplet:
  name: "AnsibleDroplet"
  region: "lon1"
  size: "s-1vcpu-2gb"
  image: "centos-7-x64"
  timeout: "500"

有两个密钥值集合;第一个处理 SSH 密钥,playbook 将上传它,第二个包含启动 Droplet 的信息。我们初始 playbook 运行的默认设置将在 DigitalOcean 伦敦数据中心启动一个 1-CPU 核心、2 GB RAM、50 GB HDD 的 CentOS 7 Droplet。

启动 Droplet 的任务应该在roles/droplet/tasks/main.yml中包含两个独立的部分;第一部分处理上传 SSH 密钥,这是必须的,以便我们可以使用它来启动 Droplet:

- name: "upload SSH key to DigitalOcean"
  digital_ocean_sshkey:
    oauth_token: "{{ do_token }}"
    name: "{{ key.name }}"
    ssh_pub_key: "{{ item }}"
    state: present
  with_file: "{{ key.path }}"

如您所见,此任务使用了我们用 Ansible Vault 加密的令牌;我们还使用了with_file指令来复制密钥文件的内容,即~/.ssh/id_rsa.pub。根据您在 DigitalOcean 帐户中已有的内容,此任务将执行三种操作中的一种:

  • 如果密钥不存在,它将上传它

  • 如果一个密钥与~/.ssh/id_rsa.pub的指纹匹配但名称不同,那么它将重命名该密钥

  • 如果密钥和名称匹配,将不会上传或更改任何内容

现在我们知道我们已经上传了我们的密钥,我们需要知道它的唯一 ID。为了找出这一点,我们应该通过运行以下任务来收集我们在 DigitalOcean 帐户中配置的所有密钥的事实:

- name: "gather facts on all of the SSH keys in DigitalOcean"
  digital_ocean_sshkey_facts:
    oauth_token: "{{ do_token }}"

这将返回一个名为ssh_keys的 JSON 数组,其中包含密钥的名称,密钥的指纹,密钥本身的内容,以及密钥的唯一 ID;这些信息对于我们在 DigitalOcean 帐户中配置的每个密钥都会返回。由于我们只需要知道这些密钥中的一个 ID,我们需要操作结果以将列表过滤为我们上传的单个密钥,然后将 ID 设置为变量。

我们知道,我们有一个存储在ssh_keys值中的潜在密钥的 JSON 数组;对我来说,看起来像这样:

{
    "fingerprint": "9e:ad:42:e9:86:01:3c:5f:de:11:60:11:e0:11:9e:11",
    "id": 2663259,
    "name": "Work",
    "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAv2cUTYCHnGcwHYjVh3vu09T6UwLEyXEKDnv3039KStLpQV3H7PvhOIpAbY7Gvxi1t2KyqkOvuBdIat5fdQKzGQMEFZiwlcgWDVQGJBKuMH02w+ceMqNYaD8sZqUO+bQQwkUDt3PuDKoyNRzhcDLsc//Dp6wAwJsw75Voe9bQecI3cWqjT54n+oareqADilQ/nO2cdFdmCEfVJP4CqOmL1QLJQNe46yQoGJWLNa9VPC8/ffmUPnwJRWa9AZRPAQ2vGbDF6meSsFwVUfhtxkn+0bof7PFxrcaycGa3zqt6m1y6o3BDh29eFN94TZf9lUK/nQrXuby2/FhrMBrRcgWE4gQ== russ@work"
},
{
    "fingerprint": "7d:ce:56:5f:af:45:71:ab:af:fe:77:c2:9f:90:bc:cf",
    "id": 19646265,
    "name": "Ansible",
    "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russ@mckendrick.io"
}

您可能已经注意到,我已经执行了 playbook 并上传了我的密钥,以便我可以与您一起完成这个任务。现在我们需要找到名为key.name的密钥,对我们来说是Ansible,然后返回 ID。为此,我们将添加以下任务:

- name: "set the SSH key ID as a fact"
  set_fact:
    pubkey: "{{ item.id }}"
  with_items: "{{ ssh_keys | json_query(key_query) }}"
  vars:
    key_query: "[?name=='{{ key.name }}']"

正如你所看到的,我们正在使用set_fact模块创建一个名为pubkey的键值对;我们正在使用一个项目的 ID,并确保我们返回的只是一个项目,我们正在对我们的数组应用 JSON 查询。这个查询确保只返回包含key.name的 JSON 在with_items列表中;从这里我们可以取得单个项目的id,这使我们可以继续进行第二部分,即启动 Droplet。

现在我们知道要使用的 SSH 密钥的 ID,我们可以继续进行角色的第二部分。以下任务启动 Droplet:

- name: "launch the droplet"
  digital_ocean:
    state: "present"
    command: "droplet"
    name: "{{ droplet.name }}"
    unique_name: "yes"
    api_token: "{{ do_token }}"
    size_id: "{{ droplet.size }}"
    region_id: "{{ droplet.region }}"
    image_id: "{{ droplet.image }}"
    ssh_key_ids: [ "{{ pubkey }}" ]
    wait_timeout: "{{ droplet.timeout }}"
  register: droplet

使用digital_ocean模块启动 Droplet。大多数项目都是不言自明的;然而,有一个重要的选项我们必须设置一个值,那就是unique_name。默认情况下,unique_name设置为no,这意味着如果我们第二次运行我们的 playbook,将创建一个具有与我们启动的第一个 Droplet 完全相同细节的新 Droplet;第三次运行将创建第三个 Droplet。将unique_name设置为yes将意味着只有一个具有droplet.name值的 Droplet 在任一时间处于活动状态。

正如你所看到的,我们正在将任务的输出注册为一个值。关于 Droplet 的一些细节将作为任务执行的一部分返回;Droplet 的 IP 地址就是其中之一,因此我们可以使用它来设置一个事实,然后打印一个带有 IP 地址的消息:

- name: "set the droplet IP address as a fact"
  set_fact:
    droplet_ip: "{{ droplet.droplet.ip_address }}"

- name: "print the IP address of the droplet" 
  debug:
    msg: "The IP of the droplet is {{ droplet_ip }}"

这完成了基本的 playbook,一旦我们更新了site.yml文件,我们就可以运行它。这应该包含以下内容:

---

- hosts: localhost
  connection: local
  gather_facts: false

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/droplet

正如你所看到的,我们只是使用本地主机,因此不需要调用主机清单文件。

运行 playbook

由于我们有一个使用 Vault 加密的值,我们需要运行以下命令来运行 playbook:

$ ansible-playbook --vault-id @prompt site.yml

这将提示输入你设置的加密 Vault 的密码。一旦输入了密码,play 将运行:

PLAY [localhost] *****************************************************************************************************************************

TASK [roles/droplet : upload SSH key to DigitalOcean] ****************************************************************************************
changed: [localhost] => (item=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russ@mckendrick.io)

TASK [roles/droplet : gather facts on all of the SSH keys in DigitalOcean] *******************************************************************
ok: [localhost]

TASK [roles/droplet : set the SSH key ID as a fact] ******************************************************************************************
ok: [localhost] => (item={u'public_key': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russ@mckendrick.io', u'fingerprint': u'7d:ce:56:5f:af:45:71:ab:af:fe:77:c2:9f:90:bc:cf', u'id': 19646265, u'name': u'Ansible'})

TASK [roles/droplet : launch the droplet] ****************************************************************************************************
changed: [localhost]

TASK [roles/droplet : set the droplet IP address as a fact] **********************************************************************************
ok: [localhost]

TASK [roles/droplet : print the IP address of the droplet] ***********************************************************************************
ok: [localhost] => {
 "msg": "The IP of the droplet is 159.65.27.87"
}

PLAY RECAP ***********************************************************************************************************************************
localhost : ok=6 changed=2 unreachable=0 failed=0

正如你所看到的,这上传了我的密钥并启动了一个具有 IP 地址159.65.27.87的 Droplet(此 IP 现在不再被此 Droplet 使用)。这反映在 DigitalOcean 控制面板中,我们可以看到已添加的密钥:

你还可以在 Droplets 页面上看到 Droplet:

此外,你可以使用root用户名 SSH 登录 Droplet:

正如你所看到的,启动和与 DigitalOcean 交互是相对简单的。在继续下一部分之前,在 DigitalOcean 控制面板内销毁你的实例。

DigitalOcean 上的 WordPress

现在我们有一个启动 Droplet 的 playbook,我们将稍作调整,并在我们启动的 Droplet 上安装 WordPress。为此,复制刚才运行的 playbook 所在的文件夹,并将其命名为digitalocean-wordpress。从Chapter06/lemp-multi/roles文件夹中复制三个角色,stack-installstack-configwordpress

主机清单

我们要更改的第一个文件是名为 production 的主机清单文件;这需要更新为以下内容:

[droplets]

[digitalocean:children]
droplets

[digitalocean:vars]
ansible_ssh_user=root
ansible_ssh_private_key_file=~/.ssh/id_rsa
host_key_checking=False
ansible_python_interpreter=/usr/bin/python

这里有一个名为droplets的空主机组,然后我们为要启动的 Droplet 设置了一些全局变量。暂时不用担心添加实际的主机;我们将在运行 playbook 期间添加它。

变量

我们将要覆盖一些默认变量。为此,更新group_vars/common.yml文件,确保它读起来像这样,确保你更新do_token值以反映你自己的值:

do_token: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          63376236316336633631353131313363666463363834524609643522613230653265373236353664
          36653763373961313433373138633933663939452257345733336238353862383432373831393839
          32316262653963333836613332366639333039393066343739303066663262323337613937623533
          3461626330663363330a303538393836613835313166383030636134623530323932303266373134
          35616339376138636530346632345734563457326532376233323930383535303563323634336162
          31386635646636363334393664383633346636616664386539393162333062343964326561343861
          33613265616632656465643664376536653334653532336335306230363834523454245337626631
          33323730636562616631

droplet:
  name: "WordPress"
  region: "lon1"
  size: "s-1vcpu-2gb"
  image: "centos-7-x64"
  timeout: "500"

wordpress:
  domain: "http://{{ hostvars['localhost'].droplet_ip }}/"
  title: "WordPress installed by Ansible on {{ os_family }} host in DigitalOcean"
  username: "ansible"
  password: "AnsiblePasswordForDigitalOcean"
  email: "test@example.com"
  theme: "sydney"
  plugins:
    - "jetpack"
    - "wp-super-cache"
    - "wordpress-seo"
    - "wordfence"
    - "nginx-helper"

正如你所看到的,大多数值都是它们的默认值;我们正在更改的四个值是:

  • droplet.name:这是对名称的简单更新,这样我们就可以在 DigitalOcean 控制面板中轻松找到我们的实例。

  • wordpress.domain:这里的重要变化。正如您所看到的,我们使用了我们在 Ansible 控制器上设置的droplet_ip变量。为了使变量对我们的 WordPress 主机可用,我们告诉 Ansible 从 localhost 使用变量。如果我们没有这样做,那么变量就不会被设置;我们将在下一节中看到原因。

  • wordpress.title:对我们的 WordPress 站点配置的标题进行了轻微调整,以反映它所托管的位置。

  • wordpress.password:更改密码使其更复杂,因为我们在公开可用的 IP 地址上启动。

playbook

我们接下来要更改的文件是site.yml。这个文件需要更新以在本地和我们启动的 Droplet 上运行角色:

---

- name: Launch the droplet in DigitalOcean
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/droplet

- name: Install WordPress on the droplet
  hosts: digitalocean
  gather_facts: true

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/stack-install
    - roles/stack-config
    - roles/wordpress

我们更新的site.yml文件包含两个不同的 play:第一个在我们的 Ansible 控制器上运行,并与 DigitalOcean API 交互以启动 Droplet,第二个 play 然后连接到digitalocean组中的主机以安装 WordPress。那么 Ansible 如何知道要连接的主机的 IP 地址呢?

droplet 角色

我们需要做一个改变,droplet角色,可以在roles/droplet/tasks/main.yml中找到;这个改变将获取动态分配的 IP 地址,并将其添加到我们的droplets主机组中。为此,请替换以下任务:

- name: "print the IP address of the droplet" 
  debug:
    msg: "The IP of the droplet is {{ droplet_ip }}"

用以下任务替换它:

- name: add our droplet to a host group for use in the next step
  add_host:
    name: "{{ droplet_ip }}"
    ansible_ssh_host: "{{ droplet_ip }}"
    groups: "droplets"

正如你所看到的,这个任务使用droplet_ip变量,并使用add_host模块将主机添加到组中。

运行 playbook

现在我们已经将 playbook 的所有部分放在一起,我们可以通过运行以下命令启动 Droplet 并安装 WordPress:

$ ansible-playbook -i production --vault-id @prompt site.yml

启动 Droplet 并执行安装需要一些时间;在最后,您应该在 play 概述中列出 IP 地址,因为 IP 地址用作我们 Droplet 主机的名称。这是我的 playbook 运行的结尾:

RUNNING HANDLER [roles/stack-config : restart nginx] *****************************************************************************************
changed: [165.227.228.104]

RUNNING HANDLER [roles/stack-config : restart php-fpm] ***************************************************************************************
changed: [165.227.228.104]

PLAY RECAP ***********************************************************************************************************************************
165.227.228.104 : ok=47 changed=37 unreachable=0 failed=0
localhost : ok=7 changed=1 unreachable=0 failed=0

在浏览器中输入 IP 地址应该会呈现出类似以下页面的内容:

您应该能够使用我们在common.yml文件中设置的新密码登录。尝试安装 WordPress;当您准备好时,从 DigitalOcean 控制面板内销毁 Droplet。但请记住:保持 Droplet 运行将产生费用。

总结

在本章中,我们使用了 Ansible 云模块之一在公共云中启动了我们的第一个实例;正如您所看到的,这个过程相对简单,我们成功在云中启动了计算资源,然后安装了 WordPress,而没有对我们在第五章中涵盖的角色进行任何更改,部署 WordPress

在下一章中,我们将扩展本章涵盖的一些技术,并返回到网络,但与上一章不同,我们在上一章中涵盖了网络设备,我们将研究公共云中的网络。

问题

  1. 我们需要安装哪个 Python 模块来支持digital_ocean模块?

  2. 正确还是错误:您应该始终加密诸如 DigitalOcean 个人访问令牌之类的敏感值。

  3. 我们使用哪个过滤器来查找我们需要使用的 SSH 密钥的 ID?

  4. 解释为什么我们在digital_ocean任务中使用了unique_name选项。

  5. 从另一个 Ansible 主机访问变量的正确语法是什么?

  6. 正确还是错误:add_server模块用于将我们的 Droplet 添加到主机组。

  7. 尝试在 Ubuntu Droplet 上安装 WordPress;要使用的镜像 ID 是ubuntu-16-04-x64,不要忘记更改ansible_python_interpreter的值。

进一步阅读

您可以在trends.netcraft.com/www.digitalocean.com/上阅读有关 DigitalOcean 的 Netcraft 统计的更多详细信息。

第九章:构建云网络

现在我们已经在 DigitalOcean 上启动了服务器,我们将继续开始研究在 Amazon Web Services(AWS)内启动服务。

在启动实例之前,我们需要为它们创建一个网络。这称为 VPC,我们需要在 playbook 中汇集一些不同的元素来创建一个 VPC,然后我们就可以用于我们的实例。

在本章中,我们将:

  • 介绍 AWS

  • 介绍我们试图实现的目标和原因

  • 创建 VPC、子网和路由-网络和路由

  • 创建安全组-防火墙

  • 创建弹性负载均衡(ELB)-负载均衡器

技术要求

在本章中,我们将使用 AWS;您需要管理员访问权限才能创建所需的角色,以允许 Ansible 与您的帐户进行交互。与其他章节一样,您可以在附带的 GitHub 存储库的Chapter09文件夹中找到完整的 playbooks,网址为github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter09/vpc

AWS 简介

AWS 自 2002 年以来一直存在;它开始提供了一些毫不相关的服务,直到 2006 年初才重新推出。重新推出的 AWS 汇集了三项服务:

  • 亚马逊弹性计算云(Amazon EC2):这是 AWS 的计算服务

  • 亚马逊简单存储服务(Amazon S3):亚马逊的可扩展对象存储可访问服务

  • 亚马逊简单队列服务(Amazon SQS):该服务主要为 Web 应用程序提供消息队列

自 2006 年以来,它已经从三项独特的服务发展到了 160 多项,涵盖了 15 个主要领域,例如:

  • 计算

  • 存储

  • 数据库

  • 网络和内容传递

  • 机器学习

  • 分析

  • 安全、身份和合规性

  • 物联网

在 2018 年 2 月的财报电话会议上,透露出 AWS 在 2017 年的收入为 174.6 亿美元,占亚马逊总收入的 10%;对于一个最初只提供空闲计算时间共享的服务来说,这并不差。

在撰写本文时,AWS 覆盖了 18 个地理区域,总共拥有 54 个可用区域:aws.amazon.com/about-aws/global-infrastructure/

那么 AWS 的成功之处在哪里?不仅在于其覆盖范围,还在于其推出服务的方式。AWS 首席执行官 Andy Jassy 曾经说过:

“我们的使命是使任何开发人员或任何公司都能够在我们的基础设施技术平台上构建他们所有的技术应用。”

作为个人,您可以访问与大型跨国公司和亚马逊自身消费其服务相同的 API、服务、区域、工具和定价模型。这确实使您有自由从小规模开始并大规模扩展。例如,亚马逊 EC2 实例的价格从每月约 4.50 美元的 t2.nano(1 vCPU,0.5G)开始,一直到每月超过 19,000 美元的 x1e.32xlarge(128 vCPU,3,904 GB RAM,两个 1920 GB SSD 存储)-可以看出,有适用于各种工作负载的实例类型。

这两个实例和大多数服务都按照按使用计费,例如 EC2 实例按秒计费,存储按每月每 GB 计费。

亚马逊虚拟私有云概述

在本章中,我们将集中讨论启动 Amazon Virtual Private Cloud(Amazon VPC);这是将容纳我们将在下一章中启动的计算和其他 Amazon 服务的网络层。

我们即将启动的 VPC 概述如下:

正如您所看到的,我们将在EU-West #1(爱尔兰)地区启动我们的 VPC;我们将跨越我们的 EC2 实例和应用弹性负载均衡器的所有三个可用区。我们将仅使用两个可用区来启动我们的 Amazon 关系数据库服务RDS)实例,以及两个区域用于亚马逊弹性文件系统Amazon EFS)卷。

这意味着我们的 Ansible playbook 需要创建/配置以下内容:

  • 一个亚马逊 VPC

  • EC2 实例的三个子网

  • 两个用于 Amazon RDS 实例的子网

  • 用于 Amazon EFS 卷的两个子网

  • 应用负载均衡器的三个子网

  • 一个互联网网关

我们还需要配置以下内容:

  • 一条允许通过互联网网关访问的路由

  • 一个安全组,允许每个人访问应用负载均衡器上的端口80(HTTP)和443(HTTPS)

  • 一个安全组,允许 EC2 实例上的端口22(SSH)的受信任来源访问

  • 一个安全组,允许应用负载均衡器从 EC2 实例访问端口80(HTTP)

  • 一个安全组,允许 EC2 实例从 Amazon RDS 实例访问端口3306(MySQL)

  • 一个安全组,允许 EC2 实例从 Amazon EFS 卷访问端口2049(NGF)

这将为我们提供基本网络,允许对除了我们希望公开的应用负载均衡器之外的所有内容进行限制性访问。在我们开始创建部署网络的 Ansible playbook 之前,我们需要获取 AWS API 访问密钥和密钥。

创建访问密钥和秘密

为您自己的 AWS 用户创建访问密钥和秘密密钥,以便为 Ansible 提供对您的 AWS 帐户的完全访问权限是完全可能的。

因此,我们将尝试为 Ansible 创建一个用户,该用户只有权限访问我们知道 Ansible 将需要与本章涵盖的任务进行交互的 AWS 部分。我们将为 Ansible 提供以下服务的完全访问权限:

  • 亚马逊 VPC

  • 亚马逊 EC2

  • 亚马逊 RDS

  • 亚马逊 EFS

要做到这一点,请登录到 AWS 控制台,该控制台可以在console.aws.amazon.com/找到。登录后,单击顶部菜单中的“服务”。在打开的菜单中,输入IAM到搜索框中,然后单击应该是唯一结果的 IAM 管理用户访问和加密密钥。这将带您到一个类似以下内容的页面:

在 IAM 页面上,单击左侧菜单中的“组”;我们将创建一个具有分配权限的组,然后我们将创建一个用户并将其分配给我们的组。

一旦您进入组页面,单击“创建新组”按钮。此过程有三个主要步骤,第一个是设置组名。在提供的空间中,输入组名Ansible,然后单击“下一步”按钮。

下一步是我们附加策略的步骤;我们将使用亚马逊提供的策略。选择 AmazonEC2FullAccess,AmazonVPCFullAccess,AmazonRDSFullAccess 和 AmazonElasticFileSystemFullAccess;一旦选择了所有四个,单击“下一步”按钮。

您现在应该在一个页面上,该页面向您概述了您选择的选项;它应该看起来类似以下内容:

当您对您的选择感到满意时,请单击“创建组”按钮,然后单击左侧菜单中的“用户”。

一旦进入用户页面,单击“添加用户”,这将带您到一个页面,您可以在其中配置所需的用户名以及您想要的用户类型。输入以下信息:

  • 用户名:在此处输入Ansible

  • AWS 访问类型:勾选“程序化访问”旁边的复选框;我们的Ansible用户不需要 AWS 管理控制台访问权限,所以不要勾选该选项

现在您应该能够点击“下一步:权限”按钮;这将带您到设置用户权限的页面。由于我们已经创建了组,请从列表中选择Ansible组,然后点击“下一步:审阅”,这将带您到您输入的选项的概述页面。如果您对它们满意,然后点击“创建用户”按钮。

这将带您到一个看起来像以下内容的页面(我已经故意模糊了访问密钥 ID):

如您所见,成功消息告诉您这是您最后一次能够下载凭据,这意味着您将无法再次看到秘密访问密钥。要么点击“显示”按钮并记下密钥,要么点击“下载 .csv”按钮;您将无法恢复秘密访问密钥,只能让其过期并生成一个新的。

现在我们有了一个具有我们需要启动 VPC 的权限的用户的访问密钥 ID 和秘密访问密钥,我们可以开始编写 playbook。

VPC playbook

首先,我们需要讨论的是如何以安全的方式将访问密钥 ID 和秘密访问密钥传递给 Ansible。由于我将在 GitHub 上的公共存储库中分享最终的 playbook,我不想与世界分享我的 AWS 密钥,因为那可能会很昂贵!通常情况下,如果是私有存储库,我会使用 Ansible Vault 加密密钥,并将其与其他可能敏感的数据(如部署密钥等)一起包含在其中。

在这种情况下,我不想在存储库中包含任何加密信息,因为这意味着人们需要解密它,编辑值,然后重新加密它。幸运的是,Ansible 提供的 AWS 模块允许您在 Ansible 控制器上设置两个环境变量;这些变量将作为 playbook 执行的一部分读取。

要设置变量,请运行以下命令,确保您用自己的访问密钥和秘密替换内容(以下列出的信息仅为占位符值):

$ export AWS_ACCESS_KEY=AKIAI5KECPOTNTTVM3EDA $ export AWS_SECRET_KEY=Y4B7FFiSWl0Am3VIFc07lgnc/TAtK5+RpxzIGTr

设置好后,您可以通过运行以下命令查看内容:

$ echo $AWS_ACCESS_KEY

如您所见,这将显示AWS_ACCESS_KEY变量的内容:

现在我们有了一种将凭据传递给 Ansible 的方法,我们可以通过运行以下命令创建 playbook 结构:

$ mkdir vpc vpc/group_vars vpc/roles $ touch vpc/production vpc/site.yml vpc/group_vars/common.yml
$ cd vpc

现在我们已经有了基本的设置,我们可以开始创建角色;与以前的章节不同,我们将在添加每个角色后运行 playbook,以便我们可以更详细地讨论发生了什么。

VPC 角色

我们要创建的第一个角色是创建 VPC 本身的角色。我们将在接下来的角色中配置/创建的所有内容都需要托管在一个 VPC 中,因此需要先创建它,然后我们需要收集一些关于它的信息,以便我们可以继续进行 playbook 的其余部分。

要引导角色,请从您的工作文件夹中运行以下命令:

$ ansible-galaxy init roles/vpc

现在我们有了角色的文件,打开roles/vpc/tasks/main.yml并输入以下内容:

- name: ensure that the VPC is present
  ec2_vpc_net:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}"
    state: present
    cidr_block: "{{ vpc_cidr_block }}"
    resource_tags: { "Name" : "{{ environment_name }}", "Environment" : "{{ environment_name }}" }
  register: vpc_info

# - name: print the information we have registered
#   debug:
#     msg: "{{ vpc_info }}"

如您所见,我们使用了一个名为ec2_vpc_net的 Ansible 模块;这个模块替换了一个名为ec2_vpc的模块,后者在 Ansible 2.5 中已被弃用和移除。

我们在任务中使用了三个变量;前两个变量ec2_regionenvironment_name应该放在group_vars/common.yml中,因为我们将在大多数我们将创建的角色中使用它们:

environment_name: "my-vpc"
ec2_region: "eu-west-1"

这两个变量都是不言自明的:第一个是我们将用来引用我们将在 AWS 中启动的各种元素的名称,第二个让 Ansible 知道我们想要在哪里创建 VPC。

第三个变量vpc_cidr_block应该放在roles/vpc/defaults/main.yml文件中:

vpc_cidr_block: "10.0.0.0/16"

这定义了我们想要使用的 CIDR;10.0.0.0/16表示我们想要保留 10.0.0.1 到 10.0.255.254,这给了我们大约 65,534 个可用的 IP 地址范围,这应该足够我们的测试了。

在第一个任务结束时,我们使用注册标志来获取在创建 VPC 过程中捕获的所有内容,并将其注册为一个变量。然后我们使用 debug 模块将这些内容打印到屏幕上。

现在我们有了第一个角色,我们可以在site.yml文件中添加一些内容:

- name: Create and configure an Amazon VPC
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml
    - group_vars/firewall.yml
    - group_vars/secrets.yml
    - group_vars/words.yml
    - group_vars/keys.yml

  roles:
    - roles/vpc

然后使用以下命令运行 playbook:

$ ansible-playbook site.yml

这应该给你一个类似下面的输出:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'

PLAY [Create and configure an Amazon VPC] *******************************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/vpc : ensure that the VPC is present] ***********************************************
changed: [localhost]

TASK [roles/vpc : print the information we have registered] *************************************
ok: [localhost] => {
 "msg": {
 "changed": true,
 "failed": false,
 "vpc": {
 "cidr_block": "10.0.0.0/16",
 "cidr_block_association_set": [
 {
 "association_id": "vpc-cidr-assoc-1eee5575",
 "cidr_block": "10.0.0.0/16",
 "cidr_block_state": {
 "state": "associated"
 }
 }
 ],
 "classic_link_enabled": false,
 "dhcp_options_id": "dopt-44851321",
 "id": "vpc-ccef75aa",
 "instance_tenancy": "default",
 "is_default": false,
 "state": "available",
 "tags": {
 "Environment": "my-vpc",
 "Name": "my-vpc"
 }
 }
 }
}

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

检查 AWS 控制台的 VPC 部分应该会显示 VPC 已经创建,并且信息应该与 Ansible 捕获的信息匹配:

如果重新运行 playbook,你会注意到,Ansible 不会再次创建 VPC,而是会认识到已经有一个名为my-vpc的 VPC,并且会发现已经存在的 VPC 的信息,并填充vpc_info变量。这是有用的,因为我们将在下一个角色中使用收集到的信息。

子网角色

现在我们有了 VPC,我们可以开始填充它。我们要配置的第一件事是 10 个子网。如果你还记得,我们需要以下内容:

  • 三个 EC2 实例

  • 三个 ELB 实例

  • 两个 RDS 实例

  • 两个 EFS 实例

通过从你的工作目录运行以下命令来创建角色:

$ ansible-galaxy init roles/subnets

现在,在roles/subnets/defaults/main.yml中输入以下内容:

the_subnets:
  - { use: 'ec2', az: 'a', subnet: '10.0.10.0/24' }
  - { use: 'ec2', az: 'b', subnet: '10.0.11.0/24' }
  - { use: 'ec2', az: 'c', subnet: '10.0.12.0/24' }
  - { use: 'elb', az: 'a', subnet: '10.0.20.0/24' }
  - { use: 'elb', az: 'b', subnet: '10.0.21.0/24' }
  - { use: 'elb', az: 'c', subnet: '10.0.22.0/24' }
  - { use: 'rds', az: 'a', subnet: '10.0.30.0/24' }
  - { use: 'rds', az: 'b', subnet: '10.0.31.0/24' }
  - { use: 'efs', az: 'b', subnet: '10.0.40.0/24' }
  - { use: 'efs', az: 'c', subnet: '10.0.41.0/24' }

正如你所看到的,我们有一个包含子网用途(ec2elbrdsefs)、子网应该创建在哪个可用区(abc)以及子网本身的变量列表。在这里,我们为每个可用区使用了/24。

像这样分组子网应该消除一些在创建子网时的重复。然而,它并没有完全消除,因为我们可以从roles/subnets/tasks/main.yml的内容中看到:

- name: ensure that the subnets are present
  ec2_vpc_subnet:
    region: "{{ ec2_region }}"
    state: present
    vpc_id: "{{ vpc_info.vpc.id }}"
    cidr: "{{ item.subnet }}"
    az: "{{ ec2_region }}{{ item.az }}"
    resource_tags: 
      "Name" : "{{ environment_name }}_{{ item.use }}_{{ ec2_region }}{{ item.az }}"
      "Environment" : "{{ environment_name }}"
      "Use" : "{{ item.use }}"
  with_items: "{{ the_subnets }}"

任务开始时非常简单:在这里,我们使用ec2_vpc_subnet模块通过循环the_subnets变量来创建子网。正如你所看到的,我们使用了在上一个角色中注册的变量来正确地将子网部署到我们的 VPC 中;这就是vpc_info.vpc.id

你可能已经注意到,我们没有注册这个任务的结果;这是因为,如果我们这样做了,我们将会得到所有十个子网的信息。相反,我们希望根据子网的用途来分解这些信息。要找出这些信息,我们可以使用ec2_vpc_subnet_facts模块来根据我们在创建子网时设置的EnvironmentUse标签进行过滤来收集信息:

- name: gather information about the ec2 subnets
  ec2_vpc_subnet_facts:
    region: "{{ ec2_region }}"
    filters:
      "tag:Use": "ec2"
      "tag:Environment": "{{ environment_name }}"
  register: subnets_ec2

- name: gather information about the elb subnets
  ec2_vpc_subnet_facts:
    region: "{{ ec2_region }}"
    filters:
      "tag:Use": "elb"
      "tag:Environment": "{{ environment_name }}"
  register: subnets_elb

- name: gather information about the rds subnets
  ec2_vpc_subnet_facts:
    region: "{{ ec2_region }}"
    filters:
      "tag:Use": "rds"
      "tag:Environment": "{{ environment_name }}"
  register: subnets_rds

- name: gather information about the efs subnets
  ec2_vpc_subnet_facts:
    region: "{{ ec2_region }}"
    filters:
      "tag:Use": "efs"
      "tag:Environment": "{{ environment_name }}"
  register: subnets_efs

正如你所看到的,这里我们正在过滤使用和注册四组不同的信息:subnets_ec2subnets_elbsubnets_rdssubnets_efs。然而,我们还没有完成,因为我们只想知道子网 ID 而不是关于每个子网的所有信息。

为了做到这一点,我们需要使用set_fact模块和一些 Jinja2 过滤:

- name: register just the IDs for each of the subnets
  set_fact:
    subnet_ec2_ids: "{{ subnets_ec2.subnets | map(attribute='id') | list  }}"
    subnet_elb_ids: "{{ subnets_elb.subnets | map(attribute='id') | list  }}"
    subnet_rds_ids: "{{ subnets_rds.subnets | map(attribute='id') | list  }}"
    subnet_efs_ids: "{{ subnets_efs.subnets | map(attribute='id') | list  }}"

最后,我们可以通过将变量连接在一起来将所有的 ID 打印到屏幕上:

# - name: print all the ids we have registered
#   debug:
#     msg: "{{ subnet_ec2_ids + subnet_elb_ids + subnet_rds_ids
      + subnet_efs_ids }}"

现在我们已经把角色的所有部分准备好了,让我们运行它。更新site.yml文件,使其看起来像下面这样:

- name: Create and configure an Amazon VPC
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/vpc
    - roles/subnets

然后使用以下命令运行 playbook:

$ ansible-playbook site.yml

在运行 playbook 之前,我在 VPC 角色中注释掉了debug任务。你的输出应该看起来像接下来的输出;你可能已经注意到,VPC 角色返回了一个ok,因为我们的 VPC 已经存在:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Create and configure an Amazon VPC] *******************************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/vpc : ensure that the VPC is present] ***********************************************
ok: [localhost]

TASK [roles/subnets : ensure that the subnets are present] **************************************
changed: [localhost] => (item={u'subnet': u'10.0.10.0/24', u'use': u'ec2', u'az': u'a'})
changed: [localhost] => (item={u'subnet': u'10.0.11.0/24', u'use': u'ec2', u'az': u'b'})
changed: [localhost] => (item={u'subnet': u'10.0.12.0/24', u'use': u'ec2', u'az': u'c'})
changed: [localhost] => (item={u'subnet': u'10.0.20.0/24', u'use': u'elb', u'az': u'a'})
changed: [localhost] => (item={u'subnet': u'10.0.21.0/24', u'use': u'elb', u'az': u'b'})
changed: [localhost] => (item={u'subnet': u'10.0.22.0/24', u'use': u'elb', u'az': u'c'})
changed: [localhost] => (item={u'subnet': u'10.0.30.0/24', u'use': u'rds', u'az': u'a'})
changed: [localhost] => (item={u'subnet': u'10.0.31.0/24', u'use': u'rds', u'az': u'b'})
changed: [localhost] => (item={u'subnet': u'10.0.40.0/24', u'use': u'efs', u'az': u'b'})
changed: [localhost] => (item={u'subnet': u'10.0.41.0/24', u'use': u'efs', u'az': u'c'})

TASK [roles/subnets : gather information about the ec2 subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the elb subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the rds subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the efs subnets] *********************************
ok: [localhost]

TASK [roles/subnets : register just the IDs for each of the subnets] ****************************
ok: [localhost]

TASK [roles/subnets : print all the ids we have registered] *************************************
ok: [localhost] => {
 "msg": [
 "subnet-2951e761",
 "subnet-24ea4a42",
 "subnet-fce80ba6",
 "subnet-6744f22f",
 "subnet-64eb083e",
 "subnet-51f15137",
 "subnet-154ef85d",
 "subnet-19e9497f",
 "subnet-4340f60b",
 "subnet-5aea0900"
 ]
}

PLAY RECAP **************************************************************************************
localhost : ok=9 changed=1 unreachable=0 failed=0

唯一记录的更改是子网的添加;如果我们再次运行它,那么这也会返回一个ok,因为子网已经存在。正如你也可以看到的,我们返回了十个子网 ID,这也反映在 AWS 控制台中:

现在我们有了子网,我们需要确保 EC2 实例可以连接到互联网。

互联网网关角色

虽然互联网网关角色只会使用我们在common.yml中定义的变量,并通过收集之前任务中的信息,我们应该继续像之前一样继续引导roles文件夹:

$ ansible-galaxy init roles/gateway

在这个角色中,我们将使用两个模块;第一个模块ec2_vpc_igw创建互联网网关并对其进行标记:

- name: ensure that there is an internet gateway
  ec2_vpc_igw:
    region: "{{ ec2_region }}"
    vpc_id: "{{ vpc_info.vpc.id }}"
    state: present
    tags:
      "Name": "{{ environment_name }}_internet_gateway"
      "Environment": "{{ environment_name }}"
      "Use": "gateway"
  register: igw_info

然后我们将已注册的关于互联网网关的信息打印到屏幕上:

# - name: print the information we have registered
#   debug:
#     msg: "{{ igw_info }}"

在最终使用第二个模块ec2_vpc_route_table之前,我们创建一个路由,将所有目的地为0.0.0.0/0的流量发送到新创建的互联网网关,只针对 EC2 子网使用我们在之前角色中创建的 ID 列表:

- name: check that we can route through internet gateway
  ec2_vpc_route_table:
    region: "{{ ec2_region }}"
    vpc_id: "{{ vpc_info.vpc.id }}"
    subnets: "{{ subnet_ec2_ids + subnet_elb_ids }}"
    routes:
      - dest: 0.0.0.0/0
        gateway_id: "{{ igw_info.gateway_id }}"
    resource_tags:
      "Name": "{{ environment_name }}_outbound"
      "Environment": "{{ environment_name }}"

将角色添加到site.yml文件中:

- name: Create and configure an Amazon VPC
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/vpc
    - roles/subnets
    - roles/gateway

然后运行 playbook:

$ ansible-playbook site.yml

此时,由于我们已经运行了 playbook 三次,我应该快速提到警告。这是因为我们没有使用清单文件,而是在我们的site.yml文件的顶部定义了localhost。你应该收到类似以下输出的内容;我已经注释掉了之前角色中的调试任务:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Create and configure an Amazon VPC] *******************************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/vpc : ensure that the VPC is present] ***********************************************
ok: [localhost]

TASK [roles/subnets : ensure that the subnets are present] **************************************
ok: [localhost] => (item={u'subnet': u'10.0.10.0/24', u'use': u'ec2', u'az': u'a'})
ok: [localhost] => (item={u'subnet': u'10.0.11.0/24', u'use': u'ec2', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.12.0/24', u'use': u'ec2', u'az': u'c'})
ok: [localhost] => (item={u'subnet': u'10.0.20.0/24', u'use': u'elb', u'az': u'a'})
ok: [localhost] => (item={u'subnet': u'10.0.21.0/24', u'use': u'elb', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.22.0/24', u'use': u'elb', u'az': u'c'})
ok: [localhost] => (item={u'subnet': u'10.0.30.0/24', u'use': u'rds', u'az': u'a'})
ok: [localhost] => (item={u'subnet': u'10.0.31.0/24', u'use': u'rds', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.40.0/24', u'use': u'efs', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.41.0/24', u'use': u'efs', u'az': u'c'})

TASK [roles/subnets : gather information about the ec2 subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the elb subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the rds subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the efs subnets] *********************************
ok: [localhost]

TASK [roles/subnets : register just the IDs for each of the subnets] ****************************
ok: [localhost]

TASK [roles/gateway : ensure that there is an internet gateway] *********************************
changed: [localhost]

TASK [roles/gateway : print the information we have registered] *********************************
ok: [localhost] => {
 "msg": {
 "changed": true,
 "failed": false,
 "gateway_id": "igw-a74235c0",
 "tags": {
 "Environment": "my-vpc",
 "Name": "my-vpc_internet_gateway",
 "Use": "gateway"
 },
 "vpc_id": "vpc-ccef75aa"
 }
}

TASK [roles/gateway : check that we can route through internet gateway] *************************
changed: [localhost]

PLAY RECAP **************************************************************************************
localhost : ok=11 changed=2 unreachable=0 failed=0

回到 AWS 控制台。你应该能够查看到互联网网关:

在上面的截图中,你可以看到默认的 VPC 互联网网关,以及我们使用 Ansible 创建的互联网网关。你还可以看到我们创建的路由表:

在这里,你可以看到 Ansible 配置的路由,以及我们创建 VPC 时创建的默认路由。这个默认路由被设置为主要路由,并允许在我们之前添加的所有子网之间进行路由。

接下来,我们需要向我们的 VPC 添加一些安全组。

安全组角色

我们在这个角色中有一些不同的目标。第一个目标很简单:创建一个安全组,将端口80443对外开放,或者在 IP 术语中是0.0.0.0/0。第二个目标是创建一个允许 SSH 访问的规则,但只允许我们访问,第三个目标是确保只有我们的 EC2 实例可以连接到 RDS 和 EFS。

第一个目标很容易,因为0.0.0.0/0是一个已知的数量,其他的就不那么容易了。我们的 IP 地址经常会变化,所以我们不想硬编码它。而且,我们还没有启动任何 EC2 实例,所以我们不知道它们的 IP 地址。

让我们引导这个角色并创建第一组规则:

$ ansible-galaxy init roles/securitygroups

我们将使用ec2_group模块在roles/securitygroups/tasks/main.yml中创建我们的第一个组:

- name: provision elb security group
  ec2_group:
    region: "{{ ec2_region }}"
    vpc_id: "{{ vpc_info.vpc.id }}"
    name: "{{ environment_name }}-elb"
    description: "opens port 80 and 443 to the world"
    tags:
      "Name": "{{ environment_name }}-elb"
      "Environment": "{{ environment_name }}"
    rules:
      - proto: "tcp"
        from_port: "80"
        to_port: "80"
        cidr_ip: "0.0.0.0/0"
        rule_desc: "allow all on port 80"
      - proto: "tcp"
        from_port: "443"
        to_port: "443"
        cidr_ip: "0.0.0.0/0"
        rule_desc: "allow all on port 443"
  register: sg_elb

在这里,我们创建了一个名为my-vpc-elb的规则,对其进行标记,然后将端口804430.0.0.0/0开放。正如你所看到的,当你知道源 IP 地址很直接的时候,添加规则就很容易。现在让我们来看看为 EC2 实例添加规则;这个有点不同。

首先,我们不想让每个人都能访问我们实例上的 SSH,所以我们需要知道我们 Ansible 控制器的 IP 地址。为了做到这一点,我们将使用ipify_facts模块。

ipify 是一个免费的 web API,简单地返回你用来查询 API 的设备的当前公共 IP 地址。

正如接下来的任务所示,我们正在调用 ipify 的 API,然后设置一个包含 IP 地址的事实,然后将 IP 地址打印到屏幕上:

- name: find out your current public IP address using https://ipify.org/
  ipify_facts:
  register: public_ip

- name: set your public ip as a fact
  set_fact:
    your_public_ip: "{{ public_ip.ansible_facts.ipify_public_ip }}/32"

# - name: print your public ip address
#   debug:
#     msg: "Your public IP address is {{ your_public_ip }}"

现在我们知道要允许访问端口22的 IP 地址,我们可以创建一个名为my-vpc-ec2的规则:

- name: provision ec2 security group
  ec2_group:
    region: "{{ ec2_region }}"
    vpc_id: "{{ vpc_info.vpc.id }}"
    name: "{{ environment_name }}-ec2"
    description: "opens port 22 to a trusted IP and port 80 to the elb group"
    tags:
      "Name": "{{ environment_name }}-ec2"
      "Environment": "{{ environment_name }}"
    rules:
      - proto: "tcp"
        from_port: "22"
        to_port: "22"
        cidr_ip: "{{ your_public_ip }}"
        rule_desc: "allow {{ your_public_ip }} access to port 22"
      - proto: "tcp"
        from_port: "80"
        to_port: "80"
        group_id: "{{ sg_elb.group_id }}"
        rule_desc: "allow {{ sg_elb.group_id }} access to port 80"
  register: sg_ec2

my-vpc-ec2安全组中还有第二个规则;这个规则允许来自具有my-vpc-elb安全组附加的任何源的端口80的访问,而在我们的情况下,这将只是 ELB。这意味着任何人访问我们的 EC2 实例上的端口80的唯一方式是通过 ELB。

我们将使用相同的原则来创建 RDS 和 EFS 组,这次只允许访问端口33062049的实例在my-vpc-ec2安全组中:

- name: provision rds security group
  ec2_group:
    region: "{{ ec2_region }}"
    vpc_id: "{{ vpc_info.vpc.id }}"
    name: "{{ environment_name }}-rds"
    description: "opens port 3306 to the ec2 instances"
    tags:
      "Name": "{{ environment_name }}-rds"
      "Environment": "{{ environment_name }}"
    rules:
      - proto: "tcp"
        from_port: "3306"
        to_port: "3306"
        group_id: "{{ sg_ec2.group_id }}"
        rule_desc: "allow {{ sg_ec2.group_id }} access to port 3306"
  register: sg_rds

- name: provision efs security group
  ec2_group:
    region: "{{ ec2_region }}"
    vpc_id: "{{ vpc_info.vpc.id }}"
    name: "{{ environment_name }}-efs"
    description: "opens port 2049 to the ec2 instances"
    tags:
      "Name": "{{ environment_name }}-efs"
      "Environment": "{{ environment_name }}"
    rules:
      - proto: "tcp"
        from_port: "2049"
        to_port: "2049"
        group_id: "{{ sg_ec2.group_id }}"
        rule_desc: "allow {{ sg_ec2.group_id }} access to port 2049"
  register: sg_efs

现在我们已经创建了我们的主要组,让我们添加一个debug任务,将安全组 ID 打印到屏幕上:

# - name: print all the ids we have registered
#   debug:
#     msg: "ELB = {{ sg_elb.group_id }}, EC2 = {{ sg_ec2.group_id }}, RDS = {{ sg_rds.group_id }} and EFS = {{ sg_efs.group_id }}"

现在我们有了完整的角色,我们可以运行 playbook。记得在site.yml文件中添加- roles/securitygroups

$ ansible-playbook site.yml

同样,我已经注释掉了securitygroups角色之外的debug模块的任何输出:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Create and configure an Amazon VPC] *******************************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/vpc : ensure that the VPC is present] ***********************************************
ok: [localhost]

TASK [roles/subnets : ensure that the subnets are present] **************************************
ok: [localhost] => (item={u'subnet': u'10.0.10.0/24', u'use': u'ec2', u'az': u'a'})
ok: [localhost] => (item={u'subnet': u'10.0.11.0/24', u'use': u'ec2', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.12.0/24', u'use': u'ec2', u'az': u'c'})
ok: [localhost] => (item={u'subnet': u'10.0.20.0/24', u'use': u'elb', u'az': u'a'})
ok: [localhost] => (item={u'subnet': u'10.0.21.0/24', u'use': u'elb', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.22.0/24', u'use': u'elb', u'az': u'c'})
ok: [localhost] => (item={u'subnet': u'10.0.30.0/24', u'use': u'rds', u'az': u'a'})
ok: [localhost] => (item={u'subnet': u'10.0.31.0/24', u'use': u'rds', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.40.0/24', u'use': u'efs', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.41.0/24', u'use': u'efs', u'az': u'c'})

TASK [roles/subnets : gather information about the ec2 subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the elb subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the rds subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the efs subnets] *********************************
ok: [localhost]

TASK [roles/subnets : register just the IDs for each of the subnets] ****************************
ok: [localhost]

TASK [roles/gateway : ensure that there is an internet gateway] *********************************
ok: [localhost]

TASK [roles/gateway : check that we can route through internet gateway] *************************
ok: [localhost]

TASK [roles/securitygroups : provision elb security group] **************************************
changed: [localhost]

TASK [roles/securitygroups : find out your current public IP address using https://ipify.org/] **
ok: [localhost]

TASK [roles/securitygroups : set your public ip as a fact] **************************************
ok: [localhost]

TASK [roles/securitygroups : print your public ip address] **************************************
ok: [localhost] => {
 "msg": "Your public IP address is 109.153.155.197/32"
}

TASK [roles/securitygroups : provision ec2 security group] **************************************
changed: [localhost]

TASK [roles/securitygroups : provision rds security group] **************************************
changed: [localhost]

TASK [roles/securitygroups : provision efs security group] **************************************
changed: [localhost]

TASK [roles/securitygroups : print all the ids we have registered] ******************************
ok: [localhost] => {
 "msg": "ELB = sg-97778eea, EC2 = sg-fa778e87, RDS = sg-8e7089f3 and EFS = sg-7b718806"
}

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

您可以在 AWS 控制台中查看 Ansible 创建的组。在下面的截图中,您可以看到my-vpc-ec2安全组:

现在我们已经配置了基本的 VPC,我们可以开始在其中启动服务,首先是 Application Load Balancer。

ELB 角色

在本章中,我们将要查看的最后一个角色是启动 Application Load Balancer 的角色。嗯,它创建了一个目标组,然后将其附加到 Application Load Balancer 上。我们将使用这个角色创建的负载均衡器很基本;在后面的章节中,我们将会更详细地介绍。

与其他角色一样,我们首先需要引导文件:

$ ansible-galaxy init roles/elb

现在打开roles/elb/tasks/main.yml并使用elb_target_group模块创建目标组:

- name: provision the target group
  elb_target_group:
    name: "{{ environment_name }}-target-group"
    region: "{{ ec2_region }}"
    protocol: "http"
    port: "80"
    deregistration_delay_timeout: "15"
    vpc_id: "{{ vpc_info.vpc.id }}"
    state: "present"
    modify_targets: "false"

正如你所看到的,我们正在在我们的 VPC 中创建目标组,并将其命名为my-vpc-target-group。现在我们有了目标组,我们可以使用elb_application_lb模块启动 Application Elastic Balancer:

- name: provision an application elastic load balancer
  elb_application_lb:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-elb"
    security_groups: "{{ sg_elb.group_id }}"
    subnets: "{{ subnet_elb_ids }}"
    listeners:
      - Protocol: "HTTP" 
        Port: "80"
        DefaultActions:
          - Type: "forward" 
            TargetGroupName: "{{ environment_name }}-target-group"
    state: present
  register: loadbalancer

在这里,我们正在为我们的 VPC 中的 Application Load Balancer 创建一个名为my-vpc-elb的负载均衡器;我们正在传递我们使用subnet_elb_ids创建的 ELB 子网的 ID。我们还使用sg_elb.group_id将 ELB 安全组添加到负载均衡器,并在端口80上配置一个侦听器,将流量转发到my-vpc-target-group

任务的最后部分打印了我们关于 ELB 的信息:

# - name: print the information on the load balancer we have registered
#   debug:
#     msg: "{{ loadbalancer }}"

这完成了我们的最终角色;更新site.yml文件,使其如下所示:

- name: Create and configure an Amazon VPC
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/vpc
    - roles/subnets
    - roles/gateway
    - roles/securitygroups
    - roles/elb

我们现在可以通过运行以下命令最后一次运行我们的 playbook:

$ ansible-playbook site.yml

你可能猜到了 playbook 运行的输出将如下所示:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Create and configure an Amazon VPC] *******************************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/vpc : ensure that the VPC is present] ***********************************************
ok: [localhost]

TASK [roles/subnets : ensure that the subnets are present] **************************************
ok: [localhost] => (item={u'subnet': u'10.0.10.0/24', u'use': u'ec2', u'az': u'a'})
ok: [localhost] => (item={u'subnet': u'10.0.11.0/24', u'use': u'ec2', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.12.0/24', u'use': u'ec2', u'az': u'c'})
ok: [localhost] => (item={u'subnet': u'10.0.20.0/24', u'use': u'elb', u'az': u'a'})
ok: [localhost] => (item={u'subnet': u'10.0.21.0/24', u'use': u'elb', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.22.0/24', u'use': u'elb', u'az': u'c'})
ok: [localhost] => (item={u'subnet': u'10.0.30.0/24', u'use': u'rds', u'az': u'a'})
ok: [localhost] => (item={u'subnet': u'10.0.31.0/24', u'use': u'rds', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.40.0/24', u'use': u'efs', u'az': u'b'})
ok: [localhost] => (item={u'subnet': u'10.0.41.0/24', u'use': u'efs', u'az': u'c'})

TASK [roles/subnets : gather information about the ec2 subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the elb subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the rds subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the efs subnets] *********************************
ok: [localhost]

TASK [roles/subnets : register just the IDs for each of the subnets] ****************************
ok: [localhost]

TASK [roles/gateway : ensure that there is an internet gateway] *********************************
ok: [localhost]

TASK [roles/gateway : check that we can route through internet gateway] *************************
ok: [localhost]

TASK [roles/securitygroups : provision elb security group] **************************************
ok: [localhost]

TASK [roles/securitygroups : find out your current public IP address using https://ipify.org/] **
ok: [localhost]

TASK [roles/securitygroups : set your public ip as a fact] **************************************
ok: [localhost]

TASK [roles/securitygroups : provision ec2 security group] **************************************
ok: [localhost]

TASK [roles/securitygroups : provision rds security group] **************************************
ok: [localhost]

TASK [roles/securitygroups : provision efs security group] **************************************
ok: [localhost]

TASK [roles/elb : provision the target group] ***************************************************
changed: [localhost]

TASK [roles/elb : provision an application elastic load balancer] *******************************
changed: [localhost]

TASK [roles/elb : print the information on the load balancer we have registered] ****************
ok: [localhost] => {
 "msg": {
 "access_logs_s3_bucket": "",
 "access_logs_s3_enabled": "false",
 "access_logs_s3_prefix": "",
 "attempts": 1,
 "availability_zones": [
 {
 "subnet_id": "subnet-51f15137",
 "zone_name": "eu-west-1a"
 },
 {
 "subnet_id": "subnet-64eb083e",
 "zone_name": "eu-west-1c"
 },
 {
 "subnet_id": "subnet-6744f22f",
 "zone_name": "eu-west-1b"
 }
 ],
 "canonical_hosted_zone_id": "Z32O12XQLNTSW2",
 "changed": true,
 "created_time": "2018-04-22T16:12:31.780000+00:00",
 "deletion_protection_enabled": "false",
 "dns_name": "my-vpc-elb-374523105.eu-west-1.elb.amazonaws.com",
 "failed": false,
 "idle_timeout_timeout_seconds": "60",
 "ip_address_type": "ipv4",
 "listeners": [
 {
 "default_actions": [
 {
 "target_group_arn": "arn:aws:elasticloadbalancing:eu-west-1:687011238589:targetgroup/my-vpc-target-group/d5bab5efb2d314a8",
 "type": "forward"
 }
 ],
 "listener_arn": "arn:aws:elasticloadbalancing:eu-west-1:687011238589:listener/app/my-vpc-elb/98dd881c7a931ab3/3f4be2b480657bf9",
 "load_balancer_arn": "arn:aws:elasticloadbalancing:eu-west-1:687011238589:loadbalancer/app/my-vpc-elb/98dd881c7a931ab3",
 "port": 80,
 "protocol": "HTTP",
 "rules": [
 {
 "actions": [
 {
 "target_group_arn": "arn:aws:elasticloadbalancing:eu-west-1:687011238589:targetgroup/my-vpc-target-group/d5bab5efb2d314a8",
 "type": "forward"
 }
 ],
 "conditions": [],
 "is_default": true,
 "priority": "default",
 "rule_arn": "arn:aws:elasticloadbalancing:eu-west-1:687011238589:listener-rule/app/my-vpc-elb/98dd881c7a931ab3/3f4be2b480657bf9/c70feab5b31460c2"
 }
 ]
 }
 ],
 "load_balancer_arn": "arn:aws:elasticloadbalancing:eu-west-1:687011238589:loadbalancer/app/my-vpc-elb/98dd881c7a931ab3",
 "load_balancer_name": "my-vpc-elb",
 "routing_http2_enabled": "true",
 "scheme": "internet-facing",
 "security_groups": [
 "sg-97778eea"
 ],
 "state": {
 "code": "provisioning"
 },
 "tags": {},
 "type": "application",
 "vpc_id": "vpc-ccef75aa"
 }
}

PLAY RECAP ******************************************************************************************************************************
localhost : ok=19 changed=2 unreachable=0 failed=0

现在您应该能够在 AWS 控制台的 EC2 部分看到 ELB:

虽然 VPC 不会产生任何费用,但 ELB 会;请确保在完成测试后立即删除任何未使用的资源。

这结束了关于 VPC playbook 的本章;我们将在下一章中使用其中的元素,在那里我们将使用 VPC 作为我们安装的基础,将我们的 WordPress 安装部署到 AWS。

总结

在本章中,我们已经迈出了使用 Ansible 在公共云中启动资源的下一步。我们通过创建 VPC,设置我们应用程序所需的子网,配置互联网网关,并设置我们的实例通过它路由其出站流量,为自动化一个相当复杂的环境奠定了基础。

我们还配置了四个安全组,其中三个包含动态内容,以在最终在我们的 VPC 中为我们的服务提供安全保障之前,最终配置了一个 ELB。

在下一章中,我们将在本章奠定的基础上构建,并启动一组更复杂的服务。

问题

  1. AWS 模块用来读取您的访问 ID 和密钥的两个环境变量是什么?

  2. 真或假:每次运行 playbook,您都会得到一个新的 VPC。

  3. 说明为什么我们不费心注册创建子网的结果。

  4. 在定义安全组规则时,使用cidr_ipgroup_id有什么区别?

  5. 真或假:在使用定义了group_id的规则时,添加安全组的顺序并不重要。

  6. 在现有 VPC 旁边创建第二个 VPC,给它一个不同的名称,并且也让它使用 10.1.0.0/24。

进一步阅读

您可以在本章中使用的 AWS 技术的以下链接找到更多详细信息:

第十章:高可用云部署

继续我们的 AWS 部署,我们将开始将服务部署到上一章创建的网络中,并在本章结束时,我们将得到一个高可用的 WordPress 安装,我们将通过删除实例并向站点发送流量来进行测试。

在上一章创建的角色基础上,我们将进行以下操作:

  • 启动和配置 Amazon RDS(数据库)

  • 启动和配置 Amazon EFS(共享存储)

  • 启动和创建Amazon Machine ImageAMI)(部署 WordPress 代码)

  • 启动和配置启动配置和自动扩展组(高可用性)

技术要求

与上一章一样,我们将使用 AWS;您将需要在上一章中创建的访问密钥和秘密密钥来启动我们高可用的 WordPress 安装所需的资源。请注意,我们将启动会产生费用的资源。同样,您可以在附带的 GitHub 存储库的Chapter10文件夹中找到完整的操作手册github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter10/aws-wordpress

部署规划

在我们深入操作手册之前,我们应该了解我们试图实现的目标。正如前面提到的,我们将通过添加实例和存储来扩展我们的 AWS VPC 角色。我们更新的图表如下:

在图中,我们有以下内容:

  • 3 个 EC2 实例(t2.micro),每个可用区一个

  • 2 个 RDS 实例(t2.micro),处于主/备份多 AZ 配置中

  • 在三个可用区中共享 5GB 的 EFS 存储

在讨论部署本身之前,基于这里的图表和规格,这个部署将给我们带来多少成本?

部署成本

在 EU-West-1 地区运行此部署的成本如下:

实例类型 # 实例 每小时总成本 每天总成本 每月总成本
EC2 实例(t2.micro) 3 $0.038 | $0.091 $27.22
RDS 实例(t2.micro)-主和备份 2 $0.036 | $0.086 $25.92
应用负载均衡器 1 $0.033 | $0.80 $23.90
5 GB EFS 1 $0.002 | £0.06 | $1.65
总计: $0.109 | $2.62 $78.69

还会有一些其他小费用,如带宽和存储包含我们软件堆栈的 AMI 的成本。我们可以通过去除一些冗余,禁用多 AZ RDS 实例以及减少 EC2 实例的数量到只有一个来显著减少这些成本;然而,这开始引入了我们部署中的单点故障,这是我们不想要的。

WordPress 考虑因素和高可用性

到目前为止,我们一直在单个服务器上部署 WordPress,这很好,但是由于我们试图尽可能多地消除部署中的单点故障,这意味着我们必须对如何初始配置和启动我们的部署进行一些思考。

首先,让我们讨论一下我们需要按顺序启动部署的顺序。我们需要按照以下基本顺序启动元素:

  • VPC、子网、互联网网关、路由和安全组:这些都是启动我们部署所需的

  • 应用弹性负载均衡器:我们将使用弹性负载均衡器的公共主机名进行安装,因此需要在开始安装之前启动它

  • RDS 数据库实例:重要的是我们的数据库实例在启动安装之前是可用的,因为我们需要创建 WordPress 数据库并引导安装

  • EFS 存储:我们需要一些存储空间来在接下来启动的 EC2 实例之间共享

到目前为止,一切都很顺利;然而,这就是我们必须开始考虑 WordPress 的地方。

正如一些人可能从经验中知道的那样,当前版本的 WordPress 实际上并不是为在多个服务器上部署而设计的。有很多技巧和变通方法可以让 WordPress 在这种部署中表现良好;然而,本章并不是关于部署 WordPress 的细节。相反,它是关于如何使用 Ansible 部署多层 Web 应用程序的。

因此,我们将选择最基本的多实例 WordPress 选项,通过在 EFS 卷上部署我们的代码和内容。这意味着我们只需安装我们的 LEMP 堆栈。需要注意的是,这个选项并不是最高性能的,但它将满足我们的需求。

现在回到任务列表。在启动我们的实例时,我们需要做以下事情:

  • 启动运行 CentOS 7 的临时 EC2 实例,以便我们可以重用现有 playbook 的部分

  • 更新操作系统并安装我们安装和运行 WordPress 所需的软件堆栈、支持工具和配置

  • 挂载 EFS 卷并设置正确的权限,并配置它在启动时挂载

  • 将临时实例附加到我们的负载均衡器,并安装和配置 WordPress

  • 从我们的临时实例创建一个 AMI

  • 创建使用我们刚创建的 AMI 的启动配置

  • 创建一个自动扩展组并附加启动配置;它还应该将我们的 WordPress 实例注册到弹性负载均衡器

在初始 playbook 执行期间,由于我们创建 AMI,可能会有短暂的停机时间;进一步的 playbook 运行应该重复这个过程,现有的实例继续运行,一旦 AMI 构建完成,它应该与当前实例一起部署,然后一旦新实例注册到弹性负载均衡器并接收流量,当前实例将被终止。这将允许我们更新操作系统软件包和配置而无需任何停机时间——这也将模拟我们部署具有我们代码基础的 AMI;稍后在本章中会详细介绍。

现在我们知道我们要达到什么目标,让我们开始我们的 playbook。

playbook

playbook 将分为几个部分。在我们开始第一部分之前,让我们创建文件夹结构。与之前的章节一样,我们只需要运行以下命令:

$ mkdir aws-wordpress aws-wordpress/group_vars aws-wordpress/roles $ touch aws-wordpress/production aws-wordpress/site.yml aws-wordpress/group_vars/common.yml

现在我们的基本结构已经就位,我们可以开始创建角色,首先是网络。

Amazon VPC

在上一章中,创建底层网络的所有工作都已经完成,这意味着我们只需要将elbgatewaysecuritygroupssubnetsvpc文件夹从之前的 playbook 复制到当前的roles文件夹中。

复制后,更新site.yml文件,使其读取:

- name: Create and configure an Amazon VPC
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/vpc
    - roles/subnets
    - roles/gateway
    - roles/securitygroups
    - roles/elb

还要将以下内容添加到group_vars/common.yml文件中:

---
# the common variables

environment_name: "wordpress"
ec2_region: "eu-west-1"

最后,我们需要更新正在创建的子网;为此,请更新roles/subnets/defaults/main.yml中的the_subnets变量为:

the_subnets:
  - { use: 'ec2', az: 'a', subnet: '10.0.10.0/24' }
  - { use: 'ec2', az: 'b', subnet: '10.0.11.0/24' }
  - { use: 'ec2', az: 'c', subnet: '10.0.12.0/24' }
  - { use: 'elb', az: 'a', subnet: '10.0.20.0/24' }
  - { use: 'elb', az: 'b', subnet: '10.0.21.0/24' }
  - { use: 'elb', az: 'c', subnet: '10.0.22.0/24' }
  - { use: 'rds', az: 'a', subnet: '10.0.30.0/24' }
  - { use: 'rds', az: 'b', subnet: '10.0.31.0/24' }
  - { use: 'efs', az: 'a', subnet: '10.0.40.0/24' }
  - { use: 'efs', az: 'b', subnet: '10.0.41.0/24' }
  - { use: 'efs', az: 'c', subnet: '10.0.42.0/24' }

正如你所看到的,我们为我们的 EFS 卷添加了一个额外的子网,使其在所有三个可用区可用。稍后会详细介绍原因。然而,这确实展示了我们 playbook 的灵活性,当我们只需要向我们的变量添加一行来创建额外的子网时。

这完成了 playbook 的第一部分;现在我们可以继续前进到一些新的领域并启动我们的 Amazon RDS 实例。

Amazon RDS

让我们通过运行以下命令来创建角色的文件结构:

$ ansible-galaxy init roles/rds

现在位置已经确定,让我们讨论一下启动 RDS 实例需要做些什么。首先,我们需要定义一些默认值;将以下内容添加到roles/rds/defaults/main.yml文件中:

rds:
  db_username: "{{ environment_name }}"
  db_password: "{{ lookup('password', 'group_vars/rds_passwordfile chars=ascii_letters,digits length=30') }}"
  db_name: "{{ environment_name }}"
  app_instance_type: "db.t2.micro"
  engine: "mariadb"
  hdd_size: "5"
  no_of_backups: "7"
  multi_az: "yes"
  wait: "yes"
  wait_time: "1200"

一些变量是不言自明的,比如db_usernamedb_passworddb_name,尽管如此,正如你所看到的,我们正在对db_password的内容进行一些有趣的处理。我们并没有硬编码密码,而是使用了一个查找插件;这些插件允许 Ansible 读取外部数据,例如文件的内容、Redis、MongoDB 或各种 API。

在我们的情况下,我们正在使用 Ansible 密码查找插件来填充 Ansible 控制器上的一个文件,其中包含一个随机生成的密码;这个文件在后续查找时保持不变,这意味着密码可以被重复使用。Ansible 将生成一个包含字母和数字的 30 个字符长的密码,并将其放在group_vars/rds_passwordfile文件中。然后将此文件添加到.gitignore文件中,以便我们不会将密码发送到版本控制中。

需要注意的其他事项是,我们正在启动一个 db.t2.micro(app_instance_type)MariaDB(engine)实例,带有 5GB(hdd_size)的存储空间,采用多 AZ 配置(multi_az)。我们将保留 7 天的备份(no_of_backups),并且在实例首次启动时,我们将等待(wait)20 分钟(wait_time),直到实例在继续 playbook 的下一部分之前变为可用。

在启动 RDS 实例之前,我们需要做一件事,那就是创建一个 RDS 子网组;这是我们将 RDS 实例与我们在启动 VPC 时创建的子网关联起来的方法。在roles/rds/tasks/main.yml中,输入以下内容:

- name: create RDS subnet group
  rds_subnet_group:
    region: "{{ ec2_region }}"
    state: present
    name: "{{ environment_name }}_rds_group"
    description: "RDS Group for {{ environment_name }}"
    subnets: "{{ subnet_rds_ids }}"

此任务使用我们在subnets角色中注册的两个子网的列表,来创建一个名为wordpress_rds_group的组。在关联子网组与我们的 RDS 实例时,我们将使用其名称而不是其唯一 ID,因此无需为以后使用注册任务的输出。角色中的下一个,也是最后一个任务是启动 RDS 实例。输入以下rds_subnet_group任务:

- name: launch the rds instance
  rds:
    region: "{{ ec2_region }}"
    command: "create"
    instance_name: "{{ environment_name }}-rds"
    db_engine: "{{ rds.engine }}"
    size: "{{ rds.hdd_size }}"
    backup_retention: "{{ rds.no_of_backups }}"
    instance_type: "{{ rds.app_instance_type }}"
    multi_zone: "{{ rds.multi_az }}"
    subnet: "{{ environment_name }}_rds_group"
    vpc_security_groups: "{{ sg_rds.group_id }}"
    username: "{{ rds.db_username }}"
    password: "{{ rds.db_password }}"
    db_name: "{{ rds.db_name }}"
    wait: "{{ rds.wait }}"
    wait_timeout: "{{ rds.wait_time }}"
    tags:
      Name: "{{ environment_name }}-rds"
      Environment: "{{ environment_name }}"

除了command选项外,其他所有内容都是使用变量填充的——这意味着如果我们想要在重用角色时更改实例的任何部分,我们只需将默认变量复制到我们的group_vars/common.yml文件中进行覆盖。与 RDS 模块交互时,可以选择几个command选项,包括:

  • create:这将创建一个 RDS 实例。如果已经存在一个实例,模块将收集有关它的信息

  • replicate:这将创建一个你传递给它的 RDS 实例的只读副本

  • delete:这将删除 RDS 实例;在实例被删除之前,您可以选择进行快照

  • facts:收集有关 RDS 实例的信息

  • modify:如果您已更改配置的任何部分,那么这将更新您的实例,要么立即更新,要么在下一个预定的维护窗口期间更新

  • promote:这将将您的读取副本之一提升为新的主服务器

  • snapshot:这将创建您的 RDS 实例的手动快照

  • reboot:这将重新启动命名的 RDS 实例

  • restore:这将从命名快照创建一个新的 RDS 实例

当前 RDS 模块存在一些小问题,你可能需要考虑。其中最大的问题是,它目前只允许你启动由磁盘存储支持的 RDS 实例。可以添加一个任务,使用 AWS 命令行工具在实例启动后将存储迁移到通用 SSD;但是在这里我们不会涉及到这一点。

此外,尽管 Amazon Aurora 被列为一个选项,但 Ansible 目前还不支持它。同样,可以创建任务,使用 AWS 命令行工具来创建和配置 Aurora 集群,但如果你想要原生的 Ansible 支持,目前还没有这个运气。

Amazon Aurora 是亚马逊自己的数据库引擎,允许您在亚马逊定制的基于 SSD 的容错和自愈数据库存储集群上运行 MySQL 或 PostgreSQL 数据库。这种定制的存储架构使您可以将数据库扩展到 60TB 以上,而无需中断或重新组织数据集。

Ansible 社区正在进行工作,重构 RDS 模块以支持自定义存储选项,并引入对 Aurora 的本地支持。然而,这仍然是一个正在进行中的工作,尚未包含在当前的 Ansible 发布中(写作时为 2.5)。

这就是我们的 RDS 实例所需要的一切;我们可以继续进行下一个角色了。

亚马逊 EFS

创建 EFS 卷只需要三个任务;和之前的角色一样,我们可以使用ansible-galaxy命令来创建文件夹和文件结构:

$ ansible-galaxy init roles/efs

在添加任务之前,我们需要添加一些默认变量和一个模板,所以将以下内容添加到roles/efs/defaults/main.yml中:

efs:
  wait: "yes"
  wait_time: "1200"

现在,在roles/efs/templates中创建一个名为targets.j2的文件,文件内容应该包含:

---

efs_targets:
{% for item in subnet_efs_ids %}
      - subnet_id: "{{ item }}"
        security_groups: [ "{{ sg_efs.group_id }}" ]
{% endfor %}

正如你所看到的,这个模板正在循环遍历subnet_efs_ids变量,以创建一个子网 ID 和安全组的列表,存储在变量名efs_targets下;我们很快就会发现为什么需要这个。

roles/efs/tasks/main.yml中的第一个任务使用template模块读取之前的文件创建一个文件并将其存储在group_vars文件夹中,第二个任务使用include_vars模块加载文件的内容:

- name: generate the efs targets file
  template:
    src: "targets.j2"
    dest: "group_vars/generated_efs_targets.yml"

- name: load the efs targets
  include_vars: "group_vars/generated_efs_targets.yml"

现在我们已经填充并加载了efs_targets变量,我们可以添加第三个和最后一个任务;这个任务使用efs模块来创建卷:

- name: create the efs volume
  efs:
    region: "{{ ec2_region }}"
    state: present
    name: "{{ environment_name }}-efs"
    tags:
        Name: "{{ environment_name }}-efs"
        Environment: "{{ environment_name }}"
    targets: "{{ efs_targets }}"
    wait: "{{ efs.wait }}"
    wait_timeout: "{{ efs.wait_time }}"

“那么,为什么要费力创建模板,生成文件,然后加载内容,而不直接使用with_items呢?”你可能会问自己。

如果我们使用with_items,那么我们的任务将如下所示:

- name: create the efs volume
  efs:
    region: "{{ ec2_region }}"
    state: present
    name: "{{ environment_name }}-efs"
    tags:
        Name: "{{ environment_name }}-efs"
        Environment: "{{ environment_name }}"
    targets:
      - subnet_id: "{{ item }}"
        security_groups: [ "{{ sg_efs.group_id }}" ]
    wait: "{{ efs.wait }}"
    wait_timeout: "{{ efs.wait_time }}"
  with_items: "{{ subnet_efs_ids }}"

乍一看,这似乎应该可以工作;然而,如果我们看一下group_vars/generated_efs_targets.yml生成后的示例,您可能会注意到一个重要的区别:

efs_targets:
      - subnet_id: "subnet-0ce64b6a"
        security_groups: [ "sg-695f8b14" ]
      - subnet_id: "subnet-2598747f"
        security_groups: [ "sg-695f8b14" ]
      - subnet_id: "subnet-ee3487a6"
        security_groups: [ "sg-695f8b14" ]

从示例中可以看出,我们有三个部分,每个部分都有一个唯一的subnet_id对应一个可用区。如果我们使用with_items,我们只会有一个部分,并且任务会执行三次,每次都会覆盖之前的目标。当然,我们可以硬编码三个目标,但是如果我们决定在只有两个可用区的地区或者有四个可用区的地区重用角色会怎么样呢?硬编码意味着我们将失去让 Ansible 根据目标的动态结果范围动态适应情况的灵活性。

现在我们的 EFS 角色已经完成,基础知识也已经完成。在我们开始启动 EC2 实例之前,我们可以测试一下我们的 playbook。

测试 playbook

正如前面提到的,现在是测试我们已经完成的角色是否按预期工作的好时机。为了做到这一点,打开site.yml文件并添加以下内容:

---

- name: Create, launch and configure our basic AWS environment
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/vpc
    - roles/subnets
    - roles/gateway
    - roles/securitygroups
    - roles/elb
    - roles/rds
    - roles/efs

在运行 playbook 之前,我们需要设置AWS_ACCESS_KEYAWS_SECRET_KEY环境变量;为了做到这一点,运行以下命令,用之前章节生成的详细信息替换每个变量的值:

$ export AWS_ACCESS_KEY=AKIAI5KECPOTNTTVM3EDA $ export AWS_SECRET_KEY=Y4B7FFiSWl0Am3VIFc07lgnc/TAtK5+RpxzIGTr

我们将想要计时我们的 playbook 运行。为了做到这一点,我们可以在ansible-playbook命令前加上time前缀,这意味着我们需要运行的命令如下:

$ time ansible-playbook -i production site.yml

不要忘记,我们已经告诉 Ansible 在启动 RDS 实例和创建 EFS 卷之前等待最多 20 分钟,因此初始 playbook 运行可能需要一些时间。

原因是当 RDS 实例启动时,首先创建,然后克隆到备用服务器,最后进行初始备份。只有完成这些步骤后,RDS 实例才被标记为就绪,我们的 playbook 运行才能继续。此外,对于 EFS 卷,我们正在跨三个可用区创建三个卷的集群,因此需要一些时间来配置它们:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'

PLAY [Create, launch and configure our basic AWS environment] ************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/vpc : ensure that the VPC is present] ***********************************************
changed: [localhost]

TASK [roles/subnets : ensure that the subnets are present] **************************************
changed: [localhost] => (item={u'subnet': u'10.0.10.0/24', u'use': u'ec2', u'az': u'a'})
changed: [localhost] => (item={u'subnet': u'10.0.11.0/24', u'use': u'ec2', u'az': u'b'})
changed: [localhost] => (item={u'subnet': u'10.0.12.0/24', u'use': u'ec2', u'az': u'c'})
changed: [localhost] => (item={u'subnet': u'10.0.20.0/24', u'use': u'elb', u'az': u'a'})
changed: [localhost] => (item={u'subnet': u'10.0.21.0/24', u'use': u'elb', u'az': u'b'})
changed: [localhost] => (item={u'subnet': u'10.0.22.0/24', u'use': u'elb', u'az': u'c'})
changed: [localhost] => (item={u'subnet': u'10.0.30.0/24', u'use': u'rds', u'az': u'a'})
changed: [localhost] => (item={u'subnet': u'10.0.31.0/24', u'use': u'rds', u'az': u'b'})
changed: [localhost] => (item={u'subnet': u'10.0.40.0/24', u'use': u'efs', u'az': u'a'})
changed: [localhost] => (item={u'subnet': u'10.0.41.0/24', u'use': u'efs', u'az': u'b'})
changed: [localhost] => (item={u'subnet': u'10.0.42.0/24', u'use': u'efs', u'az': u'c'})

TASK [roles/subnets : gather information about the ec2 subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the elb subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the rds subnets] *********************************
ok: [localhost]

TASK [roles/subnets : gather information about the efs subnets] *********************************
ok: [localhost]

TASK [roles/subnets : register just the IDs for each of the subnets] ****************************
ok: [localhost]

TASK [roles/gateway : ensure that there is an internet gateway] *********************************
changed: [localhost]

TASK [roles/gateway : check that we can route through internet gateway] *************************
changed: [localhost]

TASK [roles/securitygroups : provision elb security group] **************************************
changed: [localhost]

TASK [roles/securitygroups : find out your current public IP address using https://ipify.org/] *****
ok: [localhost]

TASK [roles/securitygroups : set your public ip as a fact] **************************************
ok: [localhost]

TASK [roles/securitygroups : provision ec2 security group] **************************************
changed: [localhost]

TASK [roles/securitygroups : provision rds security group] **************************************
changed: [localhost]

TASK [roles/securitygroups : provision efs security group] **************************************
changed: [localhost]

TASK [roles/elb : provision the target group] ***************************************************
changed: [localhost]

TASK [roles/elb : provision an application elastic load balancer] *******************************
changed: [localhost]

TASK [roles/rds : create RDS subnet group] ******************************************************
changed: [localhost]

TASK [roles/rds : launch the rds instance] ******************************************************
changed: [localhost]

TASK [roles/efs : generate the efs targets file] ************************************************
changed: [localhost]

TASK [roles/efs : load the efs targets] *********************************************************
ok: [localhost]

TASK [roles/efs : create the efs volume] ********************************************************
changed: [localhost]

PLAY RECAP **************************************************************************************
localhost : ok=23 changed=14 unreachable=0 failed=0

从输出中可以看出,playbook 运行如预期般执行。我们可以检查 AWS 控制台,确保一切都已创建,从 VPC 开始:

然后,检查弹性负载均衡器,它可以在 EC2 部分找到:

我们还可以检查我们的 RDS 实例是否正在运行:

然后,我们 playbook 的最后部分是 EFS 卷:

当我运行 playbook 时,它花了 18 分钟多一点,如下面的输出所示:

正如预期的那样,大部分时间是 Ansible 等待 RDS 实例和 EFS 卷准备就绪。

现在我们知道 playbook 可以在没有错误的情况下启动我们的基础架构,我们可以继续进行 playbook 的其余部分。或者我们可以吗?

终止资源

正如本章开头已经提到的,我们正在启动资源,当它们运行时会产生费用。由于我们仍在编写 playbook,我们不希望资源在我们工作时空闲下来并累积成本,因此让我们创建一个支持的 playbook,撤消我们刚刚运行的一切。

为此,让我们创建一个名为remove的单个角色:

$ ansible-galaxy init roles/remove

这个角色将使用 Ansible 来删除我们刚刚启动的一切,从而在我们开发 playbook 时保持成本低廉。首先,我们需要向roles/remove/defaults/main.yml添加一些默认变量;它们是:

wait:
  wait: "yes"
  wait_time: "1200"

vpc_cidr_block: "10.0.0.0/16"

vpc_cidr_block变量应该与您的 VPC CIDR 匹配。现在,我们可以开始向roles/remove/tasks/main.yml添加任务,这将删除我们启动的所有内容。我们将按照资源启动的特定顺序逆向工作,这意味着我们需要按相反的顺序删除它们。所以让我们从 EFS 卷开始:

- name: remove the efs shares
  efs:
    region: "{{ ec2_region }}"
    state: absent
    name: "{{ environment_name }}-efs"
    wait: "{{ wait.wait }}"
    wait_timeout: "{{ wait.wait_time }}"

这次我们只需要提供一些细节,因为卷已经存在;我们需要给它卷的名称,以及stateabsent。您会注意到我们在继续之前等待卷完全被移除。在这个 playbook 中,我们将有相当多的暂停,以便在继续下一个任务之前,资源完全从 AWS API 中注销。

接下来的几个任务涉及删除 RDS 实例和 RDS 子网组:

- name: terminate the rds instance
  rds:
    region: "{{ ec2_region }}"
    command: "delete"
    instance_name: "{{ environment_name }}-rds"
    wait: "{{ wait.wait }}"
    wait_timeout: "{{ wait.wait_time }}"

- name: wait for 2 minutes before continuing
  pause:
    minutes: 2

- name: remove RDS subnet group
  rds_subnet_group:
    region: "{{ ec2_region }}"
    state: absent
    name: "{{ environment_name }}_rds_group"

如您所见,我们在 RDS 实例终止和移除 RDS 子网组之间使用pause模块暂停了 2 分钟。如果我们去掉这个暂停,那么我们就有可能 RDS 实例没有完全注销,这意味着我们将无法移除子网组,这将导致 playbook 出现错误。

如果在任何阶段 playbook 抛出错误,我们应该能够再次运行它,并且它应该能够从离开的地方继续。尽管在某个时候,我们将无法完全运行 playbook;我会告诉你这是什么时候。

现在 RDS 实例和子网组已被移除,我们可以移除弹性负载均衡器:

- name: terminate the application elastic load balancer
  elb_application_lb:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-elb"
    state: "absent"

- name: prompt
  pause:
    prompt: "Make sure the elastic load balancer has been terminated before proceeding"

您会注意到,这一次虽然再次使用了pause模块,但我们没有提供时间段。相反,我们正在指示用户检查 AWS 控制台,然后在弹性负载均衡器被移除后按下一个键。这是因为elb_application_lb模块不支持等待资源被移除。

如果您在资源正在被移除的过程中只是按下Enter,那么接下来的任务将立即失败,因此需要手动检查。该任务将移除 ELB 目标组:

- name: remove the target group
  elb_target_group:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-target-group"
    state: "absent"

接下来的任务将移除安全组;由于我们有引用其他组的组,因此在移除下一个组之前会有 30 秒的暂停

- name: remove the efs security group
  ec2_group:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-efs"
    state: "absent"

- name: wait for 30 seconds before continuing
  pause:
    seconds: 30

- name: remove the rds security group
  ec2_group:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-rds"
    state: "absent"

- name: wait for 30 seconds before continuing
  pause:
    seconds: 30

- name: remove the ec2 security group
  ec2_group:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-ec2"
    state: "absent"

- name: wait for 30 seconds before continuing
  pause:
    seconds: 30

- name: remove the elb security group
  ec2_group:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-elb"
    state: "absent"

- name: wait for 30 seconds before continuing
  pause:
    seconds: 30

同样,正如你所看到的,我们只需要提供组名和absent状态。下一个任务,移除路由表,需要的不仅仅是名字:

- name: get some facts on the route table
  ec2_vpc_route_table_facts:
    region: "{{ ec2_region }}"
    filters:
      "tag:Name": "{{ environment_name }}_outbound"
      "tag:Environment": "{{ environment_name }}"
  register: route_table_facts

- name: remove the route table
  ec2_vpc_route_table:
    region: "{{ ec2_region }}"
    vpc_id: "{{ route_table_facts.route_tables[0].vpc_id }}"
    route_table_id: "{{ route_table_facts.route_tables[0].id }}"
    lookup: "id"
    state: "absent"
  ignore_errors: yes

要移除路由表,我们需要知道 VPC ID 和路由表的 ID。为了获取这些信息,我们使用ec2_vpc_route_table_facts模块根据NameEnvironment标签来收集数据,这样我们只会移除我们打算移除的内容。这些信息随后传递给ec2_vpc_route_table模块,我们指示它使用路由表的 ID 来进行lookup

我们还告诉 Ansible 忽略这里生成的任何错误。原因是如果后续任务出现错误并且我们需要重新运行 playbook,我们需要它能够在 playbook 运行中继续进行,如果此任务已成功运行,它将无法继续进行,因为没有要移除的内容,这本身会生成错误。

接下来的两个任务收集有关 VPC 的信息并移除互联网网关:

- name: get some facts on the vpc
  ec2_vpc_net_facts:
    region: "{{ ec2_region }}"
    filters:
      "tag:Name": "{{ environment_name }}"
      "tag:Environment": "{{ environment_name }}"
  register: vpc_facts

- name: ensure that there isn't an internet gateway
  ec2_vpc_igw:
    region: "{{ ec2_region }}"
    state: "absent"
    vpc_id: "{{ vpc_facts.vpcs[0].vpc_id }}"
    tags:
      "Name": "{{ environment_name }}_internet_gateway"
      "Environment": "{{ environment_name }}"
  ignore_errors: yes

同样,我们忽略任何生成的错误,以便在需要多次执行时能够继续进行 playbook 运行。该任务使用ec2_vpc_subnet_facts模块收集环境中活动的子网信息;然后我们将这些信息注册为the_subnets

- name: gather information about the subnets
  ec2_vpc_subnet_facts:
    region: "{{ ec2_region }}"
    filters:
      "tag:Environment": "{{ environment_name }}"
  register: the_subnets

一旦我们获取了子网的信息,我们可以使用它们的 CIDR 块并将state设置为absent来移除它们:

- name: ensure that the subnets are absent
  ec2_vpc_subnet:
    region: "{{ ec2_region }}"
    state: "absent"
    vpc_id: "{{ vpc_facts.vpcs[0].vpc_id }}"
    cidr: "{{ item.cidr_block }}"
  with_items: "{{ the_subnets.subnets }}"

此时,如果您运行多次并且成功进行到这一步,playbook 将会生成错误。如果出现错误,您可以手动移除 VPC。

最后,现在我们已经从 VPC 中移除了所有内容,它是空的,这意味着我们可以无错误地移除 VPC 本身:

- name: ensure that the VPC is absent
  ec2_vpc_net:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}"
    state: "absent"
    cidr_block: "{{ vpc_cidr_block }}"

现在我们已经完成了我们的角色,我们可以创建一个名为remove.yml的 playbook,其中包含以下内容:

---

- name: Terminate everything in our basic AWS environment
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/remove

现在我们已经准备好移除 AWS 环境的所有部分;要做到这一点,请运行以下命令:

$ time ansible-playbook -i production remove.yml

不要忘记检查弹性负载均衡器是否已被移除,并在 playbook 运行期间按任意键继续。否则,您将需要等待一段时间。

当我运行 playbook 时,它花了将近 12 分钟:

如果您没有跟随 playbook 的输出,您可以在这里看到ec2_vpc_subnet_facts模块收集的所有暂停和子网信息:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'

PLAY [Terminate everything in our basic AWS environment] *****************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/remove : remove the efs shares] *****************************************************
changed: [localhost]

TASK [roles/remove : terminate the rds instance] ************************************************
changed: [localhost]

TASK [roles/remove : wait for 2 minutes before continuing] **************************************
Pausing for 120 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

TASK [roles/remove : remove RDS subnet group] ***************************************************
changed: [localhost]

TASK [roles/remove : terminate the application elastic load balancer] ***************************
changed: [localhost]

TASK [roles/remove : prompt] ********************************************************************
[roles/remove : prompt]
Make sure the elastic load balancer has been terminated before proceeding:

ok: [localhost]

TASK [roles/remove : remove the target group] ***************************************************
changed: [localhost]

TASK [roles/remove : remove the efs security group] *********************************************
changed: [localhost]

TASK [roles/remove : wait for 30 seconds before continuing] *************************************
Pausing for 30 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

TASK [roles/remove : remove the rds security group] *********************************************
changed: [localhost]

TASK [roles/remove : wait for 30 seconds before continuing] *************************************
Pausing for 30 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

TASK [roles/remove : remove the ec2 security group] *********************************************
changed: [localhost]

TASK [roles/remove : wait for 30 seconds before continuing] *************************************
Pausing for 30 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

TASK [roles/remove : remove the elb security group] *********************************************
changed: [localhost]

TASK [roles/remove : wait for 30 seconds before continuing] *************************************
Pausing for 30 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

TASK [roles/remove : get some facts on the route table] *****************************************
ok: [localhost]

TASK [roles/remove : remove the route table] ****************************************************
changed: [localhost]

TASK [roles/remove : get some facts on the vpc] *************************************************
ok: [localhost]

TASK [roles/remove : ensure that there isn't an internet gateway] *******************************
changed: [localhost]

TASK [roles/remove : gather information about the subnets] **************************************
ok: [localhost]

TASK [roles/remove : ensure that the subnets are absent] ****************************************
changed: [localhost] => (item={u'availability_zone': u'eu-west-1b', u'subnet_id': u'subnet-50259618', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'rds', u'Name': u'wordpress_rds_eu-west-1b'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.31.0/24', u'available_ip_address_count': 251, u'id': u'subnet-50259618', u'map_public_ip_on_launch': False})
changed: [localhost] => (item={u'availability_zone': u'eu-west-1a', u'subnet_id': u'subnet-80f954e6', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'elb', u'Name': u'wordpress_elb_eu-west-1a'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.20.0/24', u'available_ip_address_count': 251, u'id': u'subnet-80f954e6', u'map_public_ip_on_launch': False})
changed: [localhost] => (item={u'availability_zone': u'eu-west-1c', u'subnet_id': u'subnet-499f7313', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'ec2', u'Name': u'wordpress_ec2_eu-west-1c'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.12.0/24', u'available_ip_address_count': 251, u'id': u'subnet-499f7313', u'map_public_ip_on_launch': False})
changed: [localhost] => (item={u'availability_zone': u'eu-west-1a', u'subnet_id': u'subnet-74fc5112', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'ec2', u'Name': u'wordpress_ec2_eu-west-1a'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.10.0/24', u'available_ip_address_count': 251, u'id': u'subnet-74fc5112', u'map_public_ip_on_launch': False})
changed: [localhost] => (item={u'availability_zone': u'eu-west-1b', u'subnet_id': u'subnet-9f3a89d7', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'ec2', u'Name': u'wordpress_ec2_eu-west-1b'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.11.0/24', u'available_ip_address_count': 251, u'id': u'subnet-9f3a89d7', u'map_public_ip_on_launch': False})
changed: [localhost] => (item={u'availability_zone': u'eu-west-1c', u'subnet_id': u'subnet-8e967ad4', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'efs', u'Name': u'wordpress_efs_eu-west-1c'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.42.0/24', u'available_ip_address_count': 251, u'id': u'subnet-8e967ad4', u'map_public_ip_on_launch': False})
changed: [localhost] => (item={u'availability_zone': u'eu-west-1a', u'subnet_id': u'subnet-d7fe53b1', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'efs', u'Name': u'wordpress_efs_eu-west-1a'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.40.0/24', u'available_ip_address_count': 251, u'id': u'subnet-d7fe53b1', u'map_public_ip_on_launch': False})
changed: [localhost] => (item={u'availability_zone': u'eu-west-1c', u'subnet_id': u'subnet-029b7758', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'elb', u'Name': u'wordpress_elb_eu-west-1c'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.22.0/24', u'available_ip_address_count': 251, u'id': u'subnet-029b7758', u'map_public_ip_on_launch': False})
changed: [localhost] => (item={u'availability_zone': u'eu-west-1a', u'subnet_id': u'subnet-ede5488b', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'rds', u'Name': u'wordpress_rds_eu-west-1a'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.30.0/24', u'available_ip_address_count': 251, u'id': u'subnet-ede5488b', u'map_public_ip_on_launch': False})
changed: [localhost] => (item={u'availability_zone': u'eu-west-1b', u'subnet_id': u'subnet-ec3e8da4', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'efs', u'Name': u'wordpress_efs_eu-west-1b'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.41.0/24', u'available_ip_address_count': 251, u'id': u'subnet-ec3e8da4', u'map_public_ip_on_launch': False})
changed: [localhost] => (item={u'availability_zone': u'eu-west-1b', u'subnet_id': u'subnet-c227948a', u'assign_ipv6_address_on_creation': False, u'tags': {u'Environment': u'wordpress', u'Use': u'elb', u'Name': u'wordpress_elb_eu-west-1b'}, u'default_for_az': False, u'state': u'available', u'ipv6_cidr_block_association_set': [], u'vpc_id': u'vpc-7596f013', u'cidr_block': u'10.0.21.0/24', u'available_ip_address_count': 251, u'id': u'subnet-c227948a', u'map_public_ip_on_launch': False})

TASK [roles/remove : ensure that the VPC is absent] *********************************************
changed: [localhost]

PLAY RECAP **************************************************************************************
localhost : ok=23 changed=13 unreachable=0 failed=0

我建议您仔细检查 AWS 控制台上的资源是否已经移除,因为没有人喜欢意外的账单。现在我们已经完成并执行了我们的remove playbook,以便我们不会产生不必要的费用,我们可以继续构建我们的高可用 WordPress 安装。

EC2 实例

现在我们已经拥有了 WordPress 安装所需的所有基本服务,我们可以开始部署计算资源来安装 WordPress。这是一个有趣的地方,因为我们需要在 playbook 中构建逻辑,以便如果我们的站点正在运行,我们可以在没有任何停机时间的情况下部署操作系统更新并推出新的镜像。

但如果这是一个新的部署,我们需要启动一个实例,将其附加到弹性负载均衡器,安装软件堆栈,配置 WordPress,并创建一个我们随后可以在启动配置中使用的镜像,我们需要将其附加到自动扩展组。

虽然这可能看起来很复杂,但将这种逻辑构建到 playbook 中将使其更容易维护和移交给其他人来管理/运行,因为他们不需要担心现有的部署,他们只需要运行 playbook。

实例发现

我们将简单地称这个角色为 EC2,所以我们需要运行以下命令来创建角色结构:

$ ansible-galaxy init roles/ec2

这个角色的主要目标是确保在执行结束时,我们有一个实例,无论是新的还是现有的,然后我们可以在随后的角色中使用它来基于 AMI。

roles/ec2/defaults/main.yml中的默认值定义了如果我们的角色发现这是一个新的部署,我们想要使用哪个镜像。对于我们的安装,我们将使用 AWS Marketplace 中由 CentOS 提供的 AMI;这意味着我们可以重用我们的 WordPress 安装 playbook 的大部分内容:

image:
  base: "CentOS Linux 7 x86_64*"
  owner: "679593333241"
  root_device: "ebs"
  architecture: "x86_64"
wait_port: "22"
ec2_instance_type: "t2.micro"

当我们使用镜像时,我们将更详细地解释为什么我们需要这些信息。现在我们已经有了默认值,我们可以继续进行roles/ec2/tasks/main.yml中的任务。

当我们使用自动扩展组启动我们的实例时,它们都将被命名为wordpress_ec2,所以我们的 EC2 角色首先要做的事情就是弄清楚我们是否有任何正在运行的实例。为此,我们将使用ec2_instance_facts模块来收集任何正在运行并带有名称wordpress_ec2的标记的实例的信息:

- name: gather facts on any already running instances
  ec2_instance_facts:
    region: "{{ ec2_region }}"
    filters:
      instance-state-name: "running"
      "tag:environment": "{{ environment_name }}"
      "tag:Name": "{{ environment_name }}-ec2"
  register: running_instances

虽然我们现在已经获得了任何已经运行的实例的信息,但它实际上并不是我们可以使用的格式,所以让我们将结果添加到一个名为already_running的主机组中:

- name: add any already running instances to a group
  add_host:
    name: "{{ item.public_dns_name }}"
    ansible_ssh_host: "{{ item.public_dns_name }}"
    groups: "already_running"
  with_items: "{{ running_instances.instances }}"

现在,我们留下了一个名为already_running的主机组,它可能包含从零到三个主机;我们现在需要计算组中主机的数量,并设置一个包含主机数量的事实:

- name: set the number of already running instances as a fact
  set_fact:
    number_of_running_hosts: "{{ groups['already_running'] | length | default(0) }}"

在这里,我们使用内置的 Ansible 变量groups以及我们的组名;现在我们有了一个主机列表,我们可以通过使用length过滤器来计算列表中的项目数。最后,我们说如果列表为空,则默认值应为0

现在我们有一个包含number_of_running_hosts的变量,我们现在可以根据需要做出一些决定。

首先,如果number_of_running_hosts0,那么我们正在进行新的部署,我们应该运行启动新的 EC2 实例的任务:

- name: run the tasks for a new deployment 
  include_tasks: "new_deployment.yml"
  when: number_of_running_hosts|int == 0

或者,如果number_of_running_hosts大于1,那么我们需要选择一个已经运行的实例来使用:

- name: run the tasks for an existing deployment 
  include_tasks: "existing_deployment.yml"
  when: number_of_running_hosts|int >= 1

让我们来看看这些任务,从新部署时发生的情况开始。

新的部署

如果我们正在进行新的部署,那么我们需要执行以下任务:

  1. 找到我们正在使用的区域中最新的 CentOS 7 AMI

  2. 上传我们的公钥的副本,以便我们可以用它来 SSH 进入实例

  3. 使用先前的信息启动一个实例

  4. 将新实例添加到主机组

  5. 等待 SSH 可用

  6. 将我们的实例添加到弹性负载均衡器

所有这些任务都在roles/ec2/tasks/new_deployment.yml中定义,所以让我们开始通过查找如何找到正确的 AMI 来进行这些任务。

我们不能简单地在这里提供 AMI ID,因为每个区域都有不同的 ID,而且每个 AMI 都定期更新以确保它被修补。为了解决这个问题,我们可以运行以下任务:

- name: search for all of the AMIs in the defined region which match our selection
  ec2_ami_facts:
    region: "{{ ec2_region }}"
    owners: "{{ image.owner }}"
    filters:
      name: "{{ image.base }}"
      architecture: "{{ image.architecture }}"
      root-device-type: "{{ image.root_device }}" 
  register: amiFind

- name: filter the list of AMIs to find the latest one with an EBS backed volume
  set_fact:
    amiSortFilter: "{{ amiFind.images | sort(attribute='creation_date') | last }}"

- name: finally grab AMI ID of the most recent result which matches our base image which is backed by an EBS volume
  set_fact:
    our_ami_id: "{{ amiSortFilter.image_id }}"

正如你所看到的,我们首先寻找所有由 CentOS 创建的x86_64 AMI,名称中带有CentOS Linux 7 x86_64*,并且使用弹性块存储EBS)支持的存储。这将给我们提供有关几个 AMI 的详细信息,我们已经注册为amiFind

接下来,我们需要将 AMI 列表过滤为最新的一个,因此我们设置了一个名为amiSortFilter的事实。在这里,它正在获取镜像列表amiFind,并按创建日期对其进行排序。然后,我们只获取列表中最后一个 AMI 的信息,注册为amiSortFilter。最后,我们通过设置一个名为our_ami_id的事实,将信息进一步减少,它是amiSortFilter变量中的image_id,这样我们只保留了需要的信息。

现在我们知道了 AMI ID,我们需要确保有一个 SSH 密钥可以使用,以便在启动后访问实例。首先,让我们检查一下你在 Ansible 控制器上的用户是否有 SSH 密钥;如果我们找不到一个,那么将会创建一个:

- name: check the user {{ ansible_user_id }} has a key, if not create one
  user:
    name: "{{ ansible_user_id }}"
    generate_ssh_key: yes
    ssh_key_file: "~/.ssh/id_rsa"

现在我们已经确认了密钥的存在,我们需要将公共部分上传到 AWS:

- name: upload the users public key
  ec2_key:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-{{ ansible_user_id }}"
    key_material: "{{ item }}"
  with_file: "~/.ssh/id_rsa.pub"

现在我们已经准备好启动 EC2 实例了;为了做到这一点,我们将使用在 Ansible 2.5 中引入的ec2_instance模块:

- name: launch an instance
  ec2_instance:
    region: "{{ ec2_region }}"
    state: "present"
    instance_type: "{{ ec2_instance_type }}"
    image_id: "{{ our_ami_id }}"
    wait: yes
    key_name: "{{ environment_name }}-{{ ansible_user_id }}"
    security_groups: [ "{{ sg_ec2.group_id }}" ]
    network: 
      assign_public_ip: true
    filters:
      instance-state-name: "running"
      "tag:Name": "{{ environment_name }}-tmp"
      "tag:environment": "{{ environment_name }}"
    vpc_subnet_id: "{{ subnet_ec2_ids[0] }}"
    tags:
      Name: "{{ environment_name }}-tmp"
      environment: "{{ environment_name }}"

有了这些,我们就可以将我们的 EC2 实例启动到一个 EC2 子网中,附加一个公共 IP 地址和我们的 EC2 安全组。该实例将是一个名为wordpress-tmp的 t2.micro CentOS 7 实例。我们为其分配标签,并且我们还使用过滤器,以便如果在 playbook 运行期间发生任何问题,我们需要重新运行它,它将使用我们已经运行的实例,而不是启动另一个实例。

实例启动后,我们需要找出它的信息,并将其添加到名为ec2_instance的主机组中:

- name: gather facts on the instance we just launched using the AWS API
  ec2_instance_facts:
    region: "{{ ec2_region }}"
    filters:
      instance-state-name: "running"
      "tag:Name": "{{ environment_name }}-tmp"
      "tag:environment": "{{ environment_name }}"
  register: singleinstance

- name: add our temporary instance to a host group for use in the next step
  add_host:
    name: "{{ item.public_dns_name }}"
    ansible_ssh_host: "{{ item.public_dns_name }}"
    groups: "ec2_instance"
  with_items: "{{ singleinstance.instances }}"

我们需要等待 SSH 可访问后再继续;在这里,我们将使用wait_for模块:

- name: wait until SSH is available before moving onto the next step
  wait_for:
    host: "{{ item.public_dns_name }}"
    port: 22
    delay: 2
    timeout: 320
    state: "started"
  with_items: "{{ singleinstance.instances }}"

最后,一旦 SSH 可用,我们需要将实例注册到我们的弹性负载均衡器目标组中:

- name: add the instance to the target group
  elb_target_group:
    name: "{{ environment_name }}-target-group"
    region: "{{ ec2_region }}"
    protocol: "http"
    port: "80"
    vpc_id: "{{ vpc_info.vpc.id }}"
    state: "present"
    targets:
      - Id: "{{ item.instance_id }}"
        Port: "80"
    modify_targets: "true"
  with_items: "{{ singleinstance.instances }}"

这将使我们得到一个名为wordpress-tmp的单个实例,它可以通过 SSH 访问,并且在我们的弹性负载均衡器后面处于活动状态,位于名为ec2_instance的主机组中。

现有部署

如果我们已经有运行中的实例,先前的任务将被跳过,只运行roles/ec2/existing_deployment.yml中的单个任务。这个任务只是将一个运行中的主机添加到名为ec2_instance的主机组中:

- name: add one of our running instances to a host group for use in the next step
  add_host:
    name: "{{ groups['already_running'][0] }}"
    ansible_ssh_host: "{{ groups['already_running'][0] }}"
    groups: "ec2_instance"

这使我们处于与新部署任务结束时相同的位置,有一个名为ec2_instance的主机,有一个可以通过 SSH 访问的实例。

堆栈

我们接下来要创建的角色是只在主机上执行的——名为stackec2_instance组。与之前的角色一样,我们可以从aws-wordpress文件夹中运行以下命令来创建所需的文件:

$ ansible-galaxy init roles/stack

这个角色实际上包含了三个角色。与 EC2 角色一样,我们正在构建逻辑,根据 playbook 首次连接时发现的实例状态来执行任务。让我们先看一下roles/stack/tasks/main.yml的内容。

其中的第一个任务在新部署和现有部署上都执行;它运行yum update

- name: update all of the installed packages
  yum:
    name: "*"
    state: "latest"
    update_cache: "yes"

接下来,我们需要知道 WordPress 是否已安装:

- name: are the wordpress files already there?
  stat:
    path: "{{ wordpress_system.home }}/index.php"
  register: wp_installed

接下来的两个任务包括两个额外的角色;一个安装和配置软件堆栈,另一个执行初始的 WordPress 安装,但只有在没有找到现有安装时才会执行:

- name: if no wordpress installed install and configure the software stack
  include_role:
    name: "stack"
    tasks_from: "deploy.yml"
  when: wp_installed.stat.exists == False

- name: if no wordpress installed, install it !!!
  include_role:
    name: "stack"
    tasks_from: "wordpress.yml"
  when: wp_installed.stat.exists == False

这两个角色是我们在本地安装 WordPress 时创建的角色的简化版本。

默认变量

在我们查看角色之前,让我们先看一下roles/stack/defaults/main.yml的代码,因为有一些不同之处:

wp_cli:
  download: "https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar"
  path: "/usr/local/bin/wp"

wordpress:
  domain: "http://{{ elb_results.load_balancers[0].dns_name }}/"
  title: "WordPress installed by Ansible on AWS"
  username: "ansible"
  password: "password"
  email: "test@example.com"

efs_mount_dir: "/efs"

wordpress_system:
  user: "wordpress"
  group: "php-fpm"
  comment: "wordpress system user"
  home: "{{ efs_mount_dir }}/wordpress"
  state: "present"

php:
  ip: "127.0.0.1"
  port: "9000"
  upstream: "php"
  ini:
    - { regexp: '^;date.timezone =', replace: 'date.timezone = Europe/London' }
    - { regexp: '^expose_php = On', replace: 'expose_php = Off' }
    - { regexp: '^upload_max_filesize = 2M', replace: 'upload_max_filesize = 20M' }

selinux:
  http_permissive: true

repo_packages:
  - "epel-release"
  - "https://centos7.iuscommunity.org/ius-release.rpm"

nginx_repo:
  name: "nginx"
  description: "The mainline NGINX repo"
  baseurl: "http://nginx.org/packages/mainline/centos/7/$basearch/"
  gpgcheck: "no"
  enabled: "yes"

system_packages:
  - "MySQL-python"
  - "policycoreutils-python"
  - "nfs-utils"

stack_packages:
  - "nginx"
  - "mariadb"
  - "php72u"
  - "php72u-bcmath"
  - "php72u-cli"
  - "php72u-common"
  - "php72u-dba"
  - "php72u-fpm"
  - "php72u-fpm-nginx"
  - "php72u-gd"
  - "php72u-intl"
  - "php72u-json"
  - "php72u-mbstring"
  - "php72u-mysqlnd"
  - "php72u-process"
  - "php72u-snmp"
  - "php72u-soap"
  - "php72u-xml"
  - "php72u-xmlrpc"

extra_packages:
  - "vim-enhanced"
  - "git"
  - "unzip"

主要的区别是:

  • wordpress.domain URL:这次,我们不是硬编码域名,而是使用elb_application_lb_facts模块获取的弹性负载均衡器 URL。稍后再详细介绍。

  • efs_mount_dir变量:这是一个新变量,我们将用它来定义我们想要将 EFS 共享挂载到实例的位置。

  • wordpress_system.home选项:现在使用efs_mount_dir,这样我们的 WordPress 安装可以在所有实例之间共享。

  • 缺少 MariaDB 服务器:你会注意到安装和配置 MariaDB 服务器的引用已经被移除;因为我们有了 RDS 实例,我们不再需要这些了。

我们使用include_role模块将任务作为一个角色执行,以确保变量被正确加载。

部署

第一个额外的角色,名为roles/stack/tasks/deploy.yml,如你所期望的那样,部署软件堆栈和配置。

它首先挂载 EFS 共享;首先,我们需要使用efs_facts模块收集一些关于 EFS 共享的信息:

- name: find some information on the elastic load balancer
  local_action:
    module: efs_facts
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-efs"
  become: no

你可能已经注意到我们以不同的方式调用了efs_facts模块;我们实际上使用了local_action模块,在我们的 Ansible 控制器上运行efs_facts模块,而不是在 EC2 实例上运行。这是因为我们实际上没有给我们的 EC2 实例访问 API 的权限,因为我们没有安装 Boto 或将我们的访问密钥和秘密访问密钥作为变量传递。

使用local_action模块允许我们切换回我们的 Ansible 控制器来收集关于我们的 EFS 的信息,然后将结果应用到我们的 EC2 实例上;我们将在安装中稍后再次使用这个模块。

作为这个任务的一部分,我们使用become: no;否则,它将尝试使用sudo来执行。这是因为我们告诉所有任务在site.yml文件的这部分使用become: yesbecome_method: sudo,我们将在本章后面更新。

下一个任务挂载 EFS 共享,并将其添加到fstab文件中,这意味着当我们从正在创建的 AMI 中启动的实例启动时,它将自动挂载:

- name: ensure EFS volume is mounted.
  mount:
    name: "{{ efs_mount_dir }}"
    src: "{{ efs[0].file_system_id }}.efs.{{ ec2_region }}.amazonaws.com:/"
    fstype: nfs4
    opts: nfsvers=4.1
    state: mounted

efs_mount_dir会自动创建,所以我们不需要担心提前创建它。角色的下一部分安装和配置堆栈:

- name: install the repo packages
  yum:
    name: "{{ item }}"
    state: "installed"
  with_items: "{{ repo_packages }}"

- name: add the NGINX mainline repo
  yum_repository:
    name: "{{ nginx_repo.name }}"
    description: "{{ nginx_repo.description }}"
    baseurl: "{{ nginx_repo.baseurl }}"
    gpgcheck: "{{ nginx_repo.gpgcheck }}"
    enabled: "{{ nginx_repo.enabled }}"

- name: install the stack packages
  yum:
    name: "{{ item }}"
    state: "installed"
  with_items: "{{ system_packages + stack_packages + extra_packages }}"

- name: add the wordpress user
  user: 
    name: "{{ wordpress_system.user }}"
    group: "{{ wordpress_system.group }}"
    comment: "{{ wordpress_system.comment }}"
    home: "{{ wordpress_system.home }}"
    state: "{{ wordpress_system.state }}"

- name: copy the nginx.conf to /etc/nginx/
  template:
    src: "nginx-nginx.conf.j2"
    dest: "/etc/nginx/nginx.conf"
  notify: "restart nginx"

- name: create the global directory in /etc/nginx/
  file:
    dest: "/etc/nginx/global/"
    state: "directory"
    mode: "0644"

- name: copy the restrictions.conf to /etc/nginx/global/
  copy:
    src: "nginx-global-restrictions.conf"
    dest: "/etc/nginx/global/restrictions.conf"
  notify: "restart nginx"

- name: copy the wordpress_shared.conf to /etc/nginx/global/
  template:
    src: "nginx-global-wordpress_shared.conf.j2"
    dest: "/etc/nginx/global/wordpress_shared.conf"
  notify: "restart nginx"

- name: copy the default.conf to /etc/nginx/conf.d/
  template:
    src: "nginx-confd-default.conf.j2"
    dest: "/etc/nginx/conf.d/default.conf"
  notify: "restart nginx"

- name: copy the www.conf to /etc/php-fpm.d/
  template:
    src: "php-fpmd-www.conf.j2"
    dest: "/etc/php-fpm.d/www.conf"
  notify: "restart php-fpm"

- name: configure php.ini
  lineinfile: 
    dest: "/etc/php.ini"
    regexp: "{{ item.regexp }}"
    line: "{{ item.replace }}"
    backup: "yes"
    backrefs: "yes"
  with_items: "{{ php.ini }}"
  notify: "restart php-fpm"

- name: start php-fpm
  service:
    name: "php-fpm"
    state: "started"

- name: start nginx
  service:
    name: "nginx"
    state: "started"

- name: set the selinux allowing httpd_t to be permissive is required
  selinux_permissive:
    name: httpd_t
    permissive: true
  when: selinux.http_permissive == true

为了使这个工作,你需要从我们在第五章中创建的 LEMP playbook 的stack-config角色中复制fileshandlerstemplates中的文件。

WordPress

正如你可能已经猜到的那样,这个角色可以在roles/stack/tasks/wordpress.yml文件中找到,与roles/stack/tasks/main.ymlroles/stack/tasks/deploy.yml一起,安装和配置 WordPress。

在继续执行任务之前,我们需要找出关于我们的 RDS 实例的信息:

- name: find some information on the rds instance
  local_action:
    module: rds
    region: "{{ ec2_region }}"
    command: facts
    instance_name: "{{ environment_name }}-rds"
  become: no
  register: rds_results

这样我们就可以在定义数据库连接时使用这些任务;同样,我们还需要了解弹性负载均衡器的情况:

- name: find some information on the elastic load balancer
  local_action:
    module: elb_application_lb_facts
    region: "{{ ec2_region }}"
    names: "{{ environment_name }}-elb"
  become: no
  register: elb_results

剩下的任务做以下事情:

  1. 安装 WP-CLI。

  2. 下载 WordPress。

  3. 设置 WordPress 文件夹的正确权限。

  4. 配置 WordPress 以连接到我们通过收集信息找到的 RDS 的端点;我们正在重用我们生成的密码文件。

  5. 使用弹性负载均衡器的 URL 和默认变量的详细信息安装 WordPress:

- name: download wp-cli
  get_url:
    url: "{{ wp_cli.download }}"
    dest: "{{ wp_cli.path }}"

- name: update permissions of wp-cli to allow anyone to execute it
  file:
    path: "{{ wp_cli.path }}"
    mode: "0755"

- name: are the wordpress files already there?
  stat:
    path: "{{ wordpress_system.home }}/index.php"
  register: wp_installed

- name: download wordpresss
  shell: "{{ wp_cli.path }} core download"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  when: wp_installed.stat.exists == False

- name: set the correct permissions on the homedir
  file:
    path: "{{ wordpress_system.home }}"
    mode: "0775"
  when: wp_installed.stat.exists == False

- name: is wordpress already configured?
  stat:
    path: "{{ wordpress_system.home }}/wp-config.php"
  register: wp_configured

- name: configure wordpress
  shell: "{{ wp_cli.path }} core config --dbhost={{ rds_results.instance.endpoint }} --dbname={{ environment_name }} --dbuser={{ environment_name }} --dbpass={{ lookup('password', 'group_vars/rds_passwordfile chars=ascii_letters,digits length=30') }}"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  when: wp_configured.stat.exists == False

- name: do we need to install wordpress?
  shell: "{{ wp_cli.path }} core is-installed"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  ignore_errors: yes
  register: wp_installed

- name: install wordpress if needed
  shell: "{{ wp_cli.path }} core install --url='{{ wordpress.domain }}' --title='{{ wordpress.title }}' --admin_user={{ wordpress.username }} --admin_password={{ wordpress.password }} --admin_email={{ wordpress.email }}"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  when: wp_installed.rc == 1

为了保持简单,我们不使用 Ansible 来管理主题或插件。

这是我们停止在上一个角色中发现/启动的实例上运行任务的地方;现在是时候切换回我们的 Ansible 控制器并使用我们的实例创建一个 AMI 了。

AMI

这个角色不需要做出任何选择,它只是从ec2_instances组中获取我们的主机并创建其镜像。首先,让我们创建这个角色:

$ ansible-galaxy init roles/ami

这个角色由三个任务组成,其中一个是暂停。首先,在roles/ami/tasks/main.yml中,我们需要找出一些关于实例的信息。我们使用ec2_instance_facts模块:

- name: find out some facts about the instance we have been using
  ec2_instance_facts:
    region: "{{ ec2_region }}"
    filters:
      dns-name: "{{ groups['ec2_instance'][0] }}"
  register: "our_instance"

现在我们知道了实例,我们可以创建 AMI 了:

- name: create the AMI
  ec2_ami:
    region: "{{ ec2_region }}"
    instance_id: "{{ our_instance.instances.0.instance_id }}"
    wait: "yes"
    name: "{{ environment_name }}-{{ ansible_date_time.date }}_{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}"
    tags:
        Name: "{{ environment_name }}-{{ ansible_date_time.date }}_{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}"
        Environment: "{{ environment_name }}"
        Date: "{{ ansible_date_time.date }} {{ ansible_date_time.time }}"

正如你所看到的,我们在运行ec2_instance_facts模块时使用了我们发现的instance_id;我们还使用了ansible_date_time变量,这个变量在调用gather_facts模块时被定义,用来给我们的 AMI 取一个唯一的名字。

如前所述,最后一个任务是一个暂停:

- name: wait for 2 minutes before continuing
  pause:
    minutes: 2

这是必需的,因为我们新创建的 AMI 可能需要一段时间才能完全注册并在 AWS API 中可用。

自动扩展

我们 playbook 中的最后一个角色创建了一个启动配置,然后创建/更新了一个自动扩展组,最终启动我们的实例。然后进行了一点点的清理工作。要创建这个角色,请运行:

$ ansible-galaxy init roles/autoscaling

首先,我们需要在roles/autoscaling/default/main.yml中设置一些默认变量;这些细节显示了我们希望同时运行多少个实例,以及在部署新 AMI 时替换多少个实例:

min_size: 2
max_size: 9
desired_capacity: 3
replace_size: 2
health_check_type: ELB
assign_public_ip: yes
ec2_instance_type: "t2.micro"

这些变量的含义是,我们希望始终运行三个实例,所以如果有两个,就启动更多,但一次不要启动超过九个。在部署新镜像时,每次替换两个实例。

我们还在定义健康检查,使用弹性负载均衡器检查,告诉实例使用公共 IP 地址启动,这意味着我们可以通过 SSH 访问它们,最后,我们定义要使用的实例类型。

我们在roles/autoscaling/tasks/main.yml中需要定义的第一个任务是找到要使用的正确 AMI:

- name: search through all of our AMIs
  ec2_ami_facts:
    region: "{{ ec2_region }}"
    filters:
      name: "{{ environment_name }}-*"
  register: amiFind

同样,我们需要知道我们构建的最后一个 AMI 的细节:

- name: find the last one we built
  set_fact:
    amiSortFilter: "{{ amiFind.images | sort(attribute='creation_date') | last }}"

最后,我们需要获取 AMI ID 和 AMI 名称;我们将使用这个名称来命名启动配置:

- name: grab AMI ID and name of the most recent result
  set_fact:
    our_ami_id: "{{ amiSortFilter.image_id }}"
    our_ami_name: "{{ amiSortFilter.name }}"

接下来,我们有一个任务,使用先前的信息来创建启动配置:

- name: create the launch configuration
  ec2_lc:
    region: "{{ ec2_region }}"
    name: "{{ our_ami_name }}"
    state: present
    image_id: "{{ our_ami_id }}"
    security_groups: [ "{{ sg_ec2.group_id }}" ]
    assign_public_ip: "{{ assign_public_ip }}"
    instance_type: "{{ ec2_instance_type }}"
    volumes:
    - device_name: /dev/xvda
      volume_size: 10
      volume_type: gp2
      delete_on_termination: true

创建了启动配置后,我们可以创建/更新自动扩展组来引用它。在这之前,我们需要找到目标组的 Amazon 资源名称(ARN):

- name: find out the target group ARN
  elb_target_group_facts:
    region: "{{ ec2_region }}"
    names:
      - "{{ environment_name }}-target-group"
  register: elb_target_group

有了这些信息,我们可以继续下一个任务:

- name: create / update the auto-scaling group using the launch configuration we just created
  ec2_asg:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-asg"
    target_group_arns: [ "{{ elb_target_group.target_groups[0].target_group_arn }}" ]
    launch_config_name: "{{ our_ami_name }}"
    min_size: "{{ min_size }}"
    max_size: "{{ max_size }}"
    desired_capacity: "{{ desired_capacity }}"
    health_check_period: 300
    health_check_type: "{{ health_check_type }}"
    replace_all_instances: yes
    replace_batch_size: "{{ replace_size }}"
    vpc_zone_identifier: "{{ subnet_ec2_ids }}"
    wait_for_instances: "yes"
    wait_timeout: "900"
    tags:
      - Name: "{{ environment_name }}-ec2"
      - environment: "{{ environment_name }}"

自动扩展组确保我们始终运行所需数量的 EC2 实例。如果没有运行实例,它会启动它们并将它们注册到弹性负载均衡器的目标组中。

如果已经有实例在运行,并且我们已经更新了启动配置,那么它将对我们的新配置进行滚动部署,确保在旧实例被移除之前,新实例被启动和注册,从而确保我们没有任何停机时间。

最后一个任务是删除我们可能正在运行的任何tmp实例:

- name: remove any tmp instances which are running
  ec2_instance:
    region: "{{ ec2_region }}"
    state: absent
    filters:
      instance-state-name: "running"
      "tag:environment": "{{ environment_name }}"
      "tag:Name": "{{ environment_name }}-tmp"

这样我们应该得到我们期望的状态并且没有其他东西在运行。

运行 playbook

我们需要做的第一件事是更新我们的production清单文件;应该看起来像下面这样:

# Register all of the host groups we will be creating in the playbooks
[ec2_instance]
[already_running]

# Put all the groups into into a single group so we can easily apply one config to it for overriding things like the ssh user and key location
[aws:children]
ec2_instance
already_running

# Finally, configure some bits to allow us access to the instances before we deploy our credentials using Ansible
[aws:vars]
ansible_ssh_user=centos
ansible_ssh_private_key_file=~/.ssh/id_rsa
host_key_checking=False

如您所见,我们正在定义主机组,并配置 Ansible 使用centos用户,这是我们正在使用的原始 AMI 的默认用户。

site.yml文件需要更新:

---

- name: Create, launch and configure our basic AWS environment
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/vpc
    - roles/subnets
    - roles/gateway
    - roles/securitygroups
    - roles/elb
    - roles/rds
    - roles/efs
    - roles/ec2

- name: Configure / update the EC2 instance
  hosts: ec2_instance
  become: yes
  become_method: sudo
  gather_facts: True

  vars_files: 
    - group_vars/common.yml

  roles:
    - roles/stack

- name: Create, launch and configure our AMI
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/ami
    - roles/autoscaling

如您所见,我们现在有三个部分;第一部分准备环境,正如我们已经看到的—还有ec2角色的添加。这一部分都在 Ansible 控制器上执行。

在接下来的部分,我们转到对ec2_instance组中的主机运行角色;如前所述,我们在这个主机上使用become: yesbecome_method: sudo,因为我们连接的用户centos没有我们需要安装软件栈的正确权限。这就是为什么在使用local_action模块时需要禁用become。第三部分将我们带回到我们的 Ansible 控制器,在那里我们使用 AWS API 来创建我们的 AMI 并启动它。

不要忘记设置您的访问密钥和秘密访问密钥环境变量:

$ export AWS_ACCESS_KEY=AKIAI5KECPOTNTTVM3EDA
$ export AWS_SECRET_KEY=Y4B7FFiSWl0Am3VIFc07lgnc/TAtK5+RpxzIGTr

在运行 playbook 之前,您需要确保您已经订阅了 AWS Marketplace 中的 CentOS 7 Amazon Machine Image,要做到这一点,请转到以下链接并点击订阅按钮,如果您没有订阅 AMI,当您运行 playbook 时,您将收到一个错误提示,告诉您您无法访问该镜像:aws.amazon.com/marketplace/pp/B00O7WM7QW

我们将再次计时我们的 playbook 运行,因此,要执行 playbook,请使用以下命令:

$ time ansible-playbook -i production site.yml

由于我们已经看到了一半的 playbook 运行的输出,我将跳过vpcsubnetsgatewaysecuritygroupselbrdsefs角色的输出,这意味着我们将从ec2角色开始:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Create, launch and configure our basic AWS environment] ************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/ec2 : gather facts on any already running instances] ********************************
ok: [localhost]

TASK [roles/ec2 : add any already running instances to a group] *********************************

TASK [roles/ec2 : set the number of already running instances as a fact] ***********************
ok: [localhost]

TASK [roles/ec2 : run the tasks for a new deployment] *******************************************
included: /Users/russ/Documents/Code/learn-ansible-fundamentals-of-ansible-2x/chapter10/aws-wordpress/roles/ec2/tasks/new_deployment.yml for localhost

TASK [roles/ec2 : search for all of the AMIs in the defined region which match our selection] ***
ok: [localhost]

TASK [roles/ec2 : filter the list of AMIs to find the latest one with an EBS backed volume] *****
ok: [localhost]

TASK [roles/ec2 : finally grab AMI ID of the most recent result which matches our base image which is backed by an EBS volume] ***************************************************************
ok: [localhost]

TASK [roles/ec2 : check the user russ has a key, if not create one] *****************************
ok: [localhost]

TASK [roles/ec2 : upload the users public key] **************************************************
ok: [localhost] => (item=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmuoFR01i/Yf3HATl9c3sufJvghTFgYzK/Zt29JiTqWlSQhmXhNNTh6iI6nXuPVhQGQaciWbqya6buncQ3vecISx6+EwsAmY3Mwpz1a/eMiXOgO/zn6Uf79dXcMN2JwpLFoON1f9PR0/DTpEkjwqb+eNLw9ThjH0J994+Pev+m8OrqgReFW36a/kviUYKsHxkXmkgxtPJgwKU90STNab4qyfKEGhi2w/NzECgseeQYs1H3klORaHQybhpXkoCIMmgy9gnzSH7oa2mJqKilVed27xoirkXzWPaAQlfiEE1iup+2xMqWY6Jl9qb8tJHRS+l8UcxTMNaWsQkTysLTgBAZ russ@mckendrick.io)

TASK [roles/ec2 : launch an instance] ***********************************************************
changed: [localhost]

TASK [roles/ec2 : gather facts on the instance we just launched using the AWS API] **************
ok: [localhost]

TASK [roles/ec2 : add our temporary instance to a host group for use in the next step] **********
changed: [localhost] =>

TASK [roles/ec2 : wait until SSH is available before moving onto the next step] *****************
ok: [localhost] => 

TASK [roles/ec2 : add the instance to the target group] ******************************************
changed: [localhost] =>

TASK [roles/ec2 : run the tasks for an existing deployment] *************************************
skipping: [localhost]

PLAY [Configure / update the EC2 instance] ******************************************************

TASK [Gathering Facts] **************************************************************************
ok: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [roles/stack : update all of the installed packages] ***************************************
ok: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [roles/stack : are the wordpress files already there?] *************************************
ok: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [roles/stack : if no wordpress installed install and configure the software stack] *********

TASK [stack : find some information on the elastic load balancer] *******************************
ok: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com -> localhost]

TASK [stack : ensure EFS volume is mounted.] ****************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : install the repo packages] *************************************************************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com] => (item=[u'epel-release', u'https://centos7.iuscommunity.org/ius-release.rpm'])

TASK [stack : add the NGINX mainline repo] ******************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : install the stack packages] *******************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com] => (item=[u'MySQL-python', u'policycoreutils-python', u'nfs-utils', u'nginx', u'mariadb', u'php72u', u'php72u-bcmath', u'php72u-cli', u'php72u-common', u'php72u-dba', u'php72u-fpm', u'php72u-fpm-nginx', u'php72u-gd', u'php72u-intl', u'php72u-json', u'php72u-mbstring', u'php72u-mysqlnd', u'php72u-process', u'php72u-snmp', u'php72u-soap', u'php72u-xml', u'php72u-xmlrpc', u'vim-enhanced', u'git', u'unzip'])

TASK [stack : add the wordpress user] ***********************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : copy the nginx.conf to /etc/nginx/] ***********************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : create the global directory in /etc/nginx/] ***************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : copy the restrictions.conf to /etc/nginx/global/] *********************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : copy the wordpress_shared.conf to /etc/nginx/global/] *****************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : copy the default.conf to /etc/nginx/conf.d/] **************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : copy the www.conf to /etc/php-fpm.d/] *********************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : configure php.ini] ****************************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com] => (item={u'regexp': u'^;date.timezone =', u'replace': u'date.timezone = Europe/London'})
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com] => (item={u'regexp': u'^expose_php = On', u'replace': u'expose_php = Off'})
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com] => (item={u'regexp': u'^upload_max_filesize = 2M', u'replace': u'upload_max_filesize = 20M'})

TASK [stack : start php-fpm] ********************************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : start nginx] **********************************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : set the selinux allowing httpd_t to be permissive is required] ********************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [roles/stack : if no wordpress installed, install it !!!] **********************************

TASK [stack : download wp-cli] ******************************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : update permissions of wp-cli to allow anyone to execute it] ***********************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : find some information on the rds instance] ****************************************
ok: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com -> localhost]

TASK [stack : find some information on the elastic load balancer] *******************************
ok: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com -> localhost]

TASK [stack : are the wordpress files already there?] *******************************************
ok: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : download wordpresss] **************************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : set the correct permissions on the homedir] *****************************************************************************************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : is wordpress already configured?] ***************************************************************************************************************************************
ok: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : configure wordpress] ****************************************************************************************************************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

TASK [stack : do we need to install wordpress?] ***************************************************************************************************************************************
fatal: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]: FAILED! => {"changed": true, "cmd": "/usr/local/bin/wp core is-installed", "delta": "0:00:01.547784", "end": "2018-05-06 14:19:01.301168", "msg": "non-zero return code", "rc": 1, "start": "2018-05-06 14:18:59.753384", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring

TASK [stack : install wordpress if needed] ******************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

RUNNING HANDLER [roles/stack : restart nginx] ***************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

RUNNING HANDLER [roles/stack : restart php-fpm] *************************************************
changed: [ec2-34-244-58-38.eu-west-1.compute.amazonaws.com]

PLAY [Create, launch and configure our AMI] *****************************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/ami : find out some facts about the instance we have been using] ********************
ok: [localhost]

TASK [roles/ami : create the AMI] *************************************************************************************************
changed: [localhost]

TASK [roles/ami : wait for 2 minutes before continuing] *****************************************
Pausing for 120 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

TASK [roles/autoscaling : search through all of our AMIs] ***************************************
ok: [localhost]

TASK [roles/autoscaling : find the last one we built] *******************************************
ok: [localhost]

TASK [roles/autoscaling : grab AMI ID and name of the most recent result] ***********************
ok: [localhost]

TASK [roles/autoscaling : create the launch configuration] **************************************
changed: [localhost]

TASK [roles/autoscaling : find out the target group ARN] ****************************************
ok: [localhost]

TASK [roles/autoscaling : create / update the auto-scaling group using the launch configuration we just created] ********************************************************************************
changed: [localhost]

TASK [roles/autoscaling : remove any tmp instances] *********************************************
changed: [localhost]

PLAY RECAP **************************************************************************************
ec2-34-244-58-38.eu-west-1.compute.amazonaws.com : ok=32 changed=24 unreachable=0 failed=0
localhost : ok=47 changed=21 unreachable=0 failed=0

对我来说,playbook 运行的时间如下:

real 31m34.752s
user 2m4.008s
sys  0m39.274s

因此,通过一个命令,在 32 分钟内,我们拥有了一个高可用的普通 WordPress 安装。如果您从 AWS 控制台找到弹性负载均衡器的公共 URL,您应该能够看到您的站点:

在 AWS 控制台中检查 EC2 实例,我们可以看到有三个名为wordpress-ec2的实例在运行,而wordpress-tmp实例已被终止:

现在,让我们看看当我们再次运行 playbook 时会发生什么。我们不仅应该看到它执行得更快,而且它应该跳过一些角色:

$ time ansible-playbook -i production site.yml

同样,我已经截断了输出:

WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Create, launch and configure our basic AWS environment] ************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/ec2 : gather facts on any already running instances] ********************************
ok: [localhost]

TASK [roles/ec2 : add any already running instances to a group] *********************************
changed: [localhost] => 

TASK [roles/ec2 : set the number of already running instances as a fact] ************************
ok: [localhost]

TASK [roles/ec2 : run the tasks for a new deployment] *******************************************
skipping: [localhost]

TASK [roles/ec2 : run the tasks for an existing deployment] *************************************
included: /Users/russ/Documents/Code/learn-ansible-fundamentals-of-ansible-2x/chapter10/aws-wordpress/roles/ec2/tasks/existing_deployment.yml for localhost

TASK [roles/ec2 : add one of our running instances to a host group for use in the next step] ****
changed: [localhost]

PLAY [Configure / update the EC2 instance] ******************************************************

TASK [Gathering Facts] **************************************************************************
ok: [ec2-52-211-180-156.eu-west-1.compute.amazonaws.com]

TASK [roles/stack : update all of the installed packages] ***************************************
changed: [ec2-52-211-180-156.eu-west-1.compute.amazonaws.com]

TASK [roles/stack : are the wordpress files already there?] *************************************
ok: [ec2-52-211-180-156.eu-west-1.compute.amazonaws.com]

TASK [roles/stack : if no wordpress installed install and configure the software stack] *********
skipping: [ec2-52-211-180-156.eu-west-1.compute.amazonaws.com]

TASK [roles/stack : if no wordpress installed, install it !!!] **********************************
skipping: [ec2-52-211-180-156.eu-west-1.compute.amazonaws.com]

PLAY [Create, launch and configure our AMI] *****************************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/ami : find out some facts about the instance we have been using] ********************
ok: [localhost]

TASK [roles/ami : create the AMI] ***************************************************************
changed: [localhost]

TASK [roles/ami : wait for 2 minutes before continuing] *****************************************
Pausing for 120 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

TASK [roles/autoscaling : search through all of our AMIs] ***************************************
ok: [localhost]

TASK [roles/autoscaling : find the last one we built] *******************************************
ok: [localhost]

TASK [roles/autoscaling : grab AMI ID and name of the most recent result] ***********************
ok: [localhost]

TASK [roles/autoscaling : create the launch configuration] **************************************
changed: [localhost]

TASK [roles/autoscaling : find out the target group ARN] ****************************************
ok: [localhost]

TASK [roles/autoscaling : create / update the auto-scaling group using the launch configuration we just created] ********************************************************************************
changed: [localhost]

TASK [roles/autoscaling : remove any tmp instances] *********************************************
ok: [localhost]

PLAY RECAP **************************************************************************************
ec2-52-211-180-156.eu-west-1.compute.amazonaws.com : ok=3 changed=1 unreachable=0 failed=0
localhost : ok=39 changed=5 unreachable=0 failed=0

这次,我得到了以下时间:

real 9m18.502s
user 0m48.718s
sys  0m14.115s

完成后,我检查了一下,我仍然可以使用 playbook 中设置的用户名(ansible)和密码(password)登录 WordPress,方法是访问我的弹性负载均衡器 URL 并在末尾添加/wp-admin

你可以在 AWS 控制台的自动扩展活动日志中看到发生了什么:

正如您所看到的,启动了三个新实例,终止了三个实例。

终止所有资源

在完成本章之前,我们需要查看终止资源;我们需要做的唯一补充是移除自动扩展组和 AMI。为此,我们将在roles/remove/tasks/main.yml中添加四个任务;从文件顶部开始,添加以下两个任务:

- name: remove the auto-scaling group
  ec2_asg:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}-asg"
    state: absent
    wait_for_instances: "yes"
    wait_timeout: "900"

- name: wait for 2 minutes before continuing
  pause:
    minutes: 2

第一个任务是移除自动扩展组。这将终止由它启动的任何实例。我们还内置了一个暂停,以确保一切都已从 AWS API 中正确地移除。

在角色的末尾,添加以下两个任务以移除所有 AMI:

- name: search through all of our AMIs
  ec2_ami_facts:
    region: "{{ ec2_region }}"
    filters:
      name: "{{ environment_name }}-*"
  register: amiFind

- name: unregister all of our AMIs
  ec2_ami:
    image_id: "{{ item.image_id }}"
    delete_snapshot: True
    state: absent
  with_items: "{{ amiFind.images }}"

然后,您可以使用以下命令运行 playbook:

$ ansible-playbook -i production remove.yml

与以往一样,不要忘记在继续之前检查弹性负载均衡器是否已被移除。一旦 playbook 运行完毕,我建议您登录 AWS 控制台,双重检查一切是否已正确移除。playbook 不会移除启动配置,这不应该是一个问题,因为它们没有相关的成本。但是,我建议检查未附加的 EBS 卷和快照,因为这些将产生成本。

总结

在本章中,我们通过创建和启动一个高可用的 WordPress 安装将我们的 AWS 提升到了一个新的水平。通过利用 AWS 提供的各种服务,我们消除了与实例和可用区的可用性有关的任何单点故障。

我们还在 playbook 中构建了逻辑,以便我们可以使用相同的命令启动新的部署或更新现有部署的操作系统,通过包含我们更新的软件包的新实例 AMI 进行滚动部署,从而在部署期间实现零停机。

虽然 WordPress 部署可能是我们能够做的最简单的,但是在使用更复杂的应用程序时,部署生产就绪的镜像的过程仍然是类似的。

在我们的下一章中,我们将研究从公共云迁移到私有云,并且 Ansible 如何与 VMware 交互。

问题

  1. 使用gather_facts选项注册的变量的名称是什么,其中包含了我们执行 playbook 的日期和时间?

  2. 正确或错误:Ansible 自动找出需要执行的任务,这意味着我们不必自己定义任何逻辑。

  3. 解释为什么我们必须使用local_action模块。

  4. 我们在ansible-playbook命令之前添加哪个命令来记录命令执行所花费的时间?

  5. 真或假:在使用自动扩展时,您必须手动启动 EC2 实例。

  6. 更新 playbook,以便在 playbook 运行结束时提供 Elastic Load Balancer 的公共 URL。

进一步阅读

您可以在 AWS Marketplace 的aws.amazon.com/mp/centos/找到有关 CentOS AMI 的更多详细信息。

第十一章:构建 VMware 部署

现在我们知道如何在 AWS 中启动网络和服务,我们现在将讨论在 VMware 环境中部署类似设置,并讨论核心 VMware 模块。

在本章中,我们将:

  • 快速介绍 VMware

  • 审查 Ansible VMware 模块

  • 通过一个示例 playbook 来启动几个虚拟机

技术要求

在本章中,我们将讨论 VMware 产品系列的各种组件,以及如何使用 Ansible 与它们进行交互。虽然本章中有一个示例 playbook,但它可能不容易转移到您的安装中。因此,建议您在更新之前不要使用本章中的任何示例。

VMware 简介

VMware 有近 20 年的历史,从一个隐秘的初创公司到被戴尔拥有并被 EMC 收购,收入达 79.2 亿美元。VMware 产品组合目前有大约 30 种产品;最常见的是其 hypervisors,其中有两种不同的类型。

第一个 hypervisor,VMware ESXi,是一种直接在硬件上运行的类型 1,使用大多数现代 64 位英特尔和 AMD CPU 中找到的指令集。其原始的类型 2 hypervisor 不需要 CPU 中存在虚拟化指令,就像它们需要在类型 1 中一样。它以前被称为 GSX;这个 hypervisor 早于类型 1 hypervisor,这意味着它可以支持更旧的 CPU。

VMware 在大多数企业中非常普遍;它允许管理员快速在许多标准的基于 x86 的硬件配置和类型上部署虚拟机。

VMware 模块

如前所述,VMware 范围内大约有 30 种产品;这些产品涵盖了从 hypervisors 到虚拟交换机、虚拟存储以及与基于 VMware 的主机和虚拟机进行交互的几个接口。在本节中,我们将介绍随 Ansible 一起提供的核心模块,以管理您的 VMware 资产的所有方面。

我尝试将它们分成逻辑组,并且对于每个组,都会简要解释模块所针对的产品。

要求

所有模块都有一个共同点:它们都需要安装一个名为PyVmomi的 Python 模块。要安装它,请运行以下pip命令:

$ sudo pip install PyVmomi

该模块包含了 VMware vSphere API Python 绑定,没有它,我们将在本章中要讨论的模块无法与您的 VMware 安装进行交互。

虽然本章中的模块已经在 vSphere 5.5 到 6.5 上进行了测试,但您可能会发现一些旧模块在较新版本的 vSphere 上存在一些问题。

vCloud Air

vCloud Air 是 VMware 的基础设施即服务IaaS)产品,我说因为 vCloud Air 业务部门和负责该服务的团队于 2017 年中被法国托管和云公司 OVH 从 VMware 收购。有三个 Ansible 模块直接支持 vCloud Air,以及VMware vCloud Hybrid ServicevCHS)和VMware vCloud DirectorvCD)。

vca_fw 模块

该模块使您能够从 vCloud Air 网关中添加和删除防火墙规则。以下示例向您展示了如何添加一个允许 SSH 流量的规则:

- name: example fireware rule
  vca_fw:
   instance_id: "abcdef123456-1234-abcd-1234-abcdef123456"
   vdc_name: "my_vcd"
   service_type: "vca"
   state: "present"
   fw_rules:
     - description: "Allow SSH"
       source_ip: "10.20.30.40"
       source_port: "Any"
       dest_port: "22"
       dest_ip: "192.0.10.20"
       is_enable: "true"
       enable_logging: "false"
       protocol: "Tcp"
       policy: "allow"

注意我们传递了一个service_type;这可以是vcavcdvchs

vca_nat 模块

该模块允许您管理网络地址转换NAT)规则。在下面的示例中,我们要求所有命中公共 IP 地址123.123.123.123上的端口2222的流量被转发到 IP 地址为192.0.10.20的虚拟机上的端口22

- name: example nat rule
  vca_nat:
   instance_id: "abcdef123456-1234-abcd-1234-abcdef123456"
   vdc_name: "my_vcd"
   service_type: "vca"
   state: "present"
   nat_rules:
      - rule_type: "DNAT"
        original_ip: "123.123.123.123"
        original_port: "2222"
        translated_ip: "192.0.10.20"
        translated_port: "22"

这意味着要从我们的外部网络访问虚拟机192.0.10.20上的 SSH,我们需要运行类似以下命令:

$ ssh username@123.123.123.123 -p2222

假设我们已经设置了正确的防火墙规则,我们应该通过192.0.10.20虚拟机进行路由。

vca_vapp 模块

该模块用于创建和管理 vApps。vApp 是一个或多个虚拟机的组合,用于提供一个应用程序:

- name: example vApp
  vca_vapp:
    vapp_name: "Example"
    vdc_name: "my_vcd"
    state: "present"
    operation: "poweron"
    template_name: "CentOS7 x86_64 1804"

上一个示例是使用vca_vapp模块的一个非常基本的示例,以确保存在名为Example的 vApp 并且处于开启状态。

VMware vSphere

VMware vSphere 是由 VMware 组件组成的软件套件。这就是 VMware 可能会让人有点困惑的地方,因为 VMware vSphere 由 VMware vCentre 和 VMware ESXi 组成,它们各自也有自己的 Ansible 模块,并且在表面上,它们似乎完成了类似的任务。

vmware_cluster 模块

该模块允许您管理您的 VMware vSphere 集群。VMware vSphere 集群是一组主机,当它们被集群在一起时,它们共享资源,允许您添加高可用性HA),并且还可以启动分布式资源调度器DRS),它管理集群中工作负载的放置:

- name: Create a cluster
  vmware_cluster:
    hostname: "{{ item.ip }}"
    datacenter_name: "my_datacenter"
    cluster_name: "cluster"
    enable_ha: "yes"
    enable_drs: "yes"
    enable_vsan: "yes"
    username: "{{ item.username }}"
    password: "{{ item.password }}"
  with_items: "{{ vsphere_hosts }}"

前面的代码将循环遍历主机、用户名和密码列表以创建一个集群。

vmware_datacenter 模块

VMware vSphere 数据中心是指支持您的集群的物理资源、主机、存储和网络的集合的名称:

- name: Create a datacenter
  vmware_datacenter:
    hostname: "{{ item.ip }}"
    username: "{{ item.username }}"
    password: "{{ item.password }}"
    datacenter_name: "my_datacenter"
    state: present
  with_items: "{{ vsphere_hosts }}"

上一个示例将vsphere_hosts中列出的主机添加到my_datacenter VMware vSphere 数据中心。

vmware_vm_facts 模块

该模块可用于收集运行在您的 VMware vSphere 集群中的虚拟机或模板的信息:

- name: Gather facts on all VMs in the cluster
  vmware_vm_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    vm_type: "vm"
  delegate_to: "localhost"
  register: vm_facts

上一个示例仅收集了在我们的集群中创建的虚拟机的信息,并将结果注册为vm_facts变量。如果我们想要找到有关模板的信息,我们可以将vm_type更新为 template,或者我们可以通过将vm_type更新为 all 来列出所有虚拟机和模板。

vmware_vm_shell 模块

该模块可用于连接到使用 VMware 的虚拟机并运行 shell 命令。在任何时候,Ansible 都不需要使用诸如 SSH 之类的基于网络的服务连接到虚拟机,这对于在虚拟机上线之前配置 VM 非常有用:

- name: Shell example
  vmware_vm_shell:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    datacenter: "my_datacenter"
    folder: "/my_vms"
    vm_id: "example_vm"
    vm_username: "root"
    vm_password: "supersecretpassword"
    vm_shell: "/bin/cat"
    vm_shell_args: " results_file "
    vm_shell_env:
      - "PATH=/bin"
      - "VAR=test"
    vm_shell_cwd: "/tmp"
  delegate_to: "localhost"
  register: shell_results

上一个示例连接到名为example_vm的 VM,该 VM 存储在my_datacenter数据中心根目录下的my_vms文件夹中。一旦使用我们提供的用户名和密码连接后,它将运行以下命令:

$ /bin/cat results_file

在 VM 的/tmp文件夹中,运行命令的输出被注册为shell_results,以便我们以后可以使用它。

vmware_vm_vm_drs_rule 模块

使用此模块,您可以配置 VMware DRS 亲和性规则。这允许您控制集群中虚拟机的放置:

- name: Create DRS Affinity Rule for VM-VM
  vmware_vm_vm_drs_rule:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    cluster_name: "cluster"
    vms: "{{ item }}"
    drs_rule_name: ""
    enabled: "True"
    mandatory: "True"
    affinity_rule: "True"
  with_items:
    - "example_vm"
    - "another_example_vm"

在上一个示例中,我们正在创建一个规则,使得 VMs example_vmanother_example_vm永远不会在同一台物理主机上运行。

vmware_vm_vss_dvs_migrate 模块

该模块将指定的虚拟机从标准 vSwitch 迁移到分布式 vSwitch,后者可在整个集群中使用。

- name: migrate vm to dvs
  vmware_vm_vss_dvs_migrate"
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    vm_name: "example_vm"
    dvportgroup_name: "example_portgroup"
  delegate_to: localhost

正如你所看到的,我们正在将example_vm从标准 vSwitch 移动到名为example_portgroup的分布式 vSwitch。

vsphere_copy 模块

该模块有一个单一的目的——将本地文件复制到远程数据存储:

- name: copy file to datastore
  vsphere_copy:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    src: "/path/to/local/file"
    datacenter: "my_datacenter"
    datastore: "my_datastore"
    path: "path/to/remove/file"
  transport: local

正如你所看到的,我们正在将文件从/path/to/local/file复制到my_datacenter数据中心中托管的my_datastore数据存储中的path/to/remove/file

vsphere_guest 模块

该模块已被弃用,并将在 Ansible 2.9 中删除;建议您改用vmware_guest模块。

VMware vCentre

VMware vCentre 是 VMware vSphere 套件的重要组件;它使诸如 vMotion、VMware 分布式资源调度器和 VMware 高可用性等功能进行集群化。

vcenter_folder 模块

此模块使 vCenter 文件夹管理成为可能。例如,以下示例为您的虚拟机创建一个文件夹:

- name: Create a vm folder
  vcenter_folder:
    hostname: "{{ item.ip }}"
    username: "{{ item.username }}"
    password: "{{ item.password }}"
    datacenter_name: "my_datacenter"
    folder_name: "virtual_machines"
    folder_type: "vm"
    state: "present"

以下是为您的主机创建文件夹的示例:

- name: Create a host folder
  vcenter_folder:
    hostname: "{{ item.ip }}"
    username: "{{ item.username }}"
    password: "{{ item.password }}"
    datacenter_name: "my_datacenter"
    folder_name: "hosts"
    folder_type: "host"
    state: "present"

vcenter_license 模块

此模块允许您添加和删除 VMware vCenter 许可证:

- name: Add a license
  vcenter_license:
    hostname: "{{ item.ip }}"
    username: "{{ item.username }}"
    password: "{{ item.password }}"
    license: "123abc-456def-abc456-def123"
    state: "present"
  delegate_to: localhost

vmware_guest 模块

此模块允许您在 VMware 集群中启动和管理虚拟机;以下示例显示了如何使用模板启动 VM:

- name: Create a VM from a template
  vmware_guest:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my-datacenter"
    folder: "/vms"
    name: "yet_another_example_vm"
    state: "poweredon"
    template: "centos7-x86_64-1804"
    disk:
      - size_gb: "40"
        type: "thin"
        datastore: "my_datastore"
    hardware:
      memory_mb: "4048"
      num_cpus: "4"
      max_connections: "3"
      hotadd_cpu: "True"
      hotremove_cpu: "True"
      hotadd_memory: "True"
    networks:
      - name: "VM Network"
        ip: "192.168.1.100"
        netmask: "255.255.255.0"
        gateway: "192.168.1.254"
        dns_servers:
          - "192.168.1.1"
          - "192.168.1.2"
    wait_for_ip_address: "yes"
  delegate_to: "localhost"
  register: deploy

正如您所看到的,我们对 VM 及其配置有相当多的控制权。硬件、网络和存储配置有单独的部分;我们将在本章末稍微详细地看一下这个模块。

vmware_guest_facts 模块

此模块收集有关已创建的 VM 的信息:

- name: Gather facts on the yet_another_example_vm vm
  vmware_guest_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my-datacenter"
    folder: "/vms"
    name: "yet_another_example_vm"
  delegate_to: localhost
  register: facts

前面的示例收集了我们在上一节中定义的机器的大量信息,并将信息注册为变量,以便我们可以在 playbook 运行的其他地方使用它。

vmware_guest_file_operation 模块

此模块是在 Ansible 2.5 中引入的;它允许您在 VM 上添加和获取文件,而无需 VM 连接到网络。它还允许您在 VM 内创建文件夹。以下示例在 VM 内创建一个目录:

- name: create a directory on a vm
  vmware_guest_file_operation:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my-datacenter"
    vm_id: "yet_another_example_vm"
    vm_username: "root"
    vm_password: "supersecretpassword"
    directory:
      path: "/tmp/imported/files"
      operation: "create"
      recurse: "yes"
  delegate_to: localhost

以下示例将名为config.zip的文件从我们的 Ansible 主机复制到先前创建的目录中:

- name: copy file to vm
  vmware_guest_file_operation:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my-datacenter"
    vm_id: "yet_another_example_vm"
    vm_username: "root"
    vm_password: "supersecretpassword"
    copy:
        src: "files/config.zip"
        dest: "/tmp/imported/files/config.zip"
        overwrite: "False"
  delegate_to: localhost

vmware_guest_find 模块

我们知道 VM 运行的文件夹的名称。如果我们不知道,或者由于任何原因发生了更改,我们可以使用vmware_guest_find模块动态发现位置:

- name: Find vm folder location
  vmware_guest_find:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    name: "yet_another_example_vm"
  register: vm_folder

文件夹的名称将注册为vm_folder

vmware_guest_powerstate 模块

这个模块很容易理解;它用于管理 VM 的电源状态。以下示例重新启动了一个 VM:

- name: Powercycle a vm
  vmware_guest_powerstate:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    folder: "/vms"
    name: "yet_another_example_vm"
    state: "reboot-guest"
  delegate_to: localhost

您还可以安排对电源状态的更改。以下示例在 2019 年 4 月 1 日上午 9 点关闭 VM:

- name: April fools
  vmware_guest_powerstate:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    folder: "/vms"
    name: "yet_another_example_vm"
    state: "powered-off"
    scheduled_at: "01/04/2019 09:00"
  delegate_to: localhost

并不是我会做这样的事情!

vmware_guest_snapshot 模块

此模块允许您管理 VM 快照;例如,以下创建了一个快照:

- name: Create a snapshot
  vmware_guest_snapshot:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my-datacenter"
    folder: "/vms"
    name: "yet_another_example_vm"
    snapshot_name: "pre-patching"
    description: "snapshot made before patching"
    state: "present"
  delegate_to: localhost

从前面的示例中可以看出,这个快照是因为我们即将对 VM 进行打补丁。如果打补丁顺利进行,那么我们可以运行以下任务:

- name: Remove a snapshot
  vmware_guest_snapshot:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my-datacenter"
    folder: "/vms"
    name: "yet_another_example_vm"
    snapshot_name: "pre-patching"
    state: "remove"
  delegate_to: localhost

如果一切不如预期那样进行,打补丁破坏了我们的 VM,那么不用担心,我们有一个可以恢复的快照:

- name: Revert to a snapshot
  vmware_guest_snapshot:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my-datacenter"
    folder: "/vms"
    name: "yet_another_example_vm"
    snapshot_name: "pre-patching"
    state: "revert"
  delegate_to: localhost

祈祷您永远不必恢复快照(除非计划中)。

vmware_guest_tools_wait 模块

本节的最后一个模块是另一个很容易理解的模块;它只是等待 VMware tools 可用,然后收集有关该机器的信息:

- name: Wait for VMware tools to become available by name
  vmware_guest_tools_wait:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    folder: "/vms"
    name: "yet_another_example_vm"
  delegate_to: localhost
  register: facts

VMware tools 是在 VM 内部运行的应用程序。一旦启动,它允许 VMware 与 VM 进行交互,从而使诸如vmware_guest_file_operationvmware_vm_shell等模块能够正常运行。

VMware ESXi

在大多数 VMware 安装的核心是一些 VMware ESXi 主机。VMware ESXi 是一种类型 1 的 hypervisor,可以使 VM 运行。Ansible 提供了几个模块,允许您配置和与您的 VMware ESXi 主机进行交互。

vmware_dns_config 模块

此模块允许您管理 ESXi 主机的 DNS 方面;它允许您设置主机名、域和 DNS 解析器:

- name: Configure the hostname and dns servers
  local_action
    module: vmware_dns_config:
    hostname: "{{ exsi_host }}"
    username: "{{ exsi_username }}"
    password: "{{ exsi_password }}"
    validate_certs: "no"
    change_hostname_to: "esxi-host-01"
    domainname: "my-domain.com"
    dns_servers:
        - "8.8.8.8"
        - "8.8.4.4"

在前面的示例中,我们将主机的 FQDN 设置为esxi-host-01.my-domain.com,并配置主机使用 Google 公共 DNS 解析器。

vmware_host_dns_facts 模块

一个简单的模块,用于收集您的 VMware ESXi 主机的 DNS 配置信息:

- name: gather facts on dns config
  vmware_host_dns_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"

vmware_host 模块

您可以使用此模块将您的 ESXi 主机附加到 vCenter:

- name: add an esxi host to vcenter
  vmware_host:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    datacenter_name: "my-datacenter"
    cluster_name: "my_cluster"
    esxi_hostname: "{{ exsi_host }}"
    esxi_username: "{{ exsi_username }}"
    esxi_password: "{{ exsi_password }}"
    state: present

您还可以使用该模块重新连接主机到您的 vCenter 集群:

- name: reattach an esxi host to vcenter
  vmware_host:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    datacenter_name: "my-datacenter"
    cluster_name: "my_cluster"
    esxi_hostname: "{{ exsi_host }}"
    esxi_username: "{{ exsi_username }}"
    esxi_password: "{{ exsi_password }}"
    state: reconnect

您还可以从 vCenter 集群中删除主机:

- name: remove an esxi host to vcenter
  vmware_host:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    datacenter_name: "my-datacenter"
    cluster_name: "my_cluster"
    esxi_hostname: "{{ exsi_host }}"
    esxi_username: "{{ exsi_username }}"
    esxi_password: "{{ exsi_password }}"
    state: absent

vmware_host_facts 模块

正如您可能已经猜到的那样,此模块收集有关您的 vSphere 或 vCenter 集群中的 VMware ESXi 主机的信息:

- name: Find out facts on the esxi hosts
  vmware_host_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
  register: host_facts
  delegate_to: localhost

vmware_host_acceptance 模块

使用此模块,您可以管理 VMware ESXi 主机的接受级别。VMware 支持四个接受级别,它们是:

  • VMwareCertified

  • VMwareAccepted

  • PartnerSupported

  • CommunitySupported

这些级别控制着可以安装在 ESXi 主机上的 VIB;VIB 是 ESXi 软件包。这通常决定了您将从 VMware 或 VMware 合作伙伴那里获得的支持水平。以下任务将为指定集群中的所有 ESXi 主机设置接受级别为 CommunitySupported:

- name: Set acceptance level for all esxi hosts in the cluster
  vmware_host_acceptance:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"
    acceptance_level: "community"
    state: present
  register: cluster_acceptance_level

vmware_host_config_manager 模块

使用此模块,您可以在各个 VMware ESXi 主机上设置配置选项,例如:

- name: Set some options on our esxi host
  vmware_host_config_manager:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ exsi_host }}"
    options:
        "Config.HostAgent.log.level": "verbose"
        "Annotations.WelcomeMessage": "Welcome to my awesome Ansible managed ESXi host"
        "Config.HostAgent.plugins.solo.enableMob": "false"

Ansible 将从您的 VMware 主机映射高级配置选项,因此有关可用选项的更多信息,请参阅您的文档。

vmware_host_datastore 模块

此模块使您能够在 VMware ESXi 主机上挂载和卸载数据存储;在以下示例中,我们正在在清单中的所有 VMware ESXi 主机上挂载三个数据存储:

- name: Mount datastores on our cluster
  vmware_host_datastore:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter_name: "my-datacenter"
    datastore_name: "{{ item.name }}"
    datastore_type: "{{ item.type }}"
    nfs_server: "{{ item.server }}"
    nfs_path: "{{ item.path }}"
    nfs_ro: "no"
    esxi_hostname: "{{ inventory_hostname }}"
    state: present
  delegate_to: localhost
  with_items:
      - { "name": "ds_vol01", "server": "nas", "path": "/mnt/ds_vol01", 'type': "nfs"} 
      - { "name": "ds_vol02", "server": "nas", "path": "/mnt/ds_vol02", 'type': "nfs"} 
      - { "name": "ds_vol03", "server": "nas", "path": "/mnt/ds_vol03", 'type': "nfs"} 

vmware_host_firewall_manager 模块

此模块允许您配置 VMware ESXi 主机上的防火墙规则:

- name: set some firewall rules on the esxi hosts
  vmware_host_firewall_manager:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ inventory_hostname }}"
    rules:
      - name: "vvold"
        enabled: "True"
      - name: "CIMHttpServer"
        enabled: "False"

上一个示例在主机清单中的每个 VMware ESXi 主机上启用了vvold并禁用了CIMHttpServer

vmware_host_firewall_facts 模块

正如您可能已经猜到的那样,此模块与其他事实模块一样,用于收集我们集群中所有主机的防火墙配置的信息:

- name: Get facts on all cluster hosts
  vmware_host_firewall_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"

它也可以仅收集单个主机的信息:

- name: Get facts on a single host
  vmware_host_firewall_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ exsi_host }}"

vmware_host_lockdown 模块

此模块带有一个警告,内容为:此模块具有破坏性,因为管理员权限是使用 API 管理的,请仔细阅读选项并继续。

您可以使用以下代码锁定主机:

- name: Lockdown an ESXi host
  vmware_host_lockdown:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ exsi_host }}"
    state: "present"

您可以使用以下方法将主机解除锁定:

- name: Remove the lockdown on an ESXi host
  vmware_host_lockdown:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ exsi_host }}"
    state: "absent"

如先前提到的,此模块可能会产生一些意想不到的副作用,因此您可能希望逐个主机执行此操作,而不是使用以下选项,该选项将使指定集群中的所有主机进入锁定状态:

- name: Lockdown all the ESXi hosts
  vmware_host_lockdown:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"
    state: "present"

vmware_host_ntp 模块

使用此模块,您可以管理每个 VMware ESXi 主机的 NTP 设置。以下示例配置所有主机使用相同的 NTP 服务器:

- name: Set NTP servers for all hosts
  vmware_host_ntp:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"
    state: present
    ntp_servers:
        - 0.pool.ntp.org
        - 1.pool.ntp.org
        - 2.pool.ntp.org

vmware_host_package_facts 模块

此模块可用于收集有关您集群中所有 VMware ESXi 主机的信息:

- name: Find out facts about the packages on all the ESXi hosts
  vmware_host_package_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"
  register: cluster_packages

与其他事实模块一样,它也可以仅收集单个主机的信息:

- name: Find out facts about the packages on a single ESXi host
  vmware_host_package_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ exsi_host }}"
  register: host_packages

vmware_host_service_manager 模块

此模块可让您管理集群成员或单个主机上的 ESXi 服务器:

- name: Start the ntp service on all esxi hosts
  vmware_host_service_manager:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"
    service_name: "ntpd"
    service_policy: "automatic"
    state: "present"

在此示例中,我们正在启动集群中所有主机的 NTP 服务(service_name);由于我们将service_policy定义为automatic,因此只有在配置了与防火墙规则相对应的服务时,服务才会启动。如果我们希望服务无论防火墙规则如何都启动,那么我们可以将service_policy设置为on,或者如果希望停止服务,则应将service_policy设置为off

vmware_host_service_facts 模块

使用此模块,您可以查找集群中每个 VMware ESXi 主机上配置的服务的信息:

- name: Find out facts about the services on all the ESXi hosts
  vmware_host_service_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"
  register: cluster_services

vmware_datastore_facts 模块

这是一个旧式事实模块,可用于收集数据中心中配置的数据存储的信息:

- name: Find out facts about the datastores
  vmware_datastore_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my_datacenter"
  delegate_to: localhost
  register: datastore_facts

您可能会注意到这个和之前的事实模块之间的语法有一点不同。

vmware_host_vmnic_facts 模块

从旧式事实模块返回到新模块,此模块可用于收集有关 VMware ESXi 主机上物理网络接口的信息:

- name: Find out facts about the vmnics on all the ESXi hosts
  vmware_host_vmnic_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my_datacenter"
  register: cluster_vmnics

对于单个 ESXi 主机,我们可以使用以下任务:

- name: Find out facts about the vmnics on a single ESXi host
  vmware_host_vmnic_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ exsi_host }}"
  register: host_vmnics

vmware_local_role_manager 模块

使用此模块,您可以在集群上配置角色;这些角色可用于分配特权。在以下示例中,我们正在为vmware_qa角色分配一些特权:

- name: Add a local role
  vmware_local_role_manager:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
  local_role_name: "vmware_qa"
  local_privilege_ids: [ "Folder.Create", "Folder.Delete"]
  state: "present"

vmware_local_user_manager 模块

使用此模块,您可以通过添加用户并设置其密码来管理本地用户:

- name: Add local user to ESXi
  vmware_local_user_manager:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    local_user_name: "myuser"
    local_user_password: "my-super-secret-password"
    local_user_description: "An example user added by Ansible"
  delegate_to: "localhost"

vmware_cfg_backup 模块

使用此模块,您可以创建 VMware ESXi 主机配置的备份:

- name: Create an esxi host configuration backup
  vmware_cfg_backup:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    state: "saved"
    dest: "/tmp/"
    esxi_hostname: "{{ exsi_host }}"
  delegate_to: "localhost"
  register: cfg_backup

请注意,此模块将自动将主机置于维护状态,然后保存配置。在前面的示例中,您可以使用fetch模块使用/tmp中注册的信息来获取备份的副本。

您还可以使用此模块恢复配置:

- name: Restore an esxi host configuration backup
  vmware_cfg_backup:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    state: "loaded"
    dest: "/tmp/my-host-backup.tar.gz"
    esxi_hostname: "{{ exsi_host }}"
  delegate_to: "localhost"

最后,您还可以通过运行以下代码将主机配置重置为默认设置:

- name: Reset a host configuration to the default values
  vmware_cfg_backup:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    state: "absent"
    esxi_hostname: "{{ exsi_host }}"
  delegate_to: "localhost"

vmware_vmkernel 模块

此模块允许您在主机上添加 VMkernel 接口,也称为虚拟 NIC:

- name: Add management port with a static ip
   vmware_vmkernel:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ exsi_host }}"
  vswitch_name: "my_vSwitch"
  portgroup_name: "my_portgroup"
  vlan_id: "the_vlan_id"
  network:
    type: "static"
    ip_address: "192.168.127.10"
    subnet_mask: "255.255.255.0"
  state: "present"
  enable_mgmt: "True"

在前面的示例中,我们添加了一个管理接口;还有以下选项:

  • enable_ft:启用容错流量的接口

  • enable_mgmt:启用管理流量的接口

  • enable_vmotion:启用 VMotion 流量的接口

  • enable_vsan:启用 VSAN 流量的接口

vmware_vmkernel_facts 模块

另一个事实模块,这是一个新式模块;您可能已经猜到任务的样子:

- name: Find out facts about the vmkernel on all the ESXi hosts
  vmware_vmkernel_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"
  register: cluster_vmks

- name: Find out facts about the vmkernel on a single ESXi host
  vmware_vmkernel_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ exsi_host }}"
  register: host_vmks

vmware_target_canonical_facts 模块

使用此模块,您可以找出 SCSI 目标的规范名称;您只需要知道目标设备的 ID:

- name: Get Canonical name of SCSI device
  vmware_target_canonical_facts"
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    target_id: "6"
   register: canonical_name

vmware_vmotion 模块

您可以使用此模块执行虚拟机从一个 VMware ESXi 主机迁移到另一个主机的 vMotion:

- name: Perform vMotion of VM
  vmware_vmotion
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    vm_name: "example_vm"
    destination_host: "esxi-host-02"
  delegate_to: "localhost"
  register: vmotion_results

vmware_vsan_cluster 模块

您可以使用此模块注册 VSAN 集群;此模块的工作方式与本章中的其他模块略有不同,您首先需要在单个主机上生成集群 UUID,然后再使用生成的 UUID 在其余主机上部署 VSAN。

以下任务假定您有一个名为esxi_hosts的主机组,其中包含多个主机。第一个任务将 VSAN 分配给组中的第一个主机,然后注册结果:

- name: Configure VSAN on first host in the group
  vmware_vsan_cluster:
    hostname: "{{ groups['esxi_hosts'][0] }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
  register: vsan_cluster

作为vsan_cluster注册的结果包含我们将需要在组中其余主机上使用的 VSAN 集群 UUID。以下代码配置了其余主机上的集群,跳过原始主机:

- name: Configure VSAN on the remaining hosts in the group
  vmware_vsan_cluster:
    hostname: "{{ item }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    cluster_uuid: "{{ vsan_cluster.cluster_uuid }}"
  with_items: "{{ groups['esxi_hosts'][1:] }}"

vmware_vswitch 模块

使用此模块,您可以向 ESXi 主机添加或删除VMware 标准交换机vSwitch):

- name: Add a vSwitch
  vmware_vswitch:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    switch: "vswitch_name"
    nics:
      - "vmnic1"
      - "vmnic2"
    mtu: "9000"
  delegate_to: "localhost"

在此示例中,我们添加了一个连接到多个 vmnic 的 vSwitch。

vmware_drs_rule_facts 模块

您可以使用此模块收集整个集群或单个数据中心中配置的 DRS 的事实:

- name: Find out facts about drs on all the hosts in the cluster
  vmware_drs_rule_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"
  delegate_to: "localhost"
  register: cluster_drs

- name: Find out facts about drs in a single data center
  vmware_drs_rule_facts:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my_datacenter"
  delegate_to: "localhost"
  register: datacenter_drs

vmware_dvswitch 模块

此模块允许您创建和删除分布式 vSwitches:

- name: Create dvswitch
  vmware_dvswitch:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my_datacenter"
    switch_name: "my_dvSwitch"
    switch_version: "6.0.0"
    mtu: "9000"
    uplink_quantity: "2"
    discovery_proto: "lldp"
    discovery_operation: "both"
    state: present
  delegate_to: "localhost"

vmware_dvs_host 模块

使用此模块,您可以向分布式虚拟交换机添加或删除主机:

- name: Add host to dvs
  vmware_dvs_host:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ exsi_host }}"
    switch_name: "my_dvSwitch"
    vmnics:
      - "vmnic1"
      - "vmnic2"
    state: "present"
  delegate_to: "localhost"

vmware_dvs_portgroup 模块

使用此模块,您可以管理您的 DVS 端口组:

- name: Create a portgroup with vlan 
  vmware_dvs_portgroup:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    portgroup_name: "my_portgroup_vlan123"
    switch_name: "my_dvSwitch"
    vlan_id: "123"
    num_ports: "120"
    portgroup_type: "earlyBinding"
    state: "present"
  delegate_to: "localhost"

vmware_maintenancemode 模块

使用此模块,您可以将主机置于维护模式。以下示例向您展示了如何在 VSAN 上保持对象可用性的同时将主机置于维护模式:

- name: Put host into maintenance mode
  vmware_maintenancemode:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    esxi_hostname: "{{ exsi_host }}"
    vsan: "ensureObjectAccessibility"
    evacuate: "yes"
    timeout: "3600"
    state: "present"
  delegate_to: "localhost"

vmware_portgroup 模块

此模块允许您在给定集群中的主机上创建 VMware 端口组:

- name: Create a portgroup with vlan
  vmware_portgroup:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    cluster_name: "my_cluster"
    switch_name: "my_switch"
    portgroup_name: "my_portgroup_vlan123"
    vlan_id: "123"
  delegate_to: "localhost"

vmware_resource_pool 模块

使用这个,我们要看的最后一个模块,您可以创建一个资源池。以下是如何执行此操作的示例:

- name: Add resource pool
  vmware_resource_pool:
    hostname: "{{ vsphere_host }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: "no"
    datacenter: "my_datacenter"
    cluster: "my_new_cluster"
    resource_pool: "my_resource_pool"
    mem_shares: "normal"
    mem_limit: "-1"
    mem_reservation: "0"
    mem_expandable_reservations: "True"
    cpu_shares: "normal"
    cpu_limit: "-1"
    cpu_reservation: "0"
    cpu_expandable_reservations: "True"
    state: present
  delegate_to: "localhost"

一个示例 playbook

在完成本章之前,我将分享一个我为在 VMware 集群中部署少量虚拟机编写的示例 playbook。该项目的想法是将七台虚拟机启动到客户的网络中,如下所示:

  • 一个 Linux 跳板主机

  • 一个 NTP 服务器

  • 一个负载均衡器

  • 两个 Web 服务器

  • 两个数据库服务器

所有 VM 都必须从现有模板构建;不幸的是,这个模板是使用/etc/sysconfig/network文件中硬编码的网关 IP 地址192.168.1.254构建的。这意味着为了让这些机器正确出现在网络上,我必须在每台虚拟机启动后进行更改。

我首先在我的group_vars文件夹中设置了一个名为vmware.yml的文件;其中包含了连接到我的 VMware 安装所需的信息,以及 VM 的默认凭据:

vcenter:
  host: "cluster.cloud.local"
  username: "svc_ansible@cloud.local"
  password: "mymegasecretpassword"

wait_for_ip_address: "yes"
machine_state: "poweredon"

deploy:
  datacenter: "Cloud DC4"
  folder: "/vm/Ansible"
  resource_pool: "/Resources/"

vm_shell:
  username: "root"
  password: "hushdonttell"
  cwd: "/tmp"
  cmd: "/bin/sed"
  args: "-i 's/GATEWAY=192.168.1.254/GATEWAY={{ item.gateway }}/g' /etc/sysconfig/network"

我将使用两个角色中定义的变量。接下来是group_vars/vms.yml文件;其中包含了在我的 VMware 环境中启动虚拟机所需的所有信息:

vm:
  - name: "NTPSERVER01"
    machine_name: "ntpserver01"
    machine_template: "RHEL6_TEMPLATE"
    guest_id: "rhel6_64Guest"
    host: "compute-host-01.cloud.local"
    cpu: "1"
    ram: "1024"
    networks: 
      - name: "CLOUD-CUST|Customer|MANGMENT"
        ip: "192.168.99.10"
        netmask: "255.255.255.0"
        device_type: "vmxnet3"
    gateway: "192.168.99.254"
    disk:
      - size_gb: "30"
        type: "thin"
        datastore: "cust_sas_esx_nfs_01"
  - name: "JUMPHOST01"
    machine_name: "jumphost01"
    machine_template: "RHEL6_TEMPLATE"
    guest_id: "rhel6_64Guest"
    host: "compute-host-02.cloud.local"
    cpu: "1"
    ram: "1024"
    networks: 
      - name: "CLOUD-CUST|Customer|MANGMENT"
        ip: "192.168.99.20"
        netmask: "255.255.255.0"
        device_type: "vmxnet3"
    gateway: "192.168.99.254"
    disk:
      - size_gb: "30"
        type: "thin"
        datastore: "cust_sas_esx_nfs_01"
  - name: "LOADBALANCER01"
    machine_name: "loadbalancer01"
    machine_template: "LB_TEMPLATE"
    guest_id: "rhel6_64Guest"
    host: "compute-host-03.cloud.local"
    cpu: "4"
    ram: "4048"
    networks: 
      - name: "CLOUD-CUST|Customer|DMZ"
        ip: "192.168.98.100"
        netmask: "255.255.255.0"
        device_type: "vmxnet3"
    gateway: "192.168.99.254"
    disk:
      - size_gb: "30"
        type: "thin"
        datastore: "cust_sas_esx_nfs_02"    
  - name: "WEBSERVER01"
    machine_name: "webserver01"
    machine_template: "RHEL6_TEMPLATE"
    guest_id: "rhel6_64Guest"
    host: "compute-host-01.cloud.local"
    cpu: "1"
    ram: "1024"
    networks: 
      - name: "CLOUD-CUST|Customer|APP"
        ip: "192.168.100.10"
        netmask: "255.255.255.0"
        device_type: "vmxnet3"
    gateway: "192.168.100.254"
    disk:
      - size_gb: "30"
        type: "thin"
        datastore: "cust_sas_esx_nfs_01"
  - name: "WEBSERVER02"
    machine_name: "webserver02"
    machine_template: "RHEL6_TEMPLATE"
    guest_id: "rhel6_64Guest"
    host: "compute-host-02.cloud.local"
    cpu: "1"
    ram: "1024"
    networks: 
      - name: "CLOUD-CUST|Customer|APP"
        ip: "192.168.100.20"
        netmask: "255.255.255.0"
        device_type: "vmxnet3"
    gateway: "192.168.100.254"
    disk:
      - size_gb: "30"
        type: "thin"
        datastore: "cust_sas_esx_nfs_02"      
  - name: "DBSERVER01"
    machine_name: "dbserver01"
    machine_template: "RHEL6_TEMPLATE"
    guest_id: "rhel6_64Guest"
    host: "compute-host-10.cloud.local"
    cpu: "8"
    ram: "32000"
    networks: 
      - name: "CLOUD-CUST|Customer|DB"
        ip: "192.168.101.10"
        netmask: "255.255.255.0"
        device_type: "vmxnet3"
    gateway: "192.168.101.254"
    disk:
      - size_gb: "30"
        type: "thin"
        datastore: "cust_sas_esx_nfs_01"
      - size_gb: "250"
        type: "thick"
        datastore: "cust_ssd_esx_nfs_01" 
      - size_gb: "250"
        type: "thick"
        datastore: "cust_ssd_esx_nfs_01" 
      - size_gb: "250"
        type: "thick"
        datastore: "cust_ssd_esx_nfs_01" 
  - name: "DBSERVER02"
    machine_name: "dbserver02"
    machine_template: "RHEL6_TEMPLATE"
    guest_id: "rhel6_64Guest"
    host: "compute-host-11.cloud.local"
    cpu: "8"
    ram: "32000"
    networks: 
      - name: "CLOUD-CUST|Customer|DB"
        ip: "192.168.101.11"
        netmask: "255.255.255.0"
        device_type: "vmxnet3"
    gateway: "192.168.101.254"
    disk:
      - size_gb: "30"
        type: "thin"
        datastore: "cust_sas_esx_nfs_02"
      - size_gb: "250"
        type: "thick"
        datastore: "cust_ssd_esx_nfs_02" 
      - size_gb: "250"
        type: "thick"
        datastore: "cust_ssd_esx_nfs_02" 
      - size_gb: "250"
        type: "thick"
        datastore: "cust_ssd_esx_nfs_02"

正如你所看到的,我正在为所有七台 VM 定义规格、网络和存储;在可能的情况下,我正在进行存储的薄配置,并确保在一个角色中有多个虚拟机时,我正在使用不同的存储池。

现在我已经拥有了我虚拟机所需的所有细节,我可以创建角色了。首先是roles/vmware/tasks/main.yml

- name: Launch the VMs
  vmware_guest:
    hostname: "{{vcenter.host}}"
    username: "{{ vcenter.username }}"
    password: "{{ vcenter.password }}"
    validate_certs: no
    datacenter: "{{ deploy.datacenter }}"
    folder: "{{ deploy.folder }}"
    name: "{{ item.machine_name | upper }}"
    state: "{{ machine_state }}"
    guest_id: "{{ item.guest_id }}"
    esxi_hostname: "{{ item.host }}"
    hardware:
      memory_mb: "{{ item.ram }}"
      num_cpus: "{{ item.cpu }}"
    networks: "{{ item.networks }}"
    disk: "{{ item.disk }}"
    template: "{{ item.machine_template }}"
    wait_for_ip_address: "{{ wait_for_ip_address }}"
    customization:
      hostname: "{{ item.machine_name | lower }}"
  with_items: "{{ vm }}"

正如你所看到的,这个任务循环遍历vm变量中的项目;一旦虚拟机启动,它将等待我分配的 IP 地址在 VMware 中可用。这确保了在启动下一个虚拟机或继续下一个角色之前,虚拟机已经正确启动。

下一个角色解决了在虚拟机模板中硬编码为192.168.1.254的网关的问题;它可以在roles/fix/tasks/main.yml中找到。该角色中有两个任务;第一个任务将网关更新为虚拟机所在网络的正确网关:

- name: Sort out the wrong IP address in the /etc/sysconfig/network file on the vms
  vmware_vm_shell:
    hostname: "{{vcenter.host}}"
    username: "{{ vcenter.username }}"
    password: "{{ vcenter.password }}"
    validate_certs: no
    vm_id: "{{ item.machine_name | upper }}"
    vm_username: "{{ vm_shell.username }}"
    vm_password: "{{ vm_shell.password }}"
    vm_shell: "{{ vm_shell.cmd }}"
    vm_shell_args: " {{ vm_shell.args }} "
    vm_shell_cwd: "{{ vm_shell.cwd }}"
  with_items: "{{ vm }}"

正如你所看到的,这个任务循环遍历定义为vm的虚拟机列表,并执行我们在group_vars/vmware.yml文件中定义的sed命令。一旦这个任务运行完毕,我们需要再运行一个任务。这个任务重新启动所有虚拟机上的网络,以便网关的更改被接受:

- name: Restart networking on all VMs
  vmware_vm_shell:
    hostname: "{{vcenter.host}}"
    username: "{{ vcenter.username }}"
    password: "{{ vcenter.password }}"
    validate_certs: no
    vm_id: "{{ item.machine_name | upper }}"
    vm_username: "{{ vm_shell.username }}"
    vm_password: "{{ vm_shell.password }}"
    vm_shell: "/sbin/service"
    vm_shell_args: "network restart"
  with_items: "{{ vm }}"

当我运行 playbook 时,大约需要 30 分钟才能运行完,但最终我启动了七台虚拟机,并且可以使用,所以我随后能够运行一系列的 playbooks,对环境进行引导,以便我可以将它们交给客户,让他们部署他们的应用程序。

总结

正如你从非常长的模块列表中所看到的,你可以使用 Ansible 来完成大部分作为 VMware 管理员的常见任务。再加上我们在第七章中所看到的核心网络模块,用于管理网络设备,以及支持 NetApp 存储设备的模块,你可以构建一些跨物理设备、VMware 元素甚至在虚拟化基础设施中运行的虚拟机的复杂 playbooks。

在下一章中,我们将看到如何使用 Vagrant 在本地构建我们的 Windows 服务器,然后将我们的 playbooks 移到公共云。

问题

  1. 你需要在你的 Ansible 控制器上安装哪个 Python 模块才能与 vSphere 进行交互?

  2. 真或假:vmware_dns_config只允许你在你的 ESXi 主机上设置 DNS 解析器。

  3. 列举我们已经涵盖的两个可以用来启动虚拟机的模块的名称;有三个,但其中一个已被弃用。

  4. 我们已经查看的模块中,你会使用哪一个来确保虚拟机在进行与 VMware 交互的任务之前完全可用?

  5. 真或假:使用 Ansible 可以安排更改电源状态。

进一步阅读

关于 VMware vSphere 的一个很好的概述,我推荐观看以下视频:www.youtube.com/watch?v=3OvrKZYnzjM

第十二章:Ansible Windows 模块

到目前为止,我们一直在针对 Linux 服务器进行操作。在本章中,我们将看一下支持和与基于 Windows 的服务器进行交互的核心 Ansible 模块的不断增长的集合。就个人而言,来自几乎完全是 macOS 和 Linux 背景,使用一个在 Windows 上没有本地支持的工具来管理 Windows 感觉有点奇怪。

然而,我相信在本章结束时,您会同意,它的开发人员已经尽可能地使将 Windows 工作负载引入到您的 playbook 中的过程变得无缝和熟悉。

在本章中,我们将学习如何使用 Vagrant 在本地构建我们的 Windows 服务器,然后将我们的 playbooks 移到公共云。我们将涵盖:

  • 在 Windows 中启用功能

  • 在 AWS 中启动 Windows 实例

  • 创建用户

  • 使用 Chocolatey 安装第三方软件包

技术要求

与上一章一样,我们将使用 Vagrant 和 AWS。我们将使用的 Vagrant box 包含 Windows 2016 的评估副本。我们将在 AWS 中启动的 Windows EC2 实例将是完全许可的,因此将在 EC2 资源成本之上产生额外的费用。与往常一样,您可以在附带的存储库中找到完整的 playbooks,网址为github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter12

启动和运行

对于本节,我们将使用 Vagrant 来启动一个 Windows 2016 服务器,就像我们在第二章中所做的那样,安装和运行 Ansible。让我们首先看一下我们将使用来启动我们主机的 Vagrantfile。

Vagrantfile

这个Vagrantfile看起来与我们用来启动 Linux 主机的文件并没有太大的不同:

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION  = "2"
BOX_NAME     = "StefanScherer/windows_2016"
COMMUNICATOR = "winrm"
USERNAME     = "vagrant"
PASSWORD     = "vagrant"

Vagrant.configure(API_VERSION) do |config|
  config.vm.define "vagrant-windows-2016"
  config.vm.box = BOX_NAME
  config.vm.synced_folder ".", "/vagrant", disabled: true
  config.vm.network "forwarded_port", guest: 80, host: 8080
  config.vm.communicator = COMMUNICATOR
  config.winrm.username = USERNAME
  config.winrm.password = PASSWORD

  config.vm.provider "virtualbox" do |v|
    v.memory = "4048"
    v.cpus = "4"
    v.gui = true
  end

  config.vm.provider "vmware_fusion" do |v|
    v.vmx["memsize"] = "4048"
    v.vmx["numvcpus"] = "4"
  end

end

正如您所看到的,我们正在替换对 SSH Vagrant 的引用。我们将使用Windows 远程管理WinRM)协议以及 Ansible 与虚拟机进行交互。默认情况下,config.vm.communicator是 SSH,因此用winrm覆盖这个意味着我们必须提供config.winrm.usernameconfig.winrm.password

此外,我们指示 Vagrant 不要尝试在虚拟机上挂载我们的本地文件系统,也不要添加任何额外的 IP 地址或网络接口;相反,它应该只是将本地主机的端口转发到主机。

最后,我们将本地机器上的端口8080映射到 Windows 主机上的端口80;本章后面会详细介绍。

我们可以使用以下命令之一启动主机:

$ vagrant up

这将使用 VirtualBox,或者我们可以通过运行以下命令使用 VMWare:

$ vagrant up --provider=vmware_fusion

我们使用的 Vagrant box 大小为几个 GB,因此下载需要一些时间,但一旦下载完成,您应该会看到以下输出:

一旦机器启动,您会发现您的虚拟机已经打开了一个窗口,Windows 桌面是可访问的,如下所示:

现在暂时最小化这个窗口,因为我们将不直接与 Windows 交互。关闭窗口可能会暂停并关闭虚拟机。

现在我们的 Windows 主机已经启动运行,我们需要安装一些支持 Python 模块,以便让 Ansible 与其进行交互。

Ansible 准备

如前所述,Ansible 将使用 WinRM 与我们的 Windows 主机进行交互。

WinRM 提供对称为 WS-Management 的类似 SOAP 的协议的访问。与提供用户交互式 shell 以管理主机的 SSH 不同,WinRM 接受执行的脚本,然后将结果传递回给您。

为了能够使用 WinRM,Ansible 要求我们安装一些不同的 Python 模块,Linux 用户可以使用以下命令来安装它们:

$ sudo pip install pywinrm[credssp]

如果 macOS 用户在更新时出现关于无法更新pyOpenSSL的错误,那么可能需要执行以下命令,因为它是核心操作系统的一部分:

$ sudo pip install pywinrm[credssp] --ignore-installed pyOpenSSL

安装完成后,我们现在应该能够与我们的 Windows 主机进行交互,一旦我们配置了主机清单文件。该文件名为production,看起来像下面这样:

box1 ansible_host=localhost

[windows]
box1

[windows:vars]
ansible_connection=winrm
ansible_user=vagrant
ansible_password=vagrant
ansible_port=55985
ansible_winrm_scheme=http
ansible_winrm_server_cert_validation=ignore

正如你所看到的,我们已经删除了所有关于 SSH 的引用,并用 WinRM (ansible_connection)替换了它们。同样,我们必须提供用户名(ansible_user)和密码(ansible_password)。由于我们使用的 Vagrant box 是如何构建的,我们没有使用默认的 HTTPS 方案,而是使用了 HTTP 方案(ansible_winrm_scheme)。这意味着我们必须使用端口55985(ansible_port),而不是端口99586。这两个端口都是从我们的 Ansible 控制器映射到 Windows 主机上的端口95855986

现在我们已经让 Windows 运行起来并配置了 Ansible,我们可以开始与它进行交互了。

ping 模块

并非所有的 Ansible 模块都适用于 Windows 主机,其中 ping 就是其中之一。为 Windows 提供了一个名为win_ping的模块,我们将在这里使用它。

我们需要运行的命令如下;正如你所看到的,除了模块名称之外,它与我们针对 Linux 主机执行的方式完全相同:

$ ansible windows -i production -m win_ping

如果你是 macOS 用户,并且收到了这样的错误,那么不用担心;有一个解决方法:

这个错误是 Ansible 团队正在解决的一个已知问题。与此同时,运行以下命令,或将其添加到你的~/.bash_profile文件中:

$ export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES

一旦你运行了该命令,你应该会看到以下结果:

我们接下来要运行的下一个模块是专为 Windows 或 Linux 主机设计的。

setup 模块

正如我们在第二章中发现的,安装和运行 Ansible,setup 模块在我们的目标主机上收集事实;如果我们使用ansible命令直接调用该模块,事实将直接打印在屏幕上。要调用该模块,我们需要运行以下命令:

$ ansible windows -i production -m setup

正如你从下面的屏幕中看到的,显示的信息几乎与我们针对 Linux 主机运行模块时的情况完全相同:

我们可以使用第二章中的一个 playbook,安装和运行 Ansible,来查看这一点。在playbook01.yml中,我们使用了 Ansible 首次连接到主机时收集的事实来显示一条消息。让我们更新该 playbook 以与我们的 Windows 主机交互:

---

- hosts: windows
  gather_facts: true

  tasks:
    - debug:
        msg: "I am connecting to {{ ansible_nodename }} which is running {{ ansible_distribution }} {{ ansible_distribution_version }}"

正如你所看到的,我们已经更新了主机组,使用windows而不是boxes,并且我们还删除了becomebecome_method选项,因为我们将连接的用户有足够的权限来运行我们需要的任务。

我们可以使用以下命令运行 playbook:

$ ansible-playbook -i production playbook01.yml

下面的屏幕显示了预期的输出:

现在我们已经快速地介绍了基础知识,我们可以考虑做一些有用的事情,安装一些不同的软件包。

安装 web 服务器

当我们让我们的 Linux 主机运行起来时,我们做的第一件事之一就是安装 web 服务器,所以让我们通过在我们的 Windows 主机上安装和启用Internet Information Services (IIS)来重复这个过程。

IIS 是随 Windows Server 一起提供的默认 web 服务器,它支持以下协议:HTTP、HTTPS 和 HTTP/2,以及 FTP、FTPS、SMTP 和 NNTP。它是 22 年前作为 Windows NT 的一部分首次发布的。

就像我们迄今为止所涵盖的所有 playbook 一样,让我们通过运行以下命令创建基本的框架:

$ mkdir web web/group_vars web/roles
$ touch web/production web/site.yml web/group_vars/common.yml

现在我们可以开始编写我们的 playbook 了。

IIS 角色

我们要看的第一个角色安装和配置 IIS,然后,与我们之前的剧本一样,使用模板由 Ansible 生成并上传 HTML 文件。首先,切换到web文件夹,并通过运行以下命令创建角色:

$ cd web
$ ansible-galaxy init roles/iis

roles/iis/defaults/main.yml中的默认变量开始,我们可以看到我们的角色将与我们设置 LAMP 堆栈时创建的 Apache 角色非常相似:

---
# defaults file for web/roles/iis

document_root: 'C:\inetpub\wwwroot\'
html_file: ansible.html

html_heading: "Success !!!"
html_body: |
  This HTML page has been deployed using Ansible to a <b>{{ ansible_distribution }}</b> host.<br><br>
  The weboot is <b>{{ document_root }}</b> this file is called <b>{{ html_file }}</b>.<br>

如您所见,我们提供了文档根目录的路径,我们的 HTML 文件的名称,以及我们 HTML 文件的一些内容,模板可以在roles/iis/templates/index.html.j2中找到:

<!--{{ ansible_managed }}-->
<!doctype html>
<title>{{ html_heading }}</title>
<style>
  body { text-align: center; padding: 150px; }
  h1 { font-size: 50px; }
  body { font: 20px Helvetica, sans-serif; color: #333; }
  article { display: block; text-align: left; width: 650px; margin: 0 auto; }
</style>
<article>
    <h1>{{ html_heading }}</h1>
    <div>
        <p>{{ html_body }}</p>
    </div>
</article>

这是我们之前在 Apache 角色中使用的确切模板。部署 IIS 非常简单,我们只需要在roles/iis/tasks/main.yml中完成两个任务。我们的第一个任务可以在这里找到:

- name: enable IIS
  win_feature:
    name: 
      - "Web-Server"
      - "Web-Common-Http"
    state: "present"

这使用win_feature模块来启用和启动Web-ServerWeb-Common-Http功能。下一个和最后一个任务使用win_template模块部署我们的 HTML 页面:

- name: create an html file from a template
  win_template:
    src: "index.html.j2"
    dest: "{{ document_root }}{{ html_file }}"

如您所见,语法与标准的template模块几乎相同。现在我们的角色已经完成,我们可以运行剧本,将主机清单文件的内容复制到我们在上一节中使用的production文件中,并更新site.yml,使其包含以下内容:

---

- hosts: windows
  gather_facts: true

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/iis

然后,您可以使用以下命令运行剧本:

$ ansible-playbook -i production site.yml

剧本运行的输出应该类似于以下终端输出:

完成后,您应该能够在本地计算机上打开 Web 浏览器并转到http://localhost:8080/,这应该会显示默认的 IIS 页面:

打开http://localhost:8080/ansible.html将显示我们上传的页面:

ASP.NET 角色

现在我们已经启动了 IIS,让我们看看如何启用 ASP.NET 支持。同样,让我们首先创建角色:

$ ansible-galaxy init roles/asp

roles/asp/defaults/main.yml中的变量开始,您可以看到它们看起来与 HTML 变量类似,只是我们已经用.aspx作为前缀,这样它们就不会与iis角色的变量冲突:

aspx_document_root: 'C:\inetpub\wwwroot\ansible\'
aspx_file: default.aspx

aspx_heading: "Success !!!"
aspx_body: |
  This HTML page has been deployed using Ansible to a <b>{{ ansible_distribution }}</b> host.<br><br>
  The weboot is <b>{{ aspx_document_root }}</b> this file is called <b>{{ aspx_file }}</b>.<br><br>
  The output below is from ASP.NET<br><br>
  Hello from <%= Environment.MachineName %> at <%= DateTime.UtcNow %><br><br>

从页面底部可以看出,我们包含了一个打印机器名称的函数,这在我们的情况下应该是 Vagrant,还有日期和时间。

接下来,我们在roles/asp/templates/default.aspx.j2中有模板。除了更新的变量和文件名外,内容基本上与在iis角色中使用的内容相同:

<!--{{ ansible_managed }}-->
<!doctype html>
<title>{{ html_heading }}</title>
<style>
  body { text-align: center; padding: 150px; }
  h1 { font-size: 50px; }
  body { font: 20px Helvetica, sans-serif; color: #333; }
  article { display: block; text-align: left; width: 650px; margin: 0 auto; }
</style>
<article>
    <h1>{{ aspx_heading }}</h1>
    <div>
        <p>{{ aspx_body }}</p>
    </div>
</article>

接下来,我们有应放置在roles/asp/tasks/main.yml中的任务。首先,我们使用win_feature模块来启用所需的组件,以便让我们的基本页面运行起来:

- name: enable .net
  win_feature:
    name: 
      - "Net-Framework-Features"
      - "Web-Asp-Net45"
      - "Web-Net-Ext45"
    state: "present"
  notify: restart iis

接下来,我们需要创建一个文件夹来提供我们的页面,并复制渲染的模板:

- name: create the folder for our asp.net app
  win_file:
    path: "{{ aspx_document_root }}"
    state: "directory"

- name: create an aspx file from a template
  win_template:
    src: "default.aspx.j2"
    dest: "{{ aspx_document_root }}{{ aspx_file }}"

如您所见,我们再次使用了win_template模块。除了使用win_file模块外,文件模块的语法与我们在其他章节中使用的file模块非常接近。最后一个任务检查了 IIS 中站点的配置是否正确:

- name: ensure the default web application exists
  win_iis_webapplication:
    name: "Default"
    state: "present"
    physical_path: "{{ aspx_document_root }}"
    application_pool: "DefaultAppPool"
    site: "Default Web Site"

win_iis_webapplication模块用于配置 IIS 中的 Web 应用程序,正如其名称所示。这在我们的示例中并不是严格要求的,但它可以让你了解可能的操作。

您可能已经注意到,当我们启用了附加功能时,我们发送了一个重新启动 IIS 的通知。这意味着我们必须在roles/asp/handlers/main.yml文件中添加一个任务。此任务使用win_service模块重新启动 Web 服务器:

- name: restart iis
  win_service:
    name: w3svc
    state: restarted

现在我们已经完成了角色,我们可以再次运行剧本。首先,我们需要将新角色添加到site.yml文件中:

---

- hosts: windows
  gather_facts: true

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/iis
    - roles/asp

然后,您可以使用以下命令运行剧本:

$ ansible-playbook -i production site.yml

这应该会给你以下输出的某种形式:

打开浏览器并转到http://localhost:8080/ansible/应该会显示类似以下网页的内容:

让我们删除 Vagrant 框并查看更多模块。要删除框,请运行:

$ vagrant destroy

现在我们可以使用 Ansible 创建用户,并在 AWS 中的服务器主机上安装一些桌面应用程序。

与 AWS Windows 实例交互

当我们与本地 Windows Vagrant 框进行交互时,它并未使用安全连接;让我们看看如何在 AWS 实例中启动 Windows EC2 实例,然后像我们在第十章中与 CentOS 7 实例进行交互一样与其交互。

首先,我们需要为新的 playbook 创建文件夹结构:

$ mkdir cloud cloud/group_vars cloud/roles
$ touch cloud/production cloud/site.yml cloud/group_vars/common.yml

一旦我们有了结构,我们需要创建四个角色,首先是 AWS 角色。

AWS 角色

我们的第一个角色将创建 VPC 并启动 EC2 实例。要启动角色更改,请转到 cloud 文件夹并运行:

$ cd cloud
$ ansible-galaxy init roles/aws

让我们首先从roles/aws/defaults/main.yml的内容开始:

vpc_cidr_block: "10.0.0.0/16"
the_subnets:
  - { use: 'ec2', az: 'a', subnet: '10.0.10.0/24' }

ec2:
  instance_type: "t2.large"
  wait_port: "5986"

image:
  base: Windows_Server-2016-English-Full-Base-*
  owner: amazon
  architecture: x86_64
  root_device: ebs

win_initial_password: "{{ lookup('password', 'group_vars/generated_administrator chars=ascii_letters,digits length=30') }}"

如您所见,我们只会使用一个子网,并且在 playbook 运行期间将寻找 Windows Server 2016 AMI。最后,我们正在设置一个名为win_initial_password的变量,该变量将用于在 playbook 运行期间稍后设置我们的管理员密码。

roles/aws/tasks/main.yml中的大多数任务都如您所期望的那样。首先,我们设置 VPC,创建子网,并找出用于安全组的当前 IP 地址:

- name: ensure that the VPC is present
  ec2_vpc_net:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}"
    state: present
    cidr_block: "{{ vpc_cidr_block }}"
    resource_tags: { "Name" : "{{ environment_name }}", "Environment" : "{{ environment_name }}" }
  register: vpc_info

- name: ensure that the subnets are present
  ec2_vpc_subnet:
    region: "{{ ec2_region }}"
    state: present
    vpc_id: "{{ vpc_info.vpc.id }}"
    cidr: "{{ item.subnet }}"
    az: "{{ ec2_region }}{{ item.az }}"
    resource_tags: 
      "Name" : "{{ environment_name }}_{{ item.use }}_{{ ec2_region }}{{ item.az }}"
      "Environment" : "{{ environment_name }}"
      "Use" : "{{ item.use }}"
  with_items: "{{ the_subnets }}"

- name: gather information about the ec2 subnets
  ec2_vpc_subnet_facts:
    region: "{{ ec2_region }}"
    filters:
      "tag:Use": "ec2"
      "tag:Environment": "{{ environment_name }}"
  register: subnets_ec2

- name: register just the IDs for each of the subnets
  set_fact:
    subnet_ec2_ids: "{{ subnets_ec2.subnets | map(attribute='id') | list }}"

- name: find out your current public IP address using https://ipify.org/
  ipify_facts:
  register: public_ip

- name: set your public ip as a fact
  set_fact:
    your_public_ip: "{{ public_ip.ansible_facts.ipify_public_ip }}/32"

安全组已更新,因此我们不再打开端口 22,而是打开远程桌面(端口3389)和 WinRM(端口59855986)的端口:

- name: provision ec2 security group
  ec2_group:
    region: "{{ ec2_region }}"
    vpc_id: "{{ vpc_info.vpc.id }}"
    name: "{{ environment_name }}-ec2"
    description: "Opens the RDP and WinRM ports to a trusted IP"
    tags:
      "Name": "{{ environment_name }}-ec2"
      "Environment": "{{ environment_name }}"
    rules:
      - proto: "tcp"
        from_port: "3389"
        to_port: "3389"
        cidr_ip: "{{ your_public_ip }}"
        rule_desc: "allow {{ your_public_ip }} access to port RDP"
      - proto: "tcp"
        from_port: "5985"
        to_port: "5985"
        cidr_ip: "{{ your_public_ip }}"
        rule_desc: "allow {{ your_public_ip }} access to WinRM"
      - proto: "tcp"
        from_port: "5986"
        to_port: "5986"
        cidr_ip: "{{ your_public_ip }}"
        rule_desc: "allow {{ your_public_ip }} access to WinRM"
  register: sg_ec2

然后,我们继续通过添加互联网网关和路由来构建我们的网络,然后找到要使用的正确 AMI ID:

- name: ensure that there is an internet gateway
  ec2_vpc_igw:
    region: "{{ ec2_region }}"
    vpc_id: "{{ vpc_info.vpc.id }}"
    state: present
    tags:
      "Name": "{{ environment_name }}_internet_gateway"
      "Environment": "{{ environment_name }}"
      "Use": "gateway"
  register: igw_info

- name: check that we can route through internet gateway
  ec2_vpc_route_table:
    region: "{{ ec2_region }}"
    vpc_id: "{{ vpc_info.vpc.id }}"
    subnets: "{{ subnet_ec2_ids }}"
    routes:
      - dest: 0.0.0.0/0
        gateway_id: "{{ igw_info.gateway_id }}"
    resource_tags:
      "Name": "{{ environment_name }}_outbound"
      "Environment": "{{ environment_name }}"

- name: search for all of the AMIs in the defined region which match our selection
  ec2_ami_facts:
    region: "{{ ec2_region }}"
    owners: "{{ image.owner }}"
    filters:
      name: "{{ image.base }}"
      architecture: "{{ image.architecture }}"
      root-device-type: "{{ image.root_device }}" 
  register: amiFind

- name: filter the list of AMIs to find the latest one with an EBS backed volume
  set_fact:
    amiSortFilter: "{{ amiFind.images | sort(attribute='creation_date') | last }}"

- name: finally grab AMI ID of the most recent result which matches our base image which is backed by an EBS volume
  set_fact:
    our_ami_id: "{{ amiSortFilter.image_id }}"

现在是时候启动 EC2 实例了;您可能已经注意到,我们不需要上传密钥或任何凭据。这是因为我们实际上将注入一个 PowerShell 脚本,该脚本在实例首次启动时执行。此脚本将设置管理员密码并配置实例,以便 Ansible 可以针对其运行:

- name: launch an instance
  ec2_instance:
    region: "{{ ec2_region }}"
    state: "present"
    instance_type: "{{ ec2.instance_type }}"
    image_id: "{{ our_ami_id }}"
    wait: yes
    security_groups: [ "{{ sg_ec2.group_id }}" ]
    network: 
      assign_public_ip: true
    filters:
      instance-state-name: "running"
      "tag:Name": "{{ environment_name }}"
      "tag:environment": "{{ environment_name }}"
    vpc_subnet_id: "{{ subnet_ec2_ids[0] }}"
    user_data: "{{ lookup('template', 'userdata.j2') }}"
    tags:
      Name: "{{ environment_name }}"
      environment: "{{ environment_name }}"

脚本是一个名为userdata.j2的模板,它使用user_data键在实例启动时注入。我们将在一会儿看一下模板;在此角色中剩下的就是将实例添加到主机组,然后等待 WinRM 可访问:

- name: gather facts on the instance we just launched using the AWS API
  ec2_instance_facts:
    region: "{{ ec2_region }}"
    filters:
      instance-state-name: "running"
      "tag:Name": "{{ environment_name }}"
      "tag:environment": "{{ environment_name }}"
  register: singleinstance

- name: add our temporary instance to a host group for use in the next step
  add_host:
    name: "{{ item.public_dns_name }}"
    ansible_ssh_host: "{{ item.public_dns_name }}"
    groups: "ec2_instance"
  with_items: "{{ singleinstance.instances }}"

- name: wait until WinRM is available before moving onto the next step
  wait_for:
    host: "{{ item.public_dns_name }}"
    port: "{{ ec2.wait_port }}"
    delay: 2
    timeout: 320
    state: "started"
  with_items: "{{ singleinstance.instances }}"

roles/aws/templates/中的userdata.j2模板如下所示:

<powershell>
$admin = adsi
$admin.PSBase.Invoke("SetPassword", "{{ win_initial_password }}")
Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1'))
</powershell>

脚本的第一部分设置了管理员用户的密码(win_initial_password);然后,脚本直接从 Ansible 的 GitHub 存储库下载并执行 PowerShell 脚本。此脚本对目标实例上的当前 WinRM 配置进行检查,然后进行所需的更改,以便 Ansible 能够安全连接。脚本还配置了对实例事件日志的所有操作进行记录。

用户角色

接下来,我们有用户角色,可以运行以下命令来创建:

$ ansible-galaxy init roles/user

此角色为我们创建了一个用户,以便我们连接到我们的实例。roles/user/defaults/main.yml中可以找到的默认值如下:

ansible:
  username: "ansible"
  password: "{{ lookup('password', 'group_vars/generated_ansible chars=ascii_letters,digits length=30') }}"
  groups:
    - "Users"
    - "Administrators"

如您所见,这里我们定义了一个名为ansible的用户,该用户具有 30 个字符的随机密码。ansible用户将成为UsersAdministrators组的成员。roles/user/tasks/main.yml中有一个使用win_user模块的单个任务,看起来像:

- name: ensure that the ansible created users are present
  win_user:
    name: "{{ ansible.username }}"
    fullname: "{{ ansible.username | capitalize }}"
    password: "{{ ansible.password }}"
    state: "present"
    groups: "{{ ansible.groups }}"

与所有 Windows 模块一样,语法与 Linux 等效模块相似,因此您应该对每个键的含义有一个很好的了解。从前一个任务中可以看出,我们使用了 Jinja2 转换来大写ansible.username变量的第一个字母。

Chocolatey 角色

下一个角色使用 Chocolatey 在计算机上安装一些软件。

Chocolatey 是 Windows 的软件包管理器,原理和功能类似于我们在早期章节中使用的 Homebrew,在 macOS 上使用单个命令安装所需软件。Chocolatey 通过将大多数常见 Windows 安装程序的安装过程包装成一组常见的 PowerShell 命令,简化了命令行上的软件包安装过程,非常适合像 Ansible 这样的编排工具。

要添加角色所需的文件,请运行以下命令:

$ ansible-galaxy init roles/choc

roles/choc/defaults/main.yml中,我们有一个要安装的软件包列表:

apps:
  - "notepadplusplus.install"
  - "putty.install"
  - "googlechrome"

如您所见,我们想要安装 Notepad++、PuTTY 和 Google Chrome。需要添加到roles/choc/tasks/main.yml的任务本身如下所示:

- name: install software using chocolatey
  win_chocolatey:
    name: "{{ item }}"
    state: "present"
  with_items: "{{ apps }}"

再次强调,win_chocolatey模块在针对基于 Linux 的主机时,与我们在之前章节中使用的软件包管理器模块接受类似的输入。

信息角色

我们正在创建的最终角色称为info,它的唯一目的是输出有关我们新启动和配置的 Windows Server 2016 EC2 实例的信息。正如您可能已经猜到的那样,我们需要运行以下命令:

$ ansible-galaxy init roles/info

一旦我们有了这些文件,将以下任务添加到roles/info/tasks/main.yml中:

- name: print out information on the host
  debug:
    msg: "You can connect to '{{ inventory_hostname }}' using the username of '{{ ansible.username }}' with a password of '{{ ansible.password }}'."

如您所见,这将为我们提供要连接的主机,以及用户名和密码。

运行 playbook

在运行 playbook 之前,我们需要将以下内容添加到group_vars/common.yml中:

environment_name: "windows_example"
ec2_region: "eu-west-1"

名为production的主机清单文件应包含以下内容:

[ec2_instance]

[ec2_instance:vars]
ansible_connection=winrm
ansible_user="Administrator"
ansible_password="{{ lookup('password', 'group_vars/generated_administrator chars=ascii_letters,digits length=30') }}"
ansible_winrm_server_cert_validation=ignore

如您所见,我们使用 WinRM 连接器使用管理员用户名和在启动实例时运行用户数据脚本时设置的密码连接到我们的 Windows 实例。site.yml文件应该有以下内容:

---

- name: Create the AWS environment and launch an EC2 instance
  hosts: localhost
  connection: local
  gather_facts: True

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/aws

- name: Bootstrap the EC2 instance
  hosts: ec2_instance
  gather_facts: true

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/user
    - roles/choc
    - roles/info 

在首先导出 AWS 凭据后,我们可以使用以下命令运行 playbook:

$ export AWS_ACCESS_KEY=AKIAI5KECPOTNTTVM3EDA
$ export AWS_SECRET_KEY=Y4B7FFiSWl0Am3VIFc07lgnc/TAtK5+RpxzIGTr
$ ansible-playbook -i production site.yml

playbook 运行的略有编辑的输出如下:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'

PLAY [Create the AWS environment and launch an EC2 instance] ************************************

TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [roles/aws : ensure that the VPC is present] ***********************************************
changed: [localhost]

TASK [roles/aws : ensure that the subnets are present] ******************************************
changed: [localhost] => (item={u'subnet': u'10.0.10.0/24', u'use': u'ec2', u'az': u'a'})

TASK [roles/aws : gather information about the ec2 subnets] *************************************
ok: [localhost]

TASK [roles/aws : register just the IDs for each of the subnets] ********************************
ok: [localhost]

TASK [roles/aws : find out your current public IP address using https://ipify.org/] *************
ok: [localhost]

TASK [roles/aws : set your public ip as a fact] *************************************************
ok: [localhost]

TASK [roles/aws : provision ec2 security group] *************************************************
changed: [localhost]

TASK [roles/aws : ensure that there is an internet gateway] *************************************
changed: [localhost]

TASK [roles/aws : check that we can route through internet gateway] *****************************
changed: [localhost]

TASK [roles/aws : search for all of the AMIs in the defined region which match our selection] ***
ok: [localhost]

TASK [roles/aws : filter the list of AMIs to find the latest one with an EBS backed volume] *****
ok: [localhost]

TASK [roles/aws : finally grab AMI ID of the most recent result which matches our base image which is backed by an EBS volume] ***************************************************************
ok: [localhost]

TASK [roles/aws : launch an instance] ***********************************************************
changed: [localhost]

TASK [roles/aws : gather facts on the instance we just launched using the AWS API] **************
ok: [localhost]

TASK [roles/aws : add our temporary instance to a host group for use in the next step] **********
changed: [localhost] => 

TASK [roles/aws : wait until WinRM is available before moving onto the next step] ***************
ok: [localhost] => 

PLAY [Bootstrap the EC2 instance] ***************************************************************

TASK [Gathering Facts] **************************************************************************
ok: [ec2-34-245-2-119.eu-west-1.compute.amazonaws.com]

TASK [roles/user : ensure that the ansible created users are present] **************************
changed: [ec2-34-245-2-119.eu-west-1.compute.amazonaws.com]

TASK [roles/choc : install software using chocolatey] *******************************************
changed: [ec2-34-245-2-119.eu-west-1.compute.amazonaws.com] => (item=notepadplusplus.install)
changed: [ec2-34-245-2-119.eu-west-1.compute.amazonaws.com] => (item=putty.install)
changed: [ec2-34-245-2-119.eu-west-1.compute.amazonaws.com] => (item=googlechrome)
 [WARNING]: Chocolatey was missing from this system, so it was installed during this task run.

TASK [roles/info : print out informaton on the host] ********************************************
ok: [ec2-34-245-2-119.eu-west-1.compute.amazonaws.com] => {
 "msg": "You can connect to 'ec2-34-245-2-119.eu-west-1.compute.amazonaws.com' using the username of 'ansible' with a password of 'Qb9LVPkUeZFRx5HLFgVllFrkqK7HHN'."
}

PLAY RECAP **************************************************************************************
ec2-34-245-2-119.eu-west-1.compute.amazonaws.com : ok=4 changed=2 unreachable=0 failed=0
localhost : ok=17 changed=7 unreachable=0 failed=0

从输出中可以看出,我的 EC2 实例的主机名是ec2-34-245-2-119.eu-west-1.compute.amazonaws.comansible用户的密码是Qb9LVPkUeZFRx5HLFgVllFrkqK7HHN。我可以使用这些详细信息连接到实例,使用 Microsoft RDP(记住它被锁定到您的 IP 地址)。如下截图所示,我以 Ansible 用户身份连接,并打开了 PuTTY 和 Notepad ++;您还可以看到桌面上的 Google Chrome 的快捷方式:

您可能注意到的另一件事是,我们从未安装过 Chocolatey。正如在 playbook 运行期间所述,如果win_chocolatey在目标机器上找不到 Chocolatey 安装,它将自动安装和配置它。

在 GitHub 存储库的Chapter12/cloud文件夹中有一个 playbook,用于删除我们在此处创建的资源。要运行此 playbook,请使用以下命令:

$ ansible-playbook -i production remove.yml

确保您仔细检查了一切是否按预期被移除,以确保您不会收到任何意外的账单。

总结

正如在本章开头提到的,使用诸如 Ansible 这样的传统 Linux 工具在 Windows 上总是感觉有点奇怪。然而,我相信您会同意,体验尽可能接近 Linux。当我第一次尝试使用 Windows 模块时,我惊讶地发现我成功启动了一个 EC2 Windows Server 实例,并成功部署了一个简单的 Web 应用程序,而无需远程桌面连接到目标实例。

随着每个新版本的发布,Ansible 对基于 Windows 的主机的支持越来越多,从您的 playbook 轻松管理混合工作负载。

在下一章中,我们将回到更熟悉的领域,至少对我来说是这样,并看看我们如何加固我们的 Linux 安装。

问题

  1. 以下两个模块中哪一个可以在 Windows 和 Linux 主机上使用,setup 还是 file?

  2. 真或假:您可以使用 SSH 访问您的 Windows 目标。

  3. 解释 WinRM 使用的接口类型。

  4. 你需要安装哪个 Python 模块才能在 macOS 和 Linux 上与 WinRM 进行交互?

  5. 真或假:您可以在使用win_chocolatey模块之前有一个单独的任务来安装 Chocolatey。

  6. 更新 playbook 以安装额外的软件包。

进一步阅读

您可以在chocolatey.org/找到有关优秀的 Chocolatey 的更多信息。

第十三章:使用 Ansible 和 OpenSCAP 加固您的服务器

使用像 Ansible 这样的编排和配置工具的优势之一是,它可以用于在许多主机上生成和部署一组复杂的配置,以便重复执行。在本章中,我们将看一下一个实际上为您生成配置然后应用的工具。

在本章中,我们将学习如何使用 Ansible 和 OpenSCAP 加固基于 Red Hat 的 CentOS 7.5.1804 主机。

技术要求

我们将针对运行 CentOS Linux 发行版 7.5.1804 的 Vagrant 虚拟机进行操作;我们使用这个虚拟机是因为它配备了最新版本的 OpenSCAP。最终 playbooks 的副本可以在本书附带的存储库中找到;存储库位于github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter13/scap

OpenSCAP

我们将研究由 Red Hat 维护的一组工具之一,名为 OpenSCAP。在继续之前,我觉得我应该警告您,下一节将包含大量缩写,从 SCAP 开始。

那么,什么是 SCAP?安全内容自动化协议SCAP)是一个开放标准,包括几个组件,所有这些组件本身都是开放标准,用于构建一个框架,允许您自动评估和纠正您的主机针对国家标准与技术研究所NIST)特刊 800-53。

这本出版物是一个控制目录,适用于所有美国联邦 IT 系统,除了由国家安全局NSA)维护的系统。这些控制措施已经被制定,以帮助在美国联邦部门实施 2002 年联邦信息安全管理法FISMA)。

SCAP 由以下组件组成:

  • 资产识别AID)是用于资产识别的数据模型。

  • 资产报告格式ARF)是一个供应商中立和技术不可知的数据模型,用于在不同的报告应用程序和服务之间传输资产信息。

  • 常见配置枚举CCE)是一个标准数据库,用于常见软件的推荐配置。每个建议都有一个唯一的标识符。在撰写本文时,该数据库自 2013 年以来尚未更新。

  • 常见配置评分系统CCSS)是 CCE 的延续。它用于为各种软件和硬件配置生成得分,涵盖所有类型的部署。

  • 常见平台枚举CPE)是一种识别组织基础设施中的硬件资产、操作系统和软件的方法。一旦识别,这些数据可以用于搜索其他数据库以评估资产的威胁。

  • 常见弱点枚举CWE)是一种处理和讨论系统架构、设计和代码中可能导致漏洞的弱点原因的通用语言。

  • 常见漏洞和暴露CVE)是一个公开承认的漏洞数据库。大多数系统管理员和 IT 专业人员在某个时候都会遇到 CVE 数据库。每个漏洞都有一个唯一的 ID;例如,大多数人都会知道 CVE-2014-0160,也被称为心脏出血

  • 常见漏洞评分系统CVSS)是一种帮助捕捉漏洞特征以产生标准化数值评分的方法,然后可以用于描述漏洞的影响,例如低、中、高和关键。

  • 可扩展配置清单描述格式XCCDF)是一种描述安全清单的 XML 格式。它也可以用于配置和基准,并为 SCAP 的所有部分提供一个通用语言。

  • 开放式清单交互语言OCIL)是一个用于向最终用户提出问题以及以标准化方式处理响应程序的框架。

  • 开放式漏洞评估语言OVAL)以 XML 形式定义,旨在标准化 NIST、MITRE 公司、美国计算机紧急应对小组US-CERT)和美国国土安全部DHS)提供的所有工具和服务之间的安全内容传输。

  • 安全自动化数据信任模型TMSAD)是一个旨在定义一个通用信任模型的 XML 文档,可应用于构成 SCAP 的所有组件交换的数据。

您可以想象,SCAP 及其基础组件的开发已经耗费了数千人年。其中一些项目自 90 年代中期以来一直存在,因此它们已经得到了很好的建立,并被认为是安全最佳实践的事实标准;但是,我相信您会认为这一切听起来非常复杂——毕竟,这些是由学者、安全专业人员和政府部门定义和维护的标准。

这就是 OpenSCAP 的用武之地。由 Red Hat 维护的 OpenSCAP 项目还获得了 NIST 对其支持 SCAP 1.2 标准的认证,它允许您使用命令行客户端应用我们讨论的所有最佳实践。

与许多 Red Hat 项目一样,OpenSCAP 正在获得对 Ansible 的支持,当前版本引入了自动生成 Ansible playbook 以修复 OpenSCAP 扫描中发现的不符合规范的支持。

当前版本的 OpenSCAP 中的自动修复脚本还在不断改进中,存在已知问题,我们将在本章末解决这些问题。因此,您的输出可能与本章中介绍的内容有所不同。

在接下来的章节中,我们将启动一个 CentOS 7.5.1804 Vagrant box,对其进行扫描,并生成修复 playbook。由于 playbook 支持刚刚被引入,因此修复的覆盖率还不到 100%,因此我们将再次扫描主机,然后使用 Ansible 生成修复的 bash 脚本,并在主机上执行它,然后再次执行扫描,以便比较所有三次扫描的结果。

准备主机

在开始扫描之前,我们需要一个目标主机,因此让我们快速创建文件夹结构和Vagrantfile。要创建结构,请运行以下命令:

$ mkdir scap scap/group_vars scap/roles
$ touch scap/Vagrantfile scap/production scap/site.yml scap/group_vars/common.yml

我们创建的scap/Vagrantfile应该包含以下代码:

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
BOX_NAME = "russmckendrick/centos75"
BOX_IP = "10.20.30.40"
DOMAIN = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY = '~/.ssh/id_rsa.pub'

Vagrant.configure(API_VERSION) do |config|
  config.vm.box = BOX_NAME
  config.vm.network "private_network", ip: BOX_IP
  config.vm.host_name = BOX_IP + '.' + DOMAIN
  config.vm.synced_folder ".", "/vagrant", disabled: true
  config.ssh.insert_key = false
  config.ssh.private_key_path = [PRIVATE_KEY, "~/.vagrant.d/insecure_private_key"]
  config.vm.provision "file", source: PUBLIC_KEY, destination: "~/.ssh/authorized_keys"

  config.vm.provider "virtualbox" do |v|
    v.memory = "2024"
    v.cpus = "2"
  end

  config.vm.provider "vmware_fusion" do |v|
    v.vmx["memsize"] = "2024"
    v.vmx["numvcpus"] = "2"
  end

end

这意味着主机清单文件scap/production应包含以下内容:

box1 ansible_host=10.20.30.40.nip.io

[scap]
box1

[scap:vars]
ansible_connection=ssh
ansible_user=vagrant
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False 

我们可以使用以下命令之一启动 Vagrant box:

$ vagrant up
$ vagrant up --provider=vmware_fusion

现在我们的目标主机已准备就绪,我们可以执行初始扫描了。

playbook

我们将把 playbook 拆分成几个不同的角色。与以往的章节不同,我们将使其中一些角色可重用,并在执行它们时传递参数。我们的第一个角色是一个简单的角色,安装我们运行 OpenSCAP 扫描所需的软件包。

安装角色

如前所述,这个第一个角色是一个简单的角色,安装我们运行扫描所需的软件包:

$ ansible-galaxy init roles/install

我们需要在roles/install/defaults/main.yml中设置一些默认值;这些是:

install:
  packages:
    - "openscap-scanner"
    - "scap-security-guide"

roles/install/tasks/main.yml中有一个任务,安装软件包并执行yum更新:

- name: update all of the installed packages
  yum:
    name: "*"
    state: "latest"
    update_cache: "yes"

- name: install the packages needed
  package:
    name: "{{ item }}"
    state: latest
  with_items: "{{ install.packages }}"

这就是这个角色的全部内容;我们将在每次运行扫描时调用它,以确保我们安装了正确的软件包来运行扫描本身。

扫描角色

现在我们已经安装了 OpenSCAP 软件包,我们可以创建一个执行扫描的角色:

$ ansible-galaxy init roles/scan

如前所述,我们将在整个手册中重复使用这个角色,这给我们带来了一个很容易解决的问题。默认情况下,即使你多次定义了角色,Ansible 在手册运行期间也只会执行一次角色。为了允许角色执行多次,我们需要在roles/scan/meta/main.yml文件的顶部添加以下行:

allow_duplicates: true

这指示 Ansible 在手册运行期间多次执行这个角色。接下来,我们需要向group_vars/common.yml文件添加一些变量。这些关键值将在我们手册中使用的所有角色之间共享。

oscap:
  profile: "xccdf_org.ssgproject.content_profile_pci-dss"
  policy: "ssg-centos7-ds.xml"
  policy_path: "/usr/share/xml/scap/ssg/content/"

这些定义了我们想要使用的配置文件和我们想要应用的策略。默认情况下,OpenSCAP 不附带任何策略;这些是通过scap-security-guide软件包安装的。该软件包提供了几个策略,所有这些策略都可以在/usr/share/xml/scap/ssg/content/中找到;以下终端截图显示了该文件夹的目录列表:

对于我们的手册,我们将使用ssg-centos7-ds.xml策略,或者给它一个适当的标题,PCI-DSS v3 Control Baseline for CentOS Linux 7

支付卡行业数据安全标准PCI-DSS)是所有主要信用卡运营商都同意的一个标准,任何处理持卡人数据的人都必须遵守该标准。该标准是一组安全控制,由外部审计员或通过自我评估问卷进行审核,具体取决于您处理的交易数量。

以下一组嵌套变量定义了我们将存储扫描生成的各种文件的位置:

report:
  report_remote_path: "/tmp/{{ inventory_hostname }}_report_{{ report_name }}.html"
  report_local_path: "generated/{{ inventory_hostname }}_report_{{ report_name }}.html"
  results: "/tmp/{{ inventory_hostname }}_results_{{ report_name }}.xml" 

如你所见,我们有 HTML 报告的远程和本地路径。这是因为我们将在手册运行过程中将报告复制到我们的 Ansible 控制器。

现在我们有了共享变量,我们需要在roles/scan/defaults/main.yml文件中添加一个单个默认变量:

scan_command: >
  oscap xccdf eval --profile {{ oscap.profile }}
    --fetch-remote-resources
    --results-arf {{ report.results }}
    --report {{ report.report_remote_path }}
    {{ oscap.policy_path }}{{ oscap.policy }}

这是我们将运行以启动扫描的命令。在撰写本文时,没有任何 OpenSCAP 模块,因此我们需要使用command模块执行oscap命令。值得注意的是,我已经将命令分成多行放在变量中,以便阅读。

因为我使用了>,当应用变量到任务时,Ansible 实际上会将命令呈现为单行,这意味着我们不必像在命令行上运行多行命令时那样在每行末尾添加\

角色的最后部分是任务本身。我们将把所有任务放在roles/scan/tasks/main.yml文件中,从执行我们定义的命令的任务开始。

- name: run the openscap scan
  command: "{{ scan_command }}"
  args:
    creates: "{{ report.report_remote_path }}"
  ignore_errors: yes

ignore_errors在这里非常重要。就 Ansible 而言,除非我们从扫描中获得 100%的干净健康报告,否则这个任务将始终运行。下一个任务是将扫描生成的 HTML 报告从目标主机复制到我们的 Ansible 控制器:

- name: download the html report
  fetch:
    src: "{{ report.report_remote_path }}"
    dest: "{{ report.report_local_path }}"
    flat: yes

现在我们有了两个角色,我们可以开始运行我们的第一个扫描。

运行初始扫描

现在我们已经完成了安装和扫描角色,我们可以运行我们的第一个扫描。我们还没有涵盖的唯一文件是site.yml;这个文件看起来与我们在其他章节中使用的文件略有不同:

---

- hosts: scap
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - { role: install, tags: [ "scan" ] }
    - { role: scan, tags: [ "scan" ], report_name: "01-initial-scan" }

如你所见,我们正在为角色打标签,并在运行扫描时传递一个参数。现在,我们只是运行手册而不使用任何标签。要运行手册,请发出以下命令:

$ ansible-playbook -i production site.yml

这将给我们以下结果:

PLAY [scap] ****************************************************************************************

TASK [Gathering Facts] *****************************************************************************
ok: [box1]

TASK [install : install the packages needed] *******************************************************
changed: [box1] => (item=openscap-scanner)
changed: [box1] => (item=scap-security-guide)

TASK [scan : run the openscap scan] ****************************************************************
fatal: [box1]: FAILED! => {"changed": true, "cmd": ["oscap", "xccdf", "eval", "--profile", "xccdf_org.ssgproject.content_profile_pci-dss", "--fetch-remote-resources", "--results-arf", "/tmp/box1_results_01-initial-scan.xml", "--report", "/tmp/box1_report_01-initial-scan.html", "/usr/share/xml/scap/ssg/content/ssg-centos7-ds.xml"], "delta": "0:01:03.459407", "end": "2018-05-16 08:17:50.970321", "msg": "non-zero return code", "rc": 2, "start": "2018-05-16 08:16:47.510914", "stderr": "Downloading: https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL7.xml.bz2 ... ok", "stderr_lines": ["Downloading: https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL7.xml.bz2 ... ok"], "stdout": "Title\r\tEnsure Red Hat GPG Key Installed\nRule\r\txccdf_org.ssgproject.content_rule_ensure_redhat_gpgkey_installed\nResult\r\tpass\n\nTitle\r\tEnsure gpgcheck Enabled In Main Yum "\txccdf_org.ssgproject.content_rule_chronyd_or_ntpd_specify_multiple_servers", "Result", "\tpass"]}
...ignoring

TASK [scan : download the html report] *************************************************************
changed: [box1]

PLAY RECAP *****************************************************************************************
box1 : ok=4 changed=3 unreachable=0 failed=0

我已经在此输出中截断了扫描结果,但当你运行它时,你会看到一个大部分失败的输出被标记为红色。如前所述,这是可以预料到的,不用担心。

我们初始扫描的 HTML 报告的副本现在应该在您的 Ansible 控制器上;您可以使用以下命令在浏览器中打开它:

$ open generated/box1_report_01-initial-scan.html

或者,打开generated文件夹,双击box1_report_01-initial-scan.html

如您从示例中所见,我们的主机在 OpenSCAP 运行的 94 个检查中有 51 个失败。让我们看看如何解决这些失败的检查。

生成补救的 Ansible playbook

在我们继续之前,我必须首先提醒您报告给出了以下警告:

在没有在非运行环境中进行测试的情况下,请不要尝试实施本指南中的任何设置。本指南的创建者对其他方使用本指南不承担任何责任,并且对其质量、可靠性或任何其他特性不作任何明示或暗示的保证。

虽然我们这里只针对一个测试主机,如果您喜欢并决定查看针对其他工作负载实施 OpenSCAP,请确保您慢慢进行测试,然后再运行,即使只是由开发人员使用,我们即将进行的补救可能会对目标主机的运行产生严重后果。

既然我们已经解决了这个警告,我们可以继续看如何使用自动生成的 Ansible playbook 来保护我们的主机:

$ ansible-galaxy init roles/fix-ansible

对于这个角色,我们需要一些默认值,定义我们生成的 playbook 将被排序的位置,再次需要定义需要运行的命令。这些值可以在roles/fix-ansible/defaults/main.yml中找到。

第一个块处理我们将要生成的文件在目标主机和本地存储的位置:

playbook_file:
  remote: "/tmp/{{ inventory_hostname }}_ansible.yml"
  local: "generated/{{ inventory_hostname }}_ansible.yml"
  log: "generated/{{ inventory_hostname }}_ansible.log"

接下来,我们有需要执行的命令来生成 playbook 文件:

ansible_fix_command: >
  oscap xccdf generate fix
    --profile {{ oscap.profile }}
    --template urn:xccdf:fix:script:ansible
    --output {{ playbook_file.remote }}
    {{ report.results }}

然后,我们有一些文件夹和文件的位置需要在运行 playbook 之前放在那里;否则,将导致错误和失败:

missing_folders:
  - "/etc/dconf/db/local.d/locks/"

missing_files:
  - "/etc/dconf/db/local.d/locks/00-security-settings-lock"
  - "/etc/sysconfig/prelink"

既然我们已经有了默认的变量,我们可以开始向roles/fix-ansible/tasks/main.yml添加任务,首先使用file模块放置缺失的文件夹和文件:

- name: fix missing folders
  file:
    path: "{{ item }}"
    state: "directory"
  with_items: "{{ missing_folders }}"

- name: fix missing files
  file:
    path: "{{ item }}"
    state: "touch"
  with_items: "{{ missing_files }}"

接下来,我们将添加一个检查,看看目标机器上的 playbook 文件是否已经存在:

- name: do we already have the playbook?
  stat:
    path: "{{ playbook_file.remote }}"
  register: playbook_check

我们这样做是为了有一种跳过运行已生成的 playbook 的方法。接下来,我们运行命令来生成 playbook:

- name: generate the ansible playbook with the fixes
  command: "{{ ansible_fix_command }}"
  args:
    creates: "{{ playbook_file.remote }}" 
  ignore_errors: yes

如您从示例中所见,我们正在传递参数告诉 Ansible 创建 playbook 文件的命令;如果文件存在,则命令将不会再次执行。现在我们在机器上有了 playbook,我们需要将其复制到我们的 Ansible 控制器上。在这里,我们再次使用fetch模块:

- name: download the ansible playbook
  fetch:
    src: "{{ playbook_file.remote }}"
    dest: "{{ playbook_file.local }}"
    flat: yes
  when: playbook_check.stat.exists == False

如您所见,我们正在使用when,以便任务仅在角色运行开始时 playbook 文件不存在时才运行。现在我们在本地有了 playbook 的副本,我们可以运行它。为此,我们将使用local_action模块与command模块结合在 Ansible 中运行 Ansible:

- name: run the ansible playbook locally
  local_action:
    module: "command ansible-playbook -i production --become --become-method sudo {{ playbook_file.local }}"
  become: no
  register: playbook_run
  when: playbook_check.stat.exists == False

这里发生了一些不同的事情,所以让我们更详细地分解一下,从我们正在运行的命令开始,这个命令的翻译是:

$ ansible-playbook -i production --become --become-method sudo generated/box1_ansible.yml

如您所见,我们必须传递使用becomesudo方法作为命令的一部分的指令。这是因为生成的 Ansible playbook 没有考虑到您使用 root 以外的用户进行外部连接。

这个角色的最后一个任务将上一个任务的结果写入我们的 Ansible 控制器上的一个文件:

- name: write the results to a log file
  local_action:
    module: "copy content={{ playbook_run.stdout }} dest={{ playbook_file.log }}"
  become: no
  when: playbook_check.stat.exists == False

这样就完成了角色。我们可以再次运行 playbook 来应用修复和补救措施,然后运行另一个扫描,以便我们可以更新site.yml文件,使其读取:

---

- hosts: scap
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - { role: install, tags: [ "scan" ] }
    - { role: scan, tags: [ "scan" ], report_name: "01-initial-scan" }
    - { role: fix-ansible, report_name: "01-initial-scan" }
    - { role: scan, report_name: "02-post-ansible-fix" }

如您所见,我们已经删除了fix-ansible角色的标记,并且还更新了第二次扫描的报告名称。我们可以通过运行以下命令来启动 playbook:

$ ansible-playbook -i production site.yml

这将给我们以下输出:

PLAY [scap] *************************************************************************************

TASK [Gathering Facts] **************************************************************************
ok: [box1]

TASK [install : update all of the installed packages] *******************************************
ok: [box1]

TASK [install : install the packages needed] ****************************************************
ok: [box1] => (item=openscap-scanner)
ok: [box1] => (item=scap-security-guide)

TASK [scan : run the openscap scan] *************************************************************
ok: [box1]

TASK [scan : download the html report] **********************************************************
ok: [box1]

TASK [fix-ansible : fix missing folders] ********************************************************
changed: [box1] => (item=/etc/dconf/db/local.d/locks/)

TASK [fix-ansible : fix missing files] **********************************************************
changed: [box1] => (item=/etc/dconf/db/local.d/locks/00-security-settings-lock)
changed: [box1] => (item=/etc/sysconfig/prelink)

TASK [fix-ansible : do we already have the playbook?] *******************************************
ok: [box1]

TASK [fix-ansible : generate the ansible playbook with the fixes] *******************************
changed: [box1]

TASK [fix-ansible : download the ansible playbook] **********************************************
changed: [box1]

TASK [fix-ansible : run the ansible playbook locally] *******************************************
changed: [box1 -> localhost]

TASK [fix-ansible : write the results to a log file] ********************************************
changed: [box1 -> localhost]

TASK [scan : run the openscap scan] *************************************************************
fatal: [box1]: FAILED! => 
...ignoring

TASK [scan : download the html report] **********************************************************
changed: [box1]

PLAY RECAP **************************************************************************************
box1 : ok=14 changed=8 unreachable=0 failed=0

让我们看一下报告,看看运行 Ansible playbook 有什么不同:

$ open generated/box1_report_02-post-ansible-fix.html

输出如下所示:

现在比以前好一点了;然而,我们仍然有 25 条规则失败了—为什么呢?嗯,正如已经提到的,仍在进行将所有修复规则迁移到 Ansible 的工作;例如,如果你打开原始扫描结果并滚动到底部,你应该会看到设置 SSH 空闲超时间隔检查失败。

点击它将向您显示 OpenSCAP 正在检查的信息,为什么他们正在检查,以及为什么应该修复。最后,在底部,您将注意到有显示 shell 和 Ansible 修复解决方案的选项:

现在,点击第二份报告中剩下的失败之一。你应该注意到只有使用 shell 脚本进行修复的选项。我们将在下一个角色中生成这个,但在我们继续之前,让我们快速看一下生成的 playbook。

我在撰写时生成的 playbook 包含超过 3200 行的代码,所以我不打算在这里覆盖它们所有,但正如我们已经提到的设置 SSH 空闲超时间隔检查,让我们看一下 playbook 中应用修复的任务:

    - name: Set SSH Idle Timeout Interval
      lineinfile:
        create: yes
        dest: /etc/ssh/sshd_config
        regexp: ^ClientAliveInterval
        line: "ClientAliveInterval {{ sshd_idle_timeout_value }}"
        validate: sshd -t -f %s
      #notify: restart sshd
      tags:
        - sshd_set_idle_timeout
        - low_severity
        - restrict_strategy
        - low_complexity
        - low_disruption
        - CCE-27433-2
        - NIST-800-53-AC-2(5)
        - NIST-800-53-SA-8(i)
        - NIST-800-53-AC-12
        - NIST-800-171-3.1.11
        - PCI-DSS-Req-8.1.8
        - CJIS-5.5.6
        - DISA-STIG-RHEL-07-040320

如您所见,它使用 lineinfile 模块来应用在 playbook 顶部定义的变量。此外,每个任务都带有关于修复所属的标准的许多信息,以及严重程度。这意味着我们可以对 playbook 运行的部分进行非常细致的控制;例如,您可以通过使用以下命令仅运行低干扰更改:

$ ansible-playbook -i production --become --become-method sudo --tags "low_disruption" generated/box1_ansible.yml

最后,在box1_ansible.log文件的底部,我们可以看到 playbook 运行做出了以下更改:

PLAY RECAP **************************************************************************************
box1 : ok=151 changed=85 unreachable=0 failed=0 

生成修复的 bash 脚本

为了纠正剩下的问题,我们应该生成并执行 bash 脚本:

$ ansible-galaxy init roles/fix-bash

由于这是一个很好的功能,我不打算详细介绍我们在这里添加的内容的各个方面。roles/fix-bash/defaults/main.yml的内容与fix-ansible角色中的内容类似:

bash_file:
  remote: "/tmp/{{ inventory_hostname }}_bash.sh"
  log: "generated/{{ inventory_hostname }}_bash.log"

bash_fix_command: >
  oscap xccdf generate fix
    --profile {{ oscap.profile }}
    --output {{ bash_file.remote }}
    {{ report.results }}

roles/fix-bash/tasks/main.yml中的任务也是类似的,不需要任何解释:

- name: do we already have the bash script?
  stat:
    path: "{{ bash_file.remote }}"
  register: bash_script_check

- name: generate the bash script
  command: "{{ bash_fix_command }}"
  args:
    creates: "{{ bash_file.remote }}" 
  ignore_errors: yes

- name: run the bash script
  command: "bash {{ bash_file.remote }}"
  ignore_errors: yes
  register: bash_run
  when: bash_script_check.stat.exists == False

- name: write the results to a log file
  local_action:
    module: "copy content={{ bash_run.stdout }} dest={{ bash_file.log }}"
  become: no
  when: bash_script_check.stat.exists == False

更新site.yml文件,使其读取:

- hosts: scap
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - { role: install, tags: [ "scan" ] }
    - { role: scan, tags: [ "scan" ], report_name: "01-initial-scan" }
    - { role: fix-ansible, report_name: "01-initial-scan" }
    - { role: scan, report_name: "02-post-ansible-fix" }
    - { role: fix-bash, report_name: "02-post-ansible-fix" }
    - { role: scan, report_name: "03-post-bash-fix" }

这意味着我们可以拿到在应用 Ansible 修复后运行的扫描结果,生成包含剩余修复的 bash 脚本;然后我们进行最后一次扫描。要应用最终的一批修复,运行以下命令:

$ ansible-playbook -i production site.yml

这会产生以下输出:

PLAY [scap] *************************************************************************************

TASK [Gathering Facts] **************************************************************************
ok: [box1]

TASK [install : update all of the installed packages] *******************************************
ok: [box1]

TASK [install : install the packages needed] ****************************************************
ok: [box1] => (item=openscap-scanner)
ok: [box1] => (item=scap-security-guide)

TASK [scan : run the openscap scan] *************************************************************
ok: [box1]

TASK [scan : download the html report] **********************************************************
ok: [box1]

TASK [fix-ansible : fix missing folders] ********************************************************
ok: [box1] => (item=/etc/dconf/db/local.d/locks/)

TASK [fix-ansible : fix missing files] **********************************************************
changed: [box1] => (item=/etc/dconf/db/local.d/locks/00-security-settings-lock)
changed: [box1] => (item=/etc/sysconfig/prelink)

TASK [fix-ansible : do we already have the playbook?] *******************************************
ok: [box1]

TASK [fix-ansible : generate the ansible playbook with the fixes] *******************************
skipping: [box1]

TASK [fix-ansible : download the ansible playbook] **********************************************
skipping: [box1]

TASK [fix-ansible : run the ansible playbook locally] *******************************************
skipping: [box1]

TASK [fix-ansible : write the results to a log file] ********************************************
skipping: [box1]

TASK [scan : run the openscap scan] *************************************************************
ok: [box1]

TASK [scan : download the html report] **********************************************************
ok: [box1]

TASK [fix-bash : do we already have the bash script?] *******************************************
ok: [box1]

TASK [fix-bash : generate the bash script] ******************************************************
changed: [box1]

TASK [fix-bash : run the bash script] ***********************************************************
changed: [box1]

TASK [fix-bash : write the results to a log file] ***********************************************
changed: [box1 -> localhost]

TASK [scan : run the openscap scan] *************************************************************
fatal: [box1]: FAILED! =>
...ignoring

TASK [scan : download the html report] **********************************************************
changed: [box1]

PLAY RECAP **************************************************************************************
box1 : ok=16 changed=6 unreachable=0 failed=0

通过运行检查最终报告:

$ open generated/box1_report_03-post-bash-fix.html

这应该显示总共失败检查的数量已经减少到只有五个:

运行独立扫描

当我们创建扫描角色时,提到角色应该是可重用的。当我们在site.yml文件中定义角色时,我们还为角色添加了标记。让我们快速看一下我们如何可以仅运行扫描而不是完整的 playbook 运行。要启动扫描,请运行以下命令:

$ ansible-playbook -i production --tags "scan" --extra-vars "report_name=scan-only" site.yml

这将只运行标记为scan的 playbook 部分,并且我们还覆盖了report_name变量,这是我们在site.yml文件中调用角色时设置的,以调用我们的report box1_report_scan-only.html

修复剩下的失败检查

到目前为止,我们还没有不得不采取任何硬编码的修复措施来解决扫描中发现的任何问题。我们不得不创建一些文件和文件夹来允许应用修复,但这更多是为了让自动修复工作,而不是修复。

在撰写本文时,我们已知有两个当前显示在我的扫描中的五个问题存在问题;它们是:

  • xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands

  • xccdf_org.ssgproject.content_rule_audit_rules_login_events

正在进行修复。你可以在 Red Hat 的 Bugzilla 上找到它们:

因此,将这两个放在一边,现在有三个我可以修复。为了做到这一点,我将创建一个单独的角色和 playbook,因为在你阅读这篇文章的时候,以下的修复可能已经不再需要:

$ ansible-galaxy init roles/final-fixes

直接跳转到roles/final-fixes/tasks/main.yml,我们的第一个修复是将日志每天而不是每周进行轮转,这是默认设置。为了做到这一点,我们将使用lineinfile模块将weekly替换为daily

- name: sort out the logrotate
  lineinfile:
    path: "/etc/logrotate.conf"
    regexp: "^weekly"
    line: "daily"

下一个任务添加了一个修复,应该在某个时候通过scap-security-guide软件包实现:

- name: add the missing line to the modules.rules
  lineinfile:
    path: "/etc/audit/rules.d/modules.rules"
    line: "-a always,exit -F arch=b32 -S init_module -S delete_module -k modules"

正如你在这里所看到的,我们再次使用lineinfile模块。这一次,如果/etc/audit/rules.d/modules.rules中不存在,我们将添加一行。这将添加一个规则,考虑到 32 位内核以及已经配置好的 64 位内核的修复脚本。

接下来,我们为应该在 bash 脚本执行期间执行的脚本添加了一个修复。首先,我们需要使用file模块创建一个文件:

- name: add file for content_rule_file_permissions_var_log_audit
  file:
    path: "/var/log/audit/audit.log.fix"
    state: "touch"

然后我们需要复制并执行在我们第一次运行时失败的 bash 脚本的部分:

- name: copy the content_rule_file_permissions_var_log_audit.sh script
  copy:
    src: "content_rule_file_permissions_var_log_audit.sh"
    dest: "/tmp/content_rule_file_permissions_var_log_audit.sh"

- name: run the content_rule_file_permissions_var_log_audit.sh script 
  command: "bash /tmp/content_rule_file_permissions_var_log_audit.sh"

bash 脚本本身可以在roles/final-fixes/files/content_rule_file_permissions_var_log_audit.sh中找到,它看起来是这样的:

if `grep -q ^log_group /etc/audit/auditd.conf` ; then
  GROUP=$(awk -F "=" '/log_group/ {print $2}' /etc/audit/auditd.conf | tr -d ' ')
  if ! [ "${GROUP}" == 'root' ] ; then
    chmod 0640 /var/log/audit/audit.log
    chmod 0440 /var/log/audit/audit.log.*
  else
    chmod 0600 /var/log/audit/audit.log
    chmod 0400 /var/log/audit/audit.log.*
  fi

  chmod 0640 /etc/audit/audit*
  chmod 0640 /etc/audit/rules.d/*
else
  chmod 0600 /var/log/audit/audit.log
  chmod 0400 /var/log/audit/audit.log.*
  chmod 0640 /etc/audit/audit*
  chmod 0640 /etc/audit/rules.d/*
fi

最后,我们需要创建一个名为final-fixes.yml的 playbook 文件。这应该运行我们刚刚创建的角色,然后运行最终扫描:

---

- hosts: scap
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - { role: final-fixes }
    - { role: scan, report_name: "04-final-fixes" }

要运行 playbook,请使用以下命令:

$ ansible-playbook -i production final-fixes.yml

这将产生以下结果:

PLAY [scap] *************************************************************************************

TASK [Gathering Facts] **************************************************************************
ok: [box1]

TASK [final-fixes : sort out the logrotate] *****************************************************
changed: [box1]

TASK [final-fixes : add the missing line to the modules.rules] **********************************
changed: [box1]

TASK [final-fixes : add file for content_rule_file_permissions_var_log_audit] *******************
changed: [box1]

TASK [final-fixes : copy the content_rule_file_permissions_var_log_audit.sh script] *************
changed: [box1]

TASK [final-fixes : run the content_rule_file_permissions_var_log_audit.sh script] **************
changed: [box1]

TASK [scan : run the openscap scan] *************************************************************
fatal: [box1]: FAILED! => 
...ignoring

TASK [scan : download the html report] **********************************************************
changed: [box1]

PLAY RECAP **************************************************************************************
box1 : ok=8 changed=7 unreachable=0 failed=0

打开使用以下命令生成的报告:

$ open generated/box1_report_04-final-fixes.html

这告诉我们,仍然有两个中等检查存在已知问题,仍然失败:

希望在你阅读本文时,你的主机将得到一个干净的健康证明,这最后一部分将不再需要,这就是为什么我将它从主site.yml playbook 中分离出来的原因。

销毁 Vagrant box

完成后不要忘记销毁 Vagrant box;你不希望在主机上运行一个空闲的虚拟机。要做到这一点,请运行:

$ vagrant destroy

一旦 box 消失,我建议在干净的安装上多次运行扫描和修复,以了解如何在新主机上实现这一点。

总结

在本章中,我们创建了一个 playbook,生成了一个 playbook,用于在扫描期间发现 PCI-DSS 不符合错误的修复。除了非常酷外,如果你想象一下你正在运行几十台需要符合标准的服务器,并且它们都需要完整的审计历史记录,这也是非常实用的。

现在你已经有了一个 playbook 的基础,可以每天用来定位这些主机,对它们进行审计,并将结果存储在主机之外,但是根据你的配置,你也有一种自动解决扫描期间发现的任何不符合标准的方法。

我们在本章中进行的扫描都是基于主机的;在下一章中,我们将看看如何远程扫描主机。

问题

  1. >添加到多行变量会产生什么影响?

  2. 正确或错误:OpenSCAP 已经获得 NIST 认证。

  3. 为什么我们告诉 Ansible 在scan命令标记为失败时继续运行?

  4. 解释为什么我们对某些角色使用标签。

  5. 正确或错误:我们使用copy命令将 HTML 报告从远程主机复制到 Ansible 控制器。

进一步阅读

您可以在以下链接找到本章涵盖的技术和组织的更多信息:

第十四章:部署 WPScan 和 OWASP ZAP

在本章中,我们将介绍创建一个 playbook,部署和运行两个安全工具 WPScan 和 OWASP ZAP。然后,使用之前章节的 playbooks,我们将启动一个 WordPress 安装供我们扫描。

与其他章节一样,我们将使用 Vagrant 和我们已经下载的框之一。您可以在github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter14找到完整的 playbook 副本。

准备框

在本章中,我们将启动两个 Vagrant 框,第一个将用于安装扫描工具。此主机将安装了 Docker,并且我们将使用 Docker Ansible 模块与该软件进行交互。第二个框将包含或托管 WordPress 安装,扫描工具将针对其进行扫描。

创建一个包含以下内容的Vagrantfile

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
BOX_NAME = "centos/7"
BOX_IP_SCAN = "10.20.30.40"
BOX_IP_WP = "10.20.30.41"
DOMAIN = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY = '~/.ssh/id_rsa.pub'

Vagrant.configure(API_VERSION) do |config|

  config.vm.define :scan do |scan| 
    scan.vm.box = BOX_NAME
    scan.vm.network "private_network", ip: BOX_IP_SCAN
    scan.vm.host_name = BOX_IP_SCAN + '.' + DOMAIN
    scan.ssh.insert_key = false
    scan.ssh.private_key_path = [PRIVATE_KEY, "~/.vagrant.d/insecure_private_key"]
    scan.vm.provision "file", source: PUBLIC_KEY, destination: "~/.ssh/authorized_keys"
  end

  config.vm.define :wp do |wp| 
    wp.vm.box = BOX_NAME
    wp.vm.network "private_network", ip: BOX_IP_WP
    wp.vm.host_name = BOX_IP_WP + '.' + DOMAIN
    wp.ssh.insert_key = false
    wp.ssh.private_key_path = [PRIVATE_KEY, "~/.vagrant.d/insecure_private_key"]
    wp.vm.provision "file", source: PUBLIC_KEY, destination: "~/.ssh/authorized_keys"
  end

  config.vm.provider "virtualbox" do |v|
    v.memory = "2024"
    v.cpus = "2"
  end

  config.vm.provider "vmware_fusion" do |v|
    v.vmx["memsize"] = "2024"
    v.vmx["numvcpus"] = "2"
  end

end

如您所见,我们将启动两个 CentOS 7 框,一个标记为scan,其主机名为10.20.30.40.nip.io,另一个为wp,其主机名为10.20.30.41.nip.io

主机清单文件,通常称为 production,包含以下内容:

box1 ansible_host=10.20.30.40.nip.io
box2 ansible_host=10.20.30.41.nip.io

[scan]
box1

[wordpress]
box2

[boxes]
box1
box2

[boxes:vars]
ansible_connection=ssh
ansible_user=vagrant
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

如您所见,我们定义了三个主机组;第一组名为scan,包括我们将用于运行扫描工具的单个主机。第二组wordpress,虽然只包含一个主机,但可以列出多个主机,并且扫描应该针对它们所有。第三组名为boxes,已被定义为将连接配置应用于我们在 playbook 中添加的所有主机的一种方式。

您可以使用以下两个命令之一启动这两个框:

$ vagrant up
$ vagrant up --provider=vmware_fusion

现在我们的 Vagrant 框已经启动并运行,我们可以看一下我们的 playbook 是什么样子的。

WordPress playbook

正如您已经猜到的那样,这将非常简单,因为我们已经编写了一个在 CentOS 7 主机上部署 WordPress 的 playbook。实际上,我们唯一需要做的就是从存储库的Chapter05/lemp文件夹中复制group_varsroles文件夹及其内容,以及site.yml文件,然后我们就完成了。

这是使用 Ansible 这样的工具的一个巨大优势:写一次,多次使用;我们唯一要做的更改是在添加部署软件的 plays 时更改site.yml文件。

扫描 playbook

如前所述,我们将使用 Docker 来运行 WPScan 和 OWASP ZAP。这样做的原因是,如果我们直接在主机上安装这两个软件包,我们将最终部署相当多的支持软件。虽然这不是问题,但使用诸如 Docker 这样的工具可以简化安装过程,并给我们一个借口来介绍 Docker Ansible 模块。

Docker 角色

与我们迄今为止创建的所有角色一样,我们将使用ansible-galaxy命令来生成我们角色的结构:

$ ansible-galaxy init roles/docker

对于我们的 Docker 安装,我们将使用 Docker 自己提供的yum存储库;这意味着在安装 Docker 之前,需要启用存储库。一旦启用,我们将能够安装最新的稳定版本。让我们首先在roles/docker/defaults/main.yml中填充一些默认值:

docker:
  gpg_key: "https://download.docker.com/linux/centos/gpg"
  repo_url: "https://download.docker.com/linux/centos/docker-ce.repo"
  repo_path: "/etc/yum.repos.d/docker-ce.repo"
  packages:
    - "docker-ce"
    - "device-mapper-persistent-data"
    - "lvm2"
    - "python-setuptools"
    - "libselinux-python"
  pip:
    - "docker"

正如你所看到的,我们正在定义存储库的 GPG 密钥的 URL,存储库文件的 URL,以及存储库文件应该被复制到主机的位置。我们还列出了需要安装的软件包列表,以使 Docker 正常运行。最后,我们有用于 Docker 的 Python 软件包,这将允许 Ansible 与我们的 Vagrant box 上的 Docker API 进行交互。

在使用任何已定义的变量之前,我们需要确保我们运行的主机的软件包是最新的,因此roles/docker/tasks/main.yml中的第一个任务应该执行yum update

- name: update all of the installed packages
  yum:
    name: "*"
    state: "latest"
    update_cache: "yes"

现在我们的主机已经更新,我们可以添加 GPG 密钥;对此,我们将使用rpm_key模块,我们只需提供要安装的密钥的 URL 或文件路径:

- name: add the gpg key for the docker repo
  rpm_key:
    key: "{{ docker.gpg_key }}"
    state: "present"

现在我们已经安装了 GPG 密钥,我们可以从 Docker 下载docker-ce.repo文件,并将其存储在yum在下次执行时会使用的位置:

- name: add docker repo from the remote url
  get_url:
    url: "{{ docker.repo_url }}"
    dest: "{{ docker.repo_path }}"
    mode: "0644"

如您所见,我们使用get_url模块下载文件并将其放置在主机机器上的/etc/yum.repos.d/中;我们还设置了文件的读、写和执行权限为0644

现在我们已经配置了 Docker 存储库,我们可以通过添加以下任务来安装我们定义的软件包:

- name: install the docker packages
  yum:
    name: "{{ item }}"
    state: "installed"
    update_cache: "yes"
  with_items: "{{ docker.packages }}"

我们添加了update_cache选项,因为我们刚刚添加了一个新的存储库,并希望确保它被识别。接下来,我们必须使用pip安装 Docker Python 包;默认情况下,pip未安装,因此我们需要确保它首先可用,方法是使用easy_install,而easy_install又是由先前的任务安装的python-setuptools软件包安装的。有一个easy_install模块,因此这个任务很简单:

- name: install pip
  easy_install:
    name: pip
    state: latest

现在 pip 可用,我们可以使用pip模块来安装 Docker Python 库:

- name: install the python packages
  pip:
    name: "{{ item }}"
  with_items: "{{ docker.pip }}"

倒数第二个任务是在 Vagrant 虚拟机上禁用 SELinux:

- name: put selinux into permissive mode
  selinux:
    policy: targeted
    state: permissive

默认情况下,由 Docker 提供的 Docker 版本不会自动在 CentOS/Red Hat 服务器上启动,因此这个角色的最后一个任务是启动 Docker 服务,并确保它配置为在启动时启动:

- name: start docker and configure to start on boot
  service:
    name: "docker"
    state: "started"
    enabled: "yes"

我们在 playbook 运行的这一部分完成之前完成了这个步骤,而不是使用处理程序,因为 playbook 需要在完成之前与 Docker 交互。由于处理程序只在 playbook 运行结束时调用,这意味着我们的 playbook 的下一部分将失败。在开始下载和运行容器之前,让我们快速运行 playbook。

测试 playbook

由于我们已经有了所有基本角色,我们可以尝试运行 playbook;在这样做之前,我们需要更新site.yml以包括我们扫描主机的操作:

---

- hosts: scan
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/docker

- hosts: wordpress
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/stack-install
    - roles/stack-config
    - roles/wordpress

更新后,我们可以使用以下代码运行我们的 playbook:

$ ansible-playbook -i production site.yml

这应该给我们类似以下的输出:

PLAY [scan] *************************************************************************************

TASK [Gathering Facts] **************************************************************************
ok: [box1]

TASK [roles/docker : update all of the installed packages] **************************************
changed: [box1]

TASK [roles/docker : add the gpg key for the docker repo] ***************************************
changed: [box1]

TASK [roles/docker : add docker repo from the remote url] ***************************************
changed: [box1]

TASK [roles/docker : install the docker packages] ***********************************************
changed: [box1] => (item=[u'docker-ce', u'device-mapper-persistent-data', u'lvm2', u'python-setuptools', u'libselinux-python'])

TASK [roles/docker : install pip] ***************************************************************
changed: [box1]

TASK [roles/docker : install the python packages] ***********************************************
changed: [box1] => (item=docker)

TASK [roles/docker : put selinux into permissive mode] ******************************************
changed: [box1]

TASK [roles/docker : start docker and configure to start on boot] *******************************
changed: [box1]

PLAY [wordpress] ********************************************************************************

TASK [Gathering Facts] **************************************************************************
ok: [box2]

TASK [roles/stack-install : install the repo packages] ******************************************
changed: [box2] => (item=[u'epel-release', u'https://centos7.iuscommunity.org/ius-release.rpm'])

TASK [roles/stack-install : add the NGINX mainline repo] ****************************************
changed: [box2]

TASK [roles/stack-install : update all of the installed packages] *******************************
changed: [box2]

TASK [roles/stack-install : remove the packages so that they can be replaced] *******************
changed: [box2] => (item=[u'mariadb-libs.x86_64'])

TASK [roles/stack-install : install the stack packages] *****************************************
changed: [box2] => (item=[u'postfix', u'MySQL-python', u'policycoreutils-python', u'nginx', u'mariadb101u', u'mariadb101u-server', u'mariadb101u-config', u'mariadb101u-common', u'mariadb101u-libs', u'php72u', u'php72u-bcmath', u'php72u-cli', u'php72u-common', u'php72u-dba', u'php72u-fpm', u'php72u-fpm-nginx', u'php72u-gd', u'php72u-intl', u'php72u-json', u'php72u-mbstring', u'php72u-mysqlnd', u'php72u-process', u'php72u-snmp', u'php72u-soap', u'php72u-xml', u'php72u-xmlrpc', u'vim-enhanced', u'git', u'unzip'])

TASK [roles/stack-config : add the wordpress user] **********************************************
changed: [box2]

TASK [roles/stack-config : copy the nginx.conf to /etc/nginx/] **********************************
changed: [box2]

TASK [roles/stack-config : create the global directory in /etc/nginx/] **************************
changed: [box2]

TASK [roles/stack-config : copy the restrictions.conf to /etc/nginx/global/] ********************
changed: [box2]

TASK [roles/stack-config : copy the wordpress_shared.conf to /etc/nginx/global/] ****************
changed: [box2]

TASK [roles/stack-config : copy the default.conf to /etc/nginx/conf.d/] *************************
changed: [box2]

TASK [roles/stack-config : copy the www.conf to /etc/php-fpm.d/] ********************************
changed: [box2]

TASK [roles/stack-config : configure php.ini] ***************************************************
changed: [box2] => (item={u'regexp': u'^;date.timezone =', u'replace': u'date.timezone = Europe/London'})
changed: [box2] => (item={u'regexp': u'^expose_php = On', u'replace': u'expose_php = Off'})
changed: [box2] => (item={u'regexp': u'^upload_max_filesize = 2M', u'replace': u'upload_max_filesize = 20M'})

TASK [roles/stack-config : start php-fpm] *******************************************************
changed: [box2]

TASK [roles/stack-config : start nginx] *********************************************************
changed: [box2]

TASK [roles/stack-config : configure the mariadb bind address] **********************************
changed: [box2]

TASK [roles/stack-config : start mariadb] *******************************************************
changed: [box2]

TASK [roles/stack-config : change mysql root password] ******************************************
changed: [box2] => (item=127.0.0.1)
changed: [box2] => (item=::1)
changed: [box2] => (item=10.20.30.41.nip.io)
changed: [box2] => (item=localhost)

TASK [roles/stack-config : set up .my.cnf file] *************************************************
changed: [box2]

TASK [roles/stack-config : delete anonymous MySQL user] *****************************************
ok: [box2] => (item=127.0.0.1)
ok: [box2] => (item=::1)
changed: [box2] => (item=10.20.30.41.nip.io)
changed: [box2] => (item=localhost)

TASK [roles/stack-config : remove the MySQL test database] **************************************
changed: [box2]

TASK [roles/stack-config : set the selinux allowing httpd_t to be permissive is required] *******
changed: [box2]

TASK [roles/wordpress : download wp-cli] ********************************************************
changed: [box2]

TASK [roles/wordpress : update permissions of wp-cli to allow anyone to execute it] *************
changed: [box2]

TASK [roles/wordpress : create the wordpress database] ******************************************
changed: [box2]

TASK [roles/wordpress : create the user for the wordpress database] *****************************
changed: [box2] => (item=127.0.0.1)
ok: [box2] => (item=::1)
ok: [box2] => (item=10.20.30.41.nip.io)
ok: [box2] => (item=localhost)

TASK [roles/wordpress : are the wordpress files already there?] *********************************
ok: [box2]

TASK [roles/wordpress : download wordpresss] ****************************************************
changed: [box2]

TASK [roles/wordpress : set the correct permissions on the homedir] *****************************
changed: [box2]

TASK [roles/wordpress : is wordpress already configured?] ***************************************
ok: [box2]

TASK [roles/wordpress : configure wordpress] ****************************************************
changed: [box2]

TASK [roles/wordpress : do we need to install wordpress?] ***************************************
fatal: [box2]: FAILED! =>
...ignoring

TASK [roles/wordpress : install wordpress if needed] ********************************************
changed: [box2]

TASK [roles/wordpress : do we need to install the plugins?] *************************************
failed: [box2] (item=jetpack) =>
failed: [box2] (item=wp-super-cache) =>
failed: [box2] (item=wordpress-seo) =>
failed: [box2] (item=wordfence) =>
failed: [box2] (item=nginx-helper) =>
...ignoring

TASK [roles/wordpress : set a fact if we don't need to install the plugins] *********************
skipping: [box2]

TASK [roles/wordpress : set a fact if we need to install the plugins] ***************************
ok: [box2]

TASK [roles/wordpress : install the plugins if we need to or ignore if not] *********************
changed: [box2] => (item=jetpack)
changed: [box2] => (item=wp-super-cache)
changed: [box2] => (item=wordpress-seo)
changed: [box2] => (item=wordfence)
changed: [box2] => (item=nginx-helper)

TASK [roles/wordpress : do we need to install the theme?] ***************************************
fatal: [box2]: FAILED! =>
...ignoring

TASK [roles/wordpress : set a fact if we don't need to install the theme] ***********************
skipping: [box2]

TASK [roles/wordpress : set a fact if we need to install the theme] *****************************
ok: [box2]

TASK [roles/wordpress : install the theme if we need to or ignore if not] ***********************
changed: [box2]

RUNNING HANDLER [roles/stack-config : restart nginx] ********************************************
changed: [box2]

RUNNING HANDLER [roles/stack-config : restart php-fpm] ******************************************
changed: [box2]

PLAY RECAP **************************************************************************************
box1 : ok=9 changed=8 unreachable=0 failed=0
box2 : ok=42 changed=37 unreachable=0 failed=0

如您所见,这已经执行了完整的 Docker 和 WordPress 安装;打开http://10.20.30.41.nip.io/将带您进入 WordPress 站点:

现在我们的 WordPress 站点已经运行起来了,我们可以开始执行扫描站点的角色。

WPScan 角色

我们要创建的第一个角色是运行 WPScan 的角色。WPScan 是一个执行 WordPress 站点扫描的工具;它尝试确定正在运行的 WordPress 版本,并检查是否有已知漏洞的插件。它还可以尝试暴力破解管理员用户帐户;但是,我们将跳过这一步。

与往常一样,我们可以使用以下命令引导角色:

$ ansible-galaxy init roles/wpscan

文件放置好后,我们需要将以下内容添加到roles/wpscan/defaults/main.yml中:

image: "wpscanteam/wpscan"
log:
  remote_folder: /tmp/wpscan/
  local_folder: "generated/"
  file: "{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}-{{ ansible_date_time.minute }}.txt"

这设置了我们想要从 Docker Hub 下载的镜像;在这种情况下,它是来自 WPScan 团队的官方 WPScan 镜像。然后,我们设置了我们希望用于日志的变量;您可能注意到我们正在为日志定义一个文件夹和文件名。

接下来,我们需要将任务添加到roles/wpscan/tasks/main.yml中,其中第一个任务使用docker_image模块来拉取wpscanteam/wpscan镜像的副本:

- name: pull the image
  docker_image:
    name: "{{ image }}"

接下来,我们需要创建一个文件夹,用于将日志写入我们的 Vagrant 虚拟机:

- name: create the folder which we will mount inside the container
  file:
    path: "{{ log.remote_folder }}"
    state: "directory"
    mode: "0777"

我们这样做的原因是,我们将在下一个任务中启动的容器内挂载此文件夹。由于日志是我们希望保留的每次扫描中的唯一数据,因此我们将它们写入挂载的文件夹,这意味着一旦容器退出并删除,我们就可以将日志复制到我们的 Ansible 控制器上。

在我们看下一个任务之前,让我们快速看一下如果我们直接在命令行上使用 Docker 来启动扫描,我们需要运行的命令:

$ docker container run -it --rm --name wpscan -v /tmp/wpscan/:/tmp/wpscan/ wpscanteam/wpscan \
 -u http://10.20.30.41.nip.io/ --update --enumerate --log /tmp/wpscan/10.20.30.41.nip.io-2018-05-19-12-16.txt

命令的第一行是 Docker 逻辑发生的地方;我们要求 Docker 做的是在前台(-it)启动(run)一个名为 wpscan 的容器(--name),将主机上的/tmp/wpscan/挂载到容器内的/tmp/wpscan/(-v),使用指定的镜像(wpscanteam/wpscan)。一旦进程退出,我们就移除容器(--rm)。

第二行的所有内容都传递给容器的默认入口点,在wpscanteam/wpscan镜像的情况下,入口点是/wpscan/wpscan.rb,这意味着我们在容器内运行扫描的命令实际上是这样的:

$ /wpscan/wpscan.rb -u http://10.20.30.41.nip.io/ --update --enumerate --log /tmp/wpscan/10.20.30.41.nip.io-2018-05-19-12-16.txt

现在我们知道了使用 Docker 运行命令的想法,我们可以看看在我们的任务中它会是什么样子:

- name: run the scan
  docker_container:
    detach: false
    auto_remove: true
    name: "wpscan"
    volumes: "{{ log.remote_folder }}:{{ log.remote_folder }}"
    image: "{{ image }}"
    command: "--url http://{{ hostvars[item]['ansible_host'] }} --update --enumerate --log {{ log.remote_folder }}{{ hostvars[item]['ansible_host'] }}-{{ log.file }}"
  register: docker_scan
  failed_when: docker_scan.rc == 0 or docker_scan.rc >= 2
  with_items: "{{ groups['wordpress'] }}"

任务中的选项的顺序与 Docker 命令中编写的顺序相同:

  • detach: false类似于传递-it,它将在前台运行容器;默认情况下,docker_container模块在后台运行容器。这引入了一些挑战,我们很快会讨论。

  • auto_remove: true--rm相同。

  • name: "wpscan"与运行--name wpscan完全相同。

  • volumes:"{{ log.remote_folder }}:{{ log.remote_folder }}"与在 Docker 中使用-v标志传递的内容相同。

  • image: "{{ image }}"相当于只传递镜像名称,例如wpscanteam/wpscan

  • 最后,command包含了我们想要传递给入口点的所有内容;正如你所看到的,我们在这里传递了一些动态变量。

如前所述,默认情况下,docker_container模块在后台运行容器;在大多数情况下,这通常是很好的;然而,由于我们只是将容器作为一次性任务来执行我们的扫描,所以我们需要在前台运行它。

这样做实际上会导致错误,因为我们指示 Ansible 保持连接到一个容器,然后一旦扫描过程完成,容器就会终止并被移除。为了解决这个问题,我们正在注册任务的结果,而不是使用ignore_errors,我们告诉任务在返回代码(rc)等于0或等于或大于2时失败(failed_when),因为我们的任务应该始终有一个返回代码为1

那么为什么不让容器在后台运行呢?因为下一个任务会将日志文件从 Vagrant 框复制到 Ansible 控制器,如果我们让容器在后台运行,Ansible 将立即转移到下一个任务并复制一个部分写入的文件。

连接到容器并等待其退出意味着我们正在等待扫描完成,然后再进行下一个任务,看起来像这样:

- name: download the html report
  fetch:
    src: "{{ log.remote_folder }}{{ hostvars[item]['ansible_host'] }}-{{ log.file }}"
    dest: "{{ log.local_folder }}{{ hostvars[item]['ansible_host'] }}-{{ log.file }}"
    flat: yes
  with_items: "{{ groups['wordpress'] }}"

现在我们已经编写了我们的任务,我们可以尝试运行我们的角色。

运行 WPScan

要运行扫描,更新site.yml文件,使其看起来像下面的代码:

- hosts: wordpress
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/stack-install
    - roles/stack-config
    - roles/wordpress

- hosts: scan
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/docker
    - roles/wpscan

然后运行以下命令:

$ ansible-playbook -i production site.yml

这应该给你以下结果(截图只显示了扫描而不是完整的 playbook 运行,你应该看到):

此外,你应该在生成的文件夹中找到一个日志文件;其中包含了 WPScan 运行的结果:

正如你所看到的,这里有相当多的信息;然而,由于我们是从头开始部署 WordPress 安装,我们应该有一个干净的健康状况。

OWASP ZAP 角色

既然我们已经介绍了如何在 WPScan 角色中使用 Ansible 运行容器的基础知识,那么创建运行 OWASP ZAP 的角色应该很简单;我们只需使用这个命令:

$ ansible-galaxy init roles/zap

Open Web Application Security Project Zed Attack ProxyOWASP ZAP,是一个开源的 Web 应用安全扫描器。

roles/zap/defaults/main.yml中角色的默认值应包含此代码:

image: "owasp/zap2docker-stable"
log:
  remote_folder: /tmp/zap/
  local_folder: "generated/"
  file: "{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}-{{ ansible_date_time.minute }}.html"

正如您所看到的,我们使用owasp/zap2docker-stable镜像,同时我们还在 Vagrant 框中使用/tmp/zap/文件夹来存储报告文件。

继续进行roles/zap/tasks/main.yml中的任务,我们正在拉取镜像并创建文件夹,就像我们在 WPScan 角色中所做的那样:

- name: pull the image
  docker_image:
    name: "{{ image }}"

- name: create the folder which we will mount inside the container
  file:
    path: "{{ log.remote_folder }}"
    state: "directory"
    mode: "0777"

让我们看看我们将要运行的docker命令,以找出我们需要在下一个任务中放入什么:

$ docker container run -it --rm --name zap -v /tmp/zap/:/zap/wrk/ owasp/zap2docker-stable \
 zap-baseline.py -t http://10.20.30.41.nip.io/ -g gen.conf -r 10.20.30.41.nip.io-2018-05-19-14-26.html

正如您所看到的,该命令使用了我们之前使用的所有选项;在我们将文件夹挂载到容器中的位置上有所不同,因为 OWASP ZAP 希望我们将要保存的任何文件写入/zap/wrk/。这意味着当给出报告名称时,我们不必提供完整的文件系统路径,因为应用程序将默认写入/zap/wrk/

这意味着启动容器的任务应该如下代码所示:

- name: run the scan
  docker_container:
    detach: false
    auto_remove: true
    name: "zap"
    volumes: "{{ log.remote_folder }}:/zap/wrk/"
    image: "{{ image }}"
    command: "zap-baseline.py -t http://{{ hostvars[item]['ansible_host'] }} -g gen.conf -r {{ hostvars[item]['ansible_host'] }}-{{ log.file }}"
  register: docker_scan
  failed_when: docker_scan.rc == 0 or docker_scan.rc >= 2
  with_items: "{{ groups['wordpress'] }}"

然后我们使用以下任务下载报告:

- name: download the html report
  fetch:
    src: "{{ log.remote_folder }}{{ hostvars[item]['ansible_host'] }}-{{ log.file }}"
    dest: "{{ log.local_folder }}{{ hostvars[item]['ansible_host'] }}-{{ log.file }}"
    flat: yes
  with_items: "{{ groups['wordpress'] }}"

现在我们已经安排好了任务,我们可以运行该角色。

运行 OWASP ZAP

要运行扫描,我们只需将该角色附加到我们的site.yml文件的末尾。添加后,运行以下命令:

$ ansible-playbook -i production site.yml

这将运行 playbook;输出的摘要副本可以在此处找到:

然后将 HTML 文件复制到生成的文件夹中;文件应该类似于以下内容:

现在,您可以使用以下命令删除 Vagrant 框:

$ vagrant destroy

然后重新启动框并完整运行 playbook。

摘要

在本章中,我们已经看到如何将 Ansible 与 Docker 结合使用,以启动两种不同的工具,对我们使用 Ansible playbook 启动的 WordPress 安装进行外部漏洞扫描。这显示了我们如何可以启动一些相当复杂的工具,而无需担心直接在我们的主机上编写 playbook 来安装、配置和管理它们。

在下一章中,我们将离开命令行,看一下由红帽提供的 Ansible 的两个基于 Web 的界面。

问题

  1. 为什么我们使用 Docker 而不是直接在我们的 Vagrant 框上安装 WPScan 和 OWASP ZAP?

  2. 真或假:pip默认安装在我们的 Vagrant 框中。

  3. 我们需要安装哪个 Python 模块才能使 Ansible Docker 模块正常运行的模块名称是什么?

  4. 更新Vagrantfileproduction文件,以启动第二个 WordPress Vagrant 框并扫描它们。

进一步阅读

有关本章中使用的工具的更多信息,请参阅以下链接:

第十五章:介绍 Ansible Tower 和 Ansible AWX

在本章中,我们将研究 Ansible 的两个图形界面,商业版的 Ansible Tower 和开源版的 Ansible AWX。我们将讨论如何安装它们,它们的区别,以及为什么您需要使用它们。毕竟,我们现在已经进行了 15 章的 Ansible 之旅,但还没有需要使用图形界面。

在本章结束时,我们将有:

  • 安装了 Ansible Tower 和 Ansible AWX

  • 配置了两个工具

  • 使用 Ansible Tower 部署了我们的高可用云应用

技术要求

我们将使用 Vagrant box 在本地查看使用 Ansible Tower 和 Ansible AWX;我们还将使用我们在第十章中涵盖的 playbook,高可用云部署。最终的 playbook 可以在 GitHub 存储库github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter15中找到。

基于 Web 的 Ansible

在我们查看安装工具之前,我们应该先花时间讨论为什么我们需要它们以及它们之间的区别。

我相信您已经开始注意到我们迄今为止所涵盖的所有 playbook 之间的共同点——在可能的情况下,我们允许我们运行的角色使用尽可能多的参数。这使得我们可以轻松地更改 playbook 运行的输出,而无需直接重写或编辑角色。因此,我们也应该很容易开始使用 Red Hat 提供的两个基于 Web 的工具之一来管理您的 Ansible 部署。

Ansible Tower 是一个商业许可的基于 Web 的图形界面,用于 Ansible。正如前面提到的,您可能很难看到其中的价值。想象一下,将 Ansible 连接到公司的活动目录,并让开发人员等用户使用 Ansible Tower 根据您的 playbook 部署自己的环境,为您提供一种受控的方式来在整个系统中保持一致性,同时允许自助服务。

当 Red Hat 在 2015 年 10 月宣布收购 Ansible 时,发布的 FAQ 中提出的一个问题是:Red Hat 是否会开源 Ansible 的所有技术?之所以提出这个问题,是因为 Red Hat 在多年来收购的其他技术中,几乎已经开源了它们的所有方面,不仅邀请社区贡献,还测试和构建新功能,最终使其进入了 Red Hat 的商业支持版本。

其中一个例子是 Fedora 项目。该项目是 Red Hat 企业 Linux 功能的开源上游,包括 Fedora 用户现在正在利用的 DNF,这是 YUM 的替代品。自 2015 年以来,这一直是 Fedora 的默认软件包管理器,如果一切顺利,它应该会进入 Red Hat 企业 Linux 8。

Red Hat 开源其技术的其他示例包括 WildFly,这是 JBoss 的上游,以及由 Red Hat 赞助的 ManageIQ,它是 Red Hat CloudForms 的基础。

2017 年 9 月,Red Hat 宣布将发布 Ansible AWX,这是 Ansible Tower 的开源上游。该项目将与 AWX 团队一起进行每两周的发布,使某些发布版本稳定,尽管在这种情况下,稳定并不意味着项目已经准备投入生产,因为该项目仍处于初始开发周期中。

Ansible Tower

我们将从查看 Ansible Tower 开始。正如您可能还记得的那样,这是商业软件,所以我们需要许可证;幸运的是,Red Hat 提供了试用许可证。您可以通过点击www.ansible.com/上的“尝试 Tower 免费”按钮来请求。

请注意,您必须使用一个商业地址,Ansible 不会接受来自me.comicloud.comgmail.comhotmain.com等邮箱地址的请求。

过一会儿,您将收到一封类似以下内容的电子邮件:

点击“立即下载塔(.TAR)”按钮;这将打开您的浏览器并下载一个包含我们将用来部署 Ansible Tower 的 playbooks 的 TAR 文件。接下来,我们需要一个服务器来托管我们的 Ansible Tower 安装。让我们使用我们在其他章节中使用过的Vagrantfile

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
BOX_NAME = "centos/7"
BOX_IP = "10.20.30.40"
DOMAIN = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY = '~/.ssh/id_rsa.pub'

Vagrant.configure(API_VERSION) do |config|
  config.vm.box = BOX_NAME
  config.vm.network "private_network", ip: BOX_IP
  config.vm.host_name = BOX_IP + '.' + DOMAIN
  config.ssh.insert_key = false
  config.ssh.private_key_path = [PRIVATE_KEY, "~/.vagrant.d/insecure_private_key"]
  config.vm.provision "file", source: PUBLIC_KEY, destination: "~/.ssh/authorized_keys"

  config.vm.provider "virtualbox" do |v|
    v.memory = "2024"
    v.cpus = "2"
  end

  config.vm.provider "vmware_fusion" do |v|
    v.vmx["memsize"] = "2024"
    v.vmx["numvcpus"] = "2"
  end

end

一旦Vagrantfile就位,您可以使用以下命令之一启动 Vagrant 框:

$ vagrant up
$ vagrant up --provider=vmware_fusion

一旦您的 Vagrant 框已经启动运行,您可以查看需要对清单进行的更改,这些更改包含在我们下载的 TAR 文件中。

更新清单文件

在未解压的文件夹的顶层提供了几个文件,要解压文件夹,请双击 TAR 文件:

我们只需要担心inventory文件;在文本编辑器中打开文件并更新它,使其看起来像以下内容:

[tower]
10.20.30.40.nip.io ansible_connection=ssh ansible_user=vagrant ansible_private_key_file=~/.ssh/id_rsa host_key_checking=False

[database]

[all:vars]
admin_password='password'

pg_host=''
pg_port=''

pg_database='awx'
pg_username='awx'
pg_password='iHpkiPEAHpGeR8paCoVhwLPH'

rabbitmq_port=5672
rabbitmq_vhost=tower
rabbitmq_username=tower
rabbitmq_password='WUwTLJK2AtdxCfopcXFQoVYs'
rabbitmq_cookie=cookiemonster

# Needs to be true for fqdns and ip addresses
rabbitmq_use_long_name=true

# Isolated Tower nodes automatically generate an RSA key for authentication;
# To disable this behavior, set this value to false
# isolated_key_generation=true

正如您所看到的,我们已经更新了[tower]组下列出的主机,以包括我们的 Vagrant 框的详细信息和配置;我们还为admin_passwordpg_passwordrabbitmq_password参数添加了密码。显然,您可以设置自己的密码,而不是使用这里列出的密码。

文件的最终更改是将rabbitmq_use_long_namefalse更新为true。如果不这样做,将导致 RabbitMQ 服务无法启动。

运行 playbook

现在我们已经更新了inventory文件,我们可以运行install.yml playbook 来启动 Ansible Tower 的安装。要做到这一点,请运行以下命令:

$ ansible-playbook -i inventory --become install.yml

playbook 中内置了检查,以查看 playbook 是否作为 root 用户运行。在典型的安装中,playbook 期望您在要安装 Ansible Tower 的机器上以 root 用户身份运行 playbook。然而,我们正在稍微不同的方式进行,因此我们需要使用--become标志。

安装过程大约需要 20 分钟,正如您从以下输出中所看到的,安装程序会执行许多任务:

请求许可

现在我们已经安装了 Ansible Tower,还有一些步骤需要完成安装。第一步是登录;要做到这一点,请在浏览器中输入以下 URL:https://10.20.30.40.nip.io/。当您首次打开 Tower 时,将会收到有关 SSL 证书的警告;这是因为在部署期间安装的证书是自签名的。可以安全地继续。

现在您应该看到一个登录页面;将用户名输入为admin,密码输入为password,这是我们之前在inventory文件中设置的:

然后点击“登录”按钮;这将带您到一个页面,指示您输入许可文件:

点击“请求许可”按钮将带您到www.ansible.com/license/;在这里,您可以选择为您的安装请求两种类型的许可。我们将请求免费的 Ansible Tower 试用版 - 有限功能最多支持 10 个节点的许可。选择许可类型,填写表格,并按提示提交。

过一会儿,您应该会收到几封电子邮件,其中一封欢迎您使用 Ansible Tower。另一封电子邮件包含许可文件。复制附加的许可文件,并在 Tower 许可页面上使用 BROWSE 按钮上传它。还要审查并同意最终用户协议。上传许可文件并同意最终用户许可协议后,点击提交。

几秒钟后,您将首次看到 Ansible Tower 的外观:

现在我们已经安装了 Ansible Tower,我们可以开始运行我们的第一个 playbook。

hello world 演示项目

如您所见,我们已经配置了一个项目;这是一个非常基本的项目,它从github.com/ansible/ansible-tower-samples/下载示例 playbook,并显示消息 Hello World。在运行 playbook 之前,我们首先需要从 GitHub 下载一个副本;要做到这一点,请点击顶部菜单中的 PROJECTS。

您将能够看到列出的 Demo Project。将鼠标悬停在操作下的图标上将为您提供单击时每个图标将执行的描述;我们要点击的是第一个图标,即启动 SCM 更新。不久后,您应该看到 REVISION 和LAST UPDATED都已填充:

这意味着 Ansible Tower 现在已从 GitHub 下载了演示 playbook;我们现在可以运行 playbook。要做到这一点,请点击顶部菜单中的 TEMPLATES。

同样,您应该看到有一个名为 Demo Job Template 的模板,并且在该行的右侧有几个图标。我们要点击的是看起来像火箭的图标。点击使用此模板启动作业将运行演示作业;您将被带到一个屏幕,您可以在其中监视作业的进度。

完成后,您应该看到类似以下的内容:

如您所见,在左侧,您可以看到工作本身的概述;这告诉您状态,开始和结束的时间,以及哪个用户请求执行该工作。页面右侧的部分显示了 playbook 的输出,这与我们从命令行执行 playbook 时看到的完全相同。

让我们来运行一些更复杂的东西。

启动 AWS playbook

在第十章中,高可用云部署,我们通过一个 playbook 来运行 WordPress 的 AWS 核心 Ansible 模块来启动一个集群;在 GitHub 上托管了aws-wordpress playbook 的独立版本,网址为github.com/russmckendrick/aws-wordpress/。让我们使用这个来使用 Ansible Tower 部署我们的 AWS 集群。

在配置 Ansible Tower 中的 playbook 之前,我们需要对作为 Ansible Tower 安装的一部分部署的一些 Python 模块的版本进行一些清理。这是因为我们的 playbook 的某些部分需要更高版本的 Boto 模块。

为了做到这一点,我们需要通过运行以下命令 SSH 到我们的 Ansible Tower 主机:

$ vagrant ssh

现在我们以 Vagrant 用户登录,我们可以使用以下命令更改 root:

$ sudo -i

接下来,我们切换到与 Ansible Tower 使用相同的 Python 环境;为此,我们运行以下命令:

$ source /var/lib/awx/venv/ansible/bin/activate

现在我们正在使用正确的环境,我们需要使用以下命令升级boto库:

$ pip install boto boto3 botocore --upgrade

更新后,我们可以通过运行退出 Ansible Tower Python 环境:

$ deactivate

然后,我们使用exit命令退出:

现在我们的环境已更新,我们可以继续添加一个新项目。

添加一个新项目

我们需要做的第一件事是添加一个新项目;这是我们让 Ansible Tower 知道我们的 playbook 存储库的地方。如前所述,我们将使用一个 GitHub 存储库来存放代码。要添加新项目,点击顶部菜单中的项目,然后点击右侧的+添加按钮,该按钮可以在顶部菜单的图标行下方找到。

在这里,您将被要求输入一些信息;输入以下内容:

  • 名称:AWS 项目

  • 描述:AWS WordPress 集群

  • 组织:默认

  • SCM 类型:GIT

选择 SCM 类型时,将出现第二部分,要求输入源代码存放的详细信息:

  • SCM URL:https://github.com/russmckendrick/aws-wordpress.git

  • SCM 分支/标签/提交:主

  • SCM 凭据:留空,因为这是一个公开可访问的存储库

  • 清除:打勾

  • 更新时删除:打勾

  • 启动时更新:打勾

  • 缓存超时(秒):保持为零

输入详细信息后,点击保存。如果现在返回到项目页面,您应该会看到 Ansible 已经下载了 playbook 的源代码:

添加凭据

接下来,我们需要让 Ansible Tower 知道在访问我们的 AWS 账户时要使用的凭据;要添加这些凭据,点击顶部菜单中的设置图标(顶部菜单中的齿轮图标),您将被带到一个看起来像以下内容的屏幕:

正如您所看到的,这里有很多不同的选项。您可能已经猜到,我们感兴趣的选项是凭据。点击它将带您到一个页面,该页面会给您一个现有凭据的概述;我们想要添加一些新的凭据,所以点击+添加按钮。

这将带您到一个页面,布局类似于我们添加项目的页面。填写以下信息:

  • 名称:AWS API 凭据

  • 描述:AWS API 凭据

  • 组织:默认

  • 凭据类型:点击放大镜图标,选择 Amazon Web Services

选择凭据类型后,将添加第二部分;在这里,您可以输入以下内容:

  • 访问密钥:添加您在之前的 AWS 章节中的访问密钥,例如,AKIAI5KECPOTNTTVM3EDA

  • 秘钥:添加您在之前的 AWS 章节中的秘钥,例如,Y4B7FFiSWl0Am3VIFc07lgnc/TAtK5+RpxzIGTr

  • STS 令牌:留空

表单填写完成后,点击保存。保存后,您会注意到密钥被标记为加密:

当您在 Ansible Tower 中保存敏感信息时,它会被加密,您只能选择替换或恢复它。在任何时候,您都不能再查看这些信息。

添加库存

现在我们已经有了凭据,我们需要在 Ansible Tower 中重新创建名为production的库存文件的内容。作为提醒,文件看起来像下面这样:

# Register all of the host groups we will be creating in the playbooks
[ec2_instance]
[already_running]

# Put all the groups into into a single group so we can easily apply one config to it for overriding things like the ssh user and key location
[aws:children]
ec2_instance
already_running

# Finally, configure some bits to allow us access to the instances before we deploy our credentials using Ansible
[aws:vars]
ansible_ssh_user=centos
ansible_ssh_private_key_file=~/.ssh/id_rsa
host_key_checking=False

要添加库存,点击顶部菜单中的库存,然后点击+添加按钮。您会注意到+添加按钮现在会弹出一个下拉列表;从该列表中,我们要添加一个库存。

在打开的表单中,输入以下内容:

  • 名称:AWS 库存

  • 描述:AWS 库存

  • 组织:默认

  • 洞察凭据:留空

  • 洞察组:留空

  • 变量:输入以下列出的值:

ansible_ssh_user: "centos"
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
host_key_checking: "False"

输入后,点击保存;这将创建库存,现在我们可以添加我们需要的两个组。要做到这一点,点击组,可以在表单上方的按钮行中找到:

点击+添加组,然后输入以下详细信息:

  • 名称:ec2_instance

  • 描述:ec2_instance

  • 变量:留空

然后点击保存,重复该过程,并使用以下详细信息添加第二个组:

  • 名称:already_running

  • 描述:already_running

  • 变量:留空

再次点击保存;现在应该列出两个组:

现在我们已经有了我们的项目、库存和一些用于访问我们的 AWS 的凭据,我们只需要添加模板,一个用于启动和配置集群,另一个用于终止它。

添加模板

点击顶部菜单中的 TEMPLATES,然后在+ADD 按钮的下拉菜单中选择作业模板。这是我们迄今为止遇到的最大表单;但是,当我们开始填写详细信息时,其中的部分将自动填充。让我们开始吧:

  • 名称:AWS - 启动

  • 描述:启动和部署 WordPress 实例

  • 工作类型:保持为运行

  • 库存:点击图标并选择 AWS 库存

  • 项目:点击图标并选择AWS 项目

  • 游戏规则:从下拉列表中选择site.yml

  • 凭据:选择 Amazon Web Services 的凭据类型,然后选择 AWS API 凭据;还为 MACHINE 选择演示凭据

  • 分叉:保持默认

  • 限制:留空

  • 详细程度:保持为0(正常)

  • 实例组、作业标签、跳过标签、标签:留空

  • 显示更改:关闭

  • 选项和额外变量:保持默认值

点击保存,您可以添加第二个模板来删除集群。要做到这一点,点击+ADD 按钮并再次选择作业模板;这次使用以下信息:

  • 名称:AWS - 删除

  • 描述:移除 WordPress 集群

  • 工作类型:保持为运行

  • 库存:点击图标并选择 AWS 库存

  • 项目:点击图标并选择AWS 项目

  • 游戏规则:从下拉列表中选择remove.yml

  • 凭据:选择 Amazon Web Services 的凭据类型,然后选择 AWS API 凭据;还为 MACHINE 选择演示凭据

  • 分叉:保持默认

  • 限制:留空

  • 详细程度:保持为0(正常)

  • 实例组、作业标签、跳过标签、标签:留空

  • 显示更改:关闭

  • 选项和额外变量:保持默认值

运行剧本

现在我们的剧本已经准备好运行,我们可以通过点击顶部菜单中的 TEMPLATES,然后点击AWS -Launch旁边的运行图标,来运行它。这将花费与我们从命令行执行时一样多的时间来运行:

正如您从前面的截图中所看到的,一切都按预期构建和运行,这意味着当我们转到弹性负载均衡器 URL 时,我们将能够看到我们的 WordPress 网站:

删除集群

现在我们已经启动了集群,我们可以运行第二个剧本,将其删除。要做到这一点,点击顶部菜单中的 TEMPLATES,然后点击运行图标,即AWS -Remove旁边的火箭图标。这将启动剧本,删除我们刚刚启动的一切。同样,运行所有任务需要一点时间。

需要指出的是,为了使remove.yml剧本能够通过 Ansible Tower 成功执行,您必须更新roles/remove/tasks/main.yml中的一个任务。如果您还记得,我们在那里有以下几行:

- name: prompt
  pause:
    prompt: "Make sure the elastic load balancer has been terminated before proceeding"

如果此任务存在,那么我们的剧本执行将在此任务处停顿,而不会继续进行,因为 Ansible Tower 剧本运行不是交互式的。该任务已被以下内容替换:

- name: wait for 2 minutes before continuing
  pause:
    minutes: 2

这是我们的剧本能够在 Ansible Tower 上运行所需的唯一更改;其他一切保持不变。

Tower 摘要

虽然我们只有时间运行了一个基本的剧本,但我相信您已经开始看到使用 Ansible Tower 为所有用户运行剧本的优势了。您可以使用许多功能。但是,目前有三个不同版本的 Ansible Tower 可用。以下表格提供了每个版本中可用功能的快速概述:

功能 自助支持 标准 高级
仪表板:获取 Ansible Tower 状态的概述
实时作业输出:实时查看作业的输出
作业调度:按计划执行作业;还可以设置重复运行,例如,每个工作日上午 9 点运行部署开发实例的作业
从源代码控制中拉取:将您的 playbooks 托管在源代码控制中,比如 Git 或 SVN
工作流程:在一个作业中链接多个 playbooks
基于角色的访问:对用户及其访问权限进行精细控制
与第三方身份验证集成:将您的 Tower 安装连接到 Active Directory 或 LDAP 身份验证服务器
调查:为用户构建表单,作为作业运行的一部分填写;这允许用户提供信息,而无需编写任何 YAML
来自红帽的 8x5 支持
来自红帽的 24x7 支持

Ansible Tower 的当前许可成本如下:

  • 自助支持最多 10 个节点:免费;这是我们应用于我们的安装的许可证

  • 自助支持最多 100 个节点:每年 5,000 美元

  • 自助支持最多 250 个节点:每年 10,000 美元

  • 标准最多 100 个节点:每年 10,000 美元

  • 标准超过 100 个节点:自定义定价,请联系 Ansible

  • 高级最多 100 个节点:每年 14,000 美元

  • 高级超过 100 个节点:自定义定价,请联系 Ansible

这些价格不包括由红帽支持的 Ansible Engine;如果您想要受支持的 Ansible 引擎,除了这里列出的费用之外,还有额外的费用。

因此,虽然 Ansible Tower 非常好,但可能不在每个人的预算范围内,这就是 Ansible AWX 的用武之地。

Ansible AWX

让我们直接开始安装 Ansible AWX;我们将需要一个 Vagrant box,在 Vagrant box 上安装 Docker,最后是 AWX 源的副本。

准备 playbook

对于我们的安装,我们将使用 Ansible 来准备我们的 Vagrant box 并安装 Ansible AWX。要为 playbook 创建结构,请运行以下命令:

$ mkdir awx awx/group_vars awx/roles
$ touch awx/production awx/site.yml awx/group_vars/common.yml awx/Vagrantfile

我们将使用的Vagrantfile可以在这里找到:

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
BOX_NAME = "centos/7"
BOX_IP = "10.20.30.50"
DOMAIN = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY = '~/.ssh/id_rsa.pub'

Vagrant.configure(API_VERSION) do |config|
  config.vm.box = BOX_NAME
  config.vm.network "private_network", ip: BOX_IP
  config.vm.host_name = BOX_IP + '.' + DOMAIN
  config.ssh.insert_key = false
  config.ssh.private_key_path = [PRIVATE_KEY, "~/.vagrant.d/insecure_private_key"]
  config.vm.provision "file", source: PUBLIC_KEY, destination: "~/.ssh/authorized_keys"

  config.vm.provider "virtualbox" do |v|
    v.memory = "2024"
    v.cpus = "2"
  end

  config.vm.provider "vmware_fusion" do |v|
    v.vmx["memsize"] = "2024"
    v.vmx["numvcpus"] = "2"
  end

end

我们要创建的第一个角色是我们已经涵盖过的角色;它是来自第十四章的 Docker 角色,部署 WPScan 和 OWASP ZAP

docker 角色

我不打算详细介绍任务,因为这些已经涵盖过了。我们可以通过运行以下命令来引导角色:

$ ansible-galaxy init roles/docker

现在我们已经放置了文件,我们可以使用以下内容更新roles/docker/defaults/main.yml文件:

docker:
  gpg_key: "https://download.docker.com/linux/centos/gpg"
  repo_url: "https://download.docker.com/linux/centos/docker-ce.repo"
  repo_path: "/etc/yum.repos.d/docker-ce.repo"
  packages:
    - "docker-ce"
    - "device-mapper-persistent-data"
    - "lvm2"
    - "python-setuptools"
    - "libselinux-python"
  pip:
    - "docker"

roles/docker/tasks/main.yml的内容应该是:

- name: update all of the installed packages
  yum:
    name: "*"
    state: "latest"
    update_cache: "yes"

- name: add the gpg key for the docker repo
  rpm_key:
    key: "{{ docker.gpg_key }}"
    state: "present"

- name: add docker repo from the remote url
  get_url:
    url: "{{ docker.repo_url }}"
    dest: "{{ docker.repo_path }}"
    mode: "0644"

- name: install the docker packages
  yum:
    name: "{{ item }}"
    state: "installed"
    update_cache: "yes"
  with_items: "{{ docker.packages }}"

- name: install pip
  easy_install:
    name: pip
    state: latest

- name: install the python packages
  pip:
    name: "{{ item }}"
  with_items: "{{ docker.pip }}"

- name: put selinux into permissive mode
  selinux:
    policy: targeted
    state: permissive

- name: start docker and configure to start on boot
  service:
    name: "docker"
    state: "started"
    enabled: "yes"

这应该安装 AWX 安装的 Docker 部分,并允许我们转移到下一个角色。

awx 角色

我们的 AWX 安装的下一个(有点)最终角色可以通过运行以下命令创建:

$ ansible-galaxy init roles/awx

roles/awx/defaults/main.yml中的默认变量格式与docker角色中的变量类似:

awx:
  repo_url: "https://github.com/ansible/awx.git"
  logo_url: "https://github.com/ansible/awx-logos.git"
  repo_path: "~/awx/"
  packages:
    - "git"
  pip:
    - "ansible"
    - "boto"
    - "boto3"
    - "botocore"
  install_command: 'ansible-playbook -i inventory --extra-vars "awx_official=true" install.yml'

从头开始,我们有两个不同的 GitHub 存储库 URL。第一个awx.repo_url是主 AWX 存储库,第二个awx.logo_url是官方标志包。接下来,我们有路径awx.repo_path,我们也想检出代码。在这种情况下,它是~/awx,因为我们使用become,它将是/root/awx/

要从 GitHub 检出代码,我们需要确保已安装 Git。awx.packages是我们需要使用yum安装的唯一附加软件包。接下来,我们需要安装 Ansible 本身以及我们将使用 PIP(awx.pip)安装的其他一些 Python 软件包。

最后,我们有一个命令(awx.install_command),我们需要运行以安装 Ansible AWX。如您所见,我们正在使用作为我们正在检查的代码的一部分提供的 Ansible playbook;命令本身正在通过传递awx_official=true作为额外变量来覆盖使用官方 AWX 标志的选项。

现在我们已经讨论了我们需要定义的变量,我们可以将任务添加到roles/awx/tasks/main.yml中,从安装 Yum 和 Pip 软件包的任务开始:

- name: install the awx packages
  yum:
    name: "{{ item }}"
    state: "installed"
    update_cache: "yes"
  with_items: "{{ awx.packages }}"

- name: install the python packages
  pip:
    name: "{{ item }}"
  with_items: "{{ awx.pip }}"

接下来,我们有检出两个 AWX 存储库的任务来自 GitHub:

- name: check out the awx repo
  git:
    repo: "{{ awx.repo_url }}"
    dest: "{{ awx.repo_path }}"
    clone: "yes"
    update: "yes"

- name: check out the awx logos repo
  git:
    repo: "{{ awx.logo_url }}"
    dest: "{{ awx.repo_path }}"
    clone: "yes"
    update: "yes"

如您所见,两个存储库都将移动到 Vagrant 盒子上的相同位置。最后一个任务运行了下载、配置和启动 Ansible AWX Docker 容器的 playbook:

- name: install awx
  command: "{{ awx.install_command }}"
  args:
    chdir: "{{ awx.repo_path }}installer"

运行 playbook

现在我们已经准备好了我们的 playbook,我们可以将我们的主机清单信息添加到production文件中:

box ansible_host=10.20.30.50.nip.io

[awx]
box

[awx:vars]
ansible_connection=ssh
ansible_user=vagrant
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

最后,我们可以将以下内容添加到site.yml文件中,然后我们就可以运行我们的安装了:

---

- hosts: awx
  gather_facts: true
  become: yes
  become_method: sudo

  vars_files:
    - group_vars/common.yml

  roles:
    - roles/docker
    - roles/awx

为了让 Ansible AWX 运行起来,我们需要执行以下命令中的一个来启动 Vagrant 盒子:

$ vagrant up
$ vagrant up --provider=vmware_fusion

然后,以下命令将运行 playbook:

$ ansible-playbook -i production site.yml

运行 playbook 需要几分钟时间;完成后,您应该会看到类似以下的内容:

打开浏览器并转到http://10.20.30.50.nip.io/应该会显示以下消息:

保持页面打开,几分钟后,您应该会看到一个登录提示。

使用 Ansible AWX

您应该会看到登录提示。用户名和密码是admin/password

当您首次登录时,您可能会注意到外观和感觉与 Ansible Tower 相似,尽管有一些差异:

如您所见,菜单已从顶部移至左侧,并且还有更多选项。在左侧菜单中点击 PROJECTS 将带您到页面,您可以获取我们在 Ansible Tower 中首次运行的 hello-world 示例的最新 SVM 修订版。点击云图标进行下载。

一旦项目同步完成,点击左侧菜单中的 TEMPLATES;您应该会看到一个空列表。点击+按钮并从下拉列表中选择作业模板。

这将带你到一个页面,与我们在 Ansible Tower 中添加模板时看到的相同。填写以下细节:

  • 名称:Demo Template

  • 描述:运行 hello-world 示例

  • 作业类型:保持为运行

  • 清单:点击图标并选择Demo Inventory

  • PROJECT:点击图标并选择Demo Project

  • PLAYBOOK:从下拉列表中选择hello-world.yml

  • CREDENTIAL:点击图标并从列表中选择Demo Credential

  • FORKS:保持默认

  • LIMIT:留空

  • VERBOSITY:保持为0(正常)

  • INSTANCE GROUPSJOB TAGSSKIP TAGSLABELS:留空

  • 显示更改:保持关闭

  • OPTIONSEXTRA VARIABLES:保持默认值

填写完毕后,点击表单底部的保存按钮。现在点击左侧菜单中的 TEMPLATES 将显示Demo Template

点击火箭图标,或者使用此模板启动作业,将运行 hello world playbook:

所以我们已经对 Ansible AWX 进行了一个非常快速的概述,正如我已经提到的,它与 Ansible Tower 并没有太大的不同。

AWX 摘要

让我们现在解决这个问题。在撰写本文时,红帽不建议在生产环境中使用 Ansible AWX。就我个人而言,我发现它相当稳定,尤其是对于不断发展的软件。当然,在升级时可能会出现一些问题,但大多数情况下这些问题都很小。

由于 Ansible AWX 是 Ansible Tower 的上游,因此具有一些功能,例如能够使用第三方身份验证服务和工作流程,这些功能在自支持版本的 Ansible Tower 中不存在。您可以管理的主机数量也没有限制。这使得 Ansible AWX 成为 Ansible Tower 的一个非常有吸引力的替代品;但是,您需要考虑其开发周期以及升级可能如何影响您的 AWX 安装的日常运行。

总结

在本章中,我们已经通过安装和使用两种不同的 Web 前端来运行您的 Ansible playbooks。我们还讨论了前端各个版本之间的成本、功能和稳定性差异。

我相信您会同意,使用诸如 Ansible Tower 或 Ansible AWX 这样的工具将允许您的用户、同事和最终用户以受支持和一致的方式使用您编写的 playbooks。

在下一章中,我们将更详细地了解ansible-galaxy命令和服务。

问题

  1. 阐明 Ansible Tower 和 Ansible AWX 之间的区别并解释。

  2. 使用 Ansible AWX,配置并运行 AWS WordPress playbook,就像我们在 Ansible Tower 中所做的那样。

进一步阅读

有关这两个软件的更多详细信息,请参阅以下网址:

第十六章:Ansible Galaxy

在之前的章节中,我们一直在使用ansible-galaxy命令。在本章中,我们将看看该命令提供的更多功能。Ansible Galaxy 是一个社区贡献角色的在线存储库;我们将发现一些最好的可用角色,如何使用它们,以及如何创建自己的角色并将其托管在 Ansible Galaxy 上。

到本章结束时,我们将完成以下工作:

  • 对 Ansible Galaxy 的介绍

  • 如何在自己的 playbooks 中使用 Ansible Galaxy 的角色

  • 如何编写和提交您自己的角色到 Ansible Galaxy

技术要求

在本章中,我们将再次使用本地 Vagrant 框;所使用的 playbooks 可以在附带的存储库中找到github.com/PacktPublishing/Learn-Ansible/tree/master/Chapter16。您还需要访问 GitHub 账户——一个免费账户就可以——您可以在github.com/注册一个。

对 Ansible Galaxy 的介绍

Ansible Galaxy 是许多东西:首先,它是一个网站,可以在galaxy.ansible.com/找到。该网站是社区贡献的角色和模块的家园:

到目前为止,我们一直在编写我们自己的角色,这些角色与 Ansible Core 模块进行交互,用于我们的 playbook。我们可以使用 Ansible Galaxy 上发布的 15,000 多个角色中的一个,而不是编写我们自己的角色。这些角色涵盖了多种任务,并且几乎支持 Ansible 支持的所有操作系统。

ansible-galaxy命令是一种从自己的命令行舒适地与 Ansible Galaxy 网站交互的方式,同时还能够引导角色。就像我们在之前的章节中使用它一样,我们也可以使用它来下载、搜索和发布我们自己的角色到 Ansible Galaxy。

最后,Red Hat 已经开源了 Ansible Galaxy 的代码,这意味着您也可以在需要在防火墙后分发自己的角色时运行自己的网站。

Jenkins playbook

让我们直接开始创建一个 playbook,只使用从 Ansible Galaxy 下载的角色来安装 Jenkins。

Jenkins,以前是 Hudson 项目,是一个用 Java 编写的开源持续集成和持续交付服务器。它可以使用插件进行扩展,并且已经远远超出了最初编译 Java 应用程序的目的。

首先,我们需要一些文件;现在通过运行以下命令来创建这些文件:

$ mkdir jenkins
$ cd jenkins
$ touch production requirements.yml site.yml Vagrantfile

正如您所看到的,我们并没有像在之前的章节中那样创建rolesgroup_vars文件夹。相反,我们正在创建一个requirements.yml文件。这将包含我们想要从 Ansible Galaxy 下载的角色列表。

在我们的情况下,我们将使用以下两个角色:

第一个角色geerlingguy.java管理主机上 Java 的安装,然后第二个角色geerlingguy.jenkins管理 Jenkins 本身的安装和配置。要安装这些角色,我们需要将以下行添加到我们的requirements.yml文件中:

- src: "geerlingguy.java"
- src: "geerlingguy.jenkins"

添加后,我们可以通过运行以下命令下载角色:

$ ansible-galaxy install -r requirements.yml

您应该看到类似以下的输出:

从终端输出中可以看到,这两个角色已从 GitHub 项目的roles文件夹中下载,并放置在~/.ansible/roles/文件夹中。

在 macOS 和 Linux 上使用~表示当前用户的主目录。

您可以忽略警告;它只是让我们知道geerlingguy.jenkins角色想要安装geerlingguy.java角色的旧版本。在我们的情况下,这不会造成任何问题。

现在我们已经下载了两个角色,我们可以编写site.yml文件来启动 Jenkins。应该如下所示:

---

- hosts: jenkins
  gather_facts: true
  become: yes
  become_method: sudo

  vars:
    java_packages: "java-1.8.0-openjdk"
    jenkins_hostname: "10.20.30.60.nip.io"
    jenkins_admin_username: "ansible"
    jenkins_admin_password: "Pa55w0rD"

  roles:
    - geerlingguy.java
    - geerlingguy.jenkins

请注意,我们只是提供了角色的名称。默认情况下,如果在 playbook 的本地roles文件夹中找不到角色,Ansible 将在~/.ansible/roles/文件夹中搜索角色。

我们还传递了四个变量:

  • java_packages:这是我们希望角色安装的geerlingguy.java角色的名称;由于 Jenkins 需要 Java 8,而我们正在运行 CentOS 7 主机,包名称是java-1.8.0-openjdk

剩下的三个变量影响geerlingguy.jenkins角色的配置:

  • jenkins_hostname:这是我们希望在其上访问 Jenkins 的 URL;与之前的章节一样,我们使用nip.io服务为我们的 Vagrant box 提供可解析的主机名

  • jenkins_admin_username:这是我们要配置以访问 Jenkins 的管理员用户名

  • jenkins_admin_password:这是用户的密码

接下来,我们有production主机的清单文件:

box ansible_host=10.20.30.60.nip.io

[jenkins]
box

[jenkins:vars]
ansible_connection=ssh
ansible_user=vagrant
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

最后,Vagrantfile的内容如下:

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
BOX_NAME = "centos/7"
BOX_IP = "10.20.30.60"
DOMAIN = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY = '~/.ssh/id_rsa.pub'

Vagrant.configure(API_VERSION) do |config|
  config.vm.box = BOX_NAME
  config.vm.network "private_network", ip: BOX_IP
  config.vm.host_name = BOX_IP + '.' + DOMAIN
  config.ssh.insert_key = false
  config.ssh.private_key_path = [PRIVATE_KEY, "~/.vagrant.d/insecure_private_key"]
  config.vm.provision "file", source: PUBLIC_KEY, destination: "~/.ssh/authorized_keys"

  config.vm.provider "virtualbox" do |v|
    v.memory = "2024"
    v.cpus = "2"
  end

  config.vm.provider "vmware_fusion" do |v|
    v.vmx["memsize"] = "2024"
    v.vmx["numvcpus"] = "2"
  end

end

现在我们已经将所有需要的文件放置并填充了正确的代码,我们可以启动我们的 Jenkins 服务器了。首先,我们需要创建 Vagrant box:

$ vagrant up
$ vagrant up --provider=vmware_fusion

一旦 Vagrant box 启动运行,我们可以使用以下命令运行 playbook:

$ ansible-playbook -i production site.yml

安装和配置 Java 和 Jenkins 需要几分钟;您可以在这里查看 playbook 运行的输出:

PLAY [jenkins] **********************************************************************************

TASK [Gathering Facts] **************************************************************************
ok: [box]

TASK [geerlingguy.java : Include OS-specific variables.] ****************************************
ok: [box]

TASK [geerlingguy.java : Include OS-specific variables for Fedora.] *****************************
skipping: [box]

TASK [geerlingguy.java : Include version-specific variables for Debian.] ************************
skipping: [box]

TASK [geerlingguy.java : Define java_packages.] *************************************************
skipping: [box]

TASK [geerlingguy.java : include_tasks] *********************************************************
included: /Users/russ/.ansible/roles/geerlingguy.java/tasks/setup-RedHat.yml for box

TASK [geerlingguy.java : Ensure Java is installed.] *********************************************
changed: [box] => (item=java-1.8.0-openjdk)

TASK [geerlingguy.java : include_tasks] *********************************************************
skipping: [box]

TASK [geerlingguy.java : include_tasks] *********************************************************
skipping: [box]

TASK [geerlingguy.java : Set JAVA_HOME if configured.] ******************************************
skipping: [box]

TASK [geerlingguy.jenkins : Include OS-Specific variables] **************************************
ok: [box]

TASK [geerlingguy.jenkins : Define jenkins_repo_url] ********************************************
ok: [box]

TASK [geerlingguy.jenkins : Define jenkins_repo_key_url] ****************************************
ok: [box]

TASK [geerlingguy.jenkins : Define jenkins_pkg_url] *********************************************
ok: [box]

TASK [geerlingguy.jenkins : include_tasks] ******************************************************
included: /Users/russ/.ansible/roles/geerlingguy.jenkins/tasks/setup-RedHat.yml for box

TASK [geerlingguy.jenkins : Ensure dependencies are installed.] *********************************
ok: [box]

TASK [geerlingguy.jenkins : Ensure Jenkins repo is installed.] **********************************
changed: [box]

TASK [geerlingguy.jenkins : Add Jenkins repo GPG key.] ******************************************
changed: [box]

TASK [geerlingguy.jenkins : Download specific Jenkins version.] *********************************
skipping: [box]

TASK [geerlingguy.jenkins : Check if we downloaded a specific version of Jenkins.] **************
skipping: [box]

TASK [geerlingguy.jenkins : Install our specific version of Jenkins.] ***************************
skipping: [box]

TASK [geerlingguy.jenkins : Ensure Jenkins is installed.] ***************************************
changed: [box]

TASK [geerlingguy.jenkins : include_tasks] ******************************************************
skipping: [box]

TASK [geerlingguy.jenkins : include_tasks] ******************************************************
included: /Users/russ/.ansible/roles/geerlingguy.jenkins/tasks/settings.yml for box

TASK [geerlingguy.jenkins : Modify variables in init file] **************************************
changed: [box] => (item={u'option': u'JENKINS_ARGS', u'value': u'--prefix='})
changed: [box] => (item={u'option': u'JENKINS_JAVA_OPTIONS', u'value': u'-Djenkins.install.runSetupWizard=false'})

TASK [geerlingguy.jenkins : Set the Jenkins home directory] *************************************
changed: [box]

TASK [geerlingguy.jenkins : Immediately restart Jenkins on init config changes.] ****************
changed: [box]

TASK [geerlingguy.jenkins : Set HTTP port in Jenkins config.] ***********************************
changed: [box]

TASK [geerlingguy.jenkins : Ensure jenkins_home /var/lib/jenkins exists] ************************
ok: [box]

TASK [geerlingguy.jenkins : Create custom init scripts directory.] ******************************
changed: [box]

RUNNING HANDLER [geerlingguy.jenkins : configure default users] *********************************
changed: [box]

TASK [geerlingguy.jenkins : Immediately restart Jenkins on http or user changes.] ***************
changed: [box]

TASK [geerlingguy.jenkins : Ensure Jenkins is started and runs on startup.] *********************
ok: [box]

TASK [geerlingguy.jenkins : Wait for Jenkins to start up before proceeding.] ********************
FAILED - RETRYING: Wait for Jenkins to start up before proceeding. (60 retries left).
 [WARNING]: Consider using the get_url or uri module rather than running curl. If you need to use
command because get_url or uri is insufficient you can add warn=False to this command task or set
command_warnings=False in ansible.cfg to get rid of this message.

ok: [box]

TASK [geerlingguy.jenkins : Get the jenkins-cli jarfile from the Jenkins server.] ***************
changed: [box]

TASK [geerlingguy.jenkins : Remove Jenkins security init scripts after first startup.] **********
changed: [box]

TASK [geerlingguy.jenkins : include_tasks] ******************************************************
included: /Users/russ/.ansible/roles/geerlingguy.jenkins/tasks/plugins.yml for box

TASK [geerlingguy.jenkins : Get Jenkins admin password from file.] ******************************
skipping: [box]

TASK [geerlingguy.jenkins : Set Jenkins admin password fact.] ***********************************
ok: [box]

TASK [geerlingguy.jenkins : Get Jenkins admin token from file.] *********************************
skipping: [box]

TASK [geerlingguy.jenkins : Set Jenkins admin token fact.] **************************************
ok: [box]

TASK [geerlingguy.jenkins : Create update directory] ********************************************
ok: [box]

TASK [geerlingguy.jenkins : Download current plugin updates from Jenkins update site] ***********
changed: [box]

TASK [geerlingguy.jenkins : Remove first and last line from json file] **************************
ok: [box]

TASK [geerlingguy.jenkins : Install Jenkins plugins using password.] ****************************

TASK [geerlingguy.jenkins : Install Jenkins plugins using token.] *******************************

PLAY RECAP **************************************************************************************
box : ok=32 changed=14 unreachable=0 failed=0

一旦 playbook 完成,您应该能够在http://10.20.30.60.nip.io:8080/访问您新安装的 Jenkins,并使用我们在site.yml文件中定义的管理员用户名和密码登录:

如您所见,使用预定义的社区角色部署我们的 Jenkins 安装比编写我们自己的角色要简单得多。在几分钟内,我们就能够编写一个 playbook 并部署应用程序,而且只需要基本的安装应用程序的理解。事实上,只需要快速浏览一下 Ansible Galaxy 上两个角色的 readme 文件就足够了。

发布角色

现在我们知道了下载角色有多么容易,让我们看看如何通过创建角色向社区做出贡献。在过去的几章中,我们一直在使用 Ansible 来安装 Docker。因此,让我们以此为基础,扩展角色以支持 Ubuntu,并安装 Docker CE Edge 版本,而不是稳定版本。

创建 docker 角色

首先,我们需要基本文件;要获取这些文件,请在通常存储代码的位置运行以下命令:

$ ansible-galaxy init ansible-role-docker

这将为我们提供我们新角色所需的目录和文件结构;现在我们可以开始创建角色了。

变量

我们将从vars文件夹中的文件开始;我们将保持vars/main.yml文件为空,并添加两个以vars/RedHat.yml开头的新文件:

---
# vars file for ansible-role-docker

docker:
  gpg_key: "https://download.docker.com/linux/centos/gpg"
  repo_url: "https://download.docker.com/linux/centos/docker-ce.repo"
  repo_path: "/etc/yum.repos.d/docker-ce.repo"
  edge: "docker-ce-edge"
  packages:
    - "docker-ce"
    - "device-mapper-persistent-data"
    - "lvm2"
    - "python-setuptools"
    - "libselinux-python"
  pip:
    - "docker"

要添加的下一个文件是vars/Debian.yml

---
# vars file for ansible-role-docker

docker:
  gpg_key: "https://download.docker.com/linux/ubuntu/gpg"
  repo: "deb [arch=amd64] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release | lower }} edge"
  system_packages:
    - "apt-transport-https"
    - "ca-certificates"
    - "curl"
    - "software-properties-common"
    - "python3-pip"
  packages:
    - "docker-ce"
  pip:
    - "docker"

这两个文件包含了我们安装 Docker CE 所需的所有信息。

任务

由于我们针对两个不同的操作系统,我们的tasks/main.yml文件需要如下所示:

---
# tasks file for ansible-role-docker

- name: include the operating system specific variables
  include_vars: "{{ ansible_os_family }}.yml"

- name: install the stack on centos
  import_tasks: install-redhat.yml
  when: ansible_os_family == 'RedHat'

- name: install the stack on ubuntu
  import_tasks: install-ubuntu.yml
  when: ansible_os_family == 'Debian'

正如您所看到的,在第六章中在两个操作系统上安装 LEMP Stack 时一样,tasks/install-redhat.yml文件看起来与我们在之前章节中用于安装 Docker 的任务非常相似:

---
# tasks file for ansible-role-docker

- name: add the gpg key for the docker repo
  rpm_key:
    key: "{{ docker.gpg_key }}"
    state: "present"

- name: add docker repo from the remote url
  get_url:
    url: "{{ docker.repo_url }}"
    dest: "{{ docker.repo_path }}"
    mode: "0644"

- name: install the docker packages
  yum:
    name: "{{ item }}"
    state: "installed"
    update_cache: "yes"
    enablerepo: "{{ docker.edge }}"
  with_items: "{{ docker.packages }}"

- name: install pip
  easy_install:
    name: pip
    state: latest

- name: install the python packages
  pip:
    name: "{{ item }}"
  with_items: "{{ docker.pip }}"

- name: put selinux into permissive mode
  selinux:
    policy: targeted
    state: permissive

- name: start docker and configure to start on boot
  service:
    name: "docker"
    state: "started"
    enabled: "yes"

唯一的区别是在安装软件包时启用了 Docker CE Edge 存储库,并且在安装 Docker 时我们没有运行yum update。我们之所以不这样做,是因为更新服务器不是我们角色的决定,当其他人运行角色时,我们的角色只应该安装 Docker。

最终的任务文件是tasks/install-ubuntu.yml。正如你已经猜到的那样,其中包含了在 Ubuntu 主机上安装 Docker 的任务:

---
# tasks file for ansible-role-docker

- name: install the system packages
  apt:
    name: "{{ item }}"
    state: "present"
    update_cache: "yes"
  with_items: "{{ docker.system_packages }}"

- name: add the apt keys from a key server
  apt_key:
    url: "{{ docker.gpg_key }}"
    state: present

- name: add the apt repo
  apt_repository:
    repo: "{{ docker.repo }}"
    state: present

- name: install the docker package
  apt:
    name: "{{ item }}"
    state: "present"
    update_cache: "yes"
    force: "yes"
  with_items: "{{ docker.packages }}"

- name: install the python packages
  pip:
    name: "{{ item }}"
  with_items: "{{ docker.pip }}"

- name: start docker and configure to start on boot
  service:
    name: "docker"
    state: "started"
    enabled: "yes"

这就结束了我们在两种不同操作系统上安装 Docker 所需的所有任务和变量。在以前的章节中,这已经足够让我们将角色添加到我们的 playbook 并运行任务了。然而,由于我们将在 Ansible Galaxy 上发布这个角色,我们需要添加一些关于角色的更多信息。

Metadata

当你浏览 Ansible Galaxy 时,你可能已经看到,每个上传的角色都有关于作者、适用对象、许可证、支持的 Ansible 版本等信息。这些信息都来自于meta/main.yml文件。我们发布的文件看起来像下面这样:

---

galaxy_info:
  author: "Russ McKendrick"
  description: "Role to install the Docker CE Edge release on either an Enterprise Linux or Ubuntu host"
  license: "license (BSD)"
  min_ansible_version: 2.4
  platforms:
    - name: EL
      versions:
      - 6
      - 7
    - name: Ubuntu
      versions:
      - bionic
      - artful
      - xenial
  galaxy_tags:
    - docker

dependencies: []

正如你所看到的,我们在一个 YAML 文件中提供了信息,当我们发布角色时,Ansible Galaxy 将读取这些信息。文件中的大部分信息都是不言自明的,所以我在这里不会详细介绍:

  • author: 这是你的名字或选择的别名。

  • description: 添加你的角色描述;这将出现在命令行和 web 界面的搜索结果中,所以保持简短,不要添加任何标记。

  • license: 你发布角色的许可证;默认是 BSD。

  • min_ansible_version: 你的角色将使用的 Ansible 版本。记住,如果你使用了新功能,那么你必须使用该功能发布的版本。说你使用 Ansible 1.9,但使用了来自 Ansible 2.4 的模块,这只会让用户感到沮丧。

  • platforms: 这个支持的操作系统和版本列表在显示角色信息时使用,它将在用户选择使用你的角色时发挥作用。确保这是准确的,因为我们不想让用户感到沮丧。

  • galaxy_tags: 这些标签被 Ansible Galaxy 用来帮助识别你的角色做了什么。

在我们发布它之前,还有一个角色的最后部分需要看一看:README.md文件。

README

我们需要完成的角色的最后部分是README.md文件;这个文件包含了在 Ansible Galaxy 网站上显示的信息。当我们使用ansible-galaxy初始化我们的角色时,它创建了一个带有基本结构的README.md文件。我们的角色的文件看起来像下面这样:

Ansible Docker Role
=========
This role installs the current Edge build Docker CE using the official repo, for more information on Docker CE see the official site at [`www.docker.com/community-edition`](https://www.docker.com/community-edition).

Requirements
------------
Apart from requiring root access via `become: yes` this role has no special requirements.

Role Variables
--------------
All of the variables can be found in the `vars` folder.

Dependencies
------------
None.

Example Playbook
----------------
An example playbook can be found below;

  • hosts: docker

gather_facts: true

become: yes

become_method: sudo

roles:

  • russmckendrick.docker

License
-------
BSD

Author Information
------------------
This role is published by [Russ McKendrick](http://russ.mckendrick.io/).

现在我们已经准备好了所有需要的文件,我们可以开始将我们的角色提交到 GitHub,并从那里发布到 Ansible Galaxy。

提交代码并发布

现在我们已经完成了我们的角色,我们需要将其推送到一个公共的 GitHub 存储库。有几个原因需要将其发布到公共存储库,其中最重要的是任何潜在用户都需要下载你的角色。此外,Ansible Galaxy 链接到存储库,允许用户在选择将其作为 playbook 的一部分执行之前审查你的角色。

在所有 GitHub 页面上,当你登录时,右上角有一个+图标;点击它会弹出一个菜单,其中包含创建新存储库和导入存储库的选项,以及 gists 和组织。从菜单中选择 New repository,你将看到一个如下所示的屏幕:

命名存储库并输入描述;重要的是您将您的存储库命名为 ansible-role-your-role-name。在 Ansible Galaxy 中,角色的名称将取决于您在 ansible-role 之后给出的名称,因此,在上一个示例中,我们的角色将被称为 your-role-name,对于我们将要发布的角色,它将被称为 docker

现在我们有了我们的存储库,我们需要为我们的角色添加文件。回到包含您的角色的文件夹,并在命令行上运行以下命令来在本地初始化 Git 存储库。将其推送到 GitHub,确保用您自己存储库的 URL 替换存储库 URL:

$ git init
$ git add -A .
$ git commit -m "first commit"
$ git remote add origin git@github.com:russmckendrick/ansible-role-docker.git
$ git push -u origin master

现在您应该已经上传了文件,您的存储库看起来与以下内容并没有太大不同:

如果您在推送代码时遇到任何问题,我建议您阅读 GitHub 提供的关于设置 Git (help.github.com/articles/set-up-git/) 和推送您的第一个文件 (help.github.com/articles/create-a-repo/) 的出色文档。

现在我们已经上传并可用了文件,我们可以使用我们的 GitHub 凭据登录到 Ansible Galaxy,然后导入我们的角色。转到 Ansible Galaxy 主页 galaxy.ansible.com/,然后单击“使用 GitHub 登录”链接;这将带您到 GitHub 并要求您确认您同意让 Ansible Galaxy 访问您的帐户上的信息。按照提示进行,您将返回到 Ansible Galaxy。

单击顶部菜单中的“我的内容”链接将带您到一个页面,您可以从 GitHub 导入内容;如果您没有看到您的存储库列出,请单击搜索框旁边的刷新图标:

当您看到您的存储库列出时,单击角色旁边的开关,就可以了。您的角色现在已导入。单击顶部菜单中的用户名将显示一个下拉列表;从该列表中,选择“我的导入”。这将为您提供导入的日志:

现在您的角色已发布;您可以通过单击顶部的链接查看您的角色,链接上写着 russmckendrick/ansible-role-docker。这将带您到您新添加的角色的 Ansible Galaxy 页面,例如 galaxy.ansible.com/russmckendrick/docker/

如您所见,我们添加的所有元数据都显示在列表中,包括查看从 GitHub 导入的 README 文件的链接,以及指向 GitHub 本身的链接。

测试角色

现在我们有了我们的角色,我们可以测试它。为此,我们将需要一个 playbook、清单和一个要求文件,以及一个 CentOS 和 Ubuntu 服务器。运行以下命令来创建您需要的文件:

$ mkdir docker
$ cd docker
$ touch production requirements.yml site.yml Vagrantfile

清单文件 production 应该如下所示:

centos ansible_host=10.20.30.10.nip.io 
ubuntu ansible_host=10.20.30.20.nip.io ansible_python_interpreter=/usr/bin/python3

[docker]
centos
ubuntu

[docker:vars]
ansible_connection=ssh
ansible_user=vagrant
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

我们的 requirements.yml 文件只包含我们的 Docker 角色:

- src: "russmckendrick.docker"

我们的 playbook,site.yml 文件,应该只调用我们的角色:

---

- hosts: docker
  gather_facts: true
  become: yes
  become_method: sudo

  roles:
    - russmckendrick.docker

最后,Vagrantfile 应该如下所示:

# -*- mode: ruby -*-
# vi: set ft=ruby :

API_VERSION = "2"
DOMAIN = "nip.io"
PRIVATE_KEY = "~/.ssh/id_rsa"
PUBLIC_KEY = '~/.ssh/id_rsa.pub'
CENTOS_IP = '10.20.30.10'
CENTOS_BOX = 'centos/7'
UBUNTU_IP = '10.20.30.20'
UBUNTU_BOX = 'generic/ubuntu1804'

Vagrant.configure(API_VERSION) do |config|

  config.vm.define "centos" do |centos|
      centos.vm.box = CENTOS_BOX
      centos.vm.network "private_network", ip: CENTOS_IP
      centos.vm.host_name = CENTOS_IP + '.' + DOMAIN
      centos.ssh.insert_key = false
      centos.ssh.private_key_path = [PRIVATE_KEY, "~/.vagrant.d/insecure_private_key"]
      centos.vm.provision "file", source: PUBLIC_KEY, destination: "~/.ssh/authorized_keys"

      centos.vm.provider "virtualbox" do |v|
        v.memory = "2024"
        v.cpus = "2"
      end

      centos.vm.provider "vmware_fusion" do |v|
        v.vmx["memsize"] = "2024"
        v.vmx["numvcpus"] = "2"
      end
  end

  config.vm.define "ubuntu" do |ubuntu|
      ubuntu.vm.box = UBUNTU_BOX
      ubuntu.vm.network "private_network", ip: UBUNTU_IP
      ubuntu.vm.host_name = UBUNTU_IP + '.' + DOMAIN
      ubuntu.ssh.insert_key = false
      ubuntu.ssh.private_key_path = [PRIVATE_KEY, "~/.vagrant.d/insecure_private_key"]
      ubuntu.vm.provision "file", source: PUBLIC_KEY, destination: "~/.ssh/authorized_keys"

      ubuntu.vm.provider "virtualbox" do |v|
        v.memory = "2024"
        v.cpus = "2"
      end

      ubuntu.vm.provider "vmware_fusion" do |v|
        v.vmx["memsize"] = "2024"
        v.vmx["numvcpus"] = "2"
      end
  end

end

现在我们已经把所有文件放在了正确的位置,我们可以通过运行以下命令来下载我们的角色:

$ ansible-galaxy install -r requirements.yml

如您从以下输出中所见,这将把我们的角色下载到 ~/.ansible/roles/ 文件夹中:

接下来,通过运行以下任一命令来启动两个 Vagrant boxes:

$ vagrant up
$ vagrant up --provider=vmware_fusion

一旦 boxes 运行起来,我们可以通过以下方式运行 playbook:

$ ansible-playbook -i production site.yml 

如您从以下输出中所见,一切都按计划进行,角色在两个 boxes 上都安装了 Docker:

Ansible Galaxy 命令

在我们结束本章之前,让我们快速看一下ansible-galaxy命令的一些其他功能,首先是登录。

登录

可以通过命令行登录到 Ansible Galaxy;你可以通过以下方式实现:

$ ansible-galaxy login

这将要求你的 GitHub 用户名和密码;如果你的 GitHub 账户启用了双因素身份验证,那么这种方法将无法工作。相反,你需要提供个人访问令牌。你可以在以下网址生成个人访问令牌:github.com/settings/tokens/。一旦你有了令牌,你可以使用以下命令,将令牌替换为你自己的:

$ ansible-galaxy login --github-token 0aa7c253044609b98425865wbf6z679a94613bae89 

以下截图显示了上述命令的输出:

个人访问令牌将给任何拥有它的人完全访问你的 GitHub 账户;请将它们安全地存储,并且如果可能的话定期更换。

导入

一旦登录,如果你对角色进行了更改并希望将这些更改导入到 Ansible Galaxy 中,你可以运行以下命令:

$ ansible-galaxy import russmckendrick ansible-role-docker

以下截图显示了上述命令的输出:

我们传递给命令的两个信息是 GitHub 用户名,在我的情况下是russmckendrick,以及我们想要导入的仓库的名称——所以对于我们在上一节中发布的 Docker 角色,我使用的是ansible-role-docker

搜索

你可以使用ansible-galaxy命令搜索角色。例如,运行以下命令目前返回 725 个角色:

$ ansible-galaxy search docker

如果你想按作者搜索角色,可以使用以下命令:

$ ansible-galaxy search --author=russmckendrick docker

从截图中的输出可以看出,这只返回了我们发布的角色:

这很有用,因为你不必在终端和浏览器之间来回切换。

信息

我们要看的最后一个命令是info;这个命令将打印出你提供的任何角色的信息。例如,运行以下命令将为你提供关于我们发布的角色的大量信息:

$ ansible-galaxy info russmckendrick.docker

以下截图显示了上述命令的输出:

正如你所看到的,网站上可以获取的所有信息在命令行上也是可以获取的,这意味着在与 Ansible Galaxy 交互时你可以有选择。

总结

在本章中,我们深入了解了 Ansible Galaxy,包括网站和命令行工具。我相信你会同意,Ansible Galaxy 提供了有价值的社区服务,它允许 Ansible 用户共享常见任务的角色,同时也为用户提供了一种通过发布自己的角色来为 Ansible 社区做出贡献的方式。

但是要小心。在将 Ansible Galaxy 的角色用于生产环境之前,请记得仔细检查代码并阅读错误跟踪器;毕竟,这些角色中的许多需要提升的权限才能成功执行它们的任务。

在下一章,我们将看一些将 Ansible 集成到你的日常工作流程中的方法。

问题

本章只有一个任务。拿出我们之前创建的其他角色之一,使其适用于多个操作系统,并在 Ansible Galaxy 上发布。

进一步阅读

本章开始时使用的两个角色都是由 Jeff Geerling 发布的;你可以在www.jeffgeerling.com/找到更多关于 Jeff 和他的项目的信息。

第十七章:下一步使用 Ansible

在本章中,我们将讨论如何将 Ansible 集成到您的日常工作流程中,从持续集成工具到监控工具和故障排除。我们将讨论以下主题:

  • 如何将 Ansible 与 Slack 等服务集成

  • 您如何可以使用 Ansible 来解决问题

  • 一些真实世界的例子

让我们直接深入研究如何将我们的 playbooks 连接到第三方服务。

与第三方服务集成

尽管您可能是运行 playbooks 的人,但您可能会保留 playbook 运行的日志,或者让您团队的其他成员,甚至其他部门了解 playbook 运行的结果。Ansible 附带了几个核心模块,允许您与第三方服务一起工作,以提供实时通知。

Slack

Slack 已经迅速成为各个 IT 服务部门团队协作服务的首选选择。它不仅通过其应用程序目录支持第三方应用程序,而且还具有强大的 API,您可以使用该 API 将您的工具带入 Slack 提供的聊天室。

我们将在本节中查看示例,完整的 playbook 可以在 GitHub 存储库的Chapter17/slack文件夹中找到。我已经从第九章中的 playbook 中获取了 playbook,构建云网络,在那里我们在 AWS 中创建了一个 VPC,并且我已经改编它以使用slack Ansible 模块。

生成令牌

在我们的 Playbook 中使用 Slack 模块之前,我们需要一个访问令牌来请求一个登录到您的 Slack 工作区;如果您还没有工作区,您可以免费注册一个工作区slack.com/

一旦您登录到您的工作区,无论是使用 Web 客户端还是桌面应用程序,都可以从管理应用选项中选择管理应用选项,如下截图所示:

这将打开您的浏览器,并将您带到您工作区的应用程序目录;从这里,搜索传入 WebHooks,然后点击添加配置。

配置的第一部分是选择您希望传入 Webhook 发布消息的频道。我选择了通用频道——一旦选择,您将被带到一个页面,该页面会给您一个 Webhook URL;确保您记下这个 URL,因为我们很快就会需要它。在页面底部,您可以选择自定义您的 Webhook。

在页面底部的集成设置中,我输入了以下信息:

  • 发布到频道:我将其留在#general

  • Webhook URL:这是为您预填充的;您还可以选择在此重新生成 URL

  • 描述性标签:我在这里输入了Ansible

  • 自定义名称:我也在这里输入了Ansible

  • 自定义图标:我将其保留为原样

填写完前面的细节后,我点击了保存设置按钮;这让我得到了一个传入的 Webhook:

如前所述,我还记录了 Webhook URL;对我来说,它是:

https://hooks.slack.com/services/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

现在我们已经准备好了一切,我们需要在 Slack 端进行配置,以便开始使用 Ansible 向我们的用户发送消息。

Ansible playbook

我只会在这里介绍单个角色任务的更新,这是创建 VPC 的角色。我做的第一个更改是在group_vars/common.yml文件中添加了几行:

---

environment_name: "VPC-Slack"
ec2_region: "eu-west-1"

slack:
  token: "TBCRVDMGA/BBCPTPNH1/tyudQIccviG7gh4JnfeoPScc"
  username: "Ansible"
  icon: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/05/Robot_icon.svg/200px-Robot_icon.svg.png"

正如您所看到的,我添加了以下三个嵌套变量:

  • 令牌:这是从 Webhook URL 中获取的;如您所见,我输入了https://hooks.slack.com/services/后的所有内容

  • 用户名:我们可以通过它来覆盖将发布更新的用户名,我只是将其保留为 Ansible

  • 图标:这是我们发布的一部分将显示的头像

如果您还记得之前的 VPC 角色,您会记得它包含一个使用ec2_vpc_net模块创建 VPC 的单个任务。现在,我们想引入 Slack 通知,并能够向用户提供反馈。因此,首先,让我们发送通知,说我们正在检查 VPC 是否存在:

- name: Send notification message via Slack all options
  slack:
    token: "{{ slack.token }}"
    msg: "Checking for VPC called '{{ environment_name }}'"
    username: "{{ slack.username }}"
    icon_url: "{{ slack.icon }}"
    link_names: 0
    parse: 'full'

从前面的任务中可以看到,我们正在发送一条消息,在我们的情况下,它将读取Checking for VPC called 'VPC-Slack',以及tokenusernameicon。角色中的下一个任务是原始角色中的任务:

- name: ensure that the VPC is present
  ec2_vpc_net:
    region: "{{ ec2_region }}"
    name: "{{ environment_name }}"
    state: present
    cidr_block: "{{ vpc_cidr_block }}"
    resource_tags: { "Name" : "{{ environment_name }}", "Environment" : "{{ environment_name }}" }
  register: vpc_info

现在,可能发生了两种情况:一个名为VPC-Slack的 VPC 已经创建,或者 Ansible 已经收集了关于名为VPC-Slack的现有 VPC 的信息。当我们向用户发送消息时,它应该根据 Ansible 的操作而改变。以下任务发送一条消息,通知我们的用户已经创建了一个新的 VPC:

- name: Send notification message via Slack all options
  slack:
    token: "{{ slack.token }}"
    msg: "VPC called '{{ environment_name }}' created with an ID of '{{ vpc_info.vpc.id }}'"
    username: "{{ slack.username }}"
    icon_url: "{{ slack.icon }}"
    link_names: 0
    parse: 'full'
  when: vpc_info.changed

请注意,只有在我注册的vpc_info变量标记为更改时,我才运行此任务。此外,我将 VPC 的 ID 作为消息的一部分传递。如果vpc_info没有注册任何更改,那么前面的任务将被跳过;而后面的任务将会运行:

- name: Send notification message via Slack all options
  slack:
    token: "{{ slack.token }}"
    msg: "Found a VPC called '{{ environment_name }}' which has an ID of '{{ vpc_info.vpc.id }}'"
    username: "{{ slack.username }}"
    icon_url: "{{ slack.icon }}"
    link_names: 0
    parse: 'full'
  when: vpc_info.changed == false and vpc_info.failed == false

请注意我如何改变措辞,以及它仅在没有更改时才被调用。我浏览了其他角色,添加了使用与前面代码相同逻辑的任务,向 Slack 发送通知;如前所述,您可以在存储库的Chapter17/slack文件夹中找到所有添加。

运行 playbook

运行 playbook 时,请使用以下命令:

$ export AWS_ACCESS_KEY=AKIAI5KECPOTNTTVM3EDA
$ export AWS_SECRET_KEY=Y4B7FFiSWl0Am3VIFc07lgnc/TAtK5+RpxzIGTr
$ ansible-playbook -i production site.yml

我从 Slack 收到了以下通知:

正如您所看到的,许多消息都是关于在 VPC 中创建的服务。立即重新运行 playbook 后,返回以下结果:

这一次,消息是关于查找现有服务并返回 ID。Slack 只是一个服务。现在让我们简要地看一下您可以从 Ansible playbook 与之交互的其他一些服务。

其他服务

Slack 不是 Ansible 可以与之交互的唯一服务;以下是您可能希望在 playbook 中使用的其他一些服务。

Campfire

Campfire 是内置在 Basecamp 中的聊天服务;您可以使用此模块直接从 Ansible 向项目利益相关者发送更新,例如:

- name: Send a message to Campfire
  campfire:
    subscription: "my_subscription"
    token: "my_subscription"
    room: "Demo"
    notify: "loggins"
    msg: "The task has completed and all is well"

Cisco Webex Teams(Cisco Spark)

Cisco Webex Teams,或者正式称为 Cisco Spark,是 Cisco 提供的协作服务,为您的团队提供虚拟会议空间、消息和视频通话。此外,它还具有丰富的 API,可以配置 Ansible 与之交互:

- name: Send a message to Cisco Spark
  cisco_spark:
    recipient_type: "roomId"
    recipient_id: "{{ spark.room_id }}"
    message_type: "markdown"
    personal_token: "{{ spark.token }}"
    message: "The task has **completed** and all is well"

CA Flowdock

CA Flowdock 是一个消息服务,从头开始设计,以与面向开发人员的服务集成,如 GitHub、Bitbucket、Jira、Jenkins 和 Ansible:

- name: Send a message to a Flowdock inbox
  flowdock:
    type: "inbox"
    token: "{{ flowdock.token }}"
    from_address: "{{ flowdock.email }}"
    source: "{{ flowdock.source }}"
    msg: "The task has completed and all is well"
    subject: "Task Success"

Hipchat

Hipchat 是由 Atlassian 提供的群组消息服务;它与 Atlassian 产品系列的其他产品紧密集成:

- name: Send a message to a Hipchat room
  hipchat:
    api: "https://api.hipchat.com/v2/"
    token: "{{ hipchat.token }}"
    room: "{{ hipchat.room }}"
    msg: "The task has completed and all is well"

Mail

这项服务不需要任何介绍;可以配置 Ansible 使用各种配置发送电子邮件。以下示例显示了通过外部 SMTP 服务器发送电子邮件:

- name: Send an email using external mail servers
  mail:
    host: "{{ mail.smtp_host }}"
    port: "{{ mail.smtp_port }}"
    username: "{{ mail.smtp_username }}"
    password: "{{ mail.smtp_password }}"
    to: "Russ McKendrick <russ@mckendrick.io>"
    subject: "Task Success"
    body: "The task has completed and all is well"
  delegate_to: localhost

Mattermost

Mattermost 是专有服务的开源替代品,类似于我们在列表中其他地方介绍的服务(例如 Slack、Cisco Webex Teams 和 Hipchat):

- name: Send a message to a Mattermost channel
  mattermost:
    url: "{{ mattermost.url }}"
    api_key: "{{ mattermost.api_key }}"
    text: "The task has completed and all is well"
    channel: "{{ mattermost.channel }}"
    username: "{{ mattermost.username }}"
    icon_url: "{{ mattermost.icon_url }}"

Say

大多数现代计算机都内置了一定程度的语音合成;使用此模块,您可以让 Ansible 口头通知您 playbook 运行的状态:

- name: Say a message on your Ansible host
  say:
    msg: "The task has completed and all is well"
    voice: "Daniel"
  delegate_to: localhost

ServiceNow

ServiceNow 是 ServiceNow, Inc.提供的企业级 IT 服务管理软件即服务产品。使用snow_record模块,您的 playbook 可以在 ServiceNow 安装中打开事件:

- name: Create an incident in ServiceNow
  snow_record:
    username: "{{ snow.username }}"
    password: "{{ snow.password }}"
    instance: "{{ snow.instance }}"
    state: "present"
    data:
      short_description: "The task has completed and all is well"
      severity: "3"
      priority: "3"
  register: snow_incident

Syslog

如果您从主机发送日志文件,则可能希望将 playbook 运行的结果发送到主机 syslog,以便将其发送到您的中央日志服务:

- name: Send a message to the hosts syslog
  syslogger:
    msg: "The task has completed and all is well"
    priority: "info"
    facility: "daemon"
    log_pid: "true"

Twilio

使用您的 Twilio 帐户直接从您的 Ansible playbook 发送短信消息,如下所示:

- name: Send an SMS message using Twilio
  twilio:
    msg: "The task has completed and all is well"
    account_sid: "{{ twilio.account }}"
    auth_token: "{{ twilio.auth }}"
    from_number: "{{ twilio.from_mumber }}"
    to_number: "+44 7911 123456"
  delegate_to: localhost

第三方服务摘要

我希望您从这本书中得到的一个要点是自动化很棒——它不仅可以节省时间,而且使用我们在上一章中介绍的工具,如 Ansible Tower 和 Ansible AWX,可以让非系统管理员或开发人员从友好的 Web 界面执行他们的 playbook。

我们在本节中涵盖的模块不仅允许您记录结果,还可以在播放过程中自动进行一些清理工作,并让它自己通知您的用户,从而使您的自动化水平提升到一个新的高度。

例如,假设您需要将新配置部署到服务器。您的服务台为您提出更改,以便您在 ServiceNow 安装中执行工作。您的 playbook 可以这样编写,在执行更改之前,它使用fetch模块将配置文件复制到您的 Ansible Controller。然后 playbook 可以使用snow_record模块将现有配置文件的副本附加到更改请求,继续进行更改,然后自动更新更改请求的结果。

您可以在本章中提到的服务的以下 URL 中找到详细信息:

Ansible playbook 调试器

Ansible 内置了调试器。让我们看看如何通过创建一个带有错误的简单 playbook 将其构建到您的 playbook 中。正如我们刚才提到的,我们将编写一个使用say模块的 playbook。playbook 本身如下所示:

---

- hosts: localhost
  gather_facts: false
  debugger: "on_failed"

  vars:
    message: "The task has completed and all is well"
    voice: "Daniel"

  tasks:
    - name: Say a message on your Ansible host
      say:
        msg: "{{ massage }}"
        voice: "{{ voice }}"

有两件事需要指出:第一是错误。正如您所看到的,我们正在定义一个名为message的变量,但是当我们使用它时,我输错了,输入了massage。幸运的是,因为我正在开发 playbook,每当任务失败时,我都指示 Ansible 进入交互式调试器。

调试任务

让我们运行 playbook,看看会发生什么:

$ ansible-playbook playbook.yml

第一个问题是我们没有传递主机清单文件,因此将会收到警告,只有本地主机可用;这没关系,因为我们只想在我们的 Ansible Controller 上运行say模块:

[WARNING]: Unable to parse /etc/ansible/hosts as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'

接下来,Ansible 运行 play 本身;这应该会导致致命错误:

PLAY [localhost] ***********************************************************************************

TASK [Say a message on your Ansible host] **********************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'massage' is undefined\n\nThe error appears to have been in '/Users/russ/Documents/Code/learn-ansible-fundamentals-of-ansible-2x/chapter17/say/playbook.yml': line 12, 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: Say a message on your Ansible host\n ^ here\n"}

通常,playbook 运行将停止,并且您将返回到您的 shell;但是,因为我们已经指示 Ansible 进入交互式调试器,现在我们看到以下提示:

[localhost] TASK: Say a message on your Ansible host (debug)>

从这里开始,我们可以更仔细地研究问题;例如,我们可以通过输入以下命令来查看错误:

p result._result

一旦您按下Enter键,失败任务的结果将返回:

[localhost] TASK: Say a message on your Ansible host (debug)> p result._result
{'failed': True,
 'msg': u"The task includes an option with an undefined variable. The error was: 'massage' is undefined\n\nThe error appears to have been in '/Users/russ/Documents/Code/learn-ansible-fundamentals-of-ansible-2x/chapter17/say/playbook.yml': line 12, 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: Say a message on your Ansible host\n ^ here\n"}
[localhost] TASK: Say a message on your Ansible host (debug)>

通过输入以下内容,让我们更仔细地查看任务中使用的变量:

p task.args

这将返回我们在任务中使用的两个参数:

[localhost] TASK: Say a message on your Ansible host (debug)> p task.args
{u'msg': u'{{ massage }}', u'voice': u'{{ voice }}'}
[localhost] TASK: Say a message on your Ansible host (debug)>

现在,让我们通过以下方式查看任务可用的变量:

p task_vars

您可能已经注意到,我们指示 Ansible 在 playbook 运行中执行 setup 模块;这是为了将可用变量列表保持在最小范围内:

[localhost] TASK: Say a message on your Ansible host (debug)> p task_vars
{'ansible_check_mode': False,
 'ansible_connection': 'local',
 'ansible_current_hosts': [u'localhost'],
 'ansible_diff_mode': False,
 'ansible_facts': {},
 'ansible_failed_hosts': [],
 'ansible_forks': 5,
 'ansible_inventory_sources': [u'/etc/ansible/hosts'],
 'ansible_play_batch': [],
 'ansible_play_hosts': [u'localhost'],
 'ansible_play_hosts_all': [u'localhost'],
 'ansible_playbook_python': '/usr/bin/python',
 'ansible_python_interpreter': '/usr/bin/python',
 'ansible_run_tags': [u'all'],
 'ansible_skip_tags': [],
 'ansible_version': {'full': '2.5.5',
 'major': 2,
 'minor': 5,
 'revision': 5,
 'string': '2.5.5'},
 'environment': [],
 'group_names': [],
 'groups': {'all': [], 'ungrouped': []},
 'hostvars': {},
 'inventory_hostname': u'localhost',
 'inventory_hostname_short': u'localhost',
 u'message': u'The task has completed and all is well',
 'omit': '__omit_place_holder__0529a2749315462e1ae1a0d261987dedea3bfdad',
 'play_hosts': [],
 'playbook_dir': u'/Users/russ/Documents/Code/learn-ansible-fundamentals-of-ansible-2x/chapter17/say',
 u'voice': u'Daniel'}
[localhost] TASK: Say a message on your Ansible host (debug)>

正如您从前面的输出中所看到的,关于我们的 playbook 正在执行的环境有很多信息。在变量列表中,您会注意到其中两个变量以u为前缀:它们是voicemessage。我们可以通过运行以下命令来了解更多信息:

p task_vars['message']
p task_vars['voice']

这将显示变量的内容:

[localhost] TASK: Say a message on your Ansible host (debug)> p task_vars['message']
u'The task has completed and all is well'
[localhost] TASK: Say a message on your Ansible host (debug)> p task_vars['voice']
u'Daniel'
[localhost] TASK: Say a message on your Ansible host (debug)>

我们知道我们正在将一个拼写错误的变量传递给msg参数,因此我们可以即时进行一些更改并继续 playbook 运行。为此,我们将运行以下命令:

task.args['msg'] = '{{ message }}'

这将更新参数以使用正确的变量意思,这样我们可以通过运行以下命令重新运行任务:

redo

这将立即使用正确的参数重新运行任务,并且幸运的话,您应该会听到任务已完成,一切正常

[localhost] TASK: Say a message on your Ansible host (debug)> task.args['msg'] = '{{ message }}'
[localhost] TASK: Say a message on your Ansible host (debug)> redo
changed: [localhost]

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

如您从前面的输出中所看到的,因为我们只有一个任务,playbook 已经完成。如果我们有更多任务,那么它将从离开的地方继续。现在您可以使用正确的拼写更新您的 playbook,并继续您的一天。

此外,如果我们愿意,我们可以输入continuequit来分别继续或停止。

Ansible 调试器摘要

当您正在创建大型 playbook 时,启用 Ansible 调试器是一个非常有用的选项——例如,想象一下,您有一个大约需要运行 20 分钟的 playbook,但是在结束时出现了一个错误,比如在您第一次运行 playbook 后的 15 分钟。

让 Ansible 进入交互式调试器 shell 不仅意味着您可以准确地看到定义和未定义的内容,而且还意味着您不必盲目地对 playbook 进行更改,然后等待另外 15 分钟以查看这些更改是否解决了导致致命错误的问题。

真实世界的例子

在我们结束本章,也结束本书之前,我想给出一些我如何使用 Ansible 和与 Ansible 交互的例子:第一个是使用聊天与 Ansible 交互。

聊天示例

几个月前,我需要设置一个演示来展示自动化工作,但是,我需要能够在我的笔记本电脑或手机上展示演示,这意味着我不能假设我可以访问命令行。

我想出的演示最终使用了 Slack 和其他一些我们在这本书中没有涵盖的工具,即 Hubot 和 Jenkins;在我详细介绍之前,让我们快速看一下演示运行的输出:

如前面的输出所示,我在 Slack 频道中提出了以下问题:

@bot 给我一个 Linux 服务器

然后触发了一个 Ansible playbook 运行,该运行在 AWS 中启动了一个实例,并在 playbook 确认服务器在网络上可用后返回实例的信息。我还配置它通过询问以下内容来删除所有正在运行的实例:

@bot 终止所有服务器

如您所见,这运行了另一个 playbook,并且这次,在实例被删除后返回了一个动画 GIF:

那么,我用什么做到这一点呢?正如前面提到的,首先,我使用了 Hubot。Hubot 是 GitHub 开发的一个开源可扩展的聊天机器人。它是在我的 Slack 频道中使用hubot-slack插件进行配置的,并且它会监听任何给定的命令。

我使用hubot-alias插件来定义一个别名,将@bot 给我一个 Linux 服务器翻译成build awslaunch OS=linux;这使用了hubot-yardmaster插件来触发我的 Jenkins 安装中的构建。

Jenkins 是一个开源的自动化服务器,主要用于持续集成和持续交付,它也有一个插件架构。使用 Jenkins Ansible 插件和 Jenkins Git 插件,我能够将用于启动 AWS 实例的 playbook 和角色拉到我的 Jenkins 服务器上,然后让 Jenkins 为我运行 playbook——playbook 本身与我们在第九章和第十章中讨论的 playbook 并没有太大不同,分别是构建云网络高可用云部署

playbook 中内置了一些逻辑,限制了可以启动的实例数量,随机化了要启动的实例的名称,并从几个选项中显示了一个随机的 GIF 图像——所有这些信息,以及实例和 AMI 的详细信息,都通过 Ansible Slack 模块传递给用户,给人一种 playbook 实际上做了更多事情的印象。

在前面的两个例子中,机器人用户是 Hubot,而 Jenkins 实际上是 playbook 运行的反馈。

自动化部署

另一个例子——我最近与几位开发人员合作,他们需要一种自动将代码部署到开发和分级服务器的方法。使用 Docker、GitHub、Jenkins 和 Ansible AWX 的组合,我能够为开发人员提供一个工作流程,每当他们将代码推送到 GitHub 存储库的开发或分级分支时都会触发。

为了实现这一点,我在他们自己的 Jenkins 服务器上部署了代码,使用 Ansible 在容器中部署了 Jenkins,并在同一台服务器上使用 Docker 部署了 AWX。然后,使用Jenkins GitHub插件,我将 Jenkins 项目连接到 GitHub 以创建触发构建所需的 Webhooks。然后使用Jenkins Ansible Tower插件,我让 Jenkins 触发 AWX 中的 playbook 运行。

我这样做是因为目前,AWX 与 GitHub Webhooks 的连接并不那么容易,而JenkinsJenkins GitHub插件具有很高的兼容性——我想随着 AWX 的开发速度,这个小问题很快就会得到解决。

AWX 允许您根据角色授予 playbooks 的访问权限,我给了开发经理和运维工程师运行生产 playbook 的权限,开发人员只有只读权限,以便他们可以查看 playbook 运行的结果。

这意味着部署到生产环境也能够自动化,只要有正确权限的人手动触发 playbook 运行。

AWX 允许我们控制谁可以触发部署,这与我们现有的部署策略相吻合,该策略规定开发人员不应该有权限部署他们编写的代码到生产系统。

总结

现在我们不仅结束了这一章,也结束了我们的书。我一直在努力想出一个总结 Ansible 的方法,我在 Ansible 的创建者 Michael DeHaan 的一条推特中找到了答案(twitter.com/laserllama/status/976135074117808129),他在回复一位技术招聘人员时说:

"使用 Ansible 几个月的人和使用 Ansible 三年的人一样好。这是一个故意简单的工具。"

这完美地总结了我的 Ansible 经验,希望也适用于你。一旦掌握了基础知识,就可以很容易地快速进展,开始构建更加复杂的 playbooks,这些 playbooks 不仅可以帮助部署基本的代码和应用程序,还可以帮助部署复杂的云和甚至物理架构。

不仅能够重用自己的角色,而且可以通过 Ansible Galaxy 访问大量社区贡献的角色,意味着你有许多示例或快速起点用于下一个项目。因此,你可以更快地投入工作,比起其他工具可能更快。此外,如果 Ansible 无法做某事,那么很可能有一个可以集成的工具来提供缺失的功能。

回到我们在第一章讨论过的内容,《Ansible 简介》,能够以可重复和可共享的方式以代码定义基础架构和部署,鼓励他人贡献到你的 playbooks 中,这应该是引入 Ansible 到日常工作流程中的最终目标。我希望通过这本书,你已经开始考虑 Ansible 可以帮助你节省时间的日常任务。

更多阅读材料

本章提到的工具的更多信息可以在以下网址找到:

第十八章:评估

第二章,安装和运行 Ansible

  1. 安装 Ansible 使用 pip 的命令是什么?

sudo -H pip install ansible

  1. 真或假:在使用 Homebrew 时,您可以选择要安装或回滚到的确切 Ansible 版本。

  1. 真或假:Windows 子系统在虚拟机中运行。

  1. 列出三个 Vagrant 支持的虚拟化程序。

VirtualBox,VMware 和 Hyper-V

  1. 状态并解释主机清单是什么。

主机清单是一个主机列表,以及用于访问它们的选项,Ansible 将针对它们

  1. 真或假:YAML 文件中的缩进对于它们的执行非常重要,而不仅仅是装饰性的。

第三章,Ansible 命令

  1. 在本章中提供有关主机清单的信息的命令中,哪个是默认与 Ansible 一起提供的?

ansible-inventory命令

  1. 真或假:使用 Ansible Vault 加密字符串的变量文件将与低于 2.4 版本的 Ansible 一起使用。

  1. 您将运行什么命令来获取如何调用yum模块作为任务的示例?

您将使用ansible-doc命令

  1. 解释为什么您希望针对清单中的主机运行单个模块。

如果您想要使用 Ansible 以受控的方式针对多个主机运行临时命令,您将使用单个模块。

第四章,部署 LAMP 堆栈

  1. 您会使用哪个 Ansible 模块来下载和解压缩 zip 文件?

该模块称为unarchive

  1. 真或假:在roles/rolename/default/文件夹中找到的变量会覆盖同一变量的所有其他引用。

  1. 解释如何向我们的 playbook 添加第二个用户?

通过向用户变量添加第二行,例如:{ name: "user2", group: "lamp", state: "present", key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" }

  1. 真或假:您只能从一个任务中调用一个处理程序。

第五章,部署 WordPress

  1. setup模块执行期间收集的哪个事实可以告诉我们的 playbook 目标主机有多少处理器?

事实是ansible_processor_count

  1. 真或假:在lineinfile模块中使用backref可以确保如果正则表达式不匹配,则不会应用任何更改。

  1. 解释为什么我们希望在 playbook 中构建逻辑来检查 WordPress 是否已经安装。

这样我们可以在下次运行 playbook 时跳过下载和安装 WordPress 的任务。

  1. 我们使用哪个模块来定义作为 playbook 运行的一部分的变量?

set_fact模块

  1. 我们传递哪个参数给shell模块,以便在我们选择的目录中执行我们想要运行的命令?

参数是chdir

  1. 真或假:将 MariaDB 绑定到127.0.0.1将允许我们从外部访问它。

第六章,针对多个发行版

  1. 真或假:我们需要仔细检查 playbook 中的每个任务,以便它可以在两个操作系统上运行。

  1. 哪个配置选项允许我们定义 Python 的路径,Ansible 将使用?

选项是ansible_python_interpreter

  1. 解释为什么我们需要对配置并与 PHP-FPM 服务交互的任务进行更改。

配置文件的路径不同,而且在 Ubuntu 上 PHP-FPM 默认在不同的组下运行

  1. 真或假:每个操作系统的软件包名称完全对应。

第七章,核心网络模块

  1. 真或假:您必须在模板中使用with_itemsfor循环。

  1. 哪个字符用于将您的变量分成多行?

您将使用|字符

  1. 真或假:使用 VyOS 模块时,我们不需要在主机清单文件中传递设备的详细信息。

第八章,转向云端

  1. 我们需要安装哪个 Python 模块来支持digital_ocean模块?

该模块称为dopy

  1. 真或假:您应该始终加密诸如 DigitalOcean 个人访问令牌之类的敏感值。

  1. 我们使用哪个过滤器来查找我们需要使用的 SSH 密钥的 ID 来启动我们的 Droplet?

过滤器将是[?name=='Ansible']

  1. 陈述并解释为什么我们在digital_ocean任务中使用了unique_name选项。

确保我们不会在每个 playbook 运行时启动具有相同名称的多个 droplets。

  1. 从另一个 Ansible 主机访问变量的正确语法是什么?

使用hostvars,例如使用{{ hostvars['localhost'].droplet_ip }},这已在 Ansible 控制器上注册。

  1. 真或假:add_server模块用于将我们的 Droplet 添加到主机组。

错误

第九章,构建云网络

  1. 哪两个环境变量被 AWS 模块用来读取您的访问 ID 和秘密?

它们是AWS_ACCESS_KEYAWS_SECRET_KEY

  1. 真或假:每次运行 playbook 时,您都会获得一个新的 VPC。

错误

  1. 陈述并解释为什么我们不费心注册创建子网的结果。

这样我们可以通过我们稍后在 playbook 运行中分配给它们的角色将子网 ID 列表分组在一起

  1. 在定义安全组规则时,使用cidr_ipgroup_id有什么区别?

cidr_ip创建一个规则,将提供的端口锁定到特定 IP 地址,而group_id将端口锁定到您提供的group_id中的所有主机

  1. 真或假:在使用具有group_id定义的规则时,添加安全组的顺序并不重要。

错误

第十章,高可用云部署

  1. 使用gather_facts选项注册的变量的名称是什么,其中包含我们执行 playbook 的日期和时间?

这是ansible_date_time事实

  1. 真或假:Ansible 自动找出需要执行的任务,这意味着我们不必自己定义任何逻辑。

错误

  1. 解释为什么我们必须使用local_action模块。

因为我们不想从我们使用 Ansible 的主机与 AWS API 进行交互; 相反,我们希望所有 AWS API 交互都发生在我们的 Ansible 控制器上

  1. 我们在ansible-playbook命令之前添加哪个命令来记录我们的命令执行花费了多长时间?

time命令

  1. 真或假:在使用自动扩展时,您必须手动启动 EC2 实例。

错误

第十一章,构建 VMware 部署

  1. 您需要在 Ansible 控制器上安装哪个 Python 模块才能与 vSphere 进行交互?

该模块称为 PyVmomi

  1. 真或假:vmware_dns_config只允许您在 ESXi 主机上设置 DNS 解析器。

错误

  1. 列举我们已经介绍的两个可以用于启动虚拟机的模块名称;有三个,但一个已被弃用。

vca_vappvmware_guest模块;已弃用vsphere_guest模块

  1. 我们已经查看的模块中,您将使用哪个模块来确保在进行与 VMware 通过 VMware 交互的任务之前,虚拟机完全可用?

vmware_guest_tools_wait模块

  1. 真或假:可以安排使用 Ansible 更改电源状态。

第十二章,Ansible Windows 模块

  1. 以下两个模块中哪一个可以在 Windows 和 Linux 主机上使用:setup 或 file?

setup模块

  1. 真或假:您可以使用 SSH 访问您的 Windows 目标。

错误

  1. 解释 WinRM 使用的接口类型。

WinRM 使用 SOAP 接口而不是交互式 shell

  1. 您需要安装哪个 Python 模块才能与 macOS 和 Linux 上的 WinRM 进行交互?

pywinrm模块

  1. 真或假:您可以在使用win_chocolatey模块之前单独安装 Chocolatey 的任务。

错误

第十三章,使用 Ansible 和 OpenSCAP 加固您的服务器

  1. >添加到多行变量会产生什么影响?

当 Ansible 将其插入 playbook 运行时,该变量将呈现为单行

  1. 真或假:OpenSCAP 获得了 NIST 的认证。

  1. 为什么我们告诉 Ansible 如果scan命令标记为失败就继续?

因为如果得不到 100%的分数,任务将总是失败

  1. 解释为什么我们对某些角色使用标签。

这样我们就可以在使用--tags标志时运行 playbook 的某些部分

  1. 真或假:我们使用copy命令将 HTML 报告从远程主机复制到 Ansible 控制器。

第十四章,部署 WPScan 和 OWASP ZAP

  1. 为什么我们使用 Docker 而不是直接在我们的 Vagrant box 上安装 WPScan 和 OWASP ZAP?

简化部署过程;部署两个容器比安装两个工具的支持软件堆栈更容易

  1. 真或假:pip默认安装在我们的 Vagrant box 上。

  1. 我们需要安装哪个 Python 模块才能使 Ansible Docker 模块正常工作?

docker模块

第十五章,介绍 Ansible Tower 和 Ansible AWX

  1. 阐述 Ansible Tower 和 Ansible AWX 之间的区别并解释。

Ansible Tower 是由 Red Hat 提供的商业支持的企业级软件。Ansible AWX 是未来版本的 Ansible Tower 的开源上游;它经常更新并按原样提供。

posted @ 2024-05-20 11:58  绝不原创的飞龙  阅读(11)  评论(0编辑  收藏  举报