Skip to content

Files

Latest commit

f87fb9e · Dec 29, 2021

History

History
500 lines (354 loc) · 23.5 KB

File metadata and controls

500 lines (354 loc) · 23.5 KB

七、成为大师——完整的配置指南

当你读到这一章时,你将已经了解了本书范围内的所有概念。本章将建立在前几章所学的一切基础上,使用一些基础知识,并向您展示 Ansible 可以派上用场的真实用例。本章将向您展示如何使用 Ansible 来解决简单的以及复杂的问题和场景。

一个行动手册,不同的应用,多个目标

您可能会遇到不同环境需要不同设置或部署步骤的场景,例如,部署到不同的环境,如开发、质量保证、阶段或生产。部署方案可能会有一些小的变化,例如,网络应用的质量保证实例指向数据库的本地实例,而生产部署指向不同的数据库服务器。

另一个场景可能是部署一个为不同发行版构建的应用(例如,基于 RPM 和基于 Debian 的应用)。在这种情况下,部署会有所不同,因为两个平台使用不同的应用管理器。基于 RPM 的发行版使用 Yum 或 DNF 包管理实用程序,而基于 Debian 的发行版使用 DPKG 实用程序进行包管理。此外,创建的结果包也会有所不同,一个是.rpm,另一个是.deb

在这种情况下,即使目标平台不同,部署方案或配置也各不相同,所有这些都可以通过定义角色在一个行动手册中处理。

让我们进入几个实际场景。在第一个场景中,您需要部署一个由后端数据库(MySQL)和前端 web 应用组成的应用。web 应用查询后端数据库,并根据用户的请求提供数据。web 应用和 MySQL 数据库都将部署在不同的机器上。

让我们将安装和配置任务分为两类:

  • 系统准备:对于 web 应用系统和数据库服务器来说,这是一个共同的任务。两个系统都需要首先做好安装准备。准备工作可能包括配置存储库和更新系统等任务。
  • 部署:这包括部署数据库和 web 应用,随后进行所需的任何配置更改。

如果您分析类别,系统准备对两个系统都是通用的,而部署作业则特定于每个应用。在这种情况下,您可以将作业分成角色。您可以有三个角色——一个“通用”角色,分别在两台机器上执行,一个角色分别用于数据库和 web 应用。这使得 Ansible 行动手册更加模块化,易于维护。

以下是基于上述问题陈述分析的 Ansible 行动手册:

db-webapp-role.yaml

---

- hosts: all 
  user: root
  roles:
    - { role: common }

- hosts: database
  user: root
  roles:
    - { role: database }

- hosts: webapp
  user: root
  roles:
    - { role: webapp }

前面的剧本调用了不同的角色–commonwebappdatabase,并在相应的主机组上执行它们。在所有主机组上执行common角色(即在webappdatabase上)。然后在特定主机组上执行各个角色。以下是前一部剧中的角色:

角色 : common

---
- name: Create hosts file for each machine
  template: src hosts.j2 dest=/etc/hosts

- name: Copy Repo file
  copy: src=local-tree.repo dest=/etc/yum.repos.d/

- name: Update the system
  yum: name=* state=latest

这是一个common角色,将在所有目标主机上执行。它配置了一个为目标机器提供包和依赖项的存储库。该角色配置此存储库,并将目标计算机上安装的所有软件包更新为最新版本。

以下角色将仅在清单文件中数据库组下指定的主机上执行。这将安装 MySQL 数据库并复制一个配置文件,该文件将配置数据库并在目标主机上创建所需的表。它还将确保 MYSQL 服务在目标主机上运行。根据 Ansible 玩法,在成功完成common角色后,该角色将在目标主机上执行:

角色 : database

---
- name: Install MySQL databse server
  yum: name=mysql state=present

- name: Start MySQL service
  service: name=mysqld status=started

- name: Create a directory to copy the setup script
  file: path=/temp/configdb state=directory mode=0755

