Python-Docker-实践教程-全-

Python Docker 实践教程(全)

原文:Practical Docker with Python

协议:CC BY-NC-SA 4.0

一、容器化介绍

本章介绍 Docker 是什么,容器化是什么,它与虚拟化有什么不同。其他涉及的副题包括容器化的历史、容器运行时间和容器编排。

Docker 是什么?

为了回答这个问题,我们需要澄清“Docker”这个词,因为 Docker 已经成为容器的同义词。

Docker Inc .是 Docker 背后的公司。它由所罗门·海克斯于 2010 年创立,名为 dotCloud Inc .dotCloud 工程师为 Linux 容器构建了抽象和工具,并使用了 Linux 内核特性cgroups和名称空间,目的是降低使用 Linux 容器的复杂性。dotCloud 将其工具开源,并将重点从平台即服务(PaaS)业务转向容器化。Docker Inc .将 dotCloud 出售给 cloudControl,后者最终申请破产。

Docker 是提供操作系统级虚拟化的技术,称为容器。需要注意的是,这不同于硬件虚拟化。我们将在本章的后面探讨这一点。Docker 使用 Linux 内核的资源隔离特性,如cgroups、内核名称空间和 OverlayFS,所有这些都在同一个物理或虚拟机内。OverlayFS 是一个支持 union 的文件系统,它将几个文件和目录合并成一个文件和目录,以便在同一个物理机或虚拟机中运行多个相互隔离和包含的应用。

了解 Docker 解决的问题

在很长一段时间里,设置开发人员的工作站对于系统管理员来说是一项非常麻烦的任务。即使开发人员工具的安装完全自动化,当您混合了不同的操作系统、不同版本的操作系统以及不同版本的库和编程语言时,建立一个一致并提供统一体验的工作空间几乎是不可能的。Docker 通过减少移动部件解决了这个问题。现在的目标不再是操作系统和编程版本,而是 Docker 引擎和运行时。Docker 引擎提供了底层系统的统一抽象,使得开发人员可以非常容易地测试他们的代码。

生产领域的事情变得更加复杂。假设您有一个 Python web 应用,运行在 Amazon Web Services EC2 实例上的 Python 2.7 上。为了使代码库现代化,该应用进行了一些重大升级,包括对 Python 版的更改。假设当前运行现有代码库的 Linux 发行版所提供的包中没有这个版本的 Python。要部署这个新应用,您可以选择以下任一选项:

  • 替换现有实例

  • 通过以下方式设置 Python 解释器

    • 将 Linux 发行版本更改为包含较新 Python 包的版本。

    • 添加第三方渠道,提供较新 Python 版本的打包版本。

    • 进行就地升级,保留现有版本的 Linux 发行版。

    • 从源代码编译 Python 3.5,这带来了额外的依赖性。

    • 或者使用类似于virtualenv的东西,它有自己的一套权衡。

无论从哪个角度看,应用代码的新版本部署都会带来很多不确定性。作为操作工程师,限制对配置的更改至关重要。考虑到操作系统的变化、Python 版本的变化和应用代码的变化,会产生很多不确定性。

Docker 通过显著减少不确定性的表面积来解决这个问题。您的应用正在现代化?没问题。用新的应用代码和依赖项构建一个新的容器,并交付它。现有的基础设施保持不变。如果应用的行为不符合预期,那么回滚就像重新部署旧容器一样简单——将所有生成的 Docker 映像存储在 Docker 注册表中并不少见。拥有一种简单的回滚方法而不干扰当前的基础架构,可以大大减少响应故障所需的时间。

多年来的容器化

虽然容器化在过去的几年里流行起来,但容器化的概念实际上可以追溯到 20 世纪 70 年代。

1979 年:克鲁特

系统调用chroot是在 1979 年的 UNIX 版本 7 中引入的。chroot的前提是它为当前运行的进程及其子进程改变了明显的根目录。在chroot中启动的进程不能访问指定目录树之外的文件。这种环境被称为 chroot 监狱。

2000 年:自由监狱

chroot概念的基础上,FreeBSD 增加了对一个特性的支持,该特性允许将 FreeBSD 系统划分成几个独立、隔离的系统,称为监狱。每个监狱都是主机系统上的一个虚拟环境,有自己的一组文件、进程和用户帐户。虽然chroot只将进程限制在文件系统的视图中,但 FreeBSD 将被监禁进程的活动限制在整个系统中,包括绑定到它的 IP 地址。这使得 FreeBSD jails 成为测试互联网连接软件的新配置的理想方式,可以很容易地试验不同的配置,同时不允许来自监狱的更改影响外部的主系统。

2005 年:OpenVZ

OpenVZ 在为低端虚拟专用服务器(VPS)提供商提供操作系统虚拟化方面非常受欢迎。OpenVZ 允许一个物理服务器运行多个独立的操作系统实例,称为容器。OpenVZ 使用了一个打了补丁的 Linux 内核,与所有容器共享它。每个容器充当一个独立的实体,拥有自己的一组虚拟化的文件、用户、组、进程树和虚拟网络设备。

2006 年:群体

最初被称为流程容器cgroups(控制组的简称)是由 Google 工程师启动的。cgroups是一个 Linux 内核特性,它将资源使用(如 CPU、内存、磁盘 I/O 和网络)限制并隔离给一组进程。cgroups已经被重新设计了多次,每一次重新设计都考虑到了它不断增长的用例数量和所需的特性。

2008: LXC

LXC 通过结合 Linux 内核的cgroups和对隔离名称空间的支持来提供操作系统级的虚拟化,从而为应用提供一个隔离的环境。Docker 最初使用 LXC 来提供隔离特性,但后来改用了自己的库。

容器和虚拟机

许多人认为既然容器隔离了应用,它们就和虚拟机一样。乍一看,它看起来很像,但根本的区别是容器与主机共享同一个内核。

Docker 只隔离单个进程(或者一组进程,这取决于映像是如何构建的),所有容器都运行在同一个主机系统上。因为隔离是在内核级应用的,所以与虚拟机相比,运行容器不会给主机带来很大的开销。当容器启动时,选定的进程或进程组仍在同一主机上运行,无需虚拟化或模拟任何东西。图 1-1 显示了在单个物理主机上的三个不同容器上运行的三个应用。

img/463857_2_En_1_Fig1_HTML.jpg

图 1-1

在三个不同容器上运行的三个应用的表示

相比之下,当虚拟机启动时,虚拟机管理程序会虚拟化整个系统,从 CPU 到 RAM 再到存储。为了支持这个虚拟化系统,需要安装整个操作系统。出于所有实际目的,虚拟化系统是在计算机中运行的整个计算机。现在,如果你能想象运行一个操作系统需要多少开销,想象一下如果你运行一个嵌套的操作系统会是什么样子!图 1-2 展示了在一台物理主机上的三个不同虚拟机上运行的三个应用。

img/463857_2_En_1_Fig2_HTML.jpg

图 1-2

在三个不同的虚拟机上运行的三个应用的表示

图 1-1 和 1-2 给出了在单个主机上运行的三个不同应用的指示。对于 VM 来说,不仅需要应用的依赖库,还需要操作系统来运行应用。相比之下,使用容器,与应用共享主机操作系统的内核意味着消除了额外操作系统的开销。这不仅大大提高了性能,还让您提高了资源利用率,并最大限度地减少了计算能力的浪费。

容器运行时

容器映像在启动和运行时成为一个容器。但是要做到这一点,必须有一个软件来引导运行容器所需的资源。这个软件叫做容器运行时。Docker 使用 containerd 项目实现了一个容器运行时,该项目现在是云本地计算基金会的毕业项目列表的一部分。

然而,containerd并不是惟一的容器运行时。还有其他的容器运行时项目,比如 cri-orkt (已经不在活跃开发中了)、 runC 等等。

OCI 和国际广播电台

随着更多容器运行时的开发,需要一个标准来定义什么是容器映像,即运行时的规范。这就是开放容器倡议(OCI)的由来。

OCI 是一个开放的治理结构,用于创建容器映像和运行时的行业标准规范,不受特定于供应商的特性的限制,以促进开放的生态系统。OCI 目前有两个规范:运行时规范和映像规范。

运行时规范定义了容器运行时应该如何将容器映像解包到文件系统中,以及运行容器的步骤。这确保了无论使用哪个容器运行时,容器都将按预期准确运行。

图像规范定义了一种 OCI 图像格式,其中包含了关于如何创建 OCI 图像的必要定义。OCI 映像包括映像清单、文件系统定义和映像配置。映像清单包含有关映像内容和依赖关系的元数据。映像配置包括应用参数和环境变量等数据。

容器运行时接口(CRI)是 Kubernetes 特有的术语,它定义了 Kubernetes 如何与多个容器运行时交互并引导容器。在 CRI 之前,Kubernetes 只支持 Docker 运行时。随着来自社区的支持更多容器运行时的请求,Kubernetes 团队为容器运行时实现了一个插件接口。这个插件接口允许 Kubernetes 支持可互换的容器运行时,允许来自社区的简单贡献。

Docker 工人和库柏工人

随着 Kubernetes 在行业中的使用越来越多,一个经常出现的问题是 Docker 和 Kubernetes 之间的区别。

Kubernetes 是运行容器和维护其生命周期的协调器。Docker 是多用途软件,不仅可以构建容器映像,还可以运行容器。虽然 Docker 不仅可以在单个节点上运行和维护容器的生命周期,还可以使用 Docker Compose 和 Docker Swarm 在多个节点上运行和维护容器的生命周期,但 Kubernetes 已经成为容器编排的事实标准。

Docker 和 Kubernetes 是互补的——Docker 构建容器映像,而 Kubernetes 编排这些容器的运行。Kubernetes 还可以调度容器在许多节点上的运行副本。

第八章对容器编排进行了更深入的研究。

摘要

在本章中,您了解了一些关于 Docker 公司、Docker 容器、容器与虚拟机的比较,以及容器试图解决的现实问题。您还简要了解了什么是容器运行时,以及 Docker 和 Kubernetes 是如何相互补充的。在接下来的章节中,您将对 Docker 进行一次介绍性的浏览,并就构建和运行容器进行几次实际操作。

二、Docker 101

现在你对 Docker 的工作原理和它的流行程度有了一点了解,在这一章中,你将学习一些与 Docker 相关的不同术语。您还将学习如何安装 Docker 并理解 Docker 术语,如图像、容器、Docker 文件和 Docker 合成。您还可以使用一些简单的 Docker 命令来创建、运行和停止 Docker 容器。

安装 Docker

Docker 支持 Linux、macOS 和 Windows 平台。在大多数平台上安装 Docker 很简单,我稍后会讲到。Docker Inc .提供 Docker 平台的社区版和企业版。

企业版具有与社区版相同的功能,但是它提供了额外的支持和认证的容器、插件和基础设施。对于本书的目的以及大多数一般的开发和生产用途,社区版是合适的,因此我将在本书中使用它。

在 Windows 上安装 Docker

Windows 上的 Docker 需要满足一些先决条件,然后才能安装它。其中包括:

  • Hyper-V 支持

  • 硬件虚拟化支持:这通常是从系统 BIOS 中启用的

  • 目前仅支持 64 位版本的 Windows 10(专业版/教育版/企业版,周年更新版本为 v1607)

如果您查看这些先决条件,您会注意到这看起来像是虚拟化设置所需要的,但是您在前一章中已经了解到 Docker 不是虚拟化。那么为什么 Docker for Windows 需要虚拟化所需的特性呢?

简短的回答是 Docker 依赖于众多特性,比如名称空间和cgroups,而这些在 Windows 上是没有的。为了绕过这个限制,Docker for Windows 创建了一个运行 Linux 内核的轻量级 Hyper-V 容器。如果你的电脑安装了 Windows 10 家庭版,你应该安装带有 WSL 2 后端的 Docker Desktop。这将在下一节中解释。

让我们把重点放在安装 Docker CE for Windows 上。本节假设所有先决条件都已满足,并且 Hyper-V 已启用。前往 https://store.docker.com/editions/community/docker-ce-desktop-windows 下载 Docker CE。

Note

确保您选择了稳定的频道,并单击“获取 Docker CE”按钮。

作为安装的一部分,可能会提示您启用 Hyper-V 和容器支持(参见图 2-1 )。

img/463857_2_En_2_Fig1_HTML.png

图 2-1

启用 Hyper-V 和容器功能

单击确定并完成安装。您可能需要重新启动系统,因为启用 Hyper-V 是 Windows 系统的一项功能。安装此功能需要重新启动才能启用。

安装完成后,打开一个命令提示符窗口(或者 PowerShell,如果您喜欢的话),键入以下命令检查 Docker 是否已安装并正常工作。

docker run --rm hello-world

如果安装顺利,您应该会看到清单 2-1 中所示的响应。

docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1\. The Docker client contacted the Docker daemon.
 2\. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3\. The Docker daemon created a new container from that image which runs the
 executable that produces the output you are currently reading.
 4\. The Docker daemon streamed that output to the Docker client, which sent it
 to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
...

Listing 2-1Response from the docker run command on Windows

我们稍后将深入研究这些命令的含义,所以不要担心理解它们。如果您看到“安装似乎工作正常”的消息,您现在应该没事了。

使用 WSL2 后端在 Windows 上安装 Docker

关于 WSL

在 2016 年 Windows 周年更新中宣布,Windows Subsystem for Linux (WSL)是开发人员从 Windows 中运行 GNU/Linux 应用的一种方式,无需第三方虚拟机设置或必须双重启动到 Linux。WSL 支持大多数命令行应用,对 GUI 应用的支持仍处于早期预览模式。

在 WSL 的第一个版本中,微软捆绑了一个定制的兼容层,用于在 Windows 中运行 Linux 二进制可执行文件,而无需重写或重新编译应用的源代码。微软使用一个转换层来实现这一点,该转换层从 Linux 应用中截取 Linux 系统调用,并将它们转换成 Windows 系统调用。

对于 WSL2,微软通过发布一个带有 Linux 内核的轻量级虚拟机(VM ),完全重新构建了 WSL 的工作方式。这个轻量级 VM 充当 Linux 应用的执行层。由于 Linux 应用现在在轻量级虚拟机上的 Linux 内核上本地运行,而不是使用转换层,因此与 WSL 的第一版相比,WSL2 支持 Linux 内核的所有功能,并提高了 Linux 应用的性能。

虽然虚拟机带来了大量资源使用的问题,但 Windows 在幕后管理 WSL2 虚拟机,并完成动态内存分配,随着应用请求/释放内存,增加/减少内存消耗。WSL2 仍处于早期阶段,您可能会偶尔遇到一些问题/速度变慢或大量消耗内存。快速重启 Windows 可以缓解这些问题。您也可以关闭并重新启动虚拟机,这将使 Windows 释放 Windows 保留的内存。

安装和启用 WSL2 的要求

在安装 WSL2 之前,请确保您的计算机安装了 Windows 10 64 位版本 1903 或更高版本。WSL2 不能在低于 1903 的版本上工作。你可以在终端提示符下输入 winver 来检查版本,如图 2-2 所示。

img/463857_2_En_2_Fig2_HTML.jpg

图 2-2

检查红色框中突出显示的 Windows 版本

WSL2 的安装步骤详见微软网站 https://docs.microsoft.com/en-us/windows/wsl/install-win10 。按照手动安装步骤下列出的步骤安装 WSL2。我强烈建议您也安装 Windows 终端,就像在前面的链接中提到的那样,因为它使得在 WSL2 中运行 Docker 命令更加容易。

一旦安装了 WSL,运行以下命令以确保 WSL2 被设置为默认版本。

wsl --set-default-version 2

https://desktop.docker.com/win/stable/amd64/Docker%20Desktop%20Installer.exe 下载并运行安装程序,安装 Docker Desktop with WSL2 Backend。安装完成后,打开一个命令提示符窗口(或者 PowerShell,如果您喜欢的话),键入以下命令检查 Docker 是否已安装并正常工作。

docker run –rm hello-world

如果安装顺利,您应该会看到清单 2-2 中的响应。

docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

[...]

Listing 2-2Response from the docker run Command Using WSL

"Hello from Docker!"消息表明 Docker 已安装并正常工作。请注意,实际输出类似于清单 2-1 中的输出,并且在本例中已经进行了调整。

在 macOS 上安装

安装 Docker for Mac 就像安装任何其他应用一样。进入 https://store.docker.com/editions/community/docker-ce-desktop-mac ,点击 Get Docker for CE Mac (stable)链接,双击文件运行下载的安装程序。将 Docker whale 拖到 Applications 文件夹下安装,如图 2-3 所示。

img/463857_2_En_2_Fig3_HTML.png

图 2-3

安装 Docker

安装 Docker 后,打开终端应用并运行此命令以确认安装成功。

docker run --rm hello-world

如果安装顺利,您应该会看到清单 2-3 中所示的响应。

docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

[...]

Listing 2-3Response from the docker run Command on macOS

“Docker 工人向你问好!”消息表明 Docker 已安装并正常工作。请注意,实际的输出类似于清单 2-1 中的输出,在本例中已经进行了调整。

在 Linux 上安装

要在 Linux 上安装 Docker,请访问 https://www.docker.com/community-edition 。选择您正在使用的发行版,按照命令安装 Docker。

以下部分概述了在 Ubuntu 上安装 Docker 所需的步骤。

  1. 更新apt索引:

  2. 安装在 HTTPS 上使用存储库所需的必要软件包:

sudo apt-get update

  1. 安装 Docker 的官方 GPG 密钥:
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common

  1. 添加 Docker 的稳定存储库:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

  1. 更新apt包索引:
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

  1. 安装 Docker:
sudo apt-get update

sudo apt-get install docker-ce

附加步骤

Docker 通过 root 用户拥有的 UNIX 套接字进行通信。您可以通过以下步骤避免键入sudo:

Warning

Docker 组权限仍然等同于 root 用户。

  1. 创建 Docker 工人组:

  2. 将您的用户添加到docker组:

sudo groupadd docker

  1. 注销并重新登录。运行以下命令,确认 Docker 安装正确:
sudo usermod -aG docker $USER

docker run --rm hello-world

如果安装顺利,您应该会看到清单 2-4 中所示的响应。

docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.
[...]

Listing 2-4Response from the docker run Command on Linux

“Docker 工人向你问好!”消息表明 Docker 已安装并正常工作。请注意,实际的输出类似于清单 2-1 中的输出,在本例中已经进行了调整。

理解 Docker 的行话

现在,您已经安装并运行了 Docker,是时候学习与 Docker 相关的不同术语了。

是应用于由 Docker 文件中的指令表示的 Docker 图像的修改。通常,当基础图像发生变化时,会创建一个层。例如,考虑如下所示的 docker 文件:

FROM ubuntu
Run mkdir /tmp/logs
RUN apt-get install vim
RUN apt-get install htop

在这种情况下,Docker 会将ubuntu图像视为基础图像,并添加三层:

  • 一层用于创建/tmp/logs

  • 安装vim的另一层

  • 安装htop的第三层

当 Docker 构建映像时,每一层都堆叠在一起,并使用 union 文件系统合并成一个层。使用 SHA-256 哈希对图层进行唯一标识。这使得重用和缓存它们变得容易。当 Docker 扫描一个基本映像时,它会扫描组成该映像的所有层的 id,并开始下载这些层。如果图层存在于本地缓存中,它会跳过下载缓存的图像。

Docker 工人图像

Docker image 是一个只读模板,它构成了应用的基础。它非常像一个 shell 脚本,为系统准备了所需的状态。更简单地说,它相当于一份烹饪食谱,上面有制作最终菜肴的一步一步的说明。

Docker 映像从基础映像开始,通常选择您最熟悉的操作系统,如 Ubuntu。在这个映像之上,您可以添加构建您的应用堆栈,在需要时添加包。对于一些最常见的应用堆栈,有许多预构建的映像,包括 Ruby on Rails、Django、PHP-FPM 和nginx等等。在高级规模上,为了保持图像大小尽可能小,您还可以从诸如 Alpine 甚至 scratch 之类的超薄包开始,这是 Docker 为构建其他图像保留的最小起始图像。

Docker 映像是使用一系列称为指令的命令在一个称为 Dockerfile 的文件中创建的。在项目存储库的根中出现 Dockerfile 是一个很好的指示,表明程序是容器友好的。您可以从关联的 Dockerfile 文件构建自己的映像,然后将构建的映像发布到注册表。您将在后面的章节中深入了解 Dockerfile。现在,将 Docker 映像视为最终的可执行包,它包含了运行应用所需的一切——源代码、所需的库和依赖项。

Docker 标签

标签是唯一标识 Docker 图像的特定版本的名称。标签是纯文本标签,通常用于标识特定的细节,如版本、映像的基本操作系统或 Docker 映像的架构。

标记 Docker 映像使您能够灵活地唯一引用特定版本,从而在当前映像未按预期工作时更容易回滚到 Docker 映像的以前版本。

Docker 容器

当在主机中运行时,Docker 映像产生一个具有自己的名称空间的进程,并被称为 Docker 容器。Docker 映像和容器之间的主要区别是存在一个称为容器层的薄读写层。对容器的文件系统所做的任何更改——比如写入新文件或修改现有文件——都是对这个可写容器层进行的。

需要把握的一个重要方面是,当容器运行时,更改会应用到容器层,而当容器停止/终止时,容器层不会被保存。因此,所有更改都将丢失。容器的这一方面还没有被很好地理解,因此,有状态的应用和那些需要持久数据的应用最初不被推荐作为容器化的应用。然而,有了 Docker 卷,就有办法绕过这个限制。第五章更详细地介绍了 Docker 卷。

绑定装载和卷

回想一下,当容器运行时,对容器的任何更改都会出现在文件系统的容器层中。在容器被终止的情况下,更改会丢失,并且数据不再可访问。即使容器正在运行,从容器中获取数据也不是很简单。此外,写入容器的可写层需要一个存储驱动程序来管理文件系统。存储驱动程序在文件系统上提供了一个抽象,可用于保存更改,这种抽象通常会降低性能。

出于这些原因,Docker 提供了不同的方式将数据从 Docker 主机装载到容器中:卷、绑定装载或 tmpfs 卷。虽然 tmpfs 卷仅存储在主机系统的内存中,但绑定装载和卷存储在主机文件系统中。

第五章详细探讨了 Docker 卷。

Docker 存储库

您之前了解到可以利用常见应用堆栈的现有映像,您是否想过这些映像存储在哪里,以及如何在构建应用时使用它们?Docker 存储库是一个你可以上传和存储 Docker 图片的地方。这些存储库允许在您的公司内或向公众轻松分发 Docker 图像。

Docker 注册表

Docker 存储库需要一个中心位置来存储数据——这个中心位置是一个 Docker 注册表。Docker 注册中心是各种 Docker 存储库的集合。Docker 注册中心由第三方公司托管,如果您需要满足更严格的合规性要求,也可以自行托管。Docker Hub 是一个常用的 Docker 注册表。其他一些流行的 Docker 注册表包括:

  • 谷歌容器注册

  • 亚马逊弹性容器注册中心

  • JFrog Artifactory

这些注册表中的大多数还允许将您推送的图像的可见性级别设置为公共/私有。私人注册将防止您的 Docker 图像被公众访问,允许您设置访问控制,以便只有授权用户才能使用您的 Docker 图像。

Dockerfile

一个 Dockerfile 是一组指令,告诉 Docker 如何构建一个映像。典型的 Dockerfile 文件包括以下内容:

  • 一个FROM指令,指示 Docker 什么是基本图像

  • 传递环境变量的ENV指令

  • 运行一些 shell 命令的RUN指令(例如,安装基础映像中没有的相关程序)

  • 一个CMD或一个ENTRYPOINT指令,告诉 Docker 当一个容器启动时要运行什么可执行文件

正如你所看到的,Dockerfile 指令集有一个清晰简单的语法,这使得它很容易理解。在本书的后面,您将更深入地了解 Dockerfiles。

Docker 工人引擎

Docker 引擎是 Docker 的核心部分。Docker Engine 是一个客户端-服务器应用,它提供了平台、运行时和工具,用于构建和管理 Docker 映像、Docker 容器等等。Docker 引擎提供以下功能:

  • docker daemon(Docker 守护程序)

  • CLI Docker

  • Docker API

docker daemon(Docker 守护程序)

Docker 守护进程是一种在主机后台运行的服务,它处理大部分 Docker 命令的繁重工作。这个守护进程监听 API 创建和管理 Docker 对象(如容器、网络和卷)的请求。Docker 守护进程还可以与其他守护进程对话,以管理和监控 Docker 容器。守护进程间通信的一些例子包括用于容器度量监控的通信数据狗和用于容器安全监控的 Aqua。

CLI Docker

Docker CLI 是您与 Docker 交互的主要方式。Docker CLI 公开了一组您可以提供的命令。Docker CLI 将请求转发给 Docker 守护进程,后者执行必要的工作。

虽然 Docker CLI 包括大量的命令和子命令,但本书中提到的最常见的命令如下:

docker build
docker pull
docker run
docker exec

Tip

Docker 在其文档页面上的 https://docs.docker.com/engine/reference/commandline/cli/ 维护了所有 Docker 命令的广泛引用。

在任何时间点,在命令前面加上help将会打印出关于该命令的所需文档。例如,如果您不确定从哪里开始使用 Docker CLI,您可以键入以下内容:

docker help

Usage:  docker COMMAND

A self-sufficient runtime for containers

Options:
      --config string      Location of client config files (default
                           ".docker")
  -D, --debug              Enable debug mode
  -H, --host list          Daemon socket(s) to connect to
  -l, --log-level string   Set the logging level
                           ("debug"|"info"|"warn"|"error"|"fatal")
                           (default "info")
[..]

如果您想了解关于 Docker pull的更多信息,请键入以下内容:

docker help pull

Usage:  docker pull [OPTIONS] NAME[:TAG|@DIGEST]

Pull an image or a repository from a registry

Options:
  -a, --all-tags                Download all tagged images in the repository
      --disable-content-trust   Skip image verification (default true)
      --platform string         Set platform if server is multi-platform
                                capable

Docker API

