写点什么

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

2015 年 10 月 25 日

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

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

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

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

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

1 服务镜像 VS. 可执行镜像

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

容器镜像也可以用作短暂的进程:在我们计算机上运行的、容器化的可执行命令。这些容器执行单一的任务,生命周期短暂,而且通常可以在使用后被删除。我们称之为可执行镜像。举例来说,比如编译器(Golang)或者构建工具(Maven)、演示软件(我很喜欢用 Markdown 格式写一个演示,然后用 RevealJS Docker 镜像将其展示出来),以及浏览器。可执行镜像的终极布道者是 Docker 公司的 Jessie Frazelle 。如果你希望获得更多启发,一定要阅读她博客中相关的内容,或者看下她在 DockerCon 2015 上的演讲

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

2 可执行镜像的优势

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

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

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

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

因为进程及其依赖是封装在容器中的,运行同一软件的不同版本变得非常简单!通常情况下,要开始一个Java/Maven 项目,我们需要安装所需版本的Java 开发套件(JDK)和Maven。而使用Docker,我们就可以跳过这步。 JDK 和Maven 由某个团队安装在一个可执行镜像中。于是,其他人就可以在此基础上迁出源代码,并直接编译和测试它们。我们可以为另一个使用不同JDK 版本的项目使用另一个镜像。甚至可以在同一时间编译这些项目!而不需要担心$JAVA_HOME 环境变量。

3 Maven 镜像

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

4 使用卷传递文件

假设我们有一个包含 Java 源代码的 Maven 项目,该项目至少在根目录下,包含一个 pom.xml 文件和 /src/main/java 目录。对于本文而言,可以采用任何你想用的 Maven 项目。如果你没有任何 Maven 项目,你可以去下载 Spring Boot(选择 Maven 类型)。使用命令行 cd 到项目目录(包含 pom.xml 文件的目录),执行如下命令:

复制代码
user:project$ docker run --rm \
-v $(pwd):/project \
-w /project \
maven:3.3.3-jdk-8 mvn install

该命令做了如下的事情:

  • docker run创建了 maven:3.3.3-jdk-8 镜像的一个实例。该实例中执行了mvn install命令。原则上,这不会影响主机系统。
  • -v $(pwd):/project将当前目录挂载到容器中,作为 /project 目录。这样以来,容器就可以读写主机系统的当前目录了。
  • -w /project设置了 /project 作为工作目录。这意味着执行 mvn 命令将在 project 目录中有效。
  • --rm将在执行完毕后删除容器。甩掉包袱!

这与在主机上直接运行 mvn install 的结果是一样的,只是不必实际安装 Java 或 Maven。我们以在项目目录下,获得 target 目录而告终,该目录包含了编译好的 Java 应用程序。

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

复制代码
user:project$ docker run --rm \
-v $(pwd):/project \
-w /project \
maven:3.3.3-jdk-8 mvn clean
5 使用 entry point 传递参数

Maven 镜像的功能是运行 mvn [args]。因此,我们可以认为在 Docker 命令中指定 mvn 是多余的。为此,可以使用 Docker 提供的 entrypoint。这个 entrypoint 是与命令强关联的。可以在 Dockerfile 中分别使用 ENTRYPOINT 和 CMD 指令。这两个指令将作为容器镜像的元数据,覆盖docker run命令。我们可以这样执行mvn clean install

复制代码
user:project$ docker run --rm \
-v $(pwd):/project \
-w /project \
--entrypoint mvn \
maven:3.3.3-jdk-8 clean install

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

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

复制代码
FROM maven:3.3.3-jdk-8
WORKDIR /project
ENTRYPOINT ["mvn"]
CMD ["-h"]

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

复制代码
user:project$ docker run --rm \
-v $(pwd):/project \
my_mvn clean install

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

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

6 为可执行镜像创建别名

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

复制代码
mvn() {
docker run --rm \
-v $(pwd):/project \
my_mvn $*
}

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

复制代码
user:project$ mvn clean install

7 使用卷缓存 Maven 本地仓库

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

复制代码
user:project$ docker run --rm \
-v $(pwd):/project \
-v /usr/tmp/.m2:/root/.m2 \
my_mvn install

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

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

复制代码
user:project$ docker run --name maven_data \
-v /root/.m2 \
maven:3.3.3-jdk-8 echo 'data for maven'