- name: Copy script to create database tables
  copy: src=configdb.sh dest=/temp/configdb

- name: Run configdb.sh to create database tables
  shell: configdb.sh chdir=/temp/configdb

以下角色特定于在清单文件中的 webapp 主机组上部署 web 应用。该角色将在成功完成common角色后执行,具体如下:

角色 : webapp

---
- name: Install HTTP server
  yum: name=httpd state=present

- name: Start httpd service
  service: name=httpd state=started

- name: Create temporary directory to copy over the rpm 
  file: path=/temp/webapp state=directory mode=0755

- name: Copy the application package to the target machine
  copy: src=webapp-2.4.16-1.fc22.x86_64 dest=/temp/webapp

- name: Install the webapp
  command: yum install -y ./webapp-2.4.16-1.fc22.x86_64 chdir=/temp/webapp

- name: Copy configuration script
  copy: src=configweb.sh dest=/temp/webapp

- name: Execute the configuration script
  shell: configweb.sh chdir=/temp/webapp

Ansible 角色–使用标签

Ansible 的行动手册是模块化的,能够在任何需要的时候在不同的环境中使用。为此,引入了角色。但是,仅仅使用角色可能还不够,因为您可能希望在同一台主机上对不同的环境使用不同的角色。好吧,这听起来很混乱。让我们进入一个场景。

您可以将您的 Ansible 行动手册与您的持续部署系统集成,这有助于开发人员在开发周期中随时部署应用。在这个周期中,他们可能希望以适合开发阶段的方式设置系统和配置应用。由于正在开发应用,在开发环境中部署时,并非所有功能都是完整的。但是,一旦应用完成,开发人员可能希望完整运行 Ansible 来复制生产或 QE 环境,从而确保应用以生产主机上所需的所有设置运行。在这种情况下,有两种不同的环境——发展和 QE 就绪。

由于部署是在同一台主机上完成的,并且有多个角色可以执行,因此您可以使用标记。您可以将角色与标签相结合。因此,通过从命令行指定标记,Ansible 知道要执行哪个角色。

演示这一点的一个简单方法如下。假设您有一个应用,当需要在开发环境中部署时,您从您的 Git Hub 存储库中克隆代码并运行install.sh脚本。同样在开发环境中,您有一些宽松的安全策略,比如 SeLinux 被设置为许可模式。相同的应用,当传递到 QE 时,应该打包在 RPM 中,然后安装。此外,不允许安全放松,因此 SeLinux 需要保持强制模式。由于开发人员只有一个开发实例,他或她必须在同一个实例上执行两个角色。在这种情况下,开发人员可以根据部署应用的需要,使用标签来使用不同的角色。

下面是一个 Ansible 的剧本,以及演示前面场景的角色:

角色 : development

---

- name: Create directory to clone git repo
  file: path=/tmp/gitrepo state=directory mode=0755

- name: Clone Git repo
  git: repo={{ item }} dest=/tmp/gitrepo
  with_items:
    - "{{ git_repo }}"

- name: Set selinux to permissive
  selinux: policy=targeted state=permissive

- name: Run install.sh to deploy application
  shell: install.sh chdir=/tmp/gitrepo/example

角色 : qe_ready

---

- name: Make directory to store RPM 
  file: path=/tmp/deploy state=directory mode=0755

- name: Download the RPM to Directory
  get_url: url={{ item }} dest=/tmp/deploy
  with_items:
    - "{{ rpm_link }}"

- name: Install RPM 
  command: yum install -y *.rpm chdir=/tmp/deploy

- name: Set Selinux to Enforcing
  selinux: policy=targeted state=enforcing

前面两个角色是同一个 Ansible 行动手册的一部分,将根据需要根据您指定的标签进行调用。下面的示例演示了如何将角色绑定到特定的标签:

附加赛 : demo-tag.yaml

---

