Ansible-Playbook-精要-全-

Ansible Playbook 精要(全)

原文:annas-archive.org/md5/f3d5d082c2c7cd8c77793dee22b4cf30

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

随着云计算的发展、敏捷开发方法的应用,以及近年来数据量的爆炸性增长,对大规模管理基础设施的需求也与日俱增。DevOps 工具和实践已成为自动化管理这种可扩展、动态且复杂的基础设施的必需品。配置管理工具是这一 DevOps 工具集的核心。

Ansible 是一个简单、高效、快速的配置管理、编排和应用部署工具,集多种功能于一身。本书帮助你熟悉编写 Playbook,Ansible 的自动化语言。本书采取实践方法,向你展示如何创建灵活、动态、可重用且基于数据驱动的角色。接下来,本书介绍了 Ansible 的高级功能,如节点发现、集群、通过 Vault 安全存储数据以及管理环境,最后展示如何使用 Ansible 编排多层基础设施堆栈。

本书内容

第一章,构建你的基础设施蓝图,将向你介绍 Playbook、YAML 等内容。你还将学习 Playbook 的组成部分。

第二章,使用 Ansible 角色实现模块化,将演示如何使用 Ansible 角色创建可复用的模块化自动化代码,角色是自动化的单元。

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

第四章,引入你的代码 – 自定义命令和脚本,介绍了如何将现有脚本引入并使用 Ansible 执行 Shell 命令。

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

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

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

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

第九章,管理环境,介绍了如何使用 Ansible 创建和管理隔离的环境,并将自动化代码映射到软件开发工作流中。

第十章,使用 Ansible 编排基础设施,介绍了 Ansible 的编排功能,例如滚动更新、预任务和后任务、标签、将测试构建到 playbook 中等内容。

本书所需的内容

本书假设你已经安装了 Ansible 并且熟悉 Linux/Unix 环境及系统操作,且对命令行界面也很熟悉。

本书适合谁阅读

本书的目标读者是有几年经验的系统或自动化工程师,负责管理基础设施的各个部分,包括操作系统、应用配置和部署。本书也面向那些希望以最短的学习曲线,自动化地管理系统和应用配置的任何人。

假设读者已对 Ansible 有一定的概念理解,已安装 Ansible,并熟悉基本操作,例如创建库存文件和使用 Ansible 运行临时命令。

规范

本书中,你会看到几种不同样式的文本,它们区分了不同种类的信息。以下是这些样式的一些示例及其含义的解释。

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

一段代码块的设置如下:

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

任何命令行输入或输出都如下所示:

$ ansible-playbook simple_playbook.yml -i customhosts

新术语重要词汇 以粗体显示。你在屏幕上看到的单词,例如菜单或对话框中的单词,通常会像这样出现在文本中:“结果变量哈希应包含默认值和来自变量的覆盖值”。

注意

警告或重要的说明会以像这样的框显示。

提示

小技巧和窍门以这种形式出现。

读者反馈

我们始终欢迎读者反馈。告诉我们你对本书的看法——喜欢哪些部分或不喜欢哪些部分。读者反馈对我们至关重要,帮助我们开发出更符合你需求的书籍。

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

如果你在某个主题上有专业知识,并且有兴趣撰写或贡献书籍,请查看我们作者指南:www.packtpub.com/authors

客户支持

现在你已经成为一本 Packt 书籍的骄傲拥有者,我们为你提供了许多资源,帮助你充分利用你的购买。

下载示例代码

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

勘误

尽管我们已尽力确保内容的准确性,但难免会出现错误。如果您在我们的书籍中发现错误——可能是文本或代码错误——我们将非常感激您向我们报告。这样,您可以帮助其他读者避免困扰,并帮助我们改进后续版本的书籍。如果您发现任何勘误,请访问www.packtpub.com/submit-errata报告,选择您的书籍,点击勘误提交表单链接,并填写勘误的详细信息。一旦您的勘误被验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该书籍的现有勘误列表中,显示在该书的勘误部分。您可以通过访问www.packtpub.com/support查看任何现有的勘误。

盗版

网络上侵犯版权的盗版问题在所有媒体中普遍存在。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上发现我们作品的任何非法复制,无论形式如何,请立即提供该位置地址或网站名称,以便我们采取措施。

如果您发现涉嫌盗版的材料,请通过<copyright@packtpub.com>联系我们,并附上该材料的链接。

我们感谢您的帮助,保护我们的作者,并帮助我们继续为您提供有价值的内容。

问题

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

第一章:设置学习环境

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

在本节中,我们将覆盖以下主题:

  • 理解学习环境

  • 理解先决条件

  • 安装和配置 VirtualBox 和 Vagrant

  • 创建虚拟机

  • 安装 Ansible

  • 使用示例代码

理解学习环境

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

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

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

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

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

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

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

先决条件

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

github.com/schoolofdevops/ansible-playbook-essentials

系统先决条件

一台适度配置的台式机或笔记本电脑系统应该足够用来设置学习环境。以下是软件和硬件方面的推荐先决条件:

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

基础软件

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

  • VirtualBox:Oracle 的 VirtualBox 是一款桌面虚拟化软件,可以免费使用。它支持多种操作系统,包括 Windows、OS X、Linux、FreeBSD、Solaris 等。它提供了一个虚拟机管理程序层,并允许用户在现有操作系统上创建并运行虚拟机。与本书一起提供的代码已在 VirtualBox 4.3x 版本上进行了测试。然而,任何与 Vagrant 版本兼容的 VirtualBox 版本都可以使用。

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

  • Git for Windows:尽管我们不打算使用 Git(一款版本控制软件),但我们使用此软件来在 Windows 系统上安装 SSH 工具。Vagrant 需要在路径中提供 SSH 二进制文件。Windows 系统并不自带 SSH 工具,而 Git for Windows 是在 Windows 上安装 SSH 工具的最简便方式。也有其他选项,如 Cygwin

下表列出了用于开发本书附带代码的软件的操作系统版本及其下载链接:

软件 版本 下载链接
VirtualBox 4.3.30 www.virtualbox.org/wiki/Downloads
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,请参考以下步骤。这些说明特定于我们在控制器上使用的 Ubuntu Linux 发行版。有关通用安装说明,请参考以下页面:

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
    
    

