Docker-认证助理-DCA-备考指南-全-

Docker 认证助理(DCA)备考指南(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

微服务和容器改变了开发人员创建新应用程序的方式。微服务架构使我们能够将应用程序解耦成多个组件,而今天我们有工具可以提供这些组件之间有序且无缝的交互。此外,容器也改变了应用程序的部署工件。我们从二进制文件转变为容器镜像。这种新的开发工作流帮助开发人员更快、更安全地构建应用程序,并确保最终产品能够按预期工作,无论在什么环境下,都无需做大量修改。作为容器部署的应用程序将遵循通用的代码版本控制规则,这有助于我们跟踪组件发布和行为。

本书将介绍微服务和容器,并帮助我们学习这些技术的关键概念。我们将学习容器的工作原理,了解在不同场景下如何实现网络,并探索 Docker Swarm 和 Kubernetes 编排策略及环境。我们还将涵盖所有 Docker 企业版组件和功能,帮助实现生产环境中的容器即服务平台。本书涵盖的所有主题,以及样题和详细答案,将帮助你掌握通过官方 Docker 认证助理考试所需的知识。

第一章:本书适合人群

本书面向那些希望了解容器技术并准备参加 Docker 认证助理考试的人。书中也为 Docker 企业产品编写,作为 Kubernetes 术语和功能的介绍。

需要具备良好的 Linux 和 Windows 使用知识,一些网络技能将帮助你理解容器网络和使用负载均衡器及代理来提供功能齐全的容器即服务环境。

本书中的实验室集中于 Linux 主机,因为大多数当前的 Docker 企业版组件都部署在 Linux 操作系统上。Windows 主机可以作为 Docker Swarm 和 Kubernetes 集群的一部分,但控制平面使用 Linux 主机进行部署。

本书涵盖内容

第一章,使用 Docker 的现代基础设施和应用程序,介绍了微服务架构和容器作为现代基础设施的完美匹配,并涵盖了 Docker 引擎的概念。

第二章,构建 Docker 镜像,介绍了 Docker 镜像构建过程、命令行工具以及创建良好安全镜像的最佳实践。

第三章,运行 Docker 容器,展示了 Docker 如何帮助我们在系统中运行容器,并解释了这些进程如何与 Docker 引擎主机隔离。

第四章,容器持久性与网络,解释了如何管理容器生命周期外的数据,以及容器如何与内部和外部资源交互。

第五章,部署多容器应用程序,解释了如何部署基于容器的应用程序组件。我们将学习如何使用基础设施即代码文件来管理应用程序的组件。

第六章,Docker 内容信任简介,展示了如何在基于容器的环境中提高安全性,确保镜像所有权、不可变性和来源。

第七章,编排简介,在深入 Docker Swarm 和 Kubernetes 作为编排工具之前,回顾了编排概念。

第八章,使用 Docker Swarm 进行编排,讲解了 Docker Swarm 的特性和实现,解释了如何使用这个编排工具来实现应用程序。

第九章,使用 Kubernetes 进行编排,介绍了基本的 Kubernetes 概念,并将其与 Docker Swarm 进行比较,以帮助您为不同的应用或基础设施实施最佳解决方案。

第十章,Docker Enterprise 平台简介,介绍了 Docker Enterprise 组件,并解释了 Docker 如何创建一个生产就绪的容器即服务平台。

第十一章,通用控制平面,解释了 Docker Enterprise 的控制平面组件。我们将学习如何在生产环境中实现通用控制平面,以及如何管理 Docker Enterprise 平台。

第十二章,在 Docker Enterprise 中发布应用程序,回顾了发布应用程序的不同方法,并展示了如何使用 Interlock 和 Ingress Controller 来确保我们的 Docker Swarm 和 Kubernetes 平台的安全。

第十三章,使用 DTR 实现企业级注册表,解释了 Docker Enterprise 如何提供一个生产就绪的注册表,用于管理和存储 Docker 镜像。

第十四章,总结重要概念,总结了前几章中学到的最重要概念。本章将帮助我们为 Docker Certified Associate 考试做准备。

第十五章,模拟考试题目与最终备注,包含了一些模拟的 Docker Certified Associate 考试题目,并解释了考试流程的基础知识。

要充分利用本书

为了跟随本书的实验和示例,建议你在计算机上安装 Docker Engine。本书为你提供了一套虚拟环境,使你能够在不修改计算机的情况下运行所有实验。还有许多实验需要部署集群,涉及多个节点。这些实验将部署虚拟机,因此你无需安装大量节点,尽管你可以在自己的基础设施上部署所有实验。

提供的虚拟环境要求在你的计算机上安装 Vagrant(www.vagrantup.com/)和 VirtualBox(www.virtualbox.org/)。Docker 镜像和软件将从互联网下载,因此也需要互联网连接。下表显示了运行本书所有实验所需的计算机资源。完成每个部分或章节的所有实验后,你可以通过销毁环境来释放资源。

本书中涵盖的软件/硬件 章节 运行虚拟环境的操作系统要求
Docker 独立平台(Docker Engine) 1 到 7 2 vCPU,4 GB RAM 和 10 GB 磁盘空间。
Docker Swarm 集群平台 8 4 vCPU,8 GB RAM 和 50 GB 磁盘空间。
Kubernetes 集群平台 9 4 vCPU,8 GB RAM 和 50 GB 磁盘空间。
Docker Enterprise 平台 11、12 和 13 8 vCPU,16 GB RAM 和 100 GB 磁盘空间。

第 1 到第六章的实验需要一个节点。最低要求 2 vCPU 和 4 GB RAM。第八章和第九章的实验将分别部署 4 个和 3 个虚拟节点,需要更多的本地资源。在这些情况下,你的计算机至少需要 4 vCPU 和 8 GB RAM。Docker 企业实验要求更多资源,因为平台每个虚拟节点的 CPU 和内存要求较大。这些实验将顺利运行,要求至少 8 vCPU 和 16 GB RAM,因为 Vagrant 环境将部署 4 个虚拟节点,每个节点 4 GB RAM。

就磁盘空间而言,你的计算机至少需要有 100 GB 的可用空间来支持最大的环境。

最低要求的 Vagrant 版本为 2.2.8,最低要求的 VirtualBox 版本为 6.0.0。本书中的实验可以在 macOS、Windows 10 和 Linux 上执行。实验在编写本书期间已经在 Ubuntu Linux 18.04 LTS 和 Windows 10 Pro 操作系统上进行过测试。

所有实验都可以在 Docker Swarm、Kubernetes 和 Docker Enterprise 上执行,尽管建议使用虚拟环境来执行所有实验步骤,包括安装过程。

如果你正在使用本书的数字版本,我们建议你自己输入代码,或者通过 GitHub 仓库访问代码(链接将在下一节提供)。这样做可以帮助你避免因复制粘贴代码而可能出现的错误。

在参加考试前,确保你理解并能够回答第十五章中的所有问题,模拟考试问题和最终备注。本章中的问题与当前 Docker 认证助理考试中的问题非常相似。

下载示例代码文件

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

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

  1. 登录或注册到www.packt.com

  2. 选择“支持”选项卡。

  3. 点击代码下载。

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

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

  • Windows 下的 WinRAR/7-Zip

  • Mac 下的 Zipeg/iZip/UnRarX

  • Linux 下的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide。如果代码有更新,将会在现有的 GitHub 仓库中更新。

我们还提供了来自丰富图书和视频目录的其他代码包,网址为github.com/PacktPublishing/。快来看看吧!

实战中的代码

本书的实战代码视频可以在bit.ly/34FSiEp观看。

下载彩色图像

我们还提供了一份 PDF 文件,包含本书中使用的屏幕截图/图表的彩色图像。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781839211898_ColorImages.pdf.

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。以下是一个示例:“我们可以配置所需的共享存储来执行reconfigure操作。”

代码块设置如下:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /

当我们希望引起你对代码块中特定部分的注意时,相关行或项目会设置为粗体:

services:
  colors:
    image: codegazers/colors:1.16
    deploy:
      replicas: 3

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

$ sudo mount -t nfs 10.10.10.11:/data /mnt
$ sudo cp -pR /var/lib/docker/volumes/dtr-registry-c8a9ec361fde/_data/* /mnt/

粗体:表示新术语、重要词汇或你在屏幕上看到的词汇。例如,菜单或对话框中的词汇会像这样出现在文本中。以下是一个示例:“截图显示了垃圾回收配置页面。”

警告或重要提示将以如下方式显示。

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

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并将您的问题发送至customercare@packtpub.com

勘误:尽管我们已尽力确保内容的准确性,但错误偶尔会发生。如果您发现本书中的错误,我们将感激您能报告给我们。请访问www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入相关细节。

盗版:如果您在互联网上发现任何我们作品的非法复制版,我们将非常感激您提供相关位置地址或网站名称。请通过copyright@packt.com与我们联系,并提供该资料的链接。

如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有兴趣撰写或参与编写书籍,请访问authors.packtpub.com

评价

请留下评论。在您阅读并使用完本书后,不妨在您购买该书的网站上留下评价。潜在读者可以通过您的公正意见做出购买决策,我们 Packt 也能了解您对我们产品的看法,我们的作者也可以看到您对他们书籍的反馈。谢谢!

想了解更多关于 Packt 的信息,请访问packt.com

第二章

第一部分 - 关键容器概念

本节首先聚焦于关键的容器概念。我们将学习它们的主要特点,如何创建镜像,如何提供网络和持久化存储功能,以及容器如何帮助我们提高与进程相关的安全性。你还将学习如何在 Linux 和 Windows 环境中创建和部署基于容器的应用。

本节包括以下章节:

  • 第一章,使用 Docker 构建现代基础设施与应用

  • 第二章,构建 Docker 镜像

  • 第三章,运行 Docker 容器

  • 第四章,容器持久性和网络连接

  • 第五章,部署多容器应用

  • 第六章,Docker 内容信任介绍

使用 Docker 的现代基础设施和应用程序

微服务和容器可能是近年来最常被提及的流行词汇。如今,我们仍然可以在全球各地的会议上听到关于它们的讨论。虽然这两个术语在谈论现代应用时肯定相关,但它们并不相同。事实上,我们可以在没有容器的情况下执行微服务,也可以在容器中运行大型单体应用程序。在容器的世界中,当我们谈论容器时,一个广为人知的词汇浮现在脑海中——Docker。

本书是一本关于通过 Docker 认证助理考试的指南,这项考试是对与该技术相关的知识的认证。我们将涵盖通过该考试所需的每个主题。本章将从什么是微服务以及它们在现代应用程序中的重要性开始。我们还将讲解 Docker 如何管理该应用程序的逻辑组件的需求。

本章将引导你了解 Docker 的主要概念,并为你提供一个基本的理解,帮助你掌握管理容器所需的工具和资源。

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

  • 理解应用程序的演变

  • 基础设施

  • 进程

  • 微服务与进程

  • 什么是容器?

  • 学习容器的主要概念

  • Docker 组件

  • 构建、运输和运行工作流程

  • Windows 容器

  • 自定义 Docker

  • Docker 安全

让我们开始吧!

第三章:技术要求

本章我们将学习各种 Docker 引擎的概念。我们将在本章末尾提供一些实验室,这些实验室将帮助你理解和学习所展示的概念。这些实验室可以在你的笔记本电脑或 PC 上运行,使用提供的 Vagrant 独立环境或你拥有的任何已部署的 Docker 主机。你可以在本书的 GitHub 仓库中找到更多信息:github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

请查看以下视频,了解实际中的代码:

"bit.ly/3jikiSl"

理解应用程序的演变

正如我们可能在每个 IT 媒体中看到的,微服务的概念是现代应用开发的关键。让我们稍微回顾一下,看看多年来应用程序是如何发展的。

单体应用是指将所有组件组合成一个通常在单个平台上运行的单一程序的应用程序。这些应用程序在设计时并未考虑可重用性,也没有考虑模块化。这意味着每次它们的代码需要更新时,所有应用程序都必须参与这个过程;例如,必须重新编译所有应用程序代码才能使其正常工作。当然,那时的要求并不那么严格。

应用程序在任务和功能方面的数量不断增加,其中一些任务被分配到其他系统甚至更小的应用程序中。然而,核心组件保持不变。我们使用这种编程模型,因为将所有应用组件放在同一主机上运行比从其他主机上查找所需信息更好。然而,网络速度在这方面不足。这些应用程序难以扩展,且难以升级。实际上,某些应用程序被锁定在特定的硬件和操作系统上,这意味着开发人员在开发阶段需要使用相同的硬件架构才能演化应用程序。

我们将在下一节讨论与这些单体应用相关的基础设施。以下图表展示了任务或功能的解耦是如何从单体应用程序发展到简单对象访问协议SOAP)应用程序,并最终演变成微服务的新范式:

为了实现更好的应用性能和解耦组件,我们转向了三层架构,基于表现层、应用层和数据层。这使得不同类型的管理员和开发人员可以参与到应用更新和升级中。每一层都可以在不同的主机上运行,但组件只会在同一应用内部进行通信。

这一模型现在仍然存在于我们的数据中心中,在到达数据库之前将前端与应用后端分离,数据库中存储着所有必需的数据。这些组件发展成提供可扩展性、高可用性和管理功能的系统。偶尔,我们需要加入新的中间件组件来实现这些功能(从而增加了最终的方程式;例如,应用服务器、分布式事务应用程序、排队和负载均衡器)。更新和升级变得更容易,我们通过隔离组件来专注于不同的应用功能。

随着虚拟机在数据中心的出现,这一模型得到了扩展,并变得更好。我们将在下一节中更详细地讨论虚拟机如何改进该模型的应用。

随着 Linux 系统的普及,不同组件之间的交互,最终是不同应用之间的交互,已经成为了一项必要的要求。SOAP 和其他排队消息集成技术帮助应用程序和组件交换信息,而我们数据中心的网络改进使我们能够开始将这些元素分布到不同的节点,甚至不同的地点。

微服务是将应用程序组件解耦为更小单元的一步。我们通常将微服务定义为一个小的业务功能单元,能够独立开发和部署。按照这个定义,应用程序将由许多微服务组成。微服务在主机资源使用方面非常轻量,这使得它们可以非常快速地启动和停止。同时,它还允许我们将应用程序的健康状态从高可用性概念转向弹性,即使进程崩溃(这可能是由问题或仅仅是组件代码更新引起的),我们也能尽快启动一个新的进程,以保持主要功能的健康。

微服务架构考虑的是无状态设计。这意味着微服务的状态应该在其自身逻辑之外进行管理,因为我们需要能够为我们的微服务运行多个副本(向上或向下扩展),并根据我们的全球负载要求(例如)在环境中的所有节点上运行其内容。我们将功能与基础设施解耦(我们将在下一章中看到这个“随处运行”概念可以走多远)。

微服务提供以下特性:

  • 将应用程序分成多个部分进行管理,使我们能够用更新版本甚至完全新的功能替代一个组件,而不会丢失应用程序功能。

  • 开发人员可以专注于某个特定的应用功能或特性,只需要知道如何与其他类似的部分交互。

  • 微服务之间的交互通常通过标准的 HTTP/HTTPS API 表述性状态转移REST)调用实现。RESTful 系统的目标是提高性能、可靠性以及可扩展性。

  • 微服务是准备拥有独立生命周期的组件。这意味着一个不健康的组件不会完全影响应用程序的使用。我们将为每个组件提供弹性,并且应用程序不会发生完全的故障。

  • 每个微服务可以用不同的编程语言编写,这使我们能够选择最适合的语言,以获得最佳性能和可移植性。

现在我们简要回顾了多年来发展起来的著名应用架构,接下来让我们看看现代应用程序的概念。

现代应用程序具有以下特点:

  • 这些组件将基于微服务。

  • 应用程序组件的健康状态将基于弹性。

  • 组件的状态将由外部管理。

  • 它将随处运行。

  • 它将为组件的轻松更新做好准备。

  • 每个应用程序组件将能够独立运行,但会提供一种与其他组件交互的方式。

让我们一起来看看。

基础设施

对于开发人员为其应用程序使用的每个描述的应用模型,我们需要提供一些协调一致的基础设施架构。

在单体应用中,正如我们所看到的,所有应用功能都是一起运行的。在某些情况下,应用是为特定的架构、操作系统、库、二进制版本等构建的。这意味着我们至少需要一个硬件节点用于生产环境,并且需要相同的节点架构,最终还需要相同的资源来进行开发。如果将先前的环境(例如认证或预生产环境,用于性能测试)添加到这个方程中,每个应用所需的节点数量在物理空间、资源和应用所花费的资金上都会变得非常重要。

对于每次应用发布,开发人员通常需要一个完整的生产环境,意味着只有配置在不同的环境之间会有所不同。这很困难,因为每当操作系统的组件或功能更新时,必须将这些更改复制到所有应用环境中。虽然有许多工具可以帮助我们完成这些任务,但这并不容易,而几乎完全复制的环境所带来的成本也是一个需要关注的问题。另一方面,节点的提供可能需要数月时间,因为在许多情况下,一个新的应用发布意味着必须购买新的硬件。

第三层应用通常会部署在旧的基础设施上,使用应用服务器来允许应用管理员在可能的情况下扩展组件,并优先考虑某些组件。

在我们的数据中心使用虚拟机后,我们能够在虚拟节点之间分配主机硬件资源。这在节点提供时间、维护成本和许可费用方面是一场革命。虚拟机在单体应用和第三层应用上表现得非常好,但应用性能取决于应用于虚拟节点的主机共享资源。将应用组件部署在不同的虚拟节点上是一个常见的用例,因为它使我们能够几乎在任何地方运行这些应用。另一方面,我们仍然依赖操作系统资源和版本,因此构建新版本仍然依赖于操作系统。

从开发人员的角度来看,拥有不同的环境来构建组件、并行测试它们以及对应用进行认证变得非常简单。然而,这些新的基础设施组件需要新的管理员,并且需要提供开发和部署所需的节点。在快速增长的企业中,应用程序经常发生变化,这种模型帮助开发人员提供工具和环境。然而,当新应用需要每周创建,或者我们需要每天完成大量版本发布/修复时,敏捷性问题依然存在。像 Ansible 或 Puppet 这样的新部署工具使虚拟化管理员能够比以往更快地提供这些节点,但随着基础设施的增长,管理变得更加复杂。

本地数据中心已经变得过时,尽管这花费了一些时间,但基础设施团队开始使用云计算提供商。他们从一些服务开始,比如基础设施即服务IaaS),这使我们可以像在数据中心一样,在云上部署虚拟节点。随着新的网络速度和可靠性的出现,我们可以轻松地开始将应用程序部署到任何地方,数据中心变得越来越小,应用程序开始在不同云提供商的分布式环境中运行。为了简化自动化,云提供商为我们准备了基础设施的 API,使用户能够在几分钟内部署虚拟机。

然而,随着许多虚拟化选项的出现,基于 Linux 内核功能及其隔离模型的其他选项也应运而生,重新拾起一些过去的老项目,如 chroot 和监狱环境(在伯克利软件分发版BSD)操作系统中相当常见)或 Solaris zones。

进程容器的概念并不新鲜,事实上,它已经有超过 10 年的历史。进程容器的设计目的是将某些资源,如 CPU、内存、磁盘 I/O 或网络,隔离到一组进程中。这一概念就是现在所称的控制组(也称为cgroups)。

以下图表展示了容器进入企业环境的大致时间线:

几年后,发布了一种容器管理器实现,提供了一种便捷的方式来控制 cgroups 的使用,同时也整合了 Linux 命名空间。这个项目被命名为Linux 容器LXC),至今仍然可用,并且对于其他人来说,它为提高进程隔离使用提供了一个简单的解决方案。

2013 年,关于容器如何在我们环境中运行的全新愿景被提出,提供了一个易于使用的容器接口。它从一个开源解决方案开始,Solomon Hykes 等人启动了后来被称为 Docker, Inc.的公司。他们迅速提供了一套工具,用于在社区中运行、创建和分享容器。随着容器的日益流行,Docker, Inc.迅速发展壮大。

容器为我们的应用程序和基础设施带来了巨大的变革,随着我们深入探索这一领域,容器技术将继续发展。

进程

进程是我们与底层操作系统交互的一种方式。我们可以将程序描述为一组在系统上执行的编码指令;而进程则是这些代码在执行过程中展现出来的形式。在进程执行过程中,它会使用系统资源,如 CPU 和内存,尽管它在自己的环境中运行,但它可以与在同一系统上并行运行的其他进程共享信息。操作系统提供了可以在执行过程中操作进程行为的工具。

系统中的每个进程都有一个唯一的标识符,称为进程标识符。进程之间的父子关系是在一个进程执行过程中调用另一个进程时创建的。第二个进程成为第一个进程的子进程(即其子进程),我们可以通过称为父 PID 的信息获取到这一关系。

进程的运行是因为用户或其他进程启动了它。这使得系统能够知道是谁发起了该操作,且该进程的所有者会通过其用户 ID 被识别。当主进程使用假冒身份创建子进程时,子进程的有效所有权是隐式的。新进程将使用主进程指定的用户。

为了与底层系统进行交互,每个进程都带有自己的环境变量,并且我们也可以使用操作系统的内建功能来操控这个环境。

进程可以根据需要打开、写入和关闭文件,并在执行过程中使用描述符指针,以便轻松访问文件系统的资源。

系统中运行的所有进程都由操作系统内核进行管理,并且已由内核安排在 CPU 上执行。操作系统内核负责为进程提供系统资源并与系统设备进行交互。

总结来说,我们可以说内核是操作系统的一部分,负责与主机硬件进行交互,并在内核空间定义下,使用不同形式的隔离来管理操作系统进程。其他进程则运行在用户空间的定义下。内核空间拥有更高的资源优先级并管理用户空间。

这些定义是所有现代操作系统共有的,并且在理解容器时至关重要。现在我们知道了进程是如何被识别的,并且系统与其用户之间有隔离,我们可以进入下一部分,了解容器如何与微服务编程匹配。

微服务与进程

到目前为止,我们简要回顾了几种不同的应用模型(单体应用、SOAP 以及新的微服务架构),并将微服务定义为我们可以作为应用组件构建的最小功能软件单元。

根据这个定义,我们将一个微服务与一个进程关联起来。这是运行微服务最常见的方式。一个具有完整功能的进程可以被描述为一个微服务。

一个应用程序由微服务组成,因此也由进程组成。它们之间的交互通常通过 HTTP/HTTPS/API REST 来进行。

这当然是一个定义,但我们推荐这种方法,以确保正确的微服务健康管理。

什么是容器?

到目前为止,我们已经定义了微服务以及进程如何适应这个模型。如前所述,容器与进程隔离有关。我们将容器定义为一个进程,它的所有需求都通过内核特性进行隔离。这个类似包的对象将包含运行我们的进程所需的所有代码及其依赖项、库、二进制文件和设置。凭借这个定义,我们很容易理解为什么容器在微服务环境中如此流行,但当然,我们也可以在没有容器的情况下执行微服务。相反,我们可以在一个完整的应用程序中运行容器,容器内部有许多进程,它们不需要在这个类似包的对象中相互隔离。

在多进程容器方面,虚拟机和容器之间有什么区别?让我们对比一下容器和虚拟机的特点。

容器主要基于 cgroups 和内核命名空间(kernel namespaces)。

另一方面,虚拟机基于虚拟机监控程序(hypervisor)软件。这个软件,在许多情况下可以作为操作系统的一部分运行,将为运行虚拟机操作系统的客虚拟化硬件提供沙箱式资源。这意味着每个虚拟机将运行自己的操作系统,并允许我们在同一硬件主机上执行不同的操作系统。虚拟机出现时,人们开始将它们用作沙箱环境进行测试,但随着虚拟机监控程序的成熟,数据中心开始在生产环境中使用虚拟机,现在云服务提供商也普遍采用这种做法(云服务提供商目前也提供硬件即服务)。

在这个架构中,我们展示了不同的逻辑层次,从机器硬件开始。执行虚拟机中的进程将有许多层次。每个虚拟机将拥有自己的操作系统和服务,即使我们仅仅运行一个单一的进程:

每个虚拟机将获得一部分资源和客操作系统,内核将管理如何在不同的运行进程之间共享这些资源。每个虚拟机将执行自己的内核,并运行在主机内核之上的操作系统。由于虚拟机监控程序软件将它们隔离开,因此客操作系统之间是完全隔离的。另一方面,运行多个操作系统并行会带来开销,而当考虑到微服务时,这种解决方案浪费了大量主机资源。仅仅启动操作系统就会消耗大量资源。即使是最快的硬件节点和快速的 SSD 硬盘,也需要资源和时间来启动和停止虚拟机。正如我们所见,微服务只是一个具有完整功能的进程,因此仅为少数进程运行整个操作系统似乎不是一个好主意。

在每个宿主机上,我们需要为微服务配置一切所需的内容。这意味着访问、用户、配置、网络等。事实上,我们需要为这些系统配置管理员,就像它们是裸机节点一样。这需要大量的工作,这也是配置管理工具如此流行的原因。Ansible、Puppet、Chef 和 SaltStack 等工具帮助我们统一环境。然而,记住开发人员也需要自己的环境,因此需要将这些资源乘以开发流水线中所有所需的环境数量。

我们如何在服务高峰期进行扩展?好吧,我们有虚拟机模板,目前几乎所有的虚拟机监控程序都允许我们通过命令行或它们自己的管理 API 实现与之交互,因此很容易复制或克隆节点以扩展应用组件。但这将需要双倍的资源——记住我们将运行另一个完整的操作系统,其自身的资源、文件系统、网络等。虚拟机并不是弹性服务(可以扩展、缩放、到处运行并且在许多情况下按需创建)的完美解决方案。

容器将共享相同的内核,因为它们只是隔离的进程。我们只需为进程添加一个模板化的文件系统和资源(如 CPU、内存、磁盘 I/O、网络等,有时还包括主机设备)。它将在沙箱内运行,并仅使用其定义的环境。因此,容器是轻量级的,启动和停止速度与其主进程一样快。事实上,容器和它们运行的进程一样轻量,因为容器内没有其他东西在运行。容器消耗的所有资源都是与进程相关的。这在硬件资源分配方面非常好。我们可以通过观察所有微服务的负载来了解我们应用程序的实际消耗情况。

容器是微服务的完美解决方案,因为它们只会在内部运行一个进程。该进程应具备执行特定任务所需的所有功能,正如我们在微服务中描述的那样。

类似于虚拟机,容器创建也有一个名为镜像的模板概念。Docker 镜像是许多容器运行时的标准。它们确保所有从容器镜像创建的容器都将以相同的属性和功能运行。换句话说,这消除了 在我的电脑上能运行! 的问题。

Docker 容器通过默认安全性提高了我们环境的安全性。内核隔离和容器内管理的资源类型为执行提供了一个安全的环境。我们可以通过接下来的章节进一步了解如何增强这种安全性。默认情况下,容器将仅允许有限的系统调用。

这个架构描述了在不同虚拟机上运行进程与使用容器之间的主要区别:

容器的部署和管理速度更快,轻量且默认安全。由于容器在执行时的速度,容器与弹性的概念高度契合。由于其类似软件包的环境,我们可以在任何地方运行容器。我们只需要一个容器运行时来执行任何云服务提供商上的部署,就像我们在数据中心中所做的那样。这个概念将应用于所有开发阶段,因此可以放心地进行集成和性能测试。如果前面的测试通过了,由于我们在所有阶段使用相同的工件,我们可以确保它在生产环境中的执行。

在接下来的章节中,我们将深入探讨 Docker 容器的组件。不过,目前请将 Docker 容器视为一个沙箱化的进程,它在我们的系统中运行,与同一主机上所有其他正在运行的进程隔离,并且基于一个名为 Docker 镜像的模板。

学习容器的主要概念

说到容器时,我们需要理解其背后的主要概念。让我们将容器的概念分解成不同的部分,逐一尝试理解每个部分。

容器运行时

运行容器的运行时将是使进程执行和隔离成为可能的软件和操作系统功能。

Docker, Inc. 提供了一种名为 Docker 的容器运行时,基于由他们和其他知名企业(如 Red Hat/IBM 和 Google 等)赞助的开源项目,推动容器技术的发展。这个容器运行时与其他组件和工具一起打包。我们将在Docker 组件部分详细分析每个组件。

镜像

我们使用镜像作为创建容器的模板。镜像将包含我们的进程或多个进程正确运行所需的一切。这些组件可以是二进制文件、库、配置文件等,它们可以是操作系统文件的一部分,也可以是为该应用程序专门构建的组件。

镜像就像模板一样是不可变的。这意味着它们在执行过程中不会改变。每次使用镜像时,我们都会得到相同的结果。我们只会更改配置和环境,以管理不同环境中进程的行为。开发人员将创建自己的应用程序组件模板,他们可以确保如果应用程序通过了所有测试,它将在生产环境中按预期工作。这些特性确保了更快的工作流程和更短的市场投放时间。

Docker 镜像是由一系列层构建而成的,这些层打包在一起,包含了运行我们应用程序进程所需的一切。这些层都是只读的,修改内容会在创建镜像时存储在上面的层中。这样,每一层只包含与前一层的差异。

层是经过打包的,以便在不同的系统或环境之间轻松传输,并且它们包含关于运行所需架构的元信息(例如,它是否可以在 Linux 或 Windows 上运行,或者是否需要 ARM 处理器等)。镜像包含有关如何运行进程的信息,包括哪个用户将执行主进程、持久数据将存储的位置、您的进程将暴露哪些端口以便与其他组件或用户通信等。

镜像可以通过可重现的方法使用 Dockerfile 构建,或者通过存储在运行容器中的更改来获得新镜像:

这是对镜像的简要回顾。现在,让我们来看一下容器。

容器

正如我们之前所描述的,容器是一个独立运行的进程,它包含所有所需的资源,并与在同一主机上运行的其他进程相互隔离。现在我们了解了模板是什么,我们可以说容器是使用镜像作为模板创建的。实际上,容器在镜像层之上添加了一个新的读写层,用于存储与这些镜像层的文件系统差异。下图表示了容器执行过程中涉及的不同层次。正如我们所观察到的,最上面的一层才是我们真正称之为容器的部分,因为它是读写的,并允许将更改存储到主机磁盘上:

所有镜像层都是只读层,这意味着所有的更改都存储在容器的读写层中。这意味着,当我们从主机中移除容器时,这些更改将会丢失,但镜像会一直保留,直到我们将其移除。镜像是不可变的,始终保持不变。

这种容器行为使我们能够使用相同的基础镜像运行多个容器,每个容器将在自己的读写层上存储更改。下图表示了不同的镜像如何使用相同的镜像层。所有三个容器都基于相同的镜像:

在构建镜像和容器层执行时,有不同的方法来管理镜像层。Docker 使用存储驱动程序来管理这些内容,包括只读层和读写层。这些驱动程序依赖于操作系统,但它们都实现了所谓的写时复制文件系统。

存储驱动程序(称为graph-driver)将管理 Docker 如何存储和管理层之间的交互。正如我们之前提到的,有不同的驱动程序集成可供选择,Docker 将根据主机的内核和操作系统选择最佳的驱动程序。Overlay2是 Linux 操作系统中最常见和推荐的驱动程序。其他驱动程序,如 aufs、overlay 和 btfs 等,也可用,但请记住,overlay2 被推荐用于现代操作系统中的生产环境。

Devicemapper 也是一种受支持的图形驱动程序,在现代操作系统版本(Red Hat 7.6 及以上)支持 overlay2 之前,它在 Red Hat 环境中非常常见。Devicemapper 使用块设备来存储层,并且可以采用两种不同的策略进行部署:loopback-lvm(默认且仅用于测试目的)和 direct-lvm(需要额外的块设备池配置,适用于生产环境)。以下链接提供了部署 direct-lvm 所需的步骤:docs.docker.com/storage/storagedriver/device-mapper-driver/

如您所注意到的,使用写时复制文件系统将使容器在磁盘空间使用上非常小。所有常见文件在相同的基于镜像的容器之间是共享的。它们只存储与不可变文件的差异,而这些不可变文件是镜像层的一部分。因此,容器层会非常小(当然,这取决于你在容器中存储的内容,但请记住,好的容器应该很小)。当容器中的现有文件必须被修改时(记住这些文件来自底层层),存储驱动程序会对容器层执行复制操作。这个过程很快,但请记住,容器中所有要更改的内容都将遵循这个过程。作为参考,不要在重 I/O 操作或进程日志中使用写时复制。

写时复制(Copy-on-write)是一种用于创建最大效率和小型层级文件系统的策略。该存储策略通过在层之间复制文件来工作。当某一层需要更改来自另一层的文件时,它会被复制到该层顶部。如果只是需要读取访问,它将使用来自底层层的文件。通过这种方式,I/O 访问被最小化,层的大小也非常小。

许多人常问的一个问题是容器是否是临时性的。简短的回答是不是。实际上,容器对主机来说并不是临时性的。这意味着当我们在该主机上创建或运行一个容器时,它将一直存在,直到有人将其删除。如果容器没有被删除,我们可以在同一主机上重新启动已停止的容器。容器中以前的所有内容都会保留,但它不是一个存储进程状态的好地方,因为它只对该主机本地有效。如果我们希望能够在任何地方运行容器并使用编排工具来管理它们的状态,进程必须使用外部资源来存储其状态。

正如我们在后续章节中将看到的,Swarm 或 Kubernetes 将管理服务或应用程序组件的状态,如果所需的容器失败,它将创建一个新的容器。编排将创建一个新容器,而不是重用旧容器,因为在许多情况下,这个新进程会在集群池中的其他主机上执行。因此,理解你的应用程序组件将在容器中运行时必须是逻辑上短暂的,并且它们的状态应由容器之外的机制管理(如数据库、外部文件系统、通知其他服务等)是很重要的。

同样的概念也适用于网络。通常,你会让容器运行时或编排器管理容器的 IP 地址,以简化和动态化操作。除非绝对必要,否则不要使用固定的 IP 地址,而是让内部 IPAM 来为你配置它们。

容器中的网络是基于主机桥接接口和防火墙级别的 NAT 规则。Docker 容器运行时会管理容器虚拟接口的创建,以及在不同逻辑网络之间进行进程隔离,创建上述规则。我们将在第四章中看到所有提供的网络选项及其使用案例,容器持久性与网络。此外,发布应用程序是由运行时管理的,编排将添加不同的属性和许多其他选项。

使用卷可以帮助我们管理进程与容器文件系统之间的交互。卷会绕过写时复制文件系统,因此写入操作会更快。除此之外,存储在卷中的数据不会随着容器的生命周期而改变。这意味着,即使我们删除了正在使用该卷的容器,存储在其中的所有数据仍然会保留,直到有人删除它。我们可以将卷定义为在容器之间持久化数据的机制。我们将了解到,卷是容器之间共享数据的简单方式,并且可以部署需要在应用程序生命周期内持久化数据的应用程序(例如,数据库或静态内容)。使用卷不会增加容器的层大小,但在本地使用它们时,需要额外的主机磁盘资源,这些资源位于 Docker 文件系统/目录树下。

进程隔离

正如我们之前提到的,内核提供了进程隔离的命名空间。让我们回顾一下每个命名空间提供的内容。每个容器都有自己的内核命名空间,涉及以下方面:

  • 进程:主进程将成为容器内所有其他进程的父进程。

  • 网络:每个容器将拥有自己的网络栈,配备自己的接口和 IP 地址,并将使用主机接口。

  • 用户:我们可以将容器的用户 ID 映射到不同的主机用户 ID。

  • IPC:每个容器将拥有自己的共享内存、信号量和消息队列,不会与主机上的其他进程发生冲突。

  • 挂载:每个容器将拥有自己的根文件系统,我们可以提供外部挂载,关于这一点我们将在接下来的章节中学习。

  • UTS:每个容器将拥有自己的主机名,且时间将与主机同步。

以下图示表示了从主机角度和容器内部的进程树。容器内部的进程是命名空间化的,因此,它们的父进程 PID 将是主进程,且 PID 为 1:

命名空间自 Linux 2.6.26 版本(2008 年 7 月)以来就已经可用,它为容器内运行的进程提供了第一层隔离,使其无法看到其他进程。这意味着它们无法影响主机或其他容器中运行的进程。这些内核功能的成熟度使我们能够信任 Docker 命名空间隔离的实现。

网络也是隔离的,因为每个容器都有自己的网络栈,但通信会通过主机桥接接口。每当我们为容器创建 Docker 网络时,我们会创建一个新的网络桥接,这一点我们将在第四章,容器持久性和网络中进一步学习。这意味着共享同一网络(即主机桥接接口)的容器将能彼此看到,但所有在不同接口上运行的容器将无法访问它们。编排将为容器运行时网络提供不同的处理方法,但在主机层面,已描述的规则将被应用。

容器可用的主机资源由控制组管理。这种隔离将不允许容器通过耗尽资源使主机崩溃。在生产环境中,您不应允许资源无限制的容器。此项在多租户环境中必须强制执行。

编排

本书包含了关于编排的一般章节,第七章,编排简介,以及两章分别专门讲解 Swarm 和 Kubernetes,第八章,使用 Docker Swarm 进行编排,和第九章,使用 Kubernetes 进行编排。编排是管理容器交互、发布和集群池主机健康状况的机制。它将允许我们基于多个组件或容器部署应用程序,并在整个生命周期内保持其健康。通过编排,组件更新变得容易,因为它会负责在平台中进行必要的更改,以实现新的、合适的状态。

使用编排部署应用程序将需要多个实例来运行我们的进程,定义预期的状态,并在执行过程中管理其生命周期。编排将提供新的对象,容器间的通信,运行在集群中特定节点上的容器功能,以及保持所需进程副本数和所需发布版本的机制。

Swarm 包含在 Docker 二进制文件中,并作为标准功能提供。它易于部署和管理。它的部署单元被称为服务。在 Swarm 环境中,我们不直接部署容器,因为容器并不是由编排管理的。相反,我们部署服务,这些服务将通过任务来表示,任务会运行容器以维持其状态。

目前,Kubernetes 是最广泛使用的编排形式。它需要额外的部署工作,使用 Docker 社区容器运行时。它增加了许多功能,包括共享网络层的多容器对象,称为Pod,以及为所有编排的 Pod 提供的扁平化网络等。Kubernetes 是由社区驱动的,发展非常迅速。使这个平台如此受欢迎的一个特点是能够创建自己的资源类型,当所需资源不可用时,我们可以开发新的扩展功能。

我们将在第九章《使用 Kubernetes 进行编排》中详细分析 Pod 和 Kubernetes 的功能。

Docker Enterprise 提供了在“统一控制平面”下部署的高可用性编排器,确保所有组件的高可用性。

镜像仓库

我们已经了解到,容器在一个隔离的环境中执行进程,该环境是通过模板镜像创建的。因此,将容器部署到新节点上的唯一要求是容器运行时和用于创建该容器的模板。这个模板可以通过简单的 Docker 命令选项在节点之间共享。但随着节点数量的增加,这个过程可能会变得更加复杂。为了改善镜像分发,我们将使用镜像仓库,它们是这些对象的存储点。每个镜像将存储在自己的仓库中。这个概念类似于代码仓库,允许我们使用标签来描述这些镜像,从而将代码发布与镜像版本管理对齐。

一个应用程序部署流水线有不同的环境,确保它们之间有一个共同的真实点将帮助我们在不同的工作流阶段管理这些对象。

Docker 提供了两种不同的注册表方法:社区版和 Docker Trusted Registry。社区版完全没有任何安全性,也没有基于角色的访问控制。另一方面,Docker Trusted Registry 随 Docker 企业解决方案提供,是一个企业级注册表,包含了安全性、镜像漏洞扫描、集成工作流和基于角色的访问控制。我们将在第十三章中学习 Docker 企业版的注册表,实施企业级注册表与 DTR

Docker 组件

在本节中,我们将描述用于构建、分发和部署容器的主要 Docker 组件和二进制文件,涵盖所有执行阶段。

Docker Engine 是容器平台的核心组件。Docker 是一个客户端-服务器应用程序,Docker Engine 提供服务器端。这意味着我们有一个在主机上作为守护进程运行的主进程,以及一个与服务器通过 REST API 调用进行通信的客户端应用程序。

Docker Engine 的最新版本提供了客户端和服务器的独立软件包。例如,在 Ubuntu 上,如果我们查看可用的软件包,会看到如下内容:

- docker-ce-cli – Docker CLI:开源应用容器引擎

- docker-ce – Docker:开源应用容器引擎

以下图表展示了 Docker 守护进程及其不同的管理层级:

Docker 守护进程监听 Docker API 请求,并将负责所有 Docker 对象的操作,例如创建镜像、列出卷和运行容器。

默认情况下,Docker API 可通过 Unix 套接字访问。Docker API 可以通过多种编程语言的接口在代码中使用。查询正在运行的容器可以通过 Docker 客户端或其 API 直接管理;例如,使用 curl --no-buffer -XGET --unix-socket /var/run/docker.sock http://localhost/v1.24/containers/json

在使用 Swarm 协调器部署集群环境时,守护进程将共享信息,以便在节点池中执行分布式服务。

另一方面,Docker 客户端将为用户提供与守护进程交互所需的命令行。它将构建所需的 API 调用,并携带有效负载,告诉守护进程应执行哪些操作。

现在,让我们深入了解 Docker 守护进程的一个组件,进一步了解它的行为和用法。

Docker 守护进程

Docker 守护进程通常作为 systemd 管理的服务运行,尽管它也可以作为独立进程运行(例如在调试守护进程错误时非常有用)。正如我们之前所见,dockerd 提供了一个 API 接口,允许客户端发送命令并与该守护进程交互。实际上,containerd 管理着容器。它在 Docker 1.11 中作为一个独立的守护进程引入,负责管理存储、网络以及命名空间之间的交互。此外,它还会管理镜像传输,最后,它会使用另一个外部组件来运行容器。这个外部组件,RunC,将是容器的实际执行者。它的功能就是接收运行容器的命令。这些组件是由社区维护的,因此 Docker 提供的唯一守护进程是 dockerd。所有其他守护进程组件都是社区驱动的,并使用标准的镜像规范(开放容器倡议OCI)。2017 年,Docker 将 containerd 捐赠给开源社区,作为对开源社区的贡献,现在它是 云原生计算基金会CNCF)的一部分。OCI 于 2015 年成立,作为一个开放治理结构,旨在围绕容器格式和运行时创建开放的行业标准。CNCF 主持并管理目前大多数最新技术基础设施中最常用的组件。它是非营利的 Linux 基金会的一部分,参与了像 Kubernetes、Containerd 和 The Update Framework 等项目。

总结一下,dockerd 将管理与 Docker 客户端的交互。要运行容器,首先需要创建配置,使得守护进程触发 containerd(通过 gRPC)来创建容器。此过程将创建一个 OCI 定义,并使用 RunC 来运行这个新容器。Docker 以不同的名称实现了这些组件(在不同版本之间有所变化),但其基本概念依然有效。

Docker 守护进程可以在不同类型的套接字上监听 Docker 引擎 API 请求:unixtcpfd。默认情况下,Linux 上的守护进程会使用一个 Unix 域套接字(或 IPC 套接字),该套接字在启动守护进程时会创建在 /var/run/docker.sock 路径下。只有 root 用户和 Docker 组成员可以访问此套接字,因此只有 root 用户和 Docker 组成员能够创建容器、构建镜像等。实际上,执行任何 Docker 操作都需要访问这个套接字。

Docker 客户端

Docker 客户端用于与服务器交互。它需要连接到 Docker 守护进程才能执行任何操作,例如构建镜像或运行容器。

Docker 守护进程和客户端可以在同一主机系统上运行,或者我们可以管理一个连接的远程守护进程。Docker 客户端和守护进程通过服务器端 REST API 进行通信。正如我们之前所学,这种通信可以通过 UNIX 套接字(默认情况下)或网络接口执行。

Docker 对象

Docker 守护进程将使用 Docker 客户端命令行管理各种 Docker 对象。

以下是截至本书写作时最常见的对象:

  • IMAGE

  • CONTAINER

  • VOLUME

  • NETWORK

  • PLUGIN

还有一些对象,仅在我们部署 Docker Swarm 编排时才可用:

  • NODE

  • SERVICE

  • SECRET

  • CONFIG

  • STACK

  • SWARM

Docker 命令行提供了 Docker 守护进程通过 REST API 调用可以执行的操作。有一些常见操作,比如列出(或ls)、createrm(删除)和inspect,还有一些针对特定对象的限制操作,如cp(复制)。

例如,我们可以通过运行以下命令来获取主机上正在运行的容器列表:

$ docker container ls

有许多常用的别名,例如将docker container ls简写为docker ps,将docker container run简写为docker run。我建议使用长格式命令行,因为如果我们理解每个对象可以执行哪些操作,长格式会更容易记住。

Docker 生态系统中还有其他工具,例如docker-machinedocker-compose

Docker Machine 是由 Docker 创建的社区工具,允许用户和管理员轻松地在主机上部署 Docker Engine。它的开发目的是为了快速在 Azure 和 AWS 等云服务上部署 Docker Engine,但后来发展出其他实现方式,现在可以使用许多不同的驱动程序适用于多种不同的环境。我们可以使用docker-machinedocker-engine部署到 VMWare(包括 Cloud Air、Fusion、Workstation 或 vSphere)、Microsoft Hyper-V 和 OpenStack 等平台。它对于在 VirtualBox 或 KVM 上进行快速实验、演示和测试环境也非常有用,甚至可以通过 SSH 来配置docker-engine软件。docker-machine可以在 Windows 和 Linux 上运行,并提供客户端与已配置的 Docker 主机守护进程之间的集成。通过这种方式,我们可以远程与 Docker 守护进程交互,而无需通过 SSH 连接。

另一方面,Docker Compose 是一个工具,可以让我们在单个主机上运行多容器应用程序。我们在这里介绍这个概念,主要是与在 Swarm 或 Kubernetes 集群上运行的多服务应用程序相关。我们将在第五章中了解docker-compose部署多容器应用程序

构建、发布和运行工作流

Docker 提供了创建镜像(容器的模板,记住)的工具,将这些镜像分发到除构建镜像的系统以外的其他系统,最后基于这些镜像运行容器:

Docker Engine 将在所有工作流步骤中参与,我们可以在这些过程中只使用一个主机或多个主机,包括开发人员的笔记本电脑。

让我们快速回顾一下常见的工作流过程。

构建

使用容器构建应用程序很容易。以下是标准步骤:

  1. 开发人员通常在自己的计算机上编写应用程序。

  2. 当代码准备好,或者有新版本发布、新功能,或只是修复了一个 bug 时,都会进行提交部署。

  3. 如果我们的代码需要编译,可以在这个阶段进行。如果我们使用的是解释型语言编写的代码,我们将把它添加到下一个阶段。

  4. 无论是手动操作还是使用持续集成编排,我们都可以创建一个 Docker 镜像,将编译的二进制代码或解释型代码与所需的运行时和所有依赖项集成在一起。镜像是我们新的组件制品。

我们已经完成了构建阶段,构建好的镜像包含了所有内容,必须部署到生产环境。但首先,我们需要确保它的功能和健康状态(它能正常工作吗?性能如何?)。我们可以在不同的环境中使用我们创建的镜像制品进行所有这些测试。

发布

使用容器共享创建的制品更为简单。以下是一些新的步骤:

  1. 创建的镜像位于我们的构建主机系统上(甚至在我们的笔记本电脑上)。我们将把这个制品推送到镜像注册表,以确保它可以用于下一步的工作流过程。

  2. Docker 企业版提供了与 Docker 信任注册表的集成,按照从第一次推送到镜像扫描以寻找漏洞、以及在持续集成阶段从不同环境拉取不同镜像的步骤进行操作。

  3. 所有推送和拉取操作由 Docker 引擎管理,并由 Docker 客户端触发。

现在镜像已经在不同的环境中发布,在集成和性能测试期间,我们需要使用每个阶段的环境变量或配置来启动容器。

运行

因此,我们有了易于在不同环境之间共享的新制品,但我们需要在生产环境中执行它们。以下是容器为我们的应用程序带来的一些好处:

  • 所有环境将使用 Docker 引擎来执行我们的容器(进程),但仅此而已。实际上,我们不需要除了 Docker 引擎之外的任何软件部分来正确执行镜像(当然,我们简化了这个概念,因为在许多情况下我们需要使用卷和外部资源)。

  • 如果我们的镜像通过了工作流中定义的所有测试,它就可以投入生产,这一步将和在之前的环境中部署最初构建的镜像一样简单,只需使用所有必要的参数、环境变量或配置进行生产部署。

  • 如果我们的环境是使用 Swarm 或 Kubernetes 进行编排管理的,那么所有这些步骤都会安全地运行,具备韧性,使用内部负载均衡器,并具备所需的副本等平台特性。

总结一下,请记住,Docker 引擎提供了构建、发布和运行基于容器的应用程序所需的所有操作。

Windows 容器

容器最初是基于 Linux 启动的,但如今我们可以在 Windows 上运行和编排容器。微软在 Windows 2016 中集成了容器功能。通过这一版本,他们与 Docker 建立了合作关系,共同开发了一个在 Windows 上本地运行容器的引擎。

经历几次发布后,微软决定在 Windows 上采用两种不同的容器方式,分别如下:

  • Windows Server 容器WSC),或进程容器

  • Hyper-V 容器

由于 Windows 操作系统实现的特性,我们可以共享内核,但无法将进程与系统服务和 DLL 隔离。在这种情况下,进程容器需要复制所需的系统服务和多个 DLL,以便能够向底层主机操作系统发出 API 调用。这意味着,使用进程容器隔离的容器将包含许多系统进程和 DLL。在这种情况下,镜像非常庞大,并且具有不同的可移植性;我们只能运行基于相同底层操作系统版本的 Windows 容器。

正如我们所看到的,进程容器需要将一部分底层操作系统复制到内部才能运行。这意味着我们只能运行相同操作系统的容器。例如,在 Windows Server 2016 上运行容器需要一个 Windows Server 2016 基础镜像。

另一方面,Hyper-V 容器不会有这些限制,因为它们将运行在虚拟化内核之上。这会增加一些开销,但隔离性大大提高。在这种情况下,我们将无法在较旧的 Microsoft Windows 版本上运行这些类型的容器。这些容器将使用优化的虚拟化来隔离我们的进程的新内核。

以下图表表示了两种类型的 Microsoft Windows 容器隔离:

进程隔离是 Windows Server 上的默认容器隔离模式,但 Windows 10 Pro 和 Enterprise 将运行 Hyper-V 隔离。从 Windows 10 2018 年 10 月更新开始,我们可以选择使用旧式进程隔离,并通过--isolation=process标志在 Windows 10 Pro 和 Enterprise 上启用。

请检查 Windows 操作系统的可移植性,因为这是 Windows 容器中非常常见的问题。

Windows 容器的网络配置不同于 Linux。Docker 主机使用 Hyper-V 虚拟交换机为容器提供连接,并通过主机虚拟接口(Windows Server 容器)或合成虚拟机接口(Hyper-V 容器)将其连接到虚拟交换机。

定制 Docker

Docker 行为可以在守护进程和客户端层面进行管理。这些配置可以通过命令行参数、环境变量或配置文件中的定义来执行。

定制 Docker 守护进程

Docker 守护进程行为由各种配置文件和变量进行管理:

  • key.json:这个文件包含该守护进程的唯一标识符;实际上,它是守护进程的公钥,采用 JSON Web Key 格式。

  • daemon.json:这是 Docker 守护进程的配置文件。它以 JSON 格式包含了所有的参数。它采用键值(或值的列表)格式,所有守护进程的标志都可以在此文件中进行修改。注意,在 systemd 服务文件中实现的配置必须与通过 JSON 文件设置的选项不冲突,否则守护进程将无法启动。

  • 环境变量HTTPS_PROXYHTTP_PROXYNO_PROXY(或使用小写字母)将管理 Docker 守护进程及其客户端在代理后面的使用。该配置可以在 Docker 守护进程的 systemd 单元配置文件中实现,例如 /etc/systemd/system/docker.service.d/http-proxy.conf,并按照以下内容配置 HTTPS_PROXY(相同的配置也可以应用于 HTTP_PROXY):

[Service]
Environment="HTTPS_PROXY=https://proxy.example.com:443/" "NO_PROXY=localhost,127.0.0.1,docker-registry.example.com,.corp"

在克隆虚拟机时要小心 key.json 文件,因为在不同守护进程上使用相同的密钥会导致奇怪的行为。此文件由系统管理员拥有,因此你需要使用特权用户查看其内容。这个 JSON 文件包含了 Docker 守护进程的证书,采用 JSON Web Key 格式。我们可以通过 catjq 命令查看 key.json 文件的内容(jq 并非必需,但我用它来格式化输出。此命令对 JSON 文件或 JSON 输出非常有用):

**$ sudo cat /etc/docker/key.json |jq**

{

"crv": "P-256",

"d": "f_RvzIUEPu3oo7GLohd9cxqDlT9gQyXSfeWoOnM0ZLU",

"kid": "QP6X:5YVF:FZAC:ETDZ:HOHI:KJV2:JIZW: IG47:3GU6:YQJ4:YRGF:VKMP",

"kty": "EC",

"x": "y4HbXr4BKRi5zECbJdGYvFE2KtMp9DZfPL81r_qe52I",

"y": "ami9cOOKSA8joCMwW-y96G2mBGwcXthYz3FuK-mZe14"

}

默认情况下,守护进程配置文件 daemon.json 将位于以下位置:

  • Linux 系统中的 /etc/docker/daemon.json

  • Windows 系统中的 %programdata%\docker\config\daemon.json

在这两种情况下,配置文件的位置可以通过 --config-file 来更改,以指定一个自定义的非默认文件。

让我们快速回顾一下我们将为 Docker 守护进程配置的最常见和重要的标志或键。以下这些选项非常重要,通常会出现在 Docker 认证助理考试中。别担心;我们将在这里学习最重要的选项以及它们对应的 JSON 键:

守护进程参数 JSON 键 参数描述
-b, --bridge 字符串 bridge 将容器连接到网络桥接。此选项允许我们更改默认的桥接行为。在某些情况下,创建自己的桥接接口并使用附加到其中的 Docker 守护进程会非常有用。
--cgroup-parent 字符串 cgroup-parent 为所有容器设置父级 cgroup。
-D, --debug debug 此选项启用调试模式,对于解决问题非常重要。通常,最好停止 Docker 服务并手动使用 -D 选项运行 Docker 守护进程,以查看所有 dockerd 调试事件。
--data-root string data-root 这是持久化 Docker 状态的根目录(默认为 /var/lib/docker)。通过此选项,我们可以更改存储所有 Docker 数据(Swarm 键值、镜像、内部卷等)的路径。
--dns list dns 这是要使用的 DNS 服务器(默认为 [])。这三个选项使我们能够更改容器的 DNS 行为,例如,为容器环境使用特定的 DNS。
--dns-opt list dns-opt 这是要使用的 DNS 选项(默认为 [])。
--dns-search list dns-search 这是要使用的 DNS 搜索域(默认为 [])。
--experimental experimental 这启用实验性功能;不要在生产环境中使用它。
-G, --group string group 这是 Unix 套接字的组(默认为 docker)。
-H, --host list host 这是允许我们指定要使用的套接字的选项。
--icc icc 这启用容器间通信(默认为 true)。通过此选项,我们可以禁用任何容器的内部通信。
--ip IP ip 这是绑定容器端口时的默认 IP(默认为 0.0.0.0)。通过此选项,我们可以确保只有特定子网能够访问容器暴露的端口。
--label list label 为守护进程设置键=值标签(默认为 [])。使用标签时,我们可以配置容器位置的环境属性,当使用主机集群时,便可以应用这些配置。使用 Swarm 时有更好的标签方法,我们将在第八章《使用 Docker Swarm 进行编排》中学习。
--live-restore live-restore 启用在容器仍在运行时进行 Docker 的实时恢复。
--log-driver string log-driver 这是容器日志的默认驱动程序(默认为 json-file),如果我们需要使用外部日志管理器(例如 ELK 框架或仅使用 Syslog 服务器)。
-l, --log-level string log-level 这设置了日志记录级别(debuginfowarnerrorfatal)(默认为 info)。
--seccomp-profile string seccomp-profile 如果我们希望使用默认选项以外的配置,这是 seccomp 配置文件的路径。
--selinux-enabled selinux-enabled 启用 SELinux 支持。此选项对使用 Red Hat Linux/CentOS 的生产环境至关重要。默认情况下它是禁用的。
-s--storage-driver 字符串 storage-driver 这是要使用的存储驱动程序。此参数允许我们更改 Docker 选择的默认驱动程序。在最新版本中,我们将使用overlay2,因为它具有更好的稳定性和性能。其他选项包括aufsbtrfsdevicemapper
--storage-opt 列表 storage-opts 存储驱动程序选项(默认值 [])。根据使用的存储驱动程序,我们需要添加选项作为参数,例如,使用devicemapper或指定overlay2或 Windows 过滤器(MS Windows 写时复制实现)上的最大容器大小。
--tls tls 此选项启用客户端和服务器之间的 TLS 加密(由--tlsverify隐式启用)。
--tlscacert 字符串 tlscacert 仅信任由此 CA 签名的证书(默认值 ~/.docker/ca.pem)。
--tlscert 字符串 tlscert 这是 TLS 证书文件的路径(默认值 ~/.docker/cert.pem)。
--tlskey 字符串 tlskey 这是 TLS 密钥文件的路径(默认值 ~/.docker/key.pem)。
--tlsverify tlsverify 使用 TLS 并验证远程端。

容器环境中的日志信息可以通过不同的知识层级进行部署。如前表所示,Docker 守护进程有自己的日志配置,使用--log-driver。如果我们在容器执行期间没有指定任何配置,则此配置将默认应用于所有容器。因此,我们可以使用 ELK 框架将所有容器日志重定向到某个远程日志系统,例如(www.elastic.co/es/what-is/elk-stack),而某些特定容器则可以重定向到另一个日志后端。这也可以通过不同的日志驱动程序在本地应用。

Docker 客户端定制

客户端将把配置存储在用户的主目录下的.docker目录中。这里有一个配置文件,Docker 客户端会在其中查找其配置(在 Linux 上是$HOME/.docker/config.json,在 Windows 上是%USERPROFILE%/.docker/config.json)。在这个文件中,我们将为容器设置代理,如果需要连接到互联网或其他外部服务时,例如。

如果我们需要在启动时向容器传递代理设置,我们将为我们的用户在.docker/config.json中配置proxies键,例如,使用my-company-proxy

"proxies":
{
    "default":
    {
        "httpProxy": "http://my-company-proxy:3001",
        "httpsProxy": "http://my-company-proxy:3001",
        "noProxy": "*.test.example.com,.example2.com"
    }
}

这些配置可以在启动 Docker 容器时作为参数添加,具体如下:

--env HTTP_PROXY="http://my-company-proxy:3001"
--env HTTPS_PROXY="https://my-company-proxy:3001"
--env NO_PROXY="*.test.example.com,.example2.com"

我们将在第三章《运行 Docker 容器》中看到“环境选项”是什么意思。只需记住,有时我们的公司环境需要应用程序使用代理,并且有配置这些设置的方法,无论是作为用户变量,还是通过客户端配置来实现。

其他客户端功能,例如实验性标志或输出格式,将配置在config.json文件中。以下是一些配置的示例:

{
 "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}",
  "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}",
  "statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}"
}

Docker 安全性

与容器安全相关的主题很多。本章将回顾与容器运行时相关的那些。

正如我们所看到的,Docker 提供了一个客户端-服务器环境。从客户端的角度来看,有一些事项可以改善我们访问环境的方式。

不同主机上不同集群的配置文件和证书必须使用操作系统级别的文件系统安全性来加固。然而,正如您应该注意到的,Docker 客户端总是需要一个服务器来与容器进行交互。Docker 客户端只是连接到服务器的工具。牢记这一点,客户端-服务器安全性至关重要。现在,让我们来看看访问 Docker 守护进程的不同方式。

Docker 客户端-服务器安全性

Docker 守护进程将监听系统套接字(unixtcpfd)。我们已经看到,我们可以更改此行为,默认情况下,守护进程将监听本地 Unix 套接字/var/run/docker.sock

给予用户对/var/run/docker.sock的读写权限将允许其访问本地 Docker 守护进程。这使他们能够创建镜像、运行容器(甚至是特权容器、root 用户容器,并在其中挂载本地文件系统)、创建镜像等等。了解谁可以使用您的 Docker 引擎非常重要。如果您部署了 Docker Swarm 集群,情况会更糟,因为如果被访问的主机拥有主节点角色,用户将能够创建一个服务,在整个集群中运行容器。因此,务必确保 Docker 守护进程的套接字不被不受信任的用户访问,只允许授权用户访问(实际上,我们将查看其他高级机制来为容器平台提供安全的用户访问)。

Docker 守护进程默认是安全的,因为它不会导出其服务。我们可以通过在 Docker 守护进程启动过程中添加 -H tcp://<HOST_IP> 来启用远程 TCP 访问。默认情况下,将使用端口2375。如果我们使用 0.0.0.0 作为主机 IP 地址,Docker 守护进程将会在所有接口上监听。

我们可以使用 TCP 套接字启用对 Docker 守护进程的远程访问。默认情况下,通信将不安全,并且守护进程将在端口2375上监听。为了确保客户端与守护进程之间的连接是加密的,您需要使用反向代理或内建的基于 TLS 的 HTTPS 加密套接字。我们可以允许守护进程在所有主机接口的 IP 地址上监听,或者仅使用此 IP 地址启动守护进程时进行监听。要使用基于 TLS 的通信,我们需要遵循以下步骤(假设您的服务器主机名在 $HOST 变量中):

  1. 创建证书颁发机构CA)。以下命令将创建其私钥和公钥:
$ openssl genrsa -aes256 -out ca-key.pem 4096
 Generating RSA private key, 4096 bit long modulus
 ............................................................................................................................................................................................++
 ........++
 e is 65537 (0x10001)
 Enter pass phrase for ca-key.pem:
 Verifying - Enter pass phrase for ca-key.pem:
 $ openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
 Enter pass phrase for ca-key.pem:
 You are about to be asked to enter information that will be incorporated
 into your certificate request.
 What you are about to enter is what is called a Distinguished Name or a DN.
 There are quite a few fields but you can leave some blank
 For some fields there will be a default value,
 If you enter '.', the field will be left blank.
 -----
 Country Name (2 letter code) [AU]:
 State or Province Name (full name) [Some-State]:Queensland
 Locality Name (eg, city) []:Brisbane
 Organization Name (eg, company) [Internet Widgits Pty Ltd]:Docker Inc
 Organizational Unit Name (eg, section) []:Sales
 Common Name (e.g. server FQDN or YOUR name) []:$HOST
 Email Address []:Sven@home.org.au
  1. 创建一个服务器 CA 签名的密钥,确保公共名称与您用于从客户端连接到 Docker 守护进程的主机名匹配:
$ openssl genrsa -out server-key.pem 4096
 Generating RSA private key, 4096 bit long modulus
 .....................................................................++
 .................................................................................................++
 e is 65537 (0x10001)

$ openssl req -subj "/CN=$HOST" -sha256 -new -key server-key.pem -out server.csr
 $ echo subjectAltName = DNS:$HOST,IP:10.10.10.20,IP:127.0.0.1 >> extfile.cnf
 $ echo extendedKeyUsage = serverAuth >> extfile.cnf
 $ openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
 -CAcreateserial -out server-cert.pem -extfile extfile.cnf

Signature ok
 subject=/CN=your.host.com
 Getting CA Private Key
 Enter pass phrase for ca-key.pem:
  1. 启动启用 TLS 的 Docker 守护程序,并使用 CA、服务器证书和 CA 签名的密钥作为参数。这次,使用 TLS 的 Docker 守护程序将在端口2376上运行(这是守护程序 TLS 的标准端口):
$ chmod -v 0400 ca-key.pem key.pem server-key.pem
$ chmod -v 0444 ca.pem server-cert.pem cert.pem
$ dockerd --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem \
 -H=0.0.0.0:2376
  1. 使用相同的 CA,创建一个客户端 CA 签名的密钥,指定该密钥将用于客户端身份验证:
$ openssl genrsa -out key.pem 4096
 Generating RSA private key, 4096 bit long modulus
 .........................................................++
 ................++
 e is 65537 (0x10001)
 $ openssl req -subj '/CN=client' -new -key key.pem -out client.csr
 $ echo extendedKeyUsage = clientAuth > extfile-client.cnf
 $ openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \
 -CAcreateserial -out cert.pem -extfile extfile-client.cnf
 Signature ok
 subject=/CN=client
 Getting CA Private Key
 Enter pass phrase for ca-key.pem:
  1. 我们将生成的客户端证书移动到客户端主机(例如客户端的笔记本电脑)。我们还将复制公共 CA 证书文件。拥有自己的客户端证书和 CA 后,我们将能够使用 TLS 连接到远程 Docker 守护程序以保护通信。我们将使用带有--tlsverify和其他参数的 Docker 命令行来指定服务器的相同 CA、客户端证书及其签名密钥(守护程序的 TLS 通信默认端口为2376)。让我们通过docker version命令查看一个示例:
$ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$HOST:2376 version

要提供 TLS 通信,应完成所有这些步骤,并且步骤 45应用于所有客户端连接,如果我们希望识别它们的连接(例如,如果不想使用唯一的客户端证书/密钥对)。在企业环境中,具有数百甚至数千用户时,这是不可管理的,Docker Enterprise 将通过自动包含所有这些步骤提供更好的解决方案,从而提供细粒度的访问控制。

自 Docker 18.09 版本起,我们可以使用$ docker -H ssh://me@example.com:22 ps命令与 Docker 守护程序交互。要使用 SSH 连接,您需要设置 SSH 公钥身份验证。

Docker 守护程序安全性

Docker 容器运行时安全性基于以下内容:

  • 内核为容器提供的安全性

  • 运行时本身的攻击面

  • 应用于运行时的操作系统安全性

让我们更详细地看看这些内容。

命名空间

我们一直在讨论内核命名空间及其如何为容器实现所需的隔离。每个容器都使用以下命名空间运行:

  • pid:进程隔离(进程 IDPID

  • net:管理网络接口(网络NET

  • ipc:管理对 IPC 资源(进程间通信IPC

  • mnt:管理文件系统挂载点(挂载MNT

  • uts:隔离内核和版本标识符(Unix 分时系统UTS

每个容器都以自己的pid命名空间运行,它只能访问此命名空间中列出的进程。net命名空间将提供自己的接口,允许我们在不同容器上的相同端口上启动多个进程。容器的可见性默认启用。所有容器将通过主机桥接口访问外部网络。

每个容器内部都会有一个完整的根文件系统,并将其用作标准的 Unix 文件系统(具有自己的 /tmp,以及网络文件如 /etc/hosts/etc/resolv.conf)。这个专用文件系统基于写时复制(copy-on-write),使用来自镜像的不同层。

命名空间为容器提供了隔离层,而控制组将管理容器可以使用的资源量。这将确保主机不会耗尽资源。在多租户环境中,或者仅仅是为了生产环境,管理容器资源非常重要,不能允许没有资源限制的容器。

守护进程的攻击面基于用户访问权限。默认情况下,Docker 守护进程不提供任何基于角色的访问解决方案,但我们已经看到我们可以为外部客户端确保加密通信。

由于 Docker 守护进程以 root 用户身份运行(实验模式将允许我们以无 root 模式运行),所有容器都将能够,例如,挂载主机上的任何目录。这可能是一个真正的问题,这也是为什么确保只有必要的用户才能访问 Docker 套接字(无论是本地还是远程)是如此重要。

正如我们在第三章《运行 Docker 容器》中看到的,如果我们在构建镜像或启动容器时未指定用户,容器将以 root 用户身份运行。我们稍后会复习这个话题并改进默认的用户使用方式。

推荐在专用主机上仅运行 Docker 守护进程,因为当其他服务也运行在同一主机时,Docker 可能会非常危险。

用户命名空间

正如我们之前看到的,Linux 命名空间为进程提供了隔离。这些进程只能看到 cgroups 和这些命名空间所提供的内容,对这些进程来说,它们在独立运行。

我们始终建议在容器内以非 root 用户身份运行进程(例如,nginx 如果使用上层端口,则无需 root 权限即可运行),但有些情况下它们必须以 root 用户身份运行。为了防止这些 root 容器内的权限提升,我们可以应用用户重映射。该机制将容器内的 root 用户(UID 0)映射为非 root 用户(UID 30000)。

用户重映射由两个文件管理:

  • /etc/subid:这设置了下属的用户 ID 范围。

  • /etc/subgid:这设置了下属的组 ID 范围。

使用这些文件,我们分别为用户和组设置了第一个序列 ID。这是下属 ID 的示例格式,nonroot:30000:65536。这意味着容器内的 UID 0 将映射为 Docker 主机上的 UID 30000,依此类推。

我们将配置 Docker 守护进程,使用 --userns-remap 标志或 JSON 格式中的 userns-remap 键来使用此用户重映射。在特殊情况下,我们可以在运行容器时更改用户命名空间的行为。

内核能力(seccomp)

默认情况下,Docker 以有限的能力集启动容器。这意味着容器默认以无特权模式运行。因此,在容器内运行进程默认提高了应用程序的安全性。

以下是默认情况下在你的系统中运行的任何容器可用的 14 种能力:SETPCAPMKNODAUDIT_WRITECHOWNNET_RAWDAC_OVERRIDEFOWNERFSETIDKILLSETGIDSETUIDNET_BIND_SERVICESYS_CHROOT、和SETFCAP

此时最重要的理解点是,我们可以在容器内部运行监听 1024 以下端口的进程,因为我们具备了NET_BIND_SERVICE能力,例如,或者我们可以在容器内使用 ICMP,因为我们启用了NET_RAW能力。

另一方面,有许多能力默认情况下并未启用。例如,许多系统操作需要SYS_ADMIN能力,或者我们需要NET_ADMIN能力来创建新的接口(在 Docker 容器中运行openvpn时需要此能力)。

容器内的进程不会拥有实际的 root 权限。通过使用 seccomp 能力,可以做到以下几点:

  • 拒绝mount操作

  • 拒绝访问原始套接字(以防止数据包欺骗)

  • 拒绝访问某些文件系统操作,例如文件所有权

  • 拒绝模块加载,以及其他许多操作

允许的能力是通过默认的seccomp配置文件定义的。Docker 在过滤模式下使用seccomp,禁用所有未在其 JSON 格式的配置文件中列入白名单的调用。运行容器时会使用默认配置文件。我们可以通过在启动时使用--security-opt标志来使用自定义的seccomp配置文件。因此,在容器执行过程中,操作允许的能力非常简单。我们将在第三章,运行 Docker 容器的开始部分学习更多关于如何操作容器行为的内容:

$ docker container run --cap-add=NET_ADMIN--rm -it --security-opt seccomp=custom-profile.json alpine sh

这一行将运行我们的容器,添加NET_ADMIN能力。使用自定义的seccomp配置文件,我们将添加更多能力,如custom-profile.json中所定义的。出于安全原因,如果我们确定不需要某些默认能力,还可以使用--cap-drop来删除它们。

避免使用--privileged标志,因为你的容器将在无约束的情况下运行,这意味着它将几乎与主机上运行的进程具有相同的访问权限。此时,该容器的资源将不受限制(SYS_RESOURCE能力将被启用,且限制标志不会被使用)。对于用户来说,最佳做法是移除所有不必要的能力,只保留进程运行所需的能力。

Linux 安全模块

Linux 操作系统提供了确保安全的工具。在某些情况下,它们会在开箱即用的安装中预先安装并配置好,而在其他情况下,则需要管理员进行额外的配置。

AppArmor 和 SELinux 可能是最常见的两种。它们都提供对文件操作和其他安全功能的更细粒度控制。例如,我们可以确保只有允许的进程可以修改一些特殊的文件或目录(例如,/etc/passwd)。

Docker 提供的模板和策略随产品一起安装,确保与这些工具的完整集成,从而增强 Docker 主机的安全性。绝不在生产环境中禁用 SELinux 或 AppArmor,而是使用策略为你的进程添加功能或访问权限。

我们可以通过查看Docker system info输出中的SecurityOptions部分来检查在我们的 Docker 运行时中启用了哪些安全模块。

我们可以通过使用docker system info轻松查看 Docker 运行时功能。需要注意的是,输出可以通过docker system info --format '{{json .}}'以 JSON 格式显示,而且我们可以使用--filter选项进行过滤。例如,过滤可以帮助我们仅获取应用于docker system info --format '{{json .SecurityOptions}}'守护进程的安全选项。

默认情况下,Red Hat 版本的主机不会启用 SELinux,另一方面,Ubuntu 默认情况下会启用 AppArmor。

在将默认 Docker 数据根路径移动到 Red Hat Linux 的其他位置时,有一个非常常见的问题。如果启用了 SELinux(在这些系统上默认启用),则需要通过使用# semanage fcontext -a -e /var/lib/docker _MY_NEW_DATA-ROOT_PATH来将新路径添加到允许的上下文中,然后使用# restorecon -R -v _MY_NEW_DATA-ROOT_PATH

Docker 内容信任

Docker 内容信任是 Docker 提供的一种机制,用于增强内容安全性。它将提供镜像的所有权和不可变性的验证。此选项在 Docker 运行时应用,有助于加强内容执行的安全性。我们可以确保只有特定的镜像可以在 Docker 主机上运行。这将提供两个不同级别的安全性:

  • 仅允许签名镜像

  • 仅允许特定用户或组/团队签名的镜像(我们将在第十一章中了解与 Docker UCP 集成的概念,统一控制平面

我们将在第四章中了解卷,这些卷是用于容器持久存储的对象,容器持久性和网络

启用和禁用 Docker 内容信任可以通过在客户端会话中设置DOCKER_CONTENT_TRUST=1环境变量,在systemd Docker 单元中进行管理。或者,我们可以在镜像和容器操作中使用--disable-content-trust=false(默认值为 true)。

启用任何这些标志以启用内容信任时,所有 Docker 操作都会被信任,这意味着我们将无法下载和执行任何非受信任的标志(签名镜像)。

本章实验

除非另有说明,否则我们将在本书中使用 CentOS 7 作为节点实验室的操作系统。我们将现在安装 Docker 社区版,并在涉及该平台的特定章节中安装 Docker 企业版。

如果您还没有这样做,请从本书的 GitHub 仓库(github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git)部署 environments/standalone-environment。您也可以使用自己的 CentOS 7 服务器。请从 environments/standalone-environment 文件夹使用 vagrant up 启动虚拟环境。

如果您使用的是独立环境,请等待直到它正在运行。我们可以使用 vagrant status 来检查节点的状态。使用 vagrant ssh standalone 连接到您的实验节点。standalone 是您的节点名称。您将使用具有 root 权限的 vagrant 用户,通过 sudo 进行操作。您应该会看到以下输出:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant up
Bringing machine 'standalone' up with 'virtualbox' provider...
==> standalone: Cloning VM...
==> standalone: Matching MAC address for NAT networking...
==> standalone: Checking if box 'frjaraur/centos7' version '1.4' is up to date...
==> standalone: Setting the name of the VM: standalone
...
==> standalone: Running provisioner: shell...
 standalone: Running: inline script
 standalone: Delta RPMs disabled because /usr/bin/applydeltarpm not installed.
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant status
Current machine states:
standalone running (virtualbox)
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$

现在,我们可以使用 vagrant ssh standalone 连接到独立节点。如果您之前已部署过独立虚拟节点,并且只是使用 vagrant up 启动它,那么这个过程可能会有所不同:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant ssh standalone
[vagrant@standalone ~]$ 

现在,您可以开始实验了。

安装 Docker 运行时并执行一个 "hello world" 容器

本实验将引导您完成 Docker 运行时安装步骤,并运行您的第一个容器。让我们开始吧:

  1. 为了确保没有安装任何旧版本,我们将删除任何 docker* 软件包:
[vagrant@standalone ~]$ sudo yum remove docker*

  1. 通过运行以下命令来添加所需的软件包:
[vagrant@standalone ~]$ sudo yum install -y yum-utils   device-mapper-persistent-data   lvm2
  1. 我们将使用一个稳定版本,因此我们将添加其软件包仓库,如下所示:
[vagrant@standalone ~]$ sudo yum-config-manager \
--add-repo https://download.docker.com/linux/centos/docker-ce.repo
  1. 现在,安装 Docker 软件包和 containerd。我们正在为此主机安装服务器和客户端(自版本 18.06 起,Docker 提供了不同的软件包用于 docker-cli 和 Docker 守护进程):
[vagrant@standalone ~]$ sudo yum install -y docker-ce docker-ce-cli containerd.io

  1. Docker 已安装,但在类似 Red Hat 的操作系统中,默认情况下它不会在启动时启用,因此不会自动启动。验证这种情况并启用并启动 Docker 服务:
[vagrant@standalone ~]$ sudo systemctl enable docker
[vagrant@standalone ~]$ sudo systemctl start docker
  1. 现在 Docker 已安装并正在运行,我们可以运行我们的第一个容器:
[vagrant@standalone ~]$ sudo docker container run hello-world
 Unable to find image 'hello-world:latest' locally
 latest: Pulling from library/hello-world
 1b930d010525: Pull complete
 Digest: 
sha256:b8ba256769a0ac28dd126d584e0a2011cd2877f3f76e093a7ae560f2a5301c00
 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 that runs the executable, which 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 Hub 上(hub.docker.com)。为了使用此镜像,如果我们之前没有运行过任何该镜像的容器,Docker 守护进程将下载所有镜像层;换句话说,如果该镜像在本地 Docker 主机上不存在。一旦所有镜像层下载完成,Docker 守护进程将启动一个 hello-world 容器。

本书是 DCA 考试的指南,也是我们可以轻松部署的最简单实验。尽管如此,你应该能够理解并描述这个简单的过程,并思考我们可能遇到的所有常见问题。例如,如果镜像在你的主机上并且不同,但名称和标签相同,会发生什么?如果某一层无法下载,会发生什么?如果你连接到远程守护进程,会发生什么?我们将在本章末尾回顾其中的一些问题。

  1. 正如你应该已经注意到的,我们总是使用sudo来获取根权限,因为我们的用户没有访问 Docker UNIX 套接字的权限。这是攻击者必须绕过的系统的第一层安全防护。我们通常在生产环境中启用用户运行容器,因为我们希望将操作系统的职责和管理与 Docker 隔离开来。只需要将我们的用户添加到 Docker 组中,或者添加一个新组,允许该组的用户访问套接字。在这种情况下,我们将把我们的实验室用户添加到 Docker 组中:
[vagrant@standalone ~]$ docker container ls
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

[vagrant@standalone ~]$ sudo usermod -a -G docker $USER

[vagrant@standalone ~]$ newgrp docker

[vagrant@standalone ~]$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5f7abd49b3e7 hello-world "/hello" 19 minutes ago Exited (0) 19 minutes ago  festive_feynman

Docker 运行时进程与命名空间隔离

在本实验中,我们将回顾我们关于进程隔离、Docker 守护进程组件和执行工作流的学习内容。让我们开始吧:

  1. 简要回顾一下 Docker 的systemd守护进程:
[vagrant@standalone ~]$ sudo systemctl status docker
 ● docker.service - Docker Application Container Engine
 Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
 Active: active (running) since sáb 2019-09-28 19:34:30 CEST; 25min ago
 Docs: https://docs.docker.com
 Main PID: 20407 (dockerd)
 Tasks: 10
 Memory: 58.9M
 CGroup: /system.slice/docker.service
 └─20407 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

 sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.222200934+02:00" level=info msg="[graphdriver] using prior storage driver: overlay2"
 sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.234170886+02:00" level=info msg="Loading containers: start."
 sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.645048459+02:00" level=info msg="Default bridge (docker0) is assigned with an IP a... address"
 sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.806432227+02:00" level=info msg="Loading containers: done."
 sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.834047449+02:00" level=info msg="Docker daemon" commit=6a30dfc graphdriver(s)=over...n=19.03.2
 sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.834108635+02:00" level=info msg="Daemon has completed initialization"
 sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.850703030+02:00" level=info msg="API listen on /var/run/docker.sock"
 sep 28 19:34:30 centos7-base systemd[1]: Started Docker Application Container Engine.
 sep 28 19:34:43 centos7-base dockerd[20407]: time="2019-09-28T19:34:43.558580560+02:00" level=info msg="ignoring event" module=libcontainerd namespace=mo...skDelete"
 sep 28 19:34:43 centos7-base dockerd[20407]: time="2019-09-28T19:34:43.586395281+02:00" level=warning msg="5f7abd49b3e75c58922c6e9d655d1f6279cf98d9c325ba2d3e53c36...

该输出显示服务使用的是默认的systemd单元配置,并且dockerd使用默认参数;也就是说,它使用的是/var/run/docker.sock上的文件描述符套接字和默认的docker0桥接接口。

  1. 请注意,dockerd使用一个单独的containerd进程来执行容器。让我们在后台运行一些容器并查看它们的进程。我们将运行一个简单的 alpine 镜像,带有nginx守护进程:
[vagrant@standalone ~]$ docker run -d nginx:alpine
 Unable to find image 'nginx:alpine' locally
 alpine: Pulling from library/nginx
 9d48c3bd43c5: Already exists 
 1ae95a11626f: Pull complete 
 Digest: sha256:77f340700d08fd45026823f44fc0010a5bd2237c2d049178b473cd2ad977d071
 Status: Downloaded newer image for nginx:alpine
 dcda734db454a6ca72a9b9eef98aae6aefaa6f9b768a7d53bf30665d8ff70fe7

  1. 现在,我们将查找nginxcontainerd进程(进程 ID 在你的系统上会完全不同;你只需要理解其工作流程):
[vagrant@standalone ~]$ ps -efa|grep -v grep|egrep -e containerd -e nginx 
 root     15755     1  0 sep27 ?        00:00:42 /usr/bin/containerd
 root     20407     1  0 19:34 ?        00:00:02 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
 root     20848 15755  0 20:06 ?        00:00:00 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/dcda734db454a6ca72a9
 b9eef98aae6aefaa6f9b768a7d53bf30665d8ff70fe7 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc
 root     20863 20848  0 20:06 ?        00:00:00 nginx: master process nginx -g daemon off;
 101      20901 20863  0 20:06 ?        00:00:00 nginx: worker process

  1. 请注意,最后,容器是从 PID 20848启动的。跟随runtime-runc位置,我们发现了state.json,它是容器的状态文件:
[vagrant@standalone ~]$ sudo ls -laRt /var/run/docker/runtime-runc/moby
 /var/run/docker/runtime-runc/moby:
 total 0
 drwx--x--x. 2 root root 60 sep 28 20:06 dcda734db454a6ca72a9b9eef98aae6aefaa6f9b768a7d53bf30665d8ff70fe7
 drwx------. 3 root root 60 sep 28 20:06 .
 drwx------. 3 root root 60 sep 28 13:42 ..
 /var/run/docker/runtime-runc/moby/dcda734db454a6ca72a9b9eef98aae6aefaa6f9b768a7d53bf30665d8ff70fe7:
 total 28
 drwx--x--x. 2 root root    60 sep 28 20:06 .
 -rw-r--r--. 1 root root 24966 sep 28 20:06 state.json
 drwx------. 3 root root    60 sep 28 20:06 ..

该文件包含容器运行时信息:PID、挂载、设备、应用的能力、资源等。

  1. 我们的 NGINX 服务器在 Docker 主机上运行,其 PID 为20863,而nginx子进程的 PID 为20901,但让我们深入了解一下:
[vagrant@standalone ~]$ docker container exec dcda734db454 ps -ef
 PID USER TIME COMMAND
 1 root 0:00 nginx: master process nginx -g daemon off;
 6 nginx 0:00 nginx: worker process
 7 root 0:00 ps -ef

使用docker container exec,我们可以使用容器命名空间运行一个新进程。这就像在容器内运行一个新进程。

如你所观察到,在容器内部,nginx的 PID 是1,它是工作进程的父进程。当然,我们可以看到我们的命令ps -ef,因为它是通过使用其命名空间启动的。

我们可以使用相同的镜像运行其他容器,并获得相同的结果。每个容器内部的进程与其他容器和主机进程是隔离的,但 Docker 主机上的用户可以看到所有进程及其真实 PID。

  1. 让我们来看看 nginx 进程命名空间。我们将使用 lsns 命令来查看所有主机运行的进程命名空间。我们将获取所有正在运行的进程及其命名空间的列表。我们将查找 nginx 进程(我们不会使用 grep 来过滤输出,因为我们希望查看标题):
[vagrant@standalone ~]$ sudo lsns
 NS TYPE NPROCS PID USER COMMAND
 ..............
 ..............
 4026532197 mnt 2 20863 root nginx: master process nginx -g daemon off
 4026532198 uts 2 20863 root nginx: master process nginx -g daemon off
 4026532199 ipc 2 20863 root nginx: master process nginx -g daemon off
 4026532200 pid 2 20863 root nginx: master process nginx -g daemon off
 4026532202 net 2 20863 root nginx: master process nginx -g daemon off

本实验演示了容器内进程的隔离。

Docker 能力

本实验将涵盖 seccomp 能力管理。我们将使用丢弃的能力启动容器,以确保通过使用 seccomp 避免某些系统调用,容器内的进程仅执行允许的操作。让我们开始吧:

  1. 首先,使用默认允许的能力运行一个容器。在执行这个 Alpine 容器时,我们将更改 /etc/passwd 文件的所有权:
[vagrant@standalone ~]$ docker container run --rm -it alpine sh -c "chown nobody /etc/passwd; ls -l /etc/passwd"
 -rw-r--r-- 1 nobody root 1230 Jun 17 09:00 /etc/passwd

正如我们所见,没有任何事情能阻止我们更改容器文件系统内的文件所有权,因为主进程(在本例中是 /bin/sh)以 root 用户身份运行。

  1. 丢弃所有能力。让我们看看会发生什么:
[vagrant@standalone ~]$ docker container run --rm -it --cap-drop=ALL alpine sh -c "chown nobody /etc/passwd; ls -l /etc/passwd"
 chown: /etc/passwd: Operation not permitted
 -rw-r--r-- 1 root root 1230 Jun 17 09:00 /etc/passwd

你会发现操作被禁止了。由于容器在没有任何能力的情况下运行,chown 命令不允许更改文件所有权。

  1. 现在,只需添加 CHOWN 能力,允许更改容器内文件的所有权:
[vagrant@standalone ~]$ docker container run --rm -it --cap-drop=ALL --cap-add CHOWN alpine sh -c "chown nobody /etc/passwd; ls -l /etc/passwd"
 -rw-r--r-- 1 nobody root 1230 Jun 17 09:00 /etc/passwd

总结

在本章中,我们看到了现代应用程序如何基于微服务构建。我们了解了什么是容器及其优点,以及微服务和容器如何匹配。当我们将一个进程与特定功能或任务(微服务)关联并将其运行在容器内时,我们探讨了容器的概念。接着,我们谈论了镜像、容器以及将进程与主机隔离的机制。我们介绍了编排和注册表,作为在集群环境中部署具有韧性的应用程序的要求,以及我们可以管理镜像的方式。

然后,我们了解了 Docker 的主要组件以及 Docker 客户端如何与 Docker 引擎安全交互。我们介绍了最常见的 Docker 对象以及我们将用来创建、共享和部署基于容器的新应用程序的工作流程。

现在,我们可以在 Microsoft Windows 上使用容器,但这一切始于 Linux。我们比较了这两种方法,以理解它们之间的相似性和差异,并探讨了在 Windows 上使用 Hyper-V 隔离进程的高级方法。

最后,我们回顾了如何使用 JSON 文件和环境变量配置 Docker 引擎,了解到容器默认是安全的,并回顾了实现这一点的不同机制。

在下一章中,我们将使用不同的方法构建镜像,并学习创建优质镜像所需的流程和原语。

问题

  1. 只有一个进程可以在每个容器中运行吗?(选择哪些句子是正确的)

a) 我们不能在每个容器中执行多个进程。这是一个限制。

b) 我们可以在一个容器中运行多个进程,但不推荐这样做。

c) 我们将只在每个容器中运行一个进程,以遵循微服务逻辑。

d) 上述所有句子都是错误的。

  1. 哪些内核功能提供容器的主机 CPU 资源隔离?

a) 内核命名空间。

b) Cgroups(控制组)。

c) 内核域。

d) 都不是。无法隔离主机资源。

  1. 以下哪些句子是正确的?

a) 所有容器默认都将以 root 用户身份运行。

b) 用户命名空间将允许我们将 UID 0 映射到主机系统上的另一个 UID,受控并且没有任何不必要的权限。

c) 由于 Docker 守护进程以 root 用户身份运行,因此只有 root 用户才能在 Docker 主机上运行容器。

d) 上述所有句子都是错误的。

  1. 我们关于 Windows Docker 主机学到了什么?

a) Linux 容器也可以在 Windows 主机上运行。

b) Windows Hyper-V 容器将运行一个小型虚拟机,为容器提供所需资源,并且没有任何 Windows 操作系统依赖性。

c) Windows 进程隔离需要系统 DLL 和服务才能在容器中正常运行,且无法提供完全的可移植性。

d) Windows 镜像比 Linux 镜像大,因为许多情况下,运行即使是小进程也需要集成 Windows 操作系统组件。

  1. 以下哪些句子关于 Docker 守护进程配置是正确的?

a) 我们将在 Linux 上使用 JSON 格式的键值对配置 Docker 守护进程,配置文件位于 /etc/docker/daemon.jsonsystemd 单元文件中。

b) 在 Windows 主机上,我们将使用 %programdata%\docker\config\daemon.json 来配置 Docker 守护进程。

c) 默认情况下,Docker 客户端连接到远程 Docker 守护进程是不安全的。

d) 上述句子都不正确。

进一步阅读

构建 Docker 镜像

构建镜像是部署你自己的基于容器的应用程序的第一步。这个过程很简单,任何人都可以从头开始构建镜像,但要创建足够质量和安全的生产级镜像并不容易。在本章中,我们将学习创建优秀、适用于生产的镜像的所有基础知识和技巧。我们将回顾保存和分发我们的工作所需的要求,以及如何改进这些过程,以在企业环境中当镜像和发布的数量较大时获得更好的性能。

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

  • 构建 Docker 镜像

  • 了解写时复制(Copy-on-write)文件系统

  • 使用 Dockerfile 参考构建镜像

  • 镜像标签和元信息

  • Docker 注册表与仓库

  • 镜像安全

  • 管理镜像及其他相关对象

  • 多阶段构建与镜像缓存

  • 模板化镜像

  • 镜像发布和更新

让我们开始吧!

第四章:技术要求

在本章中,我们将学习 Docker 镜像构建的概念。在本章末尾,我们将提供一些实验,帮助你理解并学习本章所解释的概念。这些实验可以在你的笔记本电脑或 PC 上运行,使用提供的 Vagrant 独立环境,或任何你自己部署的 Docker 主机。你可以在本书的 GitHub 仓库中找到更多信息,链接是 github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

查看以下视频,了解代码的实际应用:

"bit.ly/31v3AJq"

构建 Docker 镜像

开发人员创建自己的镜像,以及自己的代码和运行时组件,以运行他们的应用程序组件。然而,构建过程通常从一个之前的镜像开始。所有的镜像构建过程都会以FROM语句开始。这表示将使用之前的镜像(在多层基础上叠加)来添加新的组件、二进制文件、配置或操作,以构建我们的新镜像。

你可能会问,谁负责镜像的创建? 如果没有通过持续集成平台自动生成,开发人员可能会创建应用程序镜像,但也会有团队创建供其他用户使用的基础镜像。例如,数据库管理员会创建数据库基础镜像,因为他们知道应该包含哪些组件以及如何确保其安全。开发人员会根据这些基础镜像来创建他们的组件。在大公司中,可能有许多团队在创建镜像,或者至少定义必须包含哪些组件、哪些用户使用、暴露哪些端口等等。

然而,还有一些其他的情况。如今,许多应用程序都已为容器环境做好了准备,软件制造商会提供镜像来部署他们的软件。企业将寻求统一和架构,而 DevOps 团队将为他们的同事提供标准的基础镜像。容器的基础设施运行时将对所有这些环境通用,监控应用程序、中间件、数据库等将与开发的业务应用组件一起在这个环境中运行。

创建镜像有三种方法:

  • 使用包含所有指令的文件来创建此镜像(Dockerfile)

  • 与不同容器层中的文件交互,执行一个容器,修改其内容,然后存储所做的更改(提交)

  • 使用一个空层并手动逐个文件地添加组件,这也被称为从零开始创建镜像

现在,我们将回顾每个构件,并讨论它们的优缺点和使用场景。

使用 Dockerfile 创建镜像

Dockerfile 是一个脚本文件,描述了创建新镜像所需的所有步骤。每个步骤都会被解释,并且在许多情况下,会创建一个容器来执行声明的更改,应用于之前的层。在这个 Dockerfile 中,我们将有一个创建镜像的指南。这个指南创建了一个可重复的过程。我们将确保每次使用这个脚本时,我们都会得到相同的结果。当然,这可能依赖于一些变量,但通过一些关键机制,我们可以确保相同的结果。在本章中,我们将介绍创建 Docker 镜像的主要基本构件。

一个 Dockerfile 看起来类似于以下内容:

FROM ubuntu:18.04
RUN apt-get update -qq && apt-get install -qq package1 package2
COPY . /myapp
RUN make /myapp
CMD python /myapp/app.py

在这个简单的示例中,正如我们之前提到的,我们在开始时有一个 FROM 语句:

  1. 首先,我们使用 Ubuntu 18.04 作为基础镜像。为了使用这个镜像,我们需要在我们的构建环境中获取它。因此,如果该镜像不在我们的环境中,Docker 守护进程将下载它的层,并将其提供给我们,以便在后续步骤中使用。这个过程是自动发生的;Docker 守护进程会为我们完成这项工作。

  2. 使用下载的 Ubuntu 18.04 层,Docker 将自动运行一个容器,使用这个镜像并执行声明的命令,因为我们使用了 RUN 基本构件。在这个简单的例子中,Shell(因为它是 Ubuntu 18.04 镜像的默认命令)将执行 apt-get update 来更新容器的包缓存。如果此命令执行成功,它将使用 apt-get install 安装 package1package2

  3. 软件安装完成后,Docker 将内部执行 Docker container commit 命令,将这些更改持久化到一个新的层中,以便将其用作下一步的基础。第三行将把我们当前目录的内容复制到新运行容器中的应用代码目录。

  4. 下一行将执行make(这只是一个示例;我们没有说明我的应用程序使用的编程语言等)。这行命令将在新容器中运行该操作。结果,当操作完成时,一个新的镜像将自动创建。

  5. 我们了解到,容器总是使用镜像作为模板来创建的。代码的最后一行定义了每次我们使用此镜像创建容器时要运行的命令行。

总结来说,Dockerfile 提供了创建镜像所需的所有步骤指南,以便我们能够运行应用程序。这是一个可复现的过程,因此,每次使用该文件创建新镜像时,我们应该获得相同的结果(例如,在这种情况下,我们已更新了包缓存并安装了所需的软件;也许这些包自上次构建以来发生了变化,但如果没有,我们将拥有相同的镜像)。

构建的镜像具有一个独特的标识符,格式为algorithm:hexadecimal_code_using_algorithm。这意味着每次我们构建该镜像时,除非过程中进行了一些更改,否则我们将获得相同的镜像标识符。该镜像 ID 或摘要 ID 是通过与层的内容相关的算法计算得出的,因此任何层的更改都会获得新的 ID。这个标识符允许 Docker 引擎验证描述的镜像是否是正确的使用镜像。Docker 镜像包含关于所有层的信息,并通知 Docker 引擎所需的新容器的层内容。

当我们检查镜像信息时,我们将获得创建该镜像所需的所有必要层,RootFS。以下是一个示例:

"RootFS": {
 "Type": "layers",
 "Layers": [
 "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81",
 "sha256:402522b96a27c1af04af5650819febc11f71db14152b1db8e5eab1ae581fdb2e",
 "sha256:cf2850b10a1aba79774a291266262f1af49fac3db11341a5ca1a396430f17507",
 "sha256:c1912ec50df66e3e013851f6deb80f41810b284509eebc909811115a97a1fe01"
 ]
 }

该输出显示了使用 Dockerfile 中定义的代码创建的不同层。这些层将在可能的情况下在镜像之间互换。如果我们使用 Dockerfile 的前两行创建镜像,那么那些命令创建的层将与之前的镜像共享。这确保了最小的磁盘空间使用。

交互式创建镜像

镜像可以通过运行容器并实时修改rootfs来交互式创建。当应用程序的安装无法自动化且缺乏可复现性时,这非常有用。让我们通过一个示例来看看这个过程的实际操作:

  1. 启动一个交互式容器:
$ docker container run -ti debian
 Unable to find image 'debian:latest' locally
 latest: Pulling from library/debian
 4a56a430b2ba: Pull complete
 Digest: sha256:e25b64a9cf82c72080074d6b1bba7329cdd752d51574971fd37731ed164f3345
 Status: Downloaded newer image for debian:latest
 root@60265b7c8a61:/#
  1. 启动后,我们将收到命令提示符,因为我们通过分配伪终端并以交互模式启动容器。我们需要更新包的数据库,然后安装例如postfix包,它需要一些交互式配置(请注意,部分输出将被截断并省略):
root@60265b7c8a61:/# apt-get update -qq
 root@60265b7c8a61:/# apt-get install postfix
 Reading package lists... Done
 Building dependency tree
 Reading state information... Done
 The following additional packages will be installed:
 bzip2 cpio file libexpat1 libicu63 .....
 Suggested packages:
 bzip2-doc libarchive1 libsasl2-modules-gssapi-mit | libsasl2-modules-gssapi-heimdal  .....
 The following NEW packages will be installed:
 bzip2 cpio file libexpat1 libicu63 libmagic-mgc libmagic1 l ....
 0 upgraded, 29 newly installed, 0 to remove and 5 not upgraded.
 Need to get 19.0 MB of archives.
 After this operation, 76.4 MB of additional disk space will be used.
 Do you want to continue? [Y/n] y
 Get:1 http://cdn-fastly.deb.debian.org/debian buster/main amd64 libpython3.7-minimal amd64 3.7.3-2 [588 kB]
 .....
 .....
 debconf: falling back to frontend: Teletype
 Postfix Configuration
 ---------------------
Please select the mail server configuration type that best meets your needs.
No configuration:
 Should be chosen to leave the current configuration unchanged.
 Internet site:
 Mail is sent and received directly using SMTP.
 Internet with smarthost:
 Mail is received directly using SMTP or by running a utility such
 as fetchmail. Outgoing mail is sent using a smarthost.
 Satellite system:
 All mail is sent to another machine, called a 'smarthost', for delivery.
 Local only:
 The only delivered mail is the mail for local users. There is no network.

1\. No configuration 2\. Internet Site 3\. Internet with smarthost 4\. Satellite system 5\. Local only
 General type of mail configuration: 1
Unpacking postfix (3.4.5-1) ...
 ......
 ......
 Adding group `postfix' (GID 102) ...
 Done.
 Adding system user `postfix' (UID 101) ...
 Adding new user `postfix' (UID 101) with group `postfix' ...
 Not creating home directory `/var/spool/postfix'.
 Creating /etc/postfix/dynamicmaps.cf
 Adding group `postdrop' (GID 103) ...
 Done.
 /etc/aliases does not exist, creating it.
Postfix (main.cf) was not set up. Start with
 cp /usr/share/postfix/main.cf.debian /etc/postfix/main.cf
 . If you need to make changes, edit /etc/postfix/main.cf (and others) as
 needed. To view Postfix configuration values, see postconf(1).
After modifying main.cf, be sure to run 'service postfix reload'.
invoke-rc.d: could not determine current runlevel
 invoke-rc.d: policy-rc.d denied execution of start.
 Setting up libpython3-stdlib:amd64 (3.7.3-1) ...
 Setting up python3.7 (3.7.3-2) ...
 Setting up python3 (3.7.3-1) ...
......
......
 Processing triggers for libc-bin (2.28-10) ...
  1. 软件已安装,并且您被要求确认安装postfix包以及一些默认配置。现在,我们可以退出当前容器:
root@60265b7c8a61:/# exit
  1. 我们在这里所做的是退出当前的主进程(在 Debian 镜像中是一个 shell),结果返回到我们的主机。我们将查找在主机上执行的最后一个容器,然后将容器层保存为一个新的镜像层(这意味着我们创建了一个新的镜像,如果我们省略名称或标识的话):
$ docker container ls -l
CONTAINER ID IMAGE COMMAND LABELS
f11f8ad3b336 debian "bash"

$ docker container commit f11f8ad3b336 debian-with-postfix
sha256:a852d20d57c95bba38dc0bea942ccbe2c409d48685d8fc115827c1dcd5010aa6
  1. 最后,我们在主机系统上回顾新创建的镜像(在您的环境中,ID 可能会发生变化):
$ docker image ls
IMAGE ID REPOSITORY TAG CREATED AT
a852d20d57c9 debian-with-postfix latest 2019-10-05 13:18:45 +0200 CEST
c2c03a296d23 debian latest 2019-09-12 01:21:51 +0200 CEST

使用这种方法,我们通过与先前运行的 Debian Docker 容器进行交互,创建了一个新的镜像。如我们所见,新的镜像具有不同的摘要。如果我们检查其元信息,我们可以识别其前一个镜像层:

        "RootFS": { 
          "Type": "layers", 
          "Layers": [ 
             "sha256:78c1b9419976227e05be9d243b7fa583bea44a5258e52018b2af4cdfe23d148d", 
             "sha256:998e883275f6192039dd6eff96ece024e259cf74dd362c44c5eb9db9f3830aa0" 
           ]
        }

使用 Docker 容器提交创建的镜像有一个关键概念,那就是它们是不可重复的;你实际上不知道它们是如何创建的,因此必要的步骤应当记录下来,以便于更新和管理。

有一个镜像操作,它提供了创建镜像的详细步骤回顾。docker image history 将提供一个历史视图,展示创建该镜像时所采取的步骤。然而,它无法作用于使用提交的容器创建的镜像。我们只会看到一行包含 bash 的内容,例如,指示所有操作都发生在一个活动容器上,因此无法提取额外的信息。例如,使用之前创建的镜像,执行 docker image history debian-with-postfix 将提供以下输出:

从零开始创建镜像

从零开始创建镜像是最有效的方法。在这种情况下,我们将使用一个 Dockerfile,如第一种方法所述,但初始的基础镜像将是一个空的保留镜像,字面上称为 scratch。一个简单的示例定义如下所示:

FROM scratch
ADD hello /
CMD ["/hello"]

Dockerfile 定义中的主要区别是 FROM 行,因为我们使用了一个名为 scratch 的空白镜像。scratch 并不是真正的镜像;它仅包含根文件系统结构及其元信息。使用这种方法构建的镜像必须包含我们进程所需的所有二进制文件、库文件和其他文件(这应当是始终如此)。然而,我们并没有使用预定义的镜像及其内容;它将是空的,我们必须添加每个所需的文件。这个过程并不容易,且需要更多的实践,但镜像会更加精简,因为它们只包含我们应用所需的部分。我们将在本章末尾看到一个完整的实验。

理解写时复制文件系统

在上一章中,我们了解了什么是容器。容器内运行的隔离进程将拥有自己的根文件系统以及其他命名空间。容器在镜像层之上添加了一层薄层,每个进程执行期间所做的更改将仅存储在这一层。为了管理这些更改,Docker 存储驱动程序将使用可堆叠的层和写时复制(有时简称为 CoW)。

当容器内部的进程需要修改文件时,Docker 守护进程的存储文件系统机制会将该文件从底层复制到最上层。这些文件仅对容器可用。当创建新文件时也会发生相同的情况;新文件将只写入容器的最上层存储。其他容器中运行的进程会管理它们各自版本的文件。如果没有做任何更改,这些文件实际上就是来自其他层的原始文件。每个容器使用自己的最上层来写入文件更改。

我们已经看到了使用容器构建镜像过程的工作原理,每一层的创建过程。我们了解到,我们可以提交容器的层来获得一个新的镜像。使用 Dockerfile 创建镜像时,会运行使用先前镜像的中间容器,并将其提交以获得一个包含其各层之间所有文件更改的中间镜像。这个过程将按顺序执行,遵循 Dockerfile 代码中定义的顺序。最终,创建的镜像将是一个包含层间差异的薄层合集。

Docker 的写时复制减少了运行容器所需的空间和启动所需的时间,因为它只需要为每个容器创建这个可写层:

该镜像表示一个作为容器运行的 NGINX 进程。基础镜像是从一个全新的 alpine 3.5 镜像创建的。我们添加了一些软件包,进行了配置,并复制了我们自己的 nginx.conf 文件。最后,我们添加了一些元信息,以便使用此镜像创建容器,声明了将用于暴露 NGINX 的端口,并声明了默认用于运行容器的命令行,从而在前台启动 NGINX。

CoW 逻辑有三种策略:

  • 在基于 AUFS 和 overlay 的驱动程序中,Docker 使用联合文件系统(union filesystem)。

  • 在 BTFS 和 ZFS 驱动程序中,Docker 使用文件系统快照。

  • 在 device-mapper(适用于类似 Red Hat 的操作系统)中,Docker 使用 LVM 快照来管理块。

现在,几乎所有 Docker 主机操作系统都默认使用基于 overlay 的驱动程序(尽可能使用)。过去有些实现使用块设备,但这些现在已经被弃用。CoW 过程增加的开销取决于所使用的驱动程序。

我们可以查看一个容器使用了多少空间。Docker 提供了 docker container ls -s/--size 选项来实现这一点。它将返回当前薄层的使用空间以及从原始镜像中使用的只读数据,定义为虚拟。为了了解容器实际消耗了多少空间,我们需要将每个容器的两种大小结合起来,得到所有容器在我们环境中使用的总数据量。这不包括卷或容器的日志文件等小部分,这些都会贡献到实际的使用空间中。

CoW 是为最大化磁盘空间效率而设计的,但它取决于本地镜像中有多少层是共享的,以及有多少容器会使用相同的镜像。如你所料,向其可写层写入大量数据的容器会消耗比其他容器更多的空间。

CoW(写时复制)是一个非常快速的过程,但对于容器上的重写操作,它并不够。如果我们有一个需要创建许多小文件、非常深的目录结构,或者只是非常大的文件的进程,我们需要绕过 CoW 操作,因为性能会受到影响。这将导致我们使用卷(volumes)来缓解这种情况。我们将在第四章《容器持久性与网络》中学习卷,它们是用于容器持久存储的对象。

使用 Dockerfile 构建镜像参考

正如我们之前提到的,构建镜像很容易,但构建优秀的镜像并非如此。本节将带你了解基本知识,并提供一些技巧,帮助你在使用 Dockerfile 时改进镜像构建过程。

Dockerfile 快速参考

我们已经了解了构建镜像时可用的方法。对于生产环境,建议使用 Dockerfile,因为这种方法提供了可重现性,并且我们可以使用代码版本控制方法。我们将在此介绍 Dockerfile 的主要指令,并按照其标准使用顺序进行讲解:

指令 描述和使用方法
FROM 该指令设置基础镜像并初始化一个新的构建(我们将在本章后面的多阶段构建和镜像缓存部分回顾这个概念)。它是所有 Dockerfile 开头的唯一必选指令。我们可以使用任何有效的镜像作为基础镜像进行构建,或者使用保留的 scratch 词从一个空的根文件系统开始,正如我们在上一节中所学。我们可以在同一个 FROM 指令中使用 AS name 定义构建阶段的名称。我们将在本章最后的多阶段构建部分中使用它。基础镜像可以通过其镜像名称(仓库)和特定标签(镜像版本)或其摘要(digest)来定义,例如,FROM <image>[:tag] 或 FROM <image>[@digest]
ARG ARG指令定义了一个变量,在构建时会将其设置为提供的值,通过--build-arg <variable>=<value>将其作为参数传递。为了避免在构建过程中缺少值导致的问题,我们可以使用ARG定义一个默认值,该值将在传递参数时被覆盖。每次调用时,ARG都会获取该值。这在创建 Dockerfile 时非常重要。ARG可以在FROM指令之前使用,通过参数指定不同的基础镜像。

| LABEL | 使用LABEL,我们可以为镜像添加元信息。这些信息应以键值对的格式呈现,并且我们可以在同一个LABEL语句中包含多个键值对。下面是一些简短的示例:

LABEL version="1.0"
LABEL description="This image has these \
and these properties...."
LABEL maintainer="Javier Ramirez" team="Docker Infrastructures"  environment="preproduction"

|

| ENV | 使用ENV指令,我们可以为下一步以及之后的所有步骤设置一个环境变量。我们可以在同一个语句中添加多个环境变量,如果在创建 Docker 容器时指定了新值,原来的值将被覆盖:

ENV DATABASE_NAME=TEST

|

| WORKDIR | WORKDIR设置接下来及之后语句的工作目录。我们可以指定完整路径或相对路径:

WORKDIR /myappcode

|

| RUN | RUN可能是你在 Dockerfile 中最常用的指令之一。它会在一个新的层中执行行中的所有命令,并将结果提交到一个新的层(如我们在前一章节中所描述的)。这个新层将在下一条语句中作为基础层使用,并包含RUN指令所做的更改。这意味着每条RUN指令都会创建一个新的层。因此,RUN直接影响镜像中层的数量。为了避免使用比需要更多的层,我们通常会在每个RUN语句中添加多个命令:

RUN apt-get update -qq \
​&& apt-get install curl

|

| COPY | COPY指令将新的文件和目录从构建上下文(在构建执行过程中设置)复制到容器文件系统的指定目录中(记住,构建镜像是基于在容器上执行命令,并将结果提交为镜像,用于后续的阶段)。COPY支持--chown=<user>:<group>参数,用于为 Linux 容器提供文件所有权。如果不使用该参数,所有者将默认为root:rootCOPY还支持--from=<name or index>,用于从其他构建阶段复制文件或目录(当使用多阶段构建时,这一点至关重要,我们将在本章后面学习):

COPY mycode/* /myapp

|

| ADD | ADD类似于COPY,但也可以与 URL 和 TAR 包文件一起使用。它接受相同的所有权参数,以更改目标文件和目录的权限:

ADD http://example.com/bigpackagefile.tar.gz /myapp

|

| USER | USER 指令用于指定在接下来的语句中使用的用户及其所属组。理解我们进程所需的权限并使用 USER 指定用户及其组是非常重要的。如果没有指定,步骤将使用 root:root,并且容器内的进程将以 root 用户身份运行。在生产环境中,应强制使用特定的非 root 用户运行容器进程,如果需要 root 用户,则应使用用户映射(如前一章所述):

USER www-data:www-data

|

| VOLUME | VOLUME 定义将创建一个挂载点,用以绕过 CoW 系统。这意味着该目录的内容将不属于容器生命周期的一部分。由于它在容器外部,因此对该目录的任何后续更改都会被丢弃。因此,如果我们希望在初始化卷时提供某些文件,VOLUME 语句应位于我们已将文件放入目录之后:

VOLUME /mydata  

|

| EXPOSE | EXPOSE 用于通知 Docker 守护进程有关通过此镜像创建的容器的监听端口。这并不意味着定义的端口在 Docker 主机级别监听,它们只会在容器的网络内部监听。我们可以定义使用哪种传输协议——UDP 或 TCP(默认为 TCP):

EXPOSE 80/tcp 

|

| CMD | CMD 指令定义了执行基于该镜像的容器时的默认进程或参数。无论是否定义了 ENTRYPOINT 指令,这一行为都会被应用。默认情况下,根据使用的格式,CMD 将为 shell 提供默认的参数,shell 是默认的入口点(容器内进程的主要执行器):

CMD ["/usr/bin/curl","--help"]
CMD /usr/bin/curl -I https://www.packtpub.com

|

ENTRYPOINT ENTRYPOINT 指令将设置容器运行的命令作为可执行文件。如前所述,CMD 将是该命令的参数。CMDENTRYPOINT 之间的交互定义了在运行容器时执行的命令。它们不是必需的,但最好至少定义 CMD,以便在执行时具有默认的进程启动命令。

| HEALTHCHECK | HEALTHCHECK 定义了一个命令行,该命令将在容器内运行以验证进程或进程的健康状况。如果没有 HEALTHCHECK,Docker 守护进程将只验证主进程是否存活,如果主进程不存在,容器将退出。HEALTHCHECK 指令通过定义更好的脚本或基于二进制的进程状态监控,帮助我们改善应用程序的健康状况。我们可以调整检查之间的间隔、超时设置以及在声明为不健康状态之前的重试次数。如果我们有一个启动需要时间的进程,我们可以设置开始监控容器进程健康状况的时间:

HEALTHCHECK \
--interval=DURATION (default: 30s) \
--timeout=DURATION (default: 30s) \
--start-period=DURATION (default: 0s) \
--retries=N (default: 3) \
CMD /bin/myverificationscript 

非常重要的一点是,默认情况下,如果容器内运行的主进程没有按预期工作(进程仍在运行,但健康检查失败),在进行三次失败验证之前,它不会被标记为不健康,默认情况下每次验证之间会有 30 秒的间隔。这意味着,默认情况下,一个进程可能会失败 90 秒,容器才会被标记为不健康。这在许多情况下过长,你应该采取措施来改变这种行为。我们可以在容器内使用自己的脚本,只需要管理两种不同的退出状态(0 – 验证通过;1 – 验证失败)。

如果你在 Dockerfile 中多次定义了原始键,请小心。这些文件是从上到下读取的,定义的优先级很重要,因为某些指令的值会被覆盖(ARGENVCMDENTRYPOINTLABELUSERWORKDIRHEALTHCHECK 等),而其他指令的值会被添加(VOLUMEEXPOSE 等)。

有些指令支持两种不同的格式,shellexec,它们在每种情况下有不同的行为:

  • RUN:当使用 shell 形式时,所有命令都将在 Shell 中启动,就像我们使用 /bin/sh -c(默认在 Linux 上)或 cmd /Scmd /C(默认在 Windows 上)一样。我们可以通过 SHELL 指令来更改在该形式中使用的 Shell。
RUN <command> <argument1> <argument2> <-- shell form
RUN ["executable", "argument1", "argument2"] <-- exec JSON form

我们需要在 Windows 容器中使用 exec 格式。此格式在这种情况下是必要的,因为某些键的定义值,如目录路径,将包含反斜杠(\),并且必须避免使用。

  • CMD:此键用于定义传递给主容器进程的命令或参数:
CMD <command> <argument1> <argument2> <-- shell form
CMD ["executable or argument0", "argument1", "argument2"] <-- exec JSON form

正如我们之前所学,Shell 形式会使用 Shell 执行命令(可以通过设置 SHELL 键来更改使用的 Shell)。

为了在没有 Shell 的情况下执行 CMD 命令,我们必须使用 exec 形式。如果我们想将 CMD 值作为已定义入口点的参数使用,也将使用 exec 形式,但必须在 ENTRYPOINT 和 CMD 定义中都使用该形式。

  • ENTRYPOINT:此键用于定义在创建的容器内执行的主进程:
ENTRYPOINT <command> <argument1> <argument2> <-- shell form
ENTRYPOINT ["executable", "argument1", "argument2"] <-- exec JSON form

在 Shell 形式中预期会有相同的行为,但在这种情况下,使用此形式将不允许使用 CMD 值作为参数。因为 ENTRYPOINT 使用 Shell 形式时,会使用 /bin/sh -c 启动主进程,而在这种情况下,它将不会拥有 PID 1,并且无法直接接收 Unix 信号(我们将在第三章《运行 Docker 容器》中回顾 Unix 信号如何与容器进程交互)。

记住,为了将 CMD 值作为 ENTRYPOINT 的参数使用,ENTRYPOINT 必须以 exec 形式定义。

当我们使用基础镜像创建新的镜像时,基础镜像定义的值会被新镜像继承。这意味着,除非我们覆盖它们,否则 CMD 和 ENTRYPOINT 的定义将被使用,从而为我们的镜像设置新值。然而,有一个例外;如果我们在新镜像中设置了新的 ENTRYPOINT,CMD 会被重置为空值。

构建过程操作

Docker 命令行提供了用于管理 Docker 对象的操作,正如我们在上一章中学习的那样。镜像是 Docker 对象,命令行将提供构建和操作它们的工具。

我鼓励使用 docker image build 而不是常用的 docker build。正如你可能已经注意到的,docker image build 遵循 对象动作 模式,这种方式更容易记住。

我们可以在不同类别中查看 Docker 镜像操作:

  • 用于管理lsprunermtag。这些操作允许我们列出、移除和为镜像设置标识。

  • 获取信息historyinspect。这些操作提供了有关创建该镜像所需步骤的信息,以及其所有属性。

  • 在主机之间共享镜像pullpushloadimportsave。这些操作允许我们与注册表交互,以下载和上传镜像层,并通过不同方式导入和导出镜像到不同的 Docker 主机。

  • 创建新镜像build。通过 build 操作,我们将能够使用基础镜像或从空根文件系统开始创建新镜像。

    因此,我们将使用 Docker 镜像构建来创建新镜像。这里有一些非常重要的选项会改变构建行为,这些内容必须在 Docker Certified Associate 考试中进行复习。

我们将使用 docker image build [options] <context> 并带上一些额外的选项:

  • --add-host:此选项允许我们在镜像中包含主机到 IP 的条目。它在添加非 DNS 条目或遮蔽外部资源时非常有用,例如。

  • --build-arg:在构建新镜像时使用参数是持续集成流水线中结合模板化 Dockerfile 的标准做法。

在集群环境中,我们需要指定哪些节点应构建所需的镜像。为了确保镜像在特定节点上构建,我们将使用某些标签作为约束,将它们作为参数指定;例如,在一个同时拥有 Windows 和 Linux 节点的集群中,使用 --build-arg constraint:ostype==linux 会将构建过程仅发送到 Linux 节点。

  • --file-f:我们可以定义使用哪个 Dockerfile。我们可以为每个环境、架构等定义不同的文件,但如今有其他功能,例如“目标定义”,允许我们为不同的目的使用一个唯一的 Dockerfile,并根据需要构建每一个。

  • --force-rm:此选项将保持环境的清洁,因为它会删除所有中间容器。默认情况下,只有在构建成功后,中间容器才会被删除。

  • --isolation:在构建 Windows 镜像时,此选项是必需的,因为我们需要选择使用的隔离模式。

  • --label:此选项允许我们以键值对的格式添加元信息。

  • --no-cache:默认情况下,Docker 守护进程在构建新镜像时会使用主机缓存的层。有些情况下我们需要创建一个全新的镜像,例如当有新软件包更新时。在这些情况下,我们会使用此选项避免使用之前构建的层。使用此选项时需要注意时间和开销,因为禁用缓存会增加构建时间,并且我们需要执行所有步骤来生成新镜像。

  • --tag-t:为镜像打标签是必须的。默认情况下,Docker 不会“命名”镜像,我们只能通过其 IMAGE ID 来引用镜像(我们在本章前面已经学习过这个内容)。指定一个仓库名称(我们将在后续章节中学习什么是仓库;目前,只需理解它是一个简单的名称)及其版本对镜像管理非常重要。我们可以在构建时使用多个 --tag-t 参数,带上镜像名称和标签。我们还会学习到,镜像名称也称为仓库名称,并且在不使用 Docker Hub 时,我们需要添加自己的注册表(包括非标准端口)、用户名和所属的团队或组织作为前缀。

IMAGE ID 是唯一的。每个创建的镜像都有一个唯一的 ID 来标识这个由多层组成的镜像,且在所有系统中都是唯一的。但我们可以为该 IMAGE ID 添加标签以便于管理。一个镜像只有一个唯一的标识符,但可以有多个名称和版本。这个概念非常重要,它是确保在生产环境中执行正确镜像的关键。

  • --target:我们可以在同一个 Dockerfile 中定义多个构建阶段。这些定义允许我们在不同的结果镜像之间执行多阶段构建,例如使用已编译的二进制文件,但它们也允许我们定义多个架构或环境并选择要构建的一个,而不必使用不同的 Dockerfile 文件。

我们可以使用诸如 --cpu-quota--cpu-shares--memory 等选项限制构建过程中使用的资源,这将限制每个容器在构建过程中可用的资源数量。

构建上下文是位于某个目录或 URL(如 Git URL 或 tarball 文件)中的一组文件,我们在构建时使用它来引用这些文件。这些文件会被发送到 Docker 守护进程,供其在构建镜像时使用或不使用。因此,了解构建过程中实际需要哪些文件非常重要。如果我们在构建上下文中有很多小文件或非常大的文件,Docker 守护进程将会获取这些文件,并根据 Dockerfile 指令决定是否将其纳入镜像中。因此,构建上下文目录应仅包含镜像所需的文件。那些在镜像构建过程中不应由 Docker 管理的文件,应该不放在构建上下文中。

无论你使用的是 Git URL 还是 tarball 文件,行为都是类似的。Docker 守护进程将获取该仓库或 .tar 文件,并解压或解压缩数据,以便将临时目录作为构建上下文。

我们通常将 Dockerfile 存储在应用代码中,因此构建上下文就是 Dockerfile 所在的目录。因此,如果我们从相同目录启动构建,我们会使用 . 来表示当前目录。

一个简单的命令行示例,展示了带有多个选项的镜像构建如下:

$ docker image build [-t MY_TAG] [--label MY_LABEL=VALUE] [--file MY_DOCKERFILE] [BUILD_CONTEXT]

Docker 守护进程将尝试查找名为 Dockerfile 的文件以编写构建脚本。如果你没有使用标准名称,可以使用 --file-f,并指定文件位置(我们可以使用 Dockerfile 的完整路径或相对路径,但需要注意构建上下文的位置与其相对关系)。

并且,在替换了一些实际值后,我们将得到类似于以下内容的结果(这行内容来自本章末尾的一个实验):

$ docker build --file Dockerfile.application -t templated:production --build-arg ENVIRONMENT=production .

在这里,我们使用了一个非标准的 Dockerfile 名称,使用 ENVIRONMENT 变量并在构建过程中设置 production 值,创建一个名为 templated:production 的镜像,并使用当前目录作为构建上下文。注意命令末尾的 .。这意味着我们使用当前目录作为构建上下文来创建镜像。如果我们从上级目录运行此命令,则会使用包含所需 Dockerfile 的目录作为构建上下文。

使用相同的 Git 仓库哲学,如果有一些文件我们希望存储在 Docker 构建上下文中(例如,随 Git 仓库数据一起提供的文件),但不希望它们在构建过程中被处理,我们可以使用 .dockerignore 文件将其排除。只需在 .gitignore 中写下不需要的文件名,Docker 守护进程就不会在镜像构建过程中处理它们。

镜像标签和元信息

通常,你不会只管理少量的镜像,而是可能管理数百个或数千个镜像,因此尽可能多地了解它们的信息非常重要。

使用标签,我们将能够按环境搜索特定的镜像,如下所示:

$ docker image ls --filter label=environment
 REPOSITORY   TAG       IMAGE ID         CREATED             SIZE
 myapp        1.0      7dad160a2b02    4 seconds ago        5.6MB
 myapp       latest    285c3d16e672    7 minutes ago        5.6MB

$ docker image ls --filter label=environment=test              
 REPOSITORY      TAG       IMAGE ID       CREATED         SIZE
 myapp          latest    285c3d16e672   7 minutes ago    5.6MB

$ docker image ls --filter label=environment=production
 REPOSITORY      TAG        IMAGE ID      CREATED         SIZE
 myapp           1.0      7dad160a2b02  18 seconds ago    5.6MB

$ docker image inspect myapp:1.0 --format "{{ index .Config.Labels }}"
 map[environment:production]

请记住,一个镜像可以有多个名称和标签,但其摘要是唯一的。使用不同的标签和名称对于与不同的 CI/CD 工作流阶段交互非常有用,使用相同的镜像内容。例如,开发人员在开发和测试过程中会创建许多镜像,但只有少数会进入质量保障或认证阶段。我们可以基于 Docker 企业版中的镜像名称和标签自动化这些过程,正如我们将在第十三章中学习的,使用 DTR 实现企业级注册中心

我们已经学过,同一张图片可以有多个名称,因此通过名称删除某个镜像并不会真正删除其内容,如果它仍然被其他名称使用。如果我们使用镜像 ID 来删除 docker image rm <imageid>,Docker 守护进程会提示我们多个使用相同层的不同名称的镜像,并且除非我们使用 --force,否则它不会删除该镜像,在这种情况下,它将删除该镜像及其所有层和引用的名称。

我们可以使用 docker rmi 作为 docker image rm 的命令别名。另一方面,docker image prune 将用于删除悬空镜像。

有一些特殊的未标记镜像,它们会出现在我们创建新镜像时的 Docker 构建主机上。这些镜像是不同编译之间变更的结果。它们没有被引用且未被使用。在你的主机中,当它们作为层使用时,其他镜像不会使用它们,因此可以从系统中删除(事实上,你应该删除它们,因为它们占用了宝贵的磁盘空间)。这些镜像通常被称为悬空镜像,我们将在本章稍后学习如何清理它们。

要给镜像添加新标签,我们将使用 Docker 镜像标签,SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]。默认情况下,如果我们省略标签,系统将使用 latest 标签。避免使用 latest 标签,因为这并不真正表明这是最新构建的镜像。确保镜像构建时间的唯一方法是查看其创建日期。

Docker 注册表和仓库

镜像必须存储在某个地方。默认情况下,每个 Docker 主机会将其数据存储在 Linux 系统中的 /var/lib/docker/image 和 Windows 系统中的 c:\programdata\docker\image 目录下。但这些目录仅在本地有效,我们通常需要使用镜像来构建新的镜像并在多个节点之间共享。

我们可以使用 Docker 命令行在不同主机之间导出和导入镜像层,但这很难维护,而且这种方法无法扩展。Docker Registry 是一个服务器应用程序,用于存储并根据需要让我们下载和上传镜像。它提供一个 API,用于通过 Docker 客户端共享信息和镜像层。因此,我们可以将注册表定义为容器镜像的存储和内容分发系统。镜像将使用在 Docker 守护进程级别定义的设置本地存储。要使用远程注册表,我们将设置不同的存储后端,可以处理云环境中的 S3、Microsoft Azure、OpenStack Swift,以及本地数据中心中的 NFS。

在本章的最后,我们将进行一个实验室实践,创建一个本地注册表。Docker Registry 是一个开源解决方案,可以通过 /etc/docker/registry/config.yml 配置文件进行配置,以更改存储后端、端口和其他高级设置。

Docker Hub 是 Docker 提供的基于云的注册表。我们可以使用它来存储公有或私有镜像,并且作为一种软件即服务的解决方案,有些功能需要付费订阅。

Docker Registry 不提供任何身份验证方法,也不支持 TLS,以允许 Docker 客户端使用加密连接。这些安全增强功能仅在 Docker Hub(Docker 公有/私有镜像注册表作为服务)和 Docker Trusted Registry(部署在 Docker 企业平台上的注册表)中可用。

我们通常描述三种不同的镜像命名空间或命名约定:

  • 根目录(docker.io 托管镜像):我们使用镜像的名称和标签来引用这些镜像,例如 nginx:alpinepostgres:12.0。它们是公开的。

  • 根目录下的用户或组织镜像(docker.io 托管镜像):在这种情况下,镜像可以是私有的或公开的,具体取决于用户许可。镜像名称将包含用户名或组织名,用户可以在其中拉取或推送镜像,例如 frjaraur/simplest-demo:simplestappcodegazers/colors:1.13**。

  • 完整注册表格式(用于云端或自有数据中心的私有注册表):我们将使用用户名、团队或组织,但需要使用注册表的完全限定名称;例如,dtr.myorganization.com[:my_registry_port][/myteam 或 /myorganization][/myusername]/<repository>[:tag]

实际上,根注册表和仓库名称可以使用完整的注册表格式来填写;例如,我们可以使用完整的名称约定来拉取 docker.io/codegazers/colors:1.13 镜像。

你应该已经注意到,在这个例子中,我们添加了 my_registry_portrepository。我们添加了 my_registry_port,因为默认情况下,Docker Hub 和 Docker Trusted Registry 使用 HTTPS,因此端口是 443,但我们可以使用自定义端口部署我们自己的注册中心。repository 是对具有相同名称的镜像的引用,每个镜像都有不同的 IMAGE ID(唯一)和标签(多个)。因此,当我们谈论 nginx:alpine 镜像时,我们是指 docker.io 注册中心、nginx 仓库和 alpine 标签,所有其他镜像在本章中使用时也应遵循相同的规则。

镜像安全

如前一章所见,Docker 容器默认是安全的,但这是因为它们在命名空间和 cgroups 隔离中运行。镜像是不同的对象,它们的安全性与其内容有关。基于这个概念,很容易理解,内容越少,安全性就越高。因此,确保镜像安全的主要规则如下:

  • 镜像应仅包含运行我们容器化进程所需的必要二进制文件、库和配置。不要在生产镜像中添加任何不必要的应用程序或调试工具。减少攻击面更为重要,而过多的二进制文件会增加这个面。

  • 始终声明镜像上的资源。在这里,我们使用“资源”一词来描述用户、暴露的端口和卷。始终描述运行镜像所需的内容,并避免在容器内使用 root 权限。

  • 如果有安全漏洞修复,更新镜像内容包,重建所有衍生镜像,并重新部署容器。实际上,还有更多步骤需要遵循,但这一点是正确的。如果你发现任何可能导致安全问题的漏洞或错误,必须尽快使用 Docker 容器的生命周期进行修复。要通过更新构建修复基础镜像并重建所有衍生镜像,请遵循 CI/CD 工作流,并在将这些新版本的镜像部署到生产环境之前,重新通过所有测试。

在 Docker 企业版部分,我们将学习 Docker 镜像安全扫描,这是一种自动化工具,用于验证镜像的所有内容与 CVE 数据库的匹配情况。如果发现漏洞或漏洞利用,我们可以将镜像标记为不安全。该工具可以触发新的事件来实现安全的管道,在镜像进入生产阶段前扫描所有镜像,并提供关于发现的安全风险的信息。

我们知道镜像层对于容器是只读的,每个容器都会创建一个新的可写层。在下一章,我们将学习如何通过使用只读根文件系统来改进这种情况,只允许对外部卷进行写访问。

管理镜像及其他相关对象

我们在本章中学习了如何管理镜像容器,现在,让我们看看最常见的镜像管理任务。

如果从一开始没有处理好悬空镜像,它们可能会变成一场噩梦。正如我们在前面的章节中提到的,悬空镜像是指那些未被引用、因此不被任何其他镜像使用的镜像。实际上,它们是连续构建的结果,因为 Docker 使用它们进行缓存和提高构建性能。它们只是构建镜像过程中使用的层,在特定步骤中由于我们更改了某个包、更新了代码、更改了配置文件等原因,这些镜像不再被使用,因此它们不再必要。我们应该删除这些镜像,因为它们可能占用大量磁盘空间。

自 1.13 版本以来,Docker 提供了 Docker 镜像清理操作,默认情况下会删除所有悬空镜像。然而,我们可以选择删除我们希望删除的镜像,例如,可以按日期或标签进行过滤:

$ docker image prune --force --filter label=environment=test 
Deleted Images:
deleted: sha256:285c3d16e6721700724848024b9258b05a0c8cd75ab9bd4330d9d48f3313ff28
deleted: sha256:62ee8a779b918d678f139941d19e33eeecc8e333a1c00d120c3b83b8545a6650
deleted: sha256:fb077551608a1c7244c4ed5f88e6ba301b6be2b7db7dd2a4f7194e03db6e18dd
deleted: sha256:9a704cd7c7c2a5233fad31df5f7186a9cf631b9b22bc89bc4d32d7ab0a1bc4a7

Total reclaimed space: 19.83kB

该命令行已删除所有环境标签为 test 的悬空镜像。

Docker 镜像清理不仅会删除悬空镜像,还会删除旧镜像。然而,您应该管理这种情况,因为它取决于您在环境中运行了哪些容器。在生产环境中删除非悬空镜像之前,请确认没有容器在使用该镜像;例如,docker container ls --filter ancestor=<image_to_be_removed>

许多容器用于构建操作。默认情况下,Docker 守护进程会删除所有在构建过程中正确退出的容器,因此所有在正确构建过程中使用的容器将被移除。然而,曾在构建失败时使用的容器应手动删除。识别与镜像构建相关的故障容器非常简单。通常,我们会在其他情况下手动启动的所有容器中设置容器名称。在第二部分容器编排,专门讲解编排时,我们将学习 Kubernetes 和 Swarm 编排器在创建容器时使用的命名模式,从而帮助我们识别它们的来源。

查看 Docker 主机文件系统使用情况总是很有用,特别是 Docker 守护进程使用的空间。我们可以使用 docker system df --verbose 获取关于每个主机的镜像、容器和卷使用情况的详细信息。

其他常见任务包括检查镜像,以了解每种情况下所需的资源并共享它们。

列出镜像

列出镜像是审查主机系统内容的常见任务。我们可以通过在 GoLang 格式结构中使用 --format 修饰符来修改默认的 docker image ls 命令输出:

$ docker image ls --format "table {{.ID}}\\t{{.Repository}}:{{.Tag}}\\t{{.CreatedAt}}"
IMAGE ID            REPOSITORY:TAG               CREATED AT
 28b4509cdae8        debian-with-postfix:latest   2019-10-05 13:18:45 +0200 CEST
 c2c03a296d23        debian:latest                2019-09-12 01:21:51 +0200 CEST
 d87c83ec7a66        nginx:alpine                 2019-08-28 00:20:07 +0200 CEST

正如我们在前面的示例中学到的,我们可以使用标签过滤输出,例如,只显示特定的镜像。

使用注册表共享镜像

我们了解到,注册表是可以存储镜像的服务器,使用 HTTP REST API。Docker 客户端知道如何管理所需的请求,从而简化了在这些位置的镜像管理。

执行容器时始终需要镜像。因此,每次我们运行一个新的容器时,如果该镜像不在 Docker 主机上,它将从仓库中下载。

我们可以使用 docker image pull <IMAGE:TAG> 手动下载镜像。这将下载所有镜像层,并且我们准备好基于此镜像启动新的容器。这对于在启动容器之前预热主机非常有用;例如,想象一个 2 GB 的镜像需要从互联网下载。

我们可以使用 --all-tags 从仓库中下载所有镜像;例如,docker image pull --all-tags --quiet codegazers/colors。使用这个命令行,我们正在下载 codegazers/colors 仓库中所有可用的镜像(所有标签),且不会输出任何信息。

因此,我们将使用 Docker push 将镜像上传到仓库。但请记住使用完整的名称,包括注册表的完全限定域名和端口(如果我们没有使用 docker.io 和默认的 443 端口)。我们将使用自定义仓库的完整路径 – myregistry.com[:non-default-port]/myusername/myrepository[:tag];例如,$ docker push docker.io/codegazers/colors:test

Docker 注册表需要登录才能访问,无论是拉取还是推送。通常,我们将使用 TLS 加密连接到注册表,且在 Docker 客户端中默认启用。Docker 引擎需要信任注册表证书才能允许登录和拉取或推送镜像。如果您不想使用此功能,您需要在 /etc/docker/daemon.json 中将注册表添加为不安全注册表,并重启 Docker Daemon。

还有其他共享镜像的方法。我们可以使用 docker image save 保存一个镜像及其所有层和元数据信息。此命令默认将内容流式传输到标准输出,因此我们通常使用 --output 将所有内容存储到一个文件中(或将其输出重定向到文件):

$ docker image save docker.io/codegazers/colors:test -o /tmp/codegazers_colors_test.tar

$ file /tmp/codegazers_colors_test.tar
 /tmp/codegazers_colors_test.tar: POSIX tar archive

$ tar -tf /tmp/codegazers_colors_test.tar
.........
.........
d420450ab5b04122577e05172291941dcd735eaefd01ab61c64c056b148ebfde/layer.tar
 f99211cb5c4f5e30e2c5d6ce0f0f2ac42361aecbdcc77fd0e2eccf1650558a0c/
 f99211cb5c4f5e30e2c5d6ce0f0f2ac42361aecbdcc77fd0e2eccf1650558a0c/VERSION
 f99211cb5c4f5e30e2c5d6ce0f0f2ac42361aecbdcc77fd0e2eccf1650558a0c/json
 f99211cb5c4f5e30e2c5d6ce0f0f2ac42361aecbdcc77fd0e2eccf1650558a0c/layer.tar
 manifest.json
 repositories

因此,docker image save 将创建一个 .tar 文件,包含所有层及其所有文件,并包含重建该镜像所需的清单文件(以及其他元数据信息)以便在主机中重建该镜像。请注意,我们可以选择文件名及其扩展名(.tar 默认不会添加,但这不会影响内容的上传)。

上传这个 .tar 镜像文件很简单。我们有两个选项。

第一个选项是使用 docker image import。通过此操作,我们将只导入镜像层,而没有任何元数据信息,因此我们将无法定义入口点、命令参数、暴露的端口、卷定义等。它只会将镜像提供的层导入到我们的主机中。

因此,我们无法直接使用此镜像运行容器(但我们可以在导入时添加类似 Dockerfile 的指令以避免这种情况):

$ docker import /tmp/codegazers_colors_test.tar 
 sha256:5bd30fec31de659bbfb6e3a294e826ada0474817f4c4163dd8a62027b627c81d 

$ docker image ls 
 REPOSITORY            TAG                   IMAGE ID            CREATED             SIZE 
 <none>                <none>                5bd30fec31de        4 seconds ago       77MB

$ docker inspect codegazers/colors:test --format '{{json .Config.ExposedPorts }}'
 {"3000/tcp":{}}

$ docker inspect 5bd30fec31de --format '{{json .Config.ExposedPorts }}'
 null

我们可以使用docker image load上传保存的镜像,连同所有其层和启动容器所需的信息。这是一个直接步骤,没有任何修改,我们可以直接使用加载后的镜像。此命令默认使用标准输入读取内容,但我们也可以通过添加--input参数或直接使用重定向,来使用.tar文件:

$ docker image rm codegazers/colors:test
Untagged: codegazers/colors:test

$ docker image load </tmp/codegazers_colors_test.tar  
 Loaded image: codegazers/colors:test $ docker inspect codegazers/colors:test --format '{{json .Config.ExposedPorts }}'
 {"3000/tcp":{}}

正如你所注意到的,我们没有使用任何名称,因为它是从镜像.tar文件的元信息中获取的。

使用docker image save在原主机上保存镜像,并在目标主机上通过 Docker 的导入/加载功能,我们可以避免使用外部存储。但随着平台上镜像和主机数量的增加,这种方法已不再足够,我们应该使用注册表来管理镜像共享。

多阶段构建与镜像缓存

多阶段构建是 Docker 17.05 版本引入的一项功能。在此版本之前,如果我们想要确保最小的镜像大小并避免在最终生产镜像中使用编译器,我们通常需要安装编译所需的包,执行二进制文件的构建,然后删除所有不需要的软件,包括使用过的编译器,而这些编译器在生产环境中是一个真实的安全隐患。

自动化这种编译过程并不容易,有时我们需要创建自己的脚本,以便在每次构建时重新执行这些步骤,通常使用第三方的 CI/CD 协调工具。

我们可以在 Dockerfile 中使用多个构建定义来创建小巧且不包含编译器的镜像。这些镜像只会包括应用程序库、可执行文件和配置。所有的编译步骤将在另一个镜像上完成,我们只需将结果文件包含到新镜像中。在此过程中,我们还可以使用外部镜像。我们将仅把应用所需的文件复制到新的镜像中。这被称为多阶段构建。

让我们看一个例子,帮助我们理解这个新过程:

FROM alpine AS sdk
RUN apk update && \
apk add --update --no-cache alpine-sdk
RUN mkdir /myapp
WORKDIR /myapp
ADD hello.c /myapp
RUN mkdir bin
RUN gcc -Wall hello.c -o bin/hello

FROM alpine
COPY --from=sdk /myapp/bin/hello /myapp/hello
CMD /myapp/hello

在这个例子中,我们开始一个名为sdk的构建阶段。我们添加名称以便在下一个阶段中使用它作为引用。在sdk阶段,我们在安装了alpine-sdk包并包含了所需工具后,编译我们的 C 代码。最终,我们得到一个包含我们应用程序的 hello 二进制文件,位于/myapp/bin目录下(查看WORKDIR指令)。在下一个阶段,我们再次从一个全新的 alpine 镜像开始,只需将从sdk构建阶段(来自之前编译的镜像容器)复制过来的已编译 hello 二进制文件到新阶段构建容器的/myapp/hello目录下。正如构建过程中的常规操作一样,这个容器被提交为我们的新镜像。

多阶段构建简化了镜像的创建并提高了安全性。这样,构建过程只会添加先前创建的二进制文件和库,而不是编译器,从而避免了潜在的安全漏洞。

模板化镜像

使用准备好的 Dockerfile 并遵循特定的模板格式是很常见的。这无疑是一个非常有用的方法。在构建过程中传递参数并使用环境变量将为不同的 CI/CD 阶段创建不同的镜像,例如,使用相同的 Dockerfile。

在使用 CI/CD 编排构建时,模板化是关键,但有一些规则:

  • 不要在生产镜像中使用调试工具,因此请注意这些镜像,并且默认情况下在模板中使用更精简的镜像(包含更少的组件)。

  • 在构建时不要将凭证作为参数。管理用户和密码有其他机制,并且 Docker 的history命令将会暴露这些信息。

  • 代理设置已经准备好作为构建时的参数。因此,HTTP_PROXYHTTPS_PROXYFTP_PROXYNO_PROXY环境变量可以在构建过程中使用。这些变量将从 Docker 历史输出中排除,并且不会被缓存,因此我们需要使用ARG定义,以便在使用相同 Dockerfile 的多个构建之间更改代理设置。换句话说,在使用HTTP_PROXY变量之前,我们应该调用ARG指令来从 Docker 构建参数中获取其值:

FROM alpine:latest
ARG HTTP_PROXY
RUN apk update

之前的代码展示了一个例子,说明了如代理设置在每次构建时都会更新,如果其值发生变化的话。

操作系统和其他应用程序会使用http_proxyhttps_proxyftp_proxyno_proxy,而不是本节中描述的大写字符串。请检查应用程序的要求并使用适当的格式。

在本章的最后,我们将看到一个简单但具有说明性的实验,使用模板化的 Dockerfile 来构建生产和开发环境的不同版本,并使用包含一些调试工具的不同基础镜像,供开发人员使用。

镜像发布和更新

之前,我们提到过如何管理镜像更新。在那个实例中,我们专注于安全更新,以避免生产环境中的漏洞和 bug。同样,我们也可以将这个概念应用于应用程序修复和发布。

基础镜像应该更新关键的镜像组件,这些更改并不经常发生。通常,应用程序的发布是每周甚至每天发布(或者根据许多因素,如业务需求和关键修复,可能是每小时发布)。

根据基于特定镜像运行的容器数量,新的镜像发布可能是一次重大更改。这些更改可以在几分钟内完成,也可能需要一个小时。然而,使用容器的过程非常迅速;让编排器来处理它。Kubernetes 和 Swarm 将提供自动化的镜像更新和回滚,我们将能够管理如何执行此部署,多少容器将并行更新其镜像,我们将在这些更新之间等待多长时间,等等。

很容易理解,对基础镜像(用于构建其他镜像的镜像)进行更改需要特别小心。这些镜像的更新必须级联到所有派生镜像。我们通常会自动化这种级联构建。这些更改将要求所有派生镜像重新构建,并且会涉及更多的工作。建议使用持续集成协调器来自动化这些任务。

另一方面,当我们创建代码或二进制更新时,变化会更容易,因为我们只会影响为特定应用创建的容器。我们可以在通过组织内所有必要的测试后,快速部署这些更新。

章节实验

在本节中,我们将回顾 Docker 认证助理考试中的最重要概念。在这些实验中,我们将使用一台安装了 Docker 引擎的 CentOS Linux 主机,这一点在上一章中已经讲过。

如果你还没有部署,请从本书的 GitHub 仓库 (github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git) 部署environments/standalone-environment。你也可以使用你自己的 CentOS 7 服务器。从environments/standalone-environment文件夹中运行vagrant up来启动你的虚拟环境。

如果你正在使用standalone-environment,请等待它启动。我们可以通过vagrant status检查节点的状态。使用vagrant ssh standalone连接到你的实验节点。standalone是你的节点名称。你将使用具有 root 权限的vagrant用户,通过sudo来执行操作。你应该看到以下输出:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant up
Bringing machine 'standalone' up with 'virtualbox' provider...
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant status
Current machine states:
standalone running (virtualbox)
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$

现在,我们可以使用vagrant ssh standalone连接到standalone节点。如果你之前已经部署过standalone虚拟节点,并且刚刚通过vagrant up启动了它,这个过程可能会有所不同:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant ssh standalone
[vagrant@standalone ~]$ 

现在,你已经准备好开始实验了。

Docker 构建缓存

本实验将向我们展示在构建镜像时缓存是如何工作的。我们将能够加速构建过程,但这将取决于镜像层的排序。让我们开始吧:

  1. 首先,在 Docker 主机的主目录中创建一个名为chapter2的目录。我们将使用这个目录来进行这些实验:
[vagrant@standalone ~]$ cd $HOME
[vagrant@standalone ~]$ mkdir chapter2
[vagrant@standalone ~]$ cd chapter2
  1. 创建一个名为Dockerfile.cache的文件,文件内容如下:
FROM alpine:latest
RUN echo "hello world"
  1. 现在,构建一个名为test1的镜像,同时将该目录作为镜像上下文:
[vagrant@standalone chapter2]$ docker image build \
--file Dockerfile.cache --no-cache --label lab=lab1 -t test1 . 
 Sending build context to Docker daemon 2.048kB
 Step 1/2 : from alpine:latest
 ---> 961769676411
 Step 2/2 : run echo "hello world"
 ---> Running in af16173c7af8
 hello world
 Removing intermediate container af16173c7af8
 ---> 9b3e0608971f
 Successfully built 9b3e0608971f
 Successfully tagged test1:latest
  1. 由于我们没有使用任何特定的标签,Docker 会添加latest标签。现在,在没有任何更改的情况下重新构建镜像:
[vagrant@standalone chapter2]$ docker image build \
--file Dockerfile.cache --no-cache --label lab=lab1 -t test2 . 
 Sending build context to Docker daemon 2.048kB
 Step 1/2 : from alpine:latest
 ---> 961769676411
 Step 2/2 : run echo "hello world"
 ---> Running in 308e47ddbf7a
 hello world
 Removing intermediate container 308e47ddbf7a
 ---> aa5ec1fe2ca6
 Successfully built aa5ec1fe2ca6
 Successfully tagged test2:latest
  1. 现在,我们可以使用在构建过程中创建的lab标签列出我们的镜像:
[vagrant@standalone chapter2]$ docker image ls --filter label=lab=lab1
 REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
 test2               latest              fefb30027241        About a minute ago   5.58MB
 test1               latest              4fe733b3db42        About a minute ago   5.58MB

尽管我们没有更改任何内容,镜像 ID 还是不同。这是因为我们避免了层缓存,每一层都重新编译了。由于我们是一个接一个地启动镜像构建,两个构建之间仅相隔几秒钟。然而,元数据信息在它们之间发生了变化,因此它们的 ID 是不同的,尽管它们的内容是相同的。

  1. 现在,我们将使用缓存,因为它将提高构建时间。在许多情况下,这可以带来很大区别。让我们在 Dockerfile 中仅添加一行安装 Python 的指令。更新包缓存并安装 Python 及其依赖项将花费一些时间。当我们使用之前构建的缓存层时,构建过程会更快:
FROM alpine:latest
RUN echo "hello world"
RUN apk add --update -q python3
  1. 现在,我们重新构建一次,不使用缓存,测量过程完成所需的时间(以秒为单位):
[vagrant@standalone chapter2]$ time docker image build \
--file Dockerfile.cache -q -t test3 --no-cache .
 sha256:f2b524ac662682bdc13f77216ded929225d1b4253ebacb050f07d6d7e570bc51

 real    0m8.508s
 user    0m0.021s
 sys     0m0.042s

  1. 现在,添加一行来安装httpie,它需要安装 Python(以及包缓存)。现在,让我们分别使用和不使用缓存来运行构建:
FROM alpine:latest
RUN echo "hello world"
RUN apk add --update -q python3
RUN apk add -q httpie

不使用缓存,构建将花费超过一分钟:

[vagrant@standalone chapter2]$ time docker image build \
--file Dockerfile.cache -q -t test4 --no-cache . 
 sha256:b628f57340b34e7fd2cba0b50f71f4269cf8e8fb779535b211dd668d7c21912f real 1m28.745s
 user 0m0.023s
 sys 0m0.030s

在使用缓存进行新的构建之前,我们使用docker image rm test4删除了test4镜像,因为我们只想使用先前的层。

使用缓存,只需几秒钟:

[vagrant@standalone chapter2]$ time docker image build --file Dockerfile.cache -q -t test5 .
 sha256:7bfc6574efa9e9600d896264955dcb93afd24cb0c91ee5f19a8e5d231e4c31c7
real 0m15.038s
 user 0m0.025s
 sys 0m0.025s

由于此过程使用了先前的缓存层,它只花了 15 秒钟(test4,没有缓存,构建花了 1 分钟 28 秒)。我们只添加了一个层,且仅安装了一个包,尽管镜像很小(约 100MB),但差异超过了 1 分钟。编译 5GB 的镜像可能需要几个小时(这并不推荐,尽管使用缓存是一个不错的做法)。

在 Dockerfile 中使用卷的位置

在本实验中,我们将回顾VOLUME键的定义如何由 Docker Daemon 管理,以指定持久化存储或在构建时避免容器空间。让我们开始吧:

  1. 让我们考虑一个小型的 Dockerfile,它使用卷在构建时在各个层之间持久化数据。卷的定义还将通知 Docker Daemon 绕过 CoW 机制中的卷目录。我们将这个 Dockerfile 命名为Dockerfile.chapter2.lab2
FROM alpine
RUN mkdir /data
RUN echo "hello world" > /data/helloworld
VOLUME /data
  1. 让我们构建这个镜像:
[vagrant@standalone ~]$ docker image build \
-f Dockerfile.chapter2.lab2 -t ch2lab2 --label lab=lab2 .

 Sending build context to Docker daemon  3.072kB
 Step 1/5 : FROM alpine
 ---> 961769676411
 Step 2/5 : RUN mkdir /data
 ---> Running in fc194efe122b
 Removing intermediate container fc194efe122b
 ---> d2d208a0c39e
 Step 3/5 : RUN echo "hello world" > /data/helloworld
 ---> Running in a390abafda32
 Removing intermediate container a390abafda32
 ---> b934d9c51292
 Step 4/5 : VOLUME /data
 ---> Running in 33df48627a75
 Removing intermediate container 33df48627a75
 ---> 8f05e96b072b
 Step 5/5 : LABEL lab=lab2
 ---> Running in 353a4ec552ef
 Removing intermediate container 353a4ec552ef
 ---> 4a1ad6047fea
 Successfully built 4a1ad6047fea
 Successfully tagged ch2lab2:latest

  1. 现在,使用ch2lab2镜像运行容器,以获取容器的/data目录内容:
[vagrant@standalone ~]$ docker container run ch2lab2 ls -lt /data
 total 4
 -rw-r--r--    1 root     root            12 Oct  7 19:30 helloworld

  1. 现在,我们将改变VOLUME指令的顺序。我们将VOLUME定义写在执行echo之前。我们将使用一个名为Dockerfile.chapter2.lab2-2的新文件:
FROM alpine
RUN mkdir /data
VOLUME /data
RUN echo "hello world" > /data/helloworld
  1. 现在,让我们构建一个新的镜像,并查看/data内容发生了什么:
[vagrant@standalone ~]$ docker image build \
-f Dockerfile.chapter2.lab2-2 -t ch2lab2-2 --label lab=lab2 .

 Sending build context to Docker daemon  4.096kB
 Step 1/5 : FROM alpine
 ---> 961769676411
 Step 2/5 : RUN mkdir /data
 ---> Using cache
 ---> d2d208a0c39e
 Step 3/5 : VOLUME /data
 ---> Using cache
 ---> 18022eec6fd2
 Step 4/5 : RUN echo "hello world" > /data/helloworld
 ---> Using cache
 ---> dbab99bb29a0
 Step 5/5 : LABEL lab=lab2
 ---> Using cache
 ---> ac8ef5e1b61e
 Successfully built ac8ef5e1b61e
 Successfully tagged ch2lab2-2:latest

  1. 让我们再次查看/data内容:
[vagrant@standalone ~]$ docker container run ch2lab2-2 ls -lt /data 
 total 0

正如我们预期的那样,VOLUME指令允许容器绕过 CoW 文件系统。在构建过程中,容器不会保持卷的内容,因为提交操作仅会将容器内容转换为镜像,而卷不会出现在容器内部。

多阶段构建

在本实验中,我们将创建一个简单的 C 语言 Hello World 二进制文件,并使用中间镜像在第一阶段编译此代码,然后将二进制文件复制到一个更干净的镜像中。最终,我们将获得一个小的镜像,包含运行已编译应用程序所需的所有组件。让我们开始吧:

  1. chapter2目录下创建一个名为multistage的新目录:
[vagrant@standalone ~]$ cd $HOME/chapter2
[vagrant@standalone ~]$ mkdir multistage
[vagrant@standalone ~]$ cd multistage
  1. 现在,创建一个名为helloword.c的文件,内容如下:
#include <stdio.h>
 int main()
 {
   printf("Hello, World!\n");
   return 0;
 }

  1. 准备一个基于 alpine 的多阶段 Dockerfile,命名为 Dockerfile.multistage。第一阶段命名为 compiler,在其中我们将安装 alpine-sdk 以编译 C 代码。我们在第一阶段编译 C 代码,然后使用 COPY 语句将二进制文件从上一个阶段复制过来。它看起来是这样的:
FROM alpine AS compiler 
RUN apk update && \ 
apk add --update -q --no-cache alpine-sdk 
RUN mkdir /myapp 
WORKDIR /myapp 
ADD helloworld.c /myapp 
RUN mkdir bin 
RUN gcc -Wall helloworld.c -o bin/helloworld 

FROM alpine 
COPY --from=compiler /myapp/bin/helloworld /myapp/helloworld 
CMD /myapp/helloworld

使用之前的代码,我们将构建一个新的镜像:

[vagrant@standalone multistage]$ docker build \
--file Dockerfile.multistage --no-cache -t helloworld --label lab=lab3 . 
 Sending build context to Docker daemon  3.072kB
 Step 1/11 : FROM alpine AS compiler
 ---> 961769676411
 Step 2/11 : RUN apk update && apk add --update -q --no-cache alpine-sdk
 ---> Running in f827f4a85626
 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-102-ge3e3e39529 [http://dl-cdn.alpinelinux.org/alpine/v3.10/main]
 v3.10.2-103-g1b5ddad804 [http://dl-cdn.alpinelinux.org/alpine/v3.10/community]
 OK: 10336 distinct packages available
 Removing intermediate container f827f4a85626
 ---> f5c469c3ab61
 Step 3/11 : RUN mkdir /myapp
 ---> Running in 6eb27f4029b3
 Removing intermediate container 6eb27f4029b3
 ---> 19df6c9092ba
 Step 4/11 : WORKDIR /myapp
 ---> Running in 5b7e7ef9504a
 Removing intermediate container 5b7e7ef9504a
 ---> 759173258ccb
 Step 5/11 : ADD helloworld.c /myapp
 ---> 08033f10200a
 Step 6/11 : RUN mkdir bin
 ---> Running in eaaff98b5213
 Removing intermediate container eaaff98b5213
 ---> 63b5d119a25e
 Step 7/11 : RUN gcc -Wall helloworld.c -o bin/helloworld
 ---> Running in 247c18ccaf03
 Removing intermediate container 247c18ccaf03
 ---> 612d15bf6d3c
 Step 8/11 : FROM alpine
 ---> 961769676411
 Step 9/11 : COPY --from=compiler /myapp/bin/helloworld /myapp/helloworld
 ---> 18c68d924646
 Step 10/11 : CMD /myapp/helloworld
 ---> Running in 7055927efe3e
 Removing intermediate container 7055927efe3e
 ---> 08fd2f42bba9
 Step 11/11 : LABEL lab=lab3
 ---> Running in 3a4f4a1ad6d8
 Removing intermediate container 3a4f4a1ad6d8
 ---> 0a77589c8ecb
 Successfully built 0a77589c8ecb
 Successfully tagged helloworld:latest 

  1. 我们现在可以验证 helloworld:latest 是否按预期工作,并且它将只包含位于干净的 alpine:latest 镜像之上的 /myapp/helloworld 二进制文件:
[vagrant@standalone multistage]$ docker container run helloworld:latest
 Hello, World! 

现在,我们将列出镜像,以便查看我们最近创建的镜像:

[vagrant@standalone multistage]$ docker image ls --filter label=lab=lab3
 REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
 helloworld          latest              0a77589c8ecb        2 minutes ago       5.6MB 

部署一个本地注册中心

在这个实验中,我们将运行一个本地注册中心并推送/拉取一个镜像。让我们开始吧:

  1. 首先,我们将使用官方的 Docker Registry 镜像部署一个注册中心。我们将它启动在标准的注册中心端口 5000 上:
[vagrant@standalone ~]$ cd $HOME/chapter2  [vagrant@standalone ~]$ docker container run -d \
-p 5000:5000 --restart=always --name registry registry:2
....
....
0d63bdad4017ce925b5c4456cf9f776551070b7780f306882708c77ce3dce78c
  1. 然后,我们需要下载一个简单的 alpine:latest 镜像(如果你还没有的话):
[vagrant@standalone ~]$ docker pull alpine
Using default tag: latest
latest: Pulling from library/alpine
e6b0cf9c0882: Pull complete 
Digest: sha256:2171658620155679240babee0a7714f6509fae66898db422ad803b951257db78
Status: Downloaded newer image for alpine:latest
docker.io/library/alpine:latest
  1. 然后,我们需要为这个镜像添加一个新的标签,以便能够将其上传到我们本地的注册中心,该注册中心运行在 5000 端口:
[vagrant@standalone ~]$ docker tag alpine localhost:5000/my-alpine 

我们将使用 docker image tag <ORIGINAL_TAG> <NEW_TAG> 为镜像添加名称和标签。这样会添加新的名称和标签;旧的标签会保留,直到它们被移除。我们将使用 docker image rm 来移除镜像的名称和标签。这只会移除作为参数传入的名称和标签。与同一 ID 相关联的其他镜像会保留,直到它们被明确移除。如果我们创建一个新的构建,一些层会变成未引用的,甚至会被从任何镜像构建链中移除。

我们可以使用 docker image rm --force <IMAGE_ID> 移除与特定 ID 相关联的所有镜像。与它相关联的所有镜像名称和标签都会被移除。

未引用的镜像,也称为 dangling 镜像,应该被移除,特别是在镜像构建主机上。这些镜像在 CI/CD 环境中很常见,我们为此过程分配了一些节点。我们将使用 docker image prune 来执行该镜像的清理工作。

  1. 然后,我们将镜像推送到本地注册中心:
[vagrant@standalone ~]$ docker image push localhost:5000/my-alpine
The push refers to repository [localhost:5000/my-alpine]
6b27de954cca: Pushed 
latest: digest: sha256:3983cc12fb9dc20a009340149e382a18de6a8261b0ac0e8f5fcdf11f8dd5937e size: 528
  1. 为确保没有其他 alpine 镜像存在,我们通过其 ID 移除它:
[vagrant@standalone ~]$ docker images --filter=reference='alpine:latest' 
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest cc0abc535e36 42 hours ago 5.59MB
  1. 我们移除这个 ID 及其所有子镜像(ID 可能会有所不同):
[vagrant@standalone ~]$ docker image rm cc0abc535e36 --force
Untagged: alpine:latest
Untagged: alpine@sha256:2171658620155679240babee0a7714f6509fae66898db422ad803b951257db78
Untagged: localhost:5000/my-alpine:latest
Untagged: localhost:5000/my-alpine@sha256:3983cc12fb9dc20a009340149e382a18de6a8261b0ac0e8f5fcdf11f8dd5937e
Deleted: sha256:cc0abc535e36a7ede71978ba2bbd8159b8a5420b91f2fbc520cdf5f673640a34
  1. 然后,我们使用 localhost:5000/my-alpine:latest 镜像运行一个容器:
[vagrant@standalone ~]$ docker container run localhost:5000/my-alpine:latest ls /tmp
Unable to find image 'localhost:5000/my-alpine:latest' locally
latest: Pulling from my-alpine
e6b0cf9c0882: Already exists 
Digest: sha256:3983cc12fb9dc20a009340149e382a18de6a8261b0ac0e8f5fcdf11f8dd5937e
Status: Downloaded newer image for localhost:5000/my-alpine:latest

在这里,我们使用了从 localhost:5000 注册中心下载的镜像。

正如我们之前提到的,Docker Registry 默认是不安全的。它容易部署,但在生产环境中,我们需要身份验证和授权。身份验证可以通过前端代理与验证来部署。NGINX 即使仅提供基本身份验证也能部署,并且还可以提供 TLS 证书加密。授权并不像身份验证那样简单,因此 Docker Trusted Registry 是一个更好的解决方案。

在这个示例中,我们将我们的注册中心发布到本地端口5000。如果主进程终止,应用容器将重新启动,并且镜像的数据将保存在主机的/var/lib/docker/volumes/REGISTRY_DATA/_data目录下。我们使用了名为REGISTRY_DATA的卷,因此即使我们删除registry容器,注册中心数据也会保留。

Docker Registry 可以配置为使用不同的存储后端。我们将在第十三章中学习关于 DTR 的这个功能,实现企业级注册中心与 DTR。Docker Registry 可以通过/etc/docker/registry/config.yml文件进行配置。要在当前目录下部署一个本地主机配置文件,我们将使用$(pwd)/config.yml:/etc/docker/registry/config.yml。这将把自定义文件作为绑定挂载卷进行集成。

  1. 最后,我们移除已部署的注册中心:
[vagrant@standalone ~]$ docker container rm --force registry
registry

使用 Dockerfile 进行镜像模板化

本实验将向我们展示如何通过添加一些调试工具来构建适用于不同环境的镜像,例如,调试容器的进程。

chapter2目录下创建一个名为templating的新目录:

[vagrant@standalone ~]$ cd $HOME/chapter2
[vagrant@standalone ~]$ mkdir templating
[vagrant@standalone ~]$ cd templating

我们将有几个镜像:一个用于生产环境,一个用于开发环境。我们将为每个环境使用各自的 Dockerfile 来构建镜像;在这种情况下,我们将使用一个简单的nginx:alpine镜像作为两者的基础:

  • 开发环境 – Dockerfile.nginx-dev
FROM nginx:alpine 
RUN apk update -q
RUN apk add \ 
curl \ 
httpie
  • 生产环境 – Dockerfile.nginx
FROM nginx:alpine 
RUN apk update -q

让我们构建这两个镜像:

  1. 我们将这两个镜像分别构建为baseimage:developmentbaseimage:production
[vagrant@standalone templating]$ docker image build \
--quiet --file Dockerfile.nginx-dev -t baseimage:development --label lab=lab4 .     

 sha256:72f13a610dfb1eee3332b87bfdbd77b17f38caf08d07d5772335e963377b5f39 
[vagrant@standalone templating]$ docker image build \
 --quiet --file Dockerfile.nginx -t baseimage:production --label lab=lab4 .

 sha256:1fc2505b3bc2ecf3f0b5580a6c5c0f018b03d309b6208220fc8b4b7a65be2ec8

  1. 现在,我们可以查看镜像的大小。由于调试镜像包含了curlhttpie用于测试(这是一个示例实验),所以它们的大小有很大不同。我们将使用这些镜像启动调试工具,以便查看容器的进程或与其他组件进行比较:
[vagrant@standalone templating]$ docker image ls --filter label=lab=lab4 
 REPOSITORY       TAG         IMAGE ID      CREATED              SIZE
 baseimage    development   72f13a610dfb  13 seconds ago       83.4MB
 baseimage     production   1fc2505b3bc2  4 minutes ago        22.6MB

  1. 现在,我们可以使用ENVIRONMENT变量和模板化的Dockerfile.application文件为开发和生产环境构建我们的应用镜像:
ARG ENVIRONMENT=development 
FROM baseimage:${ENVIRONMENT} 
COPY html/* /usr/share/nginx/html

  1. 现在,我们只需准备一个名为index.html的简单文本文件,并将其放在html目录中:
[vagrant@standalone templating]$ mkdir html
[vagrant@standalone templating]$ echo "This is a simple test and of course it is not an application!!!" > html/index.html

  1. 最后,我们只需为DEVPROD环境编译这两个镜像。对于开发环境,我们使用ENVIRONMENT参数,如下所示:
[vagrant@standalone templating]$ docker image build \
--file Dockerfile.application \
-t templated:development \
--build-arg ENVIRONMENT=development \
--label lab=lab4 .
 Sending build context to Docker daemon  5.632kB
 Step 1/4 : ARG ENVIRONMENT=development
 Step 2/4 : FROM baseimage:${ENVIRONMENT}
 ---> 1fc2505b3bc2
 Step 3/4 : COPY html/* /usr/share/nginx/html
 ---> Using cache
 ---> e038e952a087
 Step 4/4 : LABEL lab=lab4
 ---> Running in bee7d26757da
 Removing intermediate container bee7d26757da
 ---> 06542624803f
 Successfully built 06542624803f
 Successfully tagged templated:development

对于生产环境,我们将做相同的操作:

[vagrant@standalone templating]$ docker image build \
--file Dockerfile.application \
-t templated:production \
--build-arg ENVIRONMENT=production \
--label lab=lab4 . 
 Sending build context to Docker daemon  5.632kB
 Step 1/4 : ARG ENVIRONMENT=development
 Step 2/4 : FROM baseimage:${ENVIRONMENT}
 ---> 1fc2505b3bc2
 Step 3/4 : COPY html/* /usr/share/nginx/html
 ---> Using cache
 ---> e038e952a087
 Step 4/4 : LABEL lab=lab4
 ---> Using cache
 ---> 06542624803f
 Successfully built 06542624803f
 Successfully tagged templated:production

通过本实验,我们使用一个 Dockerfile 构建了不同的镜像。参数将改变构建过程。

总结

本章向我们展示了如何构建容器镜像。我们学习了所有构建步骤以及一些技巧和窍门,这些将帮助我们确保镜像的安全性。构建良好的安全镜像是生产环境的关键,正如我们所学到的,拥有良好的基础镜像将帮助我们构建更好的应用镜像。我们将重用许多层,因此从底到顶确保安全性会更加安全。为了确保安全性,我们只需添加必要的软件,暴露必需的进程,并避免不必要的 root 进程。

我们还学习了如何使用类似代码版本控制的标签存储镜像及其元数据,以确保在生产环境中运行正确的镜像。

最后,我们学习了如何实现模板来为不同的环境或阶段在 CI/CD 流水线中创建镜像。

在下一章,我们将学习如何运行容器。

问题

  1. 我们如何唯一标识一个镜像?

a) 所有带有标签的镜像都是唯一的

b) 镜像 ID 是使镜像唯一的关键;我们可以拥有一个镜像 ID,但它有多个名称和标签,它们都会引用相同的层和元数据

c) 只有根注册表命名空间中的基础镜像是唯一的,因为所有其他镜像都是基于这些镜像创建的

d) 以上所有答案都正确

  1. 哪些方法可以用来创建容器镜像?

a) 我们可以从容器构建镜像,将其读写层提交到只读层之上

b) 我们可以使用 Dockerfile,从一个基础镜像开始

c) 我们可以从一个空的镜像开始,这个镜像被称为 scratch

d) 以上所有选项都正确

  1. 哪些镜像创建方法是可重现的?

a) 提交容器为镜像是可重现的,因为我们知道执行了哪些步骤

b) 使用 Dockerfile,我们将确保编写必要的步骤,并确保创建过程是可重现的

c) 创建镜像没有可重现的方法

d) 以上所有选项都不正确

  1. 哪些 Dockerfile 指令接受 Shell 和 Exec 格式?

a) RUN

b) 仅使用 CMD

c) ENTRYPOINTCMD

d) 所有 Dockerfile 指令都接受执行(Exec)和 Shell 格式

  1. 我们如何在基于镜像启动容器时避免使用命令参数?

a) 我们可以通过使用 Shell 格式的 ENTRYPOINT 来避免用户修改主进程的参数和参数

b) 永远无法修改容器主进程

c) 无论使用何种ENTRYPOINT格式,都始终可以修改主容器进程的参数

d) 以上所有选项都不正确

进一步阅读

您可以参考以下链接,了解本章涵盖的更多信息:

运行 Docker 容器

本章专门讲解 Docker 命令行。在之前的章节中,我们运行了一些容器,但并未详细讨论使用的参数和选项。

在本章中,我们将讨论不同的 Docker 对象,如镜像、容器和数据卷,以及它们的相关操作。并非所有对象都具有相同的功能,因此它们的操作和参数也不同。

记住,镜像构建是基于容器执行的。每一层都是在容器上执行命令后自动“提交”到 Docker 节点文件系统中的结果。所有这些层合并在一起,构成一个镜像。

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

  • 深入审视 Docker 命令行

  • 了解 Docker 对象

  • 运行容器

  • 与容器的交互

  • 限制主机资源

  • 将容器转换为镜像

  • 格式化与筛选信息

  • 管理设备

让我们首先来看一下如何使用 Docker 命令行。

第五章:技术要求

在本章中,我们将学习 Docker 容器的概念。在本章结束时,我们将提供一些实验,帮助你理解并学习涵盖的概念。这些实验可以在你的笔记本电脑或 PC 上运行,使用提供的 Vagrant 独立环境或你自己部署的任何 Docker 主机。更多信息可以在本书的 GitHub 仓库找到:github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

查看以下视频,看看代码如何实际运行:

"bit.ly/32AEGHU"

深入审视 Docker 命令行

正如我们在前几章中学到的那样,Docker 是一个客户端-服务器应用程序。早期版本的 Docker 会同时安装这两个组件,但新版本允许我们只安装客户端来使用远程服务器。

我们在第一章,现代基础设施与 Docker 应用 中了解了各种 Docker 守护进程的选项和参数。在本章中,我们将回顾 Docker 客户端命令行。

当我们在 Linux 或 Windows 上使用 Docker 命令行时,我们总是指向 Docker 客户端,通常情况下,二进制文件或可执行程序分别位于 Linux 和 Windows 上的 /usr/bin/dockerC:\ProgramData\Docker

Docker 的命令行使用格式是 docker [OPTIONS] COMMAND。各种选项用于定义我们将连接的守护进程以及如何建立这次通信。调试和日志级别也在此时进行管理。这些选项中的一些可以通过 Docker 客户端配置,在每个用户的 config.json 文件中设置,该文件位于用户的 home 目录下。

Docker 客户端配置文件 config.json 将管理过滤选项,我们将在本章末尾学习这些内容。它还存储对注册表的登录访问。

环境变量也可以用于配置 Docker 客户端的行为。以下是最常用的环境变量列表:

  • DOCKER_CONFIG: 这将设置 Docker 客户端的配置文件路径。

  • DOCKER_CERT_PATH: 这设置了客户端与服务器之间证书的路径。

  • DOCKER_HOST: 我们可以使用远程 Docker 引擎。默认情况下,我们将使用本地 Docker 守护进程。

  • DOCKER_TLS: 此选项启用 TLS 通信(需要证书才能正常工作)。

  • DOCKER_TLS_VERIFY: 此选项将不验证远程守护进程证书。

  • DOCKER_CONTENT_TRUST: 我们将使用此选项来启用内容信任功能(镜像不可变性和所有权)。

Docker 命令始终需要一个 Docker 守护进程,并且它们将针对对象执行。这些是由 Docker 管理的内部资源,按类别分布,具有不同的功能和属性。我们将在下一节中详细讨论这一点。

所有 Docker 对象都有自己的 ID。名称是与这些 ID 关联的标签,因此,在某些情况下,我们可以为一个对象指定多个名称。对象 ID 将唯一标识每个对象,因此,Docker 可以在不使用其类别的情况下显示或管理有关该对象的信息。我们建议在 Docker 命令行中始终使用类别。

下表显示了所有对象共有的命令:

ls or list 这将显示该类别中所有对象的列表。输出可能会有所不同,这取决于查询哪些对象,但通常会获得对象名称及其 ID。我们将使用 --all-a 修饰符来显示所选类别的所有对象,因为在某些情况下,输出可能仅显示子集。例如,如果我们列出容器对象,默认情况下,我们只会得到正在运行的容器。已停止(退出)的容器将不会显示,除非使用 --all 命令修饰符。过滤功能允许我们仅检索对象的子集。我们将使用 --filter-f 参数进行此操作。每个对象类别都将有自己的键,以便轻松过滤信息。稍后在本章节中,我们将学习如何过滤信息。格式化也非常重要。我们将使用 --format 选项格式化输出的信息。通常的格式包括 tablejson,分别用于获取类似表格的信息和 JSON 格式。我们可以定制和排序获得的信息。所有过滤器应使用 Go 模板格式构建。格式化输出是一门艺术!稍后在本章节中,我们将看到许多选项。一个很好的起点始终是使用 --format='{{json .}}' 来查看可以用于格式化的 JSON 键。我们可以使用 --quiet-q 避免完整命令的输出。此参数将仅显示该类别中列出的对象 ID。这对于将输出串联或管道化到其他命令非常有用。
rm or remove 此操作将删除定义的对象。我们可以使用它们的 ID 或名称来删除它们。一旦删除,它们将无法恢复。为了避免确认对象删除,我们将使用 --force 参数。
create 所有对象都可以创建和删除,但每个对象都有自己的参数。因此,我们将在不同章节中学习每个对象的参数。我们将从下一部分开始介绍容器参数。
inspect 要查看对象定义的属性,我们将使用 inspect 操作。默认情况下,对象描述将以 JSON 格式显示。我们还可以使用 --format 格式化其输出。在这种情况下,我们可以格式化对象描述的输出。这对于仅获取所需的几个值非常有用,如下例所示:**$ docker image inspect nginx:alpine --format "{{ json .Config.Cmd }}"**``**["nginx","-g","daemon off;"]**

Docker 客户端是用 Go 编写的,它包含许多 Go 模板格式化和过滤选项。

每次我们使用 docker ps,实际上都在执行 docker container ls.

在接下来的部分中,我们将介绍 Docker 中可用的不同资源或对象。

学习关于 Docker 对象

让我们定义独立 Docker 守护程序可用的不同对象类别:

  • 镜像:这些是创建容器的基础。在第二章《构建 Docker 镜像》中,我们了解了多层模板的概念,这些模板为容器的主进程提供根文件系统,并包含执行它所需的所有元信息。

  • 容器:正如我们在第一章《现代基础设施与 Docker 应用》中所学,容器是为进程(或多个进程)隔离命名空间、资源和文件的组合。该进程将在一个封装环境中运行,仿佛它独自在自己的系统中运行,分享主机的内核及其资源。

  • :卷用于绕过写时复制容器的文件系统。因此,我们可以将数据存储在容器之外,避免其生命周期的影响。我们将在第四章《容器持久性与网络》中深入了解卷。

  • 网络:容器在自己的网络命名空间中运行,但它们需要访问真实的基础设施网络。它们将使用主机的物理接口在桥接模式下运行,为每个容器接口创建虚拟接口。我们将在第四章《容器持久性与网络》中进一步了解这种工作模式和许多其他选项,容器持久性和网络

  • 插件:Docker 插件通过与 Docker 守护进程并行运行的进程扩展引擎功能。它们将与守护进程共享信息和配置,以提供新特性。插件有三种不同类型:授权插件、卷插件和网络插件。Docker 客户端命令行提供了安装和管理插件的接口。它们的配置将部署在 /usr/lib/docker/plugins/etc/docker/plugins 目录下。

这些对象在独立的 Docker 守护进程中可用,但当主机参与分布式 Docker Swarm 集群时,还有其他对象。我们将在编排章节中讨论这些内容,但在这里我们将简要概述一下:

  • Swarm:该对象提供集群属性。它允许我们创建新的集群并加入或离开已创建的集群。它还通过管理证书颁发机构或锁定对集群证书的访问来维护集群安全。

  • 节点:节点是集群中的主机。我们可以在集群中更新节点角色,并在需要时将其删除。我们还可以修改哪些节点将运行已定义的工作负载。

  • 服务:Docker Swarm 不会管理容器。Docker Swarm 中的最小调度单元是服务。服务将创建任务,而任务将通过容器呈现。在 Docker Swarm 中,我们通过声明服务的状态和所需的健康任务数来部署服务。我们将能够创建服务、更新它们的属性(副本、用于容器的镜像等),或删除它们。

  • 堆栈:当我们谈论在 Swarm 上部署工作负载时,通常使用堆栈,它是多服务应用程序。我们将定义一个应用程序运行所需的所有组件。这些组件将包括服务及其所有的卷、网络等,以及它们之间的交互。

Swarm 对象具有之前描述的所有操作。然而,我们还可以使用 update 操作来设置和更改对象属性。此操作仅在使用 Docker Swarm 时可用。

在下一节中,我们将学习如何使用描述的命令行安全地运行容器。

运行容器

容器只是以隔离方式在 Docker 主机上运行的进程。运行该进程所需的所有功能或属性可以在容器创建时进行调整。

主要容器操作

容器可以在需要时创建、执行和停止。下表将介绍此工作流程的主要容器操作:

create 因为容器是 Docker 对象,所以我们可以创建它们。当我们创建一个容器时,我们配置该容器的工作方式,但不启动它。这个阶段将准备一个容器,我们可以使用 inspect 查看其静态配置。任何动态配置都不会存在,因为容器尚未运行。
start 一旦容器被创建,就可以使用 start 启动它。这意味着容器定义的进程将以配置的隔离方式(内存、CPU、网络等)以及所需的外部资源执行。容器启动后,我们将能够列出它或查看其状态。
run 此操作将创建并启动一个容器。这是我们通常启动容器的方式。许多对象和操作有一些命令别名,例如 docker run。我们建议使用完整的句子,包括您正在执行操作的对象。无论是使用 docker container run 还是 docker run 启动的 Docker 容器将会在前台运行。默认情况下,您的终端将连接到容器的输出。为了避免这种行为,我们必须使用 --detach-d 将容器启动在后台,与当前终端分离。
pause/unpause 我们可以使用 Linux 中的 cgroups 冻结容器的进程。该进程将保持暂停状态,直到被解冻。
stop 停止容器将遵循以下工作流程。首先,主进程将收到 SIGTERM 信号。这将尝试正常关闭和终止进程。默认情况下,Docker 守护程序将在发送第二个信号之前等待 10 秒。然后,守护程序将发送 SIGKILL 信号以完全终止进程。因此,守护程序首先尝试优雅地终止容器的主进程,如果未停止,则会强制终止。我们可以使用 --stop-signal 配置要发送的停止容器的信号。默认为 SIGTERM,如前所述。此外,我们可以使用 --time 参数更改等待发送第二个 SIGKILL 信号的秒数(默认为 10 秒)。当容器已经运行时,可以在创建或执行中使用 --stop-timeout 进行配置。
kill 正如我们先前提到的,当我们运行 docker container stop 时,Docker 守护程序将首先尝试优雅地停止它。有些情况下,我们希望立即完全杀死主进程。在这些情况下,我们可以使用 docker container kill 立即停止容器。可以使用 -s 更改发送的信号,默认情况下将发送 SIGKILL 信号。
restart restart 操作将停止并重新启动容器。这意味着先前学习的程序将被执行,并且将执行 Docker 容器的 stopstart 操作。因此,先前描述的参数也将有效。
rm 如我们在之前的章节中学到的,容器不是临时的。它们将保留在我们的系统中,直到有人删除它们。我们将使用 docker container rm 来删除它们。运行中的容器除非使用 --force/-f 参数,否则无法删除。建议在删除生产中的容器之前停止它们,以避免因错误而删除重要容器。
prune 此命令将删除所有已停止的容器。可以使用 --force 强制执行,可以使用 --filter 参数使用过滤器限制要删除的容器。
rename 使用此操作,我们更改容器名称。
update 使用 update 操作,我们可以更改容器的主机资源限制和其重启策略。

仅使用 Hyper-V 隔离的容器可以在 Windows 上暂停。

默认情况下,所有容器将使用非限制资源执行。除非我们限制它们访问主机资源,否则它们不会运行隔离。要限制容器可用的资源数量,必须在创建时指定其阈值。我们将在本章节的 限制主机资源 部分中使用相同的参数来管理容器资源。

我们可以使用--rm选项在容器执行完毕后将其删除。它还会删除在容器生命周期内创建的所有未命名卷。这些卷是临时定义的,用于覆盖写时复制文件系统。我们必须手动删除它们,或者使用docker container rm命令配合-v参数来删除它们。

容器网络属性

容器在自己的网络命名空间中运行。它们将获得自己的 IP 地址和网络资源。默认情况下,Docker 守护进程使用桥接网络,容器将通过复制主机值来获得自己的名称解析配置。我们可以在容器创建和执行时更改此行为。让我们回顾一下可以用于配置容器内部网络的选项:

--name 我们可以为每个容器指定一个名称。如果不指定容器名称,系统将生成一个随机名称。这样,我们可以通过这个定义的名称来管理容器。默认情况下,它将作为主机名使用。
--add-host 使用此参数,我们可以添加主机及其 IP 地址。我们将使用host:ip格式的条目。
--dns 此选项将允许我们避免使用默认的 DNS 解析。每当嵌入式 DNS 服务器无法解析名称时,查询将被转发到定义的外部 DNS 服务器(默认从主机复制)。
--dns-option 此选项将把与容器相关的选项添加到嵌入式 DNS 服务器中。

每个桥接网络将提供内部名称解析,使用 Docker 嵌入式 DNS 服务器,地址为127.0.0.11。唯一的例外是默认桥接接口。在这种情况下,我们需要使用--link来根据容器名称允许从另一个容器通过桥接接口访问已部署的容器。

--dns-search 此选项设置名称解析的搜索域名。
--domainname 此选项设置容器的域名。
--ip--ip6 有时我们需要指定容器的 IP 地址,可以是 IPv4 或 IPv6。我们将在创建或执行容器时传递版本 4 或版本 6 的地址作为参数。内部 IPAM 将从桥接网络接口范围分配内部 IP 地址。
--hostname 我们可以设置容器的内部主机名,默认值为容器 ID。
--link 我们可以使用CONTAINER_NAME:DNS_ALIAS的形式添加到其他容器的内部名称解析。这些链接名称将可以被其他容器通过它们的名称或 IP 地址访问(这是默认选项)。
--mac-address 此选项允许我们设置容器的 MAC 地址。
--network 我们可以选择为容器提供何种类型的网络连接。默认情况下,所有容器将在默认的桥接网络上运行。在本章中,我们将只使用默认的网络模式,但也有其他选项,我们将在后续章节中学习这些选项。
--network-alias 此选项帮助我们为容器指定网络别名。这样我们可以对容器的 IP 地址进行更多的名称解析。

我们需要定义一个重启策略来管理容器的生命周期。我们要求容器快速停止/死亡和重新启动。弹性是应用程序可用性的关键。我们可以使用 --restart 参数来管理容器的行为。共有四个选项:

  • no:这是默认选项。如果容器停止或被手动停止,容器将保持停止状态。

  • on-failure:此选项仅当容器因主进程失败而停止时才会重新启动容器。

  • always:不管容器是被人为停止还是自我停止,我们都希望容器持续运行;因此,Docker 守护进程将始终尝试重新启动它。

  • unless-stopped:如果我们执行了 Docker stop 命令,此选项将不会重新启动容器。

这些选项非常重要,因为它们管理 Docker 守护进程在重新启动时需要对容器执行的操作。例如,当我们需要重启主机时。

容器行为定义

以下表格展示了一些可用于覆盖镜像预定义值的选项:

--entrypoint 我们可以在创建或执行容器时覆盖定义的入口点。不要依赖于此功能的安全性,因为任何人都可以将你的入口点更改为镜像中包含的任何其他二进制文件或脚本。
--env-e--env-file 我们可以覆盖基础镜像中定义的变量,或为新容器添加新的变量。
--expose 我们可以为容器暴露新的端口。这些端口将在内部可用,但不会被发布。
--health-cmd, --health-interval, --health-retries, --health-start-period, --health-timeout 所有这些选项将覆盖镜像中定义的健康检查值。
--no-healthcheck 此选项禁用镜像中定义的健康检查。
--label-l--label-file 此选项允许在创建或执行容器时添加标签。这些标签将帮助我们筛选或查找与进程相关的信息。Docker 守护进程或编排器自动添加一些标签,用于标识分组对象。
--user-u 此选项覆盖镜像中定义的用户。
--volume-v 此选项使用定义的卷或主机路径,并将其挂载到容器内。此选项非常重要,因为用于绕过写时复制文件系统的临时卷(也称为未命名卷)将在 /var/lib/docker/volumes(或 MS Windows 主机上的等效路径)下创建。它们通过随机 ID 进行标识。卷不会随容器的生命周期变化,除非我们在执行 docker container rm 操作时使用 -v 参数,否则必须手动删除。

在容器创建时传递的参数将作为参数添加到镜像定义的入口点中。因此,镜像定义的 CMD 值将被容器执行时传递的参数所覆盖。其他参数,如 --user--env--entrypoint--health-cmd--health-timeout 等,将覆盖镜像定义的值,从而修改镜像的进程行为。请注意,参数语法与镜像定义的键相关。

一旦容器被创建并执行,默认情况下,终端将连接到其标准输出和错误输出。我们将获得所有主要进程的错误和输出。我们还可以通过使用 --interactive-i 选项以交互方式启动容器。通常,我们会使用 --tty-t 分配一个伪终端,以便将一个完全功能的终端附加到主进程上。

执行容器

一个简单的例子将帮助我们理解这种行为。我们将使用nginx:alpine镜像启动一个小型的 web 服务器。在这种情况下,我们使用的是来自 docker.io 注册表的官方 nginx 镜像,标记为 alpine,它是基于 Alpine Linux 的最小镜像:

$ docker container run nginx:alpine
 Unable to find image 'nginx:alpine' locally
 alpine: Pulling from library/nginx
 9d48c3bd43c5: Already exists
 1ae95a11626f: Pull complete
 Digest: sha256:77f340700d08fd45026823f44fc0010a5bd2237c2d049178b473cd2ad977d071
 Status: Downloaded newer image for nginx:alpine

如果镜像已经存在于您的 Docker 主机上,输出可能会有所不同。所有对象 ID 在您的系统中将不同,因为它们会自动为您创建。

我们可以通过执行 exit 命令或按下 Ctrl + C 键盘组合来退出正在运行的容器的标准输出。

我们被困在这个终端中,因为我们启动了一个以 Nginx 为主进程的容器。发生了什么呢?好吧,我们正在附着到容器的主进程上。如果我们按下 Ctrl + C 键组合,由于我们附着在该进程上,它会向容器的主进程发送中断信号,导致 nginx 进程结束。然而,如果我们打开另一个终端并列出正在运行的容器,它将按预期列出:

$ docker container ls
 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
 f84f6733537c nginx:alpine "nginx -g 'daemon of…" 11 seconds ago Up 10 seconds 80/tcp gallant_lederberg

由于我们没有为容器设置名称,它会随机生成一个名称;在此情况下是 gallant_lederberg

所有名称将通过随机组合的名称和形容词来创建。

我们还可以检查这个正在运行的容器以获取其当前的 IP 地址。要访问其信息,我们可以使用其 ID 或名称。我们将获得所有由 Docker 守护进程管理的对象信息。接下来,我们将查看 docker container inspect 命令输出中的 NetworkSettings 部分:

$ docker container inspect gallant_lederberg

 {
 "Id": "f84f6733537c3733bda67387b394cabce3f35cf7ee50a46937cb1f59f2a7a680",
 "Created": "2019-10-20T09:34:46.179017074Z",
 "Path": "nginx",
......
......
......
"NetworkSettings": {
 "Bridge": "",
 "SandboxID": "7bb519745e9b7becc806f36bc16b141317448388f7c19a3bd86e1bc392bea469",
 "HairpinMode": false,
......
......
"Gateway": "172.17.0.1",
 "IPAddress": "172.17.0.2",
......
......

这个输出显示容器已创建,并且正在我们的系统上运行,IP 为172.17.0.2。我们没有将其服务暴露给外部世界,尽管我们在之前的docker container ls输出中注意到它的端口和协议(80/tcp)。创建nginx:alpine镜像的人声明了这个端口来访问容器的主进程。我们不会继续在这里审查容器的网络方面,因为我们有一个完整的网络章节,[第四章,容器持久性与网络。只需了解我们在系统中运行着一个nginx进程,但它对用户不可访问:

$ ps -fea |grep -v grep |egrep -e nginx -e f84f67
 zero 1524 5881 0 11:34 pts/0 00:00:00 docker container run nginx:alpine
 root 1562 1693 0 11:34 ? 00:00:00 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/f84f6733537c3733bda67387b394cabce3f35cf7ee50a46937cb1f59f2a7a680 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc
 root 1594 1562 0 11:34 ? 00:00:00 nginx: master process nginx -g daemon off;
 systemd+ 1644 1594 0 11:34 ? 00:00:00 nginx: worker process
 systemd+ 1646 1594 0 11:34 ? 00:00:00 nginx: worker process
 systemd+ 1647 1594 0 11:34 ? 00:00:00 nginx: worker process
 systemd+ 1648 1594 0 11:34 ? 00:00:00 nginx: worker process

我们没有更改原始镜像中的任何参数,因此我们使用了镜像创建者的选项和声明的值。例如,nginx在容器内部以 root 身份运行。容器的端口80无法从桥接网络外部访问。

我们已经学过一些允许容器交互的参数,因此让我们启动一个简单的busybox容器,访问之前容器的服务:

$ docker run -ti busybox
 Unable to find image 'busybox:latest' locally
 latest: Pulling from library/busybox
 7c9d20b9b6cd: Pull complete
 Digest: sha256:fe301db49df08c384001ed752dff6d52b4305a73a7f608f21528048e8a08b51e
 Status: Downloaded newer image for busybox:latest
# wget http://172.17.0.2 -q -O -
 <!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>
/ # exit

在运行的nginx容器的输出中,我们将读取几行内容。这些是nginx的日志行,因为主nginx进程被重定向到标准输出。实际上,错误日志和访问日志都被重定向到容器的输出。如果我们回到第一个终端,这就是我们从运行nginx容器的标准输出和错误中得到的内容:

$ docker container run nginx:alpine
 172.17.0.3 - - [20/Oct/2019:10:26:56 +0000] "GET / HTTP/1.1" 200 612 "-" "Wget" "-"
 172.17.0.3 - - [20/Oct/2019:10:27:09 +0000] "GET / HTTP/1.1" 200 612 "-" "Wget" "-"

请注意,busybox容器的 IP(在第二个终端中运行)显示在nginx请求中。

我们已经了解到,在同一网络子网中运行两个容器会有无限制的访问权限。这是因为我们没有任何规则来禁止这种交互。两个容器都使用默认的桥接网络,因此它们运行在同一网络中。

如果我们通过在容器的 shell 中使用简单的exit命令退出busybox容器,我们将退出主进程(shell),因此容器会死亡。

我们可以通过使用--all-a来列出非运行中的容器,因为默认情况下,docker container ls只会显示正在运行的容器:

$ docker container ls --all
 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
 4848ed569f61 busybox "sh" 34 minutes ago Exited (0) 31 minutes ago interesting_yalow
 f84f6733537c nginx:alpine "nginx -g 'daemon of…" About an hour ago Up About an hour 80/tcp gallant_lederberg

在这里,我们可以看到可以查看运行和停止的容器。我们将停止gallant_lederberg容器(ID:f84f6733537c)。请记住,执行docker container stop时,会先尝试优雅地停止,然后才会杀死主进程:

$ docker stop gallant_lederberg
 gallant_lederberg

容器立即停止。现在,让我们运行另一个不容易停止的容器。我们可以运行一个执行无限 ping 命令到www.google.combusybox镜像,并查看我们尝试停止它时会发生什么:

$ docker container run --name ping busybox ping www.google.com
 PING www.google.com (172.217.16.228): 56 data bytes
 64 bytes from 172.217.16.228: seq=0 ttl=56 time=694.384 ms
 64 bytes from 172.217.16.228: seq=1 ttl=56 time=291.257 ms
 64 bytes from 172.217.16.228: seq=2 ttl=56 time=365.674 ms
 64 bytes from 172.217.16.228: seq=3 ttl=56 time=433.928 ms
 64 bytes from 172.217.16.228: seq=4 ttl=56 time=718.424 ms

我们已经通过传递的参数ping www.google.com修改了busybox镜像定义的 CMD。结果,我们将获得一个无限 ping 输出。为了停止这个容器并查看它需要多长时间才能停止,我们可以从另一个终端发送stop命令:

$ time docker container stop ping
 ping
 real 0m10,721s
 user 0m0,019s
 sys 0m0,032s

我们在 Docker 命令前添加了time来查看容器停止所需的时间。正如我们预期的那样,ping 进程需要被终止,因此stop命令的执行时间超过了默认的 10 秒。

我们使用--name参数启动了一个命名容器。为了确保容器名称的唯一性,一旦使用名称创建了容器,就无法创建另一个同名的容器。当我们进入本书的编排章节时,我们将学习如何使用编排工具管理容器名称。要部署另一个 ping 容器,在这种情况下,我们需要先使用docker container rm ping删除第一个 ping 容器。

我们已经了解了如何使用docker container run命令启动容器以及如何停止容器。现在让我们回顾一下容器创建过程,以便理解容器的生命周期:

$ docker container create --name webserver nginx:alpine
 6121184dd136781ceb87a210049b25334ce140968dd110ea7d6945ced3ca6668

我们获得了容器的标识符,但它并没有运行。我们可以通过执行docker container ls --filter name=webserver来验证这一点。

如果我们过滤所有容器,包括那些未运行的容器,我们可以看到容器已创建:

$ docker container ls --all --filter name=webserver
 CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
 6121184dd136        nginx:alpine        "nginx -g 'daemon of…"   2 minutes ago       Created                                 webserver

现在容器已经创建完成,我们可以使用docker container start启动它:

$ docker container start webserver
 webserver

容器已启动,但我们没有附加到其主进程的输入/输出。容器的创建与容器的运行是不同的。正如我们将要学习的,Docker Swarm 服务和 Kubernetes Pod 会创建容器配置,并且还会启动一定数量的副本。这与启动单个容器不同。

STATUS 列显示容器现在正在运行:

$ docker container ls --filter name=webserver
 CONTAINER         ID       IMAGE    COMMAND        CREATED     STATUS PORTS NAMES
 6121184dd136 nginx:alpine "nginx -g 'daemon of…" 10 minutes ago  Up     3 minutes 80/tcp webserver

我们可以通过向docker container run命令添加--attach参数来附加容器的输入/输出。这样,我们将以交互模式运行容器。记住,您与容器主进程的交互将取决于创建容器时传递的参数。我们也可以将--interactive作为start参数。

容器安全选项

有许多与容器安全相关的选项,用于容器的创建和执行。让我们通过一些示例来回顾最重要的几个选项:

--cap-add or --cap-drop 请记住,并非所有系统调用在容器内部都可用。我们可以使用此选项添加或删除默认的系统调用。例如,如果容器需要一些特殊的网络功能,比如创建接口或允许 1024 以下的端口,我们将添加NET_ADMIN能力。
--disable-content-trust 我们使用此选项来禁用任何内容信任验证(例如检查镜像的来源或所有权)。在生产环境中不推荐使用此选项。
--isolation 此选项仅在 MS Windows 容器中使用。允许的值为 processhyper-v。我们将选择容器中使用的隔离方式。记住,它们具有不同的特性,正如我们在第一章中学习的,《使用 Docker 构建现代基础设施和应用程序》
--privileged 特权容器将以具有所有功能并且没有任何资源限制的方式运行。使用此类容器时要小心,并始终尝试确定应用程序所需的功能,而不是使用特权模式。
--read-only 我们可以使用只读根文件系统来运行容器。一般来说,这是一个非常好的实践,但我们必须确保所有必需的容器存储将使用卷。
--security-opt 我们将能够在更改默认安全行为时更改容器选项;例如,使用不同的 seccomp 配置文件或指定容器将以未受限制的方式运行。自定义的 SELinux 策略也将使用此参数来通知 SELinux 非默认值。

所有在此描述的安全选项都必须谨慎使用。理解应用程序的功能或需求非常重要,而不是使用默认或不安全的配置。 |

理解执行特权模式容器将绕过所有资源限制非常重要。确保 --privileged 选项仅在您真正理解以所有功能和没有任何资源限制的方式运行容器的影响时使用。被允许执行特权容器的用户可以在没有 CPU 或内存限制的情况下运行进程,并且可以修改重要的系统文件。 |

在执行特权容器之前,花时间审查应用程序要求。仅在非常明确的情况下使用它们,并且要注意这些容器的任何可疑行为。 |

以只读模式执行容器非常有用。我们可以确保应用程序在其生命周期中不会发生变化。当然,使用只读模式取决于您的应用程序,但花点时间分析过程并尝试使其在只读文件系统中工作是很好的做法。我们将可写目录分离到临时卷中,以存储进程数据。这是提高安全性的一个非常好的做法。 |

使用主机命名空间 |

以下选项与安全性没有直接关系,但非常重要。它们与容器隔离有关,必须小心管理,因为任何滥用都可能导致重大安全问题: |

--ipc``--pid``--uts 如果需要,我们可以共享主机命名空间。例如,如果我们在容器内执行监控应用程序,并且需要能够监视主机进程,我们将使用 --pid host 包含主机的 pid 命名空间。注意这些选项,因为如果我们还使用额外的能力或特权模式,容器将能够管理主机进程。
--network 我们之前提到过这个选项,但在这个上下文中没有提到。我们可以使用主机网络。在这种情况下,我们将在容器内使用主机的网络。因此,所有主机接口都将在容器内可用。其他容器的接口也将被包括在内。
--userns 在第一章中,我们讨论了容器内的用户命名空间。当我们介绍容器的主要概念时,我们了解了进程隔离。此选项将允许我们在容器内实现一个隔离的用户命名空间。我们必须首先准备用户映射,然后在创建或执行容器时设置使用哪个映射。

我们可以在 Docker 主机上轻松验证一些提到的选项。例如,我们可以使用主机网络模式运行一个容器,并检索容器的接口:

$ docker container run busybox ip add
 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue 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
 37: eth0@if38: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
 link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
 inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
 valid_lft forever preferred_lft forever

现在,我们可以使用相同的镜像启动另一个容器,但使用主机网络:

$ docker container run --network=host busybox ip add
 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue 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
 inet6 ::1/128 scope host
 valid_lft forever preferred_lft forever
 2: enp0s25: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel qlen 1000
 link/ether 68:f7:28:c1:bc:13 brd ff:ff:ff:ff:ff:ff
 3: wlp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
 link/ether 34:02:86:e3:f6:25 brd ff:ff:ff:ff:ff:ff
 inet 192.168.200.161/24 brd 192.168.200.255 scope global dynamic wlp3s0
 valid_lft 49sec preferred_lft 49sec
 inet6 fe80::ee87:e44f:9189:f720/64 scope link
 valid_lft forever preferred_lft forever
 6: virbr1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue qlen 1000
 link/ether 52:54:00:f7:57:34 brd ff:ff:ff:ff:ff:ff
 inet 192.168.39.1/24 brd 192.168.39.255 scope global virbr1
 valid_lft forever preferred_lft forever
 ......

所有主机接口都可以在这个小型 busybox 容器内使用。这对于监控主机资源非常有用。这可以帮助我们解决主机网络问题,而无需安装任何软件,尤其是在生产环境中。

在下一节中,我们将学习如何与运行中的容器进行交互,在容器内执行新进程,以及将内容复制到容器或从容器复制。

与容器交互

我们可以与运行中的或已停止的容器进行交互。我们需要与容器交互,以在其中运行一些进程,查看其文件,或获取主进程的输出。这些是我们将用于与容器交互的主要操作:

attach 使用 attach,我们将能够连接到主进程的 STDIN/STDOUT/STDERR。换句话说,我们将附加到这个进程上与它交互。需要小心,因为用键盘发送信号可能会中断进程和容器的生命周期(我们可以通过 --sig-proxy false 来省略这种行为)。我们只能附加到正在运行的容器上。
cp 这个操作将允许我们向容器的文件系统发送/接收内容。它的作用与正常的复制相同,但我们可以使用 --archive 来保持文件的所有权。我们只需使用源路径和目标路径,并使用 <container>:</path_to_file> 符号来引用容器内的文件。在将文件复制到/从 Docker 主机时,容器可以是停止状态。
exec 使用exec,我们能够在容器的隔离环境中执行命令。这个新命令会继承所有主进程的命名空间。因此,这个新命令似乎在容器内运行,因为它们共享命名空间。
logs 我们可以通过访问容器的STDERRSTDOUT来查看容器的所有输出。通过使用日志驱动程序可以改进日志记录功能,例如将这些日志发送到外部主机或日志后端。在执行后台容器或服务时,日志记录是非常重要的。了解容器内部发生了什么,唯一的方法就是监控其日志。

一旦附加到容器,我们可以使用 Ctrl + P + Q 键盘组合来分离,但我们可以在附加容器时使用--detach-keys选项来更改此键盘组合,或者在创建或启动容器时进行更改。

现在我们将快速查看正在运行的容器(如果你没有任何容器,请按照上一节中的描述运行一个容器):

$ docker container ls
 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
 4b2806790a4f nginx:alpine "nginx -g 'daemon of…" 2 hours ago Up 40 minutes 80/tcp webserver

现在,我们使用docker exec在容器内执行ps -ef

$ docker container exec webserver ps -ef
 PID USER TIME COMMAND
 1 root 0:00 nginx: master process nginx -g daemon off;
 6 nginx 0:00 nginx: worker process
 7 nginx 0:00 nginx: worker process
 8 nginx 0:00 nginx: worker process
 9 nginx 0:00 nginx: worker process
 10 root 0:00 ps -ef

我们在容器的隔离环境内执行了该命令,使用的是主进程声明的用户(在本示例中为root)。

如果我们想执行交互式命令——例如一个 shell,可以通过指定--interactive(或-i)并使用--tty(或-t)分配伪终端来实现。我们可以使用--env为这个新进程设置环境变量,或者使用--user改变执行用户。如果我们需要在容器内以特殊权限执行新命令,还可以使用--privileged。这在测试环境中调试时非常有用:

$ docker exec -ti --user nginx --env ENVIRONMENT=test webserver /bin/sh
 / $ id
 uid=101(nginx) gid=101(nginx) groups=101(nginx)
 / $ env|grep ENVIRON
 ENVIRONMENT=test

我们可以使用docker container cp将主机/tmp目录中的文件复制到容器内:

$ docker container cp /tmp/TEST webserver:/tmp/TEST

正如我们之前提到的,日志记录是管理容器的一个重要方面。我们可以在运行或停止的容器上使用docker container logs。这些是非常有用的选项,可以改进日志显示的方式:

--follow-f 使用此选项,我们可以获取正在运行的容器的实时输出。每次有新条目时,输出将被更新。
--tail 使用此选项,我们可以指定要显示的前几行日志。默认情况下,将显示所有行。
--since--until 这两个选项对于仅显示某个时间戳或相对时间段(例如 30 分钟或 30m)之前或之后的日志非常有用。

现在,让我们回顾一下之前执行的 Web 服务器容器中的一些docker container logs参数。在以下示例中,我们将提取所有来自webserver容器输出的行:

$ docker container logs --tail all webserver
 172.17.0.3 - - [20/Oct/2019:18:39:52 +0000] "GET / HTTP/1.1" 200 612 "-" "Wget" "-"
 172.17.0.3 - - [20/Oct/2019:18:39:55 +0000] "GET / HTTP/1.1" 200 612 "-" "Wget" "-"
 172.17.0.3 - - [20/Oct/2019:18:39:57 +0000] "GET / HTTP/1.1" 200 612 "-" "Wget" "-"

在下一节中,我们将回顾如何通过限制容器访问主机资源来避免主机问题。

限制主机资源

我们已经看过了一些限制容器资源消耗的选项。我们将能够限制对 CPU、内存和块设备的访问。当我们关注内存资源时,有两种类型的限制:软限制和硬限制。

软限制将表示资源的预留。这意味着容器可以消耗超过声明值的内存,但该值将被预留。

另一方面,硬限制将确保不会消耗超过声明值的资源。实际上,如果超过此限制,容器将被终止。如果超出内存限制,内存不足(也称为 OOM)杀手会终止主进程以防止主机出现问题。

请记住,默认情况下,如果没有指定任何限制,容器将能够消耗所有主机资源。

有许多选项可确保资源访问的限制。我们可以通过这些参数自动修改默认的 cgroups 设置:

--cpu-period--cpu-quota CFS 是 Linux 内核的 CPU 调度程序,使用这些参数,我们可以修改调度程序的周期。两个参数都必须以微秒为单位配置,并将修改 CPU 限制。
--cpu-shares 此参数管理容器主进程的权重。默认情况下,它的值为 1024,我们可以通过增减该值来设置 CPU 周期的比例。这是一个软限制,这意味着 Docker 守护进程不会阻止容器在 Docker Swarm 上的调度。
--cpus-c 此选项帮助我们设置将提供给容器进程的可用 CPU 资源量。它与主机中可用的 CPU 数量相关。例如,在拥有三个 CPU 的主机上,使用 --cpus=1.5 的值将为该容器保证一半的 CPU 资源。
--cpuset-cpus 此 CPU 设置比 CPU 权重或设置使用多少 CPU 更简单。我们只需指定一个以逗号分隔的主机 CPU 列表,容器可以在这些 CPU 上运行(我们在写 CPU 范围时从 0 开始)。
--memory-m 这将设置容器进程可用的最大内存量。这是一个阈值,Docker 守护进程不会允许容器超过此限制。每当超过此限制时,内核将终止容器的主进程。我们将得到内存不足错误。这一过程被称为 oom-killer。我们可以使用 --oom-kill-disable 禁用 oom-killer。这可能会很危险,使用此选项时必须小心,因为容器可能会消耗掉所有主机的内存资源。
--memory-reservation 使用此参数,我们将为进程配置内存预留。它应该设置为低于之前提到的 --memory 阈值。
--blkio-weight--blkio-weight-device 第一个参数将管理容器可以使用的总块直接 I/O 带宽,而第二个参数将管理特定块设备可用的带宽。默认情况下,所有容器使用相同的带宽,值为 500,我们可以将其增加或减少,范围为 10 到 1000。

我们将要使用的许多功能都需要宿主机内核支持 Linux 能力。我们可以使用 docker system info 来查看所有被禁用的能力,并查找任何 WARNING 消息。

每当我们需要更新容器限制时,可以使用 docker container update 操作,它允许我们更改容器的内存、CPU 和块设备使用限制。

有一些操作可以帮助我们审查容器的资源使用情况。

我们将使用 docker container stats 来获取容器的使用指标。默认情况下,仅显示 CPU 使用百分比、内存使用及其限制、网络和块 I/O,以及容器内进程的数量。我们可以使用 --format 参数格式化输出,支持常见的 Go 语言格式模式。通常我们会使用表格格式:

$ docker stats --all --format "table [{{.Container}}] {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
 [CONTAINER] NAME CPU % MEM USAGE / LIMIT
 [8ab15ccdc42f] stress 0.00% 0B / 0B
 [ed19e4376cdc] intelligent_easley 0.00% 0B / 0B
 [0ca76903840f] vigilant_mendeleev 0.00% 0B / 0B
 [afa67a5a2162] inspiring_mclaren 0.00% 0B / 0B
 [49229db83166] mystifying_maxwell 0.00% 0B / 0B
 [4cef73c07691] naughty_diffie 0.00% 0B / 0B
 [5dcc40de271e] adoring_wright 0.00% 0B / 0B
 [07aeb6f9c6df] focused_fermi 0.00% 0B / 0B
 [bbe4cb0d9cac] magical_chaplygin 0.00% 0B / 0B
 [4b2806790a4f] webserver 0.00% 4.676MiB / 11.6GiB

我们可以指定容器的名称或 ID,只显示它的统计信息。需要注意的是,docker stats 是一个流式命令。这意味着除非我们使用 --no-stream 参数来获取静态输出,否则它将不断刷新内容并显示新数据。

根据显示的数据量,有时,值会被截断。这在许多其他对象的操作中也可能发生。为了避免重要数据的截断,我们可以在需要获取所有列数据时使用 --no-trunc

另一方面,docker container top 将以类似 top 的格式显示容器内部所有进程的信息。使用我们之前示例中的 Web 服务器,我们可以执行 docker container top webserver 来获取 nginx 主进程及其子进程的状态:

$ docker container top webserver
 UID PID PPID C STIME TTY TIME CMD
 root 17878 17848 0 19:06 ? 00:00:00 nginx: master process nginx -g daemon off;
 systemd+ 17924 17878 0 19:06 ? 00:00:00 nginx: worker process
 systemd+ 17925 17878 0 19:06 ? 00:00:00 nginx: worker process
 systemd+ 17927 17878 0 19:06 ? 00:00:00 nginx: worker process
 systemd+ 17928 17878 0 19:06 ? 00:00:00 nginx: worker process

我们可以使用 --memory-swap--memory-swappiness 来增加交换访问,但不推荐这么做。交换可能会降低应用程序性能,而且它会破坏分布式微服务的逻辑。调度将允许我们根据不同组件的需求在不同的节点上运行它们。

在接下来的部分,我们将回顾与图像相关的操作。通过这些操作,我们将能够从容器创建图像,正如我们在 第二章,构建 Docker 图像 中所学到的那样。

将容器转换为图像

我们已经学习了三种不同的构建图像的方法,它们都以某种方式使用了容器。现在让我们回顾一下可以用来创建图像的容器操作:

commit docker commit 将允许我们从容器创建镜像。我们将一个容器的层添加为新的镜像层。结果,我们获得一个新的镜像。我们将设置一个新的镜像名称(尽管我们学会了可以根据需要随时更改镜像名称)以及其标签。在提交过程中,容器将被暂停,以避免其执行期间的文件更改。
export 此操作将创建一个包含容器文件系统(包括其所有层的数据)的.tar文件。默认情况下,此命令将二进制内容流式传输到STDOUT,但我们可以使用--output-o来定义此内容的文件。

当我们需要了解我们对原始镜像层所做的更改时,我们可以使用docker container diff。这将显示修改或创建在容器层上的所有文件的列表。

使用前面示例中的容器 Web 服务器,我们可以观察到在其执行期间所做的所有更改:

$ docker container diff webserver
 C /var
 C /var/cache
 C /var/cache/nginx
 A /var/cache/nginx/client_temp
 A /var/cache/nginx/fastcgi_temp
 A /var/cache/nginx/proxy_temp
 A /var/cache/nginx/scgi_temp
 A /var/cache/nginx/uwsgi_temp
 C /root
 A /root/.ash_history
 C /run
 A /run/nginx.pid
 C /tmp
 A /tmp/TEST 

此列表显示已添加的文件,标记为A,以及已更改的文件和目录,标记为C。请注意,每次向目录添加文件时,目录也会更改。

我们通常会在 Docker 主机中部署数十、数百甚至数千个容器。重要的是能够检索关于它们的信息,以便管理其属性和状态。在下一节中,我们将审查一些用于在容器环境中格式化和过滤信息的选项。

格式化和过滤信息

对任何命令输出进行格式化和过滤始终是有用的。在 Docker 命令中,具有长列表或输出时,这确实是必要的。让我们开始格式化一些命令输出。

几乎所有表示或显示任何信息的操作都可以进行格式化。Docker 使用 Go 模板来修改输出格式。能够根据我们的特定需求格式化输出非常有用。在这里,我们将使用表格格式。每列将表示一个指定的键。

我们将考虑一个简短的示例输出,列出主机上所有部署容器,使用docker container ls以表格格式:

$ docker container ls --all --format "table {{.Names}}: {{.Image}} {{.Command}}" --no-trunc 
NAMES: IMAGE COMMAND
loving_diffie: alpine "/bin/sh"
recursing_fermi: alpine "/bin/sh"
silly_payne: centos "/bin/bash"
wonderful_visvesvaraya: centos "/bin/bash"
optimistic_lamarr: centos "/bin/bash"
focused_shtern: centos "/bin/bash"
stress: frjaraur/stress-ng:alpine "stress-ng stress-ng --vm 2 --vm-bytes 1G --timeout 60s"
vibrant_faraday: baseimage:development "curl"
lucid_wright: baseimage:production "curl"
elastic_cori: baseimage:production "env"

我们已使用--no-trunc来禁用打印数值的截断。如果不使用此选项,所有长字符串都会被截断,只显示少量字符。通常,这些字符足以识别值,但有时我们需要整个字符串;例如,用于查看容器的主要执行命令。

知道可以查询哪些键来进行格式化非常有用。为了获取所有允许用于格式化的键,我们将使用--format='{{json .}}'。这将显示指定操作的所有列或键(例如,尝试docker container ls --all --format='{{json .}}')。输出将显示为未格式化的 JSON。

未格式化的 JSON 输出不容易阅读。我们可以使用 jq (stedolan.github.io/jq/),这是一种命令行 JSON 处理器,便于阅读。使用 jq,我们将获得更漂亮的 JSON 格式化输出。

有许多定制化的格式化选项:

json 如我们所见,此选项将输出格式化为单行 JSON 字符串。例如,我们可以使用 --format='{{json .Config}}'docker inspect 输出容器,获取其所有配置的键和值。
table 表格格式选项并非在所有输出中都可用,但在列表中会表现得非常好。
join/split 使用这些选项,我们可以连接或拆分关键输出;例如,'{{json .Mounts}}''{{split .Image ":"}}'
lower/upper/title 这些选项允许我们将字符串转换为小写、大写或仅将首字母大写;例如,'{{title .Name}}' 将显示所有首字母大写的名称。
range 此选项将帮助我们格式化列表/数组值。你必须使用 '{{range <JSON keys> }}{{end}}' 来正确管理列出的值。
println 此选项将在新的一行中打印每个查询值。它对于格式化范围值非常有趣。

--pretty 选项可用于检查某些对象。它非常有用,但如前所述,并非所有对象都可用。例如,您可以使用它来检查服务,我们将在第八章中学习,使用 Docker Swarm 进行编排

格式化将帮助我们只获取所需的信息,但当我们必须处理大量项目时,将不太容易。我们将使用 --filter 选项来过滤信息,只检索匹配某些键和值的特定对象。并非所有键都可用于过滤。我们将使用带有其值的键进行过滤,并且可以根据需要使用多个过滤选项。如果我们添加多个相同键的过滤器,它们将作为 OR 使用。但如果我们使用不同的键,则将是 AND 过滤器。我们将使用“相等”(使用 =)或“不同”(使用 <>)来比较键值。

容器对象可以通过以下方式进行过滤:

  • ID 或名称:使用这些选项,我们可以通过容器的 ID 或名称来查找容器。

  • 标签:此情况特别,因为我们可以使用键来匹配所有带有该标签或键值格式的容器,以便找到该键的特定值。

  • 已退出:当使用 --all 过滤停止并带有错误的容器时,我们将使用带有退出整数的此选项,例如。

  • 状态:我们使用此选项按容器状态(createdrestartingrunningremovingpausedexiteddead)进行过滤。

  • 祖先:这一点非常重要,因为它将允许我们按镜像名称和标签进行过滤。

  • 之前/自此:此过滤器允许我们指定日期,例如,查找运行时间较长的容器或按其创建日期进行筛选。

  • 卷/网络:这个选项允许我们筛选使用卷或网络的容器。它对于删除旧资源非常有用。

  • 发布或暴露:这些选项过滤哪些容器正在发布或暴露指定的端口。我们可以使用一系列的端口和协议(<startport-endport>/[<proto>])。

  • 健康:此过滤器允许我们根据容器的健康检查状态(健康、不健康、启动中或无)进行筛选。

  • Is-task:这个选项非常有趣,因为它允许我们在使用 Docker Swarm 编排时,按任务创建的容器进行筛选。

注意,--format 用于过滤 docker <object> inspect 命令。我们只能查询特定的对象键和子键。例如,使用 --format='{{json .Config}}' 将只显示 Config 键下的键和值

在下一部分,我们将回顾如何将主机附加的设备像在容器内部一样使用。

管理设备

我们可以为容器提供主机设备的访问权限。我们使用 --device 参数与 docker container createdocker container run 一起使用来实现这一点。这样,我们将能够使用直接连接到主机的硬件设备,例如串口控制器、块存储或音频设备。

默认情况下,设备将具有读写权限。为了能够操作特殊设备,默认情况下还会添加 mknod 权限。我们可以通过在命令行中使用 rwm 来覆盖这些默认设置,作为 --device 选项的修饰符。

作为示例,我们可以将我们的 lvm 映射的块设备挂载到定义的目录;请注意,必须添加挂载功能。在这个例子中,我们添加了 SYS_ADMIN 功能:

$ docker run -ti --cap-add SYS_ADMIN --device /dev/mapper/centos-root:/dev/sdx centos
 [root@5ccb0ef8ce84 /]# mkdir /data
 [root@5ccb0ef8ce84 /]# mount /dev/sdx /data
 [root@5ccb0ef8ce84 /]# cd /data
 [root@5ccb0ef8ce84 data]# ls
 bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  vagrant  var

在以下示例中,我们在容器内使用我们的主机声音设备。将这些设备添加到容器中将允许我们运行带有声音的应用程序:

$ docker container run -ti --device /dev/snd alpine
 / # apk add --update -q alsa-utils
 / # speaker-test -t wav -c 6 -l1

 speaker-test 1.1.9

 Playback device is default
 Stream parameters are 48000Hz, S16_LE, 6 channels
 WAV file(s)
 Rate set to 48000Hz (requested 48000Hz)
 Buffer size range from 2048 to 16384
 Period size range from 1024 to 1024
 Using max buffer size 16384
 Periods = 4
 was set period_size = 1024
 was set buffer_size = 16384
 0 - Front Left
 1 - Front Right
 2 - Unused
 3 - Unused
 4 - Unused
 5 - Unused
 Time per period = 8.298695

在这里,我们了解到不仅仅是文件或目录可以在容器内部访问。我们可以像直接连接到容器一样使用特殊设备。

本章实验

在本章的实验中,我们将运行容器并与它们互动。我们还将回顾一些示例,限制它们的资源,格式化和过滤命令输出。

若要运行这些实验,请从本书的 GitHub 仓库中部署 environments/standalone-environmentgithub.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git),如果您还没有这样做。您也可以使用自己的 CentOS 7 服务器。从 environments/standalone-environment 文件夹中使用 vagrant up 启动虚拟环境。

如果你正在使用standalone-environment,请等待它启动。我们可以使用vagrant status检查节点的状态。使用vagrant ssh standalone连接到你的实验节点。standalone是你节点的名称。你将使用vagrant用户,并通过sudo获取管理员权限。你应该得到如下输出:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant up
Bringing machine 'standalone' up with 'virtualbox' provider...
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant status
Current machine states:
standalone running (virtualbox)
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$

现在,我们可以使用vagrant ssh standalone连接到standalone节点。如果你之前已经部署过standalone虚拟节点,并且只是通过vagrant up启动它,过程可能有所不同:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant ssh standalone
[vagrant@standalone ~]$ 

如果你正在重用standalone-environment,这意味着 Docker Engine 已经安装。如果你启动了一个新的实例,请执行/vagrant/install_requirements.sh脚本,以确保你拥有所有必需的工具(Docker Engine 和 docker-compose):

[vagrant@standalone ~]$ /vagrant/install_requirements.sh 

现在,你已准备好开始实验。

审查 Docker 命令行对象选项

Docker 命令行将允许我们与 Docker 守护进程交互。我们将使用 Docker 对象或资源及其允许的操作。在下面的截图中,我们可以轻松地查看 Docker help命令输出中的这种行为:

对象将在第一部分中出现,之后是常见的选项。在底部,我们将看到所有允许的选项。正如我们在本章中提到的,并非所有对象都有相同的操作。本章专门讨论容器。那么,让我们回顾容器允许的操作(输出被截断):

[vagrant@standalone ~]$ docker container --help
Usage: docker container COMMAND
Manage containers
Commands:
 attach Attach local standard input, output, and error streams to a running container
 commit Create a new image from a container's changes
.....
.....
.....
 unpause Unpause all processes within one or more containers
 update Update configuration of one or more containers
 wait Block until one or more containers stop, then print their exit codes
Run 'docker container COMMAND --help' for more information on a command.

我们应该在每种对象上使用--help来查看它们可用的操作。如果我们没有设置任何DOCKER_HOST变量(也没有使用-H),我们将与本地 Docker 守护进程交互。我们将在命令行中使用这些参数来连接远程守护进程。

事实上,有许多知名的 Docker 命令行别名:

  • docker run: docker container run

  • docker ps: docker container ls

  • docker rm: docker container rm

  • docker start/stop: docker container start/stop

  • docker port: docker container port

  • docker rmi: docker image rm

建议使用长命令行术语,因为它们实际上指示了一个对象和一个操作。这将避免混淆或拼写错误。

执行容器

这是一个长时间的实验,我们将回顾容器的许多操作和选项。让我们开始吧:

  1. 在后台基于 Alpine 镜像执行一个交互式容器:
[vagrant@standalone ~]$ docker container run -ti -d alpine
aa73504ba37299aa7686a1c5d8023933b09a0ff13845a66be0aa69203eea8de7
  1. 现在,我们将查看并重命名容器myalpineshell
[vagrant@standalone ~]$ docker container ls -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aa73504ba372 alpine "/bin/sh" About a minute ago Up About a minute elastic_curran

我们使用-l--last来获取在 Docker 主机上执行的最新容器。请注意,接下来的代码中我们将使用-q来获取容器的 ID。

现在,我们使用容器的 ID 来重命名之前启动的容器:

[vagrant@standalone ~]$ docker container rename $(docker container ls -ql) myalpineshell

如果我们再次查看最新的容器,我们会看到它有一个不同的名称。注意,容器正在运行(输出将显示不同的日期):

[vagrant@standalone ~]$ docker container ls -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aa73504ba372 alpine "/bin/sh" 11 minutes ago Up 11 minutes myalpineshell
  1. 我们将终端附加到正在运行的myalpineshell容器,并在/tmp目录下创建一个名为TESTFILE的空文件。然后,我们从容器中exit
[vagrant@standalone ~]$ docker container attach myalpineshell
/ # touch /tmp/TESTFILE
/ # exit
  1. 如果我们再次查看容器状态,我们会发现它已经停止,但退出状态是正确的:
[vagrant@standalone ~]$ docker container ls -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aa73504ba372 alpine "/bin/sh" 14 minutes ago Exited (0) 46 seconds ago myalpineshell

该容器现在显示Exited (0)状态。Alpine 镜像的主进程是一个 shell。其 CMD 是/bin/sh。我们通过执行exit命令退出。因此,退出状态是0,执行过程中没有发现问题。

  1. 现在,我们将通过执行一个不存在于镜像中的命令来强制使容器失败。例如,我们将在一个新容器中执行curl命令:
[vagrant@standalone ~]$ docker container run alpine curl www.google.com
docker: Error response from daemon: OCI runtime create failed: container_linux.go:345: starting container process caused "exec: \"curl\": executable file not found in $PATH": unknown.
ERRO[0001] error waiting for container: context canceled 

由于curl二进制文件不存在,我们甚至无法执行所需的命令。因此,容器已创建但未执行:

[vagrant@standalone ~]$ docker container ls -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
466cc346e5d3 alpine "curl www.google.com" 17 seconds ago Created fervent_tharp
  1. 现在,我们将在新容器中执行ls -l /tmp/TESTFILE
[vagrant@standalone ~]$ docker container run alpine ls -l /tmp/TESTFILE
ls: /tmp/TESTFILE: No such file or directory

[vagrant@standalone ~]$ docker container ls -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7c328b9a0609 alpine "ls -l /tmp/TESTFILE" 8 seconds ago Exited (1) 6 seconds ago priceless_austin

如预期的那样,/tmp/TESTFILE文件在这个新容器中不存在。我们只在myalpineshell容器中创建了它。实际上,文件仍然在那里。请注意,这次容器被执行了,且退出状态显示为错误代码。这是执行ls命令时因文件不存在而导致的退出代码。

  1. 让我们再次重命名最后执行的容器:
[vagrant@standalone ~]$ docker container rename $(docker container ls -ql) secondshell
  1. 现在,我们将在主机文件系统上创建/tmp/TESTFILE文件,并将其复制到secondshell容器中:
[vagrant@standalone ~]$ touch /tmp/TESTFILE

[vagrant@standalone ~]$ docker container cp /tmp/TESTFILE secondshell:/tmp/TESTFILE

无法使用docker container cp从一个容器复制文件到另一个容器。

  1. 现在,让我们重新启动secondshell容器并观察新的结果:
[vagrant@standalone ~]$ docker container start secondshell
secondshell

[vagrant@standalone ~]$ docker container ls -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7c328b9a0609 alpine "ls -l /tmp/TESTFILE" 32 minutes ago Exited (0) 4 seconds ago secondshell

该文件现在存在于secondshell容器内,因此执行正确退出。我们可以在STATUS列中看到这一新结果(Exited (0))。我们通过将文件复制到已停止的容器中,操作了一个死容器。因此,容器仍然存在于主机系统中,直到我们将其删除。

  1. 现在,我们将删除secondshell容器,并尝试过滤容器列表的输出。我们将搜索secondshellmyalpineshell容器:
[vagrant@standalone ~]$ docker container rm secondshell
secondshell

[vagrant@standalone ~]$ docker container ls --all --filter name=myalpineshell --filter name=secondshell
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aa73504ba372 alpine "/bin/sh" 59 minutes ago Exited (0) 45 minutes ago myalpineshell

如预期的那样,我们只得到了myalpineshell容器。

  1. 为了完成这个实验,我们将再次启动myalpineshell容器,使用docker container start -a -i将终端附加到已启动的容器。然后,我们将使用Ctrl + P + Q逃脱序列将容器发送到后台。最后,我们将使用docker container exec命令附加第二个 shell 到容器:
[vagrant@standalone ~]$ docker container start -a -i myalpineshell
<PRESS Ctrl+p+q>
/ # read escape sequence

[vagrant@standalone ~]$ docker container exec -ti myalpineshell sh
/ # ps -ef
PID USER TIME COMMAND
 1 root 0:00 /bin/sh
 6 root 0:00 sh
 11 root 0:00 ps -ef
/ # exit

我们可以观察到,退出新执行的 shell 进程并不会终止myalpineshell容器。两个进程共享相同的命名空间:

[vagrant@standalone ~]$ docker container ls --all --filter name=myalpineshell
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aa73504ba372 alpine "/bin/sh" About an hour ago Up 4 minutes myalpineshell

限制容器资源

在这个实验中,我们将使用来自 Docker Hub 的frjaraur/stress-ng:alpine镜像。该镜像基于 Alpine Linux,并安装了stress-ng包。它很小,有助于我们对容器施加压力。

我们将从测试内存限制开始。在这个实验中,我们将在同一主机上使用两个终端。在第一个终端中,我们将启动 docker container stats。在所有实验过程中保持这个终端运行,因为在这个终端中,我们将观察不同的行为。

在第二个终端中,我们将启动两个容器,这两个容器将尝试消耗 2 GB 内存。我们将使用 --vm 2 --vm-bytes 1024M 创建两个进程,每个进程分配 1,024 MB 内存:

  1. 我们将启动一个具有内存预留的容器。这意味着 Docker 守护进程将为该容器预留至少该数量的内存。请记住,这不是限制;这是预留:
[vagrant@standalone ~]$ docker container run --memory-reservation=250m --name 2GBreserved -d frjaraur/stress-ng:alpine --vm 2 --vm-bytes 1024M
b07f6319b4f9da3149d41bbe9a4b1440782c8203e125bd08fd433df8bac91ba7
  1. 现在,我们将启动一个限制内存的容器。只允许 250 MB 内存,尽管该容器会尝试消耗 2 GB:
[vagrant@standalone ~]$ docker container run --memory=250m --name 2GBlimited -d frjaraur/stress-ng:alpine --vm 2 --vm-bytes 1024M
e98fbdd5896d1d182608ea35df39a7a768c0c4b843cc3b425892bee3e394eb81
  1. 在第一个终端中,我们运行 docker container stats 来查看容器的资源消耗。我们会看到类似这样的内容(ID 和使用情况会有所不同):
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
b07f6319b4f9 2GBreserved 203.05% 1.004GiB / 11.6GiB 8.65% 6.94kB / 0B 0B / 0B 5
e98fbdd5896d 2GBlimited 42.31% 249.8MiB / 250MiB 99.94% 4.13kB / 0B 1.22GB / 2.85GB 5

如果你收到关于限制资源的警告消息,这是正常的。WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap. 这条消息表示你的操作系统不会限制容器的 swap。在 Debian/Ubuntu 上,默认情况下它是禁用的。

我们可以观察到没有限制的容器正在消耗超过指定的内存。在第二个案例中,容器限制为 250 MB,尽管进程可以消耗更多内存,但它被限制在 250 MB 内,不会超过此限制。正如我们在 MEM USAGE/LIMIT MEM 列中看到的,它最多只能达到其限定的内存,但不能超过这个限制。

  1. 删除 2GBreserved2GBlimited 容器:
[vagrant@standalone ~]$ docker container rm -f 2GBlimited 2GBreserved
2GBlimited
2GBreserved

现在,我们将限制 CPU 消耗。

  1. 我们将启动三个具有不同 CPU 限制和进程要求的容器。第一个容器限制为一个 CPU,但需要两个 CPU。这不是一个真实的需求,但如果系统中有两个 CPU,进程将尝试使用两个 CPU:
[vagrant@standalone ~]$ docker container run -d --cpus=1 --name CPU2vs1 frjaraur/stress-ng:alpine --cpu 2 --timeout 120 

第二个容器限制为两个 CPU,并且需要两个 CPU。在执行过程中,它将尝试使用这两个 CPU:

[vagrant@standalone ~]$ docker container run -d --cpus=2 --name CPU2vs2 frjaraur/stress-ng:alpine --cpu 2 --timeout 120

第三个容器限制为四个 CPU,且需要两个 CPU。在这种情况下,进程可以使用四个 CPU,但由于它们只使用两个 CPU,因此除非我们尝试使用超过四个 CPU,否则不会有真正的限制:

[vagrant@standalone ~]$ docker container run -d --cpus=4 --name CPU2vs4 frjaraur/stress-ng:alpine --cpu 2 --timeout 120
  1. 如果我们观察 Docker 容器的统计输出,我们可以确认预期结果:
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT    MEM %               NET I/O             BLOCK I/O           PIDS
0dc652ed28b0        CPU2vs4             132.47%             7.379MiB / 11.6GiB   0.06%               4.46kB / 0B         0B / 0B             3
ec62ee9ed812        CPU2vs2             135.41%             7.391MiB / 11.6GiB   0.06%               5.71kB / 0B         0B / 0B             3
bb1034c8b588        CPU2vs1             98.90%              7.301MiB / 11.6GiB   0.06%               7.98kB / 0B         262kB / 0B          3

到此为止,我们已经回顾了如何限制容器的资源。我们通过 docker container stats 测试了 CPU 和内存的使用情况,将它们推向了预定的限制。

现在,让我们通过一些实验来回顾格式化和过滤。

格式化和过滤容器列表输出

在这个实验中,我们将回顾 docker container ls 输出。让我们开始吧:

  1. 启动多个容器。在这个示例中,我们将运行三个 nginx:alpine 实例,并使用顺序名称:
[vagrant@standalone ~]$ docker run -d --name web1 --label stage=production nginx:alpine
bb5c63ec7427b6cdae19f9172f5b0770f763847c699ff2dc9076e60623771da3

[vagrant@standalone ~]$ docker run -d --name web2 --label stage=development nginx:alpine
4e7607f3264c52c9c14b38412c95dfc8c286835fd1ffab1d7898c5cfab47c9b8

[vagrant@standalone ~]$ docker run -d --name web3 --label stage=development nginx:alpine
fcef82c80ed0b049705609885bc9c518bf062a39bbe2b6d68b7017bcc6dcaa14
  1. 让我们使用docker container ls默认输出列出正在运行的容器:
[vagrant@standalone ~]$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fcef82c80ed0 nginx:alpine "nginx -g 'daemon of…" About a minute ago Up 59 seconds 80/tcp web3
4e7607f3264c nginx:alpine "nginx -g 'daemon of…" About a minute ago Up About a minute 80/tcp web2
bb5c63ec7427 nginx:alpine "nginx -g 'daemon of…" About a minute ago Up About a minute 80/tcp web1
  1. 由于我们希望能够查看容器的当前状态,因此我们可以格式化输出,使其包括标签信息:
[vagrant@standalone ~]$ docker container ls \
--format "table {{.Names}} {{.Command}}\\t{{.Labels}}" 
NAMES COMMAND                 LABELS
web3 "nginx -g 'daemon of…"   maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>,stage=development
web2 "nginx -g 'daemon of…"   stage=development,maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>
web1 "nginx -g 'daemon of…"   stage=production,maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>
  1. 现在,让我们只过滤出开发容器(stage=development):
[vagrant@standalone ~]$ docker container ls --format "table {{.Names}} {{.Command}}\\t{{.Labels}}" --filter label=stage=development
NAMES COMMAND LABELS
web3 "nginx -g 'daemon of…" maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>,stage=development
web2 "nginx -g 'daemon of…" maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>,stage=development
  1. 现在,让我们使用列表输出杀死那些开发容器:
[vagrant@standalone ~]$ docker container kill $(docker container ls --format "{{.ID}}" --filter label=stage=development)

[vagrant@standalone ~]$ docker container ls --format "table {{.Names}}\\t{{.Labels}}"
NAMES               LABELS
web1                maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>,stage=production

只有被标记为productionweb1仍在按预期运行。

过滤和格式化非常有用。练习这些方法,因为它们对 Docker 认证助理考试非常重要。

总结

本章专门讲解了 Docker 命令行和运行容器。我们发现了一个强大的命令行工具,它允许我们从镜像工件创建容器,在主机之间共享它们,并执行已经构建好的应用组件。

我们学习了如何与不同的 Docker 对象进行交互,以及在独立 Docker 主机环境和编排环境中有哪些可用的对象。

然后,我们回顾了如何创建、执行、暂停/恢复、停止或杀死容器。它们会一直存在于我们的 Docker 主机中,直到被从系统中移除。我们还学习了如何操控容器的执行行为以及它们如何在网络中存在。为了提高安全性,我们介绍了许多选项,并且还学习了以只读模式执行容器是如何非常有用的。

限制容器资源是生产环境中的必要操作。默认情况下,它们可以消耗主机的所有资源,这可能非常危险。我们学习了如何通过软硬限制来避免这种情况,确保我们的应用程序可以在具有足够资源的主机上运行,并且不会干扰其他应用。

在动态环境中部署应用时,需要格式化和过滤特定信息。我们学习了格式化和过滤操作如何帮助我们检索特定的信息。

本章最后,我们学习了如何将主机的设备作为直接附加到容器的设备来使用。

在下一章,我们将讨论容器的持久性和它们的网络功能。

问题

  1. 以下哪个选项不适用于容器?

a) build b) update c) destroy d) create

  1. 以下哪个句子是错误的?

a) 容器的生命周期通过startstop命令来管理

b) 容器总是在 10 秒后停止

c) 容器可以先创建然后启动

d) 容器生命周期内创建的卷必须手动删除,除非我们在删除容器时使用-v选项

  1. 以下哪个句子是关于docker kill的正确描述?

a) 它会杀死所有容器进程

b) 它会向容器的主进程发送一个SIGKILL信号

c) 它会在容器被杀死后移除容器

d) 它默认会等待 10 秒钟再真正杀死容器

  1. 我们执行了一个名为webserver的容器。以下哪一句是错误的?

a) 可以使用docker container rm --force删除它。 b) 我们可以使用docker container update更新它的镜像。 c) 我们可以使用docker container renamewebserver容器重命名为websrv。 d) 我们可以使用docker container logs查看容器的输出。

  1. 我们执行了docker container run --name app1 --user 1000 --memory 100m --privileged alpine touch /testfile命令。以下哪一句是正确的?

a) /testfile作为 root 用户创建,因为容器执行时具备所有权限。

b) 容器无法使用超过 100m 的主机内存。

c) /testfile没有创建,因为我们使用了 ID 为1000的用户,并且该用户无法在根目录(/)写入。

d) 我们使用了--privileged选项。这个选项将禁用容器内所有的 root 权限,因此无法创建文件。

进一步阅读

请参考以下链接,了解本章涉及的主题:

容器的持久性和网络

容器是运行在主机上的进程。这看起来很简单,但在一组节点上如何实现呢?如果我们追求高可用性,能够在池中的任何主机上运行容器将确保在任何地方都能执行。但这种方法需要在我们的应用程序中一些特殊的逻辑。我们的应用必须完全可移植,并避免与任何主机之间的摩擦和依赖关系。具有大量依赖的应用总是更难移植。我们需要找到一种方法来管理容器的状态数据。本章将回顾不同的持久化策略。

另一方面,前述的主机池必须能够与所有容器进行通信。在本章中,我们将学习基本的独立主机网络,并介绍高级集群编排网络概念。

在本章中,我们将讨论无状态和有状态应用程序之间的差异,卷如何工作以及我们如何使用它们,以及 Docker 守护进程如何在独立环境中提供网络功能。我们还将考虑容器之间的交互以及如何发布容器内运行的进程提供的服务。

本章将涵盖以下主题:

  • 了解无状态容器和有状态容器

  • 了解不同的持久化策略

  • 容器中的网络

  • 了解容器交互

  • 发布应用程序

让我们开始吧!

第六章:技术要求

在本章中,我们将学习 Docker 卷和网络概念。在本章的最后,我们将提供一些实验,帮助你理解并学习所展示的概念。这些实验可以在你的笔记本电脑或 PC 上运行,使用提供的 Vagrant 独立环境或你自己已部署的任何 Docker 主机。你可以在本书的 GitHub 仓库中找到更多信息:github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

查看以下视频,看看代码如何运行:

"bit.ly/34DJ3V4"

了解无状态容器和有状态容器

可移植性是现代应用程序的关键,因为它们应该能够在每个环境中运行(无论是本地还是云端)。容器为这些情况做好了准备。我们还将追求生产环境中应用的高可用性,而容器也将在这里帮助我们。

不是所有的应用程序默认都可以使用容器。进程的状态和数据在容器内管理起来很困难。

在第一章,现代基础设施和基于 Docker 的应用程序中,我们了解到容器并不是短暂的。它们存在于我们的主机中。容器被创建、执行并停止或杀死,但它们会一直存在于主机中,直到被删除。我们可以重新启动之前停止的容器。但这仅适用于独立环境,因为所有信息都存储在主机数据路径定义的目录下(在 Linux 和 Windows 中,默认路径分别是/var/lib/dockerC:\ProgramData\docker)。如果我们将工作负载(即作为容器运行的应用组件)迁移到另一台主机上,我们将无法在那儿获取它们的数据和状态。如果我们需要升级它们的镜像版本,会发生什么呢?在这种情况下,我们可以运行一个新的容器,所有内容将重新创建。我们可以启动一个新的容器,但我们需要保持所有的应用数据。

之前,我们介绍了卷作为绕过容器内部文件系统及其生命周期的一种方法。卷中的一切,实际上都在容器的文件系统之外。这将帮助我们通过直接访问主机的设备来提升应用性能,同时它也能保存数据。即使容器被移除,卷也会保持存在(除非在移除时使用--volumes-v选项)。因此,卷有助于我们本地维护应用数据,但如果需要在其他 Docker 主机上执行怎么办?我们可以共享镜像,但除非我们也能在它们之间共享卷,否则容器相关的数据将无法存在。

在这些情况下,无状态进程——那些不需要任何持久数据来工作的进程——更容易管理。这些进程总是容器运行的候选者。

那么对于有状态的进程——那些在执行之间使用持久数据的进程,怎么办呢?在这种情况下,我们必须小心。我们应该提供外部卷或数据库来存储进程的状态及其所需数据。当我们设计基于微服务的应用架构时,这些概念是非常重要的。

让我们深入了解卷是如何工作的。

学习卷是如何工作的

之前,我们学习了如何在镜像中定义卷,以简单地绕过容器的文件系统。以下是一个简单的 Dockerfile 定义,展示了定义的卷(这是 PostgreSQL 数据库官方镜像的摘录):

FROM alpine:3.10
RUN set -ex; \
 postgresHome="$(getent passwd postgres)"; \
 postgresHome="$(echo "$postgresHome" | cut -d: -f6)"; \
 [ "$postgresHome" = '/var/lib/postgresql' ]; \
 mkdir -p "$postgresHome"; \
 chown -R postgres:postgres "$postgresHome"
...
...
RUN mkdir -p /var/run/postgresql && chown -R postgres:postgres /var/run/postgresql && chmod 2777 /var/run/postgresql
ENV PGDATA /var/lib/postgresql/data
RUN mkdir -p "$PGDATA" && chown -R postgres:postgres "$PGDATA" && chmod 777 "$PGDATA"

VOLUME /var/lib/postgresql/data

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 5432
CMD ["postgres"]

我们省略了许多行,因为我们只想回顾 VOLUME 的定义。在这种情况下,存储在/var/lib/postgresql/data 目录下的所有数据将位于容器文件系统之外。这是一个未命名卷的定义,它将在我们运行使用此镜像的容器时,通过随机 ID 在系统中进行标识。此卷的定义是为了绕过写时复制文件系统。每次创建或运行新的容器时,都将创建一个新的随机标识符卷。这些卷应当手动删除,或者在删除关联容器时使用 --volume-v 选项来删除。

现在是时候定义我们可以在 Docker 中使用的不同类型的卷了:

  • 未命名卷:这些是在镜像上定义的卷,因此使用随机标识符创建它们。由于它们没有名称,因此很难在本地文件系统上跟踪它们。由于卷可能会随着应用程序的增长而快速增大,因此在本地系统上运行任何镜像之前,检查卷定义非常重要。记住,未命名的卷将会在 Docker 数据根路径下增长,无论该路径位于何处。

  • 命名卷:这些是我们手动创建的卷。正如我们在第一章《现代基础设施与 Docker 应用》中学到的那样,卷是 Docker 对象,我们可以对其进行一些操作来控制它们。在这一章中,我们将学习与它们相关的操作以及如何使用它们。这些卷也会位于数据根路径下,但我们可以使用不同的插件或驱动程序来创建它们。驱动程序将允许本地或远程卷,例如通过 NFS。在这些情况下,我们在数据根路径下看到的将是指向实际挂载的远程文件系统的链接。因此,如果这些卷是远程的,它们将不会消耗本地存储。

  • 本地主机目录或文件:在这种情况下,我们将在容器内使用主机的目录和文件。我们通常将这些卷称为绑定挂载。我们必须小心文件和目录的权限,因为我们也可以在容器内使用任何特殊文件(包括设备)。如果权限设置得过于开放,将使用户能够访问主机设备。它们将需要适当的进程能力和权限。重要的是要理解,Docker 不关心块设备、目录和文件系统在 Docker 主机上的挂载方式。它们总是像本地可用一样被使用。绑定挂载不会作为卷列出。

  • tmpfs 卷:这种类型的卷是临时的。它们只会在主机内存中保持。当容器停止时,卷将被移除。它们内部的文件不会持久化。

各种类型的卷可以以只读模式挂载到容器内。当卷数据不应该被运行中的进程修改时,这非常重要且有用,例如在提供静态网页内容时。我们可以有一些容器应该能够修改数据,另一些容器则只会读取并以只读模式提供这些已修改的数据。

命名卷或绑定挂载将保留数据。未命名卷将在新容器中创建。请记住这一点。如果我们需要为未命名卷提供一些数据,应该在容器启动时进行。我们还可以在镜像定义中定义一个过程。这个概念非常重要,因为VOLUME定义在 Dockerfile 中的位置非常关键。正如我们在第二章《构建 Docker 镜像》中学到的那样,镜像的创建是基于容器执行的顺序。如果我们为特定路径添加一个卷,所有后续的执行将不会在该目录中保留数据。构建过程将在每个新容器上创建一个新的未命名卷,执行之间的内容不会被复用。

学习卷对象操作

卷可以被创建、使用和移除。我们还可以检查它们的所有属性。下表展示了卷对象的可执行操作:

对象 操作
create 我们能够创建命名卷。我们可以添加标签以过滤列出的输出,正如我们在之前的章节中学到的那样。我们可以指定用于创建新卷的驱动程序。默认情况下,卷将使用本地驱动程序。该驱动程序将在volumes目录下创建目录。每个新卷将拥有自己的目录,包含所需的元信息和一个_data子目录。该目录包含所有添加到卷中的文件。如前所述,一些驱动程序将提供主机外部存储资源。链接目录将提供连接信息而不是其数据。我们将使用--driver来指定除local之外的其他驱动程序。--opt-o参数允许我们为指定的驱动程序添加所需的选项。每个驱动程序都有自己的特殊选项。
inspect 所有对象都可以被检查。在这种情况下,inspect操作将提供有关对象位置、使用的驱动程序和提供的标签的信息。
ls 我们可以使用ls操作列出所有卷。本书中学到的几乎所有过滤和格式化选项都可以应用。格式化也将取决于给定卷的属性。
prune prune选项将帮助我们进行卷的清理。它将移除所有未被任何容器使用的创建卷。它不会删除任何绑定挂载,因为这些并不被真正视为卷。
rm 我们可以使用rm操作来删除卷。需要注意的是,附加到现有容器的卷无法被删除。必须先删除容器,然后才能删除卷。或者,您可以在容器删除时使用--volumes选项。

现在,让我们介绍容器如何使用卷。

在容器中使用卷

首先,我们将从未命名的卷开始。这些卷是容器镜像中定义的卷。如前所述,在执行前一定要检查镜像。如果我们运行一个将大量数据存储在预定义的未命名卷上的应用程序,我们的 Docker 主机可能会耗尽磁盘空间。因此,检查运行的镜像和所需的资源非常重要。例如,如果我们快速查看postgres:alpine镜像(基于 Alpine Linux 的 PostgreSQL 数据库镜像),我们会发现有卷定义(我们首先从 Docker Hub 拉取postgres:alpine镜像):

$ docker image pull --quiet postgres:alpine
docker.io/library/postgres:alpine

$ docker image inspect postgres:alpine --format "{{ .Config.Volumes }} "
map[/var/lib/postgresql/data:{}] 

如我们所见,postgres:alpine将定义一个未命名的卷,以绕过写时复制容器文件系统,允许进程在/var/lib/postgresql/data目录下写入或修改任何内容。

让我们创建一个名为mydb的容器,使用postgres:alpine镜像:

$ docker container run -d --name mydb postgres:alpine
e1eb5e5df725541d6a3b31ee86746ab009251c5292b1af95b22b166c9d0922de

现在,我们可以检查mydb容器,查找它的挂载点(在您的系统中标识符会有所不同):

$ docker container inspect mydb --format "{{ .Mounts }} "
[{volume c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f /var/lib/docker/volumes/c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f/_data /var/lib/postgresql/data local true }] 

使用获取的卷标识符,我们可以查看它的属性:

$ docker volume inspect c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f
[
 {
 "CreatedAt": "2019-11-03T19:20:59+01:00",
 "Driver": "local",
 "Labels": null,
 "Mountpoint": "/var/lib/docker/volumes/c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f/_data",
 "Name": "c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f",
 "Options": null,
 "Scope": "local"
 }
]

输出显示了该卷在主机上挂载的位置(/var/lib/docker/volumes/c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f/_data)以及哪个容器在使用它并将其挂载在(/var/lib/postgresql/data)的位置。

如果我们查看/var/lib/docker/volumes/c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f/_data目录,我们可以列出所有 PostgreSQL 数据库数据文件(请注意,以下日志中的目录归 root 所有,因此需要 root 权限访问):

$ sudo ls -lart /var/lib/docker/volumes/c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f/_data
total 64
drwxr-xr-x 3 root root 19 nov 3 19:20 ..
-rw------- 1 70 70 3 nov 3 19:20 PG_VERSION
drwx------ 2 70 70 6 nov 3 19:20 pg_twophase
...
...
-rw------- 1 70 70 94 nov 3 19:20 postmaster.pid
drwx------ 2 70 70 25 nov 3 19:42 pg_stat_tmp

请注意,文件和目录的所有者是userid70)和groupid70)。这是因为容器的主进程不是以 root 用户身份运行,因此所有由 PostgreSQL 进程创建的文件都将归一个内部的postgres:postgres用户所有,该用户的 ID 可能与我们的主机不同,甚至可能在我们的主机上不存在。这是在容器内使用的 ID。

让我们停止mydb容器并检查我们的卷。您将看到该卷仍然存在于系统中:

$ docker container stop mydb
 mydb

$ docker volume ls --filter name=c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f
 DRIVER VOLUME NAME
 local c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f

再次启动mydb容器,它将重用其卷数据。如果我们向数据库中添加了数据,我们仍然可以访问它,因为卷会持久化我们的数据。

现在,让我们删除mydb容器:

$ docker container rm mydb
mydb

我们可以验证该卷是否仍在/var/lib/docker/volumes下:

$ docker volume ls --filter name=c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f
DRIVER VOLUME NAME
local c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f

容量会保留在容器外,除非我们使用--volume将它们与关联的容器一起删除。我们还可以将卷的内容与其他容器共享。但未命名的容器难以管理,因为它们仅通过摘要来识别。我们将删除这个卷:

$ docker volume rm c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f
c888a831d6819aea6c6b4474f53b7d6c60e085efaa30d17db60334522281d76f

现在,让我们创建一个名为mydata的卷:

$ docker volume create mydata
mydbdata

在这种情况下,我们可以使用这个卷创建一个新容器,并且它的内容将对我们的新进程可用。

需要理解的是,镜像中的VOLUME定义并不是使用容器卷的必要条件。但它们有助于我们了解哪些目录应该管理容器外部的文件系统。好的容器镜像会定义持久数据应该存储的目录。

Docker 容器可以在容器创建或执行时使用两种不同的选项挂载卷:

--volume-v 我们将使用三个参数与这个选项,参数之间用:分隔。我们将使用最后一个参数来声明将提供的访问类型(只读或读写)。第二个参数将指示容器中将挂载卷的目录或文件。第一个参数将根据我们使用的资源类型而有所不同。如果我们使用绑定挂载,它将作为主机中的文件或目录。如果我们使用命名卷,该参数将声明将在容器中挂载的卷。

使用--volume选项时,第三个参数还有其他选项。除了读取或写入访问权限,我们在使用 SELinux 时可以指定zZ。如果卷将在多个容器之间共享,我们将使用这些选项将卷内容声明为私有且不可共享。

--mount 这个符号表示比--volume接受更多的参数。我们将使用键/值格式来声明多个选项。可用的键如下:- type:可用的值有bindvolumetmpfs- source (或 src):这将描述卷或主机路径。- destination (或 dst 或 target):这描述了将挂载卷内容的路径。- readonly:这表示卷内容的访问类型。

使用--volume--mount选项之间只有一个区别。使用--volume时,如果我们在使用绑定挂载时指定的路径在 Docker 主机中不存在,它将创建该端点,而--mount在这种情况下会报错并且不会创建该路径。

现在,我们将启动一个alpine容器,使用挂载在/data中的定义卷。我们在这里将其命名为c1。我们将在其/data目录下创建一个文件:

$ docker container run --name c1 -v mydata:/data -ti alpine
/ # touch /data/persistent-file-test
/ # exit

退出容器后,我们可以列出mydata卷文件系统下的文件:

$ sudo ls -lart /var/lib/docker/volumes/mydata/_data
total 0
drwxr-xr-x 3 root root 19 nov 3 20:34 ..
-rw-r--r-- 1 root root 0 nov 3 20:44 persistent-file-test
drwxr-xr-x 2 root root 34 nov 3 20:44 .

现在,我们可以创建一个新容器,并重新使用我们之前创建的命名卷mydata。在这个示例中,我们将其挂载到/tmp下:

$ docker container run --name c2 -v mydata:/tmp -ti alpine ls -lart /tmp
total 0
-rw-r--r-- 1 root root 0 Nov 3 19:44 persistent-file-test
drwxr-xr-x 2 root root 34 Nov 3 19:44 .
drwxr-xr-x 1 root root 6 Nov 3 19:48 ..

现在,c1c2两个容器都已经挂载了mydata卷。因此,除非两个容器都从本地系统中删除,否则我们无法删除mydata卷(即使我们使用--force进行删除):

$ docker volume rm mydata
Error response from daemon: remove mydata: volume is in use - [a40f15ab8977eba1c321d577214dc4aca0f58c6aef0eefd50d6989331a8dc723, 472b37cc19571960163cdbcd902e83020706a46f06fbb6c7f9f1679c2beeed0e]

只有当两个容器都被删除时,我们才能删除mydata卷:

$ docker container rm c1 c2
c1
c2

$ docker volume rm mydata
mydata

现在,让我们了解在容器化环境中存储持久化数据的一些策略和用例。

学习不同的持久化策略

正如我们已经了解到的,容器中的持久化有不同的方法。选择正确的解决方案将取决于环境和我们应用程序的用例或需求。

本地持久化

每当我们在隔离的独立 Docker 守护进程上部署应用程序时,我们将使用本地目录或文件。在这种方法中,你应该注意文件系统权限和安全模块配置。对于开发人员来说,这种策略非常有趣,因为他们可以在笔记本电脑上使用容器内的本地源代码文件运行多容器应用程序。因此,所有在本地文件上做出的更改将在容器内同步(实际上,它们并不会完全同步;而是作为绑定挂载卷挂载到容器文件系统中的相同文件)。我们将在章节实验部分回顾一些例子。这个解决方案不会提供高可用性。

分布式或远程卷

这些是编排环境中首选的解决方案。我们应该提供一个分布式或远程存储端点池,以便应用程序可以在集群内的任何地方运行。根据你的应用程序,卷的速度可能是决定使用哪个驱动程序的关键。我们也会有不同的云服务提供商选择。但是,对于具有静态内容的常见用例,网络文件系统NFS)就足够了。虽然它对于数据库或高 I/O 应用要求来说可能不够,但在我们使用共享资源扩展实例时,需要对文件系统文件进行锁定。Docker 守护进程不会管理这些情况,因为它们超出了 Docker 的范围。卷的 I/O 和文件锁定实际上取决于应用程序的逻辑及其架构。分布式或远程卷解决方案都不会提供高可用性。事实上,Docker 并不真正了解存储。它只关心卷,无论你的主机上存储是如何实现的。

卷驱动程序提供扩展功能,以扩展 Docker 的开箱即用功能。Docker 插件系统在 Docker 1.12 版本中发生了变化。因此,我们将旧的插件称为传统插件,这些插件不使用 docker plugin 命令进行管理。我们可以在docs.docker.com/engine/extend/legacy_plugins/#volume-plugins找到传统卷插件的列表。新的插件总是通过 docker plugin 命令行操作进行管理。这些插件可能需要特殊的权限,因为它们应该能够在主机系统级别执行特权操作。在本章的最后,我们将回顾一个快速实验,我们将使用 sshfs 插件。

这些描述的用例更偏向于数据管理。但应用程序状态呢?通常通过卷来管理,但这确实取决于你的应用架构。对于新的应用开发项目,有一个建议是将应用程序状态追踪放在容器外部,甚至是卷外部。这使得在我们需要扩展或缩减某些组件时,更容易管理实例复制。但请记住,这应该在应用程序级别管理。Docker 只会管理容器化应用程序组件的运行方式;它不会管理它们的应用状态或依赖关系。

既然我们已经了解了如何使用持久卷来管理容器数据及其状态,接下来我们来探讨网络功能。

容器中的网络

我们已经了解容器是运行在宿主操作系统上的独立进程。这种隔离通过为用户、进程树、进程间通信以及每个容器化进程的一整套网络资源使用不同的命名空间来提供。因此,每个容器将拥有自己的网络接口。为了能够与外界通信,默认情况下,Docker 守护进程将创建一个名为docker0的桥接接口。Docker 网络平面在最新版本中变化不大。它可以通过外部工具和插件扩展,基于桥接和虚拟网络接口,连接宿主机和容器资源。

默认情况下,新的 Docker 安装将显示三个网络对象:

$ docker network ls
 NETWORK ID NAME DRIVER SCOPE
 033e4c3f3608 bridge bridge local
 82faac964567 host host local
 2fb14f721dc3 none null local

正如我们已经学习的,所有对象都通过其唯一的 ID 来识别。Docker 网络列表显示了网络的NAME(我们可以设置自己的网络名称)、DRIVER(网络类型)和SCOPE列(指示此网络将在哪些地方可用)。根据网络驱动程序的不同,容器将被附加到不同类型的网络上。

除了所有常见的对象操作,如createlist(使用ls)、inspectremove(使用rmprune)外,网络还具有connectdisconnect操作,用于将容器附加或从网络中分离。

在深入每种网络类型之前,让我们先回顾一下创建选项:

选项 描述
--attachable 此选项启用手动容器附加。对于本地作用域网络,它不是必需的。
--aux-address 使用--aux-address,我们可以将一个主机及其地址添加到此网络。例如,我们可以使用--aux-address="mygateway=192.168.1.10"在声明的网络上设置特定的主机到 IP 的映射。通常用于macvlan网络。
--config-from--config-only 我们可以创建(或重用之前创建的)网络配置。这对于使用自动化工具构建配置非常有用,例如在不同的宿主机上,并能够在需要时使用它们。
--driver-d--opt 这个选项允许我们指定要使用的驱动程序。默认情况下,我们只能使用 macvlannonehostbridge。但我们可以使用其他外部插件来扩展 Docker 的网络功能。我们将使用 --opt 来定制所应用的驱动程序。
--gateway 我们可以覆盖默认网关(默认是定义子网的最低 IP 地址),并为此指定另一个 IP 地址。
--ingress 这个选项将在我们需要为内部服务管理创建一个特殊的 Swarm vxLan 网络时使用。
--internal 这个选项仅适用于覆盖网络。我们将只使用它来定义内部网络,因为默认情况下,所有覆盖网络都会连接到 docker_gwbridge 桥接网络(在操作 Swarm 时自动创建)以提供外部连接。
--ip-range 配置完子网后,我们可以指定一段 IP 地址范围供容器使用。
--ipam-driver--ipam-opt 使用这些选项,我们可以使用外部 IP 地址管理驱动程序。
--ipv6 我们将使用这个选项来启用该网络的 IPv6。
--label 通过这个选项,我们可以向网络添加元数据,以便更好地进行过滤。
--scope 通过这个选项,我们声明网络将为本地或 Swarm 使用而创建的范围。
--subnet 该选项指定一个 CIDR 格式的子网,用来表示一个网络段。

创建后,网络对象将一直存在,直到被删除。但是,只有当没有容器连接到它们时,才能进行删除。需要理解的是,已删除的容器仍然会在现有网络上配置端点,因此必须先删除容器才能删除网络。另一方面,prune 操作将删除所有未使用的网络。 |

Docker 每次创建网络或需要实现某些连接或容器进程发布时,都会为你操作 iptables 规则。你可以避免这一特性,但我们强烈建议允许 Docker 守护进程为你管理这些规则。追踪意外行为并不容易,而且会有很多规则需要管理。 |

现在我们已经掌握了基本的 create 命令选项,让我们来看一下可以创建的不同标准网络。 |

使用默认的桥接网络 |

Bridge 是所有容器的默认网络类型。任何其他网络类型必须在容器创建或执行时使用 --network 可选参数声明。 |

在操作系统术语中,我们使用桥接接口来允许从其他虚拟接口转发流量。所有这些虚拟接口将使用与桥接关联的物理接口,以与其他网络设备或连接的主机进行通信。在容器的世界中,所有容器接口都是虚拟的,它们将被附加到主机级别的这些桥接接口上。因此,附加到同一桥接接口的所有容器将彼此可见。

让我们看一个使用桥接网络的快速示例:

  1. 我们只运行了两个容器,c1c2,附加到默认网络(注意我们根本没有定义任何网络):
zero@sirius:~$ docker container run -ti -d --name c1 alpine ping 8.8.8.8
c44fbefb96b9321ef1a0e866fa6aaeb26408fc2ef484bbc9ecf904546f60ada7

zero@sirius:~$ docker container run -ti -d --name c2 alpine ping 8.8.8.8
cee980d7f9e587357375e21dafcb406688ac1004d8d7984ec39e4f97533492ef
  1. 我们找到它们的 IP 地址:
$ docker container inspect c1 --format "{{ .NetworkSettings.Networks.bridge.IPAddress }}"
172.17.0.2

$ docker container inspect c2 --format "{{ .NetworkSettings.Networks.bridge.IPAddress }}"
172.17.0.3
  1. 因此,我们可以对它们中的每一个进行 ping 测试:
$ docker exec c1 ping -c 2 172.17.0.3 
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.113 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.210 ms
--- 172.17.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.113/0.161/0.210 ms
  1. 让我们快速查看一些c1容器的属性:
$ docker container inspect c1 --format "{{json .NetworkSettings.Networks }}"
{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":"033e4c3f360841b0826f3b850fe9f5544d145bea644ee1955717e67d02df92ce","EndpointID":"390d2cf0b933ddd3b11fdebdbf6293c97f2a8568315c80794fad6f5b8eef3207","Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:11:00:02","DriverOpts":null}}
  1. 每个容器都将有自己的 IP 地址和EndpointID。让我们检查由 Docker 默认创建的桥接网络的配置:
$ docker network inspect bridge
[
 {
 "Name": "bridge",
 "Id": "033e4c3f360841b0826f3b850fe9f5544d145bea644ee1955717e67d02df92ce",
 ...
 "IPAM": {
 ... 
 "Config": [
 {
 "Subnet": "172.17.0.0/16",
 "Gateway": "172.17.0.1"
 }
 ]
 },
 ...
 "Containers": {
 "c44fbefb96b9321ef1a0e866fa6aaeb26408fc2ef484bbc9ecf904546f60ada7": {
 "Name": "c1",
 "EndpointID": "390d2cf0b933ddd3b11fdebdbf6293c97f2a8568315c80794fad6f5b8eef3207",
 "MacAddress": "02:42:ac:11:00:02",
 "IPv4Address": "172.17.0.2/16",
 "IPv6Address": ""
 },
 "cee980d7f9e587357375e21dafcb406688ac1004d8d7984ec39e4f97533492ef": {
 "Name": "c2",
 "EndpointID": "cb49b93bc0bdd3eb887ad3b6fcd43155eb4ca7688c788719a27acc9e2f2e2a9d",
 "MacAddress": "02:42:ac:11:00:03",
 "IPv4Address": "172.17.0.3/16",
 "IPv6Address": ""
 }
 },
 "Options": {
 "com.docker.network.bridge.default_bridge": "true",
 "com.docker.network.bridge.enable_icc": "true",
 "com.docker.network.bridge.enable_ip_masquerade": "true",
 "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
 "com.docker.network.bridge.name": "docker0",
 "com.docker.network.driver.mtu": "1500"
 },
 "Labels": {}
 }
]

让我们谈谈此输出中一些最重要的部分:

  • 此网络未使用 IPv6。它被称为bridge,使用了bridge驱动程序创建,并且只能在此主机上本地使用。

  • 它是使用了172.17.0.0/16子网创建的,因此,此网络上的所有容器将在该段范围内获得一个 IP 地址。

  • 桥接口的 IP 地址是172.17.0.1,将成为所有容器的默认网关。

  • 我们在此网络上有两个正在运行的容器。它们都在Containers部分下列出,带有它们的虚拟 MAC 地址、IP 地址和关联的端点。

  • 在网络创建期间,有许多有趣的选项可以使用:

    • com.docker.network.bridge.default_bridge: true: 这意味着当未定义任何网络时,这是默认的桥接。

    • com.docker.network.bridge.enable_icc: true: 此参数表示连接到此网络的容器可以彼此通信。我们可以在自定义桥上禁用此功能,只允许南北流量。

    • com.docker.network.bridge.name: docker0: 这是关联主机接口的名称。

当我们提到南北流量时,我们指的是从 Docker 主机到容器及其反向的通信类型。另一方面,东西流量是不同容器之间的流量。这些是用来描述网络流量的广为人知的网络术语参考。

理解空网络

当我们需要部署一个没有任何网络接口的容器时,使用空网络(Null 或 none 网络)。虽然这听起来似乎没有用,但实际上有很多情况我们可能需要启动一个任务来执行数学运算、压缩或许多其他不需要网络功能的操作。在这些情况下,我们只需要使用数据卷,而不需要任何网络操作。使用空网络可以确保任务只会访问所需的资源。如果不需要网络访问,就不提供网络。默认情况下,容器将使用 bridge 网络,除非我们指定 none

$ docker run -ti --network none alpine
/ # ip add
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
/ #

现在我们已经理解容器可以使用空接口来避免网络连接,接下来我们可以看看主机的网络命名空间。

理解主机网络

主机网络仅在 Linux 主机上可用。这一点非常重要,因为它是 Windows 容器的一个重要区别。

使用主机网络时,容器共享 host 网络命名空间。因此,容器将获取所有主机的 IP 地址,容器级别使用的每个端口都将在主机上设置。因此,不允许多个容器同时使用特定端口运行。另一方面,网络性能更好,因为容器服务直接连接到主机端口,不需要进行 NAT 或防火墙规则适配:

$ docker run -ti --network host alpine
/ # ip add
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
 inet6 ::1/128 scope host 
 valid_lft forever preferred_lft forever
2: enp0s25: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN qlen 1000
 link/ether 68:f7:28:c1:bc:13 brd ff:ff:ff:ff:ff:ff
3: wlp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
 link/ether 34:02:86:e3:f6:25 brd ff:ff:ff:ff:ff:ff
 inet 192.168.200.165/24 brd 192.168.200.255 scope global dynamic wlp3s0
 valid_lft 51sec preferred_lft 51sec
 inet6 fe80::ee87:e44f:9189:f720/64 scope link 
 valid_lft forever preferred_lft forever
...
...
10: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP 
 link/ether 02:42:11:73:cc:2b brd ff:ff:ff:ff:ff:ff
 inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
 valid_lft forever preferred_lft forever
 inet6 fe80::42:11ff:fe73:cc2b/64 scope link 
 valid_lft forever preferred_lft forever
...
...
18: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP 
 link/ether 02:42:4b:21:09:6d brd ff:ff:ff:ff:ff:ff
 inet 172.18.0.1/16 brd 172.18.255.255 scope global docker_gwbridge
 valid_lft forever preferred_lft forever
 inet6 fe80::42:4bff:fe21:96d/64 scope link 
 valid_lft forever preferred_lft forever
20: veth82a8134@if19: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker_gwbridge state UP 
 link/ether a6:5d:02:ed:79:0a brd ff:ff:ff:ff:ff:ff
 inet6 fe80::a45d:2ff:feed:790a/64 scope link 
 valid_lft forever preferred_lft forever
22: veth4b1102e@if21: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker0 state UP 
 link/ether fa:08:70:aa:b1:4b brd ff:ff:ff:ff:ff:ff
 inet6 fe80::f808:70ff:feaa:b14b/64 scope link 
 valid_lft forever preferred_lft forever
27: wwp0s20u4: <BROADCAST,MULTICAST,NOARP> mtu 1428 qdisc noop state DOWN qlen 1000
 link/ether 06:1b:05:d6:e9:12 brd ff:ff:ff:ff:ff:ff
/ # 

在这里,您可以看到所有主机接口被列出,因为容器正在使用其网络命名空间。

这种网络模式有一定风险,因为它允许容器之间进行任何类型的通信。应当在特权模式下谨慎使用。它在监控工具中非常常见,或者当我们运行需要高性能网络接口的应用程序时。

我们可以定义自己的网络接口。我们将在下一部分创建自定义桥接网络。

创建自定义桥接网络

正如我们在默认桥接网络示例中讨论的那样,这种网络类型将与主机的 bridge 接口关联。默认情况下,它会附加到 docker0,但每次我们创建一个新的桥接网络时,都会为我们创建一个新的 bridge 接口,所有附加的容器都将拥有与该接口相连接的虚拟接口。

默认桥接网络与自定义创建的桥接网络之间有一些非常重要的区别:

  • 自定义桥接隔离:每个创建的自定义桥接网络都会有自己关联的桥接接口,并且有自己的子网和主机 iptables。此功能提供了更高的隔离级别,因为只有附加的容器才能相互通信。所有在同一主机上运行的其他容器将无法看到运行在自定义桥接网络上的这些容器。

  • 内部 DNS:Docker 守护进程为每个自定义桥接网络提供自定义的 DNS。这意味着在同一网络上运行的所有容器将通过名称彼此识别。这是一个非常重要的特性,因为服务发现不需要任何外部的知识来源。但请记住,这仅适用于网络内的内部使用。

我们可以通过使用传统的 --link 功能,在默认桥接网络上提供这种类型的 DNS 解析。这是旧版 Docker 中连接容器的方式。如今,使用自定义桥接网络被认为提供了更好的隔离性。

  • 动态容器附加:在默认桥接网络中,我们必须在容器创建或执行时提供连接。假设我们为容器使用了一个 null 或 none 网络,并且之后想将其连接到默认桥接网络——这是不可能的。一旦容器创建,就不能再将其附加到默认桥接网络。必须从头开始重新创建该容器,并附加该网络。另一方面,自定义桥接网络是可以附加的,这意味着我们可以考虑这样一种情况:容器在创建时没有特定的网络连接,后来可以添加该连接。我们还可以在不同的自定义网络上运行一个容器,并为其提供名称解析。

让我们回顾一个简单的示例。我们将在本章的 章节实验 部分提供更详细的示例:

$ docker network create --driver bridge --internal --subnet 192.168.30.0/24 --label internal-only internal-only
c275cdd25b422b35d3f2b4fbbb153e7cd09c8721133667cfbeb9c297af89364a

我们回顾一下创建的网络属性(请注意定义的子网)和内部设置:

$ docker network inspect internal-only 
[
 {
 "Name": "internal-only",
 "Id": "c275cdd25b422b35d3f2b4fbbb153e7cd09c8721133667cfbeb9c297af89364a",
 "Created": "2019-11-10T11:03:20.490907017+01:00",
 "Scope": "local",
 "Driver": "bridge",
 "EnableIPv6": false,
 "IPAM": {
 "Driver": "default",
 "Options": {},
 "Config": [
 {
 "Subnet": "192.168.30.0/24"
 }
 ]
 },
 "Internal": true,
 "Attachable": false,
 "Ingress": false,
 "ConfigFrom": {
 "Network": ""
 },
 "ConfigOnly": false,
 "Containers": {},
 "Options": {},
 "Labels": {
 "internal-only": ""
 }
 }
]

现在,我们创建一个容器并测试互联网访问:

$ docker container run --network internal-only -ti --name intc1 alpine sh
/ # ping 8.8.8.8 -c 2
PING 8.8.8.8 (8.8.8.8): 56 data bytes
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss
/ # 

请记得使用 Ctrl + P + Q 快捷键将 intc1 容器运行在后台。

你可能已经注意到我们没有任何外部连接。让我们通过另一个容器来查看内部连接情况:

$ docker container run --network internal-only -ti --name intc2 alpine sh
/ # ping intc1 -c2
PING intc1 (192.168.30.2): 56 data bytes
64 bytes from 192.168.30.2: seq=0 ttl=64 time=0.185 ms
64 bytes from 192.168.30.2: seq=1 ttl=64 time=0.157 ms
--- intc1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.157/0.171/0.185 ms
/ # 

如前面的输出所示,我们有内部通信和 DNS 解析,但无法与任何其他外部 IP 地址进行通信。

如果我们查看 iptables,我们可以看到内部网络的创建为本地防火墙添加了一些非常有趣的规则。执行 iptables -L 并忽略所有与 Docker 无关的规则,我们可以观察到这些规则:

Chain DOCKER (4 references)
target prot opt source destination
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target prot opt source destination 
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere 
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere 
DROP all -- !192.168.30.0/24 anywhere 
DROP all -- anywhere !192.168.30.0/24 
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere 
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere 
RETURN all -- anywhere anywhere

Chain DOCKER-ISOLATION-STAGE-2 (4 references)
target prot opt source destination 
DROP all -- anywhere anywhere 
DROP all -- anywhere anywhere 
DROP all -- anywhere anywhere 
DROP all -- anywhere anywhere 
RETURN all -- anywhere anywhere

Chain DOCKER-USER (1 references)
target prot opt source destination 
RETURN all -- anywhere anywhere 

这些是管理我们先前创建的内部网络隔离的规则。

我们将在本章的 章节实验 部分检查一些多接口示例。

MacVLAN 网络 – macvlan

MacVLAN 驱动程序为每个容器接口分配一个虚拟 MAC 地址。因此,容器将能够管理其在真实网络上的 IP 地址。为了管理这种类型的网络接口,我们需要声明一个主机物理接口。由于容器将拥有自己的 MAC 地址,我们可以在这些接口上使用 VLAN,为容器提供仅访问定义 VLAN 的权限。但请注意,在这种情况下,我们需要将所有所需的 VLAN 分配给 macvlan 所分配的主机接口。

macvlan驱动程序仅在 Linux 主机上有效(内核版本需高于 3.9,推荐使用 4.0)。这种接口通常会在云服务提供商处被阻止。

因此,我们描述了macvlan的两种不同模式:

  • 桥接模式:在这种情况下(默认模式),流量将通过定义的主机物理接口。

  • 802.1q 中继桥接模式:流量将通过由 Docker 守护进程在网络创建时创建的 802.1q VLAN 接口。

在这些网络中,我们通常使用--aux-address将现有的节点或网络设备添加到这个新创建的 Docker 网络中。

我们已经回顾了 Docker 默认提供的不同接口。现在,让我们继续了解这些通信在主机级别是如何发生的。

学习容器交互

容器环境中有两种不同的通信类型:

  • 与外部世界的通信

  • 容器间通信

我们将在本节中查看这两者。

与外部世界的通信

要允许容器与外部世界通信,主机级别需要两个特性:

  • IP 转发是允许来自容器 IP 地址的数据包走出容器化环境所必需的。这是在内核级别完成的,Docker 守护进程会管理所需的参数(ip_forward内核参数将设置为1)以允许此策略。我们可以在守护进程配置中使用--ip-forward=false更改此默认行为设置。通常,容器间所有种类的通信都需要这种转发。

  • 一旦启用转发,iptables将管理所需的规则,仅严格允许所需的通信。我们可以手动设置iptables规则,而不是允许 Docker 守护进程处理这些设置,方法是在守护进程配置中使用--iptables=false选项。除非你非常清楚需要做哪些更改,否则建议允许 Docker 守护进程管理这些规则。Docker 只会管理DOCKERDOCKER-ISOLATION过滤链,而我们可以在DOCKER-USER链中管理自定义规则。

默认情况下,Docker 会转发所有数据包,并允许所有外部源 IP 地址。如果我们只需允许必要的 IP 地址,可以添加自定义规则来DROP所有不被允许的通信。

容器间通信

我们还可以通过 IP 转发和iptables来管理容器间的通信。正如我们已经学到的那样,我们可以在创建网络时使用--internal选项,仅允许内部通信。任何超出此定义子网的通信将被丢弃。

另一方面,我们可以通过应用--icc=false来禁止任何容器之间的通信。此选项管理与同一桥接网络相连的容器之间的内部交互。如果我们将该参数设置为false,即使它们运行在同一子网内,也不允许容器之间的通信。这是最安全的网络配置,因为我们仍然可以通过--link选项允许特定的通信。容器链接将创建特定的iptables规则,以允许这些特定的通信。

自定义桥接网络上的 DNS

我们已经了解了自定义桥接网络拥有一个内部 DNS。这意味着任何容器交互都可以通过容器名称进行管理。此内部 DNS 将始终运行在127.0.0.11上。我们可以修改其某些特性,例如添加新的主机等。

让我们回顾一些常见的特性,这些特性可以轻松地被操作以改善应用程序的发现和交互:

特性 描述
--network-alias=ALIAS 此选项允许我们为容器添加另一个内部 DNS 名称。
--link=CONTAINER_NAME:ALIAS 我们之前讨论了针对传统环境的链接选项。它也是一种在默认不允许容器交互的情况下,允许特定通信的方法。此选项还会向内部 DNS 添加一条条目,以允许将CONTAINER_NAME解析为定义的ALIAS。此用例与--network-alias不同,因为它用于不同的容器。
--dns,--dns-search--dns-option 这些选项将在内部 DNS 无法解析已定义名称的情况下管理转发的 DNS 解析。我们可以添加转发 DNS 及其特定选项,以允许或禁止某些容器的外部搜索。这将帮助我们使用不同的名称解析来访问内部或外部应用程序。

现在我们已经了解了可用的不同接口以及通信是如何在主机系统层面工作的,接下来让我们学习如何从客户端访问应用程序。我们刚刚介绍了iptables作为一种机制,可以在不同网络上部署容器时自动获得访问权限。在下一节中,我们将深入探讨如何在独立的 Docker 主机上发布应用程序的方法。

发布应用程序

默认情况下,所有容器进程都与外部访问隔离。这意味着尽管我们为进程服务定义了一个端口(在镜像上使用EXPOSE),但除非我们明确声明它为公开可用,否则它是无法访问的。这是一项很好的安全措施。在没有明确声明之前,不会允许任何外部通信。只有连接到同一桥接网络或主机上的容器,使用其主机内部 IP(连接到桥接网络),才能使用该进程服务。

让我们通过一个快速示例来回顾一下使用nginx:alpine基础镜像的情况。我们知道nginx:alpine暴露了80端口:

$ docker container run -d --name webserver nginx:alpine
4a37b49721b4fe6ffc57aee07c3fb42e5c08d4bcc0932e07eb7ce75fe696442d

$ docker container inspect webserver --format "{{json .NetworkSettings.Networks.bridge.IPAddress }}"
"172.17.0.4"

$ curl http://172.17.0.4
<!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>

在这种情况下,我们的主机 IP 地址是在默认桥接网络上的 172.17.0.1,我们可以访问容器的 80 端口,但其他主机无法访问此端口。它是由 webserver 容器内部暴露的。

要发布一个内部暴露的端口,我们需要在容器创建或执行时使用 --publish-p 参数进行声明。

我们将使用 --publish [HOST_IP:][HOST_PORT:]CONTAINER_PORT[/PROTOCOL] 来实现这一点。这意味着唯一需要的参数是容器端口。默认情况下,将使用 TCP 协议和 3276865000 之间的随机端口,并且该端口将在所有主机 IP 地址(0.0.0.0)上公开发布。我们还可以使用 -P 来发布给定容器镜像定义中暴露的所有端口。

如果我们需要声明 UDP 应用程序的发布,我们需要指定该协议。

主机模式网络不需要任何端口发布,因为任何暴露的容器进程都会对外部可访问。

我们可以以 --publish StartPort-EndPort[/PROTOCOL] 的形式声明一个端口范围,以发布多个端口。

出于安全原因,在多网卡主机上使用特定的 IP 地址非常重要,这样可以只允许访问指定的 IP 地址:

$ docker container run -d --name public-webserver --publish 80 nginx:alpine
562bfebccd728fdc3dff649fe6ac578d52e77c409e84eed8040db3cfc5589e40

$ docker container ls --filter name=public-webserver
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
562bfebccd72 nginx:alpine "nginx -g 'daemon of…" About a minute ago Up About a minute 0.0.0.0:32768->80/tcp public-webserver

$ curl http://0.0.0.0:32768
<!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>

我们将在下一部分看到更多关于此的例子。

本章实验

本章致力于学习如何管理有状态环境以及容器网络背后的原理。现在,让我们完成一些实验,以复习我们所学到的内容。对于这些实验,我们将使用安装了 Docker 引擎的 CentOS Linux 主机。

如果你还没有这样做,请从本书的 GitHub 仓库(github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git)部署 environments/standalone-environment。你也可以使用自己的 CentOS 7 服务器。从 environments/standalone-environment 文件夹中使用 vagrant up 启动你的虚拟环境。

如果你使用的是独立环境,请等待它运行起来。我们可以使用 vagrant status 检查节点的状态。使用 vagrant ssh standalone 连接到你的实验节点。standalone 是你的节点名称。你将使用具有 root 权限的 vagrant 用户,并使用 sudo。你应该看到以下输出:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant up
Bringing machine 'standalone' up with 'virtualbox' provider...
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant status
Current machine states:
standalone running (virtualbox)
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$

现在我们可以使用 vagrant ssh standalone 连接到独立节点。如果你之前部署了独立虚拟节点,并且刚刚使用 vagrant up 启动了它,那么这个过程可能会有所不同:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant ssh standalone
[vagrant@standalone ~]$ 

如果你正在重用你的独立环境,这意味着 Docker 引擎已经安装。如果你启动了一个新实例,请执行 /vagrant/install_requirements.sh 脚本,以确保安装所有必需的工具(Docker 引擎和 docker-compose):

[vagrant@standalone ~]$ /vagrant/install_requirements.sh 

现在,你准备好开始实验了。

在你的笔记本电脑上使用卷来编写代码

在本实验中,我们将在容器内运行包含应用程序代码的容器。由于该应用程序是使用解释型语言创建的,任何更改或代码修改都会被刷新(我们已添加调试功能,以便在每次更改时重新加载应用程序,使用debug=True):

  1. 我们为您创建了一个简单的 Python Flask 应用程序。以下是app.py文件的内容:
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')

def just_run():
 return render_template('index.html')

if __name__ == '__main__':
 app.run(debug=True,host='0.0.0.0')
  1. 我们只需要Flask Python 模块,因此requirements.txt文件中只有一行内容:
Flask
  1. 我们将使用一个简单的模板 HTML 文件templates/index.html,其内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Simple Flask Application</title>
</head>
<body>
 <h1>Simple Flask Application</h1>
 <h1>Version 1</h1>
</body>
</html>
  1. 我们将在容器内运行这个应用程序。我们将创建一个 Dockerfile,并构建一个名为simpleapp的镜像,标签为v1.0。以下是 Dockerfile 的内容:
FROM python:alpine
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY app.py .
COPY templates templates 
EXPOSE 5000
CMD ["python", "app.py"]
  1. 让我们构建我们的应用程序镜像(simpleapp:v1.0):
[vagrant@standalone ~]$ docker image build -q -t simpleapp:v1.0 .
sha256:1cf398d39b51eb7644f98671493767267be108b60c3142b3ca9e0991b4d3e45b
  1. 我们可以通过执行一个独立容器并暴露5000端口来运行这个简单的应用程序:
[vagrant@standalone ~]$ docker container run -d --name v1.0 simpleapp:v1.0 
1e775843a42927c25ee350af052f3d8e34c0d26f2510fb2d85697094937f574f
  1. 现在,我们可以查看容器的 IP 地址。我们在主机中运行此容器,这意味着我们可以访问进程端口和定义的 IP 地址:
[vagrant@standalone ~]$ docker container ls --filter name=v1.0
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1e775843a429 simpleapp:v1.0 "python app.py" 35 seconds ago Up 33 seconds 5000/tcp v1.0
 "python app.py" 35 seconds ago Up 33 seconds 5000/tcp v1.0

[vagrant@standalone ~]$ docker container inspect v1.0 \
 --format "{{.NetworkSettings.Networks.bridge.IPAddress }}"
 172.17.0.6
  1. 我们可以使用容器定义的 IP 和端口按预期访问我们的应用程序:
[vagrant@standalone ~]$ curl http://172.17.0.6:5000
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Simple Flask Application</title>
</head>
<body>
 <h1>Simple Flask Application</h1>
 <h1>Version 1</h1>
</body>
</html>
  1. 如果我们进入容器,更改index.html是很简单的。但问题在于,当我们运行一个新容器时,所做的更改不会被存储,index.html会丢失。每次都会得到在基础镜像中定义的index.html。因此,如果我们希望更改能够持久化,我们需要使用卷。让我们使用绑定挂载,在容器运行时更改index.html文件:
[vagrant@standalone ~]$ docker container run -d \
--name v1.0-bindmount -v $(pwd)/templates:/app/templates simpleapp:v1.0 
 fbf3c35c2f11121ed4a0eedc2f47b42a5ecdc6c6ff4939eb4658ed19999f87d4

[vagrant@standalone ~]$ docker container inspect v1.0-bindmount --format "{{.NetworkSettings.Networks.bridge.IPAddress }}"
172.17.0.6

[vagrant@standalone ~]$ curl http://172.17.0.6:5000
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Simple Flask Application</title>
</head>
<body>
 <h1>Simple Flask Application</h1>
 <h1>Version 1</h1>
</body>
</html>
  1. 现在,我们可以更改templates/index.html,因为我们已经使用了-v $(pwd)/templates:/app/templates,假设当前目录为工作目录。使用 vi 编辑器,我们可以修改templates/index.html文件的内容:
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Simple Flask Application</title>
</head>
<body>
 <h1>Simple Flask Application</h1>
 <h1>Version 2</h1>
</body>
</html>
~ 
~ 
  1. 我们更改包含Version键的行,并使用curl再次访问它:
[vagrant@standalone ~]$ curl http://172.17.0.6:5000
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Simple Flask Application</title>
</head>
<body>
 <h1>Simple Flask Application</h1>
 <h1>Version 2</h1>
</body>
</html>

这些变化之所以能反映出来,是因为我们在主机的文件系统上进行了操作,并且该文件系统已挂载到我们的容器内。我们还可以通过挂载app.py来更改应用程序代码。根据所使用的编程语言,我们可以实时更改应用程序代码。如果更改需要持久化,我们需要遵循版本控制策略。我们将构建一个包含所需更改的新镜像。

挂载 SSHFS

在本实验中,我们将安装并使用sshfs卷插件:

  1. 首先,我们需要安装sshfs插件:
[vagrant@standalone ~]$ docker plugin install vieux/sshfs
Plugin "vieux/sshfs" is requesting the following privileges:
 - network: [host]
 - mount: [/var/lib/docker/plugins/]
 - mount: []
 - device: [/dev/fuse]
 - capabilities: [CAP_SYS_ADMIN]
Do you grant the above permissions? [y/N] y
latest: Pulling from vieux/sshfs
52d435ada6a4: Download complete 
Digest: sha256:1d3c3e42c12138da5ef7873b97f7f32cf99fb6edde75fa4f0bcf9ed277855811
Status: Downloaded newer image for vieux/sshfs:latest
Installed plugin vieux/sshfs
  1. 让我们查看主机的 IP 地址,并启动sshdssh守护进程(具体取决于您的系统及其是否已在运行):
[vagrant@standalone ~]$ sudo systemctl status ssh
● ssh.service - OpenBSD Secure Shell server
 Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled)
 Active: active (running) since Mon 2019-11-11 23:59:38 CET; 6s ago
 Main PID: 13711 (sshd)
 Tasks: 1 (limit: 4915)
 CGroup: /system.slice/ssh.service
 └─13711 /usr/sbin/sshd -D

nov 11 23:59:38 sirius systemd[1]: Starting OpenBSD Secure Shell server...
nov 11 23:59:38 sirius sshd[13711]: Server listening on 0.0.0.0 port 22.
nov 11 23:59:38 sirius sshd[13711]: Server listening on :: port 22.
nov 11 23:59:38 sirius systemd[1]: Started OpenBSD Secure Shell server.
  1. 让我们查看已安装的插件:
[vagrant@standalone ~]$ docker plugin ls
ID NAME DESCRIPTION ENABLED
eb37e5a2e676 vieux/sshfs:latest sshFS plugin for Docker true

由于插件是对象,我们可以检查已安装的插件。我们可以查看插件的关键方面,如版本、调试模式或该插件将管理的挂载点类型:

[vagrant@standalone ~]$ docker plugin inspect eb37e5a2e676
[
 {
 "Config": {
..
 "Description": "sshFS plugin for Docker",
 "DockerVersion": "18.05.0-ce-rc1",
 "Documentation": "https://docs.docker.com/engine/extend/plugins/",
 "Entrypoint": [
 "/docker-volume-sshfs"
 ],
...
...
 "Source": "/var/lib/docker/plugins/",
 "Type": "bind"
 },
...
...
 "Enabled": true,
 "Id": "eb37e5a2e676138b6560bd91715477155f669cd3c0e39ea054fd2220b70838f1",
 "Name": "vieux/sshfs:latest",
 "PluginReference": "docker.io/vieux/sshfs:latest",
 "Settings": {
 "Args": [],
 "Devices": [
...
...
]
  1. 现在,我们将创建一个名为sshvolume的新卷(假设您已拥有有效的 SSH 用户名和密码)。请注意,我们正在使用127.0.0.1/tmp目录或文件系统进行演示:
[vagrant@standalone ~]$ docker volume create -d vieux/sshfs \
-o sshcmd=ssh_user@127.0.0.1:/tmp \
-o password=ssh_userpasswd \
sshvolume
  1. 现在,我们可以轻松地通过挂载之前创建的sshvolume来运行一个alpine容器:
[vagrant@standalone ~]$ docker container run --rm -it -v sshvolume:/data alpine sh
/ # ls -lart /data
total 92
drwx------ 1 root root 17 Nov 9 08:27 systemd-private-809bb564862047608c79c2cc81f67f24-systemd-timesyncd.service-gQ5tZx
drwx------ 1 root root 17 Nov 9 08:27 systemd-private-809bb564862047608c79c2cc81f67f24-systemd-resolved.service-QhsXg9
drwxrwxrwt 1 root root 6 Nov 9 08:27 .font-unix
drwxrwxrwt 1 root root 6 Nov 9 08:27 .XIM-unix
drwxr-xr-x 1 root root 30 Nov 11 23:13 ..
drwxrwxrwt 1 root root 4096 Nov 11 23:13 .
/ # 

让我们继续进行一些网络实验。

多网卡容器

现在,我们来看一下如何将容器连接到多个网络的快速实验。让我们开始吧:

  1. 首先,我们将创建两个不同的区域,zone-azone-b
[vagrant@standalone ~]$ docker network create zone-a
bb7cb5d22c03bffdd1ef52a7469636fe2e635b031b7528a687a85ff9c7ee4141

[vagrant@standalone ~]$ docker network create zone-b
818ba644512a2ebb44c5fd4da43c2b1165f630d4d0429073c465f0fe4baff2c7
  1. 现在,我们可以在 zone-a 上启动一个名为 cont1 的容器:
[vagrant@standalone ~]$ docker container run -d --name cont1 --network zone-a alpine sleep 3000 
ef3dfd6a354b5310a9c97fa9247739ac320da1b4f51f6a2b8da2ca465b12f95e
  1. 接下来,我们将 cont1 容器连接到 zone-b,并查看其 IP 地址:
[vagrant@standalone ~]$ docker network connect zone-b cont1

$ docker exec cont1 ip add
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
92: eth0@if93: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
 link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff
 inet 172.19.0.2/16 brd 172.19.255.255 scope global eth0
 valid_lft forever preferred_lft forever
94: eth1@if95: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
 link/ether 02:42:ac:14:00:02 brd ff:ff:ff:ff:ff:ff
 inet 172.20.0.2/16 brd 172.20.255.255 scope global eth1
 valid_lft forever preferred_lft forever
  1. 现在,我们可以通过仅一个接口运行两个容器。一个容器将连接到 zone-a,而另一个容器则仅连接到 zone-b
[vagrant@standalone ~]$ docker container run -d --name cont2 --network zone-b --cap-add NET_ADMIN alpine sleep 3000 
048e362ea27b06f5077306a71cf8adc95ea9844907aec84ec09c0b991d912a33

[vagrant@standalone ~]$ docker container run -d --name cont3 --network zone-a --cap-add NET_ADMIN alpine sleep 3000 
20c7699c54786700c65a0bbe002c750672ffb3986f41d106728b3d598065ecb5
  1. 让我们查看两个容器的 IP 地址和路由:
[vagrant@standalone ~]$ docker exec cont2 ip route
default via 172.20.0.1 dev eth0 
172.20.0.0/16 dev eth0 scope link src 172.20.0.3 

[vagrant@standalone ~]$ docker exec cont3 ip route
default via 172.19.0.1 dev eth0 
172.19.0.0/16 dev eth0 scope link src 172.19.0.3 
  1. 如果我们希望 cont3 容器能够联系 cont2 容器,我们应该通过 cont1 容器添加一条路由,因为 cont1 容器包含两个网络。在 cont2 容器中,输入以下命令:
[vagrant@standalone ~]$ docker exec cont2 route add -net 172.19.0.0 netmask 255.255.255.0 gw 172.20.0.2

[vagrant@standalone ~]$ docker exec cont2 ip route 
default via 172.20.0.1 dev eth0 
172.19.0.0/24 via 172.20.0.2 dev eth0 
172.20.0.0/16 dev eth0 scope link  src 172.20.0.3

cont3 容器中,输入以下内容:

[vagrant@standalone ~]$ docker exec cont3 route add -net 172.20.0.0 netmask 255.255.255.0 gw 172.19.0.2

[vagrant@standalone ~]$ docker exec cont3 ip route 
default via 172.19.0.1 dev eth0 
172.19.0.0/16 dev eth0 scope link  src 172.19.0.3 
172.20.0.0/24 via 172.19.0.2 dev eth0 
  1. 请记住,我们不同网络之间没有名称解析功能。因此,我们无法通过名称访问 cont2
[vagrant@standalone ~]$ docker exec cont3 ping -c 3 cont2
ping: bad address 'cont2'

[vagrant@standalone ~]$ docker exec cont3 ping -c 3 cont1
PING cont1 (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.063 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.226 ms
64 bytes from 172.19.0.2: seq=2 ttl=64 time=0.239 ms

--- cont1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.063/0.176/0.239 ms

正如我们预期的那样,zone-a 网络内的名称解析工作正常。任何其他网络中的容器将无法通过名称解析其他容器。

  1. 我们应该能够通过 cont3 使用 cont2 的 IP 地址进行 ping:
[vagrant@standalone ~]$ docker exec cont3 ping -c 3 172.20.0.3
PING 172.20.0.3 (172.20.0.3): 56 data bytes
64 bytes from 172.20.0.3: seq=0 ttl=63 time=0.151 ms
64 bytes from 172.20.0.3: seq=1 ttl=63 time=0.229 ms
64 bytes from 172.20.0.3: seq=2 ttl=63 time=0.201 ms

--- 172.20.0.3 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.151/0.193/0.229 ms

所以,尽管我们没有名称解析功能,但我们可以通过一个在所有网络上都有接口的容器网关访问其他网络上的容器。为了使其工作,我们为每个网络容器添加了一条路由,将所有其他网络流量路由到网关容器。我们本可以添加别名,通过名称访问其他网络容器。试试吧,挺简单的!

发布应用程序

在这个实验中,我们将部署一个简单的三层应用程序。实际上,它是一个两层应用程序,额外添加了一个负载均衡器以便用于我们的实验:

  1. 首先,我们将创建一个名为 simplenet 的桥接网络,在其中附加所有应用程序组件:
[vagrant@standalone ~]$ docker network create simplenet
b5ff93985be84095e70711dd3c403274c5ab9e8c53994a09e4fa8adda97f37f7
  1. 我们将部署一个 PostgreSQL 数据库,并使用 changeme 作为 root 用户的密码。我们为本实验创建了一个名为 demo 的简单数据库,包含一个名为 demo 的用户,密码为 d3m0
[vagrant@standalone ~]$ docker container run -d \
--name simpledb \
--network simplenet \
--env "POSTGRES_PASSWORD=changeme" \
codegazers/simplestlab:simpledb

请注意,我们尚未为数据库发布任何端口。

永远不要使用环境变量存储安全内容。管理此类数据有其他机制。请使用 Docker Swarm 或 Kubernetes 的密钥管理功能为这些密钥提供安全性。

  1. 现在,我们需要启动名为 simpleapp 的后端应用程序组件。请注意,在这种情况下,我们使用了许多环境变量来配置应用程序端。我们设置了数据库主机、数据库名称以及所需的凭据,如下所示:
[vagrant@standalone ~]$ docker container run -d \
 --name simpleapp \
--network simplenet \
--env dbhost=simpledb \
--env dbname=demo \
--env dbuser=demo \
--env dbpasswd=d3m0 \
codegazers/simplestlab:simpleapp
556d6301740c1f3de20c9ff2f30095cf4a49b099190ac03189cff3db5b6e02ce

我们尚未发布该应用程序。因此,它仅在本地可访问。

  1. 让我们查看已部署的应用程序组件的 IP 地址。我们将检查附加到 simplenet 的容器:
[vagrant@standalone ~]$ docker network inspect simplenet --format "{{range .Containers}} {{.IPv4Address }} {{.Name}} {{end}}"
 172.22.0.4/16 simpleapp 172.22.0.3/16 simpledb
  1. 如果我们查看每个镜像定义中暴露(未发布)的端口,我们将在数据库组件中看到以下内容:
[vagrant@standalone ~]$ docker inspect codegazers/simplestlab:simpledb \
--format "{{json .Config.ExposedPorts }}" 
{"5432/tcp":{}}

在应用程序后端中,我们将观察到以下内容:

[vagrant@standalone ~]$ docker inspect codegazers/simplestlab:simpleapp \
--format "{{json .Config.ExposedPorts }}" 
 {"3000/tcp":{}} 
  1. 现在,我们已经拥有所有必要的信息来测试与这两个组件的连接。我们甚至可以使用 curl 命令来测试服务器是否为数据库服务器。让我们尝试在 IP 地址为 172.22.0.3、端口为 5432 的数据库。我们将使用 curl -I,因为我们不关心响应内容。我们只想确保能够连接到暴露的端口:
[vagrant@standalone ~]$ curl -I 172.22.0.3:5432
curl: (52) Empty reply from server

在这种情况下,Empty reply from serverOK(它不使用 HTTP 协议)。数据库正在监听该 IP-端口组合。在 IP 地址 172.22.0.4 和端口 3000 的应用程序后台也会发生同样的情况:

[vagrant@standalone ~]$ curl -I 172.22.0.4:3000
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Date: Sat, 16 Nov 2019 11:38:22 GMT
Connection: keep-alive

在这种情况下,我们可以在浏览器中打开 http://172.22.0.4:3000。应用程序将可见,但只能在本地使用。它还没有被发布。

  1. 让我们部署负载均衡器组件。该组件将在我们的主机上发布一个端口。请注意,我们添加了两个环境变量,以允许负载均衡器连接到后台应用程序(我们通过这些变量动态配置了负载均衡器,因为这个镜像已被修改以支持这种行为):
[vagrant@standalone ~]$ docker container run -d \
--name simplelb \
--env APPLICATION_ALIAS=simpleapp \
--env APPLICATION_PORT=3000 \
--network simplenet \
--publish 8080:80 \
codegazers/simplestlab:simplelb
35882fb4648098f7c1a1d29a0a12f4668f46213492e269b6b8262efd3191582b
  1. 让我们查看本地的 iptables。Docker 守护进程已添加了一条 NAT 规则,将流量从端口 8080 引导到负载均衡器组件上的端口 80
[vagrant@standalone ~]$ sudo iptables -L DOCKER -t nat --line-numbers --numeric
Chain DOCKER (2 references)
num target prot opt source destination 
1 RETURN all -- 0.0.0.0/0 0.0.0.0/0 
2 RETURN all -- 0.0.0.0/0 0.0.0.0/0 
3 RETURN all -- 0.0.0.0/0 0.0.0.0/0 
4 RETURN all -- 0.0.0.0/0 0.0.0.0/0 
5 RETURN all -- 0.0.0.0/0 0.0.0.0/0 
6 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.22.0.2:80

请注意,负载均衡器将在所有主机的 IP 地址上可用,因为我们没有在发布选项中设置任何特定的 IP 地址。

  1. 现在,在你的网页浏览器中打开 http://localhost:8080。你将能够使用已部署的应用程序。你将在浏览器中看到以下图形用户界面:

这个图形用户界面实际上是应用程序后台的首页。正如我们之前提到的,它并不是真正的三层应用程序。我们添加了一个负载均衡器作为前端,目的是能够发布它并在其中添加一些规则。

为了确保应用程序仅在所需的接口上监听,我们可以指定它们以避免不安全的接口。始终使用特定的 IP 地址与 --publish 选项一起发布应用程序(例如,--listen MY_PUBLIC_IP_ONLY:8080:80),以便在定义的 IP 地址上发布你的应用程序。

在本实验中,我们发布了一个简单的应用程序,并确保只有特定组件在外部可见。记住,使用容器网关和仅内部网络是可行的。这些功能将提高应用程序的安全性。

摘要

在本章中,我们回顾了如何管理与容器相关的数据。我们查看了管理进程数据及其状态的不同策略。我们使用了主机文件系统、无名卷和有名卷,并学习了如何通过使用插件扩展可用的 Docker 守护进程卷管理功能。我们注意到,Docker 守护进程不会处理任何应用程序锁,也不会确定主机级别的存储资源如何定义。

有两种不同的方式可以使用 --volume--mount 将卷或绑定挂载到容器中。我们还回顾了所有必要的参数及它们之间的区别。

我们讨论了如何在高可用性环境中管理数据和进程状态。我们还没有介绍任何编排概念,但理解高可用性或多个进程实例将需要特别的应用程序逻辑是很重要的。Docker 不会管理这些逻辑,而这是你必须意识到的。

我们还介绍了一些基本的网络概念。我们解释了可以在 Docker 守护进程中直接使用的不同类型的网络以及每种网络的特殊功能。接着我们回顾了容器之间的交互,以及它们如何与外部网络通信。最后,我们通过学习如何发布在容器内部运行的应用程序进程来结束本章内容。

下一章将介绍如何在多个容器上运行应用程序。我们将学习应用程序组件如何运行和交互。

问题

在本章中,我们回顾了非集群环境中的容器持久性和网络配置。接下来,我们通过一些问题来验证我们对这些主题的理解:

  1. 以下哪一项说法是错误的?

a) 容器不是短暂的——一旦创建,它们会保留在主机中,除非被删除。

b) 我们可以使用相同的镜像同时运行多个容器。

c) 从同一镜像创建的容器共享它们的文件系统。

d) 所有这些说法都是错误的。

  1. 创建卷时允许使用哪些方法?

a) 我们可以使用 docker volume create 命令手动创建卷对象。

b) 我们可以在 Dockerfile 中声明一个 VOLUME 语句,以便在从构建的镜像创建的容器中使用卷。

c) 我们可以像使用 Docker 卷一样,在容器内部使用 Docker 主机的文件系统。

d) 仅在容器创建或执行时允许创建卷。

  1. 当我们删除容器时,所有相关的卷都会被删除。这是真的吗?

a) 这是错误的。你需要在删除容器时使用 --force-f 选项。

b) 这是错误的。你需要在删除容器时使用 --volumes-v 选项。

c) 这是错误的。你需要在删除容器时使用 --volumes-v 选项,且只会删除未命名的卷。

d) 这是错误的。卷只能通过 docker volume rmdocker volume purge 手动删除。

  1. 以下哪一项关于容器网络的说法是错误的?

a) 默认情况下,所有暴露的容器端口都可以从 Docker 主机访问。

b) docker network prune 将删除所有未使用的网络。

c) 默认情况下,所有桥接网络都可以动态连接。

d) Docker 为每个自定义桥接网络提供了一个内部 DNS。

  1. 以下哪一项关于容器发布暴露端口 80 的 Nginx Web 服务器的说法是正确的?

a) 如果我们使用主机驱动程序,我们需要以NET_ADMIN权限运行此容器。

b) 如果我们使用--publish-all-P选项,主机层将为每个暴露的容器端口关联一个介于3276865535之间的随机端口。你需要在iptables中添加一条 NAT 规则,以允许请求访问容器的内部端口80

c) 使用--publish 192.168.2.100:1080:80,我们将确保只有指向主机 IP 地址192.168.2.100端口1080的请求会被重定向到内部 Web 服务器容器端口。(我们假设 IP 地址192.168.2.100是一个主机接口。)

d) 如果我们使用--publish 80-p 80,主机层将为端口80关联一个介于3276865535之间的随机端口,并且会在iptables中添加一条 NAT 规则。

进一步阅读

以下链接将帮助你更深入地了解卷和网络概念:

部署多容器应用程序

在本章中,我们将学习 Docker Compose 工具,这是任何 Docker 环境中的关键组件。使用 Docker Compose,我们可以管理多容器应用程序,所有我们通常用来管理基于容器的应用程序的操作和功能,Docker Compose 都能在多容器环境中提供。我们可以一次性构建项目所需的所有镜像,无需逐个构建、拉取/推送和执行容器。我们可以在一个文件中声明所有组件及其相互连接、存储、环境等。我们还能够从单一端点调试多容器应用程序,这在生产环境中有许多独立元素运行时尤为重要。

但这不仅仅是一个工具。Docker Compose 声明了一种新类型的文件,docker-compose.yaml。该文件提供了多容器应用程序的所有要求,并可与其他 Docker 工具一起使用。引入这种文件类型非常重要,因为它是 Swarm 编排部署和最新的基于 CNAB 的应用程序的基础。我们在本书中不会介绍云原生应用程序捆绑包CNABs),但如果你感兴趣,可以查看cnab.io。Docker 有自己的 CNAB 实现,但在编写本书时,它处于实验阶段,并且不属于 DCA 考试的内容。

在本章中,我们将回顾 Docker Compose。我们将学习如何通过不同方法安装该工具,并了解其关键内容以及如何使用它们。我们将探索该工具提供的一些操作及其应用场景。最后,我们将给出一些使用 docker-compose 和变量的技巧,这可以让我们使用相同的部署文件为不同环境提供动态内容。

本章将涵盖以下主题:

  • 安装和使用 Docker Compose

  • 理解 docker-compose.yaml 文件

  • 使用 docker-compose

  • 使用 docker-compose 自定义镜像

  • 使用 docker-compose 自动化你的桌面和 CI/CD

让我们开始吧!

第七章:技术要求

在本章中,我们将学习 Docker 化的多容器应用程序。我们将在本章的最后提供一些实验室练习,帮助你理解和学习所涵盖的概念。这些实验室可以在你的笔记本电脑或 PC 上运行,使用提供的 Vagrant 独立环境或你自己已经部署的 Docker 主机。查看本书 GitHub 代码库中的额外信息:github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

查看以下视频,查看代码的实际操作:

"bit.ly/3hz0IB0"

安装和使用 Docker Compose

在深入了解 Docker Compose 工具之前,让我们先了解一下多容器应用和多服务应用之间的区别:

  • 多容器应用是基于多个容器的应用。这些容器将一起在同一主机上运行。因此,我们可以在我们的笔记本电脑或任何其他 Docker 守护进程上部署多容器应用。所有应用组件将一起在一台主机上运行。因此,可能的网络性能问题将得到缓解,因为所有组件会一起运行。请注意,这种部署方式如果主机宕机将无法提供高可用性。我们可以配置所有组件的自动重启,但这对于生产环境来说还不够。

  • 多服务应用是基于多个服务的应用。这些应用将通过 Swarm 编排运行,容器将分布在不同的主机上。我们将在第八章《使用 Docker Swarm 编排》中了解 Docker Swarm 编排。但你应该理解,服务是 Docker Swarm 环境中调度的最小单元。我们不会调度容器;我们会调度一个服务,基于执行多个任务的情况。这些任务与容器相关联;事实上,每个任务对应一个容器。因此,一个服务由多个任务(称为副本)组成,负责运行容器。我们在 Docker Swarm 中调度服务,设置所需的副本数量以确保服务健康。Docker Swarm 会处理容器的状态。正如我们之前提到的,服务将分布在不同的主机上运行。多服务应用的组件通常会在整个集群中分布运行。组件之间的互联将依赖于内部和外部网络,而 Swarm 则提供基于弹性的开箱即用高可用性,确保所有服务任务的可用性。请牢记这些特性。我们将在《容器编排》部分学习 Swarm 和 Kubernetes 编排背后的强大功能。

总结来说,我们在一个节点上部署多容器应用,而多服务应用则分布在不同的节点上运行。

docker-compose 在安装 Docker 时并不随附。它是一个不同的产品。在 macOS 和 Windows 上的 Docker Desktop 中,Docker Compose 已包含并可以直接使用。

我们首先需要了解 Docker Compose 是一个基于 Python 的应用。因此,我们可以像安装其他 Python 模块一样安装它,或者下载它作为二进制文件。我们还可以在容器内运行 docker-compose。我们可以在docs.docker.com/compose/install找到简单的安装说明。请注意,在撰写本文时,最新的 docker-compose 版本是 1.24.1。我们将使用此版本来进行接下来的所有安装方法。

安装 docker-compose 作为 Python 模块

使用pip(Python 模块安装程序)在 Linux 系统上安装很容易。我们将回顾这种方法,并且还将下载 Docker Compose 二进制文件。首先,我们需要在系统中安装pip。它是几乎所有 Linux 系统上都能找到的包,因此是否已经安装取决于所使用的 Linux 版本(包名可能是py-pippython3-pippip-python;这实际上取决于你的操作系统和使用的 Python 版本)。

我们不会涵盖这个软件包的安装,并假设您已在系统中安装了pip。我们将以 root 用户身份安装docker-compose模块,以便所有主机用户都可以使用它。

有一个适用于 Python 2.x 的pip版本和一个适用于 Python 3.x 的版本。记住,Python 2.x 现在已经过时,所以可能是时候迁移到 Python 3.x 了。出于这个原因,我们只会讨论 Python 3 的安装。

我们使用sudo作为 root,并加上-H来使用我们登录用户的主目录:

$ sudo -sH pip install -q docker-compose

执行后,我们将在/usr/local/bin/docker-compose安装docker-compose

使用下载的二进制文件安装 docker-compose

在这里,我们只需要curlwget来从该项目的 GitHub 页面(github.com/docker/compose/releases)下载定义版本的二进制文件。确保选择适合您架构处理器和系统的正确二进制文件和版本。我们将回顾 CentOS 7 Linux 系统的安装,它用于我们的所有实验室:

$ curl -sL "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /tmp/docker-compose

$ sudo chmod +x /tmp/docker-compose

$ sudo mv /tmp/docker-compose /usr/local/bin/docker-compose

我们还可以使用容器来执行docker-compose,如下一节中所述。

使用容器执行 docker-compose

这一点相当有趣,因为正如我们所学,作为容器执行应用程序只需要在我们的系统上运行 Docker 守护进程。这是执行应用程序的一个好方法!在这种情况下,run.sh是一个脚本,它将准备所有所需的卷和参数(curl -L将跟随重定向,-o参数将允许我们选择目标文件名):

$ sudo curl -L --fail https://github.com/docker/compose/releases/download/1.24.1/run.sh -o /usr/local/bin/docker-compose

$ sudo chmod +x /usr/local/bin/docker-compose

Docker Compose 也可以安装在 Windows 节点上,如下一节中所述。

在 Windows 服务器上安装 docker-compose

在 Windows 服务器上,我们将使用提升的 PowerShell(即以管理员身份运行)。

由于 GitHub 现在要求使用 TLS1.2,在执行安装之前,需要在管理员 PowerShell 中运行以下命令:

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

一旦进入管理员 PowerShell,我们需要运行以下命令:

Invoke-WebRequest "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-Windows-x86_64.exe" -UseBasicParsing -OutFile $Env:ProgramFiles\Docker\docker-compose.exe

在下一节中,我们将学习 Docker Compose 文件。

理解 docker-compose.yaml 文件

Docker Compose 引入了多容器应用程序的概念,使用一个集成所有应用组件定义的文件。这个文件被称为docker-compose.yaml。通常,我们会管理一个docker-compose.yaml文件。请注意,这是一个 YAML 文件,因此缩进至关重要。该文件将包含所有应用组件及其属性。

这是一个简单的docker-compose.yaml文件的样子(我们可以使用.yaml.yml扩展名来表示 YAML 文件):

version: "3.7"
services:
  lb:
    build: simplestlb
    image: myregistry/simplest-lab:simplestlb
    environment:
      - APPLICATION_ALIAS=simplestapp
      - APPLICATION_PORT=3000
    networks:
      simplestlab:
          aliases:
          - simplestlb
    ports:
      - "8080:80"
  db:
    build: simplestdb
    image: myregistry/simplest-lab:simplestdb
    environment:
        - "POSTGRES_PASSWORD=changeme"
    networks:
       simplestlab:
        aliases:
          - simplestdb
    volumes:
      - pgdata:/var/lib/postgresql/data
  app:
    build: simplestapp
    image: myregistry/simplest-lab:simplestapp
    environment:
      - dbhost=simplestdb
      - dbname=demo
      - dbuser=demo
      - dbpasswd=d3m0
    networks:
       simplestlab:
        aliases:
          - simplestapp
    depends_on:
      - lb
      - db
volumes:
  pgdata:
networks:
  simplestlab:
    ipam:
      driver: default
      config:
        - subnet: 172.16.0.0/16

docker-compose.yaml文件将包含所有基于 Docker 的应用组件(服务、网络和卷)的定义。在此文件中,我们首先声明文件定义的版本。这个定义管理 Docker Compose 如何解释一些编写的指令。我们将使用版本 3.x 的文件定义,因为它是最新的并且是目前推荐的版本。不同版本之间有一些差异,尽管docker-compose提供了向后兼容性,所以你应该查阅 Docker 文档获取更多信息。需要注意的是,版本之间的键和值结构可能有所不同,因此在使用较旧的 Docker 引擎时,应该使用特定的版本。我们将使用版本 3.7(这是编写时的当前版本)。

让我们来学习一下文件的内容。

我们使用环境变量来提供凭证并访问某些服务。这只是为了演示目的——绝不要使用环境变量来存储密码、凭证或连接字符串。在 Docker Swarm 中,我们使用secretsconfiguration对象。在 Docker Compose 中,我们没有这种类型的对象,因此建议使用外部配置工具或安全的键值存储来管理这些值。

我们有一个用于services的部分和另一个用于networks的部分。我们还可以有一个volumes部分。volumesnetworks部分将为应用程序定义它们的属性。在这些部分,我们将声明特殊功能以及为其使用的驱动程序。在示例文件中,我们声明了一个特殊的子网,用于在simplestlab网络上使用默认的桥接驱动程序。正如我们在所有服务定义中看到的,这将是为所有组件创建并使用的网络。

在 Docker Swarm 中,我们还可以定义ConfigsSecrets,它们是集群对象。我们将在文件的一个部分中声明这些对象,然后在每个服务定义中使用这些对象。

每个服务代表一个组件。让我们以app服务的定义为例,详细了解一下。

每个服务定义都有一些关键配置,用来说明该应用组件如何运行。在app服务中,我们有一个build定义,指示如何创建这个组件。build键的值指示用于构建这些组件镜像的上下文路径(即simplestapp目录)。因此,我们可以使用这个docker-compose.yaml文件和simplestapp目录中的内容来构建这个组件。我们已经学到,为了构建镜像,我们需要一个 Dockerfile;因此,simplestapp目录中必须包含一个 Dockerfile。所有需要编译myregistry/simplest-lab:simplestapp镜像的文件都应该在这个目录中。

当我们讨论 Docker Compose 中的多容器应用时,服务定义与 Swarm 服务不同,后者由 Swarm 编排管理。在非 Swarm 环境中,我们将服务称为应用组件。

下一行包含image键,定义了镜像的名称。如果该镜像在主机中不存在,它将使用这个名称进行构建。如果没有build定义,Docker 守护进程将尝试从镜像仓库下载已定义的镜像。

下一个键定义了一组变量及其值,在容器执行期间作为环境变量使用。我们可以覆盖镜像定义的ENVIRONMENTCMDENTRYPOINTVOLUME值等,正如我们在容器中通常所做的那样。稍后我们将进一步了解 Docker Compose 定义,但请记住,我们在docker container rundocker container create操作中使用的几乎每个选项,都可以作为docker-compose.yaml中的一个键来使用。

接着,我们定义了此组件中要使用的网络。我们还定义了一个别名,用于在该网络中使用。这个组件将被称为app,这是它的服务名称,同时也使用其定义的别名simplestapp

值得注意的是,Docker Compose 允许我们定义执行顺序,正如在最后几行中看到的那样。我们使用了depends_on键,直到列表中的所有组件都可用(即所有容器都被标记为健康)后才继续执行。

这样,我们已经回顾了前面代码文件中的services部分。在这个例子中,我们还有volumesnetworks部分。

volumes部分,我们有最简单的定义。它是空的,仅定义了一个具有默认参数(本地驱动程序)的卷。在services部分,我们定义了这些卷应该在哪里以及如何附加。

现在我们了解了基本概念,可以看看一些最常用的键定义:

Key Definitions

| build | 这个键定义了用于构建应用程序镜像的选项。以下是一些最常用的选项:

  • context:此选项定义了构建上下文的路径,即包含 Dockerfile 和所有其他所需文件的目录。

  • dockerfile: 这定义了一个替代的 Dockerfile 名称。

  • args: 我们可以在这里设置 Dockerfile 参数。

  • labels: 此选项允许我们设置镜像标签。

|

image 这是要使用的镜像名称。如果镜像不存在,将从注册表中拉取。如果必须构建镜像,则会使用此值作为其名称。
environment 我们可以在容器内设置环境变量。这将覆盖任何镜像定义的值。我们也可以使用env_file来定义一个包含许多值的文件。
command 这将设置或覆盖镜像的command定义。
entrypoint 这将设置或覆盖镜像的entrypoint定义。
ports 这些是服务要公开的端口,以便在主机级别可达。
expose 此选项定义了哪些服务端口将对其他服务可用。
privileged``cap_add/cap_drop``read_only 这些选项将设置与我们在第三章中讨论的容器执行相同的功能。
user 这将设置或覆盖镜像的user定义。
labels 这将设置或覆盖镜像的标签。
restart 使用restart,我们可以设置关联容器的管理方式。如果它们停止了,Docker 是否应该重新启动它们或者保持停止状态?请记住为我们的容器定义的选项 - 我们将在这里使用相同的值。
container_name 我们可以使用此变量设置容器名称。如果未定义,容器名称将使用服务项目名称作为前缀,后跟服务名称和实例编号,从1开始。请注意此参数;正如您已经了解的那样,每台主机只能有一个定义名称的容器。
hostname``domainname 这些选项将允许我们更改容器的主机名和其域名。在network定义下,我们可以添加任意数量的 DNS 别名。
extra_hosts 使用此选项,我们可以添加外部主机以通过内部 DNS 发现。这将帮助我们像在容器内运行一样访问外部服务。
depends_on 此键允许我们设置组件的依赖关系。它在版本3中已被弃用,但在这里包括是为了解释它实际上并未提供真正的依赖性。此选项只会控制启动顺序。

| networks | 我们可以设置要使用的网络驱动程序、它们的选项和子网范围,以及它们将如何可访问(内部和/或可附加)。让我们来看一个简单的例子: |

networks:
  mynet:
    driver: bridge
    ipam:
      driver: default
      config:
       - subnet: 172.28.0.0/16

在上述代码中,我们已将mynet定义为所有容器的桥接网络,并为其定义了一个子网。我们可以在每个服务部分使用这个定义的网络:

  myservice:
 build:
 context: .
 dockerfile: ./src/myapp/Dockerfile
 networks:
 - mynet

|

| volumes | 卷在volumes部分中定义。我们可以设置它们的驱动程序和特殊选项。以下是一个我们可以在services部分中使用的简单本地定义示例:

...
...
  myservice:
    image: myregistry/myimage:tag
    volumes:
     - data:/appdata/
...
...
volumes:
  config-data:
    driver: local

|

| tmpfs | 我们可以使用tmpfs创建一个内存文件系统。此选项对于绕过叠加文件系统以提高 I/O 性能或出于安全原因非常有用。当容器终止时,内存文件系统会消失:

 - type: tmpfs
     target: /app
     tmpfs:
       size: 1000

|

healthcheck 这将设置或覆盖镜像的healthcheck定义。

这些是最常用的键。有关更多信息,请查阅 Docker Compose 文档,文档可以在 Docker 官方网站上找到,网址为docs.docker.com/compose/compose-file/

有许多仅允许在 Docker Swarm 环境中使用的键。我们没有在前面的信息表中包含它们,因为 Swarm 选项将在第八章,使用 Docker Swarm 进行编排中展示。在docker-compose.yaml文件中定义容器资源限制仅在使用 Docker Swarm 模式或 Docker Compose 版本 2 时允许。

一旦我们创建了docker-compose.yaml文件,就可以使用文件中编写的 Docker Compose 命令行定义。

使用 Docker Compose 命令行界面

我们在前一节中安装了docker-compose二进制文件,这意味着我们现在可以查看可用的操作。docker-compose将提供大部分适用于 Docker 的操作,因为我们将同时在多个容器上执行这些操作。让我们在下表中查看可用的docker-compose操作:

命令 定义
build 如预期的那样,此操作将构建或重建所有docker-compose.yaml文件中的组件,或者仅构建选定的组件。此操作将查找docker-compose.yaml文件中的任何build键,并启动构建或重建。如果我们使用--project设置了项目名称,则所有镜像将在没有定义镜像名称的情况下被创建为<project_name>_<service_name>。如果定义了镜像名称,这个名称将在我们将其推送到注册表时使用。
pull/push 我们将能够一次性推送或拉取所有镜像,因为我们使用docker-compose管理所有应用程序组件。
images 此操作将列出所有应用程序镜像。
create 请记住,我们可以创建容器。在这种情况下,我们将创建应用程序所需的所有容器,但直到执行start操作时,它们才会被启动。
rm 此操作将删除所有已停止的容器。记得使用项目名称,或者留空以使用当前目录作为应用程序名称。
up (-d--detach) 我们将通过这个简单的动作创建并启动所有组件。所有组件将一次性运行。我们将使用 --detach 使应用在后台运行,就像我们在容器中学到的那样。
down 要移除所有应用组件,我们将使用 down 动作。这将结束所有应用容器,或仅结束指定的容器。请注意,外部定义的资源不会被删除,必须手动移除。
start/stop/restart 这些选项将允许我们管理组件,可以一次性应用于所有组件,或仅应用于指定的组件。
run 通过这个选项,我们可以执行一个组件来运行指定的命令,例如初始化数据库或创建所需文件。
pause/unpause 就像我们在容器中学到的那样,我们可以暂停和恢复应用组件。
ps docker-compose 将显示所有应用容器(进程)及其端口。
top 这个选项将显示为应用程序部署的每个容器上运行的进程。
exec 我们可以在任何应用容器的命名空间内运行一个进程。记住我们在第三章中学到的内容,运行 Docker 容器
logs 使用单个命令检索所有应用容器的日志非常有用。我们可以使用 logs 动作一次性检索所有应用日志。日志将按服务名称一起显示,帮助我们识别每个组件。
config 我们可以使用 config 动作验证 Docker Compose 定义。我们还可以使用 services 参数列出已定义的服务。

有了这些信息,我们可以快速了解如何在多容器环境中通过 Docker Compose 实现常见的容器工作流程,它为我们提供了一个新的命令行界面来构建、共享并一次性运行所有应用组件。 |

我们可以定义外部资源,如 volumenetworks。在这些情况下,我们将使用 external: true 选项,并且必须手动创建这些资源。 |

使用 docker-compose 部署的每个应用将有自己的项目定义。每个项目将在同一主机上与其他项目隔离运行。默认情况下,docker-compose 将使用当前目录名作为项目名。我们可以使用 --project-name-p 覆盖此行为,设置一个更具描述性的名称。

在第一章,使用 Docker 构建现代基础设施与应用程序中,我们了解到对象名称是唯一的(我们可以为对象分配多个名称,但每个名称都是唯一的,且不能有重复的名称);因此,docker-compose 会将项目名称作为前缀添加到每个创建的对象。这使得我们能够识别所有应用程序组件并确保它们具有唯一的名称。当然,我们可以使用相同的 docker-compose 文件两次部署相同的应用程序,但每次应选择不同的项目名称。

我们可以使用 docker-compose.yaml 文件多次启动相同的应用程序,但我们不能在卷之间共享唯一资源,如端口、卷和 IP 地址。组件间共享卷取决于应用程序行为,但 IP 地址或端口将在给定的 Docker 主机上保持唯一。

让我们回顾一下使用之前的 docker-compose.yaml 文件(见 理解 docker-compose 文件 部分)进行完整应用程序部署的工作流程。

首先,我们需要构建应用程序镜像。你可以从本书的 GitHub 仓库下载所有应用程序代码,链接地址为 github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

让我们克隆这个仓库,以获取所有源代码目录和配置文件。你的输出可能与以下内容不同:

$ git clone https://github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git
Cloning into 'dca-book-code'...
remote: Enumerating objects: 26, done.
remote: Counting objects: 100% (26/26), done.
remote: Compressing objects: 100% (22/22), done.
remote: Total 26 (delta 0), reused 26 (delta 0), pack-reused 0
Unpacking objects: 100% (26/26), done.

我们将为 simplest-lab 项目创建一个目录,其中包含 docker-compose.yaml 文件以及每个应用程序组件的不同目录:

$ cd chapter5/simplest-lab/

$ ls -lRt
.:
total 4
-rw-rw-r-- 1 zero zero 982 nov 24 11:06 docker-compose.yaml
drwxrwxr-x 2 zero zero 146 nov 24 11:06 simplestapp
drwxrwxr-x 3 zero zero 112 nov 24 11:06 simplestdb
drwxrwxr-x 2 zero zero 80 nov 24 11:06 simplestlb
./simplestapp:
total 32
-rw-rw-r-- 1 zero zero 91 nov 24 11:06 dbconfig.json
-rw-rw-r-- 1 zero zero 466 nov 24 11:06 Dockerfile
-rw-rw-r-- 1 zero zero 354 nov 24 11:06 package.json
-rw-rw-r-- 1 zero zero 191 nov 24 11:06 README.md
-rw-rw-r-- 1 zero zero 1244 nov 24 11:06 reset.html
-rw-rw-r-- 1 zero zero 3837 nov 24 11:06 simplestapp.html
-rw-rw-r-- 1 zero zero 6556 nov 24 11:06 simplestapp.js
./simplestdb:
total 12
drwxrwxr-x 2 zero zero 26 nov 24 11:06 docker-entrypoint-initdb.d 
-rwxrwxr-x 1 zero zero 2587 nov 24 11:06 docker-entrypoint.sh 
-rw-rw-r-- 1 zero zero 152 nov 24 11:06 Dockerfile 
-rw-rw-r-- 1 zero zero 2568 nov 24 11:06 Dockerfile.scratch
./simplestdb/docker-entrypoint-initdb.d:
total 4
-rw-rw-r-- 1 zero zero 484 nov 24 11:06 init-demo.sh
./simplestlb:
total 16
-rw-rw-r-- 1 zero zero 467 nov 24 11:06 Dockerfile
-rwxrwxr-x 1 zero zero 213 nov 24 11:06 entrypoint.sh
-rw-rw-r-- 1 zero zero 837 nov 24 11:06 nginx.conf
-rw-rw-r-- 1 zero zero 24 nov 24 11:06 README.md

在每个项目目录中,都有一个 Dockerfile,我们可以用来构建该特定组件。所以,让我们一次性构建所有组件。

我们在移除中间容器(用于构建并禁止镜像缓存)时,拥有与 docker image build 命令相同的选项。我们将分别使用 --force-rm--no-cache

为了确保定义的 docker-compose.yaml 文件有效,我们可以使用 docker-compose config --quiet。如果有问题,它将被报告。我们还可以列出已定义的服务或卷的名称:

$ docker-compose config --services
db
lb
app 
$ docker-compose config --volumes
pgdata

我们将在本节后面使用这些服务名称定义。

我们将执行 docker-compose build 来构建在 docker-compose.yaml 文件中定义的所有组件镜像。此命令将花费一些时间,因为我们不仅仅是在构建一个镜像,而是在构建所有必需的镜像。以下输出已被截断:

$ docker-compose build 
Building db
Step 1/2 : FROM postgres:alpine
alpine: Pulling from library/postgres
....
Successfully built 336fb84e7fbf
Successfully tagged myregistry/simplest-lab:simplestdb
Building lb
Step 1/10 : FROM alpine:latest
latest: Pulling from library/alpine
....
Successfully built 4a5308d90123
Successfully tagged myregistry/simplest-lab:simplestlb
Building app
Step 1/15 : FROM alpine
 ---> 965ea09ff2eb
Step 2/15 : RUN apk --update --no-progress --no-cache add nodejs npm
....
Successfully built ffa49ee4228e
Successfully tagged myregistry/simplest-lab:simplestapp

几分钟后(或几秒钟,取决于您的互联网连接和处理器速度),将创建所有三个映像。由于我们尚未设置项目名称,docker-compose已为您创建了一个项目名称。如前所述,默认情况下,所有组件都将使用目录名作为前缀创建。在这种情况下,我们在docker-compose.yaml文件上有一个映像键,因此将使用映像命名语法,而不是本地目录引用。

请注意,我们使用了一个虚拟的注册表名称(myregistry)。这意味着我们无法将映像推送到这个虚拟注册表,但重要的是理解映像名称背后的逻辑。如果我们在 Docker 守护程序上列出当前映像,我们应该有为这个项目创建的所有映像。

$ docker images --filter=reference='myregistry/*:*'
REPOSITORY TAG IMAGE ID CREATED SIZE
myregistry/simplest-lab simplestapp ffa49ee4228e About an hour ago 56.5MB
myregistry/simplest-lab simplestlb 4a5308d90123 About an hour ago 7MB
myregistry/simplest-lab simplestdb 336fb84e7fbf About an hour ago 72.8MB

现在我们有了我们的映像,我们可以分享它们。我们现在可以执行docker-compose push命令将它们推送到myregistry(在我们的示例文件中)。这将逐个上传带有定义标签的映像。

我们准备使用docker-compose up一起运行所有应用程序组件。为了在后台启动它,我们将使用--detach选项。如果我们不使用此选项,我们将连接到所有容器的标准输出和错误输出。我们学会了如何在第三章,运行 Docker 容器中附加到容器输出。请记住,在docker container run中没有--detach-d选项时,这种行为是预期的:

$ docker-compose up --detach
Creating network "simplest-lab_simplestlab" with the default driver
Creating simplest-lab_db_1 ... done
Creating simplest-lab_lb_1 ... done
Creating simplest-lab_app_1 ... done

通过这行,我们刚刚启动了我们的应用程序。重要的是要理解docker-compose up不仅仅执行所有组件。在这种情况下,我们首先构建了我们的组件,但是docker-compose up指令将验证组件映像是否存在于 Docker 主机上。如果不存在,它将构建或拉取它们。如果映像不存在,它们应该被下载,这就是 Docker 守护程序将要做的事情。

应用程序应该正在运行。让我们验证所有组件的执行。我们将使用docker-compose ps来获取应用程序组件的状态:

$ docker-compose ps
 Name Command State Ports 
----------------------------------------------------------------------------------
simplest-lab_app_1 node simplestapp.js 3000 Up 3000/tcp 
simplest-lab_db_1 docker-entrypoint.sh postgres Up 5432/tcp 
simplest-lab_lb_1 /entrypoint.sh /bin/sh -c ... Up 0.0.0.0:8080->80/tcp

查看应用程序组件名称。它们都使用simplest-lab前缀创建,后跟_和服务定义中使用的名称。这是我们预期的,因为我们还没有定义项目名称。默认情况下,目录名被用作项目名称。

我们还可以看到组件名称以 _ 结尾,后跟一个数字(在本例中为 1)。这表示我们为此组件有多个副本。我们对某些应用程序组件使用多个副本。请记住,Docker Compose 不了解我们应用程序的逻辑。因此,我们需要编写此组件以使其可伸缩。在我们的示例中,我们有一个三层应用程序,包括一个简单的负载均衡器 lb,一个应用程序后端 app 和一个数据库组件 db。我们将无法扩展我们的数据库组件,因为这将破坏数据库数据。不超过一个 postgres 进程可以使用特定的数据文件集,这也适用于我们的情况。另一方面,我们的 app 示例应用程序组件准备好多次运行。

让我们来看看我们的应用程序环境。通过查看 docker-compose ps 的输出,我们可以看到只有一个组件在暴露其服务。我们只发布了 lb 组件。这是我们的应用程序前端(实际上,它是一个负载均衡器组件,将流量路由到不同的 app 组件后端)。如果我们在 http://0.0.0.0:8080 上在 Web 浏览器中打开,我们将获得一个类似于以下截图中显示的 Web 应用程序:

在此时,应用程序已经部署完成。我们可以使用服务名称和 docker-compose logs 命令来查看组件日志。如果不添加服务名称,我们将查看使用此 docker-compose.yaml 文件部署的所有容器的日志。这非常有用,因为我们可以从单一端点查看所有输出。每个组件的日志将以不同的颜色显示,以帮助我们区分它们。

例如,要查看数据库组件日志,我们将使用以下命令:

$ docker-compose logs db
Attaching to simplest-lab_db_1
db_1 | 
db_1 | PostgreSQL Database directory appears to contain a database; Skipping initialization
db_1 | 
db_1 | 2019-11-24 11:57:14.011 UTC [1] LOG: starting PostgreSQL 12.1 on x86_64-pc-linux-musl, compiled by gcc (Alpine 8.3.0) 8.3.0, 64-bit
db_1 | 2019-11-24 11:57:14.011 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
db_1 | 2019-11-24 11:57:14.011 UTC [1] LOG: listening on IPv6 address "::", port 5432
db_1 | 2019-11-24 11:57:14.025 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"

需要注意的是,服务名称是我们 docker-compose.yaml 文件中定义的名称,而不是正在运行的服务名称。

所有 docker-compose 命令都需要一个 docker-compose.yaml 文件(或使用 --file-f 选项指定的任何其他文件名)和一个项目名称(默认情况下使用 --project-p 选项定义,或者使用当前目录)。这两个参数定义了所有 docker-compose 命令将应用的实例。

就像我们在 第三章 中处理容器时,运行 Docker 容器,我们可以使用 docker-compose exec 在容器的进程命名空间内运行新的进程:

$ docker-compose exec app sh
/APP $ ls -lart
total 344
-rwxr-xr-x 1 root root 314658 May 24 2017 Chart.js
-rw-rw-r-- 1 root root 6556 Nov 24 10:06 simplestapp.js
-rw-rw-r-- 1 root root 1244 Nov 24 10:06 reset.html
-rw-rw-r-- 1 root root 354 Nov 24 10:06 package.json
-rw-rw-r-- 1 root root 91 Nov 24 10:06 dbconfig.json
-rw-rw-r-- 1 root root 3826 Nov 24 14:38 simplestapp.html
-rw-r--r-- 1 root root 7654 Nov 24 14:38 package-lock.json
drwxr-xr-x 31 root root 4096 Nov 24 14:38 node_modules
drwxr-xr-x 1 root root 22 Nov 24 14:38 .
drwxr-xr-x 1 root root 6 Nov 24 14:38 ..

注意,默认情况下它分配一个终端。因此,不需要使用 -t-i 选项。

使用 docker-compose top 命令,我们将获取每个容器中每个进程的消耗情况:

$ docker-compose top
simplest-lab_app_1
UID PID PPID C STIME TTY TIME CMD 
--------------------------------------------------------------------------
zero 9594 9564 0 15:38 ? 00:00:05 node simplestapp.js 3000

simplest-lab_db_1
UID PID PPID C STIME TTY TIME CMD 
-------------------------------------------------------------------------------------------
70 9374 9304 0 15:38 ? 00:00:00 postgres 
70 9558 9374 0 15:38 ? 00:00:00 postgres: checkpointer 
70 9559 9374 0 15:38 ? 00:00:00 postgres: background writer 
70 9560 9374 0 15:38 ? 00:00:00 postgres: walwriter 
70 9561 9374 0 15:38 ? 00:00:00 postgres: autovacuum launcher 
70 9562 9374 0 15:38 ? 00:00:00 postgres: stats collector 
70 9563 9374 0 15:38 ? 00:00:00 postgres: logical replication launcher 
70 9702 9374 0 15:38 ? 00:00:00 postgres: demo demo 172.16.0.4(37134) idle

simplest-lab_lb_1
 UID PID PPID C STIME TTY TIME CMD 
--------------------------------------------------------------------------------------------------------------------
root 9360 9295 0 15:38 ? 00:00:00 nginx: master process nginx -g pid /run/nginx.pid; daemon off;
systemd+ 9467 9360 0 15:38 ? 00:00:01 nginx: worker process 
systemd+ 9468 9360 0 15:38 ? 00:00:00 nginx: worker process

让我们回顾一下这个多容器部署创建的一些对象。我们有一个新的网络,名称按照我们之前学过的格式定义;即,<项目或目录名>_<定义的网络名>。我们没有指定特定的网络类型,因此,默认情况下,它是一个桥接网络,符合预期。输出可能会在您的环境中有所不同,但新部署的网络名称将会存在:

$ docker network ls
NETWORK ID NAME DRIVER SCOPE
0950a6281629 bridge bridge local
82faac964567 host host local
2fb14f721dc3 none null local
a913507af228 simplest-lab_simplestlab bridge local

记住,所有自定义桥接网络都管理其自己的内部 DNS 解析。因此,在同一网络上部署的所有服务(应用组件)都可以通过它们的服务名称进行访问。

同样,我们定义的卷也会如此。如果列出本地卷,我们会得到一个新的卷,遵循相同的命名约定。输出可能会因您的环境而异,但新部署的卷名称将会存在:

$ docker volume ls
DRIVER VOLUME NAME
local 3f93b55b105f64dd03a9088405484909d2f8cad83dacc5fb5a53ea27af1f33e6
local mydbdata
local simplest-lab_pgdata
vieux/sshfs:latest sshvolume

我们可以使用docker-compose.yaml文件中定义的服务名称来停止和启动(或重启)任何服务。以下操作将重启一个定义服务的所有实例:

$ docker-compose restart lb
Restarting simplest-lab_lb_1 ... done

回到实例的概念,我们可以为一个服务定义多个实例。这就是为什么我们给所有实例编号的原因。如前所述,进程是否能扩展并不是 Docker 定义的,它与您的应用逻辑相关。在这个例子中,我们可以扩展app组件的实例数量。我们可以使用docker-compose scale命令来改变某个定义的应用组件的实例(容器)数量:

$ docker-compose scale app=5
WARNING: The scale command is deprecated. Use the up command with the --scale flag instead.
Starting simplest-lab_app_1 ... done
Creating simplest-lab_app_2 ... done
Creating simplest-lab_app_3 ... done
Creating simplest-lab_app_4 ... done
Creating simplest-lab_app_5 ... done

请注意,scale操作已被弃用,因此现在我们应该使用docker-compose up --scale <service=number_of_instances>

结果是,我们现在有了五个app应用组件实例。所有实例的 IP 地址都被添加到了内部 DNS 解析中。因此,我们可以通过轮询的方式将服务名称解析到所有实例的 IP 地址:

$ docker-compose ps
 Name Command State Ports 
----------------------------------------------------------------------------------
simplest-lab_app_1 node simplestapp.js 3000 Up 3000/tcp 
simplest-lab_app_2 node simplestapp.js 3000 Up 3000/tcp 
simplest-lab_app_3 node simplestapp.js 3000 Up 3000/tcp 
simplest-lab_app_4 node simplestapp.js 3000 Up 3000/tcp 
simplest-lab_app_5 node simplestapp.js 3000 Up 3000/tcp 
simplest-lab_db_1 docker-entrypoint.sh postgres Up 5432/tcp 
simplest-lab_lb_1 /entrypoint.sh /bin/sh -c ... Up 0.0.0.0:8080->80/tcp

如果我们回到应用程序的 GUI 界面http://localhost:8080/,会发现图表已经发生变化,因为请求现在已分配到五个不同的后端:

在这个图表中,我们可以看到现在有五个不同的 IP 地址,请求在它们之间分配。由于我们已经运行应用程序很长时间(期间自动化请求一直在执行),所以第一 IP 地址(启动的第一个实例)的请求会更多。

我们可以使用“Reset App Data”按钮从数据库中删除之前的数据。点击这个按钮后,查看请求计数。你可以选择等待更多请求(每 5 秒会生成一个新请求),或者直接点击“Make Request”按钮几次。现在,你应该看到类似于下面的图表:

该图表显示了五个已定义的 app 组件实例的请求分布。现在,我们将实例数量缩减为三个,如下所示:

$ docker-compose up -d --scale app=3
simplest-lab_db_1 is up-to-date
simplest-lab_lb_1 is up-to-date
Stopping and removing simplest-lab_app_4 ... done
Stopping and removing simplest-lab_app_5 ... done
Starting simplest-lab_app_1 ... done
Starting simplest-lab_app_2 ... done
Starting simplest-lab_app_3 ... done

现在,我们可以查看 app 实例:

$ docker-compose ps app
 Name Command State Ports 
----------------------------------------------------------------
simplest-lab_app_1 node simplestapp.js 3000 Up 3000/tcp
simplest-lab_app_2 node simplestapp.js 3000 Up 3000/tcp
simplest-lab_app_3 node simplestapp.js 3000 Up 3000/tcp

图表将再次变化,只有三个后端会接收请求(因为只有三个在运行)。我们将再次使用重置应用数据按钮,并获得一个类似于以下的图表:

快速查看与部署的 docker-compose.yaml 应用程序文件相关的运行容器。在这种情况下,我们使用过滤器来获取所有名称以 simplest 模式开头的容器。我们格式化了结果以仅显示它们的名称和标签:

请注意,docker-compose 为每个应用程序组件添加了标签,指示项目名称、容器名称和关联的服务名称。

我们可以轻松停止或杀死单个组件,或者一次性停止所有组件。我们还可以使用 downrm 选项移除所有组件。通常,我们使用 docker-compose down,因为它更容易记住。我们还可以使用 stop_grace_period 定义组件停止的超时时间,默认值为 10 秒(请参阅 docker-compose 文件参考,以获取在 docs.docker.com/compose/compose-file/ 中可用的选项)。使用 docker-compose down,组件在停止后将被移除:

$ docker-compose down 
Stopping simplest-lab_app_3 ... done
Stopping simplest-lab_app_2 ... done
Stopping simplest-lab_app_1 ... done
Stopping simplest-lab_lb_1 ... done
Stopping simplest-lab_db_1 ... done
Removing simplest-lab_app_3 ... done
Removing simplest-lab_app_2 ... done
Removing simplest-lab_app_1 ... done
Removing simplest-lab_lb_1 ... done
Removing simplest-lab_db_1 ... done
Removing network simplest-lab_simplestlab

让我们查看所有与应用程序相关的对象。在这里,我们可以看到 network 被移除,但 volume 仍然存在。这是因为 Docker 不知道该如何处理卷。我们稍后还会使用它吗?因此,除非我们使用 docker-compose down --volumes(或 -v)选项删除与应用程序相关的所有卷,否则最好不要删除卷:

$ docker volume ls
DRIVER VOLUME NAME
local 3f93b55b105f64dd03a9088405484909d2f8cad83dacc5fb5a53ea27af1f33e6
local mydbdata
local simplest-lab_pgdata
vieux/sshfs:latest sshvolume

$ docker network ls
NETWORK ID NAME DRIVER SCOPE
0950a6281629 bridge bridge local
82faac964567 host host local
2fb14f721dc3 none null local

在这一部分,我们已经学习了与常规 Docker 工作流相关的所有主要 docker-compose 操作。在下一部分,我们将回顾一些用于构建镜像的特定选项。

使用 docker-compose 自定义镜像

使用 docker-compose 构建应用程序非常有用,因为我们可以用它来创建 Docker Swarm 或 Kubernetes 环境中的所有镜像。我们只需要一个 docker-compose 文件定义和应用程序组件的代码。

我们一直在使用静态的 docker-compose 文件定义,但在许多情况下,我们会使用一些变量来为特定需求提供它们的值。事实上,我们也可以在 Dockerfile 中使用变量,以在各个层次完成动态配置。

让我们在应用程序的 docker-compose.yaml 文件中引入一些变量(我们这么做是为了允许不同的行为):

version: "3.7"

services:
  lb:
 build:
 context: ./simplestlb
 args:
 alpineversion: "latest"
 dockerfile: Dockerfile.custom
 labels:
 org.codegazers.dscription: "Test image"
 image: ${dockerhubid}/simplest-lab:simplestlb
    environment:
      - APPLICATION_ALIAS=simplestapp
      - APPLICATION_PORT=3000
    networks:
      simplestlab:
          aliases:
          - simplestlb
    ports:
      - "${LB_PORT}:80"

...
...

你可以在 github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git 找到这个文件,名为 docker-compose.dev.yaml,以及前一部分中使用的所有其他代码文件。

首先,我们将使用 docker-compose config 命令检查定义的配置:

$ docker-compose --file docker-compose.dev.yaml config
WARNING: The dockerhubid variable is not set. Defaulting to a blank string.
WARNING: The LB_PORT variable is not set. Defaulting to a blank string.
ERROR: The Compose file './docker-compose.dev.yaml' is invalid because:
services.lb.ports contains an invalid type, it should be a number, or an object

这些警告和错误表明,必须设置以下变量:

  • dockerhubid:默认情况下,此项为空。

  • LB_PORT:必须设置为一个端口号,因为这是我们将发布用于消费应用程序的端口。

我们需要为这些变量设置值。我们还可以在 Dockerfile 中使用变量来添加更多的细节。然而,这不是我们要讨论的重点,我们也不会深入探讨 Dockerfile 变量的使用。对于 Docker 认证助理考试,重要的是要了解如何使用变量为 docker-compose 部署提供值。我们可以使用带变量的动态配置,通过一个 docker-compose.yaml 文件来部署不同的项目。例如,这在构建带有开发者工具的调试镜像时非常有用。

让我们设置 LB_PORTdockerhubid 变量,并再次检查我们的项目配置:

$ LB_PORT=8081 docker-compose --file docker-compose.dev.yaml config
WARNING: The dockerhubid variable is not set. Defaulting to a blank string.
networks:
  simplestlab:
    ipam:
      config:
      - subnet: 172.16.0.0/16
      driver: default
services:
  app:
    build:
      context: <..>/Docker-Certified-Associate-DCA-Exam-Guide/simplest-lab/simplestapp
    depends_on:
    - db
    - lb
    environment:
      dbhost: simplestdb
      dbname: demo
      dbpasswd: d3m0
      dbuser: demo
    image: myregistry/simplest-lab:simplestapp
    networks:
      simplestlab:
        aliases:
        - simplestapp
  db:
    build:
      context: <..>/Docker-Certified-Associate-DCA-Exam-Guide/simplest-lab/simplestdb
    environment:
      POSTGRES_PASSWORD: changeme
    image: myregistry/simplest-lab:simplestdb
    networks:
      simplestlab:
        aliases:
        - simplestdb
    volumes:
    - pgdata:/var/lib/postgresql/data:rw
  lb:
    build:
      args:
        alpineversion: latest
      context: <..>/Docker-Certified-Associate-DCA-Exam-Guide/simplest-lab/simplestlb
      dockerfile: Dockerfile.custom
      labels:
        org.codegazers.description: Test image
    environment:
      APPLICATION_ALIAS: simplestapp
      APPLICATION_PORT: '3000'
    image: /simplest-lab:simplestlb
    networks:
      simplestlab:
        aliases:
        - simplestlb
    ports:
    - published: 8081
      target: 80
version: '3.7'
volumes:
  pgdata: {}

其他变量尚未设置。我们定义了不同的配置,以提供一些生产环境功能,例如,使用特定的凭证:

$ LB_PORT=8081 dockerhubid=frjaraur docker-compose --project-name test --file docker-compose.dev.yaml build --build-arg alpineversion="3.6" 
Building db
Step 1/2 : FROM postgres:alpine
...
...
[Warning] One or more build-args [alpineversion] were not consumed
Successfully built 336fb84e7fbf
Successfully tagged myregistry/simplest-lab:simplestdb
Building lb
Step 1/12 : ARG alpineversion=latest
...
...
Step 12/12 : LABEL org.codegazers.dscription=Test image
 ---> Using cache
 ---> ea4739af8eb5
Successfully built ea4739af8eb5
Successfully tagged frjaraur/simplest-lab:simplestlb
Building app
Step 1/15 : FROM alpine
...
...
[Warning] One or more build-args [alpineversion] were not consumed
Successfully built ff419f0998ae
Successfully tagged myregistry/simplest-lab:simplestapp

如果我们查看新的构建镜像,我们会注意到它现在有了一个新的标签,并且是使用 alpine:3.6 创建的,而不是使用最新版本:

"Labels": {
 "org.codegazers.dscription": "Test image"
 }

这样,我们就学会了如何使用变量准备不同的环境。通过变量,我们可以使用一个 docker-compose.yaml 文件来适应环境中的任何阶段。我们已经学会了如何为以下内容准备部署:

  • 开发环境,使用带有编译器或调试工具的镜像

  • 测试,通过添加工具来验证与第三方应用程序的连接,例如

  • 预生产或集成阶段,使用库执行负载和性能测试,在将应用程序推向生产环境之前

  • 生产阶段,只有经过充分测试的应用程序组件才会在镜像中标记为 release,例如

Docker Compose 允许我们通过 YAML 文件跟踪每个阶段所需的所有配置。该文件将作为代码库存储在我们的基础设施中。版本控制将帮助我们在生产中控制已部署的应用程序。

使用 Docker Compose 自动化桌面和 CI/CD 流程

Docker Compose 让我们能够轻松地在自己的笔记本电脑上进行开发。DevOps 团队将提供完整的应用程序堆栈文件、docker-compose.yaml 文件,以及所有必需的组件和配置。开发人员无需了解所有组件的工作原理。他们可以专注于自己正在开发的组件,因为其他组件将通过 docker-compose 自动运行。

我们可以在持续集成/持续部署CI/CD)管道中使用 Docker Compose,一次性构建所有组件。

Docker Compose 帮助我们在开发阶段构建所有应用组件,但我们也可以使用这个工具将所有组件一起运行。CI/CD 编排器将在不同阶段执行docker-compose文件。

通过这些步骤和变量的描述,我们可以轻松设想如何实施一个从开发阶段开始,到应用部署到生产的管道。我们将在生产中使用不同的镜像标签,这些标签是通过在不同环境之间应用不同变量值来创建的。

理解docker-compose.yaml文件在基础设施即代码IaC)环境中的关键作用非常重要。我们需要存储这些文件并使用版本控制系统。这些文件描述了将运行哪些应用组件,以及它们将使用哪些资源。例如,我们可以为应用的发布端口添加变量,以避免如果在同一主机上使用相同的docker-compose文件部署多个应用时发生端口冲突。我们还可以在开发和测试中使用相同的docker-compose文件,以及将应用部署到这些环境中。为了避免环境冲突,我们可以使用变量来定义应用组件的端点,例如数据库或在不同环境之间应有所不同的任何连接链。

开发人员将使用这些文件在他们的笔记本电脑上启动所需的应用组件,在开发新功能或修复代码错误时,他们可以专注于编写代码,因为不需要创建复杂的基础设施来测试他们编写的代码。实际上,他们根本不需要开发基础设施,因为可以使用自己的计算机。

本章将通过回顾一些实验来帮助我们理解并巩固迄今为止学到的概念。

章节实验

我们将部署一个简单的实验来回顾本章中描述的不同步骤。首先,我们将构建所需的镜像,然后继续执行并扩展组件。我们将使用安装了 Docker 引擎的 CentOS Linux 主机。

如果你还没有部署过,请从本书的 GitHub 仓库中部署environments/standalone-environmentgithub.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git)。你也可以使用你自己的 CentOS 7 服务器。从environments/standalone-environment文件夹中使用vagrant up启动你的虚拟环境。

如果你正在使用standalone-environment,请等待它启动完成。我们可以通过vagrant status检查节点的状态。使用vagrant ssh standalone连接到你的实验室节点。现在,standalone是你的节点名称。你将以vagrant用户身份使用根权限通过sudo执行操作。你应该会看到以下输出:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant up
Bringing machine 'standalone' up with 'virtualbox' provider...
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant status
Current machine states:
standalone running (virtualbox)
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$

现在,我们可以使用vagrant ssh standalone连接到standalone节点。如果你之前已部署standalone虚拟节点,并使用vagrant up启动它,那么该过程可能会有所不同:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant ssh standalone
[vagrant@standalone ~]$ 

如果你正在重复使用你的standalone-environment实例,这意味着 Docker 引擎已经安装。如果你启动了一个新的实例,请执行/vagrant/install_requirements.sh脚本,以获得所有所需的工具(Docker 引擎和docker-compose):

[vagrant@standalone ~]$ /vagrant/install_requirements.sh 

现在,你已经准备好开始这些实验了。

颜色应用程序实验

我们将通过部署一个简单的应用程序来开始这些实验,该应用程序将运行一个小的 Python 进程。这个进程是一个使用 Flask 开发的 Web 服务器,默认情况下会显示一个带有一些关于容器名称、IP 地址和应用程序版本信息的彩色页面(随机颜色)。

本实验所需的所有文件可以在本书的 GitHub 仓库中的Docker-Certified-Associate-DCA-Exam-Guide/chapter5文件夹中找到,网址为github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git。让我们开始吧:

  1. 我们先通过克隆我们的仓库,进入我们的文件夹,并列出文件夹中的文件:
[vagrant@standalone ~]$ git clone https://github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git [vagrant@standalone ~]$ cd Docker-Certified-Associate-DCA-Exam-Guide/chapter5
[vagrant@standalone chapter5]$ ls -1
app
docker-compose.loadbalancer.yaml
docker-compose.multicolor.yaml
docker-compose.random.yaml
docker-compose.red.yaml
lb
Readme.md
  1. 让我们快速查看一下docker-compose.random.yaml文件的内容:
version: "3.7"
services:
    red:
        build: app
        environment:
            COLOR: "red"
        labels:
            role: backend
        ports:
        - 3000
        networks:
        - lab
networks:
    lab:

这个非常简单。我们定义了一个random服务,使用app目录中的代码。我们将容器端口3000暴露到一个随机的主机端口。

  1. 我们将使用lab1作为项目名称来构建镜像。注意,我们定义了lab网络。Docker 守护进程将创建一个lab1_random镜像和一个lab1_lab网络:
[vagrant@standalone chapter5]$ docker-compose -p lab1 -f docker-compose.random.yaml build
Building random
Step 1/9 : FROM node:alpine
alpine: Pulling from library/node
89d9c30c1d48: Already exists
5320ee7fe9ff: Pull complete
...
...
Step 9/9 : EXPOSE 3000
 ---> Running in 51379c5e7630
Removing intermediate container 51379c5e7630
 ---> c0dce423a972

Successfully built c0dce423a972
Successfully tagged lab1_random:latest
  1. 现在,我们执行我们的多容器应用程序(在这种情况下,我们只有一个服务定义):
[vagrant@standalone chapter5]$ docker-compose -p lab1 -f docker-compose.random.yaml up -d
Creating network "lab1_lab" with the default driver
Creating lab1_random_1 ... done

让我们查看docker-compose项目的lab1执行:

[vagrant@standalone chapter5]$ docker-compose -p lab1 -f docker-compose.random.yaml ps
 Name Command State Ports 
-------------------------------------------------------------------------
lab1_random_1 docker-entrypoint.sh node ... Up 0.0.0.0:32780->3000/tcp

注意,应用程序的端口3000已链接到 Docker 主机端口32780(使用 NAT)。

  1. 我们可以通过那个随机端口访问应用程序,也就是32780
[vagrant@standalone chapter5]$ curl 0.0.0.0:32780/text
APP_VERSION: 1.0
COLOR: blue
CONTAINER_NAME: 17bc24f60799
CONTAINER_IP: 172.27.0.2
CLIENT_IP: ::ffff:172.27.0.1
CONTAINER_ARCH: linux

我们可以使用网页浏览器访问正在运行的应用程序。我们也可以使用curl,因为该应用程序已准备好使用/text URI 显示文本响应:

将使用一个随机颜色。在这种情况下,我们得到了一个蓝色页面。由于如果COLOR变量未设置,则会选择一个随机颜色,因此在你的环境中可能会有所不同。

如果你使用提供的vagrant独立环境部署了随机颜色应用程序,应该在浏览器中使用192.168.56.11:<PUBLISHED_PORT>,因为你正在使用虚拟机。然而,我们已经准备好了主机与虚拟节点之间的接口(即192.168.56.11的 IP 地址)。

  1. 我们现在可以删除该应用程序,并使用docker-compose down继续进行下一个实验:
[vagrant@standalone chapter5]$ docker-compose -p lab1 -f docker-compose.random.yaml down
Stopping lab1_random_1 ... done
Removing lab1_random_1 ... done
Removing network lab1_lab

现在,我们将创建一个red应用程序,定义一个简单的变量来改变应用程序的行为。

执行一个红色应用程序

在这个实验中,我们将通过设置COLOR环境变量来改变应用程序的行为。在这种情况下,我们将执行red应用程序。这个新应用只需少量更改就可以部署,这将帮助我们在接下来的实验中集成更多的组件。

现在,让我们执行red应用程序。在这种情况下,我们只需更改服务名称并添加一个环境变量来定义后端颜色(COLOR键和red值)。以下是docker-compose.red.yaml文件的内容:

version: "3.7"

services:
  red:
    build: app
    environment:
      COLOR: "red" 
    labels:
      role: backend
    ports:
    - 3000
    networks:
    - lab

networks:
  lab:

我们可以重用lab1项目名称,或者创建一个新的项目名称。如果我们将lab2作为新项目名称,新的标签将被添加。构建它时不会创建新的层,因为我们没有更改任何代码。我们将简单地使用docker-compose up -d,如下所示:

[vagrant@standalone ~]$ docker-compose -p lab2 -f docker-compose.red.yaml up -d 
Creating network "lab2_lab" with the default driver
Building red
Step 1/9 : FROM node:alpine
 ---> fac3d6a8e034
Step 2/9 : ENV APPDIR /APP
 ---> Using cache
 ---> 61bbe191216e
Step 3/9 : WORKDIR ${APPDIR}
 ---> Using cache
...
...
 ---> Using cache
 ---> df0f6838dfca
Step 9/9 : EXPOSE 3000
 ---> Using cache
 ---> 24ae28db3e15

Successfully built 24ae28db3e15
Successfully tagged lab2_red:latest
WARNING: Image for service red was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating lab2_red_1 ... done

我们可以使用docker-compose ps查看部署状态:

[vagrant@standalone ~]$ docker-compose -p lab2 -f docker-compose.red.yaml ps
 Name Command State Ports 
-----------------------------------------------------------------------------
lab2_red_1 docker-entrypoint.sh node ... Up 0.0.0.0:32781->3000/tcp

我们可以轻松地通过curl访问0.0.0.0:32781来访问red应用:

[vagrant@standalone ~]$ curl 0.0.0.0:32781/text
APP_VERSION: 1.0
COLOR: red
CONTAINER_NAME: fc05e400d02a
CONTAINER_IP: 172.29.0.2
CLIENT_IP: ::ffff:172.29.0.1
CONTAINER_ARCH: linux

现在,让我们尝试扩展应用实例的数量。

扩展red应用的后端

在这个实验中,我们将通过使用docker-compose扩展其中一个组件,增加应用的后端数量。

让我们使用docker-compose scale设置应用程序所需的新实例数量:

[vagrant@standalone ~]$ docker-compose -p lab2 -f docker-compose.red.yaml scale red=5
WARNING: The scale command is deprecated. Use the up command with the --scale flag instead.
Starting lab2_red_1 ... done
Creating lab2_red_2 ... done
Creating lab2_red_3 ... done
Creating lab2_red_4 ... done
Creating lab2_red_5 ... done

注意,在这种情况下,我们正在部署一个无状态应用,没有任何持久化。在这种情况下,还需要注意一件事——我们没有设置主机链接的端口。因此,每个容器实例都会使用一个随机端口。让我们使用docker-compose ps查看新的实例端口号:

[vagrant@standalone ~]$ docker-compose -p lab2 -f docker-compose.red.yaml ps
 Name Command State Ports 
-----------------------------------------------------------------------------
lab2_red_1 docker-entrypoint.sh node ... Up 0.0.0.0:32781->3000/tcp
lab2_red_2 docker-entrypoint.sh node ... Up 0.0.0.0:32784->3000/tcp
lab2_red_3 docker-entrypoint.sh node ... Up 0.0.0.0:32785->3000/tcp
lab2_red_4 docker-entrypoint.sh node ... Up 0.0.0.0:32783->3000/tcp
lab2_red_5 docker-entrypoint.sh node ... Up 0.0.0.0:32782->3000/tcp

现在,我们可以访问所有的实例。每个实例都使用自己的 NAT 端口,所有这些端口在 Docker 主机中都是可用的。我们可以再次使用curl进行检查:

[vagrant@standalone ~]$ curl 0.0.0.0:32781/text
APP_VERSION: 1.0
COLOR: red
CONTAINER_NAME: fc05e400d02a
CONTAINER_IP: 172.29.0.2
CLIENT_IP: ::ffff:172.29.0.1
CONTAINER_ARCH: linux

[vagrant@standalone ~]$ curl 0.0.0.0:32782/text
APP_VERSION: 1.0
COLOR: red
CONTAINER_NAME: f5de33465357
CONTAINER_IP: 172.29.0.3
CLIENT_IP: ::ffff:172.29.0.1
CONTAINER_ARCH: linux

[vagrant@standalone ~]$ curl 0.0.0.0:32783/text
APP_VERSION: 1.0
COLOR: red
CONTAINER_NAME: 5be016aadadb
CONTAINER_IP: 172.29.0.4
CLIENT_IP: ::ffff:172.29.0.1
CONTAINER_ARCH: linux

[vagrant@standalone ~]$ curl 0.0.0.0:32784/text
APP_VERSION: 1.0
COLOR: red
CONTAINER_NAME: 413c9d605bd5
CONTAINER_IP: 172.29.0.5
CLIENT_IP: ::ffff:172.29.0.1
CONTAINER_ARCH: linux

[vagrant@standalone ~]$ curl 0.0.0.0:32785/text
APP_VERSION: 1.0
COLOR: red
CONTAINER_NAME: fe879a59c3aa
CONTAINER_IP: 172.29.0.6
CLIENT_IP: ::ffff:172.29.0.1
CONTAINER_ARCH: linux

所有 IP 地址都是不同的,因为我们在访问不同的容器。然而,所有的都是red,正如预期的那样。

让我们删除所有应用实例:

[vagrant@standalone ~]$ docker-compose -p lab2 -f docker-compose.red.yaml down
Stopping lab2_red_2 ... done
Stopping lab2_red_3 ... done
Stopping lab2_red_4 ... done
Stopping lab2_red_5 ... done
Stopping lab2_red_1 ... done
Removing lab2_red_2 ... done
Removing lab2_red_3 ... done
Removing lab2_red_4 ... done
Removing lab2_red_5 ... done
Removing lab2_red_1 ... done
Removing network lab2_lab

在下一个实验中,我们将通过单个文件添加更多的颜色。

添加更多的颜色

我们现在将通过添加更多颜色来增加我们应用的组件。

让我们添加更多颜色的应用程序。在docker-compose.multicolor.yaml文件中,我们将添加几个服务,并为它们设置各自的COLOR变量:

version: "3.7"

services:
  red:
    build: app
    environment:
      COLOR: "red" 
    labels:
      role: backend
    ports:
    - 3000
    networks:
    - lab
  green:
    build: app
    environment:
      COLOR: "green" 
    labels:
      role: backend
    ports:
    - 3000
    networks:
    - lab
  white:
    build: app
    environment:
      COLOR: "white" 
    labels:
      role: backend
    ports:
    - 3000
    networks:
    - lab

networks:
  lab:

我们将使用docker-compose up启动我们的redgreenwhite应用程序:

[vagrant@standalone ~]$ docker-compose -p lab3 -f docker-compose.multicolor.yaml up -d 
Creating network "lab3_lab" with the default driver
Building white
Step 1/9 : FROM node:alpine
 ---> fac3d6a8e034
...
Successfully built 24ae28db3e15
Successfully tagged lab3_white:latest
...
Building green
...
Successfully tagged lab3_green:latest
...
Building red
...
Successfully tagged lab3_red:latest
WARNING: Image for service red was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating lab3_green_1 ... done
Creating lab3_white_1 ... done
Creating lab3_red_1 ... done

我们将能够访问不同的应用程序。让我们使用docker-compose ps查看它们的进程和端口,然后通过curl访问每个实例:

[vagrant@standalone ~]$ docker-compose -p lab3 -f docker-compose.multicolor.yaml ps
 Name Command State Ports 
-------------------------------------------------------------------------------
lab3_green_1 docker-entrypoint.sh node ... Up 0.0.0.0:32789->3000/tcp
lab3_red_1 docker-entrypoint.sh node ... Up 0.0.0.0:32791->3000/tcp
lab3_white_1 docker-entrypoint.sh node ... Up 0.0.0.0:32790->3000/tcp

$ curl 0.0.0.0:32789/text 
APP_VERSION: 1.0
COLOR: green
CONTAINER_NAME: a25a4cc36232
CONTAINER_IP: 172.31.0.2
CLIENT_IP: ::ffff:172.31.0.1
CONTAINER_ARCH: linux

$ curl 0.0.0.0:32791/text 
APP_VERSION: 1.0
COLOR: red
CONTAINER_NAME: 5e12b0de196c
CONTAINER_IP: 172.31.0.4
CLIENT_IP: ::ffff:172.31.0.1
CONTAINER_ARCH: linux

$ curl 0.0.0.0:32790/text 
APP_VERSION: 1.0
COLOR: white
CONTAINER_NAME: b67b09c8c836
CONTAINER_IP: 172.31.0.3
CLIENT_IP: ::ffff:172.31.0.1
CONTAINER_ARCH: linux

在这种情况下,所有应用组件都可以通过随机发布的端口进行访问。我们可以使用固定端口将用户请求路由到外部负载均衡器等。在生产环境中,我们不会使用随机端口。

请注意,后端端口是动态关联到随机端口的。这允许我们多次运行此应用,而无需更改任何docker-compose文件。我们只需要使用另一个项目名称以确保创建的对象唯一性。

现在,让我们添加一个简单的负载均衡器,看看一些其他的部署功能。我们将发布这个负载均衡器,其他服务将只能通过这个组件访问。

添加一个简单的负载均衡器

在这个实验中,我们将添加一个简单的nginx负载均衡器来将流量路由到不同的颜色后端。

让我们来看一下新的部署文件:

version: "3.7"

services:
  loadbalancer:
    build: lb
    environment:
      APPLICATION_PORT: 3000
    ports:
    - 8080:80
    networks:
    - lab
  red:
    build: app
    environment:
      COLOR: "red"
    labels:
      role: backend
    networks:
    - lab
  green:
    build: app
    environment:
      COLOR: "green"
    labels:
      role: backend
    networks:
    - lab
  white:
    build: app
    environment:
      COLOR: "white"
    labels:
      role: backend
    networks:
    - lab

networks:
  lab:

请注意,我们已经移除了所有颜色服务后端的端口。现在,我们只暴露8080端口,该端口链接到内部nginx组件的端口,也就是80端口。

让我们启动应用程序部署并使用docker-compose up -d来查看结果:

[vagrant@standalone ~]$ docker-compose -p lab5 -f docker-compose.loadbalancer.yaml up -d
Creating network "lab5_lab" with the default driver
Building white
...
Successfully tagged lab5_white:latest
WARNING: Image for service white was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Building green
...
Successfully tagged lab5_green:latest
WARNING: Image for service green was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Building red
...
Successfully tagged lab5_red:latest
WARNING: Image for service red was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Building loadbalancer
...Successfully tagged lab5_loadbalancer:latest
WARNING: Image for service loadbalancer was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating lab5_loadbalancer_1 ... done
Creating lab5_white_1 ... done
Creating lab5_red_1 ... done
Creating lab5_green_1 ... done

一旦所有组件准备就绪,我们可以使用不同的主机头来测试所有颜色的后端,以访问每个后端。我们为此准备了一个简单的nginx负载均衡配置(我们在lb/nginx.conf中提供了负载均衡器配置文件的简要回顾)。每次我们请求特定的主机头时,通过指定的颜色,我们都会被路由到正确的后端:

[vagrant@standalone ~]$ cat lb/nginx.conf 
...
...
 server {
 listen 80;
 set $port "__APPLICATION_PORT__";
...
...
 location / {
 proxy_pass http://$host:$port;
 }
...
...

使用curl,我们可以测试所有的后端:

[vagrant@standalone ~]$ curl -H "Host: white" 0.0.0.0:8080/text
APP_VERSION: 1.0
COLOR: white
CONTAINER_NAME: 86871cba5a71
CONTAINER_IP: 192.168.208.5
CLIENT_IP: ::ffff:192.168.208.4
CONTAINER_ARCH: linux

[vagrant@standalone ~]$ curl -H "Host: green" 0.0.0.0:8080/text
APP_VERSION: 1.0
COLOR: green
CONTAINER_NAME: f7d90dc89255
CONTAINER_IP: 192.168.208.2
CLIENT_IP: ::ffff:192.168.208.4
CONTAINER_ARCH: linux

[vagrant@standalone ~]$ curl -H "Host: red" 0.0.0.0:8080/text
APP_VERSION: 1.0
COLOR: red
CONTAINER_NAME: 25bb1b66bab8
CONTAINER_IP: 192.168.208.3
CLIENT_IP: ::ffff:192.168.208.4
CONTAINER_ARCH: linux

记住,除loadbalancer外,其他服务都不可访问。让我们通过docker-compose ps查看已发布的端口:

[vagrant@standalone ~]$ docker-compose -p lab5 -f docker-compose.loadbalancer.yaml ps
 Name Command State Ports 
-----------------------------------------------------------------------------------
lab5_green_1 docker-entrypoint.sh node ... Up 3000/tcp 
lab5_loadbalancer_1 /entrypoint.sh /bin/sh -c ... Up 0.0.0.0:8080->80/tcp
lab5_red_1 docker-entrypoint.sh node ... Up 3000/tcp 
lab5_white_1 docker-entrypoint.sh node ... Up 3000/tcp 

如果我们将green服务扩展到四个实例,会发生什么?我们预计可以访问所有实例,因为服务实例会被添加到内部 DNS 中。让我们使用docker-compose up -d扩展该服务:

[vagrant@standalone ~]$ docker-compose -p lab5 -f docker-compose.loadbalancer.yaml up -d --scale green=4 
Starting lab5_green_1 ... 
lab5_white_1 is up-to-date
lab5_red_1 is up-to-date
Starting lab5_green_1 ... done
Creating lab5_green_2 ... done
Creating lab5_green_3 ... done
Creating lab5_green_4 ... done

让我们再次使用curl请求green服务:

[vagrant@standalone ~]$ curl -H "Host: green" 0.0.0.0:8080/text 
APP_VERSION: 1.0
COLOR: green
CONTAINER_NAME: ba90c57914f9
CONTAINER_IP: 192.168.208.7
CLIENT_IP: ::ffff:192.168.208.4
CONTAINER_ARCH: linux

[vagrant@standalone ~]$ curl -H "Host: green" 0.0.0.0:8080/text
APP_VERSION: 1.0
COLOR: green
CONTAINER_NAME: c1a9ebcf82ac
CONTAINER_IP: 192.168.208.6
CLIENT_IP: ::ffff:192.168.208.4
CONTAINER_ARCH: linux

[vagrant@standalone ~]$ curl -H "Host: green" 0.0.0.0:8080/text
APP_VERSION: 1.0
COLOR: green
CONTAINER_NAME: d5436822ca8f
CONTAINER_IP: 192.168.208.8
CLIENT_IP: ::ffff:192.168.208.4
CONTAINER_ARCH: linux

[vagrant@standalone ~]$ curl -H "Host: green" 0.0.0.0:8080/text
APP_VERSION: 1.0
COLOR: green
CONTAINER_NAME: f7d90dc89255
CONTAINER_IP: 192.168.208.2
CLIENT_IP: ::ffff:192.168.208.4
CONTAINER_ARCH: linux

正如我们预期的那样,我们在每次请求时都得到了不同的后端,因为 DNS 给负载均衡器分配了不同的后端 IP 地址。

为了完成本实验,我们将在loadbalancer容器中安装bind-tools包,以使用host工具查询内部 DNS。我们将查询redgreen服务,以验证内部 DNS 解析。当使用组件名称进行应用程序部署时,这一点至关重要。我们将使用docker-compose execloadbalancer容器中安装bind-tools包。包安装完成后,我们将再次使用docker-compose exechost命令来查询 DNS:

[vagrant@standalone ~]$ docker-compose -p lab5 \
-f docker-compose.loadbalancer.yaml exec loadbalancer apk add -q --update bind-tools

[vagrant@standalone ~]$ docker-compose -p lab5 -f docker-compose.loadbalancer.yaml \
exec loadbalancer host red
red has address 192.168.208.3

[vagrant@standalone ~]$ docker-compose -p lab5 \
-f docker-compose.loadbalancer.yaml exec loadbalancer host green
green has address 192.168.208.8
green has address 192.168.208.2
green has address 192.168.208.7
green has address 192.168.208.6

内部 DNS 给出了与greenred服务关联的所有 IP 地址。这些就是关联容器的 IP 地址。因此,我们定义的green服务被负载均衡到所有运行中的green后端。

使用适当的docker-compose文件和项目名称,使用docker-compose down删除所有实验环境。

总结

本章介绍了如何在 Docker 主机上部署多容器应用程序。我们了解到,docker-compose 命令不仅用于部署应用程序,还允许我们构建和共享所有应用组件。由于 docker-compose 提供了一个命令行界面来获取所有应用容器的标准输出和错误输出,因此查看所有组件的状态变得更加容易。我们可以同时启动和停止所有组件。但我们可以更进一步:我们还能够扩展每个组件实例的数量。这个功能取决于我们的应用逻辑,因为 Docker 守护进程对我们的应用进程一无所知。

所有应用组件都在一个 YAML 格式的文件中定义,可以使用变量进行自定义。我们了解了此实例中最重要的键及其默认值。docker-compose 文件至关重要,因为它描述了所有应用组件及其资源,以及它们之间的交互。每个组件都有自己的版本,因为我们使用带有标签和参数的镜像。我们还可以编写版本控制系统来跟踪 docker-compose 的变更,因为这提供了基础设施即代码(IaC)信息。我们需要确切知道哪些应用组件在生产环境中运行,而 Docker Compose 允许我们为应用部署使用的文件应用版本号。这将确保正确的应用组件在运行。引入这些文件中的变量使我们能够在不同的开发和部署阶段使用它们,只需做出少量更改。

在接下来的部分中,您可以尝试一些问题,以巩固我们在本章中学习的内容。下一章将教我们如何使用 Docker 内容信任管理镜像所有权和内容。

问题

  1. 哪些陈述是错误的?

a) Docker Compose 可以运行分布在不同服务上的多服务应用程序。

b) Docker Compose 可以在 Docker 主机上运行多容器应用程序。

c) Docker Compose 是一个不包含在标准 Docker 包中的软件应用程序。

d) 上述所有都是正确的。

  1. 我们可以用 docker-compose 做什么?

a) 我们可以构建所有应用镜像。

b) 我们可以拉取和推送应用组件镜像。

c) 我们可以同时运行所有应用组件。

d) 上述所有。

  1. 如果我们执行 docker-compose up 并使用一个定义了前端、后端和数据库服务的 docker-compose 文件,会发生什么?(从以下选项中选择所有正确的说法。)

a) Docker Compose 会查找所有服务的定义镜像,如果当前主机上没有这些镜像,它会拉取它们。

b) Docker Compose 只会执行定义了 start 键的镜像。

c) Docker Compose 会同时运行所有容器,并将您的终端附加到它们的标准输出和错误输出。

d) 上述所有都是错误的。

  1. 如何使用 docker-compose 文件启动应用程序服务多次?

a) 实际上,我们无法做到这一点,但我们可以通过 scale 操作启动服务进程实例。这个服务名称将解析为所有副本的 IP 地址。

b) Docker Compose 只会执行定义了 start 键的镜像。

c) Docker Compose 会同时运行所有容器,而没有任何优先顺序。

d) 以上所有说法都是错误的。

  1. 执行 docker-compose down 命令会做什么?

a) 它将停止与应用程序相关的所有正在运行的容器。

b) 它将尝试停止与应用程序相关的所有正在运行的容器。

c) 它将尝试停止与应用程序相关的所有正在运行的容器。一旦它们全部停止,它将删除它们。

d) 它将尝试停止与应用程序相关的所有正在运行的容器。一旦它们全部停止,它将删除它们及其所有相关资源,除非它们是外部定义的。

深入阅读

你可以参考以下链接,获取更多有关本章内容的信息:

Docker 内容信任简介

在本章中,我们将学习 Docker 内容信任概念及其相关工具。为了在 Docker 环境中提供受信任的内容,我们将使用 Docker 内容信任来加密应用于 Docker 对象的元数据。因此,任何未经授权的更改或对象操作都会被报告。如果没有发现这些问题,我们就能够确保环境中的所有对象都是受信任的。

首先,我们将介绍更新框架,然后学习如何签名镜像。之后,我们将学习如何验证签名,以确保其优先性和所有权。最后,我们将应用这些概念,在生产环境中运行一个受信任的环境。

本章将涵盖以下主题:

  • 更新框架

  • 签名镜像

  • 审查签名

  • 在受信任的环境中创建和运行应用程序

让我们开始吧!

第八章:技术要求

在本章中,我们将学习关于各种 Docker 内容信任的概念。在本章末尾,我们将提供一些实验,帮助你理解并学习所展示的概念。这些实验可以在你的笔记本电脑或 PC 上运行,使用提供的 Vagrant 独立环境,或者使用你自己部署的任何 Docker 主机。你可以在本书的 GitHub 代码仓库中找到更多信息:github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

查看以下视频,了解代码如何运作:

"bit.ly/3b0qviR"

更新框架

在学习 更新框架(也称为 TUF)之前,我们将介绍一些概念。以下概念将帮助我们理解为什么我们需要工具来管理应用程序更新:

  • 软件更新系统:软件更新系统是一种持续寻找新更新的应用程序。当找到更新时,它会触发过程以获取这些更新并安装这些更改。一个很好的例子是 Google Chrome 浏览器的更新系统。它会持续寻找其组件的更新,一旦发现更新,就会向我们显示一条消息:有一个新版本,是否现在更新?

  • 库包管理器:库包管理器将管理和更新编程语言库及其依赖项。Python 的 Python 包安装器PIP)和 Node.js 的 Node 包管理器NPM)就是很好的例子。这些应用程序会查找库的更新并安装它们及其所需的依赖项。

  • 操作系统组件更新:在这种情况下,不同的软件包管理器将管理所有软件更新及其依赖项,并在某些情况下触发前面提到的某些解决方案(软件更新系统或库包管理器)。

应用程序更新通常包含三个逻辑步骤:

  1. 它会查找任何更新或更改。

  2. 它下载更新。

  3. 它将更改应用于我们的系统。

如果这些更新是恶意的,因为代码被攻击者截获并篡改了,会发生什么情况?

TUF 被创建来防止这些情况。它将处理应用程序更新的步骤,以确保下载的更改是受信任的。不会允许篡改的更改。TUF 元数据包括与受信任的密钥、加密哈希、文件、组件版本、创建和过期日期以及签名相关的信息。一个需要多个更新的应用程序无需管理此验证过程。它将请求 TUF 来管理这些过程。总而言之,我们可以这样说,TUF 提供了一种安全的方法来获取受信任的文件。

TUF 目前由 Linux 基金会托管,作为 云原生计算基金会CNCF)的一部分。它是开源的,可以在生产环境中使用。建议与一些厂商工具结合使用,这样更容易管理和使用。

TUF 元数据为软件更新系统提供了有关更新真实性的信息。然后,该组件将做出正确的决定(安装或拒绝更新)。这些元数据信息将以 JSON 格式呈现。我们将讨论四个级别的签名,并将其称为角色:

  • 根元数据(root.json)和角色:此角色与变更的所有者相关。它是顶级角色,其他角色将与该角色相关联。

  • 目标元数据(targets.json)和角色:此角色与包中包含的文件相关。

  • 快照元数据(snapshot.json)和角色:除了 timestamp.json 之外的所有文件都将在该角色中列出,以确保更新的一致性。

  • 时间戳元数据(timestamp.json)和角色:这个标志将确保更新的确切日期,并且在检查更新时它是唯一需要的标志,例如。

更新应用程序使用 TUF 与仓库和文件来源进行交互,同时管理它们的更新。角色、受信任的密钥和目标文件不应包含在这些仓库中,因为它们将用于管理这些内容。

这个框架应该有一个客户端,以便我们可以在其正常使用中包含所描述的角色。因此,客户端必须管理以下内容:

  • 所有可能的所有者的受信任根密钥,必须被信任

  • 目标委托,当一个目标有多个所有者时

  • 使用时间戳角色日期检查更新

  • 所有签名过程

现在我们已经了解了使用 TUF 来管理仓库更新的好处,接下来让我们回顾一下它在 Docker 中的实现方式。

Docker 内容信任是 Docker 实现的 TUF。它通过 Notary 集成,Notary 是一个用于发布和管理受信任内容的开源工具。Docker 客户端提供了一个界面,允许我们签名并验证内容发布者。

Notary 是一个独立的软件;可以下载并用于检查包含在 Docker 仓库中的密钥。Docker 使用其库集成了 Notary。因此,每当我们在启用 Docker 内容信任时拉取镜像(默认情况下为禁用)时,Docker 守护进程将验证其签名。镜像拉取是通过其摘要进行的。镜像名称和标签将不再使用。这确保了只有正确的镜像会被下载。

Notary 的使用超出了本书的范围。在撰写时,它并不是通过 DCA 认证考试的必需内容。不过,建议阅读以下链接提供的 Notary 特性:docs.docker.com/notary/getting_started

当我们使用 Docker 内容信任并推送镜像时,Docker 客户端会要求我们在所有描述的级别上进行签名(根级别、目标级别、快照级别和时间戳级别)。

总结来说,Docker 内容信任(Docker 的 TUF 实现)将执行以下操作:

  • 确保镜像来源的可信度

  • 在分发之前签署内容

  • 确保主机上运行的所有内容都是可信的。

在下一节中,我们将学习如何签署并使用经过 Docker 引擎验证的签名镜像。

签署镜像

到目前为止,我们已经了解了不同的角色和将用于验证和信任镜像内容的元数据。让我们在进入 Docker 签名操作之前,快速总结一下:

  • 根密钥将验证其他密钥。它签署 root.json 文件,该文件包含根、目标、快照和时间戳公钥的 ID 列表。为了验证内容签名,Docker 客户端将使用这些公钥。根密钥是离线的,必须保管好。镜像集合的所有者应该保管此密钥。不要丢失此密钥。你可以重新创建它,但所有签名的镜像将无效。

  • 目标密钥签署 targets.json 文件,其中包含内容文件名的列表,以及它们的大小和哈希值。该文件用于将信任委派给团队中的其他用户,以便其他人可以签署相同的仓库。此密钥由管理员和集合(仓库)所有者持有。

  • 委派密钥用于签署委派元数据文件。此密钥由管理员和所有能为指定集合做出贡献的人持有。

  • 快照密钥签署 snapshot.json 元数据文件。该文件还包含文件名、以及根、目标和委派文件的大小和哈希值。此密钥由管理员和集合所有者持有。如果我们使用 Notary 服务,此密钥也可以由该服务持有,以允许集合协作者签署。此密钥代表当前的包签名。

  • 时间戳密钥确保集合的新鲜度。它用于验证 snapshot.json 文件的完整性。因为这个密钥只在一定时间内有效,所以最好将其保存在 Notary 中。在这种情况下,所有者不必在每次密钥过期时重新创建密钥。Notary 将根据需要重新生成这个密钥。

现在,让我们使用 Docker 客户端签名一个镜像。

首先,我们将启用 Docker 内容信任。默认情况下,它是禁用的。我们可以为所有 Docker 命令启用它,或者每次想启用时添加一个参数。要为所有后续的 Docker 命令启用 Docker 内容信任,我们需要定义 DOCKER_CONTENT_TRUST 变量:

$ export DOCKER_CONTENT_TRUST=1

另外,我们可以为指定的命令启用 Docker 内容信任:

$ docker pull --disable-content-trust=false busybox:latest

我们在这里使用了 --disable-content-trust=false,因为默认情况下,Docker 内容信任是禁用的。

现在我们通过设置 DOCKER_CONTENT_TRUST=1 为当前会话中的所有命令启用了 Docker 内容信任,我们可以使用 docker image pull 拉取镜像:

$ export DOCKER_CONTENT_TRUST=1

$ docker image pull busybox
Using default tag: latest
Pull (1 of 1): busybox:latest@sha256:1303dbf110c57f3edf68d9f5a16c082ec06c4cf7604831669faf2c712260b5a0
sha256:1303dbf110c57f3edf68d9f5a16c082ec06c4cf7604831669faf2c712260b5a0: Pulling from library/busybox
0f8c40e1270f: Pull complete 
Digest: sha256:1303dbf110c57f3edf68d9f5a16c082ec06c4cf7604831669faf2c712260b5a0
Status: Downloaded newer image for busybox@sha256:1303dbf110c57f3edf68d9f5a16c082ec06c4cf7604831669faf2c712260b5a0
Tagging busybox@sha256:1303dbf110c57f3edf68d9f5a16c082ec06c4cf7604831669faf2c712260b5a0 as busybox:latest
docker.io/library/busybox:latest

请注意,docker image pull 命令的输出发生了变化。事实上,下载的镜像是通过其哈希值进行管理的;在这种情况下,哈希值是 busybox@sha256:1303dbf110c57f3edf68d9f5a16c082ec06c4cf7604831669faf2c712260b5a

Docker 的官方镜像和认证镜像始终是已签名的。官方镜像由 Docker 管理和构建,位于 docker.io/<REPOSITORY>:<TAG> 下。

让我们使用 docker container run 来运行这个镜像,看看会发生什么:

$ docker container run -ti busybox sh
/ # ls 
bin dev etc home proc root sys tmp usr var
/ # touch NEW_FILE
/ # exit

按预期,它成功了。我们添加了一个文件,因为我们想要在提交容器的内容之前修改容器,以创建一个新的、不受信任的镜像。对于这个过程,我们将运行 docker container commit,并为命令设置 DOCKER_CONTENT_TRUST=0。我们之所以这么做,是因为在当前会话中已启用内容信任:

$ DOCKER_CONTENT_TRUST=0 docker container commit 3da3b341e904 busybox:untrusted
sha256:67a6ce66451aa10011d379e4628205889f459c06a3d7793beca10ecd6c21b68a

现在,我们有一个不受信任的 busybox 镜像。如果我们尝试执行这个镜像,会发生什么呢?

$ docker container run -ti busybox:untrusted sh
docker: No valid trust data for untrusted.
See 'docker run --help'.

我们不能运行这个镜像,因为它不受信任;它没有任何内容信任元数据。因此,它无法通过验证,并且不会被允许运行。如果启用了 Docker 内容信任,则未签名的镜像将不被允许。

让我们签名这个镜像。在这种情况下,我们将更改镜像名称并创建一个新的 trusted 标签。签名过程需要两个密码短语,正如这里所述:

  1. 首先,我们将被要求设置一个 root 密码短语。你将被再次询问两次,以验证输入的密码(因为密码不会显示)。

  2. 然后,你会被要求设置一个 repository 密码短语。你将被再次询问两次,以验证输入的密码(因为密码不会显示)。

我们被要求输入密码短语两次,因为这是第一次设置它们的值。下次我们使用这些密钥推送或拉取到这个仓库时,只会被要求一次(如果密码输入错误,可能会要求更多次)。让我们执行 docker image push

$ docker image push frjaraur/mybusybox:trusted
The push refers to repository [docker.io/frjaraur/mybusybox]
0736ae522762: Pushed 
1da8e4c8d307: Mounted from library/busybox 
trusted: digest: sha256:e58e349eee38baa38f8398510c44e63a1f331dc1d80d4ed6010fe34960b9945f size: 734
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 6e03824: 
Repeat passphrase for new root key with ID 6e03824: 
Enter passphrase for new repository key with ID b302395: 
Repeat passphrase for new repository key with ID b302395: 
Finished initializing "docker.io/frjaraur/mybusybox"
Successfully signed docker.io/frjaraur/mybusybox:trusted

根密码短语非常重要。请妥善保管,因为如果丢失,你需要重新开始。如果发生这种情况,你之前签名的镜像将变得不受信任,你需要更新它们。如果丢失了密钥,你需要联系 Docker 支持(support@docker.com)来重置仓库状态。

你为根密钥和你的仓库选择的密码短语应该是强密码。建议使用随机生成的密码。

现在,我们拥有一个已签名的镜像。它归我们所有(在这个例子中,我是frjaraur/mybusybox:trusted的所有者)。

现在,我们可以使用docker container run来执行这个新签名的(因此是受信的)镜像:

$ docker container run -ti frjaraur/mybusybox:trusted
/ # touch OTHERFILE
/ # exit

要管理 Docker 内容信任,我们可以使用docker trust及其可用的操作。我们将能够管理密钥(加载和撤销)并签名镜像(该过程类似于前面描述的过程)。我们可以使用docker trust inspect来查看这些签名:

$ docker trust inspect --pretty docker.io/frjaraur/mybusybox:trusted

Signatures for docker.io/frjaraur/mybusybox:trusted

SIGNED TAG DIGEST SIGNERS
trusted e58e349eee38baa38f8398510c44e63a1f331dc1d80d4ed6010fe34960b9945f (Repo Admin)

Administrative keys for docker.io/frjaraur/mybusybox:trusted

 Repository Key: b3023954026f59cdc9be0b7ba093039353ce6e2d1a06c1338e4387689663abc0
 Root Key: e9120faa839a565838dbad7d45edd3c329893ae1f2085f225dc039272dec98ed

请注意,我们使用了docker.io/frjaraur/mybusybox:trusted而不是frjaraur/mybusybox:trusted。这是因为如果我们没有使用注册表的完全限定域名FQDN)且镜像在本地存在,它将用于检索所有签名信息,你将收到WARN[0006] Error while downloading remote metadata, using cached timestamp - this might not be the latest version available remotely的消息,因为你将使用缓存的时间戳,而不是实际的时间戳。

现在我们已经学习了如何签署内容——在这种情况下是镜像——接下来让我们学习如何验证签名。

审查签名

Docker 客户端将与内容信任相关的文件存储在用户主目录下的.docker/trust目录中。

如果我们导航到受信目录下,我们将在.docker/trust/tuf中找到不同的注册表文件。我们在本章的示例中使用了 Docker Hub。因此,我们会找到docker.io注册表和不同的仓库。在你的环境中可能会有所不同,你可能会有更多的注册表或仓库,这取决于你何时开始在 Docker 主机上使用 Docker 内容信任。根据前面部分的示例,我们将在.docker目录下找到类似树形结构的目录:

trust/tuf/docker.io/frjaraur/mybusybox/metadata:
total 16
-rw------- 1 zero zero  494 nov 30 17:29 timestamp.json
-rw------- 1 zero zero  531 nov 30 17:28 targets.json
-rw------- 1 zero zero  682 nov 30 17:28 snapshot.json
-rw------- 1 zero zero 2417 nov 30 17:03 root.json
...
...
trust/tuf/docker.io/library/busybox/metadata:
total 28
-rw------- 1 zero zero 498 nov 30 17:17 timestamp.json
-rw------- 1 zero zero 13335 nov 30 16:41 targets.json
-rw------- 1 zero zero 688 nov 30 16:41 snapshot.json
-rw------- 1 zero zero 2405 nov 30 16:41 root.json

记住前面一节中描述的 JSON 文件。所有这些文件都位于每个注册表和仓库的结构下。

Docker 客户端会将你的密钥存储在.docker/trust/private目录下。妥善保管这些密钥非常重要。要备份这些密钥,可以使用$ umask 077; tar -zcvf private_keys_backup.tar.gz ~/.docker/trust/private; umask 022命令。

Notary 将帮助我们管理签名。它是一个开源的服务器和客户端应用程序,可以从其 GitHub 项目页面下载(github.com/theupdateframework/notary)。

Notary 可以安装在 Linux 或 Windows 主机上。

我们将使用curl命令下载最新版本,并修改其权限和路径:

$ curl -o /tmp/notary -sL https://github.com/theupdateframework/notary/releases/download/v0.6.1/notary-Linux-amd64

$ sudo mv /tmp/notary /usr/local/bin/notary

$ sudo chmod 755 /usr/local/bin/notary

在本节中,我们将使用 Docker 自己发布的 Notary 服务器,该服务器已在互联网上发布(notary.docker.io),并且与 Docker Hub 相关联。

Docker 企业版将在你的环境中运行自己的 Docker Notary 服务器实现。

让我们验证,例如,与 Docker Hub 仓库关联的所有签名。在这个例子中,我们正在查看busybox仓库。我们使用notary list,并提供适当的服务器和目录参数:

$ notary -s https://notary.docker.io -d ~/.docker/trust list docker.io/library/busybox
NAME DIGEST SIZE (BYTES) ROLE
---- ------ ------------ ----
1 1303dbf110c57f3edf68d9f5a16c082ec06c4cf7604831669faf2c712260b5a0 1864 targets
...
...
1.31.1-uclibc 817e459ca73c567e9132406bad78845aaf72d2e0c0965ff68861b318591e949a 1210 targets
buildroot-2013.08.1 c0a08c5e4c15c53f03323bae8e82fdfd9f4fccb7fd01b97579b19e3e3205915c 5074 targets
buildroot-2014.02 ced99ae82473e7dea723e6c467f409ed8f051bda04760e07fd5f476638c33507 5071 targets
glibc 0ec061426ef36bb28e3dbcd005f9655b6bfa0345f0d219c8eb330e2954f192ac 1638 targets
latest 1303dbf110c57f3edf68d9f5a16c082ec06c4cf7604831669faf2c712260b5a0 1864 targets
...
...
uclibc 817e459ca73c567e9132406bad78845aaf72d2e0c0965ff68861b318591e949a 1210 targets

我们列出了远程受信集合中的所有目标——在这个例子中,是 Docker Hub 上的busybox集合(docker.io/library/busybox)。

现在,让我们学习如何自动化这些过程,并确保安全性,以便在我们的组织中构建一个受信环境。

在受信环境中创建和运行应用程序

在本节中,我们将考虑一个受信环境,在其中CONTENT_TRUST_ENABLED会用于所有操作。这将确保在该环境中构建的镜像始终会被签名。所有已推送和拉取的镜像都会被签名,我们只会运行基于受信镜像的容器。

将 CI/CD 编排工具加入这些流程是非常有趣的。在没有某些系统或更高安全政策的情况下,禁止非受信内容并不容易。如果我们将DOCKER_CONTENT_TRUST的值设置为仅允许 Docker 内容信任,但用户可以直接与 Docker 主机交互,那么他们可以在命令行禁用此功能。

自动化在生产环境中至关重要,尽管 Docker 企业版提供了其他方法,我们将在第十二章中讨论,通用控制平面。Kubernetes 也提供了强制执行受信内容安全的功能,但这一话题超出了本书的范围。

使用外部 CI/CD,我们可以自动化构建、共享或部署 Docker 内容。让我们看一个简单的例子,展示如何构建和推送一个镜像:

$ export DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE="MyVerySecureRootPassphraseForAutomation"
$ export DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE="MyVerySecureRepositoryPassphraseForAutomation"

$ docker build -t docker/trusttest:testing .
Using default tag: latest
latest: Pulling from docker/trusttest
b3dbab3810fc: Pull complete
a9539b34a6ab: Pull complete
Digest: sha256:d149ab53f871

$ docker push docker/trusttest:latest
The push refers to a repository [docker.io/docker/trusttest] (len: 1)
a9539b34a6ab: Image already exists
b3dbab3810fc: Image already exists
latest: digest: sha256:d149ab53f871 size: 3355
Signing and pushing trust metadata

我们可以编写一个脚本,用于 CI/CD 编排任务,使用rootrepository密码短语来确保在构建和推送到我们的注册表时应用内容信任。我们可以使用相同的方法在生产环境中进行部署,禁止任何用户与这个安全环境进行交互。在脚本中处理密码短语的环境变量时要小心,因为它们将是可见的。CI/CD 编排工具将提供安全的管理方法。这样,你就能了解如何使用你自己的管理配置工具来实现一个安全的链条。

现在,让我们回顾一个实验,更好地理解我们在本章中所学的内容。

本章实验

我们现在将完成一个实验,帮助我们提升对所学概念的理解。

如果你还没有部署过 environments/standalone-environment,请从本书的 GitHub 仓库(github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git)部署它。你也可以使用你自己的 CentOS 7 服务器。从 environments/standalone-environment 文件夹中使用 vagrant up 启动你的虚拟环境。

如果你正在使用独立环境,请等它运行起来。我们可以使用 vagrant status 检查节点的状态。使用 vagrant ssh standalone 连接到你的实验节点。standalone 是你的节点名称。你将使用具有管理员权限的 vagrant 用户,通过 sudo 执行操作。你应该会看到以下输出:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant up
Bringing machine 'standalone' up with 'virtualbox' provider...
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant status
Current machine states:
standalone running (virtualbox)
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$

现在,我们可以使用 vagrant ssh standalone 连接到独立节点。如果你之前已经部署过独立虚拟节点并且刚刚使用 vagrant up 启动它,那么此过程可能会有所不同:

Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant ssh standalone
[vagrant@standalone ~]$ 

如果你正在重复使用你的独立环境,这意味着 Docker Engine 已经安装。如果你启动了一个新的实例,请执行 /vagrant/install_requirements.sh 脚本,以确保你拥有所有必需的工具(Docker Engine 和 docker-compose):

[vagrant@standalone ~]$ /vagrant/install_requirements.sh 

现在,你已经准备好开始实验了。

为 Docker Hub 签名镜像

首先,如果你还没有 Docker Hub 账户,请登录 hub.docker.com/signup 创建一个账户。你可以使用你自己的注册中心,但你需要有一个 Notary 服务器在运行。让我们开始吧:

本实验将使用 Docker Hub 上的 frjaraur/pingo 仓库。你必须将 frjaraur 替换为你的用户名。

  1. 在本实验中,我们将从头开始。如果你之前已经签名过镜像,别删除你自己的 .docker/trust 目录。在这种情况下,请将信任目录备份到一个安全的位置,以便稍后恢复,或者干脆在 Docker 主机系统中创建一个虚拟用户。为了创建这个备份,我们将执行 cp -pR ~/.docker/trust ~/.docker/trust.BKP。在这些实验完成后,你可以恢复它:
[vagrant@standalone ~]$ rm -rf ~/.docker/trust/
  1. 现在,启用 Docker 内容信任并为本实验创建一个目录:
[vagrant@standalone ~]$ export DOCKER_CONTENT_TRUST=1

[vagrant@standalone ~]$ cd $HOME
[vagrant@standalone ~]$ mkdir chapter6
[vagrant@standalone ~]$ cd chapter6 
  1. 我们准备了一个相当简单的 Dockerfile,它将 ping 命令发送到 8.8.8.8,并执行 300 次。如果你从本书的 GitHub 仓库下载了书本示例文件,可以在 chapter6 目录中找到这些实验文件。使用你的文本编辑器创建一个包含以下内容的 Dockerfile 文件:
FROM alpine:3.8 
RUN apk add --update curl 
CMD ping 8.8.8.8 -c 300 
  1. 现在,我们可以构建镜像。记得已经启用了 Docker 内容信任(Docker Content Trust)。我们将在你编写 Dockerfile 的目录中使用 docker image build
[vagrant@standalone chapter6]$ docker image build -t frjaraur/pingo:trusted .

Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM alpine@sha256:04696b491e0cc3c58a75bace8941c14c924b9f313b03ce5029ebbc040ed9dcd9
sha256:04696b491e0cc3c58a75bace8941c14c924b9f313b03ce5029ebbc040ed9dcd9: Pulling from library/alpine
c87736221ed0: Pull complete 
Digest: sha256:04696b491e0cc3c58a75bace8941c14c924b9f313b03ce5029ebbc040ed9dcd9
Status: Downloaded newer image for alpine@sha256:04696b491e0cc3c58a75bace8941c14c924b9f313b03ce5029ebbc040ed9dcd9
 ---> dac705114996
Step 2/3 : RUN apk add --update curl
...
...
Successfully built b3aba563b2ff
Successfully tagged frjaraur/pingo:trusted
Tagging alpine@sha256:04696b491e0cc3c58a75bace8941c14c924b9f313b03ce5029ebbc040ed9dcd9 as alpine:3.8

你可能注意到来自 Docker 守护进程的新消息。守护进程使用了alpine:3.8镜像哈希值sha256:04696b491e0cc3c58a75bace8941c14c924b9f313b03ce5029ebbc040ed9dcd9,而不是镜像名称和标签。如果我们本地有相同image:tag值的镜像,它将被验证。如果哈希不匹配,它将被避免,真实镜像将从 Docker Hub 下载。这将确保下载的是受信任的alpine:3.8镜像。

  1. 现在,我们将使用docker trust sign对这个镜像进行签名。此过程将要求我们创建一个root密码短语、一个repository密码短语和一个user密码短语(这是本章的新内容,因为我们在之前的章节中没有使用 Docker Content Trust)。这将创建一个新的trust目录在.docker. 当镜像被推送时,你将再次被询问关于注册用户密码短语的问题。这不是你的 Docker Hub 密码,而是你为执行签名操作创建的密码短语。我们将使用docker trust sign
[vagrant@standalone chapter6]$ docker trust sign frjaraur/pingo:trusted
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 9e788ed: 
Repeat passphrase for new root key with ID 9e788ed: 
Enter passphrase for new repository key with ID fb7b8fd: 
Repeat passphrase for new repository key with ID fb7b8fd: 
Enter passphrase for new frjaraur key with ID f1916d7: 
Repeat passphrase for new frjaraur key with ID f1916d7: 
Created signer: frjaraur
Finished initializing signed repository for frjaraur/pingo:trusted
Signing and pushing trust data for local image frjaraur/pingo:trusted, may overwrite remote trust data
The push refers to repository [docker.io/frjaraur/pingo]
6f02cc23eebe: Pushed 
d9ff549177a9: Mounted from library/alpine 
trusted: digest: sha256:478cd976c78306bbffd51a4b5055e28873697d01504e70ef85bddd9cc348450b size: 739 
Signing and pushing trust metadata 
Enter passphrase for frjaraur key with ID f1916d7: 
Successfully signed docker.io/frjaraur/pingo:trusted
  1. 这样,镜像就已经签名并推送到 Docker Hub。我们可以使用curl验证镜像是否已上传:
[vagrant@standalone chapter6]$ curl -s https://hub.docker.com/v2/repositories/frjaraur/pingo/tags|jq
{
 "count": 1,
 "next": null,
 "previous": null,
 "results": [
 {
 "name": "trusted",
 "full_size": 4306493,
 "images": [
 {
 "size": 4306493,
 "digest": "sha256:478cd976c78306bbffd51a4b5055e28873697d01504e70ef85bddd9cc348450b",
 "architecture": "amd64",
 "os": "linux",
 "os_version": null,
 "os_features": "",
 "variant": null,
 "features": ""
 }
 ],
 "id": 78277337,
 "repository": 8106864,
 "creator": 380101,
 "last_updater": 380101,
 "last_updater_username": "frjaraur",
 "image_id": null,
 "v2": true,
 "last_updated": "2019-11-30T22:03:28.820429Z"
 }
 ]
}
  1. 最后,我们将使用docker trust inspect查看镜像签名:
[vagrant@standalone chapter6]$ docker trust inspect --pretty frjaraur/pingo:trusted
Signatures for frjaraur/pingo:trusted
SIGNED TAG DIGEST SIGNERS
trusted 478cd976c78306bbffd51a4b5055e28873697d01504e70ef85bddd9cc348450b frjaraur
List of signers and their keys for frjaraur/pingo:trusted
SIGNER KEYS
frjaraur f1916d7ad60b
Administrative keys for frjaraur/pingo:trusted
Repository Key: fb7b8fdaa22738c44b927110c377aaa7c56a6a15e2fa0ebc554fe92a57b5eb0b
 Root Key: 4a739a076032b94a79c6d376721649c79917f4b5f8c8035ca11e36a0ed0696b4

现在,在我们看一些问题之前,让我们简要总结一下本章所涵盖的主题。

总结

Docker Content Trust 帮助我们确保容器环境中的内容安全,确保镜像的来源和受信任的内容。在生产环境中,能够确保任何正在运行的容器是由受信任内容生成的至关重要。如果无法验证镜像的安全性,则不应允许基于该镜像运行容器。

我们已经了解到,Content Trust 通过四个基本密钥提高了 Docker 仓库的安全性。根密钥确保所有权,目标密钥允许验证特定集合或仓库中的内容。这些密钥将受到密码短语的保护,签名时会要求我们输入这些密码。快照和时间戳密钥不需要用户交互,将自动生成,以保证内容密钥文件以及签名镜像的日期和过期时间。

在下一章,我们将介绍编排的概念。我们将回顾管理基于容器的应用程序在分布式环境中的所有功能。

问题

  1. 以下哪句话是不正确的?

a) Docker Content Trust 基于 TUF。

b) TUF(信任更新框架)被开发用来确保软件更新过程。

c) 无法验证新的软件版本。

d) 以上所有陈述都是错误的。

  1. 以下哪项名称表示用于验证镜像内容的 Docker Content Trust 密钥?

a) 目标

b) 用户

c) 组

d) 时间戳

  1. 我们如何确保busybox:latest发布的镜像确实是最新的?

a) 我们无法确保镜像的最新性。

b) busybox:latest表示这是创建的最新镜像。

c) 内容信任将验证镜像的新鲜度;因此,我们可以确保主机确实执行的是busybox:latest镜像,尽管我们无法确保它是最新的。

d) 之前的所有陈述都是错误的。

  1. 为什么我们在尝试签署busybox:trusted时会遇到denied: requested access to the resource is denied错误?

a) 此镜像不存在。

b) 我们不被允许修改该仓库。

c) Docker 内容信任可能没有启用。

d) 之前的所有内容。

  1. 我们丢失了根密钥,因为我们更换了笔记本电脑。以下哪一项陈述是正确的?

a) 如果我们在.docker/trust/private下没有密钥,在签名时会生成一个新的密钥。

b) 如果我们进行备份,我们可以恢复私钥根密钥。

c) 如果我们生成了新的密钥,我们的旧镜像将变得不可信,并且我们需要重新签名它们。

d) 之前的所有陈述都是正确的。

进一步阅读

您可以参考以下链接,获取有关本章所涵盖主题的更多信息:

第九章

第二章 - 容器编排

在本节中,我们将讨论在集群环境中容器的编排。我们将学习如何基于集群级分布式组件部署应用程序。你还将学习编排工具如何管理应用程序的进程及其相互作用,并将它们发布给用户。

本节包含以下章节:

  • 第七章,编排概述

  • 第八章,使用 Docker Swarm 进行编排

  • 第九章,使用 Kubernetes 进行编排

编排介绍

在本章中,我们将讨论可应用于容器环境的编排概念。我们将了解为什么需要编排来部署基于容器组件的应用程序在一组节点上。编排器为环境提供了新功能,但也引入了新的管理挑战。我们还将查看新的定义,以便我们可以在分布式编排环境中提供 Docker 引擎的功能。本章将介绍重要的概念,这些概念将帮助您了解 Swarm 和 Kubernetes 编排器。

我们将学习编排作为一个概念,并引入一些有趣的话题,如在分布式和动态环境中编排的重要性,以及它允许我们轻松地扩展和缩减以及更新应用程序组件。我们还将学习如何管理无状态和有状态组件,并在分布式部署中提供数据持久性。

在本章结束时,您将了解什么是编排器及其如何应用于基于容器的应用程序环境。

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

  • 引入编排概念

  • 学习容器编排

  • 调度应用程序集群范围

  • 管理数据和持久性

  • 扩展和更新应用程序组件

本章不包括任何实验室,因为它是一个介绍性的章节,涉及理论和一般概念。

让我们从介绍编排作为管理分布式应用程序的关键概念开始。

第十章:引入编排概念

了解本章中的编排概念是关键的,以便我们可以更多地了解 Docker Swarm 或 Kubernetes。让我们想象一个交响乐团:有小提琴手、钢琴手、打击乐手等等;每位乐手都经过多年的学习成为专业音乐家。他们可以独奏完美,但当我们增加更多乐器时情况就变得困难。乐手们可以阅读乐谱,每个人都会演奏自己的部分。但即使是最优秀的音乐家在一起演奏时也需要有人引导。指挥是确保所有乐器协调运作的关键。

当我们将应用程序分解为小块 - 微服务 - 需要编排。一个应用程序需要许多组件协同工作。记住,将单片应用程序分割成不同的功能也会创建新的开发工作流程。我们可以有不同的开发人员小组,专注于一个功能。每个应用程序组件都是一个原子组件。

部署一个应用程序需要同时执行和管理其所有组件。一个编排器将管理这些组件和应用程序的生命周期。

编排还将管理组件的依赖关系,或者至少提供一些工具来实现应用程序逻辑。

当应用程序的组件分布在一组计算节点中时,编排显得尤为重要。我们甚至可以将这些组件分布在不同的云提供商上,或者将本地和云基础设施混合使用。

时间同步在分布式环境中至关重要,特别是在我们使用安全套接字层/传输层安全SSL/TLS)或其他基于证书的解决方案来保障连接安全时,这一点更为重要。

总结来说,我们可以说,编排提供了我们管理应用程序组件所需的工具,使其能够在分布式环境中无缝运行。

现在我们已经了解了协调器在分布式应用程序中的作用,接下来我们深入探讨容器环境。

学习容器编排

编排帮助我们管理运行多个组件的应用程序。在我们的案例中,这些组件或微服务将在容器中运行。因此,下面我们来总结一下容器环境中所需的特性:

  • 部署:所有应用程序组件必须以协调的方式运行。协调器应帮助我们按需部署应用程序组件,并确保它们按正确的顺序运行。

  • 配置:在分布式环境中管理配置并不容易。协调器应当管理此配置,并且该配置应在任何容器需要时都能访问。

  • 弹性:如果某个应用程序组件失败,协调器应尽可能保持应用程序的健康。

  • 扩展:微服务概念允许将应用程序组件复制,以提高应用程序性能(如果需要)。如果不再需要额外的计算资源,我们应该能够停用这些副本,以节省资源。

  • 节点分布:为了确保高可用性,我们将提供一池协调的计算节点。这样,我们可以将所有应用程序组件分布在不同的节点上。如果其中一些节点宕机,协调器应确保这些节点上的组件能够自动在其他健康的节点上运行。

  • 网络:由于我们在不同主机之间分发应用程序,协调器需要提供所需的应用程序组件交互。在这种情况下,网络非常关键。

  • 发布:协调器还应确保与正在运行的应用程序组件进行交互,因为我们应用程序的目的是为客户提供服务。

  • 状态:应用程序组件的状态很难管理。因此,编排无状态组件更容易。这就是为什么人们将容器视为临时工件的原因。我们了解到容器有它们自己的生命周期,它们并不真的是临时的。它们存在于主机上,直到有人删除它们,但编排应在允许的地方调度这些组件。这意味着容器的状态应该以协调的方式进行管理,并且组件应该在任何地方以相同的属性运行。当应用程序组件移动到另一个主机时,必须维护它们的状态。在大多数情况下,如果组件死亡,我们将运行一个新的、新鲜的容器,而不是尝试重新启动一个不健康的容器。

  • 存储:如果某些应用程序组件需要存储,例如持久化数据,编排器应提供与主机可用存储提供者交互的接口。

正如你所看到的,编排帮助我们维护应用程序逻辑,但它并不能做到魔法。事实上,编排器并不了解你的应用程序逻辑。我们必须以某种配置形式提供这些逻辑。

在本章中,我们介绍了可以应用于众所周知的容器编排器的概念。Kubernetes 和 Swarm 是最常用的,尽管还有其他编排器。

编排不会直接运行容器。容器被打包到其他编排结构中。这些原子结构将根据某些属性或关键值在整个集群中进行调度,编排器应该决定最佳的启动这些原子组件的位置。所有编排器都需要类似数据库的组件来存储编排对象、它们的属性和状态。

在 Kubernetes 中,我们部署 Pod,这是一起运行的多个容器。在 Swarm 中,我们部署服务,这些服务基于任务 - 最终,这些任务都是容器。因此,我们从不启动容器。我们有其他部署单位。如果我们将容器原样部署到主机上,它将不会由编排器管理。

在 API 时代,编排器是通过其公开的 API 进行管理的。事实上,我们将使用 kubectldocker 命令通过它们的 API 与编排过程进行交互。这对我们来说将是透明的。客户端应用程序将使用不同的参数和操作来执行工作。

编排器还基于微服务架构。它们有许多分布式组件。至少需要一个数据库,正如我们之前提到的,还需要一个 API 服务器和一个调度器来决定在哪里运行定义的应用程序工作负载。我们将考虑应用程序作为逻辑组件组,使用调度单元进行定义。

在下一节中,我们将讨论编排如何决定在整个集群环境中运行应用程序组件的位置。

在整个集群中调度应用程序

到目前为止,我们已经了解了协调器的期望功能以及使其正常工作的基本组件。我们提到了将应用程序组件分布到不同的主机上。为了能够分布应用程序组件,我们需要部署一个集群。集群是一个由多个节点协同工作的集合。将应用程序部署到主机上的过程应与将同一应用程序部署到集群上类似。协调器将管理整个工作流,这一过程对我们来说应是透明的。

协调器通常管理具有不同角色的节点。根据这些节点运行的进程类型,我们将定义管理节点和工作节点。每个协调器的实现可能会有不同的名称,但逻辑是一样的。管理节点执行协调控制平面,而工作节点执行应用程序部署。因此,工作节点是计算节点。

控制平面节点管理协调框架所需的所有操作。前面提到的数据库,用于存储所有对象定义和状态,将运行在这些节点上。调度器逻辑也将在这些节点上运行。根据使用的数据库类型,协调工作可能需要一些特殊节点。许多协调器依赖于键值数据库(通过 HTTP/HTTPS 协议访问的非常快速的数据库)。

在这些情况下,数据库使用 Raft 共识协议。这意味着在环境中的每个更改被存储到数据库之前,必须有一个定义的节点数量进行投票。只有当所有必要的投票正确收到后,数据库的值才会同步到其他节点。这确保了所有节点都有相同的信息,并且在部分节点宕机时,数据库仍然是安全的。这在这些环境中是一个非常重要的特性。这也是为什么 Swarm 和 Kubernetes 等要求特定数量的管理节点才能正常工作的原因。

所有被协调的对象都有标签。其中一些标签是由协调器自动添加的,例如设置集群节点架构。其他标签可以手动配置,以定义一些特殊的行为或特征,比如定义组件的应用层或层级。层级是管理集群对象交互的关键。

协调器还将管理所有节点资源(如 CPU、内存和可用端口等),并在部署之前检查是否有足够的资源来运行定义的工作负载。

编排器将在决定在哪个节点执行工作负载之前,审查所有节点资源、标签和其他应用要求。我们将能够设置一些亲和性和反亲和性功能,以指定一些特殊需求,当然,我们还可以使用标签帮助编排器为它们选择合适的位置。我们将使用这些标签将应用组件与更快速的节点、靠近某些必需组件的节点,或在集群中的每个节点上分布的节点关联起来。

请记住,应用组件可以在整个集群中部署。编排器应管理它们的网络交互并提供对这些部署的访问。

这些是编排的基本组件以及编排调度背后的逻辑。在下一节中,我们将简要了解数据和应用状态是如何管理的。请记住,本章仅是对一些编排概念的简要介绍,这些概念将由 Docker Swarm 和 Kubernetes 在其工作流中实现,具有不同的架构和更复杂的内容。

数据管理与持久化

在许多情况下,应用组件需要存储一些数据。在分布式环境中,这可能非常复杂。这就是为什么我们通常将容器视为短暂组件。无状态组件容易实现,但在有状态组件中,我们尝试将持久数据与容器的文件系统解耦。请记住,容器中的数据可能会丢失。事实上,编排系统并不关心数据,因此,如果一个容器崩溃,它只会运行一个新的容器。在这些情况下,我们需要将数据持久化到容器的环境之外。我们可以使用在第四章中学习到的关于卷对象的知识,容器持久化与网络,来实现这一点。我们定义了卷,以绕过容器的文件系统,从而提高性能并将数据存储在容器生命周期之外。

在分布式环境中,使用主机的本地存储会导致当应用组件从一个主机迁移到另一个主机时,出现不一致的状态。为避免这些情况,我们将使用主机的外部卷。事实上,我们将选择一种存储驱动程序,使我们能够在任何地方运行应用组件,同时保持它们所需的存储。所有编排器都可以根据需要向容器提供 NFS 存储,但在某些情况下,这还不够,需要使用专业驱动程序。云服务提供商和许多本地 软件定义存储 (SDS) 厂商提供 REST API 接口。存储驱动程序使用这些定义来帮助编排器找到合适的节点,以运行我们的应用组件。

编排器并不知道我们的应用逻辑。实现应用逻辑的代码是我们的责任。举例来说,一些编排器不会管理组件之间的任何依赖关系。我们还需要实现组件健康检查、规则以及在任何依赖组件故障时需要遵循的程序。如果所需的组件无法访问,我们应该实现重试程序。

Docker Swarm 和 Kubernetes 提供对象来确保配置文件和密钥(身份验证文件或凭证)在集群范围内分发。如前所述,编排不会管理数据,仅会管理这些类型的配置对象。

在下一节中,我们将学习编排器如何允许我们实现复制组件,以及如何在这些环境中简化应用程序升级。

扩展和更新应用组件

编排器还提供了另一个很棒的功能。如果我的应用准备运行某些组件的多个实例,编排器将帮助我们轻松管理这种复制。之所以容易,是因为组件基于容器,因此如果我们需要运行某个组件的多个副本,可以请求编排器执行更多容器。事实上,这一功能至关重要,因为在编排中,我们定义了应用组件及其所需的健康副本数量。如果所有所需副本都存活,该应用组件将被视为健康的。如果某个副本死亡,编排器会启动一个新的副本,以确保完成所需数量。

副本管理是编排器提供的一个功能。如果应用性能受到影响且应用逻辑允许复制,我们可以扩展或缩减组件的副本或实例数量。

另一方面,我们了解到微服务应用模型更适合组件的生命周期。开发人员可以专注于每个组件,修复错误和升级也更加容易。每个组件都被视为独立的功能模块。这使得我们能够将每个模块与其他模块分开管理。我们可以升级某个组件而不影响其他组件。编排器也管理这些过程。我们设定新的定义或属性,例如为应用组件设置新的镜像或不同的端口,编排器会为我们部署这些更改。我们能够设置如何进行这一过程。例如,我们决定一次更新多少个实例,或者更新之间的间隔等有趣的设置。

我们将在每个编排器章节中深入探讨这些功能。我们将在第八章,使用 Docker Swarm 进行编排和第九章,使用 Kubernetes 进行编排中了解 Docker Swarm 和 Kubernetes。

总结

在这一短小的章节中,我们学习了一些重要概念,这些概念将帮助我们理解 Docker Swarm 和 Kubernetes。我们先回顾了一下编排的概念,然后再详细了解编排所提供的功能。得益于编排,我们能够在不同的节点上跨集群分布应用组件,以提供更好的性能和可用性。应用的稳定性也得到了提升,因为我们能够执行多个组件实例。如果某些实例崩溃,编排工具将会部署新的实例。我们还能够根据应用的需求,在环境中按需扩展或缩减组件。

编排使用新的集群对象。这些对象存储在分布式数据库中,以确保高可用性。组件的状态和其他编排数据也将存储在这个数据库中。应用组件的数据和必要的逻辑并不由编排工具管理。我们使用外部组件共享信息,编排工具与它们交互,以确保当组件在不同的主机上部署时所需的数据能够随时可用。

在下一章中,我们将深入了解 Docker Swarm,并学习 Docker 如何实现我们已经回顾过的编排功能。

问题

在这一章中,我们总体了解了编排。接下来,我们将通过一些问题复习本章中介绍的内容:

  1. 以下哪个句子是正确的?

a) Kubernetes 和 Swarm 是运行分布式应用的编排工具。

b) 编排将应用逻辑复制为基于容器的对象。

c) 在分布式环境中无法管理应用数据。

d) 以上所有句子都是错误的。

  1. 编排工具管理哪些内容?

a) 应用组件的数据。

b) 应用组件的逻辑。

c) 应用组件的弹性。

d) 以上选项都不正确。

  1. 在分布式环境中部署多个组件的应用时,我们面临哪些挑战?

a) 应用组件的网络。

b) 应用组件逻辑。

c) 应用组件的弹性。

d) 以上选项都不正确。

  1. 编排为应用部署提供了哪些功能?

a) 我们通过设置所需的副本数来部署应用组件,以确保其健康。

b) 应用组件可以根据环境的需求进行扩展或缩减,编排工具将启动所需的实例。

c) 应用组件一次性全部更新。

d) 以上选项都不正确。

  1. 编排工具如何选择在哪个节点运行应用组件?

a) 具有足够资源的节点可以接收工作负载。

b) 我们可以为节点打标签,以便将某些组件固定在特定节点上。

c) 协调器将检查已定义的规则,以选择运行每个组件的位置

d) 所有前述句子都是正确的。

进一步阅读

使用 Docker Swarm 进行编排

在上一章中,我们学习了编排功能。在本章中,我们将基于此继续学习 Docker Swarm。它与 Docker 引擎(Docker 安装包)一起捆绑提供,因此我们无需安装其他软件。相比其他编排器,掌握 Docker Swarm 的基础更为简单,而且它足够强大,可以用于生产部署。

总结来说,在本章中,我们将学习如何在生产环境中部署 Docker Swarm。我们还将回顾 Docker Swarm 引入的新对象以及部署基于容器的完整应用程序所需的步骤。网络是节点分布式应用的关键,因此我们将探讨 Docker Swarm 如何提供解决方案来处理内部网络、服务发现和发布已部署的应用程序。在本章结束时,我们将回顾 Docker Swarm 如何帮助我们在不中断服务的情况下升级应用程序的组件。

本章我们将讨论以下主题:

  • 部署 Docker Swarm

  • 创建 Docker Swarm 集群

  • 在集群中调度工作负载 – 任务与服务

  • 使用 Stacks 和其他 Docker Swarm 资源部署应用程序

  • Docker Swarm 中的网络

让我们开始吧!

第十一章:技术要求

在本章中,我们将学习 Docker Swarm 的编排器功能。我们将在本章末尾提供一些实验,供你测试理解并展示你学到的概念。这些实验可以在你的笔记本电脑或个人电脑上使用提供的 Vagrant “Docker Swarm”环境运行,或者使用你自己已经部署的 Docker Swarm 集群。请查看本书的 GitHub 代码库,获取我们将在本章中使用的代码以及更多信息,地址是 github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

查看以下视频,了解代码演示:

"bit.ly/31wfqmu"

部署 Docker Swarm

Docker Swarm 是与 Docker 引擎捆绑在一起的内置编排器。它是在 Docker 引擎 1.12 版本中引入的(版本号在 1.13 后改为四位数字),以swarm 模式呈现。之前有一种称为传统 Swarm 的方法,它的架构更接近 Kubernetes,需要一个外部的键值存储数据库等组件。Swarm 模式与此不同,因为它包含了运行编排器所需的所有内容,可以开箱即用。

Swarm 架构相当简单,因为它默认提供了组件之间的安全通信。在部署 Docker Swarm 集群之前,让我们回顾一下它的主要特点:

  • 每个 Docker Engine 都包含多节点容器编排:这意味着我们可以部署一个不需要其他软件的集群。Docker Engine 提供了部署和管理集群所需的所有组件。

  • 节点角色可以在运行时更改:编排基于不同的节点角色。虽然控制平面由管理者或主节点管理,但计算或应用程序部署将在从节点、工作节点或随从节点上完成。每个编排器对这些不同角色使用不同名称,但它们本质上是相同的。Swarm 允许我们在一个角色不健康或需要执行维护任务时将节点从一个角色更改为另一个角色。

  • 工作负载将被声明为服务,定义需要保持健康的实例数量:Docker 编排器将保持所需数量的副本存活。如果其中一些实例失败,编排器将运行新任务以保持所需数量的实例存活。编排器将管理此调解过程。如果一个节点失败,编排器将所有容器迁移到一个新的、健康的节点。

  • 由于工作负载基于所需实例数量,编排器允许我们随时更改此数字:因此,我们可以根据请求的高需求来扩展或减少服务(应用程序组件)的实例数量。

  • 我们将基于多个服务组件部署应用程序,并满足它们的所有需求和连接要求:由于组件可能在任何集群节点上运行,Docker Swarm 将提供内部叠加网络以连接所有应用程序组件。

  • Swarm 将提供服务发现和内部负载均衡:在服务发现和负载均衡部分,我们将了解 Docker Swarm 如何提供内部应用程序 DNS 解析,以便所有组件可以轻松发现彼此,并通过虚拟 IP 在服务副本之间进行负载均衡。

  • 编排将允许我们自动更新应用程序组件:事实上,我们只需决定如何管理这些更新;编排将完成其余工作。这样,我们可以更新应用程序组件而不会影响用户。

  • 我们可以确保默认情况下集群安全运行:Docker Swarm 将部署传输层安全TLS)来连接控制平面组件。它将为所有节点管理证书,创建内部 CA 并验证所有节点证书。

只有控制平面默认安全,用户访问应用发布等功能需要额外配置。

正如我们在上一章中所学到的,调度器需要数据库来存储和管理工作负载及其他集群资源信息。Docker Swarm 在/var/lib/docker/swarm路径下有一个内建的键值存储(这是 Linux 上的路径;在 Windows 上,它可以在C:\ProgramData\docker目录下找到其对应的路径)。

重要的是要理解,/var/lib/docker/swarm目录是必不可少的,万一我们需要恢复一个不健康的集群时,请务必保护好这个目录,并保持备份。

我们可以使用密钥锁定用户对/var/lib/docker/swarm路径的访问权限,从而提高安全性。如果路径被解锁,拥有足够系统权限的人可以获取 Docker Swarm 证书。

Docker Swarm 整体架构

如前所述,Docker Swarm 部署了其自己的安全控制平面。节点角色有两种:

  • 管理节点:这些节点管理整个 Swarm 集群环境。它们共享一个内部的键值数据库。更具体地说,其中一个管理节点担任不同的角色,即集群的领导者。每个集群只有一个领导者,领导者负责对数据库进行所有必要的更新。其他所有管理节点都会跟随并将它们的数据库与领导者的数据库同步。管理节点维护集群健康,提供 Swarm HTTP API 服务,并在可用计算节点上调度工作负载。

  • 工作节点:工作负载将在工作节点上运行。需要注意的是,管理节点也有工作节点角色。这意味着,如果我们没有指定特殊的调度位置,工作负载也可以在管理节点上运行。工作节点永远不会参与调度决策,它们只会运行分配的工作负载。

我们通过对每个工作负载使用位置约束或禁用某些节点上的容器执行来管理工作负载的位置。

在具有多个网络接口的节点上,我们将能够选择用于控制平面的接口。管理节点将实现 Raft 共识算法来管理 Swarm 集群状态。该算法要求多个服务器就数据和状态达成一致。一旦它们就某个值做出决定,该决定会被写入磁盘。这将确保信息在多个管理节点间一致地分发。

如前所述,存在一个领导节点,它修改并存储数据库中的变更;所有其他节点将与其同步数据库。为了保持一致性,Swarm 实现了 Raft 协议。该算法将管理数据库中的所有更改,并在领导节点不健康时进行新领导的选举。当领导节点需要做出更改(例如,修改应用程序的组件状态及其数据)时,它会询问所有其他节点的意见。如果所有节点都同意更改,领导节点将提交该更改,并将其同步到所有节点。如果领导节点失败(例如,节点宕机,服务器进程崩溃等),将触发新的选举。在这种情况下,所有剩余的管理节点将投票选举新的领导节点。

该过程需要达成共识,绝大多数节点需同意选举结果。如果没有达成多数意见,将触发新的选举过程,直到选出新的领导节点。选举完成后,集群将恢复健康。记住这些概念,因为它们是 Docker Swarm 和其他协调器的关键。

以下图示表示 Swarm 协调器的基本架构:

让我们详细回顾每个平面的内容。

管理平面

管理平面是执行所有管理任务的层次。所有集群管理流量和工作负载维护都将在此平面进行。管理平面提供基于奇数个管理节点的高可用性。

默认情况下,所有在此平面中的通信都使用 TLS(双向 TLS)进行加密。这是 Raft 协议运行的地方。

控制平面

该平面管理集群的状态。Gossip 协议会定期通知所有节点集群状态,减少节点所需的相关信息,仅提供集群健康概况。这一协议管理主机之间的通信,因此被称为控制平面,因为每个主机只与它最近的伙伴通信,信息通过该平面流动,传递至控制平面内的所有节点。

数据平面

数据平面管理所有服务的内部通信。它基于 VXLAN 隧道,将二层数据包封装在三层头部内。它将使用 UDP 传输,但 VXLAN 保证没有丢包。我们可以在创建(或加入)Docker Swarm 时使用适当的标志,将数据平面与控制和管理平面隔离开来。

当我们初始化一个新的 Docker Swarm 集群时,它会生成一个自签名的证书颁发机构CA),并向每个节点发放自签名证书。这确保了双向 TLS 通信。以下是当一个新节点加入集群时,确保安全通信的步骤概述:

  1. 当一个节点加入时,它会将其加入令牌和证书请求一起发送给管理节点。

  2. 然后,如果令牌有效,管理节点接受该节点的请求并返回一个自签名的节点证书。

  3. 然后,管理节点将新节点注册到集群中,它将成为 Docker Swarm 集群的一部分。

  4. 一旦节点被包含在集群中,它将准备好(默认情况下)接受由管理节点调度的任何新工作负载。

在下一部分,我们将学习如何使用常见的 Docker 命令行操作轻松部署 Docker Swarm 集群。

使用命令行部署 Docker Swarm 集群

我们可以使用 Docker 的swarm对象来初始化一个新集群,加入或离开一个先前创建的集群,并管理所有 Docker Swarm 属性。让我们来看一下docker swarm操作:

  • init:我们将使用docker swarm init来初始化一个新集群或重新创建一个已有集群(我们将在高可用性与 Swarm部分更详细地描述这种情况)。在集群创建过程中,我们将设置许多集群选项,但有些选项可以稍后更改。最重要的选项是--data-path-addr--data-path-port,因为它们用于设置在多网卡节点上哪个节点接口将专门用于控制平面。

这些是创建集群时最常用的参数:

  • --advertise-addr:此选项允许我们设置用于宣布集群的接口。所有其他节点将使用该接口的 IP 地址来加入集群。

  • --data-path-addr/--data-path-port:这些选项配置用于控制平面的接口和端口。该接口的所有流量将使用 TLS 加密,证书将由 Swarm 内部管理。我们可以使用 IP 地址/端口或主机接口表示法。默认端口是4789

  • --external-ca/--cert-expiry:尽管 Swarm 会为我们管理 TLS,但我们可以使用此参数部署自己的 CA 以管理所有证书。我们还可以指定证书的旋转频率。默认情况下,证书每 90 天(2160 小时)自动重新创建。

  • --listen-addr:此选项允许我们指定将用于提供集群 API 的主机接口。我们可以使用 IP 地址/端口或主机接口表示法,默认值为0.0.0.0:2377

  • --autolock:正如我们之前提到的,我们可以锁定对内部 Docker Swarm 数据的访问。这很重要,因为/var/lib/docker/swarm包含了 CA 和其他证书。如果您不确定节点访问权限,最好将此目录锁定,以防止用户访问。使用此选项时要小心,因为任何系统或 Docker 守护进程的重启都需要解锁密钥才能再次启用此节点。

  • --dispatcher-heartbeat:此选项将管理节点报告健康状况的频率。默认值为 5 秒,但如果您的集群存在高延迟,可以进行更改。

  • --max-snapshots/--snapshot-interval: Swarm 将为管理节点的同步创建数据库快照。我们可以设置保留的快照数量。默认情况下,不会保留任何快照(只为同步保留一个),但这些快照在调试或灾难恢复时可能非常有用。我们还可以设置快照之间的间隔。更改此选项时要小心,因为保存过多的快照会触发大量的同步操作到其他节点,可能会导致高性能开销。但另一方面,较少的同步也可能使集群进入不同步的状态。此参数的默认值为 10,000 毫秒。

  • join: 集群初始化后,所有其他节点将加入先前创建的集群,无论它们是管理节点还是工作节点。将 Docker 节点加入集群需要集群特定的令牌,管理节点和工作节点的令牌不同。我们始终需要一个令牌和领导者的 IP 地址来加入集群。请记住,领导者的 IP 可能会发生变化。我们还可以设置控制平面的 IP 和端口、要向其他节点公布的 IP 地址以及 API 的监听 IP 地址。我们将在加入节点上执行以下命令:docker swarm join --token <MANAGER_OR_WORKER_TOKEN> <LEADER_IP:PORT>

  • leave: 一旦节点成为集群的一部分,我们可以根据需要让其退出集群。理解退出集群的含义很重要。leave命令将在退出集群的节点上执行。管理节点无法退出集群,因为这样会导致集群进入不健康状态。我们可以使用--force强制让节点退出集群,即使它是管理节点,但这会带来一些风险,必须在操作前充分理解。退出集群并不会将节点从内部的 Docker Swarm 数据库中移除。相反,我们需要通过执行docker node rm <NODE_NAME_OR_ID>命令来通知管理节点这一变化。

  • update: 通过此操作,我们可以更改 Docker Swarm 集群的一些描述属性,例如外部 CA、证书过期设置和快照行为。

  • ca: 如前所述,所有内部控制平面的通信都是基于 TLS 证书的。ca选项允许我们自定义 CA 和其他证书行为。我们可以轮换证书或选择自己的 CA。

  • join-token:通过此操作,我们可以查看当前的管理器和工作节点令牌。事实上,我们可以执行join-token,后跟所需的角色,以检索它们的值。我们不需要将其保管,因为我们可以根据需要随时检索这些令牌。这些令牌仅在加入集群时使用。我们可以随时更改它们,使用docker swarm join-token --rotate来创建一个新的令牌,这不会影响已经加入的节点。我们通常执行docker swarm join-token worker来检索加入节点到集群所需的命令行和令牌。我们可以使用--quiet仅检索令牌,这对自动化加入过程非常有用。

  • unlock/unlock-key:我们之前提到过,允许用户访问/var/lib/docker目录是不安全的。默认情况下,只有 root 用户允许访问,但将 Docker Swarm 信息加锁会更安全。例如,所有集群证书将存储在/var/lib/docker/swarm/certificates目录下。加锁 Swarm 信息是一个好习惯,但要注意不要丢失解锁密钥。每次集群节点启动时(例如 Docker Engine 或节点重启时),都需要解锁密钥。这在某些情况下会导致集群处于非自动、高可用环境。unlock选项用于解锁 Docker Swarm 集群信息,而unlock-key允许我们管理用于此行为的密钥。

Docker Swarm 还会创建新的对象:

  • swarm:这就是集群本身及其相关的属性,如前所述。

  • node:这些是集群中的节点。我们将为它们添加标签,并作为集群的一部分管理它们的角色。

  • service:我们在 Docker Swarm 集群上部署服务。我们不会部署独立的容器。我们将在集群中的工作负载调度——任务和服务部分学习更多关于服务的内容。

  • secretconfig:这两个对象允许我们在集群中共享服务配置。请记住,即使应用程序完全无状态,管理不同主机上的信息也并不容易。

  • stack:我们将使用堆栈来部署应用程序。我们将使用类似 Docker Compose 的文件格式,其中包含所有应用程序组件及其交互。

所有这些对象都将与之关联常见的操作,包括列出、部署/创建、删除和检查其属性。服务和堆栈将与容器相关联,因此我们将能够列出集群范围内的进程分布。

我们可以在笔记本电脑上运行单节点集群。对于测试或开发服务或堆栈,运行单节点集群并不成问题。

在下一部分,我们将学习如何部署具有高可用性的 Docker Swarm 环境。

部署具有高可用性的 Docker Swarm

到目前为止,我们已经了解了 Docker Swarm 集群中不同的角色。然而,为了提供高可用性,我们需要部署多个管理节点和工作节点。

Raft 一致性算法要求有奇数个健康节点才能工作,因为大多数节点必须就所有变更和资源状态达成一致。这意味着我们需要至少N/2+1个健康节点达成一致才能提交变更或资源状态。换句话说,如果少于N/2+1个管理节点健康,我们将无法保证 Docker Swarm 的可用性。让我们通过下表回顾这些选项,以便更好地理解:

管理节点数量 所需共识数量 (*N/2+1) 允许的故障数 提供高可用性?
1 1 0 否。
2 2 0 否。
3 2 1 是的。
4 3 1 是的,但这不比三节点管理选项更好,如果领导节点失败,可能会导致选举问题。
5 3 2 是的。
6 4 2 是的,但这不比五节点管理选项更好,如果领导节点失败,可能会导致选举问题。
7 4 3 是的。

当一个管理节点在 3 节点管理配置中失败时,两个节点可以达成一致,变更将不受问题影响地更新。但如果其中一个节点失败,只剩下一个节点,变更将无法提交。没有共识,也无法进行集群操作。这意味着,集群中部署的任何服务将继续运行。除非某个服务失去了一些副本,并且 Docker Swarm 应当启动新的副本以达到所需数量,否则用户不会受到影响。由于这些操作需要更新数据库数据,而这在此情况下是不允许的,因此不允许自动操作。在这种情况下,我们将无法添加或移除任何节点,集群将变得不一致。

因此,Swarm 需要奇数个管理节点来提供高可用性。虽然管理节点的数量没有限制,但不推荐超过七个。增加管理节点的数量会降低写入性能,因为领导节点需要更多节点的确认响应才能更新集群变更。这将导致更多的网络往返流量。

理解这些行为至关重要。即使我们已经部署了一个三节点集群,如果有足够多的节点变得不健康,我们仍然可能会失去法定人数。重要的是要尽快处理节点故障。

我们通常会部署三节点集群,因为它们允许 1 个节点的故障。这对生产环境来说已经足够,但在一些关键环境中,我们将部署五节点集群,以便允许两个节点的故障。

在需要将 Swarm 集群分布在不同位置的情况下,推荐的管理节点数量是七个。这将允许跨多个数据中心进行分布。我们将在第一个数据中心部署三个节点,在第二个数据中心部署两个节点,在第三个数据中心部署两个节点(3+2+2)。这种分布将使我们能够处理整个数据中心的故障,如果工作节点有足够的资源,服务可以重新分配。

当管理节点失败时会发生什么?领导节点将开始存储已提交的更改,以便在该管理节点重新恢复时同步它。这将增加 Docker Swarm 的目录大小。如果没有足够的磁盘空间来应对这种情况,节点可能会消耗整个文件系统,特别是在故障未能及时恢复的情况下。然后,你将得到第二个不健康的节点,集群将变得不一致。我们所描述的情况并不是恐怖电影——在新安装中,管理员通常认为集群可以在几周内容忍一些不健康的节点,这种情况发生得太频繁。

我们在讨论 Docker Swarm 集群初始化时提到过一个重要的选项,即 docker swarm 命令行表中的选项。在集群不健康但至少有一个管理节点正常工作的情况下,我们将使用 docker swarm init --force-new-cluster。如果集群未能达到法定人数并且无法对集群资源执行任何操作(即无法添加/删除节点,服务在失败后无法修复),我们可以强制创建一个新集群。这是一个极端情况。

在重新创建集群之前,请先处理好环境。强制创建新集群会将执行命令的节点设置为领导节点。集群中的所有其他节点(包括那些未能达到法定人数的管理节点)将被设置为工作节点。这就像是集群法定人数重置。服务和其他资源将保留其状态和配置(无论是已提交的还是从节点中恢复的)。因此,最终我们将得到一个单管理节点的集群,所有其他节点都将是工作节点。服务和其他内容不应受到影响。在这种情况下,查看管理节点的日志是一个好习惯,因为如果某些集群更改没有被提交,某些容器可能会被遗留在未管理状态。

虽然管理节点可以充当工作节点,但在生产环境中,最好仅在工作角色节点上运行工作负载。管理节点的进程可能会影响应用程序,反之亦然。

我们在生产环境中始终会部署多个工作节点。这将确保我们的服务在某个工作节点意外下线或需要执行维护任务(例如更新 Docker 引擎)时,服务的健康状态不会受到影响。通常,我们应该根据应用程序的资源需求部署工作节点。增加工作节点将提高集群的总工作负载容量。

在下一节中,我们将学习如何部署 Docker Swarm 集群。

创建 Docker Swarm 集群

现在我们已经回顾了 Docker Swarm 架构以及初始化集群所需的命令行操作,我们可以创建一个集群。到本章结束时,我们将拥有一个具有高可用性的完全功能集群。让我们先回顾一下 Docker Swarm 集群创建过程:

  1. 首先,我们在管理节点上初始化一个 Swarm 集群。由于没有其他管理节点可用,当前节点自动成为集群领导者。如果我们有一个具有多个接口的节点,我们将选择哪个接口与控制平面关联,哪些接口将用于其他节点和 Swarm API。输出将在您的环境中有所不同。让我们执行docker swarm init
$ docker swarm init
Swarm initialized: current node (ev4ocuzk61lj0375z80mkba5f) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-4dtk2ieh3rwjd0se5rzwyf2hbk7zlyxh27pbh4plg2sn0qtitx-50zsub5f0s4kchwjcfcbyuzn5  192.168.200.18:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
  1. 一旦集群创建完成,我们可以使用docker node ls来查看集群节点及其属性:
$ docker node ls
ID                            HOSTNAME    STATUS AVAILABILITY     MANAGER STATUS ENGINE VERSION
ev4ocuzk61lj0375z80mkba5f    * sirius     Ready     Active         Leader             19.03.2

第一列显示节点对象标识符。正如我们之前提到的,新的对象已通过 Docker Swarm 创建。第二列显示其来自内部主机解析服务的名称(这可能包含完全限定域名FQDN))。请注意主机名旁边的星号。这意味着我们当前正在操作该节点。所有命令都在该节点上执行,无论它是否是领导节点。

在 Docker Swarm 上,与集群范围对象相关的集群命令仅在管理节点上可用。我们不需要在领导节点上执行命令,但在工作节点上无法执行任何集群命令。我们不能列出节点或部署服务。

最后一列显示每个节点的 Docker Engine 版本。让我们来看一下STATUSAVAILABILITYMANAGER STATUS列:

  • STATUS,顾名思义,显示节点在集群中的状态。如果节点不健康,它将在此显示。

  • MANAGER STATUS显示节点的当前角色(在这种情况下,该节点是领导者)。我们有三种不同的状态:

    • Leader,当该节点是集群领导者时。

    • Manager,意味着该节点是集群的管理节点之一。

    • 如果值为空,则表示该节点具有工作节点角色,因此不属于控制平面的一部分。

  • AVAILABILITY表示节点接收工作负载的可用性。在这里,我们可以看到管理节点也能接收工作负载。我们可以设置这个节点属性。实际上,有三种不同的状态:

    • active,意味着该节点能够接收任何工作负载。

    • passive,意味着该节点不会运行任何额外的工作负载。已经在运行的工作负载将保持其状态,但不会允许新的工作负载。

    • drain是当我们禁用该节点上的任何工作负载时的状态。发生这种情况时,所有正在运行的工作负载将被移到任何其他健康且可用的节点上。

我们可以在节点加入集群时,甚至在我们创建集群时,通过使用docker swarm initdocker swarm join命令中的--availability标志来强制节点的行为。我们将为新工作负载设置节点可用性(active | pause | drain)。默认情况下,所有节点都将处于活动状态,准备接收工作负载。

  1. 我们将加入另一个节点作为工作节点来演示此操作,使用之前显示的集群初始化输出和docker swarm join
$ docker swarm join --token SWMTKN-1-4dtk2ieh3rwjd0se5rzwyf2hbk7zlyxh27pbh4plg2sn0qtitx-50zsub5f0s4kchwjcfcbyuzn5 192.168.200.18:2377
  1. 现在,我们可以通过执行docker node ls再次查看集群节点状态(记住,这个命令仅在管理节点上可用):
$ docker node ls
ID                         HOSTNAME     STATUS   AVAILABILITY MANAGER STATUS  ENGINE VERSION
glc1ovbcqubmfw6vgzh5ocjgs   antares     Ready     Active                          19.03.5
ev4ocuzk61lj0375z80mkba5f * sirius      Ready     Active          Leader          19.03.2

在这个例子中,我们在*标记的sirius节点上执行命令,它是一个领导者,因此是一个管理节点。注意,antares是一个工作节点,因为它在MANAGER STATUS列中没有值。

我们可以通过执行docker node inspect操作来查看节点信息(以下输出已被截断):

$ docker node inspect antares 
[
 {
 "ID": "glc1ovbcqubmfw6vgzh5ocjgs",
...
 "Spec": {
 "Labels": {},
 "Role": "worker",
 "Availability": "active"
 },
 "Description": {
 "Hostname": "antares",
 "Platform": {
 "Architecture": "x86_64",
 "OS": "linux"
 },
 "Resources": {
 "NanoCPUs": 16000000000,
 "MemoryBytes": 33736785920
 },
 "Engine": {
 "EngineVersion": "19.03.5",
            ...
 ...
         },
 "TLSInfo": {
 "TrustRoot": "-----BEGIN CERTIFICATE-----\nMIIBaTCCARCgAwIBAgIUUB8yKqt3uUh2wmF/z450dyg9EDAwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTkxMjI5MTA1NTAwWhcNMzkxMjI0MTA1\nNTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABACDe6KWpqXiEMyWB9Qn6y2O2+wH8HLoikR+48xqnjeU0SkW/+rPQkW9PilB\ntIYGwaviLPXpuL4EpVBWxHtMDQCjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBTbL48HmUp/lYB1Zqu3GL7q5oMrwTAKBggqhkjO\nPQQDAgNHADBEAiAh1TVNulaIHf2vh6zM9v6raer5WgTcGu8xQYBcDViPnwIgU4sl\ntK70bgSfEzLx6WpOv4yjr+c0tlJt/6Gj3waQl10=\n-----END CERTIFICATE-----\n",
 "CertIssuerSubject": "MBMxETAPBgNVBAMTCHN3YXJtLWNh",
 "CertIssuerPublicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAIN7opampeIQzJYH1CfrLY7b7AfwcuiKRH7jzGqeN5TRKRb/6s9CRb0+KUG0hgbBq+Is9em4vgSlUFbEe0wNAA=="
 }
 },
 "Status": {
 "State": "ready",
 "Addr": "192.168.200.15"
 }
 }
]

当我们检查一个节点时,关于其状态、节点 IP 地址和 TLS 信息将以 JSON 格式显示。

我们可以在节点上使用标签,帮助 Docker Swarm 为特定工作负载选择最佳位置。它使用节点架构将工作负载部署到合适的地方,但如果我们希望工作负载在特定节点上运行,可以添加一个独特的标签并添加约束来部署该工作负载。我们将在章节实验部分进一步学习服务位置和标签。

Spec键下,我们可以在docker node inspect输出中查看节点角色。我们可以在必要时更改节点角色。这相较于其他编排器是一个巨大改进,因为其他编排器的角色是静态的。请记住,角色更改将影响 Docker Swarm 架构,因为它会改变管理节点和工作节点的数量。记住高可用性要求管理节点数量为奇数,以及在节点故障时的后果。

  1. 角色只是一个节点属性,这意味着我们可以像更改其他对象属性一样更改它。记住,更改只能从管理节点部署。我们可以通过执行docker node update来更改节点的角色:
$ docker node update --role manager antares
antares

再次执行docker node ls列出集群中的所有节点,这次使用筛选器只获取管理节点:

$ docker node ls --filter role=manager
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
glc1ovbcqubmfw6vgzh5ocjgs antares Ready Active Reachable 19.03.5
ev4ocuzk61lj0375z80mkba5f * sirius Ready Active Leader 19.03.2

现在我们可以使用docker node inspect来获取ManagerStatus键:

$ docker node inspect antares --format "{{.ManagerStatus}}"
{false reachable 192.168.200.15:2377}

可以使用docker node rm将节点从集群中移除,就像我们对其他 Docker 对象所做的那样。我们只会移除工作节点。通常,将管理节点从 Docker Swarm 集群中移除需要先将其角色更改为工作节点。一旦节点角色更改为工作节点,我们就可以移除该节点。如果需要移除故障的管理节点,可以使用--force强制移除节点。但不推荐这么做,因为这可能会使集群处于不一致状态。必须在移除任何节点之前更新管理节点的数据库,这也是我们在这里描述的移除顺序如此重要的原因。

记住,如果降级或移除任何管理节点,确保管理节点的数量为奇数。如果没有奇数个管理节点,且在没有奇数个管理节点的情况下出现领导节点问题,当其他管理节点需要选举新领导时,可能会导致集群状态不一致。

如前所述,标签是节点的属性。我们可以在运行时添加和删除它们。这与第一章《使用 Docker 的现代基础设施与应用程序》中介绍的标签有很大的不同。那些标签是在 Docker 守护进程级别设置的,并且是静态的。我们需要将它们添加到daemon.json文件中,因此必须重新启动节点的 Docker 引擎才能使其生效。而在本例中,节点标签由 Docker Swarm 管理,可以通过常见的节点对象的update操作(docker node update)来更改。

如我们在前几章中观察到的那样,Docker 命令行提供了一些快捷方式。在这种情况下,我们可以通过将管理节点降级为工作节点角色,或将工作节点提升为管理节点角色来更改节点角色。我们使用docker node <promote|demote> <NODENAME_OR_ID>来在节点角色之间进行切换。

我们还可以更改节点的工作负载可用性。这使得节点可以接收(或不接收)集群部署的工作负载。与任何其他节点属性一样,我们将使用docker node update --availability <available|drain|pause> <NODENAME_OR_ID>来排空或暂停处于活动状态的节点。无论是排空还是暂停,都会阻止我们在该节点上调度任何新工作负载,而仅排空则会将任何当前运行的工作负载从受影响的节点上移除。

记住,当我们排空一个节点时,调度器会将任何正在该节点上运行的任务重新分配到其他可用的工作节点。请记住,其他节点在排空该节点之前应该有足够的资源。

在下一节中,我们将回顾如何备份和恢复故障的 Docker Swarm 集群。

恢复故障的 Docker Swarm 集群

我们将回顾一些备份和恢复 Docker Swarm 集群的步骤。丢失集群法定人数并不是什么大问题。正如我们所学,即使只有一个健康的管理节点,我们也可以通过强制初始化一个新集群来恢复集群。但是,如果丢失了集群数据,将完全摧毁您的环境,前提是没有任何一个操作正常的管理节点。在这些情况下,我们可以通过恢复在集群正常运行时采集的包含健康数据的副本来恢复集群。现在让我们学习如何备份我们的集群。

备份您的 Swarm

正如我们在本章中所学,/var/lib/docker/swarm(以及其对应的微软 Windows 目录)包含了键值存储数据、证书和加密的 Raft 日志。没有它们,我们无法恢复故障集群,因此让我们在任何一个管理节点上备份这个目录。

保持一致的备份需要静态文件。如果文件被打开或某些进程正在写入它们,那么它们将不一致。因此,我们需要停止指定节点上的 Docker Engine。不要在主节点上启动备份操作。

请记住,在备份操作进行时,如果 Docker 守护进程被停止,管理节点的数量将会受到影响。领导节点将继续管理更改,并生成新的同步点以恢复与丢失管理节点的同步。如果其他管理节点失败,您的集群将容易丧失法定人数。如果您计划进行每日备份,建议使用五个管理节点。

恢复您的 Swarm

如果我们需要恢复一个完全失败的集群(即所有管理节点无法达到法定人数,并且我们无法强制创建一个新集群),我们将停止一个管理节点上的 Docker Engine。删除所有/var/lib/docker/swarm目录内容(或其微软 Windows 对应目录)并将备份的内容恢复到该目录。然后,重新启动 Docker Engine,并使用docker swarm init --force-new-cluster重新初始化集群。

当单节点管理的集群健康时,开始添加其他旧 Swarm 集群的管理节点。在添加这些管理节点之前,确保它们已经退出了旧的 Swarm 集群。

如果我们启用了 Swarm 自动锁定功能,我们将需要与恢复的备份一起存储的密钥。即使您在备份发出后更改了密钥,您仍然需要使用旧的密钥。

在下一节中,我们将学习如何在集群上部署工作负载,以及 Docker Swarm 如何跟踪应用程序组件的健康状况,以确保在出现故障时服务不受影响。

在集群中调度工作负载 —— 任务和服务

我们不在 Swarm 集群上运行容器,而是部署服务。这些是可以在 Docker Swarm 集群中部署的原子工作负载。服务由任务定义,每个任务在 Docker Swarm 模型中由一个容器表示。Swarm 基于 SwarmKit,并继承了其逻辑。SwarmKit 是应对任何任务(例如虚拟机)集群化的需求而创建的,但 Docker Swarm 是与容器协同工作的。

Docker Swarm 调度器使用声明式模型。这意味着我们定义服务的期望状态,Docker Swarm 会处理其余的部分。如果服务的副本数或任务数不正确——例如,如果其中一个副本挂掉——Docker Swarm 将采取措施恢复服务的正确状态。在这个例子中,它会部署一个新的副本,以保持所有所需节点的健康。

以下图示表示了与容器相关的服务和任务。colors 服务有五个副本(colors.1colors.5)。每个副本都在同一镜像 codegazers/colors:1.13 的一个容器上运行,这些容器在 node1node2node3 上分布式运行:

创建服务需要以下信息:

  • 哪个镜像将运行关联的容器?

  • 该服务需要多少个容器才能保持健康?

  • 该服务是否应该在任何端口和协议上对用户可用?

  • 服务更新应如何管理?

  • 是否有该服务运行的首选位置?

创建服务时需要在命令行输入所有这些信息。由于服务是 Docker 对象,我们可以使用常见的操作,如列出、创建、删除、更新和检查它们的属性。Docker Swarm 将管理所有任务与服务的集成。我们永远不会部署任务或容器,只会创建和管理服务。让我们来看一下与服务相关的 Docker 命令行操作和选项:

  • create:这与其他对象相似,但服务有许多非标准属性。我们不会列出并审查所有服务参数,因为其中大多数是从容器继承的。在这里,我们将回顾与服务行为相关的最重要的几个:

    • --config:我们仅能创建服务配置,而不是一个实际的服务。这将创建所有服务环境和要求,但不会运行任何任务。

    • --container-label/--label:我们在这里添加这个选项是因为理解服务和容器是不同的对象很重要,我们可以为两者添加标签。默认情况下,Docker Swarm 会在每个服务容器上创建许多标签,以便将它们相互关联。我们可以轻松地使用这些标签来过滤关于我们服务容器的任何主机信息。

    • --constraint/--placement-pref:如前所述,我们可以指定哪些节点应该运行某个服务的任务。我们使用一组键值对作为约束条件来实现这一点。所有定义的键必须满足,才能在指定节点上调度该服务的任务。如果没有节点满足这些定义的约束条件,任务将无法执行,因为 Docker Swarm 的调度器找不到符合要求的节点。另一方面,placement-pref 提供了一种放置偏好设置。这不会限制哪些节点会运行任务,但我们可以通过定义的键来将服务的任务分布到不同的节点上。例如,我们可能会将某个服务的任务分布到不同的物理位置(例如数据中心)中。

    • --mode:有两种不同的服务模式(事实上,后来我们会在 Docker Swarm 网络配置 部分了解到三种,但此时只需要记住以下两种)。默认情况下,所有服务都将使用复制模式。这意味着我们将设置要保持健康的副本数(默认情况下是一个副本)。我们还有全局服务。在这种情况下,我们会创建与集群节点数量相等的副本,但每个节点上只运行一个副本。这种模式对于监控应用程序非常有趣,因为所有节点都会接收到自己的监控进程。关于这些服务,有一个重要的事情是每个加入集群的节点都会收到它自己的副本。Docker Swarm 会自动将其部署到新的节点上。

    • --with-registry-auth:这是一个非常重要的选项,因为它允许我们在集群节点之间分发凭证,从而使用私有镜像。还需要理解的是,Docker Swarm 需要外部或内部的注册表来工作。我们不再在集群节点上使用本地镜像。使用本地镜像会导致不一致的部署,因为镜像名称可能匹配,但不同节点上的内容可能完全不同。

    • --endpoint-mode:此选项设置服务如何宣布或管理它们的任务。我们可以使用 vipdnsrr 来设置。服务默认为 vip,这意味着每个服务将接收一个与其名称相关联的虚拟 IP,并且内部负载均衡器会将流量路由到与之关联的每个副本进程(容器/任务)。另一方面,dnsrr 将使用内部名称解析来关联每个副本的 IP 地址,每当我们请求服务名称时。这样,当给定服务以多个任务部署时,内部名称解析会为我们提供一个不同的 IP 地址。

    • --network:我们可以将新服务连接到现有的网络。就像我们在容器中做的那样,我们也可以使用主机网络命名空间。这里的区别在于,我们不能执行特权服务,因此我们的服务必须暴露高于 1024 的端口。

    • --publish: 我们将使用此选项来将端口公开到外部。Docker Swarm 将通过每个节点上的 Docker Swarm 路由器网格公开端口。如果外部请求到达一个没有执行任何服务任务的主机,Docker Swarm 将内部重新路由请求到适当的节点。

    • --replicas/--replicas-max-per-node: 服务是通过部署多少个副本或任务来维持其健康状态的。默认情况下,所有服务部署一个副本。正如我们稍后会看到的,我们可以随时更改副本数量。如果我们扩展或缩减副本数量,并不是所有的应用组件(进程)都会正常工作。例如,想象一个 SQL 数据库。它是一个完全有状态的组件,因为数据库进程会写入数据。如果我们增加一个新的数据库副本来访问相同的存储,数据库将会损坏。如果每个数据库副本都有自己的存储,它们将管理不同的数据。因此,并不是所有服务都可以扩展或缩减。

    • --reserve-cpu/--reserve-memory: 我们可以为服务保留所需的资源。如果没有节点提供足够的资源,该服务将不会被调度。

    • --update-delay/--update-failure-action/--update-max-failure-ratio/--update-monitor/--update-order/--update-parallelism: update选项管理服务变更的执行方式。我们将设置每次更新多少个服务任务、在每个实例更新之间等待多少秒以及如何执行更新过程。--update-order选项设置更新过程的执行顺序。默认情况下,正在运行的容器将在旧容器完全停止后被停止并创建一个新容器。使用此设置时,服务将受到影响。我们可以通过首先启动新容器来设置不同的顺序。然后,一旦一切正常,旧容器将被停止。这样,服务将不受影响,但您的应用进程必须能够适应这种情况。例如,它在标准 SQL 数据库上将无法工作,正如我们之前提到的那样。我们还将设置当某些更新失败时该如何处理,您可以选择执行自动回滚,或者暂停其余服务的更新,直到手动采取行动。

    • --rollback-delay/--rollback-failure-action/--rollback-max-failure-ratio/--rollback-monitor/--rollback-order/--rollback-parallelism: 如果更新过程出错,我们可以设置自动回滚。这些设置会修改回滚的方式。我们有与update过程相同的选项,但这次参数会涉及到rollback过程。

  • ps: 有了这个,我们可以回顾所有服务的任务及其在集群中的分布。我们还可以使用过滤器和输出格式。在章节实验部分,我们将看到一些例子。

  • logs:这是一个非常有用的操作,因为 Docker Swarm 会为我们获取所有任务的日志。我们可以从管理节点的命令行查看它们,而不是去任务运行的地方读取容器的日志。

  • update:服务属性可以进行更新。例如,我们可以更改镜像的发布版本,发布新端口,改变副本数量等。

  • rollback:通过这个操作,我们可以恢复服务的先前属性。需要理解的是,之前执行过的镜像应该保留在我们的主机上,以便支持应用程序的回滚。

  • inspect/ls/rm:这些是我们在所有其他类型的对象中都会遇到的常见操作。我们已经学习了如何使用它们。

需要注意的是,服务中不允许使用特权容器。因此,如果我们想要使用主机网络命名空间,容器进程应该暴露并使用非特权端口(大于 1024)。

Docker 服务约束可以通过自定义标签进行设置,但还有一些是默认创建的内部标签,它们非常有用:

标签 属性
node.id 节点 ID
node.hostname 节点主机名;例如,node.hostname==antares
node.role 节点的 Swarm 角色;例如,node.role!=manager
node.labels Swarm 节点分配的标签;例如,node.labels.environment==production
engine.labels Docker 引擎定义的标签;例如,engine.labels.operatingsystem==ubuntu 18.04

我们可以使用变量来定义服务属性。在以下示例中,我们在容器的主机名中使用了内部的 Docker Swarm 变量:

$ docker service create --name "top" --hostname="{{.Service.Name}}-{{.Task.ID}}" busybox top

总结一下,在继续处理其他 Swarm 资源之前:服务是一组任务,每个任务执行一个容器。所有这些容器一起运行以保持服务的状态。Docker Swarm 会监控服务的状态,如果一个容器宕机,它会运行一个新容器以保持实例的数量。需要注意的是,容器的 ID 和名称会发生变化。然而,尽管可以创建新的任务,任务的名称不会改变。

在继续下一个主题之前,让我们看一个快速的示例。我们将使用 docker service create 创建一个简单的 NGINX Web 服务器服务:

$ docker service create --name webserver --publish 80 nginx:alpine
lkcig20f3wpfcbfpe68s72fas
overall progress: 1 out of 1 tasks 
1/1: running [==================================================>] 
verify: Service converged 

我们可以使用 docker service ps 查看创建的任务运行在哪个节点:

$ docker service ps webserver
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS 
lb1akyp4dbvc webserver.1 nginx:alpine sirius Running Running about a minute ago 

然后,我们移动到任务所在的节点。一旦到达,我们使用 docker container kill 杀死相关容器:

$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6aeaee25ff9b nginx:alpine "nginx -g 'daemon of…" 6 minutes ago Up 6 minutes 80/tcp webserver.1.lb1akyp4dbvcqcfznezlhr4zk

$ docker container kill 6aeaee25ff9b              
6aeaee25ff9b

几秒钟后,一个新的任务将自动创建并伴随一个新的容器。任务名称没有改变,但它是一个新任务,从其 ID 我们可以看出这一点:

$ docker service ps webserver
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
lnabvvg6k2ne webserver.1 nginx:alpine sirius Running Running less than a second ago 
lb1akyp4dbvc \_ webserver.1 nginx:alpine sirius Shutdown Failed 7 seconds ago "task: non-zero exit (137)"

最后,我们可以查看一些由 Swarm 创建的标签,以便通过它们来完整识别使用服务的容器。我们使用 docker container inspect 来查看:

$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1d9dc2407f74 nginx:alpine "nginx -g 'daemon of…" 13 minutes ago Up 13 minutes 80/tcp webserver.1.lnabvvg6k2ne6boqv3hvqvth8

$ docker container inspect 1d9dc2407f74 --format "{{.Config.Labels}}"
map[com.docker.swarm.node.id:ev4ocuzk61lj0375z80mkba5f com.docker.swarm.service.id:lkcig20f3wpfcbfpe68s72fas com.docker.swarm.service.name:webserver com.docker.swarm.task: com.docker.swarm.task.id:lnabvvg6k2ne6boqv3hvqvth8 com.docker.swarm.task.name:webserver.1.lnabvvg6k2ne6boqv3hvqvth8 maintainer:NGINX Docker Maintainers <docker-maint@nginx.com>]

有一些服务选项可以通过字符串设置,帮助我们识别其配置和其他相关资源。当我们需要隔离特定服务任务的资源或使用一些特殊信息访问其他服务时,这是非常重要的,例如容器的主机名。我们可以使用标签为容器添加元信息,但也有一些 Docker Swarm 定义的变量,我们可以在字符串中使用它们。这些变量使用 Go 的模板语法(正如我们在格式化列出命令输出时学到的),并可以与 docker service create 以及 --hostname--mount--env 参数一起使用。

因此,我们可以使用这些变量将关联服务容器的主机名设置为任务间唯一;例如,--hostname="{{.Service.Name}}-{{.Task.ID}}"。我们甚至可以使用节点的名称,通过--hostname="{{.Node.Hostname}}"来标识任务所属的节点。这在全局服务中非常有用。

以下是有效的服务模板替换的快速列表:

  • 服务.Service.ID.Service.Name.Service.Labels

  • 节点.Node.ID.Node.Hostname

  • 任务.Task.ID.Task.Name.Task.Slot

在接下来的章节中,我们将介绍一些新的 Docker Swarm 对象,它们将帮助我们在集群中部署应用程序。

使用 Stacks 和其他 Docker Swarm 资源部署应用程序

在本节中,我们将学习其他 Docker Swarm 对象,它们将帮助我们在集群内完全部署应用程序。

我们已经学习了如何使用环境变量配置应用程序。这不推荐在生产环境中使用,因为任何具有系统 Docker 访问权限的人都可以读取它们的值。为了避免这种情况,我们将使用外部数据源。我们还学会了如何将主机资源集成到容器中。我们可以将配置和密码设置在主机和容器之间共享的文件中。这在独立环境中可以正常工作,但对于分布式工作负载(容器可能在不同主机上运行)来说则不行。我们将需要在所有集群节点上同步这些文件。

为了避免在多个节点之间同步文件,Docker Swarm 提供了两种不同的对象来管理它们。我们可以拥有私有文件、密钥和配置。两者都将其值存储在 Swarm 键值存储中。存储的值将对每个需要它们的集群节点可用。这些对象是相似的,但密钥用于密码、证书等,而配置对象则用于应用程序配置文件。现在,让我们深入了解它们。

密钥

密钥是包含密码、证书以及任何其他不应在网络上传输的信息的数据块。它们将以加密方式存储,以防止被窥探。Docker Swarm 会为我们管理和存储密钥。由于这些数据存储在键值存储中,只有管理节点才能访问我们创建的任何密钥。当容器需要使用存储的密钥时,负责运行该容器的主机(服务任务容器)也将能够访问该密钥。容器将接收到一个临时的文件系统(在 Linux 主机上为内存中的 tmpfs),其中包含该密钥。当容器停止时,该密钥将无法在主机上访问。密钥仅在容器运行时需要时才会可用。

由于密钥是 Docker Swarm 对象,因此我们可以使用所有常规操作(listcreateremoveinspect 等)。不要指望通过 inspect 操作读取密钥数据。一旦创建,无法读取或更改密钥的内容。我们可以通过文件或使用标准输入来创建密钥。我们还可以添加标签,以便在大型集群环境中轻松列出密钥。

一旦创建了密钥,我们可以在服务中使用它。我们有短格式和长格式两种表示方式。默认情况下,使用短格式时,一个包含密钥数据的文件将会在 /run/secrets/<SECRET_NAME> 下创建。该文件将在 Linux 上的 tmpfs 文件系统中挂载。Windows 不支持内存文件系统,因此不同于 Linux。我们可以使用长格式指定密钥文件在 /run/secrets 下的文件名,以及其所有权和文件权限。这样可以帮助我们避免在容器内使用 root 权限来访问该文件。让我们通过 docker secret create 创建一个密钥,并在服务中使用它:

$ echo this_is_a_super_secret_password|docker secret create app-key -
o9sh44stjm3kxau4c5651ujvr

$ docker service create --name database \
 --secret source=ssh-key,target=ssh \
 --secret source=app-key,target=app,uid=1000,gid=1001,mode=0400 \
 redis:3.0

正如我们之前提到的,无法检索密钥数据。我们可以使用常见的 docker secret inspect 操作来检查之前创建的密钥:

$ docker secret inspect app-key
[
 {
 "ID": "o9sh44stjm3kxau4c5651ujvr",
 "Version": {
 "Index": 12
 },
 "CreatedAt": "2019-12-30T20:42:59.050992148Z",
 "UpdatedAt": "2019-12-30T20:42:59.050992148Z",
 "Spec": {
 "Name": "app-key",
 "Labels": {}
 }
 }
]

在接下来的章节中,我们将学习配置对象。

配置

配置对象与密钥类似,但它们不会在 Docker Swarm Raft 日志中加密,并且不会在容器中的 tmpfs 文件系统上挂载。配置可以在服务任务运行时添加或删除。事实上,我们甚至可以更新服务配置。我们将使用这些对象来存储应用程序的配置。它们可以包含字符串或二进制文件(最多 500 KB,这对配置来说足够了)。

当我们创建一个配置对象时,Docker Swarm 会将其存储在加密的 Raft 日志中,并通过相互 TLS 协议将其复制到其他管理节点。因此,所有管理节点将拥有新的配置对象值。

在服务中使用配置文件需要在容器内有一个挂载路径。默认情况下,挂载的配置文件将是世界可读的,并且由运行容器的用户拥有,但如果需要,我们可以调整这两个属性。

让我们看一个简单的例子。我们将使用docker config create创建一个配置文件,然后在服务中使用它:

$ echo "This is a sample configuration" | docker config create sample-config -
d0nqny24g5y1tiogwggxmesox

$ docker service create \
 --name sample-service \
 --config source=sample-config,target=/etc/sample.cfg,mode=0440 \
 nginx:alpine

在这种情况下,我们可以查看配置内容并确认其可读性。使用docker config inspect,我们得到以下输出:

$ docker config inspect sample-config --pretty
ID: d0nqny24g5y1tiogwggxmesox
Name: sample-config
Created at: 2019-12-10 21:07:51.350109588 +0000 utc
Updated at: 2019-12-10 21:07:51.350109588 +0000 utc
Data:
This is a sample configuration

让我们继续讨论堆栈。

堆栈

堆栈帮助我们部署完整的应用程序。它们是基础设施即代码IaC)文件,包含所有组件定义、它们的交互方式以及部署应用所需的外部资源。我们将使用docker-compose文件定义(docker-compose.yaml)。并非所有docker-compose文件中的基本键都会可用。例如,depends_on在堆栈中不可用,因为它们没有依赖声明。这是需要在自己的应用逻辑中管理的。

正如我们在第五章《部署多容器应用程序》中所学到的,部署多容器应用程序,每个已部署的应用程序默认会在自己的网络中运行。当在 Docker Swarm 上使用堆栈时,应用组件会在整个集群中部署。将使用覆盖网络,因为每个组件应该能够相互访问,无论它们运行在哪里。堆栈也会默认在自己的网络中部署。

堆栈通过服务来部署应用程序。因此,我们将把服务定义保存在docker-compose文件中。为了能够将这些服务与其他堆栈区分开来,我们将设置堆栈的名称。

重要的是要理解,docker-compose将在一个 Docker 引擎上部署多容器应用程序,而docker stack将在 Swarm 集群上部署多服务应用程序。请注意,尽管如此,它们都使用相同类型的 IaC 文件。

让我们快速浏览一下docker stack命令行:

  • deploy:部署堆栈需要docker-compose文件版本为 3.0 及以上。我们将使用deploy操作一次性创建并运行所有应用组件。也可以使用 Docker 应用包文件,这是本书不涉及的内容,但值得知道的是,我们有多个选项可以通过 Docker 堆栈在 Docker Swarm 上部署应用程序。如前所述,我们需要为堆栈的部署命名,以便在集群中完全识别其所有组件。堆栈的所有资源将以堆栈的名称作为前缀,除非它们是从堆栈的文件定义外部创建的。在这种情况下,它们将保留原始名称。

    这些是docker stack deploy的主要选项:

    • --compose-file/-c:我们使用docker-compose.yaml作为堆栈定义文件,除非我们使用此选项指定自定义文件名。

    • --orchestrator:此选项是最近添加的,它允许我们选择哪个编排工具来部署和管理堆栈。在我们的环境中,当 Docker Swarm 和 Kubernetes 都可用时,我们可以选择其中之一。

    • --with-registry-auth:正如我们在使用服务时所学到的,使用私有注册表时共享身份验证至关重要。没有这个选项,我们无法确保所有节点使用相同的镜像,或者它们能访问注册表,因为这将依赖于本地存储的身份验证。

  • servicesservices选项显示已部署堆栈的服务列表。与所有其他列出操作一样,我们可以格式化并过滤其输出。

  • ps:此操作列出所有服务及其任务部署的位置。它的输出可以轻松进行过滤和格式化,正如我们将在本章实验部分看到的那样。

  • ls/rm:这些是常见的对象操作,用于列出和删除对象。

关于堆栈没什么更多要说的了。基础设施即代码(IaC)要求每个部署都是可重现的。即使是一个简单的独立服务,也请确保使用堆栈文件来部署它。本章实验部分将通过更多示例来讲解这些操作和选项。在下一节中,我们将学习 Swarm 如何在集群范围内更改应用程序的网络。

Docker Swarm 中的网络

当我们谈论 Docker Swarm 时,我们需要引入一个关于网络的新概念:覆盖网络。正如我们在本章开头提到的,由于 Docker Swarm 将把所有应用组件分布到多个节点上,它们必须无论运行在哪个位置都能互相访问。因此,将提供一个新的网络驱动程序,覆盖网络将在使用用户数据报协议UDP)的 VXLAN 隧道上工作。我们将能够加密这种通信,但通常会有一些额外的开销。

覆盖网络驱动程序将跨集群节点创建一个分布式网络,并自动提供数据包路由以互连分布式容器。

当 Swarm 首次初始化时,创建了两个网络:

  • docker_gwbridge:此桥接网络将连接集群中所有的 Docker 守护进程。

  • ingress:这是一个覆盖网络,将管理 Docker Swarm 服务的控制和数据流量。所有服务将连接到该网络,以便它们可以相互访问,如果我们没有指定任何自定义的覆盖网络。

Docker Swarm 只会管理覆盖网络。我们可以为应用程序创建新的覆盖网络,并且这些网络彼此隔离。当我们在本地使用自定义桥接网络时也会发生类似的情况。我们将能够将服务连接到多个网络,就像我们在桥接环境中所做的那样。我们还可以将容器连接到覆盖网络,尽管这不是常见的做法。请记住,我们不会在 Docker Swarm 中运行独立容器。

如果您的环境中启用了防火墙,您需要允许以下流量:

端口或端口范围 协议 用途
2377 TCP 集群管理流量
7946 TCP/UDP Swarm 节点之间的相互通信
4789 UDP 覆盖网络

Docker Swarm 的管理流量默认始终是加密的,正如我们在前面的章节中所了解的。我们还可以加密覆盖网络。当我们在创建覆盖网络时使用加密参数时,Docker Swarm 会在覆盖 VXLAN 上创建 互联网协议安全 (IPSEC) 加密。虽然这增加了安全性,但也会带来性能开销。您需要在应用程序中管理安全性和性能之间的平衡。由于加密发生在网络创建时,因此一旦网络创建完成,就无法更改。

创建覆盖网络很简单——我们只需通过 docker network create 指定覆盖驱动程序:

$ docker network create -d overlay testnet 
1ff11sixrjj7cqppgoxhrdu3z

默认情况下,它是以未加密且不可附加的方式创建的。这意味着容器将无法连接到此网络,只有服务才能连接。我们可以通过尝试使用 docker container run 将一个简单的容器附加到已创建的网络来验证这一点:

$ docker container run -ti --network testnet alpine 
Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
Digest: sha256:2171658620155679240babee0a7714f6509fae66898db422ad803b951257db78
Status: Downloaded newer image for alpine:latest
docker: Error response from daemon: Could not attach to network testnet: rpc error: code = PermissionDenied desc = network testnet not manually attachable.

为了避免这种情况,我们需要从一开始就将网络声明为可附加。这第二个示例还使用 docker network create --attachable --opt encrypted 添加了加密选项:

$ docker network create -d overlay testnet2 --attachable --opt encrypted
9blpskhcvahonytkifn31w91d

$ docker container run -ti --network testnet2 alpine 
/ # 

我们成功连接到了新创建的加密网络,因为它是通过具有attachable属性的方式创建的。

所有连接到同一覆盖网络的服务将通过它们的名称彼此识别,并且所有暴露的端口将在内部可用,无论它们是否已发布。

默认情况下,所有 Swarm 覆盖网络都会有 24 位掩码,这意味着我们可以分配 255 个 IP 地址。每个部署的服务可能会消耗多个 IP 地址,同时每个节点与给定覆盖网络上的节点进行对等连接时也会占用一个 IP 地址。在某些情况下,可能会遇到 IP 地址耗尽的问题。为避免这种情况,如果有很多服务需要使用网络,可以考虑创建更大的网络。

在接下来的章节中,我们将详细了解服务发现以及 Docker 如何将流量路由到所有服务副本。

服务发现与负载均衡

Docker Swarm 具有内部的 互联网协议地址管理 (IPAM) 和 域名系统 (DNS) 组件,用于自动为每个创建的服务分配虚拟 IP 地址和 DNS 条目。内部负载均衡将基于服务的 DNS 名称将请求分发到服务的任务。正如我们之前提到的,所有在同一网络上的服务都会相互识别,并且能够通过它们的暴露端口进行访问。

Docker Swarm 管理器(实际上是领导者)将使用创建的入口覆盖网络来发布我们声明为可从集群外部访问的服务。如果在服务创建时没有声明端口,Docker Swarm 会自动为每个暴露的端口分配一个端口,端口范围在 30000-32767 之间。我们必须手动声明所有大于 1024 的端口,因为我们不能创建特权服务。

所有节点都将参与此入口路由器网格。因此,无论节点是否运行请求的任务,这些节点都会接受已发布端口上的连接。路由器网格将所有传入的请求路由到所有节点上已发布端口的正在运行的任务(容器)。因此,已发布的端口将在所有 Swarm 节点上分配,因此只有一个服务能够使用已声明的端口。换句话说,如果我们在端口8080上发布一个服务,我们将无法将该端口重新用于另一个服务。这将限制集群中可以运行的最大服务数量,受限于所使用的 Linux 或 Windows 系统中可用的端口数量。我们了解到,Docker Engine 无法通过 NAT 在同一端口上发布多个容器。在这种情况下,所有节点将固定端口以供已发布的服务使用。

路由器网格监听所有节点的可用 IP 地址上的已发布端口。我们将使用集群外部的负载均衡器将流量路由到集群的主机。我们通常使用它们中的几个进行发布,并且负载均衡器会将所有请求转发到这些主机。

我们可以使用docker service update修改或删除已声明的端口,或添加新的端口。

下图展示了路由器网格在一个三节点集群中如何工作,该集群发布了一个带有两个副本的服务。颜色服务运行两个任务。因此,一个容器分别运行在 NODE1 和 NODE2 上(这些是 Docker Swarm 调度的任务,如下图所示)。在内部,这些容器通过端口3000暴露它们的应用程序。定义该容器端口为3000的服务将会发布在主机的端口上,也就是8080。即使某些节点未运行任何服务任务,该端口也会在所有节点上发布。内部负载均衡将使用入口覆盖网络将请求路由到合适的容器。最终,用户将通过外部负载均衡器访问发布的服务。这不是 Docker Swarm 环境的一部分,但它有助于我们提供高可用性,将请求转发到一组可用的节点:

我们将提供短格式和长格式用于发布服务。长格式通常提供更多选项。在以下示例中,我们将在集群端口8080上发布一个 NGINX 进程,并通过docker service create --publish将其流量转发到容器的端口80

$ docker service create --name webserver \
 --publish published=8080,target=80,protocol=tcp \
nginx:alpine

在任何节点上,我们都能通过端口8080访问 NGINX 服务。我们可以使用curl命令进行测试:

$ curl -I 0.0.0.0:8080
HTTP/1.1 200 OK
Server: nginx/1.17.6
Date: Tue, 31 Dec 2019 17:51:26 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 19 Nov 2019 15:14:41 GMT
Connection: keep-alive
ETag: "5dd406e1-264"
Accept-Ranges: bytes

我们可以通过查询 DNS 中的tasks.<SERVICE_NAME>来获取当前服务任务的 IP 地址。

默认情况下,所有服务都使用路由器网格。然而,我们可以避免这种默认行为,正如在接下来的部分中所看到的那样。

绕过路由器网格

使用主机模式或循环 DNSRRDNS)端点,我们可以绕过路由器网格。这将使我们能够在指定的节点上通过定义的端口访问实例,或应用我们自己的负载均衡器。在某些情况下,我们需要包括特殊的负载均衡特性,如权重或用户会话的持久性。Docker Swarm 的默认路由器网格行为将请求路由到所有可用服务的后端实例。识别应用程序的需求非常重要,以决定是否应该使用 Docker Swarm 的默认负载均衡来部署其组件。

Docker 的内部负载均衡器只会执行 L3 路由。它不会提供基于权重的路由或特殊功能。

使用主机模式

使用主机模式,只有运行实例的节点才会接收流量。我们可以为节点打标签,使它们只调度一些任务,并从负载均衡器接收流量。在这种情况下,我们不能为该服务运行比定义的标签节点数更多的副本。

在以下示例中,我们将在集群中的每个节点上运行一个 NGINX 进程,因为我们定义了一个全局服务。我们将使用docker service create --mode global --publish mode=host

$ docker service create --name webserver \
 --publish published=8080,target=80,protocol=tcp,mode=host \
 --mode global \
nginx:alpine

服务的定义端口将在集群中的所有节点上可用。

使用循环 DNS 模式

我们还可以使用 RRDNS 模式来避免服务的虚拟 IP 地址。在这种情况下,Docker Swarm 不会为服务分配虚拟 IP,而是为该服务创建一个 DNS 条目,包含其所有副本的 IP 地址。当我们希望在 Docker Swarm 集群内使用自己的负载均衡器,将其作为另一个服务部署时,这很有用。在负载均衡器服务中维护副本的 IP 地址并不容易。我们可能会在负载均衡器的配置中使用 DNS 解析,查询 DNS 以获取所有实例的 IP 地址。

下一节将通过一些实验帮助我们理解本章所学的概念。

本章实验

现在,我们将完成本章的实验,以帮助我们加深对所学概念的理解。如果你还没有部署本书 GitHub 仓库中的environments/swarm-environment,请部署它(github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git)。你也可以使用自己的 Linux 服务器。从environments/swarm文件夹中使用vagrant up启动虚拟环境。

等待所有节点运行完毕。我们可以使用vagrant status检查节点的状态。使用vagrant ssh swarm-node1连接到实验节点。Vagrant 已为你部署了四个节点。你将使用vagrant用户,并通过sudo获取 root 权限。你应该能看到以下输出:

Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$ vagrant up
--------------------------------------------------------------------------------------------
 Docker SWARM MODE Vagrant Environment
 Engine Version: current
 Experimental Features Enabled
--------------------------------------------------------------------------------------------
Bringing machine 'swarm-node1' up with 'virtualbox' provider...
Bringing machine 'swarm-node2' up with 'virtualbox' provider...
Bringing machine 'swarm-node3' up with 'virtualbox' provider...
Bringing machine 'swarm-node4' up with 'virtualbox' provider... 
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$

节点将有三个接口(IP 地址和虚拟硬件资源可以通过更改config.yml文件进行修改):

  • eth0 [10.0.2.15]:内部接口,Vagrant 所需。

  • eth1 [10.10.10.X/24]:用于 Docker Swarm 内部通信。第一个节点将获得 IP 地址10.10.10.11,依此类推。

  • eth2 [192.168.56.X/24]:这是一个仅主机接口,用于主机与虚拟节点之间的通信。第一个节点将获得 IP 地址192.168.56.11,依此类推。

我们将使用eth1接口进行 Docker Swarm 通信,并且可以通过192.168.56.X/24的 IP 地址范围连接到已发布的应用程序。所有节点都安装了 Docker Engine Community Edition,并且vagrant用户被允许执行docker命令。

现在,我们可以使用vagrant ssh swarm-node1连接到第一个已部署的虚拟节点。如果你之前已经部署过 Docker Swarm 虚拟环境,并通过vagrant up启动了它,这个过程可能会有所不同:

Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$ vagrant ssh swarm-node1
vagrant@swarm-node1:~$

现在,你已经准备好开始实验了。让我们先从创建 Docker Swarm 集群开始。

创建 Docker Swarm 集群

一旦 Vagrant(或你自己的环境)部署完成,我们将有四个节点(命名为node<index>,从14),每个节点都安装了 Ubuntu Xenial 和 Docker Engine。

首先,检查实验节点的 IP 地址(如果你使用了 Vagrant,地址范围为10.10.10.1110.10.10.14,因为第一个接口是 Vagrant 的内部主机到节点接口)。当你熟悉了环境的 IP 地址后,我们可以在node1上启动集群,例如。

如果你使用的是 Linux 作为 VirtualBox 主机,可以在终端执行alias vssh='vagrant ssh',使用vssh代替vagrant ssh连接节点,这样会更加符合非 Vagrant 环境的使用习惯。

现在,我们已经准备好开始实验环境,并且四个节点和 Docker Engine 已经安装好,让我们开始吧:

  1. 连接到node1并使用docker swarm init初始化一个新集群:
Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$ vagrant ssh swarm-node1
--------------------------------------------------------------------------------------------
 Docker SWARM MODE Vagrant Environment
 Engine Version: current
 Experimental Features Enabled
--------------------------------------------------------------------------------------------
...
...

vagrant@swarm-node1:~$ docker swarm init
Error response from daemon: could not choose an IP address to advertise since this system has multiple addresses on different interfaces (10.0.2.15 on eth0 and 10.10.10.11 on eth1) - specify one with --advertise-addr

如果你使用 Vagrant,这种情况是正常的,因为节点至少会有两个接口。第一个接口是 Vagrant 内部的主机到节点的接口,另一个接口是为实验室准备的。在这种情况下,我们需要使用--advertise-addr指定集群使用哪个接口。我们将执行docker swarm init --advertise-addr

vagrant@swarm-node1:~$ docker swarm init --advertise-addr 10.10.10.11
Swarm initialized: current node (b1t5o5x8mqbz77e9v4ihd7cec) is now a manager.

To add a worker to this swarm, run the following command:

 docker swarm join --token SWMTKN-1-3xfi4qggreh81lbr98d63x7299gtz1fanwfjkselg9ok5wroje-didcmb39w7apwokrah6xx4cus 10.10.10.11:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

现在,Swarm 已经正确初始化。

  1. 添加第二个节点,它将连接到node2并执行初始化输出中描述的命令。我们将使用获取的令牌,通过docker swarm join命令将其加入集群:
Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$ vagrant ssh swarm-node2

vagrant@swarm-node2:~$ docker swarm join --token SWMTKN-1-3xfi4qggreh81lbr98d63x7299gtz1fanwfjkselg9ok5wroje-didcmb39w7apwokrah6xx4cus 10.10.10.11:2377
This node joined a swarm as a worker.

这样,一个节点就作为工作节点被添加了。

  1. node1上,通过使用docker node ls验证新节点是否已被添加:
Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$ vagrant ssh swarm-node1

vagrant@swarm-node1:~$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
b1t5o5x8mqbz77e9v4ihd7cec * swarm-node1 Ready Active Leader 19.03.5
rj3rgb9egnb256cms0zt8pqew swarm-node2 Ready Active 19.03.5

注意,swarm-node1是领导者,因为这是初始化集群的节点。我们不能在swarm-node2上执行docker node ls,因为它不是管理节点。

  1. 我们将在swarm-node3上执行相同的加入过程,再次使用docker swarm join
vagrant@swarm-node1:~$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
b1t5o5x8mqbz77e9v4ihd7cec * swarm-node1 Ready Active Leader 19.03.5
rj3rgb9egnb256cms0zt8pqew swarm-node2 Ready Active 19.03.5
ui67xyztnw8kn6fjjezjdtwxd swarm-node3 Ready Active 19.03.5
  1. 接下来,我们将查看管理节点的令牌,以便将下一个节点作为管理节点添加。我们将使用docker swarm join-token manager
vagrant@swarm-node1:~$ docker swarm join-token manager
To add a manager to this swarm, run the following command:

 docker swarm join --token SWMTKN-1-3xfi4qggreh81lbr98d63x7299gtz1fanwfjkselg9ok5wroje-aidvtmglkdyvvqurnivcsmyzm 10.10.10.11:2377

现在,我们连接到 swarm-node4,并使用新的令牌执行所示的加入命令(docker swarm join):

vagrant@swarm-node4:~$ docker swarm join --token SWMTKN-1-3xfi4qggreh81lbr98d63x7299gtz1fanwfjkselg9ok5wroje-aidvtmglkdyvvqurnivcsmyzm 10.10.10.11:2377
This node joined a swarm as a manager
  1. 目前集群有四个节点:两个管理节点和两个工作节点。如果领导节点失败,这将无法提供高可用性。让我们将 swarm-node2 也提升为管理节点,例如,通过执行 docker node update --role manager
vagrant@swarm-node4:~$ docker node update --role manager swarm-node2
swarm-node2

我们也可以使用 promotedemote 命令来更改节点角色,但了解它们对节点属性更新的实际含义会更方便。此外,请注意,我们可以随时更改节点角色,但应保持健康的管理节点数量。

我们可以再次查看节点的状态。管理节点显示为 ReachableLeader,表示该节点是集群的领导者。使用 docker node ls,我们得到如下输出:

vagrant@swarm-node4:~$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
b1t5o5x8mqbz77e9v4ihd7cec swarm-node1 Ready Active Leader 19.03.5
rj3rgb9egnb256cms0zt8pqew swarm-node2 Ready Active Reachable 19.03.5
ui67xyztnw8kn6fjjezjdtwxd swarm-node3 Ready Active 19.03.5
jw9uvjcsyg05u1slm4wu0hz6l * swarm-node4 Ready Active Reachable 19.03.5

请注意,我们在 node4 上执行了这些命令。我们之所以能这样做,是因为它是一个管理节点(不是领导者,但仍是管理节点)。我们可以使用任何管理节点来管理集群,但只有领导节点会执行内部数据库的更新。

  1. 我们将只保留一个管理节点用于接下来的实验,但首先,我们将停止 node1 的 Docker 引擎守护进程,看看集群中会发生什么。我们将使用 systemctl stop docker 来停止 Docker 守护进程:
Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$ vagrant ssh swarm-node1

vagrant@swarm-node1:~$ sudo systemctl stop docker

连接到另一个管理节点(例如 node2,即最近被提升的节点)。现在,让我们使用 docker node ls 来查看该节点的状态:

Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$ vagrant ssh swarm-node2

vagrant@swarm-node2$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
b1t5o5x8mqbz77e9v4ihd7cec swarm-node1 Down Active Unreachable 19.03.5
rj3rgb9egnb256cms0zt8pqew * swarm-node2 Ready Active Reachable 19.03.5
ui67xyztnw8kn6fjjezjdtwxd swarm-node3 Ready Active 19.03.5
jw9uvjcsyg05u1slm4wu0hz6l swarm-node4 Ready Active Leader 19.03.5

从其他正在运行的管理节点中选举出了一个新的领导者。现在,我们可以重新启动 node1 的 Docker 引擎守护进程,使用 systemctl start docker

Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$ vagrant ssh swarm-node1

vagrant@swarm-node1$ sudo systemctl start docker

vagrant@swarm-node1$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
b1t5o5x8mqbz77e9v4ihd7cec * swarm-node1 Ready Active Reachable 19.03.5
rj3rgb9egnb256cms0zt8pqew swarm-node2 Ready Active Reachable 19.03.5
ui67xyztnw8kn6fjjezjdtwxd swarm-node3 Ready Active 19.03.5
jw9uvjcsyg05u1slm4wu0hz6l swarm-node4 Ready Active Leader 19.03.5

该节点仍然是管理节点,但由于在故障时选举出了新的领导者,它不再是集群的领导者。

  1. 让我们使用 docker node update --role worker 将所有非领导节点降级为工作节点,供接下来的实验使用:
vagrant@swarm-node1$ docker node update --role worker swarm-node2
swarm-node2

vagrant@swarm-node1:~$ docker node update --role worker swarm-node1
swarm-node1

vagrant@swarm-node1:~$ docker node ls
Error response from daemon: This node is not a swarm manager. Worker nodes can't be used to view or modify cluster state. Please run this command on a manager node or promote the current node to a manager.

请注意再次列出时的错误。node1 现在不是管理节点,因此我们无法再从该节点管理集群。所有管理命令将在接下来的实验中从 node4 运行。node4 是唯一的管理节点,因此它成为了集群的领导者,正如我们通过 docker node ls 再次观察到的那样:

Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$ vagrant ssh swarm-node4

vagrant@swarm-node4:~$ docker node ls
ID                            HOSTNAME      STATUS    AVAILABILITY    MANAGER STATUS    ENGINE VERSION
b1t5o5x8mqbz77e9v4ihd7cec     swarm-node1    Ready    Active                            19.03.5
rj3rgb9egnb256cms0zt8pqew     swarm-node2    Ready    Active                            19.03.5
ui67xyztnw8kn6fjjezjdtwxd     swarm-node3    Ready    Active                            19.03.5
jw9uvjcsyg05u1slm4wu0hz6l *   swarm-node4    Ready    Active        Leader              19.03.5

在下一个实验中,我们将部署一个简单的 Web 服务器服务。

部署一个简单的复制服务

swarm-node4 上,我们将创建一个复制服务(默认情况下),并测试如何将更多副本分布到不同的节点。让我们开始吧:

  1. 使用简单的 nginx:alpine 镜像,通过执行 docker service create 来部署 webserver 服务:
vagrant@swarm-node4:~$ docker service create --name webserver nginx:alpine
kh906v3xg1ni98xk466kk48p4
overall progress: 1 out of 1 tasks 
1/1: running [==================================================>] 
verify: Service converged 

请注意,我们需要等几秒钟,直到所有实例正确运行。如果镜像配置了健康检查,所需时间可能会有所不同。

我们可以在服务创建时或通过更新配置使用 --health-cmd 和其他相关参数来覆盖镜像定义的健康检查。实际上,我们可以更改几乎所有使用过的镜像,就像我们处理容器一样。

  1. 部署完成后,我们可以使用 docker service ps 来查看副本启动的位置:
vagrant@swarm-node4:~$ docker service ps webserver
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
wb4knzpud1z5        webserver.1         nginx:alpine        swarm-node3               Running             Running 14 seconds ago                                         

在这种情况下,nginx已部署在swarm-node3上。这在你的环境中可能会有所不同。

  1. 我们可以将副本数量扩展到3,并查看它们的分布情况。我们将使用docker service update --replicas命令:
$ docker service update --replicas 3 webserver
webserver
overall progress: 3 out of 3 tasks 
1/3: running [==================================================>] 
2/3: running [==================================================>] 
3/3: running [==================================================>] 
verify: Service converged 

如果我们回顾副本的分布情况,可以通过docker service ps webserver命令发现容器的运行位置:

vagrant@swarm-node4:~$ docker service ps webserver
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
wb4knzpud1z5 webserver.1 nginx:alpine swarm-node3 Running Running 2 minutes ago 
ie9br2pblxu6 webserver.2 nginx:alpine swarm-node4 Running Running 50 seconds ago 
9d021pmvnnrq webserver.3 nginx:alpine swarm-node1 Running Running 50 seconds ago 

请注意,在这种情况下,swarm-node2没有收到副本,但我们可以强制副本在该节点上运行。

  1. 要强制特定位置,我们可以为特定节点添加标签并添加约束。我们将使用docker node update --label-add命令添加标签:
vagrant@swarm-node4:~$ docker node update --label-add tier=front swarm-node2
swarm-node2

现在,我们可以修改当前的服务,使其仅在标记为tier==front的特定节点上运行。我们将使用docker service update --constraint-add node.labels.tier命令,然后再次使用docker service ps来查看其分布的任务:

vagrant@swarm-node4:~$ docker service update --constraint-add node.labels.tier==front webserver
webserver
overall progress: 3 out of 3 tasks 
1/3: running   [==================================================>] 
2/3: running   [==================================================>] 
3/3: running   [==================================================>] 
verify: Service converged 

vagrant@swarm-node4:~$ docker service ps webserver
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE             ERROR               PORTS
wjgkgkn0ullj        webserver.1         nginx:alpine        swarm-node2               Running             Running 24 seconds ago 
wb4knzpud1z5         \_ webserver.1     nginx:alpine        swarm-node3               Shutdown            Shutdown 25 seconds ago 
bz2b4dw1emvw        webserver.2         nginx:alpine        swarm-node2               Running             Running 26 seconds ago 
ie9br2pblxu6         \_ webserver.2     nginx:alpine        swarm-node4               Shutdown            Shutdown 27 seconds ago 
gwzvykixd5oy        webserver.3         nginx:alpine        swarm-node2               Running             Running 28 seconds ago 
9d021pmvnnrq         \_ webserver.3     nginx:alpine        swarm-node1               Shutdown            Shutdown 29 seconds ago  

现在,所有副本都运行在swarm-node2上。

  1. 现在,我们将对node2执行一些维护任务。在这种情况下,我们将在排空swarm-node2之前,先移除service约束。如果不这样做,其他节点将无法接收工作负载,因为它们被限制为tier=front节点标签。我们通过docker service update --constraint-rm node.labels.tier命令移除了服务的约束:
vagrant@swarm-node4:~$ docker service update --constraint-rm node.labels.tier==front webserver
webserver
overall progress: 3 out of 3 tasks 
1/3: running   [==================================================>] 
2/3: running   [==================================================>] 
3/3: running   [==================================================>] 
verify: Service converged 

vagrant@swarm-node4:~$ docker service ps webserver
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
wjgkgkn0ullj        webserver.1         nginx:alpine        swarm-node2               Running             Running 4 minutes ago 
wb4knzpud1z5         \_ webserver.1     nginx:alpine        swarm-node3               Shutdown            Shutdown 4 minutes ago 
bz2b4dw1emvw        webserver.2         nginx:alpine        swarm-node2               Running             Running 4 minutes ago 
ie9br2pblxu6         \_ webserver.2     nginx:alpine        swarm-node4               Shutdown            Shutdown 4 minutes ago 
gwzvykixd5oy        webserver.3         nginx:alpine        swarm-node2               Running             Running 4 minutes ago 
9d021pmvnnrq         \_ webserver.3     nginx:alpine        swarm-node1               Shutdown            Shutdown 4 minutes ago                       

任务没有迁移到其他节点,因为这些任务已经满足了服务约束(在新的情况下没有约束)。

Docker Swarm 永远不会迁移任务,除非真的有必要,因为它总是尽量避免任何服务中断。我们可以通过docker service update --force <SERVICE_NAME>强制更新服务任务的重新分配。

  1. 在这一步,我们将暂停swarm-node3并排空swarm-node2。我们分别使用docker node update --availability pausedocker node update --availability drain来执行这两个操作:
vagrant@swarm-node4:~$ docker node update --availability pause swarm-node3
swarm-node3

vagrant@swarm-node4:~$ docker node update --availability drain swarm-node2
swarm-node2

现在,让我们再次使用docker service ps命令回顾一下我们的服务副本分配情况:

vagrant@swarm-node4:~$ docker service ps webserver --filter desired-state=running
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
6z55nch0q8ai        webserver.1         nginx:alpine        swarm-node4               Running             Running 3 minutes ago 
8il59udc4iey        webserver.2         nginx:alpine        swarm-node4               Running             Running 3 minutes ago 
1y4q96hb3hik        webserver.3         nginx:alpine        swarm-node1               Running             Running 3 minutes ago      

请注意,只有swarm-node1swarm-node4接收了一些任务,因为swarm-node3已暂停,而且我们已移除swarm-node2上的所有任务。

我们可以使用docker node ps <NODE>命令来获取指定节点上所有服务的任务。

  1. 我们将移除webserver服务并重新启用node2node3节点。我们将执行docker service rm命令来移除该服务:
vagrant@swarm-node4:~$ docker service rm webserver
webserver

vagrant@swarm-node4:~$ docker node update --availability active swarm-node2
swarm-node2

vagrant@swarm-node4:~$ docker node update --availability active swarm-node3
swarm-node3

在下一个实验中,我们将创建一个全球服务。

部署一个全球服务

在这个实验中,我们将部署一个全球服务。它将在每个集群节点上运行一个任务。让我们学习如何使用global模式:

  1. 在本章中,我们了解到,全球服务将在每个节点上部署一个副本。让我们创建一个服务并回顾其分布情况。我们将使用docker service create --mode global命令:
vagrant@swarm-node4:~$ docker service create --name webserver --mode global nginx:alpine
4xww1in0ozy3g8q6yb6rlbidr
overall progress: 4 out of 4 tasks 
ui67xyztnw8k: running   [==================================================>] 
b1t5o5x8mqbz: running   [==================================================>] 
rj3rgb9egnb2: running   [==================================================>] 
jw9uvjcsyg05: running   [==================================================>] 
verify: Service converged 

所有节点都会收到自己的副本,正如我们通过docker service ps命令看到的那样:

vagrant@swarm-node4:~$ docker service ps webserver --filter desired-state=running
ID                  NAME                                  IMAGE               NODE                DESIRED STATE       CURRENT STATE                ERROR               PORTS
0jb3tolmta6u        webserver.ui67xyztnw8kn6fjjezjdtwxd   nginx:alpine        swarm-node3               Running             Running about a minute ago 
im69ybzgd879        webserver.rj3rgb9egnb256cms0zt8pqew   nginx:alpine        swarm-node2               Running             Running about a minute ago 
knh5ntkx7b3r        webserver.jw9uvjcsyg05u1slm4wu0hz6l   nginx:alpine        swarm-node4               Running             Running about a minute ago 
26kzify7m7xd        webserver.b1t5o5x8mqbz77e9v4ihd7cec   nginx:alpine        swarm-node1               Running             Running about a minute ago            
  1. 现在,我们将对swarm-node1进行排空操作,并查看新的任务分配情况。我们将使用docker node update --availability drain命令来排空该节点:
vagrant@swarm-node4:~$ docker node update --availability drain swarm-node1
swarm-node1

vagrant@swarm-node4:~$ docker service ps webserver --filter desired-state=running
ID                  NAME                                  IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
0jb3tolmta6u        webserver.ui67xyztnw8kn6fjjezjdtwxd   nginx:alpine        swarm-node3               Running             Running 3 minutes ago 
im69ybzgd879        webserver.rj3rgb9egnb256cms0zt8pqew   nginx:alpine        swarm-node2               Running             Running 3 minutes ago 
knh5ntkx7b3r        webserver.jw9uvjcsyg05u1slm4wu0hz6l   nginx:alpine        swarm-node4               Running             Running 3 minutes ago                       

没有节点接收到swarm-node1任务,因为全局服务只会在每个节点上运行一个定义好的服务副本。

  1. 如果我们再次使用docker node update --availability active启用swarm-node1,其副本将重新启动:
vagrant@swarm-node4:~$ docker node update --availability active swarm-node1
node1
vagrant@swarm-node4:~$ docker service ps webserver --filter desired-state=running
ID                  NAME                                  IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
sun8lxwu6p3k        webserver.b1t5o5x8mqbz77e9v4ihd7cec   nginx:alpine        swarm-node1               Running             Running 1 second ago 
0jb3tolmta6u        webserver.ui67xyztnw8kn6fjjezjdtwxd   nginx:alpine        swarm-node3               Running             Running 5 minutes ago 
im69ybzgd879        webserver.rj3rgb9egnb256cms0zt8pqew   nginx:alpine        swarm-node2               Running             Running 5 minutes ago 
knh5ntkx7b3r        webserver.jw9uvjcsyg05u1slm4wu0hz6l   nginx:alpine        swarm-node4               Running             Running 5 minutes ago                       

Swarm 将在每个节点上运行任何全局服务的一个任务。当一个新节点加入集群时,它也会接收到集群中定义的每个全局服务的一个副本。

  1. 我们将再次删除webserver服务,以便通过docker service rm webserver清理集群,为接下来的实验做准备:
vagrant@swarm-node4:~$ docker service rm webserver
webserver

现在,我们将快速了解服务更新,以学习如何更新服务的基础镜像。

更新服务的基础镜像

让我们学习如何刷新已部署并正在运行的服务的新镜像版本,同时避免用户访问中断:

  1. 首先,我们使用docker service create --replicas 6创建一个 6 副本的webserver服务:
vagrant@swarm-node4:~$ docker service create --name webserver \
--replicas 6 --update-delay 10s --update-order start-first \
nginx:alpine 
vpllw7cxlma7mwojdyswbkmbk
overall progress: 6 out of 6 tasks 
1/6: running   [==================================================>] 
2/6: running   [==================================================>] 
3/6: running   [==================================================>] 
4/6: running   [==================================================>] 
5/6: running   [==================================================>] 
6/6: running   [==================================================>] 
verify: Service converged 
  1. 接下来,我们将更新到一个支持perl的特定nginx:alpine版本,例如。我们使用docker service update --image仅更改其基础镜像:
vagrant@swarm-node4:~$ docker service update --image nginx:alpine-perl webserver
webserver
overall progress: 6 out of 6 tasks 
1/6: running [==================================================>] 
2/6: running [==================================================>] 
3/6: running [==================================================>] 
4/6: running [==================================================>] 
5/6: running [==================================================>] 
6/6: running [==================================================>] 
verify: Service converged 

更新花费了超过 60 秒,因为 Swarm 会每隔 10 秒更新一个任务。它首先会启动一个使用新定义镜像的新容器。一旦它变得健康,它将停止旧版本的容器。这必须在每个任务上执行,因此需要更多时间,但这样我们可以确保始终有一个webserver任务在运行。在这个例子中,我们没有发布任何webserver端口,所以不会有用户交互。它只是一个简单的实验,但现实环境也会是如此,内部 Docker Swarm 负载均衡会始终将用户的请求引导到存活的实例上,即使更新正在进行中。

新版本现在正在运行,我们可以通过再次使用docker service ps来观察:

vagrant@swarm-node4:~$ docker service ps webserver --filter desired-state=running
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
n9s6lrk8zp32        webserver.1         nginx:alpine-perl   swarm-node4               Running             Running 4 minutes ago 
68istkhse4ei        webserver.2         nginx:alpine-perl   swarm-node1               Running             Running 5 minutes ago 
j6pqig7njhdw        webserver.3         nginx:alpine-perl   swarm-node1               Running             Running 6 minutes ago 
k4vlmeb56kys        webserver.4         nginx:alpine-perl   swarm-node2               Running             Running 5 minutes ago 
k50fxl1gms44        webserver.5         nginx:alpine-perl   swarm-node3               Running             Running 5 minutes ago 
apur3w3nq95m        webserver.6         nginx:alpine-perl   swarm-node3               Running             Running 5 minutes ago       
  1. 我们将再次删除webserver服务,以便通过docker service rm清理集群,为接下来的实验做准备:
vagrant@swarm-node4:~$ docker service rm webserver
webserver

在下一个实验中,我们将使用堆栈部署应用程序,而不是手动创建服务,这样可以避免例如配置错误等问题。使用堆栈将提供环境的可重现性,因为我们始终会运行相同的 IaC 定义。

使用 Docker Stacks 进行部署

在这个实验中,我们将使用密钥、配置和卷在 IaC 文件中部署 PostgreSQL 数据库。该文件将包含所有应用程序的需求,并将用于作为 Docker Stack 部署应用程序。让我们开始吧:

  1. 首先,我们将创建一个用于所需 PostgreSQL 管理员用户密码的密钥。我们将执行docker service create,并将标准输入作为密钥内容:
vagrant@swarm-node4:~$ echo SuperSecretPassword|docker secret create postgres_password -
u21mmo1zoqqguh01u8guys9gt

我们将它作为docker-compose文件中的外部密钥来使用。

  1. 我们将创建一个简单的初始化脚本,在 PostgreSQL 启动时创建一个新的数据库。我们将在当前目录中创建一个名为create-docker-database.sh的文件,内容如下,并设置适当的755权限:
#!/bin/bash
set -e

psql -v ON_ERROR_STOP=0 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
    CREATE USER docker;
    CREATE DATABASE docker;
    GRANT ALL PRIVILEGES ON DATABASE docker TO docker;
EOSQL

然后,我们创建一个配置文件,并将文件的内容添加到其中。我们将使用这个文件在启动 PostgreSQL 时创建一个名为docker的数据库。这是我们可以使用的功能,因为它是官方 Docker Hub PostgreSQL 镜像提供的。我们将使用docker config createcreate-docker-database.sh文件:

vagrant@swarm-node4:~$ docker config create create-docker-database ./create-docker-database.sh
uj6zvrdq0682anzr0kobbyhk2
  1. 我们将为一些节点添加标签,以确保数据库始终在这些节点上运行,因为我们只会在该节点上创建外部卷。对于这个示例,我们将使用node2。我们将使用docker volume create来创建一个卷:
Docker-Certified-Associate-DCA-Exam-Guide/environments/swarm$ vagrant ssh swarm-node2

vagrant@swarm-node2:~$ docker volume create PGDATA
PGDATA

这个卷仅存在于swarm-node2上,所以我们将基于节点标签创建约束,以便仅在swarm-node2上运行服务任务。我们将使用docker node update --label-add tier=database来实现:

vagrant@swarm-node4:~$ docker node update --label-add tier=database swarm-node2
swarm-node2 

这是一个简单的示例。在生产环境中,你永远不会使用本地卷。我们需要定义并使用一些插件,允许我们在不同的主机之间共享同一个卷,例如 NFS 和 RexRay。

  1. 现在,我们将创建以下的 Docker Compose 文件,命名为postgres-stack.yaml
version: '3.7'
services:
  database:
    image: postgres:alpine
    deploy:
      placement:
        constraints:
          - node.role == worker
          - node.labels.tier == database
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
    secrets:
      - source: postgres_password
        target: "/run/secrets/postgres_password"
    configs:
      - source: create-docker-database
        target: "/docker-entrypoint-initdb.d/create-db.sh"
        mode: 0755
        uid: "0"
    volumes:
      - type: volume
        source: PGDATA
        target: /var/lib/postgresql/data
    ports:
      - target: 5432
        published: 15432
        protocol: tcp
    networks:
      net:
        aliases:
         - postgres
         - mydatabase
configs:
  create-docker-database:
    external: true
secrets:
  postgres_password:
    external: true
volumes:
  PGDATA:
    external: true
networks:
  net:
    driver: overlay
    attachable: true

请注意文件中的以下内容;我们在这里添加了许多已学到的信息:

  • 我们为database服务定义了postgres:alpine镜像。

  • database服务将只在具有tier标签键和值为database的工作节点上调度。在这种情况下,它只会在node2上运行任务。

  • postgres镜像可以使用 Docker Swarm 的密钥文件作为环境变量,在这种情况下,它将使用挂载在/run/secrets/postgres_password上的postgres_password。该密钥声明为外部密钥,因为它之前在这个文件之外已经创建。

  • 我们还添加了一个配置文件,用来创建一个名为docker的初始数据库。这个配置文件也是外部的,因为我们将其放在了postgres-stack.yaml文件之外。

  • 我们还添加了一个名为PGDATA的外部卷。我们将使用这个卷来存储数据库数据,但它仅存在于node2上。它被定义为外部卷,因为我们手动在node2上创建了PGDATA卷。

  • 我们将 PostgreSQL 应用程序的端口5432发布到主机的端口;即15432。我们更改了发布的端口,以便能够识别它们不相同,因为5432将是定义的名为net网络中的一个内部端口。

  • 最后,我们将net网络定义为attachable,以便能够用一个简单的容器运行postgres客户端来测试我们的数据库。我们在这个网络内的database服务中添加了两个别名:postgresmydatabase

请注意,所有为堆栈创建的对象都将使用堆栈名称作为前缀。外部定义的对象不会这样做。它们将会被使用,但我们手动创建它们,超出了堆栈的生命周期。

  1. 我们使用docker stack deploy来部署postgres堆栈:
vagrant@swarm-node4:~$ docker stack deploy -c postgres-stack.yaml postgres
Creating network postgres_net
Creating service postgres_database

我们可以使用docker stack ps轻松查看堆栈的状态。

vagrant@swarm-node4:~$ docker stack ps postgres
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
53in2mik27r0 postgres_database.1 postgres:alpine swarm-node2 Running Running 19 seconds ago 

它运行在swarm-node2上,正如我们预期的那样。

  1. 我们将端口5432发布到端口15432。我们可以从集群中任何节点的 IP 地址连接到此端口,因为 Swarm 使用路由网格。我们使用curl命令检查端口的可用性:
vagrant@swarm-node4:~$ curl 0.0.0.0:15432
curl: (52) Empty reply from server

vagrant@swarm-node2:~$ curl 0.0.0.0:15432
curl: (52) Empty reply from server

vagrant@swarm-node3:~$ curl 0.0.0.0:15432
curl: (52) Empty reply from server

我们收到此curl响应是因为我们没有使用正确的软件客户端(但端口正在监听)。让我们运行一个简单的alpine容器,并使用postgres客户端。

  1. 现在,我们可以运行一个简单的alpine容器,连接到堆栈部署的网络。在本示例中,它是postgres_net
vagrant@swarm-node4:~$ docker network ls --filter name=postgres_net
NETWORK ID NAME DRIVER SCOPE
mh53ek97pi3a postgres_net overlay swarm

在这里,我们运行了一个简单的alpine容器,并使用docker container run和适当的网络安装了postgresql-client软件包:

vagrant@swarm-node4:~$ docker container run -ti --network postgres_net alpine
Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
e6b0cf9c0882: Pull complete 
Digest: sha256:2171658620155679240babee0a7714f6509fae66898db422ad803b951257db78
Status: Downloaded newer image for alpine:latest
/ # apk add --update --no-cache postgresql-client --quiet

记住我们将mydatabasepostgres别名添加到了database服务中。因此,任何一个都可以用于测试数据库连接性,因为 Swarm 已将这些条目添加到内部 DNS 中。我们可以通过在容器内运行简单的ping命令来测试:

/ # ping -c 1 mydatabase
PING mydatabase (10.0.3.2): 56 data bytes
64 bytes from 10.0.3.2: seq=0 ttl=64 time=0.237 ms
--- mydatabase ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.237/0.237/0.237 ms

/ # ping -c 1 postgres
PING postgres (10.0.3.2): 56 data bytes
64 bytes from 10.0.3.2: seq=0 ttl=64 time=0.177 ms
--- postgres ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.177/0.177/0.177 ms

/ # ping -c 1 database
PING database (10.0.3.2): 56 data bytes
64 bytes from 10.0.3.2: seq=0 ttl=64 time=0.159 ms
--- database ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.159/0.159/0.159 ms

我们将使用已安装的客户端测试我们部署的 PostgreSQL。记得使用我们之前作为机密创建的密码SuperSecretPassword。我们将使用psql命令测试数据库的连接性:

/ # psql -h mydatabase -U postgres
Password for user postgres: 
psql (12.1)
Type "help" for help.

postgres=# \l
 List of databases
 Name | Owner | Encoding | Collate | Ctype | Access privileges 
-----------+----------+----------+------------+------------+-----------------------
 docker | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =Tc/postgres +
 | | | | | postgres=CTc/postgres+
 | | | | | docker=CTc/postgres
 postgres | postgres | UTF8 | en_US.utf8 | en_US.utf8 | 
 template0 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres +
 | | | | | postgres=CTc/postgres
 template1 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres +
 | | | | | postgres=CTc/postgres
(4 rows)

postgres=# 

我们使用\l列出了已部署的数据库,并列出了通过create-db.sh脚本创建的docker数据库。请注意,我们使用的是默认的 PostgreSQL 数据库端口5432(应客户请求未进行端口定制),而不是15432。这是因为docker容器在内部连接到数据库。postgres_database.1任务和外部运行的容器都使用相同的网络postgres_net

请注意,我们可以使用所有已学习的选项与创建的堆栈服务postgres_database。无论如何,我们可以修改 Docker Compose 文件并重新部署相同的堆栈并进行一些更改。Swarm 将审查所需的更新并对所有组件采取必要的行动。

让我们通过执行exit命令退出正在运行的容器,然后使用docker stack rm删除postgres堆栈和node2卷,以便清理为后续实验做准备:

postgres=# exit
/ # exit

vagrant@swarm-node4:~$ docker stack rm postgres
Removing service postgres_database
Removing network postgres_net

在下一个实验中,我们将启动一个简单的复制服务并审查内部入口负载均衡。

Swarm 入口内部负载均衡

在本实验中,我们将使用codegazers/colors:1.13镜像。这是一个简单的应用程序,将显示不同的随机前端颜色或文本。让我们开始吧:

  1. 让我们基于codegazers/colors:1.13镜像创建一个名为colors的服务。由于我们不会使用环境变量设置特定的颜色,因此将为我们选择随机颜色。使用docker service create --constraint node.role==worker,如下所示:
vagrant@swarm-node4:~$ docker service create --name colors \
 --publish 8000:3000 \
--constraint node.role==worker \
codegazers/colors:1.13 

mkyz0d94ovb144xmvo0q4py41
overall progress: 1 out of 1 tasks 
1/1: running [==================================================>] 
verify: Service converged 

我们选择不在管理节点上运行副本,因为在本实验中我们将从node4使用curl

  1. 让我们使用curlswarm-node4管理节点测试本地连接性:
vagrant@swarm-node4:~$ curl 0.0.0.0:8000/text
APP_VERSION: 1.0
COLOR: orange
CONTAINER_NAME: d3a886d5fe34
CONTAINER_IP: 10.0.0.11 172.18.0.3
CLIENT_IP: ::ffff:10.0.0.5
CONTAINER_ARCH: linux

我们部署了一个副本,并且它正在运行orange颜色。请注意容器的 IP 地址及其名称。

  1. 让我们通过执行docker service update --replicas 6来运行更多的副本:
vagrant@swarm-node4:~$ docker service update --replicas 6 colors --quiet
colors
  1. 如果我们再一次使用curl测试服务端口8080,我们会得到不同的颜色。这是因为容器启动时没有设置颜色:
vagrant@swarm-node4:~$ curl 0.0.0.0:8000/text
APP_VERSION: 1.0
COLOR: red
CONTAINER_NAME: 64fb2a3009b2
CONTAINER_IP: 10.0.0.12 172.18.0.4
CLIENT_IP: ::ffff:10.0.0.5
CONTAINER_ARCH: linux

vagrant@swarm-node4:~$ curl 0.0.0.0:8000/text
APP_VERSION: 1.0
COLOR: cyan
CONTAINER_NAME: 73b07ee0c287
CONTAINER_IP: 10.0.0.14 172.18.0.3
CLIENT_IP: ::ffff:10.0.0.5
CONTAINER_ARCH: linux

我们在不同的容器上得到了不同的颜色。路由器网格正在使用入口覆盖网络将请求引导到colors任务的容器。

我们可以使用docker service logs colors查看所有colors服务任务的日志。

  1. 使用docker service rm命令移除colors服务,为下一个也是最后一个实验做准备:
vagranr@swarm-node4:~$ docker service rm colors
colors

在下一个实验中,我们将回顾服务端点模式,并考虑 DNS 如何解析vipdnsrr情况。

服务发现

在本实验中,我们将创建一个测试用的覆盖附加网络,并回顾vipdnsrr端点模式的 DNS 条目。让我们开始吧:

  1. 首先,我们需要使用docker network create --attachable -d overlay命令创建一个可附加的覆盖test网络,如下所示:
vagrant@swarm-node4:~$ docker network create --attachable -d overlay test
32v9pibk7cqfseknretmyxfsw
  1. 现在,让我们创建两个不同的colors服务。每个服务将使用不同的端点模式。对于vip模式,我们将使用docker service create
vagrant@swarm-node4:~$ docker service create --replicas 2 \
--name colors-vip --network test --quiet codegazers/colors:1.13
4m2vvbnqo9wgf8awnf53zr5b2

让我们使用docker service create --endpoint-mode dnsrr命令创建第二个服务,使用dnsrr模式,如下所示:

vagrant@swarm-node4:~$ docker service create --replicas 2 \
--name colors-dnsrr --network test --quiet --endpoint-mode dnsrr codegazers/colors:1.13
wqpv929pe5ehniviclzkdvcl0
  1. 现在,让我们使用docker container runtest网络上运行一个简单的alpine容器,并测试内部名称解析功能。我们需要安装bind-tools包,以便使用hostnslookup工具:
vagrant@swarm-node4:~$ docker run -ti --rm --network test alpine 
/ # apk add --update --no-cache bind-tools --quiet
/ # host colors-vip
colors-vip has address 10.0.4.2
/ # host colors-dnsrr
colors-dnsrr has address 10.0.4.7
colors-dnsrr has address 10.0.4.8
/ #exit

如预期的那样,使用vip端点模式时,服务会获得一个虚拟 IP 地址。所有请求都会重定向到该地址,入口会使用内部负载均衡将请求路由到适当的容器。

另一方面,使用dnsrr端点模式时不会提供虚拟 IP 地址。内部 DNS 会为每个容器 IP 添加一个条目。

  1. 我们还可以查看附加到test网络的容器。这些容器将获得一个内部 IP 地址,以及一个将在覆盖网络上路由的 IP 地址。我们可以使用docker container exec命令,查看运行中的colors-dnsrr任务的容器,执行ip add show命令:
vagrant@swarm-node4:~$ docker exec -ti colors-dnsrr.1.vtmpdf0w82daq6fdyk0wwzqc7 ip add show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
 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
111: eth0@if112: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue state UP 
 link/ether 02:42:0a:00:04:07 brd ff:ff:ff:ff:ff:ff
 inet 10.0.4.7/24 brd 10.0.4.255 scope global eth0
 valid_lft forever preferred_lft forever
113: eth1@if114: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
 link/ether 02:42:ac:12:00:04 brd ff:ff:ff:ff:ff:ff
 inet 172.18.0.4/16 brd 172.18.255.255 scope global eth1
 valid_lft forever preferred_lft forever

所有 Vagrant 环境可以通过执行vagrant destroy -f来轻松移除,从而删除本实验中之前创建的所有节点。此命令应该在你的environments/swarm本地目录中执行。

使用docker service rm colors-dnsrr colors-vip命令移除你为上一个实验创建的所有服务。

总结

在本章中,我们回顾了如何部署和使用 Docker Swarm 调度器。它是 Docker 的默认调度器,因为它是 Docker Engine 自带的。

我们了解了 Docker Swarm 的功能,以及如何使用栈(基础设施即代码文件)和服务来部署应用程序,而不是使用容器。编排将管理应用程序的组件,以保持它们的运行,帮助我们在不影响用户的情况下进行升级。Docker Swarm 还引入了新的对象,如 secrets 和 config,帮助我们在集群节点之间分发工作负载。卷和网络应该在集群范围内进行管理。我们还学习了覆盖网络,以及 Docker Swarm 的路由网格如何简化应用程序发布。

在下一章中,我们将学习 Kubernetes 编排器。目前,Kubernetes 是 Docker Certified Associate 考试的一个小部分,但在未来的版本中这一部分可能会增加。同时,了解和理解 Kubernetes 的概念与 Docker Swarm 一起使用是很有用的。Docker Enterprise 提供了这两者,并且我们可以让它们协同工作。

问题

  1. 从以下选项中选择所有错误的陈述:

a) Docker Swarm 是唯一可以与 Docker 配合使用的编排器。

b) Docker Swarm 已经随 Docker Engine 一起包含在内。

c) Docker Swarm 允许我们在多个节点池上部署应用程序,这些节点池一起工作,被称为集群。

d) 所有前述陈述均为假。

  1. 以下哪些陈述关于 Swarm 默认提供的功能是错误的?

a) 服务发现

b) 内部负载均衡

c) 在集群节点之间进行分布式容器的覆盖网络

d) 所有前述陈述均为假。

  1. 以下哪些陈述关于管理节点是正确的?

a) 我们不能在管理节点上创建带有任务的复制服务。

b) 每个集群中只有一个领导节点,负责管理所有 Swarm 集群的变化和对象状态。

c) 如果领导节点故障,所有更改将被冻结,直到领导节点恢复健康。

d) 所有前述陈述均为真。

  1. 以下哪些陈述关于工作节点是错误的?

a) 工作节点只运行工作负载。

b) 如果我们排空一个工作节点,所有运行在该节点上的工作负载将转移到其他可用节点。

c) Swarm 角色可以在集群中的任何节点上根据需要进行更改。

d) 所有前述陈述均为真。

  1. 以下哪些关于 Swarm 栈的陈述是错误的?

a) 默认情况下,所有栈将部署在各自的网络上。

b) 栈将使用 Docker Compose 文件来定义所有应用组件。

c) 用于栈的所有内容应该在docker-compose文件中定义。我们不能添加外部对象。

d) 所有前述陈述均为真。

进一步阅读

请参考以下链接,了解本章所涉及的更多信息:

使用 Kubernetes 进行编排

本章专门介绍当今最广泛使用的容器编排工具——Kubernetes。2018 年,51%的容器用户选择将 Kubernetes 作为他们的主要编排工具。近年来,Kubernetes 的采用率不断增长,现在它已成为大多数 容器即服务CaaS)平台的核心。

云服务提供商已经跟随 Kubernetes 的扩展,且大多数提供商(包括 Amazon、Google 和 Azure)现在都提供自有的 Kubernetes 即服务KaaS)平台,用户无需处理 Kubernetes 的管理任务。这些服务旨在简化操作并保证在云平台上的可用性。用户只需在这些平台上运行其工作负载,而云服务提供商则负责复杂的维护任务。

在本章中,我们将了解 Kubernetes 的工作原理以及它提供的功能。我们将回顾部署一个高可用性 Kubernetes 集群所需的内容。接下来,我们将学习 Kubernetes 对象,如 pods 和服务等。网络是将工作负载分配到集群中的关键,我们将学习 Kubernetes 网络是如何工作的,以及它如何提供服务发现和负载均衡。最后,我们将回顾 Kubernetes 提供的一些特殊安全功能,用于管理集群的认证和授权。

本章将涵盖以下主题:

  • 部署 Kubernetes

  • 使用 Kubernetes 实现高可用性

  • Pods、服务和其他 Kubernetes 资源

  • 部署编排资源

  • Kubernetes 网络

  • 发布应用程序

Kubernetes 目前还不包括在 Docker Certified Associate 考试中,但它可能会在下一个版本中加入,因为 Docker Enterprise 包含了一个完全兼容的 Kubernetes 平台,并且部署在 Docker Swarm 编排工具之上。Docker Enterprise 是唯一同时提供这两种编排工具的容器平台。我们将在本书的第三部分学习 Docker Enterprise 的组件和功能,每个组件都有独立的章节。

第十二章:技术要求

在本章中,我们将了解 Docker Swarm 编排工具的功能。我们还将在章末提供一些实验室,帮助你理解和学习我们将要覆盖的概念。这些实验可以在你的笔记本电脑或个人电脑上运行,使用提供的 Vagrant Kubernetes 环境 或自行部署的任何 Docker Swarm 集群。你可以在本书的 GitHub 代码库中查看更多信息,地址为 github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

查看以下视频,观看代码演示:

"bit.ly/3gzAnS3"

使用 Docker 引擎部署 Kubernetes

Kubernetes 具有许多功能,比 Docker Swarm 更为复杂。它提供了 Docker Swarm 没有的额外功能,且不需要修改应用程序代码。Docker Swarm 更符合微服务逻辑,而 Kubernetes 更接近虚拟机应用程序的提升与迁移方法(将应用程序原样迁移到新的基础设施)。这是因为 Kubernetes 的 pod 对象可以与虚拟机进行比较(其中应用程序进程作为容器在 pod 内部运行)。

在开始讨论 Kubernetes 架构之前,让我们回顾一下我们已学到的一些编排概念。

编排应该提供部署解决方案所需的所有功能,用于执行、管理和发布基于容器的分布式应用程序。因此,它应该提供一个控制平面,以确保集群的可用性,一个调度器来部署应用程序,以及一个网络平面来互联分布式应用程序。它还应提供发布集群分布式应用程序的功能。应用程序的健康状况也将由编排器进行管理。因此,如果某个应用程序组件出现故障,编排器会部署一个新的组件,以确保应用程序的健康。

Kubernetes 提供了所有这些功能,Docker Swarm 也提供这些功能。然而,Kubernetes 有更多的功能,具有可扩展性,并且有一个更大的社区支持该项目。Docker 也在其 Docker Enterprise 2.0 版本中采纳了 Kubernetes。它是唯一一个在同一基础设施上同时支持 Docker Swarm 和 Kubernetes 的平台。

Kubernetes 提供了更高的容器密度,因为它能够为每个应用程序组件同时运行多个容器。它还提供了自动扩展功能以及其他高级调度功能。

由于 Kubernetes 是一个大型社区项目,它的某些组件也被解耦到不同的项目中,以便提供更快的部署速度。这个主要的开源项目由云原生计算基金会CNCF)托管。Kubernetes 每 6 个月发布一个新版本——想象一下每 6 个月就要在生产环境中更新旧的遗留应用程序。正如前面提到的,许多其他产品很难跟上这种应用生命周期,但 Kubernetes 提供了一种方法,使得升级到新软件版本变得更加容易。

Kubernetes 的架构模型基于常见的编排组件。我们部署主节点来执行管理任务,工作节点(也称为从节点)来运行应用程序负载。我们还部署了一个etcd键值数据库来存储所有集群对象的数据。

让我们来介绍一下 Kubernetes 的组件。主节点和工作节点运行不同的过程,它们的数量可能会根据每个角色所提供的功能而有所不同。这些组件中的大多数可以作为系统服务或容器进行安装。以下是 Kubernetes 集群组件的列表:

  • kube-apiserver

  • kube-scheduler

  • kube-controller-manager

  • etcd

  • `kubelet`

  • kube-proxy

  • 容器运行时

请注意,这个列表与我们在 Docker Swarm 中学到的内容有很大不同,在 Docker Swarm 中,一切都是内建的。让我们回顾一下每个组件的特性和属性。记住,这不是一本 Kubernetes 的书——我们只会学习基础知识。

我们将运行专用的主节点来提供隔离的集群控制平面。以下组件将在这些节点上运行:

  • kube-apiserver:这是 Kubernetes 的核心,它通过 HTTP(如果使用 TLS 证书,则为 HTTPS)暴露 Kubernetes API。我们将连接到该组件,以便部署和管理应用程序。

  • kube-scheduler:当我们部署应用程序的组件时,如果没有定义特定节点位置,调度器将决定每个组件运行的位置。为了决定在哪里运行已部署的工作负载,它将检查工作负载的属性,如特定资源、限制、架构要求、亲和性或约束。

  • kube-controller-manager:该组件将管理控制器,控制器是始终监视集群对象状态变化的过程。例如,它将管理节点和工作负载的状态,以确保所需数量的实例正在运行。

  • etcd:这是所有 Kubernetes 对象信息和状态的键值存储。某些生产环境将会将 etcd 部署在主节点基础设施之外,以避免性能问题并提高组件的高可用性。

另一方面,工作进程可以在任何节点上运行。正如我们在 Docker Swarm 中学到的,我们可以决定在工作节点和主节点上运行应用程序工作负载。这些是计算节点所需的组件:

  • kubelet:这是 Kubernetes 的核心代理组件。它将在能够执行应用程序工作负载的任何集群节点上运行。该过程还将确保分配给节点的 Kubernetes 工作负载正在运行且处于健康状态(它只会管理在 Kubernetes 内创建的 Pods)。

我们在讨论在 Kubernetes 集群上调度容器或工作负载。实际上,我们将调度的是 Pods,Pods 是 Kubernetes 特有的对象。Kubernetes 会运行 Pods;它永远不会运行独立的容器。

  • kube-proxy:该组件将使用操作系统的数据包过滤和路由功能管理工作负载的网络交互。kube-proxy 应该在任何工作节点上运行(即运行工作负载的节点)。

前面我们提到过容器运行时作为 Kubernetes 集群的一个组件。事实上,它是一个必要的要求,因为 Kubernetes 本身并不提供容器运行时。我们将使用 Docker 引擎,因为它是最广泛使用的引擎,而且我们已经在前面的章节中讨论过它。

以下工作流展示了所有 Kubernetes 组件在五个节点上的分布(注意主节点上也有工作组件,并且 etcd 也部署在主节点之外):

如在第八章中讨论的,使用 Docker Swarm 进行编排,外部负载均衡器将为复制服务提供 L4 和 L7 路由。在这种情况下,集群管理组件不会使用类似路由器网状服务。我们将通过在不同节点上运行复制进程来为核心组件提供高可用性。我们将需要一个虚拟 IP 地址,并且还会使用完全限定域名FQDN)作为传输层安全性TLS)证书。这将确保 Kubernetes 组件之间的安全通信和访问。

以下图展示了为确保组件之间的安全通信而创建的 TLS 证书:

我们将使用kubectl命令行与 Kubernetes 集群进行交互,并始终连接到kube-apiserver进程。

在下一节中,我们将学习如何实现高可用性 Kubernetes 集群环境。

部署具有高可用性的 Kubernetes 集群

Docker Swarm 容易实现。为了提供高可用性,我们只需更改节点角色来实现所需的奇数个管理节点。在 Kubernetes 中,这并非如此简单;角色不能更改,通常管理员不会更改主节点的初始数量。

因此,安装具有高可用性组件的 Kubernetes 集群需要一些规划。幸运的是,Docker Enterprise 会为你部署集群(自 2.0 版本起)。我们将在第十一章中回顾这种方法,因为统一控制平面UCP)将基于 Docker Swarm 部署 Kubernetes。

为了提供高可用性,我们将部署奇数个控制平面组件。通常会在另外三个节点上部署etcd。在这种情况下,这些节点既不是主节点也不是工作节点,因为etcd将部署在 Kubernetes 节点之外。我们只需要从主节点访问这些外部的etcd。因此,在这种情况下,我们将运行一个由八个节点组成的集群:三个节点运行etcd,三个主节点运行所有其他控制平面组件(集群管理),并且至少有两个工作节点以提供冗余,以防其中一个宕机。这种配置适用于许多 Kubernetes 环境。我们将etcd与控制平面组件隔离,以提供更好的管理性能。

我们可以在主节点上部署etcd。这与我们在 Docker Swarm 中学到的类似。我们可以拥有纯主节点—只运行管理组件—以及用于处理工作负载的工作节点。

安装 Kubernetes 并不容易,许多软件供应商已经开发了自己的 KaaS 平台,提供不同的安装方法。

为了实现高可用性,我们将运行 etcd 的分布式副本。在这种情况下,kube-apiserver 将连接到一组节点,而不是仅连接到一个 etcd 节点。kube-apiserverkube-schedulerkube-controller-manager 进程将在不同的主节点上运行多个副本(每个主节点上一个实例)。

我们将使用 kube-apiserver 来管理集群。Kubernetes 客户端将通过 HTTP/HTTPS 协议连接到此服务器进程。我们将使用外部负载均衡器在主节点上的不同副本之间分配流量。Kubernetes 使用 Raft 算法,因为 etcd 使用了该算法。

部署在集群中的应用程序将基于弹性默认具有高可用性(就像在 Docker Swarm 集群中一样)。一旦应用程序及其所有组件被部署,如果其中一个组件失败,kube-controller-manager 将启动一个新的实例。有不同的控制器进程,负责根据副本在所有节点上同时执行应用程序以及其他特定的执行情况。

在下一节中,我们将介绍 pod 概念,这是理解 Kubernetes 和 Docker Swarm 之间区别的关键。

Pods、服务和其他 Kubernetes 资源

pod 概念是理解 Kubernetes 的关键。一个 pod 是一组一起运行的容器。它非常简单。所有这些容器共享一个网络命名空间和存储。它就像一个小的逻辑主机,因为我们在一起运行多个进程,共享相同的 IP 地址和卷。我们在第一章《现代基础设施与应用程序使用 Docker》中学到的隔离方法,在这里同样适用,现代基础设施和应用程序使用 Docker

Pods

Pods 是 Kubernetes 环境中最小的调度单元。一个 pod 内的容器将共享相同的 IP 地址,并且可以通过 localhost 相互发现。因此,分配的端口在 pod 内必须是唯一的。我们不能为其他容器和进程间通信重复使用端口,因为进程将像在同一个逻辑主机上执行一样运行。一个 pod 的生命周期依赖于其容器的健康状态。

Pods 可以用于集成完整的应用程序堆栈,但它们通常用于少量容器。事实上,微服务依赖于小的功能模块;因此,我们每个节点上只运行一个容器。由于 pod 是 Kubernetes 中最小的调度单元,因此我们扩展的是 pod,而不是容器。因此,如果许多应用组件在同一个 pod 内一起执行,则完整的堆栈将被复制。

另一方面,Pods 允许我们执行容器,例如为了初始化另一个容器的某些特殊功能或属性。还记得 第八章 中的 使用 Docker Stacks 部署 部分吗?在那个实验中,我们启动了一个 PostgreSQL 数据库,并添加了一个初始化脚本来创建一个特定的数据库。我们可以在 Kubernetes 上使用 Pod 中的初始容器来完成这个操作。

终止和移除 Pod 将取决于停止或删除 Pod 内所有容器所需的时间。

以下图表示一个包含多个容器的 Pod,共享相同的 IP 地址和卷等特性(我们可以为 Pod 中的所有容器应用一个特殊的安全上下文):

现在让我们回顾一下 Kubernetes 上的服务资源。

服务

服务在 Kubernetes 中有不同的含义。服务是集群的抽象对象;我们在 Kubernetes 中不会调度服务。它们定义了一组协同工作的 Pod,用于提供应用程序组件。我们还可以将服务与外部资源(端点)关联。这个服务将像集群内的其他服务一样使用,但例如,可以使用外部 IP 地址和端口。

我们还使用服务来发布应用程序,既可以在 Kubernetes 集群内,也可以在外部发布。为此,存在不同类型的服务。除了无头服务外,所有这些服务都提供 Pod 副本之间的内部负载均衡,供公共服务使用:

  • Headless:我们使用无头服务与非 Kubernetes 服务发现解决方案进行交互。不会分配虚拟 IP。也不会有负载均衡或代理来访问服务的 Pod。这种行为类似于 Docker Swarm 的 DNSRR 模式。

  • ClusterIP:这是默认的服务类型。Kubernetes 将提供一个从可配置池中选择的内部虚拟 IP 地址。这样只有集群内部的对象才能访问定义的服务。

  • NodePort:NodePort 服务也会接收一个虚拟 IP(ClusterIP),但是暴露的服务端口将在所有集群节点上可用。Kubernetes 将请求路由到服务的 ClusterIP 地址,无论请求到达哪个节点。因此,服务的定义端口将在 <ANY_CLUSTER_NODE>:<NODEPORT_PORT> 上可用。这实际上让我们想起了 Docker Swarm 上的路由网格行为。在这种情况下,我们需要将一些集群节点添加到外部负载均衡器中,以便访问定义并暴露的服务端口。

  • LoadBalancer:此服务类型仅在云服务提供商的 Kubernetes 部署中可用。我们使用自动创建的(通过云服务提供商的 API 集成)负载均衡器将服务暴露到外部。它同时使用 ClusterIP 虚拟 IP 进行内部路由,并使用 NodePort 概念从负载均衡器访问服务定义的端口。

  • ExternalName:这种方式现在不太常见,因为它依赖于 DNS CNAME 记录,并且是新的实现。它用于添加外部服务,位于 Kubernetes 集群之外。外部服务将通过其名称进行访问,就像它们在集群内部运行一样。

    Kubernetes 集群。

以下架构表示 NodePort 服务类型的常见配置。在此示例中,服务可以通过外部负载均衡器在7000端口访问,而 Pod 则可以在5000端口内部访问。所有流量将在服务的所有 Pod 端点之间进行负载均衡:

Kubernetes 中还有许多其他资源。我们将在深入了解如何在 Kubernetes 集群上部署应用之前,快速浏览其中的一些资源。

配置映射和机密

我们已经学会了如何使用 Docker Swarm 在集群范围内分发所需的应用信息。Kubernetes 也提供了类似的解决方案。我们将使用配置映射,代替 Docker Swarm 的配置对象,以及机密。

在这两种情况下,我们可以使用文件或标准输入(使用--from-literal选项)来创建这些资源。字面量选项允许我们通过命令行创建这些对象,而不是使用 YAML 文件。

Kubernetes 的kubectl命令行提供了两种不同的方法来创建集群资源/对象(命令式和声明式)。我们将使用命令行生成器或资源文件,通常是 YAML 格式。第一种方法通常被称为命令式,但并非所有资源都可以使用这种方法,使用文件的方法被称为声明式。这适用于所有 Kubernetes 资源;因此,我们可以使用带参数的kubectl create pod,或者使用kubectl create -f <POD_DEFINITION_FILE_IN_YAML_FORMAT>。我们可以轻松地将之前生成的命令行对象导出为 YAML 格式,以便实现资源的可重现性,并将其定义保存在某个安全的地方。

配置映射和机密允许我们将配置从镜像内容中解耦,而无需使用不安全的运行时可见变量或某些节点上共享的本地文件。我们将使用机密来处理敏感数据,而配置映射则用于常见的配置。

命名空间

命名空间可以理解为基于名称的作用域。它们允许我们在不同命名空间之间隔离资源。资源的名称在每个命名空间内是唯一的。资源只能存在于一个命名空间内;因此,我们可以使用命名空间来划分对资源的访问。

命名空间的最简单用途之一是限制用户访问和 Kubernetes 对象及资源配额的使用。基于命名空间,我们将为用户提供一组特定的主机资源。例如,不同的用户组或团队将拥有自己的资源和配额,限制他们环境中的行为。

持久卷

我们在第四章《容器持久性和网络》中了解了卷。在 Kubernetes 中,卷是附加到 pod 的,而不是容器;因此,卷将跟随 pod 的生命周期。

Kubernetes 中有许多不同类型的卷,我们可以在 pods 内部混合使用它们。卷对运行在 pod 中的任何容器都可用。有些卷是专为云服务提供商设计的,还有些卷是大多数数据中心中可用的存储解决方案。让我们回顾一些有趣的、常用的卷:

  • emptyDir: 这个卷在 pod 被分配到节点时创建,并随着 pod 被删除。它一开始是空的,通常用于在同一个 pod 内的容器之间共享信息。

  • hostPath: 我们已经在 Docker 中使用过这种类型的卷。这些卷允许我们将主机上的文件或目录挂载到 pods 中。

每种卷类型都有自己独特的选项来启用其独特功能。

这些卷设计用于在 pods 内部使用,但它们并没有为 Kubernetes 集群和存储永久数据做好准备。对于这些情况,我们使用持久化卷PVs)。

PV 使我们能够抽象存储是如何提供的。无论存储主机是如何进入集群的,我们只关心如何使用它们。PV 由管理员预配置,例如,用户可以使用它。PV 是 Kubernetes 资源,因此,我们可以将它们与命名空间关联,并且它们有自己的生命周期。它们是与 pod 独立的。

PV 是通过持久化卷声明PVCs)请求的。因此,PVC 消耗已定义的 PV。这是将 pod 与 PV 关联起来的方式。

因此,PVC 允许用户使用存储。我们可以根据存储的内部属性(如速度、主机上的提供方式等)来指定存储,并通过存储类实现动态配置。通过这些对象,我们描述集群中所有可用存储解决方案的属性作为配置文件,Kubernetes 会为使用这些存储做好持久化存储准备。

需要知道的是,我们可以决定在 pod 死亡后 PV 数据的行为。回收策略描述了在 pod 不再使用卷及其内容时应该怎么做。因此,我们可以选择删除卷、保留卷及其内容,或回收它。

我们可以说,PV(持久化卷)是 Kubernetes 集群资源,用于应用程序持久化存储,而 PVC(持久化卷声明)是请求使用这些资源的方式。

存储类是一项新功能,允许管理员将动态配置集成到我们的集群中。这帮助我们在不需要手动配置每个卷的情况下提供存储。我们只需定义存储的配置文件和特性,存储提供者就会为所需的卷提供最佳解决方案。

在下一部分,我们将学习如何在 Kubernetes 集群上部署工作负载。

部署编排资源

在 Kubernetes 中部署工作负载很简单。我们将使用 kubectl 来指定要创建的资源,并与 kube-apiserver 进行交互。

如前所述,我们可以使用命令行来使用内置生成器或 YAML 文件。根据 Kubernetes API 的版本,某些选项可能不可用,但我们假设 Kubernetes 版本为 1.11 或更高。

在本章中,所有示例使用 Kubernetes 1.14,因为这是在编写本书时,当前 Docker Enterprise 3.0 版本上可用的版本。

我们从创建一个简单的 Pod 开始。我们将回顾两种方式——命令行的命令式方式和使用 YAML 清单的声明式方式。

使用 Pod 生成器,我们将运行 kubectl run --generator=run-pod/v1 命令:

$ kubectl run --generator=run-pod/v1 --image=nginx:alpine myfirstpod --labels=example=myfirstpod
pod/myfirstpod created

使用 YAML 定义文件,我们将描述 Pod 所需的所有属性:

apiVersion: v1
kind: Pod
metadata:
 name: myfirstpod
  labels:
    example: myfirstpod
spec:
  containers:
  - name: myfirstpodcontainer
    image: nginx:alpine

要部署这个 .yaml 定义文件,我们只需运行 kubectl create -f <YAML_DEFINITION_FILE>。这将会在指定的命名空间中创建文件中定义的所有资源。由于我们没有使用参数来指定命名空间,它们将在用户定义的命名空间中创建。在我们的例子中,默认使用的是 default 命名空间。

我们可以在每个 YAML 文件中定义命名空间,或者通过命令行参数来定义。后者会覆盖 YAML 定义。

两个示例都会创建相同的 Pod,Pod 内部有一个容器,运行 nginx:alpine 镜像。

在 Kubernetes 中使用 argscommand 定义时需要小心。这些键与我们用于 Docker 容器或镜像的定义不同。Kubernetes 的 command 表示 ENTRYPOINT,而 args 表示容器/镜像的 CMD 定义。

我们可以通过简单地删除它来终止这个 Pod,命令是 kubectl delete。要获取在命名空间中运行的 Pod 列表,我们将使用 kubectl get pods。如果在 kubectl 执行时省略了命名空间,将使用用户指定的命名空间:

$ kubectl get pods
NAME         READY   STATUS    RESTARTS   AGE
myfirstpod   1/1     Running   0          11s

但这只是创建了一个简单的 Pod;我们无法使用这种资源创建更多的 NGINX 副本。要使用副本,我们将使用 ReplicaSets,而不是单个 Pod。

我们将设置一个 Pod 模板部分和 Pod 选择器,以识别哪些已部署的 Pod 属于这个 ReplicaSet 资源,并将其写入新的 YAML 文件。这将帮助控制器监控 Pod 的健康状态。

在这里,针对之前的 Pod 定义,我们添加了一个 template 部分和一个带有标签的 selector 键:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myfirstrs
  labels:
    example: myfirstrs
spec:
  replicas: 3
  selector:
    matchLabels:
      example: myfirstrs
  template:
    metadata:
      name: myfirstpod
      labels:
        example: myfirstrs
    spec:
      containers:
      - name: myfirstpodcontainer
        image: nginx:alpine

因此,我们使用与之前相同的 Pod 定义创建了三个副本。这个 Pod 的定义被用作所有副本的模板。我们可以使用 kubectl get all 来查看所有已部署的资源。在以下命令中,我们过滤结果,只获取带有 example 标签和 myfirstrs 值的资源:

$ kubectl get all -l example=myfirstrs
NAME                  READY   STATUS    RESTARTS   AGE
pod/myfirstrs-2xrpk   1/1     Running   0          47s
pod/myfirstrs-94rb5   1/1     Running   0          47s
pod/myfirstrs-jm6lc   1/1     Running   0          47s

NAME                        DESIRED   CURRENT   READY   AGE
replicaset.apps/myfirstrs   3         3         3       47s

每个副本将具有相同的前缀名称,但其 ID 将成为名称的一部分。这使得该资源在 Kubernetes 集群中唯一标识。

我们正在使用 kubectl get all -l <KEY=VALUE> 来过滤所有我们用 example 键和 myfirstrs 值标记的资源。

我们可以使用DaemonSet在集群的每个节点上部署副本,就像我们在 Docker Swarm 的全局服务中所做的那样:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: myfirstds
  labels:
    example: myfirstds
spec:
  selector:
    matchLabels:
      example: myfirstds
  template:
    metadata:
      name: myfirstpod
      labels:
        example: myfirstds
    spec:
      containers:
      - name: myfirstpodcontainer
        image: nginx:alpine
        resources:
          limits:
            memory: 100Mi
          requests:
            cpu: 100m
            memory: 10Mi

我们现在可以再次使用 kubectl get all 回顾 pod 的分布情况。

请注意,我们添加了容器的资源限制和资源请求。limits 键允许我们为每个容器指定资源限制。另一方面,requests 向调度器提供有关运行此组件所需最小资源的信息。如果没有足够的资源满足请求的 CPU、内存等要求,pod 将无法在节点上运行。如果任何容器超出了它们的限制,它们将被终止:

$ kubectl get all -l example=myfirstds -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/myfirstds-cr7xc 1/1 Running 0 84s 192.168.135.5 node3 <none> <none>
pod/myfirstds-f6x8n 1/1 Running 0 84s 192.168.104.6 node2 <none> <none>

NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE CONTAINERS IMAGES SELECTOR
daemonset.apps/myfirstds 2 2 2 2 2 <none> 84s myfirstpodcontainer nginx:alpine example=myfirstds

Deployment 资源是一个更高级的概念,因为它管理 ReplicaSet 并允许我们发布应用程序组件的更新。推荐使用Deployment而不是ReplicaSet。我们将再次使用templateselect部分:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myfirstdeployment
  labels:
    example: myfirstds
spec:
  replicas: 3
  selector:
    matchLabels:
      example: myfirstdeployment
  template:
    metadata:
      name: myfirstpod
      labels:
        example: myfirstdeployment
    spec:
      containers:
      - name: myfirstpodcontainer
        image: nginx:alpine
        ports:
        - containerPort: 80

因此,部署将运行三个nginx:alpine的副本,这些副本将再次分布在集群节点上:

$  kubectl get all -l example=myfirstdeployment -o wide
NAME                                     READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
pod/myfirstdeployment-794f9bfcd7-9m8vg   1/1     Running   0          12s   192.168.135.9    node3   <none>           <none>
pod/myfirstdeployment-794f9bfcd7-f7499   1/1     Running   0          12s   192.168.104.10   node2   <none>           <none>
pod/myfirstdeployment-794f9bfcd7-kfzfk   1/1     Running   0          12s   192.168.104.11   node2   <none>           <none>

NAME                                           DESIRED   CURRENT   READY   AGE   CONTAINERS            IMAGES         SELECTOR
replicaset.apps/myfirstdeployment-794f9bfcd7   3         3         3       12s   myfirstpodcontainer   nginx:alpine   pod-template-hash=794f9bfcd7,example=myfirstdeployment

请注意,副本仅在某些节点上运行。这是因为其他节点上存在一些污点(默认情况下,某些 Kubernetes 部署会避免在主节点上运行工作负载)。污点和容忍帮助我们只允许在特定节点上调度 pods。在此示例中,主节点将不会运行工作负载,尽管它也有一个工作节点角色(它运行我们学习过的 Kubernetes 工作进程,即 kubeletkube-proxy)。这些特性让我们想起了 Docker Swarm 的节点可用性概念。实际上,我们也可以执行 kubectl cordon <NODE> 将节点设置为不可调度。

本章是 Kubernetes 主要概念的简要介绍。我们强烈建议您查看 Kubernetes 文档以获取更多信息:kubernetes.io

我们可以根据 pod 的性能和限制设置副本。这称为自动扩缩,它是 Docker Swarm 中没有的一个有趣特性。

当应用程序的副本组件需要持久性时,我们使用另一种资源类型。StatefulSets 保证 pods 的顺序和唯一性。

现在我们知道如何部署应用程序,让我们回顾一下 Kubernetes 如何本地管理和部署网络,并将组件分布在不同节点上。

Kubernetes 网络

Kubernetes 和其他编排工具一样,提供本地和分布式网络。Kubernetes 要完成几个重要的通信假设:

  • 容器到容器的通信

  • Pod 到 Pod 的通信

  • Pod 到 Service 的通信

  • 用户访问和外部或内部应用程序之间的通信

容器到容器的通信很简单,因为我们了解到,同一 pod 内的容器共享相同的 IP 和网络命名空间。

我们知道每个 pod 都有自己的 IP 地址。因此,Kubernetes 需要提供路由功能,以便从不同主机上运行的 pod 之间进行访问和通信。按照我们在 第四章《容器持久性与网络》中学到的 Docker 概念,Kubernetes 也使用桥接网络为在同一主机上运行的 pod 提供服务。因此,所有在同一主机上运行的 pod 都能通过桥接网络相互通信。

还记得 Docker 如何让我们在单一主机上部署不同的桥接网络吗?通过这种方式,我们可以使用不同的网络在主机上隔离应用程序。利用这一本地概念,覆盖网络在 Docker Swarm 集群上也会部署桥接接口。这些接口将通过在主机之间创建的 VXLAN 隧道进行连接。在 Docker 独立主机和 Docker Swarm 上,隔离变得简单。Docker 引擎必须管理所有后台的操作,使其与防火墙规则和路由配合工作,但覆盖网络是开箱即用的。

Kubernetes 提供了一种更简单的方法。所有 pod 都运行在同一网络中,因此每个 pod 都能看到同一主机上的其他 pod。实际上,我们可以更进一步——pod 可以从主机上本地访问。

让我们用几个 pod 来考虑这个概念。我们将同时运行 example-webserverexample-nettools,执行简单的 nginx:alpinefrjaraur/nettools:minimal(这是一个带有一些有用网络工具的小 alpine 镜像)pod。首先,我们将使用 kubectl create deploymentexample-webserver 创建一个部署:

$ kubectl create deployment example-webserver --image=nginx:alpine
deployment.apps/example-webserver created

我们使用 kubectl get pods 查看 pod 的 IP 地址:

$ kubectl get pods -o wide
NAME                                READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
example-webserver-7789c6d697-kts7l   1/1     Running   0          69s   192.168.104.16   node2   <none>           <none>

正如我们所说,localhost 与 pod 之间的通信是有效的。让我们从主机向 pod 的 IP 地址尝试一个简单的 ping 命令:

node3:~$ ping -c 2 192.168.104.16 
PING 192.168.104.16 (192.168.104.16) 56(84) bytes of data.
64 bytes from 192.168.104.16: icmp_seq=1 ttl=63 time=0.483 ms
64 bytes from 192.168.104.16: icmp_seq=2 ttl=63 time=0.887 ms

--- 192.168.104.16 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.483/0.685/0.887/0.202 ms

此外,我们还可以访问其正在运行的 nginx 进程。让我们再次尝试使用 pod 的 IP 地址进行 curl,但这次我们将使用端口 80

node3:~$ curl -I 192.168.104.16:80
HTTP/1.1 200 OK
Server: nginx/1.17.6
Date: Sun, 05 Jan 2020 22:20:42 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 19 Nov 2019 15:14:41 GMT
Connection: keep-alive
ETag: "5dd406e1-264"
Accept-Ranges: bytes

因此,主机可以与所有在 Docker 引擎上运行的 pod 进行通信。

我们可以使用 jsonpath 获取 pod 的 IP 地址,以格式化 pod 的信息输出,当我们有数百个 pod 时,这非常有用:kubectl get pod example-webserver -o jsonpath='{.status.podIP}'

让我们执行一个交互式 pod,使用前述的 frjaraur/nettools:minimal 镜像。我们将使用 kubectl run --generator=run-pod/v1 来执行这个新的 pod。注意,我们添加了 -ti -- sh 来在这个 pod 中运行一个交互式 shell。从这个 pod 中,我们将再次运行 curl,连接到 example-webserver pod 的 IP 地址:

$ kubectl run --generator=run-pod/v1 example-nettools --image=frjaraur/nettools:minimal -ti -- sh 
If you don't see a command prompt, try pressing enter.
/ # ping -c 2 192.168.104.16 
PING 192.168.104.16 (192.168.104.16): 56 data bytes
64 bytes from 192.168.104.16: seq=0 ttl=62 time=0.620 ms
64 bytes from 192.168.104.16: seq=1 ttl=62 time=0.474 ms

--- 192.168.104.16 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.474/0.547/0.620 ms

/ # curl -I 192.168.104.16:80
HTTP/1.1 200 OK
Server: nginx/1.17.6
Date: Sun, 05 Jan 2020 22:22:16 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 19 Nov 2019 15:14:41 GMT
Connection: keep-alive
ETag: "5dd406e1-264"
Accept-Ranges: bytes

我们已经成功地使用 pingcurl 访问了部署的 example-webserver pod,并向其运行的 nginx 进程发送了一些请求。很明显,两个容器可以相互看到。

在这个示例中,还有一个更有趣的地方:我们还没有查看这些 pod 是在哪些主机上运行的。事实上,它们运行在不同的主机上,正如我们从 kubectl get pods -o wide 命令的输出中可以看到的那样:

$ kubectl get pods -o wide
NAME                                READY   STATUS    RESTARTS   AGE    IP               NODE    NOMINATED NODE   READINESS GATES
example-nettools                     1/1     Running   1          85s    192.168.135.13   node3   <none>           <none>
example-webserver-7789c6d697-kts7l   1/1     Running   0          5m8s   192.168.104.16   node2   <none>           <none>

主机之间的网络通信由另一个组件控制,这个组件将允许这些分布式通信。在此情况下,这个组件是 Calico,它是应用于此 Kubernetes 集群的 容器网络接口 (CNI) 。Kubernetes 的网络模型提供了一个平面网络(所有 pod 都分布在同一网络上),而数据平面网络基于可互换的插件。我们将使用最适合我们环境所需特性的插件。

除了 Calico 之外,还有其他 CNI 实现,如 Flannel、Weave、Romana、Cilium 等。每个实现都提供自己的特性和主机到主机的实现。例如,Calico 使用 边界网关协议 (BGP) 来路由集群内的真实容器 IP 地址。一旦部署了 CNI,所有容器 IP 地址将由其实现进行管理。它们通常在 Kubernetes 集群实现的开始阶段进行部署。Calico 允许我们实施网络策略,这对于确保安全非常重要,因为在这个平面网络中,每个 pod 都能看到其他 pod。

我们还没有查看任何服务网络设置,这在这里也很重要。如果一个 pod 死掉了,新的 IP 地址将会分配,从而导致之前的 IP 地址无法访问;这就是我们使用服务的原因。记住,服务是 pod 的逻辑分组,通常具有虚拟 IP 地址。这个 IP 地址将从另一个 IP 地址池中分配(即服务 IP 地址池)。Pod 和服务不会共享同一个 IP 地址池。当新的 pod 被重新创建时,服务的 IP 地址不会改变。

服务发现

让我们创建一个与当前已部署的 example-webserver 部署相关联的服务。我们将使用 kubectl expose

$ kubectl expose deployment example-webserver \
--name example-webserver-svc --type=NodePort --port=80

service/example-webserver-svc exposed

我们本可以使用 kubectl create service(命令式格式)或 YAML 定义文件(声明式格式)来完成这项工作。我们使用了 kubectl expose,因为它更简单,可以快速发布任何类型的资源。我们可以使用 kubectl get services 查看服务的 IP 地址:

$ kubectl get services -o wide
NAME                   TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE   SELECTOR
kubernetes             ClusterIP   10.96.0.1      <none>        443/TCP        11h   <none>
example-webserver-svc   NodePort    10.98.107.31   <none>        80:30951/TCP   39s   app=example-webserver

记住,我们通过选择器定义与 pod 相关联的服务。在此情况下,服务将把所有带有 app 标签和 example-webserver 值的 pod 归为一组。这个标签是自动创建的,因为我们创建了 Deployment。因此,所有为此服务分组的 pod 都可以通过 10.98.107.31 IP 地址和内部 TCP 端口 80 进行访问。我们定义了哪个 pod 的端口会与此服务关联——在这两种情况下,我们设置了端口 80

$ curl -I 10.98.107.31:80
HTTP/1.1 200 OK
Server: nginx/1.17.6
Date: Sun, 05 Jan 2020 22:26:09 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 19 Nov 2019 15:14:41 GMT
Connection: keep-alive
ETag: "5dd406e1-264"
Accept-Ranges: bytes

它按预期可访问。Kubernetes 的内部网络已将此服务发布到定义的 ClusterIP 地址。

因为我们创建了一个 NodePort 服务,所以该服务已与一个随机端口关联。在这种情况下,它是端口 30951。因此,当我们通过随机选择的端口访问集群节点的 IP 地址时,请求将被路由到集群内应用程序的 pod。

NodePort 端口默认是随机分配的,但我们可以手动设置它们,范围在 3000032767 之间。

让我们验证这个功能。我们将向集群节点上监听的端口发送一些请求。在这个例子中,我们将使用 curl 命令通过本地 0.0.0.0 IP 地址和端口 30951 在不同的节点上进行测试:

node1:~$ curl -I 0.0.0.0:30951
HTTP/1.1 200 OK
Server: nginx/1.17.6
Date: Sun, 05 Jan 2020 22:26:57 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 19 Nov 2019 15:14:41 GMT
Connection: keep-alive
ETag: "5dd406e1-264"
Accept-Ranges: bytes

node3:~$ curl -I 0.0.0.0:30951
HTTP/1.1 200 OK
Server: nginx/1.17.6
Date: Sun, 05 Jan 2020 22:27:41 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 19 Nov 2019 15:14:41 GMT
Connection: keep-alive
ETag: "5dd406e1-264"
Accept-Ranges: bytes

即使 pod 没有运行在同一节点上,它们之间也可以进行通信。以下输出显示 pod 并没有运行在 node1node3 上,应用程序的 pod 运行在 node2 上,内部路由正常工作:

$ kubectl get pods -o wide -l app=example-webserver
NAME                                READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
example-webserver-7789c6d697-kts7l   1/1     Running   0          10m   192.168.104.16   node2   <none>           <none>

还有一些更有趣的事情——服务会创建一个以其名称为基础的 DNS 记录,格式如下:

<SERVICE_NAME>.<NAMESPACE>.svc.<CLUSTER>.<DOMAIN>

在我们的例子中,我们没有使用命名空间或域名。服务解析将是简单的:example-webserver.default.svc.cluster.local。这种解析默认只有在 Kubernetes 集群内可用。因此,我们可以通过执行一个 pod 并使用 hostnslookup 工具来测试此解析。我们将通过 kubectl attach 交互式连接到运行中的 example-nettools pod,并运行 hostcurl 来测试 DNS 解析:

$ kubectl attach example-nettools -c example-nettools -i -t
If you don't see a command prompt, try pressing enter.
/ # host example-webserver.default.svc.cluster.local
example-webserver.default.svc.cluster.local has address 10.101.195.251
/ # curl -I example-webserver.default.svc.cluster.local:80
HTTP/1.1 200 OK
Server: nginx/1.17.6
Date: Sun, 05 Jan 2020 21:58:37 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 19 Nov 2019 15:14:41 GMT
Connection: keep-alive
ETag: "5dd406e1-264"
Accept-Ranges: bytes

我们已经确认该服务具有一个可以被任何其他 Kubernetes 集群资源访问的 DNS 记录。我们还通过 NodePort 发布了该服务,因此它可以通过任何节点的 IP 地址访问。我们可以通过外部负载均衡器将请求路由到集群中任一节点的 IP 地址以及选择的(或手动设置的)端口。此端口将在该服务存在期间保持固定,直到它被移除。

请注意,我们使用 kubectl attach example-nettools -c example-nettools -i -t 重新连接到一个在后台运行的 pod。

在接下来的章节中,我们将学习扩容如何改变描述的行为。

负载均衡

如果我们现在将副本数扩展到三个,而不更改任何已部署的服务,我们将添加负载均衡功能。让我们使用 kubectl scale 来扩容:

$ kubectl scale --replicas=3 deployment/example-webserver
deployment.extensions/example-webserver scaled

现在,我们将有三个运行中的实例或 pod 来支持 example-webserver 部署。

请注意,我们已经通过命令行使用资源类型和名称来进行扩缩容:kubectl scale --replicas=<NUMBER_OF_REPLICAS> <RESOURCE_TYPE>/<NAME>

我们可以通过 kubectl get pods 和相关标签来查看部署的 pod:

$ kubectl get pods -o wide -l app=example-webserver
NAME                                READY   STATUS    RESTARTS   AGE    IP               NODE    NOMINATED NODE   READINESS GATES
example-webserver-7789c6d697-dnx6l   1/1     Running   0          4m8s   192.168.135.14   node3   <none>           <none>
example-webserver-7789c6d697-kts7l   1/1     Running   0          23m    192.168.104.16   node2   <none>           <none>
example-webserver-7789c6d697-zdrtr   1/1     Running   0          4m8s   192.168.104.17   node2   <none>           <none>

如果我们现在再次测试服务的访问,我们将访问到三个副本中的每一个。我们执行下一个简单的循环,以便五次访问服务的后端 pod:

$ for I in $(seq 5);do curl -I 10.98.107.31:80;done

如果我们使用 kubectl logs 查看部署的 pod 日志,我们会发现并不是所有请求都被记录。尽管我们使用服务的 IP 地址发出了超过两个请求,但日志中只记录了几个请求:

$ kubectl logs example-webserver-7789c6d697-zdrtr
192.168.166.128 - - [05/Jan/2020:22:44:32 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.47.0" "-"
192.168.166.128 - - [05/Jan/2020:22:45:38 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.47.0" "-"

每个 pod 仅记录了每个请求的三分之一;因此,内部负载均衡器在所有可用应用程序的 pod 之间分发流量。内部负载均衡默认在与服务关联的所有 pod 之间部署。

正如我们所见,Kubernetes 为 pod 和服务提供了扁平网络,简化了网络和内部应用的可访问性。另一方面,由于任何 pod 都可以访问任何其他 pod 或服务,因此它是不安全的。在接下来的部分,我们将学习如何避免这种情况。

网络策略

网络策略定义了允许组内 pod 和其他组件通信的规则。使用标签,我们可以为匹配的 pod 应用特定规则,用于定义端口上的入口和出口流量。这些规则可以使用 IP 范围、命名空间,甚至其他标签来包含或排除资源。

网络策略是通过网络插件应用的;因此,我们集群上部署的 CNI 必须支持它们。例如,Calico 支持NetworkPolicy资源。

我们将能够为集群中所有 pod 定义默认规则,隔离所有 Internet 流量,例如,或一组定义的主机。

这个 YAML 文件代表了一个应用入口和出口流量规则的NetworkPolicy资源示例:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: database-traffic
spec:
  podSelector:
    matchLabels:
      tier: database
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - ipBlock:
        cidr: 172.17.10.0/24
    - podSelector:
        matchLabels:
          tier: frontend
    ports:
    - protocol: TCP
      port: 5432
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
    ports:
    - protocol: TCP
      port: 5978

在这个示例中,我们将为包括具有database值的tier标签的所有 pod 应用定义的入口和出口规则。

入口规则允许来自同一命名空间中具有tier标签和frontend值的任何 pod 的流量。也将允许访问定义的database pods 上的子网172.17.10.0/24中的所有 IP 地址。

出口规则允许从定义的database pods 到子网10.0.0.0/24上所有 IP 地址的端口5978的流量。

如果我们没有在命名空间中应用NetworkPolicy资源,那么所有流量都是允许的。我们可以使用podSelector: {}来改变这种行为。这将匹配命名空间中的所有 pod。例如,为了禁止所有出口流量,我们可以使用以下NetworkPolicy YAML 定义:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
  - Egress

因此,我们已经学到,即使在 Kubernetes 的扁平网络上,我们也可以通过NetworkPolicy资源确保安全。让我们来回顾一下入口资源。

发布应用程序

Ingress 资源帮助我们在 Kubernetes 集群上发布部署的应用程序。它们与 HTTP 和 HTTPS 服务非常配合,提供许多功能来在服务之间分发和管理流量。这些流量将位于 OSI 模型的传输和应用层,它们也被称为第 4 层和第 7 层。它还与原始 TCP 和 UDP 服务一起工作;但在这些情况下,流量将仅在第 4 层进行负载平衡。

这些资源将流量从集群外部路由到集群内运行的服务。Ingress 资源需要一个名为 ingress 控制器 的特殊服务。这些服务将使用 ingress 资源创建的规则进行负载均衡或路由流量。因此,使用此功能发布应用程序需要两个组件:

  • Ingress 资源:应用于传入流量的规则。

  • Ingress 控制器:一个负载均衡器,它会自动将 ingress 规则转换或翻译为负载均衡配置。

两者的结合提供了应用程序的动态发布。如果某个应用程序的 pod 死亡,会自动创建一个新的 pod,服务和 ingress 控制器将自动将所有流量路由到新的 pod。这也将服务与外部网络隔离。我们将只发布一个端点,而不是为所有服务使用 NodePortLoadBalancer 服务类型,这样可以节省许多节点的端口或云 IP 地址。这个端点就是负载均衡器,它将使用 ingress 控制器和 ingress 资源规则,将流量内部路由到已部署的服务:

本章的实验向我们展示了一个有趣的负载均衡示例,使用了 NGINX Ingress 控制器。让我们快速回顾一下示例的 YAML 配置文件:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: simple-fanout-example
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: example.local
    http:
      paths:
      - path: /example1
        backend:
          serviceName: example-webserver
          servicePort: 80
      - path: /example2
        backend:
          serviceName: another-service
          servicePort: 8080

本示例概述了应应用的规则,以便将请求路由到特定的 example.local 主机头。任何 URL 中包含 /example1 的请求将被引导到 example-webserver,而包含 /example2 路径的请求则会转发到 another-service。注意,我们使用了内部服务的端口,因此不需要额外的服务曝光。一个 ingress 控制器端点将会把流量重定向到 example-webserveranother-service 服务。这节省了主机的端口(以及云提供商上的 IP 地址,因为 LoadBalancer 服务类型每个服务使用一个公开的 IP 地址)。

我们可以根据需要提供多个 ingress 控制器。实际上,在多租户环境中,我们通常会部署不止一个控制器,以便在不同租户之间隔离发布平面。

这一简要的 Kubernetes 应用程序发布介绍,已完成对 Kubernetes 主要网络功能的回顾。接下来,我们将转向 Kubernetes 的安全特性。

Kubernetes 的安全组件和特性

Kubernetes 提供了认证和授权访问其 API 的机制。这使我们能够为集群内的用户或角色应用不同级别的权限。这可以防止未经授权的访问某些核心资源,如调度或集群中的节点。

一旦用户被允许使用集群资源,我们使用命名空间将他们的资源与其他用户的资源隔离开来。这在多租户环境中也能有效工作,尤其是在需要更高安全性的场景下。

Kubernetes 与非常精细的基于角色的访问控制RBAC)环境配合使用,提供了很高的粒度,允许对某些资源执行特定操作,同时拒绝其他操作。

我们管理RoleClusterRole资源,以描述不同资源的权限。我们使用Role来定义命名空间内的权限,使用ClusterRole来定义集群范围内的资源权限。规则通过一些已定义的动词提供,例如listgetupdate等,这些动词作用于资源(甚至特定的资源名称)。RoleBindingClusterRoleBinding资源将角色中定义的权限授予用户或用户集合。

Kubernetes 还提供以下功能:

  • 服务账户用于标识 pod 内的进程对其他资源的访问

  • Pod 安全策略控制 pod 的特殊行为,例如特权容器、主机命名空间、限制以 root 用户运行容器,或启用容器的只读根文件系统等功能。

  • 审批控制器拦截 API 请求,允许我们验证或修改请求,以确保镜像的新鲜度和安全性,强制创建的 pod 始终从注册表拉取,设置默认存储,禁止在特权容器中执行进程,或在未声明时指定默认的主机资源限制范围等其他安全功能。

在生产环境中限制主机的资源使用非常重要,因为未限制的 pod 默认可以消耗所有资源。

Kubernetes 提供了许多功能,确保集群在各个层级的安全。是否使用这些功能由你决定,因为大多数功能默认情况下不会启用。我们将在第十一章中学习更多关于角色和权限应用于资源的内容,通用控制平面,因为许多这些配置已经集成到 Docker Enterprise 中。

我们不会深入探讨这个话题,因为 Kubernetes 不是当前 Docker 认证助理课程的一部分,这只是一个简要的介绍。

建议你更仔细地查看 Kubernetes 的安全功能,因为它比 Docker Swarm 有更多的安全特性。另一方面,确实 Docker Enterprise 为 Docker Swarm 提供了许多这些功能。

将 Docker Swarm 和 Kubernetes 进行并排比较

在本节中,我们将对比 Docker Swarm 和 Kubernetes 的功能,以便更好地理解它们如何解决常见问题。我们在本章和第八章,使用 Docker Swarm 进行编排中都讨论了这些概念。它们在许多问题上有共同的解决方法:

参数 Docker Swarm Kubernetes
高可用性解决方案 为核心组件提供高可用性。 为核心组件提供高可用性。
弹性 所有服务根据状态定义以弹性方式运行。 基于复制控制器的所有资源将根据状态定义提供弹性(ReplicaSetDaemonSetDeploymentStatefulSet)。
基础设施即代码 Docker Compose 文件格式将允许我们部署堆栈。 我们将使用 YAML 格式化资源文件,这将允许我们使用声明性格式部署工作负载。
动态分配 应用程序组件及其副本将自动在整个集群中分配,尽管我们可以提供一些约束。 Kubernetes 也会分配组件,但我们可以使用标签和其他功能提供高级约束。
自动更新 应用程序组件可以使用滚动更新和回滚在发生故障时进行升级。 Kubernetes 也提供滚动更新和回滚功能。
发布应用程序 Docker Swarm 在服务副本之间提供内部负载均衡,并通过路由网格同时在所有集群节点上发布应用程序的服务端口。 Kubernetes 也提供内部负载均衡,NodePort 类型服务也会在所有节点上同时发布应用程序的组件。但 Kubernetes 还提供负载均衡服务(以及其他类型的服务),可以自动配置外部负载均衡器来将请求路由到已部署的服务。
集群内部网络 部署为每个服务任务的容器可以与同一网络中部署的其他容器通信。内部 IP 管理将提供其 IP 地址,服务可以通过其名称进行访问,从而实现内部 DNS 解析。 Pod 之间的通信正常工作,IP 地址由内部互联网协议地址管理(IPAM)提供。我们还将实现服务到服务的通信和解析。
键值存储 Docker Swarm 提供一个内部存储来管理所有对象及其状态。该存储将具有高可用性,并且需要一个奇数个主节点。 Kubernetes 也需要一个键值存储来管理其资源。此组件使用 etcd 提供,并且我们可以将其部署在 Kubernetes 集群节点之外。我们应提供一个奇数个 etcd 节点以提供高可用性。

上述表格展示了我们在解决常见问题方面的主要相似之处。接下来的表格将展示主要差异:

参数 Docker Swarm Kubernetes
Pods 与任务 Docker Swarm 为服务部署任务。每个任务一次只能运行一个容器。如果容器崩溃,会创建一个新的容器以确保所需的副本数(任务)。服务是最小的部署单元。我们将部署运行其组件的应用程序作为服务。 Kubernetes 有 pod 的概念。每个 pod 可以运行多个容器,并且它们共享相同的 IP 地址(网络命名空间)。pod 内的容器共享卷,并且始终运行在相同的主机上。pod 的生命周期依赖于容器。如果其中一个容器崩溃,pod 会变得不健康。pod 是 Kubernetes 中最小的部署单元;因此,我们通过扩缩 pods 来调整它们的规模,包括所有容器。
服务 Docker Swarm 中的服务是具有 IP 地址的对象,用于在副本之间进行内部负载均衡(默认情况下,我们可以使用 dnsrr 端点模式避免此情况)。我们创建服务来执行应用程序组件,并根据需要扩展或缩减副本数量,以确保服务健康。 在 Kubernetes 中,服务有所不同。它们是逻辑资源。这意味着它们仅用于发布一组 pod 资源。Kubernetes 服务是共同工作的 pod 的逻辑分组。Kubernetes 服务还会获取一个 IP 地址用于内部负载均衡(clusterIP),我们也可以通过使用“无头”功能来避免这种情况。
网络 Docker Swarm 默认部署覆盖网络。这确保了应用程序的组件在不同主机上部署时能相互通信。Docker Swarm 中的堆栈将部署在不同的网络上。这意味着我们可以为每个应用程序提供一个子网。多个网络的部署能提供较好的安全性,因为它们彼此隔离。通过使用可用的网络加密(默认禁用),可以进一步提高安全性。然而,另一方面,它们很难管理,当我们需要为集成到多个堆栈中的服务提供隔离时,事情会变得复杂。 Kubernetes 提供了一个扁平网络,使用一个名为 CNI 的公共接口。网络已从 Kubernetes 核心解耦,以允许我们使用多种不同的网络解决方案。每个解决方案都有其特性和在集群环境中的路由实现。扁平网络简化了操作,所有的 pod 和服务默认都能互相看到。另一方面,安全性没有默认提供。我们将部署 NetworkPolicy 资源,以确保集群内资源之间的安全通信。这些策略将管理在 Kubernetes 环境中谁可以与谁通信。
身份验证和授权 Docker Swarm 默认不提供任何机制来验证或授权特定请求。一旦 Docker Swarm 节点公开了其守护进程访问权限(在 daemon.json 配置文件中),任何人都可以连接到它并管理集群(如果我们使用管理节点)。这是一种安全风险,应始终避免。我们可以使用 SSL/TLS 证书创建安全的客户端配置。但在 Docker Swarm 中,证书只确保安全的通信,并不会进行授权验证。Docker Enterprise 会提供所需的功能,为 Docker Swarm 集群提供 RBAC。 Kubernetes 确实提供了身份验证和授权功能。实际上,它包含一个功能齐全的 RBAC 系统来管理用户和应用程序对 Kubernetes 集群中资源的访问。这个 RBAC 系统允许我们为用户或团队的访问设置特定权限。使用 Kubernetes 命名空间也将在多租户或团队场景中提高安全性。
密钥 Docker 默认情况下加密密钥。它们仅在容器运行时可读。 Kubernetes 默认使用 Base64 算法对密钥进行编码。我们需要使用外部密钥提供者或额外的加密配置(EncryptionConfig)来确保密钥的完整性。
发布应用程序 Docker Swarm 仅提供用于发布应用程序的路由网格。这将把应用程序端口发布到所有集群节点。这可能不安全,因为所有节点都会发布所有应用程序,我们将使用大量端口(至少每个已发布的应用程序一个端口)。Docker Enterprise 将提供 Interlock,它具有与入口控制器相似的许多功能。 Kubernetes 提供了入口控制器资源。入口控制器发布一些端点(使用 NodePort 或任何其他云服务定义),这些内部入口将与服务的后端(pods)进行通信。这将需要更少的端口来发布应用程序(仅需要发布入口控制器的端口)。请求将通过这些资源路由到实际的后端服务。由于我们在请求中间添加了一种智能软件,帮助我们决定哪些后端将处理请求,因此安全性得到了提升。入口控制器充当反向代理,并且会验证每个请求是否使用了有效的主机头。如果没有使用,请求将被转发到默认的后端。如果请求包含有效的头部,它们将被转发到定义的服务虚拟 IP,内部负载均衡器将选择哪个 pod 最终接收它们。调度器将管理已定义的规则和集群,内部或外部负载均衡器将解读这些规则,以确保正确的后端接收到用户的请求。

到目前为止,我们已经学习了 Docker Swarm 和 Kubernetes 之间的一些相似点和差异。我们可以注意到以下几点:

  • Kubernetes 提供了更高的容器密度。

  • Docker Swarm 默认提供集群范围的网络,并使用子网进行隔离。

  • Kubernetes 提供基于角色的集群资源访问。

  • 在 Kubernetes 中发布应用时,最好使用 ingress 控制器。

现在让我们通过将所学应用到一些简单的实验中来回顾一些我们已经学过的主题。

章节实验

我们现在将进行一个较长的实验,这将帮助我们回顾迄今为止所学的概念。

如果你还没有这样做,请从本书的 GitHub 仓库中部署environments/kubernetesgithub.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git)。你也可以使用自己的 Linux 服务器。从environments/kubernetes文件夹中运行vagrant up来启动你的虚拟环境。这些实验中使用的所有文件可以在chapter9文件夹中找到。

等待所有节点都启动。我们可以通过vagrant status来检查节点的状态。使用vagrant ssh kubernetes-node1连接到你的实验节点。Vagrant 会为你部署三个节点,你将使用vagrant用户,并通过sudo获得 root 权限。你应该看到如下输出:

Docker-Certified-Associate-DCA-Exam-Guide/environments/kubernetes$ vagrant up
--------------------------------------------------------------------------------------------
 KUBERNETES Vagrant Environment
 Engine Version: current
 Kubernetes Version: 1.14.0-00
 Kubernetes CNI: https://docs.projectcalico.org/v3.8/manifests/calico.yaml
--------------------------------------------------------------------------------------------
Bringing machine 'kubernetes-node1' up with 'virtualbox' provider...
Bringing machine 'kubernetes-node2' up with 'virtualbox' provider...
Bringing machine 'kubernetes-node3' up with 'virtualbox' provider... 
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/kubernetes$

节点将有三个接口(IP 地址和虚拟硬件资源可以通过修改config.yml文件来进行更改):

  • eth0 [10.0.2.15]:这是一个内部接口,Vagrant 所必需。

  • eth1 [10.10.10.X/24]:这是为 Docker Kubernetes 内部通信准备的。第一个节点将获得10.10.10.11 IP 地址,依此类推。

  • eth2 [192.168.56.X/24]:这是一个仅限主机的接口,用于主机与虚拟节点之间的通信。第一个节点将获得192.168.56.11 IP 地址,依此类推。

我们将使用eth1接口来进行 Kubernetes,并且我们将能够使用192.168.56.X/24 IP 地址范围来连接已发布的应用。所有节点都安装了 Docker Engine Community Edition,并且允许 Vagrant 用户执行docker命令。一个小型的 Kubernetes 集群将为你部署,其中包含一个主节点(kubernetes-node1)和两个工作节点(kubernetes-node2kubernetes-node3)。

我们现在可以使用vagrant ssh kubernetes-node1连接到第一个已部署的虚拟节点。如果你已经部署了 Kubernetes 虚拟环境并且只是刚刚使用vagrant up启动它,过程可能会有所不同:

Docker-Certified-Associate-DCA-Exam-Guide/environments/kubernetes$ vagrant ssh kubernetes-node1
vagrant@kubernetes-node1:~$

现在你已经准备好开始实验了。我们将通过部署一个简单的应用程序来开始这些实验。

在 Kubernetes 中部署应用

一旦 Vagrant(或你自己的环境)部署完成,我们将拥有三个节点(命名为kubernetes-node<index>,从13),它们将安装 Ubuntu Xenial 和 Docker Engine。Kubernetes 也会为你启动并运行,包含一个主节点和两个工作节点。Calico CNI 也会为你自动部署。

首先,检查您的节点 IP 地址(如果您使用了 Vagrant,范围是10.10.10.1110.10.10.13,因为第一个接口将是 Vagrant-internal)。

部署应用的步骤如下:

  1. 连接到kubernetes-node1并使用kubectl get nodes查看已部署的 Kubernetes 集群。一个名为config的文件(包含所需的凭据和 Kubernetes API 端点)将自动复制到~/.kube目录下。我们也将此文件称为Kubeconfig。该文件为您配置kubectl命令行:
Docker-Certified-Associate-DCA-Exam-Guide/environments/kubernetes$ vagrant ssh kubernetes-node1

vagrant@kubernetes-node1:~$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
kubernetes-node1 Ready master 6m52s v1.14.0
kubernetes-node2 Ready <none> 3m57s v1.14.0
kubernetes-node3 Ready <none> 103s v1.14.0 

Kubernetes 集群版本 1.14.00 已部署并正在运行。请注意,kubernetes-node1是该集群中唯一的主节点;因此,我们没有提供高可用性。

目前,我们使用的是admin用户,默认情况下,所有部署将运行在default命名空间,除非另行指定。此配置也在~/.kube/config文件中完成。

Calico CNI 也已部署,因此,集群范围内的主机与容器的网络连接应该是正常的。

  1. 使用您喜欢的编辑器创建一个名为blue-deployment-simple.yaml的部署文件,内容如下:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: blue-app
  labels:
    color: blue
    example: blue-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: blue
  template:
    metadata:
      labels:
        app: blue
    spec:
      containers:
      - name: blue
        image: codegazers/colors:1.12
        env:
        - name: COLOR
          value: blue
        ports:
        - containerPort: 3000

这将部署codegazers/colors:1.12镜像的两个副本。部署后,我们预计会看到两个运行中的 Pod。我们将COLOR环境变量设置为blue,因此,所有应用组件都会是blue。容器将在集群内部暴露端口3000

  1. 使用kubectl create -f <KUBERNETES_RESOURCES_FILE>.yaml部署这个blue-app应用:
vagrant@kubernetes-node1:~$ kubectl create -f blue-deployment-simple.yaml
deployment.extensions/blue-app created

该命令行创建了一个名为blue-app的部署,并且有两个副本。让我们使用kubectl get deployments查看已创建的部署:

vagrant@kubernetes-node1:~$ kubectl get deployments -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
blue-app 2/2 2 2 103s blue codegazers/colors:1.12 app=blue

因此,将有两个 Pod 与blue-app部署关联。让我们使用kubectl get pods查看已部署的 Pod:

vagrant@kubernetes-node1:~$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
blue-app-54485c74fc-wgw7r 1/1 Running 0 2m8s 192.168.135.2 kubernetes-node3 <none> <none>
blue-app-54485c74fc-x8p92 1/1 Running 0 2m8s 192.168.104.2 kubernetes-node2 <none> <none>

在这种情况下,一个 Pod 运行在kubernetes-node2上,另一个 Pod 运行在kubernetes-node3上。让我们尝试连接到它们的虚拟分配 IP 地址,使用暴露的端口。请记住,IP 地址会随机分配,因此在您的环境中可能会有所不同。我们将使用curl命令连接到kubernetes-node1的 IP 地址和 Pod 的内部端口:

vagrant@kubernetes-node1:~$ curl 192.168.104.2:3000/text
APP_VERSION: 1.0
COLOR: blue
CONTAINER_NAME: blue-app-54485c74fc-x8p92
CONTAINER_IP: 192.168.104.2
CLIENT_IP: ::ffff:192.168.166.128
CONTAINER_ARCH: linux

我们可以从kubernetes-node1正确连接到其他主机上运行的 Pod。因此,Calico 工作正常。

我们应该能够连接到任何 Pod 的部署 IP 地址。每当容器死亡并且新 Pod 部署时,这些 IP 地址将发生变化。我们永远不会直接连接 Pod 以消费其应用程序进程。正如我们在本章中已经讨论过的,我们将使用服务而不是 Pod 来发布应用程序。这样,当作为 Pod 运行的应用组件需要重新创建时,它们的 IP 地址将不会发生变化。

  1. 让我们创建一个服务,用于在已部署的 Pod 之间进行负载均衡,并为其分配一个固定的虚拟 IP 地址。创建一个名为blue-service-simple.yaml的文件,内容如下:
apiVersion: v1
kind: Service
metadata:
  name: blue-svc
spec:
  ports:
  - port: 80
    targetPort: 3000
    protocol: TCP
    name: http
  selector:
    app: blue

一个随机的 IP 地址将与该服务关联。这个 IP 地址将是固定的,即使 Pod 死亡,它仍然有效。注意,我们为该服务暴露了一个新的端口。这个端口将是服务的端口,访问定义端口 80 的请求将被路由到每个 Pod 上的 3000 端口。我们将使用 kubectl get svc 获取服务的端口和 IP 地址:

vagrant@kubernetes-node1:~$ kubectl create -f blue-service-simple.yaml
service/blue-svc created

vagrant@kubernetes-node1:~$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
blue-svc ClusterIP 10.100.207.49 <none> 80/TCP 7s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 53m
  1. 我们将通过使用 curl 访问其 IP 地址的 blue-svc 服务来验证内部负载均衡,访问端口 80
vagrant@kubernetes-node1:~$ curl 10.100.207.49:80/text
APP_VERSION: 1.0
COLOR: blue
CONTAINER_NAME: blue-app-54485c74fc-x8p92
CONTAINER_IP: 192.168.104.2
CLIENT_IP: ::ffff:192.168.166.128
CONTAINER_ARCH: linux
  1. 让我们再次尝试使用 curl。我们将通过向服务的 IP 地址和端口发送请求来测试内部负载均衡:
vagrant@kubernetes-node1:~$ curl 10.100.207.49:80/text
APP_VERSION: 1.0
COLOR: blue
CONTAINER_NAME: blue-app-54485c74fc-wgw7r
CONTAINER_IP: 192.168.135.2
CLIENT_IP: ::ffff:192.168.166.128
CONTAINER_ARCH: linux

该服务已在两个 Pod 之间负载均衡了我们的请求。现在让我们尝试将此服务暴露出来,让应用程序的用户能够访问它。

  1. 现在我们将删除之前服务的定义,并部署一个新的服务,使用 NodePort 类型。我们将使用 kubectl delete -f <KUBERNETES_RESOURCES_FILE>.yaml
vagrant@kubernetes-node1:~$ kubectl delete -f blue-service-simple.yaml
service "blue-svc" deleted

创建一个新的定义文件 blue-service-nodeport.yaml,其内容如下:

apiVersion: v1
kind: Service
metadata:
  name: blue-svc
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 3000
    protocol: TCP
    name: http
  selector:
    app: blue
  1. 我们现在只需创建一个服务定义,并注意到与之关联的随机端口。部署完成后,我们还将使用 kubectl createkubectl get svc
vagrant@kubernetes-node1:~$ kubectl create -f blue-service-nodeport.yaml
service/blue-svc created

vagrant@kubernetes-node1:~$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
blue-svc NodePort 10.100.179.60 <none> 80:32648/TCP 5s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 58m
  1. 我们了解到,NodePort 服务将充当 Docker Swarm 的路由网格。因此,该服务的端口将在每个节点上固定。让我们使用 curl 来验证此功能,访问任何节点的 IP 地址和分配的端口。在本例中,它是 32648。这个端口在您的环境中可能会有所不同,因为它会动态分配:
vagrant@kubernetes-node1:~$ curl 0.0.0.0:32648/text
APP_VERSION: 1.0
COLOR: blue
CONTAINER_NAME: blue-app-54485c74fc-x8p92
CONTAINER_IP: 192.168.104.2
CLIENT_IP: ::ffff:192.168.166.128
CONTAINER_ARCH: linux
  1. 在本地,node1 的端口 32648 上可以访问该服务。它应该可以在同一端口上在任何节点上访问。例如,我们可以尝试在 node3 上使用 curl
vagrant@kubernetes-node3:~$ curl 10.10.10.13:32648/text
APP_VERSION: 1.0
COLOR: blue
CONTAINER_NAME: blue-app-54485c74fc-wgw7r
CONTAINER_IP: 192.168.135.2
CLIENT_IP: ::ffff:10.0.2.15
CONTAINER_ARCH: linux

我们了解到,即使一个节点没有运行相关工作负载,服务仍然可以通过定义的(或者在这种情况下是随机的)端口使用 NodePort 进行访问。

  1. 我们将通过升级部署镜像到新版本来完成这个实验。我们将使用 kubectl set image deployment
vagrant@kubernetes-node1:~$ kubectl set image deployment blue-app blue=codegazers/colors:1.15
deployment.extensions/blue-app image updated
  1. 让我们再次查看部署,验证更新是否完成。我们将使用 kubectl get all -o wide 获取所有已创建的资源及其位置:
vagrant@kubernetes-node1:~$ kubectl get all -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/blue-app-787648f786-4tz5b 1/1 Running 0 76s 192.168.104.3 node2 <none> <none>
pod/blue-app-787648f786-98bmf 1/1 Running 0 76s 192.168.135.3 node3 <none> <none>

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/blue-svc NodePort 10.100.179.60 <none> 80:32648/TCP 22m app=blue
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 81m <none>

NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/blue-app 2/2 2 2 52m blue codegazers/colors:1.15 app=blue

NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
replicaset.apps/blue-app-54485c74fc 0 0 0 52m blue codegazers/colors:1.12 app=blue,pod-template-hash=54485c74fc
replicaset.apps/blue-app-787648f786 2 2 2 76s blue codegazers/colors:1.15 app=blue,pod-template-hash=787648f786
  1. 注意,新创建的 Pod 使用了更新的镜像。我们可以通过 kubectl rollout status 来验证更新:
vagrant@kubernetes-node1:~$ kubectl rollout status deployment.apps/blue-app
deployment "blue-app" successfully rolled out
  1. 我们只需执行 kubectl rollout undo 就可以回到之前的镜像版本。让我们回到之前的镜像版本:
vagrant@kubernetes-node1:~$ kubectl rollout undo deployment.apps/blue-app
deployment.apps/blue-app rolled back
  1. 现在,我们可以验证当前的 blue-app 部署是否再次运行 codegazers/colors:1.12 镜像。我们将再次使用 kubectl get all 来检查部署位置:
vagrant@kubernetes-node1:~$ kubectl get all -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/blue-app-54485c74fc-kslgw 1/1 Running 0 62s 192.168.104.4 node2 <none> <none>
pod/blue-app-54485c74fc-lrkxv 1/1 Running 0 62s 192.168.135.4 node3 <none> <none>

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/blue-svc NodePort 10.100.179.60 <none> 80:32648/TCP 29m app=blue
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 87m <none>

NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/blue-app 2/2 2 2 58m blue codegazers/colors:1.12 app=blue

NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
replicaset.apps/blue-app-54485c74fc 2 2 2 58m blue codegazers/colors:1.12 app=blue,pod-template-hash=54485c74fc
replicaset.apps/blue-app-787648f786 0 0 0 7m46s blue codegazers/colors:1.15 app=blue,pod-template-hash=787648f786

返回到之前的状态非常简单。

我们可以使用 --record 选项为每次更改设置注释,记录在 update 命令中。

使用卷

在这个实验中,我们将使用不同的卷部署一个简单的 Web 服务器。我们将使用 webserver.deployment.yaml

我们已经准备好了以下卷:

  • congigMap: 配置卷,路径为/etc/nginx/conf.d/default.conf(配置文件)

  • emptyDir: NGINX 日志的空卷,路径为/var/log/nginx

  • secret: 秘密卷,用于指定一些变量来组成index.html页面

  • persistentVolumeClaim: 数据卷绑定到使用主机/mnt内容定义的persistentVolumehostPath

我们已经为我们的 Web 服务器声明了一个特定节点,以确保index.html文件位于/mnt目录下。我们在部署文件webserver.deployment.yaml中使用了nodeName: kubernetes-node2

  1. 首先,我们验证在kubernetes-node2节点的/mnt目录下没有文件。我们连接到kubernetes-node2,然后查看/mnt目录的内容:
$ vagrant ssh kubernetes-node2

vagrant@kubernetes-node2:~$ ls  /mnt/
  1. 然后,我们切换到kubernetes-node1来克隆我们的仓库并启动 Web 服务器部署:
$ vagrant ssh kubernetes-node1

vagrant@kubernetes-node1:~$ git clone https://github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

我们进入chapter9/nginx-lab/yaml目录:

vagrant@kubernetes-node1:~$ cd Docker-Certified-Associate-DCA-Exam-Guide/chapter9/nginx-lab/yaml/
vagrant@kubernetes-node1:~/Docker-Certified-Associate-DCA-Exam-Guide/chapter9/nginx-lab/yaml$
  1. 我们将在本实验中使用ConfigMapSecretServicePersistentVolumePersistentVolumeClaim资源,并通过 YAML 文件部署它们。所有资源文件将部署在yaml目录下:
vagrant@kubernetes-node1:~/Docker-Certified-Associate-DCA-Exam-Guide/chapter9/nginx-lab/yaml$ kubectl create -f .
configmap/webserver-test-config created
deployment.apps/webserver created
persistentvolume/webserver-pv created
persistentvolumeclaim/werbserver-pvc created
secret/webserver-secret created
service/webserver-svc created
  1. 现在,我们将回顾所有创建的资源。我们没有定义命名空间,因此将使用default命名空间(我们在命令中省略了它,因为它是默认命名空间)。我们将使用kubectl get all列出默认命名空间中所有可用的资源:
vagrant@kubernetes-node1:~/Docker-Certified-Associate-DCA-Exam-Guide/chapter9/nginx-lab/yaml$ kubectl get all
NAME                            READY   STATUS    RESTARTS   AGE
pod/webserver-d7fbbf4b7-rhvvn   1/1     Running   0          31s
NAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/kubernetes      ClusterIP   10.96.0.1       <none>        443/TCP        107m
service/webserver-svc   NodePort    10.97.146.192   <none>        80:30080/TCP   31s
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/webserver   1/1     1            1           31s
NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/webserver-d7fbbf4b7   1         1         1       31s

然而,并未列出所有资源。PersistentVolumePersistentVolumeClaim资源没有显示。因此,我们将使用kubectl get pvPersistentVolumes)和kubectl get pvsPersistentVolumeClaims)命令向 Kubernetes API 查询这些资源:

vagrant@kubernetes-node1:~/Docker-Certified-Associate-DCA-Exam-Guide/chapter9/nginx-lab/yaml$ kubectl get pv
NAME           CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS   REASON   AGE
webserver-pv   500Mi      RWO            Retain           Bound    default/werbserver-pvc   manual                  6m13s

vagrant@kubernetes-node1:~/Docker-Certified-Associate-DCA-Exam-Guide/chapter9/nginx-lab/yaml$ kubectl get pvc
NAME             STATUS   VOLUME         CAPACITY   ACCESS MODES   STORAGECLASS   AGE
werbserver-pvc   Bound    webserver-pv   500Mi      RWO            manual         6m15s
  1. 让我们向 Web 服务器发送一些请求。你可以在kubectl get all输出中看到,webserver-svc是通过NodePort在端口30080上发布的,将主机端口30080与服务端口80关联。如前所述,所有主机将发布端口30080;因此,我们可以在当前主机(kubernetes-node1)和端口30080上使用curl尝试访问我们的 Web 服务器的 Pods:
vagrant@kubernetes-node1:~/Docker-Certified-Associate-DCA-Exam-Guide/chapter9/nginx-lab/yaml$ curl 0.0.0.0:30080
<!DOCTYPE html>
<html>
<head>
<title>DEFAULT_TITLE</title>
<style>
 body {
 width: 35em;
 margin: 0 auto;
 font-family: Tahoma, Verdana, Arial, sans-serif;
 }
</style>
</head>
<body>
<h1>DEFAULT_BODY</h1>
</body>
</html>
  1. 我们使用了一个ConfigMap资源来指定 NGINX 配置文件webserver.configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: webserver-test-config
data:
  default.conf: |+
        server {
            listen       80;
            server_name  test;
            location / {
                root   /wwwroot;
                index  index.html index.htm;
            }
            error_page   500 502 503 504  /50x.html;
            location = /50x.html {
                root   /usr/share/nginx/html;
            }
        }

这个配置包含在我们的部署文件webserver.deployment.yaml中。以下是定义部分的代码:

...
        volumeMounts:
        - name: config-volume
          mountPath: /etc/nginx/conf.d/
...
      volumes:
      - name: config-volume
        configMap:
          name: webserver-test-config
...

第一部分声明了这个配置文件将被挂载的位置,而第二部分链接了已定义的资源:webserver-test-config。因此,ConfigMap资源中定义的数据将集成到 Web 服务器的 Pod 中,路径为/etc/nginx/conf.d/default.conf(查看数据块)。

  1. 如前所述,我们还拥有一个Secret资源(webserver.secret.yaml):
apiVersion: v1
data:
  PAGEBODY: SGVsbG9fV29ybGRfZnJvbV9TZWNyZXQ=
  PAGETITLE: RG9ja2VyX0NlcnRpZmllZF9EQ0FfRXhhbV9HdWlkZQ==
kind: Secret
metadata:
  creationTimestamp: null
  name: webserver-secret

我们可以在这里验证,密钥是可见的,而值则不可见(使用 Base64 算法进行编码)。

我们还可以通过kubectl命令行使用命令式格式来创建这个密钥:

kubectl create secret generic webserver-secret \ --from-literal=PAGETITLE="Docker_Certified_DCA_Exam_Guide" \ --from-literal=PAGEBODY="Hello_World_from_Secret"

我们还在我们的部署中使用了这个密钥资源:

...
        env:
...
        - name: PAGETITLE
          valueFrom:
            secretKeyRef:
              name: webserver-secret
              key: PAGETITLE
        - name: PAGEBODY
          valueFrom:
            secretKeyRef:
              name: webserver-secret
              key: PAGEBODY
...

在这种情况下,PAGETITLEPAGEBODY键将作为环境变量集成在 Web 服务器 Pod 内。这些值将在我们的实验中作为index.html页面的值使用。DEFAULT_BODYDEFAULT_TITLE将会从 Pod 的容器进程中更改。

  1. 这个实验还有另一个卷定义。实际上,我们在部署的定义中包含了PersistentVolumeclaim作为一个卷:
...
        volumeMounts:
...
        - mountPath: /wwwroot
          name: data-volume
...
      - name: data-volume
        persistentVolumeClaim:
          claimName: werbserver-pvc
...

卷声明在这里使用,并且挂载在 Web 服务器 Pod 内的/wwwroot目录。PersistentVolumePersistentVolumeClaim分别定义在webserver.persistevolume.yamlwebserver.persistevolumeclaim.yaml中。

  1. 最后,我们有一个emptyDir卷定义。这个将用于绕过容器的文件系统并保存 NGINX 日志:
...
        volumeMounts:
...
        - mountPath: /var/log/nginx
          name: empty-volume
          readOnly: false
...
      volumes:
...
      - name: empty-volume
        emptyDir: {}
...
  1. 第一个 Pod 执行将会在其中创建一个默认的/wwwroot/index.html文件。这个文件被挂载在kubernetes-node2节点的文件系统内,位于/mount目录。因此,在第一次执行后,我们发现/mnt/index.html文件被创建(你可以通过重新执行步骤 1来验证)。文件已发布,并且我们可以在步骤 5中执行curl 0.0.0.0:30080来获取它。

  2. 我们的应用程序非常简单,但它准备修改index.html文件的内容。如前所述,默认的标题和正文将会被在密钥资源中定义的值所替代。如果index.html文件已存在,这将在容器创建后发生。现在它已经创建,如步骤 10所验证,我们可以删除 Web 服务器的 Pod。Kubernetes 将创建一个新的 Pod,因此应用程序的内容将会发生变化。我们使用kubectl delete pod

vagrant@kubernetes-node1:~/Docker-Certified-Associate-DCA-Exam-Guide/chapter9/nginx-lab/yaml$ kubectl delete pod/webserver-d7fbbf4b7-rhvvn
pod "webserver-d7fbbf4b7-rhvvn" deleted

几秒钟后,一个新的 Pod 被创建(我们正在使用部署,Kubernetes 负责应用程序组件的容错性):

vagrant@kubernetes-node1:~/Docker-Certified-Associate-DCA-Exam-Guide/chapter9/nginx-lab/yaml$ kubectl get pods
NAME READY STATUS RESTARTS AGE
webserver-d7fbbf4b7-sz6dx 1/1 Running 0 17s
  1. 让我们再次使用curl验证 Web 服务器的内容:
vagrant@kubernetes-node1:~/Docker-Certified-Associate-DCA-Exam-Guide/chapter9/nginx-lab/yaml$ curl 0.0.0.0:30080
<!DOCTYPE html>
<html>
<head>
<title>Docker_Certified_DCA_Exam_Guide</title>
<style>
 body {
 width: 35em;
 margin: 0 auto;
 font-family: Tahoma, Verdana, Arial, sans-serif;
 }
</style>
</head>
<body>
<h1>Hello_World_from_Secret</h1>
</body>
</html>

现在内容已更改,在定义的PersistentVolume资源中。

  1. 我们也可以在kubernetes-node2中验证/mnt/index.html的内容:
$ vagrant ssh kubernetes-node2

vagrant@kubernetes-node2:~$ cat /mnt/index.html
<!DOCTYPE html>
<html>
<head>
<title>Docker_Certified_DCA_Exam_Guide</title>
<style>
 body {
 width: 35em;
 margin: 0 auto;
 font-family: Tahoma, Verdana, Arial, sans-serif;
 }
</style>
</head>
<body>
<h1>Hello_World_from_Secret</h1>
</body>
</html>

在这个实验中,我们使用了四种不同的卷资源,具有不同的定义和功能。这些实验非常简单,向你展示了如何在 Kubernetes 上部署一个小型应用程序。所有的实验都可以通过在environments/kubernetes目录中使用vagrant destroy销毁所有 Vagrant 节点来轻松删除。

我们强烈建议深入学习 Kubernetes,因为它将在不久的将来成为考试的一部分。然而,现在 Kubernetes 超出了 Docker 认证助理考试的范围。

总结

在本章中,我们快速回顾了 Kubernetes 的一些主要特性。我们将大多数必备的编排功能与 第八章 中讨论的内容进行了比较,使用 Docker Swarm 进行编排。两者都提供了工作负载的部署和分布式节点池的管理。它们监控应用程序的健康状况,并允许我们在不中断服务的情况下升级组件。它们还提供了网络和发布解决方案。

Pod 提供更高的容器密度,允许我们同时运行多个容器。这个概念更接近于在虚拟机上运行的应用程序,使得容器的采用更加容易。服务是 Pod 的逻辑组合,我们可以用它们来暴露应用程序。服务发现和负载均衡开箱即用,并且是动态的。

集群级别的网络需要在 Kubernetes 中安装额外的插件,我们还了解到,扁平化网络可以促进不同主机上的路由,并使一些操作更加简单;然而,它默认并不提供安全性。Kubernetes 提供了足够的机制来确保网络安全,利用网络策略和单一端点提供多个服务的入口。通过入口(Ingress)发布应用程序变得更加容易。它动态地增加了内部负载均衡功能,并通过使用入口资源管理规则。这使我们能够节省节点端口和公共 IP 地址。

在本章结束时,我们回顾了 Kubernetes 安全性的多个要点。我们讨论了 RBAC 如何为在同一集群中运行工作负载的用户提供不同的环境。我们还谈到了一些 Kubernetes 提供的功能,以确保资源的默认安全性。

关于 Kubernetes,还有更多内容需要学习,但我们将在这里结束本章。我们强烈建议您查看 Kubernetes 文档和项目网站上的发布说明 (kubernetes.io/)。

在下一章,我们将并排比较 Swarm 和 Kubernetes 的异同。

问题

  1. 下面哪个功能在 Kubernetes 中默认不包括?

a) 一个内部的键值存储。

b) 分布在不同 Docker 主机上的容器之间的网络通信。

c) 用于在没有服务中断的情况下部署工作负载更新的控制器。

d) 这些功能都没有包含在内。

  1. 关于 Pod,下面哪个说法是正确的?

a) Pod 总是成对运行,以提供应用程序的高可用性。

b) Pod 是 Kubernetes 中最小的部署单元。

c) 我们可以在每个 Pod 中部署多个容器。

d) 我们需要选择在 Pod 扩展时应该复制哪些容器。

  1. 关于 Pod,下面哪个说法是正确的?

a) 所有的 Pod 容器都使用独立的网络命名空间运行。

b) Pod 中的所有容器可以共享卷。

c) 所有在 Docker 引擎上运行的 Pod 都可以通过它们的 IP 地址从主机进行访问。

d) 以上所有陈述都正确。

  1. Kubernetes 提供不同的控制器来部署应用程序工作负载。以下哪项陈述是正确的?

a) DaemonSet 会在每个集群节点上运行一个副本。

b) ReplicaSet 允许我们扩展或缩减应用程序 pods。

c) Deployments 是更高层次的资源。它们管理 ReplicaSet

d) 以上所有陈述都正确。

  1. 我们如何在 Kubernetes 中将服务暴露给用户?(以下哪项陈述是错误的?)

a) ClusterIP 服务提供一个虚拟 IP,用户可以访问。

b) NodePort 服务在所有节点上监听,并通过提供的 ClusterIP 路由流量,以便访问所有服务后端。

c) LoadBalancer 在云服务提供商上创建简单的负载均衡器,以便将请求负载均衡到服务后端。

d) Ingress 控制器帮助我们使用单一端点(每个 ingress 控制器一个)将请求负载均衡到未公开的服务。

进一步阅读

你可以参考以下链接,了解本章所涵盖的主题的更多信息:

第十三章

第三节 - Docker 企业版

本节介绍 Docker 企业版容器即服务CaaS)平台,包括对其所有组件的深入分析。我们将涵盖 Docker 企业版运行时、通用控制平面和 Docker 受信注册中心。在每个部分中,我们将详细了解所有组件、每个功能的管理以及在生产环境中安装每个产品的方法。

本节包含以下章节:

  • 第十章,Docker 企业版平台简介

  • 第十一章,通用控制平面

  • 第十二章,在 Docker 企业版中发布应用

  • 第十三章,使用 DTR 实现企业级注册中心

Docker Enterprise 平台简介

在前几章中,我们讨论了 Docker 的功能和 Docker 环境。我们介绍了容器的概念,并探讨了如何将应用程序部署到协调的环境中。我们看到的所有功能都基于 Docker Community Edition。在本章中,我们将学习所有不同的 Docker 版本及其差异,然后介绍 Docker Enterprise 平台。

在本章中,我们将介绍不同的 Docker 版本和工具。我们还将回顾容器即服务CaaS)的概念,并了解在这种环境中我们需要什么。Docker 提供了一个面向企业的 CaaS 平台,我们将回顾其所有组件。

本章将涵盖以下主题:

  • 回顾 Docker 版本

  • 理解 CaaS

  • Docker Enterprise 平台

  • 规划您的 Docker Enterprise 部署

让我们从学习所有不同的 Docker 版本及其具体功能开始本章内容。

第十四章:回顾 Docker 版本

在本节中,我们将快速回顾不同的 Docker 版本。我们在前几章中一直使用的是 Docker Community,但现在是时候了解 Docker Enterprise 了。因为它对于 Docker Certified Associate 考试非常重要。实际上,它可能涉及超过 50% 的考试所需知识,因为你将学习的所有概念都将与这个平台相关。

Docker Community 是我们在开发基于容器的应用程序时使用的 Docker 平台。它是免费的,并在 GitHub 上提供支持 (github.com/docker/docker-ce) 和 Docker 论坛 (forums.docker.com/)。

Docker Enterprise 是一个面向企业的解决方案。Docker/Mirantis 提供24/7的支持,并通过订阅授权。

Docker Community

当我们谈论 Docker Community Edition,通常称为docker-ce时,我们仅指 Docker 引擎(守护进程),尽管 Docker 团队还开发了其他社区软件产品:

  • Docker Toolbox:这是微软 Windows 和苹果 Mac 用户可用的第一个工具。在 Windows 容器出现之前,这是在 Windows 节点上使用 Docker 的唯一方式。它提供了一个桌面环境,包含许多工具和快捷方式,用于大多数组件和操作。

  • Docker Machine:Docker Machine 允许我们配置 Docker 主机。它自带一些预定义的配置器,我们还可以通过外部二进制文件扩展这个列表,以便在最流行的云提供商和本地基础设施上部署节点。

  • Docker Desktop:这是在 Windows 专业版环境中 Docker Toolbox 环境的演进。开发者对使用 Docker Toolbox 的体验非常满意。为了响应这一需求,Docker 创建了一个能够启动小型 Kubernetes 环境的桌面环境,同时还包含了应用模板,帮助开发者仅通过几次点击鼠标就能轻松创建简单的应用程序。

Docker 社区版提供了一个完整的 Docker 引擎平台。因此,我们可以使用 Docker Swarm 或 Kubernetes 创建集群。所有社区版的功能在前几章中已经覆盖——我们从未讨论过任何企业版特有的集成。Docker Swarm 不提供基于角色的访问控制RBAC)来管理用户。我们还必须提供一个安全发布应用程序的解决方案。请记住,Docker 仅提供路由网格和主机发布功能,它们并不安全。对于许多用户来说,Docker Swarm 经过一些调整后已经足够使用。它易于学习和管理,还为核心组件提供了弹性和高可用性。

Kubernetes 可以部署在 Docker 社区版之上。我们将仅使用 Docker 引擎作为 Kubernetes 集群的运行时。这是目前最常见的解决方案,因为它是最常用的。Kubernetes 提供了一个丰富的生态系统,并且带有一些开箱即用的生产所需功能。然而,另一方面,某些细节(如网络)则需要第三方解决方案。Kubernetes 对容器世界的处理方式与 Docker 不同。Docker 遵循“电池包含,但可互换”的方式,提供了开箱即用的所有功能,尽管我们可以更改其中大部分组件。而 Kubernetes 则采用了“一切都应该是可插拔的”的思维方式。Kubernetes 拥有更丰富的生态系统,因为在其核心组件周围有许多解决方案。这些帮助它比 Docker 更快速、更多地发展。

Docker 企业版

Docker 企业版具备 Docker Swarm 所缺少的所有功能。它提供了一个完整的 CaaS 平台,基于两个组件:Docker Universal Control PlaneUCP)和 Docker Trusted RegistryDTR)。在 2018 年 12 月的欧洲 DockerCon 上,Docker Desktop 企业版正式宣布,并表示它将为开发者提供桌面功能。Docker Desktop 企业版允许开发者轻松使用 Docker 创建应用程序。他们还可以在本地测试已开发的容器,甚至可以选择在某个生产环境中进行测试,以确保他们的应用程序能够在生产环境中顺利运行。Docker Desktop 的创建专门为开发者考虑,而企业版则帮助他们减少开发与生产之间的摩擦。

在撰写本书时,Docker 可以在两个不同的产品品牌下找到。Mirantis 收购了 Docker 企业版产品,而 Docker 继续维护 Docker Community 软件及其桌面产品。完整的企业平台将成为 Mirantis 目录的一部分。

因此,Docker 企业版涵盖以下产品:

  • Docker 企业引擎:Docker 引擎是 Docker 企业平台所需的,它提供所有必需的运行时功能。社区版和企业版之间有一些细微的区别。事实上,最重要的一点与支持服务相关。Docker 企业版提供了企业 24/7 支持订阅选项以及工作时间支持订阅选项。而 Docker Community 版不提供此类支持。这一小差异可能会促使企业用户选择 Docker 企业版。

  • Docker UCP:集群的控制平面也包含在 Docker 企业版中。这个产品叫做 Docker UCP。它还提供了一个开箱即用的 Kubernetes 生产就绪平台,建立在一个生产就绪的 Docker Swarm 集群之上。这可能是获得一个 Kubernetes 集群的最简单方式。这个集群发行版也得到了 Docker 的支持,这意味着所有 Kubernetes 集成都已在 Docker 企业平台上经过充分测试。缺点是 Kubernetes 版本必须在产品生命周期内冻结。在撰写本书时,目前支持并分发的 Kubernetes 版本是 1.14,而社区版中一般提供的是 1.17。这对于企业产品来说是正常的。所有东西都必须在更新到更高版本之前进行测试和验证,这需要时间。

  • Docker 受信注册表:工作时始终需要一个注册表与容器配合使用。尽管 Docker 开发了 Docker Registry 并且它是开源的,但它不足以用于生产环境。它既没有身份验证也没有授权,而这对于确保安全访问镜像至关重要。我们可以集成 Docker Trusted Content但这并不简单。我们需要包括 Notary 服务并将其集成到其他已部署的平台中。相信我,这并不容易。我以前做过,实施起来很困难,维护起来更难。DTR 包括基于 RBAC 模型的身份验证和授权。我们可以创建组织、团队,并为不同用户设置不同的访问权限,还可以将部分镜像公开。我们可以完全控制访问和镜像发布。它还包括 Docker 受信内容的实现,包含所有必需的组件和集成。它还包括不同阶段的 CI/CD 工作流集成和安全镜像扫描。这些功能将确保只有经过批准且无漏洞的镜像在我们的生产 CaaS 平台上运行。

  • Docker Desktop Enterprise:这是撰写本书时最新添加的功能。目前 Docker Certified Associate 考试不包括任何关于它的问题。因此,在这里我们只提供 Docker Desktop 的基本介绍。这是一个桌面应用程序,为开发人员提供完整的 Docker Swarm 和 Kubernetes 环境,以便他们可以在笔记本电脑上开发和测试其应用程序,然后将其工件移到其他阶段。

正如我们所见,有许多不同的组件打包在 Docker Enterprise 版本中。如果我们访问 success.docker.com/article/compatibility-matrix,我们可以查看哪些组件版本是经过验证并支持一起工作的。在撰写本书时,这些是 Docker Enterprise Edition 3.0 的每个组件的最新支持版本:

  • Docker Engine 19.03.x

  • Universal Control Plane 3.2.x

  • Docker Trusted Registry 2.7.x

Docker Engine 受支持的许多 Linux 发行版(如 Red Hat/CentOS、SUSE SLES、Oracle Linux 和 Ubuntu)和 Windows(2016 和 2019 发行版)。

Windows 节点仅支持作为工作节点,并且它们仅将作为 Docker Swarm 编排的一部分。在 Docker Enterprise 3.0 上不支持 Windows 平台上的 Kubernetes。

在接下来的部分,我们将讨论 CaaS 平台是什么以及 Docker 如何提供所有期望的功能。

理解 CaaS

CaaS 平台是一个可用于向用户提供容器服务的平台。 作为服务 这个术语通常与云提供商及其解决方案相关联。在此,我们将这个术语扩展到本地环境。我们将讨论 CaaS 作为一个设计用于向用户提供完整基于容器的解决方案的应用程序框架或复合物。CaaS 解决方案必须提供完整的容器工作流程(构建、部署和运行)。现在也有另一个新术语:KaaS 解决方案。这个术语指的是 Kubernetes 作为服务 平台,其中 Kubernetes 是环境的核心。这些解决方案增加了一些在 Kubernetes 默认安装中不包含的功能,例如监控、日志记录和 CI/CD。

CaaS 和 KaaS 环境旨在为需要完整解决方案的用户提供服务。将有解决方案的管理员和将在环境中使用提供的服务的客户端。

这些平台必须提供以下内容:

  • 身份验证:访问平台的用户应该经过身份验证,以便只允许已批准的用户。

  • 授权:角色将为不同的用户提供不同的访问权限。应该有管理员和用户。每个用户在平台内部应具有不同级别的访问权限和视图。非授权用户不应该可以对容器执行操作。

  • 运行时:所有容器将在容器引擎上运行。这是一个要求。虽然有不同的引擎,但 Docker 引擎现在仍然是最常见的。

  • 发布:我们使用这些平台来创建和运行基于容器的应用程序,但人们必须能够消费我们已部署的服务。CaaS/KaaS 平台必须提供一个组件,允许我们发布部署在环境内的应用程序。

  • 注册表:所有镜像必须存储在某个地方。记住,镜像是必需的。没有镜像就没有容器,将镜像和代码更改一起版本化将有助于你跟踪问题和新功能。拥有一个包含在 CaaS/KaaS 平台中的注册表是至关重要的。

  • 状态:我们需要对所有平台组件的状态有一个完整的视图。如果出现故障,我们需要知道哪些组件会受到影响,我们是否能够推送新镜像,以及我们的服务是否正常运行,举个例子。

  • 集成:尽管在我看来,日志记录和监控并不是严格要求的,但至少为这些功能提供对外平台的集成是一个不错的选择。一些 CaaS 平台将这些服务包括在其部署中(例如 Red Hat 的 OpenShift 等),但我们应该能够轻松地将日志记录和监控环境集成。有时,运维团队会有自己的监控平台;CaaS 平台应该将所有必要的事件转发给它们。CI/CD 工作流是另一种有趣的集成方式。如果 CaaS 平台能够在平台内集成开发和测试阶段,用户将只需要编码。其他一切都可以通过 CI/CD 工具自动化。

正如我们之前提到的,这些平台需要一些管理员来完成所有的维护任务和配置,而用户只需消费提供的服务以创建和运行他们的应用程序。有些云服务提供商采取了不同的方法。Azure Kubernetes 服务AKS)、亚马逊的弹性 Kubernetes 服务EKS)和Google Kubernetes 服务GKS)是这些环境中最知名的例子。

在这些平台上,我们只需要选择要在集群中部署的工作节点数量。所有维护任务由云服务提供商管理;我们只需要配置用户访问权限,并准备一些云服务提供商的负载均衡器来路由流量。其余的配置和部署都在 Kubernetes 中进行。这非常好,因为我们可以专注于部署应用程序。我们不需要关心环境的高可用性、备份或平台升级。云服务提供商将为我们管理所有这些任务。这些平台还包括集成到其平台即服务PaaS)环境中的监控和日志记录设施。

在本节中,我们回顾了在 CaaS 或 KaaS 平台中需要提供的内容。在下一节中,我们将学习 Docker 企业版如何实现这些概念。

Docker 企业版平台

Docker 企业版提供了一个 CaaS 平台。在本节中,我们将尝试将我们对 CaaS 平台的了解应用到 Docker 企业版中。我们将涵盖许多概念,以帮助您理解如何使用 Docker 企业版实现端到端的基于容器的解决方案。我们不会涉及 Docker Desktop 企业版,因为它不包含在 Docker 认证助理考试中。

Docker 引擎

Docker 引擎是平台的核心部分。它提供了执行平台的运行时。与 Kubernetes 不同,Docker Swarm 需要 Docker 引擎才能运行。Kubernetes 提供了直接使用 containerd容器运行时接口优化版 (CRI-O,用于兼容 OCI 的容器)的选项。Docker 引擎包括 Swarm 模式,我们无需任何其他软件即可实现一个功能齐全的分布式调度环境。Docker 引擎为所有平台组件提供了底层执行层。

在 Docker 引擎之上,我们将创建一个 Docker Swarm 集群,其他 Docker 企业版组件将作为 Docker Swarm 服务或多容器应用程序运行。这一点至关重要,因为有些组件将作为代理在平台中运行,我们将自动将它们部署为 全球服务(记住这些概念)。但也有一些组件必须在集群中唯一。它们将在一些指定的主机上作为 多容器应用程序运行。这些组件将使用不同的执行架构。

对于 Docker 企业版,我们将部署 Docker 企业版引擎,并支持特定的发布版本。企业版发布必须长期得到支持,因此这会影响发布的时间。如我们在Docker 企业版引擎一节中所看到的,目前支持的版本是 19.03.x(截至写作本书时),而对于社区版,支持的版本可能不同(目前也是 19.03.6,但直到最近,版本之间可能会有较大差异)。这是正常现象,因为 Docker 工程师和支持团队必须验证所有组件的集成并解决当前 Docker 企业版发布中的任何问题,同时在此过程中通过增加新特性来推动产品的演进。这些新特性通常会先出现在 Docker 社区版中,然后在充分测试并实现后才会应用于 Docker 企业版。

由于我们将在集群环境中工作,我们将能够执行维护任务并在节点之间移动工作负载,而不会中断服务。Docker 引擎的更新将顺畅且简单。

通用控制平面

UCP 为 Docker Enterprise 平台提供控制平面。它提供了管理所有集群组件及其状态所需的所有进程和工具。UCP 将部署组件在主节点和工作节点上,正如我们将在第十一章中学习的那样,通用控制平面。它基于 Docker Swarm 编排,但正如我们之前提到的,核心组件将作为多容器应用程序运行。主节点将运行控制平面进程。如果这些进程失败,它们不会在其他节点上运行。理解这些核心进程只能在指定节点上运行非常重要。其他节点无法承载这些工作负载。如果主节点发生问题,且我们无法恢复该主节点,我们需要创建一个新的主节点。我们将提升一个工作节点,或者在移除旧节点后安装一个新的主节点。

UCP 将部署一些分布式数据库,保持其法定人数(quorum)非常重要。我们将在第十一章中回顾一些常见问题,通用控制平面。请记住,UCP 管理节点非常重要,进程必须在指定节点上运行。

所有内部集群通信将使用 TLS 进行加密。UCP 管理所有节点、所有组件及其证书。它还将为经过认证和授权的用户提供证书。默认情况下,我们可以确保客户端与服务器之间的安全通信。

Kubernetes 集群也将默认部署所需的容器网络接口CNI)Calico,并配置安全设置。UCP 提供一个生产就绪的 Docker Swarm 和 Kubernetes 平台。

集群的认证和授权将由 UCP 管理。我们将能够集成第三方认证系统,如轻量目录访问协议LDAP)和 Active Directory。所有授权机制和实现也都包含在 UCP 中。我们可以提供统一登录,将所有 DTR 认证请求委托给 UCP。这是通常的且首选的配置。UCP 提供一个完整的基于资源、角色和权限的 RBAC 系统。我们可以在集群内的任何资源上指定高度定制的访问权限。

UCP 提供一个管理 Web UI 和一个 API 接口,用于访问集群的资源。我们将能够配置所有 Docker Swarm 和 Kubernetes 资源。对于 Kubernetes,提供一个简单的界面来部署资源的 YAML 文件。我们将远程使用集群。我们永远不会允许用户访问管理节点或工作节点。

禁止任何未经授权访问集群节点非常重要。通过 SSH 访问 Docker 主机或直接访问 Docker Engine 的守护进程将绕过 UCP 应用的所有安全实现。

Web UI 还将提供一些简单的监控功能,用于验证整个集群的状态。我们可以查看所有容器、Pod、服务的状态,以及一般来说,所有由集群管理的资源。我们还可以使用 Prometheus 的标准集成导出集群的指标。Web UI 还提供对容器日志的访问,我们甚至可以使用它们来查看应用程序的行为。所有这些访问将通过 UCP 的 RBAC 系统进行管理。

Docker Swarm 和 Kubernetes 将通过其 API 提供服务。如我们在第九章《使用 Kubernetes 进行编排》中所学,Kubernetes 提供了其自有的 RBAC。Docker Swarm 则需要外部工具。UCP 提供了这些外部工具,将所有 API 请求代理到 UCP 的内部 RBAC 集成,并提供适当的身份验证和授权机制。

UCP 还提供了一个集成组件,用于发布集群内部署的应用程序。这个组件是 Interlock,并且在编写本书时,它基于 NGINX。Interlock 仅适用于 Docker Swarm 部署,监控集群的 API 以获取已定义服务的变化。我们将定义哪些服务需要发布,以及哪些头部、名称和路由应该可用。对服务应用的所有更改将自动填充到 Interlock 的反向代理组件中,该组件会将请求转发到相关的后台服务。我们将在第十二章《在 Docker Enterprise 中发布应用程序》中更深入地学习这一内容。

Docker 信任的注册中心

正如我们在讨论 CaaS 要求时提到的,我们需要一个注册中心来存储镜像。这个注册中心必须提供安全访问和角色,因为在发布镜像时,我们需要一些粒度控制。一些用户将是自己镜像的所有者,而其他用户只能使用这些镜像。我们需要确保镜像的不可变性。DTR 提供了这一点。它是建立在开源的 Docker Registry 基础之上,但添加了许多改进,以提供企业级解决方案。

DTR 提供了一个安全的存储库来存储所有 CaaS/KaaS 镜像。我们可以确保源追溯性和不可变性。我们还将为镜像提供不同的访问级别。一些用户将是基础镜像的维护者,而其他用户将能够将其用于自己的项目。我们还拥有团队和组织。我们可以在多租户环境中的组织内发布镜像,确保组织内的所有用户都能使用其公共镜像。团队将共享镜像维护责任,但只有部分成员能够修改镜像内容。

由于安全性在 CaaS 环境中至关重要,DTR 将提供镜像扫描和签名功能。镜像扫描将检查所有镜像,寻找二进制漏洞。它将使用 常见漏洞和暴露CVE)数据库来查找任何存在漏洞的文件。所有漏洞内容将被报告,管理员将在平台内处理这些问题。我们可以决定只执行干净的镜像,即没有报告任何漏洞的镜像。镜像签名将确保我们禁止任何未签名的镜像进入我们的基础设施。这确保我们只执行在我们组织内创建并签名的镜像。如果镜像被外部修改,将无法运行容器。

DTR 还可以集成到 CI/CD 流水线中,结合其镜像推广功能。镜像标签可以通过触发器进行修改。这个过程还可以告诉外部应用程序进行跟踪,帮助我们在部署工作流中实现特殊的阶段。镜像是应用程序的新代码构件,我们可以将 DTR 集成到我们的 CI/CD 流水线中。

在下一节中,我们将描述使用 Docker 企业版进行生产的最小环境。

规划你的 Docker 企业版部署

正如我们在本章中讨论的那样,Docker 企业版提供了一个生产就绪的 CaaS 平台。在本节中,我们将回顾部署 Docker 企业版到生产环境中的最小逻辑要求。

我们了解到,Docker Swarm 和 Kubernetes 需要奇数个主节点才能正常工作。Docker Swarm 不需要外部键值存储,而 Kubernetes 需要。Docker 企业版将通过 UCP 部署这个键值存储,因此至少需要三个管理节点以提供高可用性。所有管理节点将运行相同的服务。在 Docker Swarm 和 Kubernetes 中,我们有一个领导节点,用于在数据库中记录集群的变更。其他管理节点将同步其数据,但我们也可以在任何一个管理节点上运行管理命令。我们需要集成外部负载均衡器,以便将 API 请求分发到所有管理节点。

记住,三个节点只有在其中一个节点失败时才能保护集群。集群在两个管理节点下也能正常工作,但如果另一个节点失败,集群将变得不一致。

UCP 要求至少有三个管理节点。那么 DTR 呢?该组件有自己的分布式数据库:它使用RethinkDB。这个数据库也要求副本数量为奇数;因此,至少需要三个节点。DTR 将使用多容器架构在工作节点上部署。因此,我们可以说,DTR 至少需要三个工作节点。镜像扫描可能消耗大量的 CPU 资源,建议将 DTR 节点与其他工作节点隔离,以避免对应用程序产生影响。DTR 集群需要节点之间的共享存储,因为只有接收应用请求的节点才能向数据库写入更改。但所有节点必须写入相同的存储位置,因此需要共享存储。我们将在 DTR 的 API 前面使用外部负载均衡器,以便在服务节点之间分配请求。

我们将根据需要向该平台添加工作节点。事实上,我们将从最少两个工作节点开始,以确保高可用性。所有应用负载都必须具备弹性;因此,如果我们在两个架构上都进行部署,Windows 和 Linux 工作负载的最小节点数将要求至少为两个。下图展示了描述的场景:

我们将为管理节点和工作节点使用固定 IP 地址。这是首选方案,尽管工作节点可以使用 DHCP 进行部署。我们将如在第八章中讨论的那样,使用 Docker Swarm 进行编排,将控制平面与数据平面隔离。数据平面将用于应用程序,而控制平面将用于内部集群通信。

Calico 将默认作为 Kubernetes CNI 使用,检查是否存在 IP 范围冲突非常重要。下表显示了 Docker Engine、Docker Swarm 和 Kubernetes 使用的默认 IP 地址:

组件 子网 范围 默认 IP 地址
引擎 fixed-cidr docker0接口和本地容器的 CIDR 范围 172.17.0.0/16
引擎 default-address-pools docker_gwbridge接口和桥接网络的 CIDR 范围 172.18.0.0/16
Swarm default-addr-pool Docker Swarm 覆盖网络的 CIDR 范围 10.0.0.0/8
Kubernetes pod-cidr Kubernetes Pods 的 CIDR 范围 192.168.0.0/16
Kubernetes service-cluster-ip-range Kubernetes 服务的 CIDR 范围 10.96.0.0/16

为了避免任何防火墙问题,请查看以下链接,该链接描述了某些 Linux 平台上所需的配置:docs.docker.com/ee/ucp/admin/install/plan-installation

我们将使用完全合格的域名FQDNs)为与 UCP/Kubernetes 和 DTR API 相关的虚拟 IP 地址提供服务。

我们将在第十一章,通用控制平面,和第十三章,使用 DTR 实现企业级注册表中回顾所有必需的端口。但客户端通过特定的暴露端口来访问集群服务。默认情况下,UCP 和 DTR 将在端口 443 上暴露它们的 API 和 Web UI,而 Kubernetes 则暴露在端口 6443 上。

我们通常在产品安装过程中需要互联网连接,尽管我们可以执行离线安装。如果需要提供自动的镜像扫描数据库同步,DTR 需要互联网连接。例如,我们可以每周从 Docker 网站下载一个压缩的数据库文件,以避免这种所需的连接性。

许可证处理流程也可以自动化,订阅续期可以在产品许可证到期前同步。

这是对将 Docker 企业组件部署到生产环境的简要描述。我们将在后续章节中更深入地探讨这些组件。

总结

在本章中,我们介绍了 Docker 企业平台。我们回顾了 Docker Community 工具和 Docker 企业产品之间的主要差异。

我们还介绍了 CaaS 和 KaaS 平台的概念。我们了解了这些平台应该提供什么,以及不同的制造商和云服务提供商如何部署它们的实现。

我们还描述了 Docker 企业平台的最重要功能,即 Docker 企业引擎(Docker Enterprise Engine)、UCP 和 DTR。这些组件提供了 Docker 的 CaaS 解决方案。通过这些内容,我们已经涵盖了规划 Docker 企业生产环境时需要考虑的最重要事项。

在下一章中,我们将更深入地探索 UCP。

问题

  1. 以下哪一项组件不属于 Docker 企业平台?

a) DTR。

b) Docker 企业引擎。

c) Docker Machine。

d) 这些都是 Docker 企业平台的一部分。

  1. 以下哪些陈述关于 Docker Community 和 Docker Enterprise 是正确的?

a) Docker 企业版提供了一个企业级的平台。

b) 我们不能将 Docker Swarm 部署到生产环境中。

c) Docker 企业版不支持 Kubernetes;只支持 Docker Swarm。

d) Docker 注册表是一个企业级的注册表。

  1. 部署 KaaS 解决方案需要哪些 Docker 组件?

a) Docker 企业引擎。

b) UCP。

c) Kubernetes。

d) DTR。

  1. 以下关于部署 Docker 企业环境的哪些陈述是正确的?

a) 我们仅为管理节点使用固定的 IP 地址。

b) 我们只需将流量路由到其中一个管理节点。

c) 在 UCP 完成 Kubernetes 安装后,我们需要部署一个 CNI。

d) 以上都不是。

  1. 在 Docker 企业平台上执行 Linux 工作负载并确保高可用性所需的最少节点数是多少?

a) 我们需要部署三个管理节点、三个带有 DTR 的工作节点,以及一个足够资源的 Linux 工作节点来运行所有工作负载。

b) 我们需要部署三个运行 DTR 的管理节点,以及两个 Linux 工作节点。

c) 我们需要部署三个带有 DTR 的管理节点,三个 DTR 工作节点,以及两个 Linux 工作节点。

d) 所有这些选项都是有效的。

进一步阅读

你可以参考以下资料,了解本章涉及的更多内容:

Universal Control Plane

在本章中,我们将学习所有与 Docker 的 Universal Control Plane (UCP) 相关的内容,这些内容是 Docker 认证助理考试所需的。Universal Control Plane 是 Docker 企业版中负责管理集群的组件。首先,我们将介绍 UCP 的组件及其功能。值得注意的是,UCP 在近年来发生了很多变化。Docker 企业平台之前被称为 Docker Datacenter。在 2.0 版本发布时,Docker 更改了名称。这个版本也很重要,因为它是第一个将 Kubernetes 作为第二个编排工具引入的版本。在本章中,我们将学习 Kubernetes 如何集成,以及如何部署一个生产就绪的平台。

2019 年 11 月,Mirantis 公司收购了 Docker 企业平台业务,包括其产品、客户和员工。因此,Docker 企业版目前是 Mirantis 公司的产品。

我们将探索 UCP 的主要组件,并学习如何部署一个高可用的生产环境。企业环境有许多安全需求,UCP 包括基于 RBAC 的身份验证和授权系统,所有这些都可以轻松与企业的用户管理平台集成。Docker 企业版基于 Docker Swarm,但还包括集群中的企业级 Kubernetes 环境。我们将学习 UCP 的管理任务、安全配置、特殊功能,以及如何基于备份和恢复功能提供灾难恢复策略。本章将通过回顾在该平台上应监控的内容来确保其健康状态。

本章将涵盖以下主题:

  • 理解 UCP 组件和功能

  • 部署具有高可用性的 UCP

  • 回顾 Docker UCP 的环境

  • 基于角色的访问控制与隔离

  • UCP 的 Kubernetes 集成

  • UCP 管理和安全

  • 备份策略

  • 升级、健康检查和故障排除

让我们开始吧!

第十五章:技术要求

您可以在 GitHub 仓库中找到本章的代码:github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

查看以下视频,观看代码实践:

"bit.ly/34BHHdj"

理解 UCP 组件和功能

Docker 的 UCP 提供了 Docker 企业平台的控制平面。它基于 Docker Swarm,但还集成了 Kubernetes 编排器。以下是其当前功能的快速列表:

  • 集中式集群管理界面

  • 集群资源环境

  • 基于角色的访问控制

  • 通过 WebGUI 或 CLI 的客户端环境

正如我们之前提到的,UCP 基于 Docker Swarm 编排。我们将部署一个包含管理节点和工作节点角色的 Docker Swarm 集群。

首先,我们将安装一个管理节点。在安装过程中,这将是领导节点。所有组件将作为容器部署,因此我们只需要一个 Docker 企业版引擎来运行它们。

一旦安装了第一个管理节点,并且所有 UCP 组件都启动并运行,我们将继续向集群中添加节点。这个过程非常简单。

所有组件将由一个名为ucp-agent的主代理进程进行管理。该进程将根据已安装节点的角色部署其他所有组件。

让我们回顾一下部署在管理节点和工作节点角色上的不同组件。

管理节点上的 UCP 组件

在第八章《使用 Docker Swarm 进行编排》中,我们学习了这些集群是如何工作的。管理节点运行所有管理进程。我们将部署奇数个管理节点,以提供高可用性,因为 Docker Swarm 基于 Raft 协议,需要在管理平面中达成共识或法定人数。

管理节点运行所有 UCP 核心服务,包括 Web UI 和持久化 UCP 状态的数据存储。这些是运行在管理节点上的 UCP 服务:

组件 描述
ucp-agent 该组件是运行在每个节点上的代理,用于监控并确保所需的服务正在运行。
ucp-swarm-manager 为了与 Docker Swarm 环境兼容,该组件运行在管理节点上。
ucp-proxy 该组件使用 TLS 提供对平台上每个 Docker 引擎的安全访问,并将请求转发到本地套接字。
ucp-auth-api 该组件运行在管理节点上,提供身份验证 API,用于授权访问。
ucp-auth-store 该组件存储 UCP 平台上用户、组织和团队的数据及配置。
ucp-auth-worker 一个身份验证工作进程定期与外部身份验证后台进行同步任务。
ucp-client-root-ca 该组件提供一个证书颁发机构,用于签署平台上用户的包。包是由管理平台颁发的,旨在为用户提供访问权限。
ucp-cluster-root-ca 为了确保平台组件之间的安全通信,该组件提供一个证书颁发机构(CA)用于签署 TLS 证书。
ucp-kv 该组件提供一个键值数据库,用于存储集群配置。它曾仅用于传统 Swarm(我们尚未看到 Docker Swarm 过去是如何部署的,但它今天与 Kubernetes 类似),但目前它也被用作 Kubernetes 的键值存储。
ucp-controller UCP Web UI 是管理的关键。ucp-controller提供此功能。当用户无法访问集群时,它是第一个发生故障的组件。
ucp-reconcile 为了监控组件的健康状况,UCP 运行 ucp-agent,如果某些组件出现故障,它会尝试重新启动它们。此组件仅在出现问题时运行。
ucp-dsinfo UCP 可以运行故障排除报告。它执行此组件以检索所有可用的信息。我们使用支持转储将信息发送到支持服务。
ucp-metrics 该组件用于恢复节点指标。这些数据可以在其他监控环境中使用。
ucp-interlock/ucp-interlock-proxy Interlock 组件允许高级用户发布在集群中部署的应用程序。ucp-interlock 查询 UCP API 以获取要配置以发布服务的更改,并在 ucp-interlock-proxy 组件中配置反向代理,UCP 会为您自动部署和配置该反向代理。

以下进程也会在主节点上运行,但由于它们与 Kubernetes 相关,因此被单独列在不同的表格中:

进程 描述
ucp-kube-apiserver 这是 Kubernetes 主 API 组件。所有 Kubernetes 进程都将作为容器部署在我们的主机中。使用容器来部署应用程序有助于我们维护应用程序的组件及其升级。
ucp-kube-controller-manager 这个 Kubernetes 进程将管理所有控制器,用于控制、复制和监控 Pods。
ucp-kube-scheduler kube-scheduler 在集群节点中调度工作负载。
ucp-kubelet kubelet 是 Kubernetes 代理。它是 Kubernetes 用来管理节点及其交互的端点。
ucp-kube-proxy kube-proxy 管理 Pod 的发布和通信。
k8s_ucp-kube-dns/k8s_ucp-kubedns-sidecar/k8s_ucp-dnsmasq-nanny 这些容器管理和监控 DNS 过程及 UCP 和 Kubernetes 所需的解析。
k8s_calico-node/k8s_install-cni_calico-node/k8s_calico-kube-controllers Calico 是 Kubernetes 的默认容器网络接口CNI),并且在 UCP 安装过程中会自动部署。

在较新的版本中,还有一个重要组件用于帮助 Docker Swarm 和 Kubernetes 之间的交互:

进程 描述
k8s_ucp-kube-compose kube-compose 允许我们将 Docker Compose 的工作负载部署到 Docker Swarm 上作为堆栈或 Kubernetes 中。

这些是可以部署在管理节点上的组件。我们通常会部署至少三个管理节点,因为无论是 Docker Swarm 还是 Kubernetes 都需要奇数个节点来进行分布式一致性。

现在,让我们回顾一下工作节点的组件。

工作节点上的 UCP 组件

以下是工作节点加入集群后部署的组件:

组件 描述
ucp-agentucp-proxyucp-dsinfoucp-reconcile 这些进程具有与管理节点相同的功能。
ucp-interlock-extension interlock-extension 根据从 Docker Swarm 服务配置中获取的更改,为 interlock-proxy 准备配置。这一过程基于动态重新配置的模板,以完成集群范围内发布工作负载中发生的所有更新。
ucp-interlock-proxy interlock-proxy 运行一个代理进程,这一进程是通过所有其他 Interlock 进程动态配置的。它为代理组件准备一个配置文件,包含每个发布服务所需的所有运行后端。

为了使 Kubernetes 正常工作,工作节点还会执行以下进程:

进程 描述
ucp-kubeletucp-kube-proxy 这两个过程仅在工作节点上需要用于 Kubernetes。
k8s_calico-nodek8s_install-cni_calico-node 集群节点之间的网络需要 Calico,因此它的进程也会在工作节点上部署。

从这些列表中,我们可以轻松理解 Kubernetes 如何在 Docker 企业版上部署。管理节点运行 Kubernetes 的控制平面,而工作节点接收工作负载。请注意,管理节点运行 kube-proxykubelet,因此它们也能够接收工作负载。这对于 Docker Swarm 也同样适用,正如我们在第八章《使用 Docker Swarm 进行编排》中所学到的,使用 Docker Swarm 进行编排。Docker 企业版默认允许管理节点执行应用程序工作负载。

我们还将回顾在 UCP 上部署的卷,并将其分为两类:

  • 证书卷:所有这些卷与集群中的证书管理相关。请小心处理它们,因为如果我们丢失它们,将会导致 UCP/Kubernetes 进程之间出现严重的身份验证问题:

    • ucp-auth-api-certs

    • ucp-auth-store-certs

    • ucp-auth-worker-certs

    • ucp-client-root-ca

    • ucp-cluster-root-ca

    • ucp-controller-client-certs

    • ucp-controller-server-certs

    • ucp-kv-certs

    • ucp-node-certs

  • 数据卷:这些是数据卷,用于存储集群内部署的不同数据库以及从不同组件获取的度量数据:

    • ucp-auth-store-data

    • ucp-auth-worker-data

    • ucp-kv

    • ucp-metrics-data

    • ucp-metrics-inventory

所有这些卷对于集群非常重要。键值对、常见证书和身份验证数据卷会在控制平面节点上进行复制。除非我们已经使用不同的驱动程序创建它们,否则它们会使用默认的本地卷驱动程序创建。请记住,如果我们希望将数据存储在非标准位置(/var/lib/docker/volumes 或环境中定义的 data-root 路径),它们应该在部署集群之前创建。

这一部分对于 Docker 认证助理考试非常重要,因为我们需要了解组件如何分布以及它们在平台上的功能。

现在我们知道每个集群角色将部署哪些组件,接下来我们将学习如何安装生产就绪的环境。

部署高可用的 UCP

首先,我们将查看平台的硬件和软件要求。我们将使用版本 3.0——这是写本书时的当前版本。众所周知,DCA 考试甚至是在 Docker Enterprise 2.0 版本发布之前就已经准备好了。当时 Docker Desktop 和 Kubernetes 并不属于 Docker Enterprise 平台的一部分。我们将部署当前版本,因为考试已经发展到涵盖新版本中的重要主题。让我们快速回顾一下当前的维护生命周期:

Docker Enterprise 2.1 Docker Enterprise 3.0
2020-11-06 停止支持 2021-07-21 停止支持

| 组件:- 企业引擎 18.09.z - 通用控制平面 3.1.z

  • Docker 受信任注册表 2.6.z | 组件:- 企业引擎 19.03.z - 通用控制平面 3.2.z - Docker 受信任注册表 2.7.z |

Docker 提供从发布之日起 2 年的支持。我们建议查看 Docker 网站,获取有关维护生命周期和兼容性矩阵的最新信息:

所有列出的版本都包含 Kubernetes,但部署的版本会有所不同。写本书时,Docker Enterprise 部署的是 Kubernetes v1.14.8。

我们可以在本地或云提供商上部署 Docker Enterprise 平台。在部署第一个节点之前,让我们回顾一下最小节点要求:

  • 管理节点需要 8 GB 的 RAM,工作节点需要 4 GB 的 RAM。

  • 管理节点需要 2 个 vCPU。工作节点的 vCPU 数量将取决于将要部署的应用程序。

  • 管理节点的 /var 分区需要 10 GB 的可用磁盘空间(推荐至少 6 GB,因为 Kubernetes 会在安装之前检查磁盘空间),工作节点的 /var 分区需要至少 500 MB 的可用磁盘空间。工作节点的空间将取决于将要部署的应用程序,它们的镜像大小,以及需要在节点上存在的镜像发布数量。

对于控制平面和镜像管理,可能更为实际的资源配置如下:

  • 管理节点和带有 DTR 的工作节点需要 16 GB 的 RAM(我们将在第十三章,实现企业级注册表与 DTR 中学习有关 DTR 的内容)。

  • 管理节点和带有 DTR 的工作节点需要 4 个 vCPU。如果没有足够的 CPU 可用,控制平面 CPU 和镜像扫描可能会永远进行下去。

请记住,集群的规模实际上取决于要部署的应用程序。通常建议将应用程序组件分布在多个节点上,因为节点较少的集群更难维护。拥有多个资源的少数节点比将相同的资源分布在多个节点上更糟糕。当某些节点出现故障时,将资源分散到多个节点上可以提供更好的集群生命周期管理和负载分配。

在控制平面节点上,我们将部署 Docker 企业版引擎版本 19.03(写作时的最新版本)。这些节点应该部署静态 IP 地址并且 Linux 内核版本应为 3.10 或更高版本。由于我们将部署多个副本,因此我们需要一个外部负载均衡器,将控制平面的请求路由到任何可用的管理节点。我们将使用一个虚拟 IP 地址和与此负载均衡器关联的完全限定域名FQDN)。我们将它们作为主题备用名称SAN)添加,以确保证书有效。证书应该与可以作为 UCP 服务一部分访问的任何节点相关联(作为 SAN)。在这种情况下,管理节点将运行控制平面组件,因此证书应对它们中的任何一个有效,包括与 UCP 管理端点相关联的所有可能的 FQDN 名称(默认情况下,Docker Swarm 和 Kubernetes 的端口分别为 4436443)。

我们将默认公开 TCP 端口 4436443,也可以将它们更改为更适合我们环境的其他端口。第一个端口允许用户通过 Web 浏览器、API 或 Docker 命令行与 UCP 的控制平面进行交互。第二个端口则公开 Kubernetes API 服务器,使我们可以直接与 Kubernetes 调度器交互。

工作节点不需要静态 IP 地址,但它们应该能够通过 DNS 使用其名称进行访问。

我们不能在 UCP 中部署用户命名空间。(我们在第三章中了解了用于提高主机安全性的用户命名空间,运行 Docker 容器。)在 UCP 环境下使用此功能并不容易,因此不支持该功能。

高可用性环境的最低配置将包括三个管理节点和至少两个工作节点。下图显示了最小环境(不包括 DTR 节点)。我们可以说,UCP 具有三个主要的逻辑组件:

因此,总结来说,我们将需要以下逻辑要求来进行部署:

  • 管理节点的静态 IP 地址

  • 控制平面的 VIP 地址和 FQDN

  • 一个外部负载均衡器,拥有 VIP,并通过 TCP 将请求转发到管理节点的 4436443 端口(默认情况下)

应该允许以下端口和协议(除非明确说明,否则为 TCP 端口):

  • 在管理节点上:

    • 端口 443 用于 UCP Web UI 和 API

    • 端口 6443 用于 Kubernetes API 服务器

    • 23762377端口用于 Docker Swarm 通信

    • 1237912388的端口用于 UCP 组件之间的内部通信

  • 在工作节点和管理节点上:

    • 7946端口(TCP 和 UDP)用于 Docker Swarm 的 gossip 协议

    • 4789端口(UDP)用于覆盖网络

    • 12376端口用于 TLS 认证代理,以访问 Docker 引擎

    • 6444端口用于 Kubernetes API 反向代理

    • 179端口用于 Kubernetes 网络的 BGP 对等连接

    • 9099端口用于 Calico 健康检查

    • 10250端口用于 Kubernetes Kubelet

用户将通过 HTTPS 协议使用4436443端口访问 UCP 服务。

所有集群节点都会运行容器。这些节点中的一些将作为管理节点运行管理组件,而另一些则仅运行少量的工作组件和负载。但是,管理节点和工作节点有两个共同点:UCP 代理和 Docker 引擎。

Docker 引擎始终是必需的,因为我们需要运行容器。Docker Enterprise 需要 Docker Enterprise 引擎。安装过程很简单,并将基于许可证密钥文件以及每个客户在https://hub.docker.com/u/<YOUR_USER_OR_ORGANIZATION>/content可用的特定软件包。首先,我们将访问https://hub.docker.com/并注册 Docker Hub 账户。Docker 提供了 Docker Enterprise 平台的 1 个月试用,试用详情请访问 hub.docker.com/editions/enterprise/docker-ee-trial。在第十一章 通用控制平面、第十二章 在 Docker Enterprise 中发布应用程序 和 第十三章 使用 DTR 实现企业级注册表 中,我们将以我自己的账户(frjaraur)为例,并通过不同的步骤和图片帮助你理解。

以下截图显示了frjaraur内容的 URL。登录 Docker Hub 网站后,你将获得自己的内容。在注册 Docker Enterprise 30 天试用版后,我们将在此页面找到所需的许可证和我们的软件包仓库 URL:

在右下角,我们将看到软件包的 URL。点击复制按钮,并按照接下来的步骤安装 Docker Enterprise 引擎。这个过程对于每个 Linux 发行版来说会有所不同。在本书中,我们将遵循 Ubuntu 的安装步骤。该过程已在之前提供的客户内容 URL 中进行了描述。Microsoft Windows 节点也可以在 Docker Enterprise 平台中使用,尽管在撰写本书时,它们只能作为工作节点使用。

以下是在 Ubuntu 节点上安装 UCP 高可用性配置的步骤:

  1. 将 Docker Engine 的版本和之前显示的 URL 分别导出到 DOCKER_EE_VERSIONDOCKER_EE_URL 变量中。在本书编写时,最新的 Docker 企业版引擎版本是 19.03:
# export DOCKER_EE_URL="https://storebits.docker.com/ee/trial/sub-76c16081-298d-4950-8d02-7f5179771813"
# export DOCKER_EE_VERSION=19.03

请注意,你的 DOCKER_EE_URL 将完全不同。你可以申请试用许可证以便遵循这些步骤。

  1. 接下来,我们需要将 Docker 客户端的包仓库添加到我们的环境中:
# curl -fsSL "${DOCKER_EE_URL}/ubuntu/gpg" | sudo apt-key add -
# apt-key fingerprint 6D085F96
# add-apt-repository \ 
"deb [arch=$(dpkg --print-architecture)] $DOCKER_EE_URL/ubuntu \   $(lsb_release -cs) \   stable-$DOCKER_EE_VERSION"
# apt-get update -qq
  1. 最后,我们将安装所需的包:
# apt-get install -qq docker-ee docker-ee-cli containerd.io

在安装 UCP 之前,必须将这些步骤应用于所有集群节点。正如我们之前提到的,我们将为不同的 Linux 发行版提供不同的操作步骤,但我们也可以将 Microsoft Windows 节点包含在集群中。Microsoft Windows Docker Engine 的安装完全不同,具体过程请参见https://hub.docker.com/u/<YOUR_USER_OR_ORGANIZATION>/content

在安装 Docker 企业版引擎之前,始终查看 Docker 客户端的内容页面,因为安装过程可能会发生变化。

当所有集群节点都安装了 Docker Engine 后,我们可以继续安装 Docker UCP。这个工作流程并非强制要求,但建议使用,因为它可以在安装 UCP 之前避免出现任何问题。这是因为 UCP 的组件将在你的主机上以容器形式运行。

Docker 提供对不同基础设施的支持,并且认证可以在这些基础设施上运行 Docker 企业版平台。在本书编写时,亚马逊 Web 服务(AWS)和 Microsoft Azure 是认证的环境。在这两种情况下,Docker 还提供了基础设施脚本和/或逐步文档,用于成功部署 Docker 企业版平台。

Docker 企业版平台基于 Docker Swarm,尽管也有部署 Kubernetes。因此,我们将使用 UCP 安装程序创建一个 Docker Swarm 集群,然后将其他节点添加为管理员或工作节点。

安装过程将需要启动一个名为 ucp 的容器。这非常重要,因为它确保一次只进行一次安装。我们还将使用 Docker Engine 的本地套接字作为安装容器内的卷。UCP 安装过程有许多选项——我们将在这里介绍最重要的几个。

要安装 UCP,我们将启动 docker container run --name ucp docker/ucp:<RELEASE_TO_INSTALL>。安装特定版本非常重要,因为 docker/ucp 容器也将用于备份/恢复和其他任务。

让我们为集群中的第一个管理员节点写下并执行一个常规安装命令:

(first manager node) # docker container run --rm -it --name ucp \
 -v /var/run/docker.sock:/var/run/docker.sock \
 docker/ucp:3.2.5 install \
 --host-address <MANAGEMENT_HOST_IP_ADDRESS> \
 --san <LOAD_BALANCED_FQDN> \
 --san <OTHER_FQDN_ALIAS_OR_IP> \
 --admin-username <ADMIN_USER> \
 --admin-password <ADMIN_USER_PASSWORD>

 INFO[0000] Your Docker daemon version 19.03.5, build 2ee0c57608 (4.4.0-116-generic) is compatible with UCP 3.2.5 (57c1024)
  WARN[0000] None of the Subject Alternative Names we'll be using in the UCP certificates ["<NODE_IP_ADDRESS>" "<NODE_NAME>"] contain a domain component. Your generated certs may fail TLS validation unless you only use one of these short names or IP addresses to connect. You can use the --san flag to add more aliases
 INFO[0000] Checking required ports for connectivity
 INFO[0004] Checking required container images
 INFO[0007] Running install agent container ...
 INFO[0000] Loading install configuration
 INFO[0000] Running Installation Steps
 INFO[0000] Step 1 of 35: [Setup Internal Cluster CA]
 ...
 INFO[0014] Step 16 of 35: [Deploy UCP Controller Server]
 INFO[0016] Step 17 of 35: [Deploy Kubernetes API Server]
 ...
 INFO[0033] Step 24 of 35: [Install Kubernetes CNI Plugin]
 INFO[0063] Step 25 of 35: [Install KubeDNS]
 INFO[0064] Step 26 of 35: [Create UCP Controller Kubernetes Service Endpoints]
 INFO[0066] Step 27 of 35: [Install Metrics Plugin]
 INFO[0067] Step 28 of 35: [Install Kubernetes Compose Plugin]
 INFO[0073] Step 29 of 35: [Deploy Manager Node Agent Service]
 INFO[0073] Step 30 of 35: [Deploy Worker Node Agent Service]
 INFO[0073] Step 31 of 35: [Deploy Windows Worker Node Agent Service]
 INFO[0073] Step 32 of 35: [Deploy Cluster Agent Service]
 INFO[0073] Step 33 of 35: [Set License]
 INFO[0073] Step 34 of 35: [Set Registry CA Certificates]
 INFO[0073] Step 35 of 35: [Wait for All Nodes to be Ready]
 INFO[0078] All Installation Steps Completed 

在 35 个步骤之后,UCP 的环境将安装在第一个 Linux 节点上。请小心使用 DNS 解析和外部负载均衡器。如前所述,所有管理节点将运行相同的控制平面组件。因此,需要一个外部负载均衡器来引导请求到其中的任何一个。可以按照轮询算法进行,例如(无论哪个 UCP 管理节点接收请求都无所谓,但至少应有一个节点可以访问)。

外部负载均衡器将为 UCP 控制平面提供一个虚拟 IP 地址,我们还将为端口4436443(如果你更改了它们,使用自定义端口)提供端口透传路由。我们将添加此外部负载均衡器的虚拟 IP 地址以及关联的完全限定域名(FQDN)作为 SAN。实际上,我们将根据环境的需要,使用--san参数添加所需的多个 SAN。

这些步骤对于你的组织访问和Docker 可信注册表DTR)至关重要,因为通常将两者集成到 UCP 中。在这种情况下,DTR 将请求 UCP 进行用户身份验证,因此必须能够访问和解析 UCP 的 FQDN 和端口。

我们将在外部负载均衡器上使用透传或透明代理,允许 UCP 的后端管理 TLS 证书和连接。

UCP 镜像将允许我们执行以下操作:

  • 使用installuninstall-ucp操作安装和卸载 UCP。卸载选项将从所有集群节点中移除所有 UCP 组件。我们无需执行其他程序即可完全从节点中删除 UCP。Docker 引擎不会被移除。

  • 使用images选项从 Docker Hub 下载所需的 Docker 镜像。

  • 使用backuprestore操作备份和恢复 UCP 管理节点。

  • 使用iddump-certs选项提供 UCP 集群 ID 及其证书。转储证书使我们能够安全地存储它们,以避免如果我们不小心删除任何必要的卷时出现证书问题。

  • 使用support操作创建支持转储。这些转储将包含关于我们环境的所有有用信息,包括应用程序/容器日志。

  • 执行upgrade选项来升级 UCP 平台。此选项将升级所有 UCP 组件,可能会影响我们的服务。建议添加--manual-worker-upgrade参数,以避免工作节点被自动升级。我们需要处理工作负载并将其移动到工作节点上,然后手动升级 UCP。

  • 创建一个示例 UCP 配置文件并验证所需的端口状态。UCP 可以通过提供的 Web UI 或使用配置文件进行配置。使用配置文件将允许我们保持可重复性,且可以通过任何配置管理应用来管理更改。此方法可以在 UCP 安装完成后使用,或者在安装过程中通过自定义 example-config 选项生成的示例配置文件,并使用 docker/ucp install --existing-config 来应用这个修改后的文件。可用的选项在以下链接中描述:docs.docker.com/ee/ucp/admin/configure/ucp-configuration-file

以下是最常用的 UCP 安装选项:

选项 描述
--swarm-grpc-port,--controller-port,--kube-apiserver-port--swarm-port 这些选项允许我们修改多个服务使用的默认端口。最重要的端口,可能会在你的环境中进行定制,分别是 kube-apiserver-port(默认值为 6443)和 controller-port(默认值为 443)。它们发布 Kubernetes 和 UCP 用户端点,允许我们与集群进行交互。
--host-address--data-path-addr 第一个选项设置哪个节点的 IP 地址将用于发布控制平面。第二个选项允许我们将控制平面与数据平面隔离开来。我们为数据平面设置一个不同的接口或 IP 地址。
--pod-cidr, --service-cluster-ip-range--nodeport-range 这些选项允许我们自定义 Kubernetes Pods 和 Services 的 IP 地址范围以及 NodePort 服务的发布端口。
--external-server-cert 使用此选项,我们可以在 UCP 集群内配置自己的证书。
--san 我们根据需要在 UCP 证书中添加这些别名的 SAN(主题备用名称)。考虑用户和管理员如何使用 UCP 集群,并添加与这些服务相关的 FQDN 名称。
--admin-username--admin-password 建议在安装过程中设置管理员用户名和密码,以提供可重复的工作流程。我们将避免使用 --interactive 选项,从而实现 基础设施即代码 (IaC) 的 UCP 安装过程。

一旦 UCP 安装在第一个管理节点上,我们只需要将其他管理节点和工作节点加入集群,正如我们在第八章《使用 Docker Swarm 的编排》中所学到的那样。为了获取所需的 docker join 命令行,我们只需执行 docker swarm join-token manager 用于管理节点,执行 docker swarm join-token worker 用于工作节点。然后我们复制它们的输出,并在每个管理节点和工作节点上执行 docker join 命令。这个过程非常简单。

也可以通过访问共享资源 | 节点菜单,从 Web UI 获取所需的加入指令。

UCP 集群中的节点可以与 Docker Swarm 或 Kubernetes 一起工作,甚至可以使用混合模式。这允许节点同时运行 Docker Swarm 和 Kubernetes 工作负载。

不推荐在生产环境中使用混合模式,因为协调器不会共享其负载信息。因此,一个协调器的节点可能几乎满载,而另一个协调器的节点可能为空。在这种情况下,非满载的协调器仍然可以继续接收新的工作负载,从而影响其他协调器的应用性能。

总结一下,我们安装了 Docker Engine,然后安装了 UCP。我们回顾了这个过程以及在我们的环境中安装 Docker Enterprise 平台所需的主要参数。

如果你计划在 Amazon AWS 或 Microsoft Azure 云上安装 Docker Enterprise,你应该阅读 Docker 文档中的具体说明和选项(AWS 版本:docs.docker.com/ee/ucp/admin/install/cloudproviders/install-on-aws; Azure 版本:docs.docker.com/ee/ucp/admin/install/cloudproviders/install-on-azure)。

现在,我们将回顾 UCP 环境。

回顾 Docker UCP 环境

在这一部分,我们将回顾 Docker UCP 环境。我们可以使用 Web UI、命令行或者其发布的 REST API。在本书中,我们将覆盖 Web 应用界面以及如何将我们的 docker 客户端命令行与 UCP 集成。

首先,我们将介绍 Web UI。

Web UI

Web UI 将在所有管理节点上运行。我们将使用 UCP 的完全限定域名,该域名与其虚拟 IP 地址相关联。默认使用端口 443,除非你手动配置了不同的端口。如果我们在浏览器中打开 https://<UCP_FQDN>:<UCP_PORT>,我们将访问 UCP 登录页面。如果我们使用的是自动生成的证书,浏览器会提醒我们存在不受信任的 CA。这是正常现象,因为 UCP 会自动为我们生成一个内部 CA,用于签署所有内部和外部证书。我们可以将公司的或私人证书上传到 UCP。

请记得在外部负载均衡器上应用直通(或透明代理)配置,以访问 UCP 后端。我们将使用 https://<MANAGER_IP>:<UCP_PORT>/_ping 来进行后端健康检查。

让我们快速回顾一下 UCP 的 Web UI。以下截图显示了主要的登录界面。在安装过程中,我们设置了管理员密码,可以通过执行该过程并使用 --interactive 参数进行交互,或者通过添加 --admin-username--admin-password 参数来自动化这些设置。安装过程中使用的用户名和密码应当用于登录 UCP。

主页面还会提示我们,如果在安装过程中没有应用许可证文件,需要添加一个。可以使用 --license 参数来完成 docker run docker/ucp:<RELEASE> install

下图显示了 UCP 仪表盘。每个用户都可以访问自己的仪表盘。左侧面板将提供访问用户个人资料、仪表盘、访问控制、共享资源以及与 Kubernetes 和 Swarm 相关的资源:

Dashboard 屏幕展示了集群组件状态的快速回顾。它还提供了 Swarm 和 Kubernetes 工作负载的摘要以及集群负载的概览。不要认为这就足够进行监控,因为这太简单了。我们应添加监控工具来增强警报、性能报告和容量规划功能。

Access Control 仅在 UCP 管理员访问集群的 Web UI 时出现。管理员将能够管理 RBAC 的所有行为:

  • Orgs & Teams 提供了一个接口,用于创建组织和团队,并将用户集成到其中。

  • Users 端点将允许我们按预期管理用户。我们将在下一个主题中学习如何创建和管理用户。

  • Roles 提供了一个用于 Kubernetes 和 UCP 角色的接口。Kubernetes 资源应使用声明性方法(使用 YAML 资源文件)进行管理,而 Docker Swarm 的资源(由 UCP 管理)将通过 Web UI 创建。

  • Grants 帮助我们管理 Kubernetes 角色绑定和 Swarm 角色及集合集成。

Shared Resources 提供了对 Kubernetes 或 Docker Swarm 资源的访问。我们将管理集合、堆栈、容器、镜像和节点:

节点可以通过 Nodes 入口点进行管理。我们将设置节点属性和编排器模式。正如我们所看到的,添加新节点非常简单。Add Node 选项向我们展示了集群的docker join命令行。我们只需将此指令复制到新节点的终端中即可。这同样适用于 Microsoft Windows 节点。

Stacks 将显示部署在 Docker Swarm 集群上的多容器或多服务应用程序。此视图还显示 Kubernetes 工作负载。

Kubernetes 和 Swarm 端点显示了每个编排器的特定资源:

  • Kubernetes 显示命名空间、服务账户、控制器、服务、入口资源、Pod 配置和存储。我们将能够更改所有用户 Web UI 端点所使用的命名空间。我们还将使用声明性方法回顾和创建 Kubernetes 资源。

  • Swarm 允许我们创建和查看服务、卷、网络、机密和配置。

我们可以查看 UCP 文档以及 Kubernetes 和 UCP API 文档。这将帮助我们基于 REST API 集成实现自动化流程。

使用 UCP 包的命令行

UCP 包可能是访问您用户和管理员最重要的部分。虽然每个用户都可以访问 UCP 的 Web UI、CI/CD、监控工具和 DevOps,但用户将使用 Docker 命令行查看和启动他们的工作负载。因此,这种访问应该是安全的。请记住,Docker Swarm 部署了加密的控制平面。其所有内部通信将通过 TLS 进行加密保护。然而,用户的访问是没有加密的。另一方面,UCP 提供了一个完全安全的解决方案。通过使用 TLS 保护用户和管理员访问来确保安全。这个安全是通过个性化证书来管理的。每个用户都会获得一组属于他们自己的证书。Kubernetes 访问同样通过 UCP 用户包来加密保护。

要获取此 UCP 包,用户可以使用 Web UI,或者通过一个简单的curl命令——或任何命令行 Web 客户端——下载这个压缩为 ZIP 文件的包:

上面的截图展示了通过 Web GUI 访问用户包文件。我们只需通过浏览器下载它。一旦文件下载到我们的计算机上,我们将解压它。此文件包含证书、配置和脚本,用于在我们的计算机上加载客户端环境,无论它是运行 Linux、Mac 还是 Windows 操作系统。每个操作系统都有一个对应的环境文件。我们将查看 Linux 系统中的内容和过程,但在 Windows 系统中也类似(只是命令不同)。

我们可以使用curljq从命令行下载用户包:

$ AUTHTOKEN=$(curl -sk -d '{"username":"<username>","password":"<password>"}' https://<UCP_FQDN>:<UCP_PORT>/auth/login | jq -r .auth_token)

$ curl -k -H "Authorization: Bearer $AUTHTOKEN" https://<UCP_FQDN>:<UCP_PORT>/api/clientbundle -o ucp-bundle-admin.zip

如果我们使用unzip解压缩管理员包文件ucp-bundle-admin.zip,将获得连接到集群所需的所有文件:

$ unzip ucp-bundle-admin.zip

然后,我们将加载此环境。我们将在 Microsoft Windows PowerShell 中使用env.ps1,或者在命令提示符中使用env.cmd。在 Linux 主机上,我们将使用env.sh。当我们在 Shell 中加载此环境时,就可以使用dockerkubectl客户端软件远程连接到 UCP 集群:

$ source env.sh
 Cluster "ucp_<UCP_FQDN>:6443_admin" set.
 User "ucp_<UCP_FQDN>:6443_admin" set.
 Context "ucp_<UCP_FQDN>:6443_admin" created.

请注意,Kubernetes 上下文也已经设置好。因此,我们将能够管理集群并在 Kubernetes 或 Docker Swarm 上部署工作负载。每个用户的 UCP 包必须安全存储。如果我们删除它,可以生成一个新的,但要保持安全;否则,可能会有人利用它获取对您的环境文件的访问权限。

UCP 为我们的管理节点提供了适用于 Microsoft Windows 和 Linux 的客户端软件,网址为https://<UCP_FQDN>:<UCP_PORT>/manage/dashboard/dockercli。我们可以下载这些软件以连接到集群。

使用 UCP 包连接集群,而不是通过 SSH 或其他本地访问方式,是至关重要的。我们绝不会允许本地访问集群节点。每个人必须通过命令行或 Web UI 访问集群。

UCP 的 REST API 也使用证书进行安全保护。我们需要 UCP 包中的证书文件才能使用其 API 访问集群。

我们将在下一节中审查 UCP 的访问控制,并提供一个简单的示例,帮助我们理解 RBAC 概念。

基于角色的访问控制与隔离

基于角色的访问控制RBAC)管理 Docker Swarm 和 Kubernetes 的授权。Docker Enterprise 让我们管理用户对资源的访问权限。我们使用角色来允许用户查看、编辑和使用集群资源。

授权基于以下概念:

  • 主题:我们在组织中管理用户、团队和服务账户。用户是团队的一部分,包含在组织中。

  • 资源:这些是我们在第一章中讨论的 Docker 对象组,现代基础设施与应用程序使用 Docker。由于 Kubernetes 也集成到了 UCP 集群中,因此 Kubernetes 资源也属于这些组。UCP 管理按集合分组的资源。

  • 集合:这些是资源的集合,包括不同种类的对象,如卷、密钥、配置、网络、服务等。

  • 角色:这些是权限的集合,我们将其分配给不同的主题。角色定义了谁可以做什么。

  • 授权:将主题与角色和资源集结合,我们获得了授权。这些是应用于资源组的有效用户权限。

服务账户仅对 Kubernetes 有效。这些不是用户账户;它们与分配管理访问权限的应用程序或 API 相关联。

有一些预定义的角色,但我们可以创建自己的角色。以下是 Docker Enterprise 的 UCP 中包含的默认角色列表:

  • 无:此角色不提供任何 Docker Swarm 资源的访问权限。此角色应为新用户的默认角色。

  • 仅查看:拥有此角色的用户可以查看服务、卷和网络等资源,但无法创建新资源。

  • 限制控制:拥有此角色的用户可以查看和编辑资源,但不能使用绑定挂载(主机目录)或通过 docker exec 在容器中执行新进程。他们不能运行特权容器或具有增强功能的容器。

  • 调度员:此角色允许用户查看节点,从而能够在节点上调度工作负载。默认情况下,所有用户都对 /Shared 集合获得 Scheduler 角色授权。

  • 完全控制:此角色应仅限于高级用户。这些用户可以查看和编辑卷、网络和镜像,也可以创建特权容器。

用户将只能管理自己的容器或 Pods。这种行为使得可以将命名空间(Kubernetes)和集合(Docker Swarm)结合在一起。因此,具有对集合中一组资源完全控制访问权限的用户,将在 Docker Swarm 中对这些资源拥有所有权限。如果我们在命名空间中添加资源,并且该用户包含在 Kubernetes 中的完全特权角色中,同样的情况也会发生。

还有一个更高级的角色是分配给 Docker Enterprise 管理员的。他们将拥有对 UCP 环境的完全控制和管理权限。可以通过用户属性页面上的“Is Admin”复选框进行管理。

授权将用户和权限与应应用的资源集合相互连接。授权管理工作流包括它们的创建、用户分配、应应用的角色以及资源关联。通过这种方式,我们确保为分配给一组用户(或单个用户)的资源集合应用适当的权限。

集合是分层的,并包含资源。它们通过类似目录的结构来表示,每个用户都有自己的私人集合,并且拥有默认的用户权限。通过此方法,我们可以嵌套集合。一旦授予用户对某个集合的访问权限,他们将可以访问该集合的所有层级子集合。主要有三个集合://System/Shared

/System 集合下,我们将找到 UCP 的管理节点以及 UCP 和 DTR 的系统服务。默认情况下,只有管理员才能访问此集合。另一方面,/Shared 集合将包含所有准备好运行工作负载的工作节点。我们可以添加额外的集合并将一些工作节点移动到这些集合中,以便将它们隔离并提供多租户功能。将工作节点分布在不同的集合中,还可以为不同的团队或租户分配工作负载执行。

每个用户默认都有一个私人集合,位于/Collections/Swarm/Shared/Private/<USER_NAME>。这确保用户的工作负载默认是安全的,只有管理员才能访问。因此,用户必须在他们团队共享的集合中部署工作负载。

与集合关联的标签管理用户对资源的访问,这使得动态地允许或禁止用户的可见性变得容易。

让我们通过一个简短的示例来回顾这些概念。

我们的组织(myorganization)中有两个项目:projectAprojectB。我们还假设在我们的组织中有三个团队:开发人员、质量保证和 DevOps。让我们描述一些用户及其在组织中的角色:

  • 开发人员dev1dev2

  • 质量保证: qa1qa2

  • DevOps: devops1devops2

  • UCP 管理员

以下图片展示了用户创建过程的一些截图。首先,我们创建一个组织,然后在之前创建的组织内创建团队和用户:

每个用户将在 UCP 中拥有自己的用户账户。我们将创建开发人员、质量保证和 DevOps 团队,并添加他们的用户。我们还将创建三个主要的集合作为阶段,并为每个项目创建子集合。因此,我们将有以下内容:

  • development/projectAdevelopment/projectB

  • certification/projectAcertification/projectB

  • production/projectAproduction/projectB

假设每个开发者一次只负责一个项目。在开发阶段,他们应该对自己的项目拥有完全访问权限,但在认证阶段,他们只能拥有查看权限。质量保证用户在认证阶段只能创建和修改他们的部署。DevOps 可以在生产环境中创建和修改资源,并允许开发者仅对projectA进行查看权限访问。实际上,projectB应该是安全的,只有devops2用户才能修改此项目的资源。

以下截图展示了为用户添加权限以允许其访问集合的过程。首先,我们创建一个集合,然后将该集合添加到新角色中:

我们将启动两个部署,并使用不同的用户查看这些部署的情况。

我们假设所有所需的用户已经创建,并且每个用户的ucp-bundle已经下载。作为dev1用户,我们将为projectB创建一个简单的nginx部署,使用com.docker.ucp.access.label=/development/projectB作为标签:

(as user dev1)$ source env.sh 
Cluster "ucp_192.168.56.11:6443_dev1" set.
User "ucp_192.168.56.11:6443_dev1" set.
Context "ucp_192.168.56.11:6443_dev1" modified.

(as user dev1)$ docker service create --name nginx-dev --label com.docker.ucp.access.label=/development/projectB nginx
k7hsizrvlc0cmy9va78bri06k
overall progress: 1 out of 1 tasks 
1/1: running [==================================================>] 
verify: Service converged 

(as user dev1)$ $ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
k7hsizrvlc0c nginx-dev replicated 1/1 nginx:latest 

如果我们现在模拟qa1用户,我们将得到不同的结果:

(as user qa1)$ source env.sh 
Cluster "ucp_192.168.56.11:6443_qa1" set.
User "ucp_192.168.56.11:6443_qa1" set.
Context "ucp_192.168.56.11:6443_qa1" modified.

(as user qa1)$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS

用户qa1将无法列出任何服务,因为它没有访问dev1集合的权限。但是,如果我们使用devops2用户查看这个列表,我们将获得一个包含dev1用户服务的列表:

(as user devops2)$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
k7hsizrvlc0c nginx-dev replicated 1/1 nginx:latest 

如果我们尝试修改这个资源(nginx-dev服务),我们将遇到访问错误,因为我们只有查看权限。另一方面,dev2用户可以扩展副本数,因为他们属于开发者组:

(as user devops2)$ docker service update --replicas 2 nginx-dev
Error response from daemon: access denied:
no access to Service Create, Service Update, on collection 8185981a-5e15-4906-9fbf-465e9f712918
no access to Service Create, Service Update, on collection 8185981a-5e15-4906-9fbf-465e9f712918
no access to Service Update, on collection 8185981a-5e15-4906-9fbf-465e9f712918

(as user dev2)$ docker service update --replicas 2 nginx-dev
nginx-dev
overall progress: 2 out of 2 tasks 
1/2: running [==================================================>] 
2/2: running [==================================================>] 
verify: Service converged 

为了完成这个示例,我们将作为devops2用户创建两个不同的服务。我们将分别从projectBprojectA部署安全和不安全的服务:

(as user devops2)$ docker service create --quiet --name nginx-prod-secure \
--label com.docker.ucp.access.label=/production/projectB nginx

4oeuld63v96ck26efype57320

(as user devops2)$ docker service create --quiet --name nginx-prod-unsecure \
--label com.docker.ucp.access.label=/production/projectA nginx

txmuqfcr751n8cb445hqu73td

(as user devops2)$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
k7hsizrvlc0c nginx-dev replicated 2/2 nginx:latest 
4oeuld63v96c nginx-prod-secure replicated 1/1 nginx:latest 
txmuqfcr751n nginx-prod-unsecure replicated 1/1 nginx:latest

在这种情况下,devops1用户应该只能管理与projectA关联的nginx-prod-unsecure服务:

(as user devops1)$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
k7hsizrvlc0c nginx-dev replicated 2/2 nginx:latest 
txmuqfcr751n nginx-prod-unsecure replicated 1/1 nginx:latest

这是使用标签进行授权管理的一个简单示例。在这个示例中,我们手动添加了这些标签,但如果需要,我们可以为每个用户设置默认集合。这将提供一个与他们的工作流相关联的默认标签,而不是使用开箱即用的/Collections/Swarm/Shared/Private/<USER>集合。我们还可以将约束与集合关联,以确保特定位置的设置。这在多租户环境中非常重要。

UCP 的 Kubernetes 集成

正如我们所学,Kubernetes 在安装 UCP 时与 Docker Swarm 一起部署。如果我们查看所有必需的 Kubernetes 组件,我们会注意到它们都作为容器在集群中运行。所需的键值存储也会提供。端口 6443(默认)将提供 Kubernetes 访问,用户和管理员将使用此端口来管理集群或执行工作负载。

我们将使用 Docker 包中的证书和配置文件 kube.yml。正如我们在本章中学到的,我们将加载用户的包环境,然后使用 kubectl 命令行访问 Kubernetes 集群。

一旦使用 source env.sh 加载了 env.sh,我们就会获得所需的环境变量并访问我们的证书。如果使用 kubectl get nodes 获取 Kubernetes 集群节点,我们将得到它们的状态:

$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
node1 Ready master 4d13h v1.14.8-docker-1
node2 Ready master 4d13h v1.14.8-docker-1
node3 Ready master 4d13h v1.14.8-docker-1
node4 Ready <none> 4d12h v1.14.8-docker-1

如果使用 kubectl get pods -n kube-system 查看 kube-system 命名空间中运行的 Pods,我们会注意到 Kubernetes 的 calicocompose 也已部署:

$ kubectl get pods -n kube-system
 NAME READY STATUS RESTARTS AGE
 calico-kube-controllers-5c48d7d966-cncw2 1/1 Running 3 4d13h
 calico-node-8sxh2 2/2 Running 6 4d13h
 calico-node-k2fgh 2/2 Running 6 4d13h
 calico-node-nrk62 2/2 Running 6 4d13h
 calico-node-wgl9c 2/2 Running 6 4d13h
 compose-779494d49d-wk8m4 1/1 Running 3 4d13h
 compose-api-85c67b79bd-7sbhj 1/1 Running 4 4d13h
 kube-dns-6b8f7bdd9-g6tfq 3/3 Running 9 4d13h
 kube-dns-6b8f7bdd9-ls2z2 3/3 Running 9 4d13h
 ucp-metrics-6nfz4 3/3 Running 9 4d13h
 ucp-metrics-hnsfb 3/3 Running 9 4d13h
 ucp-metrics-xdl24 3/3 Running 9 4d13h

这些组件非常重要,因为 Calico 是 UCP 默认部署的 CNI。它允许我们在集群范围内部署分布式应用。即使 Pods 和服务没有在同一主机上运行,它们仍然能够在集群内进行通信。在 Docker Swarm 中不需要这个,因为覆盖网络默认包含在内。Calico 还允许我们提高 Kubernetes 的安全性,因为它可以部署网络策略来隔离和管理 Pods 和服务之间的通信。

另一方面,Compose for Kubernetes 为 Docker Swarm 和 Kubernetes 提供了一个标准接口。Docker 堆栈可以部署在 Docker Swarm 或 Kubernetes 上。

我们还可以注意到,ucp-metrics 也运行着 Kubernetes 工作负载,作为其他与系统相关的部署,使用 kubectl get deployments -A 可以获得:

$ kubectl get deployments -A 
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE 
kube-system calico-kube-controllers 1/1 1 1 4d14h 
kube-system compose 1/1 1 1 4d14h 
kube-system compose-api 1/1 1 1 4d14h 
kube-system kube-dns 2/2 2 2 4d14h 

Kubernetes 角色和角色绑定可以通过命令行和 Web UI 进行管理。所有 1.14.8 版本发布中的 Kubernetes 功能都可以使用。这一点也非常重要。Docker Enterprise 提供的是原生 Kubernetes 版本,并且产品发布也会升级 Kubernetes,但你不能手动升级 Kubernetes。

在下一部分,我们将回顾主要的管理任务和安全性改进。

UCP 管理和安全性

UCP 管理员管理 Docker Swarm 和 Kubernetes 集群。他们集成外部 LDAP/AD 认证。认证可以委托,但 UCP 管理授权,正如我们在 基于角色的访问控制和隔离 部分中学到的那样。

以下截图显示了管理员设置端点:

Docker Enterprise 许可证可以在安装过程中引入,但也可以在管理员设置的 Web UI 中进行管理。此端点还允许我们执行以下管理任务:

  • 轮换 Docker Swarm 的令牌以提高集群的安全性。令牌仅用于将节点加入集群;我们可以在需要时更改它们。

  • 管理 Interlock 的端口并启用使用此功能发布应用程序。我们将在第十二章中讨论 Interlock,Docker 企业版中的应用程序发布

  • 配置一些集群配置,如 UCP 的端口和键值数据库快照。

  • 集成外部 LDAP 并配置新用户的默认角色和一些会话设置。此选项将身份验证委托给外部 LDAP/AD 系统,如果该系统不可用,UCP 将仅作为身份验证缓存。我们使用属性设置用户筛选器,以便仅集成 UCP 环境中的部分用户。UCP 会定期同步 LDAP 更改。

  • 更改 UCP 的应用程序和审计日志级别。

  • 通过 Web UI 执行和配置备份。

  • 集成 Docker 受信注册表和 Docker 内容信任,只允许签名镜像。这将应用于集群中的所有节点。

  • 为新节点设置默认编排器。我们可以选择 Docker Swarm、Kubernetes 或混合模式。

  • 授权管理员或用户在 UCP 管理节点或运行 DTR 的工作节点上执行工作负载。我们将决定谁可以在管理节点上运行工作负载。推荐避免在管理节点上运行非控制平面工作负载。

  • 自定义并启动平台升级。这将允许我们在完全自动化的流程和部署手动升级到工作节点之间做出选择,以避免影响应用程序服务。

推荐禁止在 UCP 管理节点上运行应用程序工作负载。这些节点应仅运行 UCP 系统和 DTR 容器。这样可以避免由于 UCP 控制平面导致的应用程序性能问题。另一方面,如果某个应用组件消耗了过多资源,控制平面也不会受到影响。

在生产环境中只允许签名镜像至关重要。这将确保镜像的来源和 CI/CD 工作流。还可以要求某些特定的签名来标识镜像。例如,我们可以确保只有由 运营团队开发主管IT 经理 签名的镜像才能在生产环境中运行。这将适用于集群中的所有节点。

UCP 和 Kubernetes 的许多功能可以通过 UCP 的 REST API 查询或修改。我们应查看文档 https://<UCP_FQDN>[:443]/apidocs/https://<UCP_FQDN>[:443]/kubernetesdocs/

UCP 还提供了一些在 Kubernetes 集群中默认应用的 Pod 安全策略。这些 Pod 安全策略将执行以下操作:

  • 管理特权容器。

  • 配置主机的命名空间(IPC、PID、网络和端口)。

  • 管理主机的路径及其权限和卷类型。

  • 管理容器进程执行的用户和组,以及容器内的setuid权限。

  • 更改默认容器的权限。

  • 集成 Linux 安全模块

  • 允许使用 sysctl 配置主机内核

默认情况下,只有管理员才能在 UCP 的 Kubernetes 中部署特权容器。这是在特权 Pod 安全策略中配置的。默认情况下,UCP 提供了两个特殊的策略,如我们在 kubectl get PodSecurityPolicies 输出中看到的:

$ kubectl get PodSecurityPolicies
NAME PRIV CAPS SELINUX RUNASUSER FSGROUP SUPGROUP READONLYROOTFS VOLUMES 
privileged true * RunAsAny RunAsAny RunAsAny RunAsAny false *
unprivileged false RunAsAny RunAsAny RunAsAny RunAsAny false *

你可以在 Kubernetes 文档中阅读更多关于 Docker Enterprise 包含的 Pod 安全策略及如何创建新策略的内容,或者访问此博客文章:www.mirantis.com/blog/understanding-kubernetes-security-on-docker-enterprise-3-0/

Admission controllers(准入控制器)是 Kubernetes 安全性中的其他重要组件。它们拦截 Kubernetes API 请求,在调度或执行任何操作之前允许或修改这些请求。这样,我们可以强制在资源上应用默认的安全策略,即使用户尝试执行不被允许的操作。Admission controllers 被应用于 Kubernetes API 进程。因此,我们应该检查 ucp-kube-apiserver 容器的命令行选项,以验证哪些 Admission controllers 已应用于我们的环境。由于 Kubernetes 目前尚未包含在 DCA 考试中,我们将在这个话题上停止讨论。但重要的是要理解,Docker Enterprise 使用公认的 Kubernetes 机制在 Kubernetes 中应用安全性。UCP 应用三个特殊的 Admission controllers,以防止任何人删除 UCP 所需的核心 Kubernetes 角色,确保在需要时进行镜像签名,并仅在非混合节点上管理非系统 Kubernetes Pods 的执行。

在接下来的部分,我们将回顾如何创建和恢复 UCP 的备份。

备份策略

在这一部分,我们将学习如何备份和恢复 Docker Enterprise UCP 平台。

由于 UCP 运行在 Docker Swarm 之上,因此在准备良好的备份策略时,这是第一个需要回顾的组件。

我们应该定期备份 Docker Swarm。这些备份将允许我们恢复集群配置和工作负载。

Docker Swarm 的备份

我们在第八章中介绍了如何执行 Docker Swarm 备份,使用 Docker Swarm 进行编排。在那一章中,我们描述了需要关注的内容。接下来,让我们了解实施 Docker Swarm 生产级备份的步骤,以便为 Docker Enterprise 平台做好准备。

确保已启用自动锁定功能,以提高对 Docker Swarm 数据的安全访问,因为我们将需要它。锁定密钥不会与备份一起存储。你应该将其存放在安全的地方。

我们将在所有非领导管理节点上执行备份步骤。这将确保除领导节点外的任何管理节点都可以恢复。事实上,完全摧毁一个集群不应该容易,因此仅备份一个节点应该是足够的。如果所有集群完全丢失,我们将恢复该节点,然后添加其他节点,就像安装过程中一样。你的集群健康状况不应依赖于备份恢复功能。这就是为什么我们运行 Raft 协议来同步集群组件并运行多个管理节点。

应用程序部署及其配置应存储在代码库中,正如我们在本书中提到的几次一样。有时,使用自动化工具重新部署一个新的集群并再次启动所有应用程序更加容易。

以下步骤建议用于创建 Docker Swarm 调度器数据的良好备份:

  1. 在执行此备份过程之前,请验证平台是否健康。

  2. 我们将通过执行 systemctl stop docker 来停止非领导管理节点上的 Docker 引擎。

  3. 使用 /var/lib/docker/swarm 目录内容创建一个 .tar 文件:tar -cvzf "<DIRECTORY_FOR_YOUR_BACKUPS>/swarm-backup-$(hostname -s)-$(date +%y%m%d).tgz" /var/lib/docker/swarm/

  4. 通过执行 systemctl start docker 再次启动 Docker 引擎。

  5. 我们可以在其他非领导管理节点上执行此过程,但如果备份成功,我们将仅能通过一个节点来恢复 Docker Swarm。

UCP 运行在 Docker Swarm 之上。让我们回顾一下备份 UCP 所需的步骤。

备份 UCP

与 Docker Swarm 不同,在 UCP 中,执行备份时无需暂停或停止任何平台组件。这个功能相当新。在旧版本中,备份执行时必须暂停节点上的组件。我们将只在单个节点上执行备份,因为 UCP 数据将允许我们恢复整个集群。但关于这个备份有一些重要的注意事项:

  • 该备份不包括 Docker Swarm 部署的工作负载、网络、配置或秘密。

  • 我们不能使用旧版本的备份来恢复更新后的 UCP。

  • ucp-metrics-dataucp-node-certs 卷不包含在备份中。

  • Kubernetes 数据将包含在 UCP 备份中。

  • 无论是 Router Mesh 还是 Interlock 设置都不会被存储。一旦恢复的组件重新部署,配置也将被恢复。

  • 备份内容将存储在一个 .tar 文件中,位置由用户定义。它可以通过密码短语进行保护。

我们可以通过 Web UI、命令行或其 API(在最新版本中)来创建 UCP 备份。

使用命令行时,我们需要使用 ucp 发布容器。针对本书写作时的当前版本,我们将使用 docker/ucp:3.2.4 镜像。要通过命令行创建备份,我们将执行 docker container run docker/ucp:<RELEASE> backup

$ docker container run \
 --rm \
 --interactive \
 --log-driver none \
 --name ucp \
 --volume /var/run/docker.sock:/var/run/docker.sock \
 --volume <FULL_PATH_FOR_UCP_BACKUP_DIRECTORY>:/backup \
 docker/ucp:3.2.4 backup \
 --file <BACKUP_FILENAME>.tar \
 --passphrase "<PASSPHRASE>" \
 --include-logs=false

在这个例子中,我们没有包括 UCP 平台日志(默认情况下会包括)。如果启用了 SELinux(推荐启用),我们还需要添加 --security-opt label=disable

使用 Web UI,我们首先导航到管理员设置。然后,我们选择备份管理员,最后点击立即备份以立刻启动备份执行。

本书中不会涉及 API 方法以及如何在流程完成后验证备份内容,但在 Docker 文档网站上有相关描述。建议查阅 docs.docker.com/ee/admin/backup/back-up-ucp/ 上提供的最新备份信息。

要恢复 UCP 备份,我们可以从以下几种情况开始:

  • 我们可以从头开始,在新安装的 Docker 企业引擎上恢复 UCP 备份。

  • 我们还可以在已启动的 Docker Swarm 上恢复 UCP 备份,从而恢复一个新的、完全功能的集群。

  • 我们可以在创建 UCP 的 Docker Swarm 集群上恢复 UCP。只需选择其中一个管理节点,在先前的 UCP 部署完全卸载后运行恢复过程。这是唯一一种先前创建的用户包仍然有效的情况。

如果从头开始恢复或使用新的 Docker Swarm 集群,之前使用的 IP 地址和 SAN 将无效。因此,我们需要在 UCP 恢复后重新生成服务器证书。

成功恢复 UCP 后,您可以像全新安装后那样添加新的管理节点和工作节点。

要恢复先前创建的备份,我们将执行 docker container run docker/ucp:<RELEASE> restore。我们需要使用与创建备份时相同的镜像版本。这一点非常重要,因为我们不能从不同版本恢复:

$ docker container run \
 --rm \
 --interactive \
 --name ucp \
 --volume /var/run/docker.sock:/var/run/docker.sock \
 docker/ucp:3.2.4 restore \
--passphrase "<PASSPHRASE>" < <FULL_PATH_FOR_UCP_BACKUP_DIRECTORY>/<BACKUP_FILENAME>.tar

重要的是要知道,备份和恢复是当其他一切都无法正常工作时需要执行的操作。我们部署高可用的 UCP 环境以避免突发情况。您必须积极监控集群环境,避免监控系统上出现未处理的错误或警报。如果发生管理节点故障,集群将继续工作,但我们必须尽快恢复健康状态。根据我的经验,生产环境中的大多数问题都与文件系统无限增长、进程消耗所有资源或通信问题有关。要关注这些潜在问题,监控集群健康,并定期备份(并更新备份流程),以确保在一切失败时能恢复环境。

在下一节中,我们将学习如何监控以及如何检查不同组件的状态,以避免未被察觉的故障。

升级、监控与故障排除

在本节中,我们将回顾集群升级必须如何部署。我们将在集群环境中进行操作。为了执行平台更新而不发生服务中断,需要遵循一些步骤。在生产环境中,监控和故障排除至关重要。我们将学习哪些重要的键和值应该进行检查,以确保集群的健康,并且我们应该遵循哪些步骤来排查降级或故障的环境。

升级你的环境

我们必须查看每个版本的 Docker UCP 发布说明和升级程序。在编写本书时,当前版本的文档可以在 Docker 网站上找到:docs.docker.com/reference/ucp/3.2/cli/upgrade

在任何操作之前,我们应始终执行备份,通常我们会从升级 Docker 引擎开始。你应该查看 Docker 文档,以确保这些步骤在不同版本之间没有变化。节点的升级应逐个进行。我们将从非领导管理节点开始。一旦所有管理节点都完成升级,我们将把正在运行的服务在不同的工作节点之间迁移,以确保在升级过程中尽量减少服务中断。

一旦所有 Docker 引擎实例都已更新,我们将开始进行 UCP 升级。我们可以通过 Web UI 或命令行执行此过程。我们建议遵循命令行步骤,因为此过程将提供更多的信息。如果所有必需的镜像已事先下载到所有节点,我们可以离线执行此过程。我们可以通过以下链接查看所需镜像:docs.docker.com/ee/ucp/admin/install/upgrade-offline/。我们将使用 docker image load -i <PACKAGE_WITH_ALL_IMAGES>.tar.gz 预加载所有镜像。

我们将使用适当的参数运行 docker container run docker/ucp upgrade 来升级我们的 UCP 环境。在执行此命令之前,Docker 引擎应该已经升级。

docker container run --rm -it \
 --name ucp \
 -v /var/run/docker.sock:/var/run/docker.sock \
 docker/ucp:3.2.X \
 upgrade --interactive

如果你的节点使用多个接口,我们还需要添加 --host-address 并提供适当的 IP 地址。

我们可以使用 --debug 调试选项运行并升级该过程,如果出现问题,这非常有助于识别错误。

当前版本有一个有趣的选项,因为我们可以使用 --manual-worker-upgrade 手动升级工作节点。这帮助我们控制对环境中已部署服务的影响。

除非使用 --manual-worker-upgrade 选项,否则所有 UCP 进程将在所有节点上进行升级。升级过程结束后,环境将完全升级到新版本。此时,验证集群的健康状态非常重要。

监控集群健康状态

我们可以使用命令行或 Web UI 来检查环境的健康状况。我们将使用常见的 Docker Swarm 命令,因为我们是在该调度器上运行环境的。我们可以通过 docker node ls 来查看节点状态。如果我们使用 Docker UCP 包连接到环境,可能会错过一些组件。确保您使用的是管理员用户,以便能够获取其健康状况。使用该包时,我们可以列出所有控制平面进程,并使用 docker container ls 来验证它们的状态。

我们可以通过每个管理节点的 IP 地址或 FQDN 名称,从 https://<ucp-manager-url>/_ping 端点获取管理节点的状态。对该 URL 的请求可以返回 200 状态码表示节点健康,若有故障组件则返回 500 状态码。

需要理解的是,必须在每个节点上验证该端点,因为我们是通过负载均衡器访问每个管理节点的。此配置有助于为环境提供高可用性。

Web UI 还提供集群状态信息。仪表板页面为我们提供环境的清晰状态概览。此页面包括管理节点和工作节点的错误、警告或挂起状态计数器。我们可以快速发现平台节点的错误。管理节点和工作节点的性能摘要可以帮助我们验证集群的规模及其使用情况。我们还可以看到在 Docker Swarm 和 Kubernetes 上部署的服务信息。通过这种方式,我们可以深入到不同的 UCP 部分,详细查看遇到的错误。以下截图展示了 UCP 仪表板显示描述的平台概览时的样子:

在每个 UCP 资源部分,我们将看到资源的状态及其属性。为了监控集群节点的健康状况,我们将查看“节点”部分,该部分位于共享资源下。在此部分,我们将检查每个节点的 CPU 和内存使用情况。此视图还将帮助我们在节点资源使用过高时发现可能的服务降级。

故障排除 UCP

在本章中,我们一直在回顾 UCP 组件的主要监控端点。集群中有一些关键端点。我们还描述了一些管理重要集群持久数据的数据库过程。这些组件运行在管理节点上,并在它们之间复制数据。这些应该足够支持 UCP 环境,但有时,可能会发生一些问题,导致节点之间的同步丢失。网络延迟和性能问题可能会导致这种情况的发生。

故障排除 UCP-KV

如果我们丢失了一些管理节点,ucp-kv 可能会显示不正确的节点数量。我们可以使用 etcdctl 检查配置的节点数量。我们可以直接在 ucp-kv 容器上执行 etcdctl

$ docker exec -it ucp-kv etcdctl \
 --endpoint https://127.0.0.1:2379 \
 --ca-file /etc/docker/ssl/ca.pem \
 --cert-file /etc/docker/ssl/cert.pem \
 --key-file /etc/docker/ssl/key.pem \
 cluster-health

如果配置的管理节点数量健康,它将显示cluster is healthy消息。如果ucp-kv不健康,我们应检查所有管理节点是否正常。如果我们删除了一个管理节点,但该更改未能正确更新到其他节点,可能会导致集群不健康。要恢复此状态,我们需要使用etcdctl member removeetcd数据库中删除已删除的节点(请参见etcd.io/docs/v3.4.0/op-guide/runtime-configuration/etcd文档)。

逐一更新组件配置及其状态(例如,删除节点)。在执行新的更新命令之前,等待更改在集群中同步。

我们还可能遇到认证数据库的问题。在下一部分,我们将学习如何在丢失一个管理节点的情况下,修正节点数量。

故障排除 UCP-Auth

首先,我们会检查当前健康的管理节点数量。如果其中一些节点仍然不健康,我们应首先解决这个问题。管理节点恢复健康后,如果认证数据库仍处于不一致状态,我们将按照以下步骤进行操作:

$ docker container run --rm -v ucp-auth-store-certs:/tls \
docker/ucp-auth:<RUNNING_VERSION> \
--db-addr=<HEALTHY_MANAGER_IP_ADDRESS>:12383 \
--debug reconfigure-db --num-replicas <NUMBER_OF_MANAGERS>

该命令将使用 RethinkDB 的reconfigure-db命令运行一个docker/ucp-auth容器,以修复正确数量的管理节点。

故障排除节点

正如我们之前提到的,网络延迟和性能问题可能会导致问题的出现。请注意节点资源和文件系统。如果管理节点资源耗尽,集群将会进入不健康状态。

如果节点无法在 10 秒内联系到,则会出现心跳失败的情况。如果工作节点无法联系到管理节点,它们将进入挂起状态。我们在本地检查这些节点,也会查看是否存在网络或性能问题。

管理节点也可能变得不健康。如果其他管理节点无法访问它们,ucp-controller进程将会受到影响。我们可以检查容器日志以排查网络问题。

这些是 Docker UCP 平台上最常见的一些问题。我们通常从查看 Web UI 仪表板和ucp-controller容器日志开始。如果其他组件看起来不健康,我们会检查它们的日志。

在下一章,我们将学习如何使用 Interlock 功能发布部署在 Docker Enterprise 平台上的应用。

总结

本章介绍了 Docker UCP 的主要功能。我们学习了如何部署具有高可用性的集群,并通过 UCP 的 Web UI 或用户捆绑包结合 Docker 和 Kubernetes 命令行来管理和部署工作负载。我们还介绍了 UCP 的基于角色的访问控制,它帮助我们精细化地管理集群资源的访问。我们还查看了 Web UI 和用于管理 Docker Enterprise 控制平面的主要配置。

我们还了解了 UCP 的组件,以及如何在生产中部署和管理 Docker Enterprise 的控制平面和用户资源。最后,我们学习了如何通过验证集群组件的状态和执行备份来确保平台的可用性。

在下一章,我们将学习如何使用 Docker 的集成工具和功能发布已部署的应用程序。

问题

  1. 以下哪些句子是正确的?

a) Docker UCP 安装过程还将安装 Docker Enterprise Engine 到我们的主机上。

b) UCP 提供一个集成的 RBAC 系统,帮助我们在其数据库中进行用户身份验证和授权。

c) Docker UCP 提供两种访问方式:Web UI 和 UCP 套件。

d) 上述所有句子都是正确的。

  1. 以下哪些句子关于 docker/ucp 镜像是错误的?

a) 该镜像将提供 UCP 的备份和恢复功能。

b) 我们应该始终在我们的环境中使用最新的 docker/ucp 版本。

c) 可以使用 docker/ucp 镜像完全删除 Docker UCP。

d) 升级过程必须在每个集群节点上手动执行。

  1. 我们从 UCP 安装过程学到了什么(以下哪项是正确的)?

a) 我们可以使用特殊参数更改 UCP 控制器和 Kubernetes 端口。

b) 我们可以使用 --data-path-addr 来指定数据平面使用的接口或 IP 地址,从而隔离控制平面。

c) 我们只能为 UCP 环境设置一个主题别名,默认情况下,这将是管理节点的 IP 地址。

d) 我们将在管理节点上使用 docker/ucp install 过程安装 UCP,然后加入工作节点。

  1. 以下哪项关于 UCP 高可用性是正确的?

a) UCP 部署在 Docker Swarm 集群之上,因此我们需要一个奇数个节点来提供高可用性。

b) 安装 UCP 后,我们需要部署高可用性的 Kubernetes。

c) 需要外部负载均衡器,通过透明代理(passthrough)将客户端请求分配到不同的节点,以允许管理节点提供端到端的 TLS 隧道。

d) 我们可以通过 https://<ucp-manager-url>/_ping 端点检查管理节点的可用性。

  1. 以下哪个角色在 UCP 中默认未包含?

a) Privileged

b) Full Control

c) Administrator

d) Scheduler

进一步阅读

有关本章所涵盖主题的更多信息,请参考以下链接:

在 Docker 企业版中发布应用程序

前一章帮助我们理解了 Docker 企业版的控制平面组件。Docker UCP 在相同节点上部署 Docker Swarm 和 Kubernetes 集群。这两个编排器共享主机组件和设备。每个编排器将管理自己的硬件资源。可用内存和 CPU 等信息不会在编排器之间共享。因此,如果我们在同一主机上同时使用这两者时,必须小心。

那么,如何发布部署在它们上的应用程序呢?我们已经学习了如何在 Docker Swarm 和 Kubernetes 上发布应用程序,但在企业环境中工作必须是安全的。本章将学习如何在 Docker 企业环境中发布应用程序,使用的是 UCP 提供的工具或社区工具。

本章将向我们展示 UCP 为 Docker Swarm 和 Kubernetes 提供的主要发布资源和功能。这些组件将帮助我们只发布前端服务,从而确保应用程序的安全性。我们将了解 ingress 控制器,这是发布 Kubernetes 应用程序的首选解决方案,以及 Interlock,这是 UCP 提供的一个企业级解决方案,用于在 Docker Swarm 中发布应用程序。

本章将涵盖以下主题:

  • 理解发布概念和组件

  • 深入分析你的应用程序逻辑

  • Ingress 控制器

  • Interlock

  • 章节实验

本章将从回顾与 Docker Swarm 和 Kubernetes 部署相关的一些概念开始。

第十六章:技术要求

你可以在 GitHub 仓库中找到本章的代码:github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

查看以下视频,观看代码演示:

"bit.ly/2EHobBy"

理解发布概念和组件

第八章,使用 Docker Swarm 进行编排,向我们展示了当应用程序部署在 Docker Swarm 集群上时的工作原理。

我们将使用服务对象在 Docker Swarm 中部署应用程序。如果它们运行在同一网络中,服务之间的内部通信始终是允许的。因此,我们将把应用程序的组件部署在同一网络中,它们将与其他已发布的应用程序进行交互。如果两个应用程序必须相互作用,它们应该共享网络或被发布。

发布应用程序很简单;我们只需指定主机上应该监听的端口。然而,我们了解到 Docker Swarm 会在所有集群主机上发布应用程序的端口,而路由器网格(Router Mesh)会将内部流量路由到合适的服务任务。在回顾多服务应用程序之前,我们先回顾一下与容器和服务相关的这些主题。

我们有不同的选项来发布容器应用程序,正如我们在第四章中所学到的,容器持久性与网络。为了使进程能够从容器的隔离网络命名空间中可见,我们将使用不同的网络策略:

  • 桥接网络:这是默认选项。容器的进程将通过主机的网络地址转换NAT)功能进行暴露。因此,监听某个端口的容器进程将与主机的端口绑定。NAT 规则将应用于 Linux 或 Microsoft Windows 容器。这使我们能够使用不同主机的端口执行多个容器实例。

我们将使用--publish-p选项(甚至使用--publish-all-P发布所有镜像声明的暴露端口)来发布容器进程,同时指定可选的 Docker 主机 IP 地址和端口,以及发布的端口和协议(TCP/UDP):docker container run -p [HOST_IP:HOST_PORT:]<CONTAINER_PORT>[/PROTOCOL]。默认情况下,将使用主机的所有 IP 地址和3276865000范围内的随机端口。

  • 主机的网络命名空间:在这种情况下,我们将使用主机的网络命名空间。进程将直接可用,监听主机的端口。容器与主机之间不会使用端口转换。由于进程端口是直接绑定的,因此每个主机只允许一个容器实例。我们将使用docker container run --net=host将新容器与主机的网络命名空间关联。

  • MacVLAN:这是一个特殊情况,容器将使用自己的命名空间,但它将在主机的网络级别可用。这使我们能够将 VLAN(虚拟局域网)直接附加到容器上,并使它们在实际网络中可见。容器将获得自己的 MAC 地址,因此服务将在网络中像节点一样可用。

这些是基本选项。我们将使用外部 DNS 来宣布如何访问这些服务。我们还可以在自定义桥接网络上部署容器。自定义网络具有自己的 DNS 命名空间,容器将在同一网络内通过其名称或别名互相访问。服务不会对同一网络中运行的其他服务进行发布。我们只会将它们发布到其他网络或供用户访问。在这些情况下,我们将使用 NAT(常见的桥接网络)、主机命名空间或 MacVLAN。

这些设置适用于独立主机,但如果我们将工作负载分布到整个集群中,情况将发生变化。现在我们将介绍 Kubernetes 网络模型。该模型必须涵盖以下情况:

  • 运行在节点上的 Pods 应该能够与其他主机上运行的 Pods 进行通信,而无需使用 NAT。

  • 系统组件(kubelet 和控制平面守护进程)应该能够与运行在主机上的 Pods 进行通信。

  • 运行在节点主机网络中的 Pods 可以与其他主机上运行的所有 Pods 进行通信,而无需使用 NAT。

如我们所知,Pod 中的所有容器共享一个 IP 地址,且所有 Pod 都运行在一个扁平化网络中。Kubernetes 中没有网络分割,因此我们需要其他工具来进行隔离。我们将使用网络策略来实现类似防火墙或网络 ACL 的规则。这些规则同样适用于发布服务(入口流量)。

Docker 的网络模型基于容器网络模型CNM)标准,而 Kubernetes 的网络模型则是使用容器网络接口CNI)模型来实现的。

Docker 的 CNM 管理互联网协议地址管理IPAM)和网络插件。IPAM 将用于管理地址池和容器的 IP 地址,而网络插件则负责管理每个 Docker 引擎上的网络。CNM 通过 Docker 引擎的libnetwork库来实现,尽管我们可以添加第三方插件来替代这个内置的 Docker 驱动程序。

另一方面,CNI 模式暴露了一个接口来管理容器的网络。CNI 将为 Pod 分配 IP 地址,尽管我们也可以添加外部 IPAM 接口,并使用 JSON 格式描述其行为。这些描述了当我们添加第三方插件时,任何 CNI 插件必须提供集群和独立网络的功能。如前所述,Docker 企业版的默认 CNI 插件是 Calico。它使用 IP 内嵌(IP in IP)封装提供集群网络和安全功能(尽管它也提供 VXLAN 模式)。

接下来我们继续。Docker 引擎提供了所有主机所需的网络功能,而 Kubernetes 也将使用 CNI 提供集群范围的网络。Docker Swarm 内置了使用 VXLAN 的集群级别网络功能。一个覆盖网络驱动程序通过主机的桥接网络接口在所有主机之间创建分布式网络。我们只需要初始化一个 Docker Swarm 集群,之后无需进行额外操作。将会创建一个入口覆盖网络和一个docker_gwbridge桥接网络。前者将管理与 Swarm 服务相关的控制和数据流量,而docker_gwbridge则用于在 Docker Swarm 覆盖网络中互连 Docker 主机。

我们通过加密覆盖网络提高了集群的安全性,但这也意味着我们会遇到一定的开销,并对性能造成轻微的负面影响。如在独立网络和容器共享网络中所展示的那样,所有连接到同一覆盖网络的服务将能够互相通信,即使我们没有发布任何端口。必须显式地使用-p [ HOSTS_PORT:]<CONTAINER_PORT>[/PROTOCOL]来发布应对外部服务网络可访问的端口。

发布服务端口的格式较长,虽然需要写更多内容,但它更清晰。我们将写作-p published=<HOSTS_PORT>,target=<CONTAINER_PORT>,protocol=<PROTOCOL>

在 Docker Swarm 中发布服务将在集群中的所有主机上公开定义服务的端口。这个特性是路由器网格。所有主机将发布这个服务,即使它们实际上并不运行任何服务进程。Docker 将通过内部入口覆盖网络将流量引导到集群中的服务任务。

记住,所有服务都会获得一个虚拟 IP 地址。这个 IP 地址在服务的生命周期内是固定的。每个服务由与容器关联的任务组成。Docker 会运行尽可能多的任务,因此容器,以便这个服务可以工作。每个任务将只运行一个容器,具有其自己的 IP 地址。由于容器可以在集群中的任何地方运行,并且它们是临时的(在不同主机之间),它们将获得不同的 IP 地址。服务的 IP 地址是固定的,并将在 Docker Swarm 的内嵌 DNS 中创建 DNS 条目。因此,所有在覆盖网络内的服务都可以通过它们的名称(和别名)被访问和识别。

Kubernetes 中也有类似的方法。在这种情况下,服务只是一组 Pod。Pod 将获得不同的动态 IP 地址,因为弹性将管理它们的生命周期,如果它们死掉了,会创建新的。但是服务在其生命周期内始终有一个固定的 IP 地址。这对于 Docker Swarm 也是如此。因此,我们将发布服务,内部路由和负载均衡将引导流量到 Pod 或任务的容器。

这两个编排器都允许我们绕过这些默认行为,但我们不会深入探讨这些想法,因为我们已经在第八章,使用 Docker Swarm 进行编排,和第九章,使用 Kubernetes 进行编排中进行了覆盖。

现在我们对此有了基本的理解,我们可以介绍入口控制器。这些是一些软件组件,它们将允许我们在集群内发布较少的端口。它们将帮助我们通过默认访问控制来确保安全性,只发布较少的端口和特定的应用程序路由。入口控制器将提供反向代理和负载均衡功能,以帮助我们发布作为容器基础设施内服务运行的应用程序后端。我们将使用内部网络而不是发布应用程序的服务。我们只会发布入口控制器,所有应用程序的流量将从此端点变为内部流量。

入口控制器的概念可以应用于 Kubernetes 和 Docker Swarm。Kubernetes 有专门的资源来实现这一点,但 Docker Swarm 并没有已经准备好的内容。在这种情况下,我们将不得不使用外部应用程序。Docker Enterprise 确实为 Docker Swarm 服务提供了一个即用即走的解决方案。Interlock 集成了描述的入口控制器功能,但应用于 Docker Swarm 的行为。

在下一节中,我们将简要讨论应用程序逻辑以及容器平台上预期的行为。

理解应用程序的逻辑

我们已经回顾了如何为应用程序的组件进行发布,但它们都应该被发布吗?简短的回答可能是否定的。假设我们有一个三层应用程序。我们会有一个中间层作为某种后端,它会消耗一个数据库,并且应该通过前端来访问。在传统的数据中心中,这种分层应用程序可能会将每个服务运行在不同的节点上。这些节点会在不同的子网中运行,以通过防火墙隔离它们之间的访问。这种架构相当常见。后端组件位于中间层,处于数据库和前端之间。前端不应该直接访问数据库。实际上,数据库应该仅能从后端组件访问。那么,我们是否应该发布数据库组件服务?前端组件将访问后端,但我们是否必须发布后端组件?不,但前端应该能够访问后端服务。用户和其他应用程序将使用前端组件来消费我们的应用程序。因此,应该只发布前端组件。这通过使用容器的功能而不是防火墙和子网来保证安全性,但最终结果是一样的。

Docker Swarm 允许我们使用覆盖自定义网络来实现多网络应用程序。这些网络可以使我们将来自不同应用程序的组件互联,且共享某些网络。如果来自不同应用程序的许多服务需要访问一个服务,那么这可能会变得复杂。这种多对一的网络行为可能在你的环境中无法正常工作。为了避免这种复杂性,你有两个选择:

  • 使用扁平网络,可以迁移到 Kubernetes 或定义大型覆盖子网。在这种情况下,第一个选项更好,因为 Kubernetes 提供了网络策略来增强扁平网络的安全性。而 Docker Swarm 中的大型网络则不会为其组件提供任何安全性。你需要使用外部工具来提高安全性。

  • 发布这个常见的服务并允许其他应用程序像访问集群外部一样访问它。我们将使用 DNS 记录来为服务命名,其他应用程序将知道如何访问它。我们将使用负载均衡器和/或 API 管理器来提高可用性和安全性。这些外部组件超出了本书的范围,但它们将提供非容器化应用程序的行为。

现在我们了解了应用程序如何部署和发布,在介绍 Docker Enterprise 的 Interlock 之前,我们将先介绍 ingress 控制器及其组件的概念。

使用 ingress 控制器在 Kubernetes 中发布应用程序

如前所述,ingress 控制器是特殊的 Kubernetes 组件,部署后用于发布应用程序和服务。

入口资源将定义暴露 HTTP 和 HTTPS 部署服务所需的规则和路由。

入口控制器将作为反向代理完成这个过程,增加负载均衡功能。这些功能可以通过外部边缘路由器或集群内部部署的软件代理来配置。任何这些都将使用动态配置来管理流量,动态配置是基于入口资源规则构建的。

我们还可以使用入口控制器为 TCP 和 UDP 原始服务提供发布。这将取决于已部署的哪个入口反向代理。通常,发布一个应用的服务时,使用的是 HTTP 和 HTTPS 以外的协议。在这种情况下,我们可以使用 Docker Swarm 上的 Router Mesh 或 Kubernetes 上的 NodePort/LoadBalancer。

一个入口资源可能看起来像下面的 YAML 文件:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - http:
      paths:
      - path: /testpath
        pathType: Prefix
        backend:
          serviceName: test
          servicePort: 80

入口规则包含一个可选的 host 键,用于将此资源与传入流量的代理主机头关联。所有后续规则将应用于此主机。

它还将包含一组路径列表,每个路径与不同的服务相关联,定义为代理后的后端。所有匹配主机和路径键的请求将被重定向到列出的后端。部署的服务和端口将为每个应用定义后端。

我们将定义一个默认的后端,用于路由任何不匹配入口资源规则的请求。

如前所述,入口控制器将部署不同代理服务上的入口规则。我们将使用现有的外部硬件或软件负载均衡器,或者将这些组件部署在集群内。由于这些组件是可互换的,不同的部署将提供不同的行为,尽管入口资源配置将是相似的。这些部署应该被发布,但后端服务不需要直接外部访问。入口控制器组件将管理访问服务所需的路由和规则。

入口控制器将使用本章描述的任何方法发布,尽管我们通常会使用 NodePort 类型和 LoadBalancer 类型的服务。

我们可以在任何 Kubernetes 集群上部署多个入口控制器。这一点非常重要,因为我们可以通过为每个客户使用特定的入口控制器,改善多租户环境中的隔离性。

我们描述了一种适用于 Kubernetes 的第 7 层路由架构。下图展示了一个入口控制器部署的示例。外部负载均衡器将用户的请求路由到入口控制器。该组件将审查入口资源表并将流量路由到适当的内部服务的 ClusterIP。然后,Kubernetes 将管理内部服务与 Pod 之间的通信,确保用户的请求能够到达服务关联的 Pod:

在接下来的部分,我们将学习 Docker Enterprise 如何为 Docker Swarm 服务部署此发布逻辑。

使用 Interlock 发布在 Docker Swarm 中部署的应用

Interlock 基于之前描述的入口控制器的逻辑。Docker Swarm 架构不同,当我们谈到 Kubernetes 和 Docker Swarm 的网络实现时,它们的差异更加明显。Kubernetes 提供了一个扁平化的网络架构,正如我们所看到的那样。集群中的多个网络将增加额外的安全功能,但也会带来更多的复杂性。

Interlock 替代了以前 Docker Enterprise 的路由网格 L7 路由实现。路由网格在以前的 UCP 版本中可用。Interlock 出现在 Docker Enterprise 的 2.0 版本中。

Interlock 将集成 Docker Swarm 和 Docker Remote API 功能,以使用扩展动态地隔离和配置应用代理(如 NGINX 或 HA-Proxy)。Interlock 将利用 Docker Swarm 的著名对象,如配置和机密,来管理代理所需的配置。我们将能够管理 TLS 隧道,并集成滚动更新(和回滚)以及零停机时间的重新配置。

Interlock 的逻辑分布在三个主要服务中:

  • Interlock 服务 是主进程。它将与 Docker Remote API 交互,以监控 Docker Swarm 事件。此服务将创建代理路由请求到应用程序端点所需的所有配置,包括头部、路由和后端。它还将管理扩展和代理服务。Interlock 服务将通过其 gRPC API 被使用。其他 Interlock 服务和扩展将通过访问 Interlock 的 API 来获取它们准备好的配置。

  • Interlock-extension 服务将查询 Interlock 的 API 以获取上游创建的配置。扩展将使用此预配置来准备与扩展相关联的代理的真实配置。对于部署在集群中的代理服务,如 NGINX 或 HA-Proxy,Interlock-extension 服务将创建其配置,然后通过 API 将这些配置发送给 Interlock 服务。接着,Interlock 服务将在 Docker Swarm 集群中为部署的代理服务创建一个配置对象。

  • Interlock-proxy 是代理服务。它将使用存储在配置对象中的配置来路由和管理 HTTP 和 HTTPS 请求。

Docker Enterprise 将 NGINX 部署为 Interlock-proxy。影响已发布服务的 Docker Swarm 集群变更将动态更新。

Interlock 允许 DevOps 团队实现 蓝绿部署金丝雀部署 服务部署。这些部署方式将帮助 DevOps 在不影响用户访问的情况下进行应用程序升级。

下图展示了一个基本的 Interlock 架构。如前所述,Interlock 看起来像一个入口控制器。以下架构表示常见应用程序的流量。用户请求将由外部负载均衡器转发到 Interlock 代理实例。该组件将检查其规则,并将请求转发到配置的服务 IP 地址。然后,Docker Swarm 将使用内部路由和负载均衡将请求转发到服务的任务:

Interlock 的第 7 层路由支持以下功能:

  • 由于 Interlock 服务作为 Docker Swarm 服务运行,因此提供基于弹性的高可用性。

  • Interlock 与 Docker API 交互,因此提供动态和自动配置。

  • 自动配置:Interlock 使用 Docker API 进行配置。你不需要手动更新或重启任何服务即可使服务可用。UCP 会监控你的服务并自动重新配置代理服务。

  • 我们可以对代理服务进行上下扩展,因为它作为一个独立的组件进行部署。

  • Interlock 提供 TLS 隧道服务,可以用于 TLS 终止或 TCP 透传。证书将通过 Docker Swarm 的机密对象进行存储。

  • Interlock 支持根据上下文或路径进行请求路由。

  • 我们可以同时部署多个扩展和代理配置,以便在多租户或多区域环境中隔离访问。

Interlock-proxy 和 Interlock-extension 服务的实例运行在工作节点上。这将提高安全性,将控制平面与发布服务隔离开来。

我们可以使用主机模式网络来绕过默认的路由网格服务行为,以优化 Interlock-proxy 服务的网络性能。

使用 Interlock 发布服务是基于标签自定义的。我们至少需要以下内容:

  • com.docker.lb.hosts:此标签将管理主机头,因此也会管理服务的发布名称。

  • com.docker.lb.port:内部服务的端口也是必需的,并且通过此标签进行关联。记住,该端口不应被公开。

  • com.docker.lb.network:此标签定义了 Interlock-proxy 服务应连接到哪个网络,以便能够与定义的服务进行通信。

其他标签将允许我们修改已配置的代理行为和功能。以下是一些其他重要标签的列表:

标签 描述
com.docker.lb.ssl_certcom.docker.lb.ssl_key 这些密钥允许我们集成后端的证书和密钥。
com.docker.lb.sticky_session_cookie 我们将设置一个 cookie,以允许粘性会话定义服务实例的后端。
com.docker.lb.backend_mode 这规定了请求如何到达不同的后端(默认为 vip,这是 Docker Swarm 服务的默认模式)。
com.docker.lb.ssl_passthrough 我们可以关闭应用后端的隧道,从而启用 SSL 透传。
com.docker.lb.redirects 这个键允许我们通过主机头定义将请求重定向到不同的 FQDN。

你可以查看 Docker Enterprise 文档中所有可用的标签(docs.docker.com/ee/ucp/interlock/usage/labels-reference)。

如果服务仅隔离在一个网络中,我们不需要添加 com.docker.lb.network,但如果它与 com.docker.lb.ssl_passthrough 配合使用,则需要。如果我们使用堆栈发布服务,我们将使用堆栈的名称。

对于 Interlock 所描述的组件,有许多可用的选项和配置。我们将能够更改代理的默认端口、Docker API 套接字和轮询间隔等。扩展功能将有许多特性和配置,具体取决于外部负载均衡集成。我们建议你查看 Docker Enterprise 文档中所有可用的键和配置(docs.docker.com/ee/ucp/interlock/config)。

我们建议查看这个链接,success.docker.com/article/how-to-troubleshoot-layer-7-loadbalancing,以获取有关排查 Interlock 相关问题的一些有趣的技巧。

在下一章中,我们将介绍 Docker Trusted Registry。这个工具提供了一个安全的镜像存储,集成了镜像签名功能和漏洞扫描功能。这些功能,等等,提供了一个生产就绪的镜像存储解决方案。

回顾 Interlock 的使用

现在,我们将回顾一些 Interlock 使用的示例。

我们需要在 Docker Enterprise 中启用 Interlock。它默认是禁用的,并且是管理员设置部分的一部分。我们可以更改默认端口(HTTP 的 8080 和使用 HTTPS 的安全访问端口 8443),如下所示的截图:

启用后,Interlock 的服务会被创建,我们可以通过使用管理员的 UCP 包并执行 docker service ls 来验证:

$ docker service ls --filter name=ucp-interlock
ID NAME MODE REPLICAS IMAGE PORTS
onf2z2i5ttng ucp-interlock replicated 1/1 docker/ucp-interlock:3.2.5 
nuq8eagch4in ucp-interlock-extension replicated 1/1 docker/ucp-interlock-extension:3.2.5 
x2554tcxb7kw ucp-interlock-proxy replicated 2/2 docker/ucp-interlock-proxy:3.2.5 *:8080->80/tcp, *:8443->443/tcp

重要的是要注意,默认情况下,如果没有足够的节点来运行所需数量的实例,Interlock-proxy 将不会在工作节点上隔离。我们可以通过使用简单的位置约束来改变这一行为(docs.docker.com/ee/ucp/interlock/deploy/production)。

在这个示例中,我们将再次使用 colors 应用程序。我们在第五章,部署多容器应用程序中使用了这个简单的应用程序。这是一个简单的 docker-compose 文件,用于部署 colors 服务。我们将使用一个随机颜色,并将 COLORS 变量留空。我们将创建一个名为 colors-stack.yml 的文件,内容如下:

version: "3.2"

services:
 colors:
 image: codegazers/colors:1.16
 deploy:
 replicas: 3
      labels:
        com.docker.lb.hosts: colors.lab.local
        com.docker.lb.network: colors-network
        com.docker.lb.port: 3000
    networks:
      - colors-network

networks:
  colors-network:
    driver: overlay

我们将使用有效的用户及其捆绑包连接到 Docker Enterprise。对于本实验,我们将使用在安装过程中创建的admin用户。我们将按照第十一章中描述的任何流程下载该用户的ucp捆绑包,Universal Control Plane。下载并解压后,我们只需使用source env.sh加载 UCP 环境:

$ source env.sh 
Cluster "ucp_<UCP_FQDN>:6443_admin" set.
User "ucp_<UCP_FQDN>:6443_admin" set.
Context "ucp_<UCP_FQDN>:6443_admin" modified.

加载完 UCP 环境后,我们将使用本书的 Git 仓库(github.com/frjaraur/dca-book-code.git)。Interlock 的实验位于interlock-lab目录下。我们将使用docker stack deploy -c colors-stack.yml lab部署colors堆栈:

interlock-lab$ docker stack deploy -c colors-stack.yml lab
Creating network lab_colors-network
Creating service lab_colors

我们将使用docker stack ps查看colors实例在集群中的分布情况:

$ docker stack ps lab
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
ksoie4oin10e lab_colors.1 codegazers/colors:1.16 node4 Running Running 8 seconds ago 
b0dykjgp8ack lab_colors.2 codegazers/colors:1.16 node2 Running Preparing 9 seconds ago 
m13tvfbw5cgb lab_colors.3 codegazers/colors:1.16 node3 Running Preparing 9 seconds ago 

我们在 UCP 的管理员设置部分启用了 Interlock。我们使用了默认端口,因此应该可以在8080端口访问我们部署的服务(因为我们在本实验中使用的是 HTTP)。请注意,我们没有在docker-compose文件中使用任何port键,也没有发布任何服务的端口。让我们通过指定所需的主机头colors.lab.local来检查 Interlock 是否正常工作:

$ curl -H "host: colors.lab.local" http://<UCP_NODE>:8080/text
APP_VERSION: 1.15
COLOR: black
CONTAINER_NAME: e69a7ca3b74f
CONTAINER_IP: 10.0.5.15 172.18.0.4
CLIENT_IP: 10.0.0.2
CONTAINER_ARCH: linux
$ curl -H "host: colors.lab.local" http://<UCP_NODE>:8080/text
APP_VERSION: 1.15
COLOR: yellow
CONTAINER_NAME: 69ebb6f349f6
CONTAINER_IP: 10.0.5.14 172.18.0.3
CLIENT_IP: 10.0.0.2
CONTAINER_ARCH: linux

输出可能会有所不同,我们将发起一些请求,确保得到不同的后端(我们部署了三个实例)。如果没有指定任何主机头,将使用默认值。如果没有配置(默认行为),我们将得到一个代理错误。由于我们使用的是 NGINX(默认),我们将得到一个503错误:

$ curl -I http://<UCP_NODE>:8080/text
HTTP/1.1 503 Service Temporarily Unavailable
Server: nginx/1.14.2
Date: Tue, 31 Mar 2020 19:51:05 GMT
Content-Type: text/html
Content-Length: 537
Connection: keep-alive
ETag: "5cad421a-219"

我们可以使用特殊标签com.docker.lb.default_backend: "true"来更改默认的 Interlock 后端,并将其与我们的某个服务关联。当请求的头信息与任何已配置的服务不匹配时,这将作为默认站点。

在继续之前,让我们先移除这个实验。我们将使用docker stack rm。由于堆栈现在需要小心移除,我们可能会遇到一个错误:

$ docker stack rm lab
Removing service lab_colors
Removing network lab_colors-network
Failed to remove network 97bgcu0eo445sz8ke10bacbge: Error response from daemon: Error response from daemon: rpc error: code = FailedPrecondition desc = network 97bgcu0eo445sz8ke10bacbge is in use by service x2554tcxb7kwv0wzsasvfjh6dFailed to remove some resources from stack: lab

这个错误是正常的。Interlock-proxy 组件已连接到我们的应用网络,因此无法移除。Interlock 会每隔几秒刷新一次配置(Docker API 会每 3 秒轮询一次,经过这些间隔后,Interlock 将管理所需的更改)。如果我们等待几秒钟并再次执行移除命令,它将删除堆栈剩余的组件(网络):

$ docker stack rm lab
Removing network lab_colors-network

现在我们将使用com.docker.lb.redirects键测试一个简单的重定向。

简单的应用重定向

在这个例子中,我们将查看如何将请求从一个服务重定向到另一个服务。当我们希望将用户从旧应用程序迁移到新版本时,这可能非常有用。这里我们讨论的不是镜像升级,而是简单地使用docker network create创建一个新的覆盖网络:

$ docker network create -d overlay redirect

我们现在将创建一个简单的 Web 服务器应用程序服务(最小的 NGINX 镜像,nginx:alpine)。请注意,我们将在com.docker.lb.hosts标签中添加主机头。我们还添加了com.docker.lb.redirects,以确保所有发送到http://old.lab.local的请求将被重定向到http://new.lab.local。服务定义如下所示:

$ docker service create --name redirect --network redirect \
--label com.docker.lb.hosts=old.lab.local,new.lab.local \
--label com.docker.lb.port=80 \
--label com.docker.lb.redirects=http://old.lab.local,http://new.lab.local nginx:alpine

如果我们测试对其中一个 UCP 节点的访问,端口为8080,并使用old.lab.local作为主机头,系统会将我们重定向到http://new.lab.local。我们在curl命令中添加了-L选项,以允许所需的重定向:

$ curl -vL http://<UCP_NODE>:8080/ -H Host:old.lab.local
* Trying <UCP_NODE>...
* TCP_NODELAY set
* Connected to <UCP_NODE> (<UCP_NODE>) port 8080 (#0)
> GET / HTTP/1.1
> Host:old.lab.local
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.14.2
< Date: Tue, 31 Mar 2020 22:21:26 GMT
< Content-Type: text/html
< Content-Length: 161
< Connection: keep-alive
< Location: http://new.lab.local/
< x-request-id: d4a9735f8880cfdc99e0478b7ea7d583
< x-proxy-id: 1bfde5e3a23e
< x-server-info: interlock/v3.0.0 (27b903b2) linux/amd64
< 
* Ignoring the response-body
* Connection #0 to host <UCP_NODE> left intact
* Issue another request to this URL: 'http://new.lab.local/'
* Could not resolve host: new.lab.local
* Closing connection 1
curl: (6) Could not resolve host: new.lab.local

请注意,new.lab.local是一个虚拟的 FQDN,因此我们无法解析它,但测试请求已转发到这个新的应用程序站点。

我们现在将部署一个使用 TLS 证书保护的示例服务。Interlock 将管理其证书,并确保访问是安全的。

使用 TLS 通过 Interlock 安全发布服务

在本示例中,我们将部署一个应该使用 TLS 安全发布的服务。我们可以直接为用户创建通道到我们的服务,将 Interlock 配置为透明代理,或者允许 Interlock 管理通道。在这种情况下,服务可以通过 HTTP 部署,但从用户的角度来看,HTTPS 将是必需的。用户将首先与 Interlock-proxy 组件交互,然后才能访问定义的服务后端。

在本示例中,我们将再次使用colors应用程序,配以随机配置。我们将使用colors-stack-https.yml文件,内容如下:

version: "3.2"

services:
  colors:
    image: codegazers/colors:1.16
    deploy:
      replicas: 1
      labels:
        com.docker.lb.hosts: colors.lab.local
        com.docker.lb.network: colors-network
        com.docker.lb.port: 3000
        com.docker.lb.ssl_cert: colors_colors.lab.local.cert
        com.docker.lb.ssl_key: colors_colors.lab.local.key
    networks:
     - colors-network

networks:
  colors-network:
    driver: overlay
secrets:
  colors.lab.local.cert:
    file: ./colors-lab-local.cert
  colors.lab.local.key:
    file: ./colors-lab-local.key

我们将创建一个示例密钥和一个关联的证书,这些将自动集成到 Interlock 的配置中。

始终建议使用 Docker 服务日志查看 Interlock 组件的日志;例如,我们可以使用docker service logs ucp-interlock检测配置错误。

我们将使用openssl创建一个有效期为 365 天的证书:

$ openssl req \
 -new \
 -newkey rsa:4096 \
 -days 365 \
 -nodes \
 -x509 \
 -subj "/C=US/ST=CA/L=SF/O=colors/CN=colors.lab.local" \
 -keyout colors.lab.local.key \
 -out colors.lab.local.cert

一旦这些密钥和证书创建完成,我们将再次使用admin用户连接到 Docker Enterprise。尽管管理员环境可能已经加载(如果你按顺序跟随这些实验),我们仍将使用source env.sh加载ucp环境:

$ source env.sh 
Cluster "ucp_<UCP_FQDN>:6443_admin" set.
User "ucp_<UCP_FQDN>:6443_admin" set.
Context "ucp_<UCP_FQDN>:6443_admin" modified.

一旦 UCP 环境加载完成,我们将使用本书的示例colors-stack-ssl.yaml文件。我们将通过docker stack deploy -c colors-stack-https.yml lab部署带有 HTTPS 的colors堆栈。该目录还包含了准备好的证书和密钥:

interlock-lab$ $ docker stack deploy -c colors-stack-https.yml colors
Creating network colors_colors-network
Creating secret colors_colors.lab.local.cert
Creating secret colors_colors.lab.local.key
Creating service colors_colors

我们将通过docker stack ps查看colors实例在集群中的分布情况:

$ docker stack ps colors
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
xexbvl18d454 colors_colors.1 codegazers/colors:1.16 node4 Running Running 4 minutes ago 

我们在 UCP 的管理员设置部分启用了 Interlock。我们使用了默认端口,因此我们应通过8443端口访问我们部署的服务(因为我们使用的是 HTTPS)。请注意,我们在docker-compose文件中没有使用任何port键。我们没有发布任何服务的端口。

我们可以通过读取相关的 com.docker.interlock.proxy.<ID> 配置对象来查看 Interlock 的代理配置。我们可以使用 docker config inspect 并过滤其输出。首先,我们将获取当前的 ucp-interlock-proxy 配置对象:

$ export CFG=$(docker service inspect --format '{{(index .Spec.TaskTemplate.ContainerSpec.Configs 0).ConfigName}}' ucp-interlock-proxy)

然后,我们将检查这个对象:

$ docker config inspect --pretty ${CFG}

检查 Interlock 代理配置在排查 Interlock 问题时非常有用。尽量一次检查一个服务或堆栈。这样可以避免配置混合,帮助我们追踪错误配置问题。

总结

本章介绍了 Docker Enterprise 的发布功能。我们学习了 Docker Swarm 和 Kubernetes 的不同发布策略,以及如何将这些工具集成到 Docker Enterprise 中。

我们已经看到这些方法如何通过隔离不同层次并允许我们仅发布前端和必要的服务来提高应用程序的安全性。

下一章将教我们 Docker Enterprise 如何实现一个完全安全、适用于生产的镜像存储解决方案。

问题

  1. 使用 Interlock 发布服务需要哪些标签?

a) com.docker.lb.backend_mode

b) com.docker.lb.port

c) com.docker.lb.hosts

d) com.docker.lb.network

  1. 以下哪一项不是 Interlock 过程的一部分?

a) ucp-interlock

b) ucp-interlock-controller

c) ucp-interlock-extension

d) ucp-interlock-proxy

  1. Interlock 进程在 Docker 企业节点中运行的位置是哪里?

a) ucp-interlock 在 Docker Swarm 的领导者节点上运行。

b) ucp-interlock-extension 在任何管理节点上运行。

c) ucp-interlock-proxy 仅在工作节点上运行。

d) 以上答案均不正确。

  1. Interlock 支持哪些功能?

a) SSL/TLS 端点管理

b) 透明代理或 SSL/TLS 透传

c) 使用 Docker API 进行动态配置

d) TCP/UDP 发布

  1. 以下关于在容器编排环境中发布应用程序的哪些陈述是正确的?

a) Ingress 控制器和 Interlock 采用相同的逻辑,通过反向代理服务发布应用程序。

b) Ingress 控制器通过仅暴露必要的服务,帮助我们安全地发布应用程序。

c) Interlock 需要访问应用程序的前端服务网络。

d) 这些前提条件都不正确。

进一步阅读

有关本章所涉及主题的更多信息,请参考以下链接:

使用 DTR 实现企业级注册表

Docker 企业版是一个完整的 容器即服务 (CaaS) 平台。在前面的章节中,我们学习了 通用控制平面 (UCP) 如何为 Docker Swarm 和 Kubernetes 编排器提供完整的控制平面解决方案。我们还学习了 UCP 如何通过 Interlock 提供发布功能。一个面向企业的完整平台还应该涵盖镜像的存储。在本章中,我们将学习 Docker 可信注册表 (DTR),这是 Docker 企业平台的一个组件,旨在管理和确保 Docker 镜像的安全性。

在本章中,我们将了解 DTR 组件以及如何在其组件方面部署和管理一个安全的高可用性注册表。我们还将学习 DTR 如何通过 基于角色的访问控制 (RBAC)、镜像扫描和其他安全特性提供企业解决方案。最后的内容将展示如何将 DTR 自动化和推广功能集成到我们的 CI/CD 工作流中,并提供确保 DTR 健康的策略。通过这系列关于 Docker 企业版的章节,你将对这个平台有深入的了解。

本章将涵盖以下主题:

  • 理解 DTR 组件和特性

  • 部署具有高可用性的 DTR

  • 学习 RBAC(基于角色的访问控制)

  • 镜像扫描和安全特性

  • 集成和自动化镜像工作流

  • 备份策略

  • 更新、健康检查和故障排除

第十七章:技术要求

你可以在 GitHub 仓库中找到本章的代码:github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git

查看以下视频以查看代码示例:

"bit.ly/32tg6sn"

理解 DTR 组件和特性

DTR 是 Docker 企业版的容器平台注册表,用于存储和管理镜像。它部署在定义好的 UCP 工作节点之上。DTR 将作为一个多容器应用程序运行。这意味着所有容器将一起运行,并且仅与一个定义的节点相关联。在节点出现故障时,不会有其他节点接管其 DTR 容器。这一点非常重要,因为我们需要在不同的节点上部署多个 DTR 实例。

DTR 使用 RethinkDB 作为数据库,用于存储和同步注册表节点之间的数据。为了确保 DTR 的高可用性,我们需要部署奇数个副本。我们将使用三个副本,因此需要在三个工作节点上部署 DTR 工作负载。同步将使用覆盖网络进行。DTR 安装将创建一个 dtr-ol 覆盖网络,并将用于副本同步。

每个副本将部署以下进程:

副本(DTR 实例) 进程
dtr-api-<replica_id> 该进程内部暴露 DTR 的 API。
dtr-garant-<replica_id> DTR 的身份验证通过此组件管理。
dtr-jobrunner-<replica_id> jobrunner用于调度 DTR 的不同内部维护任务。
dtr-nginx-<replica_id> nginx进程充当反向代理,将 DTR 的 API 和 Web UI 发布在80端口和443(安全端口)上。
dtr-notary-server-<replica_id>dtr-notary-signer-<replica_id> 这些进程帮助我们签署和维护用户的签名。
dtr-registry-<replica_id> 一个基于社区的注册表将作为 DTR 的核心组件安装。
dtr-rethinkdb-<replica_id> RethinkDB 是用来存储 DTR 仓库信息的数据库。
dtr-scanningstore-<replica_id> 此组件管理并存储扫描数据。

请注意,所有进程都会有一个共同的后缀,以便识别集群中每个副本。我们将部署不同的副本,但它们的数据会同步。

公证服务器进程还会在任何用户通过启用了内容信任的客户端推送或拉取镜像时接收请求。公证签名者将执行服务器端的时间戳和镜像签名快照。

卷将用于持久化 DTR 数据。每个运行 DTR 副本的节点将管理自己的卷。如果 DTR 检测到它们的存在,它们将被使用。这可以防止销毁之前的安装(我们必须使用先前的replica_id标识):

副本(DTR 实例) 进程
dtr-ca-<replica_id> 此卷管理颁发 DTR CA 所需的密钥和根信息。
dtr-notary-<replica_id> 此卷存储公证密钥和证书。
dtr-postgres-<replica_id> 此卷用于镜像的漏洞扫描。
dtr-registry-<replica_id>dtr-nfs-registry-<replica_id> 注册表的数据存储在此卷中。这是默认选项,但我们可以集成第三方存储。实际上,共享存储将是提供 DTR 进程高可用性的必要条件。如果存储后端是 NFS,则将使用dtr-nfs-registry-<replica_id>
dtr-rethink-<replica_id> 此卷存储仓库信息。

DTR 的数据存储非常重要,因为这里是镜像存放的地方。请注意镜像的层,因为 DTR 的备份不会备份它们的数据和元信息。你必须部署自己的备份,以便能够恢复镜像数据。

DTR 可以部署在本地或云中。我们可以使用 Amazon、Google 或 Microsoft Azure。它支持以下存储后端:

  • NFS

  • Amazon S3

  • Cleversafe

  • Google Cloud Storage

  • OpenStack Swift

  • Microsoft Azure

我们可以使用任何与 S3 对象存储兼容的解决方案(例如 Minio)。对象存储非常适合存储镜像的数据,尤其是当我们有包含大量内容的大镜像层时。

DTR 提供了多站点环境下的镜像缓存功能,在这种环境下,用户与注册表之间的通信延迟可能成为问题。镜像缓存将确保用户从最近的注册表节点获取所需的镜像。

与 UCP 一样,DTR 提供了基于角色的访问控制(RBAC)。这两个应用可以集成在一起,实现单点登录解决方案,但 RBAC 是独立的。DTR 将身份验证转发给 UCP,UCP 会验证用户的身份,但每个应用会管理不同的角色和配置文件。通过这种方式,UCP 的高级用户可以对 DTR 中的镜像进行有限的访问。

DTR 的安全性基于镜像安全扫描和 Docker 内容信任。镜像安全扫描将通过二进制文件和库的物料清单BOMs)来查找镜像内容中的漏洞。常见漏洞与暴露CVE)数据库用于查找我们镜像中已知的问题。

BOM 是镜像中所有文件的详细清单。CVE 数据库是一个公开的数据库,列出了世界各地文件中已知的漏洞。它是由社区驱动的,许多贡献者在报告和寻找应用程序代码中的漏洞。

DTR 还包括镜像促销和任务调度。这些功能允许我们监控镜像标签和安全性,以触发与外部工具或 DTR 集成工具的不同修改或交互。

仓库镜像和缓存将帮助我们将 DTR 集成到企业环境中。

我们将在下一节学习如何部署具有高可用性的 DTR。

部署具有高可用性的 DTR

部署具有高可用性的 DTR 需要执行所有 DTR 组件的多个副本。我们将部署奇数个副本以确保高可用性。

DTR 应部署在专用工作节点上。这将确保非系统进程不会影响 DTR 的行为,反之亦然。DTR 在扫描和其他过程中的进程可能会占用大量 CPU。因此,我们将使用三个专用工作节点。我们通常允许工作节点使用 DHCP,但我们将要求 DTR 的工作节点使用固定 IP 地址。我们还需要固定的主机名。

我们可以将 Docker Enterprise 平台部署在本地或云端。第十一章,通用控制平面 中简要描述了 DTR 的要求。

要在专用工作节点上部署 DTR,这些节点至少需要满足以下要求:

  • 16 GB 内存

  • 2 个 vCPU(虚拟 CPU)

对于生产环境,我们将要求使用更大配置的节点,提供更多的资源:

  • 32 GB 内存

  • 4 个 vCPU

由于镜像扫描功能,硬件资源的增加是必需的。这将消耗大量的 CPU 和内存资源,因为它需要加载所有镜像的内容并创建所有二进制文件和库的 md5-checksum-hashes,以便将这些值与 CVE 数据库进行对比。

默认情况下,镜像的数据将下载到 dtr-registry-<REPLICA_ID> 卷中。例如,如果您部署了一个独立的副本进行测试,请确保您有足够的空间来存储镜像。最低要求为 25 GB,但如果您计划管理 Microsoft Windows 镜像,建议至少拥有 500 GB 的空间。 |

在编写本书时,最新的 DTR 版本是 2.7.6。我们将首先安装一个 DTR 副本。安装第一个副本后,我们将加入另外两个副本。我们建议在继续其他副本之前先配置第一个副本。这将确保副本之间配置变化的同步。这对于配置 DTR 的数据存储非常重要。 |

如果我们在 UCP 上配置了许可证,则该许可证将被复制到 DTR。如果没有,我们需要在两个环境中进行配置。 |

正如我们在 Docker 的 UCP 安装中所看到的,installation-container 将有许多相关的操作,如 backups/restoreinstalljoin: |

命令 操作
install DTR 将使用 docker/dtr 镜像进行安装。我们将从任何 UCP 节点启动此过程,因为 UCP URL 将被使用,并且一旦连接建立,过程将从管理节点执行。
join 我们将执行多个 DTR 副本以提供高可用性。在这种情况下,我们将首先安装第一个副本,然后将其他副本加入到该副本中。
reconfigure 我们可以使用 DTR 镜像修改 DTR 配置。有些配置需要重启。我们将配置 DTR 副本以避免停机。
remove 有时我们需要移除多个 DTR 副本。我们将使用 docker/dtr 镜像中的 remove 操作来删除 DTR 环境中的副本。此操作将整洁地移除副本,并更新其他副本关于此变化的信息。
destroy 此命令将用于强制删除所有 DTR 副本的容器和卷。此过程应谨慎使用,因为副本删除是强制的,且不会通知其他副本这一变化,这意味着集群可能会处于不健康状态。使用此选项可以完全从集群中删除 DTR。
backup/restore 此命令将创建一个 TAR 文件,其中包含恢复 DTR 副本所需的所有信息和文件,包括非镜像卷和配置。这不会备份镜像的数据层。镜像的数据必须使用第三方工具存储。请小心,因为您应该能够恢复您的 DTR 集群到运行状态,但可能会丢失所有镜像。
upgrade upgrade 选项将帮助我们自动部署平台升级。所有 DTR 组件将更新到定义的升级版本。如果我们已部署高可用性 DTR,这个过程应该不会影响用户。
images 我们可以在安装之前下载 DTR 所需的镜像。这非常有用,例如,在我们必须执行离线安装时。我们将使用具有互联网访问权限的 Docker Engine 实例来下载 DTR 镜像。
emergency-repair 当所有 DTR 副本都不健康,但有一个副本正在运行并且核心进程健康时,我们将使用 emergency-repair 操作来修复集群。

我们通常会为大多数操作使用以下常见参数:

参数 操作
--ucp-url 这应该是我们有效的 UCP URL。我们将使用集群的 完全限定域名FQDN)和端口(默认是 443)。
--ucp-ca--ucp-insecure-tls 我们将选择其中一个选项,使用 UCP 的有效 CA 或不安全的 TLS,避免任何 CA 认证。
--ucp-username--ucp-password 这些选项将提供 UCP 用户认证。如果没有使用这些选项,执行过程中将会要求输入。它们必须有效且具有管理员权限。

始终使用适当的 docker/dtr:<RELEASE> 版本进行所有操作。除非您正在进行 DTR 升级,否则不要混用不同的版本。本书写作时的当前版本是 2.7.6。

DTR 安装需要 UCP 的 URL 和一个管理员的用户名及密码。我们可以交互式地使用它们,但正如我们在前面的章节中所学的,最好将安装作为脚本结构的一部分。这将帮助我们提供可复现的配置和安装方法。

现在我们将描述 DTR 的安装过程。第一个副本将通过 docker container run docker/dtr:<RELEASE> install 命令安装。我们将在任何集群节点上启动安装过程。实际上,我们可以从笔记本电脑上部署 DTR,因为我们会提供 UCP 的 URL 以及管理员的用户名和密码。安装可以通过交互式或自动化过程完成。我们还将选择哪个 UCP 节点将运行第一个副本的进程,使用 --ucp-node

$ docker run -it --rm \
 docker/dtr:<RELEASE> install \
 --dtr-external-url <DTR_COMPLETE_URL>\
 --ucp-node <UCP_NODE_TO_INSTALL> \
 --ucp-username <UCP_USERNAME> \
 --ucp-password <UCP_PASSWORD> \
 --ucp-url <UCP_COMPLETE_URL> \
 --ucp-ca "$(curl -s -k <UCP_COMPLETE_URL>/ca)"

INFO[0000] Beginning Docker Trusted Registry installation 
INFO[0000] Validating UCP cert 
INFO[0000] Connecting to UCP 
INFO[0000] health checking ucp 
INFO[0000] The UCP cluster contains the following nodes without port conflicts: <LIST_OF_UCP_CLUSTER_NODES>
INFO[0000] Searching containers in UCP for DTR replicas 
...
...
INFO[0000] Creating network: dtr-ol 
INFO[0000] Connecting to network: dtr-ol 
INFO[0000] Waiting for phase2 container to be known to the Docker daemon 
INFO[0001] Setting up replica volumes... 
...
...
INFO[0011] License config copied from UCP. 
INFO[0011] Migrating db... 
...
...
INFO[0004] Migrated database from version 0 to 10 
INFO[0016] Starting all containers... 
...
...
INFO[0114] Successfully registered dtr with UCP 
INFO[0114] Installation is complete 
INFO[0114] Replica ID is set to: c8a9ec361fde 
INFO[0114] You can use flag '--existing-replica-id c8a9ec361fde' when joining other replicas to your Docker Trusted Registry Cluster 

由于 DTR 的安装过程将连接到 UCP 的 API,TLS 将被使用,并且证书将被发送。我们已将 UCP 的 CA 添加到验证其证书。

一旦第一个副本安装完成,我们将进行配置,然后加入其他副本。如果在安装过程中没有更改共享存储和其他设置,配置这些内容非常重要。

注意安装输出的最后一行。它显示了 You can use flag '--existing-replica-id c8a9ec361fde' when joining other replicas to your Docker Trusted Registry Cluster 这段文字。请记住这个副本的 ID;我们将在重新配置它并加入其他副本时使用它。

我们可以配置需要执行 reconfigure 操作的共享存储。我们可以使用文件系统或对象存储类型:

  • 文件系统存储类型网络文件系统NFS)、绑定挂载和卷

  • 对象存储(云)类型:Amazon S3、Openstack 的 Swift、Microsoft Azure 和 Google Cloud Storage

对象存储和 NFS 都是有效的共享存储选项。每个云服务提供商会要求不同的规格。常见的参数包括用户名或账户名、密码和桶名。对象存储是 DTR 共享镜像存储的首选选项。有一些本地解决方案,如 Minio,容易在我们的数据中心实现。NFS 也是有效的选项,并且在当前的数据中心中非常常见。在这种情况下,我们将使用 --nfs-storage-url 参数与 reconfigure 操作。nfs-storage-url 将要求以下格式:nfs://<ip|hostname>/<mountpoint>

DTR 的存储后端配置也可以使用 YAML 格式进行管理。

许多 DTR 选项可以通过环境变量进行设置。要查看可用的变量,执行 docker container run docker/dtr:<RELEASE> <ACTION> --help 来获取操作的帮助。变量将在每个参数或选项上显示。

加入副本将为 DTR 的进程提供高可用性。复制需要外部存储以共享镜像的 blob(数据层)和元信息。因此,如果在安装时没有选择共享存储,我们将重新配置第一个副本的存储。我们已经有了第一个副本的 ID,将使用 docker/dtr:<RELEASE> reconfigure --existing-replica-id <FIRST_REPLICA'S_ID> 来重新配置存储后端。在此示例中,我们将仅使用 NFS,这是我们数据中心常见的配置。

在执行存储配置之前,我们将把注册表卷的数据复制到我们的 NFS 文件系统中。

以下几行展示了将 NFS 端点作为本地目录挂载到 DTR 主机上的快速示例(我们使用了一个示例 IP 地址和先前创建的副本 ID):

$ sudo mount -t nfs 10.10.10.11:/data /mnt
$ sudo cp -pR /var/lib/docker/volumes/dtr-registry-c8a9ec361fde/_data/* /mnt/

这一步将确保如果我们在使用 --storage-migratedreconfigure 操作时,之前的数据不会丢失。如果您将 NFS 用作本地卷,应该确保它在重启时能够通过在 fstab 文件中添加适当的行进行挂载。这只是一个示例。我们不会在 DTR 中使用本地挂载的 NFS;我们可以直接使用 NFS,通过适当的命令行选项,将 NFS 端点挂载为 DTR 卷。

以下截图展示了 Amazon S3 选项集成在 DTR 的 Web UI 中。每种后端类型将集成不同的选项:

我们已使用变量作为命令参数,但我们保留了命令的输出,因为它展示了 NFS 和当前副本的 ID 信息:

$ docker container run --rm -it docker/dtr:<RELEASE> reconfigure \
--existing-replica-id <FIRST_REPLICA'S_ID> \
--nfs-storage-url nfs://<NFS_SERVER>/<NFS_SHARED_DIR> \
--storage-migrated \
--ucp-username <UCP_USERNAME> \
--ucp-password <UCP_PASSWORD> \
--ucp-url <UCP_COMPLETE_URL> \
--ucp-insecure-tls
INFO[0000] Starting phase1 reconfigure
INFO[0000] Validating UCP cert
INFO[0000] Connecting to UCP
INFO[0000] health checking ucp
INFO[0000] Searching containers in UCP for DTR replicas
INFO[0000] Cluster reconfiguration will occur on all DTR replicas
...
...
INFO[0000] Connecting to network: dtr-ol
INFO[0000] Waiting for phase2 container to be known to the Docker daemon
INFO[0000] Establishing connection with Rethinkdb
...
...
INFO[0003] Getting container configuration and starting containers...
INFO[0003] Waiting for database to stabilize for up to 600 seconds before attempting to reconfigure replica c8a9ec361fde
INFO[0003] Establishing connection with Rethinkdb
INFO[0003] Configuring NFS
...
... INFO[0004] Recreating volume node4/dtr-registry-nfs-c8a9ec361fde
...
... INFO[0009] Recreating dtr-registry-c8a9ec361fde...
INFO[0013] Recreating dtr-garant-c8a9ec361fde...
INFO[0017] Changing dtr-api-c8a9ec361fde mounts from [dtr-ca-c8a9ec361fde:/ca dtr-registry-c8a9ec361fde:/storage] to [dtr-ca-c8a9ec361fde:/ca dtr-registry-nfs-c8a9ec361fde:/storage]
...
... INFO[0038] Recreating dtr-scanningstore-c8a9ec361fde...
INFO[0042] Trying to get the kv store connection back after reconfigure
INFO[0042] Establishing connection with Rethinkdb
INFO[0042] Verifying auth settings...
INFO[0042] Successfully registered dtr with UCP
INFO[0042] The `--storage-migrated` flag is set. Not erasing tags.

注意 --storage-migrated 参数。如果我们在创建多个仓库后迁移存储,如果没有迁移注册表卷的数据,所有的工作将会丢失。在这种情况下,我们只复制了卷的内容。

现在我们有了共享注册表的存储后端,我们可以加入新的副本。我们将使用当前副本的 ID,因为新的副本需要一个基础副本来同步。我们将在任何集群节点上使用 join 操作,因为我们将为这个副本选择另一个工作节点(我们已经用 <NEW_UCP_NODE> 模拟了我们的例子):

$ docker container run --rm -it docker/dtr:<RELEASE> \
 join \
 --ucp-node <NEW_UCP_NODE> \
 --ucp-username <UCP_USERNAME> \
 --ucp-password <UCP_PASSWORD> \
 --ucp-url <UCP_COMPLETE_URL> \
 --ucp-insecure-tls \
 --existing-replica-id c8a9ec361fde
 INFO[0000] Beginning Docker Trusted Registry replica join
 INFO[0000] Validating UCP cert
 INFO[0000] Connecting to UCP
 INFO[0000] health checking ucp
 INFO[0000] The UCP cluster contains the following nodes without port conflicts: <UCP_NODES_AVAILABLE>
 INFO[0000] Searching containers in UCP for DTR replicas
 INFO[0001] Searching containers in UCP for DTR replicas
 INFO[0001] verifying [80 443] ports on node3
 INFO[0012] Waiting for running dtr-phase2 container to finish
 INFO[0012] starting phase 2
 INFO[0000] Validating UCP cert
 ...
 ...
 INFO[0057] Recreating dtr-scanningstore-c8a9ec361fde...
 INFO[0061] Configuring NFS
 INFO[0062] Using NFS storage: nfs://10.10.10.11/data
 INFO[0062] Using NFS options:
 ...
 ...
INFO[0176] Transferring data to new replica: cc0509711d05
 INFO[0000] Establishing connection with Rethinkdb
 ...
 ... INFO[0183] Database successfully copied
 INFO[0183] Join is complete
 INFO[0183] Replica ID is set to: cc0509711d05
 INFO[0183] There are currently 2 replicas in your Docker Trusted Registry cluster
 INFO[0183] You currently have an even number of replicas which can impact cluster availability
 INFO[0183] It is recommended that you have 3, 5 or 7 replicas in your cluster

除了第一个副本的 ID 外,所有其他值都被模拟了,join 命令的输出也已简化。请注意,我们使用了 --ucp-insecure-tls 而不是添加 UCP 的 CA。在执行了 183 步之后,新的副本已成功加入。至少需要三个副本才能保证高可用性。所有副本都作为多容器应用部署在定义好的工作节点上。

从 DTR 2.6 版本开始,在切换存储驱动程序之前,您应该先执行备份。这确保了如果您决定切换回当前的存储驱动程序,您的镜像将被保留。

DTR 将安全地公开其 API,使用 TLS 协议。因此,证书将用于创建安全的隧道。默认情况下,DTR 会创建一个 CA 来签署服务器证书。我们可以使用公司私有或公有证书。它们可以在安装过程中通过 --dtr-ca--dtr-cert 参数进行应用,但我们也可以稍后在 DTR 的 Web UI 或通过使用 reconfigure 操作进行更改。如果您使用了自定义证书,您的证书可能会包含在您的系统中。如果 Docker 为我们创建了自动签名的证书,这些证书在您的系统中将不被信任。Docker 创建了一个 CA 来签署 DTR 证书,当您尝试从命令行执行任何注册表操作时,您可能会看到以下错误信息:

Error response from daemon: Get https://<DTR_FQDN>[:DTR_PORT]/v2/: x509: certificate signed by unknown authority.

为了避免这个问题,我们可以选择不验证 SSL,定义不安全的注册表,或者将 DTR 的 CA 添加为受信任的证书:

  • 不安全的注册表:要为我们的客户端设置一个不安全的注册表,我们将在 Docker 引擎的 daemon.json 文件中添加 "insecure-registries" : ["<DTR_FQDN>[:DTR_PORT]"]。这不推荐在生产环境中使用,因为有人可能会劫持我们的服务器身份。

  • 将 DTR 的 CA 添加到我们的系统中:这个过程可能会根据 Docker 引擎主机的操作系统有所不同。我们将描述 Ubuntu/Debian 和 Red Hat/CentOS 节点的操作流程。它们在我们的数据中心中非常常见:

CA updating procedure on Ubuntu/Debian nodes: $ openssl s_client -connect <DTR_FQDN>:<DTR_PORT> -showcerts </dev/null 2>/dev/null | openssl x509 -outform PEM | sudo tee /usr/local/share/ca-certificates/<DTR_FQDN>.crt
$ sudo update-ca-certificates
$ sudo systemctl restart docker

CA updating procedure on Red Hat/CenOS nodes: $ openssl s_client -connect <DTR_FQDN>:<DTR_PORT> -showcerts </dev/null 2>/dev/null | openssl x509 -outform PEM | sudo tee /etc/pki/ca-trust/source/anchors/<DTR_FQDN>.crt
$ sudo update-ca-trust
$ sudo systemctl restart docker

将 DTR 的 CA 添加到我们的客户端系统中是首选方法,因为我们仍然会验证其证书。

我们可以使用定义好的 DTR URL 登录到 DTR 的 Web UI。由于默认情况下登录集成了 UCP,重定向将会整合到这个过程中,并且 UCP 将授权用户。

以下截图显示了我们登录后 DTR 的主界面。仓库将以树形结构展示,用户只能访问自己的资源:

DTR 的 Web UI 非常简单。它允许管理员管理用户、团队、组织和 RBAC 集成。以下是系统端点的截图:

系统的端点提供对以下资源和配置的访问:

  • 常规标签

    • 允许我们管理 DTR 的许可证。

    • DTR 的负载均衡 URL。

    • 集成企业代理以下载所需的镜像扫描 CVE 数据库。

    • UCP 和 DTR 之间的单点登录集成。

    • 配置客户端的浏览器 cookies。这将帮助我们将请求转发到特定的 DTR 后端。

    • 允许我们设置是否可以在推送时创建仓库。这使得用户可以推送镜像,如果仓库不存在,将自动创建该仓库。

  • 存储标签:此标签允许我们配置所有 DTR 的存储后端。我们可以在文件系统存储和对象存储(云)之间进行选择,每个后端会有不同的选项。

  • 安全标签:安全对于镜像至关重要。此标签允许我们配置 DTR 的镜像扫描功能。

  • 垃圾回收标签:未标记的镜像占用空间,如果某些镜像使用了非引用的层,还会增加风险。此标签允许我们安排自动删除未标记镜像。

  • 作业日志标签:可以在此标签中查看内部任务的日志。该日志将显示与镜像同步和镜像修剪等内部功能的信息。

接下来的部分将展示如何管理不同的访问权限,以使用存储在 DTR 仓库中的镜像。

了解 RBAC

DTR 提供了一个完整的 RBAC 环境。DTR 将对有效用户进行身份验证和授权。我们可以集成第三方身份验证解决方案,如在第十一章 通用控制平面 中所学的那样。集成外部的 轻量级目录访问协议LDAP)/活动目录AD)身份验证机制将允许我们将用户密码委托给他们,而 UCP 和 DTR 将管理用户授权。

默认情况下,DTR 会将用户身份验证重定向到 UCP,因为包括了单点登录。我们可以在系统 | 常规菜单中更改此行为。建议保持此设置,以便仅在一个应用程序中管理用户。所有身份验证将委托给 UCP,并且此设置将用户路由到其集成的第三方身份验证机制(如果已配置)。

一旦我们通过身份验证进入 DTR 环境,我们将获得不同的权限,允许我们管理来自仓库的镜像或仅从中拉取不同的版本。

默认情况下,匿名用户将能够从公共仓库中拉取镜像。您必须确保只有允许的镜像存储在公共仓库中。

我们可以在 UCP 或 DTR 上创建用户,因为默认情况下,我们将拥有一个单点登录环境,用户将在这两个应用程序之间共享。

用户在团队和组织中进行管理,正如我们在第十一章《通用控制平面》中所学的那样,通用控制平面。这允许我们将团队整合到组织中,同时用户会被分配到这些团队中:

  • 组织将提供一个逻辑层次的抽象和隔离。它们允许我们为其他资源命名空间。

  • 团队将允许我们为仓库分配用户访问权限。

用户将被整合到组织和团队中。这使我们能够限制组织内镜像的访问权限,并使用团队赋予的权限和允许的操作。

仓库的访问权限由两个概念管理:

  • 所有权:仓库创建者

  • 公共可访问性:公共或私人仓库

仓库的所有者可以决定其他用户的访问权限。如前所述,我们可以有公共和私有镜像。

私人仓库只能由所有者和 DTR 管理员使用。其他用户无法从这些仓库拉取镜像。只有仓库的所有者才能将镜像推送到这些仓库。

在组织内部,我们将为特定团队提供对组织私人仓库的读写权限。这些团队将能够将镜像推送到这些仓库。这些团队是这些仓库的所有者,我们可以为某些团队提供只读权限。它们只能拉取镜像。其他所有团队将无法访问,因为我们讨论的是组织的私人仓库。

公共仓库是不同的。用户的公共仓库允许其他用户从中拉取镜像,而只有仓库所有者才能推送镜像。它们具有读写权限。一个组织的公共仓库将允许用户同样拉取镜像。在这些情况下,只有具有读写权限的团队才能推送镜像。

下表表示可以应用于仓库的权限:

权限 描述
只读权限 用户可以浏览/搜索并从仓库中拉取镜像。用户无法向该仓库推送镜像。
读写权限 用户可以浏览/搜索、拉取和推送镜像到仓库。
所有者 所有者对其仓库具有读写权限,但他们也可以管理其权限和描述。还可以设置仓库的隐私级别(公共/私有)。

组织的成员对该组织的公共镜像具有只读访问权限。因此,组织的用户始终可以拉取其公共镜像。组织的成员可以看到其他成员并查看组织内所有团队。但是,我们需要将用户整合到组织的团队中,以便提供管理和读写权限。

组织的成员如果未加入任何团队,则无法管理组织的仓库。他们只能拉取其公共镜像。

另一方面,组织的所有者将能够管理该组织及其所有仓库。我们可以将任何用户包括在组织内作为所有者。这些用户还可以管理组织内的团队及其访问级别。

我们将使用一个简单的示例帮助你理解如何在不同仓库中为用户分配权限和访问。

假设有一个名为 myorganization 的组织。让我们为 devops 团队和其他 开发人员运维 团队创建一个示例。在这个示例中,devops 团队将定义核心图像,而 开发人员 将使用这些图像来构建他们的应用程序。

devops 组成员将拥有读写权限,而 开发人员 将拥有只读权限。他们只会拉取图像来创建自己的图像。他们将使用 devops 团队创建的企业定义的核心图像。在这种情况下,运维 团队无法访问这些应用程序核心图像。

另一方面,devops 团队创建了一系列用于测试平台的图像,这些图像存放在 测试图像 仓库中。这个仓库是公开的,组织内的所有用户都可以在 Docker Enterprise 平台上使用它。下图展示了描述的 RBAC 情况:

在下一部分,我们将回顾 DTR 平台中包含的图像扫描和其他安全功能。

图像扫描和安全功能

在这一部分,我们将回顾 DTR 的安全功能,如安全扫描和图像签名。

安全扫描

DTR 包含内置的图像安全扫描功能。它将扫描每个图像层中的二进制文件和库。扫描报告将包含每一层的汇总 BOM。我们现在可以完整查看图像的文件及其 MD5 哈希值。这确保了每一层内容在图像发布之间的不可变性。如果我们更改了某一层中的文件,其哈希值会发生变化,扫描将针对新层的内容执行。图像扫描还将下载并管理 Docker 提供的 CVE 数据库。这将用于将图像层报告与给定的漏洞信息进行关联。

扫描将向我们展示一份报告,关于我们图像的健康状况,报告图像层中检测到的所有已知漏洞。

这个 CVE 数据库应该频繁更新,因为新的威胁几乎每天都会出现。我们可以使用在线同步或离线手动更新。无论哪种方式,我们都需要有效的 Docker Enterprise 许可证。在线同步要求有效的互联网连接(我们可以在 DTR 内使用公司代理,通过在 DTR 安装时或安装后重新配置环境来配置 --http-proxy 和/或 --https-proxy 选项)。

不要忘记使用 --no-proxy 选项来配置你们企业内部的所有 FQDN。

镜像扫描消耗大量 DTR 主机的资源。实际上,每层的第一次安全扫描需要大量资源。随后的扫描将使用之前层的报告。如果镜像的层大小较大,扫描将消耗大量资源来生成该层的报告。报告中应包括所有文件的哈希值,以便与数据库中的数据进行关联。如果我们在镜像中使用公共层,这个过程只会执行一次。如果我们更改该层的内容,该层的报告将被更新,该层会变得过时,并且将执行新的扫描。在镜像更改时需要注意这些过程。

扫描可以在 DTR 注册表中每次更新或创建镜像时自动执行。这个功能将通过在每个仓库中启用“推送时扫描”来设置。我们也可以定期手动执行镜像扫描,但如果不使用 DTR 的 API,这将很难维护。

镜像的扫描报告将显示在每个仓库的标签上。我们将获得镜像漏洞的健康报告,如下图所示:

镜像的漏洞状态可以如下:

  • 绿色:未发现漏洞,镜像是安全的。

  • 橙色:发现了一些轻微或严重的漏洞。

  • 红色:发现了严重漏洞,可能会危及安全。

我们可以通过点击每个标签的详细信息来查看其报告。我们将能够查看完整的扫描结果,包括镜像的元数据、大小、所有者和最新扫描。

我们有两种不同的视图来查看标签的扫描详情:

  • 视图将按镜像构建的顺序显示镜像的层列表。我们将看到每一层及其上识别出的漏洞。我们可以点击每一层,深入查看其组件。

  • 组件视图将列出所有镜像的组件。组件将根据发现的漏洞数量进行排序,因为一个文件可能有多个问题。

我们可以集成触发器,在扫描结果完成后通知其他进程或应用程序。

镜像不可变性

另一个有趣的功能可以为每个镜像的仓库启用。镜像的不可变性意味着将避免覆盖标签。这将确保标签的唯一性。在生产发布方面,这非常有趣。没有人会重新使用已经使用过的标签,因此开发生命周期不会受到影响,因为每个发布都会有一个唯一的 ID。

DTR 中的内容信任

DTR 与 Docker 内容信任 (DCT) 集成。我们在第六章中讨论过这个话题,Docker 内容信任简介。我们了解到,镜像签名能够提升集群和应用的安全性,确保镜像的所有权、不变性和来源。如果我们有一个生成镜像作为应用程序工件的 CI/CD 管道,我们可以确保在生产环境中运行的是正确的镜像。UCP 允许我们只在组织内运行已签名的镜像。

DTR 提供了一个公证服务器和一个公证签名者。这些组件是 DCT 所必需的。这两个应用程序组件将通过内部代理访问,并与 UCP 的角色和访问环境集成。这种集成使得我们能够签名 UCP 可以信任并安全执行的镜像。

Docker 客户端将允许我们为仓库配置内容信任并签名镜像。我们将使用一个简单的 Docker 客户端命令行来签名镜像。企业环境中的主要区别在于,我们需要确保镜像是由企业用户签名的。我们将使用我们自己的证书,这些证书包含在我们的用户捆绑包中。我们将使用key.pemcert.pem作为私钥和公钥。

现在,我们将描述在 Docker 企业环境中签名镜像所需的步骤:

  1. 首先,我们将下载用户的捆绑包。我们已经在第十一章中描述了这个过程,通用控制平面。一旦我们将捆绑包下载到系统中(已经解压并准备使用),我们将把私钥添加到笔记本电脑或 Docker 客户端节点的信任存储中。我们将使用docker trust load
$ docker trust key load --name <MY_USERNAME> key.pem
Loading key from "key.pem"...
Enter passphrase for new <MY_USERNAME> key with ID ....:
Repeat passphrase for new <MY_USERNAME> key with ID ....:
Successfully imported key from key.pem
  1. 接下来,我们将为特定的仓库初始化信任元数据。我们应该将自己添加为每个我们将推送镜像的仓库的签名者。记住,仓库应包含注册表的 FQDN 和端口。我们将使用docker trust signer add命令:
$ docker trust signer add \
--key cert.pem \
<MY_USERNAME> <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY]
Adding signer "<MY_USERNAME>" to <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY]...
Initializing signed repository for <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY]...
Enter passphrase for root key with ID ....:
Enter passphrase for new repository key with ID ....:
Repeat passphrase for new repository key with ID .....:
Successfully initialized "<DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY]"
Successfully added signer: <MY_USERNAME> to <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY]
  1. 通过这几个步骤,我们准备好签名镜像了。让我们回顾一个简单的示例,使用alpine镜像。我们将为我们的镜像打上准备推送到注册表的标签,并使用docker trust sign进行签名:
$ docker tag alpine <DTR_FQDN>[:DTR_PORT]/myorganization/alpine:signed-test
$ docker trust sign <DTR_FQDN>[:DTR_PORT]/myorganization/alpine:signed-test
Signing and pushing trust data for local image <DTR_FQDN>[:DTR_PORT]/myorganization/alpine:signed-test, may overwrite remote trust data
The push refers to repository [<DTR_FQDN>[:DTR_PORT]/myorganization/alpine]
beee9f30bc1f: Layer already exists 
signed-test: digest: sha256:cb8a924afdf0229ef7515d9e5b3024e23b3eb03ddbba287f4a19c6ac90b8d221 size: 528
Signing and pushing trust metadata
Enter passphrase for <MY_USERNAME> key with ID c7690cd: 
Successfully signed <DTR_FQDN>[:DTR_PORT]/myorganization/alpine:signed-test
  1. 一旦签名完成,我们可以将镜像推送到注册表中。请注意,我们使用<DTR_FQDN>[:DTR_PORT]作为 DTR 的注册表地址:
$ docker push <DTR_FQDN>[:DTR_PORT]/myorganization/alpine:signed-test
The push refers to repository [192.168.56.14/myorganization/alpine-base]
beee9f30bc1f: Layer already exists 
signed-test: digest: sha256:cb8a924afdf0229ef7515d9e5b3024e23b3eb03ddbba287f4a19c6ac90b8d221 size: 528

我们现在在注册表中有了签名的镜像,正如我们在下面的截图中所看到的:

  1. 我们可以使用docker trust inspect来查看镜像的所有权及其签名:
$ docker trust inspect --pretty <DTR_FQDN>[:DTR_PORT]/myorganization/alpine:signed-test

Signatures for <DTR_FQDN>[:DTR_PORT]/myorganization/alpine:signed-test

SIGNED TAG DIGEST SIGNERS 
signed-test cb8a924afdf0229ef7515d9e5b3024e23b3eb03ddbba287f4a19c6ac90b8d221 <MY_USERNAME>

List of signers and their keys for <DTR_FQDN>[:DTR_PORT]/myorganization/alpine:signed-test

SIGNER KEYS
<MY_USERNAME> c7690cd8374b

Administrative keys for <DTR_FQDN>[:DTR_PORT]/myorganization/alpine:signed-test

Repository Key: 63116fb0f440e1d862e0d2cae8552ab2bcc5a332c26b553d9bfa0a856f15fe91
 Root Key: 69129c50992ecd90cd5be11e3a379f63071c1ffab20d99c45e1c1fa92bfee6ce

我们已经模拟了本章中看到的此输出和其他输出,但你将看到类似的输出。用户应该出现在SIGNER KEYS部分(我们在前面的命令输出中有<MY_USERNAME>)。

  1. 还有一个与签名相关的重要话题。用户可以委托其他人进行镜像签名。这个概念允许其他用户为我们签名,或者在团队内共享签名。如果我们需要模拟其他用户的签名过程,我们需要导入他们的密钥。因此,我们需要另一个用户的 key.pem 密钥文件。我们将按照之前的步骤加载该密钥:
$ docker trust key load --name <MY_TEAMMATE_USERNAME> key.pem
Loading key from "key.pem"...
Enter passphrase for new <MY_TEAMMATE_USERNAME> key with ID ......:
Repeat passphrase for new <MY_TEAMMATE_USERNAME> key with ID .....:
Successfully imported key from key.pem

我们模拟了用户的姓名和 ID。

  1. 然后,我们将团队成员的公钥添加到我们的存储库中:
$ docker trust signer add --key cert.pem <MY_TEAMMATE_USERNAME> <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY]
Adding signer "<MY_TEAMMATE_USERNAME>" to  <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY]...
Enter passphrase for repository key with ID ......:
Successfully added signer: <MY_TEAMMATE_USERNAME> to <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY]
  1. 现在,我们可以使用两个签名进行签名:
$ docker trust sign <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY][:TAG]
Signing and pushing trust metadata for <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY][:TAG]
Existing signatures for tag 1 digest 5b49c8e2c890fbb0a35f6....................
from:
<MY_TEAMMATE_USERNAME>
Enter passphrase for <MY_TEAMMATE_USERNAME> key with ID ...:
Enter passphrase for <MY_USERNAME> key with ID ...:
Successfully signed <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY][:TAG]
  1. 现在,我们可以进行进一步检查,并查看到两个签名:
$ docker trust inspect --pretty <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY][:TAG]
Signatures for <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY][:TAG]
SIGNED TAG DIGEST SIGNERS
1 5b49c8e2c890fbb0a35f6050ed3c5109c5bb47b9e774264f4f3aa85bb69e2033 <MY_TEAMMATE_USERNAME>, <MY_USERNAME>
List of signers and their keys for <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY][:TAG]
SIGNER KEYS
<MY_USERNAME> 927f30366699
<MY_TEAMMATE_USERNAME> 5ac7d9af7222
Administrative keys for <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY][:TAG]
Repository Key: e0d15a24b741ab049470298734397afbea539400510cb30d3b996540b4a2506b
 Root Key: b74854cb27cc25220ede4b08028967d1c6e297a759a6939dfef1ea72fbdd7b9a

要删除存储库的 DCT,我们将使用 notary delete <DTR_FQDN>[:DTR_PORT][/ORGANIZATION][/USERNAME][/REPOSITORY] --remote。你将需要在主机中安装 notary 应用的二进制文件。

记住,所有客户端操作都可以强制执行安全操作,通过 export DOCKER_CONTENT_TRUST=1 来启用内容信任,从而对当前 shell 中执行的所有命令生效。

内容信任可以与 CI/CD 流程编排器和其他自动化工具集成。为了避免用户在镜像签名过程中进行交互,我们可以使用以下变量:

  • DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE:将用于本地根密钥的密码短语

  • DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE:将用于存储库密码短语

正如我们所学到的,用户可以通过 UCP 使用 Docker 捆绑包来签名他们的图像。也可以使用 docker trust key generate command 命令生成密钥,但这些密钥不会被包括在 DTR 中。

DTR 内置了 Notary,因此你可以使用 DCT 来签名和验证镜像。有关在 DTR 中管理 Notary 数据的更多信息,请参考 DTR 专用的 Notary 文档。

接下来的部分将展示我们如何使用 DTR 内置功能将 Docker Enterprise 集成到我们的 CI/CD 流水线中。

集成和自动化镜像工作流

DTR 提供了与 CI/CD 流水线构建逻辑相一致的内置功能。我们将有可以触发的 Webhook,用于通知其他应用或进程某些事件,如完成图像扫描或新图像/标签到达。我们还有图像推广功能,该功能将在存储库之间重新标记图像。以下图示展示了构建、分发和执行应用程序的简单工作流。我们列出了 DTR 提供的一些功能:

该工作流展示了如何在多个开发阶段中实现 DTR。在本示例中,通过推广扫描图像进行测试,将确保其在投入生产前的安全性。在本节的末尾,我们还将回顾镜像镜像功能。这是一个用于在不同 DTR 环境之间共享图像的功能。

镜像推广

DTR 允许我们在仓库之间自动推广镜像。推广是基于仓库定义的策略进行的。因此,策略在仓库级别定义。当镜像被推送到该仓库时,策略会被审核,如果规则匹配,则会将镜像推送到另一个注册表。

镜像推广在 CI/CD 流水线阶段非常有用。通过快速示例可以更容易理解。假设有一个用于前端应用程序组件的开发仓库。开发人员将把镜像推送到development/frontend仓库,他们在这个仓库中管理所有更新。事实上,除了他们自己,没有人可以访问这个仓库。他们会在此开发新的更新,包括修复和新功能。当需要将版本发布到生产环境时,他们会准备一个release版本,并在该镜像的标签中加入release字符串。一个策略将匹配此字符串,然后会在质量保证仓库为该应用组件创建一个新的镜像。

release镜像被推送时,该过程会创建一个新的镜像供质量保证团队进行测试。这些用户无法访问非发布镜像。只有标记为release的镜像才能供质量保证用户使用。我们知道,只有镜像的 ID 是唯一的。每个镜像可以有多个标签。因此,我们并没有重复镜像,只是为定义的镜像添加了新的标签和名称。

我们可以根据以下属性定义策略:

属性 描述
标签名称 我们为仓库的镜像标签定义匹配字符串。匹配标签可以相等,或者可以是以某个字符串开头、结尾、包含某个字符串,或者是镜像定义的标签之一。
组件名称 如果镜像具有给定组件,并且其名称等于、以某个字符串开头、结尾、包含某个字符串,或者是指定的名称之一,则此规则将匹配。
漏洞 我们可以定义将监控多少个关键、重大或轻微漏洞(或所有漏洞),以便将镜像提升到另一个仓库。当满足定义的值和镜像之间的方程时,使用如“更大于”,“大于或等于”,“小于或等于”,“等于”或“非”等比较表达式,只有当方程式成立时,镜像才会被提升。
许可证 如果镜像使用许可证,则此规则将匹配。通常,这与 Microsoft Windows 镜像相关。

我们可以将多个属性应用于此策略的规则。一旦选择了将应用的标准,我们可以设置新的仓库和标签。新的镜像标签有许多命名模板。这些模板允许我们包含镜像的源标签或时间戳。

DTR Webhook

DTR 提供了一系列集成的 Webhooks,这些 Webhooks 会在特定情况下触发。当某些事件发生时,DTR 将能够向第三方应用程序发送 Webhook。这对将 DTR 集成到您的 CI/CD 流水线中至关重要。如果接收方后端也具有此功能,DTR 的 Webhooks 可以通过 TLS 安全传输。

我们将覆盖大部分重要的 Webhooks,但此链接提供了当前 Webhooks 的准确列表:docs.docker.com/ee/dtr/admin/manage-webhooks/

Webhooks 描述
TAG_PUSHTAG_PULLTAG_DELETE 仓库的标签事件将在有人推送或拉取仓库,或仓库被删除时触发 Webhook。
SCAN_COMPLETEDSCANNER_UPDATE_COMPLETED 扫描是确保安全性的关键。我们将在镜像扫描数据库更新或仓库扫描正确结束时发送通知。
PROMOTION 每当应用促销策略时,我们将发送一个 Webhook。这有助于我们跟踪 DTR 镜像的内部工作流程。

我们必须拥有仓库的管理员权限,才能通过 DTR 的 Web UI 或 API 配置其 Webhooks。Web UI 允许我们通过点击“测试”来测试已定义的 Webhooks。

以下截图显示了仓库的 Webhook 配置:

接下来的部分将向我们展示如何实现注册表镜像。

镜像在注册表之间的同步

注册表镜像也可以帮助我们在 CI/CD 中。当镜像推送到仓库并且存在镜像配置时,DTR 会将其推送到另一个定义好的注册表。这有助于我们将仓库分布到不同的注册表上,提供高可用性。

镜像配置基于之前讲解的促销逻辑。我们将首先配置镜像方向,以定义使用的操作:拉取或推送。DTR 镜像允许我们将 Docker Hub 与本地的 Docker Enterprise DTR 环境集成。

我们需要理解 DTR 的元数据在注册表之间并未同步。因此,第一个注册表中的镜像扫描和签名信息在第二个注册表中将不可用。所有这些操作也必须在镜像注册表上执行。当镜像被推送到第二个注册表时,我们可以自动集成扫描。镜像签名需要外部集成。以下截图显示了仓库的镜像配置:

现在我们已经了解了自动化的内置功能,我们将回顾注册表缓存,以改善开发者的工作。

注册表缓存

注册表缓存将帮助我们在分布式环境中管理镜像。来自远程位置的用户可能会遇到延迟问题,而大镜像可能需要很长时间才能加载。我们可以部署中间注册表缓存以减少拉取时间。

缓存对用户是透明的,因为他们将使用原始的 DTR URL。当用户拉取镜像时,DTR 将检查是否经过授权,然后会将请求重定向到定义的缓存。这些缓存从 DTR 拉取镜像层并为用户保留副本。新的请求无需再次从 DTR 拉取镜像层。

要部署注册表缓存服务,我们将使用 docker/dtr-content-cache:<RELEASE> 镜像。

注册表缓存帮助我们管理分布式环境。Docker 客户端必须进行配置才能使用此功能。我们将把 "registry-mirrors": ["https://<REGISTRY's_MIRROR_URL>"] 添加到 daemon.json 配置文件中,或者通过用户设置页面配置用户使用它。为了使其工作,必须使用 DTR 的 API 将部署的缓存注册到 DTR 配置中。详细说明可以在以下链接中找到:docs.docker.com/ee/dtr/admin/configure/deploy-caches/simple/

我们现在将了解 DTR 的自动垃圾删除功能。

垃圾回收

垃圾回收将从 DTR 中移除未引用的层和清单。注册表数据可能会占用我们存储后端大量空间。这不仅仅是存储资源的问题。如果未加固的层仍然存在,安全性可能会受到威胁。建议移除所有未使用的层(也称为悬空镜像)。

该过程分为两个阶段:

  1. DTR 的垃圾回收器将搜索所有注册表清单。那些具有活动内容并且被其他镜像包含的图像层将不会被删除。

  2. 该过程将扫描所有的二进制大对象(blobs)。未包含在第一阶段清单中的对象将被删除。

可以通过 bin/registry garbage-collect 命令手动运行垃圾回收。通常我们会通过集成到 DTR Web UI 中的计划任务来进行。垃圾回收选项将允许我们使用类似 cron 的逻辑定期配置移除未引用的层。我们还将设置允许移除过程运行的时间,因为这可能需要相当长的时间。以下截图展示了垃圾回收配置页面:

在下一部分,我们将学习如何部署 DTR 的备份。

备份策略

DTR 的备份过程不会导致任何服务中断。备份过程可以在集群的任何节点上执行。建议从同一副本创建所有备份。这将帮助我们至少恢复该副本。我们可以使用该副本重建完整的 DTR 集群环境。

以下清单显示了将作为 DTR 备份 TAR 文件一部分存储的内容:

  • DTR 配置

  • 仓库元数据

  • 用户访问控制和仓库配置

  • DTR 通信所需的 TLS 证书和密钥

  • 图像的签名和摘要,包括 Notary 的集成

  • 图像扫描结果

以下内容将不会包含在您的备份中:

  • 图像的层

  • 用户、团队和组织

  • 用于图像扫描的漏洞数据库

注意图像的内容,因为用户、团队和组织将包含在 UCP 的备份中,漏洞数据库(CVE 和报告)可以在需要时重新创建。

默认情况下,如果我们没有执行任何备份,DTR 的 web UI 会显示警告信息。

我们将通过使用REPLICA_ID=$(docker inspect -f '{{.Name}}' $(docker ps -q -f name=dtr-rethink) | cut -f 3 -d '-') && echo $REPLICA_ID来查找健康副本的 ID,然后我们将使用此 ID 执行备份:

$ docker container run \
--rm \
--interactive \
--log-driver none \
--ucp-username <UCP_USERNAME> \
--ucp-password <UCP_PASSWORD> \
--ucp-url <UCP_COMPLETE_URL> \
--ucp-ca "$(curl -s -k <UCP_COMPLETE_URL>/ca)" \
--existing-replica-id <REPLICA_ID> > dtr-backup.tar.gz

请记住,此文件不包括图像的二进制数据或元信息。我们需要为 DTR 的存储后端包含第三方备份解决方案。

我们可以使用有效的 UCP CA 证书通过--ucp-ca或使用--ucp-insecure-tls连接到 UCP。

要恢复 DTR,我们将使用相同的docker/dtr镜像版本。我们将使用docker container run docker/dtr:<RELEASE> restore

$ docker container run --rm --ti docker/dtr:<RELEASE> restore \
 --ucp-insecure-tls \
 --ucp-url <UCP_COMPLETE_URL> \
 --ucp-username <UCP_USERNAME> \
 --ucp-password <UCP_PASSWORD> < PREVIOUS-DTR-BACKUP.tar

此命令不会恢复图像的二进制数据和元信息,因为备份仅提供恢复所有 DTR 进程及其配置所需的信息。

以下部分将帮助我们了解如何监控 DTR 的健康状况。

更新、健康检查和故障排除

DTR 应用程序升级有时会集成数据库修改。因此,必须确保正确的升级路径。upgrade命令可以从任何节点执行,因为我们将在所有 DTR 副本上执行此命令。我们将使用副本的 ID 或交互模式来升级每个 DTR 副本。升级过程将替换所有副本容器。

建议查看 Docker 文档中关于更新程序的相关内容,链接如下:docs.docker.com/ee/dtr/admin/upgrade

DTR 使用语义版本控制。这对于遵循升级路径至关重要。由于有时升级会修改数据库对象,因此不支持降级。

不同补丁版本之间的升级可以跳过,如果应用了小版本。补丁不会修改数据库,因此可以应用 CA,而对象的数据不会发生变化。

另一方面,小版本之间的升级必须遵循版本号,尽管我们可以跳过中间的补丁,如前所述。

重大版本升级需要先升级到最新的小版本/补丁版本,然后才能进行下一个重大版本的升级。此过程将实施一系列更改,在此升级之前,必须确保有有效的备份。记得验证有效图像的数据备份。

为了监控 DTR,我们将使用常见的容器监控程序。我们也可以使用 UCP 的堆栈视图,因为 DTR 是作为多容器应用程序部署的。所有副本都会显示。然后,我们可以点击每个副本的链接并检查其资源。

DTR 提供以下监控端点。我们将使用它们来验证其健康状态:

端点 描述

| /_ping | 这个端点显示副本的状态。我们可以使用第三方监控工具验证状态。如果副本正常,我们将获得 "Healthy":true

$ curl -ks https://<DTR_COMPLETE_URL>/_ping
{"Error":"","Healthy":true}

|

/nginx_status 这将显示常见开源 nginx 的状态和统计信息页面。
/api/v0/meta/cluster_status 对于所有副本的状态,我们将使用这个端点。此操作需要认证,因为我们将访问 DTR 的 API。我们将使用任何管理员的访问权限。此端点将显示集群的整体状态及其副本列表和状态。

我们还将检查 DTR 容器日志中的错误。在这种情况下,我们通常会将这些日志集成到第三方日志管理应用程序中。

日志记录

按照标准,DTR 的容器日志会显示所有应用程序错误。DTR 还在其 Web UI 中提供了一个视图,显示所有作业日志。我们将获得关于环境中执行的众多操作的详细信息。此日志视图提供了有用的审计信息,因为它包含了 DTR 中执行的所有镜像管理操作。

接下来的部分将向我们展示如何恢复不健康的 DTR 环境。

DTR 灾难恢复

DTR 使用高可用策略进行部署。因此,我们会遇到多种情况,取决于有多少副本处于不健康状态。

一些副本不健康,但我们保持集群法定人数的状态

在这种情况下,大多数副本是健康的,因此整体集群的状态是健康的。我们将尽快移除不健康的副本并添加新的副本。重要的是,在移除不健康副本之后,才加入新的副本。我们将逐步执行此过程,移除一个不健康副本,添加一个新的副本,依此类推。这样可以保持整体集群的状态不变。我们只会移除已识别的不健康副本,因此我们需要首先识别哪些副本处于失败状态,使用 docker ps --format "{{.Names}}" | grep dtr 来查找。一旦识别出不健康副本,我们将执行 docker container run docker/dtr:<RELEASE> remove 来删除相关副本:

$ docker container run --rm --ti docker/dtr:<RELEASE> remove \
 --existing-replica-id <HEALTHY_REPLICA_ID> \
 --replica-ids <HEALTHY_REPLICA_ID> \
 --ucp-insecure-tls \
 --ucp-url <UCP_COMPLETE_URL> \
 --ucp-username <UCP_USERNAME> \
 --ucp-password <UCP_PASSWORD>

尽管 --replica-ids 参数允许我们移除副本列表,但建议对每个不健康的副本执行以下步骤,每移除一个副本就添加一个新的副本:

$ docker container run --rm --ti docker/dtr:<RELEASE> join \
 --existing-replica-id <HEALTHY_REPLICA_ID> \
 --ucp-insecure-tls \
 --ucp-url <UCP_COMPLETE_URL> \
 --ucp-username <UCP_USERNAME> \
 --ucp-password <UCP_PASSWORD>

这将加入一个新的副本。请务必等待同步完成后,再继续添加新的副本。

大多数副本不健康

如果大多数副本都不健康,集群的状态将变得不健康,因为它失去了法定人数。然而,如果我们仍然有至少一个健康节点,就可以使用这个副本来修复集群。我们将使用 docker container run docker/dtr:<RELEASE> emergency-repair 执行紧急修复程序。

我们可以通过 REPLICA_ID=$(docker inspect -f '{{.Name}}' $(docker ps -q -f name=dtr-rethink) | cut -f 3 -d '-') && echo $REPLICA_ID 查找到健康副本的 ID,然后执行紧急修复程序:

$ docker container run --rm --ti docker/dtr:<RELEASE> emergency-repair \
 --existing-replica-id <HEALTHY_REPLICA_ID> \
 --ucp-insecure-tls \
 --ucp-url <UCP_COMPLETE_URL> \
 --ucp-username <UCP_USERNAME> \
 --ucp-password <UCP_PASSWORD>

该过程应完全恢复一个副本,然后我们将添加新的副本以恢复集群的一致性。

所有副本都不健康

在这种情况下,如果没有现有的备份,我们将无法恢复集群。我们将使用 docker container run docker/dtr:<RELEASE> restore。拥有有效的 DTR 备份至关重要:

$ docker container run --rm --ti docker/dtr:<RELEASE> restore \
 --ucp-insecure-tls \
 --ucp-url <UCP_COMPLETE_URL> \
 --ucp-username <UCP_USERNAME> \
 --ucp-password <UCP_PASSWORD> < PREVIOUS-DTR-BACKUP.tar

此命令不会恢复 Docker 镜像。我们必须为此数据实施单独的程序。我们将使用正常的文件系统备份和恢复程序。一旦我们有了健康的副本,就可以按照先前描述的程序将新的副本加入集群。

总结

本章介绍了 DTR 的功能和组件。我们学习了如何通过高可用性策略在生产环境中实施 DTR。我们回顾了不同的安全存储镜像的解决方案。

我们还介绍了镜像扫描和签名。这两种选项使我们能够通过与 UCP 的应用程序部署平台集成来提高镜像的安全性。组织内的用户将通过 DTR 集成的 RBAC 系统享有不同级别的镜像访问权限。

CI/CD 环境已经改变了我们如今创建和部署应用程序的方式。我们回顾了使用 DTR 构建的不同功能,这些功能帮助我们在 CI/CD 管道中集成镜像构建、共享和安全性。我们还学习了如何镜像仓库并通过注册表缓存改善用户体验。

需要了解 DTR 和 UCP 的知识才能通过考试。我们需要了解它们在集群节点上的组件分布以及它们如何工作。我们还需要理解它们的安装过程,以及如何确保它们的健康状态。

这是与 Docker 企业平台相关的最后一章。后续章节将覆盖考试所需的内容,包括一些快速的主题复习和进一步的问题解答。

问题

  1. DTR 包含哪些功能?

a) 仓库负载均衡

b) 仓库镜像

c) 仓库签名

d) 以上所有内容

  1. 我们需要多少个 DTR 副本才能为 Docker 镜像的层提供高可用性?

a) 我们将需要至少三个 DTR 副本才能提供高可用性。

b) DTR 不管理数据的高可用性。我们需要为 DTR 存储提供第三方解决方案。

c) DTR 管理当我们部署多个副本时的卷同步。

d) 以上所有陈述都为真。

  1. DTR 包含哪些过程?

a) garant

b) jobrunner

c) notary-client

d) auth-store

  1. 关于如何部署具有高可用性的 DTR,哪些陈述是正确的?

a) 配置负载均衡器作为透明反向代理。我们将所有针对 DTR FQDN 的请求转发到任何一个副本。

b) 部署共享存储,允许所有 DTR 副本在相同位置存储镜像的数据和元信息。

c) 在一个节点上使用之前创建的共享存储部署第一个 DTR 副本。然后,在不同的节点上添加至少两个更多的副本。

d) 上述所有陈述都是正确的。

  1. DTR 的备份中不包含哪些内容?

a) 仓库元数据和镜像的层。

b) RBAC 配置。

c) 图像签名。

d) 上述所有陈述都是正确的。

深入阅读

以下链接将帮助我们理解本章中涉及的一些主题:

第十八章

第四章 - 为 Docker 认证助理考试做准备

本节将重点介绍 DCA 考试。本书涵盖了所有考试主题,本节将总结书中的内容,帮助你理解通过 DCA 考试所需掌握的知识。

本节包含以下章节:

  • 第十四章,总结重要概念

  • 第十五章,模拟考试题和最后的注意事项

总结重要概念

在本章中,你将了解哪些主题对考试最为重要,并对通过Docker 认证助理DCA)考试所需的知识有一个清晰的了解。

我们将回顾所有关于编排、镜像管理、Docker 平台组件的安装和配置、单节点和集群环境的网络实现、安全功能以及基于容器的应用数据管理策略的主题。这些概念已经在不同章节中涵盖。

本章我们将总结以下主题:

  • 回顾编排概念

  • Docker 镜像概念的简要总结

  • Docker 架构、安装和配置主题的总结

  • 网络主题的总结

  • 理解安全概念和相关的 Docker 功能

  • 快速总结 Docker 存储和卷

在本章结束时,你将为一些类似考试的问题做好准备,这些问题已经在下一章为你准备好。在查看一些样题之前,让我们先来讨论一下我们已经学习的编排概念。

第十九章:回顾编排概念

编排是 DCA 考试的重要主题,它占据了考试问题的 25%。在本书的第二部分,我们介绍了编排,并讲解了 Docker Swarm 和 Kubernetes。

编排概念已在第七章,编排简介,第八章,使用 Docker Swarm 进行编排,第九章,使用 Kubernetes 进行编排,第十章,Docker 企业平台简介,以及第十一章,通用控制平面中讲解。

这是 Docker Swarm 功能的简要总结。我们建议你阅读这个总结,以帮助你记住我们已经学习的概念:

  • 在介绍编排之前,我们开始讨论多容器应用程序,因为它是容器编排的第一种方法。它们在本地运行,使用 Docker Compose(docker-compose工具)和应用程序组件,它们的交互通过docker-compose.yml YAML 文件来描述。多容器应用程序将所有组件一起运行在 Docker 主机上,但我们可以对其组件进行伸缩操作,以及与它们交互和查看它们的日志。

  • Docker Swarm 编排 Docker 服务,为其提供弹性、内部发现和负载均衡,在集群环境中运行。我们的应用工作负载将在集群中分布。

  • 我们将在 Docker Swarm 中使用两种类型的节点角色——管理节点和工作节点,这些角色可以进行修改。

  • 我们将部署多个管理节点和多个工作节点,以提供集群的高可用性以及部署在其上的工作负载。

  • 其中一个管理节点也是集群的领导者,它会在一个内部数据库中更新所有集群资源的更改,并在管理节点之间进行同步。Docker Swarm 使用 Raft 算法来更新更改,因此在提交更改之前,需要管理节点之间达成共识。

  • Docker Swarm 拥有管理平面、控制平面和数据平面。管理平面和控制平面可以与数据平面隔离,并且开箱即用地进行加密。数据平面也可以加密,但不是默认加密(我们需要对每个自定义网络进行加密)。

  • Docker Swarm 会发放和维护一个内部证书授权机构CA),并为所有集群组件管理证书。我们可以锁定这些信息以保证其安全。

  • 为了维持集群健康,至少需要(管理节点数 / 2 + 1)个健康的管理节点。如果管理节点数量少于要求的数量,集群内无法进行任何更改,但应用程序工作负载将继续运行。如果服务失败,且集群不健康,则无法恢复。

  • Docker Swarm 使用 Raft 日志来维护节点之间的内部键值存储同步。因此,需要一个奇数数量的管理节点才能维持共识。这同样适用于 Kubernetes,但它使用etcd作为键值存储。

  • 所有节点都可以运行应用程序工作负载,但我们可以根据需要更改此行为,例如在需要排空节点或禁止新工作负载时,而不会中断已经运行的工作负载。

  • 集群工作负载被声明为服务,并要求有一定数量的实例或副本才能保持健康。这些资源是任务,它们将运行一个容器。

  • Docker Swarm 不管理容器;它仅管理服务。因此,我们是基于服务来部署应用程序的,而不是部署独立的容器。

  • 服务默认会分配一个虚拟 IP 地址,这个地址在服务生命周期内不会改变。任务只会运行一个容器;它们没有关联的 IP 地址,并且始终保持原有名称。如果任务的容器死亡或需要进行一些更新,系统将创建一个新的任务,并保持原有名称。容器将获得新的 IP 地址,但内部负载均衡器会将其作为服务的后端端点关联。

  • 我们可以根据需要随时扩展或缩减服务所需的任务数量。然而,在这种情况下,Docker 不会管理我们的应用程序行为。

  • 任务会自动调度到健康节点上,前提是该节点有足够的资源来运行关联服务的任务,但我们可以强制将任务定位到特定的节点上。

  • Docker 提供了一些模板工具,帮助我们使用 Docker Swarm 变量来格式化、筛选和创建独特的资源。

  • Docker Swarm 中的网络使用桥接接口,正如我们在学习 Docker 容器时也了解到的那样。我们使用 VXLAN 技术部署覆盖网络,跨集群分布,以提供在不同主机上运行的容器之间的通信及其他网络功能。

  • Docker Swarm 提供了默认的路由网格策略,用于发布集群服务,以便用户和其他应用程序使用。默认情况下,服务的端口将在所有集群节点上发布,即使它们没有运行任何服务任务。内部路由将服务请求引导到适当的后端容器。我们可以使用常见的 Docker Swarm 命令选项更改这些行为。

  • 正如我们在 Docker Engine 中学到的那样,服务默认并不会被发布供使用。我们需要手动发布服务端口和进程。

  • 将应用程序发布到外部可以通过 Docker Swarm 的路由网格或 Docker Enterprise 中的 Interlock 来完成。Interlock 提供了一种集成的自动化反向代理解决方案,保护应用程序的后端。我们只需发布 interlock-proxy 组件,而 Docker Swarm 服务会内部接收请求。因此,服务不需要额外发布;只需配置一些标签,告知 Interlock 应用程序所需的转发。

  • 我们可以根据需要创建任意数量的覆盖网络,它们将相互隔离,这一点与自定义桥接网络的工作原理类似。

  • 编排引入了一些新概念,如秘密和配置,以提供分布式集群内存储的信息。Docker Swarm 会对秘密进行加密和保护,我们使用这些秘密配置密码、证书或令牌,通过内存文件系统进行管理。配置对象帮助我们在不同主机上运行的容器间分发配置,而无需手动在节点之间同步文件。

  • 在 Docker Swarm 中,我们使用 Docker stacks 部署应用程序。这些资源允许我们部署跨集群分布的多服务应用程序。我们将在一个类似 docker-compose 的文件中定义所有所需的应用程序资源(服务、秘密、网络、配置、卷等),然后使用这些文件来部署完整的应用程序。所有应用组件的更改或更新都应该写入这些文件,因为它使我们能够将应用程序的部署管理为代码。

  • 应用组件的更新通过滚动更新进行管理。我们可以手动或使用 Docker stacks 部署更改。在这两种情况下,我们都能平稳地部署更改,避免服务中断和对用户的影响。如果更新出现问题,我们可以轻松执行回滚,恢复到之前的服务配置。

  • 我们还回顾了 Kubernetes 编排器,因为它被包含在 Docker Enterprise 中。这个编排器与 Docker Swarm 有许多不同之处,尽管两者最终都管理容器。我们学习了 Kubernetes 组件及其交互方式。

  • Docker Enterprise 为我们提供开箱即用的完整原生(非定制化)Kubernetes,包括默认使用 Calico 作为 容器网络接口CNI)。所有工作节点(DTR 需要专用工作节点)可以设置为运行 Docker Swarm、Kubernetes 工作负载,甚至同时运行两种编排工作负载。

  • Pods 是 Kubernetes 中最小的调度单元,但它们不提供弹性。我们需要将它们集成到编排的模板化资源中,例如 ReplicaSets、DaemonSets 或部署。

  • Kubernetes 提供一个扁平化的网络。这意味着所有部署的 pods 都可以互相看到。服务之间的通信默认始终是允许的。为了确保安全,在这种情况下,我们需要部署 NetworkPolicy 资源,仅允许特定组件之间的通信。

尽管 Docker 堆栈和使用 docker-compose 的多容器应用程序使用相同类型的 YAML 文件,但有些键仅对其中一个有效。例如,像 depends_onbuildvolumes_from 这样的键仅适用于 Docker Compose 多容器应用程序;因此,当我们尝试在 Docker 堆栈中使用它们时,会收到一条警告信息,提示此问题。

让我们回顾一下考试所需的主题。

考试所需的知识

考试将验证我们对以下主题的知识,除此之外还有其他内容:

  • 完成 Swarm 模式集群的设置,包括管理节点和工作节点

  • 描述并演示如何将运行单个容器的指令扩展为在 Swarm 中运行的服务

  • 描述 Swarm 集群中法定人数的重要性

  • 描述运行容器和运行服务之间的区别

  • 解释 docker inspect 命令的输出

  • 使用 docker stack deploy 将应用程序部署转换为堆栈文件,通过 YAML compose 文件

  • 操作运行中的服务堆栈

  • 描述并演示编排活动

  • 增加副本数

  • 添加网络和发布端口

  • 挂载卷

  • 描述并演示如何运行复制服务和全局服务

  • 应用节点标签来展示任务的分配

  • 描述并演示如何使用模板进行 docker service create

  • 识别排查无法部署服务的步骤

  • 描述 Docker 化应用如何与传统系统进行通信

  • 描述如何将容器化的工作负载部署为 Kubernetes pod 和部署

  • 描述如何使用 ConfigMap 和 secret 资源为 Kubernetes pods 提供配置。

这些主题摘自 Docker 官方学习指南,指南可以在 success.docker.com/certification 找到。

Docker 镜像概念的简要总结

镜像是创建容器的基础,这个主题大约占 DCA 考试题目的 20%。我们在第二章《构建 Docker 镜像》中深入讨论了 Docker 镜像,但我们在第三章《运行 Docker 容器》、第六章《Docker 内容信任介绍》以及 第十三章,使用 DTR 实现企业级注册中心 中也有所提到。在本章中,我们将快速回顾在这些章节中看到的所有概念,以便做一个总结。

在进入所需知识部分之前,让我们回顾一下与 Docker 镜像相关的最重要的概念、特性和操作:

  • 镜像基于写时复制文件系统策略。它们基于不同的重叠层,通过不同的联合文件系统和存储驱动程序应用。目前,容器最常用的文件系统驱动程序是 overlay2。Docker 引擎会为我们的系统选择最合适的图形驱动程序,尽管我们可以更改它。

  • 容器只是运行在 Docker 主机上的隔离进程。我们使用镜像作为模板,提供根文件系统和元信息来控制进程的行为。

  • 创建镜像有三种方法:

    • 使用 Dockerfile:此文件包含安装应用程序所需的所有步骤,以及如何启动它。我们还提供应该用于与容器进程通信的端口和协议。此方法是可重复的,并提供基础设施即代码(Infrastructure-as-Code)行为。

    • 运行容器和提交:在这种情况下,我们运行一个容器,并在容器内运行命令来安装和配置我们的应用程序。当所有的更改都在容器的文件系统中完成后,我们将这些更改提交,以制作镜像。此方法不可重复。我们通常在无法自动化应用程序安装的情况下使用这种工作流程,例如。

    • 从零开始构建镜像:在这种情况下,镜像是轻量级的,因为它们只包含一个空的根文件系统和应用程序二进制文件及依赖项。这个根文件系统不包含任何不必要的操作系统文件。我们通过 Dockerfile 的复制命令将二进制文件添加到其中。

  • 多阶段构建也可以作为创建镜像的替代方法。在这种情况下,我们在同一个 Dockerfile 中声明不同的构建过程。我们为每个过程定义一个描述性名称,并定义一个工作流,将文件从不同的构建中复制过来。这使我们能够定义一个阶段,使用所需的编译器、头文件或库在应用程序开发镜像中编译应用程序,然后只将最终开发产品复制到另一个包含运行时环境的阶段。因此,运行时镜像比开发镜像小得多。

  • Dockerfile 通过执行容器来创建镜像。每个容器都会在其根文件系统中进行更改,这些更改会被提交(存储)以供后续容器使用,执行时会使用先前容器的层。

  • 更小的镜像更安全,因为镜像中包含不必要的二进制文件、库和配置是有风险的。镜像应仅包含应用程序所需的内容。

  • 构建更好的镜像时,有几个重要的实践需要遵循:

    • 切勿将调试工具或编译器添加到生产环境镜像中。

    • 声明镜像上所有需要的资源,例如暴露的端口、主要进程执行所需的用户以及将用作卷的目录。这些有助于其他用户轻松理解你的应用程序如何工作并如何使用。

    • 除非进程严格要求,否则不要在应用程序镜像中使用 root 用户。

    • 构建镜像时,每个容器只运行一个进程。每个容器运行多个进程时,维护和验证其健康状态会变得更加困难。

    • 我们总是需要在镜像间层的可移植性和镜像的大小之间做出选择。有些情况下,拥有较少的层会更好,而在其他时候,拥有更多的层则更好,因为其他镜像会重用它们。镜像层缓存对于加速镜像构建过程至关重要。

    • 始终在 Dockerfile 中添加健康检查,以帮助 Docker 引擎验证容器的健康状态。

  • Docker 提供了构建和分发 Docker 镜像所需的所有命令。我们还可以检查它们的内容或构建历史,以帮助我们调试它们的过程或创建新的镜像。

  • 需要理解的是,悬挂镜像(来自先前构建的未引用层)会一直保留在 Docker 主机上,直到你删除它们。管理员应保持 Docker 平台的整洁,避免由于磁盘空间丢失导致主机性能下降。

  • 在容器平台上,良好的镜像标签管理是基础。我们还可以在 Dockerfile 中使用标签,为 Docker 镜像添加元信息。你应该尽量通过标签唯一地标识镜像,但请记住,只有镜像的 ID 才能真正唯一地标识一个镜像。一个镜像可以有多个名称和标签,但只有一个 ID。

  • 我们可以在 Dockerfile 中包含变量。这将帮助我们为不同的阶段构建具有特殊功能的镜像。我们可以将生产环境准备好的镜像交付到生产系统,同时在测试镜像中包含调试和监控工具。它们仍然会包含通用的应用程序二进制文件,但我们将使用调试版本来查看一些特定的问题。变量也可以作为 docker build 命令行的参数进行修改。

让我们了解一下考试所需的主题。

考试所需的镜像管理知识

考试将验证我们对以下主题的了解,除此之外还有其他内容:

  • 描述 Dockerfile 的使用

  • 描述选项,如 addcopyvolumesexposeentrypoint

  • 识别并显示 Dockerfile 的主要部分

  • 描述和演示如何通过 Dockerfile 创建高效的镜像

  • 描述和演示如何使用 CLI 命令管理镜像,例如listdeleteprunermi

  • 描述和演示如何使用filterformat检查镜像并报告特定属性

  • 描述和演示如何为镜像打标签

  • 描述和演示如何应用文件来创建 Docker 镜像

  • 描述和演示如何显示 Docker 镜像的各层

  • 描述和演示如何将镜像修改为单层镜像

  • 描述和演示注册表功能

  • 部署注册表

  • 登录到注册表

  • 在注册表中使用搜索功能

  • 将镜像推送到注册表

  • 在注册表中签署镜像

  • 从注册表中拉取和删除镜像

这些主题摘自 Docker 官方学习指南,可以在success.docker.com/certification找到。

Docker 架构、安装和配置主题的总结

Docker 平台的安装和配置是每个 Docker 企业管理员的关键。这些主题占考试内容的 15%。它们已在多个章节中涵盖,涉及独立和集群环境。我们在第一章《Docker 与现代基础设施和应用》,第八章《使用 Docker Swarm 进行编排》,第十章《Docker 企业平台》,第十一章《通用控制平面》,以及第十三章《使用 DTR 实现企业级注册表》中学习了这些概念。

这是关于 Docker 平台安装和配置的特别特点和技巧的快速总结。我们建议你阅读此总结,以确保记住所学的概念:

  • Docker 组件在独立和集群环境中的应用:我们应该对 Docker 企业组件的分布和功能有较好的了解。

  • 不同平台上各组件的安装过程:我们看到在 Docker 社区和 Docker 企业环境中,安装过程都很简单。回顾不同平台上的安装过程,并确保你对配置文件位置有清晰的了解。

  • 你必须了解所有组件的要求,以及部署容器即服务CaaS)企业级解决方案所需的步骤,确保核心组件的高可用性。

  • 第一章,使用 Docker 的现代基础设施与应用程序,展示了许多 Docker 引擎的配置过程。默认情况下,Docker 会选择最佳的存储驱动程序来管理 Docker 层。记住,我们使用了 overlay2,因为如果我们的安装有不同的需求,应该可以更改它。

  • 确保你清楚了解 /var/lib/docker 目录(或已配置目录)下的文件,以及 Docker 引擎备份中应存储的内容。你还了解了创建 Universal Control PlaneUCP)和 Docker Trusted RegistryDTR)备份的程序,以及在何种情况下需要进行恢复的步骤。

  • 只有 Docker Enterprise 和 Kubernetes 提供基于角色的访问控制。我们在 第十一章,Universal Control Plane 和 第十三章,使用 DTR 实现企业级镜像仓库 中分别介绍了 Docker Enterprise 权限和 UCP 与 DTR 的基本配置。

  • 回顾我们如何配置 TLS 通信以进行客户端认证,当我们不需要不同授权级别时。这部分内容在 第一章,使用 Docker 的现代基础设施与应用程序 中有详细介绍。

  • Cgroups 和内核命名空间提供容器隔离。这是确保进程拥有足够资源且不与同一主机上其他进程进行非授权通信的关键。

现在我们将回顾考试应了解的主题。

参加考试所需了解的 Docker 平台知识

考试将验证我们对以下主题的理解,及其他内容:

  • 描述安装的规模要求

  • 描述并演示如何设置仓库,选择存储驱动程序,并在多个平台上安装 Docker 引擎

  • 描述并演示如何配置日志驱动程序(splunkjournald 等)

  • 描述并演示如何设置 Swarm,配置管理节点,添加节点,并设置备份计划

  • 描述并演示如何创建和管理用户与团队

  • 描述并演示如何配置 Docker 守护进程在启动时自动启动

  • 描述并演示如何使用基于证书的客户端-服务器认证,以确保 Docker 守护进程具有访问镜像仓库的权限

  • 描述命名空间、cgroups 和证书配置的使用

  • 描述并解读错误信息,以便在没有帮助的情况下排查安装问题

  • 描述并演示如何在 Amazon Web ServicesAWS)和本地环境中,以高可用性部署 Docker 引擎、UCP 和 DTR

  • 描述并演示如何为 UCP 和 DTR 配置备份

这些主题摘自 Docker 官方的学习指南,可以在success.docker.com/certification找到。

网络主题总结

网络是微服务应用架构的核心组件之一。更快的网络促使了分布式架构的演变。通过现代基础设施,甚至在云或云混合架构上,也可以提供高可用性和弹性。容器像小型虚拟节点一样工作,并且它们获得虚拟接口。我们了解到,网络命名空间允许我们在同一主机上隔离进程,即使它们使用相同的桥接接口与实际网络进行通信,也可以从主机的网络命名空间中隔离开来。集群中的分布式网络也非常简单,因为 Docker Swarm 管理所有必要的内部基础设施和进程,以允许不同主机上的容器之间进行通信。Docker Swarm 中的覆盖网络跨集群分布,使用 VXLAN 封装流量,并且甚至可以进行加密。默认情况下,Docker Swarm 控制平面的组件通过相互 TLSMTLS)通信进行保护,我们可以将应用数据与网络管理隔离开来。

所有这些主题都在多个章节中介绍了 Docker Engine、Docker Swarm 和 Kubernetes。我们在第四章中学习了容器持久性与网络,在第八章中学习了使用 Docker Swarm 进行编排,在第九章中学习了使用 Kubernetes 进行编排,在第十一章中学习了通用控制平面,以及在第十二章中学习了在 Docker 企业版中发布应用程序

在第九章中,我们学习了 Kubernetes 如何实现集群范围的网络功能。我们还将这些功能与 Docker Swarm 实现进行并列比较,以便更好地了解如何使用两者,或者如何创建能够在它们任何一个上运行的容器工作负载。

我们还了解到,容器可以在内部暴露其应用程序进程。其他容器可以使用它们的服务,但我们需要发布它们的端口以供外部用户和应用程序访问。这一点非常重要,因为在 Docker Swarm 中,处于同一网络的容器之间会确保安全。它们是相互隔离的,因此我们只能发布前端应用程序的组件。

让我们总结一下网络相关的主题:

  • Docker 引擎的网络是基于桥接网络的,尽管我们可以使用 MacVLAN 接口(带有真实 IP 地址),基于主机的网络(使用其网络命名空间),甚至可以通过插件扩展默认行为。我们可以使用默认或自定义桥接网络。自定义网络还部署内部 DNS 设施,因此在这些网络上运行的容器可以通过它们的名称互相识别。在某些特殊情况下,部署不带网络功能的容器也是有用的。

  • Docker Swarm 中的网络配置很简单,因为 Docker 创建新的虚拟网络(覆盖网络),并部署 VXLAN 隧道来封装所有主机的流量。如果服务任务部署在同一个覆盖网络上,它们的容器可以互相看到。

  • Kubernetes 的网络模型更加简单。它基于一个扁平网络,服务和 Pod 默认情况下始终可达。为了使其工作,我们需要集成一个 CNI。每个 CNI 都有自己的扁平网络模型实现,而 Docker Enterprise 默认部署 Calico(www.projectcalico.org/)。

  • 扁平网络默认情况下不安全,因为应用程序组件没有被隔离。我们将使用网络策略来隔离应用程序,通过命名空间、标签等进行分组。NetworkPolicy 资源管理连接规则,允许或禁止特定 Pod 的连接及其流量。

  • Docker Swarm 节点默认使用加密的 TLS 通信(双向 TLS)。Docker 管理所有所需的证书。用户与集群的通信是不安全的,但我们也可以手动创建安全通信(在《第八章》中有一个完整的示例,解释所有必需的步骤,使用 Docker Swarm 进行编排),或者使用 UCP 集成的 基于角色的访问控制RBAC)。UCP 提供用户包,包含创建安全 TLS 隧道所需的所有文件。

  • Kubernetes 还会加密其控制平面。Docker Enterprise 会为我们完成所有部署工作,在安装后会启动一个完全功能的 Kubernetes 集群。证书将用于在 Kubernetes 组件和用户之间默认部署 TLS 隧道。

  • 本地自定义桥接和覆盖网络部署了内部 DNS。因此,容器和服务可以通过它们的名称进行发现。容器使用内部 DNS,外部解析将被转发到特定的外部 DNS。默认情况下,容器接收主机的 DNS 配置,但我们可以更改这种行为。

  • Kubernetes 也集成了内部 DNS。在这种情况下,kube-dns 组件将管理所有服务条目。

  • 内部负载均衡也部署在覆盖网络中。请记住,服务可以是复制的或全局的。在这两种情况下,默认使用 vip 模式,服务将获得一个特殊入口网络中的 IP 地址。此 IP 地址已注册,内部负载均衡器将请求路由到所有可用服务的副本。我们可以使用 dns-round-robin 模式来避免这种行为。

  • Kubernetes 的内部负载均衡具有类似的行为。所有服务默认都会接收一个内部虚拟 IP 地址(ClusterIP)。Kubernetes 中的服务是 Pod 的逻辑组,服务的请求默认会转发到所有关联的 Pod。

  • 如前所述,部署在容器中的应用程序默认不会发布其端口,除非我们声明这种行为。在 Docker 引擎上发布端口很简单,我们可以确保只有特定的 IP 地址会监听多网卡节点上发布的端口。桥接网络使用 NAT 来发布应用程序的端口。Docker 会创建所有所需的主机防火墙规则,以允许并路由这些流量。如果我们使用主机的网络,所有容器暴露的端口都会被发布,应用程序将直接可访问。

  • 我们还了解到,Docker Swarm 中的服务默认会在所有节点上发布,即使它们没有运行任何服务任务。这个功能被称为路由网状(router mesh),应用程序端口将在所有集群的主机上可用。内部负载均衡也将通过入口覆盖网络应用,并且不同主机上的实例将是可达的。这可能存在安全隐患,因为所有应用程序端口都可以在所有主机上访问。

  • Kubernetes 的 NodePort 服务与 Docker Swarm 的路由网状行为相似。声明为 NodePort 的服务将在所有集群节点上发布其端口。然而,Kubernetes 也有 LoadBalancer 服务类型。这些服务将通过基础设施负载均衡器直接发布。此集成仅适用于某些云服务提供商。

  • UCP 提供了 Interlock 作为避免不安全的路由器网状发布的解决方案。我们已经了解了 Interlock 的组件和部署方式,以及如何使用该工具发布应用程序。Interlock 的端口必须被发布,但所有其他应用程序的服务可以通过 Interlock 访问。因此,我们不需要发布应用程序的端口。这提高了安全性,因为 Interlock 充当反向代理,提供 TLS 安全性、基于主机和内容的服务路由以及粘性会话等功能。Interlock 代理组件将使用服务标签进行更新;因此,只有具有特定标签的服务才会被发布。我们已经了解了这些必需的标签,并审查了一些示例用法。

  • Kubernetes 可以集成入口控制器,避免 NodePort 在集群范围内发布应用程序。入口控制器部署类似反向代理的功能,将请求路由到匹配特定头部或内容规则的适当服务。这提高了安全性,因为服务不应该被直接发布。我们只发布入口控制器(例如使用服务策略),而入口资源管理着必要的规则,用于访问这些服务,尽管它们没有对外发布。

如前所述,网络在集群环境中至关重要。让我们回顾一下通过考试所需的一些主题。

通过考试所需的 Docker 网络知识

考试将验证我们对以下主题的了解,等等:

  • 描述容器网络模型及其如何与 Docker 引擎、网络和 IPAM 驱动程序交互

  • 描述内建网络驱动程序的不同类型及其使用场景

  • 描述 Docker 引擎、注册表和 UCP 控制器之间流动的流量类型

  • 描述并演示如何创建一个 Docker 桥接网络供开发人员使用其容器

  • 描述并演示如何发布端口,使应用程序可以对外访问

  • 确定容器在外部可访问的 IP 和端口

  • 比较和对比主机与入口发布模式

  • 描述并演示如何配置 Docker 使用外部 DNS

  • 描述并演示如何使用 Docker 对 HTTP/HTTPS 流量进行负载均衡,进而实现应用程序负载均衡(配置 Docker EE 的 L7 负载均衡)

  • 理解并描述 Docker 引擎、注册表和 UCP 控制器之间流动的流量类型

  • 描述并演示如何在 Docker 悬浮网络上部署服务

  • 描述并演示如何通过排查容器和引擎日志来解决容器之间的连接问题

  • 描述如何使用 ClusterIP 和 NodePort 服务将流量路由到 Kubernetes pod

  • 描述 Kubernetes 容器网络模型

这些主题摘自 Docker 官方学习指南,您可以在 success.docker.com/certification 查找。

下一部分将通过介绍与 Docker 平台安全性相关的必备知识来帮助您。

理解安全性概念及相关的 Docker 特性

当你在生产环境中运行应用程序时,安全性至关重要。我们学习了 Docker 及其组件提供的许多安全功能。我们首先回顾了容器如何与其他主机进程隔离,并了解了如何确保 Docker 引擎的安全性。然后,我们继续学习 Docker Swarm,在该环境中必须对整个集群应用安全策略。用户的访问也必须得到管理,我们需要提供身份验证和授权机制。Docker 企业版提供了更高层次的安全性。它包括完整的 RBAC 环境,使我们能够管理细粒度的对象和集群资源权限。

所有这些主题都在多章节中涉及了 Docker 引擎、Docker Swarm、Kubernetes 和 Docker 企业平台。我们在第一章,使用 Docker 进行现代基础设施与应用程序,第二章,构建 Docker 镜像,第四章,容器持久性与网络,第六章,Docker 内容信任简介,第八章,使用 Docker Swarm 进行编排,第九章,使用 Kubernetes 进行编排,第十一章,通用控制平面,第十二章,在 Docker 企业版中发布应用程序,以及第十三章,使用 DTR 实现企业级镜像仓库

我们必须记住,容器是通过镜像创建的,因此确保镜像的安全性也至关重要。遵循最佳实践是开发安全镜像的关键。Docker 企业版提供了几种策略来验证镜像的优先级、不可变性和内容安全性。

让我们回顾一下其中的一些安全主题:

  • Docker 是一个客户端-服务器应用程序。服务器将在本地(默认情况下)和远程可访问的套接字上发布其 API。我们可以通过限制对这些套接字的访问来限制 Docker 引擎的访问权限。在本地,只有具有文件系统权限的用户才能运行本地 Docker 引擎上的 Docker 命令。

  • Docker 引擎可以与操作系统提供的安全模块(如 SELinux 或 AppArmor)集成。Docker 提供了与容器配合使用的集成和默认配置文件。Docker 还与 Linux 内核集成,允许使用能力添加或移除特定的系统调用。还有一些更简单的安全建议,比如使用只读根文件系统和容器内的非 root 用户,这些也有助于我们提供安全的应用程序。

  • 镜像应当安全,以便创建安全的容器。镜像应只包含我们的流程所需的二进制文件、库和配置。所有与应用程序无关的内容都应避免。Docker Enterprise 提供了镜像内容安全扫描器。它将相关内容的文件哈希与已知的公开漏洞和漏洞利用数据库(互联网常见漏洞和暴露CVE))进行比对。我们了解了这个过程是如何工作的,以及如何集成标签提升(tag promotion),确保只有被授权的用户可以访问他们的镜像。这些都是 DTR 的一些功能。

  • 我们还可以签署镜像。这个过程确保镜像内容的不可变性和所有权。如果我们将镜像构建集成到持续集成和持续部署中,我们可以确保镜像是通过适当的工作流创建的。我们还可以提高 CaaS 的安全性,仅允许基于由组织内特定团队或用户签署的镜像的容器运行。

  • 我们了解了签署镜像所需遵循的所有自动化步骤以及集成到该过程中的所有密钥。镜像签名基于内容信任(Content Trust)逻辑,我们了解了它如何在 Docker 中集成,详细内容请参见第六章,Docker 内容信任简介

  • 我们提到了一些可以提高工作负载安全性的简单实践,例如运行只读根文件系统或为应用程序使用非根用户(或用户命名空间)。我们应该使用docker image inspect来查看镜像的规格,以便了解暴露的端口、应用程序的用户和将在容器内执行的命令。

  • 如本章所述,Docker Engine 和 Docker Swarm 都没有任何 RBAC 集成。另一方面,Docker Enterprise 组件具有集成的基于角色的访问控制。UCP 根据角色、权限和集合提供不同的 Docker Swarm 资源访问权限。我们可以配置细粒度的访问控制,管理卷、密钥、配置、网络等资源,因此用户只能对其资源执行允许的操作。用户将通过提供的 Web UI 或 Docker 命令行连接到集群,使用他们的 Docker 客户端软件和 UCP 的捆绑包来执行、审查和修改资源。这个压缩文件包含用户证书和环境脚本,旨在帮助用户轻松连接到集群。

  • DTR 有其独立的 RBAC 环境,与 UCP 的 RBAC 环境隔离。DTR 是一个镜像仓库,因此它的 RBAC 环境专门用于管理对存储在 CaaS 中的镜像的访问。我们可以精细控制权限,允许一组用户使用或修改镜像,而其他镜像则在团队或整个组织内公开。

  • DTR 和 UCP 默认通过单点登录解决方案进行集成,尽管我们可以更改这种行为。我们还可以将它们集成到我们的组织用户管理解决方案中,如 Active Directory,或任何兼容的轻量级目录访问协议LDAP)。

  • 我们学习了如何部署 Docker Enterprise 组件,以及如何管理用户、角色以及不同级别的资源和镜像访问权限。它们将通过奇数数量的软件节点进行高可用部署,我们需要一个外部负载均衡器来提供用户的访问权限。我们可以集成公司的证书,但也可以使用自动生成的证书。在这种情况下,我们需要将 DTR 的 CA 集成到我们的组织服务器和客户端主机中。

  • 虽然 Docker Swarm 需要 UCP 来集成用户管理,但 Kubernetes 实现了自己的 RBAC 系统。我们将能够使用令牌和证书进行身份验证和授权。Kubernetes RBAC 将适用于应用程序和用户,并与 Docker Enterprise 集成。

  • Docker Swarm 和 Kubernetes 提供了证书、密码、令牌等的安全存储。两者都提供了机密资源来管理任何需要保护的文件(或变量)。但是,虽然 Docker Swarm 中的机密是加密的,Kubernetes 中的机密默认情况下并未加密。在 Kubernetes 中,机密资源是使用 Base64 编码的,并且必须进行额外配置才能加密它们。

  • Kubernetes 在安全性方面有先进的功能,如 PodSecurityPolicy 资源,它允许我们强制对 Pods 进行安全设置,允许或禁止特定行为(如根进程和只读文件系统)。也可以实施访问控制器(UCP 的 Kubernetes 部署中已经预配置了一些)来强制执行 Pod 安全策略和其他安全功能,默认应用于我们 Kubernetes 集群中的任何工作负载。

  • 我们将使用 RBAC 来管理 UCP 和 DTR 用户访问。首先,我们将确保只有授权的用户才能管理和使用集群资源以支持他们的应用程序。DTR 的 RBAC 将保护镜像,只允许授权的用户操作和更新其内容。

下一部分将重点介绍通过考试所需的知识。

考试所需的 Docker 安全知识

考试将验证我们对以下主题的知识,此外还包括其他内容:

  • 描述安全管理和任务

  • 描述签名镜像的过程

  • 描述默认的引擎安全性

  • 描述 Swarm 默认安全性

  • 描述 MTLS

  • 描述身份角色

  • 比较和对比 UCP 的工作节点与管理节点

  • 描述使用外部证书与 UCP 和 DTR 集成的过程

  • 描述并演示镜像如何通过安全扫描

  • 描述并演示如何启用 Docker 内容信任

  • 描述并演示如何配置 UCP 中的 RBAC

  • 描述并演示如何将 UCP 与 LDAP/AD 集成

  • 描述并演示如何创建 UCP 客户端包

这些内容摘自 Docker 官方学习指南,网址为:success.docker.com/certification

快速总结 Docker 存储和卷

使用 Docker 容器需要不同的存储解决方案,正如我们在本书中所学到的那样。镜像和容器是通过多层文件系统策略创建的。然而,我们还必须管理容器化应用的持久性。这种持久性可以与应用数据相关,但我们还必须能够管理集群范围内的配置和状态。

我们在第一章,现代基础设施与 Docker 应用,第二章,构建 Docker 镜像,第四章,容器持久化与网络,以及第十三章,使用 DTR 实现企业级注册表中学习了有关安全性的话题。

这是本书中关于容器内存储和卷管理的主题的快速总结。我们建议你通读这个总结,以确保记住学到的概念:

  • 我们了解到,容器是基于不同的文件系统和解决方案的,具有一个共同特征——写时复制(copy-on-write)。这使我们能够创建多个不可变层来组织文件。每个层都是另一个层的基础,文件的修改将存储在最后一个被更改的层中。所有不可变层都被视为创建新容器的镜像。我们将为容器添加一个新的读写层。这些层依赖于主机存储。这种存储被称为图形存储,我们将根据主机操作系统使用不同的策略来管理它。Docker 会根据你的内核特性和安装的驱动程序选择最适合你主机的驱动程序。今天最流行且使用最广泛的驱动程序是 overlay2,它是许多 Linux 发行版的默认图形驱动程序。docker info 提供有关使用的驱动程序的信息。

  • 我们还了解到,镜像会在本地存储以便快速使用。当这些镜像必须与集群节点共享时,事情会变得复杂,尽管我们可以导出和导入镜像层。我们将使用镜像注册表来存储镜像并与主机和用户共享其内容。我们学习了如何部署 Docker Registry(社区版)以及 DTR,后者在企业环境中被推荐使用。根据是否使用云环境或本地安装,我们可以为注册表卷使用不同的存储解决方案。正如我们在第十三章,使用 DTR 实现企业级注册表中回顾的那样,对基于大层的镜像存储来说,对象存储是非常好的解决方案,这也是创建镜像最常见的方式。

  • 镜像可能会占用宿主机大量空间。我们应该注意这一点,并使用docker system df检查无用的容器和占用空间的镜像。我们应删除那些未被其他镜像作为层使用的悬空镜像。同时,我们还需要关注注册表中的空间。只保留必要的镜像,但记得验证哪些容器或应用程序会使用不同版本的旧镜像。我们在第二章,构建 Docker 镜像中学习了如何筛选这些信息。

  • 存储卷与镜像和容器存储有所不同。它们用于绕过容器存储。这些存储帮助我们在需要大量磁盘 I/O 时提升性能,并且允许我们存储持久化数据。默认情况下,我们可以使用内存文件系统、主机的本地目录(绑定挂载)、NFS 和 Docker 卷进行存储。Docker 卷在容器执行期间创建,并与容器的生命周期相关联。

  • 如前所述,Docker 默认提供了一些存储卷解决方案。我们可以通过插件和第三方集成扩展这些方案。使用 Docker Swarm 和 UCP 的分布式存储在我们需要为应用程序提供高可用性和弹性时至关重要。如果某个集群主机发生故障,默认情况下,另一个主机会接管其工作负载,但存储必须与此行为保持一致。

  • Kubernetes 对持久数据有不同的处理方式。我们讨论了卷和持久卷(persistentVolumes)。前者用于共享和管理与 Pod 容器相关的数据。另一方面,持久卷用于在集群范围内管理和持久化数据。存在不同的保留策略来管理它们的回收周期。持久卷声明(persistentVolumeClaims)用于通过标签和所需空间等参数将 Pod 与卷关联起来。因此,我们将不直接将持久卷附加到 Pod 上,而是在 Pod 的配置中使用 persistentVolumeClaims 作为卷。管理员应创建这些资源,但可以通过使用 storageClass 资源避免这种行为。他们只需通过使用标签、存储提供程序和其他高级配置来配置 storageClass 资源,从而允许对持久卷进行动态存储分配。

  • 我们了解到 Docker 提供了 ConfigSecret 对象来帮助我们管理集群节点中的信息。这些帮助我们配置应用程序,并确保应用程序的容器接收适当的配置、密码、证书等。Kubernetes 有自己的配置和机密资源。为了管理配置,我们将使用 ConfigMaps 来存储应用程序的配置文件并管理环境变量。Secret 资源用于存储受保护的数据,但在 Kubernetes 中默认并未加密。它们以 Base64 格式存储,并可用于存储密钥值对或文件。

存储数据和状态非常重要,并且是考试的一部分。让我们回顾一下你需要理解的概念,以便顺利通过考试。

考试所需的存储和卷知识。

考试将验证我们对以下主题的知识,其他内容也包括在内:

  • 确定与不同操作系统一起使用的正确图形驱动程序。

  • 描述并演示如何配置设备映射器。

  • 比较并对比对象存储和块存储,并确定何时应使用它们。

  • 描述应用程序如何由多个层组成,以及这些层在文件系统中的存储位置。

  • 描述如何使用 Docker 中的卷进行持久化存储。

  • 确定清理文件系统和 DTR 中未使用的镜像所需的步骤。

  • 描述并演示如何在集群节点之间使用存储。

  • 描述如何使用 persistentVolume 资源为 Kubernetes Pod 配置持久存储。

  • 描述 Kubernetes 中容器存储接口驱动程序、storageClasspersistentVolumeClaimvolume 对象之间的关系。

在下一章中,我们将查看一些最后的注意事项和示例考试题,以帮助我们准备 DCA 考试。

总结。

本章是通过考试所需主题的总结。我们回顾了各个主题的分布及其在考试中的大致分值。这应该能帮助你了解哪些部分比其他部分更为重要。我们建议你在阅读下一章中的所有模拟考试问题之前,先复习这一章。

我们概述了编排中最重要的主题。我们还回顾了一些安装和配置 Docker Engine、Docker Swarm 和企业组件所需的技巧。我们总结了创建镜像过程中涉及的功能和流程。安全性始终是至关重要的,我们还回顾了不同 Docker 组件提供的功能,这些功能帮助我们在生产环境中提供 CaaS 平台。我们还回顾了容器网络和不同的容器存储实现,以及数据管理的相关内容。建议你复习这些总结中不清楚的章节,并通过本书中提供的实验来巩固你对所有考试主题的知识。

下一章提供了一些模拟考试问题,帮助你为考试做好准备。

模拟考试题目和最终备注

本章提供了一些最终备注和模拟题。我们将简要总结考试规范,考试如何呈现给你,以及哪些主题比其他主题更为相关。

本书准备的模拟题与考试中将遇到的题目类似。请仔细阅读它们,因为其中一些是多项选择题。多项选择题将在考试中出现,你应该知道如何作答。

通过这些问题,你将对考试的格式、你将遇到的题目类型以及哪些主题更为相关有一个清晰的了解。

第二十章:Docker 认证工程师考试详情

撰写本书时,考试基于 Docker Enterprise 平台。请参考 Docker 官网,success.docker.com/certification,以获取最新的信息。

Docker 认证工程师考试将验证你在 Docker Enterprise 平台上的专业技能,通常需要至少 6 到 12 个月的使用经验。本书通过实验帮助你理解平台的概念和用法,教会你这些必备技能。

你可以在线支付并参加此考试,但它仅提供英文版本。尽管 Docker 网站显示结果会立即交付,但有时结果可能需要 24 到 48 小时才能交付。

考试包含 55 道题目,本书所涵盖的主题具有不同的权重。我们建议你使用 Docker Enterprise 平台的 30 天免费试用,因为会有很多关于 Docker Enterprise 组件和管理的考试问题。还建议你熟练掌握 Docker 的命令行操作和选项,包括如何获取和筛选有关 Docker 资源的信息。这些是撰写本书时的主题权重:

  • 编排:25%

  • 镜像创建、管理和注册:20%

  • 安装与配置:15%

  • 网络:15%

  • 安全性:15%

  • 存储与卷:10%

这可以帮助你了解每个主题的重要性和问题分布。在接下来的章节中,我们将提供一些模拟题,帮助你为考试做准备。这些题目模拟真实考试。注意,许多题目有多个正确答案,你应该选择所有正确答案。所有这些问题的答案可以在本书的评估章节找到。

模拟考试题目

  1. 如何限制提供给容器的 CPU 数量?

a) 使用--cap-add CPU

b) 使用--cpuset-cpus

c) 使用--cpus

d) 无法指定 CPU 的数量;我们必须使用--cpu-shares并定义 CPU 切片。

  1. 如何限制容器可用的内存量?

a) 无法限制容器可用的内存量。

b) 使用--cap-drop MEM

c) 使用--memory

d) 使用--memory-reservation

  1. 应该导出哪些环境变量才能开始使用受信环境与 Docker 客户端?

a) export DOCKER_TRUSTED_ENVIRONMENT=1

b) export DOCKER_CONTENT_TRUST=1

c) export DOCKER_TRUST=1

d) export DOCKER_TRUSTED=1

  1. 如何增加运行一个实例的服务的副本数量(标记所有正确答案)?

a) 这对于全局服务不可行。

b) 通过使用 docker service update --replicas <NUMBER_OF_REPLICAS> <SERVICE> 更新副本数量。

c) 可以通过使用 docker service scale <SERVICE>=<NUMBER_OF_REPLICAS> 增加副本数量。

d) 我们可以使用 docker service scale up 创建一个新的副本。

  1. 如果我们指定 node.role!=worker 限制,全局服务在节点上运行多少副本?

a) 工作节点和管理节点将运行一个副本。

b) 只有工作节点将运行一个副本。

c) 只有管理节点将运行一个副本。

d) 没有节点将运行任何副本。

  1. 如何停止当前执行三个副本的 web 服务器服务的所有副本?

a) 使用 docker service stop webserver

b) 使用 docker rm service webserver

c) 使用 docker service update --replicas 0 webserver

d) 以上答案均不正确。

  1. 如果我们通过 -P 在端口 8080 上发布服务,哪些节点将暴露端口 8080

a) 没有节点将暴露端口 8080 上的服务。

b) 所有节点将发布端口 8080

c) 我们应该使用特权容器来暴露端口 8080

d) 我们必须使用 --network=host 来发布 30000 以下的端口。

  1. 我们应该遵循什么步骤来移除集群中的领导节点?

a) 通过执行 docker node update --availability=drain <LEADER_NODE> 确保所有任务在其他节点上运行。

b) 通过在节点上执行 docker swarm leave,将节点从集群中移除作为领导者。

c) 将主节点降级为工作节点,然后在该节点上执行 docker swarm leave

d) 一旦节点离开集群,我们可以通过在任何可用的管理节点上执行 docker node rm <OLD_LEADER_NODE> 来完全移除该节点。

  1. 我们在哪里指定 DevOps 用户只能使用管理员组签名的镜像来运行容器?

a) 在 Universal Control Plane (UCP) 的 RBAC 中,我们允许 DevOps 用户运行他们的镜像。

b) 在 DTR 的镜像仓库中,我们为 DevOps 用户添加镜像拉取权限。

c) 镜像访问应该在 Docker Trusted Registry (DTR) 上配置,且 DevOps 用户至少应能够读取该仓库。在 UCP 上,我们仅允许集群中管理员组签名的镜像,并为 DevOps 用户的私有仓库至少添加调度器访问权限。

d) 这在 Docker 企业版中不可行。

  1. 访问使用自签名证书的安全仓库中存储的镜像需要执行什么步骤?

a) 我们可以在 Docker 引擎的 daemon.json 文件中将我们的仓库配置为“不安全”。

b) 我们应该禁用内容信任以允许从不安全的仓库拉取镜像。

c) 最佳选项是信任自签名证书。我们将把 DTR 创建的 证书颁发机构CA)添加到我们系统的受信任 CA 列表中。

d) 我们无法使用自签名证书,因此我们总是需要企业签名证书。

  1. 用户 A 执行了 docker service scale --replicas 5 webserver,而 用户 B 执行了 docker service update --replicas 3 webserver。在两者执行之后,将会运行多少个副本?

a) 这些命令都无法执行。

b) webserver 服务将运行三个副本。

c) webserver 服务将运行五个副本。

d) webserver 服务将运行八个副本。

  1. 哪一行命令会创建名为 DATA 的卷?

a) docker volume create --driver local DATA

b) docker create --volume DATA

c) 卷必须在容器执行时创建。

d) 选项中没有有效的选项。

  1. 如何确保在运行容器时使用软限制来保证最低的内存可用?

a) 我们无法确保为容器提供最低的内存。

b) 使用 --memory

c) 这必须在您的操作系统上进行配置。

d) 我们使用 --memory-reservation

  1. 关于 Swarm 网络,哪个说法是正确的?

a) 所有覆盖网络默认都是加密的。

b) 控制平面节点使用双向 TLS 加密来保护流量。

c) 内部 DNS 可以被外部消费并暴露其服务。

d) 以上所有选项都正确。

  1. 哪个概念将请求路由到已部署服务上运行的容器?

a) ingress 覆盖网络用于默认情况下通过轮询终端路由请求到不同服务的后端。

b) docker_gwbridge 网络用于与不同主机上的容器进行通信。

c) ingress 覆盖网络用于默认情况下通过服务的虚拟 IP 路由请求到不同服务的后端。

d) 我们必须使用主机网络来路由请求到容器。

  1. 关于签名镜像,以下哪些句子是正确的?

a) 镜像签名确保了镜像的所有权。

b) 可以使用 docker trust sign <IMAGE> 来签名镜像。

c) 如果我们在每个命令上设置 Docker 客户端使用 Docker 内容信任,则所有镜像将被签名。

d) 镜像签名基于以下密钥:所有者密钥、仓库密钥、快照密钥和时间戳。

  1. 如果我们有一个集群,其中节点本地有 myapplication 镜像,但镜像哈希不同,会发生什么?

a) 如果我们没有指定镜像哈希值,每个节点将使用自己的镜像运行任务。

b) 为了确保所有节点运行相同的镜像版本,我们需要为远程注册表使用 --with-registry-auth

c) 我们将在使用受信内容的节点上使用签名镜像和 Docker 引擎。

d) 无法确保节点使用正确的镜像版本。

  1. 以下哪个关于全局服务的说法是正确的?

a) 全局服务将只在每个节点上运行定义服务的一个副本。

b) 全局服务不会基于弹性提供高可用性。

c) 使节点排空不会移除全局服务。

d) 以上所有句子都是正确的。

  1. 以下哪些句子关于复制服务是正确的?

a) 我们需要在服务创建时指定实例的数量,因为之后无法更改。

b) 复制服务是默认的服务模式,如果未指定副本数量,它将运行一个实例。

c) 通过将副本数设置为0,可以停止复制服务。

d) 以上所有选项都正确。

  1. 创建 Swarm 备份需要哪些步骤?

a) 停止任何管理节点上的 Docker Engine,以确保文件保持静态。

b) 复制/var/lib/docker/swarm目录的内容以备份 Swarm。

c) Raft 日志应该与普通文件分开进行备份。

d) 以上所有步骤都是必需的。

  1. 你学到了什么关于 Swarm 网络的知识?

a) 覆盖网络使用 UDP VXLAN 隧道进行部署。

b) 默认情况下,所有服务副本都可以通过内部负载均衡器平等访问。

c) 内部 DNS 将允许所有在同一覆盖网络上运行的服务相互访问。

d) 以上所有选项都正确。

  1. 我们尝试创建一个五个副本的服务,但它无法工作。我们无法进入协调阶段,因为我们收到以下错误:1/1: no suitable node (scheduling constraints not satisfied on 5 nodes)。可能出了什么问题?

a) 服务的镜像不存在。

b) 我们使用的是私有镜像,但没有提供身份验证凭据。

c) 我们使用了约束条件来将服务的任务部署到特定节点,但没有一个节点具有所需的标签。

d) 以上所有选项都不正确。

  1. 以下哪项关于锁定 Docker Swarm 集群的说法是正确的?

a) 控制、管理和数据平面是安全的。

b) 解锁/var/lib/docker/swarm数据需要密码短语。

c) 执行systemctl restart docker将需要锁定密码短语。

d) 如果节点重启,Docker Engine 不会自动重启,因此我们会失去 Docker Swarm 的仲裁权。

  1. 以下哪项关于复制服务的说法是正确的?

a) 它们将在每个节点上运行一个实例。

b) 只有复制服务才能使用滚动更新功能进行升级。

c) 它们可以通过 Docker 服务更新命令停止:--replicas 0 <SERVICENAME>

d) 我们将使用 Go 模板以提供唯一资源,例如容器内部的卷或主机名,确保所有副本使用各自的资源。

  1. 以下哪些方法可以在 Docker Enterprise 上发布应用程序?

a) 我们可以使用 Interlock。

b) 我们可以使用 Ingress Controller。

c) 我们将发布每个应用程序容器。

d) 我们可以使用host模式将应用程序发布为好像它们直接在主机级别运行一样。

  1. 以下哪项关于 Kubernetes 与 Docker Enterprise 的集成是正确的?

a) Docker Enterprise 开箱即用提供 Kubernetes。

b) 我们必须选择在集群节点中使用哪个协调器,因为每次只能使用一个。

c) 我们可以以混合模式运行主机,以支持 Kubernetes 和 Docker Swarm 负载,尽管不推荐在生产环境中使用。

d) 我们可以使用常见的 Kubernetes 安装命令来升级 Kubernetes 组件。

  1. docker image importdocker image load 在将镜像上传到 Docker 主机时有什么区别?

a) 这些命令没有区别。

b) 两者导入相同的镜像内容。

c) docker image import 只会获取包含二进制文件、库和进程配置的镜像层,但不包含如何启动进程、使用哪些卷、应使用哪些端口等元信息。

d) 我们只能使用 docker image import 来创建新镜像。

  1. 如何让 docker build 避免使用缓存的镜像层?

a) Docker 将始终使用缓存的信息。无法避免使用镜像缓存。

b) 默认情况下,镜像缓存是禁用的,因此我们需要使用 --use-caching 来确保启用缓存,因为它会加速构建过程。

c) 为了避免镜像缓存,我们可以使用 --no-cache。这样,构建将不会使用任何先前保存的层。

d) 以上所有句子都是错误的。

  1. 如何从一个仓库中下载所有的镜像?

a) 不可能。我们需要列出所有镜像及其标签,并逐一下载。

b) 每次执行 docker image pull 时,我们都会下载所有的镜像及其层,无论是否打算使用它们。

c) 我们可以使用 docker image pull --all-tags 来检索所有与仓库相关的镜像。

d) 以上所有句子都不正确。

  1. 如何根据特定镜像筛选运行中的容器?

a) 没有这个选项。我们使用 Linux grep 命令过滤容器的特定基础镜像。

b) 我们将使用 ancestor 键列出所有使用特定镜像的运行容器。

c) 我们将使用 image 键列出所有使用特定镜像的运行容器。

d) 以上所有句子都不正确。

  1. 如何将本地构建的镜像推送到远程注册表?

a) 我们需要知道注册表的 完全限定域名 (FQDN) 或其 IP 地址。

b) 我们将镜像标记为注册表的 FQDN 或 IP、用户名或组名,以及镜像存储的仓库。

c) 如果注册表使用 TLS/SSL 证书,我们可以将其 CA 加载到系统中以信任它们,或者我们可以使用 insecure-registries 键来配置它。

d) 以上所有句子都正确。

  1. 哪个选项会将已创建的 DATA 卷绑定到容器内部的 /data 目录?

a) -v DATA:/data

b) --mount type=volume,source=DATA,target=/data

c) --mount DATA:/data

d) --volume type=volume,source=DATA,target=/data

  1. 如何将一个 Web 服务器容器暴露在主机的 80 端口(nginx:alpine 镜像暴露了 80 端口)?

a) docker container run --cap-add NET_ADMIN -p 80:80 -d nginx:alpine

b) docker container run --net=host -d nginx:alpine

c) docker container run -P nginx:alpine

d) docker container run -d -P 80:80 nginx:alpine

  1. 哪些密钥在签署镜像时需要密码短语来解锁?

a) 时间戳

b) 目标

c) 快照

d) 根用户

  1. 什么是 Docker 捆绑包,里面包含哪些内容?

a) Docker 包含客户端二进制文件和管理员配置的捆绑包。

b) 所有用户都有自己的 Docker 捆绑包,并且其中包括用户所需的所有环境文件。

c) Docker 捆绑包只包含环境脚本,我们将向管理员请求证书。

d) 用户的 Docker 捆绑包包括使用 CaaS 平台所需的所有环境文件和证书。

  1. 如果我们必须部署一个具有七个管理节点和分布式高可用性的集群,最佳的节点分布是什么?

a) 一个数据中心有四个管理节点,另一个数据中心有三个管理节点。

b) 一个数据中心有两个管理节点,第二个数据中心有两个管理节点,第三个数据中心有三个管理节点。

c) 所有管理节点应该位于同一个数据中心。

d) 我们无法通过 7 个节点来管理分布式可用性;至少需要 9 个节点。

  1. 哪个概念负责管理 Docker Swarm 服务的外部到内部负载均衡?

a) 路由器网状结构

b) 入口控制器

c) nodePort

d) clusterIP

  1. COPYADD Dockerfile 指令之间有什么区别?

a) COPY 以只读模式添加文件。

b) COPY 可以用于从外部服务下载文件。

c) ADD 可以与打包和压缩的文件一起使用,并会在层的根文件系统中解压缩。

d) ADDCOPY 完全相同,但 ADD 是更新的指令。

  1. 如何使用相同的 docker-compose.yaml 文件部署两个应用程序?

a) 我们无法使用相同的 docker-compose.yaml 文件部署两个应用程序。

b) Docker Compose 可以使用项目来部署两个应用程序,确保应用程序在不同的卷和端口上运行。

c) 我们可以使用环境变量来固定资源,以避免资源使用冲突。

d) 避免应用组件冲突的唯一选项是将应用程序部署到不同的集群中。

  1. 部署 DTR 需要什么?

a) Docker Hub 上的 Docker Enterprise 许可证和适当的仓库 URL

b) Docker Enterprise Engine 和 Docker UCP

c) Docker Engine、DTR 许可证和 Docker Content Trust。

d) 所有前述选项

  1. 如何在 Docker Engine 上启用调试?

a) 通过使用 -D 参数启动 Docker 守护进程来启用调试。

b) 通过在 config.json 文件中将 debug 键设置为 true 来启用调试。

c) 通过在 daemon.json 中启用实验功能。

d) 无前述选项

  1. 如何只列出由 alpine:3.10 镜像创建的容器?

a) docker container ls image=alpine:3.10 b) docker ps --format ancestor=alpine:3.10 c) docker container ls --filter ancestor=alpine:3.10 d) docker container ls --filter image=alpine:3.10

  1. 以下关于特权容器哪个是正确的?

a) 资源限制将被避免(CPU、内存和磁盘 I/O)。

b) 它们总是以 root 用户身份运行容器。

c) 这些容器将以所有可用的能力运行。

d) 它们使用主机的内核命名空间运行。

  1. 以下关于 Swarm 加入令牌哪个是正确的?

a) 一旦创建,我们必须将其存储在安全的地方,因为它们是不可恢复的。

b) 我们可以通过 docker swarm join-token recreate 生成新的令牌,以便为新节点获取新的值,如果我们丢失了它们。

c) 我们可以通过 docker swarm join-token 在需要时恢复它们。

d) 一旦重新生成,加入令牌将在所有节点上自动更新。

  1. 用于验证 DTR 和 UCP 节点健康状态的端点有哪些?

a) DTR 提供 /_ping/nginx_status/api/v0/meta/cluster_status

b) DTR 和 UCP 提供 /status

c) DTR 和 UCP 提供 /_ping

d) DTR 提供 /status,UCP 提供 /_ping

  1. 哪个命令可以帮助我们查看并恢复因“悬挂镜像”和死容器而丢失的空间?

a) docker system rm b) docker system prune c) docker image rm --filter="dangling" d) docker container rm -a

  1. 哪个基本组合创建的命令行最终将在容器内运行?

a) ENTRYPOINT 会设置要启动的脚本或二进制文件,而 CMD 仅在 ENTRYPOINT 未定义时使用。

b) CMD 总是覆盖 ENTRYPOINT 定义。

c) 使用 ENTRYPOINT 来定义要启动的脚本或二进制文件,并将 RUN 作为参数。

d) 仅当 ENTRYPOINT 使用 exec 格式配置时,CMD 才会将参数添加到定义的 ENTRYPOINT 中。

  1. 以下关于密钥哪个是正确的?

a) 它们只会在管理节点上可用,因此带有密钥的工作负载必须运行在这些节点上。

b) 它们是短暂的,并部署在内存文件系统上。

c) 即使是管理员,它们也是加密的,因此不能从控制平面恢复。

d) 如果我们需要更改密钥,我们需要创建一个新的密钥并使用这个新的密钥更新服务。

  1. 如何确保在生产环境中部署特定镜像?

a) 通过使用镜像的哈希值来部署容器。

b) 签名镜像将确保其标签和来源。

c) 指定正确的标签足以确保其内容。

d) 通过使用 docker image history 来查看生成镜像时使用的命令。

  1. 以下关于容器隔离的句子哪些是正确的?

a) 主机的硬件资源,如内存和 CPU,是通过 cgroups 授权的。

b) 为了确保容器的限制,我们需要使用操作系统安全模块。

c) 只有 root 用户被允许部署无限资源的容器。

d) 特权容器将绕过定义的进程能力和执行用户。

  1. 我们如何确保镜像内容的不可变性?

a) 通过使用签名镜像。

b) 在 DTR 中定义不可变标签。

c) 通过使用镜像扫描。

d) 镜像不能是不可变的。

  1. 以下关于叠加网络的哪些句子是正确的?

a) DTR 部署了一个叠加网络dtr-ol,用于路由集群的内部通信。

b) 当没有任务连接到管理节点时,叠加定义的网络只存在于管理节点上。

c) interlock-extension连接到服务定义的网络,将请求路由到适当的后端。

d) Docker Swarm 叠加网络是加密的,并使用 VXLAN 部署。

  1. HEALTHCHECK --start-period=15s CMD curl --fail https://localhost:8080 | exit 1这行在 Dockerfile 中的作用是什么?

a) 它将每 15 秒执行一次定义的curl命令,如果连续三次失败,它将标记容器为不健康。

b) 它将在第一次执行时等待 15 秒,然后 Docker 引擎将每 30 秒运行一次定义的curl命令,如果连续三次失败,它将标记容器为不健康。

c) 这一行没有任何作用;健康检查必须为每个容器单独配置。

d) Docker 引擎将每 15 秒运行此探测程序,如果失败,它将重新启动容器。

  1. 以下关于 Docker Engine 访问的哪些说法是正确的?

a) 默认情况下,只有 Docker 套接字的所有者才被允许在独立主机上运行容器。

b) 我们可以允许用户运行容器,允许他们访问 Docker Engine 的 Unix 套接字或 API 的 TCP 端口(默认启用)。

c) 任何被允许登录到主机的人也被允许运行容器。

d) 只有 root 用户被允许在主机上运行特权容器。

  1. 我们如何修改已部署服务的发布端口?

a) 这是不可能的;我们必须删除服务并重新创建它。

b) 只有在服务使用主机网络(--net=host)运行时,我们才能更改端口。

c) 我们使用docker service update --publish-add <NEW_PORT> --publish-rm <OLD_PORT>

d) 以上选项都不正确。

  1. 如何确保 Web 服务器 Docker 服务在所有集群节点上运行一个 NGINX 实例?

a) 通过使用docker service create --type=global --instances=1 --name=webserver --image=nginx:alpine

b) 通过使用docker service create --mode=global --name=webserver nginx:alpine。c) UCP 可以确保管理员在所有节点上运行任何服务,并勾选allow run on manager nodes

d) docker service create --name=webserver --image=nginx:alpine足以在所有节点上执行一个实例。

  1. 我们如何设置一个名为myregistry/myorganization/baseimages的仓库,让内部用户可以访问,并且镜像由 DevOps 组用户拥有和管理?

a) 我们需要创建一个名为myorganization的组织。

b) 我们将创建 myregistry/myorganization/baseimages 作为公共仓库。

c) 我们将 DevOps 团队用户配置为 myregistry/myorganization/baseimages 仓库的管理员。

d) myregistry/myorganization/baseimages 仓库将作为私有仓库为 myorganization 用户创建。

  1. 我们如何查看名为 webserver 的容器发布的端口?

a) 使用 docker container ls --filter name=webserver

b) 使用 docker container port webserver

c) 使用 docker container inspect webserver --format="{{ .NetworkSettings.Ports }}"

d) 以上所有答案都是正确的。

  1. 哪个概念负责管理 Kubernetes 的内部负载均衡?

a) Router Mesh

b) Ingress 控制器

c) Interlock

d) clusterIP

  1. 哪些资源用于将 Pod 与 Kubernetes 定义的卷链接?

a) persistentVolume

b) persistentVolumeClaim

c) storageClass

d) persistentDataVolume

  1. 部署服务时需要哪些标签?

a) com.docker.lb.port

b) com.docker.interlock.port

c) com.docker.interlock.hosts

d) com.docker.lb.backend

  1. 我们如何在 Kubernetes 中将服务暴露到外部?

a) 使用 Interlock

b) 使用 Ingress 控制器

c) 使用 nodePort 服务

d) 使用 clusterIP 资源

  1. 我们如何知道系统中容器和卷使用了多少空间?

a) 通过使用 docker system prune

b) 通过使用 docker system df

c) 使用 docker container df

d) 通过使用 docker volume df

  1. DBA 团队在 UCP 中应该设置什么角色,以允许他们创建自己的卷?

a) Scheduler

b) 仅查看.

c) 只有 UCP 管理员可以创建卷和其他集群资源。

d) Restricted Control 足以在他们的私有集合中创建卷。

  1. 应该使用哪个标志来配置所有可用的 FQDN 用于 UCP?

a) --san

b) --external-name

c) --external-url

d) --ucp-url

  1. 用户无法推送镜像到我们的 DTR 内部仓库。我们应该验证什么?

a) Docker 登录访问。

b) DTR CA 证书应该被信任。

c) 我们应该验证镜像的仓库是否存在。

d) 显示镜像存在漏洞,且 DTR 的镜像扫描拒绝了用户的镜像。

  1. DTR 部署了哪些内部网络?

a) docker_gwbridge

b) dtr-ol

c) ingress

d) dtr-internal

  1. 哪些 Kubernetes 资源提供应用程序的弹性?

a) Deployment

b) ReplicaSet

c) Pod

d) Replicated

  1. docker swarm --force-new-cluster 命令的作用是什么?

a) 它用于在故障情况下恢复集群。它将所有管理节点设置为工作节点,只保留一个管理节点。

b) 此命令将销毁集群,用于移除整个集群。

c) 应该使用 --force-new-cluster 停止部署在工作节点上的所有服务。

d) 应用程序的服务不会受到此命令的影响。此命令仅影响控制平面。

  1. 关于 Docker Swarm 和 Kubernetes 网络的哪些说法是正确的?

a) 默认情况下,Ingress 覆盖网络将被加密。

b) 双向 TLS 通信确保 Docker Swarm 中控制平面的安全。

c) Kubernetes 网络通过networkPolicy资源默认实现隔离。

d) Kubernetes 使用证书来确保用户访问和内部控制平面的安全性。

对于一些模拟问题,可能有多个正确答案。

总结

这是我们成为 Docker 认证助理的旅程中的最后一章。我们回顾了前几章所学习的所有内容,并通过一些模拟考试问题进行了复习。如前所述,这些问题中的一些是旧考试中出现的真实问题。

成为 Docker 认证助理的旅程并不容易。我们从头开始学习本书,理解为什么在谈到微服务架构时,容器变得如此受欢迎。接着,我们描述了如何使用 Docker 引擎执行容器,结合各种隔离策略以确保容器内进程之间的安全性。由于容器是基于镜像的,我们学习了如何使用 Docker 工具来构建和维护这些镜像。我们了解了不同的 Docker 对象,并学习了如何使用不同的网络方法、存储卷或安全策略来部署应用程序。我们学习了如何部署微服务应用,其中每个组件都在容器中运行。我们学习了在独立环境和集群分布式环境中运行所有组件的区别。编排器在分布式环境中是关键。它们保持应用程序的健康,并帮助我们更快地开发微服务,提供无中断服务更新应用程序组件的功能。我们学习了 Docker Swarm 和 Kubernetes,因为它们都是 Docker 企业平台的一部分。最后,我们介绍了 Docker 的企业级 CaaS 平台——Docker Enterprise。我们了解了它的所有组件和功能,以及它们如何帮助我们部署应用程序并提高其安全性。

这是本书所涵盖所有主题的简要总结。如果你按照此流程进行学习,你已经准备好参加 Docker 认证助理考试了。本书还应作为命令参考,尽管我们知道技术变化非常迅速。现在你已准备好参加考试,深呼吸并在prod.examity.com/docker预约考试。祝你好运!

评估

第一章

  1. b 和 c: 我们可以在每个容器中运行多个进程,但不推荐这样做,因为 Docker 引擎只会管理主容器进程。我们需要在进程之间管理额外的逻辑,才能一次性启动和停止所有进程。这并不容易,可能会在主机上留下“僵尸”进程。微服务基于每个应用组件的最小功能,这与容器非常契合。

  2. b: 控制组(cgroups)将管理提供给每个容器的主机资源,但非常重要的一点是,默认情况下,容器会以无限制的资源运行。

  3. a 和 b: 容器默认会以 root 用户身份运行,除非源镜像定义了非 root 用户,或者我们在创建容器时指定了非 root 用户。用户命名空间允许我们在容器内使用 root 权限,尽管容器外的真实用户可以拥有非 root 用户 ID。这在进程需要 UID 0 才能工作时非常有用。

  4. d: 以上所有句子都是正确的:Windows 主机将运行两种不同类型的隔离。我们可以在 Windows 上运行 Linux 容器,但反过来则不成立。

  5. a、b 和 c: 我们可以使用 systemd 单元文件或 /etc/docker/daemon.json 来配置 Linux 上的 Docker 守护进程。在 Windows 主机上,daemon.json 位于 %programdata%\docker\config\ 目录。在这两种情况下,Docker 守护进程的远程访问默认不安全。

第二章

  1. b: 镜像 ID 是列出或管理镜像时唯一的标识。我们可以有一个 ID,包含多个名称,包括注册表部分和标签。

  2. d: 所有描述的方法都是有效的。

  3. b: 使用 Dockerfile 是一种可重现的方法,因为我们会描述所有添加软件、执行命令、添加文件等操作,以构建一个新的镜像。我们可以通过自动化和使用模板来构建镜像,这也是首选的方法。

  4. a 和 c: 只有 RUNCMDENTRYPOINT 指令支持 shell 和 exec 格式。

  5. a: 使用 shell 格式时,容器的主进程(由 ENTRYPOINT 键定义)无法通过参数进行修改。

第三章

  1. a 和 c: build 仅适用于镜像对象,destroy 对任何对象都不存在。

  2. b: 这不正确。Docker 守护进程会等待一定的时间(默认是 10 秒),然后才会向容器的主进程发送 SIGKILL 信号。

  3. b: docker kill 会立即向容器的主进程发送 SIGKILL 信号。如果进程是在后台执行的,比如在容器内部,并非所有进程都会被杀死。如果这些进程没有父子依赖关系,它们可能会变成僵尸进程。如我们所学,容器必须手动删除,docker kill 并不会删除它们。

  4. b: docker container update 仅会更改容器的重启策略及其对主机资源的访问权限。

  5. c: 我们已经启动了一个特权容器,因此不会应用资源限制,尽管我们使用了 -memory 来限制内存使用。特权模式不会影响文件系统。它只会修改主进程的行为,但在这种情况下,我们使用了非 root 用户在一个由 root 拥有的目录中创建新文件,结果导致文件无法创建。

第四章

  1. c: 每个容器将使用其自己的文件系统,除非我们为它们声明共享卷。

  2. a、b 和 c: 有不同类型的卷,并且不仅仅在容器创建或执行时允许使用。

  3. b: Docker 卷可以通过 --volumes(或 -v)选项与其关联的容器一起删除。执行 Docker 卷清理将删除所有未使用的卷;即那些没有与任何容器关联的卷。但 Docker 永远不会删除绑定挂载卷的内容(即挂载到容器上的本地目录)。

  4. c: 只有自定义的桥接网络可以在容器创建后附加。如果我们创建或启动一个容器,并希望它连接到默认的桥接网络,我们需要重新创建它并在容器创建时将其连接到该网络。

  5. b: 使用 --publish-all-P 将把 3276865535 之间的随机端口关联到容器的内部端口 80。Docker 守护进程将自动创建一个 NAT 规则以允许这种通信。你可以禁用 Docker 守护进程的 iptables 管理,但默认是启用的。

第五章

  1. a: Docker Compose 会在单一主机上运行所有应用组件。我们还将使用 docker-compose 文件来部署通过 Swarm 协调的应用程序,这些应用程序的组件分布在不同的主机上,但这需要一个正在运行的集群。在这种情况下,我们不会使用 docker-compose 二进制文件来部署应用程序;只有定义文件有效,我们将使用 docker stack 操作。 在 Docker Swarm 中,我们部署的是 Swarm 服务,而不是容器。

  2. d: Docker Compose 提供了所有所需的操作来构建、共享和部署多容器应用程序。

  3. a 和 c: Docker Compose 会检查项目镜像是否存在于主机中。如果没有,Docker 守护进程将尝试下载所有缺失的镜像。一旦 Docker 守护进程拥有所有所需的镜像,它将启动所有项目容器,除非使用 --detach-d 参数,否则我们的终端将附加到容器的标准输出和错误输出。

  4. a: Docker Compose 将允许我们扩展与某个服务关联的容器数量。默认情况下,Docker Compose 会为我们的部署创建一个桥接网络,因此一个内部 DNS 将会关联并管理所有应用程序的 IP 地址和名称。在扩展服务中,每次我们请求已定义的服务名称时,我们将收到一个副本的 IP 地址。它使用轮询 DNS 解析。

  5. d: 在这种情况下,我们可以说答案 c 几乎正确,但它不完整。Docker Compose 将删除所有容器。如果容器正在运行,它们将在删除之前被停止。所有在应用执行过程中创建的相关资源也将被移除。

第六章

  1. c: Docker 内容信任基于更新框架TUF),该框架旨在通过使用不同的密钥确保在更新之间发布内容。可以使用 TUF 验证包或任何其他内容的可信度。

  2. a 和 d: Docker 内容信任将使用 Root、Targets、Snapshot 和 Timestamp 密钥来确保内容。

  3. c: 我们使用内容信任来确保镜像的新鲜度,但确实无法确保在给定仓库中标记为“latest”的镜像实际上是最新创建的镜像。我们只能确保将使用标记为“latest”的镜像。始终建议使用标签,避免使用“latest”。

  4. b: 我们尝试签署一个非公开的写仓库版本。我们不被允许修改 docker.io 上的根仓库。

  5. d: 如果我们有备份,可以恢复密钥。如果不可能,我们可以生成一个新的密钥,或者让 Docker 在首次签署时为我们生成一个新的密钥。尽管在生成新密钥后我们能够签署镜像,但所有先前签署的镜像将被视为不受信任,因为我们更改了签名。

第七章

  1. a: 编排器将不了解你的应用逻辑。另一方面,我们已经快速审查了使用编排的接口,以确保容器在分布式环境中获得适当的数据量。

  2. c: 编排器不会管理应用数据,也不了解你的应用逻辑。编排器将负责应用组件的健康状况,如果某个必需的实例死亡,它会启动一个新的实例。

  3. a 和 b: 分布式环境将帮助我们以更高的可用性和更好的性能部署应用程序。但另一方面,我们将面临新的挑战,因为我们需要能够在不同节点上分布应用逻辑和组件的交互。

  4. a 和 b: 答案 a 和 b 是正确的,而 c 是错误的,因为应用组件可以逐一管理。因此,如果应用逻辑知道如何管理情况,升级只会影响一个应用组件。

  5. c: 所有句子都是正确的。我们了解到,可以定义容器限制和所需资源。编排器会审查这些规格,并将在资源充足的节点上部署它们,以确保其正确执行。我们可以引导编排选择带有标签的节点,例如,以确保应用程序的磁盘 I/O,以及许多其他功能。每个编排器将管理不同的规则和工作流,以选择最适合每个工作负载的节点。

第八章

  1. a: Docker Swarm 是内置于 Docker 引擎中的,但我们需要启用 Swarm 模式才能使其工作。我们可以部署其他编排器,如 Kubernetes,但这将需要额外的工作来部署它们。编排器使我们能够在集群上部署应用程序,因此 Swarm 将部署分布式应用程序。

  2. d: Docker Swarm 通过 DNS 提供服务发现,为服务及其任务提供内部负载均衡,并为分布在不同节点上的服务和容器提供覆盖网络。

  3. b: 每个集群只有一个领导节点。领导节点是从可用的管理节点中选出的。当我们初始化集群时,第一个节点将是领导节点,直到需要进行新的选举为止。除非我们通过服务约束特别避免它们,否则所有管理节点都会运行工作负载。

  4. d: 角色可以根据需要进行更改,例如进行维护时。我们需要始终保持定义的奇数个管理节点,以避免集群不稳定。

  5. a 和 b: 默认情况下,Docker Swarm 将在其自身的网络上部署堆栈,除非另行指定。与待部署应用程序相关的所有内容必须在 Docker Compose 堆栈文件中进行配置。我们可以添加外部创建的组件,但它们必须在堆栈部署之前就存在,并且我们将它们设置为外部组件,在基础设施即代码的堆栈文件中进行配置。

第九章

  1. a 和 b: Kubernetes 需要 etcd 才能工作。大多数 Kubernetes 部署解决方案会为你部署 etcd,但它是一个外部应用程序,因此由你来管理并确保该键值存储解决方案提供高可用性。Kubernetes 的内部网络开箱即用,但部署在不同主机上的组件之间的通信依赖于外部插件(CNI 标准)。因此,我们需要选择并部署一个解决方案来提供这种通信。

  2. b 和 c: 我们将在 Kubernetes 中部署 pods,因此它们是最小的部署单元。我们可以在一个 pod 中部署多个容器。Kubernetes 中的容器密度较高。扩展 pod 时,将一次性复制所有组件。

  3. d: 所有句子都正确。一个 pod 中的所有容器共享相同的 IP 地址和 localhost,它们也共享 pod 卷。在同一主机上运行的 pod 之间的连接不需要容器网络接口CNI)。它们都可以通过虚拟 IP 地址进行访问。

  4. a、b 和 c: ReplicaSets 使我们能够管理复制的环境。部署将创建 ReplicaSets,并允许我们向上或向下扩展应用程序的 pods。它们还将根据所需的运行 pods 来维持应用程序的健康状态。DaemonSets 将确保每个集群节点上有一个副本。

  5. a: ClusterIP 服务类型仅提供对服务的内部访问。分配的 IP 地址无法从集群节点访问。

第十章

  1. c: Docker Machine 由 Docker 社区维护。

  2. a:Docker Enterprise 提供了一个受支持的企业级 CaaS 平台,支持 Kubernetes、通用控制平面UCP)和 Docker 受信注册表DTR,基于 Docker Registry 社区)。即使使用 Docker Enterprise Engine,我们也可以在生产环境中部署 Docker Swarm 并获得 Docker 的支持。

  3. a、b 和 d:Docker Enterprise 在我们部署 UCP 时提供 Kubernetes 开箱即用——我们不需要手动安装 Kubernetes。

  4. d:我们将为所有组件使用固定 IP 地址。我们将使用外部负载均衡器将流量转发到所有管理节点的 UCP 和所有运行 DTR 的工作节点的注册表。如果流量只转发到一个节点,则在该节点出现故障时将无法提供高可用性。Docker UCP 安装将默认为 Kubernetes 部署 Calico,但我们需要查看 pod-cidrservice-cidr,确保默认定义的子网在我们的环境中有效。

  5. c:为了提供工作负载的高可用性,我们将部署至少两个 Linux 节点。虽然可以在 UCP 管理节点上运行 DTR,但不推荐这样做,因为管理节点需要足够的资源来执行控制平面任务,而镜像扫描可能会影响集群的稳定性。我们还需要为应用程序的前端选择不同的端口,因为两者都使用端口 443

第十一章

  1. b:正如我们所学,Docker Enterprise Engine 是安装 UCP 所必需的,它不会为我们自动安装。我们可以使用 Web UI、UCP 包和 UCP API 来管理我们的工作负载和集群配置。UCP 的 RBAC 系统将管理授权,但如果没有配置外部授权源或外部授权源不可用,它也会认证用户。

  2. b 和 d:Docker 提供了一个完整的 UCP 备份和恢复解决方案,使用 docker/ucp 镜像,但请记住,我们需要关注 Docker Swarm 的文件系统,因为它不包含在 UCP 的备份中。我们应根据我们的环境使用合适的 docker/ucp 镜像版本。实际上,除了升级以外,我们将使用相同的安装版本执行其他任何操作。可以通过 docker/ucp 镜像执行 UCP 卸载,这将从集群中所有节点移除 UCP 组件。然后,我们应该移除 docker/ucp 镜像。升级过程可以使用 docker/ucp 镜像自动完成,但这可能会影响用户。我们通常会先自动升级 UCP 管理节点,然后在工作节点上手动执行升级步骤。

  3. a 和 b:我们可以使用 --controller-port-kube-apiserver-port 来修改 UCP 控制器和 Kubernetes 的 API 服务器端口。我们还可以通过在多宿主机上选择不同的接口,使用 --data-path-addr 来隔离控制平面和数据平面。主题别名名称 (SANs) 将为 UCP 的证书添加别名。我们可以通过多次使用 --san 来为我们的环境添加所有需要的别名。

  4. a、c 和 d:UCP 在 Docker Swarm 上部署一个具有高可用性的 Kubernetes 集群。由于 UCP 部署在 Docker Swarm 上,我们至少需要三个节点来提供高可用性。所有管理节点将运行相同的控制平面进程,并且需要一个外部负载均衡器来分配它们之间的访问。这需要透明代理配置,以允许管理节点管理加密通信。我们将使用 /_ping 端点来验证管理节点的健康状况,并可以在负载均衡器上用作后端健康检查。

  5. a 和 c:UCP 提供 NoneView OnlyRestricted ControlSchedulerFull Control。我们可以创建新的角色,但默认情况下没有特权或管理员角色。Docker Enterprise 管理员并不作为角色定义。用户属性中有一个复选框可以启用此功能。只有管理员才能创建授权、用户、团队、组织和集合。

第十二章

  1. b 和 c:始终需要两个标签。我们需要确保 Interlock 使用 com.docker.lb.hostscom.docker.lb.port 转发服务的请求。这些标签将包含所有必要的信息,但如果服务的实例连接到多个网络,推荐并要求使用 com.docker.lb.network。我们需要指定应该使用哪个网络作为入口。

  2. b:Interlock 解决方案基于一个名为 interlock 的主进程,负责管理外部代理服务和配置,还有一个 interlock-proxy 服务,如果没有指定外部负载均衡器,该服务将在 Docker Enterprise 环境内运行。这三个进程作为服务在 Docker Swarm 中运行,并且它们的前缀是 ucp-ucp-interlock-controller 并不存在。

  3. d:默认情况下,只有 ucp-interlock 服务会根据节点的角色来定位。所有其他组件可以在任何地方运行。我们将使用位置约束在工作节点上运行 ucp-interlock-proxyucp-interlock-extension 组件。

  4. a、b 和 c:Interlock 允许我们管理 ucp-interlock-proxy 上的 SSL/TLS 隧道,或将其配置为透明代理。在这种情况下,我们的服务后端应该管理 SSL/TLS 证书。Interlock 与 Docker API 进行交互,所有更改将自动更新到 Interlock 的代理组件。Interlock 是一个七层负载均衡器;反向代理、TCP 和 UDP 协议应该通过路由网格或主机模式发布。

  5. a 和 b:Ingress 控制器和 Interlock 具有相同的逻辑,使用少数公开端口。它们将使用负载均衡和反向代理功能来管理所有的入口流量。我们不会直接发布应用程序。没有应用程序的服务需要直接暴露。Ingress 控制器(和 Interlock)将被暴露,并且它们将把请求路由到应用程序定义的服务。Interlock 需要与应用程序的服务交互,因此它必须连接到这些服务的网络。这将自动发生。Docker Enterprise 将把 interlock-proxy 服务连接到我们的应用程序网络。

第十三章

  1. b:此列表仅显示一个有效功能。DTR 提供仓库镜像。既不提供仓库负载均衡,也不提供仓库签名功能。我们不对仓库进行签名。我们对仓库的镜像/标签进行签名。

  2. b:DTR 不管理镜像的数据高可用性。部署多个副本将为 DTR 的进程提供高可用性。DTR 的复制需要副本之间的数据共享,但我们必须包含第三方解决方案来为我们的存储提供高可用性。

  3. a 和 b:DTR 安装会运行 dtr-garantdtr-jobrunner 容器。第一个容器将管理用户身份验证,而 jobrunner 将执行 DTR 的维护任务,以删除未引用的层。dtr-notary-serverdtr-notary-signer 将部署在 DTR 中,以管理 Docker 内容信任元数据。

  4. d:所有问题的句子都描述了部署具有高可用性的 DTR 所需的步骤。

  5. a:DTR 备份不包括镜像的层。这可能包含大量数据,而这些数据是恢复镜像的关键。你应该为这些数据准备第三方解决方案。另一方面,仓库元数据、RBAC 配置和镜像的签名将存储在你的备份 TAR 文件中。

考试答案

1 - b 和 c

2 - c

3 - b

4 - a、b 和 c

5 - b

6 - c

7 - a

8 - a、c 和 d

9 - c

10 - a 和 c

11 - b

12 - a

13 - d

14 - b

15 - c

16 - a、b 和 c

17 - a、b 和 c

18 - a

19 - b 和 c

20 - a 和 b

21 - d

22 - c

23 - b、c 和 d

24 - c 和 d

25 - a、c 和 d

26 - a 和 c

27 - c

28 - c

29 - c

30 - b

31 - d

32 - a 和 b

33 - d

34 - b 和 c

35 - d

36 - b

37 - a

38 - c

39 - c

40 - a 和 b

41 - a 和 b

42 - b 和 c

43 - a 和 c

44 - c

45 - a 和 c

46 - b

47 - d

48 - b、c 和 d

49 - a 和 b

50 - a

51 - a

52 - a、b 和 c

53 - b

54 - a

55 - c

56 - b

57 - c 和 d

58 - d

59 - d

60 - b

61 - a

62 - b 和 c

63 - b

64 - b

65 - a

66 - a、b 和 c

67 - b

68 - a 和 b

69 - a

70 - b 和 d

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