- hosts: application
  user: rdas
  sudo: yes 
  roles:
    - { role: development, tags: ['development'] }
    - { role: qe_ready, tags: ['qe'] }

development角色现在绑定到development标签,而qe_ready角色绑定到标签。可通过使用-t标志以下列方式指定标签来执行 Ansible 的剧本:

# ansible-playbook -i hosts -t development demo-tag.yaml

获取基础设施信息并集中托管

在前面的章节中,你创建了一个dmidecode模块从目标机器收集系统信息并返回一个 JSON 输出。如果您希望将输出存储在目标机器上的 JSON 文件中,该模块还允许您将标志“保存”切换到true

在各自的目标机器上存储系统信息没有多大用处,因为数据仍然驻留在目标机器上,要访问数据,需要登录到不同的机器,然后解析各自的 JSON 文件。为了解决这个问题,本书向您介绍了回调,回调有助于获取 JSON 数据,并将其作为 JSON 文件存储在控制器节点上(也就是说,执行 Ansible playbook 的节点)。

然而,即使这样做了,问题也没有完全解决。您确实设法从基础架构节点收集了数据,但是可访问性仍然是一个问题。

  • 需要访问控制器机器才能访问所有文件
  • 在现实场景中,您不能授予每个人访问权限
  • 即使您计划授予特定个人访问权限,您的可用性仍然是一个瓶颈

为了解决这个问题,一个解决方案是将所有这些 JSON 文件托管到一个中央服务器,从那里可以下载所需的 JSON 文件,解析它们,并生成报告。然而,解决这个问题的一个更好的方法是在一个中央弹性搜索实例中索引数据,然后该实例通过一个 RESTful API 提供数据。

Elasticsearch 是一个建立在 Apache Lucene 之上的开源搜索引擎。Elasticsearch 是用 Java 编写的,内部使用 Lucene 进行索引和搜索。它旨在通过将 Lucene 的复杂性隐藏在一个简单的 RESTful API 后面,使全文搜索变得容易。

来源:弹性搜索来自www.elastic.co的文档。

