天天看点

可执行镜像——开发环境的Docker化之路

每位开发者都经历过软件不兼容之痛。当我们需要同时开发几个使用不同java运行时版本的项目时,这些问题会急剧爆发,特别是在osx平台上。为此,ruby使用自己的版本管理工具。我的两个同事曾用了几小时来调试他们各自用homebrew管理的openssl和python版本之间的不兼容。我们是否可以使用容器来解决这些问题呢?答案是肯定的!

容器的主要目标是交付软件。新成立的开放容器项目给出以下定义:

 标准容器的目标是使用自描述和可移植的格式,封装软件组件及其全部依赖,以便任何兼容  运行时都可以运行,无需额外的依赖,不必关心底层机器和容器的内容。

标准容器的目标是使用自描述和可移植的格式,封装软件组件及其全部依赖,以便任何兼容运行时都可以运行,无需额外的依赖,不必关心底层机器和容器的内容。

这份定义没有提及任何关于软件分发类型的描述。这是有意而为之的,因为容器的设计是内容无关的。我们要交付什么以及如何使用完全取决于我们自己。在这篇文章中,我将阐述服务镜像和可执行镜像之间的区别,并建议读者使用可执行镜像。

可执行镜像没有服务镜像那么常见,但却是一个非常有用的补充。可执行镜像要解决的是软件兼容性等问题。我们拿官方的maven镜像作为例子,探索可执行镜像是什么、它们是如何工作的,以及我们如何创建可执行镜像。其中,dockerfile中的entrypoint指令是演绎可执行镜像的核心角色。

传统上,容器镜像被用作长时间运行的进程:在服务器上运行的服务,不会影响主机,因为它们存在与容器内。我们称其为服务镜像。web服务器、负载均衡服务器和数据库服务器都是服务镜像的好例子。这类容器可以很容易与虚拟机对比.

容器镜像也可以用作短暂的进程:在我们计算机上运行的、容器化的可执行命令。这些容器执行单一的任务,生命周期短暂,而且通常可以在使用后被删除。我们称之为可执行镜像。举例来说,比如编译器(golang)或者构建工具(maven)、演示软件(我很喜欢用markdown格式写一个演示,然后用revealjs

docker镜像将其展示出来),以及浏览器。可执行镜像的终极布道者是docker公司的jessie

frazelle。如果你希望获得更多启发,一定要阅读她博客中相关的内容,或者看下她在dockercon

2015上的演讲。

其实,服务镜像和可执行镜像之间的界限并非泾渭分明。镜像都是可执行的,因为它们的任务就是运行一个进程。在容器中运行一个演示或者浏览器是非常好的本地工具示例,因此我将称其为可执行镜像。纵然他们是长时间运行的进程。话虽如此,我希望读者能够认同这样分类的道理。

如此定义的出发点,更多是从镜像的目的,而不是进程存活的长短。

那么,可执行镜像的优势是什么呢?它们是如何解决前述问题的呢?

其中一个原因是,对可执行镜像的体验是一种很好的开始使用docker的方式。这种体验非常有用,而且不会影响生产环境。此中的趣味无穷!

另一个原因是安装方便。众所周知的包管理器apt-get、yum、macports和homebrew等,通常在大部分时间有完美的表现,但是当我们真的需要它们的时候……问题在于,这些工具的伟大之处是同一件事情:管理依赖。但是,它们没有强大到可以管理同一个包的两个版本,包括其依赖关系树。容器的设计没有依赖性:所有的依赖都被固化到镜像中。安装本身只意味着运行docker、执行命令。如果镜像不存在于系统中,docker会自动下载(pull)该镜像。通过将软件与其依赖一起封装在容器镜像中的方式,实现了可靠的软件分发。测试容器镜像即是测试依赖是否能与主要功能一起工作。

容器化的可执行文件仅指容器化,换个说法叫沙箱。这降低了运行不完全信任软件的风险,避免了许多程序的漏洞。一个例子是浏览器中的可疑链接。在一个干净的文件系统中运行一个全新的浏览器会更安全。另一个例子是关于几个月前valve软件的steam删除了所有用户的文件,包括连接的驱动器的缺陷!docker的沙箱机制并非完美,但它肯定会避免发生清除照片库这样的事情。

因为进程及其依赖是封装在容器中的,运行同一软件的不同版本变得非常简单!通常情况下,要开始一个java/maven项目,我们需要安装所需版本的java开发套件(jdk)和maven。而使用docker,我们就可以跳过这步。

jdk和maven由某个团队安装在一个可执行镜像中。于是,其他人就可以在此基础上迁出源代码,并直接编译和测试它们。我们可以为另一个使用不同jdk版本的项目使用另一个镜像。甚至可以在同一时间编译这些项目!而不需要担心$java_home环境变量。

可执行镜像——开发环境的Docker化之路

构建服务镜像的目的是以指定的方式运行一个服务。这也许需要一些环境相关的信息,比如数据库地址,但不会很多。构建可执行镜像的目的是运行一个以指定方式与系统交互的工具。有很多技术可以实现这一目的。我们将以maven编译器镜像作为这一技术的实现示例。需要注意的是,这里所指的技术是通用的,所以纵然你不喜欢java,请稍安勿躁。

假设我们有一个包含java源代码的maven项目,该项目至少在根目录下,包含一个pom.xml文件和/src/main/java目录。对于本文而言,可以采用任何你想用的maven项目。如果你没有任何maven项目,你可以去下载spring

