Docker-学习手册(二)(全)

Docker 学习手册(二)(全)

原文:zh.annas-archive.org/md5/1FDAAC9AD3D7C9F0A89A69D7710EA482

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

为了建立广受需求的软件可移植性,我们已经长时间研究虚拟化技术和工具。通过利用虚拟化这种有益的抽象,通过额外的间接层来消除软件和硬件之间的抑制性依赖因素。这个想法是在任何硬件上运行任何软件。这是通过将单个物理服务器分成多个虚拟机(VMs)来实现的,每个 VM 都有自己的操作系统(OS)。通过自动化工具和受控资源共享实现的隔离,异构应用程序可以在一台物理机上容纳。

通过虚拟化,IT 基础设施变得开放、可编程、可远程监控、可管理和可维护。业务工作负载可以托管在适当大小的虚拟机中,并传送到外部世界,确保更广泛和更频繁的利用。另一方面,对于高性能应用程序,可以轻松识别跨多台物理机的虚拟机,并迅速组合以保证任何种类的高性能需求。

虚拟化范式也有其缺点。由于冗长和臃肿(每个虚拟机都携带自己的操作系统),虚拟机的配置通常需要一段时间,性能会因过度使用计算资源而下降,等等。此外,对可移植性的不断增长需求并未完全得到虚拟化的满足。来自不同供应商的 Hypervisor 软件妨碍了应用程序的可移植性。操作系统和应用程序分发、版本、版本和补丁的差异阻碍了平稳的可移植性。计算机虚拟化蓬勃发展,而与之密切相关的网络和存储虚拟化概念刚刚起步。通过虚拟机相互作用构建分布式应用程序会引发和涉及一些实际困难。

让我们继续讨论容器化。所有这些障碍都促成了容器化理念的空前成功。一个容器通常包含一个应用程序,应用程序的所有库、二进制文件和其他依赖项都被一起打包,以作为一个全面而紧凑的实体呈现给外部世界。容器非常轻量级,高度可移植,易于快速配置等等。Docker 容器实现了本地系统性能。通过应用容器,DevOps 的目标得到了充分实现。作为最佳实践,建议每个容器托管一个应用程序或服务。

流行的 Docker 容器化平台推出了一个使容器的生命周期管理变得简单和快速的启用引擎。有行业强度和开放自动化工具免费提供,以满足容器网络和编排的需求。因此,生产和维护业务关键的分布式应用变得容易。业务工作负载被系统地容器化,以便轻松地转移到云环境,并为容器工匠和作曲家提供云端软件解决方案和服务。确切地说,容器正在成为 IT 和业务服务最具特色、受欢迎和精细调整的运行时环境。

本书精心设计和开发,旨在为开发人员、云架构师、业务经理和战略家提供有关 Docker 平台及其推动行业垂直领域中的关键、复合和分布式应用的所有正确和相关信息。

本书涵盖了以下内容

《第一章》(ch01.html“第一章。使用 Docker 入门”)使用 Docker 入门,介绍了 Docker 平台以及它如何简化和加速实现容器化工作负载的过程,以便在各种平台上轻松部署和运行。本章还详细介绍了安装 Docker 引擎、从集中式 Docker Hub 下载 Docker 镜像、创建 Docker 容器以及排除 Docker 容器故障的步骤。

第二章,“处理 Docker 容器”,主要是为了阐述管理 Docker 图像和容器所需的命令。本章提供了理解 Docker 命令输出所需的基本 Docker 术语。此外,本章还涵盖了在容器内启动交互会话,管理图像,运行容器以及跟踪容器内的更改等其他细节。

第三章,“构建图像”,介绍了 Docker 集成图像构建系统。本章还涵盖了 Dockerfile 语法的快速概述以及关于 Docker 如何存储图像的一些理论。

第四章,“发布图像”,侧重于在集中式 Docker Hub 上发布图像以及如何充分利用 Docker Hub。本章的其他重要内容包括有关 Docker Hub 的更多细节,如何将图像推送到 Docker Hub,图像的自动构建,创建 Docker Hub 上的组织,以及私有存储库。

第五章,“运行您的私有 Docker 基础设施”,解释了企业如何建立自己的私有存储库。由于某些原因,企业可能不希望将特定的 Docker 图像托管在公开可用的图像存储库(如 Docker Hub)中。因此,他们需要自己的私有存储库来保存这些图像。本章包含了设置和维护私有存储库所需的所有信息。

第六章,“在容器中运行服务”,说明了如何将 Web 应用程序作为服务在 Docker 容器内运行,以及如何公开该服务,以便外部世界找到并访问它。还详细描述了如何开发适当的 Dockerfile 以简化此任务。

第七章,“与容器共享数据”,向您展示如何使用 Docker 的卷功能在 Docker 主机和其容器之间共享数据。本章还涵盖了如何在容器之间共享数据,常见用例以及要避免的典型陷阱。

第八章 编排容器,着重于编排多个容器以实现复合、容器化的工作负载。众所周知,编排在生成复合应用程序中起着重要作用。本章包括一些关于编排和可用于启用编排过程的工具集的信息。最后,您将找到一个精心编排的示例,演示如何编排容器以产生高度可重用和业务感知的容器。

第九章 使用 Docker 进行测试,侧重于在 Docker 镜像内测试您的代码。在本章中,您将了解如何在临时 Docker 镜像内运行测试。最后,您将了解如何将 Docker 测试集成到持续集成服务器(如 Jenkins)中的详细信息。

第十章 调试容器,教您如何调试在容器内运行的应用程序。还涵盖了关于 Docker 如何确保容器内运行的进程与外部世界隔离的详细信息。此外,还包括了关于使用 nsenter 和 nsinit 工具进行有效调试的描述。

第十一章 保护 Docker 容器,旨在解释正在酝酿的安全和隐私挑战和关注点,以及通过使用充分的标准、技术和工具来解决这些问题。本章描述了在镜像内降低用户权限的机制。还简要介绍了在保护 Docker 容器时,SELinux 引入的安全功能如何派上用场。

本书所需内容

Docker 平台需要 64 位硬件系统才能运行。本书的 Docker 应用程序是在 Ubuntu 14.04 上开发的,但这并不意味着 Docker 平台不能在其他 Linux 发行版上运行,比如 Redhat、CentOS、CoreOS 等。但是,Linux 内核版本必须是 3.10 或更高版本。

本书适合对象

如果您是一名应用程序开发人员,想要学习 Docker 以利用其特性进行应用部署,那么这本书适合您。不需要 Docker 的先前知识。

约定

在本书中,您会发现许多文本样式,用于区分不同类型的信息。以下是一些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:“如果docker服务正在运行,则此命令将打印状态为start/running,并显示其进程 ID。”

代码块设置如下:

FROM busybox:latest
CMD echo Hello World!!

任何命令行输入或输出都以以下形式书写:

$ sudo docker tag 224affbf9a65localhost:5000/vinoddandy/dockerfileimageforhub

新术语重要单词以粗体显示。屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:“选择Docker选项,它在下拉菜单中,然后点击立即启动。”

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会以这种方式出现。

第一章:Docker 入门

如今,Docker 技术在全球范围内的信息技术(IT)专业人士中获得了更多的市场份额和更多的关注。在本章中,我们想更多地介绍 Docker,并展示为什么它被誉为即将到来的云 IT 时代的下一个最佳选择。为了使本书与软件工程师相关,我们列出了制作高度可用的应用程序感知容器所需的步骤,将它们注册到公共注册库中,然后在多个 IT 环境(本地和离地)中部署它们。在本书中,我们清楚地解释了 Docker 的先决条件和最重要的细节,借助我们通过一系列在不同系统中谨慎实施的几个有用的 Docker 容器所获得的所有教育和经验。为了做到这一点,我们使用了我们自己的笔记本电脑以及一些领先的公共云服务提供商(CSP)。

我们想向您介绍 Docker 实用方面,以改变游戏规则的 Docker 启发式容器化运动。

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

  • Docker 简介

  • Linux 上的 Docker

  • 区分容器化和虚拟化

  • 安装 Docker 引擎

  • 了解 Docker 设置

  • 下载第一个镜像

  • 运行第一个容器

  • 在 Amazon Web Services(AWS)上运行 Docker 容器

  • 解决 Docker 容器的故障

Docker 简介

由于其在行业垂直领域的广泛使用,IT 领域已经充斥着许多新的和开创性的技术,这些技术不仅用于带来更具决定性的自动化,而且还用于克服现有的复杂性。虚拟化已经设定了将 IT 基础设施优化和可移植性带入前景的目标。然而,虚拟化技术存在严重缺陷,例如由于虚拟机(VM)的笨重性质而导致的性能下降,应用程序可移植性的缺乏,IT 资源的提供速度缓慢等。因此,IT 行业一直在稳步地踏上 Docker 启发式容器化之旅。Docker 倡议专门设计了使容器化范式更易于理解和使用的目的。Docker 使容器化过程能够以无风险和加速的方式完成。

确切地说,Docker是一个开源的容器化引擎,它自动化打包、运输和部署任何呈现为轻量、便携和自给自足容器的软件应用程序,可以在几乎任何地方运行。

Docker 容器是一个软件桶,包括运行软件所需的一切。单台机器上可以有多个 Docker 容器,这些容器彼此完全隔离,也与主机机器隔离。

换句话说,Docker 容器包括一个软件组件以及其所有依赖项(二进制文件、库、配置文件、脚本、jar 等)。因此,Docker 容器可以在支持命名空间、控制组和文件系统(如另一个联合文件系统(AUFS))的 x64 Linux 内核上流畅运行。然而,正如本章所示,对于在其他主流操作系统(如 Windows、Mac 等)上运行 Docker,有实用的解决方法。Docker 容器有自己的进程空间和网络接口。它也可以以 root 身份运行,并且有自己的/sbin/init,这可能与主机机器不同。

简而言之,Docker 解决方案让我们快速组装复合、企业规模和业务关键的应用程序。为了做到这一点,我们可以使用不同的分布式软件组件:容器消除了将代码发送到远程位置时出现的摩擦。Docker 还让我们能够尽快测试代码,然后在生产环境中部署它。Docker 解决方案主要包括以下组件:

  • Docker 引擎

  • Docker Hub

Docker 引擎用于实现特定目的和通用 Docker 容器。Docker Hub 是 Docker 镜像的快速增长的存储库,可以以不同方式组合,以产生公开可查找、网络可访问和广泛可用的容器。

Linux 上的 Docker

假设我们想要直接在 Linux 机器上运行容器。Docker 引擎产生、监控和管理多个容器,如下图所示:

Linux 上的 Docker

上图生动地说明了未来的 IT 系统将拥有数百个应用感知容器,这些容器天生具有促进其无缝集成和编排以获得模块化应用程序(业务、社交、移动、分析和嵌入式解决方案)的能力。这些包含的应用程序可以流畅地运行在融合、联合、虚拟化、共享、专用和自动化的基础设施上。

容器化和虚拟化的区别

从容器化范式中提取和阐述 Docker 启发的容器化运动的颠覆性优势是至关重要和至关重要的,这超过了广泛使用和完全成熟的虚拟化范式。在容器化范式中,通过一些关键和明确定义的合理化和计算资源的深刻共享,战略上合理的优化已经完成。一些天生的而迄今为止未充分利用的 Linux 内核功能已经被重新发现。这些功能因为带来了备受期待的自动化和加速而受到了奖励,这将使新兴的容器化理念在未来的日子里达到更高的高度,特别是在云时代。这些显著的商业和技术优势包括裸金属级性能、实时可伸缩性、更高的可用性等。所有不需要的凸起和赘肉都被明智地消除,以便以成本效益的方式加快数百个应用容器的部署速度,并缩短营销和估值所需的时间。左侧的下图描述了虚拟化方面,而右侧的图形生动地说明了容器中所实现的简化:

容器化和虚拟化的区别

下表直接比较了虚拟机和容器:

虚拟机(VMs) 容器
代表硬件级虚拟化 代表操作系统虚拟化
重量级 轻量级
缓慢的供应 实时供应和可伸缩性
有限的性能 本机性能
完全隔离,因此更安全 进程级隔离,因此不太安全

容器化和虚拟化的融合

正在开发一种混合模型,具有虚拟机和容器的特性。这就是系统容器的出现,如前述右侧图表所示。传统的虚拟化程序,隐式地代表硬件虚拟化,直接利用服务器硬件来保护环境。也就是说,虚拟机与其他虚拟机以及底层系统完全隔离。但对于容器来说,这种隔离是在进程级别进行的,因此容器容易受到任何安全侵入的影响。此外,一些在虚拟机中可用的重要功能在容器中是不可用的。例如,容器中没有对 SSH、TTY 和其他安全功能的支持。另一方面,虚拟机需要大量资源,因此它们的性能会大幅下降。事实上,在容器化术语中,经典虚拟化程序和客户操作系统的开销将被消除,以实现裸金属性能。因此,可以为单台机器提供一些虚拟机。因此,一方面,我们有性能一般的完全隔离的虚拟机,另一方面,我们有一些缺少一些关键功能但性能卓越的容器。在理解了随之而来的需求后,产品供应商正在研发系统容器。这一新举措的目标是提供具有裸金属服务器性能但具有虚拟机体验的完整系统容器。前述右侧图表中的系统容器代表了两个重要概念(虚拟化和容器化)的融合,以实现更智能的 IT。我们将在未来听到更多关于这种融合的信息。

容器化技术

认识到容器化范式对 IT 基础设施增强和加速的作用和相关性后,一些利用容器化理念的独特和决定性影响的技术应运而生,并被列举如下:

  • LXCLinux 容器):这是所有容器的鼻祖,它代表了在单个 Linux 机器上运行多个隔离的 Linux 系统(容器)的操作系统级虚拟化环境。

维基百科网站上的文章LXC指出:

“Linux 内核提供了 cgroups 功能,允许对资源(CPU、内存、块 I/O、网络等)进行限制和优先级设置,而无需启动任何虚拟机,并提供了命名空间隔离功能,允许完全隔离应用程序对操作环境的视图,包括进程树、网络、用户 ID 和挂载的文件系统。”

您可以从en.wikipedia.org/wiki/LXC获取更多信息。

  • OpenVZ:这是一种基于 Linux 内核和操作系统的操作系统级虚拟化技术。OpenVZ 允许物理服务器运行多个隔离的操作系统实例,称为容器、虚拟专用服务器(VPS)或虚拟环境(VEs)。

  • FreeBSD 监狱:这是一种实现操作系统级虚拟化的机制,它允许管理员将基于 FreeBSD 的计算机系统分成几个独立的迷你系统,称为“监狱”。

  • AIX 工作负载分区(WPARs):这些是操作系统级虚拟化技术的软件实现,提供应用环境隔离和资源控制。

  • Solaris 容器(包括 Solaris Zones):这是针对 x86 和 SPARC 系统的操作系统级虚拟化技术的实现。Solaris 容器是由“区域”提供的系统资源控制和边界分离的组合。区域在单个操作系统实例内充当完全隔离的虚拟服务器。

在本书中,考虑到 Docker 的风靡和大规模采用,我们选择深入挖掘,详细讨论 Docker 平台,这是简化和优化容器化运动的一站式解决方案。

安装 Docker 引擎

Docker 引擎是建立在 Linux 内核之上的,并且广泛利用其功能。因此,目前 Docker 引擎只能直接在 Linux 操作系统发行版上运行。尽管如此,通过使用轻量级 Linux 虚拟机和适配器(如 Boot2Docker),Docker 引擎可以在 Mac 和 Microsoft Windows 操作系统上运行。由于 Docker 的迅猛增长,它现在被所有主要的 Linux 发行版打包,以便它们可以保留他们的忠实用户并吸引新用户。您可以使用相应的 Linux 发行版的打包工具来安装 Docker 引擎;例如,使用apt-get命令安装 Debian 和 Ubuntu,使用yum命令安装 RedHat、Fedora 和 CentOS。

注意

我们选择了Ubuntu Trusty 14.04(LTS)(64 位) Linux 发行版以供所有实际目的使用。

从 Ubuntu 软件包存储库安装

本节详细解释了从 Ubuntu 软件包存储库安装 Docker 引擎涉及的步骤。在撰写本书时,Ubuntu 存储库已经打包了 Docker 1.0.1,而最新版本的 Docker 是 1.5。我们强烈建议使用下一节中描述的任一方法安装 Docker 版本 1.5 或更高版本。

但是,如果出于任何原因您必须安装 Ubuntu 打包版本,请按照这里描述的步骤进行。

  1. 安装 Ubuntu 打包版本的最佳做法是通过重新与 Ubuntu 软件包存储库同步开始安装过程。这一步将更新软件包存储库到最新发布的软件包,因此我们将确保始终使用此处显示的命令获取最新发布的版本:
$ sudo apt-get update

提示

下载示例代码

您可以从www.packtpub.com的帐户下载您购买的所有 Packt Publishing 图书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,文件将直接通过电子邮件发送给您。

  1. 使用以下命令启动安装。此设置将安装 Docker 引擎以及一些支持文件,并立即启动docker服务:
$ sudo apt-get install -y docker.io

注意

Docker 软件包被称为docker.io,因为 Ubuntu 软件包的旧版本被称为docker。因此,所有名为docker的文件都被安装为docker.io

例如/usr/bin/docker.io/etc/bash_completion.d/docker.io

  1. 为了方便起见,你可以为docker.io创建一个名为docker的软链接。这将使你能够以docker而不是docker.io执行 Docker 命令。你可以使用以下命令来实现这一点:
$ sudo ln -sf /usr/bin/docker.io /usr/local/bin/docker

注意

官方的 Ubuntu 软件包不包含最新稳定版本的docker

使用 docker.io 脚本安装最新的 Docker

官方发行版可能不会打包最新版本的 Docker。在这种情况下,你可以手动安装最新版本的 Docker,也可以使用 Docker 社区提供的自动化脚本。

要手动安装最新版本的 Docker,请按照以下步骤进行:

  1. 将 Docker 发布工具的存储库路径添加到你的 APT 源中,如下所示:
$ sudo sh -c "echo deb https://get.docker.io/ubuntu \
 docker main > /etc/apt/sources.list.d/docker.list"

  1. 通过运行以下命令导入 Docker 发布工具的公钥:
$ sudo apt-key adv --keyserver \
 hkp://keyserver.ubuntu.com:80 --recv-keys \
 36A1D7869245C8950F966E92D8576A8BA88D21E9

  1. 使用以下命令重新与软件包存储库同步:
$ sudo apt-get update

  1. 安装docker,然后启动docker服务。
$ sudo apt-get install -y lxc-docker

注意

lxc-docker命令将使用名称docker安装 Docker 镜像。

Docker 社区通过隐藏这些细节在自动安装脚本中迈出了一步。该脚本使得在大多数流行的 Linux 发行版上安装 Docker 成为可能,可以通过curl命令或wget命令来实现,如下所示:

  • 对于 curl 命令:
$ sudo curl -sSL https://get.docker.io/ | sh

  • 对于 wget 命令:
$ sudo wget -qO- https://get.docker.io/ | sh

注意

前面的自动化脚本方法将 AUFS 作为底层 Docker 文件系统。该脚本探测 AUFS 驱动程序,如果在系统中找不到,则自动安装它。此外,它还在安装后进行一些基本测试以验证其完整性。

理解 Docker 设置

重要的是要了解 Docker 的组件及其版本、存储、执行驱动程序、文件位置等。顺便说一句,对于理解 Docker 设置的追求也将揭示安装是否成功。你可以通过使用两个docker子命令来实现这一点,即docker versiondocker info

让我们通过docker version子命令开始我们的docker之旅,如下所示:

$ sudo docker version
Client version: 1.5.0
Client API version: 1.17
Go version (client): go1.4.1
Git commit (client): a8a31ef
OS/Arch (client): linux/amd64
Server version: 1.5.0
Server API version: 1.17
Go version (server): go1.4.1
Git commit (server): a8a31ef

尽管docker version子命令列出了许多文本行,作为 Docker 用户,你应该知道以下输出行的含义:

  • 客户端版本

  • 客户端 API 版本

  • 服务器版本

  • 服务器 API 版本

在这里考虑的客户端和服务器版本分别为 1.5.0 和客户端 API 和服务器 API 版本 1.17。

如果我们分析docker version子命令的内部,它首先会列出本地存储的与客户端相关的信息。随后,它将通过 HTTP 向服务器发出 REST API 调用,以获取与服务器相关的详细信息。

让我们使用docker info子命令来了解更多关于 Docker 环境的信息:

$ sudo docker -D info
Containers: 0
Images: 0
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Backing Filesystem: extfs
 Dirs: 0
Execution Driver: native-0.2
Kernel Version: 3.13.0-45-generic
Operating System: Ubuntu 14.04.1 LTS
CPUs: 4
Total Memory: 3.908 GiB
Name: dockerhost
ID: ZNXR:QQSY:IGKJ:ZLYU:G4P7:AXVC:2KAJ:A3Q5:YCRQ:IJD3:7RON:IJ6Y
Debug mode (server): false
Debug mode (client): true
Fds: 10
Goroutines: 14
EventsListeners: 0
Init Path: /usr/bin/docker
Docker Root Dir: /var/lib/docker
WARNING: No swap limit support

正如您在新安装的 Docker 引擎的输出中所看到的,容器镜像的数量始终为零。存储驱动程序已设置为aufs,并且目录已设置为/var/lib/docker/aufs位置。执行驱动程序已设置为本机模式。此命令还列出了详细信息,如内核版本操作系统CPU数量、总内存名称,即新的 Docker 主机名。

客户端服务器通信

在 Linux 安装中,Docker 通常通过使用 Unix 套接字(/var/run/docker.sock)进行服务器-客户端通信。Docker 还有一个 IANA 注册的端口,即2375。然而,出于安全原因,此端口默认情况下未启用。

下载第一个 Docker 镜像

成功安装了 Docker 引擎后,下一个逻辑步骤是从 Docker 注册表中下载镜像。Docker 注册表是一个应用程序存储库,其中托管了一系列应用程序,从基本的 Linux 镜像到高级应用程序不等。docker pull子命令用于从注册表下载任意数量的镜像。在本节中,我们将使用以下命令下载一个名为busybox的小型 Linux 版本的镜像:

$ sudo docker pull busybox
511136ea3c5a: Pull complete
df7546f9f060: Pull complete
ea13149945cb: Pull complete
4986bf8c1536: Pull complete
busybox:latest: The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security.
Status: Downloaded newer image for busybox:latest

一旦镜像被下载,可以使用docker images子命令进行验证,如下所示:

$ sudo docker images
REPOSITORY    TAG     IMAGE ID         CREATED      VIRTUAL SIZE
busybox       latest  4986bf8c1536     12 weeks ago 2.433 MB

运行第一个 Docker 容器

现在,您可以启动您的第一个 Docker 容器。以基本的Hello World!应用程序开始是标准做法。在下面的示例中,我们将使用已经下载的busybox镜像来回显Hello World!,如下所示:

$ sudo docker run busybox echo "Hello World!"
"Hello World!"

很酷,不是吗?您已经在短时间内设置了您的第一个 Docker 容器。在上面的示例中,使用了docker run子命令来创建一个容器,并使用echo命令打印Hello World!

在亚马逊网络服务上运行 Docker 容器

亚马逊网络服务AWS)在 2014 年初宣布了 Docker 容器的可用性,作为其 Elastic Beanstalk 提供的一部分。在 2014 年底,他们改革了 Docker 部署,并为用户提供了以下选项来运行 Docker 容器:

  • 亚马逊 EC2 容器服务(在撰写本书时仅处于预览模式)

  • 通过使用亚马逊弹性豆服务进行 Docker 部署

亚马逊 EC2 容器服务允许您通过简单的 API 调用启动和停止容器启用的应用程序。AWS 引入了集群的概念,用于查看容器的状态。您可以从集中式服务查看任务,并且它为您提供了许多熟悉的亚马逊 EC2 功能,如安全组、EBS 卷和 IAM 角色。

请注意,此服务仍未在 AWS 控制台中可用。您需要在您的机器上安装 AWS CLI 来部署、运行和访问此服务。

AWS Elastic Beanstalk 服务支持以下内容:

  • 使用控制台支持 Elastic Beanstalk 的单个容器。目前,它支持 PHP 和 Python 应用程序。

  • 使用一个名为eb的命令行工具支持 Elastic Beanstalk 的单个容器。它支持相同的 PHP 和 Python 应用程序。

  • 通过使用 Elastic Beanstalk 使用多个容器环境。

目前,AWS 支持最新的 Docker 版本,即 1.5。

本节提供了在运行在 AWS Elastic Beanstalk 上的 Docker 容器上部署示例应用程序的逐步过程。以下是部署的步骤:

  1. 通过使用此console.aws.amazon.com/elasticbeanstalk/ URL 登录到 AWS Elastic Beanstalk 控制台。

  2. 选择要部署应用程序的区域,如下所示:在亚马逊网络服务上运行 Docker 容器

  3. 选择下拉菜单中的Docker选项,然后点击立即启动。几分钟后,下一个屏幕将显示如下:在亚马逊网络服务上运行 Docker 容器

现在,点击旁边的 URL Default-Environment (Default-Environment-pjgerbmmjm.elasticbeanstalk.com),如下所示:

在亚马逊网络服务上运行 Docker 容器

故障排除

大多数情况下,安装 Docker 时不会遇到任何问题。然而,可能会发生意外故障。因此,有必要讨论突出的故障排除技术和技巧。让我们从本节讨论故障排除知识开始。第一个提示是使用以下命令检查 Docker 的运行状态:

$ sudo service docker status

但是,如果 Docker 是通过 Ubuntu 软件包安装的,则必须使用docker.io作为服务名称。如果docker服务正在运行,则此命令将打印状态为start/running以及其进程 ID。

如果您在 Docker 设置中仍然遇到问题,那么您可以使用/var/log/upstart/docker.log文件打开 Docker 日志进行进一步调查。

总结

容器化将成为未来企业和云 IT 环境的主导和决定性范式,因为它具有迄今为止未曾预见的自动化和加速能力。有几种机制可以将容器化运动推向更高的高度。然而,在这场激烈的竞赛中,Docker 已经遥遥领先,并成功摧毁了先前阐明的障碍。

在本章中,我们专注于 Docker 的实际应用,为您提供学习最有前途的技术的起点。我们列出了在不同环境中轻松安装 Docker 引擎的适当步骤和技巧,以及利用和构建、安装和运行一些示例 Docker 容器的方法,无论是在本地还是远程环境中。我们将深入探讨 Docker 的世界,并深入挖掘,以在接下来的章节中与您分享战术和战略上的可靠信息。请继续阅读,以获取有关高级主题(如容器集成、编排、管理、治理、安全等)的所需知识,通过 Docker 引擎。我们还将讨论大量第三方工具。

第二章:处理 Docker 容器

在上一章中,我们解释了激动人心和可持续的概念,展示了 Docker 打造未来和灵活的应用感知容器的方式。我们讨论了在多个环境(本地和离线)中生成 Docker 容器的所有相关细节。使用这些技术,您可以轻松地在自己的环境中复制这些功能,获得丰富的体验。因此,我们的下一步是以果断的方式了解容器的生命周期方面。您将学习如何以有效和无风险的方式最佳利用我们自己的容器以及其他第三方容器。容器可以被发现、评估、访问和利用,以实现更大更好的应用。出现了几种工具来简化容器的处理。

在本章中,我们将深入挖掘并详细描述容器处理的关键方面。本章还将讨论一些实用技巧和执行命令,以利用容器。

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

  • 澄清 Docker 术语

  • 与 Docker 镜像和容器一起工作

  • Docker 注册表及其存储库的含义

  • Docker Hub 注册表

  • 搜索 Docker 镜像

  • 与交互式容器一起工作

  • 跟踪容器内部的变化

  • 控制和管理 Docker 容器

  • 从容器构建镜像

  • 将容器作为守护进程启动

澄清 Docker 术语

为了使本章更易于理解并尽量减少任何形式的歧义,常用术语将在下一节中解释。

Docker 镜像和容器

Docker 镜像是构成软件应用程序的所有文件的集合。对原始镜像所做的每个更改都存储在单独的层中。准确地说,任何 Docker 镜像都必须源自基础镜像,根据各种要求。可以附加额外的模块到基础镜像,以派生出可以展现所需行为的各种镜像。每次提交到 Docker 镜像时,都会在 Docker 镜像上创建一个新的层,但原始镜像和每个现有的层都保持不变。换句话说,镜像通常是只读类型的。如果它们通过系统化附加新模块的方式得到增强,那么将创建一个带有新名称的新镜像。Docker 镜像正在成为开发和部署 Docker 容器的可行基础。