Docker 还提供了与 Docker 引擎交互的 API。如果需要从应用内部创建或管理容器,这将非常有用。Docker CLI 支持的几乎所有操作都可以通过 API 来完成。

开始使用 Docker API 最简单的方法是使用curl发送一个 API 请求。Windows Docker 主机可以访问 TCP 端点:

curl http://localhost:23img/json
[{"Containers":-1,"Created":1511223798,"Id":"sha256:f2a91732366c0332ccd7afd2a5c4ff2b9af81f549370f7a19acd460f87686bc7","Labels":null,"ParentId":"","RepoDigests":["hello-world@sha256:66ef312bbac49c39a89aa9bcc3cb4f3c9e7de3788c944158df3ee0176d32b751"],"RepoTags":["hello-world:latest"],"SharedSize":-1,"Size":1848,"VirtualSize":1848}]

在 Linux 和 Mac 上,同样可以通过使用curl向 UNIX 套接字发送请求来实现:

curl --unix-socket /var/run/docker.sock -X POST httpimg/json

[{"Containers":-1,"Created":1511223798,"Id":"sha256:f2a91732366c0332ccd7afd2a5c4ff2b9af81f549370f7a19acd460f87686bc7","Labels":null,"ParentId":"","RepoDigests":["hello-world@sha256:66ef312bbac49c39a89aa9bcc3cb4f3c9e7de3788c944158df3ee0176d32b751"],"RepoTags":["hello-world:latest"],"SharedSize":-1,"Size":1848,"VirtualSize":1848}]

Docker Compose

Docker Compose 是一个定义和运行多容器应用的工具。就像 Docker 允许您为您的应用构建一个映像并在您的容器中运行它一样,Compose 使用相同的映像结合一个定义文件(称为 compose file )来构建、启动和运行多容器应用,包括依赖容器和链接容器。

Docker Compose 最常见的用例是以与运行单个容器应用相同的简单、简化的方式运行应用及其依赖的服务(如数据库和缓存提供者)。第七章深入了解 Docker Compose。

Docker 机器

Docker Machine 是一个用于在多个虚拟主机上安装 Docker 引擎和管理主机的工具。Docker Machine 允许在本地和远程系统上创建 Docker 主机,包括 Amazon Web Services、DigitalOcean 或 Microsoft Azure 等云平台。

动手 Docker 工人

你现在可以尝试一下你在本章中读到的一些东西。在您开始研究各种可用的命令之前,请确保您的 Docker 安装是正确的,并且它按预期工作。

Tip

为了便于阅读和理解,我们使用了一个名为jq的工具来处理 Docker 的 JSON 输出。可以从 https://stedolan.github.io/jq/ 下载安装jq

打开终端窗口,键入以下命令:

docker info

您应该会看到这样的结果:

docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 1
Server Version: 17.12.0-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host ipvlan macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 89623f28b87a6004d4b785663257362d1658a729
runc version: b2567b37d7b75eb4cf325b77297b140ea686ce8f
init version: 949e6fa
Security Options:
 seccomp
  Profile: default
Kernel Version: 4.9.60-linuxkit-aufs
Operating System: Docker for Windows
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 1.934GiB
Name: linuxkit-00155d006303
ID: Y6MQ:YGY2:VSAR:WUPD:Z4DA:PJ6P:ZRWQ:C724:6RKP:YCCA:3NPJ:TRWO
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): true
 File Descriptors: 19
 Goroutines: 35
 System Time: 2018-02-11T15:56:36.2281139Z
 EventsListeners: 1
Registry: https://index.docker.io/v1/
Labels:
Experimental: true
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

如果您没有看到类似的消息,请参考前面的章节来安装和验证您的 Docker 安装。

使用 Docker 图像

现在,您可以尝试查看可用的 Docker 图像。为此,请键入以下命令:

docker image ls

这里有一个本地可用图像的列表。

REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
hello-world    latest    f2a91732366c   2 months ago     1.85kB

如果您提取更多的图像或运行更多的容器,您会看到一个更大的列表。再来看hello-world图。为此,请键入以下内容:

docker image inspect hello-world

 [
    {
        "Id": "sha256:f2a91732366c0332ccd7afd2a5c4ff2b9af81f549370f7a19acd460f87686bc7",
        "RepoTags": [
            "hello-world:latest"
        ],
        "RepoDigests": [
            "hello-world@sha256:66ef312bbac49c39a89aa9bcc3cb4f3c9e7de3788c944158df3ee0176d32b751"
        ],
        "Parent": "",
        "Comment": "",
        "Created": "2017-11-21T00:23:18.797567713Z",
        "Container": "fb0b4536aac3a96065e1bedb2b637a6019feec666c7699592206956c9d3adf5f",
        "ContainerConfig": {
            "Hostname": "fb0b4536aac3",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"/hello\"]"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:2243ee460b69c4c036bc0e42a48eaa59e82fc7737f7c9bd2714f669ef1f8370f",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "DockerVersion": "17.06.2-ce",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/hello"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:2243ee460b69c4c036bc0e42a48eaa59e82fc7737f7c9bd2714f669ef1f8370f",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 1848,
        "VirtualSize": 1848,
        "GraphDriver": {
            "Data": {
                "MergedDir": "/var/lib/docker/overlay2/5855bd20ab2f521c39e1157f98f235b46d7c12c9d8f69e252f0ee8b04ac73d33/merged",
                "UpperDir": "/var/lib/docker/overlay2/5855bd20ab2f521c39e1157f98f235b46d7c12c9d8f69e252f0ee8b04ac73d33/diff",
                "WorkDir": "/var/lib/docker/overlay2/5855bd20ab2f521c39e1157f98f235b46d7c12c9d8f69e252f0ee8b04ac73d33/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:f999ae22f308fea973e5a25b57699b5daf6b0f1150ac2a5c2ea9d7fecee50fdf"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

docker inspect提供了大量关于图像的信息。重要的是图像属性EnvCmdLayers,它们告诉您环境变量、容器启动时运行的可执行文件以及与之相关的层。

环境变量如下:

docker image inspect hello-world | jq .[].Config.Env
[
  "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
]

容器上的启动命令如下:

docker image inspect hello-world | jq .[].Config.Cmd
[
  "/hello"
]

与图像相关联的层如下:

docker image inspect hello-world | jq .[].RootFS.Layers
[
  "sha256:f999ae22f308fea973e5a25b57699b5daf6b0f1150ac2a5c2ea9d7fecee50fdf"
]

使用真实的 Docker 图像

让我们看一个更复杂的图像。Nginx 是一个非常流行的 HTTP/S 反向代理服务器,也是一个负载平衡器和 web 服务器。

要下拉nginx图像,请键入以下内容:

docker pull nginx

Using default tag: latest
latest: Pulling from library/nginx
e7bb522d92ff: Pull complete
6edc05228666: Pull complete
cd866a17e81f: Pull complete
Digest: sha256:285b4
Status: Downloaded newer image for nginx:latest

注意第一行:

Using default tag: latest

由于您没有提供标签,Docker 使用名为latest的默认标签。Docker Store 列出了与图像相关的不同标签——因此,如果您正在寻找特定的标签/版本,最好在 Docker Store 上查看。图 2-4 显示了一个图像的典型标签列表。

img/463857_2_En_2_Fig4_HTML.png

图 2-4

Docker 存储 nginx 和可用标签的列表

让我们尝试拉一个带有特定标签的图像,称为stable。命令与以前一样。您必须在标签后添加一个冒号,以明确提及该标签:

docker pull nginx:stable
stable: Pulling from library/nginx
b4d181a07f80: Already exists
e929f62bc938: Pull complete
ca8370516c99: Pull complete
6af693de7b22: Pull complete
c8fe6ce83489: Pull complete
7aa1fe8b4a84: Pull complete
Digest: sha256:a7c7c13
Status: Downloaded newer image for nginx:stable
docker.io/library/nginx:stable

您看到的不同十六进制数字是图像的相关层。默认情况下,Docker 从 Docker Hub 获取图像。您可以手动指定不同的注册表。如果 Docker 映像在 Docker Hub 上不可用,而是存储在其他地方,如本地托管的 artifactory,这将非常有用。要指定不同的注册表,您必须在映像名称前添加注册表路径。因此,如果注册表托管在docker-private-docker-registry.example.com上,那么pull命令现在将是:

docker pull private-docker-registry.example.com/nginx

如果注册中心需要认证,您可以通过输入凭证docker login来登录,如下所示:

docker login -u <username> -p <password> private-docker-registry.example.com

这样做的一个不利的副作用是,输入的密码会被记录下来,并以明文形式保存在 shell 历史记录中。Docker 会提醒你这条消息。

为了防止这种情况,您可以使用下面的命令将密码从一个文件传输到 Docker 的标准输入中,假设密码存储在一个名为docker_password的文件中

docker login -u <username> --password-stdin private-docker-registry.example.com < docker_password

使用 PowerShell 的 Windows 用户可以使用Get-Content cmdlet 实现相同的功能,如下所示:

Get-Content docker_password | docker login -u <username> --password-stdin private-docker-registry.example.com

现在有了图像,试着启动一个容器。要启动一个容器并运行相关的映像,请键入docker run

docker run -p 80:80 nginx

尝试发出一个 curl 请求,看看nginxweb 服务器是否正在运行:

curl http://localhost:80
<!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>

这证实了nginx容器确实已经启动并正在运行。在这里,您会看到一个额外的标志,-p。这个标志告诉 Docker 将暴露的端口从 Docker 容器发布到主机。

标志后的第一个参数是 Docker 主机上必须发布的端口,第二个参数是指容器内的端口。您可以使用docker inspect:确认镜像发布端口

docker image inspect nginx | jq .[].Config.ExposedPorts
{
  "80/tcp": {}
}

您可以通过更改-p标志后的第一个参数来更改 Docker 主机上发布服务的端口,如下所示:

docker run -p 8080:80 nginx

现在尝试向 8080 端口发出一个curl请求:

curl http://localhost:8080

您应该会看到相同的响应。要列出所有正在运行的容器,请键入docker ps:

docker ps

docker ps
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
fac5e92fdfac  nginx  "nginx -g 'daemon of…"  5 seconds ago       Up 3 seconds  0.0.0.0:80->80/tcp     elastic_hugle
3ed1222964de  nginx  "nginx -g 'daemon of…"  16 minutes agoUp 16 minutes 0.0.0.0:8080->80/tcp   clever_thompson

需要注意的一点是names列。Docker 会在容器启动时自动分配一个随机名称。由于您应该使用更有意义的名称,您可以通过提供-n required-name作为参数来为容器提供一个名称。

Tip

Docker 名称的格式为adjective_surname,并且是随机生成的,例外情况是,如果选择的形容词是boring并且姓氏是Wozniak,Docker 会重试名称生成。

需要注意的另一点是,当您创建第二个容器,并将其端口发布到端口 8080 时,另一个容器将继续运行。要停止容器,您必须键入以下命令:

docker stop <container-id>

其中container-id可从该列表中获得。如果停靠成功,Docker 将回显容器 ID。如果容器拒绝停止,您可以发出kill命令强制停止并杀死容器:

docker kill <container-id>

让我们试着停止一个容器。键入以下内容:

docker stop fac5e92fdfac
fac5e92fdfac

现在,让我们试着杀死另一个容器:

docker kill 3ed1222964de
3ed1222964de

让我们确认容器不再运行,为此,键入:

docker ps

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

那么,被拦下的容器呢——它们在哪里?默认情况下,docker ps只显示活动的、正在运行的容器。要列出所有容器,请键入:

docker ps -a
CONTAINER ID        IMAGE         COMMAND          CREATEDSTATUS              PORTS         NAMES
fac5e92fdfac        nginx         "nginx -g 'daemon of…"6 minutes ago       Exited (0) 4 minutes ago    elastic_hugle
3ed1222964de        nginx         "nginx -g 'daemon of…"22 minutes ago      Exited (137) 3 minutes ago  clever_thompson
febda50b0a80        nginx         "nginx -g 'daemon of…"28 minutes ago      Exited (137) 24 minutes agoobjective_franklin
dc0c33a79fb7        nginx         "nginx -g 'daemon of…"33 minutes ago      Exited (137) 28 minutes ago   vigorous_mccarthy
179f16d37403        nginx         "nginx -g 'daemon of…"34 minutes ago      Exited (137) 34 minutes ago     nginx-test

即使容器已经被停止和/或终止,这些容器仍然存在于本地文件系统中。您可以通过键入以下命令来删除容器:

docker rm <container-id>
docker rm fac5e92fdfac
fac5e92fdfac

让我们确认容器确实被移走了:

docker ps -a
CONTAINER ID        IMAGE         COMMAND             CREATED             STATUS              PORTS         NAMES
3ed1222964de        nginx         "nginx -g 'daemon of…"28 minutes ago      Exited (137) 9 minutes ago  clever_thompson
febda50b0a80        nginx         "nginx -g 'daemon of…"34 minutes ago      Exited (137) 30 minutes ago      objective_franklin
dc0c33a79fb7        nginx         "nginx -g 'daemon of…"39 minutes ago      Exited (137) 34 minutes agovigorous_mccarthy
179f16d37403        nginx         "nginx -g 'daemon of…"40 minutes ago      Exited (137) 40 minutes ago     nginx-test

从这个表中可以看到,ID 为fac5e92fdfac的容器不再显示,因此已经被删除。

同样,您可以通过键入以下命令列出系统中存在的所有图像:

docker image ls
REPOSITORY         TAG      IMAGE ID        CREATED        SIZE
nginx              1.12-alpine-perl          b6a456f1d7ae 4 weeks ago        57.7MB
nginx              latest   3f8a4339aadd    6 weeks ago   108MB
hello-world        latest   f2a91732366c    2 months ago  1.85kB
kitematic/hello-world-nginx   latest        03b4557ad7b9 2 years ago        7.91MB

让我们试着移除nginx图像:

docker rmi 3f8a4339aadd
Error response from daemon: conflict: unable to delete 3f8a4339aadd (must be forced) - image is being used by stopped container dc0c33a79fb7

在这种情况下,Docker 拒绝删除图像,因为另一个容器中存在对该图像的引用。在移除所有使用特定图像的容器之前,您不能完全移除该图像。

摘要

在本章中,你学习了如何在不同的操作系统上安装 Docker。您还了解了如何验证 Docker 是否已安装并正常工作,以及一些与 Docker 相关的常用术语。最后,您在 Docker 上运行了一些实践练习,包括如何提取图像、运行容器、列出正在运行的容器,以及最后如何停止和删除容器。

下一章简要介绍 telegram,包括如何使用 telegram 创建和注册一个 bot,以及如何运行基于 Python 的 Telegram 消息 bot,它将从 Reddit 获取帖子。

三、构建 Python 应用

对于许多开始编程的人来说,他们的首要问题之一是弄清楚他们能构建什么。仅仅通过阅读很少能学会编程。许多人认为他们可以阅读几本指南,看看语法,然后轻松地学习如何编程。但是编程需要动手实践。

因此,本书包含了一个示例 Python 项目。该项目在开始时并不复杂,但随着经验的积累,很容易对项目进行进一步的扩展和定制。

关于项目

Note

本书假设你具备 Python 的基础知识,并且已经安装了 Python 3.6 或更高版本。

为了帮助您熟悉 Docker,这本书将教您如何使用现有的 Python 应用,从 Python 命令行运行它,介绍不同的 Docker 组件,然后将应用转换为容器化的映像。

Python 应用是一个简单的应用,具有 bot 接口,使用 Telegram Messenger 从 Reddit 获取最近 10 篇文章。使用 Telegram,您可以订阅子编辑列表。web 应用将检查新帖子的订阅子编辑,如果发现新主题,它会将主题发布到 bot 接口。当用户请求时,该接口将把消息传送到电报信使。

最初,您不会保存首选项(即 subreddit 订阅),而是将重点放在启动和运行机器人上。一旦一切正常,您将学习如何将首选项保存到文本文件中,并最终保存到数据库中。

设置电报信使

在您继续之前,您需要一个电报信使帐户。要注册,请转到 https://telegram.org ,为您选择的平台下载应用并安装。一旦它运行,你将被要求提供一个手机号码。Telegram 用这个来验证你的帐户。输入您的手机号码,如图 3-1 所示。

img/463857_2_En_3_Fig1_HTML.png

图 3-1

电报注册页面

输入您的号码后,您应该会获得一个一次性密码来登录。输入一次性密码并登录,如图 3-2 所示。

img/463857_2_En_3_Fig2_HTML.png

图 3-2

电报的一次性密码

BotFather: Telegram 的机器人创建接口

Telegram 使用一个名为“机器人父亲”的机器人作为其创建新机器人和更新它们的接口。要开始使用 BotFather,请在搜索面板中键入BotFather。在聊天窗口中,输入/start

这将触发 BotFather 提供一组介绍性的消息,如图 3-3 所示。

img/463857_2_En_3_Fig3_HTML.png

图 3-3

父亲的选择

用机器人父亲创建机器人

您将使用 BotFather 生成一个新的机器人。首先在电报信使中输入/newbot。这会引发一系列你需要回答的问题(大部分都很直白)。由于 Telegram 的限制,机器人的用户名必须总是以bot结尾。这意味着你可能得不到你想要的用户名(见图 3-4 )。

img/463857_2_En_3_Fig4_HTML.png

图 3-4

电报机器人准备行动

除了文档的链接,您还会注意到 Telegram 发布了一个令牌。HTTP 是一种无状态协议——web 服务器不知道也不跟踪谁在请求资源。客户机需要标识自己,以便 web 服务器可以标识客户机、授权客户机并为请求提供服务。Telegram 使用 API 令牌(以下简称为<token>,包括在代码示例中)作为一种识别和授权机器人的方式。

Caution

令牌极其敏感。如果它被泄露到网上,任何人都可以作为你的机器人发布消息。不要将其签入您的版本控制或发布到任何地方!

当使用您不熟悉的 API 时,最好使用一个好的工具来测试和探索端点,而不是马上输入代码。REST API 测试工具的一些例子包括失眠邮差卷曲

Telegram 的 Bot API 文档可以在 https://core.telegram.org/bots/api 获得。要提出请求,您必须包含<token>。常规 URL 如下:

https://api.telegram.org/bot<token>/METHOD_NAME

让我们尝试一个样例 API 请求,它确认令牌按预期工作。Telegram Bot API 提供了一个用于测试auth令牌的/getMe端点。您可以尝试一下,首先不用令牌,如清单 3-1 所示。

curl https://api.telegram.org/bot/getMe

{
  "ok": false,
  "error_code": 404,
  "description": "Not Found"
}

Listing 3-1Making a curl Request to Telegram API Without a Token

如果没有机器人令牌,Telegram 就不会接受这个请求。现在试试这个令牌,如清单 3-2 所示。

curl https://api.telegram.org/bot<token>/getMe

{
  "ok": true,
  "result": {
    "id": 495637361,
    "is_bot": true,
    "first_name": "SubRedditFetcherBot",
    "username": "SubRedditFetcher_Bot"
  }
}

Listing 3-2Making a curl Request to Telegram API with a Valid Token

您可以看到,使用适当的令牌,Telegram 可以识别并授权机器人。这确认了 bot 令牌是正确的,您可以继续使用该应用。

新闻机器人:Python 应用

Newsbot 是一个 Python 脚本,在 Telegram bot API 的帮助下与 Bot 进行交互。新闻机器人做以下事情:

  • 持续轮询 Telegram API 以获取发布到 bot 的新更新。

  • 如果检测到获取新更新的关键字,它将从选定的子编辑中获取新闻。

在幕后,Newsbot 处理这些场景:

  • 如果有一条以/start/help开头的新消息,它会显示关于该做什么的简单帮助文本。

  • 如果有一条消息以/sources开头,后跟一个子编辑列表,那么它接受它们作为来自适用的 Reddit 帖子的子编辑。

Newsbot 依赖于几个 Python 库;

  • Praw 或 Python Reddit API 包装器,用于从子编辑中获取帖子。

  • Requests 是最流行的 Python 库之一,它为 HTTP 请求提供了更简单、更干净的 API。

新闻机器人入门

要开始使用 Newsbot,请下载该 bot 的源代码。源代码可以在本书的 GitHub 资源库中找到,在 https://github.com/Apress/practical-docker-with-python

如果您熟悉 Git,可以使用以下命令克隆 repo:

git clone https://github.com/Apress/practical-docker-with-python.git

或者,您可以单击绿色的 Code 按钮,并从 GitHub 存储库页面选择 Download ZIP 来获取源代码。一旦克隆了 repo 或解压了 ZIP 文件,通过键入以下命令切换到包含源代码的目录:

cd practical-docker-with-python/source-code/chapter-3/python-app

现在安装依赖项。为此,请键入以下内容:

pip3 install -r requirements.txt

pip (Pip 安装包)是一个安装 Python 库的包管理器。pip 包含在 Python 2.7.9 和更高版本以及 Python 3.4 和更高版本中。 pip3 表示您正在为 Python 3 安装库。如果没有安装 pip,请在继续之前安装它。

-r标志告诉 pip 从requirements.txt安装所需的包。

pip 将检查、下载和安装依赖项。如果一切顺利,您应该会看到清单 3-3 中的输出。

Collecting praw==3.6.0 (from -r requirements.txt (line 1))
  Downloading praw-3.6.0-py2.py3-none-any.whl (74kB)
Collecting requests==2.18.4 (from -r requirements.txt (line 2))
[...]
Installing collected packages: requests, update-checker, decorator, six, praw
Successfully installed decorator-4.0.11 praw-3.6.0 requests-2.18.4 six-1.10.0 update-checker-0.16

Listing 3-3The Output from a Successful pip Install

如果已经安装了一些包,pip 将不会重新安装它们,并且会用一条"Requirement already satisfied"消息通知您已经安装了依赖项。

运行新闻机器人

让我们启动机器人。bot 需要一个来自您之前创建的 Telegram 的认证令牌(称为<token>)。这需要设置为一个名为NBT_ACCESS_TOKEN的环境变量。没有这个令牌,机器人将不会运行。要设置此令牌,请打开一个终端,并根据您的平台输入以下命令。

Windows 用户:

setx NBT_ACCESS_TOKEN <token>

Linux 和 macOS 用户:

export NBT_ACCESS_TOKEN=<token>

现在,通过键入以下命令启动 Python 脚本:

python newsbot.py

如果一切正常,您应该会看到周期性的 OK 消息,如清单 3-4 所示。这意味着 Newsbot 正在运行,并且正在主动侦听更新。

python newsbot.py
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}

Listing 3-4Output from Newsbot When It Is Running and Listening to Messages from Telegram

向新闻机器人发送消息

在本节中,您将尝试向 Newsbot 发送一条消息,看看它是否接受请求。在“机器人父亲”窗口中,单击指向机器人的链接(或者,您也可以使用机器人用户名进行搜索)。单击开始按钮。这将触发一个/start命令,该命令将被机器人拦截。

注意,日志窗口显示了传入的请求和正在发送的传出消息,如清单 3-5 所示。

INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': [{'update_id': 720594461, 'message': {'message_id': 5, 'from': {'id': 7342383, 'is_bot': False, 'first_name': 'Sathya', 'last_name': 'Bhat', 'username': 'sathyabhat', 'language_code': 'en-US'}, 'chat': {'id': 7342383, 'first_name': 'Sathya', 'last_name': 'Bhat', 'username': 'sathyabhat', 'type': 'private'}, 'date': 1516558659, 'text': '/start', 'entities': [{'offset': 0, 'length': 6, 'type': 'bot_command'}]}}]}
INFO: handle_incoming_messages - Chat text received: /start
INFO: post_message - posting
                    Hi! This is a News Bot which fetches news from subreddits. Use "/source" to select a subreddit source.

 Example "/source programming, games" fetches news from r/programming, r/games.

 Use "/fetch" for the bot to go ahead and fetch the news. At the moment, bot will fetch total of 10 posts from all subreddits
                 to 7342383
INFO: get_updates - received response: {'ok': True, 'result': []}

Listing 3-5The Newsbot Responding to Commands

图 3-5 显示电报信使窗口。

img/463857_2_En_3_Fig5_HTML.png

图 3-5

新闻机器人对开始消息的响应

尝试设置一个源子编辑。在电报信使窗口中,键入以下内容:

/source python

你应该从机器人那里得到一个肯定的确认,说源被选中了(见图 3-6 )。

img/463857_2_En_3_Fig6_HTML.jpg

图 3-6

分配的来源

现在你可以告诉机器人获取一些消息。为此,请键入:

/fetch

机器人应该发送一个关于获取帖子的确认消息。然后它将发布来自 Reddit 的帖子(见图 3-7 )。

img/463857_2_En_3_Fig7_HTML.png

图 3-7

新闻机器人发布来自 Python subreddit 的头条新闻

bot 有效;正如预期的那样,它获得了最高职位。在接下来的一系列章节中,您将学习如何将 Newsbot 移动到 Docker。

摘要

在这一章中,你学习了本书的 Python 项目的细节,它是一个聊天机器人。您还了解了如何安装和配置 Telegram Messenger,如何使用 Telegram 的 BotFather 创建 bot,如何安装 bot 的依赖项,以及如何运行 bot 并确保其正常工作。在下一章中,您将深入了解 Docker,了解关于 Dockerfiles 的更多信息,并通过为其编写 Dockerfile 来将 Newsbot 应用容器化。

四、了解 Dockerfile 文件

现在您对 Docker 及其相关术语有了更好的理解,本章将向您展示如何使用 Docker 将您的项目转换成容器化的应用。在本章中,您将学习 Dockerfile 是什么,包括它的语法,并学习如何编写 Dockerfile。对 Dockerfiles 有了更好的理解,你就可以开始为 Newsbot 应用编写 docker files 的第一步了。

Dockerfile First

对于传统部署的应用,构建和打包应用通常非常繁琐。为了自动化应用的构建和打包,人们求助于不同的工具,如 GNU Make、maven、Gradle 等等,来构建应用包。类似地,在 Docker 世界中,Docker 文件是构建 Docker 映像的自动化方式。

Docker 文件包含特殊指令,告诉 Docker 引擎构建映像所需的步骤。要使用 Docker 调用构建,可以发出Docker build命令。清单 4-1 显示了一个典型的 Dockerfile 文件。

FROM ubuntu:latest
LABEL author="sathyabhat"
LABEL description="An example Dockerfile"
RUN apt-get install python
COPY hello-world.py
CMD python hello-world.py

Listing 4-1A Typical Dockerfile

查看这个 Docker 文件,很容易看到我们告诉 Docker 引擎构建什么。但是,不要让简单性欺骗了你 Docker 文件让你在生成 Docker 图像时构建复杂的条件。当发出一个Docker build命令时,它从 Docker 文件和一个构建上下文构建 Docker 映像。

构建上下文

一个构建上下文是一个或一组在特定路径或 URL 上可用的文件。为了更好地理解这一点,假设您有一些在 Docker 映像构建期间需要的支持文件——例如,之前生成的特定于应用的配置文件,它需要成为容器的一部分。

构建上下文可以是本地的,也可以是远程的——您甚至可以将构建上下文设置为 Git 存储库的 URL,如果源文件与 Docker 守护进程不在同一个主机上,或者如果您想要测试特性分支,这将非常方便。您只需设置分支的上下文。build命令如下所示:

docker build https://github.com/sathyabhat/sample-repo.git#mybranch

类似地,要构建基于 Git 标签的图像,build命令应该是这样的:

docker build https://github.com/sathyabhat/sample-repo.git#mytag

通过拉取请求处理功能?想试试那个拉请求吗?没问题,您甚至可以将上下文设置为 pull 请求:

docker build https://github.com/sathyabhat/sample-repo.git#pull/1337/head

build命令将上下文设置为所提供的路径或 URL,上传 Docker 守护进程可用的文件,并允许它构建映像。您不限于 URL 或路径的构建上下文。如果您将一个 URL 传递给一个远程的 tarball(即一个.tar文件),位于该 URL 的 tarball 将被下载到 Docker 守护进程中,并发出build命令,以此作为构建上下文。

Caution

如果您在根(/)目录下提供 Docker 文件并将其设置为上下文,那么这样做会将您的硬盘内容传输到 Docker 守护进程。

Dockerignore

现在您应该明白,在构建过程中,构建上下文将当前目录的内容转移到 Docker 守护进程。考虑这样一种情况,其中上下文目录有许多与构建过程无关的文件/目录。上传这些文件/目录会导致网络流量显著增加。Dockerignore 文件很像 gitignore ,允许您定义在构建过程中免于传输的文件。

忽略列表由名为.dockerignore的文件提供,当 Docker CLI 找到该文件时,它会修改上下文以排除文件中提供的文件/模式。任何以散列(#)开头的内容都被视为注释并被忽略。下面的代码片段显示了一个示例.dockerignore文件,它不包括temp.git.DS_Store目录:

*/temp*
.DS_Store
.git

构建工具包

随着 Docker 引擎 18.09 版本的发布,Docker 使用 BuildKit 彻底检查了他们的容器构建系统。BuildKit 现在是 Docker 的默认构建系统。对于大多数用户来说,BuildKit 与遗留构建系统完全一样。BuildKit 为 Docker 映像构建提供了一个新的命令输出,因此提供了关于构建过程的更详细的反馈。

如果您看到与其他学习资源不同的输出,这意味着它们可能没有使用 BuildKit 的输出进行更新。BuildKit 还试图尽可能并行化构建步骤,因此您可以期待更快的构建速度,尤其是对于具有大量 Dockerfile 指令的容器。对于高级用户,BuildKit 还引入了将秘密传递到构建阶段的能力,而秘密不在最终层中。当使用 BuildKit 时,构建输出如清单 4-2 所示。(注意,由于空间限制,sha输出已被截断。)

docker build .
[+] Building 11.6s (6/6) FINISHED
 => [internal] load build definition from Dockerfile 0.1s
 => => transferring dockerfile: 84B   0.0s
 => [internal] load .dockerignore  0.1s
 => => transferring context: 2B 0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest 8.7s
 => [auth] library/ubuntu:pull token for registry-1.docker.io 0.0s
 => [1/1] FROM docker.io/library/ubuntu:latest@sha256:aba80b7 2.7s
 => => resolve docker.io/library/ubuntu:latest@sha256:aba80b7 0.0s
 => => sha256:aba80b7 1.20kB / 1.20kB 0.0s
 => => sha256:376209 529B / 529B  0.0s
 => => sha256:987317 1.46kB / 1.46kB 0.0s
 => => sha256:c549ccf8 28.55MB / 28.55MB  1.1s
 => => extracting sha256:c549ccf   1.2s
 => exporting to image 0.0s
 => => exporting layers   0.0s
 => => writing image sha256:f2afdc

Listing 4-2Build Output When BuildKit Is Enabled

在撰写本章时,仍然可以通过设置DOCKER_BUILDKIT标志切换回遗留构建过程,如清单 4-3 所示。

DOCKER_BUILDKIT=0 docker build .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM ubuntu:latest
latest: Pulling from library/ubuntu
c549ccf8d472: Already exists
Digest: sha256:aba80b77e27148d99c034a987e7da3a287ed455390352663418c0f2ed40417fe
Status: Downloaded newer image for ubuntu:latest
 ---> 9873176a8ff5
Step 2/2 : CMD echo Hello World!
 ---> Running in d5ca2635eecd
Removing intermediate container d5ca2635eecd
 ---> 77711564634f
Successfully built 77711564634f

Listing 4-3Switching Back to the Legacy Build Process

除非您遇到任何问题,否则我不建议切换回遗留构建过程。坚持使用 Docker 构建工具包。如果您没有看到新的构建输出,请确保您已经更新到 Docker 的最新版本。

使用 Docker Build 构建

稍后您将返回到示例 docker 文件。让我们先从一个简单的 Dockerfile 开始。将以下代码片段复制到一个文件中,并另存为Dockerfile:

FROM ubuntu:latest
CMD echo Hello World!

现在使用docker build命令构建这个图像。您将看到如清单 4-4 所示的响应。(请注意,sha输出已被截断。)

 docker build .
[+] Building 11.6s (6/6) FINISHED
 => [internal] load build definition from Dockerfile0.1s
 => => transferring dockerfile: 84B  0.0s
 => [internal] load .dockerignore 0.1s
 => => transferring context: 2B0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest 8.7s
 => [auth] library/ubuntu:pull token for registry-1.docker.io 0.0s
 => [1/1] FROM docker.io/library/ubuntu:latest@sha256:aba80b7 2.7s
 => => resolve docker.io/library/ubuntu:latest@sha256:aba80b7 0.0s
 => => sha256:aba80b7 1.20kB / 1.20kB 0.0s
 => => sha256:376209 529B / 529B 0.0s
 => => sha256:987317 1.46kB / 1.46kB 0.0s
 => => sha256:c549ccf8 28.55MB / 28.55MB 1.1s
 => => extracting sha256:c549ccf  1.2s
 => exporting to image0.0s
 => => exporting layers  0.0s
 => => writing image sha256:f2afdc

Listing 4-4Response from Docker Engine as it Builds the Dockerfile

您可以看到 Docker 构建是分步进行的,每一步都对应于 Docker 文件中的一条指令。现在再次尝试构建过程。

docker build .
[+] Building 0.1s (5/5) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B   0.0s
=> [internal] load .dockerignore  0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest 0.0s
=> CACHED [1/1] FROM docker.io/library/ubuntu:latest   0.0s
=> exporting to image 0.0s
=> => exporting layers   0.0s
=> => writing image sha256:f2afdcc   0.0s

请注意第二次构建过程的速度有多快。Docker 已经缓存了层,不用再拉了。要运行这个映像,使用docker run命令,后跟映像 ID f2afdcc:

docker run f2afdcc
Hello World!

因此,Docker 运行时能够启动一个容器并运行由CMD指令定义的命令;因此,您得到了输出。现在,通过键入图像 ID 从图像启动容器变得很快。您可以用一个容易记住的名称来标记图像,这样会更容易。您可以通过使用docker tag命令来做到这一点,如下所示:

docker tag <image id> <tag name>
docker tag f2afdcc sathyabhat/hello-world

在下一节中,您将更深入地了解标记。Docker 还验证 docker 文件是否有有效的指令,以及它们的语法是否正确。考虑前面的 Dockerfile 文件,如清单 4-5 所示。

FROM ubuntu:latest
LABEL author="sathyabhat"
LABEL description="An example Dockerfile"
RUN apt-get install python
COPY hello-world.py
CMD python hello-world.py

Listing 4-5Dockerfile for Python with an Invalid Instruction

如果您尝试构建这个 Docker 文件,Docker 会报错,如下所示:

docker build -f Dockerfile.invalid .
[+] Building 0.1s (2/2) FINISHED
=> [internal] load build definition from Dockerfile.invalid  0.0s
=> => transferring dockerfile: 336B  0.0s
=> [internal] load .dockerignore  0.0s
=> => transferring context: 2B 0.0s
failed to solve with frontend dockerfile.v0: failed to create LLB definition: dockerfile parse error line 6:
COPY requires at least two arguments, but only one was provided. Destination could not be determined.

在本章的稍后部分,您将回到解决这个问题。现在,是时候看看一些常用的 Dockerfile 指令和标记图像了。

标签

标签是唯一标识 Docker 图像的特定版本的名称。标签是纯文本标签,通常用于标识特定的细节,如版本、映像的基本操作系统或 Docker 映像的架构。标记 Docker 映像使您能够灵活地引用特定版本,这样,如果当前映像没有按预期工作,就可以更容易地回滚到 Docker 映像的以前版本。

如果没有指定标签,Docker 将应用一个名为"latest"的字符串作为默认标签。标签通常是许多问题的来源,尤其是对于 Docker 的新用户。许多人认为将"latest"作为标签意味着 Docker 图像是图像的最新版本,并且会一直更新到最新版本。这是不正确的— latest被选为惯例,但并没有任何特殊的意义。

我不建议使用latest作为标签,尤其是对于生产工作负载。在开发阶段,省略标签将导致"latest"标签被应用到每个构建中。如果有重大变化,由于标签是通用的,先前的图像将被覆盖。这使得回滚到映像的前一个版本非常困难,除非您注意到了映像的 SHA-hash。使用特定的标签可以更容易地确定容器上运行的是哪个标签或 Docker image 的哪个版本。使用特定的标签也减少了破坏性改变被传播的机会,特别是如果你把你的图像标记为latest,并且有一个破坏性的改变或错误。下次您的容器崩溃或重启时,它可能会提取带有重大更改或错误的图像。

可以使用docker tag命令标记和重新标记 Docker 图像:

docker tag <image id> <tag name>
docker tag f2afdcc sathyabhat/hello-world

标记名通常会有 Docker 注册表作为标记名的前缀。如果没有指定注册表名称,Docker 将假设该映像是 Docker Hub 的一部分,并将尝试从那里提取它。标签可以作为构建过程的一部分通过传递-t标志来分配,如清单 4-6 所示。

docker build -t sathyabhat/helloworld .

[+] Building 0.2s (5/5) FINISHED
=> [internal] load build definition from Dockerfile0.0s
=> => transferring dockerfile: 37B  0.0s
=> [internal] load .dockerignore 0.1s
=> => transferring context: 2B0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest0.0s
=> CACHED [1/1] FROM docker.io/library/ubuntu:latest  0.0s
=> exporting to image 0.0s
=> => exporting layers  0.0s
=> => writing image sha256:f2afdcc 0.0s
=> => naming to docker.io/sathyabhat/helloworld

Listing 4-6Adding a Tag When Building the Image

请注意,尽管您没有将docker.io作为标记的一部分,但是它被添加到了标记名的前面。最后一行告诉您该图像已被成功标记。您可以通过搜索docker images来验证这一点:

docker images sathyabhat/helloworld
REPOSITORY              TAG      IMAGE ID        CREATED      SIZE
sathyabhat/helloworld   latest   f2afdccf8eeb   3 weeks ago   72.7MB

dockerfile instructions(Docker 文件说明)

当查看 Dockerfile 文件时,您很可能会遇到以下指令。

  • FROM

  • ADD

  • COPY

  • RUN

  • CMD

  • ENTRYPOINT

  • ENV

  • VOLUME

  • LABEL

  • EXPOSE

让我们看看他们怎么做。

正如您之前了解到的,每个图像都需要从基础图像开始。FROM指令告诉 Docker 引擎用于后续指令的基本映像。每个有效的 Dockerfile 必须以一个FROM指令开始。语法如下:

FROM <image> [AS <name>]

运筹学

FROM <image>[:<tag>] [AS <name>]

运筹学

FROM <image>[@<digest>] [AS <name>]

其中<image>是来自任何公共/私有存储库的有效 Docker 映像的名称。如上所述,如果标签被跳过,Docker 将获取标签为latest的图像。

工作目录

WORKDIR指令为RUNCMDENTRYPOINTCOPYADD指令设置当前工作目录。当您在源代码中有多个目录,并且您希望在这些特定的目录中完成一些特定的操作时,WORKDIR非常有用。WORKDIR也常用来为应用在容器中运行设置一个单独的位置。语法如下:

WORKDIR /path/to/directory

WORKDIR可以在 Dockerfile 文件中多次设置,如果相对目录在前面的WORKDIR指令之后,它将相对于前面设置的工作目录。让我们看一个例子来证明这一点。

考虑这个 Dockerfile 文件:

FROM ubuntu:latest
WORKDIR /app
CMD pwd

Dockerfile 从 Ubuntu 获取latest标记的图像作为基础图像,将当前工作目录设置为/app,并在图像运行时运行pwd命令。pwd命令打印当前工作目录。

让我们试着构建并运行它,并检查输出:

docker build -t sathybhat/workdir .
[+] Building 0.7s (6/6) FINISHED
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 36B 0.0s
 => [internal] load .dockerignore0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest  0.6s
 => [1/2] FROM docker.io/library/ubuntu:latest@sha256:b3e2e4  0.0s
 => CACHED [2/2] WORKDIR /app 0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:f8853df 0.0s
 => => naming to docker.io/sathybhat/workdir

现在您运行新构建的映像:

docker run sathybhat/workdir
/app

pwd的结果表明,通过WORKDIR指令将当前工作目录设置为/app。修改 Dockerfile 文件以添加几条WORKDIR指令,如下所示:

FROM ubuntu:latest
WORKDIR /usr
WORKDIR src
WORKDIR app
CMD pwd

让我们构建并运行新的映像:

docker build -t sathybhat/workdir .

[+] Building 0.7s (8/8) FINISHED
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 121B  0.0s
 => [internal] load .dockerignore 0.0s
 => => transferring context: 2B 0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest  0.6s
 => [1/4] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47  0.0s
 => CACHED [2/4] WORKDIR /usr 0.0s
 => CACHED [3/4] WORKDIR src  0.0s
 => CACHED [4/4] WORKDIR app  0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:207b405  0.0s
 => => naming to docker.io/sathyabhat/workdir

请注意,图像 ID 已经更改,因此这是一个使用相同标记构建的新图像:

docker run sathybhat/workdir
/usr/src/app

不出所料,相对目录的WORKDIR指令已经附加到初始绝对目录集。默认情况下,WORKDIR被设置为/,所以任何具有相对目录的WORKDIR指令将被附加到/。这里有一个例子来说明这一点。让我们按如下方式修改 Dockerfile 文件:

FROM ubuntu:latest
WORKDIR var
WORKDIR log/nginx
CMD pwd

建立形象:

docker build -t sathyabhat/workdir .

[+] Building 1.8s (8/8) FINISHED
 => [internal] load build definition from Dockerfile   0.0s
 => => transferring dockerfile: 115B   0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest  1.6s
 => [auth] library/ubuntu:pull token for registry-1.docker.io  0.0s
 => CACHED [1/3] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47 0.0s
 => [2/3] WORKDIR var   0.0s
 => [3/3] WORKDIR log/nginx   0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:e7ded5d 0.0s
 => => naming to docker.io/sathyabhat/workdir

现在运行它:

docker run sathyabhat/workdir
/var/log/nginx

注意,您没有在 Dockerfile 文件中设置任何绝对工作目录——相对目录被附加到默认目录中。

添加并复制

乍一看,ADDCOPY指令似乎是相同的——它们允许您将文件从主机传输到容器的文件系统。COPY支持将文件基本复制到容器,而ADD支持 tarball 自动提取(即 Docker 将自动提取从本地目录添加的压缩文件)和远程 URL 支持(即 Docker 将从远程 URL 下载资源)等功能。

两者的语法非常相似:

ADD <source> <destination>
COPY  <source> <destination>

当您从远程 URL 添加文件或者您有来自本地文件系统的压缩文件需要自动提取到容器文件系统中时,ADD指令很有用。

例如,下面的COPY指令将一个名为hugo的文件复制到容器中的/app目录:

COPY hugo /app/

下面的ADD指令从 URL 获取一个名为hugo_0.88.0_Linux-64bit.tar.gz的压缩文件,但不会自动解压缩该文件:

ADD https://github.com/gohugoio/hugo/releases/download/v0.88.0/hugo_0.88.0_Linux-64bit.tar.gz /app/

而下面的ADD指令会将压缩文件的内容复制并自动提取到容器中的/app目录。

ADD hugo_0.88.0_Linux-64bit.tar.gz /app/

对于用于构建 Linux 容器的 owner 文件,这两条指令都允许您更改添加到容器中的文件的所有者/组。这是使用--chown标志完成的,如下所示:

ADD --chown=<user>:<group> <source> <destination>
COPY --chown=<user>:<group> <source> <destination>

例如,如果您想将当前工作目录中的requirements.txt添加到/usr/share/app目录中,指令如下:

ADD requirements.txt /usr/share/app
COPY  requirements.txt /usr/share/app

在指定模式时,ADDCOPY都支持通配符。例如,在 docker 文件中包含以下指令将使用。py扩展到/apps/目录的镜像。

ADD *.py /apps/

COPY *.py /apps/

Note

Docker 建议使用COPY而不是ADD,尤其是当它是一个正在被复制的本地文件时。

选择COPY还是ADD有几点需要考虑。在COPY指令的情况下:

  • 如果<destination>在图像中不存在,它将被创建。

  • 所有新文件/目录都是以 UID 和 GID 作为0创建的,即作为 root 用户。要改变这一点,您可以使用--chown标志。

  • 如果文件/目录包含特殊字符,需要对它们进行转义。

  • <destination>可以是绝对或相对路径。在相对路径的情况下,相对性将从WORKDIR指令设置的路径中推断出来。

  • 如果<destination>不以斜杠结尾,它将被认为是一个文件,并且<source>的内容将被写入<destination>

  • 如果将<source>指定为通配符模式,则<destination>必须是一个目录,并且必须以尾随斜杠结束;否则,构建过程将会失败。

  • <source>必须在构建上下文中。它不能是构建上下文之外的文件/目录,因为 Docker 构建过程的第一步涉及到将上下文目录发送到 Docker 守护进程。

ADD指令的情况下:

  • 如果<source>是一个 URL,而<destination>不是一个目录,并且不以斜杠结尾,那么文件从 URL 下载并复制到<destination>

  • 如果<source>是一个 URL,而<destination>是一个目录并以斜杠结尾,则从 URL 中推断出文件名,并将文件下载并复制到<destination>/<filename>

  • 如果<source>是一个已知压缩格式的本地 tarball,tarball 被解压为一个目录。然而,远程 tarballs 不是未压缩的。

奔跑

RUN指令将在容器的构建步骤中执行任何命令。这将创建一个新层,可用于 docker 文件中的后续步骤。值得注意的是,跟随RUN指令的命令仅在构建映像时运行。当一个容器已经启动并正在运行时,RUN指令没有任何意义。

RUN有两种形式,shell 形式和 exec 形式。在 shell 形式中,该命令以空格分隔,如下所示:

RUN <command>

这种形式使得在RUN指令本身中使用外壳变量、子命令、命令管道和命令链成为可能。

考虑一个场景,您想要将内核发布版本嵌入到 Docker 映像的home目录中。您可以使用uname –rv命令获得内核发布和版本。这个输出可以使用 echo 打印出来,然后重定向到映像的home目录中一个名为 kernel-info 的文件。您可以使用 shell 形式的 RUN 指令来实现这一点,如下所示:

RUN echo `uname -rv` > $HOME/kernel-info

在 exec 格式中,该命令用逗号分隔并用引号括起来,如下所示:

RUN ["executible", "parameter 1", " parameter 2"] (the exec form)

除非您需要使用链接和重定向之类的 shell 特性,否则建议对RUN指令使用 exec 格式。

图层缓存

构建图像时,Docker 将缓存它提取的层。从构建日志中可以明显看出这一点。考虑以下 Dockerfile 文件:

FROM ubuntu:latest
RUN apt-get update

运行docker build时的构建日志如下所示:

docker build -f Dockerfile .
[+] Building 8.1s (7/7) FINISHED
 => [internal] load build definition from Dockerfile  0.1s
 => => transferring dockerfile: 96B 0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest  1.8s
 => [auth] library/ubuntu:pull token for registry-1.docker.io  0.0s
 => CACHED [1/2] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47 0.0s
 => [2/2] RUN apt-get update  6.0s
 => exporting to image  0.2s
 => => exporting layers 0.1s
 => => writing image sha256:a9824f6

日志表明,Docker 使用保存到磁盘的缓存层,而不是重新下载基本 Ubuntu 映像的层。这适用于所有创建的层——每当 Docker 遇到RUNCOPYADD指令时,它都会创建一个新层。拥有正确的指令顺序可以大大提高 Docker 是否会重用这些层。这不仅可以提高映像构建速度,还可以通过减少下载层数来减少容器启动时间。

由于层缓存的工作方式,最好将包更新和包安装链接成一条RUN指令。考虑一个 Dockerfile 文件,其中的运行指令如下所示:

RUN apt-get update
RUN apt-get install pkg1
RUN apt-get install pkg2
RUN apt-get install pkg3

当 Docker 构建这个映像时,它缓存由四个RUN命令创建的四个层。为了减少层数,并防止由于软件包缓存过期而无法安装软件包,最好将更新和安装链接起来,如下所示:

RUN apt-get update && apt-get install -y \
   pkg1 \
   pkg2 \
   pkg3 \
   pkg4

这将创建一个包含要安装的软件包的层,任何软件包中的任何更改都将使缓存无效,并导致使用更新的软件包创建一个新层。如果你想明确地指示 Docker 避免使用缓存,那么将--no-cache标志传递给docker build命令将跳过使用缓存。

CMD 和入口点

CMDENTRYPOINT指令定义运行容器时执行哪个命令。两者的语法如下所示:

CMD ["executable","param1","param2"] (exec form)
CMD ["param1","param2"] (as default parameters to ENTRYPOINT)
CMD command param1 param2 (shell form)
ENTRYPOINT ["executable", "param1", "param2"] (exec form)
ENTRYPOINT command param1 param2 (shell form)

当您希望您的容器像可执行文件一样运行时,ENTRYPOINT指令是最好的,并且CMD指令为正在执行的容器提供缺省值。考虑下面显示的 Dockerfile 文件:

FROM ubuntu:latest
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*
CMD ["curl"]

在这个 Docker 镜像中,Ubuntu 是基础镜像,curl安装在其上,curlCMD指令的参数。这意味着当容器被创建并运行时,它将不带任何参数地运行curl。让我们为 docker 文件构建如下所示的图像:

docker build –t sathyabhat/curl .

[+] Building 11.8s (6/6) FINISHED
 => [internal] load build definition from Dockerfile 0.0s
 => => transferring dockerfile: 50B  0.0s
 => [internal] load .dockerignore 0.0s
 => => transferring context: 2B   0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest   0.7s
 => CACHED [1/2] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47  0.0s
 => [2/2] RUN apt-get update &&   apt-get install -y curl 10.7s
 => exporting to image   0.3s
 => => exporting layers  0.3s
 => => writing image sha256:8a9fc4b  0.0s
 => => naming to docker.io/sathyabhat/curl

您可以在运行容器时看到结果:

docker run sathyabhat/curl
curl: try 'curl --help' or 'curl --manual' for more information

这是因为curl期望传递一个参数。您可以通过向docker run命令传递参数来覆盖CMD指令。例如,尝试curl wttr.in,它获取当前天气。

docker run sathyabhat/curl wttr.in
docker: Error response from daemon: OCI runtime create failed: container_linux.go:296: starting container process caused "exec: \"wttr.in\": executable file not found in $PATH": unknown.

啊哦,一个错误。如上所述,docker run之后的参数用于覆盖CMD指令。然而,您只传递了wttr.in作为参数,而不是可执行文件本身。为了让覆盖正常工作,您需要传入可执行文件,也就是curl:

docker run sathyabhat/curl -s wttr.in
Weather report: Gurgaon, India

               Haze
  _ - _ - _ -  24-25 °C
   _ - _ - _   ↖ 13 km/h
  _ - _ - _ -  3 km
               0.0 mm

每次传递一个可执行文件来覆盖一个参数是非常乏味的。这就是ENTRYPOINTCMD组合的闪光之处。您可以将ENTRYPOINT设置为可执行文件,同时该参数可以从命令行传递并将被覆盖。

按如下方式修改 Dockerfile 文件:

FROM ubuntu:latest
RUN apt-get update && \
apt-get install -y curl
ENTRYPOINT ["curl", "-s"]

再次构建图像:

docker build -t sathyabhat/curl .

[+] Building 0.7s (6/6) FINISHED
 => [internal] load build definition from Dockerfile.listing-4-x-5 0.0s
 => => transferring dockerfile: 157B 0.0s
 => [internal] load .dockerignore 0.0s
 => => transferring context: 2B   0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest   0.6s
 => [1/2] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47   0.0s
 => CACHED [2/2] RUN apt-get update &&   apt-get install -y curl 0.0s
 => exporting to image   0.0s
 => => exporting layers  0.0s
 => => writing image sha256:7e31728  0.0s
 => => naming to docker.io/sathyabhat/curl

现在,您可以通过将 URL 作为参数传递来curl任何 URL,而不必添加可执行文件。

docker run sathyabhat/curl wttr.in
Weather report: Gurgaon, India

               Haze
  _ - _ - _ -  24-25 °C
   _ - _ - _   ↖ 13 km/h
  _ - _ - _ -  3 km
               0.0 mm

当然,curl在这里只是一个例子。您可以用任何其他接受参数的程序来替换curl(比如负载测试工具、基准测试工具等。)并且CMDENTRYPOINT的组合使得分发图像变得容易。

请注意,ENTRYPOINT必须以 exec 形式提供——以 shell 形式编写意味着参数没有被正确传递,不会按预期工作。表 4-1 来自Docker 工人参考指南。解释了允许的ENTRYPOINT / CMD组合的矩阵,假设p1_cmdp1_entryp2_cmdp2_entry是您想要在容器中运行的p1p2命令的CMDENTRYPOINT变体。

表 4-1

ENTRYPOINT / CMD组合命令

|   |

ENTRYPOINT

|

ENTRYPOINT exec_entry p1_entry

|

ENTRYPOINT ["exec_entry", "p1_entry"]

|
| --- | --- | --- | --- |
| 否CMD | 错误,不允许 | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry |
| CMD ["exec_cmd", "p1_cmd"] | exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry exec_cmd p1_cmd |
| CMD ["p1_cmd", "p2_cmd"] | p1_cmd p2_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry p1_cmd p2_cmd |
| CMD exec_cmd p1_cmd | /bin/sh -c exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd |

关于 shell 和 exec 表单,需要记住以下几点:

  • 如前所述,您可以在 shell 形式和 exec 形式中指定RUNCMDENTRYPOINT。应该使用哪一种将完全取决于需求。但是作为一般指南:

    • 在 shell 形式中,命令在 shell 中运行,并将命令作为参数。这种形式提供了一个外壳,其中外壳变量、子命令、命令管道和链接都是可能的。

    • 在 exec 形式下,命令不调用命令外壳。这意味着正常的壳体加工(如$VARIABLE替换、配管等。)就不行了。

  • 以 shell 形式启动的程序将作为/bin/sh -c的子命令运行。这意味着可执行文件不会作为 PID 运行,也不会接收 UNIX 信号。因此,发送SIGTERM的 Ctrl+C 不会被转发到容器,应用可能无法正确退出。

包封/包围(动词 envelop 的简写)

ENV指令为图像设置环境变量。ENV指令有两种形式:

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

在第一种形式中,<key>之后的整个字符串被认为是值,包括空白字符。在此表单中,每行只能设置一个变量。

在第二种形式中,可以一次设置多个变量,用等号(=)字符给键赋值。

环境变量集在容器运行时保持不变。可以使用docker inspect查看它们。

考虑这个 Dockerfile 文件:

FROM ubuntu:latest
ENV LOGS_DIR="/var/log"
ENV APPS_DIR /apps/

构建 Docker 映像:

docker build  -t sathyabhat/env .
[+] Building 1.7s (6/6) FINISHED
 => [internal] load build definition from Dockerfile.listing-4-x-6   0.0s
 => => transferring dockerfile: 50B 0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest  1.6s
 => [auth] library/ubuntu:pull token for registry-1.docker.io  0.0s
 => CACHED [1/1] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47 0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:23eb815 0.0s
 => => naming to docker.io/sathyabhat/env

您可以使用以下命令检查环境变量:

docker inspect sathyabhat/env | jq ".[0].Config.Env"

输出如下所示:

[
 "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "LOGS_DIR=/var/log",
  "APPS_DIR=/apps/"
]

当运行带有-e标志的容器时,可以更改为容器定义的环境变量。在前面的示例中,将容器的LOGS_DIR值更改为/logs。这可以通过键入以下命令来实现:

docker run -it -e LOGS_DIR="/logs" sathyabhat/env

您可以通过在终端键入以下命令来确认更改后的值:

printenv | grep LOGS
LOGS_DIR=/logs

键入exit关闭容器的交互终端。要分配多个环境变量,使用-e标志传递额外的环境变量,就像传递第一个环境变量一样。在前面的例子中,如果您想覆盖LOGS_DIRAPPS_DIR,可以使用下面的命令:

docker run -it -e LOGS_DIR="/logs" -e APPS_DIR="/opt" sathyabhat/env

printenv | grep DIR
LOGS_DIR=/logs
APPS_DIR=/opt

键入exit关闭容器的交互终端。

VOLUME指令告诉 Docker 在容器上创建一个挂载点,并从主机外部挂载它。例如,这样的指令:

VOLUME /var/logs/nginx

告诉 Docker 将/var/logs/nginx目录标记为挂载点,从 Docker 主机挂载数据。当与 Docker run命令上的卷标志结合使用时,这将导致数据作为卷保存在 Docker 主机上。然后,可以使用 Docker CLI 命令备份、移动或转移该卷。在本书后面的章节中,你会学到更多关于卷的知识。

揭露

EXPOSE指令告诉 Docker 容器在运行时监听指定的网络端口。语法如下:

EXPOSE <port> [<port>/<protocol>...]

例如,如果你想暴露端口 80,EXPOSE指令如下:

EXPOSE 80

如果要在 TCP 和 UDP 上公开端口 53,Dockerfile 指令如下:

EXPOSE 53/tcp
EXPOSE 53/udp

您还可以包括端口号以及端口是侦听 TCP/UDP 还是同时侦听两者。如果没有指定,Docker 假定协议是 TCP。

Note

一个EXPOSE指令不发布端口。对于要向主机发布的端口,您需要使用带有docker run-p标志来发布和映射端口。

这里有一个示例 Dockerfile 文件,它使用了端口 80 暴露在容器中的nginx图像。

FROM nginx:alpine
EXPOSE 80

构建容器:

[+] Building 0.4s (5/5) FINISHED
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 50B 0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/nginx:alpine   0.2s
 => CACHED [1/1] FROM docker.io/library/nginx:alpine@sha256:9152859  0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:33fcd52 0.0s
 => => naming to docker.io/sathyabhat/web

要运行这个容器,您必须提供它要映射到的主机端口。将其映射到主机上的端口 8080,再映射到容器的端口 80。为此,请键入以下命令:

docker run -d -p 8080:80 sathyabhat:web

-d标志使nginx容器在后台运行,-p标志进行端口映射。确认容器正在运行:

curl http://localhost:8080
<!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>

标签

LABEL指令将元数据作为键值对添加到图像中。

LABEL <key>=<value> <key>=<value> <key>=<value> …

一个图像可以有多个标签,通常用于添加一些元数据,以帮助搜索和组织图像和其他 Docker 对象。Docker 推荐以下指南。

  • 对于钥匙:

    • 第三方工具的作者应该用他们拥有的域名的反向 DNS 符号作为每个密钥的前缀:例如,com.sathyasays.my-image

    • com.docker.*, io.docker.*,org.dockerproject.*由 Docker 保留供内部使用。

    • 标签键应该以小写字母开始和结束,并且应该只包含小写字母数字字符和句点(。)和连字符(-)。不允许使用连续的连字符和句点。

    • 句号(。)分隔名称空间字段。

  • 对于值:

    • 标签值可以包含任何可以表示为字符串的数据类型,包括 JSON、XML、YAML 和 CSV 类型。

撰写文档的指南和建议

以下是 Docker 推荐的一些编写 Docker 文件的指导原则和最佳实践。

  • 容器应该是短暂的。Docker 建议由 docker 文件生成的图像应该尽可能短暂。您应该能够在任何时候停止、销毁和重启容器,只需对容器进行最少的设置和配置。理想情况下,容器不应该将数据写入容器文件系统,任何持久数据都应该写入 Docker 卷或容器外部管理的数据存储(例如,使用像亚马逊 S3 这样的块存储)。

  • 保持构建环境最小化。您在本章前面已经了解了构建上下文。重要的是尽可能保持最小的构建上下文,以减少构建时间和图像大小。这可以通过有效利用.dockerignore文件来实现。

  • 使用多阶段构建。多阶段构建有助于大幅减少映像的大小,而不必编写复杂的脚本来传输/保留所需的工件。下一节将介绍多阶段构建。

  • 跳过不想要的包裹。拥有不需要的或有用的包会增加映像的大小,引入不需要的依赖包,并增加攻击的表面积。

  • 尽量减少层数。虽然不像以前那样令人担忧,但减少图像中的图层数量仍然很重要。从 Docker 1.10 及以上版本开始,只有RUNCOPYADD指令创建层。考虑到这一点,使用最少的指令或者组合多行相应的指令可以减少层数,最终减小图像的大小。

使用多阶段构建

从 17.05 及更高版本开始,Docker 增加了对多阶段构建的支持,允许执行复杂的映像构建,而不会使 Docker 映像不必要地臃肿。当您构建需要一些额外的构建时依赖项但在运行时不需要的应用映像时,多阶段构建特别有用。最常见的例子是使用编程语言(如 Go 或 Java)编写的应用,在多阶段构建之前,通常有两个不同的 docker 文件。一个用于构建,另一个用于从构建时映像到运行时映像的工件的发布和编排。

通过多阶段构建,可以利用单个 docker 文件来构建和部署映像——构建映像可以包含生成二进制文件或工件所需的构建工具。在第二个阶段,工件可以被复制到运行时映像,从而大大减小运行时映像的大小。对于一个典型的多阶段构建,一个构建阶段有几层—每一层用于安装构建应用、生成依赖项和生成应用所需的工具。在最终层中,从构建阶段构建的应用被复制到最终层,并且只有该层被考虑用于构建映像。构建层被丢弃,大大减小了最终图像的大小。

虽然这本书没有详细介绍多阶段构建,但您将尝试一个如何创建多阶段构建的练习,看看使用多阶段构建的苗条图像会使最终图像变得多小。有关多阶段构建的更多详细信息,请访问 Docker 网站 https://docs.docker.com/develop/develop-images/multistage-build/

练习

Building a Simple Hello World Docker Image

本章开头介绍了一个简单的 docker 文件,由于语法错误,该文件没有构建。在本练习中,您将看到如何修复 docker 文件并添加您在本章中学到的一些指令。

提示源代码和相关的 Dockerfile 可以在本书的 GitHub repo 上获得,在 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-4/exercise-1目录下。

原始 Dockerfile 文件如下:

FROM ubuntu:latest
LABEL author="sathyabhat"
LABEL description="An example Dockerfile"
RUN apt-get install python
COPY hello-world.py
CMD python hello-world.py

尝试构建它会导致错误,因为hello-world.py丢失了。让我们修复构建错误。为此,您需要添加一个读取环境变量NAME并打印Hello, $NAME!hello-world.py。如果没有定义环境变量,它将打印"Hello, World!"

hello-world.py的内容如下:

#!/usr/bin/env python3
from os import getenv

if getenv('NAME') is None:
        name = 'World'
else:
        name = getenv('NAME')
print(f"Hello, {name}!")

更正后的 Dockerfile 文件如下:

FROM python:3-alpine
LABEL description="Dockerfile for Python script which prints Hello, Name"
COPY hello-world.py /app/
ENV NAME=Readers
CMD python3 /app/hello-world.py

构建 Dockerfile 文件:

docker build -t sathyabhat/chap04-ex1 .
[+] Building 1.9s (8/8) FINISHED
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 37B 0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/python:3-alpine   1.7s
 => [auth] library/python:pull token for registry-1.docker.io  0.0s
 => [internal] load build context   0.0s
 => => transferring context: 36B 0.0s
 => [1/2] FROM docker.io/library/python:3-alpine@sha256:3998e97  0.0s
 => CACHED [2/2] COPY hello-world.py /app/   0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:538be87 0.0s
 => => naming to docker.io/sathyabhat/chap04-ex1

确认图像名称和大小:

docker images sathyabhat/chap04-ex1
REPOSITORY             TAG     IMAGE ID      CREATED      SIZE
sathyabhat/chap04-ex1  latest  538be873d192  3 hours ago  45.1MB

运行 Docker 映像:

docker run sathyabhat/chap04-ex1
Hello, Readers!

尝试在运行时覆盖环境变量。您可以通过为-e参数提供docker run来做到这一点:

docker run -e NAME=all sathyabhat/chap04-ex1
Hello, all!

恭喜你!您已经成功编写了您的第一个 Docker 文件,并构建了您的第一个 Docker 映像。

A Look at Slim Docker Release Image (Using Multi-Stage Builds)

在本练习中,您将构建两个 Docker 映像。第一个图像使用标准构建,以python:3作为基础图像,而第二个图像给出了如何利用多阶段构建的概述。

提示https://github.com/Apress/practical-docker-with-python ,在source-code/chapter-4/exercise-2/ directory的 GitHub repo 上可以获得该书的源代码和相关 Dockerfile。

使用标准构建构建 Docker 映像

用以下内容创建一个requirements.txt file:

praw==3.6.0

创建一个包含以下内容的 Dockerfile 文件:

FROM python:3
COPY requirements.txt .
RUN pip install -r requirements.txt

现在构建 Docker 映像:

[+] Building 7.2s (8/8) FINISHED
 => [internal] load build definition from Dockerfile   0.3s
 => => transferring dockerfile: 114B 0.0s
 => [internal] load .dockerignore 0.3s
 => => transferring context: 2B   0.0s
 => [internal] load metadata for docker.io/library/python:3  0.0s
 => [internal] load build context 0.6s
 => => transferring context: 54B  0.0s
 => [1/3] FROM docker.io/library/python:3  1.6s
 => [2/3] COPY requirements.txt . 0.2s
 => [3/3] RUN pip install -r requirements.txt 3.3s
 => exporting to image   1.6s
 => => exporting layers  1.5s
 => => writing image sha256:03191af  0.0s
 => => naming to docker.io/sathyabhat/base-build

映像构建成功!让我们确定图像的大小:

|

贮藏室ˌ仓库

|

标签

|

图像 ID

|

创造

|

大小

|
| --- | --- | --- | --- | --- |
| sathyabhat/base-build | latest | 03191af | 大约一分钟前 | 895MB |

docker images sathyabhat/base-build

Docker 映像有相当大的 895MB,尽管您没有添加任何应用代码,只是添加了一个依赖项。让我们把它重写为一个多阶段构建。

使用多阶段构建构建 Docker 映像

FROM python:3 as python-base
COPY requirements.txt .
RUN pip install -r requirements.txt

FROM python:3-alpine
COPY --from=python-base /root/.cache /root/.cache
COPY --from=python-base requirements.txt .
RUN pip install -r requirements.txt && rm -rf /root/.cache

Dockerfile 文件的不同之处在于有多个FROM语句,表示不同的阶段。在第一阶段,您使用python:3映像构建所需的包,它具有必要的构建工具。

在第二阶段,您复制第一阶段安装的文件,重新安装它们(注意这一次 pip 获取缓存的文件,不再构建它们),然后删除缓存的安装文件。构建日志如下所示:

[+] Building 0.6s (13/13) FINISHED
 => [internal] load build definition from Dockerfile  0.2s
 => => transferring dockerfile: 35B 0.0s
 => [internal] load .dockerignore .1s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/python:3-alpine .2s
 => [internal] load metadata for docker.io/library/python:3 0.0s
 => [internal] load build context .1s
 => => transferring context: 37B 0.0s
 => [stage-1 1/4] FROM docker.io/library/python:3-alpine@sha256:3998e97 0.0s
 => [python-base 1/3] FROM docker.io/library/python:3 0.0s
 => CACHED [python-base 2/3] COPY requirements.txt .  0.0s
 => CACHED [python-base 3/3] RUN pip install -r requirements.txt  0.0s
 => CACHED [stage-1 2/4] COPY --from=python-base /root/.cache /root/.cache 0.0s
 => CACHED [stage-1 3/4] COPY --from=python-base requirements.txt .  0.0s
 => CACHED [stage-1 4/4] RUN pip install -r requirements.txt && rm -rf /root/.cache 0.0s
 => exporting to image  0.1s
 => => exporting layers 0.0s
 => => writing image sha256:35c85a8 0.0s
 => => naming to docker.io/sathyabhat/multistage-build

使用docker images检查图像的大小,您会发现使用多阶段构建已经大大减小了图像的大小。这意味着图像尺寸减小,应用启动更快,甚至成本降低,因为您节省了提取容器图像所需的带宽。

|

贮藏室ˌ仓库

|

标签

|

图像标识

|

创造

|

大小

|
| --- | --- | --- | --- | --- |
| sathyabhat/multistage-build | latest | 35c85a8497b5 | 大约一分钟前 | 54.2 兆字节 |

docker images sathyabhat/multistage-build

Writing a Dockerfile For Newsbot

在本练习中,您将为 Newsbot(电报聊天机器人项目)编写 Dockerfile。

提示源代码和相关的 docker 文件可以在 https://github.com/Apress/practical-docker-with-python 的 GitHub repo 上的source-code/chapter-4/exercise-3/目录下获得。

让我们回顾一下您对这个项目的需求:

  • 一个基于 Python 3 的 Docker 图像

  • requirements.txt中列出的项目依赖关系

  • 名为NBT_ACCESS_TOKEN的环境变量

现在您已经有了您需要的东西,您可以编写 Dockerfile 文件了。编写 Dockerfile 文件的一般步骤如下

  1. 从一个合适的基础图像开始。

  2. 列出应用所需的文件列表。

  3. 列出应用所需的环境变量。

  4. 使用COPY指令将应用文件复制到映像。

  5. ENV指令指定环境变量。

结合这些步骤,您将得到这个 Dockerfile 文件。

FROM python:3-alpine
WORKDIR /apps/subredditfetcher/
COPY . .
RUN ["pip", "install", "-r", "requirements.txt"]
CMD ["python", "newsbot.py"]

现在构建图像:

[+] Building 0.9s (9/9) FINISHED
 => [internal] load build definition from Dockerfile   0.1s
 => => transferring dockerfile: 182B 0.0s
 => [internal] load .dockerignore 0.2s
 => => transferring context: 2B   0.0s
 => [internal] load metadata for docker.io/library/python:3-alpine 0.4s
 => [1/4] FROM docker.io/library/python:3-alpine@sha256:3998e97 0.0s
 => [internal] load build context 0.1s
 => => transferring context: 392B 0.0s
 => CACHED [2/4] WORKDIR /apps/subredditfetcher/ 0.0s
 => CACHED [3/4] COPY . .   0.0s
 => CACHED [4/4] RUN ["pip", "install", "-r", "requirements.txt"]  0.0s
 => exporting to image   0.1s
 => => exporting layers  0.0s
 => => writing image sha256:783b4c0  0.0s
 => => naming to docker.io/sathyabhat/newsbot

现在运行容器。注意用你在第三章中创建的电报机器人 API 密匙替换<token>

docker run –e NBT_ACCESS_TOKEN=<token> sathyabhat/newsbot

您应该会看到 bot 的日志,以确保它正在运行:

INFO: <module> - Starting up
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}

如果你看到这些日志,恭喜你!您不仅为 Newsbot 编写了 Dockerfile,而且还成功地构建并运行了它。

摘要

在本章中,通过回顾 Dockerfile 的语法,您对它有了更好的理解。你现在离掌握为 Newsbot 编写 docker 文件又近了一步。

五、了解 Docker 卷

在前几章中,您了解了 Docker 及其相关术语,并深入了解了如何使用 Docker 文件构建 Docker 映像。在本章中,您将看到 Docker 容器的数据持久性策略,并了解为什么您需要特殊的数据持久性策略。

数据持久性

传统上,大多数计算解决方案都附带了持久化和保存数据的相关方法。对于虚拟机,会模拟一个虚拟磁盘,保存到该虚拟磁盘的数据会作为文件保存在主机上。亚马逊网络服务(AWS)等云提供商提供不同的服务,如亚马逊弹性块存储(EBS)和亚马逊弹性文件系统(EFS)。这些服务提供了可以安装在主机虚拟机上的端点;保存到这些装载点的数据被持久化和复制。

说到容器,情况就不一样了。容器是为无状态工作负载而设计的,容器层的设计表明了这一点。第二章解释了 Docker 映像是由各种层组成的只读模板。当映像作为容器运行时,会创建一个包含少量只写数据层的容器。这意味着

  • 数据被紧紧地锁定在主机上,这使得运行跨多个容器和应用共享数据的应用变得困难。

  • 当一个容器被终止时,数据不会持久,并且不可能以一种简单的方式从容器中提取数据。

  • 写入容器的写入层需要存储驱动程序来管理文件系统。存储驱动程序在读/写速度方面不能提供可接受的性能水平,并且写入容器写层的大量数据会导致容器和 Docker 守护程序耗尽内存。

Docker 容器中的数据丢失示例

为了演示 write 层的特性,让我们从 Ubuntu 基础映像中调出一个容器。您将在 Docker 容器中创建一个文件,停止容器,并查看容器的行为。

  1. 首先创建一个nginx容器:

  2. 打开容器内的终端:

        docker run -d --name nginx-test  nginx

  1. nginxdefault.conf复制到一个新的配置文件:
        docker exec -it nginx-test bash

  1. 你不会修改nginx-test.conf的内容,因为它无关紧要。现在你需要停止容器。在 Docker 主机终端上,键入以下命令:
        cd /etc/nginx/conf.d
        cp default.conf nginx-test.conf

  1. 再次启动容器:
        docker stop nginx-test

  1. 打开容器内的终端:
        docker start nginx-test

  1. 让我们看看变化是否还在:
        docker exec -it nginx-test bash

  1. 因为容器只是被停止了,所以数据是持久的。让我们停止它,移除容器,然后调出一个新的,观察会发生什么:
        cd /etc/nginx/conf.d
        ls
        default.conf  nginx-test.conf

  1. 启动一个新容器:
        docker stop nginx-test

        docker rm nginx-test

  1. 现在,一个新的容器已经启动并运行,连接到容器的终端:
        docker run -d --name nginx-test  nginx

  1. 检查nginxconf.d目录的内容:
        docker exec -it nginx-test bash

        cd /etc/nginx/conf.d
        ls
        default.conf

由于容器被移除,与容器相关联的只写层也被移除,并且所创建的文件不再可访问。对于容器化的有状态应用,比如需要数据库的应用,这意味着当一个现有的容器被移除或者一个新的容器被添加时,来自前一个容器的数据不再可访问。为了减轻这种情况,Docker 提供了各种策略来持久化数据。

  • tmpfs 安装

  • 绑定安装

tmpfs 装载

顾名思义,tmpfs 在 tmpfs 中创建一个挂载,这是一个临时文件存储工具。tmpfs 中挂载的目录显示为挂载的文件系统,但存储在内存中,而不是存储在磁盘驱动器之类的永久存储中。

tmpfs 挂载仅限于 Linux 上的 Docker 容器。tmpfs 挂载是临时的,数据存储在 Docker 的主机内存中。一旦容器停止,tmpfs 挂载将被删除,写入 tmpfs 挂载的文件将丢失。

要创建 tmpfs 挂载,可以在运行容器时使用--tmpfs标志,如下所示:

docker run -it --name docker-tmpfs-test --tmpfs /tmpfs-mount ubuntu bash

让我们检查一下容器:

docker inspect docker-tmpfs-test | jq ".[0].HostConfig.Tmpfs"
{
 "/tmpfs-mount": ""
}

这个输出告诉您有一个 tmpfs 配置映射到容器的/tmpfs-mount目录。

tmpfs 挂载最适合于生成不需要持久化和不必写入容器可写层的数据的容器。

绑定安装

在绑定挂载中,主机上的文件/目录被挂载到容器中。相反,当使用 Docker 卷时,会在 Docker 主机上的 Docker 存储目录中创建一个新目录,并且该目录的内容由 Docker 管理。

让我们看看如何使用绑定坐骑。您将尝试将 Docker 主机的主目录挂载到容器内名为host-home的目录中。为此,请键入以下命令:

docker run -it --name bind-mount-container -v $HOME:/host-home ubuntu bash

检查创建的容器揭示了关于装载的不同特征。

docker inspect bind-mount-container | jq ".[0].Mounts"

[
  {
    "Type": "bind",
    "Source": "/home/sathya",
    "Destination": "/host-home",
    "Mode": "",
    "RW": true,
    "Propagation": "rprivate"
  }
]

这个输出表明挂载是绑定类型的,源(即被挂载的 Docker 主机的目录)是/home/sathya(即主目录),挂载的目的地是/host-home。“Propagation”属性指绑定传播——该属性指示为绑定挂载创建的挂载是否被反映到该挂载的副本上。绑定传播仅适用于 Linux 主机。对于绑定装载,该属性通常不需要修改。RW 标志表示可以写入挂载的目录。让我们检查一下host-home的内容,看看挂载是否正确。

  1. 使用以下命令打开容器的交互式终端:

  2. 在容器的终端中,键入以下内容:

        docker run -it -v $HOME:/host-home ubuntu bash

  1. 该命令的输出应该是 Docker 主机主目录的列表。尝试在host-home目录中创建一个文件。为此,请键入以下命令:
        cd  /host-home
        ls

        cd /host-home
        echo "This is a file created from container having kernel `uname -r`" > host-home-file.txt

该命令创建一个名为host-home-file.txt的文件,该文件包含容器的/host-home目录中的文本"This is a file created from container having kernel 4.9.87-linuxkit-aufs"。请注意,内容会因主机操作系统和内核版本而异。

因为这是 Docker 主机主目录的绑定挂载,所以也应该在 Docker 主机的主目录中创建该文件。你可以看看是否确实如此。

  1. 在 Docker 主机中打开一个新的终端窗口,并键入以下命令:

  2. 您应该会看到以下输出,表明文件的存在:

        cd ~
        ls host-home-file.txt

  1. 现在检查文件的上下文:
        ls host-home-file.txt
        host-home-file.txt

        cat host-home-file.txt

这个文件应该与您在上一节中看到的内容相同。这证实了在容器中创建的文件在容器外确实是可用的。因为您关心的是容器停止、移除和重新启动后的数据持久性,所以让我们看看会发生什么。

通过在 Docker 主机终端中输入以下命令来停止容器。

docker stop bind-mount-container
docker rm bind-mount-container

确认 Docker 主机上的文件仍然存在:

cat ~/host-home-file.txt
This is a file created from container having kernel 4.9.87-linuxkit-aufs

绑定挂载非常有用,并且在应用的开发阶段最常用。通过使用绑定装载,您可以在将源目录装载为绑定装载时,使用与生产相同的容器来为生产准备应用。这允许开发人员拥有快速的代码测试迭代周期,而不需要重新构建 Docker 映像。

Caution

记住,对于绑定装载,数据流在 Docker 主机和容器上是双向的。任何破坏性的操作(比如删除目录)也会对 Docker 主机产生负面影响。

注意,在将主机操作系统目录作为绑定装载装载到容器中时要格外小心。如果挂载的目录范围很广,比如主目录(如前所示)或根目录,这就更重要了。一个失控的脚本或一个错误的rm -rf命令可以完全瘫痪 Docker 主机。为了减轻这种情况,您可以创建一个带有只读选项的绑定装载,以便以只读方式装载目录。

为此,您可以使用docker run命令提供一个只读参数。这些命令如下所示:

docker run -it --name read-only-bind-mount -v $HOME:/host-home:ro ubuntu bash

现在检查创建的容器:

docker inspect read-only-bind-mount | jq ".[0].Mounts"
[
  {
    "Type": "bind",
    "Source": "/home/sathya",
    "Destination": "/host-home",
    "Mode": "ro",
    "RW": false,
    "Propagation": "rprivate"
  }
]

您可以看到“RW”标志现在为假,Mode被设置为只读(ro)。让我们像前面一样尝试写入文件。

打开容器 Docker:

docker run -it --name read-only-bind-mount -v $HOME:/host-home:ro ubuntu bash

键入以下命令在容器中创建一个文件:

echo "This is a file created from container having kernel `uname -r`" > host-home-file.txt
bash: host-home-file.txt: Read-only file system

写操作失败,bash 告诉您这是因为文件系统是以只读方式挂载的。任何破坏性操作也会遇到同样的错误:

rm host-home-file.txt
rm: cannot remove 'host-home-file.txt': Read-only file system

Docker 卷

Docker volulmes 是当前推荐的保存容器中数据的方法。卷完全由 Docker 管理,与绑定装载相比有许多优势:

  • 卷比绑定装载更容易备份或传输。

  • 卷在 Linux 和 Windows 容器上都可以工作。

  • 卷可以在多个容器之间共享,没有问题。

Docker 卷子命令

Docker 将卷 API 公开为一系列子命令。这些命令如下所示:

  • docker volume create

  • docker volume inspect

  • docker volume ls

  • docker volume prune

  • docker volume rm

卷创建

volume create子命令用于创建命名卷。最常见的用例是生成命名卷。该命令的用法如下:

docker volume create --name=<name of the volume> --label=<any extra metadata>

Tip

Docker 对象标签在第四章中讨论。

例如,该命令创建一个名为nginx-volume的命名卷:

docker volume create --name=nginx-volume

体积检查

volume inspect命令显示卷的详细信息。该命令的用法如下:

docker volume inspect <volume-name>

nginx-volume名称为例,您可以通过键入以下内容找到更多详细信息:

docker volume inspect nginx-volume

这将产生以下结果:

docker volume inspect nginx-volume
[
    {
        "CreatedAt": "2018-04-17T13:51:02Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/nginx-volume/_data",
        "Name": "nginx-volume",
        "Options": {},
        "Scope": "local"
    }
]

当您想要复制/移动/备份卷时,此命令非常有用。mount path 属性列出了 Docker 主机上保存包含卷数据的文件的位置。

列出卷

volume ls命令显示主机中存在的所有卷。用法如下:

docker volume ls

清理卷

volume prune命令删除所有未使用的本地卷。用法如下:

docker volume prune

Docker 认为至少有一个容器未使用的卷是未使用的。由于未使用的卷最终会占用大量的磁盘空间,所以定期运行prune命令并不是一个坏主意,尤其是在本地开发机器上。您可以将--force附加到命令的末尾,当命令运行时,它不会要求确认删除。

移除卷

volume rm命令删除其名称作为参数提供的卷。用法如下:

docker volume rm <name>

对于之前创建的卷,命令如下:

docker volume rm nginx-volume

Docker 不会删除正在使用的卷,并将返回一个错误。例如,如果您尝试删除连接到容器的nginx-volume卷,您将得到以下错误消息:

docker volume rm nginx-volume

Error response from daemon: unable to remove volume: remove nginx-volume: volume is in use - [6074757a]

Note

即使容器被停止,Docker 也会认为该卷正在使用中。

长标识符是与卷相关联的容器的 ID。如果卷与多个容器相关联,将列出所有容器 id。使用docker inspect命令可以找到相关容器的更多细节,如下所示:

docker inspect 6074757a

启动容器时使用卷

创建附加了卷的容器的命令如下所示:

docker run --name container-with-volume -v data:/data ubuntu

在本例中,创建了一个名为container-with-volume的容器,其中一个名为data的卷被映射到容器内的/data目录。使用卷时,不提供主机目录的完整路径,而是提供存储数据的卷名。在后台,Docker 将通过将这个卷映射到主机上的一个目录来创建和管理它。

让我们检查使用以下命令创建的容器:

docker inspect container-with-volume | jq ".[0].Mounts"
[
  {
    "Type": "volume",
    "Name": "data",
    "Source": "/var/lib/docker/volumes/data/_data",
    "Destination": "/data",
    "Driver": "local",
    "Mode": "z",
    "RW": true,
    "Propagation": ""
  }
]

查看mounts部分,您可以得出结论,Docker 在/var/lib/docker/volumes/data/_data的主机目录中创建了一个名为data的新卷,该卷的内容由 Docker 管理。这个卷被安装到容器的/data目录中。

也可以使用以下命令提前生成这些卷:

docker volume create info

您可以使用docker volume inspect来检查卷的属性:

docker volume inspect info
[
    {
        "CreatedAt": "2021-07-27T19:23:00Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/info/_data",
        "Name": "images",
        "Options": {},
        "Scope": "local"
    }
]

现在,您可以在创建/运行容器时引用该卷,如下所示:

docker run -it --name info-container -v info:/container-info ubuntu bash

让我们尝试创建与前面相同的文件。从容器内的终端,键入以下内容:

echo "This is a file created from container having kernel `uname -r`" > /container-info/docker_kernel_info.txt

退出容器,然后使用以下命令停止并移除容器:

exit
docker stop info-container
docker rm info-container

在没有卷的情况下,当容器被删除时,其可写层也将被删除。让我们看看当您启动一个附加了卷的新容器时会发生什么。请记住,这不是一个绑定挂载,因此您没有从 Docker 主机显式转发任何目录。下面的命令将在名为new-info-container的容器上启动一个 shell,其中一个名为info的卷被挂载到容器的/container-info目录中。

docker run -it --name new-info-container -v info:/container-info ubuntu bash

检查容器的/data-volume目录的内容,如下所示:

cd /container-info/
ls
docker-kernel-info.txt

检查docker-kernel-info.txt的内容,如下所示:

cat docker_kernel_info.txt
This is a file created from container having kernel 4.9.87-linuxkit-aufs.

当您将文件写入装载并映射到卷的目录中时,数据将保存在卷中。当您启动一个新的容器时,提供卷名和run命令会将卷附加到容器上,使得任何以前保存的数据都可以用于新启动的容器。

docker 文件中的卷指令

VOLUME指令将指令后面提到的路径标记为 Docker 管理的外部存储数据卷。语法如下所示:

VOLUME ["/data-volume"]

指令后面提到的路径可以是 JSON 数组,也可以是用空格分隔的路径数组。

Note

docker 文件中的VOLUME指令不支持命名卷。因此,当容器运行时,卷名将是自动生成的名称。

练习

Building and Running an Nginx Container with Volumes and Bind Mounts

在本练习中,您将构建一个附加了 Docker 卷的nginx Docker 映像,其中包含一个自定义的nginx配置。在练习的第二部分,您将附加一个绑定挂载和一个包含静态 web 页面和自定义nginx配置的卷。本练习的目的是帮助您了解如何利用卷和绑定装载来简化本地开发。

提示在本书的 GitHub repo 上 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-5/exercise-1目录下可以找到源代码和相关的 Dockerfile。

从 Dockerfile 文件开始,如下所示。

FROM nginx:alpine
COPY default.conf /etc/nginx/conf.d
VOLUME ["/var/lib"]
EXPOSE 80

这个 Dockerfile 获取一个基本的nginx映像,用定制的default.conf nginx配置文件覆盖default.conf nginx配置文件,并将/var/lib声明为一个卷。您可以使用 repo 中的docker-volume-bind-mount目录中的以下命令来构建它:

docker build -t sathyabhat/nginx-volume .

[+] Building 0.9s (7/7) FINISHED
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 37B 0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/nginx:alpine   0.8s
 => [internal] load build context   0.0s
 => => transferring context: 34B 0.0s
 => [1/2] FROM docker.io/library/nginx:alpine@sha256:ad14f34   0.0s
 => CACHED [2/2] COPY default.conf /etc/nginx/conf.d  0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:f6f3af7 0.0s
 => => naming to docker.io/sathyabhat/nginx-volume 0.0s

在运行该图像之前,请查看定制的nginx default.conf内容:

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /srv/www/starter;
        index  index.html index.htm;
    }
    access_log  /var/log/nginx/access.log;
    access_log  /var/log/nginx/error.log;

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

}

nginx配置是一个简单的配置;它告诉nginx/srv/www/starter/.提供一个名为index.html的默认文件,让我们运行 Docker 容器。由于nginx正在监听端口 80,您需要告诉 Docker 使用-p标志发布端口:

docker run -d --name nginx-volume  -p 8080:80 sathyabhat/nginx-volume

请注意,您是从 Docker 主机的端口 8080 发布到容器的端口 80。尝试通过导航至http://localhost:8080加载网页。

img/463857_2_En_5_Fig1_HTML.png

图 5-1

未装载源目录时出现 404 错误

当你加载网站时,你会看到一个 HTTP 404 - Page Not Found 错误(见图 5-1 )。这是因为在nginx config档中,你指挥nginxindex.html服务。但是,您还没有将index.html文件复制到容器中,也没有将index.html的位置作为绑定挂载挂载到容器中。结果,nginx找不到index.htm l 文件。

您可以通过将网站文件复制到容器中来纠正这个错误,正如您在上一章中看到的那样。在本练习中,您将利用之前学习的绑定挂载特性,并挂载包含源代码的整个目录。所需要做的就是使用您在前面学到的绑定挂载标志。您不必对 docker 文件进行更改。

使用以下命令停止现有容器:

docker stop nginx-volume

现在,使用绑定挂载启动一个新容器,如以下命令所示:

docker run -d --name nginx-volume-bind -v "$(pwd)"/:/srv/www  -p 8080:80 sathyabhat/nginx-volume

使用以下命令确认容器正在运行:

docker ps

您应该会看到正在运行的容器列表,如下所示:

CONTAINER ID     IMAGE       COMMAND         CREATED        STATUS      PORTS       NAMES
54c857ca065b    sathyabhat/nginx-volume   "nginx -g 'daemon of..."6 minutes ago       Up 6 minutes        0.0.0.0:8080->80/tcpnginx-volume-bind

使用以下命令确认卷和装载是否正确:

docker inspect nginx-volume-bind | jq ".[].Mounts"
[
  {
    "Type": "bind",
    "Source": "/code/practical-docker-with-python/docker-volume-bind-mount/",
    "Destination": "/srv/www",
    "Mode": "",
    "RW": true,
    "Propagation": "rprivate"
  },
  {
    "Type": "volume",
    "Name": "c069ba7",
    "Source": "/var/lib/docker/volumes/c069ba7/_data",
    "Destination": "/var/lib",
    "Driver": "local",
    "Mode": "",
    "RW": true,
    "Propagation": ""
  }
]

让我们再次导航到同一个 URL。如果 mounts 部分看起来不错,那么您应该会看到图 5-2 中的页面。

img/463857_2_En_5_Fig2_HTML.jpg

图 5-2

nginx 服务网页成功

成功!

Adding Volumes to Newsbot

在上一章的练习中,您为 Newsbot 编写了一个 docker 文件。然而,正如您可能已经注意到的,终止容器会重置 Newsbot 的状态,并且您需要重新定制该 bot。要解决这个问题,您将添加一个 SQLite 数据库,该数据库的数据文件将保存到 Docker 卷中。通过完成本练习,您将知道可以通过将容器中的数据保存到卷中,然后将该卷重新附加到新容器中来持久保存数据。

Newsbot 源代码已经从代码库进行了轻微的修改,以便将首选项(即新闻应该从哪个子编辑中获取)保存到 SQLite 数据库中。

提示在本书的 GitHub repo 上 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-5/exercise-2目录下可以找到源代码和相关的 Dockerfile。

Dockerfile 文件修改如下:

FROM python:3-alpine

RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev
WORKDIR /apps/subredditfetcher/
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "newsbot.py"]

在这个 Dockerfile 文件中,从python:3-alpine开始作为基本图像。您添加了RUN步骤来安装 Python 包所需的一些库依赖项。然后将源代码复制到容器中,并安装所需的 Python 包。另一个显著的变化是增加了VOLUME指令。正如您在前面了解到的,这是为了告诉 Docker 将指定管理的目录标记为卷,即使您没有在docker run命令中指定所需的卷名。

使用以下命令构建映像:

docker build -t sathyabhat/newsbot-sqlite .

构建日志如下所示:

[+] Building 9.5s (11/11) FINISHED
 => [internal] load build definition from Dockerfile 0.1s
 => => transferring dockerfile: 38B   0.0s
 => [internal] load .dockerignore  0.1s
 => => transferring context: 2B 0.0s
 => [internal] load metadata for docker.io/library/python:3-alpine  2.3s
 => [auth] library/python:pull token for registry-1.docker.io 0.0s
 => [internal] load build context  0.1s
 => => transferring context: 6.23kB   0.0s
 => [1/5] FROM docker.io/library/python:3-alpine@sha256:eb31d7f  0.0s
 => CACHED [2/5] RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev 0.0s
 => CACHED [3/5] WORKDIR /apps/subredditfetcher/  0.0s
 => [4/5] COPY . .  0.1s
 => [5/5] RUN pip install -r requirements.txt  6.3s
 => exporting to image 0.4s
 => => exporting layers   0.3s
 => => writing image sha256:6605a7a   0.0s
 => => naming to docker.io/sathyabhat/newsbot-sqlite 0.0s

现在使用docker run命令运行机器人。注意,您通过-v标志提供了卷名。不要忘记将第三章中生成的 Newsbot API 密匙传递给NBT_ACCESS_TOKEN环境变量。

docker run --rm --name newsbot-sqlite -e NBT_ACCESS_TOKEN -v newsbot-data:/data sathyabhat/newsbot-sqlite

run命令创建一个名为newsbot-sqlite的新容器,一个名为newsbot-data的卷被附加到该容器,并被挂载到容器内的/data目录中。--rm标志确保容器停止时被移走。

如果 bot 启动良好,您应该开始看到这些日志:

docker run --rm --name newsbot-sqlite -e NBT_ACCESS_TOKEN=<token> -v newsbot-data:/data sathyabhat/newsbot-sqlite

INFO: <module> - Starting newsbot
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}

