Ansible-剧本基础知识-全-

Ansible 剧本基础知识(全)

原文:zh.annas-archive.org/md5/F3D5D082C2C7CD8C77793DEE22B4CF30

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

随着云计算、敏捷开发方法学的演进和近年来数据的爆炸性增长,管理大规模基础设施的需求日益增长。DevOps 工具和实践已经成为自动化此类可扩展、动态和复杂基础设施的每个阶段的必要条件。配置管理工具是这种 DevOps 工具集的核心。

Ansible 是一个简单、高效、快速的配置管理、编排和应用部署工具,集所有功能于一身。本书将帮助您熟悉编写 Playbooks,即 Ansible 的自动化语言。本书采用实践方法向您展示如何创建灵活、动态、可重用和数据驱动的角色。然后,它将带您了解 Ansible 的高级功能,如节点发现、集群化、使用保险库保护数据以及管理环境,最后向您展示如何使用 Ansible 来编排多层次基础架构堆栈。

本书内容概述

第一章,为基础设施绘制蓝图,将向您介绍 Playbooks、YAML 等内容。您还将了解 Playbook 的组成部分。

第二章, 使用 Ansible 角色进行模块化,将演示如何使用 Ansible 角色创建可重用的模块化自动化代码,这些角色是自动化的单位。

第三章, 分离代码和数据 – 变量、事实和模板,涵盖了使用模板和变量创建灵活、可定制、数据驱动的角色。您还将学习自动发现变量,即事实。

第四章,引入您的代码 – 自定义命令和脚本,涵盖了如何引入您现有的脚本,并使用 Ansible 调用 Shell 命令。

第五章,控制执行流程 – 条件语句,讨论了 Ansible 提供的控制结构,以改变执行方向。

第六章,迭代控制结构 – 循环,演示了如何使用强大的 with 语句来遍历数组、哈希等内容。

第七章,节点发现和集群化,讨论了拓扑信息的发现,并使用魔术变量和事实缓存创建动态配置。

第八章,使用 Vault 加密数据,讨论了使用 Ansible-vault 在版本控制系统中存储和共享的安全变量。

第九章,“管理环境”,涵盖了使用 Ansible 创建和管理隔离环境以及将自动化代码映射到软件开发工作流程中。

第十章,“使用 Ansible 编排基础设施”,涵盖了 Ansible 的编排功能,如滚动更新、预任务和后任务、标签、在 playbook 中构建测试等。

阅读本书所需材料

本书假设您已经安装了 Ansible,并且对 Linux/Unix 环境和系统操作有很好的了解,并且熟悉使用命令行接口。

本书的目标读者

本书的目标读者是系统或自动化工程师,具有数年管理基础设施各个部分经验,包括操作系统、应用配置和部署。本书也针对任何打算以最短的学习曲线有效自动化管理系统和应用配置的人群。

假设读者对 Ansible 有概念性的了解,已经安装过,并熟悉基本操作,比如创建清单文件和使用 Ansible 运行临时命令。

约定

在本书中,您会发现一些不同类型信息的文本样式。以下是这些样式的一些示例,以及其含义的解释。

文本中的代码单词显示如下:“我们可以通过使用include指令来包含其他上下文。”

代码块设置如下:

---
# site.yml : This is a sitewide playbook
- include: www.yml

任何命令行输入或输出会写成如下形式:

$ ansible-playbook simple_playbook.yml -i customhosts

新术语重要词汇都显示为加粗。例如,屏幕上看到的文本、菜单或对话框中的单词会像这样出现在文本中:“结果变量哈希应包含defaults中的项目以及vars中的覆盖值”。

注意

警告或重要提示会显示为这样的框。

小贴士

贴士和技巧将以这种方式出现。

读者反馈

我们非常欢迎读者的反馈。请告诉我们您对本书的看法——您喜欢或不喜欢的地方。读者的反馈对我们开发您能够充分利用的标题非常重要。

要给我们发送一般性反馈,只需发送电子邮件至<feedback@packtpub.com>,并在标题中提及书名。

如果您对某个专题非常了解,并且有兴趣写作或为书籍做出贡献,请查看我们的作者指南,网址为www.packtpub.com/authors

客户支持

现在您是 Packt 书籍的骄傲拥有者,我们有一些事项可以帮助您充分利用您的购买。

下载示例代码

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

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误还是会发生。如果您在我们的任何一本书中发现错误——可能是文字或代码方面的错误——我们将不胜感激地接受您的报告。通过这样做,您可以帮助其他读者避免困惑,并帮助我们改进后续版本的本书。如果您发现任何勘误,请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,然后输入您的勘误详细信息。一旦您的勘误被验证,您的提交将被接受,并且勘误将被上传到我们的网站上,或者被添加到该书籍的任何现有勘误列表中,位于该标题的勘误部分下面。您可以通过从www.packtpub.com/support选择您的书籍来查看任何现有的勘误。

盗版

互联网上的版权盗版是各种媒体上持续存在的问题。在 Packt,我们非常重视我们的版权和许可的保护。如果您在互联网上发现我们任何形式的作品的非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。

请通过链接copyright@packtpub.com与我们联系,报告涉嫌盗版材料。

我们感谢您帮助保护我们的作者,以及我们为您提供有价值内容的能力。

问题

如果您在阅读本书的任何方面遇到问题,请通过链接questions@packtpub.com与我们联系,我们将尽力解决。

设置学习环境

要最有效地使用本书并检查、运行和编写本书提供的练习中的代码,建立学习环境至关重要。虽然 Ansible 可以与任何类型的节点、虚拟机、云服务器或已安装操作系统和运行 SSH 服务的裸机主机一起使用,但首选模式是使用虚拟机。

在本次会话中,我们将涵盖以下主题:

  • 理解学习环境

  • 理解先决条件

  • 安装和配置虚拟盒和 vagrant

  • 创建虚拟机

  • 安装 Ansible

  • 使用示例代码

理解学习环境

我们假设大多数学习者都希望在本地设置环境,因此建议使用开源和免费的软件 VirtualBox 和 Vagrant,它们支持大多数桌面操作系统,包括 Windows、OSX 和 Linux。

理想的设置包括五台虚拟机,其目的解释如下。您还可以合并一些服务,例如,负载均衡器和 Web 服务器可以是同一主机:

  • 控制器:这是唯一需要安装 Ansible 并充当控制器的主机。这用于从控制器启动ansible-playbook命令。

  • 数据库(Ubuntu):此主机配置有 Ansible 以运行 MySQL 数据库服务,并运行 Linux 的 Ubuntu 发行版。

  • 数据库(CentOS):此主机配置有 Ansible 以运行 MySQL 数据库服务,但它运行的是 Linux 的 CentOS 发行版。这是为了在为 Ansible 编写 MySQL 角色时测试多平台支持而添加的。

  • Web 服务器:此主机配置有 Ansible 以运行 Apache Web 服务器应用程序。

  • 负载均衡器:此主机配置有 haproxy 应用程序,这是一个开源的 HTTP 代理服务。此主机充当负载均衡器,接受 HTTP 请求并将负载分布到可用的 Web 服务器上。

先决条件

有关先决条件、软件和硬件要求以及设置说明的最新说明,请参阅以下 GitHub 存储库:

github.com/schoolofdevops/ansible-playbook-essentials

系统先决条件

适度配置的台式机或笔记本系统应该足以设置学习环境。以下是在软件和硬件上下文中推荐的先决条件:

处理器 2 个核心
内存 2.5 GB 可用内存
磁盘空间 20 GB 的可用空间
操作系统 Windows,OS X(Mac),Linux

基本软件

为了设置学习环境,我们建议使用以下软件:

  • VirtualBox:Oracle 的 virtualbox 是一种桌面虚拟化软件,可免费使用。它适用于各种操作系统,包括 Windows、OS X、Linux、FreeBSD、Solaris 等。它提供了一个 hypervisor 层,并允许在现有基础 OS 的顶部创建和运行虚拟机。本书提供的代码已经在 virtualbox 的 4.3x 版本上进行了测试。但是,任何与 vagrant 版本兼容的 virtualbox 版本都可以使用。

  • Vagrant:这是一个工具,允许用户轻松在大多数虚拟化程序和云平台上创建和共享虚拟环境,包括但不限于 virtualbox。它可以自动化任务,如导入镜像、指定资源(分配给 VM 的内存和 CPU 等)以及设置网络接口、主机名、用户凭据等。由于它提供了一个 Vagrant 文件形式的文本配置,虚拟机可以通过编程方式进行配置,使其易于与其他工具(如 Jenkins)一起使用,以自动化构建和测试流水线。

  • Git for Windows:尽管我们不打算使用 Git,它是一种版本控制软件,但我们使用此软件在 Windows 系统上安装 SSH 实用程序。Vagrant 需要在路径中可用的 SSH 二进制文件。Windows 未打包 SSH 实用程序,而 Git for Windows 是在 Windows 上安装它的最简单方法。还存在其他选择,如 Cygwin

以下表格列出了用于开发提供的代码的软件版本 OS,附有下载链接:

软件 版本 下载链接
VirtualBox 4.3.30 虚拟箱
Vagrant 1.7.3 www.vagrantup.com/downloads.html
Git for Windows 1.9.5 git-scm.com/download/win

建议学习者在继续之前下载、安装并参考相应的文档页面以熟悉这些工具。

创建虚拟机

安装基础软件后,您可以使用 Vagrant 来启动所需的虚拟机。Vagrant 使用一个名为 Vagrantfile 的规范文件,示例如下:

# -*- mode: ruby -*-
# vi: set ft=ruby :
# Sample Vagranfile to setup Learning Environment
# for Ansible Playbook Essentials

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "ansible-ubuntu-1204-i386"
  config.vm.box_url = "https://cloud-images.ubuntu.com/vagrant/precise/current/precise-server-cloudimg-i386-vagrant-disk1.box"
  config.vm.define "control" do |control|
    control.vm.network :private_network, ip: "192.168.61.10"
  end
  config.vm.define "db" do |db|
    db.vm.network :private_network, ip: "192.168.61.11"
  end
  config.vm.define "dbel" do |db|
    db.vm.network :private_network, ip: "192.168.61.14"
    db.vm.box = "opscode_centos-6.5-i386"
    db.vm.box = "http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_centos-6.5_chef-provisionerless.box"
  end
  config.vm.define "www" do |www|
    www.vm.network :private_network, ip: "192.168.61.12"
  end
  config.vm.define "lb" do |lb|
    lb.vm.network :private_network, ip: "192.168.61.13"
  end
end

先前的 Vagrant 文件包含了设置五个虚拟机的规范,如本章开头所述,它们是 controldbdbelwwwlb

建议学习者使用以下说明创建和启动所需的虚拟机以设置学习环境:

  1. 在系统的任何位置为学习环境设置创建一个目录结构,例如 learn/ansible

  2. 将先前提供的 Vagrantfile 文件复制到 learn/ansible 目录。现在目录树应如下所示:

                         learn
                            \_ ansible
                                  \_ Vagrantfile
    

    注意

    Vagrantfile 文件包含了前一部分描述的虚拟机的规格。

  3. 打开终端并进入 learn/ansible 目录。

  4. 启动控制节点并登录到它,步骤如下:

    $ vagrant up control 
    $ vagrant ssh control
    
    
  5. 从单独的终端窗口,在 learn/ansible 目录下,逐个启动剩余的虚拟机,步骤如下:

    $ vagrant up db
    $ vagrant up www
    $ vagrant up lb
    optionally (for centos based mysql configurations)
    $ vagrant up dbel 
    Optionally, to login to to the virtual machines as
    $ vagrant ssh db
    $ vagrant ssh www
    $ vagrant ssh lb
    optionally (for centos based mysql configurations)
    $ vagrant ssh dbel 
    
    

安装 Ansible 到控制节点

一旦虚拟机创建并启动,需要在控制节点上安装 Ansible。由于 Ansible 是无代理的,使用 SSH 传输来管理节点,因此在节点上除了确保 SSH 服务正在运行外,不需要进行额外的设置。要在控制节点上安装 Ansible,请参考以下步骤。这些说明特定适用于 Linux 的 Ubuntu 发行版,因为这是我们在控制节点上使用的操作系统。有关通用的安装说明,请参考以下页面:

docs.ansible.com/intro_installation.html.

步骤如下:

  1. 使用以下命令登录到控制节点:

    # from inside learn/ansible directory 
    $ vagrant ssh control 
    
    
  2. 使用以下命令更新仓库缓存:

    $ sudo apt-get update
    
    
  3. 安装先决软件和仓库:

    # On Ubuntu 14.04 and above 
    $ sudo apt-get install -y software-properties-common
    $ sudo apt-get install -y python-software-properties
    $ sudo apt-add-repository ppa:ansible/ansible
    
    
  4. 添加新仓库后,请更新仓库缓存,步骤如下:

    $ sudo apt-get update

    
    
  5. 使用以下命令安装 Ansible:

    $ sudo apt-get install -y ansible 
    
    
  6. 使用以下命令验证 Ansible:

    $ ansible --version
    [sample output]
    vagrant@vagrant:~$ ansible --version
    ansible 1.9.2
     configured module search path = None
    
    

使用示例代码

本书提供的示例代码按章节号进行划分。以章节号命名的目录包含代码在相应章节结束时的快照。建议学习者独立创建自己的代码,并将示例代码用作参考。此外,如果读者跳过一个或多个章节,他们可以将前一章节的示例代码用作基础。

例如,在使用 Chapter 6 迭代控制结构 - 循环 时,您可以将 Chapter 5 控制执行流程 - 条件 的示例代码用作基础。

提示

下载示例代码

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

第 1. 章 蓝图化您的基础设施

这本书是为那些对 Ansible 有概念性知识并想开始编写 Ansible playbook 自动化常见基础设施任务、编排应用部署和/或管理多个环境配置的人而设计的入门指南。本书采用渐进式方法,从基础知识开始,比如学习 playbook 的解剖学和编写简单角色以创建模块化代码。一旦熟悉了基础知识,您将被介绍如何使用变量和模板添加动态数据,并使用条件和迭代器控制执行流程等基本概念。然后是更高级的主题,比如节点发现、集群化、数据加密和环境管理。最后我们将讨论 Ansible 的编排特性。让我们通过学习 playbooks 来开始成为 Ansible 实践者的旅程吧。

在本章中,我们将学习:

  • Playbook 的解剖学

  • 什么是 plays 以及如何编写主机清单和搜索模式

  • Ansible 模块和“电池内置”方法

熟悉 Ansible

Ansible 是一个简单、灵活且非常强大的工具,它能够帮助您自动化常见的基础设施任务、运行临时命令并部署跨多台机器的多层应用程序。虽然您可以使用 Ansible 同时在多个主机上启动命令,但其真正的力量在于使用 playbooks 管理这些主机。

作为系统工程师,我们通常需要自动化的基础设施包含复杂的多层应用程序。其中每个代表一类服务器,例如负载均衡器、Web 服务器、数据库服务器、缓存应用程序和中间件队列。由于这些应用程序中的许多必须一起工作才能提供服务,所以还涉及拓扑。例如,负载均衡器会连接到 Web 服务器,后者会读写数据库并连接到缓存服务器以获取内存中的对象。大多数情况下,当我们启动这样的应用程序堆栈时,我们需要按照非常具体的顺序配置这些组件。

这里是一个非常常见的三层 Web 应用程序示例,其中包括一个负载均衡器、一个 Web 服务器和一个数据库后端:

熟悉 Ansible

Ansible 允许您将此图表转换为蓝图,定义您的基础设施策略。用于指定此类策略的格式就是 playbook。

示例策略及其应用顺序如下步骤所示:

  1. 在数据库服务器上安装、配置和启动 MySQL 服务。

  2. 安装和配置运行 NginxPHP 绑定的 Web 服务器。

  3. 在 Web 服务器上部署 Wordpress 应用程序,并向 Nginx 添加相应的配置。

  4. 在部署 Wordpress 后在所有 Web 服务器上启动 Nginx 服务。最后,在负载均衡器主机上安装、配置和启动haproxy服务。更新 haproxy 配置以包含之前创建的所有 Web 服务器的主机名。

以下是一个示例 Playbook,将基础设施蓝图转换为 Ansible 可执行的策略:

介绍 Ansible

Plays

一个 Playbook 包含一个或多个 Play,将主机组映射到明确定义的任务。上述示例包含三个 Play,每个 Play 用于配置多层 Web 应用程序中的一个层。Play 也定义了任务配置的顺序。这使我们能够编排多层部署。例如,仅在启动 Web 服务器后配置负载均衡器,或执行两阶段部署,其中第一阶段仅添加这些配置,第二阶段按预期顺序启动服务。

YAML – Playbook 语言

正如你可能已经注意到的,我们之前编写的 Playbook 更像是一个文本配置,而不是一个代码片段。这是因为 Ansible 的创建者选择使用简单、易读且熟悉的 YAML 格式来蓝图基础设施。这增加了 Ansible 的吸引力,因为这个工具的用户无需学习任何特殊的编程语言即可开始使用。Ansible 代码是自解释且自描述的。对 YAML 的快速入门就足以理解基本语法。以下是您需要了解的有关 YAML 的内容,以便开始您的第一个 Playbook:

  • Playbook 的第一行应以 "--- "(三个连字符)开头,表示 YAML 文档的开始。

  • 在 YAML 中,列表以一个连字符和一个空格表示。Playbook 包含一系列 Play;它们用 "- " 表示。每个 Play 都是一个关联数组、字典或映射,具有键值对。

  • 缩进很重要。列表的所有成员应位于相同的缩进级别。

  • 每个 Play 都可以包含用 ":" 分隔的键值对,以表示主机、变量、角色、任务等。

我们的第一个 Playbook

配备了前面解释的基本规则,并假设读者已经对 YAML 基础知识有所了解,我们现在将开始编写我们的第一个 Playbook。我们的问题陈述如下:

  1. 在所有主机上创建一个 devops 用户。该用户应该是 devops 组的一部分。

  2. 安装 "htop" 实用程序。Htop是 top 的改进版本——一个交互式系统进程监视器。

  3. 将 Nginx 仓库添加到 Web 服务器,并将其作为服务启动。

现在,我们将创建我们的第一个 Playbook 并将其保存为 simple_playbook.yml,其中包含以下代码:

---
- hosts: all
  remote_user: vagrant
  sudo: yes
  tasks:

  - group:
      name: devops
      state: present
  - name: create devops user with admin privileges

    user:
      name: devops
      comment: "Devops User"
      uid: 2001
      group: devops
  - name: install htop package
    action: apt name=htop state=present update_cache=yes

- hosts: www
  user: vagrant
  sudo: yes
  tasks:
  - name: add official nginx repository
    apt_repository:
      repo: 'deb http://nginx.org/packages/ubuntu/ lucid nginx'
  - name: install nginx web server and ensure its at the latest version
    apt:
      name: nginx
      state: latest
  - name: start nginx service
    service:
      name: nginx
      state: started

我们的 Playbook 包含两个 Play。每个 Play 包含以下两个重要部分:

  • 要配置什么:我们需要配置一个主机或一组主机来运行剧本。此外,我们需要包含有用的连接信息,例如要连接为哪个用户,是否使用sudo命令等。

  • 要运行什么:这包括要运行的任务的规范,包括要修改的系统组件以及它们应该处于的状态,例如,安装、启动或最新状态。这可以用任务来表示,稍后可以通过角色来表示。

现在让我们简要介绍一下每个。

创建主机清单

即使在我们开始使用 Ansible 编写剧本之前,我们也需要定义一个所有需要配置的主机的清单,并使其可供 Ansible 使用。稍后,我们将开始对此清单中的一些主机运行剧本。如果您有现有的清单,例如 cobbler、LDAP、CMDB 软件或希望从云提供商(如 ec2)那里拉取清单,则可以使用动态清单的概念从 Ansible 那里拉取。

对于基于文本的本地清单,默认位置是/etc/ansible/hosts。然而,对于我们的学习环境,我们将在工作目录中创建一个自定义清单文件customhosts,其内容如下所示。您可以自由创建自己的清单文件:

#customhosts
#inventory configs for my cluster
[db]
192.168.61.11  ansible_ssh_user=vagrant

[www]
www-01.example.com ansible_ssh_user=ubuntu
www-02 ansible_ssh_user=ubuntu

[lb]
lb0.example.com

现在,当我们的剧本将一场戏映射到该组时,wwwhosts: www),该组中的主机将被配置。 all关键字将匹配清单中的所有主机。

以下是创建清单文件的指南:

  • 清单文件遵循 INI 风格的配置,基本上包含以包含在“[ ]”中的主机组/类名开头的配置块。这允许选择性地在系统类别上执行操作,例如,[namenodes]

  • 单个主机可以是多个组的一部分。在这种情况下,来自两个组的主机变量将被合并,并且优先规则适用。稍后我们将详细讨论变量和优先级。

  • 每个组包含主机列表和连接详细信息,例如要连接的 SSH 用户、如果不是默认值的 SSH 端口号、SSH 凭据/密钥、sudo 凭据等。主机名还可以包含通配符、范围等,以便轻松地包含相同类型的多个主机,这些主机遵循一些命名模式。

提示

创建主机清单后,最好使用 Ansible 的 ping 模块进行连接性验证(例如,ansible -m ping all)。