本章将不深入讨论什么是弹性搜索以及如何工作,因为这超出了本书的范围。有关弹性搜索的详细信息,您可以参考在线文档或掌握弹性搜索(https://www . packtpub . com/web-development/Mastering-elastic search-第二版),由 Packt Publishing 发布。

回到弹性搜索中索引数据并通过 HTTP 提供服务的问题,应用编程接口可以解决手头的问题。要做到这一点,您必须编写一个回调插件,与一个弹性搜索实例交互,并索引 JSON 数据,然后可以通过应用编程接口提供这些数据。Python 提供了一个库pyes,用于与弹性搜索实例进行交互。

让我们命名回调插件loges.py并将其存储在 Ansible play 根目录下的callback_plugins目录中,如下代码所示:

from pyes import *
import json

# Change this to your Elasticsearch URL
ES_URL = '10.3.10.183:9200'

def index_elasticsearch(host, result):
    '''  index results in elasticsearch '''
    # Create connection object to Elasticsearch instance
    conn = ES(ES_URL)
    # Create index 'infra' if not present. Used for the first function call
    if not conn.indices.exists_index('infra'):
        conn.indices.create_index('infra')
    # Index results in Elasticsearch.
    # infra: index name
    # dmidecode: document type
    # host: ID
    conn.index(result, 'infra', 'dmidecode', host)
    print 'Data added to Elasticsearch'

class CallbackModule(object):
    ''' 
    This adds the result JSON to ElasticSearch database
    '''
    def runner_on_ok(self, host, result):
        try:
            if result['var']['dmi_data[\'msg\']']:
                index_elasticsearch(host, result['var']['dmi_data[\'msg\']'])
        except:
            pass

在创建这个回调插件后,如果你运行 Ansible play dmidecode.yaml,在成功运行后,JSON 输出将在 Elasticsearch 实例中被索引,并且应该可以通过 API 获得。数据将被编入名为infra的索引中,文档类型为dmidecode。每个索引文档都有一个唯一的标识,在这种情况下,将是HostnameIP,视情况而定。

创建刚启动实例的动态清单

针对清单文件中通常指定的目标主机,执行一个 Ansible 的行动手册,甚至单个模块。它们最基本的用途是拥有一个静态清单文件(例如,主机),其中包含所有目标主机 IP 或主机名的列表,Ansible play 必须针对这些 IP 或主机名执行。然而,在现实世界中,事情可能没有这么简单。例如,您可能需要在云上启动一个新的实例——比如 OpenStack 或 AWS——或者启动一个基本的虚拟机,然后使用 Ansible 行动手册部署您的应用。在这种情况下,在实例启动之前,目标 IP 是未知的,因此静态清单文件不能满足目的。

以编程方式运行 Ansible 和使用 Ansible API 的主要好处之一是处理运行时变量,例如目标 IP,在这种情况下。在这种情况下,您可以充分利用 Python 应用编程接口来运行 Ansible 剧本,同时创建动态清单。

为了生成动态清单文件,可以使用 Jinja2 模板。Ansible 完全支持 Jinja2,可以用来创建任何你想要的模板。Jinja2 本身就是一个庞大的主题,无法详细介绍,因为它超出了本书的范围。然而,这个特定的场景将涉及 Jinja2,以及它如何与 Ansible 结合使用。在上述情况下,Jinja2 模板将用于在运行时呈现清单文件。

让我们重温一下第 4 章探索应用编程接口中的例子,其中一个 Ansible 剧本webserver.yaml是以编程方式在库存文件hosts上执行的。与第 4 章探索 API 中的示例相反,在以下示例中,库存文件将在运行时呈现。这个在执行端到端自动化时非常方便,从启动实例和部署应用开始。

from ansible.playbook import PlayBook
from ansible.inventory import Inventory
from ansible import callbacks
from ansible import utils

import jinja2
from tempfile import NamedTemporaryFile
import os

# Boilerplace callbacks for stdout/stderr and log output

utils.VERBOSITY = 0
playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
stats = callbacks.AggregateStats()
runner_cb = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY)

# [Mock] Launch instance and return instance IP

def launch_instance(number):
    '''
    Launch instances on OpenStack and return a list of instance IPs

    args:
        number: Number of instances to launch
    return:
        target: List containing IPs of launched instances

    This is a dummy function and does not contain code for launching instances
    Launching an instance on OpenStack, AWS or a virtual machine is beyond the
    scope of this book. The example focuses on creating a dynamic inventory
    file to be used by Ansible.
    '''
    # return 2 IPs as the caller requested launching 2 instances.
    target = ['192.168.10.20', '192.168.10.25']
    return target

# Dynamic Inventory

inventory = """ 
[remote]
{% for elem in public_ip_address  %}
{{ elem }}
{% endfor %}
"""
target = launch_instance(2)
inventory_template = jinja2.Template(inventory)
rendered_inventory = inventory_template.render({
    'public_ip_address' : target
})

# Create a temporary file and write the template string to it
hosts = NamedTemporaryFile(delete=False)
hosts.write(rendered_inventory)
hosts.close()

pb = PlayBook(
    playbook = 'webserver.yaml',
    host_list = hosts.name,
    remote_user = 'rdas',
    stats = stats,
    callbacks=playbook_cb,
    runner_callbacks=runner_cb,
    private_key_file='id_rsa.pem'
)

results = pb.run()

playbook_cb.on_stats(pb.stats)

print results

在前面的示例中,launch_instance函数仅用于表示可以启动实例或虚拟机的一些代码。调用该函数时,将返回与已启动实例相关联的入侵防御系统列表。返回的列表缓存在变量target中,然后用于呈现清单文件。以下代码部分...:

inventory = """ 
[remote]
{% for elem in public_ip_address  %}
{{ elem }}
{% endfor %}
"""