比如说 Python,试着设置一个机器人应该从中获取数据的子编辑器。要做到这一点,从电报,找到机器人和类型/source python

来自应用的日志应该确认收到了命令:

INFO: - handle_incoming_messages - Chat text received: /source python
INFO: - handle_incoming_messages - Sources set for nnn to  python
INFO: - handle_incoming_messages - nnn
INFO: - post_message - posting Sources set as  python! to nnn

电报窗口现在应该如图 5-3 所示。

img/463857_2_En_5_Fig3_HTML.png

图 5-3

子编辑源的确认

现在您可以获取一些内容。为此,在 bot 窗口中键入/fetch。应用应该用一个加载消息和另一个聊天内容来响应(见图 5-4 )。

img/463857_2_En_5_Fig4_HTML.png

图 5-4

机器人正在从子编辑中获取内容

现在,您可以通过停止 bot、移除容器并创建一个新容器来测试数据持久性。首先通过按 Ctrl+C 来停止 Newsbot。由于您使用--rm标志启动了容器,Docker 将自动移除容器。通过键入之前启动容器时使用的相同命令,创建一个新容器:

docker run --rm --name newsbot-sqlite -e NBT_ACCESS_TOKEN -v newsbot-data:/data sathyabhat/newsbot-sqlite

现在,在电报聊天窗口中,再次键入/fetch。由于子编辑源已经保存到数据库中,您应该可以看到之前配置的子编辑的内容(见图 5-5 )。