模式

在上一个剧本中,以下行决定了选择哪些主机来运行特定的剧本:

- hosts: all
- hosts: www

第一行代码将匹配所有主机,而第二行代码将匹配属于www组的主机。

模式可以是以下任何一个或它们的组合:

模式类型 示例
组名 namenodes
匹配全部 all*
范围 namenode[0:100]
主机名/主机名模式 *.example.comhost01.example.com
排除 namenodes:!secondaynamenodes
交集 namenodes:&zookeeper
正则表达式 ~(nn&#124;zk).*\.example\.org

任务

Plays 将主机映射到任务。任务是针对与播放中指定的模式匹配的一组主机执行的操作序列。每个播放通常包含在匹配模式的每台机器上串行运行的多个任务。例如,看下面的代码片段:

- group:
 name:devops
 state: present
- name: create devops user with admin privileges
 user:
 name: devops
 comment: "Devops User"
 uid: 2001
 group: devops

在上述示例中,我们有两个任务。第一个是创建一个组,第二个是创建一个用户并将其添加到之前创建的组中。如果你注意到,第二个任务中有一行额外的内容,以 name: 开头。在编写任务时,最好提供一个名称,描述这个任务将实现什么。如果没有,将打印动作字符串。

任务列表中的每个操作都可以通过指定以下内容来声明:

  • 模块的名称

  • 可选地,管理的系统组件的状态

  • 可选参数

提示

使用更新的 Ansible 版本(从 0.8 开始),现在写入一个动作关键字是可选的。我们可以直接提供模块的名称。因此,这两行将具有相似的动作,即。使用 apt 模块安装软件包:

action: apt name=htop state=present update_cache=yes
apt: name=nginx state=latest

Ansible 以其一体化的方法脱颖而出,与其他配置管理工具不同。这些“电池”即为“模块”。在继续之前了解模块的含义是很重要的。

模块

模块是负责在特定平台上管理特定系统组件的封装程序。

考虑以下示例:

  • apt 模块用于 Debian,而 yum 模块用于 RedHat,有助于管理系统包

  • user 模块负责在系统上添加、删除或修改用户

  • service 模块将启动/停止系统服务

模块将实际的实现与用户抽象出来。它们公开了一个声明性语法,接受一系列参数和要管理的系统组件的状态。所有这些都可以使用人类可读的 YAML 语法,使用键-值对来声明。

在功能上,对于熟悉 Chef/Puppet 软件的人来说,模块类似于提供程序。与编写创建用户的流程不同,使用 Ansible,我们声明我们的组件应处于哪种状态,即应创建哪个用户,其状态及其特征,如 UID、组、shell 等。实际的过程是通过模块隐含地为 Ansible 所知,并在后台执行。

提示

CommandShell 模块是特殊的模块。它们既不接受键-值对作为参数,也不是幂等的。

Ansible 预先安装了一系列模块库,从管理基本系统资源的模块到发送通知、执行云集成等更复杂的模块。如果您想要在远程 PostgreSQL 服务器上配置 ec2 实例、创建数据库,并在 IRC 上接收通知,那么 Ansible 就有一个模块可供使用。这难道不是令人惊讶的吗?

无需担心找外部插件,或者努力与云提供商集成等。要查看可用模块的列表,您可以参考 Ansible 文档中的 docs.ansible.com/list_of_all_modules.html

Ansible 也是可扩展的。如果找不到适合您的模块,编写一个模块很容易,而且不一定要用 Python。模块可以用您选择的语言为 Ansible 编写。这在 docs.ansible.com/developing_modules.html 中有详细讨论。

模块和幂等性

幂等性是模块的一个重要特征。它可以多次应用于系统,并返回确定性的结果。它具有内置的智能。例如,我们有一个使用 apt 模块安装 Nginx 并确保其为最新版本的任务。如果多次运行它,会发生以下情况:

  • 每次运行幂等性时,apt 模块都会比较 playbook 中声明的内容与系统上该软件包的当前状态。第一次运行时,Ansible 将确定 Nginx 未安装,并继续安装。

  • 对于每次后续运行,它都会跳过安装部分,除非在上游仓库中有新版本的软件包可用。

这允许多次执行相同的任务而不会导致错误状态。大多数 Ansible 模块都是幂等的,除了 command 和 shell 模块。用户需要使这些模块幂等。

运行 playbook

Ansible 配备了 ansible-playbook 命令来启动 playbook。现在让我们运行我们创建的 plays:

$ ansible-playbook simple_playbook.yml -i customhosts

运行上述命令时会发生以下情况:

  • ansible-playbook 参数是一个命令,它将 playbook 作为参数(simple_playbook.yml)并对主机运行 plays。

  • simple_playbook 参数包含我们创建的两个 plays:一个用于常规任务,另一个用于安装 Nginx。

  • customhosts 参数是我们主机清单,它让 Ansible 知道要针对哪些主机或主机组执行 plays。

启动上述命令将开始调用 plays,在 playbook 中描述的顺序中进行编排。以下是上述命令的输出:

运行 playbook

现在让我们分析发生了什么:

  • Ansible 读取指定为ansible-playbook命令参数的 playbooks,并开始按顺序执行 play。

  • 我们声明的第一个 play 针对"all"主机运行。all关键字是一种特殊模式,将匹配所有主机(类似于*)。因此,第一个 play 中的任务将在我们作为参数传递的清单中的所有主机上执行。

  • 在运行任何任务之前,Ansible 将收集有关将要配置的系统的信息。这些信息以事实的形式收集。

  • 第一个 play 包括创建devops组和用户,并安装 htop 包。由于我们的清单中有三个主机,所以每个主机被打印一行,这表明被管理实体的状态是否发生了变化。如果状态没有更改,将打印“ok”。

  • 然后 Ansible 转移到下一个 play。这只在一个主机上执行,因为我们在 play 中指定了"hosts:www",而我们的清单中只包含一个位于组"www"中的单个主机。

  • 在第二个 play 期间,添加了 Nginx 存储库,安装了该软件包,并启动了该服务。

  • 最后,Ansible 会在"PLAY RECAP"部分打印播放概要。它指示进行了多少修改,如果其中任何主机无法访问,或者在任何系统上执行失败。

小贴士

如果主机无响应或无法运行任务怎么办?Ansible 具有内置智能,将识别此类问题并将失败的主机从轮换中移出。这不会影响其他主机的执行。

复习问题

你认为你已经充分理解了这一章吗?试着回答以下问题来测试你的理解:

  1. 当涉及到模块时,幂等性是什么?

  2. 主机清单是什么,为什么需要它?

  3. Playbooks 将 ___ 映射到 ___(填空)

  4. 在选择要对其运行 plays 的主机列表时,可以使用哪些类型的模式?

  5. 实际执行特定平台上操作的程序在哪里定义?

  6. 为什么说 Ansible 自带电池?

总结

在本章中,您了解了 Ansible playbooks 是什么,它们由哪些组件组成,以及如何使用它来为基础设施提供蓝图。我们还对 YAML 进行了简要介绍——用于创建 plays 的语言。您了解了 plays 如何将任务映射到主机,如何创建主机清单,如何使用模式过滤主机以及如何使用模块在我们的系统上执行操作。然后,我们创建了一个简单的 playbook 作为概念验证。

在即将到来的章节中,我们将开始重构我们的代码,创建可重用和模块化的代码块,并称之为角色。

第二章。使用 Ansible 角色进行模块化

在上一章中,你学习了使用 Ansible 编写简单 playbook。你还了解了将主机映射到任务的 plays 概念。在单个 playbook 中编写任务对于非常简单的设置可能效果很好。然而,如果我们有多个跨越多个主机的应用程序,这将很快变得难以管理。

在本章中,你将会接触到以下概念:

  • 什么是角色,角色用于什么?

  • 如何创建角色以提供抽象化?

  • 组织内容以提供模块化

  • 使用包含语句

  • 编写简单任务和处理程序

  • 使用 Ansible 模块安装包、管理服务和提供文件

理解角色

在现实生活中的场景中,我们大多数时间都会配置 web 服务器、数据库服务器、负载均衡器、中间件队列等等。如果你退一步看一下大局,你会意识到你正在以可重复的方式配置一组相同的服务器。

为了以最有效的方式管理这样的基础设施,我们需要一些抽象化的方法,使我们能够定义每个组中需要配置的内容,并通过名称进行调用。这正是角色所做的。Ansible 角色允许我们同时配置多个节点组,而不需要重复自己。角色还提供了一种创建模块化代码的方法,然后可以共享和重用。

命名角色

一个常见的做法是创建映射到你想要配置的基础设施的每个应用程序或组件的角色。例如:

  • Nginx

  • MySQL

  • MongoDB

  • Tomcat

角色的目录布局

角色只不过是以特定方式布局的目录。角色遵循预定义的目录布局约定,并期望每个组件都在为其准备的路径中。

以下是一个名为 Nginx 的角色示例:

角色的目录布局

现在让我们看看游戏规则以及前面图表中的每个组件的作用:

  • 每个角色都包含一个以自己命名的目录,例如,Nginx,其父目录为roles/。每个命名角色目录包含一个或多个可选的子目录。最常见的子目录通常包含 taskstemplateshandlers。每个子目录通常包含 main.yml 文件,这是一个默认文件。

  • 任务包含核心逻辑,例如,它们将具有安装包、启动服务、管理文件等代码规范。如果我们将角色比作电影,那么任务将是主角。

  • 任务本身无法完成所有工作。考虑我们与电影的类比,缺少支持角色是不完整的。主角有朋友、车辆、爱人和反派分子,来完成故事。同样,任务消耗数据,调用静态或动态文件,触发动作等。这就是文件、处理程序、模板、默认值和vars发挥作用的地方。让我们看看这些是什么。

  • Vars和默认值提供了关于您的应用程序/角色的数据,例如,您的服务器应该运行在哪个端口,存储应用程序数据的路径,应该以哪个用户身份运行服务等等。默认变量是在 1.3 版本中引入的,这些可以为我们提供合理的默认值。这些稍后可以被其他地方覆盖,例如,varsgroup_varshost_vars。变量被合并,并且存在优先规则。这给了我们很大的灵活性来有选择性地配置我们的服务器。例如,在除了在暂存环境中应该在端口8080上运行之外的所有主机上运行 web 服务器,在端口80上。

  • 文件和模板子目录提供了管理文件的选项。通常,文件子目录用于将静态文件复制到目的主机,例如,一些应用程序安装程序将静态文本文件存档,等等。除了静态文件,你可能经常需要管理动态生成的文件。例如,具有参数(例如端口、用户和内存)的配置文件,可以使用变量动态提供。生成这些文件需要一种称为模板的特殊类型的原始。

  • 任务可以根据状态或条件的更改触发动作。在电影中,主角可以追逐反派,基于挑衅或事件进行报复。一个例子是绑架主角的爱人这个事件。同样地,您可能需要根据之前发生的事情在您的主机上执行一个动作,例如,重新启动一个服务,这可能是由于配置文件状态的更改。可以使用处理程序指定此触发-动作关系。

继续我们的类比,许多热门电影有续集,有时甚至有前传。在这种情况下,应该按照特定的顺序观看,因为续集的故事情节取决于发生在以前的电影中的事情。同样地,一个角色可以依赖于另一个角色。一个非常常见的例子是,在安装 Tomcat 之前,系统上应该存在 Java。这些依赖关系定义在一个角色的 meta 子目录中。

让我们通过为 Nginx 应用程序创建一个角色来动手实践这个。让我们提出一个问题陈述,尝试解决它,并在过程中了解角色。

考虑以下情景。随着足球世界杯的开始,我们需要创建一个 Web 服务器来提供有关体育新闻的页面。

作为敏捷方法的追随者,我们将分阶段进行。在第一阶段,我们将只安装一个 Web 服务器并提供一个主页。现在让我们将此分解为实现此目标所需的步骤:

  1. 安装一个 Web 服务器。在这种情况下,我们将使用'Nginx',因为它是一个轻量级的 Web 服务器。

  2. 管理 Nginx Web 服务器的配置。

  3. 安装完成后启动 Web 服务器。

  4. 复制一个 HTML 文件,它将作为主页提供。

现在我们已经确定了要采取的步骤,我们还需要将它们映射到我们将用于实现每个步骤的相应模块类型:

  • 安装 Nginx = 包模块(apt)

  • 配置 Nginx = 文件模块(file)

  • 启动 Nginx = 系统模块(service)

  • 提供网页 = 文件模块(file)

在我们开始编写代码之前,我们将先创建一个布局来组织我们的文件。

创建站点范围的播放,嵌套和使用 include 语句

作为最佳实践,我们将创建一个顶级文件,其中将包含我们完整基础设施的蓝图。从技术上讲,我们可以将所有需要配置的内容都包含在一个文件中。但是,这会有两个问题:

  • 随着我们开始向这个单一文件添加任务、变量和处理程序,它会很快失控。维护这样的代码将是一场噩梦。

  • 这也将难以重用和共享这样的代码。使用 Ansible 等工具的优点之一是它能够将数据与代码分离。数据是组织特定的,而代码是通用的。然后,可以与其他人共享此通用代码。但是,如果您将所有内容都写在一个文件中,这将是不可能的。

为了避免这个问题,我们将以模块化的方式开始组织我们的代码,如下所示:

  • 我们将为需要配置的每个应用程序创建角色。在这种情况下,它是 Nginx

  • 我们的 Web 服务器可能需要安装除了 Nginx 之外的多个应用程序,例如 PHP 和 OpenSSL。为了封装所有这些内容,我们将创建一个名为www.yml的播放。

  • 我们创建的前置播放将主机与 Nginx 角色进行映射。我们以后可能会添加更多角色。

  • 我们将把这个播放添加到顶层播放,即site.yml

以下图表简要描述了前面的步骤:

创建站点范围的播放,嵌套和使用 include 语句

这是我们的site.yml文件:

---
# site.yml : This is a sitewide playbook
- include: www.yml

前面的 include 指令帮助我们模块化代码。我们不是将所有内容都写在一个文件中,而是拆分逻辑并导入所需的内容。在这种情况下,我们将包含另一个播放,称为嵌套播放

以下是一些关于可以包含和如何包含的指南:

  • include指令可用于包含任务、处理程序,甚至其他播放

  • 如果您在另一个文件中包含一个播放,就像我们在site.yml文件中所做的那样,您不能替换变量

  • include 关键字可与常规任务/处理程序规格结合使用。

  • 可以在 include 语句中传递参数。这被称为参数化 include

提示

Roles 和自动包括

Roles 具有隐式规则来自动包括文件。只要遵循目录布局约定,您可以确保所有任务、处理程序以及其他文件都会自动包含。因此,创建与 Ansible 指定的完全相同名称的子目录非常重要。

创建 www playbook

我们创建了一个网站范围的 playbook,并使用 include 语句来调用另一个名为www.yml的 playbook。我们现在将创建这个文件,其中包含一个 play,将我们的 web 服务器主机映射到 Nginx role:

---
#www.yml : playbook for web servers
- hosts: www
  remote_user: vagrant
  sudo: yes
  roles:
     - nginx

以上代码的工作方式如下:

  • 在任何映射到 hosts 文件中指定的[www]组的主机上运行此代码。

  • 对于roles/nginx/*文件内的每个目录,将roles/nginx/*/main.yml包含到 play 中。这包括taskshandlersvarsmetadefault等等。这就是自动包括规则适用的地方。

默认和自定义 role 路径

默认情况下,Ansible 会查找我们为其创建 playbooks 的项目的子目录roles/。作为一流的 devops 工程师,我们将遵循最佳实践,建立一个集中的、版本受控的仓库,用于存储您的所有 role。我们可能最终会重用 community 创建的 roles。这样做后,我们可以在多个项目中重用这些 roles。在这种情况下,我们将在一个或多个位置检出代码,例如:

  • /deploy/ansible/roles

  • /deploy/ansible/community/roles

对于非默认路径,我们需要在 ansible.cfg 中添加roles_path参数,如下命令所示:

roles_path = /deploy/ansible/roles:/deploy/ansible/community/roles

参数化 roles

有时,我们可能需要在 role 的 vars 或 default 目录中覆盖默认参数,例如,在端口 8080 上运行 web 服务器而不是 80。在这种情况下,我们也可以在前面的 playbook 中传递参数给 roles,如下所示:

---
#www.yml : playbook for web servers
- hosts: www
  roles:
- { role: nginx, port: 8080 }

创建一个基本 role

在上一章中,我们创建了一个简单的 playbook,所有 play 都写在同一个文件中。在发现关于 roles 的新信息后,我们将开始重构我们的代码,使其具有模块化。

重构我们的代码 — 创建一个基本 role

我们在simple_playbook.yml文件中编写了两个 play。我们打算在所有主机上运行第一个 play。该 play 有任务来创建用户,安装必要的软件包,等等:

重构我们的代码 — 创建一个基本 role

将所有这类基本任务组合在一起并创建一个基本 role 是一种良好的实践。您可以将其命名为 base、common、essential 或任何您喜欢的名称,但概念是相同的。我们现在将此代码移至 base role 中:

  1. 为基本 role 创建目录布局。由于我们只会指定任务,所以我们只需要在 base 内创建一个子目录:

    $ mkdir -p roles/base/tasks
    
    
  2. roles/base/tasks 目录下创建 main.yml 文件,以指定基本角色的任务。

  3. 编辑 main.yml 文件,并添加以下代码:

    ---
    # essential tasks. should run on all nodes
     - name: creating devops group
       group: name=devops state=present
     - name: create devops user
       user: name=devops comment="Devops User" uid=2001 group=devops
     - name: install htop package
       action: apt name=htop state=present update_cache=yes
    

创建 Nginx 角色

现在我们将为 Nginx 创建一个单独的角色,并将之前在 simple_playbook.yml 文件中编写的代码移动到其中,如下所示:

  1. 为 Nginx 角色创建目录布局:

    $ mkdir roles/nginx
    $ cd roles/nginx
    $ mkdir tasks meta files
    $ cd tasks
    
    
  2. roles/base 目录下创建 install.yml 文件。将与 Nginx 相关的任务移动到其中。它应该如下所示:

    ---
     - name: add official nginx repository
       apt_repository: repo='deb http://nginx.org/packages/ubuntu/ lucid nginx'
     - name: install nginx web server and ensure its at the latest version
       apt: name=nginx state=latest force=yes
    
  3. 我们还将创建 service.yml 文件来管理 Nginx 守护程序的状态:

    ---
     - name: start nginx service
       service: name=nginx state=started
    
  4. 我们之前看过 include 指令。我们将使用它来在 main.yml 文件中包含 install.ymlservice.yml 文件,如下所示:

    ---
    # This is main tasks file for nginx role
     - include: install.yml
    - include: service.yml
    

提示

最佳实践

为什么我们要创建多个文件来分别保存安装包和管理服务的代码呢?因为精心设计的角色允许您选择性地启用特定的功能。例如,有时您可能想要在多个阶段部署服务。在第一阶段,您可能只想安装和配置应用程序,并在部署的第二阶段才启动服务。在这种情况下,具有模块化任务可以帮助。您始终可以将它们全部包含在 main.yml 文件中。

添加角色依赖关系

在基本角色中指定了一些重要的任务。我们可以不断添加更多任务,这些任务是后续应用程序的先决条件。在这种情况下,我们希望我们的 Nginx 角色依赖于基本角色。现在让我们在 meta 子目录中指定这个依赖关系。让我们看看以下步骤:

  1. roles/nginx/meta/main.yml 路径下创建 main.yml 文件。

  2. meta 目录下的 main.yml 文件中添加以下代码:

    ---
    dependencies:
      - {role: base}
    

上述规范将确保在任何 Nginx 任务开始运行之前始终应用基本角色。

管理 Nginx 的文件

根据我们对情景的解决方案,我们已经有了安装 Nginx 和启动服务的 Ansible 任务。但我们还没有要提供的网页内容,也没有考虑过 Nginx 站点配置。我们难道指望 Nginx 神奇地知道如何以及从哪里提供网页吗?

我们需要执行以下步骤来提供 HTML 页面服务:

  1. 创建一个站点配置,让 Nginx 知道监听请求的端口,并在请求到来时执行什么操作。

  2. 创建一些 HTML 内容,当收到 HTTP 请求时将提供服务。

  3. tasks/main.yml 中添加代码以复制这些文件。

你可能已经注意到,步骤 1 和步骤 2 都要求你在托管 Nginx Web 服务器的主机上创建和管理一些文件。你还了解了角色的文件和子目录。你猜对了。我们将使用这个子目录来托管我们的文件,并将它们复制到所有使用 Ansible 的 Nginx 主机上。所以,现在让我们使用以下命令创建这些文件:

$ cd roles/nginx/files

创建一个default.configuration文件来管理默认的 Nginx 站点配置。这个文件应该包含端口、服务器名称和 Web 根配置等参数,如下所示:

#filename: roles/nginx/files/default.conf
server {
  listen 80;
  server_name localhost;
  location / {
    root /usr/share/nginx/html;
    index index.html;
  }
}

我们还将创建一个index.html文件,将其推送到所有的 Web 服务器上:

#filename: roles/nginx/files/indx.html
<html>
  <body>
    <h1>Ole Ole Ole </h1>
    <p> Welcome to FIFA World Cup News Portal</p>
  </body>
</html>

现在我们已经创建了这些文件,我们将添加任务来将它们复制过去,并放在roles/nginx/tasks/configure.yml中,如下所示:

---
 - name: create default site configurations
   copy: src=default.conf dest=/etc/nginx/conf.d/default.conf mode=0644
 - name: create home page for default site
   copy: src=index.html dest=/usr/share/nginx/html/index.html

我们还将在任务中的main.yaml文件中更新,包括新创建的文件,并在service.yml文件之前添加:

---
# This is the main tasks file for the nginx role
 - include: install.yml
 - include: configure.yml
 - include: service.yml

使用处理程序自动化事件和操作

假设我们在手动管理 Nginx,并且我们需要将 Nginx 监听的端口从默认站点更改为8080。我们需要做什么来实现这一点?当然,我们会编辑default.conf文件,将端口从80更改为8080。但是,这样就足够了吗?这样一编辑文件,Nginx 会立即监听端口8080吗?答案是否定的。还需要一步骤。让我们来看一下下面的截图:

使用处理程序自动化事件和操作

当我们更改配置文件时,通常也会重新启动/重新加载服务,以便读取我们的修改并应用这些修改。

到目前为止,一切都好。现在让我们回到我们的 Ansible 代码。我们将以自动化的方式在大量服务器上运行此代码,可能是数百台服务器。考虑到这一点,我们不可能登录到每个系统上,在每次更改后重新启动服务。这会违反自动化过程的目的。那么,当事件发生时,我们如何要求 Ansible 采取行动呢?这就是处理程序能够帮助的地方。

你已经了解到 Ansible 模块是幂等的。只有在存在配置漂移时,它们才会强制改变状态。在使用 Ansible 进行管理时,我们将在roles/nginx/files下提交之前的端口更改default.conf文件。如果在进行此更改后启动 Ansible 运行,它将在执行过程中比较我们角色中的文件与系统上的文件,检测到配置漂移,并将其复制到更改的文件中。而使用 Ansible,我们将添加一个通知,它将触发一个处理程序运行。在这种情况下,我们将调用处理程序重新启动 Nginx 服务。

现在让我们将这个处理程序添加到roles/nginx/handlers/main.yml中:

---
- name: restart nginx service
  service: name=nginx state=restarted

处理程序与普通任务类似。它们指定了一个模块的名称、实例和状态。为什么我们不将它们与普通任务一起添加呢?好吧,我们只需要在发生事件时执行处理程序,而不是每次运行 Ansible 时都执行。这就是为什么我们为它创建一个单独的部分的确切原因。

现在我们已经写了处理程序,我们还需要为它添加一个触发器。我们将通过在 roles/tasks/nginx/configure.yml 中添加notify指令来实现,如下所示:

使用处理程序自动化事件和操作

小贴士

即使多个任务通知处理程序,处理程序也只会在最后调用一次。这将避免不必要地多次重新启动同一服务。

到目前为止,我们的 Nginx 角色布局看起来更完整,并且具有文件、处理程序、任务和具有管理 Nginx 设置每个阶段的单独任务的目录。角色布局如下:

使用处理程序自动化事件和操作

向 playbook 中添加预先任务和后置任务

我们希望在开始应用 Nginx 之前和之后打印状态消息。让我们使用www.yml playbook,并添加pre_taskspost_tasks参数:

---
- hosts: www
 remote_user: vagrant
 sudo: yes
 pre_tasks:
 - shell: echo 'I":" Beginning to configure web server..'
 roles:
 - nginx
 post_tasks:
 - shell: echo 'I":" Done configuring nginx web server...'

在前面的示例中,我们仅使用echo命令打印了一些消息。但是,我们可以使用 Ansible 提供的任何模块创建任务,这些任务可以在应用角色之前或之后运行。

使用角色运行 playbook

现在让我们将重构后的代码应用到我们的主机上。我们将仅启动站点范围的 playbook,即site.yml文件,然后依赖于包含语句和角色来完成工作:

$ ansible-playbook -i customhosts site.yml

让我们来看看以下的屏幕截图:

使用角色运行 Playbook

除了上次看到的输出之外,这次还有一些新的消息。让我们来分析一下:

  • 在应用角色之前和之后,将触发预先任务和后置任务;这将使用 shell 模块打印消息。

  • 现在我们有了复制到我们的 Nginx Web 服务器的config.html文件的代码。

  • 我们还看到处理程序触发了 Nginx 服务的重新启动。这是由于configuration文件状态的改变,触发了处理程序。

提示

你注意到了吗?即使我们没有在www playbook 中提及基础角色,基础角色中的任务也会被触发。这就是元信息的作用。记得我们在 Nginx 的meta/main.yml中为基础角色指定了一个依赖关系吗?那就是起作用的地方。

依赖关系:

           - { role: base}

复习问题

你认为你是否足够理解了本章?试着回答以下问题来测试你的理解:

  1. 角色包含 ___ 和 ___ 子目录以指定变量/参数。

  2. 如何指定对另一个角色的依赖关系?

  3. 当我们向 play 中添加角色时,为什么不需要使用include指令?任务、处理程序等是如何自动添加到 play 中的?

  4. 如果处理程序与常规任务相似,为什么我们需要一个单独的部分来处理处理程序?

  5. 哪个模块可以用于将静态文件复制到目标主机?

  6. 如何在 playbook 中指定在应用角色之前运行的任务?

摘要

在这一章中,你学会了如何使用角色提供抽象并帮助模块化代码以供重用。这正是社区正在做的事情。创建角色,并与你分享。你还学习了关于include指令、角色的目录布局以及添加角色依赖项。然后我们进行了代码重构,并创建了一个基本角色,即 Nginx 角色。我们还了解了如何管理事件并使用处理程序采取行动。

在下一章中,我们将扩展角色的概念,并开始使用变量和模板添加动态数据。

第三章。代码和数据的分离 - 变量、事实和模板

在上一章中,我们看过如何编写一个角色以提供模块化和抽象化。在这样做的同时,我们创建了配置文件,并使用 Ansible 的复制模块将文件复制到目标主机上。

在本章中,我们将涵盖以下概念:

  • 如何将数据与代码分开?

  • 什么是 Jinja2 模板?它们是如何创建的?

  • 什么是变量?它们是如何以及在哪里使用的?

  • 什么是系统事实?它们是如何被发现的?

  • 不同类型的变量是什么?

  • 什么是变量合并顺序?它的优先规则是什么?

静态内容爆炸

让我们想象我们正在管理跨越多个数据中心的数百个 Web 服务器的集群。由于我们在配置文件中硬编码了server_name参数,因此我们将不得不为每台服务器创建一个文件。这也意味着我们将管理数百个静态文件,这将很快失控。我们的基础架构是动态的,管理变更是 DevOps 工程师日常任务中最常见的方面之一。如果明天,我们公司的政策规定应该在生产环境中运行 Web 服务器的端口为 8080 而不是端口 80,想象一下你要单独更改所有这些文件会有多么头痛。有一个接受动态输入的单个文件,这个输入是特定于它正在运行的主机的,这不是更好吗?这正是模板的作用所在,正如下图所示,一个模板可以替代多个静态文件:

静态内容爆炸

在我们定义模板是什么之前,让我们首先了解如何将代码与数据分开,以及这如何帮助我们解决静态内容爆炸的问题。

分离代码和数据

基础架构即代码工具(例如 Ansible)的真正魔力在于它分离数据和代码的能力。在我们的示例中,default.conf 文件是一个特定于 Nginx Web 服务器的配置文件。配置参数,例如端口、用户、路径等,在任何时候都保持通用和恒定,无论是谁安装和配置它们。不恒定的是这些参数的值。这是我们组织特有的。因此,对于这一点,我们将决定以下事项:

  • Nginx 应该在哪个端口运行?

  • 哪个用户应该拥有 Web 服务器进程?

  • 日志文件应该放在哪里?

  • 应该运行多少个工作进程?

我们组织特定的策略也可能要求我们根据主机所在的环境或地理位置传递不同的值给这些参数。

Ansible 将这些分成两部分:

  • 泛型代码

  • 对组织特定的数据

这有两个优点;一个优点是解决了我们的静态数据爆炸问题。现在我们已经将代码和数据分开,我们可以灵活和动态地创建config文件。第二个优点,你可能会意识到,现在代码和数据被分开了,代码中没有任何特定于特定组织的内容。这使得与任何发现它有用的人分享网站变得容易。这正是您在 Ansible-Galaxy 或者甚至在 GitHub 上找到的东西,推动了像 Ansible 这样的工具的增长。与其重新发明轮子,您可以下载别人编写的代码,自定义它,填写与代码相关的数据,然后完成工作。

现在,这段代码与数据如何分离呢?答案是 Ansible 有两种原始:

  • Jinja 模板(代码)

  • 变量(数据)

以下图解释了如何从模板和变量生成结果文件:

分离代码和数据

模板提供参数值的占位符,这些占位符然后由变量定义。变量可以来自各种地方,包括角色、剧本、清单,甚至是在启动 Ansible 时从命令行输入。现在让我们详细了解模板和变量。

Jinja2 模板

Jinja 是什么? Jinja2 是一个非常流行和强大的基于 Python 的模板引擎。由于 Ansible 是用 Python 编写的,所以它成为大多数用户的默认选择,就像其他基于 Python 的配置管理系统,例如 FabricSaltStack 一样。Jinja 的名称源自日语单词“寺庙”,与“模板”的音标相似。

Jinja2 的一些重要特性包括:

  • 它快速并且使用 Python 字节码即时编译

  • 它有一个可选的沙盒环境

  • 它易于调试

  • 它支持模板继承

模板的形成

模板看起来非常类似于普通的基于文本的文件,除了偶尔出现的变量或者围绕特殊标签的代码。这些会在运行时被评估,大多数情况下被值替换,从而创建一个文本文件,然后被复制到目标主机。以下是 Jinja2 模板接受的两种类型的标签:

  • {{ }} 将变量嵌入到模板中并在生成的文件中打印其值。这是模板的最常见用法。

    例如:

        {{ nginx_port }}
    
  • {% %} 将代码语句嵌入到模板中,例如,用于循环的 if-else 语句,这些语句在运行时被评估但不会被打印。

事实和变量

现在我们已经看过了 Jinja2 模板提供的代码,让我们来理解数据来自何处,然后在运行时嵌入到模板中。数据可以来自事实或变量。当涉及到 Jinja2 模板时,相同的规则适用于事实和变量的使用。事实是一种变量;这里的区别因素是两者的来源。事实在运行时自动可用并发现,而变量是用户定义的。

自动变量 - 事实

我们系统中的许多数据是在握手过程中由托管主机自动发现和提供给 Ansible 的。这些数据非常有用,告诉我们关于该系统的一切,例如:

  • 主机名、网络接口和 IP 地址

  • 系统架构

  • 操作系统

  • 磁盘驱动器

  • 使用的处理器和内存量

  • 是否是虚拟机;如果是,是虚拟化/云提供商吗?

提示

事实是在 Ansible 运行的最开始收集的。记住输出中的那行说 **GATHERING FACTS ********* 吗?这正是发生这种情况的时候。

您可以通过运行以下命令然后跟一个简短的输出来查找有关任何系统的事实:

$ ansible -i customhosts www -m setup | less
192.168.61.12 | success >> {
  "ansible_facts": {
    "ansible_all_ipv4_addresses": [
      "10.0.2.15",
      "192.168.61.12"
    ],
    "ansible_architecture": "i386",
    "ansible_bios_date": "12/01/2006",
    "ansible_cmdline": {
      "BOOT_IMAGE": "/vmlinuz-3.5.0-23-generic",
      "quiet": true,
      "ro": true,
      "root": "/dev/mapper/vagrant-root"
    },
    "ansible_distribution": "Ubuntu",
    "ansible_distribution_major_version": "12",
    "ansible_distribution_version": "12.04",
    "ansible_domain": "vm",
    "ansible_fqdn": "vagrant.vm",
    "ansible_hostname": "vagrant",
    "ansible_nodename": "vagrant",
    "ansible_os_family": "Debian",
    "ansible_pkg_mgr": "apt",
    "ansible_processor": [
      "GenuineIntel",
      "Intel(R) Core(TM) i5-3210M CPU @ 2.50GHz"
    ],
    "ansible_processor_cores": 1,
    "ansible_processor_count": 2,
    "ansible_processor_threads_per_core": 1,
    "ansible_processor_vcpus": 2,
    "ansible_product_name": "VirtualBox",
  }
}

上述输出是以 Ansible 自己的格式并使用其核心设置模块。类似于设置模块,还有另一个名为 facter 的模块,它发现并显示与 Puppet 发现的格式相同的事实,另一个配置管理系统。以下是如何使用 facter 模块为同一主机发现事实的示例:

$ ansible -i customhosts www -m facter | less

在使用 facter 模块时,您需要注意的一点是,该模块不是核心模块,而是作为额外模块的一部分提供的。额外模块是 Ansible 模块的一个子集,它的使用频率较低,与核心模块相比较不流行。此外,要使用 facter 模块,您需要在目标主机上预安装 "facter" 和 "ruby-json" 包。

用户定义的变量

我们看了自动可用的事实,并且发现的数据量是压倒性的。然而,它并不能为我们提供我们需要的基础设施的每个属性。例如,Ansible 无法发现:

  • 我们想让我们的 Web 服务器监听哪个端口

  • 哪个用户应该拥有一个进程

  • 用户需要创建的系统,以及授权规则

所有这些数据都是外部的系统概要,并由我们,用户,提供。这肯定是用户定义的,但我们应该如何在哪里定义它?这就是我们接下来要看的。

在哪里定义一个变量

变量可以从哪里定义是一个复杂的现象,因为 Ansible 在这方面提供了丰富的选择。这也为用户配置其基础设施的部分提供了很大的灵活性。例如,生产环境中的所有 Linux 主机应该使用本地软件包存储库或分段中的 Web 服务器,并且应该运行在端口8080上。所有这些都不需要更改代码,仅通过数据驱动完成,由变量完成。

以下是 Ansible 接受变量的地方:

  • 角色内的default目录

  • 库存变量

    • 分别在不同目录中定义的host_varsgroup_vars参数

    • 在清单文件中定义的host/group vars参数

  • 剧本和角色参数中的变量

  • 角色内的vars目录和在一个播放中定义的变量

  • 在运行时使用-e选项提供的额外变量

如何定义变量

看完变量定义的位置后,我们将开始看如何在各种地方定义它。

以下是您可以使用的一些简单规则来形成有效的 Ansible 变量:

  • 变量应始终以字母开头

  • 它可以包含:

    • 字母

    • 数字

    • 下划线

让我们看一下下面的表格:

有效变量 无效变量
app_port app-port
userid_5 5userid
logdir log.dir

我们已经看过了优先规则,现在我们知道有多个地方可以定义变量。不考虑优先级水平,所有使用相同的语法来定义变量。

要以键值对格式定义简单变量,请使用var: value,例如:

      nginx_port: 80

字典或哈希可以被定义为 Nginx:

       port: 80
       user: www-data

数组可以被定义为:

    nginx_listners:
      - '127.0.0.1:80'
      - '192.168.4.5:80'

对 Nginx 配置进行模板化

你已经学到了很多关于事实、变量和模板的知识。现在,让我们将我们的 Nginx 角色转换为数据驱动的。我们将开始为我们之前创建的 Nginx 的default.conf文件进行模板化。将文件转换为模板的方法如下:

  1. 创建所需目录以保存角色内的模板和默认变量:

    $ mkdir roles/nginx/templates
    $ mkdir roles/nginx/defaults
    
    
  2. 总是从实际的配置文件开始,即此过程的最终结果,以了解它所需的所有参数。然后,往回工作。例如,我们系统上的default.conf文件的配置如下:

            server {
                     listen       80;
                     server_name  localhost; 
                     location / {
                        root   /usr/share/nginx/html;
                        index  index.html;
                   }
             }
    
  3. 确定您想要动态生成的配置参数,删除这些参数的值,单独记录下来,并用模板变量替换它们:

        Template Snippets:
          listen {{ nginx_port }} ;
          root   {{ nginx_root }};
          index  {{ nginx_index }};
    
        Variables:
          nginx_port: 80
          nginx_root: /usr/share/nginx/html
          nginx_index: index.html
    
  4. 如果任何配置参数的值应该从事实中获取,通常是系统参数或拓扑信息,比如主机名、IP 地址等,则可以使用以下命令找到相关的事实:

    例如:

    $ ansible -i customhosts www -m setup | less
    
    

    要找出系统的主机名:

    $ ansible -i customhosts www -m setup | grep -i hostname
    
      "ansible_hostname": "vagrant",
      "ohai_hostname": "vagrant",
    
  5. 在模板中使用发现的事实,而不是用户定义的变量。例如:

      server_name  {{ ansible_hostname }},
    
  6. 将结果文件保存在模板目录中,最好使用.j2扩展名。例如,对于roles/nginx/templates/default.conf.j2,结果文件如下所示:

    #roles/nginx/templates/default.conf.j2
    server {
        listen       {{ nginx_port }};
        server_name  {{ ansible_hostname }};
    
        location / {
            root   {{ nginx_root }};
            index  {{ nginx_index }};
        }
    }
    
  7. 创建roles/nginx/defaults/main.yml并将默认值存储如下:

    ---
    #file: roles/nginx/defaults/main.yml
    nginx_port: 80
    nginx_root: /usr/share/nginx/html
    nginx_index: index.html
    
  8. 一旦模板创建完成,将configure.yml文件中的任务更改为使用模板而不是复制模块:模板化 Nginx 配置

  9. 最后,到了删除我们之前使用复制模块的静态文件的时候:

    $ rm roles/nginx/files/default.conf
    
    

    然后是运行 Ansible playbook 的时间:

    $ ansible-playbook -i customhosts site.yml
    
    

让我们来看一下以下的屏幕截图:

模板化 Nginx 配置

让我们分析此次运行中发生的情况:

  • 我们将配置任务更改为使用模板而不是复制模块,这在任务显示其更改状态时在屏幕截图中反映出来。

  • 由于任务已更新,会触发通知,该通知调用处理程序以重新启动服务。

我们的 Nginx 角色的代码树在进行此更改后如下所示:

模板化 Nginx 配置

添加另一个层——MySQL 角色。

到目前为止,我们一直关注基础架构的单个层,即 web 服务器层。仅为一个层编写代码并不有趣。作为一个酷炫的 DevOps 团队,我们将创建一个具有数据库、web 服务器和负载均衡器的多层基础架构。接下来,我们将开始创建 MySQL 角色,应用到目前为止学到的所有知识,并扩展这些知识以涵盖一些新概念。

这是我们的 MySQL 角色规范:

  • 它应该安装 MySQL 服务器包。

  • 它应该配置 'my.cnf',这是 MySQL 服务器的主配置。

  • 它应该启动 MySQL 服务器守护进程。

  • 它应该支持 Ubuntu 12.04 以及 CentOS/RedHat Enterprise 6.x。

使用 Ansible-Galaxy 创建角色的脚手架。

到目前为止,我们一直在努力理解和创建角色所需的目录结构。然而,为了让我们的工作更轻松,Ansible 提供了一个叫做Ansible-Galaxy的工具,它可以帮助我们自动创建脚手架并遵循最佳实践。实际上,Ansible-Galaxy 的功能不仅仅是如此。它还是一个连接到galaxy.ansible.com上免费可用的 Ansible 角色仓库的实用工具。这类似于我们使用CPANRubyGems的方式。

让我们首先使用以下命令使用 Ansible-Galaxy 对 MySQL 角色进行初始化:

$ ansible-galaxy init --init-path roles/ mysql

在这里,以下是对前面命令的分析:

  • init:这是传递给 Ansible-Galaxy 的子命令,用于创建脚手架。

  • --init-path-p:这些提供了角色目录路径,在该路径下创建目录结构。

  • mysql:这是角色的名称。

让我们来看一下以下的屏幕截图:

使用 Ansible-Galaxy 创建角色的支架

在使用 Ansible-Galaxy 初始化角色后创建的目录布局如上图所示,它创建了一个空角色,具有适用于 Galaxy 上传的结构。它还初始化了必要的组件,包括任务、处理程序、变量和带有占位符的元文件。

向角色添加元数据

我们之前使用meta文件指定了对另一个角色的依赖关系。除了指定依赖关系外,元文件还可以为角色指定更多数据,例如:

  • 作者和公司信息

  • 支持的操作系统和平台

  • 角色功能的简要描述

  • 支持的 Ansible 版本

  • 这个角色试图自动化的软件类别

  • 许可信息

让我们通过编辑roles/meta/main.yml来更新所有这些数据:

---
galaxy_info:
  author: Gourav Shah
  description: MySQL Database Role
  company: PACKT
  min_ansible_version: 1.4
  platforms:
  - name: EL
    versions:
      - all
  - name: Ubuntu
    versions:
      - all
  categories:
  - database:sql

在上面的片段中,我们向角色添加了元数据,如作者和公司详细信息,角色功能的简要描述,与 Ansible 版本的兼容性,支持的平台,角色所属的类别等等。

在任务和处理程序中使用变量

你已经学会了如何在模板中使用变量。那不是用来定义变量的全部代码。除了模板之外,我们还可以在任务、剧本等中使用变量。这一次,我们还承诺提供一个支持多平台的角色,支持 Ubuntu 和 RedHat。与ChefPuppet不同,Ansible 使用特定于操作系统的模块(例如,aptyum),而不是平台无关的资源(软件包)。我们将不得不创建特定于操作系统的任务文件,并根据它们将它们选择性地调用。我们是这样做的:

  • 我们将找到一个事实,确定操作系统平台/系列。这里我们有几个选项:

    • ansible_distribution

    • ansible_os_family

  • RedHat、CentOS 和 Amazon Linux 都基于rpm,行为类似。Ubuntu 和 Debian 操作系统也是同一平台系列的一部分。因此,我们选择使用ansible_os_family事实,这将为我们提供更广泛的支持。

  • 我们将在角色中的两个地方定义变量:

    • 从适用于 Debian 的默认vars文件中获取合理的默认值。

    • 如果不是 Debian 的特定于os_family的变量。

  • 我们还将创建特定于操作系统的任务文件,因为我们可能需要调用不同的模块(aptyum)和特定于该操作系统的额外任务。

  • 对于处理程序和任务,我们将使用变量提供特定于操作系统的名称(例如,MySQL 与 mysqld,用于服务)。

  • 最后,我们将创建main.yml文件,通过检查这个事实的值来选择性地包含特定于主机的变量以及任务文件。

创建变量

我们将从创建变量开始。让我们在/mysql/defaults/main.yml文件中为 Debian/Ubuntu 设置合理的默认值:

---
#roles/mysql/defaults/main.yml
mysql_user: mysql
mysql_port: 3306
mysql_datadir: /var/lib/mysql
mysql_bind: 127.0.0.1
mysql_pkg: mysql-server
mysql_pid: /var/run/mysqld/mysqld.pid
mysql_socket: /var/run/mysqld/mysqld.sock
mysql_cnfpath: /etc/mysql/my.cnf
mysql_service: mysql

然后它将在 RedHat/CentOS 机器上运行,但是我们需要覆盖一些变量,以配置特定于 RedHat 的参数。

注意事项

请注意,文件名应与 ansible_os_family fact 返回的确切名称(即 RedHat)完全匹配,并正确使用大小写。

我们将创建并编辑 roles/mysql/vars/RedHat.yml 文件,如下所示:

---
# RedHat Specific Configs.
# roles/mysql/vars/RedHat.yml
mysql_socket: /var/lib/mysql/mysql.sock
mysql_cnfpath: /etc/my.cnf
mysql_service: mysqld
mysql_bind: 0.0.0.0

最后,我们将创建 group_vars fact 并提供一个变量来覆盖默认设置。您已经学到了可以在 inventory 文件、group_varshost_vars facts 中指定变量。我们现在将开始使用 group_vars fact。您可以在库存文件中创建这些,也可以创建一个名为 group_vars 的单独目录。我们将采用第二种方法,这是推荐的方法:

# From our top level dir, which also holds site.yml
$ mkdir group_vars
$ touch group_vars/all

编辑 group_vars/all 文件并添加以下行:

mysql_bind: "{{ ansible_eth0.ipv4.address }}"

创建任务

现在是创建任务的时候了。遵循最佳实践,我们将任务分解成多个文件,并使用包括语句,就像我们为 Nginx 所做的那样。现在我们将在 roles/mysql/tasks 内创建默认的 main.yml 文件,如下所示:

---
# This is main tasks file for mysql role
# filename: roles/mysql/tasks/main.yml
# Load vars specific to OS Family. 
- include_vars: "{{ ansible_os_family }}.yml"
  when: ansible_os_family != 'Debian'

- include: install_RedHat.yml
  when: ansible_os_family == 'RedHat'

- include: install_Debian.yml
  when: ansible_os_family == 'Debian'

- include: configure.yml
- include: service.yml

我们早先已经看到了 include 语句。这里新增的内容是使用了 include_vars fact 并使用了 ansible_os_family fact。如果您注意到:

  • 我们使用了 ansible_os_family fact 和 include_vars fact,在不是 Debian 系统的情况下来确定是否包含特定于操作系统的变量。为什么不适用于 Debian 系统?因为我们已在 default 文件中指定了特定于 Debian 的配置。include_vars fact 与前面的条件语句配合得很好。

  • 我们还使用 when 条件调用特定于操作系统的安装脚本。我们目前已包含支持 Debian 和 RedHat 家族的两个脚本。但是,稍后我们可以通过添加更多 install_<os_family>.yml 脚本来扩展脚本,以支持其他平台。

现在,让我们创建适用于 Debian 和 RedHat 的安装任务:

$ vim roles/mysql/tasks/install_Debian.yml

然后如下编辑文件:

---
# filename: roles/mysql/tasks/install_Debian.yml
  - name: install mysql server
    apt:
      name:"{{ mysql_pkg }}"
      update_cache:yes

$ vim roles/mysql/tasks/install_Redhat.yml

运行前面的命令后,将文件编辑如下所示:

---
# filename: roles/mysql/tasks/install_RedHat.yml
- name: install mysql server
   yum:
     name:"{{ mysql_pkg }}"
     update_cache:yes

在上一示例中,我们在基于 Debian 和 RedHat 的系统分别使用了 aptyum 模块。遵循最佳实践,我们将编写数据驱动的角色,使用变量 mysql_pkg 提供软件包名称。该变量根据其运行的平台设置。我们来看看以下步骤:

  1. 下一步是创建用于配置 MySQL 的任务。由于我们知道每个配置文件应该是一个模板,我们将为 my.cnf 文件创建一个模板,即 MySQL 服务器的默认配置文件:

    $ touch roles/mysql/templates/my.cnf.j2
    
    

    然后如下编辑文件:

    # Notice:This file is being managed by Ansible
    # Any manual updates will be overwritten
    # filename: roles/mysql/templates/my.cnf.j2
    [mysqld]
    user = {{ mysql_user | default("mysql") }}
    pid-file	 = {{ mysql_pid }}
    socket = {{ mysql_socket }}
    port = {{ mysql_port }}
    datadir = {{ mysql_datadir }}
    bind-address = {{ mysql_bind }}
    
  2. 我们创建了一个模板,使用了 .j2 扩展名,因为它是 Jinja2 模板。这不是必须的,但建议这样做。

  3. 所有配置参数都来自 {{var}} 格式的变量。这是管理配置文件的推荐做法。我们可以让属性的优先级决定值来自哪里。

提示

为每个由 Ansible 管理的文件添加注意事项是个好习惯。这样可以避免可能的手动更新或临时更改。

我们将编写一个任务来管理这个模板,并将生成的文件复制到主机上的目标路径:

---
# filename: roles/mysql/tasks/configure.yml
 - name: create mysql config
   template: src="img/my.cnf" dest="{{ mysql_cnfpath }}" mode=0644
   notify:
    - restart mysql service

我们有一个通用的配置文件模板;然而,复制这个模板的路径因平台而异,也根据您计划使用的 MySQL 版本不同。在这里,我们使用的是默认情况下包含在 Ubuntu 和 CentOS 仓库中的 MySQL 发行版,并且我们将从角色变量中设置 mysql_cnfpath 路径,如下所示:

  • 在 Ubuntu/Debian 上,使用命令:mysql_cnfpath = /etc/mysql/my.cnf

  • 在 RedHat/CentOS 上,使用命令:mysql_cnfpath = /etc/my.cnf

同时,我们将通知发送给 MySQL 服务重启处理程序。这将确保如果配置文件发生任何更改,服务将自动重新启动。

要管理一个服务,我们将创建一个服务任务和处理程序:

任务:

$ touch roles/mysql/tasks/service.yml

然后按如下所示编辑文件:

---
# filename: roles/mysql/tasks/service.yml
 - name: start mysql server
   service: name="{{ mysql_service }}" state=started

处理程序:

$ touch roles/mysql/handlers/main.yml

运行上述命令后,按如下所示编辑文件:

---
# handlers file for mysql
# filename: roles/mysql/handlers/main.yml
- name: restart mysql service
  service: name="{{ mysql_service }}" state=restarted

在这里,任务和处理程序与 Nginx 服务类似,所以不需要太多描述。唯一的变化是我们使用 mysql_service 变量来决定要启动或重新启动服务的服务名称。

在剧本中使用变量

变量也可以在剧本中指定。这样做的首选方法是将它们作为角色参数传递,示例如下。当角色中有默认值,并且您想要覆盖一些特定于您设置的配置参数时,这通常是有用的。这样,角色仍然是通用的和可共享的,不包含组织特定的数据。

我们将创建一个用于管理数据库的剧本,然后将其包含在全局剧本中,如下所示:

$ touch db.yml

然后按如下所示编辑文件:

---
# Playbook for Database Servers
# filename: db.yml
- hosts: db
  remote_user: vagrant
  sudo: yes
  roles:
    - { role: mysql, mysql_bind: "{{ ansible_eth1.ipv4.address }}" }

在这里,我们假设主机清单包含一个名为 db 的主机组。在我们的示例中,我们有两个运行在 Ubuntu 和 CentOS 上的 db 服务器。这被添加为:

[db]
192.168.61.11 ansible_ssh_user=vagrant ansible_ssh_private_key_file=/vagrant/insecure_private_key
192.168.61.14 ansible_ssh_user=vagrant ansible_ssh_private_key_file=/vagrant/insecure_private_key

在上面的剧本中,我们使用了一个参数化角色,它覆盖了一个变量,即 mysql_bind。该值是从一个多级事实中设置的。

让我们来看一下以下的截图:

在剧本中使用变量

一个多级事实也可以被指定为 ansible_eth1["ipv4"]["address"],两种格式都是有效的。当我们想要创建多个角色实例时,例如运行在不同端口上的虚拟主机和 WordPress 实例,参数化角色也非常有用。

现在让我们使用 include 语句将这个剧本包含在顶级的 site.yml 文件中:

如下编辑 site.yml 文件:

---
# This is a sitewide playbook
# filename: site.yml
- include: www.yml 
- include: db.yml

将 MySQL 角色应用于数据库服务器

我们已准备好配置我们的数据库服务器。让我们继续将新创建的角色应用于我们库存中的所有 db 服务器:

$ ansible-playbook -i customhosts site.yml

以下图像包含仅与数据库 Play 相关的输出片段:

将 MySQL 角色应用于数据库服务器

我们在前几章已经解释了 Ansible 的运行,当我们创建第一个 Playbook 以及应用 Nginx 角色时。这里唯一的新概念是 include_var 部分。Ansible 将根据 ansible_os_family 事实检查我们的条件,并调用特定于操作系统的变量。在我们的情况下,我们每个有一个 Ubuntu 和 CentOS 主机,并且在仅在 CentOS 主机上运行时都调用 RedHat.yml 文件。

这里真正有趣的是要找出在每个平台上我们的配置文件发生了什么以及哪些变量具有优先权。

变量优先级

我们指定了变量默认值,并在库存文件中使用它们,并从不同位置定义了相同的变量(例如,默认值、vars 和库存)。现在让我们分析模板的输出,以了解所有这些变量发生了什么。

以下是显示 Ubuntu 上 my.cnf 文件的图表:

变量优先级

以下是对截图的分析:

  • 文件在注释部分有一条通知。这可以阻止管理员对文件进行手动更改。

  • 大多数变量来自角色中的默认值。这是因为 Debian 是我们默认的操作系统系列,我们已经为其设置了合理的默认值。类似地,对于其他操作系统平台,我们正在从角色的 vars 目录中设置变量默认值。

  • 尽管 bind_address 参数在默认设置和 group_vars 中指定,但它从 Playbook 的角色参数中取值,后者优先于其他两个级别。

以下图表解释了在各个级别定义变量时会发生什么情况。它们都在运行时合并。如果相同的变量在多个位置定义,则会应用优先规则:

变量优先级

要理解优先规则,让我们看看我们的 CentOS 主机上发生了什么。以下是在 CentOS 上创建的 my.cnf 文件:

变量优先级

如前图所示,在 CentOS 的情况下,我们看到一些有趣的结果:

  • userpiddatadirport 的值来自默认设置。我们已经查看了合并顺序。如果变量不相同,则合并它们以创建最终配置。

  • 套接字的值来自 vars,因为那是它唯一被定义的地方。尽管如此,我们希望这个套接字对于基于 RedHat 的系统是恒定的,因此,我们在角色的 vars 目录中指定了它。

  • bind_address参数再次来自 vars 目录。这很有趣,因为我们在以下位置定义了mysql_bind变量:

    • 角色中的默认

    • group_vars

    • playbook

    • 角色中的vars

以下图展示了当我们多次定义相同变量时的优先级规则:

变量优先级

由于我们的角色在vars目录中定义了bind_address参数,它优先于其他。

有一种方法可以使用额外的变量或在运行 Ansible 时使用-e开关来重写角色参数。这是 Ansible 管理的变量的最高优先级。

例如:

ansible-playbook -i customhosts db.yml  -e mysql_bind=127.0.0.1

在前面的启动命令中,我们使用了-e开关,它将覆盖所有其他变量级别,并确保 MySQL 服务器绑定到127.0.0.1

变量使用的最佳实践

觉得压力大了?别担心。我们将为您提供使用变量时的最佳实践建议:

  • 从一个角色中开始使用默认值。这是所有优先级中最低的。这也是提供应用程序的合理默认值的好地方,稍后可以从各种地方覆盖。

  • 组变量非常有用。很多时候,我们会进行特定于区域或环境的配置。我们还会为一组特定服务器应用特定的角色,例如,对于所有亚洲的 web 服务器,我们应用 Nginx 角色。还有一个名为"all"的默认组,其中包含所有组的所有主机。将对所有组通用的变量放在"all"(group_vars/all)中是一个好习惯,然后可以被更具体的组覆盖。

  • 如果有主机特定的异常情况,请使用hosts_vars,例如,host_vars/specialhost.example.org

  • 如果你想要将变量分开存储在不同的文件中,创建以主机名命名的目录,然后将变量文件放在其中。在这些目录中的所有文件都将被评估:

    • group_vars/asia/web

    • host_vars/specialhost/nginx

    • host_vars/specialhost/mysql

  • 如果你想要保持你的角色通用且可共享,在角色中使用默认值,然后从 playbooks 中指定特定于组织的变量。这些可以被指定为角色参数。

  • 如果希望角色变量始终优先于清单变量和 playbooks,请在角色内的vars目录中指定它们。这对于为特定平台提供角色常量非常有用。

  • 最后,如果你想要覆盖之前的任何变量并在运行时提供一些数据,请使用 Ansible 命令使用-e选项提供额外的变量。

到目前为止,我们的 MySQL 角色和 DB playbook 的树应该如下图所示:

变量使用的最佳实践

复习问题

你觉得自己对本章有足够的理解吗?尝试回答以下问题来测试你的理解:

  1. 什么是 Jinja2 模板与静态文件的区别?

  2. 什么是事实?它们是如何被发现的?

  3. 在 Jinja2 模板的上下文中 {{ }}{% %} 有什么区别?

  4. 除了模板之外,您可以在任何地方使用变量吗?如果可以,在哪里?

  5. 如果在角色的 vars 目录中定义了变量 foo,并且在 hosts_var 文件中也定义了相同的变量,那么这两者中哪个优先级更高?

  6. 如何编写支持多个平台的 Ansible 角色?

  7. 您可以在角色中的哪里指定作者和许可信息?

  8. 在启动 Ansible-playbook 命令时如何提供变量?

  9. 你会使用哪个命令来自动创建角色所需的目录结构?

  10. 如何覆盖角色的 vars 目录中指定的变量?

摘要

我们从学习使用 Ansible 变量、事实和 Jinja2 模板将数据与代码分离的原因和方法开始了这一章节。您学会了如何通过在模板、任务、处理程序和 Playbooks 中提供变量和事实来创建数据驱动的角色。此外,我们为数据库层创建了一个新角色,支持 Debian 和 RedHat 系列操作系统。您学会了系统事实是什么以及如何发现和使用它们。您学会了如何从多个位置指定变量、它们是如何合并的以及优先级规则。最后,您学会了使用变量的最佳实践。

在下一章中,我们将使用自定义命令和脚本,了解注册变量是什么,并使用所有这些信息部署一个示例 WordPress 应用程序。

第四章。引入您的代码 - 自定义命令和脚本

Ansible 附带了各种内置模块,允许我们管理各种系统组件,例如用户、软件包、网络、文件和服务。Ansible 的一揽子方法还提供了将这些组件与云平台、数据库和应用程序(如JiraApacheIRCNagios等)集成的能力。然而,有时我们会发现自己处于无法找到完全符合要求的模块的位置。例如,从源代码安装软件包涉及下载、提取源码 tarball,然后是 make 命令,最后是"make install"。没有一个单一的模块来完成这个任务。还会有一些时候,我们希望引入我们已经花费了夜晚时间创建的现有脚本,并让它们与 Ansible 一起被调用或定时执行,例如夜间备份脚本。Ansible 的命令模块将在这种情况下拯救我们。

在本章中,我们将向您介绍:

  • 如何运行自定义命令和脚本

  • Ansible 命令模块:原始、命令、shell 和脚本

  • 如何控制命令模块的幂等性

  • 已注册的变量

  • 如何创建 WordPress 应用程序

命令模块

Ansible 拥有四个属于这一类别的模块,并在运行系统命令或脚本时为我们提供选择。这四个模块是:

  • 原始

  • 命令

  • Shell

  • 脚本

我们将逐个学习这些知识点。

使用原始模块

大多数 Ansible 模块要求目标节点上存在 Python。然而,顾名思义,原始模块提供了一种通过 SSH 与主机通信以执行原始命令而不涉及 Python 的方式。使用这个模块将完全绕过 Ansible 的模块子系统。这在某些特殊情况或情况下会非常有用。例如:

  • 对于运行的 Python 版本早于 2.6 的传统系统,在运行 playbooks 之前,需要安装Python-simplejson包。可以使用原始模块连接到目标主机并安装先决条件软件包,然后再执行任何 Ansible 代码。

  • 在网络设备(如路由器、交换机和其他嵌入式系统)的情况下,Python 可能根本不存在。这些设备仍然可以使用原始模块简单地通过 Ansible 进行管理。

除了这些例外情况之外,在所有其他情况下,建议您使用命令模块或 shell 模块,因为它们提供了控制命令何时、从何处以及如何运行的方法。

让我们看看以下给定的示例:

$ ansible -i customhosts all  -m raw -a "uptime"
[Output]
192.168.61.13 | success | rc=0 >>
 04:21:10 up 1 min,  1 user,  load average: 0.27, 0.10, 0.04
192.168.61.11 | success | rc=0 >>
 04:21:10 up 5 min,  1 user,  load average: 0.01, 0.07, 0.05
192.168.61.12 | success | rc=0 >>
 04:21:12 up  9:04,  1 user,  load average: 0.00, 0.01, 0.05

上述命令连接到使用 SSH 提供的customhosts清单中的所有主机,运行一个原始命令 uptime,并返回结果。即使目标主机没有安装 Python,这也能起作用。这类似于在一组主机上编写一个for循环以进行即席 shell 命令。

同样的命令可以转换为一个任务:

   - name: running a raw command 
     raw: uptime

使用命令模块

这是在目标节点上执行命令的最佳模块。该模块接受自由形式的命令序列,并允许您运行任何可以从命令行界面启动的命令。除了命令之外,我们还可以选择指定:

  • 要从哪个目录运行命令

  • 用于执行的 shell

  • 何时不运行命令

让我们看看以下例子:

   - name: run a command on target node
     command: ls -ltr
     args:
       chdir: /etc

在这里,调用命令模块来在目标主机上运行ls -ltr,并使用一个参数来改变目录为/etc,然后再运行命令。

除了将其写为任务之外,命令模块还可以直接调用为:

$ ansible -i customhosts all  -m command -a "ls -ltr"

使用 shell 模块

这个模块与我们刚刚学到的命令模块非常相似。它接受一个自由形式的命令和可选参数,并在目标节点上执行它们。但是,shell 模块和命令模块之间存在一些微妙的差异,如下所列:

  • Shell 在目标主机上通过'/bin/sh' shell 运行命令,这也意味着任何通过此模块执行的命令都可以访问该系统上的所有 shell 变量

  • 与命令模块不同,shell 也允许使用操作符,比如重定向(<, <<, >> , >)、管道(|)、&&||

  • Shell 比命令模块不够安全,因为它可能受到远程主机上的 shell 环境的影响

让我们看看以下例子:

   - name: run a shell command on target node
     shell: ls -ltr | grep host >> /tmp/hostconfigs
     args:
       chdir: /etc

与使用命令模块类似,前面的任务使用 shell 模块运行命令序列。但是,在这种情况下,它接受操作符,如|>>,使用grep进行过滤,并将结果重定向到文件。

不要将此任务指定为 Playbook 的一部分,它可以作为一个临时命令与 Ansible 一起运行,如下所示:

ansible -i customhosts all --sudo -m shell \
 -a "ls -ltr | grep host >> /tmp/hostconfigs2 \
chdir=/etc"

在这里,您需要明确指定--sudo选项,以及模块选项作为参数,比如chdir=/etc和实际命令序列。

使用脚本模块

到目前为止我们学到的命令模块只允许在远程主机上执行一些系统命令。在某些情况下,我们会有一个现有的脚本需要复制到远程主机上,然后在那里执行。使用 shell 或命令模块,可以通过以下两个步骤实现这一目标:

  1. 使用复制模块将脚本文件传输到远程主机。

  2. 然后,使用命令或 shell 模块来执行之前传输的脚本。

Ansible 有一个专门定制的模块来更高效地解决这个问题。使用脚本模块而不是命令或 shell,我们可以一步完成复制和执行脚本。

例如,请考虑以下代码片段:

   - name: run script sourced from inside a role
     script:  backup.sh
   - name: run script sourced from a system path on target host
     script: /usr/local/bin/backup.sh

如前面的代码片段所示,脚本可以从以下两者之一中源:

  • 在调用这个模块时,从角色内部的任务中显示的角色的内部文件目录,如第一个示例所示

  • 控制主机上的绝对系统路径(这是运行 Ansible 命令的主机)

就像所有其他模块一样,脚本也可以作为临时命令调用,如下所示:

$ ansible -i customhosts www --sudo -m script \

  -a "/usr/local/backup.sh"

这里,script 模块仅在清单中属于 www 组的主机上调用。此命令将从控制主机复制一个位于 /usr/local/backup.sh 的脚本,并在目标节点上运行它;在本例中,所有属于 www 组的主机。

部署 WordPress 应用程序 - 一种实践方法

在我们的第一个迭代中,我们已经配置了一个 Nginx Web 服务器和一个 MySQL 数据库来托管一个简单的网页。我们现在将配置一个 WordPress 应用程序在 Web 服务器上托管新闻和博客。

注意

情景:

在第一次迭代中,我们已经配置了一个 Nginx Web 服务器和一个 MySQL 数据库来托管一个简单的网页。现在,我们将在 Web 服务器上配置一个 WordPress 应用程序来托管新闻和博客。

WordPress 是一个流行的基于 LAMP 平台的开源 Web 发布框架,它是 Linux、Apache、MySQL 和 PHP。WordPress 是一个简单而灵活的开源应用程序,用于支持许多博客和动态网站。运行 WordPress 需要一个 Web 服务器、PHP 和 MySQL 数据库。我们已经配置了一个 Nginx Web 服务器和 MySQL 数据库。我们将通过创建一个角色来安装和配置 WordPress,然后稍后配置 PHP。

要创建角色,我们将使用前一章节中学到的 Ansible-Galaxy 工具:

$ ansible-galaxy init --init-path roles/ wordpress

这将创建 WordPress 角色所需的脚手架。到目前为止,我们知道核心逻辑放在任务中,并由文件、模板、处理程序等支持。我们将首先编写任务以安装和配置 WordPress。首先,我们将创建主任务文件,如下所示:

---
# tasks file for wordpress
# filename: roles/wordpress/tasks/main.yml
 - include: install.yml 
 - include: configure.yml

注意

我们遵循最佳实践,并进一步模块化任务。我们将创建一个 install.yml 文件和一个 configure.yml 文件,并从主文件中包含它们,而不是将所有内容放在 main.yml 文件中。

安装 WordPress

WordPress 的安装过程将从任务目录中的 install.yml 文件中处理。安装 WordPress 的过程通常涉及:

  1. wordpress.org 下载 WordPress 安装包。

  2. 解压安装包。

  3. 将提取的目录移动到 Web 服务器的文档“根”目录中。

我们将开始为上述提到的每个步骤编写代码,如下所示:

---
# filename: roles/wordpress/tasks/install.yml
  - name: download wordpress
    command: /usr/bin/wget -c https://wordpress.org/latest.tar.gz
    args: 
      chdir: "{{ wp_srcdir }}"
      creates: "{{ wp_srcdir }}/latest.tar.gz"
    register: wp_download

我们在前面的步骤中看到了一些新功能。让我们分析一下这段代码:

  • 我们正在使用新的样式编写任务。除了为任务使用键值对外,我们还可以将参数分开,并将它们以键值格式的每一行写入。

  • 要下载 WordPress 安装程序,我们使用了带有 wget 命令的命令模块。该命令采用具有附加参数的可执行序列,这些参数是 chdircreates

  • Creates 在这里是一个特殊选项。通过此选项,我们指定了 WordPress 安装程序正在下载的文件路径。我们将看看这对我们有什么用处。

  • 我们还将此模块的结果注册到名为 wp_download 的变量中,我们将在后续任务中使用它。

提示

建议您使用 Ansible 内置的 get_url 模块通过 HTTP/FTP 协议下载文件。由于我们想要演示命令模块的使用方法,我们选择使用它而不是使用 get_url 模块。

现在让我们来看一下我们之前介绍的新概念。

控制命令模块的幂等

Ansible 自带了许多内置模块。正如我们在第一章中所学到的 Blueprinting Your Infrastructure 中提到的那样,大多数这些模块都是幂等的,并且确定配置漂移的逻辑已内置到模块代码中。

但是,命令模块允许我们运行本质上不是幂等的 shell 命令。由于命令模块无法确定任务的结果,因此预期这些模块默认情况下不是幂等的。Ansible 为我们提供了一些选项,使这些模块可以有条件地运行,并使它们成为幂等的。

以下是确定命令是否运行的两个参数:

  • Creates

  • Removes

两者都接受文件名作为参数值。在 creates 的情况下,如果文件存在,则不会运行命令。removes 命令则相反。

"creates" 和 "removes" 选项适用于除了原始模块之外的所有命令模块。

以下是如何使用 createsremoves 标志的一些指导原则:

  • 如果您执行的命令序列或脚本创建文件,请将该文件名作为参数值提供

  • 如果命令序列不创建标志,请确保在命令序列或脚本中加入创建标志文件的逻辑

注册变量

我们之前已经看过变量。但是,我们以前从未注册过变量。在我们编写用于下载 WordPress 的任务中,我们使用了以下选项:

           register: wp_download

此选项将任务的结果存储在名为 wp_download 的变量中。然后可以稍后访问此注册结果。以下是注册变量的一些重要组成部分:

  • changed:这显示了状态是否已更改

  • cmd:通过此,启动命令序列

  • rc:这是返回代码

  • stdout:这是命令的输出

  • stdout_lines:这是逐行输出

  • stderr:这些说明了错误,如果有的话

然后,这些可以作为 wp_download.rcwp_download.stdout 访问,并且可以在模板中、动作行中或更常见的是在 when 语句中使用。在这种情况下,我们将使用 wp_download 的返回代码来决定是否提取包。这是有道理的,因为提取甚至不存在的文件是没有意义的。

使用 shell 模块提取 WordPress

现在让我们编写一个任务,提取 WordPress 安装程序并将其移动到所需位置。在此之前,我们还需要确保在运行此代码之前已创建文档 root 目录:

  # filename: roles/wordpress/tasks/install.yml
  - name: create nginx docroot
    file:
      path: "{{ wp_docroot }}"
      state: directory
      owner: "{{ wp_user }}"
      group: "{{ wp_group }}"

  - name: extract wordpress
    shell: "tar xzf latest.tar.gz && mv wordpress {{ wp_docroot }}/{{ wp_sitedir }}"
    args: 
      chdir: "{{ wp_srcdir }}"
      creates: "{{ wp_docroot }}/{{ wp_sitedir }}"
    when: wp_download.rc == 0

现在让我们分析一下刚才所写的内容:

  • 我们使用 file 模块为 web 服务器创建文档根目录。路径、用户和组等参数都来自变量。

  • 为了提取 WordPress,我们使用 shell 模块而不是命令。这是因为我们在这里使用 && 运算符将两个命令组合在一起,而命令模块不支持这一点。

  • 我们使用 when 语句来决定是否运行提取命令。要检查条件,我们使用之前存储在注册变量 wp_download 中的下载命令的返回代码。

配置 WordPress

下载和提取 WordPress 后,下一步是配置它。WordPress 的主要配置位于我们提取的 wordpress 目录下的 wp-config.php 中。作为良好的实践,我们将使用模板管理此配置文件。以下是配置 WordPress 的代码:

---
# filename: roles/wordpress/tasks/configure.yml
  - name: change permissions for wordpress site
    file:
      path: "{{ wp_docroot }}/{{ wp_sitedir }}"
      state: directory
      owner: "{{ wp_user }}"
      group: "{{ wp_group }}"
      recurse: true

  - name: get unique salt for wordpress
    local_action: command curl https://api.wordpress.org/secret-key/1.1/salt
    register: wp_salt

  - name: copy wordpress template
    template:
      src: wp-config.php.j2
      dest: "{{ wp_docroot }}/{{ wp_sitedir }}/wp-config.php"
      mode: 0644

让我们分析一下这段代码:

  • 第一个任务递归地为所有 WordPress 文件设置权限。

  • 第二个任务在本地运行命令并将结果注册到 wp_salt 变量中。这是为了为 WordPress 提供额外的安全密钥。这次将使用模板内的此变量。

  • 最后一个任务是生成一个 Jinja2 模板并将其复制到目标主机上作为 wp-config.php 文件。

让我们也看一下 Jinja2 模板:

# filename: roles/wordpress/templates/wp-config.php.j2
<?php
define('DB_NAME', 'wp_dbname');
define('DB_USER', 'wp_dbuser');
define('DB_PASSWORD', '{{ wp_dbpass }}');
define('DB_HOST', '{{ wp_dbhost }}');
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
{{ wp_salt.stdout }}
$table_prefix  = 'wp_';
define('WP_DEBUG', false);
if ( !defined('ABSPATH') )
  define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');

在这里,我们将配置参数的值填充到变量中。另一个有趣的地方是,我们嵌入了使用 stdout 变量的 salt 下载的输出:

            {{ wp_salt.stdout }}

从填充变量和从注册变量的 stdut 获取的模板创建的结果文件将如下所示:

配置 WordPress

现在我们将这个新角色添加到 www.yml playbook 中,以便它在所有我们的 web 服务器上执行:

#filename: www.yml
  roles:
     - nginx
     - wordpress

然后,我们将仅针对 web 服务器运行 Ansible playbook:

$ ansible-playbook www.yml  -i customhosts

这将在所有 web 服务器主机上下载、提取和配置 WordPress。我们还没有安装 PHP 并配置 Nginx 来提供 WordPress 页面,所以我们的更改还没有反映出来。

回顾问题

你觉得你对本章的理解足够吗?试着回答以下问题来测试你的理解:

  1. 当 Ansible 采用一揽子方法时,为什么我们还需要命令模块?

  2. 何时以及为什么要使用 raw 模块?

  3. 如何在命令执行时,当执行的命令不创建文件时,使用 creates 参数?

  4. commandshell 模块有何不同?什么时候会使用 shell?

  5. 如果 var3 是一个注册变量,你将如何在模板中打印它的输出?

总结

在本章中,你学习了如何使用 Ansible 的命令模块运行自定义命令和脚本,即 raw、command、shell 和 script。你还学会了如何使用 createsremoves 标志控制命令模块的幂等性。我们开始使用注册变量来存储任务的结果,然后可以在以后有条件地运行其他任务或将输出嵌入模板中。最后,我们创建了一个角色来安装和配置 WordPress 应用程序。

在下一章中,我们将开始学习如何使用条件语句控制执行流程,如何有选择性地应用角色,以及如何在模板中使用条件控制结构。

第 5 章。控制执行流程 - 条件

控制结构指的是对程序执行流程产生影响的任何事物。控制结构主要有以下两种类型:

  • 条件

  • 迭代

有时,我们需要根据变量的值、平台类型或甚至其他某些命令的结果有条件地执行代码。有时我们还需要迭代多个对象,例如列表哈希或多级变量。

大多数编程语言和工具使用强大但机器友好的构造,例如if elseforunlessdo while等等。然而,Ansible 忠实于其设计原则,成为一种人类友好的自动化语言,并且通过万能的whenwith_*构造实现了相同的功能,这些构造更接近英语。让我们开始探索它是如何做到这一点的。

在本章中,我们将涵盖以下主题:

  • 使用when语句进行条件控制

  • 使用变量和事实跳过子例程

  • 有选择地应用角色

  • Jinja2 模板中的条件控制结构

条件控制结构

条件控制结构允许 Ansible 根据某些条件选择替代路径、跳过任务或选择要导入的特定文件。在通用编程语言中,使用if-thenelse ifelsecase语句来完成此操作。Ansible 使用"when"语句。一些示例条件包括:

  • 是否定义了某个变量

  • 较早的命令序列是否成功

  • 任务是否已经运行过

  • 目标节点上的平台是否与支持的平台匹配

  • 某个文件是否存在

when 语句

我们已经使用了when语句来根据另一个命令的结果提取 WordPress 存档,即:

- name: download wordpress
    register: wp_download
- name: extract wordpress
    when: wp_download.rc == 0

这与编写 shell 片段大致相当,如下所示:

DOWNLOAD_WORDPRESS
var=`echo $?
if [$var -eq 0]
then
    EXTRACT_WORDPRESS()
fi

除了检查前面的代码,我们还可以根据任务本身的结果简单地编写条件,如下所示:

- name: extract wordpress
    when: wp_download|success
- name: notify devops engineers
    when: wp_download|failed

为了使失败的语句起作用,我们需要在注册变量的早期任务中添加ignore_errors: True语句。以下流程图描述了相同的逻辑:

when 语句

基于事实的选择

事实是检测平台特定信息并基于此进行选择的良好信息来源,尤其是在存在混合环境时。基于这个选择,我们可以:

  • 决定是否执行任务

  • 决定是否包含任务文件

  • 决定是否导入文件

  • 决定是否在目标节点上应用角色

在编写 MySQL 时,我们已经使用了基于事实的选择,其中我们使用了事实ansible_os_family来:

  1. 为非 Debian 系统导入vars文件。

  2. 为包安装包含特定于平台的任务。

下面的代码片段显示了两个用例:

基于事实的选择

重构 MySQL 角色

我们现有的 MySQL 角色只安装和配置服务器。往往我们只需要安装 MySQL 客户端包而不是服务器。我们没有能力有选择地这样做。

注意

情景:

我们被要求重构 MySQL 角色,并根据变量值有条件地安装 MySQL 服务器。默认情况下,它应该只安装 MySQL 客户端包。

布尔变量可以用来设置一个开关,我们将添加一个变量,并将其默认值设为false。这一次,我们将创建一个多级变量或嵌套哈希。

多级变量字典

到目前为止,我们一直将变量命名为mysql_bindmysql_port等,并使用下划线对它们进行分类。如果您使用多级字典定义它们,那么变量可以更好地分类和组织,例如:

mysql:
  config:
    bind: 127.0.0.1
    port: 3306

然后可以在代码中以mysql['config]['bind']mysql['config]['port']的方式访问多级变量。现在让我们更新roles/mysql/defaults/main.yml文件以使用多级变量,并创建一个名为mysql.server的新布尔变量,它充当标志:

多级变量字典

另外,我们还需要更新mysql角色中vars目录中的文件,以使用新风格定义变量,以及更新所有任务、处理程序和模板以适当地引用它们。这一过程作为文本的一部分添加,以避免冗余。

合并哈希

多级变量或本质上来说,从不同位置定义的字典可能需要被合并。例如,如果我们在角色default中定义默认配置参数,然后在角色的vars目录中覆盖了一些参数,那么结果变量hash应包含defaults中的项以及vars中覆盖的值。

让我们来看下面的屏幕截图:

合并哈希

但是,默认情况下,Ansible 将替换字典,在上面的示例中,我们不会得到一个合并的字典,而是会失去用户和端口vars,因为角色中的vars具有更高的优先级。这可以通过将hash_behavior参数设置为merge而不是replace来避免,如下所示:

# /etc/ansible/ansible.cfg
    hash_behaviour=merge

这应该在 Ansible 控制主机上设置,不需要我们重新启动任何服务。

有选择性地配置 MySQL 服务器

在重构代码并添加由变量控制的标志之后,我们已经准备好选择性地配置 MySQL 服务器了。我们有一个名为mysql.server的变量,它采用True/False的布尔值。此变量可用于决定是否跳过服务器配置,如下所示:

#file: roles/mysql/tasks/main.yml
- include: configure.yml
  when: mysql.server

- include: service.yml
  when: mysql.server

让我们还添加任务来安装 MySQL 客户端包以及 Ansible 的 MySQL 模块所需的 Python 绑定:

---
# filename: roles/mysql/tasks/install_Debian.yml
  - name: install mysql client
    apt:
      name: "{{ mysql['pkg']['client'] }}"
      update_cache: yes

  - name: install mysql server
    apt:
      name: "{{ mysql['pkg']['server'] }}"
      update_cache: yes
    when: mysql.server

  - name: install mysql python binding
    apt:
      name: "{{ mysql['pkg']['python'] }}"

在这里,包名称来自以下变量hash

mysql:
pkg:
    server: mysql-server
    client: mysql-client
    python: python-mysqldb

默认情况下,mysql.server参数已设置为False。我们如何仅为数据库服务器启用此选项?我们可以有很多种方法来实现这一点。这次我们会选择剧本变量,因为我们有一个专门用于 DB 服务器的变量。

让我们看一下以下截图:

有选择性地配置 MySQL 服务器

Jinja2 模板中的条件控制结构

Ansible 使用 Jinja2 作为模板引擎。因此,除了 Ansible 任务支持的控制结构外,了解 Jinja2 控制结构也对我们很有用。Jinja2 的语法将控制结构封装在{% %}块内。对于条件控制,Jinja2 使用熟悉的if语句,其语法如下:

{% if condition %}
    do_some_thing
{% elif condition2 %}
    do_another_thing
{% else %}
    do_something_else
{% endif %}

更新 MySQL 模板

我们之前创建的用于生成my.cnf文件的模板假定其中引用的所有变量都在某处定义了。有可能情况并非总是如此,这可能导致在运行 Ansible 时出现错误。我们能够有选择地将配置参数包含在my.cnf文件中吗?答案是肯定的。我们可以检查变量是否被定义,只有在这种情况下,我们才将其添加到文件中,如下所示:

#filename: roles/mysql/template/my.cnf.j2
[mysqld]
user = {{ mysql['config']['user'] | default("mysql") }}
{% if mysql.config.pid is defined %}
pid-file = {{ mysql['config']['pid'] }}
{% endif %}
{% if mysql.config.socket is defined %}
socket = {{ mysql['config']['socket'] }}
{% endif %}
{% if mysql.config.port is defined %}
port = {{ mysql['config']['port'] }}
{% endif %}
{% if mysql.config.datadir is defined %}
datadir = {{ mysql['config']['datadir'] }}
{% endif %}
{% if mysql.config.bind is defined %}
bind-address = {{ mysql['config']['bind'] }}
{% endif %}

让我们分析上述代码:

  • 由于我们正在为mysql['config']['user']参数设置默认值,因此无需检查它是否已定义。它已经被优雅地处理了。

  • 对于所有其他参数,我们使用条件检查变量是否已定义,例如if mysql.config.pid is defined。如果未定义该参数,则会跳过该参数,而不会引发错误。

仅运行一次任务

有时,在播放书执行期间,角色中的特定任务可能需要仅执行一次,即使该角色应用于多个主机。这可以通过run_once条件来实现:

name: initialize wordpress database
script: initialize_wp_database.sh 
run_once: true

由于我们正在使用run_once选项,上述任务将在应用角色的清单中的第一个主机上运行。所有后续主机都将跳过此任务。

有条件地执行角色

我们之前创建的用于设置 Web 服务器的 Nginx 角色仅支持基于 Debian 的系统。在其他系统上运行此逻辑可能会导致失败。例如,Nginx 角色使用apt模块安装软件包,在依赖于yum软件包管理器的基于 RedHat 的系统上不起作用。可以通过添加when语句与事实来选择性地基于操作系统系列执行。以下是www.yml剧本中的片段:

#filename: www.yml (snippet)
- hosts: www
  roles:
    - { role: nginx, when: ansible_os_family == 'Debian' }

复习问题

您认为您是否已经充分理解了本章内容?尝试回答以下问题来测试您的理解:

  1. Ansible 中if else语句的替代方案是什么?

  2. 如何有选择地导入特定于平台的变量?

  3. 为什么 Jinja2 模板使用____来界定控制结构?

  4. 如何跳过在不兼容平台上运行的角色?

总结

在本章中,你学习了如何使用when语句、条件导入、选择性包含等来控制执行流程。你还学会了如何使用变量和事实来有选择地跳过例程并执行特定于平台的子例程。我们对 MySQL 角色进行了重构,开始使用变量字典有条件地配置 MySQL 服务器,并使用更智能的模板来预检已定义的变量。

在下一章中,我们将开始探索第二种控制结构,即迭代控制结构,我们将开始循环数组和哈希。

第六章。迭代控制结构 - 循环

在前一章节中您了解了条件控制。我们对 Ansible 的控制结构的世界进行的旅程将继续学习迭代控制。我们经常需要创建一系列目录、安装一堆软件包、定义和遍历嵌套哈希或字典。传统的编程语言使用forwhile循环进行迭代。Ansible 将它们替换为with语句。

在本章中,我们将学习:

  • 如何使用with语句进行迭代控制

  • 如何循环数组以一次创建多个对象

  • 如何定义嵌套哈希并遍历它们以创建数据驱动的角色

万能的 with 语句

使用“瑞士军刀”实用工具with语句可以实现循环纯列表、解析字典、循环一系列数字、解析路径并有选择地复制文件,或者只是从列表中挑选一个随机项。with语句采用以下形式:

with_xxx

这里,xxx参数是需要循环的数据类型,例如,项、字典等。

以下表列出了with语句可以迭代的数据类型:

构造 数据类型 描述
with_items 数组 用于循环数组项。例如,用于创建一组用户、目录,或者安装一系列软件包。
with_nested 嵌套循环 用于解析多维数组。例如,创建一个 MySQL 用户列表并为他们授予一组数据库的访问权限。
with_dict 哈希 用于解析键值对字典并创建虚拟主机。
with_fileglobs 文件模式匹配 用于解析路径并仅复制与特定模式匹配的文件。
with_together 集合 用于将两个数组合并为一个集合并循环遍历它。
with_subelements 哈希子元素 用于解析哈希的子元素。例如,遍历 SSH 密钥列表并将其分发给用户。
with_sequence 整数序列 用于循环一系列数字。
with_random_choice 随机选择 用于以随机顺序从数组中选择项目。
with_indexed_items 带索引的数组 这是一个带有索引的数组,当需要项目索引时很有用。

配置 WordPress 必备条件

在第四章安装 WordPress 的角色创建时,引入您的代码 - 自定义命令和脚本,我们创建了下载、提取和复制 WordPress 应用的任务。然而,这还不足以启动 WordPress,它有以下先决条件:

  • 一个网络服务器

  • 网络服务器的 PHP 绑定

  • MySQL 数据库和 MySQL 用户

一个 Nginx web 服务器和 MySQL 服务已经在我们的案例中安装。我们仍然需要安装并配置 PHP,以及为我们的 WordPress 应用程序所需的 MySQL 数据库和用户。为了处理 PHP 请求,我们选择实现 PHP5-FPM 处理程序,这是传统 FastCGI 实现的替代品。

PHP5-FPM 角色

PHP5-FPM 中,FPM 代表 FastCGI Process Manager。PHP5-FPM 提供了比 fastcgi 更高级的功能,对于管理高流量站点非常有用。它适用于提供我们的 fifanews 站点,该站点每天预计会有数百万次点击。根据我们创建模块化代码的设计原则,我们将保持 PHP 功能在其自己的角色中。让我们使用 Ansible-Galaxy 命令初始化 PHP5-FPM 角色,如下所示:

$ ansible-galaxy init --init-path roles/ php5-fpm

定义一个数组

PHP 安装将涉及安装多个软件包,包括 php5-fpmphp5-mysql 和其他一些软件包。到目前为止,我们一直是一次编写一个任务。例如,让我们来看看以下代码片段:

  - name: install php5-fpm
    apt: name: "php5-fpm" 
  - name: install php5-mysql
    apt: name: "php5-mysql"

但是,当我们想要安装多个软件包时,这可能会变得重复,并导致冗余代码。为了致力于编写数据驱动的角色,我们将通过一个变量来推动软件包的安装,该变量获取一个软件包列表,然后对该列表进行迭代。让我们开始定义需要列出软件包的参数,如下所示:

---
#filename: roles/php5-fpm/defaults/main.yml
#defaults file for php5-fpm
php5:
  packages:
    - php5-fpm
    - php5-common
    - php5-curl
    - php5-mysql
    - php5-cli
    - php5-gd
    - php5-mcrypt
    - php5-suhosin
    - php5-memcache
  service:
    name: php5-fpm

这是前面代码的分析:

  • php5 变量是一个变量字典,其中包含我们传递给 php5-fpm 角色的所有参数。

  • php5.packages 参数是一个包的数组,在代码的每一行定义一个包。这将被传递给一个任务,该任务将迭代每个项目并安装它。

  • php5.service 参数定义了服务的名称,该名称将在服务任务中引用。

循环一个数组

现在让我们为 php5-fpm 角色创建任务。我们需要从数组中安装软件包,然后启动服务。我们将包的功能拆分为两个独立的任务文件,并从 main.yml 文件中调用它,如下所示:

---
#filename: roles/php5-fpm/tasks/main.yml
# tasks file for php5-fpm
- include_vars: "{{ ansible_os_family }}.yml"
  when: ansible_os_family != 'Debian'

- include: install.yml
- include: service.yml

#filename: roles/php5-fpm/tasks/install.yml
  - name: install php5-fpm and family
    apt:
      name: "{{ item }}"
    with_items: php5.packages
    notify:
     - restart php5-fpm service

#filename: roles/php5-fpm/tasks/service.yml
# manage php5-fpm service
- name: start php5-fpm service
  service:
    name: "{{ php5['service']['name'] }}"
    state: started

除了任务,还可以编写重新启动 php5-fpm 角色的处理程序,如下所示:

---
# filename: roles/php5-fpm/handlers/main.yml
# handlers file for php5-fpm
- name: restart php5-fpm service
  service: name="{{ php5['service']['name'] }}" state=restarted

让我们分析前面的代码:

  • 主: main.yml 文件根据非 Debian 系统的 ansible_os_family 事实包含变量。这对于覆盖特定于平台的变量非常有用。在包含 vars 文件之后,主任务继续包含 install.ymlservice.yml 文件。

  • 安装install.yml 文件是我们迭代先前定义的一个包数组的地方。由于该文件包含一个数组,我们使用 with.items 构造与 php5.packages 变量一起使用,并将 {{ item }} 参数传递为要安装的软件包的名称。我们也可以直接传递数组,如下所示:

      with_items:
        - php5-fpm
        - php5-mysql
    
  • 服务和处理器service.yml 文件和处理器 main.yml 文件管理 php5-fom 服务的启动和重新启动。它使用字典变量 php5['service']['name'] 来确定服务名称。

创建 MySQL 数据库和用户账户

WordPress 是一个内容管理系统,需要一个可用的 MySQL DB 来存储数据,例如帖子、用户等。此外,它还需要一个具有适当权限的 MySQL 用户来从 WordPress 应用程序连接到数据库。在安装 MySQL 时会获得一个管理员用户,但是,根据需要创建额外的用户帐户并授予用户权限是一个好习惯。

创建哈希

哈希,哈希表的缩写,是键值对字典。它是一个有用的数据结构,用于创建多级变量,然后可以通过编程方式创建具有自己值的多个对象。我们将在 group_vars/all 文件中定义数据库和用户为字典项,如下所示:

#filename: group_vars/all
mysql_bind:  "{{ ansible_eth0.ipv4.address }}"
mysql:
  databases:
    fifalive:
      state: present
    fifanews:
      state: present
  users:
    fifa:
      pass: supersecure1234
      host: '%'
      priv: '*.*:ALL'
      state: present

这是上述代码的分析:

  • 我们在 group_vars/all 文件中定义了此变量哈希,而不是在角色中。这是因为我们希望保持角色的通用性和共享性,而不添加特定于我们各自环境的数据。

  • 我们将数据库和用户配置定义为多级字典或哈希。

嵌套哈希

通过以下图解释这个多级哈希:

嵌套哈希

以下是这个嵌套哈希结构的描述:

  • MySQL 变量是一个具有两个键的哈希:数据库和用户。例如:

    mysql:
        databases: value
         users: value
    
  • 这两个键的每个值都是哈希,或关于要创建的数据库和用户的信息字典。例如:

    databases:
        fifalive: value
        fifanews: value
    
  • 每个数据库本身都是键值对字典。例如,对于 MySQL 用户 fifalive,键值对是 "state:present"。

遍历哈希

创建数据库和用户账户通常需要创建具有模板的自定义脚本,然后使用命令模块调用。相反,Ansible 提供了一些现成的模块来执行与 MySQL 相关的任务,即 mysql_dbmysql_user 参数。使用 with_dict 语句,我们将遍历我们之前定义的数据库和用户字典,如下所示:

# filename: roles/mysql/tasks/configure.yml
 - name: create mysql databases
    mysql_db:
      name: "{{ item.key }}"
      state: "{{ item.value.state }}"
    with_dict: "{{ mysql['databases'] }}"

 - name: create mysql users
    mysql_user:
      name: "{{ item.key }}"
      host: "{{ item.value.host }}"
      password: "{{ item.value.pass }}"
      priv: "{{ item.value.priv }}"
      state: "{{ item.value.state }}"
    with_dict: "{{ mysql['users'] }}"

这是上述代码的分析:

  • mysql['databases']mysql['users'] 参数是使用 with_dict 语句传递给任务的字典

  • 每个字典或哈希都有一个键值对,作为 {{ item.key }}{{ item.value }} 参数传递

  • {{ item.value }} 参数是一个字典。此字典中的每个键然后称为 {{ item.value.<key> }}。例如,{{ item.value.state }} 参数

以下图解释了这个嵌套哈希是如何解析的:

遍历哈希

创建 Nginx 虚拟主机

安装完php5-fpm管理器并创建了 MySQL 数据库和用户账户后,剩下的配置是在 Nginx 中创建一个虚拟主机以服务我们的 WordPress 应用程序。之前安装的 Nginx Web 服务器只服务于一个简单的 HTML 页面,并不知道 WordPress 应用程序的存在或如何服务它。让我们从添加这些配置开始。

定义 PHP 站点信息

除了我们正在设置的fifanews.com站点外,我们可能还会在将来启动一些与足球相关的站点。因此,我们需要有能力以编程方式添加多个站点到同一个 Nginx 服务器上。创建一个定义站点信息的字典并将其嵌入到模板中听起来是一个不错的选择。由于站点信息是特定于我们的,我们将将变量哈希添加到group_vars文件中,如下所示:

#filename: group_vars/all
nginx:
  phpsites:
    fifanews:
      name: fifanews.com
      port: 8080
      doc_root: /var/www/fifanews

我们学习了如何从 Ansible 任务中解析这个字典。让我们添加一个任务,允许我们遍历这个字典,将值传递给模板,并创建虚拟主机配置:

#filename: roles/nginx/tasks/configure.yml
- name: create php virtual hosts
    template:
      src: php_vhost.j2
      dest: /etc/nginx/conf.d/{{ item.key }}.conf
    with_dict: "{{ nginx['phpsites'] }}"
    notify:
      - restart nginx service

字典中的每个项目都会传递给模板,这种情况下是传递给php_vhost.j2参数。然后,模板会读取哈希并创建一个虚拟主机模板,配置一个 PHP 应用程序,如下所示:

#filename: roles/nginx/templates/php_vhost.j2
#{{ ansible_managed }}

server {
    listen {{ item.value.port }};

  location / {
    root {{ item.value.doc_root }};
    index index.php;
  }

  location ~ .php$ {
    fastcgi_split_path_info ^(.+\.php)(.*)$;
    fastcgi_pass   backend;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  {{ item.value.doc_root }}$fastcgi_script_name;
    include fastcgi_params;
  }
}
upstream backend {
  server 127.0.0.1:9000;
}

这是前述代码的分析:

  • {{ ansible_managed }}参数是一个特殊变量,它添加了一条注释,通知服务器该文件正在被 Ansible 管理,包括该文件在 Ansible 存储库中的路径、最后修改时间以及修改它的用户。

  • 该模板获取字典项并解析其值,因为它是一个嵌套的哈希。该模板配置了使用nginx.phpsites设置的字典值创建 Nginx 的 php 虚拟主机的配置。

  • 提供的字典中的配置参数包括文档根目录、端口、后端使用的内容,这使得 Nginx 知道如何处理传入的 PHP 请求,使用哪个后端,监听哪个端口等等。

最后,我们将新角色添加到www.yaml文件中,如下所示:

# www.yml
roles:
     - { role: nginx, when: ansible_os_family == 'Debian' }
     - php5-fpm
     - wordpress

使用以下命令运行 Playbook:

$ ansible-playbook -i customhosts site.yml

运行完成后,是时候测试我们的工作了。让我们在浏览器中加载以下 URL:

http://<web_server_ip>:8080

恭喜!我们已成功创建了一个带有 Nginx Web 服务器和 MySQL 后端的 WordPress PHP 应用程序,完全配置完毕。现在,我们准备设置我们的 fifanews 网站:

定义 PHP 站点信息

复习问题

您认为自己已经足够理解了本章内容吗?尝试回答以下问题来测试您的理解:

  1. 在 Ansible 中,哪个语句替代了for循环?

  2. 如何使用with_____语句迭代字典?

  3. 如何向模板中添加一个语句,以打印修改时间和修改人?

  4. 如何打印嵌套哈希的值?

概要

在本章中,您学习了如何迭代创建多个对象。我们从全能的with语句及其各种形式的概述开始。然后,我们更深入地探讨了迭代两种最基本的数据结构,即数组和哈希。php5-fpm角色接受一个包含软件包列表的数组,并创建一个循环安装这些软件包的任务。为了创建 MySQL 数据库和用户,我们定义了变量字典或哈希并对它们进行了迭代。最后,我们通过迭代嵌套字典添加了 Nginx 模板配置,以创建多个虚拟主机来提供 PHP 应用程序。

在下一章中,您将学习如何使用魔术变量发现有关其他节点的信息。

第七章。节点发现和集群化

对于大多数现实场景,我们需要创建一个计算节点集群,其上运行着相互连接的应用程序。例如,我们正在构建的 WordPress 网站需要将 Web 服务器和数据库连接在一起。

集群基础设施具有拓扑结构,其中一类节点应该能够发现关于不同或相同类别服务器的信息。例如,WordPress 应用服务器需要发现关于数据库服务器的信息,而负载均衡器需要了解每个 Web 服务器的 IP 地址/主机名,以便将流量发送到这些服务器。本章重点介绍 Ansible 提供的用于将节点分组并发现相互连接的节点属性的原语。

在本章中,我们将学习以下内容:

  • 发现集群中其他节点的信息

  • 使用发现的魔术变量动态生成配置

  • 为什么以及如何启用事实缓存

使用魔术变量进行节点发现

我们已经看到了用户定义的变量以及系统数据,即事实。除了这些之外,还有一些变量用于定义关于节点、清单和播放的元信息,例如节点属于哪些组、哪些组包含在清单中、哪些节点属于哪些组等。这些隐式设置的变量称为魔术变量,对于发现节点和拓扑信息非常有用。下表列出了最有用的魔术变量及其描述:

魔术变量 描述
hostvars 这些是设置在另一台主机上的查找变量或事实。
groups 这是清单中组的列表。可以使用它来遍历一组节点以发现其拓扑信息。
group_names 这是节点所属的组列表。
inventory_hostname 这是清单文件中设置的主机名。它可能与ansible_hostname事实不同。
play_hosts 这是属于当前播放的所有主机的列表。

除了上表之外,还有一些额外的魔术变量,例如delegate_toinventory_dirinventory_file参数,但这些与节点发现无关,使用频率较低。

现在我们将创建一个新角色作为负载均衡器,该角色依赖于魔术变量提供的节点发现功能。

创建负载均衡器角色

我们创建了 Nginx 和 MySQL 角色来服务 WordPress 网站。但是,如果我们必须构建可扩展的网站,我们还需要添加一个负载均衡器。这个负载均衡器将作为传入请求的入口点,然后将流量分散到可用的 Web 服务器上。让我们考虑以下情况,我们的 fifanews 站点已经成为一瞬间的热门。流量呈指数增长,我们一直在使用的单个 Web 服务器方法正在出现问题。我们需要水平扩展并添加更多的 Web 服务器。一旦我们开始创建更多的 Web 服务器,我们还需要一些机制来平衡这些流量。我们被委托创建一个 haproxy 角色,它将自动发现我们集群中的所有 Web 服务器并将其添加到其配置中。

下图解释了使用 HAProxy 作为前端,在后端平衡 Web 服务器负载的情况。HAProxy 是一个广泛使用的开源 TCP/HTTP 负载均衡器。让我们看看下面的图表:

创建负载均衡器角色

在接下来的步骤中,我们不仅将创建一个 haproxy 模块,还将使用魔术变量自动配置其 IP 地址为所有 Web 服务器节点:

  1. 让我们从使用以下命令创建编写此角色所需的框架开始:

    $ ansible-galaxy init --init-path roles/ mysql
    
    

    输出如下所示:

     haproxy was created successfully
    
    
  2. 现在我们将向变量默认添加与 haproxy 角色相关的一些变量:

    ---
    # filename: roles/haproxy/defaults/main.yml
    haproxy:
      config:
        cnfpath: /etc/haproxy/haproxy.cfg
        enabled: 1
        listen_address: 0.0.0.0
        listen_port: 8080
      service: haproxy
      pkg: haproxy
    

    提示

    尽管为 haproxy 支持的每个配置添加参数是一个好的做法,但在编写这个角色时,我们将坚持使用一部分参数;这对于节点发现特别有用。

  3. 现在,让我们创建一些任务和处理程序,在 Ubuntu 主机上安装、配置和管理 haproxy 服务:

    ---
    # filename: roles/haproxy/tasks/main.yml
    - include: install.yml
    - include: configure.yml
    - include: service.yml
    
    ---
    # filename: roles/haproxy/tasks/install.yml
      - name: install haproxy
        apt:
          name: "{{ haproxy['pkg'] }}"
    
    ---
    # filename: roles/haproxy/tasks/configure.yml
     - name: create haproxy config
       template: src="img/haproxy.cfg.j2" dest="{{ haproxy['config']['cnfpath'] }}" mode=0644
       notify:
        - restart haproxy service
    
     - name: enable haproxy
       template: src="img/haproxy.default.j2" dest=/etc/default/haproxy mode=0644
       notify:
        - restart haproxy service
    
    ---
    # filename: roles/haproxy/tasks/service.yml
     - name: start haproxy server
       service:
         name: "{{ haproxy['service'] }}" 
         state: started
    
    ---
    # filename: roles/haproxy/handlers/main.yml
    - name: restart haproxy service
      service: name="{{ haproxy['service'] }}" state=restarted
    

以下是前述代码的分析:

  • 根据最佳实践,我们为每个阶段创建了单独的任务文件:install、configure 和 service。然后我们从主任务文件,即 tasks/main.yml 文件中调用这些文件。

  • HAProxy 的配置文件将使用 Jinja2 模板创建在 /etc/haproxy/haproxy.cfg 中。除了创建配置外,我们还需要在 /etc/defaults/haproxy 文件中启用 haproxy 服务。

  • 安装、服务和处理程序与我们之前创建的角色类似,因此我们将跳过描述。

我们在 configure.yml 文件中定义了模板的使用。现在让我们创建模板:

#filename: roles/haproxy/templates/haproxy.default
ENABLED="{{ haproxy['config']['enabled'] }}"

#filename: roles/haproxy/templates/haproxy.cfg.j2
global
        log 127.0.0.1 local0
        log 127.0.0.1 local1 notice
        maxconn 4096
        user haproxy
        group haproxy
        daemon

defaults
        log global
        mode http
        option httplog
        option dontlognull
        retries 3
        option redispatch
        maxconn 2000
        contimeout 5000
        clitimeout 50000
        srvtimeout 50000

listen fifanews {{ haproxy['config']['listen_address'] }}:{{ haproxy['config']['listen_port'] }}
        cookie  SERVERID rewrite
        balance roundrobin
    {% for host in groups['www'] %}
        server {{ hostvars[host]['ansible_hostname'] }} {{ hostvars[host]['ansible_eth1']['ipv4']['address'] }}:{{ hostvars[host]['nginx']['phpsites']['fifanews']['port'] }} cookie {{ hostvars[host]['inventory_hostname'] }} check
    {% endfor %}

我们在 roles/haproxy/templates/haproxy.cfg.j2 创建的第二个模板对于我们来说尤为重要,与节点发现相关。下图显示了标记了魔术变量的相关部分:

创建负载均衡器角色

让我们分析这个模板片段:

  • 我们正在使用魔术变量 groups 来查找清单中属于 www 组的所有主机,如下所示:

  • 对于每个发现的主机,我们使用 hostvars 参数获取事实以及用户定义的变量,这是另一个魔术变量。我们正在查找事实和用户定义的变量,以及另一个魔术变量 inventory_hostname,如下所示:

    {{ hostvars[host]['ansible_eth1']['ipv4']['address'] }}

    {{ hostvars[host]['inventory_hostname'] }}
    {{ hostvars[host]['nginx']['phpsites']['fifanews']['port'] }}
    

要将此角色应用于清单中定义的负载均衡器主机,我们需要创建一个 play,这应该是 site.yml 文件的一部分,这是我们的主 playbook:

---
#filename: lb.yml
- hosts: lb
  remote_user: vagrant
  sudo: yes
  roles:
     - { role: haproxy, when: ansible_os_family == 'Debian' }

---
# This is a site wide playbook 
# filename: site.yml
- include: db.yml
- include: www.yml
- include: lb.yml

现在,使用以下命令运行 playbook:

$ ansible-playbook -i customhosts site.yml

上述运行将安装 haproxy 并在后端部分的 haproxy.cfg 文件中添加所有 web 服务器的配置。haproxy.cfg 文件的示例如下所示:

listen fifanews 0.0.0.0:8080
     cookie  SERVERID rewrite
     balance roundrobin
     server  vagrant 192.168.61.12:8080 cookie 192.168.61.12 check

访问非 playbook 主机的事实

在早期的练习中,我们启动了主 playbook,该 playbook 调用所有其他 playbook 来配置整个基础架构。有时,我们可能只想配置基础架构的一部分,在这种情况下,我们可以只调用个别的 playbook,例如 lb.ymlwww.ymldb.yml。让我们尝试仅为负载均衡器运行 Ansible playbook:

$ ansible-playbook -i customhosts lb.yml

哎呀!失败了!这是输出片段的快照:

访问非 playbook 主机的事实

Ansible 因为无法从主机中找到不再属于 playbook 的变量而退出出错。下面是当涉及到魔术变量时 Ansible 的行为方式:

  • 当 Ansible 在主机上运行代码时,它开始收集事实。然后将这些事实存储在内存中,以供 playbook 运行期间使用。这是默认行为,可以关闭。

  • 要使主机 B 从主机 A 发现变量,Ansible 应该在 playbook 的早期与主机 A 进行通信。

Ansible 的这种行为可能导致不良结果,并且可能限制主机发现关于仅属于其自己 play 的节点的信息。

使用 Redis 进行事实缓存

可以通过缓存事实来避免从非 playbook 主机中发现事实的失败。此功能已在 Ansible 1.8 版本中添加,并支持在 Redis 中缓存事实,在内存数据存储中的键值。这需要两个更改:

  • 在 Ansible 控制节点上安装并启动 Redis 服务

  • 配置 Ansible 将事实发送到 Redis 实例

现在让我们使用以下命令安装并启动 Redis 服务器:

$ sudo apt-get install redis-server
$ sudo service redis-server start
$ apt-get install python-pip
$ pip install redis

这将在 Ubuntu 主机上安装 Redis 并启动服务。如果您有基于 rpm 包的系统,可以按照以下方式安装:

$ sudo yum install redis
$ sudo yum install python-pip
$ sudo service start redis
$ sudo pip install redis

提示

在启用事实缓存之前,首先检查您是否正在运行与 1.8 版本相等或更高版本的 Ansible。您可以通过运行命令 $ ansible –version 来执行此操作。

现在我们已经启动了 Redis,是时候配置 Ansible 了。让我们按照以下步骤编辑 ansible.cfg 文件:

# filename: /etc/ansible/ansible.cfg
# Comment  following lines 
# gathering = smart
# fact_caching = memory
# Add  following lines 
gathering = smart
fact_caching = redis
fact_caching_timeout = 86400
fact_caching_connection = localhost:6379:0

现在让我们通过运行配置 web 服务器的 playbook 来验证这个设置:

$ ansible-playbook -i customhosts www.yml
$ redis-cli 
$ keys *

让我们来看下面的截图:

使用 Redis 进行事实缓存

现在我们将尝试再次运行负载均衡器 playbook,使用以下命令:

$ ansible-playbook -i customhosts lb.yml

这一次成功通过。它能够发现不属于 play 的 Web 服务器的事实。

在文件中缓存事实

尽管使用 Redis 是推荐的方法,但也可以将事实缓存到平面文件中。Ansible 可以使用 JSON 格式将事实写入文件。要将 JSON 文件作为格式启用,我们只需编辑ansible.cfg文件如下:

   # filename: /etc/ansible/ansible.cfg 
   fact_caching = jsonfile
fact_caching_connection = /tmp/cache

确保指定的目录存在且具有正确的权限:

$ mkdir /tmp/cache
$ chmod 777 /tmp/cache

完成这些更改后,我们所要做的就是运行 playbook,Ansible 将开始将事实写入以此目录下创建的主机的 JSON 文件中。

回顾问题

你认为你已经足够理解本章了吗?试着回答以下问题来测试你的理解:

  1. 神奇变量与事实变量有何不同?它们用于什么?

  2. 哪个神奇变量能让我们遍历一个 Web 服务器列表,并为每个枚举一个 IP 地址?

  3. 为什么需要事实缓存?缓存事实的不同模式是什么?

  4. inventory_hostname事实变量是否总是与ansible_hostname事实变量相同?

摘要

在本章中,您学习了如何发现群集中其他节点的信息以将它们连接在一起。我们从介绍神奇变量开始,然后看了看最常用的变量。然后,我们开始为 haproxy 创建角色,它会自动发现 Web 服务器并动态创建配置。最后,我们看了一下如何访问不在 playbook 中的主机的信息的问题,并且您学会了如何通过启用事实缓存来解决它。神奇变量非常强大,特别是在使用 Ansible 编排基础架构时,自动发现拓扑信息非常有用。

在下一章中,您将学习如何使用 vault 安全地传递数据,这是一个加密的数据存储。

第八章。使用 Vault 加密数据

使用变量,我们学习了如何分离数据和代码。通常提供的数据是敏感的,例如,用户密码,数据库凭据,API 密钥和其他组织特定信息。Ansible-playbooks 作为源代码,通常存储在版本控制仓库中,如 git,这使得在协作环境中保护这些敏感信息变得更加困难。从 1.5 版本开始,Ansible 提供了一个称为 vault 的解决方案,用于安全地存储和检索此类敏感信息,使用经过验证的加密技术。使用 vault 的目的是加密数据,然后可以自由地与版本控制系统(如 git)共享,而不会泄露值。

在本章中,我们将学习以下主题:

  • 了解 Ansible-vault

  • 使用 Ansible-vault 保护数据

  • 加密、解密和重新生成密钥操作

Ansible-vault

Ansible 提供了一个名为 Ansible-vault 的实用程序,顾名思义,让您安全地管理数据。Ansible-vault 实用程序可以让您通过启动编辑器界面创建一个加密文件,或者加密现有文件。在任何一种情况下,它都会要求输入一个 vault 密码,然后使用该密码使用 AES 密码对数据进行加密。加密内容可以存储在版本控制系统中,而不会泄露。由于 AES 基于共享密码,解密时需要提供相同的密码。提供密码有两种选项,一种是在启动 Ansible 时运行 --ask-vault-pass 选项以提示输入密码,另一种是使用 --vault-password-file 选项提供包含密码的文件路径。

高级加密标准

高级加密标准AES)是一种基于 Rijndael 对称分组密码的加密标准,得名于两位比利时密码学家 —— Vincent Rijmen 和 Joan Daemen,并由美国国家标准与技术研究院(NIST)在 2001 年首次建立。AES 是美国政府用来共享机密信息的算法,并且是最流行的对称密钥密码算法。AES 也是第一个由 国家安全局NSA)批准的公开可访问的密码。

作为一个开放和流行的标准,Ansible 使用 AES 密码,密钥长度为 256 位,用于使用 vault 加密数据。

使用 Vault 加密什么?

Ansible-vault 可以加密任何结构化数据。由于 YAML 本身是一种结构化语言,几乎你为 Ansible 编写的一切都符合这个标准。以下是可以使用 vault 加密的内容的指示:

  • 最常见的是,我们加密变量,可能包括以下内容:

    • 角色中的变量文件,例如,varsdefaults

    • 存货变量,例如,host_varsgroup_vars

    • 使用 include_varsvars_files 包含的变量文件

    • 通过 -e 选项传递给 Ansible-playbook 的变量文件,例如,-e @vars.yml-e @vars.json

  • 由于任务和处理程序也是 JSON 数据,因此可以使用 vault 对其进行加密。但这应该很少见。建议您加密变量并在任务和处理程序中引用它们。

以下是不能使用 vault 加密的指针:

  • 由于 vault 的加密单位是文件,因此无法加密部分文件或值。您可以加密完整文件或不加密。

  • 文件和模板无法加密,因为它们可能与 JSON 或 YML 不同。

以下数据是加密的良好候选对象:

  • 凭证,例如,数据库密码和应用凭证

  • API 密钥,例如,AWS 访问密钥和秘密密钥

  • 用于 web 服务器的 SSL 密钥

  • 部署的私有 SSH 密钥

使用 Ansible-vault

以下表列出了 Ansible-vault 实用程序提供的所有子命令:

子命令 描述
create 这将使用编辑器从头开始创建一个加密文件。在运行命令之前,需要设置编辑器环境变量。
edit 这将使用编辑器编辑现有的加密文件,而不解密内容。
encrypt 这将使用结构化数据加密现有文件。
decrypt 这将解密文件。请谨慎使用,并且不要将解密后的文件提交到版本控制中。
rekey 这会更改用于加密或解密的密钥或密码。

加密数据

让我们使用 Ansible-vault 执行一些操作。我们将从创建一个加密文件开始。要从头开始创建新文件,Ansible-vault 使用 create 子命令。在使用此子命令之前,重要的是要在环境中设置一个编辑器,如下所示:

# setting up vi as editor
$ export EDITOR=vi
# Generate a encrypted file
$ ansible-vault create aws_creds.yml
Vault password:
Confirm Vault password:

运行此命令会打开一个由编辑器环境变量指定的编辑器。下面是您可能创建的用于以访问密钥和秘密密钥形式存储 AWS 用户凭证的 aws_creds.yml 文件示例。然后,这些密钥将用于向 Amazon web services 云平台发出 API 调用。保存此文件并退出编辑器将生成一个加密文件:

加密数据

你可以通过运行以下命令检查创建的文件类型及其内容:

# Check file type and content
$ file aws_creds.yml
aws_creds.yml: ASCII text
$ cat aws_creds.yml
$ANSIBLE_VAULT;1.1;AES256
64616236666362376630366435623538336565393331333331663663636237636335313234313134
3337303865323239623436646630336239653864356561640a363966393135316661636562333932
61323932313230383433313735646438623032613635623966646232306433383335326566343333
3136646536316261300a616438643463656263636237316136356163646161313365336239653434
36626135313138343939363635353563373865306266363532386537623463623464376134353863
37646638636231303461343564343232343837356662316262356537653066356465353432396436
31336664313661306630653765356161616266653232316637653132356661343162396331353863
34356632373963663230373866313961386435663463656561373461623830656261636564313464
37383465353665623830623363353161363033613064343932663432653666633538

更新加密数据

要更新添加到加密文件中的 AWS 密钥,可以稍后使用 Ansible-vault 的 edit 子命令,如下所示:

$ ansible-vault edit aws_creds.yml
Vault password:

edit 命令执行以下操作:

  1. 提示输入密码

  2. 使用 AES 对称密码,即时解密文件

  3. 打开编辑器界面,允许您更改文件的内容

  4. 将文件保存后再次加密

还有另一种更新文件内容的方法。您可以按如下方式解密文件:

$ ansible-vault decrypt aws_creds.yml
Vault password:
Decryption successful

更新后,该文件可以像之前学过的那样再次加密。

旋转加密密钥

作为良好的安全实践,经常更改 Ansible-vault 使用的加密密钥是个好主意。当这种情况发生时,重新为之前使用 vault 加密的所有文件重新生成密钥是至关重要的。Ansible vault 提供了一个 rekey 子命令,可以如下使用:

$ ansible-vault rekey aws_creds.yml
Vault password:
New Vault password:
Confirm New Vault password:
Rekey successful

它要求输入当前密码,然后允许您指定并确认新密码。请注意,如果您正在使用版本控制管理此文件,则还需要提交更改。即使实际内容未更改,重新生成操作也会更新所创建的结果文件,该文件是我们存储库的一部分。

加密数据库凭据

早些时候在创建数据库用户时,我们在 group_vars 中以明文提供了密码。这可能是一个潜在的威胁,特别是当提交到版本控制存储库时。让我们加密它。我们将使用 encrypt 子命令,因为我们已经有了一个变量文件。

由于我们使用 group_vars 组提供数据库凭据,因此我们将如下加密 group_vars/all 文件:

$ ansible-vault encrypt group_vars/all
Vault password:
Confirm Vault password:
Encryption successful

对于加密,Ansible-vault 要求用户输入密码或密钥。使用此密钥,vault 加密数据并用加密内容替换文件。以下图表显示了 group_vars/all 文件的左侧明文内容和等效的右侧加密内容:

加密数据库凭据

现在此文件可以安全地检入版本控制系统并共享。但是,以下是用户应该注意的注意事项:

  • 与纯文本不同,结果文件是以加密格式存储的。不可能获得不同的文件格式,例如 git diff,以便在提交到版本控制时比较更改。

  • 无法直接在此文件上使用 grepsed 或任何文本搜索或操作程序。唯一的方法是先解密它,运行文本操作工具,然后再加密回去。

提示

确保您为要在一个 Ansible-playbook 运行中解密的所有文件使用相同的密码。Ansible 一次只能接受一个密码值,如果在同一 playbook 中使用不同的密码对文件进行加密,则会失败。

现在让我们使用以下命令运行 Ansible playbook:

$ ansible-playbook -i customhosts site.yml
ERROR: A vault password must be specified to decrypt /vagrant/chap8/group_vars/all

它以错误失败!这是因为我们正在为 playbook 提供加密数据,而没有解密它的密钥。vault 的主要用途是在 Ansible 存储库中保护数据。最终,在运行 playbook 时需要解密这些值。解密密码可以使用 --ask-vault-pass 选项指定,如下所示:

$ ansible-playbook -i customhosts site.yml --ask-vault-pass

这将提示输入 "Vault 密码",然后继续像往常一样运行 Ansible 代码。

使用密码文件

每次输入密码可能不是理想的。通常情况下,您可能还希望自动化启动 Ansible playbook 运行的过程,在这种情况下,交互式方式是不可行的。可以通过将密码存储在文件中并将文件提供给 Ansible playbook 运行来避免这种情况。密码应作为单行字符串提供在此文件中。

让我们创建一个密码文件并使用正确的权限保护它:

$ echo "password" > ~/.vault_pass
(replace password with your own secret)
$ chmod 600 ~/.vault_pass

提示

当保险库密码存储为明文时,任何访问此文件的人都可以解密数据。确保密码文件受到适当权限的保护,并且不添加到版本控制中。如果决定对其进行版本控制,请使用gpg或等效措施。

现在可以将此文件提供给 Ansible playbook,如下所示:

$ ansible-playbook -i customhosts site.yml --vault-password-file ~/.vault_pass

将保险库密码文件选项添加到 Ansible 配置

使用版本 1.7,还可以将vault_password_file选项添加到ansible.cfg文件的默认部分。

考虑以下:

[defaults]
  vault_password_file = ~/.vault_pass

上述选项使您可以自由地不需要每次都指定加密密码或密码文件。让我们看一下以下命令:

# launch ansible playbook run with encrypted data
# with vault_password_file option set in the config
$ ansible-playbook -i customhosts site.yml
$ ansible-vault encrypt roles/mysql/defaults/main.yml
Encryption successful
$ ansible-vault decrypt roles/mysql/defaults/main.yml
Decryption successful

此外,在从版本 1.7 开始时,可以将脚本提供给vault_password_file选项,而不是在文件中存储明文密码。使用脚本时,请确保:

  • 脚本上启用了执行位

  • 调用此脚本会在标准输出上输出密码。

  • 如果脚本提示用户输入,则可以将其发送到标准错误

在模板中使用加密数据

您之前了解到,由于模板可能不是结构化文件,如 YAML 或 JSON,因此它无法加密。但是,有一种方法可以向模板添加加密数据。请记住,模板最终是即时生成的,动态内容实际上来自变量,这些变量可以加密。让我们讨论如何通过为 Nginx Web 服务器添加 SSL 支持来实现这一点。

为 Nginx 添加 SSL 支持

我们已经设置了一个 Nginx Web 服务器,现在让我们通过以下步骤为默认站点添加 SSL 支持:

  1. 我们首先添加变量,如下所示:

    #file: roles/nginx/defaults/main.yml 
    nginx_ssl: true
    nginx_port_ssl: 443
    nginx_ssl_path: /etc/nginx/ssl
    nginx_ssl_cert_file: nginx.crt
    nginx_ssl_key_file: nginx.key
    
  2. 让我们也创建自签名的 SSL 证书:

    $ openssl req -x509 -nodes -newkey rsa:2048 -keyout nginx.key -out nginx.crt
    
    

    上述命令将生成两个文件,nginx.keynginx.crt。这些是我们将复制到 Web 服务器的文件。

  3. 将这些文件的内容添加到变量中,并创建group_vars/www文件:

    # file: group_vars/www
    ---
    nginx_ssl_cert_content: |
        -----BEGIN CERTIFICATE-----
        -----END CERTIFICATE-----
    nginx_ssl_key_content: |
        -----BEGIN PRIVATE KEY-----
        -----END PRIVATE KEY-----
    

    在上述示例中,我们只是添加了将要替换为密钥和证书实际内容的占位符。这些密钥和证书不应暴露在版本控制系统中。

  4. 让我们使用保险库加密此文件:

    $ ansible-vault encrypt group_vars/www
    Encryption successful
    
    

    由于我们已经在配置中提供了保险库密码的路径,因此 Ansible-vault 不会询问密码。

  5. 现在让我们创建模板,以添加这些密钥:

    # filename: roles/nginx/templates/nginx.crt.j2
    {{ nginx_ssl_cert_content }}
    
    # filename: roles/nginx/templates/nginx.key.j2
    {{ nginx_ssl_key_content }}
    
  6. 还要将一个虚拟主机config文件添加到 SSL 中:

    # filename: roles/nginx/templates/nginx.key.j2
    server {
      listen {{ nginx_port_ssl }};
      server_name {{ ansible_hostname }};
      ssl on;
      ssl_certificate {{ nginx_ssl_path }}/{{ nginx_ssl_cert_file }};
      ssl_certificate_key {{ nginx_ssl_path }}/{{ nginx_ssl_key_file }};
    
      location / {
        root {{ nginx_root }};
        index {{ nginx_index }};
      }
    }
    
  7. 我们还需要创建一个任务文件来配置 SSL 站点,该文件将创建所需的目录、文件和配置:

    ---
    # filename: roles/nginx/tasks/configure_ssl.yml
     - name: create ssl directory
        file: path="{{ nginx_ssl_path }}" state=directory owner=root group=root
     - name: add ssl key 
        template: src=nginx.key.j2 dest="{{ nginx_ssl_path }}/nginx.key" mode=0644
     - name: add ssl cert 
        template: src=nginx.crt.j2 dest="{{ nginx_ssl_path }}/nginx.crt" mode=0644
     - name: create ssl site configurations 
        template: src=default_ssl.conf.j2 dest="{{ nginx_ssl_path }}/default_ssl.conf" mode=0644
        notify:
        - restart nginx service
    
  8. 最后,让我们根据nginx_ssl var参数是否设置为 true 来选择性地调用此任务:

    # filename: roles/nginx/tasks/main.yml
     - include: configure_ssl.yml
        when: nginx_ssl
    
  9. 现在,按照以下方式运行 playbook:

    $ ansible-playbook -i customhosts  site.yml
    
    

这应该配置在端口443上运行的默认 SSL 站点,使用自签名证书。现在,您应该能够使用https安全协议打开 Web 服务器地址,如下所示:

为 Nginx 添加 SSL 支持

当然,由于我们的证书是自签名的,而不是由指定的认证机构提供的,应该显示警告。

复习问题

你认为你已经足够理解这一章了吗?尝试回答以下问题来测试你的理解:

  1. 为什么需要加密提供给 Ansible playbooks 的数据?

  2. AES 是什么,对称密钥密码是什么?

  3. 更新之前使用 vault 加密的文件的两种方法是什么?

  4. 添加到 Ansible 配置文件的参数,使其了解保险库密码文件的位置是什么?

摘要

在本章中,您学习了如何使用 Ansible-vault 对传递给 playbooks 的数据进行安全保护。我们从加密数据的需求开始,讲解了 vault 的工作原理以及它使用的密码。然后,我们开始深入了解 Ansible-vault 实用程序以及创建加密文件、解密、重新密钥等基本操作。您还学习了如何通过在持有数据库凭据的vars文件上运行 Ansible-vault 来加密现有文件。最后,我们为 Nginx 添加了 SSL 支持,您学会了如何使用 vault 安全地存储 Web 服务器的私钥和证书,并使用模板将它们复制。请注意,Ansible vault 提供了一种安全地向 Ansible 模块提供数据的方式。除了使用 vault 之外,还建议采取其他系统安全措施,这不在本文的讨论范围内。

在了解了 vault 之后,在下一章中,我们将开始学习使用 Ansible 管理多个环境(如开发、演示和生产)的各种方法。这些环境通常映射到软件开发工作流程。

第九章 管理环境

大多数组织在构建其基础架构时从单个环境开始。然而,随着复杂性的增长,我们必须有一个工作流程,涉及在开发环境中编写代码并对其进行测试,然后在预备或预生产环境中进行密集的 QA 循环,以确保代码在生产环境中的稳定性得到测试,然后我们最终发布它。为了模拟真实世界的行为,这些环境必须运行相同的应用程序堆栈,但很可能在不同的规模下运行。例如,预备环境将是生产的小规模副本,服务器较少,最常见的情况是,开发环境将在虚拟化环境中的个人工作站上运行。尽管所有这些环境都运行相同的应用程序堆栈,但它们必须彼此隔离,并且必须具有特定于环境的配置,如下所述:

  • dev 组中的应用程序不应指向预备中的数据库,反之亦然

  • 生产环境可能有自己的软件包存储库

  • 测试环境可能在端口 8080 上运行 Web 服务器,而其他所有环境都在端口 80 上运行

通过角色,我们可以创建一个模块化的代码来为所有环境配置相同的环境。 Ansible 的另一个重要特性是将代码与数据分开的能力。结合使用这两者,我们可以将基础架构建模成这样一种方式,我们可以创建特定于环境的配置,而无需修改角色。我们只需提供来自不同位置的变量即可创建它们。让我们来看一下下面的截图:

管理环境

前面的图示了同一组织内的三个不同环境,即开发、预备和生产环境。这三个环境都运行相同的应用程序堆栈,其中包括负载均衡器、Web 服务器和数据库服务器。但需要注意的两点是:

  • 每个环境根据其规模不同,可以配置运行一个或多个角色(例如,dbwww)的主机。

  • 每个环境都与其他环境隔离开来。生产环境中的 Web 服务器不会连接到预备环境中的数据库,反之亦然。

在本章中,我们将介绍以下主题:

  • 使用 Ansible 管理多个环境

  • 分隔不同环境的库存文件

  • 使用 group_varshost_vars 组指定特定于环境的配置

管理环境的方法

您已经了解到需要创建具有相同角色但具有不同数据的不同环境。在撰写本文时,使用 Ansible 管理此类多个环境场景的方法不止一种。我们将在这里讨论两种方法,并且您可以根据自己的判断选择其中之一或创建您自己的方法。没有明确的创建环境的方式,但是以下是 Ansible 的内置功能,可能会派上用场:

  • 使用清单将属于一个环境的主机分组并将它们与其他环境中的主机隔离开来

  • 使用清单变量,如group_varshost_vars组,提供特定于环境的变量

在我们继续之前,回顾一下适用于清单组、变量和优先规则的清单组将会很有用。

清单组和变量

您已经学习了 Ansible 清单遵循 INI 样式配置的需求,其中主机与方括号括起来的组标签一起组合,如下图所示:

清单组和变量

然后可以指定清单变量,以使其与这些组名称匹配,使用group_vars或在host_vars文件中匹配特定主机。除了这些组名称之外,还可以使用一个名为"all"的文件为group_varshost_vars文件指定默认变量,从而产生以下结构:

清单组和变量

在这种情况下,如果你在allwebserver文件中指定了相同的变量,那么更具体的变量将优先。这意味着,如果你在group_vars下的webserver组中重新定义了一个变量,而在all中也定义了它,那么参数的值将被设置为在webserver中定义的更具体的值。这是我们在下面的方法中利用的行为。

方法 1 – 使用清单中的嵌套组

除了能够使用 INI 样式创建组外,Ansible 还支持嵌套组,其中一个完整的组可以是另一个父组的一部分。第一种方法就是基于这个特性的,并且将逐步讨论,如下所示:

  1. 创建一个环境目录,用于存储特定环境的清单文件。最好以环境命名它们。添加属于该环境的主机并对它们进行分组。一个组可以根据任何标准进行分组,比如角色、位置、服务器机架等等。例如,创建一个名为"webservers"的组来添加所有的 Apache web 服务器,或者一个名为"in"的组来添加所有属于该位置的主机。

  2. 添加一个以环境名称命名的父组,例如,production、development、staging 等,并将属于该环境的所有其他组包括为子组。每个这样的组又包括一组主机,例如:

    [dev:children]
      webservers
      databases
    
  3. 现在,在group_vars/all文件中创建通用/默认组变量。然后,可以从特定于环境的文件中覆盖这些变量。

  4. 要指定环境特定的变量,请创建group_vars/{{env}}文件,如下所示:

    group_vars
      |_ all
      |_ dev
      |_ stage
    

这也将覆盖all组中的变量。以下图示了使用此方法创建的文件结构:

方法 1 – 在清单中使用嵌套组

创建完毕后,只需运行ansible-playbook命令即可调用特定于环境的清单。

例如,让我们看一下以下命令:

$ ansible-playbook -i environments/dev site.yml

方法 2 – 使用环境特定的清单变量

第二种方法不需要嵌套组,并依赖于 Ansible 的以下两个特性:

  • Ansible-playbook 的 -i 选项还接受一个目录,该目录可以包含一个或多个清单文件

  • 主机和组变量可以相对于清单文件,并且还可以相对于 Ansible 仓库根目录中的group_varshost_vars

这种方法将为每个环境创建完全隔离的变量文件。我们创建的文件结构如下图所示:

方法 2 – 使用环境特定的清单变量

以下是用于此方法的逐步方法:

  1. 在 Ansible 仓库的根目录下创建一个名为环境的目录。在此目录下,为每个环境创建一个目录。

  2. 每个环境目录包含两个内容:

    • 主机清单。

    • 清单变量,例如,group_varshost_vars。为了进行环境特定的更改,我们关注group_vars

  3. 每个环境都包含自己的group_vars目录,该目录又可以包含一个或多个文件,包括默认的all文件。没有两个环境共享这些变量。

提示

注意: 除了特定于环境的group_vars组外,还可以使用位于 Ansible-playbook 仓库顶部的group_vars文件。但是,建议不要在此方法中使用它,因为如果值相同,环境特定更改将被 playbook 的group_vars中的值覆盖。

使用此方法,可以针对特定环境启动 playbook,如下所示:

$ ansible-playbook -i environments/dev site.py

在这里,environments/dev是一个目录。

创建一个开发环境

在了解了如何管理环境之后,让我们尝试通过重构现有代码并创建一个 dev 环境来实践一下。为了测试它,让我们创建一个名为"env_name"的变量,并将 Nginx 的默认页面动态使用该变量并打印环境名称。然后,我们将尝试从环境中覆盖此变量。让我们看看以下步骤:

  1. 让我们从设置默认变量开始:

    #group_vars/all
    env_name: default
    
  2. 然后,在roles/nginx/tasks/configure.yml文件中,将 Nginx 任务更改为使用模板而不是静态文件,因此进行以下修改:

     - name: create home page for default site
        copy: src=index.html dest=/usr/share/nginx/html/index.html
    

    将其修改为以下代码:

     - name: create home page for default site
       template:
         src: index.html.j2
         dest: /usr/share/nginx/html/index.html
    
  3. 现在,让我们尝试运行 playbook 而不创建环境:

    $ ansible-playbook -i customhosts www.yml
    
    
  4. 运行完成后,让我们检查默认网页:创建开发环境

  5. 它打印了我们从group_vars/all文件中设置的变量的值,默认值。

  6. 现在,让我们创建一个文件,以便我们可以管理dev环境。由于我们将使用相同的一组主机,因此我们可以将现有的清单转换为 dev,并在环境名称后添加一个父组:

    $ mkdir environments/
    $ mv customhosts environments/dev 
     [ edit  environments/dev ]
    
    
  7. 将所有组添加到dev环境中,如下所示:

    [dev:children]
    db
    www
    lb
    

    清单文件如下所示,我们必须进行以下更改:

    1. 现在,让我们为dev环境创建一个group_vars文件,并覆盖环境名称:

        #file: environments/dev
      env_name: dev
      
    2. 这一次,我们将以以下方式运行 playbook:

      $ ansible-playbook -i environments/dev www.yml
      
      

    我们将看到以下截图作为输出:

    创建开发环境

复习问题

您是否认为自己已经足够了解本章了?尝试回答以下问题来测试您的理解:

  1. 如何为同一环境指定多个主机清单?

  2. 如果您在environments/dev/group_vars/all文件中定义了一个变量,同时在group_vars/all文件中也定义了相同的变量,哪个会优先?

  3. 如何在主机清单文件中创建一组组?

概要

在本章中,您学习了如何创建与软件开发工作流程或阶段相对应的多个环境。我们从清单组和清单变量的简要概述开始,特别是group_vars文件。接着是用于管理环境的两种方法。最后,我们重构了我们的代码,继续创建了dev环境,并通过覆盖环境中的一个变量进行了测试。在下一章中,您将学习有关基础设施编排以及 Ansible 在编排复杂基础设施工作流程、零停机部署等方面的优势。

第十章。 使用 Ansible 编排基础架构

在不同的情况下使用编排可能意味着不同的事情。以下是一些编排场景的描述:

  • 在一组主机上并行运行临时命令,例如,使用 for 循环遍历一组 Web 服务器以重新启动 Apache 服务。这是最原始的编排形式。

  • 调用编排引擎启动另一个配置管理工具以确保正确的顺序。

  • 以特定顺序配置多层应用程序基础设施,并能够对每个步骤进行精细控制,并且在配置多个组件时具有来回移动的灵活性。例如,安装数据库,设置 Web 服务器,返回数据库,创建模式,转到 Web 服务器以启动服务等。

大多数实际场景与最后一个场景相似,涉及多层应用堆栈和多个环境,重要的是按照一定顺序和协调的方式启动和更新节点。在继续下一步之前测试应用程序是否正常运行也很有用。首次设置堆栈与推送更新的工作流可能不同。有时您可能不希望立即更新所有服务器,而是分批处理以避免停机时间。

在本章中,我们将介绍以下主题:

  • 编排场景

  • 将 Ansible 用作基础架构编排引擎

  • 实施滚动更新

  • 使用标签、限制和模式

  • 将测试构建到剧本中

Ansible 作为编排器

在任何编排情景下,Ansible 都比其他工具更加出色。当然,正如 Ansible 的创建者所说,它不仅是一个配置管理工具,这是真的。 Ansible 可以在前面讨论的任何编排场景中找到自己的位置。它旨在管理复杂的多层部署。即使您的基础架构已经使用其他配置管理工具自动化了,您也可以考虑使用 Ansible 来编排这些工具。

让我们讨论 Ansible 提供的具体功能,这些功能对编排非常有用。

多个剧本和顺序

与大多数其他配置管理系统不同,Ansible 支持在不同时间运行不同的剧本来配置或管理相同的基础架构。您可以创建一个剧本来首次设置应用程序堆栈,另一个剧本按照一定的方式推送更新。剧本的另一个属性是它可以包含多个播放,这允许将应用程序堆栈中每个层的主机分组,并同时对其进行配置。

预任务和后任务

我们之前使用过前置任务和后置任务,在编排过程中非常相关,因为这些任务允许我们在运行播放之前和之后执行任务或运行验证。让我们以更新注册在负载均衡器上的 Web 服务器为例。使用前置任务,可以将 Web 服务器从负载均衡器中移除,然后将角色应用于 Web 服务器以推送更新,随后是后置任务,将 Web 服务器重新注册到负载均衡器中。此外,如果这些服务器由 Nagios 监控,可以在更新过程中禁用警报,然后使用前置任务和后置任务自动重新启用。这可以避免监控工具可能以警报的形式产生的噪音。

委托

如果你希望任务选择性地在某一类主机上运行,特别是当前播放范围之外的主机,Ansible 的委托功能会很方便。这与之前讨论的场景相关,并且通常与前置任务和后置任务一起使用。例如,在更新 Web 服务器之前,需要将其从负载均衡器中注销。现在,这个任务应该在播放范围之外的负载均衡器上运行。可以通过使用委托功能来解决这个问题。使用前置任务时,可以使用 delegate_to 关键字在负载均衡器上启动脚本,执行注销操作,如下所示:

- name: deregister web server from lb
  shell: < script to run on lb host >
  delegate_to: lbIf there areis more than one load balancers, anan inventory group can be iterated over as, follows: 
- name: deregister web server from lb
  shell: < script to run on lb host >
  delegate_to: "{{ item }}"
  with_items: groups.lb

滚动更新

这也被称为批量更新或零停机更新。假设我们有 100 个需要更新的 Web 服务器。如果我们在清单中定义它们并针对它们启动 playbook,Ansible 将同时开始更新所有主机。这也可能导致停机时间。为了避免完全停机并实现无缝更新,有意义的做法是分批更新,例如,每次更新 20 个。在运行 playbook 时,可以使用 serial 关键字指定批处理大小。让我们看一下以下代码片段:

- hosts: www
  remote_user: vagrant
  sudo: yes
  serial: 20 

测试

在编排过程中,不仅要按顺序配置应用程序,还要确保它们实际启动并按预期工作。Ansible 模块,如 wait_foruri,可以帮助您将这些测试构建到 playbooks 中,例如:

- name: wait for mysql to be up
  wait_for: host=db.example.org port=3106 state=started
- name: check if a uri returns content
  uri: url=http://{{ inventory_hostname }}/api
  register: apicheck

wait_for 模块可以额外用于测试文件的存在。当你希望在继续之前等待服务可用时,它也非常有用。

标签

Ansible play 将角色映射到特定的主机。在运行 play 时,会执行从主要任务调用的整个逻辑。在编排时,我们可能只需要根据我们想要将基础架构带入的阶段来运行部分任务。一个例子是 zookeeper 集群,重要的是同时启动集群中的所有节点,或者在几秒钟的间隔内。Ansible 可以通过两阶段执行来轻松地实现这一点。在第一阶段,您可以在所有节点上安装和配置应用程序,但不启动它。第二阶段涉及几乎同时在所有节点上启动应用程序。这可以通过给个别任务打标签来实现,例如,configure、install、service 等。

举个例子,让我们来看下面的屏幕截图:

标签

在运行 playbook 时,所有具有特定标签的任务可以使用--tags来调用,如下所示:

$ Ansible-playbook -i customhosts site.yml –-tags install

标签不仅可以应用于任务,还可以应用于角色,如下所示:

{ role: nginx, when: Ansible_os_family == 'Debian', tags: 'www' }

如果一个特定的任务需要始终执行,即使通过标签进行过滤,使用一个名为always的特殊标签。这将使任务执行,除非使用了覆盖选项,比如--skip-tags always

模式和限制

限制可以用来在主机的一个子集上运行任务,这些主机是通过模式进行筛选的。例如,以下代码将仅在属于db组的主机上运行任务:

$ Ansible-playbook -i customhosts site.yml --limit db

模式通常包含一组要包括或排除的主机。可以指定一个以上的模式组合,如下所示:

$ Ansible-playbook -i customhosts site.yml --limit db,lb

使用冒号作为分隔符可以进一步过滤主机。以下命令将在除了属于wwwdb组的主机之外的所有主机上运行任务:

$ Ansible-playbook -i customhosts site.yml --limit 'all:!www:!db'

请注意,通常这需要用引号括起来。在这个模式中,我们使用了all组,该组匹配清单中的所有主机,并且可以用*替代。接着是!来排除db组中的主机。这个命令的输出如下,显示了由于先前使用的过滤器,名称为dbwww的 play 被跳过了,因为没有主机匹配:

模式和限制

现在让我们看看这些编排特性是如何运作的。我们将首先给角色打上标签,进而进行多阶段执行,然后编写一个新的 playbook 来管理对 WordPress 应用程序的更新。

给角色打标签

现在让我们开始给之前创建的角色打上标签。我们将创建以下标签,这些标签对应着应用程序管理中的阶段:

  • 安装

  • 配置

  • 开始

这是给haproxy角色添加标签的例子。为了避免冗余,将其他角色加上标签的操作从文本中排除掉。我们可以给角色内的任务添加标签,或者在 playbook 中给整个角色加上标签。让我们从给任务加标签开始:

---
# filename: roles/haproxy/tasks/install.yml
  - name: install haproxy
    apt:
      name: "{{ haproxy['pkg'] }}"
    tags:
     - install

---
# filename: roles/haproxy/tasks/configure.yml
 - name: create haproxy config
    template: src="img/haproxy.cfg.j2" dest="{{ haproxy['config']['cnfpath'] }}" mode=0644
   notify:
    - restart haproxy service
   tags:
    - configure

 - name: enable haproxy
    template: src="img/haproxy.default.j2" dest=/and more/default/haproxy mode=0644
    notify:
    - restart haproxy service
    tags:
    - configure

---
# filename: roles/haproxy/tasks/service.yml
 - name: start haproxy server
    service:
      name: "{{ haproxy['service'] }}" 
      state: started
    tags:
    - start

在角色中打上标签后,我们还会在 playbooks 中打上角色的标签,如下所示:

# filename: db.yml
  roles:
- { role: mysql, tags: 'mysql' }

#filename: www.yml
  roles:
     - { role: nginx, when: Ansible_os_family == 'Debian', tags: [ 'www', 'nginx' ] }
     - { role: php5-fpm, tags: [ 'www', 'php5-fpm' ] }
     - { role: wordpress, tags: [ 'www', 'wordpress' ] }

#filename: lb.yml
  roles:
- { role: haproxy, when: Ansible_os_family == 'Debian', tags: 'haproxy' }

应用后,我们主要 playbook 的标签可以列举如下:

$ Ansible-playbook -i customhosts site.yml --list-tags

#Output:
playbook: site.yml

 play #1 (db): TAGS: []
 TASK TAGS: [configure, install, mysql, start]

 play #2 (www): TAGS: []
 TASK TAGS: [configure, install, nginx, php5-fpm, ssl, start, wordpress, www]

 play #3 (lb): TAGS: []
 TASK TAGS: [configure, haproxy, install, start]

使用标签和限制的组合使我们能够在 playbook 运行中精细控制执行的内容,例如:

# Run install tasks for haproxy, 
$ Ansible-playbook -i customhosts site.yml --tags=install --limit lb

# Install and configure all but web servers
$ Ansible-playbook -i customhosts site.yml --tags=install,configure --limit 'all:!www'

# Run all tasks with tag nginx
$ Ansible-playbook -i customhosts site.yml --tags=nginx

为 WordPress 创建一个编排 playbook

我们有一个站点范围的 playbook,即site.yml文件,该文件用于安装和配置完整的 WordPress 堆栈。然而,要实现无停机更新应用程序以及部署新版本,site.yml文件并不是理想的 playbook。我们希望遵循一个涉及以下步骤的工作流程:

  1. 逐个更新 Web 服务器。这将避免任何停机时间。

  2. 在更新之前,从 haproxy 负载均衡器中注销 Web 服务器。这将停止流量流向 Web 服务器,以避免停机时间。

  3. 运行与 WordPress 应用程序相关的角色,即 Nginx、php5-fpm 和 WordPress。

  4. 确保 Web 服务器正在运行并监听端口 80。

  5. 将服务器重新注册到 haproxy 并重新开始发送流量。

让我们创建一个名为update.yml的 playbook,它正如之前解释的一样进行编排,并且使用了本章前面讨论的大部分功能。以下是这个 playbook:

 ---
# Playbook for updating web server in batches
# filename: update_www.yml
- hosts: www
  remote_user: vagrant
  sudo: yes
  serial: 1
  pre_tasks:
    - name: deregister web server from  load balancer
    shell: echo "disable server fifanews/{{ Ansible_hostname }}" | socat stdio /var/lib/haproxystats
    delegate_to: "{{ item }}"
    with_items: groups.lb
  roles:
    - { role: nginx, when: Ansible_os_family == 'Debian' }
    - php5-fpm
    - wordpress
  post_tasks:
    - name: wait for web server to come up 
    wait_for: host={{ inventory_hostname }} port=80 state=started
    - name: register webserver from  load balancer
    shell: echo "enable server fifanews/{{ Ansible_hostname }}" | socat stdio /var/lib/haproxystats
    delegate_to: "{{ item }}"
    with_items: groups.lb

让我们分析这段代码:

  • playbook 只包含一个 play,该 play 在属于www 组的主机上运行。

  • serial 关键字指定批大小,并允许无停机滚动更新。在我们的情况下,由于主机较少,我们选择逐个更新一个 Web 服务器。

  • 在应用该角色之前,使用预任务部分从负载平衡器中注销主机,该部分运行一个带有socat的 shell 命令。这在所有负载平衡器上使用delegate关键字运行。Socat 是类似于并且功能更为丰富的 Unix 实用程序(nc)。

  • 在注销主机后,应用角色;这将更新 Web 服务器的配置或部署新代码。

  • 更新后,执行后任务,首先等待 Web 服务器启动并监听端口 80,只有在 Web 服务器准备就绪时,才将其重新注册到负载平衡器。

复习问题

你认为你已经足够了解本章了吗?尝试回答以下问题来测试你的理解:

  1. 是否可能使用 Ansible 来编排另一个配置管理工具?

  2. 如何使用 Ansible 实现无停机部署应用程序?

  3. --limit命令对 Ansible playbook 有什么作用?

  4. 如何在 playbook 中针对给定角色运行任务的子集?

  5. 使用预任务和后任务的目的是什么?

  6. 可以使用哪些模块来从 playbook 运行测试?

  7. always标签为何如此特殊?

总结

我们在本章开始时讨论了编排是什么,不同的编排场景是什么,以及 Ansible 如何适应其中。您了解了 Ansible 在编排背景下的一系列丰富功能。这包括多 playbook 支持、预任务和后任务、标签和限制、运行测试等等。我们继续为之前创建的角色打标签,并学习如何使用标签、模式和限制的组合控制代码在哪些机器上运行的部分。最后,我们创建了一个新的 playbook 来编排工作流,更新 Web 服务器,其中包括零停机部署、委托、预任务和后任务以及测试。您还了解到 Ansible 可以适用于任何编排场景中。

这就是本书的结尾。在结束之前,我代表审阅者、编辑、贡献者和出版团队的其他成员,感谢您将本书视为您成为 Ansible 实践者的伴侣之一。

我们希望您现在已经熟悉了 Ansible 提供的各种原语,用于自动化常见基础设施任务、创建动态角色、管理多层应用程序配置、零停机部署、编排复杂基础设施等。我们希望您能够应用本书中所学知识创建有效的 Ansible playbook。

附录 A. 参考资料

有关 Ansible 的更多信息,请参阅以下网址:

posted @ 2024-05-20 11:58  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报