Docker-学习指南第二版-全-
Docker 学习指南第二版(全)
原文:
annas-archive.org/md5/2033beb90e50f3008350ec65ed439540译者:飞龙
第一章:发布镜像
在上一章中,你学习了如何构建 Docker 镜像。下一步就是将这些镜像发布到公共仓库,供公众发现和使用。因此,本章将重点介绍如何在 Docker Hub 上发布镜像,以及如何充分利用 Docker Hub。我们将创建一个新的 Docker 镜像,使用commit命令和Dockerfile进行构建,并将其推送到 Docker Hub。本章还会讨论 Docker 可信仓库的概念。这个 Docker 可信仓库是从 GitHub 或 Bitbucket 创建的,可以与 Docker Hub 集成,自动构建镜像,从而随着仓库更新而生成镜像。这个 GitHub 上的仓库用于存储之前创建的Dockerfile。此外,我们将展示全球各地的组织如何使其开发团队能够设计和贡献多种 Docker 镜像,并将它们存储在 Docker Hub 中。Docker Hub 的 REST API 可以用于用户管理和通过编程方式操作仓库。
本章涵盖的主题包括:
-
了解 Docker Hub
-
将镜像推送到 Docker Hub
-
镜像的自动构建
-
Docker Hub 上的私有仓库
-
在 Docker Hub 上创建组织
-
Docker Hub REST API
了解 Docker Hub
Docker Hub 是用于存储 Docker 镜像的中心平台,无论是公共仓库还是私有仓库。Docker Hub 提供了诸如 Docker 镜像仓库、用户认证、自动构建镜像、与 GitHub 或 Bitbucket 集成、管理组织和小组等功能。Docker Hub 的 Docker Registry 组件负责管理 Docker 镜像仓库。此外,你还可以使用 Docker 安全扫描来保护你的仓库,目前这一功能是免费的。此功能首次在 IBM 容器仓库中启用。
Docker Registry 是用于存储镜像的存储系统。自动构建是 Docker Hub 的一个功能,但在撰写本书时,它尚未开源。下图展示了典型功能:

为了使用 Docker Hub,你需要注册并创建一个 Docker Hub 账户,注册链接为 hub.docker.com/。你可以更新 Docker Hub ID、电子邮件地址和密码字段,如下图所示:

完成注册流程后,你需要完成通过电子邮件收到的验证。完成电子邮件验证后,你在登录 Docker Hub 时将看到类似以下的截图:

Docker Hub 账户的创建已成功完成,现在你可以从hub.docker.com/login/登录到你的 Docker Hub 账户,如下图所示:

Docker Hub 还支持使用 Ubuntu 终端访问 Docker Hub 的命令行:
$ sudo docker login
使用你的 Docker ID 登录,以便从 Docker Hub 推送和拉取镜像。如果你没有 Docker ID,可以访问hub.docker.com创建一个。然后在终端中输入你的用户名和密码:
Username: vinoddandy
Password:
成功登录后,输出如下:
Login Succeeded
你可以在 Docker Hub 上浏览可用的镜像,网址是hub.docker.com/explore/:

你还可以查看你的设置,更新个人资料,并获取支持社区的详细信息,如 Twitter、Stack Overflow、#IRC、Google Groups 和 GitHub。
将镜像推送到 Docker Hub
在这里,我们将在本地机器上创建一个 Docker 镜像,并将其推送到 Docker Hub。你需要执行以下步骤:
- 在本地机器上创建 Docker 镜像,方法如下:
-
使用
docker commit子命令 -
使用
docker commit子命令和Dockerfile
-
将这个创建的镜像推送到 Docker Hub
-
从 Docker Hub 删除镜像
我们将使用ubuntu基础镜像,运行容器,添加一个新目录和一个新文件,然后创建一个新镜像。在第三章《构建镜像》中,我们学习了如何使用Dockerfile创建 Docker 镜像。你可以参考该章节,查看Dockerfile语法的详细信息。
我们将使用ubuntu基础镜像,以containerforhub为容器名运行容器,如下面的终端代码所示:
$ sudo docker run -i --name="containerforhub" -t ubuntu /bin/bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
952132ac251a: Pull complete
Digest: sha256:f4691c96e6bbaa99d99ebafd9af1b68ace2aa2128ae95a60369c506dd6e6f6ab
Status: Downloaded newer image for ubuntu:latest
root@1068a1fae7da:/#
接下来,我们将在containerforhub容器中创建一个新的目录和文件。我们还将更新这个新文件,添加一些示例文本,以便稍后进行测试:
root@1068a1fae7da:/# mkdir mynewdir
root@1068a1fae7da:/# cd mynewdir
root@1068a1fae7da:/mynewdir# echo 'this is my new container to make image and then push to hub' > mynewfile
root@1068a1fae7da:/mynewdir# cat mynewfile
this is my new container to make image and then push to hub
root@1068a1fae7da:/mynewdir#
让我们使用docker commit命令从刚刚创建的容器中构建新镜像。
commit命令将从主机机器上执行,而不是从容器内部执行:
$ sudo docker commit -m="NewImage for second edition" containerforhub vinoddandy/imageforhub2
sha256:619a25519578b0525b4c098e3d349288de35986c1f3510958b6246fa5d3a3f56
你应该用你自己的 Docker Hub 用户名替代vinoddandy,以创建镜像。
现在,我们在本地机器上有一个新的 Docker 镜像,名称为vinoddandy/imageforhub2。此时,一个包含mynewdir和mynewfile的新镜像已经在本地创建:
$ sudo docker images -a
REPOSITORY TAG IMAGE ID CREATED SIZE
vinoddandy/imageforhub2 latest 619a25519578
2 minutes ago 126.6 MB
我们将使用sudo docker login命令登录 Docker Hub,正如本章之前所讨论的。
让我们从主机机器推送这个镜像到 Docker Hub:
$ sudo docker push vinoddandy/imageforhub2
The push refers to a repository [docker.io/vinoddandy/imageforhub2]
0ed7a0595d8a: Pushed
0cad5e07ba33: Mounted from library/ubuntu
48373480614b: Mounted from library/ubuntu
latest: digest: sha256:cd5a86d1b26ad156b0c74b0b7de449ddb1eb51db7e8ae9274307d27f810280c9 size: 1564
现在,我们将登录 Docker Hub,并在 Repositories 中验证该镜像。
要测试来自 Docker Hub 的镜像,让我们从本地机器中删除这个镜像。首先,我们需要停止容器,然后删除容器:
$ sudo docker stop containerforhub
$ sudo docker rm containerforhub
我们还将删除vinoddandy/imageforhub2镜像:
$ sudo docker rmi vinoddandy/imageforhub2
Untagged: vinoddandy/imageforhub2:latest
Untagged: vinoddandy/imageforhub2@sha256:cd5a86d1b26ad156b0c74b0b7de449ddb1eb51db7e8ae9274307d27f810280c9
Deleted: sha256:619a25519578b0525b4c098e3d349288de35986c1f3510958b6246fa5d3a3f56
我们将从 Docker Hub 拉取新创建的镜像,并在本地机器上运行新容器:
$ sudo docker run -i --name="newcontainerforhub" -t \ vinoddandy/imageforhub2 /bin/bash
Unable to find image 'vinoddandy/imageforhub2:latest' locally
latest: Pulling from vinoddandy/imageforhub2
952132ac251a: Already exists
82659f8f1b76: Already exists
Digest: sha256:cd5a86d1b26ad156b0c74b0b7de449ddb1eb51db7e8ae9274307d27f810280c9
Status: Downloaded newer image for vinoddandy/imageforhub2:latest
root@9dc6df728ae9:/# cat /mynewdir/mynewfile
this is my new container to make image and then push to hub
root@9dc6df728ae9::/#
所以,我们已经从 Docker Hub 拉取了最新镜像,并使用新创建的vinoddandy/imageforhub2镜像创建了容器。请注意,Unable to find image 'vinoddandy/imageforhub2:latest' locally消息确认镜像是从 Docker Hub 的远程仓库下载的。
mynewfile中的文本验证了它与之前创建的镜像相同。
最后,我们将在hub.docker.com/r/vinoddandy/imageforhub2/从 Docker Hub 删除该镜像,然后点击“设置”,再点击“删除”,如以下截图所示:

我们将再次创建此镜像,但这次使用Dockerfile过程。所以,让我们使用在第三章《构建镜像》中解释的Dockerfile概念来创建 Docker 镜像,并将该镜像推送到 Docker Hub。
本地机器上的Dockerfile如下所示:
###########################################
# Dockerfile to build a new image
###########################################
# Base image is Ubuntu
FROM ubuntu:16.04
# Author: Dr. Peter
MAINTAINER Dr. Peter <peterindia@gmail.com>
# create 'mynewdir' and 'mynewfile'
RUN mkdir mynewdir
RUN touch /mynewdir/mynewfile
# Write the message in file
RUN echo 'this is my new container to make image and then push to hub'
>/mynewdir/mynewfile
现在我们将使用以下命令在本地构建镜像:
$ sudo docker build -t="vinoddandy/dockerfileimageforhub1" .
Sending build context to Docker daemon 16.74 MB
Step 1 : FROM ubuntu:16.04
16.04: Pulling from library/ubuntu
862a3e9af0ae: Pull complete
7a1f7116d1e3: Pull complete
Digest: sha256:5b5d48912298181c3c80086e7d3982029b288678fccabf2265899199c24d7f89
Status: Downloaded newer image for ubuntu:16.04
---> 4a725d3b3b1c
Step 2 : MAINTAINER Dr. Peter <peterindia@gmail.com>
---> Running in 5be5edc9b970
---> 348692986c9b
Removing intermediate container 5be5edc9b970
Step 3 : RUN mkdir mynewdir
---> Running in ac2fc73d75f3
---> 21585ffffab5
Removing intermediate container ac2fc73d75f3
Step 4 : RUN touch /mynewdir/mynewfile
---> Running in c64c98954dd3
---> a6304b678ea0
Removing intermediate container c64c98954dd3
Step 5 : RUN echo 'this is my new container to make image and then push to hub' > /mynewdir/mynewfile
---> Running in 7f6d087e29fa
---> 061944a9ba54
Removing intermediate container 7f6d087e29fa
Successfully built 061944a9ba54
我们将使用此镜像运行容器,如下所示:
$ sudo docker run -i --name="dockerfilecontainerforhub" -t vinoddandy/dockerfileimageforhub1 /bin/bash
root@236bfb39fd48:/# cat /mynewdir/mynewfile
this is my new container to make image and then push to hub
mynewdir中的文本确认新镜像已经正确构建,并且包含一个新的目录和新文件。
重复在 Docker Hub 中的登录过程并推送这个新创建的镜像:
$ sudo docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username (vinoddandy): vinoddandy
Password:
Login Succeeded
$ sudo docker push vinoddandy/dockerfileimageforhub1
The push refers to a repository [docker.io/vinoddandy/dockerfileimageforhub1]
92e394693590: Pushed
821a2be25576: Pushed
dca059944a2e: Pushed
ffb6ddc7582a: Mounted from library/ubuntu
344f56a35ff9: Mounted from library/ubuntu
530d731d21e1: Mounted from library/ubuntu
24fe29584c04: Mounted from library/ubuntu
102fca64f924: Mounted from library/ubuntu
latest: digest: sha256:c418c88f260526ec51ccb6422e2c90d0f6fc16f1ab81da9c300160d0e0f7bd87 size: 1979
最后,我们可以验证该镜像是否在 Docker Hub 上可用:

自动化镜像构建过程
你已经学会了如何在本地构建镜像并将这些镜像推送到 Docker Hub。Docker Hub 还具有从 GitHub 或 Bitbucket 的仓库中Dockerfile自动构建镜像的能力。自动构建在 GitHub 和 Bitbucket 的私有和公共仓库中都受支持。Docker Hub Registry 保存所有自动构建的镜像。Docker Hub Registry 是开源的,可以从github.com/docker/docker-registry访问。
我们将讨论实现自动化构建过程所需的步骤:
-
我们首先将 Docker Hub 与我们的 GitHub 账户连接。
-
从
hub.docker.com/login/登录 Docker Hub,点击“创建”,然后导航到“创建自动构建”,如以下截图所示:

- 我们现在选择“链接账户”:

- 一旦选择 GitHub,我们将选择“公开”和“私有(推荐)”,如图所示:

点击“选择”后,您的 GitHub 仓库将显示出来。
- 现在,提供 GitHub 凭据以将 GitHub 账户与 Docker Hub 连接,然后选择“登录”:

- 成功登录后,Linked Accounts & Services 屏幕将如下所示:

因此,每当 GitHub 中的Dockerfile更新时,自动构建会触发,并且新镜像将被存储在 Docker Hub Registry 中。我们可以随时检查构建历史。我们可以更改本地机器上的Dockerfile并将其推送到 GitHub。现在,我们可以在hub.docker.com/r/vinoddandy/dockerautomatedbuild/builds/看到 Docker Hub 的自动构建链接:

Docker Hub 上的私有仓库
Docker Hub 提供了公共和私有仓库。公共仓库对用户免费,私有仓库则是付费服务。带有私有仓库的计划有不同的尺寸,例如微型、小型、中型或大型订阅。
Docker 已将其公共仓库代码开源,网址是github.com/docker/docker-registry。
通常,企业不希望将他们的 Docker 镜像存储在 Docker 的公共或私有仓库中。它们更倾向于保留、维护和支持自己的仓库。因此,Docker 还为企业提供了创建和安装自己仓库的选项。
让我们使用 Docker 提供的registry镜像在本地机器上创建一个仓库。我们将使用来自 Docker 的registry镜像在本地机器上运行注册表容器:
$ sudo docker run -p 5000:5000 -d registry
768fb5bcbe3a5a774f4996f0758151b1e9917dec21aedf386c5742d44beafa41
在自动化构建部分,我们构建了vinoddandy/dockerfileimageforhub1镜像。现在,让我们将224affbf9a65镜像 ID 标记到我们本地创建的注册表镜像上。此镜像标记是为了在本地仓库中进行唯一标识。该镜像注册表可能在仓库中有多个变种,因此这个标签将帮助你识别特定的镜像:
$ sudo docker tag 224affbf9a65 \ localhost:5000/vinoddandy/dockerfileimageforhub1
标记完成后,使用docker push命令将此镜像推送到新的注册表:
$ sudo docker push localhost:5000/vinoddandy/dockerfile
imageforhub1
The push refers to a repository [localhost:5000/vinoddandy/dockerfileimageforhub1
] (len: 1)
Sending image list
Pushing repository localhost:5000/vinoddandy/dockerfileimageforhub1 (1 tags)
511136ea3c5a: Image successfully pushed
d497ad3926c8: Image successfully pushed
----------------------------------------------------
224affbf9a65: Image successfully pushed
Pushing tag for rev [224affbf9a65] on {http://localhost:5000/v1/repositories/vinoddandy/dockerfileimageforhub1/tags/latest}
现在,新的镜像已经在本地仓库中可用。你可以从本地注册表中获取这个镜像并运行容器。这个任务留给你来完成。
Docker Hub 上的组织和团队
私有仓库的一个有用方面是,你可以仅与组织或团队的成员共享它们。Docker Hub 让你创建组织,在其中你可以与同事协作并管理私有仓库。接下来,你将学习如何创建和管理组织。
第一步是在 Docker Hub 上创建一个组织,网址是hub.docker.com/organizations/add/,如下图所示:

在你的组织内部,你可以添加更多的组织,然后将成员添加到其中:

你组织和团队的成员可以与组织和团队协作。这个功能在私有仓库的情况下会更加有用。
Docker Hub 的 REST API
Docker Hub 提供了一个 REST API,可以通过程序集成 Hub 的功能。REST API 支持用户和仓库管理。
用户管理支持以下功能:
- 用户登录:用于用户登录到 Docker Hub:
GET /v1/users
$ curl --raw -L --user vinoddandy:password
https://index.docker.io/v1/users
4
"OK"
0
- 用户注册:用于注册新用户:
POST /v1/users
- 更新用户:用于更新用户的密码和电子邮件:
PUT /v1/users/(username)/
仓库管理支持以下功能:
- 创建用户仓库:用于创建用户仓库:
PUT /v1/repositories/(namespace)/(repo_name)/
$ curl --raw -L -X POST --post301 -H
"Accept:application/json" -H "Content-Type:
application/json" --data-ascii '{"email":
"singh_vinod@yahoo.com", "password": "password",
"username": "singhvinod494" }'
https://index.docker.io/v1/users
e
"User created"
0
在你创建仓库后,你的仓库将在这里列出,如以下截图所示:
- 删除用户存储库:这将删除一个用户存储库:
DELETE /v1/repositories/(namespace)/(repo_name)/
- 创建库存储库:这将创建一个库存储库,并且仅对 Docker 管理员可用:
PUT /v1/repositories/(repo_name)/
- 删除库存储库:这将删除一个库存储库,并且仅对 Docker 管理员可用:
DELETE /v1/repositories/(repo_name)/
- 更新用户存储库镜像:这将更新用户存储库中的镜像:
PUT /v1/repositories/(namespace)/(repo_name)/images
- 列出用户存储库镜像:这将列出用户存储库中的镜像:
GET /v1/repositories/(namespace)/(repo_name)/images
- 更新库存储库镜像:这将更新库存储库中的镜像:
PUT /v1/repositories/(repo_name)/images
- 列出库存储库镜像:这将列出库存储库中的镜像:
GET /v1/repositories/(repo_name)/images
- 为库存储库授权令牌:这将为库存储库授权一个令牌:
PUT /v1/repositories/(repo_name)/auth
- 为用户存储库授权令牌:这将为用户的存储库授权一个令牌:
PUT /v1/repositories/(namespace)/(repo_name)/auth
摘要
Docker 镜像是用于生成现实世界 Docker 容器的最突出的构建块,可以作为任何网络服务公开。开发人员可以查找和检查镜像的独特能力,并根据自己的需求使用它们,以构建高度可用、公开可发现、网络可访问和认知可组合的容器。所有精心制作的镜像都需要放在公共注册库中。在本章中,我们清楚地解释了如何在存储库中发布镜像。我们还讨论了可信存储库及其独特的特征。最后,我们展示了如何利用存储库的 REST API 来推送和处理 Docker 镜像以及用户管理,以编程方式进行操作。
Docker 镜像需要存储在公共、受控和网络可访问的位置,以便全球软件工程师和系统管理员可以轻松找到并利用。Docker Hub 被誉为集中聚合、策划和管理 Docker 镜像的最佳方法,源自 Docker 爱好者(内部和外部)。然而,企业无法承担将其 Docker 镜像保存在公共域中的成本,因此接下来的章节将专门解释在私有 IT 基础设施中进行镜像部署和管理所需的步骤。
第二章:运行您的私有 Docker 基础设施
在第四章《发布镜像》中,我们讨论了 Docker 镜像,并清晰地解释了 Docker 容器是 Docker 镜像的运行时实现。如今,Docker 镜像和容器数量众多,因为容器化范式已经在 IT 领域掀起了风暴。因此,全球企业有必要出于安全考虑,将其 Docker 镜像存放在自己的私有基础设施中。因此,将 Docker Hub 部署到我们自己的基础设施的概念应运而生并不断发展。Docker Hub 对于注册和存储日益增多的 Docker 镜像至关重要且相关。Docker Hub 主要用于集中管理以下内容:
-
用户账户
-
镜像的校验和
-
公共命名空间
本章重点提供相关信息,帮助您和 Docker 容器开发者设计、构建并运行您自己的私有 Docker Hub。这一章涵盖以下重要内容:
-
Docker 注册表
-
Docker 注册表的使用案例
-
运行您自己的 Docker 注册表并将镜像推送到新创建的注册表
-
Webhook 通知
-
支持 Docker 注册表 HTTP API
Docker 注册表
Docker 注册表的实现已完全改变,与本书早期版本中提到的旧版本不同。Docker 注册表 2.0 是用于存储和分发 Docker 镜像的新实现。它取代了先前的 Docker 注册表实现(github.com/docker/docker-registry)。新实现可以在 github.com/docker/distribution 上找到。它是 Apache 许可证下的开源项目。该注册表是一个无状态、高度可扩展的服务器端应用程序,用于存储和分发 Docker 镜像。新版本中不再使用 Docker 注册表索引。以前,Docker 注册表内部使用索引来进行用户认证。
Docker 注册表 2.0 已完成,采用全新的 Go 实现,并支持 Docker 注册表 HTTP API v2。当前的 Docker Hub (hub.docker.com) 基于新版本的 Docker 注册表 2.0,并支持 Docker Engine 1.6 或更高版本。这使得它对用户来说更加可靠且透明。所有云服务提供商,包括 AWS 和 IBM,已采用这一新的 Docker 注册表。
新的注册表实现提供以下优势:
-
更快的推送与拉取
-
安全高效的实现
-
简化部署
-
可插拔存储后端
-
Webhook 通知
Docker 注册表的总体架构如下图所示,展示了它如何在前端与 Nginx 集成,在后端与存储集成:

注册表的显著特点如下:
-
该注册表与 Docker Engine 1.6.0 或更高版本兼容。
-
默认存储驱动为本地 POSIX 文件系统,适用于开发或小型部署。它还支持不同的存储后端(S3、Microsoft Azure、OpenStack Swift 和阿里云 OSS)。
-
它原生支持 TLS 和基本身份验证。
-
在新版本中,注册表还支持强大的通知系统。注册表支持在注册表内部发生事件时发送 Webhook 通知。通知响应镜像清单和图层的推送与拉取。所有这些操作都被序列化为事件,事件会被排入注册表内部广播系统,该系统排队并将事件分发到端点(
docs.docker.com/registry/notifications/#endpoints)。
最新的 Docker 注册表发布了两种选项:
-
Docker 可信注册表
-
Docker 注册表
让我们详细讨论一下这两种选项:
-
Docker 可信注册表(DTR):这是 Docker 的企业级解决方案。DTR 支持高可用性,并安装在 Docker 通用控制平面(UCP)集群中。详细信息请参见以下网站:
DTR 支持镜像管理,并具有内置的安全性和访问控制。它还可以与 LDAP 和Active Directory(AD)集成,并支持基于角色的访问控制(RBAC)。
DTR 的一般架构如下图所示:

DTR 具有内置的身份验证机制。运行在节点上的 DTR 由以下容器组成:
-
dtr-api-<replica_id>:执行 DTR 业务逻辑。它为 DTR Web 应用程序和 API 提供服务。 -
dtr-garant-<replica_id>:管理 DTR 身份验证。 -
dtr-jobrunner-<replica_id>:在后台运行清理作业。 -
dtr-nautilusstore-<replica_id>:存储安全扫描数据。 -
dtr-nginx-<replica_id>:接收 HTTP 和 HTTPS 请求,并将其代理到其他 DTR 组件。默认监听主机的80和443端口。 -
dtr-notary-server-<replica_id>:接收、验证并提供内容信任元数据,在启用内容信任时,推送或拉取到 DTR 时会咨询此服务。 -
dtr-notary-signer-<replica_id>:为内容信任元数据执行服务器端时间戳和快照签名。 -
dtr-registry-<replica_id>:实现拉取和推送 Docker 镜像的功能,并处理镜像的存储方式。 -
dtr-rethinkdb-<replica_id>:用于持久化仓库元数据的数据库。
DTR 使用以下内部命名卷来持久化数据:
-
dtr-ca:私钥和证书存储在这里 -
dtr-etcd:这是 etcd 用于存储 DTR 内部配置的组件 -
dtr-registry:这是存储镜像的卷 -
dtr-rethink:RethinkDB 用于持久化 DTR 数据,如用户和仓库
默认情况下,DTR 将镜像存储在主机机器的本地文件系统上。对于高度可用的 DTR 安装,它还支持云存储或网络文件系统。DTR 可以配置为支持 Amazon S3、OpenStack Swift 和 Microsoft Azure。
- Docker Registry:Registry 是一个无状态、高度可扩展的服务器端应用程序,用于存储和分发 Docker 镜像。Registry 是开源的,采用宽松的 Apache 许可证(
en.wikipedia.org/wiki/Apache_License)。
本书将重点介绍开源 Docker Registry 的第二种选择。
Docker Registry 的使用场景
Docker Registry 存储 Docker 镜像,并提供拉取、推送和删除镜像的基本功能。在典型的工作流中,对源代码版本控制系统的提交将触发 CI 系统上的构建,如果构建成功,CI 系统将把新镜像推送到 Registry。Registry 会发送通知,触发在暂存环境中的部署,或通知其他系统新镜像已可用。
当用户需要执行以下操作时,会使用 Docker Registry:
-
严格控制镜像存储位置
-
拥有镜像分发管道
-
将镜像存储和分发与后端开发工作流集成
Registry 的重要使用场景如下:
-
拉取或下载镜像:用户使用 Docker 客户端从 Docker Registry 请求镜像,Registry 会返回给用户相关的 Registry 详细信息。然后,Docker 客户端将直接请求 Registry 获取所需的镜像。Registry 内部会通过索引对用户进行身份验证。
-
推送或上传镜像:用户请求推送镜像,获取 Registry 信息,然后将镜像直接推送到 Registry。Registry 会验证用户身份,最后回应用户。
-
删除镜像:用户还可以请求从仓库中删除镜像。
用户可以选择使用带有或不带有索引的 Registry。使用不带索引的 Registry 最适合存储私有镜像。
除了前述的使用场景,Docker Registry 还支持镜像的版本控制。它可以与 持续集成(CI)和 持续开发(CD)系统集成。当一个新镜像成功推送到 Registry 时,Registry 会发送通知,触发在暂存环境中的部署,或通知其他系统新镜像已可用。
在 Docker Registry V2 中,还支持以下新使用场景:
-
镜像验证:Docker Engine 想要运行已验证的镜像,因此它需要确保镜像是从受信任的来源下载的,并且没有被篡改。Docker Registry V2 会返回一个清单,Docker Engine 会验证该清单的签名,然后才会下载镜像。在每一层下载之后,Engine 会验证该层的摘要,以确保内容符合清单所指定的内容。
-
可恢复推送:在将镜像上传到 Docker Registry 时,可能会丢失网络连接。现在,Docker Registry 具备通知 Docker Engine 文件上传已开始的能力。因此,Docker Engine 将只发送剩余的数据来完成镜像上传。
-
可恢复拉取:在下载镜像时,如果连接在完成之前中断,Docker Engine 会保留部分数据并请求避免重新下载重复的数据。这是通过 HTTP 范围请求实现的。
-
层上传去重:公司 Y 的构建系统通过构建过程 A 和 B 创建了两个相同的 Docker 层。构建过程 A 在 B 之前完成该层的上传。当 B 过程尝试上传该层时,注册表会指示不需要,因为该层已知。如果 A 和 B 同时上传相同的层,两个操作都会继续,首先完成的操作会被存储在注册表中(请注意,我们可能会修改此行为以防止同时请求发生,并使用某些锁机制来处理)。
这就是 Docker Registry V2 需要 Docker Engine 版本 1.6 或更高版本来支持这些功能的原因。
运行 Docker Registry 并推送镜像
安装和运行 Docker Registry 相对简单,但在生产环境中操作还需要考虑其他非功能性要求,如安全性、可用性和可扩展性。此外,日志记录和日志处理、系统监控以及安全基础知识也是生产级系统所必需的功能。如前所述,许多提供商使用 DTR 作为生产系统的一部分。然而,Docker Registry 足以在非生产环境中使用,尤其是在内部网络环境中。
在本节中,我们将使用 Ubuntu 14.04 机器来安装、运行和测试 Docker Registry。Docker Engine 的安装过程已在第一章《Docker 入门》中介绍。我们将执行以下步骤来运行我们自己的注册表,最后推送镜像:
- 在本地主机上运行 Docker Registry:像大多数服务器一样,Docker Registry 不需要安装在运行 Docker 客户端的客户端系统上。Docker Registry 可以安装在任何支持 Docker 且可以通过网络访问的服务器上。因此,多个 Docker 客户端可以访问正在运行的 Docker Registry。
Docker Registry 在 TCP 端口 5000 上接收连接,因此不会被系统中的防火墙阻塞。
如果你向 Docker Registry 推送大量镜像,它们会很快占满空间,因此建议为存储镜像配置足够的空间。在本地文件系统中,存储路径通常是/var/lib/registry。
- 启动 Registry:以下命令从 Docker Hub 下载 Registry 镜像,并在后台启动一个容器:
$ sudo docker run -d -p 5000:5000 \
--restart=always --name registry registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
df53ce740974: Pull complete
9ce080a7bfae: Pull complete
Digest:
sha256:1cfcd718fd8a49fec9ef16496940b962e30e39
27012e851f99905db55f1f4199
Status: Downloaded newer image for registry:2
8e5c4b02a43a033ec9f6a38072f58e6b06b87570ba951b3cce5
d9a031601656e
- 检查 Docker Registry 是否在本地运行:以下命令验证 Docker Registry 是否在本地的
5000端口上运行:
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND
CREATED STATUS PORTS
NAMES
8e5c4b02a43a registry:2 "/entrypoint.sh /etc/"
3 minutes ago Up 3 minutes 0.0.0.0:5000->5000/tcp
registry
- 获取并标记镜像:最常见的测试 Docker 镜像是 Docker Hub 提供的
hello-world镜像。通过本地注册表拉取该镜像:
$ sudo docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
c04b14da8d14: Pull complete
Digest:
sha256:0256e8a36e2070f7bf2d0b0763dbabdd677985124
11de4cdcf9431a1feb60fd9
Status: Downloaded newer image for
hello-world:latest
以下命令将镜像标记为localhost:5000:
$ sudo docker tag hello-world
localhost:5000/hello-world
最后,本地机器上可用的镜像列表如下:
$ sudo docker images
REPOSITORY TAG IMAGE ID
CREATED SIZE
registry 2 541a6732eadb
2 days ago 33.3 MB
localhost:5000/hello-world latest c54a2cc56cbb
12 weeks ago 1.848 kB
hello-world latest c54a2cc56cbb
12 weeks ago 1.848 kB
- 推送镜像:现在可以将这个
hello-world镜像推送到新创建的 Docker Registry:
$ sudo docker push localhost:5000/hello-world
The push refers to a repository [localhost:5000/
hello-world]
a02596fdd012: Pushed
latest: digest:
sha256:a18ed77532f6d6781500db650194e0f9396ba5f
05f8b50d4046b294ae5f83aa4 size: 524
- 拉取镜像:现在可以从新创建的 Docker Registry 拉取这个
hello-world镜像:
$ sudo docker pull localhost:5000/hello-world
Using default tag: latest
latest: Pulling from hello-world
Digest:
sha256:a18ed77532f6d6781500db650194e0f9396ba5f0
5f8b50d4046b294ae5f83aa4
Status: Downloaded newer image for localhost:5000/
hello-world:latest
- 停止 Docker Registry 并删除:现在使用以下命令停止并删除 Docker Registry:
$ sudo docker stop registry && sudo docker \
rm -v registry
registry
registry
- 存储:Docker Registry 会将所有注册数据保存在主机文件系统的 Docker 卷中。Docker 卷可以挂载到
/var/lib/registry路径,并使用以下命令将 Docker Registry 指向此路径:
$ sudo docker run -d -p 5000:5000 \
--restart=always --name registry -v \
`pwd`/data:/var/lib/registry registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
517dc3530502: Pull complete
Digest: sha256:1cfcd718fd8a49fec9ef16496940b962e30e
3927012e851f99905db55f1f4199
Status: Downloaded newer image for registry:2
5c0ea3042397720eb487f1c3fdb9103ebb0d149421aa114a
8c5a9133f775332a
存储驱动程序可以配置为inmemory、s3、azure、swift、oss和gcs:github.com/docker/distribution/blob/master/docs/storage-drivers/index.md。
在本地使用 SSL 证书运行 Docker Registry
在本节中,我们将模拟使用 SSL 安全运行 Docker Registry 的概念。在当前的本地 Docker Registry 运行场景下,需要使用 TLS 对 Docker 引擎进行加密。
按照以下步骤安全运行 Docker Registry:
- 获取证书:我们将使用自签名证书作为 TLS 证书。首先创建
certs目录,然后运行openssl命令:
$ mkdir certs
$ openssl req -newkey rsa:4096 -nodes -sha256 \
-keyout certs/domain.key -x509 -days 365 -out \
certs/domain.crt
Generating a 4096 bit RSA private key
.....................++
........................................
.........++
writing new private key to 'certs/domain.key'
-----
You are about to be asked to enter information
that will be incorporated into your certificate
request.
What you are about to enter is what is called a
Distinguished Name or a DN.
There are quite a few fields but you can leave
some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company)
[Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name)
[]:myregistrydomain.com
Email Address []:
$
- 将
certs目录复制到 Ubuntu 16.04 中/usr/local/share/ca-certificates路径下的证书目录。该路径特定于 Ubuntu(Debian)系统,如果使用的是 Red Hat 系统,可能需要使用不同的路径:
$ sudo cp certs/domain.crt \
/usr/local/share/ca-certificates/myregistrydomain.com.crt
$ sudo update-ca-certificates
还需将domain.crt文件复制到/etc/docker/certs.d/myregistrydomain.com:5000/ca.crt。
确保在运行上述命令之前创建certs.d和myregistrydomain.com:5000目录。
- 重启 Docker 引擎:
$ sudo service docker restart
- 可以如下所示启动 Docker Registry 并以安全模式运行:
$ sudo docker run -d -p 5000:5000 \
--restart=always --name registry \
> -v `pwd`/certs:/certs
> -e REGISTRY_HTTP_TLS_CERTIFICATE=
/certs/domain.crt
> -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key
> registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
c0cb142e4345: Pull complete
a5002dfce871: Pull complete
df53ce740974: Pull complete
Digest: sha256:1cfcd718fd8a49fec9ef16496940b962e30e
3927012e851f99905db55f1f4199
Status: Downloaded newer image for registry:2
d7c41de81343313f6760c2231c037008581adf07acceea
0b3372ec2c05a5a321
$
- 现在,你应该能够从远程 Docker 主机推送镜像:
docker pull ubuntu
docker tag ubuntu myregistrydomain.com:5000/ubuntu
通过更新/etc/hosts并添加127.0.0.1 myregistrydomain.com,将myregistrydomain.com指向本地地址(127.0.0.1)。
docker push myregistrydomain.com:5000/ubuntu
docker pull myregistrydomain.com:5000/ubuntu
使用限制运行 Docker Registry
Docker Registry 的安全性至关重要。建议您将其放置在安全的防火墙和入侵保护系统(IPS)/ 入侵防御系统(IDS)后,并位于安全的网络中。此外,假定注册表将仅接受 HTTPS 上的安全连接。除了这些,Docker Registry 还可以提供访问限制,最简单的实现方式是通过基本身份验证。基本身份验证是通过使用登录名和密码进行的标准 Web 服务器身份验证:
$ mkdir auth
$ sudo docker run --entrypoint htpasswd
registry:2 -Bbn testvinod testpassword > auth/htpasswd
$
下面列出了安全访问 Docker Registry 所需的步骤:
-
由于我们在安全模式下运行此注册表,请使用自签名证书并启用 TLS。
-
同时,重启 Docker 进程以获取更新的配置。
-
现在重新运行注册表,并确保当前正在运行的注册表已停止:
$ sudo docker run -d -p 5000:5000 --restart=always \
--name registry \
> -v `pwd`/auth:/auth
> -e "REGISTRY_AUTH=htpasswd"
> -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm"
> -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
> -v `pwd`/certs:/certs
> -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt
> -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key
> registry:2
- 用户需要从远程机器登录以测试注册表的用户身份验证:
$ sudo docker login myregistrydomain.com:5000
Username: testuser
Password:testpassword
Login Succeeded
- 从远程机器推送和拉取镜像:
$ sudo docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
cad964aed91d: Pull complete
3a80a22fea63: Pull complete
Digest: sha256:28d4c5234db8d5a634d5e621c363d900f8f241240ee0a6a978784c978fe9c737
Status: Downloaded newer image for ubuntu:latest
ubuntu@ip-172-30-0-126:~$ sudo docker tag ubuntu
myregistrydomain.com:5000/ubuntu
$ sudo docker push myregistrydomain.com:5000/ubuntu
The push refers to a repository
[myregistrydomain.com:5000/ubuntu]
f215f043863e: Pushed
0c291dc95357: Pushed
latest: digest: sha256:68ae734b19b499ae57bc8d9dd4c4f90d5ff17cfe801ffbd7b840b120f d61d3b4 size: 1357
$ sudo docker rmi myregistrydomain.com:5000/ubuntu
Untagged: myregistrydomain.com:5000/ubuntu:latest
Untagged: myregistrydomain.com:5000/ubuntu@sha256:68ae734b19b499ae57bc8d9dd4c4f90d5ff17cfe801ffbd7b840b120fd61d3b4
$ sudo docker pull myregistrydomain.com:5000/ubuntu
Using default tag: latest
latest: Pulling from ubuntu
Digest: sha256:68ae734b19b499ae57bc8d9dd4c4f90d5ff17cfe801ffbd7b840b120fd61d3b4
Status: Downloaded newer image for
myregistrydomain.com:5000/ubuntu:latest
使用 Docker Compose 管理 Docker Registry
随着 Docker Registry 越来越复杂,处理其配置将变得繁琐。因此,强烈建议您使用 Docker Compose。Docker Compose 将在第八章中讨论,容器编排。
docker-compose.yml 文件的创建如下:
registry:
image: registry:2
ports:
- 5000:5000
environment:
REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
REGISTRY_HTTP_TLS_KEY: /certs/domain.key
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
volumes:
- /path/data:/var/lib/registry
- /path/certs:/certs
- /path/auth:/auth
现在,运行命令以启动注册表:
$ sudo docker-compose up -d
Creating ubuntu_registry_1
这确保 Docker Registry 再次正常运行。
负载均衡考虑
在 Docker Registry 的企业部署中,需要负载均衡器来分配负载到注册表集群。为了确保负载均衡器正确工作,我们需要确保存储驱动程序、HTTP 密钥和 Redis 缓存(如果已配置)在注册表集群中保持一致。如果这些参数有所不同,注册表将无法正常处理请求。
例如,用于 Docker 镜像的存储驱动程序应在所有注册表实例中保持一致。如果某个特定的挂载点被用作文件系统,它应该在所有注册表实例中可访问并连接。类似地,如果使用的是 S3 或 IBM 对象存储,注册表应能够访问相同的存储资源。HTTP 密钥上传坐标也必须在各个实例间保持一致。实际上,为不同的注册表实例配置不同的 Redis 缓存目前可能是可行的。然而,这并不是一个好做法,因为这会导致更多请求被重定向到后端,增加开销。
Webhook 通知
Docker Registry 内置了根据注册表活动发送通知的功能:

通知通过 HTTP 发送到各个端点。这个完整的通知基于监听器和广播器架构。每个端点都有自己的队列,所有操作(推送/拉取/删除)都会触发事件。这些事件会排队,当事件到达队列末尾时,会触发一个 HTTP 请求到端点。事件会发送到每个端点,但事件的顺序不能保证。
事件具有明确定义的 JSON 结构,并作为通知的正文发送。一个或多个事件以此结构发送,并称为 信封。一个信封可以包含一个或多个事件。Registry 也能够接收来自端点的响应。带有 2XX 或 3XX 响应码的响应被视为有效响应,并认为消息已成功发送。
Docker Registry HTTP API 支持
Docker Registry 具有与 Docker 引擎交互的 HTTP 接口。它用于管理 Docker 镜像的信息并支持镜像的分发。
从 V1 到 V2 的主要更新是 Docker 镜像格式和签名清单概念的变化。新的自包含镜像清单简化了镜像定义并提高了安全性。这个规范将在此基础上构建,利用清单格式的新属性来提高性能,减少带宽使用,并降低后端损坏的可能性。
Docker Registry V2 API 的完整文档可以在此找到:
github.com/docker/distribution/blob/master/docs/spec/api.md.
这里讨论了重要的 API:
- API 版本检查:
GET /v2/:此 API 提供基于响应状态的版本支持信息。
这是检查 Docker Registry API 版本的 curl 命令:
$ curl -i http://localhost:5000/v2/
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: application/json; charset=utf-8
Docker-Distribution-Api-Version: registry/2.0
X-Content-Type-Options: nosniff
Date: Mon, 21 Nov 2016 18:37:06 GMT
支持的错误代码有 401 Unauthorized 和 404 Not Found。
- 列出仓库:
GET /v2/_catalog:此 API 提供仓库的内容。
这是获取仓库内容的 curl 命令:
$ curl -i http://localhost:5000/v2/_catalog
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Docker-Distribution-Api-Version: registry/2.0
X-Content-Type-Options: nosniff
Date: Mon, 21 Nov 2016 18:36:42 GMT
Content-Length: 33
{"repositories":["hello-world"]}
读者可能会回忆起,在启动 Docker Registry 时,我们只上传了一个文件。
- 拉取镜像:Docker 镜像主要由两部分组成——一个 JSON 格式的清单和单独的镜像层文件。
拉取镜像清单可以使用以下 URL 获取:
GET /v2/<name>/manifests/<reference>
这是获取镜像清单详情的 curl 命令。
curl -i http://localhost:5000/v2/
hello-world/manifests/latestHTTP/1.1 200 OK
Content-Length: 2742
Content-Type: application/vnd.docker.distribution.
manifest.v1+prettyjws
Docker-Content-Digest:
sha256:f18d040ea7bf47c7ea8f7ff1a8682811cf375
51c747158e37b9c75f5450e6fac
Docker-Distribution-Api-Version: registry/2.0
Date: Mon, 21 Nov 2016 18:54:05 GMT
{
"schemaVersion": 1,
"name": "hello-world",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
{
"blobSum":
"sha256:a3ed95caeb02ffe68cdd9fd8440
6680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum":
"sha256:c04b14da8d1441880ed3fe6106fb2cc
6fa1c9661846ac0266b8a5ec8edf37b7c"
}
],
"history": [
}{
"v1Compatibility": "----
}
],
"signatures":[
{
"----------------"
}
]
}
- 拉取存储在 Blob 中的镜像层:
GET /v2/<name>/blobs/<digest>
这将是一个练习,读者可以使用在前面的拉取清单 API 中收到的 <digest> 来下载镜像。
以下表格涵盖了一些方法和 URI:
| 方法 | 路径 | 实体 | 描述 |
|---|---|---|---|
GET |
/v2/ |
基础 | 检查该端点是否实现了 Docker Registry API V2 |
GET |
/v2/<name>/tag/list |
标签 | 获取由名称标识的仓库下的标签 |
GET |
/v2/<name>/manifests/<reference> |
清单 | 获取由名称和引用标识的清单,其中引用可以是标签或 digest |
PUT |
/v2/<name>/manifests/<reference> |
清单 | 提交由名称和引用标识的清单,其中引用可以是标签或摘要 |
Delete |
/v2/<name>/manifests/<reference> |
清单 | 删除由名称和引用标识的清单,其中引用可以是标签或摘要 |
GET |
/v2/<name>/blobs/<digest> |
Blob | 从注册中心检索由摘要标识的 Blob |
DELETE |
/v2/<name>/blobs/<digest> |
Blob | 从注册中心删除由摘要标识的 Blob |
POST |
/v2/<name>/blobs/uploads |
启动 Blob 上传 | 启动一个可恢复的 Blob 上传;如果成功,将提供一个上传位置来完成上传 |
GET |
/v2/<name>/blobs/uploads/<uuid> |
Blob 上传 | 检索由 uuid 标识的上传状态 |
PATCH |
/v2/<name>/blobs/uploads/<uuid> |
Blob 上传 | 更新指定上传的一个数据块 |
PUT |
/v2/<name>/blobs/uploads/<uuid> |
Blob 上传 | 完成由 uuid 标识的上传 |
DELETE |
/v2/<name>/blobs/uploads/<uuid> |
Blob 上传 | 取消未完成的上传过程,释放相关资源 |
GET |
/v2/_catalog |
目录 | 从注册中心检索排序后的仓库 JSON 列表 |
总结
Docker 引擎允许所有增值软件解决方案被容器化、索引、注册和存储。Docker 正逐渐成为一个用于系统化开发、运输、部署和在任何地方运行容器的伟大工具。虽然 docker.io 允许你将 Docker 创建物免费上传到其注册中心,但你上传的任何内容都可以被公开发现和访问。创新者和公司对此并不热衷,因此坚持使用私有 Docker Hub。在本章中,我们以易于理解的方式解释了所有步骤、语法和语义。我们展示了如何获取镜像以生成 Docker 容器,并描述了如何以安全的方式将镜像推送到 Docker Registry,以便经过身份验证的开发人员能够找到并使用它们。身份验证和授权机制是整个过程的重要组成部分,已进行了详细解释。准确来说,本章的设计和实现是作为设置你自己的 Docker Hub 的指南。随着全球组织对容器化云的示范性兴趣,私有容器 Hub 正变得更加重要。
到此为止,我们已经理解了使用 Docker Hub、DTR 和 Docker 开源注册中心来分发和管理 Docker 镜像。Docker Hub 和 DTR 都是商业产品,将开源注册中心的功能集成到各自的解决方案中。Docker Hub 是一个多租户服务,而 DTR 和开源注册中心则为用户提供了在自己防火墙后或专用云环境中托管私有注册中心的选项。
在下一章,我们将深入探讨容器,这是从镜像开始的自然发展。我们将展示在 Docker 容器中运行服务的能力,例如 web 服务器,以及它与主机和外部世界的交互。
第三章:在容器中运行服务
到目前为止,我们已经通过仔细讲解 Docker 技术的各个方面,打下了坚实的基础。前几章无疑为广泛接受的 Docker 平台奠定了基础,接下来的章节将像在这一宏大基础上精心打造的建筑一样。
我们已经描述了构建强大 Docker 容器的重要构件(高度可用和可重用的 Docker 镜像)。有关于如何通过精心设计的存储框架存储和共享 Docker 镜像的简要说明,涵盖了各种易学易用的技巧和方法。通常,镜像必须经过一系列验证、确认和完善,以确保其对开发社区的需求具有正确性和相关性。
在这一章中,我们将通过详细介绍创建一个小型 Web 服务器的关键步骤,将我们的学习提升到一个新的层次,在容器内运行该服务器,并允许外部通过互联网连接到容器化的 Web 服务器。
在这一章中,我们将涵盖以下主题:
-
容器网络
-
容器即服务 (CaaS) – 构建、运行、暴露和连接容器服务
-
发布和检索容器的端口
-
将容器绑定到特定的 IP 地址
-
自动生成 Docker 主机端口
-
使用
EXPOSE和-P选项进行端口绑定
容器网络概述
网络是企业和云 IT 的关键基础设施组成部分。特别是随着计算变得极为分布式,网络变得不可或缺。通常,一个 Docker 主机包含多个 Docker 容器,因此网络已经成为实现复合容器化应用程序的关键组成部分。Docker 容器还需要与本地及远程容器进行交互与协作,以实现分布式应用。准确地说,不同的分布式容器需要公开可见、可网络访问和可组合,以推动面向业务和流程感知的应用程序。
Docker 容器化模式的一个关键优势是能够实现无缝网络连接,用户无需花费太多精力。Docker 的早期版本仅支持桥接网络;后来,Docker 收购了 SDN 初创公司 SocketPlane,增加了更多的网络功能。从那时起,Docker 的网络能力大幅提升,并引入了一组独立的子命令,即 docker network connect、docker network create、docker network disconnect、docker network inspect、docker network ls 和 docker network rm,用于处理 Docker 网络的细节。默认情况下,在安装时,Docker 引擎会为你创建三个网络,你可以通过 docker network ls 子命令列出它们,如下所示:

如你在前面的截图中看到的,在 Docker 设置过程中,Docker 引擎创建了 bridge、host 和 none(null)网络。当 Docker 启动一个新的容器时,默认情况下,它会为容器创建一个网络堆栈,并附加到默认的 bridge 网络。然而,你也可以选择通过 docker run 子命令的 --net 选项将容器连接到 host 或 none 网络,或者用户自定义网络。如果选择 host 网络,容器将连接到 host 网络堆栈,并共享主机的 IP 地址和端口。none 网络模式只会创建一个具有回环(lo)接口的网络堆栈。我们可以通过使用 docker run --rm --net=none busybox ip addr 命令来验证这一点,如下所示:

显然,正如你在前面的截图中看到的,容器只有一个回环接口。由于这个容器只有回环接口,它无法与其他容器或外部世界通信。
bridge 网络是 Docker 引擎分配给容器的默认网络接口,除非通过 docker run 子命令的 --net 选项配置网络。为了更好地理解 bridge 网络,我们可以使用 docker network inspect 子命令进行检查,如下所示:

在前面的截图中,我们突出了三个重要的见解。你可以找到关于 Docker 安装过程中发生的相关描述:
-
docker0:Docker 在 Linux 内核中创建了一个名为docker0的以太网桥接口。这个接口用作桥梁,将以太网帧传递在容器之间以及容器与外部网络之间。 -
子网:Docker 还从172.17.0.0到172.17.255.255的地址范围中选择了一个私有 IP 子网,并为其容器保留。前面的截图中,Docker 为容器选择了172.17.0.0/16子网。 -
网关:docker0接口是bridge网络和 Docker 的网关,Docker 会从之前选择的 IP 子网范围中分配一个 IP 地址给docker0。在前面的示例中,172.17.0.1被分配为网关地址。
我们可以通过使用 ip addr show Linux 命令列出 docker0 接口来交叉验证网关地址:
$ ip addr show docker0
输出的第三行显示了分配的 IP 地址及其网络前缀:
inet 172.17.0.1/16 scope global docker0
显然,从前面的文字中可以看出,172.17.0.1 是分配给 docker0 的 IP 地址,docker0 是以太网桥接口,它也被列为 docker network inspect bridge 命令输出中的网关地址。
现在我们对桥接器的创建以及子网/网关地址选择过程有了清晰的理解,接下来让我们更详细地探讨一下 bridge 模式下的容器网络。在 bridge 网络模式下,Docker 引擎在启动容器时会创建一个网络栈,其中包含一个环回 (lo) 接口和一个以太网 (eth0) 接口。我们可以通过运行 docker run --rm busybox ip addr 命令快速检查这一点:

显然,ip addr 命令的前述输出显示 Docker 引擎已经为容器创建了一个网络栈,并且该栈有两个网络接口,分别如下:
-
第一个接口是
lo(环回)接口,Docker 引擎为其分配了127.0.0.1的环回地址。环回接口用于容器内部的本地通信。 -
第二个接口是
eth0(以太网)接口,Docker 引擎为其分配了172.17.0.3的 IP 地址。显然,这个地址也位于docker0以太网桥接接口的相同 IP 地址范围内。此外,分配给eth0接口的地址用于容器之间的通信以及主机与容器之间的通信。
ip addr 和/或 ifconfig 命令并不是所有 Docker 镜像都支持的,包括 ubuntu:14.04 和 ubuntu:16.04。docker inspect 子命令是查找容器 IP 地址的可靠方式。
之前我们提到过,docker0 以太网桥接接口作为容器之间以及容器与外部世界之间传递以太网帧的通道。不过,我们还没有说明容器是如何连接到 docker0 桥接器的。下图揭示了这一连接的一些奥秘:

如此图所示,容器的 eth0 接口通过 veth 连接到 docker0 桥接器。eth0 和 veth 接口属于一种特殊类型的 Linux 网络接口,称为 虚拟以太网 (veth) 接口。veth 接口总是成对出现,它们就像一根水管,数据从一个 veth 接口发送出去,最终会从另一个接口出来,反之亦然。Docker 引擎将其中一个 veth 接口分配给容器并命名为 eth0,并将容器的 IP 地址分配给该接口。成对的另一个 veth 接口则绑定到 docker0 桥接器接口。这样确保了 Docker 主机与容器之间的数据流畅通无阻。
Docker 为容器分配私有 IP 地址,这些地址无法从 Docker 主机外部访问。然而,容器的 IP 地址对于在 Docker 主机内部进行调试非常有用。正如我们之前所提到的,许多 Docker 镜像不支持 ip addr 或 ifconfig 命令,此外,我们可能没有直接访问容器提示符以运行这些命令的权限。幸运的是,Docker 提供了一个 docker inspect 子命令,就像瑞士军刀一样方便,可以深入查看 Docker 容器或镜像的底层细节。docker inspect 子命令报告了很多细节,包括 IP 地址和网关地址。为了实际操作,这里你可以选择一个正在运行的容器或暂时启动一个容器,如下所示:
$ sudo docker run -itd ubuntu:16.04
这里,假设容器 ID 是 4b0b567b6019,并运行 docker inspect 子命令,如下所示:
$ sudo docker inspect 4b0b567b6019
这个命令生成了关于容器的很多信息。这里,我们展示了从 docker inspect 子命令输出中提取的一些容器网络配置片段:
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "ID removed for readability",
"EndpointID": "ID removed for readability",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:03"
}
}
以下是网络配置中一些重要字段的详细信息:
-
Gateway:这是容器的网关地址,也是bridge接口的地址。 -
IPAddress:这是分配给容器的 IP 地址。 -
IPPrefixLen:这是 IP 前缀长度,另一种表示子网掩码的方式。
毫无疑问,docker inspect 子命令非常方便,用来查找容器或镜像的细节。然而,逐一浏览那些复杂的细节并找到我们需要的信息是一项繁琐的工作。或许,你可以通过使用 grep 命令缩小范围,找到所需的信息。更好的是,docker inspect 子命令通过 --format 选项帮助你从 JSON 数组中选择正确的字段。
值得注意的是,在以下示例中,我们使用 docker inspect 子命令的 --format 选项,只获取容器的 IP 地址。IP 地址可以通过 JSON 数组中的 .NetworkSettings.IPAddress 字段访问:
$ sudo docker inspect \
--format='{{.NetworkSettings.IPAddress}}' 4b0b567b6019
172.17.0.3
除了 none、host 和 bridge 网络模式外,Docker 还支持 overlay、macvlan 和 ipvlan 网络模式。
将容器视为一种服务
我们已经打下了 Docker 技术基础的良好基础。在这一部分,我们将专注于创建一个带有 HTTP 服务的镜像,在容器内启动 HTTP 服务,并演示容器内运行的 HTTP 服务的连接性。
构建 HTTP 服务器镜像
在这一部分,我们将构建一个 Docker 镜像,用以在 Ubuntu 16.04 基础镜像上安装 Apache2,并使用 ENTRYPOINT 指令配置一个可执行的 Apache HTTP 服务器。
在第三章,《构建镜像》一节中,我们阐述了使用Dockerfile在 Ubuntu 16.04 基础镜像上构建 Apache2 镜像的概念。在此示例中,我们将通过设置 Apache 日志路径并使用ENTRYPOINT指令将 Apache2 设置为默认执行应用程序,来扩展该Dockerfile。以下是Dockerfile内容的详细说明:
我们将使用ubuntu:16.04作为基础镜像,使用FROM指令构建一个镜像,正如在Dockerfile片段中所示:
###########################################
# Dockerfile to build an apache2 image
###########################################
# Base image is Ubuntu
FROM ubuntu:16.04
使用MAINTAINER指令设置作者的详细信息:
# Author: Dr. Peter
MAINTAINER Dr. Peter <peterindia@gmail.com>
使用一个RUN指令,我们将同步 APT 仓库源列表,安装apache2包,然后清理已获取的文件:
# Install apache2 package
RUN apt-get update && \
apt-get install -y apache2 && \
apt-get clean
使用ENV指令设置 Apache 日志目录路径:
# Set the log directory PATH
ENV APACHE_LOG_DIR /var/log/apache2
现在,最后的指令是使用ENTRYPOINT指令启动apache2服务器:
# Launch apache2 server in the foreground
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
在前面的行中,你可能会惊讶地看到FOREGROUND参数。这是传统模式和容器模式之间的一个关键区别。在传统模式下,服务器应用程序通常会作为服务或守护进程在后台启动,因为主机系统是通用系统。然而,在容器模式下,必须将应用程序启动在前台,因为镜像是为单一目的而制作的。
在Dockerfile中指定了镜像构建指令后,让我们进入下一个逻辑步骤,使用docker build子命令构建镜像,并将镜像命名为apache2,如下所示:
$ sudo docker build -t apache2 .
现在让我们使用docker images子命令对镜像进行快速验证:
$ sudo docker images
正如我们在前面的章节中所看到的,docker images命令显示了 Docker 主机中所有镜像的详细信息。然而,为了准确地展示使用docker build子命令创建的镜像,我们从完整的镜像列表中突出显示了apache2:latest(目标镜像)和ubuntu:16.04(基础镜像)的详细信息,如下所示的输出片段:
apache2 latest 1b34e47c273d About a minute ago 265.5 MB
ubuntu 16.04 f753707788c5 3 weeks ago 127.2 MB
构建完 HTTP 服务器镜像后,让我们继续下一节,学习如何运行 HTTP 服务。
以服务的方式运行 HTTP 服务器镜像
在本节中,我们将使用前一节中制作的 Apache HTTP 服务器镜像启动一个容器。这里,我们使用docker run子命令的-d选项以分离模式(类似于 UNIX 守护进程)启动容器:
$ sudo docker run -d apache2
9d4d3566e55c0b8829086e9be2040751017989a47b5411c9c4f170ab865afcef
启动容器后,让我们运行docker logs子命令查看我们的 Docker 容器是否会在其标准输入(stdin)或标准错误(stderr)上生成任何输出:
$ sudo docker logs \
9d4d3566e55c0b8829086e9be2040751017989a47b5411c9c4f170ab865afcef
由于我们尚未完全配置 Apache HTTP 服务器,你将看到以下警告,作为docker logs子命令的输出:
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.13\. Set the 'ServerName' directive globally to suppress this message
从前面的警告信息中可以明显看出,分配给此容器的 IP 地址是172.17.0.13。
连接到 HTTP 服务
在前一节中,从警告信息中我们发现容器的 IP 地址是172.17.0.13。在完全配置好的 HTTP 服务器容器上,不会有这样的警告,因此我们仍然运行docker inspect子命令,使用容器 ID 来获取 IP 地址:
$ sudo docker inspect \
--format='{{.NetworkSettings.IPAddress}}'
9d4d3566e55c0b8829086e9be2040751017989a47b5411c9c4f170ab865afcef
172.17.0.13
在找到了容器的 IP 地址172.17.0.13之后,让我们快速地在 Docker 主机的 shell 提示符中通过wget命令在这个 IP 地址上发起一个 Web 请求。在这里,我们选择使用-qO -选项运行wget命令,以便在安静模式下运行,并将获取的 HTML 文件显示在屏幕上:
$ wget -qO - 172.17.0.13
在这里,我们展示的是获取到的 HTML 文件的前五行:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html >
<!--
Modified from the Debian original for Ubuntu
Last updated: 2014-03-19
很棒,对吧?我们已经在容器中运行了第一个服务,并且能够从 Docker 主机访问我们的服务。
此外,在一个普通的 Docker 安装中,一个容器提供的服务是可以被同一 Docker 主机上的任何其他容器访问的。你可以继续启动一个新的 Ubuntu 容器并以交互模式运行,使用apt-get安装wget包,然后运行与我们在 Docker 主机中相同的wget -qO - 172.17.0.13命令。当然,你会看到相同的输出。
曝露容器服务
到目前为止,我们成功启动了一个 HTTP 服务,并从 Docker 主机以及同一 Docker 主机内的其他容器访问了该服务。此外,正如在第二章《处理 Docker 容器》中的从容器构建镜像一节中所展示的那样,通过连接到公开可用的 APT 仓库并通过互联网访问,容器能够成功安装wget包。然而,默认情况下,外界无法访问容器提供的服务。乍一看,这似乎是 Docker 技术的一个限制,但实际上,容器在设计上就与外界隔离。
Docker 通过 IP 地址分配标准来实现容器的网络隔离,具体列举如下:
-
为容器分配一个私有 IP 地址,该地址无法从外部网络访问
-
为容器分配一个在主机 IP 网络之外的 IP 地址
因此,即使是连接到与 Docker 主机相同 IP 网络的系统,也无法访问 Docker 容器。这个分配方案还提供了保护,防止可能出现的 IP 地址冲突。
现在,你可能会想知道如何让容器内部运行的服务对外界可访问,换句话说,如何曝露容器服务。好吧,Docker 通过在幕后利用 Linux 的iptables功能,巧妙地弥补了这个连接性差距。
在前端,Docker 为其用户提供了两种不同的构建块,用于弥合连接性差距。其中一个构建块是使用-p(将容器端口发布到主机接口)选项,通过docker run子命令绑定容器端口。另一个替代方法是结合使用Dockerfile中的EXPOSE指令和docker run子命令的-P(将所有暴露的端口发布到主机接口)选项。
发布容器端口 - -p选项
Docker 允许您通过将容器的端口绑定到主机接口,发布容器内提供的服务。docker run子命令的-p选项使您能够将容器端口绑定到 Docker 主机的用户指定端口或自动生成端口。因此,任何发送到 Docker 主机的 IP 地址和端口的通信都会被转发到容器的端口。实际上,-p选项支持以下四种格式的参数:
-
<hostPort>:<containerPort> -
<containerPort> -
<ip>:<hostPort>:<containerPort> -
<ip>::<containerPort>
这里,<ip>是 Docker 主机的 IP 地址,<hostPort>是 Docker 主机的端口号,<containerPort>是容器的端口号。在本节中,我们向您展示-p <hostPort>:<containerPort>格式,并将在后续部分介绍其他格式。
为了更好地理解端口绑定过程,让我们重新使用之前制作的apache2 HTTP 服务器镜像,并使用docker run子命令的-p选项启动一个容器。80端口是 HTTP 服务的发布端口,作为默认行为,我们的apache2 HTTP 服务器也在端口80上可用。在这里,为了演示这个功能,我们将使用docker run子命令的-p <hostPort>:<containerPort>选项,将容器的80端口绑定到 Docker 主机的80端口,如下命令所示:
$ sudo docker run -d -p 80:80 apache2
baddba8afa98725ec85ad953557cd0614b4d0254f45436f9cb440f3f9eeae134
现在,我们已经成功启动了容器,可以通过任何外部系统的 Web 浏览器(只要该系统具有网络连接)连接到我们的 HTTP 服务器,从而访问我们的 Docker 主机。
到目前为止,我们还没有向我们的apache2 HTTP 服务器镜像添加任何网页。因此,当我们从 Web 浏览器连接时,将看到以下屏幕,这只是 Ubuntu Apache2 包随附的默认页面:

容器的 NAT
在上一节中,我们看到-p 80:80选项是如何起作用的,对吧?实际上,在幕后,Docker 引擎通过自动配置 Linux iptables配置文件中的网络地址转换(NAT)规则,实现了这种无缝连接。
为了说明 Linux iptables中 NAT 规则的自动配置,下面我们查询 Docker 主机的iptables以获取其 NAT 条目,如下所示:
$ sudo iptables -t nat -L -n
以下文本摘自由 Docker 引擎自动添加的 iptables NAT 条目:
Chain DOCKER (2 references)
target prot opt source destination
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.14:80
从前面的摘录可以明显看出,Docker 引擎已经有效地添加了一个 DNAT 规则。以下是该 DNAT 规则的详细信息:
-
tcp关键字表示此DNAT规则仅适用于 TCP 传输协议。 -
第一个
0.0.0.0/0地址是源地址的元 IP 地址。该地址表示连接可以来自任何 IP 地址。 -
第二个
0.0.0.0/0地址是 Docker 主机上目标地址的元 IP 地址。该地址表示可以连接到 Docker 主机中的任何有效 IP 地址。 -
最后,
dpt:80 to:172.17.0.14:80是转发指令,用于将 Docker 主机上的80端口的任何 TCP 活动转发到172.17.0.17IP 地址,这是我们容器的 IP 地址,并且是端口80。
因此,Docker 主机收到的任何 TCP 数据包都会转发到容器的 80 端口。
检索容器端口
Docker 引擎提供了至少三种不同的选项来检索容器的端口绑定信息。在这里,我们先探索这些选项,然后继续解析检索到的信息。选项如下:
docker ps子命令总是显示容器的端口绑定信息,如下所示:
$ sudo docker ps
CONTAINER ID IMAGE COMMAND
CREATED STATUS PORTS
NAMES
baddba8afa98 apache2:latest
"/usr/sbin/apache2ct
26 seconds ago Up 25 seconds
0.0.0.0:80->80/tcp
furious_carson
docker inspect子命令是另一种选择;但是,你需要浏览相当多的详细信息。运行以下命令:
$ sudo docker inspect baddba8afa98
-
docker inspect子命令显示与端口绑定相关的信息,分为三个 JSON 对象,如下所示: -
ExposedPorts对象枚举了通过Dockerfile中的EXPOSE指令暴露的所有端口,以及使用docker run子命令中的-p选项映射的容器端口。由于我们没有在Dockerfile中添加EXPOSE指令,因此我们只有通过-p 80:80参数传递给docker run子命令映射的容器端口:
"ExposedPorts": {
"80/tcp": {}
},
PortBindings对象是HostConfig对象的一部分,该对象列出了通过docker run子命令中的-p选项进行的所有端口绑定。此对象永远不会列出通过Dockerfile中的EXPOSE指令暴露的端口:
"PortBindings": {
"80/tcp": [
{
"HostIp": "",
"HostPort": "80"
}
]
},
NetworkSettings对象中的Ports对象具有与前述PortBindings对象相同的详细级别。然而,该对象包含了通过Dockerfile中的EXPOSE指令暴露的所有端口,以及使用docker run子命令中的-p选项映射的容器端口:
"NetworkSettings": {
"Bridge": "",
"SandboxID":"ID removed for readability",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {
"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "80"
}
]
},
当然,特定的端口字段可以使用 docker inspect 子命令的 --format 选项进行过滤。
docker port 子命令使你能够通过指定容器的端口号来获取 Docker 主机上的端口绑定信息:
$ sudo docker port baddba8afa98 80
0.0.0.0:80
显然,在所有前面的输出摘录中,突出显示的信息是0.0.0.0 IP 地址和80端口号。0.0.0.0 IP 地址是一个元地址,代表了所有在 Docker 主机上配置的 IP 地址。实际上,80容器端口绑定到了 Docker 主机上所有有效的 IP 地址。因此,HTTP 服务可以通过 Docker 主机上任何有效的 IP 地址进行访问。
将容器绑定到特定 IP 地址
到目前为止,使用你学到的方法,容器总是绑定到 Docker 主机上所有配置的 IP 地址。然而,你可能希望在不同的 IP 地址上提供不同的服务。换句话说,某个特定的 IP 地址和端口将被配置来提供特定的服务。我们可以通过 Docker 的 docker run 子命令的 -p <ip>:<hostPort>:<containerPort> 选项来实现这一点,正如以下示例所示:
$ sudo docker run -d -p 198.51.100.73:80:80 apache2
92f107537bebd48e8917ea4f4788bf3f57064c8c996fc23ea0fd8ea49b4f3335
在这里,IP 地址必须是 Docker 主机上的有效 IP 地址。如果指定的 IP 地址不是 Docker 主机上的有效 IP 地址,容器启动将失败,并出现如下错误信息:
2014/11/09 10:22:10 Error response from daemon: Cannot start container
99db8d30b284c0a0826d68044c42c370875d2c3cad0b87001b858ba78e9de53b:
Error starting user land proxy: listen tcp 10.110.73.34:49153: bind:cannot assign requested address
现在,让我们快速回顾一下前面示例中的端口映射和 NAT 条目:
- 以下文本是
docker ps子命令输出的摘录,显示了该容器的详细信息:
92f107537beb apache2:latest "/usr/sbin/apache2ct
About a minute ago Up About a minute 198.51.100.73:80->80/tcp
boring_ptolemy
- 以下文本是
iptables -n nat -L -n命令输出的摘录,显示了为该容器创建的DNAT条目:
DNAT tcp -- 0.0.0.0/0 198.51.100.73 tcp dpt:80
to:172.17.0.15:80
在查看了 docker run 子命令的输出和 iptables 的 DNAT 条目后,你会发现 Docker 引擎如何优雅地配置了容器提供的服务,绑定到了 Docker 主机上 198.51.100.73 IP 地址和 80 端口。
自动生成 Docker 主机端口
Docker 容器天生轻量级,由于其轻量化的特点,你可以在单一 Docker 主机上运行多个容器,提供相同或不同的服务。特别是,根据需求,多个容器间的同一服务自动扩展是现代 IT 基础设施的需求。在本节中,你将了解启动多个相同服务容器的挑战,以及 Docker 如何应对这一挑战。
在本章早些时候,我们通过将容器绑定到 Docker 主机的 80 端口来启动了一个使用 Apache2 HTTP 服务器的容器。现在,如果我们尝试启动一个绑定到相同 80 端口的容器,该容器将无法启动,并且会出现错误信息,正如以下示例所示:
$ sudo docker run -d -p 80:80 apache2
6f01f485ab3ce81d45dc6369316659aed17eb341e9ad0229f66060a8ba4a2d0e
2014/11/03 23:28:07 Error response from daemon: Cannot start container
6f01f485ab3ce81d45dc6369316659aed17eb341e9ad0229f66060a8ba4a2d0e:
Bind for 0.0.0.0:80 failed: port is already allocated
显然,在前面的示例中,容器启动失败是因为之前的容器已经将 0.0.0.0(Docker 主机的所有 IP 地址)和 80 端口进行了映射。在 TCP/IP 通信模型中,IP 地址、端口和传输协议(如 TCP、UDP 等)的组合必须是唯一的。
我们本可以通过手动选择 Docker 主机的端口号来解决这个问题(例如,-p 81:80 或 -p 8081:80)。虽然这是一个很好的解决方案,但对于自动扩展场景来说,它的扩展性较差。相反,如果将控制权交给 Docker,它将自动生成 Docker 主机上的端口号。通过在 docker run 子命令中使用 -p <containerPort> 选项来实现端口号的自动生成,如下例所示:
$ sudo docker run -d -p 80 apache2
ea3e0d1b18cff40ffcddd2bf077647dc94bceffad967b86c1a343bd33187d7a8
成功启动具有自动生成端口的新容器后,我们来回顾一下端口映射以及前述示例的 NAT 条目:
- 以下文本是
docker ps子命令输出的摘录,显示了此容器的详细信息:
ea3e0d1b18cf apache2:latest "/usr/sbin/apache2ct
5 minutes ago Up 5 minutes 0.0.0.0:49158->80/tcp
nostalgic_morse
- 以下文本是
iptables -n nat -L -n命令输出的摘录,显示为此容器创建的DNAT条目:
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:49158
to:172.17.0.18:80
在回顾了 docker run 子命令的输出和 iptables 的 DNAT 条目后,突出显示的是端口号 49158。该端口号是 Docker 引擎在 Docker 主机上巧妙地自动生成的,借助底层操作系统的帮助。此外,0.0.0.0 元 IP 地址表示容器提供的服务可以通过 Docker 主机上配置的任何有效 IP 地址从外部访问。
您可能有一个用例,需要自动生成端口号。然而,如果您仍然希望将服务限制为 Docker 主机的特定 IP 地址,可以使用 docker run 子命令中的 -p <IP>::<containerPort> 选项,如下例所示:
$ sudo docker run -d -p 198.51.100.73::80 apache2
6b5de258b3b82da0290f29946436d7ae307c8b72f22239956e453356532ec2a7
在前述的两种场景中,Docker 引擎自动生成了 Docker 主机上的端口号,并将其暴露给外部世界。网络通信的普遍规范是通过预定义的端口号暴露任何服务,以便任何知道 IP 地址和端口号的人都可以轻松访问提供的服务。而在这里,端口号是自动生成的,因此外部世界无法直接访问该服务。因此,这种容器创建方法的主要目的是实现自动扩展,并且以这种方式创建的容器将与预定义端口上的代理或负载均衡服务进行交互。
使用 EXPOSE 和 -P 选项进行端口绑定
到目前为止,我们已经讨论了四种将容器内部运行的服务发布到外部世界的不同方法。在这四种方法中,端口绑定的决定是在容器启动时做出的,并且镜像对服务提供的端口没有任何信息。到目前为止,这种方法运作良好,因为镜像是我们构建的,我们非常清楚服务提供的端口号。
然而,在第三方镜像的情况下,容器内的端口使用必须明确发布。此外,如果我们为第三方使用或甚至是为了我们自己的使用构建镜像,明确声明容器提供服务的端口是一个良好的实践。也许,镜像构建者可以随镜像附带一个 README 文档。然而,将端口信息嵌入镜像本身会更好,这样你可以更轻松地通过手动方式或自动化脚本从镜像中找到端口详情。
Docker 技术允许我们使用EXPOSE指令在Dockerfile中嵌入端口信息,正如我们在第三章 构建镜像中介绍的那样。在这里,让我们编辑在本章之前用于构建apache2 HTTP 服务器镜像的Dockerfile,并添加EXPOSE指令,如以下代码所示。HTTP 服务的默认端口是端口80,因此端口80被暴露:
###########################################
# Dockerfile to build an apache2 image
###########################################
# Base image is Ubuntu
FROM ubuntu:16.04
# Author: Dr. Peter
MAINTAINER Dr. Peter <peterindia@gmail.com>
# Install apache2 package
RUN apt-get update &&
apt-get install -y apache2 &&
apt-get clean
# Set the log directory PATH
ENV APACHE_LOG_DIR /var/log/apache2
# Expose port 80
EXPOSE 80
# Launch apache2 server in the foreground
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
现在我们已经在Dockerfile中添加了EXPOSE指令,让我们进入下一步,使用docker build命令来构建镜像。这里我们将重新使用之前的apache2镜像名称,如下所示:
$ sudo docker build -t apache2 .
成功构建镜像后,让我们检查一下该镜像,以验证EXPOSE指令对镜像的影响。如我们之前所学,我们可以使用docker inspect子命令,如下所示:
$ sudo docker inspect apache2
通过仔细查看前面命令生成的输出,你会发现 Docker 将暴露的端口信息存储在Config对象的ExposedPorts字段中。以下是一个摘录,展示了暴露的端口信息如何显示:
"ExposedPorts": {
"80/tcp": {}
},
另外,你也可以使用--format选项应用于docker inspect子命令,以便将输出限制为非常具体的信息。在这种情况下,Config对象的ExposedPorts字段在以下示例中展示:
$ sudo docker inspect --format='{{.Config.ExposedPorts}}' apache2
map[80/tcp:map[]]
为了继续讨论EXPOSE指令,我们现在可以使用我们刚刚制作的apache2镜像启动容器。然而,EXPOSE指令本身并不能在 Docker 主机上创建端口绑定。为了为通过EXPOSE指令声明的端口创建端口绑定,Docker 引擎在docker run子命令中提供了-P选项。
在以下示例中,容器是从之前重新构建的apache2镜像启动的。这里使用-d选项将容器以分离模式启动,-P选项用于在 Docker 主机上为所有通过Dockerfile中的EXPOSE指令声明的端口创建端口绑定:
$ sudo docker run -d -P apache2
fdb1c8d68226c384ab4f84882714fec206a73fd8c12ab57981fbd874e3fa9074
既然我们已经使用通过EXPOSE指令创建的镜像启动了新的容器,就像之前的容器一样,让我们查看端口映射以及前面示例中的 NAT 条目:
- 以下文本摘自
docker ps子命令的输出,展示了该容器的详细信息:
ea3e0d1b18cf apache2:latest "/usr/sbin/apache2ct
5 minutes ago Up 5 minutes 0.0.0.0:49159->80/tcp
nostalgic_morse
- 以下文本摘自
iptables -t nat -L -n命令的输出,展示了为该容器创建的DNAT条目:
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0
tcp dpt:49159 to:172.17.0.19:80
docker run子命令的-P选项不接受任何额外的参数,如 IP 地址或端口号;因此,无法对端口绑定进行精细调节,类似于docker run子命令的-p选项。如果端口绑定的精细调节对你至关重要,你始终可以使用docker run子命令的-p选项。
概述
容器在隔离或独立的方式下无法提供任何实质性功能。它们需要系统地构建,并配备网络接口和端口号。这些因素促成了容器向外界的标准化展示,便于其他主机或容器在任何网络上发现、绑定并利用它们的独特功能。因此,网络可访问性对容器而言至关重要,使其能够被广泛察觉并以多种方式加以利用。本章旨在展示容器如何作为一种服务进行设计和部署,以及容器网络功能如何在日新月异的容器服务领域中,发挥精准而丰富的作用。在接下来的章节中,我们将详细探讨 Docker 容器在软件密集型 IT 环境中的各种能力。
第四章:与容器共享数据
“一次只做一件事,并且做到最好”,这已经成为信息技术(IT)行业一个成功的座右铭,并且已经流行了很长时间。这一广泛使用的准则也非常适合构建和暴露 Docker 容器,并且被推荐作为获得 Docker 启发的容器化范式原本设想的好处的最佳实践之一。这意味着,我们必须将一个单一应用程序及其直接依赖和库写入一个 Docker 容器中,以确保容器的独立性、自给自足、横向可扩展性和机动性。让我们看看为什么容器如此重要:
-
容器的时间性:容器通常随着应用程序的生命周期而存在,反之亦然。然而,这对应用程序数据有一些负面影响。应用程序自然会经历各种变化,以适应业务和技术的变化,即使在生产环境中也是如此。还有其他原因,如应用程序故障、版本更替和应用程序维护,使得软件应用程序需要不断地更新和升级。在通用计算模型的情况下,即使应用程序因某种原因停止运行,与该应用程序相关的持久化数据仍然可以保存在文件系统中。然而,在容器范式下,应用程序的升级通常是通过系统地创建一个新的容器,其中包含更新版本的应用程序,并简单地丢弃旧的容器来完成的。同样,当应用程序发生故障时,需要启动一个新容器,并且必须丢弃旧的容器。总的来说,容器通常具有时间性。
-
业务连续性的需求:在容器环境中,完整的执行环境,包括其数据文件,通常被打包并封装在容器内。由于任何原因,当容器被丢弃时,应用程序的数据文件也会随容器一起消失。然而,为了提供不中断和不中断的服务,这些应用程序数据文件必须保存在容器外部,并在需要时传递到容器内,以确保业务连续性。这意味着容器的弹性和可靠性需要得到保证。此外,一些应用程序数据文件,如日志文件,需要在容器外部收集并访问,以进行各种后续分析。Docker 技术通过一种名为数据卷的新构件非常创新地解决了这个文件持久性问题。
Docker 技术提供了三种不同的持久存储方式:
-
第一个推荐的方法是使用通过 Docker 的卷管理创建的卷。
-
第二种方法是将 Docker 主机的目录挂载到容器内的指定位置。
-
另一种选择是使用数据专用容器。数据专用容器是一种特别设计的容器,用于与一个或多个容器共享数据。
本章将涵盖以下主题:
-
数据卷
-
共享主机数据
-
容器间共享数据
-
可避免的常见陷阱
数据卷
数据卷是 Docker 环境中数据共享的基础构建块。在深入了解数据共享的细节之前,必须先理解数据卷的概念。到目前为止,我们在镜像或容器中创建的所有文件都是联合文件系统的一部分。容器的联合文件系统随着容器的消失而消失。换句话说,当容器被删除时,它的文件系统也会被自动删除。然而,企业级应用程序必须持久化数据,容器的文件系统无法满足这种需求。
然而,Docker 生态系统通过数据卷概念优雅地解决了这个问题。数据卷本质上是 Docker 主机文件系统的一部分,它会被挂载到容器内。你也可以通过可插拔的卷驱动程序使用其他高级文件系统,如 Flocker 和 GlusterFS,作为数据卷。由于数据卷不是容器文件系统的一部分,它具有独立于容器的生命周期。
可以通过Dockerfile中的VOLUME指令将数据卷写入 Docker 镜像中。同时,也可以在启动容器时使用docker run子命令的-v选项指定数据卷。以下示例详细演示了Dockerfile中VOLUME指令的应用:
- 创建一个非常简单的
Dockerfile,包含基础镜像(ubuntu:16.04)和数据卷(/MountPointDemo)的指令:
FROM ubuntu:16.04
VOLUME /MountPointDemo
- 使用
docker build子命令,以mount-point-demo为名称构建镜像:
$ sudo docker build -t mount-point-demo .
- 构建完镜像后,让我们使用
docker inspect子命令快速检查镜像中的数据卷:
$ sudo docker inspect mount-point-demo
[
{
"Id": "sha256:<64 bit hex id>",
"RepoTags": [
"mount-point-demo:latest"
],
... TRUNCATED OUTPUT ...
"Volumes": {
"/MountPointDemo": {}
},
... TRUNCATED OUTPUT ...
显然,在前面的输出中,数据卷已经被记录在镜像本身中。
- 现在,让我们使用
docker run子命令从之前创建的镜像启动一个交互式容器,如下所示:
$ sudo docker run --rm -it mount-point-demo
从容器提示符下,让我们使用ls -ld命令检查数据卷的存在:
root@8d22f73b5b46:/# ls -ld /MountPointDemo
drwxr-xr-x 2 root root 4096 Nov 18 19:22
/MountPointDemo
如前所述,数据卷是 Docker 主机文件系统的一部分,它会被挂载,如下所示:
root@8d22f73b5b46:/# mount | grep MountPointDemo
/dev/xvda2 on /MountPointDemo type ext3
(rw,noatime,nobarrier,errors=remount-ro,data=ordered)
- 在这一节中,我们检查了镜像,以了解镜像中的数据卷声明。现在我们已经启动了容器,让我们使用
docker inspect子命令并在不同的终端中提供容器 ID 作为参数,检查容器的数据卷。我们之前创建了一些容器,针对这个目的,我们直接从容器的提示符中获取8d22f73b5b46容器 ID:
$ sudo docker inspect -f
'{{json .Mounts}}' 8d22f73b5b46
[
{
"Propagation": "",
"RW": true,
"Mode": "",
"Driver": "local",
"Destination": "/MountPointDemo",
"Source":
"/var/lib/docker/volumes/720e2a2478e70a7cb49ab7385b8be627d4b6ec52e6bb33063e4144355d59592a/_data",
"Name": "720e2a2478e70a7cb49ab7385b8be627d4b6ec52e6bb33063e4144355d59592a"
}
]
显然,在这里,数据卷被映射到 Docker 主机中的一个目录,并且该目录以读写模式挂载。这个目录,也称为卷,是 Docker 引擎在容器启动时自动创建的。从 Docker 1.9 版本开始,卷通过顶级卷管理命令进行管理,我们将在下一节中进一步深入探讨。
到目前为止,我们已经看到了Dockerfile中VOLUME指令的含义,以及 Docker 如何管理数据卷。像Dockerfile中的VOLUME指令一样,我们可以使用docker run子命令的-v <容器挂载点路径>选项,如下所示:
$ sudo docker run -v /MountPointDemo -it ubuntu:16.04
启动容器后,我们建议您在新启动的容器中尝试执行ls -ld /MountPointDemo和mount命令,然后按照第 5 步所示检查容器。
在这里描述的两种情况中,Docker 引擎会自动在/var/lib/docker/volumes/目录下创建卷,并将其挂载到容器。当使用docker rm子命令删除容器时,Docker 引擎不会删除在容器启动时自动创建的卷。此行为是为了保持容器应用程序在卷文件系统中存储的状态。如果您想删除 Docker 引擎自动创建的卷,可以在删除容器时通过提供-v选项给docker rm子命令来实现,这适用于已停止的容器:
$ sudo docker rm -v 8d22f73b5b46
如果容器仍在运行,则可以通过在先前命令中添加-f选项来删除容器以及自动生成的目录:
$ sudo docker rm -fv 8d22f73b5b46
我们已经向您介绍了在 Docker 主机中自动生成目录并将其挂载到容器的数据卷的技巧和提示。然而,使用docker run子命令的-v选项时,可以将用户定义的目录挂载到数据卷。在这种情况下,Docker 引擎不会自动生成任何目录。
系统生成目录存在一个潜在的目录泄漏问题。换句话说,如果忘记删除系统生成的目录,可能会遇到一些不必要的问题。有关更多信息,请阅读本章中的避免常见陷阱部分。
卷管理命令
从 1.9 版本开始,Docker 引入了一个顶级卷管理命令,以便更有效地管理持久化文件系统。该卷管理命令能够管理 Docker 主机上的数据卷。除此之外,它还帮助我们通过可插拔的卷驱动程序(如 Flocker、GlusterFS 等)扩展 Docker 的持久化能力。你可以在docs.docker.com/engine/extend/legacy_plugins/找到支持的插件列表。
docker volume命令支持以下四个子命令:
-
create:这将创建一个新的卷 -
inspect:这将显示一个或多个卷的详细信息 -
ls:这将列出 Docker 主机中的卷 -
rm:这将删除一个卷
让我们通过一些例子快速探索卷管理命令。你可以使用docker volume create子命令来创建一个卷,如下所示:
$ sudo docker volume create
50957995c7304e7d398429585d36213bb87781c53550b72a6a27c755c7a99639
上述命令将通过自动生成一个 64 位十六进制数字字符串作为卷名称来创建一个卷。然而,使用一个有意义的名称来命名卷会更有效,便于识别。你可以使用--name选项通过docker volume create子命令来命名卷:
$ sudo docker volume create --name example
example
现在,我们已经创建了两个卷,一个有卷名,一个没有卷名,让我们使用docker volume ls子命令来显示它们:
$ sudo docker volume ls
DRIVER VOLUME NAME
local 50957995c7304e7d398429585d36213bb87781c53550b72a6a27c755c7a99639
local example
列出所有卷后,让我们运行docker volume inspect子命令查看我们之前创建的卷的详细信息:
$ sudo docker volume inspect example
[
{
"Name": "example",
"Driver": "local",
"Mountpoint":
"/var/lib/docker/volumes/example/_data",
"Labels": {},
"Scope": "local"
}
]
docker volume rm子命令允许你删除不再需要的卷:
$ sudo docker volume rm example
example
现在我们已经熟悉了 Docker 卷管理,让我们深入探讨接下来的数据共享部分。
共享主机数据
之前,我们描述了如何使用Dockerfile中的VOLUME指令在 Docker 镜像中创建数据卷。然而,为了确保 Docker 镜像的可移植性,Docker 并没有提供在构建时挂载主机目录或文件的机制。Docker 唯一提供的功能是在容器启动时将主机目录或文件挂载到容器的数据卷中。Docker 通过docker run子命令的-v选项暴露了主机目录或文件挂载的功能。-v选项有五种不同的格式,如下所示:
-
-v <container mount path> -
-v <host path>:<container mount path> -
-v <host path>:<container mount path>:<read write mode> -
-v <volume name>:<container mount path> -
-v <volume name>:<container mount path>:<read write mode>
<host path>格式是 Docker 主机中的绝对路径,<container mount path>是容器文件系统中的绝对路径,<volume name>是使用docker volume create子命令创建的卷的名称,<read write mode>可以是只读(ro)或读写(rw)模式。第一个-v <container mount path>格式在本章的数据卷部分已解释过,作为启动容器时创建挂载点的方法。第二和第三个格式使我们能够将 Docker 主机中的文件或目录挂载到容器挂载点。第四和第五个格式使我们能够将使用docker volume create子命令创建的卷挂载到容器。
我们希望通过一些例子深入挖掘,进一步了解主机的数据共享。在第一个例子中,我们将演示如何在 Docker 主机和容器之间共享一个目录;在第二个例子中,我们将演示文件共享。
在这里,在第一个例子中,我们从 Docker 主机挂载一个目录到容器,执行一些基本的文件操作,并验证这些操作在 Docker 主机上,具体步骤如下所示:
- 首先,让我们使用
docker run子命令的-v选项启动一个交互式容器,将 Docker 主机的/tmp/hostdir目录挂载到容器的/MountPoint目录:
$ sudo docker run -v /tmp/hostdir:/MountPoint \
-it ubuntu:16.04
如果在 Docker 主机上找不到/tmp/hostdir,Docker 引擎将会创建该目录。然而,问题是,系统生成的目录无法通过docker rm子命令的-v选项删除。
- 成功启动容器后,我们可以使用
ls命令检查/MountPoint的存在:
root@4a018d99c133:/# ls -ld /MountPoint
drwxr-xr-x 2 root root 4096 Nov 23 18:28
/MountPoint
- 现在,我们可以继续使用
mount命令检查挂载详细信息:
root@4a018d99c133:/# mount | grep MountPoint
/dev/xvda2 on /MountPoint type ext3
(rw,noatime,nobarrier,errors=
remount-ro,data=ordered)
- 在这里,我们将验证
/MountPoint,使用cd命令切换到/MountPoint目录,使用touch命令创建一些文件,并使用ls命令列出这些文件,具体操作如下脚本所示:
root@4a018d99c133:/# cd /MountPoint/
root@4a018d99c133:/MountPoint# touch {a,b,c}
root@4a018d99c133:/MountPoint# ls -l
total 0
-rw-r--r-- 1 root root 0 Nov 23 18:39 a
-rw-r--r-- 1 root root 0 Nov 23 18:39 b
-rw-r--r-- 1 root root 0 Nov 23 18:39 c
- 使用新的终端,您可能需要验证
/tmp/hostdirDocker 主机目录中的文件,使用ls命令查看,因为我们的容器是在现有终端的交互模式下运行的:
$ sudo ls -l /tmp/hostdir/
total 0
-rw-r--r-- 1 root root 0 Nov 23 12:39 a
-rw-r--r-- 1 root root 0 Nov 23 12:39 b
-rw-r--r-- 1 root root 0 Nov 23 12:39 c
在这里,我们可以看到与第 4 步中相同的文件集。然而,您可能已经注意到文件时间戳的差异。这个时间差是由于 Docker 主机和容器之间的时区差异造成的。
- 最后,让我们运行
docker inspect子命令,并将4a018d99c133容器 ID 作为参数,查看是否设置了 Docker 主机和容器挂载点之间的目录映射,具体操作如下命令所示:
$ sudo docker inspect \
--format='{{json .Mounts}}' 4a018d99c133
[{"Source":"/tmp/hostdir",
"Destination":"/MountPoint","Mode":"",
"RW":true,"Propagation":"rprivate"}]
显然,在前面的docker inspect子命令输出中,Docker 主机的/tmp/hostdir目录被挂载到容器的/MountPoint挂载点上。
在第二个示例中,我们将从 Docker 主机挂载一个文件到容器,更新容器中的文件,并按照以下步骤从 Docker 主机验证这些操作:
- 为了将文件从 Docker 主机挂载到容器中,该文件必须先在 Docker 主机上存在。否则,Docker 引擎会创建一个指定名称的新目录,并将其作为目录挂载。我们可以通过使用
touch命令在 Docker 主机上创建一个文件来开始:
$ touch /tmp/hostfile.txt
- 使用
docker run子命令的-v选项启动一个交互式容器,将 Docker 主机上的/tmp/hostfile.txt文件挂载到容器的/tmp/mntfile.txt:
$ sudo docker run -v /tmp/hostfile.txt:/mntfile.txt \
-it ubuntu:16.04
- 成功启动容器后,现在让我们使用
ls命令检查/mntfile.txt的存在:
root@d23a15527eeb:/# ls -l /mntfile.txt
-rw-rw-r-- 1 1000 1000 0 Nov 23 19:33 /mntfile.txt
- 然后,使用
mount命令继续检查挂载详细信息:
root@d23a15527eeb:/# mount | grep mntfile
/dev/xvda2 on /mntfile.txt type ext3
(rw,noatime,nobarrier,errors=remount-ro,data=ordered)
- 然后,使用
echo命令将一些文本更新到/mntfile.txt:
root@d23a15527eeb:/# echo "Writing from Container"
> mntfile.txt
- 同时,切换到 Docker 主机的另一个终端,使用
cat命令打印/tmp/hostfile.txt的内容:
$ cat /tmp/hostfile.txt
Writing from Container
- 最后,使用
docker inspect子命令,并以d23a15527eeb容器 ID 作为参数,查看 Docker 主机与容器挂载点之间的文件映射:
$ sudo docker inspect \
--format='{{json .Mounts}}' d23a15527eeb
[{"Source":"/tmp/hostfile.txt",
"Destination":"/mntfile.txt",
"Mode":"","RW":true,"Propagation":"rprivate"}]
从前面的输出可以看出,Docker 主机中的/tmp/hostfile.txt文件已作为/mntfile.txt挂载到容器内。
对于最后一个示例,我们将创建一个 Docker 卷并将命名数据卷挂载到容器中。在这个示例中,我们不会像前两个示例那样进行验证步骤。然而,建议你按照第一个示例中的验证步骤进行操作。
- 使用
docker volume create子命令创建一个命名数据卷,如下所示:
$ docker volume create --name namedvol
- 现在,使用
docker run子命令的-v选项启动一个交互式容器,将名为namedvol的数据卷挂载到容器的/MountPoint:
$ sudo docker run -v namedvol:/MountPoint \
-it ubuntu:16.04
在容器启动过程中,若namedvol还未创建,Docker 引擎会创建它。
- 成功启动容器后,你可以重复第一示例中第 2 到第 6 步的验证步骤,你会发现这个示例也会有相同的输出模式。
主机数据共享的实用性
在上一章中,我们在 Docker 容器中启动了一个 HTTP 服务。然而,如果你记得的话,HTTP 服务的日志文件仍然在容器内部,无法直接从 Docker 主机访问。在本节中,我们将逐步阐明从 Docker 主机访问日志文件的过程:
- 让我们从启动一个 Apache2 HTTP 服务容器开始,通过使用
docker run子命令的-v选项,将 Docker 主机上的/var/log/myhttpd目录挂载到容器的/var/log/apache2目录。在这个示例中,我们利用了在上一章中构建的apache2镜像,执行以下命令:
$ sudo docker run -d -p 80:80 \
-v /var/log/myhttpd:/var/log/apache2 apache2
9c2f0c0b126f21887efaa35a1432ba7092b69e0c6d523ffd50684e27eeab37ac
如果你回忆起第六章中的Dockerfile,在容器中运行服务,APACHE_LOG_DIR环境变量通过ENV指令设置为/var/log/apache2目录。这将使得 Apache2 HTTP 服务将所有日志信息路由到/var/log/apache2数据卷。
- 一旦容器启动,我们可以切换到 Docker 主机上的
/var/log/myhttpd目录:
$ cd /var/log/myhttpd
- 或许,这里快速检查一下
/var/log/myhttpd目录中的文件是合适的:
$ ls -1
access.log
error.log
other_vhosts_access.log
在这里,access.log文件包含了所有由 Apache2 HTTP 服务器处理的访问请求。error.log文件是一个非常重要的日志文件,记录了我们的 HTTP 服务器在处理任何 HTTP 请求时遇到的错误。other_vhosts_access.log文件是虚拟主机日志,在我们的案例中,它将始终为空。
- 我们可以使用带有
-f选项的tail命令来显示/var/log/myhttpd目录中所有日志文件的内容:
$ tail -f *.log
==> access.log <==
==> error.log <==
AH00558: apache2: Could not reliably determine the
server's fully qualified domain name, using 172.17.0.17\.
Set the 'ServerName' directive globally to suppress this
message
[Thu Nov 20 17:45:35.619648 2014] [mpm_event:notice]
[pid 16:tid 140572055459712] AH00489: Apache/2.4.7
(Ubuntu) configured -- resuming normal operations
[Thu Nov 20 17:45:35.619877 2014] [core:notice]
[pid 16:tid 140572055459712] AH00094: Command line:
'/usr/sbin/apache2 -D FOREGROUND'
==> other_vhosts_access.log <==
tail -f命令将持续运行,并显示文件的内容,一旦它们被更新。在这里,access.log和other_vhosts_access.log都为空,而error.log文件中有一些错误信息。显然,这些错误日志是由容器内运行的 HTTP 服务生成的。日志随后被存储在 Docker 主机目录中,该目录在容器启动时被挂载。
- 在继续运行
tail -f *时,让我们从容器内部的网页浏览器连接到 HTTP 服务,并观察日志文件:
==> access.log <==
111.111.172.18 - - [20/Nov/2014:17:53:38 +0000] "GET /
HTTP/1.1" 200 3594 "-" "Mozilla/5.0 (Windows NT 6.1;
WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65
Safari/537.36"
111.111.172.18 - - [20/Nov/2014:17:53:39 +0000] "GET
/icons/ubuntu-logo.png HTTP/1.1" 200 3688
"http://111.71.123.110/" "Mozilla/5.0 (Windows NT 6.1;
WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65
Safari/537.36"
111.111.172.18 - - [20/Nov/2014:17:54:21 +0000] "GET
/favicon.ico HTTP/1.1" 404 504 "-" "Mozilla/5.0 (Windows
NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/39.0.2171.65 Safari/537.36"
HTTP 服务会更新access.log文件,我们可以通过docker run子命令的-v选项从挂载的主机目录进行操作。
在容器之间共享数据
在前一节中,你学到了 Docker 引擎如何无缝地实现 Docker 主机和容器之间的数据共享。尽管这是大多数使用案例的非常有效的解决方案,但也有一些使用场景需要在一个或多个容器之间共享数据。为了解决这种情况,Docker 提供的方案是使用docker run子命令的--volume-from选项,将一个容器的数据卷挂载到其他容器中。
仅数据容器
在 Docker 引入顶层卷管理功能之前,数据专用容器是实现数据持久性的推荐方法。了解数据专用容器是值得的,因为你会发现许多基于数据专用容器的实现。数据专用容器的主要职责是保留数据。创建数据专用容器与数据卷部分中展示的方法非常相似。此外,容器会显式命名,方便其他容器通过容器名称挂载数据卷。而且,即使数据专用容器处于停止状态,容器的数据卷仍然可以被其他容器访问。数据专用容器可以通过两种方式创建,如下所示:
-
在启动容器时,通过配置数据卷和容器的名称来实现。
-
数据卷也可以在构建镜像时通过
Dockerfile进行配置,之后容器启动时可以指定容器名称。
在以下示例中,我们通过配置docker run子命令的-v和--name选项来启动数据专用容器,如下所示:
$ sudo docker run --name datavol \
-v /DataMount \
busybox:latest /bin/true
这里,容器是从busybox镜像启动的,该镜像因其较小的占用空间而广泛使用。在此,我们选择执行/bin/true命令,因为我们不打算在容器中进行任何操作。因此,我们使用--name选项将容器命名为datavol,并使用docker run子命令的-v选项创建了一个新的/DataMount数据卷。/bin/true命令立即以0退出状态退出,从而停止容器,并继续保持停止状态。
从其他容器挂载数据卷
Docker 引擎提供了一个便捷的接口,用于将一个容器的数据卷挂载(共享)到另一个容器。Docker 通过docker run子命令的--volumes-from选项提供这个接口。--volumes-from选项接受容器名称或容器 ID 作为输入,并自动挂载指定容器上的所有数据卷。Docker 允许你使用--volumes-from选项多次挂载多个容器的数据卷。
这是一个实际示例,展示了如何从另一个容器挂载数据卷,并一步步演示数据卷挂载过程:
- 我们首先启动一个交互式的 Ubuntu 容器,并从上节中启动的数据专用容器(
datavol)挂载数据卷:
$ sudo docker run -it \
--volumes-from datavol \
ubuntu:latest /bin/bash
- 现在,从容器的提示符开始,让我们使用
mount命令验证数据卷挂载:
root@e09979cacec8:/# mount | grep DataMount
/dev/xvda2 on /DataMount type ext3
(rw,noatime,nobarrier,errors=remount-ro,data=ordered)
在这里,我们成功地从datavol数据专用容器挂载了数据卷。
- 接下来,我们需要使用
docker inspect子命令,从另一个终端检查此容器的数据卷:
$ sudo docker inspect --format='{{json .Mounts}}'
e09979cacec8
[{"Name":
"7907245e5962ac07b31c6661a4dd9b283722d3e7d0b0fb40a90
43b2f28365021","Source":
"/var/lib/docker/volumes
/7907245e5962ac07b31c6661a4dd9b283722d3e7d0b0fb40a9043b
2f28365021/_data","Destination":"
/DataMount","Driver":"local","Mode":"",
"RW":true,"Propagation":""}]
显然,来自datavol数据-only 容器的数据卷被挂载,就像它们直接挂载到此容器一样。
我们可以从另一个容器挂载数据卷,并展示挂载点。通过使用数据卷在容器之间共享数据,我们可以使挂载的数据显示工作,正如这里所示:
- 让我们重用在前一个示例中启动的容器,并通过向文件中写入一些文本,在
/DataMount数据卷中创建/DataMount/testfile文件,如下所示:
root@e09979cacec8:/# echo \
"Data Sharing between Container" > \
/DataMount/testfile
- 只需启动一个容器,使用
cat命令显示我们在上一步中写入的文本:
$ sudo docker run --rm \
--volumes-from datavol \
busybox:latest cat /DataMount/testfile
以下是前述命令的典型输出:
Data Sharing between Container
显然,前述容器之间的数据共享输出来自我们新创建的容器化cat命令,它是我们在步骤 1 中写入datavol容器/DataMount/testfile的文本。
酷吧?你可以通过共享数据卷无缝地在容器之间共享数据。在这个示例中,我们使用数据-only 容器作为数据共享的基础容器。然而,Docker 允许我们共享任何类型的数据卷,并将数据卷一个接一个地挂载,正如这里所示:
$ sudo docker run --name vol1 --volumes-from datavol \
busybox:latest /bin/true
$ sudo docker run --name vol2 --volumes-from vol1 \
busybox:latest /bin/true
在vol1容器中,我们从datavol容器挂载了数据卷。然后,在vol2容器中,我们从vol1容器挂载了数据卷,而该数据卷最终来自datavol容器。
容器之间数据共享的实用性
在本章前面,你学习了如何从 Docker 主机访问 Apache2 HTTP 服务的日志文件。尽管通过挂载 Docker 主机目录到容器来共享数据非常方便,但后来我们了解到,容器之间可以通过仅使用数据卷来共享数据。因此,在这里,我们通过容器之间共享数据来改变 Apache2 HTTP 服务日志处理的方法。为了在容器之间共享日志文件,我们将根据以下步骤启动以下容器:
-
首先,创建一个数据-only 容器,将数据卷暴露给其他容器。
-
然后,一个利用数据-only 容器数据卷的 Apache2 HTTP 服务容器。
-
一个用于查看由 Apache2 HTTP 服务生成的日志文件的容器。
如果你在 Docker 主机的80端口运行任何 HTTP 服务,请为以下示例选择一个未使用的端口。如果没有,请首先停止 HTTP 服务,然后继续示例,以避免端口冲突。
现在,我们将细致地带你一步步完成制作相应镜像并启动容器以查看日志文件的过程:
- 在这里,我们开始使用
VOLUME指令在Dockerfile中创建/var/log/apache2数据卷。这个/var/log/apache2数据卷直接映射到APACHE_LOG_DIR,这是在第六章中通过ENV指令设置的环境变量,在容器中运行服务:
#######################################################
# Dockerfile to build a LOG Volume for Apache2 Service
#######################################################
# Base image is BusyBox
FROM busybox:latest
# Author: Dr. Peter
MAINTAINER Dr. Peter <peterindia@gmail.com>
# Create a data volume at /var/log/apache2, which is
# same as the log directory PATH set for the apache image
VOLUME /var/log/apache2
# Execute command true
CMD ["/bin/true"]
由于这个 Dockerfile 是为了启动仅数据容器而设计的,因此默认执行命令被设置为 /bin/true。
- 我们将继续使用
docker build从前面的Dockerfile构建一个名为apache2log的 Docker 镜像,如下所示:
$ sudo docker build -t apache2log .
Sending build context to Docker daemon 2.56 kB
Sending build context to Docker daemon
Step 0 : FROM busybox:latest
... TRUNCATED OUTPUT ...
- 使用
docker run子命令从apache2log镜像启动一个仅数据容器,并使用--name选项将生成的容器命名为log_vol:
$ sudo docker run --name log_vol apache2log
执行前述命令后,容器将在 /var/log/apache2 中创建一个数据卷,并将其移动到停止状态。
- 与此同时,您可以使用
docker ps子命令并加上-a选项来验证容器的状态:
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND
CREATED STATUS PORTS
NAMES
40332e5fa0ae apache2log:latest "/bin/true"
2 minutes ago Exited (0) 2 minutes ago
log_vol
根据输出,容器以 0 的退出值退出。
- 使用
docker run子命令启动 Apache2 HTTP 服务。在这里,我们将重用在第六章中构建的apache2镜像,在容器中运行服务。此外,在这个容器中,我们将使用--volumes-from选项从第 3 步启动的仅数据容器log_vol挂载/var/log/apache2数据卷:
$ sudo docker run -d -p 80:80 \
--volumes-from log_vol \
apache2
7dfbf87e341c320a12c1baae14bff2840e64afcd082dda3094e7cb0a0023cf42
在成功启动带有从 log_vol 挂载的 /var/log/apache2 数据卷的 Apache2 HTTP 服务后,我们可以通过临时容器访问日志文件。
- 在这里,我们列出了由 Apache2 HTTP 服务存储的文件,使用了一个临时容器。这个临时容器通过挂载来自
log_vol的/var/log/apache2数据卷来启动,并且使用ls命令列出了/var/log/apache2中的文件。此外,docker run子命令的--rm选项用于在执行完ls命令后移除容器:
$ sudo docker run --rm \
--volumes-from log_vol \
busybox:latest ls -l /var/log/apache2
total 4
-rw-r--r-- 1 root root 0 Dec 5 15:27
access.log
-rw-r--r-- 1 root root 461 Dec 5 15:27
error.log
-rw-r--r-- 1 root root 0 Dec 5 15:27
other_vhosts_access.log
- 最后,使用
tail命令访问 Apache2 HTTP 服务生成的错误日志,命令如下所示:
$ sudo docker run --rm \
--volumes-from log_vol \
ubuntu:16.04 \
tail /var/log/apache2/error.log
AH00558: apache2: Could not reliably determine the
server's fully qualified domain name, using 172.17.0.24\.
Set the 'ServerName' directive globally to suppress this
message
[Fri Dec 05 17:28:12.358034 2014] [mpm_event:notice]
[pid 18:tid 140689145714560] AH00489: Apache/2.4.7
(Ubuntu) configured -- resuming normal operations
[Fri Dec 05 17:28:12.358306 2014] [core:notice]
[pid 18:tid 140689145714560] AH00094: Command line:
'/usr/sbin/apache2 -D FOREGROUND'
避免常见的陷阱
到目前为止,我们已经讨论了数据卷如何有效地用于在 Docker 主机与容器之间、以及容器之间共享数据。使用数据卷进行数据共享正变得越来越强大且在 Docker 模式中不可或缺。然而,它也带来了一些需要小心识别并消除的陷阱。在本节中,我们尝试列出一些与数据共享相关的常见问题,并提出解决方法。
目录泄漏
在数据卷章节中,你已了解 Docker 引擎会根据Dockerfile中的VOLUME指令以及docker run子命令的-v选项自动创建目录。我们还明白,Docker 引擎不会自动删除这些自动生成的目录,以保持容器中运行的应用程序的状态。我们可以使用docker rm子命令的-v选项强制 Docker 删除这些目录。手动删除过程面临两大挑战,分别如下:
-
未删除的目录: 可能出现某些情况下,你可能有意或无意选择不删除生成的目录,即使在删除容器时。
-
第三方镜像: 我们经常利用可能已使用
VOLUME指令构建的第三方 Docker 镜像。同样,我们也可能拥有自己带有VOLUME指令的 Docker 镜像。当我们使用这些 Docker 镜像启动容器时,Docker 引擎会自动生成所需的目录。由于我们无法预知数据卷的创建,因此可能无法使用docker rm子命令的-v选项删除这些自动生成的目录。
在前述场景中,一旦关联的容器被删除,就没有直接方法来识别已删除容器的目录。以下是一些避免此问题的建议:
-
始终使用
docker inspect子命令检查 Docker 镜像,并检查镜像中是否包含任何数据卷。 -
始终使用
docker rm子命令的-v选项删除为容器创建的任何数据卷(目录)。即使数据卷被多个容器共享,使用docker rm子命令的-v选项仍然是安全的,因为与数据卷关联的目录仅会在最后一个共享该数据卷的容器被删除时才会被删除。 -
如果出于某种原因,你选择保留自动生成的目录,你必须保持清晰的记录,以便稍后能够删除它们。
-
实现一个审计框架,用于审计并找出与任何容器无关的目录。
数据卷的副作用
如前所述,Docker 通过在构建时使用VOLUME指令使我们能够访问 Docker 镜像中的每个数据卷。然而,数据卷不应在构建时用于存储任何数据,否则将导致不良效果。
本节中,我们将通过编写一个Dockerfile展示在构建过程中使用数据卷的副作用,并通过构建此Dockerfile来展示其影响。
以下是Dockerfile的详细信息:
- 使用 Ubuntu 16.04 作为基础镜像构建镜像:
# Use Ubuntu as the base image
FROM ubuntu:16.04
- 使用
VOLUME指令创建/MountPointDemo数据卷:
VOLUME /MountPointDemo
- 使用
RUN指令在/MountPointDemo数据卷中创建一个文件:
RUN date > /MountPointDemo/date.txt
- 使用
RUN指令显示/MountPointDemo数据卷中的文件:
RUN cat /MountPointDemo/date.txt
- 使用
docker build子命令从这个Dockerfile构建镜像,如下所示:
$ sudo docker build -t testvol .
Sending build context to Docker daemon 2.56 kB
Sending build context to Docker daemon
Step 0 : FROM ubuntu:16.04
---> 9bd07e480c5b
Step 1 : VOLUME /MountPointDemo
---> Using cache
---> e8b1799d4969
Step 2 : RUN date > /MountPointDemo/date.txt
---> Using cache
---> 8267e251a984
Step 3 : RUN cat /MountPointDemo/date.txt
---> Running in a3e40444de2e
cat: /MountPointDemo/date.txt: No such file or directory
2014/12/07 11:32:36 The command [/bin/sh -c cat
/MountPointDemo/date.txt] returned a non-zero code: 1
在docker build子命令的前面输出中,你会注意到构建在第 3 步失败,因为它找不到第 2 步中创建的文件。显然,第 2 步中创建的文件在到达第 3 步时消失了。这个不希望发生的效果是由于 Docker 构建镜像的方式造成的。理解 Docker 的镜像构建过程可以揭开这个谜团。
在构建过程中,对于Dockerfile中的每一条指令,遵循以下步骤:
-
通过将
Dockerfile指令转换为等效的docker run子命令来创建一个新容器。 -
将新创建的容器提交为镜像。
-
将第 1 步和第 2 步重复,处理新创建的镜像作为第 1 步的基础镜像。
当一个容器被提交时,它会保存容器的文件系统,并故意不保存数据卷的文件系统。因此,存储在数据卷中的任何数据将在此过程中丢失。所以,在构建过程中,绝对不要使用数据卷作为存储。
概要
对于企业级分布式应用程序来说,数据是最重要的工具和组成部分,使得其操作和输出独具特色。借助 IT 容器化,整个过程以快速且明亮的方式开始。通过智能利用 Docker 引擎,IT 和商业软件解决方案被高效容器化。然而,最初的动因是需要更快且无误地实现具备应用感知的 Docker 容器,因此数据与容器内的应用紧密耦合。然而,这种紧密关系也带来了实际的风险。如果应用崩溃,数据也会丢失。此外,多个应用可能依赖于相同的数据,因此数据必须在多个应用之间共享。
本章我们讨论了 Docker 引擎在促进 Docker 主机与容器之间以及容器之间无缝共享数据的能力。数据卷被规定为促进日益增长的 Docker 生态系统中各个组成部分之间共享数据的基础构建块。在下一章,我们将解释容器编排的概念,并看看如何通过一些自动化工具简化这一复杂的方面。编排对于实现复合容器是必不可少的。
第五章:容器编排
在前面的章节中,我们已经为容器网络的需求打下了坚实的基础,讨论了如何在 Docker 容器中运行服务,以及如何通过开放网络端口和其他前提条件将该服务暴露给外部世界。然而,最近,出现了一些先进的机制,并且一些第三方编排平台也进入了市场,旨在巧妙地建立分布式且功能各异的容器之间的动态和决定性连接,以便为处理中心、分层和企业级分布式应用程序组合出强大的容器。在这个高度多样化但又紧密相连的世界里,容器编排的概念不可能长期被忽视。本章正是为了详细解释容器编排的细节,其直接作用是从一组离散的容器中系统地组合出更符合不同业务需求和期望的复杂容器。
在本章中,我们将详细讨论以下主题:
-
连接容器
-
容器编排
-
使用
docker-compose工具编排容器
随着关键任务应用程序越来越多地通过松散耦合、但高度凝聚的组件/服务构建,并且这些组件/服务将在地理分布的 IT 基础设施和平台上运行,组合的概念受到了越来越多的关注和重视。为了保持容器化进程的顺利推进,容器编排被视为在随之而来的即时启动、适应性强且智能的 IT 时代中的关键要求之一。现在有一些经过验证的、有前景的方法和符合标准的工具,用于实现这个神秘的编排目标。
Docker 内置的服务发现
Docker 平台本身支持通过嵌入式域名服务(DNS)为附加到任何用户定义网络的容器提供服务发现功能。自1.10版本以来,Docker 就加入了此功能。嵌入式 DNS 功能使得 Docker 容器能够通过它们的名称或别名在用户定义的网络中互相发现。换句话说,容器的名称解析请求首先发送到嵌入式 DNS。用户定义的网络随后使用一个特殊的127.0.0.11 IP 地址为嵌入式 DNS 提供服务,这个地址也列在/etc/resolv.conf中。
以下示例将帮助更好地理解 Docker 内置的服务发现功能:
- 让我们首先通过以下命令创建一个用户定义的桥接网络
mybridge:
$ sudo docker network create mybridge
- 检查新创建的网络以了解子网范围和网关 IP:
$ sudo docker network inspect mybridge
[
{
"Name": "mybridge",
"Id": "36e5e088543895f6d335eb92299ee8e118cd0610e0d023f7c42e6e603b935e17",
"Created":
"2017-02-12T14:56:48.553408611Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
在这里,mybridge网络分配的子网是172.18.0.0/16,网关是172.18.0.1。
- 现在,让我们创建一个容器并将其连接到
mybridge网络,如下所示:
$ sudo docker container run \
-itd --net mybridge --name testdns ubuntu
- 继续列出分配给容器的 IP 地址,如下所示:
$ sudo docker container inspect --format \
'{{.NetworkSettings.Networks.mybridge.IPAddress}}' \
testdns
172.18.0.2
显然,testdns 容器被分配了一个172.18.0.2的 IP 地址。这个172.18.0.2 IP 地址来自 mybridge 网络的子网(即172.18.0.0/16)。
- 获得容器的 IP 地址后,让我们使用
docker container exec子命令查看容器的/etc/resolv.conf文件内容,如下所示:
$ sudo docker container exec testdns \
cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0
在这里,nameserver 配置为 127.0.0.11,这是嵌入式 DNS 的 IP 地址。
- 最后一步,让我们使用
busybox镜像来 pingtestdns容器。我们选择busybox镜像,因为ubuntu镜像不包含ping命令:
$ sudo docker container run --rm --net mybridge \
busybox ping -c 2 testdns
PING testdns (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64
time=0.085 ms
64 bytes from 172.18.0.2: seq=1 ttl=64
time=0.133 ms
--- testdns ping statistics ---
2 packets transmitted, 2 packets received,
0% packet loss
round-trip min/avg/max = 0.085/0.109/0.133 ms
太棒了,不是吗!Docker 背后的开发团队将其做得如此简单,以至于我们几乎无需任何努力,就能发现同一网络中的容器。
链接容器
在引入用户定义网络的概念之前,容器链接主要用于容器间的发现和通信。也就是说,协作容器可以相互链接,提供复杂且具业务意识的服务。链接的容器之间具有某种源-目标关系,其中源容器会链接到目标容器,而目标容器会安全地接收来自源容器的各种信息。然而,源容器对其链接的目标容器一无所知。另一个值得注意的特点是,在安全设置下,链接容器可以通过安全隧道进行通信,而不暴露设置所使用的端口给外部世界。虽然你会发现许多部署使用容器链接技术,但它们配置繁琐且耗时,同时也容易出错。因此,嵌入式 DNS 的新方法被高度偏好,胜过传统的容器链接技术。
Docker 引擎在 docker run 子命令中提供了 --link 选项,用于将源容器与目标容器连接起来。
--link 选项的格式如下:
--link <container>:<alias>
在这里,<container> 是源容器的名称,<alias> 是目标容器看到的名称。容器的名称在 Docker 主机中必须是唯一的,而别名非常具体并且局限于目标容器,因此别名在 Docker 主机中不必唯一。这为在目标容器中实现和集成功能提供了很大的灵活性,可以使用固定的源别名名称。
当两个容器被链接在一起时,Docker 引擎会自动将一些环境变量导出到目标容器。这些环境变量有明确的命名规则,变量名称总是以别名的大写形式作为前缀。例如,如果src是源容器的别名,那么导出的环境变量将以SRC_开头。Docker 导出三类环境变量,如下所示:
-
NAME:这是第一类环境变量。这些变量的形式为<ALIAS>_NAME,其值为目标容器的层次名称。例如,如果源容器的别名是src,目标容器的名称是rec,则环境变量及其值为SRC_NAME=/rec/src。 -
ENV:这是第二类环境变量,用于导出源容器中通过docker run子命令的-e选项或Dockerfile的ENV指令配置的环境变量。这类环境变量的形式为<ALIAS>_ENV_<VAR_NAME>。例如,如果源容器的别名是src,变量名是SAMPLE,则环境变量为SRC_ENV_SAMPLE。 -
PORT:这是第三类也是最后一类环境变量,用于导出源容器的连接详细信息到目标容器。Docker 会为源容器通过docker run子命令的-p选项或Dockerfile的EXPOSE指令暴露的每个端口创建一组变量。
这些变量采用<ALIAS>_PORT_<port>_<protocol>的形式。该形式用于将源容器的 IP 地址、端口和协议作为 URL 共享。例如,如果源容器的别名是src,暴露的端口是8080,协议是tcp,IP 地址是172.17.0.2,那么环境变量及其值将是SRC_PORT_8080_TCP=tcp://172.17.0.2:8080。这个 URL 进一步分解为以下三个环境变量:
-
<ALIAS>_PORT_<port>_<protocol>_ADDR:这种形式携带 URL 中的 IP 地址部分(例如,SRC_PORT_8080_TCP_ADDR=172.17.0.2)。 -
<ALIAS>_PORT_<port>_<protocol>_PORT:这种形式携带 URL 中的端口部分(例如,SRC_PORT_8080_TCP_PORT=8080)。 -
<ALIAS>_PORT_<port>_<protocol>_PROTO:这种形式携带 URL 中的协议部分(例如,SRC_PORT_8080_TCP_PROTO=tcp)。
除了前面的环境变量之外,Docker Engine 还会导出一个以<ALIAS>_PORT形式的环境变量,其值将是源容器所有暴露端口中最低编号端口的 URL。例如,如果源容器的别名是src,暴露的端口号是7070、8080和80,协议是tcp,IP 地址是172.17.0.2,那么环境变量及其值将是SRC_PORT=tcp://172.17.0.2:80。
Docker 以结构化的格式导出这些自动生成的环境变量,使得它们可以轻松地通过程序进行发现。因此,接收容器可以非常容易地发现关于源容器的信息。此外,Docker 会自动更新源 IP 地址及其别名,并将其作为条目添加到接收容器的/etc/hosts文件中。
在本章中,我们将深入探讨 Docker Engine 为容器链接提供的功能,并通过一系列实际示例进行说明。
首先,我们选择一个简单的容器链接示例。在这里,我们将向您展示如何在两个容器之间建立链接,并将一些基本信息从源容器传输到接收容器,具体步骤如下所示:
- 我们从启动一个可用于链接的交互式容器开始,使用以下命令:
$ sudo docker run --rm --name example -it \
busybox:latest
容器使用--name选项命名为example。此外,使用--rm选项可以在退出容器后立即清理容器。
- 使用
cat命令显示源容器的/etc/hosts条目:
/ # cat /etc/hosts
172.17.0.3 a02895551686
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
这里,/etc/hosts文件中的第一条条目是源容器的 IP 地址(172.17.0.3)及其主机名(a02895551686)。
- 我们将继续使用
env命令显示源容器的环境变量:
/ # env
HOSTNAME=a02895551686
SHLVL=1
HOME=/root
TERM=xterm
PATH=
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
- 我们现在已经启动了源容器。在同一 Docker 主机的另一个终端中,我们将启动交互式接收容器,并通过
docker run子命令的--link选项将其与我们的源容器链接,如下所示:
$ sudo docker run --rm --link example:ex \
-it busybox:latest
这里,名为example的源容器通过ex作为别名与接收容器链接。
- 使用
cat命令显示接收容器的/etc/hosts文件内容:
/ # cat /etc/hosts
172.17.0.4 a17e5578b98e
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
72.17.0.3 ex
当然,与往常一样,/etc/hosts文件中的第一条条目是容器的 IP 地址及其主机名。然而,/etc/hosts文件中值得注意的条目是最后一条,其中源容器的 IP 地址(172.17.0.3)及其别名(ex)会自动添加。
- 我们将继续使用
env命令显示接收容器的环境变量:
/ # env
HOSTNAME=a17e5578b98e
SHLVL=1
HOME=/root
EX_NAME=/berserk_mcclintock/ex
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
显然,一个新的EX_NAME环境变量自动添加到/berserk_mcclintock/ex中,作为其值。这里,EX是别名ex的大写形式,berserk_mcclintock是接收容器的自动生成名称。
- 最后一步,使用广泛使用的
ping命令对源容器进行 ping 操作,发送两次请求,并使用别名作为 ping 地址:
/ # ping -c 2 ex
PING ex (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64
time=0.108 ms
64 bytes from 172.17.0.3: seq=1 ttl=64
time=0.079 ms
--- ex ping statistics ---
2 packets transmitted, 2 packets received,
0% packet loss
round-trip min/avg/max = 0.079/0.093/0.108 ms
显然,源容器的别名ex解析为172.17.0.3 IP 地址,并且接收容器能够成功地访问源容器。如果启用了安全容器通信,容器之间的 ping 操作将被禁止。关于容器安全的更多细节,请参见第十一章,Docker 容器安全性。
在前面的示例中,我们可以将两个容器连接在一起,还可以观察到通过更新接收容器/etc/hosts文件中源容器的 IP 地址,容器之间的网络连接是如何优雅地启用的。
下一个示例演示如何通过容器链接将源容器的环境变量导出到接收容器,这些变量通过docker run子命令的-e选项或Dockerfile的ENV指令进行配置。为此,我们将编写一个名为Dockerfile的文件,使用ENV指令,构建一个镜像,使用该镜像启动源容器,然后通过链接将接收容器启动并与源容器连接:
- 我们从编写一个带有
ENV指令的Dockerfile开始,如下所示:
FROM busybox:latest
ENV BOOK="Learning Docker" \
CHAPTER="Orchestrating Containers"
在这里,我们设置了两个环境变量,BOOK和CHAPTER。
- 使用前面的
Dockerfile,通过docker build子命令构建一个 Docker 镜像envex:
$ sudo docker build -t envex .
- 现在,让我们使用刚刚构建的
envex镜像,启动一个名为example的交互式源容器:
$ sudo docker run -it --rm \
--name example envex
- 在源容器的提示符下,通过调用
env命令显示所有环境变量:
/ # env
HOSTNAME=b53bc036725c
SHLVL=1
HOME=/root
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
BOOK=Learning Docker
CHAPTER=Orchestrating Containers
PWD=/
在所有前面的环境变量中,BOOK和CHAPTER变量都是通过Dockerfile中的ENV指令进行配置的。
- 最后一步,为了说明
ENV类别的环境变量,使用env命令启动接收容器,如下所示:
$ sudo docker run --rm --link example:ex \
busybox:latest env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=a5e0c07fd643
TERM=xterm
EX_NAME=/stoic_hawking/ex
EX_ENV_BOOK=Learning Docker
EX_ENV_CHAPTER=Orchestrating Containers
HOME=/root
这个示例也可以在 GitHub 上找到:github.com/thedocker/learning-docker/blob/master/chap08/Dockerfile-Env。
值得注意的是,在前面的输出中,带有EX_前缀的变量是容器链接的结果。我们关注的环境变量是EX_ENV_BOOK和EX_ENV_CHAPTER,它们最初通过Dockerfile设置为BOOK和CHAPTER,但由于容器链接的效果,它们被修改为EX_ENV_BOOK和EX_ENV_CHAPTER。尽管环境变量的名称发生了转换,但存储在这些环境变量中的值保持不变。我们在前面的示例中已经讨论过EX_NAME变量名。
在前面的示例中,我们体验了 Docker 如何优雅且轻松地将ENV类别的变量从源容器导出到接收容器。这些环境变量与源容器和接收容器完全解耦,因此在一个容器中这些环境变量的值发生变化不会影响另一个容器。更准确地说,接收容器所接收的值是源容器启动时设置的值。源容器启动后,所做的任何对这些环境变量值的更改都不会影响接收容器。接收容器的启动时间无关紧要,因为这些值是从 JSON 文件中读取的。
在我们最终的容器链接示例中,我们将向您展示如何利用 Docker 的功能在两个容器之间共享连接信息。为了在容器之间共享连接信息,Docker 使用PORT类别的环境变量。以下是创建两个容器并在它们之间共享连接信息的步骤:
- 编写一个
Dockerfile,使用EXPOSE指令暴露80和8080端口,如下所示:
FROM busybox:latest
EXPOSE 8080 80
- 使用刚才创建的
Dockerfile,通过运行以下命令构建一个portexDocker 镜像:
$ sudo docker build -t portex .
- 现在,让我们使用之前构建的
portex镜像启动一个名为example的交互式源容器:
$ sudo docker run -it --rm --name example portex
- 现在我们已经启动了源容器,让我们继续在另一个终端中创建一个接收容器,并将其链接到源容器,然后调用
env命令以显示所有环境变量,如下所示:
$ sudo docker run --rm --link example:ex \
busybox:latest env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=c378bb55e69c
TERM=xterm
EX_PORT=tcp://172.17.0.4:80
EX_PORT_80_TCP=tcp://172.17.0.4:80
EX_PORT_80_TCP_ADDR=172.17.0.4
EX_PORT_80_TCP_PORT=80
EX_PORT_80_TCP_PROTO=tcp
EX_PORT_8080_TCP=tcp://172.17.0.4:8080
EX_PORT_8080_TCP_ADDR=172.17.0.4
EX_PORT_8080_TCP_PORT=8080
EX_PORT_8080_TCP_PROTO=tcp
EX_NAME=/prickly_rosalind/ex
HOME=/root
这个示例也可以在 GitHub 上找到,链接为:github.com/thedocker/learning-docker/blob/master/chap08/Dockerfile-Expose。
从env命令的前述输出中,可以明显看出,Docker 引擎为每个使用EXPOSE指令暴露的端口导出了四个PORT类别的环境变量。此外,Docker 还导出了另一个PORT类别变量EX_PORT。
容器编排
在 IT 领域,编排这一先锋概念已经存在很长时间了。例如,在服务计算(SC)领域,服务编排的理念以空前的方式蓬勃发展,旨在生成并维持高度稳健和弹性的服务。离散的或原子服务本身并没有实际意义,除非它们按照特定的顺序组合起来,从而得出过程感知的复合服务。由于编排服务在企业中表达和展示其独特能力的方式——即通过可识别/可发现、可互操作、可用和可组合的服务形式——在战略上对企业有着更大的优势,因此企业对拥有一个易于搜索的服务库(包括原子服务和复合服务)表现出了极大的兴趣。反过来,这个服务库能够帮助企业实现大规模的数据和过程密集型应用。显然,服务的多样性对于组织的增长和繁荣至关重要。这一日益增长的需求通过使用认知编排能力得到了有效解决。
现在,随着我们快速迈向容器化的 IT 环境,应用和数据容器应当智能地组合,以实现一系列新一代的软件服务。
然而,为了生成高效的编排容器,既需要精确选择和启动特定用途的容器,也需要选择与用途无关的容器,并且按正确的顺序进行启动,以便创建编排容器。这个顺序可以来自流程(控制流和数据流)图。手动进行这一复杂且令人畏惧的活动,往往会引发一系列的冷嘲热讽和批评。幸运的是,Docker 领域中有许多编排工具可以帮助构建、运行和管理多个容器,以构建企业级服务。负责生成和推广 Docker 灵感的容器生成与组装的 Docker 公司,推出了一款标准化且简化的编排工具(命名为 docker-compose),旨在减轻开发人员和系统管理员的工作负担。
SC 范式的成熟组合技术在这里被复制应用于快速发展的容器化范式中,以期收获容器化原本预期的益处,特别是在构建强大的应用感知容器方面。
微服务架构(MSA)是一种架构概念,旨在通过将软件功能分解成一组离散的服务来解耦软件解决方案。这是通过在多个原则上应用架构级别来实现的。MSA 正在逐渐成为设计和构建大规模 IT 和商业系统的主流方式。它不仅促进了松散和轻量级的耦合及软件模块化,还为敏捷开发世界中的持续集成和部署提供了巨大帮助。对应用程序的任何更改都需要对整个应用程序进行大规模修改。这一直是持续部署方面的一个障碍和难题。微服务旨在解决这种情况,因此,MSA 需要轻量级机制、小型、可独立部署的服务,并确保可扩展性和可移植性。这些要求可以通过使用 Docker 支持的容器来满足。
微服务是围绕业务能力构建的,并且可以通过完全自动化的部署机制独立部署。每个微服务可以在不影响其他微服务的情况下部署,而容器为服务提供了理想的部署和执行环境,并且提供了其他显著的优势,例如减少部署时间、隔离管理和简单的生命周期管理。新版本的服务可以轻松地在容器中快速部署。所有这些因素导致了使用 Docker 提供的功能的微服务爆炸式增长。
如前所述,Docker 被定位为下一代容器化技术,提供了一种经过验证并且潜力巨大的机制,用于高效、分布式地分发应用程序。其优势在于,开发人员可以在容器内调整应用程序组件,同时保持容器的整体完整性。这带来了更大的影响,因为现在的趋势是,企业不再将大型单体应用程序部署在单个物理或虚拟服务器上,而是构建较小、自定义、易于管理和独立的服务,将它们容器化并标准化自动化。简而言之,Docker 的容器化技术为微服务时代的到来提供了巨大的助力。
Docker 的构建和发展旨在实现一次运行,到处运行的理想目标。Docker 容器通常在进程级别进行隔离,具有跨 IT 环境的可移植性,并且容易重复。单个物理主机可以承载多个容器,因此,每个 IT 环境通常都充斥着各种 Docker 容器。容器的前所未有的增长意味着容器管理将面临挑战。容器的多样性及其异质性大大增加了容器管理的复杂性。因此,容器编排技术及其蓬勃发展的编排工具为加速容器化进程提供了战略性支持,帮助在安全的环境中推进这一进程。
跨多个容器编排包含微服务的应用程序,已经成为 Docker 世界的一个重要组成部分,通过 Google 的 Kubernetes 或 Flocker 等项目得以实现。Decking 是另一个用于促进 Docker 容器编排的选项。Docker 在这一领域的最新产品是一组三种编排服务,旨在涵盖分布式应用程序动态生命周期的各个方面,从应用程序开发到部署和维护。Helios 是另一个 Docker 编排平台,用于跨整个集群部署和管理容器。一开始,fig是最受欢迎的容器编排工具。然而,在最近,领先的 Docker 技术推广公司推出了一个先进的容器编排工具(docker-compose),旨在使开发人员在处理 Docker 容器并经历容器生命周期的过程中更加轻松。
意识到为下一代、业务关键且容器化的工作负载提供容器编排能力的重要性后,Docker 公司收购了最早构思并实现fig工具的公司。然后,Docker 公司适当地将该工具重命名为docker-compose,并进行了大量增强,使其更加符合容器开发人员和运维团队的各种期望。
下面是docker-compose的概述,它被定位为一种未来主义的、灵活的工具,用于定义和运行复杂的 Docker 应用程序。使用docker-compose,你可以在一个文件中定义应用程序的各个组件(它们的容器、配置、链接、卷等),然后只需一个命令就可以启动所有内容,完成一切工作,确保应用程序顺利运行。
该工具通过提供一套内置工具,简化了容器管理,完成许多目前仍需手动操作的任务。在本节中,我们提供了所有使用docker-compose进行容器编排的详细信息,旨在支持下一代分布式应用程序的流畅运行。
使用 docker-compose 进行容器编排
本节将讨论广泛使用的容器编排工具docker-compose。docker-compose是一个非常简单但强大的工具,旨在方便地运行一组 Docker 容器。换句话说,docker-compose是一个编排框架,让您能够定义并控制一个多容器服务。它使您能够创建一个快速且隔离的开发环境,同时在生产中编排多个 Docker 容器。docker-compose工具在内部利用 Docker 引擎拉取镜像、构建镜像、按正确顺序启动容器,并根据docker-compose.yml文件中的定义,在容器/服务之间建立正确的连接/链接。
安装 docker-compose
在写这本书时,docker-compose的最新版本是 1.11.2,建议您与 Docker 版本 1.9.1 或更高版本一起使用。您可以在 GitHub 的官方发布页面找到docker-compose的最新版本(github.com/docker/compose/releases/latest)。
我们已经自动化了docker-compose的安装过程,并将其公开提供,地址为sjeeva.github.io/getcompose。这些自动化脚本可以准确识别docker-compose的最新版本,下载并将其安装到/usr/local/bin/docker-compose位置:
- 使用
wget工具,如下所示:
$ wget -qO- http://sjeeva.github.io/getcompose \
| sudo sh
- 使用
curl工具,如下所示:
$ curl -sSL http://sjeeva.github.io/getcompose \
| sudo sh
或者,您可以选择直接从 GitHub 软件仓库安装特定版本的docker-compose。在这里,您可以找到下载和安装docker-compose版本1.11.2的方法:
使用wget工具,如下所示:
sudo sh -c 'wget -qO- \
https://github.com/docker/compose/releases/tag/1.11.2/ \
docker-compose-`uname -s`-`uname -m` > \
/usr/local/bin/docker-compose; \
chmod +x /usr/local/bin/docker-compose'
使用curl工具,如下所示:
curl -L https://github.com/docker/compose/releases/download/1.11.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
docker-compose工具也可以作为 Python 包安装,您可以使用pip安装器进行安装,方法如下:
$ sudo pip install -U docker-compose
如果系统上未安装pip,请先安装pip包,再进行docker-compose的安装。
成功安装docker-compose后,您现在可以检查docker-compose的版本:
$ docker-compose --version
docker-compose version 1.11.2, build dfed245
docker-compose 文件
docker-compose 工具使用 YAML(一种叫做 Yet Another Markup Language 的语言)来编排容器,这就是 docker-compose 文件的格式。YAML 是一种人类友好的数据序列化格式。Docker 最初作为一个容器工具出现,现如今作为一个自动化和加速大部分任务(如容器配置、网络、存储、管理、编排、安全、治理和持久性等)的生态系统,发展迅猛。因此,docker-compose 文件格式及其版本被多次修订,以跟上 Docker 平台的变化。在撰写本版本时,docker-compose 文件的最新版本为 3。以下表格列出了 docker-compose 文件与 Docker 引擎版本的兼容性矩阵:
| Docker Compose 文件格式 | Docker 引擎 | 备注 |
|---|---|---|
| 3, 3.1 | 1.13.0+ | 提供对 docker stack deploy 和 docker secrets 的支持 |
| 2.1 | 1.12.0+ | 引入了一些新参数 |
| 2 | 1.10.0+ | 引入对命名卷和网络的支持 |
| 1 | 1.9.0+ | 将在未来的 Compose 版本中弃用 |
docker-compose 工具默认使用名为 docker-compose.yml 或 docker-compose.yaml 的文件来编排容器。可以使用 docker-compose 工具的 -f 选项修改此默认文件。以下是 docker-compose 文件的格式:
version: "<version>"
services:
<service>:
<key>: <value>
<key>:
- <value>
- <value>
networks:
<network>:
<key>: <value>
volumes:
<volume>:
<key>: <value>
这里使用的选项如下:
-
<version>:这是docker-compose文件的版本。请参阅前面的版本表。 -
<service>:这是服务的名称。你可以在一个docker-compose文件中定义多个服务。服务名称后面可以跟一个或多个键。不过,所有服务必须至少有一个image或build键,后面可以跟任意数量的可选键。除了image和build键,其他键可以直接映射到docker run子命令中的选项。值可以是单一值,也可以是多个值。所有<service>定义必须归类在顶级services键下。 -
<network>:这是服务使用的网络的名称。所有<network>定义必须归类在顶级networks键下。 -
<volume>:这是服务使用的卷的名称。所有<volume>定义必须归类在顶级volume键下。
在这里,我们列出了 docker-compose 文件版本 3 支持的几个键。有关 docker-compose 支持的所有键,请参考 docs.docker.com/compose/compose-file。
-
image:这是标签或镜像 ID。 -
build:这是包含Dockerfile的目录路径。 -
command:此键覆盖默认命令。 -
deploy:此键有许多子键,用于指定部署配置。仅在docker swarm模式下使用。 -
depends_on:此选项用于指定服务之间的依赖关系,可以进一步扩展,以根据服务的条件链接服务。 -
cap_add:此选项向容器添加功能。 -
cap_drop:此选项删除容器的某项功能。 -
dns:此选项设置自定义 DNS 服务器。 -
dns_search:此选项设置自定义 DNS 搜索服务器。 -
entrypoint:此关键字覆盖默认的入口点。 -
env_file:此关键字允许您通过文件添加环境变量。 -
environment:此选项用于添加环境变量,可以使用数组或字典的形式。 -
expose:此关键字暴露端口,但不会将它们发布到主机机器。 -
extends:此选项扩展同一或不同配置文件中定义的其他服务。 -
extra_hosts:此选项使您能够向容器内的/etc/hosts添加额外的主机。 -
healthcheck:此命令允许我们配置服务健康检查。 -
labels:此关键字允许您为容器添加元数据。 -
links:此关键字用于连接到另一个服务中的容器。强烈不推荐使用链接。 -
logging:用于配置服务的日志记录。 -
network:此选项用于将服务加入到顶层networks关键字定义的网络中。 -
pid:此选项启用主机和容器之间的 PID 空间共享。 -
ports:此关键字用于暴露端口并指定HOST_port:CONTAINER_port的端口映射。 -
volumes:此关键字用于挂载路径或命名卷。命名卷需要在顶层的volumes关键字中定义。
docker-compose 命令
docker-compose 工具提供了复杂的 orchestration 功能,支持一组命令。在本节中,我们将列出 docker-compose 的选项和命令:
docker-compose [<options>] <command> [<args>...]
docker-compose 工具支持以下选项:
-
-f、--file <file>:此选项指定docker-compose使用的替代文件(默认是docker-compose.yml文件)。 -
-p、--project-name <name>:此选项指定一个替代的项目名称(默认是目录名称)。 -
--verbose:显示更多的输出。 -
-v、--version:此选项打印版本并退出。 -
-H、--host <host>:用于指定要连接的守护进程套接字。 -
-tls、--tlscacert、--tlskey和--skip-hostname-check:docker-compose工具还支持这些标志来启用传输层安全性(TLS)。
docker-compose 工具支持以下命令:
-
build:此命令用于构建或重建服务。 -
bundle:此命令用于从 compose 文件创建 Docker bundle,这是 Docker 1.13 中的实验性功能。 -
config:此命令用于验证和显示 compose 文件。 -
create:此命令用于创建 compose 文件中定义的服务。 -
down:此命令用于停止并删除容器和网络。 -
events:此命令可用于查看实时的容器生命周期事件。 -
exec:此命令使您能够在运行中的容器中执行命令,主要用于调试目的。 -
kill:此命令用于终止运行中的容器。 -
logs:此命令显示来自容器的输出。 -
pause:此命令用于暂停服务。 -
port:此命令打印端口绑定的公共端口。 -
ps:此命令列出容器。 -
pull:此命令从仓库拉取镜像。 -
push:此命令将镜像推送到仓库。 -
restart:此命令用于重启在 compose 文件中定义的服务。 -
rm:此命令移除已停止的容器。 -
run:此命令运行一次性命令。 -
scale:此命令用于为服务设置容器数量。 -
start:此命令启动 compose 文件中定义的服务。 -
stop:此命令停止服务。 -
unpause:此命令用于恢复暂停的服务。 -
up:此命令用于创建并启动容器。 -
version:此命令打印 Docker Compose 的版本。
常见用法
在这一部分中,我们将通过一个示例来体验 Docker Compose 框架提供的编排功能。为此,我们将构建一个二层 Web 应用程序,它通过 URL 接收输入并返回相应的响应文本。这个应用程序使用以下两个服务构建,具体如下:
-
Redis:这是一个键值数据库,用于存储键及其相关联的值。
-
Node.js:这是一个 JavaScript 运行时环境,用于实现 Web 服务器功能以及应用程序逻辑。
每个服务都被打包在两个不同的容器中,并通过 docker-compose 工具将它们连接在一起。以下是服务的架构表示:

在这个示例中,我们首先实现 example.js 模块,这是一个 Node.js 文件,用于实现 Web 服务器和关键字查找功能。接下来,我们将在与 example.js 文件相同的目录中编写 Dockerfile,用于打包 Node.js 运行时环境,并定义使用 docker-compose.yml 文件进行服务编排,文件与 example.js 位于同一目录。
以下是 example.js 文件,这是一个 Node.js 实现的简单请求/响应 Web 应用程序。为了演示,在这个示例代码中,我们将请求和响应限制为两个 docker-compose 命令(build 和 kill)。为了让代码更加易于理解,我们在代码中添加了注释:
// A Simple Request/Response web application
// Load all required libraries
var http = require('http');
var url = require('url');
var redis = require('redis');
// Connect to redis server running
// createClient API is called with
// -- 6379, a well-known port to which the
// redis server listens to
// -- redis, is the name of the service (container)
// that runs redis server
var client = redis.createClient(6379, 'redis');
// Set the key value pair in the redis server
// Here all the keys proceeds with "/", because
// URL parser always have "/" as its first character
client.set("/", "Welcome to Docker-Compose helpernEnter the docker-compose command in the URL for helpn", redis.print);
client.set("/build", "Build or rebuild services", redis.print);
client.set("/kill", "Kill containers", redis.print);
var server = http.createServer(function (request, response) {
var href = url.parse(request.url, true).href;
response.writeHead(200, {"Content-Type": "text/plain"});
// Pull the response (value) string using the URL
client.get(href, function (err, reply) {
if ( reply == null ) response.write("Command: " +
href.slice(1) + " not supportedn");
else response.write(reply + "n");
response.end();
});
});
console.log("Listening on port 80");
server.listen(80);
这个示例也可以在github.com/thedocker/learning-docker/tree/master/chap08/orchestrate-using-compose查看。
以下是打包 Node.js 镜像、Node.js 用的 redis 驱动和前面定义的 example.js 文件的 Dockerfile 内容:
###############################################
# Dockerfile to build a sample web application
###############################################
# Base image is node.js
FROM node:latest
# Author: Dr. Peter
MAINTAINER Dr. Peter <peterindia@gmail.com>
# Install redis driver for node.js
RUN npm install redis
# Copy the source code to the Docker image
ADD example.js /myapp/example.js
这个代码也可以在github.com/thedocker/learning-docker/tree/master/chap08/orchestrate-using-compose查看。
以下文本来自docker-compose.yml文件,该文件定义了 Docker Compose 工具管理的服务:
version: "3.1"
services:
web:
build: .
command: node /myapp/example.js
depends_on:
- redis
ports:
- 8080:80
redis:
image: redis:latest
这个示例也可以在github.com/thedocker/learning-docker/tree/master/chap08/orchestrate-using-compose找到。
我们在这个docker-compose.yml文件中定义了两个服务,这些服务分别执行以下任务:
-
名为
web的服务是使用当前目录中的Dockerfile构建的。此外,指示启动容器时需要运行node(Node.js 运行时)并传入/myapp/example.js(Web 应用实现)作为其参数。由于此 Node.js 应用使用了redis数据库,因此web服务必须在redis服务启动后启动,使用depends_on指令来实现。此外,80容器端口映射到8080的 Docker 主机端口。 -
名为
redis的服务被指示使用redis:latest镜像启动容器。如果该镜像不存在于 Docker 主机中,Docker 引擎将从中央仓库或私有仓库中拉取它。
现在,让我们继续执行示例,通过使用docker-compose build命令构建 Docker 镜像,使用docker-compose up命令启动容器,并通过浏览器连接以验证请求/响应功能,如此处逐步解释的那样:
docker-compose命令必须在存储docker-compose.yml文件的目录中执行。此外,docker-compose将每个docker-compose.yml文件视为一个项目,并从该文件所在的目录假定项目名称。当然,可以使用-p选项覆盖此设置。因此,作为第一步,让我们切换到存储docker-compose.yml文件的目录:
$ cd ~/example
- 使用
docker-compose build命令构建服务:
$ sudo docker-compose build
- 使用
docker-compose pull命令从仓库中拉取镜像:
$ sudo docker-compose pull
- 使用
docker-compose up命令根据docker-compose.yml文件中的指示启动服务:
$ sudo docker-compose up
Creating network "example_default" with the default
driver
Creating example_redis_1
Creating example_web_1
Attaching to example_redis_1, example_web_1
redis_1 | 1:C 03 Feb 18:09:40.743 # Warning: no
config file specified, using the default config.
In order to specify a config file use redis-server
/path/to/redis.conf
. . . TRUNCATED OUTPUT . . .
redis_1 | 1:M 03 Feb 18:03:47.438 * The server
is now ready to accept connections on port 6379
web_1 | Listening on port 80
web_1 | Reply: OK
web_1 | Reply: OK
web_1 | Reply: OK
由于目录名是example,docker-compose工具已假定项目名称为example。如果您注意输出的第一行,您会看到创建了example_default网络。Docker Compose 工具默认创建此桥接网络,并且此网络由服务用于 IP 地址解析。因此,服务可以通过使用在 Compose 文件中定义的服务名称相互访问。
- 成功使用
docker-compose工具编排服务后,让我们从另一个终端调用docker-compose ps命令,列出与示例docker-compose项目相关的容器:
$ sudo docker-compose ps
Name Command
State Ports
--------------------------------------------------
-------------------------
example_redis_1 /entrypoint.sh redis-server
Up 6379/tcp
example_web_1 node /myapp/example.js
Up 0.0.0.0:8080->80/tcp
显然,example_redis_1和example_web_1这两个容器已经启动并正在运行。容器名称以example_为前缀,这是docker-compose项目的名称。
- 在 Docker 主机的另一个终端上探索我们自己的请求/响应 Web 应用程序的功能,如下所示:
$ curl http://localhost:8080
Welcome to Docker-Compose helper
Enter the docker-compose command in the URL for help
$ curl http://localhost:8080/build
Build or rebuild services
$ curl http://localhost:8080/something
Command: something not supported
在这里,我们直接通过http://localhost:8080连接到web服务,因为web服务绑定到 Docker 主机的8080端口。你也可以通过 Docker 主机的 IP 地址和端口8080外部访问该服务(https://<docker host ip>:8080),前提是 IP 地址和端口可以从外部系统访问。
很酷,不是吗?只需极少的努力,再加上docker-compose.yml文件的帮助,我们就能够将两个不同的服务组合在一起,提供一个复合服务。
总结
本章被纳入本书的目的是为了提供所有关于无缝编排多个容器的详细探讨和建议。我们广泛讨论了容器编排的必要性以及简化和流畅化越来越复杂的容器编排过程的工具。为了证明容器编排在构建企业级容器中的便捷性和帮助,并展示编排过程,我们采用了一种广受欢迎的方式,通过一个简单的示例来讲解整个过程。我们开发了一个 Web 应用,并将其封装在一个标准容器中。同样,我们还使用了一个数据库容器,它是前端 Web 应用的后端。数据库在另一个容器内执行。我们看到了如何通过 Docker 引擎的容器链接特性,使 Web 应用容器能够感知数据库,并使用不同的技术实现这一点。我们使用了一个开源工具(docker-compose)来实现这一目标。
在下一章,我们将讨论 Docker 如何促进软件测试,特别是集成测试,并提供一些实用的示例。
第六章:使用 Docker 进行测试
毫无疑问,测试这一特性一直处于软件工程学科的前沿。如今,软件在我们日常环境中每种有形物体中的深远影响已广泛被接受,这些物体都为了拥有大量智能、连接和数字化的资产。此外,随着对分布式和同步软件的关注增加,软件的设计、开发、测试、调试、部署和交付的复杂性也在不断上升。各种手段和机制被发现,以简化和优化软件构建自动化以及验证软件的可靠性、弹性和可持续性。Docker 正在成为一个极其灵活的工具,用于测试各种软件应用程序。在本章中,我们将讨论如何有效地利用 Docker 的显著进展进行软件测试,并探讨其在加速和增强测试自动化方面的独特优势。
本章讨论的主题包括:
-
测试驱动开发(TDD)概述
-
在 Docker 中测试你的代码
-
将 Docker 测试过程集成到 Jenkins 中
目前,Docker 容器被广泛用于创建与生产环境完全相同的开发和测试环境。与虚拟机相比,容器所需的开销更小,虚拟机一直是开发、预发布和部署环境的主要方式。让我们从下一代软件的测试驱动开发(TDD)概述开始,看看 Docker 启发的容器化如何在简化 TDD 过程时变得非常实用。
测试驱动开发(TDD)概述
软件开发这条漫长而艰辛的旅程在过去几十年中经历了许多曲折变化,而测试驱动开发(TDD)无疑是其中一个突出的软件工程技术。
欲了解更多关于 TDD 的详细信息和文档,请访问 agiledata.org/essays/tdd.html。
简而言之,TDD 是一种软件开发实践,其中开发周期以编写一个会失败的测试用例开始,然后编写实际的软件以通过该测试,接着持续重构并重复这一周期,直到软件达到可接受的水平。这个过程如下图所示:

在 Docker 中测试你的代码
在本节中,我们将带领你踏上一个旅程,展示如何使用存根(stubs)进行 TDD,并演示在等同于部署系统的环境中开发软件时 Docker 如何发挥作用。为了这个目的,我们选择了一个网页应用的使用案例,其中有一个功能可以跟踪每个用户的访问次数。在这个例子中,我们使用 Python 作为实现语言,redis 作为存储用户访问次数的键值数据库。此外,为了展示 Docker 的测试能力,我们将实现限制为两个函数——hit 和 getHit。
本章中的所有示例都使用 Python 3 作为运行环境。Ubuntu 16.04 安装默认包含 Python 3。如果你的系统没有安装 Python 3,请参考相应的手册进行安装。
根据 TDD 实践,我们首先为hit和getHit函数添加单元测试用例,如以下代码片段所示。这里,测试文件命名为test_hitcount.py:
import unittest
import hitcount
class HitCountTest (unittest.TestCase):
def testOneHit(self):
# increase the hit count for user user1
hitcount.hit("user1")
# ensure that the hit count for user1 is just 1
self.assertEqual(b'1', hitcount.getHit("user1"))
if __name__ == '__main__':
unittest.main()
该示例也可以在github.com/thedocker/testing/tree/master/src找到。
在这里,第一行我们导入了unittest Python 模块,它提供了必要的框架和功能来运行单元测试,并生成关于测试执行的详细报告。第二行,我们导入了hitcount Python 模块,在该模块中我们将实现点击计数功能。然后,我们将继续添加测试代码,以测试hitcount模块的功能。
现在,使用 Python 的单元测试框架运行测试套件,如下所示:
$ python3 -m unittest
以下是单元测试框架生成的输出:
E
======================================================================
ERROR: test_hitcount (unittest.loader.ModuleImportFailure)
----------------------------------------------------------------------
Traceback (most recent call last):
...OUTPUT TRUNCATED ...
ImportError: No module named 'hitcount'
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
如预期所示,测试失败并显示ImportError: No module named 'hitcount'错误信息,因为我们还没有创建该文件,因此无法导入hitcount模块。
现在,在与test_hitcount.py相同的目录下创建一个名为hitcount.py的文件:
$ touch hitcount.py
继续运行单元测试套件:
$ python3 -m unittest
以下是单元测试框架生成的输出:
E
======================================================================
ERROR: testOneHit (test_hitcount.HitCountTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/user/test_hitcount.py", line 10, in testOneHit
hitcount.hit("peter")
AttributeError: 'module' object has no attribute 'hit'
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
如之前一样,测试套件失败了,但错误信息有所不同,显示为AttributeError: 'module' object has no attribute 'hit'。我们之所以收到此错误,是因为我们还没有实现hit函数。
让我们继续在hitcount.py中实现hit和getHit函数,如下所示:
import redis
# connect to redis server
r = redis.StrictRedis(host='0.0.0.0', port=6379, db=0)
# increase the hit count for the usr
def hit(usr):
r.incr(usr)
# get the hit count for the usr
def getHit(usr):
return (r.get(usr))
该示例也可以在 GitHub 上找到:github.com/thedocker/testing/tree/master/src。
要继续这个示例,你必须有与 Python 3 兼容的包管理器版本(pip3)。
以下命令用于安装pip3:
$ wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 -
在前述程序的第一行,我们导入了redis驱动,它是连接redis数据库的驱动。在接下来的行中,我们将连接到redis数据库,然后继续实现hit和getHit函数。
redis驱动是一个可选的 Python 模块,因此我们接下来将使用pip安装器安装redis驱动,如下所示:
$ sudo pip3 install redis
即使安装了redis驱动程序,我们的unittest模块仍然会失败,因为我们尚未运行redis数据库服务器。因此,我们可以运行redis数据库服务器以成功完成单元测试,或者采用模拟redis驱动程序的传统 TDD 方法。模拟是一种测试方法,其中复杂的行为被预定义或模拟的行为替代。在我们的示例中,为了模拟redis驱动程序,我们将利用一个名为mockredis的第三方 Python 包。此模拟包可在github.com/locationlabs/mockredis获取,并且pip安装器名称为mockredispy。让我们使用pip安装器安装这个模拟:
$ sudo pip3 install mockredispy
安装了mockredispy模拟redis后,让我们重构我们之前编写的测试代码test_hitcount.py,以使用mockredis模块提供的模拟redis功能。这可以通过unittest.mock模拟框架提供的patch方法来实现,如下所示的代码所示:
import unittest
from unittest.mock import patch
# Mock for redis
import mockredis
import hitcount
class HitCountTest(unittest.TestCase):
@patch('hitcount.r',
mockredis.mock_strict_redis_client(host='0.0.0.0',
port=6379, db=0))
def testOneHit(self):
# increase the hit count for user user1
hitcount.hit("user1")
# ensure that the hit count for user1 is just 1
self.assertEqual(b'1', hitcount.getHit("user1"))
if __name__ == '__main__':
unittest.main()
此示例也可在 GitHub 上获取,地址为github.com/thedocker/testing/tree/master/src。
现在,再次运行测试套件:
$ python3 -m unittest
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
最后,正如我们在前面的输出中所看到的,我们成功通过测试、代码和重构周期实现了访客计数功能。
在容器内运行测试
在前一节中,我们为您介绍了 TDD 的完整周期,我们安装了额外的 Python 包以完成开发。然而,在现实世界中,一个人可能会在多个可能存在冲突的项目上工作,因此需要隔离运行时环境。在 Docker 技术出现之前,Python 社区通常使用 Virtualenv 工具来隔离 Python 运行时环境。Docker 通过打包操作系统、Python 工具链和运行时环境进一步加强了这种隔离。这种隔离方式为开发社区提供了很大的灵活性,可以根据项目需求使用适当的软件版本和库。
下面是将上一节的测试和访客计数实现打包到 Docker 容器中并在容器内执行测试的逐步过程:
- 编写一个
Dockerfile来构建一个包含python3运行时环境、redis和mockredispy包以及test_hitcount.py测试文件和访客计数实现hitcount.py的镜像,最后启动单元测试:
#############################################
# Dockerfile to build the unittest container
#############################################
# Base image is python
FROM python:latest
# Author: Dr. Peter
MAINTAINER Dr. Peter <peterindia@gmail.com>
# Install redis driver for python and the redis mock
RUN pip install redis && pip install mockredispy
# Copy the test and source to the Docker image
ADD src/ /src/
# Change the working directory to /src/
WORKDIR /src/
# Make unittest as the default execution
ENTRYPOINT python3 -m unittest
此示例也可在 GitHub 上获取,地址为github.com/thedocker/testing/tree/master/src。
-
现在创建一个名为
src的目录,我们在其中编写了我们的Dockerfile。将test_hitcount.py和hitcount.py文件移动到新创建的src目录。 -
构建
hit_unittestDocker 镜像使用docker build子命令:
$ sudo docker build -t hit_unittest .
Sending build context to Docker daemon 11.78 kB
Sending build context to Docker daemon
Step 0 : FROM python:latest
---> 32b9d937b993
Step 1 : MAINTAINER Dr. Peter <peterindia@gmail.com>
---> Using cache
---> bf40ee5f5563
Step 2 : RUN pip install redis && pip install mockredispy
---> Using cache
---> a55f3bdb62b3
Step 3 : ADD src/ /src/
---> 526e13dbf4c3
Removing intermediate container a6d89cbce053
Step 4 : WORKDIR /src/
---> Running in 5c180e180a93
---> 53d3f4e68f6b
Removing intermediate container 5c180e180a93
Step 5 : ENTRYPOINT python3 -m unittest
---> Running in 74d81f4fe817
---> 063bfe92eae0
Removing intermediate container 74d81f4fe817
Successfully built 063bfe92eae0
- 现在我们已成功构建了镜像,让我们使用
docker run子命令启动包含单元测试包的容器,如下所示:
$ sudo docker run --rm -it hit_unittest .
---------------------------------------------------------------
-------
Ran 1 test in 0.001s
OK
显然,单元测试运行成功了,没有错误,因为我们已经打包了经过测试的代码。
在这种方法中,对于每一个变更,都会构建 Docker 镜像,然后启动容器完成测试。
使用 Docker 容器作为运行时环境
在前一节中,我们构建了一个 Docker 镜像来执行测试。特别是在 TDD 实践中,单元测试用例和代码会经历多次更改。因此,需要多次构建 Docker 镜像,这是一项令人望而却步的任务。在本节中,我们将看到一种替代方法,其中 Docker 容器带有运行时环境,开发目录作为卷挂载,并在容器内执行测试。
在这个 TDD 周期中,如果需要额外的库或更新现有库,那么容器将使用所需的库更新,并且更新后的容器将被提交为一个新镜像。这种方法提供了开发者梦寐以求的隔离性和灵活性,因为运行时及其依赖项存在于容器内,任何配置错误的运行时环境都可以被丢弃,并且可以从先前工作的镜像构建一个新的运行时环境。这也有助于保持 Docker 主机的清醒状态,避免因库的安装和卸载而导致混乱。
以下示例是如何将 Docker 容器作为一个非污染但非常强大的运行时环境的逐步说明:
- 我们首先启动 Python 运行时的交互式容器,使用
docker run子命令:
$ sudo docker run -it \
-v /home/peter/src/hitcount:/src \
python:latest /bin/bash
在这个例子中,/home/peter/src/hitcount Docker 主机目录被指定为源代码和测试文件的占位符。这个目录作为 /src 挂载到容器中。
-
现在,在 Docker 主机的另一个终端上,将
test_hitcount.py测试文件和hitcount.py访问者计数实现复制到/home/peter/src/hitcount目录中。 -
切换到 Python 运行时的交互式容器终端,将当前工作目录更改为
/src,并运行单元测试:
root@a8219ac7ed8e:~# cd /src
root@a8219ac7ed8e:/src# python3 -m unittest
E
=====================================================
=================
ERROR: test_hitcount
(unittest.loader.ModuleImportFailure)
. . . TRUNCATED OUTPUT . . .
File "/src/test_hitcount.py", line 4, in <module>
import mockredis
ImportError: No module named 'mockredis'
---------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
显然,由于找不到 mockredis Python 库,测试失败了。
- 继续安装
mockredispypip 包,因为之前的步骤由于在运行时环境中找不到mockredis库而失败了:
root@a8219ac7ed8e:/src# pip install mockredispy
- 重新运行 Python 单元测试:
root@a8219ac7ed8e:/src# python3 -m unittest
E
=====================================================
============
ERROR: test_hitcount
(unittest.loader.ModuleImportFailure)
. . . TRUNCATED OUTPUT . . .
File "/src/hitcount.py", line 1, in <module>
import redis
ImportError: No module named 'redis'
Ran 1 test in 0.001s
FAILED (errors=1)
再次,由于尚未安装 redis 驱动程序,测试失败了。
- 继续使用
pip安装器安装redis驱动程序,如下所示:
root@a8219ac7ed8e:/src# pip install redis
- 成功安装了
redis驱动程序后,让我们再次运行单元测试:
root@a8219ac7ed8e:/src# python3 -m unittest
.
---------------------------------------------------------------
--
Ran 1 test in 0.000s
OK
显然,这次单元测试通过了,没有任何警告或错误消息。
- 现在我们拥有了一个足够好的运行时环境来运行测试用例。最好将这些更改提交到 Docker 镜像中以便重复使用,使用
docker commit子命令:
$ sudo docker commit a8219ac7ed8e \
python_rediswithmock
fcf27247ff5bb240a935ec4ba1bddbd8c90cd79cba66e52b21e1b48f984c7db2
- 从现在开始,我们可以使用
python_rediswithmock镜像启动新容器进行我们的 TDD。
在本节中,我们生动地展示了如何使用 Docker 容器作为测试环境,同时通过将运行时依赖关系限制在容器内,保持 Docker 主机的正常性和神圣性。
将 Docker 测试集成到 Jenkins 中
在前一节中,我们为软件测试奠定了刺激的基础,展示了如何利用 Docker 技术进行软件测试,以及在测试阶段容器技术的独特优势。在本节中,我们将向您介绍准备 Jenkins 环境以进行 Docker 测试所需的步骤,然后演示如何扩展 Jenkins 集成并自动化 Docker 测试,使用广为人知的点击计数用例。
准备 Jenkins 环境
在本节中,我们将引导您完成安装 Jenkins、Jenkins 的 GitHub 插件以及 git 和版本控制工具的步骤。步骤如下:
- 我们首先添加 Jenkins 的受信任 PGP 公钥:
$ wget -q -O - \
https://jenkins-ci.org/debian/jenkins-ci.org.key | \
sudo apt-key add -
在这里,我们使用 wget 下载 PGP 公钥,然后使用 apt-key 工具将其添加到受信任的密钥列表中。由于 Ubuntu 和 Debian 共享相同的软件包,因此 Jenkins 提供了一个通用的软件包,适用于这两个系统。
- 将 Debian 软件包位置添加到
apt软件包源列表中,如下所示:
$ sudo sh -c \
'echo deb http://pkg.jenkins-ci.org/debian binary/ > \
/etc/apt/sources.list.d/jenkins.list'
- 添加软件包源后,继续运行
apt-get命令的update选项以重新同步来自源的软件包索引:
$ sudo apt-get update
- 现在,使用
apt-get命令的install选项安装 Jenkins,如下所示:
$ sudo apt-get install jenkins
- 最后,使用
service命令激活 Jenkins 服务:
$ sudo service jenkins start
Jenkins 服务可以通过任何 Web 浏览器访问,只需指定安装 Jenkins 的系统的 IP 地址(54.86.87.243)。Jenkins 的默认端口号是 8080。最新版本的 Jenkins 2.62 已经安装。以下截图是 Jenkins 的入口页面或仪表盘:

- 提供文件中的密码并登录。此用户为管理员:
$ sudo cat \
/var/lib/jenkins/secrets/initialAdminPassword
b7ed7cfbde1443819455ab1502a19de2
- 这将带您进入“自定义 Jenkins”页面,如下截图所示:

-
在屏幕左侧选择“安装推荐插件”,这将带我们进入安装页面。
-
在创建第一个管理员用户页面,选择“继续作为管理员”:

这将带我们进入“Jenkins 已准备好!”页面,如下截图所示:

- 现在,点击“开始使用 Jenkins”按钮将带您进入“欢迎使用 Jenkins!”页面:

- 确保已安装
git包,否则请使用apt-get命令安装git包:
$ sudo apt-get install git
- 到目前为止,我们一直使用
sudo命令运行 Docker 客户端,但不幸的是,我们无法在 Jenkins 中调用sudo,因为有时它会提示输入密码。为了解决sudo密码提示问题,我们可以利用 Docker 组,Docker 组中的任何用户都可以在不使用sudo命令的情况下调用 Docker 客户端。Jenkins 安装过程中会自动创建一个名为jenkins的用户和组,并使用该用户和组运行 Jenkins 服务。因此,我们只需要将jenkins用户添加到 Docker 组中,就可以使 Docker 客户端在没有 sudo 命令的情况下正常工作:
$ sudo gpasswd -a jenkins docker
Adding user jenkins to group docker
- 使用以下命令重启 Jenkins 服务,以便使组更改生效:
$ sudo service jenkins restart
* Restarting Jenkins Continuous Integration Server
jenkins [ OK ]
我们已经设置了一个 Jenkins 环境,现在可以自动从github.com仓库中拉取最新的源代码,将其打包成 Docker 镜像,并执行预定的测试场景。
我们还建议您从官方 Jenkins Docker 镜像中以 Docker 容器形式运行 Jenkins,镜像地址为github.com/jenkinsci/docker。这也是一个很好的练习,能帮助您验证前几章中学习的 Docker 容器概念。
自动化 Docker 测试过程
在本节中,我们将探讨如何使用 Jenkins 和 Docker 实现自动化测试。如前所述,我们将使用 GitHub 作为我们的代码仓库。我们已经将之前示例中的Dockerfile、test_hitcount.py和hitcount.py文件上传到了 GitHub,地址为github.com/thedocker/testing,接下来的示例将使用这些文件。然而,我们强烈建议您创建自己的代码仓库,地址为
github.com,使用 fork 选项,您可以在github.com/thedocker/testing找到此选项,并在接下来的示例中将此地址替换为合适的地方。
以下是自动化 Docker 测试的详细步骤:
配置 Jenkins 以在 GitHub 仓库中的文件被修改时触发构建,以下是相关子步骤的说明:
-
再次连接到 Jenkins 服务器。
-
选择创建新任务。
-
如下图所示,为项目命名(例如,
Docker-Testing),并选择自由风格项目:

- 如下图所示,在源代码管理下选择 Git 单选按钮,并在仓库 URL 文本框中指定 GitHub 仓库的 URL:

-
在构建触发器下选择 Poll SCM,以安排每次
15 分钟间隔。在 Schedule 文本框中输入以下代码
H/15 * * * *,如下图所示。出于测试目的,您可以减少轮询间隔:

- 向下滚动屏幕,点击“Build”下的“Add build step”按钮。在下拉列表中,选择“Execute shell”,并输入文本,如下截图所示:

-
最后,通过点击“保存”按钮保存配置。
-
返回 Jenkins 仪表板,你可以在仪表板上看到你的测试列出:

- 你可以等候 Jenkins 计划启动构建,或者点击屏幕右侧的时钟图标立即启动构建。构建完成后,仪表板会更新构建状态为成功或失败,并显示构建编号:

- 如果你将鼠标悬停在构建编号上,你会看到一个下拉按钮,提供一些选项,如“Changes”和“Console Output”,如下截图所示:

- “Console Output”选项将显示构建的详细信息,如下所示:
Started by user Vinod Singh
Building in workspace
/var/lib/jenkins/workspace/Docker-testing
Cloning the remote Git repository
Cloning repository
https://github.com/thedocker/testing
> git init \
/var/lib/jenkins/workspace/Docker-testing \
# timeout=10
Fetching upstream changes from
https://github.com/thedocker/testing
> git --version # timeout=10
Removing intermediate container 76a53284f1e3
Successfully built d9e22d1d52c6
+ docker run --rm docker_testing_using_jenkins
.
--------------------------------------------
--------------------------
Ran 1 test in 0.000s
OK
Finished: SUCCESS
- 现在,让我们测试由于错误的模块名
error_hitcount导致的失败案例,这是我们故意引入的。现在,让我们通过故意在test_hitcount.py中引入一个 bug 来实验一个负面场景,并观察其对 Jenkins 构建的影响。正如我们配置的 Jenkins,它会忠实地轮询 GitHub 并启动构建。
显然,构建失败正如我们预期的那样:

- 最后一步,打开失败构建的控制台输出:
Started by an SCM change
Building in workspace
/var/lib/jenkins/jobs/Docker-Testing/workspace
. . . OUTPUT TRUNCATED . . .
ImportError: No module named 'error_hitcount'
---------------------------------------------
-------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
Build step 'Execute shell' marked build as failure
Finished: FAILURE
显然,测试失败是由于我们故意引入的错误模块名error_hitcount。
酷吧?我们通过 Jenkins 和 Docker 实现了自动化测试。而且,我们能够体验到 Jenkins 和 Docker 带来的测试自动化的强大。在大规模项目中,Jenkins 和 Docker 可以结合使用,以自动化整个单元测试需求,从而自动捕捉由任何开发人员引入的缺陷和不足。
总结
容器化的潜在好处正在软件工程的各个领域得到发现。以往,测试复杂的软件系统需要多个昂贵且难以管理的服务器模块和集群。考虑到成本和复杂性,大部分软件测试都通过模拟过程和桩程序来完成。随着 Docker 技术的成熟,所有这些都将永远结束。Docker 的开放性和灵活性使其能够与其他技术无缝协作,显著减少测试时间和复杂性。
长期以来,测试软件系统的主要方式包括模拟、依赖注入等。这些方式通常要求在代码中创建许多复杂的抽象层。当前,开发和运行测试用例的做法实际上是在存根(stub)上进行,而不是在完整的应用程序上进行。这意味着,通过容器化工作流,完全有可能在具有所有依赖项的真实应用程序容器上进行测试。因此,Docker 范式,特别是在测试现象和阶段中的贡献,最近得到了细致的阐述和记录。准确来说,软件工程领域正朝着更加智能和光明的未来迈进,Docker 领域的创新正推动这一进程。
在本章中,我们清晰地阐述和解释了一种强大的集成应用程序测试框架,该框架采用了 Docker 启发的容器化范式。对于敏捷世界来说,经过验证且具有潜力的 TDD 方法正被坚持作为高效的软件构建和维持方法论。本章利用 Python 单元测试框架来说明 TDD 方法论如何成为软件工程的开创性工具。单元测试框架经过调整,以高效且优雅地容器化,并且 Docker 容器与 Jenkins 无缝集成。Jenkins 是一个现代的持续交付部署工具,是敏捷编程世界的重要组成部分,如本章所述。Docker 容器的源代码在进入 GitHub 代码仓库之前会进行预检查。Jenkins 工具从 GitHub 下载代码并在容器内运行测试。在下一章中,我们将深入探讨并描述通过容器技术进行进程隔离的理论方面,以及各种调试工具和技术。
第七章:调试容器
调试一直是软件工程领域中的一项艺术性工作。各种软件构建模块,无论是单独的还是集成的,都需要经过软件开发和测试专业人员的深入和决定性检查,以确保生成的软件应用程序的安全性和可靠性。由于 Docker 容器被认为是下一代关键任务软件工作负载的关键运行时环境,因此容器、开发人员和构建人员进行系统性、审慎的容器验证和验证显得尤为重要。
本章专门为那些掌握准确且相关信息的技术人员编写,帮助他们仔细调试容器内运行的应用程序及容器本身。在本章中,我们还将探讨容器内进程隔离的理论方面。Docker 容器在主机机器上作为用户级进程运行,通常具有操作系统提供的相同隔离级别。随着最新 Docker 版本的发布,许多调试工具已可用,可高效地用于调试应用程序。我们还将介绍主要的 Docker 调试工具,如 docker exec、stats、ps、top、events 和 logs。当前版本的 Docker 使用 Go 编写,并利用 Linux 内核的多个功能来提供其功能。
本章将涵盖的主题列表如下:
-
Docker 容器的进程级隔离
-
调试
Dockerfile -
调试容器化应用程序
本章中的所有命令都在 Ubuntu 环境中测试,如果在本地 Mac 环境中运行,结果可能会有所不同。
在主机机器上安装 Docker 引擎后,可以使用 -D 调试选项启动 Docker 守护进程:
$ docker -D login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username (vinoddandy):
此-D调试标志可以启用 Docker 配置文件(/etc/default/docker)中的调试模式:
DOCKER_OPTS="-D"
保存并关闭配置文件后,重新启动 Docker 守护进程。
Docker 容器的进程级隔离
在虚拟化范式中,虚拟机管理程序(hypervisor)模拟计算资源,并提供一个虚拟化环境(称为虚拟机 VM),在其上安装操作系统和应用程序。而在容器范式中,单一的系统(裸金属或虚拟机)实际上被划分为多个独立的服务,能够同时运行且不会相互干扰。这些服务必须相互隔离,以防止它们争用资源或发生依赖冲突(也叫依赖地狱)。Docker 容器技术通过利用 Linux 内核的构件(如命名空间和 cgroups),特别是命名空间,实现了进程级别的隔离。Linux 内核提供了以下五种强大的命名空间工具,用于隔离全球系统资源。它们是用于隔离进程间通信(IPC)资源的IPC命名空间:
-
network: 该命名空间用于隔离网络资源,如网络设备、网络堆栈和端口号
-
mount: 该命名空间隔离了文件系统的挂载点
-
PID: 该命名空间隔离了进程标识符(PID)
-
user: 该命名空间用于隔离用户 ID 和组 ID
-
UTS: 该命名空间用于隔离主机名和 NIS 域名
当我们需要调试容器内运行的服务时,这些命名空间增加了额外的复杂性,关于这点,你将在下一节中学到更多细节。
在这一节中,我们将通过一系列实践示例来讨论 Docker 引擎如何利用 Linux 命名空间提供进程级别的隔离,其中一个示例如下所示:
- 首先,使用
docker run子命令启动一个交互模式的 Ubuntu 容器,如下所示:
$ sudo docker run -it --rm ubuntu /bin/bash
root@93f5d72c2f21:/#
- 继续查找前述
93f5d72c2f21容器的进程 ID,可以通过在另一个终端使用docker inspect子命令来完成:
$ sudo docker inspect \
--format "{{ .State.Pid }}" 93f5d72c2f21
2543
显然,从上面的输出中,容器93f5d72c2f21的进程 ID 是2543。
- 获取了容器的进程 ID 后,我们继续查看容器关联的进程在 Docker 主机中是如何呈现的,可以使用
ps命令:
$ ps -fp 2543
UID PID PPID C STIME TTY TIME
CMD
root 2543 6810 0 13:46 pts/7 00:00:00
/bin/bash
真的吗?是不是很神奇?我们启动了一个以/bin/bash作为命令的容器,结果在 Docker 主机中也有了/bin/bash进程。
- 让我们进一步操作,使用
cat命令在 Docker 主机上显示/proc/2543/environ文件:
$ sudo cat -v /proc/2543/environ
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin /bin^@HOSTNAME=93f5d72c2f21^@TERM=xterm^@HOME=/root^@$
在前面的输出中,HOSTNAME=93f5d72c2f21从其他环境变量中脱颖而出,因为93f5d72c2f21既是容器 ID,也是我们之前启动的容器的主机名。
- 现在,让我们回到终端,我们正在运行交互式容器
93f5d72c2f21,并使用ps命令列出容器内运行的所有进程:
root@93f5d72c2f21:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 18:46 ? 00:00:00 /bin/bash
root 15 1 0 19:30 ? 00:00:00 ps -ef
令人惊讶,不是吗?在容器内,/bin/bash 进程的进程 ID 是 1,而在容器外的 Docker 主机上,进程 ID 是 2543。此外,父进程 ID (PPID) 是 0(零)。
在 Linux 世界中,每个系统只有一个 PID 为 1 和 PPID 为 0 的 root 进程,它是该系统完整进程树的根。Docker 框架巧妙地利用了 Linux 的 PID 命名空间来启动一个全新的进程树;因此,在容器内运行的进程无法访问 Docker 主机的父进程。然而,Docker 主机可以完全查看由 Docker 引擎创建的子 PID 命名空间。
网络命名空间确保所有容器在主机机器上都有独立的网络接口。此外,每个容器都有自己的回环接口。每个容器使用其自己的网络接口与外部世界通信。你会惊讶地发现,命名空间不仅有自己的路由表,而且还有自己的 iptables、链和规则。本章的作者正在他的主机上运行三个容器。在这里,理应为每个容器配备三个网络接口。让我们运行 docker ps 命令:
$ sudo docker ps
41668be6e513 docker-apache2:latest "/bin/sh -c 'apachec
069e73d4f63c nginx:latest "nginx -g '
871da6a6cf43 ubuntu "/bin/bash"
所以,这里有三个接口,每个容器一个。让我们通过运行以下命令来获取它们的详细信息:
$ ifconfig
veth2d99bd3 Link encap:EthernetHWaddr 42:b2:cc:a5:d8:f3
inet6addr: fe80::40b2:ccff:fea5:d8f3/64 Scope:Link
UP BROADCAST RUNNING MTU:9001 Metric:1
veth422c684 Link encap:EthernetHWaddr 02:84:ab:68:42:bf
inet6addr: fe80::84:abff:fe68:42bf/64 Scope:Link
UP BROADCAST RUNNING MTU:9001 Metric:1
vethc359aec Link encap:EthernetHWaddr 06:be:35:47:0a:c4
inet6addr: fe80::4be:35ff:fe47:ac4/64 Scope:Link
UP BROADCAST RUNNING MTU:9001 Metric:1
挂载命名空间确保挂载的文件系统仅对同一命名空间中的进程可访问。容器 A 无法看到容器 B 的挂载点。如果你想查看你的挂载点,需要先使用 exec 命令(将在下一部分中描述)登录到容器中,然后转到 /proc/mounts:
root@871da6a6cf43:/# cat /proc/mounts
rootfs / rootfsrw 0 0/dev/mapper/docker-202:1-149807 871da6a6cf4320f625d5c96cc24f657b7b231fe89774e09fc771b3684bf405fb / ext4 rw,relatime,discard,stripe=16,data=ordered 0 0 proc /procproc rw,nosuid,nodev,noexec,relatime 0 0
让我们运行一个具有挂载点的容器,该挂载点作为 存储区域网络 (SAN) 或 网络附加存储 (NAS) 设备运行,并通过登录到容器来访问它。这个部分作为练习交给你。我在工作中的一个项目中实现了这个功能。
还有其他命名空间可以将这些容器/进程隔离到,其中包括用户、IPC 和 UTS。用户命名空间允许你在命名空间内拥有 root 权限,而不会将该权限授予命名空间外的进程。使用 IPC 命名空间隔离进程时,它将拥有自己的 IPC 资源,例如,System V IPC 和 POSIX 消息。UTS 命名空间隔离了系统的主机名。
Docker 使用 clone 系统调用实现了这个命名空间。在主机机器上,你可以检查 Docker 为容器(PID 为 3728)创建的命名空间:
$ sudo ls /proc/3728/ns/
cgroup ipc mnt netpid user uts
在大多数工业化部署的 Docker 中,人们广泛使用已修补的 Linux 内核来满足特定需求。此外,一些公司修补了他们的内核,以便将任意进程附加到现有的命名空间中,因为他们认为这是部署、控制和编排容器最便捷且最可靠的方式。
控制组
Linux 容器依赖于控制组(cgroups),它不仅跟踪进程组,还暴露 CPU、内存和块 I/O 使用的度量。你可以访问这些度量并获得网络使用度量。Cgroups 是 Linux 容器的另一个重要组成部分。Cgroups 已经存在了一段时间,并且最初在 Linux 内核代码 2.6.24 中合并。它们确保每个 Docker 容器将获得固定的内存、CPU 和磁盘 I/O,因此任何容器都不能在任何情况下使主机机器崩溃。Cgroups 不起到防止一个容器被访问的作用,但它们对于抵御一些拒绝服务(DoS)攻击是至关重要的。
在 Ubuntu 16.04 中,cgroup 实现位于 /sys/fs/cgroup 路径下。Docker 的内存信息可以在 /sys/fs/cgroup/memory/docker/ 路径下找到。
同样,CPU 的详细信息可以在 /sys/fs/cgroup/cpu/docker/ 路径下找到。
让我们找出容器(41668be6e513e845150abd2dd95dd574591912a7fda947f6744a0bfdb5cd9a85)能够消耗的最大内存限制。
为此,你可以前往 cgroup 内存路径,并检查 memory.max_usage_in_bytes 文件:
/sys/fs/cgroup/memory/docker/41668be6e513e845150abd2dd95dd574591912a7
fda947f6744a0bfdb5cd9a85
执行以下命令查看内容:
$ cat memory.max_usage_in_bytes
13824000
因此,默认情况下,任何容器最多只能使用 13.18 MB 的内存。同样,CPU 参数可以在以下路径中找到:
/sys/fs/cgroup/cpu/docker/41668be6e513e845150abd2dd95dd574591912a7fda
947f6744a0bfdb5cd9a85
传统上,Docker 容器内部只运行一个进程。因此,通常你会看到有人分别为 PHP、NGINX 和 MySQL 启动三个容器。然而,这只是一个误解。你也可以将这三个进程都运行在同一个容器内。
Docker 隔离了许多底层主机的方面,允许容器中的应用程序在没有 root 权限的情况下运行。然而,这种隔离性不如虚拟机强大,虚拟机在 Hypervisor 上运行独立的操作系统实例,并且不会与底层操作系统共享内核。将具有不同安全配置文件的应用程序作为容器在同一主机上运行并不是一个好主意,但将不同的应用程序封装为容器化应用程序,避免它们直接在同一主机上运行,仍然有安全性上的好处。
调试容器化应用程序
计算机程序(软件)有时不能按预期行为执行。这可能是由于代码错误,或是开发、测试和部署系统之间的环境变化导致的。Docker 容器技术通过将所有应用程序依赖项容器化,尽可能消除开发、测试和部署之间的环境问题。然而,仍然可能存在由于代码缺陷或内核行为变化而导致的异常,这需要调试。调试是软件工程领域最复杂的过程之一,在容器化架构中,由于隔离技术的存在,调试变得更加复杂。在本节中,我们将学习一些使用 Docker 原生工具以及外部来源提供的工具来调试容器化应用程序的小技巧。
最初,许多 Docker 社区成员独立开发了自己的调试工具,但后来 Docker 开始支持原生工具,如 exec、top、logs 和 events。在本节中,我们将深入探讨以下 Docker 工具:
-
exec -
ps -
top -
stats -
events -
logs -
attach
我们还将考虑调试 Dockerfile。
docker exec 命令
docker exec 命令为用户提供了必要的帮助,尤其是那些部署自己 Web 服务器或有其他应用程序在后台运行的用户。现在,运行 SSH 守护进程不再需要登录到容器。
- 首先,创建一个 Docker 容器:
$ sudo docker run --name trainingapp \
training/webapp:latest
Unable to find image
'training/webapp:latest' locally
latest: Pulling from training/webapp
9dd97ef58ce9: Pull complete
a4c1b0cb7af7: Pull complete
Digest: sha256:06e9c1983bd6d5db5fba376ccd63bfa529e8d02f23d5079b8f74a616308fb11d
Status: Downloaded newer image for
training/webapp:latest
- 接下来,运行
docker ps -a命令以获取容器 ID:
$ sudo docker ps -a
a245253db38b training/webapp:latest
"python app.py"
- 然后,运行
docker exec命令以登录到容器:
$ sudo docker exec -it a245253db38b bash
root@a245253db38b:/opt/webapp#
- 请注意,
docker exec命令只能访问正在运行的容器,因此如果容器停止运行,你需要重启停止的容器才能继续。docker exec命令使用 Docker API 和 CLI 在目标容器中生成一个新进程。所以,如果你在目标容器内运行ps -aef命令,输出看起来如下:
# ps -aef
UID PID PPID C STIME TTY TIME
CMD
root 1 0 0 Nov 26 ? 00:00:53
python app.py
root 45 0 0 18:11 ? 00:00:00
bash
root 53 45 0 18:11 ? 00:00:00
ps -aef
在这里,python app.y 是已经在目标容器中运行的应用程序,而 docker exec 命令则在容器内启动了 bash 进程。如果你运行 kill -9 pid(45),你将自动退出容器。
如果你是一个热衷的开发者,想要增强 exec 功能,你可以参考 github.com/chris-rock/docker-exec。
建议仅将 docker exec 命令用于监控和诊断目的,我个人相信每个容器一个进程的理念,这是广泛强调的最佳实践之一。
docker ps 命令
docker ps 命令在容器内部可用,用于查看进程的状态。这个命令类似于 Linux 环境中的标准 ps 命令,而不是我们在 Docker 主机上运行的 docker ps 命令。
这个命令在 Docker 容器内部运行:
root@5562f2f29417:/# ps -s
UID PID PENDING BLOCKED IGNORED CAUGHT STAT TTY TIME COMMAND
0 1 00000000 00010000 00380004 4b817efb Ss
? 0:00 /bin/bash
0 33 00000000 00000000 00000000 73d3fef9 R+ ? 0:00 ps -s
root@5562f2f29417:/# ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 1 0 0 80 0 - 4541 wait ? 00:00:00 bash
root@5562f2f29417:/# ps -t
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 /bin/bash
35 ? R+ 0:00 ps -t
root@5562f2f29417:/# ps -m
PID TTY TIME CMD
1 ? 00:00:00 bash
- - 00:00:00 -
36 ? 00:00:00 ps
- - 00:00:00 -
root@5562f2f29417:/# ps -a
PID TTY TIME CMD
37 ? 00:00:00 ps
使用 ps --help <simple|list|output|threads|misc|all> 或 ps --help <s|l|o|t|m|a> 获取额外的帮助文本。
docker top 命令
您可以通过以下命令从 Docker 主机机器上运行 top 命令:
docker top [OPTIONS] CONTAINER [ps OPTIONS]
这将列出容器的运行进程列表,而无需登录到容器中,如下所示:
$ sudo docker top a245253db38b
UID PID PPID C
STIME TTY TIME CMD
root 5232 3585 0
Mar22 ? 00:00:53 python app.py
$ sudo docker top a245253db38b -aef
UID PID PPID C
STIME TTY TIME CMD
root 5232 3585 0
Mar22 ? 00:00:53 python app.py
Docker top 命令在 Docker 容器内运行时提供有关 CPU、内存和交换使用情况的信息:
root@a245253db38b:/opt/webapp# top
top - 19:35:03 up 25 days, 15:50, 0 users, load average: 0.00, 0.01, 0.05
Tasks: 3 total, 1 running, 2 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0%us, 0.0%sy, 0.0%ni, 99.9%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 1016292k total, 789812k used, 226480k free, 83280k buffers
Swap: 0k total, 0k used, 0k free, 521972k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM
TIME+ COMMAND
1 root 20 0 44780 10m 1280 S 0.0 1.1 0:53.69 python
62 root 20 0 18040 1944 1492 S 0.0 0.2 0:00.01 bash
77 root 20 0 17208 1164 948 R 0.0 0.1 0:00.00 top
如果在容器内运行 top 命令时出现 error - TERM environment variable not set 错误,请执行以下步骤来解决:
运行 echo $TERM 命令。您将得到结果 dumb。然后执行以下命令:
$ export TERM=dumb
这将解决错误。
docker stats 命令
docker stats 命令使您能够从 Docker 主机机器查看容器的内存、CPU 和网络使用情况,如下所示:
$ sudo docker stats a245253db38b
CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O
a245253db38b 0.02% 16.37 MiB/992.5 MiB 1.65%
3.818 KiB/2.43 KiB
您也可以运行 stats 命令来查看多个容器的使用情况:
$ sudo docker stats a245253db38b f71b26cee2f1
Docker 提供对容器统计信息的只读访问 read only 参数。这简化了容器的 CPU、内存、网络 IO 和块 IO。Docker stats 实用程序仅为运行中的容器提供这些资源使用情况详细信息。
Docker events 命令
Docker 容器将报告以下实时事件:create, destroy, die, export, kill, omm, pause, restart, start, stop 和 unpause。以下是几个示例,说明如何使用这些命令:
$ sudo docker pause a245253db38b
a245253db38b
$ sudo docker ps -a
a245253db38b training/webapp:latest "python app.py"
4 days ago Up 4 days (Paused) 0.0.0.0:5000->5000/tcp sad_sammet
$ sudo docker unpause a245253db38b
a245253db38b
$ sudo docker ps -a
a245253db38b training/webapp:latest "python app.py"
4 days ago Up 4 days 0.0.0.0:5000->5000/tcpsad_sammet
Docker 镜像还将报告 untag 和 delete 事件。
多个过滤器的使用将作为 AND 操作处理;例如,
--filter container= a245253db38b --filter event=start 将显示容器 a245253db38b 的事件,并且事件类型为 start。
目前支持的过滤器有 container、event 和 image。
docker logs 命令
此命令在不登录到容器中的情况下获取容器的日志。它批量检索执行时存在的日志。这些日志是 stdout 和 stderr 的输出。通常用法如 docker logs [OPTIONS] CONTAINER 所示。
-follow 选项将持续提供输出直到结束,-t 将提供时间戳,--tail= <number of lines> 将显示容器日志消息的行数:
$ sudo docker logs a245253db38b
* Running on http://0.0.0.0:5000/
172.17.42.1 - - [22/Mar/2015 06:04:23] "GET / HTTP/1.1" 200 -
172.17.42.1 - - [24/Mar/2015 13:43:32] "GET / HTTP/1.1" 200 -
$ sudo docker logs -t a245253db38b
2015-03-22T05:03:16.866547111Z * Running on http://0.0.0.0:5000/
2015-03-22T06:04:23.349691099Z 172.17.42.1 - - [22/Mar/2015 06:04:23] "GET / HTTP/1.1" 200 -
2015-03-24T13:43:32.754295010Z 172.17.42.1 - - [24/Mar/2015 13:43:32] "GET / HTTP/1.1" 200 -
我们还在 第二章 中使用了 docker logs 实用程序,处理 Docker 容器 和 第六章,在容器中运行服务,来查看我们容器的日志。
docker attach 命令
docker attach 命令附加正在运行的容器,当您想实时查看 stdout 中的内容时非常有用:
$ sudo docker run -d --name=newtest alpine /bin/sh -c "while true; do sleep 2; df -h; done"
Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
3690ec4760f9: Pull complete
Digest: sha256:1354db23ff5478120c980eca1611a51c9f2b88b61f24283ee8200bf9a54f2e5c
1825927d488bef7328a26556cfd72a54adeb3dd7deafb35e317de31e60c25d67
$ sudo docker attach newtest
Filesystem Size Used Available Use% Mounted on
none 7.7G 3.2G 4.1G 44% /
tmpfs 496.2M 0 496.2M 0% /dev
tmpfs 496.2M 0 496.2M 0% /sys/fs/cgroup
/dev/xvda1 7.7G 3.2G 4.1G 44% /etc/resolv.conf
/dev/xvda1 7.7G 3.2G 4.1G 44% /etc/hostname
/dev/xvda1 7.7G 3.2G 4.1G 44% /etc/hosts
shm 64.0M 0 64.0M 0% /dev/shm
tmpfs 496.2M 0 496.2M 0% /proc/sched_debug
Filesystem Size Used Available Use% Mounted on
none 7.7G 3.2G 4.1G 44% /
tmpfs 496.2M 0 496.2M 0% /dev
默认情况下,该命令会附加 stdin 并代理信号到远程进程。可以使用选项控制这两种行为。要从进程中分离,请使用默认的Ctrl + C组合键。
调试一个 Dockerfile
有时,创建一个Dockerfile并不会一开始就让一切正常工作。Dockerfile并不总是能构建镜像,有时能构建镜像,但启动容器时会崩溃。
我们在Dockerfile中设置的每个指令都将被构建为一个独立的临时镜像,供其他指令在其基础上构建。以下示例解释了这一点:
- 使用你喜欢的编辑器创建一个
Dockerfile:
FROM busybox
RUN ls -lh
CMD echo Hello world
- 现在,通过执行以下命令来构建镜像:
$ docker build .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM busybox
latest: Pulling from library/busybox
56bec22e3559: Pull complete
Digest: sha256:29f5d56d12684887bdfa50dcd29fc31eea4aaf4ad3bec43daf19026a7ce69912
Status: Downloaded newer image for busybox:latest
---> e02e811dd08f
Step 2 : RUN ls -lh
---> Running in 7b47d3c46cfa
total 36
drwxr-xr-x 2 root root 12.0K Oct 7 18:18 bin
dr-xr-xr-x 130 root root 0 Nov 27 01:36 proc
drwxr-xr-x 2 root root 4.0K Oct 7 18:18 root
dr-xr-xr-x 13 root root 0 Nov 27 01:36 sys
drwxrwxrwt 2 root root 4.0K Oct 7 18:18 tmp
---> ca5bea5887d6
Removing intermediate container 7b47d3c46cfa
Step 3 : CMD echo Hello world
---> Running in 490ecc3d10a9
---> 490d1c3eb782
Removing intermediate container 490ecc3d10a9
Successfully built 490d1c3eb782
**$**
注意---> Running in 7b47d3c46cfa这一行。7b47d3c46cfa是一个有效的镜像,可以用来重试失败的指令,并查看发生了什么。
要调试这个镜像,我们需要创建一个容器,然后登录进去分析错误。调试是分析发生了什么的过程,它因情况而异,但通常,我们开始调试的方法是尝试手动使失败的指令正常工作并理解错误。当我使指令正常工作时,通常会退出容器,更新我的Dockerfile,然后重复这一过程,直到得到一个有效的结果。
总结
在本章中,你已经看到使用 Linux 容器技术(如 LXC,现在是 Libcontainer)进行容器隔离的过程。Libcontainer 是 Docker 在 Go 编程语言中实现的,用于访问内核命名空间和 cgroups。这个命名空间用于进程级别的隔离,而 cgroups 则用于限制运行中容器的资源使用。由于容器直接作为独立进程运行在 Linux 内核之上,通用可用(GA)的调试工具不足以在容器内部调试容器化进程。Docker 现在为你提供了一整套工具,用于有效地调试容器以及容器内部的进程。docker exec命令允许你登录到容器,而无需在容器内运行 SSH 守护进程。本章中你已经看到过每个调试工具的详细内容。
docker stats命令提供容器的内存和 CPU 使用信息。docker events命令报告事件,如创建、销毁和停止。同样,docker logs命令可以在不登录容器的情况下获取容器的日志。
下一步,你可以尝试最新的 Microsoft Visual Studio Docker 工具。它提供了一种一致的方式来开发和验证在 Linux Docker 容器中的应用程序。详情请参考docs.microsoft.com/en-us/azure/vs-azure-tools-docker-edit-and-refresh。
如果你想在 IDE(如 Visual Studio Code)中实时调试运行中的 Node.js 应用程序,可以参考这个博客:blog.docker.com/2016/07/live-debugging-docker/。
下一章将阐述 Docker 容器可能面临的安全威胁,以及如何通过各种安全方法、自动化工具、最佳实践、关键指南和指标来应对这些威胁。我们还将讨论容器与虚拟机的安全性对比,并探讨 Docker 在第三方安全工具和实践方面的适应性。
第八章:容器安全性
到目前为止,我们在本书中已经讨论了很多快速发展的 Docker 技术。如果不详细阐述 Docker 特有的安全问题及其解决方法,那就不算是一个完整的结尾。因此,本章特别为此而编写,并加入到本书中,目的是全面解释 Docker 启发的容器化的安全挑战。我们还希望更多地揭示如何通过一系列开创性的技术、高质量的算法、支持工具和最佳实践来应对长期存在的安全隐患。本章中,我们将详细讨论以下关键主题:
-
Docker 容器安全吗?
-
容器的安全特性
-
新兴的安全支持方法
-
确保容器安全的最佳实践
容器化领域的安全态势
确保任何 IT 系统和商业服务的不可破坏和无法渗透的安全性,一直是几十年来 IT 领域的主要需求和主导挑战。聪明的人才能够识别并利用各种安全漏洞和缺陷(其中一些在系统构思和实现阶段就已经被疏忽或无意间引入)。这些漏洞最终会导致无数的安全 breaches 和混乱,影响 IT 服务的交付。有时,系统甚至变得无法为消费者和客户提供服务。
然而,安全专家和工程师们则尝试使用各种技巧和技术,力图阻止黑客在他们的恶意旅程中得逞。然而,到目前为止,这场战斗并没有取得彻底的胜利。时不时地,来自未知来源的侵入事件引发了令人不安的 IT 系统慢速,有时甚至崩溃。因此,全球的组织和政府正在大量投资他们的才智、时间和财富于安全研究,以完全消除所有与安全和安全性相关的事件和事故。许多专注于安全的产品供应商和托管安全服务提供商致力于最小化安全威胁和漏洞对 IT 系统造成的无法修复和难以描述的后果。准确来说,对于任何现有或新兴技术,安全一直是最至关重要的方面。需要注意的是,企业和云计算 IT 团队在满足安全需求时,不能掉以轻心或自满。
基于 Docker 的容器化代表了从物理、未充分利用、封闭、单体且单租户的 IT 资源到灵活、开放、经济、自动化、共享、面向服务、优化利用并虚拟化的 IT 资源的不可抗拒而又充满意义的旅程的下一个逻辑步骤。精确来说,我们正朝着软件定义和容器化云环境的方向发展,以便收获一系列广泛表述的商业、技术和用户利益。正如本书多次强调的那样,Docker 容器通常包括文件系统、网络栈、进程空间以及运行应用所需的一切。这意味着每个 Docker 容器都包含指定的应用程序及其所有依赖项,并以独立的方式进行打包、托管和执行。然而,这种广受赞誉的抽象化,容易受到新型且先进的安全攻击、漏洞和缺口的影响。系统可能无法访问,数据集可能被突破,服务可能会停止,等等。
精确来说,炙手可热的 Docker 技术承诺将极大地改变全球企业开发、部署和管理关键软件应用程序的方式。然而,容器并不是万能的。我们在混合 IT 环境中部署和交付应用程序时面临的相同挑战,也会在容器中得到复制。本章明确指出了减轻容器化引发的安全问题的有效方法。随着云环境被广泛容器化,坚不可摧且无法渗透的容器最终确保了安全、可靠和智能的云数据中心。长期目标是将许多强大、弹性和有价值的容器放置在公开可发现的地点。毫无疑问,已经有一些开创性的工具和平台,通过混合和协作,将这些可定制、可配置和紧凑的容器组合成更大、更好的容器。
Docker 容器的安全影响
Docker 技术日益流行,主要是因为 Docker 公司与其他相关方合作,推出了一个开源且具有行业强度的镜像格式,用于高效地打包、分发和运行软件应用程序。然而,将许多应用程序塞入一个系统中会带来明显的担忧和漏洞:
-
利用主机内核:容器共享相同的主机内核,而这种共享可能会成为容器化范式的单点故障。主机内核中的缺陷可能允许容器内的进程突破并使主机机器崩溃。因此,Docker 安全领域的工作就是探索各种选项,限制和控制内核的攻击面。安全管理员和架构师必须精心利用主机操作系统的安全功能来保护内核。
-
拒绝服务(DoS)攻击:所有容器都必须共享内核资源。如果某个容器可以垄断对某些资源(包括内存和处理能力)的访问,那么宿主机上的其他容器必定会因计算、存储和网络资源的不足而无法正常工作。最终,DoS 的问题就会悄然出现,合法用户将难以访问服务。
-
容器突破:一旦攻击者获得了对某个容器的访问权限,就不应能够访问其他容器或宿主机。默认情况下,用户没有命名空间,因此,任何突破容器的进程将拥有与在容器中相同的权限。也就是说,如果某个进程具有 root 权限,那么它在宿主机上也拥有 root 权限。这意味着用户可以通过应用程序代码中的漏洞获取提升的权限,甚至是 root 权限。最终,结果将是无法修复的损害。因此,我们需要遵循最小权限原则:每个进程和容器都应该以最小的访问权限和资源集运行。
-
中毒镜像:Docker 镜像也可能被破坏和篡改,从而导致不良容器和宿主机的出现。我们曾经讨论过如何在镜像仓库中彻底清理和管理 Docker 镜像。类似地,也采取了强有力的访问控制机制,以减轻镜像中毒的风险。
因此,Docker 镜像、容器、集群、宿主机和云服务将不可避免地受到病毒、恶意软件和其他关键威胁的影响。因此,Docker 安全领域近年来已经成为研究人员和从业者最具挑战性的领域之一,未来我们可以期待一系列具有突破性且增强安全性的算法、方法和论述。
安全方面 - 虚拟机与 Docker 容器对比
随着 Docker 容器的采用和适应不断增加,Docker 安全性被赋予了极高的重要性。毫无疑问,为了确保 Docker 容器的最大安全性,已经有大量的工作在进行,而 Docker 平台的最新版本也嵌入了多种安全增强功能。
在本节中,我们将描述在安全问题上,Docker 容器所处的位置。由于容器与虚拟机(VMs)正在密切配合进行研究,我们将从虚拟机和容器的一些安全相关点开始。首先,了解虚拟机和容器之间的区别。通常,虚拟机是重量级的,因此显得臃肿,而容器则是轻量级的,因此精简且流畅。下表概述了虚拟机和容器的知名特点:
| 虚拟机 | 容器 |
|---|---|
| 一些虚拟机在单一物理机器上共同运行(低密度)。 | 数十个容器可以在单一物理机器或虚拟机上运行(高密度)。 |
| 这确保了虚拟机的完全隔离以增强安全性。 | 这使得在进程级别上实现隔离,并通过诸如命名空间(namespace)和控制组(cgroups)等特性提供额外的隔离。 |
| 每个虚拟机都有自己的操作系统和由底层虚拟机监控程序管理的物理资源。 | 容器与 Docker 主机共享相同的内核。 |
| 在网络方面,虚拟机可以与虚拟交换机或物理交换机连接。虚拟机监控程序具有用于提高 I/O 性能、NIC 绑定等的缓冲区。 | 容器利用标准的进程间通信(IPC)机制,如信号、管道、套接字等来实现网络通信。每个容器都有自己的网络栈。 |
以下图表展示了基于虚拟机监控程序的虚拟化如何实现从物理机中创建虚拟机:

以下图表生动地展示了容器化如何显著偏离基于虚拟机监控程序的虚拟化:

关于虚拟机(VM)和容器的安全性讨论正变得越来越激烈。对于这两者,都有支持和反对的观点。在虚拟化范式中,虚拟机监控程序(hypervisor)是虚拟机的集中式核心控制器。任何新创建虚拟机的访问都必须通过这个虚拟机监控程序解决方案,它作为一道坚固的防线,防止任何未经认证、未授权和不道德的目的。因此,虚拟机的攻击面相较于容器来说要小。黑客必须攻破或入侵虚拟机监控程序,才能影响其他虚拟机。这意味着攻击者必须先绕过虚拟机内核和虚拟机监控程序,才能接触到宿主机内核。
与虚拟化范式不同,容器直接部署在宿主系统的内核之上。这种简洁高效的架构提供了更高的效率,因为它完全消除了虚拟机监控程序的仿真层,并且能够提供更高的容器密度。然而,与虚拟机范式不同,容器范式没有那么多的层次结构,因此如果任何容器被攻破,攻击者就很容易访问宿主机和其他容器。因此,容器的攻击面相较于虚拟机来说要大。
然而,Docker 平台的设计者已经充分考虑了这一安全风险,并设计了系统来防止大多数安全风险。在接下来的部分,我们将讨论系统中天生设计的安全性、为显著增强容器安全性而采取的解决方案以及最佳实践和指南。
容器的突出安全实现特性
Linux 容器,尤其是 Docker 容器,天生具备一些有趣的安全实现特性。
如前所述,Docker 使用了一系列安全防护措施来防止容器越狱。也就是说,如果一个安全机制被突破,其他机制会迅速阻止容器被黑客攻击。在评估 Docker 容器的安全性时,有几个主要领域需要检查。如前所述,Docker 为容器化应用程序提供了多种隔离功能,显著提高了它们的安全性。大多数功能是开箱即用的。细粒度的策略添加、废除和修改功能能够满足容器化的安全需求。Docker 平台允许您执行以下操作:
-
将应用程序彼此隔离
-
将应用程序与宿主机隔离
-
通过限制应用程序的能力来提高其安全性
-
鼓励采用最小权限原则
这个开源平台天生能够为不同运行时环境(如虚拟机、裸金属服务器和传统 IT)中的各种应用程序提供这些隔离功能。
不可变基础设施
当您为应用程序部署更新时,应该创建新的实例(服务器和/或容器)并销毁旧的实例,而不是尝试在原地进行升级。一旦应用程序开始运行,你就不再触碰它了! 这样带来的好处包括可重复性、减少管理开销、便于回滚等。一张不可变镜像是包含运行应用程序所需的一切内容的镜像,因此它包含了源代码。Docker 容器的一个原则是镜像是不可变的。也就是说,一旦构建完成,它就不能更改,如果你想做更改,你将得到一个新的镜像作为结果。
Docker 容器是自给自足的,因此我们只需要运行容器,而不必为其他事项(如挂载卷)担忧。这意味着我们可以以更简单、更透明的方式与用户或合作伙伴共享我们的应用程序。直接的结果是,我们可以通过 Kubernetes 等工具轻松地以自动化的方式扩展我们的系统,这些工具允许我们在一组机器上运行一组容器,也就是说,一个集群。
最后,不可变的容器如果被人尝试篡改,必定会崩溃,因此任何导致故障的操作都会在最初阶段就被无效化。
资源隔离
正如我们所知,容器正逐步成为微服务架构(MSA)时代的核心组成部分。也就是说,在一个系统中,可以有多个通用的以及特定目的的服务,它们可以动态地协作,以实现易于维护的分布式应用程序。随着物理系统中服务的多样性和异质性不断增加,安全复杂性自然会急剧上升。因此,需要明确界定和隔离资源,以避免任何潜在的安全漏洞。被广泛接受的安全方法是利用内核特性,包括命名空间。以下是命名空间和 cgroups 的解释:
-
命名空间:Linux 命名空间将一组系统资源封装,并将其呈现给命名空间内的进程,使得它们看起来似乎是专门为这些进程分配的。简而言之,命名空间是一种资源管理工具,有助于为进程隔离系统资源。内核命名空间提供了最基本的隔离形式。运行在容器中的进程不会影响其他容器或主机系统中的进程。网络命名空间确保每个容器都拥有自己的网络栈,从而限制了对其他容器接口的访问。
-
Cgroups:这是一个 Linux 内核概念,用于管理一组进程的系统资源隔离和使用情况,如 CPU 和内存。例如,如果你有一个占用大量 CPU 周期和内存的应用程序,比如科学计算应用,你可以将该应用放入一个 cgroup 中,以限制其 CPU 和内存的使用。它确保每个容器能够公平地分配内存、CPU 和磁盘 I/O,更重要的是,单个容器无法通过耗尽这些资源导致系统崩溃。
资源计量与控制
容器需要消耗不同的物理资源,以提供其独特的功能。然而,资源消耗必须有序、规范,因此需要严格的管理。当出现偏差时,容器无法按时执行指定任务的可能性就会增大。例如,如果资源使用不同步,就可能导致 DoS(拒绝服务)问题。
Linux 容器利用 cgroups 实现资源计量和审计,以无障碍地运行应用程序。正如我们所知道的,运行容器成功所需的资源有很多。它们提供许多有用的度量,并确保每个容器获得其公平的内存、CPU 和磁盘 I/O 配额。此外,它们保证单个容器无法通过耗尽这些资源中的任何一项来使系统崩溃。此功能帮助你防御一些 DoS 攻击。此功能有助于在云环境中以多租户方式运行容器,以确保其正常运行时间和性能。任何其他容器的利用行为都会被主动识别并从源头上杜绝,从而避免任何不当行为。
root 权限 - 影响及最佳实践
Docker 引擎通过利用前面提到的资源隔离和控制技术,有效地保护容器免受任何恶意活动。然而,由于 Docker 守护进程以 root 权限运行,Docker 暴露出一些潜在的安全威胁。在本节中,我们列出了一些安全风险以及缓解这些风险的最佳实践。
另一个需要遵循的重要原则是最小权限。容器中的每个进程必须以最小的访问权限和资源运行,以完成其功能。这里的优势在于,如果一个容器被攻破,其他资源和数据可以避免进一步的攻击。
受信任用户控制
由于 Docker 守护进程以 root 权限运行,因此它有能力将 Docker 主机上的任何目录挂载到容器中,而不会限制任何访问权限。也就是说,你可以启动一个容器,其中 /host 目录将是主机上的 / 目录,而容器将能够在没有任何限制的情况下更改主机文件系统。这只是众多恶意用途中的一个例子。考虑到这些活动,Docker 的后续版本通过 UNIX 套接字限制了对 Docker 守护进程的访问。如果你明确决定这样做,Docker 可以配置为通过 HTTP 的 REST API 访问守护进程。然而,你应该确保它仅能从受信任的网络或 VPN 访问,或者通过 stunnel 和客户端 SSL 证书保护。你还可以通过 HTTPS 和证书来保护它们。
非根容器
如前所述,Docker 容器默认以根权限运行,容器内运行的应用程序也是如此。从安全角度来看,这是另一个主要的担忧,因为黑客可以通过攻击容器内运行的应用程序获得 Docker 主机的根权限。Docker 提供了一个简单而强大的解决方案,将容器的权限更改为非根用户,从而防止恶意的根访问 Docker 主机。这个非根用户的修改可以通过 docker run 子命令中的 -u 或 --user 选项,或者通过 Dockerfile 中的 USER 指令来实现。
在本节中,我们将通过展示 Docker 容器的默认根权限,并继续使用 Dockerfile 中的 USER 指令将根权限修改为非根用户,来进行演示。
首先,通过在 docker run 子命令中运行简单的 id 命令,展示 Docker 容器的默认根权限,如下所示:
$ sudodocker run --rm ubuntu:16.04 id
uid=0(root) gid=0(root) groups=0(root)
现在,让我们执行以下步骤:
- 编写一个
Dockerfile,创建一个非根权限用户,并将默认的根用户修改为新创建的非根权限用户,如下所示:
##########################################
# Dockerfile to change from root to
# non-root privilege
###########################################
# Base image is Ubuntu
FROM ubuntu:16.04
# Add a new user "peter" with user id 7373
RUN useradd -u 7373 peter
# Change to non-root privilege
USER peter
- 使用
docker build子命令构建 Docker 镜像,如下所示:
$ sudo docker build -t nonrootimage .
- 最后,让我们通过在
docker run子命令中使用id命令来验证容器当前的用户:
$ sudo docker run --rm nonrootimage id
uid=7373(peter) gid=7373(peter) groups=7373(peter)
显然,容器的用户、组和组现在已更改为非根用户。
将默认的根权限修改为非根权限是防止恶意渗透到 Docker 主机内核的一个非常有效的方式。
到目前为止,我们讨论了与安全相关的独特内核特性和功能。通过理解和应用这些内核特性,大多数安全漏洞可以被修补。安全专家和倡导者在考虑到容器化理念在生产环境中的快速普及后,提出了更多的安全解决方案,具体描述如下。这些安全方法在开发、部署和交付企业级容器时,开发人员和系统管理员必须给予极高的重视,以防范任何内部或外部的安全攻击。
容器安全的 SELinux
增强安全 Linux(SELinux)是一个勇敢的尝试,旨在清除 Linux 容器中的安全漏洞,并且它是强制访问控制(MAC)机制、多级安全(MLS)和多类别安全(MCS)在 Linux 内核中的实现。有一个新的协作项目,称为 sVirt 项目,它正在基于 SELinux 构建,并且正与 Libvirt 集成,为虚拟机和容器提供一个适应性的 MAC 框架。这一新架构为容器提供了一个受保护的隔离和安全网,主要是防止容器内的 root 进程与外部其他进程进行交互和干扰。Docker 容器会自动分配到 SELinux 策略中指定的 SELinux 上下文。
SELinux 总是在标准自主访问控制(DAC)完全检查之后,检查所有允许的操作。SELinux 可以根据定义的策略在 Linux 系统上建立和执行文件和进程的规则,并对其行为进行管理。根据 SELinux 的规格,文件(包括目录和设备)被称为对象。类似地,进程,如运行命令的用户,则被称为主体。大多数操作系统使用 DAC 系统来控制主体如何与对象及彼此之间进行交互。通过在操作系统上使用 DAC,用户可以控制自己对象的权限。例如,在 Linux 操作系统上,用户可以使他们的主目录可读,从而给用户和主体提供了窃取潜在敏感信息的途径。然而,仅仅依靠 DAC 并不能提供万无一失的安全方法,DAC 的访问决策完全依赖于用户身份和所有权。通常,DAC 忽略了其他安全启用的参数,例如用户的角色、程序的功能、可信度以及数据的敏感性和完整性。
由于每个用户通常对自己的文件拥有完全的自由裁量权,因此确保系统范围的安全策略非常困难。此外,每个用户运行的程序仅继承授予该用户的所有权限,用户可以自由地更改对其文件的访问权限。这一切导致了对恶意软件的最低保护。许多系统服务和特权程序以粗粒度的权限运行,因此这些程序中的任何漏洞都可以轻松被利用,并进一步扩展,导致系统的灾难性访问。
如开头所述,SELinux 在 Linux 内核中增加了 MAC 机制。这意味着对象的所有者无法控制或决定对象的访问。内核强制执行 MAC,它是一种通用的 MAC 机制,并且需要能够强制执行管理员设定的安全策略,应用于系统中的所有进程和文件。这些文件和进程将基于包含各种安全相关信息的标签来做出决策。
MAC 具有足够保护系统的内在能力。此外,MAC 确保应用程序的安全性,防止任何恶意的黑客攻击和篡改。MAC 还提供强大的应用程序隔离,使任何被攻击或被破坏的应用程序能够独立运行。
接下来是 MCS。它主要用于保护容器免受其他容器的影响。也就是说,任何受影响的容器都没有能力使同一 Docker 主机中的其他容器崩溃。MCS 基于 MLS 能力,并独特地利用 SELinux 标签的最后一个组件,MLS 字段。一般来说,当容器启动时,Docker 守护进程会选择一个随机的 MCS 标签。Docker 守护进程会将容器内的所有内容标记为该 MCS 标签。当守护进程启动容器进程时,它会告诉内核用相同的 MCS 标签标记这些进程。内核只允许容器进程读写与其 MCS 标签匹配的文件系统内容。内核会阻止容器进程读取或写入带有不同 MCS 标签的内容。这样,被攻击的容器进程就无法攻击其他容器。Docker 守护进程负责确保没有容器使用相同的 MCS 标签。通过巧妙使用 MCS,容器之间的错误级联被有效地阻止。
SELinux 在 Ubuntu 16.04 中默认未安装,与 Red Hat Fedora 或 CentOS 发行版不同,因此需要通过运行apt-get命令来安装 SELinux,如下所示:
$ sudo apt-get install selinux
然后通过运行以下sed脚本继续启用 SELinux 模式:
$ sudo sed -i 's/SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
$ sudo sed -i 's/SELINUXTYPE=.*/SELINUXTYPE=default/' \
/etc/selinux/config
应用程序保护(AppArmor)是一个有效且易于使用的 Linux 应用程序安全系统。AppArmor 主动保护操作系统和应用程序免受任何外部或内部威胁,并防止即使是未知的应用程序漏洞被黑客滥用。AppArmor 现已用于确保 Docker 容器及容器内的应用程序的安全。政策制定正在成为确保容器安全的强大机制。政策制定和自动化执行政策在确保容器安全方面发挥着重要作用。Ubuntu 16.04 默认配备了 AppArmor,因此强烈建议使用它。
在 Docker 版本 1.13.0 及之后的版本中,Docker 二进制文件会在 TMPFS 中生成此配置文件,然后将其加载到内核中。而在 1.13.0 之前的 Docker 版本中,这个配置文件则会生成在/etc/apparmor.d/docker中。
docker-default配置文件是运行容器时的默认配置文件。它提供适度的保护,同时保持广泛的应用程序兼容性。当你运行容器时,除非使用security-opt选项覆盖,否则会使用docker-default策略。例如,以下明确指定了默认策略:
$ docker run --rm -it --security-opt \
apparmor=docker-default hello-world
安全计算模式(seccomp)由 Docker 引擎支持,这是一项在 Linux 内核中提供的安全特性。它允许管理员限制容器内可用的操作,细化到单一系统调用的粒度。这一能力极大地限制了应用程序容器对宿主系统的访问,从而减少执行操作的可能性。企业可以根据需要配置 seccomp 配置文件,并将其应用于 Docker 环境。
默认的 seccomp 配置文件为使用 seccomp 运行的容器提供了一个合理的默认设置,禁用了 300 多个系统调用中的大约 44 个。它提供了适度的保护,同时保证了广泛的应用兼容性。
大多数应用程序都可以在默认配置文件下正常运行。事实上,默认配置文件已经能够主动保护 Docker 化的应用程序免受几个以前未知的错误影响。
该功能在 Ubuntu 16.04 中默认启用:
$ cat /boot/config-`uname -r` | grep CONFIG_SECCOMP= CONFIG_SECCOMP=y
SCONE:基于 Intel SGX 的安全 Linux 容器,由 Sergei Arnautov 及其团队描述,是一种为 Docker 设计的安全容器机制,利用 Intel CPU 的 SGX 受信执行支持来保护容器进程免受外部攻击。SCONE 的设计目标如下:
-
首先,它实现了小的受信计算基础(TCB)
-
其次,它必须具有较低的性能开销
SCONE 提供了一个安全的 C 标准库接口,透明地加密/解密 I/O 数据,显著减少了线程同步和系统调用在 SGX 安全区内的性能影响。SCONE 支持用户级线程和异步系统调用。根据他们的研究论文,Docker 爱好者对 SCONE 的评估给予了高度评价。
加载 Docker 镜像及其安全影响
Docker 通常从网络拉取镜像,这些镜像通常在源头经过策划和验证。然而,为了备份和恢复的目的,Docker 镜像可以使用docker save子命令保存,并使用docker load子命令加载回来。这个机制也可以用来通过非常规方式加载第三方镜像。不幸的是,在这种实践中,Docker 引擎无法验证来源,因此镜像可能包含恶意代码。因此,作为安全的第一个防线,Docker 会在chrooted子进程中提取镜像以进行权限分离。尽管 Docker 确保了权限分离,但不推荐加载任意镜像。
使用容器扫描来确保 Docker 部署的安全性:Docker 内容信任(DCT)为发布者提供了一种简便而快速的方式,保证在如 Docker Hub 这样的 web 大规模仓库中发布的容器的真实性。然而,组织需要采取务实的措施,访问、评估并采取相应的行动,以确保容器化应用程序在整个生命周期中的安全性。准确来说,DCT 是一种确保你创建的 Docker 镜像的签名安全的方法,以确保它们的来源可信。
使用 Black Duck Hub 管理容器安全性:Black Duck Hub 是一款重要工具,用于在整个应用生命周期中管理应用容器的安全性。Black Duck Hub 允许组织识别和追踪其环境中的开源应用和组件的漏洞。评估工作基于 Black Duck 的知识库,该库包含 110 万个开源项目的信息,以及超过 100,000 个已知开源漏洞的详细数据,覆盖超过 3500 亿行代码。通过与 Red Hat 的合作,Black Duck 能够识别和清点开源及专有代码生产环境,这一能力也正在应用于容器化环境。Red Hat 推出了 深度容器检查(DCI),这是一款面向企业的解决方案,将容器认证、策略和信任整合到一个部署和管理应用容器的整体架构中。作为 DCI 的一部分,Red Hat 与 Black Duck 合作,提供一种在部署前、部署中和部署后验证容器内容的方式。
Black Duck Hub 的漏洞扫描和映射能力集成使 OpenShift 用户能够以更高的信心和安全性使用、开发和运行容器化应用程序,知道这些应用程序包含已经经过独立验证和认证的代码。该集成还提供了跟踪新披露的漏洞或与容器老化相关的变化的手段,这些变化可能会影响安全性和风险。Black Duck Hub 的应用漏洞扫描和映射能力使 Docker 用户能够在部署前后识别漏洞,并发现随着容器化应用老化或暴露于新的安全漏洞和攻击时出现的问题。
使用 TUF 进行镜像签名和验证
Docker 社区期望对 Docker 化软件的代码和版本提供强有力的加密保证。DCT 是与 Docker 平台 1.8 版本相关的新安全功能。DCT 本质上将 更新框架(TUF)集成到 Docker 中,使用 Notary 这一开源工具,为任何内容提供信任保障。
TUF 帮助开发者保护新的或现有的软件更新系统,这些系统通常容易受到许多已知攻击的威胁。TUF 通过提供一个全面且灵活的安全框架来解决这个普遍问题,开发者可以将其与任何软件更新系统集成。软件更新系统是一个在客户端系统上运行的应用程序,用于获取和安装软件。这可以包括已安装软件的更新,甚至是全新的软件。
防镜像伪造的保护:一旦建立了信任,DCT 提供了防范恶意攻击者(拥有特权网络位置的攻击者,也称为中间人攻击(MitM))的能力。
防重放攻击的保护:在典型的重放攻击中,之前有效的负载被重新发送,以欺骗另一个系统。在软件更新系统中,旧版本的签名软件可能会被呈现为最新版本。如果用户被诱导安装了较旧版本的软件,恶意攻击者可以利用已知的安全漏洞来攻陷用户的主机。DCT 在发布镜像时使用时间戳密钥,以防止重放攻击。这确保了用户收到的是最新的版本。
防密钥泄露的保护:如果密钥被泄露,你可以利用该离线密钥进行密钥轮换。密钥轮换只能由拥有离线密钥的人执行。在这种情况下,你需要创建一个新密钥,并使用离线密钥对其进行签名。
其他增强安全性的项目包括:
-
Clair:这是一个开源项目,用于静态分析应用程序 Docker 容器中的漏洞(
github.com/coreos/clair)。它会在本地审计 Docker 镜像,并检查容器注册表集成中的漏洞。最后,在第一次运行时,Clair 会通过其数据源引导其数据库,填充漏洞数据。 -
Notary:Docker Notary 项目是一个框架,允许任何人在潜在不安全的网络上安全地发布和访问内容(例如 Docker 镜像)。Notary 允许用户对内容进行数字签名和验证。
-
Project Nautilus:Nautilus 是 Docker 的镜像扫描功能,它可以检查 Docker Hub 中的镜像,以帮助识别可能存在的 Docker 容器漏洞。今天,Nautilus 只支持 Docker Hub,不支持私有或本地注册表。
-
AuthZ 插件:原生的 Docker 访问控制是全有或全无——你要么可以访问所有 Docker 资源,要么完全没有访问权限。AuthZ 框架是 Twistlock 对 Docker 代码库的贡献。AuthZ 允许任何人编写 Docker 的授权插件,从而为 Docker 资源提供细粒度的访问控制。
-
Docker 受信任注册中心(DTR):这是 Docker 的企业版 Docker Hub。你可以在本地或虚拟私有云中运行 DTR,以满足安全或合规性要求。Docker Hub 是开源的,而 DTR 是由 Docker 销售的基于订阅的产品。与注册中心的通信使用 TLS,以确保保密性和内容完整性。默认情况下,使用公共 PKI 基础设施信任的证书是强制性的,但 Docker 允许将公司内部 CA 根证书添加到信任存储中。
新兴的安全方法
正如我们所知,Docker 平台使开发人员能够轻松地更新和控制容器中的数据和软件。同样,Docker 也能高效地确保构成应用程序的所有组件始终保持最新和一致。Docker 还天生提供了同一物理主机上应用程序的逻辑隔离。这种著名的隔离完美地促进了安全策略的细粒度和高效执行。然而,正如传统环境一样,静态数据容易受到来自网络和内部攻击者的各种攻击。Docker 环境还面临着其他潜在的负面威胁,可能遭受沉重的攻击。因此,必须采取适当的保障措施。容器和数据的快速、便捷传播可能会显著增加针对容器化云的威胁数量和类型。
关于 Vormetric 透明加密
组织可以高效地在 Docker 实现中围绕敏感数据建立强有力的控制。该解决方案支持静态数据加密、特权用户访问控制,并收集结构化数据库和非结构化文件的安全智能日志。通过这些功能,组织可以围绕存储的 Docker 镜像建立持久且强大的控制,并保护所有由 Docker 容器生成的数据,尤其是在数据写入 Docker 主机存储(如 NFS 挂载或本地文件夹)时。
容器安全的最佳实践
存在强大且弹性的安全解决方案,可以增强供应商和用户对容器化进程的信心,使其以清晰和敏捷的方式推进。在本节中,我们提供了一些来自不同来源的提示、最佳实践和关键指南,帮助安全管理员和顾问紧密地保护 Docker 容器。归根结底,如果容器运行在多租户系统中,并且你没有采用经过验证的安全实践,那么在安全方面肯定会存在潜在的危险。
首先最重要的建议是,不要在系统上运行随机的、未经测试的 Docker 镜像。要有策略,利用受信任的 Docker 镜像和容器仓库来订阅和使用应用程序以及数据容器,用于应用程序开发、打包、运输、部署和交付。从以往的经验来看,任何从公共领域下载的不受信任的容器都可能导致恶意和混乱的情况。像Red Hat 企业版 Linux(RHEL)这样的 Linux 发行版已经实施了以下机制,帮助管理员确保系统的最大安全性。
Docker 专家(Daniel Walsh,Red Hat 咨询工程师)广泛推荐的最佳实践如下:
-
只运行来自可信方的容器镜像
-
容器应用应尽可能降权或在没有特权的情况下运行
-
确保内核始终更新到最新的安全修复;安全内核至关重要
-
确保有支持团队监控内核中的安全漏洞
-
使用一个良好质量、受支持的宿主系统来运行容器,并定期进行安全更新
-
不要禁用宿主操作系统的安全功能
-
检查容器镜像是否存在安全漏洞,并确保提供商及时修复这些问题
如前所述,最大的问题在于 Linux 中的所有内容并未被命名空间化。目前,Docker 使用五种命名空间来改变进程对系统的视图:进程、网络、挂载、主机名和共享内存。虽然这些能为用户提供一定程度的安全性,但远不是像 KVM 那样全面的安全性。在 KVM 环境中,虚拟机中的进程不会直接与宿主内核通信。它们无法访问内核文件系统。设备节点可以与虚拟机内核通信,而不能与宿主内核通信。因此,要想在虚拟机中进行权限提升,进程必须首先颠覆虚拟机的内核,找到虚拟化程序中的漏洞,突破 SELinux 控制(sVirt),并攻击宿主内核。在容器环境中,方法是保护宿主机免受容器内进程的影响,同时保护容器免受其他容器的影响。这一切都在于将多个安全控制组合或集群起来,来防御容器及其内容。
基本上,我们希望设置尽可能多的安全屏障,以防止任何形式的突破。如果一个特权进程能够突破某一限制机制,那么我们的目标是用下一个安全屏障将其阻挡。在 Docker 中,尽可能利用 Linux 的各种安全机制是可行的。以下是可以采取的安全措施:
-
文件系统保护:文件系统需要是只读的,以避免任何未经授权的写入。也就是说,特权容器进程不能对其进行写入,并且不会影响主机系统。通常,大多数应用程序不需要对其文件系统进行任何写入。有几个 Linux 发行版提供了只读文件系统。因此,可以阻止特权容器进程将文件系统重新挂载为可读写。这完全是通过阻止容器内任何文件系统的挂载能力来实现的。
-
写时复制文件系统:Docker 一直使用高级多层统一文件系统(AUFS)作为容器的文件系统。AUFS 是一种分层文件系统,可以透明地叠加一个或多个现有的文件系统。当进程需要修改文件时,AUFS 会首先创建该文件的副本,并能够将多个层合并成一个文件系统的表现。这一过程被称为写时复制(copy-on-write),它防止一个容器看到另一个容器的更改,即使它们写入相同的文件系统镜像。一个容器不能更改镜像内容以影响另一个容器中的进程。
-
能力选择:通常,有两种方式进行权限检查:特权进程和非特权进程。特权进程绕过所有类型的内核权限检查,而非特权进程则根据进程的凭证进行完整的权限检查。最近的 Linux 内核将传统上与超级用户相关联的权限划分为独立的单元,称为能力(capabilities),这些能力可以独立启用或禁用。能力是每个线程的属性。移除能力可以带来 Docker 容器的一些积极变化。能力通常决定了 Docker 的功能、可访问性、可用性、安全性等。因此,在增加或移除能力时需要深入思考。
-
保持系统和数据安全:在企业和服务提供商在生产环境中使用容器之前,某些安全问题需要得到解决。容器化最终将使得应用程序更容易获得安全保障,原因有三:
-
-
较小的负载减少了安全漏洞的暴露面
-
与其逐步修补操作系统,不如直接更新它
-
通过允许清晰的责任分离,容器帮助 IT 和应用团队有目的地进行协作
-
IT 部门负责基础设施中与安全漏洞相关的事务。应用团队则修复容器内部的漏洞,并且还负责运行时的依赖关系。缓解 IT 和应用开发团队之间的紧张关系有助于顺利过渡到混合云模型。每个团队的责任划分明确,以确保容器及其运行时基础设施的安全。在这种明确的分工下,系统化地完成主动识别任何显性或隐性安全风险,并及时消除这些风险,进行政策制定和执行,进行精准和完美的配置,利用合适的安全挖掘和缓解工具等工作,已经成为常态。
- 利用 Linux 内核功能:一台普通的服务器(裸金属或虚拟机)需要以 root 权限运行一堆进程。这些进程通常包括
ssh、cron、syslogd、硬件管理工具(例如,加载模块)和网络配置工具(例如,处理 DHCP、WPA 或 VPN)。容器则大不相同,因为几乎所有这些任务都是由容器托管和运行的基础设施处理的。在各种由安全专家撰写的博客中,提供了若干最佳实践、关键指南、技术诀窍等。你可以在docs.docker.com/找到一些最有趣和启发性的安全相关细节。
Docker 容器的安全部署指南
Docker 容器越来越多地在生产环境中托管,以便被公众发现并广泛使用。尤其是随着云技术的快速采用,全球各大组织和机构的 IT 环境正通过系统化的优化和转型,巧妙且果断地托管更多种类的虚拟机和容器。为了加速将容器带入云环境(私有云、公有云、混合云和社区云),例如 Flocker 和 Clocker 等新技术不断涌现和启用。在部署容器时需要遵循一些建议。正如我们所知,容器通过允许开发者和系统管理员无缝部署应用程序和服务,从而显著降低了开销,这些应用程序和服务是业务运营所必需的。然而,由于 Docker 利用与宿主系统相同的内核来减少对资源的需求,如果配置不当,容器可能会面临重大的安全风险。在部署容器时,开发者和系统管理员必须严格遵守一些经过仔细注解的指南。例如,github.com/GDSSecurity/Docker-Secure-Deployment-Guidelines 在表格形式中详细列出了所有正确的细节。
不可否认的事实是,分布式和复杂应用中的软件缺陷为智能攻击者和黑客突破托管关键、机密及客户数据的系统打开了大门。因此,安全解决方案正在被坚持并深入到 IT 栈的各个层级,因此在不同的层次和级别上会出现许多类型的安全漏洞。例如,仅解决部分问题的边界安全,因为需要根据变化的需求允许员工、客户和合作伙伴访问网络。类似地,还有防火墙、入侵检测和防御系统、应用交付控制器(ADCs)、访问控制、多因素认证与授权、补丁修复等。然后,为了保护数据在传输、持久存储和应用使用过程中的安全,采用了加密、隐写术和混合安全模型。这些都是反应性和现实机制,但越来越多的趋势是虚拟企业坚持采取前瞻性和先发制人的安全方法。随着 IT 朝着备受期待的虚拟 IT 发展,安全问题和影响正受到安全专家的额外关注。
Docker 安全的未来
在不久的将来,容器化领域将会有许多值得关注的创新、转型和颠覆。通过一系列的创新和集成,Docker 平台正被定位为强化容器化旅程的领先平台。以下是通过智能利用 Docker 技术所取得的主要成就:
-
加强分布式范式:随着计算越来越向分布式和联合式发展,MSA 在 IT 中扮演着至关重要和更深层次的角色。Docker 容器正成为托管和交付日益增多的微服务的最有效方式。随着容器编排技术和工具获得更广泛的认可,微服务(无论是特定的还是通用的)被识别、匹配、编排和协调,形成业务感知的复合服务。
-
赋能云计算范式:云计算概念正强力抓住 IT 世界,推动 IT 基础设施的理性化、简化、标准化、自动化和优化。抽象化和虚拟化概念——这些是云计算范式前所未有成功的关键——正渗透到每种 IT 模块中。从最初的服务器虚拟化到现在的存储和网络虚拟化,技术的进步使得软件定义基础设施(软件定义计算、存储和网络)得到了广泛关注。Docker 引擎,作为 Docker 平台的核心和关键部分,已经得到了坚实的巩固,以确保容器能够在软件定义的环境中顺利运行。
-
启用 IT 弹性、可移植性、敏捷性和适应性:容器正在成为灵活且具有未来感的 IT 构建模块,以增强弹性、多样性、优雅性和柔韧性。为了确保更高的可用性和实时可扩展性,快速配置 IT 资源,消除开发与运维团队之间的所有摩擦,保证 IT 的原生性能,实现有序且优化的 IT 以提高 IT 生产力等,这些都是 Docker 容器面向智能 IT 所设想的典型应用。
容器将成为虚拟机和裸金属服务器的战略补充,以实现更深层次的 IT 自动化、加速和增强,从而实现备受期待的业务敏捷性、自治性和可负担性。
摘要
安全性无疑是一个挑战,也是不能忽视的重要方面。如果一个容器被攻破,那么摧毁容器宿主机并不难。因此,确保容器及其宿主机的安全是容器化概念蓬勃发展的关键,尤其是在 IT 系统集中化和联合化日益上升的今天。本章专门聚焦于 Docker 容器中令人痛心和破坏性的安全问题,并解释了为容纳动态企业级和关键任务应用程序的容器提供万无一失的安全解决方案的方法和手段。在未来的日子里,将会有新的安全方法和解决方案来保证 Docker 容器和宿主机的安全性不可渗透且牢不可破,因为容器及其内容的安全对服务提供商和消费者至关重要。
第九章:Docker 平台 - 独特的能力和使用案例
毫无疑问,IT 领域在任何时刻都是最活跃和最显眼的领域。随着每种企业(小型、中型和大型)都通过 IT 空间中令人垂涎的进步来实现业务,IT 与业务之间存在直接而决定性的关系。由于全球经济停滞甚至滑坡,企业巨头年复一年地削减 IT 预算,这明确要求 IT 专业人士做更多的事情,用更少的资源。也就是说,通过系统地利用经过验证和有前景的技术、工具和技巧,坚持更深入、更熟练地自动化各种业务操作。通过混合云进行基础设施优化,通过集成和编排技术进行流程优化,DevOps 文化的迅速传播,通过虚拟化和容器化方法进行部门化的基础方面,API 的渗透性、普及性和说服力,MSA 的快速出现,认知分析等等,都被广泛认可和接受作为业务敏捷性、可负担性、适应性和自主性的主导和突出前进方式。
Docker 可启用的容器化是一种深受关注的机制,它具有天生的能力来为软件工程领域带来某些关键的颠覆。Docker 范式完全围绕着任何类型的软件应用程序及其依赖项的最佳打包,以便在任何本地和外部环境中进行传送、部署和执行。容器化的应用程序(应用程序及其执行容器)与当前软件行业中现有的选项相比,极为轻量级、便携、可扩展、可复制和可重复使用。
Docker 的理念促进了许多有意义的创新。Docker(通过其独特的打包格式和高度集成的平台)简化并加速了公开可发现、网络可访问和远程部署的容器化应用程序的形成,这些应用程序易于组合、消耗和配置。此外,还有用于强大的容器监控、测量和管理的软件解决方案。在本章中,我们将讨论 Docker 范式加速成熟和稳定性如何确保急需的业务转型。文献讨论了 Docker 技术对下一代 IT 的几个改变性影响,而本章旨在揭开 Docker 的神秘面纱。
描述容器
包括虚拟化和容器化在内的隔离化是 IT 敏捷性的新时代标准。虚拟化一直是云计算巨大成功的神秘基础。现在,随着容器化概念的普及和可用性,越来越多的关注被投入到使用容器来加速应用程序构建、部署和交付上。容器特别配备了一些改变游戏规则的能力,因此有一股力量在推动拥抱和发展容器化技术和工具。
容器在业界非常火热。从本质上讲,容器是轻量级、虚拟化的且可移植的,软件定义环境(SDE)使得软件可以在与同一物理主机上运行的其他软件隔离的环境中运行。容器内运行的软件通常是单一功能的应用程序。容器为 IT 环境带来了备受追捧的模块化、可移植性和简洁性。开发人员喜爱容器,因为它们加速了软件工程,而运维团队则喜爱容器,因为他们可以专注于运行时任务,如日志记录、监控、生命周期管理和资源利用,而不是管理部署和依赖关系。
区分 Docker 容器
严格来说,Docker 容器将一段软件包装在一个完整的文件系统中,该文件系统包含运行所需的一切:源代码、运行时、系统工具和系统库(任何可以安装在服务器上的东西)。这保证了软件无论在什么操作环境下运行,都会始终如一。
启用 Docker 容器化的主要动机如下:
-
运行在单台机器上的容器共享相同的操作系统内核。它们能够立即启动并使用更少的 RAM。容器镜像是由分层的文件系统构建而成,并共享公共文件,从而使磁盘使用和镜像下载更加高效。
-
Docker 容器基于开放标准。这种标准化使得容器可以在所有主要的 Linux 发行版以及其他操作系统,如 Microsoft Windows 和 Apple Macintosh 上运行。
与 Docker 容器相关的几个好处如下:
-
效率:如前所述,多个容器可以在一台机器上共享同一个内核,因此它们轻量、可以瞬间启动,并且更加高效地利用 RAM。
-
-
资源共享:这种工作负载之间的共享相比于使用专用和单一用途的设备可以提高效率。资源共享提升了资源的利用率。
-
资源分区:这确保资源被适当划分,以满足每个工作负载的系统需求。此分区的另一个目标是防止工作负载之间发生任何不良的交互。
-
资源即服务(RaaS):可以单独或共同选择、配置并直接提供各种资源给应用或用户以运行应用。
-
-
原生性能:由于容器的轻量特性和较少的资源浪费,容器保证了更高的性能。
-
可移植性:应用、依赖项和配置被打包在完整的文件系统中,确保应用能够在任何环境中无缝运行(虚拟机、裸金属服务器、本地或远程、通用或专用机器等)。这种可移植性的主要优势是,可以在部署之间更改运行时依赖项(甚至是编程语言)。
下图展示了容器如何在多个主机之间移动和交换:

-
实时可扩展性:可以在几秒钟内配置任意数量的全新容器,以应对用户和数据负载。相反,当需求减少时,可以关闭额外配置的容器。这确保了按需提供更高的吞吐量和容量。Docker Swarm、Kubernetes 和 Apache Mesos 等工具进一步简化了弹性扩展。
-
高可用性:通过运行多个容器,可以在应用中建立冗余。如果一个容器失败,那么提供相同功能的存活容器将继续提供服务。通过编排,失败的容器可以自动重新创建(重新调度),无论是在同一主机还是不同主机上,从而恢复完整的容量和冗余。
-
机动性:运行在 Docker 容器中的应用可以轻松修改、更新或扩展,而不会影响主机中其他容器。
-
灵活性:开发者可以自由选择他们偏好的编程语言和开发工具。
-
集群化:容器可以根据需求进行集群化,并且有集成的管理平台来启用和管理集群。
-
组合性:托管在容器中的软件服务可以被发现、匹配并链接,以形成业务关键、过程感知和复合型服务。
-
安全性:容器通过为应用提供额外的保护层,将应用与彼此及底层基础设施隔离开来。
-
可预测性:通过不可变的镜像,由于代码包含在镜像中,镜像在任何地方都会表现出相同的行为。这对于部署和应用生命周期管理具有重要意义。
-
可重复性:使用 Docker,用户可以构建一个镜像,测试该镜像,然后在生产环境中使用相同的镜像。
-
可复制性:使用容器,轻松实例化完整应用栈和配置的相同副本。新员工、合作伙伴、支持团队等可以使用这些副本在隔离环境中安全地进行实验。
Docker 平台简介
Linux 容器非常复杂且不易于用户操作。意识到这一事实后,一个开源项目启动,旨在创建一个复杂而模块化的平台,包括一个能够简化和优化各种容器生命周期阶段的引擎。这意味着 Docker 平台旨在自动化轻量、可扩展和自给自足容器中的任何软件应用的制作、打包、运输、部署和交付。Docker 被定位为实现高度灵活和面向未来的容器化技术,适用于高效和企业级分布式应用。这将对 IT 行业产生敏锐和决定性的影响,因为公司不再依赖于单一物理或虚拟服务器上分发的大型单块应用,而是构建更小、自定义、可持续、易管理和独立的应用。简言之,服务正在变成微服务,以推动容器化运动。
Docker 平台允许艺术般地从不同的分布式组件中组装应用程序,并消除在代码发布过程中可能出现的任何缺陷和偏差。通过一系列脚本和工具,Docker 简化了软件应用的隔离,并通过在临时容器中运行它们使它们自给自足。Docker 为每个应用程序提供了与其他应用程序以及底层主机的必要隔离。我们习惯于通过额外的间接层形成的虚拟机来实现必要的隔离。然而,这种额外的层级和开销消耗了大量宝贵资源,因此是系统减速的不良原因。另一方面,Docker 容器共享所有资源(计算、存储和网络),因此可以运行得更快。标准形式的 Docker 镜像可以广泛共享和轻松存储,以生产更大、更好的应用程序容器。简而言之,Docker 平台为各种 IT 基础设施的最佳消耗、管理和操纵奠定了刺激和引人入胜的基础。
Docker 平台是一个开源的容器化解决方案,智能迅速地将任何软件应用程序和服务打包成容器,并加速在任何 IT 环境(本地或远程系统、虚拟化或裸机、通用或嵌入式设备等)中部署容器化应用程序。Docker 平台完全负责容器的生命周期管理任务。整个过程从为识别的软件及其依赖项形成标准化和优化的镜像开始。现在,Docker 平台使用准备好的镜像来形成容器化软件。公开和私有位置都提供了镜像存储库。开发人员和运维团队可以利用它们以自动化方式加快软件部署。
Docker 生态系统正在快速发展,有许多第三方产品和工具开发者致力于将 Docker 打造成一个企业规模的容器化平台。它帮助跳过开发环境和特定语言工具的设置和维护。相反,它专注于创建和添加新功能,修复问题并发布软件。"构建一次,到处运行"是 Docker 启用的容器化的典型口号。简而言之,Docker 平台带来了以下能力:
-
灵活性:开发人员可以自由定义环境,并能创建应用程序。IT 运营团队可以更快地部署应用程序,使业务能够超越竞争对手。
-
可控性:开发人员拥有从基础架构到应用程序的所有代码。
-
可管理性:IT 运营团队成员具备标准化、安全化和扩展操作环境的管理能力,同时降低组织的总体成本。
Docker 平台不断发展的组件
Docker 是一个平台,用于开发、发布和运行由分布式微服务构建的强大应用程序。该平台在第三方产品供应商和 Docker 空间的初创公司的持续支持下正在扩展。为不同的使用案例,正在构建并发布额外的自动化工具到市场上:
-
Docker Hub
-
Docker Trusted Registry
-
Docker Engine
-
Docker Kitematic
-
Docker Toolbox
-
Docker Registry
-
Docker Machine
-
Docker Swarm
-
Docker Compose
-
Docker Cloud
-
Docker 数据中心
随着持续的需求,我们可以安全地预期在未来几天内将有新的添加到上述列表中。Docker 团队正在积极主动地研发各种工具,以实现所需的自动化和简化,减少 IT 专业人员的工作量。
Docker 技术的影响
通过系统化和明智地使用 Docker 理念,全球各地的企业和组织必将在其业务转型需求中获得巨大的好处。本节将描述 Docker 范式的重大影响和潜力。毫无疑问,容器现在是一个热门话题。企业、服务提供商(云、通信等)和消费者都在追寻 Docker 的梦想。Docker 已经在企业和云 IT 领域创造了多方面的印象和影响。系统化利用 Docker 技术无疑能为企业带来可喜的进步。
现代企业开发
从概念上讲,容器镜像可以被视为容器文件系统的快照,可以存储在磁盘上。容器文件系统通常是按层次结构排列的,每次更改都会被仔细地捕捉到一个独立的层中。这使得容器镜像能够标明其派生自哪个父镜像。Docker 镜像通过标准化和简化的格式表示,最终能够实现软件应用的快速和高效部署与执行。容器具有可移植性。这意味着“一次构建,处处运行”是可移植性目标的核心。容器可以在任何运行相关操作系统的硬件上运行。
当然也有挑战。由于一个 Docker 主机中可能有多个容器,因此在云环境(私有、公共和混合)中可能会出现容器蔓延的问题。为了有效的监控和管理,正在利用集群和编排的概念,以便查找和绑定不同的分布式容器。此外,为了通过容器化应用程序构建分布式应用,提倡通过编排技术进行服务组合。Docker Compose 是构建复合应用的关键解决方案。针对容器级别的工作,已经有自动化监控、度量、管理和编排的软件解决方案(如 Docker Swarm、Kubernetes 和 Mesos)。在接下来的部分,我们将解释为什么容器最适合灵活和高效的企业。这并不意味着虚拟化已退出历史舞台。在某些情况下,虚拟化和容器化的混合使用会产生奇妙的效果。
将这些特殊功能与容器镜像结合,形成一个可行且值得尊敬的抽象层,实现了应用程序与底层操作系统之间的清晰隔离。这种镜像和操作系统的干净解耦使得在开发、测试、预发布和生产环境中部署软件应用成为可能,且没有任何障碍或问题。Docker 使能的统一性和普遍性提高了部署的可靠性,并通过消除各种不一致和不必要的摩擦,加速了现代企业开发。广泛推荐的做法是,拥有一个严密的容器镜像,它可以将应用程序的所有依赖项打包在一起。然后,这个镜像可以被部署到容器中,确保应用程序可以随时随地运行。
MSA 和 Docker 容器
服务使能的成功推进是由多种原因和目标驱动的。每个系统(物理、机械、电气和电子)都通过易于消费的接口进行系统化使能。由于其简单性,RESTful 接口和服务已经变得无处不在。在最近,随着互联网、企业、移动和云应用的迅速普及,REST 理念显然吸引了大量关注。人们很快发现,将业务功能拆分成可重用的服务是非常有效的;然而,这也带来了一个风险点。这意味着,每当一个服务更新时,所有依赖于该更新服务的其他服务都必须经过各种正式的验证和确认。这是因为服务不可避免地需要查找、绑定并利用其他服务及其独特的功能和数据点,以确保其正确性和相关性。这种不受限制的共享可以在本地或通过网络与远程服务发生。
基本上,微服务方法简而言之规定,与其拥有一个庞大的代码库供所有开发人员操作(这往往变得难以管理),不如拥有多个较小的代码库,由小型灵活的团队负责管理,这些团队分布在不同的时区。每个代码库都必须通过良好设计和定义的 API 进行互操作。每个代码库的体积较小,且相互完全解耦。依赖关系完全消失,从而带来了更好的安全性、可靠性、简易性、可扩展性、可用性等。代码库被称为微服务。微服务如此前所未有地蓬勃发展背后的动机有很多,特别是细粒度的扩展、易管理性、灵活性、可重新配置性和可扩展性、通过 API 访问实现的强大安全性、容器作为最佳运行时环境的适用性等,都是被广泛提及的。微服务可以独立部署、横向扩展,支持任何后端数据库(SQL、NoSQL、NewSQL、内存数据库等),并且可以使用任何编程语言构建。
Docker 容器最适合承载微服务。将单个服务或进程进行容器化,使得管理、更新和扩展这些服务变得非常简单。现在,随着任何 IT 环境中微服务数量的快速增长,管理复杂度也在急剧增加。这意味着,面临的挑战包括如何在集群中管理单个服务,以及如何应对分布式和不同主机上的多个服务。Kubernetes、MaestroNG、Mesosphere 和 Fleet 应运而生,满足了这一日益增长的需求。
总结来说,一个显著的原因是微服务的兴起和大量推广,这使得容器的不可或缺性得以显现。微服务所期待的各种目标通过将微服务封装进容器中得到了实现。这一有趣的组合注定将在全球企业的 IT 团队中发挥至关重要的作用。实际上,容器化原则的广泛应用为特定目的和通用微服务的爆炸性增长奠定了激励基础。
案例研究
SA Home Loans 在开发和生产过程中面临挑战。SA 目前有四个 Scrum 团队,每个团队都拥有一个开发和系统测试实验室。团队面临着部署速度慢的问题,只能在开发实验室中构建和部署两个应用程序,导致部署周期长,有时需要花费多达 2 周的时间才能将应用程序部署到测试环境。这一问题也延伸到生产环境中。主要的住房贷款服务软件是使用传统技术构建的单体应用。
IT 团队作出了有意识的决定,采用微服务架构(MSA)以获得灵活性、可移植性和可扩展性,并且这一决策导致了 50 个微服务的诞生。在理解了 Docker 技术的巨大潜力后,团队将所有微服务迁移到了容器中。
团队还需要一个生产就绪的编排服务,能够为其提供一个集中管理和分发容器到各个节点的单一入口,并且为团队提供所有容器的高层次监控。Docker Swarm 就是编排工具。SA Home Loans 现在使用 Docker Datacenter,这是一种本地托管的解决方案,通过支持的容器即服务(CaaS)平台将容器管理和部署服务带入企业。SA Home Loans 现在每天构建和部署应用程序多达 20 到 30 次。统一控制平面(UCP)内嵌 Swarm,提供生产就绪的容器编排解决方案。
基础设施优化
虚拟化一直是大幅优化和组织各种 IT 基础设施(服务器、存储设备、网络和安全解决方案)的主要机制。通过虚拟机实现的已验证的分而治之技术是 IT 优化的主要目标。近期,Docker 容器作为一种“伪装的福音”出现了。容器只包含构建、传输和运行软件应用所需的内容。与虚拟机不同,容器不需要来宾操作系统或虚拟机监控程序。这使得企业能够大幅减少存储量,并完全消除虚拟机监控程序的许可费用。与在物理机器中堆叠虚拟机的数量相比,物理主机或虚拟机中可以容纳的容器数量更多。这意味着容器是细粒度的,而虚拟机是粗粒度的。在容器化的情况下,资源浪费非常少。容器使得 IT 基础设施和资源得到有条理的使用。
可移植性是另一个因素。这使得 IT 运维团队能够在不同的云服务、物理服务器或虚拟机之间移动工作负载,而不必将它们锁定在特定的基础设施工具中。通过容器进行的工作负载整合或优化是无误的,因为容器可以在任何地方运行。在虚拟机的情况下,考虑到虚拟机监控程序/虚拟机监控器(VMMs)的多样性,虚拟机的部署是一个复杂且困难的事务。这里的关键点是,Docker 使企业能够优化基础设施的利用率,并减少维护现有应用程序的成本,这恰恰是企业 IT 团队每天面临的最大挑战。
Docker 大大缩短了安装应用程序、扩展以满足客户需求或简单启动新容器所需的时间。这意味着,将新产品推向市场的速度极快,因为底层基础设施(无论是虚拟还是物理)只需几秒钟就能准备好。
案例研究
需要建立并提供数据库即服务(DaaS)功能的客户已经决定每个数据库实例都部署在自己的虚拟机(VM)中。有时可能会有 100 个虚拟机运行着 100 个数据库。这是极其低效的,浪费了大量昂贵的资源。而现在,相同数量的数据库实例可以在同样数量的容器中运行,容器又可以在少数几个虚拟机中运行。结果是巨大的成本节约。以下是另一个案例研究:
-
客户详情:Swisscom 是瑞士领先的电信服务提供商,提供一系列企业和消费者服务。
-
业务挑战:包括为客户提供可靠且易于维护的 DaaS,同时实现高服务器密度,以确保高效运行。
-
解决方案方法:ClusterHQ 的 Flocker 提供了程序化管理 Docker 容器持久数据的能力,这些数据存储在 EMC ScaleIO 中。
-
业务成果:该解决方案显著提高了每台服务器上托管的应用程序密度,改善了数据库的运维管理,并为消费级和企业级 IT 部门的可持续创新铺设了充满活力的平台。
启用 DevOps
如今,IT 行业越来越多地遵循敏捷开发,以优雅地确保业务敏捷性、适应性和可负担性。这意味着,通过严格采用有效的 IT 敏捷方法,满足了广泛需求的业务敏捷性。为实现 IT 敏捷性,有一系列日益可行且值得尊敬的机制。主要地,IT 敏捷性是通过敏捷编程方法来推动的,如结对编程、极限编程(XP)、精益开发、Scrum 和看板、测试驱动开发(TDD)和行为驱动开发(BDD)。
现在,软件开发过程的速度显著加快。然而,开发和运维之间存在着很大的脱节。这意味着,只有当运维团队也严格遵循敏捷、适应性和自动化的 IT 操作时,真正的 IT 敏捷性才能实现。企业级 DevOps 是建立开发者和运维人员之间有效联系的最有前途的方式,从而使 IT 系统能够快速启动。容器化是推动 DevOps 普及、渗透和具有说服力的最积极的发展趋势。
Docker 非常适合快速设置开发和测试环境以及沙箱环境。Docker 有趣的是为确保高效的 DevOps 提供了更好的关注点分离;容器开发者只需专注于构建 Docker 镜像并提交它们,使其成为容器。运维团队可以监控、管理和维护这些容器。最后,Docker 可以轻松集成到多个 DevOps 工具中,实现更好的工作流自动化和持续集成。此外,它还使 DevOps 团队能够快速、经济高效地扩展开发和测试环境,并无缝地将应用程序从开发、测试迁移到生产。
持续集成与持续部署
持续集成(CI)和持续部署(CD)是实现敏捷 IT 最受欢迎的技术和工具。过去,开发人员会使用任一构建工具来自动化他们的构建过程。然后,他们会将代码交给运维团队进行部署、管理、维护和支持。为了自动化繁琐且艰难的软件部署和交付过程,存在许多配置管理和软件部署工具。这种分离的模式带来了一些重复出现的问题。使用容器后,运维团队可以构建他们希望部署和交付的完整栈的标准容器镜像。开发人员可以使用这些镜像来部署代码并进行单元测试。经过测试、优化和加固后的相同镜像可以在所有环境(开发、测试、预生产和生产)中使用,每次都能得到相同的结果。这种容器化支持的设置专门加速了软件部署和交付活动,以无风险的方式进行。
根据 Docker 官网,CI/CD 通常将开发与测试结合起来,使开发人员能够协作构建代码,将其提交到主分支,并检查是否存在问题。这意味着开发人员可以在应用程序开发生命周期的早期阶段构建和测试代码,从而尽早发现 bug。由于 Docker 可以与 Jenkins 和 GitHub 等工具集成,开发人员可以在 GitHub 中提交代码,测试代码,并使用 Jenkins 自动触发构建。构建完成后,镜像可以添加到 Docker 注册中心。这最终简化了过程,节省了构建和设置过程的时间,同时允许开发人员并行运行测试并自动化它们,从而在测试运行时可以继续进行其他项目工作。通过容器化,环境依赖性和不一致性得到了消除。
持续交付
持续交付方法涉及快速的软件开发迭代和频繁、安全的应用程序更新。这完全是为了通过在短时间内生产可靠的软件来减少风险,并更快速地交付价值。由于 Docker 封装了应用程序及其环境或基础设施配置,它为持续交付管道的两个关键方面提供了基础构件。Docker 使得测试您将要部署的内容变得更加容易。在交接过程中出现严重错误或引入任何不希望的变化的可能性因此减少。Docker 容器鼓励持续交付的核心原则:它们在管道的每个步骤中重用相同的二进制文件,确保在构建过程中不会引入错误。
如前所述,Docker 容器为不可变基础设施提供了基础。应用程序可以被添加、移除、克隆,或其组成部分可以发生变化,而不会留下任何残余。IT 基础设施可以在不影响其上运行的应用程序的情况下进行更改。Docker 工具生态系统是其成长轨迹,因此许多与交付相关的工作得以自动化并加速,以增加业务价值。正如 Martin Fowler 所说,您实际上会在以下情况下进行持续交付:
-
如果您的软件在整个生命周期内都可以部署
-
如果您的团队优先考虑保持软件可部署,而不是开发新功能
-
如果任何人在对系统进行更改时,可以随时获得关于其生产就绪情况的快速自动反馈
-
如果您可以按需将任何版本的软件部署到任何环境
Docker 还可以轻松与 CI 和持续交付平台集成,使得开发和测试能够无缝地将更新交付到生产环境中。在任何类型的故障发生时,可以回滚到之前的工作版本。
精确测试
Docker 通过为构建、测试和管理分布式应用程序创建一个通用框架,从而加速了 DevOps 的进程,这一框架不依赖于编程语言、开发工具或环境变量。Docker 通过允许开发人员、质量保证(QA)团队和系统管理员高效地共享代码、交换内容和集成应用程序,改善了协作。我们可以确信,我们的 QA 环境与即将在生产环境中部署的环境完全一致。
促进 CaaS
我们一直在调整 IT 基础设施和平台即服务(PaaS)。裸金属服务器和虚拟机是 IT 中心中的关键计算资源。现在,随着容器的成功普及,容器即服务(CaaS)正变得异常流行并充满诱惑。传统环境中的 PaaS 存在一些问题,CaaS 被视为克服 PaaS 问题的解决方案:

高级 CaaS 架构
在前面的图中,位于左侧的开发人员正在从受信任和精心挑选的基础镜像库中拉取和推送应用内容。位于右侧的运维团队则负责监控和管理已部署的应用和基础设施。这两个团队可以通过一个工具集进行协作,工具集使得两个团队在应用生命周期内保持关注点分离的同时,能够统一协作。Docker 平台就是这样一个工具集,它使得构建一个符合不同业务需求的 CaaS 成为可能。
添加新技术组件变得极为简单。假设一家公司想要将 MongoDB 加入其技术栈中。现在,可以从 Docker Hub 拉取一个经过认证的镜像,根据需要进行调整,并迅速部署。然后,开发人员可以使用这个容器。容器还允许更多的实验。由于构建和拆除容器非常容易,开发人员可以快速比较堆栈组件的特性。例如,开发人员想要测试三种不同 NoSQL 数据库技术的性能,他们只需启动每种 NoSQL 技术的相应容器,无需处理管理基础设施和底层技术栈的复杂性。然后,开发人员可以针对每个不同的容器运行性能测试,并快速选择合适的容器。
容器天生具备令人惊叹的能力,能够提供类似 JVM 的可移植性,完全抽象掉底层基础设施。一个真正的 CaaS 模型是为多容器应用在多云环境中的部署铺平道路。
加速工作负载现代化
有各种工作负载需要适当地现代化并迁移到强大的环境(云端),以便全球用户可以轻松查找、绑定并使用这些工作负载来生产业务关键应用。工作负载通常代表软件应用、 middleware、平台等。在过去,面向服务的架构(SOA)通过集成和组合促进了软件的现代化。最近,MSA 被誉为现代化遗留系统、单体应用和大型应用的最佳方法。应用程序因此被拆分,以便更容易管理。随着复杂应用以互操作、可移植和可组合的微服务集合的形式呈现和暴露,开发、部署和管理的复杂性预计会降低。这意味着应用模块将被重构,并准备好实现松耦合,甚至解耦。此外,推荐将应用设计为无状态的,以便于扩展和独立部署。
一些应用程序可以采取“提升和转移”路径到云端。这意味着如果进行一些代码修改,它们可以被显著重构,以充分利用云中心的独特优势。应用程序正在重新设计、重新编码,并为特定的云平台重新定位。这赋予了遗留应用程序新的生命和新的用途。
容器是托管和交付微服务的高度优化和有组织的运行时环境。容器与微服务相结合,在许多方面正成为 IT 世界中最关键的组合。使用容器来“包装”或容器化现有的遗留应用程序具有一些优势。容器负责底层平台和基础设施以及与之相关的复杂性。容器化的应用程序具有可移植性,并加快了遗留应用程序现代化的速度。通过使用容器,云迁移变得更加顺畅。可以轻松快速地将安全性、Web 和服务启用以及治理等附加功能应用到容器化应用程序上。此外,现代化的遗留应用程序更适合分布式计算。
将当前和传统应用程序现代化,并将它们迁移到云端的一个好方法是利用 Kubernetes 和 Mesos 等技术,而不是构建所有的非功能需求(NFRs),如可扩展性、安全性和可持续性。
Docker 用于状态应用程序
容器通常是无状态的。然而,对于某些应用程序,状态计算资源是必需的。Docker 在将这些计算资源在主机之间迁移时,原生并未提供存储卷管理或数据持久化。ClusterHQ 的 Flocker 解决方案解决了这些需求,并通过提供一个框架来管理卷和数据持久化,使容器能够用于状态应用程序,如数据库,当计算资源从一个主机迁移到另一个主机时。Flocker 与所有主要的容器管理器兼容(包括 Docker Swarm、Kubernetes 和 Apache Mesos)。
边缘计算的容器
安全性担忧以及缺乏可视性和可控性被认为是云计算的广泛认同的缺点。私有云和云小单元是可行的选择。然而,它们也面临着某些限制。然而,最近的边缘计算或雾计算现象被认为是克服云计算所有弱点的最成功的计算范式。
边缘计算的核心在于将数据处理和存储从集中式位置(云端)转移到分布式和去中心化的环境(本地)。这意味着通过将计算、网络和存储能力更接近用户,服务质量(QoS)属性 / 非功能性要求(NFRs)能够轻松且高效地完成。传统上,所有计算和存储都发生在云环境中(本地和非本地)。然而,某些场景,如实时分析和更快的响应,需要在用户端进行计算。可以毫不夸张地说,当 IT 变得以人为中心、情境感知、具有适应性、实时性和多模态时,QoS 和用户体验会显著提高。现实世界和实时应用及服务必然要求在边缘进行计算。即使在边缘计算方面也有若干架构上的复杂性,但随着应用和容器体积的成熟和稳定,边缘计算自然得到了急需的推动。
设备网络、服务启用和集群
一般来说,边缘设备如植入设备、可穿戴设备、便携式设备、网关、移动设备、手持设备、消费电子产品和机器人通常是资源受限的。这些设备大多数不是静态的,通常是流动的。在它们之间建立无缝连接以进行过程、应用和数据整合确实是一项艰巨的任务。因此,边缘设备上的地理分布计算需要一个轻量级、本质上可扩展且智能的平台来处理极为脆弱的服务部署、交付和管理。开放服务网关接口(OSGi)是一个有趣的框架,用于优雅地激活和管理资源受限的嵌入式和连接设备及其独特的服务。任何服务或应用程序都可以容器化,并可以与各种参与设备一起加载。然后,OSGi 包的一个实例可以被容器化并托管在用户环境中一台功能较强的设备上,从而发现并管理所有设备及其服务容器。这种设置使得集中式(来自云端)以及去中心化的设备服务监控、测量和管理成为可能。Docker 化平台是一种经过验证的机制,用于安装、配置、管理、升级和终止正在运行的服务。
设备服务注册与发现
在特定环境中可能有成千上万的边缘设备。为了以系统化的方式发现、索引和管理异构、动态和分布式的设备,强烈需要服务注册和发现能力。管理平台必须具备此功能,以便自动地发现、绑定和利用多个设备。
容错
平台必须具备容错能力,以保证高可用性和可靠性,从而确保业务连续性。
缓存
可以在边缘设备上执行缓存操作,以实现更快的访问并提升整体应用性能。如果 Docker 镜像存储并缓存于边缘设备,则可以大大加速应用的配置过程。另一个使用案例是将应用数据存储在缓存中,从而显著提高应用性能。
布哈里·伊赫万·伊斯梅尔(Bukhary Ikhwan Ismail)及其团队建立了一个测试平台,以考察 Docker 作为边缘计算或雾计算候选技术之一的可行性。该测试平台由一个数据中心和三个边缘站点组成,用于模拟环境。在每个边缘站点上,都会设置一个 Docker Registry,用于在本地存储 Docker 镜像。边缘站点的 Docker 守护进程可以从 Docker Registry 中搜索并拉取 Docker 镜像。在每个边缘站点上配置 Docker Swarm,用于管理多个 Docker 守护进程。Docker Swarm 充当集群和编排工具。根据实验和评估,Docker 提供了快速部署、小巧的占用空间和良好的性能,这使其有可能成为一个可行的边缘计算平台。
马塞尔·格罗斯曼(Marcel Grossmann)及其团队开发了Hypriot Cluster Lab(HCL)。这是一个基于 ARM 架构的云解决方案,利用 Docker 进行操作。嵌入式系统和其他单板计算机(SBCs)已经获得了巨大的计算能力。随着设备的互联互通和网络化,大量的机器数据被生成,日益增长的需求是快速收集和处理这些数据,以提取实时洞察。正如前面所述,边缘/雾计算时代正在迅速发展。HCL 可以为虚拟化边缘提供基础,因为它运行在 ARM 架构上,像一个小型数据中心一样工作,并通过设计提供节能功能。
Docker 的使用案例
容器化正在成为软件行业发展的方向,它为构建和打包各种软件、将其运输并在任何地方运行提供了一种更新、更丰富的方式。这就是容器化快速发展的方面,它承诺并提供软件的可移植性,这一直是 IT 开发人员和管理员多年来的一个持续难题。Docker 的理念在这里蓬勃发展,得益于多种推动因素和方面。本节特别准备了 Docker 理念的关键使用案例。
将容器集成到工作流中
工作流是一种广泛接受并使用的抽象方法,用于明确表示任何复杂的大规模商业和科学应用程序的相关细节,并将其在分布式计算系统(如集群、云和网格)上执行。然而,工作流管理系统在传达工作流中任务运行的底层环境的相关信息方面一直存在问题。这意味着,工作流中的任务可以在为其设计的环境中完美运行。真正的挑战在于如何在不同的 IT 环境中运行这些任务,而无需修改和扭曲所需任务的源代码。随着不同操作系统、中间件、编程语言和框架、数据库等的不断发展,IT 环境日益异构。通常,工作流系统关注任务之间的数据交换,并且是特定于环境的。一个在某个环境中运行良好的工作流,迁移到不同的 IT 环境后就会崩溃。各种已知和未知的依赖关系与不兼容性会浮现出来,拖延整个 IT 设置、应用程序安装与配置、部署和交付的过程。容器是解决这个复杂局面的一劳永逸的最佳选择。
在文章《将容器集成到工作流中:使用 Makeflow、Work Queue 和 Docker 的案例研究》中,Chao Zheng 和 Douglas Thain 做得很好,分析了几种方法,旨在通过实验证明容器在提升工作流/过程管理系统方面的独特贡献。他们探讨了在 Docker 启用的集群上运行大规模生物信息学工作负载的表现,并观察到,最佳配置是在多个任务之间共享的容器上进行本地管理。
Docker 在 HPC 和 TC 应用程序中的应用
根据 Douglas M. Jacobsen 和 Richard Shane Canon 的说法,目前,容器在 Web、企业、移动和云应用程序中被广泛使用。然而,关于容器是否能够作为承载技术和科学计算应用程序的可行运行时,仍然存在疑问。尤其是,许多高性能计算(HPC)应用程序渴望找到一个完美的部署和执行环境。这篇研究论文的作者们意识到,Docker 容器可以成为 HPC 工作负载的完美答案。
在许多情况下,用户希望能够在开发过程中使用的相同环境中轻松执行他们的科学应用和工作流,或者使用他们社区采用的环境。一些研究人员尝试过云计算选项,但挑战依然很多。用户需要解决如何处理工作负载管理、文件系统和基本配置的问题。容器承诺提供云类型系统的灵活性,并结合裸金属系统的性能。此外,容器有潜力更容易地集成到传统的高性能计算环境中,这意味着用户可以在无需管理系统其他层(例如批处理系统、文件系统等)的负担下,获得灵活性的好处。
Minh Thanh Chung 和团队分析了虚拟机和容器在高性能应用中的性能,并对结果进行了基准测试,清楚地表明容器是下一代高性能计算应用的运行时。简而言之,Docker 在高性能计算环境中提供了许多吸引人的优点。为了验证这些,IBM Platform LSF 和 Docker 已在 Platform LSF 核心外进行了集成,并且该集成利用了丰富的 Platform LSF 插件框架。
我们都知道,隔离化的一个方面是资源的划分和配置。这意味着物理机器被划分为多个逻辑机器(虚拟机和容器)。现在,从另一个角度看,这种由多个物理机器划分出的逻辑系统可以被链接在一起,构建一个虚拟超级计算机,以解决某些复杂的问题。Hsi-En Yu 和 Weicheng Huang 在研究论文 通过 Docker 构建一个带自动扩展的虚拟 HPC 集群 中描述了他们如何构建虚拟高性能计算集群。他们将服务发现的自动扩展功能与轻量化虚拟化范式(Docker)集成,并开始在物理集群硬件上实现虚拟集群。
通信应用的容器
Csaba Rotter 和团队 探讨并发布了一篇题为 在通信应用中使用 Linux 容器 的调查文章。通信应用具有较强的性能和高可用性要求;因此,将它们运行在容器中需要额外的研究。 一款通信应用是一个或多个节点的应用,负责执行特定的任务。通信应用通过标准化接口与其他网络元素连接并实现标准化功能。在标准化功能之上,通信应用可以具备供应商特定的功能。它有一组 QoS 和 体验质量 (QoE) 属性,如高可用性、容量、性能/吞吐量等。该论文清晰地阐明了容器在下一代通信应用中的独特贡献。
使用 Docker-Hadoop 高效原型化容错 Map-Reduce 应用程序 由 Javier Rey 和团队 提出,主张分布式计算是面向计算和数据密集型工作负载的未来发展方向。存在两个主要趋势。数据变得庞大,而且人们认识到,通过利用开创性的算法、脚本和并行语言(如 Scala)、集成平台、新一代数据库以及动态 IT 基础设施,大数据能够带来深刻的洞察。MapReduce 是一种并行编程范式,当前用于对大量数据进行计算。Docker-Hadoop1 是一个虚拟化测试平台,旨在快速部署 Hadoop 集群。通过 Docker-Hadoop,可以控制节点的特性,运行规模性和性能测试,这些测试在其他情况下可能需要庞大的计算环境。Docker-Hadoop 促进了不同故障场景的模拟和重现,以验证应用程序的可靠性。
关于互动社交媒体应用程序,Alin Calinciuc 和团队 发布了名为 OpenStack 和 Docker:为互动社交媒体应用程序构建高性能 IaaS 平台 的研究论文。一个广为人知的事实是,互动社交媒体应用面临着有效地配置新资源的挑战,以满足不断增长的应用用户需求。作者详细描述了 Docker 如何作为一个虚拟机监控器运行,以及如何利用他们开发的 nova-docker 插件,在 OpenStack IaaS 中快速配置计算资源。
摘要
在当前时刻,Docker 已经成为一种流行趋势,全球各地的企业都在为其极端的自动化、转型和颠覆性技术而痴迷,容器化浪潮正席卷全球。随着混合 IT 的蓬勃发展,Docker 驱动的容器化在智能赋能 IT 支撑的企业中的作用日益增长。在本章中,我们讨论了 Docker 模式的主要功能和贡献。我们描述了如何将一个典型的软件包进行容器化。此外,你还可以看到一些工业和企业级的应用案例。
第十章:致谢
| 作者 Jeeva S. ChelladhuraiVinod SinghPethuru Raj | 文稿编辑 Tom Jacob |
|---|---|
| 审阅者 Werner Dijkerman | 项目协调员 Kinjal Bari |
| 委托编辑 Kartikey Pandey | 校对员 Safis Editing |
| 采购编辑 Prachi Bisht | 索引员 Mariammal Chettiyar |
| 内容开发编辑 Radhika Atitkar | 图形设计 Kirk D'Penha |
| 技术编辑 Bhagyashree Rai | 制作协调员 Melwyn Dsa |


浙公网安备 33010602011771号