使用示例代码

本书提供的示例代码按章节编号进行划分。以章节编号命名的目录中包含该章节末尾代码状态的快照。建议学习者独立编写代码,并将示例代码作为参考。此外,如果读者跳过一个或多个章节,他们可以使用上一章节的示例代码作为基础。

例如,在使用 第六章,迭代控制结构 – 循环 时,您可以使用 第五章,控制执行流 – 条件语句 的示例代码作为基础。

提示

下载示例代码

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

第一章:蓝图化你的基础设施

本书是为任何具备 Ansible 概念性知识的人准备的入门书籍,旨在帮助读者开始编写 Ansible playbooks,自动化常见的基础设施任务,编排应用程序部署,并/或管理跨多个环境的配置。本书采用逐步深入的方式,从学习 playbook 的构成和编写简单角色来创建模块化代码等基础知识开始。掌握基础后,将介绍如何通过变量和模板添加动态数据,并使用条件语句和迭代器控制执行流程。接下来是一些更高级的主题,如节点发现、集群、加密数据和环境管理。最后,我们将讨论 Ansible 的编排功能。让我们从学习 playbook 开始,迈出成为 Ansible 从业者的第一步。

本章我们将学习:

  • playbook 的结构

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

  • Ansible 模块及其“开箱即用”的方法

了解 Ansible

Ansible 是一个简单、灵活且功能极其强大的工具,能够让你自动化常见的基础设施任务、运行临时命令以及部署跨多个机器的多层应用程序。尽管你可以使用 Ansible 在多个主机上并行执行命令,但真正的力量在于使用 playbook 来管理这些主机。

作为系统工程师,我们通常需要自动化的基础设施包含复杂的多层应用程序。每个应用程序代表一种服务器类别,例如负载均衡器、Web 服务器、数据库服务器、缓存应用程序和中间件队列。由于这些应用程序往往需要协同工作以提供服务,因此也涉及拓扑结构。例如,负载均衡器会连接到 Web 服务器,Web 服务器会读写数据库,并连接到缓存服务器以获取内存中的对象。大多数情况下,当我们启动这种应用栈时,需要按特定顺序配置这些组件。

这是一个非常常见的三层 Web 应用示例,运行负载均衡器、Web 服务器和数据库后端:

了解 Ansible

Ansible 让你将这个图示转换为蓝图,定义你的基础设施策略。指定这些策略的格式就是 playbook。

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

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

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

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

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

以下是一个示例 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 包含一个 plays 列表;它们由"- "表示。每个 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 都由以下两个重要部分组成:

  • 要配置的内容:我们需要配置一个主机或主机组来运行该 play。还需要包括有用的连接信息,如连接时使用的用户、是否使用sudo命令等。

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

现在,让我们简要查看一下这些内容。

创建主机清单

在开始编写 Ansible playbook 之前,我们需要定义一个包含所有需要配置的主机的清单,并使其可以供 Ansible 使用。稍后,我们将开始针对这个清单中的部分主机运行 play。如果你已有现有清单,如 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

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

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

  • 清单文件遵循 INI 风格的配置,这实际上包括以 "[ ]" 包含的主机组/类名称开始的配置块。这样可以选择性地在系统类上执行操作,例如,[namenodes]

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

  • 每个组包含一组主机和连接详细信息,如连接时使用的 SSH 用户、如果端口非默认的 SSH 端口号、SSH 凭证/密钥、sudo 凭证等。主机名还可以包含通配符、范围等,便于包括多个具有相同类型、遵循某些命名模式的主机。

提示

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

模式

在前面的 playbook 中,以下行决定了选择哪些主机来运行特定的 play:

- hosts: all
- hosts: www

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

模式可以是以下任意一种或它们的组合:

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

任务

Play 将主机映射到任务。任务是一系列针对与 Play 中指定模式匹配的主机执行的操作。每个 Play 通常包含多个任务,这些任务在与模式匹配的每台机器上按顺序运行。例如,看看以下代码片段:

- 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 与其他配置管理工具的不同之处在于它的“一体化”方式。这些“一体化的电池”就是“模块”。在继续之前,理解模块是什么非常重要。

模块

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

考虑以下示例:

  • Debian 的apt模块和 RedHat 的yum模块有助于管理系统包

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

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

模块将实际的实现从用户中抽象出来。它们暴露了一种声明性语法,接受管理系统组件的参数和状态列表。所有这些都可以使用人类可读的 YAML 语法声明,采用键值对的形式。

就功能而言,模块类似于那些熟悉 Chef/Puppet 软件的用户中的提供者。与其编写程序来创建用户,使用 Ansible 时,我们声明组件应该处于的状态,即要创建的用户、用户状态和特性,如 UID、组、Shell 等。实际的操作通过模块被 Ansible 固有地识别,并在后台执行。

提示

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

Ansible 预装了一个模块库,从管理基本系统资源的模块到更复杂的模块,如发送通知、进行云集成等。如果你想为 ec2 实例配置、在远程 PostgreSQL 服务器上创建数据库,并在IRC上接收通知,那么 Ansible 已经为你准备好了相关模块。这是不是很棒?

无需担心寻找外部插件,或为与云服务提供商的集成而烦恼。要查看可用模块的列表,可以参考 Ansible 文档:docs.ansible.com/list_of_all_modules.html

Ansible 也是可扩展的。如果你找不到合适的模块来完成工作,写一个模块非常简单,而且不一定要用 Python。可以用你选择的语言为 Ansible 编写模块。详细信息可以参考:docs.ansible.com/developing_modules.html

模块和幂等性

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

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

  • 对于每次随后的运行,除非上游仓库有新版本的软件包,否则它将跳过安装部分。

这允许多次执行相同任务而不会导致错误状态。大多数 Ansible 模块都是幂等的,只有commandshell模块例外。用户需要使这些模块具备幂等性。

运行剧本

Ansible 自带ansible-playbook命令来启动剧本。现在让我们运行我们创建的任务:

$ ansible-playbook simple_playbook.yml -i customhosts

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

  • ansible-playbook参数是将剧本作为参数(simple_playbook.yml)并在主机上执行任务的命令

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

  • customhosts参数是我们主机的清单,它让 Ansible 知道哪些主机或主机组需要执行任务

启动上述命令将开始调用任务,按照我们在剧本中描述的顺序进行编排。以下是执行上述命令后的输出:

运行剧本

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

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

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

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

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

  • 然后,Ansible 进入下一个 play。这个 play 只在一个主机上执行,因为我们在 play 中指定了 "hosts:www",而我们的清单中 "www" 组内只有一个主机。

  • 在第二个 play 中,添加了 Nginx 仓库,安装了包,并启动了服务。

  • 最后,Ansible 在 "PLAY RECAP" 部分打印 playbook 执行的总结。它会显示有多少修改被执行,如果任何主机无法访问,或者是否有任何系统执行失败。

提示

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

复习问题

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

  1. 在模块中,幂等性是什么意思?

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

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

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

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

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

总结

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

在接下来的章节中,我们将开始重构我们的代码,创建可重用和模块化的代码块,并将它们称为角色(roles)。

第二章:使用 Ansible 角色实现模块化

在上一章中,您学习了如何使用 Ansible 编写简单的剧本。您还了解了将主机映射到任务的剧本概念。在单个剧本中编写任务可能对于非常简单的设置来说效果不错。然而,如果我们有多个跨多个主机的应用程序,这将很快变得不可管理。

在本章中,您将了解以下概念:

  • 角色是什么,角色的用途是什么?

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

  • 组织内容以提供模块化

  • 使用 include 语句

  • 编写简单的任务和处理器

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

理解角色

在实际场景中,我们大多数情况下配置的是 Web 服务器、数据库服务器、负载均衡器、中间件队列等。如果退后一步看整体情况,您会发现您实际上是在以可重复的方式配置一组相同的服务器。

为了最有效地管理这样的基础设施,我们需要一些抽象机制,允许我们定义在这些组中每个需要配置的内容,并按名称调用它们。这正是角色的作用。Ansible 角色允许我们同时配置一组节点,而无需重复自己。角色还提供了一种创建模块化代码的方式,这样的代码可以共享和重用。

命名角色

一种常见的做法是创建与您想要配置的每个应用程序或基础设施组件相对应的角色。例如:

  • Nginx

  • MySQL

  • MongoDB

  • Tomcat

角色的目录布局

角色就是以特定方式布局的目录。角色遵循预定义的目录布局约定,并期望每个组件都在它应该在的路径下。

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

角色的目录布局

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

  • 每个角色包含一个以其自身命名的目录,例如Nginx,其父目录为roles/。每个命名的角色目录包含一个或多个可选的子目录。最常见的子目录包括任务、模板和处理器。每个子目录通常包含main.yml文件,这是默认文件。

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

  • 仅靠任务本身无法完成所有工作。考虑到我们与电影的类比,故事没有配角是完整的。主角有朋友、汽车、爱人和反派来完成故事。同样,任务需要数据、调用静态或动态文件、触发操作等。这就是 files、handlers、templates、defaults 和vars的作用。让我们看看这些是做什么用的。

  • Vars 和 defaults 提供关于你的应用程序/角色的数据,例如你的服务器应该运行在哪个端口、存储应用程序数据的路径、哪个用户应当运行服务等。默认变量在版本 1.3 中引入,这些变量允许我们提供合理的默认值。之后可以从其他地方覆盖这些默认值,例如varsgroup_varshost_vars。变量会合并,并且优先级规则适用。这给我们提供了很大的灵活性来选择性地配置我们的服务器。例如,除了处于预发布环境的主机外,所有主机都应该在80端口上运行 Web 服务器,而预发布环境的主机则应在8080端口上运行。

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

  • 任务可以根据状态或条件的变化触发某些操作。在电影中,主角可能会基于挑衅或某个事件追逐反派并报仇。一个例子事件是绑架主角心爱的女士。同样,你可能需要在主机上执行某个操作,例如基于先前发生的事情重新启动服务,这可能是配置文件状态的变化。这种触发-动作关系可以通过处理器(handler)来指定。

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

让我们动手实践一下,创建一个 Nginx 应用程序的角色。我们来设定一个问题陈述,尝试解决它,并在这个过程中学习角色。

考虑以下场景。在世界杯的到来之际,我们需要创建一个 Web 服务器来提供体育新闻页面。

作为敏捷方法论的追随者,我们将分阶段进行。在第一阶段,我们将只安装一个 web 服务器并提供首页。现在,让我们把它分解成我们需要采取的步骤:

  1. 安装一个 web 服务器。在本例中,我们将使用'Nginx',因为它是一个轻量级的 web 服务器。

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

  3. 安装 web 服务器后启动它。

  4. 复制一个 HTML 文件,它将作为首页进行展示。

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

  • 安装 Nginx = 包管理模块 (apt)

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

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

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

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

创建一个全站剧本,进行嵌套并使用包含语句。

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

  • 当我们开始向这个单一文件中添加任务、变量和处理程序时,它会迅速失控。维护这样的代码将变成一场噩梦。

  • 这样做也会使得代码的重用和共享变得困难。使用像 Ansible 这样的工具的一个优势是它能够将数据与代码分离。数据是特定于组织的,而代码是通用的。这些通用代码可以与他人共享。然而,如果你把所有东西写在一个文件中,就无法做到这一点。

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

  • 我们将为需要配置的每个应用程序创建角色。在本例中,它是 Nginx。

  • 我们的 web 服务器除了 Nginx 之外,可能还需要安装其他应用程序,例如 PHP 和 OpenSSL。为了封装这些内容,我们将创建一个名为www.yml的剧本。

  • 我们创建的前述剧本将映射具有 Nginx 角色的主机。我们以后可能会向其中添加更多角色。

  • 我们将把这个剧本添加到顶层剧本中,也就是site.yml

以下图示以非常简单的方式描绘了前述步骤:

创建全站剧本,嵌套并使用包含语句

这是我们的site.yml文件:

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