img/463857_2_En_5_Fig5_HTML.png

图 5-5

移除并启动新容器后,Newsbot 从 subreddit 获取内容

再次查看内容 Docker 音量设置工作正常。恭喜你!您已成功为此项目设置了数据持久性。

摘要

在本章中,您了解了为什么数据持久性在容器中是一个问题,以及 Docker 为管理数据持久性提供的不同策略。您还深入了解了如何配置,并了解了它们与绑定挂载的不同之处。最后,您进行了一些关于如何使用绑定挂载和卷的实践练习,并为 Newsbot 添加了卷支持。在下一章中,你将学习更多关于 Docker 网络的知识,并学习容器如何相互连接。

六、了解 Docker 网络

在前面的章节中,您了解了 Docker 及其相关术语,深入了解了如何使用 Docker 文件构建 Docker 映像,并了解了如何持久存储由容器生成的数据。

在这一章中,你将看到 Docker 中的网络,并了解容器如何在 Docker 的网络特性的帮助下相互对话和发现对方。

为什么我们需要容器网络?

传统上,大多数计算解决方案被认为是单一用途的解决方案,您很少会遇到单个主机(或虚拟机)托管多个工作负载的情况,尤其是生产工作负载。有了容器,情况就变了。随着轻量级容器和先进编排平台(如 Kubernetes 和 DC/OS)的出现,在同一台主机上运行不同工作负载的多个容器,并且应用的不同实例分布在多个主机上是非常常见的。在这种情况下,容器网络有助于允许(或限制)跨容器对话。为了促进这个过程,Docker 提供了不同的网络模式。

Tip

Docker 的联网子系统是通过可插拔驱动实现的;Docker 自带四个驱动程序,更多驱动程序可从 Docker Store 获得,可在 https://store.docker.com/search?category=network&q=&type=plugin 获得。

值得注意的是,Docker 的所有网络模式都是通过软件定义网络 (SDN)实现的。具体来说,在 Linux 系统上,Docker 修改 iptables 规则以提供所需的访问/隔离级别。

默认 Docker 网络驱动程序

对于 Docker 的标准安装,以下网络驱动程序可用:

  • 圣体

  • 覆盖物

  • 麦克法兰

  • 没有人

桥接网络

网络是用户定义的网络,允许连接在同一网络上的所有容器相互通信。好处是在同一网桥网络上的容器可以相互连接、发现和对话,而不在同一网桥上的容器不能直接通信。当在同一主机上运行的容器需要相互通信时,桥接网络是有用的——如果需要通信的容器在不同的 Docker 主机上,那么就需要一个覆盖网络。

安装并启动 Docker 时,会创建一个默认的桥接网络,新启动的容器会连接到该网络。但是,如果您自己创建一个桥接网络,效果会更好。原因有很多:

  • 容器之间更好的隔离。如您所知,同一个桥接网络上的容器是可发现的,并且可以相互通信。它们自动向对方公开所有端口,没有端口向外界公开。为每个应用提供一个单独的用户定义的桥接网络可以在不同应用的容器之间提供更好的隔离。

  • 跨容器的简单名称解析。对于加入同一个桥接网络的服务,容器可以通过名称相互连接。对于默认桥接网络上的容器,容器相互连接的唯一方式是通过 IP 地址或使用--link标志,这已被否决。

  • 在用户定义的网络上轻松连接/分离容器。对于默认网络上的容器,分离它们的唯一方法是停止正在运行的容器,并在新网络上重新创建它。

主机网络

顾名思义,有了主机网络,容器就附加到了 Docker 主机上。这意味着任何到达主机的流量都被路由到容器。由于容器的所有端口都直接连接到主机,在这种模式下,发布端口的概念没有意义。当 Docker 主机上只有一个容器运行时,主机模式是最理想的。

覆盖网络

覆盖网络创建了一个跨越多个 docker 主机的网络。这种类型的网络称为覆盖网络,因为它位于现有主机网络之上,允许连接到覆盖网络的容器跨多个主机进行通信。覆盖网络是一个高级主题,主要用于以群模式建立 Docker 主机集群的情况。覆盖网络还允许您加密通过它们的应用数据流量。

Macvlan 网络

Macvlan 网络利用 Linux 内核的能力,将基于 MAC 的多个逻辑地址分配给单个物理接口。这意味着您可以将 MAC 地址分配给容器的虚拟网络接口,使其看起来好像容器具有连接到网络的物理网络接口。这带来了独一无二的机会,尤其是对于那些希望物理接口存在并连接到物理网络的遗留应用。

Macvlan 网络需要对网络接口卡 (NIC)的额外依赖,以支持所谓的“混杂”模式,这是一种特殊的模式,允许 NIC 接收所有流量并将其定向到控制器,而不是仅接收 NIC 预期接收的流量。

无网络

当容器启动时,Docker 将容器连接到默认的桥接网络。桥接网络允许容器发出网络请求。尽管容器网络绝对是一个特性和亮点,但在许多情况下,应用必须完全隔离,不允许传入或传出请求——特别是对于安全性和合规性要求高的应用。在这种情况下,无网络就派上了用场。

顾名思义,无网络是指容器没有连接到任何网络接口,也没有接收或发送任何网络流量。在这种网络模式下,仅创建环回接口,允许容器与自身对话,但不能与外界或其他容器对话。

使用此处显示的命令,可以在无网络的情况下启动容器:

docker run -d --name nginx --network=none -p 80:80 nginx

尝试curl端点导致瞬间Connection Refused,表明容器不接受连接。

curl localhost
curl: (7) Failed to connect to localhost port 80 after 1 ms: Connection refused

如果您使用容器打开一个交互式终端,并尝试使用curl发出网络请求,如下所示:

docker exec -it nginx sh
curl google.com
curl: (6) Could not resolve host: google.com

您将看到没有配置网络。容器无法接收或发送网络流量。

使用 Docker 网络

现在您已经从概念上理解了不同的网络模式,您可以尝试其中的一些模式。本章只看桥接网络,因为它是最常用的驱动程序。与其他子系统非常相似,Docker 附带了一个用于处理 Docker 网络的子命令。要开始,请尝试以下命令:

docker network

您应该会看到可用选项的说明:

docker network

Usage:   docker network COMMAND

Manage networks

Options:

Commands:
  connect     Connect a container to a network
  create      Create a network
  disconnect  Disconnect a container from a network
  inspect     Display detailed information on one or more networks
  ls          List networks
  prune       Remove all unused networks
  rm          Remove one or more networks

现在看看哪些网络可用。为此,请键入以下内容:

docker network ls

至少,您应该看到列出了这些网络:

docker network ls
NETWORK ID NAME DRIVER SCOPE
8ea951d9f963 bridge bridge local
790ed54b21ee host host local
38ce4d23e021 none null local

其中每一种都对应于前面提到的三种不同类型的网络——网桥、主机和无类型网络。您可以通过键入以下命令来检查网络的详细信息:

docker network inspect <network id or name>

例如,如果您想要检查默认桥接网络,请键入以下命令:

docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "c540708",
        "Created": "2018-04-17T13:10:43.002552762Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "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": {}
    }
]

除其他外,您可以看到:

  • Options下的com.docker.network.bridge.default_bridge键表示网桥是默认的。

  • "EnableIPv6": false表示此网桥禁用了 IPv6。

  • IPAM – Config下的"Subnet"键表示 Docker 网络子网的 CIDR 为 172.17.0.0/16。这意味着多达 65,536 个容器可以连接到这个网络(这是从/16的 CIDR 区块得出的)。

  • Options下的com.docker.network.bridge.enable_ip_masquerade表示网桥启用了 IP 伪装。这意味着外界看不到容器的私有 IP,看起来好像请求来自 Docker 主机。

  • com.docker.network.bridge.host_binding_ipv4表示主机绑定为0.0.0.0。网桥绑定到主机上的所有接口。

相反,如果您检查无网络:

docker network inspect none
[
    {
        "Name": "none",
        "Id": "d30afbe",
        "Created": "2017-05-10T10:37:04.125762206Z",
        "Scope": "local",
        "Driver": "null",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": []
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

驱动程序null表示不会为此处理任何联网。

桥接网络

在创建桥接网络之前,您需要创建两个运行的容器:

  • MySQL 数据库服务器

  • 用于管理 MySQL 数据库的网络门户

要创建 MySQL 容器,请运行以下命令:

docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=dontusethisinprod mysql:8

因为您是在分离模式下启动的(由-d标志指定),所以请跟踪日志,直到您确定容器已启动:

docker logs -f mysql

结果应该是以下几行:

Initializing database
[...]
Database initialized
[...]
MySQL init process in progress...
[...]
MySQL init process done. Ready for start-up.
[...]
[Note] mysqld: ready for connections.
Version: '8.0.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
[...]

如果您看到最后一行,MySQL 数据库容器就准备好了。创建adminer容器:

docker run -d --name adminer -p 8080:8080 adminer

以下是adminer的日志:

docker logs -f adminer
PHP 7.4.22 Development Server started

这意味着adminer准备好了。现在看看这两个容器——特别是它们的网络方面。

docker inspect mysql | jq ".[0].NetworkSettings.Networks"
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8ea951d",
    "EndpointID": "c33e38",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:03",
    "DriverOpts": null
  }
}

从这个输出中,您知道 MySQL 容器在默认的桥接网络上被分配了一个 IP 地址172.17.0.2。现在检查adminer容器:

docker inspect adminer | jq ".[0].NetworkSettings.Networks"
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8ea951d",
    "EndpointID": "a26bcc",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.3",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:04",
    "DriverOpts": null
  }
}

adminer容器与桥接网络内的172.17.0.3的 IP 地址相关联。但是,由于两个容器都绑定到了0.0.0.0的主机 IP,转换为 Docker 主机的所有接口,您应该能够通过它的端口进行连接。