容器创建完毕会打印“data for maven”,该容器创建了一个卷。这里使用什么镜像不是核心问题,在本例中使用 maven:3.3.3-jdk-8 是方便,因为它已经下载到主机了,而使用 my_mvn 不太方便,因为 entrypoint 要预先考虑 echo 声明。注意,这里没有-v /root/.m2:中的冒号,因为我们不再引入主机目录。而是让 Docker 在主机上创建自己的数据目录。使用“data”作为名字并非是必需的命令,但是这样是为了显式说明这是一个数据容器, 当执行docker ps时,该名称将会反射显示。我们可以通过--volumes-from使用这个容器的卷,而无需考虑 Docker 持有的实际目录。这样做会引入容器中的 /root/.m2 作为挂载卷。这种技术对共享容器之间的数据也非常有用。我们修改~/.profile 如下:

复制代码
mvn() {
docker run --rm \
-v $(pwd):/project \
--volumes-from maven_data \
my_mvn $*
}

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

复制代码
docker rm -v maven_data

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

  • 卷是由 Docker 管理的
  • 没有其他容器引用

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

8 总结

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

关于作者

Quinten Krijger在开始他的 IT 职业生涯前,曾经研究过物理学和一年的古典唱法。后来,他搬到阿姆斯特丹的 Trifork,主要的工作是继续使用开源技术,比如 Java、Spring、ElasticSearch 和 MongoDB,完成项目后端的工作,对最新的前端设计颇有感觉。他热衷于缩短反馈周期和启用敏捷开发:测试、CI 和 DevOps 是此中的关键词。在 Docker 出现后不久,他便产生了兴趣,并深刻地认识到,有效的容器可以提供非常广泛的可能性。他致力于启动容器解决方案上已经有半年了,目前是 ING 的一名 DevOps。

查看英文原文 Executable Images - How to Dockerize Your Development Machine

2015 年 10 月 25 日 18:097047

评论

发布
暂无评论
发现更多内容

或许是史上最好的AQS源码分析了,AQS基础一

InfoQ_d2212957090d

AQS

阿里架构师耗时三个月整理的Spring实战笔记:入门到实战

Java架构师迁哥

.NET多线程(Thread,ThreadPool,Task,Async与Await)

AI代笔

Atlassian Team Tour 9月23日登陆中国,报名通道已开启!

Atlassian

敏捷开发 数字化转型 金融 Jira

Hadoop 客户端节点

yuanhang

hadoop3

初识大数据

yuanhang

大数据

算法与数据中台实践之网约车平台

博文视点Broadview

大数据 数据中台 中台 算法 数据

通证是下一代互联网数字经济的关键

CECBC区块链专委会

区块链 通证经济

你不可不知道的Design Thinking

长沙造纸农

设计 思维方式 设计思维 设计实践 设计师

详解增强算术赋值:“-=”操作是怎么实现的?

Python猫

Python 编程

【API进阶之路】用API打造一条自动化内容生产流水线

华为云开发者社区

自动化 API 部署

实战解析丨如何对Mysql连接请求的tcpdump内容进行分析

华为云开发者社区

TCP/IP 数据传输

干货:不同场景容器内获取客户端源IP的方法

华为云开发者社区

容器 服务端 场景

LeetCode题解:232. 用栈实现队列,使用两个栈 入队 - O(1), 出队 - O(n),JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

面试官问我:看过sharding-jdbc的源码吗?我吧啦吧啦说了一通!!

冰河

数据库 分布式事务 微服务 分布式数据库 ShardingJDBC

华为HMS:风雨突然,仍求自我

脑极体

学完微软技术总监整理的44 个微服务架构设计模式,我涨薪了

Java架构师迁哥

flutter在行动之踩坑的日子(1)

霜蓝手环

flutter 跨平台 Flutter Android Apk 编程之路

终于有架构大牛把分布式系统概念讲明白了,竟然用了足足800页

周老师

Java 编程 程序员 架构 面试

作为一个架构师,我是不是应该有很多职责?

架构师修行之路

程序员 架构师

Hadoop 简介

yuanhang

hadoop

XSKY全新一代SDS一体机五大场景之超融合

XSKY融合存储

jvm疯狂吞占内存,罪魁祸首是谁?

易观大数据

众盟科技:跨越时空70年,一场别样的房地产直播开启人文探索

人称T客

阿里P9开源阿里内部秒杀系统设计方案以及设计原则,还不收藏

小Q

Java 架构 系统设计 秒杀 并发

Hadoop3 环境搭建

yuanhang

程序员快乐器之JAVA代码生成工具

Learun

敏捷开发 快速开发 生成代码

起飞!这份技术点拉满的ELk+Lucene笔记,可能价值百万

小Q

Java lucene elasticsearch 架构 面试

易观方舟Open API 及最佳实践

易观大数据

众盟科技:跨越时空70年,一场别样的房地产直播开启人文探索

脑极体

Nodejs使用es module开发CLI

zayfen

nodejs Module ES cli esm

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

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