前述我们创建的include指令帮助我们模块化代码。与其把所有内容都写在一个文件中,我们将逻辑拆分并导入所需内容。在这种情况下,我们将包含另一个剧本,它被称为嵌套剧本

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

  • include指令可以用来包含任务、处理程序甚至其他剧本

  • 如果你像在site.yml文件中那样将一个剧本包含在另一个剧本中,你不能替换变量。

  • include 关键字可以与常规的任务/处理程序规范一起使用

  • 可以使用 include 语句传递参数。这被称为 参数化的 include

提示

角色和自动包含

角色有隐式规则来自动包含文件。只要遵循目录布局规范,就可以确保所有任务、处理程序及其他文件都自动包含。因此,创建子目录时必须严格按照 Ansible 指定的名称。

创建 www 剧本

我们创建了一个全站的剧本,并使用 include 语句调用另一个名为 www.yml 的剧本。现在我们将创建这个文件,其中包含一个剧本,将我们的 Web 服务器主机映射到 Nginx 角色:

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

上面的代码工作原理如下:

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

  • 对于 roles/nginx/* 文件中的每个目录,将 roles/nginx/*/main.yml 包含到剧本中。这包括 taskshandlersvarsmetadefault 等文件。这就是自动包含规则的应用场所。

默认和自定义角色路径

默认情况下,Ansible 会查看我们为项目创建剧本时的 roles/ 子目录。作为顶尖的 DevOps 工程师,我们将遵循最佳实践,建立一个集中式的版本控制库来存储所有的角色。我们也可能会重用社区创建的角色。这样一来,我们可以在多个项目中复用这些角色。在这种情况下,我们会在一个或多个位置检出代码,例如:

  • /deploy/ansible/roles

  • /deploy/ansible/community/roles

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

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

参数化角色

有时,我们可能需要覆盖在 vars 或角色的默认目录中指定的默认参数,例如,在端口 8080 上运行 Web 服务器,而不是 80。此时,我们也可以在前面的剧本中将参数传递给角色,如下所示:

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

创建基本角色

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

重构我们的代码 – 创建基本角色

我们在 simple_playbook.yml 文件中编写了两个剧本。我们打算在所有主机上运行第一个剧本。这个剧本包含创建用户、安装基本包等任务:

重构我们的代码 – 创建基本角色

结合所有这些基本任务并创建一个基本角色是一个好习惯。你可以将其命名为 base、common、essential 等,但概念是相同的。现在我们将把这段代码移动到基本角色中:

  1. 为基本角色创建目录布局。由于我们只打算指定任务,因此只需要在基本角色中创建一个子目录:

    $ 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 网站配置。该文件应包含端口、服务器名称和网站根目录配置等参数,如下所示:

#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

我们还将更新 tasks 中的 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 监听的端口从默认的 80 改为 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 设置都有独立的任务来管理。角色布局如下:

使用处理器自动化事件和动作

pre-taskspost-tasks添加到剧本中

我们希望在应用 Nginx 之前和之后打印状态信息。让我们将其添加到www.yml剧本中,使用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 的任何模块来创建任务,这些任务可以在应用角色之前或之后运行。

使用角色运行剧本

现在让我们将重构后的代码应用到我们的主机上。我们将仅启动全站剧本,即site.yml文件,然后依赖include语句和角色来实现功能:

$ ansible-playbook -i customhosts site.yml

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

使用角色运行剧本

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

  • 在应用角色之前和之后,pre-taskspost-tasks会被触发;这时通过 shell 模块打印消息。

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

  • 我们还看到处理器触发了 Nginx 服务的重启。这是由于configuration文件的状态发生了变化,从而触发了处理器。

提示

你有没有注意到,即使我们没有在www剧本中提到基础角色,基础角色中的任务也会被触发?这就是元数据(meta information)发挥作用的地方。还记得我们在meta/main.yml中为 Nginx 指定了对基础角色的依赖关系吗?这就是起作用的原因。

依赖关系:

           - { role: base}

复习问题

你觉得自己已经充分理解这一章了吗?试着回答以下问题,检验你的理解:

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

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

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

  4. 为什么我们要为处理器单独设置一个部分,虽然它们和普通任务很相似?

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

  6. 如何在剧本中指定角色应用前要运行的任务?

总结

在本章中,你学习了如何使用角色提供抽象,并帮助模块化代码以便重用。这正是你在社区中看到的。创建角色,并与您共享。你还学习了include指令、角色的目录结构以及如何添加角色依赖关系。接着,我们对代码进行了重构,并创建了一个基础角色——Nginx 角色。我们还探讨了如何管理事件并通过处理器执行操作。

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

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

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

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

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

  • 什么是 Jinja2 模板?如何创建这些模板?

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

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

  • 变量有哪些不同类型?

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

静态内容爆炸

假设我们管理着一个横跨多个数据中心的几百台 Web 服务器的集群。由于我们将 server_name 参数硬编码到 config 文件中,我们需要为每个服务器创建一个文件。这也意味着我们将管理数百个静态文件,这些文件会迅速失控。我们的基础设施是动态的,管理变化是 DevOps 工程师日常工作中最常见的任务之一。如果明天公司政策规定我们应该将 Web 服务器的端口从 80 改为 8080,只在生产环境中执行,想象一下你得单独修改所有这些文件时会有多麻烦。是不是更好拥有一个接受特定主机动态输入的单一文件?这正是模板的用途,正如下图所示,一个模板可以替代多个静态文件:

静态内容爆炸

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

分离代码和数据