在桥接网络中,无论是默认的 Docker 桥接网络还是您创建的自定义桥接网络(您将在本章的练习中看到这一点),所有的容器都可以使用及其容器名称来访问。但是,只有当这些容器的端口已经暴露时,才能从主机访问它们。为了演示这一点,尝试通过adminer连接到数据库。导航至http://localhost:8080

mysql的身份进入服务器并尝试登录。你会注意到登录会失败(见图 6-1 )。

img/463857_2_En_6_Fig1_HTML.jpg

图 6-1

与指定主机的连接失败

尝试再次登录,这次是在服务器框中。输入 MySQL 容器的 IP 地址,如图 6-2 所示。

img/463857_2_En_6_Fig2_HTML.jpg

图 6-2

尝试使用容器的 IP 地址登录

当你尝试登录时,应该会成功(见图 6-3 )。

img/463857_2_En_6_Fig3_HTML.jpg

图 6-3

使用 IP 地址登录成功

登录成功。虽然当只有一个依赖容器时,输入 IP 是一种可以接受的变通方法,但是许多应用有多个依赖项。这种方法在这些情况下就失效了。

创建命名桥接网络

在本节中,您将创建一个数据库网络,并尝试将 MySQL 和adminer容器连接到网络。您可以通过键入以下命令来创建桥接网络:

docker network create <network name>

Docker 在指定子网方面给了你更多的选择,但是大部分情况下缺省值是好的。请注意,桥接网络只允许您创建一个子网。

使用以下命令创建一个名为database的网络:

docker network create database

现在检查您创建的网络:

docker network inspect database
[
    {
        "Name": "database",
        "Id": "8574145",
        "Created": "2021-07-31T15:58:11.4652433Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

请注意,创建的网络有一个子网172.18.0.0/16.,使用以下命令停止并删除现有容器:

docker stop adminer
docker rm adminer
docker stop mysql
docker rm mysql

现在启动 MySQL 容器,这次连接到数据库网络。该命令如下所示:

docker run -d --network database --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=dontusethisinprod mysql:8

注意附加的--network标志,它告诉 Docker 应该将容器附加到哪个网络。等待容器初始化。您还可以检查日志,确保容器准备就绪:

docker logs -f mysql

结果应该是以下几行:

Initializing database
[...]
Database initialized
[...]
MySQL init process in progress...
[...]
MySQL init process done. Ready for start up.
[...]
[Note] mysqld: ready for connections.
Version: '8.0.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
[...]

现在检查容器:

docker inspect mysql | jq ".[0].NetworkSettings.Networks"
{
  "database": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": [
      "6149cb2453da"
    ],
    "NetworkID": "8574145",
    "EndpointID": "3343960402",
    "Gateway": "172.18.0.1",
    "IPAddress": "172.18.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:12:00:02",
    "DriverOpts": null
  }
}

注意,容器是数据库网络的一部分。您也可以通过检查数据库网络来确认这一点。

docker network inspect database | jq ".[0].Containers"
{
  "6149cb2": {
    "Name": "mysql",
    "EndpointID": "3343960",
    "MacAddress": "02:42:ac:12:00:02",
    "IPv4Address": "172.18.0.2/16",
    "IPv6Address": ""
  }
}

注意,数据库网络中的 containers 键包含 MySQL 容器。启动adminer容器。键入以下命令:

docker run -d --name adminer -p 8080:8080 adminer

请注意,--network命令已被省略。这意味着adminer将连接到默认的桥接网络:

docker inspect adminer | jq ".[0].NetworkSettings.Networks"
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8ea951d",
    "EndpointID": "c1a5df0",
    "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
  }
}

将容器连接到命名的桥接网络

Docker 让你可以很容易地将一个容器连接到另一个网络上。为此,请键入以下命令:

dockr network connect <network name> <container name>

您需要将adminer容器连接到数据库网络,如下所示:

docker network connect database adminer

现在检查adminer容器:

docker inspect adminer | jq ".[0].NetworkSettings.Networks"
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8ea951d",
[...]
    "DriverOpts": null
  },
  "database": {
    "IPAMConfig": {},
    "Links": null,
    "Aliases": [
      "2a7363ec1888"
    ],
    "NetworkID": "8574145",
    [...]
    "DriverOpts": {}
  }
}

请注意,networks 键有两个网络,默认网桥网络和您刚刚连接到的数据库网络。因为容器不需要连接到默认的桥接网络,所以您可以断开它。为此,命令如下:

docker network disconnect bridge adminer

现在使用下面的命令检查adminer容器,您可以只看到连接的数据库网络。

docker inspect adminer | jq ".[0].NetworkSettings.Networks"
{
  "database": {
    "IPAMConfig": {},
    "Links": null,
    "Aliases": [
      "2a7363ec1888"
    ],
    "NetworkID": "8574145",
[...]
    "DriverOpts": {}
  }
}

桥接网络不再连接到adminer网络。通过导航到http://localhost:8080启动adminer。在服务器字段中,输入想要连接的容器名称,即数据库容器名称mysql,如图 6-4 所示。

img/463857_2_En_6_Fig4_HTML.jpg

图 6-4

通过命名主机连接到容器

输入详细信息,然后单击登录。登录应该成功,您应该会看到如图 6-5 所示的屏幕。

img/463857_2_En_6_Fig5_HTML.jpg

图 6-5

命名主机解析为 IP 并成功连接

因此,用户定义的桥接网络使得连接服务非常容易;你不必去寻找 IP 地址。Docker 通过让您使用容器的名称作为主机来连接到服务,使这变得很容易。Docker 处理容器名到 IP 地址的幕后翻译。

主机网络

在主机网络中,Docker 不为容器创建虚拟网络;相反,Docker 主机的网络接口被绑定到容器。

当您只有一个容器在主机上运行并且不需要任何桥接网络或网络隔离时,主机网络是非常好的。现在您将创建一个在主机模式下运行的nginx容器,看看如何运行它。

前面你看到已经有一个网络叫host。这不是控制网络是否是主机网络的名称;是司机。回想一下,主机网络有一个主机驱动程序,因此任何连接到主机网络的容器都将以主机网络模式运行。

要启动容器,只需传递--network host参数。尝试以下命令来启动一个nginx容器,并将容器的端口 80 发布到主机的 8080 端口。

docker run -d --network host -p 8080:80 nginx:alpine
WARNING: Published ports are discarded when using host network mode

注意 Docker 警告您没有使用端口发布。因为容器的端口直接绑定到 Docker post,所以发布端口的概念不会出现。实际的命令应该如下所示:

docker run -d --network host nginx:alpine

练习

Connecting a Mysql Container to a Newsbot Container

在前一章的练习中,您为 Newsbot 编写了一个 Dockerfile 并构建了容器。然后使用 Docker 卷跨容器持久化数据库。在本练习中,您将修改 Newsbot,以便数据持久保存到 MySQL 数据库,而不是保存到 SQLite DB。然后,您将创建一个定制的桥接网络来连接项目容器和 MySQL 容器。

提示在本书的 GitHub repo 上 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-6/exercise-1目录下可以找到源代码和相关的 Dockerfile。

考虑下面的 Dockerfile 文件。它看起来,实际上,与你在第五章的练习 2 中使用的 Dockerfile 非常相似。唯一需要改变的是 Newsbot 的代码,使它连接到 MySQL 服务器,而不是从 SQLite 数据库读取。

FROM python:3-alpine

RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev
WORKDIR /apps/subredditfetcher/
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "newsbot.py"]

现在使用以下命令构建容器:

docker build -t sathyabhat/newsbot-mysql .
[+] Building 2.9s (11/11) FINISHED
 => [internal] load build definition from Dockerfile   0.1s
 => => transferring dockerfile: 38B  0.0s
 => [internal] load .dockerignore 0.1s
 => => transferring context: 2B   0.0s
 => [internal] load metadata for docker.io/library/python:3-alpine 2.6s
 => [auth] library/python:pull token for registry-1.docker.io   0.0s
 => [1/5] FROM docker.io/library/python:3-alpine@sha256:1e8728b 0.0s
 => => resolve docker.io/library/python:3-alpine@sha256:1e8728b 0.0s
 => [internal] load build context 0.0s
 => => transferring context: 309B 0.0s
 => CACHED [2/5] RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev cargo   0.0s
 => CACHED [3/5] WORKDIR /apps/subredditfetcher/ 0.0s
 => CACHED [4/5] COPY . .   0.0s
 => CACHED [5/5] RUN pip install --upgrade pip && pip install -r requirements.txt 0.0s
 => exporting to image   0.0s
 => => exporting layers  0.0s
 => => writing image sha256:44cd813  0.0s
 => => naming to docker.io/sathyabhat/newsbot-mysql 0.0s

创建一个名为newsbot的新网络,容器将连接到这个网络。为此,请键入以下内容:

docker network create newsbot

现在,您将打开一个新的 MySQL 容器,并将其连接到您之前创建的网络。因为您希望数据持久化,所以您还将 MySQL 数据库挂载到一个名为newsbot-db的卷上。这个练习使用root作为用户名,使用dontusethisinprod作为密码。这些凭证非常脆弱,我们强烈建议您不要在现实世界中使用它们。

键入以下命令启动 MySQL 容器:

docker run -d --name mysql --network newsbot -v newsbot-db:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=dontusethisinprod mysql:8

注意--network标志,它告诉 Docker 将mysql容器连接到名为newsbot的网络。MySQL 将所有与数据库相关的文件保存在/var/lib/mysql目录中,-v newsbot-db:/var/lib/mysql标志指示 Docker 将容器中/var/lib/mysql目录的内容保存到名为newsbot-db的卷中。这样,即使在容器被移除后,内容仍然存在。

遵循日志并验证 MySQL 数据库是否启动:

docker logs mysql

Initializing database
[...]
Database initialized
[...]
MySQL init process in progress
[...]
MySQL init process done. Ready for start up.
[...]
2021-08-01T12:41:15.295013Z 0 [Note] mysqld: ready for connections.
Version: '8.0.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)

最后几行表示 MySQL 数据库启动了。现在启动 Newsbot 容器,同时将其连接到您创建的newsbot网络。为此,请键入以下命令:

docker run --rm --network newsbot --name newsbot-mysql -e NBT_ACCESS_TOKEN=<token> sathyabhat/newsbot-mysql

注意用第三章中生成的 Newsbot API 键的值替换<token>

您应该会看到以下日志:

INFO: <module> - Starting up
INFO: <module> - Waiting for 60 seconds for db to come up
INFO: <module> - Checking on dbs
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}

因为创建了新卷,所以上一章设置的源不可用。

再次设置机器人应该从中获取数据的 subreddit,比如 Docker。要做到这一点,从电报,找到机器人和类型/source docker。来自应用的日志应该确认收到了命令:

INFO: handle_incoming_messages - Chat text received: /source docker
INFO: handle_incoming_messages - Sources set for 7342383 to  docker
INFO: handle_incoming_messages - 7342383
INFO: post_message - posting Sources set as  docker! to 7342383
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}

您的电报窗口应该如图 6-6 所示。

img/463857_2_En_6_Fig6_HTML.jpg

图 6-6

子编辑源的确认

现在您可以获取一些内容。为此,在 bot 窗口中键入/fetch。应用应该通过加载一个消息和另一个聊天内容来响应,如图 6-7 所示。

img/463857_2_En_6_Fig7_HTML.png

图 6-7

机器人正在从子编辑中获取内容

现在,您将确认 Newsbot 确实正在将源保存到数据库中。为此,使用以下命令连接到正在运行的mysql容器:

docker exec --it mysql sh

现在,在容器外壳中,键入以下命令以连接到 MySQL 服务器:

mysql –p

输入密码(如前所述)进行连接。如果您输入了正确的密码,将会收到以下消息:

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 32
Server version: 8.0.26 MySQL Community Server - GPL
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>

在 MySQL 提示符下,键入以下命令以确保 Newsbot 数据库存在:

show databases;

您应该会看到类似于以下列表的数据库列表:

show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| newsbot            |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.03 sec)

键入以下命令选择数据库,然后获取名为source的表的内容:

use newsbot
select * from source;
+-----------+------------+
| person_id | fetch_from |
+-----------+------------+
|   7342383 |  docker    |
+-----------+------------+
1 row in set (0.00 sec)

这向您展示了 Newsbot 可以成功地连接到 MySQL 容器并将数据保存到数据库中。

摘要

在这一章中,你学习了容器网络的基础知识和 Docker 网络的不同模式。您还学习了如何创建和使用定制的 Docker 桥接网络,并了解了对 Docker 主机网络的见解。最后,您运行了一些关于创建单独的数据库容器(使用 MySQL)的实践练习,并学习了如何将数据库容器连接到 Newsbot 项目。在下一章中,您将了解 Docker Compose,以及 Docker Compose 如何让您轻松运行多个相互依赖的容器。

七、了解 Docker Compose

在前面的章节中,您了解了 Docker 及其相关术语,深入了解了如何使用 Docker 文件构建 Docker 映像,了解了如何持久存储容器生成的数据,并借助 Docker 的网络功能链接了各种运行中的容器。

在这一章中,你将会看到 Docker Compose,这是一个运行多容器应用的工具,可以调出各种链接的、相关的容器等等——所有这些都只需要一个配置文件和一个命令。

Docker 编写概述

随着软件变得越来越复杂,以及您越来越倾向于微服务架构,需要部署的组件数量也会大大增加。虽然微服务可能通过鼓励松散耦合的服务来帮助保持整个系统的流动性,但从运营的角度来看,事情会变得更加复杂。当您有依赖的应用时,这尤其具有挑战性。例如,为了让 web 应用正常工作,它需要其数据库在 web 层开始响应请求之前工作。

Docker 可以很容易地将每个微服务绑定到一个容器。Docker Compose 使得编排所有这些容器变得非常容易。如果没有 Docker Compose,容器编排步骤将涉及构建各种映像、创建所需的网络,然后按照必要的顺序使用一系列docker run命令运行应用。随着容器数量的增加和部署目标的增加,手动运行这些步骤变得不合理,您将需要走向自动化。

从本地开发的角度来看,手动启动多个相互关联的服务非常繁琐和痛苦。Docker Compose 大大简化了这一点。Docker Compose 只需提供一个描述所需容器和容器间关系的 YAML 文件,就可以用一个命令显示所有容器。Docker Compose 不仅可以打开容器,还可以让您完成以下任务:

  • 构建、停止和启动与应用相关联的容器。

  • 跟踪正在运行的容器的日志,省去为每个容器打开多个终端会话的麻烦。

  • 查看每个容器的状态。

Docker Compose 帮助您实现持续集成。通过提供多个可处理的、可复制的环境,Docker Compose 允许您独立地运行集成测试,允许对这些自动化测试用例采用一种干净的方法。它允许您运行测试,验证结果,然后彻底地拆除环境。

正在安装 Docker Compose

Docker Compose 作为 Docker 安装的一部分预先安装,不需要任何额外的步骤就可以在 macOS 和 Windows 系统上开始使用。在 Linux 系统上,你可以从它的 GitHub 发布页面下载 Docker Compose 二进制文件,可以在 https://github.com/docker/compose/releases 下载。或者,您可以运行下面的curl命令来下载正确的二进制文件。

sudo curl -L https://github.com/docker/compose/releases/download/1.21.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose

如果您已经安装了 Python 和 pip,您也可以使用以下命令使用 pip 来安装docker-compose:

pip install docker-compose

Note

确保pip install docker-compose命令中的版本号与 GitHub 发布页面上 Docker Compose 的最新版本相匹配。否则,你最终会得到一个过时的版本。

下载完二进制文件后,更改权限,以便可以使用以下命令执行它:

sudo chmod +x /usr/local/bin/docker-compose

如果文件是手动下载的,请在运行命令之前将下载的文件复制到/usr/local/bin目录。要确认安装成功且运行正常,请运行以下命令:

docker-compose version

结果应该是 Docker Compose 的版本,如下所示:

docker-compose version 1.29.1, build 5becea4c
docker-py version: 5.0.0
CPython version: 3.9.0
OpenSSL version: OpenSSL 1.1.1g 21 Apr 2020

Docker 合成基础知识

与 Docker 文件(Docker 引擎关于如何构建 Docker 映像的一组指令)不同,撰写文件是一个 YAML 配置文件,它定义了启动应用所需的服务、网络和卷。Docker 希望合成文件出现在调用docker-compose命令的同一路径中,并被称为docker-compose.yaml(或docker-compose.yml)。这可以使用-f标志,后跟撰写文件名的路径来覆盖。

Docker 合成版本概述

在 Docker 桌面 3.4 版中,Docker 推出了一个新版本的 Docker Compose,称为 Compose V2。Compose V2 被认为是旧版本 Compose 的替代产品。Docker 提取了 Compose 文件的 YAML 文件模型,围绕它创建了一个社区,并将其作为一个规范提交,称为 Compose 规范。撰写 V2 实现撰写规范。然而,它的功能还不能与 Compose V1 媲美,可以从 Docker 桌面设置的实验设置中启用。鉴于缺乏功能对等,这一章的重点是撰写 V1。如果您需要“合成 V2”中的特定功能,例如对 GPU 设备和配置文件的支持,您可以使用本章的其余部分作为指南。只需将docker-compose命令(带连字符)替换为docker compose(用空格替换连字符),这些命令仍然可以工作。

合成文件版本和合成规范

虽然合成文件是一个 YAML 文件,但 Docker 使用文件开头的版本密钥来确定支持 Docker 引擎的哪些功能。合成文件格式有三种版本。随着 Docker Compose v1.27.0 和 Docker Compose V2 的推出,Docker 统一了 2.x 和 3.x 版本的 Compose 文件格式,并将其作为规范提交给了社区。以下是合成文件格式的前三个版本的简要说明:

  • 版本 1: 版本 1 被认为是遗留格式。如果 Docker 合成文件在 YAML 文件的开头没有版本密钥,Docker 会将其视为版本 1 格式。版本 1 已被否决,不再受支持。

  • 版本 2.x: 由 YAML 文件开头的版本 2.x 密钥标识的版本 2.x。

  • 版本 3.x: 由 YAML 文件开头的版本 3.x 密钥标识的版本 3.x。

  • Compose spec:Compose spec 统一了 Compose 文件格式的 2.x 和 3.x 版本,并已作为规范提交给社区。Compose 规范也反对版本键。

以下几节将讨论这三个主要版本之间的差异。

版本 1

在 YAML 文件的根目录下没有版本密钥的 Docker 合成文件被视为版本 1 合成文件。Docker Compose 的未来版本将弃用并删除版本 1,因此我不建议编写版本 1 文件。除了不赞成之外,版本 1 还有以下主要缺点:

  • 版本 1 文件不能声明命名服务、卷或生成参数。

  • 只有使用 links 标志才能启用容器发现。

版本 2

Docker Compose 第 2 版文件具有值为 2 或 2.x 的版本密钥。第 2 版引入了一些更改,这使得第 2 版与以前版本的 Compose 文件不兼容。其中包括:

  • 所有服务都必须存在于 services 键中。

  • 所有容器都位于特定于应用的默认网络上,可以通过主机名发现容器,主机名由服务名指定。

  • 链接变得多余。

  • 引入了depends_on标志,允许您指定相关的容器以及容器出现的顺序。

版本 3

Docker Compose 第 3 版文件有一个值为 3 或 3.x 的版本密钥。第 3 版删除了几个不推荐使用的选项,包括volume_drivervolumes_from等等。版本 3 还增加了一个deploy键,用于 Docker Swarm 上服务的部署和运行。

撰写规范

Docker 统一了 Compose 文件格式的 2.x 和 3.x 版本,并引入了 Compose 规范。在 Docker Compose 及以上版本中,Docker 将 Compose 规范实现为当前最新的格式。Docker 也声明以前的版本是遗留的,尽管它们仍然受支持。合成规范也反对合成文件中的版本密钥。Compose 规范允许您定义不依赖于任何特定云提供商的容器应用,包括多容器应用所需的基本构建块:

  • Services key 定义了计算方面,实现为一个或多个容器。

  • Networks key 定义了服务如何相互通信。

  • Volumes 键定义服务如何存储持久数据。

清单 7-1 中显示了一个示例引用组合文件。

services:
    database:
      image: mysql
      environment:
        MYSQL_ROOT_PASSWORD: dontusethisinprod
      volumes:
        - db-data:/var/lib/mysql
    webserver:
      image: 'nginx:alpine'
      ports:
        - 8080:80
      depends_on:
        - cache
        - database
    cache:
      image: redis

volumes:
    db-data:

Listing 7-1A Sample Docker Compose File

与 docker 文件类似,Compose 文件可读性很强,很容易理解。这个合成文件用于一个典型的 web 应用,该应用包括一个 web 服务器、一个数据库服务器和一个缓存服务器。Compose 文件声明当 Docker Compose 运行时,它将启动三个服务——web 服务器、数据库服务器和缓存服务器。web 服务器依赖于数据库和缓存服务,这意味着除非启动数据库和缓存服务,否则不会启动 web 服务。缓存和数据库关键字表明,对于缓存,Docker 必须为数据库调出 Redis 映像和 MySQL 映像。

要调出所有容器,发出以下命令:

docker-compose up -d

[+] Running 4/4
 ⠿ Network code_default        Created   0.1s
 ⠿ Container code_database_1   Started   1.2s
 ⠿ Container code_cache_1      Started   1.1s
 ⠿ Container code_webserver_1  Started   2.3s

命令发出后,Docker 会在后台调出所有的服务。请注意,尽管合成文件首先定义了数据库,其次是 web 服务器,最后是缓存,Docker 仍然在调用 web 服务器容器之前调用缓存容器和数据库容器。这是因为您为 web 服务器定义了如下的depends_on键:

depends_on:
    - cache
    - database

这告诉 Docker 在启动 web 服务器之前先启动缓存和数据库容器。然而,Docker Compose 不会等待并检查缓存容器是否准备好接受连接,然后打开数据库容器——它只是按照指定的顺序打开容器。

您可以通过键入以下命令来查看日志:

docker-compose logs

webserver_1  | [notice] 1#1: nginx/1.21.1
database_1   | [Note] [Entrypoint]: Switching to dedicated user 'mysql'
cache_1      | # Server initialized
cache_1      | * Ready to accept connections

Docker 将聚合每个容器的STDOUT,并在前台运行时将它们流式传输。默认情况下,docker-compose日志将只显示日志的快照。如果您想让日志连续流式传输,您可以添加-f--follow标志来告诉 Docker 继续流式传输日志。或者,如果您想查看每个容器中最后的 n 个日志,您可以键入:

docker-compose logs --tail=n

其中 n 是你需要看到的行数。停止容器就像发出stop命令一样简单,如下所示:

docker-compose stop

[+] Running 3/3
 ⠿ Container code_webserver_1  Stopped   0.5s
 ⠿ Container code_database_1   Stopped   1.4s
 ⠿ Container code_cache_1      Stopped   0.4s

要恢复停止的容器,发出start命令:

docker-compose start
[+] Running 3/3
 ⠿ Container code_database_1   Started  1.8s
 ⠿ Container code_cache_1      Started  1.9s
 ⠿ Container code_webserver_1  Started  0.7s

要完全拆除容器,请发出以下命令:

docker-compose down

这将停止所有容器,还将删除发出docker-compose up 时创建的相关容器、网络和卷。

[+] Running 4/4
 ⠿ Container code_webserver_1  Removed  0.5s
 ⠿ Container code_cache_1      Removed  0.6s
 ⠿ Container code_database_1   Removed  1.3s
 ⠿ Network code_default        Removed  0.2s

Docker 构成档案参考

回想一下,合成文件是一个 YAML 文件,用于 Docker 读取和设置合成作业的配置。本节解释了 Docker 合成文件中不同键的作用。

服务密钥

服务是组合 YAML 的第一个根键,它是需要创建的容器的配置。

构建密钥

生成密钥包含在生成时应用的配置选项。构建键可以是构建上下文的路径,也可以是由上下文和可选 Dockerfile 位置组成的详细对象:

services:
    app:
        build: ./app

services:
    app:
        build:
            context: ./app
            Dockerfile: dockerfile-app

上下文关键字

上下文键设置构建的上下文。如果上下文是相对路径,则该路径被视为相对于合成文件的位置。

build:
    context: ./app
    Dockerfile: dockerfile-app

图像键

如果图像标签与构建选项一起提供,Docker 将构建图像,然后用提供的图像名称和标签命名和标记图像。

services:
    app:
        build: ./app
        image: sathyabhat:app

环境/env_file 键

environment键为应用设置环境变量,而env_file提供了环境文件的路径,读取该文件以设置环境变量。environmentenv_file都可以接受单个文件或多个文件作为一个数组。

在下面的例子中,对于 app 服务,两个环境变量——PATHAPI_KEY,分别具有值/homethisisnotavalidkey——被设置为 app 服务。

services:
    app:
        image: mysql
        environment:
            PATH: /home
            API_KEY: thisisnotavalidkey

在下面的示例中,从名为.env的文件中获取环境变量,并将这些值分配给 app 服务。

services:
    app:
        image: mysql
        env_file: .env

在下面的示例中,提取了在env_file键下定义的多个环境文件,并将这些值分配给 app 服务。

services:
    app:
        image: mysql
        env_file:
            - common.env
            - app.env
            - secrets.env

依赖键

该键用于设置跨各种服务的依赖性要求。考虑以下配置:

services:
    database:
        image: mysql
    webserver:
        image: nginx:alpine
        depends_on:
            - cache
            - database
    cache:
        image: redis

发出docker-compose up时,Docker 将按照定义的依赖顺序调出服务。在前一个例子中,Docker 在启动 webserver 服务之前启动了缓存和数据库服务。

Caution

使用depends_on键,Docker 将只按定义的顺序调出服务;它不会等待每个服务都准备好,然后调用后续服务。

图像键

此键指定当一个容器被打开时使用的图像的名称。如果映像在本地不存在,Docker 会在构建密钥不存在的情况下尝试提取它。如果构建密钥在合成文件中,Docker 将尝试构建并标记图像。

services:
    database:
        image: mysql