...是使用以下代码渲染的 Jinja2 模板:

inventory_template = jinja2.Template(inventory)
rendered_inventory = inventory_template.render({
    'public_ip_address' : target
})

然后,使用以下代码将渲染的清单写入临时文件:

hosts = NamedTemporaryFile(delete=False)
hosts.write(rendered_inventory)
hosts.close()

这将在运行时用目标机器(新启动的实例)的入侵防御系统创建一个清单文件,如launch_instance方法返回的。

可通过堡垒主机传送

在现实世界中,生产服务器通常被配置为阻止来自其自己的专用网络之外的 SSH 连接。这是为了减少可能的攻击向量的数量,也是为了将接入点保持在最低限度。这有助于限制访问,创建更好的日志记录,并提高安全性。这是一种常见的安全实践,通过使用堡垒主机来实现。

堡垒主机是专门为抵御攻击而设计的。通常,堡垒主机只运行一个服务。为了最大限度地减少威胁,将删除或禁用其他服务。

在这种情况下,由于出现了堡垒主机,Ansible 无法从控制器节点直接 SSH 到目标主机。它需要通过堡垒主机代理其命令,以便到达目标机器。

要实现这一点,您只需要修改您的 Ansible play 根目录中的三个文件:

  • hosts:库存档案
  • ansible.cfg : Ansible 的配置文件
  • ssh.cfg : SSH 配置

清单文件包括一个组bastion,以及通常的目标主机。以下代码是一个样本库存hosts文件:

[bastion]
10.68.214.8

[database_servers]
172.16.10.5
172.16.10.6

由于 Ansible 几乎所有操作都使用 SSH,下一步就是配置 SSH。SSH 本身允许我们根据需要自定义设置。要为这个特定的 Ansible 播放配置 SSH,您需要在 Ansible 播放手册的根目录下创建一个包含以下内容的ssh.cfg文件:

Host 172.16.*
  ProxyCommand  ssh -q -A rdas@10.68.214.8 nc %h:%p
Host *
  ControlMaster    auto
  ControlPath    ~/.ssh/mux-%r@%h:%p
  ControlPersist    15m

前面的 SSH 配置通过我们的堡垒主机10.68.214.8将所有命令代理到网络中的节点172.16.*。控制设置ControlPersist允许 SSH 重用已经建立的连接,从而提高性能并加快 Ansible 剧本的执行。

现在 SSH 已经配置好了,您需要告诉 Ansible 使用这个 SSH 配置。为此,需要在 Ansible play 的根目录下创建一个ansible.cfg文件,内容如下:

[ssh_connection]
ssh_args = -F ssh.cfg
control_path = ~/.ssh/mux-%r@%h:%p

Ansible 现在将使用上述配置来使用ssh.cfg作为 SSH 配置文件,从而通过堡垒主机代理命令。

快乐的管理者=快乐的你

到目前为止,本章一直是关于为管理、部署和配置实现 Ansible 的。嗯,还有一点仍然存在——报告。

在长时间的行动手册执行结束时,您可能已经部署了应用,并且您可能已经拥有了基础架构的审计数据或行动手册设计要做的任何事情。此外,您还可以拥有行动手册执行的日志。但是,假设在一天结束时,您被要求提供一份报告。现在,您必须坐下来创建报告并填写一个 Excel 电子表格,因为这是您的经理所要求的–对事物现状的概述。这也可以通过扩展 Ansible 来实现。

所以,你做了一个剧本运行,你得到的是stdout上的运行日志。现在的问题变成了:如何用它制作 Excel 报表?是的,你猜对了——回调插件来拯救。您可以编写自己的自定义回调插件,帮助您记录您的 Ansible 播放结果,并使用它们创建电子表格。这将减少手动创建报告的开销任务。

对于不同的用例,报告可能会有所不同,因为不是一个单一的报告适合所有情况。因此,您必须为您想要生成的不同类型的报告编写回调插件。有些人更喜欢基于 HTML 的报告,而有些人更喜欢 Excel 电子表格。