基础设施即代码工具(如 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 自有格式,使用的是其核心的 setup 模块。与 setup 模块类似,还有一个名为 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参数

  • Playbooks 中的变量和角色参数

  • 角色中的vars目录和在 play 中定义的变量

  • 在运行时通过-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 文件。

向角色添加元数据

我们之前使用过 meta 文件来指定对其他角色的依赖关系。除了指定依赖关系外,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 版本的兼容性、支持的平台、角色所属的类别等。

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

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

  • 我们将找到一个事实来确定操作系统平台/家族。我们在这里有几个选择:

    • ansible_distribution

    • ansible_os_family

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

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

    • 来自默认的 vars 文件,包含 Debian 的合理默认值。

    • 来自特定于 os_family 的变量(如果不是 Debian)。

  • 我们还将创建特定于操作系统的任务文件,因为我们可能需要调用不同的模块(例如 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事实返回的确切名称(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事实,并使用一个变量来覆盖我们的默认设置。您已经了解到可以在inventory文件、group_varshost_vars事实中指定变量。我们现在将开始使用group_vars事实。您可以在您的清单文件中创建这些,也可以创建一个名为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事实和使用ansible_os_family事实。如果你注意到:

  • 我们在这里使用了ansible_os_family事实和include_vars事实,以确定在不是 Debian 系统的情况下是否包含特定于操作系统的变量。为什么不适用于 Debian 系统?因为我们已经在default文件中指定了特定于 Debian 的配置。include_vars事实在前述条件下运行良好。

  • 我们还使用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

在前面的示例中,我们分别使用了aptyum模块来支持 Debian 和 RedHat 基于系统。按照最佳实践,我们将通过使用变量mysql_pkg来写入数据驱动角色,根据运行的平台设置包名称。让我们看看以下步骤:

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

    $ 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 变量来决定启动或重启的服务名称。

在 playbook 中使用变量

变量也可以在 playbook 中指定。推荐的做法是将它们作为角色参数传递,以下是一个示例。这通常在角色中有默认值时很有用,如果你想覆盖一些特定于你配置的参数。这样,角色仍然是通用的和可共享的,并且不会包含特定于组织的数据。

我们将创建一个 playbook 来管理我们的数据库,然后将其包含在全局的 playbook 中,示例如下:

$ 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 的主机组。在我们的示例中,我们有两个 db 服务器,一个运行在 Ubuntu 上,另一个运行在 CentOS 上。可以这样添加:

[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

在前面的 playbook 中,我们使用了一个带参数的角色,覆盖了一个变量,即 mysql_bind。这个值是从多级事实中设置的。

让我们看看以下截图:

在 playbook 中使用变量

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

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

编辑site.yml文件如下:

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

将 MySQL 角色应用于 DB 服务器

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

$ ansible-playbook -i customhosts site.yml

以下图片包含了与数据库 play 相关的输出片段:

将 MySQL 角色应用于 DB 服务器

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

这里真正有趣的是,找出每个平台上我们的配置文件发生了什么变化,以及哪些变量的优先级更高。

变量优先级

我们指定了变量的默认值,将它们用于清单文件,并从不同的位置定义了相同的变量(例如,defaults、vars 和 inventory)。现在,让我们分析模板的输出,了解这些变量到底发生了什么。

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

变量优先级

以下是对截图的分析:

  • 文件的评论部分有一个通知。这可以防止管理员手动修改文件。

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

  • 即使在默认值和group_vars中已指定bind_address参数,它也会从 playbook 的角色参数中获取值,该值的优先级高于其他两个级别。

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

变量优先级

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

变量优先级

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

  • userpiddatadirport的值来自默认值。我们已经看过合并顺序。如果变量不完全相同,它们将被合并以创建最终配置。

  • 套接字的值来自 vars,因为这是唯一定义它的地方。不过,我们希望这个套接字在基于 RedHat 的系统中保持常量,因此我们将其指定在角色的 vars 目录中。

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

    • 角色中的Default

    • 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

  • 如果你希望保持角色的通用性并且能够共享,请在角色中使用默认值,然后从 playbook 中指定特定于组织的变量。这些可以作为角色参数来指定。

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

  • 最后,如果你希望在运行时覆盖上述任何变量并提供一些数据,可以在使用 Ansible 命令时通过-e选项提供额外的变量。

到目前为止,我们的 MySQL 角色和数据库 playbook 的目录结构应该像下面的图示一样:

变量使用最佳实践

复习问题

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

  1. Jinja2 模板与静态文件有什么不同?

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

  3. 在 Jinja2 模板中,{{ }}{% %} 有什么区别?

  4. 除了模板外,你还可以在其他地方使用变量吗?如果可以,在哪里?

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

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

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

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

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

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

总结

本章的开始,我们学习了为什么以及如何使用 Ansible 变量、事实和 Jinja2 模板将数据与代码分离。你学会了通过在模板、任务、处理器和 playbook 中提供变量和事实来创建数据驱动的角色。此外,我们为数据库层创建了一个新角色,该角色支持 Debian 和 RedHat 两大操作系统家族。你学会了系统事实是什么,以及它们是如何被发现和使用的。你了解了变量可以从多个地方指定,它们是如何合并的以及优先级规则。最后,你了解了使用变量的最佳实践。

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

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

Ansible 提供了多种内置模块,允许我们管理各种系统组件,例如用户、软件包、网络、文件和服务。Ansible 的内置式方法还提供了将组件与云平台、数据库和应用程序(如JiraApacheIRCNagios等)集成的能力。然而,偶尔我们会遇到找不到完全符合需求的模块的情况。例如,从源代码安装软件包涉及下载它、解压源代码 tarball、运行 make 命令,最后执行“make install”。没有单一模块可以完成这些操作。有时,我们也希望将已有的脚本(比如我们花费多夜编写的脚本)引入,只需使用 Ansible 调用或安排执行这些脚本,例如,每夜备份脚本。在这种情况下,Ansible 的命令模块将成为我们的救星。

在本章中,我们将介绍:

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

  • Ansible 命令模块:raw、command、shell 和 script

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

  • 注册变量

  • 如何创建 WordPress 应用

命令模块

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

  • Raw

  • 命令

  • Shell

  • Script

我们将逐一开始学习这些内容。

使用 raw 模块

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

  • 对于运行 Python 版本低于 2.6 的遗留系统,Ansible 要求在运行 playbooks 之前安装 Python-simplejson 包。可以使用 raw 模块连接到目标主机并在执行任何 Ansible 代码之前安装所需的前置包。

  • 对于网络设备,如路由器、交换机以及其他嵌入式系统,可能根本没有 Python。这些设备仍然可以仅通过使用 raw 模块来使用 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 清单中的所有主机,运行 raw 命令 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 的一部分,可以将其作为 Ad hoc 命令通过 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 服务器上托管新闻和博客。

注意

场景:

在第 1 次迭代成功启动简单网页后,项目管理部门要求我们在第 2 次迭代中设置一个 WordPress 应用程序,用于发布新闻文章和博客。

WordPress 是一个基于 LAMP 平台的流行开源网站发布框架,LAMP 平台包含 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

注意

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

安装 WordPress

WordPress 的安装过程将在任务目录中的 install.yml 文件中处理。安装 WordPress 通常包括以下过程:

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

  2. 解压安装包。

  3. 将解压后的目录移动到 Web 服务器文档的 root 目录中。

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

---
# 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的变量中,稍后将在其他任务中使用该变量。

提示

推荐使用内置的get_url模块,通过 HTTP/FTP 协议下载文件。由于我们想演示命令模块的使用,因此选择了使用命令模块,而不是get_url模块。

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

控制命令模块的幂等性

Ansible 自带了大量的内置模块。正如我们在第一章《构建基础架构蓝图》中所学,大多数这些模块是幂等的,并且确定配置漂移的逻辑已内置于模块代码中。

然而,命令模块允许我们执行本质上非幂等的 Shell 命令。由于命令模块无法确定任务的结果,因此这些模块默认情况下是非幂等的。Ansible 为我们提供了一些选项,使得这些模块可以根据条件运行,从而实现幂等性。

以下是决定是否执行命令的两个参数:

  • Creates

  • Removes

两者都接受文件名作为参数的值。对于creates,如果文件存在,命令将不会执行。removes命令则相反。

"creates" 和 "removes" 选项适用于所有命令模块,除了 raw 模块。

以下是如何使用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模块而不是command模块。这是因为我们在这里将两个命令通过&&操作符结合在一起,而command模块不支持这种方式。

  • 我们使用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. 当执行的命令不创建文件时,如何在 shell 中使用 creates 参数?

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

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

概要

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

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

第五章:执行流程控制 - 条件语句

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

  • 条件语句

  • 迭代

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

大多数编程语言和工具使用强大但机器友好的构造,如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。我们如何仅为数据库服务器启用该参数?我们有很多方法可以做到这一点。这次我们选择使用 playbook 变量,因为我们为数据库服务器专门设置了一个变量。

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

选择性配置 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。如果变量未定义,这将跳过该参数,而不是抛出错误。

只运行任务一次

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

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

由于我们使用了 run_once 选项,前面的任务将在角色应用的第一个主机上运行。所有随后的主机会跳过此任务。

条件执行角色

我们之前创建的 Nginx 角色用于设置 Web 服务器,仅支持基于 Debian 的系统。在其他系统上运行此逻辑可能会导致失败。例如,Nginx 角色使用 apt 模块安装软件包,但在基于 RedHat 的系统上,这将不起作用,因为它们依赖于 yum 包管理器。通过在 when 语句中添加事实条件,可以基于操作系统家族选择性地执行任务,从而避免这种情况。以下是 www.yml playbook 的代码片段:

#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,它有以下先决条件:

  • 一个 Web 服务器

  • PHP 绑定的 Web 服务器

  • MySQL 数据库和 MySQL 用户

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

PHP5-FPM 角色

PHP5-FPM 中,FPM 代表 FastCGI 进程管理器。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 文件包括基于 ansible_os_family 信息的变量,用于非 Debian 系统。这对于覆盖特定平台的变量非常有用。在包含 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 数据库来存储数据,如文章、用户等。此外,它还需要一个具有适当权限的 MySQL 用户,以便从 WordPress 应用程序连接到数据库。在安装 MySQL 时我们会获得一个管理员用户,但创建一个额外的用户账户并根据需要授予权限是一种好的做法。

创建哈希

哈希(hash)的缩写是哈希表,它是一个键值对字典。它是一个有用的数据结构,用来创建多级变量,然后通过程序化方式创建多个对象,每个对象具有自己的值。我们将在 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 的 PHP 虚拟主机配置,使用nginx.phpsites设置的字典值。

  • 字典提供的配置参数包括文档根目录、端口、使用的后端,告诉 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模块,还会利用魔法变量自动配置所有 Web 服务器节点的 IP 地址:

  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
    

下面是对前面代码的分析:

  • 根据最佳实践,我们为每个阶段(安装、配置和服务)创建了单独的任务文件。然后我们从主任务文件中调用这些任务文件,即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 参数获取事实以及用户定义的变量,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 并创建一个配置,将所有 Web 服务器添加到 haproxy.cfg 文件的后端部分。以下是 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 在处理魔法变量时的行为:

  • Ansible 在运行代码时开始收集主机的事实。这些事实会在 playbook 运行期间存储在内存中。这是默认行为,可以关闭。

  • 为了使主机 B 能够发现来自主机 A 的变量,Ansible 必须在 playbook 中先与主机 A 通信。

Ansible 这种行为可能会导致不想要的结果,并且可能会限制主机只发现属于其自身 play 的节点信息。

使用 Redis 进行事实缓存

通过缓存事实,可以避免从非 playbook 主机中发现事实的问题。此功能是在 Ansible 1.8 版本中添加的,并支持在 Redis 中缓存事实,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

提示

在启用事实缓存之前,最好首先检查你是否运行的是 Ansible 1.8 版本或更高版本。你可以通过运行命令 $ 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 缓存事实

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

$ ansible-playbook -i customhosts lb.yml

这次成功通过了。它能够发现 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

在做出这些更改后,我们只需运行剧本,Ansible 就会开始将事实写入以主机名命名的 JSON 文件,这些文件会被创建在该目录下。

回顾问题

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

  1. 魔法变量与事实不同吗?它们有什么用途?

  2. 哪个魔法变量允许我们遍历一组 Web 服务器并列举每个服务器的 IP 地址?

  3. 为什么需要缓存事实?缓存事实有哪些不同的模式?

  4. inventory_hostname 事实是否总是与 ansible_hostname 事实相同?

总结

在本章中,您学习了如何发现集群中其他节点的信息以将它们连接起来。我们从魔法变量的介绍开始,并查看了最常用的几个变量。接着,我们开始创建一个 haproxy 角色,它可以自动发现 Web 服务器并动态创建配置。最后,我们讨论了如何访问不在剧本中的主机信息,并通过启用事实缓存来解决这个问题。魔法变量非常强大,特别是在使用 Ansible 协调基础设施时,自动发现拓扑信息非常有用。

在下一章中,您将学习如何使用 vault(加密数据存储)安全地传递数据。

第八章:使用 Vault 加密数据

使用变量时,我们看到如何将数据与代码分离。通常,提供的数据是敏感的,例如用户密码、数据库凭证、API 密钥以及其他组织特定的信息。Ansible-playbook 作为源代码,通常存储在版本控制仓库中,如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 使用 256 位密钥大小的 AES 密码来加密 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_creds.yml 文件示例,该文件用于存储 AWS 用户凭证(包括访问密钥和秘密密钥)。这些密钥随后用于向亚马逊 Web 服务云平台发起 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

提示

当 vault 密码以明文存储时,任何访问此文件的人都可以解密数据。确保密码文件以适当的权限保护,并且不被添加到版本控制中。如果你决定进行版本控制,请使用 gpg 或同等措施。

现在可以将此文件提供给 Ansible playbook,如下所示:

$ ansible-playbook -i customhosts site.yml --vault-password-file ~/.vault_pass

将 vault 密码文件选项添加到 Ansible 配置中

在版本 1.7 中,还可以在默认部分的 ansible.cfg 文件中添加 vault_password_file 选项。

请考虑以下内容:

[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. 让我们使用 vault 加密这个文件:

    $ ansible-vault encrypt group_vars/www
    Encryption successful
    
    

    由于我们已经在配置中提供了 vault 密码的路径,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. 此外,让我们为 SSL 添加一个虚拟主机 config 文件:

    # 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
    
    

这应该配置默认的 SSL 网站,运行在 443 端口,并使用自签名证书。现在,你应该能够使用 https 安全协议打开 Web 服务器地址,如下所示:

为 Nginx 添加 SSL 支持

当然,由于我们的证书是自签名的,并未由指定的认证机构提供,因此它应显示一个警告。

复习问题

你觉得你已经充分理解了本章的内容吗?尝试回答以下问题来测试你的理解:

  1. 为什么需要加密提供给 Ansible playbooks 的数据?

  2. 什么是 AES,什么是对称密钥加密算法?

  3. 更新之前用 vault 加密的文件的两种方法是什么?

  4. 添加到 Ansible 配置文件中的参数是什么,以使其知道 vault 密码文件的位置?

摘要

在本章中,你学习了如何使用 Ansible-vault 来保护传递给 playbooks 的数据。我们从加密数据的需求开始,讲解了 vault 的工作原理以及它使用的加密算法。接着,我们深入探讨了 Ansible-vault 工具及其基本操作,如创建加密文件、解密、重新加密等。你还学习了如何通过运行 Ansible-vault 对保存数据库凭证的 vars 文件进行加密。最后,我们为 Nginx 添加了 SSL 支持,并学习了如何使用 vault 安全存储私钥和证书,并通过模板复制它们。请注意,Ansible vault 提供了一种安全地向 Ansible 模块提供数据的方法。除了使用 vault 之外,建议采取额外的系统安全措施,这些内容不在本书的范围内。

在了解了 vault 后,在下一章中,我们将开始学习如何使用 Ansible 管理多个环境(如开发、预发布和生产环境)。这些环境通常映射到软件开发工作流中。

第九章:管理环境

大多数组织在构建基础设施时,从单一环境开始。然而,随着复杂性的增加,必须拥有一个工作流,包括编写代码并在开发环境中进行测试,然后经过严格的 QA 周期,确保代码在预生产(即 staging)环境中经过稳定性测试,最后才发布到生产环境。为了模拟现实世界的行为,这些环境必须运行相同的应用堆栈,但很可能是不同的规模。例如,staging 环境将是生产环境的小规模复制,具有较少的服务器,而开发环境通常运行在虚拟化的个人工作站上。即使所有这些环境都运行相同的应用堆栈,它们必须相互隔离,并且必须具有特定环境的配置,具体如下:

  • dev组中的应用不应指向 staging 中的数据库,反之亦然

  • 生产环境可能有自己的包存储库

  • 预生产环境可能在端口8080上运行 Web 服务器,而其他所有环境则在端口80上运行

通过角色,我们可以创建一个模块化的代码,以便为所有环境配置相同的环境。Ansible 的另一个重要特性是它能够将代码与数据分离。通过这两者的结合,我们可以以一种方式建模基础设施,使得我们能够在不修改角色的情况下创建特定环境的配置。我们只需要通过提供来自不同位置的变量就能创建它们。让我们来看一下下面的截图:

管理环境

上面的图表展示了同一组织中的三种不同环境:开发(dev)、预生产(stage)和生产(production)。这三者都运行相同的应用堆栈,包括负载均衡器、Web 服务器和数据库服务器。然而,需要注意的两点是:

  • 每个环境的规模不同,基于此,主机可以配置为运行一个或多个角色(例如,dbwww)。

  • 每个环境都彼此隔离。生产环境中的 Web 服务器不会连接到 staging 中的数据库,反之亦然。

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

  • 使用 Ansible 管理多个环境

  • 按环境分隔库存文件

  • 使用group_varshost_vars组来指定特定环境的配置

管理环境的方法

你已经了解到,需要创建具有相同角色但数据不同的不同环境。在写这篇文章时,已经存在多种方法来管理 Ansible 中的多环境场景。我们将在这里讨论两种方法,你可以根据自己的判断选择其中一种,或者创建自己的方法。没有明确的方式来创建环境,但以下是 Ansible 的内置功能,可能会派上用场:

  • 使用库存将属于同一环境的主机分组,并将它们与其他环境中的主机隔离开

  • 使用库存变量,例如group_varshost_vars组,来提供特定于环境的变量

在我们继续之前,回顾适用于库存组、变量和优先级规则会很有帮助。

库存组和变量

你已经了解到,Ansible 的库存遵循 INI 样式的配置,其中主机通过带有方括号的组标签分组,如下图所示:

库存组和变量

然后可以指定库存变量,使其与这些组名匹配,使用group_vars或在host_vars文件中匹配特定主机。除了这些组名外,还可以使用名为"all"的文件为group_varshost_vars文件指定默认变量,从而形成以下结构:

库存组和变量

在这种情况下,如果在allwebserver文件中指定相同的变量,具有更高特定性的变量将优先。也就是说,如果你在'all'中定义了一个变量,并在'webserver'组的group_vars下再次定义该变量,那么该变量的值将被设置为'webserver'中定义的值,因为它更具体。这就是我们在以下方法中利用的行为:

方法 1 – 在库存中使用嵌套组

除了能够使用 INI 样式创建组,Ansible 还支持嵌套组,其中一个完整的组可以是另一个父组的一部分。第一种方法就是基于这个特性,并按以下步骤逐步讨论:

  1. 创建一个环境目录来存储特定于环境的库存文件。最好按照环境命名它们。添加特定于该环境的主机并将其分组。组可以基于任何标准,例如角色、位置、服务器机架等等。例如,创建一个'webservers'组,将所有 Apache web 服务器添加进去,或者创建一个名为'in'的组,将所有属于该位置的主机添加进去。

  2. 添加一个父组,名称与环境名称相同,如生产、开发、预发布等,并将所有属于该环境的其他组作为子组包含进来。每个子组依次包含一个主机组,例如:

    [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 选项也接受一个目录,目录下可以包含一个或多个清单文件。

  • hostgroup 变量除了位于 Ansible 仓库根目录的 group_varshost_vars 组外,还可以相对于清单文件。

这种方法将为每个环境创建完全隔离的变量文件。我们创建的文件结构如下图所示:

方法 2 – 使用特定于环境的清单变量

以下是此方法的逐步操作:

  1. 在 Ansible 仓库的根目录中创建一个环境目录。在此目录下为每个环境创建一个子目录。

  2. 每个环境目录包含两样东西:

    • 主机的清单。

    • 清单变量,例如 group_varshost_vars。要进行环境特定的更改,group_vars 与我们相关。

  3. 每个环境包含自己的 group_vars 目录,目录下可以有一个或多个文件,其中包括默认的 all 文件。不同环境之间不会共享这些变量。

提示

注意:除了特定于环境的 group_vars 组外,您还可以使用位于 Ansible-playbook 仓库顶部的 group_vars 文件。然而,建议不要在这种方法中使用它,因为如果它们相同,环境特定的更改会被播放簿 group_vars 中的值覆盖。

使用这种方法,播放簿可以针对特定环境启动,如下所示:

$ ansible-playbook -i environments/dev site.py

这里,environments/dev 是一个目录。

创建开发环境

学习了如何管理环境后,让我们通过重构现有代码并创建一个 dev 环境来实践。为了测试它,我们创建了一个名为"env_name"的变量,并将 Nginx 的默认页面修改为动态使用这个变量并打印环境名称。然后我们会尝试从环境中覆盖这个变量。让我们来看看以下步骤:

  1. 让我们首先设置默认变量:

    #group_vars/all
    env_name: default
    
  2. 然后,将 Nginx 任务更改为使用模板而不是静态文件,因此在roles/nginx/tasks/configure.yml文件中做出如下修改:

     - 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 作为基础设施编排引擎

  • 实现滚动更新

  • 使用标签、限制和模式

  • 在 playbook 中构建测试

Ansible 作为编排工具

在任何形式的编排中,Ansible 相较于其他工具表现得尤为突出。当然,正如 Ansible 的创始人所说,它不仅仅是一个配置管理工具,这一点是正确的。Ansible 可以在之前讨论的任何编排场景中找到自己的位置。它的设计初衷就是为了管理复杂的多层部署。即使你的基础设施已经通过其他配置管理工具进行自动化,你也可以考虑使用 Ansible 来进行编排。

让我们讨论一下 Ansible 所提供的特性,这些特性对于编排非常有用。

多个 playbook 和执行顺序

与大多数其他配置管理系统不同,Ansible 支持在不同的时间运行不同的 playbook 来配置或管理相同的基础设施。你可以创建一个 playbook 来首次设置应用栈,另一个 playbook 来以某种方式逐步推送更新。Playbook 的另一个特点是它可以包含多个 play,这使得能够将应用栈中的每一层的主机分组,并同时对它们进行配置。

前置任务和后置任务

之前我们已经使用过前任务和后任务,在编排时非常相关,因为这些允许我们在运行播放之前和之后执行任务或运行验证。让我们举一个更新已在负载平衡器注册的 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 plays 将角色映射到特定的主机。在执行 play 时,从主任务调用的整个逻辑都会被执行。在编排过程中,我们可能只需要根据我们希望将基础设施带到的阶段来运行部分任务。一个例子是 zookeeper 集群,在这个场景中,重要的是同时启动集群中的所有节点,或者在几秒钟的间隔内启动。Ansible 可以轻松地通过两阶段执行来编排此过程。在第一阶段,您可以在所有节点上安装和配置应用程序,但不启动它。第二阶段涉及几乎同时在所有节点上启动应用程序。这可以通过标记单独的任务来实现,例如配置、安装、服务等。

例如,来看一下以下截图:

标签

在运行 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

在角色中标记任务后,我们还将在 playbook 中标记角色,具体如下:

# 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. 在更新之前,将 Web 服务器从 haproxy 负载均衡器中注销。这样可以停止对 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,它运行在属于 www 组的主机上。

  • serial 关键字指定批处理大小,并允许进行零停机的滚动更新。在我们的案例中,由于我们有较少的主机,我们选择一次更新一个 Web 服务器。

  • 在应用角色之前,主机通过 pre-tasks 部分从负载均衡器中注销,该部分运行一个带有socat的 shell 命令。这个命令将在所有负载均衡器上使用 delegate 关键字运行。Socat 是一个类似于 nc(netcat)的 Unix 工具,但功能更强大。

  • 在注销主机后,会应用角色;这将更新 Web 服务器的配置或部署新代码。

  • 更新后,后任务将启动,首先等待直到 Web 服务器启动并监听 80 端口,只有在其准备好后,才会将其注册回负载均衡器。

复习问题

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

  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 @ 2025-07-02 17:45  绝不原创的飞龙  阅读(39)  评论(0)    收藏  举报