端口键

此键指定将向端口公开的端口。当提供此密钥时,您可以指定两个端口(即,容器端口将向其公开的 Docker 主机端口或仅容器端口),在这种情况下,将选择主机上随机的临时端口号。

services:
    database:
        image: nginx
        ports:
            - "8080:80"

services:
    database:
        image: nginx
        ports:
            - "80"

卷密钥

Volumes 既可以作为顶级键,也可以作为服务的子选项。当volumes被称为顶级键时,它允许您提供将用于底层服务的命名卷。其配置如下所示:

services:
    database:
        image: mysql
        environment:
            MYSQL_ROOT_PASSWORD: dontusethisinprod
        volumes:
            - "dbdata:/var/lib/mysql"
    webserver:
        image: nginx:alpine
        depends_on:
            - cache
            - database
    cache:
        image: redis

volumes:
    dbdata:

如果没有顶级 volumes 键,Docker 将在创建容器时抛出一个错误。考虑以下配置,其中跳过了volumes键:

services:
  database:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: dontusethisinprod
    volumes:
        - "dbdata:/var/lib/mysql"
  webserver:
    image: nginx:alpine
    depends_on:
        - cache
        - database
  cache:
    image: redis

尝试启动容器会引发错误,如下所示:

docker-compose up
service "database" refers to undefined volume dbdata: invalid compose project

也可以使用绑定安装。您只需提供路径,而不是引用指定的卷。考虑这种配置:

services:
    database:
        image: mysql
        environment:
            MYSQL_ROOT_PASSWORD: dontusethisinprod
        volumes:
            - ./dbdir:/var/lib/mysql
    webserver:
        image: nginx:alpine
        depends_on:
            - cache
            - database
    cache:
        image:redis

volume键的值为./dbdir:/var/lib/mysql,这意味着 Docker 将把当前目录中的dbdir挂载到容器的/var/lib/mysql目录中。相对路径是相对于合成文件的目录来考虑的。

重新启动键

重启键为容器提供重启策略。默认情况下,重启策略设置为no,这意味着无论如何 Docker 都不会重启容器。以下是可用的重启策略:

  • no:容器永远不会重启

  • always:容器在退出后总是会重新启动

  • on-failure:如果由于错误退出,容器将重新启动

  • unless-stopped:除非明确退出或者 Docker 守护进程停止,否则容器将总是重新启动

docker 由 CLI 参考组成

命令自带一组子命令。下面几节将对它们进行解释。

build 子命令

build命令读取合成文件,扫描构建密钥,然后继续构建和标记图像。图像被标记为project_service。如果合成文件没有构建键,Docker 将跳过构建任何图像。用法如下:

docker-compose build <options> <service...>

如果提供了服务名,Docker 将继续为该服务构建映像。否则,它将为所有服务构建映像。一些常用的选项如下:

--compress: Compresses the build context
--no-cache Ignore the build cache when building the image

down 子命令

down命令停止容器,并将继续移除容器、卷和网络。其用法如下:

docker-compose down

exec 子命令

compose exec命令相当于docker exec命令。它允许您在任何容器上运行特定的命令。其用法如下:

docker-compose exec  <service> <command>

logs 子命令

logs命令显示所有服务的日志输出。其用法如下:

docker-compose logs <options> <service>

默认情况下,logs将只显示所有服务的最后日志。通过提供服务名,您可以只显示一个服务的日志。-f选项跟随日志输出。

停止子命令

stop命令停止容器。其用法如下:

docker-compose stop

练习

Building and Running a Mysql Database Container With a Web UI for Managing the Database

在本练习中,您将构建一个多容器应用,其中包含一个用于 MySQL 数据库的容器和另一个用于 MySQL 的流行 Web UIadminer的容器。因为您已经有了 MySQL 和adminer的预构建映像,所以您不需要构建它们。

提示与本练习相关的源代码、Dockerfile 和docker-compose文件可以在本书的 GitHub repo 上 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-7/exercise-1目录中找到。

您可以从 Docker 合成文件开始,如下所示:

services:
  mysql:
    image: mysql
    environment:
        MYSQL_ROOT_PASSWORD: dontusethisinprod
    ports:
        - 3306:3306
    volumes:
        - dbdata:/var/lib/mysql
  adminer:
    image: adminer
    ports:
        - 8080:8080

volumes:
    dbdata:

这个组合文件将您在本书中学到的所有内容合并到一个简洁的文件中。因为您的目标是 Compose 规范,所以可以省略 version 标记。在 Services 下,定义两个服务——一个用于数据库,它拉入一个名为mysql的 Docker 映像。创建容器时,环境变量MYSQL_ROOT_PASSWORD为数据库设置 root 密码,容器的端口 3306 被发布到主机。

MySQL 数据库中的数据存储在一个名为dbdata的卷中,并挂载到容器的/var/lib/mysql目录中。这是 MySQL 存储数据的地方。换句话说,保存到容器中数据库的任何数据都由名为dbdata的卷处理。另一个名为adminer的服务只是拉入一个名为adminer的 Docker 映像,并将端口 8080 从容器发布到主机。

通过键入以下命令来验证合成文件:

docker-compose config

如果一切正常,Docker 将打印出解析后的合成文件;它应该是这样的:

services:
  adminer:
    image: adminer
    networks:
      default: null
    ports:
    - mode: ingress
      target: 8080
      published: 8080
      protocol: tcp
  mysql:
    environment:
      MYSQL_ROOT_PASSWORD: dontusethisinprod
    image: mysql
    networks:
      default: null
    ports:
    - mode: ingress
      target: 3306
      published: 3306
      protocol: tcp
    volumes:
    - type: volume
      source: dbdata
      target: /var/lib/mysql
      volume: {}
networks:
  default:
    name: docker-compose-adminer_default
volumes:
  dbdata:

通过键入如下命令运行所有容器:

docker-compose up -d

容器将在后台启动,如下所示:

docker-compose up -d
[+] Running 3/3
 ⠿ Network docker-compose-adminer_default      Created   0.1s
 ⠿ Container docker-compose-adminer_adminer_1  Started   1.0s
 ⠿ Container docker-compose-adminer_mysql_1    Started   1.1s

现在看一下日志。键入以下命令:

docker-compose logs
adminer_1  | PHP 7.4.22 Development Server (http://[::]:8080) started
mysql_1    | [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.26-1debian10 started.
mysql_1    | [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.26'

这告诉你adminer UI 和 MySQL 数据库已经准备好了。导航至http://localhost:8080尝试登录。应当加载adminer登录页面(见图 7-1 )。

img/463857_2_En_7_Fig1_HTML.jpg

图 7-1

管理员登录页面

注意,服务器已经填充了值db。因为docker-compose为应用创建了自己的网络,所以每个容器的主机名就是服务名。在这种情况下,MySQL 数据库服务名是mysql,数据库将可以通过mysql主机名访问。输入用户名root和在MYSQL_ROOT_PASSWORD环境变量中输入的密码(见图 7-2 )。

img/463857_2_En_7_Fig2_HTML.jpg

图 7-2

管理员登录详细信息

如果细节正确,您应该会看到如图 7-3 所示的数据库页面。

img/463857_2_En_7_Fig3_HTML.jpg

图 7-3

登录后即可获得数据库详细信息

Converting Newsbot to a Docker Compose Project

在第六章的练习中,您向 Newsbot 添加了卷,数据被保存到 MySQL 容器中。您还分别启动了newsbotmysql容器,并将它们连接到公共桥接网络。在本练习中,您将编写一个包含 Newsbot 容器和 MySQL 容器的 Docker 合成文件,并附加一个卷来保存数据。在本练习中,您将看到 Docker Compose 如何轻松地打开多个容器,每个容器都有其相关的属性。

提示与本练习相关的源代码、Dockerfile 和docker-compose文件可以在本书的 GitHub repo 上 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-7/exercise-2目录中找到。

让我们创建一个新的 Docker 合成文件,并添加以下内容:

services:
  newsbot:
    build: .
    depends_on:
      - mysql
    restart: "on-failure"
    environment:
      - NBT_ACCESS_TOKEN=${NBT_ACCESS_TOKEN}
    networks:
      - newsbot

  mysql:
    image: mysql
    volumes:
        - newsbot-db:/var/lib/mysql
    environment:
        - MYSQL_ROOT_PASSWORD=dontusethisinprod
    networks:
      - newsbot

volumes:
  newsbot-db:

networks:
  newsbot:

因为您需要两个服务,一个用于 Newsbot,一个用于 MySQL 服务器,所以它们都有对应的键。对于 Newsbot,您添加一个值为mysqldepends_on键,表示 MySQL 容器应该在 Newsbot 之前启动。但是正如您之前看到的,Docker 不会等待 MySQL 容器准备好,因此 Newsbot 被修改为在尝试连接到mysql容器之前等待 60 秒。还有一个重启策略,在应用失败时重启newsbot容器。

Newsbot 需要 Telegram bot API 令牌,您可以将它从同一个主机环境变量传递给容器环境变量NBT_ACCESS_TOKEN。这两个服务中的每一个都有一个网络密钥,指示容器将被连接到newsbot网络。最后,添加卷和网络的顶级键,声明为newsbot-db用于保存卷的 MySQL 数据,声明为newsbot用于保存网络。

您可以通过键入如下所示的config命令来验证合成文件是否正确有效:

docker-compose config

Docker 打印您编写的合成的配置,类似于合成文件本身。

services:
  mysql:
    environment:
      MYSQL_ROOT_PASSWORD: dontusethisinprod
    image: mysql
    networks:
      newsbot: null
    volumes:
    - type: volume
      source: newsbot-db
      target: /var/lib/mysql
      volume: {}
  newsbot:
    build:
      context: exercise-2/newsbot-compose
      dockerfile: exercise-2/newsbot-compose/Dockerfile
    depends_on:
      mysql:
        condition: service_started
    environment:
      NBT_ACCESS_TOKEN: ""
    networks:
      newsbot: null
    restart: on-failure
networks:
  newsbot:
    name: newsbot-compose_newsbot
volumes:
  newsbot-db:
    name: newsbot-compose_newsbot-db

现在运行撰写应用。不要忘记传递你在第三章中生成的 Newsbot API 键的值。

NBT_ACCESS_TOKEN=<token> docker-compose up

您应该看到容器正在构建和启动,如下所示:

[+] Running 4/4
 ⠿ Network newsbot-compose_newsbot      Created    0.0s
 ⠿ Volume "newsbot-compose_newsbot-db"  Created    0.0s
 ⠿ Container newsbot-compose_mysql_1    Started    1.6s
 ⠿ Container newsbot-compose_newsbot_1  Started    1.8s

Attaching to mysql_1, newsbot_1
newsbot_1  | INFO:  <module> - Starting up
newsbot_1  | INFO:  <module> - Waiting for 60 seconds for db to come up
mysql_1    |  [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
mysql_1    |  [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
newsbot_1  | INFO:  <module> - Checking on dbs
newsbot_1  | INFO:  get_updates - received response: {'ok': True, 'result': []}
newsbot_1  | INFO:  get_updates - received response: {'ok': True, 'result': []}
newsbot_1  | INFO:  get_updates - received response: {'ok': True, 'result': []}
newsbot_1  | INFO:  get_updates - received response: {'ok': True, 'result': []}

最后一行表示机器人正在工作。尝试通过在电报机器人中输入/sources docker然后输入/fetch来设置一个源并获取数据。如果一切顺利,您应该会在图 7-4 中看到结果。

img/463857_2_En_7_Fig4_HTML.jpg

图 7-4

行动中的 subreddit fetcher 机器人

您可以更进一步,通过修改 Compose 文件来包含adminer服务,这样您就有一个 WebUI 来检查内容是否被保存到数据库中。修改现有的 Docker compose 文件,使其包含如下所示的adminer服务,并将其保存到名为docker-compose.adminer.yml的文件中:

services:
  newsbot:
    build: .
    depends_on:
      - mysql
    restart: "on-failure"
    environment:
      - NBT_ACCESS_TOKEN=${NBT_ACCESS_TOKEN}
    networks:
      - newsbot

  mysql:
    image: mysql
    volumes:
        - newsbot-db:/var/lib/mysql
    environment:
        - MYSQL_ROOT_PASSWORD=dontusethisinprod
    networks:
      - newsbot

  adminer:
    image: adminer
    ports:
        - 8080:8080
    networks:
      - newsbot

volumes:
  newsbot-db:

networks:
  newsbot:

按如下方式键入config命令,确认合成文件有效:

docker-compose -f docker-compose.adminer.yml config
services:
  adminer:
    image: adminer
    networks:
      newsbot: null
    ports:
    - mode: ingress
      target: 8080
      published: 8080
      protocol: tcp
  mysql:
    environment:
      MYSQL_ROOT_PASSWORD: dontusethisinprod
    image: mysql
    networks:
      newsbot: null
    volumes:
    - type: volume
      source: newsbot-db
      target: /var/lib/mysql
      volume: {}
  newsbot:
    build:
      context: exercise-2/newsbot-compose
      dockerfile: exercise-2/newsbot-compose/Dockerfile
    depends_on:
      mysql:
        condition: service_started
    environment:
      NBT_ACCESS_TOKEN: ""
    networks:
      newsbot: null
    restart: on-failure
networks:
  newsbot:
    name: newsbot-compose_newsbot
volumes:
  newsbot-db:
    name: newsbot-compose_newsbot-db

现在,使用以下命令删除现有的合成文件:

docker-compose down

[+] Running 3/3
 ⠿ Container newsbot-compose_newsbot_1  Removed      1.0s
 ⠿ Container newsbot-compose_mysql_1    Removed      0.1s
 ⠿ Network newsbot-compose_newsbot      Removed      0.1s

由于数据保存在卷中,您不必担心数据丢失。

使用以下命令再次启动服务。不要忘记传递你在第三章中生成的 Newsbot API 键的值。

NBT_ACCESS_TOKEN=<token> docker-compose -f docker-compose.adminer.yml up

Running 4/4
 ⠿ Network newsbot-compose_newsbot      Created  0.1s
 ⠿ Container newsbot-compose_adminer_1  Started  7.1s
 ⠿ Container newsbot-compose_mysql_1    Started  7.1s
 ⠿ Container newsbot-compose_newsbot_1  Started  5.1s
Attaching to adminer_1, mysql_1, newsbot_1

mysql_1    | [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
newsbot_1  | INFO: <module> - Starting up
newsbot_1  | INFO: <module> - Waiting for 60 seconds for db to come up
newsbot_1  | INFO: <module> - Checking on dbs
newsbot_1  | INFO: get_updates - received response: {'ok': True, 'result': []}

通过前往http://localhost:8080导航至adminer。使用root用户名登录,密码设置为MYSQL_ROOT_PASSWORD值,服务器值为mysql。单击 Newsbot 数据库中的source作为表,然后选择选择数据。您应该会看到之前设置为source的子编辑(参见图 7-5 )。

img/463857_2_En_7_Fig5_HTML.jpg

图 7-5

项目运行时将数据保存到数据库中

成功!应用正在运行,数据被保存到 MySQL 数据库并被持久化,尽管删除并重新创建了容器。

摘要

在本章中,您了解了 Docker Compose,包括如何安装它以及为什么使用它。您还深入研究了 Docker 编写文件和 CLI。您运行了一些关于使用 Docker Compose 构建多容器应用的练习。您还了解了如何使用 Docker Compose 将 Newsbot 项目扩展到多容器应用,方法是添加一个链接数据库和一个 Web UI 来编辑数据库。

八、准备生产部署

在前几章中,您了解了 Docker 及其相关术语,并深入了解了如何使用 Docker 文件构建 Docker 映像。您还了解了如何持久化容器生成的数据,并借助 Docker 的网络特性实现了运行中的容器之间的网络通信。然后,您了解了 Docker Compose 如何通过在一个简单的 YAML 文件中编写您的需求并将其作为输入提供给 Docker Compose 来简化多容器应用的运行。

在本章中,您将学习如何准备 Docker 映像以在生产中部署您的应用,包括持续集成的简要概述,以及如何使用 GitHub 操作设置持续集成。本章涉及容器编排和各种可用的编排器。它包括对市场上最受欢迎的容器编排器之一 Kubernetes 的概述。

持续集成

持续集成是每天多次自动将每个开发人员的代码变更合并到源代码库的主要分支中的实践。除了合并之外,该过程还运行不同的测试——包括单元测试、集成测试、功能测试——当所有测试都通过时,就会创建并保存一个构建工件,通常保存到某种工件存储中。

这个生成的工件被带入下一步,部署到开发和阶段环境中,形成 CI/CD(持续集成/持续交付)管道。随着软件构建和测试过程的成熟,许多团队从连续交付切换到连续部署并不罕见。在连续交付中,最终的工件可以随时进行部署,但是部署通常是手动启动的。持续部署完全自动化了端到端的构建到发布的管道,最终的构建工件也被自动部署。

由于 CI/CD 管道提供的快速反馈周期,CI/CD 在今天的软件开发生命周期中变得非常流行。有了一个定义良好的管道,开发人员就有可能打开一个 GitHub Pull 请求,并对其源代码进行更改,让持续集成管道生效,开始新代码的测试,完成静态分析,并准备好一个工件进行部署,所有这些都在几分钟内自动完成,无需任何人手动启动或运行任何东西。

有了 Docker,CI/CD 变得更加轻松。有了 Dockerfile,就有了一种简单、可重复的方法来重建所需的依赖关系映像,Docker 映像的可移植性意味着该映像可以在任何安装了 Docker 守护程序的主机上运行。这是与以前打包软件方式的一个重要区别。Docker 图像是独立的。不再有关于正确获得所需版本的依赖关系、主机操作系统依赖关系等等的麻烦。对于微服务,作为源代码签入的一部分,测试依赖系统变得更加容易。有了定义了所需服务的 Docker Compose 文件,一个简单的docker-compose up就足以调出服务并测试它们。

市场上有许多 CI 工具,大多数源代码管理(SCM)系统——如 GitHub 和 git lab——本身提供了持续集成特性的子集。下一节解释如何使用 GitHub 动作在 GitHub 上设置持续集成。

GitHub 动作

GitHub Actions 使得围绕您正在处理的 Git 存储库建立自动化部署和工作流变得容易。使用 GitHub Actions,您可以定义一个在每次提交时触发的工作流,或者将它推到一个分支来执行各种操作。这些动作可以从简单的回声到复杂的林挺,旋转多个容器。

GitHub 动作是事件驱动的,所以工作流是基于特定的事件触发的,比如一个新的拉请求被打开或者一个新的提交被推送到存储库,等等。每个事件都会触发一个工作流。一个工作流可以有一个或多个作业,一个作业可以包括在 GitHub 或其他地方(如 Python 包索引(PyPI)或 Docker Hub)上构建、测试、打包或发布的一系列步骤。

GitHub 动作运行在名为 runners 的服务器上。跑步者安装了 GitHub Actions runner 应用,监听命令。GitHub 提供托管的跑步者,但是你可以运行你自己的跑步者。如果您有在自己的环境中构建软件的法规遵从性要求,这将特别有用。

要作为管道的一部分执行的步骤是使用称为动作工作流文件的文件来定义的。工作流文件使用基于 YAML 的规范来定义需要运行的事件、作业和步骤,并支持条件以允许特定作业在满足条件时运行。对于 GitHub 获取并运行工作流的操作,GitHub 希望这些工作流文件保存在存储库根目录下的.github/workflows目录中。

在本节中,您将编写一个将在每次提交时运行的示例工作流文件。在继续之前,您应该有一个空的公共 GitHub repo 来测试 GitHub 操作。清单 8-1 中显示了一个示例工作流文件。

name: Run compose
on: [push]

jobs:
  run-compose:
    timeout-minutes: 10
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v1
    - name: Start containers
      run: docker-compose version

Listing 8-1A Sample Github Actions Workflow File

Tip

动作规范文件的语法和密钥可以在 GitHub 文档页面的 https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions 找到。

您可以检查操作 YAML 文件中的各种键:

  • name:name键定义将显示在动作选项卡上的工作流文件的名称。

  • on:on键定义了触发工作流的事件。这使您可以确定工作流应该在什么时间点或在哪个事件期间运行。

  • timeout-minutes:timeout键让你定义一个任务在被 GitHub 取消之前可以运行多长时间。

  • runs-on:runs-on键定义了运行作业的转轮。

  • steps:steps键定义了特定任务必须运行的步骤。

  • uses : 这个键告诉 GitHub 获取一个特定的动作。除了您可以运行的步骤之外,GitHub Actions 还允许您使用自己开发的第三方操作,减少了重新构建一切的需要。在这个具体的例子中,您指示 GitHub Actions 获取 checkout 操作,后者执行 Git checkout 并将源代码下载到 runner。

  • run:run键让你定义可以运行的自定义命令。当您想要运行自定义命令没有涵盖的内容时,这很有用。

再次查看 Actions 工作流文件,它定义了一个 GitHub runner,它将检查源代码并运行一个docker-compose version命令。

将该文件另存为.github/workflows/compose.yaml,提交文件,并将其推送到 GitHub repo。一旦将提交推送到远程存储库,GitHub 应该会立即启动工作流。从您的 GitHub 存储库页面,点击 Actions 选项卡,您可以看到工作流的结果。

如果你更喜欢 CLI 方法,GitHub 有一个 CLI 工具,它可以给你反馈,而不必打开标签页并导航到操作。要开始使用,需要安装 GitHub CLI,如 https://cli.github.com/ 中所述。

安装 CLI 后,打开一个新的终端会话。通过键入以下命令使用 GitHub 进行身份验证:

gh auth login

按照说明操作,您应该能够成功登录。登录后,切换到您创建的公共回购目录。使用以下命令切换到新克隆的存储库:

cd <repo>

在 repo 中,您可以使用gh workflow list命令查看工作流:

gh workflow list

这应该只显示一个工作流,即您创建的工作流,如下所示:

gh workflow list
Run compose  active  <workflow id>

末尾的数字是工作流的 ID。您可以使用workflow view命令检查工作流程的结果,如下所示:

gh workflow view <workflow id>

Run compose - compose.yaml
ID: <workflow id>

Total runs 1
Recent runs
✓  test actions  Run compose  master  push  <run id>

您可以通过使用gh run命令更深入地研究这个运行,如下所示:

gh run view <run id>

✓ master Run compose · <run id>
Triggered via push about 10 hours ago

JOBS
✓ run-compose in 5s (ID <job id>)

您可以使用作业 ID 和带有--job参数的gh run命令进一步研究作业的结果:

gh run view --job <job id>

✓ master Run compose · <run id>
Triggered via push about 10 hours ago

✓ run-compose in 5s (ID <job id>)
  ✓ Set up job
  ✓ Checkout
  ✓ Start containers
  ✓ Complete job

因此,通过一个简单的 YAML 文件,您已经定义并设置了您的持续集成工作流,它将在每次新的推送时自动触发。这个例子仅仅展示了一个运行docker-compose version命令的简单例子。然而,有了大量定制的社区构建的动作以及对定制命令的支持,很容易建立一个全面的持续集成系统,它可以运行 linters,执行容器安全检查,甚至构建一个新的映像。在本章的后面,作为练习的一部分,您将看到这一过程的运行,并为 Newsbot 建立一个 CI 系统,以便在每次推送时建立一个新的 Docker 映像。

容器编排

编排是将容器部署到合适的主机(或许多主机)并管理已部署容器的生命周期的过程,包括根据不同的指标(如 CPU/内存利用率、网络流量等)增加或减少容器以及底层节点的数量。编排还负责在节点和容器崩溃或出错时替换它们。Orchestrators 用于执行保持容器平稳运行所需的许多手动任务,而不需要操作员的手动干预。

对管弦乐队的需求

前面,您了解了容器使得部署应用变得容易。只需一个命令,您就可以启动并运行一个或多个服务,所需的依赖项包含在 Docker 映像中,或者包含在 Docker 合成文件中的多个链接容器中。那么问题就来了——如果应用和依赖项是自包含的,为什么还需要 orchestrators 呢?

容器减轻了开发人员运行链接服务的痛苦。开发人员可以构建他们的映像,在本地运行它们,并继续进行本地更改,而不必更新本地开发设置或将软件部署到指定的开发环境。这个过程会不断发展和变化,尤其是当你有很多人在一个项目上工作的时候。通过消除运行软件所涉及的辛劳,并让开发人员通过简单的docker rundocker-compose up来运行他们的应用,Docker 使迭代和构建变得更加容易。

在生产环境中,事情会变得更加复杂。容器使生产部署变得更容易,但是必须运行许多容器并维护它们的生命周期变成了一件乏味的事情。您可能想知道,为什么需要运行多个容器。

考虑一下 Newsbot,你在这本书里一直在做的聊天机器人应用。这是一个简单的 Python 应用,它不断轮询 Telegram bot API,响应消息,并回发到 Telegram。当请求数量较少时,一个容器就足以及时响应请求。然而,随着越来越多的人开始使用它,机器人必须处理的请求数量显著增加,在某一点上,只有一个容器将不足以响应请求。为了应对需求,您需要通过增加容器的数量来扩大规模。如果没有 orchestrators,要做到这一点,您必须运行命令来打开新的容器。这样做一次或两次是可以的,但必须重复这样做是不可行的。这就是管弦乐队的用武之地。

管弦乐队是如何工作的?

虽然 orchestrators 的具体实现因工具而异,但总体过程是相同的。大多数管弦乐队通常分为两层:

  • 控制层,也称为控制平面

  • 工作层,也称为工作层

协调器的控制平面处理与控制、运行和管理协调器相关的传入请求和操作,而工作平面处理指定节点中容器的实际调度和协调。

编排流程从对预期目标的声明性描述开始:这可以是一个 YAML 或 JSON 文件,描述要运行什么服务、从哪里下载所需的容器映像(通常指向容器注册表)、要运行的副本数量、链接容器需要什么类型的网络以及在哪里存储持久数据。如果这看起来像你在第七章中学到的 Docker Compose 文件,这是一个有效的观察。

Compose Spec 文件描述了这些确切的需求。然而,Docker Compose 是为单个节点设计的。它不能跨多个节点编排容器,不适合作为编排器,尤其是对于跨多个节点的工作负载。然而,对于单节点工作负载,使用 Docker Compose 可能更容易、更简单。

一旦 orchestrator 收到增加容器数量或部署新容器的请求,它将在运行容器之前执行一系列步骤:

  • 调度程序确定容器将被调度的节点。这是基于可能存在的几个约束来完成的,例如所需的内存、容器所需的 CPU、是否需要 GPU 或特定类别的存储等等。

  • 选择合适的节点;发送启动容器的请求。这包括不同的步骤,例如提取 Docker 映像(如果尚未存在)、设置容器网络以及与所需的卷关联。

  • 启动容器。

  • 如果容器已经配置了运行状况检查,请等待运行状况检查结果为肯定,然后发出容器准备好接受工作负载的信号。

  • 一旦容器启动并运行,orchestrator 将持续监控容器的运行状况检查。如果运行状况检查失败,orchestrator 将终止容器,并在其位置上启动一个新容器。

整个过程在一个循环中连续发生,orchestrator 每隔几秒钟检查一次提交给 orchestrator 的每个容器请求。

流行管弦乐队

Kubernetes 很可能是最受欢迎的管弦乐队,但绝不是唯一的管弦乐队。其他可用的编排器包括:

  • Docker 工人群

  • DC/OS

  • 哈希科尔游牧部落

随着 DC 操作系统的寿命结束和支持时间的延长,HashiCorp Nomad 在不需要 Kubernetes 的所有功能的小公司中慢慢变得更受欢迎。需要注意的另一点是,您不必亲自运行容器编排器来充分利用容器。有许多托管容器编排器处理编排器的控制平面。这将您从运行、维护和升级集群控制平面的操作负担中解放出来,您可以专注于运行和维护您的应用。这些托管协调器包括:

  • 亚马逊女战士

  • 亚马逊 ECS

  • 亚马逊灯塔

  • 蓝色忽必烈服务

  • Azure 容器实例

  • 谷歌库比厄引擎

  • 谷歌云运行

Amazon ECS、Amazon Fargate、Azure Container Instances 和 Google Cloud Run 使用每个公司各自的专有编排引擎,并需要提交它们的自定义规范,之后将对容器进行调度和编排。

亚马逊 EKS、Azure Kubernetes 服务和谷歌 Kubernetes 引擎是托管的 Kubernetes 服务,支持您期望从 Kubernetes 提供商获得的所有功能。Kubernetes 是一个巨大的主题,涵盖它的所有特性是另一本书的主题,超出了本章的范围。出于这个原因,接下来的小节将使用kind(Docker 中的 Kubernetes)来测试 Kubernetes 集群,并尝试运行一些示例应用。

忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈

Kubernetes(也称为 k8s )已经成为当今最流行的容器编排器。Kubernetes 是一个用于部署、扩展、操作和管理容器化应用的开源系统。Kubernetes 是由一群谷歌工程师创建的,他们利用自己运行谷歌内部容器编排器 Borg 的经验来构建一个开源项目。Kubernetes 简化了一些复杂性,缓解了使用 Borg 时观察到的棘手问题。Kubernetes 之所以受欢迎,是因为它相对容易使用,并且提供了开箱即用的功能,包括但不限于:

  • 自动化推出和回滚

  • 全容器生命周期管理

  • 支持水平和垂直缩放

  • 自我修复能力,包括容器和节点级故障恢复能力

  • 基于角色的高级访问控制(RBAC)功能,仅允许授权用户和组访问

Kubernetes 集群有各种运行容器化应用的节点。这些节点通常由运行在云中的虚拟机驱动。在行业中,Kubernetes 节点也可以在运行于内部数据中心机器上的强大裸机硬件上运行,以及在运行于低功耗设备上的边缘设备上运行。如前一节所述,节点被进一步划分为 Kubernetes 控制平面和工作平面,其中包括各种组件。

立方控制平面

Kubernetes 控制平面组件控制集群的状态,并管理集群中要调度的工作负载。控制平面包括各种组件,每个组件可以在单个主节点或多个主节点上运行,这需要高可用性和容错能力。控制平面组件包括:

  • Kubernetes API server(或 kube-API server):kube-API server 公开 Kubernetes API,充当集群的前端,通过它接受对 Kubernetes 集群的任何请求。

  • Etcd : etcd 是一个高度可用的键值存储,用作 Kubernetes 集群数据的后备存储。丢失 etcd 是灾难性的损失,因此应采取一切措施定期备份数据。

  • 调度器:Kubernetes 调度器持续监控可以调度工作负载的可用节点。当新的请求开始一个新的工作负载时,它确定并调度可以调度工作负载的相关节点。

  • 控制器管理器:控制器是一个负责维护单个子组件状态的进程,比如单个节点的状态、一次性作业等等。控制器管理器运行这些控制器中的每一个,并确保它们按预期工作。

Kubernetes 工人飞机

Kubernetes 工作平面包括一个或多个工作节点,每个节点运行各种维护工作负载的节点组件。这些组件包括:

  • kube let:kube let 是一个运行在集群中每个节点上的进程,它向 API 服务器注册它正在运行的节点以接受工作负载。kubelet 确保容器和工作负载在节点中运行,并按照 API 服务器的指示维护容器的生命周期。

  • Kube-proxy:Kube-proxy 是一个网络代理,运行在每个节点上,实现 Kubernetes 的网络特性。kube-proxy 维护网络规则和会话,并将流量路由到所需的容器。

与 Kubernetes 的大多数交互都是通过 API 进行的,kubectl命令行应用允许您通过与 Kubernetes API 对话来控制 Kubernetes 集群。kubectl应用实现与集群交互所需的所有命令,并且在内部,kubectl将 API 调用转换为对 kube-apiserver 的相应 API 调用,以执行这些操作。

一看就亲切

建立一个完整的 Kubernetes 集群是一个相当复杂和乏味的过程,涉及许多步骤,包括创建和提供 TLS 证书、提供所需的节点和安装各种组件、加入各种工作节点和主节点等等。虽然可以使用各种工具为生产用例进行设置,例如kOps (Kubernetes Operations)、kubeadm等等,但是对于本地测试,您不必使用这些工具。

kind,Docker 中 Kubernetes 的简称,是一个使用 Docker 容器作为节点运行本地 Kubernetes 集群的工具。Kubernetes 项目本身使用kind来测试集群版本,您可以使用kind进行本地开发和测试。kind包含一个自包含的 Go 二进制文件,可与 Docker CLI 交互以启动和配置 Kubernetes 集群,几乎不需要对单节点集群进行任何配置。如果需要模拟多个节点,可以提供一个配置文件,其中包含引导这样一个集群所需的节点数。

使用 kind 创建 Kubernetes 集群

在创建 Kubernetes 集群之前,您需要下载并安装kind。这可以通过在 https://github.com/kubernetes-sigs/kind/releases 访问 GitHub 上kind的静态发布页面来实现。一旦安装了所需的二进制文件,就可以通过提供保存kind的磁盘上的完整路径来调用kind

Note

本节仅指kind命令,但一定要替换为kind二进制文件的完整路径,尤其是如果kind二进制文件没有被移动到PATH变量引用的位置。

您还需要下载并安装kubectl,这是用于与 Kubernetes 集群交互的命令行程序。您可以按照位于 https://kubernetes.io/docs/tasks/tools/ 的 Kubernetes 文档页面中的说明进行操作。

要创建群集,请运行以下命令:

kind create cluster --name kind

集群创建可能需要几分钟时间,但是一旦完成,您应该会看到以下日志:

kind create cluster --name kind

img/463857_2_En_8_Figa_HTML.gif

img/463857_2_En_8_Figb_HTML.gif

您可以使用docker ps命令查看由kind调出的容器,如下所示:

docker ps
CONTAINER ID   IMAGE                NAMES
5a5ba27eac95   kindest/node:v1.21.1 kind-control-plane

现在看看集群中运行的 pod。为此,请键入以下命令:

kubectl get pods -A

该命令列出了所有正在运行的窗格。一个 pod 是 Kubernetes 中最小的执行单元。默认情况下,kubectl命令从当前作为上下文激活的名称空间获取资源。要显示所有名称空间的窗格,包括系统名称空间,传递标志-A

NAME                    READY   STATUS
coredns-6p84s           1/1     Running
coredns-ctpsm           1/1     Running
etcd                    1/1     Running
kindnet-76dht           1/1     Running
kube-apiserver          1/1     Running
kube-controller-manager 1/1     Running
kube-proxy-87lbc        1/1     Running
kube-scheduler          1/1     Running

从运行窗格中,您可以看到各种各样的窗格,每个窗格对应于您在上一节中了解到的组件。要删除集群,请键入delete命令,如下所示:

kind delete cluster --name kind

在 Kubernetes 中运行示例服务

现在,您对容器编排有了更好的理解,让我们看看如何获取 Docker 映像并对其进行编排。在本节中,您将使用kind创建一个示例 Kubernetes 集群。一旦运行了一个集群,您将部署一个示例nginx容器。虽然 Docker 图像过于简单,但它让您很好地了解了从使用docker run命令在本地运行容器到使用 Kubernetes 部署容器所需的步骤。

首先,您将使用kind创建一个新的 Kubernetes 集群。键入以下命令启动群集:

img/463857_2_En_8_Figc_HTML.gif

吊舱和部署

在 Kubernetes 中,一个 pod 是运行应用的核心组件。一个 pod 至少有一个容器,但也可以容纳一组相关的容器。一个部署是一个 Kubernetes 对象,它创建 pod,告诉 Kubernetes 应该创建多少个 pod 副本,并指示应该何时/如何更新一个新的 pod。要创建一个部署,您可以应用一个具有 Kubernetes 规范的 YAML 文件,该规范描述了要运行的 pod。

或者,作为一个快速的开始,您也可以使用kubectl应用来创建一个部署,只传递 Docker 映像的名称,使用该映像来创建部署。这适用于快速测试部署,但不建议用于完全部署。要使用 Docker 映像创建部署,请运行以下命令:

kubectl create deployment nginx --image <docker image:tag>

要使用 Docker 映像创建 Kubernetes 部署,请使用以下命令:

kubectl create deployment nginx   --image nginx:1.21
deployment.apps/nginx created

虽然此命令可以让您快速创建一个示例部署,但是更新现有部署可能会很繁琐。通过创建一个 Kubernetes spec YAML 并在需要时更新 YAML,您可以指示kubectl应用 YAML 文件。让我们检查一下因为这个部署而创建的部署规范。为此,请键入以下命令:

kubectl get deploy nginx -o yaml > nginx-deploy.yaml

这将以 YAML 格式输出部署规范,并将其保存到名为nginx-deploy.yaml的文件中。在您喜欢的代码编辑器中打开该文件。您应该看到文件的内容,如清单 8-2 所示。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx
  name: nginx
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:1.21
        imagePullPolicy: IfNotPresent
        name: nginx
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
status: {}

Listing 8-2A Kubernetes Deployment Object Specification File in YAML

虽然对这些字段的深入解释超出了本书的范围,但仍然值得注意的是,像 Kubernetes 这样的 orchestrator 可以为使用docker run命令启动和运行的容器提供几个附加功能。一些值得注意的功能包括:

  • 一个名称空间键,用于将应用隔离在它们自己的范围内,允许更强的基于角色的访问策略的应用。

  • 标签允许跨集群识别对象。

  • 一个副本键,指示编排器应该总是维护多少个容器副本。

  • 一个策略对象,指示应该如何部署新的容器映像,应该推出多少新的容器,以及对于不可用容器的数量应该有多大的容忍百分比。

  • 一个 imagePullPolicy ,描述何时以及如何从容器注册表中提取容器图像。

这些只是部署对象的一些特性。Kubernetes 支持更多针对特定工作负载的内置对象:

  • 一个 StatefulSet 允许您运行一个或多个需要跟踪其持久性和状态的 pod(例如,数据库工作负载)。

  • 一个 DaemonSet 在集群的每个节点上运行 pod(例如,日志代理)。

  • Jobs 和 CronJobs 运行一次性任务,并在任务完成时停止。

因此,orchestrators 为运行各种专门的工作负载提供了大量功能。并不是每个需要容器的人都会从 orchestrators 中受益,因为运行和维护它们会带来开销。对于大型组织来说,在考虑将工作负载转移到容器时,orchestrator 是一项无价的投资。

练习

在这一章中,你学习了基本的持续集成和容器编排。现在,您可以在本地计算机上使用kind和 Kubernetes 尝试一些关于构建持续集成工作流和运行多节点 orchestrator 的实践练习。

Creating Multi-Node Clusters with Kind

You learned earlier that kind,Docker 中 Kubernetes 的简称,是一个使用 Docker 容器作为节点运行本地 Kubernetes 集群的工具。在本练习中,您将学习如何使用kind启动多节点 Kubernetes 集群。

提示与本练习相关的kind配置文件可以在本书的 GitHub repo 上 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-8/exercise-1目录中找到。

kind轻松创建多节点集群进行本地测试。为此,首先在 YAML 创建一个 kind 配置文件。清单 8-3 中的配置文件显示了创建具有三个控制平面节点和三个工作节点的多节点集群所需的配置。

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: control-plane
- role: control-plane
- role: worker
- role: worker
- role: worker

Listing 8-3Configuration Needed to Create a Multi-Node Cluster

将文件另存为kind-multi-node.yml。现在,使用之前使用的命令创建一个新的集群,但是带有一个额外的标志(将该文件用作配置文件),如下所示:

kind create cluster --name kind-multi-node --config kind-multi-node.yml

集群创建可能需要几分钟时间,但是一旦完成,您应该会看到如下所示的日志:

img/463857_2_En_8_Figd_HTML.gif

您可以使用docker ps命令查看由kind调出的容器,如下所示:

CONTAINER ID   IMAGE                  NAMES
0f27d1316302   kindest/haproxy:v202   kind-multi-node-external-load-balancer
2a5b37dc51cc   kindest/node:v1.21.1   kind-multi-node-worker
4413cc424783   kindest/node:v1.21.1   kind-multi-node-control-plane2
bf6f2db610d9   kindest/node:v1.21.1   kind-multi-node-control-plane3
c11c07e67abd   kindest/node:v1.21.1   kind-multi-node-worker3
02afa01cdce6   kindest/node:v1.21.1   kind-multi-node-control-plane
e2e2d427a70f   kindest/node:v1.21.1   kind-multi-node-worker2

由于kind使用容器作为模拟节点的方式,您可以看到有三个控制平面节点、三个工作节点和一个外部负载平衡器节点来路由进入集群的流量。有了多节点 Kubernetes 集群,在生产级 orchestrator 上运行容器化的应用变得非常容易。

Setting Up Continuous Integration for Newsbot

在本练习中,您将为 Newsbot 设置一个持续集成工作流,该工作流将运行 flake8 ,构建 Docker 映像,并将结果映像推送到 Docker Hub。持续集成工作流将使用 GitHub Actions 来设置,但是相同的原理也可以应用于任何持续集成工具。

提示与本练习相关的源代码和 docker 文件,以及 GitHub Actions 工作流文件,都可以在本书的 GitHub repo 上 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-8/exercise-2目录中找到。

本练习还假设您正在使用第七章练习 2 中的 Newsbot 源代码和 docker 文件。您还将设置图书回购的工作流程,即 https://github.com/Apress/practical-docker-with-python 。我们鼓励您派生这个 repo,将它克隆到您的本地计算机上,并在您的派生中实践它,或者在一个完全不同的存储库中实现它。

在本章的前面,您已经了解到 GitHub Actions 工作流文件是一个基于 YAML 的规范文件。让我们从您之前使用的样本规范文件开始。您将对此进行修改,添加三个步骤:

  1. 查看源代码。

  2. 安装所需的 Python 版本。

  3. 使用 pip 安装所需的依赖项。

工作流文件如清单 8-4 所示。

name: Lint and build Docker
on: [push, pull_request]

jobs:
  lint:
    timeout-minutes: 10
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v1

    - name: Setup Python
      uses: actions/setup-python@v2
      with:
        python-version: "3.7"

    - name: Install Dependencies
      run: |
        python -m pip install --upgrade pip
        pwd
        cd source-code/chapter-7/exercise-2/newsbot-compose
        pip install -r requirements.txt

Listing 8-4GitHub Actions Workflow File to Install the Dependencies

将该文件保存到 Git 存储库中的.github/workflows/build-newsbot.yaml,提交更改,并将更改推送到 GitHub。GitHub 动作应该会立即触发。正如您之前看到的,您将使用 GitHub CLI 来验证动作是否被触发。

首先验证工作流是否已创建。键入以下命令:

gh workflow list

如果出现提示,请记住选择正确的基本存储库。您应该会看到这样的结果:

gh workflow list
Lint and build Docker  active  <workflow id>

您可以使用以下命令检查工作流状态的摘要:

gh workflow view <workflow id>
Lint and build Docker - build-newsbot.yaml
ID: <workflow id>

Total runs 1
Recent runs
✓  add workflow  Lint and build Docker  add-lint-build-workflow  push  <run id>

勾号表示工作流运行成功。您可以更详细地检查运行的细节,就像您在前面学到的那样,但是现在,知道它是成功的就足够了。让我们在工作流程中增加一些步骤。

大多数 CI 工作流都有某种林挺和风格指南报告器,以便编写的代码符合编程语言和/或组织的指导方针。对于这个工作流,您将添加 flake8,它将分析代码并提供改进建议。经过这一更改,GitHub Actions 工作流文件现在看起来如清单 8-5 所示。

name: Lint and build Docker
on: [push, pull_request]

jobs:
  lint:
    timeout-minutes: 10
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v1

    - name: Setup Python
      uses: actions/setup-python@v2
      with:
        python-version: "3.7"

    - name: Install Dependencies
      run: |
        python -m pip install --upgrade pip
        cd source-code/chapter-7/exercise-2/newsbot-compose
        pip install -r requirements.txt

    - name: Lint with flake8
      run: |
        pip install flake8
        cd source-code/chapter-7/exercise-2/newsbot-compose
        # run flake8 first to detect any python syntax errors
        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
        # run again to exit treating all errors as warnings
        flake8 . --count --exit-zero --max-complexity=10 --statistics

Listing 8-5GitHub Actions Workflow Updated to Analyze Source Code

保存更改,将它们提交给 repo,并将更改推送到远程。这将再次触发工作流运行,您可以使用gh CLI 应用检查运行。因为您知道工作流存在,所以您可以使用以下命令查看最近运行的工作流:

gh run list
STATUS  NAME               WORKFLOW                 ID
✓      <commit message>  Lint and build Docker  <run id>
X      <commit message>  Lint and build Docker  <run id>
✓      <commit message>  Lint and build Docker  <run id>

您对上一次运行的细节感兴趣,所以使用下面的命令来查看它,注意用上一个命令的输出替换运行 ID 的值:

gh run view <run id>

您应该会得到类似如下所示的结果:

gh run view <run id>
✓ add-lint-build-workflow Lint and build Docker · <run id>
Triggered via push about 4 minutes ago
JOBS
✓ lint in 17s (ID <job id>)

因此,棉绒正常工作。让我们扩展这个工作流,添加一个 Docker 构建和推送作业。您定义了一个名为docker-build的新作业,以及检查代码和运行docker build命令的步骤。

因为它在每个 pull 请求或 push 上运行,而不是用任意版本标记它,所以您可以使用GITHUB_SHA,它是 g itHub 公开的一个环境变量,包含用于构建 Docker 映像的 Git 提交的散列。由于篇幅限制,这里只突出显示与 Docker 构建相关的部分;完整的代码可以在 GitHub repo 上的练习中看到。

  docker-build:
    timeout-minutes: 10
    runs-on: ubuntu-latest
    needs: lint

    steps:
    - name: Checkout
      uses: actions/checkout@v1

    - name: Build Docker Image
      run: |
        cd source-code/chapter-7/exercise-2/newsbot-compose
        docker build -t newsbot:${GITHUB_SHA} .

将这个部分保存到工作流文件,提交它,并将更改推送到 GitHub repo。这应该会再次触发 GitHub 工作流。使用以下命令检查最近的运行:

STATUS   NAME             WORKFLOW              EVENT ID
✓       <commit message>  Lint and build Docker <run id>
✓       <commit message>  Lint and build Docker <run id>
X       <commit message>  Lint and build Docker <run id>
✓       <commit message>  Lint and build Docker <run id>

使用以下命令查看最后一次运行:

gh run view <run id>

✓ add-lint-build-workflow Lint and build Docker · <run id>
Triggered via push about 20 hours ago

JOBS
✓ lint in 11s (ID <job id>)
✓ docker-build in 2m57s (ID <job id>)

您可以看到 Docker 构建作业也是成功的。您可以使用以下命令检查完整的作业日志:

gh run view --log --job=<job id>

日志如清单 8-6 所示。

docker-build    Set up job Current runner version: '2.280.3'
docker-build    Set up job ##[group]Operating System
docker-build    Set up job Ubuntu
docker-build    Set up job 20.04.2
docker-build    Set up job LTS
...

docker-build    Build Docker Image Step 7/7 : CMD ["python", "newsbot.py"]
docker-build    Build Docker Image ---> Running in 6f3911bd1009
docker-build    Build Docker Image Removing intermediate container 6f3911bd1009
docker-build    Build Docker Image ---> ab0d26e8298e
docker-build    Build Docker Image Successfully built ab0d26e8298e
docker-build    Build Docker Image Successfully tagged newsbot:639bc2

Listing 8-6The Full Job Logs

为了完成这个练习,添加最后一个步骤,将新构建的 Docker 映像推送到 Docker Hub。在此之前,您必须在 https://hub.docker.com 上创建一个帐户。记下注册时使用的用户名和密码——您将使用它来验证 GitHub 操作。要推送到 Docker Hub 存储库,您必须进行两项更改:

  1. 在构建步骤中,将您的 Docker Hub 用户名作为映像的前缀。

  2. 将 Docker Hub 凭据添加到 GitHub。

要添加 Docker Hub 凭据,请从您推送更改的 GitHub 存储库中,选择“设置”、“密码”。单击 New Repository Secret,添加DOCKER_USERNAME作为名称,并输入您的 Docker Hub 用户名。对密码重复相同的过程,将名称DOCKER_PASSWORD和值作为您用来注册帐户的 Docker Hub 密码。一旦两者都被添加,秘密部分应该看起来像图 8-1 。

img/463857_2_En_8_Fig1_HTML.png

图 8-1

在 GitHub 存储库设置中配置的秘密

将凭证添加到 GitHub 后,现在可以修改作业来注入这些秘密。这可以通过使用${{ secrets.SecretName }}格式引用秘密名称来完成。工作流文件的docker-build部分现在应该如清单 8-7 所示。

  docker-build:
    timeout-minutes: 10
    runs-on: ubuntu-latest
    needs: lint

    steps:
    - name: Checkout
      uses: actions/checkout@v1

    - name: Build Docker Image
      env:
        DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}

      run: |
        cd source-code/chapter-7/exercise-2/newsbot-compose
        docker login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD}
        docker build -t ${DOCKER_USERNAME}/newsbot:${GITHUB_SHA} .
        docker push ${DOCKER_USERNAME}/newsbot:${GITHUB_SHA}

Listing 8-7docker-build Job Updated with Added Docker Hub Credentials and Push to Docker Hub

现在,验证推送是否成功发生。如前所述,您可以使用gh run list命令找到最新的运行:

gh run list
STATUS  NAME              WORKFLOW             EVENT  ID
✓       <commit message> Lint and build Docker <run id>
X       <commit message> Lint and build Docker <run id>
✓       <commit message> Lint and build Docker <run id>

然后使用以下命令查找工作流的结果:

 gh run view <run id>

✓ add-lint-build-workflow Lint and build Docker · <run id>
Triggered via push about 59 minutes ago

JOBS
✓ lint in 14s (ID <job id>)
✓ docker-build in 3m57s (ID <job id>)

要查看docker-build作业的结果,请键入以下命令:

gh run view --job <job id>

✓ add-lint-build-workflow Lint and build Docker · 1198342464
Triggered via push about 1 hour ago

✓ docker-build in 3m57s (ID 3507041628)
  ✓ Set up job
  ✓ Checkout
  ✓ Build Docker Image
  ✓ Complete job

您可以从摘要中看到,所有步骤都已成功完成。要检查作业的日志,请发出如下所示的命令:

gh run view --log --job=<job id>

docker-build  Set up job Ubuntu
docker-build  Set up job 20.04.3
docker-build  Set up job LTS
[...]
docker-build  Build Docker Image Step 7/7 : CMD ["python", "newsbot.py"]
docker-build  Build Docker Image Successfully built b65633d72071
docker-build  Build Docker Image Successfully tagged ***/newsbot:48e085beba409747b3a87dcf918549017ae8c173
docker-build  Build Docker Image The push refers to repository [docker.io/***/newsbot]
[...]
docker-build  Build Docker Image 54d6343a1c01: Pushed

您已经成功地配置了持续集成,以便在每次推送时构建 Docker 映像。当您查看 GitHub 操作页面时,它应该看起来如图 8-2 所示。现在,您可以引用这个映像和标记来进行部署。

img/463857_2_En_8_Fig2_HTML.png

图 8-2

新闻机器人 lint 和 build 的 GitHub 操作

摘要

在这一章中,你学习了持续集成,以及如何在每次 Git 提交后使用持续集成自动构建 Docker 映像,使得测试容器和应用变得更加容易。您还了解了 container orchestrators,对 Kubernetes 进行了概述,并学习了如何使用kind在本地系统上部署 Kubernetes 集群,以简化 Docker 应用的测试,并为生产部署做好准备。最后,您尝试了一些使用kind为本地开发部署多节点 Kubernetes 集群的练习,并设置了一个持续集成管道,该管道验证、链接 Newsbot 源代码,然后使用 GitHub 操作在每次提交时自动构建并发布 Newsbot Docker 映像到 Docker Hub。有了这些,我希望你能应用你在书中学到的原则,并在你的应用中实现类似的步骤!

posted @ 2024-08-09 17:40  绝不原创的飞龙  阅读(31)  评论(0编辑  收藏  举报