Puppet-容器化指南-全-

Puppet 容器化指南(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书教你如何利用容器化系统(如 Docker、Kubernetes、Docker Swarm 和 Docker UCP)的新优势,同时不失去适当配置管理的全局视野。你将学习如何将容器化的应用程序和模块与 Puppet 工作流集成。

本书内容

第一章,使用 Puppet 安装 Docker,讲解了如何使用 Puppet 创建一个基于 Docker 的开发环境。我们将学习如何安装 Vagrant 和 VirtualBox。然后,我们将了解 Puppet Forge 以及如何搜索模块和它们的依赖关系。我们还会简要介绍 r10k,它将作为我们从 Puppet Forge 到环境的传输机制。之后,我们将使用 Puppet 构建我们的环境。

第二章,使用 Docker Hub,详细介绍了 Docker Hub 生态系统的内容:什么是官方镜像,自动构建如何工作,以及当然,如何以三种不同方式使用镜像。

第三章,构建单容器应用程序,介绍了我们的第一个 Puppet 模块,用于创建 Docker 容器。在本章中,我们将学习如何编写 rspec-puppet 单元测试,确保我们的模块按预期工作。我们还将学习如何使用 metadata.jsonfixtures.yml 文件映射 Puppet 模块的依赖关系。

第四章,构建多容器应用程序,介绍了 Docker Compose。我们将学习 docker-compose .yaml 文件结构。接下来,我们将运用这些知识创建一个 Puppet 模板(.erb 文件)并将其封装成模块。我们还会介绍 Docker Compose 的功能,它将帮助我们扩展容器。

第五章,配置服务发现和 Docker 网络,介绍了在使用容器时的两个非常重要的话题。首先,我们将了解什么是服务发现,为什么需要它,以及服务发现的不同类型。

第六章,多节点应用程序,整合了你在本书中学到的所有技能。我们将提升难度。在本章中,我们将部署四台服务器,并学习如何搭建 Consul 集群。我们还将探讨两种网络容器的方式:首先是使用标准主机 IP 网络,供我们的 Consul 集群通信。我们还将安装 ELKElasticsearchLogstashKibana)栈。

第七章,容器调度器,介绍了 Docker Swarm 和 Kubernetes 等容器调度器。然后,我们将构建一个包含四个服务器、三个集群节点和一个主节点的开发环境。我们还将构建一个 Docker 网络和服务发现框架。

第八章,日志记录、监控和恢复技术,将在我们上一章创建的环境中,添加监控、日志记录和恢复技术。这将使我们的应用程序更加稳健,并准备好投入生产。

第九章,实践中的最佳实践,更侧重于在容器化环境中部署 Puppet 本身的最佳实践,利用你在前面几章中学到的所有新技能。在这段旅程结束时,读者将能够掌握 Puppet 和 Docker,并在实际应用中加以运用。

本书所需条件

本书需要 Intel i5 或以上处理器,8 GB 内存(推荐 16 GB),50 GB 可用磁盘空间,以及任何可以运行 Vagrant 的操作系统。

本书适合谁阅读

本书面向那些希望探索容器化技术的系统管理员。假定读者已经具备中级 Puppet 使用经验。

约定

本书中你将看到多种文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。

文字中的代码、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名显示如下:“我们对 servers.yaml 文件做的另一个更改是,我们添加了条目到 /etc/hosts 目录中。”

所有的命令行输入或输出如下所示:

command: -server --client 0.0.0.0 --advertise <%= @consul_advertise %>  -bootstrap-expect <%= @consul_bootstrap_expect %>

新术语重要词汇 用粗体显示。你在屏幕上看到的词汇,例如在菜单或对话框中,会像这样出现在文本中:“接下来我们需要做的是点击 创建 按钮。”

注意

警告或重要提示会以如下框框显示。

提示

提示和技巧会以如下方式显示。

读者反馈

我们始终欢迎读者反馈。让我们知道你对本书的看法——你喜欢或不喜欢的部分。读者反馈对我们非常重要,它帮助我们开发出你能真正受益的书籍。

若要向我们发送一般反馈,请通过电子邮件联系 <feedback@packtpub.com>,并在邮件主题中注明书名。

如果你在某个主题上有专业知识,并且对写作或贡献一本书感兴趣,请参阅我们的作者指南:www.packtpub.com/authors

客户支持

现在,既然你已经拥有了一本 Packt 的书籍,我们为你提供了许多帮助,以便你从购买中获得最大的收益。

下载示例代码

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

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

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持选项卡上。

  3. 点击代码下载和勘误表

  4. 搜索框中输入书名。

  5. 选择您希望下载代码文件的书籍。

  6. 从下拉菜单中选择您购买本书的地方。

  7. 点击代码下载

文件下载后,请确保使用最新版本的解压软件解压或提取文件夹:

  • 适用于 Windows 的 WinRAR / 7-Zip

  • 适用于 Mac 的 Zipeg / iZip / UnRarX

  • 适用于 Linux 的 7-Zip / PeaZip

下载本书的彩色图片

我们还为您提供了一个包含本书中使用的截图/图表的彩色图片的 PDF 文件。这些彩色图片将帮助您更好地理解输出中的变化。您可以从 www.packtpub.com/sites/default/files/downloads/PuppetforContainerization_ColorImages.pdf 下载此文件。

勘误表

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

要查看之前提交的勘误表,请访问 www.packtpub.com/books/content/support,并在搜索框中输入书名。所需的信息将显示在勘误表部分。

盗版

网络上的版权盗版问题在所有媒体中持续存在。Packt 对我们的版权和许可非常重视。如果您在互联网上遇到我们作品的任何非法复制品,请立即提供位置地址或网站名称,以便我们采取补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。

我们感谢你在保护我们的作者和我们提供有价值内容的能力方面给予的帮助。

问题

如果你在本书的任何部分遇到问题,可以通过 <questions@packtpub.com> 与我们联系,我们将尽最大努力解决问题。

第一章:使用 Puppet 安装 Docker

在本章中,我们将设置我们的开发环境,以便开发第一个容器应用程序。为此,我们将使用 Vagrant。在第一个话题中,我们将介绍如何安装 Vagrant。我们将了解如何使用 Puppet 作为提供者构建 Vagrantfile。我们还将探讨如何通过 puppetfile 和 r10k 从 Puppet Forge 获取 Puppet 模块。在最后一个话题中,我们将使用 Puppet 在 Centos 7 环境中安装 Docker。本章我们将涵盖以下主题:

  • 安装 Vagrant

  • Puppet Forge 介绍

  • 安装 Docker

安装 Vagrant

你可能会问,为什么我们要使用 Vagrant 作为开发环境?

Vagrant 是 Puppet 开发的必备工具。能够在几分钟内为本地开发创建环境,这一想法是 Vagrant 初期版本的革命性特点。如今,Vagrant 已经发展迅速,支持多个提供者,如 Chef 和 Salt。并且支持多种虚拟化后端,如 VirtualBox、VMware Workstation/Fusion、KVM,我们将使用 VirtualBox 和 Puppet 作为提供者。

安装

让我们安装 Vagrant。首先,我们需要我们的虚拟化后端,所以我们先下载并安装 VirtualBox。在撰写本书时,我们使用的是 VirtualBox 5.0.10 r104061。如果你在阅读本书时发现版本已经过时,只需下载最新版本即可。

你可以从www.virtualbox.org/wiki/Downloads下载 VirtualBox。选择适合你操作系统的版本,如下图所示:

安装过程

下载完安装包后,按照适合你操作系统的安装过程进行操作。

VirtualBox

按照以下步骤在 Mac OSX 上安装 Vagrant:

  1. 前往你的 Downloads 文件夹,双击 VirtualBox.xxx.xxx.dmg。接下来会弹出以下安装框:VirtualBox

  2. 然后,点击 VirtualBox.pkg。继续进入下一步,如下图所示:VirtualBox

    安装程序将检查该软件是否与 Mac OSX 版本兼容。

  3. 然后,点击 继续。检查通过后,我们可以进入下一步:VirtualBox

  4. 接下来,选择默认的安装位置并点击 安装

  5. 接着,输入你的管理员密码并点击 安装软件VirtualBox

安装现在已经完成。下图显示了完成安装后的屏幕界面:

VirtualBox

现在我们已经有了虚拟化后端,可以安装 Vagrant 了:

VirtualBox

注意

在撰写本书时,我们将使用 Vagrant 1.7.4;如果该版本不再是最新版本,请下载最新版本。您可以在 www.vagrantup.com/downloads.html 找到该版本的 Vagrant。再次提醒,请下载适用于您操作系统的安装包。

Vagrant

在这里,我们将进行一个标准安装。请按照以下步骤进行操作:

  1. 转到您下载 vagrant.1.7.4.dmg 的文件夹,双击安装程序。然后,您将看到以下弹窗:Vagrant

  2. 双击 vagrant.pkg

  3. 然后,在下一个对话框中,点击 继续Vagrant

  4. 然后,点击 安装 按钮:Vagrant

  5. 在给定的字段中输入您的管理员密码:Vagrant

  6. 安装完成后,打开您的终端应用程序。在命令提示符中输入 vagrant。然后,您应该会看到以下截图:Vagrant

Vagrantfile

现在我们已经有了一个完全工作的 Vagrant 环境,我们可以开始查看 Vagrant 是如何工作的,以及我们如何为我们的机器进行配置。由于本书并不是关于 Vagrant 的,所以我们不会从头编写一个 Vagrantfile。相反,我已经创建了一个我们将在全书中使用的 Vagrantfile:

Vagrantfile

注意

您可以从 github.com/scotty-c/vagrant-template 下载或 Git 拉取该仓库。

让我们来看一下 Vagrantfile 的构建:

Vagrantfile

正如前面的截图所示,Vagrantfile 实际上是一个 Ruby 文件。由于它是 Ruby 文件,我们可以利用 Ruby 来使我们的代码优雅且高效。因此,在这个 Vagrantfile 中,我们提取了所有底层配置,并用几个参数替代它们。为什么我们要这么做?原因是将逻辑与配置分离,并且通过迭代配置来避免代码重复。那么,所有的配置都存储在哪里呢?答案是 servers.yaml 文件。在这个文件中,我们设置了我们想要部署的 Vagrant box、box 的 CPU 数量、内部网络的 IP、主机名、来宾与主机之间的转发端口、内存以及我们需要用来准备环境供 Puppet 运行的 bash 命令的 shell 提供者,例如,从 Puppet Forge 下载模块及其依赖项:

Vagrantfile

这种方法的好处还在于,任何使用 Vagrantfile 的开发者都不需要实际修改 Vagrantfile 中的逻辑。他们只需要更新servers.yaml中的配置。随着我们继续本书的内容,我们将一起工作仓库中的其他文件,比如Puppetfilehieradatamanifests。现在我们已经设置好了 Vagrant 环境,接下来让我们看看如何从 Puppet Forge 获取我们的 Puppet 模块。

欢迎来到 Puppet Forge

在本主题中,我们将学习如何从 Puppet Forge 查找模块。接着,我们将看到如何通过 puppetfile 和 r10k 拉取这些模块及其依赖项。这为我们最后一个主题——使用 Puppet 安装 Docker奠定基础。

Puppet Forge

Puppetlabs 及其产品的一大亮点是其社区。如果你有机会参加 PuppetConf 或 Puppet Camp,无论你住在哪个地方,我都强烈推荐你参加。那里会有丰富的知识,你还能结识到一些非常棒的人。

Puppet Forge 是由 puppetlabs 运行的一个网站。它是其他 Puppet 开发者发布已准备好使用的模块的地方。你可能会问,GitHub 呢?难道不能从那里获取模块吗?当然可以。从 Puppet Forge 和 GitHub 的区别在于,Puppet Forge 是模块的稳定版本,而 GitHub 是贡献模块的地方,也就是说,是用来创建 pull request 的地方。

注意

你可以在Puppet Forge找到 Puppet Forge。

以下截图显示了 Puppet Forge 的首页:

The Puppet Forge

现在我们已经了解了 Puppet Forge,接下来让我们利用它来查找我们将用来构建环境的 Docker 模块。

注意

我们将使用 garethr/docker Docker 模块,你可以在Puppet Forge上找到它。

现在我们已经选择了模块,可以继续设置我们的 puppetfile:

The Puppet Forge

创建我们的 puppetfile

在前一个主题中,我们使用 Git 克隆了 Vagrant 模板。在那个仓库中,也有一个 puppetfile。puppetfile 作为我们模块的控制文件,它会列出所有我们需要的模块(在这个例子中,就是安装 Docker)。r10k 随后会引用 puppetfile,将模块从 Puppet Forge 拉取到我们环境的目录中。

由于模块之间有依赖关系,我们需要确保在 puppetfile 中捕捉到它们。对于 Docker 模块,我们有三个依赖项:puppetlabs/stdlib (>= 4.1.0)puppetlabs/apt (>= 1.8.0 <= 3.0.0)stahnma/epel (>= 0.0.6),如下面的截图所示。

现在,我们知道了构建 Docker 环境所需的所有模块。我们只需要将它们添加到我们的 puppetfile 中。

以下截图是 puppetfile 应该是什么样子的示例:

创建我们的 puppetfile

现在,当我们运行 vagrant up 时,r10k 将从 Puppet Forge 拉取模块。我们在 servers.yaml 的第 13 行调用 r10k,使用 r10k puppetfile install—verbose 命令。以下截图显示了该命令的输出:

创建我们的 puppetfile

如果成功,终端将显示以下输出:

创建我们的 puppetfile

现在我们已经设置好 puppetfile,就可以安装 Docker 了。

安装 Docker

本节中,我们将结合 Vagrant 仓库中的所有配置和对 Puppet Forge 的了解,创建 Docker 环境。

设置我们的清单

安装 Docker 的第一步是将我们的清单设置为在节点上包含 Docker 类。为此,让我们进入我们的 Vagrant 仓库。在该仓库中,manifests 目录下有一个名为 default.pp 的文件。我们需要编辑该文件,以包含 Docker 类 node 'node-01' { include docker}。现在我们可以保存该文件,并准备好运行我们的环境。

第一步是打开终端并切换到 Vagrant 仓库的根目录。然后,我们需要输入 vagrant up 命令:

设置我们的清单

现在,我们将得到 CentOS 7 环境。安装 r10k 后,运行 Puppet 并应用 Docker 类。根据你的笔记本和网络连接,这个过程大约需要 4 分钟。如果环境成功配置,你将看到以下输出:

设置我们的清单

我们还可以通过 SSH 登录到环境中来验证 Docker 是否安装成功。我们可以使用 vagrant ssh 命令进行登录。登录后,我们将使用 sudo -i 提升为 root 用户。现在,让我们通过 docker 命令检查 Docker 是否已经安装。

你将在终端看到以下输出:

设置我们的清单

总结

在本章中,我们讲解了如何使用 Puppet 创建 Docker 开发环境。我们首先安装了 Vagrant 和 VirtualBox,然后介绍了 Puppet Forge,讲解了如何搜索模块及其依赖项。接着,我们将依赖项映射到 puppetfile 中。我们简要介绍了 r10k,这是从 Puppet Forge 到我们环境的传输机制。最后,我们使用 Puppet 构建了我们的环境。

在下一章中,我们将介绍如何访问 Docker Hub 并拉取公共镜像。

第二章:与 Docker Hub 的工作

在本章中,我们将了解 Docker Hub,它是什么,如何注册账号,如何拉取镜像,如何推送镜像,以及自动化镜像构建。这将为我们在需要与官方镜像打交道时打下一个良好的基础。

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

  • 什么是 Docker Hub?

  • 自动化构建

  • 使用官方镜像

使用 Docker Hub

在本节中,我们将讨论 Docker Hub,它的用途是什么,提供了哪些功能,最后它与其他仓库网站(如 GitHub 或 Puppet Forge)有何不同。接下来,我们将创建一个账号并探索我们的账户设置。之后,我们将查看官方镜像,为下一个主题打下坚实的基础。

Docker Hub 概述

在上一章中,我们看了 Puppet 的仓库服务,也就是社区称之为 The Forge(forge.puppetlabs.com/)。现在,让我们来看一下 Docker 的仓库服务——Docker Hub。我们可以在hub.docker.com/找到 Docker Hub。

以下截图显示了屏幕的样子:

Docker Hub 概述

在 Docker Hub 中,有两种类型的镜像:

  • 官方镜像

  • 开发者创建的镜像

首先,我们将讨论官方镜像。在 Docker Hub 上,你几乎可以获取到任何主流操作系统或应用程序的官方镜像。因此,对于你作为开发者的好处是,安装应用程序的工作已经为你完成,从而节省了时间和精力。这使你能够将时间集中在开发上。让我们看一个例子——我们将使用 golang。

首先,我们将在首页右上角的搜索框中搜索 golang,如下图所示:

Docker Hub 概述

我们的搜索将返回以下结果:

Docker Hub 概述

我们将点击 golang 的官方版本,如下图所示:

Docker Hub 概述

如我们在前面的截图中看到的,这个仓库为我们提供了很多选择。所以,我们可以使用多个不同版本的 golang,甚至在多个不同的操作系统上使用。所以,构建一个 golang 应用程序,我们只需要选择一个镜像。在我们的 Dockerfile 中,我们将使用以下镜像:

Docker Hub 概述

然后,我们将在 Dockerfile 中使用COPY方法将代码放入构建时的容器中。最后,我们将运行以下截图所示的命令来构建我们的容器:

Docker Hub 概述

如你所见,构建我们的应用非常简单,几乎所有开发时间都可以专注于实际应用程序的开发。这将提高我们的生产力,并大大加快将应用推向生产环境的速度。在这个敏捷至上的时代,不看到这种好处简直是不理智的。

Docker Hub 上的第二类镜像是由开发者开发并开源的,且由他们个人维护。判断一个镜像是否为官方镜像,或者是否由个人开发,最简单的方法就是查看镜像的名称。在我们上一个例子中,我们看到了 golang 镜像,它的名称是 golang。现在,让我们来看一下我开源的一个容器。作为示例,我们来看一下我的 consul 镜像。如果你想使用我的镜像,你可以调用 scottyc/consul。如你所见,镜像名称不同,因为它包含了作者名称 scottyc,然后是镜像名称 consul。现在,你可以看到官方镜像和个人作者镜像之间的命名规则差异。

现在我们已经介绍了在 Docker Hub 上托管的不同镜像,接下来我们可以讲解如何将镜像上传到 Docker Hub。上传镜像有两种不同的方式。无论哪种方式,我们都需要一个 Docker Hub 账户,接下来我们会在下一节讲解如何创建账户。

第一种方式是本地构建镜像,然后使用 docker push 命令进行推送。第二种方式是使用自动构建,这是 Docker 内置的一个非常棒的功能,支持在 Docker Hub 上自动构建镜像。我们稍后会更详细地讲解这个过程。从高层次来看,这是一个CD持续交付)流程,通过存储在 GitHub 或 Bitbucket 公共仓库中的 Dockerfile 来构建镜像。

创建 Docker Hub 账户

在本节中,我们将创建一个 Docker Hub 账户,并查看如何手动登录到 Docker 守护进程(我们将在下一章讲解如何通过 Puppet 完成此操作)。那么,我们开始吧。首先,我们需要访问 Docker Hub(hub.docker.com/),然后在页面右侧填写表单。只需将 yourusername 替换为你想要的用户名,you@youremail.com 替换为你的电子邮件地址,当然,输入一个安全的密码:

创建 Docker Hub 账户

填写完表单后,前往你的电子邮件账户,确认你的账户。这会将你重定向到 Docker 登录页面。登录后,你应该会看到如下网页:

创建 Docker Hub 账户

现在我们已经有了一个账户,接下来让我们登录到我们的守护进程。我们可以通过 vagrant ssh 重新登录到 Docker vagrant 环境。接着,我们将切换到 root 用户(sudo –i),然后输入 docker login 命令:

创建 Docker Hub 账户

输入我们刚才创建的用户名:

创建 Docker Hub 账户

然后,输入你的密码:

创建 Docker Hub 账户

完成后,输入你的电子邮件 ID。完成后,你应该看到以下输出:

创建 Docker Hub 账户

你现在已经成功登录 Docker 守护进程。

探索官方镜像

本主题将简要概述如何在 Docker Hub 上搜索镜像。有两种方法可以做到这一点:

  • 通过 Docker Hub 网站

  • 通过命令行

首先让我们看看网站。如果你还记得,在我们的 Golang 示例中,我们已经使用了 Web 界面来搜索镜像。我们再来看一个例子。在这个例子中,我们将搜索 Bitbucket,Atlassian 的 Git 服务器。所以,我们将返回到 Docker Hub (hub.docker.com/),并在搜索框中输入 bitbucket

探索官方镜像

我们的搜索将返回以下结果:

探索官方镜像

从上面的截图中可以看到,我们得到了 43 个结果。那么,我们应该寻找哪些标准来选择正确的镜像呢?我们总是会寻找以下三点:

  • 我们检查拉取次数。使用人数越多,镜像运行时出现问题的可能性就越小。

  • 我们还检查 Docker 官方的评分系统:一个仓库有多少颗星。星星是由社区中的其他成员在他们喜欢某个镜像时授予的,这与 GitHub 上的星标系统非常相似。

  • 我们检查仓库中是否有 Dockerfile。这让你对镜像是如何构建的有了安心感。你可以看到为完成构建而执行的所有命令。

使用这三项指标,我们来挑选一个镜像。看着这些结果,atlassian/bitbucket-server 看起来不错,拥有 21 颗星和 7.3k 次拉取。所以,让我们点击这个仓库并查看是否有 Dockerfile:

探索官方镜像

如果我们点击主镜像标题下的 Dockerfile 标签,它会带我们到 Dockerfile 页面。并非每个仓库都有 Dockerfile;然而,这并不意味着它是一个不好的镜像。这只是意味着在将其用于生产之前需要更多的测试。一些作者,比如来自 Docker 的 Jess (Jessie Frazelle),把他们的 Dockerfile 放在 GitHub 页面上。她在 Docker Hub 上有很棒的镜像,Dockerfiles 可以在 github.com/jfrazelle/dockerfiles 找到。好了,回到我们的例子。如你所见,以下截图中有一个 Dockerfile:

探索官方镜像

所以,我觉得这是赢家!!!

现在,让我们从命令行进行相同的搜索。在命令行中输入 docker search bitbucket,搜索将返回以下结果:

探索官方镜像

如你所见,它返回了相同的信息,唯一缺少的是拉取次数。同样,似乎我们将使用 atlassian/bitbucket-server

Docker Hub 中的自动构建

在本节中,我们将概述自动化构建如何工作,并演示如何通过推送方法将镜像发布到 Docker Hub。

自动化构建

在 Docker Hub 中,我们有两种方式来发布镜像:通过简单的推送方法或通过自动化构建。在本节中,我们将介绍自动化构建。首先,我们将看一下自动化构建的流程。在本示例中,我们将使用 GitHub,但你也可以使用 Bitbucket。因此,我们需要做的第一件事是将我们的 Docker Hub 账户与 GitHub 账户关联。可以通过导航至设置 | 已连接账户与服务来完成:

自动化构建

只需按照提示连接账户即可。

完成此步骤后,接下来我们需要访问我们的 GitHub 账户并创建一个仓库。我将使用我已经设置好的仓库:

自动化构建

正如你在前面的截图中看到的,仓库包含一个 Dockerfile。现在,让我们看看在 Docker Hub 上查看同一个仓库的情况:

自动化构建

接下来,我们将查看构建详情选项卡:

自动化构建

那么,这个构建是如何自动化的呢?其实很简单。每次我们向 GitHub 仓库提交更改时,它都会触发 Docker Hub 的 Webhooks。当 Docker Hub 接收到触发时,它会抓取 Dockerfile 并构建镜像。Docker Hub 会处理每次构建时的版本号等事宜。所以,总的来说,自动化构建就是这样工作的。

推送到 Docker Hub

这是一种将镜像推送到 Docker Hub 的相对简单方法,但缺点是没有自动化构建过程,且 Dockerfile 不会自动放入 Docker Hub 仓库。因此,在本示例中,我们假设我们已经创建了一个名为 scottyc/super_app 的镜像。要将其推送到 Docker Hub,我们只需在终端输入 docker push scottyc/super_app。请注意,在推送时,Docker 守护进程需要登录。

使用官方镜像

现在我们已经了解了 Docker Hub 如何为我们提供镜像,接下来让我们看看如何通过三种方法将它们集成到我们的代码中。第一种方法是 Dockerfile,第二种是 docker-compose.yaml 文件,最后一种是直接集成到 Puppet 清单中。

Dockerfile

在本节中,我们将展示如何在基础 Dockerfile 中使用 nginx。在 Dockerfile 中,我们需要添加几个元素。第一个是作为我们应用程序基础的镜像;对我们而言,它是 nginx。第二个是维护者。它应该看起来像以下截图所示:

Dockerfile

由于基础 nginx 镜像已经暴露了 80 和 443 端口,我们在 Dockerfile 中无需进行该配置。接下来,我们将添加一个简单的 run 命令,用于更新容器中的软件包。由于它的基础操作系统是 Debian,我们将添加如下截图中第 5 行所示的命令:

Dockerfile

由于我们正在构建一个简单的应用程序,这就是我们将在 Dockerfile 中添加的所有内容。Dockerfile 还有很多配置选项可以使用。

注意

如果你想了解更多关于 Dockerfile 的内容,可以访问 docs.docker.com/engine/reference/builder/

现在,让我们构建我们的镜像。你会注意到,我们的 Vagrant 仓库中的 server.yaml 已经将端口 80 转发到了端口 8080,所以我们无需在这里做任何更改。将我们创建的 Dockerfile 复制到你的 Vagrant 仓库的根目录中。然后,从终端使用 vagrant up 启动我们的 vagrant box。接着,当 box 启动后,使用 vagrant ssh 连接。让我们切换到 root 用户(sudo -i)。然后,如果我们切换到 /vagrant 目录,我们应该能看到我们的 Dockerfile。现在,使用命令 docker build -t YOUR AUTHOR NAME/nginx . 构建我们的镜像(请注意,命令中的 . 是命令的一部分)。你将在终端中看到以下输出:

Dockerfiles

接下来,让我们测试我们的镜像并通过以下命令启动一个容器:

docker run -d -p 80:80 --name=nginx YOUR AUTHOR NAME/nginx

如果成功,我们应该能在浏览器中看到 nginx 默认页面,网址为 127.0.0.1:8080,如下所示:

Dockerfiles

Docker Compose

现在,我们将使用 Docker Compose 部署相同的 nginx 镜像。在本节中,我们将简要介绍 Docker Compose 以便理解这项技术。在本书的另一章节中,我们会更深入地探讨它。首先,我们需要安装 Docker Compose。

提示

在写这本书时,我的 pull request 仍然是开放的,所以我们必须使用 Gareth 模块的我的分支。

为此,让我们修改 Vagrant 仓库中的 puppetfile,使用下方截图中显示的命令:

Docker Compose

所以,在 Puppetfile 中,我们添加了一个新的模块依赖项 stankevich/python,因为 Docker Compose 是用 Python 编写的。我们还更新了 epel 模块以使用最新版本。为了获得一个全新的工作环境,我们将在终端中运行命令 vagrant destroy && vagrant up。一旦 box 启动,我们将使用 vagrant ssh 然后切换到 root 用户(sudo -i)。接着,我们会切换到 /vagrant 目录并输入 docker-compose

如果构建成功,我们将看到以下界面:

Docker Compose

现在,让我们创建 docker-compose.yaml 文件:

Docker Compose

如你所见,我们使用了官方镜像,并给容器命名为 nginx,再次暴露了端口 80:80 以便访问 nginx 页面。因此,如果我们将 docker-compose.yml 文件复制到 Vagrant 目录的根目录,登录到我们的 vagrant box 并切换到 root 用户(vagrant ssh,然后 sudo -i),我们将能够再次切换到 /vagrant 目录。现在,运行 docker-compose up -d。运行后,我们将看到以下输出:

Docker Compose

然后我们可以打开我们的网页浏览器,访问127.0.0.1:8080,看到 nginx 页面:

Docker Compose

注意

如果你想了解更多关于 Docker Compose 的内容,可以访问docs.docker.com/compose/

Puppet 清单

在本节中,我们将通过一个简单的 Puppet 清单来构建相同的 nginx 容器。这仅仅是一个概念验证。在下一章中,我们将编写一个完整的模块。这只是为了给我们一个基础,理解 Puppet 是如何与 Docker 互动的。所以,在我们的 Vagrant 仓库中,让我们修改manifest/default.pp。文件应包含以下代码:

Puppet 清单

然后我们将在 Vagrant 仓库的根目录打开终端,并运行vagrant provision。请注意,此时应没有其他容器在运行。你将看到以下输出,显示 Puppet 已经配置了一个名为 nginx 的 Docker 容器:

Puppet 清单

然后我们可以再次检查我们的浏览器,访问127.0.0.1:8080。我们将再次看到 nginx 页面:

Puppet 清单

总结

在本章中,我们详细介绍了 Docker Hub 生态系统。我们讨论了什么是官方镜像,自动构建是如何工作的,当然还有如何以三种不同的方式使用镜像。完成本章后,我们现在拥有了使用 Puppet 构建第一个应用的工具。

在下一章中,我们将编写第一个 Puppet 模块来创建一个 Docker 容器,并编写 rspec-puppet 单元测试,确保我们的模块能够按照预期工作。

第三章:构建单一容器应用

在这一章中,我们将编写第一个模块来部署我们的第一个容器化应用。我们将要部署的应用是来自 HashiCorp 的 Consul(www.consul.io/)。稍后我们会稍微介绍一下 Consul。我们首先要看的内容是如何构建一个包含正确文件结构、单元测试和 gems 的 Puppet 模块。一旦我们拥有了模块骨架,我们将探索使用 Puppet 在容器中部署 Consul 的两种方式。第一种方式是使用清单中的资源声明,第二种方式是使用 Docker Compose 作为模板 .erb 文件。这一章我们将涵盖以下主题:

  • 构建 Puppet 模块骨架

  • 使用资源声明编程

  • 使用 .erb 文件编程

构建 Puppet 模块骨架

开发中的一项最重要的事情是拥有一个坚实的基础。编写 Puppet 模块也不例外。本章内容对本书的其余部分至关重要,因为从现在开始,我们将一遍又一遍地重用代码来构建所有模块。我们将首先学习如何使用 Puppet 模块生成器构建模块。一旦我们拥有了模块骨架,我们将探讨其结构。我们将了解 Puppet 使用 Ruby 实现的基础设施,最后学习基本的单元测试。

Puppet 模块生成器

使用 Puppet 的一大优势是有很多工具可用,既来自社区,也来自 puppetlabs 自身。Puppet 模块生成器是一个由 puppetlabs 开发的工具,遵循最佳实践来创建模块骨架。这个工具的最大优点是它与每个 Puppet 代理的安装捆绑在一起。因此,我们不需要安装任何额外的软件。现在,让我们登录到上章中创建的 Vagrant 盒子。切换到 Vagrant 仓库的根目录,然后使用 vagrant up && vagrant ssh 命令登录。登录到盒子后,让我们使用 sudo 切换到 root 用户(sudo -i),并将目录切换到 /vagrant。这样做的原因是该文件夹将映射到我们本地的盒子中。之后,我们可以在本章稍后的部分使用我们喜欢的文本编辑器。进入 /vagrant 后,我们可以运行命令来构建 Puppet 模块骨架。对我来说,puppet module generate <AUTHOR>-consul 命令将如下所示:puppet module generate scottyc-consul

然后脚本会提出一些问题,比如版本、作者名称、描述、源代码位置等。这些问题在你想要将模块发布到 Puppet Forge(forge.puppetlabs.com/)时非常重要,但现在,我们只需按以下示例回答问题:

Puppet 模块生成器

现在我们已经有了 Puppet 模块骨架,接下来我们来看一下它的结构是怎样的:

Puppet 模块生成器

现在,我们将添加一些文件来帮助我们进行单元测试。第一个文件是.fixtures.yml。该文件由spec-puppet在我们运行单元测试时用来将任何模块依赖项拉取到spec/fixtures目录。对于这个模块,.fixtures.yml文件应如下图所示:

Puppet 模块生成器

接下来,我们要添加的文件是.rspec文件。这个文件是rspec-puppet在需要spec_helper时使用的,它为我们的单元测试文件夹结构设置了模式。文件内容应如下截图所示:

Puppet 模块生成器

现在我们已经有了文件夹结构,让我们安装运行单元测试所需的 gems。我的个人偏好是将 gems 安装到 Vagrant 虚拟机中;如果你想在本地机器上使用也没问题。那么,让我们登录到 Vagrant 虚拟机(进入 Vagrant 仓库的根目录,使用vagrant ssh命令,然后使用sudo -i切换到 root 用户)。首先,我们将使用yum install -y ruby安装 Ruby。安装完成后,让我们进入/vagrant/<your modules folder>目录,然后运行gem install bundler && bundle install。你应该看到以下输出:

Puppet 模块生成器

如前面的截图所示,我们收到了一些警告。这是因为我们以 root 用户运行了gem install。在生产系统中我们不会这么做,但由于这是我们的开发虚拟机,这不会造成问题。现在我们已经安装了所有单元测试所需的 gems,让我们在/spec/classes/init_spec.rb中添加一些基本的事实。我们将要添加的事实是osfamilyoperatingsystemrelease。所以,文件将如下截图所示:

Puppet 模块生成器

最后,我们将编辑的是仓库根目录中的metadata.json文件。该文件定义了我们的模块依赖项。对于这个模块,我们有一个依赖项docker,所以我们需要将其添加到metadata.json文件的底部,如下截图所示:

Puppet 模块生成器

我们需要做的最后一件事是将所有内容放置在我们 Vagrant 仓库的适当位置。我们通过在 Vagrant 仓库的根目录中创建一个名为modules的文件夹来实现这一点。然后,我们执行mv <AUTHOR>-consul/ modules/consul命令。请注意,我们移除了作者名,因为我们需要该模块来复制在 Puppet 主机上的样子。现在我们已经准备好了基本的模块骨架,接下来可以开始编写代码了。

使用资源声明进行编码

在本节中,我们将使用我们的模块骨架来构建第一个 Docker 应用程序。我们将使用标准的 Puppet 清单来编写它。

但首先,为什么我们写的第一个模块是 Consul?我选择这个应用程序有几个原因。首先,Consul 拥有很多强大的功能,如服务发现和健康检查,还可以用作键值存储。第二个原因是,稍后在本书中,我们将使用我刚才提到的所有功能。因此,当我们查看 Docker Swarm 时,这将派上用场。

文件结构

让我们在 manifests 文件夹中创建两个新文件,install.ppparams.pp。结构应该如下所示:

文件结构

编写我们的模块

让我们开始编写我们的模块。我们将从 init.pp 开始;这个模块不会非常复杂,因为我们只需要添加几行代码和一些参数。正如前面的截图所示,我们在 manifests 目录中创建了三个文件。当我编写一个模块时,我总是喜欢从 params.pp 开始,因为它为我提供了一个很好的起始结构,可以用于编写提供模块逻辑的代码。所以,让我们看一下这个模块的 params.pp,如下图所示:

编写我们的模块

现在,让我们看一下我们设置的参数:

  • $docker_version:这是我们将要安装的 Docker 版本。

  • $docker_tcp_bind:这是 Docker API 将要绑定的 IP 地址和端口。

  • $docker_image:这是我们将从 Docker Hub 使用的 Docker 镜像。我们将使用我的 Consul 镜像。要了解更多关于该镜像的信息或获取 Dockerfile,请访问hub.docker.com/r/scottyc/consul/

  • $container_hostname:这将设置容器内的主机名。

  • $consul_advertise:这是 Consul 将要广播的 IP 地址。我们将使用一个内置的 Puppet fact,$::ipaddress_enp0s8

  • $consul_bootstrap_expect:这设置了 Consul 集群中的节点数量。我们只使用一个节点。如果是生产集群,至少需要使用三个节点。

现在我们已经设置好了参数,让我们开始编写 install.pp。顾名思义,这个类将包含安装 Docker、拉取镜像并运行容器的逻辑。所以,让我们看看以下截图中显示的代码:

编写我们的模块

为了更深入地查看代码,我们将把类分为两部分,Docker 安装和容器配置。在 Docker 安装中,第一段代码是 device-mapper-libs 的简单包类型。我们确保安装这个包及其依赖项的原因是,它将成为 Docker 用来挂载容器文件系统的存储驱动。

现在,我们继续进行 Docker 安装。我们首先声明docker类。对于这个类,我们将设置 Docker 版本,调用我们在params.pp中设置的参数,我们使用的 Docker 版本是 1.9.1(这是编写本书时的最新版本)。接下来的配置是 Docker API 的 TCP 绑定。同样,我们将调用params.pp类并将其值设置为tcp://127.0.0.1:4242。这将使 API 绑定并监听本地主机地址的 TCP 端口4242

我们将要为 Docker 安装设置的最后一个值是 Unix 套接字,Docker 将使用该套接字。我们将声明它,而不调用参数。代码的最后一部分确保device-mapper-libs在 Docker 之前安装,因为它是 Docker 安装的前提条件:

编写我们的模块

既然我们已经安装了 Docker,让我们来看一下构建 Consul 容器的代码。我们调用的第一个类是docker::image。在调用docker::run类之前,它会从 Docker Hub 拉取镜像。在docker::run类中,我们将navmar设置为与容器主机名相同的值。我们将从params.pp中获取该值,并将其设置为consul

我们将要设置的下一个配置是镜像。这与调用docker::image不同。当你调用docker::image时,它会从 Docker Hub 拉取镜像到本地文件系统。当我们在docker::run类中设置镜像值时,它会设置容器部署所需的基础镜像值。该值被设置为scottyc/consul,我们同样会从params.pp中获取这个值。hostname参数将设置容器内的主机名。

现在我们来看resource属性,它将运行时配置的参数传递给容器。command属性是一个任意属性,允许你在启动时将配置传递给容器。在本例中,我们将为 Consul 设置传递启动配置,包括服务器角色、Consul 应用程序将绑定的 IP 地址,以及 Consul 集群中的服务器数量。在第一个例子中,我们传递给command属性的所有参数值都来自params.pp

编写我们的模块

现在,最后但绝对重要的一步,让我们看看我们的init.pp文件包含了什么。你会注意到,在主类声明后面的顶部,所有参数都映射到了params.pp。我们这样做的原因是为了在params.pp中设置一些合理的配置或默认值,而对于敏感数据,我们可以通过 Hiera 查找来覆盖这些默认值。我们将在下一章中讨论 Hiera 查找。代码的最后一行包括了我们的consul::install类,这部分我们在前面的章节中已经讲解过:

编写我们的模块

现在,让我们运行我们的模块。

运行我们的模块

现在我们已经编写了我们的模块,我确信我们都很乐意运行它;但是,在我们这样做之前,我们需要向servers.ymldefault.pp添加一个配置片段。首先,我们需要确保我们的模块consul位于modules/consul中。接下来的步骤是打开我们的servers.yml文件,并在 shell 命令底部添加以下行:

- { shell: cp /home/vagrant/node-01/modules/* -R /tmp/modules }

这将把我们的模块复制到 Vagrant 盒子中的正确模块路径。我们还需要转发 Consul 端口,以便可以访问 GUI。通过添加- { guest: 8500, host: 8500 }到转发端口属性来完成。它应如下屏幕截图所示:

运行我们的模块

现在,让我们打开我们的manifests目录并编辑default.pp。我们只需要将我们的模块添加到节点定义中。您可以通过添加include consul配置来实现这一点,如下面的屏幕截图所示,并保存这两个文件:

运行我们的模块

让我们前往我们的终端,将目录更改为我们的 Vagrant 存储库的根目录,并输入vagrant up命令。现在,如果此虚拟机已经在运行,可以执行vagrant destroy -f && vagrant up命令。输出应如下屏幕截图所示:

运行我们的模块

即使我们已经成功运行了 Puppet,容器首次启动可能需要几分钟,因为它从 Docker Hub 下载镜像,所以请耐心等待。您可以通过在浏览器中输入127.0.0.1:8500来轻松检查容器何时启动。您应该看到 Consul GUI,如以下屏幕截图所示:

运行我们的模块

正如您所见,我们有一个名为consul的运行节点,这是我们给容器命名的主机名。

使用.erb文件进行编码

在本主题中,我们将使用docker-compose部署相同的容器,但有所不同。不同之处在于使用 Puppet,我们可以将docker-compose.yml文件转换为docker-compose.yml.erb。这使我们能够利用 Puppet 提供的所有工具来操作模板文件。这绝对是我喜欢使用 Puppet 部署容器的方式;然而,随着我们继续阅读本书,我会让您自己决定最喜欢的方法。

使用 Docker Compose 编写我们的模块

在本章中,我们将看看如何将docker-compose作为.erb模板文件使用。在本例中,我们仅部署单个容器,但是当应用程序包含五到六个带有链接的容器时,这种方式比使用标准清单声明更为高效。

因此,我们将采用上一主题中的consul模块,并现在修改它以使用docker-compose。如果您想保留该模块,只需复制即可。首先,我们不会触及init.ppparams.pp——它们将保持不变。现在,让我们来看看install.pp

使用 Docker Compose 编写我们的模块

如你所见,在上面的截图中,类的上半部分完全相同。然而,我们仍然安装 device-mapper-libs 并以相同的方式声明 docker 类。下一个属性有所不同;这里,我们调用文件资源类型。原因是,这是用于将 docker-compose 文件放置到本地文件系统上的配置。你可以看到,我们正在使用一个位于模块 templates 目录中的模板文件来声明文件内容。稍后我们会回到这一部分。

现在,让我们看一下 install.pp 中的最后一个资源类型。我们调用了 docker_compose 类型,因为这是会运行 docker-compose 命令来启动容器的资源类型。我们来看看我们已经配置的属性。第一个是 navmar;这将设置容器的 Docker 名称标签。我们从 params.pp 中调用这个值,它将被设置为 consulensure 是一个 Puppet 元参数容器,它确保容器始终存在。

如果我们想删除容器,需要将该值设置为 absent。下一个属性是 source;它设置了 docker-compose 命令查找 docker-compose 文件的文件夹位置。我们已将其设置为 root。你可以将此值更改为系统中的任何文件夹。最后一个属性是 scale。它告诉 docker-compose 我们需要多少个容器。这个实例中,我们将值设置为 1。如果我们要部署一个 nginx 网页服务器集群,可能会将此值设置为 5。现在,让我们回到模板文件。我们需要做的第一件事是在 consul 模块的根目录下创建一个名为 templates 的文件夹:

使用 Docker Compose 编写我们的模块

接下来的步骤是创建我们的 .erb 模板文件。在 install.pp 中,我们声明了文件名为 docker-compose.yml.erb,所以在我们的 templates 目录中,创建一个同名的文件。文件的内容应如下图所示:

使用 Docker Compose 编写我们的模块

因此,你应该注意到上图中的第一个要点是设置的变量,例如 <%= @container_hostname %>。它映射回 init.pp 中的 $container_hostname。正如你所看到的,imagehostnameportscommand 等属性看起来非常熟悉。这是因为它们与我们在前一部分中声明的属性相同。在这个例子中,我们只为单个容器配置了 docker-compose 文件;在接下来的主题中,我们将探讨更复杂的配置。在我们深入探讨之前,先确保该模块能正常运行。

使用 Puppet 启动 Docker Compose

要运行我们的模块,确保模块位于 Vagrant 仓库根目录下的modules/consul目录中。我们已经配置了端口转发8500forwarded_ports: - { guest: 8500, host: 8500 }),并通过- { shell: cp /home/vagrant/node-01/modules/* -R /tmp/modules }将我们的模块复制到模块路径目录。

一旦设置完成,在我们 Vagrant 仓库的根目录下运行vagrant up命令。如果你已经有一个正在运行的 box,请执行vagrant destroy -f && vagrant up命令。终端应该会显示以下输出:

使用 Puppet 启动 Docker Compose

同样,我们可以访问127.0.0.1:8500来查看 Consul 的 GUI 界面:

使用 Puppet 启动 Docker Compose

现在,让我们登录到我们的 Vagrant box;我们可以通过在终端中从 Vagrant 仓库的根目录执行vagrant ssh命令来实现。一旦登录,我们可以使用su切换到 root 用户(sudo -i)。然后,我们可以执行docker ps命令查看所有正在运行的容器。终端应该会显示以下输出:

使用 Puppet 启动 Docker Compose

如你所见,容器已经启动并在运行。

总结

在本章中,我们使用 Puppet 部署了第一个容器。在这个过程中,我们实际上已经涵盖了许多内容。现在,我们已经拥有了创建 Puppet 模块的脚本,并且知道如何通过metadata.json.fixtures.yml来映射 Puppet 模块的依赖关系。

在我们的工具箱中,现在有两种使用 Puppet 部署容器的方法,这将在接下来的章节中非常有用。

第四章:构建多容器应用程序

在之前的几个章节中,我们用 Puppet 和 Docker 构建了一些很酷的东西,过程都很简单。现在,我们进入了更复杂的话题,例如如何保持状态以及为什么要在容器中保持状态。Docker 与微服务架构紧密结合,容器通常只是接收数据并将其转换为输出,供下游应用或数据源使用。因此,容器本身并不保持任何状态。Docker 并不局限于这种应用场景。在本章中,我们将通过构建一个完全功能的 Bitbucket 服务器并使用独立的 Postgres 后端来证明这一点。我们将把有状态的数据挂载到底层主机,并将应用程序与该状态解耦。

构建 Bitbucket 的另一个好处是,你可以将其用作你家里的 Git 服务器,以确保所有未来的模块都在源代码控制中。Atlassian 提供了一个 $10 美元的许可,供初创公司或开发人员使用,最多支持 10 个用户,这是个超值的优惠。

在本章中,我们将使用标准的清单资源和 Docker Compose 来构建应用程序。在开始编写代码之前,我们先聊一下如何将应用程序的状态与应用程序本身解耦。以下是我们将在本章中讨论的主题:

  • 解耦状态

  • Docker_bitbucket(清单资源)

  • Docker_bitbucket(Docker Compose)

解耦状态

本主题我们将讨论容器中的状态。在这个话题中,我们将探讨一些理论内容,但不用担心,我们将在编写模块时将这些理论付诸实践。我们需要理解有状态与无状态的理论背景,这样在未来编写自己的 Puppet 模块以部署 Docker 时,你可以做出关于状态的正确设计选择。

有状态与无状态

在前一部分中,我们简要讨论了有状态与无状态的话题,现在让我们深入探讨这一主题。我们来看两个示例应用程序:一个有状态,一个无状态。首先,我们从一个 redis 容器开始。它的任务是作为 activemq 的后台存储。使用队列的应用程序包含逻辑,用于检查消息是否已被接收并期待回应。如果失败,它将重试发送该消息。因此,队列本身是无状态的。我们将 redis 用作缓存。redis 容器不需要保持状态,因为它保存的信息是暂时的。如果容器发生故障,我们只需重启一个新容器,它将再次缓存队列。

现在,看看一个需要状态的容器,以及我们保持状态的选项。在这个应用程序中,我们将构建两个容器。一个是 Bitbucket 本身,另一个是用作后台数据库的 Postgres。因此,这两个容器都需要状态。首先,我们来看 Bitbucket。如果我们不给 Bitbucket 服务器状态,每次重启容器时,所有通过 Git 检入的项目都会丢失。显然,这不是一个可行的解决方案。现在,让我们看一下我们可以用来给容器赋予状态的选项。首先,我们可以在 Dockerfile 中添加卷:

有状态与无状态

这将赋予容器状态;我们可以重启应用程序,所有数据都会保留,这很好。这正是我们想要的。但这种方法也有一个缺点。卷位于容器内部,未与应用程序解耦。因此,当我们遇到问题时,主要的操作问题是需要更新容器中应用程序的版本。由于所有数据都存储在容器内,我们不能直接拉取最新版本。因此,我们只能使用现有的选项。现在,让我们来看一下如何将本地文件夹映射到容器中。我们通过使用 /data:/var/atlassian/application-data/bitbucket 来实现。冒号左侧是运行 Docker 守护进程的本地主机,右侧是容器。

Docker 使用完全限定路径,因此它将创建 /data 目录。现在,我们已经将应用程序与数据解耦。如果我们想更新 Bitbucket 的版本,我们只需要在 Puppet 代码中将 image 标签更改为新版本,然后运行 Puppet。完成后,新的 Bitbucket 版本将与现有数据一起启动。使用这种方法也有一定的代价。我们现在已经将容器与一个主机绑定。如果你使用的是 Kubernetes 或 Docker Swarm 等调度器,这可能不是最佳选择。这个问题已经被 Docker 引擎 1.8 及以上版本中新增的卷驱动程序解决了。该驱动程序允许我们创建存储对象,这些对象位于引擎运行的主机之外。

注意

这超出了本书的范围,但如果你想深入了解这项技术,我推荐你访问 clusterhq.com/flocker/introduction/。现在,我们对有状态与无状态容器有了清晰的理解。让我们开始有趣的部分,开始编码吧!

Docker_bitbucket(清单资源)

在本节中,我们将编写一个模块,用于安装 Atlassian 的 Bitbucket。该应用将由两个容器组成,其中一个将是我们前面提到的 Postgres 容器。我们实际上将调整该容器以增强后端安全性,使其仅对 Bitbucket 可访问。然后,我们将运行 Bitbucket 容器,配置它的运行用户,接着将主机系统的文件系统映射到容器中。

创建我们的模块骨架

这将是一个快速的回顾,因为我们在上一章已经讲解过了。如果你仍然对这个步骤感到不太舒服,我建议你再回顾一下上一章,直到你对整个过程有一个很好的理解。

所以,我们将打开终端并切换目录,或者使用cd命令进入 Vagrant 仓库的根目录。然后,我们将输入vagrant up,当虚拟机启动后,我们使用vagrant ssh通过 SSH 连接到它。接下来的步骤是切换到 root 用户(sudo -i)。现在我们是 root 用户了,让我们切换到/vagrant目录,该目录会映射回我们的本地机器。接着,我们将执行puppet module generate <AUTHOR>-docker_bitbucket命令。再说一次,还有一些我们需要调整的内容,但它们在上一章中已经讲过了,因此这里就不重复了。当你完成了剩余的任务,你就可以继续进入下一章。

让我们开始编码

现在,我们已经有了模块骨架,并且将它移到了我们 Vagrant 环境根目录下的modules文件夹中。我们需要添加两个新文件:install.ppparams.pp。我们的模块应如以下截图所示:

开始编码

在这个例子中,我们有一些新的内容,因此我在这个示例中没有使用params.pp。这给了你一个很好的机会,去应用上一章学到的知识。所以,现在我们将保持params.pp为空。既然我们没有在init.pp中放置参数,那我们先来看一下它:

开始编码

如前面的截图所示,我们只调用了docker_bitbucket::install类。现在我们可以继续处理较大的install.pp类。同样,我们将把它分为三部分,这样更容易解释该类的逻辑。我们来看第一部分,内容如下:

开始编码

在该类的顶部部分,我们正在安装device-mapper-libs软件包。这是 RHEL 系列和 Docker 的先决条件。接下来我们声明的是 Docker 类。在这个资源中,我们定义了要安装的 Docker 版本、Docker 将使用的 TCP 绑定地址,以及最后 Docker 将绑定的 Unix 套接字。这与我们在上一章中定义 Docker 守护进程时使用的配置相同。在进入 Docker 调度器之前,这部分内容会保持相对静态。现在我们继续讨论 Postgres:

开始编码

首先,我们将定义我们希望使用的 Postgres 镜像。对于本示例,我们使用的是 Postgres 9.2。因此,从 Docker Hub 中正确的标签是postgres:9.2。现在,让我们来看一下docker::run类;这是定义 Postgres 所有配置的地方。所以,你可以看到我们调用的是在前面的资源中设置的镜像postgres:9.2。接着,我们将主机名设置为bitbucket-db。这个设置非常重要,所以我们将其记住以备后用。

让我们看看env资源声明,因为那里有一些内容。在这一行中,我们声明了 Postgres 的用户、数据库密码、我们将与 Bitbucket 连接的数据库名称,以及最后 Postgres 存储数据库的路径。最后,我们声明了我们的卷为/root/db:/var/lib/postgresql/data/pgdata

如前所述,冒号左边是映射到本地机器,右边是映射到容器。有两个主要的要点需要注意。首先,/root/db文件夹是任意的,在生产环境中不会使用。第二,你会注意到冒号左边的/var/lib/postgresql/data/pgdata文件夹和env中的值PGDATA是相同的。这并非巧合;这个文件夹保存了我们关心的唯一状态:实际的数据库。这是我们将保留的唯一状态,别无他物。你会注意到,我们没有暴露这个容器的任何端口。这样做是有意为之。我们将把我们的 Bitbucket 容器与 Postgres 容器链接起来。链接是什么意思?这意味着默认情况下,Postgres 镜像会暴露端口 5432。我们将使用这个端口来连接我们的数据库。通过链接容器,只有 Bitbucket 容器可以访问 5432 端口;如果我们暴露了端口(5432:5432),那么任何能够访问主机实例的应用都可以访问该端口。因此,链接要更安全。我们需要记住从这一部分代码中提取几个内容以备后用:主机名和整个env行。现在,我们继续看 Bitbucket 容器:

让我们编码

如你在前面的截图中看到的,镜像资源是相同的,但是我们不是调用 Postgres,而是调用atlassian/bitbucket-server。接下来我们将声明的资源是端口资源。你会注意到我们声明了两个端口7990:7990,这是我们访问 Web UI 的端口,和7999:7999,这是 Bitbucket 用于 SSH 的端口。我们将用户名设置为root。这是 Atlassian 文档中推荐的做法(hub.docker.com/r/atlassian/bitbucket-server/)。

接下来,我们将映射我们的卷驱动器。在这种情况下,我们只会映射 Bitbucket 的数据目录。这里存储着所有我们的 Git 仓库、用户信息等。再次强调,/data是一个任意位置;你可以选择任何你喜欢的位置。需要注意的重要位置是在冒号左侧的/var/atlassian/application-data/bitbucket

最后,我们将链接我们的两个容器。链接容器的另一个好处是,Docker 守护进程会将它们的主机名和 IP 地址写入两个容器的/etc/hosts文件中。因此,容器之间可以毫无问题地互相通信。无需担心 IP 地址,因为它是任意的,并由 Docker 守护进程处理。现在,我们已经写好了模块,可以开始构建应用程序了。

运行我们的模块

我们需要做的第一件事是转发正确的端口到我们的servers.yml文件中,这样我们才能访问转发给 Bitbucket 的端口。为此,我们需要修改文件,使其看起来像下面的截图所示:

运行我们的模块

所以,打开我们的终端并将目录切换到 Vagrant 仓库的根目录,然后运行vagrant up。你应该会看到如下输出:

运行我们的模块

现在我们的应用程序已经构建完成,我们可以访问http://127.0.0.1:7990。我们应该看到以下页面:

运行我们的模块

在本节之前,我们已经记住了关于我们的 Postgres 安装的一些细节。现在是时候使用它们了,接下来我们开始吧。我们需要做的第一件事是使用外部数据库。接下来我们需要选择的配置项是数据库类型。当然,我们将选择 Postgres。

主机名将设置为bitbucket-db容器的主机名,端口是5432,数据库名称是我们在代码中设置的bitbucket。我们将使用 PostgreSQL 作为用户名,密码将是Gr33nTe@。请参见以下截图以了解更多:

运行我们的模块

接下来,点击测试按钮,我们应该会看到成功建立数据库连接的消息,如下图所示:

运行我们的模块

我会让你完成其余的设置。但我们刚刚设置的并不简单,现在我们已经有了一个非常坚实的基础,可以继续进行更复杂的应用程序开发。

Docker_bitbucket(Docker Compose)

本节中,我们将构建相同的 Bitbucket 应用程序。不同之处在于,这次我们将使用docker-compose作为.erb文件,而不是在清单中的资源声明。

让我们编码——第二次

在上一个主题中,我们讲解了很多幕后发生的事情。本主题将不再重复之前的内容,因此本章内容将专注于代码。我们会保持init.ppparams.pp与上一个主题中的一致。所以,让我们直接跳到install.pp。它看起来将与上一章的install.pp非常相似:

让我们编码 – 第 2 次

所有的“魔法”都发生在我们的模板文件中。所以,让我们跳到位于我们模块根目录templates文件夹中的.erb文件:

让我们编码 – 第 2 次

如您在前面的截图中看到的我们.erb文件,所有配置都很熟悉。与我们在上一个主题中讲解的内容完全相同,没有任何变化。

运行我们的模块 – 第 2 次

让我们打开终端,将目录切换到 Vagrant 仓库的根目录,并运行vagrant up。您应该会看到以下输出:

运行我们的模块 – 第 2 次

现在,让我们访问http://127.0.0.1:7990,应该会看到以下页面:

运行我们的模块 – 第 2 次

只需按照前面主题中的相同设置来配置 Bitbucket。您可以使用试用许可证来尝试该应用程序,或者正如我之前提到的,可以在bitbucket.org/product/pricing?tab=server-pricing找到一个开发/启动许可证,$10 的许可证费用将捐赠给慈善机构。

总结

通过构建一个多容器应用程序,我们学到了很多内容。我们首先了解了有状态和无状态容器,拥有状态的优缺点,以及我们需要做出哪些设计选择来保持状态。接着,我们看了链接容器以及它们如何通过各自的主机文件进行相互通信。本章的所有内容都为我们提供了必要的知识,以便进一步探索服务发现和容器调度等主题。

第五章:配置服务发现与 Docker 网络

在本章中,我们将讨论在使用容器时的两个非常重要的话题。首先,我们将探讨什么是服务发现,为什么我们需要它,以及不同类型的服务发现。第二个话题是 Docker 网络。运行容器网络有很多种方式。这里有一些很棒的技术,比如 CoreOS 项目的 flannel(coreos.com/flannel/docs/latest/)。还有 Weave Works 的 Weave(weave.works/),但是我们将使用 Docker 引擎 1.9.1 版本中发布的原生 Docker 网络栈。

服务发现

这是容器世界中一个相当重要的话题,特别是当我们开始进入多节点应用程序和 Docker 调度器时。问题是,什么是服务发现?它仅限于容器吗?在我们的 Puppet 模块中,哪些服务发现类型可以帮助我们做出明智的设计选择?

理论

当我们开始处理多节点应用程序时,服务发现至关重要,因为它使得我们的应用程序可以在节点之间移动时相互通信。因此,正如你所看到的,在容器的世界里,这个问题非常重要。当选择一个服务发现后端时,我们有几个选择。这个领域中的两个大名鼎鼎的名字是etcdcoreos.com/etcd/),它同样来自 CoreOS,以及来自 HashiCorp 的Consulwww.consul.io/)。

你可能还记得我们已经编写了一个 consul 模块。因此,在本章中,我们将选择相同的模块,因为我们已经有了现成的代码。首先,让我们来看一下 Consul 的架构,以便我们理解后端是如何工作的,它如何处理故障,以及通过配置 Consul,我们可以获得哪些选项。

那么,让我们来谈谈 Consul 是如何工作的。在 Consul 中,我们可以为服务器提供两种类型的配置。第一种是服务器角色,第二种是代理角色。虽然这两者有交互作用,但它们服务于不同的目的。让我们先深入了解服务器角色。服务器的角色是参与 RAFT 法定人数;这就是维护集群状态的任务。在 Consul 中,我们有数据中心的概念。你可能会问,什么是数据中心?它是由一组逻辑服务器和代理组成的。例如,如果你在 AWS 上,数据中心可以是一个可用区(AZ)或甚至一个虚拟私有云(VPC)。Consul 允许数据中心之间进行连接;服务器的角色是负责数据中心之间的通信。Consul 使用传播协议(gossip protocol)来实现这一点。服务器还持有键/值存储,并使用 Serf 协议在服务器之间复制它。让我们来看一下我们讨论过的图示:

理论

代理的角色是向服务器报告机器的状态以及可能分配给它的健康检查。同样,Consul 将使用 serf 协议来传递通信。

现在我们已经理解了 Consul 在后台所做的工作,让我们来看一下它有哪些功能,可以在我们的 Puppet 模块中利用。我们将首先利用的功能是 DNS 服务发现。在容器环境中,这是非常重要的。随着我们的容器在节点间迁移,我们需要知道如何连接到它们。DNS 服务发现非常巧妙地解决了这个问题。那么,让我们来看一个示例来理解这一点。

在这个示例中,我们有一个 mario 服务,并且有一个包含三个节点的 Docker swarm 集群。当我们访问 Docker API 并由 swarm 调度容器时,我们并不知道 mario 会在哪个节点上启动。但是,我们有其他服务需要在 mario 启动后尽快找到它。如果我们告诉其他服务,mario 实际上位于 mario.service.consul,那么无论容器在哪个节点上启动,它都会将 mario.service.consul 解析到正确的地址。请参见以下图示,了解更多细节:

理论

在这种情况下,如果我们 ping mario.service.consul,我们会得到 192.168.100.11。根据我们在 swarm 中的调度配置,如果 Server b 失败,mario.service.consul 可能会转移到 Server d。因此,mario.service.consul 的响应现在将来自 192.168.100.13。这一切无需人工干预,对应用程序来说是无缝的。这就是我们在本章中看到的有关服务发现的所有理论;后续章节中我们会涵盖更多内容。现在,让我们开始编写一些代码。

服务发现模块

在本模块中,我们将编写一个使用 consul 作为 DNS 服务发现后端的模块。由于我们已经有了一个 consul 模块,因此我们不会从头开始,而是向现有模块中添加新功能。我们将再次使用清单和 Docker Compose 来编写模块。那么,让我们从清单开始。

我们的文件夹结构应该如下所示:

服务发现模块

让我们直接跳到 install.pp。在不做任何更改的情况下,它应该看起来像下面的截图:

服务发现模块

现在,我们要添加一个额外的容器,它将作为我们 DNS 服务发现解决方案的一部分。我们需要一个东西来在容器启动时将其注册到 Consul。为此,我们将使用一个名为 registrator 的 Golang 应用程序(github.com/gliderlabs/registrator)。这是一个非常棒的应用,我已经使用它超过一年,效果非常稳定。所以,让我们修改 params.pp 文件,允许新容器的加入。目前,params.pp 文件的内容如下所示:

服务发现模块

我们要做的第一件事是修改 docker_imagecontainer_hostname 参数。由于我们已经使用了 consul_xxx 的命名惯例,所以可以继续使用:

服务发现模块

现在,我们来添加 registrator 的参数:

服务发现模块

如你所见,我们已经为镜像添加了 $reg_docker_image = 'gliderlabs/registrator' 参数,并为主机名添加了 $reg_container_hostname = 'registrator' 参数。我们告诉容器监听主机的 $reg_net = 'host' 网络。接下来的参数需要一些解释。registrator 将 Docker 守护进程绑定的 Unix 套接字映射到它自己的 Unix 套接字。这是为了监听任何新启动的服务,并将其注册到 Consul 以便进行发现。如你所见,我们使用 $reg_volume = ['/var/run/docker.sock:/tmp/docker.sock'] 来实现这一点。最后一个参数告诉 registrator 如何找到 consul。我们将通过 $reg_command = "consul://$::ipaddress_enp0s8:8500" 来设置这个参数。现在,让我们转到 init.pp 文件。

我们的 init.pp 文件应该如下所示:

服务发现模块

让我们添加新的参数,如下图所示:

服务发现模块

现在我们已经设置好了所有参数,可以进入 install.pp 文件,添加代码来安装 registrator:

服务发现模块

如前面截图所示,我们在文件底部添加了一段新的代码块。这与我们配置 Consul 的代码类似;不过,有一些不同的参数。我们之前已经讲解过这些内容,所以不再重复。既然我们对模块做了一些修改,我们应该在 Vagrant 中运行它,检查是否存在任何问题。在我们运行 Vagrant 之前,我们需要更改 Vagrant 仓库根目录下的 servers.yaml 文件,以便让我们可以访问 Consul 的 8500 端口。我们可以通过以下代码更改来实现这一点:

服务发现模块

现在,让我们打开终端并将目录切换到 Vagrant 仓库的根目录。从那里,我们只需要执行 vagrant up 命令。终端的输出应如下面的截图所示:

服务发现模块

之后,让我们打开浏览器,访问 127.0.0.1:8500

服务发现模块

你会注意到,现在在 Consul 网页界面中列出了更多的服务,而不是我们在上一章运行模块时看到的那些。这是因为现在,registrator 正在监听 Unix 套接字,并且任何与主机端口映射的容器都会被注册。所以,好消息是我们的模块正在工作。接下来,让我们往模块中添加一个应用程序。

最简单的方法是向我们的节点添加另一个容器模块。接下来,让我们添加我们的 bitbucket 模块。我们通过向 manifests 目录中的 default.pp 文件添加类来实现:

服务发现模块

我们还需要对 bitbucket 模块进行一些快速修改,以避免重复声明错误。请注意,这不是你在生产环境中会做的事。但对我们测试实验室来说,这已经足够了。我们需要注释掉如下面截图所示的顶部代码块:

服务发现模块

我们甚至可以注释掉如下面截图所示的代码:

服务发现模块

这取决于你是否使用了 manifest 模块或 compose 模块。我使用的是 compose 模块。

接下来,让我们回到终端并在 Vagrant 仓库的根目录下执行 vagrant provision 命令。终端的输出应如下面的截图所示:

服务发现模块

现在,让我们再看看浏览器。我们可以看到我们的 bitbucket 服务已经注册,如下图所示:

服务发现模块

我们已经使服务发现功能正常工作;但是,我们仍然需要在我们的模块中添加另一个类,用于 DNS 服务发现。接下来,让我们回到我们的 consul 模块。我们将添加一个名为 package.pp 的新文件。在这个文件中,我们将安装 bind 包并添加两个模板,一个用于配置 named.conf,另一个用于配置 /etc/ 目录下的 consul.conf。让我们开始编码。首先,我们需要做的是在模块的 manifests 目录中创建我们的 package.pp 文件:

服务发现模块

我们将向文件中添加以下代码:

服务发现模块

现在,让我们创建一个 templates 文件夹。在这个示例中,我们没有对文件进行参数化,而在生产环境中你会这么做。这就是我们使用 templates 文件夹而不是文件的原因:

服务发现模块

现在,让我们创建一个名为named.conf.erb的文件,并添加以下代码:

服务发现模块

这段代码仅仅是将我们的 DNS 解析器设置为监听127.0.0.1。记住,我们已经在 Consul 容器上设置了端口转发,将端口53转发。这就是主机如何连接到容器的方式。最后,它会调用我们下一个模板文件/etc/named/consul.conf。让我们现在创建它:

服务发现模块

我们将添加的代码如下:

服务发现模块

你会注意到我们正在转发端口8600,这是 Consul 用于 DNS 流量的端口,并且移除了端口53。由于 TCP 绑定将使用端口53,我们将请求转发到8600,如下面的代码所示:

服务发现模块

在运行 Puppet 之前,我们需要再做一次更改。我们需要将package.pp的新代码添加到init.pp文件中。我们可以这样做:

服务发现模块

现在,我们可以运行我们的模块了。让我们打开终端并切换到我们的 Vagrant 仓库的根目录。我们将输入vagrant up命令,如果你已经有一个盒子在运行,只需输入vagrant destroy -f && vagrant up命令。现在,让我们检查 Web UI(127.0.0.1:8500):

服务发现模块

正如你在上面的截图中看到的,我们在端口8600上注册了一个新服务(consul-8600)。现在,我们需要确保我们的机器在它们的接口上监听正确的 DNS 服务器。我们将通过servers.yaml来完成这一操作,正如我通常会将此配置添加到 AWS 的用户数据中。你也可以使用 Puppet 来控制此配置。因此,未来你可以根据自己的环境决定配置的位置。我们要添加的行是- { shell: 'echo -e "PEERDNS=no\nDNS1=127.0.0.1\nDNS2=8.8.8.8">>/etc/sysconfig/network-scripts/ifcfg-enp0s3 && systemctl restart network'}。我们将按照以下截图所示的方式添加:

服务发现模块

现在,让我们打开终端并输入vagrant up命令。如果你已经有一个盒子在运行,则输入vagrant destroy -f && vagrant up命令。终端输出应该如下所示:

服务发现模块

然后,我们可以使用vagrant ssh登录到我们的 vagrant 盒子,测试我们的 DNS 设置是否有效。我们可以通过选择一个服务并尝试 ping 它来进行测试。我们将选择ping bitbucket-server-7990服务,输入ping bitbucket-server-7990.service.consul命令,应该得到如下结果:

服务发现模块

如上图所示,它返回的是回环的 echo 响应,因为该服务在本地主机上运行。如果我们在主机外部,它将返回运行服务的主机 IP 地址。现在,我们运行容器调度器,如 Docker Swarm,这些调度器有多个主机。我们现在了解了服务发现的工作原理。

现在,让我们看一下使用 Docker Compose 的情况。

为了避免重复,让我们将init.pp文件做成与使用manifests方法的模块一样。我们需要对params.pp文件进行一个小改动;docker-compose期望你传递字符串。所以,我们需要删除 $reg_volume 周围的括号,如下图所示:

服务发现模块

然后,我们将像之前一样添加package.pp文件,并为我们的bind配置创建两个模板。接下来,我们需要更新 templates 目录中的 docker-compose.yml.erb 文件。我们需要添加第二个容器registrator。我们将使用本章之前模块中的相同参数。此代码应如下图所示:

服务发现模块

你还会注意到,我们更改了 Consul 容器的端口,就像本章前面所做的一样(我们删除了端口53并添加了8600 tcp/udp)。现在,我们可以进入终端,切换到 Vagrant 仓库的根目录,并执行vagrant up命令。我们的终端应显示如下图所示:

服务发现模块

同样,我们也可以在浏览器中查看 127.0.0.1:8500

服务发现模块

如上图所示,它看起来和本章前面部分一样。

让我们登录到我们的机器并测试我们的 DNS 服务发现。为此,输入vagrant ssh命令,然后 ping 一个服务。这一次,我们选择不同的服务。我们将使用ping consul-8500.service.consul命令。运行后,我们应该会得到以下响应:

服务发现模块

本章关于服务发现的内容就到这里。我们将在容器调度器章节中再次讨论它。

Docker 网络

在本节中,我们将介绍 Docker 引擎自带的本地网络堆栈。通过阅读这一主题,你将能获得大量有价值的知识。我强烈建议你这么做,因为在 Docker 网络方面有很多值得探索的内容。如果你以前没有使用过 Docker 网络,建议你从docs.docker.com/engine/userguide/networking/dockernetworks/开始阅读。在这里,你可以了解不同类型的驱动程序,如何使用 VXLAN 来隔离网络,以及设计 Docker 网络时的最佳实践。我们现在将介绍基础内容,更多高级功能将在后续章节中讨论。

前提条件

在我们开始为网络编写代码之前,有几件事情是必须的。首先,我们需要一个键值存储。Docker 将使用这个存储来映射所有创建的容器、IP 地址和 vxlans。由于通常会有多个主机连接到一个网络,键值存储通常是分布式的,以增强其容错能力。幸运的是,我们已经建立了一个可以利用的键值存储,它当然就是 Consul。你还需要的另一个配置是在启动 Docker 引擎时传递的额外参数。这是为了让 Docker 引擎知道如何访问键值存储。这些是我们开始编码所需要的基本前提条件。

代码

让我们创建第一个 Docker 网络。为此,我们将添加到我们的consul模块。我不会为manifestsdocker-compose两者重复操作,因为配置可以在两者之间移植。我将使用docker-compose模块作为示例。如果这是你第一次创建 Docker 网络,尝试将配置移植到两者之间将是一次有价值的练习。那么,让我们开始吧。我们只会对我们的install.pp文件进行更改。我们要做的第一项更改是为docker-engine守护进程添加额外的参数。我们通过添加以下截图所示的代码来实现:

代码

这段代码设置了我们的键值存储的地址和端口。然后,它还告诉其他机器我们在什么接口和端口上发布我们的网络。

我们接下来要添加的代码将创建网络。我们将创建一个名为network.pp的新文件。然后,我们将向其中添加以下截图所示的代码:

代码

接下来,我们需要确保我们的类按照正确的顺序安装,因为 Docker 网络依赖于 Consul 的存在。如果没有 Consul,我们的目录将无法正常工作。因此,我们需要使用 Puppet 内置的contain功能。我们通过添加以下截图所示的代码来实现:

代码

正如你看到的,我们只是在设置一个基本的网络。我们可以设置 IP 地址范围、网关等。如果我们这么做,结果将是这样的:

The code

现在我们有了代码,让我们回到终端,在 Vagrant 仓库根目录下执行 vagrant up 命令。我们的终端输出应该像下面的截图一样:

The code

现在,我们可以通过登录到我们的 Vagrant 虚拟机(在 Vagrant 仓库根目录下执行 vagrant ssh)来检查我们的网络是否存在。一旦登录到虚拟机,我们需要切换到 root 用户(执行 sudo -i),然后发出 docker network ls 命令。这将列出虚拟机上可用的网络。我们要找的是使用 overlay 驱动的 docker-internal 网络:

The code

正如你从终端输出中看到的那样,我们成功了,网络已经配置好。这就是本章中关于网络配置的所有内容。在下一章中,我们将连接容器,并将我们的 Docker 网络扩展到多个主机。

概述

在这一章中,你学习了容器生态系统如何处理服务发现。我要强调的是,当你开始大规模使用容器时,理解这个话题将变得极为重要。在继续后续章节之前,我强烈建议你对服务发现有一个扎实的理解。我们还讨论了 Docker 网络的基础知识。别担心,在下一章中,我们将深入探讨 Docker 网络,因为我们将构建多主机应用。

第六章:多节点应用

在这一章,我们将开始使用一些非常酷的东西。我们将利用到目前为止在书中学到的所有技能,并在此基础上进一步提升。在这一章,我们将部署四台服务器。我们将研究如何将 Consul 集群化,这将为我们提供一个完美的机会来进一步扩展我们的模块功能。在这一章中,我们将研究两种容器网络配置方式。首先是使用标准的主机 IP 网络,让我们的 Consul 集群在该网络上进行通信。我们还将安装 ELKElasticsearchLogstashKibana)堆栈 (www.elastic.co/)。为此,我们将为每个产品编写一个模块。因为 Elasticsearch 是我们解决方案中的数据存储,我们希望将其隐藏,只允许 Logstash 和 Kibana 访问该应用程序。我们将通过使用本地 Docker 网络堆栈,并通过 VXLAN 隔离 Elasticsearch 来实现这一点。正如你所看到的,我们将在这一章中完成很多内容。我们将在这一章中涵盖以下主题:

  • 我们解决方案的设计

  • 将一切整合起来

我们解决方案的设计

由于解决方案中涉及很多动态部分,最好可视化我们将要编写的代码。由于这是从上一章迈出的重要一步,我们将分解解决方案。在第一个主题中,我们将查看 Consul 集群的设计。

Consul 集群

在本设计中,我们将使用四台服务器:node-01node-02node-03node-04。我们将使用 node-01 来启动我们的 Consul 集群。然后将其他三台节点作为服务器添加到集群中。它们将能够加入集群、进行投票,并复制键值存储。我们将设置一个位于 172.17.8.0/24 网络的 IP 网络,并将我们的容器端口映射到位于 172.17.8.0/24 网络上的主机端口。下图将展示网络流向:

Consul 集群

ELK 堆栈

现在我们已经有了 Consul 集群,我们可以看看我们的 ELK 栈将是什么样子的。首先,我们将详细了解网络的设计。该栈将连接到本地 Docker 网络。请注意,我没有列出我们的 Docker 网络的 IP 地址。这样做的原因是我们将让 Docker 守护进程选择网络地址范围。对于这个解决方案,我们不打算将任何流量路由到这个网络之外,所以让守护进程选择 IP 范围是可以的。你还会注意到 Elasticsearch 只连接到我们的 Docker 网络。这是因为我们只希望 Logstash 和 Kibana 连接。这样做是为了防止其他应用程序能够向 Elasticsearch 发送请求或查询。你会注意到,Logstash 和 Kibana 都连接到了 Docker 网络和主机网络。这样做的原因是我们希望应用程序将日志发送到 Logstash,并且我们希望能够访问 Kibana 的 Web 应用程序。

ELK 栈

要全面了解架构,我们只需要将两个图表叠加在一起。所以,让我们开始编写代码吧!

将所有内容整合起来

现在我们已经看过了设计,接下来我们将把所有内容整合起来。我们将查看 Vagrant 仓库的管道更改。然后,我们会将额外的功能编码到consul模块中。接着,我们将运行 Vagrant 仓库,以确保 Consul 集群正常运行。在完成这项任务后,我们将构建 ELK 栈,并为每个产品构建一个模块。我们还将设置 Logstash 将日志转发到node-03,以便我们可以测试确保我们的 ELK 栈是正确的。让我们来深入了解一下这个。

服务器设置

接下来,我们将查看我们要对新的 Vagrant 仓库所做的更改。我们要查看的第一个文件是servers.yaml文件。我们需要做的第一件事是更改我们的基础盒子。由于我们将容器连接到本地 Docker 网络,我们的主机必须运行版本高于 3.19 的内核。我已经创建了一个预构建的 vagrant 盒子,正是这一配置。它是我们在所有其他章节中使用的 Puppetlabs 盒子,内核已更新至 4.4 版本:

服务器设置

正如你在前面的截图中看到的,我们对servers.yaml文件所做的另一个更改是我们已向/etc/hosts目录添加了条目。我们这样做是为了模拟传统的 DNS 基础设施。如果这是在你的生产环境中,我们就不需要添加这个配置。

现在,我们需要添加另外三个服务器。以下截图将准确显示它应该是什么样子:

服务器设置

所以,一旦所有服务器构建完成,我们将访问的端口是8500(在node-01上,即 Consul 的 Web UI 127.0.0.1:8500)和8081(Kibana 的 Web UI 127.0.0.1:8081)。

Consul 集群

我们现在已经非常熟悉consul模块,但我们将把它提升到一个新的水平。在这一章中,我们将只使用 compose 版本。原因是当你开始处理更复杂的应用程序或需要使用if语句来添加逻辑时,.erb文件为我们提供了这样的自由。这一模块有相当多的更改。那么,让我们从params.pp文件重新开始:

Consul 集群

如您所见,我们添加了两个新参数。第一个是$consul_master_ip,另一个是$consul_is_master。我们将使用它来定义哪个服务器将引导我们的 Consul 集群,哪个服务器将加入集群。我们已将node-01的主机名硬编码。如果这是一个生产模块,我不会硬编码主机名,它应该是一个参数,可以在 Hiera 中查找(docs.puppetlabs.com/hiera/3.0/)。当我们查看docker-compose.yml.erb文件时,我们会再次提到这一点。其他参数应该对您来说很熟悉。

接下来,让我们看一下我们的init.pp文件:

Consul 集群

正如您在这里看到的,我们没有对这个文件做太多更改,因为我们仅添加了一个布尔值($consul_is_master)。然而,我们仍然需要验证输入。我们通过调用标准库函数validate_bool来完成这项工作。

让我们快速浏览一下install.pp文件:

Consul 集群

现在,让我们来看一下network.pp文件:

Consul 集群

最后,我们将查看package.pp文件:

Consul 集群

如您所见,我们并没有对这些文件进行更改。现在,我们可以查看将真正部署容器逻辑的文件。接下来,我们将转到templates文件夹,查看我们的docker-compose.yml.erb文件。这是模块中大多数更改的地方。

那么,让我们来看一下文件的内容,如下图所示:

Consul 集群

正如您所见,这个文件中的代码已被复制。我们将其分为三部分,如下图所示:

Consul 集群

在第一块代码中,您会注意到的第一个变化是if语句。这是一个用于确定节点是否为 Consul 引导主节点或集群中的服务器的选择。如果您还记得我们的params.pp文件,我们将node-01设置为我们的主节点。当我们将这个类应用到节点时,如果是node-01,它将引导集群。我们接下来要关注的行如下:

command: -server --client 0.0.0.0 --advertise <%= @consul_advertise %>  -bootstrap-expect <%= @consul_bootstrap_expect %>

我们应该注意对比下一块代码中的相同一行:

Consul 集群

首先,我们可以看到这是elsif,是if语句的后半部分。因此,这将是安装 Consul 到其他三个节点的代码块。它们仍然是集群中的服务器,只是没有启动集群的任务。我们可以通过以下一行代码来判断这一点:

command: -server -bind 0.0.0.0 --client 0.0.0.0  --advertise <%= @consul_advertise %> -join <%= @consul_master_ip %>

记得我们之前看过代码的第一行吗?你看到了区别吗?在第一块代码中,我们声明了-bootstrap-expect <%= @consul_bootstrap_expect %>,而在第二块代码中,我们声明了-join <%= @consul_master_ip %>。通过查看代码,我们可以判断启动顺序。最后,我们可以看到,我们声明了<% end -%>来结束if语句。

现在,让我们来看最后一段代码:

Consul 集群

如你所见,它将部署registrator容器。由于这个容器位于if语句之外,它将部署到所有应用了consul类的节点上。到目前为止,我们已经取得了很大进展。在继续创建新的弹性模块之前,我们应该先检查一下模块的更改。我们最后需要更改的是default.pp清单文件,内容如下:

Consul 集群

如你所见,每个节点都有一个节点声明,并且都应用了consul类。现在,让我们打开终端,切换到 Vagrant 仓库的根目录,并执行vagrant up命令。这一次,它将从 Hashicloud 下载一个新的基础框。根据你的网络连接情况,可能需要一些时间。请记住,我们需要这个新框的原因是,它具有更新的内核,可以利用本地的 Docker 网络。在上一章中,我们能够创建一个网络,但无法将容器连接到它。在本章中,我们将能做到这一点。同时,我们将构建四台服务器,所以运行 Vagrant 应该需要大约 5 分钟。一旦我们的第一台机器启动,我们就可以登录到 Consul Web UI。在那里,我们可以看到每个节点加入的进度。

如下图所示,我们的集群已经启动:

Consul 集群

我们还可以通过查看SERVICES标签,检查所有服务是否已启动并稳定,如下图所示:

Consul 集群

如下图所示,我们的第二个节点已成功加入:

Consul 集群

以下截图显示了当我们进入SERVICES标签时屏幕的样子:

Consul 集群

如你所见,我们的服务数量已经翻倍。因此,情况看起来不错。

现在,vagrant up命令已完成,我们的终端输出应该如下图所示:

Consul 集群

让我们重新登录到我们的浏览器,访问 Consul UI(127.0.0.1:8500)。在NODES标签下,我们现在应该能看到所有四个节点:

Consul 集群

我们可以看到我们的集群状态良好,因为所有四个节点的服务数量相同,都是10个,并且所有服务都是绿色的。我们需要检查的最后一项是我们的 DNS 服务发现。现在让我们登录到其中一台机器。我们选择node-03。在终端中,我们输入命令vagrant ssh node-03。我们需要指定节点,因为我们有多个 vagrant 主机。接下来,我们将 ping Consul 服务 8500。我们只需输入命令ping consul-8500.service.consul。终端输出应该像下面的截图一样:

Consul 集群

现在它工作得非常完美。那么,让我们再检查一件事。我们需要确保我们的 Docker 网络已配置好。为此,我们需要切换到 root 目录(sudo -i),然后输入docker network ls命令,如下所示:

Consul 集群

现在一切都已启动并运行,让我们继续进行 ELK 堆栈的配置。

ELK 堆栈

在规划这本书时,我的一个重点是使用能够应用于现实世界的例子,以便读者能够获得一些实际价值。ELK 堆栈也不例外。ELK 堆栈是一套非常强大的应用程序,可以帮助你汇总所有的应用程序日志,以查看应用程序的健康状况。想了解更多 ELK 堆栈的信息,请访问www.elastic.co/。该网站有关于所有产品的优秀文档。现在,让我们开始我们的第一个新模块。

根据我们的设计,安装 ELK 堆栈是有顺序的。由于 Logstash 和 Kibana 都依赖于 Elasticsearch,我们将首先构建 Elasticsearch。我们将在模块中使用的所有镜像都是由 Elasticsearch 构建、维护并发布的,因此我们可以确保质量是可靠的。我们首先需要做的是创建一个名为<AUTHOR>-elasticsearch的新模块。我们在上一章中介绍了如何创建模块,如果你不确定如何操作,可以回去阅读那一章。现在我们已经有了模块,让我们把它移到 Vagrant 仓库根目录下的模块目录中。

由于这些容器已经由 Elasticsearch 构建,这些模块将会简短明了。我们只需要向init.pp文件添加代码:

ELK 堆栈

正如你所看到的,我们正在调用 docker::image 类来下载 elasticsearch。在 docker::run 类中,我们调用我们的 elasticsearch 容器,并且我们将只将容器绑定到 docker-internal Docker 网络。你会注意到我们没有绑定任何端口。这是因为,默认情况下,这个容器将暴露 9200 端口。我们只希望在 Docker 网络上暴露 9200 端口。Docker 足够智能,能够自动允许在 Docker 本地网络上暴露端口。在下一个资源中,我们只声明了 elasticsearch 的主机网络。我们指定了 0.0.0.0,因为我们不知道容器从 Docker 网络中获得的 IP 地址。由于该服务将对外界隐藏,因此这个配置是没问题的。然后,我们将映射一个持久化的驱动器来保存我们的数据。

接下来我们需要做的是将 elasticsearch 添加到一个节点中。根据我们的设计,我们将把 elasticsearch 添加到 node-02。我们在 manifests 目录下的 default.pp 文件中进行此操作,如下图所示:

ELK 堆栈

你会注意到我使用了 contain 而不是 include。这是因为我希望确保 consul 类在 elasticsearch 类之前被应用,因为我们需要在 elasticsearch 启动之前,确保 Docker 网络已存在。如果网络不存在,我们的目录将无法构建,因为容器无法启动。

我们接下来要编写的模块是 logstash。我们的 logstash 模块将稍微复杂一些,因为它既会在 Docker 网络上,也会在主机网络上。我们之所以要在这两个网络上都部署它,是因为我们希望应用程序能将日志转发到 logstash。我们还需要 logstashelasticsearch 进行通信。因此,我们还将 logstash 添加到 Docker 网络中。我们将以与 elasticsearch 相同的方式创建该模块。我们将把模块命名为 <AUTHOR>-logstash。接下来,让我们看一下 init.pp 文件中的代码,如下所示:

ELK 堆栈

在这里,首先你会注意到我们正在创建一个目录。这是为了映射到容器,并且将包含我们的 logstash.conf 文件。接下来的声明是文件类型。这是我们的 logstash.conf 文件,正如我们所看到的,它是来自代码的模板。那么,在查看完 init.pp 文件中的其余代码后,我们再回来看它。下一行代码将从 Docker Hub 拉取我们的 logstash 镜像。在 docker::run 类中,我们将调用我们的 logstash 容器,使用 logstash 镜像,并将容器附加到我们的 docker-internal Docker 网络。

下一行代码将告诉logstash开始使用我们的logstash.conf文件;然后,我们会将之前在init.pp文件中创建的目录挂载到容器中。现在,您可以在这个模块中看到我们已经将端口暴露到主机网络。在最后一行,我们告诉logstash我们的 Elasticsearch 主机和端口。Logstash 是如何知道 Elasticsearch 的位置的呢?我们并没有像前面章节中那样连接容器。这与我们将 Elasticsearch 容器命名为elasticsearch的方式是一样的,我们的 Docker 网络内置了一个 DNS 服务器,地址是127.0.0.11。任何加入该网络的容器都会以其容器名称注册自己。这就是docker-internal网络上的服务如何互相发现的方式。

我们需要查看的最后一件事是我们在init.pp文件中声明的logstash.conf文件的模板文件。所以,在我们模块的根目录中创建一个名为templates的新文件夹,然后创建一个名为logstash.conf.erb的文件。我们将添加以下配置,以接受来自 syslog 和 Docker 的日志。

最后,在底部,我们将我们的 Elasticsearch 配置放在这里,如下截图所示:

ELK 堆栈

现在,让我们按照之前在elasticsearch模块中做过的方式将logstash模块添加到node-03中。

ELK 堆栈

再次,我们将使用contain而不是include。现在是时候继续创建我们的最后一个模块了。我们将像前两个模块一样创建它。我们将这个模块命名为<AUTHOR>-kibana

在 Kibana 中,我们只会向init.pp文件中添加代码,如下截图所示:

ELK 堆栈

如您所见,我们正在下载kibana镜像。在docker::run类中,我们使用kibana镜像调用我们的kibana容器,并将该容器连接到本地 Docker 网络。在下一行,我们将容器端口5601(Kibana 的默认端口)映射到主机的80端口。这只是为了方便我们实验室的使用。在最后一行,我们告诉kibana如何连接到elasticsearch

让我们再次使用contain而不是include,将kibana添加到node-04

ELK 堆栈

我们现在准备好运行我们的 Vagrant 环境了。让我们打开终端并将目录切换到 Vagrant 仓库的根目录。我们将从零开始构建,所以让我们发出vagrant destroy -f && vagrant up命令。

这个过程大约需要 5 分钟左右,具体取决于您的互联网连接速度,请耐心等待。一旦构建完成,我们的终端应该没有错误,并且看起来像以下截图:

ELK 堆栈

我们接下来要检查的是我们的 Consul Web UI(127.0.0.1:8500):

ELK 堆栈

在上面的截图中,你可以看到我们的 Logstash 和 Kibana 服务已经运行,但 Elasticsearch 在哪里呢?别担心,Elasticsearch 已经在那儿,只是因为我们没有将任何端口转发到主机网络,所以在 Consul 中看不到它。Registrator 只会注册具有暴露端口的服务。我们可以通过登录到 Kibana 的 Web UI (127.0.0.1:8080) 来确认我们的 ELK 堆栈是否配置正确:

ELK 堆栈

接下来我们需要做的是点击创建按钮。然后,如果我们进入发现标签页,我们可以看到来自 Logstash 的日志:

ELK 堆栈

Logstash 的日志

总结

在这一章中,我们学习了如何使用 Puppet 在多个节点上部署容器。我们利用了本地的 Docker 网络来隐藏服务。这在处理生产环境时是一种很好的安全实践。本章唯一的问题是我们的应用程序没有任何故障转移或弹性处理。这就是容器调度器如此重要的原因。

在下一章中,我们将深入探讨三种不同的调度器,为你提供未来做出合理设计决策所需的知识。

第七章:容器调度器

现在,我们已经进入本书的核心部分。目前,关于这个话题有很多讨论。容器的未来将会是这样的,而调度器可以解决很多问题,比如根据负载将我们的应用程序负载分配到多个主机上,以及在原始主机失败时,在另一个实例上启动容器。在这一章中,我们将介绍三种不同的调度器。首先,我们将介绍 Docker Swarm,这是一个 Docker 开源调度器。我们将搭建五台服务器,并演示如何创建一个复制的主节点。然后,我们将运行几个容器,看看 Swarm 如何在节点之间调度它们。接下来,我们将介绍 Docker UCPUniversal Control Plane)。这是 Docker 的企业解决方案,并与 Docker Compose 集成。我们将搭建一个三节点集群并部署我们的 Consul 模块。由于 UCP 有图形化界面,我们将从那里了解 UCP 的调度方式。最后,我们将介绍 Kubernetes。这是谷歌提供的解决方案,也是开源的。对于 Kubernetes,我们将使用容器搭建一个单节点,并利用 Puppet 定义更复杂的类型。正如你所看到的,我们将从不同的角度来看待每个调度器,因为它们各自有不同的优缺点。根据你的使用场景,你可能会选择其中一个或多个调度器来解决你可能遇到的问题。

Docker Swarm

对于我们的第一个调度器,我们将介绍 Docker Swarm。这是一个非常稳定的产品,在我看来,相较于 Kubernetes,它有些被低估了。在过去几次发布中,Swarm 有了长足的进展。现在它支持主节点复制,并能在主机失败时重新调度容器。那么,接下来我们将看一下我们要构建的架构,之后我们会进入编程部分。

Docker Swarm 架构

在这个示例中,我们将搭建五台服务器,其中两台将作为主节点的复制,其他三台将加入 Swarm 集群。由于 Docker Swarm 需要一个键值存储后端,我们将使用 Consul。在这个示例中,我们不会使用我们的 Consul 模块,而是使用forge.puppetlabs.com/KyleAnderson/consul。这样做的原因是,在这三个示例中,我们将采用不同的设计选择。因此,在构建解决方案时,你将面临多种实现方式。在这个例子中,我们将使用 Puppet 将 Swarm 和 Consul 安装到操作系统上,然后在其上运行容器。

编程

在这个例子中,我们将创建一个新的 Vagrant 仓库。所以,我们将使用 Git 克隆github.com/scotty-c/vagrant-template.git到我们选择的目录中。我们首先要编辑的是 Puppetfile。它位于 Vagrant 仓库的根目录中。我们将向文件中添加以下更改:

编码

接下来我们将编辑的文件是 servers.yaml。同样,该文件位于我们 Vagrant 仓库的根目录下。我们将向其中添加五个服务器。因此,我会将该文件分解为五个部分,每个部分对应一个服务器。

首先,让我们看一下服务器 1 的代码:

编码

现在,让我们看看服务器 2 的代码:

编码

以下截图显示了服务器 3 的代码:

编码

以下截图显示了服务器 4 的代码:

编码

最终,服务器 5 的代码如下:

编码

这一切对你来说应该比较熟悉。需要特别注意的是,我们已使用了服务器 1 至 3 作为我们的集群节点。4 和 5 将是我们的主节点。

接下来我们要做的是向 Hiera 添加一些值。这是我们第一次使用 Hiera,所以让我们查看位于 Vagrant 仓库根目录下的 hiera.yaml 文件,看看我们的配置,如下所示:

编码

如你所见,我们有一个基本的层级结构。我们只需要查看一个文件,即我们的 global.yaml 文件。我们可以看到该文件位于 hieradata 文件夹中,因为它被声明为我们的 data 目录。因此,如果我们打开 global.yaml 文件,我们将向其中添加以下值:

编码

第一个值将告诉 Swarm 我们要使用哪个版本。最后一个值设置了我们将使用的 Consul 版本,即 0.6.3

接下来我们需要做的是编写部署 Consul 和 Swarm 集群的模块。到目前为止,我们在书中已经创建了相当多的模块,因此这里不再重复讲解。我们将创建一个名为 <AUTHOR>-config 的模块。然后,我们将把该模块移到 Vagrant 仓库根目录下的 modules 文件夹中。现在,让我们看看我们将要添加到 init.pp 文件中的内容:

编码

如你所见,这将设置我们的模块。我们需要为 init.pp 文件中的每个条目创建一个 .pp 文件,例如 consul_config.ppswarm.pp 等等。我们还声明了一个名为 consul_ip 的变量,其值实际上来自 Hiera,因为我们之前就在那里设置了这个值。我们将按字母顺序查看我们的文件。因此,我们将从 config::compose.pp 开始,它如下所示:

编码

在这个类中,我们正在设置一个注册器。我们将使用 Docker Compose 来实现这一点,因为它是一种熟悉的配置,而且我们之前已经讲解过。你会注意到代码中有一个 if 语句。这是一个逻辑,只有在集群成员上才会运行容器。我们不希望在主节点上运行容器应用程序。接下来,我们将查看的文件是 consul_config.pp,它如下所示:

编码

在这个类中,我们将配置 Consul,这个内容我们在本书中已经介绍过。我总是喜欢寻找多种方法来完成同样的任务,因为你永远不知道明天可能需要的解决方案,而且你总是想为每个任务选择合适的工具。因此,在这个例子中,我们将不会在容器中配置 Consul,而是直接在主机操作系统上配置。

你可以看到,代码被分成了三个块。第一个块引导我们的 Consul 集群。你会注意到,配置是熟悉的,因为它们与我们在前一章节中用来设置容器的配置相同。第二个代码块在集群引导后设置集群成员。我们之前没有见过这个第三个代码块。它为 Consul 设置了一个监控服务。这只是我们可以管理的一小部分,但我们将在下一章深入探讨。接下来让我们看看下一个文件,即dns.pp

编码

你会注意到,这个文件与我们在consul模块中使用的文件完全相同。所以,我们可以继续下一个文件,也就是run_containers.pp

编码

这是在我们的集群上运行容器的类。顶部声明了我们希望第二个主机发起对集群的调用。我们将部署三个容器应用。第一个是jenkins,如你所见,我们将把 Jenkins 的 Web 端口8080暴露给容器运行的主机。下一个容器是nginx,我们将同时将80443端口转发到主机,并且将nginx连接到我们的私有网络,即swarm-private集群。为了从nginx获取日志,我们将告诉容器使用 syslog 作为日志驱动程序。我们将运行的最后一个应用是redis。这将作为nginx的后端。你会注意到,我们没有转发任何端口,因为我们将 Redis 隐藏在我们的内部网络中。现在我们的容器已经配置完毕,我们还有一个文件没有处理,那就是swarm.pp。这个文件将配置我们的 Swarm 集群和内部 Swarm 网络,如下所示:

编码

第一个资源声明将安装 Swarm 二进制文件。下一个资源将配置每个主机上的 Docker 网络。接下来的if语句将定义 Swarm 节点是主机还是集群的一部分,通过hostname来判断。如你所见,我们声明了一些默认值,这些值是告诉 Swarm 使用 Consul 作为后端的初始值。第二个值告诉 Swarm Consul 后端的 IP 地址,即172.17.8.101。第三个值告诉 Swarm 它可以在8500端口访问 Swarm。第四个值告诉 Swarm 在哪个接口上广播集群,在我们的情况下是enp0s8。最后一个值设置 Swarm 将使用的键值存储的根目录。现在,让我们在模块的根目录下创建templates文件夹。

我们将在那里创建三个文件。第一个文件将是 consul.conf.erb,其内容如下:

编码

下一个文件将是 named.conf.erb,其内容如下:

编码

最后的文件将是 registrator.yml.erb,文件内容如下:

编码

接下来,我们需要在 Vagrant 仓库根目录下的 manifest 文件夹中的 default.pp 清单文件中添加我们的 config 类:

编码

现在,我们的模块已完成,准备运行我们的集群。所以,让我们打开终端,将目录更改为 Vagrant 仓库的根目录,并输入 vagrant up 命令。现在,我们正在构建五个服务器,所以请耐心等待。待最后一个服务器构建完成后,终端输出应该与截图中所示的内容类似:

编码

终端输出

现在,我们可以通过 127.0.0.1:9501 查看我们的 Consul 集群的 Web 界面(记得我们在 servers.yaml 文件中更改了端口),如下所示:

编码

现在,让我们看看我们的 Jenkins 服务运行在哪个主机上:

编码

在这个例子中,我的服务在集群节点 101 上启动。你可能会在集群中得到一个不同的主机。所以,我们需要检查 8080 端口转发到我们的 servers.yaml 文件中的哪个端口。在我的例子中,它是 8081。所以,如果我打开浏览器,打开一个新标签页并访问 127.0.0.1:8081,我们会看到 Jenkins 页面,内容如下:

编码

你可以用 nginx 做同样的事,我将把这个任务留给你作为挑战。

Docker UCP

本主题将介绍 Docker 的新产品 UCP。该产品并非开源,因此需要付费许可。你可以获得一个 30 天的试用许可(www.docker.com/products/docker-universal-control-plane),我们将使用这个试用许可。Docker UCP 简化了管理调度器所有组件的复杂性,这可能在你的使用场景中是一个巨大的优势。Docker UCP 还带有一个 Web UI 用于管理。所以,如果容器调度器看起来令你感到棘手,这可能是一个完美的解决方案。

Docker UCP 架构

在这个例子中,我们将构建三个节点。第一个将是我们的 UCP 控制器,其他两个节点将是 UCP HA 副本,提供故障容忍性。由于 UCP 是一个封装产品,我不会深入讨论所有的组成部分。

请参考下图,了解主要组件的可视化:

Docker UCP 架构

编码

在这个模块中,我们将做一些不同的事情,目的是展示我们的模块可以移植到任何操作系统。我们将使用 Ubuntu 14.04 服务器来构建这个模块。同样,对于这个环境,我们将创建一个新的 Vagrant 仓库。那么,让我们通过 Git 克隆我们的 Vagrant 仓库(github.com/scotty-c/vagrant-template.git)。和上一个话题一样,我们首先会查看管道设置,然后再编写我们的模块。我们要做的第一件事是创建一个名为config.json的文件。该文件将包含你的 Docker Hub 认证信息:

编码

接下来是docker_subscription.lic文件。该文件将包含你的试用许可证。

现在,让我们来看一下 Vagrant 仓库根目录中的servers.yaml文件,如下所示:

编码

这里的主要内容是,我们现在使用的是puppetlabs/ubuntu-14.04-64-puppet-enterprise Vagrant 盒子。我们已经将yum改为apt-get。接着,我们将config.jsondocker_subscription.lic文件复制到 Vagrant 盒子上的正确位置。

现在,我们将查看需要在我们的 Puppetfile 中进行的更改:

编码

你会看到我们需要从 Forge 下载一些新的模块。Docker 模块是熟悉的,stdlib 模块也一样。我们还需要 Puppetlab 的apt模块来控制 Ubuntu 用来拉取 Docker 的仓库。最后一个模块是 Puppetlabs 为 UCP 本身提供的模块。要了解更多关于这个模块的信息,可以访问forge.puppetlabs.com/puppetlabs/docker_ucp。我们将编写一个包装此类并为我们的环境配置的模块。

现在,来看一下我们在hieradata/global.yaml中的 Hiera 文件:

编码

如你所见,我们添加了两个值。第一个是ucpconfig::ucp_url:,我们将其设置为我们的第一个 Vagrant 盒子。接下来的值是ucpconfig::ucp_fingerprint:,我们暂时将其留空。但请记住它,因为我们稍后会回来处理这个话题。

现在,我们将创建一个名为<AUTHOR>-ucpconfig的模块。我们已经做过几次了,所以一旦你创建了模块,就在我们的 Vagrant 仓库的根目录中创建一个名为modules的文件夹,并将ucpconfig移到该文件夹中。

然后,我们将在模块的manifest目录中创建三个清单文件。第一个文件将是master.pp,第二个文件将是node.pp,最后一个文件将是params.pp

现在,让我们将代码添加到params.pp文件中,如下所示:

编码

如你所见,我们有四个值:Ucp_url,它来自 Hiera;ucp_username,其默认值设置为 admin;接下来是 ucp_password,其默认值设置为 orca。最后一个值是 ucp_fingerprint,它也来自 Hiera。现在,在生产环境中,我会将用户名和密码都设置在 Hiera 中,并覆盖我们在 params.pp 中设置的默认值。在这个测试实验室中,我们将使用默认值。

我们接下来要查看的文件是我们的 init.pp 文件,内容如下:

编码

你可以看到,在类的顶部,我们正在映射我们的 params.pp 文件。接下来的声明安装 docker 类,并设置守护进程的 socket_bind 参数。接下来的一段逻辑定义了节点是否为主节点或普通节点,这取决于主机名。如你所见,我们只将 ucp-01 设置为我们的主节点。

现在,让我们看一下 master.pp

编码

在这个类中,我们有安装 UCP 控制器或主节点的逻辑。在类的顶部,我们将参数映射到我们的 init.pp 文件。接下来的代码块调用了 docker_ucp 类。如你所见,我们将控制器的值设置为 true,主机地址设置为我们的第二个接口,集群的备用名称设置为我们的第一个接口,版本设置为 1.0.1(这是编写本书时的最新版本)。然后,我们将为控制器和 Swarm 设置端口。接着,我们会告诉 UCP Docker 套接字的位置,并提供许可证文件的位置。

现在,让我们看一下我们的最后一个文件,node.pp

编码

如你所见,大部分设置可能看起来很熟悉。需要注意的是,我们需要将节点指向控制器 URL(我们在 Hiera 中设置了该 URL)。稍后我们将了解管理员用户名和密码以及集群指纹。所以,这就完成了我们的模块。现在,我们需要将我们的类添加到节点中,这将通过在 Vagrant 仓库根目录下的 manifests/default.pp 位置添加 default.pp 清单文件来实现,具体如下:

编码

现在,我们进入终端并将目录切换到我们 Vagrant 仓库的根目录。这次,我们要做些不同的事情。我们将执行命令vagrant up ucp-01。这将只启动第一个节点。这样做是因为我们需要获取在 UCP 启动时生成的指纹。

我们的终端输出应如下图所示:

编码

终端输出

你会注意到指纹已经显示在你的终端输出中。以我的示例为例,指纹是 INFO[0031] UCP Server SSL: SHA1 Fingerprint=C2:7C:BB:C8:CF:26:59:0F:DB:BB:11:BC:02:18:C4:A4:18:C4:05:4E。所以,我们将把这个指纹添加到我们的 Hiera 文件中,即 global.yaml

编码

现在我们的第一个节点已经启动,我们应该能够登录 Web UI。我们在浏览器中执行此操作。我们将访问 https:127.0.0.1:8443,并看到如下登录页面:

编码

然后,我们将添加在 params.pp 文件中设置的用户名和密码:

编码

然后,在我们登录后,你会看到我们有一个健康集群,如下所示:

编码

登录后健康集群

现在,让我们回到终端,执行 vagrant up ucp-02 && vagrant up ucp-03 命令。

完成后,如果我们查看 Web UI,就能看到集群中有三个节点,具体如下:

编码

在本书中,我们不会深入探讨如何通过 Web UI 管理集群。我强烈建议你探索这个产品,它具有一些非常酷的功能。所有文档都可以在 docs.docker.com/ucp/overview/ 获取。

Kubernetes

当前 Kubernetes 备受关注。这是 Google 为容器世界提供的解决方案。Kubernetes 得到了 Google、CoreOS 和 Netflix 等重量级企业的支持。在我们考察过的所有调度器中,Kubernetes 是最复杂的,而且高度依赖于 API。如果你是 Kubernetes 的新手,我建议你进一步了解这个产品,参考 kubernetes.io/。我们将首先了解 Kubernetes 的架构,因为它包含一些动态组件。然后,我们将编写模块,使用容器来完全构建 Kubernetes。

架构

我们将会在单个节点上构建 Kubernetes。这样做的原因是,它可以减少使用 Docker 桥接网络时 Flannel 的一些复杂性。本模块将帮助你深入了解 Kubernetes 的工作原理,并使用更高级的 Puppet 技巧。如果你掌握了这一章的内容并希望更进一步,我建议你访问 forge.puppetlabs.com/garethr/kubernetes 上的模块。这个模块能将 Puppet 和 Kubernetes 推向一个全新的水平。

所以我们要编写的代码如下图所示:

架构

如你所见,我们有一个运行 etcd 的容器(要了解更多关于 etcd 的信息,请访问 coreos.com/etcd/docs/latest/)。etcd 类似于我们熟悉的 Consul。在接下来的几个容器中,我们将使用 hyperkube (godoc.org/k8s.io/kubernetes/pkg/hyperkube)。它将为我们在多个容器中负载均衡所需的 Kubernetes 组件。看起来挺简单,对吧?让我们进入代码,深入理解所有这些动态组件。

编码

我们将再次创建一个新的 Vagrant 仓库。我们不会再重复创建的步骤,因为我们在本章中已经介绍过两次。如果你不确定,只需查看本章的前面部分。

一旦我们创建了 Vagrant 仓库,接下来打开我们的 servers.yaml 文件,如下所示:

Coding

如你所见,这里没有什么特别的内容,我们在本书中都已经涉及过了。这里只有我们之前提到的单节点 kubernetes。接下来我们要查看的文件是我们的 Puppetfile。我们当然需要我们的 Docker 模块,stdlib,最后是 wget。我们需要 wget 来获取 kubectl

Coding

这就是我们设置仓库所需的所有基础设施。让我们创建一个新的模块,名为 <AUTHOR>-kubernetes_docker。一旦它创建完成,我们将把它移动到 Vagrant 仓库根目录下的 modules 目录中。

我们将在模块中创建两个新文件夹。第一个将是 templates 文件夹,另一个是 lib 目录。我们将在编码的后期介绍 lib 目录。我们首先要创建和编辑的文件是 docker-compose.yml.erb。这样做的原因是它是我们模块的基础。我们将向其中添加以下代码:

Coding

让我们将这个文件分成三个部分,因为里面有很多内容。第一块代码将会设置我们的 etcd 集群。你可以从截图的名称看出,我们使用的是 Google 官方镜像,并且我们使用的是 etcd 版本 2.2.1。我们将容器的访问权限授予主机网络。然后,在命令资源中,我们在启动时传递一些参数给 etcd。

下一个我们创建的容器是 hyperkube。同样,它是一个官方的 Google 镜像。现在,我们将赋予这个容器访问大量主机卷、主机网络和主机进程的权限,使得容器具有特权。这是因为第一个容器将引导 Kubernetes 启动,并且它将启动更多的容器来运行各种 Kubernetes 组件。现在,在命令资源中,我们再次传递一些参数给 hyperkube。我们需要关注的两个主要参数是 API 服务器地址和配置清单。你会注意到我们已经将文件夹 /kubeconfig:/etc/kubernetes/manifests:ro 映射过来了。我们将修改我们的清单文件,使得 Kubernetes 环境可以对外部可用。我们稍后会介绍这个,但我们先完成对这个文件中代码的分析。

最后的容器和第三块代码将会设置我们的服务代理。我们将赋予这个容器访问主机网络和进程的权限。在命令资源中,我们将指定该容器为代理。接下来需要注意的是,我们指定了代理可以找到 API 的位置。现在,让我们创建下一个文件,master.json.erb。这是 hyperkube 用来调度所有 Kubernetes 组件的文件,具体如下:

编码

如你所见,我们已经定义了三个容器。这是我们第一次定义 Kubernetes pod(kubernetes.io/docs/user-guide/pods/)。Pod 是一组容器,它们共同构成一个应用程序。这与我们在 Docker Compose 中做的类似。如你所见,我们已将所有 IP 地址更改为 <%= @master_ip %> 参数。我们将创建四个新文件:apps.ppconfig.ppinstall.ppparams.pp

现在,我们将继续操作 modules 清单目录中的文件。准备好,因为这里是魔法发生的地方。其实,这不完全正确。魔法发生在这里和我们的 lib 目录中。我们需要为 Puppet 编写一些自定义类型和提供程序,以便它能够控制 Kubernetes,因为 kubectl 是用户界面(关于类型,请访问 docs.puppetlabs.com/guides/custom_types.html,关于提供程序,请访问 docs.puppetlabs.com/guides/provider_development.html)。

让我们从 init.pp 文件开始,如下所示:

编码

如你所见,这个文件里没有太多内容。我们将使用 init.pp 文件来控制类执行的顺序。我们还声明了 param <%= @master_ip %>。接下来,我们将继续操作 install.pp 文件,如下所示:

编码

在这个文件中,我们像之前一样安装 Docker。我们将放置我们之前创建的两个模板。然后,我们将运行 Docker Compose 来启动我们的集群。接下来,我们将继续操作 config.pp,如下所示:

编码

我们首先声明,我们希望将 wget 设置为我们的 kubectl 客户端,并将其放置在 /usr/bin/ 目录下(kubernetes.io/docs/user-guide/kubectl/kubectl/)。你需要真正理解这个接口的作用,否则接下来的步骤可能会让你有些迷茫。所以,我建议你对 kubectl 有一个相对清晰的认识,并了解它的功能。接下来,我们将使其可执行并对所有用户可用。现在,最后这段代码没有意义,因为我们还没有调用 kubectl_config 类:

编码

现在,我们需要跳转到 lib 目录。首先,我们将创建所有需要的文件夹。我们将首先在 lib 目录下创建一个名为 puppet 的文件夹。我们将先来看一下我们的自定义类型。我们将在 puppet 文件夹下创建一个名为 type 的文件夹。以下截图将帮助你理解结构:

编码

type 文件夹下,我们将创建一个名为 kubectl_config.rb 的文件。在该文件中,我们将添加新的 type 参数,如下所示:

编码

让我解释一下这里发生了什么。在第一行中,我们将声明我们的新类型kubectl_config。然后,当新类型被声明为present时,我们将设置其默认值。接下来,我们将为我们的类型声明三个值:nameclusterkube_context。这些都是我们将添加到config文件中的设置,该文件将在与kubectl交互时使用。现在,我们将在lib目录下创建一个名为provider的文件夹。然后,在其中创建一个文件夹,文件夹名称与我们自定义类型kubectl_config相同。在该文件夹内,我们将创建一个名为ruby.rb的文件。在这个文件中,我们将放置提供逻辑的 Ruby 代码,如下所示:

Coding

提供程序需要有三个方法,以便 Puppet 能够运行代码。它们是exists?createdestroy。这些方法都很容易理解。exists?方法检查类型是否已经被 Puppet 执行,create运行类型,destroy在类型设置为absent时被调用。

我们现在将从上到下地处理这个文件。我们需要首先加载一些 Ruby 库,以支持我们的某些方法。然后,我们将把这个提供程序与我们的类型绑定。接下来我们需要声明的是kubectl可执行文件。

现在我们将编写我们的第一个方法interface。这个方法将从主机的主机名获取 IP 地址。接着我们将创建三个其他方法。我们还会创建一个数组,并将所有配置添加到其中。你会注意到,我们正在将我们的参数从类型映射到这些数组中。

在我们的exists?方法中,我们将检查我们的kubectl配置文件。

在我们的create方法中,我们调用kubectl可执行文件,并将我们的数组作为参数传递。然后,我们将把config文件链接到根目录的主目录(对于我们的实验室环境来说是可以的,在生产环境中,我会使用一个专门的用户账户)。

最后,如果类型被设置为absent,我们将删除config文件。然后我们将回到manifests目录,查看我们的最后一个文件,即apps.pp

Coding

在这个文件中,我们将运行一个容器应用程序在我们的 Kubernetes 集群上。再一次,我们将编写另一个自定义类型和提供程序。在我们进入这个内容之前,我们应该先看看这个类中的代码。如你所见,我们的类型叫做kubernetes_run。我们可以看到我们的服务名称是nginx,我们将拉取的 Docker 镜像是nginx,然后我们将暴露端口80

让我们回到lib目录。然后,我们将在type文件夹中创建一个名为kubernetes_run.rb的文件。在这个文件中,我们将像之前一样设置我们的自定义类型:

Coding

如你所见,我们正在映射与apps.pp文件中相同的参数。接着,我们将在provider文件夹下创建一个与kubernetes_run类型同名的文件夹。同样,在新创建的目录下,我们将创建一个名为ruby.rb的文件。它将包含如下截图中的代码:

Coding

在这个文件中,我们这次将添加两个命令:第一个是kubectl,第二个是docker。我们将创建两个方法,同样使用数组来映射我们类型中的值。

现在,让我们看看我们的exists?方法。我们将传递一个数组作为参数给kubectl,以检查服务是否存在。如果kubectl在请求时抛出错误并返回false,我们将捕获这个错误。这用于在集群中没有部署任何服务的情况。

在我们的create方法中,我们将首先传递一个数组给kubectl以获取集群中的节点。我们将使用这个命令作为一个任意命令来确保集群正常运行。接着,我们会捕获错误并重试命令直到成功。一旦成功,我们将通过ensure资源部署我们的容器。

destroy方法中,我们将使用docker来移除我们的容器。

现在,我们已经完成了所有的代码编写。接下来,我们只需要通过编辑 Vagrant 仓库根目录下manifests文件夹中的default.pp文件,将我们的类添加到节点中,如下所示:

Coding

现在,让我们打开终端并将目录切换到 Vagrant 仓库的根目录,然后执行vagrant up命令。Puppet 执行完毕后,终端应该呈现如下截图:

Coding

现在,我们将通过执行vagrant ssh命令登录到我们的 vagrant 盒子,然后输入sudo -i切换到 root 用户。成为 root 后,我们将查看集群中的服务。我们通过执行kubectl get svc命令来实现,如下所示:

Coding

如你所见,我们的集群上正在运行两个服务:Kubernetesnginx。如果我们打开网页浏览器并访问我们为第二个网络接口设置的地址http://172.17.9.101,我们将看到以下 nginx 默认页面:

Coding

现在,我们的集群已经成功运行,并且nginx服务也在正常工作。

总结

我们介绍了我最喜欢的三个容器调度器,每一个都有各自的优缺点。现在,你已经掌握了相关的知识和所需的代码,可以对这三者进行一次充分的测试。我建议你这样做,这样在选择环境设计时,你可以做出正确的选择。

第八章:日志记录、监控和恢复技术

在本章中,我们将研究我们的一个调度器,并在其上进行一些额外的操作任务包装。到目前为止,在本书中,我们已经涵盖了许多更为引人注目的主题;然而,监控、日志记录和自动恢复同样重要。我们希望将这些知识应用到实际工作中。从那里开始,我们可以开始看到开发和运维团队的好处。本章我们将使用 Docker Swarm 作为调度器。对于日志记录,我们将使用 ELK 堆栈,而对于监控,我们将使用 Consul。自 Docker Swarm 版本 1.1.3 以来,出现了一些很酷的功能,帮助我们使用恢复,因此我们将重点讨论这些功能。本章将涵盖以下主题:

  • 日志记录

  • 监控

  • 恢复技术

日志记录

日志记录在解决方案中的重要性无可比拟。如果我们需要调试任何代码/基础设施的问题,日志是最先需要查看的地方。在容器世界中,这一点也不例外。在之前的章节中,我们构建了 ELK 堆栈。我们将再次使用它来处理所有来自容器的日志。在这个解决方案中,我们将利用到目前为止学到的大量知识。我们将使用调度器、Docker 网络,并最终使用 Consul 进行服务发现。所以,让我们来看一下这个解决方案,就像在之前的章节中一样,我们将开始编码。

解决方案

正如我在本章介绍部分提到的,我们将使用 Docker Swarm 来实现这个解决方案。选择 Swarm 的原因是我想强调 Swarm 的一些新特性,因为它在最近几个版本中有了显著的进展。在本章的日志记录部分,我们将部署三个容器并让 Swarm 来调度它们。我们将结合使用 Docker 网络 DNS 和 Consul 的服务发现来将所有内容连接起来。在 Swarm 中,我们将使用与上一章相同的服务器:三个成员节点和两个复制的主节点。每个节点都将是我们 Consul 集群的成员。我们将再次使用 Puppet 在主机系统上本地安装 Consul。

代码

在本章中,我们将基于上一章中使用的 Docker Swarm 代码进行扩展。所以,我们将快速浏览 Vagrant 仓库的基本架构,只指出与上一章的不同之处。我们将再次为本章创建一个新的 Vagrant 仓库。到现在为止,你应该已经非常熟练了。一旦新仓库设置好,打开servers.yaml文件。我们将向其中添加以下代码:

代码

servers.yaml文件的代码

正如你所看到的,与上一章节相比,并没有太大的不同。有一个值得注意的地方。我们为每个服务器添加了一个新的行 - { shell: 'echo -e "PEERDNS=no\nDNS1=127.0.0.1\nDNS2=8.8.8.8">>/etc/sysconfig/network-scripts/ifcfg-enp0s8 && systemctl restart network'}。我们会在服务器多重主机环境下确保正确解析 DNS。

接下来我们将看一下 puppetfile 文件,如下所示:

代码

正如你从代码中看到的,与上一章节相比,并没有什么新的变化。所以,让我们转向我们模块根目录中的heiradata/global.yml中的 Hiera 文件:

代码

正如你从代码中看到的,我们正在将 Swarm 版本设置为v1.1.3,后端设置为consul。我们设置了 Consul 集群中第一个节点的 IP 地址,并将 Consul 端口设置为8500。我们将设置我们从中广播的swarm接口,并最后但同样重要的是,我们将设置我们的 Consul 版本为0.6.3

现在,我们将创建我们的模块。我们再次调用config模块。一旦你创建了你的<AUTHOR>-config模块,请将它移动到你的 Vagrant 存储库根目录中的modules文件夹中。

现在我们有了我们的模块,让我们在其中添加我们的代码。我们需要在manifests目录下创建以下文件:compose.ppconsul_configdns.pprun_containers.ppswarm.pp。由于在此示例中使用 Hiera,我们不需要params.pp

所以,让我们按照字母顺序浏览文件。在我们的compose.pp文件中,我们将添加以下代码:

代码

正如你从代码中看到的,我们正在将我们的docker-compose.yml文件添加到任何不是 Swarm 主节点的节点上。当我们查看templates目录时,我们将回到docker-compose.yml文件。接下来的文件是consul_config.pp,如下所示:

代码

在这个文件中,我们声明了我们的 Consul 集群,并定义了引导服务器。这应该看起来很熟悉,因为这与我们在上一章节中使用的代码相同。接下来是dns.pp文件,如下所示:

代码

这段代码应该看起来很熟悉,因为我们在上一章节中已经使用过它。简要回顾一下,这里是设置和配置我们的绑定包,使用 Consul 作为 DNS 服务器。接下来我们将查看init.pp文件:

代码

init.pp文件中,我们只是在我们的模块内部排序我们的类。现在我们将转向run_containers.pp。这里我们将在 Swarm 集群中安排我们的 ELK 容器:

代码

让我们详细看一下这一点,因为这里有很多新的代码。我们将使用的第一个声明是从第二个 Swarm 主节点调度容器。

下一个代码块将配置我们的logstash容器。我们需要首先在这个示例中有这些容器,因为我们将它们作为 syslog 服务器使用。如果在创建容器时它们无法连接到logstash的 TCP 端口5000,容器构建将失败。因此,让我们继续配置logstash。我们将使用我提供的容器,因为它是官方容器,并且我们已经添加了logstash.conf文件。接下来,我们将logstash添加到我们的内部swarm-private Docker 网络,并在所有网络上暴露logstash的所有端口。这样,我们就可以从任何地方将日志传输到它。之后,我们将设置elasticsearch的位置,然后给出启动命令。

Logstash

在第二个代码块中,我们将安装并配置elasticsearch。我们将使用官方的elasticsearch容器(版本 2.1.0)。我们只会将elasticsearch添加到我们的私有 Docker 网络swarm-private中。我们将通过声明卷映射来保持数据持久性。我们将设置命令和参数,以便通过命令值启动elasticsearch。接下来,我们将设置日志驱动为 syslog,并将 syslog 服务器设置为tcp://logstash-5000.service.consul:5000。请注意,我们正在使用我们的 Consul 服务发现地址,因为我们在外部网络上暴露了logstash。最后,我们设置logstash的依赖关系。正如我之前提到的,syslog 服务器需要在该容器启动时可用,因此我们需要logstash在此容器或kibana之前就已经存在。说到 Kibana,让我们继续到最后一个代码块。

在我们的kibana容器中,我们将添加以下配置。首先,我们将使用官方的kibana镜像(版本 4.3.0)。我们将kibana添加到我们的swarm-private网络,以便它能够访问我们的elasticsearch容器。我们将把端口5601映射到主机网络上的80端口。在最后几行中,我们将以与elasticsearch相同的方式设置 syslog 配置。

现在,是时候处理我们的最后一个文件swarm.pp,内容如下:

Logstash

在这段代码中,我们正在配置我们的 Swarm 集群和 Docker 网络。

现在,我们将进入模块根目录下的templates文件夹。我们需要创建三个文件。两个文件Consul.conf.erbnamed.conf.erb是用于我们的绑定配置。最后一个文件是我们的registrator.yml.erb Docker Compose 文件。我们将把代码添加到以下文件中。

首先,让我们看看consul.conf.erb的代码,内容如下:

Logstash

现在,让我们看看named.conf.erb的代码,内容如下:

Logstash

最后,让我们看看registrator.yml.erb的代码,内容如下:

Logstash

这些文件中的所有代码应该都非常熟悉,因为我们在之前的章节中已经使用过它。

现在,在运行我们的集群之前,我们只需要做最后一项配置。让我们前往 Vagrant 仓库根目录中的manifests文件夹中的default.pp清单文件。

现在,我们将向清单文件中添加相关的节点定义:

Logstash

我们现在准备进入终端,切换到我们的 Vagrant 仓库根目录。像以前一样,我们将运行vagrant up命令。如果你还保留了上一章配置的盒子,可以运行vagrant destroy -f && vagrant up命令。

一旦 Vagrant 运行并且 Puppet 构建了我们的五个节点,我们就可以打开浏览器并访问http://127.0.0.1:9501/。此时我们应该看到以下页面:

Logstash

如你所见,所有我们的服务都以绿色显示,表示健康状态。接下来,我们需要找到我们的kibana容器运行在哪个节点上。我们将通过点击kibana服务来完成这一步。

Logstash

在我的示例中,kibana已经在swarm-101上启动。如果你的情况不同,不用担心,因为 Swarm 集群可能已经将容器调度到了三个节点中的任何一个。现在,我将打开一个浏览器标签,输入127.0.0.1:8001/,如下面的截图所示:

Logstash

如果你的主机不同,请参考servers.yaml文件以获取正确的端口。

然后,我们将创建索引并点击Discovery标签,如截图所示,我们的日志已经进入:

Logstash

创建索引后的日志

监控

在容器的世界里,你可以部署几种监控级别。例如,你有传统的运维监控。比如 Nagios、Zabbix,甚至可能是像 Datadog 这样的云解决方案。这些解决方案都能很好地与 Docker 集成,并且可以通过 Puppet 进行部署。在本书中,我们假设运维团队已经完成了这一部分工作,传统监控已经就绪。我们将关注下一个监控级别。我们将集中在容器的连接性和 Swarm 集群的健康状况上。我们将在 Consul 中完成这些,并通过 Puppet 部署我们的代码。

我们关注这个级别的监控是因为我们可以根据 Consul 报告做出决策。我们是否需要扩展容器?一个 Swarm 节点是否出现故障?我们是否应该将它从集群中移除?要解决这些问题,我们需要写一本单独的书。我不会覆盖这些解决方案。我们将关注的是达到这些目标的第一步。现在,种子已经播下,你将希望进一步探索你的选项。挑战在于改变我们对监控的思维方式,以及它如何需要人类的反应性互动,这样我们就可以信任我们的代码为我们做出选择,并使我们的解决方案完全自动化。

使用 Consul 进行监控

使用 Consul 的一个很好的优点是,Hashicorp 在文档编写方面做得非常出色,Consul 也不例外。如果你想了解更多关于 Consul 监控选项的信息,请参考 www.consul.io/docs/agent/services.htmlwww.consul.io/docs/agent/checks.html 的文档。我们将设置检查和服务。在上一章中,我们编写了一个服务来使用 Consul 监控每个节点上的 Docker 服务:

使用 Consul 进行监控

在 Consul Web UI 上,我们看到以下 Docker 服务的节点读取:

使用 Consul 进行监控

我们将把所有新的检查应用到两个 Swarm 主节点。这样做的原因是,这两个节点位于集群之外。这样的抽象化让我们无需担心容器运行在哪些节点上。你还可以从多个位置进行监控轮询。例如,在 AWS 中,你的 Swarm 主节点可能分布在多个可用区(AZ)。因此,即使丢失一个可用区,你的监控仍然可用。

因为我们将使用前一节中介绍的日志解决方案,所以我们需要检查并确保 Logstash 和 Kibana 都可用;Logstash 在端口 5000,Kibana 在端口 80。

我们将向 consul_config.pp 文件中的配置模块添加两个新的服务检查,如下所示:

使用 Consul 进行监控

如你所见,我们为 kibanalogstash 都设置了 TCP 检查,并且我们将使用服务发现地址来测试连接。接下来,我们将在终端中打开并切换到 Vagrant 仓库的根目录。

现在,我们假设你的五个节点正在运行。我们将向 Vagrant 发出命令,仅为两个主节点进行配置。这个命令是 vagrant provision swarm-master-01 && vagrant provision swarm-master-02。然后,我们将打开浏览器并输入 127.0.0.1:9501。接着你可以点击swarm-master-01swarm-master-02,选择权在你。完成后,你应该会看到如下结果:

使用 Consul 进行监控

如你所见,我们的监控是成功的。接下来我们将回到代码中,为我们的 swarm master 添加一个检查,以确定其健康状况。我们将通过以下代码来实现:

使用 Consul 进行监控

然后,我们将执行 Vagrant 提供命令,vagrant provision swarm-master-01 && vagrant provision swarm-master-02。接着,我们将打开浏览器并点击swarm-master-01swarm-master-02。完成后,你应该会看到如下结果:

使用 Consul 进行监控

从健康检查中可以看到的信息,我们可以轻松地识别出哪个 Swarm 主节点是主节点,以及调度策略。这在你遇到问题时会非常有用。

如你所见,Consul 是一个非常实用的工具,如果你想了解我们在本章中讨论的内容,你真的可以做一些很酷的事情。

恢复技术

在每个解决方案中,拥有一些恢复技术是非常重要的。在容器世界中,情况也不例外。有很多种方式可以实现这一点,比如使用 HA 代理进行负载均衡,甚至使用专为此目的设计的容器化应用程序,例如 interlock(github.com/ehazlett/interlock)。如果你还没有查看过 interlock,真的是太棒了!!!根据底层应用程序的不同,我们可以使用许多不同的解决方案组合。所以在这里,我们将讨论 Docker Swarm 中的内建高可用性。从这里,你可以使用类似 interlock 的工具,确保你的容器访问没有停机时间。

内建高可用性

Docker Swarm 有两种类型的节点:主节点和成员节点。每种节点都有不同的内建故障保护。我们将首先看一下主节点。

在上一主题中,我们设置了健康检查,以获取有关我们的 Swarm 集群的信息。我们看到我们有一个主节点或主要的 Swarm 主节点和一个副本。Swarm 会通过 TCP 端口 4000 复制所有的集群信息。因此,为了模拟故障,我们将关闭主节点。我的主节点是 swarm-master-01,但你的可能不同。我们将使用已经创建的健康检查来测试故障并观察 Swarm 如何自我处理。我们将执行 vagrant halt swarm-master-01 命令。然后,我们会再次打开浏览器,访问我们的 Consul Web UI,127.0.0.1:9501。正如我们在以下截图中看到的,swarm-master-02 现在是主节点:

内建高可用性

现在,我们将继续讨论在我们的 Swarm 节点高可用性(HA)下的容器重启。从版本 1.1.3 开始,Swarm 提供了一项功能,当原始节点发生故障时,容器会在健康的节点上重新启动。对此有一些规则,例如在你使用过滤规则或链接容器时。想要了解更多相关信息,可以阅读位于 github.com/docker/swarm/tree/master/experimental 的 Docker Swarm 文档。

为了测试这一点,我将暂停托管 Kibana 的节点。我们需要在 kibana 容器中添加一些代码,以便在故障时能够重启。如下截图所示,这些设置被添加到了 env 资源中:

内建高可用性

我们首先需要终止旧的容器,以便添加重启策略。我们可以通过将 ensure 资源设置为 absent 来做到这一点,然后运行 Vagrant 配置 swarm-master-02

一旦 Vagrant 运行完成,我们将其恢复为当前状态,并运行vagrant provision swarm-master-02

对我来说,我的kibana容器在swarm-102上(这可能对你来说会有所不同)。一旦该节点失败,kibana会在健康节点上重启。所以,让我们执行vagrant halt swarm-102。如果我们访问 Consul 的 URL 127.0.0.1:9501,我们应该会看到一些节点和检查的失败,如下图所示:

内置高可用性

如果你等个一分钟左右,你会看到kibana警报恢复,并且容器在另一台服务器上启动。对我来说,kibana恢复在swarm-101上,正如你在下面的截图中看到的那样:

内置高可用性

然后我们可以在浏览器中查看kibana。对我来说,它会在127.0.0.1:8001上:

内置高可用性

连接到 Elasticsearch 后的 Kibana

如你所见,所有的日志都在那;我们的服务发现工作得非常完美,因为容器一旦更换节点,我们的健康检查就变成了绿色。

总结

在这一章中,我们探讨了如何使用 Puppet 将容器环境实现自动化。我们介绍了使用 ELK 的日志解决方案。我们将 Consul 提升到了新的水平,进行了更深入的健康检查,并创建了监控集群的服务。接着我们测试了 Swarm 自带的内置高可用性功能。从我们在第二章《与 Docker Hub 合作》中的初步尝试开始,我们已经走了很长一段路。你已经完全装备好,可以将这里获得的知识应用到实际工作中了。

第九章:第 9 章。现实世界的最佳实践

到目前为止,我们在本书中真的涵盖了很多内容。现在我们到了最后一章。在这里,我们将看看如何结合您学到的所有技能并创建一个可投入生产的模块。为了留下深刻印象,我们将创建一个配置和部署 Kubernetes 作为前端的模块(请注意,将 Kubernetes 作为前端运行有其限制,并不是生产中的最佳选择。模块的 UCP 组件将准备就绪)。由于将涉及大量敏感数据,我们将利用 Hiera。我们将创建一个自定义事实以自动检索 UCP 指纹,并将所有 Kubernetes 组件拆分并使用 interlock 代理我们的 API 服务。我们还将深入研究 UCP,并查看如何将 Docker 守护程序重新指向使用 UCP 集群。服务器架构将与我们在调度器章节中讨论的设计相同。我们将使用三个节点,所有节点都运行 Ubuntu 14.04,并更新内核以支持本地 Docker 网络命名空间。本章我们将涵盖以下主题:

  • Hiera

  • 代码

Hiera

在这个主题中,我们将看看如何使我们的模块无状态。我们将移动所有应用于模块的特定于节点的数据到 Hiera 中(docs.puppetlabs.com/hiera/3.1/)。这背后有两个主要动机。第一个是将任何敏感数据(如密码、密钥等)从我们的模块中移除。第二个是,如果我们将节点特定数据或状态从我们的模块中移除,使它们通用化,我们可以将它们应用于任意数量的主机而不改变模块的逻辑。这为我们提供了将模块发布给 Puppet 社区其他成员的灵活性。

应该放在 Hiera 中的数据

当我们第一次坐下来开始开发一个新模块时,我们应该考虑的一些事情是:我们是否可以使我们的模块与操作系统无关,我们如何在多台机器上运行模块而不需要逻辑更改或额外开发,以及我们如何保护我们的敏感数据?

所有这些问题的答案都是 Hiera。

我们通过参数化我们的类来利用 Hiera。这将允许 Puppet 在目录编译开始时自动查找 Hiera。因此,让我们探讨一些您将放入 Hiera 中的数据示例。

请注意,在我们关于容器调度程序的章节中,我们简要使用了 Hiera。我们设置了以下值:

应该放在 Hiera 中的数据

如你所见,我们设置了诸如版本之类的参数。为什么我们要在 Hiera 中设置版本,而不是直接在模块中设置?例如,如果我们设置了 Consul 的版本,我们可能在生产环境中运行的是 5.0 版本,而 Hashicorp 刚发布了 6.0 版本,通过更改 Hiera 中环境的值来进行切换(有关 Puppet 环境的更多信息,请访问 docs.puppetlabs.com/puppet/latest/reference/environments.html)。在从开发环境到 6.0 的 Hiera 版本中,我们可以运行多个版本的应用程序,而无需进行模块开发。

同样的操作也适用于 IP 地址或 URL。例如,在开发环境的 hieradata 中,你的 Swarm 集群 URL 可以是 dev.swarm.local,而生产环境中则可以是 swarm.local

另一种你需要分离的数据类型是密码/密钥。你不希望开发环境中的密码/密钥与生产环境中的相同。同样,Hiera 允许你对这些数据进行模糊处理。

我们可以通过 Puppet 支持的 eyaml 进一步保护这些数据。这使你可以使用密钥加密 Hiera 的 .yaml 文件。因此,当文件被提交到源代码管理时,它们会被加密。这有助于防止数据泄漏。有关 eyaml 的更多信息,请访问 puppetlabs.com/blog/encrypt-your-data-using-hiera-eyaml

如你所见,Hiera 为你提供了将数据从模块转移到 Hiera 以外部化配置的灵活性,从而使模块保持无状态。

Hiera 的小贴士和技巧

在使用 Hiera 时,有一些非常实用的小贴士可以参考。Puppet 允许你调用函数、查找项和查询事实。掌握这些功能会非常有用。如果这是你第一次接触,建议在继续本章节之前阅读 docs.puppetlabs.com/hiera/3.1/variables.html 中的文档。

那么,让我们看一个从 Hiera 查找事实的示例。首先,为什么要这么做?一个非常好的理由是 IP 地址查找。如果你有一个类应用到三个节点,并且需要像在 consul 模块中那样宣传一个 IP 地址,将 IP 地址设置在 Hiera 中是行不通的,因为每台机器的 IP 地址会不同。我们创建一个名为 node.yaml 的文件并将 IP 地址添加到那里。问题是,现在我们将有多个 Hiera 文件。每次 Puppet 加载目录时,它都会查找所有 Hiera 文件,检查是否有值发生变化。文件越多,主服务器的负载就越大,Puppet 执行的速度也会变慢。因此,我们可以告诉 Hiera 查找我们想要宣传的事实和接口。以下是代码的示例:

ucpconfig::ucp_host_address: "%{::ipaddress_eth1}"

唯一需要注意的是,如果我们从模块中调用这个事实,我们将使用完全限定的名称$::ipaddress_eth1。不幸的是,Hiera 不支持使用这个名称。所以我们可以使用::ipaddress_eth1事实的简短名称。

代码

现在我们已经很好地理解了如何使我们的模块无状态,让我们开始编写代码吧。我们将把编码分成两部分。在第一部分中,我们将编写安装和配置 Docker UCP 的模块。最后的主题将是运行 Kubernetes 作为前端。

UCP

我们需要做的第一件事是为本章创建一个新的 Vagrant 仓库。到现在为止,我们应该已经掌握了如何创建一个新的 Vagrant 仓库。一旦创建好仓库,我们将创建一个新的模块,名为<AUTHOR>-ucpconfig,并将其移动到 Vagrant 仓库根目录下的modules目录中。我们将首先通过添加下图所示的代码来设置servers.yml文件:

UCP

正如你所看到的,我们正在设置三台服务器,其中ucp-01将成为集群的主节点,另外两个节点将加入集群。我们将添加两个文件:config.jsondocker_subscription.lic。其中,config.json将包含 Docker Hub 的授权密钥,docker_subscription.lic将包含我们 UCP 的试用许可证。请注意,我们在容器调度章节中已经讲解了这两个文件。如果你在设置这些文件时遇到问题,请参考该章节。

接下来我们要查看的文件是 puppetfile。我们需要将下图所示的代码添加到该文件中:

UCP

现在我们已经设置好了 Vagrant 仓库,可以继续进行我们的模块开发了。我们将需要创建四个文件:config.ppmaster.ppnode.ppparams.pp

我们首先要查看的文件是params.pp。我想先编写这个文件,因为它为模块的其余部分奠定了良好的基础。我们这样做如下:

UCP

正如你所看到的,我们正在设置所有的参数。我们将深入研究每一个参数,并在将其应用到类时了解它的含义。这样,我们就能理解所设置值的背景。你可能注意到,我们将很多变量设置为空字符串。这是因为我们将使用 Hiera 来查找这些值。我也硬编码了一些值,比如 UCP 版本为 1.0.0。这只是一个默认值,如果 Hiera 中没有相应的值,就会使用这个值。然而,我们将把 UCP 的版本设置为 1.0.3。预期的行为是 Hiera 中的值会优先级更高。你会注意到我们引用了一个事实,即$::ucp_fingerprint。这是一个自定义的事实。它将自动传递 UCP 的指纹。如果你记得,在容器调度章节中,我们必须构建ucp-01来获取指纹并将其添加到 Hiera,供其他节点使用。通过自定义事实,我们将自动化这个过程。

要创建一个自定义事实,首先我们需要在模块的根目录下创建一个 lib 文件夹。在该文件夹下,我们将创建一个名为 facter 的文件夹。在该文件夹中,我们将创建一个名为 ucp_fingerprint.rb 的文件。在编写自定义事实时,文件名需要与事实的名称相同。我们将添加到自定义事实中的代码如下:

UCP

在这段代码中,你可以看到我们添加了一个 bash 命令来查询我们的 UCP master,以获取指纹。我在这个事实中硬编码了主机的 IP 地址。在我们的环境中,我们会编写逻辑来使其更加灵活,以便允许有多个环境、主机名等。关于自定义事实,最重要的是理解命令本身。

现在我们将返回到我们的init.pp文件,内容如下:

UCP

你首先可以看到的是,我们在类的顶部声明了所有的变量。这是 Puppet 查找 Hiera 并匹配我们声明的任何变量的地方。模块中的第一段代码将设置我们的 Docker 守护进程。我们将添加额外的配置来告诉守护进程它可以在哪里找到 Docker 原生网络的后端。然后,我们将在$::hostname事实上声明一个 case 语句。你会注意到,我们为主机名设置了一个参数。这是为了使我们的模块更加可移植。在 case 语句中,如果主机是 master,你会看到我们将使用我们的 Consul 容器作为 Docker 网络的后端。接着,我们将按顺序执行应用于节点的类。

在 case 语句中的下一个代码块中,我们为 $::hostname 事实声明了 $ucp_deploy_node 变量。我们将使用此节点来部署 Kubernetes。稍后我们会回到这个话题。我们 case 语句中的最后一段代码是 catch alldefault。如果 Puppet 无法在我们声明的变量中找到 $::hostname 的事实,它将应用这些类。

现在我们将继续我们的 master.pp 文件,内容如下:

UCP

你首先会注意到的是,我们再次在这个类的顶部声明了变量。这样做是因为我们声明了很多参数,这使得我们的模块更具可读性。在复杂的模块中这样做是必须的,我真的推荐你遵循这种做法。当涉及到调试时,这会让你的工作轻松很多。正如你在下面的截图中看到的,我们将参数绑定回我们的init.pp文件:

UCP

从前面的代码可以看出,我们在模块中设置的值的类型非常少。

现在我们将继续处理我们的 node.pp 文件,内容如下:

UCP

正如你所见,我们在类的顶部声明了参数,再次将它们与我们的init.pp文件关联。我们已经声明了大部分的值,因为我们将使用 Hiera。

现在,我们将继续我们的config.pp文件,如下所示:

UCP

在这个类中,我们将进行一些对集群至关重要的配置。所以,我们将逐个代码块地讲解。让我们先看看第一个:

UCP

在这个代码块中,我们将声明我们的变量,就像在本模块的所有类中一样。有一点我还没有提到,那就是我们仅声明应用于其类的变量,并不是声明所有的参数。现在让我们看看下一个代码块:

UCP

在这段代码中,我们将传递一个包数组,这些包是我们需要用来通过curl连接到 UCP 主节点并获取 SSL 证书包的,以便节点之间的 TLS 通信。现在,让我们看看第三个代码块:

UCP

在这段代码中,我们将创建一个名为get_ca.sh.erb的脚本,从主节点获取证书包。首先,我们需要在模块的根目录下创建一个templates文件夹,然后在该文件夹中创建我们的get_ca.sh.erb文件。我们将向该文件中添加以下代码:

UCP

正如你在脚本中所看到的,我们需要创建一个auth令牌并将其传递给 API。Puppet 本身并不擅长处理这些任务,因为我们在curl命令中使用了变量。只要我们确保其幂等性,创建一个模板文件并运行exec函数是可以的。在下一个代码块中,我们将这样做:

UCP

在这段代码中,我们将运行之前的脚本。你可以看到,我们设置了commandpathcwd当前工作目录)这几个参数。下一个资源是creates,它告诉 Puppet 当前工作目录中应该有一个名为ca.pem的文件。如果 Puppet 发现该文件不存在,它将执行exec,如果文件存在,Puppet 将什么也不做。这将使我们的exec具备幂等性。

在下一个代码块中,我们将在/etc/profile.d中创建一个文件,该文件将把每个节点上的 Docker 守护进程指向主节点的 IP 地址,从而使我们能够在集群中调度容器:

UCP

现在,让我们在templates目录中创建一个docker.sh文件。在该文件中,我们将放入以下代码:

UCP

在这个文件中,我们告诉 Docker 守护进程使用 TLS,设置我们之前在类中获取的密钥文件的位置。我们设置的最后一项是 Docker 主机,它将指向 UCP 主节点。

这个类中的最后一段代码应该非常熟悉,因为我们正在设置一个 Docker 网络:

UCP

现在我们可以继续处理存放所有数据的文件——我们的 Hiera 文件。该文件位于 hieradata 文件夹中,该文件夹位于 Vagrant 仓库的根目录。以下截图展示了 Hiera 文件中的各种数据:

UCP

那么,让我们列出我们在这里定义的所有数据。我们将 UCP 主节点定义为 ucp-01,我们的部署节点为 ucp-03(这是我们从中部署 Kubernetes 的节点)。主节点的 UCP URL 为 https://172.17.10.101。当我们将节点连接到主节点时,或者获取我们的 ca 包和 UCP 指纹时,都将使用该 URL。我们将用户名和密码保持为 adminorca。我们将使用 UCP 版本 1.0.3。然后,我们将使用之前在本书中讨论过的 Hiera fact 查找来设置 UCP 的主机地址和备用名称。

下一个参数将告诉 UCP 我们将使用内部 CA。接下来,我们将设置调度器使用 spread,定义 Swarm 和控制器的端口,告诉 UCP 保留证书,并发送 UCP 许可证文件的位置。接下来,我们将设置一些 Consul 的日期信息,例如主节点 IP、要广告的接口、要使用的镜像,以及在启动 Consul 集群时预期的节点数量。最后,我们将设置 Docker 守护进程使用的变量,例如我们的 Docker 网络名称 swarm-private、网络驱动程序 overlay、证书路径以及我们在 docker.sh 文件中设置的 Docker 主机,该文件位于 /etc/profile.d/

如你所见,我们在 Hiera 中有大量数据。然而,正如我们之前讨论的,某些数据可能会根据不同的环境发生变化。因此,正如你所看到的,将模块设为无状态并将数据抽象到 Hiera 中,尤其是当你希望编写易于扩展的模块时,是非常有益的。

接下来,我们将以下代码添加到我们的清单文件 default.pp 中,该文件位于 Vagrant 仓库根目录的 manifests 文件夹中。以下代码定义了我们的节点定义:

UCP

然后,我们可以打开终端并将目录切换到 Vagrant 仓库的根目录。接下来,我们将发出 vagrant up 命令来运行 Vagrant。当三个虚拟机构建完成后,你应该会看到以下终端输出:

UCP

然后,我们可以登录到网址 https://127.0.0.1:8443

UCP

我们将使用 admin 用户名和 orca 密码进行登录。在以下截图中,我们可以看到我们的集群已启动并处于健康状态:

UCP

Kubernetes

既然我想以一个精彩的方式结束,我们现在来做一些很酷的事情。我想部署一个有多个容器的应用程序,我们可以使用 interlock 来展示应用路由/负载均衡。有什么比 Kubernetes 更好的应用程序呢?正如我之前提到的,像这样运行 Kubernetes 有一些局限性,它仅限于实验室使用。我们可以从中学到的技能并将其应用到我们的 Puppet 模块中,就是应用路由/负载均衡。在我们上一节中,我们在 init.pp 文件的 case 语句中设置了 $ucp_deploy_node 参数。在那个特定的代码块中,我们有一个叫做 compose.pp 的类。这个类将会在我们的 UCP 集群中部署 Kubernetes。让我们看一下这个文件:

Kubernetes

第一个资源只是创建一个名为 kubernetes 的目录。我们将在这里放置我们的 Docker Compose 文件。现在,你会注意到我们运行 Docker Compose 的方式有些不同。使用 exec?为什么我们要用 exec,而不是使用一个已经验证过的、完全可靠的 provider 呢?我们在这种情况下使用 exec 的原因是,我们在上次 Puppet 运行时更改了 $PATH。我是什么意思呢?还记得我们添加到 /etc/profile.d/ 目录的文件吗?它更改了 DOCKER_HOST 指向的 shell 设置。这只会在下一次 Puppet 运行时生效,所以 Docker 守护进程不会指向集群。这就意味着所有的 Kubernetes 容器都会在同一台主机上启动。这将导致清单失败,因为我们会遇到两个容器使用 8080 端口时的冲突。现在,这只会在我们一次性运行整个模块时生效,也就是作为一个单独的清单。

现在,让我们看一下我们的 Docker Compose 文件:

Kubernetes

在本书中,我一直强调我更喜欢使用 Docker Compose 方法来部署我的容器应用程序。这份 Docker Compose 文件正是这一方法的完美示例。我们在这个 Compose 文件中有七个容器。我觉得这样很简单,因为所有的逻辑都摆在我们面前,我们只需要写最少的代码。现在,让我们看看代码。首先不同的是,我们声明了 version 2。这是因为 Docker Compose 的 1.6.2 版本已发布(github.com/docker/compose/releases/tag/1.6.2)。因此,为了利用新特性,我们需要声明我们想使用 version 2

我们声明的第一个容器是 Interlock。我们将使用 Interlock 作为应用程序路由器,它会向 Kubernetes API 发起服务器请求。对于这个容器,我们将端口 44380808443 转发到主机。然后,我们会将主机的 /etc/docker 映射到容器中。这样做的原因是我们需要密钥来连接到 Swarm API。所以,我们将利用本章早些时候安装的 bundle。

在命令资源中,我们将告诉 Interlock 哪里可以找到证书、Swarm URL,并且最后,告诉它我们要使用 haproxy。接下来,我们将把这个容器添加到我们的覆盖网络 swarm-private。接下来,在环境资源中,我们将设置一个约束,并告诉 Compose 只在 ucp-03 上运行 Interlock。我们这样做是为了避免与 Kubernetes API 服务的端口冲突。下一个容器是 etcd。自从我们在调度器章节中配置 Kubernetes 后,关于这一部分没有太大变化,因此我们将跳过。下一个容器是 kubernetes API 服务。需要特别注意的是,在环境资源中我们声明了 - INTERLOCK_DATA={"hostname":"kubernetes","domain":"ucp-demo.local"}。这是 Interlock 在发送请求到 API 时将查找的 URL。

这是我们运行 Kubernetes 的主要原因,目的是掌握应用程序路由的技能。因此,我不会继续讲解其余的容器。Kubernetes 在kubernetes.io/上有详细的文档。我建议你阅读一下相关特性,并探索 Kubernetes——它非常强大,有很多东西可以学习。

现在我们已经有了所有的代码。让我们运行它吧!

为了看到完整的构建过程,我们将打开终端并将目录切换到我们 Vagrant 仓库的根目录。如果你已经根据之前的章节构建了服务器,可以运行 vagrant destroy -f && vagrant up;如果没有,直接运行 vagrant up 命令即可。一旦 Puppet 执行完毕,我们的终端应该会显示以下输出:

Kubernetes

然后我们可以登录到我们的 Web UI,地址是 https://127.0.0.1:8443

Kubernetes

然后,我们将使用 admin 用户名和 orca 密码登录。登录成功后,我们应该能看到如下截图:

Kubernetes

你会注意到现在我们有了一个应用程序。如果我们双击该应用程序,就可以看到 Kubernetes 已经启动并运行:

Kubernetes

你会注意到,由于我们在 Docker Compose 文件中的环境设置,我们的容器被分布在 ucp-02ucp-03 上。需要注意的一点是,Interlock 在 ucp-03 上,Kubernetes API 服务则在 ucp-02 上。

现在我们已经成功构建了一切,我们需要登录到 ucp-03 并下载 kubectl 客户端。我们可以通过发出以下命令来实现:

$'wget https://storage.googleapis.com/kubernetes-release/release/v1.1.8/bin/linux/amd64/kubectl'

那么,让我们登录到ucp-03,并从我们的 Vagrant 仓库根目录执行vagrant ssh ucp-03命令。接着,我们将切换到 root(sudo -i)。然后,我们将执行wget命令,具体见下图:

Kubernetes

接下来,我们将通过执行以下命令使文件具有可执行权限:

$'chmod +x kubectl'

现在请记住,我们在环境设置中为- INTERLOCK_DATA={"hostname":"kubernetes","domain":"ucp-demo.local"} API 容器设置了一个 URL。我们需要在主机文件中设置该值。所以,使用你喜欢的编辑文件方式,如 vim、nano、sed 等,添加172.17.10.103 kubernetes.ucp-demo.local。该 IP 地址指向ucp-03,因为 Interlock 就在此处运行。

现在我们准备好测试我们的集群了。我们将通过执行./kubectl -s kubernetes.ucp-demo.local get nodes命令来实现。执行后,我们应该得到如下输出:

Kubernetes

如你所见,一切都已正常运行。如果我们回到 UCP 控制台,你会看到 API 服务器正在ucp-02上运行(IP 地址为172.17.10.102)。那么,我们是如何做到这一点的呢?Interlock 正在处理8080上的 HTTP 请求,并将其路由到我们的 API 服务器。这告诉我们应用路由已经就绪。这个例子非常基础,但你应该尝试一下,因为使用 Interlock 你可以设计出非常流畅的解决方案。

总结

在本章中,我们重点讲解了如何构建一个可部署的 Puppet 模块。使用 Hiera 以及将数据与模块逻辑分离的做法,不仅适用于部署容器的模块,也适用于你编写的任何 Puppet 模块。在你的 Puppet 生涯中,总有一天你会开源一个模块或为已开源的模块做贡献。本章中学到的内容在这两种情况中都将非常有价值。最后,我们做了一些有趣的事,将 Kubernetes 作为 UCP 的前端进行部署。通过这个过程,我们还了解了应用路由/负载均衡。显然,这是一项非常重要的技能,尤其是当你的容器环境不断扩展时,避免端口冲突等问题。

posted @ 2025-06-29 10:39  绝不原创的飞龙  阅读(20)  评论(0)    收藏  举报