以下示例重用了第 3 章中的dmidecode模块,深入挖掘可执行模块。该模块用于生成 JSON 输出,有利于机器处理。然而,对于报告来说,JSON 并不是人们想要手动通读的东西。在 Excel 电子表格中表示数据更有意义,而将报表创建为电子表格更便于阅读,并且可以一目了然地提供完整的图片。具有非技术背景的人也可以从 Excel 表中读取数据,没有太多麻烦。

下面是一个回调模块,它创建一个 Excel 工作表,读取通过执行dmidecode模块生成的 JSON 输出,并在 Excel 电子表格中追加每个主机的数据。用 Python 编写,使用openpyxl库创建 Excel 电子表格。

#!/bin/python
import openpyxl
import json
import os

PATH = '/tmp'

def create_report_file():
    ''' Create the initial workbook if not exists
    '''
    os.chdir(PATH)
    wb = openpyxl.Workbook()
    sheet = wb.get_active_sheet()
    sheet.title = 'Infrastructure'
    sheet['A1'] = 'Machine IP'
    sheet['B1'] = 'Serial No'
    sheet['C1'] = 'Manufacturer'
    fname = 'Infra-Info.xlsx'
    wb.save(fname)
    return fname

def write_data(host, serial_no, manufacturer):
    ''' Write data to Excel '''
    os.chdir(PATH)
    wb = openpyxl.load_workbook('Infra-Info.xlsx')
    sheet = wb.get_sheet_by_name('Infrastructure')
    rowNum = sheet.max_row + 1 
    sheet.cell(row=rowNum, column=1).value = host
    sheet.cell(row=rowNum, column=2).value = serial_no
    sheet.cell(row=rowNum, column=3).value = manufacturer
    wb.save('tmp-Infra-Info.xlsx')

def rename_file():
    os.chdir(PATH)
    os.remove('Infra-Info.xlsx')
    os.rename('tmp-Infra-Info.xlsx', 'Infra-Info.xlsx')

def extract_data(host, result_json):
    ''' Write data to the sheet
    '''
    serial_no = result_json['Hardware Specs']['System']['Serial Number']
    manufacturer = result_json['Hardware Specs']['System']['Manufacturer']
    if not os.path.exists('/tmp/Infra-Info.xlsx'):
        create_report_file()
    write_data(host, serial_no, manufacturer)
    rename_file()

class CallbackModule(object):

    def runner_on_ok(self, host, result):
        try:
            if result['var']['dmi_data[\'msg\']']:
                extract_data(host, result['var']['dmi_data[\'msg\']'])
        except:
            pass

前面的回调模块只是您如何在 Excel 电子表格中表示数据并生成报告的一个示例。回调模块可以扩展,以便根据报告的要求填写更多的细节。前面的模块仅添加主机、序列号和主机制造商。

请注意,由于上面的回调模块将数据追加到同一个 Excel 电子表格中,Ansible 应该一次在一台主机上执行任务。因此,您应该将叉子设置为1

这可以通过使用--forks标志来完成。以下代码是 Ansible 行动手册的执行方式:

ansible-playbook -i hosts dmidecode.yaml --forks 1

以下是生成的 Excel 报告:

Happy managers = happy you

总结

本章将带您了解各种可以使用 Ansible 的真实场景,以及如何扩展 Ansible 以满足您的需求。本章从 Ansible 的基础知识开始,比如定义角色和使用标签。然后,这一章逐渐发展到更复杂的场景,建立在前面几章的例子之上。本章还包括一个非常常见的场景,其中 Ansible 需要自定义配置,以便通过堡垒主机代理任务。这一章还向您介绍了如何利用 Ansible 来自动化一些常规任务,如报告。

总的来说,这一章结合了从前面几章中学到的一切,并提供了相同的真实场景和用例。