Docker-实践指南-全-
Docker 实践指南(全)
原文:Docker in Practice
译者:飞龙
第一部分. Docker 基础知识
本书第一部分由第一章和第二章组成,这些章节将帮助您开始使用 Docker 并涵盖其基础知识。
第一章解释了 Docker 的起源及其核心概念,如镜像、容器和分层。最后,您将通过创建第一个 Dockerfile 来亲自动手。
第二章介绍了一些有用的技术,以帮助您更深入地理解 Docker 的架构。依次介绍每个主要组件,我们将涵盖 Docker 守护进程与其客户端、Docker 注册表和 Docker Hub 之间的关系。
到第一部分结束(部分 1)时,您将熟悉核心 Docker 概念,并能够展示一些有用的技术,为本书的其余部分打下坚实的基础。
第二章. 理解 Docker:引擎室内部
本章涵盖
-
Docker 的架构
-
跟踪您主机上 Docker 的内部结构
-
使用 Docker Hub 查找和下载镜像
-
设置您自己的 Docker 注册表
-
容器之间进行通信
掌握 Docker 的架构是全面理解 Docker 的关键。在本章中,您将了解您机器和网络上的 Docker 主要组件的概述,并学习一些将有助于发展这种理解的技术。
在此过程中,您将学习一些实用的技巧,这些技巧将帮助您更有效地使用 Docker(和 Linux)。本书中更晚和更高级的技术大多基于您在这里看到的内容,因此请特别注意以下内容。
2.1. Docker 的架构
图 2.1 展示了 Docker 的架构,这将是本章的核心。我们将从高层次的角度开始,然后专注于每个部分,使用旨在巩固您理解的技术。
图 2.1. Docker 架构概述

在撰写本文时,您的主机上的 Docker 分为两部分——一个带有 RESTful API 的守护进程和一个与守护进程通信的客户端。图 2.1 显示了运行 Docker 客户端和守护进程的主机。
提示
RESTful API 使用标准的 HTTP 请求类型,如GET、POST、DELETE等,来表示资源和它们上的操作。在这种情况下,镜像、容器、卷等是表示的资源。
您可以通过调用 Docker 客户端来获取守护进程的信息或向其下达指令;守护进程是一个服务器,它通过 HTTP 协议接收来自客户端的请求并返回响应。反过来,它将向其他服务发出请求以发送和接收镜像,同样使用 HTTP 协议。服务器将接受来自命令行客户端或任何被授权连接的人的请求。守护进程还负责在幕后处理您的镜像和容器,而客户端则作为您和 RESTful API 之间的中介。
私有 Docker 仓库是一个存储 Docker 镜像的服务。这些镜像可以从任何具有相关访问权限的 Docker 守护进程请求。此仓库位于内部网络中,不可公开访问,因此被认为是私有的。
您的主机通常会位于一个私有网络中。如果需要,Docker 守护进程会调用互联网以检索镜像。
Docker Hub 是由 Docker Inc. 运营的公共仓库。互联网上还可以存在其他公共仓库,您的 Docker 守护进程可以与它们交互。
在第一章中我们提到 Docker 容器可以被运送到任何可以运行 Docker 的地方——这并不完全正确。实际上,只有当 守护进程 可以安装时,容器才会在该机器上运行。
从 图 2.1 中可以得出的关键点是,当您在机器上运行 Docker 时,您可能正在与机器上的其他进程交互,甚至可能与网络或互联网上运行的服务交互。
现在您已经了解了 Docker 的布局,我们将介绍与图中不同部分相关的各种技术。
2.2. Docker 守护进程
Docker 守护进程(见 图 2.2)是您与 Docker 交互的中心,因此它是开始了解所有相关部分的最佳起点。它控制您机器上 Docker 的访问权限,管理容器和镜像的状态,并协调与外部世界的交互。
图 2.2. Docker 守护进程

提示
守护进程 是一个在后台运行而不是直接受用户控制的进程。服务器 是一个接收来自客户端的请求并执行满足这些请求所需操作的进程。守护进程通常也是服务器,它们接受来自客户端的请求以执行这些操作。docker 命令是一个客户端,而 Docker 守护进程则充当服务器,负责处理您的 Docker 容器和镜像。
让我们看看一些技术,这些技术展示了 Docker 如何作为一个守护进程有效运行,以及你使用docker命令与之交互时,仅限于执行简单请求以执行操作,这与与 Web 服务器的交互非常相似。第一种技术允许其他人连接到你的 Docker 守护进程并执行你可能在主机机器上执行的操作,第二种技术说明了 Docker 容器是由守护进程管理的,而不是由你的 shell 会话管理。
将你的 Docker 守护进程向世界开放
尽管默认情况下你的 Docker 守护进程只能在你的主机上访问,但允许其他人访问它可能有很好的理由。你可能有一个需要远程调试的问题,或者你可能希望允许你的 DevOps 工作流程的另一个部分在主机机器上启动一个进程。
警告
虽然这可能是一种强大且有用的技术,但它被认为是不安全的。任何有权访问的人(包括挂载了 Docker 套接字的容器)都可以利用 Docker 套接字获得 root 权限。
问题
你希望向其他人开放你的 Docker 服务器。
解决方案
以开放的 TCP 地址启动 Docker 守护进程。
图 2.3 概述了此技术的工作原理。
图 2.3. Docker 可访问性:正常和开放

在你开放 Docker 守护进程之前,你必须首先关闭正在运行的守护进程。你如何做到这一点将取决于你的操作系统(非 Linux 用户应参阅附录 A)。如果你不确定如何操作,请从以下命令开始:
$ sudo service docker stop
如果你收到如下消息,
The service command supports only basic LSB actions (start, stop, restart,
try-restart, reload, force-reload, status). For other actions, please try
to use systemctl.
则你有基于 systemctl 的启动系统。尝试以下命令:
$ systemctl stop docker
如果此操作成功,你不应看到此命令的任何输出:
$ ps -ef | grep -E 'docker(d| -d| daemon)\b' | grep -v grep
一旦 Docker 守护进程停止,你可以手动重新启动它,并使用以下命令将其向外部用户开放:
$ sudo docker daemon -H tcp://0.0.0.0:2375
此命令以守护进程(docker daemon)启动,使用-H标志定义主机服务器,使用 TCP 协议,打开所有 IP 接口(使用0.0.0.0),并打开标准的 Docker 服务器端口(2375)。
你可以使用以下命令从外部连接:
$ docker -H tcp://<your host's ip>:2375 <subcommand>
或者,你可以导出DOCKER_HOST环境变量(如果你必须使用sudo来运行 Docker,则此方法不可用——参见技术 41 以了解如何删除此要求):
$ export DOCKER_HOST=tcp://<your host's ip>:2375
$ docker <subcommand>
注意,你还需要在本地机器上执行以下操作之一,因为 Docker 不再监听默认位置。
如果你希望在你的主机上永久更改此设置,你需要配置你的启动系统。有关如何操作的更多信息,请参阅附录 B。
警告
如果你使用此技术使 Docker 守护进程监听端口,请注意,将 IP 指定为0.0.0.0将允许所有网络接口(包括公共和私有)的用户访问,这通常被认为是不安全的。
讨论
如果你有一个强大的机器,在安全的私有本地网络内专门用于 Docker,这是一个很好的技术,因为网络上的每个人都可以轻松地将 Docker 工具指向正确的位置——DOCKER_HOST是一个众所周知的环境变量,它将通知大多数访问 Docker 的程序在哪里查找。
作为停止 Docker 服务并手动运行它的相对繁琐过程的替代,你可以将挂载 Docker 套接字作为卷(从技术 45)与使用 socat 工具从外部端口转发流量结合起来——只需运行docker run -p 2375:2375 -v /var/run/docker.sock:/var/run/docker.sock sequenceid/socat。
你将在本章后面的技术 5 中看到这个技术允许的特定示例。
| |
以守护进程运行容器
随着你对 Docker(如果你和我们一样)越来越熟悉,你可能会开始考虑 Docker 的其他用例,其中第一个就是将 Docker 容器作为后台服务运行。
通过软件隔离以可预测的行为运行 Docker 容器是 Docker 的主要用例之一。这项技术将允许你以适合你操作的方式管理服务。
问题
你想在后台以服务形式运行一个 Docker 容器。
解决方案
使用docker run命令的-d标志,并使用相关的容器管理标志来定义服务特性。
Docker 容器——就像大多数进程一样——默认情况下将在前台运行。将 Docker 容器在后台运行的最明显方法就是使用标准的&控制操作符。虽然这可行,但如果你在终端会话中注销,你可能会遇到问题,需要使用nohup标志,它会在你的本地目录中创建一个包含输出的文件,你需要管理这个文件...你明白了:使用 Docker 守护进程的功能来做这件事要整洁得多。
要这样做,你使用-d标志。
$ docker run -d -i -p 1234:1234 --name daemon ubuntu:14.04 nc -l 1234
当与docker run一起使用时,-d标志将容器作为守护进程运行。-i标志使这个容器能够与你的 Telnet 会话交互。使用-p你可以将容器中的 1234 端口发布到主机。--name标志允许你给容器起一个名字,这样你以后就可以引用它了。最后,你使用 netcat(nc)在 1234 端口上运行一个简单的监听回声服务器。
你现在可以连接到它,并通过 Telnet 发送消息。你会看到容器通过使用docker logs命令接收到了消息,如下所示。
列表 2.1. 使用 Telnet 连接到容器 netcat 服务器
$ telnet localhost 1234 *1*
Trying ::1...
Connected to localhost.
Escape character is '^]'.
hello daemon *2*
^] *3*
telnet> q *4*
Connection closed.
$ docker logs daemon *5*
hello daemon
$ docker rm daemon *6*
daemon
$
-
1 使用 telnet 命令连接到容器的 netcat 服务器
-
2 输入要发送到 netcat 服务器的文本行
-
3 按下 Ctrl-]然后回车键退出 Telnet 会话
-
4 输入 q 然后回车键退出 Telnet 程序
-
5 运行 docker logs 命令以查看容器的输出
-
6 使用 rm 命令清理容器
你可以看到,以守护进程模式运行容器很简单,但在操作上还有一些问题需要回答:
-
如果服务失败会发生什么?
-
当服务终止时会发生什么?
-
如果服务反复失败会发生什么?
幸运的是,Docker 为这些问题中的每一个都提供了标志!
注意
虽然重启标志通常与守护进程标志(-d)一起使用,但使用-d运行这些标志不是必需的。
docker run--restart标志允许你在容器终止时应用一组规则(所谓的“重启策略”)(见表 2.1)。
表 2.1. Docker 重启标志选项
| 策略 | 描述 |
|---|---|
| no | 容器退出时不重启 |
| always | 容器退出时总是重启 |
| unless-stopped | 总是重启,但会记住显式停止 |
| on-failure[:max-retry] | 仅在失败时重启 |
no策略很简单:当容器退出时,不会重启。这是默认设置。
always策略也很简单,但值得简要讨论:
$ docker run -d --restart=always ubuntu echo done
此命令以守护进程模式(-d)运行容器,并在终止时总是重启容器(--restart=always)。它发出一个简单的echo命令,快速完成,然后退出容器。
如果你运行前面的命令,然后运行一个docker ps命令,你会看到类似以下的内容:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES
69828b118ec3 ubuntu:14.04 "echo done" 4 seconds ago
Restarting (0) Less than a second ago sick_brattain
docker ps命令列出所有正在运行的容器及其信息,包括以下内容:
-
容器创建的时间(
CREATED)。 -
容器的当前状态——通常这将会是
Restarting,因为它只会运行很短的时间(STATUS)。 -
容器上次运行的退出代码(也位于
STATUS下)。0表示运行成功。 -
容器名称。默认情况下,Docker 通过连接两个随机单词来命名容器。有时这会产生奇怪的结果(这就是我们通常建议给它们一个有意义的名称的原因)。
注意,STATUS列还告诉我们容器在不到一秒前退出并正在重启。这是因为echo done命令立即退出,而 Docker 必须不断地重启容器。
需要注意的是,Docker 会重用容器 ID。重启时不会改变,并且对于这个 Docker 调用,ps表中只会有一条记录。
指定unless-stopped几乎等同于always——两者都会在运行docker stop命令停止容器时停止重启,但unless-stopped会确保如果守护进程重启(例如,如果你重启了电脑),则记住这个停止状态,而always将再次将容器启动起来。
最后,on-failure 策略仅在容器从其主进程返回非零退出代码(通常表示失败)时重启:
$ docker run -d --restart=on-failure:10 ubuntu /bin/false
此命令以守护进程模式(-d)运行容器,并设置重启尝试次数的限制(--restart=on-failure:10),如果超过此限制则退出。它运行一个简单的命令(/bin/false),该命令快速完成并肯定会失败。
如果您运行前面的命令并等待一分钟,然后运行 docker ps -a,您将看到类似以下内容的输出:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES
b0f40c410fe3 ubuntu:14.04 "/bin/false" 2 minutes ago
Exited (1) 25 seconds ago loving_rosalind
讨论
创建在后台运行的服务往往伴随着确保在异常环境中后台服务不会崩溃的困难。因为它不是立即可见的,用户可能不会注意到某些东西没有正确工作。
这种技术让您不再需要考虑由环境和处理重启引起的服务的偶然复杂性。您可以专注于核心功能。
具体来说,您和您的团队可能会使用这项技术在同一台机器上运行多个数据库,并避免编写设置它们的说明或保持它们运行而需要打开终端。
| |
将 Docker 移动到不同的分区
Docker 在一个文件夹下存储与您的容器和镜像相关的所有数据。因为它可以存储大量不同的镜像,所以这个文件夹可能会迅速变大!
如果您的宿主机有不同分区(在企业 Linux 工作站中很常见),您可能会更快地遇到空间限制。在这些情况下,您可能希望将 Docker 运行的目录移动。
问题
您想移动 Docker 存储数据的位置。
解决方案
使用 -g 标志指定新位置来停止和启动 Docker 守护进程。
假设您想从 /home/dockeruser/mydocker 运行 Docker。首先,停止您的 Docker 守护进程(参见附录 B [kindle_split_035.xhtml#app02],讨论如何执行此操作)。然后运行此命令:
$ dockerd -g /home/dockeruser/mydocker
在此目录下将创建一组新的文件夹和文件。这些文件夹是 Docker 内部的,因此请谨慎操作(正如我们所发现的!)。
您应该知道,此命令看起来会从您之前的 Docker 守护进程中清除容器和镜像。不要绝望。如果您终止了您刚刚运行的 Docker 进程,并重新启动您的 Docker 服务,您的 Docker 客户端将重新指向其原始位置,您的容器和镜像将返回给您。如果您想使这次移动永久化,您需要相应地配置主机的启动过程。
讨论
除了这个明显的用例(在磁盘空间有限的情况下回收磁盘空间)之外,如果你想要严格划分图像和容器的集合,你也可以使用这个技术。例如,如果你可以访问多个由不同所有者拥有的私有 Docker 注册库,那么确保你不小心将私有数据给了错误的人可能值得额外的努力。
2.3. Docker 客户端
Docker 客户端(见图 2.4)是 Docker 架构中最简单的组件。这是你在机器上输入docker run或docker pull等命令时运行的。它的任务是通过对 Docker 守护进程的 HTTP 请求进行通信。
图 2.4. Docker 客户端

在本节中,你将了解如何窃听 Docker 客户端和服务器之间的消息。你还将看到一种使用浏览器作为 Docker 客户端的方法,以及一些与端口映射相关的基本技术,这些技术是向编排迈出的第一步,在本书的第四部分(kindle_split_022.xhtml#part04)中讨论。
使用 socat 监控 Docker API 流量
有时docker命令可能不会按预期工作。大多数情况下,命令行参数的某个方面没有被理解,但偶尔会有更严重的问题,例如 Docker 二进制文件过时。为了诊断问题,查看你与 Docker 守护进程通信的数据流可能很有用。
注意
别慌!这个技术的存在并不意味着 Docker 需要经常调试,或者它在任何方面都不稳定!这个技术主要作为一个理解 Docker 架构的工具,同时也向你介绍 socat,一个强大的工具。如果你像我们一样,在许多不同的位置使用 Docker,你使用的 Docker 版本可能会有所不同。和任何软件一样,不同版本将会有不同的功能和标志,这可能会让你感到困惑。
问题
你想调试一个与 Docker 命令相关的问题。
解决方案
使用流量嗅探器检查 API 调用并构建自己的。
在这个技术中,你将在你的请求和服务器套接字之间插入一个代理 Unix 域套接字,以查看通过它的内容(如图 2.5 所示)。请注意,你需要 root 或 sudo 权限才能使这个操作生效。
图 2.5. 主机上的 Docker 客户端/服务器架构

要创建这个代理,你将使用 socat。
小贴士
socat是一个强大的命令,它允许你在几乎任何类型的数据通道之间中继数据。如果你熟悉 netcat,你可以将其视为加强版的 netcat。要安装它,请使用你系统的标准包管理器。
$ socat -v UNIX-LISTEN:/tmp/dockerapi.sock,fork \
UNIX-CONNECT:/var/run/docker.sock &
在这个命令中,-v使输出可读,并指示数据流。UNIX-LISTEN部分告诉socat监听 Unix 套接字,fork确保socat在第一个请求后不会退出,而UNIX-CONNECT告诉socat连接到 Docker 的 Unix 套接字。&指定命令在后台运行。如果你通常使用 sudo 运行 Docker 客户端,你在这里也需要这样做。
你请求的守护进程将经过的新路由可以在图 2.6 中看到。每个方向的所有流量都将被socat看到并记录到你的终端,除了 Docker 客户端提供的任何输出。
图 2.6. 在 socat 作为代理插入后的 Docker 客户端和服务器

简单docker命令的输出现在看起来类似于这个:
$ docker -H unix:///tmp/dockerapi.sock ps -a *1*
> 2017/05/15 16:01:51.163427 length=83 from=0 to=82
GET /_ping HTTP/1.1\r
Host: docker\r
User-Agent: Docker-Client/17.04.0-ce (linux)\r
\r
< 2017/05/15 16:01:51.164132 length=215 from=0 to=214
HTTP/1.1 200 OK\r
Api-Version: 1.28\r
Docker-Experimental: false\r
Ostype: linux\r
Server: Docker/17.04.0-ce (linux)\r
Date: Mon, 15 May 2017 15:01:51 GMT\r
Content-Length: 2\r
Content-Type: text/plain; charset=utf-8\r
\r
OK> 2017/05/15 16:01:51.165175 length=105 from=83 to=187 *2*
GET /v1.28/containers/json?all=1 HTTP/1.1\r
Host: docker\r
User-Agent: Docker-Client/17.04.0-ce (linux)\r
\r
< 2017/05/15 16:01:51.165819 length=886 from=215 to=1100 *3*
HTTP/1.1 200 OK\r
Api-Version: 1.28\r
Content-Type: application/json\r
Docker-Experimental: false\r
Ostype: linux\r
Server: Docker/17.04.0-ce (linux)\r
Date: Mon, 15 May 2017 15:01:51 GMT\r
Content-Length: 680\r
\r
[{"Id":"1d0d5b5a7b506417949653a59deac030ccbcbb816842a63ba68401708d55383e",
"Names":["/example1"],"Image":"todoapp","ImageID":
"sha256:ccdda5b6b021f7d12bd2c16dbcd2f195ff20d10a660921db0ac5bff5ecd92bc2",
"Command":"npm start","Created":1494857777,"Ports":[],"Labels":{},
"State":"exited","Status":"Exited (0) 45 minutes ago","HostConfig":
{"NetworkMode":"default"},"NetworkSettings":{"Networks":{"bridge":
{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":
"6f327d67a38b57379afa7525ea63829797fd31a948b316fdf2ae0365faeed632",
"EndpointID":"","Gateway":"","IPAddress":"","IPPrefixLen":0,
"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,
"MacAddress":""}}},"Mounts":[]}] *4*
CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES *5*
1d0d5b5a7b50 todoapp "npm start" 45 minutes ago
Exited (0) 45 minutes ago example1
-
1 你发出的查看请求和响应的命令
-
2 HTTP 请求从这里开始,左侧带有右尖括号。
-
3 HTTP 响应从这里开始,左侧带有左尖括号。
-
4 Docker 服务器响应的 JSON 内容
-
5 用户通常看到的输出,由 Docker 客户端从前面的 JSON 中解释
前面输出的具体内容将随着 Docker API 的增长和发展而变化。当你运行前面的命令时,你会看到更晚的版本号和不同的 JSON 输出。你可以通过运行docker version命令来检查你的客户端和服务器 API 版本。
警告
如果你以前作为 root 运行了 socat,你需要使用 sudo 来运行docker -H命令。这是因为 dockerapi.sock 文件属于 root。
使用 socat 不仅是一种强大的调试 Docker 的方法,而且还可以调试你在工作中可能遇到的任何其他网络服务。
讨论
对于这项技术,可以想出许多其他用例:
-
Socat 就像瑞士军刀一样,可以处理相当多的不同协议。前面的例子显示了它正在监听 Unix 套接字,但你也可以使用
TCP-LISTEN:2375,fork而不是UNIX-LISTEN:...参数来让它监听外部端口。这相当于技术 1 的一个更简单版本。采用这种方法,无需重新启动 Docker 守护进程(这将杀死所有正在运行的容器)。你可以根据需要随时启动和停止 socat 监听器。 -
由于前面的点设置简单且临时,你可以将其与技术 47 结合使用,以远程连接同事的运行中的容器并帮助他们调试问题。你还可以使用不常用的
docker attach命令连接到他们使用docker run启动的相同终端,这样你可以直接协作。 -
如果你有一个共享的 Docker 服务器(可能使用技术 1 设置),你可以使用暴露外部的能力,并在 Docker 套接字和外部世界之间设置 socat 作为代理,使其充当原始审计日志,记录所有请求的来源和它们所做的事情。
在浏览器中使用 Docker
推广新技术可能会很困难,所以简单有效的演示是无价的。使演示亲身体验甚至更好,这就是为什么我们发现创建一个允许用户在浏览器中与容器交互的网页是一个很好的技术。它以易于访问的方式让新用户第一次尝试 Docker,允许他们创建容器,在浏览器中使用容器终端,连接到他人的终端,并共享控制。显著的“哇”效果也不无裨益!
问题
你希望展示 Docker 的强大功能,而不需要用户自己安装它或运行他们不理解命令。
解决方案
使用开放端口和启用跨源资源共享(CORS)的 Docker 守护进程启动,然后在你的选择中提供 docker-terminal 存储库。
REST API 最常见的使用是在服务器上公开它,并在网页上使用 JavaScript 调用它。因为 Docker 恰好通过 REST API 执行所有交互,所以你应该能够以相同的方式控制 Docker。尽管这最初可能看起来令人惊讶,但这种控制甚至可以扩展到通过浏览器中的终端与容器交互。
我们已经在技术 1 中讨论了如何在 2375 端口启动守护进程,所以这里不会对此进行详细说明。此外,CORS 的内容也太多,无法在这里展开。如果你不熟悉它,可以参考 Monsur Hossain 的《CORS in Action》(Manning, 2014)。简而言之,CORS 是一种机制,它小心地绕过了 Java-Script 通常的限制,只允许访问当前域名。在这种情况下,CORS 允许守护进程在不同于你提供 Docker 终端页面的端口上监听。要启用 CORS,你需要使用带有--api-enable-cors选项启动 Docker 守护进程,并选择使其监听端口的选项。
现在所有先决条件都已解决,让我们开始运行。首先,你需要获取代码:
git clone https://github.com/aidanhs/Docker-Terminal.git
cd Docker-Terminal
然后你需要提供文件:
python2 -m SimpleHTTPServer 8000
上述命令使用 Python 内置的模块从目录中提供静态文件。你可以自由使用你喜欢的任何等效方法。现在在你的浏览器中访问 http://localhost:8000 并启动一个容器。
图 2.7 展示了 Docker 终端是如何连接的。页面托管在你的本地计算机上,并连接到本地计算机上的 Docker 守护进程以执行任何操作。
图 2.7. Docker 终端的工作原理

如果你想将此链接提供给其他人,请注意以下事项:
-
另一个人不得使用任何类型的代理。这是我们遇到的最常见的错误来源——Docker 终端使用 WebSockets,而目前它们无法通过代理工作。
-
显然,提供
localhost的链接不会起作用——您需要提供外部 IP 地址。 -
Docker 终端需要知道在哪里找到 Docker API——它应该根据您在浏览器中访问的地址自动完成此操作,但这是需要注意的事项。
提示
如果您对 Docker 更熟悉,您可能会想知道为什么我们没有在这个技术中使用 Docker 镜像。原因是我们仍在介绍 Docker,并且不想为 Docker 新手增加复杂性。将这项技术 Docker 化留作读者的练习。
讨论
尽管我们最初使用这项技术作为 Docker 的一个令人兴奋的演示(在可丢弃的机器上轻松共享多个终端可能很难设置,即使使用终端复用器),但我们发现它在一些无关领域有一些有趣的应用。一个例子是使用它来监控命令行中的一些任务的小组训练员。您或他们无需安装任何东西;只需打开您的浏览器,您就可以连接到他们的终端,随时提供帮助!
在类似的情况下,这也有一些协作的优势。在过去,当我们想要与同事分享一个错误时,我们会在 Docker 容器中重现错误,以便我们可以一起追踪它。有了这项技术,就不需要事先进行可能的“但为什么我要用 Docker?”讨论。
| |
通过端口连接到容器
Docker 容器从一开始就被设计为运行服务。在大多数情况下,这些将是一定类型的 HTTP 服务。其中相当一部分将通过浏览器访问的 Web 服务。
这导致了一个问题。如果您在内部环境中在端口 80 上运行多个 Docker 容器,它们都无法在您的宿主机的端口 80 上访问。这项技术展示了您如何通过公开和映射容器中的端口来管理这种常见场景。
问题
您想在宿主机的端口上提供多个 Docker 容器服务。
解决方案
使用 Docker 的-p标志将容器的端口映射到您的宿主机器。
在这个例子中,我们将使用 tutum-wordpress 镜像。假设您想在宿主机器上运行两个这样的镜像来提供不同的博客。
由于许多人之前都想要这样做,有人已经准备了一个任何人都可以获取并启动的镜像。要从外部位置获取镜像,您可以使用docker pull命令。默认情况下,镜像将从 Docker Hub 下载:
$ docker pull tutum/wordpress
当您尝试运行图片而它们尚未存在于您的机器上时,图片也会自动检索。
要运行第一个博客,请使用以下命令:
$ docker run -d -p 10001:80 --name blog1 tutum/wordpress
此 docker run 命令以守护进程(-d)模式运行容器,并带有发布标志(-p)。它标识了要映射到容器端口(80)的宿主机端口(10001),并为容器指定一个名称以识别它(--name blog1 tutum/wordpress)。
您可以对第二个博客做同样的操作:
$ docker run -d -p 10002:80 --name blog2 tutum/wordpress
如果您现在运行此命令,
$ docker ps | grep blog
您将看到两个博客容器被列出,它们的端口映射看起来像这样:
$ docker ps | grep blog
9afb95ad3617 tutum/wordpress:latest "/run.sh" 9 seconds ago
Up 9 seconds 3306/tcp, 0.0.0.0:10001->80/tcp blog1
31ddc8a7a2fd tutum/wordpress:latest "/run.sh" 17 seconds ago
Up 16 seconds 3306/tcp, 0.0.0.0:10002->80/tcp blog2
现在,您可以通过导航到 http://localhost:10001 和 http://localhost:10002 来访问您的容器。
当您完成时(假设您不想保留它们——我们将在下一个技术中使用它们),运行此命令:
$ docker rm -f blog1 blog2
如果需要,您现在应该能够通过自己管理端口分配来在您的宿主机上运行多个相同的镜像和服务。
小贴士
当使用 -p 标志时,很容易忘记哪个端口是宿主机的,哪个端口是容器的。我们将其视为像从左到右阅读一句话。用户连接到宿主机(-p),然后该宿主机端口被传递到容器端口(host_port:container_port)。如果您熟悉 SSH 的端口转发命令,这也是相同的格式。
讨论
暴露端口是 Docker 许多用例中极其重要的部分,您将在本书的许多地方遇到它,尤其是在 第四部分 中,容器之间的通信是日常生活的一部分。
在 技术 80 中,我们将向您介绍虚拟网络,并解释它们在幕后做什么以及它们如何将宿主机端口导向正确的容器。
允许容器通信
最后一种技术展示了您如何通过暴露端口将容器打开到宿主机网络。您并不总是想将服务暴露给宿主机或外界,但您会想将容器连接到彼此。
此技术展示了您如何使用 Docker 的用户定义网络功能来实现这一点,确保外界无法访问您的内部服务。
问题
您希望允许容器之间进行内部通信。
解决方案
使用用户定义的网络来使容器能够相互通信。
用户定义的网络简单且灵活。我们使用之前的技术在容器中运行了几个 WordPress 博客,让我们看看我们如何从另一个容器(而不是从您已经看到的从外界)访问它们。
首先,您需要创建一个用户定义的网络:
$ docker network create my_network
0c3386c9db5bb1d457c8af79a62808f78b42b3a8178e75cc8a252fac6fdc09e4
此命令创建了一个新的虚拟网络,它存在于您的机器上,您可以使用它来管理容器通信。默认情况下,您连接到该网络的任何容器都可以通过其名称相互看到。
接下来,假设你仍然使用前一种技术运行着 blog1 和 blog2 容器,你可以即时将其中一个连接到你的新网络。
$ docker network connect my_network blog1
最后,你可以启动一个新的容器,明确指定网络,并查看你是否能从博客的着陆页检索到前五行 HTML。
$ docker run -it --network my_network ubuntu:16.04 bash
root@06d6282d32a5:/# apt update && apt install -y curl
[...]
root@06d6282d32a5:/# curl -sSL blog1 | head -n5
<!DOCTYPE html>
<html lang="en-US" xml:lang="en-US">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
root@06d6282d32a5:/# curl -sSL blog2
curl: (6) Could not resolve host: blog2
小贴士
给容器命名非常有用,可以分配易于记忆的主机名,稍后可以引用,但这不是严格必要的——如果只有出站连接,那么你可能不需要查找容器。如果你发现你确实想查找主机名但尚未分配名称,你可以求助于使用终端提示中列出的短镜像 ID(除非它已被主机名覆盖)或在 docker ps 输出中。
我们的新容器成功访问了我们连接到 my_network 的博客,显示了在浏览器中访问它时我们会看到的页面的一些 HTML。另一方面,我们的新容器看不到第二个博客。因为我们从未将其连接到 my_network,这很合理。
讨论
你可以使用这种技术在一个集群上设置任意数量的容器,只要容器有某种方式可以发现彼此的名称。在 技术 80 中,你将看到一种与 Docker 网络很好地集成的实现方法。同时,下一个技术将从更小的规模开始,展示能够明确连接单个容器及其提供的服务的一些好处。
一个值得注意的额外点是 blog1 容器的有趣最终状态。默认情况下,所有容器都连接到 Docker bridge 网络,所以当我们要求它加入 my_network 时,它除了已经连接的网络外,还加入了该网络。在 技术 80 中,我们将更详细地探讨这一点,看看如何使用 网络跨越 作为某些实际场景的模型。
为端口隔离链接容器
在前一种技术中,你看到了如何让容器与用户定义的网络通信。但还有一种更老的方法来声明容器通信——Docker 的链接标志。这不再是推荐的工作方式,但它已经存在于 Docker 中很长时间了,如果你在野外遇到它,了解这一点是值得的。
问题
你希望在不使用用户定义网络的情况下允许容器之间通信。
解决方案
使用 Docker 的链接功能,允许容器之间相互通信。
以 WordPress 示例为背景,我们将把 MySQL 数据库层从 WordPress 容器中分离出来,并将它们相互链接,而不需要端口配置或创建网络。图 2.8 展示了最终状态。
图 2.8. WordPress 配置与链接容器

注意
如果您已经可以暴露端口到主机并使用它,为什么还要使用链接呢?链接允许您封装并定义容器之间的关系,而无需将服务暴露给主机的网络(以及可能的外部世界)。例如,您可能出于安全原因想要这样做。
按以下顺序运行您的容器,在第一个和第二个命令之间暂停大约一分钟:
$ docker run --name wp-mysql \
-e MYSQL_ROOT_PASSWORD=yoursecretpassword -d mysql
$ docker run --name wordpress \
--link wp-mysql:mysql -p 10003:80 -d wordpress
首先,您需要给 MySQL 容器命名为 wp-mysql,这样您以后就可以引用它了。您还必须提供一个环境变量,以便 MySQL 容器可以初始化数据库(-e MYSQL_ROOT_PASSWORD=yoursecretpassword)。您将以守护进程模式运行这两个容器(-d),并使用 Docker Hub 的官方 MySQL 镜像引用。
在第二个命令中,您将 WordPress 镜像命名为 wordpress,以防您以后需要引用它。您还将 wp-mysql 容器链接到 WordPress 容器(--link wp-mysql:mysql)。WordPress 容器内部对 mysql 服务器的引用将被发送到名为 wp-mysql 的容器。您还使用了本地端口映射(-p 10003:80),如技术 6 所述,并添加了 Docker Hub 的官方 WordPress 镜像引用(wordpress)。请注意,链接不会等待链接容器中的服务启动;因此,需要在命令之间暂停。更精确的做法是在运行 WordPress 容器之前,在 docker logs wp-mysql 的输出中查找“mysqid: ready for connections”。
如果您现在导航到 http://localhost:10003,您将看到 WordPress 的介绍屏幕,并可以设置您的 WordPress 实例。
这个示例的核心是第二个命令中的 --link 标志。此标志设置容器的宿主文件,以便 WordPress 容器可以引用 MySQL 服务器,并且这将路由到名为 wp-mysql 的任何容器。这具有显著的好处,即可以无需对 WordPress 容器进行任何更改即可更换不同的 MySQL 容器,这使得不同服务的配置管理变得更加容易。
注意
容器必须按正确的顺序启动,以便可以在已存在的容器名称上进行映射。在撰写本文时,Docker 不支持动态解析链接。
为了以这种方式链接容器,它们的端口必须在构建镜像时指定为暴露。这是通过在镜像构建的 Dockerfile 中使用 EXPOSE 命令来实现的。Dockerfile 中 EXPOSE 指令中列出的端口也用于使用 -P 标志(“发布所有端口”而不是 -p,后者指定特定端口)到 docker run 命令。
通过以特定顺序启动不同的容器,您已经看到了 Docker 编排的一个简单示例。Docker 编排是任何协调 Docker 容器运行的过程。这是一个庞大且重要的主题,我们将在本书的第四部分(part 4)中深入探讨。
通过将工作负载分割成独立的容器,您已经朝着为您的应用程序创建微服务架构迈出一步。在这种情况下,您可以在不触及 WordPress 容器的情况下在 MySQL 容器上执行工作,反之亦然。这种对运行服务的精细控制是微服务架构的关键操作优势之一。
讨论
对一组容器进行这种精确控制并不常见,但它可以作为非常直接且易于推理的替换容器的简单方法。使用此技术示例,您可能想测试不同的 MySQL 版本——WordPress 镜像不需要了解这一点,因为它只是查找mysql链接。
2.4. Docker 注册服务器
一旦创建了您的镜像,您可能希望与其他用户共享它们。这就是 Docker 注册器概念出现的地方。
图 2.9 中的三个注册服务器在可访问性方面有所不同。一个是私有网络上的,一个是公开在公共网络上的,另一个是公开的,但仅对注册了 Docker 的用户可访问。它们都使用相同的 API 执行相同的功能,这就是 Docker 守护进程如何能够相互交换地与它们通信。
图 2.9. Docker 注册服务器

Docker 注册服务器允许多个用户通过 RESTful API 从中央存储库推送和拉取镜像。
注册服务器代码,就像 Docker 本身一样,是开源的。许多公司(如我们公司)设置私有注册服务器以存储和共享其专有镜像。在我们更详细地了解 Docker Inc. 的注册服务器之前,我们将讨论这一点。
设置本地 Docker 注册服务器
您已经看到 Docker Inc. 提供了一个服务,人们可以公开分享他们的镜像(如果您想私下进行,您可以选择付费)。但是,您可能有多个原因希望在无需通过枢纽的情况下共享镜像——一些企业喜欢尽可能多地保留内部资源,也许您的镜像很大,通过互联网传输会太慢,或者您可能希望在实验期间保持镜像的私密性,而不想承诺付费。无论原因如何,都有一个简单的解决方案。
问题
您希望有一种方式来本地托管您的镜像。
解决方案
在您的本地网络中设置一个注册服务器。只需在拥有大量磁盘空间的机器上执行以下命令:
$ docker run -d -p 5000:5000 -v $HOME/registry:/var/lib/registry registry:2
此命令使注册表在 Docker 主机的 5000 端口上可用(-p 5000:5000)。使用 -v 标志,它使你的主机上的注册表文件夹(/var/lib/registry)在容器中作为 $HOME/registry 可用。因此,注册表的文件将存储在主机上的 /var/lib/registry 文件夹中。
在所有你想访问此注册表的机器上,将以下内容添加到你的守护进程选项中(其中 HOSTNAME 是你的新注册表服务器的主机名或 IP 地址):--insecure-registry HOSTNAME(有关如何操作的详细信息,请参阅附录 B)。你现在可以发出以下命令:docker push HOSTNAME:5000/image:tag。
如你所见,本地注册表最基本的配置级别,所有数据都存储在 $HOME/registry 目录中,是简单的。如果你想扩展或使其更健壮,GitHub 上的存储库(github.com/docker/distribution/blob/v2.2.1/docs/storagedrivers.md)概述了一些选项,例如在 Amazon S3 中存储数据。
你可能想知道 --insecure-registry 选项。为了帮助用户保持安全,Docker 只允许你从具有签名 HTTPS 证书的注册表拉取。我们已经覆盖了这一点,因为我们相当确信我们可以信任我们的本地网络。不过,不用说,你在互联网上做这件事应该更加谨慎。
讨论
由于注册表设置起来非常简单,因此出现了许多可能性。如果你的公司有多个团队,你可能会建议每个人在备用机器上启动并维护一个注册表,以便在存储镜像和移动它们时具有一定的灵活性。
如果你有一个内部 IP 地址范围,这特别有效——--insecure-registry 命令将接受 CIDR 表示法,例如 10.1.0.0/16,用于指定允许不安全的 IP 地址范围。如果你不熟悉这个,我们强烈建议你联系你的网络管理员。
2.5. Docker Hub
Docker Hub(见图 2.10)是由 Docker Inc. 维护的注册表。它上面有数万个可供下载和运行的镜像。任何 Docker 用户都可以设置一个免费账户,并将公共 Docker 镜像存储在那里。除了用户提供的镜像外,还维护了官方镜像以供参考。
图 2.10. Docker Hub

你的镜像通过用户身份验证得到保护,还有一个类似 GitHub 的星级系统来衡量流行度。官方镜像可以是像 Ubuntu 或 CentOS 这样的 Linux 发行版,预安装的软件包如 Node.js,或者像 WordPress 这样的完整软件栈。
查找和运行 Docker 镜像
Docker 仓库支持类似于 GitHub 的社交编码文化。如果您想尝试新的软件应用程序,或者寻找一个特定目的的新应用程序,Docker 镜像可以是一个简单的方法来实验,而不会干扰您的宿主机,配置虚拟机,或者担心安装步骤。
问题
您想要找到一个作为 Docker 镜像的应用程序或工具并尝试使用它。
解决方案
使用 docker search 命令查找要拉取的镜像,然后运行它。
假设您对 Node.js 感兴趣。在以下示例中,我们使用 docker search 命令搜索了匹配“node”的镜像:
$ docker search node
NAME DESCRIPTION
STARS OFFICIAL AUTOMATED
node Node.js is a JavaScript-based platform for...
3935 [OK] *1*
nodered/node-red-docker Node-RED Docker images.
57 [OK] *2*
strongloop/node StrongLoop, Node.js, and tools.
38 [OK] *3*
kkarczmarczyk/node-yarn Node docker image with yarn package manage...
25 [OK] *4*
bitnami/node Bitnami Node.js Docker Image
19 [OK]
siomiz/node-opencv _/node + node-opencv
10 [OK]
dahlb/alpine-node small node for gitlab ci runner
8 [OK]
cusspvz/node Super small Node.js container (~15MB) ba...
7 [OK]
anigeo/node-forever Daily build node.js with forever
4 [OK]
seegno/node A node docker base image.
3 [OK]
starefossen/ruby-node Docker Image with Ruby and Node.js installed
3 [OK]
urbanmassage/node Some handy (read, better) docker node images
1 [OK]
xataz/node very light node image
1 [OK]
centralping/node Bare bones CentOS 7 NodeJS container.
1 [OK]
joxit/node Slim node docker with some utils for dev
1 [OK]
bigtruedata/node Docker image providing Node.js & NPM
1 [OK]
1science/node Node.js Docker images based on Alpine Linux
1 [OK]
domandtom/node Docker image for Node.js including Yarn an...
0 [OK]
makeomatic/node various alpine + node based containers
0 [OK]
c4tech/node NodeJS images, aimed at generated single-p...
0 [OK]
instructure/node Instructure node images
0 [OK]
octoblu/node Docker images for node
0 [OK]
edvisor/node Automated build of Node.js with commonly u...
0 [OK]
watsco/node node:7
0 [OK]
codexsystems/node Node.js for Development and Production
0 [OK]
-
1
docker search的输出按星级数量排序。 -
2 描述是上传者对镜像目的的解释。
-
3 官方镜像是指 Docker Hub 信任的镜像。
-
4 自动构建的镜像是指使用 Docker Hub 的自动构建功能构建的镜像。
一旦您选择了一个镜像,您可以通过在名称上执行 docker pull 命令来下载它:
$ docker pull node *1*
Using default tag: latest
latest: Pulling from library/node
5040bd298390: Already exists
fce5728aad85: Pull complete
76610ec20bf5: Pull complete
9c1bc3c30371: Pull complete
33d67d70af20: Pull complete
da053401c2b1: Pull complete
05b24114aa8d: Pull complete
Digest:
sha256:ea65cf88ed7d97f0b43bcc5deed67cfd13c70e20a66f8b2b4fd4b7955de92297
Status: Downloaded newer image for node:latest *2*
-
1 从 Docker Hub 拉取名为
node的镜像 -
2 如果 Docker 拉取了新的镜像(而不是确定您已经拥有的镜像没有更新的镜像),您将看到此消息。您的输出可能会有所不同。
然后,您可以使用 -t 和 -i 标志交互式地运行它。-t 标志为您创建一个 TTY 设备(一个终端),而 -i 标志指定此 Docker 会话是交互式的:
$ docker run -t -i node /bin/bash
root@c267ae999646:/# node
> process.version
'v7.6.0'
>
提示
您可以通过在先前的 docker run 调用中将 -t -i 替换为 -ti 或 -it 来节省按键。从现在起,您将在本书中看到这一点。
通常,镜像维护者会有关于如何运行镜像的具体建议。在 hub.docker.com 网站上搜索镜像将带您到镜像的页面。描述标签可能会提供更多信息。
警告
如果您下载了一个镜像并运行它,您正在运行可能无法完全验证的代码。尽管使用受信任的镜像相对安全,但在互联网上下载和运行软件时,没有任何东西可以保证 100%的安全性。
带着这些知识和经验,您现在可以充分利用 Docker Hub 上的丰富资源。实际上有成千上万的镜像可以尝试,有很多东西可以学习。享受吧!
讨论
Docker Hub 是一个极好的资源,但有时它可能很慢——值得停下来决定如何最好地构建您的 Docker 搜索命令以获得最佳结果。无需打开浏览器即可进行搜索的能力,让您能够快速了解生态系统中可能感兴趣的项目,因此您可以更好地针对可能满足您需求的镜像的文档。
当您正在重建镜像时,偶尔运行一次搜索以查看星星数量是否表明 Docker 社区已经开始围绕您当前使用的不同镜像聚集也是有益的。
摘要
-
您可以将 Docker 守护进程 API 对外开放,他们只需要一种方式来发起 HTTP 请求——一个网页浏览器就足够了。
-
容器不必接管您的终端。您可以在后台启动它们,稍后再回来处理。
-
您可以使容器通过用户定义的网络(推荐方法)或通过链接进行通信,以非常明确地控制容器间的通信。
-
由于 Docker 守护进程 API 通过 HTTP,如果您遇到问题,使用网络监控工具调试它相对容易。
-
一个特别有用的调试和跟踪网络调用的工具是
socat。 -
设置注册表不仅仅是 Docker Inc. 的领域;您可以在本地网络上免费设置自己的注册表以存储私有镜像。
-
Docker Hub 是一个寻找和下载现成镜像的好地方,尤其是那些由 Docker Inc. 正式提供的镜像。
第二部分. Docker 和开发
在 第一部分 中,你通过示例学习了 Docker 的核心概念和架构。第二部分 将带你从这个基础出发,展示 Docker 在开发中的应用。
第三章 涵盖了使用 Docker 作为轻量级虚拟机。这是一个有争议的领域。尽管虚拟机和 Docker 容器之间存在关键差异,但在许多情况下,使用 Docker 可以显著加快开发速度。这也是一个在进入更高级的 Docker 使用之前熟悉 Docker 的有效方法。
第四章,第五章,和 第六章 涵盖了超过 20 种技术,使构建、运行和管理 Docker 容器更加高效。除了构建和运行容器外,你还将学习如何使用卷持久化数据以及保持你的 Docker 主机有序。
第七章 涵盖了配置管理的重要领域。你将使用 Dockerfile 和传统的配置管理工具来控制你的 Docker 构建。我们还将涵盖创建和维护最小 Docker 镜像以减少镜像膨胀。在本部分结束时,你将拥有大量有用的技术,用于单次使用的 Docker,并准备好将 Docker 带入 DevOps 环境。
第三章. 使用 Docker 作为轻量级虚拟机
本章涵盖
-
将虚拟机转换为 Docker 镜像
-
管理你的容器服务的启动
-
在进行过程中保存你的工作
-
管理你机器上的 Docker 镜像
-
在 Docker Hub 上共享镜像
-
用 Docker 玩 2048 并获胜
自世纪初以来,虚拟机(VM)在软件开发和部署中变得无处不在。将机器抽象为软件使得在互联网时代软件和服务的移动和控制变得更加容易和便宜。
小贴士
虚拟机是一种模拟计算机的应用程序,通常用于运行操作系统和应用程序。它可以放置在任何(兼容的)可用的物理资源上。最终用户会像在物理机器上一样体验软件,但管理硬件的人员可以专注于更大规模的资源分配。
Docker 不是一个 VM 技术。它不模拟机器的硬件,也不包含操作系统。默认情况下,Docker 容器不受特定硬件限制。如果 Docker 虚拟化了任何东西,它虚拟化的是服务运行的环境,而不是机器。此外,Docker 无法轻松运行 Windows 软件(甚至是为其他 Unix 衍生操作系统编写的软件)。
然而,从某些角度来看,Docker 可以像虚拟机一样使用。对于互联网时代的开发人员和测试人员来说,没有 init 进程或直接硬件交互的事实通常并不重要。而且存在许多显著的共性,例如其与周围硬件的隔离以及其易于采用更细粒度的软件交付方法。
本章将带您了解您可能以前使用虚拟机时使用的 Docker 场景。使用 Docker 不会给您带来任何明显的功能优势,但 Docker 在环境移动和跟踪方面的速度和便利性可能会改变您的开发流程。
3.1. 从虚拟机到容器
在一个理想的世界里,从虚拟机迁移到容器将只是运行针对与虚拟机类似的 Docker 镜像的配置管理脚本这么简单。对于我们这些不处于这种幸福状态的人来说,本节将展示您如何将虚拟机转换为容器——或者容器。
将您的虚拟机转换为容器
Docker Hub 没有所有可能的基镜像,因此对于一些利基 Linux 发行版和用例,人们需要创建自己的。例如,如果您在虚拟机中有一个现有的应用程序状态,您可能希望将该状态放入 Docker 镜像中,以便您可以在此基础上进一步迭代,或者通过使用存在于那里的工具和相关技术来从 Docker 生态系统获益。
理想情况下,您可能希望从头开始构建您虚拟机的等效版本,使用标准的 Docker 技术,例如 Dockerfile 与标准配置管理工具相结合(参见第七章 chapter 7)。然而,现实情况是,许多虚拟机并没有得到仔细的配置管理。这可能是因为虚拟机随着人们的使用而有机地增长,而将其以更结构化的方式重新创建所需的投入并不值得。
问题
您有一个想要转换为 Docker 镜像的虚拟机。
解决方案
归档并复制虚拟机的文件系统,并将其打包成 Docker 镜像。
首先,我们将虚拟机分为两大类:
-
本地虚拟机——虚拟机磁盘镜像存在于您的计算机上,虚拟机执行也在您的计算机上。
-
远程虚拟机——虚拟机磁盘镜像存储和虚拟机执行发生在别处。
对于这两组虚拟机(以及您想要从其创建 Docker 镜像的任何其他内容)的原则是相同的——您得到一个文件系统的 TAR 包,并将 TAR 文件添加到scratch镜像的/目录下。
小贴士
ADD Dockerfile 命令(与它的兄弟命令COPY不同)在它们放置在像这样的镜像中时,会解包 TAR 文件(以及 gzip 文件和其他类似文件类型)。
小贴士
scratch镜像是一个零字节的伪镜像,您可以在其上构建。通常,它用于此类情况,您想要使用 Dockerfile 复制(或添加)一个完整的文件系统。
我们现在将探讨一个您有一个本地 VirtualBox 虚拟机的情况。
在你开始之前,你需要做以下几步:
-
安装
qemu-nbd工具(在 Ubuntu 上作为qemu-utils包的一部分提供)。 -
确定你的虚拟机磁盘镜像的路径。
-
关闭你的虚拟机。
如果你的虚拟机磁盘镜像格式为.vdi 或.vmdk,这个技术应该效果很好。其他格式可能会遇到不同的成功。以下代码演示了如何将你的虚拟机文件转换为虚拟磁盘,这允许你复制其中的所有文件。
列表 3.1. 提取虚拟机镜像的文件系统
$ VMDISK="$HOME/VirtualBox VMs/myvm/myvm.vdi" *1*
$ sudo modprobe nbd *2*
$ sudo qemu-nbd -c /dev/nbd0 -r $VMDISK3((CO1-3)) *3*
$ ls /dev/nbd0p* *4*
/dev/nbd0p1 /dev/nbd0p2
$ sudo mount /dev/nbd0p2 /mnt *5*
$ sudo tar cf img.tar -C /mnt . *6*
$ sudo umount /mnt && sudo qemu-nbd -d /dev/nbd0 *7*
-
1 设置一个指向你的虚拟机磁盘镜像的变量
-
2 初始化 qemu-nbd 所需的内核模块
-
3 将虚拟机磁盘连接到虚拟设备节点
-
4 列出可用于在此磁盘上挂载的分区编号
-
5 使用 qemu-nbd 将选定的分区挂载到/mnt
-
6 从/mnt 创建名为 img.tar 的 TAR 文件
-
7 卸载并清理 qemu-nbd 后的操作
注意
要选择要挂载的分区,运行sudo cfdisk /dev/nbd0以查看可用选项。注意,如果你在任何地方看到 LVM,你的磁盘有一个非平凡的分区方案——你需要做一些额外的研究来了解如何挂载 LVM 分区。
如果你的虚拟机是远程管理的,你有选择:要么关闭虚拟机并要求你的运维团队执行你想要的分区的转储,要么在虚拟机运行时创建虚拟机的 TAR 文件。
如果你得到了分区转储,你可以相当容易地挂载它,然后按照以下步骤将其转换为 TAR 文件:
列表 3.2. 提取分区
$ sudo mount -o loop partition.dump /mnt
$ sudo tar cf $(pwd)/img.tar -C /mnt .
$ sudo umount /mnt
或者,你也可以从运行中的系统创建一个 TAR 文件。在登录系统后,这个过程相当简单:
列表 3.3. 提取运行中虚拟机的文件系统
$ cd /
$ sudo tar cf /img.tar --exclude=/img.tar --one-file-system /
现在,你已经有了一个文件系统镜像的 TAR 文件,你可以使用scp将其传输到另一台机器。
警告
从运行中的系统创建 TAR 文件可能看起来是最简单的选项(无需关闭,安装软件或向其他团队提出请求),但它有一个严重的缺点——你可能会复制一个处于不一致状态的文件,并在尝试使用你的新 Docker 镜像时遇到奇怪的问题。如果你必须走这条路,首先尽可能停止尽可能多的应用程序和服务。
一旦你获得了你的文件系统的 TAR 文件,你可以将其添加到你的镜像中。这是这个过程最简单的一步,只需要一个两行的 Dockerfile。
列表 3.4. 将存档添加到 Docker 镜像
FROM scratch
ADD img.tar /
现在,你可以运行docker build.,你就有你的镜像了!
注意
Docker 提供了ADD命令的替代方案,即docker import命令,你可以使用cat img.tar | docker import - new_image _name。但是,在镜像上使用额外指令构建将需要你创建一个 Dockerfile,所以可能更简单的是走ADD路线,这样你可以轻松地查看你的镜像历史。
现在,你已经在 Docker 中有了镜像,你可以开始对其进行实验。在这种情况下,你可能从基于你的新镜像创建一个新的 Dockerfile 开始,以实验去除文件和包。
一旦你完成了这个,并且对结果满意,你就可以在运行的容器上使用 docker export 来导出一个新的、更精简的 TAR 文件,你可以将其用作创建新镜像的基础,并重复此过程,直到你得到一个满意的镜像。
图 3.1 中的流程图展示了这个过程。
图 3.1. 镜像剥离流程图

讨论
这种技术演示了一些在将虚拟机转换为 Docker 镜像之外的环境中也有用的基本原理和技术。
最广泛地说,它表明 Docker 镜像本质上是一组文件和一些元数据:scratch 镜像是可以在其上放置 TAR 文件的空文件系统。当我们查看 slim Docker 镜像时,我们将回到这个主题。
更具体地说,你已经看到了如何将 TAR 文件添加到 Docker 镜像中,以及如何使用 qemu-nbd 工具。
一旦你有了你的镜像,你可能需要知道如何像更传统的宿主机一样运行它。因为 Docker 容器通常只运行一个应用程序进程,这有些违背常理,下一项技术将涉及这一点。
类似主机的容器
我们现在将转向 Docker 社区内讨论的更具争议性的领域之一——运行类似主机的镜像,从启动时就有多个进程运行。
在 Docker 社区的一些部分,这被认为是不良的格式。容器不是虚拟机——它们之间存在显著差异——假装没有差异可能会导致混淆和后续问题。
无论好坏,这项技术将向你展示如何运行类似主机的镜像,并讨论一些与此相关的问题。
注意
运行类似主机的镜像可以是说服 Docker 拒绝者 Docker 有用的好方法。随着他们更多地使用它,他们将更好地理解这种范式,并且微服务方法对他们来说将更有意义。在我们引入 Docker 的公司,我们发现这种单体方法是将人们从在开发服务器和笔记本电脑上开发转移到更封闭和可管理的环境中的绝佳方式。从那里,将 Docker 引入测试、持续集成、托管和 DevOps 工作流程变得微不足道。
虚拟机与 Docker 容器之间的差异
这些是虚拟机和 Docker 容器之间的一些差异:
-
Docker 是以应用程序为导向的,而虚拟机是以操作系统为导向的。
-
Docker 容器与其他 Docker 容器共享操作系统。相比之下,每个虚拟机都有自己的操作系统,由虚拟机管理程序管理。
-
Docker 容器的设计是为了运行一个主要进程,而不是管理多个进程集。
问题
你希望你的容器拥有一个类似主机的正常环境,其中设置了多个进程和服务。
解决方案
使用设计用于运行多个进程的基础容器。
对于这个技术,你将使用一个设计用于模拟主机的镜像,并配备你需要的应用程序。基础镜像将是 phusion/baseimage Docker 镜像,这是一个设计用于运行多个进程的镜像。
首步是启动镜像,然后使用 docker exec 进入它。
列表 3.5. 运行 phusion 基础镜像
user@docker-host$ docker run -d phusion/baseimage *1*
3c3f8e3fb05d795edf9d791969b21f7f73e99eb1926a6e3d5ed9e1e52d0b446e *2*
user@docker-host$ docker exec -i -t 3c3f8e3fb05d795 /bin/bash *3*
root@3c3f8e3fb05d:/# *4*
-
1 在后台启动镜像
-
2 返回新容器的 ID
-
3 将容器 ID 传递给 docker exec 并分配一个交互式终端
-
4 启动的容器终端的提示
在此代码中,docker run 将在后台启动镜像,启动镜像的默认命令,并返回新创建的容器的 ID。
然后,你将这个容器 ID 传递给 docker exec,这是一个在已运行的容器内启动新进程的命令。-i 标志允许你与新的进程交互,而 -t 表示你想要设置一个 TTY,以便你可以在容器内启动一个终端(/bin/bash)。
如果你等待一分钟,然后查看进程表,你的输出将类似于以下内容。
列表 3.6. 在类似主机的容器中运行的进程
root@3c3f8e3fb05d:/# ps -ef *1*
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 13:33 ? 00:00:00 /usr/bin/python3 -u /sbin/my_init *2*
root 7 0 0 13:33 ? 00:00:00 /bin/bash *3*
root 111 1 0 13:33 ? 00:00:00 /usr/bin/runsvdir -P /etc/service *4*
root 112 111 0 13:33 ? 00:00:00 runsv cron *5*
root 113 111 0 13:33 ? 00:00:00 runsv sshd
root 114 111 0 13:33 ? 00:00:00 runsv syslog-ng
root 115 112 0 13:33 ? 00:00:00 /usr/sbin/cron -f
root 116 114 0 13:33 ? 00:00:00 syslog-ng -F -p /var/run/syslog-ng.pid
--no-caps
root 117 113 0 13:33 ? 00:00:00 /usr/sbin/sshd -D
root 125 7 0 13:38 ? 00:00:00 ps -ef *6*
-
1 运行 ps 命令以列出所有运行中的进程
-
2 一个简单的 init 进程,用于运行所有其他服务
-
3 由 docker exec 启动并作为你的 shell 的 bash 进程
-
4 runsvdir 运行在传入的 /etc/service 目录中定义的服务。
-
5 在这里使用 runsv 命令启动了三个标准服务(cron、sshd 和 syslog)
-
6 当前正在运行的 ps 命令
你可以看到,容器启动起来就像主机一样,初始化了诸如 cron 和 sshd 等服务,使其看起来类似于标准的 Linux 主机。
讨论
尽管这可能在为新手工程师进行 Docker 初始演示时很有用,或者在你特定的环境下确实有用,但值得注意的是,这是一个有些争议的想法。
容器使用的历史通常倾向于将它们用于将工作负载隔离到“每个容器一个服务”。类似主机的镜像方法的支持者认为,这并不违反该原则,因为容器仍然可以在其运行的系统中履行一个单一的离散功能。
最近,Kubernetes 的 pod 和 docker-compose 概念的日益流行使得类似主机的容器相对冗余——可以在更高层次上将独立的容器连接成一个单一实体,而不是使用传统的 init 服务来管理多个进程。
下一个技术将探讨如何将这样的单体应用程序拆分成微服务风格的容器。
| |
将系统分割成微服务容器
我们已经探讨了如何将容器用作单体实体(如经典服务器),并解释了这可以快速将系统架构迁移到 Docker 的一种很好的方式。然而,在 Docker 的世界里,通常认为将系统尽可能分割是最佳实践,直到每个容器运行一个服务,并且所有容器通过网络连接。
使用每个容器一个服务的主要原因是通过单一责任原则更容易地分离关注点。如果你有一个容器只做一项工作,那么在开发、测试和生产软件开发生命周期中,你更容易处理这个容器,同时减少对其与其他组件交互的担忧。这使得交付更加敏捷,软件项目更具可扩展性。然而,这也创造了管理开销,因此考虑它是否值得你的用例是很好的。
不论哪种方法更适合你,最佳实践方法的一个明显优势是,使用 Dockerfile 进行实验和重建要快得多,正如你将看到的。
问题
你希望将你的应用程序分解成独立且更易于管理的服务。
解决方案
为每个独立的服务进程创建一个容器。
如我们之前提到的,在 Docker 社区中关于“每个容器一个服务”规则应该多么严格地遵循有一些争议,其中一部分源于对定义的分歧——是单个进程,还是满足需求的一组进程的组合?这通常归结为一个声明,即如果有机会从头开始重新设计系统,大多数人会选择微服务。但有时实用性胜过理想主义——在评估我们的组织使用 Docker 时,我们发现自己不得不走单体路线,以便尽可能快且容易地让 Docker 工作。
让我们来看看使用 Docker 内部单体的一些具体缺点。首先,以下列表展示了如何构建一个包含数据库、应用程序和 Web 服务器的单体。
注意
这些示例仅用于说明目的,并相应地进行了简化。直接尝试运行它们不一定能成功。
列表 3.7. 设置一个简单的 PostgreSQL、NodeJS 和 Nginx 应用程序
FROM ubuntu:14.04
RUN apt-get update && apt-get install postgresql nodejs npm nginx
WORKDIR /opt
COPY . /opt/ # {*}
RUN service postgresql start && \
cat db/schema.sql | psql && \
service postgresql stop
RUN cd app && npm install
RUN cp conf/mysite /etc/nginx/sites-available/ && \
cd /etc/nginx/sites-enabled && \
ln -s ../sites-available/mysite
小贴士
每个 Dockerfile 命令在之前的基础上创建一个单独的新层,但使用&&在你的RUN语句中实际上确保了多个命令作为一个命令运行。这很有用,因为它可以使你的镜像更小。如果你以这种方式运行一个包更新命令,如apt-get update,并附带一个安装命令,你将确保每次安装包时,它们都将来自更新的包缓存。
前面的示例是一个概念上简单的 Dockerfile,它在容器内安装所需的一切,然后设置数据库、应用程序和 Web 服务器。不幸的是,如果你想要快速重建你的容器——任何对你仓库下任何文件的更改都将从 {*} 开始重建一切,因为缓存无法重用。如果你有一些缓慢的步骤(数据库创建或 npm install),你可能需要等待一段时间才能重建容器。
解决这个问题的方法是将 COPY . /opt/ 指令拆分为应用程序的各个部分(数据库、应用程序和 Web 设置)。
列表 3.8. 单体应用的 Dockerfile
FROM ubuntu:14.04
RUN apt-get update && apt-get install postgresql nodejs npm nginx
WORKDIR /opt
COPY db /opt/db -+
RUN service postgresql start && \ |- db setup
cat db/schema.sql | psql && \ |
service postgresql stop -+
COPY app /opt/app -+
RUN cd app && npm install |- app setup
RUN cd app && ./minify_static.sh -+
COPY conf /opt/conf -+
RUN cp conf/mysite /etc/nginx/sites-available/ && \ +
cd /etc/nginx/sites-enabled && \ |- web setup
ln -s ../sites-available/mysite -+
在前面的代码中,COPY 命令被拆分为两个单独的指令。这意味着数据库不会在每次代码更改时重建,因为可以重用之前在代码之前交付的未更改文件的缓存。不幸的是,因为缓存功能相当简单,每次对模式脚本进行更改时,容器仍然需要完全重建。唯一解决这个问题的方法是从顺序设置步骤中移开,并创建多个 Dockerfile,如 列表 3.9 到 3.11 所示。
列表 3.9. postgres 服务的 Dockerfile
FROM ubuntu:14.04
RUN apt-get update && apt-get install postgresql
WORKDIR /opt
COPY db /opt/db
RUN service postgresql start && \
cat db/schema.sql | psql && \
service postgresql stop
列表 3.10. nodejs 服务的 Dockerfile
FROM ubuntu:14.04
RUN apt-get update && apt-get install nodejs npm
WORKDIR /opt
COPY app /opt/app
RUN cd app && npm install
RUN cd app && ./minify_static.sh
列表 3.11. nginx 服务的 Dockerfile
FROM ubuntu:14.04
RUN apt-get update && apt-get install nginx
WORKDIR /opt
COPY conf /opt/conf
RUN cp conf/mysite /etc/nginx/sites-available/ && \
cd /etc/nginx/sites-enabled && \
ln -s ../sites-available/mysite
当 db、app 或 conf 中的任何一个文件夹发生变化时,只需要重建一个容器。当你有超过三个容器或者有耗时设置步骤时,这尤其有用。通过一些小心谨慎,你可以为每个步骤添加必要的最小文件,从而获得更有用的 Dockerfile 缓存。
在应用程序 Dockerfile (列表 3.10) 中,npm install 的操作由一个文件 package.json 定义,因此你可以修改你的 Dockerfile 以利用 Dockerfile 层缓存,并且只在必要时重建缓慢的 npm install 步骤,如下所示。
列表 3.12. 更快的 nginx 服务的 Dockerfile
FROM ubuntu:14.04
RUN apt-get update && apt-get install nodejs npm
WORKDIR /opt
COPY app/package.json /opt/app/package.json
RUN cd app && npm install
COPY app /opt/app
RUN cd app && ./minify_static.sh
现在,你有了三个独立的、分开的 Dockerfile,而之前只有一个。
讨论
很遗憾,没有免费的午餐——你用多个 Dockerfile 中的重复内容换取了单个简单的 Dockerfile。你可以通过添加另一个 Dockerfile 作为基础镜像来部分解决这个问题,但一些重复内容并不罕见。此外,现在启动你的镜像有一些复杂性——除了 EXPOSE 步骤使适当的端口可用于链接和更改 Postgres 配置外,你还需要确保每次容器启动时都链接容器。幸运的是,有一个名为 Docker Compose 的工具可以做到这一点,我们将在 技巧 76 中介绍。
到目前为止,在本节中,你已经将虚拟机转换成了 Docker 镜像,运行了一个类似主机的容器,并将单体拆分成了单独的 Docker 镜像。
如果在阅读这本书之后,您仍然想在容器内运行多个进程,有一些特定的工具可以帮助您做到这一点。其中之一——Supervisord——将在下一个技术中介绍。
| |
管理容器服务的启动
如 Docker 文献中明确指出的,Docker 容器 不是 虚拟机。Docker 容器和虚拟机之间的一个主要区别是,容器设计用来运行一个进程。当该进程完成后,容器就会退出。这与 Linux 虚拟机(或任何 Linux OS)不同,因为它没有 init 进程。
init 进程在 Linux OS 上运行,进程 ID 为 1,父进程 ID 为 0。这个 init 进程可能被称为“init”或“systemd”。无论它叫什么,它的任务是管理该操作系统上运行的所有其他进程的维护工作。
如果您开始尝试使用 Docker,可能会发现您想要启动多个进程。例如,您可能想要运行 cron 作业来整理您的本地应用程序日志文件,或者在内容器中设置一个内部 memcached 服务器。如果您选择这条路径,您可能最终会编写 shell 脚本来管理这些子进程的启动。实际上,您将模拟 init 进程的工作。不要这样做!其他人之前已经遇到过许多由进程管理引起的问题,并且已经在预包装系统中得到了解决。
无论您在容器内运行多个进程的原因是什么,避免重新发明轮子是很重要的。
问题
您想要在容器内管理多个进程。
解决方案
使用 Supervisor 来管理容器中的进程。
我们将向您展示如何配置一个同时运行 Tomcat 和 Apache 网络服务器的容器,并以受管理的方式启动和运行,由 Supervisor 应用程序 (supervisord.org/) 管理进程启动。
首先,在新的空目录中创建您的 Dockerfile,如下所示。
列表 3.13. 示例 Supervisor Dockerfile
FROM ubuntu:14.04 *1*
ENV DEBIAN_FRONTEND noninteractive *2*
RUN apt-get update && apt-get install -y python-pip apache2 tomcat7 *2*
RUN pip install supervisor *3*
RUN mkdir -p /var/lock/apache2 *4*
RUN mkdir -p /var/run/apache2 *4*
RUN mkdir -p /var/log/tomcat *4*
RUN echo_supervisord_conf > /etc/supervisord.conf *5*
ADD ./supervisord_add.conf /tmp/supervisord_add.conf *6*
RUN cat /tmp/supervisord_add.conf >> /etc/supervisord.conf *7*
RUN rm /tmp/supervisord_add.conf *8*
CMD ["supervisord","-c","/etc/supervisord.conf"] *9*
-
1 从 ubuntu:14.04 开始
-
2 设置一个环境变量以指示此会话是非交互式的
-
3 安装 python-pip(用于安装 Supervisor)、apache2 和 tomcat7
-
4 使用 pip 安装 Supervisor
-
5 创建运行应用程序所需的维护目录
-
6 使用 echo_supervisord_conf 工具创建默认的 supervisord 配置文件
-
7 将 Apache 和 Tomcat 的 supervisord 配置设置追加到 supervisord 配置文件
-
8 删除您上传的文件,因为它不再需要
-
9 现在您只需要在容器启动时运行 Supervisor
您还需要为 Supervisor 配置,以指定它需要启动哪些应用程序,如下一个列表所示。
列表 3.14. supervisord_add.conf
[supervisord] *1*
nodaemon=true *2*
# apache
[program:apache2] *3*
command=/bin/bash -c "source /etc/apache2/envvars && exec /usr/sbin/apache2
-DFOREGROUND" *4*
# tomcat
[program:tomcat] *5*
command=service start tomcat *6*
redirect_stderr=true *7*
stdout_logfile=/var/log/tomcat/supervisor.log *7*
stderr_logfile=/var/log/tomcat/supervisor.error_log
-
1 声明 supervisord 的全局配置部分
-
2 不会将 Supervisor 进程作为守护进程运行,因为它是容器的前台进程
-
3 新程序的节声明
-
4 启动该节中声明的程序的命令
-
5 新程序的节声明
-
6 启动该节中声明的程序的命令
-
7 与日志记录相关的配置
你使用 Dockerfile 构建镜像时使用标准的单命令 Docker 过程,因为你正在使用 Dockerfile。运行此命令以执行构建:
docker build -t supervised .
你现在可以运行你的镜像了!
列表 3.15. 运行受监督的容器
$ docker run -p 9000:80 --name supervised supervised *1*
2015-02-06 10:42:20,336 CRIT Supervisor running as root (no user in config
file) *2*
2015-02-06 10:42:20,344 INFO RPC interface 'supervisor' initialized
2015-02-06 10:42:20,344 CRIT Server 'unix_http_server' running without any
HTTP authentication checking
2015-02-06 10:42:20,344 INFO supervisord started with pid 1 *2*
2015-02-06 10:42:21,346 INFO spawned: 'tomcat' with pid 12 *3*
2015-02-06 10:42:21,348 INFO spawned: 'apache2' with pid 13 *3*
2015-02-06 10:42:21,368 INFO reaped unknown pid 29
2015-02-06 10:42:21,403 INFO reaped unknown pid 30
2015-02-06 10:42:22,404 INFO success: tomcat entered RUNNING state, process *4*
has stayed up for > than 1 seconds (startsecs) *4*
2015-02-06 10:42:22,404 INFO success: apache2 entered RUNNING state, process
has stayed up for > than 1 seconds (startsecs) *4*
-
1 将容器的端口 80 映射到主机的端口 9000,给容器命名,并指定你正在运行的镜像名称,该名称已通过之前的构建命令标记
-
2 启动 Supervisor 进程
-
3 启动管理进程
-
4 Supervisor 已认为管理进程已成功启动。
如果你导航到 http://localhost:9000,你应该能看到你启动的 Apache 服务器的默认页面。
要清理容器,请运行以下命令:
docker rm -f supervised
讨论
这种技术使用 Supervisor 来管理你的 Docker 容器中的多个进程。
如果你感兴趣的是 Supervisor 的替代方案,还有runit,它在技术 12 中提到的 phusion 基础镜像中使用过。
3.2. 保存和恢复你的工作
有些人说,代码只有在提交到源代码控制中才算完成——对于容器来说,持有同样的态度并不总是有害的。使用快照可以通过 VM 保存状态,但 Docker 采取了更加积极的措施来鼓励保存和重用你的现有工作。
我们将介绍“保存游戏”的开发方法、标记的细微之处、使用 Docker Hub 以及在你的构建中引用特定镜像。由于这些操作被认为是如此基本,Docker 使它们相对简单快捷。尽管如此,这仍然可能是 Docker 新手感到困惑的话题,所以在下一段中,我们将带你了解这一主题的完整理解步骤。
“保存游戏”方法:低成本源代码控制
如果你曾经开发过任何类型的软件,你很可能至少一次惊叹过,“我确信它之前是工作的!”也许你的语言没有这么冷静。当你匆忙地编写代码以赶上截止日期或修复错误时,无法将系统恢复到已知的好(或可能只是“更好”)状态,这是许多损坏键盘的原因。
源代码控制在这方面有很大帮助,但在这个特定情况下存在两个问题:
-
源代码可能不会反映你的“工作”环境文件系统的状态。
-
你可能还不愿意提交代码。
第一个问题比第二个问题更重要。尽管像 Git 这样的现代源代码控制工具可以轻松创建本地临时分支,但捕获整个开发文件系统的状态并不是源代码控制的目的。
Docker 通过其提交功能提供了一种便宜且快速的方法来存储容器开发文件系统的状态,这正是我们将要探讨的。
问题
您想保存您开发环境的状态。
解决方案
定期提交您的容器,以便您可以在该点恢复状态。
让我们假设您想从 第一章 中更改您的待办事项应用程序。ToDoCorp 的 CEO 不高兴,希望浏览器标题显示“ToDoCorp 的待办应用”而不是“Swarm+React - TodoMVC”。
您不确定如何实现这一点,所以您可能想启动您的应用程序并通过更改文件来实验,看看会发生什么。
列表 3.16. 在终端中调试应用程序
$ docker run -d -p 8000:8000 --name todobug1 dockerinpractice/todoapp
3c3d5d3ffd70d17e7e47e90801af7d12d6fc0b8b14a8b33131fc708423ee4372
$ docker exec -i -t todobug1 /bin/bash 2((CO7-2))
docker run 命令在后台(-d)启动待办事项应用程序,将容器的端口 8000 映射到主机上的端口 8000(-p 8000:8000),命名为 todobug1(--name todobug1)以便于引用,并返回容器 ID。在容器中启动的命令将是构建 dockerinpractice/todoapp 镜像时指定的默认命令。我们已经为您构建了它,并在 Docker Hub 上提供。
第二个命令将在运行中的容器中启动 /bin/bash。使用 todobug1 作为名称,但您也可以使用容器 ID。-i 使此 exec 运行交互式,-t 确保该 exec 将像终端一样工作。
现在您已经在容器中,所以实验的第一步是安装一个编辑器。我们更喜欢 vim,所以我们使用了以下命令:
apt-get update
apt-get install vim
经过一点努力,您会意识到需要更改的文件是 local.html。因此,您将此文件的第 5 行更改为以下内容:
<title>ToDoCorp's ToDo App</title>
然后,传来消息说 CEO 可能希望标题为小写,因为她听说这样看起来更现代。您想两种情况都做好准备,所以您提交了当前的状态。在另一个终端中,您运行以下命令。
列表 3.17. 提交容器状态
$ docker commit todobug1 *1*
ca76b45144f2cb31fda6a31e55f784c93df8c9d4c96bbeacd73cad9cd55d2970 *2*
-
1 将您之前创建的容器转换为镜像
-
2 您已提交的容器的新的镜像 ID
您现在已将容器提交为镜像,您可以在以后运行它。
注意
提交容器只存储提交时文件系统的状态,而不是进程。记住,Docker 容器不是虚拟机!如果您的环境状态依赖于无法通过标准文件恢复的运行进程的状态,这种技术不会以您需要的方式存储状态。在这种情况下,您可能希望考虑使您的开发流程可恢复。
接下来,您将本地.html 文件更改为其他可能的必需值:
<title>todocorp's todo app</title>
再次提交:
$ docker commit todobug1
071f6a36c23a19801285b82eafc99333c76f63ea0aa0b44902c6bae482a6e036
现在,你有两个图像 ID 代表两种选项(在我们的例子中是 ca76b45144f2 cb31f da6a31e55f784c93df8c9d4c96bbeacd73cad9cd55d2970 和 071f6a36c23a19801285 b82eafc99333c76f63ea0aa0b44902c6bae482a6e036,但你的将是不同的)。当 CEO 来评估她想要哪一个时,你可以运行任何一个镜像并让她决定要提交哪一个。
你可以通过打开新的终端并运行以下命令来完成这项操作。
列表 3.18. 同时运行两个提交的镜像作为容器
$ docker run -p 8001:8000 \
ca76b45144f2cb31fda6a31e55f784c93df8c9d4c96bbeacd73cad9cd55d2970 *1*
$ docker run -p 8002:8000 \
071f6a36c23a19801285b82eafc99333c76f63ea0aa0b44902c6bae482a6e036 *2*
-
1 将容器的端口 8000 映射到主机的端口 8001 并指定小写图像 ID
-
2 将容器的端口 8000 映射到主机的端口 8002 并指定大写图像 ID
以这种方式,你可以在 http://localhost:8001 上提供大写选项,在 http://localhost:8002 上提供小写选项。
注意
容器外部(如数据库、Docker 卷或其他服务)的任何依赖项在提交时都不会存储。这种技术没有外部依赖项,所以你不需要担心这一点。
讨论
这种技术展示了 docker commit 的功能,以及它如何在开发工作流程中使用。Docker 用户往往被引导仅将 docker commit 作为正式的 commit-tag-push 工作流程的一部分来使用,因此记住它还有其他用途也是好的。
我们发现,当我们协商了一系列复杂的命令来设置应用程序时,这是一个有用的技术。一旦容器提交成功,也会记录你的 bash 会话历史,这意味着有一组步骤可以恢复你的系统状态。这可以节省 很多 时间!当你正在尝试一个新功能且不确定是否完成,或者当你重新创建了一个错误并希望尽可能确保可以返回到损坏状态时,这也很有用。
你可能会想知道是否有比使用长随机字符字符串更好的方法来引用镜像。接下来的技术将探讨给这些容器命名,以便更容易引用。
Docker 标记
通过提交,你现在已经保存了容器的状态,并且你的镜像 ID 是一个随机字符串。显然,很难记住和管理这些图像 ID 的大量数量。使用 Docker 的标记功能给你的图像提供可读的名称(和标签)并将它们创建的原因提醒自己会很有帮助。
精通这项技术将允许你一眼看出你的镜像用途,使你在机器上的镜像管理变得简单得多。
问题
你想要方便地引用和存储 Docker 提交。
解决方案
使用 docker tag 命令来命名你的提交。
在其基本形式中,对 Docker 镜像进行标记是简单的。
列表 3.19. 简单的 docker tag 命令
$ docker tag \ *1*
071f6a36c23a19801285b82eafc99333c76f63ea0aa0b44902c6bae482a6e036 \ *2*
imagename *3*
-
1 docker tag 命令
-
2 你想命名的图像 ID
-
3 你想要给你的镜像取的名字
这给你的镜像赋予了一个你可以引用的名称,如下所示:
docker run imagename
这比记住随机的字母和数字字符串要容易得多!
如果你想要与他人分享你的镜像,标签的使用远不止这些。不幸的是,关于标签的术语可能相当令人困惑。诸如 镜像名称 和 存储库 这样的术语可以互换使用。表 3.1 提供了一些定义。
表 3.1. Docker 标签术语
| 术语 | 含义 |
|---|---|
| 镜像 | 一个只读层。 |
| 名称 | 你的镜像名称,例如 “todoapp。” |
| 标签 | 作为动词,它指的是给镜像命名。作为名词,它是镜像名称的修饰符。 |
| 存储库 | 一个托管标记的镜像集合,这些镜像共同构成了容器的文件系统。 |
在这个表格中最令人困惑的术语可能是“镜像”和“存储库”。我们一直在松散地使用术语 镜像 来指代我们从中启动容器的层集合,但从技术上讲,一个镜像是一个单层,它递归地引用其父层。一个 存储库 是托管的意思,这意味着它存储在某处(要么是在你的 Docker 守护进程中,要么是在一个注册表中)。此外,一个存储库是由标记的镜像组成的集合,这些镜像构成了容器的文件系统。
与 Git 的类比在这里可能会有所帮助。当克隆 Git 存储库时,你检查出你请求的文件的状态。这与镜像类似。存储库是每个提交的文件的历史记录,追溯到初始提交。因此,你在“头部”的“层”处检查出存储库。其他“层”(或提交)都在你克隆的存储库中。
实际上,“镜像”和“存储库”这两个术语的使用几乎是互换的,所以不必过于担心这一点。但请注意,这些术语确实存在,并且被类似地使用。
你到目前为止所看到的是如何给镜像 ID 赋予一个名称。令人困惑的是,这个名称并不是镜像的“标签”,尽管人们经常这样称呼它。我们区分了“标记”(动词)和可以赋予镜像名称的“标签”(名词)。这个标签(名词)允许你命名镜像的特定版本。你可能添加一个标签来管理对同一镜像不同版本的引用。例如,你可以用版本名称或提交日期来标记一个镜像。
一个带有多个标签的存储库的好例子是 Ubuntu 镜像。如果你拉取 Ubuntu 镜像然后运行 docker images,你会得到类似于以下列表的输出。
列表 3.20. 带有多个标签的镜像
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu trusty 8eaa4ff06b53 4 weeks ago 192.7 MB
ubuntu 14.04 8eaa4ff06b53 4 weeks ago 192.7 MB
ubuntu 14.04.1 8eaa4ff06b53 4 weeks ago 192.7 MB
ubuntu latest 8eaa4ff06b53 4 weeks ago 192.7 MB
存储库列列出了称为“ubuntu”的托管层集合。通常这被称为“镜像”。标签列这里列出了四个不同的名称(trusty、14.04、14.04.1 和 latest)。镜像 ID 列列出了相同的镜像 ID。这是因为这些不同标记的镜像实际上是相同的。
这表明你可以从相同的镜像 ID 拥有多个标签的仓库。从理论上讲,尽管这些标签以后可能指向不同的镜像 ID。例如,如果“trusty”获得安全更新,例如,维护者可能会通过新的提交更改图像 ID,并用“trusty”、“14.04.2”和“latest”进行标记。
默认情况下,如果没有指定标签,则给你的镜像一个“latest”标签。
注意
在 Docker 中,“latest”标签没有特殊意义——它是标记和拉取的默认值。这并不一定意味着这是为该镜像设置的最后一个标签。你的镜像的“latest”标签可能是一个旧版本的镜像,因为后来构建的版本可能被标记为特定的标签,如“v1.2.3”。
讨论
在本节中,我们介绍了 Docker 镜像标记。就其本身而言,这项技术相对简单。我们发现的真正挑战——并且在这里集中关注的是——在 Docker 用户中术语的松散使用。值得再次强调的是,当人们谈论一个镜像时,他们可能指的是一个标记过的镜像,甚至是一个仓库。另一个特别常见的错误是将镜像称为容器:“只需下载容器并运行它。”使用 Docker 一段时间的工作同事仍然经常向我们提问,“容器和镜像之间的区别是什么?”
在下一个技术中,你将学习如何使用 Docker 镜像库与他人共享你现在标记的图像。
| |
在 Docker Hub 上共享图像
如果你能与他人共享这些名称(和图像)以及描述性的图像名称,那么标记图像将更有帮助。为了满足这一需求,Docker 提供了轻松将图像移动到其他地方的能力,并且 Docker Inc.创建了 Docker Hub 作为一项免费服务,以鼓励这种共享。
注意
要遵循这项技术,你需要一个 Docker Hub 账户,你之前已经通过在主机机器上运行docker login登录过。如果你还没有设置一个,你可以在hub.docker.com上设置一个。只需按照说明进行注册。
问题
你希望公开分享一个 Docker 镜像。
解决方案
使用 Docker Hub 注册表来共享你的图像。
与标记一样,围绕注册表的术语可能会令人困惑。表 3.2 应有助于你了解术语的使用方式。
表 3.2. Docker 注册表术语
| 术语 | 含义 |
|---|---|
| 用户名 | 你的 Docker 注册表用户名。 |
| 注册表 | 注册表存储图像。注册表是一个你可以上传图像或从中下载图像的存储库。注册表可以是公共的或私有的。 |
| 注册表主机 | Docker 注册表运行的主机。 |
| Docker Hub | 默认的公共注册表,托管在hub.docker.com。 |
| 索引 | 与注册表主机相同。这似乎是一个已弃用的术语。 |
正如您之前看到的,您可以多次标记一个镜像。这对于“复制”一个镜像以便您控制它非常有用。
假设您在 Docker Hub 上的用户名是“adev”。以下三个命令展示了如何将“debian:wheezy”镜像从 Docker Hub 复制到您自己的账户下。
列表 3.21. 复制公共镜像并将其推送到 adev 的 Docker Hub 账户
docker pull debian:wheezy *1*
docker tag debian:wheezy adev/debian:mywheezy1 *2*
docker push adev/debian:mywheezy1 *3*
-
1 从 Docker Hub 拉取 Debian 镜像
-
2 使用您的用户名(adev)和标签(mywheezy1)标记 wheezy 镜像
-
3 推送新创建的标签
现在,您已经有一个可以维护、参考和构建的 Debian wheezy 镜像的引用。
如果您有一个私有仓库要推送,过程是相同的,但您必须在标签之前指定仓库的地址。比如说,您有一个从 mycorp.private.dockerregistry 提供服务的仓库。以下列表将标记并推送镜像。
列表 3.22. 复制公共镜像并将其推送到开发者的私有仓库
docker pull debian *1*
docker tag debian:wheezy \
mycorp.private.dockerregistry/adev/debian:mywheezy1 *2*
docker push mycorp.private.dockerregistry/adev/debian:mywheezy1 *3*
-
1 从 Docker Hub 拉取 Debian 镜像
-
2 使用您的仓库(mycorp.private.dockerregistry)、用户名(adev)和标签(mywheezy1)标记 wheezy 镜像
-
3 将新创建的标签推送到私有仓库。请注意,在打标签和推送时都需要私有仓库服务器的地址,这样 Docker 才能确保它推送到正确的位置。
前面的命令不会将镜像推送到公共 Docker Hub,而是将其推送到私有仓库,这样任何有权访问该服务资源的人都可以拉取它。
讨论
现在,您有权限与他人共享您的镜像。这是一种很好的方式来分享工作、想法,甚至您面临的问题与其他工程师。
正如 GitHub 不是唯一的公开 Git 服务器一样,Docker Hub 也不是唯一的公开 Docker 仓库。但就像 GitHub 一样,它是最受欢迎的。例如,RedHat 在 access.redhat.com/containers 有一个中心。
再次强调,就像 Git 服务器一样,公共和私有 Docker 仓库可能具有不同的功能和特性,使得其中一个或另一个对您更有吸引力。如果您正在评估它们,您可能需要考虑诸如成本(购买、订阅或维护)、遵守 API、安全功能和性能等因素。
在下一个技术中,我们将探讨如何引用特定的镜像以帮助避免当您使用的镜像引用不具体时出现的问题。
| |
在构建中引用特定镜像
大多数时候,您会在构建中引用通用镜像名称,如“node”或“ubuntu”,并且不会有问题。
如果你引用了一个镜像名称,那么在标签保持不变的情况下,镜像本身可能会发生变化,这听起来可能有些矛盾。仓库名称只是一个参考,它可能被修改以指向不同的底层镜像。使用冒号表示法(例如 ubuntu:trusty)指定标签也不能消除这种风险,因为安全更新可以使用相同的标签自动重建易受攻击的镜像。
大多数情况下,你会希望这样——镜像的维护者可能已经找到了改进,修补安全漏洞通常是一件好事。然而,偶尔这也可能给你带来痛苦。这不仅仅是一个理论风险:这种情况在我们身上已经发生多次,导致持续交付构建难以调试。在 Docker 的早期阶段,最流行的镜像会定期添加和删除包(包括一次难忘的场合,passwd 命令消失了!),使得之前工作的构建突然中断。
问题
你想要确保你的构建来自一个特定且不变的镜像。
解决方案
为了确保你正在构建针对一组特定的文件,请在 Dockerfile 中指定特定的镜像 ID。
这里有一个例子(可能对你不起作用):
列表 3.23. 带有特定镜像 ID 的 Dockerfile
FROM 8eaa4ff06b53 *1*
RUN echo "Built from image id:" > /etc/buildinfo *2*
RUN echo "8eaa4ff06b53" >> /etc/buildinfo *2*
RUN echo "an ubuntu 14.4.01 image" >> /etc/buildinfo *2*
CMD ["echo","/etc/buildinfo"] *3*
-
1 从特定的镜像(或层)ID 构建
-
2 在此镜像内运行命令以将构建的镜像记录在新镜像中的一个文件中
-
3 构建的镜像默认会输出你在 /etc/buildinfo 文件中记录的信息。
要从特定的镜像(或层)ID 构建如下,该镜像 ID 及其数据必须存储在本地 Docker 守护进程上。Docker 仓库不会执行任何类型的查找,以在 Docker Hub 上可用的镜像层中找到该镜像 ID,或在任何其他配置的仓库中。
注意,你引用的镜像不需要标记——它可以是任何你本地的层。你可以从任何你希望的层开始构建。这可能对某些需要为 Dockerfile 构建分析执行的手术或实验性程序很有用。
如果你希望远程持久化镜像,最好是将镜像标记并推送到你控制的远程仓库。
警告
值得指出的是,当之前工作的 Docker 镜像突然不再工作时,可能会发生几乎相反的问题。通常这是因为网络中发生了变化。一个难忘的例子是,我们有一次构建在早上无法执行 apt-get update。我们假设这是本地 deb 缓存的问题,并尝试调试但没有成功,直到一位友好的系统管理员指出,我们正在构建的 Ubuntu 版本已经不再受支持。这意味着 apt-get update 的网络调用返回了 HTTP 错误。
讨论
虽然这可能听起来有点理论化,但了解您想要构建或运行的镜像的更具体信息的优缺点是很重要的。
更加具体可以使得您行动的结果更加可预测和可调试,因为关于哪个 Docker 镜像被下载或曾经下载的模糊性更少。缺点是您的镜像可能不是最新可用的,因此您可能会错过关键更新。您更倾向于哪种状态将取决于您的特定用例以及您在 Docker 环境中需要优先考虑的内容。
在下一节中,您将应用您所学到的知识到一个相对有趣的现实场景:在 2048 中获胜。
3.3. 环境作为进程
一种看待 Docker 的方式是将其视为将环境转换为进程。虚拟机也可以以同样的方式处理,但 Docker 使这一过程更加方便和高效。
为了说明这一点,我们将向您展示快速启动、存储和重新创建容器状态如何让您做到其他情况下(几乎)不可能的事情——在 2048 中获胜!
“保存游戏”方法:在 2048 中获胜
这种技术旨在在向您展示如何使用 Docker 轻松回滚状态的同时,为您提供一些轻松的娱乐。如果您不熟悉 2048,它是一款上瘾的游戏,您需要在棋盘上推动数字。如果您想先熟悉一下,原始版本可在gabrielecirulli.github.io/2048上找到。
问题
您希望定期保存容器状态,以便在必要时回滚到已知状态。
解决方案
使用docker commit来“保存游戏”,无论您是否确定能在 2048 中存活。
我们创建了一个单体镜像,您可以在其中使用 Docker 容器玩 2048,该容器包含一个 VNC 服务器和 Firefox。
要使用此镜像,您需要安装一个 VNC 客户端。流行的实现包括 TigerVNC 和 VNC Viewer。如果您没有,在主机上的包管理器中快速搜索“vnc client”应该会得到有用的结果。
要启动容器,请运行以下命令。
列表 3.24. 启动 2048 容器
$ docker run -d -p 5901:5901 -p 6080:6080 --name win2048 imiell/win2048 *1*
$ vncviewer localhost:1 *2*
-
1 运行 imiell/win2048 镜像作为守护进程
-
2 使用 VNC 获取容器的 GUI 访问权限
首先,您从为我们准备的imiell/win2048镜像运行一个容器。您将其在后台启动,并指定它应该打开两个端口(5901 和 6080)到主机。这些端口将由容器内自动启动的 VNC 服务器使用。您还给了容器一个名字,以便以后方便使用:win2048。
您现在可以运行您的 VNC 查看器(可执行文件可能取决于您安装了什么),并指示它连接到您的本地计算机。由于容器已暴露了适当的端口,连接到 localhost 实际上会连接到容器。如果您的宿主上没有 X 显示(除了标准桌面),则 localhost 后的 :1 是合适的——如果您有,您可能需要选择不同的数字,并查看您的 VNC 查看器的文档以手动指定 VNC 端口为 5901。
一旦您连接到 VNC 服务器,您将需要输入密码。此镜像上 VNC 的密码是“vncpass”。然后您将看到一个带有 Firefox 标签和预加载的 2048 表格的窗口。点击它以获得焦点,并玩到您准备好保存游戏为止。
要保存游戏,您在提交后标记命名容器:
列表 3.25. 提交并标记游戏状态
$ docker commit win2048 1((CO14-1)) *1*
4ba15c8d337a0a4648884c691919b29891cbbe26cb709c0fde74db832a942083 *2*
$ docker tag 4ba15c8d337 my2048tag:$(date +%s) *3*
-
1 提交
win2048容器 -
2 引用您的提交的标记
-
3 使用当前时间作为整数标记提交
通过提交 win2048 容器生成了一个镜像 ID,现在您想给它一个独特的名称(因为您可能正在创建许多这样的镜像)。为此,您可以使用 date +%s 的输出作为镜像名称的一部分。这输出自 1970 年第一天以来的秒数,提供了一个独特(对我们来说)、不断增长的价值。$(command) 语法只是在该位置替换 command 的输出。如果您愿意,您可以手动运行 date +%s 并将输出粘贴为镜像名称的一部分。
您可以继续玩游戏,直到您输掉为止。现在来点魔法!您可以使用以下命令返回到您的保存点。
列表 3.26. 返回保存的游戏
$ docker rm -f win2048
$ docker run -d -p 5901:5901 -p 6080:6080 --name win2048 my2048tag:$mytag
$mytag 是从 docker images 命令中选择的标记。重复 tag、rm 和 run 步骤,直到完成 2048。
讨论
我们希望这很有趣。这个例子比实际更有趣,但我们已经使用——并看到其他开发者使用——这种技术取得了很好的效果,尤其是在他们的环境复杂且他们所做的工作相对具有调查性和复杂时。
摘要
-
您可以创建一个看起来像“正常”主机的 Docker 容器。有些人认为这是不好的做法,但它可能对您的业务有益或适合您的用例。
-
将虚拟机转换为 Docker 镜像相对简单,这是迈向 Docker 的第一步。
-
您可以监督容器上的服务,以模仿它们之前的类似虚拟机操作。
-
随时保存您的作品是提交的正确方式。
-
您可以通过使用其构建 ID 来指定要从中构建的特定 Docker 镜像。
-
您可以为您的图像命名,并在 Docker Hub 上免费与世界分享。
-
您甚至可以使用 Docker 的提交功能在 2048 等游戏中获胜!
第四章. 构建镜像
本章涵盖
-
镜像创建的一些基础知识
-
操作 Docker 构建缓存以实现快速和可靠的构建
-
在镜像构建过程中配置时区
-
从主机直接在容器上运行命令
-
深入研究镜像构建过程中创建的层
-
在构建和使用镜像时使用更高级的 ONBUILD 功能
要超越 Docker 的基本使用,你将想要开始创建自己的构建块(镜像),以有趣的方式组合在一起。本章将涵盖镜像创建的一些重要部分,探讨你可能遇到的实际问题。
4.1. 构建镜像
尽管 Dockerfile 的简单性使它们成为节省时间的强大工具,但也有一些细微之处可能会引起混淆。我们将向您介绍一些节省时间的功能及其细节,从 ADD 指令开始。然后我们将介绍 Docker 构建缓存,它如何让你失望,以及如何操纵它以获得优势。
记得查阅官方 Docker 文档,了解完整的 Dockerfile 指令,网址为 docs.docker.com。
使用 ADD 将文件注入到您的镜像中
尽管可以在 Dockerfile 中使用 RUN 命令和基本的 shell 基本操作来添加文件,但这很快就会变得难以管理。为了解决将大量文件放入镜像而无需麻烦的需求,ADD 命令被添加到了 Dockerfile 命令列表中。
问题
你希望以简洁的方式下载并解压 tarball 到你的镜像中。
解决方案
将文件打包并压缩,然后在 Dockerfile 中使用 ADD 指令。
使用 mkdir add_example && cd add_example 创建一个用于此 Docker 构建的新环境。然后检索一个 tarball 并给它一个你可以稍后引用的名字。
列表 4.1. 下载 TAR 文件
$ curl \
https://www.flamingspork.com/projects/libeatmydata/
libeatmydata-105.tar.gz > my.tar.gz
在这种情况下,我们使用了一个来自另一种技术的 TAR 文件,但它可以是任何你喜欢的 tarball。
列表 4.2. 将 TAR 文件添加到镜像中
FROM debian
RUN mkdir -p /opt/libeatmydata
ADD my.tar.gz /opt/libeatmydata/
RUN ls -lRt /opt/libeatmydata
使用 docker build --no-cache . 构建此 Dockerfile,输出应该如下所示:
列表 4.3. 使用 TAR 文件构建镜像
$ docker build --no-cache .
Sending build context to Docker daemon 422.9 kB
Sending build context to Docker daemon
Step 0 : FROM debian
---> c90d655b99b2
Step 1 : RUN mkdir -p /opt/libeatmydata
---> Running in fe04bac7df74
---> c0ab8c88bb46
Removing intermediate container fe04bac7df74
Step 2 : ADD my.tar.gz /opt/libeatmydata/
---> 06dcd7a88eb7
Removing intermediate container 3f093a1f9e33
Step 3 : RUN ls -lRt /opt/libeatmydata
---> Running in e3283848ad65
/opt/libeatmydata:
total 4
drwxr-xr-x 7 1000 1000 4096 Oct 29 23:02 libeatmydata-105
/opt/libeatmydata/libeatmydata-105:
total 880
drwxr-xr-x 2 1000 1000 4096 Oct 29 23:02 config
drwxr-xr-x 3 1000 1000 4096 Oct 29 23:02 debian
drwxr-xr-x 2 1000 1000 4096 Oct 29 23:02 docs
drwxr-xr-x 3 1000 1000 4096 Oct 29 23:02 libeatmydata
drwxr-xr-x 2 1000 1000 4096 Oct 29 23:02 m4
-rw-r--r-- 1 1000 1000 9803 Oct 29 23:01 config.h.in
[...edited...]
-rw-r--r-- 1 1000 1000 1824 Jun 18 2012 pandora_have_better_malloc.m4
-rw-r--r-- 1 1000 1000 742 Jun 18 2012 pandora_header_assert.m4
-rw-r--r-- 1 1000 1000 431 Jun 18 2012 pandora_version.m4
---> 2ee9b4c8059f
Removing intermediate container e3283848ad65
Successfully built 2ee9b4c8059f
你可以从这个输出中看到,tarball 已经被 Docker 守护进程(所有文件的扩展输出已被编辑)解压到目标目录中。Docker 将解压大多数标准类型的 tarfile(.gz, .bz2, .xz, .tar)。
值得注意的是,尽管你可以从 URL 下载 tarball,但只有当它们存储在本地文件系统中时,它们才会自动解压。这可能会导致混淆。
如果你使用以下 Dockerfile 重复执行前面的过程,你会注意到文件被下载但没有解压。
列表 4.4. 直接从 URL 添加 TAR 文件
FROM debian
RUN mkdir -p /opt/libeatmydata
ADD \ *1*
https://www.flamingspork.com/projects/libeatmydata/libeatmydata-105.tar.gz \
/opt/libeatmydata/ *2*
RUN ls -lRt /opt/libeatmydata
-
1 文件通过 URL 从互联网检索。
-
2 目标目录由目录名和尾随斜线指示。如果没有尾随斜线,则参数被视为下载文件的文件名。
这里是生成的构建输出:
Sending build context to Docker daemon 422.9 kB
Sending build context to Docker daemon
Step 0 : FROM debian
---> c90d655b99b2
Step 1 : RUN mkdir -p /opt/libeatmydata
---> Running in 6ac454c52962
---> bdd948e413c1
Removing intermediate container 6ac454c52962
Step 2 : ADD \
https://www.flamingspork.com/projects/libeatmydata/libeatmydata-105.tar.gz
/opt/libeatmydata/
Downloading [==================================================>] \
419.4 kB/419.4 kB
---> 9d8758e90b64
Removing intermediate container 02545663f13f
Step 3 : RUN ls -lRt /opt/libeatmydata
---> Running in a947eaa04b8e
/opt/libeatmydata:
total 412
-rw------- 1 root root 419427 Jan 1 1970 \
libeatmydata-105.tar.gz *1*
---> f18886c2418a
Removing intermediate container a947eaa04b8e
Successfully built f18886c2418a
- 1 libeatmydata-105.tar.gz 文件已下载并放置在/opt/libeatmydata 目录中,未解压。
注意,在先前的 Dockerfile 中的ADD行末尾没有斜杠,文件将下载并保存为该文件名。末尾的斜杠表示文件应该下载并放置在指定的目录中。
所有新的文件和目录都属于 root(或在容器内具有组或用户 ID 为 0 的任何人)。
文件名中的空白字符
如果你的文件名中有空白字符,你需要使用ADD(或COPY)的引号形式:
ADD "space file.txt" "/tmp/space file.txt"
讨论
ADD Dockerfile 指令是一个非常强大的工具,具有许多不同的功能,你可以利用这些功能。如果你打算编写超过几个 Dockerfile(随着你阅读这本书,你很可能会这样做),阅读官方 Dockerfile 指令文档是值得的——文档并不多(在撰写本文时,文档中列出了 18 条指令)并且你只会经常使用其中的一些。
人们经常询问如何添加未解压的压缩文件。为此,你应该使用COPY命令,它看起来与ADD命令完全一样,但不会解压任何文件,也不会从互联网上下载。
| |
不使用缓存重建
使用 Dockerfile 构建利用了一个有用的缓存功能:只有当命令已更改时,已构建的步骤才会重新构建。下一个列表显示了从第一章重建待办事项应用程序的重建输出。
列表 4.5. 使用缓存重建
$ docker build .
Sending build context to Docker daemon 2.56 kB
Sending build context to Docker daemon
Step 0 : FROM node
---> 91cbcf796c2c
Step 1 : MAINTAINER ian.miell@gmail.com
---> Using cache *1*
---> 8f5a8a3d9240 *2*
Step 2 : RUN git clone -q https://github.com/docker-in-practice/todo.git
---> Using cache
---> 48db97331aa2
Step 3 : WORKDIR todo
---> Using cache
---> c5c85db751d6
Step 4 : RUN npm install > /dev/null
---> Using cache
---> be943c45c55b
Step 5 : EXPOSE 8000
---> Using cache
---> 805b18d28a65
Step 6 : CMD npm start
---> Using cache
---> 19525d4ec794
Successfully built 19525d4ec794 *3*
-
1 表示你正在使用缓存
-
2 指定缓存的镜像/层 ID
-
3 最终镜像“重建”,但实际上没有任何变化。
尽管这很有用且节省时间,但这并不总是你想要的行为。
以先前的 Dockerfile 为例,假设你已更改了源代码并将其推送到 Git 仓库。新的代码不会被检出,因为git clone命令没有改变。就 Docker 构建而言,它是相同的,因此可以重用缓存的镜像。
在这些情况下,你将想要不使用缓存重建你的镜像。
问题
你想在不使用缓存的情况下重建 Dockerfile。
解决方案
要强制不使用镜像缓存进行重建,请使用带有--no-cache标志的docker build运行。以下列表使用--no-cache运行了先前的构建。
列表 4.6. 不使用缓存强制重建
$ docker build --no-cache . *1*
Sending build context to Docker daemon 2.56 kB
Sending build context to Docker daemon
Step 0 : FROM node
---> 91cbcf796c2c
Step 1 : MAINTAINER ian.miell@gmail.com
---> Running in ca243b77f6a1 *2*
---> 602f1294d7f1 *3*
Removing intermediate container ca243b77f6a1
Step 2 : RUN git clone -q https://github.com/docker-in-practice/todo.git
---> Running in f2c0ac021247
---> 04ee24faaf18
Removing intermediate container f2c0ac021247
Step 3 : WORKDIR todo
---> Running in c2d9cd32c182
---> 4e0029de9074
Removing intermediate container c2d9cd32c182
Step 4 : RUN npm install > /dev/null
---> Running in 79122dbf9e52
npm WARN package.json todomvc-swarm@0.0.1 No repository field.
---> 9b6531f2036a
Removing intermediate container 79122dbf9e52
Step 5 : EXPOSE 8000
---> Running in d1d58e1c4b15
---> f7c1b9151108
Removing intermediate container d1d58e1c4b15
Step 6 : CMD npm start
---> Running in 697713ebb185
---> 74f9ad384859
Removing intermediate container 697713ebb185
Successfully built 74f9ad384859 *4*
-
1 重建 Docker 镜像,忽略带有--no-cache 标志的缓存层
-
2 这次没有提及缓存
-
3 中间的镜像 ID 与先前的列表不同。
-
4 构建了一个新镜像。
输出结果没有提及缓存,并且每个中间层 ID 都与列表 4.5 中的输出不同。
类似的问题也可能在其他情况下发生。我们最初在使用 Dockerfile 时遇到了困惑,因为网络问题导致命令无法从网络中正确检索某些内容,但命令并没有出错。我们一直调用docker build,但产生的错误却一直存在!这是因为一个“不良”的镜像已经进入了缓存,而我们不了解 Docker 缓存的工作方式。最终我们弄明白了。
讨论
在您有了最终的 Dockerfile 之后,移除缓存可以是一个有用的合理性检查,以确保它从头到尾都能正常工作,尤其是当您在公司内部使用可能已经更改的 Web 资源时。如果您使用ADD,这种情况不会发生,因为 Docker 会每次都下载文件以检查它是否已更改,但如果您非常确信它将保持不变,只想继续编写 Dockerfile 的其余部分,这种行为可能会让人感到厌烦。
| |
打破缓存
使用--no-cache标志通常足以解决任何与缓存相关的问题,但有时您可能需要一个更细致的解决方案。例如,如果您有一个耗时较长的构建,您可能希望使用缓存到某个点,然后使缓存失效以重新运行命令并创建新镜像。
问题
您希望从 Dockerfile 构建的特定点无效化 Docker 构建缓存。
解决方案
在命令后添加一个无害的注释以使缓存失效。
从github.com/docker-in-practice/todo中的 Dockerfile 开始(这对应于以下输出中的Step行),我们已经进行了构建,并在 Dockerfile 中CMD行的旁边添加了注释。您可以在下面看到再次执行docker build的输出:
$ docker build . *1*
Sending build context to Docker daemon 2.56 kB
Sending build context to Docker daemon
Step 0 : FROM node
---> 91cbcf796c2c
Step 1 : MAINTAINER ian.miell@gmail.com
---> Using cache
---> 8f5a8a3d9240
Step 2 : RUN git clone -q https://github.com/docker-in-practice/todo.git
---> Using cache
---> 48db97331aa2
Step 3 : WORKDIR todo
---> Using cache
---> c5c85db751d6
Step 4 : RUN npm install
---> Using cache
---> be943c45c55b
Step 5 : EXPOSE 8000
---> Using cache *2*
---> 805b18d28a65
Step 6 : CMD ["npm","start"] #bust the cache *3*
---> Running in fc6c4cd487ce
---> d66d9572115e *4*
Removing intermediate container fc6c4cd487ce
Successfully built d66d9572115e
-
1 一个“正常”的 Docker 构建
-
2 缓存已使用到这里。
-
3 缓存已被失效,但命令本身实际上没有改变。
-
4 已创建新镜像。
这个技巧之所以有效,是因为 Docker 将行中的非空白更改视为新命令,因此不会重新使用缓存的层。
您可能想知道(就像我们最初查看 Docker 时那样),您是否可以将 Docker 层从镜像移动到另一个镜像,像 Git 中的更改集一样随意合并。在 Docker 中目前无法做到这一点。层被定义为从给定镜像的更改集。因此,一旦缓存被打破,它就不能在构建过程中稍后重用的命令中重新应用。因此,建议如果可能的话,将不太可能改变的命令放在 Dockerfile 的顶部。
讨论
对于 Dockerfile 的初始迭代,将每个命令拆分成单独的层对于迭代速度来说非常好,因为你可以选择性地重新运行过程的部分,如前一个列表所示,但这对于生成小型最终镜像来说并不那么好。对于具有一定复杂性的构建来说,接近 42 层的硬限制并不罕见。为了减轻这种情况,一旦你有一个满意的正常构建,你应该查看 技术 56 中的步骤,以创建一个生产就绪的镜像。
| |
使用构建参数进行智能缓存中断
在之前的技术中,你看到了如何通过更改相关行在构建过程中中断缓存。
在这个技术中,我们将通过控制是否从构建命令中断缓存来更进一步。
问题
你希望在构建时按需中断缓存,而不需要编辑 Dockerfile。
解决方案
在你的 Dockerfile 中使用 ARG 指令来启用手术式缓存中断。
为了演示这一点,你将再次使用 github.com/docker-in-practice/todo 中的 Dockerfile,但对其做一点小的修改。
你想要在 npm install 之前控制缓存的中断。你为什么要这样做呢?正如你所学的,默认情况下,Docker 只有在 Dockerfile 中的命令更改时才会中断缓存。但是,让我们想象一下,有更新的 npm 包可用,你想要确保你得到它们。一个选项是手动更改这一行(如你在之前的技术中看到的),但实现相同目标的一种更优雅的方法是使用 Docker ARGS 指令和 bash 技巧。
按照以下方式将 ARG 行添加到 Dockerfile 中。
列表 4.7. 具有可中断缓存的简单 Dockerfile
WORKDIR todo
ARG CACHEBUST=no *1*
RUN npm install
- 1
ARG指令为构建设置环境变量。
在这个例子中,你使用 ARG 指令设置 CACHEBUST 环境变量,并在 docker build 命令未设置时将其默认设置为 no。
现在按照“正常”方式构建 Dockerfile:
$ docker build .
Sending build context to Docker daemon 2.56kB
Step 1/7 : FROM node
latest: Pulling from library/node
aa18ad1a0d33: Pull complete
15a33158a136: Pull complete
f67323742a64: Pull complete
c4b45e832c38: Pull complete
f83e14495c19: Pull complete
41fea39113bf: Pull complete
f617216d7379: Pull complete
cbb91377826f: Pull complete
Digest: sha256:
a8918e06476bef51ab83991aea7c199bb50bfb131668c9739e6aa7984da1c1f6
Status: Downloaded newer image for node:latest
---> 9ea1c3e33a0b
Step 2/7 : MAINTAINER ian.miell@gmail.com
---> Running in 03dba6770157
---> a5b55873d2d8
Removing intermediate container 03dba6770157
Step 3/7 : RUN git clone https://github.com/docker-in-practice/todo.git
---> Running in 23336fd5991f
Cloning into 'todo'...
---> 8ba06824d184
Removing intermediate container 23336fd5991f
Step 4/7 : WORKDIR todo
---> f322e2dbeb85
Removing intermediate container 2aa5ae19fa63
Step 5/7 : ARG CACHEBUST=no
---> Running in 9b4917f2e38b
---> f7e86497dd72
Removing intermediate container 9b4917f2e38b
Step 6/7 : RUN npm install
---> Running in a48e38987b04
npm info it worked if it ends with ok
[...]
added 249 packages in 49.418s
npm info ok
---> 324ba92563fd
Removing intermediate container a48e38987b04
Step 7/7 : CMD npm start
---> Running in ae76fa693697
---> b84dbc4bf5f1
Removing intermediate container ae76fa693697
Successfully built b84dbc4bf5f1
如果你再次使用完全相同的 docker build 命令构建它,你会观察到 Docker 构建缓存被使用,并且结果镜像没有进行任何更改。
$ docker build .
Sending build context to Docker daemon 2.56kB
Step 1/7 : FROM node
---> 9ea1c3e33a0b
Step 2/7 : MAINTAINER ian.miell@gmail.com
---> Using cache
---> a5b55873d2d8
Step 3/7 : RUN git clone https://github.com/docker-in-practice/todo.git
---> Using cache
---> 8ba06824d184
Step 4/7 : WORKDIR todo
---> Using cache
---> f322e2dbeb85
Step 5/7 : ARG CACHEBUST=no
---> Using cache
---> f7e86497dd72
Step 6/7 : RUN npm install
---> Using cache
---> 324ba92563fd
Step 7/7 : CMD npm start
---> Using cache
---> b84dbc4bf5f1
Successfully built b84dbc4bf5f1
到目前为止,你决定你想强制重新构建 npm 包。也许一个错误已经被修复,或者你想要确保你是最新的。这就是你在 列表 4.7 中添加到 Dockerfile 中的 ARG 变量发挥作用的地方。如果这个 ARG 变量在你的主机上从未使用过,那么从那个点开始缓存将被中断。
这是你在 docker build 中使用 build-arg 标志并配合 bash 技巧强制使用新值的地方:
$ docker build --build-arg CACHEBUST=${RANDOM} . *1*
Sending build context to Docker daemon 4.096 kB
Step 1/9 : FROM node
---> 53d4d5f3b46e
Step 2/9 : MAINTAINER ian.miell@gmail.com
---> Using cache
---> 3a252318543d
Step 3/9 : RUN git clone https://github.com/docker-in-practice/todo.git
---> Using cache
---> c0f682653a4a
Step 4/9 : WORKDIR todo
---> Using cache
---> bd54f5d70700
Step 5/9 : ARG CACHEBUST=no *2*
---> Using cache
---> 3229d52b7c33
Step 6/9 : RUN npm install *3*
---> Running in 42f9b1f37a50
npm info it worked if it ends with ok
npm info using npm@4.1.2
npm info using node@v7.7.2
npm info attempt registry request try #1 at 11:25:55 AM
npm http request GET https://registry.npmjs.org/compression
npm info attempt registry request try #1 at 11:25:55 AM
[...]
Step 9/9 : CMD npm start
---> Running in 19219fe5307b
---> 129bab5e908a
Removing intermediate container 19219fe5307b
Successfully built 129bab5e908a
-
1 使用带有
build-arg标志的docker build命令,将CACHEBUST参数设置为 bash 生成的伪随机值 -
2 因为
ARG CACHEBUST=no行本身没有更改,所以这里使用了缓存。 -
3 因为 CACHEBUST 参数被设置为一个之前未设置的值,缓存被打破,npm 安装命令再次运行。
注意,缓存是在 ARG 行之后的行上打破的,而不是 ARG 行本身。这可能会有些令人困惑。关键是要注意“运行中”这个短语——这意味着已经创建了一个新的容器来运行构建行。
解释 ${RANDOM} 参数的用法是值得的。Bash 提供了这个保留变量名,以便你能够轻松地获取一个长度为一到五位数的值:
$ echo ${RANDOM}
19856
$ echo ${RANDOM}
26429
$ echo ${RANDOM}
2856
这可能很有用,比如当你想要一个可能唯一的值来为特定脚本的运行创建文件时。
如果你担心冲突,你甚至可以生成一个更长的随机数:
$ echo ${RANDOM}${RANDOM}
434320509
$ echo ${RANDOM}${RANDOM}
1327340
注意,如果你没有使用 bash(或具有此 RANDOM 变量的 shell),这个技术将不起作用。在这种情况下,你可以使用 date 命令来生成一个新鲜值:
$ docker build --build-arg CACHEBUST=$(date +%s) .
讨论
这种技术在使用 Docker 时展示了一些实用的功能。你已经学会了如何使用 --build-args 标志向 Dockerfile 传递一个值,并在需要时打破缓存,创建一个不更改 Dockerfile 的新构建。
如果你使用 bash,你也已经学会了 RANDOM 变量的用法,以及它在 Docker 构建之外的其他上下文中的有用性。
| |
使用 ADD 指令进行智能缓存打破
在前面的技术中,你看到了如何在构建过程中选择性地打破缓存,这比使用 --no-cache 标志完全忽略缓存要高级。
现在,你将把它提升到下一个层次,这样你就可以在必要时自动打破缓存。这可以为你节省大量的时间和计算资源——因此,也可以节省金钱!
问题
你希望在远程资源发生变化时打破缓存。
解决方案
使用 Dockerfile 的 ADD 指令仅在 URL 的响应发生变化时打破缓存。
Dockerfile 早期的一些批评之一是,它们声称能够产生可靠的构建结果是不准确的。确实,我们在 2013 年就这个问题与 Docker 的创造者进行了讨论(mng.bz/B8E4)。
具体来说,如果你在 Dockerfile 中使用如下指令调用网络,
RUN git clone https://github.com/nodejs/node
默认情况下,Docker 构建将在每个 Docker 守护程序上执行一次。GitHub 上的代码可能会发生重大变化,但就你的 Docker 守护程序而言,构建是更新的。即使年复一年,同一个 Docker 守护程序仍然会使用缓存。
这可能听起来像是一个理论上的担忧,但对于许多用户来说,这是一个非常现实的问题。我们在工作中已经多次看到这种情况发生,导致困惑。你已经看到了一些解决方案,但对于许多复杂或大型构建,这些解决方案还不够细致。
智能缓存打破模式
假设你有一个如下所示的 Dockerfile(注意,它不会工作!它只是一个 Dockerfile 模式,用于展示原理)。
列表 4.8. 一个示例 Dockerfile
FROM ubuntu:16.04
RUN apt-get install -y git and many other packages *1*
RUN git clone https://github.com/nodejs/node *2*
WORKDIR node
RUN make && make install *3*
-
1 安装一系列作为先决条件的包
-
2 克隆一个经常变化的仓库(nodejs 只是一个例子)
-
3 运行 make 和 install 命令,用于构建项目
这个 Dockerfile 在创建高效的构建过程中提出了一些挑战。如果你每次都想要从头开始构建一切,解决方案很简单:使用 docker build 的 --no-cache 参数。但问题在于,每次你运行构建时,你都会在第二行重复包的安装,而这(大部分)是不必要的。
这个挑战可以通过在 git clone 之前清除缓存来解决(如上一种技术所示)。然而,这又提出了另一个挑战:如果 Git 仓库没有更改呢?那么你将进行可能代价高昂的网络传输,随后是可能代价高昂的 make 命令。网络、计算和磁盘资源都被不必要地使用了。
解决这个问题的方法之一是使用 技术 23,每次当你知道远程仓库已更改时,都传递一个具有新值的构建参数。但这也仍然需要手动调查来确定是否发生了更改,并采取干预措施。
你需要的是一个命令,它可以确定资源自上次构建以来是否已更改,然后才清除缓存。
ADD 指令——意外的好处
现在是时候使用 ADD 指令了!
你已经熟悉 ADD,因为它是 Dockerfile 的一个基本指令。通常它用于将文件添加到结果镜像中,但 ADD 有两个有用的特性,你可以在这种情况下利用它们:它缓存它引用的文件的内容,并且它可以接受网络资源作为参数。这意味着你可以在网络请求的输出发生变化时随时清除缓存。
你如何在克隆仓库时利用这一点?嗯,这取决于你通过网络引用的资源性质。许多资源在仓库本身更改时会有一个页面发生变化,但这些会因资源类型而异。在这里,我们将关注 GitHub 仓库,因为这是一个常见的用例。
GitHub API 提供了一个有用的资源,可以帮助你在这里。它为每个仓库提供了返回最新提交 JSON 的 URL。当创建新提交时,响应的内容会发生变化。
列表 4.9. 使用 ADD 触发缓存清除
FROM ubuntu:16.04
ADD https://api.github.com/repos/nodejs/node/commits *1*
/dev/null *2*
RUN git clone https://github.com/nodejs/node *3*
[...]
-
1 当创建新提交时更改的 URL
-
2 文件输出去向无关紧要,所以我们将其发送到 /dev/null。
-
3 仅在发生更改时才会进行 git clone
前一个列表的结果是,只有在最后一次构建之后向仓库提交了提交时,缓存才会被破坏。不需要人工干预,也不需要手动检查。
如果你想要使用频繁更改的仓库测试此机制,请尝试使用 Linux 内核。
列表 4.10. 将 Linux 内核代码添加到镜像中
FROM ubuntu:16.04
ADD https://api.github.com/repos/torvalds/linux/commits /dev/null *1*
RUN echo "Built at: $(date)" >> /build_time *2*
-
1 使用 Linux 仓库的 ADD 命令
-
2 将系统日期输出到构建的镜像中,这将显示最后一次缓存破坏构建发生的时间
如果你创建一个文件夹,将前面的代码放入 Dockerfile 中,然后定期运行以下命令(例如每小时一次),输出日期只有在 Linux Git 仓库更改时才会改变。
列表 4.11. 构建 Linux 代码镜像
$ docker build -t linux_last_updated . *1*
$ docker run linux_last_updated cat /build_time *2*
-
1 构建镜像并给它命名为 linux_last_updated
-
2 输出结果镜像中 /build_time 文件的内容
讨论
这种技术展示了一种宝贵的自动化技术,以确保仅在必要时进行构建。
它还展示了 ADD 命令的一些工作细节。你看到“文件”可以是网络资源,如果文件(或网络资源)的内容从以前的构建中更改,则会发生缓存破坏。
此外,你还看到了网络资源有相关的资源可以指示你引用的资源是否已更改。虽然你可以,例如,引用主 GitHub 页面来查看是否有任何更改,但该页面可能比最后一次提交更改得更频繁(例如,如果网页响应的时间被埋藏在页面源代码中,或者如果每个响应都有一个唯一的引用字符串)。
在 GitHub 的情况下,你可以引用 API,正如你所看到的。其他服务,如 BitBucket,提供类似资源。例如,Kubernetes 项目提供此 URL 来指示哪个版本是稳定的:storage.googleapis.com/kubernetesrelease/release/stable.txt。如果你正在构建基于 Kubernetes 的项目,你可以在 Dockerfile 中添加一条 ADD 行,以便在响应更改时破坏缓存。
在容器中设置正确的时间区域
如果你曾经安装过完整的操作系统,你就会知道设置时区是设置过程的一部分。即使容器不是操作系统(或虚拟机),它也包含告诉程序如何解释配置时区时间的文件。
问题
你想要为你的容器设置正确的时间区域。
解决方案
将容器的 localtime 文件替换为指向你想要的时间区域的链接。
以下列表展示了问题。无论你在世界的哪个地方运行它,容器都会显示相同的时区。
列表 4.12. 以错误时区开始的容器
$ date +%Z *1*
GMT *2*
$ docker run centos:7 date +%Z *3*
UTC *4*
-
1 运行命令以显示主机上的时区
-
2 主机上的时区是 GMT。
-
3 运行容器并输出容器内的日期
-
4 容器中的时区是 GMT。
容器包含确定容器使用哪个时区来解释它获取的时间值的文件。实际使用的时间当然由主机操作系统跟踪。
下一个列表显示了如何设置您想要的时区。
列表 4.13. 替换 centos:7 默认时区的 Dockerfile
FROM centos:7 *1*
RUN rm -rf /etc/localtime *2*
RUN ln -s /usr/share/zoneinfo/GMT /etc/localtime *3*
CMD date +%Z *4*
-
1 从我们刚才查看的 centos 镜像启动
-
2 删除现有的 localtime 链接文件
-
3 将 /etc/localtime 链接替换为指向您想要的时区的链接
-
4 显示容器的时区作为默认要运行的命令
在 列表 4.13 中,关键文件是 /etc/localtime。它指向一个文件,告诉容器在请求时间时使用哪个时区。默认时间是以 UTC 时间标准给出的,如果文件不存在(例如,最小的 BusyBox 镜像没有它),则使用该时间。
下一个列表显示了构建前面 Dockerfile 的输出。
列表 4.14. 构建替换时区的 Dockerfile
$ docker build -t timezone_change . *1*
Sending build context to Docker daemon 62.98 kB
Step 1 : FROM centos:7
7: Pulling from library/centos
45a2e645736c: Pull complete
Digest: sha256:
c577af3197aacedf79c5a204cd7f493c8e07ffbce7f88f7600bf19c688c38799
Status: Downloaded newer image for centos:7
---> 67591570dd29
Step 2 : RUN rm -rf /etc/localtime
---> Running in fb52293849db
---> 0deda41be8e3
Removing intermediate container fb52293849db
Step 3 : RUN ln -s /usr/share/zoneinfo/GMT /etc/localtime
---> Running in 47bf21053b53
---> 5b5cb1197183
Removing intermediate container 47bf21053b53
Step 4 : CMD date +%Z
---> Running in 1e481eda8579
---> 9477cdaa73ac
Removing intermediate container 1e481eda8579
Successfully built 9477cdaa73ac
$ docker run timezone_change *2*
GMT *3*
-
1 构建容器
-
2 运行容器
-
3 输出指定的时区
以这种方式,您可以在容器内部指定要使用的时区——并且仅限于容器内部。许多应用程序都依赖于这个设置,所以如果您运行 Docker 服务,它并不罕见。
这种容器级时间粒度还可以解决另一个问题。如果您为跨国组织工作,并在全球数据中心的服务器上运行许多不同的应用程序,那么在您的镜像中更改时区并相信它无论在哪里都能报告正确的时间,这是一个有用的技巧。
讨论
由于 Docker 镜像的目的是明确地提供无论在哪里运行容器都一致的经验,因此如果您希望根据镜像部署的位置得到不同的结果,您可能会遇到一些事情。
例如,如果您正在为不同地点的用户自动生成数据 CSV 电子表格,他们可能对数据格式有一定的期望。美国用户可能期望日期以 mm/dd 格式显示,而欧洲用户可能期望日期以 dd/mm 格式显示,中国用户可能期望日期以他们自己的字符集显示。
在下一个技术中,我们将考虑区域设置,这会影响日期和时间在 local 格式中的打印方式,以及其他方面。
| |
区域管理
除了时区之外,区域也是 Docker 镜像的另一个方面,在构建镜像或运行容器时可能相关。
注意
区域设置定义了您的程序应使用哪种语言和国家设置。通常,区域设置将通过LANG、LANGUAGE和locale-gen变量在环境中设置,以及以LC_开头的变量,例如LC_TIME,其设置决定了时间如何显示给用户。
| |
注意
编码(在这个上下文中)是文本在计算机上以字节形式存储的方式。关于这个主题的良好介绍可以在 W3C 这里找到:www.w3.org/International/questions/qa-what-is-encoding。花时间理解这个主题是值得的,因为它在各种上下文中都会出现。
问题
您在应用程序构建或部署中看到编码错误。
解决方案
确保在 Dockerfile 中正确设置了语言特定的环境变量。
编码问题并不总是对所有用户都很明显,但在构建应用程序时可能会致命。
这里是构建 Docker 中的应用程序时典型的编码错误的一些示例。
列表 4.15. 典型的编码错误
MyFileDialog:66: error: unmappable character for encoding ASCII
UnicodeEncodeError: 'ascii' codec can't encode character u'\xa0' in
position 20: ordinal not in range(128)
这些错误可能会使构建或应用程序完全失败。
提示
在错误中需要注意的关键词列表不完整,包括“encoding”、“ascii”、“unicode”、“UTF-8”、“character”和“codec”。如果您看到这些单词,那么您可能正在处理一个编码问题。
这与 Docker 有什么关系?
当您设置一个完整的操作系统时,您通常会通过一个设置过程引导,该过程要求您确认首选时区、语言、键盘布局等。
如您所知,Docker 容器不是为通用用途设置的完整操作系统。相反,它们是(越来越多地)用于运行应用程序的最小环境。因此,默认情况下,它们可能不会包含您在操作系统中习惯的所有设置。
尤其是 Debian 在 2011 年移除了对区域设置包的依赖,这意味着默认情况下,基于 Debian 镜像的容器中没有区域设置。例如,以下列表显示了基于 Debian 的 Ubuntu 镜像的默认环境。
列表 4.16. Ubuntu 容器上的默认环境
$ docker run -ti ubuntu bash
root@d17673300830:/# env
HOSTNAME=d17673300830
TERM=xterm
LS_COLORS=rs=0 [...]
HIST_FILE=/root/.bash_history
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
SHLVL=1
HOME=/root
_=/usr/bin/envj
默认情况下,在镜像中没有可用的LANG或类似的LC_设置。
我们的 Docker 主机在下一个列表中显示。
列表 4.17. Docker 主机 OS 上的 LANG 设置
$ env | grep LANG
LANG=en_GB.UTF-8
在我们的 shell 中有一个LANG设置,它通知应用程序我们的终端首选编码是英国英语,文本以 UTF-8 编码。
为了演示编码问题,我们将在本地创建一个包含 UTF-8 编码的英国货币符号(英国的英镑符号)的文件,然后展示根据终端的编码,该文件的解释如何变化。
列表 4.18. 创建并显示一个 UTF-8 编码的英国货币符号
$ env | grep LANG
LANG=en_GB.UTF-8
$ echo -e "\xc2\xa3" > /tmp/encoding_demo *1*
$ cat /tmp/encoding_demo *2*
£ *2*
-
1 使用带有-e 标志的 echo 将两个字节输出到文件中,这两个字节代表一个英国英镑符号
-
2 读取文件;我们将看到一个英镑符号。
在 UTF-8 中,英镑符号由两个字节表示。我们使用echo -e和\x表示法输出这两个字节,并将输出重定向到文件。当我们cat文件时,终端读取这两个字节并知道将输出解释为英镑符号。
现在如果我们更改我们的终端编码以使用西方(ISO Latin 1)编码(这也设置了我们的本地LANG),然后输出文件,看起来相当不同:
列表 4.19. 使用英国货币符号演示编码问题
$ env | grep LANG *1*
LANG=en_GB.ISO8859-1 *1*
$ cat /tmp/encoding_demo *2*
£ *2*
-
1 现在
LANG环境变量被设置为西方(ISO Latin 1),这是由终端设置的。 -
2 这两个字节被解释为两个不同的字符,显示给我们。
\xc2字节被解释为带上方重音符号的大写A,而\xa3字节被解释为英国英镑符号!
注意
我们上面故意使用“我们”而不是“你”!调试和控制编码是一件棘手的事情,这可以取决于运行应用程序的状态、你设置的环境变量、运行的应用程序以及所有创建你正在检查的数据的前置因素!
如你所见,编码可能会受到终端中设置的编码集的影响。回到 Docker,我们注意到在我们的 Ubuntu 容器中默认没有设置编码环境变量。因此,当你在主机或容器中运行相同的命令时,可能会得到不同的结果。如果你看到似乎与编码相关的错误,你可能需要在你的 Dockerfile 中设置它们。
在 Dockerfile 中设置编码
我们现在将探讨如何控制基于 Debian 的镜像的编码。我们选择这个镜像,因为它可能是更常见的上下文之一。此示例将设置一个简单的镜像,它只输出其默认环境变量。
列表 4.20. 设置 Dockerfile 示例
FROM ubuntu:16.04 *1*
RUN apt-get update && apt-get install -y locales *2*
RUN locale-gen en_US.UTF-8 *3*
ENV LANG en_US.UTF-8 *4*
ENV LANGUAGE en_US:en *5*
CMD env *6*
-
1 使用基于 Debian 的基础镜像
-
2 更新软件包索引并安装区域设置包
-
3 为美国英语生成区域设置,编码为 UTF-8
-
4 设置
LANG环境变量 -
5 设置
LANGUAGE环境变量 -
6 默认的命令
env将显示容器的环境设置。
你可能想知道LANG和LANGUAGE变量之间的区别是什么。简而言之,LANG是首选语言和编码设置的默认设置。它还在应用程序查找更具体的LC_*设置时提供默认值。LANGUAGE用于在主要语言不可用时,为应用程序提供一个按顺序排列的语言偏好列表。更多信息可以通过运行man locale来获取。
现在你可以构建镜像,并运行它以查看发生了什么变化。
列表 4.21. 构建encoding镜像并运行
$ docker build -t encoding . *1*
[...]
$ docker run encoding *2*
no_proxy=*.local, 169.254/16
LANGUAGE=en_US:en *3*
HOSTNAME=aa9d6c8a3ff5
HOME=/root
HIST_FILE=/root/.bash_history
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=en_US.UTF-8 *4*
PWD=/
-
1 构建编码 Docker 镜像
-
2 运行构建的 Docker 镜像
-
3 在环境中设置了 LANGUAGE 变量
-
4 在环境中设置了 LANG 变量
讨论
与之前的时间区域技术一样,这个技术展示了人们经常遇到的一个问题。像许多我们遇到的更令人烦恼的问题一样,这些问题在构建镜像时并不总是显而易见,这使得调试这些问题所浪费的时间非常令人沮丧。因此,在支持使用 Docker 镜像的其他人时,值得记住这些设置。
| |
使用 image-stepper 通过镜像层进行步骤
如果你构建了一个包含多个步骤的镜像,你可能会发现自己想要知道某个特定文件是在哪个步骤中引入的,或者它在构建的某个特定时刻处于什么状态。逐层检查每个镜像层可能会很费力,因为你需要确定层的顺序,检索每个 ID,并使用该 ID 启动每一层。
这个技术展示了如何用一行命令按顺序标记构建的每一层,这意味着你只需要递增一个数字就可以遍历镜像并找出你需要知道的信息。
问题
你希望轻松地引用构建的每个步骤。
解决方案
使用 docker-in-practice/image-stepper 镜像来为你的镜像排序标签。
为了说明这个技术,我们首先会展示一个实现这一结果的脚本,以便你理解它是如何工作的。然后,我们将提供一个构建的镜像,以便更容易地实现这一结果。
这是一个简单的脚本,它会按照创建顺序给给定镜像(myimage)中的每一层打上标签。
myimage 的 Dockerfile 如下。
列表 4.22. 具有多层镜像的 Dockerfile
FROM debian *1*
RUN touch /file1 *2*
RUN touch /file2 *2*
RUN touch /file3 *2*
RUN touch /file4 *2*
RUN touch /file5 *2*
RUN touch /file6 *2*
RUN touch /file7 *2*
RUN touch /file8 *2*
RUN touch /file9 *2*
RUN touch /file10 *2*
CMD ["cat","/file1"] *3*
-
1 使用 debian 作为基础镜像
-
2 在单独的层中创建 10 个文件
-
3 运行一个自定义命令来显示第一个文件
这是一个简单的 Dockerfile,但它清楚地表明了你在构建过程中的哪个阶段。
使用以下命令构建此 Docker 镜像。
列表 4.23. 构建 myimage 镜像
$ docker build -t myimage -q . *1*
sha256:b21d1e1da994952d8e309281d6a3e3d14c376f9a02b0dd2ecbe6cabffea95288 *2*
-
1 使用静默(-q)标志构建镜像,并标记为 myimage
-
2 镜像标识符是唯一的输出。
镜像构建完成后,你可以运行以下脚本。
列表 4.24. 以数字顺序标记 myimage 的每一层
#!/bin/bash
x=1 *1*
for id in $(docker history -q "myimage:latest" | *2*
grep -vw missing *3*
| tac) *4*
do
docker tag "${id}" "myimage:latest_step_${x}" *5*
((x++)) *6*
done
-
1 初始化计数器变量(x)为 1
-
2 运行一个循环来检索镜像的历史记录
-
3 不考虑远程构建的层,这些层标记为缺失(见下面的注释)
-
4 使用 tac 实用程序来反转 docker history 命令输出的镜像 ID 顺序
-
5 在循环的每次迭代中,使用递增的数字适当地标记镜像
-
6 增加步骤计数器
如果你将前面的文件保存为 tag.sh 并运行它,镜像将按层顺序标记。
注意
这种打标签的方法技术仅适用于本地构建的镜像。有关更多信息,请参阅技术 16 中的注释。
列表 4.25. 打标签和显示层
$ ./tag.sh *1*
$ docker images | grep latest_step *2*
myimage latest_step_12 1bfca0ef799d 3 minutes ago 123.1 MB *3*
myimage latest_step_11 4d7f66939a4c 3 minutes ago 123.1 MB *3*
myimage latest_step_10 78d31766b5cb 3 minutes ago 123.1 MB *3*
myimage latest_step_9 f7b4dcbdd74f 3 minutes ago 123.1 MB *3*
myimage latest_step_8 69b2fa0ce520 3 minutes ago 123.1 MB *3*
myimage latest_step_7 b949d71fb58a 3 minutes ago 123.1 MB *3*
myimage latest_step_6 8af3bbf1e7a8 3 minutes ago 123.1 MB *3*
myimage latest_step_5 ce3dfbdfed74 3 minutes ago 123.1 MB *3*
myimage latest_step_4 598ed62cabb9 3 minutes ago 123.1 MB *3*
myimage latest_step_3 6b290f68d4d5 3 minutes ago 123.1 MB *3*
myimage latest_step_2 586da987f40f 3 minutes ago 123.1 MB *3*
myimage latest_step_1 19134a8202e7 7 days ago 123.1 MB *4*
-
1 运行来自列表 4.24 的脚本
-
2 运行带有简单 grep 的 docker images 命令,以查看打标签的层
-
3 构建 myimage 镜像的步骤
-
4 原始(较旧)的基础镜像也已标记为 latest_step_1。
现在你已经了解了原理,我们将演示如何将这个一次性脚本 docker 化,使其适用于通用情况。
注意
该技术的代码可在github.com/docker-in-practice/image-stepper找到。
首先,将之前的脚本转换成一个可以接受参数的脚本。
列表 4.26. image-stepper 镜像的通用打标签脚本
#!/bin/bash *1*
IMAGE_NAME=$1 *1*
IMAGE_TAG=$2 *1*
if [[ $IMAGE_NAME = '' ]] *1*
then *1*
echo "Usage: $0 IMAGE_NAME [ TAG ]" *1*
exit 1 *1*
fi *1*
if [[ $IMAGE_TAG = '' ]] *1*
then *1*
IMAGE_TAG=latest *1*
fi *1*
x=1 *2*
for id in $(docker history -q "${IMAGE_NAME}:${IMAGE_TAG}" | *2*
grep -vw missing | tac) *2*
do *2*
docker tag "${id}" "${IMAGE_NAME}:${IMAGE_TAG}_step_$x" *2*
((x++)) *2*
done *2*
-
1 定义一个 bash 脚本,它可以接受两个参数:要处理的镜像名称和要升级到的标签
-
2 将参数替换后的列表 4.24 中的脚本
你可以将列表 4.26 中的脚本嵌入到一个 Docker 镜像中,并将其放置在 Dockerfile 中,作为默认的ENTRYPOINT运行。
列表 4.27. image-stepper 镜像的 Dockerfile
FROM ubuntu:16.04 *1*
RUN apt-get update -y && apt-get install -y docker.io *2*
ADD image_stepper /usr/local/bin/image_stepper *3*
ENTRYPOINT ["/usr/local/bin/image_stepper"] *4*
-
1 使用 Ubuntu 作为基础层
-
2 安装 docker.io 以获取 Docker 客户端二进制文件
-
3 将列表 4.26 中的脚本添加到镜像中
-
4 默认运行 image_stepper 脚本
列表 4.27 中的 Dockerfile 创建了一个运行列表 4.26 中脚本的镜像。在列表 4.28 中的命令运行此镜像,并将myimage作为参数。
当这个镜像在你的主机上构建的另一个 Docker 镜像上运行时,将为每个步骤创建标签,让你可以轻松地按顺序查看层。
由 docker.io 包安装的客户端二进制版本必须与主机机器上 Docker 守护进程的版本兼容,通常意味着客户端不能比守护进程更新。
列表 4.28. 对另一个镜像运行 image-stepper
$ docker run --rm *1*
-v /var/run/docker.sock:/var/run/docker.sock *2*
dockerinpractice/image-stepper *3*
myimage *4*
Unable to find image 'dockerinpractice/image-stepper:latest' locally *5*
latest: Pulling from dockerinpractice/image-stepper *5*
b3e1c725a85f: Pull complete *5*
4daad8bdde31: Pull complete *5*
63fe8c0068a8: Pull complete *5*
4a70713c436f: Pull complete *5*
bd842a2105a8: Pull complete *5*
1a3a96204b4b: Pull complete *5*
d3959cd7b55e: Pull complete *5*
Digest: sha256: *5*
65e22f8a82f2221c846c92f72923927402766b3c1f7d0ca851ad418fb998a753 *5*
Status: Downloaded newer image for dockerinpractice/image-stepper:latest *5*
$ docker images | grep myimage *6*
myimage latest 2c182dabe85c 24 minutes ago 123 MB *7*
myimage latest_step_12 2c182dabe85c 24 minutes ago 123 MB *7*
myimage latest_step_11 e0ff97533768 24 minutes ago 123 MB *7*
myimage latest_step_10 f46947065166 24 minutes ago 123 MB *7*
myimage latest_step_9 8a9805a19984 24 minutes ago 123 MB *7*
myimage latest_step_8 88e42bed92ce 24 minutes ago 123 MB *7*
myimage latest_step_7 5e638f955e4a 24 minutes ago 123 MB *7*
myimage latest_step_6 f66b1d9e9cbd 24 minutes ago 123 MB *7*
myimage latest_step_5 bd07d425bd0d 24 minutes ago 123 MB *7*
myimage latest_step_4 ba913e75a0b1 24 minutes ago 123 MB *7*
myimage latest_step_3 2ebcda8cd503 24 minutes ago 123 MB *7*
myimage latest_step_2 58f4ed4fe9dd 24 minutes ago 123 MB *7*
myimage latest_step_1 19134a8202e7 2 weeks ago 123 MB *7*
$ docker run myimage:latest_step_8 ls / | grep file *8*
file1 *9*
file2 *9*
file3 *9*
file4 *9*
file5 *9*
file6 *9*
file7 *9*
-
1 以容器形式运行 image-stepper 镜像,完成后删除容器
-
2 挂载主机的 docker 套接字,这样你就可以使用列表 4.27 中安装的 Docker 客户端
-
3 从 Docker Hub 下载 image-stepper 镜像
-
4 为之前创建的 myimage 打标签
-
5 docker run 命令的输出
-
6 运行 docker images 并 grep 出你刚刚标记的镜像
-
7 镜像已打标签。
-
8 随机选择一个步骤并列出根目录中的文件,grep 出列表 4.27 中的 Dockerfile 创建的文件
-
9 显示的文件是到该步骤为止创建的文件。
注意
在非 Linux 操作系统(如 Mac 和 Windows)上,你可能需要在 Docker 首选项中指定 Docker 运行的文件夹,作为文件共享设置。
这种技术有助于查看在构建过程中某个特定文件是如何被添加的,或者文件在构建的某个特定点处于什么状态。在调试构建时,这可能非常有价值!
讨论
这种技术用于技术 52 中,以演示已删除的秘密在镜像的层中是可访问的。
ONBUILD 和 golang
ONBUILD指令可能会让新的 Docker 用户感到困惑。这个技术通过构建和运行一个带有两行 Dockerfile 的 Go 应用程序,在真实世界场景中展示了其用法。
问题
你希望减少构建应用程序所需的镜像步骤。
解决方案
使用ONBUILD命令来自动化和封装镜像的构建。
首先,你将运行这个过程,然后我们将解释正在发生的事情。我们将使用的例子是 outyet 项目,这是 golang GitHub 仓库中的一个例子。它所做的只是设置一个返回页面的网络服务,告诉你 Go 1.4 是否已经可用。
按以下方式构建镜像。
列表 4.29. 构建 outyet 镜像
$ git clone https://github.com/golang/example *1*
$ cd example/outyet *2*
$ docker build -t outyet . *3*
-
1 克隆 Git 仓库
-
2 导航到 outyet 文件夹
-
3 构建 outyet 镜像
从生成的镜像运行容器,并检索提供的网页。
列表 4.30. 运行和验证 outyet 镜像
$ docker run *1*
--publish 8080:8080 *2*
--name outyet1 -d outyet *3*
$ curl localhost:8080 *4*
<!DOCTYPE html><html><body><center> *5*
<h2>Is Go 1.4 out yet?</h2> *5*
<h1> *5*
<a href="https://go.googlesource.com/go/+/go1.4">YES!</a> *5*
</h1> *5*
</center></body></html> *5*
-
1
--publish标志告诉 Docker 将容器的端口 8080 发布到外部端口 6060。 -
2
--name标志为容器提供一个可预测的名称,以便更容易地与之交互。 -
3 在后台运行容器
-
4 卷曲输出容器的端口
-
5 容器提供的网页
就这样——一个简单的应用程序,返回一个网页,告诉你 Go 1.4 是否已经发布。
如果你环顾克隆的仓库,你会看到 Dockerfile 只有两行!
列表 4.31. onyet Dockerfile
FROM golang:onbuild *1*
EXPOSE 8080 *2*
-
1 从 golang:onbuild 镜像开始构建
-
2 暴露端口 8080
还感到困惑吗?好吧,当你查看 golang:onbuild 镜像的 Dockerfile 时,这可能更有意义。
列表 4.32. golang:onbuild Dockerfile
FROM golang:1.7 *1*
RUN mkdir -p /go/src/app *2*
WORKDIR /go/src/app *3*
CMD ["go-wrapper", "run"] *4*
ONBUILD COPY . /go/src/app *5*
ONBUILD RUN go-wrapper download *6*
ONBUILD RUN go-wrapper install *7*
-
1 使用 golang:1.7 镜像作为基础
-
2 创建一个文件夹来存储应用程序
-
3 移动到那个文件夹
-
4 将结果镜像的命令设置为调用 go-wrapper 来运行 go 应用程序
-
5 第一个 ONBUILD 命令将 Dockerfile 上下文中的代码复制到镜像中。
-
6 第二个 ONBUILD 命令使用 go-wrapper 命令下载任何依赖项。
-
7 第三个 ONBUILD 指令
golang:onbuild 镜像定义了当在其他 Dockerfile 的FROM指令中使用该镜像时会发生什么。结果是,当 Dockerfile 使用此镜像作为基础时,ONBUILD命令将在FROM镜像下载后立即触发,并且(如果未覆盖)当结果镜像作为容器运行时将运行CMD。
现在下面列表中docker build命令的输出可能更有意义。
Step 1 : FROM golang:onbuild *1*
onbuild: Pulling from library/golang *1*
6d827a3ef358: Pull complete *1*
2726297beaf1: Pull complete *1*
7d27bd3d7fec: Pull complete *1*
62ace0d726fe: Pull complete *1*
af8d7704cf0d: Pull complete *1*
6d8851391f39: Pull complete *1*
988b98d9451c: Pull complete *1*
5bbc96f59ddc: Pull complete *1*
Digest: sha256:
886a63b8de95d5767e779dee4ce5ce3c0437fa48524aedd93199fb12526f15e0
Status: Downloaded newer image for golang:onbuild
# Executing 3 build triggers... *2*
Step 1 : COPY . /go/src/app *3*
Step 1 : RUN go-wrapper download *4*
---> Running in c51f9b0c4da8
+ exec go get -v -d *5*
Step 1 : RUN go-wrapper install *6*
---> Running in adaa8f561320
+ exec go install -v *7*
app
---> 6bdbbeb8360f
Removing intermediate container 47c446aa70e3
*8*
Removing intermediate container c51f9b0c4da8 *8*
Removing intermediate container adaa8f561320
*8*
Step 2 : EXPOSE 8080 *9*
---> Running in 564d4a34a34b
---> 5bf1767318e5
Removing intermediate container 564d4a34a34b
Successfully built 5bf1767318e5
-
1 执行 FROM 指令,并使用 golang:onbuild 镜像。
-
2 Docker 构建发出运行 ONBUILD 指令的意图。
-
3 执行第一个 ONBUILD 指令,将 Dockerfile 上下文中的 Go 代码复制到构建中。
-
4 触发第二个 ONBUILD 指令,用于下载。
-
5 go-wrapper 调用触发对 go get 的 shell 调用。
-
6 执行第三个 ONBUILD 指令,该指令安装应用程序。
-
7 go-wrapper 调用触发对 go install 的 shell 调用。
-
8 由 ONBUILD 命令创建的三个容器被移除。
-
9 Dockerfile 的第二行中运行的 EXPOSE 指令。
这种技术的结果是,你有一个简单的方法来构建一个只包含运行它所需的代码的镜像,不再需要更多。在镜像中留下构建工具不仅使其比需要的大,还增加了运行容器的安全攻击面。
讨论
由于 Docker 和 Go 是目前经常一起看到的流行技术,我们使用了这个例子来展示如何使用ONBUILD来构建 Go 二进制文件。
ONBUILD镜像的其他示例存在。Docker Hub 上有可用的node:onbuild和python:onbuild镜像。
希望这能激发你构建自己的ONBUILD镜像,以帮助你的组织处理常见的构建模式。这种标准化可以帮助进一步减少不同团队之间的阻抗不匹配。
概述
-
你可以从你的本地机器和互联网上插入文件到镜像中。
-
缓存是构建镜像的关键部分,但它可能是一个反复无常的朋友,有时需要提示才能完成你想要的事情。
-
你可以使用构建参数或使用
ADD指令来“打破”缓存,或者你可以使用no-cache选项完全忽略缓存。 -
ADD指令通常用于将本地文件和文件夹注入到构建的镜像中。 -
系统配置在 Docker 内部可能仍然相关,而镜像构建时间是一个很好的进行配置的时间。
-
你可以使用“image-stepper”技术(技术 27)来调试你的构建过程,该技术为你标记构建的每个阶段。
-
时区设置是在配置容器时最常见的“陷阱”,尤其是当你是一家非美国或跨国公司时。
-
带有
ONBUILD的镜像非常容易使用,因为你可能根本不需要自定义构建。
第五章. 运行容器
本章涵盖
-
在 Docker 中使用 GUI 应用程序
-
获取容器信息
-
您可以终止容器的不同方式
-
在远程机器上启动容器
-
使用和管理 Docker 卷以持久共享数据
-
学习您的第一个 Docker 模式:数据和开发工具容器
在使用 Docker 时,如果不运行容器,您将无法走得很远,如果您想充分利用它们提供的功能,那么有很多东西需要理解。
本章将探讨运行容器的一些细节,检查一些具体用例,并详细阐述通过卷实现的可能性的处理。
5.1. 运行容器
尽管这本书的大部分内容都是关于运行容器的,但有一些与在您的宿主机上运行容器相关的实用技术可能并不立即明显。我们将探讨您如何使 GUI 应用程序工作,在远程机器上启动容器,检查容器的状态及其源镜像,关闭容器,管理远程机器上的 Docker 守护程序,以及使用通配符 DNS 服务使测试更容易。
在 Docker 中运行 GUI
您已经看到了使用 VNC 服务器在技术 19 中从 Docker 容器中提供 GUI 的一种方式。这是查看 Docker 容器中应用程序的一种方法,它是自包含的,只需要一个 VNC 客户端即可使用。
幸运的是,有一种更轻量级且集成度更高的方式在您的桌面上运行 GUI,但这需要您进行更多的设置。它将目录挂载到管理 X 服务器的宿主机上,以便容器可以访问。
问题
您希望在容器中运行 GUI,就像它们是正常的桌面应用程序一样。
解决方案
创建一个包含您的用户凭证和程序的镜像,并将您的 X 服务器绑定挂载到它上。
图 5.1 显示了最终设置将如何工作。
图 5.1. 与宿主机的 X 服务器通信

容器通过宿主机/tmp/.X11目录的挂载与宿主机链接,这就是容器如何在宿主机的桌面上执行操作的方式。
首先,在某个方便的地方创建一个新的目录,并使用id命令确定您的用户和组 ID,如下所示。
列表 5.1. 设置目录和获取用户详细信息
$ mkdir dockergui
$ cd dockergui
$ id *1*
uid=1000(dockerinpractice) \ *2*
gid=1000(dockerinpractice) \ *3*
groups=1000(dockerinpractice),10(wheel),989(vboxusers),990(docker)
-
1 获取您在 Dockerfile 中需要的关于您用户的信息
-
2 注意您的用户 ID(uid)。在这种情况下,它是 1000。
-
3 注意您的组 ID(gid)。在这种情况下,它是 1000。
现在创建一个名为 Dockerfile 的文件,如下所示。
列表 5.2. Dockerfile 中的 Firefox
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y firefox *1*
RUN groupadd -g GID USERNAME *2*
RUN useradd -d /home/USERNAME -s /bin/bash \
-m USERNAME -u UID -g GID *3*
USER USERNAME *4*
ENV HOME /home/USERNAME *5*
CMD /usr/bin/firefox *6*
-
1 安装 Firefox 作为 GUI 应用程序。您可以将此更改为您可能想要的任何应用程序。
-
2 将宿主机的组添加到镜像中。用 GID 替换您的组 ID,用 USERNAME 替换您的用户名。
-
3 将您的用户账户添加到镜像中。将 USERNAME 替换为您的用户名,UID 替换为您的用户 ID,GID 替换为您的组 ID。
-
4 镜像应以您创建的用户身份运行。将 USERNAME 替换为您的用户名。
-
5 设置 HOME 变量。将 USERNAME 替换为您的用户名。
-
6 默认情况下在启动时运行 Firefox
现在,您可以从该 Dockerfile 构建,并将结果标记为“gui”:
$ docker build -t gui .
按以下方式运行:
docker run -v /tmp/.X11-unix:/tmp/.X11-unix \ *1*
-h $HOSTNAME -v $HOME/.Xauthority:/home/$USER/.Xauthority \ *2*
-e DISPLAY=$DISPLAY gui *3*
-
1 将 X 服务器目录绑定到容器
-
2 在容器中将 DISPLAY 变量设置为与主机上使用的相同,以便程序知道与哪个 X 服务器通信
-
3 为容器提供适当的凭据
您将看到一个 Firefox 窗口弹出!
讨论
您可以使用此技术来避免混淆您的桌面工作和开发工作。例如,使用 Firefox,您可能想以可重复的方式查看您的应用程序在没有网络缓存、书签或搜索历史的情况下如何表现,以便进行测试。如果您在尝试启动镜像并运行 Firefox 时看到无法打开显示的错误消息,请参阅技术 65 了解允许容器在主机上启动图形应用程序的其他方法。
我们理解有些人几乎在 Docker 内运行所有应用程序,包括游戏!虽然我们没有做到那么极端,但了解有人可能已经遇到了您遇到的问题是有用的。
| |
检查容器
虽然 Docker 命令让您可以访问有关镜像和容器的信息,但有时您可能想了解更多关于这些 Docker 对象内部元数据的信息。
问题
您想找出容器的 IP 地址。
解决方案
使用 docker inspect 命令。
docker inspect 命令以 JSON 格式提供 Docker 的内部元数据,包括 IP 地址。此命令会产生大量输出,因此这里只显示了镜像元数据的一个简短片段。
列表 5.3. 镜像的原始 inspect 输出
$ docker inspect ubuntu | head
{
"Architecture": "amd64",
"Author": "",
"Comment": "",
"Config": {
"AttachStderr": false,
"AttachStdin": false,
"AttachStdout": false,
"Cmd": [
"/bin/bash"
$
您可以通过名称或 ID 检查镜像和容器。显然,它们的元数据将不同——例如,容器将具有“状态”等运行时字段,而镜像则没有(镜像没有状态)。
在这种情况下,您想找出主机上的容器 IP 地址。为此,您可以使用带有 format 标志的 docker inspect 命令。
列表 5.4. 确定容器的 IP 地址
docker inspect \ *1*
--format '{{.NetworkSettings.IPAddress}}' \ *2*
0808ef13d450 *3*
-
1 docker inspect 命令
-
2 格式标志。这使用 Go 模板(此处未介绍)来格式化输出。在此,IPAddress 字段是从 inspect 输出的 NetworkSettings 字段中获取的。
-
3 您想要检查的 Docker 项目的 ID
这种技术对于自动化很有用,因为接口可能比其他 Docker 命令更稳定。
以下命令提供了所有运行容器的 IP 地址并对其进行了 ping 操作。
列表 5.5. 获取运行容器的 IP 地址并对每个进行 ping
$ docker ps -q | \ *1*
xargs docker inspect --format='{{.NetworkSettings.IPAddress}}' | \ *2*
xargs -l1 ping -c1 *3*
PING 172.17.0.5 (172.17.0.5) 56(84) bytes of data.
64 bytes from 172.17.0.5: icmp_seq=1 ttl=64 time=0.095 ms
--- 172.17.0.5 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.095/0.095/0.095/0.000 ms
-
1 获取所有运行容器的容器 ID
-
2 针对所有容器 ID 运行 inspect 命令以获取它们的 IP 地址
-
3 逐个 IP 地址运行 ping 命令
注意,因为 ping 只接受一个 IP 地址,所以我们不得不向 xargs 传递一个额外的参数,告诉它为每行运行命令。
小贴士
如果你没有正在运行的容器,运行以下命令来启动一个:docker run -d ubuntu sleep 1000。
讨论
检查容器和在 [技术 47 中跳入容器的方法可能是你调试容器为何不工作的两个最重要的工具。当你认为你已经以特定方式配置了容器但行为出乎意料时,Inspect 最为出色——你的第一步应该是检查容器,以验证 Docker 是否同意你对端口和卷映射的预期,以及其他事项。
| |
干净地杀死容器
如果容器终止时的状态对你很重要,你可能想了解 docker kill 和 docker stop 之间的区别。如果你需要应用程序优雅地关闭以保存数据,这个区别也可能很重要。
问题
你想要干净地终止一个容器。
解决方案
使用 docker stop 而不是 docker kill 来干净地终止容器。
需要理解的关键点是 docker kill 的行为与标准的命令行 kill 程序不同。
kill 程序通过向指定的进程发送 TERM 信号(即信号值 15),除非有其他指示。此信号指示程序应该终止,但它不会强制程序。大多数程序在处理此信号时会执行某种类型的清理,但程序可以按自己的意愿行事——包括忽略信号。
与之相反,KILL 信号(即信号值 9)强制指定的程序终止。
令人困惑的是,docker kill 在运行进程上使用 KILL 信号,给其中的进程没有机会处理终止。这意味着可能会在文件系统中留下一些散乱的文件,例如包含运行进程 ID 的文件。根据应用程序管理状态的能力,这可能会或可能不会在你再次启动容器时给你带来问题。
更令人困惑的是,docker stop 命令的行为类似于标准的 kill 命令,发送 TERM 信号(见 表 5.1),但它将等待 10 秒钟,如果容器没有停止,则发送 KILL 信号。
表 5.1. 停止和杀死
| 命令 | 默认信号 | 默认信号值 |
|---|---|---|
| kill | TERM | 15 |
| docker kill | KILL | 9 |
| docker stop | TERM | 15 |
简而言之,不要像使用kill命令那样使用docker kill。你最好养成使用docker stop的习惯。
讨论
虽然我们推荐在日常使用中使用docker stop,但docker kill有一些额外的可配置性,允许你通过--signal参数选择发送给容器的信号。如前所述,默认是KILL,但你也可以发送TERM或一些不太常见的 Unix 信号。
如果你正在编写自己的应用程序,你将在容器中启动它,那么USR1信号可能会引起你的兴趣。这个信号是专门为应用程序保留的,以便它们可以随意使用它,在某些地方它被用作打印进度信息或等效信息的指示——你可以用它来做任何你觉得合适的事情。"HUP"是另一个流行的信号,传统上被服务器和其他长时间运行的应用程序解释为触发配置文件的重新加载和“软”重启。当然,在开始向应用程序发送随机信号之前,确保检查你正在运行的应用程序的文档!
| |
使用 Docker Machine 来配置 Docker 主机
在你的本地机器上设置 Docker 可能并不太难——有一个方便的脚本可以使用,或者你可以使用几个命令来为你的包管理器添加适当的源。但是当你试图在其他主机上管理 Docker 安装时,这可能会变得很繁琐。
问题
你想在机器上启动一个与你的机器分开的 Docker 主机上的容器。
解决方案
Docker Machine 是管理远程机器上 Docker 安装的官方解决方案。
如果你需要在多个外部主机上运行 Docker 容器,这项技术将非常有用。你可能出于许多原因想要这样做:通过在你的物理主机内配置一个虚拟机来测试 Docker 容器之间的网络;通过 VPS 提供商在更强大的机器上配置容器;进行某种疯狂实验以冒主机损坏的风险;在多个云提供商之间进行选择。无论出于什么原因,Docker Machine 可能正是你所需要的答案。它也是进入更复杂的编排工具(如 Docker Swarm)的门户。
什么是 Docker Machine
Docker Machine 主要是一个方便的程序。它将配置外部主机的大量潜在复杂的指令包装起来,并将它们转换成几个易于使用的命令。如果你熟悉 Vagrant,它有类似的感觉:通过一致的界面简化了其他机器环境的配置和管理。如果你回想起我们在第二章中的架构概述,看待 Docker Machine 的一种方式是想象它正在帮助管理来自一个客户端的不同 Docker 守护进程(参见图 5.2)。
图 5.2. Docker Machine 作为外部主机的客户端

图 5.2 中 Docker 主机提供商的列表并不全面,并且可能会增长。在撰写本文时,以下驱动程序可用,允许您配置给定的主机提供商:
-
Amazon Web Services
-
DigitalOcean
-
Google Compute Engine
-
IBM SoftLayer
-
Microsoft Azure
-
Microsoft Hyper-V
-
OpenStack
-
Oracle VirtualBox
-
Rackspace
-
VMware Fusion
-
VMware vCloud Air
-
VMware vSphere
必须指定的选项将根据驱动程序提供的功能有很大差异。在一端,在你的机器上配置 Oracle VirtualBox VM 只提供了 3 个用于 create 的标志,而与 OpenStack 的 17 个相比。
注意
值得澄清的是,Docker Machine 并不是 Docker 的任何类型的集群解决方案。其他工具,如 Docker Swarm,履行这一功能,我们将在后面讨论。
安装
安装涉及一个简单的二进制文件。不同架构的下载链接和安装说明可在此处找到:github.com/docker/machine/releases。
注意
您可能希望将二进制文件移动到标准位置,如 /usr/bin,并在继续之前确保将其重命名或创建到 docker-machine 的符号链接,因为下载的文件可能具有较长的名称,后缀为二进制文件的架构。
使用 Docker Machine
为了演示 Docker Machine 的使用,您可以首先创建一个带有 Docker 守护进程的 VM,您可以在其上工作。
注意
为了使此操作生效,您需要安装 Oracle 的 VirtualBox。它在大多数包管理器中广泛可用。
$ docker-machine create --driver virtualbox host1 *1*
INFO[0000] Creating CA: /home/imiell/.docker/machine/certs/ca.pem
INFO[0000] Creating client certificate:
/home/imiell/.docker/machine/certs/cert.pem
INFO[0002] Downloading boot2docker.iso to /home/imiell/.docker/machine/cache/
boot2docker.iso...
INFO[0011] Creating VirtualBox VM...
INFO[0023] Starting VirtualBox VM...
INFO[0025] Waiting for VM to start...
INFO[0043] "host1" has been created and is now the active machine. *2*
INFO[0043] To point your Docker client at it, run this in your shell:
$(docker-machine env host1) *3*
-
1 使用 docker-machine 的 create 子命令创建一个新的主机,并使用 --driver 标志指定其类型。该主机已被命名为
host1。 -
2 您的机器现在已创建。
-
3 运行此命令以设置 DOCKER_HOST 环境变量,该变量设置 Docker 命令将运行的默认主机
Vagrant 用户在这里会感到非常自在。通过运行这些命令,您已创建了一个可以现在管理 Docker 的机器。如果您遵循输出中给出的说明,可以直接 SSH 到新的 VM:
$ eval $(docker-machine env host1) *1*
$ env | grep DOCKER *2*
DOCKER_HOST=tcp://192.168.99.101:2376 *3*
DOCKER_TLS_VERIFY=yes *4*
DOCKER_CERT_PATH=/home/imiell/.docker/machine/machines/host1 *4*
DOCKER_MACHINE_NAME=host1
$ docker ps -a *5*
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
$ docker-machine ssh host1 *6*
## .
## ## ## ==
## ## ## ## ===
/""""""""""""""""\___/ ===
~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~
\______ o __/
\ \ __/
\____\______/
_ _ ____ _ _
| |__ ___ ___ | |_|___ \ __| | ___ ___| | _____ _ __
| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__| < __/ |
|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
Boot2Docker version 1.5.0, build master : a66bce5 - Tue Feb 10 23:31:27 UTC 2015
Docker version 1.5.0, build a8a31ef
docker@host1:~$
-
1 $() 将 docker-machine env 命令的输出应用于您的环境。docker-machine env 输出一组命令,您可以使用这些命令来设置 Docker 命令的默认主机。
-
2 环境变量名称都以前缀 DOCKER_ 开头。
-
3 DOCKER_HOST 变量是 VM 上 Docker 守护进程的端点。
-
4 这些变量处理与新主机连接的安全性方面。
-
5 docker 命令现在指向您创建的 VM 主机,而不是之前使用的宿主机。您在新 VM 上没有创建任何容器,因此没有输出。
-
6 ssh 子命令将直接带您到新的 VM 本身。
管理主机
从一个客户端机器管理多个 Docker 宿主机可能会使跟踪发生的事情变得困难。Docker Machine 提供了各种管理命令来简化这个过程,如 表 5.2 所示。
表 5.2. Docker-machine 命令列表
| 子命令 | 操作 |
|---|---|
| create | 创建新的机器 |
| ls | 列出 Docker 宿主机 |
| stop | 停止机器 |
| start | 启动机器 |
| restart | 停止并启动机器 |
| rm | 销毁机器 |
| kill | 关闭机器 |
| inspect | 返回机器元数据的 JSON 表示形式 |
| config | 返回连接到机器所需的配置 |
| ip | 返回机器的 IP 地址 |
| url | 返回机器上 Docker 守护进程的 URL |
| upgrade | 将宿主机上的 Docker 版本升级到最新版本 |
以下示例列出了两个机器。活动机器带有星号标记,并且与它关联有一个状态,类似于容器或进程的状态:
$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM
host1 virtualbox Running tcp://192.168.99.102:2376
host2 * virtualbox Running tcp://192.168.99.103:2376
小贴士
你可能想知道如何切换回原始宿主机 Docker 实例。在撰写本文时,我们还没有找到一种简单的方法来做这件事。你可以选择使用 docker-machine rm 删除所有机器,或者如果这不是一个选项,你可以手动取消之前使用 unset DOCKER_HOST DOCKER_TLS_VERIFY DOCKER_CERT_PATH 设置的环境变量。
讨论
你可以将这看作是将机器转化为进程,就像 Docker 本身可以将环境转化为进程一样。
使用 Docker Machine 设置手动管理多个宿主机上的容器可能很有吸引力,但如果你在代码更改时发现需要手动关闭容器、重新构建它们并重新启动,我们鼓励你查看本书的 第四部分。这类繁琐的任务完全可以由计算机完美完成。技术 87 涵盖了 Docker Inc. 提供的官方解决方案,用于创建容器的自动集群。技术 84 如果你喜欢集群的统一视图,但又希望保留对容器最终运行位置的完全控制,可能很有吸引力。
| |
通配符 DNS
当使用 Docker 时,拥有许多需要引用中心或外部服务的运行容器是非常常见的。在测试或开发此类系统时,通常使用静态 IP 地址为这些服务。但对于许多基于 Docker 的系统,如 OpenShift,IP 地址是不够的。这类应用程序要求存在 DNS 查询。
解决这个问题的通常方法是编辑你在其上运行服务的宿主机的 /etc/hosts 文件。但这并不总是可能的。例如,你可能没有权限编辑该文件。这也不总是实用的。你可能需要维护太多的宿主机,或者其他的定制 DNS 查询缓存可能会造成干扰。
在这些情况下,有一个使用“真实”DNS 服务器的解决方案。
问题
你需要一个可解析的 DNS URL 来指定一个 IP 地址。
解决方案
使用 NIP.IO 网络服务,无需任何 DNS 设置,将 IP 地址解析为可解析的 DNS URL。
这很简单。NIP.IO 是一个基于网络的在线服务,可以自动将 IP 地址转换为 URL。你只需将 URL 中的“IP”部分“IP.nip.io”替换为你想要的 IP 地址即可。
假设你想要解析的 URL 指向的 IP 地址是“10.0.0.1”。你的 URL 可能看起来像这样,
其中myappname指的是你为应用程序选择的名称,10.0.0.1指的是你希望 URL 解析到的 IP 地址,而nip.io是互联网上管理此 DNS 查找服务的“真实”域名。
myappname.部分是可选的,因此这个 URL 解析到的 IP 地址相同:
讨论
这种技术在各种环境中都很有用,而不仅仅是使用基于 Docker 的服务。
显然,这种技术不适合生产或正式的 UAT 环境,因为它向第三方提交 DNS 请求,并揭示了有关你内部 IP 地址布局的信息。但它在开发工作中可以是一个非常实用的工具。
如果你使用 HTTPS 服务,请确保你使用的 URL(或一个合适的通配符)已经嵌入到你所使用的证书中。
5.2. 卷——一个持续的问题
容器是一个强大的概念,但有时你想要访问的东西并不总是准备好被封装。你可能有一个存储在大型集群上的引用 Oracle 数据库,你想要连接到它进行测试。或者,你可能有一个已经设置好的大型遗留服务器,上面有无法轻易复制的二进制文件。
当你开始使用 Docker 时,你想要访问的大部分内容可能都是容器外部的数据和程序。我们将带你从从主机简单挂载文件到更复杂的容器模式:数据容器和开发工具容器。我们还将展示我们偏爱的远程网络挂载方法,这只需要 SSH 连接即可工作,我们还将探讨通过 BitTorrent 协议与其他用户共享数据的方法。
卷是 Docker 的核心部分,外部数据引用的问题也是 Docker 生态系统快速变化的另一个领域。
Docker 卷:持久性问题
容器的强大之处很大程度上源于它们封装了环境文件系统的大部分状态,这对于有用。
有时,你可能不想将文件放入容器中。你可能有一些大文件想要在容器之间共享或单独管理。一个经典的例子是你想要容器访问的大型集中式数据库,但你也不想其他(可能更传统的)客户端与你的新式容器一起访问。
解决方案是卷,这是 Docker 管理容器生命周期之外文件的一种机制。尽管这与容器“部署在任何地方”的哲学相悖(例如,你无法在没有兼容数据库可供挂载的地方部署依赖于数据库的容器),但在现实世界的 Docker 使用中,这是一个有用的功能。
问题
你想在容器内访问主机上的文件。
解决方案
使用 Docker 的卷标志从容器内访问主机文件。图 5.3 说明了使用卷标志与主机文件系统交互。
图 5.3. 容器内的卷

以下命令显示了主机上的 /var/db/tables 目录被挂载到 /var/data1,并且可以运行以启动容器,如图 5.3 所示。
$ docker run -v /var/db/tables:/var/data1 -it debian bash
-v 标志(长格式为 --volume)表示需要外部容器卷。随后的参数以冒号分隔的两个目录的形式给出卷规范,指示 Docker 将外部 /var/db/tables 目录映射到容器的 /var/data1 目录。如果这两个目录不存在,它们将被创建。
警惕覆盖现有目录。即使目录已经在镜像中存在,容器的目录也会被映射。这意味着你映射到容器内部的目录将实际上消失。如果你尝试映射一个关键目录,会发生一些有趣的事情!例如,尝试将空目录挂载到 /bin 上。
还要注意,在 Dockerfile 中,假设卷不会持久化。如果你在 Dockerfile 中添加了一个卷,然后对该文件夹进行了更改,这些更改不会持久化到生成的镜像中。
警告
如果你的主机运行 SELinux,你可能会遇到困难。如果强制执行 SELinux 策略,容器可能无法写入 /var/db/tables 目录。你会看到一个“权限被拒绝”的错误。如果你需要解决这个问题,你将不得不与你的系统管理员(如果你有的话)交谈,或者关闭 SELinux(仅限开发目的)。有关 SELinux 的更多信息,请参阅技术 113。
讨论
从主机在容器中公开文件是我们进行单个容器实验时最常执行的操作之一——容器旨在是短暂的,在某个文件上花费大量时间工作后,很容易将其删除。最好确保文件安全,无论发生什么情况。
此外,还有优势,那就是使用技术 114 中的方法将文件复制到容器中的正常开销根本不存在。像技术 77 中的数据库这样的数据库,如果它们变得很大,将是明显的受益者。
最后,您将看到许多使用 -v /var/run/docker.sock:/var/run/docker.sock 的技术,其中之一就是技术 45。这会将特殊的 Unix 套接字文件暴露给容器,并展示了这项技术的重要功能——您不仅限于所谓的“常规”文件——您还可以允许更多基于文件系统的非典型用例。但如果您遇到设备节点(例如)的权限问题,您可能需要参考技术 93 来了解 --privileged 标志的作用。
| |
使用 Resilio 同步的分布式卷
当在团队中实验 Docker 时,您可能希望团队成员之间共享大量数据,但您可能没有足够的资源来分配一个具有足够容量的共享服务器。这种懒惰的解决方案是在需要时从其他团队成员那里复制最新的文件——对于较大的团队来说,这会迅速失控。
解决方案是使用用于文件共享的去中心化工具——无需专用资源。
问题
您想在互联网上跨主机共享卷。
解决方案
使用名为 Resilio 的技术通过互联网共享卷。
图 5.4 展示了您所追求的设置。
图 5.4. 使用 Resilio

最终结果是卷(/data)通过互联网方便地同步,无需任何复杂的设置。
在您的主服务器上,运行以下命令以在第一台主机上设置容器:
[host1]$ docker run -d -p 8888:8888 -p 55555:55555 \ *1*
--name resilio ctlc/btsync
$ docker logs resilio *2*
Starting btsync with secret: \
ALSVEUABQQ5ILRS2OQJKAOKCU5SIIP6A3
*3*
By using this application, you agree to our Privacy Policy and Terms.
http://www.bittorrent.com/legal/privacy
http://www.bittorrent.com/legal/terms-of-use
total physical memory 536870912 max disk cache 2097152
Using IP address 172.17.4.121
[host1]$ docker run -i -t --volumes-from resilio \ *4*
ubuntu /bin/bash
$ touch /data/shared_from_server_one *5*
$ ls /data
shared_from_server_one
-
1 以守护进程容器运行已发布的 ctlc/btsync 图像,调用 btsync 二进制文件,并打开所需的端口
-
2 获取 resilio 容器的输出,以便您可以记录下密钥
-
3 记录此密钥——它将因您的运行而不同
-
4 启动一个带有 Resilio 服务器卷的交互式容器
-
5 将文件添加到 /data 卷
在第二台服务器上打开终端并运行以下命令以同步卷:
[host2]$ docker run -d --name resilio-client -p 8888:8888 \
-p 55555:55555 \
ctlc/btsync ALSVEUABQQ5ILRS2OQJKAOKCU5SIIP6A3 *1*
[host2]$ docker run -i -t --volumes-from resilio-client \
ubuntu bash
*2*
$ ls /data
shared_from_server_one
*3*
$ touch /data/shared_from_server_two *4*
$ ls /data
shared_from_server_one shared_from_server_two
-
1 以守护进程模式启动一个由 host1 上运行的守护进程生成的密钥的 resilio 客户端容器
-
2 启动一个交互式容器,挂载来自您的客户端守护进程的卷
-
3 在 host1 上创建的文件已传输到 host2。
-
4 在 host2 上创建第二个文件
在 host1 的运行容器上,您应该看到文件已经在主机之间同步,就像第一个文件一样:
[host1]$ ls /data
shared_from_server_one shared_from_server_two
讨论
文件的同步没有时间保证,因此您可能需要等待数据同步。对于较大的文件来说,这种情况尤为明显。
警告
由于数据可能通过互联网发送,并且由你无法控制的协议处理,如果你有任何有意义的关于安全性、可扩展性或性能的约束,你不应该依赖这种技术。
我们已经演示了这种技术在两个容器之间是有效的,正如开头提到的,但它也应该适用于团队中的许多成员。除了明显的大文件不适合版本控制的使用场景之外,还包括备份,甚至可能是 Docker 镜像本身,尤其是如果这种技术与像技术 72 中展示的高效压缩机制结合使用时。为了避免冲突,确保镜像总是单向流动(例如,从构建机器到多个服务器),或者遵循一个协议来执行更新。
| |
保留你的容器 bash 历史
在容器内进行实验,知道你完成时可以清除一切,可以是一种解放的经历。但当你这样做时,你会失去一些便利。我们多次遇到的一个问题是忘记在容器内运行的一系列命令。
问题
你希望将你的容器 bash 历史与主机的 bash 历史共享。
解决方案
使用-e标志、Docker 挂载和 bash 别名可以自动将容器的 bash 历史与主机的 bash 历史共享。
为了理解这个问题,我们将展示一个简单的场景,其中丢失这个历史记录是非常令人烦恼的。
想象你在 Docker 容器中进行实验,在工作的过程中你做了一些有趣且可重用的操作。我们将使用一个简单的echo命令作为这个例子,但这也可能是一个由多个程序组成的复杂拼接,最终产生有用的输出:
$ docker run -ti --rm ubuntu /bin/bash
$ echo my amazing command
$ exit
经过一段时间后,你想回忆起你之前运行的令人难以置信的echo命令。不幸的是,你记不起来了,而且你也不再在屏幕上看到终端会话以便滚动查看。出于习惯,你尝试在主机的 bash 历史中查找:
$ history | grep amazing
没有任何东西返回,因为 bash 历史被保存在现在已移除的容器中,而不是你返回的主机中。
要将你的 bash 历史与主机共享,你可以在运行 Docker 镜像时使用卷挂载。以下是一个示例:
$ docker run -e HIST_FILE=/root/.bash_history \ *1*
-v=$HOME/.bash_history:/root/.bash_history \ *2*
-ti ubuntu /bin/bash
-
1 设置 bash 获取的环境变量。这确保了你挂载的 bash 历史文件是你想要的。
-
2 将容器的根 bash 历史文件映射到主机上
提示
你可能希望将容器的 bash 历史与主机的 bash 历史分开。一种方法是通过更改前面-v参数的第一部分值来实现。
每次都要输入这些内容确实很麻烦,所以为了使其更用户友好,你可以在你的~/.bashrc文件中设置一个别名:
$ alias dockbash='docker run -e HIST_FILE=/root/.bash_history \
-v=$HOME/.bash_history:/root/.bash_history
这仍然不是无缝的,因为如果您想执行docker run命令,您必须记得输入dockbash。为了获得更无缝的体验,您可以将这些添加到您的~/.bashrc 文件中:
列表 5.6. 自动挂载主机 bash 历史记录的函数别名
function basher() { *1*
if [[ $1 = 'run' ]] *2*
then
shift *3*
/usr/bin/docker run \ *4*
-e HIST_FILE=/root/.bash_history \
-v $HOME/.bash_history:/root/.bash_history "$@" *5*
else
/usr/bin/docker "$@" *6*
fi
}
alias docker=basher *7*
-
1 *创建一个名为 basher 的 bash 函数,该函数将处理 docker 命令*
-
2 确定 basher/docker 的第一个参数是否为“run”
-
3 从您传递的参数列表中删除该参数
-
4 运行您之前运行的 docker run 命令,调用 Docker 运行时的绝对路径以避免与以下 docker 别名混淆。绝对路径是通过在实施此解决方案之前在主机上运行“which docker”命令来发现的。
-
5 将“run”之后的参数传递给 Docker 运行时
-
6 使用原始参数运行 docker 命令
-
7 当在命令行上调用 docker 命令时,将 docker 命令别名设置为已创建的 basher 函数。这确保在 bash 在路径上找到 docker 二进制文件之前,docker 的调用被捕获。
讨论
现在,当您下次打开 bash shell 并运行任何docker run命令时,该容器内运行的命令将被添加到您的主机 bash 历史记录中。请确保 Docker 的路径正确。例如,它可能位于/bin/docker。
注意
您需要从主机的原始 bash 会话中注销,以便更新历史文件。这是由于 bash 及其如何更新内存中保留的 bash 历史的一个细微差别。如果有疑问,退出您所知道的全部 bash 会话,然后重新启动一个以确保您的历史记录尽可能最新。
许多带有提示的命令行工具也会存储历史记录,SQLite 就是一个例子(将历史记录存储在.sqlite_history 文件中)。如果您不想使用 Docker 中描述的集成日志解决方案技术 102,您可以使用类似的实践使您的应用程序写入一个最终位于容器外的文件。请注意,日志的复杂性,如日志轮转,意味着可能更简单的是使用日志目录卷而不是仅仅一个文件。
| |
数据容器
如果您在主机上大量使用卷,管理容器的启动可能会变得复杂。您可能还希望数据仅由 Docker 管理,而不是在主机上普遍可访问。更干净地管理这些事情的一种方法是用数据仅容器设计模式。
问题
您想在容器内使用外部卷,但只想让 Docker 访问文件。
解决方案
启动一个数据容器,并在运行其他容器时使用--volumes-from标志。
图 5.5 展示了数据容器模式的结构,并解释了它是如何工作的。需要注意的是,在第二个主机上,容器不需要知道数据在磁盘上的位置。它们只需要知道数据容器的名称,然后就可以正常工作了。这可以使容器的操作更加便携。
图 5.5. 数据容器模式

与直接映射主机目录相比,这种方法的一个好处是,对这些文件的访问由 Docker 管理,这意味着非 Docker 进程不太可能影响其内容。
注意
人们通常对是否需要运行仅数据容器感到困惑。它不需要!它只需要存在,已经在主机上运行过,并且没有被删除。
让我们通过一个简单的例子来了解如何使用这项技术。
首先,您运行您的数据容器:
$ docker run -v /shared-data --name dc busybox \
touch /shared-data/somefile
-v 参数不会将卷映射到主机目录,因此它会在该容器的责任范围内创建目录。这个目录通过 touch 命令添加一个文件,容器立即存在——数据容器不需要运行就可以被使用。我们使用了小巧但功能齐全的 busybox 镜像来减少数据容器需要的额外负担。
然后您运行另一个容器来访问您刚刚创建的文件:
docker run -t -i --volumes-from dc busybox /bin/sh
/ # ls /shared-data
somefile
讨论
--volumes-from 标志允许您通过在当前容器中挂载它们来引用数据容器的文件——您只需要传递一个定义了卷的容器的 ID。busybox 镜像没有 bash,因此您需要启动一个更简单的 shell 来验证 dc 容器中的 /shared-data 文件夹对您是否可用。
您可以启动任意数量的容器,所有这些容器都从指定的数据容器的卷中读取和写入。
您不需要使用这种模式来使用卷——您可能会发现这种方法比直接挂载主机目录更难管理。然而,如果您喜欢将管理数据的责任干净利落地委托给 Docker 内部管理的单个点,不受其他主机进程的干扰,那么数据容器可能对您很有用。
警告
如果您的应用程序从多个容器向同一个数据容器记录日志,确保每个容器的日志文件都写入唯一的文件路径是很重要的。如果不这样做,不同的容器可能会覆盖或截断文件,导致数据丢失,或者它们可能会写入交错的数据,这更难分析。同样,如果您从数据容器中调用 --volumes-from,您允许该容器可能覆盖您的目录,因此在这里要小心名称冲突。
重要的是要理解这种模式可能会导致大量磁盘使用,这可能相对难以调试。因为 Docker 在数据容器内管理卷,并且当最后一个引用它的容器退出时不会删除卷,所以卷上的任何数据都将持续存在。这是为了防止数据丢失。有关管理方面的建议,请参阅技术 43。
使用 SSHFS 进行远程卷挂载
我们已经讨论了挂载本地文件,但很快就会提出如何挂载远程文件系统的问题。例如,您可能希望将远程服务器上的参考数据库共享并作为本地处理。
虽然在理论上您可以在主机系统和服务器上设置 NFS,并通过挂载该目录来访问文件系统,但对于大多数用户来说,这有一个更快、更简单的方法,不需要在服务器端进行设置(只要存在 SSH 访问)。
注意
您需要 root 权限才能使此技术生效,并且您需要安装 FUSE(Linux 的“用户空间文件系统”内核模块)。您可以通过在终端中运行ls /dev/fuse来查看该文件是否存在,以确定您是否安装了后者。
问题
您希望挂载远程文件系统而不需要任何服务器端配置。
解决方案
使用 SSHFS(SSH 文件系统)技术挂载远程文件系统,使其看起来像是您机器上的本地文件系统。
此技术通过使用 SSH 的 FUSE 内核模块提供对文件系统的标准接口,同时在后台通过 SSH 进行所有通信。SSHFS 还提供各种幕后功能(如远程文件预读),以促进文件本地化的错觉。结果是,一旦用户登录到远程服务器,他们就会看到文件,就像它们是本地的一样。图 5.6 有助于解释这一点。
图 5.6. 使用 SSHFS 挂载远程文件系统

警告
虽然此技术不使用 Docker 卷功能,并且文件通过文件系统可见,但这并不提供任何容器级别的持久性。任何更改都仅发生在远程服务器的文件系统中。
您可以通过运行以下命令开始,根据您的环境进行调整。
第一步是在您的宿主机上启动一个带有--privileged的容器:
$ docker run -t -i --privileged debian /bin/bash
然后,当它启动时,在容器内部运行apt-get update && apt-get install sshfs来安装 SSHFS。
当 SSHFS 成功安装后,按照以下方式登录到远程主机:
$ LOCALPATH=/path/to/local/directory *1*
$ mkdir $LOCALPATH *2*
$ sshfs user@host:/path/to/remote/directory $LOCALPATH *3*
-
1 选择一个目录来挂载远程位置
-
2 创建挂载的本地目录
-
3 在此处替换您的远程主机用户名、远程主机地址和远程路径
现在,您将在您刚刚创建的文件夹中看到远程服务器路径的内容。
小贴士
最简单的方法是将挂载到新创建的目录上,但如果您使用-o nonempty选项,也可以挂载一个已存在的目录,其中已经存在文件。有关更多信息,请参阅 SSHFS 手册页。
要干净地卸载文件,请使用以下fusermount命令,根据需要替换路径:
fusermount -u /path/to/local/directory
讨论
这是一种快速从容器(和在标准 Linux 机器上)内实现远程挂载的绝佳方法,只需付出最小的努力。
尽管我们只在这个技术中讨论了 SSHFS,但成功管理这一点将打开 Docker 内部 FUSE 文件系统的美妙(有时也奇怪)世界。从在 Gmail 中存储您的数据到跨多台机器存储 PB 级数据的分布式 GlusterFS 文件系统,为您打开了众多机会。
| |
通过 NFS 共享数据
在一个大公司中,NFS 共享目录可能已经在使用中——NFS 是中央位置提供文件的一个经过充分验证的选项。为了使 Docker 获得影响力,通常非常重要能够访问这些共享文件。
Docker 默认不支持 NFS,在每一个容器上安装 NFS 客户端以便挂载远程文件夹并不是最佳实践。相反,建议的方法是让一个容器充当从 NFS 到更符合 Docker 概念的翻译器:卷。
问题
您希望无缝访问通过 NFS 的远程文件系统。
解决方案
使用基础设施数据容器来代理对您的远程 NFS 文件系统的访问。
这种技术建立在技术 37 的基础上,我们在那里创建了一个数据容器来管理运行系统中的数据。
图 5.7 展示了这一技术的抽象概念。NFS 服务器将内部目录作为/export 文件夹暴露出来,该文件夹绑定在主机上。然后 Docker 主机使用 NFS 协议将此文件夹挂载到其/mnt 文件夹上。然后创建了一个所谓的“基础设施容器”,它绑定挂载文件夹。
图 5.7. 作为 NFS 访问经纪人的基础设施容器

乍一看,这可能会显得有些过度设计,但好处是它为 Docker 容器提供了一定程度的间接性:它们只需要挂载来自预先约定的基础设施容器的卷,而负责基础设施的人可以关心内部管道、可用性、网络等问题。
对 NFS 的全面介绍超出了本书的范围。在这个技术中,我们将通过在同一个主机上拥有 NFS 服务器组件,通过在 Docker 容器上设置这样的共享来介绍这一步骤。这已经在 Ubuntu 14.04 上进行了测试。
假设您想共享主机上的/opt/test/db 文件夹的内容,该文件夹包含文件 mybigdb.db。
以 root 身份安装 NFS 服务器并创建一个具有开放权限的导出目录:
# apt-get install nfs-kernel-server
# mkdir /export
# chmod 777 /export
注意
我们已使用开放权限创建了 NFS 共享,这对于生产系统来说并不是一个安全的方法。我们采取这种方法是为了简化本教程。NFS 安全性是一个复杂且多样化的主题,超出了本书的范围。有关 Docker 和安全的更多信息,请参阅第十四章 chapter 14。
现在将 db 目录绑定到您的导出目录:
# mount --bind /opt/test/db /export
您现在应该能够看到 /opt/test/db 目录的内容在 /export 中:
提示
如果您希望重启后此设置持续有效,请将此行添加到您的 /etc/fstab 文件中:/opt/test/db /export none bind 0 0
现在将此行添加到您的 /etc/exports 文件中:
/export 127.0.0.1(ro,fsid=0,insecure,no_subtree_check,async)
对于这个概念验证示例,我们在 127.0.0.1 上进行本地挂载,这在一定程度上抵消了效果。在现实世界的场景中,您会将其限制为某个 IP 地址类,例如 192.168.1.0/24。如果您喜欢玩火,可以用 * 代替 127.0.0.1 来向世界开放。为了安全起见,我们在这里以只读(ro)方式挂载,但您可以通过将 ro 替换为 rw 来挂载为读写。请记住,如果您这样做,您需要在 async 标志之后添加一个 no_root_squash 标志,但在走出这个沙盒之前请考虑安全问题。
通过 NFS 将目录挂载到 /mnt 目录,导出您之前在 /etc/exports 中指定的文件系统,然后重新启动 NFS 服务以应用更改:
# mount -t nfs 127.0.0.1:/export /mnt
# exportfs -a
# service nfs-kernel-server restart
现在您已经准备好运行您的基础设施容器:
# docker run -ti --name nfs_client --privileged
-v /mnt:/mnt busybox /bin/true
现在您可以在没有权限或对底层实现了解的情况下运行您想要访问的目录:
# docker run -ti --volumes-from nfs_client debian /bin/bash
root@079d70f79d84:/# ls /mnt
myb
root@079d70f79d84:/# cd /mnt
root@079d70f79d84:/mnt# touch asd
touch: cannot touch `asd': Read-only file system
讨论
这种在多个容器中集中挂载共享资源并提供特权的模式非常强大,可以使开发工作流程变得更加简单。
提示
如果您要管理很多这样的容器,您可以通过为容器指定命名约定(例如 --name nfs_client_opt_database_live)来简化管理,该容器公开了 /opt/database/live 路径。
| |
提示
请记住,这种技术只通过隐蔽性提供安全性(这根本不是安全性)。正如您稍后将会看到的,任何可以运行 Docker 可执行文件的人实际上在主机上都有 root 权限。
用于代理访问和抽象细节的基础设施容器在某种程度上等同于网络中的服务发现工具——服务的运行细节或其所在位置的具体细节并不重要。您只需要知道它的名字。
事实上,您之前已经在技术 35 中看到过--volumes-from的使用。细节略有不同,因为访问是通过容器内运行的而不是在主机上运行的基础设施进行协商的,但使用名称引用可用卷的原则仍然适用。您甚至可以用这个技术中的容器替换掉那个容器,如果配置正确,应用程序不会注意到它们检索文件的位置有任何变化。
| |
开发工具容器
如果您是一位经常在其他机器上遇到困难,没有您在独特如雪花般的开发环境中拥有的程序或配置的工程师,这项技术可能适合您。同样,如果您想与他人分享您升级后的开发环境,Docker 可以使其变得简单。
问题
您想在其他机器上访问您的开发环境。
解决方案
创建一个包含您设置的 Docker 镜像,并将其放置在注册表中。
作为演示,我们将使用我们的一个开发工具镜像。您可以通过运行docker pull dockerinpractice/docker-dev-tools-image来下载它。如果您想检查 Dockerfile,该仓库可在github.com/docker-in-practice/docker-dev-tools-image找到。
启动容器很简单——一个简单的docker run -t -i docker-inpractice/docker-dev-tools-image将为您提供一个我们开发环境中的 shell。您可以浏览我们的 dotfiles,也许可以给我们一些关于设置的反馈。
当与其他技术结合使用时,这种技术的真正威力才会显现。在下面的列表中,您可以看到一个用于在主机网络上显示 GUI 和挂载主机代码的 dev 工具容器。
列表 5.7. 运行带 GUI 的 dev-tools 镜像
docker run -t -i \
-v /var/run/docker.sock:/var/run/docker.sock \ *1*
-v /tmp/.X11-unix:/tmp/.X11-unix \ *2*
-e DISPLAY=$DISPLAY \ *3*
--net=host --ipc=host \ *4*
-v /opt/workspace:/home/dockerinpractice \ *5*
dockerinpractice/docker-dev-tools-image
-
1 将 Docker 套接字挂载到主机,以便访问主机的 Docker 守护进程
-
2 挂载 X 服务器 Unix 域套接字,以便您可以启动基于 GUI 的应用程序(见技术 29)
-
3 设置环境变量,指示容器使用主机显示
-
4 这些参数绕过容器网络桥接,允许您访问主机的进程间通信文件(见技术 109)。
-
5 将工作区域挂载到容器的家目录
之前的命令为您提供了一个可以访问主机资源的环境:
-
网络
-
Docker 守护进程(在主机上运行正常 Docker 命令)
-
进程间通信(IPC)文件
-
X 服务器启动基于 GUI 的应用程序,如果需要
注意
如挂载主机目录时通常所做的那样,请务必小心不要挂载任何关键目录,因为您可能会造成损害。通常最好避免在根目录下挂载任何主机目录。
讨论
我们提到你可以访问 X 服务器,因此值得查看技术 29 以提醒一些可能性。
对于一些更侵入式的开发工具,可能用于检查主机上的进程,你可能需要查看技术 109 以了解如何授予查看系统某些(默认情况下)受限部分的权限。技术 93 也是一篇重要的阅读材料——仅仅因为容器可以看到你系统的一部分,并不意味着它有权限修改它们。
摘要
-
如果你需要从容器内部访问外部数据,你应该使用卷。
-
SSHFS 是一种简单的方法,无需额外设置即可访问其他机器上的数据。
-
在 Docker 中运行 GUI 应用程序只需要对镜像进行少量准备。
-
你可以使用数据容器来抽象化你的数据位置。
第六章. 每日 Docker
本章涵盖
-
监控你的容器和卷空间使用情况
-
从容器中分离而不停止它们
-
在图表中可视化你的 Docker 镜像谱系
-
从主机直接在容器上运行命令
就像任何中等复杂度的软件项目一样,Docker 有很多角落和缝隙,如果你想要尽可能保持体验的流畅,那么了解这些角落和缝隙是很重要的。
本章的技术将向你展示其中一些更重要的事项,以及介绍一些第三方构建的工具来解决他们自己的问题。把它想象成你的 Docker 工具箱。
6.1. 保持整洁
如果你和我们一样(如果你认真地遵循这本书),你日益增长的 Docker 依赖性意味着你将在选择的主机上启动多个容器,并下载各种镜像到你的主机上。
随着时间的推移,Docker 将占用越来越多的资源,需要对容器和卷进行一些维护。我们将展示如何以及为什么需要这样做。我们还将介绍一些可视化工具,以保持你的 Docker 环境干净整洁,以防你想要从命令行中解脱出来。
运行容器固然很好,但你很快就会发现自己想要做的不仅仅是启动一个前台的单个命令。我们将探讨在不终止容器的情况下退出运行中的容器,以及在运行中的容器内执行命令。
无需 sudo 运行 Docker
Docker 守护进程以 root 用户身份在机器的背景中运行,赋予它相当大的权限,它将这些权限暴露给用户。需要使用sudo是这一结果,但它可能不方便,并使得一些第三方 Docker 工具无法使用。
问题
你希望能够在不使用sudo的情况下运行docker命令。
解决方案
官方解决方案是将自己添加到docker组。
Docker 通过用户组管理围绕 Docker Unix 域套接字的权限。出于安全原因,发行版默认不会让您成为该组的成员,因为这实际上授予了系统完全的 root 访问权限。
通过将自己添加到该组,您将能够以自己的身份使用docker命令:
$ sudo addgroup -a username docker
重新启动 Docker 并完全注销并重新登录,或者如果更容易的话,重新启动您的机器。现在您不需要记住输入sudo或设置别名以以自己的身份运行 Docker。
讨论
这是在本书后面部分使用的许多工具中的一项极其重要的技术。一般来说,任何想要与 Docker 通信(而不在容器中启动)的东西都需要访问 Docker 套接字,这需要sudo或本技术中描述的设置。Docker Compose,在技术 76 中介绍,是 Docker Inc.的官方工具,是此类工具的例子。
维护容器
新的 Docker 用户经常抱怨,在短时间内,您可能会在系统中拥有许多处于各种状态的容器,而在命令行上没有标准工具来管理这些容器。
问题
您想清理系统上的容器。
解决方案
设置别名以运行清理旧容器的命令。
这里最简单的方法是删除所有容器。显然,这是一个有点像核选项的操作,只有在您确定这是您想要的时才应该使用。
以下命令将删除您主机上的所有容器。
$ docker ps -a -q | \ *1*
xargs --no-run-if-empty docker rm -f *2*
-
1 获取所有容器 ID 的列表,包括正在运行的和已停止的,并将它们传递给...
-
2 ...
docker rm -f命令,它将删除传递给它的任何容器,即使它们正在运行。
简单解释一下xargs,它将输入的每一行都作为参数传递给后续命令。我们在这里添加了一个额外的参数--no-run-if-empty,如果前一个命令没有输出,则避免运行该命令,以避免错误。
如果您正在运行可能希望保留的容器,但想删除所有已退出的容器,您可以过滤docker ps命令返回的项目:
docker ps -a -q --filter status=exited | \ *1*
xargs --no-run-if-empty docker rm *2*
-
1
--filter标志告诉docker ps命令您想要返回哪些容器。在这种情况下,您正在限制它只返回已退出的容器。其他选项是正在运行和重新启动。 -
2 这次您不需要强制删除容器,因为根据您提供的过滤器,它们不应该在运行。
事实上,删除所有已停止的容器是一个如此常见的用例,以至于 Docker 添加了一个专门为此用例的命令:docker container prune。然而,此命令仅限于该用例,并且您需要参考本技术中的命令来进行任何更复杂的容器操作。
作为更高级用例的示例,以下命令将列出所有退出代码非零的容器。如果您系统中有许多容器,并且您想自动化检查和删除任何意外退出的容器,您可能需要这样做:
comm -3 \ *1*
<(docker ps -a -q --filter=status=exited | sort) \ *2*
<(docker ps -a -q --filter=exited=0 | sort) | \ *3*
xargs --no-run-if-empty docker inspect > error_containers *4*
-
1 运行 comm 命令以比较两个文件的内容。-3 参数抑制同时出现在两个文件中的行(在本例中,那些退出代码为零的行)并输出其他行。
-
2 查找已退出的容器 ID,对它们进行排序,并将它们作为文件传递给 comm
-
3 查找退出代码为 0 的容器,对它们进行排序,并将它们作为文件传递给 comm
-
4 对退出代码非零的容器运行 docker inspect(通过 comm 管道传入)并将输出保存到 error_containers 文件
提示
如果您之前没有见过,<(command) 语法称为 进程替换。它允许您将命令的输出作为文件处理,并将其传递给另一个命令,这在无法使用管道输出时非常有用。
上述示例相当复杂,但它展示了通过组合不同的实用程序可以获得的强大功能。它输出所有已停止的容器 ID,然后仅选择那些退出代码非零的容器(那些以意外方式退出的容器)。如果您觉得难以理解,可以先单独运行每个命令,并首先以这种方式理解它们,这有助于学习构建块。
这样的命令对于收集生产环境中的容器信息可能很有用。您可能需要对其进行修改,以便运行 cron 来清除以预期方式退出的容器。
将这些单行命令作为命令提供
您可以将命令作为别名添加,以便在登录到您的宿主机时更容易运行。为此,请将以下类似行添加到您的 ~/.bashrc 文件末尾:
alias dockernuke='docker ps -a -q | \
xargs --no-run-if-empty docker rm -f'
您下次登录时,可以从命令行运行 dockernuke 来删除您系统上找到的任何 Docker 容器。
我们发现这节省了惊人的时间。但请注意!以这种方式删除生产容器很容易,正如我们所证明的那样。即使您足够小心,不会删除正在运行的容器,您仍然可能会删除非运行但仍然有用的数据容器。
讨论
本书中的许多技术最终都会创建容器,尤其是在介绍 Docker Compose 的 技术 76 以及在有关编排的章节中——毕竟,编排就是管理多个容器。您可能会发现这里讨论的命令对于清理您的机器(本地或远程)以在每个技术完成后获得一个全新的开始很有用。
| |
维护卷
尽管卷是 Docker 的一个强大功能,但它们也伴随着显著的运营劣势。因为卷可以在不同的容器之间共享,所以当挂载它们的容器被删除时,卷不能被删除。想象一下图 6.1 中描述的场景。
图 6.1. 当容器被移除时,/var/db 会发生什么?

“很简单!”你可能会想,“当最后一个引用容器的容器被删除时,删除卷!”确实,Docker 可以选择这个选项,当垃圾回收编程语言从内存中删除对象时,这种方法就是:当没有其他对象引用它时,它可以被删除。
但 Docker 认为这可能会让人们不小心丢失有价值的数据,因此更喜欢让用户决定是否在删除容器时删除卷。不幸的是,这的一个副作用是,默认情况下,卷会保留在你的 Docker 守护进程的主机磁盘上,直到它们被手动删除。如果这些卷充满了数据,你的磁盘可能会被填满,因此了解管理这些孤儿卷的方法是有用的。
问题
你使用太多的磁盘空间,因为存在孤儿 Docker 挂载在你的主机上。
解决方案
在调用docker rm时使用-v标志,或者如果你忘记了,可以使用docker volume子命令来销毁它们。
在图 6.1 描述的场景中,如果你总是使用带有-v标志的docker rm命令,你可以确保删除/var/db。-v标志会在没有其他容器挂载的情况下删除任何关联的卷。幸运的是,Docker 足够智能,能够知道是否有其他容器挂载了该卷,因此不会有任何令人不快的惊喜。
最简单的方法是养成习惯,每次删除容器时都输入-v。这样你就可以保留控制卷是否被删除的权利。但这种方法的问题是你可能并不总是想删除卷。如果你正在向这些卷写入大量数据,你很可能不想丢失这些数据。此外,如果你养成了这样的习惯,它很可能会变得自动化,你只有在为时已晚时才会意识到你已经删除了某些重要的东西。
在这些场景中,你可以使用在经过许多抱怨和第三方解决方案之后添加到 Docker 中的命令:docker volume prune。这将删除任何未使用的卷:
$ docker volume ls *1*
DRIVER VOLUME NAME
local 80a40d34a2322f505d67472f8301c16dc75f4209b231bb08faa8ae48f *2*
36c033f *2*
local b40a19d89fe89f60d30b3324a6ea423796828a1ec5b613693a740b33 *2*
77fd6a7b *2*
local bceef6294fb5b62c9453fcbba4b7100fc4a0c918d11d580f362b09eb *2*
58503014 *2*
$ docker volume prune *3*
WARNING! This will remove all volumes not used by at least one container.
Are you sure you want to continue? [y/N] y *4*
Deleted Volumes:
80a40d34a2322f505d67472f8301c16dc75f4209b231bb08faa8ae48f36c033f *5*
b40a19d89fe89f60d30b3324a6ea423796828a1ec5b613693a740b3377fd6a7b *5*
Total reclaimed space: 230.7MB
-
1 运行命令以列出 Docker 所知的卷
-
2 存在于机器上的卷,无论它们是否在使用
-
3 运行命令以删除未使用的卷
-
4 确认删除卷
-
5 已删除的卷
如果你想要跳过确认提示,可能是因为自动化脚本,你可以将-f传递给docker volume prune以跳过它。
小贴士
如果你想要从不再被任何容器引用的未删除卷中恢复数据,你可以使用 docker volume inspect 来发现卷所在的目录(可能位于 /var/lib/docker/volumes/ 之下)。然后你可以以 root 用户浏览它。
讨论
删除卷可能不是你经常需要做的事情,因为容器中的大文件通常是从主机机器挂载的,并且不会存储在 Docker 数据目录中。但每周清理一次是值得的,以避免它们堆积,尤其是如果你在使用来自技术 37 的数据容器。
| |
不停止容器就分离容器
当使用 Docker 时,你经常会发现自己处于一个有交互式 shell 的位置,但退出 shell 会终止容器,因为它是容器的主要进程。幸运的是,有一种方法可以从容器中分离出来(如果你想要的话,可以使用 docker attach 再次连接到容器)。
问题
你想要在不停止容器的情况下从容器交互中分离出来。
解决方案
使用 Docker 内置的快捷键组合从容器中退出。
Docker 实用地实现了一个不太可能被任何其他应用程序需要的键序列,并且也不太可能意外按下。
假设你使用 docker run -t -i -p 9005:80 ubuntu /bin/bash 启动了一个容器,然后使用 apt-get 安装了一个 Nginx 网络服务器。你想要通过一个快速的 curl 命令来测试它是否可以从你的主机访问 localhost:9005。
按下 Ctrl-P 然后按下 Ctrl-Q。请注意,不是一次性按下这三个键。
注意
如果你使用 --rm 和分离运行,容器在终止时仍然会被删除,无论是命令完成还是你手动停止它。
讨论
如果你已经启动了一个容器,但可能忘记在后台启动它,这种技术很有用,就像在技术 2 中展示的那样。它还允许你在需要检查容器状态或提供一些输入时,自由地连接和断开容器。
| |
使用 Portainer 管理你的 Docker 守护进程
当演示 Docker 时,很难演示容器和镜像之间的区别——终端上的线条不是可视的。此外,如果你想要从许多容器中杀死和删除特定的容器,Docker 的命令行工具可能不太友好。这个问题通过创建一个用于管理主机上镜像和容器的点击工具得到了解决。
问题
你想在主机上管理容器和镜像,而不使用 CLI。
解决方案
使用 Portainer,这是 Docker 的核心贡献者之一创建的工具。
Portainer 最初是 DockerUI,您可以在github.com/portainer/portainer上阅读关于它的内容并找到源代码。因为没有先决条件,您可以直接跳到运行它:
$ docker run -d -p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
portainer/portainer -H unix:///var/run/docker.sock
这将在后台启动 Portainer 容器。如果您现在访问 http://localhost:9000,您将看到仪表板,它为您提供了计算机上 Docker 的快速信息。
容器管理功能可能是这里最有用的功能之一——转到容器页面,您将看到正在运行的容器列表(包括 Portainer 容器),并有一个显示所有容器的选项。从这里,您可以执行容器的批量操作(例如终止它们)或点击容器名称以深入了解容器并执行与该容器相关的单个操作。例如,您将看到移除正在运行的容器的选项。
图片页面看起来与容器页面相当相似,也允许您选择多个图片并对它们执行批量操作。点击图片 ID 会提供一些有趣的选择,例如从图片创建容器和标记图片。
请记住,Portainer 可能落后于官方 Docker 功能——如果您想使用最新和最好的功能,您可能被迫求助于命令行。
讨论
Portainer 是许多 Docker 接口之一,也是最受欢迎的之一,具有许多功能和活跃的开发。作为一个例子,您可以使用它来管理远程机器,也许是在它们上启动容器后使用技术 32。
| |
生成 Docker 镜像的依赖关系图
Docker 的文件分层系统是一个极其强大的想法,它可以节省空间并使构建软件更快。但是,一旦您开始使用大量图片,理解您的图片之间的关系可能会变得困难。docker images -a命令将返回系统上所有层的列表,但这并不是一个用户友好的方式来理解这些关系——通过使用 Graphviz 创建它们的树状图来可视化这些关系要容易得多。
这也是 Docker 使复杂任务变得简单的力量的一个展示。在主机机器上安装所有组件以生成图片以前可能需要一系列冗长且容易出错的操作,但使用 Docker,它可以变成一个单一的便携式命令,失败的可能性要小得多。
问题
您想可视化存储在主机上的图片树。
解决方案
使用我们创建的具有此功能的图片(基于 CenturyLink Labs 的一个版本)以输出 PNG 或获取网页视图。此图片包含使用 Graphviz 生成 PNG 图像文件的脚本。
这种技术使用 dockerinpractice/docker-image-graph 的 Docker 镜像。这个镜像可能会随着时间的推移而过时,停止工作,因此你可能需要运行以下命令来确保它是最新的。
列表 6.1. 构建最新的 docker-image-graph 镜像(可选)
$ git clone https://github.com/docker-in-practice/docker-image-graph
$ cd docker-image-graph
$ docker build -t dockerinpractice/docker-image-graph
在你的 run 命令中,你只需要挂载 Docker 服务器套接字,然后就可以开始了,正如下一个列表所示。
列表 6.2. 生成你的层树镜像
$ docker run --rm \ *1*
-v /var/run/docker.sock:/var/run/docker.sock \ *2*
dockerinpractice/docker-image-graph > docker_images.png *3*
-
1 在生成镜像时删除容器
-
2 挂载 Docker 服务器的 Unix 域套接字,以便你可以在容器内访问 Docker 服务器。如果你已经更改了 Docker 守护进程的默认设置,这将不起作用。
-
3 指定一个镜像并生成一个 PNG 作为输出
图 6.2 显示了我们机器上的一个镜像树的 PNG 图像。你可以从这张图中看出,节点和 golang:1.3 镜像共享一个共同的根,而 golang:runtime 只与 golang:1.3 镜像共享全局根。同样,mesosphere 镜像是基于与 ubuntu-upstart 镜像相同的根构建的。
图 6.2. 镜像树图

你可能会想知道树上的全局根节点是什么。这是 scratch 伪镜像,大小正好为 0 字节。
讨论
当你开始构建更多的 Docker 镜像时,也许作为第九章(kindle_split_020.xhtml#ch09)中持续交付的一部分,跟踪镜像的历史和它基于什么构建可能会变得令人不知所措。如果你试图通过共享更多层来优化大小以加快交付速度,这尤其重要。定期拉取所有镜像并生成图表可以是一种很好的跟踪方法。
| |
直接操作:在容器上执行命令
在 Docker 的早期阶段,许多用户向他们的镜像添加了 SSH 服务器,以便他们可以从外部使用 shell 访问它们。Docker 对此持批评态度,因为它将容器视为虚拟机(我们知道容器不是虚拟机),并为不需要的系统增加了进程开销。许多人反对说,一旦启动,就没有简单的方法进入容器。因此,Docker 引入了 exec 命令,这是一个更整洁的解决方案,用于在容器启动后影响和检查其内部结构。我们在这里讨论的就是这个命令。
问题
你想在运行的容器上执行命令。
解决方案
使用 docker exec 命令。
以下命令在后台启动一个容器(使用 -d)并告诉它永远睡眠(什么也不做)。我们将这个命令命名为 sleeper。
docker run -d --name sleeper debian sleep infinity
现在你已经启动了一个容器,你可以使用 Docker 的 exec 命令对它执行各种操作。这个命令可以被视为有三个基本模式,如 表 6.1 中列出。
表 6.1. Docker exec 模式
| 模式 | 描述 |
|---|---|
| 基本 | 在容器中同步地在命令行上运行命令 |
| 守护进程 | 在容器后台运行命令 |
| 交互式 | 运行命令并允许用户与之交互 |
首先,我们将介绍基本模式。以下命令在我们的sleeper容器内运行一个echo命令。
$ docker exec sleeper echo "hello host from container"
hello host from container
注意,此命令的结构与docker run命令非常相似,但不同的是,我们给出的是正在运行的容器的 ID,而不是镜像的 ID。echo命令指的是容器内的 echo 二进制,而不是外部。
守护进程模式在后台运行命令;您在终端中看不到输出。这可能对定期维护任务很有用,例如清理日志文件,您只需运行命令然后忘记:
$ docker exec -d sleeper \ *1*
find / -ctime 7 -name '*log' -exec rm {} \; *2*
$ *3*
-
1 -d 标志以守护进程的方式在后台运行命令,类似于 docker run。
-
2 删除过去七天未更改且以“log”结尾的所有文件
-
3 立即返回,无论完成所需时间长短
最后,我们有交互模式。这允许您在容器内运行任何您喜欢的命令。要启用此模式,您通常需要指定 shell 应该以交互式方式运行,在以下代码中是 bash:
$ docker exec -i -t sleeper /bin/bash
root@d46dc042480f:/#
-i和-t参数与您在docker run中熟悉的参数做相同的事情——它们使命令交互式,并设置一个 TTY 设备,以便 shell 可以正确运行。运行此命令后,您将在容器内看到一个提示符正在运行。
讨论
当出现问题时,进入容器是一个基本的调试步骤,或者如果您想了解容器正在做什么。通常不可能使用技术 44 启用的附加和分离方法,因为容器中的进程通常在前台运行,这使得无法访问 shell 提示符。因为exec允许您指定要运行的二进制文件,所以这不是问题...只要容器文件系统实际上有您要运行的二进制文件。
特别是,如果您已经使用技术 58 创建了一个包含单个二进制的容器,您将无法启动一个 shell。在这种情况下,您可能希望坚持使用技术 57 作为低开销的方式来允许exec。
| |
您是否在一个 Docker 容器中?
在创建容器时,通常将逻辑放在 shell 脚本中,而不是直接在 Dockerfile 中编写脚本。或者您可能有一些在容器运行时使用的脚本。无论如何,这些任务通常针对容器内的使用进行了精心定制,并在“正常”机器上运行可能会造成损害。在这种情况下,有一些安全措施来防止意外在容器外执行是有用的。
问题
您的代码需要知道您是否在 Docker 容器中操作。
解决方案
检查/.dockerenv 文件是否存在。如果存在,你很可能在一个 Docker 容器中。
注意,这并不是一个绝对保证——如果有人或任何东西移除了/.dockerenv 文件,这个检查可能会给出误导性的结果。这些情况不太可能发生,但最坏的情况是你会得到一个假阳性,没有任何不良影响;你会认为你不在 Docker 容器中,在最坏的情况下不会运行可能具有破坏性的代码。
一个更现实的情况是,Docker 的这个未经记录的行为在 Docker 的新版本中已经被改变或删除(或者你使用的是在行为首次实现之前发布的版本)。
代码可能是启动 bash 脚本的一部分,如下所示,然后是启动脚本代码的其余部分。
列表 6.3. 如果在容器外运行,Shell 脚本将失败
#!/bin/bash
if ! [ -f /.dockerenv ]
then
echo 'Not in a Docker container, exiting.'
exit 1
fi
当然,你也可以使用相反的逻辑来确定你不在容器内运行,如果你有这个需求:
列表 6.4. 如果在容器内运行,Shell 脚本将失败
#!/bin/bash
if [ -f /.dockerenv ]
then
echo 'In a Docker container, exiting.'
exit 1
fi
此示例使用 bash 来确定文件的存在,但绝大多数编程语言都会有自己的方式来确定容器(或主机)文件系统上文件的存在。
讨论
你可能会想知道这种情况发生的频率。这种情况经常发生,足以成为 Docker 论坛上的常规讨论点,在那里关于这是否是一个有效用例,或者你的应用程序设计中的其他方面是否有问题的宗教式争论会爆发。
将这些讨论放在一边,你可能会轻易地陷入需要根据是否在 Docker 容器中切换代码路径的情况。我们遇到的一个例子是使用 Makefile 构建容器。
摘要
-
你可以配置你的机器,让你可以在不使用
sudo的情况下运行 Docker。 -
使用内置的 Docker 命令来清理未使用的容器和卷。
-
可以使用外部工具以新的方式公开有关你的容器的信息。
-
docker exec命令是进入正在运行的容器的正确方式——请抵制安装 SSH。
第七章. 配置管理:整理你的环境
本章涵盖
-
使用 Dockerfile 管理镜像构建
-
使用传统的配置管理工具构建镜像
-
管理构建镜像所需的秘密信息
-
减小镜像大小以实现更快、更轻、更安全的交付
配置管理是管理你的环境,使其稳定和可预测的艺术。例如,Chef 和 Puppet 等工具试图减轻系统管理员管理多台机器的负担。在一定程度上,Docker 通过使软件环境隔离和可移植来减少这种负担。即便如此,仍然需要配置管理技术来生成 Docker 镜像,这是一个重要的认识点。
到本章结束时,你将知道如何将现有工具与 Docker 集成,解决一些 Docker 特定的问题,如从层中删除机密,并遵循最小化最终镜像的最佳实践。随着你对 Docker 的经验越来越丰富,这些技术将使你能够为满足任何配置需求构建镜像。
7.1. 配置管理和 Dockerfile
Dockerfile 被认为是构建 Docker 镜像的标准方式。在配置管理方面,Dockerfile 常常令人困惑。你可能会有很多问题(尤其是如果你有其他配置管理工具的经验),例如
-
如果基础镜像发生变化会怎样?
-
如果我安装的软件包发生变化并且我重新构建会发生什么?
-
这是否取代了 Chef/Puppet/Ansible?
事实上,Dockerfile 非常简单:从一个给定的镜像开始,Dockerfile 指定了一系列 shell 命令和元指令给 Docker,这将产生所需的最终镜像。
Dockerfile 为提供 Docker 镜像提供了一个通用、简单和通用的语言。在它们内部,你可以使用任何你喜欢的方式来达到预期的最终状态。你可以调用 Puppet,复制另一个脚本,或者复制整个文件系统!
首先,我们将考虑你如何处理 Dockerfile 带来的某些小挑战。然后,我们将继续讨论我们刚刚概述的更复杂的问题。
使用 ENTRYPOINT 创建可靠的定制工具
Docker 允许你在任何地方运行命令的潜力意味着,复杂的定制指令或脚本可以在命令行上预先配置并封装成打包的工具。
容易被误解的 ENTRYPOINT 指令是这一过程的关键部分。你将看到它是如何使你能够创建封装良好、定义清晰且足够灵活以供使用的 Docker 镜像的。
问题
你想要定义容器将运行的命令,但将命令的参数留给用户。
解决方案
使用 Dockerfile 的 ENTRYPOINT 指令。
作为演示,我们将想象一个简单的企业场景,其中常规管理员任务之一是清理旧的日志文件。这通常容易出错,人们会不小心删除错误的东西,因此我们将使用 Docker 镜像来降低出现问题的风险。
以下脚本(你保存时应将其命名为clean_log)会删除超过一定天数的日志,天数作为命令行选项传入。在任何地方创建一个新文件夹,取任何你喜欢的名字,进入它,并在其中放置 clean_log。
列表 7.1. clean_log shell 脚本
#!/bin/bash
echo "Cleaning logs over $1 days old"
find /log_dir -ctime "$1" -name '*log' -exec rm {} \;
注意,日志清理发生在 /log_dir 文件夹上。这个文件夹只有在运行时挂载时才会存在。你可能也注意到脚本中没有检查是否传入了参数。原因将在我们通过技术时揭晓。
现在,让我们在同一目录下创建一个 Dockerfile 来创建一个图像,其中脚本作为定义的命令或入口点运行。
列表 7.2. 使用 clean_log 脚本创建图像
FROM ubuntu:17.04
ADD clean_log /usr/bin/clean_log *1*
RUN chmod +x /usr/bin/clean_log
ENTRYPOINT ["/usr/bin/clean_log"] *2*
CMD ["7"] *3*
-
1 将之前的公司 clean_log 脚本添加到图像中
-
2 定义此图像的入口点为 clean_log 脚本
-
3 定义入口点命令的默认参数(7 天)
小贴士
你会注意到我们通常更喜欢 CMD 和 ENTRYPOINT 的数组形式(例如,CMD ["/usr/bin/command"]),而不是 shell 形式(CMD /usr/bin/command)。这是因为 shell 形式会自动将一个 /bin/bash -c 命令添加到你提供的命令之前,这可能会导致意外的行为。然而,有时 shell 形式更有用(参见技术 55)。
ENTRYPOINT 和 CMD 之间的区别常常让人困惑。理解的关键点是,当图像启动时,入口点总是会运行,即使你在 docker run 调用中提供了命令。如果你尝试提供命令,它将作为参数添加到入口点,替换 CMD 指令中定义的默认值。你只能通过在 docker run 命令中显式传递 --entrypoint 标志来覆盖入口点。这意味着使用 /bin/bash 命令运行图像不会给你一个 shell;相反,它将 /bin/bash 作为参数提供给 clean_log 脚本。
由于 CMD 指令定义了默认参数,因此提供的参数不需要检查。以下是如何构建和调用此工具的方法:
docker build -t log-cleaner .
docker run -v /var/log/myapplogs:/log_dir log-cleaner 365
在构建图像后,通过将 /var/log/myapplogs 挂载到脚本将使用的目录,并传递 365 来删除一年以上的日志文件,而不是一周,来调用图像。
如果有人试图不指定天数而错误地使用该图像,他们将会收到一个错误信息:
$ docker run -ti log-cleaner /bin/bash
Cleaning logs over /bin/bash days old
find: invalid argument `-name' to `-ctime'
讨论
这个例子相当简单,但你可以想象一个公司可以将它应用于在其整个企业中集中管理脚本,这样它们就可以通过私有仓库安全地维护和分发。
您可以在 Docker Hub 的 dockerinpractice/log-cleaner 上查看和使用我们在此技术中创建的图像。
| |
通过指定版本避免包漂移
Dockerfile 具有简单的语法和有限的功能,它们可以极大地帮助阐明构建的要求,并有助于提高镜像生产的稳定性,但它们不能保证可重复的构建。我们将探讨解决此问题的多种方法之一,以减少当底层包管理依赖项发生变化时出现意外风险。
这种技术有助于避免那些“昨天还工作过”的时刻,如果您使用过经典配置管理工具,可能会觉得这种方法很熟悉。构建 Docker 镜像与维护服务器在本质上相当不同,但一些经验教训仍然适用。
注意
这种技术仅适用于基于 Debian 的镜像,例如 Ubuntu。Yum 用户可以在他们的包管理器下找到类似的技术来实现。
问题
您想确保您的 deb 软件包是您期望的版本。
解决方案
运行一个脚本来捕获您按需设置的系统的所有依赖包的版本。然后在您的 Dockerfile 中安装特定的版本,以确保版本与您期望的完全一致。
您可以使用apt-cache调用在您已验证为 OK 的系统上执行基本版本检查:
$ apt-cache show nginx | grep ^Version:
Version: 1.4.6-1ubuntu3
然后,您可以在 Dockerfile 中这样指定版本:
RUN apt-get -y install nginx=1.4.6-1ubuntu3
这可能足以满足您的需求。但这并不能保证 nginx 这个版本的依赖项与您最初验证的版本相同。
您可以通过在参数中添加--recurse标志来获取所有这些依赖项的信息:
apt-cache --recurse depends nginx
此命令的输出非常大,因此获取版本要求列表很困难。幸运的是,我们维护了一个 Docker 镜像(还有什么别的?),这使得这个过程更容易。它输出您需要放入 Dockerfile 中的RUN行,以确保所有依赖项的版本都是正确的。
$ docker run -ti dockerinpractice/get-versions vim
RUN apt-get install -y \
vim=2:7.4.052-1ubuntu3 vim-common=2:7.4.052-1ubuntu3 \
vim-runtime=2:7.4.052-1ubuntu3 libacl1:amd64=2.2.52-1 \
libc6:amd64=2.19-0ubuntu6.5 libc6:amd64=2.19-0ubuntu6.5 \
libgpm2:amd64=1.20.4-6.1 libpython2.7:amd64=2.7.6-8 \
libselinux1:amd64=2.2.2-1ubuntu0.1 libselinux1:amd64=2.2.2-1ubuntu0.1 \
libtinfo5:amd64=5.9+20140118-1ubuntu1 libattr1:amd64=1:2.4.47-1ubuntu1 \
libgcc1:amd64=1:4.9.1-0ubuntu1 libgcc1:amd64=1:4.9.1-0ubuntu1 \
libpython2.7-stdlib:amd64=2.7.6-8 zlib1g:amd64=1:1.2.8.dfsg-1ubuntu1 \
libpcre3:amd64=1:8.31-2ubuntu2 gcc-4.9-base:amd64=4.9.1-0ubuntu1 \
gcc-4.9-base:amd64=4.9.1-0ubuntu1 libpython2.7-minimal:amd64=2.7.6-8 \
mime-support=3.54ubuntu1.1 mime-support=3.54ubuntu1.1 \
libbz2-1.0:amd64=1.0.6-5 libdb5.3:amd64=5.3.28-3ubuntu3 \
libexpat1:amd64=2.1.0-4ubuntu1 libffi6:amd64=3.1~rc1+r3.0.13-12 \
libncursesw5:amd64=5.9+20140118-1ubuntu1 libreadline6:amd64=6.3-4ubuntu2 \
libsqlite3-0:amd64=3.8.2-1ubuntu2 libssl1.0.0:amd64=1.0.1f-1ubuntu2.8 \
libssl1.0.0:amd64=1.0.1f-1ubuntu2.8 readline-common=6.3-4ubuntu2 \
debconf=1.5.51ubuntu2 dpkg=1.17.5ubuntu5.3 dpkg=1.17.5ubuntu5.3 \
libnewt0.52:amd64=0.52.15-2ubuntu5 libslang2:amd64=2.2.4-15ubuntu1 \
vim=2:7.4.052-1ubuntu3
在某个时候,您的构建将因为某个版本不再可用而失败。当这种情况发生时,您将能够看到哪个软件包已更改,并审查更改以确定它是否适合您特定镜像的需求。
此示例假设您正在使用 ubuntu:14.04。如果您使用的是不同的 Debian 版本,请 fork 仓库并更改 Dockerfile 的FROM指令,然后构建它。仓库在此处可用:github.com/docker-in-practice/get-versions.git。
尽管这种技术可以帮助您提高构建的稳定性,但在安全性方面却无能为力,因为您仍然在下载您无法直接控制的仓库中的软件包。
讨论
这种技术可能看起来需要付出很多努力来确保文本编辑器完全符合你的预期。然而,在实际应用中,包的漂移可能导致难以追踪的 bug。库和应用程序在构建过程中可能会以微妙的方式发生变化,弄清楚发生了什么可能会让你的日子变得糟糕。
通过在 Dockerfile 中尽可能紧密地锁定版本,你可以确保以下两种情况之一发生。要么构建成功,你的软件将像昨天一样表现,要么由于某个软件发生变化而无法构建,你需要重新测试你的开发流程。在后一种情况下,你会知道发生了什么变化,并且可以将任何随之而来的失败缩小到那个特定的变化。
重点是,当你进行持续构建和集成时,减少变化的变量数量可以减少调试时间。这对你企业的资金来说意味着节省。
使用 perl -p-i -e 替换文本
当使用 Dockerfile 构建镜像时,你通常需要在多个文件中替换特定的文本项。存在许多解决方案,但我们将介绍一个相对不寻常但特别适用于 Dockerfile 的解决方案。
问题
你希望在构建过程中修改文件中的特定行。
解决方案
使用perl -p -i -e命令。
我们推荐这个命令有几个原因:
-
与
sed -i(一个语法和效果相似的命令)不同,这个命令默认就可以在多个文件上工作,即使它遇到其中一个文件的问题。这意味着你可以在目录中使用'*'通配符模式运行它,而不用担心在包的后续版本中添加目录时它会突然中断。 -
与
sed一样,你可以在搜索和替换命令中用其他字符替换正斜杠。 -
记忆起来很容易(我们称之为“perl pie”命令)。
注意
这种技术假设你对正则表达式有所了解。如果你不熟悉正则表达式,有很多网站可以帮助你。
这里是这个命令典型使用的一个例子:
perl -p -i -e 's/127\.0\.0\.1/0.0.0.0/g' *
在这个命令中,-p标志让 Perl 在处理所有看到的行时假设一个循环。-i标志让 Perl 就地更新匹配的行,而-e标志让 Perl 将提供的字符串视为 Perl 程序。s是 Perl 的一个指令,用于搜索和替换输入中匹配的字符串。在这里,127.0.0.1被替换为0.0.0.0。g修饰符确保所有匹配项都被更新,而不仅仅是任何给定行的第一个匹配项。最后,星号(*)将更新应用于此目录中的所有文件。
前面的命令是 Docker 容器中一个相当常见的操作。当用作监听地址时,它将标准的 localhost IP 地址(127.0.0.1)替换为指示“任何”IPv4 地址(0.0.0.0)。许多应用程序通过只监听该地址来限制对 localhost IP 的访问,并且通常你会在它们的配置文件中将地址更改为“任何”地址,因为你会从主机访问应用程序,对于容器来说,主机看起来是一个外部机器。
提示
如果 Docker 容器中的应用程序似乎无法从主机机器访问,尽管端口是开放的,那么尝试在应用程序配置文件中将监听地址更新为0.0.0.0并重新启动可能是有价值的。可能是因为应用程序拒绝你,因为你不是从它的 localhost 来的。在运行镜像时使用--net=host(稍后在技巧 109 中介绍)可以帮助确认这个假设。
perl -p -i -e(和sed)的另一个不错特性是,如果你在转义斜杠时感到尴尬,可以使用其他字符来替换正斜杠。以下是我们脚本中的一个真实世界示例,该脚本向默认的 Apache 站点文件添加了一些指令。这个尴尬的命令,
perl -p -i -e 's/\/usr\/share\/www/\/var\/www\/html/g' /etc/apache2/*
变成这样:
perl -p -i -e 's@/usr/share/www@/var/www/html/@g' /etc/apache2/*
在极少数情况下,如果你想匹配或替换/和@字符,你可以尝试其他字符,如|或#。
讨论
这是一个在 Docker 世界内外都适用的技巧。这是你武器库中的一个有用工具。
我们发现这个技巧特别有用,因为它在 Dockerfile 中的应用非常广泛,而且易于记忆:如果你不介意这个双关语,那就是“易如反掌”。
| |
简化镜像
Dockerfile 的设计及其生成 Docker 镜像的后果是,最终的镜像包含了 Dockerfile 中每个步骤的数据状态。在构建镜像的过程中,可能需要复制秘密以确保构建可以工作。这些秘密可能是 SSH 密钥、证书或密码文件。在提交镜像之前删除这些秘密并不能真正提供保护,因为它们将存在于最终镜像的更高层。恶意用户可以轻易地从镜像中提取它们。
处理这个问题的方法之一是简化生成的镜像。
问题
你想要从镜像的层历史中删除秘密信息。
解决方案
使用该镜像实例化一个容器,导出它,导入它,然后使用原始镜像 ID 对其进行标记。
为了演示这种情况可能有用的情况,让我们在一个新目录中创建一个简单的 Dockerfile,该目录包含一个“大秘密”。运行mkdir secrets && cd secrets,然后在那个文件夹中创建一个包含以下内容的 Dockerfile。
列表 7.3. 一个复制并删除秘密的 Dockerfile
FROM debian
RUN echo "My Big Secret" >> /tmp/secret_key *1*
RUN cat /tmp/secret_key *2*
RUN rm /tmp/secret_key *3*
-
1 在你的构建中放置一个包含一些机密信息的文件。
-
2 对包含机密信息的文件进行操作。这个 Dockerfile 只是简单地显示文件,但你的可能需要 SSH 到另一个服务器或在镜像中加密那个机密信息。
-
3 删除机密文件。
现在运行docker build -t mysecret .来构建和标记该 Dockerfile。
一旦构建完成,你可以使用docker history命令检查生成的 Docker 镜像的层:
$ docker history mysecret *1*
IMAGE CREATED CREATED BY SIZE
55f3c131a35d 3 days ago /bin/sh -c rm /tmp/secret_key *2*
5b376ff3d7cd 3 days ago /bin/sh -c cat /tmp/secret_key 0 B
5e39caf7560f 3 days ago /bin/sh -c echo "My Big Secret" >> /tmp/se 14 B *3*
c90d655b99b2 6 days ago /bin/sh -c #(nop) CMD [/bin/bash] 0 B
30d39e59ffe2 6 days ago /bin/sh -c #(nop) ADD file:3f1a40df75bc567 85.01 MB *4*
511136ea3c5a 20 months ago 0 B *5*
-
1 运行针对你创建的镜像名称的 docker history 命令
-
2 删除机密密钥的层
-
3 添加机密密钥的层
-
4 添加了 Debian 文件系统的层。请注意,这个层是历史中最大的一个。
-
5 空层(无内容层)
现在假设你已经从公共注册表中下载了这个镜像。你可以检查层历史记录,然后运行以下命令来揭示机密信息:
$ docker run 5b376ff3d7cd cat /tmp/secret_key
My Big Secret
在这里,我们运行了一个特定的层,并指示它cat出我们在更高层删除的机密密钥。如你所见,文件是可访问的。
现在你有一个包含机密信息且你已看到可以被黑客攻击以揭示其机密的“危险”容器。为了使这个镜像安全,你需要将其扁平化。这意味着你将在镜像中保留相同的数据,但删除中间分层信息。为了实现这一点,你需要将镜像导出为一个简单运行的容器,然后重新导入并标记生成的镜像:
$ docker run -d mysecret /bin/true *1*
28cde380f0195b24b33e19e132e81a4f58d2f055a42fa8406e755b2ef283630f
$ docker export 28cde380f | docker import - mysecret *2*
$ docker history mysecret
IMAGE CREATED CREATED BY SIZE
fdbeae08751b 13 seconds ago 85.01 MB *3*
-
1 运行一个简单的命令以允许容器快速退出,因为你不需要它一直运行
-
2 运行 docker export 命令,以容器 ID 作为参数,输出文件系统内容的 TAR 文件。这个 TAR 文件被管道传输到 docker import 命令,该命令接受 TAR 文件并从内容创建镜像。
-
3 显示最终文件集的唯一层的 docker history 输出
docker import命令的-参数表示你希望从命令的标准输入读取 TAR 文件。docker import命令的最后一个参数表示导入的镜像应该如何标记。在这种情况下,你正在覆盖之前的标记。
因为现在镜像中只有一个层,所以没有包含机密信息的层的记录。现在无法从镜像中提取任何机密信息。
讨论
这种技术在本书的多个地方都很有用,例如在第 7.3 节中。
如果你考虑使用这种技术,需要考虑的一点是,多层镜像在层缓存和下载时间上的好处可能会丢失。如果你的组织对此进行周密计划,这种技术可以在这些镜像的实际应用中发挥作用。
| |
使用 Alien 管理外部包
尽管本书(以及互联网上)的大多数 Dockerfile 示例都使用基于 Debian 的镜像,但软件开发的现实意味着许多人不会仅处理它们。
幸运的是,存在一些工具可以帮助你完成这项工作。
问题
你想从外国发行版安装一个包。
解决方案
使用名为 Alien 的工具转换包。Alien 集成到我们将作为技术一部分使用的 Docker 镜像中。
Alien 是一个命令行工具,旨在将各种格式之间的包文件进行转换,这些格式列于 表 7.1 中。不止一次,我们被要求使来自外国包管理系统的包工作,例如 CentOS 中的 .deb 文件,以及非 Red Hat 基础系统中的 .rpm 文件。
表 7.1. Alien 支持的包格式
| 扩展名 | 描述 |
|---|---|
| .deb | Debian 包 |
| .rpm | Red Hat 包管理 |
| .tgz | Slackware 压缩的 TAR 文件 |
| .pkg | Solaris PKG 包 |
| .slp | Stampede 包 |
注意
对于这项技术的目的,Solaris 和 Stampede 包并未完全涵盖。Solaris 需要特有的 Solaris 软件,而 Stampede 是一个已废弃的项目。
在研究这本书时,我们发现,在非 Debian 基础发行版上安装 Alien 可能会有点麻烦。鉴于这是一本 Docker 书,我们自然决定以 Docker 镜像的形式提供转换工具。作为额外的好处,这个工具使用了 技术 49 中的 ENTRYPOINT 命令来简化工具的使用。
例如,让我们下载并转换(使用 Alien)eatmydata 包,该包将在 技术 62 中使用。
$ mkdir tmp && cd tmp *1*
$ wget \
http://mirrors.kernel.org/ubuntu/pool/main/libe/libeatmydata
/eatmydata_26-2_i386.deb *2*
$ docker run -v $(pwd):/io dockerinpractice/alienate *3*
Examining eatmydata_26-2_i386.deb from /io
eatmydata_26-2_i386.deb appears to be a Debian package *4*
eatmydata-26-3.i386.rpm generated
eatmydata-26.slp generated
eatmydata-26.tgz generated
================================================
/io now contains:
eatmydata-26-3.i386.rpm
eatmydata-26.slp
eatmydata-26.tgz
eatmydata_26-2_i386.deb
================================================
$ ls -1 *5*
eatmydata_26-2_i386.deb
eatmydata-26-3.i386.rpm
eatmydata-26.slp
eatmydata-26.tgz
-
1 创建一个工作目录
-
2 检索你想要转换的包文件
-
3 运行 dockerinpractice/alienate 镜像,将当前目录挂载到容器的 /io 路径。容器将检查该目录,并尝试转换它找到的任何有效文件。
-
4 容器在运行 Alien 包装脚本时通知你其操作。
-
5 文件已转换为 RPM、Slackware TGZ 和 Stampede 文件。
或者,你可以将包的 URL 直接传递给 docker run 命令进行下载和转换:
$ mkdir tmp && cd tmp
$ docker run -v $(pwd):/io dockerinpractice/alienate \
http://mirrors.kernel.org/ubuntu/pool/main/libe/libeatmydata
/eatmydata_26-2_i386.deb
wgetting http://mirrors.kernel.org/ubuntu/pool/main/libe/libeatmydata
/eatmydata_26-2_i386.deb
--2015-02-26 10:57:28-- http://mirrors.kernel.org/ubuntu/pool/main/libe
/libeatmydata/eatmydata_26-2_i386.deb
Resolving mirrors.kernel.org (mirrors.kernel.org)... 198.145.20.143,
149.20.37.36, 2001:4f8:4:6f:0:1994:3:14, ...
Connecting to mirrors.kernel.org (mirrors.kernel.org)|198.145.20.143|:80...
connected.
HTTP request sent, awaiting response... 200 OK
Length: 7782 (7.6K) [application/octet-stream]
Saving to: 'eatmydata_26-2_i386.deb'
0K ....... 100% 2.58M=0.003s
2015-02-26 10:57:28 (2.58 MB/s) - 'eatmydata_26-2_i386.deb' saved
[7782/7782]
Examining eatmydata_26-2_i386.deb from /io
eatmydata_26-2_i386.deb appears to be a Debian package
eatmydata-26-3.i386.rpm generated
eatmydata-26.slp generated
eatmydata-26.tgz generated
=========================================================
/io now contains:
eatmydata-26-3.i386.rpm
eatmydata-26.slp
eatmydata-26.tgz
eatmydata_26-2_i386.deb
=========================================================
$ ls -1
eatmydata_26-2_i386.deb
eatmydata-26-3.i386.rpm
eatmydata-26.slp
eatmydata-26.tgz
如果你想在容器中运行 Alien,你可以使用以下命令启动容器:
docker run -ti --entrypoint /bin/bash dockerinpractice/alienate
警告
Alien 是一个尽力而为的工具,并且不能保证它能与给定的包一起工作。
讨论
Docker 的使用使沉睡已久的“发行版之战”变得尖锐。大多数组织已经适应了仅仅成为 Red Hat 或 Debian 商店,无需担心其他包管理系统。现在,在组织内部引入基于“alien”发行版的 Docker 镜像的请求并不罕见。
这就是这项技术可以提供帮助的地方,因为“外国”包可以被转换为更友好的格式。这个话题将在 第十四章 中再次讨论,我们将讨论安全性。
7.2. 使用 Docker 的传统配置管理工具
现在我们将讨论 Dockerfile 如何与更传统的配置管理工具一起工作。
我们在这里将探讨传统的配置管理使用 make,展示您如何使用现有的 Chef 脚本来使用 Chef Solo 配置您的镜像,并查看一个构建来帮助非 Docker 专家构建镜像的 shell 脚本框架。
传统方式:使用 make 与 Docker
在某个时候,您可能会发现拥有大量的 Dockerfile 限制了您的构建过程。例如,如果您将自己限制在运行 docker build,则无法生成任何输出 文件,并且 Dockerfile 中没有变量。
这个对额外工具的需求可以通过多种工具(包括普通的 shell 脚本)来解决。在这个技术中,我们将探讨如何将古老的 make 工具扭曲以与 Docker 一起工作。
问题
您想在 docker build 执行周围添加额外的任务。
解决方案
使用一个古老的(在计算机术语中)工具,称为 make。
如果您之前没有使用过它,make 是一个工具,它接受一个或多个输入文件并生成一个输出文件,但它也可以用作任务运行器。以下是一个简单的示例(注意,所有缩进都必须是制表符):
列表 7.4. 一个简单的 Makefile
.PHONY: default createfile catfile *1*
default: createfile *2*
createfile: x.y.z *3*
catfile: *4*
cat x.y.z
x.y.z: *5*
echo "About to create the file x.y.z"
echo abc > x.y.z
-
1 默认情况下,make 假设所有目标都是将被任务创建的文件名。.PHONY 表示对于哪些任务名称这不是真的。
-
2 按照惯例,Makefile 中的第一个目标是“default”。当没有明确的目标运行时,make 将选择文件中的第一个。您可以看到,“default”将执行“createfile”作为其唯一的依赖项。
-
3 createfile 是一个依赖于 x.y.z 任务的虚拟任务。
-
4 catfile 是一个运行单个命令的虚拟任务。
-
5 x.y.z 是一个文件任务,运行两个命令并创建目标文件 x.y.z。
警告
Makefile 中的所有缩进都必须是制表符,并且每个目标中的命令都在不同的 shell 中运行(因此环境变量不会传递)。
一旦您在名为 Makefile 的文件中有了前面的内容,您可以使用类似 make createfile 的命令调用任何目标。
现在我们来看看 Makefile 中的一些有用模式——我们将讨论的其余目标将是虚拟的,因为使用文件更改跟踪来自动触发 Docker 构建(尽管可能,但很困难)。Dockerfile 使用层缓存,因此构建通常很快。
第一步是运行一个 Dockerfile。因为 Makefile 由 shell 命令组成,所以这很简单。
列表 7.5. 构建镜像的 Makefile
base:
docker build -t corp/base .
正常情况下,这种变化会按预期工作(例如,通过管道将文件传递到 docker build 以删除上下文,或使用 -f 使用不同命名的 Dockerfile),你可以使用 make 的依赖项功能自动构建必要的基镜像(用于 FROM)。例如,如果你将多个仓库检出到名为 repos 的子目录中(这也很容易用 make 完成),你可以在以下列表中添加一个目标。
列表 7.6. 在子目录中构建镜像的 Makefile
app1: base
cd repos/app1 && docker build -t corp/app1 .
这种方法的缺点是,每次你的基础镜像需要重建时,Docker 都会上传一个包含所有你的仓库的构建上下文。你可以通过显式传递一个构建上下文 TAR 文件给 Docker 来解决这个问题。
列表 7.7. 使用特定文件集构建镜像的 Makefile
base:
tar -cvf - file1 file2 Dockerfile | docker build -t corp/base -
这种显式的依赖声明如果目录中包含大量与构建无关的文件,将提供显著的加速。如果你想将所有构建依赖项保存在不同的目录中,你可以稍微修改这个目标。
列表 7.8. 使用特定文件集和重命名路径构建镜像的 Makefile
base:
tar --transform 's/^deps\///' -cf - deps/* Dockerfile | \
docker build -t corp/base -
在这里,你将 deps 目录中的所有内容添加到构建上下文中,并使用 tar 的 --transform 选项(在 Linux 上最近的 tar 版本中可用)从文件名中去除任何前缀“deps/”。在这种情况下,一个更好的方法是将 deps 和 Dockerfile 放在自己的目录中,以便允许正常的 docker build,但了解这种高级用法是有用的,因为它在最不可能的地方也可能派上用场。不过,在使用之前,一定要仔细思考,因为它会增加你的构建过程的复杂性。
简单的变量替换是一个相对简单的问题,但(就像之前使用 --transform 一样)在使用之前要仔细思考——Dockerfile 故意不支持变量,以便保持构建过程易于重复。
在这里,我们将使用传递给 make 的变量,并使用 sed 进行替换,但你也可以按你喜欢的方式传递和替换。
列表 7.9. 使用基本的 Dockerfile 变量替换构建镜像的 Makefile
VAR1 ?= defaultvalue
base:
cp Dockerfile.in Dockerfile
sed -i 's/{VAR1}/$(VAR1)/' Dockerfile
docker build -t corp/base .
每次运行基础目标时,Dockerfile 都会重新生成,你可以通过添加更多的 sed -i 行来添加更多的变量替换。要覆盖 VAR1 的默认值,运行 make VAR1=newvalue base。如果你的变量包含斜杠,你可能需要选择不同的 sed 分隔符,如 sed -i 's#{VAR1}#$(VAR1)#' Dockerfile。
最后,如果你一直将 Docker 作为构建工具使用,你需要知道如何从 Docker 中提取文件。我们将根据你的使用情况介绍几种不同的可能性。
列表 7.10. 从镜像中复制文件的 Makefile
singlefile: base
docker run --rm corp/base cat /path/to/myfile > outfile
multifile: base
docker run --rm -v $(pwd)/outdir:/out corp/base sh \
-c "cp -r /path/to/dir/* /out/"
在这里,singlefile 在一个文件上运行 cat 并将输出管道到一个新文件。这种方法的优势是自动设置文件的正确所有者,但如果有多个文件,就会变得繁琐。multifile 方法在容器中挂载一个卷,并将所有文件从目录复制到卷。你可以通过一个 chown 命令来设置文件的正确所有者,但请注意,你可能需要用 sudo 来调用它。
Docker 项目本身在从源构建 Docker 时使用卷挂载方法。
讨论
对于像 make 这样古老的工具出现在一本关于相对较新的技术 Docker 的书中,可能会显得有些奇怪。为什么不使用像 Ant、Maven 或其他许多通用构建工具中的一种较新的构建技术呢?
答案是,尽管 make 有很多缺点,但它是一个工具,
-
很难在短时间内消失
-
文档齐全
-
非常灵活
-
广泛可用
在花费许多小时与新的构建技术的错误或文档不完善(或未记录)的限制作斗争,或者尝试安装这些系统的依赖项之后,make 的功能已经帮我们节省了很多次。而且,make 在五年后仍然可能可用,而其他工具可能已经消失,或者已经由其所有者停止维护。
| |
使用 Chef Solo 构建镜像
对于 Docker 的新手来说,可能会感到困惑,不知道 Dockerfile 是否是唯一支持的配置管理工具,以及现有的配置管理工具是否应该移植到 Dockerfile 中。这两者都不是事实。
尽管 Dockerfile 被设计成一种简单且可移植的提供镜像的方法,但它们也足够灵活,允许任何其他配置管理工具接管。简而言之,如果你能在终端中运行它,你就可以在 Dockerfile 中运行它。
作为这个演示的一部分,我们将向你展示如何在 Dockerfile 中使用 Chef,这是最成熟的配置管理工具之一,以展示如何在 Dockerfile 中启动和运行 Chef。使用像 Chef 这样的工具可以减少你配置镜像所需的工作量。
注意
虽然不需要熟悉 Chef 就能理解这个技术,但第一次轻松地使用它时,需要一些熟悉度。涵盖整个配置管理工具本身就是一本书。通过仔细学习和一些研究,这个技术可以帮助你很好地理解 Chef 的基础知识。
问题
你希望通过使用 Chef 来减少配置工作量。
解决方案
在你的容器中安装 Chef,并在该容器内使用 Chef Solo 运行食谱来配置它,所有这些都在你的 Dockerfile 中完成。
你将要配置的是一个简单的 Hello World Apache 网站。这将让你尝到 Chef 在配置方面能为你做什么。
Chef Solo 不需要外部 Chef 服务器设置。如果你已经熟悉 Chef,此示例可以轻松地修改以启用你的现有脚本来联系你的 Chef 服务器,如果你希望这样做的话。
我们将逐步介绍创建此 Chef 示例的过程,但如果你想要下载可工作的代码,它作为一个 Git 仓库可用。要下载它,请运行以下命令:
git clone https://github.com/docker-in-practice/docker-chef-solo-example.git
我们将从设置一个输出“Hello World!”(还能是什么?)的 Apache 服务器开始,当你访问它时。网站将从 mysite.com 服务器提供,并在镜像上设置一个 mysiteuser 用户。
首先,创建一个目录,并使用你需要的文件来设置 Chef 配置。
列表 7.11. 为 Chef 配置创建必要的文件
$ mkdir chef_example
$ cd chef_example
$ touch attributes.json *1*
$ touch config.rb *2*
$ touch Dockerfile *3*
$ mkdir -p cookbooks/mysite/recipes *4*
$ touch cookbooks/mysite/recipes/default.rb
$ mkdir -p cookbooks/mysite/templates/default *5*
$ touch cookbooks/mysite/templates/default/message.erb
-
1 Chef 属性文件,它定义了此镜像(或节点,在 Chef 术语中)的变量,将包含此镜像的运行列表中的食谱和其他信息。
-
2 Chef 配置文件,它为 Chef 配置设置一些基本变量
-
3 构建镜像的 Dockerfile
-
4 创建默认食谱文件夹,用于存储构建镜像的 Chef 指令
-
5 创建动态配置内容的模板
首先,我们将填写 attributes.json。
列表 7.12. attributes.json
{
"run_list": [
"recipe[apache2::default]",
"recipe[mysite::default]"
]
}
此文件列出你将要运行的食谱。apache2 食谱将从公共仓库检索;mysite 食谱将在这里编写。
接下来,在你的 config.rb 中填写一些基本信息,如下一个列表所示。
列表 7.13. config.rb
base_dir "/chef/"
file_cache_path base_dir + "cache/"
cookbook_path base_dir + "cookbooks/"
verify_api_cert true
此文件设置有关位置的基本信息,并添加配置设置 verify_api_cert 以抑制一个无关的错误。
现在,我们进入工作的核心:镜像的 Chef 食谱。代码块中每个以 end 结尾的段落定义了一个 Chef 资源。
列表 7.14. cookbooks/mysite/recipes/default.rb
user "mysiteuser" do *1*
comment "mysite user"
home "/home/mysiteuser"
shell "/bin/bash"
end
directory "/var/www/html/mysite" do *2*
owner "mysiteuser"
group "mysiteuser"
mode 0755
action :create
end
template "/var/www/html/mysite/index.html" do *3*
source "message.erb"
variables(
:message => "Hello World!"
)
user "mysiteuser"
group "mysiteuser"
mode 0755
end
web_app "mysite" do *4*
server_name "mysite.com"
server_aliases ["www.mysite.com","mysite.com"] *5*
docroot "/var/www/html/mysite"
cookbook 'apache2'
end
-
1 创建一个用户
-
2 创建一个用于 web 内容的目录
-
3 定义一个将放置在 web 文件夹中的文件。此文件将从“source”属性中定义的模板创建。
-
4 定义一个用于 apache2 的 web 应用
-
5 在实际场景中,你需要将 mysite 的引用更改为你的网站名称。如果你从主机访问或测试,这并不重要。
网站的内容包含在模板文件中。它包含一行,Chef 将读取该行,并用 config.rb 中的“Hello World!”消息进行替换。然后,Chef 将替换后的文件写入模板目标 (/var/www/html/mysite/index.html)。这里使用的是我们不会在这里介绍的模板语言。
列表 7.15. cookbooks/mysite/templates/default/message.erb
<%= @message %>
最后,你使用 Dockerfile 将所有内容组合起来,该文件设置 Chef 预先条件并运行 Chef 来配置镜像,如下一个列表所示。
列表 7.16. Dockerfile
FROM ubuntu:14.04
RUN apt-get update && apt-get install -y git curl
RUN curl -L \
https://opscode-omnibus-packages.s3.amazonaws.com/ubuntu/12.04/x86_64
/chefdk_0.3.5-1_amd64.deb \
-o chef.deb
RUN dpkg -i chef.deb && rm chef.deb *1*
COPY . /chef *2*
WORKDIR /chef/cookbooks *3*
RUN knife cookbook site download apache2 *3*
RUN knife cookbook site download iptables *3*
RUN knife cookbook site download logrotate *3*
RUN /bin/bash -c 'for f in $(ls *gz); do tar -zxf $f; rm $f; done' *4*
RUN chef-solo -c /chef/config.rb -j /chef/attributes.json *5*
CMD /usr/sbin/service apache2 start && sleep infinity *6*
-
1 下载并安装 Chef。如果这个下载对您不起作用,请检查本讨论中之前提到的 docker-chef-solo-example 中的最新代码,因为可能现在需要 Chef 的较新版本。
-
2 将工作文件夹的内容复制到镜像上的/chef 文件夹
-
3 移动到 cookbooks 文件夹,并使用 Chef 的 knife 工具将 apache2 cookbooks 及其依赖项作为 tar 包下载
-
4 提取下载的 tar 包并删除它们
-
5 运行 chef 命令来配置您的镜像。提供您已经创建的属性和配置文件。
-
6 定义镜像的默认命令。无限期休眠的命令确保容器在服务命令完成其工作后不会立即退出。
您现在可以构建并运行该\image:
docker build -t chef-example .
docker run -ti -p 8080:80 chef-example
如果您现在导航到 http://localhost:8080,您应该看到您的“Hello World!”消息。
警告
如果您的 Chef 构建耗时较长且您正在使用 Docker Hub 工作流程,构建可能会超时。如果发生这种情况,您可以在您控制的机器上执行构建,支付支持的服务费用,或者将构建步骤分解成更小的块,以便 Dockerfile 中的每个单独步骤所需的时间更少。
虽然这是一个简单的例子,但使用这种方法的好处应该是显而易见的。使用相对简单的配置文件,将镜像配置到所需状态的具体细节由配置管理工具处理。这并不意味着您可以忘记配置的细节;更改值将需要您理解语义以确保不会破坏任何东西。但这种方法可以为您节省大量时间和精力,尤其是在您不需要深入了解细节的项目中。
讨论
这种技术的目的是纠正关于 Dockerfile 概念的常见误解,特别是它与其他配置管理工具(如 Chef 和 Ansible)是竞争对手。
Docker 真正是(正如我们在本书的其他地方所说)一种打包工具。它允许您以可预测和打包的方式展示构建过程的结果。您如何选择构建它取决于您。您可以使用 Chef、Puppet、Ansible、Makefiles、shell 脚本,或者手动雕刻它们。
大多数人之所以不使用 Chef、Puppet 等工具来构建镜像,主要是因为 Docker 镜像往往被构建为单一用途和单一进程的工具。但如果你已经有了配置脚本,为什么不重用它们呢?
7.3. 小巧玲珑
如果你正在创建大量镜像并将它们发送到各个地方,镜像大小的问题更有可能出现。尽管 Docker 使用镜像分层可以帮助解决这个问题,但您可能拥有如此众多的镜像,这并不实用来管理。
在这些情况下,在你的组织中有关将镜像减小到尽可能小的最佳实践可能会有所帮助。在本节中,我们将向你展示一些这些实践,甚至如何将标准实用工具镜像减小一个数量级——一个更小的对象可以在你的网络中传输。
减小镜像大小的技巧
假设你从第三方那里获得了一个镜像,并且你想使镜像更小。最简单的方法是从一个可以工作的镜像开始,移除不必要的文件。
经典的配置管理工具通常不会移除任何东西,除非明确指示它们这样做——相反,它们从一个非工作状态开始,添加新的配置和文件。这导致了为特定目的而构建的 雪花 系统,这些系统可能与你在对全新服务器运行配置管理工具时得到的结果大相径庭,尤其是如果配置随着时间的推移而演变的话。得益于 Docker 中的分层和轻量级镜像,你可以执行这个过程的逆过程,并尝试移除某些内容。
问题
你希望使你的镜像更小。
解决方案
按照以下步骤通过移除不必要的包和文档文件来减小图像大小:
-
运行镜像。
-
进入容器。
-
移除不必要的文件。
-
将容器提交为新的镜像(参见技术 15)。
-
平滑化镜像(参见技术 52)。
书中已经介绍了最后两个步骤,所以这里我们只介绍前三个步骤。
为了说明如何操作,我们将使用技术 49 中创建的镜像,并尝试减小该镜像的大小。
首先,将镜像作为容器运行:
docker run -ti --name smaller --entrypoint /bin/bash \
dockerinpractice/log-cleaner
因为这是一个基于 Debian 的镜像,你可以先查看你可能不需要哪些包,并移除它们。运行 dpkg -l | awk '{print $2}' 将会得到系统上安装的包列表。
然后,你可以逐个对这些包运行 apt-get purge -y package_name 命令。如果出现警告信息“你即将执行可能有害的操作”,按回车键继续。
一旦你移除了所有可以安全移除的包,你可以运行以下命令来清理 apt 缓存:
apt-get autoremove
apt-get clean
这是一个相对安全的方式来减少镜像中的空间。
通过移除文档可以进一步显著节省空间。例如,运行 rm -rf /usr/share/doc/* /usr/share/man/* /usr/share/info/* 常常可以移除你很可能永远不需要的大文件。你可以通过手动运行 rm 命令来删除你不需要的二进制文件和库,将这一过程提升到下一个层次。
另一个可以大量节省空间的地方是 /var 文件夹,它应该包含临时数据,或者不是程序运行所必需的数据。
此命令将删除所有以 .log 后缀结尾的文件:
find /var | grep '\.log$' | xargs rm -v
现在,你将拥有一个比之前更小的镜像,准备好提交。
讨论
使用这个相对手动的过程,你可以轻松地将原始 dockerinpractice/ log-cleaner 镜像减小到几十 MB,如果你有动力,甚至可以使其更小。记住,由于 Docker 的分层,你需要像在 技术 52 中解释的那样导出和导入镜像;否则,镜像的整体大小将包括已删除的文件。
技术 59 将向您展示一种更有效(但风险更大)的方法,可以显著减小您镜像的大小。
小贴士
这里描述的命令的示例维护在 github.com/docker-in-practice/log-cleaner-purged,并且可以通过 Docker 从 dockerinpractice/log-cleaner-purged 拉取。
| |
| |
使用 BusyBox 和 Alpine 的微型 Docker 镜像
自从 Linux 开始,就存在可以嵌入到低功耗或廉价计算机上的小型、可用的操作系统。幸运的是,这些项目的努力已经重新用于生产用于重要尺寸的 Docker 镜像。
问题
你需要一个小巧、功能齐全的镜像。
解决方案
在构建自己的镜像时,使用小型基础镜像,如 BusyBox 或 Alpine。
这又是技术前沿快速变化的一个领域。两个流行的最小 Linux 基础镜像选择是 BusyBox 和 Alpine,它们各有不同的特点。
如果您追求精简但实用,BusyBox 可能符合您的需求。如果您使用以下命令启动 BusyBox 镜像,会发生一些令人惊讶的事情:
$ docker run -ti busybox /bin/bash
exec: "/bin/bash": stat /bin/bash: no such file or directory2015/02/23 >
09:55:38 Error response from daemon: Cannot start container >
73f45e34145647cd1996ae29d8028e7b06d514d0d32dec9a68ce9428446faa19: exec: >
"/bin/bash": stat /bin/bash: no such file or directory
BusyBox 非常精简,没有 bash!相反,它使用 ash,这是一个符合 POSIX 标准的 shell——实际上是一个比 bash 和 ksh 等更高级的 shell 有限的版本。
$ docker run -ti busybox /bin/ash
/ #
由于许多类似这样的决策,BusyBox 镜像的重量不到 2.5 MB。
警告
BusyBox 可能会包含一些其他令人惊讶的惊喜。例如,tar 版本将难以解包使用 GNU tar 创建的 TAR 文件。
如果您只想编写一个只需要简单工具的小脚本,这将非常棒,但如果您想运行其他任何东西,您将不得不自己安装它。BusyBox 不包含包管理。
其他维护者已经向 BusyBox 添加了包管理功能。例如,progrium/busybox 可能不是最小的 BusyBox 容器(它目前略小于 5 MB),但它有 opkg,这意味着你可以轻松地安装其他常见包,同时将镜像大小保持在绝对最小。例如,如果你缺少 bash,你可以这样安装:
$ docker run -ti progrium/busybox /bin/ash
/ # opkg-install bash > /dev/null
/ # bash
bash-4.3#
当提交时,这将产生一个 6 MB 的镜像。
另一个有趣的 Docker 镜像是 gliderlabs/alpine。它与 BusyBox 类似,但提供了更广泛的包,您可以在 pkgs.alpinelinux.org/packages 上浏览。
这些包被设计为在安装时保持精简。以一个具体的例子来说,以下是一个 Dockerfile,它生成的镜像大小略超过四分之一千兆字节。
列表 7.17. Ubuntu 加上 mysql-client
FROM ubuntu:14.04
RUN apt-get update -q \
&& DEBIAN_FRONTEND=noninteractive apt-get install -qy mysql-client \
&& apt-get clean && rm -rf /var/lib/apt
ENTRYPOINT ["mysql"]
小贴士
在 apt-get install 前面的 DEBIAN_FRONTEND=noninteractive 确保安装过程中不会提示任何输入。由于在运行命令时无法轻易设计对问题的响应,这在 Dockerfile 中通常很有用。
相比之下,以下列表的结果是一个略大于 36 MB 的镜像。
列表 7.18. Alpine 加上 mysql-client
FROM gliderlabs/alpine:3.6
RUN apk-install mysql-client
ENTRYPOINT ["mysql"]
讨论
这是在过去几年中发展迅速的一个领域。Alpine 基础镜像已经超越了 BusyBox,在 Docker 标准中占据了一席之地,这得益于 Docker Inc. 的支持。
此外,其他更“标准”的基础镜像也在进行瘦身。当我们准备本书的第二版时,Debian 镜像大约有 100 MB,比最初要小得多。
这里值得注意的一点是,关于减少镜像大小或使用更小的基础镜像的讨论很多,但这并不是需要解决的问题。记住,通常最好的做法是花时间和精力克服现有的瓶颈,而不是追求可能效果甚微的理论效益。
| |
最小容器 Go 模型
虽然通过删除冗余文件来精简工作容器可以提供启示,但还有一个选项——编译没有依赖项的最小二进制文件。
这样做极大地简化了配置管理的任务——如果只有一个文件要部署且不需要任何包,那么大量的配置管理工具就变得多余了。
问题
你想要构建没有外部依赖的二进制 Docker 镜像。
解决方案
构建一个静态链接的二进制文件——在它启动运行时不会尝试加载任何系统库。
为了展示这如何有用,我们首先创建一个包含小型 C 程序的 Hello World 镜像。然后我们将展示如何为更有用的应用程序做类似的事情。
最小 Hello World 二进制文件
要创建最小的 Hello World 二进制文件,首先创建一个新的目录和一个 Dockerfile,如下所示。
列表 7.19. Hello Dockerfile
FROM gcc *1*
RUN echo 'int main() { puts("Hello world!"); }' > hi.c *2*
RUN gcc -static hi.c -w -o hi *3*
-
1 gcc 镜像是为编译设计的。
-
2 创建一个简单的单行 C 程序
-
3 使用 -static 标志编译程序,并使用 -w 抑制警告
上述 Dockerfile 编译了一个没有依赖项的简单 Hello World 程序。你现在可以构建它,并从容器中提取那个二进制文件,如下一个列表所示。
列表 7.20. 从镜像中提取二进制文件
$ docker build -t hello_build . *1*
$ docker run --name hello hello_build /bin/true *2*
$ docker cp hello:/hi hi *3*
$ docker rm hello *4*
hello *4*
$ docker rmi hello_build *4*
Deleted: 6afcbf3a650d9d3a67c8d67c05a383e7602baecc9986854ef3e5b9c0069ae9f2
$ mkdir -p new_folder *5*
$ mv hi new_folder *6*
$ cd new_folder *7*
-
1 构建包含静态链接“hi”二进制的镜像
-
2 使用一个简单的命令运行镜像以复制出二进制文件
-
3 使用 docker cp 命令复制“hi”二进制文件
-
4 清理:你不再需要这些了
-
5 创建一个名为“new_folder”的新文件夹
-
6 将“hi”二进制文件移动到这个文件夹
-
7 更改目录到这个新文件夹
你现在在新的目录中有一个静态构建的二进制文件,并且已经移动到该目录。
现在创建另一个 Dockerfile,如下列所示。
列表 7.21. 最小 Hello Dockerfile
FROM scratch *1*
ADD hi /hi *2*
CMD ["/hi"] *3*
-
1 使用零字节 scratch 镜像
-
2 将“hi”二进制文件添加到镜像
-
3 默认镜像运行“hi”二进制文件
按照以下列表所示构建和运行它。
列表 7.22. 创建最小容器
$ docker build -t hello_world .
Sending build context to Docker daemon 931.3 kB
Sending build context to Docker daemon
Step 0 : FROM scratch
--->
Step 1 : ADD hi /hi
---> 2fe834f724f8
Removing intermediate container 01f73ea277fb
Step 2 : ENTRYPOINT /hi
---> Running in 045e32673c7f
---> 5f8802ae5443
Removing intermediate container 045e32673c7f
Successfully built 5f8802ae5443
$ docker run hello_world
Hello world!
$ docker images | grep hello_world
hello_world latest 5f8802ae5443 24 seconds ago 928.3 kB
镜像构建、运行,总大小不到 1 MB。
一个最小的 Go 网络服务器镜像
这只是一个相对简单的例子,但你也可以将同样的原则应用到用 Go 编写的程序中。Go 语言的一个有趣特性是构建这样的静态二进制文件相对容易。
为了展示这一能力,我们创建了一个简单的 Go 语言网络服务器,其代码可在 github.com/docker-in-practice/go-web-server 找到。
构建此简单网络服务器的 Dockerfile 如下列所示。
列表 7.23. 用于静态编译 Go 网络服务器的 Dockerfile
FROM golang:1.4.2 *1*
RUN CGO_ENABLED=0 go get \ *2*
-a -ldflags '-s' -installsuffix cgo \ *3*
github.com/docker-in-practice/go-web-server *4*
CMD ["cat","/go/bin/go-web-server"] *5*
-
1 这个构建已知可以与 golang 镜像的这个版本号兼容;如果构建失败,可能是因为这个版本已经不再可用。
-
2 “go get”从提供的 URL 获取源代码并在本地编译。CGO_ENABLED 环境变量设置为 0 以防止交叉编译。
-
3 为 Go 编译器设置一系列杂项标志以确保静态编译并减小大小
-
4 Go 网络服务器源代码仓库
-
5 默认生成的镜像输出可执行文件
如果你将这个 Dockerfile 保存到一个空目录中并构建它,你现在将拥有一个包含程序的镜像。因为你指定了镜像的默认命令以输出可执行内容,你现在只需运行镜像并将输出发送到主机上的文件,如下列所示。
列表 7.24. 从镜像获取 Go 网络服务器
$ docker build -t go-web-server . *1*
$ mkdir -p go-web-server && cd go-web-server *2*
$ docker run go-web-server > go-web-server *3*
$ chmod +x go-web-server *4*
$ echo Hi > page.html *5*
-
1 构建并标记镜像
-
2 创建一个新目录以存放二进制文件并移动到该目录
-
3 运行镜像并将二进制输出重定向到文件
-
4 使二进制文件可执行
-
5 为服务器创建一个网页以提供服务
现在,就像“hi”二进制文件一样,你有一个没有库依赖或需要访问文件系统的二进制文件。因此,我们将从零字节 scratch 镜像创建一个 Dockerfile 并将其添加到其中,就像之前一样。
列表 7.25. Go 网络服务器 Dockerfile
FROM scratch
ADD go-web-server /go-web-server *1*
ADD page.html /page.html *2*
ENTRYPOINT ["/go-web-server"] *3*
-
1 将静态二进制文件添加到镜像
-
2 添加一个网页供网络服务器服务
-
3 将二进制文件设置为图像默认运行的程序
现在构建它并运行图像。生成的图像大小略大于 4 MB。
列表 7.26. 构建和运行 Go 网络服务器图像
$ docker build -t go-web-server .
$ docker images | grep go-web-server
go-web-server latest de1187ee87f3 3 seconds ago 4.156 MB
$ docker run -p 8080:8080 go-web-server -port 8080
您可以通过 http://localhost:8080 访问它。如果端口已被占用,您可以将前面代码中的 8080 替换为您选择的端口。
讨论
如果您可以将应用程序捆绑到一个二进制文件中,为什么还要使用 Docker 呢?您可以移动二进制文件,运行多个副本,等等。
如果您想这样做,可以,但您将失去以下内容:
-
Docker 生态系统中的所有容器管理工具
-
Docker 镜像中包含重要应用程序信息的元数据,例如端口、卷和标签
-
给予 Docker 操作能力的隔离性
作为具体示例,etcd 默认是一个静态二进制文件,但当我们在技术 74 中检查它时,我们将演示它在容器内,以便更容易地看到相同的过程如何在多台机器上工作,并简化部署。
| |
使用 inotifywait 精简容器
我们现在将通过使用一个巧妙的工具来将我们的容器精简到下一级别,这个工具会告诉我们当运行容器时正在引用哪些文件。
这可以被称为核选项,因为它在生产环境中实施可能会有相当大的风险。但即使您不真正使用它,它也可以是一种了解您系统的有益方式——配置管理的一个关键部分是了解您的应用程序需要什么才能正确运行。
问题
您希望将容器减少到可能的最小文件和权限集。
解决方案
使用 inotify 监控程序访问的文件,然后删除任何看起来未使用的文件。
在高层次上,您需要知道在容器中运行命令时哪些文件被访问。如果您从容器文件系统中删除所有其他文件,理论上您仍然会拥有所需的一切。
在本教程中,我们将使用来自技术 56 的 log-cleaner-purged 图像。您将安装 inotify-tools,然后运行inotifywait来获取关于哪些文件被访问的报告。然后,您将运行图像的入口点(log_clean 脚本)的模拟。接着,使用生成的文件报告,您将删除任何未被访问的文件。
列表 7.27. 在使用 inotifywait 监控的同时执行手动安装步骤
[host]$ docker run -ti --entrypoint /bin/bash \ *1*
--name reduce dockerinpractice/log-cleaner-purged *2*
$ apt-get update && apt-get install -y inotify-tools *3*
$ inotifywait -r -d -o /tmp/inotifywaitout.txt \ *4*
/bin /etc /lib /sbin /var *5*
inotifywait[115]: Setting up watches. Beware: since -r was given, this >
may take a while!
inotifywait[115]: Watches established.
$ inotifywait -r -d -o /tmp/inotifywaitout.txt /usr/bin /usr/games \ *6*
/usr/include /usr/lib /usr/local /usr/sbin /usr/share /usr/src
inotifywait[118]: Setting up watches. Beware: since -r was given, this >
may take a while!
inotifywait[118]: Watches established.
$ sleep 5 *7*
$ cp /usr/bin/clean_log /tmp/clean_log *8*
$ rm /tmp/clean_log *8*
$ bash *9*
$ echo "Cleaning logs over 0 days old" *9*
$ find /log_dir -ctime "0" -name '*log' -exec rm {} \; *9*
$ awk '{print $1$3}' /tmp/inotifywaitout.txt | sort -u > \
/tmp/inotify.txt *10*
$ comm -2 -3 \ *11*
<(find /bin /etc /lib /sbin /var /usr -type f | sort) \
<(cat /tmp/inotify.txt) > /tmp/candidates.txt
$ cat /tmp/candidates.txt | xargs rm *12*
$ exit *13*
$ exit *13*
-
1 覆盖此图像的默认入口点
-
2 为容器赋予一个您稍后可以引用的名字
-
3 安装 inotify-tools 包
-
4 以递归(-r)和守护进程(-d)模式运行
inotifywait以获取在 outfile(使用-o 标志指定)中访问的文件列表 -
5 指定您感兴趣的文件夹。请注意,您不需要监控/tmp,因为如果监控它本身,/tmp/inotifywaitout.txt 将导致无限循环。
-
6 在/usr 文件夹的子文件夹上再次调用 inotifywait。/usr 文件夹中的文件太多,inotifywait 无法处理,因此您需要分别指定每个文件夹。
-
7 休眠以给 inotifywait 足够的时间启动
-
8 访问您需要使用的脚本文件以及 rm 命令,以确保它们被标记为已使用。
-
9 启动 bash shell,就像脚本执行时一样,并运行脚本中的命令。请注意,这将失败,因为我们没有从主机挂载任何实际的日志文件夹。
-
10 使用 awk 实用程序从 inotifywait 日志的输出中生成文件名列表,并将其转换为唯一且排序的列表
-
11 使用 comm 实用程序输出文件系统上未访问的文件列表
-
12 删除所有未访问的文件
-
13 退出您启动的 bash shell 然后是容器本身
到目前为止,您已经
-
对文件进行监控以查看哪些文件被访问
-
运行所有命令以模拟脚本的运行
-
运行命令以确保您访问到您肯定会需要的脚本,以及 rm 实用程序
-
获取了运行期间未访问的所有文件的列表
-
删除了所有未访问的文件
现在,您可以扁平化这个容器(参见技术 52)以创建一个新的镜像并测试它是否仍然工作。
列表 7.28. 扁平化镜像并运行它
$ ID=$(docker export reduce | docker import -) *1*
$ docker tag $ID smaller *2*
$ docker images | grep smaller
smaller latest 2af3bde3836a 18 minutes ago 6.378 MB *3*
$ mkdir -p /tmp/tmp *4*
$ touch /tmp/tmp/a.log *4*
$ docker run -v /tmp/tmp:/log_dir smaller \
/usr/bin/clean_log 0
Cleaning logs over 0 days old
$ ls /tmp/tmp/a.log *5*
ls: cannot access /tmp/tmp/a.log: No such file or directory
-
1 将镜像扁平化并将 ID 放入变量“ID”中
-
2 将新扁平化的镜像标记为“较小”
-
3 现在镜像的大小已经小于之前的 10%。
-
4 创建一个新的文件夹和文件以模拟测试的日志目录
-
5 在测试目录上运行新创建的镜像,并检查创建的文件是否已被删除
我们将这个镜像的大小从 96 MB 减少到大约 6.5 MB,并且它仍然看起来可以工作。节省了很多!
警告
这种技术,就像超频 CPU 一样,并不是对粗心大意者的优化。这个特定的例子之所以效果良好,是因为它是一个范围相当有限的应用程序,但您的关键业务应用程序可能更复杂和动态,在访问文件方面。您可能会轻易删除在运行期间未访问但可能在其他某个时刻需要的文件。
如果您对通过删除将来需要的文件而可能破坏镜像感到有些紧张,您可以使用/tmp/candidates.txt 文件来获取未更改的最大文件列表,如下所示:
cat /tmp/candidates.txt | xargs wc -c | sort -n | tail
您可以删除那些您确信应用不需要的大文件。这里同样可以取得显著的成效。
讨论
虽然这种技术被作为 Docker 技术提出,但它属于“通常有用”的技术类别,可以在其他环境中应用。它在调试你不太清楚发生了什么的过程时特别有用,你希望看到哪些文件被引用。strace是另一种执行此操作的方法,但inotifywait在某些方面是一个更容易使用的工具。
这种一般方法也被用作减少容器攻击面的一个攻击途径,在技术 97 的背景下。
| |
大可以很美
虽然本节是关于保持镜像小,但值得记住的是,小不一定更好。正如我们将讨论的,一个相对较大的单体镜像可能比一个小型镜像更有效率。
问题
你想要减少 Docker 镜像的磁盘空间使用和网络带宽。
解决方案
为你的组织创建一个通用、大型的单体基础镜像。
这看似矛盾,但一个大的单体镜像可能会节省磁盘空间和网络带宽。
记住,当 Docker 容器运行时,Docker 使用的是写时复制机制。这意味着你可能有数百个 Ubuntu 容器在运行,但每个启动的容器只使用少量的额外磁盘空间。
如果你你的 Docker 服务器上有许多不同的、较小的镜像,如图 7.1 所示,那么使用的磁盘空间可能比有一个包含所有所需内容的较大单体镜像要多。
图 7.1. 许多小型基础镜像与较少的大型基础镜像

你可能会想起共享库的原则。共享库可以被多个应用程序同时加载,从而减少运行所需程序所需的磁盘和内存量。同样,为你的组织创建一个共享的基础镜像可以节省空间,因为它只需要下载一次,并且应该包含你所需的一切。之前在多个镜像中需要的程序和库现在只需要一次。
此外,跨团队共享一个中央管理的单体镜像还可以带来其他好处。这个镜像的维护可以集中化,改进可以共享,构建过程中出现的问题只需要解决一次。
如果你打算采用这种技术,以下是一些需要注意的事项:
-
基础镜像首先应该是可靠的。如果它表现不一致,用户会避免使用它。
-
基础镜像的更改必须在用户可以查看的地方进行跟踪,以便用户可以自己调试问题。
-
回归测试对于更新基础镜像时减少混淆是必不可少的。
-
在基础镜像中添加的内容要小心,一旦它被添加到基础镜像中,就很难移除,镜像可能会迅速膨胀。
讨论
我们在我们的 600 人开发公司中有效地使用了这项技术。核心应用程序的每月构建被捆绑到一个大型镜像中,并发布到内部 Docker 注册库。团队默认基于所谓的“纯”企业镜像进行构建,如果需要,可以在其上创建定制层。
值得查看技术 12 以获取有关单体容器的一些额外细节——特别是关于 phusion/base 镜像 Docker 镜像的提及,这是一个考虑到运行多个进程而设计的镜像。
摘要
-
ENTRYPOINT是另一种启动 Docker 容器的方法,它允许在运行时配置参数。 -
通过扁平化镜像可以防止构建过程中的机密信息通过镜像层泄露。
-
可以使用 Alien 将不属于所选基础镜像发行版的软件包集成。
-
传统的构建工具,如
make,以及现代的如 Chef,在 Docker 世界中仍然有其位置。 -
可以通过使用更小的基础镜像、使用适合任务的编程语言或删除不必要的文件来减小 Docker 镜像的大小。
-
考虑到镜像的大小是否是您需要解决的最重要挑战是值得考虑的。
第三部分. Docker 和 DevOps
现在,你已经准备好将 Docker 超越你的开发环境,并开始在软件交付的其他阶段使用它。构建和测试自动化是 DevOps 运动的基石。我们将通过自动化软件交付生命周期、部署和现实环境测试来展示 Docker 的强大功能。
第八章 将展示各种交付和改进持续集成的技术,使你的软件交付更加可靠和可扩展。
第九章 专注于持续交付。我们将解释什么是持续交付,并探讨 Docker 可以如何用来改进你开发管道的这一方面。
第十章 展示了如何充分利用 Docker 的网络模型,创建多容器服务,模拟现实网络,以及按需创建网络。
本部分将带你从开发一直走到你可以考虑在生产环境中运行 Docker 的阶段。
第八章. 持续集成:加速你的开发管道
本章涵盖
-
使用 Docker Hub 工作流程作为 CI 工具
-
加速你的 I/O 重量级构建
-
使用 Selenium 进行自动化测试
-
在 Docker 中运行 Jenkins
-
使用 Docker 作为 Jenkins 从属节点
-
与你的开发团队一起扩展可用计算资源
在本章中,我们将探讨各种技术,这些技术将使用 Docker 来使你的持续集成(CI)工作得以实现和改进。
到现在为止,你应该已经理解 Docker 非常适合用于自动化。它的轻量级特性,以及它赋予你将环境从一个地方迁移到另一个地方的能力,可以使它成为 CI 的关键推动者。我们发现本章中的技术对于在商业环境中使 CI 过程可行非常有价值。
在本章结束时,你将了解 Docker 如何使 CI(持续集成)过程更快、更稳定、可重复。通过使用测试工具如 Selenium,以及使用 Jenkins Swarm 插件扩展你的构建能力,你将看到 Docker 如何帮助你从 CI 过程中获得更多。
注意
如果你不知道,持续集成 是一种软件生命周期策略,用于加速开发管道。通过在代码库中每次进行重大更改时自动重新运行测试,你可以获得更快、更稳定的交付,因为正在交付的软件有一个基本的稳定性水平。
8.1. Docker Hub 自动构建
Docker Hub 自动构建功能在技术 10 中提到,尽管我们没有详细介绍它。简而言之,如果您指向包含 Dockerfile 的 Git 仓库,Docker Hub 将处理构建镜像并使其可供下载的过程。任何 Git 仓库中的更改都会触发镜像重建,这使得它作为 CI 流程的一部分非常有用。
使用 Docker Hub 工作流程
这种技术将向您介绍 Docker Hub 工作流程,它使您能够触发镜像的重建。
备注
对于本节,您需要在 docker.com 上创建一个账户,并将其与 GitHub 或 Bitbucket 账户关联。如果您还没有设置并关联这些账户,可以在 github.com 和 bitbucket.org 的主页上找到说明。
问题
您希望在代码更改时自动测试并推送更改到您的镜像。
解决方案
设置 Docker Hub 仓库并将其与您的代码链接。
虽然 Docker Hub 构建并不复杂,但需要执行多个步骤:
-
在 GitHub 或 Bitbucket 上创建您的仓库。
-
克隆新的 Git 仓库。
-
向您的 Git 仓库添加代码。
-
提交源代码。
-
推送 Git 仓库。
-
在 Docker Hub 上创建一个新的仓库。
-
将 Docker Hub 仓库链接到 Git 仓库。
-
等待 Docker Hub 构建完成。
-
提交并推送源代码的更改。
-
等待第二次 Docker Hub 构建完成。
备注
Git 和 Docker 都使用“仓库”一词来指代一个项目。这可能会让人困惑。Git 仓库和 Docker 仓库不是同一回事,尽管在这里我们将这两种类型的仓库关联起来。
在 GitHub 或 Bitbucket 上创建您的仓库
在 GitHub 或 Bitbucket 上创建一个新的仓库。您可以给它任何您想要的名称。
克隆新的 Git 仓库
将您的新的 Git 仓库克隆到您的宿主机上。此命令将可在 Git 项目的主页上找到。
切换到该仓库的目录。
向您的 Git 仓库添加代码
现在您需要向项目中添加代码。
您可以添加任何喜欢的 Dockerfile,但以下列表显示了一个已知可以工作的示例。它由两个文件组成,代表一个简单的开发工具环境。它安装了一些首选的实用工具,并输出您拥有的 bash 版本。
列表 8.1. Dockerfile—简单的开发工具容器 Dockerfile
FROM ubuntu:14.04
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y curl *1*
RUN apt-get install -y nmap *1*
RUN apt-get install -y socat *1*
RUN apt-get install -y openssh-client *1*
RUN apt-get install -y openssl *1*
RUN apt-get install -y iotop *1*
RUN apt-get install -y strace *1*
RUN apt-get install -y tcpdump *1*
RUN apt-get install -y lsof *1*
RUN apt-get install -y inotify-tools *1*
RUN apt-get install -y sysstat *1*
RUN apt-get install -y build-essential *1*
RUN echo "source /root/bash_extra" >> /root/.bashrc *2*
ADD bash_extra /root/bash_extra *3*
CMD ["/bin/bash"]
-
1 安装有用的软件包
-
2 在根的 bashrc 中添加一行以源 bash_extra
-
3 将源代码中的 bash_extra 添加到容器中
现在您需要添加您所引用的 bash_extra 文件,并给它以下内容:
bash --version
此文件仅用于说明。它表明您可以在启动时创建一个 bash 文件。在这种情况下,它显示您在 shell 中使用的 bash 版本,但它可以包含所有各种设置您 shell 到您首选状态的东西。
提交源代码
要提交您的源代码,请使用以下命令:
git commit -am "Initial commit"
推送 Git 仓库
现在您可以使用以下命令将源代码推送到 Git 服务器:
git push origin master
在 Docker Hub 上创建一个新的仓库
接下来,您需要在 Docker Hub 上为该项目创建一个仓库。转到hub.docker.com,确保您已登录。然后点击创建并选择创建自动化构建。
第一次,您需要完成账户链接过程。您将看到一个提示,要求将您的账户链接到托管 Git 服务。选择您的服务并按照说明链接您的账户。您可能会被提供选择给予 Docker Inc.完全访问权限或更有限访问权限以进行集成的选项。如果您选择更有限的访问权限,您应该阅读您特定服务的官方文档,以确定您在剩余步骤中可能需要做哪些额外工作。
将 Docker Hub 仓库链接到 Git 仓库
您将看到一个选择 Git 服务的屏幕。选择您使用的源代码服务(GitHub 或 Bitbucket),并从提供的列表中选择您的新仓库。
您将看到一个带有构建配置选项的页面。您可以保留默认设置,然后在底部点击创建仓库。
等待 Docker Hub 构建完成
您将看到一个带有解释链接成功的消息的页面。点击构建详情链接。
接下来,您将看到一个显示构建详细信息的页面。在构建历史记录下,将有一个条目用于这个第一个构建。如果您没有看到任何列出内容,您可能需要点击按钮来手动触发构建。构建 ID 旁边的状态字段将显示为挂起、完成、构建中或错误。如果一切顺利,您将看到前三个状态之一。如果您看到错误,说明出了问题,您需要点击构建 ID 来查看错误详情。
注意
构建开始可能需要一些时间,所以在等待时看到挂起状态一段时间是完全正常的。
定期刷新,直到您看到构建已完成。一旦完成,您可以使用同一页面上列出的docker pull命令拉取镜像。
提交并推送源代码的更改
现在您决定在登录时想要更多关于您环境的信息,因此您想要输出您正在运行的发行版的详细信息。为了实现这一点,将这些行添加到您的 bash_extra 文件中,使其现在看起来像这样:
bash --version
cat /etc/issue
然后按照步骤 4 和 5 进行提交和推送。
等待第二个 Docker Hub 构建完成
如果您返回到构建页面,构建历史记录部分应该会显示一条新行,您可以像第 8 步一样跟踪此构建。
小贴士
如果您的构建出现错误(如果没有问题则不会收到邮件),您将会收到邮件,所以一旦您习惯了这种工作流程,您只需要在收到邮件时检查一下。
现在,你可以使用 Docker Hub 工作流程。你会很快习惯这个框架,并发现它对于保持构建更新和减少手动重建 Dockerfile 的认知负担非常有价值。
讨论
由于 Docker Hub 是镜像的规范来源,在你的 CI 流程中推送那里可以使一些事情变得更加简单(例如,向第三方分发镜像)。不需要自己运行构建过程更容易,并给你带来一些额外的优势,比如在 Docker Hub 的列表上有一个勾选标记,表明构建是在受信任的服务器上执行的。
对你的构建有额外的信心可以帮助你遵守 技术 70 中的 Docker 协议——在 技术 113 中,我们将探讨特定机器有时如何影响 Docker 构建,因此使用一个完全独立的系统对于提高最终结果的可信度是有益的。
8.2. 更高效的构建
CI 意味着更频繁地重建你的软件和测试。尽管 Docker 使交付 CI 更容易,但你可能遇到的下一个问题是计算资源上的负载增加。
我们将探讨在磁盘 I/O、网络带宽和自动化测试方面缓解这种压力的方法。
使用 eatmydata 加速 I/O 密集型构建
由于 Docker 非常适合自动化构建,随着时间的推移,你可能会执行大量的磁盘 I/O 密集型构建。Jenkins 作业、数据库重建脚本和大型代码签出都会对你的磁盘造成严重打击。在这些情况下,你会感激任何可以获得的加速,无论是为了节省时间还是为了最小化由于资源竞争而产生的许多开销。
这种技术已被证明可以提高高达 1:3 的速度,我们的经验也证实了这一点。这可不是小事情!
问题
你希望加速你的 I/O 密集型构建。
解决方案
eatmydata 是一个程序,它将你的系统调用写入数据,并通过绕过持久化这些更改所需的工作来使它们变得超级快。这涉及一些安全性的缺乏,因此不建议在常规使用中,但它对于设计用于不持久化的环境非常有用,例如在测试中。
安装 eatmydata
要在你的容器中安装 eatmydata,你有多种选择:
-
如果你运行的是基于 deb 的发行版,你可以使用
apt-get install安装它。 -
如果你运行的是基于 rpm 的发行版,你可以在网上搜索并下载它后使用
rpm --install安装它。例如,rpmfind.net 是一个很好的起点。 -
作为最后的手段,如果你已经安装了编译器,你可以直接下载并按照下面的列表进行编译。
列表 8.2. 编译和安装 eatmydata
$ url=https://www.flamingspork.com/projects/libeatmydata
/libeatmydata-105.tar.gz *1*
$ wget -qO- $url | tar -zxf - && cd libeatmydata-105 *2*
$ ./configure --prefix=/usr *3*
$ make *4*
$ sudo make install *5*
-
1 Flamingspork.com 是维护者的网站。
-
2 如果此版本未下载,请访问网站查看是否已更新到大于 105 的版本。
-
3 如果您想将 eatmydata 可执行文件安装到除/usr/bin 之外的其他位置,请更改前缀目录。
-
4 构建 eatmydata 可执行文件
-
5 安装软件;此步骤需要 root 权限
使用 eatmydata
一旦在您的镜像上安装了 libeatmydata(无论是从软件包还是从源代码),在执行任何命令之前运行 eatmydata 包装脚本,以利用它:
docker run -d mybuildautomation eatmydata /run_tests.sh
图 8.1 从高层次展示了 eatmydata 如何节省您处理时间。
图 8.1. 应用程序写入磁盘(顶部)和带有 eatmydata(底部)的情况

警告
eatmydata 跳过了确保数据安全写入磁盘的步骤,因此存在数据尚未写入磁盘而程序认为它已经写入磁盘的风险。对于测试运行,这通常无关紧要,因为数据是可丢弃的,但不要使用 eatmydata 来加速任何数据重要的环境!
请注意,运行eatmydata docker run ...以启动 Docker 容器,可能是在您的宿主机上安装 eatmydata 或挂载 Docker 套接字之后,由于第二章中概述的 Docker 客户端/服务器架构,这不会产生您可能期望的效果。相反,您需要在每个您想要使用 eatmydata 的容器内安装 eatmydata。
讨论
尽管具体用例可能会有所不同,但您应该能够立即应用的一个地方是技术 68。在 CI 作业的数据完整性通常并不重要——您通常只对成功或失败感兴趣,在失败的情况下,您通常只关心日志。
另一项相关的技术是技术 77。数据库是数据完整性真正非常重要的一个地方(任何流行的数据库都将设计为在机器电源丢失的情况下不会丢失数据),但如果你只是运行一些测试或实验,这将是您不需要的开销。
| |
为快速构建设置包缓存
由于 Docker 适合于开发、测试和生产中频繁重建服务,您可能会迅速达到一个需要反复大量访问网络的点。一个主要原因是下载来自互联网的包文件。这甚至可能在单台机器上也是一个缓慢(且昂贵)的开销。这项技术向您展示了如何设置本地缓存以用于您的包下载,包括 apt 和 yum。
问题
您希望通过减少网络 I/O 来加速您的构建。
解决方案
为您的包管理器安装一个 Squid 代理。图 8.2 说明了这项技术的工作原理。
图 8.2. 使用 Squid 代理缓存包

由于软件包的调用首先发送到本地 Squid 代理,并且只有在第一次请求时才通过互联网请求,因此对于每个软件包,应该只有一个互联网请求。如果你有数百个容器都从互联网下载相同的大型软件包,这可以为你节省大量时间和金钱。
注意
当你在主机上设置此配置时,可能会遇到网络配置问题。以下各节提供了建议,以确定是否出现这种情况,但如果你不确定如何进行,你可能需要寻求友好的网络管理员帮助。
Debian
对于 Debian(也称为 apt 或 .deb)软件包,设置更简单,因为有预包装版本。
在基于 Debian 的主机上运行以下命令:
sudo apt-get install squid-deb-proxy
通过 telnet 到端口 8000 确保服务已启动:
$ telnet localhost 8000
Trying ::1...
Connected to localhost.
Escape character is '^]'.
如果看到前面的输出,请按 Ctrl-] 然后按 Ctrl-d 退出。如果没有看到此输出,则 Squid 要么未正确安装,要么安装在了非标准端口。
要设置你的容器使用此代理,我们提供了以下示例 Dockerfile。请注意,从容器的角度来看,主机的 IP 地址可能会在每次运行时发生变化。因此,你可能希望在安装新软件之前将此 Dockerfile 转换为在容器内运行的脚本。
列表 8.3. 配置 Debian 镜像以使用 apt 代理
FROM debian
RUN apt-get update -y && apt-get install net-tools *1*
RUN echo "Acquire::http::Proxy \"http://$( \
route -n | awk '/⁰.0.0.0/ {print $2}' \ *2*
):8000\";" \ *3*
> /etc/apt/apt.conf.d/30proxy *4*
RUN echo "Acquire::http::Proxy::ppa.launchpad.net DIRECT;" >> \
/etc/apt/apt.conf.d/30proxy
CMD ["/bin/bash"]
-
1 确保路由工具已安装
-
2 为了确定容器视角下的主机 IP 地址,运行路由命令并使用 awk 从输出中提取相关 IP 地址(见技术 67)。
-
3 端口 8000 用于连接到主机上的 Squid 代理。
-
4 将带有适当 IP 地址和配置的回显行添加到 apt 的代理配置文件中。
Yum
在主机上,通过使用你的包管理器安装 squid 软件包来确保 Squid 已安装。
然后,你需要更改 Squid 配置以创建更大的缓存空间。打开 /etc/squid/squid.conf 文件,将开始的注释行 #cache_dir ufs /var/spool/squid 替换为以下内容:cache_dir ufs /var/spool/squid 10000 16 256。这会创建一个 10,000 MB 的空间,应该足够使用。
通过 telnet 到端口 3128 确保服务已启动:
$ telnet localhost 3128
Trying ::1...
Connected to localhost.
Escape character is '^]'.
如果看到前面的输出,请按 Ctrl-] 然后按 Ctrl-d 退出。如果没有看到此输出,则 Squid 要么未正确安装,要么安装在了非标准端口。
要设置你的容器使用此代理,我们提供了以下示例 Dockerfile。请注意,从容器的角度来看,主机的 IP 地址可能会在每次运行时发生变化。你可能希望在安装新软件之前将此 Dockerfile 转换为在容器内运行的脚本。
列表 8.4. 配置 CentOS 镜像以使用 yum 代理
FROM centos:centos7
RUN yum update -y && yum install -y net-tools *1*
RUN echo "proxy=http://$(route -n | \ *2*
awk '/⁰.0.0.0/ {print $2}'):3128" >> /etc/yum.conf *3*
RUN sed -i 's/^mirrorlist/#mirrorlist/' \
/etc/yum.repos.d/CentOS-Base.repo *4*
RUN sed -i 's/^#baseurl/baseurl/' \ *4*
/etc/yum.repos.d/CentOS-Base.repo *4*
RUN rm -f /etc/yum/pluginconf.d/fastestmirror.conf *5*
RUN yum update -y *6*
CMD ["/bin/bash"]
-
1 确保路由工具已安装
-
2 为了从容器的角度确定主机的 IP 地址,运行 route 命令并使用 awk 从输出中提取相关 IP 地址
-
3 使用端口 3128 连接到主机上的 Squid 代理。
-
4 尽可能避免缓存未命中,移除镜像列表并仅使用基础 URL。这确保你只击中一组 URL 来获取软件包,因此你更有可能击中缓存文件。
-
5 移除 fastestmirror 插件,因为它不再需要。
-
6 确保镜像被检查。当运行 yum update 时,配置文件中列出的镜像可能包含过时的信息,因此第一次更新可能会很慢。
如果你以这种方式设置两个容器并在两个容器中依次安装相同的大型软件包,你应该会注意到第二个安装比第一个下载其依赖项要快得多。
讨论
你可能已经观察到你可以在容器上而不是在主机上运行 Squid 代理。这里没有展示这个选项是为了保持解释简单(在某些情况下,需要更多步骤才能使 Squid 在容器中工作)。你可以阅读更多关于此内容,以及如何使容器自动使用代理的信息,请参阅github.com/jpetazzo/squid-in-a-can。
| |
容器中的无头 Chrome
运行测试是 CI 的关键部分,大多数单元测试框架都可以在 Docker 中无问题运行。但有时需要更复杂的测试,从确保多个微服务正确协作到确保网站前端功能仍然正常。访问网站前端需要某种类型的浏览器,因此为了解决这个问题,我们需要一种方法在容器内启动浏览器,然后对其进行程序化控制。
问题
你想在容器内测试 Chrome 浏览器,而不需要 GUI。
解决方案
在镜像中使用 Puppeteer Node.js 库来自动化 Chrome 操作。
这个库由 Google Chrome 开发团队维护,它允许你针对测试目的编写针对 Chrome 的脚本。它是“无头”的,这意味着你不需要 GUI 就可以与之工作。
注意
此镜像也由我们在 GitHub 上维护github.com/docker-in-practice/docker-puppeteer。它也可以作为 Docker 镜像通过 docker pull dockerinpractice/docker-puppeteer 获取。
以下列表显示了一个 Dockerfile,它将创建一个包含所有启动 Puppeteer 所需内容的镜像。
列表 8.5. Puppeteer Dockerfile
FROM ubuntu:16.04 *1*
RUN apt-get update -y && apt-get install -y \ *2*
npm python-software-properties curl git \ *2*
libpangocairo-1.0-0 libx11-xcb1 \ *2*
libxcomposite1 libxcursor1 libxdamage1 \ *2*
libxi6 libxtst6 libnss3 libcups2 libxss1 \ *2*
libxrandr2 libgconf-2-4 libasound2 \ *2*
libatk1.0-0 libgtk-3-0 vim gconf-service \ *2*
libappindicator1 libc6 libcairo2 libcups2 \ *2*
libdbus-1-3 libexpat1 libfontconfig1 libgcc1 \ *2*
libgdk-pixbuf2.0-0 libglib2.0-0 libnspr4 \ *2*
libpango-1.0-0 libstdc++6 libx11-6 libxcb1 \ *2*
libxext6 libxfixes3 libxrender1 libxtst6 \ *2*
ca-certificates fonts-liberation lsb-release \ *2*
xdg-utils wget *2*
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - *3*
RUN apt-get install -y nodejs *4*
RUN useradd -m puser *5*
USER puser *6*
RUN mkdir -p /home/puser/node_modules *7*
ENV NODE_PATH /home/puppeteer/node_modules *8*
WORKDIR /home/puser/node_modules *9*
RUN npm i webpack *10*
RUN git clone https://github.com/GoogleChrome/puppeteer *11*
WORKDIR /home/puser/node_modules/puppeteer *12*
RUN npm i . *13*
WORKDIR /home/puser/node_modules/puppeteer/examples *14*
RUN perl -p -i -e \ *15*
"s/puppeteer.launch\(\)/puppeteer.launch({args: ['--no-sandbox']})/" **15*
CMD echo 'eg: node pdf.js' && bash *15*
-
1 以 Ubuntu 基础镜像开始
-
2 安装所有必需的软件。这是大多数用于在容器中运行 Chrome 所需的显示库。
-
3 设置最新的 nodejs 版本
-
4 安装 Ubuntu 的 nodejs 软件包
-
5 创建一个非 root 用户,“puser”(库运行所需的用户)
-
6 创建一个 node modules 文件夹
-
7 将 NODE_PATH 环境变量设置为 node 模块文件夹
-
8 将当前工作目录设置为 node 模块路径
-
9 安装 webpack(Puppeteer 的依赖项)
-
10 克隆 Puppeteer 模块代码
-
11 进入 Puppeteer 代码文件夹
-
12 安装 Puppeteer NodeJS 库
-
13 进入 Puppeteer 示例文件夹
-
14 将 no-sandbox 参数添加到 Puppeteer 启动参数中,以克服在容器内运行时的安全设置
-
15 使用 bash 启动容器,并添加一个有用的 echo 命令
使用以下命令构建和运行此 Dockerfile:
$ docker build -t puppeteer .
然后运行它:
$ docker run -ti puppeteer
eg: node pdf.js
puser@03b9be05e81d:~/node_modules/puppeteer/examples$
您将看到一个终端和运行node pdf.js的建议。
pdf.js 文件包含一个简单的脚本,作为使用 Puppeteer 库可以做什么的示例。
列表 8.6. pdf.js
'use strict'; *1*
const puppeteer = require('puppeteer'); *2*
(async() => { *3*
const browser = await puppeteer.launch(); *4*
const page = await browser.newPage(); *5*
await page.goto(
'https://news.ycombinator.com', {waitUntil: 'networkidle'}
); *6*
await page.pdf({ *7*
path: 'hn.pdf', *7*
format: 'letter' *7*
}); *7*
await browser.close(); *8*
})(); *9*
-
1 以严格模式运行 JavaScript 解释器,这可以捕获技术上允许但常见的不安全操作
-
2 导入 Puppeteer 库
-
3 创建一个异步块,代码将在其中运行
-
4 使用
puppeteer.launch函数启动浏览器。代码在启动完成前暂停,使用await关键字 -
5 使用
newPage函数使浏览器等待页面(相当于浏览器标签页)可用 -
6 使用
page.goto函数打开 HackerNews 网站,并在继续之前等待没有网络流量 -
7 使用
page.pdf函数以信函格式创建当前标签页的 PDF,并调用文件hn.pdf -
8 关闭浏览器并等待终止完成
-
9 调用异步块返回的函数
Puppeteer 用户除了这个简单的示例之外,还有许多选项可用。本技术的范围不包括详细解释 Puppeteer API。如果您想更深入地了解 API 并调整此技术,请查看 GitHub 上的 Puppeteer API 文档:github.com/GoogleChrome/puppeteer/blob/master/docs/api.md。
讨论
这种技术展示了如何使用 Docker 来针对特定浏览器进行测试。
下一个技术以两种方式扩展了这一点:通过使用 Selenium,这是一个流行的测试工具,可以针对多个浏览器进行工作,并将其与一些 X11 的探索相结合,这样您就可以看到在图形窗口中运行的浏览器,而不是像本技术中使用的那种无头模式。
| |
在 Docker 内运行 Selenium 测试
我们还没有详细研究的一个 Docker 用例是运行图形应用程序。在第三章中,使用 VNC 连接到容器以在“保存游戏”的开发方法(技术 19)中,但这可能有些笨拙——窗口被包含在 VNC 观看器窗口内,桌面交互可能有点受限。我们将通过演示如何使用 Selenium 编写图形测试来探索这种方法的替代方案。我们还将向你展示如何使用此镜像作为 CI 工作流程的一部分来运行测试。
问题
你希望在 CI 流程中运行图形程序的同时,有选择地在自己的屏幕上显示这些相同的图形程序。
解决方案
将你的 X11 服务器套接字共享以在你的屏幕上查看程序,并在你的 CI 流程中使用 xvfb。
无论你需要做什么来启动你的容器,你都必须将 X11 用于显示窗口的 Unix 套接字作为卷挂载在容器内,并且你需要指出你的窗口应该显示在哪个显示上。你可以在你的主机上运行以下命令来双重检查这两件事是否设置为默认值:
~ $ ls /tmp/.X11-unix/
X0
~ $ echo $DISPLAY
:0
首个命令检查 X11 服务器 Unix 套接字是否在技术其余部分假设的位置运行。第二个命令检查应用程序使用的环境变量以查找 X11 套接字。如果你的这些命令的输出与这里的输出不匹配,你可能需要修改此技术中命令的一些参数。
现在你已经检查了你的机器设置,你需要确保容器内的应用程序能够无缝地显示在容器外部。你需要克服的主要问题是你的计算机为了防止其他人连接到你的机器、接管你的显示以及可能记录你的按键而设置的安全措施。在技术 29 中,你简要地看到了如何做到这一点,但我们没有讨论它是如何工作的或查看任何替代方案。
X11 有多种方式来验证容器以使用你的 X 套接字。首先,我们将查看 .Xauthority 文件——它应该存在于你的家目录中。它包含主机名以及每个主机必须使用的“秘密饼干”以连接。通过给你的 Docker 容器分配与你的机器相同的主机名,并使用容器外相同的用户名,你可以使用现有的 .Xauthority 文件。
列表 8.7. 使用启用 Xauthority 的显示启动容器
$ ls $HOME/.Xauthority
/home/myuser/.Xauthority
$ docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix \
--hostname=$HOSTNAME -v $HOME/.Xauthority:$HOME/.Xauthority \
-it -e EXTUSER=$USER ubuntu:16.04 bash -c 'useradd $USER && exec bash'
允许 Docker 访问套接字的第二种方法是一个更直接的工具,但它存在安全问题,因为它禁用了 X 为您提供的所有保护。如果没有人能访问您的计算机,这可能是一个可接受的解决方案,但您应该始终首先尝试使用.Xauthority 文件。您可以通过运行xhost -(尽管这将锁定您的 Docker 容器)来在尝试以下步骤后再次保护自己:
列表 8.8. 使用 xhost 启用显示启动容器
$ xhost +
access control disabled, clients can connect from any host
$ docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix \
-it ubuntu:16.04 bash
在前面的列表中,第一行禁用了对 X 的所有访问控制,第二行运行了容器。请注意,您不需要设置主机名或挂载除了 X 套接字之外的内容。
一旦您启动了容器,就是时候检查它是否正常工作了。如果您选择.Xauthority 路径,可以通过运行以下命令来完成:
root@myhost:/# apt-get update && apt-get install -y x11-apps
[...]
root@myhost:/# su - $EXTUSER -c "xeyes"
或者,如果您选择 xhost 路径,可以使用以下略有不同的命令,因为您不需要以特定用户身份运行该命令:
root@ef351febcee4:/# apt-get update && apt-get install -y x11-apps
[...]
root@ef351febcee4:/# xeyes
这将启动一个经典的应用程序来测试 X 是否工作——xeyes。当您在屏幕上移动光标时,应该会看到眼睛跟随。请注意,(与 VNC 不同)应用程序集成到您的桌面中——如果您多次启动 xeyes,您会看到多个窗口。
是时候开始使用 Selenium 了。如果您以前从未使用过它,它是一个具有自动化浏览器操作能力的工具,通常用于测试网站代码——它需要一个图形显示来运行浏览器。尽管它最常与 Java 一起使用,但我们将使用 Python 来允许更多的交互性。
以下列表首先安装 Python、Firefox 和一个 Python 包管理器,然后使用 Python 包管理器安装 Selenium Python 包。它还下载了 Selenium 用于控制 Firefox 的“驱动”二进制文件。然后启动 Python REPL,并使用 Selenium 库创建 Firefox 实例。
为了简单起见,这里只涵盖 xhost 路径——要选择 Xauthority 路径,您需要为用户创建一个家目录,以便 Firefox 有地方保存其配置文件设置。
列表 8.9. 安装 Selenium 需求并启动浏览器
root@myhost:/# apt-get install -y python2.7 python-pip firefox wget
[...]
root@myhost:/# pip install selenium
Collecting selenium
[...]
Successfully installed selenium-3.5.0
root@myhost:/# url=https://github.com/mozilla/geckodriver/releases/download
/v0.18.0/geckodriver-v0.18.0-linux64.tar.gz
root@myhost:/# wget -qO- $url | tar -C /usr/bin -zxf -
root@myhost:/# python
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from selenium import webdriver
>>> b = webdriver.Firefox()
正如您可能已经注意到的,Firefox 已经启动并出现在您的屏幕上。
您现在可以尝试使用 Selenium。以下是一个针对 GitHub 的示例会话——您需要了解 CSS 选择器的基本知识才能理解这里发生的事情。请注意,网站经常更改,因此这个特定的片段可能需要修改才能正确工作:
>>> b.get('https://github.com/search')
>>> searchselector = '#search_form input[type="text"]'
>>> searchbox = b.find_element_by_css_selector(searchselector)
>>> searchbox.send_keys('docker-in-practice')
>>> searchbox.submit()
>>> import time
>>> time.sleep(2) # wait for page JS to run
>>> usersxpath = '//nav//a[contains(text(), "Users")]'
>>> userslink = b.find_element_by_xpath(usersxpath)
>>> userslink.click()
>>> dlinkselector = '.user-list-info a'
>>> dlink = b.find_elements_by_css_selector(dlinkselector)[0]
>>> dlink.click()
>>> mlinkselector = '.meta-item a'
>>> mlink = b.find_element_by_css_selector(mlinkselector)
>>> mlink.click()
这里详细的内容并不重要,尽管您可以通过在命令之间切换到 Firefox 来了解正在发生的事情——我们正在导航到 GitHub 上的 docker-in-practice 组织,并点击组织链接。主要的收获是我们正在容器中用 Python 编写命令,并看到它们在容器内运行的 Firefox 窗口中生效,但它们在桌面上可见。
这对于调试你编写的测试非常棒,但你是如何将它们集成到具有相同 Docker 镜像的 CI 管道中的呢?CI 服务器通常没有图形显示,所以你需要在不挂载自己的 X 服务器套接字的情况下使它工作。但 Firefox 仍然需要一个 X 服务器来运行。
有一个有用的工具叫做 xvfb,它模拟了一个 X 服务器的运行,供应用程序使用,但不需要显示器。
为了看看这是如何工作的,我们将安装 xvfb,提交容器,将其标记为selenium,并创建一个测试脚本:
列表 8.10. 创建 Selenium 测试脚本
>>> exit()
root@myhost:/# apt-get install -y xvfb
[...]
root@myhost:/# exit
$ docker commit ef351febcee4 selenium
d1cbfbc76790cae5f4ae95805a8ca4fc4cd1353c72d7a90b90ccfb79de4f2f9b
$ cat > myscript.py << EOF
from selenium import webdriver
b = webdriver.Firefox()
print 'Visiting github'
b.get('https://github.com/search')
print 'Performing search'
searchselector = '#search_form input[type="text"]'
searchbox = b.find_element_by_css_selector(searchselector)
searchbox.send_keys('docker-in-practice')
searchbox.submit()
print 'Switching to user search'
import time
time.sleep(2) # wait for page JS to run
usersxpath = '//nav//a[contains(text(), "Users")]'
userslink = b.find_element_by_xpath(usersxpath)
userslink.click()
print 'Opening docker in practice user page'
dlinkselector = '.user-list-info a'
dlink = b.find_elements_by_css_selector(dlinkselector)[99]
dlink.click()
print 'Visiting docker in practice site'
mlinkselector = '.meta-item a'
mlink = b.find_element_by_css_selector(mlinkselector)
mlink.click()
print 'Done!'
EOF
注意dlink变量赋值的微妙差异(索引位置为99而不是0)。通过尝试获取包含文本“Docker in Practice”的第 100 个结果,你会触发一个错误,这将导致 Docker 容器以非零状态退出,并触发 CI 管道中的失败。
是时候尝试一下了:
$ docker run --rm -v $(pwd):/mnt selenium sh -c \
"xvfb-run -s '-screen 0 1024x768x24 -extension RANDR'\
python /mnt/myscript.py"
Visiting github
Performing search
Switching to user search
Opening docker in practice user page
Traceback (most recent call last):
File "myscript.py", line 15, in <module>
dlink = b.find_elements_by_css_selector(dlinkselector)[99]
IndexError: list index out of range
$ echo $?
1
你已经运行了一个自我删除的容器,它在一个虚拟 X 服务器下执行 Python 测试脚本。不出所料,它失败了,并返回了一个非零退出代码。
注意
sh -c "command string here"是 Docker 默认处理CMD值的不幸结果。如果你用 Dockerfile 构建了这个镜像,你就可以移除sh -c,并将xvfb-run -s '-screen 0 1024x768x24 -extension RANDR'作为 entrypoint,这样你就可以通过镜像参数传递测试命令。
讨论
Docker 是一个灵活的工具,可以用于一些最初令人惊讶的用途(在这种情况下是图形应用程序)。有些人甚至将所有图形应用程序都在 Docker 中运行,包括游戏!
我们不会走那么远(技术 40 确实考虑了至少为你的开发工具做这件事),但我们发现重新审视对 Docker 的假设可以导致一些令人惊讶的使用案例。例如,附录 A 讨论了在安装 Docker for Windows 后,在 Windows 上运行图形 Linux 应用程序。
8.3. 容器化你的 CI 流程
一旦你在团队间建立了一致的开发流程,也重要的是要有一个一致的建设流程。随机失败的构建会抵消 Docker 的作用。
因此,将整个 CI 流程容器化是有意义的。这不仅确保了你的构建是可重复的,还允许你将 CI 流程移动到任何地方,而不用担心会遗漏一些重要的配置(很可能会在后来的沮丧中找到)。
在这些技术中,我们将使用 Jenkins(因为这是最广泛使用的 CI 工具),但同样的技术也适用于其他 CI 工具。我们这里不假设对 Jenkins 有很高的熟悉度,但也不会涵盖设置标准测试和构建的内容。这些信息对这里的技术不是必需的。
在 Docker 容器中运行 Jenkins 主节点
将 Jenkins 主节点放在容器内并不像为从节点做同样的事情那样有那么多好处(参见下一技术),但它确实提供了不可变镜像的正常 Docker 优势。我们发现,能够提交已知良好的主节点配置和插件,可以显著减轻实验的负担。
问题
你需要一个可移植的 Jenkins 服务器。
解决方案
使用官方 Jenkins Docker 镜像来运行你的服务器。
在 Docker 容器中运行 Jenkins 为你提供了比直接主机安装更多的优势。在我们办公室,经常听到“别动我的 Jenkins 服务器配置!”或者更糟糕的是,“谁动了我的 Jenkins 服务器?”的抱怨,而能够通过docker export运行中的容器状态来克隆 Jenkins 服务器的状态,以便进行升级和更改的实验,有助于平息这些抱怨。同样,备份和迁移也变得更容易。
在这个技术中,我们将使用官方的 Jenkins Docker 镜像并进行一些修改,以方便后续需要访问 Docker 套接字的技术,例如从 Jenkins 中执行 Docker 构建。
注意
本书中的 Jenkins 相关示例可在 GitHub 上找到:git clone https://github.com/docker-in-practice/jenkins.git。
| |
注意
本书中的 Jenkins 相关技术将使用此 Jenkins 镜像及其run命令作为服务器。
构建服务器
我们首先准备一个我们想要的服务器插件列表,并将其放置在一个名为 jenkins_plugins.txt 的文件中:
swarm:3.4
这个非常短的列表包括 Jenkins 的 Swarm 插件(与 Docker Swarm 无关),我们将在后续技术中使用它。
以下列表显示了构建 Jenkins 服务器的 Dockerfile。
列表 8.11. Jenkins 服务器构建
FROM jenkins *1*
COPY jenkins_plugins.txt /tmp/jenkins_plugins.txt *2*
RUN /usr/local/bin/plugins.sh /tmp/jenkins_plugins.txt *3*
USER root *4*
RUN rm /tmp/jenkins_plugins.txt *4*
RUN groupadd -g 999 docker *5*
RUN addgroup -a jenkins docker *5*
USER jenkins *6*
-
1 使用官方 Jenkins 镜像作为基础
-
2 复制要安装的插件列表
-
3 将插件运行到服务器中
-
4 切换到 root 用户并删除插件文件
-
5 将具有与主机机器相同组 ID 的 Docker 组添加到容器中(你的数字可能不同)
-
6 切换回容器中的 Jenkins 用户
没有给出CMD或ENTRYPOINT指令,因为我们希望继承官方 Jenkins 镜像中定义的启动命令。
Docker 在你的主机机器上的组 ID 可能不同。要查看你的 ID,请运行以下命令以查看本地组 ID:
$ grep -w ^docker /etc/group
docker:x:999:imiell
如果不同,请替换该值。
警告
如果你计划在 Jenkins Docker 容器内运行 Docker,Jenkins 服务器环境和你的从节点环境中的组 ID 必须匹配。如果你选择移动服务器(在本地服务器安装中会遇到相同的问题),也可能存在潜在的便携性问题。环境变量本身无法帮助解决这个问题,因为组需要在构建时设置,而不是动态配置。
在此场景下构建镜像,请运行以下命令:
docker build -t jenkins_server .
运行服务器
现在,您可以使用此命令在 Docker 下运行服务器:
docker run --name jenkins_server -p 8080:8080 \ *1*
-p 50000:50000 \ *2*
-v /var/run/docker.sock:/var/run/docker.sock \ *3*
-v /tmp:/var/jenkins_home \ *4*
-d \ *5*
jenkins_server
-
1 将 Jenkins 服务器端口 8080 打开到主机
-
2 如果您想附加 Jenkins“构建从属节点”服务器,容器上需要打开 50000 端口。
-
3 挂载 Docker 套接字,以便您可以从容器内与 Docker 守护进程交互
-
4 将 Jenkins 应用程序数据挂载到主机机器/tmp,这样您就不会遇到文件权限错误。如果您在生产环境中使用此功能,请考虑运行它时挂载一个任何用户都可写入的文件夹。
-
5 以守护进程模式运行服务器
如果您访问 http://localhost:8080,您将看到 Jenkins 配置界面——按照流程进行,可能需要使用docker exec(在技术 12 中描述)来检索在第一步中提示的密码。
一旦完成,您的 Jenkins 服务器将准备就绪,您的插件已经安装(以及一些其他插件,具体取决于您在设置过程中选择的选项)。要检查此,请转到“管理 Jenkins”>“管理插件”>“已安装”,并查找 Swarm 以验证它是否已安装。
讨论
您会看到我们像在技术 45 中做的那样,将 Docker 套接字挂载到这个 Jenkins 主节点上,从而提供对 Docker 守护进程的访问。这允许您通过在主机上运行容器来使用内置的主从节点执行 Docker 构建。
注意
此技术及相关技术的代码可在 GitHub 上找到:github.com/docker-in-practice/jenkins。
| |
| |
包含复杂开发环境
Docker 的可移植性和轻量级特性使其成为 CI 从属节点(CI 主节点连接以执行构建的机器)的明显选择。Docker CI 从属节点是 VM 从属节点(甚至是从裸机构建机器的更大飞跃)。它允许您在单个主机上执行多种环境下的构建,快速拆解和建立干净的环境以确保不受污染的构建,并使用所有熟悉的 Docker 工具来管理您的构建环境。
能够将 CI 从属节点视为另一个 Docker 容器特别有趣。您在某个 Docker CI 从属节点上遇到神秘的构建失败吗?拉取镜像并尝试自行构建。
问题
您想缩放和修改您的 Jenkins 从属节点。
解决方案
使用 Docker 将您的从属节点配置封装在 Docker 镜像中,并部署。
许多组织设置了一个重型 Jenkins 从属节点(通常与服务器在同一主机上),由中央 IT 功能维护,在一段时间内发挥了有用的作用。随着时间的推移,团队扩大了代码库并发生了分歧,对安装、更新或更改更多软件的需求也随之增长,以便作业可以运行。
图 8.3 展示了这个场景的简化版本。想象一下,数百个软件包和多个新的请求都给过载的基础设施团队带来了头疼。
图 8.3. 过载的 Jenkins 服务器

注意
这种技术已被构建来向您展示在容器中运行 Jenkins 从属节点的关键要素。这使得结果不太便携,但更容易掌握。一旦您理解了本章中所有技术,您将能够创建一个更便携的设置。
已知会出现僵局,因为系统管理员可能不愿意更新他们的配置管理脚本,因为他们担心会破坏另一组的构建,而且团队对变化的缓慢越来越感到沮丧。
Docker(自然地)通过允许多个团队使用他们自己的 Jenkins 从属节点的基镜像,同时使用之前相同的硬件,提供了一个解决方案。您可以在上面创建包含所需共享工具的镜像,并允许团队根据他们的需求对其进行修改。
一些贡献者已在 Docker Hub 上上传了他们自己的参考从属节点;您可以通过在 Docker Hub 上搜索“jenkins slave”来找到它们。以下列表是一个最小的 Jenkins 从属节点 Dockerfile。
列表 8.12. 纯粹的 Jenkins 从属节点 Dockerfile
FROM ubuntu:16.04
ENV DEBIAN_FRONTEND noninteractive
RUN groupadd -g 1000 jenkins_slave *1*
RUN useradd -d /home/jenkins_slave -s /bin/bash \ *1*
-m jenkins_slave -u 1000 -g jenkins_slave *1*
RUN echo jenkins_slave:jpass | chpasswd *2*
RUN apt-get update && apt-get install -y \
openssh-server openjdk-8-jre wget iproute2 *3*
RUN mkdir -p /var/run/sshd *4*
CMD ip route | grep "default via" \ *4*
| awk '{print $3}' && /usr/sbin/sshd -D
-
1 创建 Jenkins 从属用户和组
-
2 将 Jenkins 用户密码设置为“jpass”。在更复杂的设置中,您可能希望使用其他认证方法。
-
3 安装所需的软件以作为 Jenkins 从属节点运行。
-
4 在启动时,从容器的角度输出主机的 IP 地址,并启动 SSH 服务器
构建从属节点镜像,标记为 jenkins_slave:
$ docker build -t jenkins_slave .
使用以下命令运行它:
$ docker run --name jenkins_slave -ti -p 2222:22 jenkins_slave
172.17.0.1
Jenkins 服务器需要运行
如果您的主机上还没有运行 Jenkins 服务器,请使用之前的技术设置一个。如果您急于完成,请运行以下命令:
$ docker run --name jenkins_server -p 8080:8080 -p 50000:50000 \
dockerinpractice/jenkins:server
如果您在本地机器上运行它,这将使 Jenkins 服务器在 http://localhost:8080 上可用。您在使用它之前需要完成设置过程。
如果您导航到 Jenkins 服务器,您将看到图 8.4 所示的页面。
图 8.4. Jenkins 主页

您可以通过点击“构建执行器状态”>“新建节点”并添加节点名称作为永久代理来添加从属节点,如图 8.5 所示。将其命名为 mydockerslave。
图 8.5. 命名新节点页面

点击“确定”,并使用这些设置进行配置,如图 8.6 所示:
-
将“远程根目录”设置为/home/jenkins_slave。
-
给它一个标签“dockerslave”。
-
确保选中“通过 SSH 启动从节点代理”选项。
-
将主机设置为容器内看到的路由 IP 地址(使用之前的
docker run命令输出)。 -
点击“添加”以添加凭据,并将用户名设置为“jenkins_slave”,密码设置为“jpass”。现在从下拉列表中选择这些凭据。
-
将“主机密钥验证策略”设置为手动信任密钥验证策略,这将接受首次连接时的 SSH 密钥,或者设置为非验证验证策略,这将不执行 SSH 主机密钥检查。
-
点击“高级”以显示端口字段,并将其设置为 2222。
-
点击“保存”。
图 8.6. Jenkins 节点设置页面

现在点击进入新的从节点,并点击“启动从节点代理”(假设这不会自动发生)。一分钟后你应该能看到从节点代理被标记为在线。
通过点击左上角的 Jenkins 返回主页,然后点击“新建项目”。创建一个名为“test”的 Freestyle 项目,在“构建”部分,点击“添加构建步骤”>“执行 Shell”,命令为echo done。向上滚动,并选择“限制项目运行位置”并输入标签表达式“dockerslave”。你应该能看到“标签下的从节点”设置为 1,这意味着作业现在已链接到 Docker 从节点。点击“保存”以创建作业。
点击“立即构建”,然后点击左侧出现的构建链接“#1”。然后点击“控制台输出”,你应该在主窗口中看到如下输出:
Started by user admin
Building remotely on mydockerslave (dockerslave)
in workspace /home/jenkins_slave/workspace/test
[test] $ /bin/sh -xe /tmp/jenkins5620917016462917386.sh
+ echo done
done
Finished: SUCCESS
干得好!你已经成功创建了你的 Jenkins 从节点。
现在如果你想创建自己的定制从节点,你只需要修改从节点镜像的 Dockerfile 以符合你的口味,然后运行它而不是示例中的那个。
注意
该技术及相关技术的代码可在 GitHub 上找到,链接为github.com/docker-in-practice/jenkins。
讨论
这种技术引导你创建一个容器来充当虚拟机,类似于技术 12,但增加了 Jenkins 集成的复杂性。一个特别有用的策略是在容器内挂载 Docker 套接字并安装 Docker 客户端二进制文件,以便你可以执行 Docker 构建。有关挂载 Docker 套接字(用于不同目的)的信息,请参阅技术 45,有关安装详情,请参阅附录 A。
使用 Jenkins 的 Swarm 插件扩展 CI
能够重现环境是一个巨大的胜利,但你的构建能力仍然受限于你拥有的专用构建机器的数量。如果你想要利用 Docker 从节点的新发现灵活性在不同的环境中进行实验,这可能会变得令人沮丧。容量也可能因为更平凡的原因成为问题——你团队的成长!
问题
你希望你的 CI 计算能力与你的开发工作率同步扩展。
解决方案
使用 Jenkins 的 Swarm 插件和 Docker Swarm 从节点动态提供 Jenkins 从节点。
注意
这之前已经提到过,但在这里重复一遍:Jenkins 的 Swarm 插件与 Docker 的 Swarm 技术根本无关。它们是完全无关的两件事,碰巧使用了同一个词。它们可以在这里一起使用纯粹是巧合。
许多中小型企业都有一个 CI 模型,其中一个或多个 Jenkins 服务器被专门用于提供运行 Jenkins 作业所需的资源。这如图 8.7 所示。
图 8.7. 之前:Jenkins 服务器——对一个开发者来说可以,但无法扩展

这在一段时间内工作得很好,但随着 CI 流程变得更加嵌入式,容量限制通常会被达到。大多数 Jenkins 工作负载都是由源控制的提交触发的,因此随着更多开发者的提交,工作负载会增加。当忙碌的开发者不耐烦地等待他们的构建结果时,运营团队收到的投诉数量就会激增。
一个整洁的解决方案是拥有与提交代码的人数一样多的 Jenkins 从节点,如图 8.8 所示。
图 8.8. 之后:计算能力随团队扩展

在列表 8.13 中显示的 Dockerfile 创建了一个安装了 Jenkins Swarm 客户端插件的镜像,允许具有适当 Jenkins Swarm 服务器插件的 Jenkins 主节点连接并运行作业。它以与上一技术中正常 Jenkins 从节点 Dockerfile 相同的方式开始。
列表 8.13. Dockerfile
FROM ubuntu:16.04
ENV DEBIAN_FRONTEND noninteractive
RUN groupadd -g 1000 jenkins_slave
RUN useradd -d /home/jenkins_slave -s /bin/bash \
-m jenkins_slave -u 1000 -g jenkins_slave
RUN echo jenkins_slave:jpass | chpasswd
RUN apt-get update && apt-get install -y \
openssh-server openjdk-8-jre wget iproute2
RUN wget -O /home/jenkins_slave/swarm-client-3.4.jar \ *1*
https://repo.jenkins-ci.org/releases/org/jenkins-ci/plugins/swarm-client
/3.4/swarm-client-3.4.jar
COPY startup.sh /usr/bin/startup.sh *2*
RUN chmod +x /usr/bin/startup.sh *3*
ENTRYPOINT ["/usr/bin/startup.sh"] *4*
-
1 检索 Jenkins Swarm 插件
-
2 将启动脚本复制到容器中
-
3 将启动脚本标记为可执行
-
4 将启动脚本设置为默认运行的命令
以下列表是复制到前面 Dockerfile 中的启动脚本。
列表 8.14. startup.sh
#!/bin/bash
export HOST_IP=$(ip route | grep ^default | awk '{print $3}') *1*
export JENKINS_IP=${JENKINS_IP:-$HOST_IP} *2*
export JENKINS_PORT=${JENKINS_PORT:-8080} *3*
export JENKINS_LABELS=${JENKINS_LABELS:-swarm} *4*
export JENKINS_HOME=${JENKINS_HOME:-$HOME} *5*
echo "Starting up swarm client with args:"
echo "$@"
echo "and env:"
echo "$(env)"
set -x *6*
java -jar \ *7*
/home/jenkins_slave/swarm-client-3.4.jar \
-sslFingerprints '[]' \
-fsroot "$JENKINS_HOME" \ *8*
-labels "$JENKINS_LABELS" \ *9*
-master http://$JENKINS_IP:$JENKINS_PORT "$@" *10*
-
1 确定主机的 IP 地址
-
2 使用主机 IP 作为 Jenkins 服务器 IP,除非在此脚本的调用环境中设置了 JENKINS_IP
-
3 默认将 Jenkins 端口设置为 8080
-
4 将此从节点的 Jenkins 标签设置为“swarm”
-
5 默认将 Jenkins 主目录设置为 jenkins_slave 用户的家目录
-
6 将在此处运行的命令记录为脚本的输出部分
-
7 运行 Jenkins Swarm 客户端
-
8 将根目录设置为 Jenkins 主目录
-
9 设置标签以识别作业客户端
-
10 将 Jenkins 服务器设置为指向从属节点
大部分前面的脚本设置并输出了 Java 调用末尾的环境。Java 调用运行 Swarm 客户端,将运行它的机器转换为一个动态的 Jenkins 从属节点,该节点以-fsroot标志指定的目录为根,运行带有-labels标志的作业,并指向带有-master标志指定的 Jenkins 服务器。带有echo的行仅提供了有关参数和环境设置的调试信息。
构建和运行容器是一个简单的过程,只需运行现在应该熟悉的模式:
$ docker build -t jenkins_swarm_slave .
$ docker run -d --name \
jenkins_swarm_slave jenkins_swarm_slave \
-username admin -password adminpassword
username和password应该是具有创建从属节点权限的 Jenkins 实例上的账户——admin账户将工作,但您也可以为此目的创建另一个账户。
现在您已经在这台机器上设置了一个从属节点,您可以在其上运行 Jenkins 作业。按照常规设置 Jenkins 作业,但在“限制此项目可以运行的位置”部分添加swarm作为标签表达式(参见技术 67)。
警告
Jenkins 作业可能是一个繁琐的过程,而且它们运行可能会对笔记本电脑产生负面影响。如果作业很重,您可以适当地设置作业和 Swarm 客户端的标签。例如,您可能将一个作业的标签设置为 4CPU8G,并将其与在 4CPU 机器上运行且具有 8GB 内存的 Swarm 容器匹配。
此技术给出了一些关于 Docker 概念的指示。可预测且可移植的环境可以放置在多个主机上,减少昂贵服务器的负载,并将所需的配置减少到最低。
虽然这不是一个不考虑性能就无法推广的技术,但我们认为在这里有很大的空间将贡献开发者的计算机资源转化为一种游戏形式,在不需要昂贵的新硬件的情况下提高开发组织的效率。
讨论
您可以通过将其设置为所有您地产的 PC 上的监督系统服务来自动化此过程(参见技术 82)。
注意
该技术及相关技术的代码可在 GitHub 上找到,链接为github.com/docker-in-practice/jenkins。
| |
| |
安全升级容器化的 Jenkins 服务器
如果您在生产环境中使用 Jenkins 有一段时间,您会知道 Jenkins 经常发布更新以进行安全和功能更改。
在一个专用、非 Docker 化的主机上,这通常通过软件包管理为您管理。使用 Docker,升级的推理可能会稍微复杂一些,因为您可能已经将服务器与其数据分离。
问题
您希望可靠地升级您的 Jenkins 服务器。
解决方案
运行一个 Jenkins 更新器镜像,该镜像将处理 Jenkins 服务器的升级。
这种技术作为由多个部分组成的 Docker 镜像提供。
首先,我们将概述构建镜像的 Dockerfile。此 Dockerfile 从库 Docker 镜像(其中包含 Docker 客户端)中提取,并添加一个管理升级的脚本。
该镜像在 Docker 命令中运行,将主机上的 Docker 项目挂载,使其能够管理任何所需的 Jenkins 升级。
Dockerfile
我们从 Dockerfile 开始。
列表 8.15. Jenkins 升级器的 Dockerfile
FROM docker *1*
ADD jenkins_updater.sh /jenkins_updater.sh *2*
RUN chmod +x /jenkins_updater.sh *3*
ENTRYPOINT /jenkins_updater.sh *4*
-
1 使用 docker 标准库镜像
-
2 添加 jenkins_updater.sh 脚本(将在下文讨论)
-
3 确保 jenkins_updater.sh 脚本可执行
-
4 将镜像的默认入口点设置为 jenkins_updater.sh 脚本
上述 Dockerfile 将备份 Jenkins 的需求封装在一个可执行的 Docker 镜像中。它使用 docker 标准库镜像来获取一个 Docker 客户端在容器中运行。该容器将运行 列表 8.16 中的脚本,以管理主机上 Jenkins 所需的任何升级。
注意
如果你的 docker 守护进程版本与 docker Docker 镜像中的版本不同,你可能会遇到问题。请尝试使用相同的版本。
jenkins_updater.sh
这是管理容器内升级的 shell 脚本。
列表 8.16. 用于备份和重启 Jenkins 的 Shell 脚本
#!/bin/sh *1*
set -e *2*
set -x *3*
if ! docker pull jenkins | grep up.to.date *4*
then
docker stop jenkins *5*
docker rename jenkins jenkins.bak.$(date +%Y%m%d%H%M) *6*
cp -r /var/docker/mounts/jenkins_home \ *7*
/var/docker/mounts/jenkins_home.bak.$(date +%Y%m%d%H%M) *7*
docker run -d \ *8*
--restart always \ *9*
-v /var/docker/mounts/jenkins_home:/var/jenkins_home \ *10*
--name jenkins \ *11*
-p 8080:8080 \ *12*
jenkins *13*
fi
-
1 此脚本使用 sh shell(而不是 /bin/bash shell),因为 Docker 镜像上只有 sh 可用
-
2 确保如果脚本中的任何命令失败,则脚本将失败
-
3 将脚本中运行的所有命令记录到标准输出
-
4 仅在“docker pull jenkins”不输出“up to date”时触发
-
5 升级时,首先停止 Jenkins 容器
-
6 停止后,将 Jenkins 容器重命名为“jenkins.bak.”,后面跟着分钟数
-
7 将 Jenkins 容器镜像状态文件夹复制到备份
-
8 运行 Docker 命令启动 Jenkins,并以守护进程方式运行
-
9 设置 Jenkins 容器始终重启
-
10 将 Jenkins 状态卷挂载到主机文件夹
-
11 给容器命名为“jenkins”,以防止意外同时运行多个此类容器
-
12 将容器中的 8080 端口映射到主机的 8080 端口
-
13 最后,将要运行的 Jenkins 镜像名称传递给 Docker 命令
上述脚本尝试使用 docker pull 命令从 Docker Hub 拉取 jenkins。如果输出包含“up to date”短语,则 docker pull | grep ... 命令返回 true。但只有当输出中没有“up to date”时,你才希望升级。这就是为什么在 if 语句后面用 ! 符号取反的原因。
结果是,只有当你下载了“最新”的 Jenkins 图像的新版本时,if 块中的代码才会被触发。在这个块中,运行的 Jenkins 容器会被停止并重命名。你选择重命名而不是删除它,以防升级失败,你需要恢复到之前的版本。此外,包含 Jenkins 状态的主机挂载文件夹也会进行备份。
最后,使用 docker run 命令启动最新下载的 Jenkins 图像。
注意
根据个人喜好,你可能想要更改主机挂载文件夹或运行中的 Jenkins 容器的名称。
你可能会想知道这个 Jenkins 图像是如何连接到主机的 Docker 守护进程的。为了实现这一点,使用在 技术 66 中看到的方法运行图像。
jenkins-updater 图像调用
以下命令将执行 Jenkins 升级,使用之前创建的包含 shell 脚本的图像:
列表 8.17. 运行 Jenkins 更新器的 Docker 命令
docker run *1*
--rm \ *2*
-d \ *3*
-v /var/lib/docker:/var/lib/docker \ *4*
-v /var/run/docker.sock:/var/run/docker.sock \ *5*
-v /var/docker/mounts:/var/docker/mounts *6*
dockerinpractice/jenkins-updater *7*
-
1 docker run 命令
-
2 容器完成其工作后删除容器
-
3 在后台运行容器
-
4 将主机上的 docker 守护进程文件夹挂载到容器中
-
5 将主机上的 docker socket 挂载到容器中,以便在容器内运行 docker 命令
-
6 挂载主机上的 docker 挂载文件夹,其中存储 Jenkins 数据,以便 jenkins_updater.sh 脚本可以复制文件
-
7 指定 dockerinpractice/jenkins-updater 图像是将要运行的图像
自动化升级
以下一行命令使得在 crontab 中运行变得容易。我们在我们的家用服务器上运行这个命令。
0 * * * * docker run --rm -d -v /var/lib/docker:/var/lib/docker -v
/var/run/docker.sock:/var/run/docker.sock -v
/var/docker/mounts:/var/docker/mounts dockerinpractice/jenkins-updater
注意
前面的命令都在一行中,因为 crontab 如果前面有反斜杠,不会忽略换行符,这与 shell 脚本的行为不同。
最终结果是,单个 crontab 条目可以安全地管理 Jenkins 实例的升级,而无需你担心。
自动清理旧备份容器和卷挂载的任务留作读者的练习。
讨论
这种技术展示了我们在整本书中遇到的一些事情,这些事情可以应用于除了 Jenkins 之外的类似情境。
首先,它使用核心 docker 图像与主机上的 Docker 守护进程通信。其他可移植脚本可能被编写来以其他方式管理 Docker 守护进程。例如,你可能想要编写脚本来删除旧卷,或者报告守护进程的活动。
更具体地说,if 块模式可以用来在可用新图像时更新和重启其他图像。出于安全原因或进行小幅度升级而更新图像并不罕见。
如果您担心升级版本时的困难,也值得指出的是,您不需要采取“最新”的镜像标签(这项技术就是这样做的)。许多镜像有不同的标签,跟踪不同的版本号。例如,您的镜像exampleimage可能有一个exampleimage:latest标签,以及example-image:v1.1和exampleimage:v1标签。这些标签中的任何一个都可能随时更新,但:v1.1标签不太可能移动到新的版本,而:latest标签则更有可能移动到新的版本,比如与新的:v1.2标签相同(可能需要升级步骤)或甚至:v2.1标签,其中新的主要版本2表明可能对任何升级过程造成更大的破坏。
这种技术还概述了 Docker 升级的回滚策略。通过使用卷挂载将容器和数据分离可能会对任何升级的稳定性造成紧张。通过保留在服务正常工作时的旧容器和旧数据的副本,更容易从故障中恢复。
数据库升级和 Docker
数据库升级是一个特别的环境,其中稳定性问题至关重要。如果您想将数据库升级到新版本,您必须考虑升级是否需要更改数据库数据结构和存储。仅仅运行新版本的镜像作为容器并期望它工作是不够的。如果数据库足够智能,知道它看到的数据版本并可以相应地进行升级,那么情况会变得更加复杂。在这些情况下,您可能会更愿意进行升级。
许多因素会影响您的升级策略。您的应用程序可能可以容忍乐观的方法(正如您在这里看到的 Jenkins 示例),假设一切都会顺利,并在(不是如果)发生故障时做好准备。另一方面,您可能要求 100%的可用性,并且不能容忍任何类型的故障。在这种情况下,通常需要一个经过充分测试的升级计划,并且比仅运行docker pull更深入地了解平台(无论是否涉及 Docker)。
虽然 Docker 不能消除升级问题,但版本化镜像的不可变性可以使推理它们变得更加简单。Docker 还可以通过两种方式帮助您为故障做准备:在主机卷中备份状态,并使测试可预测状态变得更容易。您在管理和理解 Docker 所做之事时付出的代价可以给您在升级过程中带来更多的控制和确定性。
摘要
-
您可以使用 Docker Hub 工作流程在代码更改时自动触发构建。
-
通过使用 eatmydata 和包缓存可以显著加快构建速度。
-
通过使用代理缓存外部工件(如系统包)可以加快构建速度。
-
您可以在 Docker 内部运行 GUI 测试(如 Selenium)。
-
您的 CI 平台(如 Jenkins)本身也可以从容器中运行。
-
Docker CI 从属节点让你可以完全控制你的环境。
-
你可以使用 Docker 和 Jenkins 的 Swarm 插件将构建过程外包给整个团队。
第九章. 持续交付:与 Docker 原则完美契合
本章涵盖
-
开发者和运维之间的 Docker 合同
-
在不同环境之间手动控制构建可用性
-
在低带宽连接之间移动构建
-
在环境中集中配置所有容器
-
使用 Docker 实现零停机时间部署
一旦你确信所有的构建都在一致的 CI 过程中进行了质量检查,下一步合乎逻辑的做法就是开始考虑将每个好的构建部署给用户。这个目标被称为持续交付(CD)。
在本章中,我们将提到你的“CD 管道”——你的构建在“CI 管道”之后所经历的过程。有时这条分界线可能会变得模糊,但将 CD 管道视为当你有一个在构建过程中通过初始测试的最终镜像时开始。图 9.1 图 9.1 展示了镜像可能如何通过 CD 管道直到(希望)达到生产。
图 9.1. 典型的 CD 管道

值得重复的是,最后一点——CI 输出的镜像应该是最终的,并且在 CD 过程中不应被修改!Docker 通过不可变镜像和状态封装使这一点变得容易执行,因此使用 Docker 已经让你在 CD 道路上前进了一步。
当本章完成后,你将完全理解为什么 Docker 的不可变性使其成为你的 CD 策略的完美伙伴。通过这种方式,Docker 可以成为任何组织中任何 DevOps 策略的关键推动者。
9.1. 与 CD 管道中的其他团队互动
首先,我们将稍微退后一步,看看 Docker 如何改变开发和运维之间的关系。
软件开发中的一些最大挑战并非技术性的——根据角色和专长将人们分成团队是一种常见做法,但这也可能导致沟通障碍和孤立。拥有一个成功的 CD 管道需要所有团队在流程的所有阶段都参与进来,从开发到测试再到生产。为所有团队提供一个单一参考点可以通过提供结构来帮助缓解这种互动。
Docker 合同:减少摩擦
Docker 的一个目标是可以轻松地表达输入和输出,这些输入和输出与包含单个应用程序的容器相关。这可以在与其他人合作时提供清晰性——沟通是协作的重要组成部分,了解 Docker 如何通过提供一个单一参考点来简化事情,可以帮助你赢得 Docker 怀疑者。
问题
你希望合作团队的交付成果清晰且无歧义,以减少你的交付管道中的摩擦。
解决方案
使用Docker 合约来促进团队之间的清晰交付。
随着公司的规模扩大,他们经常发现,他们曾经拥有的扁平、精简的组织结构,其中关键人物“了解整个系统”,转变为一个更加结构化的组织,其中不同的团队有不同的责任和能力。我们在我们工作的组织中亲眼目睹了这一点。
如果不进行技术投资,随着团队之间的交付,摩擦可能会产生。关于日益复杂的系统、将发布版本“扔过墙”以及有缺陷的升级的抱怨变得司空见惯。越来越多的“嗯,在我们的机器上它运行得很好!”的叫声将引起所有相关人员的挫败感。图 9.2 提供了一个简化的但具有代表性的场景视图。
图 9.2. 之前:典型的软件工作流程

图 9.2 中的工作流程有许多问题,这些可能对你来说很熟悉。它们都归结为管理状态困难。测试团队可能在与运维团队设置不同的机器上测试某些内容。理论上,所有环境的更改都应该被仔细记录,当发现问题时应回滚,并保持一致性。不幸的是,商业压力和人类行为的现实通常与这一目标作对,导致环境漂移。
解决这个问题的现有方案包括虚拟机和 RPM。虚拟机可以通过向其他团队提供完整的机器表示来减少环境风险的范围。缺点是虚拟机是相对单一的整体,团队难以高效地操作。在另一端,RPM 提供了一种打包应用程序的标准方式,有助于在部署软件时定义依赖关系。但这并没有消除配置管理问题,而且使用由其他团队创建的 RPM 进行部署比使用在互联网上经过实战检验的 RPM 更容易出错。
Docker 合约
Docker 所能做到的是在团队之间提供一个清晰的分离线,其中 Docker 镜像既是边界也是交换的单位。我们称之为Docker 合约,如图 9.3 所示。
图 9.3. 之后:Docker 合约

使用 Docker,所有团队参考点变得更加清晰。而不是处理在不可复制的状态下蔓延的单一虚拟(或真实)机器,所有团队都在讨论相同的代码,无论是测试、生产还是开发。此外,数据和代码之间有一个清晰的分离,这使得更容易推断问题是由数据或代码的变化引起的。
由于 Docker 使用非常稳定的 Linux API 作为其环境,因此交付软件的团队在以他们喜欢的任何方式构建软件和服务方面拥有更多的自由,同时有信心它在各种环境中可以可预测地运行。这并不意味着你可以忽略它运行的环境,但它确实减少了环境差异导致问题的风险。
由于只有一个参考接触点,因此产生了各种操作效率。由于所有团队都能从一个已知的起点描述和重现问题,因此错误重现变得容易得多。升级成为负责交付变更的团队的责任。简而言之,状态由进行变更的人管理。所有这些好处都大大减少了通信开销,并允许团队继续他们的工作。这种减少的通信开销还可以帮助鼓励向微服务架构的转变。
这不是纯粹的理论优势:我们在一个拥有 500 多名开发者的公司中亲身体验了这种改进,并且这是 Docker 技术聚会上的常见讨论话题。
讨论
这种技术概述了一种策略,在你继续阅读本书的过程中,有助于确定其他技术如何适应这个新世界。例如,技术 76 描述了一种以与在生产系统中运行相同的方式运行基于微服务的应用程序的方法,消除了配置文件调整的来源。当你发现自己在不同环境中遇到外部 URL 或其他不变因素时,技术 85 将提供关于服务发现的信息——将配置文件散布变成单一真相来源的好方法。
9.2. 促进 Docker 镜像的部署
在尝试实施 CD 时遇到的第一问题是将构建过程的输出移动到适当的位置。如果你能够为 CD 管道的所有阶段使用单个注册库,这似乎解决了这个问题。但这也未涵盖 CD 的一个关键方面。
CD 背后的一个关键思想是构建提升:管道的每个阶段(用户验收测试、集成测试和性能测试)只有在前一个阶段成功后才能触发下一个阶段。使用多个注册库,你只需通过仅在构建阶段通过时在下一个注册库中提供它们,就可以确保只使用提升的构建。
我们将探讨几种在注册库之间移动镜像的方法,甚至探讨一种在没有注册库的情况下共享 Docker 对象的方法。
手动镜像注册库镜像
最简单的镜像镜像场景是你有一个与两个注册库都有高速连接的机器。这允许使用正常的 Docker 功能来执行镜像复制。
问题
你想在两个注册库之间复制一个镜像。
解决方案
手动使用 Docker 的标准拉取和推送命令来传输图片。
解决这个问题的方法包括:
-
从仓库拉取图片
-
重新标记图片
-
推送重新标记的图片
如果你有一个在 test-registry.company.com 的镜像,并且你想将其移动到 stage-registry.company.com,这个过程很简单。
列表 9.1. 从测试仓库到预发布仓库传输图片
$ IMAGE=mygroup/myimage:mytag
$ OLDREG=test-registry.company.com
$ NEWREG=stage-registry.company.com
$ docker pull $OLDREG/$MYIMAGE
[...]
$ docker tag -f $OLDREG/$MYIMAGE $NEWREG/$MYIMAGE
$ docker push $NEWREG/$MYIMAGE
$ docker rmi $OLDREG/$MYIMAGE
$ docker image prune -f
在这个过程中有三个重要点需要注意:
-
新镜像已被强制标记。这意味着任何在机器上(为了层缓存目的而留下)的具有相同名称的旧镜像将丢失图片名称,因此新镜像可以用所需的名称进行标记。
-
所有悬挂的图片都已经删除。尽管层缓存对于加快部署速度非常有用,但留下未使用的图片层会迅速耗尽磁盘空间。一般来说,随着时间的推移,旧层被使用的可能性越小,它们就越过时。
-
你可能需要使用
docker login登录到你的新仓库。
图片现在可以在新的仓库中使用,用于 CD 管道的后续阶段。
讨论
这种技术说明了关于 Docker 标记的一个简单点:标记本身包含有关它所属仓库的信息。
大多数情况下,用户看不到这个问题,因为他们通常从默认仓库(docker.io 上的 Docker Hub)拉取。当你开始使用仓库时,这个问题就会显现出来,因为你必须明确地用仓库位置标记仓库,以便将其推送到正确的端点。
| |
通过受限连接传输图片
即使有分层,推送和拉取 Docker 镜像也可能是一个带宽密集型的过程。在一个免费大带宽连接的世界里,这不会是问题,但有时现实迫使我们必须处理数据中心之间的低带宽连接或昂贵的带宽计费。在这种情况下,你需要找到一种更有效的方法来传输差异,否则你每天多次运行管道的理想 CD 将遥不可及。
理想解决方案是一个工具,它将减少镜像的平均大小,使其甚至比经典的压缩方法还要小。
问题
你想在两个机器之间通过低带宽连接复制一张图片。
解决方案
导出镜像,将其分割,传输块,然后在另一端导入重新组合的镜像。
要完成所有这些,我们首先需要介绍一个新工具:bup。它被创建为一个具有非常高效去重功能的备份工具——去重是指识别数据重复使用的地方,并且只存储一次。它在包含许多相似文件的存档上工作得特别出色,这恰好是 Docker 允许你导出镜像的格式。
对于这项技术,我们创建了一个名为 dbup(代表“Docker bup”)的镜像,这使得使用 bup 去重镜像变得更加容易。您可以在github.com/docker-in-practice/dbup找到其背后的代码。
作为演示,让我们看看从 ubuntu:14.04.1 镜像升级到 ubuntu:14.04.2 时,我们可以节省多少带宽。请注意,在实际操作中,您会在每个镜像的顶部有一系列层,Docker 会在底层层发生变化后完全重新传输这些层。相比之下,这项技术会识别出显著的相似性,并为您节省比以下示例中看到的更多的带宽。
第一步是拉取这两个镜像,以便我们可以看到通过网络传输了多少内容。
列表 9.2. 检查并保存两个 Ubuntu 镜像
$ docker pull ubuntu:14.04.1 && docker pull ubuntu:14.04.2
[...]
$ docker history ubuntu:14.04.1
IMAGE CREATED CREATED BY SIZE
ab1bd63e0321 2 years ago /bin/sh -c #(nop) CMD [/bin/bash] 0B
<missing> 2 years ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\... 1.9kB
<missing> 2 years ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/po... 195kB
<missing> 2 years ago /bin/sh -c #(nop) ADD file:62400a49cced0d7... 188MB
<missing> 4 years ago 0B
$ docker history ubuntu:14.04.2
IMAGE CREATED CREATED BY SIZE
44ae5d2a191e 2 years ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 2 years ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\... 1.9kB
<missing> 2 years ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/po... 195kB
<missing> 2 years ago /bin/sh -c #(nop) ADD file:0a5fd3a659be172... 188MB
$ docker save ubuntu:14.04.1 | gzip | wc -c
65973497
$ docker save ubuntu:14.04.2 | gzip | wc -c
65994838
每个镜像的底层(ADD)是大小的主要部分,您可以看到被添加的文件是不同的,因此您可以将其整个镜像大小视为推送新镜像时将传输的量。此外,请注意,Docker 注册表使用 gzip 压缩来传输层,因此我们在测量中包括了这一点(而不是从docker history获取大小)。在初始部署和后续部署中,都正在传输大约 65 MB。
为了开始,您需要两样东西——一个目录来存储 bup 用作存储的数据池,以及 dockerinpractice/dbup 镜像。然后,您可以继续将您的镜像添加到 bup 数据池中。
列表 9.3. 将两个 Ubuntu 镜像保存到 bup 数据池中
$ mkdir bup_pool
$ alias dbup="docker run --rm \
-v $(pwd)/bup_pool:/pool -v /var/run/docker.sock:/var/run/docker.sock \
dockerinpractice/dbup"
$ dbup save ubuntu:14.04.1
Saving image!
Done!
$ du -sh bup_pool
74M bup_pool
$ dbup save ubuntu:14.04.2
Saving image!
Done!
$ du -sh bup_pool
96M bup_pool
将第二张图片添加到 bup 数据池中,仅使大小增加了大约 20 MB。假设您在添加 ubuntu:14.04.1 后同步了文件夹到另一台机器(可能使用 rsync),再次同步文件夹时,只需传输 20 MB(而不是之前的 65 MB)。
然后,您需要加载另一端的镜像。
列表 9.4. 从 bup 数据池中加载镜像
$ dbup load ubuntu:14.04.1
Loading image!
Done!
在注册表之间传输的过程可能看起来像这样:
-
在 host1 上执行
docker pull -
在 host1 上执行
dbup save -
从 host1 到 host2 的
rsync -
在 host2 上执行
dbup load -
在 host2 上执行
docker push
这项技术打开了许多以前可能无法实现的可能性。例如,现在您可以重新排列和合并层,而无需担心通过低带宽连接传输所有新层需要多长时间。
讨论
即使遵循最佳实践并在最后阶段添加您的应用程序代码,bup 也可能有所帮助——它会识别出大部分代码没有变化,并且只将差异添加到数据池中。
数据池可以非常大,例如数据库文件,bup 可能会表现得非常好(如果你已经决定在容器内部使用技术 77,这意味着没有卷)。这实际上有些不寻常——数据库的导出和备份通常非常高效地进行增量传输,但实际的磁盘存储可以有很大差异,有时甚至会使像 rsync 这样的工具失效。除此之外,dbup 将你的镜像的完整历史记录放在你的指尖——无需存储三个完整的镜像副本以进行回滚。你可以随意从池中提取它们。不幸的是,目前还没有方法清理你不再需要的镜像池,所以你可能需要时不时地清理池。
虽然你可能目前看不到 dbup 的即时需求,但请记住它,以防你的带宽账单开始增长。
| |
将 Docker 对象作为 TAR 文件共享
TAR 文件是 Linux 上移动文件的经典方法。Docker 允许你在没有可用注册表且无法设置注册表的情况下创建 TAR 文件并手动传输。在这里,我们将向您展示这些命令的细节。
问题
你想要与他人共享镜像和容器,但没有可用的注册表。
解决方案
使用 docker export 或 docker save 创建 TAR 文件形式的工件,然后通过 SSH 使用 docker import 或 docker load 消费它们。
如果你随意使用这些命令,区分它们可能很难理解,所以让我们花点时间快速了解一下它们的功能。表 9.1 概述了这些命令的输入和输出。
表 9.1. 导出和导入与保存和加载
| 命令 | 创建? | 什么? | 从什么? |
|---|---|---|---|
| export | TAR 文件 | 容器文件系统 | 容器 |
| import | Docker 镜像 | 平坦文件系统 | TAR 文件 |
| save | TAR 文件 | Docker 镜像(带有历史记录) | 镜像 |
| load | Docker 镜像 | Docker 镜像(带有历史记录) | TAR 文件 |
前两个命令与平坦文件系统一起工作。docker export 命令输出构成容器状态的文件 TAR 文件。像 Docker 中的常规操作一样,运行中的进程的状态不会存储——只有文件。docker import 命令从一个 TAR 文件创建一个没有历史记录或元数据的 Docker 镜像。
这些命令不是对称的——你不能仅使用 import 和 export 从现有的容器创建一个容器。这种不对称性可能很有用,因为它允许你使用 docker export 将镜像导出到 TAR 文件,然后使用 docker import “丢失”所有层历史记录和元数据。这是技术 52 中描述的镜像扁平化方法。
如果你导出或保存到 TAR 文件,文件默认发送到 stdout,所以请确保你将其保存到如下文件:
docker pull debian:7:3
[...]
docker save debian:7.3 > debian7_3.tar
与刚刚创建的 TAR 文件类似,可以在网络上安全地传输(尽管你可能想先使用gzip进行压缩),其他人可以使用它们完整地导入镜像。如果你有权限,可以通过电子邮件或scp发送:
$ scp debian7_3.tar example.com:/tmp/debian7_3.tar
你可以更进一步,直接将镜像发送到其他用户的 Docker 守护进程——前提是你有权限。
列表 9.5. 通过 SSH 直接发送镜像
docker save debian:7.3 | \ *1*
ssh example.com \ *2*
docker load - *3*
-
1 提取 Debian 版本 7.3 的镜像并将其通过 ssh 命令管道传输
-
2 在远程机器上运行命令,例如 example.com
-
3 接受给定的 TAR 文件并创建包含所有历史的镜像。破折号表示 TAR 文件是通过标准输入传递的。
如果你想丢弃镜像的历史,可以使用import而不是load。
列表 9.6. 通过 SSH 直接传输 Docker 镜像,丢弃层
docker export $(docker run -d debian:7.3 true) | \
ssh example.com docker import
注意
与docker import不同,docker load不需要在末尾添加破折号来指示 TAR 文件是通过标准输入传递的。
讨论
你可能还记得技术 52 中的导出和导入过程,你看到了如何通过扁平化镜像来移除可能隐藏在下层的秘密。如果你将镜像传输给其他人,考虑到秘密可能在下层可访问的事实是值得注意的——意识到你在顶层镜像中删除了公钥,但在下层它仍然可用,可能会带来真正的麻烦,因为你应该将其视为已泄露并更改所有地方。
如果你发现自己经常使用这种技术进行镜像传输,可能值得花点时间设置自己的注册表,使事情不那么随意。技术 9。
9.3. 配置你的镜像以适应环境
如本章引言所述,持续交付(CD)的一个基石是“在所有地方做同样的事情”的概念。在没有 Docker 的情况下,这意味着构建一次部署工件并在所有地方使用它。在 Docker 化的世界中,这意味着在所有地方使用相同的镜像。
但环境并不完全相同——例如,外部服务可能有不同的 URL。对于“正常”应用程序,你可以使用环境变量来解决这个问题(前提是它们不容易应用于多台机器)。相同的解决方案也可以用于 Docker(明确传递变量),但 Docker 还有更好的方法来做这件事,并带来一些额外的优势。
使用 etcd 通知你的容器
Docker 镜像被设计成可以在任何地方部署,但部署后你通常会想添加一些额外的信息来影响应用程序运行时的行为。此外,运行 Docker 的机器可能需要保持不变,因此你可能需要一个外部信息源(这使得环境变量不太适用)。
问题
在运行容器时,您需要一个外部配置源。
解决方案
设置 etcd,一个分布式键/值存储,以存储您的容器配置。
etcd 存储信息片段,可以是多节点集群的一部分以实现容错。在这个技术中,您将创建一个 etcd 集群来存储您的配置,并使用 etcd 代理来访问它。
注意
etcd 存储的每个值都应该保持较小——小于 512 KB 是一个很好的经验法则;超过这个点,您应该考虑进行基准测试以验证 etcd 是否仍然按您期望的方式运行。这个限制不仅适用于 etcd。您应该在其他键/值存储(如 Zookeeper 和 Consul)中记住这一点。
因为 etcd 集群节点需要相互通信,所以第一步是确定您的外部 IP 地址。如果您打算在不同的机器上运行节点,您需要每个节点的外部 IP 地址。
列表 9.7. 确定本地机器的 IP 地址
$ ip addr | grep 'inet ' | grep -v 'lo$\|docker0$'
inet 192.168.1.123/24 brd 192.168.1.255 scope global dynamic wlp3s0
inet 172.18.0.1/16 scope global br-0c3386c9db5b
在这里,我们查找了所有 IPv4 接口,并排除了 LoopBack 和 Docker。该列表的第一行(该行上的第一个 IP 地址)是您需要的,它代表本地网络上的机器——如果您不确定,请从另一台机器上尝试 ping 它。
我们现在可以开始使用三个节点集群,所有节点都在同一台机器上运行。请注意以下参数——每行中公开和宣传的端口都会改变,集群节点和容器的名称也是如此。
列表 9.8. 设置三个节点的 etcd 集群
$ IMG=quay.io/coreos/etcd:v3.2.7
$ docker pull $IMG
[...]
$ HTTPIP=http://192.168.1.123 *1*
$ CLUSTER="etcd0=$HTTPIP:2380,etcd1=$HTTPIP:2480,etcd2=$HTTPIP:2580" *2*
$ ARGS="etcd"
$ ARGS="$ARGS -listen-client-urls http://0.0.0.0:2379" *3*
$ ARGS="$ARGS -listen-peer-urls http://0.0.0.0:2380" *4*
$ ARGS="$ARGS -initial-cluster-state new"
$ ARGS="$ARGS -initial-cluster $CLUSTER"
$ docker run -d -p 2379:2379 -p 2380:2380 --name etcd0 $IMG \
$ARGS -name etcd0 -advertise-client-urls $HTTPIP:2379 \
-initial-advertise-peer-urls $HTTPIP:2380
912390c041f8e9e71cf4cc1e51fba2a02d3cd4857d9ccd90149e21d9a5d3685b
$ docker run -d -p 2479:2379 -p 2480:2380 --name etcd1 $IMG \
$ARGS -name etcd1 -advertise-client-urls $HTTPIP:2479 \
-initial-advertise-peer-urls $HTTPIP:2480
446b7584a4ec747e960fe2555a9aaa2b3e2c7870097b5babe65d65cffa175dec
$ docker run -d -p 2579:2379 -p 2580:2380 --name etcd2 $IMG \
$ARGS -name etcd2 -advertise-client-urls $HTTPIP:2579 \
-initial-advertise-peer-urls $HTTPIP:2580
3089063b6b2ba0868e0f903a3d5b22e617a240cec22ad080dd1b497ddf4736be
$ curl -L $HTTPIP:2579/version
{"etcdserver":"3.2.7","etcdcluster":"3.2.0"}
$ curl -sSL $HTTPIP:2579/v2/members | python -m json.tool | grep etcd
"name": "etcd0", *5*
"name": "etcd1", *5*
"name": "etcd2", *5*
-
1 您的机器的外部 IP 地址
-
2 在集群定义中使用机器的外部 IP 地址,为节点提供与其他节点通信的方式。因为所有节点都将位于同一主机上,所以集群端口(用于连接到其他节点)必须不同。
-
3 处理客户端请求的端口
-
4 用于与其他集群节点通信的监听端口,对应于$CLUSTER 中指定的端口
-
5 当前连接到集群的节点
您现在已启动了集群,并从其中一个节点收到了响应。在前面的命令中,任何提到“对等节点”的内容都是控制 etcd 节点如何找到并相互通信的方式,而任何提到“客户端”的内容则定义了其他应用程序如何连接到 etcd。
让我们看看 etcd 的分布式特性是如何发挥作用的。
列表 9.9. 测试 etcd 集群的容错能力
$ curl -L $HTTPIP:2579/v2/keys/mykey -XPUT -d value="test key"
{"action":"set","node": >
{"key":"/mykey","value":"test key","modifiedIndex":7,"createdIndex":7}}
$ sleep 5
$ docker kill etcd2
etcd2
$ curl -L $HTTPIP:2579/v2/keys/mykey
curl: (7) couldn't connect to host
$ curl -L $HTTPIP:2379/v2/keys/mykey
{"action":"get","node": >
{"key":"/mykey","value":"test key","modifiedIndex":7,"createdIndex":7}}
在前面的代码中,您向您的 etcd2 节点添加了一个键,然后将其杀死。但 etcd 已自动将信息复制到其他节点,并且仍然能够提供信息。尽管前面的代码暂停了五秒钟,但 etcd 通常在不到一秒钟内就会复制(即使是在不同的机器上)。现在您可以自由地docker start etcd2使其再次可用——在此期间您所做的任何更改都会复制回它。
你可以看到数据仍然可用,但手动选择另一个节点来连接有点不方便。幸运的是,etcd 有一个解决方案——你可以以“代理”模式启动一个节点,这意味着它不会复制任何数据;相反,它将请求转发到其他节点。
列表 9.10. 使用 etcd 代理
$ docker run -d -p 8080:8080 --restart always --name etcd-proxy $IMG \
etcd -proxy on -listen-client-urls http://0.0.0.0:8080 \
-initial-cluster $CLUSTER
037c3c3dba04826a76c1d4506c922267885edbfa690e3de6188ac6b6380717ef
$ curl -L $HTTPIP:8080/v2/keys/mykey2 -XPUT -d value="t"
{"action":"set","node": >
{"key":"/mykey2","value":"t","modifiedIndex":12,"createdIndex":12}}
$ docker kill etcd1 etcd2
$ curl -L $HTTPIP:8080/v2/keys/mykey2
{"action":"get","node": >
{"key":"/mykey2","value":"t","modifiedIndex":12,"createdIndex":12}}
这现在让你有了一些自由去实验当超过一半的节点离线时 etcd 的行为。
列表 9.11. 使用超过一半节点下线的 etcd
$ curl -L $HTTPIP:8080/v2/keys/mykey3 -XPUT -d value="t"
{"errorCode":300,"message":"Raft Internal Error", >
"cause":"etcdserver: request timed out","index":0}
$ docker start etcd2
etcd2
$ curl -L $HTTPIP:8080/v2/keys/mykey3 -XPUT -d value="t"
{"action":"set","node": >
{"key":"/mykey3","value":"t","modifiedIndex":16,"createdIndex":16}}
当一半或更多的节点不可用时,etcd 允许读取但阻止写入。
你现在可以看到,在集群中的每个节点上启动一个 etcd 代理作为检索集中配置的“大使容器”是可能的,如下所示。
列表 9.12. 在大使容器中使用 etcd 代理
$ docker run -it --rm --link etcd-proxy:etcd ubuntu:14.04.2 bash
root@8df11eaae71e:/# apt-get install -y wget
root@8df11eaae71e:/# wget -q -O- http://etcd:8080/v2/keys/mykey3
{"action":"get","node": >
{"key":"/mykey3","value":"t","modifiedIndex":16,"createdIndex":16}}
小贴士
大使(ambassador)是一种所谓的“Docker 模式”,在 Docker 用户中有些流行。大使容器被放置在应用程序容器和某些外部服务之间,并处理请求。它类似于代理,但它内置了一些智能来处理特定情况的具体要求——就像现实生活中的大使一样。
一旦你在所有环境中运行了 etcd,在环境中创建一个机器只需启动它并链接到 etcd-proxy 容器——然后所有 CD 构建到该机器都将使用环境的正确配置。下一个技术将展示如何使用 etcd 提供的配置来驱动零停机时间升级。
讨论
上一节中展示的大使容器利用了技术 8 中引入的链接标志。正如那里所指出的,链接在 Docker 世界中已经有些不受欢迎,现在实现相同功能的一种更符合习惯的方式是在虚拟网络上的命名容器,这在技术 80 中有介绍。
拥有一个提供世界一致视图的关键/值服务器集群,比在多台机器上管理配置文件前进了一大步,并帮助你朝着实现技术 70 中描述的 Docker 合同迈进。
9.4. 升级运行中的容器
为了实现每天多次部署到生产环境的理想,在部署过程的最后一步——关闭旧应用程序并启动新应用程序——减少停机时间非常重要。如果每次切换都是一个小时的流程,那么每天部署四次就没有意义了!
由于容器提供了一个隔离的环境,许多问题已经得到了缓解。例如,你不必担心两个版本的应用程序使用相同的工 作目录并相互冲突,或者重新读取某些配置文件并获取新值而不需要重新启动使用新代码。
不幸的是,这有一些缺点——不再简单地在原地更改文件,因此软重启(用于获取配置文件更改)变得更加困难。因此,我们发现,无论你是更改几个配置文件还是数千行代码,始终执行相同的升级过程是一种最佳实践。
让我们看看一个升级过程,它将实现面向 Web 应用程序的零停机时间部署的黄金标准。
使用 confd 实现零停机时间切换
因为容器可以在主机上并排存在,所以移除一个容器并启动一个新的容器的简单切换方法可以在几秒钟内完成(并且它允许快速回滚)。
对于大多数应用程序来说,这可能会足够快,但对于启动时间较长或高可用性要求的应用程序,需要另一种方法。有时这可能是一个不可避免地复杂的流程,需要与应用程序本身进行特殊处理,但面向 Web 的应用程序有一个你可能首先考虑的选项。
问题
您需要能够以零停机时间升级面向 Web 的应用程序。
解决方案
在您的主机上使用 confd 与 nginx 结合进行两阶段切换。
Nginx 是一个非常受欢迎的 Web 服务器,它具有一个关键内置功能——它可以在不断开服务器连接的情况下重新加载配置文件。通过将其与 confd 结合,这是一个可以从中央数据存储(如 etcd)检索信息并相应地更改配置文件的工具,你可以更新 etcd 上的最新设置,并观察其他一切为你处理。
注意
Apache HTTP 服务器和 HAProxy 也都提供零停机时间重新加载,如果你有现有的配置专业知识,可以使用它们代替 nginx。
第一步是启动一个将作为旧应用程序运行的应用程序,你最终会更新它。Python 随 Ubuntu 一起提供,并内置了 Web 服务器,所以我们将用它作为示例。
列表 9.13. 在容器中启动简单的文件服务器
$ ip addr | grep 'inet ' | grep -v 'lo$\|docker0$'
inet 10.194.12.221/20 brd 10.194.15.255 scope global eth0
$ HTTPIP=http://10.194.12.221
$ docker run -d --name py1 -p 80 ubuntu:14.04.2 \
sh -c 'cd / && python3 -m http.server 80'
e6b769ec3efa563a959ce771164de8337140d910de67e1df54d4960fdff74544
$ docker inspect -f '{{.NetworkSettings.Ports}}' py1
map[80/tcp:[{0.0.0.0 32768}]]
$ curl -s localhost:32768 | tail | head -n 5
<li><a href="sbin/">sbin/</a></li>
<li><a href="srv/">srv/</a></li>
<li><a href="sys/">sys/</a></li>
<li><a href="tmp/">tmp/</a></li>
<li><a href="usr/">usr/</a></li>
HTTP 服务器已成功启动,我们使用了 inspect 命令的过滤器选项来提取有关主机上哪个端口映射到容器内部点的信息。
现在请确保 etcd 正在运行——这个技术假设你仍然在之前技术相同的工作环境中。这次你将使用 etcdctl(简称“etcd 控制器”)与 etcd 交互(而不是直接 curl etcd),以简化操作。
列表 9.14. 下载并使用 etcdctl Docker 镜像
$ IMG=dockerinpractice/etcdctl
$ docker pull dockerinpractice/etcdctl
[...]
$ alias etcdctl="docker run --rm $IMG -C \"$HTTPIP:8080\""
$ etcdctl set /test value
value
$ etcdctl ls
/test
这已下载了我们准备好的 etcdctl Docker 镜像,并设置了一个别名,始终连接之前设置的 etcd 集群。现在启动 nginx。
列表 9.15. 启动 nginx + confd 容器
$ IMG=dockerinpractice/confd-nginx
$ docker pull $IMG
[...]
$ docker run -d --name nginx -p 8000:80 $IMG $HTTPIP:8080
ebdf3faa1979f729327fa3e00d2c8158b35a49acdc4f764f0492032fa5241b29
这是我们之前准备的一个镜像,它使用 confd 从 etcd 检索信息并自动更新配置文件。我们传递的参数告诉容器它可以连接到哪个 etcd 集群。不幸的是,我们还没有告诉它在哪里可以找到我们的应用程序,所以日志中充满了错误。
让我们在 etcd 中添加适当的信息。
列表 9.16. 展示 nginx 容器的自动配置
$ docker logs nginx
Using http://10.194.12.221:8080 as backend
2015-05-18T13:09:56Z ebdf3faa1979 confd[14]: >
ERROR 100: Key not found (/app) [14]
2015-05-18T13:10:06Z ebdf3faa1979 confd[14]: >
ERROR 100: Key not found (/app) [14]
$ echo $HTTPIP
http://10.194.12.221
$ etcdctl set /app/upstream/py1 10.194.12.221:32768
10.194.12.221:32768
$ sleep 10
$ docker logs nginx
Using http://10.194.12.221:8080 as backend
2015-05-18T13:09:56Z ebdf3faa1979 confd[14]: >
ERROR 100: Key not found (/app) [14]
2015-05-18T13:10:06Z ebdf3faa1979 confd[14]: >
ERROR 100: Key not found (/app) [14]
2015-05-18T13:10:16Z ebdf3faa1979 confd[14]: >
ERROR 100: Key not found (/app) [14]
2015-05-18T13:10:26Z ebdf3faa1979 confd[14]: >
INFO Target config /etc/nginx/conf.d/app.conf out of sync
2015-05-18T13:10:26Z ebdf3faa1979 confd[14]: >
INFO Target config /etc/nginx/conf.d/app.conf has been updated
$ curl -s localhost:8000 | tail | head -n5
<li><a href="sbin/">sbin/</a></li>
<li><a href="srv/">srv/</a></li>
<li><a href="sys/">sys/</a></li>
<li><a href="tmp/">tmp/</a></li>
<li><a href="usr/">usr/</a></li>
etcd 的更新已被 confd 读取并应用到 nginx 配置文件中,允许你访问你的简单文件服务器。包含sleep命令是因为 confd 已被配置为每 10 秒检查更新。在幕后,运行在 confd-nginx 容器中的 confd 守护进程会轮询 etcd 集群中的更改,使用容器内的模板仅在检测到更改时重新生成 nginx 配置。
假设我们决定我们想要服务/etc 而不是/。现在我们将启动我们的第二个应用程序并将其添加到 etcd。因为我们将有两个后端,我们最终会从它们各自得到响应。\
列表 9.17. 使用 confd 为 nginx 设置两个后端 Web 服务
$ docker run -d --name py2 -p 80 ubuntu:14.04.2 \
sh -c 'cd /etc && python3 -m http.server 80'
9b5355b9b188427abaf367a51a88c1afa2186e6179ab46830715a20eacc33660
$ docker inspect -f '{{.NetworkSettings.Ports}}' py2
map[80/tcp:[{0.0.0.0 32769}]]
$ curl -s $HTTPIP:32769 | tail | head -n 5
<li><a href="udev/">udev/</a></li>
<li><a href="update-motd.d/">update-motd.d/</a></li>
<li><a href="upstart-xsessions">upstart-xsessions</a></li>
<li><a href="vim/">vim/</a></li>
<li><a href="vtrgb">vtrgb@</a></li>
$ echo $HTTPIP
http://10.194.12.221
$ etcdctl set /app/upstream/py2 10.194.12.221:32769
10.194.12.221:32769
$ etcdctl ls /app/upstream
/app/upstream/py1
/app/upstream/py2
$ curl -s localhost:8000 | tail | head -n 5
<li><a href="sbin/">sbin/</a></li>
<li><a href="srv/">srv/</a></li>
<li><a href="sys/">sys/</a></li>
<li><a href="tmp/">tmp/</a></li>
<li><a href="usr/">usr/</a></li>
$ curl -s localhost:8000 | tail | head -n 5
<li><a href="udev/">udev/</a></li>
<li><a href="update-motd.d/">update-motd.d/</a></li>
<li><a href="upstart-xsessions">upstart-xsessions</a></li>
<li><a href="vim/">vim/</a></li>
<li><a href="vtrgb">vtrgb@</a></li>
在前面的过程中,我们在将其添加到 etcd 之前检查了新容器是否正确启动(见图 9.4)。我们可以通过覆盖 etcd 中的/app/upstream/py1键来一步完成这个过程——这在你只需要一次只访问一个后端时也很有用。
图 9.4. 将 py2 容器添加到 etcd

在两阶段切换中,第二阶段是移除旧的后端和容器。
列表 9.18. 移除旧的上游地址
$ etcdctl rm /app/upstream/py1
PrevNode.Value: 192.168.1.123:32768
$ etcdctl ls /app/upstream
/app/upstream/py2
$ docker rm -f py1
py1
新的应用程序自行启动并运行!应用程序在任何时候都没有对用户不可用,也没有必要手动连接到 Web 服务器机器来重新加载 nginx。
讨论
confd 的使用不仅限于配置 Web 服务器:如果你有任何包含需要根据外部值更新的文本的文件,confd 就会介入——它是存储在磁盘上的配置文件和单点真相 etcd 集群之间一个有用的连接器。
如前所述的技术中提到的,etcd 不是为存储大值而设计的。也没有理由你必须与 confd 一起使用 etcd——对于最流行的键值存储,有大量的集成可用,所以如果你已经有了对你有效的东西,你可能不需要添加另一个移动部件。
在后续的技术 86 中,当我们查看在生产中使用 Docker 时,你会看到一个方法,如果你想要更新服务的后端服务器,可以避免手动更改 etcd。
摘要
-
Docker 为开发和运维团队之间的合同提供了一个很好的基础。
-
在注册表中移动镜像可以是控制构建在 CD 管道中进展的好方法。
-
Bup 比层更能有效地压缩镜像传输。
-
Docker 镜像可以作为 TAR 文件进行移动和共享。
-
etcd 可以作为环境的中央配置存储。
-
通过结合 etcd、confd 和 nginx 可以实现零停机时间部署。
第十章. 网络模拟:无需痛苦的现实环境测试
本章涵盖
-
掌握 Docker Compose
-
在麻烦的网络上进行应用程序测试
-
初探 Docker 网络驱动程序
-
为 Docker 主机之间无缝通信创建基础网络
作为您的 DevOps 工作流程的一部分,您可能会以某种方式使用网络。无论您是试图找出本地 memcache 容器在哪里,连接到外部世界,还是将运行在不同主机上的 Docker 容器连接起来,您迟早都会想要连接到更广泛的网络。
在阅读本章后,您将了解如何使用 Docker Compose 将容器作为一个单元进行管理,并通过使用 Docker 的虚拟网络工具来模拟和管理网络。本章是向编排和服务发现迈出的小小第一步——我们将在本书的第四部分(part 4)中更深入地探讨这些主题。
10.1. 容器通信:超越手动链接
在技术 8 中,您看到了如何使用链接连接容器,我们提到了容器依赖关系明确声明提供的优势。不幸的是,链接有许多缺点。启动每个容器时必须手动指定链接,容器必须按正确顺序启动,链接中不允许有循环,而且无法替换链接(如果容器死亡,所有依赖的容器都必须重新启动以重新创建链接)。除此之外,它们已被弃用!
Docker Compose 是目前最受欢迎的替代方案,用于替代之前涉及复杂链接设置的所有内容,我们现在将探讨它。
一个简单的 Docker Compose 集群
Docker Compose 最初是fig,这是一个现在已弃用的独立项目,旨在简化使用适当的参数(链接、卷和端口)启动多个容器的痛苦。Docker Inc.非常喜欢这个项目,因此他们收购了它,进行了改造,并以新的名称发布。
这种技术通过一个简单的 Docker 容器编排示例向您介绍 Docker Compose。
问题
您想要协调主机机器上连接的容器。
解决方案
使用 Docker Compose,这是一个用于定义和运行多容器 Docker 应用程序的工具。
核心思想是,而不是用复杂的 shell 脚本或 Makefile 连接容器启动命令,您声明应用程序的启动配置,然后使用单个简单命令启动应用程序。
注意
我们假设您已安装 Docker Compose——有关最新建议,请参阅官方说明 (docs.docker.com/compose/install)。
在这个技术中,我们将使用 echo 服务器和客户端尽可能简单。客户端每五秒向 echo 服务器发送熟悉的“Hello world!”消息,然后接收回消息。
提示
该技术的源代码可在 github.com/docker-in-practice/docker-compose-echo 找到。
以下命令为您创建一个目录,以便在创建服务器镜像时工作:
$ mkdir server
$ cd server
使用以下列表中的代码创建服务器 Dockerfile。
列表 10.1. Dockerfile——一个简单的 echo 服务器
.FROM debian
RUN apt-get update && apt-get install -y nmap *1*
CMD ncat -l 2000 -k --exec /bin/cat *2*
-
1 安装 nmap 包,它提供了此处使用的 ncat 程序
-
2 默认情况下,在启动镜像时运行 ncat 程序
-l 2000 参数指示 ncat 在端口 2000 上监听,-k 告诉它同时接受多个客户端连接,并在客户端关闭连接后继续运行,以便更多客户端可以连接。最后的参数 --exec /bin/cat 将使 ncat 为任何传入的连接运行 /bin/cat,并将通过连接传输的任何数据转发到正在运行的程序。
接下来,使用此命令构建 Dockerfile:
$ docker build -t server .
现在您可以设置发送消息到服务器的客户端镜像。创建一个新的目录,并将 client.py 文件和 Dockerfile 放在那里:
$ cd ..
$ mkdir client
$ cd client
我们将在下一列表中使用一个简单的 Python 程序作为 echo 服务器客户端。
列表 10.2. client.py——一个简单的 echo 客户端
import socket, time, sys *1*
while True:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) *2*
s.connect(('talkto',2000)) *3*
s.send('Hello, world\n') *4*
data = s.recv(1024) *5*
print 'Received:', data *6*
sys.stdout.flush() *7*
s.close() *8*
time.sleep(5) *9*
-
1 导入所需的 Python 包
-
2 创建一个套接字对象
-
3 使用套接字连接到端口 2000 上的“talkto”服务器
-
4 向套接字发送带有换行符的字符串
-
5 创建一个 1024 字节的缓冲区以接收数据,并在收到消息时将数据放入 data 变量
-
6 将接收到的数据打印到标准输出
-
7 清空标准输出缓冲区,以便您可以查看传入的消息
-
8 关闭套接字对象
-
9 等待 5 秒并重复
客户端的 Dockerfile 很简单。它安装 Python,添加 client.py 文件,并在启动时默认运行,如下所示列表。
列表 10.3. Dockerfile——一个简单的 echo 客户端
FROM debian
RUN apt-get update && apt-get install -y python
ADD client.py /client.py
CMD ["/usr/bin/python","/client.py"]
使用此命令构建客户端:
docker build -t client .
为了展示 Docker Compose 的价值,我们首先将手动运行这些容器:
docker run --name echo-server -d server
docker run --name client --link echo-server:talkto client
完成后,使用 Ctrl-C 退出客户端,并删除容器:
docker rm -f client echo-server
即使在这个简单的例子中,也可能出现很多问题:首先启动客户端会导致应用程序启动失败;忘记删除容器会在你尝试重启时导致问题;以及错误地命名容器会导致失败。随着你的容器及其架构变得更加复杂,这类编排问题只会增加。
Compose 通过将这些容器的启动和配置的编排封装在一个简单的文本文件中,为你管理启动和关闭命令的细节。
Compose 需要一个 YAML 文件。你可以在新目录中创建它:
cd ..
mkdir docker-compose
cd docker-compose
YAML 文件的内容如下所示。
列表 10.4. docker-compose.yml—Docker Compose echo 服务器和客户端 YAML 文件
version: "3" *1*
services:
echo-server: *2*
image: server *3*
expose: *4*
- "2000"
client: *2*
image: client *5*
links: *6*
- echo-server:talkto
-
1 此 Docker Compose 文件遵循规范的第 3 版。
-
2 运行服务的引用名称是它们的标识符:在这个例子中是 echo-server 和 client。
-
3 每个部分必须定义使用的镜像:在这个例子中是客户端和服务器镜像。
-
4 将 echo-server 的端口 2000 暴露给其他服务
-
5 每个部分必须定义使用的镜像:在这个例子中是客户端和服务器镜像。
-
6 定义了一个到 echo-server 的链接。客户端内的“talkto”引用将被发送到 echo 服务器。映射是通过在运行容器中动态设置/etc/hosts 文件来完成的。
docker-compose.yml 的语法相当容易理解:每个服务都在services键下命名,配置在缩进的部分中声明。每个配置项的名称后面都有一个冒号,这些项的属性要么在同一行上声明,要么在下一行上声明,且与缩进级别相同。
在这里需要理解的关键配置项是客户端定义中的links。这以与docker run命令设置链接相同的方式创建,但 Compose 会为你处理启动顺序。实际上,大多数 Docker 命令行参数在 docker-compose.yml 语法中都有直接的对应项。
在这个例子中,我们使用了image:语句来定义每个服务使用的镜像,但你也可以通过在build:语句中定义 Dockerfile 的路径来让 docker-compose 动态地重建所需的镜像。Docker Compose 会为你执行构建。
小贴士
YAML 文件是一种具有简单语法的文本配置文件。你可以了解更多信息,请参阅yaml.org。
现在所有基础设施都已设置,运行应用程序变得简单:
$ docker-compose up
Creating dockercompose_server_1...
Creating dockercompose_client_1...
Attaching to dockercompose_server_1, dockercompose_client_1
client_1 | Received: Hello, world
client_1 |
client_1 | Received: Hello, world
client_1 |
小贴士
如果你启动 docker-compose 时遇到类似“无法连接到 Docker 守护进程在 http+unix://var/run/docker.sock—is it running?”的错误,问题可能是你需要用 sudo 运行它。
当您看够了,多次按 Ctrl-C 退出应用程序。您可以使用相同的命令随意重新启动它,无需担心删除容器。请注意,如果您重新运行它,它将输出“Recreating”而不是“Creating”。
讨论
我们在上一节中提到了可能需要 sudo 的情况——如果您适用,您可能需要重新查看技术 41,因为它使得使用与 Docker 守护进程交互的工具变得容易得多。
Docker Inc. 宣称 Docker Compose 已准备好在生产环境中使用,无论是像这里所示的单台机器,还是通过集群模式在多台机器上部署——您将在技术 87 中看到如何做到这一点。
现在,您已经掌握了 Docker Compose 的使用方法,我们将继续探讨 docker-compose 的更复杂和实际应用场景:使用 socat、卷和链接的替代品,为主机机器上运行的 SQLite 实例添加类似服务器的功能。
| |
使用 Docker Compose 的 SQLite 服务器
SQLite 默认没有 TCP 服务器的概念。通过构建在之前的技术之上,此技术为您提供了一种使用 Docker Compose 实现 TCP 服务器功能的方法。
具体来说,它基于以下之前介绍的工具和概念:
-
卷
-
使用 socat 进行代理
-
Docker Compose
我们还将介绍链接的替代品——网络。
注意
此技术要求在您的宿主机上安装 SQLite 版本 3。我们还建议您安装 rlwrap,以便在与 SQLite 服务器交互时进行行编辑更加友好(这是可选的)。这些软件包可以从标准的软件包管理器中免费获取。
此技术的代码可在此处下载:github.com/docker-in-practice/docker-compose-sqlite。
问题
您希望使用 Docker 在宿主机上高效地开发一个复杂的应用程序,该应用程序引用外部数据。
解决方案
使用 Docker Compose。
图 10.1 给出了此技术架构的概述。从高层次来看,有两个正在运行的 Docker 容器:一个负责执行 SQLite 客户端,另一个负责代理到这些客户端的单独 TCP 连接。请注意,执行 SQLite 的容器没有暴露给宿主机;代理容器实现了这一点。这种将责任分离成离散单元的做法是微服务架构的常见特征。
图 10.1. SQLite 服务器的工作原理

我们将为所有节点使用相同的镜像。在下一个列表中设置 Dockerfile。
列表 10.5. SQLite 服务器、客户端和代理 Dockerfile 的集成
FROM ubuntu:14.04
RUN apt-get update && apt-get install -y rlwrap sqlite3 socat *1*
EXPOSE 12345 *2*
-
1 安装所需的应用程序
-
2 暴露端口 12345,以便节点可以通过 Docker 守护进程进行通信
以下列表显示了 docker-compose.yml,它定义了容器应该如何启动。
列表 10.6. SQLite 服务器和代理 docker-compose.yml
version: "3"
services:
server: *1*
command: socat TCP-L:12345,fork,reuseaddr >
EXEC:'sqlite3 /opt/sqlite/db',pty *2*
build: . *3*
volumes: *4*
- /tmp/sqlitedbs/test:/opt/sqlite/db
networks:
- sqlnet *5*
proxy: *6*
command: socat TCP-L:12346,fork,reuseaddr TCP:server:12345 *7*
build: . *8*
ports: *9*
- 12346:12346
networks:
- sqlnet *10*
networks: *11*
sqlnet:
driver: bridge
-
1 服务器和代理容器定义在这个段落中。
-
2 创建一个 socat 代理,将 SQLite 调用的输出链接到 TCP 端口
-
3 在启动时从同一目录下的 Dockerfile 构建镜像
-
4 将测试 SQLite 数据库文件挂载到容器内的/opt/sqlite/db
-
5 这两个服务将成为 sqlnet Docker 网络的一部分。
-
6 服务器和代理容器定义在这个段落中。
-
7 创建一个 socat 代理,将数据从 12346 端口传递到服务器容器的 12345 端口
-
8 在启动时从同一目录下的 Dockerfile 构建镜像
-
9 将端口 12346 发布到主机
-
10 这两个服务将成为 sqlnet Docker 网络的一部分。
-
11 定义了容器可以加入的网络的列表
服务器容器中的 socat 进程将在 12345 端口上监听,并允许多个连接,如TCP-L:12345,fork,reuseaddr参数所指定的。EXEC:后面的部分告诉socat为每个连接在/opt/sqlite/db 文件上运行 SQLite,并为进程分配一个伪终端。客户端容器中的 socat 进程具有与服务器容器相同的监听行为(除了在不同的端口上),但不会在响应传入连接时运行任何东西,而是将建立到 SQLite 服务器的 TCP 连接。
与之前的技术相比,一个显著的不同点是使用网络而不是链接——网络提供了一种在 Docker 内部创建新虚拟网络的方法。Docker Compose 默认情况下始终会使用一个新的“桥接”虚拟网络;它只是在先前的 Compose 配置中明确命名了。因为任何新的桥接网络都允许通过服务名称访问容器,所以不需要使用链接(尽管如果你想为服务设置别名,仍然可以使用链接)。
尽管这个功能可以在一个容器中实现,但服务器/代理容器设置使得这个系统的架构更容易扩展,因为每个容器负责一项工作。服务器负责打开 SQLite 连接,而代理负责将服务暴露给主机机器。
以下列表(从存储库中的原始内容简化,github.com/docker-in-practice/docker-compose-sqlite)在您的宿主机上创建了两个最小的 SQLite 数据库,test 和 live。
列表 10.7. setup_dbs.sh
#!/bin/bash
echo "Creating directory"
SQLITEDIR=/tmp/sqlitedbs
rm -rf $SQLITEDIR *1*
if [ -a $SQLITEDIR ] *2*
then
echo "Failed to remove $SQLITEDIR"
exit 1
fi
mkdir -p $SQLITEDIR
cd $SQLITEDIR
echo "Creating DBs"
echo 'create table t1(c1 text);' | sqlite3 test *3*
echo 'create table t1(c1 text);' | sqlite3 live *4*
echo "Inserting data"
echo 'insert into t1 values ("test");' | sqlite3 test *5*
echo 'insert into t1 values ("live");' | sqlite3 live *6*
cd - > /dev/null 2>&1 *7*
echo "All done OK"
-
1 从之前的运行中删除任何目录
-
2 如果目录仍然存在,则抛出错误
-
3 创建包含一个表的测试数据库
-
4 创建包含一个表的实时数据库
-
5 向表中插入一行包含字符串“test”
-
6 向表中插入一个包含字符串“live”的行
-
7 返回到上一个目录
要运行此示例,请设置数据库并调用 docker-compose up,如下所示。
列表 10.8. 启动 Docker Compose 集群
$ chmod +x setup_dbs.sh
$ ./setup_dbs.sh
$ docker-compose up
Creating network "tmpnwxqlnjvdn_sqlnet" with driver "bridge"
Building proxy
Step 1/3 : FROM ubuntu:14.04
14.04: Pulling from library/ubuntu
[...]
Successfully built bb347070723c
Successfully tagged tmpnwxqlnjvdn_proxy:latest
[...]
Successfully tagged tmpnwxqlnjvdn_server:latest
[...]
Creating tmpnwxqlnjvdn_server_1
Creating tmpnwxqlnjvdn_proxy_1 ... done
Attaching to tmpnwxqlnjvdn_server_1, tmpnwxqlnjvdn_proxy_1
然后,在另一个或多个其他终端中,您可以通过运行 Telnet 来创建针对一个 SQLite 数据库的多个会话。
列表 10.9. 连接到 SQLite 服务器
$ rlwrap telnet localhost 12346 *1*
Trying 127.0.0.1... *2*
Connected to localhost. *2*
Escape character is '^]'. *2*
SQLite version 3.7.17 *3*
Enter ".help" for instructions
sqlite> select * from t1; *4*
select * from t1;
test
sqlite>
-
1 使用 rlwrap 包装的 Telnet 连接到代理,以获得命令行的编辑和历史功能
-
2 Telnet 连接的输出
-
3 连接到 SQLite
-
4 在 sqlite 提示符下运行 SQL 命令
如果您想将服务器切换到实时状态,您可以通过更改 docker-compose.yml 中的 volumes 行来更改配置,
- /tmp/sqlitedbs/test:/opt/sqlite/db
到此为止:
- /tmp/sqlitedbs/live:/opt/sqlite/db
然后重新运行此命令:
$ docker-compose up
警告
尽管我们对这种 SQLite 客户端的复用进行了一些基本测试,但我们不对该服务器在任何负载下的数据完整性和性能做出任何保证。SQLite 客户端并非设计成以这种方式工作。这项技术的目的是演示以这种方式公开二进制文件的一般方法。
这种技术展示了 Docker Compose 如何将相对复杂和棘手的事情变得健壮和简单。在这里,我们使用了 SQLite,并通过将容器连接到代理来将 SQLite 调用代理到主机上的数据,从而赋予了它额外的服务器功能。使用 Docker Compose 的 YAML 配置可以显著简化容器复杂性的管理,它将正确编排容器的棘手问题从手动、易出错的流程转变为更安全、自动化的流程,并且可以置于源代码控制之下。这是我们进入编排之旅的开始,您将在本书的第四部分中听到更多关于它的内容。
讨论
使用 Docker Compose 的 depends_on 功能,您可以通过控制启动顺序来有效地模拟链接的功能。要全面了解 Docker Compose 所提供的所有可能选项,我们建议您阅读官方文档docs.docker.com/compose/compose-file/。
要了解更多关于 Docker 虚拟网络的信息,请查看技术 80——它详细介绍了 Docker Compose 在幕后是如何设置您的虚拟网络的。
10.2. 使用 Docker 模拟现实世界的网络
大多数使用互联网的人将其视为一个黑盒,它以某种方式从世界各地的其他地方检索信息并将其显示在他们的屏幕上。有时他们会遇到速度慢或连接中断的情况,因此对 ISP 的咒骂并不少见。
当您构建包含需要连接的应用程序镜像时,您可能对哪些组件需要连接到何处以及整体设置看起来如何有了更深入的了解。但有一点是恒定的:您仍然可能会遇到网络缓慢和连接中断。即使是拥有并运营自己数据中心的大型公司,也观察到了不可靠的网络以及它给应用程序带来的问题。
我们将探讨几种您可以通过实验不稳定的网络来确定您可能在实际世界中遇到的问题的方法。
使用 Comcast 模拟麻烦的网络
尽管我们可能希望当我们在多台机器上分发应用程序时拥有完美的网络条件,但现实情况要糟糕得多——关于数据包丢失、连接中断和网络分区的故事比比皆是,尤其是在通用云服务提供商那里。
在您的堆栈在实际世界中遇到这些情况之前对其进行测试是明智的,以了解其行为——为高可用性设计的应用程序不应该因为外部服务开始经历显著的额外延迟而停止运行。
问题
您希望能够将不同的网络条件应用到单个容器中。
解决方案
使用 Comcast(网络工具,而不是 ISP)。
Comcast (github.com/tylertreat/Comcast) 是一个有趣命名的工具,用于在您的 Linux 机器上更改网络接口,以便将不寻常的(或者如果您不幸,是典型的!)条件应用于它们。
每当 Docker 创建一个容器时,它也会创建虚拟网络接口——这就是所有容器都有不同的 IP 地址并且可以互相 ping 的原因。因为这些是标准网络接口,只要您能找到网络接口名称,您就可以在上面使用 Comcast。这说起来容易做起来难。
以下列表显示了一个包含 Comcast、所有先决条件和一些调整的 Docker 镜像。
列表 10.10. 准备运行 comcast 镜像
$ IMG=dockerinpractice/comcast
$ docker pull $IMG
latest: Pulling from dockerinpractice/comcast
[...]
Status: Downloaded newer image for dockerinpractice/comcast:latest
$ alias comcast="docker run --rm --pid=host --privileged \
-v /var/run/docker.sock:/var/run/docker.sock $IMG"
$ comcast -help
Usage of comcast:
-cont string
Container ID or name to get virtual interface of
-default-bw int
Default bandwidth limit in kbit/s (fast-lane) (default -1)
-device string
Interface (device) to use (defaults to eth0 where applicable)
-dry-run
Specifies whether or not to actually commit the rule changes
-latency int
Latency to add in ms (default -1)
-packet-loss string
Packet loss percentage (e.g. 0.1%)
-stop
Stop packet controls
-target-addr string
Target addresses, (e.g. 10.0.0.1 or 10.0.0.0/24 or >
10.0.0.1,192.168.0.0/24 or 2001:db8:a::123)
-target-bw int
Target bandwidth limit in kbit/s (slow-lane) (default -1)
-target-port string
Target port(s) (e.g. 80 or 1:65535 or 22,80,443,1000:1010)
-target-proto string
Target protocol TCP/UDP (e.g. tcp or tcp,udp or icmp) (default >
"tcp,udp,icmp")
-version
Print Comcast's version
我们在这里添加的调整提供了 -cont 选项,允许您引用容器而不是必须找到虚拟接口的名称。请注意,我们不得不向 docker run 命令添加一些特殊标志,以便给容器更多的权限——这样 Comcast 就可以自由地检查和修改网络接口。
为了了解 Comcast 可以带来的差异,我们首先需要了解正常网络连接是什么样的。打开一个新的终端并运行以下命令,以设定对基准网络性能的预期:
$ docker run -it --name c1 ubuntu:14.04.2 bash
root@0749a2e74a68:/# apt-get update && apt-get install -y wget
[...]
root@0749a2e74a68:/# ping -q -c 5 www.example.com
PING www.example.com (93.184.216.34) 56(84) bytes of data.
--- www.example.com ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, >
time 4006ms *1*
rtt min/avg/max/mdev = 86.397/86.804/88.229/0.805 ms *2*
root@0749a2e74a68:/# time wget -o /dev/null https://www.example.com
real 0m0.379s *3*
user 0m0.008s
sys 0m0.008s
root@0749a2e74a68:/#
-
1 这台机器与 www.example.com 之间的连接似乎很可靠,没有数据包丢失。
-
2 平均往返时间是大约 100 毫秒,对于 www.example.com。
-
3 下载 www.example.com 的 HTML 主页所需的总时间大约是 0.7 秒。
完成这些操作后,让容器继续运行,你可以对其应用一些网络条件:
$ comcast -cont c1 -default-bw 50 -latency 100 -packet-loss 20%
Found interface veth62cc8bf for container 'c1'
sudo tc qdisc show | grep "netem"
sudo tc qdisc add dev veth62cc8bf handle 10: root htb default 1
sudo tc class add dev veth62cc8bf parent 10: classid 10:1 htb rate 50kbit
sudo tc class add dev veth62cc8bf parent 10: classid 10:10 htb rate 1000000kb
it
sudo tc qdisc add dev veth62cc8bf parent 10:10 handle 100: netem delay 100ms
loss 20.00%
sudo iptables -A POSTROUTING -t mangle -j CLASSIFY --set-class 10:10 -p tcp
sudo iptables -A POSTROUTING -t mangle -j CLASSIFY --set-class 10:10 -p udp
sudo iptables -A POSTROUTING -t mangle -j CLASSIFY --set-class 10:10 -p icmp
sudo ip6tables -A POSTROUTING -t mangle -j CLASSIFY --set-class 10:10 -p tcp
sudo ip6tables -A POSTROUTING -t mangle -j CLASSIFY --set-class 10:10 -p udp
sudo ip6tables -A POSTROUTING -t mangle -j CLASSIFY --set-class 10:10 -p icmp
Packet rules setup...
Run `sudo tc -s qdisc` to double check
Run `comcast --device veth62cc8bf --stop` to reset
上述命令应用了三种不同的条件:对所有目的地的 50 KBps 带宽限制(让人回忆起拨号上网),增加 100 毫秒的延迟(加上任何固有的延迟),以及 20%的丢包率。
Comcast 首先识别容器适当的虚拟网络接口,然后调用一系列标准的 Linux 命令行网络工具来应用流量规则,并在执行过程中列出其操作。让我们看看我们的容器对此的反应:
root@0749a2e74a68:/# ping -q -c 5 www.example.com
PING www.example.com (93.184.216.34) 56(84) bytes of data.
--- www.example.com ping statistics ---
5 packets transmitted, 2 received, 60% packet loss, time 4001ms
rtt min/avg/max/mdev = 186.425/189.429/195.008/3.509 ms
root@0749a2e74a68:/# time wget -o /dev/null https://www.example.com
real 0m1.993s
user 0m0.011s
sys 0m0.011s
成功!ping 报告了额外的 100 毫秒延迟,而wget的计时显示略大于 5 倍减速,大约符合预期(带宽限制、延迟增加和丢包都将影响这个时间)。但关于丢包有些奇怪——它似乎比预期大三倍。重要的是要记住,ping 只发送了几包,丢包不是一个精确的“五分之一”计数器——如果你将 ping 计数增加到 50,你会发现结果丢包更接近预期。
注意,我们应用的规定适用于通过此网络接口的所有网络连接。这包括与主机和其他容器的连接。
现在我们指导 Comcast 移除这些规则。遗憾的是,Comcast 目前还不能添加和移除单个条件,因此更改网络接口上的任何内容意味着完全移除并重新添加接口上的规则。如果你想要恢复正常的容器网络操作,也需要移除这些规则。不过,如果你退出容器,无需担心移除它们——当 Docker 删除虚拟网络接口时,它们将被自动删除。
$ comcast -cont c1 -stop
Found interface veth62cc8bf for container 'c1'
[...]
Packet rules stopped...
Run `sudo tc -s qdisc` to double check
Run `comcast` to start
如果你想要深入了解,可以研究 Linux 流量控制工具,可能使用带有-dry-run的 Comcast 生成要使用的命令示例集。全面探讨所有可能性超出了本技术的范围,但请记住,如果你可以将它放入容器中,并且它触网,你就可以对其进行操作。
讨论
经过一些实施努力,你完全有理由使用 Comcast 不仅仅是为了手动控制容器带宽。例如,假设你正在使用像 btsync 这样的工具(技术 35),但希望限制可用带宽以避免占用你的连接——下载 Comcast,将其放入容器中,并使用ENTRYPOINT(技术 49)在容器启动时设置带宽限制。
要做到这一点,你需要安装 Comcast 的依赖项(在 Dockerfile 中的alpine镜像中列出,见github.com/docker-in-practice/docker-comcast/blob/master/Dockerfile),并且可能需要给容器至少赋予网络管理员权限——你可以在技术 93 中了解更多关于权限的内容。
使用 Blockade 模拟麻烦的网络
康卡斯特(Comcast)是一个功能丰富的工具,有多个应用场景,但它无法解决一个重要的用例——如何批量将网络条件应用到容器中?手动对数十个容器运行 Comcast 将非常痛苦,而对数百个容器来说则几乎不可想象!这对于容器来说尤其是一个相关的问题,因为启动容器的成本非常低——如果你试图在单台机器上运行一个包含数百个虚拟机而不是容器的复杂网络模拟,你可能会发现你面临更大的问题,比如内存不足!
在讨论模拟多机网络的背景下,有一种特定的网络故障在这个规模下变得有趣——网络分区。这是指一组网络机器分裂成两个或更多部分,使得同一部分中的所有机器可以相互通信,但不同部分之间无法通信。研究表明,这种情况的发生频率可能比你想象的要高,尤其是在消费级云上!
沿着经典的 Docker 微服务路线,这些问题变得尤为突出,拥有进行实验的工具对于理解你的服务如何处理这些问题至关重要。
问题
你希望协调为大量容器设置网络条件,包括创建网络分区。
解决方案
使用 Blockade(github.com/worstcase/blockade)——一个开源软件,最初来自戴尔团队,用于“测试网络故障和分区”。
阻塞(Blockade)通过读取当前目录下的配置文件(blockade.yml)来定义如何启动容器以及应用哪些条件到它们上。为了应用条件,它可能会下载包含所需实用工具的其他镜像。完整的配置细节可以在 Blockade 文档中找到,因此我们只介绍基本内容。
首先,你需要创建一个 blockade.yml 文件。
列表 10.11. blockade.yml 文件
containers:
server:
container_name: server
image: ubuntu:14.04.2
command: /bin/sleep infinity
client1:
image: ubuntu:14.04.2
command: sh -c "sleep 5 && ping server"
client2:
image: ubuntu:14.04.2
command: sh -c "sleep 5 && ping server"
network:
flaky: 50%
slow: 100ms
driver: udn
在前面的配置中,容器被设置为代表两个客户端连接的服务器。在实践中,这可能是数据库服务器及其客户端应用程序,你没有必要限制你想要模拟的组件数量。如果你能在 compose .yml 文件中(参见技术 76)表示它,那么你很可能在 Blockade 中对其进行建模。
我们在这里指定了网络驱动程序为udn——这使得 Blockade 模仿 Docker Compose 在技术 77 中的行为,创建一个新的虚拟网络,以便容器可以通过容器名称相互 ping。为此,我们必须明确指定服务器的container_name,因为 Blockade 默认会生成一个。sleep 5命令是为了确保在启动客户端之前服务器正在运行——如果你更喜欢使用 Blockade 的链接,它们将确保容器按正确的顺序启动。现在不用担心network部分;我们很快就会回到它。
使用 Blockade 的第一步通常是拉取镜像:
$ IMG=dockerinpractice/blockade
$ docker pull $IMG
latest: Pulling from dockerinpractice/blockade
[...]
Status: Downloaded newer image for dockerinpractice/blockade:latest
$ alias blockade="docker run --rm -v \$PWD:/blockade \
-v /var/run/docker.sock:/var/run/docker.sock $IMG"
你会注意到,与之前的技术(如--privileged和--pid=host)相比,我们缺少了一些docker run的参数。Blockade 使用其他容器来执行网络操作,因此它本身不需要权限。另外,请注意挂载当前目录到容器中的参数,这样 Blockade 就能访问 blockade.yml 并在一个隐藏文件夹中存储状态。
注意
如果你在一个网络文件系统上运行,当你第一次启动 Blockade 时可能会遇到奇怪的权限问题——这很可能是由于 Docker 试图以 root 用户创建隐藏状态文件夹,但网络文件系统不配合。解决方案是使用本地磁盘。
最后,我们来到了关键时刻——运行 Blockade。确保你位于保存 blockade.yml 的目录中:
$ blockade up
NODE CONTAINER ID STATUS IP NETWORK PARTITION
client1 613b5b1cdb7d UP 172.17.0.4 NORMAL
client2 2aeb2ed0dd45 UP 172.17.0.5 NORMAL
server 53a7fa4ce884 UP 172.17.0.3 NORMAL
注意
在启动时,Blockade 有时可能会显示一些关于/proc 中文件不存在的神秘错误。首先需要检查的是容器是否在启动时立即退出,这阻止了 Blockade 检查其网络状态。此外,请尽量抵制使用 Blockade -c选项来指定自定义配置文件路径的诱惑——容器内部只能访问当前目录的子目录。
我们配置文件中定义的所有容器都已启动,并且我们得到了有关已启动容器的大量有用信息。现在让我们应用一些基本的网络条件。在新终端(使用docker logs -f 613b5b1cdb7d)中跟踪 client1 的日志,这样你就可以看到当你改变设置时发生了什么:
$ blockade flaky --all *1*
$ sleep 5 *2*
$ blockade slow client1 *3*
$ blockade status *4*
NODE CONTAINER ID STATUS IP NETWORK PARTITION
client1 613b5b1cdb7d UP 172.17.0.4 SLOW
client2 2aeb2ed0dd45 UP 172.17.0.5 FLAKY
server 53a7fa4ce884 UP 172.17.0.3 FLAKY
$ blockade fast --all *5*
-
1 使所有容器的网络变得不可靠(丢弃数据包)
-
2 延迟下一个命令以给前一个命令足够的时间生效并记录一些输出
-
3 使 client1 容器的网络变慢(向数据包添加延迟)
-
4 检查容器当前的状态
-
5 将所有容器恢复到正常操作
flaky和slow命令使用前一个配置文件中network部分定义的值(列表 10.11)——无法在命令行上指定限制。如果你愿意,可以在容器运行时编辑 blockade.yml,然后选择性地将新限制应用于容器。请注意,容器可以处于慢速或不可靠的网络中,但不能同时处于两者。尽管有这些限制,但针对数百个容器运行此功能的便利性相当显著。
如果你回顾一下client1的日志,你现在应该能够看到不同命令何时生效:
64 bytes from 172.17.0.3: icmp_seq=638 ttl=64 time=0.054 ms *1*
64 bytes from 172.17.0.3: icmp_seq=639 ttl=64 time=0.098 ms
64 bytes from 172.17.0.3: icmp_seq=640 ttl=64 time=0.112 ms
64 bytes from 172.17.0.3: icmp_seq=645 ttl=64 time=0.112 ms *2*
64 bytes from 172.17.0.3: icmp_seq=652 ttl=64 time=0.113 ms
64 bytes from 172.17.0.3: icmp_seq=654 ttl=64 time=0.115 ms
64 bytes from 172.17.0.3: icmp_seq=660 ttl=64 time=100 ms *3*
64 bytes from 172.17.0.3: icmp_seq=661 ttl=64 time=100 ms
64 bytes from 172.17.0.3: icmp_seq=662 ttl=64 time=100 ms
64 bytes from 172.17.0.3: icmp_seq=663 ttl=64 time=100 ms
-
1
icmp_seq是连续的(没有数据包被丢弃)且time低(延迟小)。 -
2
icmp_seq开始跳过数字——flaky命令已经生效。 -
3
time的时间发生了大幅跳跃——slow命令已经生效。
所有这些都很实用,但我们已经可以通过在 Comcast 之上进行一些(可能很痛苦)的脚本编写来实现,所以让我们来看看 Blockade 的杀手级功能——网络分区:
$ blockade partition server client1,client2
$ blockade status
NODE CONTAINER ID STATUS IP NETWORK PARTITION
client1 613b5b1cdb7d UP 172.17.0.4 NORMAL 2
client2 2aeb2ed0dd45 UP 172.17.0.5 NORMAL 2
server 53a7fa4ce884 UP 172.17.0.3 NORMAL 1
这使得我们的三个节点被分成了两个盒子——服务器在一个盒子中,客户端在另一个盒子中,它们之间无法通信。你会看到client1的日志已经停止了任何操作,因为所有的 ping 数据包都丢失了。尽管如此,客户端之间仍然可以互相通信,你可以通过在它们之间发送几个 ping 数据包来验证这一点:
$ docker exec 613b5b1cdb7d ping -qc 3 172.17.0.5
PING 172.17.0.5 (172.17.0.5) 56(84) bytes of data.
--- 172.17.0.5 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2030ms
rtt min/avg/max/mdev = 0.109/0.124/0.150/0.018 ms
没有数据包丢失,延迟低……看起来连接良好。分区和其他网络条件独立运行,因此你可以在应用分区的同时玩转数据包丢失。你可以定义的分区数量没有限制,因此你可以随心所欲地玩转复杂的场景。
讨论
如果你需要的功能比 Blockade 和 Comcast 各自能提供的还要强大,你可以将它们结合起来。Blockade 在创建分区和执行启动容器的繁重工作方面非常出色;将 Comcast 加入其中,你可以对每个容器的网络连接进行精细控制。
值得注意的是,查看 Blockade 的完整帮助文档——它提供了其他你可能觉得有用的功能,例如“混乱”功能,可以随机影响具有各种条件的容器,以及命令的--random参数,这样你就可以(例如)看到当容器随机被杀死时你的应用程序如何反应。如果你听说过 Netflix 的 Chaos Monkey,这是一种在更小规模上模仿它的方法。
10.3 Docker 和虚拟网络
Docker 的核心功能都是关于隔离。前面的章节展示了进程和文件系统隔离的一些好处,而本章你看到了网络隔离。
你可以认为网络隔离有两个方面:
-
独立沙盒——每个容器都有自己的 IP 地址和要监听的端口集合,不会与其他容器(或主机)冲突。
-
沙盒组——这是单个沙盒的逻辑扩展——所有隔离的容器都在一个私有网络中分组在一起,允许您在不干扰您机器所在的网络(以及招致您公司网络管理员的愤怒!)的情况下进行实验。
前两种技术提供了网络隔离这两个方面的实际示例——Comcast 通过操纵单个沙盒来为每个容器应用规则,而 Blockade 中的分区则依赖于对私有容器网络的完全监督能力来将其分割成片段。幕后,它看起来有点像图 10.2。
图 10.2. 主机机器上的内部 Docker 网络

桥接如何工作的确切细节并不重要。只需说,桥接在容器之间创建了一个扁平网络(它允许直接通信,没有中间步骤)并将请求转发到外部连接。
Docker Inc.随后根据用户反馈调整了此模型,允许您使用网络驱动程序创建自己的虚拟网络,这是一个插件系统,用于扩展 Docker 的网络功能。这些插件要么是内置的,要么由第三方提供,应该完成所有必要的网络连接工作,让您可以继续使用它。
您创建的新网络可以被视为额外的沙盒组,通常在沙盒内提供访问权限,但不允许跨沙盒通信(尽管网络行为的精确细节取决于驱动程序)。
创建另一个 Docker 虚拟网络
当人们第一次了解到他们可以创建自己的虚拟网络时,一个常见的反应是询问他们如何创建默认 Docker 桥接的副本,以便容器集之间可以通信但与其他容器隔离。Docker Inc.意识到这将是一个受欢迎的请求,因此它被实现为初始实验版中虚拟网络的第一批功能之一。
问题
您需要一个由 Docker Inc.支持的创建虚拟网络解决方案。
解决方案
使用嵌套在docker network下的 Docker 子命令集创建您自己的虚拟网络。
内置的“桥接”驱动程序可能是最常用的驱动程序——它是官方支持的,并允许您创建默认内置桥接的新副本。然而,在这个技术中,我们将稍后探讨一个重要的区别——在非默认桥接中,您可以通过名称 ping 容器。
您可以使用docker network ls命令查看内置网络列表:
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
100ce06cd9a8 bridge bridge local
d53919a3bfa1 host host local
2d7fcd86306c none null local
在这里,你可以看到在我的机器上始终可用的三个网络,容器可以加入这些网络。bridge 网络是容器默认到达的地方,能够与其他桥接上的容器通信。host 网络指定了在启动容器时使用 --net=host 的情况(容器将网络视为机器上运行的任何正常程序),而 none 对应于 --net=none,这是一个只有回环接口的容器。
让我们添加一个新的 bridge 网络,为容器提供一个新的平坦网络以自由通信:
$ docker network create --driver=bridge mynet
770ffbc81166d54811ecf9839331ab10c586329e72cea2eb53a0229e53e8a37f
$ docker network ls | grep mynet
770ffbc81166 mynet bridge local
$ ip addr | grep br-
522: br-91b29e0d29d5: <NO-
CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group
default
inet 172.18.0.1/16 scope global br-91b29e0d29d5
$ ip addr | grep docker
5: docker0: <NO-
CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group
default
inet 172.17.0.1/16 scope global docker0
这创建了一个新的网络接口,它将使用与正常 Docker 桥接不同的 IP 地址范围。对于桥接,新的网络接口名称目前将以 br- 开头,但将来可能会改变。
让我们现在启动两个连接到网络的容器:
$ docker run -it -d --name c1 ubuntu:14.04.2 bash *1*
87c67f4fb376f559976e4a975e3661148d622ae635fae4695747170c00513165
$ docker network connect mynet c1 *2*
$ docker run -it -d --name c2 \
--net=mynet ubuntu:14.04.2 bash *3*
0ee74a3e3444f27df9c2aa973a156f2827bcdd0852c6fd4ecfd5b152846dea5b
$ docker run -it -d --name c3 ubuntu:14.04.2 bash *4*
-
1 启动名为 c1 的容器(在默认桥接上)
-
2 将容器 c1 连接到 mynet 网络
-
3 在 mynet 网络中创建名为 c2 的容器
-
4 启动名为 c3 的容器(在默认桥接上)
前面的命令展示了将容器连接到网络的两种不同方式——先启动容器然后附加服务,以及一步创建和附加。
这两种情况有所不同。第一个会在启动时连接到默认网络(通常是 Docker 桥接网络,但可以通过 Docker 守护进程的参数进行自定义),然后添加一个新的接口,以便它也能访问 mynet。第二个只会连接到 mynet——任何在正常 Docker 桥接上的容器都无法访问它。
让我们进行一些连通性检查。首先我们应该查看我们容器的 IP 地址:
$ docker exec c1 ip addr | grep 'inet.*eth' *1*
inet 172.17.0.2/16 scope global eth0
inet 172.18.0.2/16 scope global eth1
$ docker exec c2 ip addr | grep 'inet.*eth' *2*
inet 172.18.0.3/16 scope global eth0
$ docker exec c3 ip addr | grep 'inet.*eth'
inet 172.17.0.3/16 scope global eth0 *3*
-
1 列出 c1 的接口和 IP 地址——一个在默认桥接上,一个在 mynet 中
-
2 列出 mynet 内 c2 的接口和 IP 地址
-
3 列出默认桥接上 c3 的接口和 IP 地址
现在我们可以进行一些连通性测试:
$ docker exec c2 ping -qc1 c1 *1*
PING c1 (172.18.0.2) 56(84) bytes of data.
--- c1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.041/0.041/0.041/0.000 ms
$ docker exec c2 ping -qc1 c3
*2*
ping: unknown host c3 *2*
$ docker exec c2 ping -qc1 172.17.0.3
*2*
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
--- 172.17.0.3 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
$ docker exec c1 ping -qc1 c2 *3*
PING c2 (172.18.0.3) 56(84) bytes of data.
--- c2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.047/0.047/0.047/0.000 ms
$ docker exec c1 ping -qc1 c3
*4*
ping: unknown host c3 *4*
$ docker exec c1 ping -qc1 172.17.0.3
*4*
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
--- 172.17.0.3 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.095/0.095/0.095/0.000 ms
-
1 尝试从容器 2 ping 容器 1 的名称(成功)
-
2 尝试从容器 2 ping 容器 3 的名称和 IP 地址(失败)
-
3 尝试从容器 1 ping 容器 2 的名称(成功)
-
4 尝试从容器 1 ping 容器 3 的名称和 IP 地址(失败,成功)
这里发生了很多事情!以下是关键要点:
-
在新的桥接上,容器可以通过 IP 地址和名称相互 ping。
-
在默认桥接上,容器只能通过 IP 地址相互 ping。
-
横跨多个桥接的容器可以访问它们所属的任何网络中的容器。
-
容器之间无法通过桥接相互访问,即使使用 IP 地址。
讨论
这种新的桥接器创建功能在技术 77 中使用 Docker Compose,在技术 79 中使用 Blockade 时被使用,以提供容器通过名称 ping 对方的能力。但你也已经看到,这是一个高度灵活的功能,具有建模合理复杂网络的可能性。
例如,你可能想尝试使用一个堡垒主机,一个单独的锁定机器,它为访问另一个价值更高的网络提供访问权限。通过将你的应用程序服务放在一个新的桥接器中,然后仅通过连接到默认和新的桥接器的容器来公开服务,你可以在保持自己机器隔离的同时开始运行一些相对现实的渗透测试。
使用 Weave 设置底物网络
底物网络是在另一个网络之上构建的软件级网络层。实际上,你最终得到一个看起来像是本地的网络,但在底层它是在其他网络之间进行通信。这意味着从性能的角度来看,网络的行为将不如本地网络可靠,但从可用性的角度来看,它可以非常方便:你可以像它们在同一房间里一样与完全不同位置的节点通信。
这对于 Docker 容器来说尤其有趣——容器可以在主机之间无缝连接,就像在网络之间连接主机一样。这样做消除了对计划单个主机上可以容纳多少容器的迫切需求。
问题
你希望在主机之间无缝地通信容器。
解决方案
使用 Weave Net(在本技术中其余部分简称为“Weave”)来设置一个网络,允许容器像在本地网络中一样相互通信。
我们将使用 Weave (www.weave.works/oss/net/)来演示底物网络的原理,这是一个为此目的设计的工具。图 10.3 展示了典型 Weave 网络的概览。
图 10.3.一个典型的 Weave 网络

在图 10.3 中,主机 1 无法访问主机 3,但它们可以通过 Weave 网络相互通信,就像它们是本地连接的一样。Weave 网络不对公众开放——只对在 Weave 下启动的容器开放。这使得在不同环境中开发、测试和部署代码相对简单,因为网络拓扑可以在每种情况下都保持一致。
安装 Weave
Weave 是一个单一的二进制文件。你可以在www.weave.works/docs/net/latest/install/installing-weave/找到安装说明。
之前链接(以及以下方便起见列出)中的说明对我们有效。Weave 需要安装在你希望成为 Weave 网络一部分的每个主机上:
$ sudo curl -L git.io/weave -o /usr/local/bin/weave
$ sudo chmod +x /usr/local/bin/weave
警告
如果你在此技术中遇到问题,你的机器上可能已经有一个 Weave 二进制文件,它是另一个软件包的一部分。
设置 Weave
要遵循此示例,你需要两个主机。我们将它们称为host1和host2。确保它们可以通过 ping 相互通信。你需要第一个启动 Weave 的主机的 IP 地址。
获取主机公共 IP 地址的一个快速方法是使用浏览器访问ifconfig.co/,或者运行curl https://ifconfig.co,但请注意,你可能需要为两个主机打开防火墙,以便它们可以通过开放的互联网连接。如果你选择正确的 IP 地址,你还可以在本地网络中运行 Weave。
提示
如果你在使用此技术时遇到问题,很可能是网络以某种方式被防火墙隔离。如果你不确定,请咨询你的网络管理员。具体来说,你需要确保端口 6783 对 TCP 和 UDP 都开放,端口 6784 仅对 UDP 开放。
在第一个主机上,你可以运行第一个 Weave 路由器:
host1$ curl https://ifconfig.co *1*
1.2.3.4
host1$ weave launch
*2*
[...]
host1$ eval $(weave env) *3*
host1$ docker run -it --name a1 ubuntu:14.04 bash *4*
root@34fdd53a01ab:/# ip addr show ethwe *5*
43: ethwe@if44: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1376 qdisc noqueue
state UP group default
link/ether 72:94:41:e3:00:df brd ff:ff:ff:ff:ff:ff
inet 10.32.0.1/12 scope global ethwe
valid_lft forever preferred_lft forever
-
1 确定 host1 的 IP 地址
-
2 在 host1 上启动 Weave 服务。这需要在每个主机上执行一次,并且它将下载并运行一些 Docker 容器,在后台运行以管理底层数据网络。
-
3 在此 shell 中设置 docker 命令以使用 Weave。如果你关闭了 shell 或打开了一个新的 shell,你需要再次运行此命令。
-
4 启动容器
-
5 检索 Weave 网络上容器的 IP 地址
Weave 负责在容器中插入一个额外的接口ethwe,它为 Weave 网络提供一个 IP 地址。
你可以在host2上执行类似的步骤,但需要告诉 Weave 关于 host1 位置的信息:
host2$ sudo weave launch 1.2.3.4 *1*
host2$ eval $(weave env) *2*
host2$ docker run -it --name a2 ubuntu:14.04 bash
*3*
root@a2:/# ip addr show ethwe *3*
553: ethwe@if554: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1376 qdisc noqueue
state UP group default
link/ether fe:39:ca:74:8a:ca brd ff:ff:ff:ff:ff:ff
inet 10.44.0.0/12 scope global ethwe
valid_lft forever preferred_lft forever
-
1 以 root 用户在 host2 上启动 Weave 服务。这次你需要添加第一个主机的公共 IP 地址,以便它可以连接到另一台主机。
-
2 为 Weave 的服务设置适当的环境
-
3 与 host1 的步骤相同
在host2上唯一的区别是,你需要告诉 Weave 它与host1上的 Weave 进行对等连接(通过 IP 地址或主机名指定,可选的:port,这样 host2 就可以到达它)。
测试你的连接
现在你已经设置好了一切,你可以测试你的容器是否可以相互通信。让我们以host2上的容器为例:
root@a2:/# ping -qc1 10.32.0.1 *1*
PING 10.32.0.1 (10.32.0.1) 56(84) bytes of data.
--- 10.32.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms *2*
rtt min/avg/max/mdev = 1.373/1.373/1.373/0.000 ms
-
1 ping 其他服务器分配的 IP 地址
-
2 成功的 ping 响应
如果你收到成功的 ping 响应,你已证明在跨越两个主机的自分配私有网络内存在连接性。你也能够通过容器名称 ping,就像使用自定义网桥一样。
提示
由于 ICMP 协议(ping 使用)消息可能被防火墙阻止,因此这可能不起作用。如果不起作用,尝试在另一台主机上 telnet 到端口 6783 以测试是否可以建立连接。
讨论
子网是强有力地使网络和防火墙偶尔混乱的世界有秩序的工具。Weave 甚至声称可以智能地路由您的流量穿过部分分割的网络,其中某些主机 B 可以看到 A 和 C,但 A 和 C 不能通信—这可能会在技术 80 中熟悉。尽管如此,请记住,有时这些复杂的网络设置存在是有原因的——堡垒主机的全部目的就是为了安全而隔离。
所有这些功能都付出了代价—有报道称 Weave 网络有时比“原始”网络慢得多,并且您必须在后台运行额外的管理工具(因为网络插件模型不涵盖所有用例)。
Weave 网络具有许多额外的功能,从可视化到与 Kubernetes 的集成(我们将在技术 88 中介绍 Kubernetes 作为编排器)。我们建议您查看 Weave Net 概述以了解更多信息并充分利用您的网络—www.weave.works/docs/net/latest/overview/.
我们在这里没有涵盖的一件事是内置的覆盖网络插件。根据您的用例,这可能值得研究作为 Weave 的可能替代品,尽管它需要使用 Swarm 模式(技术 87)或设置一个全局可访问的键/值存储(可能是 etcd,来自技术 74)。
摘要
-
Docker Compose 可以用来设置容器集群。
-
康卡斯特和 Blockade 都是测试坏网络中容器的有用工具。
-
Docker 虚拟网络是链接的替代方案。
-
您可以使用虚拟网络在 Docker 中手动建模网络。
-
Weave Net 对于在主机之间连接容器很有用。
第四部分. 从单机到云端的编排
第四部分 涵盖了编排的基本领域。一旦你在同一环境中运行任意数量的容器,你就需要考虑如何以一致和可靠的方式管理它们,因此我们将探讨一些目前最流行的工具。
第十一章 解释了编排的重要性,并从使用 systemd 管理基于单个主机的 Docker 服务开始,逐步发展到使用 Consul 和 Registrator 在网络上进行服务发现。
第十二章 讨论了集群化的 Docker 环境,我们将简要介绍 Docker Swarm,然后讨论最流行的编排工具 Kubernetes。然后我们将通过展示如何使用 Docker 在本地模拟 AWS 服务来反转这个过程。最后,我们将介绍在 Mesos 上构建 Docker 框架。
第十三章 对在选择基于 Docker 的平台时可能考虑的因素进行了深入讨论。选择可能会让人感到困惑,但这可能有助于你整理思路,并在需要时做出更好的决策。
第十一章. 容器编排入门
本章内容涵盖
-
使用 systemd 管理简单的 Docker 服务
-
使用 Helios 管理多主机 Docker 服务
-
使用 Hashicorp 的 Consul 进行服务发现
-
使用 Registrator 进行服务注册
Docker 所基于的技术以不同形式存在了一段时间,但 Docker 是成功吸引技术行业兴趣的解决方案。这使得 Docker 处于一个令人羡慕的位置——Docker 的份额完成了启动工具生态系统的初始工作,这形成了一个自我维持的循环,吸引人们进入生态系统并为它做出贡献。
这在编排方面尤其明显。在看到提供该领域服务的公司名单后,你会认为每个人都对如何做事有自己的看法,并开发了他们自己的工具。
尽管生态系统是 Docker(以及为什么我们在本书中如此频繁地从中汲取)的一个巨大优势,但可能的编排工具数量对于新手和资深人士来说都可能令人感到压倒性。本章将介绍一些最显著的工具,并让你对高级提供的内容有所了解,这样在评估你希望框架为你做什么时,你将更加了解。
安排编排工具的家族树有许多方法。图 11.1 展示了我们熟悉的一些工具。树的根部是docker run,这是启动容器的最常见方式。所有受 Docker 启发的工具都是这个分支的延伸。左侧是那些将容器组视为单一实体的工具。中间展示了专注于在 systemd 和服务文件下管理容器的工具。最后,右侧将单个容器视为其本身。随着你向下移动分支,这些工具最终会为你做更多的事情,无论是跨多台主机工作还是从你手中移除手动容器部署的繁琐。
图 11.1. Docker 生态系统中的编排工具

你会注意到图中有两个看似孤立的区域——Mesos 和 Consul/etcd/Zookeeper 组。Mesos 是一个有趣的案例——它在 Docker 之前就存在了,它对 Docker 的支持是一个附加功能而不是核心功能。尽管如此,它工作得非常好,应该仔细评估,至少为了看看你可能在其他工具中想要哪些功能。相比之下,Consul、etcd 和 Zookeeper 根本不是编排工具。相反,它们提供了编排的重要补充:服务发现。
本章和下一章将导航这个编排生态系统。在本章中,我们将介绍一些工具,这些工具可以让你有更细粒度的控制,并且可能感觉从手动管理容器过渡过来不那么跳跃。我们将查看在单个主机和多个主机上管理 Docker 容器,然后查看保存和检索容器部署位置信息的方法。然后,在下一章中,我们将查看更完整的解决方案,这些解决方案抽象了很多细节。
阅读这两章时,当你遇到每个编排工具时,后退一步,尝试想出一个工具可能有用的场景可能会有所帮助。这将有助于明确某个特定工具对你是否相关。我们将沿途提供一些示例,帮助你开始。
我们将从关注单个计算机开始,逐步深入。
11.1. 简单的单主机 Docker
在本地机器上管理容器可能是一个痛苦的经历。Docker 提供的用于管理长时间运行容器的功能相对原始,使用链接和共享卷启动容器可能是一个令人沮丧的手动过程。
在第十章中,我们探讨了使用 Docker Compose 来简化链接管理,因此现在我们将解决另一个痛点,看看如何在单台机器上管理长时间运行的容器,使其更加稳健。
使用 systemd 管理主机的容器
在这个技术中,我们将带你设置一个简单的 Docker 服务与 systemd。如果你已经熟悉 systemd,这一章将相对容易理解,但我们假设你对这个工具没有先前的知识。
使用 systemd 来控制 Docker 对于拥有运营团队且更喜欢坚持使用他们已经理解和有工具支持的成熟技术的公司来说是有用的。
问题
你想要管理主机上运行的 Docker 容器服务。
解决方案
使用 systemd 来管理你的容器服务。
systemd 是一个系统管理守护进程,它在 Fedora 中取代了 SysV init 脚本。它管理你的系统上的服务——从挂载点到进程到一次性脚本,作为单独的“单元”。随着它传播到其他发行版和操作系统,它越来越受欢迎,尽管一些系统(以写作时的 Gentoo 为例)可能存在安装和启用它的问题。值得四处寻找其他人对你类似的设置中 systemd 的经验。
在这个技术中,我们将演示如何通过运行第一章中的待办事项应用程序来由 systemd 管理你的容器的启动。
安装 systemd
如果你的主机系统上没有 systemd(你可以通过运行systemctl status来检查,看你是否能得到一个连贯的响应),你可以使用标准的软件包管理器直接在你的主机操作系统上安装它。
如果你不喜欢以这种方式干扰你的主机系统,推荐的玩法是使用 Vagrant 来配置一个 systemd 就绪的虚拟机,如下面的列表所示。我们在这里简要介绍它,但请参阅附录 C 以获取有关安装 Vagrant 的更多建议。
列表 11.1.一个 Vagrant 设置
$ mkdir centos7_docker *1*
$ cd centos7_docker *1*
$ vagrant init jdiprizio/centos-docker-io *2*
$ vagrant up ) *3*
$ vagrant ssh *4*
-
1创建并进入一个新的文件夹
-
2初始化文件夹以用作 Vagrant 环境,指定 Vagrant 镜像
-
3启动虚拟机
-
4SSH 连接到虚拟机
注意
在写作时,jdiprizio/centos-docker-io 是一个合适且可用的虚拟机镜像。如果你在阅读时它不再可用,你可以将前面列表中的字符串替换为另一个镜像名称。你可以在 HashiCorp 的“Discover Vagrant Boxes”页面上搜索一个:app.vagrantup.com/boxes/search(“box”是 Vagrant 用来指代虚拟机镜像的术语)。为了找到这个镜像,我们搜索了“docker centos”。在尝试启动它之前,你可能需要查找有关命令行vagrant box add命令的帮助,以了解如何下载你的新虚拟机。
在 systemd 下设置简单的 Docker 应用程序
现在你有一个安装了 systemd 和 Docker 的机器,我们将使用它来运行第一章中的待办事项应用程序。
systemd 通过读取简单的 INI 文件格式的配置文件来工作。
提示
INI 文件是简单的文本文件,其基本结构由部分、属性和值组成。
首先,你需要以 root 用户在/etc/systemd/system/todo.service 中创建一个服务文件,如下所示。在这个文件中,你告诉 systemd 在这个主机上以端口 8000 运行名为“todo”的 Docker 容器。
列表 11.2. /etc/systemd/system/todo.service
[Unit] *1*
Description=Simple ToDo Application
After=docker.service *2*
Requires=docker.service *3*
[Service] *4*
Restart=always *5*
ExecStartPre=/bin/bash \
-c '/usr/bin/docker rm -f todo || /bin/true' *6*
ExecStartPre=/usr/bin/docker pull dockerinpractice/todo *7*
ExecStart=/usr/bin/docker run --name todo \
-p 8000:8000 dockerinpractice/todo *8*
ExecStop=/usr/bin/docker rm -f todo *9*
[Install] *10*
WantedBy=multi-user.target *11*
-
1单元部分定义了 systemd 对象的通用信息。
-
2在 Docker 服务启动后启动此单元。
-
3此单元要成功运行,Docker 服务必须正在运行。
-
4服务部分定义了特定于 systemd 服务单元类型的配置信息。
-
5如果服务终止,总是重新启动它。
-
6如果服务 termExecStartPre 定义了一个在单元启动前运行的命令。为了确保在启动之前删除容器,你在这里可以毫不犹豫地删除它。
-
7确保在运行容器之前已下载镜像。
-
8ExecStart 定义了在服务启动时运行的命令。
-
9ExecStop 定义了在服务停止时运行的命令。
-
10安装部分包含 systemd 启用单元时的信息。
-
11通知 systemd,当它进入多用户目标阶段时,你想启动此单元。
此配置文件应明确指出 systemd 提供了一种简单的声明性模式来管理进程,将依赖关系管理的细节留给 systemd 服务。这并不意味着你可以忽略细节,但它确实为你提供了大量管理 Docker(和其他)进程的工具。
注意
Docker 默认不设置任何容器重启策略,但请注意,你设置的任何策略都将与大多数进程管理器冲突。如果你正在使用进程管理器,请不要设置重启策略。
启用新单元只需调用systemctl enable命令。如果你想在这个单元启动时自动启动系统,你还可以在 multi-user.target.wants systemd 目录中创建一个符号链接。一旦完成,你可以使用systemctl start启动单元。
$ systemctl enable /etc/systemd/system/todo.service
$ ln -s '/etc/systemd/system/todo.service' \
'/etc/systemd/system/multi-user.target.wants/todo.service'
$ systemctl start todo.service
然后只需等待它启动。如果有问题,你会得到通知。
要检查一切是否正常,请使用systemctl status命令。它将打印出有关单元的一些一般信息,例如运行时间长短和进程 ID,然后是来自进程的若干日志行。在这种情况下,看到Swarm 服务器启动端口 8000是一个好兆头:
[root@centos system]# systemctl status todo.service
todo.service - Simple ToDo Application
Loaded: loaded (/etc/systemd/system/todo.service; enabled)
Active: active (running) since Wed 2015-03-04 19:57:19 UTC; 2min 13s ago
Process: 21266 ExecStartPre=/usr/bin/docker pull dockerinpractice/todo \
(code=exited, status=0/SUCCESS)
Process: 21255 ExecStartPre=/bin/bash -c /usr/bin/docker rm -f todo || \
/bin/true (code=exited, status=0/SUCCESS)
Process: 21246 ExecStartPre=/bin/bash -c /usr/bin/docker kill todo || \
/bin/true (code=exited, status=0/SUCCESS)
Main PID: 21275 (docker)
CGroup: /system.slice/todo.service
??21275 /usr/bin/docker run --name todo
-p 8000:8000 dockerinpractice/todo
Mar 04 19:57:24 centos docker[21275]: TodoApp.js:117: \
// TODO scroll into view
Mar 04 19:57:24 centos docker[21275]: TodoApp.js:176: \
if (i>=list.length()) { i=list.length()-1; } // TODO .length
Mar 04 19:57:24 centos docker[21275]: local.html:30: \
<!-- TODO 2-split, 3-split -->
Mar 04 19:57:24 centos docker[21275]: model/TodoList.js:29: \
// TODO one op - repeated spec? long spec?
Mar 04 19:57:24 centos docker[21275]: view/Footer.jsx:61: \
// TODO: show the entry's metadata
Mar 04 19:57:24 centos docker[21275]: view/Footer.jsx:80: \
todoList.addObject(new TodoItem()); // TODO create default
Mar 04 19:57:24 centos docker[21275]: view/Header.jsx:25: \
// TODO list some meaningful header (apart from the id)
Mar 04 19:57:24 centos docker[21275]: > todomvc-swarm@0.0.1 start /todo
Mar 04 19:57:24 centos docker[21275]: > node TodoAppServer.js
Mar 04 19:57:25 centos docker[21275]: Swarm server started port 8000
你现在可以访问端口 8000 上的服务器。
讨论
这种技术中的原则可以应用于不仅仅是 systemd——大多数进程管理器,包括其他 init 系统,都可以以类似的方式进行配置。如果你感兴趣,你可以利用这一点来替换系统上运行的现有服务(可能是一个 PostgreSQL 数据库)的 docker 化版本。
在下一个技术中,我们将进一步在 systemd 中实现我们在技术 77 中创建的 SQLite 服务器。
| |
编排宿主机的容器启动
与 docker-compose(截至写作时)不同,systemd 是一种成熟的技术,适用于生产。在这个技术中,我们将向你展示如何使用 systemd 实现类似于 docker-compose 的本地编排功能。
注意
如果你遇到这个技术的问题,你可能需要升级你的 Docker 版本。版本 1.7.0 或更高版本应该可以正常工作。
问题
你想在生产环境中在一个主机上管理更复杂的容器编排。
解决方案
使用具有依赖服务的 systemd 来管理你的容器。
为了演示在更复杂场景下使用 systemd 的方法,我们将重新实现来自技术 77 的 SQLite TCP 服务器示例,并在 systemd 中实现。展示了我们计划中的 systemd 服务单元配置的依赖关系。
图 11.2. systemd 单元依赖关系图

这与你在技术 77 中看到的 Docker Compose 示例中的架构类似。这里的一个关键区别是,而不是将 SQLite 服务视为一个单一的单一实体,每个容器都是一个独立的实体。在这个场景中,SQLite 代理可以独立于 SQLite 服务器停止。
这是 SQLite 服务器服务的列表。和之前一样,它依赖于 Docker 服务,但与之前技术中的待办示例有一些不同。
列表 11.3. /etc/systemd/system/sqliteserver.service
[Unit] *1*
Description=SQLite Docker Server
After=docker.service *2*
Requires=docker.service *3*
[Service]
Restart=always
ExecStartPre=-/bin/touch /tmp/sqlitedbs/test *4*
ExecStartPre=-/bin/touch /tmp/sqlitedbs/live *4*
ExecStartPre=/bin/bash \
-c '/usr/bin/docker kill sqliteserver || /bin/true' *5*
ExecStartPre=/bin/bash \ *5*
-c '/usr/bin/docker rm -f sqliteserver || /bin/true'
ExecStartPre=/usr/bin/docker \
pull dockerinpractice/docker-compose-sqlite *6*
ExecStart=/usr/bin/docker run --name sqliteserver \ *7*
-v /tmp/sqlitedbs/test:/opt/sqlite/db \
dockerinpractice/docker-compose-sqlite /bin/bash -c \
'socat TCP-L:12345,fork,reuseaddr \
EXEC:"sqlite3 /opt/sqlite/db",pty'
ExecStop=/usr/bin/docker rm -f sqliteserver *8*
[Install]
WantedBy=multi-user.target
-
1单元部分定义了 systemd 对象的一般信息。
-
2在 Docker 服务启动后启动此单元
-
3此单元要成功运行,Docker 服务必须正在运行。
-
4这些行确保在服务启动之前 SQLite 数据库文件存在。touch 命令前的破折号表示 systemd,如果命令返回错误代码,则启动应失败。
-
5ExecStartPre 定义了在单元启动之前要运行的命令。为了确保在启动容器之前将其删除,你在这里有偏见地删除它。
-
6确保在运行容器之前下载了镜像
-
7ExecStart 定义了服务启动时要运行的命令。请注意,我们将 socat 命令包装在“/bin/bash -c”调用中,以避免混淆,因为 ExecStart 行是由 systemd 运行的。
-
8ExecStop 定义了服务停止时要运行的命令。
小贴士
在 systemd 中,路径必须是绝对路径。
接下来是 SQLite 代理服务的列表。这里的关键区别在于代理服务依赖于你刚刚定义的服务进程,而这个服务进程又依赖于 Docker 服务。
列表 11.4. /etc/systemd/system/sqliteproxy.service
[Unit]
Description=SQLite Docker Proxy
After=sqliteserver.service *1*
Requires=sqliteserver.service *2*
[Service]
Restart=always
ExecStartPre=/bin/bash -c '/usr/bin/docker kill sqliteproxy || /bin/true'
ExecStartPre=/bin/bash -c '/usr/bin/docker rm -f sqliteproxy || /bin/true'
ExecStartPre=/usr/bin/docker pull dockerinpractice/docker-compose-sqlite
ExecStart=/usr/bin/docker run --name sqliteproxy \
-p 12346:12346 --link sqliteserver:sqliteserver \
dockerinpractice/docker-compose-sqlite /bin/bash \
-c 'socat TCP-L:12346,fork,reuseaddr TCP:sqliteserver:12345' *3*
ExecStop=/usr/bin/docker rm -f sqliteproxy
[Install]
WantedBy=multi-user.target
-
1代理单元必须在之前定义的 sqliteserver 服务之后运行。
-
2代理要求在启动之前服务器实例必须正在运行。
-
3运行容器
使用这两个配置文件,我们已经为在 systemd 的控制下安装和运行 SQLite 服务奠定了基础。现在我们可以启用这些服务:
$ sudo systemctl enable /etc/systemd/system/sqliteserver.service
ln -s '/etc/systemd/system/sqliteserver.service' \
'/etc/systemd/system/multi-user.target.wants/sqliteserver.service'
$ sudo systemctl enable /etc/systemd/system/sqliteproxy.service
ln -s '/etc/systemd/system/sqliteproxy.service' \
'/etc/systemd/system/multi-user.target.wants/sqliteproxy.service'
并启动它们:
$ sudo systemctl start sqliteproxy
$ telnet localhost 12346
[vagrant@centos ~]$ telnet localhost 12346
Trying ::1...
Connected to localhost.
Escape character is '^]'.
SQLite version 3.8.2 2013-12-06 14:53:30
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> select * from t1;
select * from t1;
test
注意,由于 SQLite 代理服务依赖于 SQLite 服务器服务来运行,你只需要启动代理——依赖项会自动启动。
讨论
在本地机器上管理长期运行的应用程序时面临的挑战之一是依赖服务的管理。例如,一个 Web 应用程序可能期望作为服务在后台运行,但也可能依赖于数据库和 Web 服务器。这可能听起来很熟悉——你在技术 13 中覆盖了 Web-app-db 结构。
技术 76 展示了如何使用依赖关系等设置这种结构,但像 systemd 这样的工具已经在这个问题上工作了一段时间,并且可能提供 Docker Compose 不提供的灵活性。例如,一旦你写好了服务文件,你可以启动你想要的任何一个,systemd 将处理启动任何依赖服务,甚至在必要时启动 Docker 守护进程本身。
11.2. 手动多主机 Docker
现在你已经熟悉了在机器上一些相当复杂的 Docker 容器排列,是时候考虑更宏大的目标了——让我们进入多主机世界,以便我们能够在更大规模上使用 Docker。
在本章的剩余部分,你将手动使用 Helios 运行一个多主机环境,以介绍你多主机 Docker 的概念。在下一章,你将看到更多自动化和复杂的方法来实现相同的结果以及更多。
使用 Helios 手动多主机 Docker
将一组机器的所有控制权交给一个应用程序可能会让人感到害怕,所以用更手动的方法慢慢适应是有好处的。
对于那些大部分基础设施都是静态的,并且对使用 Docker 运行关键服务感兴趣但(可以理解地)希望在过程中有人监督的公司来说,Helios 是理想的。
问题
你希望能够用容器配置多个 Docker 主机,但仍然保留对运行位置的手动控制。
解决方案
使用 Spotify 的 Helios 工具来精确管理其他主机上的容器。
Helios 是 Spotify 目前用于在生产环境中管理服务器的一个工具,它有一个令人愉悦的特性,那就是易于上手且稳定(正如你所期望的)。Helios 允许你管理跨多个主机的 Docker 容器的部署。它提供了一个单行命令界面,你可以使用它来指定你想要运行的内容以及运行的位置,以及查看当前状态的权限。
由于我们只是介绍 Helios,我们将为了简单起见在 Docker 内的单个节点上运行一切——请不要担心,任何与在多个主机上运行相关的内容都将被清楚地突出显示。Helios 的高层次架构在图 11.3 中概述。
图 11.3. Helios 安装的鸟瞰图
![Images/11fig03_alt.jpg]
如你所见,运行 Helios 时只需要一个额外的服务:Zookeeper。Helios 使用 Zookeeper 来跟踪所有主机的状态,并在主节点和代理节点之间作为通信通道。
小贴士
Zookeeper 是一个用 Java 编写的轻量级分布式数据库,它针对存储配置信息进行了优化。它是 Apache 开源软件产品套件的一部分。它在功能上与 etcd(你可以在第九章 chapter 9 中了解到,本章你还将再次看到)相似。
对于这项技术,你需要知道的是 Zookeeper 以一种方式存储数据,使得可以通过运行多个 Zookeeper 实例将其分布到多个节点上(既为了可扩展性也为了可靠性)。这听起来可能与我们第九章 chapter 9 中对 etcd 的描述相似——这两个工具有显著的相似之处。
要启动我们将在这项技术中使用的单个 Zookeeper 实例,请运行以下命令:
$ docker run --name zookeeper -d jplock/zookeeper:3.4.6
cd0964d2ba18baac58b29081b227f15e05f11644adfa785c6e9fc5dd15b85910
$ docker inspect -f '{{.NetworkSettings.IPAddress}}' zookeeper
<*co>172.17.0.9
注意
当在一个单独的节点上启动 Zookeeper 实例时,你将想要暴露端口以便其他主机可以访问,并使用卷来持久化数据。查看 Docker Hub 上的 Dockerfile 获取有关应使用哪些端口和文件夹的详细信息(hub.docker.com/r/jplock/zookeeper/~/dockerfile/)。你也可能想在多个节点上运行 Zookeeper,但配置 Zookeeper 集群超出了这项技术的范围。
你可以使用 zkCli.sh 工具检查 Zookeeper 存储的数据,无论是交互式地还是通过管道输入到它。初始启动时相当健谈,但它会带你进入一个交互式提示,你可以在其中运行针对 Zookeeper 存储数据的类似文件树结构的命令。
$ docker exec -it zookeeper bin/zkCli.sh
Connecting to localhost:2181
2015-03-07 02:56:05,076 [myid:] - INFO [main:Environment@100] - Client >
environment:zookeeper.version=3.4.6-1569965, built on 02/20/2014 09:09 GMT
2015-03-07 02:56:05,079 [myid:] - INFO [main:Environment@100] - Client >
environment:host.name=917d0f8ac077
2015-03-07 02:56:05,079 [myid:] - INFO [main:Environment@100] - Client >
environment:java.version=1.7.0_65
2015-03-07 02:56:05,081 [myid:] - INFO [main:Environment@100] - Client >
environment:java.vendor=Oracle Corporation
[...]
2015-03-07 03:00:59,043 [myid:] - INFO
[main-SendThread(localhost:2181):ClientCnxn$SendThread@1235] -
Session establishment complete on server localhost/0:0:0:0:0:0:0:1:2181,
sessionid = 0x14bf223e159000d, negotiated timeout = 30000
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
目前还没有任何进程针对 Zookeeper 运行,所以目前存储的只有一些内部 Zookeeper 信息。请保持此提示打开,随着我们的进展我们将重新访问它。
Helios 本身分为三个部分:
-
主节点——这是用于在 Zookeeper 中进行更改的接口。
-
代理——这个代理在每个 Docker 主机上运行,根据 Zookeeper 启动和停止容器,并将状态信息反馈回来。
-
命令行工具——这些工具用于向主节点发送请求。
图 11.4 展示了当我们对其执行操作时,最终系统是如何连接在一起的(箭头指示数据流)。
图 11.4. 在单主机 Helios 安装上启动容器

现在 Zookeeper 已经启动,是时候启动 Helios 了。我们需要在指定我们之前启动的 Zookeeper 节点 IP 地址的情况下运行主节点:
$ IMG=dockerinpractice/docker-helios
$ docker run -d --name hmaster $IMG helios-master --zk 172.17.0.9
896bc963d899154436938e260b1d4e6fdb0a81e4a082df50043290569e5921ff
$ docker logs --tail=3 hmaster
03:20:14.460 helios[1]: INFO [MasterService STARTING] ContextHandler: >
Started i.d.j.MutableServletContextHandler@7b48d370{/,null,AVAILABLE}
03:20:14.465 helios[1]: INFO [MasterService STARTING] ServerConnector: >
Started application@2192bcac{HTTP/1.1}{0.0.0.0:5801}
03:20:14.466 helios[1]: INFO [MasterService STARTING] ServerConnector: >
Started admin@28a0d16c{HTTP/1.1}{0.0.0.0:5802}
$ docker inspect -f '{{.NetworkSettings.IPAddress}}' hmaster
172.17.0.11
现在让我们看看 Zookeeper 中有什么新变化:
[zk: localhost:2181(CONNECTED) 1] ls /
[history, config, status, zookeeper]
[zk: localhost:2181(CONNECTED) 2] ls /status/masters
[896bc963d899]
[zk: localhost:2181(CONNECTED) 3] ls /status/hosts
[]
看起来 Helios 主节点创建了一系列新的配置项,包括将自己注册为主节点。不幸的是,我们目前还没有任何主机。
让我们通过启动一个代理来解决这个问题,该代理将使用当前主机的 Docker 套接字在以下位置启动容器:
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -d --name hagent \
dockerinpractice/docker-helios helios-agent --zk 172.17.0.9
5a4abcb271070d0171ca809ff2beafac5798e86131b72aeb201fe27df64b2698
$ docker logs --tail=3 hagent
03:30:53.344 helios[1]: INFO [AgentService STARTING] ContextHandler: >
Started i.d.j.MutableServletContextHandler@774c71b1{/,null,AVAILABLE}
03:30:53.375 helios[1]: INFO [AgentService STARTING] ServerConnector: >
Started application@7d9e6c27{HTTP/1.1}{0.0.0.0:5803}
03:30:53.376 helios[1]: INFO [AgentService STARTING] ServerConnector: >
Started admin@2bceb4df{HTTP/1.1}{0.0.0.0:5804}
$ docker inspect -f '{{.NetworkSettings.IPAddress}}' hagent
172.17.0.12
再次,让我们回到 Zookeeper 查看:
[zk: localhost:2181(CONNECTED) 4] ls /status/hosts
[5a4abcb27107]
[zk: localhost:2181(CONNECTED) 5] ls /status/hosts/5a4abcb27107
[agentinfo, jobs, environment, hostinfo, up]
[zk: localhost:2181(CONNECTED) 6] get /status/hosts/5a4abcb27107/agentinfo
{"inputArguments":["-Dcom.sun.management.jmxremote.port=9203", [...]
[...]
您可以看到,/status/hosts现在包含一个条目。进入主机对应的 Zookeeper 目录,可以揭示 Helios 存储的主机内部信息。
注意
当在多个主机上运行时,您需要将--name $(hostname -f)作为参数传递给 Helios 主节点和代理。您还需要为主节点暴露端口 5801 和 5802,为代理暴露端口 5803 和 5804。
让我们使与 Helios 的交互更加简单:
$ alias helios="docker run -i --rm dockerinpractice/docker-helios \
helios -z http://172.17.0.11:5801"
前面的别名意味着调用helios将启动一个临时容器来执行您想要的操作,并首先指向正确的 Helios 集群。请注意,命令行界面需要指向 Helios 主节点而不是 Zookeeper。
一切都已经设置好了。我们能够轻松地与我们的 Helios 集群交互,所以现在是时候尝试一个示例了。
$ helios create -p nc=8080:8080 netcat:v1 ubuntu:14.04.2 -- \
sh -c 'echo hello | nc -l 8080'
Creating job: {"command":["sh","-c","echo hello | nc -l 8080"], >
"creatingUser":null,"env":{},"expires":null,"gracePeriod":null, >
"healthCheck":null,"id": >
"netcat:v1:2067d43fc2c6f004ea27d7bb7412aff502e3cdac", >
"image":"ubuntu:14.04.2","ports":{"nc":{"externalPort":8080, >
"internalPort":8080,"protocol":"tcp"}},"registration":{}, >
"registrationDomain":"","resources":null,"token":"","volumes":{}}
Done.
netcat:v1:2067d43fc2c6f004ea27d7bb7412aff502e3cdac
$ helios jobs
JOB ID NAME VERSION HOSTS COMMAND ENVIRONMENT
netcat:v1:2067d43 netcat v1 0 sh -c "echo hello | nc -l 8080"
Helios 围绕作业的概念构建——在将任务发送到主机执行之前,必须将所有要执行的内容表达为作业。至少,您需要一个包含 Helios 启动容器所需的基本信息的镜像:要执行的命令以及任何端口、卷或环境选项。您可能还希望使用一些其他高级选项,包括健康检查、过期日期和服务注册。
之前的命令创建了一个将在端口 8080 上监听、打印“hello”给第一个连接到端口的实体,然后终止的任务。
您可以使用helios hosts列出可用于作业部署的主机,然后使用helios deploy实际执行部署。然后helios status命令显示作业已成功启动:
$ helios hosts
HOST STATUS DEPLOYED RUNNING CPUS MEM LOAD AVG MEM USAGE >
OS HELIOS DOCKER
5a4abcb27107.Up 19 minutes 0 0 4 7 gb 0.61 0.84 >
Linux 3.13.0-46-generic 0.8.213 1.3.1 (1.15)
$ helios deploy netcat:v1 5a4abcb27107
Deploying Deployment{jobId=netcat:v1: >
2067d43fc2c6f004ea27d7bb7412aff502e3cdac, goal=START, deployerUser=null} >
on [5a4abcb27107]
5a4abcb27107: done
$ helios status
JOB ID HOST GOAL STATE CONTAINER ID PORTS
netcat:v1:2067d43 5a4abcb27107.START RUNNING b1225bc nc=8080:8080
当然,我们现在想验证服务是否正常工作:
$ curl localhost:8080
hello
$ helios status
JOB ID HOST GOAL STATE CONTAINER ID PORTS
netcat:v1:2067d43 5a4abcb27107.START PULLING_IMAGE b1225bc nc=8080:8080
curl 的结果清楚地告诉我们服务正在运行,但现在 helios status 显示了一些有趣的内容。在定义作业时,我们注意到在服务“hello”之后,作业将终止,但前面的输出显示了一个 PULLING_IMAGE 状态。这是由于 Helios 管理作业的方式——一旦你部署到主机上,Helios 将尽力保持作业运行。你在这里看到的状态是 Helios 正在完成完整的作业启动过程,这恰好涉及到确保图像被拉取。
最后,我们需要清理自己的事情。
$ helios undeploy -a --yes netcat:v1
Undeploying netcat:v1:2067d43fc2c6f004ea27d7bb7412aff502e3cdac from >
[5a4abcb27107]
5a4abcb27107: done
$ helios remove --yes netcat:v1
Removing job netcat:v1:2067d43fc2c6f004ea27d7bb7412aff502e3cdac
netcat:v1:2067d43fc2c6f004ea27d7bb7412aff502e3cdac: done
我们要求将作业从所有节点中移除(如果需要,终止它并停止任何更多的自动重启),然后我们删除了作业本身,这意味着它不能再部署到任何其他节点。
讨论
Helios 是将你的容器部署到多个主机的一种简单且可靠的方式。与我们在后面将要讨论的许多技术不同,背后没有魔法来确定适当的位置——Helios 以最小的麻烦在你想放置容器的确切位置启动容器。
但这种简单性一旦你转移到更高级的部署场景中就会付出代价——像资源限制、动态扩展等功能目前都缺失,因此你可能发现自己需要重新发明部分像 Kubernetes (技术 88) 这样的工具来达到你想要的部署行为。
11.3. 服务发现:这里有什么?
本章的引言将服务发现称为编排的另一方面——能够将你的应用程序部署到数百台不同的机器上是没有问题的,但如果你无法找出哪些应用程序位于何处,你就无法真正使用它们。
尽管它不像编排那样饱和,但服务发现领域仍然有许多竞争对手。它们都提供略微不同的功能集并不利于这一领域。
在服务发现方面,通常有两个功能是人们所期望的:一个通用的键/值存储和一个通过某些方便的接口(可能是 DNS)检索服务端点的方法。etcd 和 Zookeeper 是前者的例子,而 SkyDNS(我们不会深入探讨的工具)是后者的例子。实际上,SkyDNS 使用 etcd 来存储它所需的信息。
使用 Consul 来发现服务
etcd 是一个非常流行的工具,但它确实有一个特定的竞争对手经常被提及:Consul。这有点奇怪,因为还有其他工具与 etcd 更相似(Zookeeper 与 etcd 有相似的功能集,但实现语言不同),而 Consul 通过一些有趣的功能来区分自己,如服务发现和健康检查。实际上,如果你眯起眼睛,Consul 可能看起来有点像 etcd、SkyDNS 和 Nagios 的结合体。
问题
您需要能够分发信息到、在容器内发现服务以及监控一组容器。
解决方案
在每个 Docker 主机上启动带有 Consul 的容器,以提供服务目录和配置通信系统。
Consul 试图成为一个通用的工具,用于执行在需要协调多个独立服务时所需的一些重要任务。这些任务可以通过其他工具执行,但在一个地方配置它们可能很有用。从高层次来看,Consul 提供以下功能:
-
服务配置—一个用于存储和共享小值的键/值存储,例如 etcd 和 Zookeeper
-
服务发现—一个用于注册服务的 API 和一个用于发现它们的 DNS 端点,例如 SkyDNS
-
服务监控—一个用于注册健康检查的 API,例如 Nagios
您可以使用所有、一些或其中之一的功能,因为没有绑定。如果您有现有的监控基础设施,就没有必要用 Consul 替换它。
这个技术将涵盖 Consul 的服务发现和服务监控方面,但不涉及键/值存储。在这一点上,etcd 和 Consul 之间有很强的相似性,使得第九章中的最后两个技术(技术 74 和 75)在阅读 Consul 文档后可以相互转换。
图 11.5 展示了一个典型的 Consul 配置。
图 11.5. 一个典型的 Consul 配置

存储在 Consul 中的数据是服务器代理的责任。这些代理负责对存储的信息达成共识——这一概念存在于大多数分布式数据存储系统中。简而言之,如果您丢失了不到一半的服务器代理,您有保证能够恢复您的数据(参见技术 74 中关于 etcd 的示例)。因为这些服务器非常重要并且有更高的资源需求,将它们放在专用机器上是一个典型的选择。
注意
尽管这个技术中的命令将把 Consul 数据目录(/data)留在容器内,但通常将此目录指定为至少服务器的卷是一个好主意,这样您可以保留备份。
建议您控制的所有可能希望与 Consul 交互的机器都应该运行一个客户端代理。这些代理将请求转发到服务器并运行健康检查。
启动 Consul 的第一步是启动一个服务器代理:
c1 $ IMG=dockerinpractice/consul-server
c1 $ docker pull $IMG
[...]
c1 $ ip addr | grep 'inet ' | grep -v 'lo$\|docker0$\|vbox.*$'
inet 192.168.1.87/24 brd 192.168.1.255 scope global wlan0
c1 $ EXTIP1=192.168.1.87
c1 $ echo '{"ports": {"dns": 53}}' > dns.json
c1 $ docker run -d --name consul --net host \
-v $(pwd)/dns.json:/config/dns.json $IMG -bind $EXTIP1 -client $EXTIP1 \
-recursor 8.8.8.8 -recursor 8.8.4.4 -bootstrap-expect 1
88d5cb48b8b1ef9ada754f97f024a9ba691279e1a863fa95fa196539555310c1
c1 $ docker logs consul
[...]
Client Addr: 192.168.1.87 (HTTP: 8500, HTTPS: -1, DNS: 53, RPC: 8400)
Cluster Addr: 192.168.1.87 (LAN: 8301, WAN: 8302)
[...]
==> Log data will now stream in as it occurs:
2015/08/14 12:35:41 [INFO] serf: EventMemberJoin: mylaptop 192.168.1.87
[...]
2015/08/14 12:35:43 [INFO] consul: member 'mylaptop' joined, marking >
health alive
2015/08/14 12:35:43 [INFO] agent: Synced service 'consul'
因为我们想将 Consul 用作 DNS 服务器,所以我们已将一个文件插入到 Consul 读取配置的文件夹中,以请求它监听端口 53(DNS 协议的注册端口)。然后我们使用您可能从早期技术中认识到的命令序列来尝试找到机器的外部 IP 地址,以便与其他代理通信并监听客户端请求。
注意
IP 地址0.0.0.0通常用于指示应用程序应在机器上的所有可用接口上监听。我们故意没有这样做,因为一些 Linux 发行版有一个监听在127.0.0.1的 DNS 缓存守护进程,这禁止在0.0.0.0:53上监听。
在前面的docker run命令中有三个值得注意的事项:
-
我们使用了
--net host。虽然在 Docker 世界中这可能会被视为一个错误,但替代方案是在命令行上暴露多达八个端口——这是一个个人偏好的问题,但我们认为在这里是合理的。它还有助于绕过一个潜在的 UDP 通信问题。如果你选择手动操作,则不需要设置 DNS 端口——你可以将默认的 Consul DNS 端口(8600)在主机上暴露为端口 53。 -
两个
recursor参数告诉 Consul 在请求的地址由 Consul 本身未知时,应该查看哪些 DNS 服务器。 -
-bootstrap-expect 1参数意味着 Consul 集群将仅用一个代理启动,这并不稳健。典型的设置会将此设置为 3(或更多),以确保集群在所需数量的服务器加入之前不会启动。要启动额外的服务器代理,请添加一个-join参数,正如我们将在启动客户端时讨论的那样。
现在让我们转到第二台机器,启动一个客户端代理,并将其添加到我们的集群中。
警告 由于 Consul 期望在与其他代理通信时能够监听特定的端口集,因此在单个机器上设置多个代理同时展示其在现实世界中的工作方式是有些棘手的。我们现在将使用不同的主机——如果你决定使用 IP 别名,请确保传递一个-node newAgent,因为默认情况下将使用主机名,这可能会产生冲突。
c2 $ IMG=dockerinpractice/consul-agent
c2 $ docker pull $IMG
[...]
c2 $ EXTIP1=192.168.1.87
c2 $ ip addr | grep docker0 | grep inet
inet 172.17.42.1/16 scope global docker0
c2 $ BRIDGEIP=172.17.42.1
c2 $ ip addr | grep 'inet ' | grep -v 'lo$\|docker0$'
inet 192.168.1.80/24 brd 192.168.1.255 scope global wlan0
c2 $ EXTIP2=192.168.1.80
c2 $ echo '{"ports": {"dns": 53}}' > dns.json
c2 $ docker run -d --name consul-client --net host \
-v $(pwd)/dns.json:/config/dns.json $IMG -client $BRIDGEIP -bind $EXTIP2 \
-join $EXTIP1 -recursor 8.8.8.8 -recursor 8.8.4.4
5454029b139cd28e8500922d1167286f7e4fb4b7220985ac932f8fd5b1cdef25
c2 $ docker logs consul-client
[...]
2015/08/14 19:40:20 [INFO] serf: EventMemberJoin: mylaptop2 192.168.1.80
[...]
2015/08/14 13:24:37 [INFO] consul: adding server mylaptop >
(Addr: 192.168.1.87:8300) (DC: dc1)
注意
我们使用的镜像基于 gliderlabs/consul-server:0.5 和 gliderlabs/consul-agent:0.5,并包含一个 Consul 的新版本,以避免 UDP 通信中可能出现的“Refuting a suspect message”等日志记录的潜在问题。当镜像的 0.6 版本发布时,你可以切换回 gliderlabs 的镜像。
所有客户端服务(HTTP、DNS 等)都已配置为在 Docker 桥接 IP 地址上监听。这为容器提供了一个已知的位置,它们可以从 Consul 检索信息,并且它只在机器内部暴露 Consul,迫使其他机器直接访问服务器代理,而不是通过客户端代理到服务器代理的较慢路径。为确保桥接 IP 地址在所有主机上保持一致,你可以查看 Docker 守护进程的--bip参数。
如前所述,我们已经找到了外部 IP 地址并将集群通信绑定到它。-join参数告诉领事最初在哪里查找以找到集群。不用担心对集群形成进行微观管理——当两个代理最初相遇时,它们会进行“八卦”,传递有关在集群中找到其他代理的信息。最后的-recursor参数告诉领事用于 DNS 请求的 DNS 服务器(这些请求不是尝试查找已注册的服务)。
让我们验证代理是否已通过客户端机器上的 HTTP API 连接到服务器。我们将使用的 API 调用将返回客户端代理当前认为在集群中的成员列表。在大型、快速变化的集群中,这可能并不总是与集群成员匹配——为此还有一个(较慢)的 API 调用。
c2 $ curl -sSL $BRIDGEIP:8500/v1/agent/members | tr ',' '\n' | grep Name
[{"Name":"mylaptop2"
{"Name":"mylaptop"
现在领事基础设施已经搭建完成,是时候看看您如何注册和发现服务了。注册的典型流程是在初始化后让您的应用程序对本地客户端代理进行 API 调用,这会提示客户端代理将信息分发到服务器代理。为了演示目的,我们将手动执行注册步骤。
c2 $ docker run -d --name files -p 8000:80 ubuntu:14.04.2 \
python3 -m http.server 80
96ee81148154a75bc5c8a83e3b3d11b73d738417974eed4e019b26027787e9d1
c2 $ docker inspect -f '{{.NetworkSettings.IPAddress}}' files
172.17.0.16
c2 $ /bin/echo -e 'GET / HTTP/1.0\r\n\r\n' | nc -i1 172.17.0.16 80 \
| head -n 1
HTTP/1.0 200 OK
c2 $ curl -X PUT --data-binary '{"Name": "files", "Port": 8000}' \
$BRIDGEIP:8500/v1/agent/service/register
c2 $ docker logs consul-client | tail -n 1
2015/08/15 03:44:30 [INFO] agent: Synced service 'files'
在这里,我们在容器中设置了一个简单的 HTTP 服务器,将其暴露在主机上的 8000 端口,并检查它是否工作。然后我们使用 curl 和领事 HTTP API 注册了一个服务定义。这里绝对必要的是服务的名称——端口,以及领事文档中列出的其他字段都是可选的。ID 字段值得提一下——它默认为服务的名称,但必须在所有服务中是唯一的。如果您想有多个服务实例,您需要指定它。
来自领事日志的行告诉我们服务已同步,因此我们应该能够从服务 DNS 接口检索有关它的信息。这些信息来自服务器代理,因此它作为验证,表明该服务已被接受到领事目录中。您可以使用dig命令查询服务 DNS 信息并检查其是否存在:
c2 $ EXTIP1=192.168.1.87
c2 $ dig @$EXTIP1 files.service.consul +short *1*
192.168.1.80
c2 $ BRIDGEIP=172.17.42.1
c2 $ dig @$BRIDGEIP files.service.consul +short *2*
192.168.1.80
c2 $ dig @$BRIDGEIP files.service.consul srv +short *3*
1 1 8000 mylaptop2.node.dc1.consul.
c2 $ docker run -it --dns $BRIDGEIP ubuntu:14.04.2 bash *4*
root@934e9c26bc7e:/# ping -c1 -q www.google.com *5*
PING www.google.com (216.58.210.4) 56(84) bytes of data.
--- www.google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 25.358/25.358/25.358/0.000 ms
root@934e9c26bc7e:/# ping -c1 -q files.service.consul *6*
PING files.service.consul (192.168.1.80) 56(84) bytes of data.
--- files.service.consul ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.062/0.062/0.062/0.000 ms
-
1从服务器代理 DNS 查找文件服务的 IP 地址。此 DNS 服务对您的领事集群中的任意机器都可用,允许它们也能从服务发现中受益。
-
2从客户端代理 DNS 查找文件服务的 IP 地址。如果使用\(BRIDGEIP 失败,您可能希望尝试使用\)EXTIP1。
-
3从客户端代理 DNS 请求文件服务的 SRV 记录
-
4启动一个配置为仅使用本地客户端代理作为 DNS 服务器的容器
-
5验证外部地址的查找仍然有效
-
6验证服务查找在容器内自动工作
注意
SRV 记录是通过 DNS 通信服务信息的一种方式,包括协议、端口和其他条目。在前面的例子中,您可以在响应中看到端口号,并且您得到了提供服务的机器的规范主机名而不是 IP 地址。
高级用户可能希望避免手动设置--dns参数,而是通过配置 Docker 守护进程本身的--dns和--bip参数来避免,但请记住覆盖 Consul 代理的默认值,否则您可能会遇到意外的行为。
在技术 80 中,Consul DNS 服务与 Docker 虚拟网络之间的相似之处很有趣——两者都允许您通过可读的名称发现容器,而 Docker 具有内置的跨多个节点使用 overlay 网络使此功能工作的能力。关键区别在于 Consul 存在于 Docker 之外,因此可能更容易集成到现有系统中。
然而,如本技术的开头所述,Consul 还有一个有趣的功能,我们将对其进行探讨:健康检查。
健康检查是一个大主题,所以我们将在全面的 Consul 文档中留出细节,并查看监控的一个选项——脚本检查。它运行一个命令,并根据返回值设置健康状态,0 表示成功,1 表示警告,任何其他值表示关键。您可以在最初定义服务时注册健康检查,或者在我们这里这样做的一个单独的 API 调用中。
c2 $ cat >check <<'EOF' *1*
#!/bin/sh
set -o errexit
set -o pipefail
SVC_ID="$1"
SVC_PORT=\
"$(wget -qO - 172.17.42.1:8500/v1/agent/services | jq ".$SVC_ID.Port")"
wget -qsO - "localhost:$SVC_PORT"
echo "Success!"
EOF
c2 $ cat check | docker exec -i consul-client sh -c \
'cat > /check && chmod +x /check' *2*
c2 $ cat >health.json <<'EOF' *3*
{
"Name": "filescheck",
"ServiceID": "files",
"Script": "/check files",
"Interval": "10s"
}
EOF
c2 $ curl -X PUT --data-binary @health.json \
172.17.42.1:8500/v1/agent/check/register *4*
c2 $ sleep 300 *5*
c2 $ curl -sSL 172.17.42.1:8500/v1/health/service/files | \
python -m json.tool | head -n 13 *6*
[
{
"Checks": [
{
"CheckID": "filescheck",
"Name": "filescheck",
"Node": "mylaptop2",
"Notes": "",
"Output": "/check: line 6: jq: not \
found\nConnecting to 172.17.42.1:8500 (172.17.42.1:8500)\n",
"ServiceID": "files",
"ServiceName": "files",
"Status": "critical"
},
c2 $ dig @$BRIDGEIP files.service.consul srv +short *7*
c2 $
-
1创建一个检查脚本,验证服务的 HTTP 状态码是否为“200 OK”。服务端口从作为参数传递给脚本的服务 ID 中查找。
-
2将检查脚本复制到 Consul 代理容器中
-
3创建一个用于发送到 Consul HTTP API 的健康检查定义。服务 ID 必须在 ServiceID 字段和脚本命令行中指定。
-
4将健康检查 JSON 提交给 Consul 代理
-
5等待检查输出被传达给服务器代理
-
6检索您已注册的检查的健康检查信息
-
7尝试查找文件服务,但没有结果
注意
由于健康检查的输出可能在每次执行时都会改变(例如,如果它包含时间戳),Consul 仅在状态改变时或每五分钟(尽管这个间隔是可配置的)与服务器同步检查输出。因为状态最初是关键的,所以在这种情况下没有初始状态改变,因此您需要等待间隔以获取输出。
我们为文件服务添加了一个每 10 秒运行一次的健康检查,但检查结果显示该服务处于关键状态。因此,Consul 已自动将失败的端点从 DNS 返回的条目中移除,使我们没有服务可用。这在生产环境中自动从多后端服务中移除服务器特别有用。
在容器内运行 Consul 时遇到错误的原因是一个重要的注意事项。所有检查都在容器内运行,因此,由于检查脚本必须复制到容器中,你还需要确保任何需要的命令都已安装在容器中。在这种情况下,我们缺少jq命令(一个从 JSON 中提取信息的有用工具),我们可以手动安装,尽管对于生产环境来说,正确的方法是向镜像中添加层。
c2 $ docker exec consul-client sh -c 'apk update && apk add jq'
fetch http://dl-4.alpinelinux.org/alpine/v3.2/main/x86_64/APKINDEX.tar.gz
v3.2.3 [http://dl-4.alpinelinux.org/alpine/v3.2/main]
OK: 5289 distinct packages available
(1/1) Installing jq (1.4-r0)
Executing busybox-1.23.2-r0.trigger
OK: 14 MiB in 28 packages
c2 $ docker exec consul-client sh -c \
'wget -qO - 172.17.42.1:8500/v1/agent/services | jq ".files.Port"'
8000
c2 $ sleep 15
c2 $ curl -sSL 172.17.42.1:8500/v1/health/service/files | \
python -m json.tool | head -n 13
{
"Checks": [
{
"CheckID": "filescheck",
"Name": "filescheck",
"Node": "mylaptop2",
"Notes": "",
"Output": "Success!\n",
"ServiceID": "files",
"ServiceName": "files",
"Status": "passing"
},
我们现在使用 Alpine Linux(见[技术 57)包管理器将jq安装到镜像中,通过手动执行脚本中之前失败的行来验证它是否工作,然后等待检查重新运行。现在它成功了!
使用脚本健康检查,你现在拥有了构建围绕应用程序的监控的关键构建块。如果你能将健康检查表达为一系列在终端中运行的命令,你就可以让 Consul 自动运行它——这不仅仅限于 HTTP 状态。如果你发现自己想要检查 HTTP 端点返回的状态码,那么你很幸运,因为这是一项如此常见的任务,以至于 Consul 中的三种健康检查类型之一就是专门为此设计的,而且你不需要使用脚本健康检查(我们上面为了说明目的这样做)。
最后一种健康检查类型,即生存时间,需要与你的应用程序进行更深入的集成。状态必须定期设置为健康,否则检查将自动设置为失败。结合这三种类型的健康检查,你可以在系统之上构建全面的监控。
为了完成这项技术,我们将查看服务器代理镜像中包含的可选 Consul Web 界面。它提供了对集群当前状态的宝贵洞察。你可以通过访问服务器代理的外部 IP 地址上的端口 8500 来访问它。在这种情况下,你需要访问$EXTIP1:8500。记住,即使你在服务器代理主机上,localhost或127.0.0.1也不会工作。
讨论
我们在这个技术中涵盖了大量的内容——Consul 是一个大主题!幸运的是,正如你在技术 74 中关于利用 etcd 中的键值存储所获得的知识可以迁移到其他键值存储(如 Consul)一样,这个服务发现知识也可以迁移到其他提供 DNS 接口的工具(SkyDNS 可能是你遇到的一个)。
我们所讨论的与使用主机网络堆栈和使用外部 IP 地址相关的细微差别也是可以迁移的。大多数需要跨多个节点进行发现的容器化分布式工具可能存在类似问题,因此了解这些潜在问题是有价值的。
| |
使用 Registrator 进行自动服务注册
到目前为止,Consul(以及任何服务发现工具)的明显缺点是管理服务条目创建和删除的开销。如果你将其集成到你的应用程序中,你将有多处实现,并且有多个可能出错的地方。
集成也不适用于你无法完全控制的程序,所以当你启动数据库等时,你最终将不得不编写包装脚本。
问题
你不希望手动管理 Consul 中的服务条目和健康检查。
解决方案
使用注册器。
这种技术将在之前的技术基础上构建,并假设你有一个两部分的 Consul 集群可用,如之前所述。我们还将假设其中没有服务,所以你可能需要重新创建你的容器从头开始。
注册器(gliderlabs.com/registrator/latest/)简化了管理 Consul 服务的复杂性——它监视容器的启动和停止,根据暴露的端口和容器环境变量注册服务。看到这一功能的最简单方法是亲自尝试。
我们所做的一切都将是在具有客户端代理的机器上。如前所述,除了服务器代理之外,不应在其他机器上运行任何容器。
启动注册器所需的命令如下:
$ IMG=gliderlabs/registrator:v6
$ docker pull $IMG
[...]
$ ip addr | grep 'inet ' | grep -v 'lo$\|docker0$'
inet 192.168.1.80/24 brd 192.168.1.255 scope global wlan0
$ EXTIP=192.168.1.80
$ ip addr | grep docker0 | grep inet
inet 172.17.42.1/16 scope global docker0
$ BRIDGEIP=172.17.42.1
$ docker run -d --name registrator -h $(hostname)-reg \
-v /var/run/docker.sock:/tmp/docker.sock $IMG -ip $EXTIP -resync \
60 consul://$BRIDGEIP:8500 # if this fails, $EXTIP is an alternative
b3c8a04b9dfaf588e46a255ddf4e35f14a9d51199fc6f39d47340df31b019b90
$ docker logs registrator
2015/08/14 20:05:57 Starting registrator v6 ...
2015/08/14 20:05:57 Forcing host IP to 192.168.1.80
2015/08/14 20:05:58 consul: current leader 192.168.1.87:8300
2015/08/14 20:05:58 Using consul adapter: consul://172.17.42.1:8500
2015/08/14 20:05:58 Listening for Docker events ...
2015/08/14 20:05:58 Syncing services on 2 containers
2015/08/14 20:05:58 ignored: b3c8a04b9dfa no published ports
2015/08/14 20:05:58 ignored: a633e58c66b3 no published ports
这里前几个命令,用于拉取镜像和查找外部 IP 地址,应该看起来很熟悉。这个 IP 地址被提供给注册器,以便它知道为服务宣传哪个 IP 地址。Docker 套接字被挂载,以便注册器能够自动通知容器启动和停止。我们还告诉注册器如何连接到 Consul 代理,并且我们希望所有容器每 60 秒刷新一次。由于注册器应该自动通知容器更改,因此此最终设置有助于减轻注册器可能错过更新的影响。
现在,注册器正在运行,注册第一个服务变得极其简单。
$ curl -sSL 172.17.42.1:8500/v1/catalog/services | python -m json.tool
{
"consul": []
}
$ docker run -d -e "SERVICE_NAME=files" -p 8000:80 ubuntu:14.04.2 python3 \
-m http.server 80
3126a8668d7a058333d613f7995954f1919b314705589a9cd8b4e367d4092c9b
$ docker inspect 3126a8668d7a | grep 'Name.*/'
"Name": "/evil_hopper",
$ curl -sSL 172.17.42.1:8500/v1/catalog/services | python -m json.tool
{
"consul": [],
"files": []
}
$ curl -sSL 172.17.42.1:8500/v1/catalog/service/files | python -m json.tool
[
{
"Address": "192.168.1.80",
"Node": "mylaptop2",
"ServiceAddress": "192.168.1.80",
"ServiceID": "mylaptop2-reg:evil_hopper:80",
"ServiceName": "files",
"ServicePort": 8000,
"ServiceTags": null
}
]
在注册服务时,我们唯一需要付出的努力是传递一个环境变量来告诉注册器使用哪个服务名称。默认情况下,注册器使用基于斜杠之后和标签之前的容器名称组件的名称:“mycorp.com/myteam/myimage:0.5”将具有名称“myimage”。这是否有用或者你想手动指定某些内容,将取决于你的命名约定。
其余的值基本上如您所期望的那样。注册器已经发现了正在监听的端口,将其添加到 Consul,并设置了一个服务 ID,试图给出你可以找到容器的提示(这就是为什么在注册器容器中设置了主机名)。
讨论
Registrator 在处理快速变化的环境和容器高周转率方面非常出色,确保你不需要担心你的服务创建检查被创建。
除了服务详情外,如果存在,Registrator 还会从环境中收集一些信息,包括标签、每个端口的(如果有多个)服务名称,以及使用健康检查(如果你使用 Consul 作为数据存储)。通过在环境中指定检查详情的 JSON 格式,可以启用所有三种类型的 Consul 健康检查——你可以在“Registrator 后端”文档的 Consul 部分gliderlabs.com/registrator/latest/user/backends/#consul中了解更多信息,或者回顾先前的技术以获得对 Consul 健康检查的简要介绍。
摘要
-
systemd 单元对于控制单台机器上的容器执行非常有用。
-
可以在 systemd 单元中表达依赖关系以提供启动编排。
-
Helios 是一个生产级、简单、多主机编排解决方案。
-
Consul 可以存储有关你服务的信息,从而实现动态服务发现。
-
Registrator 可以自动将基于容器的服务注册到 Consul 中。
第十二章. 以 Docker 为操作系统的数据中心
本章涵盖
-
如何使用官方 Docker 解决方案进行编排
-
Mesos 可以用来管理 Docker 容器的不同方式
-
Docker 编排生态系统中的两个重量级工具,Kubernetes 和 OpenShift
如果你回顾一下上一章中的图 11.1,我们现在将继续沿着树的分支向下移动,并转向那些可以去除一些细节以提高生产力的工具。这些工具大多数都是针对跨多台机器的大规模部署而设计的,但你完全可以在一台机器上使用它们。
就像上一章一样,我们建议为每个工具尝试想出一个场景,以明确你环境中可能的用例。我们将继续在过程中给出示例作为起点。
12.1. 多主机 Docker
将 Docker 容器移动到目标机器并启动的最佳流程在 Docker 界是一个备受争议的话题。许多知名公司已经创建了他们自己的做事方式,并将其发布到世界。如果你能决定使用哪些工具,你将能从中获得巨大的益处。
这是一个快速发展的主题——我们见证了多个 Docker 编排工具的诞生和消亡,因此我们在考虑是否迁移到全新的工具时建议谨慎。因此,我们尝试选择了具有显著稳定性或动力的工具(或两者兼有)。
使用 swarm 模式的无缝 Docker 集群
能够完全控制您的集群是件好事,但有时微观管理并不是必要的。实际上,如果您有多个没有复杂要求的应用程序,您可以充分利用 Docker 在任何地方运行的承诺——没有理由您不能将容器投放到集群中,让集群决定在哪里运行它们。
如果实验室能够将计算密集型问题分解成小块,Swarm 模式对于研究实验室可能很有用。这将使他们能够非常容易地在机器集群上运行他们的问题。
问题
您有多个安装了 Docker 的主机,并且您希望能够在不需要微观管理它们运行位置的情况下启动容器。
解决方案
使用 Docker 的 Swarm 模式,这是 Docker 本身构建的用于处理编排的功能。
Docker 的 Swarm 模式是 Docker Inc. 提供的官方解决方案,用于将一组主机视为单个 Docker 守护进程并将服务部署到它们。它的命令行与您熟悉的 docker run 命令非常相似。Swarm 模式是从 Docker 官方工具演变而来的,您会将其与 Docker 一起使用,并且它已被集成到 Docker 守护进程本身。如果您在任何地方看到对“Docker Swarm”的旧引用,它们可能指的是较旧的工具。
Docker 集群由多个节点组成。每个节点可能是一个管理者或一个工作节点,这些角色是灵活的,可以在集群中随时更改。管理者负责协调服务部署到可用的节点,而工作节点只会运行容器。默认情况下,管理者也可以运行容器,但您将看到如何更改这一点。
当管理者启动时,它会为集群初始化一些状态,然后监听来自其他节点以添加到集群的传入连接。
注意
集群中使用的所有 Docker 版本必须至少为 1.12.0。理想情况下,您应该尽量保持所有版本完全相同,否则您可能会遇到由于版本不兼容而产生的问题。
首先,让我们创建一个新的集群:
h1 $ ip addr show | grep 'inet ' | grep -v 'lo$\|docker0$' # get external IP
inet 192.168.11.67/23 brd 192.168.11.255 scope global eth0
h1 $ docker swarm init --advertise-addr 192.168.11.67
Swarm initialized: current node (i5vtd3romfl9jg9g4bxtg0kis) is now a
manager.
To add a worker to this swarm, run the following command:
docker swarm join \
--token SWMTKN-1-4blo74l0m2bu5p8synq3w4239vxr1pyoa29cgkrjonx0tuid68
-dhl9o1b62vrhhi0m817r6sxp2 \
192.168.11.67:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and
follow the instructions.
这创建了一个新的集群,并将主机 h1 上的 Docker 守护进程设置为管理者。
现在,您可以检查您新创建的集群:
h1 $ docker info
[...]
Swarm: active
NodeID: i5vtd3romfl9jg9g4bxtg0kis
Is Manager: true
ClusterID: sg6sfmsa96nir1fbwcf939us1
Managers: 1
Nodes: 1
Orchestration:
Task History Retention Limit: 5
Raft:
Snapshot Interval: 10000
Number of Old Snapshots to Retain: 0
Heartbeat Tick: 1
Election Tick: 3
Dispatcher:
Heartbeat Period: 5 seconds
CA Configuration:
Expiry Duration: 3 months
Node Address: 192.168.11.67
Manager Addresses:
192.168.11.67:2377
[...]
h1 $ docker node ls
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
i5vtd3romfl9jg9g4bxtg0kis * h1 Ready Active Leader
现在,您可以通过运行管理者启动后指定的命令,使不同主机上的 Docker 守护进程加入作为工作节点:
h2 $ docker swarm join \
--token SWMTKN-1-4blo74l0m2bu5p8synq3w4239vxr1pyoa29cgkrjonx0tuid68
-dhl9o1b62vrhhi0m817r6sxp2 \
192.168.11.67:2377
This node joined a swarm as a worker.
h2 现已作为工作节点添加到我们的集群中。在任一主机上运行 docker info 将会显示 Nodes 计数已增加到 2,而 docker node ls 将列出这两个节点。
最后,让我们启动一个容器。在 Swarm 模式中,这被称为部署服务,因为有一些额外的功能与容器不兼容。在部署服务之前,我们将标记管理器为具有可用性drain——默认情况下,所有管理器都可用于运行容器,但在这个技术中,我们想展示远程机器调度能力,所以我们将限制以避免管理器。Drain 将导致节点上任何现有的容器重新部署到其他地方,并且不会在该节点上安排新的服务。
h1 $ docker node update --availability drain i5vtd3romfl9jg9g4bxtg0kis
h1 $ docker service create --name server -d -p 8000:8000 ubuntu:14.04 \
python3 -m http.server 8000
vp0fj8p9khzh72eheoye0y4bn
h1 $ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
vp0fj8p9khzh server replicated 1/1 ubuntu:14.04 *:8000->8000/tcp
这里有几个需要注意的事项。最重要的是,群集会自动选择一台机器来启动容器——如果你有多个工作节点,管理器会根据负载均衡选择一个。你可能也认出了docker service create的一些参数与docker run中的参数相似——许多参数是共享的,但阅读文档是值得的。例如,docker run中的--volume参数在--mount参数中有不同的格式,你应该阅读相关的文档。
现在是时候检查并查看我们的服务是否正在运行:
h1 $ docker service ps server
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE
ERROR PORTS
mixc9w3frple server.1 ubuntu:14.04 h2 Running Running 4
minutes ago
h1 $ docker node inspect --pretty h2 | grep Addr
Address: 192.168.11.50
h1 $ curl -sSL 192.168.11.50:8000 | head -n4
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ascii">
Swarm 模式默认启用了一项附加功能,称为路由网格。这允许群集中的每个节点似乎都可以为群集中已发布端口的全部服务提供服务——任何传入的连接都会转发到适当的节点。
例如,如果你再次回到h1管理节点(我们知道它没有运行服务,因为它有可用性drain),它仍然会在端口 8000 上对任何请求做出响应:
h1 $ curl -sSL localhost:8000 | head -n4
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ascii">
这对于一种简单的服务发现尤其有用——只要你知道一个节点的地址,你就可以非常容易地访问所有服务。
一旦完成 Swarm 的使用,你可以关闭所有服务并删除集群。
$ docker service rm server
server
$ docker swarm leave
Error response from daemon: You are attempting to leave the swarm on a >
node that is participating as a manager. Removing the last manager erases >
all current state of the swarm. Use `--force` to ignore this message.
$ docker swarm leave --force
Node left the swarm.
正如你所见,如果正在关闭节点中的最后一个管理节点,Swarm 模式会警告你,因为群集中的所有信息都将丢失。你可以使用--force来覆盖此警告。你还需要在所有工作节点上运行docker swarm leave。
讨论
这是对 Docker 中 Swarm 模式的简要介绍,这里还没有涵盖很多内容。例如,你可能已经注意到我们在初始化 Swarm 后提到的能够连接更多主节点到 Swarm 的能力——这对于弹性很有用。其他感兴趣的主题包括存储服务配置信息的内置功能(就像你在技术 74 中使用 etcd 一样),使用约束来引导容器的放置,以及有关如何在失败时回滚容器升级的信息。我们建议你参考docs.docker.com/engine/swarm/的官方文档以获取更多信息。
使用 Kubernetes 集群
您已经看到了两种极端的编排方法——Helios 的保守方法以及 Docker Swarm 的更自由的方法。但一些用户和公司可能会期望他们的工具更加复杂。
这种可定制编排的需求可以通过许多选项来满足,但有一些选项比其他选项使用和讨论得更多。在一种情况下,这无疑部分归因于背后的公司,但人们希望 Google 知道如何构建编排软件。
问题
您想要跨主机管理 Docker 服务。
解决方案
使用 Kubernetes 和其强大的抽象功能来管理您的容器集群。
Kubernetes 是由 Google 创建的一个工具,适用于那些希望获得清晰指导和建议,以及最佳实践来安排应用程序和它们之间状态关系的公司。它允许您使用专门设计的工具来管理基于指定结构的动态基础设施。
在我们开始使用 Kubernetes 之前,让我们快速看一下 Kubernetes 的高级架构,如图 12.1 图 12.1。
图 12.1. Kubernetes 高级视图

Kubernetes 采用主从架构。主节点负责接收有关在集群上运行什么的命令,并编排其资源。每个从节点上都安装了 Docker,以及一个 kubelet 服务,该服务管理每个节点上运行的 pods(容器组)。集群的信息存储在 etcd 中,这是一个分布式键/值数据存储(见技术 74),这也是集群的真相来源。
提示
我们将在本技术的后面再次讨论这个问题,所以现在不必过于担心,但一个 pod 是一组相关的容器。这个概念的存在是为了便于管理和维护 Docker 容器。
Kubernetes 的最终目标是使大规模运行容器变得简单,只需声明您想要的内容,让 Kubernetes 确保集群满足您的需求。在这个技术中,您将看到如何通过运行一个命令将一个简单的服务扩展到指定的大小。
注意
Kubernetes 最初是由 Google 开发的,作为一种在规模上管理容器的方法。Google 已经在规模上运行容器超过十年,当 Docker 变得流行时,它决定开发这个容器编排系统。Kubernetes 建立在 Google 丰富经验的基础上。Kubernetes 也被称为“K8s”。
Kubernetes 的安装、设置和功能的全面介绍是一个大且快速变化的话题,超出了本书的范围(无疑也将很快成为一本自己的书)。在这里,我们将专注于 Kubernetes 的核心概念,并设置一个简单的服务,以便您可以了解它。
安装 Kubernetes
你可以直接在主机上通过 Minikube 安装 Kubernetes,这将为你提供一个单节点集群,或者使用 Vagrant 安装一个由虚拟机管理的多节点集群。在这个技术中,我们将关注第一种选项——后一种选项最好通过研究来识别 Kubernetes 最新版本的正确选项。
在本地开始使用 Kubernetes 的推荐方法是按照 Minikube 的官方文档在kubernetes.io/docs/tasks/tools/install-minikube/上安装一个单节点集群。
Minikube 是 Kubernetes 项目中的一个专用工具,旨在简化本地开发过程,但它目前有些受限。如果你想挑战自己更多,我们建议搜索设置使用 Vagrant 的多节点 Kubernetes 集群的指南——这个过程会随着 Kubernetes 版本的更新而变化,所以我们这里不会提供具体的建议(尽管,在撰写本文时,我们发现github.com/Yolean/kubeadm-vagrant是一个合理的起点)。
在你安装了 Kubernetes 之后,你可以从这里开始。以下输出将基于一个多节点集群。我们将首先创建一个单个容器,并使用 Kubernetes 将其扩展。
单个容器的扩展
用于管理 Kubernetes 的命令是kubectl。在这种情况下,你将使用run子命令在 pod 内运行指定的镜像作为容器。
$ kubectl run todo --image=dockerinpractice/todo *1*
$ kubectl get pods | egrep "(POD|todo)" *2*
POD IP CONTAINER(S) IMAGE(S) HOST >
LABELS STATUS CREATED MESSAGE
todo-hmj8e 10.245.1.3/ > *3*
run=todo Pending About a minute *4*
-
1 “todo”是结果 pod 的名称,要启动的镜像通过“--image”标志指定;这里我们使用的是来自第一章的 todo 镜像。
-
2 “get pods”子命令到 kubectl 列出所有 pod。我们只对“todo”感兴趣,所以我们 grep 那些和标题。
-
3 “todo-hmj8e”是 pod 名称。
-
4 标签是与 pod 关联的 name=value 对,例如这里的“run”标签。pod 的状态是“挂起”,这意味着 Kubernetes 正在准备运行它,很可能是由于它正在从 Docker Hub 下载镜像。
Kubernetes 通过从run命令(在先前的例子中是todo)中获取名称,添加一个连字符,并添加一个随机字符串来选择 pod 名称。这确保了它不会与其他 pod 名称冲突。
在等待几分钟下载 todo 镜像后,你最终会看到其状态已变为“运行”:
$ kubectl get pods | egrep "(POD|todo)"
POD IP CONTAINER(S) IMAGE(S) >
HOST LABELS STATUS CREATED MESSAGE
todo-hmj8e 10.246.1.3 >
10.245.1.3/10.245.1.3 run=todo Running 4 minutes
todo dockerinpractice/todo >
Running About a minute
这次 IP、CONTAINER(S)和 IMAGE(S)列都被填充了。IP 列给出了 pod 的地址(在这种情况下是10.246.1.3),容器列中每行对应 pod 中的一个容器(在这种情况下我们只有一个,todo)。
你可以通过直接点击 IP 地址和端口来测试容器(todo)确实已经启动并运行,并正在提供服务:
$ wget -qO- 10.246.1.3:8000
<html manifest="/todo.appcache">
[...]
到目前为止,我们还没有看到直接运行 Docker 容器时的太多区别。为了体验 Kubernetes,您可以通过运行resize命令来扩展此服务:
$ kubectl resize --replicas=3 replicationController todo
resized
此命令告诉 Kubernetes,您希望 todo 副本控制器确保集群中运行着三个 todo 应用的实例。
提示
一个副本控制器是 Kubernetes 服务,确保集群中运行着正确数量的 pod。
您可以使用kubectl get pods命令检查 todo 应用的额外实例是否已启动:
$ kubectl get pods | egrep "(POD|todo)"
POD IP CONTAINER(S) IMAGE(S) >
HOST LABELS STATUS CREATED MESSAGE
todo-2ip3n 10.246.2.2 >
10.245.1.4/10.245.1.4 run=todo Running 10 minutes
todo dockerinpractice/todo >
Running 8 minutes
todo-4os5b 10.246.1.3 >
10.245.1.3/10.245.1.3 run=todo Running 2 minutes
todo dockerinpractice/todo >
Running 48 seconds
todo-cuggp 10.246.2.3 >
10.245.1.4/10.245.1.4 run=todo Running 2 minutes
todo dockerinpractice/todo >
Running 2 minutes
Kubernetes 已经采用了resize指令和 todo 副本控制器,并确保启动了正确数量的 pod。注意,它在一个主机(10.245.1.4)上放置了两个,在另一个(10.245.1.3)上放置了一个。这是因为 Kubernetes 的默认调度器默认情况下有一个算法,它会将 pod 分散到各个节点上。
提示
调度器是一种软件,它决定工作项应该在何时何地运行。例如,Linux 内核有一个调度器,它决定下一个应该运行的任务。调度器从极其简单到极其复杂不等。
您已经开始看到 Kubernetes 如何使跨多个主机管理容器变得更加容易。接下来,我们将深入探讨 Kubernetes 的核心概念——pod。
使用 pod
一个pod是一组设计成以某种方式协同工作并共享资源的容器。
每个 pod 都有自己的 IP 地址,并共享相同的卷和网络端口范围。因为 pod 的容器共享 localhost,所以容器可以依赖不同服务在部署的任何地方都是可用和可见的。
图 12.2 使用两个共享卷的容器说明了这一点。在该图中,容器 1 可能是一个读取共享卷中数据文件的 Web 服务器,而共享卷则由容器 2 更新。因此,这两个容器都是无状态的;状态存储在共享卷中。
图 12.2. 一个包含两个容器的 pod

这种分离责任的设计通过允许您分别管理服务的每个部分,从而促进了微服务方法。您可以在不关心其他容器的情况下升级 pod 中的一个容器。
以下 pod 规范定义了一个复杂的 pod,其中一个容器(simplewriter)每 5 秒向文件写入随机数据,另一个容器从同一文件中读取。文件通过卷(pod-disk)共享。
列表 12.1. complexpod.json
{
"id": "complexpod", *1*
"kind": "Pod", *2*
"apiVersion": "v1beta1", *3*
"desiredState": { *4*
"manifest": { *4*
"version": "v1beta1", *3*
"id": "complexpod", *5*
"containers": [{ *6*
"name": "simplereader", *7*
"image": "dockerinpractice/simplereader", *7*
"volumeMounts": [{ *8*
"mountPath": "/data", *9*
"name": "pod-disk" *10*
}]
},{
"name": "simplewriter", *7*
"image": "dockerinpractice/simplewriter", *7*
"volumeMounts": [{ *11*
"mountPath": "/data", *12*
"name": "pod-disk" *13*
}]
}],
"volumes": [{ *14*
"name": "pod-disk", *15*
"emptydir": {} *16*
}]
}
}
}
-
1 为实体赋予名称
-
2 指定此对象的类型
-
3 指定 JSON 针对的 Kubernetes 版本
-
4 pod 规范的核心在于“desiredState”和“manifest”属性。
-
5 为实体赋予名称
-
6 pod 中容器的详细信息存储在这个 JSON 数组中
-
7 每个容器都有一个用于参考的名称,Docker 镜像在“image”属性中指定。
-
8 每个容器都指定了卷挂载点。
-
9 挂载路径是挂载在容器文件系统上的卷的路径。这可以为每个容器设置不同的位置。
-
10 卷挂载名称指的是 pod 清单中“volumes”定义中的名称。
-
11 每个容器都指定了卷挂载点。
-
12 挂载路径是挂载在容器文件系统上的卷的路径。这可以为每个容器设置不同的位置。
-
13 卷挂载名称指的是 pod 清单中“volumes”定义中的名称。
-
14 “volumes”属性定义了为此 Pod 创建的卷。
-
15 卷的名称在先前的“volumeMounts”条目中引用。
-
16 一个与 Pod 生命周期共享的临时目录
要加载此 Pod 规范,创建一个包含前面列表的文件,并运行此命令:
$ kubectl create -f complexpod.json
pods/complexpod
等待一分钟以下载镜像后,您可以通过运行kubectl log并指定感兴趣的 Pod 和容器来查看容器的日志输出。
$ kubectl log complexpod simplereader
2015-08-04T21:03:36.535014550Z '? U
[2015-08-04T21:03:41.537370907Z] h(³eSk4y
[2015-08-04T21:03:41.537370907Z] CM(@
[2015-08-04T21:03:46.542871125Z] qm>5
[2015-08-04T21:03:46.542871125Z] {Vv_
[2015-08-04T21:03:51.552111956Z] KH+74 f
[2015-08-04T21:03:56.556372427Z] j?p+!\
讨论
我们在这里只是触及了 Kubernetes 功能和潜力的表面,但这应该让您对它能做什么以及它如何简化 Docker 容器的编排有一个概念。
下一个技术将直接利用 Kubernetes 的一些更多功能。Kubernetes 还作为 OpenShift 背后的编排引擎在技术 90 和 99 中使用。
| |
从 Pod 内部访问 Kubernetes API
通常,Pod 可以完全独立于彼此运行,甚至不知道它们是作为 Kubernetes 集群的一部分运行的。但 Kubernetes 确实提供了一个丰富的 API,并且允许容器访问这个 API,这为自我检查和自适应行为打开了大门,同时也使得容器能够自行管理 Kubernetes 集群。
问题
您想从 Pod 内部访问 Kubernetes API。
解决方案
使用curl从 Pod 中的容器内部访问 Kubernetes API,使用对容器可用的授权信息。
这是本书中较短的技巧之一,但它包含了很多内容。这也是为什么它是一个有用的技巧来研究。在其它方面,我们将涵盖
-
kubectl命令 -
启动 Kubernetes Pod
-
访问 Kubernetes Pod
-
Kubernetes 的反模式
-
持久令牌
-
Kubernetes 机密
-
Kubernetes 的“向下 API”
没有 Kubernetes 集群?
如果你没有访问 Kubernetes 集群的权限,你有几种选择。许多云服务提供商提供按需付费的 Kubernetes 集群。然而,为了减少依赖,我们推荐使用 Minikube(在上一技术中提到),它不需要信用卡。
关于如何安装 Minikube 的信息,请参阅kubernetes.io/docs/tasks/tools/install-minikube/文档。
创建 pod
首先,你将使用 kubectl 命令在新的 ubuntu pod 内部创建一个容器,然后你将在命令行中访问该容器内的 shell。(kubectl run 目前在 pod 和容器之间强制执行 1-1 的关系,尽管在一般情况下 pod 的灵活性更高。)
列表 12.2. 创建和设置容器
$ kubectl run -it ubuntu --image=ubuntu:16.04 --restart=Never *1*
If you don't see a command prompt, try pressing enter. *2*
root@ubuntu:/# apt-get update -y && apt-get install -y curl *3*
[...]
root@ubuntu:/ *4*
-
1 使用 -ti 标志的 kubectl 命令,将 pod 命名为“ubuntu”,使用现在熟悉的 ubuntu:16.04 镜像,并告诉 Kubernetes 一旦 pod/container 退出不要重启
-
2 kubectl 有用地向你指出,除非你按下 Enter 键,否则你的终端可能不会显示提示。
-
3 这是按下 Enter 键时在容器内看到的提示,我们正在更新容器的包系统并安装 curl。
-
4 安装完成后,将返回提示。
你现在处于 kubectl 命令创建的容器中,并确保 curl 已安装。
警告
从 shell 访问和修改 pod 被视为 Kubernetes 的反模式。我们在这里使用它来演示 pod 内部可以做到什么,而不是 pod 应该如何使用。
列表 12.3. 从 pod 访问 Kubernetes API
root@ubuntu:/# $ curl -k -X GET \ *1*
-H "Authorization: Bearer \ *2*
$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" <3> \
https://${KUBERNETES_PORT_443_TCP_ADDR}:${KUBERNETES_SERVICE_PORT_HTTPS} *3*
{
"paths": [ *4*
"/api",
"/api/v1",
"/apis",
"/apis/apps",
"/apis/apps/v1beta1",
"/apis/authentication.k8s.io",
"/apis/authentication.k8s.io/v1",
"/apis/authentication.k8s.io/v1beta1",
"/apis/authorization.k8s.io",
"/apis/authorization.k8s.io/v1",
"/apis/authorization.k8s.io/v1beta1",
"/apis/autoscaling",
"/apis/autoscaling/v1",
"/apis/autoscaling/v2alpha1",
"/apis/batch",
"/apis/batch/v1",
"/apis/batch/v2alpha1",
"/apis/certificates.k8s.io",
"/apis/certificates.k8s.io/v1beta1",
"/apis/extensions",
"/apis/extensions/v1beta1",
"/apis/policy",
"/apis/policy/v1beta1",
"/apis/rbac.authorization.k8s.io",
"/apis/rbac.authorization.k8s.io/v1alpha1",
"/apis/rbac.authorization.k8s.io/v1beta1",
"/apis/settings.k8s.io",
"/apis/settings.k8s.io/v1alpha1",
"/apis/storage.k8s.io",
"/apis/storage.k8s.io/v1",
"/apis/storage.k8s.io/v1beta1",
"/healthz",
"/healthz/ping",
"/healthz/poststarthook/bootstrap-controller",
"/healthz/poststarthook/ca-registration",
"/healthz/poststarthook/extensions/third-party-resources",
"/logs",
"/metrics",
"/swaggerapi/",
"/ui/",
"/version"
]
}
root@ubuntu:/# curl -k -X GET -H "Authorization: Bearer $(cat *5*
/var/run/secrets/kubernetes.io/serviceaccount/token)" *5*
https://${KUBERNETES_PORT_443_TCP_ADDR}: *5*
${KUBERNETES_SERVICE_ORT_HTTPS}/version *5*
{
"major": "1", *6*
"minor": "6",
"gitVersion": "v1.6.4",
"gitCommit": "d6f433224538d4f9ca2f7ae19b252e6fcb66a3ae",
"gitTreeState": "dirty",
"buildDate": "2017-06-22T04:31:09Z",
"goVersion": "go1.7.5",
"compiler": "gc",
"platform": "linux/amd64"
}
-
1 使用 curl 命令访问 Kubernetes API。-k 标志允许 curl 在客户端未部署证书的情况下工作,而与 API 通信所使用的 HTTP 方法由 -X 标志指定为 GET。
-
2 -H 标志向请求添加一个 HTTP 头部。这是一个将在稍后讨论的认证令牌。
-
3 要联系的网络地址是由 pod 内可用的环境变量构建的。
-
4 API 的默认响应是列出它提供的可消费路径。
-
5 这次请求是针对 /version 路径的。
-
6 对 /version 请求的响应是指定正在运行的 Kubernetes 版本。
前面的列表涵盖了大量的新内容,但我们希望它能让你对在 Kubernetes pod 中动态执行的操作有一个大致的了解,而不需要任何设置。
从这个列表中可以得出的关键点是,信息被提供给 pod 内的用户,允许 pod 与 Kubernetes API 建立联系。这些信息项统称为“向下 API”。目前,向下 API 包括两类数据:环境变量和暴露给 pod 的文件。
在前面的例子中,使用文件向 Kubernetes API 提供认证令牌。这个令牌在文件 /var/run/secrets/kubernetes.io/serviceaccount/token 中可用。在列表 12.3 中,这个文件通过cat命令运行,cat命令的输出作为Authorization: HTTP 头的一部分提供。此头指定使用的授权类型为Bearer,携带令牌是cat命令的输出,因此curl的-H参数如下:
-H "Authorization: Bearer
$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
注意
Bearer tokens是一种认证方法,它只需要提供一个指定的令牌——不需要提供更多的身份信息(如用户名/密码)。Bearer shares基于类似的原则,持有股份的人有权出售它们。现金也是同样的方式——实际上,在英国现金中,钞票上有“我承诺按需支付持票人...”的短语。
向下公开的 API 项是一种 Kubernetes“秘密”。任何秘密都可以使用 Kubernetes API 创建并通过 pod 中的文件公开。这种机制允许将秘密与 Docker 镜像和 Kubernetes pod 或部署配置分离,这意味着权限可以独立于更开放的项目进行管理。
讨论
值得注意的是这种技术,因为它涵盖了大量的内容。关键是要掌握的是,Kubernetes pods 拥有可供它们使用的信息,允许它们与 Kubernetes API 交互。这允许应用程序在 Kubernetes 中运行,监控并针对集群周围的活动进行操作。例如,你可能有一个基础设施 pod,它监视 API 以查找新出现的 pods,调查它们的活动,并将这些数据记录在其他地方。
虽然基于角色的访问控制(RBAC)不在此书的范围之内,但值得提一下,这会对安全性产生影响,因为你不希望集群中的任何用户都能拥有这种级别的访问权限。因此,API 的部分部分将需要除了携带令牌之外的东西来获取访问权限。
这些与安全相关的考虑使得这项技术与 Kubernetes 和安全各占一半。无论如何,这对于任何打算“真正”使用 Kubernetes 的人来说都是一个重要的技术,可以帮助他们了解 API 的工作原理以及它可能被滥用的潜在方式。
使用 OpenShift 在本地运行 AWS API
在本地开发中,一个巨大的挑战是测试应用程序与其他服务的兼容性。如果服务可以被放入容器中,Docker 可以提供帮助,但这仍然没有解决外部第三方服务的大范围问题。
一个常见的解决方案是拥有测试 API 实例,但这些通常提供虚假的响应——如果应用程序围绕一个服务构建,则无法进行更完整的测试。例如,想象一下您想使用 AWS S3 作为应用程序的上传位置,然后处理上传——测试这一点将花费金钱。
问题
您希望在本地上有 AWS 类似的 API 可供开发使用。
解决方案
设置 LocalStack 并使用可用的 AWS 服务等效项。
在本教程中,您将使用 Minishift 设置一个 OpenShift 系统,然后在上面运行 LocalStack 的一个 Pod。OpenShift 是围绕 Kubernetes 的一个由 RedHat 赞助的包装器,它提供了更适合企业级 Kubernetes 生产部署的额外功能。
在本技术中,我们将介绍
-
在 OpenShift 中创建路由
-
安全上下文约束
-
OpenShift 和 Kubernetes 之间的差异
-
使用公共 Docker 镜像测试 AWS 服务
注意
要遵循此技术,您需要安装 Minishift。Minishift 类似于您在技术 89 中看到的 Minikube。区别在于它包含 OpenShift 的安装(在技术 99 中全面介绍)。
LocalStack
LocalStack 是一个项目,旨在为您提供尽可能完整的 AWS API 集,以便在没有成本的情况下进行开发。这对于测试或在运行真正的 AWS 之前尝试代码来说非常棒,可能避免浪费时间和金钱。
LocalStack 在您的本地机器上启动以下核心云 API:
-
API 网关在 http://localhost:4567
-
Kinesis 在 http://localhost:4568
-
DynamoDB 在 http://localhost:4569
-
DynamoDB Streams 在 http://localhost:4570
-
Elasticsearch 在 http://localhost:4571
-
Firehose 在 http://localhost:4573
-
Lambda 在 http://localhost:4574
-
SNS 在 http://localhost:4575
-
SQS 在 http://localhost:4576
-
Redshift 在 http://localhost:4577
-
ES(Elasticsearch Service)在 http://localhost:4578
-
SES 在 http://localhost:4579
-
Route53 在 http://localhost:4580
-
CloudFormation 在 http://localhost:4581
-
CloudWatch 在 http://localhost:4582
LocalStack 支持在 Docker 容器中运行,或直接在机器上运行。它基于 Moto 构建,而 Moto 又基于 Boto 构建,Boto 是一个 Python AWS SDK 的模拟框架。
在 OpenShift 集群中运行可以让你运行许多这些 AWS API 环境。然后,你可以为每组服务创建不同的端点,并将它们彼此隔离。此外,你不必太担心资源使用,因为集群调度器会处理这个问题。但是 LocalStack 不是直接运行的,所以我们将指导您完成使其工作所需的所有步骤。
确保 Minishift 已设置
在这个阶段,我们假设你已经设置了 Minishift ——你应该查看官方文档以了解如何开始,请参阅docs.openshift.org/latest/minishift/getting-started/index.html。
列表 12.4. 检查 Minishift 是否设置正确
$ eval $(minishift oc-env)
$ oc get all
No resources found.
更改默认安全上下文约束
安全上下文约束(SCCs)是 OpenShift 的一个概念,它允许对 Docker 容器的权限进行更细粒度的控制。它们控制 SELinux 上下文(参见技术 100),可以从运行中的容器中删除能力(参见技术 93),可以确定 pod 可以以哪个用户运行,等等。
要使此运行,你需要更改默认的 restricted SCC。你也可以创建一个单独的 SCC 并将其应用于特定项目,但你可以自己尝试。
要更改 restricted SCC,你需要成为集群管理员:
$ oc login -u system:admin
然后,你需要使用以下命令编辑受限 SCC:
$ oc edit scc restricted
你将看到 restricted SCC 的定义。
在这个阶段,你需要做两件事:
-
允许容器以任何用户(在这种情况下为
root)运行 -
防止 SCC 限制你的 setuid 和 setgid 能力
允许 RunAsAny
LocalStack 容器默认以 root 用户运行,但出于安全原因,OpenShift 默认不允许容器以 root 用户运行。相反,它会选择一个非常高的范围内的 UID,并以该 UID 运行。请注意,UID 是数字,与映射到 UID 的字符串用户名不同。
为了简化问题,并允许 LocalStack 容器以 root 用户运行,更改以下行,
runAsUser:
type: MustRunAsRange
读取如下:
runAsUser:
type: RunAsAny
这允许容器以任何用户身份运行,而不是在 UID 范围内。
允许 SETUID 和 SETGID 能力
当 LocalStack 启动时,它需要成为另一个用户来启动 ElastiCache。ElastiCache 服务不会以 root 用户启动。
为了解决这个问题,LocalStack 将启动命令 su 到容器中的 LocalStack 用户。由于 restricted SCC 明确禁止更改用户或组 ID 的操作,你需要移除这些限制。通过删除以下行来完成此操作:
- SETUID
- SETGID
保存文件
完成这两个步骤后,保存文件。
记录主机信息。如果你运行此命令,
$ minishift console --machine-readable | grep HOST | sed 's/^HOST=\(.*\)/\1/'
你将获得 Minishift 实例从你的机器可访问的主机。注意这个主机,因为你稍后需要替换它。
部署 pod
部署 LocalStack 与运行以下命令一样简单:
$ oc new-app localstack/localstack --name="localstack"
备注
如果你想要深入了解 localstack 镜像,它可在github.com/localstack/localstack找到。
这将使用 localstack/localstack 镜像,并为您创建围绕它的 OpenShift 应用程序,设置内部服务(基于 LocalStack Docker 镜像的 Dockerfile 中公开的端口),在 Pod 中运行容器,并执行各种其他管理任务。
创建路由
如果您想从外部访问服务,您需要创建 OpenShift 路由,这些路由为 OpenShift 网络内的服务创建外部地址。例如,要为 SQS 服务创建路由,创建一个如下所示的文件,称为 route.yaml:
列表 12.5. route.yaml
apiVersion: v1 *1*
kind: Route *2*
metadata: *3*
name: sqs *4*
spec: *5*
host: sqs-test.HOST.nip.io *6*
port: *7*
targetPort: 4576-tcp *7*
to: *8*
kind: Service *9*
name: localstack *9*
-
1 yaml 文件顶部指定了 Kubernetes API 版本。
-
2 正在创建的对象类型被指定为“Route”。
-
3 元数据部分包含有关路由的信息,而不是路由本身的规范。
-
4 在这里为路由指定了一个名称。
-
5 规范部分指定了路由的详细信息。
-
6 主机是路由将被映射到的 URL,即客户端击中的 URL。
-
7 端口部分标识了路由将前往“to”部分中指定的服务的哪个端口
-
8 “to”部分标识了请求将被路由到的位置。
-
9 在这种情况下,它基于 LocalStack 服务。
通过运行此命令创建路由,
$ oc create -f route.yaml
这将根据您刚刚创建的 yaml 文件创建路由。然后,对于您想要设置的每个服务,都会重复此过程。
然后运行 oc get all 以查看您在 OpenShift 项目中创建的内容:
$ oc get all *1*
NAME DOCKER REPO TAGS UPDATED *2*
is/localstack 172.30.1.1:5000/myproject/localstack latest 15 hours ago *2*
NAME REVISION DESIRED CURRENT TRIGGERED BY *3*
dc/localstack 1 1 1 config,image(localstack:latest) *3*
NAME DESIRED CURRENT READY AGE *4*
rc/localstack-1 1 1 1 15
*4*
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD *5*
routes/sqs sqs-test.192.168.64.2.nip.io localstack 4576-tcp None
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE *6*
svc/localstack 172.30.187.65 4567/TCP,4568/TCP,4569/TCP,4570/TCP,4571/TCP *6*
4572/TCP,4573/TCP,4574/TCP,4575/TCP,4576/TCP,4577/TCP,4578/TCP, *6*
4579/TCP,4580/TCP,4581/TCP,4582/TCP,8080/TCP 15h *6*
NAME READY STATUS RESTARTS AGE *7*
po/localstack-1-hnvpw 1/1 Running 0 15h *7*
-
1 返回您 OpenShift 项目中最重要的项目
-
2 首先列出的是图像流。这些对象跟踪本地或远程图像的状态。
-
3 接下来,列出部署配置,这些配置指定了 Pod 应该如何部署到集群中。
-
4 第三类是复制配置,它指定了运行 Pod 的复制特性。
-
5 第四类是您项目中设置的路线。
-
6 接下来列出的是服务。在这里,您可以看到 Dockerfile 中公开的端口在服务中公开。
-
7 最后,列出项目中的 Pod。
虽然技术上不是您项目中可用的所有对象,但oc get all命令显示了运行应用程序最重要的那些对象。
类似于 SQS 的 AWS 服务现在可以通过 URL 端点访问,以测试您的代码。
访问服务
您现在可以从主机访问服务。以下是一个创建 SQS 流的示例:
$ aws --endpoint-url=http://kinesis-test.192.168.64.2.nip.io kinesis
list-streams *1*
{ *2*
"StreamNames": [] *2*
} *2*
$ aws --endpoint-url=http://kinesis-test.192.168.64.2.nip.io kinesis
create-stream --stream-name teststream --shard-count 2 *3*
$ aws --endpoint-url=http://kinesis-test.192.168.64.2.nip.io kinesis
list-streams *4*
{ *5*
"StreamNames": [ *5*
"teststream" *5*
] *5*
} *5*
-
1 aws 客户端应用程序用于访问新创建的端点,并要求 kinesis 列出其流。
-
2 JSON 输出指示不存在流。
-
3 再次调用 aws 客户端以创建一个名为“teststream”的 SQS 流,其分片数为 2。
-
4 再次,您请求 kinesis 流的列表。
-
5 JSON 输出指示存在一个名为“teststream”的流。
注意
aws 客户端是您需要安装以使此功能正常工作的组件。或者,您可以直接 curl API 端点,但我们不建议这样做。还假设您已经运行了 aws configure 并指定了您的 AWS 密钥和默认区域。实际指定的值对 LocalStack 没有关系,因为它不进行身份验证。
在这里,我们只介绍了一种服务类型,但这种技术可以轻松扩展到本技术开头列出的其他服务。
讨论
这种技术让您感受到了 OpenShift(以及 OpenShift 所基于的 Kubernetes)的力量。以一个可用的端点和所有内部连接都得到妥善处理的方式启动一个有用的应用程序,在许多方面是 Docker 提供的可移植性承诺的实现,扩展到了数据中心层面。
例如,这可以进一步发展,可以在同一个 OpenShift 集群上启动多个 LocalStack 实例。对 AWS API 的测试可以在不必要增加更多资源的情况下并行进行(当然,这取决于您的 OpenShift 集群大小和测试需求)。因为这些都是代码,持续集成可以设置成在每次提交 AWS 代码库时动态启动和关闭 LocalStack 实例以进行通信。
除了指出 Kubernetes 的各个方面外,这个特定的技术还展示了像 OpenShift 这样的产品是如何在 Kubernetes 上构建,以扩展其功能。例如,安全上下文约束是 OpenShift 的概念(尽管安全上下文也在 Kubernetes 中),而“路由”是 OpenShift 在 Kubernetes 上创建的一个概念,最终被直接用于 Kubernetes 的实现。随着时间的推移,为 OpenShift 开发的功能已经上传到 Kubernetes,并成为了其提供的一部分。
您将在技术 99 中再次看到 OpenShift,我们将探讨它如何作为一个平台,安全地让用户运行容器。
| |
在 Mesos 上构建框架
当讨论众多编排可能性时,您可能会发现特别提到的一个替代 Kubernetes 的选项:Mesos。通常这后面会跟着一些晦涩难懂的说法,比如“Mesos 是一个框架的框架”和“Kubernetes 可以在 Mesos 上运行”。
我们遇到的最恰当的类比是将 Mesos 视为为您的数据中心提供内核。仅凭它本身,您无法做任何有用的事情——价值在于您将其与初始化系统和应用程序结合使用时。
对于一个低技术含量的解释,想象一下您面前有一只猴子坐在控制所有机器的面板前,它有权随意启动和停止应用程序。当然,您需要给猴子一个非常清晰的指令列表,说明在特定情况下应该做什么,何时启动应用程序等等。您可以自己完成所有这些,但这很耗时,而猴子又便宜。
Mesos 是那只猴子!
Mesos 对于具有高度动态和复杂基础设施的公司来说很理想,这些公司可能有过自行开发生产编排解决方案的经验。如果您不符合这些条件,您可能更适合使用现成的解决方案,而不是花费时间定制 Mesos。
问题
您有一系列规则用于控制应用程序和作业的启动,您希望在不手动在远程机器上启动它们并跟踪它们的状态的情况下强制执行这些规则。
解决方案
使用 Mesos,这是一个灵活且强大的工具,它提供了资源管理的抽象。
Mesos 是一个成熟的软件,用于在多台机器上提供资源管理的抽象。它已经由您所熟知的公司在生产环境中进行了实战测试,因此它稳定且可靠。
注意
您需要 Docker 1.6.2 或更高版本,以便 Mesos 能够使用正确的 Docker API 版本。
图 12.3 展示了一个通用的生产 Mesos 设置。
图 12.3. 一个通用的生产 Mesos 设置

参考此图,您可以看到启动任务的基本 Mesos 生命周期如下:
-
1从节点在节点上运行,跟踪资源可用性并向主节点报告。
-
2主节点从一个或多个从节点接收有关可用资源的信息,并向调度器提供资源。
-
3调度器从主节点接收资源,决定在哪里运行任务,并将此信息反馈给主节点。
-
4主节点将任务信息传递给适当的从节点。
-
5每个从节点将任务信息传递给节点上的现有执行器或启动一个新的执行器。
-
6执行器读取任务信息并在节点上启动任务。
-
7任务运行。
Mesos 项目提供了主节点和从节点,以及内置的 shell 执行器。您的任务是提供一个 框架(或 应用程序),它由一个调度器(在我们的猴子类比中的“指令列表”)和可选的自定义执行器组成。
许多第三方项目提供了可以集成到 Mesos 中的框架(我们将在下一技术中更详细地探讨一个),但为了更好地理解如何充分利用 Mesos 和 Docker 的功能,我们将构建一个仅包含调度器的框架。如果您有启动应用程序的高度复杂逻辑,这可能就是您的最终选择路线。
注意
您不必在 Mesos 中使用 Docker,但由于本书的主题是关于这个,我们将使用它。由于 Mesos 非常灵活,因此我们不会深入探讨许多细节。我们还将在一个计算机上运行 Mesos,但我们会尽量使其尽可能真实,并指出您需要做什么才能投入使用。
我们尚未解释 Docker 在 Mesos 生命周期中的位置——这个谜题的最后一部分是 Mesos 提供了对 containerizers 的支持,允许您隔离您的执行器或任务(或两者)。Docker 不是唯一可以在这里使用的工具,但由于它非常流行,Mesos 为您提供了一些特定的 Docker 功能来帮助您开始。
我们的示例将仅对运行的任务进行容器化,因为我们使用的是默认执行器。如果您有一个仅运行语言环境的自定义执行器,其中每个任务都涉及动态加载和执行一些代码,您可能需要考虑对执行器进行容器化。作为一个示例用例,您可能有一个作为执行器的 JVM,它会在运行时加载和执行代码片段,从而避免为可能非常小的任务带来 JVM 启动开销。
图 12.4 展示了在我们的示例中创建新的 Docker 化任务时幕后将发生什么。
图 12.4. 单主机 Mesos 设置启动容器

不再拖延,让我们开始吧。首先您需要启动一个主节点:
列表 12.6. 启动主节点
$ docker run -d --name mesmaster redjack/mesos:0.21.0 mesos-master \
--work_dir=/opt
24e277601260dcc6df35dc20a32a81f0336ae49531c46c2c8db84fe99ac1da35
$ docker inspect -f '{{.NetworkSettings.IPAddress}}' mesmaster
172.17.0.2
$ docker logs -f mesmaster
I0312 01:43:59.182916 1 main.cpp:167] Build: 2014-11-22 05:29:57 by root
I0312 01:43:59.183073 1 main.cpp:169] Version: 0.21.0
I0312 01:43:59.183084 1 main.cpp:172] Git tag: 0.21.0
[...]
主节点启动时有些冗长,但你应该会发现它很快就会停止记录日志。请保持此终端打开,以便您可以看到启动其他容器时发生了什么。
注意
通常,Mesos 设置将具有多个 Mesos 主节点(一个活动节点和几个备份节点),以及一个 Zookeeper 集群。在 Mesos 网站上的“Mesos 高可用性模式”页面(mesos.apache.org/documentation/latest/high-availability)上有设置此内容的文档。您还需要公开端口 5050 以进行外部通信,并使用 work_dir 文件夹作为卷来保存持久信息。您还需要一个从属节点。不幸的是,这有点麻烦。Mesos 的一个定义特征是能够对任务强制资源限制,这要求从属节点能够自由地检查和管理进程。因此,运行从属节点的命令需要将许多外部系统细节暴露在容器内部。
列表 12.7. 启动从属节点
$ docker run -d --name messlave --pid=host \
-v /var/run/docker.sock:/var/run/docker.sock -v /sys:/sys \
redjack/mesos:0.21.0 mesos-slave \
--master=172.17.0.2:5050 --executor_registration_timeout=5mins \
--isolation=cgroups/cpu,cgroups/mem --containerizers=docker,mesos \
--resources="ports(*):[8000-8100]"
1b88c414527f63e24241691a96e3e3251fbb24996f3bfba3ebba91d7a541a9f5
$ docker inspect -f '{{.NetworkSettings.IPAddress}}' messlave
172.17.0.3
$ docker logs -f messlave
I0312 01:46:43.341621 32398 main.cpp:142] Build: 2014-11-22 05:29:57 by root
I0312 01:46:43.341789 32398 main.cpp:144] Version: 0.21.0
I0312 01:46:43.341795 32398 main.cpp:147] Git tag: 0.21.0
[...]
I0312 01:46:43.554498 32429 slave.cpp:627] No credentials provided. >
Attempting to register without authentication
I0312 01:46:43.554633 32429 slave.cpp:638] Detecting new master
I0312 01:46:44.419646 32424 slave.cpp:756] Registered with master >
master@172.17.0.2:5050; given slave ID 20150312-014359-33558956-5050-1-S0
[...]
在这一点上,您也应该在 Mesos 主节点终端中看到一些活动,开始于几行像这样的内容:
I0312 01:46:44.332494 9 master.cpp:3068] Registering slave at >
slave(1)@172.17.0.3:5051 (8c6c63023050) with id >
20150312-014359-33558956-5050-1-S0
I0312 01:46:44.333772 8 registrar.cpp:445] Applied 1 operations in >
134310ns; attempting to update the 'registry'
这两个日志的输出显示您的从属节点已启动并连接到主节点。如果您没有看到这些,请停止并仔细检查您的主节点 IP 地址。当没有连接的从属节点来启动任务时,尝试调试框架为什么无法启动任务可能会很令人沮丧。
无论如何,命令中 列表 12.7 有很多内容。在 run 和 redjack/mesos:0.21.0 之间的所有参数都是 Docker 参数,它们主要包含向从机容器提供大量关于外部世界的信息。在 mesos-slave 之后的参数更有趣。首先,master 告诉你的从机在哪里可以找到你的主机(或你的 Zookeeper 集群)。接下来的三个参数,executor _registration_timeout、isolation 和 containerizers,都是针对 Mesos 设置的调整,当与 Docker 一起工作时应该始终应用。最后,但同样重要的是,你需要让 Mesos 从机知道哪些端口是可以分配作为资源的。默认情况下,Mesos 提供 31000–32000,但我们想要一个更低且更容易记住的。
现在简单的步骤已经完成,我们来到了设置 Mesos 的最后阶段——创建一个调度器。
幸运的是,我们有一个现成的示例框架供你使用。让我们试试看它做了什么,然后探索它是如何工作的。请保持你的两个 docker logs -f 命令在你的主容器和从容器上打开,这样你就可以看到通信是如何发生的。
以下命令将从 GitHub 获取示例框架的源代码库并启动它。
列表 12.8. 下载并启动示例框架
$ git clone https://github.com/docker-in-practice/mesos-nc.git
$ docker run -it --rm -v $(pwd)/mesos-nc:/opt redjack/mesos:0.21.0 bash
# apt-get update && apt-get install -y python
# cd /opt
# export PYTHONUSERBASE=/usr/local
# python myframework.py 172.17.0.2:5050
I0312 02:11:07.642227 182 sched.cpp:137] Version: 0.21.0
I0312 02:11:07.645598 176 sched.cpp:234] New master detected at >
master@172.17.0.2:5050
I0312 02:11:07.645800 176 sched.cpp:242] No credentials provided. >
Attempting to register without authentication
I0312 02:11:07.648449 176 sched.cpp:408] Framework registered with >
20150312-014359-33558956-5050-1-0000
Registered with framework ID 20150312-014359-33558956-5050-1-0000
Received offer 20150312-014359-33558956-5050-1-O0\. cpus: 4.0, mem: 6686.0, >
ports: 8000-8100
Creating task 0
Task 0 is in state TASK_RUNNING
[...]
Received offer 20150312-014359-33558956-5050-1-O5\. cpus: 3.5, mem: 6586.0, >
ports: 8005-8100
Creating task 5
Task 5 is in state TASK_RUNNING
Received offer 20150312-014359-33558956-5050-1-O6\. cpus: 3.4, mem: 6566.0, >
ports: 8006-8100
Declining offer
你会注意到我们已经将 Git 仓库挂载到 Mesos 镜像中。这是因为它包含了我们需要的所有 Mesos 库。不幸的是,如果不这样做,安装它们可能会有些痛苦。
我们的 mesos-nc 框架旨在在所有可用的主机上,从 8000 到 8005 的所有可用端口上运行 echo 'hello <task id>' | nc -l <port>。由于 netcat 的工作方式,这些“服务器”在你访问它们时就会终止,无论是通过 curl、Telnet、nc 还是你的浏览器。你可以通过在新终端中运行 curl localhost:8003 来验证这一点。它将返回预期的响应,并且你的 Mesos 日志将显示正在生成一个任务来替换已终止的任务。你还可以使用 docker ps 来跟踪正在运行的任务。
值得指出的是,Mesos 在这里跟踪分配的资源,并在任务终止时将其标记为可用。特别是,当你访问 localhost:8003(随时可以再次尝试)时,仔细看看 Received offer 行——它显示了两个端口范围(因为它们没有连接),包括刚刚释放的那个:
Received offer 20150312-014359-33558956-5050-1-O45\. cpus: 3.5, mem: 6586.0, >
ports: 8006-8100,8003-8003
警告
Mesos 从机使用前缀“mesos-”命名它启动的所有容器,并且它假设类似的东西可以被从机自由管理。请小心你的容器命名,否则你可能会让 Mesos 从机杀死自己。
框架代码(myframework.py)注释良好,以防你感到好奇。我们将探讨一些高级设计。
class TestScheduler
(mesos.interface.Scheduler):
[...]
def registered(self, driver, frameworkId, masterInfo):
[...]
def statusUpdate(self, driver, update):
[...]
def resourceOffers(self, driver, offers):
[...]
所有 Mesos 调度器都是基于基本 Mesos 调度器类的子类,并且它们实现了一系列方法,Mesos 会在适当的时候调用这些方法,以便你的框架能够对事件做出反应。尽管我们在前面的代码片段中实现了三个方法,但其中两个是可选的,并且为了演示目的添加了额外的日志记录。你必须实现的方法是resourceOffers——如果一个框架不知道何时可以启动任务,那么它就没有多少意义。你可以自由地添加任何额外的用于你自己的目的的方法,例如init和_makeTask,只要它们不与 Mesos 期望使用的方法冲突,所以请确保你阅读了文档(mesos.apache.org/documentation/latest/app-framework-development-guide/)。
小贴士
如果你最终编写了自己的框架,你可能需要查看一些方法和结构的相关文档。不幸的是,在撰写本文时,只有 Java 方法的文档被生成。寻找结构探索起点的读者可能希望从 Mesos 源代码中的 include/mesos/mesos.proto 文件开始。祝你好运!
让我们更详细地看看我们感兴趣的主要方法:resourceOffers。这是决定启动任务或拒绝 offer 的地方。图 12.5 显示了在 Mesos 调用我们的框架中的resourceOffers之后执行的流程(通常是因为某些资源已可供框架使用)。
图 12.5. 框架resourceOffers执行流程

resourceOffers 接收一个包含多个 offer 的列表,其中每个 offer 对应一个 Mesos slave。offer 包含了在从节点上启动的任务可用的资源详情,典型的实现会使用这些信息来确定启动任务的最佳位置。启动任务会向 Mesos master 发送消息,然后 master 继续执行图 12.3 中概述的生命周期。
讨论
需要注意的是resourceOffers的灵活性——你的任务启动决策可以基于你选择的任何标准,从外部服务的健康检查到月亮的相位。这种灵活性可能是一个负担,因此存在预制的框架来移除一些低级细节并简化 Mesos 的使用。这些框架中的一个将在下一个技巧中介绍。
你可能想查阅 Roger Ignazio 的《Mesos in Action》(Manning,2016)以获取更多关于 Mesos 可以做什么的详细信息——我们在这里只是触及了表面,你看到了 Docker 如何轻松地嵌入其中。
使用 Marathon 微管理 Mesos
到现在为止,你应该已经意识到,即使是对于一个非常简单的框架,你也需要考虑很多关于 Mesos 的事情。能够依赖应用程序被正确部署非常重要——框架中一个错误的后果可能从无法部署新应用程序到整个服务中断。
随着你规模的扩大,风险也在增加,除非你的团队习惯于编写可靠的动态部署代码,否则你可能想要考虑一个经过更多实战检验的方法——Mesos 本身非常稳定,但一个定制的内部框架可能不如你期望的那样可靠。
Marathon 适合没有内部部署工具经验的公司,但需要在一个相对动态的环境中部署容器时,需要一个支持良好且易于使用的解决方案。
问题
你需要一个可靠的方式来利用 Mesos 的力量,同时避免陷入编写自己框架的困境。
解决方案
使用 Marathon,这是 Mesos 之上的一层,提供了一个更简单的接口,让你更快地投入生产。
Marathon 是由 Mesosphere 为管理长期运行的应用程序而构建的 Apache Mesos 框架。营销材料将其描述为数据中心(其中 Mesos 是内核)的init或upstart守护进程。这不是一个不合理的类比。
Marathon 通过允许你启动一个包含 Mesos master、Mesos slave 以及 Marathon 本身的单个容器来简化入门过程。这对于演示很有用,但并不适合生产环境的 Marathon 部署。为了得到一个真实的 Marathon 设置,你需要一个 Mesos master 和 slave(来自之前的技术)以及一个 Zookeeper 实例(来自技术 84)。确保所有这些都在运行,然后我们将通过运行 Marathon 容器开始。
$ docker inspect -f '{{.NetworkSettings.IPAddress}}' mesmaster
172.17.0.2
$ docker inspect -f '{{.NetworkSettings.IPAddress}}' messlave
172.17.0.3
$ docker inspect -f '{{.NetworkSettings.IPAddress}}' zookeeper
172.17.0.4
$ docker pull mesosphere/marathon:v0.8.2
[...]
$ docker run -d -h $(hostname) --name marathon -p 8080:8080 \
mesosphere/marathon:v0.8.2 --master 172.17.0.2:5050 --local_port_min 8000 \
--local_port_max 8100 --zk zk://172.17.0.4:2181/marathon
accd6de46cfab65572539ccffa5c2303009be7ec7dbfb49e3ab8f447453f2b93
$ docker logs -f marathon
MESOS_NATIVE_JAVA_LIBRARY is not set. Searching in /usr/lib /usr/local/lib.
MESOS_NATIVE_LIBRARY, MESOS_NATIVE_JAVA_LIBRARY set to >
'/usr/lib/libmesos.so'
[2015-06-23 19:42:14,836] INFO Starting Marathon 0.8.2 >
(mesosphere.marathon.Main$:87)
[2015-06-23 19:42:16,270] INFO Connecting to Zookeeper... >
(mesosphere.marathon.Main$:37)
[...]
[2015-06-30 18:20:07,971] INFO started processing 1 offers, >
launching at most 1 tasks per offer and 1000 tasks in total
(mesosphere.marathon.tasks.IterativeOfferMatcher$:124)
[2015-06-30 18:20:07,972] INFO Launched 0 tasks on 0 offers, >
declining 1 (mesosphere.marathon.tasks.IterativeOfferMatcher$:216)
就像 Mesos 本身一样,Marathon 相当健谈,但(也像 Mesos 一样)它停止得相当快。在这个阶段,它将进入你从编写自己的框架中熟悉的循环——考虑资源提供并决定如何处理它们。因为我们还没有启动任何东西,你应该看不到任何活动;这就是为什么在先前的日志中看到declining 1的原因。
Marathon 提供了一个看起来很不错的 Web 界面,这就是为什么我们在主机上暴露了 8080 端口——在你的浏览器中访问 http://localhost:8080 来打开它。
我们将直接进入 Marathon,因此让我们创建一个新的应用程序。为了澄清一些术语——在 Marathon 的世界里,“app”指的是一组具有完全相同定义的一个或多个任务。
点击右上角的“新建应用”按钮,将弹出一个对话框,您可以使用它来定义您想要启动的应用。我们将继续使用我们自己创建的框架的风格,将 ID 设置为“marathon-nc”,保留 CPU、内存和磁盘空间在默认值(以匹配我们对 mesos-nc 框架施加的资源限制),并将命令设置为echo "hello $MESOS_TASK_ID" | nc -l $PORT0(使用任务可用的环境变量——注意,这是数字零)。将端口字段设置为 8000,以指示您想要监听的位置。目前我们将跳过其他字段。点击创建。
您新定义的应用现在将列在 Web 界面上。状态将短暂显示为“部署中”,然后显示为“运行中”。您的应用现在已启动!
如果您点击“应用列表”中的“/marathon-nc”条目,您将看到您应用的唯一 ID。您可以从以下片段中获取完整的配置,并通过 curl 适当的端口上的 Mesos 从节点容器来验证它是否正在运行。确保您保存 REST API 返回的完整配置,因为它将在以后很有用——在以下示例中已保存到 app.json 中。
$ curl http://localhost:8080/v2/apps/marathon-nc/versions
{"versions":["2015-06-30T19:52:44.649Z"]}
$ curl -s \
http://localhost:8080/v2/apps/marathon-nc/versions/2015-06-30T19:52:44.649Z \
> app.json
$ cat app.json
{"id":"/marathon-nc", >
"cmd":"echo \"hello $MESOS_TASK_ID\" | nc -l $PORT0",[...]
$ curl http://172.17.0.3:8000
hello marathon-nc.f56f140e-19e9-11e5-a44d-0242ac110012
注意从curl应用输出的“hello”后面的文本——它应该与界面中的唯一 ID 匹配。但是检查要快——运行那个curl命令将使应用终止,Marathon 将重新启动它,并且 Web 界面中的唯一 ID 将改变。一旦您验证了所有这些,请继续点击“销毁应用”按钮以删除 marathon-nc。
这工作得很好,但你可能已经注意到,我们没有通过 Marathon 实现我们设定的目标——编排 Docker 容器。尽管我们的应用在容器内,但它是在 Mesos 从节点容器中启动的,而不是在自己的容器中。阅读 Marathon 文档揭示,在 Docker 容器内创建任务需要更多的配置(就像我们编写自己的框架时一样)。
幸运的是,我们之前启动的 Mesos 从节点已经具备所需的设置,所以我们只需要修改一些 Marathon 选项——特别是应用选项。通过使用之前保存的 Marathon API 响应(保存在 app.json 中),我们可以专注于添加启用 Docker 使用的 Marathon 设置。为了在这里进行操作,我们将使用方便的jq工具,尽管通过文本编辑器来做也同样简单。
$ JQ=https://github.com/stedolan/jq/releases/download/jq-1.3/jq-linux-x86_64
$ curl -Os $JQ && mv jq-linux-x86_64 jq && chmod +x jq
$ cat >container.json <<EOF
{
"container": {
"type": "DOCKER",
"docker": {
"image": "ubuntu:14.04.2",
"network": "BRIDGE",
"portMappings": [{"hostPort": 8000, "containerPort": 8000}]
}
}
}
$ # merge the app and container details
$ cat app.json container.json | ./jq -s add > newapp.json
我们现在可以将新的应用定义发送到 API,并看到 Marathon 启动它:
$ curl -X POST -H 'Content-Type: application/json; charset=utf-8' \
--data-binary @newapp.json http://localhost:8080/v2/apps
{"id":"/marathon-nc", >
"cmd":"echo \"hello $MESOS_TASK_ID\" | nc -l $PORT0",[...]
$ sleep 10
$ docker ps --since=marathon
CONTAINER ID IMAGE COMMAND CREATED >
STATUS PORTS NAMES
284ced88246c ubuntu:14.04 "\"/bin/sh -c 'echo About a minute ago >
Up About a minute 0.0.0.0:8000->8000/tcp mesos- >
1da85151-59c0-4469-9c50-2bfc34f1a987
$ curl localhost:8000
hello mesos-nc.675b2dc9-1f88-11e5-bc4d-0242ac11000e
$ docker ps --since=marathon
CONTAINER ID IMAGE COMMAND CREATED >
STATUS PORTS NAMES
851279a9292f ubuntu:14.04 "\"/bin/sh -c 'echo 44 seconds ago >
Up 43 seconds 0.0.0.0:8000->8000/tcp mesos- >
37d84e5e-3908-405b-aa04-9524b59ba4f6
284ced88246c ubuntu:14.04 "\"/bin/sh -c 'echo 24 minutes ago >
Exited (0) 45 seconds ago mesos-1da85151-59c0-
4469-9c50-2bfc34f1a987
与我们之前的技术中自定义的框架一样,Mesos 已经为我们启动了一个运行应用的 Docker 容器。运行curl命令将终止应用和容器,然后自动启动一个新的容器。
讨论
与之前的技术中的自定义框架和 Marathon 相比,存在一些显著的不同。例如,在自定义框架中,我们对接受资源出价有极其细粒度的控制,以至于我们可以挑选和选择要监听的各个端口。要在 Marathon 中做类似的事情,你需要对每个单独的从节点施加设置。
相比之下,Marathon 内置了许多功能,这些功能如果自己构建可能会出错,包括健康检查、事件通知系统和 REST API。这些都不是简单的事情来实现,使用 Marathon 可以让你有信心,你不是第一个尝试的人。至少,与定制框架相比,Marathon 的支持要容易得多,我们发现 Marathon 的文档比 Mesos 的文档更容易接近。
我们已经介绍了设置和使用 Marathon 的基础知识,但还有许多更多的事情要查看和执行。我们看到的更有趣的建议之一是使用 Marathon 启动其他 Mesos 框架,可能包括你自己的定制框架!我们鼓励你探索——Mesos 是一个高质量的编排工具,而 Marathon 在其之上提供了一个可用的层。
摘要
-
你可以使用 Docker Swarm 模式在机器集群上启动服务。
-
为 Mesos 编写自定义框架可以让你对容器调度有细粒度的控制。
-
在 Mesos 之上的 Marathon 框架提供了一种简单的方式来利用 Mesos 的一些功能。
-
Kubernetes 是一个生产质量的编排工具,并有一个你可以利用的 API。
-
OpenShift 可以用来设置一些 AWS 服务的本地版本。
第十三章. Docker 平台
本章涵盖
-
影响 Docker 平台选择的因素
-
采用 Docker 时需要考虑的领域
-
2018 年 Docker 供应商领域的状况
本章的标题可能看起来有些令人困惑。前一章没有涵盖 Kubernetes 和 Mesos 这样的 Docker 平台吗?
嗯,是的,也不是。尽管 Kubernetes 和 Mesos 可以说是可以运行 Docker 的平台,但在本书中,我们将“平台”理解为一种产品(或集成技术集),它允许您以结构化的方式运行和管理 Docker 容器的操作。您可以将本章视为比纯技术更偏向基础设施。
在撰写本文时,存在几个 Docker 平台:
-
AWS Fargate
-
AWS ECS (弹性容器服务)
-
AWS EKS (弹性 Kubernetes 服务)
-
Azure AKS (Azure Kubernetes 服务)
-
OpenShift
-
Docker 数据中心
-
“原生” Kubernetes
注意
“原生” Kubernetes 意味着在您偏好的任何底层基础设施上运行和管理自己的集群。您可能希望在您自己的数据中心中的专用硬件上运行它,或者在云提供商的虚拟机上运行它。
平台采用的难点在于决定选择哪个平台,以及了解在组织内查看 Docker 采用时需要考虑什么。本章将提供一个决策图,以帮助做出合理的平台选择。它将帮助你理解为什么你可能会选择 OpenShift 而不是 Kubernetes,或者 AWS ECS 而不是 Kubernetes 等等。
本章分为三个部分。第一部分讨论了对于希望采用 Docker 的组织来说,哪些技术或解决方案是合适的决策因素。第二部分讨论了在考虑采用 Docker 时需要考虑的领域。第三部分讨论了截至 2018 年的供应商格局。
我们已在多个组织中部署了 Docker,并在多次会议以及在这些组织中讨论了采用的挑战。这些经验教会我们的是,尽管这些组织面临的挑战组合是独特的,但在开始容器之旅之前,需要理解决策模式和挑战类别。
13.1. 组织选择因素
本节将概述一些可能影响你组织内 Docker 平台选择的重大因素。图 13.1 展示了这些因素及其相互关系。
图 13.1. 驱动平台选择的因素

在详细讨论这些因素之前,我们将简要定义每个因素及其含义。你可能已经考虑过所有这些因素并理解了它们是什么,但组织内部和组织之间的不同术语可能会使术语变得不明确,并且某些术语在某些组织中比在其他组织中更常用。
-
购买与自建——这指的是组织在新的软件部署方面采取的不同方法。一些组织更喜欢购买解决方案,而另一些组织则更喜欢自己构建和维护。这反过来又可能影响选择哪个平台(或哪些平台)。
-
技术驱动因素——一些企业通过其技术的特定特性来区分自己,例如高性能或运营成本效率。支撑这些特性的基础可能非常特定,而一些特定的技术组件可能无法由通用服务或工具提供支持。这可能导致更多定制化解决方案,从而推动“自建”而不是“购买”的方法。
-
单体与模块化——这同样是一种组织可以采取的一般文化方法,针对软件解决方案。一些组织更喜欢将解决方案集中在一个单体实体中(一个集中的服务器或服务),而另一些组织则更喜欢逐个解决问题。后者方法可以被视为更灵活和适应性更强,而前者在规模上可能更有效率。
-
上市时间—经常,组织(出于商业或文化原因)会感到压力,需要快速向用户交付解决方案。这种压力可能会在未来牺牲成本或灵活性,从而优先考虑某些平台而非其他平台。
-
开源与许可—如今,组织通常更倾向于选择开源产品而非许可产品,但仍然有很好的理由从供应商那里许可产品。另一个推动组织向开源解决方案转变的相关主题是对特定供应商或平台的锁定恐惧,这可能导致随着时间的推移,对该产品的依赖性增加,从而增加许可成本。
-
消费者独立性—你部署的平台将会有消费者。这些可以是个人、团队或整个业务单元。无论这些消费者的规模如何,他们都将有自己的文化和运作模式。这里的关键问题是他们在运营环境中在技术上是多么自我管理,以及他们的开发需求是多么定制化?对这些问题的回答可能会决定你决定部署的平台特征。
-
云战略—如今,很少有组织对云计算没有明确的立场。无论你是否打算立即将工作负载迁移到云中,解决方案是否为云原生可能会成为你决策过程中的一个因素。即使你已经决定迁移到云,你仍然需要考虑该战略是否仅限于一个云,或者是否设计为可以在云之间甚至返回数据中心的可移植性。
-
安全立场—随着组织越来越重视安全作为其 IT 战略的一部分,越来越多的组织正在认真对待安全问题。无论是国家支持的行为者、业余(或专业)黑客、工业间谍活动还是简单的盗窃,安全是每个人都持有立场的问题。对这个领域的关注程度可能有所不同,因此这可能在平台选择中发挥作用。
-
组织结构—许多先前的定义,如果你在一家企业组织中工作,可能对你更有意义,而不是在相反类型的组织中工作。
在这本书中,我们将企业广泛定义为内部各个职能之间独立性程度较低的组织。例如,如果你运行一个集中的 IT 职能,你是否可以在不参考业务其他部分(如安全、开发团队、开发工具团队、财务、运营/DevOps 团队)的情况下部署解决方案而不会产生后果?如果是这样,我们认为这正是一个企业组织的对立面。企业组织往往规模更大(因此职能更独立),并且受到更多的监管(内部和外部),这往往限制了它们实施变革的自由,从而减少了变革的后果。
相比之下,非企业组织(在本书中)是指那些通过自我决定过程自由部署解决方案的组织。根据这个定义,初创公司通常被视为非企业组织,因为它们可以快速做出决定,而不需要参考或更快地确定他人的需求。
虽然非企业组织倾向于偏好某些策略(如“构建”而非“购买”),但考虑这些决策对业务长期后果的影响仍然是有益的。
让我们更具体地看看各种因素如何相互作用,以支持或反对不同的平台。希望其中一些能与你或你的情况产生共鸣。
在这次讨论之后,我们将继续探讨运行 Docker 平台可能带来的具体挑战。有了这些因素作为背景,你可以做出明智的决定,选择最适合你组织需求的技术。
13.1.1. 市场投放时间
首先考虑最简单的因素:市场投放时间可能是有帮助的。组织内的每个人都感到一些压力,需要快速交付解决方案,但这种压力的可协商性或可取性可能会有所不同。
如果直接竞争对手已经采用容器化策略,并成功地利用它来降低成本,那么高级管理层可能会对您的解决方案交付时间产生兴趣。
另一方面,如果你为一家更保守的组织工作,快速交付的解决方案可能会被视为产生负面影响,例如锁定在匆忙交付或过时平台,这种平台无法随着需求的变化而变化。
更有经验的人可能会建议你抵制在面临这些危险时采用第一个可信解决方案的冲动。
通常,快速交付的压力推动人们倾向于“购买”而非“构建”,以及“单一”而非“分块”的解决方案来解决复杂的业务挑战。(这些选择将在下一节中进一步讨论。)这些挑战可以通过将解决问题的责任分配给这些供应商的解决方案来解决。但这并不总是可能的,尤其是如果产品还不够成熟。
迫于交付的压力也可能导致匆忙交付定制解决方案,以满足企业短期需求。这在高度技术导向的组织中尤为普遍,并且可能非常有效,通过控制核心技术和了解其运作方式,在竞争中取得优势。然而,如果技术不是你业务的关键差异化因素,这可能会导致难以摆脱的“白象”技术,如果行业超过了你的领先地位,那么这种技术可能会变得难以移除。
类似地,采用点击即用的云技术可以显著缩短你的上市时间。缺点可能是随之而来的对该提供商解决方案的锁定,随着规模的扩大而增加成本,以及未来任何迁移的成本。它还可以减少技术特性或解决方案的灵活性,使你依赖于云供应商产品的增长和发展。
13.1.2. 购买与构建
购买解决方案可以以多种方式成为有效的策略。正如你所看到的,它可以缩短上市时间。如果你的组织在开发人员方面受到限制,你还可以利用产品的(假设)不断扩大的功能集,以相对较少的投资向客户提供更多服务。
如果你选择将其作为供应商提供的服务在本地以外的地方运营,购买解决方案还可以降低运营成本。你能够走这条道路的程度可能受到你的安全立场的限制:软件只有在运行在由使用该组织的硬件和运营下才被认为是安全的。
自己搭建平台,无论是从头开始还是基于现有的开源软件,可能对你有吸引力,因为你正在阅读这本书。毫无疑问,你会在这一过程中学到很多东西,但从商业角度来看,这种方法存在许多风险。
首先,你可能需要一支高度熟练的团队来继续构建和维护这个产品。与你的想象相比,这可能要困难得多(尤其是如果你在工作中和大学里一直围绕着计算机科学家)去寻找能够编程和操作复杂 IT 系统的人,尤其是在近年来这些技能需求很高的时期。
第二,随着时间的推移,容器平台世界将成熟,现有玩家将提供类似的功能集和围绕它们商品化的技能。在这些产品面前,几年前为特定组织需求构建的定制解决方案可能会显得不必要地昂贵,而曾经它是市场差异化因素。
可以采用的一种策略是“先建后买”,即组织为了满足其即时需求而构建一个平台,但等到市场已经确定了一个看起来将成为标准的产品时再考虑购买。当然,存在这样的风险,即构建的平台变成一个难以放弃的“宠物”。截至写作时,Kubernetes 似乎已经几乎完全主导了大多数流行的 Docker 平台。因此,如果你认为这是一个对未来有利的赌注,你可能会放弃定制的解决方案,转而选择 Kubernetes。
早期就下注的两个平台之一是 OpenShift,它在 Docker 出现在技术舞台上后不久就拥抱了它。它围绕 Docker 和 Kubernetes 重写了其整个代码库。因此,它目前是企业中一个非常受欢迎的选择。相比之下,亚马逊使用 Mesos 作为其 ECS 解决方案的基础,随着 Kubernetes 的普及,它越来越显得小众。
13.1.3. 单体与零散
是否运行一个针对所有 Docker 需求的单一“单体”平台,还是从独立的“零散”解决方案中构建功能,这个问题与“购买与自建”问题密切相关。当考虑从供应商那里购买单体解决方案时,上市时间可能是一个有力的理由来选择与他们合作。同样,这种方法也有其权衡之处。
最大的危险是所谓的“锁定”。一些供应商对每个部署解决方案的机器收费。如果你的 Docker 资产随着时间的推移而显著增长,许可费用可能会变得难以承受,平台可能会成为你脖子上的财务磨石。一些供应商甚至拒绝支持其他供应商提供的 Docker 容器,这使得它们的实际采用几乎成为不可能。
相反的是零散的方法。通过零散,我们指的是你可以(例如)有一个用于构建容器的解决方案,另一个用于存储容器(如 Docker 注册表),另一个用于扫描容器,还有一个用于运行容器(也许甚至为这个或任何前面的类别提供多个解决方案)。我们将在本章下一节中更深入地探讨可能需要解决的“部分”是什么。
再次,如果你是一个需要快速行动的小型(可能资金充裕)企业,单体方法可以为你提供帮助。零散的方法允许你在需要时采用不同的解决方案,为不同的部分提供更多灵活性和努力的重点。
13.1.4. 开源与许可
在过去十年中,开源已经取得了长足的进步,现在已成为供应商或支持解决方案的标准要求。这其中包含了一个不常明显的危险。尽管许多解决方案是开源的,但锁定并不一定能够避免。理论上,如果你与支持供应商发生冲突,软件的知识产权可供使用,但通常管理和支持代码库所需的技能并不具备。
如最近一位会议演讲者所说,“开源加供应商支持是新的锁定。”有人可能会认为这是供应商为你的组织带来的价值的有效理由——如果你需要大量罕见技能来管理所需的平台,你无论如何都需要为此付费。
这个混合体中一个有趣的新增元素是云计算解决方案,它们可以被视为既开源又许可。它们通常基于开源软件和开放标准(如亚马逊的 EKS),但它们可以将你锁定在其特定实现的标准和技术上,并以此方式获得你的锁定。
另一个有趣的情况是红帽(Red Hat)的 OpenShift 平台。OpenShift 是一个需要许可证才能运行的供应商提供的平台。但它的代码可在 GitHub 上获取,社区贡献可以被接受到主线中。因此,红帽提供的有价值的服务包括支持、功能开发和历史代码库的维护。从理论上讲,因此,如果你觉得他们的产品没有带来价值,你可以离开他们的实现。
13.1.5. 安全立场
安全问题可能会对平台选择产生重大影响。企业供应商如红帽(Red Hat)在安全管理方面有着强大的历史,OpenShift 在原生 Kubernetes 提供的安全保护之上增加了 SELinux 保护,以增强容器安全。
安全对你来说的重要性可能会有很大差异。我们参与过一些公司,其中开发人员对生产数据库拥有完全和信任的访问权限,也参与过一些公司,其中对安全的担忧达到了顶峰。这些不同的担忧程度在开发和生产中驱使出非常不同的行为,因此也会影响平台选择。
以一个简单的例子来说明:你是否信任你的数据和代码符合亚马逊网络服务(AWS)的安全标准和产品?在此我们并没有特别指出 AWS——据我们所知和所经历,他们的安全标准在云空间中通常被认为是首屈一指的。此外,你是否信任你的开发团队能够管理与应用团队相关的必要责任?关于 AWS S3 存储桶上暴露的私人数据已经有很多故事,这已经成为许多公司的关注点。
备注
在 S3 上暴露数据的责任明确属于 AWS 的消费者,而不是 AWS 本身。AWS 为你提供了全面的安全管理工具,但他们无法为你管理安全和操作需求。
13.1.6. 消费者独立性
一个很少被考虑的因素是团队希望自我管理的程度。在较小的组织中,这种程度的变化通常小于在较大的组织中。在较大的组织中,你可以得到从高度熟练且要求尖端技术平台到不太熟练且只想以精心策划的方式部署简单稳定 Web 应用的开发团队。
这些不同的需求可能导致不同的平台选择。例如,我们见过一些环境中,一个业务单元对集中式、精心策划和单一的平台感到满意,而另一个业务单元则要求高度的控制和特定的技术要求。这类用户可能会推动你选择比供应商提供的更定制的平台。如果这些用户愿意帮助构建和维护平台,就可以形成富有成效的合作伙伴关系。
如果您足够大,并且您的开发社区足够多样化,您甚至可能需要考虑为您的 Docker 平台追求多个选项。
13.1.7. 云战略
大多数从事 IT 业务的公司都对云平台持有某种立场。一些公司完全接受它,而另一些公司仍在开始走向它的旅程,正在迁移过程中,甚至正在回到老式的数据中心。
您的组织是否采用云 Docker 平台可以通过这种立场来确定。需要考虑的因素集中在是否存在所谓的“云供应商锁定”的恐惧,即将应用程序和数据从云供应商的数据中心迁移变得过于昂贵而无法容忍。这可以通过使用开放标准和产品来防范,甚至可以通过在那些云供应商提供的通用计算资源上运行现有产品(而不是使用他们精选的有时是云供应商特定的产品)来防范。
13.1.8. 组织结构
组织结构是任何公司的基本特征,它影响着这里的所有其他因素。例如,如果开发团队与运维团队分离,这往往意味着采用一个标准化的平台,两个团队都可以管理和与之工作。
类似地,如果运营的不同部分被不同的团队原子化,这往往会导致平台交付的零散方法。我们看到的这种例子之一是在大型组织中管理 Docker 注册库。如果已经有一个集中管理的工件存储库,那么简单地升级现有的存储库并将其用作 Docker 注册库(假设它支持这种用途)是有意义的。这样,存储库的管理和运营成本比为本质上相同的挑战构建单独的解决方案要低。
13.1.9. 多个平台?
在这一点上可能适当提及的一个模式是,对于有不同需求的大型组织,另一种方法是有可能的。您可能有一些消费者更喜欢他们可以使用的托管平台,而同一组织中的其他消费者可能要求更定制的解决方案。
在这种情况下,为第一组用户提供一个高度意见化且易于管理的平台,为其他人提供更灵活且可能更自我管理的解决方案是有意义的。在我们所了解的一个案例中,有三个选项可供选择:一个自我管理的 Nomad 集群、一个 AWS 管理的解决方案和一个 OpenShift 选项。
这种方法的明显困难在于,管理多个平台类别的成本增加,以及在整个组织中有效沟通这些选项的挑战。
13.1.10. 组织因素结论
希望这次讨论能引起你的共鸣,并给你一些关于在需求不同的组织中为 Docker(或任何技术)选择适当平台复杂性的想法。尽管这可能看起来有些抽象,但下一节将更加具体,因为我们将探讨在选择业务解决方案时你可能需要考虑的具体挑战。这次讨论为我们提供了评估这些问题及其可能解决方案的适当视角。
13.2. 采用 Docker 时需要考虑的领域
最后,我们将讨论在实施 Docker 平台时可能需要解决的具体功能挑战。
它分为三个部分:
-
安全和控制—探讨将取决于你组织的安全和控制立场的项目
-
构建和推送镜像—探讨在开发和交付镜像和工作负载时需要考虑的一些事项
-
运行容器—考虑在操作平台时需要思考的问题
在此过程中,我们将考虑一些具体的技术。提及一个产品并不意味着我们对其表示认可,我们提及的产品也不会详尽无遗。软件产品可以改进和衰落,可以被替换或合并。它们在这里被提及只是为了说明你平台选择的实际后果。
如果我们讨论的许多项目看起来很神秘,或者与你的组织无关,那么很可能你的组织没有太多限制,因此你有更大的自由去做你想做的事情。如果是这样,你可以考虑这一章提供了对大型和受监管企业中看到的某些挑战的见解。
13.2.1. 安全和控制
我们将首先处理安全问题,因为从许多方面来看,你的安全和控制立场将从根本上影响你处理所有其他话题的方式。此外,如果你的组织对安全的关注不如其他组织,你可能对解决本节中概述的问题不太关心。
注意
当我们提到“控制”时,指的是覆盖在开发团队和运行团队操作之上的治理系统。这包括集中管理的软件开发生命周期、许可证管理、安全审计、一般审计等。一些组织的管理较为宽松,而另一些则较为严格。
镜像扫描
无论你在哪里存储你的镜像,你都有在存储点检查这些镜像是否符合你期望的黄金机会。你可能想要检查的内容取决于你的用例,但以下是一些你可能希望实时回答的具体问题的例子:
-
哪些镜像使用了 bash 的 shellshock 版本?
-
任何镜像上是否有过时的 SSL 库?
-
哪些镜像基于现在可疑的基础镜像?
-
哪些镜像上安装了非标准(或完全错误)的开发库或工具?
注意
Shellshock 是 2014 年发现的 bash 中的一个特别严重的安全漏洞。安全公司在披露一系列相关漏洞的第一天就记录了数百万次针对该漏洞的攻击和探测。
图 13.2 展示了软件开发生命周期中图像扫描的基本工作流程。图像被构建并推送到注册表,这会触发图像扫描。扫描器可以检查注册表上的图像,或者下载它并对其进行处理。根据你对图像的担忧程度,你可以同步检查图像并阻止其使用,直到它得到批准,或者你可以异步检查图像并向提交用户提供报告。通常,对于生产中使用的图像,采用偏执的方法,而在开发中使用异步建议方法。
图 13.2. 图像扫描工作流程

在图像扫描的世界里,有很多选择,但它们并不完全相同。最重要的是理解扫描器大致分为两类:一类专注于已安装的包,另一类主要是为深入扫描图像中的软件而设计的。第一类的例子有 Clair 和 OpenSCAP,第二类的例子有 Black Duck Software、Twistlock、Aqua Security、Docker Inc. 以及许多其他公司。这两类之间有一些重叠,但主要的分界线是成本:维护必要的信息数据库以跟上各种库或二进制文件中的弱点需要更高的成本,因此深度扫描器往往成本更高。
这种划分可能对你的决策有相关性。如果你的图像是半可信的,你可能可以假设用户没有恶意,并使用一个更简单的包扫描器。这将为你提供有关标准包及其适当风险水平的指标和信息,而无需花费太多成本。
虽然扫描器可以降低你图像中恶意或不受欢迎软件的风险,但它们并不是万能的。我们评估它们的经验表明,即使是最好的扫描器也不是完美的,并且它们在识别某些类型的二进制文件或库的问题上可能比其他类型更好。例如,有些可能比用 C++ 编写的(比如说)更成功地识别 npm 包问题,反之亦然。参见第十四章技术 94 中的图像,我们用它来测试和验证这些扫描器。
另一点需要注意是,尽管扫描器可以在不可变图像上工作并检查这些图像的静态内容,但仍然存在一个风险,即容器可以在运行时构建和运行恶意软件。静态图像分析无法解决这个问题,因此你可能需要考虑运行时控制。
就像本节中的所有主题一样,在选择扫描器时,你必须考虑你想要实现的目标。你可能想要
-
防止恶意行为者在构建中插入对象
-
强制执行公司范围内的软件使用标准
-
快速修补已知的和标准的 CVE
注意
CVE 是软件漏洞的标识符,用于允许对特定错误的通用和明确的识别。
最后,你可能还想要考虑将此工具集成到你的 DevOps 管道中的成本。如果你找到一个让你满意的扫描器,并且它与你的平台(或其他相关 DevOps 工具)很好地集成,那么这可能是它有利的一个因素。
镜像完整性
镜像完整性和镜像扫描经常被混淆,但它们并不是同一件事。而镜像扫描确定镜像中有什么,镜像完整性确保从 Docker 注册表中检索的内容与安全放置的内容相同。(镜像验证也是描述这一要求的另一种常见方式。)
想象以下场景:Alice 将一个镜像放入仓库(镜像 A),在它经过任何必须的流程以检查该镜像之后,Bob 想要在服务器上运行该镜像。Bob 从服务器请求镜像 A,但 Bob 并不知道,攻击者(Carol)已经破坏了网络,并在 Bob 和注册表之间放置了一个代理。当 Bob 下载镜像时,他实际上得到了一个恶意镜像(镜像 C),该镜像会运行代码,将机密数据传输到网络外的第三方 IP 地址。(见图 13.3。)
图 13.3. 镜像完整性破坏

问题随之而来:当下载 Docker 镜像时,你如何确保它就是你所请求的那个?确保这一点正是镜像完整性所解决的问题。
Docker Inc. 在其 Content Trust 产品中带了个头,也称为 Notary。该产品使用私钥对镜像清单进行签名,确保当内容使用公钥解密时,内容与上传到注册表的内容相同。Content Trust 提供了关于密钥责任委派的其他功能,这里不会详细介绍。
在 Docker 提供的范围内,截至 2018 年没有太多可以报告的,这可以说是对他们在这一领域工程领导地位的致敬。像 Kubernetes 和 OpenShift 这样的领先产品在出厂时提供的功能非常有限,所以如果你不购买 Docker 的产品,你可能必须自己集成这些。对于许多组织来说,这样的努力不值得付出努力,因此他们将依赖现有的(可能是外围的)防御措施。
如果你设法实现了镜像完整性解决方案,你仍然必须考虑如何在你的组织中管理这些密钥。那些足够关心并走到这一步的组织可能已经为此制定了政策和解决方案。
第三方镜像
在继续讨论图像的话题时,另一个在提供平台时常见的挑战是如何处理外部图像的问题。同样,这里的根本困难是信任问题:如果你有一个想要将 Docker 图像带入你平台的供应商,你如何确保它安全运行?在多租户环境中,这是一个特别重要的问题,因为不同的团队(他们不一定相互信任)必须在同一主机上运行容器。
一种方法就是简单地禁止所有第三方图像,只允许使用存储在企业网络内的代码和工件构建已知和精选的基础图像。一些供应商的图像仍然可以在这种制度下运行。如果供应商图像本质上是一个在标准 JVM(Java 虚拟机)下运行的 JAR(Java 归档)文件,那么可以从该工件在网络上重新创建和构建该图像,并在批准的 JVM 图像下运行。
然而,不可避免的是,并非所有图像或供应商都会接受这种方法。如果允许第三方图像的压力足够大(根据我们的经验,确实是这样的),你有几个选择:
-
信任你的扫描仪
-
通过肉眼检查图像
-
让将图像带入组织的团队负责其管理
在没有图像完全嵌入到系统中之前,你不太可能完全信任扫描仪为你提供关于第三方图像安全性的充分确定性,因此责任可能需要放在其他地方。
第二种选择,手动检查图像,不可扩展且容易出错。最后一种选择是最简单且最容易实施的。
我们看到过采取所有三种方法的环境,平台管理团队对图像进行合理性检查,但最终责任落在将图像带入的组织应用团队。通常,组织中已经存在将虚拟机图像带入组织的流程,因此对于 Docker 图像,简单的方法是复制此程序。这里值得指出的一项关键差异是,尽管虚拟机是多租户的,因为它们与同租户共享一个虚拟机管理程序,但 Docker 图像共享一个功能齐全的操作系统,这为攻击提供了更大的攻击面(有关更多信息,请参阅第十四章 chapter 14 关于安全的内容)。
另一个选择是在自己的硬件环境中沙箱化图像的运行,例如通过在集群上标记 Kubernetes 节点,或使用像 ECS 这样的云产品的单独实例,或者在不同的硬件甚至网络上运行一个完全独立的平台。
秘密
以某种方式(尤其是在你进入生产阶段时),需要以安全的方式管理特权信息。特权信息包括传递给构建的文件或数据,例如
-
SSL 密钥
-
用户名/密码组合
-
客户识别数据
将秘密数据传递到软件生命周期中的做法可以在多个点进行。一种方法是在构建时将秘密嵌入到你的镜像中。这种方法被高度反对,因为它会将特权数据传播到镜像的任何地方。
更受认可的方法是在运行时让平台将秘密放入你的容器中。有各种方法可以做到这一点,但需要回答几个问题:
-
秘密在存储时是否被加密?
-
秘密在传输过程中是否被加密?
-
谁可以访问秘密(在存储中或在容器运行时)?
-
如何在容器内暴露秘密?
-
你能否追踪或审计谁看到了或使用了秘密?
Kubernetes 有一个所谓的“秘密”功能。许多人对这一点感到惊讶的是,它在持久存储(一个 etcd 数据库)中以纯文本形式存储。技术上,它是 base64 编码的,但从安全角度来看,这是纯文本(未加密,且容易逆转)。如果有人带着包含这些信息的磁盘离开,他们可以轻松地访问这些秘密。
目前,有一些概念验证实现,如 HashiCorp 的 vault 与 Kubernetes 集成。Docker Swarm 自带更安全的秘密支持,但 Docker Inc.似乎在 2017 年底将宝押在了 Kubernetes 上。
审计
在生产环境(或任何其他敏感环境)中运行时,证明你对谁运行了什么命令以及何时运行可能变得至关重要。这对于开发者来说可能不是那么明显,因为开发者不太关心恢复此类信息。
这个“根”问题的原因在第十四章中有详细说明,但可以简要地在这里说,给予用户对 Docker 套接字的访问实际上给了他们整个主机的 root 控制权。这在许多组织中是禁止的,因此至少需要可追溯地访问 Docker。
这些是你可能需要回答的一些问题:
-
谁(或什么)能够运行
docker命令? -
你对谁运行它有什么控制权?
-
你对运行的内容有什么控制权?
对于这个问题,存在一些解决方案,但它们相对较新,通常作为其他更大解决方案的一部分。例如,OpenShift 通过为 Kubernetes 添加强大的 RBAC(基于角色的访问控制)而走在前列。Kubernetes 后来将其添加到其核心。云提供商通常有更多云原生的方法通过(在 AWS 的情况下)使用 IAM 角色或嵌入在 ECS 或 EKS 中的类似功能来实现这种控制。
由 Twistlock 和 Aqua Security 等供应商提供的容器安全工具提供了一种管理特定 Docker 子命令和标志可以由谁运行的方法,通常是通过在你和 Docker 套接字之间添加一个中间套接字或其他类型的代理来实现,该代理可以代理对 Docker 命令的访问。
在记录谁做了什么方面,原生功能在 OpenShift 等产品中进展缓慢,但现在已经有了。如果你查看其他产品,不要假设这种功能已经完全实现!
运行时控制
运行时控制可以被视为更高层次的审计。受监管的企业可能希望能够确定其整个环境中正在运行的内容,并对此进行报告。这些报告的输出可以与现有的配置管理数据库(CMDB)进行比较,以查看是否存在任何异常或无法解释的运行工作负载。
在这个层面,你可能需要回答以下问题:
-
你如何判断正在运行什么?
-
你能将内容与你的注册表/注册表和/或你的 CMDB 匹配起来吗?
-
自启动以来,是否有容器更改了关键文件?
再次强调,这可能与一些可能成为你 Docker 策略一部分的其他产品有关,所以要注意它们。或者,这可能是你整体应用程序部署策略和网络架构的副作用。例如,如果你使用 Amazon VPC 构建和运行容器,建立和报告其中的内容是一个相对简单的问题。
在这个领域,另一个常见的卖点就是异常检测。安全解决方案提供了复杂的机器学习解决方案,声称能够学习容器应该做什么,如果它看起来做了不寻常的事情,比如连接到与应用程序无关的外部应用程序端口,它们会向你发出警报。
这听起来很棒,但你需要考虑这在操作层面上如何运作。你可能会得到很多误报,这些可能需要大量的维护——你有资源来处理这些吗?一般来说,组织越大,对安全的意识越强,就越有可能对此表示关注。
法医
法医类似于审计,但更加专注。当发生安全事件时,各方都想知道发生了什么。在物理和虚拟机旧世界中,有大量的安全措施来协助事件后的调查。各种描述的代理和监视进程可能已经在操作系统上运行,或者可以在网络甚至硬件级别放置监听器。
这些是一些安全事件发生后,法医团队可能希望得到解答的问题:
-
你能说出谁运行了容器吗?
-
你能说出谁构建了容器吗?
-
你能确定容器消失后它做了什么吗?
-
你能确定容器消失后可能做了什么吗?
在这个背景下,你可能需要强制使用特定的日志解决方案,以确保系统活动信息在容器实例化之间持续存在。
Sysdig 及其 Falco 工具(这是一个开源工具)是该领域另一个有趣且具有潜力的产品。如果你熟悉 tcpdump,这个工具看起来非常相似,允许你查询正在进行的系统调用。以下是一个此类规则的示例:
container.id != host and proc.name = bash
如果在容器中运行 bash shell,则匹配。
Sysdig 的商业产品不仅限于监控,还允许你根据定义的规则集对跟踪的行为采取行动。
13.2.2. 构建和分发镜像
在安全得到保障后,我们转向构建和分发。本节探讨了在构建和分发镜像时你可能需要考虑的事项。
构建镜像
在构建镜像时,有几个领域你可能需要考虑。
第一,尽管 Dockerfile 是标准,但构建镜像的其他方法也存在(参见第七章),因此如果多种方式可能会引起混淆或彼此不兼容,强制执行一个标准可能是可取的。你可能还有一个战略性的配置管理工具,你希望将其与标准操作系统部署集成。
我们的实际经验表明,Dockerfile 方法在开发者中根深蒂固且受欢迎。学习更复杂的 CM 工具以符合公司对虚拟机的标准,通常不是开发者有时间或意愿去做的事情。S2I 或 Chef/Puppet/Ansible 等方法更常用于便利或代码重用。支持 Dockerfile 将确保你将收到更少的问题和来自开发社区的反对。
第二,在敏感环境中,你可能不希望所有用户都能构建镜像,因为镜像可能被内部或外部的其他团队信任。可以通过适当的标记或镜像提升(见下文)或基于角色的访问控制来限制构建。
第三,值得考虑的是开发者的体验。出于安全原因,并不总是可能允许用户从公共仓库下载 Docker 镜像,甚至无法在他们的本地环境中运行 Docker 工具(参见第十四章)。如果情况如此,你可能想要探索以下几种选项:
-
获得标准工具的批准。这可能会很昂贵,有时由于安全挑战和业务需求,成本过高以至于难以实现。
-
创建一个可丢弃的沙盒,其中可以构建 Docker 镜像。如果虚拟机是瞬时的、受限制的且经过严格审计,许多安全担忧将显著减轻。
-
通过任何 Docker 客户端提供对上述沙盒的远程访问(但请注意,这并不一定显著减少许多攻击面)。
第四,在部署应用程序时,开发者体验的一致性也值得考虑。例如,如果开发者在他们的笔记本电脑或测试环境中使用 docker-compose,他们可能会对在生产环境中切换到 Kubernetes 部署感到抵触。(随着时间的推移,这个最后一点变得越来越不重要,因为 Kubernetes 已成为标准。)
注册表
到现在为止,您应该很明显需要注册表。有一个开源示例,Docker Distribution,但它不再是主导选择,主要是因为 Docker 注册表是一个知名 API 的实现。如果您想付费购买企业注册表,或者想自己运行开源注册表,现在有众多选择可供选择。
Docker Distribution 是 Docker 数据中心产品的一部分,它具有一些吸引人的功能(如内容信任)。
无论您选择哪个产品,都有一些可能不那么明显的问题需要考虑:
-
这个注册表与您的身份验证系统兼容吗?
-
它是否有基于角色的访问控制(RBAC)?
认证和授权对企业来说非常重要。一个快速且便宜的免费注册表解决方案可以在开发中完成工作,但如果您有安全或 RBAC 标准需要维护,这些要求将排在您的列表之首。
一些工具具有较粗粒度的 RBAC 功能,如果您突然发现自己正在接受审计并发现不足,这可能是一个很大的漏洞。
-
它有推广图像的手段吗?—并非所有图像都是平等的。有些是快速且草率的开发实验,其中正确性不是必需的,而有些则是为防弹的生产使用而设计的。您组织的流程可能需要您区分这两者,而注册表可以通过管理通过单独实例或通过标签强制执行的门来帮助您做到这一点。
-
它与您的其他工件存储库兼容吗?—您可能已经有了用于 TAR 文件、内部包等工件存储库。在一个理想的世界里,您的注册表可能只是那个存储库中的一个功能。如果这不是一个选项,集成或管理开销将是一个您应该注意的成本。
基础镜像
如果您在考虑标准,团队使用的基镜像(或镜像)可能需要一些考虑。
首先,您想使用哪个根镜像,以及应该包含什么?通常,组织会有一个他们偏好的标准 Linux 发行版。如果是这样,那么这个版本可能被强制作为基础。
第二,您将如何构建和维护这些镜像?在发现漏洞的情况下,谁(或什么)负责确定您是否受到影响或哪些镜像受到影响?谁负责修补受影响的领域?
第三,基础镜像中应该包含什么?是否有一组所有用户都想要的工具,或者你想让各个团队自己决定?你想将这些需求分离成单独的子镜像吗?
第四,这些镜像和子镜像将如何重建?通常需要创建某种类型的管道。通常这会使用某种类型的持续集成工具,例如 Jenkins,在触发某些事件时自动构建基础镜像(以及随后从该镜像构建任何子镜像)。
如果你负责基础镜像,你可能会经常被问到这个镜像的大小。人们经常争论说,瘦镜像更好。在某些方面(如安全性)可能会有这样的争论,但这个问题通常是被想象出来的,而不是真实的,尤其是在性能方面。这种情况的矛盾性质在技术 60 中进行了讨论。
软件开发生命周期
软件开发生命周期(SDLC)是软件采购、创建、测试、部署和退役的既定流程。在其理想状态下,它旨在通过确保软件在具有共同利益并希望集中资源的团队中一致性地评估、购买和使用来减少低效率。
如果你已经有 SDLC 流程,Docker 如何融入其中?人们可能会陷入哲学讨论,即 Docker 容器是一个包(如 rpm)还是一个完整的 Linux 发行版(因为其内容可能在开发者的控制之下)。无论如何,争论的关键点通常是所有权。谁对镜像中的什么负责?这就是 Docker 分层文件系统(见第一章)发挥作用的地方。因为最终镜像中创建的内容是完全可审计的(假设内容是可信的),所以追踪到谁负责软件栈的哪个部分相对简单。
一旦确定了责任,你可以考虑如何处理补丁:
-
你如何确定哪些镜像需要更新?—一个扫描器可以在这里帮助,或者任何可以识别可能感兴趣的工件中文件的工具。
-
你如何更新它们?—一些平台允许你触发容器的重建和部署(如 OpenShift,或者可能是你手工制作的管道)。
-
你如何告诉团队进行更新?—一封电子邮件足够吗?或者你需要一个可识别的负责人。再次强调,你的公司政策可能会在这里起到指导作用。现有的政策应该适用于更传统的已部署软件。
在这个新世界中,关键点是负责容器的团队数量可能比过去更多,需要评估或更新的容器数量也可能显著增加。如果你没有建立相应的流程来处理软件交付的增加,所有这些都可能给你的基础设施团队带来沉重的负担。如果情况恶化,你可能需要通过向用户的镜像中添加层来强制他们更新,如果他们不排队的话。如果你运行的是共享平台,这一点尤为重要。你甚至可以考虑使用你的编排工具将“顽皮”的容器放在特定的隔离主机上以降低风险。通常,这些事情都是考虑得太晚,必须即兴发挥。
13.2.3. 运行容器
现在我们将探讨容器的运行。在许多方面,运行容器与运行单个进程几乎没有区别,但 Docker 的引入可能会带来自己的挑战,Docker 所启用的行为变化也可能迫使你思考基础设施的其他方面。
操作系统
当运行 Docker 平台时,你运行的操作系统可能变得很重要。企业操作系统可能落后于最新的内核版本,正如你将在第十六章(chapter 16)中看到的,运行的内核版本对你的应用程序可能非常重要。
从历史上看,Docker 是一个非常快速发展的代码库,并不是所有的精选操作系统都能够跟上(1.10 对我们来说是一个特别痛苦的过渡,因为对镜像的存储格式进行了重大更改)。在你向供应商承诺他们的应用程序将在你的 Kubernetes 集群上运行之前,检查你的包管理器中可用的 Docker(和相关技术,如 Kubernetes)的版本是值得的。
共享存储
当你的用户开始部署应用程序时,他们首先关心的一件事就是他们的数据去哪里。Docker 的核心使用的是与运行中的容器无关的卷(见第五章 chapter 5)。
这些卷可以由多种类型的存储支持,这些存储可以是本地或远程挂载的,但关键点是存储可以被多个容器共享,这使得它非常适合运行跨容器周期持久化的数据库。
-
共享存储是否容易配置?——共享存储在维护和配置方面可能很昂贵,无论是从所需的基础设施还是按小时成本来看。在许多组织中,配置存储不仅仅是调用 API 并等待几秒钟的事情,就像在 AWS 这样的云提供商中那样。
-
共享存储支持是否准备好应对增加的需求?——因为部署 Docker 容器和开发或测试的新环境非常容易,对共享存储的需求可能会急剧增加。考虑你是否为此做好了准备是值得的。
-
共享存储是否可在部署位置之间使用?——你可能拥有多个数据中心或云提供商,甚至两者混合。所有这些位置能否无缝通信?这是否是一个要求?或者,这是一个不要求的要求?监管约束和为开发者启用功能的需求都可能为你创造工作。
网络
关于网络,在实施 Docker 平台时,你可能需要考虑以下几点。
如 第十章 所见,默认情况下,每个 Docker 容器都会从预留的 IP 地址集中分配一个 IP 地址。如果你引入了一个管理你网络上容器运行的产品,可能还有其他网络地址集被预留。例如,Kubernetes 的服务层使用一组网络地址来维护和路由其节点集群中的稳定端点。
一些组织为特定目的预留 IP 范围,因此你需要警惕冲突。例如,如果某个 IP 地址范围被预留用于特定的数据库集,那么使用你集群内 IP 范围的容器或服务可能接管这些 IP,从而阻止集群内其他应用程序访问该数据库集。这些数据库的流量最终会被路由到集群内的容器或服务 IP。
网络性能也可能变得非常重要。如果你已经在网络上部署了软件定义网络(SDN,例如 Nuage 或 Calico),再为 Docker 平台添加更多 SDN(例如 OpenVSwitch 或甚至另一个 Calico 层)可能会明显降低性能。
容器也可能以你意想不到的方式影响网络。许多应用程序传统上使用稳定的源 IP 地址作为外部服务认证的一部分。然而,在容器世界中,容器提供的源 IP 可能是容器 IP 或容器运行的宿主机的 IP(该宿主机执行网络地址转换 [NAT] 回到容器)。此外,如果它来自主机集群,提供的 IP 不能保证是稳定的。确保 IP 展示稳定性的方法存在,但通常需要一些设计和实施工作。
负载均衡是另一个可能需要大量努力的领域。关于这个话题有很多内容要涵盖,它可能成为另一本书的主题,但这里有一个简要的列表:
-
哪个产品更受欢迎/是标准产品(例如,NGinx、F5s、HAProxy、HTTPD)?
-
你将如何处理 SSL 终止?
-
你是否需要一个相互认证的 TLS 解决方案?
-
证书将在你的平台上如何生成和管理?
-
你的负载均衡器是否以与其他业务应用程序一致的方式影响头部信息(如果不一致,请准备进行大量的调试)?
最后,如果您除了已经拥有或使用的任何数据中心外,还在使用云服务提供商,您可能需要考虑用户如何从云服务提供商连接回本地服务。
记录
几乎每个应用程序都会与其相关的日志文件。大多数应用程序都希望以持久的方式访问这些日志(尤其是在生产环境中),因此通常需要某种集中式日志记录服务。由于容器是短暂的(而虚拟机和专用服务器通常不是),如果容器死亡且日志存储在其文件系统上,这些日志数据可能会丢失。因此,转向容器化世界可能会因为这些原因而使日志记录挑战更加突出。
由于记录是应用程序功能的核心和常见部分,因此集中化和标准化它通常是有意义的。容器提供了实现这一目标的机会。
监控
大多数应用程序在某种程度上都需要进行监控,与容器监控相关的供应商和产品种类繁多。这仍然是一个新兴领域。
在 Docker 领域有一个产品非常受欢迎,那就是 Prometheus。它最初由 SoundCloud 开发,随着时间的推移越来越受欢迎,尤其是在它成为云原生计算基金会的一部分之后。
由于容器与虚拟机或物理机器不同,传统的监控工具在容器内部、作为边车,或者在主机上(如果它们不具备容器感知能力)可能不会很好地工作。
话虽如此,如果您正在运行一组主机并需要维护它们,传统的、成熟的监控工具将非常有用。很可能,在您试图从集群中榨取最大性能以供最终用户使用时,它们会被大量依赖。这是假设平台成功的前提下。我们的经验表明,需求往往远超过供应。
13.3. 供应商、组织和产品
想从 Docker 中赚钱的公司和组织并不少见。在这里,我们将探讨截至 2018 年最大的和最有影响力的参与者,并尝试描述他们的努力方向以及他们的产品可能如何为您服务。
13.3.1. 云原生计算基金会(CNCF)
这些组织中的第一个不同之处在于它不是一个公司,但可能是这个领域最有影响力的参与者。CNCF(云原生计算基金会)成立于 2015 年,旨在推广容器技术的共同标准。创始成员包括
-
谷歌
-
推特
-
英特尔
-
思科
-
国际商业机器公司(IBM)
-
Docker
-
虚拟机软件公司(VMWare)
它的成立与 Kubernetes 1.0 的发布相吻合,Kubernetes 1.0 是由谷歌捐赠给 CNCF 的(尽管谷歌在之前一段时间就已经开源了它)。
CNCF 在容器领域的作用实际上是“造王者”。由于涉及的各种玩家的集体力量如此之大,当 CNCF 支持一项技术时,你知道关于它的两件事:它将得到投资和支持,而且不太可能有一个供应商比另一个供应商更受青睐。后一个因素对 Docker 平台消费者尤其重要,因为它意味着你的技术选择在可预见的未来不太可能过时。
CNCF 已经批准了大量的技术。我们将探讨其中一些最重要的:
-
Kubernetes
-
CNI
-
Containerd
-
Envoy
-
Notary
-
Prometheus
Kubernetes
Kubernetes 是 CNCF 的创始和最重要的技术之一。它是由谷歌捐赠给社区的,最初是作为开源软件,然后是给 CNCF。
虽然它是开源的,但其对社区的捐赠是谷歌将云技术商品化并使消费者更容易离开其他云服务提供商(其中最突出的是 AWS)的策略的一部分。
Kubernetes 是大多数 Docker 平台的基础技术,最著名的是 OpenShift,还有 Rancher,甚至是 Docker Inc. 自己的 Docker Datacenter,因为它们除了支持 Swarm 之外,还支持 Kubernetes。
CNI
CNI 代表容器网络接口。该项目为容器管理网络接口提供了一个标准接口。正如你在第十章节中看到的,网络对于容器管理来说可能是一个复杂的领域,而这个项目就是试图帮助简化其管理。
这里有一个(非常)简单的示例,它定义了一个环回接口:
{
"cniVersion": "0.2.0",
"type": "loopback"
}
此文件可能被放置在 /etc/cni/net.d/99-loopback.conf 中,并用于配置环回网络接口。
更复杂的示例可以在以下 Git 仓库中找到:github.com/containernetworking/cni。
Containerd
Containerd 是 Docker 守护进程的社区版本。它管理容器的生命周期。Runc 是其姐妹项目,负责运行容器本身。
Envoy
Envoy 是在 Lyft 建立的,以将他们的架构从单体架构迁移到微服务架构。它是一个高性能的开源边缘和服务代理,使网络对应用程序透明。
它允许直接管理关键的网络和集成挑战,如负载均衡、代理和分布式跟踪。
Notary
Notary 是 Docker Inc. 设计和构建的工具,用于签名和验证容器镜像的完整性。(请参阅第 317 页,“镜像完整性。”)
Prometheus
Prometheus 是一个与容器配合得很好的监控工具。它在社区中越来越受欢迎,例如,Red Hat 在他们的 OpenShift 平台上从 Hawkular 切换到 Prometheus。
13.3.2. Docker, Inc.
Docker Inc.是寻求从开源 Docker 项目中获利的商业实体。
注意
开源 Docker 项目已被 Docker Inc.更名为 Moby,试图为盈利目的保留 Docker 名称。到目前为止,这个名字还没有流行起来,所以在这本书中您不会看到太多关于 Moby 的提及。
如您所预期的那样,Docker Inc.在 Docker 产品空间中是一个早期的领导者。他们将多个产品组合成一个名为 Docker Datacenter 的单一产品。这包括对 Notary、注册表、Swarm 以及 Docker 开源的几个其他项目的支持、集成和功能。最近,Kubernetes 的支持已经提供。
由于 Docker 在早期就加入了这个领域,并且其在 Docker 早期就拥有强大的技术声誉,因此他们的产品在“快速进入生产”这一指标上非常吸引人。随着时间的推移,Docker 的产品在其他人赶上之后失去了优势。由于其“全要全不要”的策略和按服务器计费的模式,Docker 的业务模式在内部销售上遇到了困难,这使客户对单一供应商产生了强烈的依赖,该供应商可能对他们整个 Docker 平台进行勒索。
13.3.3. Google
Kubernetes 是由谷歌在 2014 年创建的,当时 Docker 的流行度激增。它的目的是将谷歌内部容器平台(Borg)背后的原则带给更广泛的受众。
大约在同一时间,谷歌云服务诞生了。推广 Kubernetes 是他们云战略的一部分。(请参阅第 327 页,“Kubernetes。”)
谷歌提供了一项名为 Google Kubernetes Engine (GKE)的付费服务,用于管理 Kubernetes 集群,类似于 AWS 的 EKS。
对于谷歌来说,其云服务是他们的一项关键业务优先事项,而 Kubernetes 的支持和鼓励是该战略的核心部分。
13.3.4. Microsoft
微软在多个方面参与了 Docker,所有这些都有助于扩展其 Azure 云服务。
首先,从 Windows 10 开始,微软已经在 Windows 平台上原生实现了 Docker API 到容器的支持。这使得 Windows 容器可以被构建和运行。对于 Windows 节点上的 Kubernetes 支持已有计划,但截至写作时,它仍处于早期阶段。
其次,微软在其.NET 平台的基础上推出了一项名为 Dotnet Core(如果您更喜欢,可以称为.NET Core)的产品,该产品为 Linux 上的.NET 代码库提供支持。并非所有.NET 库都受到支持,因此将您的 Windows 应用程序迁移过去绝非易事(到目前为止),但许多组织会对在 Linux 平台上运行他们的 Windows 代码的可能性感兴趣,甚至对从头开始构建以在任一平台上运行的可能性感兴趣。
第三,Azure 提供了一种针对 Kubernetes(AKS)的产品,也类似于 AWS 的 EKS 和谷歌云的 GKE。
所有这些努力都可以看作是为了鼓励用户迁移到 Azure 云。能够在 Windows 或 Linux(甚至两者都相同)上运行类似的工作负载对许多组织来说很有吸引力。特别是如果数据已经存储在他们自己的数据中心中。此外,微软处于一个很好的位置,可以向已经在微软技术上有大量投资并希望迁移到云的组织提供有吸引力的许可证套餐。
13.3.5. 亚马逊
亚马逊现在有几款容器产品,但可以说它在容器领域起步较晚。它的第一个产品是弹性容器服务(ECS),它使用 Mesos 在底层来管理容器的部署及其宿主机的管理。
这最初获得了一些关注,但很快在行业中被 Kubernetes 的流行所超越。亚马逊在 2017 年底通过宣布弹性 Kubernetes 服务(EKS)做出了回应,该服务(就像之前提到的 GKE 和 AKS 服务一样)是一个精选的 Kubernetes 服务。ECS 仍然得到支持,但似乎很自然地认为 EKS 将是它们更具战略性的服务。在 2017 年底还宣布了 Fargate,这是一个无需管理任何 EC2 实例即可原生运行容器的服务。
所有这些服务都提供了与其他 AWS 服务的紧密集成,如果你将 AWS 视为你软件的长期平台,这将非常方便。显然,AWS 的商业目标是确保你愿意继续支付他们的服务费用,但他们对 Kubernetes API 的广泛支持可以给消费者一些安慰,即与 AWS 平台的联系可以比其他服务更松散。
13.3.6. 红帽
红帽的商业战略是为客户定制、支持和管理工作核心软件,所谓的“开源品酒师”战略。红帽与其他商业玩家不同,它们不提供通用的云服务供消费者使用(尽管 OpenShift online 可以被视为云服务,因为它是一个外部托管的服务)。
红帽在两个领域关注容器。第一个是 OpenShift,这是一个围绕 Kubernetes 的产品,可以在多个环境中运行和支持,例如在本地上与这里提到的云提供商(以及一些其他提供商)一起运行,以及作为 Red Hat 的 OpenShift Online 服务的一部分。
OpenShift 的开发引入了各种企业功能(如 RBAC、内置镜像存储和 Pod 部署触发器),这些功能已经融入到核心 Kubernetes 中。
摘要
-
影响你选择 Docker 平台的一些主要决定因素可能包括你的“购买”与“构建”立场、你的安全立场、你的云战略,以及你的组织是否倾向于用“单体”或“零散”的产品来解决技术挑战。
-
这些因素反过来又可能受到你的软件的技术驱动因素、上市时间要求、消费者独立性的水平、你的开源策略以及你的组织结构的影响。
-
在一个较大的组织中,采用多平台方法可能是有意义的,但可能需要小心确保这些平台之间方法的一致性,以减少未来的组织低效率。
-
在实施 Docker 平台时可能需要考虑的主要功能区域包括如何构建镜像、镜像扫描和完整性、密钥管理、镜像注册以及底层操作系统。
-
Docker 平台领域的显著参与者包括 Docker Inc.、三大云服务提供商(AWS、Google Cloud Platform 和 Microsoft Azure)以及云原生计算基金会(CNCF)。
-
CNCF 是一个极具影响力的组织,它孵化并支持 Docker 平台的关键开源技术组件。CNCF 的全面认可是一个信号,表明这项技术将是可持续的。
第五部分. 生产环境中的 Docker
最后,我们准备开始考虑在生产环境中运行 Docker。在第五部分中,我们将讨论在生产环境中运行 Docker 时需要考虑的关键操作问题。
安全性是第十四章的重点。通过实际的技术,你将真正理解 Docker 带来的安全挑战以及你可能如何解决这些问题。
备份、日志记录和资源管理在第十五章中进行了讨论,我们将向您展示如何在 Docker 环境中管理这些传统的系统管理员任务。
最后,在第十六章中,我们将探讨当事情出错时你可以做什么,涵盖 Docker 可能遇到的一些常见问题区域,以及如何在生产环境中调试容器。
第十四章. Docker 和安全
本章涵盖
-
Docker 提供的默认安全功能
-
Docker 为提高安全性所采取的措施
-
其他方面都在做什么
-
可以采取哪些其他步骤来缓解安全担忧
-
如何使用 aPaaS 管理用户 Docker 权限,可能在多租户环境中
如 Docker 在其文档中明确指出的,对 Docker API 的访问意味着对 root 权限的访问,这就是为什么 Docker 通常需要使用 sudo 运行,或者用户必须被添加到允许访问 Docker API 的用户组(可能被称为“docker”或“dockerroot”)中。
在本章中,我们将探讨 Docker 中的安全问题。
14.1. Docker 访问及其含义
你可能想知道如果用户可以运行 Docker,他们能造成什么样的损害。作为一个简单的例子,以下命令(不要运行它!)将删除你主机机器上 /sbin 中的所有二进制文件(如果你移除了虚假的 --donotrunme 标志):
docker run --donotrunme -v /sbin:/sbin busybox rm -rf /sbin
值得指出的是,即使你不是 root 用户,这也同样适用。
以下命令将显示主机系统安全影子密码文件的内容:
docker run -v /etc/shadow:/etc/shadow busybox cat /etc/shadow
Docker 的不安全性通常被误解,部分原因是由于对内核中命名空间好处的误解。Linux 命名空间提供了对系统其他部分的隔离,但你拥有的隔离程度由你自行决定(如前述 docker run 示例所示)。此外,Linux 操作系统的所有部分并不都具有命名空间的能力。设备和内核模块是两个核心 Linux 功能的例子,它们不是命名空间化的。
小贴士
Linux 命名空间是为了允许进程拥有与其他进程不同的系统视图而开发的。例如,进程命名空间意味着容器只能看到与该容器关联的进程——在相同主机上运行的其它进程对他们来说是不可见的。网络命名空间意味着容器似乎有自己的网络堆栈可供使用。命名空间已经成为了 Linux 内核的多年组成部分。
此外,因为你能够通过系统调用从容器内以 root 身份与内核交互,任何内核漏洞都可能被 Docker 容器内的 root 用户利用。当然,虚拟机也有类似的攻击可能,这是通过访问虚拟机管理程序来实现的,因为虚拟机管理程序也有针对它们的安全漏洞报告。
理解这里风险的另一种方式是将运行 Docker 容器视为(从安全角度来看)与能够通过包管理器安装任何包没有区别。运行 Docker 容器时的安全需求应该与安装包时的需求相同。如果你有 Docker,你可以以 root 身份安装软件。这也是为什么有些人认为 Docker 最好被理解为一个软件打包系统的一部分原因。
小贴士
正在进行通过用户命名空间来消除这种风险的工作,它将容器中的 root 映射到主机上的非特权用户。
14.1.1. 你关心吗?
由于对 Docker API 的访问等同于 root 访问权限,接下来的问题是“你关心吗?”尽管这听起来可能是一条奇怪的论断,但安全就是关于信任的,如果你信任你的用户在他们操作的环境中安装软件,那么他们运行 Docker 容器时应该没有障碍。安全困难主要出现在考虑多租户环境时。因为容器内的 root 用户在关键方面与容器外的 root 用户相同,所以有大量不同的用户在系统中拥有 root 权限可能会引起担忧。
小贴士
多租户环境是指许多不同的用户共享相同资源的环境。例如,两个团队可能使用两个不同的虚拟机共享同一台服务器。通过共享硬件而不是为特定应用程序配置硬件,多租户提供了成本节约。但它也可能带来与服务可靠性和安全隔离相关的一些挑战,这些挑战可能会抵消成本节约。
一些组织采取为每个用户运行 Docker 在专用虚拟机上的方法。虚拟机可以用于安全、操作或资源隔离。在虚拟机的信任边界内,用户运行 Docker 容器以获得它们带来的性能和操作优势。这是 Google Compute Engine 采取的方法,它在用户的容器和底层基础设施之间放置一个虚拟机,以增加一层安全性和一些操作优势。Google 拥有大量的计算资源,所以他们并不介意这样做带来的开销。
14.2. Docker 中的安全措施
Docker 维护者已经采取了各种措施来降低运行容器时的安全风险。例如,
-
现在将某些核心挂载点(如/proc 和/sys)挂载为只读。
-
默认 Linux 权限已经降低。
-
支持第三方安全系统,如 SELinux 和 AppArmor。
在本节中,我们将更深入地探讨这些问题,以及你可以采取的一些措施来降低在系统上运行容器时的风险。
限制能力
正如我们已经提到的,容器中的 root 用户与主机上的 root 用户相同。但并非所有 root 用户都是平等的。Linux 为你提供了在进程内为 root 用户分配更细粒度权限的能力。
这些细粒度权限被称为能力,它们允许你限制用户即使作为 root 用户也能造成的损害。这项技术展示了如何在运行 Docker 容器时操作这些能力,特别是如果你不完全信任其内容时。
问题
你希望减少容器在主机上执行破坏性操作的能力。
解决方案
使用--drop-cap标志来降低容器可用的能力。
Unix 信任模型
要理解“降低能力”的含义和作用,需要一点背景知识。当 Unix 系统设计时,信任模型并不复杂。你有一些受信任的管理员(root 用户)和不受信任的用户。root 用户可以做任何事情,而标准用户只能影响自己的文件。由于系统通常在大学实验室中使用且规模较小,这种模型是合理的。
随着 Unix 模型的发展以及互联网的到来,这种模型变得越来越没有意义。像 Web 服务器这样的程序需要 root 权限来在端口 80 上提供服务内容,但它们也有效地充当了在主机上运行命令的代理。为了处理这种情况,建立了标准模式,例如绑定到端口 80 并将有效用户 ID 降低到非 root 用户。执行各种角色的用户,从系统管理员到数据库管理员,再到应用支持工程师和开发者,都可能需要系统上不同资源的细粒度访问。Unix 组在一定程度上缓解了这个问题,但任何系统管理员都会告诉你,建模这些权限需求是一个非平凡的问题。
Linux 能力
为了支持对特权用户管理的更细粒度方法,Linux 内核工程师开发了能力。这是尝试将单一的 root 权限分解成可以单独授予的功能片段。你可以通过运行man 7 capabilities(假设你已经安装了 man 页面)来详细了解它们。
Docker 默认关闭了一些能力。这意味着即使你在容器中有 root 权限,也有一些事情你无法做到。例如,CAP_NET_ADMIN能力,它允许你影响主机的网络堆栈,默认是禁用的。
表 14.1 列出了 Linux 能力,简要描述了它们允许做什么,并指出了它们在 Docker 容器中是否默认允许。
表 14.1. Docker 容器中的 Linux 功能
| 功能 | 描述 | 已开启? |
|---|---|---|
| CHOWN | 对任何文件进行所有权更改 | Y |
| DAC_OVERRIDE | 覆盖读取、写入和执行检查 | Y |
| FSETID | 修改文件时不清除 suid 和 guid 位 | Y |
| FOWNER | 在保存文件时覆盖所有权检查 | Y |
| KILL | 绕过信号上的权限检查 | Y |
| MKNOD | 使用 | Y |
| NET_RAW | 使用原始套接字和数据包套接字,并绑定到端口以进行透明代理 | Y |
| SETGID | 更改进程的组所有权 | Y |
| SETUID | 更改进程的用户所有权 | Y |
| SETFCAP | 设置文件功能 | Y |
| SETPCAP | 如果文件功能不受支持,则将功能限制应用于来自和来自其他进程 | Y |
| NET_BIND_SERVICE | 将套接字绑定到小于 1024 的端口 | Y |
| SYS_CHROOT | 使用 chroot | Y |
| AUDIT_WRITE | 写入内核日志 | Y |
| AUDIT_CONTROL | 启用/禁用内核日志 | N |
| BLOCK_SUSPEND | 使用阻止系统挂起的功能 | N |
| DAC_READ_SEARCH | 在读取文件和目录时绕过文件权限检查 | N |
| IPC_LOCK | 锁定内存 | N |
| IPC_OWNER | 绕过进程间通信对象的权限 | N |
| LEASE | 在普通文件上建立租约(监视尝试打开或截断) | N |
| LINUX_IMMUTABLE | 设置 FS_APPEND_FL 和 FS_IMMUTABLE_FL i 节点标志 | N |
| MAC_ADMIN | 覆盖强制访问控制(与 Smack Linux 安全模块(SLM)相关) | N |
| MAC_OVERRIDE | 强制访问控制更改(与 SLM 相关) | N |
| NET_ADMIN | 各种与网络相关的操作,包括 IP 防火墙更改和接口配置 | N |
| NET_BROADCAST | 未使用 | N |
| SYS_ADMIN | 一系列管理功能——更多信息请参阅 man capabilities | N |
| SYS_BOOT | 重启 | N |
| SYS_MODULE | 加载/卸载内核模块 | N |
| SYS_NICE | 操作进程的优先级 | N |
| SYS_PACCT | 打开或截断尝试的监视(进程会计) | N |
| SYS_PTRACE | 跟踪进程的系统调用和其他进程操作能力 | N |
| SYS_RAWIO | 在系统的各个核心部分执行 I/O,例如内存和 SCSI 设备命令 | N |
| SYS_RESOURCE | 控制和覆盖各种资源限制 | N |
| SYS_TIME | 设置系统时钟 | N |
| SYS_TTY_CONFIG | 在虚拟终端上进行特权操作 | N |
注意
如果你没有使用 Docker 的默认容器引擎(libcontainer),这些功能在你的安装中可能不同。如果你有系统管理员并且想确保,请向他们询问。
不幸的是,内核维护者只在该系统中分配了 32 个功能,因此随着越来越多的细粒度 root 权限从内核中划分出来,功能范围已经扩大。最值得注意的是,名为CAP_SYS_ADMIN的功能涵盖了从更改主机的域名到超出系统范围内打开文件数量限制的各种操作。
一种极端的方法是移除容器中默认启用的所有 Docker 功能,并查看哪些停止工作。在这里,我们启动一个 bash shell,移除了默认启用的功能:
$ docker run -ti --cap-drop=CHOWN --cap-drop=DAC_OVERRIDE \
--cap-drop=FSETID --cap-drop=FOWNER --cap-drop=KILL --cap-drop=MKNOD \
--cap-drop=NET_RAW --cap-drop=SETGID --cap-drop=SETUID \
--cap-drop=SETFCAP --cap-drop=SETPCAP --cap-drop=NET_BIND_SERVICE \
--cap-drop=SYS_CHROOT --cap-drop=AUDIT_WRITE debian /bin/bash
如果你从这个 shell 运行你的应用程序,你可以看到它在哪里无法按预期工作,并重新添加所需的功能。例如,你可能需要更改文件所有权的功能,因此你需要取消上一段代码中FOWNER功能的删除才能运行你的应用程序:
$ docker run -ti --cap-drop=CHOWN --cap-drop=DAC_OVERRIDE \
--cap-drop=FSETID --cap-drop=KILL --cap-drop=MKNOD \
--cap-drop=NET_RAW --cap-drop=SETGID --cap-drop=SETUID \
--cap-drop=SETFCAP --cap-drop=SETPCAP --cap-drop=NET_BIND_SERVICE \
--cap-drop=SYS_CHROOT --cap-drop=AUDIT_WRITE debian /bin/bash
小贴士
如果你想要启用或禁用所有功能,可以使用all而不是特定的功能,例如docker run -ti --cap-drop=all ubuntu bash。
讨论
如果你使用 bash shell 运行一些基本命令,并且禁用了所有功能,你会发现它相当可用。然而,当运行更复杂的应用程序时,你的体验可能会有所不同。
警告
值得明确的是,许多这些功能与影响系统上其他用户对象的 root 功能相关,而不是 root 自己的对象。例如,如果 root 用户在容器中,并且通过卷挂载访问主机的文件,他们仍然可以更改主机上 root 的文件的所有权。因此,仍然值得确保应用程序尽快降级为非 root 用户,以保护系统,即使所有这些功能都已关闭。
这种微调容器功能的能力意味着使用--privileged标志来运行docker run应该是多余的。需要功能的过程将是可审计的,并且受主机管理员控制。
| |
一个“不良”的 Docker 镜像用于扫描
在 Docker 生态系统中,一个迅速认识到的问题是漏洞——如果你有一个不变的镜像,你也不会得到任何安全修复。如果你遵循 Docker 最佳实践中的镜像最小化,这可能不是问题,但很难判断。
图像扫描器是为了解决这个问题而创建的——一种识别镜像问题的方法,但这仍然留下了如何评估它们的问题。
问题
你想要确定图像扫描仪的有效性。
解决方案
创建一个“已知不良”的镜像来测试你的扫描器。
我们在工作中遇到了这个问题。存在许多 Docker 镜像扫描器(如 Clair),但商业产品声称可以更深入地检查镜像,以确定其中可能存在的任何潜在问题。
但没有包含已知和记录的漏洞的图像,我们可以用它来测试这些扫描器的有效性。这几乎不出所料,因为大多数图像不会宣传它们自己的不安全性!
因此,我们发明了一个已知不良的图像。该图像可供下载:
$ docker pull imiell/bad-dockerfile
原则很简单:创建一个 Dockerfile 来构建一个充满记录漏洞的图像,并将该图像指向您的候选扫描器。
Dockerfile 的最新版本可在github.com/ianmiell/bad-dock找到。它仍在变化中,所以这里没有打印出来。然而,它的形式非常简单:
FROM <base image> *1*
RUN <install 'bad' software> *2*
COPY <copy 'bad' software in> *2*
[...]
CMD echo 'Vulnerable image' && /bin/false *3*
-
1 参考 bad-dockerfile 存储库使用 centos 图像,但您可能希望将其替换为与您的基图像更接近的一个。
-
2 各种 RUN/COPY/ADD 命令会将已知漏洞的软件安装到图像中。
-
3 图像的 CMD 指令出于明显的原因,尽可能避免自己被运行。
图像包含一系列漏洞,旨在将扫描器推到极限。
在最简单的情况下,图像使用包管理器安装已知漏洞的软件。在每个类别中,Docker 图像试图包含不同严重程度的漏洞。
通过(例如)COPY受漏洞影响的 JavaScript、使用特定语言的包管理器(例如 npm 用于 JavaScript、gem 用于 Ruby 和 pip 用于 Python)安装受漏洞影响的代码,甚至编译特定版本的 bash(一个带有臭名昭著的 Shellshock 漏洞的版本)并将其放置在意外位置以避免许多扫描技术,进行更复杂的漏洞放置。
讨论
您可能认为最好的扫描解决方案是能够捕获最多 CVE 的解决方案。但这并不一定是事实。显然,如果扫描器能够发现图像中存在漏洞,这是很好的。然而,除了这一点之外,漏洞扫描可能更多地成为一门艺术而不是一门科学。
提示
常见漏洞披露(CVE)是一般可用软件中发现的具体漏洞的标识符。CVE 的一个例子可能是 CVE-2001-0067,其中前四位数字是发现年份,后四位是该年的已识别漏洞数量。
例如,一个漏洞可能非常严重(例如在您的宿主服务器上获得 root 权限),但非常难以利用(例如需要国家层面的资源)。您(或您负责的组织)可能对此比一个不那么严重但容易利用的漏洞更不担心。例如,如果您的系统遭到 DoS 攻击,没有数据泄露或渗透的风险,但您可能会因此失去业务,所以您会更关注修补这个问题,而不是需要数万美元计算能力的某些晦涩的加密攻击。
什么是 DoS 攻击?
DoS 代表“服务拒绝”。这意味着一种攻击,会导致你的系统应对需求的能力降低。服务拒绝攻击可能会使你的 Web 服务器超负荷,以至于无法响应用户的合法请求。
还值得考虑的是,漏洞实际上是否存在于正在运行的容器中。Apache Web 服务器的旧版本可能存在于镜像中,但如果容器从未实际运行过,那么漏洞实际上是可以忽略的。这种情况经常发生。包管理器通常会引入一些不必要的依赖项,仅仅是因为它使依赖项的管理变得简单。
如果安全性是一个很大的担忧,那么拥有小图像(参见第七章)可以成为另一个原因——即使某个软件未被使用,它仍然可能出现在安全扫描中,浪费你的组织在确定是否需要打补丁时的时间。
希望这项技术在你考虑哪种扫描器适合你时能给你一些思考。一如既往,这是成本、你需要什么以及你愿意为获得正确解决方案付出多少努力之间的平衡。
14.3. 保护 Docker 的访问
防止 Docker 守护进程的不安全使用最好的方法是完全防止任何使用。
你可能第一次遇到受限访问是在安装 Docker 并需要使用sudo运行 Docker 本身时。技术 41 描述了如何选择性地允许本地机器上的用户使用 Docker 而不受此限制。
但如果你有用户从另一台机器连接到 Docker 守护进程,这并没有帮助。我们将探讨在那些情况下提供更多安全性的几种方法。
Docker 实例上的 HTTP 身份验证
在技术 1 中,你看到了如何打开对守护进程的网络访问,而在技术 4 中,你看到了如何使用 socat 窃听 Docker API。
这种技术结合了这两者:你将能够远程访问你的守护进程并查看响应。访问权限仅限于具有用户名/密码组合的用户,因此它稍微安全一些。作为额外的好处,你不需要重新启动 Docker 守护进程就能实现它——启动一个容器守护进程。
问题
你希望你的 Docker 守护进程上提供基本身份验证和网络访问。
解决方案
使用 HTTP 身份验证暂时与他人共享你的 Docker 守护进程。
图 14.1 展示了该技术的最终架构。
图 14.1. 基本身份验证的 Docker 守护进程架构

注意
本讨论假设你的 Docker 守护进程正在使用 Docker 默认的 Unix 套接字访问方法,位于/var/run/docker.sock。
本技术中的代码可在github.com/docker-in-practice/docker-authenticate找到。以下列出的是该存储库中 Dockerfile 的内容,用于创建本技术的镜像。
列表 14.1. Dockerfile
FROM debian
RUN apt-get update && apt-get install -y \
nginx apache2-utils *1*
RUN htpasswd -c /etc/nginx/.htpasswd username *2*
RUN htpasswd -b /etc/nginx/.htpasswd username password *3*
RUN sed -i 's/user .*;/user root;/' \
/etc/nginx/nginx.conf *4*
ADD etc/nginx/sites-enabled/docker \
/etc/nginx/sites-enabled/docker *5*
CMD service nginx start && sleep infinity *6*
-
1 确保所需的软件已更新并安装
-
2 为名为 username 的用户创建密码文件
-
3 将名为 username 的用户的密码设置为“password”
-
4 Nginx 需要以 root 用户身份运行以访问 Docker Unix 套接字,因此您需要将用户行替换为“root”用户详情。
-
5 复制 Docker 的 nginx 站点文件(列表 14.8)
-
6 默认情况下,启动 nginx 服务并无限期等待
使用htpasswd命令设置的.htpasswd文件包含在允许(或拒绝)访问 Docker 套接字之前需要检查的凭据。如果您自己构建此镜像,您可能希望在这两个步骤中更改username和password以自定义访问 Docker 套接字的凭据。
警告
请务必不要分享这张图片,因为它将包含您设置的密码!
以下列出的是 Docker 的 nginx 站点文件。
列表 14.2. /etc/nginx/sites-enabled/docker
upstream docker {
server unix:/var/run/docker.sock; *1*
}
server {
listen 2375 default_server; *2*
location / {
proxy_pass http://docker; *3*
auth_basic_user_file /etc/nginx/.htpasswd; *4*
auth_basic "Access restricted"; *5*
}
}
-
1 在 nginx 中将 docker 位置定义为指向 Docker 的域套接字
-
2 监听端口 2375(Docker 的标准端口)
-
3 将这些请求代理到之前定义的 docker 位置
-
4 定义要使用的密码文件
-
5 通过密码限制访问
现在以守护进程容器的形式运行镜像,映射主机机器所需资源:
$ docker run -d --name docker-authenticate -p 2375:2375 \
-v /var/run:/var/run dockerinpractice/docker-authenticate
这将以docker-authenticate的名称在后台运行容器,以便您可以稍后引用。容器的主机端口 2375 被暴露,并且容器通过挂载包含 Docker 套接字的默认目录作为卷来获得对 Docker 守护进程的访问权限。如果您使用的是带有您自己的 username 和 password 的自定义构建镜像,您需要在此处将镜像名称替换为您自己的。
网络服务现在将启动并运行。如果您使用您设置的 username 和 password curl该服务,您应该看到 API 响应:
$ curl http://username:password@localhost:2375/info *1*
{"Containers":115,"Debug":0, > *2*
"DockerRootDir":"/var/lib/docker","Driver":"aufs", >
"DriverStatus":[["Root Dir","/var/lib/docker/aufs"], >
["Backing Filesystem","extfs"],["Dirs","1033"]], >
"ExecutionDriver":"native-0.2", >
"ID":"QSCJ:NLPA:CRS7:WCOI:K23J:6Y2V:G35M:BF55:OA2W:MV3E:RG47:DG23", >
"IPv4Forwarding":1,"Images":792, >
"IndexServerAddress":"https://index.docker.io/v1/", >
"InitPath":"/usr/bin/docker","InitSha1":"", >
"KernelVersion":"3.13.0-45-generic", >
"Labels":null,"MemTotal":5939630080,"MemoryLimit":1, >
"NCPU":4,"NEventsListener":0,"NFd":31,"NGoroutines":30, >
"Name":"rothko","OperatingSystem":"Ubuntu 14.04.2 LTS", >
"RegistryConfig":{"IndexConfigs":{"docker.io": >
{"Mirrors":null,"Name":"docker.io", >
"Official":true,"Secure":true}}, >
"InsecureRegistryCIDRs":["127.0.0.0/8"]},"SwapLimit":0}
-
1 将 username:password 放入 curl 的 URL 中,以及@符号后面的地址。此请求是 Docker 守护进程 API 的/info 端点。
-
2 Docker 守护进程的 JSON 响应
完成后,使用以下命令删除容器:
$ docker rm -f docker-authenticate
现在访问已被撤销。
使用 docker 命令?
读者可能会想知道其他用户是否能够使用docker命令连接——例如,如下所示:
docker -H tcp://username:password@localhost:2375 ps
在撰写本文时,认证功能并未内置到 Docker 本身。但我们创建了一个可以处理认证并允许 Docker 连接到守护进程的镜像。只需按以下方式使用镜像:
$ docker run -d --name docker-authenticate-client \ *1*
-p 127.0.0.1:12375:12375 \ *2*
dockerinpractice/docker-authenticate-client \ *3*
192.168.1.74:2375 username:password *4*
-
1 在后台运行客户端容器并给它命名
-
2 公开一个端口以连接 Docker 守护进程,但仅限于来自本地机的连接
-
3 我们制作的允许与 Docker 进行认证连接的镜像
-
4 图像的两个参数:指定认证连接另一端的位置,以及用户名和密码(这两个都应该根据您的设置适当替换)
注意,localhost或127.0.0.1不适用于指定认证连接的另一端——如果您想在单个主机上尝试,您必须使用ip addr来识别您的机器的外部 IP 地址。
您现在可以使用以下命令使用认证连接:
docker -H localhost:12375 ps
请注意,由于某些实现限制,交互式 Docker 命令(带有-i参数的run和exec)无法通过此连接工作。
讨论
在这项技术中,我们向您展示了如何在受信任的网络中为您的 Docker 服务器设置基本认证。在下一项技术中,我们将探讨加密流量,这样窃听者就不能窥探您在做什么,甚至注入恶意数据或代码。
警告
这种技术为您提供了基本的认证,但它并不提供严重的安全性(特别是,能够监听您网络流量的人可以拦截您的用户名和密码)。设置使用 TLS 加密的服务器要复杂得多,将在下一技术中介绍。
| |
| |
保护您的 Docker API
在这项技术中,我们将展示如何通过 TCP 端口将您的 Docker 服务器向他人开放,同时确保只有受信任的客户端可以连接。这是通过创建一个只有受信任的主机才会获得的秘密密钥来实现的。只要这个受信任的密钥在服务器和客户端机器之间保持秘密,Docker 服务器应该保持安全。
问题
您希望您的 Docker API 通过端口安全地提供服务。
解决方案
创建一个自签名证书,并使用--tls-verify标志运行 Docker 守护进程。
这种安全方法依赖于在服务器上创建所谓的密钥文件。这些文件是通过特殊工具创建的,确保在没有服务器密钥的情况下难以复制。图 14.2 概述了这是如何工作的。
图 14.2. 关键设置和分发

小贴士
服务器密钥是一个文件,它包含一个只有服务器知道的秘密数字,并且需要读取使用服务器(所谓的客户端密钥)提供的秘密密钥文件加密的消息。一旦密钥被创建并分发,它们就可以用来确保客户端和服务器之间的连接安全。
设置 Docker 服务器证书
首先,您创建证书和密钥。
生成密钥需要 OpenSSL 包,您可以通过在终端中运行openssl来检查它是否已安装。如果没有安装,您需要先安装它,然后才能使用以下代码生成证书和密钥。
列表 14.3. 使用 OpenSSL 创建证书和密钥
$ sudo su *1*
$ read -s PASSWORD *2*
$ read SERVER *2*
$ mkdir -p /etc/docker *3*
$ cd /etc/docker *3*
$ openssl genrsa -aes256 -passout pass:$PASSWORD \
-out ca-key.pem 2048 *4*
$ openssl req -new -x509 -days 365 -key ca-key.pem -passin pass:$PASSWORD \
-sha256 -out ca.pem -subj "/C=NL/ST=./L=./O=./CN=$SERVER" *5*
$ openssl genrsa -out server-key.pem 2048 *6*
$ openssl req -subj "/CN=$SERVER" -new -key server-key.pem \
-out server.csr *7*
$ openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem
-passin "pass:$PASSWORD" -CAcreateserial \
-out server-cert.pem *8*
$ openssl genrsa -out key.pem 2048 *9*
$ openssl req -subj '/CN=client' -new -key key.pem\
-out client.csr *10*
$ sh -c 'echo "extendedKeyUsage = clientAuth" > extfile.cnf'
$ openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem \
-passin "pass:$PASSWORD" -CAcreateserial -out cert.pem \
-extfile extfile.cnf *11*
$ chmod 0400 ca-key.pem key.pem server-key.pem *12*
$ chmod 0444 ca.pem server-cert.pem cert.pem *13*
$ rm client.csr server.csr *14*
-
1 确保您是 root 用户。
-
2 输入您的证书密码和您将用于连接 Docker 服务器的服务器名。
-
3 如果不存在,则创建 docker 配置目录,并进入该目录。
-
4 使用 2048 位安全性生成证书颁发机构(CA) .pem 文件。
-
5 使用您的密码和地址为 CA 密钥签名,有效期为一年。
-
6 生成一个 2048 位安全性的服务器密钥。
-
7 使用您的主机名处理服务器密钥。
-
8 使用您的密码为密钥签名,有效期为一年。
-
9 生成一个 2048 位安全性的客户端密钥。
-
10 将密钥作为客户端密钥处理。
-
11 使用您的密码为密钥签名,有效期为一年。
-
12 将服务器文件的权限更改为 root 只读。
-
13 将客户端文件的权限更改为所有人只读。
-
14 删除遗留文件。
提示
可能有一个名为 CA.pl 的脚本已安装在您的系统上,它可以简化此过程。在这里,我们展示了原始的openssl命令,因为它们更具指导性。
设置 Docker 服务器
接下来,您需要在您的 Docker 守护进程配置文件中设置 Docker opts,以指定用于加密通信的密钥(有关如何配置和重启 Docker 守护进程的说明,请参阅附录 B)。
列表 14.4. 使用新密钥和证书的 Docker 选项
DOCKER_OPTS="$DOCKER_OPTS --tlsverify" *1*
DOCKER_OPTS="$DOCKER_OPTS \
--tlscacert=/etc/docker/ca.pem" *2*
DOCKER_OPTS="$DOCKER_OPTS \
--tlscert=/etc/docker/server-cert.pem" *3*
DOCKER_OPTS="$DOCKER_OPTS \
--tlskey=/etc/docker/server-key.pem" *4*
DOCKER_OPTS="$DOCKER_OPTS -H tcp://0.0.0.0:2376" *5*
DOCKER_OPTS="$DOCKER_OPTS \
-H unix:///var/run/docker.sock" *6*
-
1 告诉 Docker 守护进程您想使用 TLS 安全来保护与其的连接
-
2 指定 Docker 服务器的 CA 文件
-
3 指定服务器的证书
-
4 指定服务器使用的私钥
-
5 通过 TCP 端口 2376 将 Docker 守护进程对外部客户端开放。
-
6 以正常方式通过 Unix 套接字在本地打开 Docker 守护进程。
分发客户端密钥
接下来,您需要将密钥发送到客户端主机,以便它可以连接到服务器并交换信息。您不希望将您的秘密密钥透露给任何人,因此这些密钥需要安全地传递给客户端。一种相对安全的方法是从服务器直接使用 SCP(安全复制)将它们复制到客户端。SCP 实用程序使用与我们在下面展示的相同技术来确保数据传输的安全性,只是使用了已经设置好的不同密钥。
在客户端主机上,创建 Docker 配置文件夹在/etc中,就像您之前做的那样:
user@client:~$ sudo su
root@client:~$ mkdir -p /etc/docker
然后,从服务器将文件 SCP 复制到客户端。确保在以下命令中将“客户端”替换为您的客户端机器的主机名。还要确保所有文件都可以由将在客户端上运行docker命令的用户读取。
user@server:~$ sudo su
root@server:~$ scp /etc/docker/ca.pem client:/etc/docker
root@server:~$ scp /etc/docker/cert.pem client:/etc/docker
root@server:~$ scp /etc/docker/key.pem client:/etc/docker
测试
要测试您的设置,首先尝试在没有凭证的情况下向 Docker 服务器发出请求。您应该被拒绝:
root@client~: docker -H myserver.localdomain:2376 info
FATA[0000] Get http://myserver.localdomain:2376/v1.17/info: malformed HTTP >
response "\x15\x03\x01\x00\x02\x02". Are you trying to connect to a >
TLS-enabled daemon without TLS?
然后,使用凭证连接,应该返回有用的输出:
root@client~: docker --tlsverify --tlscacert=/etc/docker/ca.pem \
--tlscert=/etc/docker/cert.pem --tlskey=/etc/docker/key.pem \
-H myserver.localdomain:2376 info
243 info
Containers: 3
Images: 86
Storage Driver: aufs
Root Dir: /var/lib/docker/aufs
Backing Filesystem: extfs
Dirs: 92
Execution Driver: native-0.2
Kernel Version: 3.16.0-34-generic
Operating System: Ubuntu 14.04.2 LTS
CPUs: 4
Total Memory: 11.44 GiB
Name: rothko
ID: 4YQA:KK65:FXON:YVLT:BVVH:Y3KC:UATJ:I4GK:S3E2:UTA6:R43U:DX5T
WARNING: No swap limit support
讨论
这种技术为您提供了两全其美的解决方案——一个对其他人开放的 Docker 守护进程,以及一个仅对受信任用户可访问的守护进程。确保您保管好这些密钥!
密钥管理是大多数大型组织 IT 管理流程中的一个关键方面。这肯定是一个成本,因此在实施 Docker 平台时,它可能会变得非常突出。安全地将密钥部署到容器中是一个挑战,这在大多数 Docker 平台设计中可能需要考虑。
14.4. Docker 外部的安全性
在您的宿主机上,安全性并不随着docker命令的结束而停止。在本节中,您将看到一些其他方法来保护您的 Docker 容器,这次是从 Docker 外部进行。
我们将首先介绍一些技术,这些技术可以修改您的图像,以减少外部攻击的表面积,一旦它们启动并运行。接下来的两种技术考虑了以受限方式运行容器的方法。
在这些后两种技术中,第一种演示了应用平台即服务(aPaaS)方法,这确保 Docker 在管理员设置和控制下的紧身衣中运行。作为一个例子,我们将使用 Docker 命令运行一个 OpenShift Origin 服务器(一种以管理方式部署 Docker 容器的 aPaaS)。您将看到最终用户的权限可以被管理员限制和管理,并且可以移除对 Docker 运行时的访问。
第二种方法超越了这一级别的安全性,进一步限制运行容器内可用的自由度,使用 SELinux,这是一种提供细粒度控制谁可以做什么的安全技术。
提示
SELinux 是由美国国家安全局(NSA)构建并开源的一个工具,它满足了他们对强大访问控制的需求。它已经是一段时间以来的安全标准,并且非常强大。不幸的是,当许多人遇到问题时,他们只是简单地将其关闭,而不是花时间去理解它。我们希望这里展示的技术能帮助使这种方法不那么诱人。
使用 DockerSlim 减小容器的攻击面
在第 7.3 节中,我们讨论了几种创建小镜像的不同方法,以应对对在网络中移动的数据量的合理担忧。但还有另一个原因要做这件事——如果你的镜像内容更少,攻击者可以利用的东西也就更少。作为一个具体的例子,如果没有安装 shell,就无法在容器中获得 shell。
为你的容器建立一个“预期行为”配置文件,并在运行时强制执行,这意味着意外行为有被检测和阻止的合理机会。
问题
你希望将镜像减小到最基本,以减少其攻击面。
解决方案
使用 DockerSlim 工具分析你的镜像并修改它以减小攻击面。
这个工具旨在将 Docker 镜像减小到最基本。它可在github.com/docker-slim/docker-slim找到。
DockerSlim 至少以两种不同的方式减小你的 Docker 镜像。首先,它将你的镜像减小到仅包含所需文件,并将这些文件放置在单个层中。最终结果是,这个镜像比其原始的胖镜像小得多。
其次,它为你提供了一个 seccomp 配置文件。这是通过对你运行镜像的动态分析实现的。用通俗易懂的话说,这意味着它会运行你的镜像并跟踪使用哪些文件和系统调用。当 DockerSlim 分析你的运行容器时,你需要像所有典型用户一样使用该应用程序,以确保必要的文件和系统调用被捕获。
警告
如果你使用这种动态分析工具来减小你的镜像,请务必确保你在分析阶段已经充分测试。这个指南使用了一个简单的镜像,但你可能有一个更复杂的镜像,更难以完全分析。
这种技术将使用一个简单的 Web 示例应用程序来展示技术。你将
-
设置 DockerSlim
-
构建镜像
-
使用 DockerSlim 工具将镜像作为容器运行
-
打击应用程序的端点
-
使用创建的 seccomp 配置文件运行精简后的镜像
注意
系统调用配置文件(seccomp profile)本质上是一个白名单,列出了容器可以从其中调用的系统调用。当运行容器时,你可以根据应用程序的需求,指定具有降低或提升权限的 seccomp 配置文件。默认的 seccomp 配置文件禁用了 300 多个系统调用中的大约 45 个。大多数应用程序需要的系统调用远少于这个数量。
设置 DockerSlim
运行以下命令以下载并设置 docker-slim 二进制文件。
列表 14.5. 下载 docker-slim 并将其安装到目录中
$ mkdir -p docker-slim/bin && cd docker-slim/bin *1*
$ wget https://github.com/docker-slim/docker-slim/releases/download/1.18
/dist_linux.zip *2*
$ unzip dist_linux.zip *3*
$ cd .. *4*
-
1 创建 docker-slim 文件夹和 bin 子文件夹
-
2 从其发布文件夹获取 docker-slim zip 文件
-
3 解压获取的 zip 文件
-
4 移动到父目录,docker-slim
注意
这种技术已在先前的 docker-slim 版本上进行了测试。你可能想访问 GitHub 上的github.com/docker-slim/docker-slim/ releases,看看是否有任何更新。这不是一个快速发展的项目,所以更新可能不会太重要。
现在你已经将 docker-slim 二进制文件放在了一个 bin 子文件夹中。
构建胖镜像
接下来,你将构建一个使用 NodeJS 的示例应用程序。这是一个简单的应用程序,它简单地在一个端口 8000 上提供 JSON 字符串。以下命令克隆了 docker-slim 仓库,移动到示例应用程序代码,并将其 Dockerfile 构建成一个名为 sample-node-app 的镜像。
列表 14.6. 构建示例 docker-slim 应用程序
$ git clone https://github.com/docker-slim/docker-slim.git *1*
$ cd docker-slim && git checkout 1.18 *2*
$ cd sample/apps/node *3*
$ docker build -t sample-node-app . *4*
$ cd - *5*
-
1 克隆 docker-slim 仓库,其中包含示例应用程序
-
2 检出 docker-slim 仓库的一个已知工作版本
-
3 移动到 NodeJS 示例应用程序文件夹
-
4 构建镜像,将其命名为 sample-node-app
-
5 返回到包含 docker-slim 二进制文件的上一目录
运行胖镜像
现在你已经创建了胖镜像,下一步是将它作为带有 docker-slim 包装器的容器运行。一旦应用程序初始化,你就可以通过访问应用程序端点来测试其代码。最后,将后台的 docker-slim 应用程序带到前台并等待其终止。
$ ./docker-slim build --http-probe sample-node-app & *1*
$ sleep 10 && curl localhost:32770 *2*
{"status":"success","info":"yes!!!","service":"node"} *3*
$ fg *4*
./docker-slim build --http-probe sample-node-app *5*
INFO[0014] docker-slim: HTTP probe started... *5*
INFO[0014] docker-slim: http probe - GET http://127.0.0.1:32770/ => 200 *5*
INFO[0014] docker-slim: HTTP probe done. *5*
INFO[0015] docker-slim: shutting down 'fat' container... *5*
INFO[0015] docker-slim: processing instrumented 'fat' container info... *5*
INFO[0015] docker-slim: generating AppArmor profile... *5*
INFO[0015] docker-slim: building 'slim' image... *5*
Step 1 : FROM scratch *6*
---> *6*
Step 2 : COPY files / *6*
---> 0953a87c8e4f *6*
Removing intermediate container 51e4e625017e *6*
Step 3 : WORKDIR /opt/my/service *6*
---> Running in a2851dce6df7 *6*
---> 2d82f368c130 *6*
Removing intermediate container a2851dce6df7 *6*
Step 4 : ENV PATH "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:*6*
/bin"
*6*
---> Running in ae1d211f118e *6*
---> 4ef6d57d3230 *6*
Removing intermediate container ae1d211f118e *6*
Step 5 : EXPOSE 8000/tcp *6*
---> Running in 36e2ced2a1b6 *6*
---> 2616067ec78d *6*
Removing intermediate container 36e2ced2a1b6 *6*
Step 6 : ENTRYPOINT node /opt/my/service/server.js *6*
---> Running in 16a35fd2fb1c *6*
---> 7451554aa807 *6*
Removing intermediate container 16a35fd2fb1c *6*
Successfully built 7451554aa807 *6*
INFO[0016] docker-slim: created new image: sample-node-app.slim *6*
$ *7*
$
-
1 运行 docker-slim 二进制文件针对 sample-node-app 镜像。将进程置于后台。http-probe 将在所有公开的端口上调用应用程序
-
2 暂停 10 秒以允许 sample-node-app 进程启动,然后点击应用程序运行的端口
-
3 将应用程序的 JSON 响应发送到终端
-
4 将 docker-slim 进程置于前台并等待其完成
-
5 docker-slim 的输出第一部分显示了其工作日志。
-
6 Docker-slim 构建“瘦”容器。
-
7 完成时,你可能需要按 Return 键以获取提示。
在这种情况下,“锻炼代码”只是涉及点击一个 URL 并获取响应。更复杂的应用程序将需要更多样化和多样化的探测和检查,以确保它们已被完全锻炼。
注意,根据文档,我们不需要自己击打端口 32770 的应用程序,因为我们使用了 http-probe 参数。如果您启用 HTTP 探针,它将默认在所有暴露的端口上运行 HTTP 和 HTTPS GET 请求到根 URL(“/”)。我们手动进行 curl 操作只是为了演示目的。
到目前为止,您已经创建了 sample-node-app.slim 版本的镜像。如果您检查 docker images 的输出,您会看到其大小已经大幅减少。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
sample-node-app.slim latest 7451554aa807 About an hour ago 14.02 MB *1*
sample-node-app latest 78776db92c2a About an hour ago 418.5 MB *2*
-
1 sample-node-app.slim 镜像的大小仅为 14 MB 左右。
-
2 原始的 sample-node-app 镜像大小超过 400 MB。
如果您比较胖样本应用程序的 docker history 输出与其精简版本,您会发现它们的结构相当不同。
$ docker history sample-node-app *1*
IMAGE CREATED CREATED BY SIZE *2*
78776db92c2a 42 hours ago /bin/sh -c #(nop) ENTRYPOINT ["node" 0 B *2*
0f044b6540cd 42 hours ago /bin/sh -c #(nop) EXPOSE 8000/tcp 0 B *2*
555cf79f13e8 42 hours ago /bin/sh -c npm install 14.71 MB *2*
6c62e6b40d47 42 hours ago /bin/sh -c #(nop) WORKDIR /opt/my/ser 0 B *2*
7871fb6df03b 42 hours ago /bin/sh -c #(nop) COPY dir:298f558c6f2 656 B *2*
618020744734 42 hours ago /bin/sh -c apt-get update && apt-get 215.8 MB *2*
dea1945146b9 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B *2*
<missing> 7 weeks ago /bin/sh -c mkdir -p /run/systemd && ec 7 B *2*
<missing> 7 weeks ago /bin/sh -c sed -i 's/^#\s*\(deb.*unive 2.753 kB *2*
<missing> 7 weeks ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0 B *2*
<missing> 7 weeks ago /bin/sh -c set -xe && echo '#!/bin/s 194.6 kB *2*
<missing> 7 weeks ago /bin/sh -c #(nop) ADD file:8f997234193 187.8 MB *2*
$ docker history sample-node-app.slim *3*
IMAGE CREATED CREATED BY SIZE *4*
7451554aa807 42 hours ago /bin/sh -c #(nop) ENTRYPOINT ["node" 0 B *4*
2616067ec78d 42 hours ago /bin/sh -c #(nop) EXPOSE 8000/tcp 0 B *4*
4ef6d57d3230 42 hours ago /bin/sh -c #(nop) ENV PATH=/usr/local 0 B *4*
2d82f368c130 42 hours ago /bin/sh -c #(nop) WORKDIR /opt/my/ser 0 B *4*
0953a87c8e4f 42 hours ago /bin/sh -c #(nop) COPY dir:36323da1e97 14.02 MB *4*
-
1 在 sample-node-app 镜像上运行 docker history 命令。
-
2 此镜像的历史记录显示了创建时每个命令的情况。
-
3 在 sample-node-app.slim 镜像上运行 docker history 命令。
-
4 slim 容器的历史记录包含更少的命令,包括原始胖镜像中不存在的 COPY 命令。
上述输出提供了 DockerSlim 所做部分工作的线索。它通过获取最终的文件系统状态,并将该目录作为镜像的最后一层来复制,成功将镜像大小减少到(实际上)单个 14 MB 层。
DockerSlim 生成的其他工件与其最初描述的第二目的相关。它生成一个 seccomp.json 文件(在本例中为 sample-node-app-seccomp.json),该文件可用于限制运行容器的操作。
让我们来看看这个文件的详细内容(此处已编辑,因为它相当长)。
列表 14.7. 一个 seccomp 配置文件
$ SECCOMPFILE=$(ls $(pwd)/.images/*/artifacts/sample-node-app-seccomp.json) *1*
$ cat ${SECCOMPFILE} *2*
{
"defaultAction": "SCMP_ACT_ERRNO", *3*
"architectures": [ *4*
"SCMP_ARCH_X86_64" *4*
], *4*
"syscalls": [ *5*
{ *5*
"name": "capset", *5*
"action": "SCMP_ACT_ALLOW" *5*
}, *5*
{ *5*
"name": "rt_sigaction", *5*
"action": "SCMP_ACT_ALLOW" *5*
}, *5*
{ *5*
"name": "write", *5*
"action": "SCMP_ACT_ALLOW" *5*
}, *5*
[...] *5*
{ *5*
"name": "execve", *5*
"action": "SCMP_ACT_ALLOW" *5*
}, *5*
{ *5*
"name": "getcwd", *5*
"action": "SCMP_ACT_ALLOW" *5*
} *5*
]
}
-
1 将 seccomp 文件的位置捕获到变量 SECCOMPFILE 中
-
2 将此文件内容输出到查看
-
3 指定尝试调用任何禁止的系统调用的进程的退出代码
-
4 指定此配置文件应应用其上的硬件架构
-
5 在此处通过指定 SCMP_ACT_ALLOW 动作对它们进行白名单管理,以控制受控的系统调用。
最后,您将再次运行带有 seccomp 配置文件的 slim 镜像,并检查它是否按预期工作:
$ docker run -p32770:8000 -d \
--security-opt seccomp=/root/docker-slim-bin/.images/${IMAGEID}/artifacts
/sample-node-app-seccomp.json sample-node-app.slim *1*
4107409b61a03c3422e07973248e564f11c6dc248a6a5753a1db8b4c2902df55 *2*
$ sleep 10 && curl localhost:3277l *3*
{"status":"success","info":"yes!!!","service":"node"} *4*
-
1 以守护进程运行 slim 镜像,暴露 DockerSlim 在分析阶段暴露的相同端口,并应用 seccomp 配置文件
-
2 将容器 ID 输出到终端
-
3 重新运行 curl 命令以确认应用程序仍然像以前一样工作
-
4 输出与您精简的胖镜像相同。
讨论
这个简单的例子展示了如何不仅减小镜像的大小,还可以缩小其可以执行的操作范围。这是通过删除非必要文件(也在技术 59 中讨论过)以及将其可用的系统调用减少到仅运行应用程序所需的那些来实现的。
在这里“锻炼”应用程序的方法很简单(一个对默认端点的 curl 请求)。对于真实的应用程序,你可以采取多种方法来确保你已经覆盖了所有可能性。一种方法是对已知的端点开发一系列测试,另一种方法是使用“模糊器”以自动化的方式向应用程序抛出大量输入(这是找到你的软件中的错误和安全漏洞的一种方法)。最简单的方法是让应用程序运行更长的时间,期望所有需要的文件和系统调用都会被引用。
许多企业级 Docker 安全工具基于这个原则工作,但以更自动化的方式进行。通常,它们允许应用程序运行一段时间,并跟踪哪些系统调用被使用,哪些文件被访问,以及(可能)使用了哪些操作系统功能。基于这些信息——以及可配置的学习期——它们可以确定应用程序的预期行为,并报告任何看似异常的行为。例如,如果攻击者获得了正在运行的容器的访问权限并启动了 bash 二进制文件或打开了意外的端口,这可能会在系统中引发警报。DockerSlim 允许您从一开始就控制这个过程,即使攻击者获得了访问权限,也能减少他们可能能够执行的操作。
另一种考虑缩小应用程序攻击面的方法是限制其功能。这将在技术 93 中介绍。
| |
删除构建过程中添加的秘密
当你在企业环境中构建镜像时,通常需要使用密钥和凭证来检索数据。如果你使用 Dockerfile 构建应用程序,即使在使用后删除,这些秘密通常也会出现在历史记录中。
这可能是一个安全问题:如果有人获得了镜像,他们也可能获得早期层中的秘密。
问题
你想从镜像的历史记录中删除一个文件。
解决方案
使用 docker-squash 从镜像中移除层。
有一些简单的方法可以解决这个问题,从理论上来说是可行的。例如,你可以在使用秘密时将其删除,如下所示。
列表 14.8. 在层内不留下秘密的粗略方法
FROM ubuntu
RUN echo mysecret > secretfile && command_using_secret && rm secretfile
这种方法存在一些缺点。它需要将秘密放入 Dockerfile 中的代码,因此它可能以纯文本的形式存储在你的源代码控制中。
为了避免这个问题,您可能需要在源控制中的 .gitignore(或类似)文件中添加该文件,并在构建镜像时将其 ADD 到镜像中。这将在一个单独的层中添加文件,无法轻易从生成的镜像中移除。
最后,您可以使用环境变量来存储秘密,但这也会带来安全风险,因为这些变量很容易在非安全的持久存储中设置,例如 Jenkins 作业。在任何情况下,您可能会收到一个用户提供的镜像,并要求您从其中清除秘密。首先,我们将通过一个简单的示例演示这个问题,然后我们将向您展示一种从基础层中删除秘密的方法。
带有秘密的图像
以下 Dockerfile 将创建一个镜像,使用名为 secret_file 的文件作为您放入镜像中的某些秘密数据的占位符。
列表 14.9. 带有秘密的简单 Dockerfile
FROM ubuntu
CMD ls / *1*
ADD /secret_file secret_file *2*
RUN cat /secret_file *3*
RUN rm /secret_file *4*
-
1 为了节省一点时间,我们用文件列表命令覆盖了默认命令。这将演示文件是否在历史记录中。
-
2 将秘密文件添加到镜像构建中(此文件必须存在于您的当前工作目录中,与 Dockerfile 一起)
-
3 将秘密文件作为构建的一部分使用。在这种情况下,我们使用简单的 cat 命令输出文件,但这可以是 git clone 或其他更有用的命令。
-
4 删除秘密文件
现在,您可以构建这个镜像,将生成的镜像命名为 secret_build。
列表 14.10. 构建带有秘密的简单 Docker 镜像
$ echo mysecret > secret_file
$ docker build -t secret_build .
Sending build context to Docker daemon 5.12 kB
Sending build context to Docker daemon
Step 0 : FROM ubuntu
---> 08881219da4a
Step 1 : CMD ls /
---> Running in 7864e2311699
---> 5b39a3cba0b0
Removing intermediate container 7864e2311699
Step 2 : ADD /secret_file secret_file
---> a00886ff1240
Removing intermediate container 4f279a2af398
Step 3 : RUN cat /secret_file
---> Running in 601fdf2659dd
My secret
---> 2a4238c53408
Removing intermediate container 601fdf2659dd
Step 4 : RUN rm /secret_file
---> Running in 240a4e57153b
---> b8a62a826ddf
Removing intermediate container 240a4e57153b
Successfully built b8a62a826ddf
镜像构建完成后,您可以通过使用技术 27 来演示它包含秘密文件。
列表 14.11. 为每个步骤添加标签并展示带有秘密的层
$ x=0; for id in $(docker history -q secret_build:latest);
do ((x++)); docker tag $id secret_build:step_$x; done *1*
$ docker run secret_build:step_3 cat /secret_file' *2*
mysecret
-
1 演示秘密文件存在于这个镜像标签中]
-
2 按数字顺序标记构建的每个步骤
压缩镜像以删除秘密
您已经看到,即使秘密不在最终的镜像中,秘密也可以保留在镜像的历史记录中。这就是 docker-squash 发挥作用的地方——它移除了中间层,但保留了 Dockerfile 命令(如 CMD、PORT、ENV 等)以及您历史记录中的原始基础层。
以下列表下载、安装并使用 docker-squash 来比较压缩前后的镜像。
列表 14.12. 使用 docker_squash 减少镜像的层
$ wget -qO- https://github.com/jwilder/docker-squash/releases/download
/v0.2.0/docker-squash-linux-amd64-v0.2.0.tar.gz | \
tar -zxvf - && mv docker-squash /usr/local/bin *1*
$ docker save secret_build:latest | \ *2*
docker-squash -t secret_build_squashed | \ *2*
docker load *2*
$ docker history secret_build_squashed *3*
IMAGE CREATED CREATED BY SIZE
ee41518cca25 2 seconds ago /bin/sh -c #(nop) CMD ["/bin/sh" " 0 B
b1c283b3b20a 2 seconds ago /bin/sh -c #(nop) CMD ["/bin/bash 0 B
f443d173e026 2 seconds ago /bin/sh -c #(squash) from 93c22f56 2.647 kB
93c22f563196 2 weeks ago /bin/sh -c #(nop) ADD file:7529d28 128.9 MB
$ docker history secret_build *4*
IMAGE CREATED CREATED BY SIZE
b8a62a826ddf 3 seconds ago /bin/sh -c rm /secret_file 0 B
2a4238c53408 3 seconds ago /bin/sh -c cat /secret_file 0 B
a00886ff1240 9 seconds ago /bin/sh -c #(nop) ADD file:69e77f6 10 B
5b39a3cba0b0 9 seconds ago /bin/sh -c #(nop) CMD ["/bin/sh" " 0 B
08881219da4a 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash 0 B
6a4ec4bddc58 2 weeks ago /bin/sh -c mkdir -p /run/systemd & 7 B
98697477f76a 2 weeks ago /bin/sh -c sed -i 's/^#\s*\(deb.*u 1.895 kB
495ec797e6ba 2 weeks ago /bin/sh -c rm -rf /var/lib/apt/lis 0 B
e3aa81f716f6 2 weeks ago /bin/sh -c set -xe && echo '#!/bin 745 B
93c22f563196 2 weeks ago /bin/sh -c #(nop) ADD file:7529d28 128.9 MB
$ docker run secret_build_squashed ls /secret_file *5*
ls: cannot access '/secret_file': No such file or directory
$ docker run f443d173e026 ls /secret_file *6*
ls: cannot access '/secret_file': No such file or directory
-
1 安装 docker-squash. (您可能需要参考
github.com/jwilder/docker-squash以获取最新的安装说明。) -
2 将镜像保存到 TAR 文件中,然后加载结果镜像,将其标记为“secret_build_squashed”
-
3 压缩后的镜像的历史记录中没有 secret_file 的记录。
-
4 原始镜像中仍然包含 secret_file。
-
5 演示 secret_file 不在压缩后的镜像中
-
6 演示了 secret_file 不在压缩镜像的“压缩”层中
关于“缺失”镜像层的说明
Docker 在 1.10 版本中改变了层的基本性质。从那时起,下载的镜像在历史中显示为“
你仍然可以通过 docker save 镜像并从该 TAR 文件中提取 TAR 文件来获取你已下载的层的内容。以下是一个示例会话,它为已下载的 Ubuntu 镜像执行了此操作。
列表 14.13. “缺失”的层在下载的镜像中
$ docker history ubuntu *1*
IMAGE CREATED CREATED BY SIZE
104bec311bcd 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
<missing> 2 weeks ago /bin/sh -c mkdir -p /run/systemd && ech 7 B
<missing> 2 weeks ago /bin/sh -c sed -i 's/^#\s*\(deb.*univer 1.9 kB
<missing> 2 weeks ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0 B
<missing> 2 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh 745 B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:7529d28035b4 129 MB
$ docker save ubuntu | tar -xf - *2*
$ find . | grep tar$ *3*
./042e55060780206b2ceabe277a8beb9b10f48262a876fd21b495af318f2f2352/layer.tar
./1037e0a8442d212d5cc63d1bc706e0e82da0eaafd62a2033959cfc629f874b28/layer.tar
./25f649b30070b739bc2aa3dd877986bee4de30e43d6260b8872836cdf549fcfc/layer.tar
./3094e87864d918dfdb2502e3f5dc61ae40974cd957d5759b80f6df37e0e467e4/layer.tar
./41b8111724ab7cb6246c929857b0983a016f11346dcb25a551a778ef0cd8af20/layer.tar
./4c3b7294fe004590676fa2c27a9a952def0b71553cab4305aeed4d06c3b308ea/layer.tar
./5d1be8e6ec27a897e8b732c40911dcc799b6c043a8437149ab021ff713e1044f/layer.tar
./a594214bea5ead6d6774f7a09dbd7410d652f39cc4eba5c8571d5de3bcbe0057/layer.tar
./b18fcc335f7aeefd87c9d43db2888bf6ea0ac12645b7d2c33300744c770bcec7/layer.tar
./d899797a09bfcc6cb8e8a427bb358af546e7c2b18bf8e2f7b743ec36837b42f2/layer.tar
./ubuntu.tar
$ tar -tvf
./4c3b7294fe004590676fa2c27a9a952def0b71553cab4305aeed4d06c3b308ea
/layer.tar
drwxr-xr-x 0 0 0 0 15 Dec 17:45 etc/
drwxr-xr-x 0 0 0 0 15 Dec 17:45 etc/apt/
-rw-r--r-- 0 0 0 1895 15 Dec 17:45 etc/apt/sources.list
-
1 使用 docker history 命令显示 Ubuntu 镜像的层历史
-
2 使用 docker save 命令输出镜像层的 TAR 文件,该文件直接通过管道传输到 tar 并提取
-
3 演示了 TAR 文件只包含该层内的文件更改
讨论
虽然在意图上与 技术 52 有一定相似性,但使用专用工具在最终结果上存在一些显著差异。在前面的解决方案中,你可以看到像 CMD 这样的元数据层已经被保留,而之前关于这个主题的技术会完全丢弃它们,因此你需要通过另一个 Dockerfile 手动重新创建这些元数据层。
这种行为意味着 docker-squash 工具可以在镜像到达注册表时自动清理镜像,如果你不信任用户在镜像构建中使用秘密数据——它们都应该正常工作。
话虽如此,你应该警惕你的用户将秘密信息放入任何元数据层中——特别是环境变量是一个威胁,并且可能会在最终镜像中被保留。
| |
OpenShift:一个应用程序平台即服务
OpenShift 是由 Red Hat 管理的一个产品,它允许组织以服务的形式运行应用程序平台(aPaas)。它为应用程序开发团队提供了一个平台,可以在其中运行代码,而无需关心硬件细节。该产品的第 3 版是在 Go 语言中从头开始重写的,使用 Docker 作为容器技术,Kubernetes 和 etcd 进行编排。在此基础上,Red Hat 还添加了企业功能,使其更容易在企业和安全重点环境中部署。
尽管 OpenShift 有许多我们可以讨论的功能,但在这里我们将使用它作为管理安全性的手段,通过剥夺用户直接运行 Docker 的能力,但保留使用 Docker 的好处。
OpenShift 既可以作为企业支持的产品提供,也可以作为一个名为 Origin 的开源项目提供,该项目由 github.com/openshift/origin 维护。
问题
你希望管理不受信任的用户调用 docker run 的安全风险。
解决方案
使用 aPaaS 工具通过代理接口管理和调解与 Docker 的交互。
aPaaS 有许多好处,但在这里我们将关注其管理用户权限并在用户代表下运行 Docker 容器的能力,为运行 Docker 容器的用户提供一个安全的审计点。
这为什么很重要?使用这个 aPaaS 的用户没有直接访问 docker 命令的权限,因此他们不能在不绕过 OpenShift 提供的安全性的情况下造成任何损害。例如,容器默认由非 root 用户部署,克服这一点需要管理员授予的权限。如果你不信任你的用户,使用 aPaaS 是一种有效的方式,让他们能够访问 Docker。
提示
aPaaS 为用户提供按需启动应用以进行开发、测试或生产的能力。Docker 是这些服务的自然选择,因为它提供了一种可靠且隔离的应用交付格式,允许运维团队处理部署的细节。
简而言之,OpenShift 基于 Kubernetes(见技术 88),但增加了功能,以提供完整的 aPaaS。这些附加功能包括
-
用户管理
-
权限管理
-
配额
-
安全上下文
-
路由
安装 OpenShift
OpenShift 安装的完整概述超出了本书的范围。如果您想使用我们维护的 Vagrant 进行自动化安装,请参阅github.com/docker-in-practice/shutit-openshift-origin。如果您需要安装 Vagrant 的帮助,请参阅附录 C。
其他选项,如仅 Docker 安装(单节点)或完整的手动构建,都是可用的,并在 OpenShift Origin 代码库中有文档说明github.com/openshift/origin.git。
提示
OpenShift Origin 是 OpenShift 的上游版本。上游意味着它是 Red Hat 为 OpenShift 取得变更的代码库,它是 Red Hat 的支持产品。Origin 是开源的,任何人都可以使用和贡献,但 Red Hat 精选的版本作为 OpenShift 出售和支持。上游版本通常更前沿但更不稳定。
一个 OpenShift 应用
在这个技术中,我们将通过 OpenShift 网页界面展示一个创建、构建、运行和访问应用的简单示例。该应用将是一个基本的 NodeJS 应用,它提供了一个简单的网页。
应用将使用 Docker、Kubernetes 和 S2I。Docker 用于封装构建和部署环境。源到镜像(S2I)构建方法是由 Red Hat 在 OpenShift 中用于构建 Docker 容器的一种技术,而 Kubernetes 用于在 OpenShift 集群上运行应用。
登录
要开始,从 shutit-openshift-origin 文件夹运行 ./run.sh,然后导航到 https://localhost:8443,绕过所有安全警告。你会看到如图 图 14.3 所示的登录页面。注意,如果你使用 Vagrant 安装,你需要在你的虚拟机中启动一个网络浏览器。(有关在虚拟机中获得 GUI 的帮助,请参阅 附录 C)。
图 14.3. OpenShift 登录页面

使用任何密码登录为 hal-1。
构建 NodeJS 应用
你现在已以开发者的身份登录到 OpenShift(见图 图 14.4)。
图 14.4. OpenShift 项目页面

通过点击创建来创建一个项目。填写表格,如图 图 14.5 所示。然后再次点击创建。
图 14.5. OpenShift 项目创建页面

一旦项目设置完成,再次点击创建,并输入建议的 GitHub 仓库 (github.com/openshift/nodejs-ex),如图 图 14.6 所示。
图 14.6. OpenShift 项目源页面

点击下一步,你将看到一系列构建器镜像的选择,如图 图 14.7 所示。构建镜像定义了代码将构建的上下文。选择 NodeJS 构建器镜像。
图 14.7. OpenShift 构建器镜像选择页面

现在填写表格,如图 图 14.8 所示。在滚动表格时,在页面底部点击 NodeJS 的创建。
图 14.8. OpenShift NodeJS 模板表单

几分钟后,你应该会看到一个类似于 图 14.9 的屏幕。
图 14.9. OpenShift 构建开始页面

几分钟后,如果你向下滚动,你会看到构建已经开始,如图 图 14.10 所示。
图 14.10. OpenShift 构建信息窗口

小贴士
在 OpenShift 的早期版本中,构建有时不会自动开始。如果这种情况发生,几分钟后点击开始构建按钮。
一段时间后,你会看到应用正在运行,如图 图 14.11 所示。
图 14.11. 应用运行页面

通过点击浏览和 Pods,你可以看到 pod 已经部署,如 图 14.12 所示。
图 14.12. OpenShift pods 列表

小贴士
查看 技术 88 了解 pod 的解释。
如何访问你的 pod?如果你查看服务标签(见图 图 14.13),你会看到一个 IP 地址和端口号来访问。
图 14.13. OpenShift NodeJS 应用服务详情

将你的浏览器指向该地址,哇,你将拥有你的 NodeJS 应用,如图 图 14.14 所示。
图 14.14. NodeJS 应用程序登录页面

讨论
让我们回顾一下我们在这里所取得的成果,以及为什么这对安全性很重要。
从用户的角度来看,他们登录了一个 Web 应用程序,并使用基于 Docker 的技术部署了一个应用程序,而没有接近 Dockerfile 或 docker run 命令。
OpenShift 的管理员可以
-
控制用户访问
-
按项目限制资源使用
-
集中提供资源
-
默认确保代码以非特权状态运行
这比直接给用户 docker run 访问权限要安全得多。
如果您想在此基础上构建应用程序并了解一个 aPaaS 如何促进迭代方法,您可以分叉 Git 仓库,更改该分叉仓库中的代码,然后创建一个新的应用程序。我们在这里就是这样做的:github.com/docker-in-practice/nodejs-ex。
要了解更多关于 OpenShift 的信息,请访问 www.openshift.org。
| |
使用安全选项
您已经在前面的技术中看到,默认情况下,您在 Docker 容器中拥有 root 权限,并且这个用户与宿主机的 root 用户相同。为了减轻这一点,我们向您展示了如何降低此用户的 root 能力,即使它逃出了容器,内核也不会允许此用户执行某些操作。
但您可以更进一步。通过使用 Docker 的安全选项标志,您可以保护宿主机的资源免受容器内执行的操作的影响。这限制了容器只能影响宿主机授予其权限的资源。
问题
您希望保护您的宿主免受容器操作的影响。
解决方案
使用 SELinux 对您的容器施加约束。
在这里,我们将使用 SELinux 作为我们的内核支持的强制访问控制(MAC)工具。SELinux 大约是行业标准,最有可能被特别关注安全的组织使用。它最初由 NSA 开发,用于保护他们的系统,后来开源。它在基于 Red Hat 的系统中作为标准使用。
SELinux 是一个很大的主题,所以我们不能在这本书中深入探讨。我们将向您展示如何编写和执行一个简单的策略,以便您可以了解它是如何工作的。如果您需要,您可以进一步探索并进行实验。
提示
Linux 中的强制访问控制(MAC)工具强制执行超出您可能习惯的标准安全规则。简而言之,它们不仅确保了文件和进程的读写执行等常规规则得到执行,而且还可以在内核级别应用更细粒度的规则。例如,MySQL 进程可能仅被允许在特定目录下写入文件,例如 /var/lib/mysql。基于 Debian 的系统的等效标准是 AppArmor。
这种技术假设你有一个启用了 SELinux 的主机。这意味着你必须首先安装 SELinux(假设它尚未安装)。如果你正在运行 Fedora 或其他基于 Red Hat 的系统,你很可能已经安装了它。
要确定你是否启用了 SELinux,运行命令 sestatus:
# sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: permissive
Mode from config file: permissive
Policy MLS status: enabled
Policy deny_unknown status: allowed
Max kernel policy version: 28
输出的第一行将告诉你 SELinux 是否已启用。如果该命令不可用,则表示你的主机上未安装 SELinux。
你还需要有相关的 SELinux 政策创建工具可用。例如,在支持 yum 的机器上,你需要运行 yum -y install selinux-policy -devel。
Vagrant 机器上的 SELinux
如果你没有 SELinux 并且希望它为你构建,你可以使用 ShutIt 脚本在你的主机机器内部构建一个带有 Docker 和 SELinux 预安装的虚拟机。它所做的工作在图 14.15 中进行了高层次解释。
图 14.15. 配置 SELinux 虚拟机的脚本

提示
ShutIt 是一个通用的 shell 自动化工具,我们创建它是为了克服 Dockerfile 的一些限制。如果你想了解更多关于它的信息,请参阅 GitHub 页面:ianmiell.github.io/shutit。
图 14.5 识别了设置策略所需的步骤。该脚本将执行以下操作:
-
设置 VirtualBox
-
启动合适的 Vagrant 镜像
-
登录到虚拟机
-
确保 SELinux 的状态正确
-
安装 Docker 的最新版本
-
安装 SELinux 政策开发工具
-
给你一个 shell
这里是设置和运行它的命令(已在 Debian 和基于 Red Hat 的发行版上测试):
列表 14.14. 安装 ShutIt
sudo su - *1*
apt-get install -y git python-pip docker.io || \
yum install -y git python-pip docker.io *2*
pip install shutit *3*
git clone https://github.com/ianmiell/docker-selinux.git *4*
cd docker-selinux *4*
shutit build --delivery bash \ *5*
-s io.dockerinpractice.docker_selinux.docker_selinux \
compile_policy no *6*
-
1 确保在开始运行之前你是 root 用户
-
2 确保主机上安装了所需的软件包
-
3 安装 ShutIt
-
4 克隆 SELinux ShutIt 脚本并进入其目录
-
5 运行 ShutIt 脚本。“--delivery bash”表示命令在 bash 中执行,而不是通过 SSH 或 Docker 容器执行。
-
6 配置脚本不编译 SELinux 政策,因为我们将会手动完成
运行此脚本后,你应该最终看到如下输出:
Pause point:
Have a shell:
You can now type in commands and alter the state of the target.
Hit return to see the prompt
Hit CTRL and ] at the same time to continue with build
Hit CTRL and u to save the state
现在,你有一个运行在带有 SELinux 的虚拟机内部的 shell。如果你输入 sestatus,你会看到 SELinux 以许可模式启用(如列表 14.14 所示)。要返回到主机的 shell,请按 Ctrl-]。
编译 SELinux 政策
无论你是否使用了 ShutIt 脚本,我们假设你现在有一个启用了 SELinux 的主机。输入 sestatus 以获取状态摘要。
列表 14.15. 安装并启用后 SELinux 的状态
# sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: permissive
Mode from config file: permissive
Policy MLS status: enabled
Policy deny_unknown status: allowed
Max kernel policy version: 28
在这种情况下,我们处于允许模式,这意味着 SELinux 正在记录日志中的安全违规,但不会强制执行它们。这对于安全测试新策略而不会使系统不可用是有益的。要将 SELinux 状态更改为允许模式,请以 root 身份输入setenforce Permissive。如果您由于安全原因无法在主机上执行此操作,请不要担心;有一个选项可以将策略设置为允许模式,如列表 14.15 中概述。
注意
如果您在主机上自行安装 SELinux 和 Docker,请确保 Docker 守护进程已将--selinux-enabled设置为标志。您可以使用ps -ef | grep 'docker -d.*--selinux-enabled'来检查此设置,它应该在输出中返回一个匹配的进程。
为您的策略创建一个文件夹并进入它。然后以 root 身份创建一个策略文件,内容如下,命名为 docker_apache.te。此策略文件包含我们将尝试应用的策略。
列表 14.16. 创建 SELinux 策略
mkdir -p /root/httpd_selinux_policy && >
cd /root/httpd_selinux_policy *1*
cat > docker_apache.te << END *2*
policy_module(docker_apache,1.0) *3*
virt_sandbox_domain_template(docker_apache) *4*
allow docker_apache_t self: capability { chown dac_override kill setgid >
setuid net_bind_service sys_chroot sys_nice >
sys_tty_config } ; *5*
allow docker_apache_t self:tcp_socket >
create_stream_socket_perms; *6*
allow docker_apache_t self:udp_socket > *6*
create_socket_perms; *6*
corenet_tcp_bind_all_nodes(docker_apache_t) *6*
corenet_tcp_bind_http_port(docker_apache_t) *6*
corenet_udp_bind_all_nodes(docker_apache_t) *6*
corenet_udp_bind_http_port(docker_apache_t) *6*
sysnet_dns_name_resolve(docker_apache_t) *7*
#permissive docker_apache_t *8*
END *9*
-
1 创建一个文件夹来存储策略文件,并进入该文件夹
-
2 创建将作为“here”文档编译的策略文件
-
3 使用 policy_module 指令创建名为 docker_apache 的 SELinux 策略模块
-
4 使用提供的模板创建 docker_apache_t SELinux 类型,该类型可以作为 Docker 容器运行。此模板为 docker_apache SELinux 域提供了运行所需的最少权限。我们将添加这些权限以创建一个有用的容器环境。
-
5 Apache 网络服务器需要这些功能才能运行;使用 allow 指令在此处添加它们。
-
6 这些 allow 和 corenet 规则允许容器在网络中监听 Apache 端口。
-
7 使用 sysnet 指令允许 DNS 服务器解析
-
8 可选地使 docker_apache_t 类型为允许模式,即使主机正在执行 SELinux 策略,此策略也不会被强制执行。如果你无法设置主机的 SELinux 模式,请使用此选项。
-
9 终止“here”文档,将其写入磁盘
提示
关于前面的权限的更多信息,以及探索其他权限,您可以安装 selinux-policy-doc 包,并使用浏览器浏览位于 file:///usr/share/doc-base/selinux-policy-doc/html/index.html 的文档。文档也在线上可用,网址为oss.tresys.com/docs/refpolicy/api/。
现在您将编译此策略,并看到您的应用程序在强制模式下无法启动。然后您将重新启动它以检查违规行为并在稍后进行纠正:
$ make -f /usr/share/selinux/devel/Makefile \
docker_apache.te *1*
Compiling targeted docker_apache module
/usr/bin/checkmodule: loading policy configuration from >
tmp/docker_apache.tmp
/usr/bin/checkmodule: policy configuration loaded
/usr/bin/checkmodule: writing binary representation (version 17) >
to tmp/docker_apache.mod
Creating targeted docker_apache.pp policy package
rm tmp/docker_apache.mod tmp/docker_apache.mod.fc
$ semodule -i docker_apache.pp *2*
$ setenforce Enforcing *3*
$ docker run -ti --name selinuxdock >
--security-opt label:type:docker_apache_t httpd *4*
Unable to find image 'httpd:latest' locally
latest: Pulling from library/httpd
2a341c7141bd: Pull complete
[...]
Status: Downloaded newer image for httpd:latest
permission denied
Error response from daemon: Cannot start container >
650c446b20da6867e6e13bdd6ab53f3ba3c3c565abb56c4490b487b9e8868985: >
[8] System error: permission denied
$ docker rm -f selinuxdock *5*
selinuxdock
$ setenforce Permissive *6*
$ docker run -d --name selinuxdock >
--security-opt label:type:docker_apache_t httpd *7*
-
1 将 docker_apache.te 文件编译为具有.pps 后缀的二进制 SELinux 模块
-
2 安装模块
-
3 将 SELinux 模式设置为“enforcing”
-
4 以守护进程方式运行 httpd 镜像,应用你在 SELinux 模块中定义的 docker_apache_t 安全标签类型。此命令应失败,因为它违反了 SELinux 安全配置。
-
5 删除新创建的容器
-
6 将 SELinux 模式设置为“permissive”以允许应用程序启动
-
7 以守护进程方式运行 httpd 镜像,应用你在 SELinux 模块中定义的 docker_apache_t 安全标签类型。此命令应成功运行。
检查违规
到目前为止,你已经创建了一个 SELinux 模块并将其应用到你的主机上。因为在这个主机上 SELinux 的强制模式设置为 permissive,所以那些在强制模式下会被禁止的操作允许在审计日志中记录下来。你可以通过运行以下命令来检查这些消息:
$ grep -w denied /var/log/audit/audit.log
type=AVC msg=audit(1433073250.049:392): avc: > *1*
denied { transition } for > *2*
pid=2379 comm="docker" > *3*
path="/usr/local/bin/httpd-foreground" dev="dm-1" ino=530204 > *4*
scontext=system_u:system_r:init_t:s0 >
tcontext=system_u:system_r:docker_apache_t:s0:c740,c787 > *5*
tclass=process *6*
type=AVC msg=audit(1433073250.049:392): avc: denied { write } for >
pid=2379 comm="httpd-foregroun" path="pipe:[19550]" dev="pipefs" >
ino=19550 scontext=system_u:system_r:docker_apache_t:s0:c740,c787 >
tcontext=system_u:system_r:init_t:s0 tclass=fifo_file
type=AVC msg=audit(1433073250.236:394): avc: denied { append } for >
pid=2379 comm="httpd" dev="pipefs" ino=19551 >
scontext=system_u:system_r:docker_apache_t:s0:c740,c787 >
tcontext=system_u:system_r:init_t:s0 tclass=fifo_file
type=AVC msg=audit(1433073250.236:394): avc: denied { open } for >
pid=2379 comm="httpd" path="pipe:[19551]" dev="pipefs" ino=19551 >
scontext=system_u:system_r:docker_apache_t:s0:c740,c787 >
tcontext=system_u:system_r:init_t:s0 tclass=fifo_file
[...]
-
1 审计日志中的消息类型始终为 AVC,表示 SELinux 违规,时间戳以自纪元(定义为 1970 年 1 月 1 日)以来的秒数给出。
-
2 拒绝的操作类型显示在花括号中。
-
3 触发违规的命令的进程 ID 和名称
-
4 目标文件的路径、设备和 inode
-
5 目标的安全上下文
-
6 目标对象的类别
呼吁!这里有太多的术语,我们没有时间教你可能需要知道的所有关于 SELinux 的知识。如果你想了解更多,一个好的开始是查看 Red Hat 的 SELinux 文档:access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/5/html/Deployment_Guide/ch-selinux.html。
目前,你需要检查违规是否没有不寻常之处。什么可能看起来不寻常?如果一个应用程序尝试打开你未预期的端口或文件,你可能会三思而后行:使用新的 SELinux 模块修补这些违规。
在这种情况下,我们很高兴 httpd 可以写入管道。我们已经确定这是 SELinux 阻止的原因,因为提到的“拒绝”操作是对于 VM 上 pipefs 文件的append、write和open。
修补 SELinux 违规
一旦你确定你看到的违规是可以接受的,有一些工具可以自动生成你需要应用的政策文件,因此你不需要自己编写一个,避免痛苦和风险。以下示例使用 audit2allow 工具来实现这一点。
列表 14.17. 创建新的 SELinux 策略
mkdir -p /root/selinux_policy_httpd_auto *1*
cd /root/selinux_policy_httpd_auto
audit2allow -a -w *2*
audit2allow -a -M newmodname create policy *3*
semodule -i newmodname.pp *4*
-
1 创建一个新的文件夹来存储新的 SELinux 模块
-
2 使用 audit2allow 工具显示从读取审计日志生成的策略。再次审查以确保其合理。
-
3 使用-M 标志和所选模块的名称创建你的模块
-
4 从新创建的 .pp 文件中安装模块
重要的是要理解我们创建的这个新的 SELinux 模块“包含”(或“需要”)并修改了我们之前创建的模块,通过引用并添加权限到 docker_apache_t 类型。如果你选择,你可以将这两个合并成一个完整且独立的策略,在单个 .te 文件中。
测试你的新模块
现在你已经安装了新模块,你可以尝试重新启用 SELinux 并重新启动容器。
小贴士
如果你之前无法将主机设置为宽容模式(并且你已将已删除的行添加到原始 docker_apache.te 文件中),则在继续之前重新编译并重新安装原始 docker_apache.te 文件(已删除宽容行)。
列表 14.18. 以 SELinux 限制启动容器
docker rm -f selinuxdock
setenforce Enforcing
docker run -d --name selinuxdock \
--security-opt label:type:docker_apache_t httpd
docker logs selinuxdock
grep -w denied /var/log/audit/audit.log
审计日志中不应出现新的错误。你的应用程序已在这个 SELinux 环境的上下文中启动。
讨论
SELinux 以其复杂和难以管理而闻名,最常听到的抱怨是它更常被关闭而不是调试。这几乎不安全。尽管 SELinux 的细微之处确实需要付出极大的努力才能掌握,但我们希望这项技术已经向你展示了如何创建一个安全专家可以审查——如果 Docker 不符合出厂设置,则理想情况下可以批准——的东西。
摘要
-
你可以使用功能来细粒度地控制容器内 root 的权限。
-
你可以使用 HTTP 通过 Docker API 对人员进行身份验证。
-
Docker 内置了对使用证书进行 API 加密的支撑。
-
SELinux 是一种经过良好测试的方法,可以降低容器以 root 身份运行的风险。
-
作为一种服务(aPaaS)的应用程序平台可以用来控制对 Docker 运行时的访问。
第十五章. 一帆风顺:在生产环境中运行 Docker
本章涵盖
-
你的日志容器输出的选项
-
监控运行中的容器
-
管理你的容器资源使用
-
使用 Docker 的功能来帮助管理传统的系统管理员任务
在本章中,我们将涵盖在生产环境中运行时出现的一些主题。在生产环境中运行 Docker 是一个很大的主题,Docker 的生产使用仍然是一个不断发展的领域。许多主要工具都处于早期开发阶段,并且在我们撰写本书的第一版和第二版时正在发生变化。
在本章中,我们将专注于向你展示一些当你从易变环境过渡到稳定环境时应考虑的关键事项。
15.1. 监控
当你在生产环境中运行 Docker 时,你首先想要考虑的一件事是如何跟踪和衡量你的容器正在做什么。在本节中,你将学习如何获取你运行中的容器日志活动和性能的操作视图。
这仍然是 Docker 生态系统的一个发展中的方面,但一些工具和技术正在成为比其他更主流的选择。我们将探讨将应用程序日志重定向到主机的 syslog,将docker logs命令的输出重定向到单个位置,以及 Google 的面向容器的性能监控工具 cAdvisor。
将容器的日志记录到主机的 syslog
Linux 发行版通常运行一个 syslog 守护进程。这个守护进程是系统日志功能的客户端部分——应用程序将消息发送到这个守护进程,以及元数据,如消息的重要性,守护进程将决定将消息保存到何处(如果有的话)。这个功能被各种应用程序使用,从网络连接管理器到内核本身在遇到错误时输出信息。
由于它非常可靠且广泛使用,因此您自己编写的应用程序记录到 syslog 是合理的。不幸的是,一旦您将应用程序容器化(因为默认情况下容器中没有 syslog 守护进程),这将停止工作。如果您决定在所有容器中启动 syslog 守护进程,您需要访问每个单独的容器来检索日志。
问题
您希望在您的 Docker 主机上集中捕获 syslogs。
解决方案
运行一个充当 Docker 容器 syslog 守护进程的服务容器。
这种技术的基本思想是运行一个服务容器,该容器运行 syslog 守护进程,并通过主机的文件系统共享日志接触点(/dev/log)。日志本身可以通过查询 syslog Docker 容器来检索,并存储在卷中。
图 15.1 说明了主机文件系统上的/tmp/syslogdev 如何用作主机上所有 syslog 操作的接触点。日志容器挂载并写入其 syslog 到该位置,而 syslogger 容器汇总所有这些输入。
图 15.1. Docker 容器的集中 syslog 概述

小贴士
syslog 守护进程是在服务器上运行的一个进程,它收集和管理发送到中央文件的消息,这通常是一个 Unix 域套接字。它通常使用/dev/log 作为接收日志消息的文件,并将日志输出到/var/log/syslog。
使用这个简单的 Dockerfile 可以创建 syslogger 容器。
列表 15.1. 构建 syslogger 容器
FROM ubuntu:14.043
RUN apt-get update && apt-get install rsyslog *1*
VOLUME /dev *2*
VOLUME /var/log *3*
CMD rsyslogd -n *4*
-
1 安装 rsyslog 包,这使得 rsyslogd 守护进程程序可用。“r”代表“可靠”。
-
2 创建一个用于与其他容器共享的 /dev 卷
-
3 创建一个 /var/log 卷,以便 syslog 文件可以持久化
-
4 在启动时运行 rsyslogd 进程
接下来,您构建容器,使用 syslogger 标签标记它,并运行它:
docker build -t syslogger .
docker run --name syslogger -d -v /tmp/syslogdev:/dev syslogger
你将容器的/dev 文件夹绑定挂载到主机的/tmp/syslogdev 文件夹,这样你就可以将/dev/log 套接字作为卷挂载到每个容器中,正如你很快就会看到的。容器将继续在后台运行,读取任何来自/dev/log 文件的消息并处理它们。
在主机上,你现在会看到 syslog 容器的/dev 文件夹已经挂载到主机的/tmp/syslogdev 文件夹:
$ ls -1 /tmp/syslogdev/
fd
full
fuse
kcore
log
null
ptmx
random
stderr
stdin
stdout
tty
urandom
zero
对于这个演示,我们将启动 100 个守护进程容器,它们将从 0 到 100 记录自己的启动顺序到 syslog,使用logger命令。然后,你可以通过在主机上运行docker exec来查看 syslogger 容器的 syslog 文件,从而看到这些消息。
首先,启动容器。
列表 15.2. 启动日志容器
for d in {1..100}
do
docker run -d -v /tmp/syslogdev/log:/dev/log ubuntu logger hello_$d
done
之前的卷挂载将容器的 syslog 端点(/dev/log)链接到主机上的/tmp/syslogdev/log 文件,该文件反过来映射到 syslogger 容器的/dev/log 文件。通过这种连接,所有 syslog 输出都发送到同一个文件。
当完成时,你会看到类似以下(编辑后)的输出:
$ docker exec -ti syslogger tail -f /var/log/syslog
May 25 11:51:25 f4fb5d829699 logger: hello
May 25 11:55:15 f4fb5d829699 logger: hello_1
May 25 11:55:15 f4fb5d829699 logger: hello_2
May 25 11:55:16 f4fb5d829699 logger: hello_3
[...]
May 25 11:57:38 f4fb5d829699 logger: hello_97
May 25 11:57:38 f4fb5d829699 logger: hello_98
May 25 11:57:39 f4fb5d829699 logger: hello_99
如果你愿意,可以使用修改后的exec命令来存档这些 syslogs。例如,你可以运行以下命令来获取 5 月 25 日第 11 小时的所有日志存档到一个压缩文件中:
$ docker exec syslogger bash -c "cat /var/log/syslog | \
grep '^May 25 11'" | xz - > /var/log/archive/May25_11.log.xz
注意
为了让消息显示在中央 syslog 容器中,你的程序需要记录到 syslog。我们通过运行logger命令来确保这一点,但你的应用程序也应该这样做才能正常工作。大多数现代日志方法都有一种写入本地可见 syslog 的方法。
讨论
你可能会想知道如何使用这种技术区分不同容器的日志消息。这里有几个选项。你可以更改应用程序的日志输出以输出容器的主机名,或者你可以查看下一个技术,让 Docker 为你做这项繁重的工作。
注意
这种技术与下一个使用 Docker syslog 驱动的技术看起来很相似,但它不同。这种技术将容器运行进程的输出作为docker logs命令的输出,而下一个技术接管了logs命令,使得这种技术变得冗余。
| |
| |
记录 Docker 日志输出
正如你所见,Docker 提供了一个基本的日志系统,它捕获了容器启动命令的输出。如果你是一个在单个主机上运行许多服务的系统管理员,手动使用docker logs命令逐个容器跟踪和捕获日志可能会在操作上感到疲惫。
在这个技术中,我们将介绍 Docker 的日志驱动功能。这让你可以使用标准的日志系统来跟踪单个主机上的多个服务,甚至跨多个主机。
问题
你想在 Docker 主机上集中捕获docker logs输出。
解决方案
使用--log-driver标志将日志重定向到所需的位置。
默认情况下,Docker 日志被捕获在 Docker 守护进程中,您可以使用docker logs命令访问这些日志。如您所知,这显示了容器主进程的输出。
在撰写本文时,Docker 提供了多个选项来将此输出重定向到多个log drivers,包括
-
syslog
-
journald
-
json-file
默认为 json-file,但可以使用--log-driver标志选择其他选项。syslog 和 journald 选项将日志输出发送到同名守护进程。您可以在docs.docker.com/engine/reference/logging/找到所有可用日志驱动程序的官方文档。
警告
此技术需要 Docker 版本 1.6.1 或更高版本。
syslog 守护进程是在服务器上运行的过程,它收集和管理发送到中央文件(通常是 Unix 域套接字)的消息。它通常使用/dev/log作为接收日志消息的文件,并将日志输出到/var/log/syslog。
Journald 是一个系统服务,用于收集和存储日志数据。它创建并维护一个结构化索引,记录来自各种来源的日志。可以使用journalctl命令查询日志。
将日志记录到 syslog
要将输出定向到 syslog,请使用--log-driver标志:
$ docker run --log-driver=syslog ubuntu echo 'outputting to syslog'
outputting to syslog
这将记录 syslog 文件中的输出。如果您有权限访问该文件,可以使用标准 Unix 工具检查日志:
$ grep 'outputting to syslog' /var/log/syslog
Jun 23 20:37:50 myhost docker/6239418882b6[2559]: outputting to syslog
将日志记录到 journald
输出到日志守护进程看起来类似:
$ docker run --log-driver=journald ubuntu echo 'outputting to journald'
outputting to journald
$ journalctl | grep 'outputting to journald'
Jun 23 11:49:23 myhost docker[2993]: outputting to journald
警告
在运行前面的命令之前,请确保您的宿主机上运行着日志守护进程。
应用于所有容器
将此参数应用于您主机上的所有容器可能很费力,因此您可以将 Docker 守护进程更改为默认使用这些支持的机制进行日志记录。
修改守护进程/etc/default/docker、/etc/sysconfig/docker或您的发行版设置的任何 Docker 配置文件,以便激活DOCKER_OPTS=""行并包含日志驱动程序标志。例如,如果该行是
DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4"
更改为:
DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4 --log-driver syslog"
小贴士
有关如何在您的宿主机上更改 Docker 守护进程配置的详细信息,请参阅附录 B。
如果您重新启动 Docker 守护进程,容器应将日志记录到相关服务。
讨论
在此上下文中值得提及的另一个常见选择(但在此处未涵盖)是,您可以使用容器来实现 ELK(Elasticsearch、Logstash、Kibana)日志基础设施。
警告
将此守护进程设置更改为json-file或journald以外的任何内容,将意味着默认情况下标准docker logs命令将不再工作。此 Docker 守护进程的用户可能不会欣赏这种变化,尤其是因为/var/log/syslog文件(由syslog驱动程序使用)通常对非 root 用户不可访问。
| |
| |
使用 cAdvisor 监控容器
一旦在生产环境中运行了大量容器,你将希望像在主机上运行多个进程时那样,精确地监控它们的资源使用情况和性能。
监控领域(无论是普遍的,还是针对 Docker)是一个广泛的领域,有许多候选者。在这里选择了 cAdvisor,因为它是一个流行的选择。由 Google 开源,它迅速获得了人气。如果你已经使用 Zabbix 或 Sysdig 等传统主机监控工具,那么值得看看它是否已经提供了你需要的功能——许多工具正在添加容器感知功能,正如我们编写时一样。
问题
你想监控你容器的性能。
解决方案
使用 cAdvisor 作为监控工具。
cAdvisor 是由 Google 开发的一款用于监控容器的工具。它在 GitHub 上开源,网址为 github.com/google/cadvisor。
cAdvisor 以守护进程的形式运行,收集正在运行的容器的性能数据。其中之一,它跟踪
-
资源隔离参数
-
历史资源使用情况
-
网络统计信息
cAdvisor 可以在主机上本地安装或作为 Docker 容器运行。
列表 15.3. 运行 cAdvisor
$ docker run \ *1*
--volume /:/rootfs:ro \ *2*
--volume /var/run:/var/run:rw \
--volume /sys:/sys:ro \ *3*
--volume /var/lib/docker/:/var/lib/docker:ro \ *4*
-p 8080:8080 -d --name cadvisor \ *5*
--restart on-failure:10 google/cadvisor *6*
-
1 允许 cAdvisor 以只读方式访问根文件系统,以便它可以跟踪有关主机的信息
-
2 以读写访问权限挂载 /var/run 文件夹。预计每个主机上最多运行一个 cAdvisor 实例。
-
3 允许 cAdvisor 以只读方式访问主机的 /sys 文件夹,其中包含有关内核子系统和连接到主机的设备的信息
-
4 允许 cAdvisor 以只读方式访问 Docker 的主机目录
-
5 cAdvisor 的 Web 界面在容器的 8080 端口上提供服务,因此我们在同一端口上将其发布到主机。运行容器的标准 Docker 参数也被用于在后台运行容器并给容器命名。
-
6 在失败时重启容器,最多重启 10 次。镜像存储在 Docker Hub 上,属于 Google 的账户。
一旦启动了镜像,你可以使用浏览器访问 http://localhost:8080 来开始检查数据输出。这里有关于主机的信息,但通过点击主页顶部的 Docker 容器链接,你可以检查 CPU、内存和其他历史数据的图表。只需点击“子容器”标题下列出的运行容器即可。
在容器运行期间,数据被收集并保存在内存中。GitHub 页面上有关于将数据持久化到 InfluxDB 实例的文档。GitHub 仓库还提供了关于 REST API 和用 Go 编写的示例客户端的详细信息。
提示
InfluxDB 是一个开源数据库,旨在处理时间序列数据的跟踪。因此,它非常适合记录和分析实时提供的监控信息。
讨论
监控是一个快速发展和分化的领域,cAdvisor 只是众多组件之一。例如,Prometheus,作为 Docker 的快速崛起的标准,可以接收和存储由 cAdvisor 产生而不是直接放入 InfluxDB 的数据。
监控也是开发者可能非常热衷的一个主题。制定一个灵活的监控策略,以适应不断变化的潮流是有益的。
15.2. 资源控制
运行生产环境中的服务时,一个核心的担忧是资源的公平和有效分配。在底层,Docker 使用核心操作系统概念 cgroups 来管理容器的资源使用。默认情况下,当容器争夺资源时,使用的是简单且均等份额的算法,但有时这还不够。您可能希望出于运营或服务原因,为容器或容器类别预留或限制资源。
在本节中,您将学习如何调整容器的 CPU 和内存使用。
限制容器可以执行的 CPU 核心
默认情况下,Docker 允许容器在您的机器上的任何核心上执行。具有单个进程和线程的容器显然只能使用一个核心,但容器中的多线程程序(或多个单线程程序)将能够使用所有 CPU 核心。如果您有一个比其他容器更重要的容器,您可能想要改变这种行为——对于面向客户的应用程序来说,每次内部日常报告运行时都要争夺 CPU 并不理想。您还可以使用这种技术来防止失控的容器阻止您通过 SSH 访问服务器。
问题
你希望容器拥有最低的 CPU 分配,对 CPU 消耗有硬性限制,或者想要限制容器可以运行的 CPU 核心数。
解决方案
使用--cpuset-cpus选项为您的容器预留 CPU 核心。
要正确探索--cpuset-cpus选项,您需要在具有多个核心的计算机上执行此技术。如果您使用的是云机器,可能不是这种情况。
提示
旧版本的 Docker 使用--cpuset标志,现在已弃用。如果您无法使--cpuset-cpus工作,请尝试使用--cpuset代替。
要查看--cpuset-cpus选项的效果,我们将使用htop命令,它提供了计算机核心使用情况的直观图形视图。在继续之前,请确保已安装此命令——它通常作为系统包管理器中的htop包提供。或者,您可以在使用--pid=host选项启动的 Ubuntu 容器内安装它,以便将主机进程信息暴露给容器。
如果您现在运行htop,您可能会看到没有任何核心在使用。为了在几个容器内模拟一些负载,请在两个不同的终端中运行以下命令:
docker run ubuntu:14.04 sh -c 'cat /dev/zero >/dev/null'
回顾htop,你应该会看到现在有两个核心显示 100%的使用率。要将其限制在一个核心上,使用docker kill终止之前的容器,然后在两个终端中运行以下命令:
docker run --cpuset-cpus=0 ubuntu:14.04 sh -c 'cat /dev/zero >/dev/null'
现在htop将显示这些容器只使用了你的第一个核心。
--cpuset-cpus选项允许以逗号分隔的列表(0,1,2)、范围(0-2)或两者的组合(0-1,3)指定多个核心。因此,为宿主机保留 CPU 是一个选择范围的问题,这个范围不包括任何核心。
讨论
你可以用多种方式使用这个功能。例如,你可以通过持续将剩余的 CPU 分配给运行中的容器来为宿主进程保留特定的 CPU。或者,你也可以将特定的容器限制在它们自己的专用 CPU 上运行,这样它们就不会干扰其他容器的计算。
在多租户环境中,这可以确保工作负载不会相互干扰,真是一个天赐之物。
| |
为重要容器分配更多 CPU
当容器在宿主机上竞争 CPU 时,它们通常会平均共享 CPU 使用率。你已经看到了如何做出绝对的保证或限制,但这些可能有点不灵活。如果你想让一个进程能够比其他进程使用更多的 CPU,那么不断为它保留整个核心是浪费的,如果你有很少的核心,这样做可能会有限制。
Docker 为希望将应用程序带到共享服务器的用户提供了多租户支持。这可能导致那些有虚拟机经验的人所熟知的嘈杂邻居问题,其中一个用户消耗了资源并影响了另一个用户在相同硬件上运行的虚拟机。
作为具体例子,当我们编写这本书时,我们必须使用这个功能来减少一个特别贪婪的 Postgres 应用程序的资源使用,该应用程序消耗了 CPU 周期,剥夺了机器上 Web 服务器为最终用户提供服务的能力。
问题
你希望能够给更重要的一些容器分配更多的 CPU 份额,或者将某些容器标记为不太重要。
解决方案
使用docker run命令的-c/--cpu-shares参数来定义 CPU 使用的相对份额。
当容器启动时,它会被分配一个数字(默认为 1024)的CPU 份额。当只有一个进程运行时,如果需要,它将能够访问 100%的 CPU,无论它有多少 CPU 份额。只有在与其他容器竞争 CPU 时,这个数字才会被使用。
假设我们有三个容器(A、B 和 C)都在尝试使用所有可用的 CPU 资源:
-
如果它们都被分配了相同的 CPU 份额,那么每个容器将分配到 CPU 的三分之一。
-
如果 A 和 B 被分配了 512,C 被分配了 1024,那么 C 将获得一半的 CPU,A 和 B 各自获得四分之一。
-
如果 A 被分配 10,B 被分配 100,C 被分配 1000,A 将只能获得可用 CPU 资源的不到 1%,并且只有在 B 和 C 空闲时才能执行资源密集型操作。
所有这些都假设你的容器可以使用你机器上的所有核心(或者你只有一个核心)。Docker 将在可能的情况下将容器的负载分散到所有核心。如果你在一个双核心机器上运行两个单线程应用程序,显然没有方法可以在最大化使用可用资源的同时应用相对权重。每个容器都将被分配一个核心来执行,而不管它的权重如何。
如果你想尝试一下,运行以下命令:
列表 15.4. 使 Docker shell 缺乏 CPU
docker run --cpuset-cpus=0 -c 10000 ubuntu:14.04 \
sh -c 'cat /dev/zero > /dev/null' &
docker run --cpuset-cpus=0 -c 1 -it ubuntu:14.04 bash
现在看看在 bash 提示符下做任何事情都多么缓慢。请注意,这些数字是相对的——你可以将它们都乘以 10(例如),它们将意味着完全相同的事情。但是默认的授予值仍然是 1024,所以一旦你开始更改这些数字,考虑一下没有在命令中指定 CPU 份额且在相同 CPU 集上运行的过程会发生什么就很有价值了。
小贴士
为您的用例找到正确的 CPU 份额级别是一种艺术。查看 top 和 vmstat 等程序的输出以确定什么在消耗 CPU 时间很有价值。当使用 top 时,特别有用的是按“1”键来显示每个 CPU 核心分别在做些什么。
讨论
尽管我们在现实世界中很少直接看到这种技术的使用,而且它的使用通常在底层平台上看到,但了解并玩转底层机制以了解当租户抱怨缺乏(或明显的缺乏)资源访问时它是如何工作的,这很好。这在现实世界环境中很常见,尤其是如果租户的工作负载对基础设施可用性的波动敏感。
限制容器内存使用
当你运行一个容器时,Docker 将允许它从主机分配尽可能多的内存。通常这是可取的(并且与虚拟机相比有一个很大的优势,虚拟机有固定的内存分配方式)。但有时应用程序可能会失控,分配过多的内存,并在开始交换时使机器缓慢下来。这很烦人,我们过去曾多次遇到过这种情况。我们想要一种限制容器内存消耗的方法来防止这种情况。
问题
你想要能够限制容器的内存消耗。
解决方案
使用docker run的-m/--memory参数。
如果你正在运行 Ubuntu,那么你很可能默认没有启用内存限制功能。要检查,请运行 docker info。如果输出中的某一行是关于 No swap limit support 的警告,那么不幸的是,你需要做一些设置工作。请注意,进行这些更改可能会对你的机器上所有应用程序的性能产生影响——有关更多信息,请参阅 Ubuntu 安装文档 (docs.docker.com/engine/installation/ubuntulinux/#adjust-memory-and-swap-accounting)。
简而言之,你需要在启动时向内核指示你想要这些限制可用。为此,你需要按照以下方式修改 /etc/default/grub。如果 GRUB_CMDLINE_LINUX 已经有值,请将新值添加到末尾:
-GRUB_CMDLINE_LINUX=""
+GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"
现在,你需要运行 sudo update-grub 并重新启动你的计算机。运行 docker info 应该不再显示警告,你现在可以继续进行主要活动了。
首先,让我们通过使用最低可能的限制 4 MB 来粗略地演示内存限制确实起作用。
列表 15.5. 为容器设置最低可能的内存限制
$ docker run -it -m 4m ubuntu:14.04 bash *1*
root@cffc126297e2:/# \
python3 -c 'open("/dev/zero").read(10*1024*1024)' *2*
Killed *3*
root@e9f13cacd42f:/# \
A=$(dd if=/dev/zero bs=1M count=10 | base64) *4*
$ *5*
$ echo $? *6*
137 *7*
-
1 以 4 MB 的内存限制运行容器
-
2 尝试将大约 10 MB 的内容加载到内存中
-
3 进程消耗了过多的内存,因此被终止。
-
4 尝试将 10 MB 的内存直接加载到 bash 中
-
5 Bash 被终止,因此容器退出了。
-
6 检查退出代码
-
7 退出代码非零,表示容器因错误而退出。
这种类型的约束有一个需要注意的地方。为了演示这一点,我们将使用 jess/stress 镜像,它包含 stress 工具,这是一个用于测试系统极限的工具。
小贴士
Jess/stress 是一个用于测试你对容器施加的任何资源限制的有用镜像。如果你想进行更多实验,请尝试使用此镜像之前的技巧。
如果你运行以下命令,你可能会惊讶地发现它并没有立即退出:
docker run -m 100m jess/stress --vm 1 --vm-bytes 150M --vm-hang 0
你已要求 Docker 将容器限制在 100 MB,并且你已指示 stress 使用 150 MB。你可以通过运行此命令来验证 stress 是否按预期运行:
docker top <container_id> -eo pid,size,args
大小列以 KB 为单位,显示你的容器确实使用了大约 150 MB 的内存,这引发了为什么它没有被终止的问题。实际上,Docker 对内存进行了双重预留——一半用于物理内存,另一半用于交换空间。如果你尝试以下命令,容器将立即终止:
docker run -m 100m jess/stress --vm 1 --vm-bytes 250M --vm-hang 0
这种双重预留只是一个默认设置,可以通过 --memory-swap 参数来控制,该参数指定了总虚拟内存大小(内存 + 交换空间)。例如,要完全消除交换空间的使用,应将 --memory 和 --memory-swap 设置为相同的大小。您可以在 Docker run 参考文档中查看更多示例:docs.docker.com/engine/reference/run/#user-memory-constraints。
讨论
内存限制是任何运行 Docker 平台的运营(或 DevOps)团队的热门话题之一。配置不当或配置不佳的容器经常耗尽分配(或预留)的内存(我看着你,Java 开发者!),需要编写常见问题解答和操作手册来指导用户在遇到问题时如何操作。
了解这里发生的事情对于支持此类平台并向用户提供正在发生的事情的背景非常有帮助。
15.3. Docker 的系统管理员用例
在本节中,我们将探讨 Docker 可以用于的一些令人惊讶的用途。尽管乍一看可能觉得奇怪,但 Docker 可以使您的计划任务管理更加容易,并且可以用作备份工具的一种形式。
提示
计划任务(cron job)是一种定时、定期的命令,由几乎所有 Linux 系统中作为服务包含的守护进程运行。每个用户都可以指定自己的命令运行计划。系统管理员广泛使用它来运行周期性任务,例如清理日志文件或运行备份。
这绝不是潜在用途的详尽列表,但它应该让您尝到 Docker 的灵活性,并对其功能如何以意想不到的方式使用有一些了解。
使用 Docker 运行计划任务
如果您曾经需要在多个主机上管理计划任务,您可能已经遇到过必须将相同的软件部署到多个位置并确保 crontab 本身具有您想要运行的程序的正确调用的操作难题。
尽管有其他解决方案可以解决这个问题(例如使用 Chef、Puppet、Ansible 或其他配置管理工具来管理跨主机的软件部署),但一个选项是使用 Docker 仓库来存储正确的调用。
这并不是解决上述问题的最佳方案,但它是一个引人注目的例子,说明了拥有一个隔离和可移植的应用程序运行时配置存储库的好处,如果您已经使用 Docker,这将免费获得。
问题
您希望您的计划任务能够集中管理和自动更新。
解决方案
将您的计划任务脚本作为 Docker 容器拉取并运行。
如果您有一大批需要定期运行作业的机器,您通常会使用 crontabs 并手动配置它们(是的,这仍然会发生),或者您会使用 Puppet 或 Chef 等配置管理工具。更新它们的食谱将确保当机器的配置管理控制器下次运行时,更改将应用到 crontab 中,以便在之后的运行中执行。
小贴士
一个 crontab 文件是由用户维护的特定文件,它指定了脚本应该运行的时间。通常这些会是维护任务,比如压缩和存档日志文件,但它们也可能是业务关键应用程序,例如信用卡支付结算器。
在这个技术中,我们将向您展示如何使用来自注册表的 Docker 镜像替换此方案,并通过 'docker pull' 来交付。
在正常情况下,如图 15.2 所示,维护者更新配置管理工具,然后在代理运行时将其交付到服务器。同时,cron 作业在旧代码和新代码之间运行,而系统在更新。
图 15.2. 每个服务器在 CM 代理计划运行期间更新 cron 脚本

在 Docker 场景中,如图 15.3 所示,服务器在 cron 作业运行之前会拉取代码的最新版本。
图 15.3. 每个服务器在每次 cron 作业运行时都会拉取最新镜像

在这一点上,您可能想知道,如果您已经有了可行的解决方案,为什么还要费心去做这件事。以下是使用 Docker 作为交付机制的一些优点:
-
每次运行一个作业时,作业将从中央位置更新到最新版本。
-
您的 crontab 文件变得更加简单,因为脚本和代码都封装在 Docker 镜像中。
-
对于更大或更复杂的更改,只需要拉取 Docker 镜像的增量,从而加快交付和更新速度。
-
您不需要在机器本身上维护代码或二进制文件。
-
您可以将 Docker 与其他技术结合使用,例如将日志输出到 syslog,以简化并集中管理这些管理服务。
在这个例子中,我们将使用我们在 技术 49 中创建的 log_cleaner 镜像。您无疑会记得这个镜像封装了一个清理服务器上日志文件的脚本,并接受一个参数来指定要清理的日志文件的天数。使用 Docker 作为交付机制的 crontab 将类似于以下列表。
列表 15.6. 日志清理器 crontab 条目
0 0 * * * \ *1*
IMG=dockerinpractice/log_cleaner && \
docker pull $IMG && \ *2*
docker run -v /var/log/myapplogs:/log_dir $IMG 1 *3*
-
1 每天午夜运行此操作
-
2 首先拉取镜像的最新版本
-
3 运行日志清理器处理一天的日志文件
小贴士
如果你不太熟悉 cron,你可能想知道要编辑你的 crontab,你可以运行crontab -e。每一行指定一行开始处的五个项目所指定的时间运行的命令。通过查看 crontab man 页面了解更多信息。
如果出现故障,标准的 cron 发送电子邮件机制应该启动。如果你不依赖这个,添加一个带有or操作符的命令。在以下示例中,我们假设你的定制警报命令是my_alert_command。
列表 15.7. 在错误时带有警报的日志清理 crontab 条目
0 0 * * * \
(IMG=dockerinpractice/log_cleaner && \
docker pull $IMG && \
docker run -v /var/log/myapplogs:/log_dir $IMG 1) \
|| my_alert_command 'log_cleaner failed'
提示
一个or操作符(在这种情况下,双竖线:||)确保运行任一侧的命令。如果第一个命令失败(在这种情况下,cron 指定0 0 * * *后面的括号内的两个命令之一,由and操作符&&连接),则将运行第二个命令。
||操作符确保如果日志清理作业的任何部分运行失败,则将运行警报命令。
讨论
我们真的很喜欢这种技术,因为它简单,并且使用经过实战检验的技术以独特的方式解决问题。
Cron 已经存在了几十年(根据维基百科,自 1970 年代末以来)并且通过 Docker 镜像的增强是我们在家以简单方式管理常规任务所使用的技术。
备份的“保存游戏”方法
如果你曾经运行过事务性系统,你会知道当事情出错时,推断出问题发生时系统状态的能力对于根本原因分析是至关重要的。
通常这是通过多种手段的组合来完成的:
-
应用程序日志分析
-
数据库取证(确定在特定时间点数据的状态)
-
构建历史分析(确定在特定时间点上服务上运行了哪些代码和配置)
-
实时系统分析(例如,是否有人登录到该设备并更改了某些内容?)
对于如此关键的系统,采取简单但有效的方法备份 Docker 服务容器可能是有益的。尽管你的数据库可能与你 Docker 基础设施分开,但配置、代码和日志的状态可以通过几个简单的命令存储在注册表中。
问题
你希望保留 Docker 容器的备份。
解决方案
在运行时提交容器,并将生成的镜像推送到专门的 Docker 仓库。
遵循 Docker 最佳实践并利用一些 Docker 功能可以帮助你避免需要存储容器备份的需求。例如,使用技术 102 中描述的日志驱动程序而不是将日志记录到容器文件系统中,这意味着不需要从容器备份中检索日志。
但有时现实迫使你不能按照你希望的方式做所有事情,你真的需要看到容器的外观。以下命令显示了提交和推送备份容器的整个过程。
列表 15.8. 提交和推送备份容器
DATE=$(date +%Y%m%d_%H%M%S) *1*
TAG="your_log_registry:5000/live_pmt_svr_backup:$(hostname -s)_${DATE}" *2*
docker commit -m="$DATE" -a="Backup Admin" live_pmt_svr $TAG *3*
docker push $TAG *4*
-
1 生成一个精确到秒的时间戳
-
2 生成一个指向您的注册表 URL 的标签,该标签包含主机名和日期
-
3 以日期作为消息,并以“备份管理员”作为作者提交容器
-
4 将容器推送到注册表
警告
此技术将在容器运行时暂停它,有效地将其从服务中移除。您的服务应该能够容忍中断,或者您应该有其他节点在此时运行,以负载均衡的方式处理请求。
如果在所有主机上以交错轮换的方式执行此操作,您将拥有一个有效的备份系统,以及尽可能减少歧义地恢复支持工程师状态的手段。图 15.4 展示了这种设置的简化视图。
图 15.4. 两个主机备份的服务

备份仅推送基本镜像和备份时容器状态的差异,并且备份是交错进行的,以确保至少在一个主机上服务保持运行。注册服务器只存储每个提交点的基镜像和差异,节省磁盘空间。
讨论
您可以通过结合所谓的“凤凰部署”模型将此技术进一步发展。凤凰部署是一种强调尽可能多地替换系统而不是就地升级部署的部署模型。这是许多 Docker 工具的核心原则。
在这种情况下,而不是提交容器并在之后继续,您可以执行以下操作:
-
从您的注册表中拉取最新镜像的副本
-
停止正在运行的容器
-
启动一个新的容器
-
提交、标记并推送旧容器到注册表
结合这些方法可以提供更多确定性,即实时系统没有从源镜像中漂移。我们中的一人使用这种方法来管理家庭服务器上的实时系统。
摘要
-
您可以将容器的日志直接定向到主机上的 syslog 守护进程。
-
Docker 日志输出可以捕获到主机级别的服务。
-
cAdvisor 可用于监控容器性能。
-
容器对 CPU、核心和内存的使用可以受到限制和控制。
-
Docker 有一些令人惊讶的使用方式,例如作为 cron 交付工具和备份系统。
第十六章。在生产中处理 Docker 的挑战
本章涵盖
-
绕过 Docker 的命名空间功能并直接使用宿主机的资源
-
确保主机操作系统不会因为内存不足而杀死容器中的进程
-
直接使用宿主机的工具调试容器的网络
-
跟踪系统调用以确定为什么容器在您的宿主机上无法工作
在本章中,我们将讨论当 Docker 的抽象不适合你时你可以做什么。这些主题必然涉及到深入了解 Docker,以了解为什么需要这样的解决方案,在这个过程中,我们旨在让你对使用 Docker 时可能出错的情况有更深入的了解,以及如何着手修复这些问题。
16.1. 性能:你不能忽视细节
尽管 Docker 试图将应用程序从其运行的主机抽象出来,但人们永远不能完全忽视主机。为了提供其抽象,Docker 必须添加间接层。这些层可能对你的运行系统有影响,有时为了解决操作挑战或绕过它们,需要理解这些层。
在本节中,我们将探讨如何绕过这些抽象之一,最终得到一个几乎不含 Docker 剩余部分的 Docker 容器。我们还将展示,尽管 Docker 似乎抽象掉了你使用的存储细节,但有时这可能会对你造成伤害。
从容器访问主机资源
我们在技术 34 中介绍了最常用的 Docker 抽象绕过方法——卷。它们便于从主机共享文件,并防止大文件进入镜像层。与容器文件系统相比,它们在文件系统访问方面也可能显著更快,因为某些存储后端对某些工作负载施加了显著的开销——这并不是所有应用程序都需要的,但在某些情况下是至关重要的。
除了某些存储后端强加的开销之外,由于 Docker 设置的用于给每个容器提供其自己的网络的网络接口,还会产生另一个性能损失。与文件系统性能一样,网络性能绝对不是每个人的瓶颈,但可能是你希望自行基准测试的东西(尽管网络调优的详细内容远远超出了本书的范围)。或者,你可能有自己的原因想要完全绕过 Docker 网络——一个打开随机端口以监听的服务器可能不会在 Docker 上监听端口范围得到很好的服务,特别是由于暴露的端口范围将在主机上分配,无论它们是否在使用中。
无论你的原因是什么,有时 Docker 的抽象会阻碍你的工作,而 Docker 确实提供了退出选项,如果你需要的话。
问题
你希望允许从容器访问主机的资源。
解决方案
使用 Docker 提供的docker run标志来绕过 Docker 使用的内核命名空间功能。
提示
内核命名空间是内核提供给程序的服务,允许程序以这种方式获取全局资源的视图,即它们似乎有自己的独立实例。例如,一个程序可以请求一个网络命名空间,这将给你一个看似完整的网络堆栈。Docker 使用并管理这些命名空间来创建其容器。
表 16.1 总结了 Docker 如何使用命名空间,以及如何有效地关闭它们。
表 16.1. 命名空间和 Docker
| 内核命名空间 | 描述 | 在 Docker 中使用? | “关闭选项” |
|---|---|---|---|
| 网络 | 网络子系统 | 是 | --net=host |
| IPC | 进程间通信:共享内存、信号量等 | 是 | --ipc=host |
| UTS | 主机名和 NIS 域 | 是 | --uts=host |
| PID | 进程 ID | 是 | --pid=host |
| 挂载 | 挂载点 | 是 | --volume, --device |
| 用户 | 用户和组 ID | 否 | N/A |
注意
如果这些标志中的任何一个不可用,那很可能是由于你的 Docker 版本过旧。
如果你的应用程序是共享内存的重度使用者,例如,并且你希望容器与宿主机共享这个空间,你可以使用--ipc=host标志来实现这一点。这种用法相对高级,所以我们将会关注其他更常见的用法。
网络和主机名
要使用宿主机的网络,你需要使用--net标志将容器设置为host,如下所示:
user@yourhostname:/$ docker run -ti --net=host ubuntu /bin/bash
root@yourhostname:/#
你会注意到,这与网络命名空间容器立即不同,因为容器内的主机名与宿主机相同。在实用层面上,这可能会导致混淆,因为不明显知道自己在容器中。
在一个网络隔离的容器中,快速执行netstat命令将显示启动时没有连接:
host$ docker run -ti ubuntu
root@b1c4877a00cd:/# netstat
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
Active UNIX domain sockets (w/o servers)
Proto RefCnt Flags Type State I-Node Path
root@b1c4877a00cd:/#
使用宿主机的网络运行类似操作将显示一个忙碌的技术作者通常的网络繁忙的主机:
$ docker run -ti --net=host ubuntu
root@host:/# netstat -nap | head
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID
/Program name
tcp 0 0 127.0.0.1:47116 0.0.0.0:* LISTEN -
tcp 0 0 127.0.1.1:53 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:3000 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:54366 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:32888 127.0.0.1:47116 ESTABLISHED -
tcp 0 0 127.0.0.1:32889 127.0.0.1:47116 ESTABLISHED -
tcp 0 0 127.0.0.1:47116 127.0.0.1:32888 ESTABLISHED -
root@host:/#
注意
netstat是一个命令,允许你查看本地网络堆栈上的网络信息。它最常用于确定网络套接字的状态。
使用net=host标志的原因有很多。首先,它可以使连接容器变得更容易。但你会失去容器端口映射的好处。例如,如果你有两个监听 80 端口的容器,你不能以这种方式在同一个主机上运行它们。第二个原因是,使用此标志时,网络性能比 Docker 本身有显著提升。
图 16.1 从高层次展示了在 Docker 与原生网络中,网络数据包必须经过的额外层。原生网络只需要通过主机的 TCP/IP 堆栈到网络接口卡(NIC),而 Docker 还需要维护一个虚拟以太对(一个“veth pair”——通过以太网电缆的物理连接的虚拟表示),一个在此 veth 对和主机网络之间的网络桥,以及一层网络地址转换(NAT)。这种开销可能导致 Docker 网络在正常使用情况下速度仅为原生主机网络的一半。
图 16.1. Docker 网络与原生网络

PID
PID 命名空间标志与其他标志类似:
imiell@host:/$ docker run ubuntu ps -p 1 *1*
PID TTY TIME CMD
1 ? 00:00:00 ps *2*
imiell@host:/$ docker run --pid=host ubuntu ps -p 1 *3*
PID TTY TIME CMD
1 ? 00:00:27 systemd *4*
-
1 在容器化环境中运行 ps 命令,只显示具有 PID 1 的进程
-
2 我们运行的 ps 是这个容器中唯一的进程,并赋予它 PID 1。
-
3 运行不带 PID 命名空间的相同 ps 命令,从而让我们看到主机进程
-
4 这次 PID 1 是 systemd 命令,它是主机操作系统的启动进程。这可能会因你的发行版而异。
上述示例演示了主机上的 systemd 进程在具有主机 PID 视图的容器中的进程 ID 为 1,而没有这种视图时,唯一看到的进程就是ps命令本身。
挂载
如果你想访问主机的设备,使用--device标志使用特定设备,或者使用--volume标志挂载整个主机的文件系统:
docker run -ti --volume /:/host ubuntu /bin/bash
上述命令将主机的/目录挂载到容器的/host目录。你可能想知道为什么不能将主机的/目录挂载到容器的/目录。这是由docker命令明确禁止的。
你可能还在想是否可以使用这些标志创建一个几乎与主机无法区分的容器。这引出了下一个部分...
类似主机的容器
你可以使用以下标志来创建一个具有几乎透明主机视图的容器:
host:/$ docker run -ti --net=host --pid=host --ipc=host \ *1*
--volume /:/host \ *2*
busybox chroot /host *3*
-
1 运行具有三个主机参数(net、pid、ipc)的容器
-
2 将主机根文件系统挂载到容器的
/host目录。Docker 不允许将卷挂载到“/”文件夹,因此你必须指定/host子文件夹卷。 -
3 启动一个 BusyBox 容器。你只需要 chroot 命令,这是一个包含该命令的小镜像。Chroot 被执行,使得挂载的文件系统看起来像是你的根目录。
令人讽刺的是,Docker 被描述为“强化版的chroot”,而在这里我们使用的是一个被描述为框架的东西来以颠覆chroot主要目的的方式运行chroot,即保护主机文件系统。通常在这个时候,我们尽量不去深入思考。
无论如何,很难想象这个命令(尽管有教育意义)在现实世界中有实际用途。如果你想到了一个,请给我们发邮件。
话虽如此,你可能想将其用作以下更有用命令的基础:
$ docker run -ti --workdir /host \
--volume /:/host:ro ubuntu /bin/bash
在这个例子中,--workdir /host将容器启动时的工作目录设置为宿主文件系统的根目录,正如使用--volume参数挂载的那样。卷规范中的:ro部分表示宿主文件系统将以只读方式挂载。
使用这个命令,你可以在具有安装工具(使用标准的 Ubuntu 包管理器)以检查文件系统的环境中获得文件系统的只读视图。例如,你可以使用一个运行一个报告主机文件系统安全问题的巧妙工具的镜像,而无需在主机上安装它。
警告
如前所述的讨论所暗示的,使用这些标志会使你面临更多的安全风险。在安全术语中,使用它们应被视为与使用--privileged标志运行等效。
讨论
在这个技术中,你已经学会了如何在容器内绕过 Docker 的抽象。禁用这些功能可以给你带来速度提升或其他便利,使 Docker 更好地满足你的需求。我们过去使用的一个变体是在容器内安装网络工具(例如,像在技术 112 中提到的 tcpflow)并暴露主机网络接口。这让你可以临时尝试不同的工具,而无需安装它们。
下一个技术将探讨如何绕过 Docker 底层磁盘存储的限制。
禁用 OOM 杀手
“OOM 杀手”听起来像一部糟糕的恐怖电影或严重的疾病,但实际上它是 Linux 操作系统内核中的一个线程,当宿主机内存不足时,它决定要做什么。在操作系统耗尽硬件内存、用尽所有可用的交换空间以及从内存中移除所有缓存文件后,它将调用 OOM 杀手来决定哪些进程应该被终止。
问题
你想要防止容器被 OOM 杀手杀死。
解决方案
在启动容器时使用--oom-kill-disable标志。
解决这个挑战就像给你的 Docker 容器添加一个标志一样简单。但正如通常情况那样,整个故事并不那么简单。
下面的列表显示了如何禁用容器的 OOM 杀手:
列表 16.1. --oom-kill-disable显示警告
$ docker run -ti --oom-kill-disable ubuntu sleep 1 *1*
WARNING: Disabling the OOM killer on containers without setting a
'-m/--memory' limit may be dangerous. *2*
-
1 将--oom-kill-disable 标志添加到正常的 docker run 命令中。
-
2 输出有关可能设置的另一个标志的警告。
你看到的警告很重要。它告诉你使用此设置是危险的,但它没有告诉你原因。设置此选项是危险的,因为如果宿主机的内存耗尽,操作系统将在你的进程之前杀死所有其他用户进程。
有时这是可取的,例如,如果您有一项关键的基础设施需要保护免受故障的影响——可能是一个跨(或为)主机上所有容器运行的审计或日志记录过程。即使如此,您也可能会想两次考虑这将对您的环境造成多大的干扰。例如,您的容器可能依赖于同一主机上运行的其他基础设施。如果您在 OpenShift 这样的容器平台上运行,即使关键平台进程被杀死,您的容器也能幸存。您可能希望在该容器之前让关键基础设施保持运行。
列表 16.2. --oom-kill-disable 没有警告
$ docker run -ti --oom-kill-disable --memory 4M ubuntu sleep 1 *1*
$ *2*
-
1 将 --memory 标志添加到正常的 docker run 命令中。
-
2 这次,没有看到警告。
注意
您可以分配的最小内存量是 4M,其中“M”代表兆字节。您也可以按“G”分配千兆字节。
您可能想知道如何判断您的容器是否被 OOM 杀手杀死。这可以通过使用 docker inspect 命令轻松完成:
列表 16.3. 判断您的容器是否被“OOM-killed”
$ docker inspect logger | grep OOMKilled
"OOMKilled": false,
此命令输出容器被杀死的详细信息,包括是否由 OOM 杀手将其杀死。
讨论
OOM 杀手不需要在容器中设置扩展权限,也不需要您是 root 用户——您只需要访问 docker 命令。这是又一个要小心不要在不信任用户拥有 root 权限的情况下给予他们 docker 命令访问权限的理由(参见第十四章关于安全)。
这不仅是一个安全风险,也是一个稳定性风险。如果用户可以运行 docker,他们可以运行一个逐渐泄漏内存的过程(这在许多生产环境中很常见)。如果没有对内存设置边界,操作系统将在选项耗尽后介入,并首先杀死内存使用量最大的用户进程(这是对 Linux OOM-killer 算法的简化,该算法经过多年的实战检验并不断完善)。然而,如果容器是以禁用 OOM 杀手的方式启动的,它可能会破坏主机上的所有容器,给用户造成更大的破坏和不稳定。
对于更精细的内存管理方法,您可以使用 --oom-score-adj 标志调整容器的“OOM 分数”。另一种可能适合您的方法是在内核中禁用内存过载提交。这将全局关闭 OOM 杀手,因为只有当内存确实可用时才会分配内存。然而,这可能会限制您主机上可以运行的容器数量,这也可能是不希望的。
正如往常一样,性能管理是一门艺术!
16.2. 当容器泄漏时——调试 Docker
在本节中,我们将介绍一些技术,帮助你理解和修复在 Docker 容器中运行的应用程序的问题。我们将介绍如何在使用主机工具调试问题时跳入容器的网络,并查看一个直接监控网络接口以避免容器操作的选择方案。
最后,我们将演示 Docker 抽象如何崩溃,导致容器在一个主机上工作而在另一个主机上不工作,以及如何在实时系统上调试这个问题。
使用 nsenter 调试容器的网络
在理想的世界里,你可以在大使容器中使用 socat(见技术 4)来诊断容器通信的问题。你会启动额外的容器,并确保连接到这个新的容器,它充当代理。代理允许你诊断和监控连接,然后将它们转发到正确的位置。不幸的是,仅为了调试目的设置这样的容器并不总是方便(或可能)。
提示
有关大使模式的描述,请参阅技术 74。
你已经在技术 15 和 19 中阅读了关于docker exec的内容。这项技术讨论了nsenter,这是一个看起来相似的工具,但它允许你在容器内部使用来自你机器的工具,而不是仅限于容器安装的工具。
问题
你想在容器中调试一个网络问题,但工具不在容器中。
解决方案
使用 nsenter 跳入容器的网络,但保留主机的工具。
如果你已经在你的 Docker 主机上安装了 nsenter,你可以使用以下命令构建它:
$ docker run -v /usr/local/bin:/target jpetazzo/nsenter
这将在/usr/local/bin 中安装 nsenter,你将能够立即使用它。nsenter 也可能在你的发行版(util-linux 包)中可用。
到现在为止,你可能已经注意到,通常有用的 BusyBox 镜像默认不包含 bash。作为 nsenter 的演示,我们将展示如何使用主机的 bash 程序进入 BusyBox 容器:
$ docker run -ti busybox /bin/bash
FATA[0000] Error response from daemon: Cannot start container >
a81e7e6b2c030c29565ef7adb94de20ad516a6697deeeb617604e652e979fda6: >
exec: "/bin/bash": stat /bin/bash: no such file or directory
$ CID=$(docker run -d busybox sleep 9999) *1*
$ PID=$(docker inspect --format {{.State.Pid}} $CID) *2*
$ sudo nsenter --target $PID \ *3*
--uts --ipc --net /bin/bash *4*
root@781c1fed2b18:~#
-
1 启动 BusyBox 容器并保存容器 ID (CID)
-
2 检查容器,提取进程 ID (PID)(见技术 30)
-
3 运行 nsenter,使用--target 标志指定要进入的容器。可能不需要“sudo”。
-
4 使用剩余的标志指定要进入的容器的命名空间
有关 nsenter 理解的命名空间的更多详细信息,请参阅技术 109。在命名空间选择的关键点是不要使用--mount标志,因为这会使用容器的文件系统,因为 bash 将不可用。/bin/bash 被指定为要启动的可执行文件。
应该指出的是,你无法直接访问容器的文件系统,但你确实拥有主机上的所有工具。
我们之前需要的是一种方法来找出主机上的哪个 veth 接口设备对应于哪个容器。例如,有时快速将容器从网络上移除是有用的。无权限的容器无法关闭网络接口,因此你需要从主机通过找出 veth 接口名称来完成此操作。
$ docker run -d --name offlinetest ubuntu:14.04.2 sleep infinity
fad037a77a2fc337b7b12bc484babb2145774fde7718d1b5b53fb7e9dc0ad7b3
$ docker exec offlinetest ping -q -c1 8.8.8.8 *1*
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 2.966/2.966/2.966/0.000 ms
$ docker exec offlinetest ifconfig eth0 down *2*
SIOCSIFFLAGS: Operation not permitted
$ PID=$(docker inspect --format {{.State.Pid}} offlinetest)
$ nsenter --target $PID --net ethtool -S eth0 *3*
NIC statistics:
peer_ifindex: 53
$ ip addr | grep '⁵³' *4*
53: veth2e7d114: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue >
master docker0 state UP
$ sudo ifconfig veth2e7d114 down *5*
$ docker exec offlinetest ping -q -c1 8.8.8.8 *6*
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
-
1 验证从新容器内部尝试 ping 是否成功
-
2 无法将容器中的接口关闭。请注意,你的接口可能不是 eth0,如果这不起作用,你可能希望使用 ip addr 来找出你的主要接口名称。
-
3 进入容器的网络空间,使用主机上的 ethtool 命令查找对端接口索引——虚拟接口的另一端
-
4 在主机上的接口列表中查找适合容器的适当 veth 接口
-
5 关闭虚拟接口
-
6 验证从容器内部尝试 ping 是否失败
你可能还想在容器内部使用的一个程序示例是 tcpdump,这是一个记录网络接口上所有 TCP 数据包的工具。要使用它,你需要运行带有 --net 命令的 nsenter,这样你就可以从主机“看到”容器的网络,因此可以使用 tcpdump 监控数据包。
例如,以下代码中的 tcpdump 命令将所有数据包记录到 /tmp/google.tcpdump 文件中(我们假设你仍然处于之前启动的 nsenter 会话中)。然后通过检索网页来触发一些网络流量:
root@781c1fed2b18:/# tcpdump -XXs 0 -w /tmp/google.tcpdump &
root@781c1fed2b18:/# wget google.com
--2015-08-07 15:12:04-- http://google.com/
Resolving google.com (google.com)... 216.58.208.46, 2a00:1450:4009:80d::200e
Connecting to google.com (google.com)|216.58.208.46|:80... connected.
HTTP request sent, awaiting response... 302 Found
Location: http://www.google.co.uk/?gfe_rd=cr&ei=tLzEVcCXN7Lj8wepgarQAQ >
[following]
--2015-08-07 15:12:04-- >
http://www.google.co.uk/?gfe_rd=cr&ei=tLzEVcCXN7Lj8wepgarQAQ
Resolving www.google.co.uk (www.google.co.uk)... 216.58.208.67, >
2a00:1450:4009:80a::2003
Connecting to www.google.co.uk (www.google.co.uk)|216.58.208.67|:80... >
connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: 'index.html'
index.html [ <=> ] 18.28K --.-KB/s in 0.008s
2015-08-07 15:12:05 (2.18 MB/s) - 'index.html' saved [18720]
root@781c1fed2b18:# 15:12:04.839152 IP 172.17.0.26.52092 > >
google-public-dns-a.google.com.domain: 7950+ A? google.com. (28)
15:12:04.844754 IP 172.17.0.26.52092 > >
google-public-dns-a.google.com.domain: 18121+ AAAA? google.com. (28)
15:12:04.860430 IP google-public-dns-a.google.com.domain > >
172.17.0.26.52092: 7950 1/0/0 A 216.58.208.46 (44)
15:12:04.869571 IP google-public-dns-a.google.com.domain > >
172.17.0.26.52092: 18121 1/0/0 AAAA 2a00:1450:4009:80d::200e (56)
15:12:04.870246 IP 172.17.0.26.47834 > lhr08s07-in-f14.1e100.net.http: >
Flags [S], seq 2242275586, win 29200, options [mss 1460,sackOK,TS val >
49337583 ecr 0,nop,wscale 7], length 0
小贴士
根据你的网络设置,你可能需要暂时更改你的 resolv.conf 文件以允许 DNS 查询工作。如果你收到“名称解析临时失败”错误,请尝试将 nameserver 8.8.8.8 行添加到你的 /etc/resolv.conf 文件顶部。完成操作后,别忘了恢复。
讨论
这种技术为你提供了一种快速更改容器网络行为的方法,而无需使用第十章中提到的任何工具(技术 78 和 79)来模拟网络故障。
你也看到了 Docker 的一个令人信服的使用案例——在 Docker 提供的隔离网络环境中调试网络问题比在不受控制的环境中更容易。在深夜试图记住 tcpdump 的正确参数以适当过滤掉无关数据包是一个容易出错的过程。使用 nsenter,你可以忘记这一点,并捕获容器内的所有内容,而无需在镜像上安装(或必须安装)tcpdump。
| |
使用 tcpflow 在不重新配置的情况下进行调试
tcpdump 是网络调查的事实标准,并且很可能是当被要求深入调试网络问题时,大多数人首先会使用的工具。
但 tcpdump 通常用于显示数据包摘要和检查数据包头部和协议信息——它并不是特别适合显示两个程序之间的应用程序级数据流。当调查两个应用程序通信的问题时,这可能会非常重要。
问题
你需要监控容器化应用程序的通信数据。
解决方案
使用 tcpflow 捕获跨越接口的流量。
tcpflow 与 tcpdump 类似(接受相同的模式匹配表达式),但它旨在让你更好地了解应用程序数据流。tcpflow 可能可以从你的系统包管理器中获取,如果没有,我们已准备了一个你可以使用的 Docker 镜像,其功能应与等效包管理器安装几乎相同:
$ IMG=dockerinpractice/tcpflow
$ docker pull $IMG
$ alias tcpflow="docker run --rm --net host $IMG"
你可以使用两种方式与 Docker 一起使用 tcpflow:将其指向 docker0 接口,并使用数据包过滤表达式仅检索你想要的包,或者使用前一种技术中的技巧来找到你感兴趣的容器的 veth 接口,并在该接口上捕获。
提示
你可能希望参考 第十章 10.2 图 来刷新你对 Docker 内部网络流量流动的记忆,并了解为什么在 docker0 上捕获会捕获容器流量。
表达式过滤是 tcpflow 在连接到接口后使用的一个强大功能,让你可以深入到你感兴趣的流量。我们将展示一个简单的示例来帮助你入门:
$ docker run -d --name tcpflowtest alpine:3.2 sleep 30d
fa95f9763ab56e24b3a8f0d9f86204704b770ffb0fd55d4fd37c59dc1601ed11
$ docker inspect -f '{{ .NetworkSettings.IPAddress }}' tcpflowtest
172.17.0.1
$ tcpflow -c -J -i docker0 'host 172.17.0.1 and port 80'
tcpflow: listening on docker0
在前面的示例中,你要求 tcpflow 打印任何流向或来自你的容器且源或目标端口为 80(通常用于 HTTP 流量)的彩色流。你现在可以通过在新终端中检索容器中的网页来尝试这个操作:
$ docker exec tcpflowtest wget -O /dev/null http://www.example.com/
Connecting to www.example.com (93.184.216.34:80)
null 100% |*******************************| 1270 0:00:00 ETA
你将在 tcpflow 终端中看到彩色输出。到目前为止命令的累积输出将类似于以下内容:
$ tcpflow -J -c -i docker0 'host 172.17.0.1 and (src or dst port 80)'
tcpflow: listening on docker0
172.017.000.001.36042-093.184.216.034.00080: >
GET / HTTP/1.1 *1*
Host: www.example.com
User-Agent: Wget
Connection: close
093.184.216.034.00080-172.017.000.001.36042: >
HTTP/1.0 200 OK *2*
Accept-Ranges: bytes
Cache-Control: max-age=604800
Content-Type: text/html
Date: Mon, 17 Aug 2015 12:22:21 GMT
[...]
<!doctype html>
<html>
<head>
<title>Example Domain</title>
[...]
-
1 蓝色着色开始
-
2 红色着色开始
讨论
tcpflow 是你工具箱的一个很好的补充,因为它非常不引人注目。你可以针对长时间运行的容器启动它,以获得它们现在正在传输的内容的一些洞察,或者与 tcpdump(前一种技术)一起使用,以获得你应用程序发出的请求类型和传输的信息的更完整视图。
除了 tcpdump,前一种技术还涵盖了使用 nsenter 来监控单个容器上的流量,而不是所有容器(这是监控 docker0 会做的事情)。
调试特定主机上失败的容器
前两种技术已经展示了你可以如何开始调查由你的容器与其他位置(无论是更多容器还是互联网上的第三方)之间的交互引起的问题。
如果你已经将问题隔离到一个主机上,并且确信外部交互不是原因,下一步应该尝试减少可移动部件的数量(移除卷和端口)并检查主机本身的详细信息(可用磁盘空间、打开的文件描述符数量等)。也许还值得检查每个主机是否运行了 Docker 的最新版本。
在某些情况下,上述方法都无济于事——你有一个可以不带参数运行(例如 docker run imagename)的镜像,它应该被完美地隔离,但在不同的主机上运行时却有所不同。
问题
你想要确定为什么容器内的特定操作在特定主机上不起作用。
解决方案
使用 strace 跟踪进程以查看它正在执行哪些系统调用,并将其与正常工作的系统进行比较。
尽管 Docker 声明的目标是让用户“在任何地方运行任何应用”,但它试图实现这一目标的手段并不总是万无一失。
Docker 将 Linux 内核 API 视为其宿主(它可以在其中运行的环境)。当人们刚开始学习 Docker 的工作原理时,很多人会问 Docker 如何处理 Linux API 的变化。据我们所知,它还没有这样做。幸运的是,Linux API 是向后兼容的,但可以想象,在未来某个时候,可能会创建一个新的 Linux API 调用,并被 Docker 化的应用程序使用,然后该应用程序被部署到一个足够新以运行 Docker 但旧到不支持该特定 API 调用的内核上。
注意
你可能会认为 Linux 内核 API 的变化是一个理论上的问题,但我们在编写这本书的第一版时遇到了这种情况。我们正在开发的一个项目使用了memfd_create Linux 系统调用,它只存在于版本 3.17 及以上的内核中。因为一些我们正在工作的主机有较旧的内核,我们的容器在一些系统上失败,而在其他系统上则工作正常。
那种场景并不是 Docker 抽象失败的唯一方式。容器可能会因为应用程序对主机上的文件所做的假设而失败。虽然这种情况很少见,但它确实会发生,因此重要的是要警惕这种风险。
SELinux 对容器的干扰
Docker 抽象可能崩溃的一个例子是与 SELinux 交互的任何内容。如第十四章讨论所述,SELinux 是在内核中实现的一层安全机制,它工作在正常用户权限之外。
Docker 使用这一层来允许通过管理容器内可以执行的操作来加强容器安全性。例如,如果您在容器内是 root 用户,您与主机上的 root 用户是相同的用户。尽管很难突破容器以获得主机上的 root 权限,但这并非不可能;已经发现了漏洞,可能还有社区尚未知晓的其他漏洞。SELinux 可以做到的是提供另一层保护,即使 root 用户从容器突破到主机,他们可以在主机上执行的操作也有限制。
到目前为止一切顺利,但 Docker 的问题在于 SELinux 是在主机上实现的,而不是在容器内。这意味着在容器中运行的程序查询 SELinux 的状态并发现它已启用时,可能会对其运行的环境做出某些假设,如果这些期望没有得到满足,可能会以意想不到的方式失败。
在以下示例中,我们正在运行一个安装了 Docker 的 CentOS 7 Vagrant 虚拟机,并在其中运行一个 Ubuntu 12.04 容器。如果我们运行一个相当直接的命令来添加用户,退出代码是 12,表示错误,并且确实用户没有被创建:
[root@centos vagrant]# docker run -ti ubuntu:12.04
Unable to find image 'ubuntu:12.04' locally
Pulling repository ubuntu
78cef618c77e: Download complete
b5da78899d3a: Download complete
87183ecb6716: Download complete
82ed8e312318: Download complete
root@afade8b94d32:/# useradd -m -d /home/dockerinpractice dockerinpractice
root@afade8b94d32:/# echo $?
12
在ubuntu:14.04容器上运行的相同命令工作正常。如果您想尝试重现此结果,您需要一个 CentOS 7 机器(或类似)。但为了学习目的,使用任何命令和容器遵循其余的技术将足够。
小贴士
在 bash 中,$?会给你上一个运行的命令的退出代码。退出代码的含义因命令而异,但通常退出代码为 0 表示调用成功,非零代码表示错误或某种异常情况。
调试 Linux API 调用
因为我们知道容器之间可能存在的差异是由于主机上运行的内核 API 之间的差异,strace 可以帮助您确定对内核 API 的调用之间的差异。
strace 是一个允许您监视进程(即系统调用)对 Linux API 进行的调用的工具。它是一个极其有用的调试和教育工具。您可以在图 16.2 中看到它是如何工作的。
图 16.2. strace 的工作原理

首先,您需要使用适当的包管理器在容器上安装 strace,然后运行不同的命令,并在前面加上strace命令。以下是失败的useradd调用的示例输出:
# strace -f \
*1*
useradd -m -d /home/dockerinpractice dockerinpractice *2*
execve("/usr/sbin/useradd", ["useradd", "-m", "-d", > *3*
"/home/dockerinpractice", "dockerinpractice"], [/* 9 vars */]) = 0
[...]
open("/proc/self/task/39/attr/current", > *4*
O_RDONLY) = 9
read(9, "system_u:system_r:svirt_lxc_net_"..., > *5*
4095) = 46
close(9) = 0 *6*
[...]
open("/etc/selinux/config", O_RDONLY) = >
*7*
-1 ENOENT (No such file or directory) *7*
open("/etc/selinux/targeted/contexts/files/ >
*7*
file_contexts.subs_dist", O_RDONLY) = -1 ENOENT (No such file or directory) *7*
open("/etc/selinux/targeted/contexts/files/ >
*7*
file_contexts.subs", O_RDONLY) = -1 ENOENT (No such file or directory)
*7*
open("/etc/selinux/targeted/contexts/files/ >
*7*
file_contexts", O_RDONLY) = -1 ENOENT (No such file or directory)
*7*
[...]
exit_group(12) *8*
-
1 使用-f 标志运行 strace,这确保了您的命令所产生的过程及其任何后代都会被 strace“跟随”
-
2 将您要调试的命令附加到 strace 调用中
-
3 strace 输出的每一行都以 Linux API 调用开始。这里的 execve 调用执行您给 strace 的命令。最后的 0 是调用的返回值(成功)。
-
4 “open”系统调用打开一个文件以供读取。返回值(9)是后续调用中用于操作文件的文件句柄号。在这种情况下,SELinux 信息是从/proc 文件系统中检索的,该文件系统包含有关运行进程的信息。
-
5 “read”系统调用作用于之前打开的文件(文件描述符为 9),并返回读取的字节数(46)。
-
6 “close”系统调用关闭了由文件描述符引用的文件。
-
7 程序尝试打开它期望存在的 SELinux 文件,但在每种情况下都失败了。strace 有用地告诉你返回值的含义:“没有这样的文件或目录。”
-
8 进程以值 12 退出,对于 useradd 来说,这意味着目录无法创建。
起初,前面的输出可能看起来很令人困惑,但经过几次之后,它就变得相对容易阅读了。每一行代表对 Linux 内核的调用,以在所谓的内核空间(与用户空间相对,在用户空间中,操作由程序执行,而不将责任交给内核)执行某些操作。
提示
如果你想了解更多关于特定系统调用的信息,你可以运行man 2 callname。你可能需要使用apt-get install manpages-dev或类似命令为你的包装系统安装 man 页面。或者,通过 Google 搜索man 2 callname可能会得到你需要的信息。
这就是 Docker 的抽象崩溃的例子。在这种情况下,操作失败是因为程序期望 SELinux 文件存在,因为 SELinux 似乎在容器上被启用,但执行细节保留在宿主机上。
提示
如果你认真对待成为一名开发者,阅读所有系统调用的man 2页面非常有用。一开始它们可能看起来充满了你不理解的术语,但随着你对各种主题的阅读,你会学到很多关于 Linux 基本概念的知识。在某个时候,你将开始了解大多数语言是如何从这个根源衍生出来的,它们的一些怪癖和奇怪之处将更有意义。但是要有耐心,因为你不会立即理解所有内容。
讨论
虽然这种情况很少见,但使用 strace 进行调试和理解程序如何交互的能力是一种非常有价值的技巧,不仅适用于 Docker,也适用于更广泛的开发生态。
如果你拥有非常小的 Docker 镜像,可能通过利用技术 57 创建,并且你不想在容器上安装 strace,那么你可以使用宿主机的 strace。你需要使用docker top <container_id>来找到容器中进程的 PID,并使用 strace 的-p参数来附加到特定的运行进程。别忘了使用 sudo。附加到进程可能允许你读取其秘密,因此需要额外的权限。
| |
从镜像中提取文件
使用docker cp命令从容器中复制文件很容易实现。不经常地,你可能想要从一个镜像中提取文件,但你没有干净运行的容器来复制。在这些情况下,你可以人为地运行一个镜像的容器,运行docker cp,然后删除容器。这已经是三个命令了,如果你,例如,镜像有一个默认的 entrypoint,它需要有效的参数,你可能会遇到麻烦。
这种技术给你一个单一的命令别名,你可以将其放入你的 shell 启动脚本中,通过一个命令和两个参数完成所有这些操作。
问题
你想要从镜像复制文件到你的主机。
解决方案
使用别名从镜像中运行一个容器,并将 entrypoint 设置为将文件内容输出到主机上的文件。
首先,我们将向你展示如何构建一个docker run命令来从镜像中提取文件,然后你会看到如何将其转换为方便的别名。
列表 16.4. 使用docker run从镜像中提取文件
$ docker run --rm \ *1*
-i \ *2*
-t \ *3*
--entrypoint=cat \ *4*
ubuntu \ *5*
/etc/os-release \ *6*
> ubuntu_os-release *7*
$ cat ubuntu_os-release
NAME="Ubuntu"
VERSION="16.04.1 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.1 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial
$ cat /etc/os-release *8*
cat: /etc/os-release: No such file or directory
-
1 使用--rm 标志在运行此命令后立即删除容器
-
2 使用-i 标志使容器交互式
-
3 使用-t 标志给容器提供一个虚拟终端来写入
-
4 将容器的 entrypoint 设置为‘cat’
-
5 你想要从其中提取文件的镜像名称
-
6 输出的文件名
-
7 将文件内容重定向到主机上的本地文件
-
8 为了强调这一点,我们展示了在主机上不存在
/etc/os-release。
你可能想知道为什么在这里使用entrypoint,而不是简单地运行cat命令来输出文件。这是因为某些镜像已经设置了一个 entrypoint。当这种情况发生时,Docker 会将cat视为entrypoint命令的参数,从而导致你不希望的行为。
为了方便,你可能想将此命令放入别名中。
列表 16.5. 使用别名从镜像中提取文件
$ alias imagecat='docker run --rm -i -t --entrypoint=cat' *1*
$ imagecat ubuntu /etc/os-release *2*
NAME="Ubuntu"
VERSION="16.04.1 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.1 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial
-
1 将命令别名设置为“imagecat”,包含从列表 16.4 到图像和文件参数的所有命令内容
-
2 使用两个参数(图像和文件名)调用“imagecat”
这种技术假设你的容器中存在cat。如果你使用技术 58 构建了最小化容器,这可能不是情况,因为容器中只有你的二进制文件——没有标准 Linux 工具。
如果是这样的话,你可能想考虑使用技术 73 中的docker export,但不是将它们发送到另一台机器,而是可以直接从它们中提取你想要的文件。记住,容器不需要成功启动你才能导出它——你可以尝试使用容器内不存在的命令来运行它,然后导出停止的容器(或者直接使用docker create,它为执行准备容器而不启动它)。
摘要
-
你可以向 Docker 传递参数来禁用不同类型的隔离,要么是为了提高容器的灵活性,要么是为了性能。
-
你可以禁用单个容器的 Linux OOM 杀手,以指示 Linux 永远不会通过杀死此进程来尝试回收有限的内存。
-
nsenter 可以用来从主机获取容器的网络上下文。
-
tcpflow 允许你监控容器内外所有流量,而无需重新配置或重启任何东西。
-
strace 是识别为什么 Docker 容器在特定主机上不起作用的一个关键工具。
这本书到此结束!我们希望我们已经打开了你的眼界,让你看到了 Docker 的一些用途,并给你提供了一些将其整合到你的公司或个人项目中的想法。如果你想与我们联系或给我们一些反馈,请在 Manning Docker in Practice 论坛(forums.manning.com/forums/docker-in-practice-second-edition)中创建一个帖子,或者针对“docker-in-practice”GitHub 仓库中的一个创建一个问题。
附录 A. 安装和使用 Docker
本书中的某些技术有时需要您从 GitHub 创建文件和克隆仓库。为了避免干扰,我们建议您在需要一些工作空间时为每种技术创建一个新的空文件夹。
对于 Linux 用户来说,安装和使用 Docker 相对容易,尽管不同 Linux 发行版之间的细节可能会有很大差异。我们建议您查看最新的 Docker 文档,以了解详细信息,文档地址为docs.docker.com/installation/。Docker 的社区版(CE)适合与本书一起使用。
虽然我们假设您正在使用 Linux 发行版(您将要查看的容器是基于 Linux 的,这样可以使事情变得简单),但许多对 Docker 感兴趣的用户正在使用基于 Windows 或 macOS 的机器。对于这些用户来说,值得注意的是,本书中的技术仍然适用,因为 Docker for Linux 官方支持这些平台。对于那些不想(或不能)遵循前面链接中的说明的用户,您可以使用以下方法之一来设置 Docker 守护进程。
注意
微软致力于支持 Docker 容器范式和管理界面,并与 Docker Inc.合作,允许创建基于 Windows 的容器。尽管在 Linux 上学习后,您可以将许多经验应用到 Windows 容器中,但由于生态系统和底层层的巨大差异,有许多事情是不同的。如果您对此感兴趣,我们建议您从微软和 Docker 提供的免费电子书开始,但请注意,这个领域较新,可能不够成熟:blogs.msdn.microsoft.com/microsoft_press/2017/08/30/free-ebook-introduction-to-windows-containers/。
虚拟机方法
在 Windows 或 macOS 上使用 Docker 的一种方法是安装一个完整的 Linux 虚拟机。一旦完成,您就可以像使用任何原生 Linux 机器一样使用虚拟机。
实现这一目标最常见的方式是安装 VirtualBox。有关更多信息及安装指南,请参阅virtualbox.org。
连接到外部 Docker 服务器的 Docker 客户端
如果您已经将 Docker 守护进程作为服务器设置好了,您可以在 Windows 或 macOS 机器上安装一个客户端,与之通信。请注意,暴露的端口将暴露在外部 Docker 服务器上,而不是在您的本地机器上——您可能需要更改 IP 地址才能访问暴露的服务。
有关此更高级方法的基本知识,请参阅技术 1,有关使其安全的方法的详细信息,请参阅技术 96。
原生 Docker 客户端和虚拟机
一种常见(且官方推荐)的方法是运行 Linux 和 Docker 的最小虚拟机,以及一个与该虚拟机上的 Docker 通信的 Docker 客户端。
目前推荐和支持的做法是
-
Mac 用户应安装 Docker for Mac:
docs.docker.com/docker-for-mac/ -
Windows 用户应安装 Docker for Windows:
docs.docker.com/docker-for-windows/
与之前描述的虚拟机方法不同,Docker for Mac/Windows 工具创建的虚拟机非常轻量级,因为它只运行 Docker,但你应该意识到,如果你正在运行资源密集型程序,可能还需要在设置中修改虚拟机的内存。
不要将 Docker for Windows 与 Windows 容器混淆(尽管你可以在安装 Docker for Windows 后使用 Windows 容器)。请注意,由于依赖于最新的 Hyper-V 功能,Docker for Windows 需要 Windows 10(但不是Windows 10 家庭版)。
如果你使用的是 Windows 10 家庭版或更早的版本,你可能还想尝试安装 Docker Toolbox,这是对相同方法的旧版本实现。Docker Inc.将其描述为遗留版本,我们强烈建议如果可能的话,追求使用 Docker 的替代方法,因为你可能会遇到一些像这样的奇怪问题:
-
卷需要在开头使用双斜杠(
github.com/docker/docker/issues/12751)。 -
由于容器是在一个与系统集成不佳的虚拟机(VM)中运行的,如果你想要从主机访问一个暴露的端口,你需要在 shell 中使用
docker-machine ip default来找到 VM 的 IP 地址,以便访问它。 -
如果你想要将端口暴露给主机外部,你需要使用像
socat这样的工具来转发端口。
如果你之前一直在使用 Docker Toolbox,并希望升级到新工具,你可以在 Docker 网站上找到 Mac 和 Windows 的迁移说明。
我们不会在本文中详细讨论 Docker Toolbox,只是将其作为上述替代方法之一提一下。
Windows 上的 Docker
由于 Windows 与 Mac 和 Linux 是截然不同的操作系统,我们将更详细地介绍一些常见问题和解决方案。你应该已经从docs.docker.com/docker-for-windows/安装了 Docker for Windows,并确保没有勾选使用 Windows 容器代替 Linux 容器复选框。启动新创建的 Docker for Windows 将开始加载 Docker,这可能需要一分钟——启动后它会通知你,你就可以开始使用了!
你可以通过打开 PowerShell 并运行docker run hello-world来检查它是否工作。Docker 会自动从 Docker Hub 拉取hello-world镜像并运行它。这个命令的输出给出了关于 Docker 客户端和守护进程之间通信所采取的步骤的简要描述。如果它看起来没有太多意义,请不要担心——关于幕后发生的事情的更多细节可以在第二章中找到。
请注意,由于本书中使用的脚本假定你正在使用bash(或类似的 shell)并且有包括用于本书中下载代码示例的git在内的许多实用工具,因此在 Windows 上可能会有一些不可避免的奇怪之处。我们建议调查 Cygwin 和 Windows Subsystem for Linux(WSL),以填补这一空白——两者都提供了类似 Linux 的环境,并具有socat、ssh和perl等命令,尽管在非常具体的 Linux 工具(如strace和ip(用于ip addr))方面,你可能会发现 WSL 提供了更完整的体验。
小贴士
Cygwin,可在www.cygwin.com/找到,是一组在 Windows 上可用的 Linux 工具集合。如果你需要一个类似 Linux 的环境进行实验,或者想要在 Windows 上原生使用(作为.exe 文件)的 Linux 工具,Cygwin 应该是你的首选。它包含了一个包管理器,因此你可以浏览可用的软件。相比之下,WSL(在docs.microsoft.com/en-us/windows/wsl/install-win10中描述)是微软为了在 Windows 上提供一个完整的模拟 Linux 环境而做出的尝试,以至于你可以从实际的 Linux 机器中复制可执行文件并在 WSL 中运行它们。它还不是完美的(例如,你不能运行 Docker 守护进程),但对于大多数用途来说,你可以有效地将其视为一台 Linux 机器。对这些内容的全面处理超出了本附录的范围。
下面列出了某些命令和组件的 Windows 替代品,但请注意,其中一些将会有明显的不足之处——这本书的重点是使用 Docker 运行 Linux 容器,因此一个“完整”的 Linux 安装(无论是肥虚拟机、云中的盒子还是本地机器上的安装)将能够更好地挖掘 Docker 的全部潜力。
-
ip addr—这个命令在这本书中通常用于查找机器在本地网络上的 IP 地址。Windows 上的等效命令是ipconfig。 -
strace—本书中用于连接在容器中运行的过程。请参阅类似主机的容器部分,在技术 109 中了解如何绕过 Docker 容器化并获取在运行 Docker 的虚拟机内的主机类似访问权限的详细信息——你将想要启动一个 shell 而不是运行chroot,并且使用带有包管理器的 Linux 发行版,如 Ubuntu,而不是 BusyBox。从那里,你可以像在主机上运行一样安装和运行命令。这个技巧适用于许多命令,几乎让你可以像对待胖虚拟机一样对待你的 Docker VM。
在 Windows 上外部暴露端口
当使用 Docker for Windows 时,端口转发是自动处理的,所以你应该能够像预期的那样使用localhost来访问暴露的端口。如果你尝试从外部机器连接,Windows 防火墙可能会造成障碍。
如果你在一个受信任且设置了防火墙的网络中,你可以通过暂时禁用 Windows 防火墙来解决这个问题,但记得之后要重新启用它!我们中的一人发现,在特定的网络中这并没有帮助,最终确定该网络在 Windows 上被设置为“域”网络,需要进入 Windows 防火墙的高级设置来执行临时的禁用。
Windows 上的图形应用程序
在 Windows 上运行 Linux 图形应用程序可能具有挑战性——不仅你必须让所有代码在 Windows 上工作,你还需要决定如何显示它。Linux 上使用的窗口系统(称为X 窗口系统或X11)并没有内置在 Windows 中。幸运的是,X 允许你在网络上显示应用程序窗口,因此你可以使用 Windows 上的 X 实现来显示在 Docker 容器中运行的应用程序。
Windows 上有几种不同的 X 实现,所以我们只将介绍你可以通过 Cygwin 获得的安装。官方文档在x.cygwin.com/docs/ug/setup.html#setup-cygwin-x-installing,你应该遵循。在选择要安装的软件包时,你必须确保选择了xorg-server、xinit和xhost。
安装完成后,打开 Cygwin 终端并运行XWin :0 -listen tcp -multiwindow。这将在你 Windows 机器上启动一个 X 服务器,具有监听来自网络的连接的能力(-listen tcp),并且每个应用程序都在自己的窗口中显示(-multiwindow),而不是一个作为虚拟屏幕来显示应用程序的单个窗口。一旦启动,你应该在你的系统托盘区域看到一个“X”图标。
注意
虽然这个 X 服务器可以监听网络,但它目前只信任本地机器。在我们所看到的所有情况下,这允许从您的 Docker 虚拟机访问,但如果您遇到授权问题,您可能想尝试运行不安全的xhost +命令以允许从所有机器访问。如果您这样做,请确保您的防火墙已配置为拒绝来自网络的任何连接尝试——绝不能在 Windows 防火墙禁用的情况下运行它!如果您运行了这个命令,请记住稍后运行xhost-来重新安全化。
是时候尝试您的 X 服务器了。使用ipconfig找出您本地机器的 IP 地址。我们通常在使用外部适配器的 IP 地址时成功,无论是无线还是有线连接,因为这似乎是来自您的容器连接看起来像是从那里来的地方。如果您有多个这样的适配器,您可能需要逐一尝试每个适配器的 IP 地址。
启动您的第一个图形应用程序应该像在 PowerShell 中运行docker run -e DISPLAY=$MY_IP:0 --rm fr3nd/xeyes一样简单,其中$MY_IP是您找到的 IP 地址。
如果您未连接到网络,您可以通过使用不安全的xhost +命令来简化问题,允许您使用DockerNAT接口。和之前一样,记得在完成后运行xhost +。
获取帮助
如果您运行的是非 Linux 操作系统并且想要获取更多帮助或建议,Docker 文档(docs.docker.com/install/)提供了针对 Windows 和 macOS 用户的最新官方推荐建议。
附录 B. Docker 配置
在本书的各个部分,你被建议更改 Docker 配置,以便在启动 Docker 主机时使更改永久化。附录 B 将为你提供实现此目的的最佳实践建议。你使用的操作系统分发版在此背景下将非常重要。
配置 Docker
大多数主流分发版的配置文件位置列在 表 B.1 中。
表 B.1. Docker 配置文件位置
| 分发 | 配置 |
|---|---|
| Ubuntu, Debian, Gentoo | /etc/default/docker |
| OpenSuse, CentOS, Red Hat | /etc/sysconfg/docker |
注意,一些分发版将配置保留为单个文件,而其他分发版则使用目录和多个文件。例如,在 Red Hat Enterprise License 上,有一个名为 /etc/sysconfig/docker/docker-storage 的文件,按照惯例,它包含与 Docker 守护进程存储选项相关的配置。
如果你的分发版中没有与表 B.1 中列出的名称匹配的文件,那么检查 /etc/docker 文件夹是值得的,因为那里可能存在相关的文件。
在这些文件中,管理 Docker 守护进程启动命令的参数。例如,当编辑时,以下类似的行允许你为主机上的 Docker 守护进程设置启动参数。
DOCKER_OPTS=""
例如,如果你想将 Docker 的根目录位置从默认位置(即 /var/lib/docker)更改为其他位置,你可能需要将前面的行更改为以下内容:
DOCKER_OPTS="-g /mnt/bigdisk/docker"
如果你的分发版使用 systemd 配置文件(而不是 /etc),你还可以在 systemd 文件夹下的 docker 文件中搜索 ExecStart 行,并根据需要更改它。例如,该文件可能位于 /usr/lib/systemd/system/service/docker 或 /lib/systemd/system/docker.service。以下是一个示例文件:
[Unit]
Description=Docker Application Container Engine
Documentation=http://docs.docker.io
After=network.target
[Service]
Type=notify
EnvironmentFile=-/etc/sysconfig/docker
ExecStart=/usr/bin/docker -d --selinux-enabled
Restart=on-failure
LimitNOFILE=1048576
LimitNPROC=1048576
[Install]
WantedBy=multi-user.target
EnvironmentFile 行将启动脚本指向我们之前讨论的 DOCKER_OPTS 条目的文件。如果你直接更改 systemctl 文件,你需要运行 systemctl daemon-reload 来确保更改被 systemd 守护进程拾取。
重启 Docker
仅更改 Docker 守护进程的配置是不够的——为了应用更改,守护进程必须重启。请注意,这将停止任何正在运行的容器并取消任何正在进行的镜像下载。
使用 systemctl 重启
大多数现代 Linux 分发版使用 systemd 来管理机器上服务的启动。如果你在命令行上运行 systemctl 并得到一页的输出,那么你的主机正在运行 systemd。如果你得到“命令未找到”的消息,请转到下一节。
如果你想更改配置,你可以按照以下步骤停止并启动 Docker:
$ systemctl stop docker
$ systemctl start docker
或者,你也可以直接重启:
$ systemctl restart docker
通过运行以下命令来检查进度:
$ journalctl -u docker
$ journalctl -u docker -f
这里第一行输出了 docker 守护进程的可用日志。第二行跟随任何新的条目。
使用服务重启
如果您的系统正在运行基于 System V 的 init 脚本集,请尝试运行 service --status-all。如果它返回服务列表,您可以使用 service 命令以新的配置重启 Docker。
$ service docker stop
$ service docker start
附录 C. Vagrant
在本书的各个部分,我们使用虚拟机来演示 Docker 需要完整机器表示或甚至多个虚拟机编排的技术。Vagrant 提供了一种简单的方法,可以从命令行启动、配置和管理虚拟机,并且它在多个平台上都可用。
设置
访问 www.vagrantup.com 并遵循那里的说明来设置。
图形用户界面
当运行 vagrant up 以启动虚拟机时,Vagrant 会读取名为 Vagrantfile 的本地文件以确定设置。
你可以在你的 provider 部分创建或更改的一个有用设置是 gui:
v.gui = true
例如,如果你的提供者是 VirtualBox,一个典型的配置部分可能看起来像这样:
Vagrant.configure(2) do |config|
config.vm.box = "hashicorp/precise64"
config.vm.provider "virtualbox" do |v|
v.memory = 1024
v.cpus = 2
v.gui = false
end
end
在运行 vagrant up 以启动虚拟机之前,你可以将 v.gui 行的 false 设置更改为 true(或者如果它还没有添加,则添加它)以获得正在运行的虚拟机的图形用户界面。
小贴士
Vagrant 中的 provider 是提供虚拟机环境的程序的名称。对于大多数用户来说,这将是 virtualbox,但它也可能是 libvirt、openstack 或 vmware_fusion(以及其他)。
内存
Vagrant 使用虚拟机来创建其环境,这些环境可能会非常消耗内存。如果你正在运行一个由每个虚拟机占用 2 GB 内存的三节点集群,你的机器将需要 6 GB 的可用内存。如果你的机器运行困难,这种内存不足很可能是原因——唯一的解决方案是停止任何非必要的虚拟机或购买更多内存。能够避免这种情况是 Docker 比虚拟机更强大的原因之一——你不需要预先为容器分配资源——它们只需消耗它们需要的。


浙公网安备 33010602011771号