这里展示了一个基础镜像。Debian 是基础镜像,可以在基础镜像上合并各种所需的功能模块,以得到多个镜像:

Docker 镜像和容器

每个镜像都有一个唯一的ID,如下一节所述。基础镜像可以进行增强,以便它们可以创建父镜像,而父镜像反过来可以用于创建子镜像。基础镜像没有任何父级,也就是说,父镜像位于基础镜像之上。当我们使用一个镜像时,如果没有通过适当的标识(比如一个新名称)指定该镜像,那么 Docker 引擎将始终识别并使用latest镜像(最近生成的)。

根据 Docker 官网,Docker 镜像是一个只读模板。例如,一个镜像可以包含一个 Ubuntu 操作系统,上面安装了 Apache 和你的 Web 应用程序。Docker 提供了一种简单的方法来构建新的镜像或更新现有的镜像。你也可以下载其他人已经创建的 Docker 镜像。Docker 镜像是 Docker 容器的构建组件。一般来说,基础 Docker 镜像代表一个操作系统,在 Linux 的情况下,基础镜像可以是其发行版之一,比如 Debian。向基础镜像添加额外的模块最终形成一个容器。最简单的想法是,容器是一个位于一个或多个只读镜像上的读写层。当容器运行时,Docker 引擎不仅将所有所需的镜像合并在一起,还将读写层的更改合并到容器本身。这使得它成为一个自包含、可扩展和可执行的系统。可以使用 Docker 的docker commit子命令来合并更改。新容器将容纳对基础镜像所做的所有更改。新镜像将形成基础镜像的新层。

下图将清楚地告诉你一切。基础镜像是Debian发行版,然后添加了两个镜像(emacsApache服务器),这将导致容器:

Docker 镜像和容器

每次提交都会生成一个新镜像。这使得镜像数量稳步增加,因此管理它们变得复杂。然而,存储空间并不是一个大挑战,因为生成的新镜像只包括新添加的模块。在某种程度上,这类似于云环境中流行的对象存储。每次更新对象,都会创建一个带有最新修改的新对象,然后以新的ID存储。在对象存储的情况下,存储大小会显著增加。

一个 Docker 层

一个Docker 层可以代表只读镜像或读写镜像。然而,容器堆栈的顶层始终是读写(可写)层,其中托管着一个 Docker 容器。

一个 Docker 容器

从前面的图表可以看出,读写层是容器层。容器层下面可能有几个只读镜像。通常,容器是通过 commit 操作从只读镜像创建的。当您 start 一个容器时,实际上是通过其唯一的 ID 引用一个镜像。Docker 拉取所需的镜像及其父镜像。它继续拉取所有父镜像,直到达到基础镜像。

Docker 注册表

Docker 注册表是 Docker 镜像可以存储的地方,以便全球开发人员可以快速创建新的复合应用程序,而不会有任何风险。因为所有存储的镜像都经过多次验证、核实和完善,这些镜像的质量将非常高。使用 Docker push 命令,您可以将 Docker 镜像发送到注册表,以便注册和存储。澄清一下,注册表是用于注册 Docker 镜像的,而仓库是用于在公开可发现和集中的位置存储这些已注册的 Docker 镜像。Docker 镜像存储在 Docker 注册表中的仓库中。每个用户或帐户的仓库都是唯一的。

Docker 仓库

Docker 仓库是用于存储 Docker 镜像的命名空间。例如,如果您的应用程序命名为 helloworld,并且您的用户名或注册表的命名空间为 thedockerbook,那么在 Docker 仓库中,此镜像将存储在 Docker 注册表中,命名为 thedockerbook/helloworld

基础镜像存储在 Docker 仓库中。基础镜像是实现更大更好镜像的源泉,通过谨慎添加新模块来帮助实现。子镜像是具有自己父镜像的镜像。基础镜像没有任何父镜像。坐在基础镜像上的镜像被称为父镜像,因为父镜像承载着子镜像。

使用 Docker 镜像

在上一章中,我们通过使用busybox镜像演示了典型的Hello World!示例。现在需要仔细观察docker pull子命令的输出,这是一个用于下载 Docker 镜像的标准命令。您可能已经注意到输出文本中存在busybox:latest的文本,我们将通过对docker pull子命令添加-a选项来详细解释这个谜团。

$ sudo docker pull -a busybox

令人惊讶的是,您会发现 Docker 引擎使用-a选项下载了更多的镜像。您可以通过运行docker images子命令轻松检查 Docker 主机上可用的镜像,这非常方便,并且通过运行此命令可以揭示有关:latest和其他附加镜像的更多细节。让我们运行这个命令:

$ sudo docker images

您将获得以下镜像列表:

REPOSITORY TAG                  IMAGE ID      CREATED       VIRTUAL SIZE
busybox    ubuntu-14.04         f6169d24347d  3 months ago  5.609 MB
busybox    ubuntu-12.04         492dad4279ba  3 months ago  5.455 MB
busybox    buildroot-2014.02    4986bf8c1536  3 months ago  2.433 MB
busybox    latest               4986bf8c1536  3 months ago  2.433 MB
busybox    buildroot-2013.08.1  2aed48a4e41d  3 months ago  2.489 MB

显然,我们在前面的列表中有五个项目,为了更好地理解这些项目,我们需要理解 Docker images 子命令打印出的信息。以下是可能的类别列表:

  • 仓库: 这是仓库或镜像的名称。在前面的例子中,仓库名称是busybox

  • 标签: 这是与镜像相关联的标签,例如buildroot-2014.02ubuntu-14.04latest。一个镜像可以关联一个或多个标签。

注意

ubuntu-标记的镜像是使用busybox-static Ubuntu 软件包构建的,以buildroot-标记的镜像是使用buildroot工具链从头开始构建的。

  • 镜像 ID: 每个镜像都有一个唯一的ID。镜像ID由一个 64 位十六进制长的随机数表示。默认情况下,Docker images 子命令只会显示 12 位十六进制数。您可以使用--no-trunc标志显示所有 64 位十六进制数(例如:sudo docker images --no-trunc)。

  • 创建时间: 表示镜像创建的时间。

  • 虚拟大小: 突出显示镜像的虚拟大小。

也许您会想知道,在前面的例子中,一个带有-a选项的pull命令是如何能够下载五个镜像的,尽管我们只指定了一个名为busybox的镜像。这是因为每个 Docker 镜像存储库都可以有同一镜像的多个变体,-a选项会下载与该镜像相关的所有变体。在前面的例子中,这些变体被标记为buildroot-2013.08.1ubuntu-14.04ubuntu-12.04buildroot-2014.02latest。对镜像 ID 的仔细观察将揭示buildroot-2014.02latest共享镜像 ID4986bf8c1536

默认情况下,Docker 始终使用标记为latest的镜像。每个镜像变体都可以通过其标签直接识别。可以通过在标签和存储库名称之间添加:来将镜像标记为合格。例如,您可以使用busybox:ubuntu-14.04标签启动一个容器,如下所示:

$ sudo docker run -t -i busybox:ubuntu-14.04

docker pull子命令将始终下载具有该存储库中latest标签的镜像变体。但是,如果您选择下载除最新版本之外的其他镜像变体,则可以通过使用以下命令来限定镜像的标签名称来执行此操作:

$ sudo docker pull busybox:ubuntu-14.04

Docker Hub 注册表

在上一节中,当您运行docker pull子命令时,busybox镜像神秘地被下载了。在本节中,让我们揭开docker pull子命令周围的神秘,并且 Docker Hub 对这一意外成功做出了巨大贡献。

Docker 社区的热心人士已经构建了一个镜像存储库,并且已经将其公开放置在默认位置index.docker.io。这个默认位置称为 Docker 索引。docker pull子命令被编程为在此位置查找镜像。因此,当您pull一个busybox镜像时,它会轻松地从默认注册表中下载。这种机制有助于加快 Docker 容器的启动速度。Docker Index 是官方存储库,其中包含由全球 Docker 开发社区创建和存放的所有经过精心策划的镜像。

这所谓的治疗措施是为了确保存储在 Docker 索引中的所有镜像都通过一系列隔离任务是安全的。有经过验证和验证的方法来清理任何故意或无意引入的恶意软件、广告软件、病毒等等,从这些 Docker 镜像中。数字签名是 Docker 镜像的最高完整性的突出机制。然而,如果官方镜像已经被损坏或篡改,那么 Docker 引擎将发出警告,然后继续运行镜像。

除了官方存储库之外,Docker Hub 注册表还为第三方开发人员和提供商提供了一个平台,供他们共享供一般用户使用的镜像。第三方镜像以其开发人员或存款人的用户 ID 为前缀。例如,thedockerbook/helloworld是一个第三方镜像,其中thedockerbook是用户 ID,helloworld是镜像存储库名称。您可以使用docker pull子命令下载任何第三方镜像,如下所示:

$ sudo docker pull thedockerbook/helloworld

除了前面的存储库之外,Docker 生态系统还提供了一种利用来自 Docker Hub 注册表以外的任何第三方存储库中的镜像的机制,并且它还提供了本地存储库中托管的镜像。如前所述,Docker 引擎默认情况下已编程为在index.docker.io中查找镜像,而在第三方或本地存储库中,我们必须手动指定应从哪里拉取镜像的路径。手动存储库路径类似于没有协议说明符的 URL,例如https://http://ftp://。以下是从第三方存储库中拉取镜像的示例:

$ sudo docker pull registry.example.com/myapp

搜索 Docker 镜像

正如我们在前一节中讨论的,Docker Hub 存储库通常托管官方镜像以及由第三方 Docker 爱好者贡献的镜像。在撰写本书时,超过 14,000 个镜像(也称为 Docker 化应用程序)可供用户使用。这些镜像可以直接使用,也可以作为用户特定应用程序的构建块使用。

您可以使用docker search子命令在 Docker Hub 注册表中搜索 Docker 镜像,如本示例所示:

$ sudo docker search mysql

mysql上的搜索将列出 400 多个镜像,如下所示:

NAME             DESCRIPTION          STARS  OFFICIAL   AUTOMATED
mysql            MySQL is the...      147    [OK]
tutum/mysql      MySQL Server..       60                [OK]
orchardup/mysql                       34                [OK]
. . . OUTPUT TRUNCATED . . .

如前面的搜索输出摘录所示,图像是根据其星级排序的。搜索结果还表明图像是否官方。为了保持专注,在这个例子中,我们将只显示两个图像。在这里,您可以看到mysql的官方版本,它拉取了一个147星级的图像作为其第一个结果。第二个结果显示,这个版本的mysql图像是由用户tutum发布的。Docker 容器正迅速成为分布式应用程序的标准构建块。借助全球许多社区成员的热情贡献,将实现 Docker 图像的动态存储库。基于存储库的软件工程将使用户和程序员更容易快速编写和组装他们的项目。官方存储库可以免费从 Docker Hub Registry 下载,这些是经过策划的图像。它们代表了一个专注于为应用程序提供良好图像基础的社区努力,以便开发人员和系统管理员可以专注于构建新功能和功能,同时最大程度地减少他们在商品脚手架和管道上的重复工作。

根据 Docker Hub Registry 中的搜索查询和与许多开发人员社区成员的讨论,Docker 公司强有力而充满激情地领导了 Docker 运动,得出结论,开发人员社区希望获得他们最喜爱的编程语言的预构建堆栈。具体来说,开发人员希望尽快开始编写代码,而不浪费时间与环境、脚手架和依赖进行斗争。

与交互式容器一起工作

在第一章中,我们运行了我们的第一个Hello World!容器,以了解容器化技术的工作原理。在本节中,我们将以交互模式运行一个容器。docker run子命令以镜像作为输入,并将其作为容器启动。您必须在 docker run 子命令中传递-t-i标志,以使容器变为交互式。-i标志是关键驱动程序,它通过获取容器的标准输入(STDIN)使容器变得交互式。-t标志分配一个伪 TTY 或伪终端(终端仿真器),然后将其分配给容器。

在下面的示例中,我们将使用ubuntu:14.04镜像和/bin/bash作为命令启动一个交互式容器:

$ sudo docker run -i -t ubuntu:14.04 /bin/bash

由于ubuntu镜像尚未下载,如果我们使用docker pull子命令,那么我们将收到以下消息,并且run命令将自动开始拉取ubuntu镜像,并显示以下消息:

Unable to find image 'ubuntu:14.04' locally
Pulling repository ubuntu

一旦下载完成,容器将与ubuntu:14.04镜像一起启动。它还将在容器内启动一个 bash shell,因为我们已指定/bin/bash作为要执行的命令。这将使我们进入一个 bash 提示符,如下所示:

root@742718c21816:/#

前面的 bash 提示将确认我们的容器已成功启动,并且准备好接受我们的输入。如果您对提示中的十六进制数字742718c21816感到困惑,那么它只是容器的主机名。在 Docker 术语中,主机名与容器ID相同。

让我们快速交互式地运行一些命令,然后确认我们之前提到的提示是正确的,如下所示:

root@742718c21816:/# hostname
742718c21816
root@742718c21816:/# id
uid=0(root) gid=0(root) groups=0(root)
root@742718c21816:/# echo $PS1
${debian_chroot:+($debian_chroot)}\u@\h:\w\$
root@742718c21816:/#

从前面的三个命令可以清楚地看出,提示是通过使用用户 ID、主机名和当前工作目录组成的。

现在,让我们使用 Docker 的一个特色功能,将其从交互式容器中分离出来,然后查看 Docker 为该容器管理的细节。是的,我们可以通过使用Ctrl + PCtrl + Q转义序列将其从容器中分离出来。这个转义序列将从容器中分离 TTY,并将我们置于 Docker 主机提示符$中,但是容器将继续运行。docker ps子命令将列出所有正在运行的容器及其重要属性,如下所示:

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
742718c21816        ubuntu:14.04        "/bin/bash"         About a minute ago   Up About a minute                       jolly_lovelace

docker ps子命令将列出以下详细信息:

  • 容器 ID:这显示了与容器关联的容器ID。容器ID是一个 64 位十六进制长随机数。默认情况下,docker ps子命令将只显示 12 位十六进制数。您可以使用--no-trunc标志显示所有 64 位数字(例如:sudo docker ps --no-trunc)。

  • 镜像:这显示了 Docker 容器所制作的镜像。

  • 命令:这显示了容器启动期间执行的命令。

  • 创建时间:这告诉您容器何时创建。

  • 状态:这告诉您容器的当前状态。

  • PORTS:这告诉你是否已经为容器分配了任何端口。

  • NAMES:Docker 引擎通过连接形容词和名词自动生成一个随机容器名称。容器的ID或名称都可以用来对容器进行进一步操作。容器名称可以通过在docker run子命令中使用--name选项手动配置。

查看了容器状态后,让我们使用docker attach子命令将其重新附加到我们的容器中,如下例所示。我们可以使用容器的ID或名称。在这个例子中,我们使用了容器的名称。如果你看不到提示符,那么再次按下Enter键:

$ sudo docker attach jolly_lovelace
root@742718c21816:/#

注意

Docker 允许任意次数地附加到容器,这对屏幕共享非常方便。

docker attach子命令将我们带回容器提示符。让我们使用这些命令对正在运行的交互式容器进行更多实验:

root@742718c21816:/# pwd
/
root@742718c21816:/# ls
bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
boot  etc  lib   media  opt  root  sbin  sys  usr
root@742718c21816:/# cd usr
root@742718c21816:/usr# ls
bin  games  include  lib  local  sbin  share  src
root@742718c21816:/usr# exit
exit
$

一旦对交互式容器发出 bash 退出命令,它将终止 bash shell 进程,进而停止容器。因此,我们将会回到 Docker 主机的提示符$

在容器内跟踪更改

在上一节中,我们演示了如何以ubuntu为基础镜像创建容器,然后运行一些基本命令,比如分离和附加容器。在这个过程中,我们还向您介绍了docker ps子命令,它提供了基本的容器管理功能。在本节中,我们将演示如何有效地跟踪我们在容器中引入的更改,并将其与我们启动容器的镜像进行比较。

让我们以交互模式启动一个容器,就像在上一节中所做的那样:

$ sudo docker run -i -t ubuntu:14.04 /bin/bash

让我们把目录切换到/home,如下所示:

root@d5ad60f174d3:/# cd /home

现在我们可以使用touch命令创建三个空文件,如下面的代码片段所示。第一个ls -l命令将显示目录中没有文件,第二个ls -l命令将显示有三个空文件:

root@d5ad60f174d3:/home# ls -l
total 0
root@d5ad60f174d3:/home# touch {abc,cde,fgh}
root@d5ad60f174d3:/home# ls -l
total 0
-rw-r--r-- 1 root root 0 Sep 29 10:54 abc
-rw-r--r-- 1 root root 0 Sep 29 10:54 cde
-rw-r--r-- 1 root root 0 Sep 29 10:54 fgh
root@d5ad60f174d3:/home#

Docker 引擎优雅地管理其文件系统,并允许我们使用docker diff子命令检查容器文件系统。为了检查容器文件系统,我们可以将其与容器分离,或者使用 Docker 主机的另一个终端,然后发出docker diff子命令。由于我们知道任何ubuntu容器都有其主机名,这是其提示的一部分,也是容器的ID,我们可以直接使用从提示中获取的容器ID运行docker diff子命令,如下所示:

$ sudo docker diff d5ad60f174d3

在给定的示例中,docker diff子命令将生成四行,如下所示:

C /home
A /home/abc
A /home/cde
A /home/fgh

前面的输出表明/home目录已被修改,这由C,表示,/home/abc/home/cde/home/fgh文件已被添加,这些由A表示。此外,D表示删除。由于我们没有删除任何文件,因此它不在我们的示例输出中。

控制 Docker 容器

到目前为止,我们已经讨论了一些实际示例,以清楚地阐明 Docker 容器的细枝末节。在本节中,让我们介绍一些基本的以及一些高级的命令结构,以精确地说明如何管理 Docker 容器。

Docker 引擎使您能够使用一组docker子命令startstoprestart容器。让我们从docker stop子命令开始,该子命令停止正在运行的容器。当用户发出此命令时,Docker 引擎向容器内运行的主进程发送 SIGTERM(-15)。SIGTERM信号请求进程优雅地终止自身。大多数进程会处理此信号并促进优雅退出。但是,如果此进程未能这样做,那么 Docker 引擎将等待一段宽限期。即使在宽限期之后,如果进程未被终止,那么 Docker 引擎将强制终止该进程。通过发送 SIGKILL(-9)来实现强制终止。SIGKILL信号无法被捕获或忽略,因此它将导致进程在没有适当清理的情况下突然终止。

现在,让我们启动我们的容器,并尝试使用docker stop子命令,如下所示:

$ sudo docker run -i -t ubuntu:14.04 /bin/bash
root@da1c0f7daa2a:/#

启动容器后,让我们使用从提示中获取的容器ID在该容器上运行docker stop子命令。当然,我们必须使用第二个屏幕或终端来运行此命令,命令将始终回显到容器ID,如下所示:

$ sudo docker stop da1c0f7daa2a
da1c0f7daa2a

现在,如果我们切换到正在运行容器的屏幕或终端,我们将注意到容器正在被终止。如果你更仔细观察,你还会注意到容器提示旁边的文本exit。这是由于 bash shell 的 SIGTERM 处理机制导致的,如下所示:

root@da1c0f7daa2a:/# exit
$

如果我们再进一步运行docker ps子命令,那么我们将在列表中找不到这个容器。事实上,默认情况下,docker ps子命令总是列出处于运行状态的容器。由于我们的容器处于停止状态,它已经舒适地被从列表中排除了。现在,你可能会问,我们如何看到处于停止状态的容器呢?好吧,docker ps子命令带有一个额外的参数-a,它将列出 Docker 主机中的所有容器,而不管它的状态如何。可以通过运行以下命令来实现:

$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                      PORTS               NAMES
da1c0f7daa2a        ubuntu:14.04        "/bin/bash"            20 minutes ago        Exited (0) 10 minutes ago                        desperate_engelbart
$

接下来,让我们看看docker start子命令,它用于启动一个或多个已停止的容器。容器可以通过docker stop子命令或正常或异常地终止容器中的主进程而被移动到停止状态。对于正在运行的容器,此子命令没有任何效果。

让我们使用docker start子命令start先前停止的容器,如下所示:

$ sudo docker start da1c0f7daa2a
da1c0f7daa2a
$

默认情况下,docker start子命令不会附加到容器。您可以通过在docker start子命令中使用-a选项或显式使用docker attach子命令将其附加到容器,如下所示:

$ sudo docker attach da1c0f7daa2a
root@da1c0f7daa2a:/#

现在让我们运行docker ps并验证容器的运行状态,如下所示:

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                      PORTS               NAMES
da1c0f7daa2a        ubuntu:14.04        "/bin/bash"            25 minutes ago        Up 3 minutes                        desperate_engelbart
$

restart命令是stopstart功能的组合。换句话说,restart命令将通过docker stop子命令遵循的精确步骤stop一个正在运行的容器,然后它将启动start过程。此功能将默认通过docker restart子命令执行。

下一个重要的容器控制子命令集是docker pausedocker unpausedocker pause子命令将基本上冻结容器中所有进程的执行。相反,docker unpause子命令将解冻容器中所有进程的执行,并从冻结的点恢复执行。

在看完pauseunpause的技术解释后,让我们看一个详细的示例来说明这个功能是如何工作的。我们使用了两个屏幕或终端场景。在一个终端上,我们启动了容器,并使用了一个无限循环来显示日期和时间,每隔 5 秒睡眠一次,然后继续循环。我们将运行以下命令:

$ sudo docker run -i -t ubuntu:14.04 /bin/bash
root@c439077aa80a:/# while true; do date; sleep 5; done
Thu Oct  2 03:11:19 UTC 2014
Thu Oct  2 03:11:24 UTC 2014
Thu Oct  2 03:11:29 UTC 2014
Thu Oct  2 03:11:34 UTC 2014
Thu Oct  2 03:11:59 UTC 2014
Thu Oct  2 03:12:04 UTC 2014
Thu Oct  2 03:12:09 UTC 2014
Thu Oct  2 03:12:14 UTC 2014
Thu Oct  2 03:12:19 UTC 2014
Thu Oct  2 03:12:24 UTC 2014
Thu Oct  2 03:12:29 UTC 2014
Thu Oct  2 03:12:34 UTC 2014
$

我们的小脚本非常忠实地每 5 秒打印一次日期和时间,但在以下位置有一个例外:

Thu Oct  2 03:11:34 UTC 2014
Thu Oct  2 03:11:59 UTC 2014

在这里,我们遇到了 25 秒的延迟,因为这是我们在第二个终端屏幕上启动了docker pause子命令的时候,如下所示:

$ sudo docker pause c439077aa80a
c439077aa80a

当我们暂停容器时,我们使用docker ps子命令查看了容器上的进程状态,它在同一屏幕上,并清楚地指示容器已被暂停,如此命令结果所示:

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                   PORTS               NAMES
c439077aa80a        ubuntu:14.04        "/bin/bash"         47 seconds ago      Up 46 seconds (Paused)                       ecstatic_torvalds

我们继续使用docker unpause子命令,解冻了我们的容器,继续执行,并开始打印日期和时间,就像我们在前面的命令中看到的那样,如下所示:

$ sudo docker unpause c439077aa80a
c439077aa80a

我们在本节开始时解释了pauseunpause命令。最后,使用docker stop子命令停止了容器和其中运行的脚本,如下所示:

$ sudo docker stop c439077aa80a
c439077aa80a

容器清理

在许多先前的示例中,当我们发出docker ps -a时,我们看到了许多已停止的容器。如果我们选择不进行干预,这些容器可能会继续停留在停止状态很长时间。起初,这可能看起来像是一个故障,但实际上,我们可以执行操作,比如从容器中提交一个镜像,重新启动已停止的容器等。然而,并非所有已停止的容器都会被重用,每个未使用的容器都会占用 Docker 主机文件系统中的磁盘空间。Docker 引擎提供了几种方法来缓解这个问题。让我们开始探索它们。

在容器启动期间,我们可以指示 Docker 引擎在容器达到停止状态时立即清理容器。为此,docker run子命令支持--rm选项(例如:sudo docker run -i -t --rm ubuntu:14.04 /bin/bash)。

另一种选择是使用docker ps子命令的-a选项列出所有容器,然后通过使用docker rm子命令手动删除它们,如下所示:

$ sudo docker ps -a
CONTAINER ID IMAGE        COMMAND     CREATED       STATUS
                   PORTS   NAMES
7473f2568add ubuntu:14.04 "/bin/bash" 5 seconds ago Exited(0) 3 seconds ago         jolly_wilson
$ sudo docker rm 7473f2568add
7473f2568add
$

两个docker子命令,即docker rmdocker ps,可以组合在一起自动删除所有当前未运行的容器,如下命令所示:

$ sudo docker rm 'sudo docker ps -aq --no-trunc'

在上述命令中,反引号内的命令将产生每个容器的完整容器 ID 列表,无论是运行还是其他状态,这将成为docker rm子命令的参数。除非使用-f选项强制执行其他操作,否则docker rm子命令将仅删除未运行状态的容器。对于正在运行的容器,它将生成以下错误,然后继续到列表中的下一个容器:

Error response from daemon: You cannot remove a running container. Stop the container before attempting removal or use -f

从容器构建镜像

到目前为止,我们已经使用标准基本镜像busyboxubuntu创建了一些容器。在本节中,让我们看看如何在运行的容器中向基本镜像添加更多软件,然后将该容器转换为镜像以供将来使用。

让我们以ubuntu:14.04作为基本镜像,安装wget应用程序,然后通过以下步骤将运行的容器转换为镜像:

  1. 通过使用以下docker run子命令启动ubuntu:14.04容器,如下所示:
$ sudo docker run -i -t ubuntu:14.04 /bin/bash

  1. 启动容器后,让我们快速验证我们的镜像中是否有wget可用。我们已经使用which命令并将wget作为参数用于此目的,在我们的情况下,它返回空值,这基本上意味着它在这个容器中找不到任何wget安装。该命令如下运行:
root@472c96295678:/# which wget
root@472c96295678:/#

  1. 现在让我们继续下一步,涉及wget安装。由于这是一个全新的ubuntu容器,在安装wget之前,我们必须与ubuntu软件包存储库同步,如下所示:
root@472c96295678:/# apt-get update

  1. 一旦ubuntu软件包存储库同步完成,我们可以继续安装wget,如下所示:
root@472c96295678:/# apt-get install -y wget

  1. 完成wget安装后,让我们通过调用which命令并将wget作为参数来确认我们的wget安装,如下所示:
root@472c96295678:/#which wget
/usr/bin/wget
root@472c96295678:/#

  1. 安装任何软件都会改变基础镜像的组成,我们也可以通过本章节跟踪容器内部变化介绍的docker diff子命令来追踪这些变化。我们可以在第二个终端或屏幕上使用docker diff子命令,如下所示:
$ sudo docker diff 472c96295678

前面的命令将显示对ubuntu镜像的几百行修改。这些修改包括软件包存储库的更新,wget二进制文件以及wget的支持文件。

  1. 最后,让我们转向提交镜像的最重要步骤。Docker commit子命令可以在运行或停止的容器上执行。当在运行容器上执行commit时,Docker 引擎将在commit操作期间暂停容器,以避免任何数据不一致。我们强烈建议在停止的容器上执行commit操作。我们可以通过docker commit子命令将容器提交为镜像,如下所示:
$ sudo docker commit 472c96295678 \
 learningdocker/ubuntu_wget
a530f0a0238654fa741813fac39bba2cc14457aee079a7ae1fe1c64dc7e1ac25

我们已经使用名称learningdocker/ubuntu_wget提交了我们的镜像。

我们逐步看到了如何从容器创建镜像。现在,让我们快速列出我们的 Docker 主机上的镜像,并使用以下命令查看这个新创建的镜像是否是镜像列表的一部分:

$ sudo docker images
REPOSITORY                      TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
learningdocker/ubuntu_wget   latest              a530f0a02386        48 seconds ago      221.3 MB
busybox                         buildroot-2014.02   e72ac664f4f0        2 days ago          2.433 MB
ubuntu                          14.04               6b4e8a7373fe        2 days ago          194.8 MB

从前面的docker images子命令输出中,很明显我们从容器创建的镜像非常成功。

现在您已经学会了如何通过几个简单的步骤从容器创建镜像,我们鼓励您主要使用这种方法进行测试。创建镜像的最优雅和最推荐的方法是使用Dockerfile方法,这将在下一章介绍。

