Docker-研讨会(全)

Docker 研讨会(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

关于本书

Docker 容器是高度可扩展软件系统的未来,并且可以轻松创建、运行和部署应用程序。

如果您希望利用它们而不被技术细节所压倒,那么请将The Docker Workshop添加到您的阅读列表中!

通过本书,您将能够快速掌握容器和 Docker 的知识,并通过互动活动使用它们。

这个研讨会从 Docker 容器的概述开始,让您了解它们的工作原理。您将运行第三方 Docker 镜像,并使用 Dockerfiles 和多阶段 Dockerfiles 创建自己的镜像。接下来,您将为 Docker 镜像创建环境,并通过持续集成加快部署过程。在前进的过程中,您将涉足有趣的主题,并学习如何使用 Docker Swarm 实现生产就绪环境。为了进一步保护 Docker 镜像,并确保生产环境以最大容量运行,您将应用最佳实践。随后,您将学会成功将 Docker 容器从开发转移到测试,然后进入生产。在此过程中,您将学习如何解决问题,清理资源瓶颈,并优化服务的性能。

通过本 Docker 书籍,您将精通 Docker 基础知识,并能够在实际用例中使用 Docker 容器。

受众

如果您是开发人员或 Docker 初学者,希望在实践中了解 Docker 容器,那么这本书是理想的指南。在开始阅读本 Docker 容器书籍之前,需要具备运行命令行和了解 IntelliJ、Atom 或 VSCode 编辑器的知识。

关于章节

第一章运行我的第一个 Docker 容器,从 Docker 的基本介绍开始,讨论了背景架构、生态系统和基本 Docker 命令。

第二章使用 Dockerfiles 入门,向您介绍了 Dockerfile、其背景以及如何使用 Dockerfile 创建和运行您的第一个 Docker 容器。

第三章管理您的 Docker 镜像,提供了有关 Docker 镜像、镜像存储库和发布您自己的镜像的更多细节。

第四章多阶段 Dockerfiles,向您展示如何进一步扩展您的 Dockerfile,在项目中使用多阶段 Dockerfile。

第五章使用 Docker Compose 组合环境,介绍了 Docker Compose 以及如何使用 docker-compose 文件生成整个工作环境。

第六章介绍 Docker 网络,解释了为什么在 Docker 中需要以不同的方式处理网络,以及如何实现服务和主机系统之间的通信。

第七章Docker 存储,详细介绍了在您的 Docker 容器和环境中利用存储的方法。

第八章CI/CD 流水线,描述了使用 Jenkins 创建持续集成/持续部署流水线。

第九章Docker Swarm,介绍了使用 Swarm 编排您的 Docker 服务。

第十章Kubernetes,将您的编排提升到下一个级别,向您介绍了 Kubernetes 以及如何在基本集群中部署您的容器镜像。

第十一章Docker 安全,指导您如何使您的 Docker 镜像和容器尽可能安全,提供了在使用容器时减少风险的方法。

第十二章最佳实践,提供了关于如何确保您的容器尽可能高效运行的信息。

第十三章监控 Docker 指标,涵盖了正在运行的 Docker 容器的指标收集以及如何实现 Prometheus 来帮助监控这些指标。

第十四章收集容器日志,教你如何使用 Splunk 从正在运行的 Docker 容器中收集日志,这将允许你聚合、搜索和显示日志详细信息。

第十五章使用插件扩展 Docker,介绍了通过创建自己的插件来进一步扩展 Docker 的方法,以便与您的 Docker 应用程序一起使用。

注意

此外,本书的免费互动版本还附带了一个额外的章节,Docker 的未来展望。您可以在以下网址找到它:https://courses.packtpub.com/。

约定

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入显示如下:

“在当前工作目录中创建一个名为docker-compose.yml的文件。”

代码块、终端命令或创建 YAML 文件的文本设置如下:

docker build -t test . 

新的重要单词显示为:“Docker 提供了一个在线存储库来存储您的镜像,称为Docker Hub。”

屏幕上显示的文字(例如菜单或对话框中的文字)会在文本中以这种方式出现:“在左侧边栏上,点击设置,然后点击用户。”

代码片段的关键部分如下所示:

1 FROM alpine
2 
3 RUN apk update
4 RUN apk add wget curl
5
6 RUN wget -O test.txt https://github.com/PacktWorkshops/   The-Docker-Workshop/raw/master/Chapter3/Exercise3.02/     100MB.bin
7
8 CMD mkdir /var/www/
9 CMD mkdir /var/www/html/

长代码片段被截断,并且在截断的代码顶部放置了 GitHub 上代码文件的相应名称。完整代码的永久链接放置在代码片段下方。它应该如下所示:

Dockerfile

7 # create root directory for our project in the container
7 RUN mkdir /service
9 RUN mkdir /service/static
10
11# Set the working directory to /service
12 WORKDIR /service

此示例的完整代码可以在packt.live/2E9OErr找到。

设置您的环境

在我们详细探讨本书之前,我们需要设置特定的软件和工具。在接下来的部分中,我们将看到如何做到这一点。

硬件要求

您至少需要一台支持虚拟化的双核 CPU,4GB 内存和 20GB 的可用磁盘空间。

操作系统要求

推荐的操作系统是 Ubuntu 20.04 LTS。如果您使用 Mac 或 Windows,您应该能够运行本书中的命令,但不能保证它们都能正常工作。我们建议您在系统上安装虚拟化环境,例如 VirtualBox 或 VMware。我们还在本节末尾提供了有关如何在 Windows 系统上设置双引导以使用 Ubuntu 的说明。

安装和设置

本节列出了 Docker 和 Git 的安装说明,因为它们是本次研讨会的主要要求。任何其他使用的软件的安装说明将在涵盖它的特定章节中提供。由于我们推荐使用 Ubuntu,我们将使用 APT 软件包管理器在 Ubuntu 中安装大部分所需的软件。

更新您的软件包列表

在 Ubuntu 上使用 APT 安装任何软件包之前,请确保您的软件包是最新的。使用以下命令:

sudo apt update

此外,您可以使用以下命令选择升级计算机上的任何可升级软件包:

sudo apt upgrade

安装 Git

本研讨会的代码包可以在我们的 GitHub 存储库上找到。您可以使用 Git 克隆存储库以获取所有代码文件。

使用以下命令在 Ubuntu 上安装 Git:

sudo apt install git-all

Docker

Docker 是本研讨会使用的默认容器化引擎。随着您阅读本书的章节,您将更多地了解该应用程序。

使用以下命令在 Ubuntu 上安装 Docker:

sudo apt install docker.io -y

安装完成后,您需要确保 Docker 守护程序已启动并在系统上运行。使用以下命令执行此操作,确保您以sudo命令作为提升的用户运行此命令:

sudo systemctl start docker

确保 Docker 守护程序在下次启动系统时启动。运行以下命令,以确保 Docker 在您安装它的系统上每次停止或重新启动时启动:

sudo systemctl enable docker

使用docker命令和--version选项验证您安装的 Docker 版本。运行以下命令:

docker –version

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

Docker version 19.03.8, build afacb8b7f0

如果您不是以 root 用户身份执行命令,很有可能无法运行所需的大部分命令。如果运行以下示例命令,可能会遇到连接到 Docker 守护程序的访问问题:

docker ps

如果您以没有提升权限的用户身份运行该命令,可能会看到以下错误:

Got permission denied while trying to connect to the 
Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/containers/json: 
dial unix /var/run/docker.sock: connect: permission denied

要解决此问题,请将当前用户添加到安装应用程序时创建的 Docker 组中。使用以下命令在您的系统上执行此操作:

sudo usermod -aG docker ${USER}

要激活这些更改,您需要注销系统,然后重新登录,或执行以下命令为当前用户创建一个新会话:

sudo su ${USER}

再次运行docker ps命令,以确保您的更改成功:

docker ps

如果一切正常,您应该看到类似以下的输出,显示您的系统上没有运行 Docker 容器:

CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES

为 Windows 用户双引导 Ubuntu

在本节中,您将找到有关在运行 Windows 的情况下如何双引导 Ubuntu 的说明。

注意

在安装任何操作系统之前,强烈建议您备份系统状态和所有数据。

调整分区大小

如果您的计算机上安装了 Windows,那么您的硬盘很可能已完全被使用,即所有可用空间都已分区并格式化。您需要在硬盘上有一些未分配的空间,因此请调整具有大量可用空间的分区的大小,以为 Ubuntu 分区腾出空间。

  1. 打开“计算机管理”实用程序。按下Win + R,输入compmgmt.msc图 1.0:Windows 上的计算机管理实用程序

图 1.0:Windows 上的计算机管理实用程序

  1. 在左侧窗格中,转到存储 > 磁盘管理选项,如下截图所示:图 0.2:磁盘管理

图 0.2:磁盘管理

您将在屏幕下半部分看到所有分区的摘要。您还可以看到与所有分区关联的驱动器号以及有关 Windows 引导驱动器的信息。如果您有一个有大量空闲空间(20 GB +)且既不是引导驱动器(C:)也不是恢复分区,也不是可扩展固件接口EFI)系统分区的分区,则这将是理想的选择。如果没有这样的分区,那么您可以调整C:驱动器的大小。

  1. 在本示例中,您将选择D:驱动器。右键单击任何分区并打开属性以检查可用的空闲空间:图 0.3:检查 D:驱动器的属性

图 0.3:检查 D:驱动器的属性

现在,在调整分区大小之前,您需要确保文件系统或任何硬件故障没有错误。在 Windows 上使用chkdsk实用程序进行此操作。

  1. 按下Win + R打开命令提示符,然后输入cmd.exe。现在运行以下命令:
chkdsk D: /f

用要使用的驱动器号替换它。您应该看到类似以下的响应:

图 0.4:扫描驱动器以查找任何文件系统错误

图 0.4:扫描驱动器以查找任何文件系统错误

请注意,在图 0.4中,Windows 报告它已扫描文件系统并未发现问题。如果您的情况遇到任何问题,您应该先解决这些问题,以防止数据丢失。

  1. 现在,返回到计算机管理窗口,右键单击所需的驱动器,然后单击收缩卷,如下截图所示:图 0.5:打开收缩卷对话框

图 0.5:打开收缩卷对话框

  1. 在提示窗口中,输入要收缩的空间量。在此示例中,您正在通过收缩D:驱动器来清除大约 25 GB 的磁盘空间:图 0.6:通过收缩现有卷清除 25 GB

图 0.6:通过收缩现有卷清除 25 GB

收缩驱动器后,您应该能够在驱动器上看到未分配的空间:

图 0.7:收缩卷后的未分配空间

图 0.7:缩小卷后的未分配空间

现在,您已经准备好安装 Ubuntu 了。但首先,您需要下载它并创建一个可启动的 USB,这是最方便的安装介质之一。

创建可启动的 Ubuntu USB 驱动器

您需要一个至少容量为 4GB 的闪存驱动器来创建一个可启动的 USB 驱动器。请注意,其中的所有数据将被删除:

  1. releases.ubuntu.com/20.04/下载 Ubuntu 桌面版的 ISO 镜像。

  2. 接下来,将 ISO 镜像刻录到 USB 闪存盘并创建一个可启动的 USB 驱动器。有许多可用的工具,您可以使用其中任何一个。在本例中,您将使用免费开源的 Rufus。您可以从www.fosshub.com/Rufus.html获取它。

  3. 安装好 Rufus 后,插入您的 USB 闪存盘并打开 Rufus。确保选择了正确的“设备”选项,如图 0.8所示。

  4. 按“启动选择”下的“选择”按钮,然后打开您下载的 Ubuntu 20.04 镜像。

  5. “分区方案”的选择将取决于您的 BIOS 和磁盘驱动器的配置。对于大多数现代系统来说,GPT将是最佳选择,而MBR将兼容较旧的系统:图 0.8:Rufus 配置

图 0.8:Rufus 配置

  1. 您可以将所有其他选项保留为默认值,然后按“开始”。完成后,关闭 Rufus。现在您已经有一个可启动的 USB 驱动器可以安装 Ubuntu 了。

安装 Ubuntu

现在,使用可启动的 USB 驱动器来安装 Ubuntu:

  1. 要安装 Ubuntu,使用刚刚创建的可启动安装介质进行引导。在大多数情况下,您只需在启动计算机时插入 USB 驱动器即可。如果您没有自动引导到 Ubuntu 设置界面,请进入 BIOS 设置,并确保您的 USB 设备处于最高的启动优先级,并且安全启动已关闭。通常,在 POST 检查期间,BIOS 设置的输入说明通常显示在启动画面(启动计算机时显示您的 PC 制造商标志的屏幕)上。您也可能在启动时有进入启动菜单的选项。通常情况下,您必须在 PC 启动时按住DeleteF1F2F12或其他一些键。这取决于您主板的 BIOS。

你应该看到一个带有“尝试 Ubuntu”或“安装 Ubuntu”选项的屏幕。如果你没有看到这个屏幕,而是看到一个以“最小的 BASH 类似行编辑支持…”开头的消息的 shell,那么很可能在下载 ISO 文件或创建可启动的 USB 驱动器时可能发生了一些数据损坏。通过计算你下载文件的MD5SHA1SHA256哈希值来检查下载的 ISO 文件的完整性,并将其与你可以在 Ubuntu 下载页面上找到的文件MD5SUMSSHA1SUMSSHA256SUMS中的值进行比较。然后,重复上一节中的步骤来重新格式化和重新创建可启动的 USB 驱动器。

如果你已经在 BIOS 中将最高启动优先级设置为正确的 USB 设备,但仍然无法使用 USB 设备启动(你的系统可能会忽略它并启动到 Windows),那么你很可能正在处理以下一个或两个问题:

  • USB 驱动器没有正确配置为可识别的可启动设备,或者 GRUB 引导加载程序没有正确设置。验证你下载的镜像的完整性并重新创建可启动的 USB 驱动器应该在大多数情况下解决这个问题。

  • 你选择了错误的“分区方案”选项来配置你的系统。尝试另一个选项并重新创建 USB 驱动器。

  1. 一旦你使用 USB 驱动器启动你的机器,选择“安装 Ubuntu”。

  2. 选择你想要的语言,然后点击“继续”。

  3. 在下一个屏幕上,选择适当的键盘布局,并继续到下一个屏幕。

  4. 在下一个屏幕上,选择“正常安装”选项。

勾选“在安装 Ubuntu 时下载更新”和“安装用于图形和 Wi-Fi 硬件以及其他媒体格式的第三方软件”选项。

然后,继续到下一个屏幕。

  1. 在下一个屏幕上,选择“在 Windows 引导管理器旁边安装 Ubuntu”,然后点击“立即安装”。你会看到一个提示,描述 Ubuntu 将对你的系统进行的更改,比如将要创建的新分区。确认更改并继续到下一个屏幕。

  2. 在下一个屏幕上,选择你的地区并点击“继续”。

  3. 在下一个屏幕上,设置你的名字(可选)、用户名、计算机名和密码,然后点击“继续”。

安装现在应该开始了。这将需要一些时间,具体取决于您的系统配置。安装完成后,您将收到提示重新启动计算机。拔掉您的 USB 驱动器,然后点击“立即重启”。

如果您忘记拔掉 USB 驱动器,可能会重新启动 Ubuntu 安装。在这种情况下,只需退出设置。如果已启动 Ubuntu 的实时实例,请重新启动您的机器。这次记得拔掉 USB 驱动器。

如果重新启动后,您直接进入 Windows,没有选择操作系统的选项,可能的问题是 Ubuntu 安装的 GRUB 引导加载程序没有优先于 Windows 引导加载程序。在某些系统中,硬盘上引导加载程序的优先级/优先级是在 BIOS 中设置的。您需要在 BIOS 设置菜单中找到适当的设置。它可能被命名为类似于“UEFI 硬盘驱动器优先级”的东西。确保将GRUB/Ubuntu设置为最高优先级。

安装任何操作系统后,确保所有硬件组件都按预期工作是个好主意。

其他要求

Docker Hub 账户:您可以在hub.docker.com/免费创建 Docker 账户。

访问代码文件

您可以在我们的 GitHub 仓库中找到这个研讨会的完整代码文件,网址为packt.live/2RC99QI

安装 Git 后,您可以使用以下命令克隆存储库:

git clone https://github.com/PacktWorkshops/The-Docker-Workshop
cd The-Docker-Workshop

如果您在安装过程中遇到任何问题或有任何疑问,请发送电子邮件至workshops@packt.com

第一章:运行我的第一个 Docker 容器

概述

在本章中,您将学习 Docker 和容器化的基础知识,并探索将传统的多层应用程序迁移到快速可靠的容器化基础设施的好处。通过本章的学习,您将对运行容器化应用程序的好处有深入的了解,以及使用docker run命令运行容器的基础知识。本章不仅将向您介绍 Docker 的基础知识,还将为您提供对本次研讨会中将要构建的 Docker 概念的扎实理解。

介绍

近年来,各行各业的技术创新迅速增加了软件产品交付的速度。由于技术趋势,如敏捷开发(一种快速编写软件的方法)和持续集成管道,使软件的快速交付成为可能,运营人员最近一直在努力快速构建基础设施,以满足不断增长的需求。为了跟上发展,许多组织选择迁移到云基础设施。

云基础设施提供了托管的虚拟化、网络和存储解决方案,可以按需使用。这些提供商允许任何组织或个人注册并获得传统上需要大量空间和昂贵硬件才能在现场或数据中心实施的基础设施。云提供商,如亚马逊网络服务和谷歌云平台,提供易于使用的 API,允许几乎立即创建大量的虚拟机(或 VMs)。

将基础设施部署到云端为组织面临的许多传统基础设施解决了难题,但也带来了与在规模上运行这些服务相关的管理成本的额外问题。公司如何管理全天候运行昂贵服务器的持续月度和年度支出?

虚拟机通过利用 hypervisors 在较大的硬件之上创建较小的服务器,从而革新了基础设施。虚拟化的缺点在于运行虚拟机的资源密集程度。虚拟机本身看起来、行为和感觉都像真正的裸金属硬件,因为 hypervisors(如 Zen、KVM 和 VMWare)分配资源来引导和管理整个操作系统镜像。与虚拟机相关的专用资源使其变得庞大且难以管理。在本地 hypervisor 和云之间迁移虚拟机可能意味着每个虚拟机移动数百 GB 的数据。

为了提供更高程度的自动化,更好地利用计算密度,并优化他们的云存在,公司发现自己朝着容器化和微服务架构的方向迈进作为解决方案。容器提供了进程级别的隔离,或者在主机操作系统内核的隔离部分内运行软件服务。与运行整个操作系统内核以提供隔离不同,容器可以共享主机操作系统的内核来运行多个软件应用程序。这是通过 Linux 内核中的控制组(或 cgroups)和命名空间隔离等功能实现的。在单个虚拟机或裸金属机器上,用户可能会运行数百个容器,这些容器在单个主机操作系统上运行各自的软件应用程序实例。

这与传统的虚拟机架构形成鲜明对比。通常,当我们部署虚拟机时,我们目的是让该机器运行单个服务器或一小部分服务。这会导致宝贵的 CPU 周期的浪费,这些周期本可以分配给其他任务并提供其他请求。理论上,我们可以通过在单个虚拟机上安装多个服务来解决这个困境。然而,这可能会在关于哪台机器运行哪项服务方面造成极大的混乱。它还将多个软件安装和后端依赖项的托管权放在单个操作系统中。

容器化的微服务方法通过允许容器运行时在主机操作系统上调度和运行容器来解决了这个问题。容器运行时不关心容器内运行的是什么应用程序,而是关心容器是否存在,并且可以在主机操作系统上下载和执行。容器内运行的应用程序是 Go web API、简单的 Python 脚本还是传统的 Cobol 应用程序都无关紧要。由于容器是以标准格式存在的,容器运行时将下载容器镜像并在其中执行软件。在本书中,我们将学习 Docker 容器运行时,并学习在本地和规模化运行容器的基础知识。

Docker 是一个容器运行时,于 2013 年开发,旨在利用 Linux 内核的进程隔离功能。与其他容器运行时实现不同的是,Docker 开发了一个系统,不仅可以运行容器,还可以构建和推送容器到容器存储库。这一创新引领了容器不可变性的概念——只有在软件发生变化时才通过构建和推送容器的新版本来改变容器。

如下图所示(图 1.1),我们在两个 Docker 服务器上部署了一系列容器化应用程序。在两个服务器实例之间,部署了七个容器化应用程序。每个容器都托管着自己所需的二进制文件、库和自包含的依赖关系。当 Docker 运行一个容器时,容器本身承载了其正常运行所需的一切。甚至可以部署同一应用程序框架的不同版本,因为每个容器都存在于自己的内核空间中。

图 1.1:在两个不同的容器服务器上运行的七个容器

图 1.1:在两个不同的容器服务器上运行的七个容器

在本章中,您将通过容器化的帮助了解 Docker 提供的各种优势。您还将学习使用docker run命令来运行容器的基础知识。

使用 Docker 的优势

在传统的虚拟机方法中,代码更改需要运维人员或配置管理工具访问该机器并安装软件的新版本。不可变容器的原则意味着当代码更改发生时,将构建新版本的容器映像,并创建为新的构件。如果需要回滚此更改,只需下载并重新启动容器映像的旧版本就可以了。

利用容器化方法还使软件开发团队能够在各种场景和多个环境中可预测和可靠地测试应用程序。由于 Docker 运行时环境提供了标准的执行环境,软件开发人员可以快速重现问题并轻松调试问题。由于容器的不可变性,开发人员可以确保相同的代码在所有环境中运行,因为相同的 Docker 映像可以部署在任何环境中。这意味着配置变量,如无效的数据库连接字符串、API 凭据或其他特定于环境的差异,是故障的主要来源。这减轻了运维负担,并提供了无与伦比的效率和可重用性。

使用 Docker 的另一个优势是,与传统基础设施相比,容器化应用程序通常更小、更灵活。容器通常只提供运行应用程序所需的必要库和软件包,而不是提供完整的操作系统内核和执行环境。

在构建 Docker 容器时,开发人员不再受限于主机操作系统上安装的软件包和工具,这些可能在不同环境之间有所不同。他们可以将容器映像中仅包含应用程序运行所需的确切版本的库和实用程序。在部署到生产机器上时,开发人员和运维团队不再关心容器运行在什么硬件或操作系统版本上,只要他们的容器在运行就可以了。

例如,截至 2020 年 1 月 1 日,Python 2 不再受支持。因此,许多软件仓库正在逐步淘汰 Python 2 包和运行时。利用容器化方法,您可以继续以受控、安全和可靠的方式运行传统的 Python 2 应用程序,直到这些传统应用程序可以被重写。这消除了担心安装操作系统级补丁的恐惧,这些补丁可能会移除 Python 2 支持并破坏传统应用程序堆栈。这些 Python 2 容器甚至可以与 Docker 服务器上的 Python 3 应用程序并行运行,以在这些应用程序迁移到新的现代化堆栈时提供精确的测试。

现在我们已经了解了 Docker 是什么以及它是如何工作的,我们可以开始使用 Docker 来了解进程隔离与虚拟化和其他类似技术的区别。

注意

在我们开始运行容器之前,您必须在本地开发工作站上安装 Docker。有关详细信息,请查看本书的前言部分。

Docker 引擎

Docker 引擎是提供对 Linux 内核进程隔离功能的接口。由于只有 Linux 暴露了允许容器运行的功能,因此 Windows 和 macOS 主机利用后台的 Linux 虚拟机来实现容器执行。对于 Windows 和 macOS 用户,Docker 提供了“Docker 桌面”套件,用于在后台部署和运行这个虚拟机。这允许从 macOS 或 Windows 主机的终端或 PowerShell 控制台本地执行 Docker 命令。Linux 主机有特权直接在本地执行 Docker 引擎,因为现代版本的 Linux 内核支持cgroups和命名空间隔离。

注意

由于 Windows、macOS 和 Linux 在网络和进程管理方面具有根本不同的操作系统架构,本书中的一些示例(特别是在网络方面)有时会根据在您的开发工作站上运行的操作系统而有不同的行为。这些差异会在出现时进行说明。

Docker 引擎不仅支持执行容器镜像,还提供了内置机制,可以从源代码文件(称为Dockerfiles)构建和测试容器镜像。构建容器镜像后,可以将其推送到容器镜像注册表。镜像注册表是容器镜像的存储库,其他 Docker 主机可以从中下载和执行容器镜像。Docker 引擎支持运行容器镜像、构建容器镜像,甚至在配置为这样运行时托管容器镜像注册表。

当容器启动时,Docker 默认会下载容器镜像,将其存储在本地容器镜像缓存中,最后执行容器的entrypoint指令。entrypoint指令是启动应用程序主要进程的命令。当这个进程停止或关闭时,容器也将停止运行。

根据容器内运行的应用程序,entrypoint指令可能是长期运行的服务器守护程序,始终可用,或者可能是一个短暂的脚本,在执行完成后自然停止。另外,许多容器执行entrypoint脚本,在启动主要进程之前完成一系列设置步骤,这可能是长期或短期的。

在运行任何容器之前,最好先了解将在容器内运行的应用程序类型,以及它是短暂执行还是长期运行的服务器守护程序。

运行 Docker 容器

构建容器和微服务架构的最佳实践规定,一个容器应该只运行一个进程。牢记这一原则,我们可以设计容器,使其易于构建、故障排除、扩展和部署。

容器的生命周期由容器的状态和其中运行的进程定义。根据操作员、容器编排器或容器内部运行的应用程序的状态,容器可以处于运行或停止状态。例如,操作员可以使用docker stopdocker start命令手动停止或启动容器。如果 Docker 检测到容器进入不健康状态,它甚至可能自动停止或重新启动容器。此外,如果容器内部运行的主要应用程序失败或停止,运行的容器实例也应该停止。许多容器运行时平台,如 Docker,甚至提供自动机制来自动重新启动进入停止状态的容器。许多容器平台利用这一原则构建作业和任务执行功能。

由于容器在容器内部的主要进程完成时终止,容器是执行脚本和其他类型的具有无限寿命的作业的优秀平台。下面的图 1.2说明了典型容器的生命周期:

图 1.2:典型容器的生命周期

图 1.2:典型容器的生命周期

一旦在目标操作系统上下载并安装了 Docker,就可以开始运行容器。Docker CLI 具有一个名为docker run的命令,专门用于启动和运行 Docker 容器。正如我们之前学到的,容器提供了与系统上运行的其他应用程序和进程隔离的功能。由于这个事实,Docker 容器的生命周期由容器内部运行的主要进程决定。当容器停止时,Docker 可能会尝试重新启动容器,以确保应用程序的连续性。

为了查看主机系统上正在运行的容器,我们还将利用docker ps命令。docker ps命令类似于 Unix 风格的ps命令,用于显示 Linux 或基于 Unix 的操作系统上运行的进程。

记住,当 Docker 首次运行容器时,如果它在本地缓存中没有存储容器镜像,它将从容器镜像注册表中下载容器镜像。要查看本地存储的容器镜像,使用docker images命令。

以下练习将演示如何使用docker rundocker psdocker images命令来启动和查看简单的hello-world容器的状态。

练习 1.01:运行 hello-world 容器

一个简单的“Hello World”应用程序通常是开发人员在学习软件开发或开始新的编程语言时编写的第一行代码,容器化也不例外。Docker 发布了一个非常小且简单执行的hello-world容器。该容器演示了运行单个具有无限寿命的进程的容器的特性。

在这个练习中,你将使用docker run命令启动hello-world容器,并使用docker ps命令查看容器在执行完成后的状态。这将为你提供一个在本地开发环境中运行容器的基本概述。

  1. 在 Bash 终端或 PowerShell 窗口中输入docker run命令。这会指示 Docker 运行一个名为hello-world的容器:
$ docker run hello-world

你的 shell 应该返回类似以下的输出:

Unable to find image 'hello-world: latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete 
Digest: sha256:
8e3114318a995a1ee497790535e7b88365222a21771ae7e53687ad76563e8e76
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working 
correctly.
To generate this message, Docker took the following steps:
 1\. The Docker client contacted the Docker daemon.
 2\. The Docker daemon pulled the "hello-world" image from the 
Docker Hub.
    (amd64)
 3\. The Docker daemon created a new container from that image 
which runs the executable that produces the output you are 
currently reading.
4\. The Docker daemon streamed that output to the Docker 
client, which sent it to your terminal.
To try something more ambitious, you can run an Ubuntu 
container with:
 $ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/
For more examples and ideas, visit:
 https://docs.docker.com/get-started/

刚刚发生了什么?你告诉 Docker 运行名为hello-world的容器。所以,首先,Docker 会在本地容器缓存中查找具有相同名称的容器。如果找不到,它将尝试在互联网上的容器注册表中查找以满足命令。通过简单地指定容器的名称,Docker 将默认查询 Docker Hub 以获取该名称的已发布容器镜像。

如你所见,它能够找到一个名为library/hello-world的容器,并开始逐层拉取容器镜像。在第二章《使用 Dockerfiles 入门》中,你将更深入地了解容器镜像和层。一旦镜像完全下载,Docker 运行该镜像,显示Hello from Docker输出。由于该镜像的主要进程只是显示该输出,容器在显示输出后停止并停止运行。

  1. 使用docker ps命令查看系统上正在运行的容器。在 Bash 或 PowerShell 终端中,输入以下命令:
$ docker ps

这将返回类似以下的输出:

CONTAINER ID      IMAGE     COMMAND      CREATED
  STATUS              PORTS                   NAMES

docker ps命令的输出为空,因为它默认只显示当前正在运行的容器。这类似于 Linux/Unix 的ps命令,它只显示正在运行的进程。

  1. 使用docker ps -a命令显示所有容器,甚至是已停止的容器:
$ docker ps -a

在返回的输出中,你应该看到hello-world容器实例:

CONTAINER ID     IMAGE           COMMAND     CREATED
  STATUS                          PORTS         NAMES
24c4ce56c904     hello-world     "/hello"    About a minute ago
  Exited (0) About a minute ago                 inspiring_moser

正如你所看到的,Docker 给容器分配了一个唯一的容器 ID。它还显示了运行的IMAGE,在该映像中执行的COMMAND,创建的TIME,以及运行该容器的进程的STATUS,以及一个唯一的可读名称。这个特定的容器大约一分钟前创建,执行了程序/hello,并成功运行。你可以看出程序运行并成功执行,因为它产生了一个Exited (0)的代码。

  1. 你可以查询你的系统,看看 Docker 本地缓存了哪些容器映像。执行docker images命令来查看本地缓存:
$ docker images

返回的输出应该显示本地缓存的容器映像:

REPOSITORY     TAG        IMAGE ID        CREATED         SIZE
hello-world    latest     bf756fb1ae65    3 months ago    13.3kB

到目前为止,唯一缓存的映像是hello-world容器映像。这个映像正在运行latest版本,创建于 3 个月前,大小为 13.3 千字节。从前面的输出中,你知道这个 Docker 映像非常精简,开发者在 3 个月内没有发布过这个映像的代码更改。这个输出对于排除现实世界中软件版本之间的差异非常有帮助。

由于你只是告诉 Docker 运行hello-world容器而没有指定版本,Docker 将默认拉取最新版本。你可以通过在docker run命令中指定标签来指定不同的版本。例如,如果hello-world容器映像有一个版本2.0,你可以使用docker run hello-world:2.0命令运行该版本。

想象一下,如果容器比一个简单的hello-world应用程序复杂一些。想象一下,你的同事编写了一个软件,需要下载许多第三方库的非常特定的版本。如果你传统地运行这个应用程序,你将不得不下载他们开发语言的运行环境,以及所有的第三方库,以及详细的构建和执行他们的代码的说明。

然而,如果他们将他们的代码的 Docker 镜像发布到内部 Docker 注册表,他们只需要向您提供运行容器的docker run语法。由于您拥有 Docker,无论您的基础平台是什么,容器图像都将运行相同。容器图像本身已经包含了库和运行时的详细信息。

  1. 如果您再次执行相同的docker run命令,那么对于用户输入的每个docker run命令,都将创建一个新的容器实例。值得注意的是,容器化的一个好处是能够轻松运行多个软件应用的实例。为了看到 Docker 如何处理多个容器实例,再次运行相同的docker run命令,以创建hello-world容器的另一个实例:
$ docker run hello-world

您应该看到以下输出:

Hello from Docker!
This message shows that your installation appears to be 
working correctly.
To generate this message, Docker took the following steps:
 1\. The Docker client contacted the Docker daemon.
 2\. The Docker daemon pulled the "hello-world" image from 
    the Docker Hub.
    (amd64)
 3\. The Docker daemon created a new container from that image 
    which runs the executable that produces the output you 
    are currently reading.
 4\. The Docker daemon streamed that output to the Docker client, 
    which sent it to your terminal.
To try something more ambitious, you can run an Ubuntu container 
with:
 $ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/
For more examples and ideas, visit:
 https://docs.docker.com/get-started/

请注意,这一次,Docker 不必再次从 Docker Hub 下载容器图像。这是因为您现在在本地缓存了该容器图像。相反,Docker 能够直接运行容器并将输出显示在屏幕上。让我们看看您的docker ps -a输出现在是什么样子。

  1. 在您的终端中,再次运行docker ps -a命令:
docker ps -a

在输出中,您应该看到这个容器图像的第二个实例已经完成了执行并进入了停止状态,如输出的STATUS列中的Exit (0)所示:

CONTAINER ID     IMAGE           COMMAND       CREATED
  STATUS                      PORTS               NAMES
e86277ca07f1     hello-world     "/hello"      2 minutes ago
  Exited (0) 2 minutes ago                        awesome_euclid
24c4ce56c904     hello-world     "/hello"      20 minutes ago
  Exited (0) 20 minutes ago                       inspiring_moser

您现在在输出中看到了这个容器的第二个实例。每次执行docker run命令时,Docker 都会创建该容器的一个新实例,具有其属性和数据。您可以运行尽可能多的容器实例,只要您的系统资源允许。在这个例子中,您 20 分钟前创建了一个实例。您 2 分钟前创建了第二个实例。

  1. 再次执行docker images命令,检查基本图像:
$ docker images

返回的输出将显示 Docker 从单个基本图像创建的两个运行实例:

REPOSITORY     TAG       IMAGE ID        CREATED         SIZE
hello-world    latest    bf756fb1ae65    3 months ago    13.3kB

在这个练习中,您使用docker run启动了hello-world容器。为了实现这一点,Docker 从 Docker Hub 注册表下载了图像,并在 Docker Engine 中执行了它。一旦基本图像被下载,您可以使用后续的docker run命令创建任意数量的该容器的实例。

Docker 容器管理比在开发环境中仅仅启动和查看容器状态更加复杂。Docker 还支持许多其他操作,这些操作有助于了解在 Docker 主机上运行的应用程序的状态。在接下来的部分中,我们将学习如何使用不同的命令来管理 Docker 容器。

管理 Docker 容器

在我们的容器之旅中,我们将经常从本地环境中拉取、启动、停止和删除容器。在将容器部署到生产环境之前,我们首先需要在本地运行容器,以了解其功能和正常行为。这包括启动容器、停止容器、获取有关容器运行方式的详细信息,当然还包括访问容器日志以查看容器内运行的应用程序的关键细节。这些基本命令如下所述:

  • docker pull:此命令将容器镜像下载到本地缓存

  • docker stop:此命令停止运行中的容器实例

  • docker start:此命令启动不再处于运行状态的容器实例

  • docker restart:此命令重新启动运行中的容器

  • docker attach:此命令允许用户访问(或附加)运行中的 Docker 容器实例的主要进程

  • docker exec:此命令在运行中的容器内执行命令

  • docker rm:此命令删除已停止的容器

  • docker rmi:此命令删除容器镜像

  • docker inspect:此命令显示有关容器状态的详细信息

容器生命周期管理是生产环境中有效容器管理的关键组成部分。在评估容器化基础设施的健康状况时,了解如何调查运行中的容器是至关重要的。

在接下来的练习中,我们将逐个使用这些命令,深入了解它们的工作原理以及如何利用它们来了解容器化基础设施的健康状况。

练习 1.02:管理容器生命周期

在开发和生产环境中管理容器时,了解容器实例的状态至关重要。许多开发人员使用包含特定基线配置的基础容器镜像,他们的应用程序可以在其上部署。Ubuntu 是一个常用的基础镜像,用户用它来打包他们的应用程序。

与完整的操作系统镜像不同,Ubuntu 基础容器镜像非常精简,故意省略了许多完整操作系统安装中的软件包。大多数基础镜像都有软件包系统,可以让您安装任何缺失的软件包。

请记住,在构建容器镜像时,您希望尽可能保持基础镜像的精简,只安装最必要的软件包。这样可以确保 Docker 主机可以快速拉取和启动容器镜像。

在这个练习中,您将使用官方的 Ubuntu 基础容器镜像。这个镜像将用于启动容器实例,用于测试各种容器生命周期管理命令,比如docker pulldocker startdocker stop。这个容器镜像很有用,因为默认的基础镜像允许我们在长时间运行的会话中运行容器实例,以了解容器生命周期管理命令的功能。在这个练习中,您还将拉取Ubuntu 18.04容器镜像,并将其与Ubuntu 19.04容器镜像进行比较:

  1. 在新的终端或 PowerShell 窗口中,执行docker pull命令以下载Ubuntu 18.04容器镜像:
$ docker pull ubuntu:18.04

您应该看到以下输出,表明 Docker 正在下载基础镜像的所有层:

5bed26d33875: Pull complete 
f11b29a9c730: Pull complete 
930bda195c84: Pull complete 
78bf9a5ad49e: Pull complete 
Digest: sha256:bec5a2727be7fff3d308193cfde3491f8fba1a2ba392
        b7546b43a051853a341d
Status: Downloaded newer image for ubuntu:18.04
docker.io/library/ubuntu:18.04
  1. 使用docker pull命令下载Ubuntu 19.04基础镜像:
$ docker pull ubuntu:19.04

当 Docker 下载Ubuntu 19.04基础镜像时,您将看到类似的输出:

19.04: Pulling from library/ubuntu
4dc9c2fff018: Pull complete 
0a4ccbb24215: Pull complete 
c0f243bc6706: Pull complete 
5ff1eaecba77: Pull complete 
Digest: sha256:2adeae829bf27a3399a0e7db8ae38d5adb89bcaf1bbef
        378240bc0e6724e8344
Status: Downloaded newer image for ubuntu:19.04
docker.io/library/ubuntu:19.04
  1. 使用docker images命令确认容器镜像已下载到本地容器缓存:
$ docker images

本地容器缓存的内容将显示Ubuntu 18.04Ubuntu 19.04基础镜像,以及我们之前练习中的hello-world镜像:

REPOSITORY     TAG        IMAGE ID         CREATED         SIZE
ubuntu         18.04      4e5021d210f6     4 weeks ago     64.2MB
ubuntu         19.04      c88ac1f841b7     3 months ago    70MB
hello-world    latest     bf756fb1ae65     3 months ago    13.3kB
  1. 在运行这些镜像之前,使用docker inspect命令获取关于容器镜像的详细输出以及它们之间的差异。在你的终端中,运行docker inspect命令,并使用Ubuntu 18.04容器镜像的 ID 作为主要参数:
$ docker inspect 4e5021d210f6

inspect输出将包含定义该容器的所有属性的大型列表。例如,你可以看到容器中配置了哪些环境变量,容器在最后一次更新镜像时是否设置了主机名,以及定义该容器的所有层的详细信息。这个输出包含了在规划升级时可能会有价值的关键调试细节。以下是inspect命令的截断输出。在Ubuntu 18.04镜像中,"Created"参数应该提供构建容器镜像的日期和时间:

"Id": "4e5021d210f6d4a0717f4b643409eff23a4dc01c4140fa378b1b
       f0a4f8f4",
"Created": "2020-03-20T19:20:22.835345724Z",
"Path": "/bin/bash",
"Args": [],
  1. 检查Ubuntu 19.04容器,你会看到这个参数是不同的。在Ubuntu 19.04容器镜像 ID 中运行docker inspect命令:
$ docker inspect c88ac1f841b7

在显示的输出中,你会看到这个容器镜像是在一个不同的日期创建的,与18.04容器镜像不同:

"Id": "c88ac1f841b74e5021d210f6d4a0717f4b643409eff23a4dc0
       1c4140fa"
"Created": "2020-01-16T01:20:46.938732934Z",
"Path": "/bin/bash",
"Args": []

如果你知道 Ubuntu 基础镜像中可能存在安全漏洞,这可能是至关重要的信息。这些信息也可以对帮助你确定要运行哪个版本的容器至关重要。

  1. 在检查了两个容器镜像之后,很明显你最好的选择是坚持使用 Ubuntu 长期支持版 18.04 版本。正如你从前面的输出中看到的,18.04 版本比 19.04 版本更加更新。这是可以预期的,因为 Ubuntu 通常会为长期支持版本提供更稳定的更新。

  2. 使用docker run命令启动 Ubuntu 18.04 容器的一个实例:

$ docker run -d ubuntu:18.04

请注意,这次我们使用了带有-d标志的docker run命令。这告诉 Docker 以守护进程模式(或后台模式)运行容器。如果我们省略-d标志,容器将占用我们当前的终端,直到容器内的主要进程终止。

注意

成功调用docker run命令通常只会返回容器 ID 作为输出。某些版本的 Docker 不会返回任何输出。

  1. 使用docker ps -a命令检查容器的状态:
$ docker ps -a

这将显示类似于以下内容的输出:

CONTAINER ID     IMAGE           COMMAND        CREATED
  STATUS                     PORTS         NAMES
c139e44193de     ubuntu:18.04    "/bin/bash"    6 seconds ago
  Exited (0) 4 seconds ago                 xenodochial_banzai

正如你所看到的,你的容器已经停止并退出。这是因为容器内的主要进程是/bin/bash,这是一个 shell。Bash shell 不能在没有以交互模式执行的情况下运行,因为它期望来自用户的文本输入和输出。

  1. 再次运行docker run命令,传入-i标志以使会话交互(期望用户输入),并传入-t标志以为容器分配一个伪 tty处理程序。伪 tty处理程序将基本上将用户的终端链接到容器内运行的交互式 Bash shell。这将允许 Bash 正确运行,因为它将指示容器以交互模式运行,期望用户输入。您还可以通过传入--name标志为容器指定一个易读的名称。在您的 Bash 终端中键入以下命令:
$ docker run -i -t -d --name ubuntu1 ubuntu:18.04
  1. 再次执行docker ps -a命令以检查容器实例的状态:
$ docker ps -a 

您现在应该看到新的实例正在运行,以及刚刚无法启动的实例:

CONTAINER ID    IMAGE          COMMAND         CREATED
  STATUS            PORTS               NAMES
f087d0d92110    ubuntu:18.04   "/bin/bash"     4 seconds ago
  Up 2 seconds                          ubuntu1
c139e44193de    ubuntu:18.04   "/bin/bash"     5 minutes ago
  Exited (0) 5 minutes ago              xenodochial_banzai
  1. 您现在有一个正在运行的 Ubuntu 容器。您可以使用docker exec命令在此容器内运行命令。运行exec命令以访问 Bash shell,这将允许我们在容器内运行命令。类似于docker run,传入-i-t标志使其成为交互式会话。还传入容器的名称或 ID,以便 Docker 知道您要定位哪个容器。docker exec的最后一个参数始终是您希望执行的命令。在这种情况下,它将是/bin/bash,以在容器实例内启动 Bash shell:
docker exec -it ubuntu1 /bin/bash

您应该立即看到您的提示更改为根 shell。这表明您已成功在 Ubuntu 容器内启动了一个 shell。容器的主机名cfaa37795a7b取自容器 ID 的前 12 个字符。这使用户可以确定他们正在访问哪个容器,如下例所示:

root@cfaa37795a7b:/#
  1. 在容器内,您所拥有的工具非常有限。与 VM 镜像不同,容器镜像在预安装的软件包方面非常精简。但是echo命令应该是可用的。使用echo将一个简单的消息写入文本文件:
root@cfaa37795a7b:/# echo "Hello world from ubuntu1" > hello-world.txt
  1. 运行exit命令退出ubuntu1容器的 Bash shell。您应该返回到正常的终端 shell:
root@cfaa37795a7b:/# exit

该命令将返回以下输出。请注意,对于运行该命令的每个用户,输出可能会有所不同:

user@developmentMachine:~/
  1. 现在创建一个名为ubuntu2的第二个容器,它也将在您的 Docker 环境中使用Ubuntu 19.04镜像运行:
$ docker run -i -t -d --name ubuntu2 ubuntu:19.04
  1. 运行docker exec来访问第二个容器的 shell。记得使用你创建的新容器的名称或容器 ID。同样地,访问这个容器内部的 Bash shell,所以最后一个参数将是/bin/bash
$ docker exec -it ubuntu2 /bin/bash

你应该观察到你的提示会变成一个 Bash root shell,类似于Ubuntu 18.04容器镜像的情况:

root@875cad5c4dd8:/#
  1. ubuntu2容器实例内部运行echo命令,写入类似的hello-world类型的问候语:
root@875cad5c4dd8:/# echo "Hello-world from ubuntu2!" > hello-world.txt
  1. 目前,在你的 Docker 环境中有两个运行中的 Ubuntu 容器实例,根账户的主目录中有两个单独的hello-world问候消息。使用docker ps来查看这两个运行中的容器镜像:
$ docker ps

运行容器的列表应该反映出两个 Ubuntu 容器,以及它们创建后经过的时间:

CONTAINER ID    IMAGE            COMMAND        CREATED
  STATUS              PORTS               NAMES
875cad5c4dd8    ubuntu:19.04     "/bin/bash"    3 minutes ago
  Up 3 minutes                            ubuntu2
cfaa37795a7b    ubuntu:18.04     "/bin/bash"    15 minutes ago
  Up 15 minutes                           ubuntu1
  1. 不要使用docker exec来访问容器内部的 shell,而是使用它来显示你通过在容器内执行cat命令写入的hello-world.txt文件的输出:
$ docker exec -it ubuntu1 cat hello-world.txt

输出将显示你在之前步骤中传递给容器的hello-world消息。请注意,一旦cat命令完成并显示输出,用户就会被移回到主终端的上下文中。这是因为docker exec会话只会存在于用户执行命令的时间内。

在之前的 Bash shell 示例中,只有用户使用exit命令终止它时,Bash 才会退出。在这个例子中,只显示了Hello world输出,因为cat命令显示了输出并退出,结束了docker exec会话:

Hello world from ubuntu1

你会看到hello-world文件的内容显示,然后返回到你的主终端会话。

  1. ubuntu2容器实例中运行相同的cat命令:
$ docker exec -it ubuntu2 cat hello-world.txt

与第一个例子类似,ubuntu2容器实例将显示之前提供的hello-world.txt文件的内容:

Hello-world from ubuntu2!

正如你所看到的,Docker 能够在两个容器上分配一个交互式会话,执行命令,并直接返回输出到我们正在运行的容器实例中。

  1. 与你用来在运行中的容器内执行命令的方式类似,你也可以停止、启动和重新启动它们。使用docker stop命令停止其中一个容器实例。在你的终端会话中,执行docker stop命令,然后是ubuntu2容器的名称或容器 ID:
$ docker stop ubuntu2

该命令应该不返回任何输出。

  1. 使用docker ps命令查看所有正在运行的容器实例:
$ docker ps

输出将显示ubuntu1容器正在运行:

CONTAINER ID    IMAGE           COMMAND        CREATED
  STATUS              PORTS               NAMES
cfaa37795a7b    ubuntu:18.04    "/bin/bash"    26 minutes ago
  Up 26 minutes                           ubuntu1
  1. 执行docker ps -a命令以查看所有容器实例,无论它们是否正在运行,以查看您的容器是否处于停止状态:
$ docker ps -a

该命令将返回以下输出:

CONTAINER ID     IMAGE            COMMAND         CREATED
  STATUS                      PORTS             NAMES
875cad5c4dd8     ubuntu:19.04     "/bin/bash"     14 minutes ago
  Exited (0) 6 seconds ago                      ubuntu2
  1. 使用docker startdocker restart命令重新启动容器实例:
$ docker start ubuntu2

该命令将不返回任何输出,尽管某些版本的 Docker 可能会显示容器 ID。

  1. 使用docker ps命令验证容器是否再次运行:
$ docker ps

注意STATUS显示该容器只运行了很短的时间(1 秒),尽管容器实例是 29 分钟前创建的:

CONTAINER ID    IMAGE           COMMAND         CREATED
  STATUS              PORTS               NAMES
875cad5c4dd8    ubuntu:19.04    "/bin/bash"     17 minutes ago
  Up 1 second                             ubuntu2
cfaa37795a7b    ubuntu:18.04    "/bin/bash"     29 minutes ago
  Up 29 minutes                           ubuntu1

从这个状态开始,您可以尝试启动、停止或在这些容器内执行命令。

  1. 容器管理生命周期的最后阶段是清理您创建的容器实例。使用docker stop命令停止ubuntu1容器实例:
$ docker stop ubuntu1

该命令将不返回任何输出,尽管某些版本的 Docker 可能会返回容器 ID。

  1. 执行相同的docker stop命令以停止ubuntu2容器实例:
$ docker stop ubuntu2
  1. 当容器实例处于停止状态时,使用docker rm命令彻底删除容器实例。使用docker rm后跟名称或容器 ID 删除ubuntu1容器实例:
$ docker rm ubuntu1

该命令将不返回任何输出,尽管某些版本的 Docker 可能会返回容器 ID。

ubuntu2容器实例上执行相同的步骤:

$ docker rm ubuntu2
  1. 执行docker ps -a以查看所有容器,即使它们处于停止状态。您会发现停止的容器由于之前的命令已被删除。您也可以删除hello-world容器实例。使用从docker ps -a输出中捕获的容器 ID 删除hello-world容器:
$ docker rm b291785f066c
  1. 要完全重置我们的 Docker 环境状态,请删除您在此练习中下载的基本图像。使用docker images命令查看缓存的基本图像:
$ docker images

您的本地缓存中将显示 Docker 图像列表和所有关联的元数据:

REPOSITORY     TAG        IMAGE ID        CREATED         SIZE
ubuntu         18.04      4e5021d210f6    4 weeks ago     64.2MB
ubuntu         19.04      c88ac1f841b7    3 months ago    70MB
hello-world    latest     bf756fb1ae65    3 months ago    13.3kB
  1. 执行docker rmi命令,后跟图像 ID 以删除第一个图像 ID:
$ docker rmi 4e5021d210f6

类似于docker pullrmi命令将删除每个图像和所有关联的层:

Untagged: ubuntu:18.04
Untagged: ubuntu@sha256:bec5a2727be7fff3d308193cfde3491f8fba1a2b
a392b7546b43a051853a341d
Deleted: sha256:4e5021d210f65ebe915670c7089120120bc0a303b9020859
2851708c1b8c04bd
Deleted: sha256:1d9112746e9d86157c23e426ce87cc2d7bced0ba2ec8ddbd
fbcc3093e0769472
Deleted: sha256:efcf4a93c18b5d01aa8e10a2e3b7e2b2eef0378336456d86
53e2d123d6232c1e
Deleted: sha256:1e1aa31289fdca521c403edd6b37317bf0a349a941c7f19b
6d9d311f59347502
Deleted: sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b0916
21ce19357e19d853

对于要删除的每个映像,执行此步骤,替换各种映像 ID。对于删除的每个基本映像,您将看到所有图像层都被取消标记并与其一起删除。

定期清理 Docker 环境很重要,因为频繁构建和运行容器会导致长时间大量的硬盘使用。现在您已经知道如何在本地开发环境中运行和管理 Docker 容器,可以使用更高级的 Docker 命令来了解容器的主要进程功能以及如何解决问题。在下一节中,我们将看看docker attach命令,直接访问容器的主要进程。

注意

为了简化清理环境的过程,Docker 提供了一个prune命令,将自动删除旧的容器和基本映像:

$ docker system prune -fa

执行此命令将删除任何未绑定到现有运行容器的容器映像,以及 Docker 环境中的任何其他资源。

使用 attach 命令附加到容器

在上一个练习中,您看到了如何使用docker exec命令在运行的容器实例中启动新的 shell 会话以执行命令。docker exec命令非常适合快速访问容器化实例以进行调试、故障排除和了解容器运行的上下文。

但是,正如本章前面所述,Docker 容器按照容器内部运行的主要进程的生命周期运行。当此进程退出时,容器将停止。如果要直接访问容器内部的主要进程(而不是次要的 shell 会话),那么 Docker 提供了docker attach命令来附加到容器内部正在运行的主要进程。

使用docker attach时,您可以访问容器中运行的主要进程。如果此进程是交互式的,例如 Bash 或 Bourne shell 会话,您将能够通过docker attach会话直接执行命令(类似于docker exec)。但是,如果容器中的主要进程终止,整个容器实例也将终止,因为 Docker 容器的生命周期取决于主要进程的运行状态。

在接下来的练习中,您将使用docker attach命令直接访问 Ubuntu 容器的主要进程。默认情况下,此容器的主要进程是/bin/bash

练习 1.03:附加到 Ubuntu 容器

docker attach命令用于在主要进程的上下文中附加到运行中的容器。在此练习中,您将使用docker attach命令附加到运行中的容器并直接调查主容器entrypoint进程:

  1. 使用docker run命令启动一个新的 Ubuntu 容器实例。以交互模式(-i)运行此容器,分配一个 TTY 会话(-t),并在后台(-d)运行。将此容器命名为attach-example1
docker run -itd --name attach-example1 ubuntu:latest

这将使用 Ubuntu 容器图像的最新版本启动一个名为attach-example1的新 Ubuntu 容器实例。

  1. 使用docker ps命令来检查该容器是否在我们的环境中运行:
docker ps 

将显示运行中容器实例的详细信息。请注意,此容器的主要进程是 Bash shell(/bin/bash):

CONTAINER ID    IMAGE            COMMAND          CREATED
  STATUS              PORTS               NAMES
90722712ae93    ubuntu:latest    "/bin/bash"      18 seconds ago
  Up 16 seconds                           attach-example1
  1. 运行docker attach命令以附加到此容器内部的主要进程(/bin/bash)。使用docker attach后跟容器实例的名称或 ID:
$ docker attach attach-example1

这应该将您放入此容器实例的主 Bash shell 会话中。请注意,您的终端会话应更改为根 shell 会话,表示您已成功访问了容器实例:

root@90722712ae93:/#

在这里需要注意,使用诸如exit之类的命令来终止 shell 会话将导致停止容器实例,因为您现在已连接到容器实例的主要进程。默认情况下,Docker 提供了Ctrl + P然后Ctrl + Q的快捷键序列,以正常分离attach会话。

  1. 使用键盘组合Ctrl + P然后Ctrl + Q正常分离此会话:
root@90722712ae93:/# CTRL-p CTRL-q

注意

您不会输入CTRL-p CTRL-q这些单词;相反,您将按住Ctrl键,按下P键,然后释放两个键。然后,再次按住Ctrl键,按下Q键,然后再次释放两个键。

成功分离容器后,将显示单词read escape sequence,然后将您返回到主终端或 PowerShell 会话:

root@90722712ae93:/# read escape sequence
  1. 使用docker ps验证 Ubuntu 容器是否仍然按预期运行:
$ docker ps

attach-example1容器将被显示为预期运行:

CONTAINER ID    IMAGE            COMMAND          CREATED
  STATUS              PORTS               NAMES
90722712ae93    ubuntu:latest    "/bin/bash"      13 minutes ago
  Up 13 minutes                           attach-example1
  1. 使用docker attach命令再次附加到attach-example1容器实例:
$ docker attach attach-example1

您应该被放回到主进程的 Bash 会话中:

root@90722712ae93:/#
  1. 现在,使用exit命令终止这个容器的主进程。在 Bash shell 会话中,输入exit命令:
root@90722712ae93:/# exit

终端会话应该已经退出,再次返回到您的主终端。

  1. 使用docker ps命令观察attach-example1容器不再运行:
$ docker ps

这应该不会显示任何正在运行的容器实例:

CONTAINER ID    IMAGE            COMMAND              CREATED
  STATUS              PORTS               NAMES
  1. 使用docker ps -a命令查看所有容器,即使已停止或已退出的容器也会显示:
$ docker ps -a

这应该显示attach-example1容器处于停止状态:

CONTAINER ID      IMAGE                COMMAND 
  CREATED            STATUS    PORTS           NAMES
90722712ae93      ubuntu:latest        "/bin/bash"
  20 minutes ago     Exited (0) 3 minutes ago  attach-example1

正如你所看到的,容器已经优雅地终止(Exited (0))大约 3 分钟前。exit命令会优雅地终止 Bash shell 会话。

  1. 使用docker system prune -fa命令清理已停止的容器实例:
docker system prune -fa

这应该删除所有已停止的容器实例,包括attach-example1容器实例,如下面的输出所示:

Deleted Containers:
ry6v87v9a545hjn7535jk2kv9x8cv09wnkjnscas98v7a762nvnw7938798vnand
Deleted Images:
untagged: attach-example1

在这个练习中,我们使用docker attach命令直接访问正在运行的容器的主进程。这与我们在本章中早些时候探讨的docker exec命令不同,因为docker exec在运行的容器内执行一个新的进程,而docker attach直接附加到容器的主进程。然而,在附加到容器时,必须注意不要通过终止主进程来停止容器。

在下一个活动中,我们将整合本章中涵盖的 Docker 管理命令,开始组装成全景徒步旅行微服务应用程序堆栈的构建块容器。

活动 1.01:从 Docker Hub 拉取并运行 PostgreSQL 容器镜像

全景徒步旅行是我们将在本书中构建的多层 Web 应用程序。与任何 Web 应用程序类似,它将包括一个 Web 服务器容器(NGINX)、一个 Python Django 后端应用程序和一个 PostgreSQL 数据库。在部署 Web 应用程序或前端 Web 服务器之前,您必须先部署后端数据库。

在这个活动中,您被要求使用默认凭据启动一个 PostgreSQL 版本 12 的数据库容器。

注意

官方的 Postgres 容器映像提供了许多环境变量覆盖,您可以利用这些变量来配置 PostgreSQL 实例。在 Docker Hub 上查看有关容器的文档hub.docker.com/_/postgres

执行以下步骤:

  1. 创建一个 Postgres 数据库容器实例,将作为我们应用程序堆栈的数据层。

  2. 使用环境变量在运行时配置容器以使用以下数据库凭据:

username: panoramic
password: trekking
  1. 验证容器是否正在运行和健康。

预期输出:

运行docker ps命令应返回以下输出:

CONTAINER ID  IMAGE         COMMAND                 CREATED
  STATUS              PORTS               NAMES
29f115af8cdd  postgres:12   "docker-entrypoint.s…"  4 seconds ago
  Up 2 seconds        5432/tcp            blissful_kapitsa

注意

此活动的解决方案可以通过此链接找到。

在下一个活动中,您将访问刚刚在容器实例中设置的数据库。您还将与容器交互,以获取容器中运行的数据库列表。

活动 1.02:访问全景徒步应用程序数据库

本活动将涉及使用PSQL CLI 实用程序访问在容器实例内运行的数据库。一旦您使用凭据(panoramic/trekking)登录,您将查询容器中运行的数据库列表。

执行以下步骤:

  1. 使用 PSQL 命令行实用程序登录到 Postgres 数据库容器。

  2. 登录到数据库后,默认情况下返回 Postgres 中的数据库列表。

注意

如果您对 PSQL CLI 不熟悉,以下是一些参考命令的列表,以帮助您完成此活动:

登录:psql --username username --password

列出数据库:\l

退出 PSQL shell:\q

预期输出:

图 1.3:活动 1.02 的预期输出

图 1.3:活动 1.02 的预期输出

注意

此活动的解决方案可以通过此链接找到。

摘要

在本章中,您学习了容器化的基础知识,以及在容器中运行应用程序的好处,以及管理容器化实例的基本 Docker 生命周期命令。您了解到容器作为一个真正可以构建一次并在任何地方运行的通用软件部署包。因为我们在本地运行 Docker,我们可以确信在我们的本地环境中运行的相同容器映像可以部署到生产环境并且可以放心地运行。

通过诸如docker rundocker startdocker execdocker psdocker stop之类的命令,我们通过 Docker CLI 探索了容器生命周期管理的基础知识。通过各种练习,我们从相同的基础映像启动了容器实例,使用docker exec对其进行了配置,并使用其他基本的容器生命周期命令(如docker rmdocker rmi)清理了部署。

在本章的最后部分,我们毅然决然地迈出了第一步,通过启动一个 PostgreSQL 数据库容器实例,开始运行我们的全景徒步应用程序。我们在docker run命令中使用环境变量创建了一个配置了默认用户名和密码的实例。我们通过在容器内部执行 PSQL 命令行工具并查询数据库来测试配置,以查看模式。

虽然这只是触及 Docker 能力表面的一部分,但我们希望它能激发你对即将在后续章节中涵盖的内容的兴趣。在下一章中,我们将讨论使用Dockerfilesdocker build命令构建真正不可变的容器。编写自定义的Dockerfiles来构建和部署独特的容器映像将展示在规模上运行容器化应用程序的强大能力。

第二章:使用 Dockerfile 入门

概述

在本章中,您将学习Dockerfile及其指令的形式和功能,包括FROMLABELCMD,您将使用这些指令来 dockerize 一个应用程序。本章将为您提供关于 Docker 镜像的分层文件系统和在 Docker 构建过程中使用缓存的知识。在本章结束时,您将能够使用常见指令编写Dockerfile并使用Dockerfile构建自定义 Docker 镜像。

介绍

在上一章中,我们学习了如何通过从 Docker Hub 拉取预构建的 Docker 镜像来运行我们的第一个 Docker 容器。虽然从 Docker Hub 获取预构建的 Docker 镜像很有用,但我们必须知道如何创建自定义 Docker 镜像。这对于通过安装新软件包和自定义预构建 Docker 镜像的设置来在 Docker 上运行我们的应用程序非常重要。在本章中,我们将学习如何创建自定义 Docker 镜像并基于它运行 Docker 容器。

这将使用一个名为Dockerfile的文本文件完成。该文件包含 Docker 可以执行以创建 Docker 镜像的命令。使用docker build(或docker image build)命令从Dockerfile创建 Docker 镜像。

注意

从 Docker 1.13 开始,Docker CLI 的语法已重构为 Docker COMMAND SUBCOMMAND 的形式。例如,docker build命令被替换为docker image build命令。此重构是为了清理 Docker CLI 语法并获得更一致的命令分组。目前,两种语法都受支持,但预计将来会弃用旧语法。

Docker 镜像由多个层组成,每个层代表Dockerfile中提供的命令。这些只读层叠加在一起,以创建最终的 Docker 镜像。Docker 镜像可以存储在 Docker 注册表(如 Docker Hub)中,这是一个可以存储和分发 Docker 镜像的地方。

Docker 容器是 Docker 镜像的运行实例。可以使用docker run(或docker container run)命令从单个 Docker 镜像创建一个或多个 Docker 容器。一旦从 Docker 镜像创建了 Docker 容器,将在 Docker 镜像的只读层之上添加一个新的可写层。然后可以使用 docker ps(或 docker container list)命令列出 Docker 容器:

图 2.1:图像层和容器层

图 2.1:图像层和容器层

如前图所示,Docker 镜像可以由一个或多个只读层组成。这些只读层是在Dockerfile中的每个命令在 Docker 镜像构建过程中生成的。一旦从镜像创建了 Docker 容器,新的可写层(称为容器层)将被添加到镜像层之上,并将承载在运行容器上所做的所有更改。

在本章中,我们将编写我们的第一个Dockerfile,从Dockerfile构建 Docker 镜像,并从我们的自定义 Docker 镜像运行 Docker 容器。然而,在执行任何这些任务之前,我们必须首先定义一个Dockerfile

什么是 Dockerfile?

Dockerfile是一个文本文件,包含了创建 Docker 镜像的指令。这些命令称为指令Dockerfile是我们根据需求创建自定义 Docker 镜像的机制。

Dockerfile的格式如下:

# This is a comment
DIRECTIVE argument

Dockerfile可以包含多行注释和指令。这些行将由Docker 引擎按顺序执行,同时构建 Docker 镜像。与编程语言一样,Dockerfile也可以包含注释。

所有以#符号开头的语句将被视为注释。目前,Dockerfiles只支持单行注释。如果您希望编写多行注释,您需要在每行开头添加#符号。

然而,与大多数编程语言不同,Dockerfile中的指令不区分大小写。即使DIRECTIVE不区分大小写,最好将所有指令都以大写形式编写,以便与参数区分开来。

在下一节中,我们将讨论在Dockerfiles中可以使用的常见指令,以创建自定义 Docker 镜像。

注意

如果您使用的是 18.04 之后的 ubuntu 版本,将会提示输入时区。请使用ARG DEBIAN_FRONTEND=non_interactive来抑制提示

Dockerfile 中的常见指令

如前一节所讨论的,指令是用于创建 Docker 镜像的命令。在本节中,我们将讨论以下五个Dockerfile指令:

  1. FROM指令

  2. LABEL指令

  3. RUN指令

  4. CMD指令

  5. ENTRYPOINT指令

FROM指令

Dockerfile通常以FROM指令开头。这用于指定我们自定义 Docker 镜像的父镜像。父镜像是我们自定义 Docker 镜像的起点。我们所做的所有自定义将应用在父镜像之上。父镜像可以是来自 Docker Hub 的镜像,如 Ubuntu、CentOS、Nginx 和 MySQL。FROM指令接受有效的镜像名称和标签作为参数。如果未指定标签,将使用latest标签。

FROM指令的格式如下:

FROM <image>:<tag> 

在以下FROM指令中,我们使用带有20.04标签的ubuntu父镜像:

FROM ubuntu:20.04

此外,如果需要从头开始构建 Docker 镜像,我们可以使用基础镜像。基础镜像,即 scratch 镜像,是一个空镜像,主要用于构建其他父镜像。

在以下FROM指令中,我们使用scratch镜像从头开始构建我们的自定义 Docker 镜像:

FROM scratch

现在,让我们在下一节中了解LABEL指令是什么。

LABEL指令

LABEL是一个键值对,可用于向 Docker 镜像添加元数据。这些标签可用于适当地组织 Docker 镜像。例如,可以添加Dockerfile的作者姓名或Dockerfile的版本。

LABEL指令的格式如下:

LABEL <key>=<value>

Dockerfile可以有多个标签,遵循前面的键值对格式:

LABEL maintainer=sathsara@mydomain.com
LABEL version=1.0
LABEL environment=dev

或者这些标签可以在单行上用空格分隔包含:

LABEL maintainer=sathsara@mydomain.com version=1.0 environment=dev

现有的 Docker 镜像标签可以使用docker image inspect命令查看。

运行docker image inspect <image>:<tag>命令时,输出应该如下所示:

...
...
"Labels": {
    "environment": "dev",
    "maintainer": "sathsara@mydomain.com",
    "version": "1.0"
}
...
...

如此所示,docker image inspect 命令将输出使用LABEL指令在Dockerfile中配置的键值对。

在下一节中,我们将学习如何使用RUN指令在构建镜像时执行命令。

RUN指令

RUN指令用于在图像构建时执行命令。这将在现有层的顶部创建一个新层,执行指定的命令,并将结果提交到新创建的层。RUN指令可用于安装所需的软件包,更新软件包,创建用户和组等。

RUN指令的格式如下:

RUN <command>

<command>指定您希望作为图像构建过程的一部分执行的 shell 命令。一个Dockerfile可以有多个RUN指令,遵循上述格式。

在以下示例中,我们在父镜像的基础上运行了两个命令。apt-get update用于更新软件包存储库,apt-get install nginx -y用于安装 Nginx 软件包:

RUN apt-get update
RUN apt-get install nginx -y

或者,您可以通过使用&&符号将多个 shell 命令添加到单个RUN指令中。在以下示例中,我们使用了相同的两个命令,但这次是在单个RUN指令中,用&&符号分隔:

RUN apt-get update && apt-get install nginx -y

现在,让我们继续下一节,我们将学习CMD指令。

CMD 指令

Docker 容器通常预期运行一个进程。CMD指令用于提供默认的初始化命令,当从 Docker 镜像创建容器时将执行该命令。Dockerfile只能执行一个CMD指令。如果Dockerfile中有多个CMD指令,Docker 将只执行最后一个。

CMD指令的格式如下:

CMD ["executable","param1","param2","param3", ...]

例如,使用以下命令将"Hello World"作为 Docker 容器的输出:

CMD ["echo","Hello World"]

当我们使用docker container run <image>命令(用 Docker 镜像的名称替换<image>)运行 Docker 容器时,上述CMD指令将产生以下输出:

$ docker container run <image>
Hello World

然而,如果我们使用docker container run <image>命令行参数,这些参数将覆盖我们定义的CMD指令。例如,如果我们执行以下命令(用 Docker 镜像的名称替换<image>),则会忽略使用CMD指令定义的默认的"Hello World"输出。相反,容器将输出"Hello Docker !!!":

$ docker container run <image> echo "Hello Docker !!!"

正如我们讨论过的,RUNCMD指令都可以用来执行 shell 命令。这两个指令之间的主要区别在于,RUN指令提供的命令将在镜像构建过程中执行,而CMD指令提供的命令将在从构建的镜像启动容器时执行。

RUNCMD指令之间的另一个显着区别是,在Dockerfile中可以有多个RUN指令,但只能有一个CMD指令(如果有多个CMD指令,则除最后一个之外的所有其他指令都将被忽略)。

例如,我们可以使用RUN指令在 Docker 镜像构建过程中安装软件包,并使用CMD指令在从构建的镜像启动容器时启动软件包。

在下一节中,我们将学习ENTRYPOINT指令,它提供了与CMD指令相同的功能,除了覆盖。

ENTRYPOINT 指令

CMD指令类似,ENTRYPOINT指令也用于提供默认的初始化命令,该命令将在从 Docker 镜像创建容器时执行。CMD指令和ENTRYPOINT指令之间的区别在于,与CMD指令不同,我们不能使用docker container run命令发送的命令行参数来覆盖ENTRYPOINT命令。

注意

--entrypoint标志可以与docker container run命令一起发送,以覆盖镜像的默认ENTRYPOINT

ENTRYPOINT指令的格式如下:

ENTRYPOINT ["executable","param1","param2","param3", ...]

CMD指令类似,ENTRYPOINT指令也允许我们提供默认的可执行文件和参数。我们可以在ENTRYPOINT指令中使用CMD指令来为可执行文件提供额外的参数。

在以下示例中,我们使用ENTRYPOINT指令将"echo"作为默认命令,将"Hello"作为默认参数。我们还使用CMD指令提供了"World"作为额外的参数:

ENTRYPOINT ["echo","Hello"]
CMD ["World"]

echo命令的输出将根据我们如何执行docker container run命令而有所不同。

如果我们启动 Docker 镜像而没有任何命令行参数,它将输出消息Hello World

$ docker container run <image>
Hello World

但是,如果我们使用额外的命令行参数(例如Docker)启动 Docker 镜像,输出消息将是Hello Docker

$ docker container run <image> "Docker"
Hello Docker

在进一步讨论Dockerfile指令之前,让我们从下一个练习开始创建我们的第一个Dockerfile

练习 2.01:创建我们的第一个 Dockerfile

在这个练习中,您将创建一个 Docker 镜像,可以打印您传递给 Docker 镜像的参数,前面加上文本You are reading。例如,如果您传递hello world,它将输出You are reading hello world。如果没有提供参数,则将使用The Docker Workshop作为标准值:

  1. 使用mkdir命令创建一个名为custom-docker-image的新目录。该目录将是您的 Docker 镜像的上下文上下文是包含成功构建镜像所需的所有文件的目录:
$ mkdir custom-docker-image
  1. 使用cd命令导航到新创建的custom-docker-image目录,因为我们将在此目录中创建构建过程中所需的所有文件(包括Dockerfile):
$ cd custom-docker-image
  1. custom-docker-image目录中,使用touch命令创建一个名为Dockerfile的文件:
$ touch Dockerfile
  1. 现在,使用您喜欢的文本编辑器打开Dockerfile
$ vim Dockerfile
  1. 将以下内容添加到Dockerfile中,保存并退出Dockerfile
# This is my first Docker image
FROM ubuntu 
LABEL maintainer=sathsara@mydomain.com 
RUN apt-get update
CMD ["The Docker Workshop"]
ENTRYPOINT ["echo", "You are reading"]

Docker 镜像将基于 Ubuntu 父镜像。然后,您可以使用LABEL指令提供Dockerfile作者的电子邮件地址。接下来的一行执行apt-get update命令,将 Debian 的软件包列表更新到最新可用版本。最后,您将使用ENTRYPOINTCMD指令来定义容器的默认可执行文件和参数。

我们已经提供了echo作为默认可执行文件,You are reading作为默认参数,不能使用命令行参数进行覆盖。此外,我们还提供了The Docker Workshop作为一个额外的参数,可以使用docker container run命令的命令行参数进行覆盖。

在这个练习中,我们使用了在前几节中学到的常见指令创建了我们的第一个Dockerfile。该过程的下一步是从Dockerfile构建 Docker 镜像。只有在从Dockerfile构建 Docker 镜像之后,才能运行 Docker 容器。在下一节中,我们将看看如何从Dockerfile构建 Docker 镜像。

构建 Docker 镜像

在上一节中,我们学习了如何创建Dockerfile。该过程的下一步是使用Dockerfile构建Docker 镜像

Docker 镜像是用于构建 Docker 容器的模板。这类似于如何可以使用房屋平面图从相同的设计中创建多个房屋。如果您熟悉面向对象编程的概念,Docker 镜像和 Docker 容器的关系与对象的关系相同。面向对象编程中的类可用于创建多个对象。

Docker 镜像是一个二进制文件,由Dockerfile中提供的多个层组成。这些层堆叠在彼此之上,每个层依赖于前一个层。每个层都是基于其下一层的更改而生成的。Docker 镜像的所有层都是只读的。一旦我们从 Docker 镜像创建一个 Docker 容器,将在其他只读层之上创建一个新的可写层,其中包含对容器文件系统所做的所有修改:

图 2.2:Docker 镜像层

图 2.2:Docker 镜像层

如前图所示,docker image build 命令将从Dockerfile创建一个 Docker 镜像。Docker 镜像的层将映射到Dockerfile中提供的指令。

这个图像构建过程是由 Docker CLI 发起并由 Docker 守护程序执行的。要生成 Docker 镜像,Docker 守护程序需要访问Dockerfile,源代码(例如index.html)和其他文件(例如属性文件),这些文件在Dockerfile中被引用。这些文件通常存储在一个被称为构建上下文的目录中。在执行 docker image build 命令时将指定此上下文。整个上下文将在图像构建过程中发送到 Docker 守护程序。

docker image build命令采用以下格式:

$ docker image build <context>

我们可以从包含Dockerfile和其他文件的文件夹中执行 docker image build 命令,如下例所示。请注意,命令末尾的点(.)用于表示当前目录:

$ docker image build.

让我们看看以下示例Dockerfile的 Docker 镜像构建过程:

FROM ubuntu:latest
LABEL maintainer=sathsara@mydomain.com
CMD ["echo","Hello World"]

这个Dockerfile使用最新的ubuntu镜像作为父镜像。然后,使用LABEL指令将sathsara@mydomain.com指定为维护者。最后,使用CMD指令将 echo"Hello World"用作图像的输出。

执行上述Dockerfile的 docker 镜像构建命令后,我们可以在构建过程中的控制台上看到类似以下的输出:

Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM ubuntu:latest
latest: Pulling from library/ubuntu
2746a4a261c9: Pull complete 
4c1d20cdee96: Pull complete 
0d3160e1d0de: Pull complete 
c8e37668deea: Pull complete
Digest: sha256:250cc6f3f3ffc5cdaa9d8f4946ac79821aafb4d3afc93928
        f0de9336eba21aa4
Status: Downloaded newer image for ubuntu:latest
 ---> 549b9b86cb8d
Step 2/3 : LABEL maintainer=sathsara@mydomain.com
 ---> Running in a4a11e5e7c27
Removing intermediate container a4a11e5e7c27
 ---> e3add5272e35
Step 3/3 : CMD ["echo","Hello World"]
 ---> Running in aad8a56fcdc5
Removing intermediate container aad8a56fcdc5
 ---> dc3d4fd77861
Successfully built dc3d4fd77861

输出的第一行是Sending build context to Docker daemon,这表明构建开始时将构建上下文发送到 Docker 守护程序。上下文中的所有文件将被递归地发送到 Docker 守护程序(除非明确要求忽略某些文件)。

接下来,有Step 1/3Step 2/3的步骤,对应于Dockerfile中的指令。作为第一步,Docker 守护程序将下载父镜像。在上述输出中,从 library/ubuntu 拉取表示这一点。对于Dockerfile的每一行,都会创建一个新的中间容器来执行指令,一旦这一步完成,这个中间容器将被移除。Running in a4a11e5e7c27Removing intermediate container a4a11e5e7c27这两行用于表示这一点。最后,当构建完成且没有错误时,将打印出Successfully built dc3d4fd77861这一行。这行打印出了新构建的 Docker 镜像的 ID。

现在,我们可以使用docker image list命令列出可用的 Docker 镜像:

$ docker image list

此列表包含了本地构建的 Docker 镜像和从远程 Docker 仓库拉取的 Docker 镜像:

REPOSITORY   TAG       IMAGE ID        CREATED          SIZE
<none>       <none>    dc3d4fd77861    3 minutes ago    64.2MB
ubuntu       latest    549b9b86cb8d    5 days ago       64.2MB

如上述输出所示,我们可以看到两个 Docker 镜像。第一个 Docker 镜像的 IMAGE ID 是dc3d4fd77861,是在构建过程中本地构建的 Docker 镜像。我们可以看到,这个IMAGE IDdocker image build命令的最后一行中的 ID 是相同的。下一个镜像是我们用作自定义镜像的父镜像的 ubuntu 镜像。

现在,让我们再次使用docker image build命令构建 Docker 镜像:

$ docker image build
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM ubuntu:latest
 ---> 549b9b86cb8d
Step 2/3 : LABEL maintainer=sathsara@mydomain.com
 ---> Using cache
 ---> e3add5272e35
Step 3/3 : CMD ["echo","Hello World"]
 ---> Using cache
 ---> dc3d4fd77861
Successfully built dc3d4fd77861

这次,镜像构建过程是瞬时的。这是因为缓存。由于我们没有改变Dockerfile的任何内容,Docker 守护程序利用了缓存,并重用了本地镜像缓存中的现有层来加速构建过程。我们可以在上述输出中看到,这次使用了缓存,有Using cache行可用。

Docker 守护程序将在启动构建过程之前执行验证步骤,以确保提供的Dockerfile在语法上是正确的。在语法无效的情况下,构建过程将失败,并显示来自 Docker 守护程序的错误消息:

$ docker image build
Sending build context to Docker daemon  2.048kB
Error response from daemon: Dockerfile parse error line 5: 
unknown instruction: INVALID

现在,让我们使用docker image list命令重新查看本地可用的 Docker 镜像:

$ docker image list

该命令应返回以下输出:

REPOSITORY    TAG       IMAGE ID         CREATED          SIZE
<none>        <none>    dc3d4fd77861     3 minutes ago    64.2MB
ubuntu        latest    549b9b86cb8d     5 days ago       64.2MB

请注意,我们的自定义 Docker 镜像没有名称。这是因为我们在构建过程中没有指定任何存储库或标签。我们可以使用 docker image tag 命令为现有镜像打标签。

让我们用IMAGE ID dc3d4fd77861作为my-tagged-image:v1.0来为我们的镜像打标签:

$ docker image tag dc3d4fd77861 my-tagged-image:v1.0

现在,如果我们再次列出我们的镜像,我们可以看到REPOSITORYTAG列下的 Docker 镜像名称和标签:

REPOSITORY        TAG       IMAGE ID        CREATED         SIZE
my-tagged-image   v1.0      dc3d4fd77861    20 minutes ago  64.2MB
ubuntu            latest    549b9b86cb8d    5 days ago      64.2MB

我们还可以通过指定-t标志在构建过程中为镜像打标签:

$ docker image build -t my-tagged-image:v2.0 .

上述命令将打印以下输出:

Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM ubuntu:latest
 ---> 549b9b86cb8d
Step 2/3 : LABEL maintainer=sathsara@mydomain.com
 ---> Using cache
 ---> e3add5272e35
Step 3/3 : CMD ["echo","Hello World"]
 ---> Using cache
 ---> dc3d4fd77861
Successfully built dc3d4fd77861
Successfully tagged my-tagged-image:v2.0

这一次,除了成功构建 dc3d4fd77861行之外,我们还可以看到成功标记 my-tagged-image:v2.0行,这表明我们的 Docker 镜像已经打了标签。

在本节中,我们学习了如何从Dockerfile构建 Docker 镜像。我们讨论了Dockerfile和 Docker 镜像之间的区别。然后,我们讨论了 Docker 镜像由多个层组成。我们还体验了缓存如何加速构建过程。最后,我们为 Docker 镜像打了标签。

在下一个练习中,我们将从练习 2.01:创建我们的第一个 Dockerfile中创建的Dockerfile构建 Docker 镜像。

练习 2.02:创建我们的第一个 Docker 镜像

在这个练习中,您将从练习 2.01:创建我们的第一个 Dockerfile中创建的Dockerfile构建 Docker 镜像,并从新构建的镜像运行 Docker 容器。首先,您将在不传递任何参数的情况下运行 Docker 镜像,期望输出为“您正在阅读 Docker Workshop”。接下来,您将以Docker Beginner's Guide作为参数运行 Docker 镜像,并期望输出为“您正在阅读 Docker Beginner's Guide”:

  1. 首先,请确保您在练习 2.01:创建我们的第一个 Dockerfile中创建的custom-docker-image目录中。确认该目录包含在练习 2.01:创建我们的第一个 Dockerfile中创建的以下Dockerfile
# This is my first Docker image
FROM ubuntu 
LABEL maintainer=sathsara@mydomain.com 
RUN apt-get update
CMD ["The Docker Workshop"]
ENTRYPOINT ["echo", "You are reading"]
  1. 使用docker image build命令构建 Docker 镜像。此命令具有可选的-t标志,用于指定镜像的标签。将您的镜像标记为welcome:1.0
$ docker image build -t welcome:1.0 .

注意

不要忘记在前述命令的末尾加上点(.),用于将当前目录作为构建上下文。

可以从以下输出中看到,在构建过程中执行了Dockerfile中提到的所有五个步骤。输出的最后两行表明成功构建并打了标签的镜像:

图 2.3:构建 welcome:1.0 Docker 镜像

图 2.3:构建 welcome:1.0 Docker 镜像

  1. 再次构建此镜像,而不更改Dockerfile内容:
$ docker image build -t welcome:2.0 .

请注意,由于使用了缓存,此构建过程比以前的过程快得多:

图 2.4:使用缓存构建 welcome:1.0 Docker 镜像

图 2.4:使用缓存构建 welcome:1.0 Docker 镜像

  1. 使用docker image list命令列出计算机上所有可用的 Docker 镜像:
$ docker image list

这些镜像可以在您的计算机上使用,无论是从 Docker 注册表中拉取还是在您的计算机上构建:

REPOSITORY   TAG      IMAGE ID        CREATED          SIZE
welcome      1.0      98f571a42e5c    23 minutes ago   91.9MB
welcome      2.0      98f571a42e5c    23 minutes ago   91.9MB
ubuntu       latest   549b9b86cb8d    2 weeks ago      64.2MB

从前述输出中可以看出,有三个 Docker 镜像可用。ubuntu镜像是从 Docker Hub 拉取的,welcome镜像的1.02.0版本是在您的计算机上构建的。

  1. 执行docker container run命令,从您在步骤 1中构建的 Docker 镜像(welcome:1.0)启动一个新容器:
$ docker container run welcome:1.0

输出应如下所示:

You are reading The Docker Workshop

您将收到预期的输出You are reading The Docker WorkshopYou are reading是由ENTRYPOINT指令提供的参数引起的,The Docker Workshop来自CMD指令提供的参数。

  1. 最后,再次执行docker container run命令,这次使用命令行参数:
$ docker container run welcome:1.0 "Docker Beginner's Guide"

由于命令行参数Docker 初学者指南ENTRYPOINT指令中提供的You are reading参数,您将获得输出You are reading Docker 初学者指南

You are reading Docker Beginner's Guide

在这个练习中,我们学习了如何使用Dockerfile构建自定义 Docker 镜像,并从镜像运行 Docker 容器。在下一节中,我们将学习可以在Dockerfile中使用的其他 Docker 指令。

其他 Dockerfile 指令

在 Dockerfile 中的常见指令部分,我们讨论了可用于Dockerfile的常见指令。在该部分中,我们讨论了FROMLABELRUNCMDENTRYPOINT指令以及如何使用它们创建一个简单的Dockerfile

在本节中,我们将讨论更高级的Dockerfile指令。这些指令可以用于创建更高级的 Docker 镜像。例如,我们可以使用VOLUME指令将主机机器的文件系统绑定到 Docker 容器。这将允许我们持久化 Docker 容器生成和使用的数据。另一个例子是HEALTHCHECK指令,它允许我们定义健康检查以评估 Docker 容器的健康状态。在本节中,我们将研究以下指令:

  1. ENV指令

  2. ARG指令

  3. WORKDIR指令

  4. COPY指令

  5. ADD指令

  6. USER指令

  7. VOLUME指令

  8. EXPOSE指令

  9. HEALTHCHECK指令

  10. ONBUILD指令

ENV 指令

Dockerfile中的 ENV 指令用于设置环境变量。环境变量被应用程序和进程用来获取有关进程运行环境的信息。一个例子是PATH环境变量,它列出了要搜索可执行文件的目录。

环境变量按以下格式定义为键值对:

ENV <key> <value>

PATH 环境变量设置为以下值:

$PATH:/usr/local/myapp/bin/

因此,可以使用ENV指令设置如下:

ENV PATH $PATH:/usr/local/myapp/bin/

我们可以在同一行中用空格分隔设置多个环境变量。但是,在这种形式中,keyvalue应该由等号(=)分隔:

ENV <key>=<value> <key>=<value> ...

在下面的示例中,配置了两个环境变量。PATH环境变量配置为$PATH:/usr/local/myapp/bin/的值,VERSION环境变量配置为1.0.0的值:

ENV PATH=$PATH:/usr/local/myapp/bin/ VERSION=1.0.0

一旦使用Dockerfile中的ENV指令设置了环境变量,该变量就会在所有后续的 Docker 镜像层中可用。甚至在从此 Docker 镜像启动的 Docker 容器中也可用。

在下一节中,我们将研究ARG指令。

ARG 指令

ARG指令用于定义用户可以在构建时传递的变量。ARG是唯一可以在Dockerfile中的FROM指令之前出现的指令。

用户可以在构建 Docker 镜像时使用--build-arg <varname>=<value>传递值,如下所示:

$ docker image build -t <image>:<tag> --build-arg <varname>=<value> .

ARG指令的格式如下:

ARG <varname>

Dockerfile中可以有多个ARG指令,如下所示:

ARG USER
ARG VERSION

ARG指令也可以定义一个可选的默认值。如果在构建时没有传递值,将使用此默认值:

ARG USER=TestUser
ARG VERSION=1.0.0

ENV变量不同,ARG变量无法从正在运行的容器中访问。它们仅在构建过程中可用。

在下一个练习中,我们将利用迄今为止所学到的知识,在Dockerfile中使用ENVARG指令。

练习 2.03:在 Dockerfile 中使用 ENV 和 ARG 指令

您的经理要求您创建一个Dockerfile,该文件将使用 ubuntu 作为父镜像,但您应该能够在构建时更改 ubuntu 版本。您还需要指定发布者的名称和 Docker 镜像的应用程序目录作为环境变量。您将使用Dockerfile中的ENVARG指令来执行此练习:

  1. 使用mkdir命令创建一个名为env-arg-exercise的新目录:
mkdir env-arg-exercise
  1. 使用cd命令导航到新创建的env-arg-exercise目录:
cd env-arg-exercise
  1. env-arg-exercise目录中,创建一个名为Dockerfile的文件:
touch Dockerfile
  1. 现在,使用您喜欢的文本编辑器打开Dockerfile
vim Dockerfile
  1. 将以下内容添加到Dockerfile中。然后,保存并退出Dockerfile
# ENV and ARG example
ARG TAG=latest
FROM ubuntu:$TAG
LABEL maintainer=sathsara@mydomain.com 
ENV PUBLISHER=packt APP_DIR=/usr/local/app/bin
CMD ["env"]

Dockerfile首先定义了一个名为TAG的参数,其默认值为最新版本。接下来是FROM指令,它将使用带有TAG变量值的 ubuntu 父镜像与build命令一起发送(或者如果没有使用build命令发送值,则使用默认值)。然后,LABEL指令设置了维护者的值。接下来是ENV指令,它使用值packt定义了PUBLISHER的环境变量,并使用值/usr/local/app/bin定义了APP_DIR的环境变量。最后,使用CMD指令执行env命令,该命令将打印所有环境变量。

  1. 现在,构建 Docker 镜像:
$ docker image build -t env-arg --build-arg TAG=19.04 .

注意使用env-arg --build-arg TAG=19.04标志将TAG参数发送到构建过程中。输出应如下所示:

图 2.5:构建 env-arg Docker 镜像

图 2.5:构建 env-arg Docker 镜像

请注意,在构建过程中使用了 ubuntu 镜像的19.04标签作为父镜像。这是因为您在构建过程中使用了--build-arg标志,并设置了值为TAG=19.04

  1. 现在,执行docker container run命令,从您在上一步中构建的 Docker 镜像启动一个新的容器:
$ docker container run env-arg

从输出中我们可以看到,PUBLISHER环境变量的值为packtAPP_DIR环境变量的值为/usr/local/app/bin

图 2.6:运行 env-arg Docker 容器

图 2.6:运行 env-arg Docker 容器

在这个练习中,我们使用ENV指令为 Docker 镜像定义了环境变量。我们还体验了如何在 Docker 镜像构建时使用ARG指令传递值。在下一节中,我们将介绍WORKDIR指令,它可以用来定义 Docker 容器的当前工作目录。

工作目录指令

WORKDIR指令用于指定 Docker 容器的当前工作目录。任何后续的ADDCMDCOPYENTRYPOINTRUN指令都将在此目录中执行。WORKDIR指令的格式如下:

WORKDIR /path/to/workdir

如果指定的目录不存在,Docker 将创建此目录并将其设置为当前工作目录,这意味着该指令隐式执行mkdircd命令。

Dockerfile中可以有多个WORKDIR指令。如果在后续的WORKDIR指令中提供了相对路径,那么它将相对于前一个WORKDIR指令设置的工作目录。

WORKDIR /one
WORKDIR two
WORKDIR three
RUN pwd

在前面的例子中,我们在Dockerfile的末尾使用pwd命令来打印当前工作目录。pwd命令的输出将是/one/two/three

在下一节中,我们将讨论COPY指令,该指令用于将文件从本地文件系统复制到 Docker 镜像文件系统。

复制指令

在 Docker 镜像构建过程中,我们可能需要将文件从本地文件系统复制到 Docker 镜像文件系统。这些文件可以是源代码文件(例如 JavaScript 文件)、配置文件(例如属性文件)或者构件(例如 JAR 文件)。在构建过程中,可以使用COPY指令将文件和文件夹从本地文件系统复制到 Docker 镜像。该指令有两个参数。第一个是本地文件系统的源路径,第二个是镜像文件系统上的目标路径:

COPY <source> <destination>

在下面的例子中,我们使用COPY指令将index.html文件从本地文件系统复制到 Docker 镜像的/var/www/html/目录中:

COPY index.html /var/www/html/index.html

通配符也可以用来指定匹配给定模式的所有文件。以下示例将把当前目录中所有扩展名为.html的文件复制到 Docker 镜像的/var/www/html/目录中:

COPY *.html /var/www/html/

除了复制文件外,--chown标志还可以与COPY指令一起使用,以指定文件的用户和组所有权:

COPY --chown=myuser:mygroup *.html /var/www/html/

在上面的例子中,除了将所有 HTML 文件从当前目录复制到/var/www/html/目录外,--chown标志还用于设置文件所有权,用户为myuser,组为mygroup

注意

--chown标志仅在 Docker 版本 17.09 及以上版本中受支持。对于低于 17.09 版本的 Docker,您需要在COPY命令之后运行chown命令来更改文件所有权。

在下一节中,我们将看一下ADD指令。

ADD 指令

ADD指令也类似于COPY指令,格式如下:

ADD <source> <destination>

但是,除了COPY指令提供的功能外,ADD指令还允许我们将 URL 用作<source>参数:

ADD http://sample.com/test.txt /tmp/test.txt

在上面的例子中,ADD指令将从http://sample.com下载test.txt文件,并将文件复制到 Docker 镜像文件系统的/tmp目录中。

ADD指令的另一个特性是自动提取压缩文件。如果我们将一个压缩文件(gzip、bzip2、tar 等)添加到<source>参数中,ADD指令将会提取存档并将内容复制到镜像文件系统中。

假设我们有一个名为html.tar.gz的压缩文件,其中包含index.htmlcontact.html文件。以下命令将提取html.tar.gz文件,并将index.htmlcontact.html文件复制到/var/www/html目录:

ADD html.tar.gz /var/www/html

由于COPYADD指令提供几乎相同的功能,建议始终使用COPY指令,除非您需要ADD指令提供的附加功能(从 URL 添加或提取压缩文件)。这是因为ADD指令提供了额外的功能,如果使用不正确,可能会表现出不可预测的行为(例如,在想要提取文件时复制文件,或者在想要复制文件时提取文件)。

在下一个练习中,我们将使用WORKDIRCOPYADD指令将文件复制到 Docker 镜像中。

练习 2.04:在 Dockerfile 中使用 WORKDIR,COPY 和 ADD 指令

在这个练习中,您将部署自定义的 HTML 文件到 Apache Web 服务器。您将使用 Ubuntu 作为基础镜像,并在其上安装 Apache。然后,您将将自定义的 index.html 文件复制到 Docker 镜像,并从 https://www.docker.com 网站下载 Docker 标志,以与自定义的 index.html 文件一起使用:

  1. 使用mkdir命令创建一个名为workdir-copy-add-exercise的新目录:
mkdir workdir-copy-add-exercise
  1. 导航到新创建的workdir-copy-add-exercise目录:
cd workdir-copy-add-exercise
  1. workdir-copy-add-exercise目录中,创建一个名为index.html的文件。此文件将在构建时复制到 Docker 镜像中:
touch index.html 
  1. 现在,使用您喜欢的文本编辑器打开index.html
vim index.html 
  1. 将以下内容添加到index.html文件中,保存并退出index.html
<html>
  <body>
    <h1>Welcome to The Docker Workshop</h1>
    <img src="logo.png" height="350" width="500"/>
  </body>
</html>

此 HTML 文件将在页面的标题中输出“欢迎来到 Docker 工作坊”,并作为图像输出logo.png(我们将在 Docker 镜像构建过程中下载)。您已经定义了logo.png图像的高度为350,宽度为500

  1. workdir-copy-add-exercise目录中,创建一个名为Dockerfile的文件:
touch Dockerfile
  1. 现在,使用您喜欢的文本编辑器打开Dockerfile
vim Dockerfile
  1. 将以下内容添加到Dockerfile中,保存并退出Dockerfile
# WORKDIR, COPY and ADD example
FROM ubuntu:latest 
RUN apt-get update && apt-get install apache2 -y 
WORKDIR /var/www/html/
COPY index.html .
ADD https://www.docker.com/sites/default/files/d8/2019-07/  Moby-logo.png ./logo.png
CMD ["ls"]

这个Dockerfile首先将 ubuntu 镜像定义为父镜像。下一行是RUN指令,它将执行apt-get update来更新软件包列表,以及apt-get install apache2 -y来安装 Apache HTTP 服务器。然后,您将设置/var/www/html/为工作目录。接下来,将我们在步骤 3中创建的index.html文件复制到 Docker 镜像中。然后,使用ADD指令从www.docker.com/sites/default/files/d8/2019-07/Moby-logo.png下载 Docker 标志到 Docker 镜像中。最后一步是使用ls命令打印/var/www/html/目录的内容。

  1. 现在,使用标签workdir-copy-add构建 Docker 镜像:
$ docker image build -t workdir-copy-add .

您会注意到,由于我们没有明确为镜像打标签,因此该镜像已成功构建并标记为latest

图 2.7:使用 WORKDIR、COPY 和 ADD 指令构建 Docker 镜像

图 2.7:使用 WORKDIR、COPY 和 ADD 指令构建 Docker 镜像

  1. 执行docker container run命令,从您在上一步中构建的 Docker 镜像启动一个新的容器:
$ docker container run workdir-copy-add

从输出中可以看到,index.htmllogo.png文件都在/var/www/html/目录中可用:

index.html
logo.png

在这个练习中,我们观察了WORKDIRADDCOPY指令在 Docker 中的工作方式。在下一节中,我们将讨论USER指令。

USER 指令

Docker 将使用 root 用户作为 Docker 容器的默认用户。我们可以使用USER指令来改变这种默认行为,并指定一个非 root 用户作为 Docker 容器的默认用户。这是通过以非特权用户身份运行 Docker 容器来提高安全性的好方法。在Dockerfile中使用USER指令指定的用户名将用于运行所有后续的RUNCMDENTRYPOINT指令。

USER指令采用以下格式:

USER <user>

除了用户名之外,我们还可以指定可选的组名来运行 Docker 容器:

USER <user>:<group>

我们需要确保<user><group>的值是有效的用户和组名。否则,Docker 守护程序在尝试运行容器时会抛出错误:

docker: Error response from daemon: unable to find user my_user: 
        no matching entries in passwd file.

现在,让我们在下一个练习中尝试使用USER指令。

练习 2.05:在 Dockerfile 中使用 USER 指令

您的经理要求您创建一个 Docker 镜像来运行 Apache Web 服务器。由于安全原因,他特别要求您在运行 Docker 容器时使用非 root 用户。在这个练习中,您将使用Dockerfile中的USER指令来设置默认用户。您将安装 Apache Web 服务器并将用户更改为www-data。最后,您将执行whoami命令来验证当前用户的用户名:

注意

www-data用户是 Ubuntu 上 Apache Web 服务器的默认用户。

  1. 为这个练习创建一个名为user-exercise的新目录:
mkdir user-exercise
  1. 导航到新创建的user-exercise目录:
cd user-exercise
  1. user-exercise目录中,创建一个名为Dockerfile的文件:
touch Dockerfile
  1. 现在,用你喜欢的文本编辑器打开Dockerfile
vim Dockerfile
  1. 将以下内容添加到Dockerfile中,保存并退出Dockerfile
# USER example
FROM ubuntu
RUN apt-get update && apt-get install apache2 -y 
USER www-data
CMD ["whoami"]

这个Dockerfile首先将 Ubuntu 镜像定义为父镜像。下一行是RUN指令,它将执行apt-get update来更新软件包列表,以及apt-get install apache2 -y来安装 Apache HTTP 服务器。接下来,您使用USER指令将当前用户更改为www-data用户。最后,您有CMD指令,它执行whoami命令,将打印当前用户的用户名。

  1. 构建 Docker 镜像:
$ docker image build -t user .

输出应该如下:

图 2.8:构建用户 Docker 镜像

图 2.8:构建用户 Docker 镜像

  1. 现在,执行docker container run 命令来从我们在上一步中构建的 Docker 镜像启动一个新的容器:
$ docker container run user

如您从以下输出中所见,www-data是与 Docker 容器关联的当前用户:

www-data

在这个练习中,我们在Dockerfile中实现了USER指令,将www-data用户设置为 Docker 镜像的默认用户。

在下一节中,我们将讨论VOLUME指令。

VOLUME 指令

在 Docker 中,Docker 容器生成和使用的数据(例如文件、可执行文件)将存储在容器文件系统中。当我们删除容器时,所有数据都将丢失。为了解决这个问题,Docker 提出了卷的概念。卷用于持久化数据并在容器之间共享数据。我们可以在Dockerfile中使用VOLUME指令来创建 Docker 卷。一旦在 Docker 容器中创建了VOLUME,底层主机将创建一个映射目录。Docker 容器的卷挂载的所有文件更改将被复制到主机机器的映射目录中。

VOLUME指令通常以 JSON 数组作为参数:

VOLUME ["/path/to/volume"]

或者,我们可以指定一个包含多个路径的普通字符串:

VOLUME /path/to/volume1 /path/to/volume2

我们可以使用docker container inspect <container>命令查看容器中可用的卷。docker 容器 inspect 命令的输出 JSON 将打印类似以下内容的卷信息:

"Mounts": [
    {
        "Type": "volume",
        "Name": "77db32d66407a554bd0dbdf3950671b658b6233c509ea
ed9f5c2a589fea268fe",
        "Source": "/var/lib/docker/volumes/77db32d66407a554bd0
dbdf3950671b658b6233c509eaed9f5c2a589fea268fe/_data",
        "Destination": "/path/to/volume",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
],

根据前面的输出,Docker 为卷指定了一个唯一的名称。此外,输出中还提到了卷的源路径和目标路径。

此外,我们可以执行docker volume inspect <volume>命令来显示有关卷的详细信息:

[
    {
        "CreatedAt": "2019-12-28T12:52:52+05:30",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/77db32d66407a554
bd0dbdf3950671b658b6233c509eaed9f5c2a589fea268fe/_data",
        "Name": "77db32d66407a554bd0dbdf3950671b658b6233c509eae
d9f5c2a589fea268fe",
        "Options": null,
        "Scope": "local"
    }
]

这也类似于先前的输出,具有相同的唯一名称和卷的挂载路径。

在下一个练习中,我们将学习如何在Dockerfile中使用VOLUME指令。

练习 2.06:在 Dockerfile 中使用 VOLUME 指令

在这个练习中,您将设置一个 Docker 容器来运行 Apache Web 服务器。但是,您不希望在 Docker 容器失败时丢失 Apache 日志文件。作为解决方案,您决定通过将 Apache 日志路径挂载到底层 Docker 主机来持久保存日志文件。

  1. 创建一个名为volume-exercise的新目录:
mkdir volume-exercise
  1. 转到新创建的volume-exercise目录:
cd volume-exercise
  1. volume-exercise目录中,创建一个名为Dockerfile的文件:
touch Dockerfile
  1. 现在,使用您喜欢的文本编辑器打开Dockerfile
vim Dockerfile
  1. 将以下内容添加到Dockerfile中,保存并退出Dockerfile
# VOLUME example
FROM ubuntu
RUN apt-get update && apt-get install apache2 -y
VOLUME ["/var/log/apache2"]

这个Dockerfile首先定义了 Ubuntu 镜像作为父镜像。接下来,您将执行apt-get update命令来更新软件包列表,以及apt-get install apache2 -y命令来安装 Apache Web 服务器。最后,使用VOLUME指令来设置一个挂载点到/var/log/apache2目录。

  1. 现在,构建 Docker 镜像:
$ docker image build -t volume .

输出应该如下:

图 2.9:构建卷 Docker 镜像

图 2.9:构建卷 Docker 镜像

  1. 执行 docker 容器运行命令,从您在上一步构建的 Docker 镜像中启动一个新的容器。请注意,您正在使用--interactive--tty标志来打开一个交互式的 bash 会话,以便您可以从 Docker 容器的 bash shell 中执行命令。您还使用了--name标志来将容器名称定义为volume-container
$ docker container run --interactive --tty --name volume-container volume /bin/bash

您的 bash shell 将会被打开如下:

root@bc61d46de960: /#
  1. 从 Docker 容器命令行,切换到/var/log/apache2/目录:
# cd /var/log/apache2/

这将产生以下输出:

root@bc61d46de960: /var/log/apache2#
  1. 现在,列出目录中可用的文件:
# ls -l

输出应该如下:

图 2.10:列出/var/log/apache2 目录的文件

图 2.10:列出/var/log/apache2 目录的文件

这些是 Apache 在运行过程中创建的日志文件。一旦您检查了该卷的主机挂载,相同的文件应该也是可用的。

  1. 现在,退出容器以检查主机文件系统:
# exit
  1. 检查volume-container以查看挂载信息:
$ docker container inspect volume-container

在"Mounts"键下,您可以看到与挂载相关的信息:

图 2.11:检查 Docker 容器

图 2.11:检查 Docker 容器

  1. 使用docker volume inspect <volume_name>命令来检查卷。<volume_name>可以通过前面输出的Name字段来识别:
$ docker volume inspect 354d188e0761d82e1e7d9f3d5c6ee644782b7150f51cead8f140556e5d334bd5

您应该会得到类似以下的输出:

图 2.12:检查 Docker 卷

图 2.12:检查 Docker 卷

我们可以看到容器被挂载到"/var/lib/docker/volumes/354d188e0761d82e1e7d9f3d5c6ee644782b 7150f51cead8f140556e5d334bd5/_data"的主机路径上,这在前面的输出中被定义为Mountpoint字段。

  1. 列出主机文件路径中可用的文件。主机文件路径可以通过前面输出的"Mountpoint"字段来识别:
$ sudo ls -l /var/lib/docker/volumes/354d188e0761d82e1e7d9f3d5c6ee644782b7150f51cead8f14 0556e5d334bd5/_data

在下面的输出中,您可以看到容器中/var/log/apache2目录中的日志文件被挂载到主机上:

图 2.13:列出挂载点目录中的文件

图 2.13:列出挂载点目录中的文件

在这个练习中,我们观察了如何使用VOLUME指令将 Apache Web 服务器的日志路径挂载到主机文件系统上。在下一节中,我们将学习EXPOSE指令。

EXPOSE 指令

EXPOSE指令用于通知 Docker 容器在运行时监听指定端口。我们可以使用EXPOSE指令通过 TCP 或 UDP 协议公开端口。EXPOSE指令的格式如下:

EXPOSE <port>

然而,使用EXPOSE指令公开的端口只能从其他 Docker 容器内部访问。要将这些端口公开到 Docker 容器外部,我们可以使用docker container run命令的-p标志来发布端口:

docker container run -p <host_port>:<container_port> <image>

举个例子,假设我们有两个容器。一个是 NodeJS Web 应用容器,应该通过端口80从外部访问。第二个是 MySQL 容器,应该通过端口3306从 Node 应用容器访问。在这种情况下,我们必须使用EXPOSE指令公开 NodeJS 应用的端口80,并在运行容器时使用docker container run命令和-p标志来将其公开到外部。然而,对于 MySQL 容器,我们在运行容器时只能使用EXPOSE指令,而不使用-p标志,因为3306端口只能从 Node 应用容器访问。

因此,总结来说,以下陈述定义了这个指令:

  • 如果我们同时指定EXPOSE指令和-p标志,公开的端口将可以从其他容器以及外部访问。

  • 如果我们不使用-p标志来指定EXPOSE,那么公开的端口只能从其他容器访问,而无法从外部访问。

在下一节中,您将学习HEALTHCHECK指令。

HEALTHCHECK 指令

在 Docker 中使用健康检查来检查容器是否正常运行。例如,我们可以使用健康检查来确保应用程序在 Docker 容器内部运行。除非指定了健康检查,否则 Docker 无法判断容器是否健康。如果在生产环境中运行 Docker 容器,这一点非常重要。HEALTHCHECK指令的格式如下:

HEALTHCHECK [OPTIONS] CMD command

Dockerfile中只能有一个HEALTHCHECK指令。如果有多个HEALTHCHECK指令,只有最后一个会生效。

例如,我们可以使用以下指令来确保容器可以在http://localhost/端点接收流量:

HEALTHCHECK CMD curl -f http://localhost/ || exit 1

在上一个命令的最后,退出代码用于指定容器的健康状态。01是此字段的有效值。0 用于表示健康的容器,1用于表示不健康的容器。

除了命令,我们可以在HEALTHCHECK指令中指定一些其他参数,如下所示:

  • --interval:指定每次健康检查之间的时间间隔(默认为 30 秒)。

  • --timeout:如果在此期间未收到成功响应,则健康检查被视为失败(默认为 30 秒)。

  • --start-period:在运行第一次健康检查之前等待的持续时间。这用于为容器提供启动时间(默认为 0 秒)。

  • --retries:如果健康检查连续失败给定次数的重试(默认为 3 次),则容器将被视为不健康。

在下面的示例中,我们通过使用HEALTHCHECK指令提供我们的自定义值来覆盖了默认值:

HEALTHCHECK --interval=1m --timeout=2s --start-period=2m --retries=3 \    CMD curl -f http://localhost/ || exit 1

我们可以使用docker container list命令来检查容器的健康状态。这将在STATUS列下列出健康状态:

CONTAINER ID  IMAGE     COMMAND                  CREATED
  STATUS                        PORTS                NAMES
d4e627acf6ec  sample    "apache2ctl -D FOREG…"   About a minute ago
  Up About a minute (healthy)   0.0.0.0:80->80/tcp   upbeat_banach

一旦我们启动容器,健康状态将是健康:启动中。成功执行HEALTHCHECK命令后,状态将变为健康

在下一个练习中,我们将使用EXPOSEHEALTHCHECK指令来创建一个带有 Apache web 服务器的 Docker 容器,并为其定义健康检查。

练习 2.07:在 Dockerfile 中使用 EXPOSE 和 HEALTHCHECK 指令

你的经理要求你将 Apache web 服务器 docker 化,以便从 Web 浏览器访问 Apache 首页。此外,他要求你配置健康检查以确定 Apache web 服务器的健康状态。在这个练习中,你将使用EXPOSEHEALTHCHECK指令来实现这个目标:

  1. 创建一个名为expose-healthcheck的新目录:
mkdir expose-healthcheck
  1. 导航到新创建的expose-healthcheck目录:
cd expose-healthcheck
  1. expose-healthcheck目录中,创建一个名为Dockerfile的文件:
touch Dockerfile
  1. 现在,用你喜欢的文本编辑器打开Dockerfile
vim Dockerfile
  1. 将以下内容添加到Dockerfile中,保存并退出Dockerfile
# EXPOSE & HEALTHCHECK example
FROM ubuntu
RUN apt-get update && apt-get install apache2 curl -y 
HEALTHCHECK CMD curl -f http://localhost/ || exit 1
EXPOSE 80
ENTRYPOINT ["apache2ctl", "-D", "FOREGROUND"]

这个Dockerfile首先将 ubuntu 镜像定义为父镜像。接下来,我们执行apt-get update命令来更新软件包列表,以及apt-get install apache2 curl -y命令来安装 Apache web 服务器和 curl 工具。Curl是执行HEALTHCHECK命令所需的。接下来,我们使用 curl 将HEALTHCHECK指令定义为http://localhost/端点。然后,我们暴露了 Apache web 服务器的端口80,以便我们可以从网络浏览器访问首页。最后,我们使用ENTRYPOINT指令启动了 Apache web 服务器。

  1. 现在,构建 Docker 镜像:
$ docker image build -t expose-healthcheck.

您应该会得到以下输出:

图 2.14:构建 expose-healthcheck Docker 镜像

图 2.14:构建 expose-healthcheck Docker 镜像

  1. 执行docker container run命令,从前一步构建的 Docker 镜像启动一个新的容器。请注意,您使用了-p标志将主机的端口80重定向到容器的端口80。此外,您使用了--name标志将容器名称指定为expose-healthcheck-container,并使用了-d标志以分离模式运行容器(这将在后台运行容器):
$ docker container run -p 80:80 --name expose-healthcheck-container -d expose-healthcheck
  1. 使用docker container list命令列出正在运行的容器:
$ docker container list

在下面的输出中,您可以看到expose-healthcheck-containerSTATUS为健康:

图 2.15:运行容器列表

图 2.15:运行容器列表

  1. 现在,您应该能够查看 Apache 首页。从您喜欢的网络浏览器转到http://127.0.0.1端点:图 2.16:Apache 首页

图 2.16:Apache 首页

  1. 现在清理容器。首先,使用docker container stop命令停止 Docker 容器:
$ docker container stop expose-healthcheck-container
  1. 最后,使用docker container rm命令删除 Docker 容器:
$ docker container rm expose-healthcheck-container

在这个练习中,您利用了EXPOSE指令将 Apache web 服务器暴露为 Docker 容器,并使用了HEALTHCHECK指令来定义一个健康检查,以验证 Docker 容器的健康状态。

在下一节中,我们将学习ONBUILD指令。

ONBUILD 指令

ONBUILD指令用于在Dockerfile中创建可重用的 Docker 镜像,该镜像将用作另一个 Docker 镜像的基础。例如,我们可以创建一个包含所有先决条件的 Docker 镜像,如依赖和配置,以便运行一个应用程序。然后,我们可以使用这个“先决条件”镜像作为父镜像来运行应用程序。

在创建先决条件镜像时,我们可以使用ONBUILD指令,该指令将包括应仅在此镜像作为另一个Dockerfile中的父镜像时执行的指令。ONBUILD指令在构建包含ONBUILD指令的Dockerfile时不会被执行,而只有在构建子镜像时才会执行。

ONBUILD指令采用以下格式:

ONBUILD <instruction>

举个例子,假设我们的自定义基础镜像的Dockerfile中有以下ONBUILD指令:

ONBUILD ENTRYPOINT ["echo","Running ONBUILD directive"]

如果我们从自定义基础镜像创建一个 Docker 容器,那么"Running ONBUILD directive"值将不会被打印出来。然而,如果我们将我们的自定义基础镜像用作新的子 Docker 镜像的基础,那么"Running ONBUILD directive"值将被打印出来。

我们可以使用docker image inspect命令来列出父镜像的 OnBuild 触发器:

$ docker image inspect <parent-image>

该命令将返回类似以下的输出:

...
"OnBuild": [
    "CMD [\"echo\",\"Running ONBUILD directive\"]"
]
...

在下一个练习中,我们将使用ONBUILD指令来定义一个 Docker 镜像来部署 HTML 文件。

练习 2.08:在 Dockerfile 中使用 ONBUILD 指令

你的经理要求你创建一个能够运行软件开发团队提供的任何 HTML 文件的 Docker 镜像。在这个练习中,你将构建一个带有 Apache Web 服务器的父镜像,并使用ONBUILD指令来复制 HTML 文件。软件开发团队可以使用这个 Docker 镜像作为父镜像来部署和测试他们创建的任何 HTML 文件。

  1. 创建一个名为onbuild-parent的新目录:
mkdir onbuild-parent
  1. 导航到新创建的onbuild-parent目录:
cd onbuild-parent
  1. onbuild-parent目录中,创建一个名为Dockerfile的文件:
touch Dockerfile
  1. 现在,用你喜欢的文本编辑器打开Dockerfile
vim Dockerfile
  1. 将以下内容添加到Dockerfile中,保存并退出Dockerfile
# ONBUILD example
FROM ubuntu
RUN apt-get update && apt-get install apache2 -y 
ONBUILD COPY *.html /var/www/html
EXPOSE 80
ENTRYPOINT ["apache2ctl", "-D", "FOREGROUND"]

这个Dockerfile首先将 ubuntu 镜像定义为父镜像。然后执行apt-get update命令来更新软件包列表,以及apt-get install apache2 -y命令来安装 Apache Web 服务器。ONBUILD指令用于提供一个触发器,将所有 HTML 文件复制到/var/www/html目录。EXPOSE指令用于暴露容器的端口80ENTRYPOINT用于使用apache2ctl命令启动 Apache Web 服务器。

  1. 现在,构建 Docker 镜像:
$ docker image build -t onbuild-parent .

输出应该如下所示:

图 2.17:构建 onbuild-parent Docker 镜像

图 2.17:构建 onbuild-parent Docker 镜像

  1. 执行docker container run命令以从上一步构建的 Docker 镜像启动新容器:
$ docker container run -p 80:80 --name onbuild-parent-container -d onbuild-parent

在上述命令中,您已经以分离模式启动了 Docker 容器,同时暴露了容器的端口80

  1. 现在,您应该能够查看 Apache 首页。在您喜欢的网络浏览器中转到http://127.0.0.1端点。请注意,默认的 Apache 首页是可见的:图 2.18:Apache 首页

图 2.18:Apache 首页

  1. 现在,清理容器。使用docker container stop命令停止 Docker 容器:
$ docker container stop onbuild-parent-container
  1. 使用docker container rm命令删除 Docker 容器:
$ docker container rm onbuild-parent-container
  1. 现在,使用onbuild-parent-container作为父镜像创建另一个 Docker 镜像,以部署自定义 HTML 首页。首先,将目录更改回到上一个目录:
cd ..
  1. 为这个练习创建一个名为onbuild-child的新目录:
mkdir onbuild-child
  1. 导航到新创建的onbuild-child目录:
cd onbuild-child
  1. onbuild-child目录中,创建一个名为index.html的文件。这个文件将在构建时由ONBUILD命令复制到 Docker 镜像中:
touch index.html 
  1. 现在,使用您喜欢的文本编辑器打开index.html文件:
vim index.html 
  1. 将以下内容添加到index.html文件中,保存并退出index.html文件:
<html>
  <body>
    <h1>Learning Docker ONBUILD directive</h1>
  </body>
</html>

这是一个简单的 HTML 文件,将在页面的标题中输出Learning Docker ONBUILD指令。

  1. onbuild-child目录中,创建一个名为Dockerfile的文件:
touch Dockerfile
  1. 现在,使用您喜欢的文本编辑器打开Dockerfile
vim Dockerfile
  1. 将以下内容添加到Dockerfile中,保存并退出Dockerfile
# ONBUILD example
FROM onbuild-parent

这个Dockerfile只有一个指令。它将使用FROM指令来利用您之前创建的onbuild-parent Docker 镜像作为父镜像。

  1. 现在,构建 Docker 镜像:
$ docker image build -t onbuild-child .

图 2.19:构建 onbuild-child Docker 镜像

图 2.19:构建 onbuild-child Docker 镜像

  1. 执行docker container run命令,从上一步构建的 Docker 镜像启动一个新的容器:
$ docker container run -p 80:80 --name onbuild-child-container -d onbuild-child

在这个命令中,您已经从onbuild-child Docker 镜像启动了 Docker 容器,同时暴露了容器的端口80

  1. 您应该能够查看 Apache 首页。在您喜欢的网络浏览器中转到http://127.0.0.1端点:图 2.20:Apache web 服务器的自定义首页

图 2.20:Apache web 服务器的自定义首页

  1. 现在,清理容器。首先使用docker container stop命令停止 Docker 容器:
$ docker container stop onbuild-child-container
  1. 最后,使用docker container rm命令删除 Docker 容器:
$ docker container rm onbuild-child-container

在这个练习中,我们观察到如何使用ONBUILD指令创建一个可重用的 Docker 镜像,能够运行提供给它的任何 HTML 文件。我们创建了名为onbuild-parent的可重用 Docker 镜像,其中包含 Apache web 服务器,并暴露了端口80。这个Dockerfile包含ONBUILD指令,用于将 HTML 文件复制到 Docker 镜像的上下文中。然后,我们使用onbuild-parent作为基础镜像创建了第二个 Docker 镜像,名为onbuild-child,它提供了一个简单的 HTML 文件,用于部署到 Apache web 服务器。

现在,让我们通过在下面的活动中使用 Apache web 服务器来测试我们在本章中学到的知识,将给定的 PHP 应用程序进行 docker 化。

活动 2.01:在 Docker 容器上运行 PHP 应用程序

假设您想要部署一个 PHP 欢迎页面,根据日期和时间来问候访客,使用以下逻辑。您的任务是使用安装在 Ubuntu 基础镜像上的 Apache web 服务器,对这里给出的 PHP 应用程序进行 docker 化。

<?php
$hourOfDay = date('H');
if($hourOfDay < 12) {
    $message = "Good Morning";
} elseif($hourOfDay > 11 && $hourOfDay < 18) {
    $message = "Good Afternoon";
} elseif($hourOfDay > 17){
    $message = "Good Evening";
}
echo $message;
?>

这是一个简单的 PHP 文件,根据以下逻辑来问候用户:

图 2.21:PHP 应用程序的逻辑

图 2.21:PHP 应用程序的逻辑

执行以下步骤来完成这个活动:

  1. 创建一个文件夹来存储活动文件。

  2. 创建一个welcome.php文件,其中包含之前提供的代码。

  3. 创建一个Dockerfile,并在 Ubuntu 基础镜像上使用 PHP 和 Apache2 设置应用程序。

  4. 构建并运行 Docker 镜像。

  5. 完成后,停止并删除 Docker 容器。

注意

这项活动的解决方案可以通过此链接找到。

摘要

在本章中,我们讨论了如何使用Dockerfile来创建我们自己的自定义 Docker 镜像。首先,我们讨论了什么是Dockerfile以及Dockerfile的语法。然后,我们讨论了一些常见的 Docker 指令,包括FROMLABELRUNCMDENTRYPOINT指令。然后,我们使用我们学到的常见指令创建了我们的第一个Dockerfile

在接下来的部分,我们专注于构建 Docker 镜像。我们深入讨论了关于 Docker 镜像的多个方面,包括 Docker 镜像的分层文件系统,Docker 构建中的上下文,以及在 Docker 构建过程中缓存的使用。然后,我们讨论了更高级的Dockerfile指令,包括ENVARGWORKDIRCOPYADDUSERVOLUMEEXPOSEHEALTHCHECKONBUILD指令。

在下一章中,我们将讨论 Docker 注册表是什么,看看私有和公共 Docker 注册表,并学习如何将 Docker 镜像发布到 Docker 注册表。

第三章:管理您的 Docker 镜像

概述

在本章中,我们将深入研究 Docker 层,并分析缓存如何帮助加快镜像构建。我们还将深入研究 Docker 镜像,并设置 Docker 注册表,以增加镜像的可重用性。

通过本章的学习,您将能够演示 Docker 如何使用层构建镜像以及如何通过缓存加快镜像构建。您将使用镜像标签,并为 Docker 镜像设置标记策略。本章将使您能够为您的项目利用 Docker Hub,并区分公共和私有注册表。在处理项目时,它还将帮助您设置自己的 Docker 注册表。

介绍

我们之前的章节已经在 Docker 镜像上做了很多工作。正如您所看到的,我们已经能够获取 Docker Hub 中提供给公众的现有镜像,并在其基础上构建后运行或重用它们以满足我们的目的。镜像本身帮助我们简化流程,并减少我们需要做的工作。

在本章中,我们将更深入地了解镜像以及如何在系统上使用它们。我们将学习如何更好地组织和标记镜像,了解不同层的镜像如何工作,并设置公共和私有的注册表,以进一步重用我们创建的镜像。

Docker 镜像也非常适合应用程序开发。镜像本身是应用程序的自包含版本,其中包括运行所需的一切。这使开发人员能够在本地机器上构建镜像,并将其部署到开发或测试环境,以确保它与应用程序的其余部分良好配合。如果一切顺利,他们可以将相同的镜像作为发布推送到生产环境,供用户消费。当我们开始在更大的开发人员群体中工作时,我们需要在使用我们的镜像时保持一致。

本章还将帮助您制定一致的服务标记策略,以帮助限制问题,并确保在问题出现时能够追踪或回滚。了解如何分发镜像以供消费和协作也是我们将在本章进一步讨论的内容。因此,让我们立即开始本章的学习,了解 Docker 中的层和缓存是什么。

Docker 层和缓存

注册表是存储和分发 Docker 镜像的一种方式。当您从注册表拉取 Docker 镜像时,您可能已经注意到镜像是分成多个部分而不是作为单个镜像拉取的。当您在系统上构建镜像时,同样的事情也会发生。

这是因为 Docker 镜像由多层组成。当您使用Dockerfile创建新镜像时,它会在您已构建的现有镜像之上创建更多的层。您在Dockerfile中指定的每个命令都将创建一个新的层,每个层都包含在执行命令之前和之后发生的所有文件系统更改。当您从Dockerfile运行镜像作为容器时,您正在在只读层的顶部创建可读写的层。这个可写层被称为容器层

正如您将在接下来的练习中看到的那样,当您从Dockerfile构建容器时,所呈现的输出显示了在Dockerfile中运行的每个命令。它还显示了通过运行每个命令创建的层,这些层由随机生成的 ID 表示。一旦镜像构建完成,您就可以使用docker history命令查看在构建过程中创建的层,包括镜像名称或 ID。

注意

在设置构建环境并在开发过程中进一步进行时,请记住,层数越多,镜像就会越大。因此,这额外的存储空间在构建时间和开发和生产环境中使用的磁盘空间方面可能会很昂贵。

Dockerfile构建镜像时,当使用RUNADDCOPY命令时会创建层。Dockerfile中的所有其他命令都会创建中间层。这些中间层的大小为 0 B;因此,它们不会增加 Docker 镜像的大小。

在构建我们的 Docker 镜像时,我们可以使用docker history命令和镜像名称或 ID 来查看用于创建镜像的层。输出将提供有关用于生成层的命令以及层的大小的详细信息:

docker history <image_name|image_id>

docker image inspect命令在提供有关我们镜像的层位于何处的进一步详细信息方面非常有用:

docker image inspect <image_id>

在本章的后面部分,当我们看创建基本图像时,我们将使用docker image命令,该命令与我们正在创建的图像的 TAR 文件版本一起使用。如果我们能够访问正在运行的容器或虚拟机,我们将能够将运行系统的副本放入 TAR 存档中。然后将存档的输出传输到docker import命令中,如此处所示:

cat <image_tar_file_name> | docker import - <new_image_name>

下一个练习将让您亲身体验到我们迄今为止学到的知识以及如何使用 Docker 镜像层进行工作。

注意

请使用touch命令创建文件,使用vim命令使用 vim 编辑器处理文件。

练习 3.01:使用 Docker 镜像层

在这个练习中,您将使用一些基本的Dockerfiles来看看 Docker 如何使用层来构建图像。您将首先创建一个Dockerfile并构建一个新的图像。然后重新构建图像以查看使用缓存的优势以及由于使用缓存而减少的构建时间:

  1. 使用您喜欢的文本编辑器创建一个名为Dockerfile的新文件,并添加以下细节:
FROM alpine
RUN apk update
RUN apk add wget
  1. 保存Dockerfile,然后从命令行确保您在与您创建的Dockerfile相同的目录中。使用docker build命令使用-t选项为其命名为basic-app来创建新的镜像:
docker build -t basic-app .

如果图像构建成功,您应该会看到类似以下的输出。我们已经用粗体突出显示了每个构建步骤。每个步骤都作为中间层构建,如果成功完成,然后将其转移到只读层:

Sending build context to Docker daemon 4.096kB
Step 1/3 : FROM alpine
latest: Pulling from library/alpine
9d48c3bd43c5: Pull complete 
Digest: sha256:72c42ed48c3a2db31b7dafe17d275b634664a
        708d901ec9fd57b1529280f01fb
Status: Downloaded newer image for alpine:latest
  ---> 961769676411
Step 2/3 : RUN apk update
  ---> Running in 4bf85f0c3676
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/main/
  x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/community/
  x86_64/APKINDEX.tar.gz
v3.10.2-64-g631934be3a [http://dl-cdn.alpinelinux.org/alpine
  /v3.10/main]
v3.10.2-65-ge877e766a2 [http://dl-cdn.alpinelinux.org/alpine
  /v3.10/community]
OK: 10336 distinct packages available
Removing intermediate container 4bf85f0c3676
  ---> bcecd2429ac0
Step 3/3 : RUN apk add wget
  ---> Running in ce2a61d90f77
(1/1) Installing wget (1.20.3-r0)
Executing busybox-1.30.1-r2.trigger
OK: 6 MiB in 15 packages
Removing intermediate container ce2a61d90f77
  ---> a6d7e99283d9
Successfully built 0e86ae52098d
Successfully tagged basic-app:latest
  1. 使用docker history命令以及basic-app的图像名称来查看图像的不同层:
docker history basic-app

历史记录提供了创建细节,包括每个层的大小:

IMAGE         CREATED            CREATED BY 
                      SIZE
a6d7e99283d9  About a minute ago /bin/sh -c apk add wget
                      476kB
bcecd2429ac0  About a minute ago /bin/sh -c apk update
                      1.4MB
961769676411  5 weeks ago        /bin/sh -c #(nop)
CMD ["/bin/sh"]       0B
<missing>     5 weeks ago        /bin/sh -c #(nop) 
ADD file:fe6407fb…    5.6MB

注意

docker history命令显示了作为Dockerfile FROM命令的一部分使用的原始图像的层为<missing>。在我们的输出中显示为missing,因为它是在不同的系统上创建的,然后被拉到您的系统上。

  1. 不做任何更改再次运行构建:
docker build -t basic-app .

这将显示构建是使用 Docker 镜像缓存中存储的层完成的,从而加快了我们的构建速度。尽管这只是一个小图像,但更大的图像将显示显着的增加:

Sending build context to Docker daemon  4.096kB
Step 1/3 : FROM alpine
  ---> 961769676411
Step 2/3 : RUN apk update
  ---> Using cache
  ---> bcecd2429ac0
Step 3/3 : RUN apk add wget
  ---> Using cache
  ---> a6d7e99283d9
Successfully built a6d7e99283d9
Successfully tagged basic-app:latest
  1. 假设您忘记在图像创建的过程中安装curl包。在步骤 1中的Dockerfile中添加以下行:
FROM alpine
RUN apk update
RUN apk add wget curl
  1. 再次构建图像,现在您将看到图像由缓存层和需要创建的新层混合而成:
docker build -t basic-app .

突出显示了输出的第三步,显示了我们在Dockerfile中所做的更改:

Sending build context to Docker daemon 4.096kB
Step 1/3 : FROM alpine
  ---> 961769676411
Step 2/3 : RUN apk update
  ---> Using cache
  ---> cb8098d0c33d
Step 3/3 : RUN apk add wget curl
  ---> Running in b041735ff408
(1/5) Installing ca-certificates (20190108-r0)
(2/5) Installing nghttp2-libs (1.39.2-r0)
(3/5) Installing libcurl (7.66.0-r0)
(4/5) Installing curl (7.66.0-r0)
(5/5) Installing wget (1.20.3-r0)
Executing busybox-1.30.1-r2.trigger
Executing ca-certificates-20190108-r0.trigger
OK: 8 MiB in 19 packages
Removing intermediate container b041735ff408
  ---> c7918f4f95b9
Successfully built c7918f4f95b9
Successfully tagged basic-app:latest
  1. 再次运行docker images命令:
docker images

您现在会注意到图像被命名和标记为<none>,以显示我们现在创建了一个悬空图像:

REPOSITORY   TAG      IMAGE ID        CREATED           SIZE
basic-app    latest   c7918f4f95b9    25 seconds ago    8.8MB
<none>       <none>   0e86ae52098d    2 minutes ago     7.48MB
Alpine       latest   961769676411    5 weeks ago       5.58MB

注意

悬空图像,在我们的图像列表中表示为<none>,是由于一个层与我们系统上的任何图像都没有关联而引起的。这些悬空图像不再起作用,并将占用您系统上的磁盘空间。我们的示例悬空图像只有 7.48 MB,这很小,但随着时间的推移,这可能会累积起来。

  1. 使用图像 ID 运行docker image inspect命令,查看悬空图像在我们系统上的位置:
docker image inspect 0e86ae52098d

以下输出已从实际输出减少,仅显示图像的目录:

... 
  "Data": {
    "LowerDir": "/var/lib/docker/overlay2/
      41230f31bb6e89b6c3d619cafc309ff3d4ca169f9576fb003cd60fd4ff
      4c2f1f/diff:/var/lib/docker/overlay2/
      b8b90262d0a039db8d63c003d96347efcfcf57117081730b17585e163f
      04518a/diff",
    "MergedDir": "/var/lib/docker/overlay2/
      c7ea9cb56c5bf515a1b329ca9fcb2614f4b7f1caff30624e9f6a219049
      32f585/
      merged",
    "UpperDir": "/var/lib/docker/overlay2/
      c7ea9cb56c5bf515a1b329ca9fcb2614f4b7f1caff30624e9f6a21904
      932f585/diff",
    "WorkDir": "/var/lib/docker/overlay2/
      c7ea9cb56c5bf515a1b329ca9fcb2614f4b7f1caff30624e9f6a21904
      932f585/work"
  },
...

我们所有的图像都位于与悬空图像相同的位置。由于它们共享相同的目录,任何悬空图像都会浪费我们系统上的空间。

  1. 从命令行运行du命令,查看我们的图像使用的总磁盘空间:
du -sh /var/lib/docker/overlay2/

该命令将返回您的图像使用的总磁盘空间

11M    /var/lib/docker/overlay2/

注意

如果您正在使用 Docker Desktop,可能是在 Mac 上,您会注意到您无法看到图像,因为 Docker 在您的系统上以虚拟图像运行,即使docker image inspect命令显示的位置与上面相同。

  1. 再次使用docker images命令,并使用-a选项:
docker images -a

它还会显示在构建我们的图像时使用的中间层:

REPOSITORY   TAG      IMAGE ID      CREATED          SIZE
basic-app    latest   c7918f4f95b9  25 seconds ago   8.8MB
<none>       <none>   0e86ae52098d  2 minutes ago    7.48MB
<none>       <none>   112a4b041305  11 minutes ago   7MB
Alpine       latest   961769676411  5 weeks ago      5.58MB
  1. 运行docker image prune命令以删除所有悬空图像。您可以使用docker rmi命令逐个删除所有悬空图像,使用图像 ID,但docker image prune命令是更简单的方法:
docker image prune

您应该会得到以下输出:

WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y
Deleted Images:
deleted: sha256:0dae3460f751d16f41954e0672b0c41295d46ee99d71
         d63e7c0c8521bd9e6493
deleted: sha256:d74fa92b37b74820ccccea601de61d45ccb3770255b9
         c7dd22edf16caabafc1c
Total reclaimed space: 476.4kB
  1. 再次运行docker images命令:
docker images

您会看到我们的图像列表中不再有悬空图像:

REPOSITORY   TAG      IMAGE ID        CREATED           SIZE
basic-app    latest   c7918f4f95b9    25 seconds ago    8.8MB
Alpine       latest   961769676411    5 weeks ago       5.58MB
  1. 再次在图像目录上运行du命令:
du -sh /var/lib/docker/overlay2/

您还应该观察到尺寸的小幅减小:

10M    /var/lib/docker/overlay2/

这个练习只显示了较小的图像尺寸,但在运行生产和开发环境时,这绝对是需要牢记的事情。本章的这一部分为您提供了 Docker 如何在其构建过程中使用层和缓存的基础。

对于我们的下一个练习,我们将进一步研究我们的层和缓存,以查看它们如何用于加快图像构建过程。

练习 3.02:增加构建速度和减少层

到目前为止,您一直在处理较小的构建。但是,随着您的应用程序在大小和功能上的增加,您将开始考虑您正在创建的 Docker 图像的大小和层数以及创建它们的速度。本练习的目标是加快构建时间并减小图像的大小,并在构建 Docker 图像时使用--cache-from选项:

  1. 创建一个新的Dockerfile来演示您将要进行的更改,但首先,请清理系统上的所有图像。使用docker rmi命令并带有-f选项来强制进行任何需要的删除,括号中的命令将提供系统上所有图像 ID 的列表。使用-a选项来显示所有正在运行和停止的容器,使用-q选项仅显示容器图像哈希值,而不显示其他内容。
docker rmi -f $(docker images -a -q)

该命令应返回以下输出:

Untagged: hello-world:latest
...
deleted: sha256:d74fa92b37b74820ccccea601de61d45ccb3770255
         b9c7dd22edf16caabafc1c

可以观察到hello-world:latest镜像已被取消标记,并且具有 IDsha256:d74fa92b37b74820ccccea601 de61d45ccb3770255b9c7dd22edf16caabafc1c的镜像已被删除。

注意

请注意,我们可以使用rmiprune命令来删除图像。在这里,我们使用了rmi命令,因为prune直到最近才可用。

  1. 将以下代码添加到您的Dockerfile(您在练习 3.01中创建的)。它将模拟一个简单的 Web 服务器,并在构建过程中打印我们的Dockerfile的输出:
1 FROM alpine
2 
3 RUN apk update
4 RUN apk add wget curl
5
6 RUN wget -O test.txt https://github.com/PacktWorkshops/   The-Docker-Workshop/blob/master/Chapter03/Exercise3.02/100MB.bin
7
8 CMD mkdir /var/www/
9 CMD mkdir /var/www/html/
10
11 WORKDIR /var/www/html/
12
13 COPY Dockerfile.tar.gz /tmp/
14 RUN tar -zxvf /tmp/Dockerfile.tar.gz -C /var/www/html/
15 RUN rm /tmp/Dockerfile.tar.gz
16
17 RUN cat Dockerfile

您会注意到Dockerfile第 6 行正在执行一个相当琐碎的任务(下载一个名为100MB.bin的 100MB 文件),这在Dockerfile中通常不会执行。我们已经添加了它来代表一个构建任务或类似的东西,例如,下载内容或从文件构建软件。

  1. 使用docker pull命令下载基本图像,以便您可以从每次测试开始使用相同的图像:
docker pull alpine
  1. 创建一个 TAR 文件,以便按照我们在Dockerfile第 13 行中指示的方式添加到我们的图像中:
tar zcvf Dockerfile.tar.gz Dockerfile
  1. 使用与basic-app相同的名称构建一个新图像。您将在代码开头使用time命令,以便我们可以衡量构建图像所花费的时间:
time docker build -t basic-app .

输出将返回构建图像所花费的时间:

...
real 4m36.810s
user 0m0.354s
sys 0m0.286s
  1. 对新的basic-app镜像运行docker history命令:
docker history basic-app

与上一个练习相比,我们的Dockerfile中有一些额外的命令。因此,我们将在新镜像中看到 12 层,这并不奇怪:

IMAGE         CREATED      CREATED BY                           SIZE
5b2e3b253899 2 minutes ago /bin/sh -c cat Dockerfile            0B
c4895671a177 2 minutes ago /bin/sh -c rm /tmp/Dockerfile.tar.gz 0B
aaf18a11ba25 2 minutes ago /bin/sh -c tar -zxvf /tmp/Dockfil…   283B
507161de132c 2 minutes ago /bin/sh -c #(nop) COPY file:e39f2a0… 283B
856689ad2bb6 2 minutes ago /bin/sh -c #(nop) WORKDIR /var/…     0B
206675d145d4 2 minutes ago /bin/sh -c #(nop)  CMD ["/bin/sh"…   0B
c947946a36b2 2 minutes ago /bin/sh -c #(nop)  CMD ["/bin/sh"…   0B
32b0abdaa0a9 2 minutes ago /bin/sh -c curl https://github.com…  105MB
e261358addb2 2 minutes ago /bin/sh -c apk add wget curl         1.8MB
b6f77a768f90 2 minutes ago /bin/sh -c apk update                1.4MB
961769676411 6 weeks ago   /bin/sh -c #(nop)  CMD ["/bin/sh"]   0B
<missing>    6 weeks ago   /bin/sh -c #(nop) ADD file:fe3dc…    5.6MB

我们可以看到Dockerfile中的RUNCOPYADD命令正在创建特定大小的层,与运行的命令或添加的文件相关,并且Dockerfile中的所有其他命令的大小都为 0 B。

  1. 通过合并Dockerfile第 3 行和第 4 行RUN命令以及合并第 8 行和第 9 行CMD命令,减少镜像中的层数。通过这些更改,我们的Dockerfile现在应该如下所示:
1 FROM alpine
2 
3 RUN apk update && apk add wget curl
4 
5 RUN wget -O test.txt https://github.com/PacktWorkshops/    The-Docker-Workshop/blob/master/Chapter03/Exercise3.02/100MB.bin
6 
7 CMD mkdir -p /var/www/html/
8 
9 WORKDIR /var/www/html/
10 
11 COPY Dockerfile.tar.gz /tmp/
12 RUN tar -zxvf /tmp/Dockerfile.tar.gz -C /var/www/html/
13 RUN rm /tmp/Dockerfile.tar.gz
14 
15 RUN cat Dockerfile

再次运行docker build将会减少新镜像的层数,从 12 层减少到 9 层,因为即使运行的命令数量相同,它们在第 3 行第 7 行中被链接在一起。

  1. 第 11 行第 12 行第 13 行Dockerfile正在使用COPYRUN命令来copyunzip我们的归档文件,然后删除原始的解压文件。用ADD命令替换这些行,而无需运行解压和删除.tar文件的行:
1 FROM alpine
2 
3 RUN apk update && apk add wget curl
4
5 RUN wget -O test.txt https://github.com/PacktWorkshops/    The-Docker-Workshop/blob/master/Chapter03/Exercise3.02/100MB.bin
6 
7 CMD mkdir -p /var/www/html/
8 
9 WORKDIR /var/www/html/
10 
11 ADD Dockerfile.tar.gz /var/www/html/
12 RUN cat Dockerfile
  1. 再次构建镜像,将新镜像的层数从 9 层减少到 8 层。如果您一直在观察构建过程,您可能会注意到大部分时间是在Dockerfile第 3 行第 5 行中运行,我们在那里运行apk update,然后安装wgetcurl,然后从网站获取内容。这样做一两次不会有问题,但如果我们创建了基础镜像,Dockerfile可以在其上运行,您将能够完全从Dockerfile中删除这些行。

  2. 进入一个新目录,并创建一个新的Dockerfile,它将只拉取基础镜像并运行apk命令,如下所示:

1 FROM alpine
2
3 RUN apk update && apk add wget curl
4
5 RUN wget -O test.txt https://github.com/PacktWorkshops/    The-Docker-Workshop/blob/master/Chapter03/Exercise3.02/100MB.bin
  1. 从上一个Dockerfile中构建新的基础镜像,并将其命名为basic-base
docker build -t basic-base .
  1. 从原始Dockerfile中删除第 3 行,因为它将不再需要。进入项目目录,并将FROM命令中使用的镜像更新为basic-base,并删除第 3 行中的apk命令。我们的Dockerfile现在应该如下所示:
1 FROM basic-base
2
3 CMD mkdir -p /var/www/html/
4
5 WORKDIR /var/www/html/
6
7 ADD Dockerfile.tar.gz /var/www/html/
8 RUN cat Dockerfile
  1. 再次运行新的Dockerfile进行构建。再次使用time命令进行构建,我们现在可以看到构建在 1 秒多钟内完成:
time docker build -t basic-app .

如果您一直在观看构建过程,您会注意到与我们以前的构建相比,它运行得更快:

...
real 0m1.810s
user 0m0.117s
sys  0m0.070s

注意

您将观察到镜像的层将保持不变,因为我们正在在我们的系统上构建基础镜像,该系统执行apk命令。即使我们没有减少层数,这仍然是一个很好的结果,可以加快构建速度。

  1. 我们可以使用之前使用的basic-base镜像的不同方式。使用docker build命令和--cache-from选项指定构建镜像时将使用的缓存层。设置FROM命令仍然使用alpine镜像,并使用后面的--cache-from选项,以确保用于构建basic-base的层被用于我们当前的镜像:
docker build --cache-from basic-base -t basic-app .

在完成此练习之前,我们还有一些任务要完成。在接下来的步骤中,我们将查看提交对镜像的更改,以查看它如何影响我们的层。这不是我们经常使用的东西,但有时我们需要将生产数据复制到开发或测试环境中,其中一种方法是使用带有commit命令的 Docker 镜像,该命令将更改我们运行容器的顶部可写层。

  1. 以交互式 shell 模式运行basic-app以创建一些生产数据。为此,请使用-it选项运行以下docker run命令以交互模式运行,并使用sh shell 访问运行的容器:
docker run -it basic-app sh
/var/www/html #
  1. 使用 vi 文本编辑器创建一个名为prod_test_data.txt的新文本文件:
vi prod_test_data.txt
  1. 添加以下文本行作为一些测试数据。文本中的数据并不重要;这只是一个示例,表明我们可以将这些更改复制到另一个镜像中:

  2. 这是一个示例生产数据。退出运行的容器,然后使用带有-a选项的docker ps命令检查容器 ID:

docker ps -a

您将获得以下输出:

CONTAINER ID    IMAGE        COMMAND    CREATED
ede3d51bba9e    basic-app    "sh"       4 minutes ago
  1. 运行docker commit命令,使用容器 ID 创建一个包含所有这些更改的新镜像。确保添加新镜像的名称。在本例中,使用basic-app-test
docker commit ede3d51bba9e basic-app-test

您将获得以下输出:

sha256:0717c29d29f877a7dafd6cb0555ff6131179b457
       e8b8c25d9d13c2a08aa1e3f4
  1. 在新创建的镜像上运行docker history命令:
docker history basic-app-test

现在,这应该显示出我们添加了示例生产数据的额外层,显示在我们的输出中,大小为 72B:

IMAGE        CREATED       CREATED BY                         SIZE
0717c29d29f8 2 minutes ago sh                                 72B
302e01f9ba6a 2 minutes ago /bin/sh -c cat Dockerfile          0B
10b405ceda34 2 minutes ago /bin/sh -c #(nop) ADD file:e39f…   283B
397f533f4019 2 minutes ago /bin/sh -c #(nop) WORKDIR /var/…   0B
c8782986b276 2 minutes ago /bin/sh -c #(nop)  CMD ["/bin/sh"… 0B
6dee05f36f95 2 minutes ago /bin/sh -c apk update && apk ad    3.2MB
961769676411 6 weeks ago   /bin/sh -c #(nop)  CMD ["/bin/sh"] 0B
<missing>    6 weeks ago   /bin/sh -c #(nop) ADD file:fe3dc…  5.6MB
  1. 现在,运行新创建的basic-app-test镜像和cat,我们添加的新文件:
docker run basic-app-test cat prod_test_data.txt

这应该显示我们添加的输出,表明我们可以在需要时重用现有镜像:

This is a sample production piece of data

注意

在撰写本文时,docker build命令还允许使用--squash选项的新实验性功能。该选项尝试在构建时将所有层合并为一层。我们还没有涵盖这个功能,因为它仍处于实验阶段。

这个练习演示了构建缓存和镜像层是如何改善构建时间的。到目前为止,我们所有的构建都是使用从 Docker Hub 下载的镜像开始的,但如果你希望进一步控制,也可以使用自己创建的镜像。下一节将帮助你创建自己的基础 Docker 镜像。

创建基础 Docker 镜像

创建自己的基础 Docker 镜像实际上很简单。就像我们之前使用docker commit命令从运行的容器创建镜像一样,我们也可以从最初运行我们应用程序的系统或服务器创建镜像。我们需要记住,创建基础镜像仍然需要保持小巧和轻量级。这不仅仅是将现有应用程序从现有服务器迁移到 Docker 的问题。

我们可以使用我们正在专门工作的系统,但如果你正在使用生产服务器,镜像实际上可能会很大。如果你有一个小型虚拟机,认为它非常适合作为基础镜像,可以使用以下步骤创建基础镜像。类似于docker commit命令,这可以用于任何你可以访问的系统。

练习 3.03:创建自己的基础 Docker 镜像

以下练习将使用我们当前正在运行的basic-app镜像,并展示创建基础镜像有多么简单。对于更大、更复杂的环境,也可以使用相同的步骤:

  1. 执行docker run命令以同时运行容器并登录:
docker run -it basic-app sh
  1. 运行tar命令在运行的容器上创建系统的备份。为了限制新镜像中的信息,排除.proc.tmp.mnt.dev.sys目录,并将所有内容创建在basebackup.tar.gz文件下:
tar -czf basebackup.tar.gz --exclude=backup.tar.gz --exclude=proc --exclude=tmp --exclude=mnt --exclude=dev --exclude=sys /
  1. 为确保basebackup.tar.gz文件中有数据,请运行du命令,确保其大小足够大:
du -sh basebackup.tar.gz 

输出返回basebackup.tar.gz文件的大小:

4.8M	basebackup.tar.gz
  1. 运行docker ps命令找到当前保存新备份文件的容器 ID,.tar文件:
docker ps

该命令将返回镜像的容器 ID:

CONTAINER ID        IMAGE        COMMAND      CREATED
6da7a8c1371a        basic-app    "sh"         About a minute ago
  1. .tar文件复制到您的开发系统上,使用docker cp命令,使用正在运行的容器的容器 ID 以及要复制的位置和文件。以下命令将使用您的容器 ID 执行此操作,并将其移动到您的/tmp目录中:
docker cp 6da7a8c1371a:/var/www/html/basebackup.tar.gz /tmp/
  1. 使用docker import命令创建一个新的镜像。只需将basebackup.tar.gz文件的输出导入docker import命令中,并在此过程中命名新镜像。在我们的示例中,将其命名为mynew-base
cat /tmp/basebackup.tar.gz | docker import - mynew-base
  1. 使用您的新镜像的名称运行docker images命令,以验证它是否已在上一步中创建:
docker images mynew-base

您应该得到以下类似的输出:

REPOSITORY    TAG     IMAGE ID      CREATED         SIZE
mynew-base    latest  487e14fca064  11 seconds ago  8.79MB
  1. 运行docker history命令:
docker history mynew-base

您将看到我们的新镜像中只有一个层:

IMAGE         CREATED         CREATED BY   SIZE   COMMENT
487e14fca064  37 seconds ago               .79MB  Imported from –
  1. 要测试新镜像,请在新镜像上运行docker run命令,并列出您的/var/www/html/目录中的文件:
docker run mynew-base ls -l /var/www/html/

该命令应返回类似的输出:

total 4
-rw-r--r--    1 501      dialout      283 Oct  3 04:07 Dockerfile

可以看到镜像已成功创建,并且/var/www/html/目录中有 24 个文件。

这个练习向您展示了如何从运行的系统或环境中创建一个基本镜像,但如果您想要创建一个小的基本镜像,那么下一节将向您展示如何使用scratch镜像。

空白镜像

空白镜像是 Docker 专门为构建最小化镜像而创建的镜像。如果您有一个二进制应用程序,比如 Java、C++等编写并编译的应用程序,可以独立运行而无需任何支持应用程序,那么空白镜像将帮助您使用您可以创建的最小镜像之一来运行该镜像。

当我们在我们的Dockerfile中使用FROM scratch命令时,我们指定将使用 Docker 保留的最小镜像,该镜像命名为scratch来构建我们的新容器镜像。

练习 3.04:使用空白镜像

在这个练习中,您将创建一个小的 C 应用程序在镜像上运行。您实际上不需要了解 C 语言的任何内容来完成这个练习。您创建的应用程序将安装在您的空白基本镜像上,以确保该镜像尽可能小。您创建的应用程序将向您展示如何创建一个最小的基本镜像之一:

  1. 使用docker pull命令拉取空白镜像:
docker pull scratch

您会注意到无法拉取该镜像,并将收到错误:

Using default tag: latest
Error response from daemon: 'scratch' is a reserved name
  1. 创建一个 C 程序,将其构建到我们的Dockerfile中使用的镜像中。创建一个名为test.c的程序文件:
touch test.c
  1. 打开文件并添加以下代码,它将在控制台上简单地从 1 数到 10:
#include <stdio.h>
int main()
{
    int i;
    for (i=1; i<=10; i++)
    {
        printf("%d\n", i);
    }
    return 0;
}
  1. 通过运行以下命令构建 C 程序来从命令行构建镜像:
g++ -o test -static test.c

注意

如果你想在构建镜像之前测试它,可以在命令行上运行./test来进行测试。

  1. 创建DockerfileDockerfile将非常简洁,但需要以FROM scratch开头。文件的其余部分将把 C 程序添加到你的镜像,然后在第 4 行运行它:
1 FROM scratch
2
3 ADD test /
4 CMD ["/test"]
  1. 构建一个新的镜像。在这种情况下,使用以下命令将镜像命名为scratchtest
docker build -t scratchtest .
  1. 从命令行运行镜像:
docker run scratchtest

你将看到你在这个练习中创建和编译的测试 C 文件的输出:

1
2
3
4
5
6
7
8
9
10
  1. 运行docker images命令查看你的新镜像:
docker images scratchtest

这将向你展示一些令人印象深刻的结果,因为你的镜像只有913 kB大小。

REPOSITORY   TAG     IMAGE ID         CREATED          SIZE
scratch      latest  221adbe23c26     20 minutes ago   913kB
  1. 使用docker history命令查看镜像的层:
docker history scratchtest

你将看到类似以下的输出,它只有两层,一层是从头开始的原始层,另一层是我们ADD测试 C 程序的层:

IMAGE        CREATED        CREATED BY                        SIZE
221adbe23c26 23 minutes ago /bin/sh -c #(nop)  CMD ["/test"]  0B
09b61a3a1043 23 minutes ago /bin/sh -c #(nop) ADD file:80933… 913kB

在这个练习中创建的 scratch 镜像在一定程度上创建了一个既功能齐全又最小化的镜像,并且还表明,如果你考虑一下你想要实现什么,就可以轻松加快构建速度并减小镜像的大小。

我们现在将暂停构建镜像的工作,更仔细地研究如何命名和标记我们的 Docker 镜像。

Docker 镜像命名和标记

我们已经提到了标签,但随着我们更密切地与 Docker 镜像一起工作,现在可能是深入了解镜像标签的好时机。简单来说,标签是 Docker 镜像上的标签,应该为使用该镜像的用户提供一些有用的信息,关于镜像或镜像版本。

到目前为止,我们一直在像独立开发者一样处理我们的镜像,但当我们开始与更大的开发团队合作时,就需要更加努力地考虑如何命名和标记我们的镜像。本章的下一部分将为你的先前工作增添内容,并让你开始为你的项目和工作制定命名和标记策略。

有两种主要方法来命名和标记您的 Docker 图像。您可以使用docker tag命令,也可以在从Dockerfile构建图像时使用-t选项。要使用docker tag命令,您需要指定要使用的源存储库名称作为基础和要创建的目标名称和标记:

docker tag <source_repository_name>:<tag> <target_repository_name>:tag

当您使用docker build命令命名图像时,使用的Dockerfile将创建您的源,然后使用-t选项来命名和标记您的图像如下:

docker build -t <target_repository_name>:tag Dockerfile

存储库名称有时可以以主机名为前缀,但这是可选的,并且将用于让 Docker 知道存储库的位置。我们将在本章后面演示这一点,当我们创建自己的 Docker 注册表时。如果您要将图像推送到 Docker Hub,还需要使用您的 Docker Hub 用户名作为存储库名称的前缀,就像这样:

docker build -t <dockerhub_user>/<target_repository_name>:tag Dockerfile

在图像名称中使用两个以上的前缀仅在本地图像注册表中受支持,并且通常不使用。下一个练习将指导您完成标记 Docker 图像的过程。

练习 3.05:给 Docker 图像打标签

在接下来的练习中,您将使用不同的图像,使用轻量级的busybox图像来演示标记的过程,并开始在项目中实施标记。BusyBox 用于将许多常见的 UNIX 实用程序的微小版本组合成一个小的可执行文件:

  1. 运行docker rmi命令来清理您当前系统上的图像,这样您就不会因为大量的图像而感到困惑:
docker rmi -f $(docker images -a -q)
  1. 在命令行上,运行docker pull命令以下载最新的busybox容器:
docker pull busybox
  1. 运行docker images命令:
docker images

这将为我们提供开始组合一些标签命令所需的信息:

REPOSITORY    TAG       IMAGE ID        CREATED      SIZE
Busybox       latest    19485c79a9bb    2 weeks ago  1.22MB
  1. 使用tag命令对图像进行命名和标记。您可以使用图像 ID 或存储库名称来标记图像。首先使用图像 ID,但请注意在您的系统上,您将有一个不同的图像 ID。将存储库命名为new_busybox,并包括标签ver_1
docker tag 19485c79a9bb new_busybox:ver_1
  1. 使用存储库名称和图像标签。使用您的名称创建一个新的存储库,并使用ver_1.1的新版本如下:
docker tag new_busybox:ver_1 vince/busybox:ver_1.1

注意

在这个例子中,我们使用了作者的名字(vince)。

  1. 运行docker images命令:
docker images

您应该看到类似于以下内容的输出。当然,您的图像 ID 将是不同的,但存储库名称和标签应该是相似的:

REPOSITORY     TAG      ID             CREATED        SIZE
Busybox        latest   19485c79a9bb   2 weeks ago    1.22MB
new_busybox    ver_1    19485c79a9bb   2 weeks ago    1.22MB
vince/busybox  ver_1.1  19485c79a9bb   2 weeks ago    1.22MB
  1. 使用Dockerfiledocker build命令的-t选项来创建一个基本图像,并为其命名和打上标签。在本章中,你已经做过几次了,所以从命令行中运行以下命令来创建一个基本的Dockerfile,使用你之前命名的new_busybox图像。还要包括图像名称的标签,因为 Docker 将尝试使用latest标签,但由于它不存在,所以会失败。
echo "FROM new_busybox:ver_1" > Dockerfile
  1. 运行docker build命令来创建图像,并同时为其命名和打上标签:
docker build -t built_image:ver_1.1.1 .
  1. 运行docker images命令:
docker images

你现在应该在你的系统上有四个可用的图像。它们都有相同的容器 ID,但会有不同的仓库名称和标记版本。

REPOSITORY     TAG        ID            CREATED      SIZE
built_image    ver_1.1.1  19485c79a9bb  2 weeks ago  1.22MB
Busybox        latest     19485c79a9bb  2 weeks ago  1.22MB
new_busybox    ver_1      19485c79a9bb  2 weeks ago  1.22MB
vince/busybox  ver_1.1    19485c79a9bb  2 weeks ago  1.22MB

给图像打上一个与你的组织或团队相关的适当版本的标签并不需要太多时间,尤其是经过一点练习。本章的这一部分向你展示了如何给你的图像打上标签,这样它们就不再带有latest的默认标签了。你将在下一节中看到,使用latest标签并希望它能正常工作实际上可能会给你带来一些额外的问题。

在 Docker 中使用 latest 标签

在我们使用标签的过程中,我们已经多次提到不要使用latest标签,这是 Docker 提供的默认标签。正如你很快就会看到的,使用latest标签可能会导致很多问题,特别是在部署图像到生产环境时。

我们首先需要意识到的是,latest只是一个标签,就像我们在之前的例子中使用ver_1一样。它绝对不意味着我们的代码的最新版本。它只是表示我们的图像的最新构建,没有包括标签。

在大型团队中使用latest也会导致很多问题,每天多次部署到环境中。这也意味着你将没有历史记录,这会使得回滚错误更加困难。因此,请记住,每次构建或拉取图像时,如果你没有指定标签,Docker 将使用latest标签,并不会做任何事情来确保图像是最新版本。在下一个练习中,我们将检查使用latest标签可能会导致什么问题。

练习 3.06:使用 latest 时出现的问题

您可能仍然是使用 Docker 和标签的新手,因此您可能尚未遇到使用latest标签时出现任何问题。这个练习将为您提供一些明确的想法,说明使用latest标签可能会导致您的开发过程出现问题,并为您提供应避免使用它的原因。在上一个练习中,您使用了new_busybox:ver_1映像创建了一个简单的Dockerfile。在这个练习中,您将进一步扩展此文件:

  1. 打开Dockerfile并修改文件,使其看起来像以下文件。这是一个简单的脚本,将创建带有简单代码的version.sh脚本,以输出我们服务的最新版本。新文件将被命名为Dockerfile_ver1
1 FROM new_busybox:ver_1
2
3 RUN echo "#!/bin/sh\n" > /version.sh
4 RUN echo "echo \"This is Version 1 of our service\""   >> /version.sh
5
6 ENTRYPOINT ["sh", "/version.sh"]
  1. 构建映像并以您的姓名命名,并显示该映像只是一个测试:
docker build -t vince/test .

注意

我们在这里使用了vince作为名称,但您可以使用任何理想的名称。

  1. 使用docker run命令运行映像:
docker run vince/test

现在应该看到versions.sh脚本的输出:

This is Version 1 of our service
  1. 使用docker tag命令将此映像标记为version1
docker tag vince/test vince/test:version1
  1. 打开Dockerfile并对第 4 行进行以下更改:
1 FROM new_busybox:ver_1
2
3 RUN echo "#!/bin/sh\n" > /version.sh
4 RUN echo "echo \"This is Version 2 of our service\""   >> /version.sh
5
6 ENTRYPOINT ["sh", "/version.sh"]
  1. 构建您修改后的Dockerfile并使用version2标记:
docker build -t vince/test:version2 .
  1. 使用docker run命令运行修改后的映像:
docker run vince/test

您应该看到您最新的代码更改。

This is Version 1 of our service

这不是我们要找的版本,是吗?如果不使用正确的标签,Docker 将运行带有latest标签的最新版本的映像。此映像是在步骤 3中创建的。

  1. 现在,使用latestversion2标签运行两个映像:
docker run vince/test:latest
This is Version 1 of our service

现在我们可以看到输出的差异:

docker run vince/test:version2
This is Version 2 of our service

正如您可能已经想到的,您需要指定version2标签来运行修改后的代码版本。您可能已经预料到了,但请记住,如果您有多个开发人员将映像推送到共享注册表,这将使跟踪变得更加困难。如果您的团队正在使用编排并使用latest版本,您可能会在生产环境中运行混合版本的服务。

这些练习为您提供了如何使用标签的示例,同时向您展示了如果决定仅使用latest标签可能会导致的后果。接下来的部分将介绍标记策略以及如何实施自动化流程。

Docker 映像标记策略

随着开发团队规模的增加和他们所工作的项目复杂性的增加,团队的标记策略变得更加重要。如果您的团队没有正确使用标记,就像我们在之前的部分中所演示的那样,这可能会导致很多混乱,实际上会导致更多问题。早期制定标记策略是一个好习惯,以确保您不会遇到这些问题。

在本章的这一部分,我们将涵盖团队内可以使用的不同标记策略,并举例说明它们如何实施。在设置标记策略时很少有对错之分,但需要及早做出决定,并确保团队中的每个人都同意。

语义化版本控制是一个版本控制系统,也可以作为标记策略的一部分使用。如果您不熟悉语义化版本控制,它是一个可信赖的版本系统,使用major_version.minor_version.patch格式的三部分数字。例如,如果您看到一个应用程序的语义版本是 2.1.0,它将显示版本 2 为主要发布版本,1 为次要发布版本,0 为没有补丁。语义化版本控制可以很容易地自动化,特别是在自动化构建环境中。另一个选择是使用哈希值,比如您的代码的git commit哈希。这意味着您可以将标记与您的存储库匹配,这样任何人都可以具体看到自代码实施以来所做的代码更改。您还可以使用日期值,这也可以很容易地自动化。

这里的共同主题是我们的标记策略应该是自动化的,以确保它被使用、理解和遵守。在接下来的练习中,我们将研究使用哈希值作为标记策略的一部分,然后创建一个脚本来构建我们的 Docker 图像,并为我们的标记添加语义版本控制。

练习 3.07:自动化您的图像标记

在这个练习中,您将研究如何自动化图像标记,以减少标记 Docker 图像所需的个人干预量。这个练习再次使用basic-base图像:

  1. 通过创建以下Dockerfile再次创建basic-base图像:
1 FROM alpine
2
3 RUN apk update && apk add wget curl
  1. 从前面的Dockerfile构建新的基础图像,并将其命名为basic-base
docker build -t basic-base .
  1. 创建basic-base图像后,设置名为Dockerfile_ver1Dockerfile以再次构建basic-app。在这种情况下,返回到此处列出的先前的Dockerfile
1 FROM basic-base
2
3 CMD mkdir -p /var/www/html/
4
5 WORKDIR /var/www/html/
6
7 ADD Dockerfile.tar.gz /var/www/html/
8 RUN cat Dockerfile
  1. 如果您一直在使用 Git 跟踪和提交代码更改,您可以使用git log命令将图像标记为来自 Git 的提交哈希。因此,像往常一样使用docker build命令构建新图像,但在这种情况下,添加标签以提供来自git的短提交哈希:
docker build -t basic-app:$(git log -1 --format=%h) .
...
Successfully tagged basic-app:503a2eb

注意

如果您是 Git 的新手,它是一个源代码控制应用程序,允许您跟踪更改并与其他用户在不同的编码项目上进行协作。如果您以前从未使用过 Git,则以下命令将初始化您的存储库,将Dockerfile添加到存储库,并提交这些更改,以便我们有一个 Git 日志:

git init; git add Dockerfile; git commit –m "initial commit"

  1. 使用您的Dockerfile在构建图像时添加参数。打开您一直在为basic-app使用的Dockerfile,并添加以下两行以将变量设置为未知,然后在构建时将LABEL设置为使用git-commit构建参数提供的值。您的Dockerfile现在应如下所示:
1 FROM basic-base
2
3 ARG GIT_COMMIT=unknown
4 LABEL git-commit=$GIT_COMMIT
5
6 CMD mkdir -p /var/www/html/
7
8 WORKDIR /var/www/html/
9
10 ADD Dockerfile.tar.gz /var/www/html/
11 RUN cat Dockerfile
  1. 再次使用--build-arg选项构建图像,并使用GIT_COMMIT参数,该参数现在等于您的git commit哈希值:
docker build -t basic-app --build-arg GIT_COMMIT=$(git log -1 --format=%h) .
  1. 运行docker inspect命令,搜索"git-commit"标签:
docker inspect -f '{{index .ContainerConfig.Labels "git-commit"}}' basic-app

您可以在构建时看到您添加的 Git 哈希标签:

503a2eb

这开始朝着您需要的方向发展,但是如果您需要使用语义版本控制,因为您的团队已经决定这是开发的最佳选项,该怎么办?本练习的其余部分将设置一个构建脚本,用于构建和设置标签为语义版本号。

  1. 在您的Dockerfile旁边,创建一个名为VERSION的版本文件。将basic-app的此构建的新版本设置为1.0.0
echo "1.0.0" > VERSION
  1. Dockerfile进行更改,以删除先前添加的GIT_COMMIT详细信息,并将VERSION文件添加为构建的一部分。将其添加到图像本身意味着用户可以随时参考VERSION文件,以验证图像版本号:
1 FROM basic-base
2
3 CMD mkdir -p /var/www/html/
4
5 WORKDIR /var/www/html/
6
7 ADD VERSION /var/www/html/
8 ADD Dockerfile.tar.gz /var/www/html/
9 RUN cat Dockerfile
  1. 创建一个构建脚本来构建和标记您的图像。将其命名为build.sh,并且它将驻留在与您的DockerfileVERSION文件相同的目录中:
touch build.sh
  1. 将以下详细信息添加到build.sh第 3 行将是您的 Docker Hub 用户名,第 4 行是您正在构建的图像或服务的名称(在以下示例中为basic-app)。然后,脚本从您的VERSION文件中获取版本号,并将所有变量汇集在一起,以使用与您的新语义版本相关的漂亮名称和标记构建您的图像:
1 set -ex
2
3 USER=<your_user_name>
4 SERVICENAME=basic-app
5
6 version=`cat VERSION`
7 echo "version: $version"
8
9 docker build -t $USER/$SERVICENAME:$version .
  1. 确保构建脚本已设置为可执行脚本,使用命令行上的chmod命令:
chmod +x build.sh 
  1. 从命令行运行构建脚本。set -xe在脚本的第 1 行将确保所有命令都输出到控制台,并确保如果任何命令导致错误,脚本将停止。现在运行构建脚本,如下所示:
./build.sh 

这里只显示了构建脚本的输出,其余的构建过程都是正常进行的:

++ USERNAME=vincesestodocker
++ IMAGE=basic-app
+++ cat VERSION
++ version=1.0.0
++ echo 'version: 1.0.0'
version: 1.0.0
++ docker build -t vincesestodocker/basic-app:1.0.0 .
  1. 使用docker images命令查看图像:
docker images vincesestodocker/basic-app

它应该反映在构建脚本中创建的名称和标记:

REPOSITORY                   TAG    IMAGE ID
  CREATED            SIZE
vincesestodocker/basic-app   1.0.0  94d0d337a28c
  29 minutes ago     8.8MB

这项练习在自动化我们的标记过程中起到了很大作用,并且允许将build脚本添加到源代码控制中,并作为构建流水线的一部分轻松运行。不过,这只是一个开始,你将在本章末尾的活动中看到,我们将进一步扩展这个构建脚本。目前,我们已经完成了关于图像的标记和命名的部分,并且它与下一部分很好地契合,该部分涵盖了存储和发布您的 Docker 图像。

存储和发布您的 Docker 图像

自 Docker 历史的早期以来,它的主要吸引力之一就是一个中央网站,用户可以在那里下载图像,重用和改进这些图像以满足他们的目的,并重新上传它们以授予其他用户访问权限。Docker Hub 已经发展壮大,尽管它曾经存在一些安全问题,但通常仍然是人们需要新图像或项目资源时首先寻找的地方。

作为一个公共存储库,Docker Hub 仍然是人们研究和使用图像所需的第一个地方,以简化或改进他们的新开发项目。对于公司和开发人员来说,它也是一个重要的地方,用于托管他们的开源图像,供公众利用。然而,Docker Hub 并不是您存储和分发 Docker 图像的唯一解决方案。

对于开发团队来说,Docker Hub 上的公共存储库虽然易于访问且高度可用,但可能不是最佳选择。如今,您的团队可能会考虑将生产图像存储在基于云的注册表解决方案中,例如 Amazon Elastic Container Registry、Google Container Registry,或者正如本章后面将看到的,另一个选择是设置本地注册表。

在本章的这一部分,我们将首先看看如何实际将图像从一台机器移动到另一台机器,然后更仔细地了解如何使用 Docker Hub。我们将看到如何开始将我们的图像移动到 Docker Hub 作为公开存储的图像。然后,我们将看看如何在开发系统上设置一个本地托管的 Docker 注册表。

docker save命令将用于从命令行保存图像。在这里,我们使用-o选项来指定输出文件和目录,我们将保存图像到该目录中:

docker save -o <output_file_and_Directory> <image_repo_name/image_name:tag>

然后,我们将能够使用load命令,类似于本章前面创建新基础图像时使用的import命令,指定我们之前创建的文件。

docker load -i <output_file_and_Directory>

请记住,并非所有 Docker Hub 上的图像都应该以相同的方式对待,因为它包含了由 Docker Inc.创建的官方图像和由 Docker 用户创建的社区图像的混合物。官方图像仍然是开源图像和解决方案,可供您添加到您的项目中。社区图像通常由公司或个人提供,希望您利用他们的技术。

注意

即使是从 Docker Hub 获取图像时也要小心。尽量限制从不可靠来源拉取图像,并且尽量避免那些没有经过大量用户审核或下载的来源,因为它们可能构成潜在的安全风险。

练习 3.08:手动传输 Docker 图像

有时,无论是网络上的防火墙问题还是其他安全措施,您可能需要直接从一台系统复制图像到另一台系统。幸运的是,Docker 有一种实现这一点的方法,在这个练习中,您将在不使用注册表的情况下将图像从一台系统移动到另一台系统:

  1. 使用docker save命令和-o选项来保存本章最后部分创建的图像。该命令需要用户指定文件名和目录。在下面的示例中,它是/tmp/basic-app.tar。还要指定图像的用户、图像名称和标签。
docker save -o /tmp/basic-app.tar vincesestodocker/basic-app:1.0.0

现在,您应该在/tmp目录中看到打包的镜像。您正在使用.tar作为文件名的扩展名,因为save命令会创建镜像的 TAR 文件。实际上,您可以为文件的扩展名使用任何名称。

  1. 使用du命令验证basic-app.tar文件中是否有数据:
du -sh /tmp/basic-app.tar 
8.9M    /tmp/basic-app.tar
  1. 现在,您可以根据需要移动镜像,无论是通过rsyncscp还是cp。由于它是一个 TAR 文件,如果需要在传输过程中节省一些空间,您还可以将文件压缩为 ZIP 文件。在这个例子中,您将简单地从当前系统中删除镜像。运行docker rmi命令,后面跟着您刚保存的镜像的 ID:
docker rmi -f 94d0d337a28c
  1. 使用docker load命令将新镜像作为 Docker 镜像加载回来,使用-i选项,指向打包镜像的位置。在这种情况下,它是/tmp目录:
docker load -i /tmp/basic-app.tar 

您应该会得到以下输出:

Loaded image: vincesestodocker/basic-app:1.0.0
  1. 使用docker image命令将您刚刚加载到本地环境中的镜像启动:
docker images vincesestodocker/basic-app

您应该会得到以下输出:

REPOSITORY                    TAG      IMAGE ID
  CREATED             SIZE
vincesestodocker/basic-app    1.0.0    2056b6e48b1a
  29 minutes ago      8.8MB

这只是一个简单的练习,但希望它能向您展示,如果有任何情况导致您无法连接到注册表,您仍然可以传输您的 Docker 镜像。接下来的练习更侧重于存储、发布和分发 Docker 镜像的常规方法。

在 Docker Hub 中存储和删除 Docker 镜像

尽管您可以在 Docker Hub 上免费使用,但您需要知道,您的帐户只能免费获得一个私有存储库。如果您想要更多,您需要在 Docker 上支付月度计划。如果 Docker Hub 是您的团队选择使用的解决方案,您很少需要只有一个私有存储库。如果您决定免费帐户适合您,那么您将获得无限数量的免费存储库。

练习 3.09:在 Docker Hub 中存储 Docker 镜像并删除存储库

在这个练习中,您将为您正在工作的basic-app创建一个新的存储库,并将镜像存储在 Docker Hub 中。一旦您将镜像推送到 Docker Hub,您还将看到如何删除存储库:

注意

以下练习将需要您在 Docker Hub 上拥有帐户。我们将只使用免费存储库,因此您不需要付费月度计划,但如果您还没有在 Docker Hub 上注册免费帐户,请转到hub.docker.com/signup

  1. 登录到您的 Docker Hub 帐户,在存储库部分下,您将看到右侧的蓝色按钮创建存储库选项。单击此按钮,以便为您正在工作的basic-app设置存储库:图 3.1:在 Docker Hub 中创建存储库

图 3.1:在 Docker Hub 中创建存储库

  1. 创建新存储库时,您将看到一个类似下面的页面。填写存储库的名称,通常是您要存储的图像或服务的名称(在本例中为basic-app)。您还可以选择将存储库设置为公共私有,在这种情况下,选择公共图 3.2:Docker Hub 的存储库创建屏幕

图 3.2:Docker Hub 的存储库创建屏幕

  1. 在屏幕底部,还有构建图像的选项。单击屏幕底部的创建按钮:图 3.3:Docker Hub 的存储库创建屏幕

图 3.3:Docker Hub 的存储库创建屏幕

  1. 创建新存储库后,它将提供有关如何开始将图像推送到新存储库的详细信息。使用<account_name>/<image_name>:tag标记您的图像,以便 Docker 知道它将推送图像的位置以及 Docker 将要将其推送到哪个存储库:
docker tag basic-app vincesestodocker/basic-app:ver1
  1. 现在,您的系统上的 Docker 知道在哪里推送图像。使用docker push <account_name>/<image_name>:tag命令推送图像:
docker push vincesestodocker/basic-app:ver1
denied: requested access to the resource is denied

您需要确保您已经从命令行和 Web 界面登录到 Docker Hub。

  1. 使用docker login命令,并输入创建新存储库时使用的相同凭据:
docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: vincesestodocker
Password: 
Login Succeeded
  1. 现在,将您的图像推送到新的存储库,就像在本练习的步骤 5中所做的那样,之前失败了。它应该给您一个成功的结果:
docker push basic-app vincesestodocker/basic-app:ver1
  1. 返回 Docker Hub Web 界面,现在您应该看到您推送的图像版本,位于您新创建的存储库中:图 3.4:您新创建的 Docker Hub 存储库中的图像

图 3.4:您新创建的 Docker Hub 存储库中的图像

现在您有一个公共存储库可供任何想要拉取您的镜像并为其目的重用的人使用。如果有人需要使用您的镜像,他们只需使用镜像的完整名称,包括docker pull命令或Dockerfile中的FROM命令。

  1. 您会注意到在前面的图像中,屏幕右侧有一个“公共视图”按钮。这给了您一个选项,可以看到公众在搜索您的镜像时会看到什么。点击按钮,您应该会看到类似以下的屏幕:图 3.5:您的 Docker Hub 存储库的公共视图

图 3.5:您的 Docker Hub 存储库的公共视图

这正是公众将看到的您的存储库。现在轮到您确保您的概述是最新的,并确保您的镜像得到支持,以确保任何想要使用您的镜像的人没有问题。

  1. 最后,在这个练习中,清理您刚刚创建的存储库。如果您还没有在存储库的网络界面中,请返回到 Docker Hub 网页,然后点击屏幕顶部的“设置”选项卡:图 3.6:Docker Hub 存储库的设置屏幕

图 3.6:Docker Hub 存储库的设置屏幕

  1. 在这里,您将有选择使您的存储库私有,但在这个练习中,您将删除存储库。点击“删除存储库”选项,并确认您现在要删除它。

在这个练习中,Docker Hub 为您提供了一个简单的方式来分发镜像,以便其他用户可以合作或利用您已经完成的工作。公共存储库并不总是企业的最佳选择,但就像 GitHub 允许开发人员分发他们的代码并与其他开发人员合作一样,Docker Hub 也可以为您的 Docker 镜像做同样的事情。

Docker Registry

Docker Registry 是托管您的镜像的服务。大多数情况下,存储库是私有的,只对团队有权限访问。有很多很好的选择,其中之一是 Docker 提供和维护的注册表镜像。

有几个不同的原因会让您想要运行自己的 Docker 注册表。这可能是由于安全问题,或者您不希望您的最新工作公开可用。甚至可能只是为了方便,在您正在工作的系统上运行您的注册表。在本章的这一部分,我们将在您的工作环境上设置一个注册表,并开始将您的镜像存储在注册表上。

注意

Docker 为我们简化了事情,因为他们在 Docker Hub 上提供了一个注册表镜像供您下载并用于您的项目。有关我们将要使用的镜像的更多信息,您可以在以下位置找到:

hub.docker.com/_/registry

练习 3.10:创建本地 Docker 注册表

在这个练习中,您将为您的镜像设置一个 Docker 注册表,并在您的系统上运行它们。您不打算为您的团队或外部世界设置一个可用的注册表。您将设置一个漂亮的域名在您的系统上使用,以反映您正在进行的工作。这将帮助您决定是否将此注册表提供给您的团队或其他用户:

  1. 要设置您的域名,请将本地注册表的域名添加到您的系统主机文件中。在 Windows 系统上,您需要访问C:\Windows\System32\drivers\etc\hosts中的主机文件,而在 Linux 或 Max 上,它将是/etc/hosts。打开hosts文件并将以下行添加到文件中:
127.0.0.1       dev.docker.local

这将允许您使用dev.docker.local域,而不是在本地注册表中使用 localhost。

  1. 从 Docker Hub 拉取最新的registry镜像:
docker pull registry
  1. 使用以下命令来运行注册表容器。提供您可以访问注册表的端口;在这种情况下,使用端口5000。您还需要使用--restart=always选项,这将确保容器在 Docker 或系统需要重新启动时重新启动:
docker run -d -p 5000:5000 --restart=always --name registry registry

注意

在接下来的章节中,您将学习如何通过挂载来自主机系统的目录来扩展 Docker 容器的文件容量,然后作为运行容器的一部分运行。为此,您将使用-v--volume选项作为您的docker run命令的一部分,提供文件和容器上的挂载点。例如,您可以运行上述命令来挂载主机系统上的目录如下:

docker run -d -p 5000:5000 --restart=always --volume <directory_name>:/var/lib/registry:rw --name registry

  1. 运行docker ps命令,显示在您的系统上运行的registry容器,该容器已准备好接受并存储新的映像:
docker ps

该命令将返回以下输出:

CONTAINER ID  IMAGE     COMMAND                 CREATED
41664c379bec  registry  "/entrypoint.sh /etc…"  58 seconds ago
  1. 运行docker tag命令,使用注册表主机名和端口dev.docker.local:5000对现有映像进行标记。
docker tag vincesestodocker/basic-app:ver1 dev.docker.local:5000/basic-app:ver1

这将确保您的basic-app映像将自动推送到本地注册表:

docker push dev.docker.local:5000/basic-app:ver1
  1. 使用docker image remove命令从您当前正在使用的系统中删除原始映像:
docker image remove dev.docker.local:5000/basic-app:ver1
  1. 现在,通过在pull命令中包含注册表主机名和端口dev.docker.local:5000,从本地注册表中拉取映像:
docker pull dev.docker.local:5000/basic-app:ver1

这将带我们到本节的结束,我们已经在本地系统上创建了我们的注册表来存储我们的 Docker 映像。注册表本身很简单,实际上并不受支持,但它确实有助于帮助您了解注册表将如何工作以及如何与您的团队合作。如果您正在寻找更强大和受支持的映像,Docker 还提供了 Docker Trusted Registry,这是 Docker 提供的商业产品。

现在是时候测试到目前为止所学到的知识了。在下一个活动中,我们将修改PostgreSQL容器映像的构建脚本,以使用 Git 提交哈希而不是语义版本。

活动 3.01:使用 Git 哈希版本化的构建脚本

在本章的前面,您创建了一个构建脚本,自动化了正在构建的映像的标记和版本化过程。在这个活动中,您将进一步使用全景徒步应用程序,并被要求为PostgreSQL容器映像设置一个构建脚本。您可以使用之前创建的构建脚本,但是您需要修改脚本,不再使用语义版本,而是使用当前的 Git 提交哈希。另外,请确保您的构建脚本将构建的映像推送到您的 Docker 注册表。

完成所需的步骤如下:

  1. 确保您已经为您的PostgreSQL容器映像创建了一个运行的Dockerfile

  2. 创建您的构建脚本,执行以下操作:

  1. 设置您的 Docker 注册表的变量,正在构建的服务名称和 Git 哈希版本

  2. 将 Git 哈希版本打印到屏幕上

  3. 构建您的 PostgreSQL Docker 映像

  4. 将您的 Docker 映像推送到您的注册表

  1. 确保构建脚本运行并成功完成。

预期输出:

./BuildScript.sh 
++ REGISTRY=dev.docker.local:5000
++ SERVICENAME=basic-app
+++ git log -1 --format=%h
++ GIT_VERSION=49d3a10
++ echo 'version: 49d3a10 '
version: 49d3a10 
++ docker build -t dev.docker.local:5000/basic-app:49d3a10 .
Sending build context to Docker daemon  3.072kB
Step 1/1 : FROM postgres
 ---> 873ed24f782e
Successfully built 873ed24f782e
Successfully tagged dev.docker.local:5000/basic-app:49d3a10
++ docker push dev.docker.local:5000/basic-app:49d3a10
The push refers to repository [dev.docker.local:5000/basic-app]

注意

此活动的解决方案可以通过此链接找到。

在下一个活动中,您将通过更改docker run命令将本地 Docker 注册表存储在您的主目录中的一个目录中。

活动 3.02:配置本地 Docker 注册表存储

在本章中,您设置了您的注册表并开始使用基本选项来使其运行。注册表本身正在主机文件系统上存储镜像。在这个活动中,您希望更改docker run命令以将其存储在您的主目录中的一个目录中。您将创建一个名为test_registry的目录,并运行 Docker 命令将镜像存储在您的主目录中的test_registry目录中。

完成所需的步骤如下:

  1. 在您的主目录中创建一个目录来挂载您的本地注册表。

  2. 运行本地注册表。这次将新创建的卷挂载为注册表的一部分。

  3. 通过将新镜像推送到本地注册表来测试您的更改。

提示

在运行注册表容器时使用-v–volume选项。

预期输出:

在列出本地目录中的所有文件时,您将能够看到推送的图像:

ls  ~/test_registry/registry/docker/registry/v2/repositories/
basic-app

注意

此活动的解决方案可以通过此链接找到。

总结

本章演示了 Docker 如何允许用户使用镜像将其应用程序与工作环境一起打包,以便在不同的工作环境之间移动。您已经了解到 Docker 如何使用层和缓存来提高构建速度,并确保您也可以使用这些层来保留资源或磁盘空间。

我们还花了一些时间创建了一个只有一个图像层的基本图像。我们探讨了标记和标记实践,您可以采用这些实践来解决部署和发布图像相关的问题。我们还看了一下我们可以发布图像和与其他用户和开发人员共享图像的不同方法。我们才刚刚开始,还有很长的路要走。

在下一章中,我们将进一步使用我们的Dockerfiles来学习多阶段Dockerfiles的工作原理。我们还将找到更多优化我们的 Docker 镜像的方法,以便在发布到生产环境时获得更好的性能。

第四章:多阶段 Dockerfiles

概述

在本章中,我们将讨论普通的 Docker 构建。您将审查和实践Dockerfile的最佳实践,并学习使用构建模式和多阶段Dockerfile来创建和优化 Docker 镜像的大小。

介绍

在上一章中,我们学习了 Docker 注册表,包括私有和公共注册表。我们创建了自己的私有 Docker 注册表来存储 Docker 镜像。我们还学习了如何设置访问权限并将我们的 Docker 镜像存储在 Docker Hub 中。在本章中,我们将讨论多阶段Dockerfiles的概念。

多阶段Dockerfiles是在 Docker 版本 17.05 中引入的一个功能。当我们想要在生产环境中运行 Docker 镜像时,这个功能是可取的。为了实现这一点,多阶段Dockerfile将在构建过程中创建多个中间 Docker 镜像,并有选择地从一个阶段复制只有必要的构件到另一个阶段。

在引入多阶段 Docker 构建之前,构建模式被用来优化 Docker 镜像的大小。与多阶段构建不同,构建模式需要两个Dockerfiles和一个 shell 脚本来创建高效的 Docker 镜像。

在本章中,我们将首先检查普通的 Docker 构建以及与之相关的问题。接下来,我们将学习如何使用构建模式来优化 Docker 镜像的大小,并讨论与构建模式相关的问题。最后,我们将学习如何使用多阶段Dockerfiles来克服构建模式的问题。

普通的 Docker 构建

使用 Docker,我们可以使用Dockerfiles来创建自定义的 Docker 镜像。正如我们在第二章,使用 Dockerfiles 入门中讨论的那样,Dockerfile是一个包含如何创建 Docker 镜像的指令的文本文件。然而,在生产环境中运行它们时,拥有最小尺寸的 Docker 镜像是至关重要的。这使开发人员能够加快他们的 Docker 容器的构建和部署时间。在本节中,我们将构建一个自定义的 Docker 镜像,以观察与普通的 Docker 构建过程相关的问题。

考虑一个例子,我们构建一个简单的 Golang 应用程序。我们将使用以下Dockerfile部署一个用 Golang 编写的hello world应用程序:

# Start from latest golang parent image
FROM golang:latest
# Set the working directory
WORKDIR /myapp
# Copy source file from current directory to container
COPY helloworld.go .
# Build the application
RUN go build -o helloworld .
# Run the application
ENTRYPOINT ["./helloworld"]

这个Dockerfile以最新的 Golang 镜像作为父镜像开始。这个父镜像包含构建 Golang 应用程序所需的所有构建工具。接下来,我们将把/myapp目录设置为当前工作目录,并将helloworld.go源文件从主机文件系统复制到容器文件系统。然后,我们将使用RUN指令执行go build命令来构建应用程序。最后,使用ENTRYPOINT指令来运行在上一步中创建的helloworld可执行文件。

以下是helloworld.go文件的内容。这是一个简单的文件,当执行时将打印文本"Hello World"

package main
import "fmt"
func main() {
    fmt.Println("Hello World")
}

一旦Dockerfile准备好,我们可以使用docker image build命令构建 Docker 镜像。这个镜像将被标记为helloworld:v1

$ docker image build -t helloworld:v1 .

现在,使用docker image ls命令观察构建的镜像。您将获得类似以下的输出:

REPOSITORY   TAG   IMAGE ID       CREATED          SIZE
helloworld   v1    23874f841e3e   10 seconds ago   805MB

注意镜像大小。这个构建导致了一个大小为 805 MB 的巨大 Docker 镜像。在生产环境中拥有这些大型 Docker 镜像是低效的,因为它们将花费大量时间和带宽在网络上传输。小型 Docker 镜像更加高效,可以快速推送、拉取和部署。

除了镜像的大小,这些 Docker 镜像可能容易受到攻击,因为它们包含可能存在潜在安全漏洞的构建工具。

注意

潜在的安全漏洞可能会因给定的 Docker 镜像中包含哪些软件包而有所不同。例如,Java JDK 有许多漏洞。您可以在以下链接中详细了解与 Java JDK 相关的漏洞:

www.cvedetails.com/vulnerability-list/vendor_id-93/product_id-19116/Oracle-JDK.html

为了减少攻击面,建议在生产环境中运行 Docker 镜像时只包含必要的构件(例如编译代码)和运行时。例如,使用 Golang 时,需要 Go 编译器来构建应用程序,但不需要运行应用程序。

理想情况下,您希望有一个最小尺寸的 Docker 镜像,其中只包含运行时工具,而排除了用于构建应用程序的所有构建工具。

现在,我们将使用以下练习中的常规构建过程构建这样一个 Docker 镜像。

练习 4.01:使用正常构建过程构建 Docker 镜像

您的经理要求您将一个简单的 Golang 应用程序 docker 化。您已经提供了 Golang 源代码文件,您的任务是编译和运行此文件。在这个练习中,您将使用正常的构建过程构建一个 Docker 镜像。然后,您将观察最终 Docker 镜像的大小:

  1. 为此练习创建一个名为normal-build的新目录:
$ mkdir normal-build
  1. 转到新创建的normal-build目录:
$ cd normal-build
  1. normal-build目录中,创建一个名为welcome.go的文件。此文件将在构建时复制到 Docker 镜像中:
$ touch welcome.go
  1. 现在,使用您喜欢的文本编辑器打开welcome.go文件:
$ vim welcome.go
  1. 将以下内容添加到welcome.go文件中,保存并退出welcome.go文件:
package main
import "fmt"
func main() {
    fmt.Println("Welcome to multi-stage Docker builds")
}

这是一个用 Golang 编写的简单的hello world应用程序。这将在执行时输出"Welcome to multi-stage Docker builds"

  1. normal-build目录中,创建一个名为Dockerfile的文件:
$ touch Dockerfile
  1. 现在,使用您喜欢的文本编辑器打开Dockerfile
$ vim Dockerfile 
  1. 将以下内容添加到Dockerfile中并保存文件:
FROM golang:latest
WORKDIR /myapp
COPY welcome.go .
RUN go build -o welcome .
ENTRYPOINT ["./welcome"]

DockerfileFROM指令开头,指定最新的 Golang 镜像作为父镜像。这将把/myapp目录设置为 Docker 镜像的当前工作目录。然后,COPY指令将welcome.go源文件复制到 Docker 文件系统中。接下来是go build命令,用于构建您创建的 Golang 代码。最后,将执行 welcome 代码。

  1. 现在,构建 Docker 镜像:
$ docker build -t welcome:v1 .

您将看到该镜像成功构建,镜像 ID 为b938bc11abf1,标记为welcome:v1

图 4.1:构建 Docker 镜像

图 4.1:构建 Docker 镜像

  1. 使用docker image ls命令列出计算机上所有可用的 Docker 镜像:
$ docker image ls

该命令应返回以下输出:

图 4.2:列出所有 Docker 镜像

图 4.2:列出所有 Docker 镜像

可以观察到welcome:v1镜像的镜像大小为805MB

在本节中,我们讨论了如何使用普通的 Docker 构建过程来构建 Docker 镜像,并观察了其大小。结果是一个巨大的 Docker 镜像,大小超过 800 MB。这些大型 Docker 镜像的主要缺点是它们将花费大量时间来构建、部署、推送和拉取网络。因此,建议尽可能创建最小尺寸的 Docker 镜像。在下一节中,我们将讨论如何使用构建模式来优化镜像大小。

什么是构建模式?

构建模式是一种用于创建最优尺寸的 Docker 镜像的方法。它使用两个 Docker 镜像,并从一个镜像选择性地复制必要的构件到另一个镜像。第一个 Docker 镜像称为构建镜像,用作构建环境,用于从源代码构建可执行文件。这个 Docker 镜像包含在构建过程中所需的编译器、构建工具和开发依赖项。

第二个 Docker 镜像称为运行时镜像,用作运行由第一个 Docker 容器创建的可执行文件的运行时环境。这个 Docker 镜像只包含可执行文件、依赖项和运行时工具。使用一个 shell 脚本来使用docker container cp命令复制构件。

使用构建模式构建镜像的整个过程包括以下步骤:

  1. 创建Build Docker 镜像。

  2. Build Docker 镜像创建一个容器。

  3. 将构件从Build Docker 镜像复制到本地文件系统。

  4. 使用复制的构件构建Runtime Docker 镜像:

图 4.3:使用构建模式构建镜像

图 4.3:使用构建模式构建镜像

如前面的图所示,Build Dockerfile用于创建构建容器,其中包含构建源代码所需的所有工具,包括编译器和构建工具,如 Maven、Gradle 和开发依赖项。创建构建容器后,shell 脚本将从构建容器复制可执行文件到 Docker 主机。最后,将使用从Build容器复制的可执行文件创建Runtime容器。

现在,观察如何使用构建模式来创建最小的 Docker 镜像。以下是用于创建Build Docker 容器的第一个Dockerfile。这个Dockerfile被命名为 Dockerfile.build,以区别于Runtime Dockerfile

# Start from latest golang parent image
FROM golang:latest
# Set the working directory
WORKDIR /myapp
# Copy source file from current directory to container
COPY helloworld.go .
# Build the application
RUN go build -o helloworld .
# Run the application
ENTRYPOINT ["./helloworld"]

这是我们观察到的与普通 Docker 构建相同的Dockerfile。这是用来从helloworld.go源文件创建helloworld可执行文件的。

以下是用于构建Runtime Docker 容器的第二个Dockerfile

# Start from latest alpine parent image
FROM alpine:latest
# Set the working directory
WORKDIR /myapp
# Copy helloworld app from current directory to container
COPY helloworld .
# Run the application
ENTRYPOINT ["./helloworld"]

与第一个Dockerfile相反,它是从golang父镜像创建的,这个第二个Dockerfile使用alpine镜像作为其父镜像,因为它是一个仅有 5MB 的最小尺寸的 Docker 镜像。这个镜像使用 Alpine Linux,一个轻量级的 Linux 发行版。接下来,/myapp目录被配置为工作目录。最后,helloworld构件被复制到 Docker 镜像中,并且使用ENTRYPOINT指令来运行应用程序。

这个helloworld构件是在第一个Dockerfile中执行的go build -o helloworld .命令的结果。我们将使用一个 shell 脚本将这个构件从build Docker 容器复制到本地文件系统,然后从那里将这个构件复制到运行时 Docker 镜像。

考虑以下用于在 Docker 容器之间复制构建构件的 shell 脚本:

#!/bin/sh
# Build the builder Docker image 
docker image build -t helloworld-build -f Dockerfile.build .
# Create container from the build Docker image
docker container create --name helloworld-build-container   helloworld-build
# Copy build artifacts from build container to the local filesystem
docker container cp helloworld-build-container:/myapp/helloworld .
# Build the runtime Docker image
docker image build -t helloworld .
# Remove the build Docker container
docker container rm -f helloworld-build-container
# Remove the copied artifact
rm helloworld

这个 shell 脚本将首先使用Dockerfile.build文件构建helloworld-build Docker 镜像。下一步是从helloworld-build镜像创建一个 Docker 容器,以便我们可以将helloworld构件复制到 Docker 主机。容器创建后,我们需要执行命令将helloworld构件从helloworld-build-container复制到 Docker 主机的当前目录。现在,我们可以使用docker image build命令构建运行时容器。最后,我们将执行必要的清理任务,如删除中间构件,比如helloworld-build-container容器和helloworld可执行文件。

一旦我们执行了 shell 脚本,我们应该能够看到两个 Docker 镜像:

REPOSITORY         TAG      IMAGE ID       CREATED       SIZE
helloworld         latest   faff247e2b35   3 hours ago   7.6MB
helloworld-build   latest   f8c10c5bd28d   3 hours ago   805MB

注意两个 Docker 镜像之间的大小差异。helloworld Docker 镜像的大小仅为 7.6MB,这是从 805MB 的helloworld-build镜像中大幅减少的。

正如我们所看到的,构建模式可以通过仅复制必要的构件到最终镜像来大大减小 Docker 镜像的大小。然而,构建模式的缺点是我们需要维护两个Dockerfiles和一个 shell 脚本。

在下一个练习中,我们将亲自体验使用构建模式创建优化的 Docker 镜像。

练习 4.02:使用构建模式构建 Docker 镜像

练习 4.01中,使用常规构建过程构建 Docker 镜像,您创建了一个 Docker 镜像来编译和运行 Golang 应用程序。现在应用程序已经准备就绪,但经理对 Docker 镜像的大小不满意。您被要求创建一个最小尺寸的 Docker 镜像来运行应用程序。在这个练习中,您将使用构建模式优化 Docker 镜像:

  1. 为这个练习创建一个名为builder-pattern的新目录:
$ mkdir builder-pattern
  1. 导航到新创建的builder-pattern目录:
$ cd builder-pattern
  1. builder-pattern目录中,创建一个名为welcome.go的文件。这个文件将在构建时复制到 Docker 镜像中:
$ touch welcome.go
  1. 现在,使用您喜欢的文本编辑器打开welcome.go文件:
$ vim welcome.go
  1. 将以下内容添加到welcome.go文件中,然后保存并退出该文件:
package main
import "fmt"
func main() {
    fmt.Println("Welcome to multi-stage Docker builds")
}

这是一个用 Golang 编写的简单的hello world应用程序。一旦执行,它将输出“欢迎来到多阶段 Docker 构建”。

  1. builder-pattern目录中,创建一个名为Dockerfile.build的文件。这个文件将包含您将用来创建build Docker 镜像的所有指令:
$ touch Dockerfile.build
  1. 现在,使用您喜欢的文本编辑器打开Dockerfile.build
$ vim Dockerfile.build
  1. 将以下内容添加到Dockerfile.build文件中并保存该文件:
FROM golang:latest
WORKDIR /myapp
COPY welcome.go .
RUN go build -o welcome .
ENTRYPOINT ["./welcome"]

这与您在练习 4.01中为Dockerfile创建的内容相同,使用常规构建过程构建 Docker 镜像

  1. 接下来,为运行时容器创建Dockerfile。在builder-pattern目录中,创建一个名为Dockerfile的文件。这个文件将包含您将用来创建运行时 Docker 镜像的所有指令:
$ touch Dockerfile
  1. 现在,使用您喜欢的文本编辑器打开Dockerfile
$ vim Dockerfile
  1. 将以下内容添加到Dockerfile并保存该文件:
FROM scratch
WORKDIR /myapp
COPY welcome .
ENTRYPOINT ["./welcome"]

这个Dockerfile使用了 scratch 镜像,这是 Docker 中最小的镜像,作为父镜像。然后,它将/myapp目录配置为工作目录。接下来,欢迎可执行文件从 Docker 主机复制到运行时 Docker 镜像。最后,使用ENTRYPOINT指令来执行欢迎可执行文件。

  1. 创建一个 shell 脚本,在两个 Docker 容器之间复制可执行文件。在builder-pattern目录中,创建一个名为build.sh的文件。这个文件将包含协调两个 Docker 容器之间构建过程的步骤:
$ touch build.sh
  1. 现在,使用您喜欢的文本编辑器打开build.sh文件:
$ vim build.sh
  1. 将以下内容添加到 shell 脚本中并保存文件:
#!/bin/sh
echo "Creating welcome builder image"
docker image build -t welcome-builder:v1 -f Dockerfile.build .
docker container create --name welcome-builder-container   welcome-builder:v1
docker container cp welcome-builder-container:/myapp/welcome .
docker container rm -f welcome-builder-container
echo "Creating welcome runtime image"
docker image build -t welcome-runtime:v1 .
rm welcome

这个 shell 脚本将首先构建welcome-builder Docker 镜像并从中创建一个容器。然后它将从容器中将编译的 Golang 可执行文件复制到本地文件系统。接下来,welcome-builder-container容器将被删除,因为它是一个中间容器。最后,将构建welcome-runtime镜像。

  1. build.sh shell 脚本添加执行权限:
$ chmod +x build.sh
  1. 现在您已经有了两个Dockerfiles和 shell 脚本,通过执行build.sh shell 脚本构建 Docker 镜像:
$ ./build.sh

镜像将成功构建并标记为welcome-runtime:v1

图 4.4:构建 Docker 镜像

图 4.4:构建 Docker 镜像

  1. 使用docker image ls 命令列出计算机上所有可用的 Docker 镜像:
docker image ls

您应该得到所有可用 Docker 镜像的列表,如下图所示:

图 4.5:列出所有 Docker 镜像

图 4.5:列出所有 Docker 镜像

从前面的输出中可以看到,有两个 Docker 镜像可用。welcome-builder 具有所有构建工具,大小为 805 MB,而 welcome-runtime 的镜像大小显著较小,为 2.01 MB。golang:latest是我们用作welcome-builder父镜像的 Docker 镜像。

在这个练习中,您学会了如何使用构建模式来减小 Docker 镜像的大小。然而,使用构建模式来优化 Docker 镜像的大小意味着我们必须维护两个Dockerfiles和一个 shell 脚本。在下一节中,让我们观察如何通过使用多阶段Dockerfile来消除它们。

多阶段 Dockerfile 简介

多阶段 Dockerfile是一个功能,允许单个Dockerfile包含多个阶段,可以生成优化的 Docker 镜像。正如我们在上一节中观察到的构建模式一样,这些阶段通常包括一个构建状态,用于从源代码构建可执行文件,以及一个运行时阶段,用于运行可执行文件。多阶段Dockerfiles将在Dockerfile中为每个阶段使用多个FROM指令,并且每个阶段将以不同的基础镜像开始。只有必要的文件将从一个阶段有选择地复制到另一个阶段。在多阶段Dockerfiles之前,这是通过构建模式实现的,正如我们在上一节中讨论的那样。

多阶段 Docker 构建允许我们创建与构建模式类似但消除了与之相关的问题的最小尺寸 Docker 镜像。正如我们在前面的示例中看到的,构建模式需要维护两个Dockerfile和一个 shell 脚本。相比之下,多阶段 Docker 构建只需要一个Dockerfile,并且不需要任何 shell 脚本来在 Docker 容器之间复制可执行文件。此外,构建模式要求您在将可执行文件复制到最终 Docker 镜像之前将其复制到 Docker 主机。而多阶段 Docker 构建不需要这样做,因为我们可以使用--from标志在 Docker 镜像之间复制可执行文件,而无需将其复制到 Docker 主机。

现在,让我们观察一下多阶段Dockerfile的结构:

# Start from latest golang parent image
FROM golang:latest
# Set the working directory
WORKDIR /myapp
# Copy source file from current directory to container
COPY helloworld.go .
# Build the application
RUN go build -o helloworld .
# Start from latest alpine parent image
FROM alpine:latest
# Set the working directory
WORKDIR /myapp
# Copy helloworld app from current directory to container
COPY --from=0 /myapp/helloworld .
# Run the application
ENTRYPOINT ["./helloworld"]

普通的Dockerfile和多阶段Dockerfile之间的主要区别在于,多阶段Dockerfile将使用多个FROM指令来构建每个阶段。每个新阶段将从一个新的父镜像开始,并且不包含来自先前镜像的任何内容,除了有选择地复制的可执行文件。使用COPY --from=0将可执行文件从第一阶段复制到第二阶段。

构建 Docker 镜像并将镜像标记为multi-stage:v1

docker image build -t multi-stage:v1 .

现在,您可以列出可用的 Docker 镜像:

REPOSITORY    TAG      IMAGE ID       CREATED         SIZE
multi-stage   latest   75e1f4bcabd0   7 seconds ago   7.6MB

您可以看到,这导致了与构建模式中观察到的相同大小的 Docker 镜像。

注意

多阶段Dockerfile减少了所需的Dockerfile数量,并消除了 shell 脚本,而不会对镜像的大小产生任何影响。

默认情况下,多阶段Dockerfile中的阶段由整数编号引用,从第一阶段开始为0。可以通过在FROM指令中添加AS <NAME>来为这些阶段命名,以增加可读性和可维护性。以下是您在前面的代码块中观察到的多阶段Dockerfile的改进版本:

# Start from latest golang parent image
FROM golang:latest AS builder 
# Set the working directory
WORKDIR /myapp
# Copy source file from current directory to container
COPY helloworld.go .
# Build the application
RUN go build -o helloworld .
# Start from latest alpine parent image
FROM alpine:latest AS runtime
# Set the working directory
WORKDIR /myapp
# Copy helloworld app from current directory to container
COPY --from=builder /myapp/helloworld .
# Run the application
ENTRYPOINT ["./helloworld"]

在前面的示例中,我们将第一阶段命名为builder,第二阶段命名为runtime,如下所示:

FROM golang:latest AS builder
FROM alpine:latest AS runtime

然后,在第二阶段复制工件时,您使用了--from标志的名称builder

COPY --from=builder /myapp/helloworld .

在构建多阶段Dockerfile时,可能会有一些情况,您只想构建到特定的构建阶段。考虑到您的Dockerfile有两个阶段。第一个是构建开发阶段,包含所有构建和调试工具,第二个是构建仅包含运行时工具的生产镜像。在项目的代码开发阶段,您可能只需要构建到开发阶段,以便在必要时测试和调试代码。在这种情况下,您可以使用docker build命令的--target标志来指定一个中间阶段作为最终镜像的阶段:

docker image build --target builder -t multi-stage-dev:v1 .

在前面的例子中,您使用了--target builder来停止在构建阶段停止构建。

在下一个练习中,您将学习如何使用多阶段Dockerfile来创建一个大小优化的 Docker 镜像。

练习 4.03:使用多阶段 Docker 构建构建 Docker 镜像

Exercise 4.02使用构建模式构建 Docker 镜像中,您使用了构建模式来优化 Docker 镜像的大小。然而,这会带来操作负担,因为您需要在 Docker 镜像构建过程中管理两个Dockerfiles和一个 shell 脚本。在这个练习中,您将使用多阶段Dockerfile来消除这种操作负担。

  1. 为这个练习创建一个名为multi-stage的新目录:
mkdir multi-stage
  1. 导航到新创建的multi-stage目录:
cd multi-stage
  1. multi-stage目录中,创建一个名为welcome.go的文件。这个文件将在构建时复制到 Docker 镜像中:
$ touch welcome.go
  1. 现在,使用您喜欢的文本编辑器打开welcome.go文件:
$ vim welcome.go
  1. 将以下内容添加到welcome.go文件中,然后保存并退出此文件:
package main
import "fmt"
func main() {
    fmt.Println("Welcome to multi-stage Docker builds")
}

这是一个用 Golang 编写的简单的hello world应用程序。一旦执行,它将输出"Welcome to multi-stage Docker builds"

在 multi-stage 目录中,创建一个名为Dockerfile的文件。这个文件将是多阶段Dockerfile

touch Dockerfile
  1. 现在,使用您喜欢的文本编辑器打开Dockerfile
vim Dockerfile
  1. 将以下内容添加到Dockerfile中并保存文件:
FROM golang:latest AS builder
WORKDIR /myapp
COPY welcome.go .
RUN go build -o welcome .
FROM scratch
WORKDIR /myapp
COPY --from=builder /myapp/welcome .
ENTRYPOINT ["./welcome"]

这个多阶段Dockerfile使用最新的golang镜像作为父镜像,这个阶段被命名为builder。接下来,指定/myapp目录作为当前工作目录。然后,使用COPY指令复制welcome.go源文件,并使用RUN指令构建 Golang 文件。

Dockerfile的下一阶段使用scratch镜像作为父镜像。这将把/myapp目录设置为 Docker 镜像的当前工作目录。然后,使用COPY指令将welcome可执行文件从构建阶段复制到此阶段。最后,使用ENTRYPOINT运行welcome可执行文件。

  1. 使用以下命令构建 Docker 镜像:
docker build -t welcome-optimized:v1 .

镜像将成功构建并标记为welcome-optimized:v1

图 4.6:构建 Docker 镜像

图 4.6:构建 Docker 镜像

  1. 使用docker image ls命令列出计算机上所有可用的 Docker 镜像。这些镜像可以在您的计算机上使用,无论是从 Docker Registry 拉取还是在您的计算机上构建:
docker images

从以下输出中可以看出,welcome-optimized镜像与您在*练习 4.02 中构建的welcome-runtime镜像大小相同,构建 Docker 镜像使用以下命令:

图 4.7:列出所有 Docker 镜像

图 4.7:列出所有 Docker 镜像

在本练习中,您学习了如何使用多阶段Dockerfiles构建优化的 Docker 镜像。以下表格总结了构建器模式和多阶段Docker Builds之间的关键差异:

图 4.8:构建器模式和多阶段 Docker 构建之间的差异

图 4.8:构建器模式和多阶段 Docker 构建之间的差异

在下一节中,我们将回顾编写Dockerfile时应遵循的最佳实践。

Dockerfile 最佳实践

在前一节中,我们讨论了如何使用多阶段Dockerfiles构建高效的 Docker 镜像。在本节中,我们将介绍编写Dockerfiles的其他推荐最佳实践。这些最佳实践将确保减少构建时间、减少镜像大小、增加安全性和增加 Docker 镜像的可维护性。

使用适当的父镜像

在构建高效的 Docker 镜像时,使用适当的基础镜像是其中的关键建议之一。

在构建自定义 Docker 镜像时,建议始终使用Docker Hub的官方镜像作为父镜像。这些官方镜像将确保遵循所有最佳实践,提供文档,并应用安全补丁。例如,如果您的应用程序需要JDK(Java 开发工具包),您可以使用openjdk官方 Docker 镜像,而不是使用通用的ubuntu镜像并在ubuntu镜像上安装 JDK:

图 4.9:使用适当的父镜像

图 4.9:使用适当的父镜像

其次,在为生产环境构建 Docker 镜像时,避免使用父镜像的latest标签。latest标签可能会指向 Docker Hub 发布新版本时的新版本镜像,而新版本可能与您的应用程序不兼容,导致生产环境中的故障。相反,最佳实践是始终使用特定版本的标签作为父镜像:

图 4.10:避免使用父镜像的最新标签

图 4.10:避免使用父镜像的最新标签

最后,使用父镜像的最小版本对于获得最小尺寸的 Docker 镜像至关重要。Docker Hub 中的大多数官方 Docker 镜像都是围绕 Alpine Linux 镜像构建的最小尺寸镜像。此外,在我们的示例中,我们可以使用JRE(Java 运行环境)来运行应用程序,而不是 JDK,后者包含构建工具:

图 4.11:使用最小尺寸的镜像

图 4.11:使用最小尺寸的镜像

openjdk:8-jre-alpine镜像的大小仅为 84.9 MB,而openjdk:8的大小为 488 MB。

为了更好的安全性,使用非根用户

默认情况下,Docker 容器以 root(id = 0)用户运行。这允许用户执行所有必要的管理活动,如更改系统配置、安装软件包和绑定特权端口。然而,在生产环境中运行 Docker 容器时,这是高风险的,被认为是一种不良的安全实践,因为黑客可以通过攻击 Docker 容器内运行的应用程序来获得对 Docker 主机的 root 访问权限。

以非 root 用户身份运行容器是改善 Docker 容器安全性的推荐最佳实践。这将遵循最小特权原则,确保应用程序只具有执行其任务所需的最低权限。我们可以使用两种方法来以非 root 用户身份运行容器:使用--user(或-u)标志,以及使用USER指令。

使用--user(或-u)标志与docker run命令是在运行 Docker 容器时更改默认用户的一种方法。--user(或-u)标志可以指定用户名或用户 ID:

$ docker run --user=9999 ubuntu:focal

在前面的命令中,我们指定了用户 ID 为9999。如果我们将用户指定为 ID,则相应的用户不必在 Docker 容器中可用。

此外,我们可以在Dockerfile中使用USER指令来定义默认用户。但是,可以在启动 Docker 容器时使用--user标志覆盖此值:

FROM ubuntu:focal
RUN apt-get update 
RUN useradd demo-user
USER demo-user
CMD whoami

在前面的例子中,我们使用了USER指令将默认用户设置为demo-user。这意味着在USER指令之后的任何命令都将以demo-user身份执行。

使用 dockerignore

.dockerignore文件是 Docker 上下文中的一个特殊文本文件,用于指定在构建 Docker 镜像时要排除的文件列表。一旦执行docker build命令,Docker 客户端将整个构建上下文打包为一个 TAR 归档文件,并将其上传到 Docker 守护程序。当我们执行docker build命令时,输出的第一行是Sending build context to Docker daemon,这表示 Docker 客户端正在将构建上下文上传到 Docker 守护程序。

Sending build context to Docker daemon  18.6MB
Step 1/5 : FROM ubuntu:focal

每次构建 Docker 镜像时,构建上下文都将被发送到 Docker 守护程序。由于这将在 Docker 镜像构建过程中占用时间和带宽,建议排除所有在最终 Docker 镜像中不需要的文件。.dockerignore文件可用于实现此目的。除了节省时间和带宽外,.dockerignore文件还用于排除机密文件,例如密码文件和密钥文件,以防止其出现在构建上下文中。

.dockerignore文件应该创建在构建上下文的根目录中。在将构建上下文发送到 Docker 守护程序之前,Docker 客户端将在构建上下文的根目录中查找.dockerignore文件。如果.dockerignore文件存在,Docker 客户端将从构建上下文中排除.dockerignore文件中提到的所有文件。

以下是一个示例.dockerignore文件的内容:

PASSWORDS.txt
tmp/
*.md
!README.md

在上面的示例中,我们特别排除了PASSWORDS.txt文件和tmp目录,以及除README.md文件之外的所有扩展名为.md的文件。

最小化层

Dockerfile中的每一行都将创建一个新的层,这将占用 Docker 镜像中的空间。因此,在构建 Docker 镜像时,建议尽可能少地创建层。为了实现这一点,尽可能合并RUN指令。

例如,考虑以下Dockerfile,它将首先更新软件包存储库,然后安装redis-servernginx软件包:

FROM ubuntu:focal
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get install -y redis-server

这个Dockerfile可以通过合并三个RUN指令来优化:

FROM ubuntu:focal
RUN apt-get update \
  && apt-get install -y nginx redis-server

不安装不必要的工具

不安装不必要的调试工具(如vimcurltelnet)并删除不必要的依赖项可以帮助创建尺寸小的高效 Docker 镜像。一些软件包管理器(如apt)将自动安装推荐和建议的软件包,以及所需的软件包。我们可以通过在apt-get install命令中指定no-install-recommends标志来避免这种情况:

FROM ubuntu:focal
RUN apt-get update \
  && apt-get install --no-install-recommends -y nginx 

在上面的示例中,我们正在使用no-install-recommends标志安装nginx软件包,这将帮助将最终镜像大小减少约 10MB。

除了使用no-install-recommends标志之外,我们还可以删除apt软件包管理器的缓存,以进一步减少最终的 Docker 镜像大小。这可以通过在apt-get install命令的末尾运行rm -rf /var/lib/apt/lists/*来实现:

FROM ubuntu:focal
RUN apt-get update \
    && apt-get install --no-install-recommends -y nginx \
    && rm -rf /var/lib/apt/lists/*

在本节中,我们讨论了编写Dockerfile时的最佳实践。遵循这些最佳实践将有助于减少构建时间,减小镜像大小,增加安全性,并增加 Docker 镜像的可维护性。

现在,让我们通过在下一个活动中使用多阶段 Docker 构建部署一个 Golang HTTP 服务器来测试我们的知识。

活动 4.01:使用多阶段 Docker 构建部署 Golang HTTP 服务器

假设你被要求将一个 Golang HTTP 服务器部署到一个 Docker 容器中。你的经理要求你构建一个最小尺寸的 Docker 镜像,并在构建Dockerfile时遵循最佳实践。

这个 Golang HTTP 服务器将根据调用 URL 返回不同的响应:

图 4.12:基于调用 URL 的响应

图 4.12:基于调用 URL 的响应

你的任务是使用多阶段Dockerfile来将下面代码块中给出的 Golang 应用程序 docker 化:

package main
import (
    "net/http"
    "fmt"
    "log"
    "os"
)
func main() {
    http.HandleFunc("/", defaultHandler)
    http.HandleFunc("/contact", contactHandler)
    http.HandleFunc("/login", loginHandler)
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    log.Println("Service started on port " + port)
    err := http.ListenAndServe(":"+port, nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
        return
    }
}
func defaultHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "<h1>Home Page</h1>")
}
func contactHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "<h1>Contact Us</h1>")
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "<h1>Login Page</h1>")
}

执行以下步骤完成这个活动:

  1. 创建一个文件夹来存储活动文件。

  2. 创建一个main.go文件,其中包含前面代码块中提供的代码。

  3. 使用两个阶段创建一个多阶段Dockerfile。第一阶段将使用golang镜像。这个阶段将使用go build命令构建 Golang 应用程序。第二阶段将使用alpine镜像。这个阶段将从第一阶段复制可执行文件并执行它。

  4. 构建并运行 Docker 镜像。

  5. 完成后,停止并移除 Docker 容器。

当你访问 URL http://127.0.0.1:8080/时,你应该得到以下输出:

图 4.13:活动 4.01 的预期输出

图 4.13:活动 4.01 的预期输出

注意

这个活动的解决方案可以通过此链接找到。

总结

我们从定义普通的 Docker 构建开始,使用普通的 Docker 构建过程创建了一个简单的 Golang Docker 镜像。然后我们观察了生成的 Docker 镜像的大小,并讨论了最小尺寸的 Docker 镜像如何加快 Docker 容器的构建和部署时间,并通过减少攻击面来增强安全性。

然后,我们使用构建器模式创建了最小尺寸的 Docker 镜像,在这个过程中利用了两个Dockerfiles和一个 shell 脚本来创建镜像。我们探讨了多阶段 Docker 构建——这是 Docker 17.05 版本引入的新功能,可以帮助消除维护两个Dockerfiles和一个 shell 脚本的操作负担。最后,我们讨论了编写Dockerfiles的最佳实践以及这些最佳实践如何确保减少构建时间、减小镜像大小、增强安全性,同时提高 Docker 镜像的可维护性。

在下一章中,我们将介绍docker-compose以及如何使用它来定义和运行多容器 Docker 应用程序。

第五章:使用 Docker Compose 组合环境

概述

本章涵盖了使用 Docker Compose 创建和管理多容器应用程序。您将学习如何创建 Docker Compose 文件来定义复杂的容器化应用程序,以及如何运行 Docker Compose CLI 来管理多容器应用程序的生命周期。本章将使您能够使用不同的方法配置 Docker Compose 应用程序,并设计依赖于其他应用程序的应用程序。

介绍

在前几章中,我们讨论了如何使用 Docker 容器和Dockerfiles来创建容器化应用程序。随着应用程序变得更加复杂,容器及其配置的管理变得更加复杂。

例如,想象一下,您正在开发一个具有前端、后端、支付和订购微服务的在线商店。每个微服务在构建、打包和配置之前都是用最合适的编程语言实现的。因此,在 Docker 生态系统中,复杂应用程序被设计为在单独的容器中运行。不同的容器需要多个Dockerfiles来定义 Docker 镜像。

它们还需要复杂的命令来配置、运行和排除应用程序故障。所有这些都可以通过Docker Compose来实现,这是一个用于定义和管理多个容器中的应用程序的工具。诸如 YAML 文件之类的复杂应用程序可以在 Docker Compose 中用单个命令进行配置和运行。它适用于各种环境,包括开发、测试、持续集成CI)流水线和生产环境。

Docker Compose 的基本特性可以分为三类:

  • 隔离:Docker Compose 允许您在完全隔离的环境中运行多个复杂应用程序实例。虽然这似乎是一个微不足道的功能,但它使得在开发人员机器、CI 服务器或共享主机上运行多个相同应用程序堆栈的副本成为可能。因此,资源共享增加了利用率,同时减少了操作复杂性。

  • 有状态数据管理:Docker Compose 管理容器的卷,以便它们不会丢失之前运行的数据。这个特性使得更容易创建和操作那些在磁盘上存储状态的应用程序,比如数据库。

  • 迭代设计:Docker Compose 与明确定义的配置一起工作,该配置由多个容器组成。配置中的容器可以通过新容器进行扩展。例如,想象一下你的应用程序中有两个容器。如果添加第三个容器并运行 Docker Compose 命令,前两个容器将不会被重新启动或重新创建。Docker Compose 只会创建并加入新添加的第三个容器。

这些特性使得 Compose 成为在各种平台上创建和管理多个容器应用程序的重要工具。在本章中,您将看到 Docker Compose 如何帮助您管理复杂应用程序的完整生命周期。

您将首先深入了解 Compose CLI 和文件解剖。之后,您将学习如何使用多种技术配置应用程序以及如何定义服务依赖关系。由于 Docker Compose 是 Docker 环境中的重要工具,因此技术和实践经验对您的工具箱至关重要。

Docker Compose CLI

Docker Compose 与Docker Engine一起工作,用于创建和管理多容器应用程序。为了与 Docker Engine 交互,Compose 使用名为docker-compose的 CLI 工具。在 Mac 和 Windows 系统上,docker-compose已经是 Docker Desktop 的一部分。然而,在 Linux 系统上,您需要在安装 Docker Engine 后安装docker-compose CLI 工具。它被打包成一个单独的可执行文件,您可以使用以下命令在 Linux 系统上安装它。

在 Linux 中安装 Docker Compose CLI

  1. 使用以下命令将二进制文件下载到/usr/local/bin中:
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  1. 使用以下命令使下载的二进制文件可执行:
sudo chmod +x /usr/local/bin/docker-compose
  1. 在所有操作系统的终端上使用以下命令测试 CLI 和安装:
docker-compose version

如果安装正确,您将看到 CLI 及其依赖项的版本如下。例如,在以下输出中,docker-compose CLI 的版本为1.25.1-rc1,其依赖项docker-pyCPythonOpenSSL也列出了它们的版本:

图 5.1:docker-compose 版本输出

图 5.1:docker-compose 版本输出

到目前为止,我们已经学会了如何在 Linux 中安装 Docker Compose CLI。现在我们将研究管理多容器应用程序的完整生命周期的命令和子命令。

Docker Compose CLI 命令

docker-compose命令能够管理多容器应用程序的完整生命周期。通过子命令,可以启动、停止和重新创建服务。此外,还可以检查正在运行的堆栈的状态并获取日志。在本章中,您将通过实践掌握这些基本命令。同样,可以使用以下命令列出所有功能的预览:

docker-compose --help

命令的输出应该如下所示:

图 5.2:docker-compose 命令

图 5.2:docker-compose 命令

有三个基本的docker-compose命令用于管理应用程序的生命周期。生命周期和命令可以如下所示:

图 5.3:docker-compose 生命周期

图 5.3:docker-compose 生命周期

  • docker-compose up:此命令创建并启动配置中定义的容器。可以构建容器镜像或使用来自注册表的预构建镜像。此外,可以使用-d--detach标志在detached模式下在后台运行容器。对于长时间运行的容器(例如 Web 服务器),使用detached模式非常方便,我们不希望在短期内停止它们。可以使用docker-compose up --help命令检查其他选项和标志。

  • docker-compose ps:此命令列出容器及其状态信息。对于故障排除和容器健康检查非常有帮助。例如,如果创建了一个具有后端和前端的双容器应用程序,可以使用docker-compose ps命令检查每个容器的状态。这有助于找出您的后端或前端是停机、不响应其健康检查,还是由于错误配置而无法启动。

  • docker-compose down:此命令停止并删除所有资源,包括容器、网络、镜像和卷。

Docker Compose 文件

多容器应用程序是使用docker-compose CLI 运行和定义的。按照惯例,这些文件的默认名称是docker-compose.yaml。Docker Compose 是一个强大的工具;然而,它的强大取决于配置。因此,知道如何创建docker-compose.yaml文件是必不可少的,并且需要特别注意。

注意

Docker Compose 默认使用docker-compose.yamldocker-compose.yml文件扩展名。

docker-compose.yaml 文件由四个主要部分组成,如 图 5.4 所示:

图 5.4:docker-compose 文件结构

图 5.4:docker-compose 文件结构

  • version:此部分定义了 docker-compose 文件的语法版本,目前最新的语法版本是 3

  • services:此部分描述了将在需要时构建并由 docker-compose 启动的 Docker 容器。

  • networks:此部分描述了服务将使用的网络。

  • volumes:此部分描述了将挂载到服务中的数据卷。

对于 services 部分,有两个关键选项可以创建容器。第一个选项是构建容器,第二个选项是使用来自注册表的 Docker 镜像。当您在本地创建和测试容器时,建议构建镜像。另一方面,对于生产和 CI/CD 系统,使用来自注册表的 Docker 镜像更快速、更简便。

假设您想要使用名为 Dockerfile-serverDockerfile 构建服务器容器。然后,您需要将文件放在具有以下文件夹结构的 server 文件夹中:

图 5.5:文件夹结构

图 5.5:文件夹结构

tree 命令的输出显示了一个包含 Dockerfile-serverserver 文件夹。

当在根目录的 docker-compose.yaml 文件中定义了以下内容时,server 容器将在运行服务之前构建:

version: "3"
services:
  server:
    build:
      context: ./server
      dockerfile: Dockerfile-server

同样,如果您想要使用来自 Docker 注册表的镜像,可以仅定义一个带有 image 字段的服务:

version: "3"
services:
  server:
    image: nginx

Docker Compose 默认创建一个网络,每个容器都连接到此网络。此外,容器可以使用主机名连接到其他容器。例如,假设您在 webapp 文件夹中有以下 docker-compose.yaml 文件:

version: "3"
services:
  server:
    image: nginx
  db:
    image: postgres
    ports:
      - "8032:5432"

当您使用此配置启动 docker-compose 时,它首先创建了名为 webapp_default 的网络。随后,docker-compose 创建了 serverdb 容器,并分别以 serverdb 的名称加入了 webapp_default 网络。

此外,server容器可以使用其container端口和主机名连接到数据库,如下所示:postgres://db:5432。同样,数据库可以通过主机端口8032从主机机器访问,如下所示:postgres://localhost:8032。网络结构如下图所示:

图 5.6:网络结构

图 5.6:网络结构

docker-compose.yaml文件中,您可以定义自定义网络,而不是使用默认网络。network配置使您能够基于自定义网络驱动程序创建复杂的网络技术。Docker 容器的网络技术在第六章Docker 网络简介中有全面介绍。在接下来的章节中将介绍如何使用自定义网络驱动程序扩展 Docker 引擎。

Docker Compose 还作为docker-compose.yaml文件的一部分创建和管理卷。卷在容器之间提供持久性,并由 Docker 引擎管理。所有服务容器都可以重用卷。换句话说,数据可以在容器之间共享,用于同步、数据准备和备份操作。在第七章Docker 存储中,将详细介绍 Docker 的卷。

使用以下docker-compose.yaml文件,docker-compose将使用 Docker 引擎中的默认卷插件创建名为data的卷。此卷将被挂载到database容器的/database路径和backup容器的/backup路径。此 YAML 文件及其内容创建了一个服务堆栈,运行数据库并在没有停机时间的情况下持续备份:

version: "3"
services:
  database:
    image: my-db-service
    volumes:
      - data:/database
  backup:
    image: my-backup-service
    volumes:
      - data:/backup
volumes:
  data:

注意

Docker Compose 文件的官方参考文档可在docs.docker.com/compose/compose-file/找到。

在以下练习中,将使用 Docker Compose 创建一个具有网络和卷使用的多容器应用程序。

注意

请使用touch命令创建文件,并使用vim命令在文件上使用 vim 编辑器。

练习 5.01:使用 Docker Compose 入门

容器中的 Web 服务器在启动之前需要进行操作任务,例如配置、文件下载或依赖项安装。使用docker-compose,可以将这些操作定义为多容器应用程序,并使用单个命令运行它们。在这个练习中,您将创建一个准备容器来生成静态文件,例如index.html文件。然后,服务器容器将提供静态文件,并且可以通过网络配置从主机机器访问。您还将使用各种docker-compose命令来管理应用程序的生命周期。

要完成练习,请执行以下步骤:

  1. 创建一个名为server-with-compose的文件夹,并使用cd命令进入其中:
mkdir server-with-compose
cd server-with-compose
  1. 创建一个名为init的文件夹,并使用cd命令进入其中:
mkdir init
cd init
  1. 创建一个包含以下内容的 Bash 脚本文件,并将其保存为prepare.sh
#!/usr/bin/env sh
rm /data/index.html
echo "<h1>Welcome from Docker Compose!</h1>" >> /data/index.html
echo "<img src='http://bit.ly/moby-logo' />" >> /data/index.html

该脚本使用echo命令生成一个示例 HTML 页面。

  1. 创建一个名为Dockerfile的文件,并包含以下内容:
FROM busybox
ADD prepare.sh /usr/bin/prepare.sh
RUN chmod +x /usr/bin/prepare.sh
ENTRYPOINT ["sh", "/usr/bin/prepare.sh"] 

Dockerfile基于busybox,这是一个用于节省空间的容器的微型操作系统,并将prepare.sh脚本添加到文件系统中。此外,它使文件可执行,并将其设置为ENTRYPOINT命令。ENTRYPOINT命令,在我们的情况下,prepare.sh脚本在 Docker 容器启动时被初始化。

  1. 使用cd ..命令将目录更改为父文件夹,并创建一个名为docker-compose.yaml的文件,包含以下内容:
version: "3"
services:
  init:
    build:
      context: ./init
    volumes:
      - static:/data

  server:
    image: nginx
    volumes:
      - static:/usr/share/nginx/html  
    ports:
      - "8080:80"
volumes:
  static:

docker-compose文件创建一个名为static的卷,以及两个名为initserver的服务。该卷被挂载到两个容器上。此外,服务器已发布端口8080,连接到容器端口80

  1. 使用以下命令以detach模式启动应用程序,以继续使用终端:
docker-compose up --detach 

以下图片显示了执行上述命令时发生的情况:

图 5.7:启动应用程序

图 5.7:启动应用程序

前面的命令以分离模式创建并启动容器。它首先创建server-with-compose_default网络和server-with-compose_static卷。然后,使用步骤 4中的Dockerfile构建init容器,为服务器下载nginx Docker 镜像,并启动容器。最后,它打印容器的名称并使它们在后台运行。

注意

您可以忽略关于 Swarm 模式的警告,因为我们希望将所有容器部署到同一节点。

  1. 使用docker-compose ps命令检查应用程序的状态:图 5.8:应用程序状态

图 5.8:应用程序状态

此输出列出了两个容器。init容器成功退出,代码为0,而server容器处于运行中状态,其端口可用。这是预期的输出,因为init容器旨在准备index.html文件并完成其操作,而server容器应始终处于运行状态。

  1. 在浏览器中打开http://localhost:8080。以下图显示了输出:图 5.9:服务器输出

图 5.9:服务器输出

图 5.9显示了由init容器创建的index.html页面。换句话说,它显示了docker-compose创建了卷,将其挂载到容器,并成功启动了它们。

  1. 如果不需要应用程序运行,使用以下命令停止并移除所有资源:
docker-compose down

该命令将返回以下输出:

图 5.10:停止应用程序

图 5.10:停止应用程序

在这个练习中,使用docker-compose创建和配置了一个多容器应用程序。网络和卷选项存储在docker-compose.yaml文件中。此外,还展示了用于创建应用程序、检查状态和移除应用程序的 CLI 命令。

在接下来的部分中,将介绍 Docker Compose 环境中应用程序的配置选项。

服务配置

云原生应用程序应该将它们的配置存储在环境变量中。环境变量易于在不更改源代码的情况下在不同平台之间更改。环境变量是存储在基于 Linux 的系统中并被应用程序使用的动态值。换句话说,这些变量可以通过更改它们的值来配置应用程序。

例如,假设您的应用程序使用LOG_LEVEL环境变量来配置日志记录内容。如果您将LOG_LEVEL环境变量从INFO更改为DEBUG并重新启动应用程序,您将看到更多日志并能更轻松地解决问题。此外,您可以使用不同的环境变量集部署相同的应用程序到暂存、测试和生产环境。同样,在 Docker Compose 中配置服务的方法是为容器设置环境变量。

在 Docker Compose 中定义环境变量有三种方法,优先级如下:

  1. 使用 Compose 文件

  2. 使用 shell 环境变量

  3. 使用环境文件

如果环境变量不经常更改但容器需要使用它们,最好将它们存储在docker-compose.yaml文件中。如果有敏感的环境变量,比如密码,建议在调用docker-compose CLI 之前通过 shell 环境变量传递它们。但是,如果变量的数量很大并且在测试、暂存或生产系统之间变化很大,最好将它们收集在.env文件中,并将它们传递到docker-compose.yaml文件中。

docker-compose.yaml文件的services部分,可以为每个服务定义环境变量。例如,以下是在 Docker Compose 文件中为server服务设置的LOG_LEVELMETRICS_PORT环境变量:

server:
  environment:
    - LOG_LEVEL=DEBUG
    - METRICS_PORT=8444

当在docker-compose.yaml文件中未为环境变量设置值时,可以通过运行docker-compose命令从 shell 中获取值。例如,server服务的HOSTNAME环境变量将直接从 shell 中设置:

server:
  environment:
    - HOSTNAME

当运行docker-compose命令的 shell 没有HOSTNAME环境变量的值时,容器将以空环境变量启动。

此外,可以将环境变量存储在.env文件中,并在docker-compose.yaml文件中进行配置。一个名为database.env的示例文件可以按键值列表的方式进行结构化,如下所示:

DATABASE_ADDRESS=mysql://mysql:3535
DATABASE_NAME=db

docker-compose.yaml文件中,环境变量文件字段配置在相应的服务下,如下所示:

server:
  env_file:
    - database.env

当 Docker Compose 创建server服务时,它将把database.env文件中列出的所有环境变量设置到容器中。

在接下来的练习中,您将使用 Docker Compose 中的所有三种配置方法来配置一个应用程序。

练习 5.02:使用 Docker Compose 配置服务

Docker Compose 中的服务是通过环境变量进行配置的。在这个练习中,您将创建一个由不同设置变量方法配置的 Docker Compose 应用程序。在一个名为print.env的文件中,您将定义两个环境变量。此外,您将在docker-compose.yaml文件中创建和配置一个环境变量,并在终端上即时传递一个环境变量。您将看到来自不同来源的四个环境变量如何汇聚在您的容器中。

要完成练习,请执行以下步骤:

  1. 创建一个名为server-with-configuration的文件夹,并使用cd命令进入其中:
mkdir server-with-configuration
cd server-with-configuration
  1. 创建一个名为print.env.env文件,并包含以下内容:
ENV_FROM_ENV_FILE_1=HELLO
ENV_FROM_ENV_FILE_2=WORLD

在这个文件中,使用它们的值定义了两个环境变量ENV_FROM_ENV_FILE_1ENV_FROM_ENV_FILE_2

  1. 创建一个名为docker-compose.yaml的文件,并包含以下内容:
version: "3"
services:
  print:
    image: busybox
    command: sh -c 'sleep 5 && env'
    env_file:
    - print.env
    environment:
    - ENV_FROM_COMPOSE_FILE=HELLO
    - ENV_FROM_SHELL

在这个文件中,定义了一个单容器应用程序,容器运行env命令来打印环境变量。它还使用名为print.env的环境文件,以及两个额外的环境变量ENV_FROM_COMPOSE_FILEENV_FROM_SHELL

  1. 使用以下命令将ENV_FROM_SHELL导出到 shell 中:
export ENV_FROM_SHELL=WORLD
  1. 使用docker-compose up命令启动应用程序。输出应该如下所示:图 5.11:启动应用程序

图 5.11:启动应用程序

输出是在docker-compose文件中定义的print容器的结果。该容器有一个要运行的命令env,它会打印出可用的环境变量。如预期的那样,有两个环境变量ENV_FROM_ENV_FILE_1ENV_FROM_ENV_FILE_2,对应的值分别为HELLOWORLD。此外,在步骤 3中在docker-compose.yaml文件中定义的环境变量以ENV_FROM_COMPOSE_FILE的名称和值HELLO可用。最后,在步骤 4中导出的环境变量以ENV_FROM_SHELL的名称和值WORLD可用。

在这个练习中,创建了一个 Docker Compose 应用程序,并使用不同的方法进行配置。使用 Docker Compose 文件、环境定义文件和导出的值可以将相同的应用程序部署到不同的平台上。

由于 Docker Compose 管理多容器应用程序,因此需要定义它们之间的相互依赖关系。Docker Compose 应用程序中容器的相互依赖关系将在以下部分中介绍。

服务依赖

Docker Compose 运行和管理在docker-compose.yaml文件中定义的多容器应用程序。尽管容器被设计为独立的微服务,但创建相互依赖的服务是非常常见的。例如,假设您有一个包含数据库和后端组件的两层应用程序,比如一个 PostgreSQL 数据库和一个 Java 后端。Java 后端组件需要 PostgreSQL 处于运行状态,因为它需要连接到数据库来运行业务逻辑。因此,您可能需要定义多容器应用程序的服务之间的依赖关系。通过 Docker Compose,可以控制服务的启动和关闭的顺序。

假设您有一个包含三个容器的应用程序,其docker-compose.yaml文件如下:

version: "3"
services:
  init:
    image: busybox
  pre:
    image: busybox
    depends_on:
    - "init"
  main:
    image: busybox
    depends_on:
    - "pre"

在这个文件中,main容器依赖于pre容器,而pre容器依赖于init容器。Docker Compose 按照initpremain的顺序启动容器,如图 5.12所示。此外,容器将按相反的顺序停止:mainpre,然后是init

图 5.12:服务启动顺序

图 5.12:服务启动顺序

在接下来的练习中,容器的顺序将用于填充文件的内容,然后使用 Web 服务器提供它。

练习 5.03:使用 Docker Compose 进行服务依赖

在 Docker Compose 中,服务可以配置为依赖于其他服务。在这个练习中,您将创建一个包含四个容器的应用程序。前三个容器将依次运行,以创建一个静态文件,由第四个容器提供服务。

要完成这个练习,执行以下步骤:

  1. 创建一个名为server-with-dependency的文件夹,并使用cd命令进入其中:
mkdir server-with-dependency
cd server-with-dependency
  1. 创建一个名为docker-compose.yaml的文件,并包含以下内容:
version: "3"
services:
  clean:
    image: busybox
    command: "rm -rf /static/index.html"
    volumes:
      - static:/static 
  init:
    image: busybox
    command: "sh -c 'echo This is from init container >>       /static/index.html'"
    volumes:
      - static:/static 
    depends_on:
    - "clean"
  pre:
    image: busybox
    command: "sh -c 'echo This is from pre container >>       /static/index.html'"
    volumes:
      - static:/static 
    depends_on:
    - "init"
  server:
    image: nginx
    volumes:
      - static:/usr/share/nginx/html  
    ports:
      - "8080:80"
    depends_on:
    - "pre"
volumes:
  static:

这个文件包括四个服务和一个卷。卷的名称是static,它被挂载到所有服务上。前三个服务对静态卷采取单独的操作。clean容器删除index.html文件,然后init容器开始填充index.html。随后,pre容器向index.html文件写入额外的一行。最后,server容器提供static文件夹中的内容。

  1. 使用docker-compose up命令启动应用程序。输出应该如下所示:图 5.13:启动应用程序

图 5.13:启动应用程序

输出显示,Docker Compose 按照cleaninit,然后pre的顺序创建容器。

  1. 在浏览器中打开http://localhost:8080图 5.14:服务器输出

图 5.14:服务器输出

服务器的输出显示,cleaninitpre容器按照预期的顺序工作。

  1. 返回到步骤 3中的终端,并使用Ctrl + C优雅地关闭应用程序。您将看到一些 HTTP 请求日志,最后是Stopping server-with-dependency_server_1行:图 5.15:停止应用程序

图 5.15:停止应用程序

在这个练习中,使用 Docker Compose 创建了一个具有相互依赖服务的应用程序。展示了 Docker Compose 如何按照定义的顺序启动和管理容器。这是 Docker Compose 的一个重要特性,您可以使用它来创建复杂的多容器应用程序。

现在,让我们通过实施以下活动来测试我们在本章中迄今为止所学到的知识。在下一个活动中,您将学习如何使用 Docker Compose 安装 WordPress。

活动 5.01:使用 Docker Compose 安装 WordPress

您被指派设计并部署一个博客及其数据库作为 Docker 中的微服务。您将使用WordPress,因为它是最流行的内容管理系统CMS),被超过三分之一的互联网上的所有网站使用。此外,开发和测试团队需要在不同平台上多次安装 WordPress 和数据库,并进行隔离。因此,您需要将其设计为 Docker Compose 应用程序,并使用docker-compose CLI 进行管理。

执行以下步骤以完成此活动:

  1. 首先创建一个用于您的docker-compose.yaml文件的目录。

  2. 使用 MySQL 在docker-compose.yaml文件中创建一个数据库服务和一个卷。确保设置MYSQL_ROOT_PASSWORDMYSQL_DATABASEMYSQL_USERMYSQL_PASSWORD环境变量。

  3. docker-compose.yaml文件中创建一个 WordPress 的服务。确保 WordPress 容器在数据库之后启动。对于 WordPress 的配置,不要忘记根据步骤 2设置WORDPRESS_DB_HOSTWORDPRESS_DB_USERWORDPRESS_DB_PASSWORDWORDPRESS_DB_NAME环境变量。此外,您需要发布其端口以便能够从浏览器访问它。

  4. detached模式启动 Docker Compose 应用程序。成功部署后,您将有两个运行的容器:图 5.16:WordPress 和数据库容器

图 5.16:WordPress 和数据库容器

然后您将能够在浏览器中访问 WordPress 的设置屏幕:

图 5.17:WordPress 设置屏幕

图 5.17:WordPress 设置屏幕

注意

此活动的解决方案可以通过此链接找到。

在下一个活动中,您将通过创建一个包含三个容器的 Docker 应用程序,并使用docker-compose CLI 进行管理,获得安装全景徒步应用的实际经验。

活动 5.02:使用 Docker Compose 安装全景徒步应用

您的任务是使用 Docker Compose 创建 Panoramic Trekking App 的部署。您将利用 Panoramic Trekking App 的三层架构,并创建一个包含数据库、Web 后端和nginx容器的三个容器 Docker 应用程序。因此,您将将其设计为 Docker Compose 应用程序,并使用docker-compose CLI 进行管理。

执行以下步骤完成此活动:

  1. 为您的docker-compose.yaml文件创建一个目录。

  2. 使用 PostgreSQL 为数据库创建一个服务,并在docker-compose.yaml文件中定义一个卷。确保将POSTGRES_PASSWORD环境变量设置为docker。此外,您需要在docker-compose.yaml中创建一个db_data卷,并将其挂载到/var/lib/postgresql/data/以存储数据库文件。

  3. docker-compose.yaml文件中为 Panoramic Trekking App 创建一个服务。确保您使用的是packtworkshops/the-docker-workshop:chapter5-pta-web Docker 镜像,该镜像已经预先构建并准备好从注册表中使用。此外,由于应用程序依赖于数据库,您应该配置容器在数据库之后启动。为了存储静态文件,在docker-compose.yaml中创建一个static_data卷,并将其挂载到/service/static/

最后,为nginx创建一个服务,并确保您正在使用注册表中的packtworkshops/the-docker-workshop:chapter5-pta-nginx Docker 镜像。确保nginx容器在 Panoramic Trekking App 容器之后启动。您还需要将相同的static_data卷挂载到/service/static/位置。不要忘记将nginx端口80发布到8000,以便从浏览器访问。

  1. 以“分离”模式启动 Docker Compose 应用程序。成功部署后,将有三个容器在运行:图 5.18:应用程序、数据库和 nginx 容器

图 5.18:应用程序、数据库和 nginx 容器

  1. 在浏览器中转到 Panoramic Trekking App 的管理部分,地址为http://0.0.0.0:8000/admin图 5.19:管理员设置登录

图 5.19:管理员设置登录

您可以使用用户名admin和密码changeme登录,并添加新的照片和国家:

图 5.20:管理员设置视图

图 5.20:管理员设置视图

  1. 在浏览器中访问全景徒步应用程序的地址http://0.0.0.0:8000/photo_viewer图 5.21:应用程序视图

图 5.21:应用程序视图

注意

此活动的解决方案可通过此链接找到。

摘要

本章重点介绍了使用 Docker Compose 设计、创建和管理多容器应用程序。随着微服务架构的兴起,容器化应用程序的复杂性也增加。因此,如果没有适当的工具,创建、管理和排除多容器应用程序将变得困难。Docker Compose 是 Docker 工具箱中的官方工具,用于此目的。

在本章中,主要重点是全面学习docker-compose。为此,本章从docker-compose CLI 的功能及其命令和标志开始。然后介绍了docker-compose.yaml文件的结构。Docker Compose 的强大之处实际上来自于docker-compose.yaml文件中定义的配置能力。因此,学习如何使用这些文件来管理多容器应用是至关重要的。

接下来,演示了 Docker Compose 中服务的配置。您已经学会了如何为不同环境配置服务并适应未来的变化。然后我们转向了服务依赖关系,以学习如何创建更复杂的容器化应用程序。

本章中的每个练习都旨在展示 Docker 的能力,包括不同的 CLI 命令和 YAML 文件部分。必须亲自体验 CLI 和创建用于测试和生产环境中的多容器应用所需的文件。

在下一章中,您将学习 Docker 中的网络。容器化和可扩展应用程序中的网络是基础设施的关键部分之一,因为它将分布式部分粘合在一起。这就是为什么 Docker 中的网络由可插拔驱动程序和选项组成,以增强容器化应用程序的开发和管理体验。

第六章:Docker 网络简介

概述

本章的目标是为您提供容器网络如何工作的简明概述,以及它与 Docker 主机级别的网络有何不同,以及容器如何利用 Docker 网络提供对其他容器化服务的直接网络连接。通过本章的学习,您将了解如何使用bridgeoverlaymacvlanhost等网络配置部署容器。您将了解不同网络驱动程序的优势,并在何种情况下应选择特定的网络驱动程序。最后,我们将研究在 Docker swarm 集群中部署的主机之间的容器化网络。

介绍

在整个研讨会中,我们已经研究了与 Docker 相关的容器化和微服务架构的许多方面。我们已经了解了如何将应用程序封装为执行离散功能的微服务,从而创建了一种非常灵活的架构,可以实现快速部署和强大的水平扩展。也许与容器化相关的更有趣和复杂的话题之一是网络。毕竟,为了开发灵活和敏捷的微服务架构,需要考虑适当的网络,以确保容器实例之间可靠的连接。

在涉及容器网络时,始终要记住容器主机上的网络(底层网络)与同一主机上或不同集群内的容器之间的网络(覆盖网络)之间的区别。Docker 支持许多不同类型的网络配置,可以根据基础设施和部署策略的需求进行定制。

例如,一个容器可能具有一个 IP 地址,该 IP 地址是该容器实例独有的,在容器主机之间的虚拟子网上存在。这种类型的网络是 Docker swarm 集群配置的典型特征,其中网络流量被加密并通过主机机器的网络接口传输,然后在不同的主机上解密,然后传递给接收微服务。这种类型的网络配置通常涉及 Docker 维护容器和服务名称到容器 IP 地址的映射。这提供了强大的服务发现机制,即使容器在不同的集群主机上终止和重新启动,也可以进行容器网络。

另外,容器也可以以更简单的主机网络模式运行。在这种情况下,运行在集群或独立主机中的容器会在主机机器的网络接口上公开端口,以发送和接收网络流量。容器本身仍然可能有它们的 IP 地址,这些地址通过 Docker 映射到主机上的物理网络接口。当您的微服务需要主要与容器化基础设施之外存在的服务进行通信时,这种类型的网络配置非常有用。

默认情况下,Docker 以桥接网络模式运行。bridge网络在主机上创建一个单一网络接口,充当与主机上配置的另一个子网进行桥接的桥接器。所有传入(入口)和传出(出口)的网络流量都通过bridge网络接口在容器子网和主机之间传输。

在 Linux 环境中安装了 Docker 引擎后,如果运行ifconfig命令,Docker 将创建一个名为docker0的新虚拟桥接网络接口。该接口将默认创建的 Docker 私有子网(通常为172.16.0.0/16)与主机的网络堆栈进行桥接。如果一个容器在默认的 Docker 网络中以 IP 地址172.17.8.1运行,并且您尝试联系该 IP 地址,内部路由表将通过docker0桥接接口将流量传递到私有子网上容器的 IP 地址。除非通过 Docker 发布端口,否则无法从外部访问该容器的 IP 地址。在本章中,我们将深入探讨 Docker 提供的各种网络驱动程序和配置选项。

在下一个练习中,我们将看看如何在默认的 Docker bridge网络中创建 Docker 容器,以及如何将容器端口暴露给外部世界。

练习 6.01:Docker 网络实践

默认情况下,在 Docker 中运行容器时,您创建的容器实例将存在于一个 Docker 网络中。Docker 网络是 Docker 用来为在即时 Docker 服务器或 Docker 集群中的服务器上运行的容器分配网络资源的子网集合、规则和元数据。该网络将为容器提供访问同一子网中的其他容器,甚至提供对其他外部网络(包括互联网)的出站(egress)访问。每个 Docker 网络都与一个网络驱动程序相关联,该驱动程序确定了网络在容器所在系统的上下文中的功能。

在这个练习中,您将运行 Docker 容器,并使用基本网络来运行两个简单的 Web 服务器(Apache2 和 NGINX),它们将在几种不同的基本网络场景中暴露端口。然后,您将访问容器的暴露端口,以更多地了解 Docker 网络是如何在最基本的级别上工作的。启动容器并暴露服务端口以使它们可用是最常见的网络场景之一,当您首次开始使用容器化基础设施时:

  1. 列出当前在您的 Docker 环境中配置的网络,使用docker network ls命令:
$ docker network ls

显示的输出将显示系统中所有配置的 Docker 网络。它应该类似于以下内容:

NETWORK ID      NAME      DRIVER     SCOPE
0774bdf6228d    bridge    bridge     local
f52b4a5440ad    host      host       local
9bed60b88784    none      null       local
  1. 在 Docker 中创建容器时,如果没有指定网络或网络驱动程序,Docker 将使用bridge网络创建容器。这个网络存在于您的主机操作系统中配置的bridge网络接口后面。在 Linux 或 macOS 的 Bash shell 中使用ifconfig,或在 Windows PowerShell 中使用ipconfig,来查看 Docker 桥接口配置为哪个接口。通常被称为docker0
$ ifconfig 

此命令的输出将列出环境中所有可用的网络接口,如下图所示:

图 6.1:列出可用的网络接口

图 6.1:列出可用的网络接口

可以观察到在前面的图中,Docker 的bridge接口被称为docker0,并且具有 IP 地址172.17.0.1

  1. 使用docker run命令创建一个简单的 NGINX Web 服务器容器,使用latest镜像标签。使用-d标志将容器设置为在后台启动,并使用--name标志为其指定一个可读的名称webserver1
$ docker run -d –-name webserver1 nginx:latest 

如果命令成功,终端会没有返回任何输出。

  1. 执行docker ps命令来检查容器是否正在运行:
$ docker ps

如您所见,webserver1容器正在如预期地运行:

CONTAINER ID  IMAGE         COMMAND                 CREATED
  STATUS                   PORTS               NAMES
0774bdf6228d  nginx:latest  "nginx -g 'daemon of…"  4 seconds ago
  Up 3 seconds             80/tcp              webserver1
  1. 执行docker inspect命令来检查这个容器默认的网络配置:
$ docker inspect webserver1

Docker 将以 JSON 格式返回有关正在运行的容器的详细信息。在这个练习中,重点关注NetworkSettings块。特别注意networks子块下面的GatewayIPAddressPortsNetworkID参数:

图 6.2:docker inspect 命令的输出

图 6.2:docker inspect 命令的输出

从这个输出可以得出结论,这个容器存在于默认的 Dockerbridge网络中。观察NetworkID的前 12 个字符,您会发现它与docker network ls命令的输出中使用的标识符相同,该命令是在步骤 1中执行的。还应该注意,这个容器配置使用的Gatewaydocker0``bridge接口的 IP 地址。Docker 将使用这个接口作为访问自身以外子网中的网络的出口点,同时将来自我们环境的流量转发到子网中的容器。还可以观察到,这个容器在 Docker 桥接网络中有一个唯一的 IP 地址,在本例中为172.17.0.2。由于我们有docker0``bridge接口可用来转发流量,我们的本地机器可以路由到这个子网。最后,可以观察到,默认情况下,NGINX 容器正在暴露 TCP 端口80用于传入流量。

  1. 在 Web 浏览器中,通过 IP 地址和端口80访问webserver1容器。在您喜欢的 Web 浏览器中输入webserver1容器的 IP 地址:图 6.3:通过 IP 地址访问 NGINX Web 服务器容器默认的 Docker 桥接网络

图 6.3:通过默认的 Docker 桥接网络通过 IP 地址访问 NGINX Web 服务器容器

  1. 或者,使用curl命令查看类似的输出,尽管是以文本格式:
$ curl 172.17.0.2:80

以下 HTML 响应表示您已从正在运行的 NGINX 容器收到响应:

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully 
installed and working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
  1. 访问本地bridge子网中容器的 IP 地址对于测试本地容器非常有效。要将您的服务暴露给其他用户或服务器的网络,请在docker run命令中使用-p标志。这将允许您将主机上的端口映射到容器上的公开端口。这类似于路由器或其他网络设备上的端口转发。要通过端口向外部世界暴露容器,请使用docker run命令,后跟-d标志以在后台启动容器。-p标志将使您能够指定主机上的端口,用冒号分隔,并指定要公开的容器上的端口。还要为此容器指定一个唯一的名称,webserver2
$ docker run -d -p 8080:80 –-name webserver2 nginx:latest

成功启动容器后,您的 shell 将不会返回任何内容。但是,某些版本的 Docker 可能会显示完整的容器 ID。

  1. 运行docker ps命令,检查是否有两个正在运行的 NGINX 容器:
$ docker ps

将显示两个正在运行的容器,webserver1webserver2

CONTAINER ID IMAGE         COMMAND                 CREATED
  STATUS              PORTS                  NAMES
b945fa75b59a nginx:latest  "nginx -g 'daemon of…"  1 minute ago
  Up About a minute   0.0.0.0:8080->80/tcp   webserver2
3267bf4322ed nginx:latest  "nginx -g 'daemon of…"  2 minutes ago
  Up 2 minutes        80/tcp                 webserver1

PORTS列中,您将看到 Docker 现在正在将webserver容器上的端口80转发到主机上的端口8080。这是从输出的0.0.0.0:8080->80/tcp部分推断出来的。

注意

重要的是要记住,使用-p标志指定端口时,主机机器端口始终位于冒号的左侧,而容器端口位于右侧。

  1. 在您的网络浏览器中,导航至http://localhost:8080,以查看您刚刚生成的运行容器实例:图 6.4:NGINX 默认页面,指示您已成功转发将端口映射到您的 Web 服务器容器

图 6.4:NGINX 默认页面,指示您已成功将端口转发到您的 Web 服务器容器

  1. 现在,在相同的 Docker 环境中运行两个 NGINX 实例,具有略有不同的网络配置。webserver1实例仅在 Docker 网络上运行,没有任何端口暴露。使用docker inspect命令检查webserver2实例的配置,后跟容器名称或 ID:
$ docker inspect webserver2

JSON 输出底部的NetworkSettings部分将类似于以下内容。请特别注意networks子块下面的参数(GatewayIPAddressPortsNetworkID):

图 6.5:docker inspect 命令的输出

图 6.5:docker inspect 命令的输出

正如docker inspect输出显示的那样,webserver2容器的 IP 地址为172.17.0.3,而您的webserver1容器的 IP 地址为172.17.0.1。根据 Docker 分配 IP 地址给容器的方式,您本地环境中的 IP 地址可能略有不同。这两个容器都位于同一个 Docker 网络(bridge)上,并且具有相同的默认网关,即主机上的docker0 bridge接口。

  1. 由于这两个容器都位于同一个子网上,您可以在 Dockerbridge网络内测试容器之间的通信。运行docker exec命令以访问webserver1容器上的 shell:
docker exec -it webserver1 /bin/bash

提示符应明显更改为根提示符,表示您现在在webserver1容器的 Bash shell 中:

root@3267bf4322ed:/#
  1. 在根 shell 提示符下,使用apt软件包管理器在此容器中安装ping实用程序:
root@3267bf4322ed:/# apt-get update && apt-get install -y inetutils-ping

然后,aptitude 软件包管理器将在webserver1容器中安装ping实用程序。请注意,apt软件包管理器将安装ping以及运行ping命令所需的其他依赖项:

图 6.6:在 Docker 容器内安装 ping 命令

图 6.6:在 Docker 容器内安装 ping 命令

  1. 安装ping实用程序后,使用它来 ping 另一个容器的 IP 地址:
root@3267bf4322ed:/# ping 172.17.0.3

输出应显示 ICMP 响应数据包,表明容器可以通过 Dockerbridge网络成功 ping 通彼此:

PING 172.17.0.1 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: icmp_seq=0 ttl=64 time=0.221 ms
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.207 ms
  1. 您还可以使用curl命令访问 NGINX 默认的 Web 界面。使用apt软件包管理器安装curl
root@3267bf4322ed:/# apt-get install -y curl

接下来的输出应显示,正在安装curl实用程序和所有必需的依赖项:

图 6.7:安装 curl 实用程序

图 6.7:安装 curl 实用程序

  1. 安装curl后,使用它来 curlwebserver2的 IP 地址:
root@3267bf4322ed:/# curl 172.17.0.3

您应该看到以 HTML 格式显示的“欢迎使用 nginx!”页面,这表明您能够通过 Dockerbridge网络成功联系到webserver2容器的 IP 地址:

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully 
installed and working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>

由于您正在使用curl导航到 NGINX 欢迎页面,它将以原始 HTML 格式呈现在您的终端显示器上。

在这一部分,我们已经成功在同一个 Docker 环境中生成了两个 NGINX Web 服务器实例。我们配置了一个实例,不在默认 Docker 网络之外暴露任何端口,而我们配置了第二个 NGINX 实例在同一网络上运行,但将端口80暴露给主机系统的端口8080。我们看到这些容器可以使用标准的互联网浏览器以及 Linux 中的curl实用程序进行访问。

在这个练习中,我们还看到了容器如何使用 Docker 网络直接与其他容器通信。我们使用webserver1容器调用webserver2容器的 IP 地址,并显示容器托管的网页的输出。

在这个练习中,我们还能够演示容器实例之间使用本机 Dockerbridge网络进行网络连接。然而,当我们大规模部署容器时,很难知道 Docker 网络中的哪个 IP 地址属于哪个容器。

在接下来的部分,我们将看看本机 Docker DNS,并学习如何使用可靠的人类可读的 DNS 名称将网络流量可靠地发送到其他容器实例。

本机 Docker DNS

运行容器化基础架构的最大好处之一是能够快速轻松地横向扩展工作负载。在一个具有共享overlay网络的集群中有多台机器意味着你可以在多台服务器上运行许多容器。

正如我们在前面的练习中看到的,Docker 赋予我们的权力允许容器通过 Docker 提供的各种网络驱动程序(如bridgemacvlanoverlay驱动程序)直接与集群中的其他容器通信。在前面的例子中,我们利用 Dockerbridge网络使容器能够通过各自的 IP 地址相互通信。然而,当您的容器部署在真实服务器上时,通常不能依赖容器具有一致的 IP 地址来相互通信。每当一个新的容器实例终止或重新生成时,Docker 都会给该容器一个新的 IP 地址。

类似于传统基础设施场景,我们可以利用容器网络内的 DNS 来为容器提供可靠的通信方式。通过为 Docker 网络中的容器分配可读的名称,用户不再需要每次想要在 Docker 网络上的容器之间发起通信时查找 IP 地址。Docker 本身将跟踪容器的 IP 地址,因为它们生成和重新生成。

在旧版本的 Docker 中,可以通过在docker run命令中使用--link标志在容器之间建立链接来实现简单的 DNS 解析。使用链接,Docker 会在链接的容器的hosts文件中创建一个条目,从而实现简单的名称解析。然而,正如你将在即将进行的练习中看到的,使用容器之间的链接可能会很慢,不可扩展,并且容易出错。最近的 Docker 版本支持在同一 Docker 网络上运行的容器之间的原生 DNS 服务。这允许容器查找在同一 Docker 网络中运行的其他容器的名称。这种方法的唯一注意事项是,原生 Docker DNS 在默认的 Docker bridge网络上不起作用;因此,必须首先创建其他网络来构建您的容器。

为了使原生 Docker DNS 工作,我们必须首先使用docker network create命令创建一个新的网络。然后,我们可以使用docker run命令和--network-alias标志在该网络中创建新的容器。在接下来的练习中,我们将使用这些命令来学习原生 Docker DNS 是如何工作的,以实现容器实例之间的可扩展通信。

练习 6.02:使用 Docker DNS

在接下来的练习中,您将学习在运行在同一网络上的 Docker 容器之间的名称解析。您将首先使用传统的链接方法启用简单的名称解析。然后,您将通过使用更新的、更可靠的原生 Docker DNS 服务来对比这种方法:

  1. 首先,在默认的 Docker bridge网络上创建两个 Alpine Linux 容器,它们将使用--link标志相互通信。Alpine 是这个练习的一个很好的基础镜像,因为它默认包含ping实用程序。这将使您能够快速测试各种情况下容器之间的连接。要开始,请创建一个名为containerlink1的容器,以指示您是使用传统的链接方法创建了这个容器。
$ docker run -itd --name containerlink1 alpine:latest

这将在名为containerlink1的默认 Docker 网络中启动一个容器。

  1. 在默认的 Docker 桥接网络中启动另一个名为containerlink2的容器,它将创建一个到containerlink1的链接以启用基本的 DNS:
$ docker run -itd --name containerlink2 --link containerlink1 alpine:latest

这将在名为containerlink2的默认 Docker 网络中启动一个容器。

  1. 运行docker exec命令以访问containerlink2容器内部的 shell。这将允许您调查链接功能的工作方式。由于此容器正在运行 Alpine Linux,默认情况下您无法访问 Bash shell。而是使用sh shell 进行访问:
$ docker exec -it containerlink2 /bin/sh

这应该将您放入containerlink2容器中的 root sh shell 中。

  1. containerlink2容器的 shell 中,ping containerlink1
/ # ping containerlink1

您将收到ping请求的回复:

PING container1 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.307 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.162 ms
64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.177 ms
  1. 使用cat实用程序查看containerlink2容器的/etc/hosts文件。hosts文件是 Docker 可以维护和覆盖的可路由名称到 IP 地址的列表:
/ # cat /etc/hosts

hosts文件的输出应该显示并类似于以下内容:

127.0.0.1  localhost
::1  localhost ip6-localhost ip6-loopback
fe00::0    ip6-localnet
ff00::0    ip6-mcastprefix
ff02::1    ip6-allnodes
ff02::2    ip6-allrouters
172.17.0.2    containerlink1 032f038abfba
172.17.0.3    9b62c4a57ce3

containerlink2容器的hosts文件输出中,观察到 Docker 正在为containerlink1容器名称以及其容器 ID 添加条目。这使得containerlink2容器可以知道名称,并且容器 ID 映射到 IP 地址172.17.0.2。输入exit命令将终止sh shell 会话,并将您带回到您环境的主终端。

  1. 运行docker exec命令以访问containerlink1容器内部的sh shell:
$ docker exec -it containerlink1 /bin/sh

这应该将您放入containerlink1容器的 shell 中。

  1. 使用ping实用程序对containerlink2容器进行 ping 测试:
/ # ping containerlink2

您应该看到以下输出:

ping: bad address 'containerlink2'

由于容器之间的链接只能单向工作,所以无法对containerlink2容器进行 ping 测试。containerlink1容器不知道containerlink2容器的存在,因为在containerlink1容器实例中没有创建hosts文件条目。

注意

您只能使用容器之间的传统链接方法链接到运行中的容器。这意味着第一个容器不能链接到稍后启动的容器。这是使用容器之间的链接不再是推荐方法的许多原因之一。我们在本章中介绍这个概念,以向您展示功能是如何工作的。

  1. 由于使用传统链接方法存在限制,Docker 还支持使用用户创建的 Docker 网络来支持本机 DNS。为了利用这个功能,创建一个名为dnsnet的 Docker 网络,并在该网络中部署两个 Alpine 容器。首先,使用docker network create命令创建一个新的 Docker 网络,使用192.168.56.0/24子网和 IP 地址192.168.54.1作为默认网关:
$ docker network create dnsnet --subnet 192.168.54.0/24 --gateway 192.168.54.1

根据您使用的 Docker 版本,成功执行此命令可能会返回您创建的网络的 ID。

注意

仅使用docker network create dnsnet命令将创建一个具有 Docker 分配的子网和网关的网络。此练习演示了如何为 Docker 网络指定子网和网关。还应注意,如果您的计算机连接到192.168.54.0/24子网或与该空间重叠的子网,可能会导致网络连接问题。请为此练习使用不同的子网。

  1. 使用docker network ls命令列出此环境中可用的 Docker 网络:
$ docker network ls

应返回 Docker 网络列表,包括您刚刚创建的dnsnet网络:

NETWORK ID      NAME       DRIVER     SCOPE
ec5b91e88a6f    bridge     bridge     local
c804e768413d    dnsnet     bridge     local
f52b4a5440ad    host       host       local
9bed60b88784    none       null       local
  1. 运行docker network inspect命令查看此网络的配置:
$ docker network inspect dnsnet

应显示dnsnet网络的详细信息。特别注意子网网关参数。这些是您在步骤 8中用来创建 Docker 网络的相同参数:

图 6.8:来自 docker network inspect 命令的输出

图 6.8:来自 docker network inspect 命令的输出

  1. 由于这是一个 Docker“桥接”网络,Docker 还将为此网络创建一个相应的桥接网络接口。桥接网络接口的 IP 地址将与您在创建此网络时指定的默认网关地址相同。使用ifconfig命令在 Linux 或 macOS 上查看配置的网络接口。如果您使用 Windows,请使用ipconfig命令:
$ ifconfig

这应该显示所有可用网络接口的输出,包括新创建的bridge接口:

图 6.9:分析新创建的 Docker 网络的桥接网络接口

图 6.9:分析新创建的 Docker 网络的桥接网络接口

  1. 现在已经创建了一个新的 Docker 网络,使用docker run命令在此网络中启动一个新的容器(alpinedns1)。使用docker run命令,使用--network标志指定刚刚创建的dnsnet网络,并使用--network-alias标志为容器指定自定义 DNS 名称:
$ docker run -itd --network dnsnet --network-alias alpinedns1 --name alpinedns1 alpine:latest

成功执行命令后,应显示完整的容器 ID,然后返回到正常的终端提示符。

  1. 使用相同的--network--network-alias设置启动第二个容器(alpinedns2):
$ docker run -itd --network dnsnet --network-alias alpinedns2 --name alpinedns2 alpine:latest

注意

重要的是要理解--network-alias标志和--name标志之间的区别。--name标志用于在 Docker API 中为容器指定一个易于阅读的名称。这使得通过名称轻松启动、停止、重新启动和管理容器。然而,--network-alias标志用于为容器创建自定义 DNS 条目。

  1. 使用docker ps命令验证容器是否按预期运行:
$ docker ps 

输出将显示正在运行的容器实例:

CONTAINER ID    IMAGE           COMMAND      CREATED 
  STATUS              PORTS             NAMES
69ecb9ad45e1    alpine:latest   "/bin/sh"    4 seconds ago
  Up 2 seconds                          alpinedns2
9b57038fb9c8    alpine:latest   "/bin/sh"    6 minutes ago
  Up 6 minutes                          alpinedns1
  1. 使用docker inspect命令验证容器实例的 IP 地址是否来自指定的子网(192.168.54.0/24):
$ docker inspect alpinedns1

以下输出被截断以显示相关细节:

图:6.10:alpinedns1 容器实例的网络部分输出

图:6.10:alpinedns1 容器实例的网络部分输出

可以从输出中观察到,alpinedns1容器部署时具有 IP 地址192.168.54.2,这是在创建 Docker 网络时定义的子网的一部分。

  1. 以类似的方式执行docker network inspect命令,针对alpinedns2容器:
$ docker inspect alpinedns2

输出再次被截断以显示相关的网络细节:

图 6.11:alpinedns2 容器实例的网络部分输出

图 6.11:alpinedns2 容器实例的网络部分输出

可以观察到在前面的输出中,alpinedns2容器具有 IP 地址192.168.54.3,这是dnsnet子网内的不同 IP 地址。

  1. 运行docker exec命令以访问alpinedns1容器中的 shell:
$ docker exec -it alpinedns1 /bin/sh

这应该将您放入容器内的 root shell 中。

  1. 进入alpinedns1容器后,使用ping实用程序对alpinedns2容器进行 ping 测试:
/ # ping alpinedns2

ping输出应显示与alpinedns2容器实例的成功网络连接:

PING alpinedns2 (192.168.54.3): 56 data bytes
64 bytes from 192.168.54.3: seq=0 ttl=64 time=0.278 ms
64 bytes from 192.168.54.3: seq=1 ttl=64 time=0.233 ms
  1. 使用exit命令返回到主要终端。使用docker exec命令访问alpinedns2容器内的 shell:
$ docker exec -it alpinedns2 /bin/sh

这将使您进入alpinedns2容器内的 shell。

  1. 使用ping实用程序通过名称 pingalpinedns1容器:
$ ping alpinedns1

输出应显示来自alpinedns1容器的成功响应:

PING alpinedns1 (192.168.54.2): 56 data bytes
64 bytes from 192.168.54.2: seq=0 ttl=64 time=0.115 ms
64 bytes from 192.168.54.2: seq=1 ttl=64 time=0.231 ms

注意

与传统链接方法相比,Docker DNS 允许在同一 Docker 网络中的容器之间进行双向通信。

  1. 在任何alpinedns容器内使用cat实用程序来揭示 Docker 正在使用真正的 DNS,而不是容器内的/etc/hosts文件条目:
# cat /etc/hosts

这将显示各自容器内/etc/hosts文件的内容:

127.0.0.1  localhost
::1  localhost ip6-localhost ip6-loopback
fe00::0    ip6-localnet
ff00::0    ip6-mcastprefix
ff02::1    ip6-allnodes
ff02::2    ip6-allrouters
192.168.54.2    9b57038fb9c8

使用exit命令终止alpinedns2容器内的 shell 会话。

  1. 使用docker stop命令停止所有正在运行的容器来清理您的环境:
$ docker stop  containerlink1
$ docker stop  containerlink2
$ docker stop  alpinedns1
$ docker stop  alpinedns2
  1. 使用docker system prune -fa命令清理剩余的已停止容器和网络:
$ docker system prune -fa

成功执行此命令应清理dnsnet网络以及容器实例和镜像:

Deleted Containers:
69ecb9ad45e16ef158539761edc95fc83b54bd2c0d2ef55abfba1a300f141c7c
9b57038fb9c8cf30aaebe6485e9d223041a9db4e94eb1be9392132bdef632067
Deleted Networks:
dnsnet
Deleted Images:
untagged: alpine:latest
untagged: alpine@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c
    59c978c1ed79109ea4fb9a54
deleted: sha256:f70734b6a266dcb5f44c383274821207885b549b75c8e119
    404917a61335981a
deleted: sha256:3e207b409db364b595ba862cdc12be96dcdad8e36c59a03b
    b3b61c946a5741a
Total reclaimed space: 42.12M

系统清理输出的每个部分都将识别并删除不再使用的 Docker 资源。在这种情况下,它将删除dnsnet网络,因为当前在该网络中没有部署容器实例。

在这个练习中,您看到了使用名称解析在 Docker 网络上启用容器之间通信的好处。使用名称解析是高效的,因为应用程序不必担心其他正在运行的容器的 IP 地址。相反,通信可以通过简单地按名称调用其他容器来启动。

我们首先探讨了名称解析的传统链接方法,通过该方法,运行的容器可以建立关系,利用容器的hosts文件中的条目进行单向关系。使用 DNS 在容器之间进行通信的第二种更现代的方法是创建用户定义的 Docker 网络,允许双向 DNS 解析。这将使网络上的所有容器都能够通过名称或容器 ID 解析所有其他容器,而无需任何额外的配置。

正如我们在本节中所见,Docker 提供了许多独特的方法来为容器实例提供可靠的网络资源,例如在相同的 Docker 网络上启用容器之间的路由和容器之间的本机 DNS 服务。这只是 Docker 提供的网络选项的冰山一角。

在下一节中,我们将学习如何使用其他类型的网络驱动程序部署容器,以便在部署容器化基础架构时提供最大的灵活性。

本机 Docker 网络驱动程序

由于 Docker 是近年来最广泛支持的容器平台之一,Docker 平台已在许多生产级网络场景中得到验证。为了支持各种类型的应用程序,Docker 提供了各种网络驱动程序,可以灵活地创建和部署容器。这些网络驱动程序允许容器化应用程序在几乎任何直接在裸机或虚拟化服务器上支持的网络配置中运行。

例如,可以部署共享主机服务器网络堆栈的容器,或者以允许它们从底层网络基础设施分配唯一 IP 地址的配置。在本节中,我们将学习基本的 Docker 网络驱动程序以及如何利用它们为各种类型的网络基础设施提供最大的兼容性:

  • bridgebridge是 Docker 将容器运行在其中的默认网络。如果在启动容器实例时没有定义任何内容,Docker 将使用docker0接口后面的子网,其中容器将被分配在172.17.0.0/16子网中的 IP 地址。在bridge网络中,容器可以与bridge子网中的其他容器进行网络连接,也可以与互联网进行出站连接。到目前为止,在本章中创建的所有容器都在bridge网络中。Docker 的bridge网络通常用于仅公开简单端口或需要与同一主机上存在的其他容器进行通信的简单 TCP 服务。

  • host:以host网络模式运行的容器直接访问主机机器的网络堆栈。这意味着容器暴露的任何端口也会暴露给运行容器的主机机器上的相同端口。容器还可以看到主机上运行的所有物理和虚拟网络接口。通常在运行消耗大量带宽或利用多个协议的容器实例时,会首选host网络。

  • nonenone网络不提供网络连接给部署在该网络中的容器。在none网络中部署的容器实例只有一个环回接口,根本无法访问其他网络资源。没有驱动程序操作这个网络。使用none网络模式部署的容器通常是在存储或磁盘工作负载上运行的应用程序,不需要网络连接。出于安全目的而被隔离在网络连接之外的容器也可以使用这个网络驱动程序进行部署。

  • macvlan:在 Docker 中创建的macvlan网络用于容器化应用程序需要 MAC 地址和直接网络连接到底层网络的情况。使用macvlan网络,Docker 将通过主机机器上的物理接口为容器实例分配一个 MAC 地址。这使得您的容器在部署的网络段上看起来像是一个物理主机。需要注意的是,许多云环境,如 AWS、Azure 和许多虚拟化 hypervisor,不允许在容器实例上配置macvlan网络。macvlan网络允许 Docker 根据连接到主机机器的物理网络接口,为容器分配 IP 地址和 MAC 地址。如果未正确配置,使用macvlan网络可能会很容易导致 IP 地址耗尽或 IP 地址冲突。macvlan容器网络通常用于非常特定的网络用例,例如监视网络流量模式或其他网络密集型工作负载的应用程序。

关于 Docker 网络的讨论如果没有对Docker 叠加网络进行简要概述,就不完整。叠加网络是 Docker 处理与集群的网络的方式。当在节点之间定义 Docker 集群时,Docker 将使用将节点连接在一起的物理网络来定义在节点上运行的容器之间的逻辑网络。这允许容器在集群节点之间直接通信。在练习 6.03,探索 Docker 网络中,我们将看看 Docker 默认支持的各种类型的 Docker 网络驱动程序,如hostnonemacvlan。在练习 6.04中,定义叠加网络,我们将定义一个简单的 Docker 集群,以发现在集群模式下配置的 Docker 主机之间的叠加网络是如何工作的。

练习 6.03:探索 Docker 网络

在这个练习中,我们将研究 Docker 默认支持的各种类型的网络驱动程序,如hostnonemacvlan。我们将从bridge网络开始,然后再看看nonehostmacvlan网络:

  1. 首先,您需要了解在您的 Docker 环境中如何设置网络。从 Bash 或 PowerShell 终端,在 Windows 上使用ifconfigipconfig命令。这将显示您的 Docker 环境中的所有网络接口:
$ ifconfig

这将显示您可用的所有网络接口。您应该看到一个名为docker0bridge接口。这是 Docker 的bridge接口,用作默认 Docker 网络的入口(或入口点):

图 6.12:来自您的 Docker 开发环境的示例 ifconfig 输出

图 6.12:来自您的 Docker 开发环境的示例 ifconfig 输出

  1. 使用docker network ls命令查看您的 Docker 环境中可用的网络:
$ docker network ls

这应该列出之前定义的三种基本网络类型,显示网络 ID、Docker 网络的名称和与网络类型相关联的驱动程序:

NETWORK ID       NAME      DRIVER     SCOPE
50de4997649a     bridge    bridge     local
f52b4a5440ad     host      host       local
9bed60b88784     none      null       local
  1. 使用docker network inspect命令查看这些网络的详细信息,然后跟上要检查的网络的 ID 或名称。在这一步中,您将查看bridge网络的详细信息:
$ docker network inspect bridge

Docker 将以 JSON 格式显示bridge网络的详细输出:

图 6.13:检查默认的桥接网络

图 6.13:检查默认的桥接网络

在这个输出中需要注意的一些关键参数是ScopeSubnetGateway关键字。根据这个输出,可以观察到这个网络的范围只是本地主机(Scope: Local)。这表明该网络不在 Docker 集群中的主机之间共享。在Config部分下,这个网络的Subnet值是172.17.0.0/16,子网的Gateway地址是定义的子网内的 IP 地址(172.17.0.1)。子网的Gateway值是子网内的 IP 地址,以便在该子网中部署的容器可以访问该网络范围之外的其他网络。最后,这个网络与主机接口docker0绑定,它将作为网络的bridge接口。docker network inspect命令的输出对于全面了解在该网络中部署的容器预期行为非常有帮助。

  1. 使用docker network inspect命令查看host网络的详细信息:
$ docker network inspect host

这将以 JSON 格式显示host网络的详细信息:

图 6.14:主机网络的 docker 网络检查输出

图 6.14:主机网络的 docker 网络检查输出

正如你所看到的,host网络中没有太多的配置。由于它使用host网络驱动程序,所有容器的网络将与主机共享。因此,这个网络配置不需要定义特定的子网、接口或其他元数据,就像我们之前在默认的bridge网络中看到的那样。

  1. 接下来调查none网络。使用docker network inspect命令查看none网络的详细信息:
docker network inspect none

详细信息将以 JSON 格式显示:

图 6.15:none 网络的 docker 网络检查输出

图 6.15:none 网络的 docker 网络检查输出

host网络类似,none网络大部分是空的。由于在这个网络中部署的容器将通过null驱动程序没有网络连接,因此不需要太多的配置。

注意

请注意,nonehost 网络之间的区别在于它们使用的驱动程序,尽管配置几乎相同。在 none 网络中启动的容器根本没有网络连接,并且没有网络接口分配给容器实例。然而,在 host 网络中启动的容器将与主机系统共享网络堆栈。

  1. 现在在 none 网络中创建一个容器以观察其操作。在您的终端或 PowerShell 会话中,使用 docker run 命令使用 --network 标志在 none 网络中启动一个 Alpine Linux 容器。将此容器命名为 nonenet,以便我们知道它部署在 none 网络中:
$ docker run -itd --network none --name nonenet alpine:latest 

这将在 none 网络中拉取并启动一个 Alpine Linux Docker 容器。

  1. 使用 docker ps 命令验证容器是否按预期运行:
$ docker ps 

输出应显示 nonenet 容器已启动并运行:

CONTAINER ID    IMAGE            COMMAND      CREATED 
  STATUS              PORTS              NAMES
972a80984703    alpine:latest    "/bin/sh"    9 seconds ago
  Up 7 seconds                           nonenet
  1. 执行 docker inspect 命令,以及容器名称 nonenet,以更深入地了解此容器的配置:
$ docker inspect nonenet

docker inspect 的输出将以 JSON 格式显示完整的容器配置。这里提供了一个突出显示 NetworkSettings 部分的缩略版本。请特别注意 IPAddressGateway 设置:

图 6.16:nonenet 容器的 docker inspect 输出

图 6.16:nonenet 容器的 docker inspect 输出

docker inspect 输出将显示该容器没有 IP 地址,也没有网关或任何其他网络设置。

  1. 使用 docker exec 命令访问此容器内部的 sh shell:
$ docker exec -it nonenet /bin/sh

成功执行此命令后,您将进入容器实例中的 root shell:

/ #
  1. 执行 ip a 命令查看容器中可用的网络接口:
/ $ ip a 

这将显示在此容器中配置的所有网络接口:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state 
UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
    valid_lft forever preferred_lft forever

此容器可用的唯一网络接口是其 LOOPBACK 接口。由于此容器未配置 IP 地址或默认网关,常见的网络命令将无法使用。

  1. 使用 Alpine Linux Docker 镜像中默认提供的 ping 实用程序测试网络连接的缺失。尝试 ping IP 地址为 8.8.8.8 的谷歌 DNS 服务器:
/ # ping 8.8.8.8

ping 命令的输出应显示它没有网络连接:

PING 8.8.8.8 (8.8.8.8): 56 data bytes
ping: sendto: Network unreachable

使用exit命令返回到主终端会话。

现在您已经仔细查看了none网络,请考虑host网络驱动程序。Docker 中的host网络驱动程序是独特的,因为它没有任何中间接口或创建任何额外的子网。相反,host网络驱动程序与主机操作系统共享网络堆栈,因此主机可用的任何网络接口也适用于以host模式运行的容器。

  1. 要开始以host模式运行容器,请执行ifconfig(如果在 macOS 或 Linux 上运行)或使用ipconfig(如果在 Windows 上运行)来列出主机机器上可用的网络接口清单:
$ ifconfig

这应该输出主机机器上可用的网络接口列表:

图 6.17:主机机器上配置的网络接口列表

图 6.17:主机机器上配置的网络接口列表

在此示例中,您的主机的主要网络接口是enp1s0,IP 地址为192.168.122.185

注意

在 macOS 或 Windows 上的某些版本的 Docker Desktop 可能无法正确启动和运行host网络模式或使用macvlan网络驱动程序的容器,因为这些功能依赖于 Linux 内核。在 macOS 或 Windows 上运行这些示例时,您可能会看到运行 Docker 的基础 Linux 虚拟机的网络详细信息,而不是 macOS 或 Windows 主机机器上可用的网络接口。

  1. 使用docker run命令在host网络中启动一个 Alpine Linux 容器。将其命名为hostnet1以区分其他容器:
docker run -itd --network host --name hostnet1 alpine:latest

Docker 将使用host网络在后台启动此容器。

  1. 使用docker inspect命令查看刚创建的hostnet1容器的网络配置:
$ docker inspect hostnet1

这将以 JSON 格式显示运行容器的详细配置,包括网络详细信息:

图 6.18:hostnet1 容器的 docker inspect 输出

图 6.18:hostnet1 容器的 docker inspect 输出

需要注意的是,NetworkSettings块的输出看起来很像您在none网络中部署的容器。在“主机”网络模式下,Docker 不会为容器实例分配 IP 地址或网关,因为它直接与主机机器共享所有网络接口。

  1. 使用docker exec访问此容器内的sh shell,提供名称hostnet1
$ docker exec -it hostnet1 /bin/sh

这应该会将您放入hostnet1容器内的 root shell 中。

  1. hostnet1容器内,执行ifconfig命令列出可用的网络接口:
/ # ifconfig

应该显示此容器内可用的完整网络接口列表:

图 6.19:显示 hostnet1 容器内可用的网络接口

图 6.19:显示 hostnet1 容器内可用的网络接口

请注意,这个网络接口列表与直接查询主机机器时遇到的是相同的。这是因为这个容器和主机机器直接共享网络。对主机机器可用的任何东西也将对在“主机”网络模式下运行的容器可用。

  1. 使用exit命令结束 shell 会话并返回到主机机器的终端。

  2. 为了更充分地了解 Docker 中共享网络模型的工作原理,以“主机”网络模式启动一个 NGINX 容器。NGINX 容器会自动暴露端口80,以前我们必须将其转发到主机机器上的一个端口。使用docker run命令在主机机器上启动一个 NGINX 容器:

$ docker run -itd --network host --name hostnet2 nginx:latest

这个命令将在“主机”网络模式下启动一个 NGINX 容器。

  1. 在主机机器上使用 Web 浏览器导航至http://localhost:80图 6.20:访问运行在主机网络模式下的容器的 NGINX 默认网页在主机网络模式下运行

图 6.20:访问在主机网络模式下运行的容器的 NGINX 默认网页

您应该能够在 Web 浏览器中看到 NGINX 默认网页。需要注意的是,docker run命令没有明确地将任何端口转发或暴露给主机机器。由于容器在“主机”网络模式下运行,容器默认暴露的任何端口将直接在主机机器上可用。

  1. 使用docker run命令在host网络模式下创建另一个 NGINX 实例。将此容器命名为hostnet3,以便与其他两个容器实例区分开来:
$ docker run -itd --network host --name hostnet3 nginx:latest
  1. 现在使用docker ps -a命令列出所有容器,包括运行和停止状态的容器:
$ docker ps -a

将显示运行中的容器列表:

CONTAINER ID  IMAGE         COMMAND                CREATED
  STATUS                        PORTS           NAMES
da56fcf81d02  nginx:latest  "nginx -g 'daemon of…" 4 minutes ago
  Exited (1) 4 minutes ago                      hostnet3
5786dac6fd27  nginx:latest  "nginx -g 'daemon of…" 37 minutes ago
  Up 37 minutes                                 hostnet2
648b291846e7  alpine:latest "/bin/sh"              38 minutes ago
  Up 38 minutes                                 hostnet
  1. 根据上述输出,您可以看到hostnet3容器已退出并当前处于停止状态。要更充分地了解原因,使用docker logs命令查看容器日志:
$ docker logs hostnet3

日志输出应显示如下:

图 6.21:hostnet3 容器中的 NGINX 错误

图 6.21:hostnet3 容器中的 NGINX 错误

基本上,这第二个 NGINX 容器实例无法正常启动,因为它无法绑定到主机上的端口80。原因是hostnet2容器已经在监听该端口。

注意

请注意,以host网络模式运行的容器需要谨慎部署和考虑。如果没有适当的规划和架构,容器的泛滥可能会导致在同一台机器上运行的容器实例之间发生各种端口冲突。

  1. 您将要调查的下一个类型的本机 Docker 网络是macvlan。在macvlan网络中,Docker 将为容器实例分配一个 MAC 地址,使其在特定网络段上看起来像是物理主机。它可以在bridge模式下运行,该模式使用父host网络接口来获得对底层网络的物理访问,或者在802.1Q trunk模式下运行,该模式利用 Docker 动态创建的子接口。

  2. 首先,使用docker network create命令指定主机上的物理接口作为父接口,通过macvlan Docker 网络驱动程序创建一个新网络。

  3. 在之前的ifconfigipconfig输出中,你看到enp1s0接口是机器上的主要网络接口。替换你的机器的主要网络接口的名称。由于你正在使用主机机器的主要网络接口作为父接口,为我们的容器的网络连接指定相同的子网(或者在该空间内更小的子网)。在这里使用192.168.122.0/24子网,因为它是主要网络接口的相同子网。同样,你想要指定与父接口相同的默认网关。使用主机机器的相同子网和网关:

$ docker network create -d macvlan --subnet=192.168.122.0/24 --gateway=192.168.122.1 -o parent=enp1s0 macvlan-net1

这个命令应该创建一个名为macvlan-net1的网络。

  1. 使用docker network ls命令来确认网络已经被创建,并且正在使用macvlan网络驱动程序:
$ docker network ls

这个命令将输出当前在你的环境中定义的所有网络。你应该会看到macvlan-net1网络:

NETWORK ID       NAME            DRIVER     SCOPE
f4c9408f22e2     bridge          bridge     local
f52b4a5440ad     host            host       local
b895c821b35f     macvlan-net1    macvlan    local
9bed60b88784     none            null       local
  1. 现在macvlan网络已经在 Docker 中定义,创建一个在这个网络中的容器,并从主机的角度调查网络连接。使用docker run命令在macvlan网络macvlan-net1中创建另一个名为macvlan1的 Alpine Linux 容器:
$ docker run -itd --name macvlan1 --network macvlan-net1 alpine:latest

这应该会在后台启动一个名为macvlan1的 Alpine Linux 容器实例。

  1. 使用docker ps -a命令来检查并确保这个容器实例正在运行:
$ docker ps -a

这应该显示名为macvlan1的容器正在按预期运行:

CONTAINER ID   IMAGE           COMMAND      CREATED
  STATUS              PORTS              NAMES
cd3c61276759   alpine:latest   "/bin/sh"    3 seconds ago
  Up 1 second                            macvlan1
  1. 使用docker inspect命令来调查这个容器实例的网络配置:
$ docker inspect macvlan1

容器配置的详细输出应该被显示出来。以下输出已经被截断,只显示了 JSON 格式的网络设置部分:

图 6.22:macvlan1 网络的 docker 网络检查输出

图 6.22:macvlan1 网络的 docker 网络检查输出

从这个输出中,你可以看到这个容器实例(类似于其他网络模式的容器)既有 IP 地址又有默认网关。还可以得出结论,这个容器也在192.168.122.0/24网络中拥有一个 OSI 模型第 2 层的 MAC 地址,根据Networks子部分下的MacAddress参数。这个网络段内的其他主机会认为这台机器是另一个物理节点,而不是托管在子网中的节点上的容器。

  1. 使用docker run创建macvlan-net1网络内的第二个容器实例命名为macvlan2
$ docker run -itd --name macvlan2 --network macvlan-net1 alpine:latest

这应该在macvlan-net1网络中启动另一个容器实例。

  1. 运行docker inspect命令以查看macvlan-net2容器实例的 MAC 地址:
$ docker inspect macvlan2

这将以 JSON 格式输出macvlan2容器实例的详细配置,此处仅显示相关的网络设置。

图 6.23:macvlan2 容器的 docker inspect 输出

图 6.23:macvlan2 容器的 docker inspect 输出

可以在此输出中看到macvlan2容器具有与macvlan1容器实例不同的 IP 地址和 MAC 地址。Docker 分配不同的 MAC 地址以确保在许多容器使用macvlan网络时不会出现第 2 层冲突。

  1. 运行docker exec命令以访问此容器内的sh shell:
$ docker exec -it macvlan1 /bin/sh

这应该将您放入容器内的 root 会话。

  1. 在容器内使用ifconfig命令观察在macvlan1容器的docker inspect输出中看到的 MAC 地址是否存在于容器的主要网络接口的 MAC 地址中:
/ # ifconfig

eth0接口的详细信息中,查看HWaddr参数。您还可以注意inet addr参数下列出的 IP 地址,以及通过此网络接口传输和接收的字节数-RX 字节(接收的字节数)和TX 字节(传输的字节数):

eth0      Link encap:Ethernet  HWaddr 02:42:C0:A8:7A:02
          inet addr:192.168.122.2  Bcast:192.168.122.255
                                   Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:353 errors:0 dropped:0 overruns:0 frame:0
          TX packets:188 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:1789983 (1.7 MiB)  TX bytes:12688 (12.3 KiB)
  1. 使用 Alpine Linux 容器中可用的apk软件包管理器安装arping实用程序。这是一个用于向 MAC 地址发送arp消息以检查第 2 层连接的工具:
/ # apk add arping

arping实用程序应该安装在macvlan1容器内:

fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main
/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community
/x86_64/APKINDEX.tar.gz
(1/3) Installing libnet (1.1.6-r3)
(2/3) Installing libpcap (1.9.1-r0)
(3/3) Installing arping (2.20-r0)
Executing busybox-1.31.1-r9.trigger
OK: 6 MiB in 17 packages
  1. macvlan2容器实例的第 3 层 IP 地址指定为arping的主要参数。现在,arping将自动查找 MAC 地址并检查与其的第 2 层连接:
/ # arping 192.168.122.3

arping实用程序应该报告macvlan2容器实例的正确 MAC 地址,表明成功的第 2 层网络连接:

ARPING 192.168.122.3
42 bytes from 02:42:c0:a8:7a:03 (192.168.122.3): index=0 
time=8.563 usec
42 bytes from 02:42:c0:a8:7a:03 (192.168.122.3): index=1 
time=18.889 usec
42 bytes from 02:42:c0:a8:7a:03 (192.168.122.3): index=2 
time=15.917 use
type exit to return to the shell of your primary terminal. 
  1. 使用docker ps -a命令检查容器的状态:
$ docker ps -a 

此命令的输出应显示环境中所有正在运行和停止的容器实例。

  1. 接下来,使用docker stop停止所有正在运行的容器,然后是容器名称或 ID:
$ docker stop hostnet1

对您环境中的所有运行容器重复此步骤。

  1. 使用 docker system prune 命令清理容器镜像和未使用的网络:
$ docker system prune -fa 

这个命令将清理您的机器上剩余的所有未使用的容器镜像、网络和卷。

在这个练习中,我们看了一下 Docker 默认提供的四种默认网络驱动程序:bridgehostmacvlannone。对于每个示例,我们探讨了网络的功能,使用这些网络驱动程序部署的容器如何与主机机器一起工作,以及它们如何在网络上与其他容器一起工作。

Docker 默认公开的网络功能可以用来部署非常高级的网络配置,正如我们迄今所见。Docker 还提供了管理和协调集群化的容器网络之间的能力。

在下一节中,我们将看看创建网络,这些网络将在 Docker 主机之间创建覆盖网络,以确保容器实例之间的直接连接。

Docker Overlay 网络

Overlay 网络是在特定目的下在物理(底层)网络之上创建的逻辑网络。例如,虚拟专用网络VPN)是一种常见的 overlay 网络类型,它使用互联网来创建与另一个私有网络的连接。Docker 可以创建和管理容器之间的 overlay 网络,这些网络可以用于容器化应用程序直接相互通信。当容器部署到 overlay 网络中时,它们部署在集群中的哪个主机上并不重要;它们将直接连接到同一 overlay 网络中存在的其他容器化服务,就像它们存在于同一物理主机上一样。

练习 6.04:定义 Overlay 网络

Docker overlay 网络用于在 Docker 集群中的机器之间创建网格网络。在这个练习中,您将使用两台机器创建一个基本的 Docker 集群。理想情况下,这些机器将存在于同一个网络段,以确保它们之间的直接网络连接和快速网络连接。此外,它们应该在支持的 Linux 发行版(如 RedHat、CentOS 或 Ubuntu)上运行相同版本的 Docker。

您将定义将跨越 Docker 集群中的主机的 overlay 网络。然后,您将确保部署在不同主机上的容器可以通过 overlay 网络相互通信:

注意

这个练习需要访问一个安装了 Docker 的辅助机器。通常,基于云的虚拟机或部署在另一个虚拟化程序中的机器效果最好。在使用 Docker Desktop 在系统上部署 Docker 集群可能会导致网络问题或严重的性能下降。

  1. 在第一台机器 Machine1 上运行 docker --version 来查找当前正在运行的 Docker 版本。
Machine1 ~$ docker --version

将显示 Machine1 的 Docker 安装的版本细节:

Docker version 19.03.6, build 369ce74a3c

然后,您可以对 Machine2 执行相同的操作:

Machine2 ~$ docker --version

将显示 Machine2 的 Docker 安装的版本细节:

Docker version 19.03.6, build 369ce74a3c

在继续之前,验证已安装的 Docker 版本是否相同。

注意

Docker 版本可能会因系统而异。

  1. Machine1 上,运行 docker swarm init 命令来初始化 Docker 集群:
Machine1 ~$ docker swarm init

这应该打印出您可以在其他节点上使用的命令,以加入 Docker 集群,包括 IP 地址和 join 令牌:

docker swarm join --token SWMTKN-1-57n212qtvfnpu0ab28tewiorf3j9fxzo9vaa7drpare0ic6ohg-5epus8clyzd9xq7e7ze1y0p0n 
192.168.122.185:2377
  1. Machine2 上,运行由 Machine1 提供的 docker swarm join 命令,以加入 Docker 集群:
Machine2 ~$  docker swarm join --token SWMTKN-1-57n212qtvfnpu0ab28tewiorf3j9fxzo9vaa7drpare0ic6ohg-5epus8clyzd9xq7e7ze1y0p0n 192.168.122.185:2377

Machine2 应成功加入 Docker 集群:

This node joined a swarm as a worker.
  1. 在两个节点上执行 docker info 命令,以确保它们已成功加入集群:

Machine1

Machine1 ~$ docker info

Machine2

Machine2 ~$ docker info

以下输出是 docker info 输出的 swarm 部分的截断。从这些细节中,您将看到这些 Docker 节点配置在一个集群中,并且集群中有两个节点,一个是单个管理节点(Machine1)。这些参数在两个节点上应该是相同的,除了 Is Manager 参数,其中 Machine1 将是管理节点。默认情况下,Docker 将为默认的 Docker 集群 overlay 网络分配一个默认子网 10.0.0.0/8

 swarm: active
  NodeID: oub9g5383ifyg7i52yq4zsu5a
  Is Manager: true
  ClusterID: x7chp0w3two04ltmkqjm32g1f
  Managers: 1
  Nodes: 2
  Default Address Pool: 10.0.0.0/8  
  SubnetSize: 24
  Data Path Port: 4789
  Orchestration:
    Task History Retention Limit: 5
  1. Machine1 中,使用 docker network create 命令创建一个 overlay 网络。由于这是一个将跨越简单集群中的多个节点的网络,因此需要将 overlay 驱动程序指定为网络驱动程序。将此网络命名为 overlaynet1。使用尚未被 Docker 主机上的任何网络使用的子网和网关,以避免子网冲突。使用 172.45.0.0/16172.45.0.1 作为网关:
Machine1 ~$ docker network create overlaynet1 --driver overlay --subnet 172.45.0.0/16 --gateway 172.45.0.1

将创建 overlay 网络。

  1. 使用 docker network ls 命令来验证网络是否成功创建并且是否使用了正确的 overlay 驱动程序:
Machine1 ~$ docker network ls

将显示 Docker 主机上可用的网络列表:

NETWORK ID       NAME              DRIVER     SCOPE
54f2af38e6a8     bridge            bridge     local
df5ebd75303e     docker_gwbridge   bridge     local
f52b4a5440ad     host              host       local
8hm1ouvt4z7t     ingress           overlay    swarm
9bed60b88784     none              null       local
60wqq8ewt8zq     overlaynet1       overlay    swarm
  1. 使用docker service create命令创建一个将跨多个节点的 swarm 集群的服务。将容器部署为服务允许您指定一个容器实例的多个副本,以进行水平扩展或在集群中的节点之间扩展容器实例以实现高可用性。为了保持这个例子简单,创建一个 Alpine Linux 的单个容器服务。将此服务命名为alpine-overlay1
Machine1 ~$ docker service create -t --replicas 1 --network overlaynet1 --name alpine-overlay1 alpine:latest

一个基于文本的进度条将显示alpine-overlay1服务部署的进度:

overall progress: 1 out of 1 tasks 
1/1: running   [===========================================>]
verify: Service converged 
  1. 重复相同的docker service create命令,但现在将alpine-overlay2指定为服务名称:
Machine1 ~$ docker service create -t --replicas 1 --network overlaynet1 --name alpine-overlay2 alpine:latest

一个基于文本的进度条将再次显示服务部署的进度:

overall progress: 1 out of 1 tasks 
1/1: running   [===========================================>]
verify: Service converged

注意

有关在 Docker swarm 中创建服务的更多详细信息,请参阅《第九章,Docker Swarm》。由于本练习的范围是网络,我们现在将专注于网络组件。

  1. Machine1节点,执行docker ps命令以查看此节点上正在运行的服务:
Machine1 ~$ docker ps 

正在运行的容器将被显示。Docker 将在 Docker swarm 集群中的节点之间智能地扩展容器。在本例中,alpine-overlay1服务的容器落在了Machine1上。根据 Docker 部署服务的方式,您的环境可能会有所不同:

CONTAINER ID    IMAGE           COMMAND     CREATED
  STATUS              PORTS             NAMES
4d0f5fa82add    alpine:latest   "/bin/sh"   59 seconds ago
  Up 57 seconds                         alpine-overlay1.1.
r0tlm8w0dtdfbjaqyhobza94p
  1. 运行docker inspect命令以查看正在运行的容器的详细信息:
Machine1 ~$ docker inspect alpine-overlay1.1.r0tlm8w0dtdfbjaqyhobza94p

将显示正在运行的容器实例的详细信息。以下输出已被截断以显示docker inspect输出的NetworkSettings部分:

图 6.24:检查 alpine-overlay1 容器实例

图 6.24:检查 alpine-overlay1 容器实例

注意,此容器的 IP 地址与您在Machine1上指定的子网中的预期值相同。

  1. Machine2实例上,执行docker network ls命令以查看主机上可用的 Docker 网络:
Machine2 ~$ docker network ls

将显示 Docker 主机上所有可用的 Docker 网络的列表:

NETWORK ID       NAME              DRIVER     SCOPE
8c7755be162f     bridge            bridge     local
28055e8c63a0     docker_gwbridge   bridge     local
c62fb7ac090f     host              host       local
8hm1ouvt4z7t     ingress           overlay    swarm
6182d77a8f62     none              null       local
60wqq8ewt8zq     overlaynet1       overlay    swarm

注意,Machine1上定义的overlaynet1网络也可在Machine2上使用。这是因为使用overlay驱动程序创建的网络可用于 Docker swarm 集群中的所有主机。这使得可以使用此网络部署容器以在集群中的所有主机上运行。

  1. 使用docker ps命令列出此 Docker 实例上正在运行的容器:
Machine2 ~$ docker ps

将所有正在运行的容器列出。在这个例子中,alpine-overlay2 服务中的容器落在了 Machine2 集群节点上:

CONTAINER ID   IMAGE           COMMAND      CREATED
  STATUS              PORTS               NAMES
53747ca9af09   alpine:latest   "/bin/sh"    33 minutes ago
  Up 33 minutes                           alpine-overlay2.1.ui9vh6zn18i48sxjbr8k23t71

注意

在您的示例中,服务落在哪个节点可能与此处显示的不同。Docker 根据各种标准(如可用的 CPU 带宽、内存和对部署容器的调度限制)来决定如何部署容器。

  1. 使用 docker inspect 来调查该容器的网络配置:
Machine2 ~$ docker inspect alpine-overlay2.1.ui9vh6zn18i48sxjbr8k23t71

将显示详细的容器配置。此输出已被截断,以 JSON 格式显示输出的 NetworkSettings 部分:

图 6.25:alpine-overlay2 容器实例的 docker inspect 输出

图 6.25:alpine-overlay2 容器实例的 docker inspect 输出

请注意,该容器还在 overlaynet1 overlay 网络中拥有一个 IP 地址。

  1. 由于两个服务都部署在同一个 overlay 网络中,但存在于两个独立的主机中,您可以看到 Docker 正在使用 underlay 网络来代理 overlay 网络的流量。通过尝试从一个服务到另一个服务的 ping 来检查服务之间的网络连接。需要注意的是,类似于部署在同一网络中的静态容器,部署在同一网络上的服务可以使用 Docker DNS 通过名称解析彼此。在 Machine2 主机上使用 docker exec 命令访问 alpine-overlay2 容器内的 sh shell:
Machine2 ~$ docker exec -it alpine-overlay2.1.ui9vh6zn18i48sxjbr8k23t71 /bin/sh

这应该将您放入 alpine-overlay2 容器实例的 root shell。使用 ping 命令发起与 alpine-overlay1 容器的网络通信:

/ # ping alpine-overlay1
PING alpine-overlay1 (172.45.0.10): 56 data bytes
64 bytes from 172.45.0.10: seq=0 ttl=64 time=0.314 ms
64 bytes from 172.45.0.10: seq=1 ttl=64 time=0.274 ms
64 bytes from 172.45.0.10: seq=2 ttl=64 time=0.138 ms

请注意,即使这些容器部署在两个独立的主机上,它们也可以通过名称使用共享的 overlay 网络进行通信。

  1. Machine1 主机,您可以尝试与 alpine-overlay2 服务容器进行相同的通信。使用 docker exec 命令在 Machine1 主机上访问 alpine-overlay2 容器内的 sh shell:
Machine1 ~$ docker exec -it alpine-overlay1.1.r0tlm8w0dtdfbjaqyhobza94p /bin/sh

这应该将您放入容器内的 root shell。使用 ping 命令发起与 alpine-overlay2 容器实例的网络通信:

/ # ping alpine-overlay2
PING alpine-overlay2 (172.45.0.13): 56 data bytes
64 bytes from 172.45.0.13: seq=0 ttl=64 time=0.441 ms
64 bytes from 172.45.0.13: seq=1 ttl=64 time=0.227 ms
64 bytes from 172.45.0.13: seq=2 ttl=64 time=0.282 ms

再次注意,通过使用 Docker DNS,可以使用 overlay 网络驱动程序在主机之间解析 alpine-overlay2 容器的 IP 地址。

  1. 使用 docker service rm 命令从 Machine1 节点中删除这两个服务:
Machine1 ~$ docker service rm alpine-overlay1
Machine1 ~$ docker service rm alpine-overlay2

对于这些命令中的每一个,服务名称将会短暂地显示,表明命令执行成功。在两个节点上,docker ps 将显示当前没有正在运行的容器。

  1. 使用 docker rm 命令并指定名称 overlaynet1 删除 overlaynet1 Docker 网络。
Machine1 ~$ docker network rm overlaynet1

overlaynet1 网络将被删除。

在这个练习中,我们研究了 Docker 集群中两个主机之间的 overlay 网络。Overlay 网络在 Docker 容器集群中非常有益,因为它允许在集群中的节点之间水平扩展容器。从网络的角度来看,这些容器可以通过使用服务网格在主机机器的物理网络接口上直接进行通信。这不仅减少了延迟,还通过利用 Docker 的许多功能(如 DNS)简化了部署。

现在我们已经看过了所有本地 Docker 网络类型以及它们的功能示例,我们可以看看 Docker 网络的另一个方面,这个方面最近变得越来越受欢迎。由于 Docker 网络非常模块化,正如我们所见,Docker 支持插件系统,允许用户部署和管理自定义网络驱动程序。

在下一节中,我们将通过从 Docker Hub 安装第三方网络驱动程序来了解非本地 Docker 网络是如何工作的。

非本地 Docker 网络

在本章的最后一节中,我们将讨论非本地 Docker 网络。除了可用的本地 Docker 网络驱动程序之外,Docker 还支持用户编写或通过 Docker Hub 从第三方下载的自定义网络驱动程序。自定义的第三方网络驱动程序在需要非常特定的网络配置或容器网络需要以特定方式运行的情况下非常有用。例如,一些网络驱动程序提供了用户设置自定义策略以控制对互联网资源的访问,或者定义容器化应用之间通信的白名单的能力。从安全、策略和审计的角度来看,这是有帮助的。

在接下来的练习中,我们将下载并安装 Weave Net 驱动程序,并在 Docker 主机上创建一个网络。Weave Net 是一个得到高度支持的第三方网络驱动程序,可以很好地查看容器网格网络,允许用户创建可以跨多云场景的复杂服务网格基础设施。我们将从 Docker Hub 安装 Weave Net 驱动程序,并在前面练习中定义的简单集群中配置一个基本网络。

练习 6.05:安装和配置 Weave Net Docker 网络驱动程序

在这个练习中,您将下载并安装 Weave Net Docker 网络驱动程序,并在之前创建的 Docker 集群中部署它。Weave Net 是最常见和灵活的第三方 Docker 网络驱动程序之一。使用 Weave Net,可以定义非常复杂的网络配置,以实现基础设施的最大灵活性:

  1. Machine1节点上使用docker plugin install命令从 Docker Hub 安装 Weave Net 驱动程序:
Machine1 ~$ docker plugin install store/weaveworks/net-plugin:2.5.2

这将提示您在安装它的机器上授予 Weave Net 权限。授予请求的权限是安全的,因为 Weave Net 需要这些权限才能在主机操作系统上正确设置网络驱动程序:

Plugin "store/weaveworks/net-plugin:2.5.2" is requesting 
the following privileges:
 - network: [host]
 - mount: [/proc/]
 - mount: [/var/run/docker.sock]
 - mount: [/var/lib/]
 - mount: [/etc/]
 - mount: [/lib/modules/]
 - capabilities: [CAP_SYS_ADMIN CAP_NET_ADMIN CAP_SYS_MODULE]
Do you grant the above permissions? [y/N]

通过按下y键来回答提示。Weave Net 插件应该安装成功。

  1. Machine2节点上运行相同的docker plugin install命令。Docker 集群中的所有节点都应该安装了插件,因为所有节点都将参与到集群网格网络中:
Machine2 ~$ docker plugin install store/weaveworks/net-plugin:2.5.2

权限提示将被显示。在提示继续安装时回答y

Plugin "store/weaveworks/net-plugin:2.5.2" is requesting 
the following privileges:
 - network: [host]
 - mount: [/proc/]
 - mount: [/var/run/docker.sock]
 - mount: [/var/lib/]
 - mount: [/etc/]
 - mount: [/lib/modules/]
 - capabilities: [CAP_SYS_ADMIN CAP_NET_ADMIN CAP_SYS_MODULE]
Do you grant the above permissions? [y/N]
  1. Machine1节点上使用docker network create命令创建一个网络。将 Weave Net 驱动程序指定为主驱动程序,网络名称为weavenet1。对于子网和网关参数,请使用之前练习中尚未使用的唯一子网:
Machine1 ~$  docker network create --driver=store/weaveworks/net-plugin:2.5.2 --subnet 10.1.1.0/24 --gateway 10.1.1.1 weavenet1

这应该在 Docker 集群中创建一个名为weavenet1的网络。

  1. 使用docker network ls命令列出 Docker 集群中可用的网络:
Machine1 ~$ docker network ls 

weavenet1网络应该显示在列表中:

NETWORK ID     NAME             DRIVER
  SCOPE
b3f000eb4699   bridge           bridge
  local
df5ebd75303e   docker_gwbridge  bridge
  local
f52b4a5440ad   host             host
  local
8hm1ouvt4z7t   ingress          overlay
  swarm
9bed60b88784   none             null
  local
q354wyn6yvh4   weavenet1        store/weaveworks/net-plugin:2.5.2
  swarm
  1. Machine2节点上执行docker network ls命令,以确保weavenet1网络也存在于该机器上:
Machine2 ~$ docker network ls 

weavenet1网络应该被列出:

NETWORK ID    NAME              DRIVER
  SCOPE
b3f000eb4699  bridge            bridge
  local
df5ebd75303e  docker_gwbridge   bridge
  local
f52b4a5440ad  host              host
  local
8hm1ouvt4z7t  ingress           overlay
  swarm
9bed60b88784  none              null
  local
q354wyn6yvh4  weavenet1         store/weaveworks/net-plugin:2.5.2
  swarm
  1. Machine1节点上,使用docker service create命令创建一个名为alpine-weavenet1的服务,该服务使用weavenet1网络:
Machine1 ~$ docker service create -t --replicas 1 --network weavenet1 --name alpine-weavenet1 alpine:latest

文本进度条将显示服务的部署状态。它应该在没有任何问题的情况下完成:

overall progress: 1 out of 1 tasks 
1/1: running   [===========================================>]
verify: Service converged 
  1. 再次使用docker service create命令在weavenet1网络中创建另一个名为alpine-weavenet2的服务:
Machine1 ~$ docker service create -t --replicas 1 --network weavenet1 --name alpine-weavenet2 alpine:latest

文本进度条将再次显示,指示服务创建的状态:

overall progress: 1 out of 1 tasks 
1/1: running   [===========================================>]
verify: Service converged 
  1. 运行docker ps命令验证集群中每个节点上是否成功运行了 Alpine 容器:

Machine1

Machine1 ~$ docker ps

Machine2

Machine2 ~$ docker ps

其中一个服务容器应该在两台机器上都正常运行:

Machine1

CONTAINER ID    IMAGE           COMMAND      CREATED
  STATUS              PORTS               NAMES
acc47f58d8b1    alpine:latest   "/bin/sh"    7 minutes ago
  Up 7 minutes                            alpine-weavenet1.1.zo5folr5yvu6v7cwqn23d2h97

Machine2

CONTAINER ID    IMAGE           COMMAND     CREATED
  STATUS              PORTS        NAMES
da2a45d8c895    alpine:latest   "/bin/sh"   4 minutes ago
  Up 4 minutes                     alpine-weavenet2.1.z8jpiup8yetj
rqca62ub0yz9k
  1. 使用docker exec命令访问weavenet1.1容器实例内部的sh shell。确保在运行此容器的 swarm 集群中的节点上运行此命令:
Machine1 ~$ docker exec -it alpine-weavenet1.1.zo5folr5yvu6v7cwqn23d2h97 /bin/sh

这应该将您带入容器内部的 root shell:

/ #
  1. 使用ifconfig命令查看此容器内部存在的网络接口:
/ # ifconfig

这将显示一个名为ethwe0的新命名网络接口。Weave Net 核心网络策略的核心部分是在容器内创建自定义命名的接口,以便进行简单识别和故障排除。需要注意的是,此接口被分配了一个 IP 地址,该地址来自我们提供的子网作为配置参数:

ethwe0  Link encap:Ethernet  HWaddr AA:11:F2:2B:6D:BA  
        inet addr:10.1.1.3  Bcast:10.1.1.255  Mask:255.255.255.0
        UP BROADCAST RUNNING MULTICAST  MTU:1376  Metric:1
        RX packets:37 errors:0 dropped:0 overruns:0 frame:0
        TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
        collisions:0 txqueuelen:0 
        RX bytes:4067 (3.9 KiB)  TX bytes:0 (0.0 B)
  1. 从容器内部,使用ping实用程序通过名称 pingalpine-weavenet2服务:
ping alpine-weavenet2

您应该看到来自alpine-weavenet2服务的解析 IP 地址的响应:

64 bytes from 10.1.1.4: seq=0 ttl=64 time=3.430 ms
64 bytes from 10.1.1.4: seq=1 ttl=64 time=1.541 ms
64 bytes from 10.1.1.4: seq=2 ttl=64 time=1.363 ms
64 bytes from 10.1.1.4: seq=3 ttl=64 time=1.850 ms

注意

由于 Docker 和 Docker Swarm 最近版本中 Docker libnetwork 堆栈的更新,通过名称 ping 服务:alpine-weavenet2可能无法正常工作。为了证明网络按预期工作,请尝试直接 ping 容器的名称而不是服务的名称:alpine-weavenet2.1.z8jpiup8yetjrqca62ub0yz9k - 请记住,在您的实验环境中,此容器的名称将不同。

  1. 还可以尝试从这些容器中通过开放的互联网 ping Google DNS 服务器(8.8.8.8)以确保这些容器具有互联网访问权限:
ping 8.8.8.8

您应该看到返回的响应,表明这些容器具有互联网访问权限:

/ # ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=51 time=13.224 ms
64 bytes from 8.8.8.8: seq=1 ttl=51 time=11.840 ms
type exit to quit the shell session in this container.
  1. 使用docker service rm命令从Machine1节点中删除两个服务:
Machine1 ~$ docker service rm alpine-weavenet1
Machine1 ~$ docker service rm alpine-weavenet2

这将删除两个服务,停止并删除容器实例。

  1. 通过运行以下命令删除 Weave Net 网络:
Machine1 ~$ docker network rm weavenet1

应删除和移除 Weave Net 网络。

在容器化网络概念的强大系统中,Docker 拥有广泛的网络驱动程序,几乎可以满足工作负载需求的任何情况。然而,对于所有超出默认 Docker 网络驱动程序范围的用例,Docker 支持第三方自定义驱动程序,几乎可以满足可能出现的任何网络条件。第三方网络驱动程序使 Docker 能够灵活地与各种平台甚至跨多个云提供商进行集成。在这个练习中,我们看了安装和配置 Weave Net 网络插件,并在 Docker 集群中创建简单服务以利用这个网络。

在接下来的活动中,您将应用本章学到的知识,使用各种 Docker 网络驱动程序部署多容器基础架构解决方案。这些容器将使用不同的 Docker 网络驱动程序在同一台主机甚至跨多台主机在 Docker 集群配置中进行通信。

活动 6.01:利用 Docker 网络驱动程序

在本章的前面,我们看了各种类型的 Docker 网络驱动程序以及它们以不同的方式运行,为您的容器环境提供各种程度的网络功能。在这个活动中,您将在 Docker“桥接”网络中部署 Panoramic Trekking 应用程序的示例容器。然后,您将以“主机”网络模式部署一个辅助容器,该容器将作为监控服务器,并能够使用curl来验证应用程序是否按预期运行。

执行以下步骤完成此活动:

  1. 创建一个自定义的 Docker“桥接”网络,具有自定义子网和网关 IP。

  2. 在“桥接”网络中部署一个名为webserver1的 NGINX web 服务器,将容器上的转发端口80暴露到主机上的端口8080

  3. 在“主机”网络模式下部署一个 Alpine Linux 容器,该容器将作为监控容器。

  4. 使用 Alpine Linux 容器对 NGINX web 服务器进行curl操作并获得响应。

预期输出:

在活动完成后连接到转发端口8080webserver1容器的 IP 地址上的端口80,您应该会得到以下输出:

图 6.26:从容器实例的 IP 地址访问 NGINX web 服务器

图 6.26:从容器实例的 IP 地址访问 NGINX Web 服务器

注意

本活动的解决方案可以通过此链接找到。

在下一个活动中,我们将看看如何利用 Docker overlay网络为我们的全景徒步应用程序提供水平可扩展性。通过在多个主机上部署全景徒步,我们可以确保可靠性和耐久性,并利用环境中多个节点的系统资源。

活动 6.02:overlay网络实战

在本章中,您已经看到了在集群主机之间部署多个容器时,overlay网络是多么强大,它们之间可以直接进行网络连接。在本活动中,您将重新访问双节点 Docker swarm 集群,并从全景徒步应用程序创建服务,这些服务将使用 Docker DNS 在两个主机之间进行连接。在这种情况下,不同的微服务将在不同的 Docker swarm 主机上运行,但仍然能够利用 Docker overlay网络直接相互通信。

要成功完成此活动,请执行以下步骤:

  1. 使用自定义子网和网关的 Docker overlay网络

  2. 一个名为trekking-app的应用程序 Docker swarm 服务,使用 Alpine Linux 容器

  3. 一个名为database-app的数据库 Docker swarm 服务,使用 PostgreSQL 12 容器(额外学分提供默认凭据)

  4. 证明trekking-app服务可以使用overlay网络与database-app服务通信

预期输出:

trekking-app服务应能够与database-app服务通信,可以通过 ICMP 回复进行验证,例如:

PING database-app (10.2.0.5): 56 data bytes
64 bytes from 10.2.0.5: seq=0 ttl=64 time=0.261 ms
64 bytes from 10.2.0.5: seq=1 ttl=64 time=0.352 ms
64 bytes from 10.2.0.5: seq=2 ttl=64 time=0.198 ms

注意

本活动的解决方案可以通过此链接找到。

摘要

在本章中,我们探讨了与微服务和 Docker 容器相关的许多网络方面。Docker 配备了许多驱动程序和配置选项,用户可以使用这些选项来调整他们的容器网络在几乎任何环境中的工作方式。通过部署正确的网络和正确的驱动程序,可以快速地建立强大的服务网格网络,实现容器之间的访问,而无需从任何物理 Docker 主机出口。甚至可以创建绑定到主机网络结构以利用底层网络基础设施的容器。

在 Docker 中可以启用的最强大的网络功能可能是能够在 Docker 主机集群之间创建网络的能力。这可以让我们快速地在主机之间创建和部署水平扩展的应用程序,以实现高可用性和冗余。通过利用底层网络,在集群中的“覆盖”网络允许容器利用强大的 Docker DNS 系统直接联系其他集群主机上运行的容器。

在下一章中,我们将看看强大的容器化基础设施的下一个支柱:存储。通过了解容器存储如何用于有状态的应用程序,可以设计出非常强大的解决方案,不仅涉及容器化的无状态应用程序,还涉及可以像其他容器一样轻松部署、扩展和优化的容器化数据库服务。

第七章:Docker 存储

概述

在本章中,您将学习 Docker 如何管理数据。了解在哪里存储数据以及您的服务将如何访问它是至关重要的。本章将探讨无状态与有状态的 Docker 容器运行,并深入探讨不同应用程序存储的配置设置选项。到本章结束时,您将能够区分 Docker 中不同的存储类型,并识别容器的生命周期及其各种状态。您还将学习如何创建和管理 Docker 卷。

介绍

在之前的章节中,您学习了如何从镜像运行容器以及如何配置其网络。您还学习了在从镜像创建容器时可以传递各种 Docker 命令。在本章中,您将学习如何在创建容器后控制这些容器。

假设您被指派为电子商店构建一个网络应用程序。您将需要一个数据库来存储产品目录、客户信息和购买交易。为了存储这些细节,您需要配置应用程序的存储设置。

Docker 中有两种类型的数据存储。第一种是与容器生命周期紧密耦合的存储。如果容器被移除,该存储类型上的文件也将被移除且无法检索。这些文件存储在容器本身内部的薄读/写层中。这种存储类型也被称为其他术语,例如本地存储、graphdriver存储和存储驱动程序。本章的第一部分专注于这种存储类型。这些文件可以是任何类型,例如,在基础镜像之上安装新层后 Docker 创建的文件。

本章的第二部分探讨了无状态和有状态服务。有状态应用程序是需要持久存储的应用程序,例如持久且超越容器生存期的数据库。在有状态服务中,即使容器被移除,数据仍然可以被访问。

容器以两种方式在主机上存储数据:通过卷和绑定挂载。不推荐使用绑定挂载,因为绑定挂载会将主机上的现有文件或目录绑定到容器内的路径。这种绑定增加了在主机上使用完整或相对路径的负担。然而,当您使用卷时,在主机上的 Docker 存储目录中创建一个新目录,并且 Docker 管理目录的内容。我们将在本章的第三部分专注于使用卷。

在探索 Docker 中不同类型的存储之前,让我们先探索容器的生命周期。

容器生命周期

容器是从其基本镜像中制作的。容器通过在图像层堆栈的顶部创建一个薄的读/写层来继承图像的文件系统。基本镜像保持完整,不对其进行任何更改。所有更改都发生在容器的顶层。例如,假设您创建了一个ubuntu:14.08的容器。这个镜像中没有wget软件包。当您安装wget软件包时,实际上是在顶层安装它。因此,您有一个用于基本镜像的层,以及另一个用于wget的层。

如果您也安装了Apache服务器,它将成为前两层之上的第三层。要保存所有更改,您需要将所有这些更改提交到新的镜像,因为您不能覆盖基本镜像。如果您不将更改提交到新镜像,这些更改将随容器的移除而被删除。

容器在其生命周期中经历许多其他状态,因此重要的是要了解容器在其生命周期中可能具有的所有状态。因此,让我们深入了解不同的容器状态:

图 7.1:容器生命周期

图 7.1:容器生命周期

容器经历的不同阶段如下:

  • 使用docker container run子命令,容器进入CREATED状态,如图 7.1所示。

  • 在每个容器内部,都有一个主要的进程在运行。当这个进程开始运行时,容器的状态会变为UP状态。

  • 使用docker container pause子命令,容器的状态变为UP(PAUSED)。容器被冻结或暂停,但仍处于UP状态,没有停止或移除。

  • 要恢复运行容器,请使用docker container unpause子命令。在这里,容器的状态将再次变为UP状态。

  • 使用docker container stop子命令停止容器而不删除它。容器的状态将变为EXITED状态。

  • 容器将在执行docker container killdocker container stop子命令时退出。要杀死容器,请使用docker container kill子命令。容器状态将变为EXITED。但是,要使容器退出,应该使用docker container stop子命令而不是docker container kill子命令。不要杀死你的容器;总是删除它们,因为删除容器会触发对容器的优雅关闭,给予时间,例如将数据保存到数据库,这是一个较慢的过程。然而,杀死不会这样做,可能会导致数据不一致。

  • 在停止或杀死容器后,您还可以恢复运行容器。要启动容器并将其返回到UP状态,请使用docker container startdocker container start -a子命令。docker container start -a等同于运行docker container start然后docker container attach。您不能将本地标准输入、输出和错误流附加到已退出的容器;容器必须首先处于UP状态才能附加本地标准输入、输出和错误流。

  • 要重新启动容器,请使用docker container restart子命令。重新启动子命令的作用类似于执行docker container stop,然后执行docker container start

  • 停止或杀死容器不会从系统中删除容器。要完全删除容器,请使用docker container rm子命令。

注意

您可以将几个 Docker 命令连接在一起 - 例如,docker container rm -f $(docker container ls -aq)。您想要首先执行的命令应包含在括号中。

在这种情况下,docker container ls -aq告诉 Docker 以安静模式列出所有容器,甚至是已退出的容器。-a选项表示显示所有容器,无论它们的状态如何。-q选项用于安静模式,这意味着仅显示数字 ID,而不是所有容器的详细信息。此命令的输出docker container ls -aq将成为docker container rm -f命令的输入。

了解 Docker 容器生命周期事件可以为某些应用程序是否需要持久存储提供良好的背景。在继续讨论 Docker 中存在的不同存储类型之前,让我们执行上述命令并在以下练习中探索不同的容器状态。

注意

请使用touch命令创建文件,并使用vim命令使用 vim 编辑器处理文件。

练习 7.01:通过 Docker 容器的常见状态进行过渡

ping www.google.com 是验证服务器或集群节点是否连接到互联网的常见做法。在这个练习中,您将在检查服务器或集群节点是否连接到互联网的同时,经历 Docker 容器的所有状态。

在这个练习中,您将使用两个终端。一个终端将用于运行一个容器来 ping www.google.com,另一个终端将用于通过执行先前提到的命令来控制这个正在运行的容器。

要 ping www.google.com,您将从ubuntu:14.04镜像中创建一个名为testevents的容器:

  1. 打开第一个终端并执行docker container run命令来运行一个容器。使用--name选项为容器指定一个特定的昵称,例如testevents。不要让 Docker 主机为您的容器生成一个随机名称。使用ubuntu:14.04镜像和ping google.com命令来验证服务器是否在容器上运行:
$docker container run --name testevents ubuntu:14.04 ping google.com

输出将如下所示:

PING google.com (172.217.165.142) 56(84) bytes of data.
64 bytes from lax30s03-in-f14.1e100.net (172.217.165.142):
icmp_seq=1 ttl=115 time=68.9 ms
64 bytes from lax30s03-in-f14.1e100.net (172.217.165.142):
icmp_seq=2 ttl=115 time=349 ms
64 bytes from lax30s03-in-f14.1e100.net (172.217.165.142):
icmp_seq=3 ttl=115 time=170 ms

如前面的输出所示,ping 已经开始。您将发现数据包正在传输到google.com

  1. 将第一个终端用于 ping 输出。现在,通过在另一个终端中执行命令来控制此容器。在第二个终端中,执行docker container ls以列出所有正在运行的容器:
$docker container ls

查找名称为testevents的容器。状态应为Up

CONTAINER ID    IMAGE           COMMAND            CREATED
   STATUS           PORTS          NAMES
10e235033813     ubuntu:14.04   "ping google.com"  10 seconds ago
   Up 5 seconds                    testevents
  1. 现在,在第二个终端中运行docker container pause命令来暂停第一个终端中正在运行的容器:
$docker container pause testevents

您将看到 ping 已经停止,不再传输数据包。

  1. 再次使用第二个终端中的docker container ls列出正在运行的容器:
$docker container ls

如下面的输出所示,testevents的状态为Up(Paused)。这是因为您之前运行了docker container pause命令:

CONTAINER ID    IMAGE         COMMAND            CREATED
   STATUS            PORTS          NAMES
10e235033813    ubuntu:14.04  "ping google.com"  26 seconds ago
   Up 20 seconds (Paused)           testevents
  1. 在第二个终端中使用 docker container unpause 来启动暂停的容器,并使其恢复发送数据包:
$docker container unpause testevents

您会发现 ping 恢复,并且在第一个终端中传输新的数据包。

  1. 在第二个终端中,再次运行 docker container ls 命令以查看容器的当前状态:
$docker container ls

您将看到 testevents 容器的状态是 Up

CONTAINER ID    IMAGE         COMMAND            CREATED
   STATUS            PORTS          NAMES
10e235033813    ubuntu:14.04  "ping google.com"  43 seconds ago
   Up 37 seconds                    testevents
  1. 现在,运行 docker container stop 命令来停止容器:
$docker container stop testevents

您将观察到容器退出,并且在第一个终端中返回了 shell 提示符:

64 bytes from lax30s03-in-f14.1e100.net (142.250.64.110):
icmp_seq = 42 ttl=115 time=19.8 ms
64 bytes from lax30s03-in-f14.1e100.net (142.250.64.110):
icmp_seq = 43 ttl=115 time=18.7 ms
  1. 现在,在任何终端中运行 docker container ls 命令:
$docker container ls

您会发现 testevents 容器不再在列表中,因为 docker container ls 子命令只显示正在运行的容器:

CONTAINER ID      IMAGE      COMMAND     CREATED
        STATUS         PORTS                   NAMES
  1. 运行 docker container ls -a 命令来显示所有容器:
$docker container ls -a

您可以看到 testevents 容器的状态现在是 Exited

CONTAINER ID    IMAGE         COMMAND            CREATED
   STATUS            PORTS          NAMES
10e235033813    ubuntu:14.04  "ping google.com"  1 minute ago
   Exited (137) 13 seconds ago      testevents
  1. 使用 docker container start 命令来启动容器。另外,添加 -a 选项来附加本地标准输入、输出和错误流到容器,并查看其输出:
$docker container start -a testevents

如您在以下片段中所见,ping 恢复并在第一个终端中执行:

64 bytes from lax30s03-in-f14.1e100.net (142.250.64.110):
icmp_seq = 55 ttl=115 time=63.5 ms
64 bytes from lax30s03-in-f14.1e100.net (142.250.64.110):
icmp_seq = 56 ttl=115 time=22.2 ms
  1. 在第二个终端中再次运行 docker ls 命令:
$docker container ls

您将观察到 testevents 返回到列表中,其状态为 Up,并且正在运行:

CONTAINER ID    IMAGE         COMMAND            CREATED
   STATUS            PORTS          NAMES
10e235033813    ubuntu:14.04  "ping google.com"  43 seconds ago
   Up 37 seconds                    testevents
  1. 现在,使用带有 -f 选项的 rm 命令来删除 testevents 容器。 -f 选项用于强制删除容器:
$docker container rm -f testevents

第一个终端停止执行 ping 命令,第二个终端将返回容器的名称:

testevents
  1. 运行 ls -a 命令来检查容器是否正在运行:
$docker container ls -a

您将在列表中找不到 testevents 容器,因为我们刚刚从系统中删除了它。

现在,您已经看到了容器的各种状态,除了 CREATED。这是很正常的,因为通常不会看到 CREATED 状态。在每个容器内部,都有一个主进程,其 进程 ID (PID) 为 0,父进程 ID (PPID) 为 1。此进程在容器外部具有不同的 ID。当此进程被终止或移除时,容器也会被终止或移除。通常情况下,当主进程运行时,容器的状态会从 CREATED 改变为 UP,这表明容器已成功创建。如果主进程失败,容器状态不会从 CREATED 改变,这就是您要设置的内容:

  1. 运行以下命令以查看CREATED状态。使用docker container run命令从ubuntu:14.04镜像创建一个名为testcreate的容器:
$docker container run --name testcreate ubuntu:14.04 time

time命令将生成一个错误,因为ubuntu:14.04中没有这样的命令。

  1. 现在,列出正在运行的容器:
$docker container ls

您会看到列表是空的:

CONTAINER ID    IMAGE         COMMAND            CREATED
   STATUS            PORTS          NAMES
  1. 现在,通过添加-a选项列出所有的容器:
$docker container ls -a

在列表中查找名为testcreate的容器;您会观察到它的状态是Created

CONTAINER ID    IMAGE         COMMAND         CREATED
   STATUS            PORTS          NAMES
C262e6718724    ubuntu:14.04  "time"          30 seconds ago
   Created                          testcreate

如果一个容器停留在CREATED状态,这表明已经生成了一个错误,并且 Docker 无法使容器运行起来。

在这个练习中,您探索了容器的生命周期及其不同的状态。您还学会了如何使用docker container start -a <container name or ID>命令开始附加,并使用docker container rm <container name or ID>停止容器。最后,我们讨论了如何使用docker container rm -f <container name or ID>强制删除正在运行的容器。然后,我们看到了CREATED的罕见情况,只有在命令生成错误并且容器无法启动时才会显示。

到目前为止,我们已经关注容器的状态而不是其大小。在下一个练习中,我们将学习如何确定容器所占用的内存大小。

练习 7.02:检查磁盘上容器的大小

当您首次创建一个容器时,它的大小与基础镜像相同,并带有一个顶部的读/写层。随着每个添加到容器中的层,其大小都会增加。在这个练习中,您将创建一个以ubuntu:14.04作为基础镜像的容器。在其上更新并安装wget以突出状态转换对数据保留的影响:

  1. 使用-it选项运行docker container run命令以创建一个名为testsize的容器。-it选项用于在运行的容器内部运行命令时具有交互式终端:
$docker container run -it --name testsize ubuntu:14.04

提示现在会变成root@<container ID>:/#,其中容器 ID 是 Docker Engine 生成的一个数字。因此,当您在自己的机器上运行此命令时,会得到一个不同的数字。如前所述,处于容器内意味着容器将处于UP状态。

  1. 将第一个终端专用于运行的容器,并在第二个终端中执行命令。有两个终端可以避免我们分离容器来运行命令,然后重新附加到容器以在其中运行另一个命令。

现在,验证容器最初是否具有ubuntu:14.04基础镜像的大小。使用docker image ls命令在第二个终端中列出镜像。检查ubuntu:14.04镜像的大小:

$docker image ls

如下输出所示,镜像的大小为188MB

REPOSITORY     TAG      IMAGE ID         CREATED
  SIZE
ubuntu         14.04    971bb3841501     23 months ago
  188MB
  1. 现在,通过运行docker container ls -s命令来检查容器的大小:
$docker container ls -s

寻找testsize容器。您会发现大小为0B(虚拟 188MB)

CONTAINER ID    IMAGE          COMMAND      CREATED
  STATUS     PORTS    NAMES      SIZE
9f2d2d1ee3e0    ubuntu:14.04   "/bin/bash"  6 seconds ago
  Up 6 minutes        testsize   0B (virtual 188MB)

SIZE列指示容器的薄读/写层的大小,而虚拟大小指示容器中封装的所有先前层的薄读/写层的大小。因此,在这种情况下,薄层的大小为0B,虚拟大小等于镜像大小。

  1. 现在,安装wget软件包。在第一个终端中运行apt-get update命令。在 Linux 中,一般建议在安装任何软件包之前运行apt-get update以更新系统上当前软件包的最新版本:
root@9f2d2d1ee3e0: apt-get update
  1. 当容器完成更新后,运行以下命令以在基础镜像上安装wget软件包。使用-y选项自动回答所有安装问题为是。
root@9f2d2d1ee3e: apt-get install -y wget
  1. 在在ubuntu:14.04上安装wget完成后,通过在第二个终端中运行ls -s命令来重新检查容器的大小:
$docker container ls -s

如下片段所示,testsize容器的大小为27.8MB(虚拟 216MB)

CONTAINER ID    IMAGE          COMMAND      CREATED
  STATUS     PORTS    NAMES      SIZE
9f2d2d1ee3e0    ubuntu:14.04   "/bin/bash"  9 seconds ago
  Up 9 minutes        testsize   27.8MB (virtual 216MB)

现在,薄层的大小为27.8MB,虚拟大小等于所有层的大小。在这个练习中,层包括基础镜像,大小为 188MB;更新;以及wget层,大小为 27.8MB。因此,近似后总大小将为 216MB。

在这个练习中,您了解了docker container ls子命令中使用的-s选项的功能。此选项用于显示基础镜像和顶部可写层的大小。了解每个容器消耗的大小对于避免磁盘空间不足异常是有用的。此外,它可以帮助我们进行故障排除并为每个容器设置最大大小。

注意

Docker 使用存储驱动程序来写入可写层。存储驱动程序取决于您使用的操作系统。要查找更新的存储驱动程序列表,请访问 https://docs.docker.com/storage/storagedriver/select-storage-driver/。

要找出您的操作系统正在使用哪个驱动程序,请运行$docker info命令。

了解 Docker 容器生命周期事件可以为研究某些应用程序是否需要持久存储提供良好的背景,并概述了在容器明确移除之前 Docker 的默认主机存储区域(文件系统位置)。

现在,让我们深入研究有状态和无状态模式,以决定哪个容器需要持久存储。

有状态与无状态容器/服务

容器和服务可以以两种模式运行:有状态无状态。无状态服务是不保留持久数据的服务。这种类型比有状态服务更容易扩展和更新。有状态服务需要持久存储(如数据库)。因此,它更难 dockerize,因为有状态服务需要与应用程序的其他组件同步。

假设您正在处理一个需要某个文件才能正常工作的应用程序。如果这个文件保存在容器内,就像有状态模式一样,当这个容器因任何原因被移除时,整个应用程序就会崩溃。然而,如果这个文件保存在卷或外部数据库中,任何容器都可以访问它,应用程序将正常工作。假设业务蒸蒸日上,我们需要增加运行的容器数量来满足客户的需求。所有容器都可以访问该文件,并且扩展将变得简单和顺畅。

Apache 和 NGINX 是无状态服务的示例,而数据库是有状态容器的示例。Docker 卷和有状态持久性部分将重点关注数据库镜像所需的卷。

在接下来的练习中,您将首先创建一个无状态服务,然后创建一个有状态服务。两者都将使用 Docker 游乐场,这是一个网站,可以在几秒钟内提供 Docker Engine。这是一个免费的虚拟机浏览器,您可以在其中执行 Docker 命令并在集群模式下创建集群。

练习 7.03:创建和扩展无状态服务,NGINX

通常,在基于 Web 的应用程序中,有前端和后端。例如,在全景徒步应用程序中,您在前端使用 NGINX,因为它可以处理大量连接并将负载分发到后端较慢的数据库。因此,NGINX 被用作反向代理服务器和负载均衡器。

在这个练习中,您将专注于创建一个无状态服务,仅仅是 NGINX,并看看它有多容易扩展。您将初始化一个 swarm 来创建一个集群,并在其上扩展 NGINX。您将使用 Docker playground 在 swarm 模式下工作:

  1. 连接到 Docker playground,网址为 https://labs.play-with-docker.com/,如*图 7.2*所示:图 7.2:Docker playground

图 7.2:Docker playground

  1. 在左侧菜单中点击ADD NEW INSTANCE来创建一个新节点。从顶部节点信息部分获取节点 IP。现在,使用docker swarm init命令创建一个 swarm,并使用–advertise-addr选项指定节点 IP。如图 7.2所示,Docker 引擎生成一个长令牌,允许其他节点(无论是管理节点还是工作节点)加入集群:
$docker swarm init --advertise-addr <IP>
  1. 使用docker service create命令创建一个服务,并使用-p选项指定端口80。将--replicas选项的副本数设置为2,使用nginx:1.14.2镜像:
$ docker service create -p 80 --replicas 2 nginx:1.14.2

docker service create命令从容器内的nginx:1.14.2镜像创建了两个副本服务,端口为80。Docker 守护程序选择任何可用的主机端口。在这种情况下,它选择了端口30000,如图 7.2顶部所示。

  1. 验证服务是否已创建,使用docker service ls命令列出所有可用的服务:
$docker service ls

如下输出所示,Docker 守护程序自动生成了一个服务 ID,并为服务分配了一个名为amazing_hellman的名称,因为您没有使用--name选项指定一个名称:

ID            NAME             MODE        REPLICAS  IMAGE
     PORTS
xmnp23wc0m6c  amazing_hellman  replicated  2/2       nginx:1.14.2
     *:30000->80/tcp

注意

在容器中,Docker 守护程序为容器分配一个随机的形容词 _ 名词名称。

  1. 使用curl <IP:Port Number> Linux 命令来查看服务的输出并连接到它,而不使用浏览器:
$curl 192.168.0.223:3000

输出是NGINX欢迎页面的 HTML 版本。这表明它已经正确安装:

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
</body>
<h1>Welcome to nginx!<h1>
<p>If you see this page, the nginx web server is successfully 
installed and working. Further configuration is required. </p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
<html>
  1. 假设业务更加繁荣,两个副本已经不够了。您需要将其扩展到五个副本,而不是两个。使用docker service scale <service name>=<number of replicas>子命令:
$docker service scale amazing_hellman=5

您将获得以下输出:

amazing_hellman scaled to 5
overall progress: 5 out of 5 tasks
1/5: running
2/5: running
3/5: running
4/5: running
5/5: running
verify: Service converged
  1. 要验证 Docker Swarm 是否复制了服务,请再次使用docker service ls子命令:
$docker service ls

输出显示副本数量从2增加到5个副本:

ID            NAME             MODE        REPLICAS  IMAGE
     PORTS
xmnp23wc0m6c  amazing_hellman  replicated  5/5       nginx:1.14.2
     *:30000->80/tcp
  1. 使用docker service rm子命令删除服务:
$docker service rm amazing_hellman

该命令将返回服务的名称:

amazing_hellman
  1. 要验证服务是否已删除,请再次使用docker service ls子命令列出服务:
$docker service ls

输出将是一个空列表:

ID       NAME      MODE      REPLICAS      IMAGE      PORTS

在这个练习中,您部署了一个无状态服务 NGINX,并使用docker service scale命令进行了扩展。然后,您使用了 Docker playground(一个免费的解决方案,您可以使用它来创建一个集群,并使用 Swarm 来初始化一个 Swarm)。

注意

这个练习使用 Docker Swarm。要使用 Kubernetes 做同样的事情,您可以按照 https://kubernetes.io/docs/tasks/run-application/run-stateless-application-deployment/上的步骤进行操作。

现在,我们完成了 NGINX 的前端示例。在下一个练习中,您将看到如何创建一个需要持久数据的有状态服务。我们将使用数据库服务 MySQL 来完成以下练习。

练习 7.04:部署有状态服务,MySQL

如前所述,基于 Web 的应用程序有前端和后端。您已经在上一个练习中看到了前端组件的示例。在这个练习中,您将部署一个单个有状态的 MySQL 容器作为后端组件的数据库。

要安装 MySQL,请按照 https://hub.docker.com/_/mysql 中的通过 stack deploy部分的步骤进行操作。选择并复制stack.yml文件到内存:

  1. 使用编辑器粘贴stack.yml文件。您可以使用vinano Linux 命令在 Linux 中打开文本编辑器并粘贴 YAML 文件:
$vi stack.yml

粘贴以下代码:

# Use root/example as user/password credentials
version: '3.1'
services:
  db:
    image: mysql
    command: --default-authentication-plugin=      mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080

在这个 YAML 文件中,您有两个服务:dbadminerdb服务基于mysql镜像,而adminer镜像是adminer服务的基础镜像。adminer镜像是一个数据库管理工具。在db服务中,您输入命令并设置环境变量,其中包含具有始终重新启动策略的数据库密码,如果出现任何原因失败。同样,在adminer服务中,如果出现任何原因失败,策略也被设置为始终重新启动。

  1. 在键盘上按Esc键。然后,运行以下命令退出并保存代码:
:wq
  1. 要验证文件是否已正确保存,请使用cat Linux 命令显示stack.yml的内容:
$cat stack.yml

文件将被显示。如果出现错误,请重复上一步。

  1. 如果代码正确,请使用docker stack deploy子命令部署YML文件:
$docker stack deploy -c stack.yml mysql

您应该看到如下输出:

Ignoring unsupported options: restart
Creating network mysql_default
Creating service mysql_db
Creating service mysql_adminer

要连接到服务,请在 Docker playground 窗口顶部的节点 IP 旁边右键单击端口8080,并在新窗口中打开它:

图 7.3:连接到服务

图 7.3:连接到服务

  1. 使用docker stack ls子命令列出堆栈:
$docker stack ls

您应该看到如下输出:

NAME     SERVICES    ORCHESTRATOR
mysql    2           Swarm
  1. 使用docker stack rm子命令来移除堆栈:
$docker stack rm mysql

在移除堆栈时,Docker 将移除两个服务:dbadminer。它还将移除默认创建的用于连接所有服务的网络:

Removing service mysql_adminer
Removing service mysql_db
Removing network mysql_default

在这个练习中,您部署了一个有状态的服务 MySQL,并能够从浏览器访问数据库服务。同样,我们使用 Docker playground 作为执行练习的平台。

注意

复制 MySQL 并不是一件容易的事情。您不能像在练习 7.03创建和扩展无状态服务,NGINX中那样在一个数据文件夹上运行多个副本。这种方式不起作用,因为必须应用数据一致性、数据库锁定和缓存以确保您的数据正确。因此,MySQL 使用主从复制,您在主服务器上写入数据,然后数据同步到从服务器。要了解更多关于 MySQL 复制的信息,请访问 https://dev.mysql.com/doc/refman/8.0/en/replication.html。

我们已经了解到容器需要持久存储,超出容器生命周期,但还没有涵盖如何做到这一点。在下一节中,我们将学习关于卷来保存持久数据。

Docker 卷和有状态的持久性

我们可以使用卷来保存持久数据,而不依赖于容器。您可以将卷视为一个共享文件夹。在任何情况下,如果您将卷挂载到任意数量的容器中,这些容器将能够访问卷中的数据。创建卷有两种方式:

  • 通过使用docker volume create子命令,创建一个独立于任何容器之外的卷。

将卷作为容器之外的独立对象创建,可以增加数据管理的灵活性。这些类型的卷也被称为命名卷,因为您为其指定了一个名称,而不是让 Docker 引擎生成一个匿名的数字名称。命名卷会比系统中的所有容器存在更久,并保留其数据。

尽管这些卷被挂载到容器中,但即使系统中的所有容器都被删除,这些卷也不会被删除。

  • 通过在docker container run子命令中使用--mount-v--volume选项创建一个卷。Docker 会为您创建一个匿名卷。当容器被删除时,除非使用-v选项显式指示docker container rm子命令或使用docker volume rm子命令,否则卷也不会被删除。

以下练习将提供每种方法的示例。

练习 7.05:管理容器范围之外的卷并将其挂载到容器

在这个练习中,您将创建一个不受限于容器的卷。您将首先创建一个卷,将其挂载到一个容器上,并在上面保存一些数据。然后您将删除容器并列出卷,以检查即使在系统中没有容器时,卷是否仍然存在:

  1. 使用docker volume create命令创建名为vol1的卷:
$docker volume create vol1

该命令将返回卷的名称,如下所示:

vol1
  1. 使用docker volume ls命令列出所有卷:
$docker volume ls

这将导致以下输出:

DRIVER            VOLUME NAME
Local             vol1
  1. 使用以下命令检查卷以获取其挂载点:
$docker volume inspect vol1

您应该会得到以下输出:

[
    {
        "CreatedAt": "2020-06-16T16:44:13-04:00",
        "Driver": "local",
        "Labels": {},
        "Mountpoint: "/var/lib/docker/volumes/vol1/_data",
        "Name": "vol1",
        "Options": {},
        "Scope": "local"
    }
]

卷检查显示了其创建日期和时间、挂载路径、名称和范围。

  1. 将卷挂载到容器并修改其内容。添加到vol1的任何数据都将被复制到容器内的卷中:
$ docker container run -it -v vol1:/container_vol --name container1 ubuntu:14.04 bash

在上述命令中,您使用ubuntu:14.04镜像和bash命令创建了一个容器。bash命令允许您在容器内部输入命令。-it选项用于启用交互式终端。-v选项用于在主机和容器内部的container_vol之间同步数据。使用--name选项为容器命名为container1

  1. 提示会改变,表示您现在在容器内。在名为new_file.txt的文件中写入单词hello到卷中。容器内的卷称为container_vol。在这种情况下,该卷在主机和容器之间共享。从主机上,该卷称为vol1
root@acc8900e4cf1:/# echo hello > /container_vol/new_file.txt
  1. 列出卷的内容以验证文件是否已保存:
root@acc8900e4cf1:/# ls /container_vol
  1. 使用exit命令退出容器:
root@acc8900e4cf1:/# exit
  1. 通过运行以下命令检查来自主机的新文件的内容:
$ sudo ls /var/lib/docker/volumes/vol1/_data

该命令将返回新文件的名称:

new_file.txt
  1. 通过运行以下命令验证单词hello作为文件内容是否也已保存:
$ sudo cat /var/lib/docker/volumes/vol1/_data/new_file.txt
  1. 使用-v选项删除容器以删除在容器范围内创建的任何卷:
$docker container rm -v container1

该命令将返回容器的名称:

container1
  1. 通过列出所有卷来验证卷是否仍然存在:
$docker volume ls

vol1被列出,表明该卷是在容器之外创建的,即使使用-v选项,当容器被删除时也不会被删除:

DRIVER        VOLUME NAME
Local         vol1
  1. 现在,使用rm命令删除卷:
$docker volume rm vol1

该命令应返回卷的名称:

vol1
  1. 通过列出当前的卷列表来验证卷是否已被删除:
$docker volume ls

将显示一个空列表,表明卷已被删除:

DRIVER        VOLUME NAME

在这个练习中,您学会了如何在 Docker 中创建独立的卷对象,而不在容器的范围内,并且如何将这个卷挂载到容器上。卷在删除容器时没有被删除,因为卷是在容器范围之外创建的。最后,您学会了如何删除这些类型的卷。

在下一个练习中,我们将创建、管理和删除一个在容器范围内的未命名或匿名卷。

练习 7.06:管理容器范围内的卷

在运行容器之前,您不需要创建卷。Docker 会自动为您创建一个无名卷。同样,除非在docker container rm子命令中指定-v选项,否则在删除容器时不会删除卷。在这个练习中,您将在容器的范围内创建一个匿名卷,然后学习如何删除它:

  1. 使用以下命令创建一个带有匿名卷的容器:
$docker container run -itd -v /newvol --name container2 ubuntu:14.04 bash

该命令应返回一个长的十六进制数字,这是卷的 ID。

  1. 列出所有卷:
$ docker volume ls

请注意,这次 VOLUME NAME 是一个长的十六进制数字,而不是一个名称。这种类型的卷被称为匿名卷,可以通过在 docker container rm 子命令中添加 -v 选项来删除:

DRIVER     VOLUME NAME
Local      8f4087212f6537aafde7eaca4d9e4a446fe99933c3af3884d
0645b66b16fbfa4
  1. 这次删除带有卷的容器。由于它处于分离模式并在后台运行,使用 -f 选项强制删除容器。还添加 v 选项(使其为 -fv)以删除卷。如果这个卷不是匿名的,并且您为其命名了,那么它将不会被此选项删除,您必须使用 docker volume rm <volume name> 来删除它:
$docker container rm -fv container2

该命令将返回容器的名称。

  1. 验证卷已被删除。使用 docker volume ls 子命令,您会发现列表为空:
$ docker volume ls

与之前的练习相比,使用 -v 选项在删除容器时删除了卷。Docker 这次删除了卷,因为卷最初是在容器的范围内创建的。

注意

1. 如果您将卷挂载到服务而不是容器,则不能使用 -v--volume 选项。您必须使用 --mount 选项。

2. 要删除所有未在其容器被删除时删除的匿名卷,您可以使用 docker volume prune 子命令。

有关更多详细信息,请访问 https://docs.docker.com/storage/volumes/。

现在,我们将看到一些更多的例子,展示卷与有状态容器一起使用。请记住,将卷与有状态容器(如数据库)一起使用是最佳实践。容器是临时的,而数据库上的数据应该保存在持久卷中,任何新容器都可以获取并使用保存的数据。因此,卷必须被命名,您不应该让 Docker 自动生成一个带有十六进制数字作为名称的匿名卷。

在下一个练习中,您将运行一个带有卷的 PostgreSQL 数据库容器。

练习 7.07:运行带有卷的 PostgreSQL 容器

假设您在一个使用带有数据库卷的 PostgreSQL 容器的组织中工作,由于某些意外情况,容器被删除了。但是,数据仍然存在并且超出了容器的生存期。在这个练习中,您将运行一个带有数据库卷的 PostgreSQL 容器:

  1. 运行一个带有卷的 PostgreSQL 容器。将容器命名为db1。如果您本地没有该镜像,Docker 将为您拉取镜像。从postgress镜像创建一个名为db1的容器。使用-v选项将主机上的db卷与容器内的/var/lib/postgresql/data共享,并使用-e选项将 SQL 回显到标准输出流。使用POSTGRES_PASSWORD选项设置数据库密码,并使用-d选项以分离模式运行此容器:
$docker container run --name db1 -v db:/var/lib/postgresql/data -e POSTGRES_PASSWORD=password -d postgres
  1. 使用exec命令与容器进行交互,从bash中执行命令。exec命令不会创建新进程,而是用要执行的命令替换bash。在这里,提示将更改为posgres=#,表示您在db1容器内:
$ docker container exec -it db1 psql -U postgres

psql命令允许您交互式输入、编辑和执行 SQL 命令。使用-U选项输入数据库的用户名,即postgres

  1. 创建一个名为PEOPLE的表,有两列 - Nameage
CREATE TABLE PEOPLE(NAME TEXT, AGE int);
  1. PEOPLE表中插入一些值:
INSERT INTO PEOPLE VALUES('ENGY','41');
INSERT INTO PEOPLE VALUES('AREEJ','12');
  1. 验证表中的值是否正确插入:
SELECT * FROM PEOPLE;

该命令将返回两行,验证数据已正确插入:

图 7.4:SELECT 语句的输出

图 7.4:SELECT 语句的输出

  1. 退出容器以退出数据库。shell 提示将返回:
\q
  1. 使用volume ls命令验证您的卷是否是命名卷而不是匿名卷:
$ docker volume ls

您应该会得到以下输出:

DRIVER            VOLUME NAME
Local             db
  1. 使用-v选项删除db1容器:
$ docker container rm -fv db1

该命令将返回容器的名称:

db1
  1. 列出卷:
$ docker volume ls

列表显示卷仍然存在,并且未随容器一起删除:

DRIVER          VOLUME NAME
Local           db
  1. 步骤 1一样,创建一个名为db2的新容器,并挂载卷db
$docker container run --name db2 -v db:/var/lib/postgresql/data -e POSTGRES_PASSWORD=password -d postgres
  1. 运行exec命令从bash执行命令,并验证即使删除db1,数据仍然存在:
$ docker container exec -it db2 psql -U postgres
postgres=# SELECT * FROM PEOPLE;

上述命令将导致以下输出:

图 7.5:SELECT 语句的输出

图 7.5:SELECT 语句的输出

  1. 退出容器以退出数据库:
\q
  1. 现在,使用以下命令删除db2容器:
$ docker container rm -f db2

该命令将返回容器的名称:

db2
  1. 使用以下命令删除db卷:
$ docker volume rm db

该命令将返回卷的名称:

db

在这个练习中,您使用了一个命名卷来保存您的数据库以保持数据持久。您看到即使在删除容器后数据仍然持久存在。新容器能够追上并访问您在数据库中保存的数据。

在下一个练习中,您将运行一个没有卷的 PostgreSQL 数据库,以比较其效果与上一个练习的效果。

练习 7.08:运行没有卷的 PostgreSQL 容器

在这个练习中,您将运行一个默认的 PostgreSQL 容器,而不使用数据库卷。然后,您将删除容器及其匿名卷,以检查在删除容器后数据是否持久存在:

  1. 运行一个没有卷的 PostgreSQL 容器。将容器命名为db1
$ docker container run --name db1 -e POSTGRES_PASSWORD=password -d postgres
  1. 运行exec命令以执行来自bash的命令。提示将更改为posgres=#,表示您在db1容器内:
$ docker container exec -it db1 psql -U postgres
  1. 创建一个名为PEOPLE的表,其中包含两列 - NAMEAGE
CREATE TABLE PEOPlE(NAME TEXT, AGE int);
  1. PEOPLE表中插入一些值:
INSERT INTO PEOPLE VALUES('ENGY','41');
INSERT INTO PEOPLE VALUES('AREEJ','12');
  1. 验证表中的值是否已正确插入:
SELECT * FROM PEOPLE;

该命令将返回两行,从而验证数据已正确插入:

图 7.6:SELECT 语句的输出

图 7.6:SELECT 语句的输出

  1. 退出容器以退出数据库。shell 提示将返回:
\q
  1. 使用以下命令列出卷:
$ docker volume ls

Docker 已为db1容器创建了一个匿名卷,如下输出所示:

DRIVER     VOLUME NAME
Local      6fd85fbb83aa8e2169979c99d580daf2888477c654c
62284cea15f2fc62a42c32
  1. 使用以下命令删除带有匿名卷的容器:
$ docker container rm -fv db1

该命令将返回容器的名称:

db1
  1. 使用docker volume ls命令列出卷,以验证卷是否已删除:
$docker volume ls

您将观察到列表是空的:

DRIVER     VOLUME NAME

与之前的练习相反,这次练习使用了匿名卷而不是命名卷。因此,该卷在容器的范围内,并且已从容器中删除。

因此,我们可以得出结论,最佳做法是将数据库共享到命名卷上,以确保数据库中保存的数据将持久存在并超出容器的生命周期。

到目前为止,您已经学会了如何列出卷并对其进行检查。但是还有其他更强大的命令可以获取有关您的系统和 Docker 对象的信息,包括卷。这将是下一节的主题。

杂项有用的 Docker 命令

可以使用许多命令来排除故障和检查您的系统,其中一些命令如下所述:

  • 使用docker system df命令来查找系统中所有 Docker 对象的大小:
$docker system df

如下输出所示,列出了图像、容器和卷的数量及其大小:

TYPE            TOTAL     ACTIVE     SIZE      RECLAIMABLE
Images          6         2          1.261GB   47.9MB (75%)
Containers      11        2          27.78MB   27.78MB (99%)
Local Volumes   2         2          83.26MB   OB (0%)
Build Cache                          0B        0B
  • 您可以通过在docker system df命令中添加-v选项来获取有关 Docker 对象的更详细信息:
$docker system df -v

它应该返回以下类似的输出:

图 7.7:docker system df -v 命令的输出

图 7.7:docker system df -v 命令的输出

  • 运行docker volume ls子命令来列出系统上所有的卷:
$docker volume ls

复制卷的名称,以便用它来获取使用它的容器的名称:

DRIVER    VOLUME NAME
local     a7675380798d169d4d969e133f9c3c8ac17e733239330397ed
ba9e0bc05e509fc
local     db

然后,运行docker ps -a --filter volume=<Volume Name>命令来获取正在使用该卷的容器的名称:

$docker ps -a --filter volume=db

您将获得容器的详细信息,如下所示:

CONTAINER ID    IMAGE     COMMAND                 CREATED
  STATUS       PORTS         NAMES
55c60ad38164    postgres  "docker-entrypoint.s…"  2 hours ago
  Up 2 hours   5432/tcp      db_with

到目前为止,我们一直在容器和 Docker 主机之间共享卷。这种共享类型并不是 Docker 中唯一可用的类型。您还可以在容器之间共享卷。让我们在下一节中看看如何做到这一点。

持久卷和临时卷

有两种类型的卷:持久卷和临时卷。到目前为止,我们所看到的是持久卷,它们位于主机和容器之间。要在容器之间共享卷,我们使用--volumes-from选项。这种卷只存在于被容器使用时。当最后一个使用该卷的容器退出时,该卷就会消失。这种类型的卷可以从一个容器传递到下一个,但不会被保存。这些卷被称为临时卷。

卷可用于在主机和容器之间或容器之间共享日志文件。在主机上共享它们会更容易,这样即使容器因错误而被移除,我们仍然可以通过在容器移除后检查主机上的日志文件来跟踪错误。

在实际微服务应用程序中,卷的另一个常见用途是在卷上共享代码。这种做法的优势在于可以实现零停机时间。开发团队可以即时编辑代码。团队可以开始添加新功能或更改接口。Docker 会监视代码的更新,以便执行新代码。

在接下来的练习中,我们将探索数据容器,并学习一些在容器之间共享卷的新选项。

练习 7.09:在容器之间共享卷

有时,你需要一个数据容器在不同操作系统上运行的各种容器之间共享数据。在将数据发送到生产环境之前,测试在不同平台上相同的数据是很有用的。在这个练习中,你将使用数据容器,它将使用--volume-from在容器之间共享卷:

  1. 创建一个名为c1的容器,使用一个名为newvol的卷,这个卷不与主机共享:
$docker container run -v /newvol --name c1 -it ubuntu:14.04 bash
  1. 移动到newvol卷:
cd newvol/
  1. 在这个卷内保存一个文件:
echo hello > /newvol/file1.txt
  1. 按下转义序列,CTRL + P,然后CTRL + Q,这样容器就会在后台以分离模式运行。

  2. 创建第二个容器c2,使用--volumes-from选项挂载c1容器的卷:

$docker container run --name c2 --volumes-from c1 -it ubuntu:14.04 bash
  1. 验证c2能够通过ls命令访问你从c1保存的file1.txt
cd newvol/
ls
  1. c2内添加另一个文件file2.txt
echo hello2 > /newvol/file2.txt
  1. 验证c2能够通过ls命令访问你从c1保存的file1.txtfile2.txt
ls

你会看到两个文件都被列出:

file1.txt	file2.txt
  1. 将本地标准输入、输出和错误流附加到c1
docker attach c1
  1. 检查c1能够通过ls命令访问这两个文件:
ls

你会看到两个文件都被列出:

file1.txt	file2.txt
  1. 使用以下命令退出c1
exit
  1. 使用以下命令列出卷:
$ docker volume ls

你会发现即使你退出了c1,卷仍然存在:

DRIVER    VOLUME NAME
local     2d438bd751d5b7ec078e9ff84a11dbc1f11d05ed0f82257c
4e8004ecc5d93350
  1. 使用-v选项移除c1
$ docker container rm -v c1
  1. 再次列出卷:
$ docker volume ls

你会发现c1退出后卷并没有被移除,因为c2仍在使用它:

DRIVER    VOLUME NAME
local     2d438bd751d5b7ec078e9ff84a11dbc1f11d05ed0f82257c
4e8004ecc5d93350
  1. 现在,使用-v选项移除c2以及它的卷。你必须同时使用-f选项来强制移除容器,因为它正在运行中:
$ docker container rm -fv c2
  1. 再次列出卷:
$ docker volume ls

你会发现卷列表现在是空的:

DRIVER           VOLUME NAME

这证实了当使用卷的所有容器被移除时,临时卷也会被移除。

在这个练习中,你使用了--volumes-from选项在容器之间共享卷。此外,这个练习还证明了最佳实践是始终使用-v选项移除容器。只要至少有一个容器在使用该卷,Docker 就不会移除这个卷。

如果我们将c1c2中的任何一个提交为一个新镜像,那么保存在共享卷上的数据仍然不会被上传到新镜像中。即使卷在容器和主机之间共享,卷上的数据也不会被上传到新镜像中。

在下一节中,我们将看到如何使用文件系统而不是卷将这些数据刻在新提交的镜像中。

卷与文件系统和图像

请注意,卷不是镜像的一部分,因此保存在卷上的数据不会随镜像一起上传或下载。卷将被刻在镜像中,但不包括其数据。因此,如果您想在镜像中保存某些数据,请将其保存为文件,而不是卷。

下一个练习将演示并澄清在卷上保存数据和在文件上保存数据时的不同输出。

练习 7.10:在卷上保存文件并将其提交到新镜像

在这个练习中,您将运行一个带有卷的容器,在卷上保存一些数据,将容器提交到一个新的镜像,并根据这个新的镜像创建一个新的容器。当您从容器内部检查数据时,您将找不到它。数据将丢失。这个练习将演示当将容器提交到一个新的镜像时数据将如何丢失。请记住,卷上的数据不会被刻在新镜像中:

  1. 创建一个带有卷的新容器:
$docker container run --name c1 -v /newvol -it ubuntu:14.04 bash
  1. 在这个卷中保存一个文件:
echo hello > /newvol/file.txt
cd newvol
  1. 导航到newvol卷:
cd newvol
  1. 验证c1可以使用ls命令访问file.txt
ls

你会看到文件已列出:

file.txt
  1. 使用cat命令查看文件的内容:
cat file.txt

这将产生以下输出:

hello
  1. 使用以下命令退出容器:
exit
  1. 将此容器提交到名为newimage的新镜像:
$ docker container commit c1 newimage
  1. 检查镜像以验证卷是否被刻在其中:
$ docker image inspect newimage --format={{.ContainerConfig.Volumes}}

这将产生以下输出:

map[/newvol:{}]
  1. 根据您刚刚创建的newimage镜像创建一个容器:
$ docker container run -it newimage
  1. 导航到newvol并列出卷及其数据中的文件。您会发现文件和单词hello没有保存在镜像中:
cd newvol
ls
  1. 使用以下命令退出容器:
exit

从这个练习中,您了解到卷上的数据不会上传到镜像中。为了解决这个问题,请使用文件系统而不是卷。

假设单词hello是我们希望保存在镜像中的重要数据,以便我们在从这个镜像创建容器时可以访问它。您将在下一个练习中看到如何做到这一点。

练习 7.11:在新镜像文件系统中保存文件

在这个练习中,您将使用文件系统而不是卷。您将创建一个目录而不是卷,并将数据保存在这个新目录中。然后,您将提交容器到一个新的镜像中。当您使用这个镜像作为基础镜像创建一个新的容器时,您会发现容器中有这个目录和其中保存的数据:

  1. 删除之前实验中可能存在的任何容器。您可以将多个 Docker 命令连接在一起:
$ docker container rm -f $(docker container ls -aq)

该命令将返回将被移除的容器的 ID。

  1. 创建一个没有卷的新容器:
$ docker container run --name c1 -it ubuntu:14.04 bash
  1. 使用mkdir命令创建一个名为new的文件夹,并使用cd命令打开它:
mkdir new 
cd new
  1. 导航到new目录,并将单词hello保存在一个名为file.txt的新文件中:
echo hello > file.txt
  1. 使用以下命令查看文件的内容:
cat file.txt

该命令应返回hello

hello
  1. 使用以下命令退出c1
exit
  1. 将此容器提交到名为newimage的新镜像中:
$ docker container commit c1 newimage
  1. 根据您刚刚创建的newimage镜像创建一个容器:
$ docker container run -it newimage
  1. 使用ls命令列出文件:
ls

这次你会发现file.txt被保存了:

bin  boot  dev  etc  home  lib  lib64  media  mnt  new  opt
proc  root  run sbin  srv  sys  tmp  usr  var
  1. 导航到new目录,并使用ls命令验证容器是否可以访问file.txt
cd new/
ls

您会看到文件被列出:

file.txt
  1. 使用cat命令显示file.txt的内容:
cat file.txt

它将显示单词hello已保存:

hello
  1. 使用以下命令退出容器:
exit

在这个练习中,您看到当使用文件系统时数据被上传到镜像中,与我们在数据保存在卷上看到的情况相比。

在接下来的活动中,我们将看到如何将容器的状态保存在 PostgreSQL 数据库中。因此,如果容器崩溃,我们将能够追溯发生了什么。它将充当黑匣子。此外,您将在接下来的活动中使用 SQL 语句查询这些事件。

活动 7.01:将容器事件(状态)数据存储在 PostgreSQL 数据库中

在 Docker 中可以通过几种方式进行日志记录和监控。其中一种方法是使用docker logs命令,它获取单个容器内部发生的情况。另一种方法是使用docker events子命令,它实时获取 Docker 守护程序内发生的一切。这个功能非常强大,因为它监视发送到 Docker 服务器的所有对象事件,而不仅仅是容器。这些对象包括容器、镜像、卷、网络、节点等等。将这些事件存储在数据库中是有用的,因为它们可以被查询和分析以调试和排除任何错误。

在这个活动中,您将需要使用docker events --format '{{json .}}'命令将容器事件的样本输出存储到 PostgreSQL 数据库中的JSON格式中。

执行以下步骤完成此活动:

  1. 清理您的主机,删除任何 Docker 对象。

  2. 打开两个终端:一个用于查看docker events --format '{{json .}}'的效果,另一个用于控制运行的容器。

  3. docker events终端中点击Ctrl + C来终止它。

  4. 了解 JSON 输出结构。

  5. 运行 PostgreSQL 容器。

  6. 创建一个表。

  7. 从第一个终端复制docker events子命令的输出。

  8. 将这个 JSON 输出插入到 PostgreSQL 数据库中。

  9. 使用以下 SQL 查询使用 SQLSELECT语句查询 JSON 数据。

查询 1

SELECT * FROM events WHERE info ->> 'status' = 'pull';

您应该得到以下输出:

图 7.8:查询 1 的输出

图 7.8:查询 1 的输出

查询 2

SELECT * FROM events WHERE info ->> 'status' = 'destroy';

您将获得类似以下内容的输出:

图 7.9:查询 2 的输出

图 7.9:查询 2 的输出

查询 3

SELECT info ->> 'id' as id FROM events WHERE info ->> status'     = 'destroy';

最终输出应该类似于以下内容:

图 7.10:查询 3 的输出

图 7.10:查询 3 的输出

注意

此活动的解决方案可以通过此链接找到。

在下一个活动中,我们将看另一个示例,分享容器的 NGINX 日志文件,而不仅仅是它的事件。您还将学习如何在容器和主机之间共享日志文件。

活动 7.02:与主机共享 NGINX 日志文件

正如我们之前提到的,将应用程序的日志文件共享到主机是很有用的。这样,如果容器崩溃,您可以轻松地从容器外部检查其日志文件,因为您无法从容器中提取它们。这种做法对有状态和无状态容器都很有用。

在这个活动中,您将共享从 NGINX 镜像创建的无状态容器的日志文件到主机。然后,通过访问主机上的 NGINX 日志文件来验证这些文件。

步骤

  1. 请验证您的主机上是否没有/var/mylogs文件夹。

  2. 运行基于 NGINX 镜像的容器。在run命令中指定主机上和容器内共享卷的路径。在容器内,NGINX 使用/var/log/nginx路径来存储日志文件。在主机上指定路径为/var/mylogs

  3. 转到/var/mylogs路径。列出该目录中的所有文件。您应该在那里找到两个文件:

access.log       error.log

注意

此活动的解决方案可以通过此链接找到。

摘要

本章涵盖了 Docker 容器的生命周期和各种事件。它比较了有状态和无状态应用程序以及每种应用程序如何保存其数据。如果我们需要数据持久化,我们应该使用卷。本章介绍了卷的创建和管理。它进一步讨论了不同类型的卷,以及在容器提交到新镜像时卷的使用和文件系统的区别,以及两者的数据在容器提交到新镜像时受到的影响。

在下一章中,您将学习持续集成和持续交付的概念。您将学习如何集成 GitHub、Jenkins、Docker Hub 和 SonarQube,以便自动将您的图像发布到注册表,以便准备投入生产。

第八章:CI/CD 流水线

概述

在进入生产之前,本章介绍了持续集成和持续交付CI/CD)这一最关键的步骤。这是开发和生产之间的中间阶段。本章将演示 Docker 是 CI 和 CD 的强大技术,以及它如何轻松地与其他广泛使用的平台集成。在本章结束时,您将能够配置 GitHub、Jenkins 和 SonarQube,并将它们整合以自动发布您的图像以供生产使用。

介绍

在之前的章节中,您学习了如何编写docker-compose文件,并探索了服务的网络和存储。在本章中,您将学习如何集成应用程序的各种微服务并将其作为一个整体进行测试。

CI/CD代表持续集成和持续交付。有时,CD也用于持续部署。这里的部署意味着通过自动化流水线工作流从特定 URL 公开访问应用程序,而交付意味着使应用程序准备部署。在本章中,我们将重点讨论 CI/CD 的概念。

本章讨论了 Docker 如何在 CI/CD 流水线中进行逐步练习。您还将学习如何安装和运行 Jenkins 作为 Docker 容器。Jenkins 是一个开源的自动化服务器。您可以使用它来构建、测试、部署,并通过自动化软件开发的部分来促进 CI/CD。安装 Jenkins 只需一个 Docker 命令。在 Docker 上安装 Jenkins 比将其安装为应用程序更加强大,并且不会与特定操作系统紧密耦合。

注意

如果您没有 GitHub 和 Docker Hub 的帐户,请创建它们。您可以在以下链接免费创建:www.github.comhub.docker.com

什么是 CI/CD?

CI/CD 是一种帮助应用程序开发团队更频繁和可靠地向用户提供代码更改的方法。CI/CD 将自动化引入到代码部署的各个阶段中。

当多个开发人员共同协作并为同一应用程序做出贡献(每个人负责特定的微服务或修复特定的错误)时,他们使用代码版本控制提供程序使用开发人员上传和推送的最新代码版本来汇总应用程序。GitHub、Bitbucket 和 Assembla 是版本控制系统的示例。开发人员和测试人员将应用程序代码和 Docker 文件推送到自动化软件以构建、测试和部署 CI/CD 流水线。Jenkins、Circle CI 和 GitLab CI/CD 是此类自动化平台的示例。

通过测试后,将构建 Docker 镜像并发布到您的存储库。这些存储库可以是 Docker Hub、您公司的 Docker Trusted Register(DTR)或 Amazon Elastic Container Registry(ECR)。

在本章中,就像图 8.1一样,我们将使用 GitHub 存储库进行代码版本控制。然后,我们将使用 Jenkins 来构建和发布框架,并使用 Docker Hub 作为注册表。

图 8.1:CI/CD 流水线

图 8.1:CI/CD 流水线

在生产阶段之前,您必须构建 Docker 镜像,因为在生产中使用的docker-stack.yml文件中没有build关键字。然后将在集成和自动化的目标环境中将镜像部署到生产环境。在生产中,运维(或 DevOps)人员配置编排器从注册表中拉取镜像。Kubernetes、Docker Swarm 和 Google Kubernetes Engine 是可以用来从注册表中拉取镜像的生产编排器和管理服务的示例。

总之,我们有三个主要步骤:

  1. 将代码上传到 GitHub。

  2. 在 Jenkins 中创建一个项目,并输入 GitHub 和 Docker Hub 凭据。Jenkins 将自动构建镜像并将其推送到您的 Docker Hub 账户。当您将代码推送到 GitHub 时,Jenkins 会自动检测、测试和构建镜像。如果没有生成错误,Jenkins 会将镜像推送到注册表。

  3. 验证镜像是否在您的 Docker Hub 账户上。

在下一个练习中,您将安装 Jenkins 作为一个容器,用于构建镜像。Jenkins 是市场上最受欢迎的测试平台之一。Jenkins 有几种项目类型。在本章中,我们将使用 Freestyle 项目类型。

注意

请使用touch命令创建文件,使用vim命令在 vim 编辑器中处理文件。

练习 8.01:将 Jenkins 安装为一个容器

在这个练习中,您将安装 Jenkins,完成其设置,并安装初步插件。您将安装 Git 和 GitHub 插件,这些插件将在本章中使用。执行以下步骤成功地将 Jenkins 安装为一个容器:

  1. 运行以下命令拉取 Jenkins 镜像:
$docker run -d -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock jenkinsci/blueocean

这将导致类似以下的输出:

图 8.2:docker run 命令的输出

图 8.2:docker run 命令的输出

注意

Docker Hub 上有许多 Jenkins 镜像。随意拉取其中任何一个,并玩转端口和共享卷,但要注意弃用的镜像,因为 Jenkins 官方镜像现在已弃用为Jenkins/Jenkins:lts镜像。因此,请仔细阅读镜像的文档。但是,如果一个镜像不起作用,不要担心。这可能不是你的错。寻找另一个镜像,并严格按照文档的说明操作。

  1. 打开浏览器,并连接到http://localhost:8080上的 Jenkins 服务。

如果它给出一个错误消息,说明它无法连接到 Docker 守护程序,请使用以下命令将 Jenkins 添加到docker组:

$ sudo groupadd docker
$ sudo usermod –aG docker jenkins

注意

如果您的计算机操作系统是 Windows,本地主机名可能无法解析。在 Windows PowerShell 中运行ipconfig命令。在输出的第二部分中,ipconfig显示switch网络的信息。复制 IPv4 地址,并在练习中使用它,而不是本地主机名。

您还可以从控制面板 > 网络和共享中心获取 IP 地址,然后点击您的以太网或 Wi-Fi 连接的详细信息

安装完成后,Jenkins 将要求输入管理员密码来解锁它:

图 8.3:开始使用 Jenkins

图 8.3:开始使用 Jenkins

Jenkins 会为您生成一个密码,用于解锁应用程序。在下一步中,您将看到如何获取这个密码。

  1. 运行docker container ls命令以获取当前正在运行的容器列表:
$ docker container ls

您将获得从jekinsci/blueocean镜像创建的容器的详细信息:

CONTAINER ID IMAGE              COMMAND               CREATED
  STATUS              PORTS
9ed51541b036 jekinsci/blueocean "/sbin/tini../usr/.." 5 minutes ago
  Up 5 minutes        0.0.0.0:8080->8080/tcp, 5000/tcp
  1. 复制容器 ID 并运行docker logs命令:
$ docker logs 9ed51541b036

在日志文件的末尾,您将找到六行星号。密码将在它们之间。复制并粘贴到浏览器中:

图 8.4:docker logs 命令的输出

图 8.4:docker logs 命令的输出

  1. 选择“安装建议的插件”。然后,单击“跳过并继续作为管理员”。单击“保存并完成”:图 8.5:安装插件以自定义 Jenkins

图 8.5:安装插件以自定义 Jenkins

在建议的插件中,有 Git 和 GitHub 插件,Jenkins 将自动为您安装这些插件。您将需要这些插件来完成所有即将进行的练习。

注意

练习 8.04集成 Jenkins 和 Docker Hub中,您将需要安装更多插件,以便 Jenkins 可以将镜像推送到 Docker Hub 注册表。稍后将详细讨论这一点,以及如何逐步管理 Jenkins 插件的实验。

  1. 安装完成后,它将显示“Jenkins 已准备就绪!”。单击“开始使用 Jenkins”:图 8.6:设置 Jenkins

图 8.6:设置 Jenkins

  1. 单击“创建作业”以构建软件项目:图 8.7:Jenkins 的欢迎页面

图 8.7:Jenkins 的欢迎页面

前面的截图验证了您已成功在系统上安装了 Jenkins。

在接下来的章节中,我们将遵循本章的 CI/CD 流水线。第一步是将代码上传到 GitHub,然后将 Jenkins 与 GitHub 集成,以便 Jenkins 可以自动拉取代码并构建镜像。最后一步将是将 Jenkins 与注册表集成,以便将该镜像推送到注册表而无需任何手动干预。

集成 GitHub 和 Jenkins

安装 Jenkins 后,我们将创建我们的第一个作业并将其与 GitHub 集成。在本节中,就像图 8.8中一样,我们将专注于 GitHub 和 Jenkins。Docker Hub 稍后将进行讨论。

图 8.8:集成 GitHub 和 Jenkins

图 8.8:集成 GitHub 和 Jenkins

我们将使用一个简单的 Python 应用程序来统计网站点击次数。每次刷新页面,计数器都会增加,从而增加网站点击次数。

注意

Getting Started应用程序的代码文件可以在以下链接找到:github.com/efoda/hit_counter

该应用程序由四个文件组成:

  • app.py:这是 Python 应用程序代码。它使用Redis来跟踪网站点击次数的计数。

  • requirments.txt:该文件包含应用程序正常工作所需的依赖项。

  • Dockerfile:这将使用所需的库和依赖项构建图像。

  • docker-compose.yml:当两个或更多容器一起工作时,拥有 YAML 文件是必不可少的。

在这个简单的应用程序中,我们还有两个服务,WebRedis,如图 8.9所示:

图 8.9:hit_counter 应用程序架构

图 8.9:hit_counter 应用程序架构

如果您不知道如何将此应用程序上传到您的 GitHub 帐户,请不要担心。下一个练习将指导您完成此过程。

练习 8.02:将代码上传到 GitHub

您可以使用 GitHub 保存您的代码和项目。在这个练习中,您将学习如何下载和上传代码到 GitHub。您可以通过在 GitHub 网站上 fork 代码或从命令提示符推送代码来实现。在这个练习中,您将从命令提示符中执行。

执行以下步骤将代码上传到 GitHub:

  1. 在 GitHub 网站上,创建一个名为hit_counter的新空存储库。打开终端并输入以下命令来克隆代码:
$ git clone https://github.com/efoda/hit_counter

这将产生类似以下的输出:

Cloning into 'hit counter'...
remote: Enumerating objects: 38, done.
remote: Counting objects: 100% (38/38), done
remote: Compressing objects: 100% (35/35), done
remote: Total 38 (delta 16), reused 0 (delta 0), pack-reused 0
Receiving object: 100% (38/38), 8.98 KiB | 2.25 MiB/s, done.
Resolving deltas: 100% (16/16), done
  1. 通过列出目录来验证代码是否已下载到本地计算机。然后,打开应用程序目录:
$ cd hit_counter
~/hit_counter$ ls

您会发现应用程序文件已下载到您的本地计算机:

app.py docker-compose.yml Dockerfile README.md requirements.txt
  1. 初始化并配置 Git:
$ git init

您应该会得到类似以下的输出:

Reinitialized existing Git repository in 
/home/docker/hit_counter/.git/
  1. 输入您的用户名和电子邮件:
$ git config user.email "<you@example.com>"
$ git config user.name "<Your Name>"
  1. 指定 Git 帐户的名称,origindestination
$ git remote add origin https://github.com/efoda/hit_counter.git
fatal: remote origin already exists.
$ git remote add destination https://github.com/<your Github Username>/hit_counter.git
  1. 添加当前路径中的所有内容:
$ git add .

您还可以通过输入以下命令添加特定文件而不是所有文件:

$ git add <filename>.<extension>
  1. 指定一个commit消息:
$ git commit -m "first commit"

这将产生类似以下的输出:

On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
  1. 将代码推送到您的 GitHub 帐户:
$ git push -u destination master

它会要求您输入用户名和密码。一旦您登录,文件将被上传到您的 GitHub 存储库:

图 8.10:将代码推送到 GitHub

图 8.10:将代码推送到 GitHub

  1. 检查您的 GitHub 帐户。您会发现文件已经上传到那里。

现在我们已经完成了 CI/CD 管道的第一步,并且已经将代码上传到 GitHub,我们将把 GitHub 与 Jenkins 集成。

注意

从这一点开始,将 GitHub 用户名efoda替换为您的用户名。

练习 8.03:集成 GitHub 和 Jenkins

练习 8.01将 Jenkins 安装为容器中,您安装了 Jenkins 作为一个容器。在这个练习中,您将在 Jenkins 中创建一个作业,并将其配置为 GitHub。您将检查 Jenkins 的输出控制台,以验证它是否成功构建了镜像。然后,您将修改 GitHub 上的Dockerfile,并确保 Jenkins 已经检测到Dockerfile中的更改并自动重新构建了镜像:

  1. 回到浏览器中的 Jenkins。点击创建作业图 8.11:在 Jenkins 中创建作业

图 8.11:在 Jenkins 中创建作业

  1. 输入项目名称文本框中填写项目名称。点击自由风格项目,然后点击确定图 8.12:选择自由风格项目

图 8.12:选择自由风格项目

您将看到六个选项卡:常规源代码管理构建触发器构建环境构建后构建操作,就像图 8.13中那样。

  1. 常规选项卡中,选择丢弃旧构建选项,以防止旧构建占用磁盘空间。Jenkins 也会为您做好清理工作:图 8.13:选择丢弃旧构建选项

图 8.13:选择丢弃旧构建选项

  1. 源代码管理选项卡中,选择Git。在存储库 URL中,输入https://github.com/<your GitHub username>/hit_counter,就像图 8.14中那样。如果您没有 Git,请检查您的插件并下载 Git 插件。我们将在练习 8.04集成 Jenkins 和 Docker Hub中讨论管理插件的问题:图 8.14:输入 GitHub 存储库 URL

图 8.14:输入 GitHub 存储库 URL

  1. 构建触发器选项卡中,选择轮询 SCM。这是您指定 Jenkins 执行测试的频率的地方。如果您输入H/5,并在每个星号之间加上四个星号和空格,这意味着您希望 Jenkins 每分钟执行一次测试,就像图 8.16中那样。如果您输入H * * * *,这意味着轮询将每小时进行一次。如果您输入H/15 * * * *,则每 15 分钟进行一次轮询。点击文本框外部。如果您输入的代码正确,Jenkins 将显示下次执行作业的时间。否则,它将显示红色的错误。图 8.15:构建触发器

图 8.15:构建触发器

  1. 点击“构建”选项卡。点击“添加构建步骤”。选择“执行 shell”,如图 8.17所示:图 8.16:选择执行 shell

图 8.16:选择执行 shell

  1. 将显示一个文本框。写入以下命令:
docker build -t hit_counter .

然后点击“保存”,如图 8.17所示:

图 8.17:在“执行 shell 命令”框中输入 docker 构建命令

图 8.17:在“执行 shell 命令”框中输入 docker 构建命令

应该出现类似以下截图的屏幕:

图 8.18:成功创建 hit_count 项目

图 8.18:成功创建 hit_count 项目

  1. 在 Jenkins 中进一步操作之前,请检查您主机上当前拥有的镜像。在终端中运行docker images命令列出镜像:
$docker images

如果在本章之前清理了实验室,您将只有jenkinsci/blueocean镜像:

REPOSITORY           TAG     IMAGE ID      CREATED
       SIZE
jenkinsci/blueocean  latest  e287a467e019  Less than a second ago
       562MB
  1. 返回到 Jenkins。从左侧菜单中点击“立即构建”。

注意

如果连接到 Docker 守护程序时出现权限被拒绝错误,请执行以下步骤:

1. 如果尚不存在,请将 Jenkins 用户添加到 docker 主机:

$ sudo useradd jenkins

2. 将 Jenkins 用户添加到 docker 组中:

$ sudo usermod -aG docker jenkins

3. 从/etc/group获取 docker 组 ID,即998

$ sudo cat /etc/group | grep docker

4. 使用docker exec命令在运行中的 Jenkins 容器中创建一个 bash shell:

$ docker container ls

$ docker exec -it -u root <CONTAINER NAME | CONTAINER ID> /bin/bash

5. 编辑 Jenkins 容器内的/etc/group文件:

# vi /etc/group

6. 用从主机获取的 ID 替换 docker 组 ID,并将 Jenkins 用户添加到 docker 组中:

docker:x:998:jenkins

7. 保存/etc/group文件并关闭编辑器:

:wq

8. 退出 Jenkins 容器:

# exit

9. 停止 Jenkins 容器:

$ docker container ls

$ docker container stop <CONTAINER NAME | CONTAINER ID>

注意

10. 重新启动 Jenkins 容器:

$ docker container ls

$ docker container start <CONTAINER NAME | CONTAINER ID>

现在,作业将成功构建。

  1. 点击“返回仪表板”。将出现以下屏幕。在左下角,您将看到“构建队列”和“构建执行器状态”字段。您可以看到一个构建已经开始,并且旁边有#1,如图 8.19所示:图 8.19:检查构建队列

图 8.19:检查构建队列

构建尚未成功或失败。构建完成后,其状态将显示在屏幕上。过一段时间,您会发现已经进行了两次构建。

  1. 单击最后成功字段下的#2旁边的小箭头。将出现一个下拉菜单,如下图所示。选择控制台输出以检查 Jenkins 自动为我们执行的操作,如图 8.20所示:图 8.20:选择控制台输出

图 8.20:选择控制台输出

控制台输出中,您会发现 Jenkins 执行了您在项目配置期间在构建步骤中输入的docker build命令:

向下滚动到控制台输出的底部,以查看执行结果。您将看到镜像已成功构建。您还会找到镜像 ID 和标签:

图 8.21:验证镜像是否成功构建

图 8.21:验证镜像是否成功构建

  1. 从终端验证镜像 ID 和标签。重新运行docker images命令。
$docker images

您会发现已为您创建了hit_counter镜像。您还会发现python:3.7-alpine镜像,因为这是Dockerfile中的基础镜像,Jenkins 已自动拉取它:

REPOSITORY           TAG           IMAGE ID
  CREATED                      SIZE
jenkinsci/blueocean  latest        e287a467e019
  Less than a second ago       562MB
hit_counter          latest        bdaf6486f2ce
  3 minutes ago                227MB
python               3.7-alpine    6a5ca85ed89b
  2 weeks ago                  72.5MB

通过这一步,您可以确认 Jenkins 能够成功地从 GitHub 拉取文件。

  1. 现在,您将在 GitHub 代码中进行所需的更改。但首先,请验证您尚未提交任何更改到代码。返回 Jenkins,向上滚动并在顶部的左侧菜单中单击返回项目。然后单击最近的更改,如图 8.22所示:图 8.22:选择最近的更改

图 8.22:选择最近的更改

Jenkins 将显示所有构建中都没有更改,如下图所示:

图 8.23:验证代码中的更改

图 8.23:验证代码中的更改

  1. 前往 GitHub,并编辑Dockerfile,将基础镜像的标签从3.7-alpine更改为仅alpine

您也可以像以前一样从终端通过任何文本编辑器编辑文件来执行相同操作。然后运行git addgit push命令:

$ git add Dockerfile
$ git commit -m "editing the Dockerfile"
$ git push -u destination master
  1. 向下滚动并将更改提交到 GitHub。

  2. 返回 Jenkins。删除hit_counterpython:3.7-alpine镜像,以确保 Jenkins 不使用先前的本地镜像:

$ docker rmi hit_counter python:3.7-alpine
  1. 再次点击立即构建以立即开始构建作业。刷新最近更改页面。它将显示一个消息,说明发生了变化。

如果您点击发生的更改,它会将您转到 GitHub,显示旧代码和新代码之间的差异。

  1. 点击浏览器返回到 Jenkins。再次检查控制台输出,以查看 Jenkins 使用的基础镜像:

在底部,您会发现 Jenkins 成功构建了镜像。

  1. 转到终端并再次检查镜像:
$ docker images

您会发现hit_counterpython:alpine在列表上:

REPOSITORY             TAG           IMAGE ID
  CREATED                      SIZE
jenkinsci/blueocean    latest        e287a467e019
  Less than a second ago       562MB
hit_counter            latest        6288f76c1f15
  3 minutes ago                234MB
<none>                 <none>        786bdbef6ea2
  10 minutes ago               934MB
python                 alpine        8ecf5a48c789
  2 weeks ago                  78.9MB
  1. 清理您的实验室,以便进行下一个练习,删除除jenkinsci/blueocean之外的所有列出的镜像:
$ docker image rm hit_counter python:alpine 786

在这个练习中,您学会了如何将 Jenkins 与 GitHub 集成。Jenkins 能够自动从 GitHub 拉取代码并构建镜像。

在接下来的部分,您将学习如何将这个镜像推送到您的注册表,而无需手动干预,以完成您的 CI/CD 流水线。

集成 Jenkins 和 Docker Hub

在本节中,就像图 8.31中一样,我们将专注于我们的 CI/CD 流水线的最后一步,即将 Jenkins 与 Docker Hub 集成。正如我们之前提到的,市面上有很多注册表。我们将使用 Docker Hub,因为它是免费且易于使用的。在您的工作场所,您的公司可能会有一个私有的本地注册表。您需要向运维或 IT 管理员申请一个账户,并授予您一些权限,以便您能够访问注册表并将您的镜像推送到其中。

图 8.24:集成 Jenkins 和 Docker Hub

图 8.24:集成 Jenkins 和 Docker Hub

在接下来的练习中,您将学习如何将 Jenkins 与 Docker Hub 集成,以及如何推送 Jenkins 在上一个练习中构建的镜像。

练习 8.04:集成 Jenkins 和 Docker Hub

在这个练习中,您将把 Jenkins 与 Docker Hub 集成,并将该镜像推送到您的存储库。首先,您将安装Dockerdocker-build-step和“Cloudbees Docker 构建和发布”插件,以便 Jenkins 可以连接到 Docker Hub。然后,您将学习如何在 Jenkins 中输入您的 Docker Hub 凭据,以便 Jenkins 可以自动访问您的 Docker Hub 帐户并将您的镜像推送到其中。最后,您将在 Docker Hub 中检查您的镜像,以验证流水线是否正确执行。在本练习结束时,您将通过检查您的 Docker Hub 帐户来验证镜像是否成功推送到存储库:

  1. 点击左侧菜单中的“管理 Jenkins”以安装插件:图 8.25:点击“管理 Jenkins”

图 8.25:点击“管理 Jenkins”

  1. 点击“插件管理”。会出现四个选项卡。点击“可用”选项卡,然后选择Dockerdocker-build-step和“Cloudbees Docker 构建和发布”插件:图 8.26:安装 Docker、docker-build-step 和 CloudbeesDocker 构建和发布插件

图 8.26:安装 Docker、docker-build-step 和 Cloudbees Docker 构建和发布插件

  1. 点击“无需重启安装”。安装完成后,勾选“安装完成且没有作业运行时重新启动 Jenkins”。

  2. Jenkins 将花费较长时间来重新启动,具体取决于您的磁盘空间、内存和互联网连接速度。等待直到完成,并显示仪表板。点击项目的名称,即hit_count图 8.27:Jenkins 仪表板显示 hit_count 项目

图 8.27:Jenkins 仪表板显示 hit_count 项目

  1. 点击左侧菜单中的“配置”以修改项目配置:图 8.28:左侧菜单中的配置选项

图 8.28:左侧菜单中的配置选项

  1. 只修改“构建”选项卡中的详细信息。点击它,然后选择“添加构建步骤”。会出现比之前更大的菜单。如果你在菜单中看到“Docker 构建和发布”,那么说明你的插件安装成功了。点击“Docker 构建和发布”:图 8.29:从菜单中选择 Docker 构建和发布

图 8.29:从菜单中选择 Docker 构建和发布

  1. 在“注册表凭据”中,点击“添加”。然后从下拉菜单中选择Jenkins

  2. 将出现一个弹出框。输入您的 Docker Hub 用户名和密码。然后,单击“添加”:图 8.30:添加 Jenkins 凭据

图 8.30:添加 Jenkins 凭据

  1. 现在,在“注册表凭据”中,单击第一个下拉菜单,并选择您在上一步中输入的凭据。然后,在“存储库名称”字段中输入“<您的 Docker Hub 用户名>/<图像名称>”。通过单击右上角的红色X删除您在练习 8.02“将代码上传到 GitHub”中输入的“执行 Shell”选项。现在,您将只有一个构建步骤,即“Docker 构建和发布”步骤。单击“保存”以保存新配置:图 8.31:Docker 构建和发布步骤

图 8.31:Docker 构建和发布步骤

  1. 再次在左侧菜单中单击“立即构建”,并在“构建历史”选项中,跟踪图像构建的进度。它将与您在上一步中指定的“存储库名称”相同。Jenkins 将自动添加docker build步骤,因为您从插件中选择了它。如果图像成功通过构建,Jenkins 将使用您的 Docker 凭据并自动连接到 Docker Hub 或您在“存储库名称”中指定的任何注册表。最后,Jenkins 将自动将新图像推送到您的注册表,这在本练习中是您的 Docker Hub 注册表。

  2. 作为进一步检查,当图像正在构建并在完成之前,转到终端并使用docker images命令列出您拥有的图像:

$ docker images

因为您在上一练习结束时清理了实验室,所以您应该只会找到jenkinsci/blueocean图像:

REPOSITORY              TAG        IMAGE ID
  CREATED                       SIZE
jenkinsci/blueocean     latest     e287a467e019
  Less than a second ago        562MB

此外,检查您的 Docker Hub 帐户,以验证是否构建了hit_counter图像。您将找不到hit_counter图像:

图 8.32:检查您的 Docker Hub

图 8.32:检查您的 Docker Hub

  1. 如果作业成功构建,您将在图像名称旁边找到一个蓝色的球。如果是红色的球,这意味着出现了错误。现在,单击图像名称旁边的箭头,并选择“控制台输出”:图 8.33:选择控制台输出

图 8.33:选择控制台输出

如下图所示,您将发现 Jenkins 成功构建了图像并将其推送到您的 Docker Hub:

图 8.34:在控制台输出中,验证 Jenkins 是否已构建并推送了图像

图 8.34:在控制台输出中,验证 Jenkins 是否已构建并推送了镜像

  1. 返回终端并重新运行docker images命令以列出镜像:
$ docker images

您将在<your Docker Hub Username>/hit_count中找到一张图片:

REPOSITORY             TAG             IMAGE ID
  CREATED                      SIZE
jenkinsci/blueocean    latest          e287a467e019
  Less than a second ago       562MB
engyfouda/hit_count    latest          65e2179392ca
  5 minutes ago                227MB
<none>                 <none>          cf4adcf1ac88
  10 minutes ago               1.22MB
python                 3.7alpine       6a5ca85ed89b
  2 weeks ago                  72.5MB
  1. 在浏览器中刷新 Docker Hub 页面。您会在顶部找到您的镜像;Jenkins 会自动为您推送它:图 8.35:验证 Jenkins 是否已自动将镜像推送到您的 Docker Hub

图 8.35:验证 Jenkins 是否已自动将镜像推送到您的 Docker Hub

在这个练习中,我们完成了 CI/CD 管道的最后阶段,并将 Jenkins 与 Docker Hub 集成。Jenkins 将构建的镜像推送到 Docker Hub。您还通过检查 Docker Hub 账户来验证镜像是否被正确推送。

在下一个活动中,我们将应用相同的方法来安装额外的插件,将 Jenkins 与 SonarQube 集成。SonarQube 是另一个强大的工具,可以分析代码并生成关于其质量的报告,并在大量编程语言中检测错误、代码异味和安全漏洞。

活动 8.01:利用 Jenkins 和 SonarQube

通常,在提交代码给测试人员之前,您会被要求评估代码的质量。您可以利用 Jenkins 进一步检查代码,通过添加 SonarQube 插件生成关于调试错误、代码异味和安全漏洞的报告。

在这个活动中,我们将利用 Jenkins 和 SonarQube 插件来进行我们的hit_count Python 示例。

步骤

  1. 在容器中安装和运行 SonarQube,就像你在练习 8.01将 Jenkins 安装为容器中所做的那样。使用默认端口9000

  2. 在 Jenkins 中安装 SonarQube 插件。使用admin/admin登录 SonarQube 并生成身份验证令牌。不要忘记复制令牌并将其保存在文本文件中。在这一步之后,您将无法检索到令牌。如果您丢失了令牌,请删除 SonarQube 容器,像步骤 1中重新构建它,并重新执行这些步骤。

  3. 重新启动 Jenkins。

  4. 在 Jenkins 中,将 SonarQube 的身份验证令牌添加到全局凭据域中作为秘密文本。

  5. 通过调整全局系统配置配置系统选项来将 Jenkins 与 SonarQube 集成。

  6. 通过启用准备 SonarQube 扫描仪环境来修改构建环境选项卡中的字段。

  7. 修改构建步骤并添加分析属性

  8. 在浏览器中,转到 SonarQube 窗口,并检查其报告。

输出应该如下所示:

图 8.36:预期的 SonarQube 输出

图 8.36:预期的 SonarQube 输出

注意

此活动的解决方案可以通过此链接找到。

在下一个活动中,您将集成 Jenkins 和 SonarQube 与我们的全景徒步应用程序。

活动 8.02:在全景徒步应用中利用 Jenkins 和 SonarQube

全景徒步应用程序也有前端和后端,就像hit_counter应用程序一样。在这个活动中,您将在 Jenkins 中创建一个新项目,该项目链接到 GitHub 上的全景徒步应用程序。然后,您将运行 SonarQube 以获取关于其错误和安全漏洞的详细报告,如果徒步应用程序有任何问题的话。

按照以下步骤完成活动:

  1. 在 Jenkins 中创建一个名为trekking的新项目。

  2. 将其选择为FREESTYLE项目。

  3. 在“常规”选项卡中,选择“丢弃旧构建”。

  4. 在“源代码管理”中,选择GIT。然后输入 URLhttp://github.com/efoda/trekking_app

  5. 在“构建触发器”中,选择“轮询 SCM”,并将其设置为每 15 分钟进行分析和测试。

  6. 在“构建”选项卡中,输入“分析属性”代码。

  7. 保存并单击“立即构建”。

  8. 在浏览器的SonarQube选项卡中检查报告。

输出应该如下所示在 SonarQube 中:

图 8.37:活动 8.02 的预期输出

图 8.37:活动 8.02 的预期输出

注意

此活动的解决方案可以通过此链接找到。

摘要

本章提供了集成代码使用 CI/CD 流水线的实际经验。CI 帮助开发人员将代码集成到共享和易于访问的存储库中。CD 帮助开发人员将存储在存储库中的代码交付到生产环境。CI/CD 方法还有助于使产品与最新技术保持同步,并为新功能和错误修复提供快速交付的最新版本给客户。

一旦本章定义的 CI/CD 流水线的三个阶段成功完成,您只需要专注于在 GitHub 上编辑您的代码。Jenkins 将成为您的自动助手,并且将自动处理其余的阶段,并使图像可用于生产。

在下一章中,您将学习关于 Docker 集群模式以及如何执行服务发现、集群、扩展和滚动更新。

第九章:Docker Swarm

概述

在本章中,您将使用命令行与 Docker Swarm 一起工作,管理运行节点,部署服务,并在需要时对服务进行滚动更新。您将学习如何排查 Swarm 节点并使用现有的 Docker Compose 文件部署整个堆栈,以及如何使用 Swarm 来管理服务配置和秘密。本章的最后部分将为您提供使用 Swarmpit 的知识,这是一个用于运行和管理 Docker Swarm 服务和集群的基于 Web 的界面。

介绍

到目前为止,在本书中,我们已经通过直接命令(如docker run)从命令行运行了我们的 Docker 容器并控制了它们的运行方式。我们的下一步是使用 Docker Compose 自动化事务,它允许整个容器环境一起工作。Docker Swarm 是管理我们的 Docker 环境的下一步。Docker Swarm允许您编排容器的扩展和协作,以为最终用户提供更可靠的服务。

Docker Swarm 允许您设置多个运行 Docker Engine 的服务器,并将它们组织为一个集群。然后,Docker Swarm 可以运行命令来协调整个集群中的容器,而不仅仅是一个服务器。Swarm 将配置您的集群,以确保您的服务在整个集群中平衡,确保您的服务更加可靠。它还会根据集群的负载决定将哪个服务分配给哪个服务器。Docker Swarm 在管理容器运行方式方面是一个进步,并且默认情况下由 Docker 提供。

Docker Swarm 允许您为服务配置冗余和故障转移,同时根据负载增加或减少容器的数量。您可以对服务进行滚动更新,以减少停机的可能性,这意味着可以将容器应用的新版本应用到集群中,而这些更改不会导致客户停机。它将允许您通过 Swarm 编排容器工作负载,而不是手动逐个管理容器。

当涉及管理您的环境时,Swarm 还引入了一些新术语和概念,定义如下列表:

  • Swarm:多个 Docker 主机以集群模式运行,充当管理者和工作者。拥有多个节点和工作者并非 Docker Swarm 的必要部分。您可以将您的服务作为单节点集群运行,在本章中我们将使用这种方式,即使生产集群可能有多个节点可用,以确保您的服务尽可能具有容错性。

  • 任务:经理将任务分配给节点内部运行。任务包括一个 Docker 容器和将在容器内运行的命令。

  • 服务:这定义了要在管理者或工作者上执行的任务。服务和独立容器之间的区别在于,您可以修改服务的配置而无需重新启动服务。

  • 节点:运行 Docker Engine 并参与集群的个体系统是一个节点。通过虚拟化,一个物理计算机可以同时运行多个节点。

注意

我们将只在我们的系统上使用一个节点。

  • 经理:经理将任务分配给工作节点。经理进行编排和集群管理。它还在集群上托管服务。

  • 领导节点:集群中的管理节点选举一个单一的主领导节点来负责整个集群的编排任务。

  • 工作节点:工作节点执行经理节点分配的任务。

现在您熟悉了关键术语,让我们在下一节中探讨 Docker Swarm 的工作原理。

Docker Swarm 的工作原理?

集群管理节点处理集群管理,主要目标是维护集群和运行在其上的服务的一致状态。这包括确保集群始终运行,并在需要时运行和调度服务。

由于同时运行多个管理者,这意味着在生产环境中有容错能力。也就是说,如果一个管理者关闭,集群仍将有另一个管理者来协调集群上的服务。工作节点的唯一目的是运行 Docker 容器。它们需要至少一个管理者才能运行,但如果需要,工作节点可以晋升为管理者。

服务允许您将应用程序镜像部署到 Docker swarm。这些是要运行的容器和在运行容器内执行的命令。在创建服务时提供了服务选项,您可以在其中指定应用程序可以发布的端口、CPU 和内存限制、滚动更新策略以及可以运行的镜像副本数量。

服务的期望状态已经设置,并且管理节点的责任是监视服务。如果服务不处于期望的状态,它将纠正任何问题。如果一个任务失败,编排器会简单地移除与失败任务相关的容器并替换它。

现在您已经了解了 Docker Swarm 的工作原理,接下来的部分将带您开始使用基本命令,并通过一个实际操作来进一步演示其操作。

使用 Docker Swarm

本章的前一部分已经向您展示了 Swarm 使用了与您在本书中已经学到的类似概念。您将看到,使用 Swarm 会将您已经非常熟悉的 Docker 命令扩展到允许您创建集群、管理服务和配置节点。Docker Swarm 大大简化了运行服务的工作,因为 Swarm 会确定最佳放置服务的位置,负责安排容器的调度,并决定最适合放置服务的节点。例如,如果一个节点上已经运行了三个服务,而第二个节点上只有一个服务,Swarm 会知道应该均匀地分配服务到您的系统中。

默认情况下,Docker Swarm 是禁用的,因此要在 swarm 模式下运行 Docker,您需要加入现有集群或创建一个新的 swarm。要创建一个新的 swarm 并在系统中激活它,您可以使用此处显示的swarm init命令:

docker swarm init

这将在您当前工作的节点上创建一个新的单节点 swarm 集群。您的系统将成为您刚刚创建的 swarm 的管理节点。当您运行init命令时,还将提供有关允许其他节点加入您的 swarm 所需的命令的详细信息。

要加入集群的节点需要一个秘密令牌,工作节点的令牌与管理节点的不同。管理令牌需要得到强有力的保护,以免使您的集群集群变得脆弱。一旦您获得了节点需要加入的集群的令牌、IP 地址和端口,您可以运行类似于下面显示的命令,使用--token选项:

docker swarm join --token <swarm_token> <ip_address>:<port>

如果出于某种原因您需要更改令牌(可能是出于安全原因),您可以运行join-token --rotate选项来生成新的令牌,如下所示:

docker swarm join-token --rotate

从集群管理节点,以下node ls命令将允许您查看集群中可用的节点,并提供有关节点状态的详细信息,无论它是管理节点还是工作节点,以及节点是否存在任何问题:

docker node ls

一旦您的集群可用并准备好开始托管服务,您可以使用service create命令创建一个服务,提供服务的名称、容器镜像以及服务正确运行所需的命令,例如,如果您需要暴露端口或挂载卷:

docker service create --name <service> <image> <command>

然后可以对服务配置进行更改,或者您可以使用update命令更改服务的运行方式,如下所示:

docker service update <service> <changes>

最后,如果您需要删除或停止服务运行,您只需使用service remove命令:

docker service remove <service>

我们在这里提供了关于 Docker Swarm 的许多理论,希望它为您提供了清晰的了解,以及您如何使用 Swarm 来启动您的服务并在需求高时进行扩展以提供稳定的服务。以下练习将会将我们迄今为止学到的知识,并向您展示如何在您的项目中实施它。

注意

请使用touch命令创建文件,并使用vim命令在文件上使用 vim 编辑器。

练习 9.01:使用 Docker Swarm 运行服务

此练习旨在帮助您熟悉使用 Docker Swarm 命令来管理您的服务和容器。在这个练习中,您将激活一个集群,设置一个新的服务,测试扩展服务,然后使用 Docker Swarm 从集群中删除服务:

  1. 虽然 Swarm 默认包含在 Docker 安装中,但您仍然需要在系统上激活它。使用docker swarm init命令将您的本地系统置于 Docker Swarm 模式:
docker swarm init

您的输出可能与此处看到的有些不同,但如您所见,一旦创建了 swarm,输出将提供有关如何使用docker swarm join命令向集群添加额外节点的详细信息:

Swarm initialized: current node (j2qxrpf0a1yhvcax6n2ajux69) is 
now a manager.
To add a worker to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-2w0fk5g2e18118zygvmvdxartd43n0ky6cmywy0ucxj8j7net1-5v1xvrt7
1ag6ss7trl480e1k7 192.168.65.3:2377
To add a manager to this swarm, run 'docker swarm join-token 
manager' and follow the instructions.
  1. 现在使用node ls命令列出您在集群中拥有的节点:
docker node ls

您应该有一个您当前正在使用的节点,并且其状态应为Ready

ID         HOSTNAME          STATUS    AVAILABILITY
  MANAGER STATUS
j2qx.. *   docker-desktop    Ready     Active
  Leader 

为了清晰起见,我们已经从输出中删除了Engine Version列。

  1. 从您的节点上,使用docker info命令检查您的 swarm 的状态,提供有关 Swarm 集群以及节点与其交互方式的进一步详细信息。如果您需要以后排除故障,它还会为您提供额外的信息:
docker info

如您从输出中所见,您将获得有关您的 Docker Swarm 集群的所有具体细节,包括NodeIDClusterID。如果您在系统上没有正确设置 Swarm,您将只看到Swarm: inactive的输出:

…
Swarm: active
  NodeID: j2qxrpf0a1yhvcax6n2ajux69
  Is Manager: true
  ClusterID: pyejfsj9avjn595voauu9pqjv
  Managers: 1
  Nodes: 1
  Default Address Pool: 10.0.0.0/8  
  SubnetSize: 24
  Data Path Port: 4789
  Orchestration:
   Task History Retention Limit: 5
  Raft:
   Snapshot Interval: 10000
   Number of Old Snapshots to Retain: 0
   Heartbeat Tick: 1
   Election Tick: 10
  Dispatcher:
   Heartbeat Period: 5 seconds
  CA Configuration:
   Expiry Duration: 3 months
   Force Rotate: 0
  1. 在新创建的 swarm 上启动您的第一个服务。使用docker service create命令和--replicas选项创建一个名为web的服务,以设置两个容器实例运行:
docker service create --replicas 2 -p 80:80 --name web nginx

您将看到成功创建了两个实例:

uws28u6yny7ltvutq38166alf
overall progress: 2 out of 2 tasks 
1/2: running   [==========================================>] 
2/2: running   [==========================================>] 
verify: Service converged
  1. 类似于docker ps命令,您可以使用docker service ls命令查看集群上正在运行的服务的列表。执行docker service ls命令以查看在步骤 4中创建的web服务的详细信息:
docker service ls

该命令将返回web服务的详细信息:

ID              NAME  MODE          REPLICAS   IMAGE
  PORTS
uws28u6yny7l    web   replicated    2/2        nginx:latest
  *:80->80/tcp
  1. 要查看当前在您的 swarm 上运行的容器,请使用docker service ps命令并提供您的服务名称web
docker service ps web

如您所见,您现在有一个正在运行我们服务的容器列表:

ID     NAME    IMAGE    NODE               DESIRED
  CURRENT STATE
viyz   web.1   nginx    docker-desktop     Running
  Running about a minute ago
mr4u   web.2   nginx    docker-desktop     Running
  Running about a minute ago
  1. 该服务将仅运行默认的Welcome to nginx!页面。使用节点 IP 地址查看页面。在这种情况下,它将是您的本地主机 IP,0.0.0.0图 9.1:来自 Docker Swarm 的 nginx 服务

图 9.1:来自 Docker Swarm 的 nginx 服务

  1. 使用 Docker Swarm 轻松扩展运行服务的容器数量。只需提供scale选项和您想要运行的总容器数量,swarm 将为您完成工作。执行此处显示的命令,将您正在运行的 web 容器扩展到3
docker service scale web=3

以下输出显示web服务现在扩展到3个容器:

web scaled to 3
overall progress: 3 out of 3 tasks 
1/3: running   [==========================================>]
2/3: running   [==========================================>]
3/3: running   [==========================================>]
verify: Service converged
  1. 如本练习的步骤 5中所述,运行service ls命令:
docker service ls

现在你应该看到你的集群上运行了三个web服务:

ID              NAME    MODE          REPLICAS   IMAGE
    PORTS
uws28u6yny7l    web     replicated    3/3        nginx:latest
    *:80->80/tcp
  1. 以下更改更适合于具有多个节点的集群,但你可以运行它来看看会发生什么。运行以下node update命令将可用性设置为drain,并使用你的节点 ID 号或名称。这将删除在该节点上运行的所有容器,因为它在你的集群上不再可用。你将得到节点 ID 作为输出:
docker node update --availability drain j2qxrpf0a1yhvcax6n2ajux69
  1. 如果你运行docker service ps web命令,你会看到每个web服务在尝试启动新的web服务时关闭。由于你只有一个正在运行的节点,服务将处于等待状态,并显示no suitable node错误。运行docker service ps web命令:
docker service ps web

输出已经减少,只显示第二、第三、第五和第六列,但你可以看到服务无法启动。CURRENT STATE列同时具有PendingShutdown状态:

NAME         IMAGE            CURRENT STATE
  ERROR
web.1        nginx:latest     Pending 2 minutes ago
  "no suitable node (1 node…"
\_ web.1     nginx:latest     Shutdown 2 minutes ago
web.2        nginx:latest     Pending 2 minutes ago
  "no suitable node (1 node…"
\_ web.2     nginx:latest     Shutdown 2 minutes ago
web.3        nginx:latest     Pending 2 minutes ago
  "no suitable node (1 node…"
\_ web.3     nginx:latest     Shutdown 2 minutes ago
  1. 运行docker node ls命令:
docker node ls

这表明你的节点已准备就绪,但处于AVAILABILITY状态为Drain

ID         HOSTNAME          STATUS    AVAILABILITY
  MANAGER STATUS
j2qx.. *   docker-desktop    Ready     Drain
  Leader 
  1. 停止服务运行。使用service rm命令,后跟服务名称(在本例中为web)来停止服务运行:
docker service rm web

唯一显示的输出将是你要移除的服务的名称:

web
  1. 你不想让你的节点处于Drain状态,因为你希望在练习的其余部分继续使用它。要使节点退出Drain状态并准备开始管理 Swarm,使用以下命令将可用性设置为active,并使用你的节点 ID:
docker node update --availability active j2qxrpf0a1yhvcax6n2ajux69

该命令将返回节点的哈希值,对于每个用户来说都是不同的。

  1. 运行node ls命令:
docker node ls

现在它将显示我们节点的可用性为Active,并准备好再次运行你的服务:

ID         HOSTNAME          STATUS    AVAILABILITY
  MANAGER STATUS
j2qx.. *   docker-desktop    Ready     Active
  Leader 
  1. 使用docker node inspect命令和--format选项,并搜索ManagerStatus.Reachability状态,以确保你的节点是可访问的:
docker node inspect j2qxrpf0a1yhvcax6n2ajux69 --format "{{ .ManagerStatus.Reachability }}"

如果节点可用并且可以联系,你应该看到一个reachable的结果:

reachable
  1. 搜索Status.State以确保节点已准备就绪:
docker node inspect j2qxrpf0a1yhvcax6n2ajux69 --format "{{ .Status.State }}"

这应该产生ready

ready

这个练习应该让你对 Docker Swarm 如何简化你的工作有一个很好的了解,特别是当你开始考虑将你的工作部署到生产环境时。我们使用了 Docker Hub NGINX 镜像,但我们可以轻松地使用我们创建的任何服务作为 Docker 镜像,这些镜像对我们的 Swarm 节点可用。

下一节将快速讨论一些操作,如果您发现自己在 Swarm 节点出现问题时需要采取的措施。

排除 Swarm 节点问题

在本章中,我们将只使用单节点 swarm 来托管我们的服务。Docker Swarm 多年来一直提供生产级环境。然而,这并不意味着您的环境永远不会出现问题,特别是当您开始在多节点 swarm 中托管服务时。如果您需要排除集群上运行的任何节点的问题,您可以采取一些步骤来确保您正在纠正它们可能存在的任何问题:

  • 重新启动:通常最简单的选择是重新启动节点系统,以查看是否解决了您可能遇到的问题。

  • 降级节点:如果节点是您集群中的管理节点,请尝试使用node demote命令降级节点:

docker node demote <node_id>

如果此节点是领导者,它将允许其他管理节点之一成为 swarm 的领导者,并希望解决您可能遇到的任何问题。

  • 从集群中删除节点:使用node rm命令,您可以从集群中删除节点:
docker node rm <node_id>

如果节点与 swarm 的其余部分没有正确通信,这也可能是一个问题,您可能需要使用--force选项从集群中删除节点:

docker node rm --force <node_id>
  • 重新加入集群:如果前面的操作正确执行,您可以使用swarm join命令成功将节点重新加入集群。记得使用加入 swarm 时使用的令牌:
docker node swarm join --token <token> <swarm_ip>:<port>

注意

如果您的服务在 Docker Swarm 上仍然存在问题,并且您已经纠正了所有与 Swarm 节点相关的问题,Swarm 只是使用 Docker 在您的环境中运行和部署服务。任何问题可能归结为对您尝试在 Swarm 上运行的容器映像的基本故障排除,而不是 Swarm 环境本身。

一组管理节点被称为仲裁,大多数管理节点需要就提议更新 swarm 达成一致意见,例如添加新节点或缩减容器数量。正如我们在前一节中看到的,您可以通过运行docker node ls命令来监视 swarm 管理节点或节点的健康状况,然后使用管理节点的 ID 来使用docker node inspect命令,如下所示:

docker node inspect <node_id>

注意

关于您的 Swarm 节点的最后一点是要记住将服务部署到已创建为 Docker 镜像的节点上。容器镜像本身需要从中央 Docker 注册表下载,该注册表可供所有节点从中下载,而不仅仅是在 Swarm 节点上构建。

虽然我们已经快速讨论了解决 Swarm 节点故障的方法,但这不应该是在 Swarm 上运行服务的主要方面。本章的下一部分将进一步向前迈进,向您展示如何使用新的或现有的docker-compose.yml文件来自动部署您的服务到 Docker Swarm 中。

使用 Docker Compose 部署 Swarm 部署

使用 Docker Swarm 部署完整环境很容易;如果您一直在使用 Docker Compose 运行容器,您会发现大部分工作已经完成。这意味着您不需要像我们在本章的前一部分中那样手动逐个启动 Swarm 中的服务。

如果您已经有一个可用的docker-compose.yml文件来启动您的服务和应用程序,那么它很可能会在没有问题的情况下简单地工作。Swarm 将使用stack deploy命令在 Swarm 节点上部署所有您的服务。您只需要提供compose文件并为堆栈分配一个名称:

docker stack deploy --compose-file <compose_file> <swarm_name>

堆栈创建快速而无缝,但在后台会发生很多事情,以确保所有服务都正常运行,包括在所有服务之间设置网络,并按需要的顺序启动每个服务。使用在创建时提供的swarm_name运行stack ps命令将向您显示部署中所有服务是否正在运行:

docker stack ps <swarm_name>

一旦您完成了在您的 swarm 上使用服务,或者您需要清理部署的所有内容,您只需使用stack rm命令,提供您在创建堆栈部署时提供的swarm_name。这将自动停止和清理在您的 swarm 中运行的所有服务,并准备好让您重新分配给其他服务:

docker stack rm <swarm_name>

现在,既然我们知道了用于部署、运行和管理我们的 Swarm 堆栈的命令,我们可以看看如何为我们的服务执行滚动更新。

Swarm 服务滚动更新

Swarm 还具有对正在运行的服务执行滚动更新的能力。这意味着如果您对 Swarm 上运行的应用程序有新的更新,您可以创建一个新的 Docker 镜像并更新您的服务,Swarm 将确保新镜像在成功运行之前将旧版本的容器镜像关闭。

在 Swarm 中对正在运行的服务执行滚动更新只是运行service update命令的简单问题。在以下命令中,您可以看到新的容器镜像名称和要更新的服务。Swarm 将处理其余部分。

docker service update --image <image_name:tag> <service_name>

您很快就有机会使用我们在这里解释过的所有命令。在以下示例中,您将使用 Django 和 PostgreSQL 创建一个小型测试应用程序。您将要设置的 Web 应用程序非常基本,因此没有必要事先了解 Django Web 框架。只需跟着做,我们将在练习中逐步解释发生的事情。

练习 9.02:从 Docker Compose 部署您的 Swarm

在以下练习中,您将使用docker-compose.yml创建一个使用 PostgreSQL 数据库和 Django Web 框架的基本 Web 应用程序。然后,您将使用此compose文件将服务部署到 Swarm 中,而无需手动运行服务。

  1. 首先,创建一个目录来运行您的应用程序。将目录命名为swarm,并使用cd命令进入该目录。
mkdir swarm; cd swarm
  1. 在新目录中为您的 Django 应用程序创建一个Dockerfile,并使用文本编辑器输入以下代码块中的详细信息。Dockerfile将使用默认的Python3镜像,设置与 Django 相关的环境变量,安装相关应用程序,并将代码复制到容器镜像的当前目录中。
FROM python:3
ENV PYTHONUNBUFFERED 1
RUN mkdir /application
WORKDIR /application
COPY requirements.txt /application/
RUN pip install -r requirements.txt
COPY . /application/
  1. 创建requirements.txt文件,您的Dockerfile在上一步中使用它来安装运行所需的所有相关应用程序。使用文本编辑器添加以下两行以安装 Django 应用程序与 PostgreSQL 数据库通信所需的DjangoPsycopg2版本。
1 Django>=2.0,<3.0
2 psycopg2>=2.7,<3.0
  1. 使用文本编辑器创建一个docker-compose.yml文件。根据以下代码添加第一个数据库服务。db服务将使用 Docker Hub 上的最新postgres镜像,公开端口5432,并设置POSTGRES_PASSWORD环境变量。
1 version: '3.3'
2
3 services:
4   db:
5     image: postgres
6     ports:
7       - 5432:5432
8     environment:
9       - POSTGRES_PASSWORD=docker
  1. docker-compose.yml文件的后半部分构建和部署您的 web 应用程序。在第 10 行中构建您的Dockerfile,将端口8000暴露出来,以便从 Web 浏览器访问,并将数据库密码设置为与您的db服务匹配。您还会注意到第 13 行中的 Python 命令,它将启动 Django 应用程序的开发 Web 服务器:
10   web:
11     build: .
12     image: swarm_web:latest
13     command: python manage.py runserver 0.0.0.0:8000
14     volumes:
15       - .:/application
16     ports:
17       - 8000:8000
18     environment:
19       - PGPASSWORD=docker
20     depends_on:
21       - db
  1. 运行以下命令来拉取和构建docker-compose.yml中的dbweb服务。然后该命令将运行django-admin startproject,这将创建您的基本 Django 项目,名为chapter_nine
docker-compose run web django-admin startproject chapter_nine .

该命令应返回以下输出,其中您可以看到正在拉取和构建的容器:

…
Status: Downloaded newer image for postgres:latest
Creating swarm_db_1 ... done
Building web
…
Successfully built 41ff06e17fe2
Successfully tagged swarm_web:latest
  1. 在上一步中运行的startproject命令应该在您的 swarm 目录中创建了一些额外的文件和目录。运行ls命令列出 swarm 目录中的所有文件和目录:
ls -l

您之前创建了Dockerfiledocker-compose.yml文件和requirements.txt文件,但现在容器的构建已经添加了chapter_nine Django 目录和manage.py文件:

-rw-r--r--  1 user  staff  175  3 Mar 13:45 Dockerfile
drwxr-xr-x  6 user  staff  192  3 Mar 13:48 chapter_nine
-rw-r--r--  1 user  staff  304  3 Mar 13:46 docker-compose.yml
-rwxr-xr-x  1 user  staff  634  3 Mar 13:48 manage.py
-rw-r--r--  1 user  staff   36  3 Mar 13:46 requirements.txt
  1. 要使您的基本应用程序运行,您需要对 Django 项目设置进行一些微小的更改。用文本编辑器打开chapter_nine/settings.py文件,并找到以DATABASES开头的条目。这控制 Django 如何连接到您的数据库,默认情况下,Django 设置为使用 SQLite 数据库。DATABASES条目应如下所示:
76 DATABASES = {
77     'default': {
78         'ENGINE': 'django.db.backends.sqlite3',
79         'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
80     }
81 }

您有一个要部署到 Swarm 的 PostgreSQL 数据库作为我们安装的一部分,因此使用以下八行编辑DATABASES设置,以便 Django 将访问此 PostgreSQL 数据库:

settings.py

76 DATABASES = {
77     'default': {
78         'ENGINE': 'django.db.backends.postgresql',
79         'NAME': 'postgres',
80         'USER': 'postgres',
81         'PASSWORD': 'docker',
82         'HOST': 'db',
83         'PORT': 5432,
84     }
85 }

此步骤的完整代码可在packt.live/2DWP9ov找到。

  1. 在我们的settings.py文件的第 28 行,我们还需要添加我们将用作ALLOWED_HOSTS配置的 IP 地址。我们将配置我们的应用程序可以从 IP 地址0.0.0.0访问。对设置文件进行相关更改,使其在第 28 行看起来像下面的代码:
 27 
 28 ALLOWED_HOSTS = ["0.0.0.0"]
  1. 现在测试一下,看看您的基本项目是否按预期工作。从命令行,使用stack deploy命令将您的服务部署到 Swarm。在以下命令中,使用--compose-file选项指定要使用的docker-compose.yml文件,并命名堆栈为test_swarm
docker stack deploy --compose-file docker-compose.yml test_swarm

该命令应该设置 swarm 网络、数据库和 web 服务:

Creating network test_swarm_default
Creating service test_swarm_db
Creating service test_swarm_web
  1. 运行docker service ls命令,您应该能够看到test_swarm_dbtest_swarm_web服务的状态:
docker service ls

如下输出所示,它们都显示了REPLICAS值为1/1

ID     NAME            MODE        REPLICAS  IMAGE
  PORTS
dsr.   test_swarm_db   replicated  1/1       postgres
kq3\.   test_swarm_web  replicated  1/1       swarm_web:latest
  *:8000.
  1. 如果您的工作成功,可以通过打开 Web 浏览器并转到http://0.0.0.0:8000来进行测试。如果一切正常,您应该在 Web 浏览器上看到以下 Django 测试页面显示:图 9.2:使用 Docker Compose 文件将服务部署到 Swarm

图 9.2:使用 Docker Compose 文件将服务部署到 Swarm

  1. 要查看当前在您的系统上运行的堆栈,请使用stack ls命令:
docker stack ls

您应该看到以下输出,显示了两个以test_swarm名称运行的服务:

NAME                SERVICES            ORCHESTRATOR
test_swarm          2                   Swarm
  1. 使用您的 swarm 名称运行stack ps命令,查看正在运行的服务并检查是否存在任何问题:
docker stack ps test_swarm

IDDESIRED STATEERROR列未包含在以下精简输出中。还可以看到test_swarm_web.1test_swarm_db.1服务正在运行:

NAME                IMAGE               NODE
  CURRENT STATE
test_swarm_web.1    swarm_web:latest    docker-desktop
  Running
test_swarm_db.1     postgres:latest     docker-desktop
  Running
  1. 就像您可以使用deploy命令一次启动所有服务一样,您也可以一次停止所有服务。使用stack rm命令加上您的 swarm 名称来停止所有正在运行的服务并移除堆栈:
docker stack rm test_swarm

请注意,以下输出中所有服务都已停止:

Removing service test_swarm_db
Removing service test_swarm_web
Removing network test_swarm_default
  1. 作为本练习的一部分,您仍然希望在 swarm 上执行一些额外的工作,但首先对compose文件进行一些微小的更改。使用文本编辑器打开docker-compose.yml文件,并向您的 web 服务添加以下行,以便在部署到 swarm 时创建两个副本 web 服务:
22     deploy:
23       replicas: 2

完整的docker-compose.yml文件应该如下所示:

version: '3.3'
services:
  db:
    image: postgres
    ports:
      - 5432:5432
    environment:
      - POSTGRES_PASSWORD=docker
  web:
    build: .
    image: swarm_web:latest
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/application
    ports:
      - 8000:8000
    environment:
      - PGPASSWORD=docker
    deploy:
      replicas: 2
    depends_on:
      - db
  1. 使用相同的命令再次部署 swarm,就像在步骤 8中所做的那样。即使test_swarm堆栈仍在运行,它也会注意并对服务进行相关更改:
docker stack deploy --compose-file docker-compose.yml test_swarm
  1. 运行以下docker ps命令:
docker ps | awk '{print $1 "\t" $2 }'

这里显示的输出中只打印了前两列。现在您可以看到有两个swarm_web服务正在运行:

CONTAINER         ID
2f6eb92414e6      swarm_web:latest
e9241c352e12      swarm_web:latest
d5e6ece8a9bf      postgres:latest
  1. 要在不停止服务的情况下将swarm_web服务的新版本部署到您的 swarm 中,首先构建我们 Web 服务的新 Docker 镜像。不要对图像进行任何更改,但是这次使用patch1标签标记图像以演示在服务运行时的更改:
docker build . -t swarm_web:patch1
  1. 要执行滚动更新,请使用service update命令,提供要更新到的图像的详细信息和服务名称。运行以下命令,该命令使用您刚刚创建的带有patch1标签的图像,在test_swarm_web服务上:
docker service update --image swarm_web:patch1 test_swarm_web

Swarm 将管理更新,以确保在将更新应用于其余图像之前,其中一个服务始终在运行:

image swarm_web:patch1 could not be accessed on a registry 
to record its digest. Each node will access 
swarm_web:patch1 independently, possibly leading to different 
nodes running different versions of the image.
test_swarm_web
overall progress: 2 out of 2 tasks 
1/2: running   [=========================================>]
2/2: running   [=========================================>]
verify: Service converged

注意

您会注意到输出显示图像在存储库中不可用。由于我们只有一个运行我们的 swarm 的节点,因此更新将使用在节点上构建的图像。在现实世界的情况下,我们需要将此图像推送到所有我们的节点都可以访问的中央存储库,以便它们可以拉取它。

  1. 运行此处给出的docker ps命令,将其输出传输到awk命令,以仅打印CONTAINERID的前两列:
docker ps | awk '{print $1 "\t" $2 }'

该命令将返回以下输出:

CONTAINER         ID
ef4107b35e09      swarm_web:patch1
d3b03d8219dd      swarm_web:patch1
d5e6ece8a9bf      postgres:latest
  1. 如果您想要控制滚动更新的方式怎么办?运行以下命令对test_swarm_web服务执行新的滚动更新。撤消对使用latest标签部署图像所做的更改,但是这次确保在执行更新时有30秒的延迟,这将给您的 Web 服务额外的时间在第二次更新运行之前启动:
docker service update --update-delay 30s --image swarm_web:latest test_swarm_web
  1. 再次运行docker ps命令:
docker ps | awk '{print $1 "\t" $2 }'

请注意,在执行滚动更新后,容器现在再次运行swarm_web:latest图像:

CONTAINER         ID
414e62f6eb92      swarm_web:latest
352e12e9241c      swarm_web:latest
d5e6ece8a9bf      postgres:latest

到目前为止,您应该看到使用 swarm 的好处,特别是当我们开始使用 Docker Compose 扩展我们的应用程序时。在这个练习中,我们演示了如何使用 Docker Compose 轻松部署和管理一组服务到您的 swarm,并使用滚动更新升级服务。

本章的下一部分将进一步扩展您的知识,以展示您如何使用 Swarm 来管理您环境中使用的配置和秘密值。

使用 Docker Swarm 管理秘密和配置

到目前为止,在本章中,我们已经观察到 Docker Swarm 在编排我们的服务和应用方面的熟练程度。它还提供了功能,允许我们在环境中定义配置,然后使用这些值。但是,我们为什么需要这个功能呢?

首先,我们一直以来存储诸如 secrets 之类的细节并不是非常安全,特别是当我们在docker-compose.yml文件中以明文输入它们,或者将它们作为构建的 Docker 镜像的一部分包含在其中。对于我们的 secrets,Swarm 允许我们存储加密值,然后由我们的服务使用。

其次,通过使用这些功能,我们可以开始摆脱在Dockerfile中设置配置的方式。这意味着我们可以创建和构建我们的应用作为一个容器镜像。然后,我们可以在任何环境中运行我们的应用,无论是笔记本上的开发系统还是测试环境。我们还可以在生产环境中运行应用,为其分配一个单独的配置或 secrets 值在该环境中使用。

创建一个 Swarm config很简单,特别是如果你已经有一个现有的文件可以使用。以下代码展示了我们如何使用config create命令创建一个新的config,并提供我们的config_nameconfiguration_file的名称:

docker config create <config_name> <configuration_file> 

这个命令创建了一个作为 Swarm 一部分存储的config,并且可以在集群中的所有节点上使用。要查看系统和 Swarm 上可用的配置,可以使用config命令的ls选项运行:

docker config ls

您还可以使用config inspect命令查看配置的详细信息。确保使用--pretty选项,因为输出以长 JSON 格式呈现,如果没有该选项,几乎无法阅读:

docker config inspect --pretty <config_name>

在 Swarm 中使用 secrets 提供了一种安全的方式来创建和存储环境中的敏感信息,比如用户名和密码,以加密的方式存储,然后可以被我们的服务使用。

要创建一个只包含单个值的 secret,比如用户名或密码,我们可以简单地从命令行创建 secret,将 secret 值传递到secret create命令中。以下示例命令提供了如何做到这一点的示例。记得在创建时给 secret 命名:

echo "<secret_password>" | docker secret create <secret_name> –

您可以从文件中创建一个秘密。例如,假设您想将证书文件设置为一个秘密。以下命令显示如何使用secret create命令来创建秘密,提供秘密的名称和您需要从中创建秘密的文件的名称:

docker secret create <secret_name> <secret_file> 

创建后,您的秘密将在您的集群上运行的所有节点上都可用。就像您能够查看您的config一样,您可以使用secret ls命令来查看集群中所有可用秘密的列表:

docker secret ls

我们可以看到,Swarm 为我们提供了灵活的选项,在我们的编排中实现配置和秘密,而不需要将其设置为我们的 Docker 镜像的一部分。

以下练习将演示如何在当前的 Docker Swarm 环境中同时使用配置和秘密。

练习 9.03:在您的集群中实现配置和秘密

在这个练习中,您将进一步扩展您的 Docker Swarm 环境。您将向您的环境添加一个服务,该服务将帮助 NGINX 通过代理路由请求,然后进入您的 Web 服务。您将使用传统方法设置这一点,然后使用configsecret函数作为您的环境的一部分来观察它们在 Swarm 中的操作,并帮助用户更有效地部署和配置服务:

  1. 目前,Web 服务正在使用 Django 开发 Web 服务器通过runserver命令来处理 Web 请求。NGINX 将无法将流量请求路由到这个开发服务器,而是需要将gunicorn应用程序安装到我们的 Django Web 服务上,以便通过 NGINX 路由流量。首先打开您的requirements.txt文件,使用文本编辑器添加应用程序,如下所示的第三行:
Django>=2.0,<3.0
psycopg2>=2.7,<3.0
gunicorn==19.9.0

注意

Gunicorn 是Green Unicorn的缩写,用作 Python 应用程序的Web 服务网关接口WSGI)。Gunicorn 被广泛用于生产环境,因为它被认为是最稳定的 WSGI 应用程序之一。

  1. 要将 Gunicorn 作为您的 Web 应用程序的一部分运行,请调整您的docker-compose.yml文件。使用文本编辑器打开docker-compose.yml文件,并将第 13 行更改为运行gunicorn应用程序,而不是 Django 的manage.py runserver命令。以下gunicorn命令通过其 WSGI 服务运行chapter_nine Django 项目,并绑定到 IP 地址和端口0.0.0.0:8000
12     image: swarm_web:latest
13     command: gunicorn chapter_nine.wsgi:application          --bind 0.0.0.0:8000
14     volumes:
  1. 重新构建您的 web 服务,以确保 Gunicorn 应用程序已安装在容器上并可运行。运行docker-compose build命令:
docker-compose build
  1. Gunicorn 也可以在没有 NGINX 代理的情况下运行,因此通过再次运行stack deploy命令来测试您所做的更改。如果您已经部署了服务,不用担心,您仍然可以再次运行此命令。它将简单地对您的 swarm 进行相关更改,并匹配您的docker-compose.yml中的更改:
docker stack deploy --compose-file docker-compose.yml test_swarm

该命令将返回以下输出:

Ignoring unsupported options: build
Creating network test_swarm_default
Creating service test_swarm_web
Creating service test_swarm_db
  1. 为确保更改已生效,请确保打开您的 web 浏览器,并验证 Django 测试页面仍然由您的 web 服务提供,然后再进行下一步。根据您的更改,页面应该仍然显示在http://0.0.0.0:8000

  2. 要启动 NGINX 的实现,请再次打开docker-compose.yml文件,并将第 16 行和第 17 行更改为从原始ports命令中暴露端口8000

10   web:
11     build: .
12     image: swarm_web:latest
13     command: gunicorn chapter_nine.wsgi:application          --bind 0.0.0.0:8000
14     volumes:
15       - .:/application
16     ports:
17       - 8000:8000
18     environment:
19       - PGPASSWORD=docker
20     deploy:
21       replicas: 2
22     depends_on:
23       - db
  1. 保持docker-compose.yml文件打开,将您的nginx服务添加到compose文件的末尾。现在,这里的所有信息对您来说应该都很熟悉。第 25 行提供了一个新的 NGINX 目录的位置,您将很快创建的Dockerfile,以及服务部署时要使用的镜像的名称。第 27 行第 28 行将端口1337映射到端口80第 29 行第 30 行显示 NGINX 需要依赖web服务才能运行:
24   nginx:
25     build: ./nginx
26     image: swarm_nginx:latest
27     ports:
28       - 1337:80
29     depends_on:
30       - web
  1. 现在,为服务设置 NGINX Dockerfile和配置。首先创建一个名为nginx的目录,如下命令所示:
mkdir nginx
  1. nginx目录中创建一个新的Dockerfile,用文本编辑器打开文件,并添加这里显示的细节。Dockerfile是从 Docker Hub 上可用的最新nginx镜像创建的。它删除了第 3 行中的默认配置nginx文件,然后添加了一个您需要很快设置的新配置:
FROM nginx
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
  1. 创建nginx.conf文件,Dockerfile将使用它来创建您的新镜像。在nginx目录中创建一个名为nginx.conf的新文件,并使用文本编辑器添加以下配置细节:
upstream chapter_nine {
    server web:8000;
}
server {
    listen 80;
    location / {
        proxy_pass http://chapter_nine;
        proxy_set_header X-Forwarded-For             $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }
}

如果您对 NGINX 配置不熟悉,上述细节只是在寻找对 web 服务的请求,并将请求路由到chapter_nine Django 应用程序。

  1. 现在所有细节都已就绪,请为在您的docker-compose.yml文件中设置的 NGINX 服务构建新的映像。运行以下命令构建映像:
docker-compose build
  1. 再次运行stack deploy命令:
docker stack deploy --compose-file docker-compose.yml test_swarm

这次,您会注意到您的输出显示test_swarm_nginx服务已被创建并应该正在运行:

Creating network test_swarm_default
Creating service test_swarm_db
Creating service test_swarm_web
Creating service test_swarm_nginx
  1. 使用stack ps命令验证所有服务是否作为 swarm 的一部分运行:
docker stack ps test_swarm

结果输出已减少,仅显示了八列中的四列。您可以看到test_swarm_nginx服务现在正在运行:

NAME                  IMAGE                 NODE
  DESIRED STATE
test_swarm_nginx.1    swarm_nginx:latest    docker-desktop
  Running
test_swarm_web.1      swarm_web:latest      docker-desktop
  Running
test_swarm_db.1       postgres:latest       docker-desktop
  Running
test_swarm_web.2      swarm_web:latest      docker-desktop
  Running
  1. 为了证明请求正在通过 NGINX 代理路由,请使用端口1337而不是端口8000。确保仍然可以从您的 Web 浏览器中使用新的 URL http://0.0.0.0:1337提供网页。

  2. 这是对在 Swarm 上运行的服务的一个很好的补充,但它没有使用正确的配置管理功能。您之前在此练习中已经创建了一个 NGINX 配置。使用config create命令和新配置的名称以及要创建配置的文件来创建一个 Swarm 配置。运行以下命令从您的nginx/nginx.conf文件创建新配置:

docker config create nginx_config nginx/nginx.conf 

该命令的输出将为您提供创建的配置 ID:

u125x6f6lhv1x6u0aemlt5w2i
  1. Swarm 还提供了一种列出作为 Swarm 一部分创建的所有配置的方法,使用config ls命令。确保在上一步中已创建新的nginx_config文件,并运行以下命令:
docker config ls

nginx_config已在以下输出中创建:

ID           NAME           CREATED           UPDATED
u125x6f6…    nginx_config   19 seconds ago    19 seconds ago
  1. 使用docker config inspect命令查看您创建的配置的完整细节。运行以下命令并使用--pretty选项,以确保配置输出以可读形式显示:
docker config inspect --pretty nginx_config

输出应该看起来类似于您在这里看到的内容,显示了您刚刚创建的 NGINX 配置的细节:

ID:             u125x6f6lhv1x6u0aemlt5w2i
Name:           nginx_config
Created at:          2020-03-04 19:55:52.168746807 +0000 utc
Updated at:          2020-03-04 19:55:52.168746807 +0000 utc
Data:
upstream chapter_nine {
    server web:8000;
}
server {
    listen 80;
    location / {
        proxy_pass http://chapter_nine;
        proxy_set_header X-Forwarded-For             $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }
}
  1. 由于您现在已经在 Swarm 中设置了配置,请确保配置不再内置到容器映像中。相反,它将在部署 Swarm 时提供。打开nginx目录中的Dockerfile并删除Dockerfile的第四行。现在它应该看起来类似于这里给出的细节:
FROM nginx:1.17.4-alpine
RUN rm /etc/nginx/conf.d/default.conf

注意

请记住,我们在这里所做的更改将确保我们不需要在配置更改时每次构建新的 NGINX 镜像。这意味着我们可以使用相同的镜像并将其部署到开发 Swarm 或生产 Swarm。我们所要做的就是更改配置以适应环境。但是,我们确实需要创建可以使用我们在 Swarm 中创建和存储的配置的镜像。

  1. 在这个练习的上一步中,对nginxDockerfile进行了更改,现在重新构建镜像以确保其是最新的:
docker-compose build
  1. 用文本编辑器打开docker-compose.yml文件,更新compose文件,以便我们的nginx服务现在将使用新创建的 Swarmconfig。在nginx服务的底部,添加配置细节,使用你之前创建的nginx_cof配置的源名称。确保将其添加到运行的nginx服务中,以便容器可以使用它。然后,为文件设置一个单独的配置。即使你在之前的步骤中手动创建了它,当部署时你的 Swarm 也需要知道它。将以下内容添加到你的docker-compose.yml中:
25   nginx:
26     build: ./nginx
27     image: swarm_nginx:latest
28     ports:
29       - 1337:80
30     depends_on:
31       - web
32     configs:
33       - source: nginx_conf
34         target: /etc/nginx/conf.d/nginx.conf
35 
36 configs:
37   nginx_conf:
38     file: nginx/nginx.conf
  1. 再次部署你的 Swarm:
docker stack deploy --compose-file docker-compose.yml test_swarm

在下面的输出中,你现在应该看到一个额外的行,显示Creating config test_swarm_nginx_conf

Creating network test_swarm_default
Creating config test_swarm_nginx_conf
Creating service test_swarm_db
Creating service test_swarm_web
Creating service test_swarm_nginx
  1. 还有更多你可以做来利用 Swarm,一个尚未使用的额外功能是秘密功能。就像你在这个练习中之前创建配置一样,你可以使用类似的命令创建一个secret。这里显示的命令首先使用echo来输出你想要作为秘密值的密码,然后使用secret create命令,它使用这个输出来创建名为pg_password的秘密。运行以下命令来命名你的新秘密pg_password
echo "docker" | docker secret create pg_password –

该命令将输出创建的秘密的 ID:

4i1cwxst1j9qoh2e6uq5fjb8c
  1. 使用secret ls命令查看你的 Swarm 中的秘密。现在运行这个命令:
docker secret ls

你可以看到你的秘密已成功创建,名称为pg_password

ID                          NAME           CREATED
  UPDATED
4i1cwxst1j9qoh2e6uq5fjb8c   pg_password    51 seconds ago
  51 seconds ago
  1. 现在,对您的docker-compose.yml文件进行相关更改。以前,您只需输入您想要为您的postgres用户设置的密码。如下面的代码所示,在这里,您将把环境变量指向您之前创建的秘密,作为/run/secrets/pg_password。这意味着它将搜索您的 Swarm 中可用的秘密,并分配存储在pg_password中的秘密。您还需要在db服务中引用秘密以允许其访问。使用文本编辑器打开文件,并对文件进行以下更改:
4   db:
5     image: postgres
6     ports:
7       - 5432:5432
8     environment:
9       - POSTGRES_PASSWORD=/run/secrets/pg_password
10    secrets:
11      - pg_password
  1. web服务使用相同的秘密来访问 PostgreSQL 数据库。进入docker-compose.ymlweb服务部分,并将第 21 行更改为以下内容,因为它现在将使用您创建的秘密:
20    environment:
21       - PGPASSWORD=/run/secrets/pg_password
22    deploy:
  1. 最后,就像您对配置所做的那样,在docker-compose.yml的末尾定义秘密。在您的compose文件的末尾添加以下行:
41 secrets:
42  pg_password:
43    external: true
  1. 在部署更改之前,您已经对compose文件进行了许多更改,因此您的docker-compose.yml文件应该与下面的代码块中显示的内容类似。您有三个服务正在运行,使用dbwebnginx服务设置,现在我们有一个config实例和一个secret实例:

docker-compose.yml

version: '3.3'
services:
  db:
    image: postgres
    ports:
      - 5432:5432
    environment:
      - POSTGRES_PASSWORD=/run/secrets/pg_password
    secrets:
      - pg_password
  web:
    build: .
    image: swarm_web:latest

命令:gunicorn chapter_nine.wsgi:application --bind 0.0.0.0:8000

    volumes:
      - .:/application
    ports:
      - 8000:8000

此步骤的完整代码可以在packt.live/3miUJD8找到。

注意

我们的服务有一些更改,如果在将更改部署到 Swarm 时出现任何问题,删除服务然后重新部署以确保所有更改正确生效可能是值得的。

这是本练习中 Swarm 部署的最终运行:

docker stack deploy --compose-file docker-compose.yml test_swarm
  1. 运行部署,并确保服务成功运行和部署:
Creating network test_swarm_default
Creating config test_swarm_nginx_conf
Creating service test_swarm_db
Creating service test_swarm_web
Creating service test_swarm_nginx

在这个练习中,您已经练习使用 Swarm 来部署一整套服务,使用您的docker-compose.yml文件,并让它们在几分钟内运行。本章的这一部分还演示了 Swarm 的一些额外功能,使用configsecret实例来帮助我们减少将服务移动到不同环境所需的工作量。现在您知道如何从命令行管理 Swarm,您可以在下一节中进一步探索 Swarm 集群管理,使用 Swarmpit 的 Web 界面。

使用 Swarmpit 管理 Swarm

命令行为用户提供了一种高效和有用的方式来控制他们的 Swarm。如果您的服务和节点随着需求增加而增加,这可能会让一些用户感到困惑。帮助管理和监控您的 Swarm 的一种方法是使用诸如 Swarmpit 提供的 Web 界面,以帮助您管理不同的环境。

正如您很快将看到的,Swarmpit 提供了一个易于使用的 Web 界面,允许您管理 Docker Swarm 实例的大多数方面,包括堆栈、秘密、服务、卷网络和配置。

注意

本章仅涉及 Swarmpit 的使用,但如果您想了解更多关于该应用程序的信息,以下网站应该为您提供更多详细信息:swarmpit.io

Swarmpit 是一个简单易用的安装 Docker 镜像,当在您的系统上运行时,它会在您的环境中创建其服务群来运行管理和 Web 界面。安装完成后,Web 界面可以从http://0.0.0.0:888访问。

要在您的系统上运行安装程序以启动 Swarm,请执行以下docker run命令。通过这样做,您可以将容器命名为swampit-installer,并挂载容器卷到/var/run/docker.sock,以便它可以管理我们系统上的其他容器,使用swarmpit/install:1.8镜像:

docker run -it --rm   --name swarmpit-installer   --volume /var/run/docker.sock:/var/run/docker.sock   swarmpit/install:1.8

安装程序将设置一个带有数据库、代理、Web 应用程序和网络的 Swarm,并引导您设置一个管理用户,以便首次登录到界面。一旦您登录到 Web 应用程序,界面就直观且易于导航。

以下练习将向您展示如何在运行系统上安装和运行 Swarmpit,并开始管理已安装的服务。

练习 9.04:安装 Swarmpit 并管理您的堆栈

在这个练习中,您将安装和运行 Swarmpit,简要探索 Web 界面,并开始从 Web 浏览器管理您的服务:

  1. 这并不是完全必要的,但如果您已经停止了test_swarm堆栈的运行,请再次启动它。这将为您提供一些额外的服务,以便从 Swarmpit 进行监视:
docker stack deploy --compose-file docker-compose.yml test_swarm

注意

如果您担心系统上会同时运行太多服务,请随时跳过此test_swarm堆栈的重启。该练习可以在安装过程中创建的 Swarmpit 堆栈上执行。

  1. 运行以下docker run命令:
docker run -it --rm   --name swarmpit-installer   --volume /var/run/docker.sock:/var/run/docker.sock   swarmpit/install:1.8

它从swarmpit存储库中提取install:1.8镜像,然后通过设置环境详细信息的过程,允许用户对堆栈名称、端口、管理员用户名和密码进行更改。然后创建运行应用程序所需的相关服务:

_____      ____ _ _ __ _ __ ___  _ __ (_) |_ 
/ __\ \ /\ / / _` | '__| '_ ` _ \| '_ \| | __|
\__ \\ V  V / (_| | |  | | | | | | |_) | | |_ 
|___/ \_/\_/ \__,_|_|  |_| |_| |_| .__/|_|\__|
                                 |_|          
Welcome to Swarmpit
Version: 1.8
Branch: 1.8
…
Application setup
Enter stack name [swarmpit]: 
Enter application port [888]: 
Enter database volume driver [local]: 
Enter admin username [admin]: 
Enter admin password (min 8 characters long): ****
DONE.
Application deployment
Creating network swarmpit_net
Creating service swarmpit_influxdb
Creating service swarmpit_agent
Creating service swarmpit_app
Creating service swarmpit_db
DONE.
  1. 在命令行上运行stack ls命令,确保您已经将 Swarmpit swarm 部署到您的节点上:
docker stack ls

以下输出确认了 Swarmpit 已部署到我们的节点上:

NAME               SERVICES         ORCHESTRATOR
swarmpit           4                Swarm
test_swarm         3                Swarm
  1. 使用service ls命令验证 Swarmpit 所需的服务是否正在运行:
docker service ls | grep swarmpit

为了清晰起见,这里显示的输出仅显示了前四列。输出还显示每个服务的REPLICAS值为1/1

ID              NAME                 MODE          REPLICAS
vi2qbwq5y9c6    swarmpit_agent       global        1/1
4tpomyfw93wy    swarmpit_app         replicated    1/1
nuxi5egfa3my    swarmpit_db          replicated    1/1
do77ey8wz49a    swarmpit_influxdb    replicated    1/1

现在是时候登录到 Swarmpit web 界面了。打开您的网络浏览器,使用http://0.0.0.0:888打开 Swarmpit 登录页面,并输入您在安装过程中设置的管理员用户名和密码:

图 9.3:Swarmpit 登录屏幕

图 9.3:Swarmpit 登录屏幕

  1. 一旦您登录,您将看到 Swarmpit 欢迎屏幕,显示您在节点上运行的所有服务的仪表板,以及节点上正在使用的资源的详细信息。屏幕左侧提供了一个菜单,您可以监视和管理 Swarm 堆栈的所有不同方面,包括堆栈本身、ServicesTasksNetworksNodesVolumesSecretsConfigsUsers。单击左侧菜单中的Stacks选项,然后选择test_swarm堆栈:图 9.4:Swarmpit 欢迎仪表板

图 9.4:Swarmpit 欢迎仪表板

  1. 您将看到类似于以下内容的屏幕。为了清晰起见,屏幕的大小已经缩小,但正如您所看到的,它提供了堆栈的所有交互组件的详细信息,包括可用的服务以及正在使用的秘密和配置。如果您点击堆栈名称旁边的菜单,如图所示,您可以编辑堆栈。现在点击Edit Stack图 9.5:使用 Swarmpit 管理您的 swarm

图 9.5:使用 Swarmpit 管理您的 swarm

  1. 编辑堆栈会弹出一个页面,您可以直接对堆栈进行更改,就像对docker-compose.yml进行更改一样。移动到文件底部,找到 Web 服务的副本条目,并将其从2更改为3图 9.6:使用 Swarmpit 编辑您的 Swarm

图 9.6:使用 Swarmpit 编辑您的 Swarm

  1. 单击屏幕底部的“部署”按钮。这将在环境中部署对test_swarm堆栈的更改,并将您返回到test_swarm堆栈屏幕,在那里您现在应该看到正在运行的 Web 服务的3/3副本:图 9.7:在 Swarmpit 中增加 Web 服务的数量

图 9.7:在 Swarmpit 中增加 Web 服务的数量

  1. 请注意,Swarmpit 中的大多数选项都是相互关联的。在test_swarm堆栈页面上,如果您从“服务”面板中单击 Web 服务,您将打开test_swarm_web服务的“服务”页面。如果单击菜单,您应该会看到以下页面:图 9.8:使用 Swarmpit 管理服务

图 9.8:使用 Swarmpit 管理服务

  1. 从菜单中选择“回滚服务”,您将看到test_swarm_web服务的副本数量回滚到两个副本。

  2. 最后,返回到“堆栈”菜单,再次选择test_swarm。打开test_swarm堆栈后,您可以通过单击屏幕顶部的垃圾桶图标来删除堆栈。确认您要删除堆栈,这将再次关闭test_swarm,它将不再在您的节点上运行:图 9.9:在 Swarmpit 中删除 Web 服务

图 9.9:在 Swarmpit 中删除 Web 服务

注意

注意,Swarmpit 将允许您删除swarmpit堆栈。您会看到一个错误,但当您尝试重新加载页面时,所有服务都将被停止运行,因此页面将不会再次出现。

尽管这只是对 Swarmpit 的简要介绍,但借助本章的先前知识,界面将允许您直观地部署和更改您的服务和堆栈。几乎您可以从命令行执行的任何操作,也可以从 Swarmpit Web 界面执行。这就是本练习的结束,也是本章的结束。本章下一节的活动旨在帮助您进一步扩展您的知识。

活动 9.01:将全景徒步应用程序部署到单节点 Docker Swarm

您需要使用 Docker Swarm 在全景徒步应用程序中部署 Web 和数据库服务。您将收集配置以创建一个应用程序的组合文件,并使用docker-compose.yml文件将它们部署到单节点 Swarm 中。

完成此活动所需的步骤如下:

  1. 收集所有应用程序并构建 Swarm 服务所需的 Docker 镜像。

  2. 创建一个docker-compose.yml文件,以便将服务部署到 Docker Swarm。

  3. 创建部署后服务所需的任何支持镜像。

  4. 将您的服务部署到 Swarm 并验证所有服务能够成功运行。

您的运行服务应该类似于此处显示的输出:

ID       NAME                MODE         REPLICAS
  IMAGE
k6kh…    activity_swarm_db   replicated   1/1
  postgres:latest
copa…    activity_swarm_web  replicated   1/1
  activity_web:latest  

注意

此活动的解决方案可通过此链接找到。

继续进行下一个活动,因为这将有助于巩固您在本章中已经学到的一些信息。

活动 9.02:在 Swarm 运行时执行应用程序更新

在这项活动中,您需要对全景徒步应用程序进行微小更改,以便您可以构建一个新的镜像并将该镜像部署到正在运行的 Swarm 中。在这项活动中,您将执行滚动更新以将这些更改部署到您的 Swarm 集群。

完成此活动所需的步骤如下:

  1. 如果您没有来自活动 9.01:将全景徒步应用程序部署到单节点 Docker Swarm的 Swarm 仍在运行,请重新部署 Swarm。

  2. 对全景徒步应用程序中的代码进行微小更改——一些可以测试的小改动,以验证您已在环境中进行了更改。您正在进行的更改并不重要,因此可以是诸如配置更改之类的基本内容。这项活动的重点是执行滚动更新服务。

  3. 构建一个新的镜像,部署到正在运行的环境中。

  4. 对环境进行更新,并验证更改是否成功。

注意

此活动的解决方案可通过此链接找到。

摘要

本章在将我们的 Docker 环境从手动启动单个镜像服务转移到更适合生产并且完整的环境中进行了大量工作,使用了 Docker Swarm。我们从深入讨论 Docker Swarm 开始,介绍了如何通过命令行管理服务和节点,提供了一系列命令及其用法,并将它们作为运行测试 Django Web 应用程序的新环境的一部分进行了实施。

然后,我们进一步扩展了这个应用程序,使用了 NGINX 代理,并利用了 Swarm 功能来存储配置和秘密数据,这样它们就不再需要作为我们 Docker 镜像的一部分,而是可以包含在我们部署的 Swarm 中。然后,我们向您展示了如何使用 Web 浏览器使用 Swarmpit 来管理您的 Swarm,提供了我们之前在命令行上所做工作的概述,并且在 Web 浏览器中进行了许多这些更改。当使用 Docker 时,Swarm 并不是编排环境的唯一方式。

在下一章中,我们将介绍 Kubernetes,这是另一个用于管理 Docker 环境和应用程序的编排工具。在这里,您将看到如何将 Kubernetes 作为项目的一部分,以帮助减少管理服务的时间并改善应用程序的更新。

第十章:Kubernetes

概述

在本章中,我们将学习 Kubernetes,这是市场上最流行的容器管理系统。从基础知识、架构和资源开始,您将创建 Kubernetes 集群并在其中部署真实应用程序。

在本章结束时,您将能够识别 Kubernetes 设计的基础知识及其与 Docker 的关系。您将创建和配置本地 Kubernetes 集群,使用客户端工具使用 Kubernetes API,并使用基本的 Kubernetes 资源来运行容器化应用程序。

介绍

在之前的章节中,您使用 Docker Compose 和 Docker Swarm 运行了多个 Docker 容器。在各种容器中运行的微服务帮助开发人员创建可扩展和可靠的应用程序。

然而,当多个应用程序分布在数据中心的多台服务器上,甚至分布在全球多个数据中心时,管理这些应用程序变得更加复杂。与分布式应用程序复杂性相关的问题有很多,包括但不限于网络、存储和容器管理。

例如,应该配置在相同节点上运行的容器以及不同节点上运行的容器之间的网络。同样,应该使用中央控制器管理包含应用程序的容器的卷(可以进行扩展或缩减)。幸运的是,分布式容器的管理有一个被广泛接受和采用的解决方案:Kubernetes。

Kubernetes 是一个用于运行可扩展、可靠和强大的容器化应用程序的开源容器编排系统。可以在从 Raspberry Pi 到数据中心的各种平台上运行 Kubernetes。Kubernetes 使得可以运行具有挂载卷、插入密钥和配置网络接口的容器。此外,它专注于容器的生命周期,以提供高可用性和可扩展性。凭借其全面的方法,Kubernetes 是目前市场上领先的容器管理系统。

Kubernetes 在希腊语中意为船长。与 Docker 对船只和容器的类比一样,Kubernetes 将自己定位为航海大师。Kubernetes 的理念源于在过去十多年中管理 Google 服务(如 Gmail 或 Google Drive)的容器。从 2014 年至今,Kubernetes 一直是一个由Cloud Native Computing FoundationCNCF)管理的开源项目。

Kubernetes 的主要优势之一来自于其社区和维护者。它是 GitHub 上最活跃的存储库之一,有近 88,000 次提交来自 2400 多名贡献者。此外,该存储库拥有超过 62,000 个星标,这意味着超过 62,000 人对该存储库有信心。

图 10.1:Kubernetes GitHub 存储库

图 10.1:Kubernetes GitHub 存储库

在本章中,您将探索 Kubernetes 的设计和架构,然后了解其 API 和访问,并使用 Kubernetes 资源来创建容器化应用程序。由于 Kubernetes 是领先的容器编排工具,亲身体验它将有助于您进入容器化应用程序的世界。

Kubernetes 设计

Kubernetes 专注于容器的生命周期,包括配置、调度、健康检查和扩展。通过 Kubernetes,可以安装各种类型的应用程序,包括数据库、内容管理系统、队列管理器、负载均衡器和 Web 服务器。

举例来说,想象一下你正在一家名为InstantPizza的新在线食品外卖连锁店工作。你可以在 Kubernetes 中部署你的移动应用的后端,并使其能够根据客户需求和使用情况进行扩展。同样,你可以在 Kubernetes 中实现消息队列,以便餐厅和顾客之间进行通信。为了存储过去的订单和收据,你可以在 Kubernetes 中部署一个带有存储的数据库。此外,你可以使用负载均衡器来为你的应用实现Blue/GreenA/B 部署

在本节中,讨论了 Kubernetes 的设计和架构,以说明它如何实现可伸缩性和可靠性。

注意

Blue/green 部署专注于安装同一应用的两个相同版本(分别称为蓝色和绿色),并立即从蓝色切换到绿色,以减少停机时间和风险。

A/B 部署侧重于安装应用程序的两个版本(即 A 和 B),用户流量在版本之间分配,用于测试和实验。

Kubernetes 的设计集中在一个或多个服务器上运行,即集群。另一方面,Kubernetes 由许多组件组成,这些组件应分布在单个集群上,以便拥有可靠和可扩展的应用程序。

Kubernetes 组件分为两组,即控制平面节点。尽管 Kubernetes 景观的组成元素有不同的命名约定,例如控制平面的主要组件而不是主控组件,但分组的主要思想并未改变。控制平面组件负责运行 Kubernetes API,包括数据库、控制器和调度器。Kubernetes 控制平面中有四个主要组件:

  • kube-apiserver: 这是连接集群中所有组件的中央 API 服务器。

  • etcd: 这是 Kubernetes 资源的数据库,kube-apiserver 将集群的状态存储在 etcd 上。

  • kube-scheduler: 这是将容器化应用程序分配给节点的调度器。

  • kube-controller-manager: 这是在集群中创建和管理 Kubernetes 资源的控制器。

在具有节点角色的服务器上,有两个 Kubernetes 组件:

  • kubelet: 这是运行在节点上的 Kubernetes 客户端,用于在 Kubernetes API 和容器运行时(如 Docker)之间创建桥接。

  • kube-proxy: 这是在每个节点上运行的网络代理,允许集群中的工作负载进行网络通信。

控制平面和节点组件以及它们的交互如下图所示:

图 10.2: Kubernetes 架构

图 10.2: Kubernetes 架构

Kubernetes 设计用于在可扩展的云系统上运行。然而,有许多工具可以在本地运行 Kubernetes 集群。minikube 是官方支持的 CLI 工具,用于创建和管理本地 Kubernetes 集群。其命令侧重于集群的生命周期事件和故障排除,如下所示:

  • minikube start: 启动本地 Kubernetes 集群

  • minikube stop: 停止正在运行的本地 Kubernetes 集群

  • minikube delete: 删除本地 Kubernetes 集群

  • minikube service: 获取本地集群中指定服务的 URL(s)

  • minikube ssh:登录或在具有 SSH 的机器上运行命令

在下一个练习中,您将创建一个本地 Kubernetes 集群,以检查本章讨论的组件。为了创建一个本地集群,您将使用minikube作为官方的本地 Kubernetes 解决方案,并运行其命令来探索 Kubernetes 组件。

注意

minikube在虚拟机上运行集群,您需要根据您的操作系统安装虚拟机监控程序,如 KVM、VirtualBox、VMware Fusion、Hyperkit 或基于 Hyper-V。您可以在kubernetes.io/docs/tasks/tools/install-minikube/#install-a-hypervisor上查看官方文档以获取更多信息。

注意

请使用touch命令创建文件,并使用vim命令在 vim 编辑器中处理文件。

练习 10.01:启动本地 Kubernetes 集群

Kubernetes 最初设计为在具有多个服务器的集群上运行。这是一个容器编排器的预期特性,用于在云中运行可扩展的应用程序。然而,有很多时候您需要在本地运行 Kubernetes 集群,比如用于开发或测试。在这个练习中,您将安装一个本地 Kubernetes 提供程序,然后创建一个 Kubernetes 集群。在集群中,您将检查本节讨论的组件。

要完成这个练习,请执行以下步骤:

  1. 下载适用于您操作系统的最新版本的minikube可执行文件,并通过在终端中运行以下命令将二进制文件设置为本地系统可执行:
# Linux
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
# MacOS
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64 
chmod +x minikube 
sudo mv minikube /usr/local/bin

上述命令下载了 Linux 或 Mac 的二进制文件,并使其在终端中可用:

图 10.3:安装 minikube

图 10.3:安装 minikube

  1. 使用以下命令在终端中启动 Kubernetes 集群:
minikube start

前面的单个命令执行多个步骤,成功创建一个集群。您可以按如下方式检查每个阶段及其输出:

图 10.4:启动一个新的 Kubernetes 集群

图 10.4:启动一个新的 Kubernetes 集群

输出以打印版本和环境开始。然后,拉取并启动 Kubernetes 组件的镜像。最后,经过几分钟后,您将拥有一个本地运行的 Kubernetes 集群。

  1. 使用以下命令连接到由minikube启动的集群节点:
minikube ssh

使用ssh命令,您可以继续在集群中运行的节点上工作:

图 10.5:集群节点

图 10.5:集群节点

  1. 使用以下命令检查每个控制平面组件:
docker ps --filter „name=kube-apiserver" --filter „name=etcd" --filter „name=kube-scheduler" --filter „name=kube-controller-manager" | grep -v „pause"

此命令检查 Docker 容器并使用控制平面组件名称进行过滤。以下输出不包含暂停容器,该容器负责 Kubernetes 中容器组的网络设置,以便进行分析:

图 10.6:控制平面组件

图 10.6:控制平面组件

输出显示,四个控制平面组件在minikube节点的 Docker 容器中运行。

  1. 使用以下命令检查第一个节点组件kube-proxy
docker ps --filter "name=kube-proxy"  | grep -v "pause"

步骤 4类似,此命令列出了一个在 Docker 容器中运行的kube-proxy组件:

图 10.7:minikube 中的 kube-proxy

图 10.7:minikube 中的 kube-proxy

可以看到在 Docker 容器中运行的kube-proxy组件已经运行了 21 分钟。

  1. 使用以下命令检查第二个节点组件kubelet
pgrep -l kubelet

此命令列出了在minikube中运行的进程及其 ID:

2554 kubelet

由于kubelet在容器运行时和 API 服务器之间进行通信,因此它被配置为直接在机器上运行,而不是在 Docker 容器内部运行。

  1. 使用以下命令断开与步骤 3中连接的minikube节点的连接:
exit

你应该已经返回到你的终端并获得类似以下的输出:

logout

在这个练习中,您已经安装了一个 Kubernetes 集群并检查了架构组件。在下一节中,将介绍 Kubernetes API 和访问方法,以连接和使用本节中创建的集群。

Kubernetes API 和访问

Kubernetes API是 Kubernetes 系统的基本构建模块。它是集群中所有组件之间的通信中心。外部通信,如用户命令,也是通过对 Kubernetes API 的 REST API 调用来执行的。Kubernetes API 是基于 HTTP 的资源接口。换句话说,API 服务器旨在使用资源来创建和管理 Kubernetes 资源。在本节中,您将连接到 API,在接下来的部分中,您将开始使用 Kubernetes 资源,包括但不限于 Pods、Deployments、Statefulsets 和 Services。

Kubernetes 有一个官方的命令行工具用于客户端访问,名为kubectl。如果您想访问 Kubernetes 集群,您需要安装kubectl工具并配置它以连接到您的集群。然后,您可以安全地使用该工具来管理运行在集群中的应用程序的生命周期。kubectl能够执行基本的创建、读取、更新和删除操作,以及故障排除和日志检索。

例如,您可以使用kubectl安装一个容器化应用程序,将其扩展到更多副本,检查日志,最后如果不再需要,可以删除它。此外,kubectl还具有用于检查集群和服务器状态的集群管理命令。因此,kubectl是访问 Kubernetes 集群和管理应用程序的重要命令行工具。

kubectl是控制 Kubernetes 集群的关键,具有丰富的命令集。基本的和与部署相关的命令可以列举如下:

  • kubectl create:此命令使用-f标志从文件名创建资源或标准终端输入。在首次创建资源时很有帮助。

  • kubectl apply:此命令创建或更新 Kubernetes 资源的配置,类似于create命令。如果在第一次创建后更改资源配置,则这是一个必要的命令。

  • kubectl get:此命令显示集群中一个或多个资源及其名称、标签和其他信息。

  • kubectl edit:此命令直接在终端中使用诸如vi之类的编辑器编辑 Kubernetes 资源。

  • kubectl delete:此命令删除 Kubernetes 资源并传递文件名、资源名称和标签标志。

  • kubectl scale:此命令更改 Kubernetes 集群资源的数量。

类似地,所需的集群管理和配置命令列举如下:

  • kubectl cluster-info:此命令显示集群的摘要及其 API 和 DNS 服务。

  • kubectl api-resources:此命令列出服务器支持的 API 资源。如果您使用支持不同 API 资源集的不同 Kubernetes 安装,这将特别有帮助。

  • kubectl version:此命令打印客户端和服务器版本信息。如果您使用不同版本的多个 Kubernetes 集群,这是一个有用的命令,可以捕捉版本不匹配。

  • kubectl config:此命令配置 kubectl 将不同的集群连接到彼此。kubectl 是一个设计用于通过更改其配置与多个集群一起工作的 CLI 工具。

在下面的练习中,您将安装和配置 kubectl 来连接到本地 Kubernetes 集群,并开始使用其丰富的命令集来探索 Kubernetes API。

练习 10.02:使用 kubectl 访问 Kubernetes 集群

Kubernetes 集群安装在云系统中,并可以从各种位置访问。要安全可靠地访问集群,您需要一个可靠的客户端工具,即 Kubernetes 的官方客户端工具 kubectl。在这个练习中,您将安装、配置和使用 kubectl 来探索其与 Kubernetes API 的能力。

要完成此练习,请执行以下步骤:

  1. 下载适用于您操作系统的 kubectl 可执行文件的最新版本,并通过在终端中运行以下命令将其设置为本地系统的可执行文件:
# Linux
curl -LO https://storage.googleapis.com/kubernetes-release/release/'curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt'/bin/linux/amd64/kubectl
# MacOS
curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/darwin/amd64/kubectl"
chmod +x kubectl 
sudo mv kubectl /usr/local/bin

上述命令下载了适用于 Linux 或 Mac 的二进制文件,并使其在终端中准备就绪:

图 10.8:minikube 的安装

图 10.8:minikube 的安装

  1. 在您的终端中,运行以下命令来配置 kubectl 连接到 minikube 集群并将其用于进一步访问:
kubectl config use-context minikube

use-context 命令配置 kubectl 上下文以使用 minikube 集群。在接下来的步骤中,所有命令将与在 minikube 内运行的 Kubernetes 集群通信:

Switched to context "minikube".
  1. 使用以下命令检查集群和客户端版本:
kubectl version --short

该命令返回可读的客户端和服务器版本信息:

Client Version: v1.17.2
Server Version: v1.17.0
  1. 使用以下命令检查有关集群的更多信息:
kubectl cluster-info

此命令显示 Kubernetes 组件的摘要,包括主节点和 DNS:

Kubernetes master is running at https://192.168.64.5:8443
KubeDNS is running at https://192.168.64.5:8445/api/v1/
namespaces/kube-system/Services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 
'kubectl cluster-info dump'.
  1. 使用以下命令获取集群中节点的列表:
kubectl get nodes

由于集群是一个 minikube 本地集群,只有一个名为 minikube 的节点具有 master 角色:

NAME        STATUS        ROLES        AGE        VERSION
Minikube    Ready         master       41h        v1.17.0
  1. 使用以下命令列出 Kubernetes API 中支持的资源:
kubectl api-resources --output="name"

此命令列出 Kubernetes API 服务器支持的 api-resourcesname 字段。长列表显示了 Kubernetes 如何创建不同的抽象来运行容器化应用程序:

图 10.9:Kubernetes 资源列表

图 10.9:Kubernetes 资源列表

输出列出了我们连接到的 Kubernetes 集群中可用的 API 资源。正如您所看到的,有数十种资源可供使用,每种资源都可以帮助您创建云原生、可扩展和可靠的应用程序。

在这个练习中,您已连接到 Kubernetes 集群并检查了客户端工具的功能。kubectl 是访问和管理在 Kubernetes 中运行的应用程序最关键的工具。通过本练习的结束,您将学会如何安装、配置和连接到 Kubernetes 集群。此外,您还将检查其版本、节点的状态和可用的 API 资源。有效地使用 kubectl 是开发人员在与 Kubernetes 交互时日常生活中的重要任务。

在接下来的部分中,将介绍主要的 Kubernetes 资源(在上一个练习的最后一步中看到)。

Kubernetes 资源

Kubernetes 提供了丰富的抽象,用于定义云原生应用程序中的容器。所有这些抽象都被设计为 Kubernetes API 中的资源,并由控制平面管理。换句话说,应用程序在控制平面中被定义为一组资源。同时,节点组件尝试实现资源中指定的状态。如果将 Kubernetes 资源分配给节点,节点组件将专注于附加所需的卷和网络接口,以保持应用程序的正常运行。

假设您将在 Kubernetes 上部署 InstantPizza 预订系统的后端。后端由数据库和用于处理 REST 操作的 Web 服务器组成。您需要在 Kubernetes 中定义一些资源:

  • 一个StatefulSet资源用于数据库

  • 一个Service资源用于从其他组件(如 Web 服务器)连接到数据库

  • 一个Deployment资源,以可扩展的方式部署 Web 服务器

  • 一个Service资源,以使外部连接到 Web 服务器

当这些资源在控制平面通过 kubectl 定义时,节点组件将在集群中创建所需的容器、网络和存储。

在 Kubernetes API 中,每个资源都有独特的特征和模式。在本节中,您将了解基本的 Kubernetes 资源,包括 Pods、Deployments、StatefulSet 和 Services。此外,您还将了解更复杂的 Kubernetes 资源,如 Ingresses、Horizontal Pod Autoscaling 和 Kubernetes 中的 RBAC 授权。

Pods

Pod 是 Kubernetes 中容器化应用程序的基本构建块。它由一个或多个容器组成,这些容器可以共享网络、存储和内存。Kubernetes 将 Pod 中的所有容器调度到同一个节点上。此外,Pod 中的容器一起进行扩展或缩减。容器、Pod 和节点之间的关系可以概括如下:

图 10.10:容器、Pod 和节点

图 10.10:容器、Pod 和节点

从上图可以看出,一个 Pod 可以包含多个容器。所有这些容器共享共同的网络、存储和内存资源。

Pod 的定义很简单,有四个主要部分:

apiVersion: v1
kind: Pod
metadata:
  name: server
spec:
  containers:
  - name: main
    image: nginx

所有 Kubernetes 资源都需要这四个部分:

  • apiVersion定义了对象的资源的版本化模式。

  • kind代表 REST 资源名称。

  • metadata保存了资源的信息,如名称、标签和注释。

  • spec是资源特定部分,其中包含资源特定信息。

当在 Kubernetes API 中创建前面的 server Pod 时,API 首先会检查定义是否符合apiVersion=v1kind=Pod的模式。然后,调度程序将 Pod 分配给一个节点。随后,节点中的kubelet将为main容器创建nginx容器。

Pods 是 Kubernetes 对容器的第一个抽象,它们是更复杂资源的构建块。在接下来的部分中,我们将使用资源,如 Deployments 和 Statefulsets 来封装 Pods,以创建更复杂的应用程序。

Deployments

部署是 Kubernetes 资源,专注于可伸缩性和高可用性。部署封装了 Pod 以扩展、缩小和部署新版本。换句话说,您可以将三个副本的 Web 服务器 Pod 定义为部署。控制平面中的部署控制器将保证副本的数量。此外,当您将部署更新到新版本时,控制器将逐渐更新应用程序实例。

部署和 Pod 的定义类似,尽管在部署的模式中添加了标签和副本:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: server
spec:
  replicas: 10
  selector:
    matchLabels:
      app: server
  template:
    metadata:
      labels:
        app: server
    spec:
      containers:
      - name: main
        image: nginx
        ports:
        - containerPort: 80 

部署server具有带有标签app:server的 Pod 规范的 10 个副本。此外,每个服务器实例的主容器的端口80都被发布。部署控制器将创建或删除实例以匹配定义的 Pod 的 10 个副本。换句话说,如果具有两个运行实例的服务器部署的节点下线,控制器将在剩余节点上创建两个额外的 Pod。Kubernetes 的这种自动化使我们能够轻松创建可伸缩和高可用的应用程序。

在接下来的部分中,将介绍用于有状态应用程序(如数据库和消息队列)的 Kubernetes 资源。

StatefulSets

Kubernetes 支持在磁盘卷上存储其状态的有状态应用程序的运行,使用StatefulSet资源。StatefulSets 使得在 Kubernetes 中运行数据库应用程序或数据分析工具具有与临时应用程序相同的可靠性和高可用性。

StatefulSets 的定义类似于部署的定义,具有卷挂载声明添加

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: database
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: mysql
  replicas: 1
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "root"
        ports:
        - name: mysql
          containerPort: 3306
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
        subPath: mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 2Gi

数据库资源定义了一个具有2GB磁盘卷的MySQL数据库。当在 Kubernetes API 中创建服务器StatefulSet资源时,cloud-controller-manager将创建一个卷并在预定的节点上准备好。在创建卷时,它使用volumeClaimTemplates下的规范。然后,节点将根据spec中的volumeMounts部分在容器中挂载卷。

在此资源定义中,还有一个设置MYSQL_ROOT_PASSWORD环境变量的示例。StatefulSets 是 Kubernetes 中至关重要的资源,因为它们使得可以在相同的集群中运行有状态应用程序和临时工作负载。

在下面的资源中,将介绍 Pod 之间连接的 Kubernetes 解决方案。

服务

Kubernetes 集群托管在各个节点上运行的多个应用程序,大多数情况下,这些应用程序需要相互通信。假设您有一个包含三个实例的后端部署和一个包含两个实例的前端应用程序部署。有五个 Pod 在集群中运行,并分布在各自的 IP 地址上。由于前端实例需要连接到后端,前端实例需要知道后端实例的 IP 地址,如图 10.11所示:

图 10.11:前端和后端实例

图 10.11:前端和后端实例

然而,这并不是一种可持续的方法,随着集群的扩展或缩减以及可能发生的大量潜在故障。Kubernetes 提出了服务资源,用于定义具有标签的一组 Pod,并使用服务的名称访问它们。例如,前端应用程序可以通过使用backend-service的地址连接到后端实例,如图 10.12所示:

图 10.12:通过后端服务连接的前端和后端实例

图 10.12:通过后端服务连接的前端和后端实例

服务资源的定义相当简单,如下所示:

apiVersion: v1
kind: Service
metadata:
  name: my-db
spec:
  selector:
    app: mysql
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306

创建my-db服务后,集群中的所有其他 Pod 都将能够通过地址my-db连接到标有app:mysql标签的 Pod 的3306端口。在下面的资源中,将介绍使用 Kubernetes Ingress 资源对集群中服务进行外部访问的方法。

Ingress

Kubernetes 集群旨在为集群内外的应用程序提供服务。Ingress 资源被定义为将服务暴露给外部世界,并具有额外的功能,如外部 URL 和负载平衡。虽然 Ingress 资源是原生的 Kubernetes 对象,但它们需要在集群中运行 Ingress 控制器。换句话说,Ingress 控制器不是kube-controller-manager的一部分,您需要在集群中安装一个 Ingress 控制器。市场上有多种实现可用。但是,Kubernetes 目前正式支持和维护GCEnginx控制器。

注意

官方文档中提供了其他 Ingress 控制器的列表,链接如下:kubernetes.io/docs/concepts/Services-networking/Ingress-controllers

具有主机 URL 为my-db.docker-workshop.io,连接到my-db服务上的端口3306的 Ingress 资源如下所示:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: my-db
spec:
  rules:
  - host: my-db.docker-workshop.io
    http:
      paths:
      - path: /
        backend:
          serviceName: my-db
          servicePort: 3306

Ingress 资源对于向外界打开服务至关重要。然而,它们的配置可能比看起来更复杂。根据您集群中运行的 Ingress 控制器,Ingress 资源可能需要单独的注释。

在接下来的资源中,将介绍使用水平 Pod 自动缩放器来自动缩放 Pod 的功能。

水平 Pod 自动缩放

Kubernetes 集群提供了可扩展和可靠的容器化应用环境。然而,手动跟踪应用程序的使用情况并在需要时进行扩展或缩减是繁琐且不可行的。因此,Kubernetes 提供了水平 Pod 自动缩放器,根据 CPU 利用率自动缩放 Pod 的数量。

水平 Pod 自动缩放器是 Kubernetes 资源,具有用于缩放和目标指标的目标资源。

apiVersion: Autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: server-scaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: server
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 50

当创建server-scaler资源时,Kubernetes 控制平面将尝试通过扩展或缩减名为server的部署来实现50%的目标 CPU 利用率。此外,最小和最大副本数设置为110。这确保了当部署未被使用时不会缩减到0,也不会扩展得太高以至于消耗集群中的所有资源。水平 Pod 自动缩放器资源是 Kubernetes 中创建可扩展和可靠应用程序的重要部分,这些应用程序是自动管理的。

在接下来的部分,您将了解 Kubernetes 中的授权。

RBAC 授权

Kubernetes 集群旨在安全地连接和更改资源。然而,当应用程序在生产环境中运行时,限制用户的操作范围至关重要。

假设您已经赋予项目组中的每个人广泛的权限。在这种情况下,将无法保护集群中运行的应用免受删除或配置错误的影响。Kubernetes 提供了基于角色的访问控制RBAC)来管理用户的访问和能力,基于赋予他们的角色。换句话说,Kubernetes 可以限制用户在特定 Kubernetes 资源上执行特定任务的能力。

让我们从Role资源开始定义能力:

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: critical-project
  name: Pod-reader
rules:
  - apiGroups: [""]
    resources: ["Pods"]
    verbs: ["get", "watch", "list"]

在前面片段中定义的Pod-reader角色只允许在critical-project命名空间中getwatchlist Pod 资源。当用户只有Pod-reader角色时,他们将无法删除或修改critical-project命名空间中的资源。让我们看看如何使用RoleBinding资源将角色分配给用户:

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: read-Pods
  namespace: critical-project
subjects:
  - kind: User
    name: new-intern
roleRef:
  kind: Role
  name: Pod-reader
  apiGroup: rbac.authorization.k8s.io

RoleBinding资源将Role资源与主体结合起来。在read-Pods RoleBinding中,用户new-intern被分配到Pod-reader角色。当在 Kubernetes API 中创建read-Pods资源时,new-intern用户将无法修改或删除critical-project命名空间中的 Pods。

在接下来的练习中,您将使用kubectl和本地 Kubernetes 集群来实践 Kubernetes 资源。

练习 10.03:Kubernetes 资源实践

由于云原生容器化应用的复杂性,需要多个 Kubernetes 资源。在这个练习中,您将使用一个Statefulset、一个Deployment和两个Service资源在 Kubernetes 上创建一个流行的 WordPress 应用的实例。此外,您将使用kubectlminikube检查 Pods 的状态并连接到 Service。

要完成这个练习,请执行以下步骤:

  1. 在一个名为database.yaml的文件中创建一个StatefulSet定义,内容如下:
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: database
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: mysql
  replicas: 1
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "root"
        ports:
        - name: mysql
          containerPort: 3306
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 2Gi

这个StatefulSet资源定义了一个数据库,将在接下来的步骤中被 WordPress 使用。只有一个名为mysql的容器,使用mysql:5.7的 Docker 镜像。容器规范中定义了一个根密码的环境变量和一个端口。此外,在前述定义中声明了一个卷并将其附加到/var/lib/mysql

  1. 通过在终端中运行以下命令将StatefulSet部署到集群中:
kubectl apply -f database.yaml

这个命令将应用database.yaml文件中的定义,因为它带有-f标志:

StatefulSet.apps/database created
  1. 在本地计算机上创建一个database-service.yaml文件,包含以下内容:
apiVersion: v1
kind: Service
metadata:
  name: database-service
spec:
  selector:
    app: mysql
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306

这个 Service 资源定义了数据库实例上的 Service 抽象。WordPress 实例将使用指定的 Service 连接到数据库。

  1. 使用以下命令部署 Service 资源:
kubectl apply -f database-service.yaml

这个命令部署了在database-service.yaml文件中定义的资源:

Service/database-service created
  1. 创建一个名为wordpress.yaml的文件,并包含以下内容:
apiVersion: apps/v1 
kind: Deployment
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  replicas: 3
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
      - image: wordpress:4.8-apache
        name: wordpress
        env:
        - name: WORDPRESS_DB_HOST
          value: database-Service
        - name: WORDPRESS_DB_PASSWORD
          value: root
        ports:
        - containerPort: 80
          name: wordpress

这个Deployment资源定义了一个三个副本的 WordPress 安装。有一个容器定义了wordpress:4.8-apache镜像,并且database-service作为环境变量传递给应用程序。通过这个环境变量的帮助,WordPress 连接到步骤 3中部署的数据库。此外,定义了一个容器端口,端口号为80,以便我们可以在接下来的步骤中从浏览器中访问应用程序。

  1. 使用以下命令部署 WordPress Deployment:
kubectl apply -f wordpress.yaml

这个命令部署了在wordpress.yaml文件中定义的资源:

Deployment.apps/wordpress created
  1. 在本地计算机上创建一个wordpress-service.yaml文件,包含以下内容:
apiVersion: v1
kind: Service
metadata:
  name: wordpress-service
spec:
  type: LoadBalancer
  selector:
    app: wordpress
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

这个 Service 资源定义了 WordPress 实例上的 Service 抽象。该 Service 将用于通过端口80从外部世界连接到 WordPress。

  1. 使用以下命令部署Service资源:
kubectl apply -f wordpress-service.yaml

这个命令部署了在wordpress-service.yaml文件中定义的资源:

Service/wordpress-service created
  1. 使用以下命令检查所有运行中的 Pod 的状态:
kubectl get pods

这个命令列出了所有 Pod 及其状态,有一个数据库和三个 WordPress Pod 处于Running状态:

图 10.13:Pod 列表

图 10.13:Pod 列表

  1. 通过运行以下命令获取wordpress-service的 URL:
minikube service wordpress-service --url

这个命令列出了可以从主机机器访问的 Service 的 URL:

http://192.168.64.5:32765

在浏览器中打开 URL 以访问 WordPress 的设置屏幕:

图 10.14:WordPress 设置屏幕

图 10.14:WordPress 设置屏幕

设置屏幕显示 WordPress 实例正在运行,并且可以通过它们的服务访问。此外,它显示StatefulSet数据库也正在运行,并且可以通过 WordPress 实例的服务访问。

在这个练习中,您已经使用不同的 Kubernetes 资源来定义和安装 Kubernetes 中的复杂应用程序。首先,您部署了一个Statefulset资源来在集群中安装 MySQL。然后,您部署了一个Service资源来在集群内部访问数据库。随后,您部署了一个Deployment资源来安装 WordPress 应用程序。类似地,您创建了另一个Service来在集群外部访问 WordPress 应用程序。您使用不同的 Kubernetes 资源创建了独立可伸缩和可靠的微服务,并将它们连接起来。此外,您已经学会了如何检查Pods的状态。在接下来的部分,您将了解 Kubernetes 软件包管理器:Helm。

Kubernetes 软件包管理器:Helm

由于云原生微服务架构的特性,Kubernetes 应用程序由多个容器、卷和网络资源组成。微服务架构将大型应用程序分成较小的块,因此会产生大量的 Kubernetes 资源和大量的配置值。

Helm 是官方的 Kubernetes 软件包管理器,它将应用程序的资源收集为模板,并填充提供的值。这里的主要优势在于积累的社区知识,可以按照最佳实践安装应用程序。即使您是第一次使用,也可以使用最流行的方法安装应用程序。此外,使用 Helm 图表增强了开发人员的体验。

例如,在 Kubernetes 中安装和管理复杂的应用程序就变得类似于在 Apple Store 或 Google Play Store 中下载应用程序,只需要更少的命令和配置。在 Helm 术语中,一个单个应用程序的资源集合被称为chart。当您使用 Helm 软件包管理器时,可以使用图表来部署从简单的 pod 到带有 HTTP 服务器、数据库、缓存等的完整 Web 应用程序堆栈。将应用程序封装为图表使得部署复杂的应用程序变得更容易。

此外,Helm 还有一个图表存储库,其中包含流行和稳定的应用程序,这些应用程序被打包为图表,并由 Helm 社区维护。稳定的 Helm 图表存储库拥有各种各样的应用程序,包括 MySQL、PostgreSQL、CouchDB 和 InfluxDB 等数据库;Jenkins、Concourse 和 Drone 等 CI/CD 工具;以及 Grafana、Prometheus、Datadog 和 Fluentd 等监控工具。图表存储库不仅使安装应用程序变得更加容易,还确保您使用 Kubernetes 社区中最新、广受认可的方法部署应用程序。

Helm 是一个客户端工具,其最新版本为 Helm 3。您只需要在本地系统上安装它,为图表存储库进行配置,然后就可以开始部署应用程序。Helm 是一个功能强大的软件包管理器,具有详尽的命令集,包括以下内容:

  • helm repo:此命令向本地 Helm 安装添加、列出、移除、更新和索引图表存储库。

  • helm search:此命令使用用户提供的关键字或图表名称在各种存储库中搜索 Helm 图表。

  • helm install:此命令在 Kubernetes 集群上安装 Helm 图表。还可以使用值文件或命令行参数设置变量。

  • helm listhelm ls:这些命令列出了从集群中安装的图表。

  • helm uninstall:此命令从 Kubernetes 中移除已安装的图表。

  • helm upgrade:此命令使用新值或新的图表版本在集群上升级已安装的图表。

在接下来的练习中,您将安装 Helm,连接到图表存储库,并在集群上安装应用程序。

练习 10.04:安装 MySQL Helm 图表

Helm 图表由官方客户端工具helm安装和管理。您需要在本地安装helm客户端工具,以从图表存储库检索图表,然后在集群上安装应用程序。在此练习中,您将开始使用 Helm,并从其稳定的 Helm 图表中安装MySQL

要完成此练习,请执行以下步骤:

  1. 在终端中运行以下命令以下载带有安装脚本的helm可执行文件的最新版本:
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

该脚本将下载适用于您的操作系统的helm二进制文件,并使其在终端中可用。

图 10.15:安装 Helm

图 10.15:安装 Helm

  1. 通过在终端中运行以下命令,将图表存储库添加到helm中:
helm repo add stable https://kubernetes-charts.storage.googleapis.com/

此命令将图表存储库的 URL 添加到本地安装的helm实例中:

"stable" has been added to your repositories
  1. 使用以下命令列出步骤 2stable存储库中的图表:
helm search repo stable

此命令将列出存储库中所有可用的图表:

图 10.16:图表存储库列表

图 10.16:图表存储库列表

  1. 使用以下命令安装 MySQL 图表:
helm install database stable/mysql

此命令将从stable存储库中安装 MySQL Helm 图表,并打印如何连接到数据库的信息:

图 10.17:MySQL 安装

图 10.17:MySQL 安装

如果您想要使用mysql客户端在集群内部或外部连接到 MySQL 安装,输出中的信息是有价值的。

  1. 使用以下命令检查安装的状态:
helm ls

我们可以看到有一个名为mysql-chart-1.6.2的安装,状态为deployed

图 10.18:Helm 安装状态

图 10.18:Helm 安装状态

您还可以使用helm ls命令来检查应用程序和图表版本,例如5.7.28mysql-1.6.2

  1. 使用以下命令检查与步骤 4中安装相关的 Kubernetes 资源:
kubectl get all -l release=database

此命令列出所有具有标签release = database的资源:

图 10.19:Kubernetes 资源列表

图 10.19:Kubernetes 资源列表

由于安装生产级别的 MySQL 实例并不简单,并且由多个资源组成,因此列出了各种资源。多亏了 Helm,我们不需要配置每个资源并连接它们。此外,使用标签release = database进行列出有助于在 Helm 安装的某些部分失败时提供故障排除概述。

在这个练习中,您已经安装和配置了 Kubernetes 包管理器 Helm,并使用它安装了应用程序。如果您计划在生产环境中使用 Kubernetes 并需要管理复杂的应用程序,Helm 是一个必不可少的工具。

在接下来的活动中,您将配置并部署全景徒步应用程序到 Kubernetes 集群。

活动 10.01:在 Kubernetes 上安装全景徒步应用程序

您被指派在 Kubernetes 上创建全景徒步应用程序的部署。您将利用全景徒步应用程序的三层架构和最先进的 Kubernetes 资源。您将使用 Helm 安装数据库,并使用 Statefulset 和nginx安装后端。因此,您将将其设计为 Kubernetes 应用程序,并使用kubectlhelm进行管理。

执行以下步骤完成练习:

  1. 使用 PostgreSQL Helm 图表安装数据库。确保POSTGRES_PASSWORD环境变量设置为kubernetes

  2. 为全景徒步应用程序的后端和nginx创建一个具有两个容器的 Statefulset。确保使用 Docker 镜像packtworkshops/the-docker-workshop:chapter10-pta-webpacktworkshops/the-docker-workshop:chapter10-pta-nginx。为了存储静态文件,您需要创建一个volumeClaimTemplate部分,并将其挂载到两个容器的/Service/static/路径。最后,不要忘记发布nginx容器的端口80

  3. 为全景徒步应用程序创建一个 Kubernetes 服务,以连接到步骤 2中创建的 Statefulset。确保服务的typeLoadBalancer

  4. 成功部署后,获取步骤 3中创建的 Kubernetes 服务的 IP,并在浏览器中连接到$SERVICE_IP/admin地址:图 10.20:管理员登录

图 10.20:管理员登录

  1. 使用用户名admin和密码changeme登录,并添加新的照片和国家:图 10.21:管理员设置

图 10.21:管理员设置

  1. 全景徒步应用程序将在浏览器中的地址$SERVICE_IP/photo_viewer上可用:图 10.22:应用程序视图

图 10.22:应用程序视图

注意

此活动的解决方案可以通过此链接找到。

摘要

本章重点介绍了使用 Kubernetes 设计、创建和管理容器化应用程序。Kubernetes 是市场上新兴的容器编排器,具有很高的采用率和活跃的社区。在本章中,您已经了解了其架构和设计,接着是 Kubernetes API 及其访问方法,并深入了解了创建复杂的云原生应用程序所需的关键 Kubernetes 资源。

本章中的每个练习都旨在说明 Kubernetes 的设计方法和其能力。通过 Kubernetes 资源及其官方客户端工具kubectl,可以配置、部署和管理容器化应用程序。

在接下来的章节中,您将了解 Docker 世界中的安全性。您将学习容器运行时、容器镜像和 Linux 环境的安全概念,以及如何在 Docker 中安全运行容器。

第十一章:Docker 安全性

概述

在本章中,我们将为您提供所需的信息,以确保您的容器是安全的,并且不会对使用其上运行的应用程序的人员构成安全风险。您将使用特权和非特权容器,并了解为什么不应该以 root 用户身份运行容器。本章将帮助您验证镜像是否来自可信的来源,使用签名密钥。您还将为 Docker 镜像设置安全扫描,确保您的镜像可以安全使用和分发。您将使用 AppArmor 进一步保护您的容器,并使用 Linux 的安全计算模式(seccomp)来创建和使用seccomp配置文件与您的 Docker 镜像。

介绍

本章试图解决一个可以专门写一本书的主题。我们试图在教育您如何使用 Docker 来处理安全性方面走一部分路。之前的章节已经为您提供了使用 Docker 构建应用程序的坚实基础,本章希望利用这些信息为它们提供安全稳定的容器来运行。

Docker 和微服务架构使我们能够从更安全和健壮的环境开始管理我们的服务,但这并不意味着我们需要完全忘记安全性。本章详细介绍了在创建和维护跨环境服务时需要考虑的一些方面,以及您可以开始在工作系统中实施这些程序的方式。

Docker 安全性不应该与您的常规 IT 安全流程分开,因为概念是相同的。Docker 有不同的处理这些概念的方法,但总的来说,开始使用 Docker 安全性的好地方包括以下内容:

  • 访问控制:确保运行的容器无法被攻击者访问,并且权限也受到限制。

  • 更新和修补操作系统:我们需要确保我们使用的镜像来自可信的来源。我们还需要能够扫描我们的镜像,以确保引入的任何应用程序也不会引入额外的漏洞。

  • 数据敏感性:所有敏感信息都应该保持不可访问。这可能是密码、个人信息,或者任何您不希望被任何人获取的数据。

在本章中,我们将涵盖许多信息,包括前述的内容以及更多。我们将首先考虑在运行时您的 Docker 容器可能具有的不同访问权限,以及您如何开始限制它们可以执行的操作。然后,我们将更仔细地研究如何保护镜像,使用签名密钥,以及如何验证它们来自可信任的来源。我们还将练习扫描您的镜像以确保它们可以安全使用的已知漏洞。本章的最后两节将重点介绍使用 AppArmor 和seccomp安全配置文件来进一步限制正在运行的容器的功能和访问权限。

注意

在 Docker 镜像中使用密码和秘钥时,编排方法如 Swarm 和 Kubernetes 提供了安全的存储秘钥的方式,无需将它们存储为明文配置供所有人访问。如果您没有使用这些编排方法,我们也将在下一章提供一些关于如何在镜像中使用秘钥的想法。

容器中的特权和 root 用户访问权限

提高容器安全性的一个重要方法是减少攻击者在获得访问权限后可以做的事情。攻击者在容器上可以运行的命令类型受限于运行容器进程的用户的访问权限级别。因此,如果运行容器的用户没有 root 或提升的特权,这将限制攻击者可以做的事情。另一个需要记住的事情是,如果容器被攻破并以 root 用户身份运行,这也可能允许攻击者逃离容器并访问运行 Docker 的主机系统。

容器上运行的大多数进程都是不需要 root 访问权限的应用程序,这与在服务器上运行进程是一样的,您也不会将它们作为 root 运行。在容器上运行的应用程序应该只能访问它们所需的内容。提供 root 访问权限的原因,特别是在基础镜像中,是因为应用程序需要安装在容器上,但这应该只是一个临时措施,您的完整镜像应该以另一个用户身份运行。

为了做到这一点,在创建我们的镜像时,我们可以设置一个 Dockerfile 并创建一个将在容器上运行进程的用户。下面这行与在 Linux 命令行上设置用户相同,我们首先设置组,然后将用户分配到这个组中:

RUN addgroup --gid <GID> <UID> && adduser <UID> -h <home_directory> --disabled-password --uid <UID> --ingroup <UID> <user_name>

在上述命令中,我们还使用adduser选项来设置home目录并禁用登录密码。

注意

addgroupadduser是特定于基于 Alpine 的镜像的命令,这些镜像是基于 Linux 的镜像,但使用不同的软件包和实用程序来自基于 Debian 的镜像。Alpine 镜像使用这些软件包的原因是它们选择更轻量级的实用程序和应用程序。如果您使用的是基于 Ubuntu/Debian 或 Red Hat 的镜像,您需要改用useraddgroupadd命令,并使用这些命令的相关选项。

正如您将在即将进行的练习中看到的,我们将切换到我们专门创建的用户以创建我们将要运行的进程。您可以自行决定组和用户的名称,但许多用户更喜欢使用四位或五位数字作为这将不会向潜在攻击者突出显示该用户的任何更多特权,并且通常是创建用户和组的标准做法。在我们的 Dockerfile 中,在创建进程之前,我们包括USER指令,并包括我们先前创建的用户的用户 ID:

USER <UID>

在本章的这一部分,我们将介绍一个新的镜像,并展示如果容器上的进程由 root 用户运行可能会出现的问题。我们还将向您展示容器中的 root 用户与底层主机上的 root 用户是相同的。然后,我们将更改我们的镜像,以展示删除容器上运行的进程的 root 访问权限的好处。

注意

请使用touch命令创建文件,并使用vim命令在文件上使用 vim 编辑器进行操作。

练习 11.01:以 root 用户身份运行容器

当我们以 root 用户身份运行容器进程时,可能会出现许多问题。本练习将演示特定的安全问题,例如更改访问权限、终止进程、对 DNS 进行更改,以及您的镜像和底层操作系统可能会变得脆弱。您将注意到,作为 root 用户,攻击者还可以使用诸如nmap之类的工具来扫描网络以查找开放的端口和网络目标。

您还将纠正这些问题,从而限制攻击者在运行容器上的操作:

  1. 使用您喜欢的文本编辑器创建一个名为Dockerfile_original的新 Dockerfile,并将以下代码输入文件。在此步骤中,所有命令都是以 root 用户身份运行的:
1 FROM alpine
2
3 RUN apk update
4 RUN apk add wget curl nmap libcap
5
6 RUN echo "#!/sh\n" > test_memory.sh
7 RUN echo "cat /proc/meminfo; mpstat; pmap -x 1"     >> test_memory.sh
8 RUN chmod 755 test_memory.sh
9
10 CMD ["sh", "test_memory.sh"]

这将创建一个基本的应用程序,将运行一个名为test_memory.sh的小脚本,该脚本使用meminfompstatpmap命令来提供有关容器内存状态的详细信息。您还会注意到在第 4 行上,我们正在安装一些额外的应用程序,以使用nmap查看网络进程,并使用libcap库查看用户容器的功能。

  1. 构建security-app镜像并在同一步骤中运行该镜像:
docker build -t security-app . ; docker run –rm security-app

输出已经大大减少,您应该看到镜像构建,然后运行内存报告:

MemTotal:        2036900 kB
MemFree:         1243248 kB
MemAvailable:    1576432 kB
Buffers:          73240 kB
…
  1. 使用whoami命令查看容器上的运行用户:
docker run --rm security-app whoami

不应该让人感到惊讶的是运行用户是 root 用户:

root
  1. 使用capsh –print命令查看用户在容器上能够运行的进程。作为 root 用户,您应该拥有大量的功能:
docker run --rm -it security-app capsh –print

您会注意到用户可以访问更改文件所有权(cap_chown),杀死进程(cap_kill)和对 DNS 进行更改(cap_net_bind_service)等功能。这些都是可以在运行环境中引起许多问题的高级进程,不应该对容器可用:

Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,
cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,
cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,
cap_setfcap+eip
groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),
11(floppy),20(dialout),26(tape),27(video)
  1. 作为 root 用户,攻击者还可以使用我们之前安装的nmap等工具来扫描网络以查找开放的端口和网络目标。通过传递nmap命令再次运行您的容器镜像,查找localhost下已打开的443端口:
docker run --rm -it security-app sh -c 'nmap -sS -p 443 localhost'

命令的输出如下:

Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-13 02:40 UTC
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000062s latency).
Other addresses for localhost (not scanned): ::1
PORT    STATE  SERVICE
443/tcp closed https
Nmap done: 1 IP address (1 host up) scanned in 0.27 seconds

注意

前面的nmap扫描没有找到任何开放的网络,但这是一个不应该能够由任何用户运行的提升命令。我们将在本练习的后面演示非 root 用户无法运行此命令。

  1. 如前所述,在容器上作为 root 用户与在底层主机上作为 root 用户是相同的。这可以通过将一个由 root 拥有的文件挂载到容器上来证明。为此,创建一个秘密文件。将您的秘密密码回显到/tmp/secret.txt文件中:
echo "secret password" > /tmp/secret.txt

更改所有权以确保 root 用户拥有它:

sudo chown root /tmp/secret.txt
  1. 使用docker run命令将文件挂载到运行的容器上,并检查是否能够访问并查看文件中的数据。容器上的用户可以访问只有主机系统上的 root 用户才能访问的文件:
docker run -v /tmp/secret.txt:/tmp/secret.txt security-app sh -c 'cat /tmp/secret.txt'

来自 docker run 命令的输出将是“secret password

secret password

然而,Docker 容器不应该能够暴露这些信息。

  1. 要开始对容器进行一些简单的更改,以阻止再次发生这种访问,再次打开 Dockerfile 并添加突出显示的代码(行 6789),保持先前的代码不变。这些代码将创建一个名为10001的组和一个名为20002的用户。然后将设置一个带有home目录的用户,然后您将进入该目录并开始使用行 9中的USER指令进行操作:
1 FROM alpine
2
3 RUN apk update
4 RUN apk add wget curl nmap libcap
5
6 RUN addgroup --gid 10001 20002 && adduser 20002 -h     /home/security_apps --disabled-password --uid 20002     --ingroup 20002
7 WORKDIR /home/security_apps
8
9 USER 20002
  1. 行 15进行更改,以确保脚本是从新的security_app目录运行的,然后保存 Dockerfile:
11 RUN echo "#!/sh\n" > test_memory.sh
12 RUN echo "cat /proc/meminfo; mpstat; pmap -x 1" >>     test_memory.sh
13 RUN chmod 755 test_memory.sh
14
15 CMD ["sh", "/home/security_apps/test_memory.sh"]

完整的 Dockerfile 应该如下所示:

FROM alpine
RUN apk update
RUN apk add wget curl nmap libcap
RUN addgroup --gid 10001 20002 && adduser 20002 -h   /home/security_apps --disabled-password --uid 20002     --ingroup 20002
WORKDIR /home/security_apps
USER 20002
RUN echo "#!/sh\n" > test_memory.sh
RUN echo "cat /proc/meminfo; mpstat; pmap -x 1" >>   test_memory.sh
RUN chmod 755 test_memory.sh
CMD ["sh", "/home/security_apps/test_memory.sh"]
  1. 再次构建图像并使用whoami命令运行它:
docker build -t security-app . ; docker run --rm security-app whoami

您将看到一个新用户为20002而不是 root 用户:

20002
  1. 以前,您可以从容器中运行nmap。验证新用户是否被阻止访问nmap命令以扫描网络漏洞:
docker run --rm -it security-app sh -c 'nmap -sS -p 443 localhost'

通过使用nmap -sS命令再次运行您的镜像,您现在应该无法运行该命令,因为容器正在以20002用户身份运行,没有足够的权限来运行该命令:

You requested a scan type which requires root privileges.
QUITTING!
  1. 您现在已经大大限制了运行容器的功能,但是由主机 root 用户拥有的文件是否仍然可以被运行的security-app容器访问?再次挂载文件,看看是否可以输出文件的信息:
docker run -v /tmp/secret.txt:/tmp/secret.txt security-app sh -c 'cat /tmp/secret.txt'

您应该在结果中看到Permission denied,确保容器不再可以访问secret.txt文件:

cat: can't open '/tmp/secret.txt': Permission denied

正如我们在本练习中所演示的,删除正在运行的容器对 root 用户的访问权限是减少攻击者可以实现的目标的一个良好的第一步。下一节将快速查看运行容器的特权和能力以及如何使用docker run命令进行操作。

运行时特权和 Linux 能力

在运行容器时,Docker 提供了一个标志,可以覆盖所有安全和用户选项。这是通过使用––privileged选项来运行容器来实现的。尽管您已经看到了当容器以 root 用户身份运行时用户可以实现什么,但我们正在以非特权状态运行容器。尽管提供了––privileged选项,但应该谨慎使用,如果有人请求以此模式运行您的容器,我们应该谨慎对待。有一些特定情况,例如,如果您需要在树莓派上运行 Docker 并需要访问底层架构,那么您可能希望为用户添加功能。

如果您需要为容器提供额外的特权以运行特定命令和功能,Docker 提供了一种更简单的方法,即使用––cap–add––cap–drop选项。这意味着,与使用––privileged选项提供完全控制不同,您可以使用––cap–add––cap–drop来限制用户可以实现的内容。

在运行容器时,––cap–add––cap–drop可以同时使用。例如,您可能希望包括––cap–add=all––cap–drop=chown

以下是一些可用于––cap``–add––cap–drop的功能的简短列表:

  • setcap:修改正在运行系统的进程功能。

  • mknod:使用mknod命令在运行系统上创建特殊文件。

  • chown:对文件的 UID 和 GID 值执行文件所有权更改。

  • kill:绕过发送信号以停止进程的权限。

  • setgid/setuid:更改进程的 UID 和 GID 值。

  • net_bind_service:将套接字绑定到域端口。

  • sys_chroot:更改运行系统上的root目录。

  • setfcap:设置文件的功能。

  • sys_module:在运行系统上加载和卸载内核模块。

  • sys_admin:执行一系列管理操作。

  • sys_time:对系统时钟进行更改和设置时间。

  • net_admin:执行与网络相关的一系列管理操作。

  • sys_boot:重新启动系统并在系统上加载新内核以供以后执行。

要添加额外的功能,您只需包括该功能,如果您在执行docker run命令时添加或删除功能,您的命令将如下所示:

docker run –-cap-add|--cap-drop <capability_name> <image_name>

正如您所看到的,语法使用––cap–add来添加功能,––cap–drop来移除功能。

注意

如果您有兴趣查看在运行容器时可以添加和删除的全部功能列表,请访问man7.org/linux/man-pages/man7/capabilities.7.html

我们已经简要介绍了使用特权和功能。在本章的后面,我们将有机会在测试安全配置文件时使用这些功能。不过,现在我们将看看如何使用数字签名来验证我们的 Docker 镜像的真实性。

签署和验证 Docker 镜像

就像我们可以确保我们购买和安装在系统上的应用程序来自可信任的来源一样,我们也可以对我们使用的 Docker 镜像进行同样的操作。运行一个不受信任的 Docker 镜像可能会带来巨大的风险,并可能导致系统出现重大问题。这就是为什么我们应该寻求对我们使用的镜像进行特定的验证。不受信任的来源可能会向正在运行的镜像添加代码,这可能会将整个网络暴露给攻击者。

幸运的是,Docker 有一种方式可以对我们的镜像进行数字签名,以确保我们使用的是来自经过验证的供应商或提供者的镜像。这也将确保自签名之初镜像未被更改或损坏,从而确保其真实性。这不应该是我们信任镜像的唯一方式。正如您将在本章后面看到的那样,一旦我们有了镜像,我们可以扫描它以确保避免安装可能存在安全问题的镜像。

Docker 允许我们签署和验证镜像的方式是使用Docker 内容信任DCT)。DCT 作为 Docker Hub 的一部分提供,并允许您对从您的注册表发送和接收的所有数据使用数字签名。DCT 与镜像标签相关联,因此并非所有镜像都需要标记,因此并非所有镜像都会有与之相关的 DCT。这意味着任何想要发布镜像的人都可以这样做,但可以确保在需要签署之前镜像是否正常工作。

DCT 并不仅限于 Docker Hub。如果用户在其环境中启用了 DCT,他们只能拉取、运行或构建受信任的镜像,因为 DCT 确保用户只能看到已签名的镜像。DCT 信任是通过使用签名密钥来管理的,这些密钥是在首次运行 DCT 时创建的。当密钥集创建时,它包括三种不同类型的密钥:

  • 离线密钥:用于创建标记密钥。它们应该被小心存放,并由创建图像的用户拥有。如果这些密钥丢失或被 compromise,可能会给发布者带来很多问题。

  • 存储库或标记密钥:这些与发布者相关,并与图像存储库相关联。当您签署准备推送到存储库的受信任图像时使用。

  • 服务器管理的密钥:这些也与图像存储库相关联,并存储在服务器上。

注意

确保您保管好您的离线密钥,因为如果您丢失了离线密钥,它将会导致很多问题,因为 Docker 支持很可能需要介入来重置存储库状态。这还需要所有使用过存储库中签名图像的消费者进行手动干预。

就像我们在前面的章节中看到的那样,Docker 提供了易于使用的命令行选项来生成、加载和使用签名密钥。如果您启用了 DCT,Docker 将使用您的密钥直接对图像进行签名。如果您想进一步控制事情,您可以使用docker trust key generate命令来创建您的离线密钥,并为它们分配名称:

docker trust key generate <name>

您的密钥将存储在您的home目录的.docker/trust目录中。如果您有一组离线密钥,您可以使用docker trust key load命令和您创建它们的名称来使用这些密钥,如下所示:

docker trust key load <pem_key_file> –name <name>

一旦您拥有您的密钥,或者加载了您的原始密钥,您就可以开始对图像进行签名。您需要使用docker trust sign命令包括图像的完整注册表名称和标签:

docker trust sign <registry>/<repo>:<tag>

一旦您签署了您的图像,或者您有一个需要验证签名的图像,您可以使用docker trust inspect命令来显示签名密钥和签发者的详细信息:

docker trust inspect –pretty <registry>/<repo>:<tag>

在开发过程中使用 DCT 可以防止用户使用来自不受信任和未知来源的容器图像。我们将使用本章前几节中我们一直在开发的安全应用程序来创建和实施 DCT 签名密钥。

练习 11.02:签署 Docker 图像并在您的系统上利用 DCT

在接下来的练习中,您将学习如何在您的环境中使用 DCT 并实施使用签名图像的流程。您将首先导出DOCKER_CONTENT_TRUST环境变量以在您的系统上启用 DCT。接下来,您将学习如何对图像进行签名和验证签名的图像:

  1. DOCKER_CONTENT_TRUST环境变量导出到您的系统,以在您的系统上启用 DCT。还要确保将变量设置为1
export DOCKER_CONTENT_TRUST=1
  1. 现在启用了 DCT,您将无法拉取或处理任何没有与其关联签名密钥的 Docker 图像。我们可以通过从 Docker Hub 存储库中拉取security-app图像来测试:
docker pull vincesestodocker/security-app

从错误消息中可以看出,我们无法拉取最新的图像,这是个好消息,因为我们最初没有使用签名密钥进行推送:

Using default tag: latest
Error: remote trust data does not exist for docker.io/vincesestodocker/security-app: notary.docker.io does 
not have trust data for docker.io/vincesestodocker/security-app
  1. 将图像推送到您的图像存储库:
docker push vincesestodocker/security-app

您不应该能够这样做,因为本地图像也没有关联签名密钥:

The push refers to repository 
[docker.io/vincesestodocker/security-app]
No tag specified, skipping trust metadata push
  1. 将新图像标记为trust1,准备推送到 Docker Hub:
docker tag security-app:latest vincesestodocker/security-app:trust1
  1. 如前所述,当我们第一次将图像推送到存储库时,签名密钥将自动与图像关联。确保给你的图像打上标签,因为这将阻止 DCT 识别需要签名。再次将图像推送到存储库:
docker push vincesestodocker/security-app:trust1

在运行上述命令后,将打印以下行:

The push refers to repository 
[docker.io/vincesestodocker/security-app]
eff6491f0d45: Layer already exists 
307b7a157b2e: Layer already exists 
03901b4a2ea8: Layer already exists 
ver2: digest: sha256:7fab55c47c91d7e56f093314ff463b7f97968e
e0f80f5ee927430fc39f525f66 size: 949
Signing and pushing trust metadata
You are about to create a new root signing key passphrase. 
This passphrase will be used to protect the most sensitive key 
in your signing system. Please choose a long, complex passphrase 
and be careful to keep the password and the key file itself 
secure and backed up. It is highly recommended that you use a 
password manager to generate the passphrase and keep it safe. 
There will be no way to recover this key. You can find the key 
in your config directory.
Enter passphrase for new root key with ID 66347fd: 
Repeat passphrase for new root key with ID 66347fd: 
Enter passphrase for new repository key with ID cf2042d: 
Repeat passphrase for new repository key with ID cf2042d: 
Finished initializing "docker.io/vincesestodocker/security-app"
Successfully signed docker.io/vincesestodocker/security-app:
trust1

以下输出显示,当图像被推送到注册表时,作为该过程的一部分创建了一个新的签名密钥,要求用户在过程中创建新的根密钥和存储库密钥。

  1. 现在更加安全了。不过,在您的系统上运行图像呢?现在我们的系统上启用了 DCT,运行容器图像会有任何问题吗?使用docker run命令在您的系统上运行security-app图像:
docker run -it vincesestodocker/security-app sh

该命令应返回以下输出:

docker: No valid trust data for latest.
See 'docker run --help'.

在上面的输出中,我们故意没有使用trust1标签。与前几章一样,Docker 将尝试使用latest标签运行图像。由于这也没有与之关联的签名密钥,因此无法运行它。

  1. 您可以直接从工作系统对图像进行签名,并且可以使用之前创建的密钥对后续标记的图像进行签名。使用trust2标签对图像进行标记:
docker tag vincesestodocker/security-app:trust1 vincesestodocker/security-app:trust2
  1. 使用在此练习中创建的签名密钥对新标记的图像进行签名。使用docker trust sign命令对图像和图像的层进行签名:
docker trust sign vincesestodocker/security-app:trust2

该命令将自动将已签名的图像推送到我们的 Docker Hub 存储库:

Signing and pushing trust data for local image 
vincesestodocker/security-app:trust2, may overwrite remote 
trust data
The push refers to repository 
[docker.io/vincesestodocker/security-app]
015825f3a965: Layer already exists 
2c32d3f8446b: Layer already exists 
1bbb374ec935: Layer already exists 
bcc0069f86e9: Layer already exists 
e239574b2855: Layer already exists 
f5e66f43d583: Layer already exists 
77cae8ab23bf: Layer already exists 
trust2: digest: sha256:a61f528324d8b63643f94465511132a38ff945083c
3a2302fa5a9774ea366c49 size: 1779
Signing and pushing trust metadataEnter passphrase for 
vincesestodocker key with ID f4b834e: 
Successfully signed docker.io/vincesestodocker/security-app:
trust2
  1. 使用docker trust命令和inspect选项查看签名信息:
docker trust inspect --pretty vincesestodocker/security-app:trust2

输出将为您提供签名者的详细信息,已签名的标记图像以及有关图像的其他信息:

Signatures for vincesestodocker/security-app:trust2
SIGNED TAG      DIGEST                     SIGNERS
trust2          d848a63170f405ad3…         vincesestodocker
List of signers and their keys for vincesestodocker/security-app:
trust2
SIGNER              KEYS
vincesestodocker    f4b834e54c71
Administrative keys for vincesestodocker/security-app:trust2
  Repository Key:
    26866c7eba348164f7c9c4f4e53f04d7072fefa9b52d254c573e8b082
    f77c966
  Root Key:
    69bef52a24226ad6f5505fd3159f778d6761ac9ad37483f6bc88b1cb4
    7dda334
  1. 使用docker trust revoke命令来移除相关密钥的签名:
docker trust revoke vincesestodocker/security-app:trust2
Enter passphrase for vincesestodocker key with ID f4b834e: 
Successfully deleted signature for vincesestodocker/security-app:
trust2

注意

如果您正在使用自己的 Docker 注册表,您可能需要设置一个公证服务器,以允许 DCT 与您的 Docker 注册表一起工作。亚马逊的弹性容器注册表和 Docker 可信注册表等产品已经内置了公证功能。

正如您所看到的,使用 DCT 对 Docker 映像进行签名和验证可以轻松地控制您作为应用程序一部分使用的映像。从可信源使用已签名的映像只是方程式的一部分。在下一节中,我们将使用 Anchore 和 Snyk 来开始扫描我们的映像以查找漏洞。

Docker 映像安全扫描

安全扫描在不仅确保应用程序的正常运行时间方面发挥着重要作用,而且还确保您不会运行过时、未打补丁或容器映像存在漏洞。应该对团队使用的所有映像以及您的环境中使用的所有映像进行安全扫描。无论您是从头开始创建它们并且信任它们与否,这都是减少环境中潜在风险的重要步骤。本章的这一部分将介绍两种扫描映像的选项,这些选项可以轻松地被您的开发团队采用。

通过对我们的 Docker 映像实施安全扫描,我们希望实现以下目标:

  • 我们需要保持一个已知且最新的漏洞数据库,或者使用一个将代表我们保持这个数据库的应用程序。

  • 我们将我们的 Docker 映像与漏洞数据库进行扫描,不仅验证底层操作系统是否安全和打了补丁,还验证容器使用的开源应用程序和我们软件实现所使用的语言是否安全。

  • 安全扫描完成后,我们需要得到一个完整的报告,报告和警报任何在扫描过程中可能被突出显示的问题。

  • 最后,安全扫描可以提供任何发现的问题的修复,并通过更新 Dockerfile 中使用的基础镜像或支持的应用程序来发出警报。

市场上有很多可以为您执行安全扫描的产品,包括付费和开源产品。在本章中,由于篇幅有限,我们选择了两项我们发现易于使用并提供良好功能的服务。首先是 Anchore,这是一个开源的容器分析工具,我们将安装到我们的系统上,并作为本地工具来测试我们的图像。然后我们将看看 Snyk,这是一个在线 SaaS 产品。Snyk 有免费版本可用,这也是我们在本章中将使用的版本,以演示其工作原理。它提供了不错的功能,而无需支付月费。

使用 Anchore 安全扫描本地扫描图像

Anchore 容器分析是一个开源的静态分析工具,允许您扫描您的 Docker 图像,并根据用户定义的策略提供通过或失败的结果。Anchore Engine 允许用户拉取图像,并在不运行图像的情况下分析图像的内容,并评估图像是否适合使用。Anchore 使用 PostgreSQL 数据库存储已知漏洞的详细信息。然后,您可以使用命令行界面针对数据库扫描图像。Anchore 还非常容易上手,正如我们将在接下来的练习中看到的那样,它提供了一个易于使用的docker-compose文件,以自动安装并尽快让您开始使用。

注意

如果您对 Anchore 想了解更多信息,可以在docs.anchore.com/current/找到大量的文档和信息。

在即将进行的练习中,一旦我们的环境正常运行,您将使用 Anchore 的 API 进行交互。anchore-cli命令带有许多易于使用的命令,用于检查系统状态并开始评估我们图像的漏洞。

一旦我们的系统正常运行,我们可以使用system status命令来提供所有服务的列表,并确保它们正常运行:

anchore-cli system status

一旦系统正常运行,您需要做的第一件事情之一就是验证 feeds 列表是否是最新的。这将确保您的数据库已经填充了漏洞 feeds。这可以通过以下system feeds list命令来实现:

anchore-cli system feeds list

默认情况下,anchore-cli将使用 Docker Hub 作为您的图像注册表。如果您的图像存储在不同的注册表上,您将需要使用anchore-cli registry add命令添加注册表,并指定注册表名称,以及包括 Anchore 可以使用的用户名和密码:

anchore-cli registry add <registry> <user> <password>

要将图像添加到 Anchore,您可以使用image add命令行选项,包括 Docker Hub 位置和图像名称:

anchore-cli image add <repository_name>/<image_name>

如果您希望扫描图像以查找漏洞,可以使用image vuln选项,包括您最初扫描的图像名称。我们还可以使用os选项来查找特定于操作系统的漏洞,以及non-os来查找与语言相关的漏洞。在以下示例中,我们使用了all来包括osnon-os选项:

anchore-cli image vuln <repository_name>/<image_name> all

然后,要查看图像的完成评估,并根据图像是否安全可用提供通过或失败,您可以使用anchore-cli命令的evaluate check选项:

anchore-cli evaluate check <repository_name>/<image_name>

考虑到所有这些,Anchore 确实提供了一个支持和付费版本,带有易于使用的 Web 界面,但正如您将在以下练习中看到的,需要很少的工作即可让 Anchore 应用程序在您的系统上运行和扫描。

注意

上一个练习在创建和签署容器时使用了 DCT。在以下练习中,用于练习的 Anchore 图像使用了latest标签,因此如果您仍在运行 DCT,则需要在进行下一个练习之前停止它:

export DOCKER_CONTENT_TRUST=0

练习 11.03:开始使用 Anchore 图像扫描

在以下练习中,您将使用docker-compose在本地系统上安装 Anchore,并开始分析您在本章中使用的图像:

  1. 创建并标记您一直在使用的security-app图像的新版本。使用scan1标记图像:
docker tag security-app:latest vincesestodocker/security-app:scan1 ;

将其推送到 Docker Hub 存储库:

docker push vincesestodocker/security-app:scan1
  1. 创建一个名为aevolume的新目录,并使用以下命令进入该目录。这是我们将执行工作的地方:
mkdir aevolume; cd aevolume
  1. Anchore 为您提供了一切您需要开始使用的东西,一个易于使用的docker-compose.yaml文件来设置和运行 Anchore API。使用以下命令拉取最新的anchore-engine Docker Compose 文件:
curl -O https://docs.anchore.com/current/docs/engine/quickstart/docker-compose.yaml
  1. 查看docker-compose.yml文件。虽然文件包含超过 130 行,但文件中没有太复杂的内容。Compose文件正在设置 Anchore 的功能,包括 PostgreSQL 数据库、目录和分析器进行查询;一个简单的队列和策略引擎;以及一个 API 来运行命令和查询。

  2. 使用docker-compose pull命令拉取docker-compose.yml文件所需的镜像,确保您在与Compose文件相同的目录中:

docker-compose pull

该命令将开始拉取数据库、目录、分析器、简单队列、策略引擎和 API:

Pulling anchore-db           ... done
Pulling engine-catalog       ... done
Pulling engine-analyzer      ... done
Pulling engine-policy-engine ... done
Pulling engine-simpleq       ... done
Pulling engine-api           ... done
  1. 如果我们的所有镜像现在都可用,如前面的输出所示,除了使用docker-compose up命令运行Compose文件之外,没有其他事情要做。使用-d选项使所有容器作为守护进程在后台运行:
docker-compose up -d

该命令应该输出以下内容:

Creating network "aevolume_default" with the default driver
Creating volume "aevolume_anchore-db-volume" with default driver
Creating volume "aevolume_anchore-scratch" with default driver
Creating aevolume_anchore-db_1 ... done
Creating aevolume_engine-catalog_1 ... done
Creating aevolume_engine-analyzer_1      ... done
Creating aevolume_engine-simpleq_1       ... done
Creating aevolume_engine-api_1           ... done
Creating aevolume_engine-policy-engine_1 ... done
  1. 运行docker ps命令,以查看系统上正在运行的包含 Anchore 的容器,准备开始扫描我们的镜像。表格中的IMAGECOMMANDCREATED列已被删除以方便查看:
docker-compose ps

输出中的所有值应该显示每个 Anchore Engine 容器的healthy状态:

CONTAINER ID       STATUS         PORTS
    NAMES
d48658f6aa77       (healthy)      8228/tcp
    aevolume_engine-analyzer_1
e4aec4e0b463   (healthy)          8228/tcp
    aevolume_engine-policy-engine_1
afb59721d890   (healthy)          8228->8228/tcp
    aevolume_engine-api_1
d61ff12e2376   (healthy)          8228/tcp
    aevolume_engine-simpleq_1
f5c29716aa40   (healthy)          8228/tcp
    aevolume_engine-catalog_1
398fef820252   (healthy)          5432/tcp
    aevolume_anchore-db_1
  1. 现在环境已部署到您的系统上,使用docker-compose exec命令来运行前面提到的anchor-cli命令。使用pip3命令将anchorecli包安装到您的运行系统上。使用--version命令来验证anchore-cli是否已成功安装:
pip3 install anchorecli; anchore-cli --version

该命令返回anchor-cli的版本:

anchore-cli, version 0.5.0

注意

版本可能会因系统而异。

  1. 现在您可以运行您的anchore-cli命令,但您需要指定 API 的 URL(使用--url)以及用户名和密码(使用--u--p)。相反,使用以下命令将值导出到您的环境中,这样您就不需要使用额外的命令行选项:
export ANCHORE_CLI_URL=http://localhost:8228/v1
export ANCHORE_CLI_USER=admin
export ANCHORE_CLI_PASS=foobar

注意

上述变量是 Anchore 提供的Compose文件的默认值。如果您决定在部署环境中设置运行环境,您很可能会更改这些值以提高安全性。

  1. 现在anchore-cli已安装和配置好,使用anchore-cli system status命令来验证分析器、队列、策略引擎、目录和 API 是否都正常运行:
anchore-cli system status

可能会出现一两个服务宕机的情况,这意味着您很可能需要重新启动容器:

Service analyzer (anchore-quickstart, http://engine-analyzer:
8228): up
Service simplequeue (anchore-quickstart, http://engine-simpleq:
8228): up
Service policy_engine (anchore-quickstart, http://engine-policy-engine:8228): up
Service catalog (anchore-quickstart, http://engine-catalog:
8228): up
Service apiext (anchore-quickstart, http://engine-api:8228): 
up
Engine DB Version: 0.0.11
Engine Code Version: 0.5.1

注意

Engine DB VersionEngine Code Version可能会因系统而异。

  1. 使用anchore-cli system feeds list命令查看数据库中的所有漏洞:
anchore-cli system feeds list

由于提供给数据库的漏洞数量很大,以下输出已经被缩减:

Feed                Group          LastSync
    RecordCount
nvdv2               nvdv2:cves     None
    0
vulnerabilities     alpine:3\.      2019-10-24T03:47:28.504381
    1485
vulnerabilities     alpine:3.3     2019-10-24T03:47:36.658242
    457
vulnerabilities     alpine:3.4     2019-10-24T03:47:51.594635
    681
vulnerabilities     alpine:3.5     2019-10-24T03:48:03.442695
    875
vulnerabilities     alpine:3.6     2019-10-24T03:48:19.384824
    1051
vulnerabilities     alpine:3.7     2019-10-24T03:48:36.626534
    1253
vulnerabilities     alpine:3.8     None
    0
vulnerabilities     alpine:3.9     None
    0
vulnerabilities     amzn:2         None
    0

在前面的输出中,您会注意到一些漏洞 feed 显示为None。这是因为数据库是最近设置的,并且尚未更新所有漏洞。继续显示 feed 列表,就像在上一步中所做的那样,一旦所有条目在LastSync列中显示日期,您就可以开始扫描镜像了。

  1. 一旦 feed 完全更新,使用anchore-cli image add命令添加镜像。记得使用完整路径,包括镜像仓库标签,因为 Anchore 将使用位于 Docker Hub 上的镜像:
anchore-cli image add vincesestodocker/security-app:scan1

该命令将镜像添加到 Anchore 数据库,准备进行扫描:

Image Digest: sha256:7fab55c47c91d7e56f093314ff463b7f97968ee0
f80f5ee927430
fc39f525f66
Parent Digest: sha256:7fab55c47c91d7e56f093314ff463b7f97968ee
0f80f5ee927430fc39f525f66
Analysis Status: not_analyzed
Image Type: docker
Analyzed At: None
Image ID: 8718859775e5d5057dd7a15d8236a1e983a9748b16443c99f8a
40a39a1e7e7e5
Dockerfile Mode: None
Distro: None
Distro Version: None
Size: None
Architecture: None
Layer Count: None
Full Tag: docker.io/vincesestodocker/security-app:scan1
Tag Detected At: 2019-10-24T03:51:18Z 

当您添加镜像时,您会注意到我们已经强调输出显示为not_analyzed。这将被排队等待分析,对于较小的镜像,这将是一个快速的过程。

  1. 监控您的镜像,查看是否已使用anchore-cli image list命令进行分析:
anchore-cli image list

这将提供我们当前添加的所有镜像列表,并显示它们是否已经被分析的状态:

Full Tag               Image Digest            Analysis Status
security-app:scan1     sha256:a1bd1f6fec31…    analyzed
  1. 现在镜像已经添加并分析完成,您可以开始查看镜像,并查看基础镜像和安装的应用程序,包括版本和许可证号。使用anchore-cliimage content os命令。您还可以使用其他内容类型,包括file用于镜像上的所有文件,npm用于所有 Node.js 模块,gem用于 Ruby gems,java用于 Java 存档,以及python用于 Python 工件。
anchore-cli image content vincesestodocker/security-app:scan1 os

该命令将返回以下输出:

Package                   Version        License
alpine-baselayout         3.1.2          GPL-2.0-only
alpine-keys               2.1            MIT
apk-tools                 2.10.4         GPL2 
busybox                   1.30.1         GPL-2.0
ca-certificates           20190108       MPL-2.0 GPL-2.0-or-later
ca-certificates-cacert    20190108       MPL-2.0 GPL-2.0-or-later
curl                      7.66.0         MIT
libc-utils                0.7.1          BSD
libcrypto1.1              1.1.1c         OpenSSL
libcurl                   7.66.0         MIT
libssl1.1                 1.1.1c         OpenSSL
libtls-standalone         2.9.1          ISC
musl                      1.1.22         MIT
musl-utils                1.1.22         MIT BSD GPL2+
nghttp2-libs              1.39.2         MIT
scanelf                   1.2.3          GPL-2.0
ssl_client                1.30.1         GPL-2.0
wget                      1.20.3         GPL-3.0-or-later
zlib                      1.2.11         zlib
  1. 使用anchore-cli image vuln命令,并包括您要扫描的图像以检查漏洞。如果没有漏洞存在,您将不会看到任何输出。我们在下面的命令行中使用了all来提供关于操作系统和非操作系统漏洞的报告。我们也可以使用os来获取特定于操作系统的漏洞,使用non-os来获取与语言相关的漏洞:
anchore-cli image vuln vincesestodocker/security-app:scan1 all
  1. 对图像进行评估检查,为我们提供图像扫描的“通过”或“失败”结果。使用anchore-cli evaluate check命令来查看图像是否安全可用:
anchore-cli evaluate check vincesestodocker/security-app:scan1
From the output of the above command, it looks like our image 
is safe with a pass result.Image Digest: sha256:7fab55c47c91d7e56f093314ff463b7f97968ee0f80f5ee927430fc
39f525f66
Full Tag: docker.io/vincesestodocker/security-app:scan1
Status: pass
Last Eval: 2019-10-24T03:54:40Z
Policy ID: 2c53a13c-1765-11e8-82ef-23527761d060

所有前面的练习都已经很好地确定了我们的图像是否存在漏洞并且是否安全可用。接下来的部分将向您展示 Anchore 的替代方案,尽管它有付费组件,但仍然通过访问免费版本提供了大量的功能。

使用 Snyk 进行 SaaS 安全扫描

Snyk 是一个在线 SaaS 应用程序,提供易于使用的界面,允许您扫描 Docker 图像以查找漏洞。虽然 Snyk 是一个付费应用程序,但它提供了一个免费的功能大量的免费版本。它为开源项目提供无限的测试,并允许 GitHub 和 GitLab 集成,提供对开源项目的修复和持续监控。您所能进行的容器漏洞测试受到限制。

下面的练习将通过使用 Web 界面来指导您如何注册帐户,然后添加要扫描安全漏洞的容器。

练习 11.04:设置 Snyk 安全扫描

在这个练习中,您将使用您的网络浏览器与 Snyk 合作,开始对我们的security-app图像实施安全扫描。

  1. 如果您以前没有使用过 Snyk 或没有帐户,请在 Snyk 上创建一个帐户。除非您想将帐户升级到付费版本,否则您不需要提供任何信用卡详细信息,但在这个练习中,您只需要免费选项。因此,请登录 Snyk 或在app.snyk.io/signup上创建一个帐户。

  2. 您将看到一个网页,如下面的屏幕截图所示。选择您希望创建帐户的方法,并按照提示继续:图 11.1:使用 Snyk 创建帐户

图 11.1:使用 Snyk 创建帐户

  1. 登录后,您将看到一个类似于图 11.2的页面,询问您想要测试的代码在哪里?。Snyk 不仅扫描 Docker 图像,还扫描您的代码以查找漏洞。您已经在 Docker Hub 中有了您的security-app图像,所以点击Docker Hub按钮开始这个过程:图 11.2:使用 Snyk 开始安全扫描

图 11.2:使用 Snyk 开始安全扫描

注意

如果您没有看到上述的网页,您可以转到以下网址添加一个新的存储库。请记住,将以下网址中的<your_account_name>更改为您创建 Snyk 帐户时分配给您的帐户名称:

https://app.snyk.io/org/<your_account_name>/add

  1. 通过 Docker Hub 进行身份验证,以允许其查看您可用的存储库。当出现以下页面时,输入您的 Docker Hub 详细信息,然后点击Continue图 11.3:在 Snyk 中与 Docker Hub 进行身份验证

图 11.3:在 Snyk 中与 Docker Hub 进行身份验证

  1. 验证后,您将看到 Docker Hub 上所有存储库的列表,包括每个存储库存储的标签。在本练习中,您只需要选择一个图像,并使用本节中创建的scan1标签。选择带有scan1标签的security-app图像。一旦您对选择满意,点击屏幕右上角的Add selected repositories按钮:图 11.4:选择要由 Snyk 扫描的 Docker Hub 存储库

图 11.4:选择要由 Snyk 扫描的 Docker Hub 存储库

  1. 一旦您添加了图像,Snyk 将立即对其进行扫描,根据图像的大小,这应该在几秒钟内完成。点击屏幕顶部的Projects选项卡,查看扫描结果,并点击选择您想要查看的存储库和标签:图 11.5:在 Snyk 中查看您的项目报告

图 11.5:在 Snyk 中查看您的项目报告

单击存储库名称后,您将看到图像扫描报告,概述图像的详细信息,使用了哪些基本图像,以及在扫描过程中是否发现了任何高、中或低级问题:

图 11.6:Snyk 中的图像扫描报告页面

图 11.6:Snyk 中的图像扫描报告页面

Snyk 将每天扫描您的镜像,如果发现任何问题,将会通知您。除非发现任何漏洞,否则每周都会给您发送一份报告。如果有漏洞被发现,您将尽快收到通知。

使用 Snyk,您可以使用易于遵循的界面扫描您的镜像中的漏洞。作为一种 SaaS 基于 Web 的应用程序,这也意味着无需管理应用程序和服务器进行安全扫描。这是关于安全扫描我们的镜像的部分的结束,我们现在将转向使用安全配置文件来帮助阻止攻击者利用他们可能能够访问的任何镜像。

使用容器安全配置文件

安全配置文件允许您利用 Linux 中现有的安全工具,并在您的 Docker 镜像上实施它们。在接下来的部分中,我们将涵盖 AppArmor 和seccomp。这些都是您可以在 Docker 环境中运行时减少进程获取访问权限的方式。它们都很容易使用,您很可能已经在您的镜像中使用它们。我们将分别查看它们,但请注意,AppArmor 和 Linux 的安全计算在功能上有重叠。目前,您需要记住的是,AppArmor 可以阻止应用程序访问它们不应该访问的文件,而 Linux 的安全计算将帮助阻止利用任何 Linux 内核漏洞。

默认情况下,特别是如果您正在运行最新版本的 Docker,您可能已经同时运行了两者。您可以通过运行docker info命令并查找Security Options来验证这一点。以下是一个显示两个功能都可用的系统的输出:

docker info
Security Options:
  apparmor
  seccomp
   Profile: default

以下部分将涵盖 Linux 的 AppArmor 和安全计算,并清楚地介绍如何在系统上实施和使用两者。

在您的镜像上实施 AppArmor 安全配置文件

AppArmor 代表应用程序装甲,是一个 Linux 安全模块。AppArmor 的目标是保护操作系统免受安全威胁,并作为 Docker 版本 1.13.0 的一部分实施。它允许用户向其运行的容器加载安全配置文件,并可以创建以锁定容器上服务可用的进程。Docker 默认包含的提供了中等保护,同时仍允许访问大量应用程序。

为了帮助用户编写安全配置文件,AppArmor 提供了complain 模式,允许几乎任何任务在没有受限制的情况下运行,但任何违规行为都将被记录到审计日志中。它还有一个unconfined 模式,与 complain 模式相同,但不会记录任何事件。

注意

有关 AppArmor 的更多详细信息,包括文档,请使用以下链接,它将带您到 GitLab 上 AppArmor 主页:

gitlab.com/apparmor/apparmor/wikis/home

AppArmor 还配备了一套命令,帮助用户管理应用程序,包括将策略编译和加载到内核中。默认配置文件对新用户来说可能有点令人困惑。您需要记住的主要规则是,拒绝规则优先于允许和所有者规则,这意味着如果它们都在同一个应用程序上,则允许规则将被随后的拒绝规则覆盖。文件操作使用'r'表示读取,'w'表示写入,'k'表示锁定,'l'表示链接,'x'表示执行。

我们可以开始使用 AppArmor,因为它提供了一些易于使用的命令行工具。您将使用的第一个是aa-status命令,它提供了系统上所有正在运行的配置文件的状态。这些配置文件位于系统的/etc/apparmor.d目录中:

aa-status

如果我们的系统上安装了配置文件,我们至少应该有docker-default配置文件;它可以通过docker run命令的--security-opt选项应用于我们的 Docker 容器。在下面的示例中,您可以看到我们将--security-opt值设置为apparmor配置文件,或者您可以使用unconfined配置文件,这意味着没有配置文件与该镜像一起运行:

docker run --security-opt apparmor=<profile> <image_name>

要生成我们的配置文件,我们可以使用aa-genprof命令来进一步了解需要设置为配置文件的内容。AppArmor 将在您执行一些示例命令时扫描日志,然后为您在系统上创建一个配置文件,并将其放在默认配置文件目录中:

aa-genprof <application>

一旦您满意您的配置文件,它们需要加载到您的系统中,然后您才能开始使用它们与您的镜像。您可以使用apparmor_parser命令,带有-r(如果已经设置,则替换)和-W(写入缓存)选项。然后可以将配置文件与正在运行的容器一起使用:

apparmor_parser -r -W <path_to_profile>

最后,如果您希望从 AppArmor 中删除配置文件,可以使用apparmor_parser命令和-R选项来执行此操作:

apparmor_parser -R <path_to_profile>

AppArmor 看起来很复杂,但希望通过以下练习,您应该能够熟悉该应用程序,并对生成自定义配置文件增加额外的信心。

练习 11.05:开始使用 AppArmor 安全配置文件

以下练习将向您介绍 AppArmor 安全配置文件,并帮助您在运行的 Docker 容器中实施新规则:

  1. 如果您正在运行 Docker Engine 版本 19 或更高版本,则 AppArmor 应已作为应用程序的一部分设置好。运行docker info命令来验证它是否正在运行:
docker info
…
Security Options:
  apparmor
…
  1. 在本章中,我们通过创建用户20002更改了容器的运行用户。我们将暂停此操作,以演示 AppArmor 在此情况下的工作原理。使用文本编辑器打开Dockerfile,这次将第 9 行注释掉,就像我们在下面的代码中所做的那样:
  8 
  9 #USER 20002
  1. 再次构建Dockerfile并验证镜像一旦再次作为 root 用户运行:
docker build -t security-app . ; docker run --rm security-app whoami

上述命令将构建Dockerfile,然后返回以下输出:

root
  1. 通过在命令行中运行aa-status使用 AppArmorstatus命令:
aa-status

注意

如果您被拒绝运行aa-status命令,请使用sudo

这将显示类似于以下内容的输出,并提供加载的配置文件和加载的配置文件类型。您会注意到输出包括在 Linux 系统上运行的所有 AppArmor 配置文件:

apparmor module is loaded.
15 profiles are loaded.
15 profiles are in enforce mode.
    /home/vinces/DockerWork/example.sh
    /sbin/dhclient
    /usr/bin/lxc-start
    /usr/lib/NetworkManager/nm-dhcp-client.action
    /usr/lib/NetworkManager/nm-dhcp-helper
    /usr/lib/connman/scripts/dhclient-script
    /usr/lib/lxd/lxd-bridge-proxy
    /usr/lib/snapd/snap-confine
    /usr/lib/snapd/snap-confine//mount-namespace-capture-helper
    /usr/sbin/tcpdump
    docker-default
    lxc-container-default
    lxc-container-default-cgns
    lxc-container-default-with-mounting
    lxc-container-default-with-nesting
0 profiles are in complain mode.
1 processes have profiles defined.
1 processes are in enforce mode.
    /sbin/dhclient (920) 
0 processes are in complain mode.
0 processes are unconfined but have a profile defined.
  1. 在后台运行security-app容器,以帮助我们测试 AppArmor:
docker run -dit security-app sh
  1. 由于我们没有指定要使用的配置文件,AppArmor 使用docker-default配置文件。通过再次运行aa-status来验证这一点:
aa-status

您将看到,在输出的底部,现在显示有两个进程处于强制模式,一个显示为docker-default

apparmor module is loaded.
…
2 processes are in enforce mode.
    /sbin/dhclient (920) 
    docker-default (9768)
0 processes are in complain mode.
0 processes are unconfined but have a profile defined.
  1. 删除我们当前正在运行的容器,以便在本练习中稍后不会混淆:
docker kill $(docker ps -a -q)
  1. 在不使用 AppArmor 配置文件的情况下启动容器,使用-–security-opt Docker 选项指定apparmor=unconfined。还使用–-cap-add SYS_ADMIN功能,以确保您对运行的容器具有完全访问权限:
docker run -dit --security-opt apparmor=unconfined --cap-add SYS_ADMIN security-app sh
  1. 访问容器并查看您可以运行哪些类型的命令。使用docker exec命令和CONTAINER ID访问容器,但请注意,您的CONTAINER ID值将与以下不同:
docker exec -it db04693ddf1f sh
  1. 通过创建两个目录并使用以下命令将它们挂载为绑定挂载来测试你所拥有的权限:
mkdir 1; mkdir 2; mount --bind 1 2
ls -l

能够在容器上挂载目录是一种提升的权限,所以如果你能够做到这一点,那么很明显没有配置文件在阻止我们,并且我们可以像这样访问挂载文件系统:

total 8
drwxr-xr-x    2 root     root          4096 Nov  4 04:08 1
drwxr-xr-x    2 root     root          4096 Nov  4 04:08 2
  1. 使用docker kill命令退出容器。你应该看到默认的 AppArmor 配置文件是否会限制对这些命令的访问:
docker kill $(docker ps -a -q)
  1. 创建security-app镜像的一个新实例。在这个实例中,也使用--cap-add SYS_ADMIN能力,以允许加载默认的 AppArmor 配置文件:
docker run -dit --cap-add SYS_ADMIN security-app sh

当创建一个新的容器时,该命令将返回提供给用户的随机哈希。

  1. 通过使用exec命令访问新的运行容器来测试更改,并查看是否可以执行绑定挂载,就像之前的步骤一样:
docker exec -it <new_container_ID> sh 
mkdir 1; mkdir 2; mount --bind 1 2

你应该会看到Permission denied

mount: mounting 1 on 2 failed: Permission denied
  1. 再次退出容器。使用docker kill命令删除原始容器:
docker kill $(docker ps -a -q)

在这个练习的下一部分,你将看到是否可以为我们的 Docker 容器实现自定义配置文件。

  1. 使用 AppArmor 工具收集需要跟踪的资源信息。使用aa-genprof命令跟踪nmap命令的详细信息:
aa-genprof nmap

注意

如果你没有安装aa-genprof命令,使用以下命令安装它,然后再次运行aa-genprof nmap命令:

sudo apt install apparmor-utils

我们已经减少了命令的输出,但如果成功的话,你应该会看到一个输出,显示正在对/usr/bin/nmap命令进行分析:

…
Profiling: /usr/bin/nmap
[(S)can system log for AppArmor events] / (F)inish

注意

如果你的系统中没有安装nmap,运行以下命令:

sudo apt-get update

sudo apt-get install nmap

  1. 在一个单独的终端窗口中运行nmap命令,以向aa-genprof提供应用程序的详细信息。在docker run命令中使用-u root选项,以 root 用户身份运行security-app容器,这样它就能够运行nmap命令:
docker run -it -u root security-app sh -c 'nmap -sS -p 443 localhost'
  1. 返回到你一直在运行aa-genprof命令的终端。按下S来扫描系统日志以查找事件。扫描完成后,按下F来完成生成:
Reading log entries from /var/log/syslog.
Updating AppArmor profiles in /etc/apparmor.d.

所有配置文件都放在/etc/apparmor.d/目录中。如果一切正常,你现在应该在/etc/apparmor.d/usr.bin.nmap文件中看到类似以下输出的文件:

1 # Last Modified: Mon Nov 18 01:03:31 2019
2 #include <tunables/global>
3 
4 /usr/bin/nmap {
5   #include <abstractions/base>
6 
7   /usr/bin/nmap mr,
8 
9 }
  1. 使用apparmor_parser命令将新文件加载到系统上。使用-r选项来替换已存在的配置文件,使用-W选项将其写入缓存:
apparmor_parser -r -W /etc/apparmor.d/usr.bin.nmap
  1. 运行aa-status命令来验证配置文件现在是否可用,并查看是否有一个新的配置文件指定了nmap
aa-status | grep nmap

请注意,配置文件的名称与应用程序的名称相同,即/usr/bin/nmap,这是在运行容器时需要使用的名称:

/usr/bin/nmap
  1. 现在,测试您的更改。以-u root用户运行容器。还使用--security-opt apparmor=/usr/bin/nmap选项以使用新创建的配置文件运行容器:
docker run -it -u root --security-opt apparmor=/usr/bin/nmap security-app sh -c 'nmap -sS -p 443 localhost'

您还应该看到Permission denied的结果,以显示我们创建的 AppArmor 配置文件正在限制使用,这正是我们希望看到的:

sh: nmap: Permission denied

在这个练习中,我们演示了如何在您的系统上开始使用 AppArmor,并向您展示了如何创建您自己的配置文件。在下一节中,我们将继续介绍类似的应用程序,即 Linux 的seccomp

Linux 容器的 seccomp

Linux 的seccomp是从 3.17 版本开始添加到 Linux 内核中的,它提供了一种限制 Linux 进程可以发出的系统调用的方法。这个功能也可以在我们运行的 Docker 镜像中使用,以帮助减少运行容器的进程,确保如果容器被攻击者访问或感染了恶意代码,攻击者可用的命令和进程将受到限制。

seccomp使用配置文件来建立可以执行的系统调用的白名单,默认配置文件提供了一个可以执行的系统调用的长列表,并且还禁用了大约 44 个系统调用在您的 Docker 容器上运行。在阅读本书的章节时,您很可能一直在使用默认的seccomp配置文件。

Docker 将使用主机系统的seccomp配置,可以通过搜索/boot/config文件并检查CONFIG_SECCOMP选项是否设置为y来找到它:

cat /boot/config-'uname -r' | grep CONFIG_SECCOMP=

在运行我们的容器时,如果我们需要以无seccomp配置文件的方式运行容器,我们可以使用--security-opt选项,然后指定seccomp配置文件未确认。以下示例提供了此语法的示例:

docker run --security-opt seccomp=unconfined <image_name>

我们也可以创建我们自定义的配置文件。在这些情况下,我们将自定义配置文件的位置指定为seccomp的值,如下所示:

docker run --security-opt seccomp=new_default.json <image_name>

练习 11.06:开始使用 seccomp

在这个练习中,您将在当前环境中使用seccomp配置文件。您还将创建一个自定义配置文件,以阻止您的 Docker 镜像对文件执行更改所有权命令:

  1. 检查您运行的 Linux 系统是否已启用seccomp。然后可以确保它也在 Docker 上运行:
cat /boot/config-'uname -r' | grep CONFIG_SECCOMP=

在引导配置目录中搜索CONFIG_SECCOMP,它的值应为y

CONFIG_SECCOMP=y
  1. 使用docker info命令确保 Docker 正在使用配置文件:
docker info

在大多数情况下,您会注意到它正在运行默认配置文件:

…
Security Options:
  seccomp
   Profile: default
…

我们已经减少了docker info命令的输出,但是如果您查找Security Options标题,您应该会在系统上看到seccomp。如果您希望关闭此功能,您需要将CONFIG_SECCOMP的值更改为n

  1. 运行security-app,看看它是否也在运行时使用了seccomp配置文件。还要在/proc/1/status文件中搜索单词Seccomp
docker run -it security-app grep Seccomp /proc/1/status

值为2将显示容器一直在使用Seccomp配置文件运行:

Seccomp:    2
  1. 可能会有一些情况,您希望在不使用seccomp配置文件的情况下运行容器。您可能需要调试容器或运行在其上的应用程序。要在不使用任何seccomp配置文件的情况下运行容器,请使用docker run命令的--security-opt选项,并指定seccomp将不受限制。现在对您的security-app容器执行此操作,以查看结果:
docker run -it --security-opt seccomp=unconfined security-app grep Seccomp /proc/1/status

值为0将显示我们已成功关闭Seccomp

Seccomp:    0
  1. 创建自定义配置文件也并不是很困难,但可能需要一些额外的故障排除来完全理解语法。首先,测试security-app容器,看看我们是否可以在命令行中使用chown命令。然后,您的自定义配置文件将尝试阻止此命令的可用性:
docker run -it security-app sh
  1. 当前作为默认值运行的seccomp配置文件应该允许我们运行chown命令,因此在您可以访问运行的容器时,测试一下是否可以创建新文件并使用chown命令更改所有权。最后运行目录的长列表以验证更改是否已生效:
/# touch test.txt
/# chown 1001 test.txt
/# ls -l test.txt

这些命令应该提供类似以下的输出:

-rw-r--r--    1 1001      users        0 Oct 22 02:44 test.txt
  1. 通过修改默认配置文件来创建您的自定义配置文件。使用wget命令从本书的官方 GitHub 帐户下载自定义配置文件到您的系统上。使用以下命令将下载的自定义配置文件重命名为new_default.json
wget https://raw.githubusercontent.com/docker/docker/v1.12.3/profiles/seccomp/default.json -O new_default.json
  1. 使用文本编辑器打开new_default.json文件,尽管会有大量的配置列表,但要搜索控制chown的特定配置。在撰写本文时,这位于默认seccomp配置文件的第 59 行
59                 {  
60                         "name": "chown",
61                         "action": "SCMP_ACT_ALLOW",
62                         "args": []
63                 },

SCMP_ACT_ALLOW操作允许运行命令,但如果从new_default.json文件中删除第 5963 行,这应该会阻止我们的配置文件允许运行此命令。删除这些行并保存文件以供我们使用。

  1. 与此练习中步骤 4一样,使用--security-opt选项并指定使用我们编辑过的new_default.json文件来运行镜像:
docker run -it --security-opt seccomp=new_default.json security-app sh
  1. 执行与此练习中步骤 6相同的测试,如果我们的更改起作用,seccomp配置文件现在应该阻止我们运行chown命令:
/# touch test.txt
/# chown 1001 test.txt
chown: test.txt: Operation not permitted

只需进行最少量的工作,我们就成功创建了一个策略,以阻止恶意代码或攻击者更改容器中文件的所有权。虽然这只是一个非常基本的例子,但它让您了解了如何开始配置seccomp配置文件,以便根据您的需求进行特定的微调。

活动 11.01:为全景徒步应用程序设置 seccomp 配置文件

全景徒步应用程序正在顺利进行,但本章表明您需要确保用户在容器上可以执行的操作受到限制。如果容器可以被攻击者访问,您需要设置一些防范措施。在此活动中,您将创建一个seccomp配置文件,可用于应用程序中的服务,以阻止用户能够创建新目录,终止运行在容器上的进程,并最后,通过运行uname命令了解有关运行容器的更多详细信息。

完成此活动所需的步骤如下:

  1. 获取默认的seccomp配置文件的副本。

  2. 查找配置文件中将禁用mkdirkilluname命令的特定控件。

  3. 运行全景徒步应用程序的服务,并确保新配置文件应用于容器。

  4. 访问容器并验证您是否不再能够执行在seccomp配置文件中被阻止的mkdirkilluname命令。例如,如果我们在添加了新配置文件的新图像上执行mkdir命令,我们应该看到类似以下的输出:

$ mkdir test
mkdir: can't create directory 'test': Operation not permitted

注意

可以通过此链接找到此活动的解决方案。

活动 11.02:扫描全景徒步应用图像以查找漏洞

我们一直在使用其他用户或开发人员提供的全景徒步应用的基本图像。在这个活动中,您需要扫描图像以查找漏洞,并查看它们是否安全可用。

完成此活动需要采取的步骤如下:

  1. 决定使用哪种服务来扫描您的图像。

  2. 将图像加载到准备好进行扫描的服务中。

  3. 扫描图像并查看图像上是否存在任何漏洞。

  4. 验证图像是否安全可用。您应该能够在 Anchore 中执行评估检查,并看到类似以下输出的通过状态:

Image Digest: sha256:57d8817bac132c2fded9127673dd5bc7c3a976546
36ce35d8f7a05cad37d37b7
Full Tag: docker.io/dockerrepo/postgres-app:sample_tag
Status: pass
Last Eval: 2019-11-23T06:15:32Z
Policy ID: 2c53a13c-1765-11e8-82ef-23527761d060

注意

可以通过此链接找到此活动的解决方案。

总结

本章主要讨论了安全性,限制在使用 Docker 和我们的容器图像时的风险,以及我们如何在 Docker 安全方面迈出第一步。我们看到了以 root 用户身份运行容器进程的潜在风险,并了解了如何通过进行一些微小的更改来防止这些问题的出现,如果攻击者能够访问正在运行的容器。然后,我们更仔细地研究了如何通过使用图像签名证书来信任我们正在使用的图像,然后在我们的 Docker 图像上实施安全扫描。

在本章结束时,我们开始使用安全配置文件。我们使用了两种最常见的安全配置文件 - AppArmor 和seccomp - 在我们的 Docker 图像上实施了两种配置文件,并查看了减少容器特定访问权限的结果。下一章将探讨在运行和创建我们的 Docker 图像时实施最佳实践。

第十二章:最佳实践

概述

在本章中,您将学习一些在使用 Docker 和容器镜像时的最佳实践,这将使您能够监视和管理容器使用的资源,并限制其对主机系统的影响。您将分析 Docker 的最佳实践,并了解为什么重要的是只在一个容器中运行一个服务,确保您的容器是可扩展的和不可变的,并确保您的基础应用程序在短时间内启动。本章将通过使用 hadolintFROM:latest 命令和 dcvalidator 在应用程序和容器运行之前对您的 Dockerfilesdocker-compose.yml 文件进行检查,以帮助您强制执行这些最佳实践。

介绍

安全的前一章涵盖了一些 Docker 镜像和服务的最佳实践,这些实践已经遵循了这些最佳实践。我们确保我们的镜像和服务是安全的,并且限制了如果攻击者能够访问镜像时可以实现的内容。本章不仅将带您了解创建和运行 Docker 镜像的最佳实践,还将关注容器性能、配置我们的服务,并确保运行在其中的服务尽可能高效地运行。

我们将从深入了解如何监视和配置服务使用的资源开始,比如内存和 CPU 使用情况。然后,我们将带您了解一些您可以在项目中实施的重要实践,看看您如何创建 Docker 镜像以及在其上运行的应用程序。最后,本章将为您提供一些实用工具,用于测试您的 Dockerfilesdocker-compose.yml 文件,这将作为一种确保您遵循所述实践的方式。

本章展示了如何确保尽可能优化您的服务和容器,以确保它们从开发环境到生产环境都能无故障地运行。本章的目标是确保您的服务尽快启动,并尽可能高效地处理。本章提到的实践还确保了可重用性(也就是说,他们确保任何想要重用您的镜像或代码的人都可以这样做,并且可以随时了解具体发生了什么)。首先,以下部分讨论了如何使用容器资源。

使用容器资源

从传统服务器环境迁移到 Docker 的主要好处之一是,即使在转移到生产环境时,它使我们能够大大减少服务和应用程序的占用空间。然而,这并不意味着我们可以简单地在容器上运行任何东西,期望所有进程都能顺利完成执行。就像在独立服务器上运行服务时一样,我们需要确保我们的容器使用的资源(如 CPU、内存和磁盘输入输出)不会导致我们的生产环境或任何其他容器崩溃。通过监控开发系统中使用的资源,我们可以帮助优化流程,并确保最终用户在将其移入生产环境时体验到无缝操作。

通过测试我们的服务并监控资源使用情况,我们将能够了解运行应用程序所需的资源,并确保运行我们 Docker 镜像的主机具有足够的资源来运行我们的服务。最后,正如您将在接下来的章节中看到的,我们还可以限制容器可以访问的 CPU 和内存资源的数量。在开发运行在 Docker 上的服务时,我们需要在开发系统上测试这些服务,以确切了解它们在移入测试和生产环境时会发生什么。

当我们将多种不同的服务(如数据库、Web 服务器和 API 网关)组合在一起创建一个应用程序时,有些服务比其他服务更重要,在某些情况下,这些服务可能需要分配更多资源。然而,在 Docker 中,运行的容器默认情况下并没有真正的资源限制。

在之前的章节中,我们学习了使用 Swarm 和 Kubernetes 进行编排,这有助于在系统中分配资源,但本章的这一部分将教您一些基本工具来测试和监视您的资源。我们还将看看您可以如何配置您的容器,以不再使用默认可用的资源。

为了帮助我们在本章的这一部分,我们将创建一个新的镜像,该镜像将仅用于演示我们系统中的资源使用情况。在本节的第一部分中,我们将创建一个将添加一个名为 stress 的应用程序的镜像。stress 应用程序的主要功能是对我们的系统施加重负载。该镜像将允许我们查看在我们的主机系统上使用的资源,然后允许我们在运行 Docker 镜像时使用不同的选项来限制使用的资源。

注意

本章的这一部分将为您提供有关监视我们正在运行的 Docker 容器资源的简要指南。本章将仅涵盖一些简单的概念,因为我们将在本书的另一章节中专门提供有关监视容器指标的深入细节。

为了帮助我们查看正在运行的容器消耗的资源,Docker 提供了stats命令,作为我们正在运行的容器消耗资源的实时流。如果您希望限制流所呈现的数据,特别是如果您有大量正在运行的容器,您可以通过指定容器的名称或其 ID 来指定只提供某些容器:

docker stats <container_name|container_id>

docker stats命令的默认输出将为您提供容器的名称和 ID,容器正在使用的主机 CPU 和内存的百分比,容器正在发送和接收的数据,以及从主机存储中读取和写入的数据量:

NAME                CONTAINER           CPU %
docker-stress       c8cf5ad9b6eb        400.43%

以下部分将重点介绍如何使用docker stats命令来监视我们的资源。我们还将向stats命令提供格式控制,以提供我们需要的信息。

管理容器 CPU 资源

本章的这一部分将向您展示如何设置容器使用的 CPU 数量限制,因为没有限制的容器可能会占用主机服务器上所有可用的 CPU 资源。我们将着眼于优化我们正在运行的 Docker 容器,但实际上大量使用 CPU 的问题通常出现在基础设施或容器中运行的应用程序上。

当我们讨论 CPU 资源时,通常是指单个物理计算机芯片。如今,CPU 很可能有多个核心,更多的核心意味着更多的进程。但这并不意味着我们拥有无限的资源。当我们显示正在使用的 CPU 百分比时,除非您的系统只有一个 CPU 和一个核心,否则您很可能会看到超过 100%的 CPU 使用率。例如,如果您的系统的 CPU 中有四个核心,而您的容器正在利用所有的 CPU,您将看到 400%的值。

我们可以修改在我们的系统上运行的docker stats命令,通过提供--format选项来仅提供 CPU 使用情况的详细信息。这个选项允许我们指定我们需要的输出格式,因为我们可能只需要stats命令提供的一两个指标。以下示例配置了stats命令的输出以以table格式显示,只呈现容器的名称、ID 和正在使用的 CPU 百分比:

docker stats --format "table {{.Name}}\t{{.Container}}\t{{.CPUPerc}}"

如果我们没有运行 Docker 镜像,这个命令将提供一个包含以下三列的表格:

NAME                CONTAINER           CPU %

为了控制我们正在运行的容器使用的 CPU 核心数量,我们可以在docker run命令中使用--cpus选项。以下语法向我们展示了运行镜像,但通过使用--cpus选项限制了镜像可以访问的核心数量:

docker run --cpus 2 <docker-image>

更好的选择不是设置容器可以使用的核心数量,而是设置它可以共享的总量。Docker 提供了--cpushares-c选项来设置容器可以使用的处理能力的优先级。通过使用这个选项,这意味着在运行容器之前我们不需要知道主机机器有多少个核心。这也意味着我们可以将正在运行的容器转移到不同的主机系统,而不需要更改运行镜像的命令。

默认情况下,Docker 将为每个运行的容器分配 1,024 份份额。如果您将--cpushares值设置为256,它将拥有其他运行容器的四分之一的处理份额:

docker run --cpushares 256 <docker-image>

注意

如果系统上没有运行其他容器,即使您已将--cpushares值设置为256,容器也将被允许使用剩余的处理能力。

即使您的应用程序可能正在正常运行,查看减少其可用 CPU 量以及在正常运行时消耗多少的做法总是一个好习惯。

在下一个练习中,我们将使用stress应用程序来监视系统上的资源使用情况。

注意

请使用touch命令创建文件,并使用vim命令使用 vim 编辑器处理文件。

练习 12.01:了解 Docker 镜像上的 CPU 资源

在这个练习中,您将首先创建一个新的 Docker 镜像,这将帮助您在系统上生成一些资源。我们将演示如何在镜像上使用已安装的stress应用程序。该应用程序将允许您开始监视系统上的资源使用情况,以及允许您更改镜像使用的 CPU 资源数量:

  1. 创建一个新的Dockerfile并打开您喜欢的文本编辑器输入以下细节。您将使用 Ubuntu 作为基础来创建镜像,因为stress应用程序尚未作为易于在 Alpine 基础镜像上安装的软件包提供:
FROM ubuntu
RUN apt-get update && apt-get install stress
CMD stress $var
  1. 使用docker build命令的-t选项构建新镜像并将其标记为docker-stress
docker build -t docker-stress .
  1. 在运行新的docker-stress镜像之前,请先停止并删除所有其他容器,以确保结果不会被系统上运行的其他容器混淆:
docker rm -f $(docker -a -q)
  1. Dockerfile第 3 行上,您会注意到CMD指令正在运行 stress 应用程序,后面跟着$var变量。这将允许您通过环境变量直接向容器上运行的 stress 应用程序添加命令行选项,而无需每次想要更改功能时都构建新镜像。通过运行您的镜像并使用-e选项添加环境变量来测试这一点。将var="--cpu 4 --timeout 20"作为stress命令的命令行选项添加:
docker run --rm -it -e var="--cpu 4 --timeout 20" docker-stress

docker run命令已添加了var="--cpu 4 --timeout 20"变量,这将特别使用这些命令行选项运行stress命令。--cpu选项表示将使用系统的四个 CPU 或核心,--timeout选项将允许压力测试运行指定的秒数 - 在本例中为20

stress: info: [6] dispatching hogs: 4 cpu, 0 io, 0 vm, 0 hdd
stress: info: [6] successful run completed in 20s

注意

如果我们需要连续运行stress命令而不停止,我们将简单地不包括--timeout选项。我们的示例都包括timeout选项,因为我们不想忘记并持续使用运行主机系统的资源。

  1. 运行docker stats命令,查看这对主机系统的影响。使用--format选项限制所提供的输出,只提供 CPU 使用情况:
docker stats --format "table {{.Name}}\t{{.Container}}\t{{.CPUPerc}}"

除非您的系统上运行着一个容器,否则您应该只看到表头,类似于此处提供的输出:

NAME                CONTAINER           CPU %
  1. 在运行stats命令的同时,进入一个新的终端窗口,并再次运行docker-stress容器,就像本练习的步骤 4中一样。使用--name选项确保在使用docker stress命令时查看正确的镜像:
docker run --rm -it -e var="--cpu 4 --timeout 20" --name docker-stress docker-stress
  1. 返回到运行docker stats的终端。现在您应该看到一些输出呈现在您的表上。您的输出将与以下内容不同,因为您的系统上可能运行着不同数量的核心。以下输出显示我们的 CPU 百分比使用了 400%。运行该命令的系统有六个核心。它显示 stress 应用程序正在使用四个可用核心中的 100%:
NAME                CONTAINER           CPU %
docker-stress       c8cf5ad9b6eb        400.43%
  1. 再次运行docker-stress容器,这次将--cpu选项设置为8
docker run --rm -it -e var="--cpu 8 --timeout 20" --name docker-stress docker-stress

如您在以下统计输出中所见,我们已经达到了 Docker 容器几乎使用系统上所有六个核心的极限,为我们的系统上的次要进程留下了一小部分处理能力:

NAME                CONTAINER           CPU %
docker-stress       8946da6ffa90        599.44%
  1. 通过使用--cpus选项并指定要允许镜像使用的核心数量,来管理您的docker-stress镜像可以访问的核心数量。在以下命令中,将2设置为我们的容器被允许使用的核心数量:
docker run --rm -it -e var="--cpu 8 --timeout 20" --cpus 2 --name docker-stress docker-stress
  1. 返回到运行docker stats的终端。您将看到正在使用的 CPU 百分比不会超过 200%,显示 Docker 将资源使用限制在我们系统上仅有的两个核心:
NAME                CONTAINER           CPU %
docker-stress       79b32c67cbe3        208.91%

到目前为止,您只能一次在我们的系统上运行一个容器。这个练习的下一部分将允许您以分离模式运行两个容器。在这里,您将测试在运行的一个容器上使用--cpu-shares选项来限制它可以使用的核心数量。

  1. 如果您没有在终端窗口中运行docker stats,请像之前一样启动它,以便我们监视正在运行的进程:
docker stats --format "table {{.Name}}\t{{.Container}}\t{{.CPUPerc}}"
  1. 访问另一个终端窗口,并启动两个docker-stress容器 - docker-stress1docker-stress2。第一个将使用--timeout值为60,让压力应用程序运行 60 秒,但在这里,将--cpu-shares值限制为512
docker run --rm -dit -e var="--cpu 8 --timeout 60" --cpu-shares 512 --name docker-stress1 docker-stress

容器的 ID 将返回如下:

5f617e5abebabcbc4250380b2591c692a30b3daf481b6c8d7ab8a0d1840d395f

第二个容器将不受限制,但--timeout值只有30,所以它应该先完成:

docker run --rm -dit -e var="--cpu 8 --timeout 30" --name docker-stress2 docker-stress2

容器的 ID 将返回如下:

83712c28866dd289937a9c5fe4ea6c48a6863a7930ff663f3c251145e2fbb97a
  1. 回到运行docker stats的终端。您会看到两个容器正在运行。在以下输出中,我们可以看到名为docker-stress1docker-stress2的容器。docker-stress1容器被设置为只有512 CPU 份额,而其他容器正在运行。还可以观察到它只使用了第二个名为docker-stress2的容器的一半 CPU 资源:
NAME                CONTAINER           CPU %
docker-stress1      5f617e5abeba        190.25%
docker-stress2      83712c28866d        401.49%
  1. 当第二个容器完成后,docker-stress1容器的 CPU 百分比将被允许使用运行系统上几乎所有六个可用的核心:
NAME                CONTAINER           CPU %
stoic_keldysh       5f617e5abeba        598.66%

CPU 资源在确保应用程序以最佳状态运行方面起着重要作用。这个练习向您展示了在将容器部署到生产环境之前,监视和配置容器的处理能力有多么容易。接下来的部分将继续对容器的内存执行类似的监视和配置更改。

管理容器内存资源

就像我们可以监视和控制容器在系统上使用的 CPU 资源一样,我们也可以对内存的使用情况进行相同的操作。与 CPU 一样,默认情况下,运行的容器可以使用主机的所有内存,并且在某些情况下,如果没有限制,可能会导致系统变得不稳定。如果主机系统内核检测到没有足够的内存可用,它将显示内存不足异常并开始终止系统上的进程以释放内存。

好消息是,Docker 守护程序在您的系统上具有高优先级,因此内核将首先终止运行的容器,然后才会停止 Docker 守护程序的运行。这意味着如果高内存使用是由容器应用程序引起的,您的系统应该能够恢复。

注意

如果您的运行容器正在被关闭,您还需要确保已经测试了您的应用程序,以确保它对正在运行的进程的影响是有限的。

再次强调,docker stats命令为我们提供了关于内存使用情况的大量信息。它将输出容器正在使用的内存百分比,以及当前内存使用量与其能够使用的总内存量的比较。与之前一样,我们可以通过--format选项限制所呈现的输出。在以下命令中,我们通过.Name.Container.MemPerc.MemUsage属性,仅显示容器名称和 ID,以及内存百分比和内存使用量:

docker stats --format "table {{.Name}}\t{{.Container}}\t{{.MemPerc}}\t{{.MemUsage}}"

没有运行的容器,上述命令将显示以下输出:

NAME         CONTAINER          MEM %         MEM USAGE / LIMIT

如果我们想要限制或控制运行容器使用的内存量,我们有一些选项可供选择。其中一个可用的选项是--memory-m选项,它将设置运行容器可以使用的内存量的限制。在以下示例中,我们使用了--memory 512MB的语法来限制可用于镜像的内存量为512MB

docker run --memory 512MB <docker-image>

如果容器正在运行的主机系统也在使用交换空间作为可用内存的一部分,您还可以将内存从该容器分配为交换空间。这只需使用--memory-swap选项即可。这只能与--memory选项一起使用,正如我们在以下示例中所演示的。我们已将--memory-swap选项设置为1024MB,这是容器可用内存的总量,包括内存和交换内存。因此,在我们的示例中,交换空间中将有额外的512MB可用:

docker run --memory 512MB --memory-swap 1024MB <docker-image>

但需要记住,交换内存将被分配到磁盘,因此会比 RAM 更慢、响应更慢。

注意

--memory-swap选项需要设置为高于--memory选项的数字。如果设置为相同的数字,您将无法为运行的容器分配任何内存到交换空间。

另一个可用的选项,只有在需要确保运行容器始终可用时才能使用的是--oom-kill-disable选项。此选项会阻止内核在主机系统内存过低时杀死运行的容器。这应该只与--memory选项一起使用,以确保您设置了容器可用内存的限制。如果没有限制,--oom-kill-disable选项很容易使用主机系统上的所有内存:

docker run --memory 512MB --oom-kill-disable <docker-image>

尽管您的应用程序设计良好,但前面的配置为您提供了一些选项来控制运行容器使用的内存量。

下一节将为您提供在分析 Docker 镜像上的内存资源方面的实践经验。

练习 12.02:分析 Docker 镜像上的内存资源

这项练习将帮助您分析在主机系统上运行时活动容器如何使用内存。再次使用之前创建的docker-stress镜像,但这次使用选项仅在运行容器上使用内存。这个命令将允许我们实现一些可用的内存限制选项,以确保我们运行的容器不会使主机系统崩溃:

  1. 运行docker stats命令以显示所需的百分比内存和内存使用值的相关信息:
docker stats --format "table {{.Name}}\t{{.Container}}\t{{.MemPerc}}\t{{.MemUsage}}"

这个命令将提供以下类似的输出:

NAME        CONTAINER       MEM %         MEM USAGE / LIMIT
  1. 打开一个新的终端窗口再次运行stress命令。你的docker-stress镜像只有在使用--cpu选项时才会利用 CPU。使用以下命令中的--vm选项来启动你希望产生的工作进程数量以消耗内存。默认情况下,每个工作进程将消耗256MB
docker run --rm -it -e var="--vm 2 --timeout 20" --name docker-stress docker-stress

当你返回监视正在运行的容器时,内存使用量只达到了限制的 20%左右。这可能因不同系统而异。由于只有两个工作进程在运行,每个消耗 256MB,你应该只会看到内存使用量达到大约 500MB:

NAME            CONTAINER      MEM %      MEM USAGE / LIMIT
docker-stress   b8af08e4d79d   20.89%     415.4MiB / 1.943GiB
  1. 压力应用程序还有--vm-bytes选项来控制每个被产生的工作进程将消耗的字节数。输入以下命令,将每个工作进程设置为128MB。当你监视它时,它应该显示较低的使用量:
docker run --rm -it -e var="--vm 2 --vm-bytes 128MB --timeout 20" --name stocker-stress docker-stress

正如你所看到的,压力应用程序在推动内存使用量时并没有取得很大的进展。如果你想要使用系统上可用的全部 8GB RAM,你可以使用--vm 8 --vm-bytes 1,024 MB:

NAME            CONTAINER      MEM %    MEM USAGE / LIMIT
docker-stress   ad7630ed97b0   0.04%    904KiB / 1.943GiB
  1. 使用--memory选项减少docker-stress镜像可用的内存。在以下命令中,你会看到我们将正在运行的容器的可用内存限制为512MB
docker run --rm -it -e var="--vm 2 --timeout 20" --memory 512MB --name docker-stress docker-stress
  1. 返回到运行docker stats的终端,你会看到内存使用率飙升到了接近 100%。这并不是一件坏事,因为它只是你正在运行的容器分配的一小部分内存。在这种情况下,它是 512MB,仅为之前的四分之一:
NAME            CONTAINER      MEM %     MEM USAGE / LIMIT
docker-stress   bd84cf27e480   88.11%    451.1MiB / 512MiB
  1. 同时运行多个容器,看看我们的stats命令如何响应。在docker run命令中使用-d选项将容器作为守护进程在主机系统的后台运行。现在,两个docker-stress容器都将使用六个工作进程,但我们的第一个镜像,我们将其命名为docker-stress1,被限制在512MB的内存上,而我们的第二个镜像,名为docker-stress2,只运行 20 秒,将拥有无限的内存:
docker run --rm -dit -e var="--vm 6 --timeout 60" --memory 512MB --name docker-stress1 docker-stress
ca05e244d03009531a6a67045a5b1edbef09778737cab2aec7fa92eeaaa0c487
docker run --rm -dit -e var="--vm 6 --timeout 20" --name docker-stress2 docker-stress
6d9cbb966b776bb162a47f5e5ff3d88daee9b0304daa668fca5ff7ae1ee887ea
  1. 返回到运行docker stats的终端。你会看到只有一个容器,即docker-stress1容器,被限制在 512MB,而docker-stress2镜像被允许在更多的内存上运行:
NAME             CONTAINER       MEM %    MEM USAGE / LIMIT
docker-stress1   ca05e244d030    37.10%   190MiB / 512MiB
docker-stress2   6d9cbb966b77    31.03%   617.3MiB / 1.943GiB

如果你等待一会儿,docker-stress1镜像将被留下来独自运行:

NAME             CONTAINER      MEM %    MEM USAGE / LIMIT
docker-stress1   ca05e244d030   16.17%   82.77MiB / 512MiB

注意

我们在这里没有涵盖的一个选项是--memory-reservation选项。这也与--memory选项一起使用,并且需要设置为低于内存选项。这是一个软限制,当主机系统的内存不足时激活,但不能保证限制将被执行。

本章的这一部分帮助我们确定如何运行容器并监视使用情况,以便在将它们投入生产时,它们不会通过使用所有可用内存来停止主机系统。现在,您应该能够确定您的镜像正在使用多少内存,并在长时间运行或内存密集型进程出现问题时限制可用内存量。在下一节中,我们将看看我们的容器如何在主机系统磁盘上消耗设备的读写资源。

管理容器磁盘的读写资源

运行容器消耗的 CPU 和内存通常是环境运行不佳的最大罪魁祸首,但您的运行容器也可能存在问题,尝试读取或写入主机的磁盘驱动器过多。这很可能对 CPU 或内存问题影响较小,但如果大量数据被传输到主机系统的驱动器上,仍可能引起争用并减慢服务速度。

幸运的是,Docker 还为我们提供了一种控制运行容器执行读取和写入操作的方法。就像我们之前看到的那样,我们可以在docker run命令中使用多个选项来限制我们要读取或写入设备磁盘的数据量。

docker stats命令还允许我们查看传输到和从我们的运行容器的数据。它有一个专用列,可以使用docker stats命令中的BlockIO值将其添加到我们的表中,该值代表对我们的主机磁盘驱动器或目录的读写操作:

docker stats --format "table {{.Name}}\t{{.Container}}\t{{.BlockIO}}"

如果我们的系统上没有任何运行的容器,上述命令应该为我们提供以下输出:

NAME                CONTAINER           BLOCK I/O

如果我们需要限制正在运行的容器可以移动到主机系统磁盘存储的数据量,我们可以从使用--blkio-weight选项开始,该选项与我们的docker run命令一起使用。此选项代表块输入输出权重,允许我们为容器设置一个相对权重,介于101000之间,并且相对于系统上运行的所有其他容器。所有容器将被设置为相同比例的带宽,即 500。如果为任何容器提供值 0,则此选项将被关闭。

docker run --blkio-weight <value> <docker-image>

我们可以使用的下一个选项是--device-write-bps,它将限制指定的设备可用的特定写入带宽,以字节每秒的值为单位。特定设备是相对于容器在主机系统上使用的设备。此选项还有一个“每秒输入/输出(IOPS)”选项,也可以使用。以下语法提供了该选项的基本用法,其中限制值设置为 MB 的数值:

docker run --device-write-bps <device>:<limit> <docker-image>

就像有一种方法可以限制写入进程到主机系统的磁盘一样,也有一种选项可以限制可用的读取吞吐量。同样,它还有一个“每秒输入/输出(IOPS)”选项,可以用来限制可以从正在运行的容器中读取的数据量。以下示例使用--device-read-bps选项作为docker run命令的一部分:

docker run --device-read-bps <device>:<limit> <docker-image>

如果您遵守容器最佳实践,磁盘输入或输出的过度消耗不应该是太大的问题。尽管如此,没有理由认为这不会给您造成任何问题。就像您已经处理过 CPU 和内存一样,您的磁盘输入和输出应该在将服务实施到生产环境之前在运行的容器上进行测试。

练习 12.03:理解磁盘读写

这个练习将使您熟悉查看正在运行的容器的磁盘读写。它将允许您通过在运行时使用可用的选项来配置磁盘使用速度的限制来开始运行您的容器:

  1. 打开一个新的终端窗口并运行以下命令:
docker stats --format "table {{.Name}}\t{{.Container}}\t{{.BlockIO}}" 

docker stats命令与BlockIO选项帮助我们监视从我们的容器到主机系统磁盘的输入和输出级别。

  1. 启动容器以从 bash 命令行访问它。在运行的docker-stress镜像上直接执行一些测试。stress 应用程序确实为您提供了一些选项,以操纵容器和主机系统上的磁盘利用率,但它仅限于磁盘写入:
docker run -it --rm --name docker-stress docker-stress /bin/bash
  1. 与 CPU 和内存使用情况不同,块输入和输出显示容器使用的总量,因此它不会随着运行容器执行更多更改而动态变化。回到运行docker stats的终端。您应该看到输入和输出都为0B
NAME                CONTAINER           BLOCK I/O
docker-stress       0b52a034f814        0B / 0B
  1. 在这种情况下,您将使用 bash shell,因为它可以访问time命令以查看每个进程需要多长时间。使用dd命令,这是一个用于复制文件系统和备份的 Unix 命令。在以下选项中,使用if(输入文件)选项创建我们的/dev/zero目录的副本,并使用of(输出文件)选项将其输出到disk.out文件。bs选项是块大小或应该一次读取的数据量,count是要读取的总块数。最后,将oflag值设置为direct,这意味着复制将避免缓冲区缓存,因此您将看到磁盘读取和写入的真实值:
time dd if=/dev/zero of=disk.out bs=1M count=10 oflag=direct
10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.0087094 s, 1.2 GB/s
real    0m0.010s
user    0m0.000s
sys     0m0.007s
  1. 回到运行docker stats命令的终端。您将看到超过 10MB 的数据发送到主机系统的磁盘。与 CPU 和内存不同,传输完成后,您不会看到此数据值下降:
NAME                CONTAINER           BLOCK I/O
docker-stress       0b52a034f814        0B / 10.5MB

您还会注意到步骤 4中的命令几乎立即完成,time命令显示实际只需0.01s即可完成。您将看到如果限制可以写入磁盘的数据量会发生什么,但首先退出运行的容器,以便它不再存在于我们的系统中。

  1. 要再次启动我们的docker-stress容器,请将--device-write-bps选项设置为每秒1MB/dev/sda设备驱动器上:
docker run -it --rm --device-write-bps /dev/sda:1mb --name docker-stress docker-stress /bin/bash
  1. 再次运行dd命令,之前加上time命令,以测试需要多长时间。您会看到该命令花费的时间比步骤 4中的时间长得多。dd命令再次设置为复制1MB块,10次:
time dd if=/dev/zero of=test.out bs=1M count=10 oflag=direct

因为容器限制为每秒只能写入 1MB,所以该命令需要 10 秒,如下面的输出所示:

10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 10.0043 s, 1.0 MB/s
real    0m10.006s
user    0m0.000s
sys     0m0.004s

我们已经能够很容易地看到我们的运行容器如何影响底层主机系统,特别是在使用磁盘读写时。我们还能够看到我们如何轻松地限制可以写入设备的数据量,以便在运行容器之间减少争用。在下一节中,我们将快速回答一个问题,即如果您正在使用docker-compose,您需要做什么,并且限制容器使用的资源数量。

容器资源和 Docker Compose

诸如 Kubernetes 和 Swarm 之类的编排器在控制和运行资源以及在需要额外资源时启动新主机方面发挥了重要作用。但是,如果您在系统或测试环境中运行docker-compose,您该怎么办呢?幸运的是,前面提到的资源配置也适用于docker-compose

在我们的docker-compose.yml文件中,在我们的服务下,我们可以在deploy配置下使用resources选项,并为我们的服务指定资源限制。就像我们一直在使用--cpus--cpu_shares--memory等选项一样,我们在我们的docker-compose.yml文件中也会使用相同的选项,如cpuscpu_sharesmemory

以下代码块中的示例compose文件部署了我们在本章中一直在使用的docker-stress镜像。如果我们看第 8 行,我们可以看到deploy语句,后面是resources语句。这是我们可以为我们的容器设置限制的地方。就像我们在前面的部分中所做的那样,我们在第 11 行上将cpus设置为2,在第 12 行上将memory设置为256MB

1 version: '3'
2 services:
3   app:
4     container_name: docker-stress
5     build: .
6     environment:
7       var: "--cpu 2 --vm 6 --timeout 20"
8     deploy:
9       resources:
10         limits:
11           cpus: '2'
12           memory: 256M

尽管我们只是简单地涉及了这个主题,但前面涵盖资源使用的部分应该指导您如何在docker-compose.yml文件中分配资源。这就是我们关于 Docker 容器资源使用的部分的结束。从这里开始,我们将继续研究创建我们的Dockerfiles的最佳实践,以及如何开始使用不同的应用程序来确保我们遵守这些最佳实践。

Docker 最佳实践

随着我们的容器和服务规模和复杂性的增长,重要的是要确保在创建 Docker 镜像时我们遵循最佳实践。对于我们在 Docker 镜像上运行的应用程序也是如此。在本章的后面,我们将查看我们的Dockerfilesdocker-compose.yml文件,这将分析我们的文件中的错误和最佳实践,从而让您更清楚地了解。与此同时,让我们来看看在创建 Docker 镜像和应用程序与之配合工作时需要牢记的一些更重要的最佳实践。

注意

本章可能涵盖了一些之前章节的内容,但我们将能够为您提供更多信息和清晰解释为什么我们要使用这些实践。

在接下来的部分,我们将介绍一些在创建服务和容器时应该遵循的常见最佳实践。

每个容器只运行一个服务

在现代微服务架构中,我们需要记住每个容器只应安装一个服务。容器的主要进程由Dockerfile末尾的ENTRYPOINTCMD指令设置。

您在容器中安装的服务很容易运行多个进程,但为了充分利用 Docker 和微服务的优势,您应该每个容器只运行一个服务。更进一步地说,您的容器应该只负责一个单一的功能,如果它负责的事情超过一个,那么它应该拆分成不同的服务。

通过限制每个容器的功能,我们有效地减少了镜像使用的资源,并可能减小了镜像的大小。正如我们在上一章中看到的,这也将减少攻击者在获得运行中的容器访问权限时能够执行任何不应该执行的操作的机会。这也意味着,如果容器因某种原因停止工作,对环境中运行的其他应用程序的影响有限,服务将更容易恢复。

基础镜像

当我们为我们的容器选择基础镜像时,我们需要做的第一件事之一是确保我们使用的是最新的镜像。还要做一些研究,确保您不使用安装了许多不需要的额外应用程序的镜像。您可能会发现,受特定语言支持的基础镜像或特定焦点将限制所需的镜像大小,从而限制您在创建镜像时需要安装的内容。

这就是为什么我们使用受 PostgreSQL 支持的 Docker 镜像,而不是在构建时在镜像上安装应用程序。受 PostgreSQL 支持的镜像确保它是安全的,并且运行在最新版本,并确保我们不在镜像上运行不需要的应用程序。

在为我们的Dockerfile指定基础镜像时,我们需要确保还指定了特定版本,而不是让 Docker 简单地使用latest镜像。另外,确保您不是从不是来自值得信赖的提供者的存储库或注册表中拉取镜像。

如果您已经使用 Docker 一段时间,可能已经遇到了MAINTAINER指令,您可以在其中指定生成图像的作者。现在这已经被弃用,但您仍然可以使用LABEL指令来提供这些细节,就像我们在以下语法中所做的那样:

LABEL maintainer="myemailaddress@emaildomain.com"

安装应用程序和语言

当您在镜像上安装应用程序时,永远记住不需要执行apt-get updatedist-upgrade。如果您需要以这种方式升级镜像版本,应该考虑使用不同的镜像。如果您使用apt-getapk安装应用程序,请确保指定您需要的特定版本,因为您不希望安装新的或未经测试的版本。

在安装软件包时,确保使用-y开关,以确保构建不会停止并要求用户提示。另外,您还应该使用--no-install-recommends,因为您不希望安装大量您的软件包管理器建议的不需要的应用程序。此外,如果您使用基于 Debian 的容器,请确保使用apt-getapt-cache,因为apt命令专门用于用户交互,而不是用于脚本化安装。

如果您正在从其他形式安装应用程序,比如从代码构建应用程序,请确保清理安装文件,以再次减小您创建的镜像的大小。同样,如果您正在使用apt-get,您还应该删除/var/lib/apt/lists/中的列表,以清理安装文件并减小容器镜像的大小。

运行命令和执行任务

当我们的镜像正在创建时,通常需要在我们的Dockerfile中执行一些任务,以准备好我们的服务运行的环境。始终确保您不使用sudo命令,因为这可能会导致一些意外的结果。如果需要以 root 身份运行命令,您的基础镜像很可能正在以 root 用户身份运行;只需确保您创建一个单独的用户来运行您的应用程序和服务,并且在构建完成之前容器已经切换到所需的用户。

确保您使用WORKDIR切换到不同的目录,而不是运行指定长路径的指令,因为这可能会让用户难以阅读。对于CMDENTRYPOINT参数,请使用JSON表示法,并始终确保只有一个CMDENTRYPOINT指令。

容器需要是不可变的和无状态的

我们需要确保我们的容器和运行在其中的服务是不可变的。我们不能像传统服务器那样对待容器,特别是在运行容器上更新应用程序的服务器。您应该能够从代码更新容器并部署它,而无需访问它。

当我们说不可变时,我们指的是容器在其生命周期内不会被修改,不会进行更新、补丁或配置更改。您的代码或更新的任何更改都应该通过构建新镜像然后部署到您的环境中来实现。这样做可以使部署更安全,如果升级出现任何问题,您只需重新部署旧版本的镜像。这也意味着您在所有环境中运行相同的镜像,确保您的环境尽可能相同。

当我们谈论容器需要是无状态的时候,这意味着运行容器所需的任何数据都应该在容器外部运行。文件存储也应该在容器外部,可能在云存储上或者使用挂载卷。将数据从容器中移除意味着容器可以在任何时候被干净地关闭和销毁,而不必担心数据丢失。当创建一个新的容器来替换旧的容器时,它只需连接到原始数据存储。

设计应用程序以实现高可用性和可扩展性

在微服务架构中使用容器旨在使您的应用程序能够扩展到多个实例。因此,在开发您的应用程序时,您应该预期可能会出现许多实例同时部署的情况,需要在需要时进行上下扩展。当容器负载较重时,您的服务运行和完成也不应该有问题。

当您的服务需要因为增加的请求而扩展时,应用程序需要启动的时间就成为一个重要问题。在将您的服务部署到生产环境之前,您需要确保启动时间很快,以确保系统能够更有效地扩展而不会给用户的服务造成任何延迟。为了确保您的服务符合行业最佳实践,您的服务应该在不到 10 秒内启动,但不到 20 秒也是可以接受的。

正如我们在前一节中所看到的,改善应用程序的启动时间不仅仅是提供更多的 CPU 和内存资源的问题。我们需要确保我们容器中的应用程序能够高效运行,如果它们启动和运行特定进程的时间太长,可能是因为一个应用程序执行了太多的任务。

图像和容器需要适当地打标签

我们在《第三章》《管理您的 Docker 镜像》中详细介绍了这个主题,并明确指出,我们需要考虑如何命名和标记我们的图像,特别是当我们开始与更大的开发团队合作时。为了让所有用户能够理解图像的功能,并了解部署到环境中的版本,需要在团队开始大部分工作之前决定并达成一致的相关标记和命名策略。

图像和容器名称需要与它们运行的应用程序相关,因为模糊的名称可能会引起混淆。还必须制定一个版本的约定标准,以确保任何用户都可以确定在特定环境中运行的版本以及最新稳定版本是什么版本。正如我们在第三章中提到的管理您的 Docker 镜像中所提到的,尽量不要使用latest,而是选择语义版本控制系统或 Git 存储库commit哈希,用户可以参考文档或构建环境,以确保他们拥有最新版本的镜像。

配置和秘密

环境变量和秘密不应该内置到您的 Docker 镜像中。通过这样做,您违反了可重用图像的规则。使用您的秘密凭据构建图像也是一种安全风险,因为它们将存储在图像层中,因此任何能够拉取图像的人都将能够看到凭据。

在为应用程序设置配置时,可能需要根据环境的不同进行更改,因此重要的是要记住,当需要时,您需要能够动态更改这些配置。这可能包括应用程序所编写的语言的特定配置,甚至是应用程序需要连接到的数据库。我们之前提到过,如果您正在配置应用程序作为您的Dockerfile的一部分,这将使其难以更改,您可能需要为您希望部署图像的每个环境创建一个特定的Dockerfile

配置图像的一种方法,就像我们在docker-stress图像中看到的那样,是使用在运行图像时在命令行上设置的环境变量。如果未提供变量,则入口点或命令应包含默认值。这意味着即使未提供额外的变量,容器仍将启动和运行:

docker run -e var="<variable_name>" <image_name>

通过这样做,我们使我们的配置更加动态,但是当您有一个更大或更复杂的配置时,这可能会限制您的配置。环境变量可以很容易地从您的docker run命令转移到docker-compose,然后在 Swarm 或 Kubernetes 中使用。

对于较大的配置,您可能希望通过 Docker 卷挂载配置文件。这意味着您可以设置一个配置文件并在系统上轻松测试运行,然后如果需要转移到诸如 Kubernetes 或 Swarm 之类的编排系统,或者外部配置管理解决方案,您可以轻松将其转换为配置映射。

如果我们想要在本章中使用的docker-stress镜像中实现这一点,可以修改为使用配置文件来挂载我们想要运行的值。在以下示例中,我们修改了Dockerfile以设置第 3 行运行一个脚本,该脚本将代替我们运行stress命令:

1 FROM ubuntu
2 RUN apt-get update && apt-get install stress
3 CMD ["sh","/tmp/stress_test.sh"]

这意味着我们可以构建 Docker 镜像,并使其随时准备好供我们使用。我们只需要一个脚本,我们会挂载在/tmp目录中运行。我们可以使用以下示例:

1 #!/bin/bash
2 
3 /usr/bin/stress --cpu 8 --timeout 20 --vm 6 --timeout 60

这说明了将我们的值从环境变量移动到文件的想法。然后,我们将执行以下操作来运行容器和 stress 应用程序,知道如果我们想要更改stress命令使用的变量,我们只需要对我们挂载的文件进行微小的更改:

docker run --rm -it -v ${PWD}/stress_test.sh:/tmp/stress_test.sh docker-stress

注意

阅读完这些最佳实践清单时,你可能会认为我们违背了很多内容,但请记住,我们在很多情况下都这样做是为了演示一个流程或想法。

使您的镜像尽可能精简和小

第三章管理您的 Docker 镜像,还让我们尽可能地减小了镜像的大小。我们发现通过减小镜像的大小,可以更快地构建镜像。它们也可以更快地被拉取并在我们的系统上运行。在我们的容器上安装的任何不必要的软件或应用程序都会占用额外的空间和资源,并可能因此减慢我们的服务速度。

正如我们在第十一章Docker 安全中所做的那样,使用 Anchore Engine 这样的应用程序显示了我们可以审计我们的镜像以查看其内容,以及安装在其中的应用程序。这是一种简单的方法,可以确保我们减小镜像的大小,使其尽可能精简。

您现在已经了解了您应该在容器镜像和服务中使用的最佳实践。本章的以下部分将帮助您通过使用应用程序来验证您的Dockerfilesdocker-compose.yml是否按照应有的方式创建来强制执行其中的一些最佳实践。

在您的代码中强制执行 Docker 最佳实践

就像我们在开发应用程序时寻求使我们的编码更加简单一样,我们可以使用外部服务和测试来确保我们的 Docker 镜像遵守最佳实践。在本章的以下部分,我们将使用三种工具来确保我们的Dockerfilesdocker-compose.yml文件遵守最佳实践,并确保我们在构建 Docker 镜像时不会引入潜在问题。

这些工具将使用起来非常简单,并提供强大的功能。我们将首先使用hadolint在我们的系统上直接对我们的Dockerfiles进行代码检查,它将作为一个独立的 Docker 镜像运行,我们将把我们的Dockerfiles输入到其中。然后我们将看一下FROM:latest,这是一个在线服务,提供一些基本功能来帮助我们找出Dockerfiles中的问题。最后,我们将看一下Docker Compose ValidatorDCValidator),它将执行类似的功能,但在这种情况下,我们将对我们的docker-compose.yml文件进行代码检查,以帮助找出潜在问题。

通过在构建和部署我们的镜像之前使用这些工具,我们希望减少我们的 Docker 镜像的构建时间,减少我们引入的错误数量,可能减少我们的 Docker 镜像的大小,并帮助我们更多地了解和执行 Docker 最佳实践。

使用 Docker Linter 检查您的镜像

包含本书所有代码的 GitHub 存储库还包括将与构建的 Docker 镜像进行比较的测试。另一方面,代码检查器将分析您的代码,并在构建镜像之前寻找潜在错误。在本章的这一部分,我们正在寻找我们的Dockerfiles中的潜在问题,特别是使用一个名为hadolint的应用程序。

名称hadolintHaskell Dockerfile Linter的缩写,并带有自己的 Docker 镜像,允许您拉取该镜像,然后将您的Dockerfile发送到正在运行的镜像以进行测试。即使您的Dockerfile相对较小,并且构建和运行没有任何问题,hadolint通常会提供许多建议,并指出Dockerfile中的缺陷,以及可能在将来出现问题的潜在问题。

要在您的Dockerfiles上运行hadolint,您需要在您的系统上有hadolint Docker 镜像。正如您现在所知,这只是运行docker pull命令并使用所需镜像的名称和存储库的问题。在这种情况下,存储库和镜像都称为hadolint

docker pull hadolint/hadolint

然后,您可以简单地运行hadolint镜像,并使用小于(<)符号将您的Dockerfile指向它,就像我们在以下示例中所做的那样:

docker run hadolint/hadolint < Dockerfile

如果您足够幸运,没有任何问题与您的Dockerfile,您不应该看到前面命令的任何输出。如果有需要忽略特定警告的情况,您可以使用--ignore选项,后跟触发警告的特定规则 ID:

docker run hadolint/hadolint hadolint --ignore <hadolint_rule_id> - < Dockerfile

如果您需要忽略一些警告,尝试在命令行中实现可能会有点复杂,因此hadolint还有设置配置文件的选项。hadolint配置文件仅限于忽略警告并提供受信任存储库的列表。您还可以使用 YAML 格式设置包含您忽略警告列表的配置文件。然后,hadolint将需要在运行的镜像上挂载此文件,以便应用程序使用它,因为它将在应用程序的主目录中查找.hadolint.yml配置文件位置:

docker run --rm -i -v ${PWD}/.hadolint.yml:/.hadolint.yaml hadolint/hadolint < Dockerfile

hadolint是用于清理您的Dockerfiles的更好的应用程序之一,并且可以轻松地作为构建和部署流水线的一部分进行自动化。作为替代方案,我们还将看一下名为FROM:latest的在线应用程序。这个应用程序是一个基于 Web 的服务,不提供与hadolint相同的功能,但允许您轻松地将您的Dockerfile代码复制粘贴到在线编辑器中,并获得有关Dockerfile是否符合最佳实践的反馈。

练习 12.04:清理您的 Dockerfile

此练习将帮助您了解如何在系统上访问和运行hadolint,以帮助您强制执行Dockerfiles的最佳实践。我们还将使用一个名为FROM:latest的在线Dockerfile linter 来比较我们收到的警告:

  1. 使用以下docker pull命令从hadolint存储库中拉取镜像:
docker pull hadolint/hadolint
  1. 您已经准备好一个Dockerfile,其中包含您在本章早些时候用来测试和管理资源的docker-stress镜像。运行hadolint镜像以对此Dockerfile进行检查,或者对任何其他Dockerfile进行检查,并使用小于(<)符号发送到Dockerfile,如以下命令所示:
docker run --rm -i hadolint/hadolint < Dockerfile

从以下输出中可以看出,即使我们的docker-stress镜像相对较小,hadolint也提供了许多不同的方式,可以改善性能并帮助我们的镜像遵守最佳实践:

/dev/stdin:1 DL3006 Always tag the version of an image explicitly
/dev/stdin:2 DL3008 Pin versions in apt get install. Instead of 
'apt-get install <package>' use 'apt-get install 
<package>=<version>'
/dev/stdin:2 DL3009 Delete the apt-get lists after installing 
something
/dev/stdin:2 DL3015 Avoid additional packages by specifying 
'--no-install-recommends'
/dev/stdin:2 DL3014 Use the '-y' switch to avoid manual input 
'apt-get -y install <package>'
/dev/stdin:3 DL3025 Use arguments JSON notation for CMD 
and ENTRYPOINT arguments

注意

如果您的Dockerfile通过hadolint成功运行,并且没有发现任何问题,则在命令行上不会向用户呈现任何输出。

  1. hadolint还为您提供了使用--ignore选项来抑制不同检查的选项。在以下命令中,我们选择忽略DL3008警告,该警告建议您将安装的应用程序固定到特定版本号。执行docker run命令以抑制DL3008警告。请注意,在指定运行的镜像名称之后,您需要提供完整的hadolint命令,以及在提供Dockerfile之前提供额外的破折号(-):
docker run --rm -i hadolint/hadolint hadolint --ignore DL3008 - < Dockerfile

您应该获得以下类似的输出:

/dev/stdin:1 DL3006 Always tag the version of an image explicitly
/dev/stdin:2 DL3009 Delete the apt-get lists after installing 
something
/dev/stdin:2 DL3015 Avoid additional packages by specifying 
'--no-install-recommends'
/dev/stdin:2 DL3014 Use the '-y' switch to avoid manual input 
'apt-get -y install <package>'
/dev/stdin:3 DL3025 Use arguments JSON notation for CMD and 
ENTRYPOINT arguments
  1. hadolint还允许您创建一个配置文件,以添加要忽略的任何警告,并在命令行上指定它们。使用touch命令创建一个名为.hadolint.yml的文件:
touch .hadolint.yml
  1. 使用文本编辑器打开配置文件,并在ignored字段下输入您希望忽略的任何警告。如您所见,您还可以添加一个trustedRegistries字段,在其中列出您将从中拉取镜像的所有注册表。请注意,如果您的镜像不来自配置文件中列出的注册表之一,hadolint将提供额外的警告:
ignored:
  - DL3006
  - DL3008
  - DL3009
  - DL3015
  - DL3014
trustedRegistries:
  - docker.io
  1. hadolint将在用户的主目录中查找您的配置文件。由于您正在作为 Docker 镜像运行hadolint,因此在执行docker run命令时,使用-v选项将文件从当前位置挂载到运行镜像的主目录上:
docker run --rm -i -v ${PWD}/.hadolint.yml:/.hadolint.yaml hadolint/hadolint < Dockerfile

该命令将输出如下:

/dev/stdin:3 DL3025 Use arguments JSON notation for CMD and ENTRYPOINT arguments

注意

hadolint的源代码存储库提供了所有警告的列表,以及如何在您的Dockerfile中解决这些问题的详细信息。如果您还没有这样做,可以随意查看 Hadolint 维基页面github.com/hadolint/hadolint/wiki

  1. 最后,hadolint还允许您选择以 JSON 格式输出检查结果。再次,我们需要在命令行中添加一些额外的值。在命令行中,在将您的Dockerfile添加和解析到hadolint之前,添加额外的命令行选项hadolint -f json。在以下命令中,您还需要安装jq软件包:
docker run --rm -i -v ${PWD}/.hadolint.yml:/.hadolint.yaml hadolint/hadolint hadolint -f json - < Dockerfile | jq

您应该得到以下输出:

[
  {
    "line": 3,
    "code": "DL3025",
    "message": "Use arguments JSON notation for CMD and ENTRYPOINT arguments",
    "column": 1,
    "file": "/dev/stdin",
    "level": "warning"
  }
]

注意

hadolint可以轻松集成到您的构建流水线中,在构建之前对您的Dockerfiles进行检查。如果您有兴趣直接将hadolint应用程序安装到您的系统上,而不是使用 Docker 镜像,您可以通过克隆以下 GitHub 存储库来实现github.com/hadolint/hadolint

hadolint并不是您可以用来确保您的Dockerfiles遵守最佳实践的唯一应用程序。这个练习的下一步将介绍一个名为FROM:latest的在线服务,也可以帮助强制执行Dockerfiles的最佳实践。

  1. 要使用FROM:latest,打开您喜欢的网络浏览器,输入以下 URL:
https://www.fromlatest.io

当网页加载时,您应该看到类似以下截图的页面。在网页的左侧,您应该看到输入了一个示例Dockerfile,在网页的右侧,您应该看到一个潜在问题或优化Dockerfile的方法列表。右侧列出的每个项目都有一个下拉菜单,以向用户提供更多详细信息:

图 12.1:FROM:latest 网站的截图,显示输入了一个示例 Dockerfile

图 12.1:FROM:latest 网站的截图,显示输入了一个示例 Dockerfile

  1. 在这个练习的前一部分中,我们将使用docker-stress镜像的Dockerfile。要将其与FROM:latest一起使用,请将以下代码行复制到网页左侧,覆盖网站提供的示例Dockerfile
FROM ubuntu
RUN apt-get update && apt-get install stress
CMD stress $var

一旦您将Dockerfile代码发布到网页上,页面将开始分析命令。正如您从以下截图中所看到的,它将提供有关如何解决潜在问题并优化Dockerfile以使镜像构建更快的详细信息:

图 12.2:我们的 docker-stress 镜像输入的 Dockerfile

图 12.2:我们的 docker-stress 镜像输入的 Dockerfile

hadolintFROM latest都提供了易于使用的选项,以帮助您确保您的Dockerfiles遵守最佳实践。下一个练习将介绍一种类似的方法,用于检查您的docker-compose.yml文件,以确保它们也可以无故障运行,并且不会引入任何不良实践。

练习 12.05:验证您的 docker-compose.yml 文件

Docker 已经有一个工具来验证您的docker-compose.yml文件,但是内置的验证器无法捕捉到docker-compose文件中的所有问题,包括拼写错误、相同的端口分配给不同的服务或重复的键。我们可以使用dcvalidator来查找诸如拼写错误、重复的键和分配给数字服务的端口等问题。

要执行以下练习,您需要在系统上安装 Git 和最新版本的 Python 3。在开始之前,您不会被引导如何执行安装,但在开始之前需要这些项目。

  1. 要开始使用dcvalidator,请克隆该项目的 GitHub 存储库。如果您还没有这样做,您需要运行以下命令来克隆存储库:
git clone https://github.com/serviceprototypinglab/dcvalidator.git
  1. 命令行应用程序只需要 Python 3 来运行,但是您需要确保首先安装了所有的依赖项,因此请切换到您刚刚克隆的存储库的dcvalidator目录:
cd dcvalidator
  1. 安装dcvalidator的依赖项很容易,您的系统很可能已经安装了大部分依赖项。要安装依赖项,请在dcvalidator目录中使用pip3 install命令,并使用-r选项来使用服务器目录中的requirments.txt文件:
pip3 install -r server/requirments.txt
  1. 从头开始创建一个docker-compose文件,该文件将使用本章中已经创建的一些镜像。使用touch命令创建一个docker-compose.yml文件:
touch docker-compose.yml
  1. 打开您喜欢的文本编辑器来编辑docker-compose文件。确保您还包括我们故意添加到文件中的错误,以确保dcvalidator能够发现这些错误,并且我们将使用本章前面创建的docker-stress镜像。确保您逐字复制此文件,因为我们正在努力确保在我们的docker-compose.yml文件中强制出现一些错误:
version: '3'
services:
  app:
    container_name: docker-stress-20
    build: .
    environment:
      var: "--cpu 2 --vm 6 --timeout 20"
    ports:
      - 80:8080
      - 80:8080
    dns: 8.8.8
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
  app2:
    container_name: docker-stress-30
    build: .
    environment:
      var: "--cpu 2 --vm 6 --timeout 30"
    dxeploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
  1. 使用-f选项运行validator-cli.py脚本来解析我们想要验证的特定文件——在以下命令行中,即docker-compose.yml文件。然后,-fi选项允许您指定可用于验证我们的compose文件的过滤器。在以下代码中,我们正在使用validator-cli目前可用的所有过滤器:
python3 validator-cli.py -f docker-compose.yml -fi 'Duplicate Keys,Duplicate ports,Typing mistakes,DNS,Duplicate expose'

您应该获得以下类似的输出:

Warning: no kafka support
loading compose files....
checking consistency...
syntax is ok
= type: docker-compose
- service:app
Duplicate ports in service app port 80
=================== ERROR ===================
Under service: app
The DNS is not appropriate!
=============================================
- service:app2
=================== ERROR ===================
I can not find 'dxeploy' tag under 'app2' service. 
Maybe you can use: 
deploy
=============================================
services: 2
labels:
time: 0.0s

正如预期的那样,validator-cli.py已经能够找到相当多的错误。它显示您在应用服务中分配了重复的端口,并且您设置的 DNS 也是不正确的。App2显示了一些拼写错误,并建议我们可以使用不同的值。

注意

在这一点上,您需要指定您希望您的docker-compose.yml文件针对哪些过滤器进行验证,但这将随着即将发布的版本而改变。

  1. 您会记得,我们使用了一个docker-compose文件来安装 Anchore 镜像扫描程序。当您有compose文件的 URL 位置时,使用-u选项传递文件的 URL 以进行验证。在这种情况下,它位于 Packt GitHub 账户上:
python3 validator-cli.py -u https://github.com/PacktWorkshops/The-Docker-Workshop/blob/master/Chapter11/Exercise11.03/docker-compose.yaml -fi 'Duplicate Keys,Duplicate ports,Typing mistakes,DNS,Duplicate expose'

如您在以下代码块中所见,dcvalidator没有在docker-compose.yml文件中发现任何错误:

Warning: no kafka support
discard cache...
loading compose files....
checking consistency...
syntax is ok
= type: docker-compose=
- service:engine-api
- service:engine-catalog
- service:engine-simpleq
- service:engine-policy-engine
- service:engine-analyzer
- service:anchore-db
services: 6
labels:
time: 0.6s

如您所见,Docker Compose 验证器相当基本,但它可以发现我们可能错过的docker-compose.yml文件中的一些错误。特别是在我们有一个较大的文件时,如果我们在尝试部署环境之前可能错过了一些较小的错误,这可能是可能的。这已经将我们带到了本章的这一部分的结束,我们一直在使用一些自动化流程和应用程序来验证和清理我们的Dockerfilesdocker-compose.yml文件。

现在,让我们继续进行活动,这将帮助您测试对本章的理解。在接下来的活动中,您将查看 Panoramic Trekking App 上运行的一个服务使用的资源。

活动 12.01:查看 Panoramic Trekking App 使用的资源

在本章的前面,我们看了一下我们正在运行的容器在我们的主机系统上消耗了多少资源。在这个活动中,您将选择全景徒步应用程序上运行的服务之一,使用其默认配置运行容器,并查看它使用了什么 CPU 和内存资源。然后,再次运行容器,更改 CPU 和内存配置,以查看这如何影响资源使用情况:

您需要完成此活动的一般步骤如下:

  1. 决定在全景徒步应用程序中选择一个您想要测试的服务。

  2. 创建一组测试,然后用它们来测量服务的资源使用情况。

  3. 启动您的服务,并使用您在上一步中创建的测试来监视资源使用情况。

  4. 停止您的服务运行,并再次运行它,这次更改 CPU 和内存配置。

  5. 再次使用您在步骤 2中创建的测试监视资源使用情况,并比较资源使用情况的变化。

注意

此活动的解决方案可以通过此链接找到。

下一个活动将帮助您在您的Dockerfiles上使用hadolint来改进最佳实践。

活动 12.02:使用 hadolint 改进 Dockerfiles 上的最佳实践

hadolint提供了一个很好的方式来强制执行最佳实践,当您创建您的 Docker 镜像时。在这个活动中,您将再次使用docker-stress镜像的Dockerfile,以查看您是否可以使用hadolint的建议来改进Dockerfile,使其尽可能地符合最佳实践。

您需要完成此活动的步骤如下:

  1. 确保您的系统上有hadolint镜像可用并正在运行。

  2. docker-stress镜像的Dockerfile运行hadolint镜像,并记录结果。

  3. 对上一步中的Dockerfile进行推荐的更改。

  4. 再次测试Dockerfile

完成活动后,您应该获得以下输出:

图 12.3:活动 12.02 的预期输出

图 12.3:活动 12.02 的预期输出

注意

此活动的解决方案可以通过此链接找到。

总结

本章我们深入研究了许多理论知识,以及对练习进行了深入的工作。我们从查看我们的运行 Docker 容器如何利用主机系统的 CPU、内存和磁盘资源开始了本章。我们研究了监视这些资源如何被我们的容器消耗,并配置我们的运行容器以减少使用的资源数量。

然后,我们研究了 Docker 的最佳实践,涉及了许多不同的主题,包括利用基础镜像、安装程序和清理、为可扩展性开发底层应用程序,以及配置应用程序和镜像。然后,我们介绍了一些工具,帮助您执行这些最佳实践,包括hadolintFROM:latest,帮助您对Dockerfiles进行代码检查,以及dcvalidator来检查您的docker-compose.yml文件。

下一章将进一步提升我们的监控技能,介绍使用 Prometheus 来监控我们的容器指标和资源。

第十三章:监控 Docker 指标

概述

本章将为您提供设置系统监控环境以开始收集容器和资源指标所需的技能。通过本章结束时,您将能够为您的指标制定监控策略,并确定在开始项目开发之前需要考虑的事项。您还将在系统上实施基本的 Prometheus 配置。本章将通过探索用户界面、PromQL 查询语言、配置选项以及收集 Docker 和应用程序指标来扩展您对 Prometheus 的了解。它还将通过将 Grafana 作为 Prometheus 安装的一部分来增强您的可视化和仪表板功能。

介绍

在本书的上一章中,我们花了一些时间研究了我们的容器如何在其主机系统上使用资源。我们这样做是为了确保我们的应用程序和容器尽可能高效地运行,但是当我们开始将我们的应用程序和容器转移到更大的生产环境时,使用诸如docker stats之类的命令行工具将变得繁琐。您会注意到,随着您的容器数量的增加,仅使用stats命令来理解指标变得困难。正如您将在接下来的页面中看到的,通过一点规划和配置,为我们的容器环境设置监控将使我们能够轻松跟踪我们的容器和系统的运行情况,并确保我们的生产服务的正常运行时间。

随着我们转向更敏捷的开发流程,应用程序的开发需要纳入对应用程序的监控。在项目开始阶段制定清晰的应用程序监控计划将允许开发人员将监控工具纳入其开发流程。这意味着在创建应用程序之前,就有必要清楚地了解我们计划如何收集和监控我们的应用程序。

除了应用程序和服务之外,监控基础设施、编排和在我们环境中运行的容器也很重要,这样我们就可以全面了解我们环境中发生的一切。

在制定指标监控政策时,您需要考虑以下一些事项:

  • 应用程序和服务:这包括您的代码可能依赖的第三方应用程序,这些应用程序不驻留在您的硬件上。它还将包括您的应用程序正在运行的编排服务。

  • 硬件:有时候,退一步并确保您注意到所有您的服务所依赖的硬件,包括数据库、API 网关和服务器,是很有必要的。

  • 要监控和警报的服务:随着您的应用程序增长,您可能不仅想要监控特定的服务或网页;您可能还想确保用户能够执行所有的交易。这可能会增加您的警报和监控系统的复杂性。

  • 仪表板和报告:仪表板和报告可以为非技术用户提供大量有用的信息。

  • 适合您需求的应用程序:如果您在一家较大的公司工作,他们很可能会有一个您可以选择的应用程序列表。但这并不意味着一刀切。您决定用来监控您的环境的应用程序应该适合特定目的,并得到项目中所有相关人员的认可。

这就是Prometheus发挥作用的地方。在本章中,我们将使用 Prometheus 作为监控解决方案,因为它被广泛采用,是开源的,并且免费使用。市场上还有许多其他免费和企业应用程序可提供类似的监控功能,包括自托管的应用程序,如 Nagios 和 SCOM,以及较新的订阅式服务,包括 New Relic、Sumo Logic 和 Datadog。Prometheus 是为了监控云上的服务而构建的。它提供了领先市场的功能,领先于其他主要竞争对手。

其他一些应用程序还提供日志收集和聚合,但我们已经将这部分分配给了一个单独的应用程序,并将在下一章专门讨论我们的 Docker 环境的日志管理。Prometheus 只专注于指标收集和监控,由于在日志管理方面有合适的免费和开源替代品,它并没有将日志管理纳入其重点范围。

使用 Prometheus 监控环境指标

Prometheus 最初是由 SoundCloud 创建和开发的,因为他们需要一种监控高度动态的容器环境的方法,并且当时对当前的工具感到不满意,因为他们觉得它不符合他们的需求。Prometheus 被开发为 SoundCloud 监控他们的容器以及运行其服务的基础托管硬件和编排的一种方式。

它最初是在 2012 年创建的,自那时起,该项目一直是免费和开源的,并且是云原生计算基金会的一部分。它还被全球各地的公司广泛采用,这些公司需要更多地了解他们的云环境的性能。

Prometheus 通过从系统中收集感兴趣的指标并将其存储在本地磁盘上的时间序列数据库中来工作。它通过从服务或应用程序提供的 HTTP 端点进行抓取来实现这一点。

端点可以被写入应用程序中,以提供与应用程序或服务相关的基本网络界面,提供指标,或者可以由导出器提供,导出器将从服务或应用程序中获取数据,然后以 Prometheus 能理解的形式暴露出来。

注意

本章多次提到了 HTTP 端点,这可能会引起混淆。您将在本章后面看到,HTTP 端点是由服务或应用程序提供的非常基本的 HTTP 网页。正如您很快将看到的那样,这个 HTTP 网页提供了服务向 Prometheus 公开的所有指标的列表,并提供了存储在 Prometheus 时间序列数据库中的指标值。

Prometheus 包括多个组件:

  • Prometheus:Prometheus 应用程序执行抓取和收集指标,并将其存储在其时间序列数据库中。

  • Grafana:Prometheus 二进制文件还包括一个基本的网络界面,帮助您开始查询数据库。在大多数情况下,Grafana 也会被添加到环境中,以允许更具视觉吸引力的界面。它将允许创建和存储仪表板,以便更轻松地进行指标监控。

  • 导出器:导出器为 Prometheus 提供了收集来自不同应用程序和服务的数据所需的指标端点。在本章中,我们将启用 Docker 守护程序来导出数据,并安装cAdvisor来提供有关系统上运行的特定容器的指标。

  • AlertManager:虽然本章未涉及,但通常会与 Prometheus 一起安装AlertManager,以在服务停机或环境中触发的其他警报时触发警报。

Prometheus 还提供了基于 Web 的表达式浏览器,允许您使用功能性的 PromQL 查询语言查看和聚合您收集的时间序列指标。这意味着您可以在收集数据时查看数据。表达式浏览器功能有限,但可以与 Grafana 集成,以便您创建仪表板、监控服务和AlertManager,从而在需要时触发警报并得到通知。

Prometheus 易于安装和配置(您很快就会看到),并且可以收集有关自身的数据,以便您开始测试您的应用程序。

由于 Prometheus 的采用率和受欢迎程度,许多公司为其应用程序和服务创建了出口器。在本章中,我们将为您提供一些出口器的示例。

现在是时候动手了。在接下来的练习中,您将在自己的系统上下载并运行 Prometheus 二进制文件,以开始监控服务。

注意

请使用touch命令创建文件,并使用vim命令在 vim 编辑器中处理文件。

练习 13.01:安装和运行 Prometheus

在本练习中,您将下载并解压 Prometheus 二进制文件,启动应用程序,并探索 Prometheus 的 Web 界面和一些基本配置。您还将练习监控指标,例如发送到 Prometheus 接口的总 HTTP 请求。

注意

截至撰写本书时,Prometheus 的最新版本是 2.15.1。应用程序的最新版本可以在以下网址找到:https://prometheus.io/download/。

  1. 找到最新版本的 Prometheus 进行安装。使用wget命令将压缩的存档文件下载到您的系统上。您在命令中使用的 URL 可能与此处的 URL 不同,这取决于您使用的操作系统和 Prometheus 的版本:
wget https://github.com/prometheus/prometheus/releases/download/v2.15.1/prometheus-2.15.1.<operating-system>-amd64.tar.gz
  1. 使用tar命令解压您在上一步下载的 Prometheus 存档。以下命令使用zxvf选项解压文件,然后提取存档和文件,并显示详细输出:
tar zxvf prometheus-2.15.1.<operating-system>-amd64.tar.gz
  1. 存档提供了一个完全创建的 Prometheus 二进制应用程序,可以立即启动。进入应用程序目录,查看目录中包含的一些重要文件:
cd prometheus-2.15.1.<operating-system>-amd64
  1. 使用ls命令列出应用程序目录中的文件,以查看我们应用程序中的重要文件:
ls

注意输出,它应该类似于以下内容,其中prometheus.yml文件是我们的配置文件。prometheus文件是应用程序二进制文件,tsdb和数据目录是我们存储时间序列数据库数据的位置:

LICENSE    console_libraries    data    prometheus.yml    tsdb
NOTICE    consoles    prometheus    promtool

在前面的目录列表中,请注意console_librariesconsoles目录包括用于查看我们即将使用的 Prometheus Web 界面的提供的二进制文件。promtool目录包括您可以使用的工具来处理 Prometheus,包括一个配置检查工具,以确保您的prometheus.yml文件有效。

  1. 如果您的二进制文件没有问题,应用程序已准备就绪,您应该能够验证 Prometheus 的版本。使用--version选项从命令行运行应用程序:
./prometheus --version

输出应该如下所示:

prometheus, version 2.15.1 (branch: HEAD, revision: 8744510c6391d3ef46d8294a7e1f46e57407ab13)
  build user:       root@4b1e33c71b9d
  build date:       20191225-01:12:19
  go version:       go1.13.5
  1. 您不会对配置文件进行任何更改,但在开始之前,请确保它包含 Prometheus 的有效信息。运行cat命令查看文件的内容:
cat prometheus.yml 

输出中的行数已经减少。从以下输出中可以看出,全局的scrap_interval参数和evaluation_interval参数设置为15秒:

# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 
15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. 
The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).
…

如果您有时间查看prometheus.yml配置文件,您会注意到它分为四个主要部分:

global:这控制服务器的全局配置。配置包括scrape_interval,用于了解它将多久抓取目标,以及evaluation_interval,用于控制它将多久评估规则以创建时间序列数据和生成规则。

alerting:默认情况下,配置文件还将通过 AlertManager 设置警报。

rule_files:这是 Prometheus 将定位为其度量收集加载的附加规则的位置。rule_files指向规则存储的位置。

scrape_configs:这些是 Prometheus 将监视的资源。我们希望监视的任何其他目标都将添加到配置文件的此部分中。

  1. 启动 Prometheus 只是运行二进制文件并使用--config.file命令行选项指定要使用的配置文件的简单问题。运行以下命令启动 Prometheus:
./prometheus --config.file=prometheus.yml

几秒钟后,您应该会看到消息“服务器已准备好接收 Web 请求。”:

…
msg="Server is ready to receive web requests."
  1. 输入 URL http://localhost:9090。Prometheus 提供了一个易于使用的 Web 界面。如果应用程序已正确启动,您现在应该能够在系统上打开 Web 浏览器。应该会呈现给您表达式浏览器,类似于以下屏幕截图。虽然表达式浏览器看起来并不那么令人印象深刻,但它在开箱即用时具有一些很好的功能。它分为三个不同的部分。

主菜单:屏幕顶部的主菜单,黑色背景,允许您通过“状态”下拉菜单查看额外的配置细节,通过“警报”选项查看警报历史,并通过PrometheusGraph选项返回主表达式浏览器屏幕。

表达式编辑器:这是顶部的文本框,我们可以在其中输入我们的 PromQL 查询或从下拉列表中选择指标。然后,单击“执行”按钮开始显示数据。

图形和控制台显示:一旦确定要查询的数据,它将以表格格式显示在“控制台”选项卡中,并以时间序列图形格式显示在“图形”选项卡中,您可以使用“添加图形”按钮在网页下方添加更多图形:

图 13.1:首次加载表达式浏览器

图 13.1:首次加载表达式浏览器

  1. 单击“状态”下拉菜单。您将看到以下图像,其中包括有用的信息,包括“运行时和构建信息”以显示正在运行的版本的详细信息,“命令行标志”以运行应用程序,“配置”显示当前运行的config文件,以及用于警报规则的“规则”。下拉菜单中的最后两个选项显示“目标”,您当前正在从中获取数据的目标,以及“服务发现”,显示正在监控的自动服务:图 13.2:状态下拉菜单

图 13.2:状态下拉菜单

  1. 从“状态”菜单中选择“目标”选项,您将能够看到 Prometheus 正在从哪里抓取数据。您也可以通过转到 URL“HTTP:localhost:9090/targets”来获得相同的结果。您应该会看到类似于以下内容的屏幕截图,因为 Prometheus 目前只监视自身:图 13.3:Prometheus 目标页面

图 13.3:Prometheus 目标页面

  1. 单击目标端点。您将能够看到目标公开的指标。现在您可以看到 Prometheus 如何利用其拉取架构从目标中抓取数据。单击链接或打开浏览器,输入 URLhttp://localhost:9090/metrics以查看 Prometheus 指标端点。您应该会看到类似于以下内容的内容,显示了 Prometheus 正在公开的所有指标点,然后由自身抓取:
# HELP go_gc_duration_seconds A summary of the GC invocation 
durations.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 9.268e-06
go_gc_duration_seconds{quantile="0.25"} 1.1883e-05
go_gc_duration_seconds{quantile="0.5"} 1.5802e-05
go_gc_duration_seconds{quantile="0.75"} 2.6047e-05
go_gc_duration_seconds{quantile="1"} 0.000478339
go_gc_duration_seconds_sum 0.002706392
…
  1. 通过单击返回按钮或输入 URLhttp://localhost:9090/graph返回到表达式浏览器。单击“执行”按钮旁边的下拉列表,以查看所有可用的指标点:图 13.4:从表达式浏览器中获得的 Prometheus 指标

图 13.4:从表达式浏览器中获得的 Prometheus 指标

  1. 从下拉列表或查询编辑器中,添加prometheus_http_requests_total指标以查看发送到 Prometheus 应用程序的所有 HTTP 请求。您的输出可能与以下内容不同。单击“执行”按钮,然后单击“图形”选项卡以查看我们数据的可视化视图:图 13.5:从表达式浏览器中显示的 Prometheus HTTP 请求图

图 13.5:从表达式浏览器中显示的 Prometheus HTTP 请求图

如果你对我们到目前为止所取得的成就还感到有些困惑,不要担心。在短时间内,我们已经设置了 Prometheus 并开始收集数据。尽管我们只收集了 Prometheus 本身的数据,但我们已经能够演示如何快速轻松地可视化应用程序执行的 HTTP 请求。接下来的部分将向您展示如何通过对 Prometheus 配置进行小的更改,开始从 Docker 和正在运行的容器中捕获数据。

使用 Prometheus 监控 Docker 容器

Prometheus 监控是了解应用程序能力的好方法,但它对于帮助我们监控 Docker 和我们系统上运行的容器并没有太多帮助。幸运的是,我们有两种方法可以收集数据,以便更深入地了解我们正在运行的容器。我们可以使用 Docker 守护程序将指标暴露给 Prometheus,并且还可以安装一些额外的应用程序,比如cAdvisor,来收集我们系统上运行的容器的更多指标。

通过对 Docker 配置进行一些小的更改,我们能够将指标暴露给 Prometheus,以便它收集运行在我们系统上的 Docker 守护程序的特定数据。这将部分地收集指标,但不会给我们提供实际运行容器的指标。这就是我们需要安装cAdvisor的地方,它是由谷歌专门用来收集我们运行容器指标的。

注意

如果我们需要收集更多关于底层硬件、Docker 和我们的容器运行情况的指标,我们还可以使用node_exporter来收集更多的指标。我们将不会在本章中涵盖node_exporter,但支持文档可以在以下网址找到:

https://github.com/prometheus/node_exporter。

由于 Docker 已经在您的主机系统上运行,设置它以允许 Prometheus 连接其指标只是向/etc/docker/daemon.json文件添加一个配置更改。在大多数情况下,该文件很可能是空白的。如果您已经在文件中有详细信息,您只需将以下示例中的第 2 行第 3 行添加到您的配置文件中。第 2 行启用了这个experimental功能,以便暴露给 Prometheus 收集指标,第 3 行设置了这些数据点要暴露的 IP 地址和端口:

1 {
2        "experimental": true,
3        "metrics-addr": "0.0.0.0:9191"
4 }

由于配置更改,您系统上的 Docker 守护程序需要重新启动才能生效。但一旦发生这种情况,您应该可以在daemon.json文件中添加的指定 IP 地址和端口处获得可用的指标。在上面的示例中,这将是在http://0.0.0.0:9191

要安装cAdvisor,谷歌提供了一个易于使用的 Docker 镜像,可以从谷歌的云注册表中拉取并在您的环境中运行。

要运行cAdvisor,您将运行镜像,挂载所有与 Docker 守护程序和运行容器相关的目录。您还需要确保暴露度量标准将可用的端口。默认情况下,cAdvisor配置为在端口8080上公开度量标准,除非您对cAdvisor的基础图像进行更改,否则您将无法更改。

以下docker run命令在容器上挂载卷,例如/var/lib/docker/var/run,将端口8080暴露给主机系统,并最终使用来自 Google 的最新cadvisor镜像:

docker run \
  --volume=<host_directory>:<container_directory> \
  --publish=8080:8080 \
  --detach=true \
  --name=cadvisor \
  gcr.io/google-containers/cadvisor:latest

注意

cAdvisor的基础图像进行更改不是本章将涵盖的内容,但您需要参考cAdvisor文档并对cAdvisor代码进行特定更改。

cAdvisor镜像还将提供一个有用的 Web 界面来查看这些指标。cAdvisor不保存任何历史数据,因此您需要使用 Prometheus 收集数据。

一旦 Docker 守护程序和cAdvisor有数据可供 Prometheus 收集,我们需要确保我们有一个定期的配置,将数据添加到时间序列数据库中。应用程序目录中的prometheus.yml配置文件允许我们执行此操作。您只需在文件的scrape_configs部分添加配置。正如您从以下示例中看到的,您需要添加一个job_name参数,并提供指标提供位置的详细信息作为targets条目:

    - job_name: '<scrap_job_name>'
      static_configs:
      - targets: ['<ip_address>:<port>']

一旦目标对 Prometheus 可用,您就可以开始搜索数据。现在我们已经提供了如何开始使用 Prometheus 收集 Docker 指标的分解,以下练习将向您展示如何在运行系统上执行此操作。

练习 13.02:使用 Prometheus 收集 Docker 指标

在此练习中,您将配置 Prometheus 开始从我们的 Docker 守护程序收集数据。这将使您能够查看 Docker 守护程序本身特别使用了哪些资源。您还将运行cAdvisor Docker 镜像,以开始收集运行容器的特定指标:

  1. 要开始从 Docker 守护程序收集数据,您首先需要在系统上启用此功能。首先通过文本编辑器打开/etc/docker/daemon.json文件,并添加以下详细信息:
1 {
2        "experimental": true,
3        "metrics-addr": "0.0.0.0:9191"
4 }

您对配置文件所做的更改将会公开 Docker 守护程序的指标,以允许 Prometheus 进行抓取和存储这些值。要启用此更改,请保存 Docker 配置文件并重新启动 Docker 守护程序。

  1. 通过打开您的 Web 浏览器并使用您在配置中设置的 URL 和端口号来验证是否已经生效。输入 URL http://0.0.0.0:9191/metrics,您应该会看到一系列指标被公开以允许 Prometheus 进行抓取:
# HELP builder_builds_failed_total Number of failed image builds
# TYPE builder_builds_failed_total counter
builder_builds_failed_total{reason="build_canceled"} 0
builder_builds_failed_total{reason="build_target_not_reachable
_error"} 0
builder_builds_failed_total{reason="command_not_supported_
error"} 0
builder_builds_failed_total{reason="dockerfile_empty_error"} 0
builder_builds_failed_total{reason="dockerfile_syntax_error"} 0
builder_builds_failed_total{reason="error_processing_commands_
error"} 0
builder_builds_failed_total{reason="missing_onbuild_arguments_
error"} 0
builder_builds_failed_total{reason="unknown_instruction_error"} 0
…
  1. 现在,您需要让 Prometheus 知道它可以在哪里找到 Docker 正在向其公开的指标。您可以通过应用程序目录中的prometheus.yml文件来完成这一点。不过,在这样做之前,您需要停止 Prometheus 服务的运行,以便配置文件的添加生效。打开 Prometheus 正在运行的终端并按下Ctrl + C。成功执行此操作时,您应该会看到类似以下的输出:
level=info ts=2020-04-28T04:49:39.435Z caller=main.go:718 
msg="Notifier manager stopped"
level=info ts=2020-04-28T04:49:39.436Z caller=main.go:730 
msg="See you next time!"
  1. 使用文本编辑器打开应用程序目录中的prometheus.yml配置文件。转到文件的scrape_configs部分的末尾,并添加行 2134。额外的行将告诉 Prometheus 它现在可以从已在 IP 地址0.0.0.0和端口9191上公开的 Docker 守护程序获取指标:

prometheus.yml

21 scrape_configs:
22   # The job name is added as a label 'job=<job_name>' to any        timeseries scraped from this config.
23   - job_name: 'prometheus'
24
25     # metrics_path defaults to '/metrics'
26     # scheme defaults to 'http'.
27 
28     static_configs:
29     - targets: ['localhost:9090']
30 
31   - job_name: 'docker_daemon'
32     static_configs:
33     - targets: ['0.0.0.0:9191']
34

此步骤的完整代码可以在 https://packt.live/33satLe 找到。

  1. 保存您对prometheus.yml文件所做的更改,并按照以下方式从命令行再次启动 Prometheus 应用程序:
./prometheus --config.file=prometheus.yml
  1. 如果您返回到 Prometheus 的表达式浏览器,您可以再次验证它现在已配置为从 Docker 守护程序收集数据。从Status菜单中选择Targets,或者使用 URL http://localhost:9090/targets,现在应该包括我们在配置文件中指定的docker_daemon作业:图 13.6:带有 docker_daemon 的 Prometheus Targets

图 13.6:带有 docker_daemon 的 Prometheus Targets

  1. 通过搜索engine_daemon_engine_cpus_cpus来验证您是否正在收集数据。这个值应该与您的主机系统上可用的 CPU 或核心数量相同。将其输入到 Prometheus 表达式浏览器中,然后单击Execute按钮:图 13.7:主机系统上可用的 docker_daemon CPU

图 13.7:主机系统上可用的 docker_daemon CPU

  1. Docker 守护程序受限于其可以向 Prometheus 公开的数据量。设置cAdvisor镜像以收集有关正在运行的容器的详细信息。在命令行上使用以下docker run命令将其作为由 Google 提供的容器运行。docker run命令使用存储在 Google 容器注册表中的cadvisor:latest镜像,类似于 Docker Hub。无需登录到此注册表;镜像将自动拉到您的系统中:
docker run \
  --volume=/:/rootfs:ro \
  --volume=/var/run:/var/run:ro \
  --volume=/sys:/sys:ro \
  --volume=/var/lib/docker/:/var/lib/docker:ro \
  --volume=/dev/disk/:/dev/disk:ro \
  --publish=8080:8080 \
  --detach=true \
  --name=cadvisor \
  gcr.io/google-containers/cadvisor:latest
  1. cAdvisor带有一个 Web 界面,可以为您提供一些基本功能,但由于它不存储历史数据,您将收集数据并将其存储在 Prometheus 上。现在,打开另一个 Web 浏览器会话,并输入 URL http://0.0.0.0:8080,您应该会看到一个类似以下的网页:图 13.8:cAdvisor 欢迎页面

图 13.8:cAdvisor 欢迎页面

  1. 输入 URL http://0.0.0.0:8080/metrics,以查看cAdvisor在 Web 界面上显示的所有数据。

注意

当对 Prometheus 配置文件进行更改时,应用程序需要重新启动才能生效。在我们进行的练习中,我们一直通过停止服务来实现相同的结果。

  1. 与 Docker 守护程序一样,配置 Prometheus 定期从指标端点抓取数据。停止运行 Prometheus 应用程序,并再次使用文本编辑器打开prometheus.yml配置文件。在配置文件底部,添加另一个cAdvisor的配置,具体如下:

prometheus.yml

35   - job_name: 'cadvisor'
36     scrape_interval: 5s
37     static_configs:
38     - targets: ['0.0.0.0:8080']

此步骤的完整代码可在 https://packt.live/33BuFub 找到。

  1. 再次保存您的配置更改,并从命令行运行 Prometheus 应用程序,如下所示:
./prometheus --config.file=prometheus.yml

如果现在查看 Prometheus Web 界面上的Targets,您应该会看到类似以下的内容,显示cAdvisor也在我们的界面上可用:

图 13.9:添加了 cAdvisor 的 Prometheus Targets 页面

图 13.9:添加了 cAdvisor 的 Prometheus Targets 页面

  1. 通过 Prometheus 的Targets页面显示cAdvisor现在可用并已连接,验证了 Prometheus 现在正在从cAdvisor收集指标数据。您还可以从表达式浏览器中测试这一点,以验证它是否按预期工作。通过从顶部菜单中选择GraphsPrometheus进入表达式浏览器。页面加载后,将以下 PromQL 查询添加到查询编辑器中,然后单击Execute按钮:
(time() - process_start_time_seconds{instance="0.0.0.0:8080",job="cadvisor"})

注意

我们开始使用一些更高级的 PromQL 查询,可能看起来有点混乱。本章的下一部分致力于让您更好地理解 PromQL 查询语言。

查询正在使用process_start_time_seconds指标,特别是针对cAdvisor应用程序和time()函数来添加总秒数。您应该在表达式浏览器上看到类似以下的结果:

图 13.10:来自表达式浏览器的 cAdvisor 正常运行时间

图 13.10:来自表达式浏览器的 cAdvisor 正常运行时间

通过这个练习,我们现在有一个正在运行的 Prometheus 实例,并且正在从 Docker 守护程序收集数据。我们还设置了cAdvisor,以便为我们提供有关正在运行的容器实例的更多信息。本章的下一部分将更深入地讨论 PromQL 查询语言,以帮助您更轻松地查询 Prometheus 提供的指标。

了解 Prometheus 查询语言

正如我们在本章的前几部分中所看到的,Prometheus 提供了自己的查询语言,称为 PromQL。它允许您搜索、查看和聚合存储在 Prometheus 数据库中的时间序列数据。本节将帮助您进一步了解查询语言。Prometheus 中有四种核心指标类型,我们将从描述每种类型开始。

计数器

计数器随时间计算元素;例如,这可以是您网站的访问次数。当服务或应用程序重新启动时,计数只会增加或重置。它们适用于在某个时间点计算特定事件的次数。每次计数器更改时,收集的数据中的数字也会反映出来。

计数器通常以_total后缀结尾。但由于计数器的性质,每次服务重新启动时,计数器将被重置为 0。使用我们查询中的rate()irate()函数,我们将能够随时间查看我们的指标速率,并忽略计数器被重置为 0 的任何时间。rate()irate()函数都使用方括号[]指定时间值,例如[1m]

如果您对我们正在收集的数据中的计数器示例感兴趣,请打开cAdvisor的数据收集的指标页面,网址为http://0.0.0.0:8080/metrics。提供的第一个指标之一是container_cpu_system_seconds_total。如果我们浏览指标页面,我们将看到有关指标值和类型的信息如下:

# HELP container_cpu_system_seconds_total Cumulative system cpu time 
consumed in seconds.
# TYPE container_cpu_system_seconds_total counter
container_cpu_system_seconds_total{id="/",image="",name=""} 
195.86 1579481501131
…

现在,我们将研究 Prometheus 中可用的第二种指标类型,也就是仪表。

仪表

仪表旨在处理随时间可能减少的值,并设计用于公开某物的当前状态的任何指标。就像温度计或燃料表一样,您将能够看到当前状态值。仪表在功能上受到限制,因为可能会在时间点之间存在缺失值,因此它们比计数器不太可靠,因此计数器仍然用于数据的时间序列表示。

如果我们再次转到cAdvisor的指标页面,您可以看到一些指标显示为仪表。我们看到的第一个指标之一是container_cpu_load_average_10s,它作为一个仪表提供,类似于以下值:

# HELP container_cpu_load_average_10s Value of container cpu load 
average over the last 10 seconds.
# TYPE container_cpu_load_average_10s gauge
container_cpu_load_average_10s{id="/",image="",name=""} 0 
1579481501131
…

下一部分将带您了解直方图,Prometheus 中可用的第三种指标类型。

直方图

直方图比仪表和计数器复杂得多,并提供额外信息,如观察的总和。它们用于提供一组数据的分布。直方图使用抽样,可以用于在 Prometheus 服务器上估计分位数。

直方图比仪表和计数器更不常见,似乎没有为cAdvisor设置,但我们可以在 Docker 守护程序指标中看到一些可用的直方图。转到 URL http://0.0.0.0:9191/metrics,您将能够看到列出的第一个直方图指标是engine_daemon_container_actions_seconds。这是 Docker 守护程序处理每个操作所需的秒数:

# HELP engine_daemon_container_actions_seconds The number of seconds 
it takes to process each container action
# TYPE engine_daemon_container_actions_seconds histogram
engine_daemon_container_actions_seconds_bucket{action="changes",
le="0.005"} 1
…

接下来的部分将介绍第四种可用的指标类型,换句话说,摘要。

摘要

摘要是直方图的扩展,是在客户端计算的。它们具有更高的准确性优势,但对客户端来说也可能很昂贵。我们可以在 Docker 守护程序指标中看到摘要的示例,其中http_request_duration_microseconds在这里列出:

# HELP http_request_duration_microseconds The HTTP request latencies in microseconds.
# TYPE http_request_duration_microseconds summary
http_request_duration_microseconds{handler="prometheus",quantile=
"0.5"} 3861.5
…

现在,既然我们已经解释了 PromQL 中可用的指标类型,我们可以进一步看看这些指标如何作为查询的一部分实现。

执行 PromQL 查询

在表达式浏览器上运行查询很容易,但您可能并不总是能获得所需的信息。只需添加指标名称,例如countainer_cpu_system_seconds_total,我们就可以得到相当多的响应。不过,响应的数量取决于我们系统上的容器数量以及我们主机系统上正在运行的每个文件系统的返回值。为了限制结果中提供的响应数量,我们可以使用花括号{}搜索特定文本。

考虑以下示例。以下命令提供了我们希望查看的"cadvisor"容器的完整名称:

container_cpu_system_seconds_total{ name="cadvisor"}

以下示例使用与 GO 兼容的正则表达式。该命令查找以ca开头并在后面有更多字符的任何名称:

container_cpu_system_seconds_total{ name=~"ca.+"} 

以下代码片段正在搜索任何名称值不为空的容器,使用不等于(!=)值:

container_cpu_system_seconds_total{ name!=""}

如果我们将任何这些指标搜索放在表达式浏览器中并创建图表,您会注意到图表会随着时间线性上升。正如我们之前提到的,这是因为指标container_cpu_system_seconds_total是一个计数器,它只会随着时间增加或被设置为零。通过使用函数,我们可以计算更有用的时间序列数据。以下示例使用rate()函数来计算匹配时间序列数据的每秒速率。我们使用了[1m],表示 1 分钟。数字越大,图表就会更平滑:

rate(container_cpu_system_seconds_total{name="cadvisor"}[1m])

rate函数只能用于计数器指标。如果我们运行了多个容器,我们可以使用sum()函数将所有值相加,并使用(name)函数按容器名称提供图表,就像我们在这里做的那样:

sum(rate(container_cpu_system_seconds_total[1m])) by (name)

注意

如果您想查看 PromQL 中所有可用函数的列表,请转到官方 Prometheus 文档提供的以下链接:

https://prometheus.io/docs/prometheus/latest/querying/functions/。

PromQL 还允许我们从查询中执行算术运算。在以下示例中,我们使用process_start_time_seconds指标并搜索 Prometheus 实例。我们可以从time()函数中减去这个时间,该函数给出了当前的日期和时间的时代时间:

(time() - process_start_time_seconds{instance="localhost:9090",job="prometheus"})

注意

时代时间是从 1970 年 1 月 1 日起的秒数,用一个数字表示;例如,1578897429 被转换为 2020 年 1 月 13 日上午 6:37(GMT)。

我们希望这个 PromQL 入门能让您更深入地了解在项目中使用查询语言。以下练习将通过具体的使用案例,进一步加强我们学到的内容,特别是监视我们正在运行的 Docker 容器。

练习 13.03:使用 PromQL 查询语言

在以下练习中,我们将在您的系统上引入一个新的 Docker 镜像,以帮助您演示使用 Prometheus 时特定于 Docker 的一些可用指标。这个练习将加强您到目前为止对 PromQL 查询语言的学习,通过一个具体的使用案例来收集和显示基本网站的指标数据。

  1. 打开一个新的终端并创建一个名为web-nginx的新目录:
mkdir web-nginx; cd web-nginx
  1. web-nginx目录中创建一个新文件,命名为index.html。用文本编辑器打开新文件,并添加以下 HTML 代码:
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
    <h1>
        Hello Prometheus
    </h1>
</body>
</html>
  1. 用以下命令运行一个新的 Docker 容器。到目前为止,您应该已经熟悉了语法,但以下命令将拉取最新的nginx镜像,命名为web-nginx,并暴露端口80,以便您可以查看在上一步中创建的挂载的index.html文件:
docker run --name web-nginx --rm -v ${PWD}/index.html:/usr/share/nginx/html/index.html -p 80:80 -d nginx
  1. 打开一个网络浏览器,访问http://0.0.0.0。你应该看到的唯一的东西是问候语Hello Prometheus图 13.11:示例网页

图 13.11:示例网页

  1. 如果 Prometheus 没有在您的系统上运行,请打开一个新的终端,并从 Prometheus 应用程序目录中,从命令行启动应用程序:
./prometheus --config.file=prometheus.yml

注意

在本章的这一部分,我们不会展示所有 PromQL 查询的屏幕截图,因为我们不想浪费太多空间。但是这些查询应该对我们设置的正在运行的容器和系统都是有效的。

  1. 现在在 Prometheus 中可用的大部分cAdvisor指标将以container开头。使用count()函数与指标container_memory_usage_bytes,以查看当前内存使用量的字节数:
count(container_memory_usage_bytes)

上述查询提供了系统上正在运行的 28 个结果。

  1. 为了限制您正在寻找的信息,可以使用花括号进行搜索,或者如下命令中所示,使用不搜索(!=)特定的图像名称。目前,您只有两个正在运行的容器,图像名称为cAdvisorweb-nginx。通过使用scalar()函数,您可以计算系统上随时间运行的容器数量。在输入以下查询后,单击Execute按钮:
scalar(count(container_memory_usage_bytes{image!=""}) > 0)
  1. 单击Graphs选项卡,现在您应该有一个绘制的查询图。该图应该类似于以下图像,其中您启动了第三个图像web-nginx容器,以显示 Prometheus 表达式浏览器如何显示此类型的数据。请记住,您只能在图表中看到一条线,因为这是我们系统上两个容器使用的内存,而没有单独的内存使用值:图 13.12:来自表达式浏览器的 cAdvisor 指标

图 13.12:来自表达式浏览器的 cAdvisor 指标

  1. 使用container_start_time_seconds指标获取容器启动的 Unix 时间戳:
container_start_time_seconds{name="web-nginx"}

您将看到类似于 1578364679 的东西,这是自纪元时间以来的秒数,即 1970 年 1 月 1 日。

  1. 使用time()函数获取当前时间,然后从该值中减去container_start_time_seconds,以显示容器已运行多少秒:
(time() - container_start_time_seconds{name="web-nginx"})
  1. 监视您的应用程序通过 Prometheus 的prometheus_http_request_duration_seconds_count指标的 HTTP 请求。使用rate()函数绘制每个 HTTP 请求到 Prometheus 的持续时间的图表:
rate(prometheus_http_request_duration_seconds_count[1m])

注意

使用web-nginx容器查看其 HTTP 请求时间和延迟将是很好的,但是该容器尚未设置为向 Prometheus 提供此信息。我们将在本章中很快解决这个问题。

  1. 使用算术运算符将prometheus_http_request_duration_seconds_sum除以prometheus_http_request_duration_seconds_count,这将提供所做请求的 HTTP 延迟:
rate(prometheus_http_request_duration_seconds_sum[1m]) / rate(prometheus_http_request_duration_seconds_count[1m])
  1. 使用 container_memory_usage_bytes 指标运行以下命令,以查看系统上每个运行容器使用的内存。在此查询中,我们使用 sum by (name) 命令按容器名称添加值:
sum by (name) (container_memory_usage_bytes{name!=""})

如果您执行上述查询,您将在表达式浏览器中看到图形,显示 web-nginxcAdvisor 容器使用的内存:

图 13.13:我们系统上运行的两个容器的内存

图 13.13:我们系统上运行的两个容器的内存

本节帮助您更加熟悉 PromQL 查询语言,并组合您的查询以开始从表达式浏览器查看指标。接下来的部分将提供有关如何开始从在 Docker 中创建的应用程序和服务收集指标的详细信息,使用出口商以一种 Prometheus 友好的方式公开数据。

使用 Prometheus 出口商

在本章中,我们已配置应用程序指标以提供数据供 Prometheus 抓取和收集,那么为什么我们需要担心出口商呢?正如您所见,Docker 和 cAdvisor 已经很好地公开了数据端点,Prometheus 可以从中收集指标。但这些功能有限。正如我们从我们的新 web-nginx 网站中看到的,我们的镜像上运行的网页没有相关的数据暴露出来。我们可以使用出口商来帮助从应用程序或服务中收集指标,然后以 Prometheus 能够理解和收集的方式提供数据。

尽管这可能看起来是 Prometheus 工作方式的一个主要缺陷,但由于 Prometheus 的使用增加以及它是开源的事实,供应商和第三方提供商现在提供出口商来帮助您从应用程序获取指标。

这意味着,通过安装特定库或使用预构建的 Docker 镜像来运行您的应用程序,您可以公开您的指标数据供收集。例如,我们在本章前面创建的 web-nginx 应用程序正在 NGINX 上运行。要获取我们的 Web 应用程序的指标,我们可以简单地在运行我们的 Web 应用程序的 NGINX 实例上安装 ngx_stub_status_prometheus 库。或者更好的是,我们可以找到某人已经构建好的 Docker 镜像来运行我们的 Web 应用程序。

注意

本章的这一部分重点介绍了 NGINX Exporter,但是大量应用程序的导出器可以在其支持文档或 Prometheus 文档中找到。

在下一个练习中,我们将以我们的nginx容器为例,并将导出器与我们的web-nginx容器一起使用,以公开可供 Prometheus 收集的指标。

练习 13.04:使用应用程序的指标导出器

到目前为止,我们已经使用nginx容器提供了一个基本的网页,但我们没有特定的指标可用于我们的网页。在这个练习中,您将使用一个不同的 NGINX 镜像,该镜像带有可以暴露给 Prometheus 的指标导出器。

  1. 如果web-nginx容器仍在运行,请使用以下命令停止容器:
docker kill web-nginx
  1. 在 Docker Hub 中,您有一个名为mhowlett/ngx-stud-status-prometheus的镜像,其中已经安装了ngx_stub_status_prometheus库。该库将允许您设置一个 HTTP 端点,以从您的nginx容器向 Prometheus 提供指标。将此镜像下载到您的工作环境中:
docker pull mhowlett/ngx-stub-status-prometheus
  1. 在上一个练习中,您使用容器上的默认 NGINX 配置来运行您的 Web 应用程序。要将指标暴露给 Prometheus,您需要创建自己的配置来覆盖默认配置,并将您的指标作为可用的 HTTP 端点提供。在您的工作目录中创建一个名为nginx.conf的文件,并添加以下配置细节:
daemon off;
events {
}
http {
  server {
    listen 80;
    location / {
      index  index.html;
    }
    location /metrics {
      stub_status_prometheus;
    }
  }
}

上述配置将确保您的服务器仍然在端口80上可用第 8 行第 11 行将确保提供您当前的index.html页面,第 14 行将设置一个子域/metrics,以提供ngx_stub_status_prometheus库中可用的详细信息。

  1. 提供index.html文件的挂载点,以启动web-nginx容器并使用以下命令挂载您在上一步中创建的nginx.conf配置:
docker run --name web-nginx --rm -v ${PWD}/index.html:/usr/html/index.html -v ${PWD}/nginx.conf:/etc/nginx/nginx.conf -p 80:80 -d mhowlett/ngx-stub-status-prometheus
  1. 您的web-nginx应用程序应该再次运行,并且您应该能够从 Web 浏览器中看到它。输入 URL http://0.0.0.0/metrics以查看指标端点。您的 Web 浏览器窗口中的结果应该类似于以下信息:
# HELP nginx_active_connections_current Current number of 
active connections
# TYPE nginx_active_connections_current gauge
nginx_active_connections_current 2
# HELP nginx_connections_current Number of connections currently 
being processed by nginx
# TYPE nginx_connections_current gauge
nginx_connections_current{state="reading"} 0
nginx_connections_current{state="writing"} 1
nginx_connections_current{state="waiting"} 1
…
  1. 您仍然需要让 Prometheus 知道它需要从新的端点收集数据。因此,停止 Prometheus 的运行。再次进入应用程序目录,并使用您的文本编辑器,在prometheus.yml配置文件的末尾添加以下目标:

prometheus.yml

40   - job_name: 'web-nginx'
41     scrape_interval: 5s
42     static_configs:
43     - targets: ['0.0.0.0:80']

此步骤的完整代码可在 https://packt.live/3hzbQgj 找到。

  1. 保存配置更改,并重新启动 Prometheus 的运行:
./prometheus --config.file=prometheus.yml
  1. 确认 Prometheus 是否配置为从您刚刚创建的新指标端点收集数据。打开您的网络浏览器,输入 URL http://0.0.0.0:9090/targets图 13.14:显示 web-nginx 的目标页面

图 13.14:显示 web-nginx 的目标页面

在这个练习中,您学会了向在您的环境中运行的应用程序添加导出器。我们首先扩展了我们之前的web-nginx应用程序,以允许它显示多个 HTTP 端点。然后,我们使用了一个包含了ngx_stub_status_prometheus库的 Docker 镜像,以便我们能够显示我们的web-nginx统计信息。然后,我们配置了 Prometheus 以从提供的端点收集这些详细信息。

在接下来的部分,我们将设置 Grafana,以便我们能够更仔细地查看我们的数据,并为我们正在收集的数据提供用户友好的仪表板。

使用 Grafana 扩展 Prometheus

Prometheus web 界面提供了一个功能表达式浏览器,允许我们在有限的安装中搜索和查看我们的时间序列数据库中的数据。它提供了一个图形界面,但不允许我们保存任何搜索或可视化。Prometheus web 界面也有限,因为它不能在仪表板中分组查询。而且,界面提供的可视化并不多。这就是我们可以通过使用 Grafana 等应用程序进一步扩展我们收集的数据的地方。

Grafana 允许我们直接连接到 Prometheus 时间序列数据库,并执行查询并创建视觉上吸引人的仪表板。Grafana 可以作为一个独立的应用程序在服务器上运行。我们可以预先配置 Grafana Docker 镜像,部署到我们的系统上,并配置到我们的 Prometheus 数据库的连接,并设置一个基本的仪表板来监视我们正在运行的容器。

当您第一次登录 Grafana 时,会显示下面的屏幕,Grafana 主页仪表板。您可以通过点击屏幕左上角的 Grafana 图标来返回到这个页面。这是主要的工作区,您可以在这里开始构建仪表板,配置您的环境,并添加用户插件:

图 13.15:Grafana 主页仪表板

图 13.15:Grafana 主页仪表板

屏幕左侧是一个方便的菜单,可以帮助您进一步配置 Grafana。加号符号将允许您向安装中添加新的仪表板和数据源,而仪表板图标(四个方块)将所有仪表板组织到一个区域进行搜索和查看。在仪表板图标下面是探索按钮,它提供一个表达式浏览器,就像 Prometheus 一样,以便运行 PromQL 查询,而警报图标(铃铛)将带您到窗口,您可以在其中配置在不同事件发生后触发警报。配置图标将带您到屏幕,您可以在其中配置 Grafana 的操作方式,而服务器管理员图标允许您管理谁可以访问您的 Grafana Web 界面以及他们可以拥有什么权限。

在下一个练习中安装 Grafana 时,随意探索界面,但我们将努力尽可能自动化这个过程,以避免对您的工作环境进行任何更改。

练习 13.05:在您的系统上安装和运行 Grafana

在这个练习中,您将在您的系统上设置 Grafana,并允许应用程序开始使用您在 Prometheus 数据库中存储的数据。您将使用 Grafana 的 Docker 镜像安装 Grafana,提供界面的简要说明,并开始设置基本的仪表板:

  1. 如果 Prometheus 没有运行,请重新启动。另外,请确保您的容器、cAdvisor和测试 NGINX 服务器(web-nginx)正在运行:
./prometheus --config.file=prometheus.yml
  1. 打开您系统的/etc/hosts文件,并将一个域名添加到主机 IP127.0.0.1。不幸的是,您将无法使用您一直用来访问 Prometheus 的 localhost IP 地址来自动为 Grafana 配置数据源。诸如127.0.0.10.0.0.0或使用 localhost 的 IP 地址将不被识别为 Grafana 的数据源。根据您的系统,您可能已经添加了许多不同的条目到hosts文件中。通常您将会在最前面的 IP 地址列表中有127.0.0.1的 IP 地址,它将引用localhost域并将prometheus修改为这一行,就像我们在以下输出中所做的那样:
1 127.0.0.1       localhost prometheus
  1. 保存hosts文件。打开您的网络浏览器并输入 URLhttp://prometheus:9090。Prometheus 表达式浏览器现在应该显示出来。您不再需要提供系统 IP 地址。

  2. 要自动配置您的 Grafana 镜像,您需要从主机系统挂载一个provisioning目录。创建一个 provisioning 目录,并确保该目录包括额外的目录dashboardsdatasourcespluginsnotifiers,就像以下命令中所示:

mkdir -p provisioning/dashboards provisioning/datasources provisioning/plugins provisioning/notifiers
  1. provisioning/datasources目录中创建一个名为automatic_data.yml的文件。用文本编辑器打开文件并输入以下细节,告诉 Grafana 它将使用哪些数据来提供仪表板和可视化效果。以下细节只是命名数据源,提供数据类型以及数据的位置。在这种情况下,这是您的新 Prometheus 域名:
apiVersion: 1
datasources:
- name: Prometheus
  type: prometheus
  url: http://prometheus:9090
  access: direct
  1. 现在,在provisioning/dashboards目录中创建一个名为automatic_dashboard.yml的文件。用文本编辑器打开文件并添加以下细节。这只是提供了未来仪表板可以在启动时存储的位置:
apiVersion: 1
providers:
- name: 'Prometheus'
  orgId: 1
  folder: ''
  type: file
  disableDeletion: false
  editable: true
  options:
    path: /etc/grafana/provisioning/dashboards

您已经做了足够的工作来启动我们的 Grafana Docker 镜像。您正在使用提供的受支持的 Grafana 镜像grafana/grafana

注意

我们目前没有任何代码可以添加为仪表板,但在接下来的步骤中,您将创建一个基本的仪表板,稍后将自动配置。如果您愿意,您也可以搜索互联网上 Grafana 用户创建的现有仪表板,并代替它们进行配置。

  1. 运行以下命令以拉取并启动 Grafana 镜像。它使用-v选项将您的配置目录挂载到 Docker 镜像上的/etc/grafana/provisioning目录。它还使用-e选项,使用GF_SECURITY_ADMIN_PASSWORD环境变量将管理密码设置为secret,这意味着您不需要每次登录到新启动的容器时重置管理密码。最后,您还使用-p将您的镜像端口3000暴露到我们系统的端口3000
docker run --rm -d --name grafana -p 3000:3000 -e "GF_SECURITY_ADMIN_PASSWORD=secret" -v ${PWD}/provisioning:/etc/grafana/provisioning grafana/grafana

注意

虽然使用 Grafana Docker 镜像很方便,但每次镜像重新启动时,您将丢失所有更改和仪表板。这就是为什么我们将在演示如何同时使用 Grafana 的同时进行安装配置。

  1. 您已经在端口3000上启动了镜像,因此现在应该能够打开 Web 浏览器。在您的 Web 浏览器中输入 URLhttp://0.0.0.0:3000。它应该显示 Grafana 的欢迎页面。要登录到应用程序,请使用具有用户名admin和我们指定为GF_SECURITY_ADMIN_PASSWORD环境变量的密码的默认管理员帐户:图 13.16:Grafana 登录屏幕

图 13.16:Grafana 登录屏幕

  1. 登录后,您将看到 Grafana 主页仪表板。单击屏幕左侧的加号符号,然后选择“仪表板”以添加新的仪表板:图 13.17:Grafana 欢迎屏幕

图 13.17:Grafana 欢迎屏幕

注意

您的 Grafana 界面很可能显示为深色默认主题。我们已将我们的更改为浅色主题以便阅读。要在您自己的 Grafana 应用程序上更改此首选项,您可以单击屏幕左下角的用户图标,选择“首选项”,然后搜索“UI 主题”。

  1. 单击“添加新面板”按钮。

  2. 要使用Prometheus数据添加新查询,请从下拉列表中选择Prometheus作为数据源:图 13.18:在 Grafana 中创建我们的第一个仪表板

图 13.18:在 Grafana 中创建我们的第一个仪表板

  1. 在指标部分,添加 PromQL 查询sum (rate (container_cpu_usage_seconds_total{image!=""}[1m])) by (name)。该查询将提供系统上所有正在运行的容器的详细信息。它还将随时间提供每个容器的 CPU 使用情况。根据您拥有的数据量,您可能希望在查询选项下拉菜单中将相对时间设置为15m

此示例使用15m来确保您有足够的数据用于图表,但是此时间范围可以设置为您希望的任何值:

图 13.19:添加仪表板指标

图 13.19:添加仪表板指标

  1. 选择显示选项按钮以向仪表板面板添加标题。在下图中,面板的标题设置为CPU Container Usage图 13.20:添加仪表板标题

图 13.20:添加仪表板标题

  1. 单击屏幕顶部的保存图标。这将为您提供命名仪表板的选项—在这种情况下为Container Monitoring。单击保存后,您将被带到已完成的仪表板屏幕,类似于这里的屏幕:图 13.21:仪表板屏幕

图 13.21:仪表板屏幕

  1. 在仪表板屏幕顶部,在保存图标的左侧,您将有选项以JSON格式导出您的仪表板。如果这样做,您可以使用此JSON文件添加到您的配置目录中。当您运行时,它将帮助您将仪表板安装到 Grafana 映像中。选择导出并将文件保存到/tmp目录,文件名将默认为类似于仪表板名称和时间戳数据的内容。在此示例中,它将JSON文件保存为Container Monitoring-1579130313205.json。还要确保未打开用于外部共享的导出选项,如下图所示:图 13.22:将仪表板导出为 JSON

图 13.22:将您的仪表板导出为 JSON

  1. 要将仪表板添加到您的配置文件中,您需要首先停止运行 Grafana 映像。使用以下docker kill命令执行此操作:
docker kill grafana
  1. 将您在步骤 15中保存的仪表板文件添加到provisioning/dashboards目录,并将文件命名为ContainerMonitoring.json作为复制的一部分,如下命令所示:
cp /tmp/ContainerMonitoring-1579130313205.json provisioning/dashboards/ContainerMonitoring.json
  1. 重新启动 Grafana 映像,并使用默认管理密码登录应用程序:
docker run --rm -d --name grafana -p 3000:3000 -e "GF_SECURITY_ADMIN_PASSWORD=secret" -v ${PWD}/provisioning:/etc/grafana/provisioning grafana/grafana

注意

通过这种方式预配仪表板和数据源,这意味着您将无法再从 Grafana Web 界面创建仪表板。从现在开始,当您创建仪表板时,您将被要求将仪表板保存为 JSON 文件,就像我们在导出仪表板时所做的那样。

  1. 现在登录主页仪表板。您应该会看到Container Monitoring仪表板作为最近访问的仪表板可用,但如果您点击屏幕顶部的主页图标,它也会显示在您的 Grafana 安装的General文件夹中可用:图 13.23:可用和预配的容器监控仪表板

图 13.23:可用和预配的容器监控仪表板

我们现在已经设置了一个完全功能的仪表板,当我们运行 Grafana Docker 镜像时会自动加载。正如你所看到的,Grafana 提供了一个专业的用户界面,帮助我们监视正在运行的容器的资源使用情况。

这就是我们本节的结束,我们向您展示了如何使用 Prometheus 收集指标,以帮助监视您的容器应用程序的运行情况。接下来的活动将使用您在之前章节中学到的知识,进一步扩展您的安装和监控。

活动 13.01:创建一个 Grafana 仪表板来监视系统内存

在以前的练习中,您已经设置了一个快速仪表板,以监视我们的 Docker 容器使用的系统 CPU。正如您在上一章中所看到的,监视正在运行的容器使用的系统内存也很重要。在这个活动中,您被要求创建一个 Grafana 仪表板,用于监视正在运行的容器使用的系统内存,并将其添加到我们的Container Monitoring仪表板中,确保在启动我们的 Grafana 镜像时可以预配:

您需要完成此活动的步骤如下:

  1. 确保您的环境正在被 Prometheus 监视,并且 Grafana 已安装在您的系统上。确保您使用 Grafana 在 Prometheus 上存储的时间序列数据上进行搜索。

  2. 创建一个 PromQL 查询,监视正在运行的 Docker 容器使用的容器内存。

  3. 保存您的Container Monitoring仪表板上的新仪表板面板。

  4. 确保新的改进后的Container Monitoring仪表板现在在启动 Grafana 容器时可用和预配。

预期输出

当您启动 Grafana 容器时,您应该在仪表板顶部看到新创建的内存容器使用情况面板:

图 13.24:显示内存使用情况的新仪表板面板

图 13.24:显示内存使用情况的新仪表板面板

注意

此活动的解决方案可以通过此链接找到。

下一个活动将确保您能够舒适地使用导出器,并向 Prometheus 添加新的目标,以开始跟踪全景徒步应用程序中的额外指标。

活动 13.02:配置全景徒步应用程序以向 Prometheus 暴露指标

您的指标监控环境开始看起来相当不错,但是全景徒步应用程序中有一些应用可能会提供额外的细节和指标供监控,例如在您的数据库上运行的 PostgreSQL 应用程序。选择全景徒步应用程序中的一个应用程序,将其指标暴露给您的 Prometheus 环境:

您需要完成此活动的步骤如下:

  1. 确保 Prometheus 正在您的系统上运行并收集指标。

  2. 选择作为全景徒步应用程序一部分运行的服务或应用程序,并研究如何暴露指标以供 Prometheus 收集。

  3. 将更改实施到您的应用程序或服务中。

  4. 测试您的更改,并验证指标是否可供收集。

  5. 配置 Prometheus 上的新目标以收集新的全景徒步应用程序指标。

  6. 验证您能够在 Prometheus 上查询您的新指标。

成功完成活动后,您应该在 Prometheus 的Targets页面上看到postgres-web目标显示:

图 13.25:在 Prometheus 上显示的新 postgres-web 目标页面

图 13.25:在 Prometheus 上显示的新 postgres-web 目标页面

注意

此活动的解决方案可以通过此链接找到。

总结

在本章中,我们深入研究了度量标准和监控我们的容器应用程序和服务。我们从讨论为什么您需要在度量监控上制定清晰的策略以及为什么您需要在项目开始开发之前做出许多决策开始。然后,我们介绍了 Prometheus,并概述了其历史、工作原理以及为什么它在很短的时间内就变得非常流行。然后是重新开始工作的时候,我们将 Prometheus 安装到我们的系统上,熟悉使用 Web 界面,开始从 Docker 收集度量标准(进行了一些小的更改),并使用cAdvisor收集正在运行的容器的度量标准。

Prometheus 使用的查询语言有时可能会有点令人困惑,因此我们花了一些时间来探索 PromQL,然后再看看如何使用导出器来收集更多的度量标准。我们在本章结束时将 Grafana 集成到我们的环境中,显示来自 Prometheus 的时间序列数据,并创建有用的仪表板和可视化数据。

我们的下一章将继续监控主题,收集和监控我们正在运行的容器的日志数据。

第十四章:收集容器日志

概述

在上一章中,我们确保为我们运行的 Docker 容器和服务收集了指标数据。本章将在此基础上,致力于收集和监控 Docker 容器和其中运行的应用程序的日志。它将从讨论为什么我们需要为我们的开发项目建立清晰的日志监控策略开始,并讨论我们需要记住的一些事情。然后,我们将介绍我们日志监控策略中的主要角色 - 即 Splunk - 以收集、可视化和监控我们的日志。我们将安装 Splunk,从我们的系统和运行的容器中转发日志数据,并使用 Splunk 查询语言设置与我们收集的日志数据配合工作的监控仪表板。通过本章的学习,您将具备为您的 Docker 容器项目建立集中式日志监控服务的技能。

介绍

每当我们的运行应用程序或服务出现问题时,我们通常首先在应用程序日志中寻找线索,以了解问题的原因。因此,了解如何收集日志并监控项目的日志事件变得非常重要。

随着我们实施基于 Docker 的微服务架构,确保我们能够查看应用程序和容器生成的日志变得更加重要。随着容器和服务数量的增加,尝试单独访问每个运行的容器作为故障排除的手段变得越来越不方便。对于可伸缩的应用程序,根据需求进行伸缩,跨多个容器跟踪日志错误可能变得越来越困难。

确保我们有一个合适的日志监控策略将有助于我们排除应用程序故障,并确保我们的服务以最佳效率运行。这也将帮助我们减少在日志中搜索所花费的时间。

在为您的项目构建日志监控策略时,有一些事情您需要考虑:

  • 您的应用程序将使用一个框架来处理日志。有时,这可能会对容器造成负担,因此请确保测试您的容器,以确保它们能够在不出现与此日志框架相关的任何问题的情况下运行。

  • 容器是瞬时的,因此每次关闭容器时日志都会丢失。您必须将日志转发到日志服务或将日志存储在数据卷中,以确保您可以解决可能出现的任何问题。

  • Docker 包含一个日志驱动程序,用于将日志事件转发到主机上运行的 Syslog 实例。除非您使用 Docker 的企业版,否则如果您使用特定的日志驱动程序,log命令将无法使用(尽管对于 JSON 格式的日志可以使用)。

  • 日志聚合应用通常会根据其所摄取的数据量向您收费。而且,如果您在环境中部署了一个服务,您还需要考虑存储需求,特别是您计划保留日志的时间有多长。

  • 您需要考虑您的开发环境与生产环境的运行方式。例如,在开发环境中没有必要长时间保留日志,但生产环境可能要求您保留一段时间。

  • 您可能不仅需要应用程序数据。您可能需要收集应用程序的日志,应用程序运行的容器以及应用程序和容器都在运行的基础主机和操作系统的日志。

我们可以在日志监控策略中使用许多应用程序,包括 Splunk、Sumo Logic、Nagios Logs、Data Dog 和 Elasticsearch。在本章中,我们决定使用 Splunk 作为我们的日志监控应用程序。它是最古老的应用程序之一,拥有庞大的支持和文档社区。在处理数据和创建可视化方面也是最好的。

在接下来的章节中,您将看到如何轻松地启动、运行和配置应用程序,以便开始监控系统日志和我们的容器应用程序。

介绍 Splunk

在 Docker 的普及之前很久,Splunk 于 2003 年成立,旨在帮助公司从不断增长的应用和服务提供的大量数据中发现一些模式和信息。Splunk 是一款软件应用,允许您从应用程序和硬件系统中收集日志和数据。然后,它让您分析和可视化您收集的数据,通常在一个中央位置。

Splunk 允许您以不同的格式输入数据,在许多情况下,Splunk 将能够识别数据所在的格式。然后,您可以使用这些数据来帮助排除应用程序故障,创建监控仪表板,并在特定事件发生时创建警报。

注意

在本章中,我们只会触及 Splunk 的一部分功能,但如果您感兴趣,有许多宝贵的资源可以向您展示如何从数据中获得运营智能,甚至使用 Splunk 创建机器学习和预测智能模型。

Splunk 提供了许多不同的产品来满足您的需求,包括 Splunk Cloud,适用于希望选择云日志监控解决方案的用户和公司。

对于我们的日志监控策略,我们将使用 Splunk Enterprise。它易于安装,并带有大量功能。在使用 Splunk 时,您可能已经知道许可成本是按您发送到 Splunk 的日志数据量收费,然后对其进行索引。Splunk Enterprise 允许您在试用期内每天索引高达 500 MB 的数据。60 天后,您可以选择升级许可证,或者继续使用免费许可证,该许可证将继续允许您每天记录 500 MB 的数据。用户可以申请开发者许可证,该许可证允许用户每天记录 10 GB 的数据。

要开始使用 Splunk,我们首先需要了解其基本架构。这将在下一节中讨论。

Splunk 安装的基本架构

通过讨论 Splunk 的架构,您将了解每个部分的工作原理,并熟悉我们在本章中将使用的一些术语:

  • 索引器:对于较大的 Splunk 安装,建议您设置专用和复制的索引器作为环境的一部分。索引器的作用是索引您的数据 - 也就是说,组织您发送到 Splunk 的日志数据。它还添加元数据和额外信息,以帮助加快搜索过程。然后索引器将存储您的日志数据,这些数据已准备好供搜索头使用和查询。

  • 搜索头:这是主要的 Web 界面,您可以在其中执行搜索查询和管理您的 Splunk 安装。搜索头将与索引器连接,以查询已收集和存储在它们上面的数据。在较大的安装中,您甚至可能有多个搜索头,以允许更多的查询和报告进行。

  • 数据转发器:通常安装在您希望收集日志的系统上。它是一个小型应用程序,配置为在您的系统上收集日志,然后将数据推送到您的 Splunk 索引器。

在接下来的部分中,我们将使用官方的 Splunk Docker 镜像,在活动容器上同时运行搜索头和索引器。我们将继续在 Splunk 环境中使用 Docker,因为它还提供了索引器和数据转发器作为受支持的 Docker 镜像。这使您可以在继续安装之前测试和沙盒化安装。

注意

请注意,我们使用 Splunk Docker 镜像是为了简单起见。如果需要,它将允许我们移除应用程序。如果您更喜欢这个选项,安装应用程序并在您的系统上运行它是简单而直接的。

Splunk 的另一个重要特性是,它包括由 Splunk 和其他第三方提供者提供的大型应用程序生态系统。这些应用程序通常是为了帮助用户监视将日志转发到 Splunk 的服务而创建的,然后在搜索头上安装第三方应用程序。这将为这些日志提供专门的仪表板和监控工具。例如,您可以将日志从思科设备转发,然后安装思科提供的 Splunk 应用程序,以便在开始索引数据后立即开始监视您的思科设备。您可以创建自己的 Splunk 应用程序,但要使其列为官方提供的应用程序,它需要经过 Splunk 的认证。

注意

有关可用的免费和付费 Splunk 应用程序的完整列表,Splunk 已设置他们的 SplunkBase,允许用户从以下网址搜索并下载可用的应用程序:splunkbase.splunk.com/apps/

这是对 Splunk 的快速介绍,应该帮助您了解接下来我们将要做的一些工作。然而,让您熟悉 Splunk 的最佳方法是在您的系统上运行容器,这样您就可以开始使用它了。

在 Docker 上安装和运行 Splunk

作为本章的一部分,我们将使用官方的 Splunk Docker 镜像在我们的系统上安装它。尽管直接在主机系统上安装 Splunk 并不是一个困难的过程,但将 Splunk 安装为容器镜像将有助于扩展我们对 Docker 的知识,并进一步提升我们的技能。

我们的 Splunk 安装将在同一个容器上同时运行搜索头和索引器,因为我们将监控的数据量很小。然而,如果您要在多个用户访问数据的生产环境中使用 Splunk,您可能需要考虑安装专用的索引器,以及一个或多个专用的搜索头。

注意

在本章中,我们将使用 Splunk Enterprise 版本 8.0.2。本章中将进行的大部分工作不会太高级,因此应该与将来的 Splunk 版本兼容。

在我们开始使用 Splunk 之前,让我们先了解一下 Splunk 应用程序使用的三个主要目录。虽然我们只会执行基本的配置和更改,但以下细节将有助于理解应用程序中的目录是如何组织的,并且会帮助您进行 Docker 容器设置。

在主要的 Splunk 应用程序目录中,通常安装为/opt/splunk/,您将看到这里解释的三个主要目录:

  • etc 目录:这是我们 Splunk 安装的所有配置信息所在的地方。我们将创建一个目录,并将 etc 目录挂载为我们运行的容器的一部分,以确保我们对配置所做的任何更改都得以保留,当我们关闭应用程序时不会被销毁。这将包括用户访问、软件设置和保存的搜索、仪表板以及 Splunk 应用程序。

  • bin 目录:这是存储所有 Splunk 应用程序和二进制文件的地方。在这一点上,您不需要访问此目录或更改此目录中的文件,但这可能是您需要进一步调查的内容。

  • var 目录:Splunk 的索引数据和应用程序日志存储在这个目录中。当我们开始使用 Splunk 时,我们不会费心保留我们存储在 var 目录中的数据。但是当我们解决了部署中的所有问题后,我们将挂载 var 目录以保留我们的索引数据,并确保我们可以继续对其进行搜索,即使我们的 Splunk 容器停止运行。

注意

要下载本章中使用的一些应用程序和内容,您需要在splunk.com上注册一个帐户以获取访问权限。注册时无需购买任何东西或提供信用卡详细信息,这只是 Splunk 用来跟踪谁在使用他们的应用程序的手段。

要运行我们的 Splunk 容器,我们将从 Docker Hub 拉取官方镜像,然后运行类似以下的命令:

docker run --rm -d -p <port:port> -e "SPLUNK_START_ARGS=--accept-license" -e "SPLUNK_PASSWORD=<admin-password>" splunk/splunk:latest

正如您从前面的命令中所看到的,我们需要暴露所需的相关端口,以便访问安装的不同部分。您还会注意到,我们需要指定两个环境变量作为运行容器的一部分。第一个是SPLUNK_START_ARGS,我们将其设置为--accept-license,这是您在安装 Splunk 时通常会接受的许可证。其次,我们需要为SPLUNK_PASSWORD环境变量提供一个值。这是管理员帐户使用的密码,也是您首次登录 Splunk 时将使用的帐户。

我们已经提供了大量的理论知识,为本章的下一部分做好准备。现在是时候将这些理论付诸实践,让我们的 Splunk 安装运行起来,这样我们就可以开始从主机系统收集日志。在接下来的练习中,我们将在运行的主机系统上安装 Splunk 数据转发器,以便收集日志并转发到我们的 Splunk 索引器。

注意

请使用touch命令创建文件,并使用vim命令在文件上使用 vim 编辑器进行操作。

练习 14.01:运行 Splunk 容器并开始收集数据

在这个练习中,您将使用 Docker Hub 上提供的官方 Splunk Docker 镜像来运行 Splunk。您将进行一些基本的配置更改,以帮助管理用户访问镜像上的应用程序,然后您将在系统上安装一个转发器,以便开始在 Splunk 安装中消耗日志:

  1. 创建一个名为chapter14的新目录:
mkdir chapter14; cd chapter14/
  1. 使用docker pull命令从 Docker Hub 拉取由 Splunk 创建的最新支持的镜像。仓库简单地列为splunk/splunk
docker pull splunk/splunk:latest
  1. 使用docker run命令在您的系统上运行 Splunk 镜像。使用--rm选项确保容器在被杀死时完全被移除,使用-d选项将容器作为守护进程在系统后台运行,使用-p选项在主机上暴露端口8000,以便您可以在 Web 浏览器上查看应用程序。最后,使用-e选项在启动容器时向系统提供环境变量:
docker run --rm -d -p 8000:8000 -e "SPLUNK_START_ARGS=--accept-license" -e "SPLUNK_PASSWORD=changeme" --name splunk splunk/splunk:latest

在上述命令中,您正在为 Web 界面暴露端口8000,使用一个环境变量接受 Splunk 许可,并将管理密码设置为changeme。该命令还以-d作为守护进程在后台运行。

  1. Splunk 将需要 1 到 2 分钟来启动。使用docker logs命令来查看应用程序的进展情况:
docker logs splunk

当您看到类似以下内容的行显示Ansible playbook complete时,您应该准备好登录了:

…
Ansible playbook complete, will begin streaming 
  1. 输入 URL http://0.0.0.0:8000 来访问我们的 Splunk 安装的 Web 界面。您应该会看到类似以下的内容。要登录,请使用admin作为用户名,并使用在运行镜像时设置的SPLUNK_PASSWORD环境变量作为密码。在这种情况下,您将使用changeme图 14.1:Splunk Web 登录页面

图 14.1:Splunk Web 登录页面

登录后,您将看到 Splunk 主屏幕,它应该看起来类似于以下内容。主屏幕分为不同的部分,如下所述:

图 14.2:Splunk 欢迎屏幕

图 14.2:Splunk 欢迎屏幕

主屏幕可以分为以下几个部分:

  • Splunk>:这是屏幕左上角的图标。如果您简单地点击该图标,它将随时带您回到主屏幕。

  • 应用程序菜单:这在屏幕的左侧,允许您安装和配置 Splunk 应用程序。

  • 菜单栏:它位于屏幕顶部,包含不同的选项,取决于您在帐户中拥有的特权级别。由于您已经以管理员帐户登录,您可以获得完整的选项范围。这使我们能够配置和管理 Splunk 的运行和管理方式。菜单栏中的主要配置选项是“设置”。它提供了一个大的下拉列表,让您控制 Splunk 运行的大部分方面。

  • 主工作区:主工作区填充了页面的其余部分,您可以在这里开始搜索数据,设置仪表板,并开始可视化数据。您可以设置一个主仪表板,这样每次登录或单击“Splunk>`图标时,您也会看到此仪表板。我们将在本章后面设置主仪表板,以向您展示如何操作。

  1. 您可以开始对我们的 Splunk 配置进行更改,但如果容器因某种原因停止运行,所有更改都将丢失。相反,创建一个目录,您可以在其中存储所有 Splunk 环境所需的相关配置信息。使用以下命令停止当前正在运行的 Splunk 服务器:
docker kill splunk
  1. 创建一个可以挂载到 Splunk 主机上的目录。为此目的命名为testSplunk
mkdir -p ${PWD}/testsplunk
  1. 再次运行 Splunk 容器,这次使用-v选项将您在上一步创建的目录挂载到容器上的/opt/splunk/etc目录。暴露额外的端口9997,以便稍后将数据转发到我们的 Splunk 安装中。
docker run --rm -d -p 8000:8000 -p 9997:9997 -e 'SPLUNK_START_ARGS=--accept-license' -e 'SPLUNK_PASSWORD=changeme' -v ${PWD}/testsplunk:/opt/splunk/etc/ --name splunk splunk/splunk
  1. 一旦 Splunk 再次启动,以管理员帐户重新登录到 Splunk Web 界面。

  2. 向系统添加一个新用户,以确保通过屏幕顶部的“设置”菜单将相关配置详细信息保存在您的挂载目录中。单击“设置”菜单:图 14.3:Splunk 设置菜单

图 14.3:Splunk 设置菜单

  1. 打开“设置”菜单,移动到底部部分,然后在“用户和身份验证”部分中单击“用户”。您应该看到已在 Splunk 安装中创建的所有用户的列表。目前只有管理员帐户会列在其中。要创建新用户,请单击屏幕顶部的“新用户”按钮。

  2. 您将看到一个网页表单,您可以在其中添加新用户帐户的详细信息。填写新用户的详细信息。一旦您对添加的详细信息感到满意,点击屏幕底部的“保存”按钮:图 14.4:在 Splunk 上创建新用户

图 14.4:在 Splunk 上创建新用户

  1. 为了确保您现在将这些数据保存在您的挂载目录中,请返回到您的终端,查看新用户是否存储在您的挂载目录中。只需使用以下命令列出testsplunk/users目录中的目录:
ls testsplunk/users/

您应该看到已为您在上一步中创建的新帐户设置了一个目录;在这种情况下是vincesesto

admin        splunk-system-user        users.ini
users.ini.default        vincesesto
  1. 现在是时候开始向在您的系统上运行的 Splunk 实例发送数据了。在开始从正在运行的 Docker 容器中收集数据之前,在您的运行系统上安装一个转发器,并从那里开始转发日志。要访问特定于您系统的转发器,请转到以下网址并下载特定于您操作系统的转发器:www.splunk.com/en_us/download/universal-forwarder.html

  2. 按照提示接受许可证,以便您可以使用该应用程序。还要接受安装程序中呈现的默认选项:图 14.5:Splunk 转发器安装程序

图 14.5:Splunk 转发器安装程序

  1. 转发器通常会自动启动。通过访问终端并使用cd命令切换到系统安装目录,验证转发器是否正在运行。对于 Splunk 转发器,二进制和应用程序文件将位于/opt/splunkforwarder/bin/目录中:
cd /opt/Splunkforwarder/bin/
  1. bin目录中,通过运行./splunk status命令来检查转发器的状态,如下所示:
./splunk status

如果它正在运行,您应该看到类似于以下输出:

splunkd is running (PID: 2076).
splunk helpers are running (PIDs: 2078).
  1. 如果转发器在安装时没有启动,请使用以下命令从bin目录运行带有start选项的转发器:
./splunk start

提供的输出将显示 Splunk 守护程序和服务的启动。它还将显示正在系统上运行的服务的进程 ID(PID):

splunkd is running (PID: 2076).
splunk helpers are running (PIDs: 2078).
Splunk> Be an IT superhero. Go home early.
...
Starting splunk server daemon (splunkd)...Done
  1. 您需要让 Splunk 转发器知道它需要发送数据的位置。在本练习的步骤 8中,我们确保运行了具有端口9997的 Splunk 容器,以便出于这个特定的原因暴露。使用./splunk命令告诉转发器将数据发送到我们运行在 IP 地址0.0.0.0端口9997上的 Splunk 容器,使用我们 Splunk 实例的管理员用户名和密码:
./splunk add forward-server 0.0.0.0:9997 -auth admin:changeme

该命令应返回类似以下的输出:

Added forwarding to: 0.0.0.0:9997.
  1. 最后,为了完成 Splunk 转发器的设置,指定一些日志文件转发到我们的 Splunk 容器。使用转发器上的./splunk命令监视我们系统的/var/log目录中的文件,并将它们发送到 Splunk 容器进行索引,以便我们可以开始查看它们:
./splunk add monitor /var/log/
  1. 几分钟后,如果一切正常,您应该有一些日志事件可以在 Splunk 容器上查看。返回到您的网络浏览器,输入以下 URL 以打开 Splunk 搜索页面:http://0.0.0.0:8000/en-US/app/search/search

注意

以下步骤使用非常基本的 Splunk 搜索查询来搜索安装中的所有数据。如果您之前没有使用过 Splunk 查询语言,不用担心;我们将花费一个完整的部分,使用 Splunk 查询语言,更深入地解释查询语言。

  1. 通过简单地将星号(*)作为搜索查询添加,执行基本搜索,如下截图所示。如果一切正常,您应该开始在搜索页面的结果区域看到日志事件:图 14.6:Splunk 搜索窗口,显示来自我们的转发器的数据

图 14.6:Splunk 搜索窗口,显示来自我们的转发器的数据

  1. 在本练习的最后部分,您将练习将数据上传到 Splunk 的最简单方法,即直接将文件上传到正在运行的系统中。从packt.live/3hFbh4C下载名为weblog.csv的示例数据文件,并将其放在您的/tmp目录中。

  2. 返回到您的 Splunk 网络界面,单击设置菜单选项。从菜单选项的右侧选择添加数据,如下截图所示:图 14.7:直接导入文件到 Splunk

图 14.7:直接导入文件到 Splunk

  1. 单击屏幕底部的“从我的计算机上传文件”:图 14.8:在 Splunk 上上传文件

图 14.8:在 Splunk 上上传文件

  1. 下一个屏幕将允许您从您的计算机中选择源文件。在此练习中,选择您之前下载的weblog.csv文件。当您选择文件后,请点击屏幕顶部的Next按钮。

  2. 设置Source Type以选择或接受 Splunk 查看数据的格式。在这种情况下,它应该已经将您的数据识别为.csv文件。点击Next按钮。

  3. Input Settings页面让您设置主机的名称,但将索引保留为默认值。点击Review按钮:图 14.9:输入设置页面

图 14.9:输入设置页面

  1. 如果所有条目看起来正确,请点击Submit按钮。然后,点击Start Searching,您应该看到您的搜索屏幕,以及可供搜索的示例 Web 日志数据。它应该看起来类似于以下内容:图 14.10:在 Splunk 中搜索导入的文件

图 14.10:在 Splunk 中搜索导入的文件

在短时间内,我们已经在系统上设置了 Splunk 搜索头和索引器,并安装了 Splunk 转发器将日志发送到索引器和搜索头。我们还手动向我们的索引中添加了日志数据,以便我们可以查看它。

本章的下一部分将重点介绍如何将 Docker 容器日志传输到我们正在运行的新 Splunk 容器中。

将容器日志传输到 Splunk

我们的日志监控环境开始成形,但我们需要将我们的 Docker 容器日志传输到应用程序中,以使其值得工作。我们已经设置了 Splunk 转发器,将日志从我们的系统发送到/var/log目录。到目前为止,我们已经学会了我们可以简单地挂载我们容器的日志文件,并使用 Splunk 转发器将日志发送到 Splunk 索引器。这是一种方法,但 Docker 提供了一个更简单的选项来将日志发送到 Splunk。

Docker 提供了一个特定于 Splunk 的日志驱动程序,它将通过我们的网络将容器日志发送到我们 Splunk 安装中的 HTTP 事件收集器。我们需要打开一个新端口来暴露事件收集器,因为 Splunk 使用端口8088来收集数据。到目前为止,我们已经在 Splunk 安装中暴露了端口80009997。在我们继续本章的其余部分之前,让我们看看 Splunk 上所有可用的端口以及它们在 Splunk 上的功能:

  • 8000:您一直在使用这个端口进行 web 应用程序,这是用于在浏览器中访问 Splunk 的专用默认 web 端口。

  • 9997:这个端口是 Splunk 转发器用来将数据转发到索引器的默认端口。我们在本章的前一节中暴露了这个端口,以确保我们能够从正在运行的系统中收集日志。

  • 8089:Splunk 自带一个 API,默认作为搜索头的一部分运行。端口8089是 API 管理器所在的位置,用于与运行在您的实例上的 API 进行接口。

  • 8088:端口8088需要暴露以允许信息被转发到已在您的系统上设置的事件收集器。在即将进行的练习中,我们将使用这个端口开始将 Docker 容器日志发送到 HTTP 事件收集器。

  • 8080:如果我们有一个更大的 Splunk 安装,有专用的索引器,端口8080用于索引器之间的通信,并允许这些索引器之间的复制。

注意

Splunk 的 web 界面默认在端口8000上运行,但如果您在同一端口上托管应用程序,可能会与我们的 Panoramic Trekking App 发生冲突。如果这造成任何问题,请随意将 Splunk 容器上的端口暴露为不同的端口,例如端口8080,因为您仍然可以访问 web 界面,并且不会对使用该端口的我们的服务造成任何问题。

一旦在 Splunk 上设置了HTTP 事件收集器,将日志转发到 Splunk 只是在我们的docker run命令中添加正确的选项。以下示例命令使用--log-driver=splunk来向正在运行的容器发出信号,以使用 Splunk 日志驱动程序。

然后需要包括进一步的--log-opt选项,以确保日志被正确转发。第一个是splunk-url,这是您的系统当前托管的 URL。由于我们没有设置 DNS,我们可以简单地使用托管 Splunk 实例的 IP 地址,以及端口8088。第二个是splunk-token。这是在创建 HTTP 事件收集器时由 Splunk 分配的令牌:

docker run --log-driver=splunk \
--log-opt splunk-url=<splunk-url>:8088 \
--log-opt splunk-token=<event-collector-token> \
<docker-image>

您可以将 Splunk 日志驱动程序的详细信息添加到您的 Docker 配置文件中。在这里,您需要将以下详细信息添加到/etc/docker配置文件中的daemon.json文件中。只有当您将 Splunk 作为单独的应用程序而不是系统上的 Docker 实例时,这才能起作用。由于我们已将 Splunk 实例设置为 Docker 容器,因此此选项将不起作用。这是因为 Docker 守护程序将需要重新启动并连接到配置中列出的splunk-url。当然,在没有运行 Docker 守护程序的情况下,splunk-url将永远不可用。

{
  "log-driver": "splunk",
  "log-opts": {
    "splunk-token": "<splunk-token>",
    "splunk-url": "<splunk-url>::8088"
  }
}

在接下来的练习中,我们将扩展我们的 Splunk 安装,打开特定于我们的HTTP 事件收集器的端口,我们也将创建它。然后,我们将开始将日志从我们的容器发送到 Splunk,准备开始查看它们。

练习 14.02:创建 HTTP 事件收集器并开始收集 Docker 日志

在这个练习中,您将为您的 Splunk 安装创建一个HTTP 事件收集器,并使用 Dockerlog驱动程序将日志转发到您的事件收集器。您将使用chentex存储库提供的random-logger Docker 镜像,并可在 Docker Hub 上使用,以在系统中生成一些日志,并进一步演示 Splunk 的使用:

  1. 再次启动 Splunk 镜像,这次将端口8088暴露给所有我们的 Docker 容器,以将它们的日志推送到其中:
docker run --rm -d -p 8000:8000 -p 9997:9997 -p 8088:8088 \
 -e 'SPLUNK_START_ARGS=--accept-license' \
 -e 'SPLUNK_PASSWORD=changeme' \
 -v ${PWD}/testsplunk:/opt/splunk/etc/ \
 --name splunk splunk/splunk:latest
  1. 等待 Splunk 再次启动,并使用管理员账户重新登录 web 界面。

  2. 转到设置菜单,选择数据输入以创建新的HTTP 事件收集器。从选项列表中选择HTTP 事件收集器

  3. 单击HTTP 事件收集器页面上的全局设置按钮。您将看到一个类似以下内容的页面。在此页面上,单击启用按钮,旁边是所有令牌,并确保未选择启用 SSL,因为在这个练习中您将不使用 SSL。这将使您的操作变得更加简单。当您对屏幕上的细节满意时,单击保存按钮保存您的配置:图 14.11:在您的系统上启用 HTTP 事件收集器

图 14.11:在您的系统上启用 HTTP 事件收集器

  1. 当您返回到“HTTP 事件收集器”页面时,请点击屏幕右上角的“新令牌”按钮。您将看到一个类似以下的屏幕。在这里,您将设置新的事件收集器,以便可以收集 Docker 容器日志:图 14.12:在 Splunk 上命名您的 HTTP 事件收集器

图 14.12:在 Splunk 上命名您的 HTTP 事件收集器

前面的屏幕是您设置新事件收集器名称的地方。输入名称Docker Logs,对于其余的条目,通过将它们留空来接受默认值。点击屏幕顶部的“下一步”按钮。

  1. 接受“输入设置”和“审阅”页面的默认值,直到您看到一个类似以下的页面,在这个页面上创建了一个新的“HTTP 事件收集器”,并提供了一个可用的令牌。令牌显示为5c051cdb-b1c6-482f-973f-2a8de0d92ed8。您的令牌将不同,因为 Splunk 为用户信任的数据源提供了一个唯一的令牌,以便安全地传输数据。使用此令牌允许您的 Docker 容器开始在 Splunk 安装中记录数据:图 14.13:在 Splunk 上完成 HTTP 事件收集器

图 14.13:在 Splunk 上完成 HTTP 事件收集器

  1. 使用hello-world Docker 镜像,确保您可以将数据发送到 Splunk。在这种情况下,作为您的docker run命令的一部分,添加四个额外的命令行选项。指定--log-driversplunk。将日志选项指定为我们系统的splunk-url,包括端口8088splunk-token(您在上一步中创建的),最后,将splunk-=insecureipverify状态指定为true。这个最后的选项将限制在设置 Splunk 安装时所需的工作,这样您就不需要组织将与我们的 Splunk 服务器一起使用的 SSL 证书:
docker run --log-driver=splunk \
--log-opt splunk-url=http://127.0.0.1:8088 \
--log-opt splunk-token=5c051cdb-b1c6-482f-973f-2a8de0d92ed8 \
--log-opt splunk-insecureskipverify=true \
hello-world

命令应返回类似以下的输出:

Hello from Docker!
This message shows that your installation appears to be 
working correctly.
…
  1. 返回到 Splunk Web 界面,点击“开始搜索”按钮。如果您已经从上一个屏幕中移开,请转到 Splunk 搜索页面,网址为http://0.0.0.0:8000/en-US/app/search/search。在搜索查询框中,输入source="http:Docker Logs",如下截图所示。如果一切顺利,您还应该看到hello-world镜像提供的数据条目:图 14.14:开始使用 Splunk 收集 docker 日志

图 14.14:开始使用 Splunk 收集 docker 日志

  1. 上一步已经表明,Splunk 安装现在能够收集 Docker 日志数据,但您需要创建一个新的卷来存储您的索引数据,以便在停止 Splunk 运行时不被销毁。回到您的终端并杀死运行中的splunk容器:
docker kill splunk
  1. 在创建原始testsplunk目录的同一目录中,创建一个新目录,以便我们可以挂载我们的 Splunk 索引数据。在这种情况下,将其命名为testsplunkindex
mkdir testsplunkindex
  1. 从您的工作目录开始,再次启动 Splunk 镜像。挂载您刚刚创建的新目录,以存储您的索引数据:
docker run --rm -d -p 8000:8000 -p 9997:9997 -p 8088:8088 \
 -e 'SPLUNK_START_ARGS=--accept-license' \
 -e 'SPLUNK_PASSWORD=changeme' \
 -v ${PWD}/testsplunk:/opt/splunk/etc/ \
 -v ${PWD}/testsplunkindex:/opt/splunk/var/ \
 --name splunk splunk/splunk:latest
  1. 使用random-logger Docker 镜像在您的系统中生成一些日志。在以下命令中,有一个额外的tag日志选项。这意味着每个生成并发送到 Splunk 的日志事件也将包含此标签作为元数据,这可以帮助您在 Splunk 中搜索数据时进行搜索。通过使用{{.Name}}{{.FullID}}选项,这些细节将被自动添加,就像容器名称和 ID 号在创建容器时将被添加为您的标签一样:
docker run --rm -d --log-driver=splunk \
--log-opt splunk-url=http://127.0.0.1:8088 \
--log-opt splunk-token=5c051cdb-b1c6-482f-973f-2a8de0d92ed8 \
--log-opt splunk-insecureskipverify=true \
--log-opt tag="{{.Name}}/{{.FullID}}" \
--name log-generator chentex/random-logger:latest

注意

如果您的 Splunk 实例运行不正确,或者您没有正确配置某些内容,log-generator容器将无法连接或运行。您将看到类似以下的错误:

docker: Error response from daemon: failed to initialize logging driver:

  1. 一旦这个运行起来,回到 web 界面上的 Splunk 搜索页面,在这种情况下,包括你在上一步创建的标签。以下查询将确保只有log-generator镜像提供的新数据将显示在我们的 Splunk 输出中:
source="http:docker logs" AND "log-generator/"

您的 Splunk 搜索应该会产生类似以下的结果。在这里,您可以看到由log-generator镜像生成的日志。您可以看到它在随机时间记录,并且每个条目现在都带有容器的名称和实例 ID 作为标签:

图 14.15:Splunk 搜索结果

图 14.15:Splunk 搜索结果

我们的 Splunk 安装进展顺利,因为我们现在已经能够配置应用程序以包括HTTP 事件收集器,并开始从log-generator Docker 镜像中收集日志。即使我们停止 Splunk 实例,它们仍应可供我们搜索和提取有用信息。

下一节将提供如何使用 Splunk 查询语言的更深入演示。

使用 Splunk 查询语言

Splunk 查询语言可能有点难以掌握,但一旦掌握,您会发现它有助于解释、分析和呈现来自 Splunk 环境的数据。熟悉查询语言的最佳方法就是简单地开始使用。

在使用查询语言时需要考虑以下几点:

  • 缩小您的搜索范围:您想要搜索的数据量越大,您的查询就会花费更长的时间返回结果。如果您知道时间范围或源,比如我们为docker logs创建的源,查询将更快地返回结果。

  • 使用简单的搜索词条:如果您知道日志中会包含什么(例如,ERRORDEBUG),这是一个很好的起点,因为它还将帮助限制您接收到的数据量。这也是为什么在前一节中在向 Splunk 实例添加日志时我们使用了标签的另一个原因。

  • 链接搜索词条:我们可以使用AND来组合搜索词条。我们还可以使用OR来搜索具有多个搜索词条的日志。

  • 添加通配符以搜索多个词条:查询语言还可以使用通配符,比如星号。例如,如果您使用了ERR*查询,它将搜索不仅是ERROR,还有ERRERRORS

  • 提取的字段提供了更多的细节:Splunk 将尽其所能在日志事件中找到和定位字段,特别是如果您的日志采用已知的日志格式,比如 Apache 日志文件格式,或者是识别格式,比如 CSV 或 JSON 日志。如果您为您的应用程序创建日志,如果您将数据呈现为键值对,Splunk 将会出色地提取字段。

  • 添加函数来对数据进行分组和可视化:向搜索词条添加函数可以帮助您转换和呈现数据。它们通常与管道(|)字符一起添加到搜索词条中。下面的练习将使用statscharttimechart函数来帮助聚合搜索结果和计算统计数据,比如averagecountsum。例如,如果我们使用了一个搜索词条,比如ERR*,然后我们可以将其传输到stats命令来计算我们看到错误事件的次数:ERR* | stats count

Splunk 在输入查询时还提供了方便的提示。一旦您掌握了基础知识,它将帮助您为数据提供额外的功能。

在接下来的练习中,您将发现,即使 Splunk 找不到您提取的字段,您也可以创建自己的字段,以便分析您的数据。

练习 14.03:熟悉 Splunk 查询语言

在这个练习中,您将运行一系列任务,演示查询语言的基本功能,并帮助您更熟悉使用它。这将帮助您检查和可视化您自己的数据:

  1. 确保您的 Splunk 容器正在运行,并且log-generator容器正在向 Splunk 发送数据。

  2. 当您登录 Splunk 时,从主页,点击左侧菜单中的“搜索和报告应用程序”,或者转到 URLhttp://0.0.0.0:8000/en-US/app/search/search来打开搜索页面。

  3. 当您到达搜索页面时,您会看到一个文本框,上面写着“在此输入搜索”。从一个简单的术语开始,比如单词ERROR,如下截图所示,然后按Enter让 Splunk 运行查询:图 14.16:Splunk 搜索页面

图 14.16:Splunk 搜索页面

如果您只输入术语ERR*,并在术语的末尾加上一个星号(*),这也应该会产生类似于前面截图中显示的结果。

  1. 使用AND链接搜索项,以确保我们的日志事件包含多个值。输入类似于sourcetype=htt* AND ERR*的搜索,以搜索所有HTTP事件收集器日志,这些日志还显示其日志中的ERR值:图 14.17:链接搜索项

图 14.17:链接搜索项

  1. 您输入的搜索很可能默认搜索自安装以来的所有数据。查看所有数据可能会导致非常耗时的搜索。通过输入时间范围来缩小范围。单击查询文本框右侧的下拉菜单,以限制搜索运行的数据。将搜索限制为“最近 24 小时”:图 14.18:限制时间范围内的搜索

图 14.18:限制时间范围内的搜索

  1. 查看结果页面左侧的提取字段。您会注意到有两个部分。第一个是“已选择字段”,其中包括特定于您的搜索的数据。第二个是“有趣的字段”。这些数据仍然相关并且是您的数据的一部分,但与您的搜索查询没有特定关联:图 14.19:提取的字段

图 14.19:提取的字段

  1. 要创建要列出的字段,请点击“提取您自己的字段”链接。以下步骤将引导您完成创建与log-generator容器提供的数据相关的新字段的过程。

  2. 您将被带到一个新页面,其中将呈现您最近正在搜索的httpevent源类型的示例数据。首先,您需要选择一个示例事件。选择与此处列出的类似的第一行。点击屏幕顶部的“下一步”按钮,以继续下一步:

{"line":"2020-02-19T03:58:12+0000 ERROR something happened in this execution.","source":"stdout","tag":"log-generator/3eae26b23d667bb12295aaccbdf919c9370ffa50da9e401d0940365db6605e3"}
  1. 然后,您将被要求选择要使用的提取字段的方法。如果您正在处理具有明确分隔符的文件,例如.SSV文件,请使用“分隔符”方法。但在这种情况下,您将使用“正则表达式”方法。点击“正则表达式”,然后点击“下一步”按钮:图 14.20:字段提取方法

图 14.20:字段提取方法

  1. 现在您应该有一行数据,可以开始选择要提取的字段。由log-generator容器提供的所有日志数据都是相同的,因此此行将作为 Splunk 接收的所有事件的模板。如下截图所示,点击ERROR,当您有机会输入字段名称时,输入level,然后选择“添加提取”按钮。选择ERROR后的文本行。在此示例中,它是“此执行中发生了某事”。添加一个字段名称message。点击“添加提取”按钮。然后,在选择所有相关字段后,点击“下一步”按钮:图 14.21:Splunk 中的字段提取

图 14.21:Splunk 中的字段提取

  1. 您现在应该能够看到所有已突出显示的新字段的事件。点击“下一步”按钮:图 14.22:带有新字段的事件

图 14.22:带有新字段的事件

  1. 最后,你将看到一个类似以下的屏幕。在权限部分,点击所有应用按钮,允许此字段提取在整个 Splunk 安装中进行,而不限制在一个应用或所有者中。如果你对提取的名称和其他选项满意,点击屏幕顶部的完成按钮:图 14.23:在 Splunk 中完成字段提取

图 14.23:在 Splunk 中完成字段提取

  1. 返回到搜索页面,并在搜索查询中添加sourcetype=httpevent。加载完成后,浏览提取的字段。现在你应该有你添加的levelmessage字段作为感兴趣的字段。如果你点击level字段,你将得到接收事件数量的详细信息,类似于下面截图中显示的内容:图 14.24:在搜索结果中显示字段细分

图 14.24:在搜索结果中显示字段细分

  1. 使用stats函数来计算日志中每个错误级别的事件数量。通过使用sourcetype=httpevent | stats count by level搜索查询来获取上一步搜索结果的结果,并将stats函数的值传递给count by level图 14.25:使用 stats 函数

图 14.25:使用 stats 函数

  1. stats函数提供了一些很好的信息,但如果你想看到数据在一段时间内的呈现,可以使用timechart函数。运行sourcetype=httpevent | timechart span=1m count by level查询,以在一段时间内给出结果。如果你在过去 15 分钟内进行搜索,上述查询应该给出每分钟的数据细分。点击搜索查询文本框下的可视化选项卡。你将看到一个代表我们搜索结果的图表:图 14.26:从搜索结果创建可视化

图 14.26:从搜索结果创建可视化

你可以在查询中使用 span 选项来按分钟(1m)、小时(5)、天(1d)等对数据进行分组。

  1. 在前面的截图中,提到了图表类型(柱状图),你可以更改当前显示的类型。点击柱状图文本。它将让你从几种不同类型的图表中选择。在这种情况下,使用折线图:图 14.27:选择图表类型

图 14.27:选择图表类型

注意

在接下来的步骤中,你将为数据可视化创建一个仪表板。仪表板是一种向用户显示数据的方式,用户无需了解 Splunk 或涉及的数据的任何特定信息。对于非技术用户来说,这是完美的,因为你只需提供仪表板的 URL,用户就可以加载仪表板以查看他们需要的信息。仪表板也非常适合你需要定期执行的搜索,以限制你需要做的工作量。

  1. 当你对图表满意时,点击屏幕顶部的“另存为”按钮,然后选择“仪表板面板”。你将看到一个类似下面截图中所示的表单。创建一个名为“日志容器仪表板”的新仪表板,将其“共享在应用程序”(当前的搜索应用程序)中,并包括你刚创建的特定面板,命名为“错误级别”:图 14.28:从搜索结果创建仪表板

图 14.28:从搜索结果创建仪表板

  1. 点击“保存”按钮创建新仪表板。当你点击保存时,你将有机会查看你的仪表板。但如果你需要在以后查看仪表板,前往你创建仪表板的应用程序(在本例中是“搜索与报告”应用程序),然后点击屏幕顶部的“仪表板”菜单。你将看到可用的仪表板。在这里你可以点击相关的仪表板。你会注意到你有另外两个可用的仪表板,这是作为 Splunk 安装的默认部分提供的:图 14.29:Splunk 中的仪表板

图 14.29:Splunk 中的仪表板

  1. 打开你刚创建的“日志容器”仪表板,然后点击屏幕顶部的“编辑”按钮。这样可以让你在不需要返回搜索窗口的情况下向仪表板添加新面板。

  2. 当你点击“编辑”按钮时,你将获得额外的选项来更改仪表板的外观和感觉。现在点击“添加面板”按钮。

  3. 当你选择“添加面板”时,屏幕右侧将出现一些额外的选择。点击“新建”菜单选项,然后选择“单个数值”。

  4. 将面板命名为“总错误”,并将sourcetype=httpevent AND ERROR | stats count添加为搜索字符串。您可以添加新仪表板面板的屏幕应该类似于以下内容。它应该提供有关“内容标题”和“搜索字符串”的详细信息:图 14.30:将面板添加到您的 Splunk 仪表板

图 14.30:向您的 Splunk 仪表板添加面板

  1. 单击“添加到仪表板”按钮,将新面板添加到仪表板底部作为单个值面板。

  2. 在编辑模式下,您可以根据需要移动和调整面板的大小,并添加额外的标题或详细信息。当您对新面板感到满意时,请单击屏幕右上角的“保存”按钮。

希望您的仪表板看起来类似于以下内容:

图 14.31:向您的仪表板添加新面板

图 14.31:向您的仪表板添加新面板

最后,您的仪表板面板具有一些额外的功能,您可以通过单击屏幕右上角的省略号按钮找到这些功能。如果您对您的仪表板不满意,您可以从这里删除它。

  1. 单击“设置为主页仪表板面板”选项,该选项在省略号按钮下可用。这将带您回到 Splunk 主屏幕,在那里您的“日志容器仪表板”现在可用,并且在登录到 Splunk 时将是您看到的第一件事:图 14.32:日志容器仪表板

图 14.32:日志容器仪表板

这个练习向您展示了如何执行基本查询,如何使用函数将它们链接在一起,并开始创建可视化效果、仪表板和面板。虽然我们只花了很短的时间来讨论这个主题,但它应该让您更有信心进一步处理 Splunk 查询。

在下一节中,我们将看看 Splunk 应用程序是什么,以及它们如何帮助将您的数据、搜索、报告和仪表板分隔到不同的区域。

Splunk 应用程序和保存的搜索

Splunk 应用程序是一种让您将数据、搜索、报告和仪表板分隔到不同区域的方式,然后您可以配置谁可以访问什么。Splunk 提供了一个庞大的生态系统,帮助第三方开发人员和公司向公众提供这些应用程序。

我们在本章前面提到过,Splunk 还提供了“SplunkBase”,用于由 Splunk 为用户认证的已批准应用程序,例如用于思科网络设备的应用程序。它不需要是经过批准的应用程序才能在您的系统上使用。Splunk 允许您创建自己的应用程序,如果需要,您可以将它们打包成文件分发给希望使用它们的用户。Splunk 应用程序、仪表板和保存的搜索的整个目的是减少重复的工作量,并在需要时向非技术用户提供信息。

以下练习将为您提供一些关于使用 Splunk 应用程序的实际经验。

练习 14.04:熟悉 Splunk 应用程序和保存搜索

在这个练习中,您将从 SplunkBase 安装新的应用程序,并对其进行修改以满足您的需求。这个练习还将向您展示如何保存您的搜索以备将来使用。

  1. 确保您的 Splunk 容器正在运行,并且log-generator容器正在向 Splunk 发送数据。

  2. 当您重新登录 Splunk 时,请单击“应用程序”菜单中“应用程序”旁边的齿轮图标。当您进入“应用程序”页面时,您应该会看到类似以下内容。该页面包含当前安装在系统上的所有 Splunk 应用程序的列表。您会注意到一些应用程序已启用,而一些已禁用。

您还可以选择从 Splunk 应用程序库中浏览更多应用程序,安装来自文件的应用程序,或创建自己的 Splunk 应用程序:

图 14.33:在 Splunk 中使用应用程序页面

图 14.33:在 Splunk 中使用应用程序页面

  1. 单击屏幕顶部的“浏览更多应用程序”按钮。

  2. 您将进入一个页面,该页面提供了系统中所有可用的 Splunk 应用程序的列表。其中一些是付费的,但大多数是免费使用和安装的。您还可以按名称、类别和支持级别进行搜索。在屏幕顶部的搜索框中输入“离港板 Viz”,然后单击Enter图 14.34:离港板 Viz 应用程序

图 14.34:离港板 Viz 应用程序

注意

本节以Departures Board Viz应用为例,因为它易于使用和安装,只需进行最小的更改。每个应用程序都应该为您提供有关其使用的信息类型以及如何开始使用所需数据的一些详细信息。您会注意到有数百种应用程序可供选择,因此您一定会找到适合您需求的东西。

  1. 您需要在 Splunk 注册后才能安装和使用可用的应用程序。单击Departures Board Viz应用的“安装”按钮,并按照提示进行登录,如果需要的话:图 14.35:安装 Departures Board Viz 应用

图 14.35:安装 Departures Board Viz 应用

  1. 如果安装成功,您应该会收到提示,要么打开您刚刚安装的应用程序,要么返回到 Splunk 主页。返回主页以查看您所做的更改。

  2. 从主页,您现在应该看到已安装了名为Departures Board Viz的新应用程序。这只是一个可视化扩展。单击主屏幕上的Departures Board Vis按钮以打开该应用程序:图 14.36:打开 Departures Board Viz 应用

图 14.36:打开 Departures Board Viz 应用

  1. 当您打开应用程序时,它将带您到“关于”页面。这只是一个提供应用程序详细信息以及如何与您的数据一起使用的仪表板。单击屏幕顶部的“编辑”按钮以继续:图 14.37:Departures Board Viz 应用的“关于”页面

图 14.37:Departures Board Viz 应用的“关于”页面

  1. 单击“编辑搜索”以添加一个新的搜索,显示特定于您的数据。

  2. 删除默认搜索字符串,并将sourcetype=httpevent | stats count by level | sort - count | head 1 | fields level搜索查询放入文本框中。该查询将浏览您的log-generator数据,并提供每个级别的计数。然后,将结果从最高到最低排序(sort - count),并提供具有最高值的级别(head 1 | fields level):图 14.38:添加新的搜索查询

图 14.38:添加新的搜索查询

  1. 单击“保存”按钮以保存您对可视化所做的更改。您应该看到我们的数据提供的最高错误级别,而不是Departures Board Viz默认提供的城市名称。如下截图所示,我们日志中报告的最高错误是INFO图 14.39:在 Splunk 中编辑应用程序

图 14.39:在 Splunk 中编辑应用程序

  1. 现在您已经添加了一个 Splunk 应用程序,您将创建一个非常基本的应用程序来进一步修改您的环境。返回到主屏幕,再次点击“应用程序”菜单旁边的齿轮。

  2. 在“应用程序”页面上,单击屏幕右侧的“创建应用程序”按钮:图 14.40:Splunk 应用程序

图 14.40:Splunk 应用程序

  1. 当您创建自己的应用程序时,您将看到一个类似于此处所示的表单。您将为您的 Splunk 安装创建一个测试应用程序。使用以下截图中提供的信息填写表单,但确保为“名称”和“文件夹名称”添加值。版本也是一个必填字段,需要以major_version.minor_version.patch_version的形式。将版本号添加为1.0.0。以下示例还选择了sample_app选项,而不是barebones模板。这意味着该应用程序将填充有示例仪表板和报告,您可以修改这些示例以适应您正在处理的数据。您不会使用任何这些示例仪表板和报告,因此您可以选择任何一个。如果您有预先创建的 Splunk 应用程序可用,则只需要“上传资产”选项,但在我们的实例中,可以将其留空:图 14.41:创建 Splunk 应用程序

图 14.41:创建 Splunk 应用程序

  1. 单击“保存”按钮以创建您的新应用程序,然后返回到您的安装的主屏幕。您会注意到现在在主屏幕上列出了一个名为Test Splunk App的应用程序。单击您的新应用程序以打开它:图 14.42:主屏幕上的测试 Splunk 应用程序

图 14.42:主屏幕上的测试 Splunk 应用程序

  1. 该应用程序在“搜索和报告”应用程序中看起来并无不同,但是如果您点击屏幕顶部的“报告或仪表板”选项卡,您会注意到已经有一些示例报告和仪表板。不过,暂时创建一个您以后可以参考的报告。首先确保您在应用程序的“搜索”选项卡中。

  2. 在查询栏中输入sourcetype=httpevent earliest=-7d | timechart span=1d count by level。您会注意到我们已将值设置为earliest=-7d,这样会自动选择过去 7 天的数据,因此您无需指定搜索的时间范围。然后它将创建您的数据的时间图表,按每天的值进行汇总。

  3. 点击屏幕顶部的“另存为”按钮,然后从下拉菜单中选择“报告”。然后您将看到以下表单,以便保存您的报告。只需在点击屏幕底部的“保存”按钮之前命名报告并提供描述:图 14.43:在您的 Splunk 应用程序中创建保存的报告

图 14.43:在您的 Splunk 应用程序中创建保存的报告

  1. 当您点击“保存”时,您将有选项查看您的新报告。它应该看起来类似于以下内容:图 14.44:Splunk 中的每日错误级别报告

图 14.44:Splunk 中的每日错误级别报告

如果您以后需要再次参考此报告,可以点击您的新 Splunk 应用程序的“报告”选项卡,它将与应用程序首次创建时提供的示例报告一起列出。以下屏幕截图显示了您的应用程序的“报告”选项卡,其中列出了示例报告,但您还有刚刚创建的“每日错误”报告,它已添加到列表的顶部:

图 14.45:报告页面

图 14.45:报告页面

这就结束了这个练习,我们在其中安装了第三方 Splunk 应用程序并创建了自己的应用程序。这也是本章的结束。不过,在您继续下一章之前,请确保您通过下面提供的活动来重新确认您在本章学到的一切。

活动 14.01:为您的 Splunk 安装创建 docker-compose.yml 文件

到目前为止,您一直在使用docker run命令简单地在 Docker 容器上运行 Splunk。现在是时候利用您在本书前几节中所学到的知识,创建一个docker-compose.yml文件,以便在需要时在您的系统上安装和运行我们的 Splunk 环境。作为这个活动的一部分,添加作为全景徒步应用程序一部分运行的一个容器。还要确保您可以查看所选服务的日志。

执行以下步骤以完成此活动:

  1. 决定一旦作为 Docker Compose 文件的一部分运行起来,您希望您的 Splunk 安装看起来如何。这将包括挂载目录和需要作为安装的一部分暴露的端口。

  2. 创建您的docker-compose.yml文件并运行Docker Compose。确保它根据上一步中的要求启动您的 Splunk 安装。

  3. 一旦 Splunk 安装运行起来,启动全景徒步应用程序的一个服务,并确保您可以将日志数据发送到您的 Splunk 设置中。

预期输出:

这应该会产生一个类似以下的屏幕:

图 14.46:活动 14.01 的预期输出

图 14.46:活动 14.01 的预期输出

注意

此活动的解决方案可以通过此链接找到。

下一个活动将允许您为 Splunk 中记录的新数据创建一个 Splunk 应用程序和仪表板。

活动 14.02:创建一个 Splunk 应用程序来监视全景徒步应用程序

在上一个活动中,您确保了作为全景徒步应用程序的一部分设置的一个服务正在使用您的 Splunk 环境记录数据。在这个活动中,您需要在您的安装中创建一个新的 Splunk 应用程序,以专门监视您的服务,并创建一个与向 Splunk 记录数据的服务相关的仪表板。

您需要按照以下步骤完成此活动:

  1. 确保您的 Splunk 安装正在运行,并且来自全景徒步应用程序的至少一个服务正在向 Splunk 记录数据。

  2. 创建一个新的 Splunk 应用程序,并为监视全景徒步应用程序命名一个相关的名称。确保您可以从 Splunk 主屏幕查看它。

  3. 创建一个与您正在监视的服务相关的仪表板,并添加一些可视化效果,以帮助您监视您的服务。

预期输出:

成功完成此活动后,应显示类似以下的仪表板:

图 14.47:活动 14.02 的预期解决方案

图 14.47:活动 14.02 的预期解决方案

注意

此活动的解决方案可通过此链接找到。

总结

本章教会了您如何使用诸如 Splunk 之类的应用程序来帮助您通过将容器日志聚合到一个中心区域来监视和排除故障您的应用程序。我们从讨论在使用 Docker 时日志管理策略的重要性开始了本章,然后通过讨论其架构以及如何运行该应用程序的一些要点来介绍了 Splunk。

我们直接使用 Splunk 运行 Docker 容器映像,并开始将日志从我们的运行系统转发。然后,我们使用 Splunk 日志驱动程序将我们的容器日志直接发送到我们的 Splunk 容器,挂载重要目录以确保我们的数据即使在停止容器运行后也能保存和可用。最后,我们更仔细地研究了 Splunk 查询语言,通过它我们创建了仪表板和保存搜索,并考虑了 Splunk 应用程序生态系统的优势。

下一章将介绍 Docker 插件,并教您如何利用它们来帮助扩展您的容器和运行在其上的服务。

第十五章:通过插件扩展 Docker

概述

在本章中,您将学习如何通过创建和安装插件来扩展 Docker Engine 的功能。您将了解如何在使用 Docker 容器时实现高级和自定义需求。在本章结束时,您将能够识别扩展 Docker 的基础知识。您还将能够安装和配置不同的 Docker 插件。此外,您将使用 Docker 插件 API 来开发自定义插件,并使用各种 Docker 插件来扩展 Docker 中卷、网络和授权的功能。

介绍

在之前的章节中,您使用 Docker Compose 和 Docker Swarm 运行了多个 Docker 容器。此外,您监控了容器的指标并收集了日志。Docker 允许您管理容器的完整生命周期,包括网络、卷和进程隔离。如果您想要定制 Docker 的操作以适应您的自定义存储、网络提供程序或身份验证服务器,您需要扩展 Docker 的功能。

例如,如果您有一个自定义的基于云的存储系统,并希望将其挂载到 Docker 容器中,您可以实现一个存储插件。同样,您可以使用授权插件从企业用户管理系统对用户进行身份验证,并允许他们与 Docker 容器一起工作。

在本章中,您将学习如何通过插件扩展 Docker。您将从插件管理和 API 开始,然后学习最先进和最受欢迎的插件类型:授权、网络和卷。接下来的部分将涵盖在 Docker 中安装和操作插件。

插件管理

Docker 中的插件是独立于 Docker Engine 运行的外部进程。这意味着 Docker Engine 不依赖于插件,反之亦然。我们只需要告知 Docker Engine 有关插件位置和其功能。Docker 提供以下 CLI 命令来管理插件的生命周期:

  • docker plugin create:此命令创建新的插件及其配置。

  • docker plugin enable/disable:这些命令启用或禁用插件。

  • docker plugin install:此命令安装插件。

  • docker plugin upgrade:此命令将现有插件升级到更新版本。

  • docker plugin rm:此命令通过从 Docker Engine 中删除其信息来删除插件。

  • docker plugin ls:此命令列出已安装的插件。

  • docker plugin inspect:此命令显示有关插件的详细信息。

在接下来的部分中,您将学习如何使用插件 API 在 Docker 中实现插件。

插件 API

Docker 维护插件 API,以帮助社区编写他们的插件。这意味着只要按照插件 API 的规定实现,任何人都可以开发新的插件。这种方法使 Docker 成为一个开放和可扩展的平台。插件 API 是一种远程过程调用RPC)风格的 JSON API,通过 HTTP 工作。Docker 引擎向插件发送 HTTP POST 请求,并使用响应来继续其操作。

Docker 还提供了一个官方的开源 SDK,用于创建新的插件和辅助包以扩展 Docker 引擎。辅助包是样板模板,如果您想轻松创建和运行新的插件。目前,由于 Go 是 Docker 引擎本身的主要实现语言,因此只有 Go 中的辅助包。它位于github.com/docker/go-plugins-helpers,并为 Docker 支持的每种插件提供辅助程序:

图 15.1:Go 插件助手

图 15.1:Go 插件助手

您可以检查存储库中列出的每个文件夹,以便轻松创建和运行不同类型的插件。在本章中,您将通过几个实际练习来探索支持的插件类型,即授权、网络和卷插件。这些插件使 Docker 引擎能够通过提供额外的功能来实现自定义业务需求,同时还具有默认的 Docker 功能。

授权插件

Docker 授权基于两种模式:启用所有类型的操作禁用所有类型的操作。换句话说,如果用户可以访问 Docker 守护程序,他们可以运行任何命令并使用 API 或 Docker 客户端命令。如果需要更细粒度的访问控制方法,则需要在 Docker 中使用授权插件。授权插件增强了 Docker 引擎操作的身份验证和权限。它们使得可以更细粒度地控制谁可以在 Docker 引擎上执行特定操作。

授权插件通过请求上下文批准或拒绝 Docker 守护程序转发的请求。因此,插件应实现以下两种方法:

  • AuthZReq:在 Docker 守护程序处理请求之前调用此方法。

  • AuthZRes:在从 Docker 守护程序返回响应给客户端之前调用此方法。

在接下来的练习中,您将学习如何配置和安装授权插件。您将安装由 Open Policy Agent 创建和维护的基于策略的授权插件(www.openpolicyagent.org/)。基于策略的访问是基于根据一些规则(即策略)授予用户访问权限的想法。插件的源代码可在 GitHub 上找到(github.com/open-policy-agent/opa-docker-authz),它与类似以下的策略文件一起使用:

package docker.authz 
allow {
    input.Method = "GET"
}

策略文件存储在 Docker 守护程序可以读取的主机系统中。例如,这里显示的策略文件只允许GET作为请求的方法。它实际上通过禁止任何其他方法(如POSTDELETEUPDATE)使 Docker 守护程序变为只读。在接下来的练习中,您将使用一个策略文件并配置 Docker 守护程序与授权插件通信并限制一些请求。

注意

在以下练习中,插件和命令在 Linux 环境中效果最佳,考虑到 Docker 守护程序的安装和配置。如果您使用的是自定义或工具箱式的 Docker 安装,您可能希望使用虚拟机来完成本章的练习。

注意

请使用touch命令创建文件,并使用vim命令在 vim 编辑器中处理文件。

练习 15.01:具有授权插件的只读 Docker 守护程序

在这个练习中,您需要创建一个只读的 Docker 守护程序。如果您想限制对生产环境的访问和更改,这是一种常见的方法。为了实现这一点,您将安装并配置一个带有策略文件的插件。

要完成练习,请执行以下步骤:

  1. 通过运行以下命令在/etc/docker/policies/authz.rego位置创建一个文件:
mkdir -p /etc/docker/policies
touch /etc/docker/policies/authz.rego
ls /etc/docker/policies

这些命令创建一个位于/etc/docker/policies的文件:

authz.rego
  1. 用编辑器打开文件并插入以下数据:
package docker.authz 
allow {
    input.Method = "GET"
}

您可以使用以下命令将内容写入文件中:

cat > /etc/docker/policies/authz.rego << EOF
package docker.authz 
allow {
    input.Method = "GET"
}
EOF
cat /etc/docker/policies/authz.rego

注意

cat命令用于在终端中使文件内容可编辑。除非您在无头模式下运行 Ubuntu,否则可以跳过使用基于 CLI 的命令来编辑文件内容。

策略文件仅允许 Docker 守护程序中的GET方法;换句话说,它使 Docker 守护程序变为只读。

  1. 通过在终端中运行以下命令安装插件,并在提示权限时输入y
docker plugin install --alias opa-docker-authz:readonly \
openpolicyagent/opa-docker-authz-v2:0.5 \
opa-args="-policy-file /opa/policies/authz.rego"

该命令安装位于openpolicyagent/opa-docker-authz-v2:0.5的插件,并使用别名opa-docker-authz:readonly。此外,来自步骤 1的策略文件被传递为opa-args

图 15.2:插件安装

图 15.2:插件安装

  1. 使用以下命令检查已安装的插件:
docker plugin ls

该命令列出插件:

图 15.3:插件列表

图 15.3:插件列表

  1. 使用以下版本编辑 Docker 守护程序配置位于/etc/docker/daemon.json
{
    "authorization-plugins": ["opa-docker-authz:readonly"]
}

您可以使用cat /etc/docker/daemon.json命令检查文件的内容。

  1. 使用以下命令重新加载 Docker 守护程序:
sudo kill -HUP $(pidof dockerd)

该命令通过使用pidof命令获取dockerd的进程 ID 来终止dockerd的进程。此外,它发送HUP信号,这是发送给 Linux 进程以更新其配置的信号。简而言之,您正在使用新的授权插件配置重新加载 Docker 守护程序。运行以下列出命令以检查列出操作是否被允许:

docker ps

该命令列出正在运行的容器,并显示列出操作是允许的:

CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
  1. 运行以下命令以检查是否允许创建新容器:
docker run ubuntu

该命令创建并运行一个容器;但是,由于该操作不是只读的,因此不被允许:

Error response from daemon: authorization denied by plugin 
opa-docker-authz:readonly: request rejected by administrative policy.
See 'docker run –-help'.
  1. 检查 Docker 守护程序的日志是否有任何与插件相关的行:
journalctl -u docker | grep plugin | grep "OPA policy decision"

注意

journalctl是用于显示来自systemd进程的日志的命令行工具。systemd进程以二进制格式存储日志。需要journalctl来读取日志文本。

以下输出显示步骤 7步骤 8中测试的操作通过授权插件,并显示了"Returning OPA policy decision: true""Returning OPA policy decision: false"行。它显示我们的插件已允许第一个操作并拒绝了第二个操作:

图 15.4:插件日志

图 15.4:插件日志

  1. 通过从/etc/docker/daemon.json中删除authorization-plugins部分并重新加载 Docker 守护程序,停止使用插件,类似于步骤 6中所做的操作:
cat > /etc/docker/daemon.json << EOF
{}
EOF
cat /etc/docker/daemon.json
sudo kill -HUP $(pidof dockerd)
  1. 通过以下命令禁用和删除插件:
docker plugin disable opa-docker-authz:readonly 
docker plugin rm opa-docker-authz:readonly  

这些命令通过返回插件的名称来禁用和删除 Docker 中的插件。

在这个练习中,您已经配置并安装了一个授权插件到 Docker 中。在下一节中,您将学习更多关于 Docker 中的网络插件。

网络插件

Docker 通过 Docker 网络插件支持各种网络技术。虽然它支持容器对容器和主机对容器的完整功能的网络,但插件使我们能够将网络扩展到更多的技术。网络插件实现了远程驱动程序作为不同网络拓扑的一部分,比如虚拟可扩展局域网(vxlan)和 MAC 虚拟局域网(macvlan)。您可以使用 Docker 插件命令安装和启用网络插件。此外,您需要使用--driver标志指定网络驱动程序的名称。例如,如果您已经安装并启用了my-new-network-technology驱动程序,并且希望您的新网络成为其中的一部分,您需要设置一个driver标志:

docker network create --driver my-new-network-technology mynet

这个命令创建了一个名为mynet的网络,而my-new-network-technology插件管理所有网络操作。

社区和第三方公司开发了网络插件。然而,目前在 Docker Hub 上只有两个经过认证的网络插件 - Weave Net 和 Infoblox IPAM Plugin。

图 15.5:Docker Hub 中的网络插件

图 15.5:Docker Hub 中的网络插件

Infoblox IPAM Plugin专注于提供 IP 地址管理服务,比如编写 DNS 记录和配置 DHCP 设置。Weave Net专注于为 Docker 容器创建弹性网络,具有加密、服务发现和组播网络。

go-plugin-helpers提供的官方 SDK 中,有用于为 Docker 创建网络扩展的 Go 处理程序。Driver接口定义如下:

// Driver represent the interface a driver must fulfill.
type Driver interface {
     GetCapabilities() (*CapabilitiesResponse, error)
     CreateNetwork(*CreateNetworkRequest) error
     AllocateNetwork(*AllocateNetworkRequest)        (*AllocateNetworkResponse, error)
     DeleteNetwork(*DeleteNetworkRequest) error
     FreeNetwork(*FreeNetworkRequest) error
     CreateEndpoint(*CreateEndpointRequest)        (*CreateEndpointResponse, error)
     DeleteEndpoint(*DeleteEndpointRequest) error
     EndpointInfo(*InfoRequest) (*InfoResponse, error)
     Join(*JoinRequest) (*JoinResponse, error)
     Leave(*LeaveRequest) error
     DiscoverNew(*DiscoveryNotification) error
     DiscoverDelete(*DiscoveryNotification) error
     ProgramExternalConnectivity(*ProgramExternalConnectivityRequest)        error
     RevokeExternalConnectivity(*RevokeExternalConnectivityRequest)        error
}

注意

完整的代码可在github.com/docker/go-plugins-helpers/blob/master/network/api.go找到。

当您检查接口功能时,网络插件应提供网络、端点和外部连接的操作。例如,网络插件应使用CreateNetworkAllocateneNetworkDeleteNetworkFreeNetwork函数实现网络生命周期。

同样,端点生命周期应该由CreateEndpointDeleteEndpointEndpointInfo函数实现。此外,还有一些扩展集成和管理函数需要实现,包括GetCapabilitiesLeaveJoin。服务还需要它们特定的请求和响应类型以在托管插件环境中工作。

在接下来的练习中,您将使用 Weave Net 插件创建一个新网络,并让容器使用新网络连接。

练习 15.02:Docker 网络插件实战

Docker 网络插件接管特定网络实例的网络操作并实现自定义技术。在这个练习中,您将安装和配置一个网络插件来创建一个 Docker 网络。然后,您将创建一个 Docker 镜像的三个副本应用程序,并使用插件连接这三个实例。您可以使用 Weave Net 插件来实现这个目标。

要完成练习,请执行以下步骤:

  1. 通过在终端中运行以下命令初始化 Docker swarm(如果之前未启用):
docker swarm init

此命令创建一个 Docker swarm 以部署多个应用程序实例:

图 15.6:Swarm 初始化

图 15.6:Swarm 初始化

  1. 通过运行以下命令安装Weave Net插件:
docker plugin install --grant-all-permissions \
store/weaveworks/net-plugin:2.5.2

此命令从商店安装插件并授予所有权限:

图 15.7:插件安装

图 15.7:插件安装

  1. 使用以下命令使用驱动程序创建新网络:
docker network create  \
--driver=store/weaveworks/net-plugin:2.5.2  \
weave-custom-net

使用插件提供的驱动程序创建名为weave-custom-net的新网络:

图 15.8:创建网络

图 15.8:创建网络

成功创建网络后,将打印出随机生成的网络名称,如前面的代码所示。

  1. 使用以下命令创建一个三个副本的应用程序:
docker service create --network=weave-custom-net \
--replicas=3 \
--name=workshop \
-p 80:80 \
onuryilmaz/hello-plain-text

该命令创建了onuryilmaz/hello-plain-text镜像的三个副本,并使用the weave-custom-net网络连接实例。此外,它使用名称workshop并发布到端口80

图 15.9:应用程序创建

图 15.9:应用程序创建

  1. 通过运行以下命令获取容器的名称:
FIRST_CONTAINER=$(docker ps --format "{{.Names}}" |grep "workshop.1")
echo $FIRST_CONTAINER
SECOND_CONTAINER=$(docker ps --format "{{.Names}}" |grep "workshop.2")
echo $SECOND_CONTAINER
THIRD_CONTAINER=$(docker ps --format "{{.Names}}" |grep "workshop.3")
echo $THIRD_CONTAINER

这些命令列出了正在运行的 Docker 容器名称,并按workshop实例进行过滤。您将需要容器的名称来测试它们之间的连接:

图 15.10:容器名称

图 15.10:容器名称

  1. 运行以下命令将第一个容器连接到第二个容器:
docker exec -it $FIRST_CONTAINER sh -c "curl $SECOND_CONTAINER" 

该命令使用curl命令连接第一个和第二个容器:

图 15.11:容器之间的连接

图 15.11:容器之间的连接

上述命令在第一个容器内运行,并且curl命令到达第二个容器。输出显示了服务器和请求信息。

  1. 类似于步骤 6,将第一个容器连接到第三个容器:
docker exec -it $FIRST_CONTAINER sh -c "curl $THIRD_CONTAINER" 

如预期的那样,在步骤 6步骤 7中检索到了不同的服务器名称和地址:

图 15.12:容器之间的连接

图 15.12:容器之间的连接

这表明使用自定义 Weave Net 网络创建的容器正在按预期工作。

  1. 您可以使用以下命令删除应用程序和网络:
docker service rm workshop
docker network rm weave-custom-net

在这个练习中,您已经在 Docker 中安装并使用了一个网络插件。除此之外,您还创建了一个使用自定义网络驱动程序连接的容器化应用程序。在下一节中,您将学习更多关于 Docker 中的卷插件。

卷插件

Docker 卷被挂载到容器中,以允许有状态的应用程序在容器中运行。默认情况下,卷是在主机文件系统中创建并由 Docker 管理的。此外,在创建卷时,可以指定卷驱动程序。例如,您可以挂载网络或存储提供程序(如 Google、Azure 或 AWS)的卷。您还可以在 Docker 容器中本地运行数据库,而数据卷在 AWS 存储服务中是持久的。这样,您的数据卷可以在将来与在任何其他位置运行的其他数据库实例一起重用。要使用不同的卷驱动程序,您需要使用卷插件增强 Docker。

Docker 卷插件控制卷的生命周期,包括CreateMountUnmountPathRemove等功能。在插件 SDK 中,卷驱动程序接口定义如下:

// Driver represent the interface a driver must fulfill.
type Driver interface {
     Create(*CreateRequest) error
     List() (*ListResponse, error)
     Get(*GetRequest) (*GetResponse, error)
     Remove(*RemoveRequest) error
     Path(*PathRequest) (*PathResponse, error)
     Mount(*MountRequest) (*MountResponse, error)
     Unmount(*UnmountRequest) error
     Capabilities() *CapabilitiesResponse
}

注意

完整的驱动程序代码可在github.com/docker/go-plugins-helpers/blob/master/volume/api.go找到。

驱动程序接口的功能显示,卷驱动程序专注于卷的基本操作,如CreateListGetRemove操作。插件负责将卷挂载到容器中并从容器中卸载。如果要创建新的卷驱动程序,需要使用相应的请求和响应类型实现此接口。

Docker Hub 和开源社区已经提供了大量的卷插件。例如,目前在 Docker Hub 上已经分类和验证了 18 个卷插件:

图 15.13:Docker Hub 上的卷插件

图 15.13:Docker Hub 上的卷插件

大多数插件专注于从不同来源提供存储,如云提供商和存储技术。根据您的业务需求和技术堆栈,您可以在 Docker 设置中考虑卷插件。

在接下来的练习中,您将使用 SSH 连接在远程系统中创建卷,并在容器中创建卷。对于通过 SSH 连接创建和使用的卷,您将使用github.com/vieux/docker-volume-sshfs上提供的open-source docker-volume-sshfs插件。

练习 15.03:卷插件的实际应用

Docker 卷插件通过从不同提供商和技术提供存储来管理卷的生命周期。在这个练习中,您将安装和配置一个卷插件,以通过 SSH 连接创建卷。在成功创建卷之后,您将在容器中使用它们,并确保文件被持久化。您可以使用docker-volume-sshfs插件来实现这个目标。

要完成这个练习,请执行以下步骤:

  1. 在终端中运行以下命令安装docker-volume-sshfs插件:
docker plugin install --grant-all-permissions vieux/sshfs

此命令通过授予所有权限来安装插件:

图 15.14:插件安装

图 15.14:插件安装

  1. 使用以下命令创建一个带有 SSH 连接的 Docker 容器,以便为其他容器提供卷:
docker run -d -p 2222:22 \
--name volume_provider \
rastasheep/ubuntu-sshd:14.04

此命令创建并运行一个名为volume_providersshd容器。端口2222被发布,并将在接下来的步骤中用于连接到此容器。

您应该会得到以下输出:

87eecaca6a1ea41e682e300d077548a4f902fdda21acc218a51253a883f725d
  1. 通过运行以下命令创建一个名为volume-over-ssh的新卷:
docker volume create -d vieux/sshfs \
--name volume-over-ssh \
-o sshcmd=root@localhost:/tmp \
-o password=root \
-o port=2222

此命令使用vieux/sshfs驱动程序和sshcmdpasswordport参数指定的ssh连接创建一个新卷:

volume-over-ssh
  1. 通过运行以下命令在步骤 3中创建的卷中创建一个新文件并保存:
docker run --rm -v volume-over-ssh:/data busybox \
sh -c "touch /data/test.txt && echo 'Hello from Docker Workshop' >> /data/test.txt"

此命令通过挂载volume-over-ssh来运行一个容器。然后创建一个文件并写入其中。

  1. 通过运行以下命令检查步骤 4中创建的文件的内容:
docker run --rm -v volume-over-ssh:/data busybox \
cat /data/test.txt

此命令通过挂载相同的卷来运行一个容器,并从中读取文件:

Hello from Docker Workshop
  1. (可选)通过运行以下命令删除卷:
docker volume rm volume-over-ssh

在这个练习中,您已经在 Docker 中安装并使用了卷插件。此外,您已经创建了一个卷,并从多个容器中用于写入和读取。

在接下来的活动中,您将使用网络和卷插件在 Docker 中安装 WordPress。

活动 15.01:使用网络和卷插件安装 WordPress

您的任务是在 Docker 中使用网络和卷插件设计和部署博客及其数据库作为微服务。您将使用 WordPress,因为它是最流行的内容管理系统,被超过三分之一的网站使用。存储团队要求您使用 SSH 来进行 WordPress 内容的卷。此外,网络团队希望您在容器之间使用 Weave Net 进行网络连接。使用这些工具,您将使用 Docker 插件创建网络和卷,并将它们用于 WordPress 及其数据库:

  1. 使用 Weave Net 插件创建一个名为wp-network的 Docker 网络。

  2. 使用vieux/sshfs驱动程序创建名为wp-content的卷。

  3. 创建一个名为mysql的容器来运行mysql:5.7镜像。确保设置MYSQL_ROOT_PASSWORDMYSQL_DATABASEMYSQL_USERMYSQL_PASSWORD环境变量。此外,容器应该使用步骤 1中的wp-network

  4. 创建一个名为wordpress的容器,并使用步骤 2中挂载在/var/www/html/wp-content的卷。对于 WordPress 的配置,不要忘记根据步骤 3设置WORDPRESS_DB_HOSTWORDPRESS_DB_USERWORDPRESS_DB_PASSWORDWORDPRESS_DB_NAME环境变量。此外,您需要将端口80发布到端口8080,可以从浏览器访问。

您应该运行wordpressmysql容器:

图 15.15:WordPress 和数据库容器

图 15.15:WordPress 和数据库容器

此外,您应该能够在浏览器中访问 WordPress 设置屏幕:

图 15.16:WordPress 设置屏幕

图 15.16:WordPress 设置屏幕

此活动的解决方案可以通过此链接找到。

摘要

本章重点介绍了使用插件扩展 Docker。通过安装和使用 Docker 插件,可以通过自定义存储、网络或授权方法增强 Docker 操作。您首先考虑了 Docker 中的插件管理和插件 API。通过插件 API,您可以通过编写新插件来扩展 Docker,并使 Docker 为您工作。

本章然后涵盖了授权插件以及 Docker 守护程序如何配置以与插件一起工作。如果您在生产或企业环境中使用 Docker,授权插件是控制谁可以访问您的容器的重要工具。然后您探索了网络插件以及它们如何扩展容器之间的通信。

尽管 Docker 已经涵盖了基本的网络功能,但我们看了一下网络插件如何成为新网络功能的入口。这导致了最后一部分,其中介绍了卷插件,以展示如何在 Docker 中启用自定义存储选项。如果您的业务环境或技术堆栈要求您扩展 Docker 的功能,学习插件以及如何使用它们是至关重要的。

本章的结尾也标志着本书的结束。你从第一章开始学习 Docker 的基础知识,运行了你的第一个容器,看看你已经走了多远。在本书的学习过程中,你使用 Dockerfile 创建了自己的镜像,并学会了如何使用 Docker Hub 等公共仓库发布这些镜像,或者将它们存储在你的系统上运行的仓库中。你学会了使用多阶段的 Dockerfile,并使用 docker-compose 来实现你的服务。你甚至掌握了网络和容器存储的细节,以及在项目中实施 CI/CD 流水线和在 Docker 镜像构建中进行测试。

你练习了使用 Docker Swarm 和 Kubernetes 等应用程序编排你的 Docker 环境,然后更深入地了解了 Docker 安全和容器最佳实践。你的旅程继续进行,监控你的服务指标和容器日志,最后使用 Docker 插件来帮助扩展你的容器服务功能。我们涵盖了大量内容,以提高你对 Docker 的技能和知识。希望这将使你的应用经验达到一个新的水平。请参考交互式版本,了解如何在出现问题时进行故障排除和报告。你还将了解 Docker Enterprise 的当前状态以及在 Docker 的使用和开发方面即将迈出的重要步伐。

附录

1. 运行我的第一个 Docker 容器

活动 1.01:从 Docker Hub 拉取并运行 PostgreSQL 容器映像

解决方案

  1. 要启动 Postgres Docker 容器,首先确定需要设置数据库的默认用户名和密码凭据的环境变量。通过阅读官方 Docker Hub 页面,您可以看到POSTGRES_USERPOSTGRES_PASSWORD环境变量的配置选项。使用-e标志传递环境变量。启动我们的 Postgres Docker 容器的最终命令如下:
docker run -itd -e "POSTGRES_USER=panoramic" -e "POSTGRES_PASSWORD=trekking" postgres:12

运行此命令将启动容器。

  1. 执行docker ps命令以验证其正在运行并且健康:
$ docker ps

该命令应返回如下输出:

CONTAINER ID  IMAGE         COMMAND                 CREATED
  STATUS              PORTS               NAMES
29f115af8cdd  postgres:12   "docker-entrypoint.s…"  4 seconds ago
  Up 2 seconds        5432/tcp            blissful_kapitsa

从前面的输出可以看出,具有 ID29f115af8cdd的容器正在运行。

在这个活动中,您已成功启动了一个 PostgreSQL 版本 12 容器,该容器是 Panoramic Trekking App 的一部分,该应用程序将在本书的过程中构建。

活动 1.02:访问 Panoramic Trekking App 数据库

解决方案

  1. 使用docker exec登录到数据库实例,启动容器内的 PSQL shell,传递--username标志并将--password标志留空:
$ docker exec -it <containerID> psql --username panoramic --password

这应提示您输入密码并启动 PSQL shell。

  1. 使用\l命令列出所有数据库:
psql (12.2 (Debian 12.2-2.pgdg100+1))
Type "help" for help.
panoramic=# \l

将返回在容器中运行的数据库列表:

图 1.4:数据库列表

图 1.4:数据库列表

  1. 最后,使用\q快捷方式退出 shell。

  2. 使用docker stopdocker rm命令停止和清理容器实例。

在这个活动中,您通过使用在Activity 1.01中设置的凭据登录到容器中运行的数据库。您还列出了在容器中运行的数据库。该活动让您亲身体验了如何使用 PSQL shell 访问在任何容器中运行的数据库。

2. 使用 Dockerfiles 入门

活动 2.01:在 Docker 容器上运行 PHP 应用程序

解决方案

  1. 为此活动创建一个名为activity-02-01的新目录:
mkdir activity-02-01
  1. 导航到新创建的activity-02-01目录:
cd activity-02-01
  1. activity-02-01目录中,创建一个名为welcome.php的文件:
touch welcome.php 
  1. 现在,使用您喜欢的文本编辑器打开welcome.php
vim welcome.php 
  1. 创建 welcome.php 文件,并使用活动开始时提供的内容,然后保存并退出 welcome.php 文件:
<?php
$hourOfDay = date('H');
if($hourOfDay < 12) {
    $message = «Good Morning»;
} elseif($hourOfDay > 11 && $hourOfDay < 18) {
    $message = «Good Afternoon»;
} elseif($hourOfDay > 17){
    $message = «Good Evening»;
}
echo $message;
?>
  1. activity-02-01 目录中,创建一个名为 Dockerfile 的文件:
touch Dockerfile
  1. 现在,使用您喜爱的文本编辑器打开 Dockerfile
vim Dockerfile
  1. 将以下内容添加到 Dockerfile 中,然后保存并退出 Dockerfile
# Start with Ubuntu base image
FROM ubuntu:18.04
# Set labels
LABEL maintainer=sathsara
LABEL version=1.0 
# Set environment variables
ENV DEBIAN_FRONTEND=noninteractive
# Install Apache, PHP, and other packages
RUN apt-get update && \
    apt-get -y install apache2 \
    php \ 
    curl
# Copy all php files to the Docker image
COPY *.php /var/www/html
# Set working directory
WORKDIR /var/www/html
# Create health check
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD curl -f   http://localhost || exit 1
# Expose Apache
EXPOSE 80
# Start Apache
ENTRYPOINT ["apache2ctl", "-D", "FOREGROUND"]

我们将使用 ubuntu 基础镜像开始这个 Dockerfile,然后设置一些标签。接下来,将 DEBIAN_FRONTEND 环境变量设置为 noninteractive,以使包安装变为非交互式。然后安装 apache2phpcurl 包,并将 PHP 文件复制到 /var/www/html 目录。接下来,配置健康检查并暴露端口 80。最后,使用 apache2ctl 命令启动 Apache web 服务器。

  1. 现在,构建 Docker 镜像:
$ docker image build -t activity-02-01 .

运行 build 命令后,您应该会得到以下输出:

图 2.22:构建活动-02-01 Docker 镜像

图 2.22:构建活动-02-01 Docker 镜像

  1. 执行 docker container run 命令以从您在上一步中构建的 Docker 镜像启动新容器:
$ docker container run -p 80:80 --name activity-02-01-container -d activity-02-01

由于您是以分离模式(使用 -d 标志)启动 Docker 容器,上述命令将输出生成的 Docker 容器的 ID。

  1. 现在,您应该能够查看 Apache 主页。在您喜爱的网络浏览器中转到 http://127.0.0.1/welcome.php 终端节点:图 2.23:PHP 应用程序页面

图 2.23:PHP 应用程序页面

请注意,默认的 Apache 主页是可见的。在前面的输出中,您收到了 Good Morning 的输出。此输出可能会有所不同,根据您运行此容器的时间,可能会显示为 Good AfternoonGood Evening

  1. 现在,清理容器。首先,使用 docker container stop 命令停止 Docker 容器:
$ docker container stop activity-02-01-container
  1. 最后,使用 docker container rm 命令移除 Docker 容器:
$ docker container rm activity-02-01-container

在这个活动中,我们学习了如何使用本章中迄今为止学到的 Dockerfile 指令来将示例 PHP 应用程序 docker 化。我们使用了多个 Dockerfile 指令,包括 FROMLABELENVRUNCOPYWORKDIRHEALTHCHECKEXPOSEENTRYPOINT

3. 管理您的 Docker 镜像

活动 3.01:使用 Git 哈希版本控制构建脚本

解决方案

有多种方法可以完成这个活动。以下是一个例子:

  1. 创建一个新的构建脚本。第一行显示设置–ex命令,将每个步骤打印到屏幕上,并且如果任何步骤失败,脚本将失败。第 3 行第 4 行设置了你的注册表和服务名称的变量:
1 set -ex
2
3 REGISTRY=dev.docker.local:5000
4 SERVICENAME=postgresql
  1. 第 6 行,将GIT_VERSION变量设置为指向你的短 Git 提交哈希。构建脚本然后在第 7 行将这个值打印到屏幕上:
6 GIT_VERSION=`git log -1 --format=%h`
7 echo "version: $GIT_VERSION "
  1. 第 9 行使用docker build命令创建你的新镜像,并在第 11 行添加docker push命令将镜像推送到你的本地 Docker 注册表:
9 docker build -t $REGISTRY/$SERVICENAME:$GIT_VERSION .
10
11 docker push $REGISTRY/$SERVICENAME:$GIT_VERSION

脚本文件将如下所示:

1 set -ex
2
3 REGISTRY=dev.docker.local:5000
4 SERVICENAME= postgresql
5
6 GIT_VERSION=`git log -1 --format=%h`
7 echo "version: $GIT_VERSION "
8
9 docker build -t $REGISTRY/$SERVICENAME:$GIT_VERSION .
10
11 docker push $REGISTRY/$SERVICENAME:$GIT_VERSION
  1. 运行以下命令来确保脚本已构建并成功运行:
./build.sh

你应该会得到以下输出:

./BuildScript.sh 
++ REGISTRY=dev.docker.local:5000
++ SERVICENAME=basic-app
+++ git log -1 --format=%h
++ GIT_VERSION=49d3a10
++ echo 'version: 49d3a10 '
version: 49d3a10 
++ docker build -t dev.docker.local:5000/basic-app:49d3a10 .
Sending build context to Docker daemon  3.072kB
Step 1/1 : FROM postgres
 ---> 873ed24f782e
Successfully built 873ed24f782e
Successfully tagged dev.docker.local:5000/basic-app:49d3a10
++ docker push dev.docker.local:5000/basic-app:49d3a10
The push refers to repository [dev.docker.local:5000/basic-app]

活动 3.02:配置本地 Docker 注册表存储

解决方案

以下步骤描述了实现活动目标的一种方式:

  1. 在你的主目录中创建test_registry目录:
mkdir /home/vincesesto/test_registry/
  1. 运行本地注册表,但在这种情况下,包括-v选项,将你在前一步创建的目录连接到/var/lib/registry容器目录。还要使用:rw选项确保你可以读写该目录:
docker run -d -p 5000:5000 --restart=always --name registry -v /home/vincesesto/test_registry/registry:/var/lib/registry:rw registry
  1. 现在,像往常一样将镜像推送到新挂载的注册表中:
docker push dev.docker.local:5000/basic-app:ver1
  1. 为了验证镜像现在是否存储在你新挂载的目录中,列出registry/docker/registry/v2/repositories/目录中的文件。
ls  ~/test_registry/registry/docker/registry/v2/repositories/

你应该会看到你刚刚在上一步推送的新镜像:

basic-app

这个活动让我们开始使用一些更高级的 Docker 选项。别担心,将会有更多章节专门帮助你理解在运行容器时的卷挂载和存储。

4. 多阶段 Docker 文件

活动 4.01:使用多阶段 Docker 构建部署 Golang HTTP 服务器

解决方案:

  1. 为这个活动创建一个名为activity-04-01的新目录:
mkdir activity-04-01
  1. 导航到新创建的activity-04-01目录:
cd activity-04-01
  1. activity-04-01目录中,创建一个名为main.go的文件:
$ touch main.go
  1. 现在,使用你喜欢的文本编辑器打开main.go文件:
$ vim main.go
  1. 将以下内容添加到main.go文件中,然后保存并退出该文件:
package main
import (
    "net/http"
    "fmt"
    "log"
    "os"
)
func main() {
    http.HandleFunc("/", defaultHandler)
    http.HandleFunc("/contact", contactHandler)
    http.HandleFunc("/login", loginHandler)
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    log.Println("Service started on port " + port)
    err := http.ListenAndServe(":"+port, nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
        return
    }
}
func defaultHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "<h1>Home Page</h1>")
}
func contactHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "<h1>Contact Us</h1>")
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "<h1>Login Page</h1>")
}
  1. activity-04-01目录中,创建一个名为Dockerfile的文件。这个文件将是多阶段Dockerfile
touch Dockerfile
  1. 现在,使用你喜欢的文本编辑器打开Dockerfile
vim Dockerfile
  1. 将以下内容添加到Dockerfile并保存文件:
FROM golang:1.14.2-alpine AS builder
WORKDIR /myapp
COPY main.go .
RUN go build -o main .
FROM alpine:latest AS runtime
WORKDIR /myapp
COPY --from=builder /myapp/main .
ENTRYPOINT ["./main"]
EXPOSE 8080

这个Dockerfile有两个阶段,名为builderruntime。构建阶段使用 Golang Docker 镜像作为父镜像,负责从 Golang 源文件创建可执行文件。运行时阶段使用alpine Docker 镜像作为父镜像,并执行从builder阶段复制的可执行文件。

  1. 现在,使用docker build命令构建 Docker 镜像:
docker build -t activity-04-01:v1 .

您应该会得到以下输出:

图 4.14:构建 Docker 镜像

图 4.14:构建 Docker 镜像

  1. 使用docker image ls 命令列出计算机上所有可用的 Docker 镜像。验证镜像的大小:
docker images

该命令将返回所有可用的 Docker 镜像列表:

图 4.15:列出所有 Docker 镜像

图 4.15:列出所有 Docker 镜像

在前面的输出中,您可以看到名为activity-04-01的优化 Docker 镜像的大小为 13.1 MB,而在构建阶段使用的父镜像(Golang 镜像)的大小为 370 MB。

  1. 执行docker container run命令,从您在上一步中构建的 Docker 镜像启动一个新的容器:
$ docker container run -p 8080:8080 --name activity-04-01-container activity-04-01:v1

您应该会得到类似以下的输出:

2020/08/30 05:14:10 Service started on port 8080
  1. 在您喜欢的网络浏览器中查看以下 URL 的应用程序:
http://127.0.0.1:8080/

当我们导航到 URL http://127.0.0.1:8080/时,以下图片显示了主页:

图 4.16:Golang 应用程序-主页

图 4.16:Golang 应用程序-主页

  1. 现在,在您喜欢的网络浏览器中浏览以下 URL:
http://127.0.0.1:8080/contact

当我们导航到 URL http://127.0.0.1:8080/contact时,以下图片显示了联系页面:

图 4.17:Golang 应用程序-联系我们页面

图 4.17:Golang 应用程序-联系我们页面

  1. 现在,在您喜欢的网络浏览器中输入以下 URL:
http://127.0.0.1:8080/login 

当我们导航到 URL http://127.0.0.1:8080/login时,以下图片显示了登录页面:

图 4.18:Golang 应用程序-登录页面

图 4.18:Golang 应用程序-登录页面

在这个活动中,我们学习了如何部署一个 Golang HTTP 服务器,它可以根据调用 URL 返回不同的响应。在这个活动中,我们使用了多阶段的 Docker 构建来创建一个最小尺寸的 Docker 镜像。

5. 使用 Docker Compose 组合环境

活动 5.01:使用 Docker Compose 安装 WordPress

解决方案

可以通过以下步骤创建数据库并安装 WordPress:

  1. 创建所需的目录并使用cd命令进入其中:
mkdir wordpress
cd wordpress
  1. 创建一个名为docker-compose.yaml的文件,内容如下:
version: "3"
services:
  database:
    image: mysql:5.7
    volumes:
      - data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: db
      MYSQL_USER: user
      MYSQL_PASSWORD: password
  wordpress:
    depends_on:
      - database
    image: wordpress:latest
    ports:
      - "8080:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: database:3306
      WORDPRESS_DB_USER: user
      WORDPRESS_DB_PASSWORD: password
      WORDPRESS_DB_NAME: db
volumes:
     data: {} 
  1. 使用docker-compose up --detach命令启动应用程序:图 5.22:应用程序的启动

图 5.22:应用程序的启动

  1. 使用docker-compose ps命令检查正在运行的容器。您应该会得到以下输出:图 5.23:WordPress 和数据库容器

图 5.23:WordPress 和数据库容器

  1. 在浏览器中打开http://localhost:8080以检查 WordPress 设置屏幕:图 5.24:WordPress 设置屏幕

图 5.24:WordPress 设置屏幕

在这个活动中,您使用 Docker Compose 创建了一个真实应用程序的部署。该应用程序包括一个数据库容器和一个 WordPress 容器。这两个容器服务都使用环境变量进行配置,通过 Docker Compose 网络和卷进行连接。

活动 5.02:使用 Docker Compose 安装全景徒步应用程序

解决方案

可以通过以下步骤创建数据库和全景徒步应用程序:

  1. 创建所需的目录并切换到其中:
mkdir pta-compose
cd pta-compose
  1. 创建一个名为docker-compose.yaml的文件,内容如下:
version: "3"
services:
  db:
    image: postgres
    volumes:
      - db_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_PASSWORD=docker
  web:
    image: packtworkshops/the-docker-workshop:chapter5-pta-web
    volumes:
      - static_data:/service/static
    depends_on:
      - db
  nginx:
    image: packtworkshops/the-docker-workshop:chapter5-pta-nginx
    volumes:
      - static_data:/service/static
    ports:
      - 8000:80
    depends_on:
      - web
volumes:
  db_data:
  static_data:
  1. 使用docker-compose up --detach命令启动应用程序。您应该会得到类似以下的输出:图 5.25:应用程序的启动

图 5.25:应用程序的启动

注意

您也可以使用docker-compose up -d命令来启动应用程序。

  1. 使用docker-compose ps命令检查正在运行的容器。您应该会得到类似以下的输出:图 5.26 应用程序、数据库和 nginx 容器

图 5.26 应用程序、数据库和 nginx 容器

  1. 使用地址http://0.0.0.0:8000/admin在浏览器中打开全景徒步应用程序的管理部分:图 5.27:管理员设置登录

图 5.27:管理员设置登录

注意

您也可以运行firefox http://0.0.0.0:8000/admin命令来打开全景徒步应用程序的管理部分。

使用用户名admin和密码changeme登录,并添加新的照片和国家。将出现以下屏幕:

图 5.28:管理员设置视图

图 5.28:管理员设置视图

  1. 在浏览器中打开全景徒步应用程序,地址为http://0.0.0.0:8000/photo_viewer图 5.29:应用程序视图

图 5.29:应用程序视图

在此活动中,您使用 Docker Compose 创建了一个三层应用程序,其中包括用于 PostgreSQL 数据库、后端和代理服务的层。所有服务都使用 Docker Compose 进行配置和连接,具有其网络和存储功能。

6. Docker 网络简介

活动 6.01:利用 Docker 网络驱动程序

解决方案

以下是根据最佳实践完成此活动的最常见方法:

  1. 使用docker network create命令为 NGINX Web 服务器创建一个网络。将其命名为webservernet,并为其分配子网192.168.1.0/24和网关192.168.1.1
$ docker network create webservernet --subnet=192.168.1.0/24 --gateway=192.168.1.1

这应该创建bridge网络webservernet

  1. 使用docker run命令创建一个 NGINX Web 服务器。使用-p标志将主机上的端口8080转发到容器实例上的端口80
$ docker run -itd -p 8080:80 --name webserver1 --network webservernet nginx:latest

这将在webservernet网络中启动webserver1容器。

  1. 使用docker run命令以host网络模式启动名为monitor的 Alpine Linux 容器。这样,您将知道容器可以访问主系统的主机端口以及bridge网络的 IP 地址:
$ docker run -itd --name monitor --network host alpine:latest

这将在host网络模式下启动一个 Alpine Linux 容器实例。

  1. 使用docker inspect查找webserver1容器的 IP 地址:
$ docker inspect webserver1

容器的详细信息将以 JSON 格式显示;从IPAddress参数中获取 IP 地址:

图 6.27:检查 webserver1 容器实例

图 6.27:检查 webserver1 容器实例

  1. 使用docker exec命令在监控容器内部启动sh shell:
$ docker exec -it monitor /bin/sh

这应该将您放入根 shell。

  1. 使用apk install命令在此容器内安装curl命令:
/ # apk add curl

这应该安装curl实用程序:

fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main
/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community
/x86_64/APKINDEX.tar.gz
(1/4) Installing ca-certificates (20191127-r1)
(2/4) Installing nghttp2-libs (1.40.0-r0)
(3/4) Installing libcurl (7.67.0-r0)
(4/4) Installing curl (7.67.0-r0)
Executing busybox-1.31.1-r9.trigger
Executing ca-certificates-20191127-r1.trigger
OK: 7 MiB in 18 packages
  1. 使用curl命令验证主机级别的连接是否正常工作,调用主机机器上的端口8080
/ # curl -v http://localhost:8080

您应该收到来自 NGINX 的200 OK响应,表示在主机级别成功连接:

图 6.28:从主机上的暴露端口访问 webserver1 容器

图 6.28:从主机上的暴露端口访问 webserver1 容器

  1. 同样,使用curl命令直接通过端口80访问 Dockerbridge网络中容器的 IP 地址:
/ # curl -v 192.168.1.2:80

您应该同样收到另一个200 OK响应,表明连接成功:

图 6.29:从 IP 访问 NGINX Web 服务器容器实例的地址

图 6.29:从容器实例的 IP 地址访问 NGINX Web 服务器

在这个活动中,我们能够说明使用不同的 Docker 网络驱动程序在容器之间建立连接。这种情况适用于真实的生产基础设施,因为在部署容器化解决方案时,工程师们将努力部署尽可能不可变的基础设施。通过在 Docker 中部署容器,确切地模拟主机级别的网络,可以设计出需要在主机操作系统上进行非常少量配置的基础设施。这使得在部署和扩展 Docker 部署的主机时非常容易。诸如curl和其他监控工具的软件包可以部署到在 Docker 主机上运行的容器中,而不是安装在主机上。这保证了部署和维护的便利性,同时提高了满足不断增长的需求所需的主机部署速度。

活动 6.02:叠加网络实践

解决方案

  1. 在 Docker swarm 集群中的Machine1上使用docker network create命令创建一个名为panoramic-net的 Dockeroverlay网络,通过传递自定义的subnetgatewayoverlay网络驱动程序:
$ docker network create panoramic-net --subnet=10.2.0.0/16 --gateway=10.2.0.1 --driver overlay
  1. Machine1上使用docker service create命令创建一个名为trekking-app的服务,加入panoramic-net网络:
$ docker service create -t --name trekking-app --replicas=1 --network panoramic-net alpine:latest

这将在panoramic-net overlay网络中启动一个名为trekking-app的服务。

  1. Machine1上使用docker service create命令创建一个名为database-app的服务,加入panoramic-net网络。设置默认凭据并指定postgres:12版本的 Docker 镜像:
$ docker service create -t --name database-app --replicas=1 --network panoramic-net -e "POSTGRES_USER=panoramic" -e "POSTGRES_PASSWORD=trekking" postgres:12
  1. 使用docker exec访问trekking-app服务容器内的sh shell:
$ docker exec -it trekking-app.1.qhpwxol00geedkfa9p6qswmyv /bin/sh

这应该将您放入trekking-app容器实例内的根 shell 中。

  1. 使用ping命令验证对database-app服务的网络连接:
/ # ping database-app

ICMP 回复应指示连接成功:

PING database-app (10.2.0.5): 56 data bytes
64 bytes from 10.2.0.5: seq=0 ttl=64 time=0.261 ms
64 bytes from 10.2.0.5: seq=1 ttl=64 time=0.352 ms
64 bytes from 10.2.0.5: seq=2 ttl=64 time=0.198 ms

在这个活动中,我们利用了 Docker 集群中的自定义 Dockeroverlay网络,以说明两个 Docker 集群服务之间的连接性,使用 Docker DNS。在真实的多层应用程序中,许多微服务可以部署在使用overlay网络网格直接相互通信的大型 Docker 集群中。了解overlay网络如何与 Docker DNS 协同工作对于实现容器化基础设施的高效可扩展性至关重要。

7. Docker 存储

活动 7.01:将容器事件(状态)数据存储在 PostgreSQL 数据库中

解决方案

  1. 运行以下命令以删除主机中的所有对象:
$ docker container rm -fv $(docker container ls -aq)
$docker image rm $(docker image ls -q)
  1. 获取卷名称,然后使用以下命令删除所有卷:
$docker volume ls
$docker volume rm <volume names separated by spaces>
  1. 获取网络名称,然后使用以下命令删除所有网络:
$docker network ls
$docker network rm <network names separated by spaces>
  1. 打开两个终端,一个专门用于查看docker events --format '{{json .}}'的效果。另一个应该打开以执行先前提到的高级步骤。

  2. 在第一个终端中,运行以下命令:

docker events --format '{{json .}}'.

您应该得到以下输出:

图 7.11:docker events 命令的输出

图 7.11:docker events 命令的输出

  1. 运行以下命令在第二个终端中启动ubuntu容器:
$docker run -d ubuntu:14.04

您应该得到以下输出:

图 7.12:docker run 命令的输出

图 7.12:docker run 命令的输出

  1. 使用第二个终端中的以下命令创建名为vol1的卷:
$docker volume create vol1
  1. 使用第二个终端中的以下命令创建名为net1的网络:
$docker network create net1
  1. 使用以下命令删除容器:
$docker container rm -fv <container ID>
  1. 使用以下命令删除卷和网络:
$docker volume rm vol1
$docker network rm net1
  1. docker events终端中单击Ctrl + C以终止它。

  2. 检查以下两个示例以了解 JSON 输出:

示例 1

{"status":"create","id":"43903b966123a7c491b50116b40827daa03
da5d350f8fef2a690fc4024547ce2","from":"ubuntu:14.04","Type":
"container","Action":"create","Actor":{"ID":"43903b966123a7c
491b50116b40827daa03da5d350f8fef2a690fc4024547ce2","Attributes":
{"image":"ubuntu:14.04","name":"upbeat_johnson"}},"scope":"local",
"time":1592516703,"timeNano":1592516703507582404}

示例 2

{"Type":"network","Action":"connect","Actor":{"ID":"52855e1561
8e37b7ecc0bb26bc42847af07cae65ddd3b68a029e40006364a9bd",
"Attributes":{"container":"43903b966123a7c491b50116b40827daa03d
a5d350f8fef2a690fc4024547ce2","name":"bridge","type":"bridge"}},
"scope":"local","time":1592516703,"timeNano":1592516703911851347}

您会发现根据对象的不同属性和结构而有所不同。

  1. 运行带有卷的 PostgreSQL 容器。将容器命名为db1
$docker container run --name db1 -v db:/var/lib/postgresql/data -e POSTGRES_PASSWORD=password -d postgres
  1. 运行exec命令,以便 bash 被要执行的命令替换。shell 将更改为posgres=#,表示您在容器内部:
$ docker container exec -it db1 psql -U postgres
  1. 创建一个具有两列的表:IDserial类型,infojson类型:
CREATE TABLE events (ID serial NOT NULL PRIMARY KEY, info json NOT NULL);
  1. 将第一个示例的JSON输出的第一行插入表中:
INSERT INTO events (info) VALUES ('{"status":"create","id":"43903b966123a7c491b50116b40827daa03da 5d350f8fef2a690fc4024547ce2","from":"ubuntu:14.04","Type":"container","Action":"create","Actor":{"ID":"43903b966123a7c49 1b50116b40827daa03da5d350f8fef2a690fc4024547ce2","Attributes":{"image":"ubuntu:14.04","name":"upbeat_johnson"}},"scope":"local","time":1592516703,"timeNano":1592516703507582404}');
  1. 通过输入以下 SQL 语句验证数据库中是否保存了行:
select * from events;

您应该得到以下输出:

图 7.13:验证数据库中是否保存了行

图 7.13:验证数据库中是否保存了行

  1. 使用 SQL 的insert命令将 Docker 事件插入events表中。

注意

请参考packt.live/2ZKfGgB上的events.txt文件,使用insert命令插入 Docker 事件。

您应该得到以下输出:

图 7.14:在数据库中插入多行

图 7.14:在数据库中插入多行

从这个输出中,可以清楚地看到已成功将 11 个事件插入到 PostgreSQL 数据库中。

  1. 依次运行以下三个查询。

查询 1

SELECT * FROM events WHERE info ->> 'status' = 'pull';

输出将如下所示:

图 7.15:查询 1 的输出

图 7.15:查询 1 的输出

查询 2

SELECT * FROM events WHERE info ->> 'status' = 'destroy';

输出将如下所示:

图 7.16:查询 2 的输出

图 7.16:查询 2 的输出

查询 3

SELECT info ->> 'id' as id FROM events WHERE info ->> 'status'=     'destroy';

输出将如下所示:

图 7.17:查询 3 的输出

图 7.17:查询 3 的输出

在这个活动中,您学习了如何记录和监视容器,并使用 SQL 语句查询容器的事件,以及如何获得事件的 JSON 输出并保存在 PostgreSQL 数据库中。您还学习了 JSON 输出结构以及如何查询它。

活动 7.02:与主机共享 NGINX 日志文件

解决方案

  1. 通过运行以下命令验证您的主机上是否没有/var/mylogs文件夹:
$cd /var/mylogs

您应该得到以下输出:

Bash: cd: /var/mylogs: No such file or directory
  1. 基于 NGINX 镜像运行一个容器。在run命令中指定主机和容器内共享卷的路径。在容器内,NGINX 使用/var/log/nginx路径存储日志文件。在主机上指定路径为/var/mylogs
$docker container run -d -v /var/mylogs:/var/log/nginx nginx

如果您本地没有该镜像,Docker 引擎将自动拉取该镜像:

图 7.18:运行 docker run 命令的输出

图 7.18:运行 docker run 命令的输出

  1. 进入/var/mylogs路径。列出该目录中的所有文件:
$cd /var/mylogs
$ls

您应该在那里找到两个文件:

access.log         error.log
  1. (可选)如果没有生成错误,这两个文件将是空的。你可以使用cat Linux 命令或者使用tail Linux 命令来检查内容。因为我们之前使用了cat命令,所以让我们在这个例子中使用tail命令:
$tail -f *.log

你应该得到以下的输出:

==>  access.log  <==
==>  error.log   <==

由于这个 NGINX 服务器没有生成任何错误或者没有被访问,这些文件目前是空的。然而,如果 NGINX 在任何时刻崩溃,生成的错误将会保存在error.log中。

在这个活动中,你学会了如何将容器的日志文件共享到主机上。你使用了 NGINX 服务器,所以如果它崩溃了,你可以从它的日志文件中追溯发生了什么。

8. 服务发现

活动 8.01:利用 Jenkins 和 SonarQube

解决方案

  1. 安装 SonarQube 并使用以下命令作为容器运行:
docker run -d --name sonarqube -p 9000:9000 -p 9092:9092 sonarqube

你应该得到容器 ID 作为输出:

4346a99b506b1bec8000e429471dabac57e3f565b154ee921284ec685497bfae
  1. 使用admin/admin凭据登录到 SonarQube:图 8.38:登录到 SonarQube

图 8.38:登录到 SonarQube

成功登录后,应该出现类似以下的屏幕:

图 8.39:SonarQube 仪表板

图 8.39:SonarQube 仪表板

  1. 在右上角,点击用户。会出现一个下拉菜单。点击我的账户

  2. 向下滚动并点击安全下的生成来生成一个令牌。你现在必须复制它,因为以后将无法访问它:图 8.40:生成令牌

图 8.40:生成令牌

  1. 在 Jenkins 中,点击管理 Jenkins > 插件管理器。在可用列表中搜索Sonar。安装SonarQube Scanner插件。图 8.41:安装 SonarQube Scanner 插件

图 8.41:安装 SonarQube Scanner 插件

  1. 通过点击hit_count项目,然后点击配置选项来验证安装是否正确。在构建选项卡上点击添加构建步骤,然后点击执行 SonarQube Scanner,就像图 8.43中那样:图 8.42:选择执行 SonarQube Scanner

图 8.42:选择执行 SonarQube Scanner

  1. 然而,新的框将会生成错误,就像下面截图中显示的那样。为了纠正这个问题,通过系统配置全局工具配置选项将 SonarQube 和 Jenkins 集成起来:图 8.43:由于 SonarQube 尚未配置而生成的错误

图 8.43:由于 SonarQube 尚未配置而生成的错误

  1. 在 Jenkins 中,点击“管理 Jenkins”。点击“全局工具配置”选项,然后点击“添加 SonarQube 扫描仪”:图 8.44:在全局工具配置页面上添加 SonarQube 扫描仪

图 8.44:在全局工具配置页面上添加 SonarQube 扫描仪

  1. 输入名称“SonarQube 扫描仪”。勾选“自动安装”。在“从 Maven 中央安装”下,在“版本”中选择SonarQube Scanner 3.2.0.1227。点击“添加安装程序”。在“标签”字段中,输入SonarQube。在“二进制存档的下载 URL”字段中,输入链接https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-3.2.0.1227-linux.zip

点击“保存”。

图 8.45:为 SonarQube 扫描仪添加详细信息

图 8.45:为 SonarQube 扫描仪添加详细信息

您现在已完成“全局工具配置”选项,现在是时候转到“配置系统”选项了。

  1. 在“管理 Jenkins”中,点击“配置系统”:图 8.46:在管理 Jenkins 页面上点击配置系统

图 8.46:在管理 Jenkins 页面上点击配置系统

  1. 您现在无法进入系统配置,因为它要求“服务器身份验证令牌”。当您点击“添加”按钮时,它将不起作用。在以下步骤中将令牌输入为秘密文本,然后返回到“管理 Jenkins”:图 8.47:在 Jenkins 配置中插入 SonarQube 令牌

图 8.47:在 Jenkins 配置中插入 SonarQube 令牌

  1. 点击“管理凭据”:图 8.48:管理 Jenkins 页面

图 8.48:管理 Jenkins 页面

  1. 点击Jenkins图 8.49:Jenkins 凭据页面

图 8.49:Jenkins 凭据页面

  1. 点击“全局凭据(不受限制)”:图 8.50:全局凭据(不受限制)域

图 8.50:全局凭据(不受限制)域

  1. 点击“添加一些凭据”:图 8.51:添加一些凭据

图 8.51:添加一些凭据

  1. 在“类型”下拉菜单中,点击“秘密文本”:图 8.52:选择类型为秘密文本

图 8.52:选择类型为秘密文本

  1. 在“秘密”文本框中,粘贴您在本活动的步骤 5中复制的令牌。在ID字段中,输入SonarQubeToken。点击“确定”:图 8.53:将令牌添加到秘密文本框

图 8.53:将令牌添加到秘密文本框

SonarQubeToken将保存在“全局凭据”选项中。您将看到类似以下内容的屏幕:

图 8.54:SonarQubeToken 保存在全局凭据中

图 8.54:SonarQubeToken 保存在全局凭据中

  1. 返回到“管理 Jenkins”。点击“配置系统”,然后点击“刷新”。现在,在“服务器身份验证令牌”下拉菜单中,您将找到SonarQubeToken。勾选“启用将 SonarQube 服务器配置注入构建环境变量”。在“名称”字段中输入SonarQube。在“服务器 URL”字段中输入“http://<您的 IP>:9000”。然后点击“保存”:

您可以运行ifconfig命令来获取您的 IP。您将在输出的en0部分找到 IP:

$ ifconfig

这是将 Jenkins 与 SonarQube 集成的最后一步。让我们返回到项目中。

  1. 在“构建环境”中,勾选“准备 SonarQube 扫描器环境”。将“服务器身份验证令牌”设置为SonarQubeToken

  2. 现在,点击项目名称,然后点击“配置”。在“构建”步骤中,在“分析属性”字段中输入以下代码:

sonar.projectKey=hit_count
sonar.projectName=hit_count
sonar.projectVersion=1.0
sonar.sources=.
sonar.language=py
sonar.sourceEncoding=UTF-8
# Test Results
sonar.python.xunit.reportPath=nosetests.xml
# Coverage
sonar.python.coverage.reportPath=coverage.xml
# Linter (https://docs.sonarqube.org/display/PLUG/Pylint+Report)
#sonar.python.pylint=/usr/local/bin/pylint
#sonar.python.pylint_config=.pylintrc
#sonar.python.pylint.reportPath=pylint-report.txt

点击“保存”。

  1. 保存后,您将在项目页面上看到 SonarQube 标志,如图 8.55所示。点击“立即构建”:图 8.55:我们项目仪表板上显示 SonarQube 选项

图 8.55:我们项目仪表板上显示 SonarQube 选项

  1. 在“构建历史”中,点击“控制台输出”。您应该会看到类似以下内容的屏幕:图 8.56:控制台输出

图 8.56:控制台输出

  1. 在浏览器中检查SonarQube的报告。输入http://<ip>:9000http://localhost:9000。您将发现 Jenkins 自动将您的hit_count项目添加到 SonarQube 中:

  2. 点击hit_count。您将找到一个详细的报告。每当 Jenkins 构建项目时,SonarQube 将自动分析代码。

在本活动中,您学习了如何将 Jenkins 与 SonarQube 集成并安装所需的插件,通过在浏览器中检查 SonarQube 进行验证。您还将 SonarQube 应用于您的简单 Web 应用程序hit_counter

活动 8.02:在全景徒步应用程序中利用 Jenkins 和 SonarQube

解决方案

  1. 在 Jenkins 中创建一个名为trekking的新项目。将其选择为FREESTYLE项目。点击“确定”。

  2. 在“常规”选项卡中,选择“丢弃旧构建”。

  3. 在“源代码管理”选项卡中,选择GIT。然后输入 URLhttp://github.com/efoda/trekking_app图 8.57:插入 GitHub URL

图 8.57:插入 GitHub URL

  1. 在“构建触发器”中,选择“轮询 SCM”,并输入H/15 * * * *图 8.58:插入调度代码

图 8.58:插入调度代码

  1. 在“构建环境”选项卡中,选择“准备 SonarQube 扫描器环境”。从下拉菜单中选择“服务器身份验证令牌”:图 8.59:选择 SonarQubeToken 作为服务器身份验证令牌

图 8.59:选择 SonarQubeToken 作为服务器身份验证令牌

  1. 在“构建”选项卡中,在“分析属性”中输入以下代码:
sonar.projectKey=trekking
sonar.projectName=trekking
sonar.projectVersion=1.0
sonar.sources=.
sonar.language=py
sonar.sourceEncoding=UTF-8
# Test Results
sonar.python.xunit.reportPath=nosetests.xml
# Coverage
sonar.python.coverage.reportPath=coverage.xml
# Linter (https://docs.sonarqube.org/display/PLUG/Pylint+Report)
#sonar.python.pylint=/usr/local/bin/pylint
#sonar.python.pylint_config=.pylintrc
#sonar.python.pylint.reportPath=pylint-report.txt

点击“保存”。

  1. 选择“立即构建”。当构建成功完成时,选择“控制台输出”。以下输出将指示它已成功完成:图 8.60:验证 Jenkins 已成功构建镜像

图 8.60:验证 Jenkins 已成功构建镜像

  1. 切换到浏览器中的SonarQube选项卡,并检查输出。以下报告表明徒步应用程序有两个错误和零个安全漏洞:图 8.61:在 SonarQube 浏览器选项卡中显示的报告

图 8.61:在 SonarQube 浏览器选项卡中显示的报告

如果单击“新代码”,它将为空,因为您只构建了项目一次。当 Jenkins 再次构建它时,您将找到两次构建之间的比较。

  1. 如果您想编辑项目的代码,请将 GitHub 代码 fork 到您的帐户,并编辑代码以修复错误和漏洞。编辑项目的配置,使其使用您的 GitHub 代码,而不是“源代码”选项卡中提供的代码。

在这个活动中,您将 Jenkins 与 SonarQube 集成,并将其应用于全景徒步应用程序。在活动结束时,您将检查 SonarQube 生成的报告,显示代码中的错误和漏洞。

9. Docker Swarm

活动 9.01:将全景徒步应用部署到单节点 Docker Swarm

解决方案

有许多方法可以执行此活动。以下步骤是其中一种方法:

  1. 为应用程序创建一个目录。在这种情况下,您将创建一个名为Activity1的目录,并使用cd命令进入新目录:
mkdir Activity1; cd Activity1
  1. 从其 GitHub 存储库克隆应用程序,以确保您拥有部署到 Swarm 的 Panoramic Trekking App 服务所需的所有相关信息和应用程序:
git clone https://github.com/vincesesto/trekking_app.git
  1. 您不需要 NGINX 的任何支持目录,但请确保您的 Web 服务和运行的数据库在此处列出,包括panoramic_trekking_appphoto_viewer目录以及Dockerfileentrypoint.shmanage.pyrequirements.txt脚本和文件:
ls -l

该命令应返回类似以下的输出:

-rw-r--r--   1 vinces  staff   533 12 Mar 15:02 Dockerfile
-rwxr-xr-x   1 vinces  staff  1077 12 Mar 15:02 entrypoint.sh
-rwxr-xr-x   1 vinces  staff   642 12 Mar 15:02 manage.py
drwxr-xr-x   9 vinces  staff   288 12 Mar 15:02 
panoramic_trekking_app
drwxr-xr-x  12 vinces  staff   384 12 Mar 15:02 photo_viewer
-rw-r--r--   1 vinces  staff   105 12 Mar 15:02 requirements.txt
  1. 在目录中创建.env.dev文件,并添加以下详细信息,供panoramic_trekking_app在其settings.py文件中使用。这些环境变量将设置数据库名称、用户、密码和其他数据库设置:
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=pta_database
SQL_USER=pta_user
SQL_PASSWORD=pta_password
SQL_HOST=db
SQL_PORT=5432
PGPASSWORD=docker
  1. 创建一个新的docker-compose.yml文件,并用文本编辑器打开它,并添加以下详细信息:
version: '3.3'
services:
  web:
    build: .
    image: activity_web:latest
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - static_volume:/service/static
    ports:
      - 8000:8000
    environment:
      - PGPASSWORD=docker
    env_file:
      - ./.env.dev
    depends_on:
      - db
  db:
    image: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_PASSWORD=docker
    ports:
      - 5432:5432
volumes:
  postgres_data:
  static_volume:

如您从docker-compose.yml文件中的突出显示的行中所见,web服务依赖于activity_web:latest Docker 镜像。

  1. 运行以下docker build命令来构建镜像并适当地标记它:
docker build -t activity_web:latest .
  1. 现在是时候将堆栈部署到 Swarm 了。使用您创建的docker-compose.yml文件运行以下stack deploy命令:
docker stack deploy --compose-file docker-compose.yml activity_swarm

创建网络后,您应该看到activity_swarm_webactivity_swarm_db服务可用:

Creating network activity_swarm_default
Creating service activity_swarm_web
Creating service activity_swarm_db
  1. 运行service ls命令:
docker service ls

验证所有服务是否已成功启动,并显示1/1副本,就像我们这里一样:

ID       NAME                MODE         REPLICAS
  IMAGE
k6kh…    activity_swarm_db   replicated   1/1
  postgres:latest
copa…    activity_swarm_web  replicated   1/1
  activity_web:latest  
  1. 最后,打开您的网络浏览器,并验证您能够从http://localhost:8000/admin/http://localhost:8000/photo_viewer/访问该网站。

Panoramic Trekking App 的创建和设置方式与本章中已经完成的一些其他服务类似。

活动 9.02:在 Swarm 运行时执行应用程序更新

解决方案:

有许多种方法可以执行此活动。以下步骤详细说明了一种方法:

  1. 如果您没有运行 Swarm,请部署您在活动 9.01中创建的docker-compose.yml文件,将 Panoramic Trekking App 部署到单节点 Docker Swarm
docker stack deploy --compose-file docker-compose.yml activity_swarm

如您所见,现在所有三个服务都在运行:

Creating network activity_swarm_default
Creating service activity_swarm_web
Creating service activity_swarm_db
  1. 在执行stack deploy命令的同一目录中,使用文本编辑器打开photo_viewer/templates/photo_index.html文件,并将第四行更改为与以下详细信息匹配,基本上是在主标题中添加单词Patch

photo_index.html

1 {% extends "base.html" %}
2 {% load static %}
3 {% block page_content %}
4 <h1>Patch Panoramic Trekking App - Photo Viewer</h1>

您可以在此处找到完整的代码packt.live/3ceYnta

  1. 构建一个新图像,这次使用以下命令将图像标记为patch_1
docker build -t activity_web:patch_1 .
  1. 使用service update命令将补丁部署到您的 Swarm Web 服务。还提供要应用更新的图像名称和服务:
docker service update --image activity_web:patch_1 activity_swarm_web

输出应如下所示:

…
activity_swarm_web
overall progress: 1 out of 1 tasks 
1/1: running   [=======================================>] 
verify: Service converged
  1. 列出正在运行的服务,并验证新图像是否作为activity_swarm_web服务的一部分正在运行:
docker service ls

从输出中可以看出,Web 服务不再使用latest标记。它现在显示patch_1图像标记:

ID         NAME                  MODE          REPLICAS
  IMAGE
k6kh…      activity_swarm_db     replicated    1/1
  postgres:latest
cu5p…      activity_swarm_web    replicated    1/1
  activity_web:patch_1
  1. 通过访问http://localhost:8000/photo_viewer/并查看标题现在显示为Patch Panoramic Trekking App来验证更改是否已应用于图像:图 9.10:全景徒步应用程序的 Patch 版本

图 9.10:全景徒步应用程序的 Patch 版本

在此活动中,您对全景徒步应用程序进行了微小更改,以便可以对服务进行滚动更新。然后,您将图像部署到运行环境中,并执行滚动更新以验证更改是否成功。标题中的更改表明滚动更新已成功执行。

10. Kubernetes

活动 10.01:在 Kubernetes 上安装全景徒步应用程序

解决方案

可以通过以下步骤创建数据库和全景徒步应用程序:

  1. 使用以下helm命令安装数据库:
helm install database stable/postgresql --set postgresqlPassword=kubernetes

这将为 PostgreSQL 安装多个 Kubernetes 资源,并显示摘要如下:

图 10.23:数据库安装

图 10.23:数据库安装

此输出首先列出与 Helm 图表相关的信息,例如名称、部署时间、状态和修订版本,然后是与 PostgreSQL 实例相关的信息以及如何访问它。这是 Helm 图表中广泛接受的方法,在安装图表后提供此类信息。否则,将很难学习如何连接到 Helm 安装的应用程序。

  1. 创建一个statefulset.yaml文件,其中包含以下内容:
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: panoramic-trekking-app
spec:
  serviceName: panoramic-trekking-app
  replicas: 1
  selector:
    matchLabels:
      app: panoramic-trekking-app
  template:
    metadata:
      labels:
        app: panoramic-trekking-app
    spec:
      containers:
      - name: nginx
        image: packtworkshops/the-docker-workshop:          chapter10-pta-nginx
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: static
          mountPath: /service/static
      - name: pta
        image: packtworkshops/the-docker-workshop:          chapter10-pta-web
        volumeMounts:
        - name: static
          mountPath: /service/static
  volumeClaimTemplates:
  - metadata:
      name: static
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

此文件创建了一个名为panoramic-trekking-app的 Statefulset。在spec部分定义了两个名为nginxpta的容器。此外,还定义了一个名为static的卷索赔,并将其挂载到两个容器上。

  1. 使用以下命令部署panoramic-trekking-app StatefulSet:
kubectl apply -f statefulset.yaml

这将为我们的应用程序创建一个 StatefulSet:

StatefulSet.apps/panoramic-trekking-app created
  1. 创建一个service.yaml文件,内容如下:
apiVersion: v1
kind: Service
metadata:
  name: panoramic-trekking-app
  labels:
    app: panoramic-trekking-app
spec:
  ports:
  - port: 80
    name: web
  type: LoadBalancer
  selector:
    app: panoramic-trekking-app

此服务定义具有LoadBalancer类型,以访问具有标签app: panoramic-trekking-app的 Pod。端口80将可用于访问 Pod 的web端口。

  1. 使用以下命令部署panoramic-trekking-app服务:
kubectl apply -f service.yaml

这将创建以下 Service 资源:

Service/panoramic-trekking-app created
  1. 使用以下命令获取 Service 的 IP:
minikube service panoramic-trekking-app --url
http://192.168.64.14:32009

在以下步骤中存储 IP 以访问 Panoramic Trekking App。

  1. 在浏览器中打开 Panoramic Trekking App 的管理部分,网址为http://$SERVICE_IP/admin图 10.24:管理员登录视图

图 10.24:管理员登录视图

  1. 使用用户名admin和密码changeme登录,并添加新的照片和国家:图 10.25:管理员设置视图

图 10.25:管理员设置视图

  1. 在浏览器中打开 Panoramic Trekking App,网址为http://$SERVICE_IP/photo_viewer图 10.26:应用程序视图

图 10.26:应用程序视图

照片查看器应用程序显示已从数据库中检索到照片和国家。它还表明应用程序已正确设置并且正常运行。

在这个活动中,您已经将 Panoramic Trekking App 部署到了 Kubernetes 集群。您首先使用其 Helm 图表创建了一个数据库,然后为应用程序创建了 Kubernetes 资源。最后,您从浏览器访问了应用程序,并通过添加新的照片进行了测试。在这个活动结束时,您已经了解了如何使用官方 Helm 图表部署数据库,创建一系列 Kubernetes 资源来连接数据库并部署应用程序,并从集群中收集信息以访问应用程序。该活动中的步骤涵盖了在 Kubernetes 集群中部署容器化应用程序的生命周期。

11. Docker 安全

活动 11.01:为 Panoramic Trekking App 设置 seccomp 配置文件

解决方案

有多种方法可以创建一个seccomp配置文件,阻止用户执行mkdirkilluname命令。以下步骤展示了如何完成这一操作:

  1. 如果你本地没有postgres镜像,请执行以下命令:
docker pull postgres
  1. 在你的系统上使用wget命令获取默认的seccomp配置文件的副本。将你下载的文件命名为activity1.json
wget https://raw.githubusercontent.com/docker/docker/v1.12.3/profiles/seccomp/default.json - O activity1.json
  1. 从配置文件中删除以下三个命令,以允许我们进一步锁定我们的镜像。用你喜欢的文本编辑器打开activity1.json文件,并从文件中删除以下行。你应该删除行 15001504以删除uname命令,669673以删除mkdir命令,以及行 579583以删除kill命令的可用性:
1500                 {
1501                         "name": "uname",
1502                         "action": "SCMP_ACT_ALLOW",
1503                         "args": []
1504                 },

669                 {
670                         "name": "mkdir",
671                         "action": "SCMP_ACT_ALLOW",
672                         "args": []
673                 },

579                 {
580                         "name": "kill",
581                         "action": "SCMP_ACT_ALLOW",
582                         "args": []
583                 },

你可以在以下链接找到修改后的activity1.json文件:packt.live/32BI3PK

  1. 使用–-security-opt seccomp=activity1.json选项在运行镜像时为postgres镜像分配一个新配置文件:
docker run --rm -it --security-opt seccomp=activity1.json postgres sh
  1. 现在你已经登录到正在运行的容器中,测试你已经分配给容器的新配置文件的权限。执行mkdir命令在系统上创建一个新目录:
~ $ mkdir test

该命令应该显示一个Operation not permitted的输出:

mkdir: can't create directory 'test': Operation not permitted
  1. 为了测试你不再能够杀死正在运行的进程,你需要启动一些东西。启动top进程并在后台运行。在命令行中输入top,然后添加&,然后按Enter在后台运行该进程。接下来的命令提供了进程命令(ps),以查看容器上正在运行的进程:
~ $ top & ps

如下输出所示,top进程正在以PID 8运行:

PID   USER         TIME    COMMAND
 1    20002        0:00    sh
 8    20002        0:00    top
10    20002        0:00    ps
[1]+  Stopped  (tty output)       top

注意

基于postgres镜像的容器中不可用pstop命令。然而,这不会造成任何问题,因为用任意随机的 pid 号运行kill命令足以证明该命令不被允许运行。

  1. 使用kill -9命令杀死top进程,后面跟着你想要杀死的进程的 PID 号。kill -9命令将尝试强制停止命令:
~ $ kill -9 8

你应该看到Operation not permitted

sh: can't kill pid 8: Operation not permitted
  1. 测试uname命令。这与其他命令有些不同:
~ $ uname

你将得到一个Operation not permitted的输出:

Operation not permitted

这是一个很好的活动,表明如果我们的镜像被攻击者访问,我们仍然可以采取很多措施来限制对它们的操作。

活动 11.02:扫描您的全景徒步应用镜像的漏洞

解决方案:

有许多方法可以扫描我们的镜像以查找漏洞。以下步骤是使用 Anchore 来验证postgres-app镜像是否对我们的应用程序安全的一种方法:

  1. 给镜像打标签并将其推送到您的 Docker Hub 仓库。在这种情况下,使用我们的仓库名称给postgres-app镜像打标签,并将其标记为activity2。我们还将其推送到我们的 Docker Hub 仓库:
docker tag postgres <your repository namespace>/postgres-app:activity2 ; docker push <your repository name>/postgres-app:activity2
  1. 您应该仍然拥有您在本章最初使用的docker-compose.yaml文件。如果您还没有运行 Anchore,请运行docker-compose命令并导出ANCHORE_CLI_URLANCHORE_CLI_URLANCHORE_CLI_URL变量,就像您之前做的那样,以便我们可以运行anchore-cli命令:
docker-compose up -d
  1. 通过运行anchore-cli system status命令来检查 Anchore 应用的状态:
anchore-cli system status
  1. 使用feeds list命令来检查 feeds 列表是否都已更新:
anchore-cli system feeds list
  1. 一旦所有的 feeds 都已经更新,添加我们推送到 Docker Hub 的postgres-app镜像。使用anchore-cli提供的image add命令,并提供我们想要扫描的镜像的仓库、镜像和标签。这将把镜像添加到我们的 Anchore 数据库中,准备进行扫描:
anchore-cli image add <your repository namespace>/postgres-app:activity2
  1. 使用image list命令来验证我们的镜像是否已经被分析。一旦完成,您应该在Analysis Status列中看到analyzed这个词:
anchore-cli image list
  1. 使用我们的镜像名称执行image vuln命令,以查看在我们的postgres-app镜像上发现的所有漏洞的列表。这个镜像比我们之前测试过的镜像要大得多,也要复杂得多,所以当我们使用all选项时,会发现很长的漏洞列表。幸运的是,大多数漏洞要么是Negligible,要么是Unknown。运行image vuln命令并将结果传输到wc -l命令:
anchore-cli image vuln <your repository namespace>/postgres-app:activity2 all | wc -l

这将给我们一个漏洞数量的统计。在这种情况下有超过 100 个值:

108
  1. 最后,使用evaluate check命令来查看发现的漏洞是否会给我们通过或失败:
anchore-cli evaluate check <your repository namespace>/postgres-app:activity2

幸运的是,正如您从以下输出中看到的,我们通过了:

Image Digest: sha256:57d8817bac132c2fded9127673dd5bc7c3a97654
636ce35d8f7a05cad37d37b7
Full Tag: docker.io/vincesestodocker/postgres-app:activity2
Status: pass
Last Eval: 2019-11-23T06:15:32Z
Policy ID: 2c53a13c-1765-11e8-82ef-23527761d060

由于该镜像由一个大型组织提供,他们有责任确保您可以安全使用它,但由于扫描镜像如此容易,我们仍然应该扫描它们以验证它们是否 100%安全可用。

12. 最佳实践

活动 12.01:查看 Panoramic Trekking App 使用的资源

解决方案:

我们在本章中执行第一个活动的方式有很多种。以下步骤是通过使用docker stats命令查看 Panoramic Trekking App 中服务使用的资源的一种方法。在本例中,我们将使用作为 Panoramic Trekking App 的一部分运行的postgresql-app服务:

  1. 创建一个脚本,将创建一个新表并用随机值填充它。以下脚本正是我们在这种情况下想要的,因为我们想创建一个长时间的处理查询,并查看它如何影响我们容器上的资源。添加以下细节,并使用您喜欢的编辑器将文件保存为resource_test.sql
1 CREATE TABLE test_data
2 (
3     random_value NUMERIC NOT NULL,
4     row1         NUMERIC NOT NULL,
5     row2         NUMERIC NOT NULL
6 );
7 
8 INSERT INTO test_data
9     SELECT random_value.*,
10     gen.* ,
11     CEIL(RANDOM()*100)
12     FROM GENERATE_SERIES(1, 300) random_value,
13     GENERATE_SERIES(1, 900000) gen
14     WHERE gen <= random_value * 300;

第 1 行第 6 行创建新表并设置它包含的三行,而第 8 行到第 14 行遍历一个新表,用随机值填充它。

  1. 如果您还没有 PostgreSQL Docker 镜像的副本,请使用以下命令从受支持的 PostgreSQL Docker Hub 存储库中拉取镜像:
docker pull postgres
  1. 进入新的终端窗口并运行docker stats命令,查看正在使用的CPU百分比,以及正在使用的内存和内存百分比:
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.MemUsage}}"

在以下命令中,我们没有显示容器 ID,因为我们希望限制输出中显示的数据量:

NAME         CPU %       MEM %      MEM USAGE / LIMIT
  1. 要简单测试这个镜像,您不需要将运行的容器挂载到特定卷上,以使用您先前为此镜像使用的数据。切换到另一个终端来监视您的 CPU 和内存。启动容器并将其命名为postgres-test,确保数据库可以从主机系统访问,通过暴露运行psql命令所需的端口。我们还在此示例中使用环境变量(-e)选项指定了临时密码为docker
docker run --rm --name postgres-test -v ${PWD}/resource_test.sql:/resource_test.sql -e POSTGRES_PASSWORD=docker -d -p 5432:5432 postgres
  1. 在运行测试脚本之前,切换到监视 CPU 和内存使用情况的终端。您可以看到我们的容器已经在没有真正做任何事情的情况下使用了一些资源:
NAME            CPU %    MEM %     MEM USAGE / LIMIT
postgres-test   0.09%    0.47%     9.273MiB / 1.943GiB
  1. 使用以下命令进入容器内的终端:
docker exec -it postgres-test /bin/bash
  1. 使用psql命令发送postgres-test容器命令以创建名为resource_test的新数据库:
psql -h localhost -U postgres -d postgres -c 'create database resource_test;'
Password for user postgres: 
CREATE DATABASE
  1. 运行您之前创建的脚本。确保在运行脚本之前包括time命令,因为这将允许您查看完成所需的时间:
time psql -h localhost -U postgres -d resource_test -a -f resource_test.sql

我们已经减少了以下代码块中命令的输出。用数据填充resource_database表花费了 50 秒:

Password for user postgres: 
…
INSERT 0 13545000
real    0m50.446s
user    0m0.003s
sys     0m0.008s
  1. 移动到运行docker stats命令的终端。您将看到一个输出,取决于系统运行的核心数量和可用的内存。正在运行的脚本似乎不太耗费内存,但它正在将容器可用的 CPU 推高到 100%:
NAME            CPU %      MEM %    MEM USAGE / LIMIT
postgres-test   100.66%    2.73%    54.36MiB / 1.943GiB
  1. 在对 CPU 和内存配置进行更改后运行容器之前,删除正在运行的容器,以确保您有一个新的数据库运行,使用以下命令:
docker kill postgres-test
  1. 再次运行容器。在这种情况下,您将将可用的 CPU 限制为主机系统上一半的一个核心,并且由于测试不太耗费内存,将内存限制设置为256MB
docker run --rm --name postgres-test -e POSTGRES_PASSWORD=docker -d -p 5432:5432 --cpus 0.5 --memory 256MB postgres
  1. 使用exec命令进入容器:
docker exec -it postgres-test /bin/bash
  1. 再次,在运行测试之前,创建resource_test数据库:
psql -h localhost -U postgres -d postgres -c 'create database resource_test;'
Password for user postgres: 
CREATE DATABASE
  1. 现在,为了查看对我们的资源所做的更改,限制容器可以使用的资源。再次运行resource_test.sql脚本,并通过限制资源,特别是 CPU,我们可以看到现在完成需要超过 1 分钟:
time psql -h localhost -U postgres -d resource_test -a -f resource_test.sql
Password for user postgres: 
…
INSERT 0 13545000
real    1m54.484s
user    0m0.003s
sys     0m0.005s
  1. 移动到运行docker stats命令的终端。它看起来也会有所不同,因为可用于使用的 CPU 百分比将减半。您对 CPU 所做的更改减慢了脚本的运行,并且似乎也减少了内存的使用:
NAME            CPU %     MEM %      MEM USAGE / LIMIT
postgres-test   48.52%    13.38%     34.25MiB / 256MiB

这项活动为您提供了一个很好的指示,有时您需要执行平衡的行为,当您监视和配置容器资源时。它确实澄清了您需要了解您的服务正在执行的任务,以及对配置的更改将如何影响您的服务的运行方式。

活动 12.02:使用 hadolint 改进 Dockerfile 的最佳实践

解决方案

有许多种方法可以执行此活动。以下步骤展示了一种方法:

  1. 使用以下docker pull命令从hadolint存储库中拉取镜像:
docker pull hadolint/hadolint
  1. 使用hadolint来检查我们在本章中一直在使用的docker-stress Dockerfile并记录所呈现的警告:
docker run --rm -i hadolint/hadolint < Dockerfile

你会收到以下警告:

/dev/stdin:1 DL3006 Always tag the version of an image explicitly
/dev/stdin:2 DL3008 Pin versions in apt get install. Instead of 
'apt-get install <package>' use 'apt-get install 
<package>=<version>'
/dev/stdin:2 DL3009 Delete the apt-get lists after installing 
something
/dev/stdin:2 DL3015 Avoid additional packages by specifying 
'--no-install-recommends'
/dev/stdin:2 DL3014 Use the '-y' switch to avoid manual input 
'apt-get -y install <package>'
/dev/stdin:3 DL3025 Use arguments JSON notation for CMD 
and ENTRYPOINT arguments

与你最初测试镜像时没有真正的变化。然而,在Dockerfile中只有三行代码,所以看看是否可以减少hadolint呈现的警告数量。

  1. 正如本章前面提到的,hadolint维基页面将为你提供如何解决所呈现的每个警告的详细信息。然而,如果你逐行进行,应该能够解决所有这些警告。首先呈现的DL3006要求标记你正在使用的 Docker 镜像版本,这是 Ubuntu 镜像的新版本。将你的Dockerfile行 1更改为现在包括18.08镜像版本,如下所示:
1 FROM ubuntu:18.08
  1. 接下来的四个警告都与我们Dockerfile的第二行有关。DL3008要求固定安装的应用程序版本。在下面的情况下,将 stress 应用程序固定到 1.0.3 版本。DL3009指出你应该删除任何列表。这就是我们在下面的代码中添加行 4行 5的地方。DL3015指出你还应该使用--no-install-recommends,确保你不安装不需要的应用程序。最后,DL3014建议你包括-y选项,以确保你不会被提示验证应用程序的安装。编辑Dockerfile如下所示:
2 RUN apt-get update \
3 && apt-get install -y stress=1.0.4 --no-install-recommends \
4 && apt-get clean \
5 && rm -rf /var/lib/apt/lists/*
  1. DL3025是你的最后警告,并且指出你需要将CMD指令以 JSON 格式编写。这可能会导致问题,因为你正在尝试在 stress 应用程序中使用环境变量。为了消除这个警告,使用sh -c选项运行stress命令。这样应该仍然允许你使用环境变量运行命令:
6 CMD ["sh", "-c", "stress ${var}"]

你的完整Dockerfile,现在遵循最佳实践,应该如下所示:

FROM ubuntu:18.04
RUN apt-get update \
 && apt-get install -y stress=1.0.4 --no-install-recommends \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*
CMD ["sh", "-c", "stress ${var}"]
  1. 现在,再次使用hadolintDockerfile进行检查,不再呈现任何警告:
docker run --rm -i hadolint/hadolint < Dockerfile
  1. 如果你想百分之百确定Dockerfile看起来尽可能好,进行最后一次测试。在浏览器中打开FROM:latest,你会看到Dockerfile显示最新更改时的没有找到问题或建议!图 12.4:docker-stress Dockerfile 现在遵循最佳实践

图 12.4:docker-stress Dockerfile 现在遵循最佳实践

您的Dockerfiles可能比本章中呈现的要大得多,但是正如您所看到的,逐行系统地处理将帮助您纠正Dockerfiles可能存在的任何问题。使用诸如hadolintFROM latest之类的应用程序,以及它们关于如何解决警告的建议,将使您熟悉随着实践而来的最佳实践。这就是我们活动和本章的结束,但是还有更多有趣的内容要学习,所以现在不要停下来。

13.监控 Docker 指标

活动 13.01:创建 Grafana 仪表板以监控系统内存

解决方案

有许多方法可以执行此活动。以下步骤是一种方法:

  1. 确保 Prometheus 正在运行和收集数据,Docker 和cAdvisor已配置为公开指标,并且 Grafana 正在运行并配置为使用 Prometheus 作为数据源。

  2. 打开 Grafana Web 界面和您在练习 13.05:在您的系统上安装和运行 Grafana中创建的Container Monitoring仪表板

  3. 仪表板顶部和仪表板名称右侧有一个“添加面板”的选项。单击“添加面板”图标以添加新的仪表板面板:图 13.26:向容器监控仪表板添加新面板

图 13.26:向容器监控仪表板添加新面板

  1. 从下拉列表中选择Prometheus作为我们将使用的数据源,以生成新的仪表板面板。

  2. metrics部分,添加以下 PromQL 查询,container_memory_usage_bytes,仅搜索具有名称值的条目。然后,按每个名称求和,为每个容器提供一条线图:

sum by (name) (container_memory_usage_bytes{name!=""})
  1. 根据时间序列数据库中可用的数据量进行调整相对时间(如果需要)。也许将相对时间设置为15m。前三个步骤如下图所示:图 13.27:向容器监控仪表板添加新面板

图 13.27:向容器监控仪表板添加新面板

  1. 选择“显示选项”并添加“内存容器使用”作为标题。

  2. 如果单击“保存”,您会注意到无法保存面板,因为仪表板已在启动时进行了配置。您可以导出 JSON,然后将其添加到您的配置目录中。单击“共享仪表板”按钮并导出 JSON。选择“将 JSON 保存到文件”并将仪表板文件存储在“/tmp 目录”中:图 13.28:警告,我们无法保存新的仪表板

图 13.28:警告,我们无法保存新的仪表板

  1. 停止运行 Grafana 容器,以便您可以添加到环境中的配置文件。使用以下docker kill命令执行此操作:
docker kill grafana
  1. 您已经在provisioning/dashboards目录中有一个名为ContainerMonitoring.json的文件。从您的tmp目录中复制刚创建的 JSON 文件,并替换provisioning/dashboards目录中的原始文件:
cp /tmp/ContainerMonitoring-1579130313205.json provisioning/dashboards/ContainerMonitoring.json
  1. 再次启动 Grafana 图像,并使用默认管理密码登录应用程序:
docker run --rm -d --name grafana -p 3000:3000 -e "GF_SECURITY_ADMIN_PASSWORD=secret" -v ${PWD}/provisioning:/etc/grafana/provisioning grafana/Grafana
  1. 再次登录到 Grafana,并转到您一直在配置的“容器监控”仪表板。您现在应该在我们的仪表板顶部看到新创建的“内存容器使用情况”面板,类似于以下屏幕截图:图 13.29:显示内存使用情况的新仪表板面板

图 13.29:显示内存使用情况的新仪表板面板

现在应该更容易监视系统上运行的容器的内存和 CPU 使用情况。仪表板提供了比查看docker stats命令更简单的界面,特别是当您开始在系统上运行更多容器时。

活动 13.02:配置全景徒步应用程序向 Prometheus 公开指标

解决方案

我们可以以多种方式执行此活动。在这里,我们选择向全景徒步应用程序的 PostgreSQL 容器添加导出器:

  1. 如果您没有全景徒步应用程序在运行,请确保至少 PostgreSQL 容器正在运行,以便您可以完成此活动。您还不需要运行 Prometheus,因为您需要先对配置文件进行一些更改。运行以下命令以验证 PostgreSQL 数据库是否正在运行:
docker run --rm -d --name postgres-test -e POSTGRES_PASSWORD=docker -p 5432:5432 postgres

要从您的 PostgreSQL 容器中收集更多指标,您可以在 GitHub 上找到用户albertodonato已经创建的导出器。使用其他人已经创建的导出器比自己创建要容易得多。文档和详细信息可以在以下网址找到:github.com/albertodonato/query-exporter

  1. 上述的 GitHub 帐户有一个很好的分解如何设置配置和指标。设置一个基本的配置文件以开始。通过运行以下docker inspect命令找到 PostgreSQL 容器正在运行的 IP 地址。这会给出容器正在运行的内部 IP 地址。您还需要替换您正在运行的容器名称为<container_name>
docker inspect --format '{{ .NetworkSettings.IPAddress }}' <container_name>

您的 IP 地址可能与此处的不同:

172.17.0.3
  1. 对于这个导出器,您需要设置一些额外的配置来输入到导出器中。首先,在您的工作目录中创建一个名为psql_exporter_config.yml的配置文件,并用文本编辑器打开该文件。

  2. 在下面的配置文件中输入前四行。这是导出器连接到数据库的方式。您需要提供可以访问数据库的密码以及在上一步中获得的 IP 地址,或者如果为数据库分配了域,则需要提供域:

1 databases:
2   pg:
3     dsn: postgresql+psycopg2://postgres:<password>@<ip|domain>/        postgres
4
  1. 将您的第一个指标添加到配置文件中。输入以下行以添加指标名称、规模类型、描述和标签:
5 metrics:
6   pg_process:
7     type: gauge
8     description: Number of PostgreSQL processes with their         states
9     labels: [state]
10
  1. 设置一个数据库查询,以收集您想要用于pg_process规模的指标详细信息。第 13 行显示您想要创建一个数据库查询,第 1415 行将结果分配给您之前创建的指标。第 1623 行是我们想要在数据库上运行的查询,以便为数据库上运行的进程数量创建一个规模:

psql_exporter_config.yml

11 queries:
12   process_stats:
13     databases: [pg]
14     metrics:
15       - pg_process
16     sql: >
17       SELECT
18         state,
19         COUNT(*) AS pg_process
20       FROM pg_stat_activity
21       WHERE state IS NOT NULL
22       GROUP BY state
23       FROM pg_stat_database

您可以在此处找到完整的代码packt.live/32C47K3

  1. 保存配置文件并从命令行运行导出器。导出器将在端口9560上公开其指标。挂载您在此活动中创建的配置文件。您还将获得adonato/query-exporter图像的最新版本:
docker run -p 9560:9560/tcp -v --name postgres-exporter ${PWD}/psql_exporter_config.yml:/psql_exporter_config.yml --rm -itd adonato/query-exporter:latest -- /psql_exporter_config.yml
  1. 打开一个网络浏览器,使用 URLhttp://0.0.0.0:9560/metrics来查看您为全景徒步应用程序运行的 PostgreSQL 容器设置的新指标:
# HELP database_errors_total Number of database errors
# TYPE database_errors_total counter
# HELP queries_total Number of database queries
# TYPE queries_total counter
queries_total{database="pg",status="success"} 10.0
queries_total{database="pg",status="error"} 1.0
# TYPE queries_created gauge
queries_created{database="pg",status="success"} 
1.5795789188074727e+09
queries_created{database="pg",status="error"} 
1.57957891880902e+09
# HELP pg_process Number of PostgreSQL processes with their states
# TYPE pg_process gauge
pg_process{database="pg",state="active"} 1.0
  1. 进入您安装了 Prometheus 的目录,用您喜欢的文本编辑器打开prometheus.yml文件,并添加导出器详细信息,以允许 Prometheus 开始收集数据:
45   - job_name: 'postgres-web'
46     scrape_interval: 5s
47     static_configs:
48     - targets: ['0.0.0.0:9560']
  1. 保存您对prometheus.yml文件所做的更改,并再次从命令行启动 Prometheus 应用程序,如下所示:
./prometheus --config.file=prometheus.yml
  1. 如果一切都按照应该的方式进行了,您现在应该在 Prometheus 的Targets页面上看到postgres-web目标,如图所示:图 13.30:在 Prometheus 上显示的新的 postgres-web 目标页面

图 13.30:在 Prometheus 上显示的新的 postgres-web 目标页面

这就是活动的结束,也是本章的结束。这些活动应该有助于巩固之前学到的知识,并为您提供了在更加用户友好的方式中收集应用程序和运行系统的指标的经验。

14. 收集容器日志

活动 14.01:为您的 Splunk 安装创建一个 docker-compose.yml 文件

解决方案

有许多方法可以执行这项活动。以下步骤概述了一种可能的方法。

在这里,您将设置一个docker-compose.yml文件,该文件至少会以与本章中一直运行的方式运行您的 Splunk 容器。您将设置两个卷,以便挂载/opt/splunk/etc目录,以及/opt/splunk/var目录。您需要公开端口800099978088,以允许访问您的 Web 界面并允许数据转发到 Splunk 实例。最后,您需要设置一些环境变量,以接受 Splunk 许可证并添加管理员密码。让我们开始吧:

  1. 创建一个名为docker-compose.yml的新文件,并用您喜欢的文本编辑器打开它。

  2. 从您喜欢的Docker Compose版本开始,并创建要用于挂载varext目录的卷:

1 version: '3'
2
3 volumes:
4   testsplunk:
5   testsplunkindex:
6
  1. 使用splunk作为主机名和splunk/splunk作为您安装的镜像来设置 Splunk 安装的服务。此外,设置SPLUNK_START_ARGSSPLUNK_PASSWORD的环境变量,如下所示:
7 services:
8   splunk:
9     hostname: splunk
10    image: splunk/splunk
11    environment:
12      SPLUNK_START_ARGS: --accept-license
13      SPLUNK_PASSWORD: changeme
  1. 最后,挂载卷并公开安装所需的访问 Web 界面和从转发器和容器转发数据的端口:
14    volumes:
15      - ./testsplunk:/opt/splunk/etc
16      - ./testsplunkindex:/opt/splunk/var
17    ports:
18      - "8000:8000"
19      - "9997:9997"
20      - "8088:8088"
  1. 运行docker-compose up命令,确保一切都正常工作。使用-d选项确保它作为后台守护程序在我们系统中运行:
docker-compose up -d

该命令应返回类似以下的输出:

Creating network "chapter14_default" with the default driver
Creating chapter14_splunk_1 ... done
  1. 一旦您的 Splunk 安装再次运行,就该是让 Panoramic Trekking App 中的一个服务运行起来,这样您就可以将日志转发到 Splunk 进行索引。在使用docker run命令时,添加日志驱动程序的详细信息,就像您在本章中之前所做的那样,并确保您包括了正确的HTTP 事件收集器的令牌:
docker run --rm -d --name postgres-test \
-e POSTGRES_PASSWORD=docker -p 5432:5432 \
--log-driver=splunk \
--log-opt splunk-url=http://127.0.0.1:8088 \
--log-opt splunk-token=5c051cdb-b1c6-482f-973f-2a8de0d92ed8 \
--log-opt splunk-insecureskipverify=true \
--log-opt tag="{{.Name}}/{{.FullID}}" \
postgres -c log_statement=all 

请注意

请注意,我们在docker run命令中使用了-c log_statement=all,这将确保我们所有的 PostgreSQL 查询都被记录并发送到 Splunk。

  1. 登录 Splunk Web 界面并访问搜索和报告应用程序。在界面中输入source="http:docker logs" AND postgres-test查询,然后按Enter。由于您已经给我们的容器打了标签,您应该会看到您的容器带有名称和完整 ID 的标签,因此在搜索中添加postgres-test将确保只有您的 PostgreSQL 日志可见:图 14.48:Splunk 中显示的 PostgreSQL 日志

图 14.48:Splunk 中显示的 PostgreSQL 日志

从前面的屏幕截图中可以看出,我们的日志已经成功地通过 Splunk 流动。请注意在日志条目中添加的标签,就像前面的屏幕截图中所示的那样。

这项活动教会了我们如何在开发项目中使用 Docker Compose 实施日志记录程序。

活动 14.02:创建一个 Splunk 应用程序来监视 Panoramic Trekking App

解决方案

有许多方法可以执行这项活动。以下步骤是一种方法。在这里,您将向作为 Panoramic Trekking App 的一部分正在运行的PostgreSQL容器添加一个导出器:

  1. 确保 Splunk 正在运行,并且您一直在监视的服务已经运行了一段时间,以确保您正在为这项活动收集一些日志。

  2. 登录 Splunk Web 界面。从 Splunk 主屏幕上,点击应用程序菜单旁边的齿轮图标;您将看到您的 Splunk 环境的应用程序页面:图 14.49:Splunk 环境的应用程序页面

图 14.49:Splunk 环境的应用程序页面

  1. 点击“创建”应用按钮并填写表格。表格将类似于以下内容,其中“名称”设置为“全景徒步应用”,“文件夹名称”设置为panoramic_trekking_app,“版本”设置为1.0.0。点击“保存”以创建新应用:图 14.50:在 Splunk 中创建您的新应用

图 14.50:在 Splunk 中创建您的新应用

  1. 返回到 Splunk 主页,并确保您的“全景徒步应用”从“应用”菜单中可见。点击“全景徒步应用”以打开“搜索和报告”页面,以便您可以开始查询您的数据:图 14.51:选择全景徒步应用

图 14.51:选择全景徒步应用

  1. 在查询栏中输入source="http:docker logs" AND postgres-test AND INSERT AND is_superuser | stats count,然后按 Enter 键。搜索将查找作为应用程序的一部分创建的任何“超级用户”。当您的数据出现时,点击“可视化”选项卡,并将其更改为显示单个值的可视化:图 14.52:在查询栏中输入查询

图 14.52:在查询栏中输入查询

  1. 点击屏幕顶部的“另存为”按钮,然后选择“仪表板”面板。当您看到此屏幕时,选择要添加到新仪表板的面板,并将其命名为“PTA 监控”。还要给面板命名为“超级用户访问”,然后点击“保存”:图 14.53:向仪表板面板添加详细信息

图 14.53:向仪表板面板添加详细信息

  1. 当您看到新的仪表板时,点击“编辑”和“添加”面板按钮。选择“新建”,然后选择“单个值”作为可视化类型。将“内容标题”设置为“数据库创建”。添加source="http:docker logs" AND postgres-test AND CREATE DATABASE | stats count源字符串,然后点击“保存”。这将通过日志搜索以显示是否有人在 PostgreSQL 数据库上创建了任何数据库,这应该只在设置和创建应用程序时发生:图 14.54:编辑仪表板面板

图 14.54:编辑仪表板面板

  1. 再次点击“新面板”按钮,选择“新建”,然后从可视化中选择“柱状图”。添加一个“内容标题”为“应用使用情况”,添加source="http:docker logs" AND postgres-test AND SELECT AND photo_viewer_photo earliest=-60m | timechart span=1m count搜索查询,并点击“保存”。这个搜索将为您提供一段时间内使用应用程序查看照片的人数。

  2. 随意移动仪表板上的面板。当您对更改感到满意时,点击“保存”按钮。您的仪表板应该看起来类似于以下内容:图 14.55:用于监视 PostgreSQL 使用情况的新仪表板面板

图 14.55:用于监视 PostgreSQL 使用情况的新仪表板面板

这个活动帮助您收集 Panoramic Trekking App 的日志数据,并使用 Splunk 以更用户友好的方式显示它。

15. 使用插件扩展 Docker

活动 15.01:使用网络和卷插件安装 WordPress

解决方案:

可以使用以下步骤使用卷和网络插件为数据库和 WordPress 博客创建容器:

  1. 使用以下命令创建一个网络:
docker network create  \
--driver=store/weaveworks/net-plugin:2.5.2 \
--attachable \
wp-network

这个命令使用 Weave Net 插件创建一个网络,并使用driver标志进行指定。此外,卷被指定为attachable,这意味着您可以在将来连接到 Docker 容器。最后,容器的名称将是wp-network。您应该得到以下输出:

mk0pmhpb2gx3f6s00o57j2vd
  1. 使用以下命令创建一个卷:
docker volume create -d vieux/sshfs \
--name wp-content \
-o sshcmd=root@localhost:/tmp \
-o password=root \
-o port=2222

这个命令使用vieux/sshfs插件通过 SSH 创建一个卷。卷的名称是wp-content,并传递了ssh命令、端口和密码的额外选项:

wp-content
  1. 使用以下命令创建mysql容器:
docker run --name mysql -d \
-e MYSQL_ROOT_PASSWORD=wordpress \
-e MYSQL_DATABASE=wordpress \
-e MYSQL_USER=wordpress \
-e MYSQL_PASSWORD=wordpress \
--network=wp-network \
mysql:5.7

这个命令以分离模式运行mysql容器,并使用环境变量和wp-network连接。

  1. 使用以下命令创建wordpress容器:
docker run --name wordpress -d \
-v wp-content:/var/www/html/wp-content \
-e WORDPRESS_DB_HOST=mysql:3306 \
-e WORDPRESS_DB_USER=wordpress \
-e WORDPRESS_DB_PASSWORD=wordpress \
-e WORDPRESS_DB_NAME=wordpress \
--network=wp-network \
-p 8080:80 \
wordpress

这个命令以分离模式运行wordpress容器,并使用环境变量和wp-network连接。此外,容器的端口80在主机系统的端口8080上可用。

成功启动后,您将有两个运行中的mysqlwordpress容器:

docker ps

图 15.17:WordPress 和数据库容器

图 15.17:WordPress 和数据库容器

  1. 在浏览器中打开http://localhost:8080,以检查 WordPress 设置屏幕:图 15.18:WordPress 设置屏幕

图 15.18:WordPress 设置屏幕

WordPress 设置屏幕验证了 WordPress 是否使用了网络和卷插件。

在这个活动中,您使用 Weave Net 插件创建了一个自定义网络,并使用sshfs插件创建了一个自定义卷。您创建了一个使用自定义网络的数据库容器,以及一个使用自定义网络和自定义卷的 WordPress 容器。通过成功的设置,您的 Docker 容器可以通过自定义网络相互连接,并通过 SSH 使用卷。通过这个活动,您已经为一个真实的应用程序使用了 Docker 扩展。现在您可以自信地根据自己的业务需求和技术扩展 Docker。

posted @ 2024-05-06 18:33  绝不原创的飞龙  阅读(11)  评论(0编辑  收藏  举报