boot(选择maven类型)。使用命令行cd到项目目录(包含pom.xml文件的目录),执行如下命令:

该命令做了如下的事情:

<code>docker run</code>创建了maven:3.3.3-jdk-8镜像的一个实例。该实例中执行了<code>mvn install</code>命令。原则上,这不会影响主机系统。

<code>-v $(pwd):/project</code>将当前目录挂载到容器中,作为/project目录。这样以来,容器就可以读写主机系统的当前目录了。

<code>-w /project</code>设置了/project作为工作目录。这意味着执行mvn命令将在project目录中有效。

<code>--rm</code>将在执行完毕后删除容器。甩掉包袱!

这与在主机上直接运行mvn

install的结果是一样的,只是不必实际安装java或maven。我们以在项目目录下,获得target目录而告终,该目录包含了编译好的java应用程序。

可以运行maven clean命令清理项目:

maven镜像的功能是运行mvn

[args]。因此,我们可以认为在docker命令中指定mvn是多余的。为此,可以使用docker提供的entrypoint。这个entrypoint是与命令强关联的。可以在dockerfile中分别使用entrypoint和cmd指令。这两个指令将作为容器镜像的元数据,覆盖<code>docker run</code>命令。我们可以这样执行<code>mvn clean install</code>:

entrypoint和命令将连接在一起执行。它的优点是关注点分离。对于可执行容器镜像而言,entrypoint可以用作定义恒定部分,命令可以用作定义可变部分。

如果我们将entrypoint融入容器镜像,分离会更加优雅。为此,我们在另一目录中创建一个dockerfile文件,内容如下:

其中,我们同样增加了一个工作目录,因此我们的新镜像希望maven项目挂载在/project目录之下。dockerfile以exec的形式定义了entrypoint和cmd,方括号内的参数最终被解析为shell。在dockerfile文件所在的目录下,执行<code>docker build -t my_mvn .</code>命令构建镜像,这个镜像简化了前述的执行命令:

其中,<code>clean install</code>当然可以替换为mvn的其他参数。如果我们忘记包含命令参数,将会打印<code>maven help</code>,因为在dockerfile文件中定义了默认的命令参数,<code>-h</code>即表示help。

entrypoint的另一个很好的用途是在方括号内定义辅助脚本。例如,如果在实际服务正常启动之前,我们需要执行一些命令,辅助脚本可以很好地处理。另外,这样的脚本还可以检查当前是否具备了必要的全部运行时配置,比如链接或环境变量等。命令本身作为启动脚本的参数,但是对执行脚本是透明的。关于这一点的更多信息以及简单示例,请参阅docker文档中的dockerfile最佳实践。

我们可以为可执行镜像创建一个别名。这样,我们就可以输入简短的指令,就像普通程序一样。在~/.profile中添加:

因为我们要传递参数,所以使用函数代替了别名。在执行<code>source ~/.profile</code>命令,加载变更后,我们就可以这样简单地使用了:

当前方案的缺点是,每次执行时都需要下载maven工件。本地maven安装总会包含一个仓库目录,其中存储了所有的maven工件。目前的方法是很简洁,但是并不实用。让我们将maven仓库作为卷添加进来。创建一个目录,比如<code>/usr/tmp/.m2</code>,然后运行:

现在,主机上的<code>/usr/tmp/.m2</code>目录中存储了maven下载下来的工件。我们以后每次用这种方式启动maven容器镜像,因为引入了这个目录,所以maven会重用那些工件。可以重复执行<code>mvn install</code>两次来检验不同。

我们只是让maven构建更快了。但是,为此,我们不得不在主机上管理一个目录。在本文的最后一步中,我们将使用docker管理这个卷。首先,我们创建一个叫data的容器:

容器创建完毕会打印“data for

maven”,该容器创建了一个卷。这里使用什么镜像不是核心问题,在本例中使用maven:3.3.3-jdk-8是方便,因为它已经下载到主机了,而使用my_mvn不太方便,因为entrypoint要预先考虑echo声明。注意,这里没有<code>-v /root/.m2:</code>中的冒号,因为我们不再引入主机目录。而是让docker在主机上创建自己的数据目录。使用“data”作为名字并非是必需的命令,但是这样是为了显式说明这是一个数据容器,

当执行<code>docker ps</code>时,该名称将会反射显示。我们可以通过<code>--volumes-from</code>使用这个容器的卷,而无需考虑docker持有的实际目录。这样做会引入容器中的/root/.m2作为挂载卷。这种技术对共享容器之间的数据也非常有用。我们修改~/.profile如下:

现在,当我们运行mvn时,maven主目录将映射到这个卷。maven容器自身会被删除,但是卷会在缓存的本地仓库中保留。如果我们希望清理系统,可以使用如下命令删除数据容器:

<code>-v</code>表示与之相关的容器满足如下条件时,删除该卷:

卷是由docker管理的

没有其他容器引用

一个忠告:如果你忘记了使用<code>-v</code>选项,最终会产生孤儿卷目录。

可执行容器镜像是一种强大的docker应用程序。对于软件分发,以及以限制和验证的方式在计算机上运行时,非常有用。此外,这是一种有趣的开始docker体验的方式。我希望你能通过此文,在开始尝试docker和使用相关技术上,得到了启发。