作为守护进程启动容器

我们已经尝试过交互式容器,跟踪了对容器的更改,从容器创建了镜像,然后深入了解了容器化范式。现在,让我们继续了解 Docker 技术的真正工作马。是的,没错。在本节中,我们将为您介绍启动容器的步骤,以分离模式启动容器的步骤。换句话说,我们将了解启动容器作为守护进程所需的步骤。我们还将查看在容器中生成的文本。

docker run子命令支持一个选项-d,它将以分离模式启动一个容器,也就是说,它将以守护进程的方式启动一个容器。为了举例说明,让我们回到我们在“暂停和恢复”容器示例中使用的日期和时间脚本,如下所示:

$ sudo docker run -d ubuntu \
 /bin/bash -c "while true; do date; sleep 5; done"
0137d98ee363b44f22a48246ac5d460c65b67e4d7955aab6cbb0379ac421269b

docker logs子命令用于查看守护进程容器生成的输出,如下所示:

$ sudo docker logs \
0137d98ee363b44f22a48246ac5d460c65b67e4d7955aab6cbb0379ac421269b
Sat Oct  4 17:41:04 UTC 2014
Sat Oct  4 17:41:09 UTC 2014
Sat Oct  4 17:41:14 UTC 2014
Sat Oct  4 17:41:19 UTC 2014

总结

在本章中,我们描述了在后期实施阶段获得的知识,主要是关于 Docker 容器的操作方面。我们通过澄清重要术语(如镜像、容器、注册表和仓库)来开始本章,以便让您能够清晰地理解随后阐述的概念。我们解释了如何在 Docker 仓库中搜索镜像。我们还讨论了 Docker 容器的操作和处理,如何跟踪容器内部的更改,如何控制和维护容器。在下一章中,我们将以易于理解的方式解释 Docker 镜像构建的过程。

第三章:构建镜像

在上一章中,我们详细向您解释了镜像和容器处理以及其维护技巧和提示。除此之外,我们还解释了在 Docker 容器上安装任何软件包的标准过程,然后将容器转换为镜像以供将来使用和操作。本章与之前的章节非常不同,它清楚地描述了如何使用Dockerfile构建 Docker 镜像的标准方式,这是为软件开发社区提供高度可用的 Docker 镜像的最有力的方式。利用Dockerfile是构建强大镜像的最有竞争力的方式。

本章将涵盖以下主题:

  • Docker 集成的镜像构建系统

  • Dockerfile 的语法快速概述

  • Dockerfile构建指令

  • Docker 如何存储镜像

Docker 集成的镜像构建系统

Docker 镜像是容器的基本构建模块。这些镜像可以是非常基本的操作环境,比如我们在前几章中使用 Docker 进行实验时发现的busyboxubuntu。另外,这些镜像也可以构建用于企业和云 IT 环境的高级应用程序堆栈。正如我们在上一章中讨论的,我们可以通过从基础镜像启动容器,安装所有所需的应用程序,进行必要的配置文件更改,然后将容器提交为镜像来手动制作镜像。

作为更好的选择,我们可以采用使用Dockerfile自动化方法来制作镜像。Dockerfile是一个基于文本的构建脚本,其中包含了一系列特殊指令,用于从基础镜像构建正确和相关的镜像。Dockerfile中的顺序指令可以包括基础镜像选择、安装所需应用程序、添加配置和数据文件,以及自动运行服务并将这些服务暴露给外部世界。因此,基于 Dockerfile 的自动化构建系统简化了镜像构建过程。它还在构建指令的组织方式和可视化完整构建过程的方式上提供了很大的灵活性。

Docker 引擎通过docker build子命令紧密集成了这个构建过程。在 Docker 的客户端-服务器范式中,Docker 服务器(或守护程序)负责完整的构建过程,Docker 命令行界面负责传输构建上下文,包括将Dockerfile传输到守护程序。

为了窥探本节中Dockerfile集成构建系统,我们将向您介绍一个基本的Dockerfile。然后我们将解释将该Dockerfile转换为图像,然后从该图像启动容器的步骤。我们的Dockerfile由两条指令组成,如下所示:

$ cat Dockerfile
FROM busybox:latest
CMD echo Hello World!!

接下来,我们将讨论前面提到的两条指令:

  • 第一条指令是选择基础图像。在这个例子中,我们将选择busybox:latest图像

  • 第二条指令是执行CMD命令,指示容器echo Hello World!!

现在,让我们通过调用docker build以及Dockerfile的路径来生成一个 Docker 图像。在我们的例子中,我们将从存储Dockerfile的目录中调用docker build子命令,并且路径将由以下命令指定:

$ sudo docker build .

发出上述命令后,build过程将通过将build context发送到daemon并显示以下文本开始:

Sending build context to Docker daemon 3.072 kB
Sending build context to Docker daemon
Step 0 : from busybox:latest

构建过程将继续,并在完成后显示以下内容:

Successfully built 0a2abe57c325

在前面的例子中,图像是由IMAGE ID 0a2abe57c325构建的。让我们使用这个图像通过使用docker run子命令来启动一个容器,如下所示:

$ sudo docker run 0a2abe57c325
Hello World!!

很酷,不是吗?凭借极少的努力,我们已经能够制作一个以busybox为基础图像,并且能够扩展该图像以生成Hello World!!。这是一个简单的应用程序,但是使用相同的技术也可以实现企业规模的图像。

现在让我们使用docker images子命令来查看图像的详细信息,如下所示:

$ sudo docker images
REPOSITORY     TAG         IMAGE ID      CREATED       VIRTUAL SIZE
<none>       <none>       0a2abe57c325    2 hours ago    2.433 MB

在这里,你可能会惊讶地看到IMAGEREPOSITORY)和TAG名称被列为<none>。这是因为当我们构建这个图像时,我们没有指定任何图像或任何TAG名称。你可以使用docker tag子命令指定一个IMAGE名称和可选的TAG名称,如下所示:

$ sudo docker tag 0a2abe57c325 busyboxplus

另一种方法是在build时使用-t选项为docker build子命令构建镜像名称,如下所示:

$ sudo docker build -t busyboxplus .

由于Dockerfile中的指令没有变化,Docker 引擎将高效地重用具有ID 0a2abe57c325的旧镜像,并将镜像名称更新为busyboxplus。默认情况下,构建系统会将latest作为TAG名称。可以通过在IMAGE名称之后指定TAG名称并在它们之间放置:分隔符来修改此行为。也就是说,<image name>:<tag name>是修改行为的正确语法,其中<image name>是镜像的名称,<tag name>是标签的名称。

再次使用docker images子命令查看镜像详细信息,您会注意到镜像(存储库)名称为busyboxplus,标签名称为latest

$ sudo docker images
REPOSITORY     TAG         IMAGE ID      CREATED       VIRTUAL SIZE
busyboxplus     latest       0a2abe57c325    2 hours ago    2.433 MB

始终建议使用镜像名称构建镜像是最佳实践。

在体验了Dockerfile的魔力之后,我们将在随后的章节中向您介绍Dockerfile的语法或格式,并解释一打Dockerfile指令。

注意

最新的 Docker 发布版(1.5)在docker build子命令中增加了一个额外选项(-f),用于指定具有替代名称的Dockerfile

Dockerfile 的语法快速概述

在本节中,我们将解释Dockerfile的语法或格式。Dockerfile由指令、注释和空行组成,如下所示:

# Comment

INSTRUCTION arguments

Dockerfile的指令行由两个组件组成,指令行以指令本身开头,后面跟着指令的参数。指令可以以任何大小写形式编写,换句话说,它是不区分大小写的。然而,标准做法或约定是使用大写以便与参数区分开来。让我们再次看一下我们之前示例中的Dockerfile的内容:

FROM busybox:latest
CMD echo Hello World!!

这里,FROM是一个指令,它以busybox:latest作为参数,CMD是一个指令,它以echo Hello World!!作为参数。

Dockerfile 中的注释行必须以 # 符号开头。指令后的 # 符号被视为参数。如果 # 符号前面有空格,则 docker build 系统将视其为未知指令并跳过该行。现在,让我们通过一个示例更好地理解这些情况,以更好地理解注释行:

  • 有效的 Dockerfile 注释行始终以 # 符号作为行的第一个字符:
# This is my first Dockerfile comment
  • # 符号可以作为参数的一部分:
CMD echo ### Welcome to Docker ###
  • 如果 # 符号前面有空格,则构建系统将其视为未知指令:
    # this is an invalid comment line

docker build 系统会忽略 Dockerfile 中的空行,因此鼓励 Dockerfile 的作者添加注释和空行,以大大提高 Dockerfile 的可读性。

Dockerfile 构建指令

到目前为止,我们已经看过集成构建系统、Dockerfile 语法和一个示例生命周期,包括如何利用示例 Dockerfile 生成镜像以及如何从该镜像中生成容器。在本节中,我们将介绍 Dockerfile 指令、它们的语法以及一些合适的示例。

FROM 指令

FROM 指令是最重要的指令,也是 Dockerfile 的第一个有效指令。它设置了构建过程的基础镜像。随后的指令将使用这个基础镜像并在其上构建。docker build 系统允许您灵活地使用任何人构建的镜像。您还可以通过添加更精确和实用的功能来扩展它们。默认情况下,docker build 系统在 Docker 主机中查找镜像。但是,如果在 Docker 主机中找不到镜像,则 docker build 系统将从公开可用的 Docker Hub Registry 拉取镜像。如果 docker build 系统在 Docker 主机和 Docker Hub Registry 中找不到指定的镜像,则会返回错误。

FROM 指令具有以下语法:

FROM <image>[:<tag>]

在上述代码语句中,请注意以下内容:

  • <image>:这是将用作基础镜像的镜像的名称。

  • <tag>:这是该镜像的可选标签限定符。如果未指定任何标签限定符,则假定为标签 latest

以下是使用镜像名称 centosFROM 指令的示例:

FROM centos

以下是带有镜像名称ubuntu和标签限定符14.04FROM指令的另一个示例:

FROM ubuntu:14.04

Docker 允许在单个Dockerfile中使用多个FROM指令以创建多个镜像。Docker 构建系统将拉取FROM指令中指定的所有镜像。Docker 不提供对使用多个FROM指令生成的各个镜像进行命名的任何机制。我们强烈不建议在单个Dockerfile中使用多个FROM指令,因为可能会产生破坏性的冲突。

MAINTAINER 指令

MAINTAINER指令是Dockerfile的信息指令。此指令能力使作者能够在镜像中设置详细信息。Docker 不对在Dockerfile中放置MAINTAINER指令施加任何限制。但强烈建议您在FROM指令之后放置它。

以下是MAINTAINER指令的语法,其中<author's detail>可以是任何文本。但强烈建议您使用镜像作者的姓名和电子邮件地址,如此代码语法所示:

MAINTAINER <author's detail>

以下是带有作者姓名和电子邮件地址的MAINTAINER指令的示例:

MAINTAINER Dr. Peter <peterindia@gmail.com>

COPY指令

COPY指令使您能够将文件从 Docker 主机复制到新镜像的文件系统中。以下是COPY指令的语法:

COPY <src> ... <dst>

前面的代码术语包含了这里显示的解释:

  • <src>:这是源目录,构建上下文中的文件,或者是执行docker build子命令的目录。

  • ...:这表示可以直接指定多个源文件,也可以通过通配符指定多个源文件。

  • <dst>:这是新镜像的目标路径,源文件或目录将被复制到其中。如果指定了多个文件,则目标路径必须是目录,并且必须以斜杠/结尾。

推荐为目标目录或文件使用绝对路径。如果没有绝对路径,COPY指令将假定目标路径将从根目录/开始。COPY指令足够强大,可以用于创建新目录,并覆盖新创建的镜像中的文件系统。

在下面的示例中,我们将使用COPY指令将源构建上下文中的html目录复制到镜像文件系统中的/var/www/html,如下所示:

COPY html /var/www/html

这是另一个示例,多个文件(httpd.confmagic)将从源构建上下文复制到镜像文件系统中的/etc/httpd/conf/

COPY httpd.conf magic /etc/httpd/conf/

ADD 指令

ADD指令类似于COPY指令。但是,除了COPY指令支持的功能之外,ADD指令还可以处理 TAR 文件和远程 URL。我们可以将ADD指令注释为“功能更强大的 COPY”。

以下是ADD指令的语法:

ADD <src> ... <dst>

ADD指令的参数与COPY指令的参数非常相似,如下所示:

  • <src>:这既可以是构建上下文中的源目录或文件,也可以是docker build子命令将被调用的目录中的文件。然而,值得注意的区别是,源可以是存储在构建上下文中的 TAR 文件,也可以是远程 URL。

  • ...:这表示多个源文件可以直接指定,也可以使用通配符指定。

  • <dst>:这是新镜像的目标路径,源文件或目录将被复制到其中。

这是一个示例,演示了将多个源文件复制到目标镜像文件系统中的各个目标目录的过程。在此示例中,我们在源构建上下文中使用了一个 TAR 文件(web-page-config.tar),其中包含http守护程序配置文件和网页文件的目录结构,如下所示:

$ tar tf web-page-config.tar
etc/httpd/conf/httpd.conf
var/www/html/index.html
var/www/html/aboutus.html
var/www/html/images/welcome.gif
var/www/html/images/banner.gif

Dockerfile内容中的下一行包含一个ADD指令,用于将 TAR 文件(web-page-config.tar)复制到目标镜像,并从目标镜像的根目录(/)中提取 TAR 文件,如下所示:

ADD web-page-config.tar /

因此,ADD指令的 TAR 选项可用于将多个文件复制到目标镜像。

ENV 指令

ENV指令在新镜像中设置环境变量。环境变量是键值对,可以被任何脚本或应用程序访问。Linux 应用程序在启动配置中经常使用环境变量。

以下一行形成了ENV指令的语法:

ENV <key> <value>

在这里,代码术语表示以下内容:

  • <key>:这是环境变量

  • <value>:这是要设置为环境变量的值

以下几行给出了ENV指令的两个示例,在第一行中,DEBUG_LVL已设置为3,在第二行中,APACHE_LOG_DIR已设置为/var/log/apache

ENV DEBUG_LVL 3
ENV APACHE_LOG_DIR /var/log/apache

USER 指令

USER指令设置新镜像中的启动用户 ID 或用户名。默认情况下,容器将以root作为用户 ID 或UID启动。实质上,USER指令将把默认用户 ID 从root修改为此指令中指定的用户 ID。

USER指令的语法如下:

USER <UID>|<UName>

USER指令接受<UID><UName>作为其参数:

  • <UID>:这是一个数字用户 ID

  • <UName>:这是一个有效的用户名

以下是一个示例,用于在启动时将默认用户 ID 设置为73。这里73是用户的数字 ID:

USER 73

但是,建议您拥有一个与/etc/passwd文件匹配的有效用户 ID,用户 ID 可以包含任意随机数值。但是,用户名必须与/etc/passwd文件中的有效用户名匹配,否则docker run子命令将失败,并显示以下错误消息:

finalize namespace setup user get supplementary groups Unable to find user

WORKDIR 指令

WORKDIR指令将当前工作目录从/更改为此指令指定的路径。随后的指令,如RUNCMDENTRYPOINT也将在WORKDIR指令设置的目录上工作。

以下一行提供了WORKDIR指令的适当语法:

WORKDIR <dirpath>

在这里,<dirpath>是要设置的工作目录的路径。路径可以是绝对路径或相对路径。在相对路径的情况下,它将相对于WORKDIR指令设置的上一个路径。如果在目标镜像文件系统中找不到指定的目录,则将创建该目录。

以下一行是DockerfileWORKDIR指令的一个明确示例:

WORKDIR /var/log

VOLUME 指令

VOLUME指令在镜像文件系统中创建一个目录,以后可以用于从 Docker 主机或其他容器挂载卷。

VOLUME指令有两种语法,如下所示:

  • 第一种类型是 exec 或 JSON 数组(所有值必须在双引号(")内):
VOLUME ["<mountpoint>"]
  • 第二种类型是 shell,如下所示:
VOLUME <mountpoint>

在前一行中,<mountpoint>是必须在新镜像中创建的挂载点。

EXPOSE 指令

EXPOSE指令打开容器网络端口,用于容器与外部世界之间的通信。

EXPOSE指令的语法如下:

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

在这里,代码术语的含义如下:

  • <port>:这是要向外部世界暴露的网络端口。

  • <proto>:这是一个可选字段,用于指定特定的传输协议,如 TCP 和 UDP。如果未指定传输协议,则假定 TCP 为传输协议。

EXPOSE指令允许您在一行中指定多个端口。

以下是DockerfileEXPOSE指令的示例,将端口号7373暴露为UDP端口,端口号8080暴露为TCP端口。如前所述,如果未指定传输协议,则假定TCP传输协议为传输协议:

EXPOSE 7373/udp 8080

RUN 指令

RUN指令是构建时的真正工作马,它可以运行任何命令。一般建议使用一个RUN指令执行多个命令。这样可以减少生成的 Docker 镜像中的层,因为 Docker 系统固有地为Dockerfile中每次调用指令创建一个层。

RUN指令有两种语法类型:

  • 第一种是 shell 类型,如下所示:
RUN <command>

在这里,<command>是在构建时必须执行的 shell 命令。如果要使用这种类型的语法,那么命令总是使用/bin/sh -c来执行。

  • 第二种语法类型要么是 exec,要么是 JSON 数组,如下所示:
RUN ["<exec>", "<arg-1>", ..., "<arg-n>"]

在其中,代码术语的含义如下:

  • <exec>:这是在构建时要运行的可执行文件。

  • <arg-1>, ..., <arg-n>:这些是可执行文件的参数(零个或多个)。

与第一种语法不同,这种类型不会调用/bin/sh -c。因此,这种类型不会发生 shell 处理,如变量替换($USER)和通配符替换(*?)。如果 shell 处理对您很重要,那么建议您使用 shell 类型。但是,如果您仍然更喜欢 exec(JSON 数组类型),那么请使用您喜欢的 shell 作为可执行文件,并将命令作为参数提供。

例如,RUN ["bash", "-c", "rm", "-rf", "/tmp/abc"]

现在让我们看一下RUN指令的一些示例。在第一个示例中,我们将使用RUN指令将问候语添加到目标图像文件系统的.bashrc文件中,如下所示:

RUN echo "echo Welcome to Docker!" >> /root/.bashrc

第二个示例是一个Dockerfile,其中包含在Ubuntu 14.04基础镜像上构建Apache2应用程序镜像的指令。接下来的步骤将逐行解释Dockerfile指令:

  1. 我们将使用FROM指令构建一个以ubuntu:14.04为基础镜像的镜像,如下所示:
###########################################
# Dockerfile to build an Apache2 image
###########################################
# Base image is Ubuntu
FROM ubuntu:14.04
  1. 通过使用MAINTAINER指令设置作者的详细信息,如下所示:
# Author: Dr. Peter
MAINTAINER Dr. Peter <peterindia@gmail.com>
  1. 通过一个RUN指令,我们将同步apt存储库源列表,安装apache2软件包,然后清理检索到的文件,如下所示:
# Install apache2 package
RUN apt-get update && \
   apt-get install -y apache2 && \
   apt-get clean

CMD 指令

CMD指令可以运行任何命令(或应用程序),类似于RUN指令。但是,这两者之间的主要区别在于执行时间。通过RUN指令提供的命令在构建时执行,而通过CMD指令指定的命令在从新创建的镜像启动容器时执行。因此,CMD指令为此容器提供了默认执行。但是,可以通过docker run子命令参数进行覆盖。应用程序终止时,容器也将终止,并且应用程序与之相反。

CMD指令有三种语法类型,如下所示:

  • 第一种语法类型是 shell 类型,如下所示:
CMD <command>

在其中,<command>是 shell 命令,在容器启动时必须执行。如果使用此类型的语法,则始终使用/bin/sh -c执行命令。

  • 第二种语法类型是 exec 或 JSON 数组,如下所示:
CMD ["<exec>", "<arg-1>", ..., "<arg-n>"]

在其中,代码术语的含义如下:

  • <exec>:这是要在容器启动时运行的可执行文件。

  • <arg-1>, ..., <arg-n>:这些是可执行文件的参数的变量(零个或多个)数字。

  • 第三种语法类型也是 exec 或 JSON 数组,类似于前一种类型。但是,此类型用于将默认参数设置为ENTRYPOINT指令,如下所示:

CMD ["<arg-1>", ..., "<arg-n>"]

在其中,代码术语的含义如下:

  • <arg-1>, ..., <arg-n>:这些是ENTRYPOINT指令的变量(零个或多个)数量的参数,将在下一节中解释。

从语法上讲,你可以在Dockerfile中添加多个CMD指令。然而,构建系统会忽略除最后一个之外的所有CMD指令。换句话说,在多个CMD指令的情况下,只有最后一个CMD指令会生效。

在这个例子中,让我们使用DockerfileCMD指令来制作一个镜像,以提供默认执行,然后使用制作的镜像启动一个容器。以下是带有CMD指令的Dockerfile,用于echo一段文本:

########################################################
# Dockerfile to demonstrate the behaviour of CMD
########################################################
# Build from base image busybox:latest
FROM busybox:latest
# Author: Dr. Peter
MAINTAINER Dr. Peter <peterindia@gmail.com>
# Set command for CMD
CMD ["echo", "Dockerfile CMD demo"]

现在,让我们使用docker build子命令和cmd-demo作为镜像名称来构建一个 Docker 镜像。docker build系统将从当前目录(.)中读取Dockerfile中的指令,并相应地制作镜像,就像这里所示的那样:

$ sudo docker build -t cmd-demo .

构建了镜像之后,我们可以使用docker run子命令来启动容器,就像这里所示的那样:

$ sudo docker run cmd-demo
Dockerfile CMD demo

很酷,不是吗?我们为容器提供了默认执行,并且我们的容器忠实地回显了Dockerfile CMD demo。然而,这个默认执行可以很容易地被通过将另一个命令作为参数传递给docker run子命令来覆盖,就像下面的例子中所示:

$ sudo docker run cmd-demo echo Override CMD demo
Override CMD demo

ENTRYPOINT 指令

ENTRYPOINT指令将帮助制作一个镜像,用于在容器的整个生命周期中运行一个应用程序(入口点),该应用程序将从镜像中衍生出来。当入口点应用程序终止时,容器也将随之终止,应用程序与容器相互关联。因此,ENTRYPOINT指令会使容器的功能类似于可执行文件。从功能上讲,ENTRYPOINT类似于CMD指令,但两者之间的主要区别在于入口点应用程序是通过ENTRYPOINT指令启动的,无法通过docker run子命令参数来覆盖。然而,这些docker run子命令参数将作为额外的参数传递给入口点应用程序。话虽如此,Docker 提供了通过docker run子命令中的--entrypoint选项来覆盖入口点应用程序的机制。--entrypoint选项只能接受一个单词作为其参数,因此其功能有限。

从语法上讲,ENTRYPOINT指令与RUNCMD指令非常相似,它有两种语法,如下所示:

  • 第一种语法是 shell 类型,如下所示:
ENTRYPOINT <command>

在这里,<command>是在容器启动时执行的 shell 命令。如果使用这种类型的语法,则始终使用/bin/sh -c执行命令。

  • 第二种语法是 exec 或 JSON 数组,如下所示:
ENTRYPOINT ["<exec>", "<arg-1>", ..., "<arg-n>"]

在这里,代码术语的含义如下:

  • <exec>:这是在容器启动时必须运行的可执行文件。

  • <arg-1>, ..., <arg-n>:这些是可执行文件的变量(零个或多个)参数。

从语法上讲,你可以在Dockerfile中有多个ENTRYPOINT指令。然而,构建系统将忽略除最后一个之外的所有ENTRYPOINT指令。换句话说,在多个ENTRYPOINT指令的情况下,只有最后一个ENTRYPOINT指令会生效。

为了更好地理解ENTRYPOINT指令,让我们使用带有ENTRYPOINT指令的Dockerfile来创建一个镜像,然后使用这个镜像启动一个容器。以下是带有ENTRYPOINT指令的Dockerfile,用于回显文本:

########################################################
# Dockerfile to demonstrate the behaviour of ENTRYPOINT
########################################################
# Build from base image busybox:latest
FROM busybox:latest
# Author: Dr. Peter
MAINTAINER Dr. Peter <peterindia@gmail.com>
# Set entrypoint command
ENTRYPOINT ["echo", "Dockerfile ENTRYPOINT demo"]

现在,让我们使用docker build作为子命令和entrypoint-demo作为镜像名称来构建一个 Docker 镜像。docker build系统将从当前目录(.)中存储的Dockerfile中读取指令,并创建镜像,如下所示:

$ sudo docker build -t entrypoint-demo .

构建完镜像后,我们可以使用docker run子命令启动容器:

$ sudo docker run entrypoint-demo
Dockerfile ENTRYPOINT demo

在这里,容器将像可执行文件一样运行,回显Dockerfile ENTRYPOINT demo字符串,然后立即退出。如果我们向docker run子命令传递任何额外的参数,那么额外的参数将传递给入口点命令。以下是使用docker run子命令给出额外参数启动相同镜像的演示:

$ sudo docker run entrypoint-demo with additional arguments
Dockerfile ENTRYPOINT demo with additional arguments

现在,让我们看一个例子,我们可以使用--entrypoint选项覆盖构建时的入口应用程序,然后在docker run子命令中启动一个 shell(/bin/sh),如下所示:

$ sudo docker run --entrypoint="/bin/sh" entrypoint-demo
/ #

ONBUILD 指令

ONBUILD指令将构建指令注册到镜像中,并在使用此镜像作为其基本镜像构建另一个镜像时触发。任何构建指令都可以注册为触发器,并且这些指令将在下游Dockerfile中的FROM指令之后立即触发。因此,ONBUILD指令可用于将构建指令的执行从基本镜像延迟到目标镜像。

ONBUILD指令的语法如下:

ONBUILD <INSTRUCTION>

在其中,<INSTRUCTION>是另一个Dockerfile构建指令,稍后将被触发。ONBUILD指令不允许链接另一个ONBUILD指令。此外,它不允许FROMMAINTAINER指令作为ONBUILD触发器。

以下是ONBUILD指令的示例:

ONBUILD ADD config /etc/appconfig

.dockerignore 文件

Docker 集成的镜像构建系统部分,我们了解到docker build过程将完整的构建上下文发送到守护程序。在实际环境中,docker build上下文将包含许多其他工作文件和目录,这些文件和目录永远不会构建到镜像中。然而,docker build系统仍然会将这些文件发送到守护程序。因此,您可能想知道如何通过不将这些工作文件发送到守护程序来优化构建过程。嗯,Docker 背后的人也考虑过这个问题,并提供了一个非常简单的解决方案:使用.dockerignore文件。

.dockerignore是一个以换行分隔的文本文件,在其中您可以提供要从构建过程中排除的文件和目录。文件中的排除列表可以包含完全指定的文件或目录名称和通配符。

以下片段是一个示例.dockerignore文件,通过它,构建系统已被指示排除.git目录和所有具有.tmp扩展名的文件:

.git
*.tmp

Docker 镜像管理的简要概述

正如我们在前一章和本章中所看到的,有许多方法可以控制 Docker 镜像。您可以使用docker pull子命令从公共存储库下载完全设置好的应用程序堆栈。否则,您可以通过使用docker commit子命令手动或使用Dockerfiledocker build子命令组合自动创建自己的应用程序堆栈。

Docker 镜像被定位为容器化应用程序的关键构建模块,从而实现了部署在云服务器上的分布式应用程序。Docker 镜像是分层构建的,也就是说,可以在其他镜像的基础上构建镜像。原始镜像称为父镜像,生成的镜像称为子镜像。基础镜像是一个捆绑包,包括应用程序的常见依赖项。对原始镜像所做的每个更改都将作为单独的层存储。每次提交到 Docker 镜像时,都会在 Docker 镜像上创建一个新的层,对原始镜像所做的每个更改都将作为单独的层存储。由于层的可重用性得到了便利,制作新的 Docker 镜像变得简单而快速。您可以通过更改Dockerfile中的一行来创建新的 Docker 镜像,而无需重新构建整个堆栈。

现在我们已经了解了 Docker 镜像中的层次结构,您可能想知道如何在 Docker 镜像中可视化这些层。好吧,docker history子命令是可视化图像层的一个非常好用的工具。

让我们看一个实际的例子,以更好地理解 Docker 镜像中的分层。为此,让我们按照以下三个步骤进行:

  1. 在这里,我们有一个Dockerfile,其中包含自动构建 Apache2 应用程序镜像的指令,该镜像是基于 Ubuntu 14.04 基础镜像构建的。本章之前制作和使用的Dockerfile中的RUN部分将在本节中被重用,如下所示:
###########################################
# Dockerfile to build an Apache2 image
###########################################
# Base image is Ubuntu
FROM ubuntu:14.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
  1. 现在,通过使用docker build子命令从上述Dockerfile中制作一个镜像,如下所示:
$ sudo docker build -t apache2 .

  1. 最后,让我们使用docker history子命令来可视化 Docker 镜像中的层次结构:
$ sudo docker history apache2

  1. 这将生成关于apache2 Docker 镜像的每个层的详细报告,如下所示:
IMAGE          CREATED       CREATED BY                   SIZE
aa83b67feeba    2 minutes ago    /bin/sh -c apt-get update &&   apt-get inst  35.19 MB
c7877665c770    3 minutes ago    /bin/sh -c #(nop) MAINTAINER Dr. Peter <peter  0 B
9cbaf023786c    6 days ago     /bin/sh -c #(nop) CMD [/bin/bash]        0 B
03db2b23cf03    6 days ago     /bin/sh -c apt-get update && apt-get dist-upg  0 B
8f321fc43180    6 days ago     /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/  1.895 kB
6a459d727ebb    6 days ago     /bin/sh -c rm -rf /var/lib/apt/lists/*     0 B
2dcbbf65536c    6 days ago     /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic 194.5 kB
97fd97495e49    6 days ago     /bin/sh -c #(nop) ADD file:84c5e0e741a0235ef8  192.6 MB
511136ea3c5a    16 months ago                            0 B

在这里,apache2镜像由十个镜像层组成。顶部两层,具有图像 IDaa83b67feebac7877665c770的层,是我们DockerfileRUNMAINTAINER指令的结果。图像的其余八层将通过我们Dockerfile中的FROM指令从存储库中提取。

编写 Dockerfile 的最佳实践

毫无疑问,一套最佳实践总是在提升任何新技术中起着不可或缺的作用。有一份详细列出所有最佳实践的文件,用于编写Dockerfile。我们发现它令人难以置信,因此,我们希望分享给您以供您受益。您可以在docs.docker.com/articles/dockerfile_best-practices/找到它。

摘要

构建 Docker 镜像是 Docker 技术的关键方面,用于简化容器化的繁琐任务。正如之前所指出的,Docker 倡议已经成为颠覆性和变革性的容器化范式。Dockerfile 是生成高效 Docker 镜像的最主要方式,可以被精心使用。我们已经阐明了所有命令、它们的语法和使用技巧,以赋予您所有易于理解的细节,这将简化您的镜像构建过程。我们提供了大量示例,以证实每个命令的内在含义。在下一章中,我们将讨论 Docker Hub,这是一个专门用于存储和共享 Docker 镜像的存储库,并且我们还将讨论它对容器化概念在 IT 企业中的深远贡献。

第四章:发布图像

在上一章中,我们学习了如何构建 Docker 镜像。下一个逻辑步骤是将这些镜像发布到公共存储库以供公众发现和使用。因此,本章重点介绍了在 Docker Hub 上发布图像以及如何充分利用 Docker Hub。我们可以使用commit命令和Dockerfile创建一个新的 Docker 镜像,对其进行构建,并将其推送到 Docker Hub。将讨论受信任存储库的概念。这个受信任的存储库是从 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 Registry 是用于存储图像的存储系统。自动构建是 Docker Hub 的一个功能,在撰写本书时尚未开源。以下图表显示了典型的功能:

理解 Docker Hub

要使用 Docker Hub,您必须在 Docker Hub 上注册,并使用以下链接创建帐户:hub.docker.com/account/signup。您可以更新用户名密码电子邮件地址,如下面的屏幕截图所示:

理解 Docker Hub

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

理解 Docker Hub

Docker Hub 中的帐户创建已成功完成,现在您可以使用hub.docker.com/account/login/?next=/account/welcome/登录到您的 Docker Hub 帐户,如下截图所示:

理解 Docker Hub

Docker Hub 还支持使用 Ubuntu 终端对 Docker Hub 进行命令行访问:

ubuntu@ip-172-31-21-44:~$ sudo docker login
Username: vinoddandy
Password:
Email: vinoddandy@gmail.com

成功登录后,输出如下:

Login Succeeded

您可以浏览 Docker Hub 中的可用图像,如下所示:

理解 Docker Hub

此外,您可以查看您的设置,更新您的个人资料,并获取支持的社区的详细信息,如 Twitter、stackoverflow、#IRC、Google Groups 和 GitHub。

将图像推送到 Docker Hub

在这里,我们将在本地机器上创建一个 Docker 图像,并将此图像推送到 Docker Hub。您需要在本节中执行以下步骤:

  1. 通过以下方式在本地机器上创建 Docker 图像之一:
  • 使用docker commit子命令

  • 使用Dockerfiledocker commit子命令

  1. 将此创建的图像推送到 Docker Hub。

  2. 从 Docker Hub 中删除图像。

我们将使用 Ubuntu 基础图像,运行容器,添加一个新目录和一个新文件,然后创建一个新图像。在第三章,构建图像中,我们已经看到了使用Dockerfile创建 Docker 图像。您可以参考这个来检查Dockerfile语法的细节。

我们将从基本的ubuntu图像中使用名称为containerforhub的容器运行容器,如下终端代码所示:

$ sudo docker run -i --name="containerforhub" -t ubuntu /bin/bash
root@e3bb4b138daf:/#

接下来,我们将在containerforhub容器中创建一个新目录和文件。我们还将更新新文件,以便稍后进行测试:

root@bd7cc5df6d96:/# mkdir mynewdir
root@bd7cc5df6d96:/# cd mynewdir
root@bd7cc5df6d96:/mynewdir# echo 'this is my new container to make image and then push to hub' >mynewfile
root@bd7cc5df6d96:/mynewdir# cat mynewfile
This is my new container to make image and then push to hub
root@bd7cc5df6d96:/mynewdir#

让我们使用刚刚创建的容器的docker commit命令构建新图像。请注意,commit命令将从主机机器上执行,从容器正在运行的位置执行,而不是从容器内部执行:

$ sudo docker commit -m="NewImage" containerforhub vinoddandy/imageforhub
3f10a35019234af2b39d5fab38566d586f00b565b99854544c4c698c4a395d03

现在,我们在本地机器上有一个名为vinoddandy/imageforhub的新 Docker 图像。此时,本地创建了一个带有mynewdirmynewfile的新图像。

我们将使用sudo docker login命令登录到 Docker Hub,就像本章前面讨论的那样。

让我们从主机机器将此图像推送到 Docker Hub:

$ sudo docker push vinoddandy/imageforhub
The push refers to a repository [vinoddandy/imageforhub] (len: 1)
Sending image list
Pushing tag for rev [c664d94bbc55] on {https://cdn-registry-1.docker.io/v1/repositories/vinoddandy/imageforhub/tags/latest}

现在,我们将登录到 Docker Hub 并在存储库中验证图像。

为了测试来自 Docker Hub 的图像,让我们从本地机器中删除此图像。要删除图像,首先需要停止容器,然后删除容器:

$ sudo docker stop containerforhub
$ sudo docker rm containerforhub
$

我们还将删除vinoddandy/imageforhub图像:

$ sudo docker rmi vinoddandy/imageforhub

我们将从 Docker Hub 中拉取新创建的图像,并在本地机器上运行新容器:

$ sudo docker run -i --name="newcontainerforhub" -t vinoddandy/imageforhub /bin/bash
Unable to find image 'vinoddandy/imageforhub' locally
Pulling repository vinoddandy/imageforhub
c664d94bbc55: Pulling image (latest) from vinoddandy/imageforhub, endpoint: http
c664d94bbc55: Download complete
5506de2b643b: Download complete
root@9bd40f1b5585:/# cat /mynewdir/mynewfile
This is my new container to make image and then push to hub
root@9bd40f1b5585:/#

因此,我们已经从 Docker Hub 中拉取了最新的图像,并使用新图像vinoddandy/imageforhub创建了容器。请注意,无法在本地找到图像'vinoddandy/imageforhub'的消息证实了该图像是从 Docker Hub 的远程存储库中下载的。

mynewfile中的文字证实了它是之前创建的相同图像。

最后,我们将从 Docker Hub 中删除图像,使用registry.hub.docker.com/u/vinoddandy/imageforhub/,然后点击删除存储库,如下面的截图所示:

将图像推送到 Docker Hub

我们将再次创建此图像,但使用Dockerfile过程。因此,让我们使用第三章中解释的Dockerfile概念创建 Docker 图像,并将此图像推送到 Docker Hub。

本地机器上的Dockerfile如下所示:

###########################################
# Dockerfile to build a new image
###########################################
# Base image is Ubuntu
FROM ubuntu:14.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/dockerfileimageforhub" .
Sending build context to Docker daemon  2.56 kB
Sending build context to Docker daemon
Step 0 : FROM ubuntu:14.04
---> 5506de2b643b
Step 1 : MAINTAINER Vinod Singh <vinod.puchi@gmail.com>
---> Running in 9f6859e2ca75
---> a96cfbf4a810
removing intermediate container 9f6859e2ca75
Step 2 : RUN mkdir mynewdir
---> Running in d4eba2a31467
---> 14f4c15610a7
removing intermediate container d4eba2a31467
Step 3 : RUN touch /mynewdir/mynewfile
---> Running in 7d810a384819
---> b5bbd55f221c
removing intermediate container 7d810a384819
Step 4 : RUN echo 'this is my new container to make image and then push to hub'
/mynewdir/mynewfile
---> Running in b7b48447e7b3
---> bcd8f63cfa79
removing intermediate container b7b48447e7b3
successfully built 224affbf9a65
ubuntu@ip-172-31-21-44:~/dockerfile_image_hub$

我们将使用此图像运行容器,如下所示:

$ sudo docker run -i --name="dockerfilecontainerforhub" –t vinoddandy/dockerfileimageforhub
root@d3130f21a408:/# cat /mynewdir/mynewfile
this is my new container to make image and then push to hub

mynewdir中的这段文字证实了新图像是通过新目录和新文件正确构建的。

重复登录过程,在 Docker Hub 中,然后推送这个新创建的镜像:

$ sudo docker login
Username (vinoddandy):
Login Succeeded
$ sudo docker push vinoddandy/dockerfileimageforhub
The push refers to a repository [vinoddandy/dockerfileimageforhub] (len: 1)
Sending image list
Pushing repository vinoddandy/dockerfileimageforhub (1 tags)
511136ea3c5a: Image already pushed, skipping
d497ad3926c8: Image already pushed, skipping
b5bbd55f221c: Image successfully pushed
bcd8f63cfa79: Image successfully pushed
224affbf9a65: Image successfully pushed
Pushing tag for rev [224affbf9a65] on {https://cdn-registry-1.docker.io/v1/repos
itories/vinoddandy/dockerfileimageforhub/tags/latest}
$

最后,我们可以验证 Docker Hub 上图像的可用性:

将图像推送到 Docker Hub

自动化图像构建过程

我们学会了如何在本地构建图像并将这些图像推送到 Docker Hub。Docker Hub 还具有从存储在 GitHub 或 Bitbucket 仓库中的Dockerfile自动构建图像的功能。自动构建支持 GitHub 和 Bitbucket 的私有和公共仓库。Docker Hub Registry 保存所有自动构建图像。Docker Hub Registry 基于开源,并且可以从github.com/docker/docker-registry访问。

我们将讨论实施自动构建过程所需的步骤:

  1. 我们首先将 Docker Hub 连接到我的 GitHub 帐户。

登录到 Docker Hub,并点击查看个人资料,然后转到添加仓库 | 自动构建,如下面的截图所示:

自动化构建图像的过程

  1. 现在我们选择GitHub自动化构建图像的过程

  2. 选择GitHub后,它将要求授权。在这里,我们将选择公共和私有,如下所示:自动化构建图像的过程

  3. 点击选择后,它现在会显示您的 GitHub 仓库:自动化构建图像的过程

  4. 点击您的仓库vinodsinghh/dockerautomationbuild选择按钮,如前面的截图所示:

  5. 我们选择默认分支,并使用Githubimage更新标签。此外,我们将保持位置为其默认值,即我们的 Docker Hub 的根目录,如下面的截图所示:自动化构建图像的过程

  6. 最后,我们将点击创建仓库,如前面的截图所示:自动化构建图像的过程

  7. 点击构建详情以跟踪构建状态,如前面的截图所示。它将引导您到下面的截图:自动化构建图像的过程

因此,每当 GitHub 中的Dockerfile更新时,自动构建就会被触发,并且新的镜像将存储在 Docker Hub 注册表中。我们可以随时检查构建历史记录。我们可以在本地机器上更改Dockerfile并推送到 GitHub。然后,我们可以在 Docker Hub 上看到自动构建链接registry.hub.docker.com/u/vinoddandy/dockerautomatedbuild/builds_history/82194/

Docker Hub 上的私有仓库

Docker Hub 提供公共和私有仓库。公共仓库对用户免费,私有仓库是付费服务。私有仓库的计划有不同的大小,如微型、小型、中型或大型订阅。

Docker 已经将他们的公共仓库代码发布为开源,网址是github.com/docker/docker-registry

通常,企业不喜欢将他们的 Docker 镜像存储在 Docker 的公共或私有仓库中。他们更喜欢保留、维护和支持自己的仓库。因此,Docker 也为企业提供了创建和安装自己的仓库的选项。

让我们使用 Docker 提供的注册表镜像在本地机器上创建一个仓库。我们将在本地机器上运行注册表容器,使用来自 Docker 的注册表镜像:

$ sudo docker run -p 5000:5000 -d registry
768fb5bcbe3a5a774f4996f0758151b1e9917dec21aedf386c5742d44beafa41

在自动构建部分,我们构建了vinoddandy/dockerfileforhub镜像。让我们将镜像 ID 224affbf9a65标记到我们本地创建的registry镜像上。这个镜像的标记是为了在本地仓库中进行唯一标识。这个registry镜像可能在仓库中有多个变体,所以这个tag将帮助您识别特定的镜像:

$ sudo docker tag 224affbf9a65localhost:5000/vinoddandy/dockerfileimageforhub

标记完成后,使用docker push命令将此镜像推送到新的注册表:

$ sudo docker push localhost:5000/vinoddandy/dockerfile
imageforhub
The push refers to a repository [localhost:5000/vinoddandy/dockerfileimageforhub
] (len: 1)
Sending image list
Pushing repository localhost:5000/vinoddandy/dockerfileimageforhub (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/vin
oddandy/dockerfileimageforhub/tags/latest}
ubuntu@ip-172-31-21-44:~$

现在,新的镜像已经在本地仓库中可用。您现在可以从本地注册表中检索此镜像并运行容器。这个任务留给你来完成。

Docker Hub 上的组织和团队

私有仓库的一个有用方面是,您可以只与组织或团队成员共享它们。Docker Hub 允许您创建组织,在那里您可以与同事合作并管理私有仓库。您可以学习如何创建和管理组织。

第一步是在 Docker Hub 上创建一个组织,如下截图所示:

Docker Hub 上的组织和团队

在您的组织中,您可以添加更多的组织,然后向其中添加成员:

Docker Hub 上的组织和团队

您的组织和团队成员可以与组织和团队合作。在私人存储库的情况下,此功能将更加有用。

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/(usename)/

存储库管理支持以下功能:

  • 创建用户存储库:这将创建一个用户存储库:
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

创建存储库后,您的存储库将在此处列出,如此屏幕截图所示:

Docker Hub 的 REST API

  • 删除用户存储库:这将删除用户存储库:
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 注册表和索引

通常,Docker Hub 由 Docker 索引和注册表组成。 Docker 客户端可以通过网络连接和与 Docker Hub 交互。注册表具有以下特征:

  • 它存储一组存储库的图像和图形

  • 它没有用户帐户数据

  • 它没有用户帐户或授权的概念

  • 它将认证和授权委托给 Docker Hub 认证服务

  • 它支持不同的存储后端(S3、云文件、本地文件系统等)

  • 它没有本地数据库

  • 它有与之关联的源代码

Docker 注册表的高级功能包括bugsnagnew reliccorsbugsnag功能可检测和诊断应用程序中的崩溃,new relic封装了注册表并监视性能,cors可以启用以在我们自己的注册表域之外共享资源。建议您使用代理(如 nginx)将注册表部署到生产环境。您还可以直接在 Ubuntu 和基于 Red Hat Linux 的系统上运行 Docker 注册表。

目前,负责开发 Docker 平台的公司已在 GitHub 上发布了 Docker 注册表作为开源服务github.com/docker/docker-registry。值得注意的是,Docker 索引只是一个建议,在撰写本书时,Docker 尚未发布任何开源项目。在本章中,我们将从 Docker 注册表的用例开始,然后从 GitHub 开始实际部署索引元素和 Docker 注册表。

Docker 注册表用例

以下是 Docker 注册表的用例:

  1. 拉取或下载图像

  2. 推送图像

  3. 删除图像

现在我们将详细介绍每个用例:

  1. 拉取或下载图像:用户使用 Docker 客户端从索引请求图像,索引反过来向用户返回注册表详细信息。然后,Docker 客户端将直接请求注册表以获取所需的图像。注册表在内部使用索引对用户进行身份验证。如下图所示,图像拉取是通过客户端、索引和注册表模块的协作完成的:Docker 注册表用例

  2. 推送图像:用户请求推送图像,从索引获取注册表信息,然后直接将图像推送到注册表。注册表使用索引对用户进行身份验证,最后回应用户。控制流程如下图所示:Docker 注册表用例

  3. 删除图像:用户还可以请求从存储库中删除图像。

用户可以选择使用带有或不带有索引的注册表。在不带有索引的情况下使用注册表最适合存储私人图像。

运行自己的索引和注册表

在本节中,我们将执行以下步骤来运行自己的索引和注册表,并最终推送图像:

  1. 从 GitHub 部署索引组件和注册表。

  2. 配置 nginx 与 Docker 注册表。

  3. 在 Web 服务器上设置 SSL 以进行安全通信。

第 1 步-从 GitHub 部署索引组件和注册表

索引组件包括apache-utilsngnix,用于密码验证和 HTTPS 支持的 SSL 功能。用户必须注意,Docker 注册表的当前版本仅支持使用 HTTP 连接到注册表。因此,用户必须部署和使用安全套接字层SSL)来保护数据。 SSL 在 Web 服务器和客户端的 Web 浏览器之间创建了加密连接,允许私人数据在没有窃听、数据篡改或消息伪造问题的情况下传输。这是使用广泛接受的 SSL 证书来保护数据的一种经过验证的方法。

Docker 注册表是一个 Python 应用程序,我们可以使用以下命令从github.com/docker/docker-registry在本地 Ubuntu 机器上安装 Python:

$ sudo apt-get -y install build-essential python-dev \
 libevent-dev python-pip liblzma-dev swig libssl-dev

现在,安装 Docker 注册表:

$ sudo pip install docker-registry

这将更新 Python 软件包中的 Docker 注册表,并更新以下路径中的配置文件:

$ cd /usr/local/lib/python2.7/dist-packages/config/

config_sample.yml文件复制到config.yml

$ sudo cp config_sample.yml config.yml

默认情况下,Docker 将其数据保存在/tmp目录中,这可能会导致问题,因为在许多 Linux 系统上,/tmp文件夹在重新启动时会被清除。让我们创建一个永久文件夹来存储我们的数据:

$ sudo mkdir /var/docker-registry

让我们更新我们之前的config.yml文件,以适应以下两个位置的更新路径。第一个位置的更新代码如下:

sqlalchemy_index_database:
    _env:SQLALCHEMY_INDEX_DATABASE:sqlite:////var/docker-registry/docker-registry.db

以下是第二个位置的代码:

local: &local
    storage: local
    storage_path: _env:STORAGE_PATH:/var/docker-registry/registry

config.yml文件的其他默认配置正常工作。

现在,让我们使用gunicorn启动 Docker 注册表。 Gunicorn,也称为 Green Unicorn,是 Linux 系统的 Python Web 服务器网关接口WSGI)HTTP 服务器:

$ sudo gunicorn --access-logfile - --debug -k gevent -b \
 0.0.0.0:5000 -w 1 docker_registry.wsgi:application
01/Dec/2014:04:59:23 +0000 WARNING: Cache storage disabled!
01/Dec/2014:04:59:23 +0000 WARNING: LRU cache disabled!
01/Dec/2014:04:59:23 +0000 DEBUG: Will return docker-registry.drivers.file.Storage

现在,Docker 注册表作为用户本地机器上的一个进程正在运行。

我们可以使用Ctrl + C来停止这个进程。

我们可以按以下方式启动 Linux 服务:

  1. docker-registry工具创建一个目录:
$ sudo mkdir -p /var/log/docker-registry

  1. 创建并更新 Docker 注册表配置文件:
$ sudo vi /etc/init/docker-registry.conf

  1. 更新文件中的以下内容:
description "Docker Registry"
start on runlevel [2345]
stop on runlevel [016]
respawn
respawn limit 10 5
script
exec gunicorn --access-logfile /var/log/docker-registry/access.log --error-logfile /var/log/docker-registry/server.log -k gevent --max-requests 100 --graceful-timeout 3600 -t 3600 -b localhost:5000 -w 8 docker_registry.wsgi:application
end script
  1. 保存文件后,运行 Docker 注册表服务:
$ sudo service docker-registry start
docker-registry start/running, process 25760

  1. 现在,使用apache-utils来保护此注册表,启用密码保护功能,如下所示:
$ sudo apt-get -y install nginx apache2-utils

  1. 用户创建登录 ID 和密码来访问 Docker 注册表:
$ sudo htpasswd -c /etc/nginx/docker-registry.htpasswd vinod1

  1. 在提示时输入新密码。此时,我们有登录 ID 和密码来访问 Docker 注册表。

第 2 步 - 配置 nginx 与 Docker 注册表

接下来,我们需要告诉 nginx 使用该认证文件(在上一节的第 6 步和第 7 步中创建)来转发请求到我们的 Docker 注册表。

我们需要创建 nginx 配置文件。为此,我们需要按照以下步骤进行:

  1. 通过运行以下命令创建 ngnix 配置文件:
$ sudo vi /etc/nginx/sites-available/docker-registry

使用以下内容更新文件:

upstream docker-registry {
 server localhost:5000;
}
server {
 listen 8080;
 server_name my.docker.registry.com;
 # ssl on;
 # ssl_certificate /etc/ssl/certs/docker-registry;
 # ssl_certificate_key /etc/ssl/private/docker-registry;
 proxy_set_header Host       $http_host;   # required for Docker client sake
 proxy_set_header X-Real-IP  $remote_addr; # pass on real client IP
 client_max_body_size 0; # disable any limits to avoid HTTP 413 for large image uploads
 # required to avoid HTTP 411: see Issue #1486 (https://github.com/dotcloud/docker/issues/1486)
 chunked_transfer_encoding on;
 location / {
     # let Nginx know about our auth file
     auth_basic              "Restricted";
     auth_basic_user_file    docker-registry.htpasswd;
     proxy_pass http://docker-registry;
 } location /_ping {
     auth_basic off;
     proxy_pass http://docker-registry;
 }   location /v1/_ping {
     auth_basic off;
     proxy_pass http://docker-registry;
 }
}
  1. 创建软链接并重新启动 ngnix 服务:
$ sudo ln -s /etc/nginx/sites-available/docker-registry  \
 /etc/nginx/sites-enabled/docker-registry
$ sudo service nginx restart

  1. 让我们检查一切是否正常工作。运行以下命令,我们应该得到这个输出:
$ sudo curl localhost:5000
"\"docker-registry server\""

太好了!现在我们的 Docker 注册表正在运行。现在,我们必须检查 nginx 是否按我们的预期工作。要做到这一点,请运行以下命令:

$ curl localhost:8080

这次,我们会收到一个未经授权的消息:

<html>
<head><title>401 Authorization Required</title></head>
<body bgcolor="white">
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.4.6 (Ubuntu)</center>
</body>
</html>

使用之前创建的密码登录:

$ curl vinod1:vinod1@localhost:8080
"\"docker-registry server\""ubuntu@ip-172-31-21-44:~$

这证实了您的 Docker 注册表受到密码保护。

第 3 步 - 在 Web 服务器上设置 SSL 以进行安全通信

这是在本地机器上设置 SSL 的最后一步,该机器托管了用于加密数据的 Web 服务器。我们创建以下文件:

$sudo vi /etc/nginx/sites-available/docker-registry

使用以下内容更新文件:

server {
 listen 8080;
 server_name mydomain.com;
 ssl on;
 ssl_certificate /etc/ssl/certs/docker-registry;
 ssl_certificate_key /etc/ssl/private/docker-registry;

请注意,我的 Ubuntu 机器可以在 Internet 上使用名称mydomain.com,并且 SSL 已设置为证书和密钥的路径。

让我们按照以下方式签署证书:

$ sudo mkdir ~/certs
$ sudo cd ~/certs

使用以下命令生成根密钥:

$ sudo openssl genrsa -out devdockerCA.key 2048
Generating RSA private key, 2048 bit long modulus
..........+++
....................+++
e is 65537 (0x10001)

现在我们有了根密钥,让我们生成一个根证书(在命令提示符处输入任何你想要的):

$ sudo openssl req -x509 -new -nodes -key devdockerCA.key -days  \
 10000 -out devdockerCA.crt

然后,为我们的服务器生成一个密钥:

$ sudo openssl genrsa -out dev-docker-registry.com.key 2048

现在,我们必须创建一个证书签名请求。一旦我们运行签名命令,请确保“通用名称”是我们的服务器名称。这是强制性的,任何偏差都会导致错误:

$ sudo openssl req -new -key dev-docker-registry.com.key -out \
 dev-docker-registry.com.csr

在这里,“通用名称”看起来像mydomain.com。这是在 AWS 上运行的 Ubuntu VM。

上述命令的输出如下:

Country Name (2 letter code) [AU]:
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) []:mydomain.com
Email Address []:
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

“挑战密码”输入为空,并且用户也可以自由填写。然后,我们需要通过运行以下命令签署证书请求:

$ sudo openssl x509 -req -in dev-docker-registry.com.csr -CA  \
 devdockerCA.crt -CAkey devdockerCA.key -CAcreateserial -out \
 dev-docker-registry.com.crt -days 10000

现在我们已经生成了证书所需的所有文件,我们需要将这些文件复制到正确的位置。

首先,将证书和密钥复制到 nginx 期望它们在的路径:

$ sudo cp dev-docker-registry.com.crt /etc/ssl/certs/docker-registry
$ sudo chmod 777 /etc/ssl/certs/docker-registry
$ sudo cp dev-docker-registry.com.key /etc/ssl/private/docker-registry
$ sudo chmod 777 /etc/ssl/private/docker-registry

请注意,我们已经创建了自签名证书,并且它们是由任何已知的证书颁发机构签名的,因此我们需要通知注册表这是一个合法的证书:

$ sudo mkdir /usr/local/share/ca-certificates/docker-dev-cert
$ sudo cp devdockerCA.crt /usr/local/share/ca-certificates/docker-dev-cert
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/updated....done.
ubuntu@ip-172-31-21-44:~/certs$

让我们重新启动 nginx 以重新加载配置和 SSL 密钥:

$ sudo service nginx restart

现在,我们将测试 SSL 证书,以检查它是否正常工作。由于mydomain.com不是互联网地址,请在/etc/hosts文件中添加条目:

172.31.24.44 mydomain.com

现在运行以下命令:

$ sudo curl https://vinod1:vinod1@ mydomain.com:8080
"\"docker-registry server\""ubuntu@ip-172-31-21-44:~$

因此,如果一切顺利,您应该会看到类似于这样的内容:

"docker-registry server"

将图像推送到新创建的 Docker 注册表

最后,将图像推送到 Docker 注册表。因此,让我们在本地 Ubuntu 机器上创建一个图像:

$ sudo docker run -t -i ubuntu /bin/bash
root@9593c56f9e70:/# echo "TEST" >/mydockerimage
root@9593c56f9e70:/# exit
$ sudo docker commit $(sudo docker ps -lq) vinod-image
e17b685ee6987bb0cd01b89d9edf81a9fc0a7ad565a7e85650c41fc7e5c0cf9e

让我们登录到在 Ubuntu 机器上本地创建的 Docker 注册表:

$ sudo docker --insecure-registry=mydomain.com:8080 \
 login https://mydomain.com:8080
Username: vinod1
Password:
Email: vinod.puchi@gmail.com
Login Succeeded

在将图像推送到注册表之前对其进行标记:

$ sudo docker tag vinod-image mydomain.com:8080/vinod-image

最后,使用push命令上传图像:

$ sudo docker push \
mydomain.com:8080/vinod-image
The push refers to a repository [mydomain.com
:8080/vinod-image] (len: 1)
Sending image list
Pushing repository mydomain.com:8080/vi
nod-image (1 tags)
511136ea3c5a: Image successfully pushed
5bc37dc2dfba: Image successfully pushed
----------------------------------------------------
e17b685ee698: Image successfully pushed
Pushing tag for rev [e17b685ee698] on {https://mydomain.com
:8080/v1/repositories/vinod-image/tags/latest}
$

现在,从本地磁盘中删除图像,并从 Docker 注册表中pull它:

$ sudo docker pull mydomain.com:8080/vinod-image
Pulling repository mydomain.com:8080/vi
nod-image
e17b685ee698: Pulling image (latest) from mydomain.com
17b685ee698: Download complete
dc07507cef42: Download complete
86ce37374f40: Download complete
Status: Downloaded newer image for mydomain.com:8080/vinod-image:latest
$

总结

Docker 引擎允许每个增值软件解决方案被容器化、索引化、注册化和存储化。Docker 正在成为一个系统化开发、发布、部署和在各处运行容器的强大工具。虽然docker.io允许您免费将 Docker 创建上传到他们的注册表,但您在那里上传的任何内容都是公开可发现和可访问的。创新者和公司对此并不感兴趣,因此坚持使用私人 Docker Hub。在本章中,我们以易于理解的方式为您解释了所有步骤、语法和语义。我们看到了如何检索图像以生成 Docker 容器,并描述了如何以安全的方式将我们的图像推送到 Docker 注册表,以便经过身份验证的开发人员找到并使用。认证和授权机制作为整个过程的重要部分,已经被详细解释。确切地说,本章被构想和具体化为设置自己的 Docker Hub 的指南。随着世界组织对容器化云表现出示范性兴趣,私人容器中心变得更加重要。

在下一章中,我们将深入探讨容器,这是从图像自然而然的发展。我们将演示在 Docker 容器中运行服务的能力,比如 Web 服务器,并展示它与主机和外部世界的交互。

第六章:在容器中运行服务

我们一步步地走到了这一步,为快速发展的 Docker 技术奠定了坚实而令人振奋的基础。我们谈论了高度可用和可重复使用的 Docker 镜像的重要构建模块。此外,您可以阅读如何通过精心设计的存储框架存储和共享 Docker 镜像的易于使用的技术和提示。通常情况下,镜像必须不断经过一系列验证、验证和不断完善,以使它们更加正确和相关,以满足渴望发展的社区的需求。在本章中,我们将通过描述创建一个小型 Web 服务器的步骤,将其运行在容器内,并从外部世界连接到 Web 服务器,将我们的学习提升到一个新的水平。

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

  • 容器网络

  • 容器即服务CaaS)-构建、运行、暴露和连接到容器服务

  • 发布和检索容器端口

  • 将容器绑定到特定 IP 地址

  • 自动生成 Docker 主机端口

  • 使用EXPOSE-P选项进行端口绑定

容器网络简要概述

与任何计算节点一样,Docker 容器需要进行网络连接,以便其他容器和客户端可以找到并访问它们。在网络中,通常通过 IP 地址来识别任何节点。此外,IP 地址是任何客户端到达任何服务器节点提供的服务的唯一机制。Docker 内部使用 Linux 功能来为容器提供网络连接。在本节中,我们将学习有关容器 IP 地址分配和检索容器 IP 地址的过程。

当容器启动时,Docker 引擎会无需用户干预地选择并分配 IP 地址给容器。您可能会对 Docker 如何为容器选择 IP 地址感到困惑,这个谜团分为两部分来解答,如下所示:

  1. 在安装过程中,Docker 在 Docker 主机上创建一个名为docker0的虚拟接口。它还选择一个私有 IP 地址范围,并从所选范围中为docker0虚拟接口分配一个地址。所选的 IP 地址始终位于 Docker 主机 IP 地址范围之外,以避免 IP 地址冲突。

  2. 稍后,当我们启动一个容器时,Docker 引擎会从为docker0虚拟接口选择的 IP 地址范围中选择一个未使用的 IP 地址。然后,引擎将这个 IP 地址分配给新启动的容器。

默认情况下,Docker 会选择 IP 地址172.17.42.1/16,或者在172.17.0.0172.17.255.255范围内的 IP 地址之一。如果与172.17.x.x地址直接冲突,Docker 将选择不同的私有 IP 地址范围。也许,老式的ifconfig(显示网络接口详细信息的命令)在这里很有用,可以用来找出分配给虚拟接口的 IP 地址。让我们用docker0作为参数运行ifconfig,如下所示:

$ ifconfig docker0

输出的第二行将显示分配的 IP 地址及其子网掩码:

inet addr:172.17.42.1  Bcast:0.0.0.0  Mask:255.255.0.0

显然,从前面的文本中,172.17.42.1是分配给docker0虚拟接口的 IP 地址。IP 地址172.17.42.1是从172.17.0.0172.17.255.255的私有 IP 地址范围中的一个地址。

现在迫切需要我们学习如何找到分配给容器的 IP 地址。容器应该使用-i选项以交互模式启动。当然,我们可以通过在容器内运行ifconfig命令来轻松找到 IP 地址,如下所示:

$ sudo docker run -i -t ubuntu:14.04 /bin/bash
root@4b0b567b6019:/# ifconfig

ifconfig命令将显示 Docker 容器中所有接口的详细信息,如下所示:

eth0      Link encap:Ethernet  HWaddr e6:38:dd:23:aa:3f
 inet addr:172.17.0.12  Bcast:0.0.0.0  Mask:255.255.0.0
 inet6 addr: fe80::e438:ddff:fe23:aa3f/64 Scope:Link
 UP BROADCAST RUNNING  MTU:1500  Metric:1
 RX packets:6 errors:0 dropped:2 overruns:0 frame:0
 TX packets:7 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:488 (488.0 B)  TX bytes:578 (578.0 B)

lo        Link encap:Local Loopback
 inet addr:127.0.0.1  Mask:255.0.0.0
 inet6 addr: ::1/128 Scope:Host
 UP LOOPBACK RUNNING  MTU:65536  Metric:1
 RX packets:0 errors:0 dropped:0 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0
 RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

显然,ifconfig命令的前面输出显示 Docker 引擎为容器虚拟化了两个网络接口,如下所示:

  • 第一个是eth0(以太网)接口,Docker 引擎分配了 IP 地址172.17.0.12。显然,这个地址也在docker0虚拟接口的相同 IP 地址范围内。此外,分配给eth0接口的地址用于容器内部通信和主机到容器的通信。

  • 第二个接口是lo(环回)接口,Docker 引擎分配了环回地址127.0.0.1。环回接口用于容器内部的本地通信。

简单吧?然而,当使用docker run子命令中的-d选项以分离模式启动容器时,检索 IP 地址变得复杂起来。分离模式中的这种复杂性的主要原因是没有 shell 提示符来运行ifconfig命令。幸运的是,Docker 提供了一个docker inspect子命令,它像瑞士军刀一样方便,并允许我们深入了解 Docker 容器或镜像的低级细节。docker inspect子命令以 JSON 数组格式生成请求的详细信息。

以下是我们之前启动的交互式容器上docker inspect子命令的示例运行。4b0b567b6019容器 ID 取自容器的提示符:

$ sudo docker inspect 4b0b567b6019

该命令生成有关容器的大量信息。在这里,我们展示了从docker inspect子命令的输出中提取的容器网络配置的一些摘录:

"NetworkSettings": {
 "Bridge": "docker0",
 "Gateway": "172.17.42.1",
 "IPAddress": "172.17.0.12",
 "IPPrefixLen": 16,
 "PortMapping": null,
 "Ports": {}
 },

在这里,网络配置列出了以下详细信息:

  • Bridge:这是容器绑定的桥接口

  • Gateway:这是容器的网关地址,也是桥接口的地址

  • IPAddress:这是分配给容器的 IP 地址

  • IPPrefixLen:这是 IP 前缀长度,表示子网掩码的另一种方式

  • PortMapping:这是端口映射字段,现在已经被弃用,其值始终为 null

  • Ports:这是端口字段,将列举所有端口绑定,这将在本章后面介绍

毫无疑问,docker inspect子命令对于查找容器或镜像的细节非常方便。然而,浏览令人生畏的细节并找到我们渴望寻找的正确信息是一项繁琐的工作。也许,您可以使用grep命令将其缩小到正确的信息。或者更好的是,使用docker inspect子命令,它可以帮助您使用docker inspect子命令的--format选项从 JSON 数组中选择正确的字段。

值得注意的是,在以下示例中,我们使用docker inspect子命令的--format选项仅检索容器的 IP 地址。IP 地址可以通过 JSON 数组的.NetworkSettings.IPAddress字段访问:

$ sudo docker inspect \
 --format='{{.NetworkSettings.IPAddress}}' 4b0b567b6019
172.17.0.12

将容器视为服务

我们为 Docker 技术的基础打下了良好的基础。在本节中,我们将专注于使用 HTTP 服务创建镜像,使用创建的镜像在容器内启动 HTTP 服务,然后演示连接到容器内运行的 HTTP 服务。

构建 HTTP 服务器镜像

在本节中,我们将创建一个 Docker 镜像,以在Ubuntu 14.04基础镜像上安装Apache2,并配置Apache HTTP Server以作为可执行文件运行,使用ENTRYPOINT指令。

在第三章构建镜像中,我们演示了使用 Dockerfile 在Ubuntu 14.04基础镜像上创建Apache2镜像的概念。在这个例子中,我们将通过设置Apache日志路径和使用ENTRYPOINT指令将Apache2设置为默认执行应用程序来扩展这个 Dockerfile。以下是Dockerfile内容的详细解释。

我们将使用FROM指令以ubuntu:14.04作为基础镜像构建镜像,如Dockerfile片段所示:

###########################################
# Dockerfile to build an apache2 image
###########################################
# Base image is Ubuntu
FROM ubuntu:14.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:14.04(基础镜像)的详细信息,如下面的输出片段所示:

apache2             latest              d5526cd1a645        About a minute ago   232.6 MB
ubuntu              14.04               5506de2b643b        5 days ago           197.8 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

在 Docker 主机的 shell 提示符下,找到容器的 IP 地址为172.17.0.13,让我们快速在这个 IP 地址上运行一个 web 请求,使用wget命令。在这里,我们选择使用-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 主机内的另一个容器访问了该服务。此外,正如在第二章的从容器构建镜像部分所演示的,容器能够通过在互联网上连接到公共可用的 apt 仓库来成功安装wget软件包。然而,默认情况下,外部世界无法访问容器提供的服务。起初,这可能看起来像是 Docker 技术的一个限制。然而,事实是,容器是根据设计与外部世界隔离的。

Docker 通过 IP 地址分配标准实现容器的网络隔离,具体列举如下:

  1. 为容器分配一个私有 IP 地址,该地址无法从外部网络访问。

  2. 为容器分配一个在主机 IP 网络之外的 IP 地址。

因此,Docker 容器甚至无法从与 Docker 主机相同的 IP 网络连接的系统访问。这种分配方案还可以防止可能会出现的 IP 地址冲突。

现在,您可能想知道如何使服务在容器内运行,并且可以被外部访问,换句话说,暴露容器服务。嗯,Docker 通过在底层利用 Linux iptables功能来弥合这种连接差距。

在前端,Docker 为用户提供了两种不同的构建模块来弥合这种连接差距。其中一个构建模块是使用docker run子命令的-p(将容器的端口发布到主机接口)选项来绑定容器端口。另一种选择是使用EXPOSE Dockerfile 指令和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软件包一起提供的默认页面:

发布容器端口- -p 选项

容器的网络地址转换

在上一节中,我们看到-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 活动转发到 IP 地址172.17.0.17,即我们容器的 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指令,我们只有使用-p80: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": "docker0",
 "Gateway": "172.17.42.1",
 "IPAddress": "172.17.0.14",
 "IPPrefixLen": 16,
 "PortMapping": null,
 "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

显然,在所有先前的输出摘录中,突出显示的信息是 IP 地址0.0.0.0和端口号80。IP 地址0.0.0.0是一个元地址,代表了 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 userland proxy: listen tcp 198.51.100.73:80: 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子命令的输出和iptablesDNAT条目之后,您将意识到 Docker 引擎如何优雅地配置了容器在 Docker 主机的 IP 地址198.51.100.73和端口80上提供的服务。

自动生成 Docker 主机端口

Docker 容器天生轻量级,由于其轻量级的特性,您可以在单个 Docker 主机上运行多个相同或不同服务的容器。特别是根据需求在多个容器之间自动扩展相同服务的需求是当今 IT 基础设施的需求。在本节中,您将了解在启动多个具有相同服务的容器时所面临的挑战,以及 Docker 解决这一挑战的方式。

在本章的前面,我们使用apache2 http server启动了一个容器,并将其绑定到 Docker 主机的端口80。现在,如果我们尝试再启动一个绑定到相同端口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子命令的输出和iptablesDNAT条目之后,引人注目的是端口号49158。端口号49158是由 Docker 引擎在 Docker 主机上巧妙地自动生成的,借助底层操作系统的帮助。此外,元 IP 地址0.0.0.0意味着容器提供的服务可以通过 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 选项进行端口绑定

到目前为止,我们已经讨论了将容器内运行的服务发布到外部世界的四种不同方法。在这四种方法中,端口绑定决策是在容器启动时进行的,并且镜像对于提供服务的端口没有任何信息。到目前为止,这已经运作良好,因为镜像是由我们构建的,我们对提供服务的端口非常清楚。然而,在第三方镜像的情况下,容器内的端口使用必须明确发布。此外,如果我们为第三方使用或甚至为自己使用构建镜像,明确声明容器提供服务的端口是一个良好的做法。也许,镜像构建者可以随镜像一起提供一个自述文件。然而,将端口详细信息嵌入到镜像本身中会更好,这样您可以轻松地从镜像中手动或通过自动化脚本找到端口详细信息。

Docker 技术允许我们使用Dockerfile中的EXPOSE指令嵌入端口信息,我们在第三章构建镜像中介绍过。在这里,让我们编辑之前在本章中使用的构建apache2 HTTP 服务器镜像的Dockerfile,并添加一个EXPOSE指令,如下所示。HTTP 服务的默认端口是端口80,因此端口80被暴露:

###########################################
# Dockerfile to build an apache2 image
###########################################
# Base image is Ubuntu
FROM ubuntu:14.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": {}
 },

或者,您可以将格式选项应用于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 主机文件系统的一部分,它只是在容器内部挂载。

数据卷可以使用DockerfileVOLUME指令在 Docker 镜像中进行刻录。此外,可以在启动容器时使用docker run子命令的-v选项进行指定。在下面的示例中,将详细说明在Dockerfile中使用VOLUME指令的含义,具体步骤如下:

  1. 创建一个非常简单的Dockerfile,其中包含基础镜像(ubuntu:14.04)和数据卷(/MountPointDemo)的指令:
FROM ubuntu:14.04
VOLUME /MountPointDemo
  1. 使用docker build子命令构建名称为mount-point-demo的镜像:
$ sudo docker build -t mount-point-demo .

  1. 构建完镜像后,让我们使用docker inspect子命令快速检查我们的数据卷:
$ sudo docker inspect mount-point-demo
[{
 "Architecture": "amd64",
... TRUNCATED OUTPUT ...
 "Volumes": {
 "/MountPointDemo": {}
 },
... TRUNCATED OUTPUT ...

显然,在前面的输出中,数据卷是直接刻录在镜像中的。

  1. 现在,让我们使用先前创建的镜像启动一个交互式容器,如下命令所示:
$ 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
... TRUNCATED OUTPUT ...
/dev/disk/by-uuid/721cedbd-57b1-4bbd-9488-ec3930862cf5 on /MountPointDemo type ext3 (rw,noatime,nobarrier,errors=remount-ro,data=ordered)
... TRUNCATED OUTPUT ...

  1. 在本节中,我们检查了镜像,以了解镜像中的数据卷声明。现在我们已经启动了容器,让我们在另一个终端中使用docker inspect子命令和容器 ID 作为参数来检查容器的数据卷。我们之前创建了一些容器,为此,让我们直接从容器的提示符中获取容器 ID8d22f73b5b46
$ sudo docker inspect 8d22f73b5b46
... TRUNCATED OUTPUT ...
 "Volumes": {
 "/MountPointDemo": "/var/lib/docker/vfs/dir/737e0355c5d81c96a99d41d1b9f540c2a212000661633ceea46f2c298a45f128"
 },
 "VolumesRW": {
 "/MountPointDemo": true
 }
}

显然,在这里,数据卷被映射到 Docker 主机中的一个目录,并且该目录以读写模式挂载。这个目录是由 Docker 引擎在容器启动时自动创建的。

到目前为止,我们已经看到了DockerfileVOLUME指令的含义,以及 Docker 如何管理数据卷。像Dockerfile中的VOLUME指令一样,我们可以使用docker run子命令的-v <容器挂载点路径>选项,如下面的命令所示:

$ sudo docker run –v /MountPointDemo -it ubuntu:14.04

启动容器后,我们鼓励您尝试在新启动的容器中使用ls -ld /MountPointDemomount命令,然后也像前面的步骤 5 中所示那样检查容器。

在这里描述的两种情况中,Docker 引擎会自动在/var/lib/docker/vfs/目录下创建目录,并将其挂载到容器中。当使用docker rm子命令删除容器时,Docker 引擎不会删除在容器启动时自动创建的目录。这种行为本质上是为了保留存储在目录中的容器应用程序的状态。如果您想删除 Docker 引擎自动创建的目录,可以在删除容器时使用docker rm子命令提供-v选项来执行,前提是容器已经停止:

$ sudo docker rm -v 8d22f73b5b46

如果容器仍在运行,则可以通过在上一个命令中添加-f选项来删除容器以及自动生成的目录:

$ sudo docker rm -fv 8d22f73b5b46

我们已经介绍了在 Docker 主机中自动生成目录并将其挂载到容器数据卷的技术和提示。然而,使用docker run子命令的-v选项可以将用户定义的目录挂载到数据卷。在这种情况下,Docker 引擎不会自动生成任何目录。

注意

系统生成的目录存在目录泄漏的问题。换句话说,如果您忘记删除系统生成的目录,可能会遇到一些不必要的问题。有关更多信息,您可以阅读本章节中的避免常见陷阱部分。

共享主机数据

之前,我们描述了在 Docker 镜像中使用Dockerfile中的VOLUME指令创建数据卷的步骤。然而,Docker 没有提供任何机制在构建时挂载主机目录或文件,以确保 Docker 镜像的可移植性。Docker 提供的唯一规定是在容器启动时将主机目录或文件挂载到容器的数据卷上。Docker 通过docker run子命令的-v选项公开主机目录或文件挂载功能。-v选项有三种不同的格式,如下所列:

  1. -v <容器挂载路径>

  2. -v <host path>/<container mount path>

  3. -v <host path>/<container mount path>:<read write mode>

<host path>是 Docker 主机上的绝对路径,<container mount path>是容器文件系统中的绝对路径,<read write mode>可以是只读(ro)或读写(rw)模式。第一个-v <container mount path>格式已经在本章的数据卷部分中解释过,作为在容器启动时创建挂载点的方法。第二和第三个选项使我们能够将 Docker 主机上的文件或目录挂载到容器的挂载点。

我们希望通过几个例子深入了解主机数据共享。在第一个例子中,我们将演示如何在 Docker 主机和容器之间共享一个目录,在第二个例子中,我们将演示文件共享。

在第一个例子中,我们将一个目录从 Docker 主机挂载到一个容器中,在容器上执行一些基本的文件操作,并从 Docker 主机验证这些操作,详细步骤如下:

  1. 首先,让我们使用docker run子命令的-v选项启动一个交互式容器,将 Docker 主机目录/tmp/hostdir挂载到容器的/MountPoint
$ sudo docker run -v /tmp/hostdir:/MountPoint \
 -it ubuntu:14.04

注意

如果在 Docker 主机上找不到/tmp/hostdir,Docker 引擎将自行创建该目录。然而,问题在于系统生成的目录无法使用docker rm子命令的-v选项删除。

  1. 成功启动容器后,我们可以使用ls命令检查/MountPoint的存在:
root@4a018d99c133:/# ls -ld /MountPoint
drwxr-xr-x 2 root root 4096 Nov 23 18:28 /MountPoint

  1. 现在,我们可以继续使用mount命令检查挂载细节:
root@4a018d99c133:/# mount
... TRUNCATED OUTPUT ...
/dev/disk/by-uuid/721cedbd-57b1-4bbd-9488-ec3930862cf5 on /MountPoint type ext3 (rw,noatime,nobarrier,errors=remount-ro,data=ordered)
... TRUNCATED OUTPUT ...

  1. 在这里,我们将验证/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

  1. 可能值得努力使用新终端上的ls命令验证/tmp/hostdir Docker 主机目录中的文件,因为我们的容器正在现有终端上以交互模式运行:
$ 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 主机和容器之间的时区差异造成的。

  1. 最后,让我们运行docker inspect子命令,以容器 ID4a018d99c133作为参数,查看 Docker 主机和容器挂载点之间是否设置了目录映射,如下命令所示:
$ sudo docker inspect \
 --format={{.Volumes}} 4a018d99c133
map[/MountPoint:/tmp/hostdir]

显然,在docker inspect子命令的先前输出中,Docker 主机的/tmp/hostdir目录被挂载到容器的/MountPoint挂载点上。

对于第二个示例,我们可以将文件从 Docker 主机挂载到容器中,从容器中更新文件,并从 Docker 主机验证这些操作,详细步骤如下:

  1. 为了将文件从 Docker 主机挂载到容器中,文件必须在 Docker 主机上预先存在。否则,Docker 引擎将创建一个具有指定名称的新目录,并将其挂载为目录。我们可以通过使用touch命令在 Docker 主机上创建一个文件来开始:
$ touch /tmp/hostfile.txt

  1. 使用docker run子命令的-v选项启动交互式容器,将/tmp/hostfile.txt Docker 主机文件挂载到容器上,作为/tmp/mntfile.txt
$ sudo docker run -v /tmp/hostfile.txt:/mountedfile.txt \
 -it ubuntu:14.04

  1. 成功启动容器后,现在让我们使用ls命令检查/mountedfile.txt的存在:
root@d23a15527eeb:/# ls -l /mountedfile.txt
-rw-rw-r-- 1 1000 1000 0 Nov 23 19:33 /mountedfile.txt

  1. 然后,继续使用mount命令检查挂载细节:
root@d23a15527eeb:/# mount
... TRUNCATED OUTPUT ...
/dev/disk/by-uuid/721cedbd-57b1-4bbd-9488-ec3930862cf5 on /mountedfile.txt type ext3 (rw,noatime,nobarrier,errors=remount-ro,data=ordered)
... TRUNCATED OUTPUT ...

  1. 然后,使用echo命令更新/mountedfile.txt中的一些文本:
root@d23a15527eeb:/# echo "Writing from Container" \
 > mountedfile.txt

  1. 同时,在 Docker 主机中切换到另一个终端,并使用cat命令打印/tmp/hostfile.txt Docker 主机文件:
$ cat /tmp/hostfile.txt
Writing from Container

  1. 最后,运行docker inspect子命令,以容器 IDd23a15527eeb作为参数,查看 Docker 主机和容器挂载点之间的文件映射:
$ sudo docker inspect \
 --format={{.Volumes}} d23a15527eeb
map[/mountedfile.txt:/tmp/hostfile.txt]

从前面的输出可以看出,来自 Docker 主机的/tmp/hostfile.txt文件被挂载为容器内的/mountedfile.txt

注意

在 Docker 主机和容器之间共享文件的情况下,文件必须在启动容器之前存在。然而,在目录共享的情况下,如果 Docker 主机中不存在该目录,则 Docker 引擎会在 Docker 主机中创建一个新目录,如前面所述。

主机数据共享的实用性

在上一章中,我们在 Docker 容器中启动了一个HTTP服务。然而,如果你记得正确的话,HTTP服务的日志文件仍然在容器内,无法直接从 Docker 主机访问。在这里,在本节中,我们逐步阐述了从 Docker 主机访问日志文件的过程:

  1. 让我们开始启动一个 Apache2 HTTP 服务容器,将 Docker 主机的/var/log/myhttpd目录挂载到容器的/var/log/apache2目录,使用docker run子命令的-v选项。在这个例子中,我们正在利用我们在上一章中构建的apache2镜像,通过调用以下命令:
$ sudo docker run -d -p 80:80 \
 -v /var/log/myhttpd:/var/log/apache2 apache2
9c2f0c0b126f21887efaa35a1432ba7092b69e0c6d523ffd50684e27eeab37ac

如果你还记得第六章中的Dockerfile在容器中运行服务APACHE_LOG_DIR环境变量被设置为/var/log/apache2目录,使用ENV指令。这将使 Apache2 HTTP 服务将所有日志消息路由到/var/log/apache2数据卷。

  1. 容器启动后,我们可以在 Docker 主机上切换到/var/log/myhttpd目录:
$ cd /var/log/myhttpd

  1. 也许,在这里适当地快速检查/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文件是虚拟主机日志,在我们的情况下始终为空。

  1. 我们可以使用tail命令和-f选项显示/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.logother_vhosts_access.log 都是空的,并且 error.log 文件上有一些错误消息。显然,这些错误日志是由容器内运行的 HTTP 服务生成的。然后,这些日志被储存在 Docker 主机目录中,在容器启动时被挂载。

  1. 当我们继续运行 tail –f * 时,让我们从容器内运行的 Web 浏览器连接到 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 选项将该容器的数据卷挂载到其他容器。

数据专用容器

数据专用容器的主要责任是保存数据。创建数据专用容器与数据卷部分所示的方法非常相似。此外,容器被明确命名,以便其他容器使用容器的名称挂载数据卷。即使数据专用容器处于停止状态,其他容器也可以访问数据专用容器的数据卷。数据专用容器可以通过以下两种方式创建:

  • 在容器启动时通过配置数据卷和容器名称。

  • 数据卷也可以在构建镜像时通过 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选项来挂载多个容器的数据卷。

这是一个实际的示例,演示了如何从另一个容器挂载数据卷,并逐步展示数据卷挂载过程。

  1. 我们首先启动一个交互式 Ubuntu 容器,通过挂载数据专用容器(datavol)中的数据卷来进行操作,如前述所述:
$ sudo docker run –it \
 --volumes-from datavol \
 ubuntu:latest /bin/bash

  1. 现在从容器的提示符中,让我们使用mount命令验证数据卷挂载:
root@e09979cacec8:/# mount
. . . TRUNCATED OUTPUT . . .
/dev/disk/by-uuid/32a56fe0-7053-4901-ae7e-24afe5942e91 on /DataMount type ext3 (rw,noatime,nobarrier,errors=remount-ro,data=ordered)
. . . TRUNCATED OUTPUT . . .

在这里,我们成功地从datavol数据专用容器中挂载了数据卷。

  1. 接下来,我们需要使用docker inspect子命令从另一个终端检查该容器的数据卷:
$ sudo docker inspect  e09979cacec8
. . . TRUNCATED OUTPUT . . .
 "Volumes": {
 "/DataMount": "/var/lib/docker/vfs/dir/62f5a3314999e5aaf485fc692ae07b3cbfacbca9815d8071f519c1a836c0f01e"
},
 "VolumesRW": {
 "/DataMount": true
 }
}

显然,来自datavol数据专用容器的数据卷被挂载,就好像它们直接挂载到了这个容器上一样。

我们可以从另一个容器挂载数据卷,并展示挂载点。我们可以通过使用数据卷在容器之间共享数据来使挂载的数据卷工作,如下所示:

  1. 让我们重用在上一个示例中启动的容器,并通过向数据卷/DataMount写入一些文本来创建一个/DataMount/testfile文件,如下所示:
root@e09979cacec8:/# echo \
 "Data Sharing between Container" > \
 /DataMount/testfile

  1. 只需将一个容器分离出来,以显示我们在上一步中编写的文本,使用cat命令:
$ sudo docker run --rm \
 --volumes-from datavol \
 busybox:latest cat /DataMount/testfile

以下是前述命令的典型输出:

Data Sharing between Container

显然,我们新容器化的cat命令的前面输出容器之间的数据共享是我们在步骤 1 中写入/DataMount/testfiledatavol容器中的文本。

很酷,不是吗?您可以通过共享数据卷在容器之间无缝共享数据。在这个例子中,我们使用数据专用容器作为数据共享的基础容器。然而,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 服务日志处理的方法。为了在容器之间共享日志文件,我们将按照以下步骤启动以下容器:

  1. 首先,一个仅用于数据的容器,将向其他容器公开数据卷。

  2. 然后,一个利用数据专用容器的数据卷的 Apache2 HTTP 服务容器。

  3. 一个用于查看我们 Apache2 HTTP 服务生成的日志文件的容器。

注意

注意:如果您在 Docker 主机机器的端口号80上运行任何 HTTP 服务,请为以下示例选择任何其他未使用的端口号。如果没有,请先停止 HTTP 服务,然后按照示例进行操作,以避免任何端口冲突。

现在,我们将逐步为您介绍如何制作相应的镜像并启动容器以查看日志文件,如下所示:

  1. 在这里,我们首先使用VOLUME指令使用/var/log/apache2数据卷来制作Dockerfile/var/log/apache2数据卷是对Dockerfile中第六章中设置的环境变量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

  1. 我们将继续使用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 ...

  1. 使用docker run子命令从apache2log镜像启动一个仅数据的容器,并将生成的容器命名为log_vol,使用--name选项:
$ sudo docker run --name log_vol apache2log

根据上述命令,容器将在/var/log/apache2中创建一个数据卷并将其移至停止状态。

  1. 与此同时,您可以使用-a选项运行docker ps子命令来验证容器的状态:
$ 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退出。

  1. 使用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 服务后,我们可以使用临时容器访问日志文件。

  1. 在这里,我们使用临时容器列出了 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

  1. 最后,通过使用tail命令访问 Apache2 HTTP 服务生成的错误日志,如下命令所示:
$ sudo docker run  --rm  \
 --volumes-from log_vol \
 ubuntu:14.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 删除这些目录。手动删除的过程会带来以下两个主要挑战:

  1. 未删除的目录: 可能会出现这样的情况,您可能有意或无意地选择不删除生成的目录,而删除容器。

  2. 第三方镜像: 我们经常利用第三方 Docker 镜像,这些镜像可能已经使用了VOLUME指令进行构建。同样,我们可能也有自己的 Docker 镜像,其中包含了VOLUME。当我们使用这些 Docker 镜像启动容器时,Docker 引擎将自动生成指定的目录。由于我们不知道数据卷的创建,我们可能不会使用-v选项调用docker rm子命令来删除自动生成的目录。

在前面提到的情况中,一旦相关的容器被移除,就没有直接的方法来识别那些容器被移除的目录。以下是一些建议,可以避免这种问题:

  • 始终使用docker inspect子命令检查 Docker 镜像,查看镜像中是否有数据卷。

  • 始终使用docker rm子命令的-v选项来删除为容器创建的任何数据卷(目录)。即使数据卷被多个容器共享,仍然可以安全地使用docker rm子命令的-v选项,因为只有当共享该数据卷的最后一个容器被移除时,与数据卷关联的目录才会被删除。

  • 无论出于何种原因,如果您选择保留自动生成的目录,您必须保留清晰的记录,以便以后可以删除它们。

  • 实施一个审计框架,用于审计并找出没有任何容器关联的目录。

数据卷的不良影响

如前所述,Docker 允许我们在构建时使用VOLUME指令将数据卷刻录到 Docker 镜像中。然而,在构建过程中不应该使用数据卷来存储任何数据,否则会产生不良影响。

在本节中,我们将通过制作一个Dockerfile来演示在构建过程中使用数据卷的不良影响,然后通过构建这个Dockerfile来展示其影响:

以下是Dockerfile的详细信息:

  1. 使用Ubuntu 14.04作为基础镜像构建镜像:
# Use Ubuntu as the base image
FROM ubuntu:14.04
  1. 使用VOLUME指令创建一个/MountPointDemo数据卷:
VOLUME /MountPointDemo
  1. 使用RUN指令在/MountPointDemo数据卷中创建一个文件:
RUN date > /MountPointDemo/date.txt
  1. 使用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:14.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 步创建的文件。显然,在第 3 步时创建的文件在第 2 步时消失了。这种不良影响是由 Docker 构建其镜像的方法造成的。了解 Docker 镜像构建过程将揭开这个谜团。

在构建过程中,对于Dockerfile中的每个指令,按照以下步骤进行:

  1. 通过将Dockerfile指令转换为等效的docker run子命令来创建一个新的容器

  2. 将新创建的容器提交为镜像

  3. 通过将新创建的镜像视为第 1 步的基础镜像,重复执行第 1 步和第 2 步。

当容器被提交时,它保存容器的文件系统,并故意不保存数据卷的文件系统。因此,在此过程中存储在数据卷中的任何数据都将丢失。因此,在构建过程中永远不要使用数据卷作为存储。

总结

对于企业规模的分布式应用来说,数据是其运营和产出中最重要的工具和成分。通过 IT 容器化,这个旅程以迅速和明亮的方式开始。通过巧妙利用 Docker 引擎,IT 和业务软件解决方案都被智能地容器化。然而,最初的动机是更快速、无缺陷地实现应用感知的 Docker 容器,因此,数据与容器内的应用紧密耦合。然而,这种紧密性带来了一些真正的风险。如果应用程序崩溃,那么数据也会丢失。此外,多个应用程序可能依赖于相同的数据,因此数据必须进行共享。

在本章中,我们讨论了 Docker 引擎在促进 Docker 主机和容器之间以及容器之间无缝数据共享方面的能力。数据卷被规定为实现不断增长的 Docker 生态系统中各组成部分之间数据共享的基础构件。在下一章中,我们将解释容器编排背后的概念,并看看如何通过一些自动化工具简化这个复杂的方面。编排对于实现复合容器至关重要。

第八章:容器编排

在早期的章节中,我们为容器网络的需求奠定了坚实的基础,以及如何在 Docker 容器内运行服务,以及如何通过打开网络端口和其他先决条件来将此服务暴露给外部世界。然而,最近,已经提供了先进的机制,并且一些第三方编排平台进入市场,以明智地建立分布式和不同功能的容器之间的动态和决定性联系,以便为全面但紧凑地包含面向过程、多层和企业级分布式应用程序组合强大的容器。在这个极其多样化但相互连接的世界中,编排的概念不能长期远离其应有的突出地位。本章专门用于解释容器编排的细枝末节,以及它在挑选离散容器并系统地组合成更直接符合不同业务期望和迫切需求的复杂容器方面的直接作用。

在本章中,我们将讨论以下主题的相关细节:

  • 链接容器

  • 编排容器

  • 使用docker-compose工具进行容器编排

随着关键任务的应用程序主要是通过松散耦合但高度内聚的组件/服务构建,旨在在地理分布的 IT 基础设施和平台上运行,组合的概念受到了很多关注和吸引力。为了维持良好的容器化旅程,容器的编排被认为是在即时、自适应和智能的 IT 时代中最关键和至关重要的要求之一。有一些经过验证和有前途的方法和符合标准的工具,可以实现神秘的编排目标。

链接容器

Docker 技术的一个显著特点之一是链接容器。也就是说,合作容器可以链接在一起,提供复杂和业务感知的服务。链接的容器具有一种源-接收关系,其中源容器链接到接收容器,并且接收容器安全地从源容器接收各种信息。但是,源容器对其链接的接收者一无所知。链接容器的另一个值得注意的特性是,在安全设置中,链接的容器可以使用安全隧道进行通信,而不会将用于设置的端口暴露给外部世界。

Docker 引擎在docker run子命令中提供了--link选项,以将源容器链接到接收容器。

--link选项的格式如下:

--link <container>:<alias>

在这里,<container>是源容器的名称,<alias>是接收容器看到的名称。容器的名称在 Docker 主机中必须是唯一的,而别名非常具体且局限于接收容器,因此别名不需要在 Docker 主机上是唯一的。这为在接收容器内部使用固定的源别名名称实现和整合功能提供了很大的灵活性。

当两个容器链接在一起时,Docker 引擎会自动向接收容器导出一些环境变量。这些环境变量具有明确定义的命名约定,其中变量始终以别名名称的大写形式作为前缀。例如,如果src是源容器的别名,则导出的环境变量将以SRC_开头。Docker 导出三类环境变量,如下所列:

  1. 名称:这是环境变量的第一类。这个变量采用<ALIAS>_NAME的形式,并将接收容器的分层名称作为其值。例如,如果源容器的别名是src,接收容器的名称是rec,那么环境变量及其值将是SRC_NAME=/rec/src

  2. ENV:这是环境变量的第二类。这些变量通过docker run子命令的-e选项或DockerfileENV指令在源容器中配置的环境变量。这种类型的环境变量采用<ALIAS>_ENV_<VAR_NAME>的形式。例如,如果源容器的别名是src,变量名是SAMPLE,那么环境变量将是SRC_ENV_SAMPLE

  3. PORT:这是最终的第三类环境变量,用于将源容器的连接详细信息导出给接收方。Docker 为源容器通过docker run子命令的-p选项或DockerfileEXPOSE指令暴露的每个端口创建一组变量。

这些变量采用以下形式:

*<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 引擎还在此类别中导出了一个变量,即<ALIAS>_PORT的形式,其值将是源容器暴露的所有端口中最低的 URL。例如,如果源容器的别名是src,暴露的端口号是7070808080,协议是tcp,IP 地址是172.17.0.2,那么环境变量及其值将是SRC_PORT=tcp://172.17.0.2:80

Docker 以良好结构的格式导出这些自动生成的环境变量,以便可以轻松地通过程序发现。因此,接收容器可以很容易地发现有关源容器的信息。此外,Docker 会自动将源 IP 地址及其别名更新为接收容器的/etc/hosts文件中的条目。

在本章中,我们将通过一系列实用示例深入介绍 Docker 引擎提供的容器链接功能。

首先,让我们选择一个简单的容器链接示例。在这里,我们将向您展示如何在两个容器之间建立链接,并将一些基本信息从源容器传输到接收容器,如下所示的步骤:

  1. 我们首先启动一个交互式容器,可以作为链接的源容器使用,使用以下命令:
$ sudo docker run --rm --name example -it busybox:latest

容器使用--name选项命名为example。此外,使用--rm选项在退出容器时清理容器。

  1. 使用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)。

  1. 我们将继续使用env命令显示源容器的环境变量:
/ # env
HOSTNAME=a02895551686
SHLVL=1
HOME=/root
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/

  1. 启动源容器后,从相同 Docker 主机的另一个终端,让我们使用docker run子命令的--link选项启动一个交互式接收容器,将其链接到我们的源容器,如下所示:
$ sudo docker run --rm --link example:ex -it busybox:latest

在这里,名为example的源容器与接收容器链接,其别名为ex

  1. 让我们使用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
172.17.0.3      ex

当然,像往常一样,/etc/hosts文件的第一个条目是容器的 IP 地址和其主机名。然而,/etc/hosts文件中值得注意的条目是最后一个条目,其中源容器的 IP 地址(172.17.0.3)和其别名(ex)会自动添加。

  1. 我们将继续使用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是接收容器的自动生成名称。

  1. 最后一步,使用广泛使用的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被解析为 IP 地址172.17.0.3,并且接收容器能够成功到达源容器。在安全容器通信的情况下,容器之间是不允许 ping 的。我们在第十一章中对保护容器方面进行了更多详细说明,保护 Docker 容器

在前面的示例中,我们可以将两个容器链接在一起,并且观察到源容器的 IP 地址如何优雅地更新到接收容器的/etc/hosts文件中,从而实现容器之间的网络连接。

下一个示例是演示容器链接如何导出源容器的环境变量,这些环境变量是使用docker run子命令的-e选项或DockerfileENV指令配置的,然后导入到接收容器中。为此,我们将编写一个带有ENV指令的Dockerfile,构建一个镜像,使用该镜像启动一个源容器,然后通过链接到源容器来启动一个接收容器:

  1. 我们首先编写一个带有ENV指令的Dockerfile,如下所示:
FROM busybox:latest
ENV BOOK="Learning Docker" \
    CHAPTER="Orchestrating Containers"

在这里,我们设置了两个环境变量BOOKCHAPTER

  1. 继续使用前面的Dockerfile从头构建一个名为envex的 Docker 镜像:
$ sudo docker build -t envex .

  1. 现在,让我们使用刚刚构建的envex镜像启动一个交互式源容器,名称为example
$ sudo docker run -it --rm \
 --name example envex

  1. 从源容器提示符中,通过调用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=/

在所有前述的环境变量中,BOOKCHAPTER变量都是使用DockerfileENV指令配置的。

  1. 最后一步,为了说明环境变量的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_BOOKEX_ENV_CHAPTER,它们最初是通过Dockerfile设置为BOOKCHAPTER,但由于容器链接而修改为EX_ENV_BOOKEX_ENV_CHAPTER。尽管环境变量名称被翻译,但存储在这些环境变量中的值保持不变。我们在前面的示例中已经讨论了EX_NAME变量名。

在前面的示例中,我们可以体验到 Docker 如何优雅而轻松地将源容器中的ENV类别变量导出到接收容器中。这些环境变量与源和接收完全解耦,因此一个容器中这些环境变量值的更改不会影响另一个容器。更准确地说,接收容器接收的值是在启动源容器时设置的值。在源容器启动后对这些环境变量值进行的任何更改都不会影响接收容器。接收容器的启动时间并不重要,因为这些值是从 JSON 文件中读取的。

在我们最后的容器链接示例中,我们将向您展示如何利用 Docker 功能来共享两个容器之间的连接详细信息。为了共享容器之间的连接详细信息,Docker 使用环境变量的PORT类别。以下是用于创建两个容器并共享它们之间连接详细信息的步骤:

  1. 编写一个Dockerfile,使用EXPOSE指令来公开端口808080,如下所示:
FROM busybox:latest
EXPOSE 8080 80
  1. 继续使用docker build子命令从刚刚创建的Dockerfile构建 Docker 镜像portex,运行以下命令:
$ sudo docker build -t portex .

  1. 现在,让我们使用之前构建的镜像portex启动一个名为example的交互式源容器:
$ sudo docker run -it --rm \
 --name example portex

  1. 现在我们已经启动了源容器,让我们继续在另一个终端上创建一个接收容器,并将其链接到源容器,然后调用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 引擎为每个使用Dockerfile中的EXPOSE指令暴露的端口导出了一组四个PORT类别的环境变量。此外,Docker 还导出了另一个PORT类别的变量EX_PORT

容器的编排

IT 领域中编排的开创性概念已经存在很长时间了。例如,在服务计算SC)领域,服务编排的概念以前所未有的方式蓬勃发展,以生产和维护高度健壮和有弹性的服务。离散或原子服务除非按特定顺序组合在一起以获得具有过程感知的复合服务,否则不会起到实质作用。由于编排服务在表达和展示企业独特能力方面更具战略优势,可以以可识别/可发现、可互操作、可用和可组合的服务形式向外界展示;企业对拥有一个易于搜索的服务库(原子和复合)表现出了极大的兴趣。反过来,这个库使企业能够实现大规模的数据和过程密集型应用。很明显,服务的多样性对于组织的增长和发展非常关键。这个日益受到强制要求的需求通过经过验证和有前途的编排能力得到了解决,具有认知能力。

现在,随着我们迅速向容器化的 IT 环境迈进;应用程序和数据容器应该被巧妙地组合起来,以实现一系列新一代软件服务。

然而,要生成高度有效的编排容器,需要精心选择并按正确顺序启动特定目的和不可知目的的容器,以创建编排容器。顺序可以来自过程(控制和数据)流程图。手动完成这一复杂而艰巨的活动引发了一系列怀疑和批评。幸运的是,在 Docker 领域有编排工具可以帮助构建、运行和管理多个容器,以构建企业级服务。Docker 公司负责生产和推广 Docker 启发的容器的生成和组装,推出了一种标准化和简化的编排工具(名为docker-compose),以减轻开发人员和系统管理员的工作负担。

服务计算范式的成熟组合技术正在这里复制到激烈的容器化范式中,以实现容器化最初设想的好处,特别是构建功能强大的应用程序感知容器。

微服务架构是一种旨在通过将其功能分解为一组离散服务的架构概念,以解耦软件解决方案的方法。这是通过在架构层面应用标准原则来实现的。微服务架构正在逐渐成为设计和构建大规模 IT 和业务系统的主导方式。它不仅有助于松散和轻量级耦合和软件模块化,而且对于敏捷世界的持续集成和部署也是一个福音。对应用程序的任何更改都意味着必须进行大规模的更改。这一直是持续部署方面的一大障碍。微服务旨在解决这种情况,因此,微服务架构需要轻量级机制、小型、可独立部署的服务,并确保可扩展性和可移植性。这些要求可以通过 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-compose,您可以在单个文件中定义应用程序的组件(它们的容器、配置、链接、卷等),然后,您可以用一个命令启动所有内容,这样就可以使其运行起来。

这个工具通过提供一组内置工具来简化容器管理,以执行目前手动执行的许多工作。在本节中,我们提供了使用docker-compose执行容器编排的所有细节,以便拥有一系列下一代分布式应用程序。

使用 docker-compose 编排容器

在本节中,我们将讨论广泛使用的容器编排工具docker-composedocker-compose工具是一个非常简单但功能强大的工具,旨在简化运行一组 Docker 容器。换句话说,docker-compose是一个编排框架,可以定义和控制多容器服务。

它使您能够创建一个快速和隔离的开发环境,以及在生产环境中编排多个 Docker 容器的能力。docker-compose工具在内部利用 Docker 引擎来拉取镜像、构建镜像、按正确顺序启动容器,并根据docker-compose.yml文件中给定的定义在容器/服务之间进行正确的连接/链接。

安装 docker-compose

在撰写本书时,最新版本的docker-compose是 1.2.0,建议您将其与 Docker 1.3 或更高版本一起使用。您可以在 GitHub 位置(github.com/docker/compose/releases/latest)找到最新的官方发布的docker-compose

docker-compose版本 1.2.0 的 Linux x86-64 二进制文件可在github.com/docker/compose/releases/download/1.2.0/docker-compose-Linux-x86_64下载,您可以直接使用wget工具或curl工具进行安装,如下所示:

  • 使用wget工具:
$ sudo sh -c 'wget -qO-       https://github.com/docker/compose/releases/download/1.2.0/docker-compose-'uname -s'-'uname -m' >  /usr/local/bin/docker-compose; chmod +x /usr/local/bin/docker-compose'

  • 使用curl工具:
$ sudo sh -c 'curl  -sSL  https://github.com/docker/compose/releases/download/1.2.0/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,请在安装docker-compose之前安装pip包。

成功安装docker-compose后,您可以检查docker-compose的版本,如下所示:

$ docker-compose --version
docker-compose 1.2.0

docker-compose.yml 文件

docker-compose工具使用docker-compose.yml文件编排容器,在其中可以定义需要创建的服务、这些服务之间的关系以及它们的运行时属性。docker-compose.yml文件是YAML Ain't Markup LanguageYAML)格式文件,这是一种人类友好的数据序列化格式。默认的docker-compose文件是docker-compose.yml,可以使用docker-compose工具的-f选项进行更改。以下是docker-compose.yml文件的格式:

<service>:
   <key>: <value>
   <key>:
       - <value>
       - <value>

在这里,<service>是服务的名称。您可以在单个docker-compose.yml文件中有多个服务定义。服务名称后面应跟一个或多个键。但是,所有服务必须具有imagebuild键,后面可以跟任意数量的可选键。除了imagebuild键之外,其余的键可以直接映射到docker run子命令中的选项。值可以是单个值或多个值。

以下是docker-compose版本 1.2.0 中支持的键列表:

  • image:这是标签或镜像 ID

  • build:这是包含Dockerfile的目录路径

  • command:此键覆盖默认命令

  • links:此键链接到另一个服务中的容器

  • external_links:此键链接到由其他docker-compose.yml或其他方式(而不是docker-compose)启动的容器

  • ports:此键公开端口并指定端口HOST_port:CONTAINER_port

  • expose:此键公开端口,但不将其发布到主机

  • volumes:此键将路径挂载为卷

  • volumes_from:此键从另一个容器挂载所有卷

  • environment:此键添加环境变量,并使用数组或字典

  • env_file:此键将环境变量添加到文件

  • extends:这扩展了同一或不同配置文件中定义的另一个服务

  • net:这是网络模式,具有与 Docker 客户端--net选项相同的值

  • pid:这使主机和容器之间共享 PID 空间

  • dns:这设置自定义 DNS 服务器

  • cap_add:这会向容器添加一个功能

  • cap_drop:这会删除容器的某个功能

  • dns_search:这设置自定义 DNS 搜索服务器

  • working_dir:这会更改容器内的工作目录

  • entrypoint:这会覆盖默认的入口点

  • 用户: 这设置默认用户

  • 主机名: 这设置了容器的主机名

  • 域名: 这设置域名

  • mem_limit: 这限制内存

  • 特权: 这给予扩展权限

  • 重启: 这设置容器的重启策略

  • stdin_open: 这启用标准输入设施

  • tty: 这启用基于文本的控制,如终端

  • cpu_shares: 这设置 CPU 份额(相对权重)

docker-compose 命令

docker-compose工具提供了一些命令的复杂编排功能。所有docker-compose命令都使用docker-compose.yml文件作为一个或多个服务的编排基础。以下是docker-compose命令的语法:

docker-compose [<options>] <command> [<args>...]

docker-compose工具支持以下选项:

  • --verbose: 这显示更多输出

  • --版本: 这打印版本并退出

  • -f, --file <file>: 这指定docker-compose的替代文件(默认为docker-compose.yml文件)

  • -p, --project-name <name>: 这指定替代项目名称(默认为目录名称)

docker-compose工具支持以下命令:

  • 构建: 这构建或重建服务

  • 杀死: 这杀死容器

  • 日志: 这显示容器的输出

  • 端口: 这打印端口绑定的公共端口

  • ps: 这列出容器

  • 拉取: 这拉取服务镜像

  • rm: 这删除已停止的容器

  • 运行: 这运行一次性命令

  • 规模: 这为服务设置容器数量

  • 开始: 这启动服务

  • 停止: 这停止服务

  • 启动: 这创建并启动容器

常见用法

在本节中,我们将通过一个示例来体验 Docker-Compose 框架提供的编排功能的威力。为此,我们将构建一个接收您输入的 URL 并以相关响应文本回复的两层 Web 应用程序。该应用程序使用以下两个服务构建,如下所列:

  • Redis: 这是一个用于存储键和其关联值的键值数据库

  • Node.js: 这是一个用于实现 Web 服务器功能和应用逻辑的 JavaScript 运行环境

这些服务中的每一个都打包在两个不同的容器中,这些容器使用docker-compose工具进行组合。以下是服务的架构表示:

常见用法

在这个示例中,我们首先实现了example.js模块,这是一个node.js文件,用于实现 Web 服务器和键查找功能。接下来,我们将在与example.js相同的目录中编写Dockerfile,以打包node.js运行环境,然后使用与example.js相同的目录中的docker-compose.yml文件定义服务编排。

以下是example.js文件,它是一个简单的请求/响应 Web 应用程序的node.js实现。为了便于演示,在这段代码中,我们限制了buildkill docker-compose命令。为了使代码更加易懂,我们在代码之间添加了注释:

// 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 link name of the 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 helper\nEnter the docker-compose command in the URL for help\n", redis.print);
client.set("/build", "Build or rebuild services", redis.print);
client.set("/kill", "Kill contianers", 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 supported\n");
    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找到。

以下文本是Dockerfile的内容,该文件打包了node.js镜像、node.jsredis驱动程序和之前定义的example.js文件:

###############################################
# 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工具要执行的服务编排:

web:
  build: .
  command: node /myapp/example.js
  links:
   - 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 应用程序实现)作为参数来启动容器。该容器链接到redis容器,并且容器端口80映射到 Docker 主机的端口8080

  • 服务名为redis的服务被指示使用redis:latest镜像启动一个容器。如果该镜像不在 Docker 主机上,Docker 引擎将从中央仓库或私有仓库中拉取该镜像。

现在,让我们继续我们的示例,使用docker-compose build命令构建 Docker 镜像,使用docker-compose up命令启动容器,并使用浏览器连接以验证请求/响应功能,如下逐步解释:

  1. docker-compose命令必须从存储docker-compose.yml文件的目录中执行。docker-compose工具将每个docker-compose.yml文件视为一个项目,并且它假定项目名称来自docker-compose.yml文件的目录。当然,可以使用-p选项覆盖此设置。因此,作为第一步,让我们更改存储docker-compose.yml文件的目录:
$ cd ~/example

  1. 使用docker-compose build命令构建服务:
$ sudo docker-compose build

  1. 按照docker-compose.yml文件中指示的服务启动服务,使用docker-compose up命令:
$ sudo docker-compose up
Creating example_redis_1...
Pulling image redis:latest...
latest: Pulling from redis
21e4345e9035: Pull complete
. . . TRUNCATED OUTPUT . . .
redis:latest: The image you are pulling has been verified.
Important: image verification is a tech preview feature and should not be relied on to provide security.
Digest: sha256:dad98e997480d657b2c00085883640c747b04ca882d6da50760e038fce63e1b5
Status: Downloaded newer image for redis:latest
Creating example_web_1...
Attaching to example_redis_1, example_web_1
. . . TRUNCATED OUTPUT . . .
redis_1 | 1:M 25 Apr 18:12:59.674 * 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

由于目录名为exampledocker-compose工具假定项目名称为example

  1. 成功使用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_1example_web_1容器正在运行。容器名称以example_为前缀,这是docker-compose项目名称。

  1. 在 Docker 主机的不同终端上探索我们自己的请求/响应 Web 应用程序的功能,如下所示:
$ curl http://0.0.0.0:8080
Welcome to Docker-Compose helper
Enter the docker-compose command in the URL for help
$ curl http://0.0.0.0:8080/build
Build or rebuild services
$ curl http://0.0.0.0:8080/something
Command: something not supported

注意

在这里,我们直接使用http://0.0.0.0:8080连接到 Web 服务,因为 Web 服务绑定到 Docker 主机的端口8080

很酷,不是吗?凭借极少的努力和docker-compose.yml文件的帮助,我们能够将两个不同的服务组合在一起并提供一个复合服务。

总结

本章已纳入书中,以提供有关无缝编排多个容器的所有探查和指定细节。我们广泛讨论了容器编排的需求以及使我们能够简化和流畅进行容器编排日益复杂过程的工具。为了证实编排如何在打造企业级容器中方便和有用,并且为了说明编排过程,我们采用了通过一个简单的例子来解释整个范围的广泛做法。我们开发了一个网络应用并将其包含在一个标准容器中。同样,我们使用了一个数据库容器,它是前端网络应用的后端,并且数据库在另一个容器中执行。我们看到如何通过 Docker 引擎的容器链接功能,使用不同的技术使网络应用容器意识到数据库。我们使用了开源工具(docker-compose)来实现这一目的。

在下一章中,我们将讨论 Docker 如何促进软件测试,特别是通过一些实用的例子进行集成测试。

第九章:使用 Docker 进行测试

毫无疑问,测试的特征一直处于软件工程学科的前沿。人们普遍认为,如今软件已经深入并决定性地渗透到我们日常环境中的各种有形物体中,以便拥有大量智能、连接和数字化的资产。此外,随着对分布式和同步软件的高度关注,软件设计、开发、测试和调试、部署以及交付的复杂性不断攀升。正在发现手段和机制来简化和优化软件构建的必要自动化,以及对软件可靠性、弹性和可持续性的认证。Docker 正成为测试各种软件应用的极其灵活的工具。在本章中,我们将讨论如何有效地利用值得注意的 Docker 进展进行软件测试,以及它在加速和增强测试自动化方面的独特优势。

本章讨论以下主题:

  • 测试驱动开发(TDD)的简要概述

  • 在 Docker 中测试您的代码

  • 将 Docker 测试过程集成到 Jenkins 中

新兴情况是,Docker 容器被利用来创建开发和测试环境,这些环境与生产环境完全相同。与虚拟机相比,容器需要更少的开销,虚拟机一直是开发、分级和部署环境的主要环境。让我们从下一代软件的测试驱动开发概述开始,以及 Docker 启发的容器化如何简化 TDD 过程。

测试驱动开发的简要概述

软件开发的漫长而艰难的旅程在过去的几十年里经历了许多转折,而其中一种突出的软件工程技术无疑是 TDD。关于 TDD 的更多细节和文档请参见agiledata.org/essays/tdd.html

简而言之,测试驱动开发,也被称为 TDD,是一种软件开发实践,其中开发周期始于编写一个会失败的测试用例,然后编写实际的软件使测试通过,并继续重构和重复这个周期,直到软件达到可接受的水平。这个过程在下面的图表中描述了:

测试驱动开发的简要概述

在 Docker 中测试您的代码

在本节中,我们将带您进行一次旅程,向您展示如何使用存根进行 TDD,并且 Docker 如何在开发软件中变得方便。为此,我们以一个具有跟踪每个用户访问次数功能的 Web 应用程序用例为例。在这个例子中,我们使用 Python 作为实现语言,redis作为键值对数据库来存储用户的点击次数。此外,为展示 Docker 的测试能力,我们将我们的实现限制在只有两个功能:hitgetHit

注意

注意:本章中的所有示例都使用python3作为运行环境。ubuntu 14.04安装默认带有python3。如果您的系统上没有安装python3,请参考相应的手册安装python3

根据 TDD 实践,我们首先为hitgetHit功能添加单元测试用例,如下面的代码片段所示。在这里,测试文件的名称为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中实现hitgetHit函数,如下所示:

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

注意:要继续进行此示例,您必须具有与pip3兼容的python3版本的软件包安装程序。

以下命令用于安装pip3

$ wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 - 

在此程序的第一行中,我们导入了redis驱动程序,这是redis数据库的连接驱动程序。在接下来的一行中,我们将连接到redis数据库,然后我们将继续实现hitgetHit函数。

redis驱动程序是一个可选的 Python 模块,因此让我们继续使用 pip 安装程序安装redis驱动程序,如下所示:

$ sudo pip3 install redis 

即使安装了redis驱动程序,我们的unittest仍然会失败,因为我们尚未运行redis数据库服务器。因此,我们可以运行redis数据库服务器以成功完成我们的单元测试,或者采用传统的 TDD 方法来模拟redis驱动程序。模拟是一种测试方法,其中复杂的行为被预定义或模拟的行为替代。在我们的示例中,为了模拟 redis 驱动程序,我们将利用一个名为 mockredis 的第三方 Python 包。这个模拟包可以在github.com/locationlabs/mockredis找到,pip安装程序的名称是mockredispy。让我们使用 pip 安装这个模拟:

$ sudo pip3 install mockredispy 

安装了mockredispyredis模拟器之后,让我们重构我们之前编写的测试代码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 容器中并在容器内执行测试的逐步过程:

  1. 创建一个Dockerfile来构建一个带有python3运行时、redismockredispy包、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

  1. 现在在我们制作Dockerfile的目录中创建一个名为src的目录。将test_hitcount.pyhitcount.py文件移动到新创建的src目录中。

  2. 使用docker build子命令构建hit_unittest Docker 镜像:

$ 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 

  1. 现在我们已经成功构建了镜像,让我们使用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 容器用作非污染但非常强大的运行时环境的逐步说明:

  1. 我们开始启动 Python 运行时交互式容器,使用docker run子命令:
$ sudo docker run -it \ 
 -v /home/peter/src/hitcount:/src \ 
 python:latest /bin/bash 

在这个例子中,/home/peter/src/hitcount Docker 主机目录被标记为源代码和测试文件的占位符。该目录在容器中被挂载为/src

  1. 现在,在 Docker 主机的另一个终端上,将test_hitcount.py测试文件和访客计数实现hitcount.py复制到/home/peter/src/hitcount目录中。

  2. 切换到 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 库。

  1. 继续安装mockredispy pip包,因为前一步失败了,无法在运行时环境中找到mockredis库:
root@a8219ac7ed8e:/src# pip install mockredispy 

  1. 重新运行 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驱动程序。

  1. 继续使用 pip 安装程序安装redis驱动程序,如下所示:
root@a8219ac7ed8e:/src# pip install redis 

  1. 成功安装了redis驱动程序后,让我们再次运行单元测试:
root@a8219ac7ed8e:/src# python3 -m unittest 
. 
----------------------------------------------------------------- 
Ran 1 test in 0.000s 

OK 

显然,这次单元测试通过了,没有警告或错误消息。

  1. 现在我们有一个足够好的运行时环境来运行我们的测试用例。最好将这些更改提交到 Docker 镜像以便重用,使用docker commit子命令:
$ sudo docker commit a8219ac7ed8e python_rediswithmock 
fcf27247ff5bb240a935ec4ba1bddbd8c90cd79cba66e52b21e1b48f984c7db2 

从现在开始,我们可以使用python_rediswithmock镜像来启动新的容器进行 TDD。

在本节中,我们生动地阐述了如何将 Docker 容器作为测试环境的方法,同时通过在容器内隔离和限制运行时依赖项,保持 Docker 主机的完整性和纯洁性。

将 Docker 测试集成到 Jenkins 中

在上一节中,我们阐述了关于软件测试的激动人心的基础,如何利用 Docker 技术进行软件测试,以及在测试阶段容器技术的独特优势。在本节中,我们将介绍为了使用 Docker 准备 Jenkins 环境所需的步骤,然后演示如何扩展 Jenkins 以集成和自动化使用 Docker 进行测试,使用众所周知的点击计数用例。

准备 Jenkins 环境

在本节中,我们将带您完成安装jenkins、Jenkins 的 GitHub 插件和git以及修订控制工具的步骤。这些步骤如下:

  1. 我们首先要添加 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 为两者提供了一个通用的软件包。

  1. 将 Debian 软件包位置添加到apt软件包源列表中,如下所示:
$ sudo sh -c \ 
 'echo deb http://pkg.jenkins-ci.org/debian binary/ > \ 
 /etc/apt/sources.list.d/jenkins.list' 

  1. 添加了软件包源后,继续运行apt-get命令更新选项,以重新同步来自源的软件包索引:
$ sudo apt-get update 

  1. 现在,使用apt-get命令安装选项来安装jenkins,如下所示:
$ sudo apt-get install jenkins 

  1. 最后,使用service命令激活jenkins服务:
$ sudo service jenkins start 

  1. jenkins服务可以通过任何 Web 浏览器访问,只需指定安装了 Jenkins 的系统的 IP 地址(10.1.1.13)。Jenkins 的默认端口号是8080。以下截图是Jenkins的入口页面或仪表板准备 Jenkins 环境

  2. 在本例中,我们将使用 GitHub 作为源代码存储库。Jenkins 默认不支持 GitHub,因此需要安装 GitHub 插件。在安装过程中,有时 Jenkins 不会填充插件可用性列表,因此您必须强制它下载可用插件列表。您可以通过执行以下步骤来实现:

  3. 在屏幕左侧选择管理 Jenkins,这将带我们到管理 Jenkins页面,如下面的屏幕截图所示:准备 Jenkins 环境

  4. 管理 Jenkins页面上,选择管理插件,这将带我们到插件管理器页面,如下面的屏幕截图所示:准备 Jenkins 环境

  5. 插件管理器页面上,选择高级选项卡,转到页面底部,您将在页面右下角找到立即检查按钮。单击立即检查按钮开始插件更新。或者,您可以通过导航到http://<jenkins-server>:8080/pluginManager/advanced直接转到高级页面上的立即检查按钮,其中<jenkins-server>是安装 Jenkins 的系统的 IP 地址。

注意

注意:如果 Jenkins 没有更新可用的插件列表,很可能是镜像站点的问题,因此使用有效的镜像 URL 修改更新站点字段。

  1. 更新了可用插件列表后,让我们继续安装 GitHub 插件,如下面的子步骤所示:

  2. 插件管理器页面中选择可用选项卡,其中将列出所有可用的插件。

  3. 输入GitHub 插件作为过滤器,这将只列出 GitHub 插件,如下面的屏幕截图所示:准备 Jenkins 环境

  4. 选择复选框,然后单击立即下载并在重启后安装。您将进入一个屏幕,显示插件安装的进度:准备 Jenkins 环境

  5. 在所有插件成功下载后,继续使用http://< jenkins-server >:8080/restart重新启动 Jenkins,其中<jenkins-server>是安装 Jenkins 的系统的 IP 地址。

  6. 确保安装了git软件包,否则使用apt-get命令安装git软件包:

$ sudo apt-get install git 

  1. 到目前为止,我们一直在使用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 

  1. 重新启动jenkins服务,以使组更改生效,使用以下命令:
$ sudo service jenkins restart 
 * Restarting Jenkins Continuous Integration Server jenkins              [ OK ] 

我们已经设置了一个 Jenkins 环境,现在能够自动从github.com存储库中拉取最新的源代码,将其打包为 Docker 镜像,并执行规定的测试场景。

自动化 Docker 测试流程

在本节中,我们将探讨如何使用 Jenkins 和 Docker 自动化测试。如前所述,我们将使用 GitHub 作为我们的存储库。我们已经将我们之前示例的Dockerfiletest_hitcount.pyhitcount.py文件上传到 GitHub 上的github.com/thedocker/testing,我们将在接下来的示例中使用它们。但是,我们强烈建议您在github.com上设置自己的存储库,使用您可以在github.com/thedocker/testing找到的分支选项,并在接下来的示例中替换此地址。

以下是自动化 Docker 测试的详细步骤:

  1. 配置 Jenkins 在 GitHub 存储库中的文件修改时触发构建,如下面的子步骤所示:

  2. 再次连接到 Jenkins 服务器。

  3. 选择新项目创建新作业自动化 Docker 测试流程

  4. 在下一个截图中,为项目命名(例如Docker-Testing),并选择自由风格项目单选按钮:自动化 Docker 测试流程

  5. 在下一个截图中,在源代码管理下选择Git单选按钮,并在存储库 URL文本字段中指定 GitHub 存储库 URL:自动化 Docker 测试流程

  6. 构建触发器下选择轮询 SCM,以便每15分钟间隔进行 GitHub 轮询。在计划文本框中输入以下代码H/15 * * * *,如下面的屏幕截图所示。为了测试目的,您可以缩短轮询间隔:自动化 Docker 测试流程

  7. 向下滚动屏幕,然后在构建下选择添加构建步骤按钮。在下拉列表中,选择执行 shell并输入以下文本,如下面的屏幕截图所示:自动化 Docker 测试流程

  8. 最后,通过点击保存按钮保存配置。

  9. 返回 Jenkins 仪表板,您可以在仪表板上找到您的测试:自动化 Docker 测试流程

  10. 您可以等待 Jenkins 计划启动构建,也可以点击屏幕右侧的时钟图标立即启动构建。一旦构建完成,仪表板将更新构建状态为成功或失败,并显示构建编号:自动化 Docker 测试流程

  11. 如果将鼠标悬停在构建编号附近,将会出现一个下拉按钮,其中包括更改控制台输出等选项,如下面的屏幕截图所示:自动化 Docker 测试流程

  12. 控制台输出选项将显示构建的详细信息,如下所示:

Started by user anonymous 
Building in workspace /var/lib/jenkins/jobs/Docker-Testing/workspace 
Cloning the remote Git repository 
Cloning repository https://github.com/thedocker/testing/ 
. . . OUTPUT TRUNCATED . . . 
+ docker build -t docker_testing_using_jenkins . 
Sending build context to Docker daemon 121.9 kB 

Sending build context to Docker daemon 
Step 0 : FROM python:latest 
. . . OUTPUT TRUNCATED . . . 
Successfully built ad4be4b451e6 
+ docker run --rm docker_testing_using_jenkins 
. 
---------------------------------------------------------------------- 
Ran 1 test in 0.000s 

OK 
Finished: SUCCESS 

  1. 显然,测试失败是因为错误的模块名error_hitcount,这是我们故意引入的。现在,让我们故意在test_hitcount.py中引入一个错误,观察对 Jenkins 构建的影响。由于我们已经配置了 Jenkins,它会忠实地轮询 GitHub 并启动构建:自动化 Docker 测试流程

显然,构建失败了,正如我们预期的那样。

  1. 最后一步,打开失败构建的控制台输出
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 的开放性和灵活性使其能够与其他技术无缝地配合,从而大大减少测试时间和复杂性。

长期以来,测试软件系统的主要方法包括模拟、依赖注入等。通常,这些方法需要在代码中创建许多复杂的抽象。目前的做法是针对应用程序开发和运行测试用例实际上是在存根上进行,而不是在完整的应用程序上进行。也就是说,通过容器化工作流,很可能对具有所有依赖关系的真实应用程序容器进行测试。因此,Docker 范式的贡献,特别是对测试现象和阶段的贡献,近来正在被认真阐述和记录。确切地说,软件工程领域正在朝着 Docker 空间的所有创新迈进,迎来智能和更加晴朗的日子。

在本章中,我们清楚地阐述和解释了使用受 Docker 启发的容器化范式的集成应用程序的强大测试框架。对于敏捷世界来说,经过验证的 TDD 方法被坚持为高效的软件构建和维护方法。本章利用 Python 单元测试框架来说明 TDD 方法是软件工程的开创性工具。单元测试框架被调整为高效、优雅的容器化,并且 Docker 容器与 Jenkins 无缝集成,后者是持续交付的现代部署工具,并且是敏捷编程世界的重要组成部分,正如本章所描述的。Docker 容器源代码在进入 GitHub 代码存储库之前经过预检。Jenkins 工具从 GitHub 下载代码并在容器内运行测试。在下一章中,我们将深入探讨并描述容器技术和各种调试工具和技术的理论方面。

第十章:调试容器

调试一直是软件工程领域的艺术组成部分。各种软件构建模块个别以及集体都需要经过软件开发和测试专业人员深入而决定性的调查流程,以确保最终软件应用程序的安全性和安全性。由于 Docker 容器被认为是下一代关键运行时环境,用于使命关键的软件工作负载,因此对于容器、制作者和作曲家来说,进行容器的系统和明智的验证和验证是相关和至关重要的。

本章专门为技术人员撰写,旨在为他们提供所有正确和相关的信息,以便精心调试在容器内运行的应用程序和容器本身。在本章中,我们将从理论角度探讨作为容器运行的进程的进程隔离方面。Docker 容器在主机上以用户级进程运行,通常具有与操作系统提供的隔离级别相同的隔离级别。随着 Docker 1.5 的发布,许多调试工具可供使用,可以有效地用于调试应用程序。我们还将介绍主要的 Docker 调试工具,如 Docker execstatspstopeventslogs。最后,我们将介绍nsenter工具,以便登录到容器而无需运行Secure ShellSSH)守护程序。

本章将涵盖的主题列表如下:

  • Docker 容器的进程级隔离

  • 调试容器化应用程序

  • 安装和使用nsenter

Docker 容器的进程级隔离

在虚拟化范式中,hypervisor 模拟计算资源并提供一个虚拟化环境,称为 VM,用于在其上安装操作系统和应用程序。而在容器范式的情况下,单个系统(裸机或虚拟机)被有效地分区,以便同时运行多个服务而互不干扰。为了防止它们相互干扰,这些服务必须相互隔离,以防止它们占用对方的资源或产生依赖冲突(也称为依赖地狱)。Docker 容器技术基本上通过利用 Linux 内核构造(如命名空间和 cgroups,特别是命名空间)实现了进程级别的隔离。Linux 内核提供了以下五个强大的命名空间,用于将全局系统资源相互隔离。这些是用于隔离进程间通信资源的进程间通信IPC)命名空间:

  • 网络命名空间用于隔离网络资源,如网络设备、网络堆栈、端口号等

  • 挂载命名空间隔离文件系统挂载点

  • PID 命名空间隔离进程标识号

  • 用户命名空间用于隔离用户 ID 和组 ID

  • UTS 命名空间用于隔离主机名和 NIS 域名

当我们必须调试容器内运行的服务时,这些命名空间会增加额外的复杂性,我们将在下一章节中更详细地学习。

在本节中,我们将讨论 Docker 引擎如何通过一系列实际示例利用 Linux 命名空间提供进程隔离,其中之一列在此处:

  1. 首先,通过使用docker run子命令以交互模式启动一个ubuntu容器,如下所示:
$ sudo docker run -it --rm ubuntu /bin/bash
root@93f5d72c2f21:/#

  1. 继续在不同的终端中使用docker inspect子命令查找前面容器93f5d72c2f21的进程 ID:
$ sudo docker inspect \
 --format "{{ .State.Pid }}" 93f5d72c2f21
2543

显然,从前面的输出中,容器93f5d72c2f21的进程 ID 是2543

  1. 得到容器的进程 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进程。

  1. 让我们再进一步,使用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,也是我们之前启动的容器的主机名。

  1. 现在,让我们回到终端,我们正在运行交互式容器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。此外,父进程 IDPPID)为0(零)。

在 Linux 世界中,每个系统只有一个 PID 为 1 且 PPID 为 0 的根进程,这是该系统完整进程树的根。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:14.04            "/bin/bash"

所以这里有三个接口,每个容器一个。让我们通过运行以下命令来获取它们的详细信息:

$ ifconfig
veth2d99bd3 Link encap:Ethernet  HWaddr 42:b2:cc:a5:d8:f3
 inet6 addr: fe80::40b2:ccff:fea5:d8f3/64 Scope:Link
 UP BROADCAST RUNNING  MTU:9001  Metric:1
veth422c684 Link encap:Ethernet  HWaddr 02:84:ab:68:42:bf
 inet6 addr: fe80::84:abff:fe68:42bf/64 Scope:Link
 UP BROADCAST RUNNING  MTU:9001  Metric:1
vethc359aec Link encap:Ethernet  HWaddr 06:be:35:47:0a:c4
 inet6 addr: 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 / rootfs rw 0 0/dev/mapper/docker-202:1-149807 871da6a6cf4320f625d5c96cc24f657b7b231fe89774e09fc771b3684bf405fb / ext4 rw,relatime,discard,stripe=16,data=ordered 0 0 proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0

让我们运行一个带有挂载点的容器,作为存储区域网络SAN)或网络附加存储NAS)设备,并通过登录到容器来访问它。这是给您的一个练习。我在工作中的一个项目中实现了这一点。

这些容器/进程可以被隔离到其他命名空间中,包括用户、IPC 和 UTS。用户命名空间允许您在命名空间内拥有根权限,而不会将该特定访问权限授予命名空间外的进程。使用 IPC 命名空间隔离进程会为其提供自己的进程间通信资源,例如 System V IPC 和 POSIX 消息。UTS 命名空间隔离系统的主机名

Docker 使用clone系统调用实现了这个命名空间。在主机上,您可以检查 Docker 为容器创建的命名空间(带有pid 3728):

$ sudo ls /proc/3728/ns/
ipc  mnt  net  pid  user  uts

在大多数 Docker 的工业部署中,人们广泛使用经过修补的 Linux 内核来满足特定需求。此外,一些公司已经修补了他们的内核,以将任意进程附加到现有的命名空间,因为他们认为这是部署、控制和编排容器最方便和可靠的方式。

控制组

Linux 容器依赖于控制组(cgroups),它们不仅跟踪进程组,还公开 CPU、内存和块 I/O 使用情况的指标。您还可以访问这些指标,并获取网络使用情况指标。控制组是 Linux 容器的另一个重要组件。控制组已经存在一段时间,并最初是在 Linux 内核代码 2.6.24 中合并的。它们确保每个 Docker 容器都将获得固定数量的内存、CPU 和磁盘 I/O,以便任何容器都无法在任何情况下使主机机器崩溃。控制组不会阻止访问一个容器,但它们对抵御一些拒绝服务DoS)攻击至关重要。

在 Ubuntu 14.04 上,cgroup实现在/sys/fs/cgroup路径中。Docker 的内存信息可在/sys/fs/cgroup/memory/docker/路径下找到。

类似地,CPU 详细信息可以在/sys/fs/cgroup/cpu/docker/路径中找到。

让我们找出容器(41668be6e513e845150abd2dd95dd574591912a7fda947f6744a0bfdb5cd9a85)可以消耗的最大内存限制。

为此,您可以转到cgroup内存路径,并检查memory.max.usage文件:

/sys/fs/cgroup/memory/docker/41668be6e513e845150abd2dd95dd574591912a7fda947f6744a0bfdb5cd9a85
$ cat memory.max_usage_in_bytes
13824000

因此,默认情况下,任何容器只能使用最多 13.18 MB 的内存。

类似地,CPU 参数可以在以下路径中找到:

/sys/fs/cgroup/cpu/docker/41668be6e513e845150abd2dd95dd574591912a7fda947f6744a0bfdb5cd9a85

传统上,Docker 在容器内部只运行一个进程。因此,通常情况下,您会看到人们为 PHP、nginx 和 MySQL 分别运行三个容器。然而,这是一个谬论。您可以在单个容器内运行所有三个进程。

Docker 在不具备 root 权限的情况下,隔离了容器中运行的应用程序与底层主机的许多方面。然而,这种分离并不像虚拟机那样强大,虚拟机在 hypervisor 之上独立运行独立的操作系统实例,而不与底层操作系统共享内核。在同一主机上以容器化应用程序的形式运行具有不同安全配置文件的应用程序并不是一个好主意,但将不同的应用程序封装到容器化应用程序中具有安全性的好处,否则这些应用程序将直接在同一主机上运行。

调试容器化应用程序

计算机程序(软件)有时无法按预期行为。这是由于错误的代码或由于开发、测试和部署系统之间的环境变化。Docker 容器技术通过将所有应用程序依赖项容器化,尽可能消除开发、测试和部署之间的环境问题。尽管如此,由于错误的代码或内核行为的变化,仍可能出现异常,需要进行调试。调试是软件工程世界中最复杂的过程之一,在容器范式中变得更加复杂,因为涉及到隔离技术。在本节中,我们将学习使用 Docker 本机工具以及外部提供的工具来调试容器化应用程序的一些技巧和窍门。

最初,Docker 社区中的许多人单独开发了自己的调试工具,但后来 Docker 开始支持本机工具,如exectoplogsevents等。在本节中,我们将深入探讨以下 Docker 工具:

  • exec

  • ps

  • top

  • stats

  • events

  • logs

Docker exec 命令

docker exec命令为部署自己的 Web 服务器或在后台运行的其他应用程序的用户提供了非常需要的帮助。现在,不需要登录到容器中运行 SSH 守护程序。

首先,运行docker ps -a命令以获取容器 ID:

$ sudo docker ps -a
b34019e5b5ee        nsinit:latest             "make local"
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 在目标容器中生成一个新进程。因此,如果你在目标容器内运行 pe -aef 命令,结果如下:

# ps -aef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 Mar22 ?        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
0 R     0    34     1  0  80   0 -  1783 -      ?        00:00:00 ps
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 命令提供有关 CPU、内存和交换使用情况的信息,如果你在 Docker 容器内运行它:

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 1.5 版本中,Docker 为您提供了对容器统计信息的只读访问权限。这将简化容器的 CPU、内存、网络 IO 和块 IO。这有助于您选择资源限制,以及进行性能分析。Docker stats 实用程序仅为正在运行的容器提供这些资源使用详细信息。您可以使用端点 API 在docs.docker.com/reference/api/docker_remote_api_v1.17/#inspect-a-container获取详细信息。

Docker stats 最初是从 Michael Crosby 的代码贡献中获取的,可以在github.com/crosbymichael上访问。

Docker 事件命令

Docker 容器将报告以下实时事件:createdestroydieexportkillommpauserestartstartstopunpause。以下是一些示例,说明如何使用这些命令:

$ 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/tcp   sad_sammet

Docker 镜像还将报告取消标记和删除事件。

使用多个过滤器将被视为AND操作;例如,--filter container= a245253db38b --filter event=start将显示容器a245253db38b的事件和事件类型为 start 的事件。

目前,支持的过滤器有 container、event 和 image。

Docker 日志命令

此命令获取容器的日志,而无需登录到容器中。它批量检索执行时存在的日志。这些日志是STDOUTSTDERR的输出。通用用法显示在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实用程序,以查看我们的容器的日志。

安装和使用 nsenter

在任何商业 Docker 部署中,您可能会使用各种容器,如 Web 应用程序、数据库等。但是,您需要访问这些容器以修改配置或调试/排除故障。这个问题的一个简单解决方案是在每个容器中运行一个 SSH 服务器。这不是一个访问机器的好方法,因为会带来意想不到的安全影响。然而,如果您在 IBM、戴尔、惠普等世界一流的 IT 公司工作,您的安全合规人员绝不会允许您使用 SSH 连接到机器。

所以,这就是解决方案。nsenter工具为您提供了登录到容器的访问权限。请注意,nsenter将首先作为 Docker 容器部署。使用部署的nsenter,您可以访问您的容器。按照以下步骤进行:

  1. 让我们运行一个简单的 Web 应用程序作为一个容器:
$ sudo docker run -d -p 5000:5000 training/webapp python app.py
------------------------
a245253db38b626b8ac4a05575aa704374d0a3c25a392e0f4f562df92bb98d74

  1. 测试 Web 容器:
$ curl localhost:5000
Hello world!

  1. 安装nsenter并将其作为一个容器运行:
$ sudo docker run -v /usr/local/bin:/target jpetazzo/nsenter

现在,nsenter作为一个容器正在运行。

  1. 使用 nsenter 容器登录到我们在步骤 1 中创建的容器(a245253db38b)。

运行以下命令以获取PID值:

$ PID=$(sudo docker inspect --format {{.State.Pid}} a245253db38b)

  1. 现在,访问 Web 容器:
$ sudo nsenter --target $PID --mount --uts --ipc --net --pid
root@a245253db38b:/#

然后,您可以登录并开始访问您的容器。以这种方式访问您的容器将使您的安全和合规专业人员感到满意,他们会感到放松。

自 Docker 1.3 以来,Docker exec 是一个支持的工具,用于登录到容器中。

nsenter工具不进入 cgroups,因此规避了资源限制。这样做的潜在好处是调试和外部审计,但对于远程访问,docker exec是当前推荐的方法。

nsenter工具仅在 Intel 64 位平台上进行测试。您不能在要访问的容器内运行nsenter,因此只能在主机上运行nsenter。通过在主机上运行nsenter,您可以访问该主机上的所有容器。此外,您不能使用在特定主机 A 上运行的nsenter来访问主机 B 上的容器。

总结

Docker 利用 Linux 容器技术(如 LXC 和现在的libcontainer)为您提供容器的隔离。Libcontainer 是 Docker 在 Go 编程语言中的自己的实现,用于访问内核命名空间和控制组。这个命名空间用于进程级别的隔离,而控制组用于限制运行容器的资源使用。由于容器作为独立进程直接在 Linux 内核上运行,因此通常可用GA)的调试工具不足以在容器内部工作以调试容器化的进程。Docker 现在为您提供了丰富的工具集,以有效地调试容器,以及容器内部的进程。Docker exec 将允许您登录到容器,而无需在容器中运行 SSH 守护程序。

Docker stats 提供有关容器内存和 CPU 使用情况的信息。Docker events 报告事件,比如创建、销毁、杀死等。同样,Docker logs 从容器中获取日志,而无需登录到容器中。

调试是可以用来制定其他安全漏洞和漏洞的基础。因此,下一章将详细阐述 Docker 容器的可能安全威胁,以及如何通过各种安全方法、自动化工具、最佳实践、关键指南和指标来抑制这些威胁。

第十一章:保护 Docker 容器

到目前为止,我们在本书中已经谈了很多关于快速兴起的 Docker 技术。如果不详细阐述 Docker 特定的安全问题和解决方法,这本书就不会有一个完美的结局。因此,本章是专门为了向您详细解释 Docker 启发的容器的不断增长的安全挑战而精心制作和纳入本书的。我们还希望更多地阐明,通过一系列开创性技术、高质量算法、启用工具和最佳实践,如何解决悬而未决的安全问题。

在本章中,我们将详细讨论以下主题:

  • Docker 容器安全吗?

  • 容器的安全特性

  • 新兴的安全方法

  • 容器安全的最佳实践

确保任何 IT 系统和业务服务的不可破坏和无法渗透的安全性,是 IT 领域数十年来的主要需求和主要挑战之一。聪明的头脑可以识别和利用在系统构思和具体化阶段被漫不经心和无意识引入的各种安全漏洞和缺陷。这个漏洞最终在 IT 服务交付过程中带来无数的违规和破坏。另一方面,安全专家和工程师尝试各种技巧和技术,以阻止黑客的邪恶行程。然而,到目前为止,这并不是一场彻底的胜利。在各个地方,都有一些来自未知来源的引人注目的入侵,导致高度令人不安的 IT 减速,有时甚至崩溃。因此,全球各个组织和政府正在大力投资于安全研究工作,以完全消灭所有与安全和安全相关的事件和事故。

为了最大程度地减少安全威胁和漏洞对 IT 系统造成的不可挽回和难以描述的后果,有大量专门的安全产品供应商和托管安全服务提供商。确切地说,对于任何现有和新兴的技术来说,安全性都是最关键和最重要的方面,不能轻视。

Docker 是 IT 领域快速成熟的容器化技术,最近,安全方面被赋予了首要重要性,考虑到 Docker 容器的采用和适应性不断上升。此外,一系列特定目的和通用容器正在进入生产环境,因此安全难题具有特殊意义。毫无疑问,未来 Docker 平台发布将会有很多关注安全参数的内容,因为这个开源 Docker 倡议的市场份额和思想份额一直在上升。

Docker 容器安全吗?

随着 Docker 容器在生产 IT 环境中受到精心评估,不同领域对容器的安全漏洞提出了质疑。因此,有人呼吁研究人员和安全专家大力加强容器安全,以提高服务提供商和消费者的信心。在本节中,我们将描述 Docker 容器在安全方面的立场。由于容器正在与虚拟机同步进行密切审查,我们将从几个与虚拟机和容器相关的安全要点开始。

安全方面 - 虚拟机与 Docker 容器

让我们从理解虚拟机与容器的区别开始。通常,虚拟机是笨重的,因此臃肿,而容器是轻量级的,因此苗条而时尚。

以下表格概括了虚拟机和容器的著名特性:

虚拟机 容器
几个虚拟机可以在单个物理机上运行(低密度)。 几十个容器可以在单个物理或虚拟机上运行(高密度)。
这确保了虚拟机的完全隔离以确保安全。 这使得在进程级别进行隔离,并使用命名空间和 cgroups 等功能提供额外的隔离。
每个虚拟机都有自己的操作系统,物理资源由底层的 hypervisor 管理。 容器与其 Docker 主机共享相同的内核。
对于网络,虚拟机可以连接到虚拟或物理交换机。Hypervisors 具有用于 I/O 性能改进的缓冲区,NIC 绑定等。容器利用标准的 IPC 机制,如信号,管道,套接字等进行网络连接。每个容器都有自己的网络堆栈。

下图清楚地说明了成熟的虚拟化范式和快速发展的容器化理念之间的结构差异。

安全方面-虚拟机与 Docker 容器

关于 VM 和容器安全方面的辩论正在加剧。有人支持其中一种,也有人反对。前面的图表帮助我们可视化、比较和对比了两种范式中的安全影响。

在虚拟化范式中,hypervisors 是虚拟机的集中和核心控制器。对于新提供的虚拟机的任何访问都需要通过这个 hypervisor 解决方案,它是任何未经身份验证、未经授权和不道德目的的坚实墙。因此,与容器相比,虚拟机的攻击面更小。必须破解或攻破 hypervisor 才能影响其他虚拟机。

与虚拟化范式相比,容器直接放置在主机系统的内核之上。这种精简高效的架构大大提高了效率,因为它完全消除了 hypervisor 的仿真层,并且提供了更高的容器密度。然而,与虚拟机范式不同,容器范式没有太多的层,因此如果任何一个容器受到损害,就可以轻松地访问主机和其他容器。因此,与虚拟机相比,容器的攻击面更大。

然而,Docker 平台的设计者已经充分考虑了这种安全风险,并设计了系统来阻止大多数安全风险。在接下来的部分中,我们将讨论系统中固有设计的安全性,所提出的大幅增强容器安全性的解决方案,以及最佳实践和指南。

容器的安全特性

Linux 容器,特别是 Docker 容器,具有一些有趣的固有安全功能。因此,容器化运动在安全方面是受到了良好的保护。在本节中,我们将详细讨论这些与安全相关的功能。

Docker 平台提倡分层安全方法,以为容器带来更果断和灵巧的安全性,如下图所示:

容器的安全功能

讨论中,Docker 使用了一系列安全屏障来阻止入侵。也就是说,如果一个安全机制被破坏,其他机制会迅速阻止容器被黑客攻击。在评估 Docker 容器的安全影响时,有一些关键领域需要进行检查。

资源隔离

众所周知,容器被定位为微服务架构时代的产物。也就是说,在单个系统中,可以有多个通用的、以及特定目的的服务,它们动态地相互协作,实现易于维护的分布式应用程序。随着物理系统中服务的多样性和异构性不断增加,安全复杂性必然会上升。因此,资源需要明确定界并隔离,以避免任何危险的安全漏洞。被广泛接受的安全方法是利用命名空间的内核特性。

内核命名空间为 Linux 容器提供了必要的隔离功能。Docker 项目为 Docker 容器添加了一些额外的命名空间,容器的每个独立方面都在自己的命名空间中运行,因此无法在外部访问。以下是 Docker 使用的命名空间列表:

  • PID 命名空间:用于一系列操作,以实现进程级别的隔离

  • 网络命名空间:用于对网络接口进行执行控制

  • IPC 命名空间:用于控制对 IPC 资源的访问

  • 挂载命名空间:用于管理挂载点

  • UTS 命名空间:用于隔离内核和版本标识符

内核命名空间提供了首要的隔离形式。在一个容器中运行的进程不会影响在另一个容器或主机系统中运行的进程。网络命名空间确保每个容器都有自己的网络堆栈,从而限制对其他容器接口的访问。从网络架构的角度来看,给定 Docker 主机上的所有容器都位于桥接接口上。这意味着它们就像连接到共同以太网交换机的物理机器一样。

资源会计和控制

容器消耗不同的物理资源以提供其独特的功能。然而,资源消耗必须受到纪律、有序和严格的监管。一旦出现偏差,容器执行其分配的任务的可能性就会更大。例如,如果资源使用没有系统地同步,就会导致拒绝服务(DoS)攻击。

Linux 容器利用控制组(cgroups)来实现资源会计和审计,以便以无摩擦的方式运行应用程序。众所周知,有多种资源有助于成功运行容器。它们提供了许多有用的指标,并确保每个容器都能公平地分享内存、CPU 和磁盘 I/O。

此外,它们保证单个容器不能通过耗尽任何一个资源来使系统崩溃。这个特性有助于抵御一些 DoS 攻击。这个特性有助于在云环境中以多租户身份运行容器,以确保它们的正常运行和性能。任何其他容器的任何利用都会被及时识别和制止,以避免任何不良事件的发生。

根权限-影响和最佳实践

Docker 引擎通过利用最近提到的资源隔离和控制技术有效地保护容器免受任何恶意活动的影响。尽管如此,Docker 暴露了一些潜在的安全威胁,因为 Docker 守护程序以根权限运行。在这一部分,我们列出了一些安全风险和减轻它们的最佳实践。

受信任的用户控制

由于 Docker 守护程序以根权限运行,它有能力将 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 容器的默认根权限,如下所示:

$ sudo docker run --rm ubuntu:14.04 id
uid=0(root) gid=0(root) groups=0(root)

现在,让我们执行以下步骤:

  1. 制作一个Dockerfile,创建一个非根权限用户,并将默认的根用户修改为新创建的非根权限用户,如下所示:
#######################################################
# Dockerfile to change from root to non-root privilege
#######################################################

# Base image is Ubuntu
FROM ubuntu:14.04

# Add a new user "peter" with user id 7373
RUN useradd -u 7373  peter

# Change to non-root privilege
USER peter
uid=0(root) gid=0(root) groups=0(root)
  1. 继续使用docker build子命令构建 Docker 镜像,如下所示:
$ sudo docker build –t nonrootimage .

  1. 最后,让我们使用docker run子命令中的id命令来验证容器的当前用户:
$ sudo docker run --rm nonrootimage id
uid=7373(peter) gid=7373(peter) groups=7373(peter)

显然,容器的用户、组和组现在已更改为非根用户。

将默认的根特权修改为非根特权是遏制恶意渗透进入 Docker 主机内核的一种非常有效的方法。

加载 Docker 镜像和安全影响

Docker 通常从网络中拉取镜像,这些镜像通常在源头进行筛选和验证。然而,为了备份和恢复,Docker 镜像可以使用docker save子命令保存,并使用docker load子命令加载回来。这种机制也可以用于通过非常规手段加载第三方镜像。不幸的是,在这种做法中,Docker 引擎无法验证源头,因此这些镜像可能携带恶意代码。因此,作为第一道安全屏障,Docker 在特权分离的 chrooted 子进程中提取镜像。即使 Docker 确保了特权分离,也不建议加载任意镜像。

新兴的安全方法

到目前为止,我们已经讨论了与安全相关的内核特性和能力。通过理解和应用这些内核能力,大多数安全漏洞可以得到关闭。安全专家和倡导者考虑到了容器化理念在生产环境中更快更广泛的采用,提出了一些额外的安全解决方案,我们将详细描述这些安全方法。在开发、部署和交付企业级容器时,开发人员和系统管理员需要极为重视这些安全方法,以消除任何内部或外部的安全攻击。

用于容器安全的安全增强型 Linux

安全增强型 Linux(SELinux)是清理 Linux 容器中的安全漏洞的一次勇敢尝试,它是 Linux 内核中强制访问控制(MAC)机制、多级安全(MLS)和多类别安全(MCS)的实现。一个名为 Virtproject 的新的协作倡议正在基于 SELinux 构建,并且正在与 Libvirt 集成,为虚拟机和容器提供一个可适应的 MAC 框架。这种新的架构为容器提供了一个安全的隔离和安全网,因为它主要阻止容器内的根进程与容器外运行的其他进程进行接口和干扰。Docker 容器会自动分配到 SELinux 策略中指定的 SELinux 上下文中。

在完全检查自由裁量访问控制DAC)之后,SELinux 始终检查所有允许的操作。SELinux 可以根据定义的策略在 Linux 系统中的文件和进程以及它们的操作上建立和强制执行规则。根据 SELinux 规范,文件(包括目录和设备)被称为对象。同样,进程,比如运行命令的用户,被称为主体。大多数操作系统使用 DAC 系统来控制主体如何与对象和彼此交互。在操作系统上使用 DAC,用户可以控制自己对象的权限。例如,在 Linux 操作系统上,用户可以使他们的主目录可读,从而给用户和主体窃取潜在敏感信息的机会。然而,单独使用 DAC 并不是一个绝对安全的方法,DAC 访问决策仅基于用户身份和所有权。通常,DAC 简单地忽略其他安全启用参数,如用户的角色、功能、程序的可信度以及数据的敏感性和完整性。

由于每个用户通常对其文件拥有完全自由裁量权,确保系统范围的安全策略是困难的。此外,用户运行的每个程序都只是继承了用户被授予的所有权限,用户可以自由更改对他/她的文件的访问权限。所有这些都导致对恶意软件的最小保护。许多系统服务和特权程序以粗粒度权限运行,因此这些程序中的任何缺陷都可以轻松利用并扩展以获得对系统的灾难性访问。

正如在开头提到的,SELinux 将强制访问控制MAC)添加到 Linux 内核中。这意味着对象的所有者对对象的访问没有控制或自由裁量权。内核强制执行 MAC,这是一种通用的 MAC 机制,它需要能够对系统中的所有进程和文件强制执行管理设置的安全策略。这些文件和进程将用于基于包含各种安全信息的标签做出决策。MAC 具有足够保护系统的固有能力。此外,MAC 确保应用程序安全,防止任何恶意入侵和篡改。MAC 还提供了强大的应用程序隔离,以便任何受攻击和受损的应用程序都可以独立运行。

接下来是多类别安全MCS)。MCS 主要用于保护容器免受其他容器的影响。也就是说,任何受影响的容器都无法使同一 Docker 主机中的其他容器崩溃。MCS 基于多级安全(MLS)功能,并独特地利用 SELinux 标签的最后一个组件,MLS 字段。一般来说,当容器启动时,Docker 守护程序会选择一个随机的 MCS 标签。Docker 守护程序会使用该 MCS 标签为容器中的所有内容打上标签。

当守护程序启动容器进程时,它告诉内核使用相同的 MCS 标签为进程打标签。只要进程的 MCS 标签与文件系统内容的 MCS 标签匹配,内核就只允许容器进程读取/写入自己的内容。内核会阻止容器进程读取/写入使用不同 MCS 标签标记的内容。这样,被黑客入侵的容器进程就无法攻击其他容器。Docker 守护程序负责确保没有容器使用相同的 MCS 标签。通过巧妙地使用 MCS,禁止了容器之间的错误级联。

受 SELinux 启发的好处

SELinux 被定位为将绝对安全带给 Docker 容器的主要改进之一。很明显,SELinux 具有几个与安全相关的优势。由于 Docker 容器原生运行在 Linux 系统上,通过优雅的 SELinux 方法在 Linux 系统中进行的核心和关键改进也可以轻松地复制到 Docker 容器中。所有进程和文件都被标记为一种类型。一种类型能够定义和区分进程的域和文件的不同域。通过在它们自己的域中运行它们,进程彼此之间完全分离,对其他进程的任何侵入都受到严格监控并在萌芽阶段被制止。SELinux 赋予我们建立和执行策略规则的权力,以定义进程如何与文件和彼此交互。例如,只有在有明确阐述的 SELinux 策略允许所需和划定的访问时,才允许任何访问。确切地说,SELinux 在强制执行数据保密性和完整性方面非常方便。SELinux 还有助于保护进程免受不受信任的输入。它具有以下好处:

  • 细粒度访问控制:SELinux 访问决策是基于考虑各种安全影响信息,比如 SELinux 用户、角色、类型和级别。SELinux 策略可以在系统级别进行管理定义、执行和实施。通过全面利用 SELinux 升级,用户在放宽和减轻安全和访问策略方面的自由裁量权完全被消除。

  • 减少特权升级攻击的漏洞性:这些进程通常在域中运行,因此彼此之间干净地分离。SELinux 策略规则定义了进程如何访问文件和其他进程。也就是说,如果一个进程被有意或无意地破坏,攻击者只能访问该进程的标准功能和该进程被配置为访问的文件。例如,如果一个 Web 服务器被关闭,攻击者不能使用该进程来读取其他文件,除非特定的 SELinux 策略规则被纳入以允许这样的访问。

  • SELinux 中的进程分离:这些进程被安排在自己的域中运行,防止进程访问其他进程使用的文件,同时也防止进程访问其他进程。例如,在运行 SELinux 时,攻击者无法破坏服务器模块(例如 Samba 服务器),然后利用它作为攻击向量来读写其他进程使用的文件,比如后端数据库。SELinux 在大大限制了由不当配置错误造成的损害方面非常有用。域名系统(DNS)服务器经常在彼此之间复制信息,这被称为区域传输。攻击者可以使用区域传输来向 DNS 服务器更新虚假信息。SELinux 防止区域文件被任何黑客滥用。我们对 Docker 容器使用两种类型的 SELinux 执行。

  • 类型强制:这保护主机免受容器内部的进程的影响。运行 Docker 容器的默认类型是svirt_lxc_net_t。所有容器进程都以这种类型运行,容器内的所有内容都标记有svirt_sandbox_file_t类型。svirt_lxc_net_t默认类型被允许管理任何标记为svirt_sandbox_file_t的内容。此外,svirt_lxc_net_t还能够读取/执行主机上/usr目录下的大多数标签。

  • 安全问题:如果所有容器进程都以svirt_lxc_net_t运行,并且所有内容都标记为svirt_sandbox_file_t,则容器进程可能被允许攻击运行在其他容器中的进程和其他容器拥有的内容。这就是多类别安全(MCS)执行变得很有用的地方。

  • 多类别安全(MCS):这是对 SELinux 的一个实质性增强,允许用户为文件打上类别标签。这些类别实际上用于进一步限制自主访问控制DAC)和类型强制TE)逻辑。一个类别的例子是公司机密。只有有权访问该类别的用户才能访问带有该类别标签的文件,假设现有的 DAC 和 TE 规则也允许访问。术语类别指的是多级安全MLS)中使用的非层次化类别。在 MLS 下,对象和主体被标记有安全级别。这些安全级别包括一个分层敏感值,比如绝密,以及零个或多个非层次化类别,比如加密。类别提供了敏感级别内的隔间,并强制实施需要知道的安全原则。MCS 是对 MLS 的一种改编,代表了一种政策变化。除了访问控制,MCS 还可以用于在打印页面的顶部和底部显示 MCS 类别。这可能还包括一张封面,以指示文件处理程序。

  • AppArmor:这是一个有效且易于使用的 Linux 应用程序安全系统。AppArmor 通过强制执行良好的行为并防止甚至未知的应用程序缺陷被利用,主动保护操作系统和应用程序免受任何外部或内部威胁,甚至零日攻击。AppArmor 安全策略完全定义了个别应用程序可以访问的系统资源以及权限。AppArmor 包含了许多默认策略,并且使用高级静态分析和基于学习的工具的组合,即使是非常复杂的应用程序,也可以在几小时内成功部署 AppArmor 策略。AppArmor 适用于支持它的系统上的 Docker 容器。AppArmor 提供企业级主机入侵防范,并保护操作系统和应用程序免受内部或外部攻击、恶意应用程序和病毒的有害影响。因此,企业可以保护关键数据,降低系统管理成本,并确保符合政府法规。全面的企业范围网络应用程序安全需要关注用户和应用程序。这是一个突出的选择,可为 Docker 容器和容器内的应用程序带来无法渗透的安全性。策略正在成为确保容器安全的强大机制。策略制定和自动执行策略在保证容器安全方面起着重要作用。

容器安全的最佳实践

有强大而有韧性的安全解决方案,可以增强服务提供者和用户对容器化旅程的信心,以及对其有清晰和敏捷的态度。在本节中,我们提供了许多提示、最佳实践和关键指南,这些来自不同来源,旨在使安全管理员和顾问能够严密地保护 Docker 容器。基本上,如果容器在多租户系统中运行,并且您没有使用经过验证的安全实践,那么安全前方肯定存在着明显的危险。如前所述,安全漏洞可能发生在不同的服务级别,因此安全架构师需要弄清楚可能出现的问题,并规定经过验证和开创性的安全保护方法。安全领域的先驱和权威建议采用以下易于理解和遵循的做法,以实现最初设想的容器益处:

  • 摒弃特权访问

  • 尽量以非 root 用户身份运行您的容器和服务

首要建议是不要在系统上运行随机和未经测试的 Docker 镜像。制定策略,利用受信任的 Docker 镜像和容器存储库来订阅和使用应用程序和数据容器,用于应用程序开发、打包、装运、部署和交付。从过去的经验来看,从公共领域下载的任何不受信任的容器可能会导致恶意和混乱的情况。Linux 发行版,如Red Hat Enterprise LinuxRHEL),已经采取了以下机制,以帮助管理员确保最高的安全性:

  • 一个可信赖的软件存储库可供下载和使用

  • 安全更新和补丁来修复漏洞

  • 一个安全响应团队来查找和管理漏洞

  • 一个工程团队来管理/维护软件包并致力于安全增强

  • 常见标准认证来检查操作系统的安全性

如前所述,最大的问题是并非所有 Linux 都有命名空间。目前,Docker 使用五个命名空间来改变进程对系统的视图——进程、网络、挂载、主机名和共享内存。虽然这些给用户一定程度的安全性,但绝不像 KVM 那样全面。在 KVM 环境中,虚拟机中的进程不直接与主机内核通信。它们无法访问内核文件系统。设备节点可以与虚拟机内核通信,但不能与主机通信。因此,为了从虚拟机中提升权限,进程必须破坏虚拟机内核,找到超级监视器中的漏洞,突破 SELinux 控制(sVirt),并攻击主机内核。在容器环境中,方法是保护主机免受容器内进程的影响,并保护容器免受其他容器的影响。这就是将多个安全控制组合或聚合在一起,以保护容器及其内容。

基本上,我们希望尽可能设置多个安全屏障,以防止任何形式的突破。如果特权进程能够突破一个封闭机制,那么就要用层次结构中的下一个屏障来阻止它们。使用 Docker,可以尽可能利用 Linux 的多个安全机制。

以下是可能采取的安全措施:

  • 文件系统保护:为了避免任何未经授权的写入,文件系统需要是只读的。也就是说,特权容器进程不能向其写入,也不会影响主机系统。一般来说,大多数应用程序不需要向其文件系统写入任何内容。有几个 Linux 发行版使用只读文件系统。因此,可以阻止特权容器进程重新挂载文件系统为读写模式。这就是阻止容器内挂载任何文件系统的能力。

  • 写时复制文件系统:Docker 一直在使用高级多层统一文件系统AuFS)作为容器的文件系统。AuFS 是一个分层文件系统,可以透明地覆盖一个或多个现有的文件系统。当一个进程需要修改一个文件时,AuFS 首先创建该文件的副本,并能够将多个层合并成一个文件系统的单一表示。这个过程称为写时复制,这可以防止一个容器看到另一个容器的更改,即使它们写入相同的文件系统镜像。一个容器不能改变镜像内容以影响另一个容器中的进程。

  • 功能的选择:通常有两种方法来执行权限检查:特权进程和非特权进程。特权进程可以绕过所有类型的内核权限检查,而非特权进程则根据进程的凭据进行完整的权限检查。最近的 Linux 内核将传统上与超级用户相关联的特权划分为称为功能的不同单元,这些功能可以独立启用和禁用。功能是每个线程的属性。删除功能可以在 Docker 容器中带来几个积极的变化。无论如何,功能决定了 Docker 的功能、可访问性、可用性、安全性等等。因此,在增加或删除功能的过程中需要仔细考虑。

  • 保持系统和数据的安全:在企业和服务提供商在生产环境中使用容器之前,需要解决一些安全问题。出于以下三个原因,容器化最终将使得更容易保护应用程序:

  • 较小的有效负载减少了安全漏洞的表面积

  • 可以更新操作系统而不是逐步打补丁

  • 通过允许明确的关注分离,容器有助于 IT 和应用团队有目的地合作。

IT 部门负责基础设施相关的安全漏洞。应用团队修复容器内部的缺陷,也负责运行时依赖关系。缓解 IT 和应用开发团队之间的紧张关系有助于平稳过渡到混合云模型。每个团队的责任都清晰地划分,以确保容器及其运行时基础设施的安全。通过这样清晰的分工,积极地识别任何可见和不可见的危害安全的事件,并及时消除,制定和执行策略,精确和完美的配置,利用适当的安全发现和缓解工具等,都在系统地完成。

  • 利用 Linux 内核功能:一个普通的服务器(裸机或虚拟机)需要以 root 身份运行一堆进程。这些通常包括sshcronsyslogd、硬件管理工具(例如加载模块)、网络配置工具(例如处理 DHCP、WPA 或 VPN)等。容器非常不同,因为几乎所有这些任务都由容器所托管和运行的基础设施处理。安全专家撰写的各种博客上有一些最有趣和鼓舞人心的安全相关细节的最佳实践、关键指南、技术知识等。您可以在docs.docker.com/articles/security/找到一些最有趣和鼓舞人心的安全相关细节。

数字签名验证

Docker,这家知名的开源容器公司,宣布已将数字签名验证添加到 Docker 镜像中。这将确保当您从官方 Docker 仓库下载一个容器化应用时,您得到的是真实版本。此时,Docker 引擎会自动使用数字签名检查官方仓库中所有镜像的来源和完整性。数字签名为 Docker 镜像带来了额外的信任。也就是说,特定的 Docker 镜像没有被篡改或扭曲,因此可以放心和清晰地完全使用。

这种新添加的加密验证用于为用户提供额外的安全保证。将来,将会有一些功能,比如发布者认证、镜像完整性和授权、公钥基础设施(PKI)管理等,供镜像发布者和消费者使用。如果官方镜像被损坏或篡改,Docker 将立即发出警告。目前,Docker 引擎不会阻止任何受影响的镜像运行,非官方镜像也不会被验证。随着 Docker 社区加固代码并解决不可避免的可用性问题,未来版本将会改变这一点。

在开发应用程序时,有时需要在其运行时查看它。最近出现了一些工具,如nsinitnsenter,以帮助开发人员调试其容器化的应用程序。一些用户已经开始运行一个 init 进程,以在他们的应用程序中生成sshd,以允许他们访问,这会带来风险和开销。

Docker 的安全部署指南

Docker 容器越来越多地托管在生产环境中,可以被公开发现和被许多人使用。特别是随着云技术的更快采用,全球组织和机构的 IT 环境正在被系统地优化和转变,以灵活和果断地托管更多种类的虚拟机和容器。有一些新的改进和功能,比如 Flocker 和 Clocker,可以加快将容器部署到云环境(私有、公共、混合和社区)的过程。在部署容器时必须遵循一些建议。众所周知,容器通过允许开发人员和系统管理员无缝部署应用程序和服务,显著减少了开销。然而,由于 Docker 利用与主机系统相同的内核来减少资源需求,如果配置不足,容器可能面临重大安全风险。在部署容器时,开发人员和系统管理员必须严格遵循一些仔细注释的指南。例如,github.com/GDSSecurity/Docker-Secure-Deployment-Guidelines以表格形式详细阐述了所有正确的细节。

毫无疑问,分布式和复杂应用程序中的软件缺陷为智能攻击者和黑客打开了入侵托管关键、机密和客户数据的系统的大门。因此,安全解决方案被坚持并融入到 IT 堆栈的所有层中,因此在不同级别和层次上出现了许多类型的安全漏洞。例如,周界安全只解决了部分问题,因为不断变化的要求要求允许员工、客户和合作伙伴访问网络。同样,还有防火墙、入侵检测和预防系统、应用交付控制器(ADC)、访问控制、多因素身份验证和授权、打补丁等。然后,为了在传输、持久性和应用程序使用数据时保护数据,有加密、隐写术和混合安全模型。所有这些都是反应性和现实的机制,但趋势增长的是虚拟业务坚持采用积极主动的安全方法。随着 IT 趋向和趋势向着备受期待的虚拟 IT 世界发展,安全问题和影响正在受到安全专家的额外重视。

未来

在未来的日子里,容器化领域将会有更多值得注意的即兴创新、转型和颠覆。通过一系列创新和整合,Docker 平台正在被定位为加强容器化旅程的领先平台。以下是通过巧妙利用 Docker 技术取得的主要成就:

  • 加强分布式范式:随着计算越来越分布和联合,微服务架构(MSA)将在 IT 中发挥非常决定性和更深层次的作用。Docker 容器正日益成为托管和交付日益增长的微服务数组的最有效方式。随着容器编排技术和工具获得更广泛的认可,微服务(特定的和通用的)被识别、匹配、编排和编排,形成业务感知的复合服务。

  • 赋能云范式:云理念正在牢牢抓住 IT 世界,以实现迫切需要的 IT 基础设施合理化、简化、标准化、自动化和优化。抽象和虚拟化概念是云范式取得空前成功的关键,正在渗透到各种 IT 模块中。最初,它始于服务器虚拟化,现在已经涉及存储和网络虚拟化。随着我们周围所有技术的进步,人们普遍渴望实现软件定义基础设施(软件定义计算、存储和网络)。Docker 引擎,作为 Docker 平台的核心和关键部分,已经得到充分巩固,以使容器在软件定义环境中无障碍地运行。

  • 实现 IT 弹性、可移植性、敏捷性和适应性:容器正逐渐成为灵活和未来化的 IT 构建模块,为实现更强韧性、多功能性、优雅和柔韧性做出贡献。更快速地提供 IT 资源以确保更高的可用性和实时可伸缩性,轻松消除开发和运营团队之间的各种摩擦,保证 IT 的原生性能,实现组织化和优化的 IT 以提高 IT 生产力等,这些都是对 Docker 容器的一些典型设想,以实现更智能的 IT。

注意

容器将成为虚拟机(VM)和裸金属服务器的战略补充,以实现更深层次的 IT 自动化、加速和增强,从而实现备受炒作和期望的业务敏捷性、自主性和可负担性。

摘要

安全性绝对是一个挑战,也是一个重要的方面,不容忽视。如果一个容器被 compromise,那么让容器主机垮掉就不是一件困难的事情。因此,确保容器和主机的安全对于容器化概念的蓬勃发展至关重要,特别是在 IT 系统的集中化和联邦化日益增长的情况下。在本章中,我们特别关注了 Docker 容器的令人作呕和毁灭性的安全问题,并解释了为容纳动态、企业级和关键任务应用程序的容器提供无懈可击的安全解决方案的方法和手段。在未来的日子里,将会有新的安全方法和解决方案,以确保 Docker 容器和主机的安全无法被渗透和破坏,因为容器及其内容的安全对于服务提供商和消费者来说至关重要。

posted @ 2024-05-06 18:33  绝不原创的飞龙  阅读(13)  评论(0编辑  收藏  举报