展望 Docker 1.10 镜像新面貌

本文刊载于《程序员》杂志 2016 年 2 月期。作者:孙宏亮,责任编辑:周建丁。

作者介绍

allen(wechat)

孙宏亮@DaoCloud 是 DaoCloud 的技术合伙人,主要负责 DaoCloud 企业级容器云平台的研发。他是《Docker 源码分析》一书的作者,也是国内第一批研究及实践 Docker 的工程师。

编者按:刚刚发布的 Docker 1.10 版本中,Docker 镜像最大的看点之一--通过特殊的组织方式,无论是存储效率还是镜像安全,都将达到一个新的高度。

Docker 概述

过去的 2015 年中,Docker 无疑是全球最赚尽眼球的开源项目。作为一门新技术,如果说 2014 年是工业界对 Docker 观望的一年,那么 2015 年绝对是属于 Docker 的实践年。

从 2013 年 3 月诞生至今,Docker 仍不足三周岁,然而待挖掘的潜力却不断震惊着世人。作为容器技术的新生代表,Docker 俨然是一副「初生牛犊不怕虎」的姿态,在传统虚拟化技术面前,也是不遑多让;同时,借助镜像技术的优势,又是掀起了一阵阵「重新定义应用」的飓风;当然,Docker 背后的推手 Docker 公司,更是依靠庞大的用户群体以及 Docker 的受宠,迅速建立起完善的生态体系,云计算领域的战略布局也是愈发明显。

Docker 软件在过去的两年多时间中,发展不可谓不迅猛。从支持传统 LXC 技术,直到 2014 年 6 月份发布 1.0.0 这个大版本,Docker 一直朝着生产环境在努力;随后的一年多时间中,Docker 无论在用户体验、功能完善、安全增强上,都有着十足的进步,2015 年在旧金山举办的「DockerCon 2015」现场,Docker 公司的 CTO 暨 Docker 创始人 Solomon Hykes 更是直言 Docker 已经「Production Ready」;2015 年末随着 docker 1.9.0 的发布,万众瞩目的「Docker 容器跨主机通信能力」已经进入主线,并在 Docker Swarm 项目中原生支持。几乎每个版本的发布,Docker 都极大的刺激着用户的神经,带来非一般的体验。

2016 年新年之际,元月中旬,Docker 发布了 docker 1.10-rc,并专文介绍 1.10 版本的重大新特性。可以说,即将发布的 docker 1.10 版本中,Docker 镜像是最大的看点之一。Docker 镜像通过特殊的组织方式,无论是存储效率、还是镜像安全,都将达到一个新的高度。

Docker 1.10 镜像新特性

谈及 Docker 的镜像技术,不管是资深的 Docker 用户,还是 Docker 新手,肯定对 Docker 镜像的分层管理、镜像复用等技术不陌生。传统的联合文件系统等技术,有能力将多层镜像叠加后为 Linux 容器提供文件系统视角;同时,镜像分层管理以及相邻镜像之间的父子关系,使得镜像共享成为可能,从而达到镜像复用的效果,减少存储之余也降低镜像的传输带宽。

Docker 镜像的分层管理,对于传统文件系统的管理提出了新的要求。为了记录镜像之间的关系,Docker 不得不为每一层镜像设置一个镜像标识(ID),并通过镜像配置文件的父镜像字段来记录父镜像的镜像标识(ID)。Docker Engine 在 1.10 版本前构建出的镜像关系如图 1(以镜像 ubuntu:14.04 为例):

640-2

图1:Docker Engine 在 1.10 版本前构建出的镜像关系

值得一提的是:Docker 1.10 前构建出的镜像默认支持 docker registry v1 版本,当使用docker push命令上传 docker registry v2(Docker 官方称之为 distribution)时,Docker Engine 自动将其转换为 v2 格式。

由图 1 可见,Docker 镜像的组织关系主要通过镜像标识(ID)来标记镜像以及镜像之间的关系。然而,docker 1.10 开始,原生 Docker Engine 构建镜像时开始弱化镜像标识(ID)的概念,而是采用 hash 值来标记镜像,并且直接支持 Docker Registry v2 格式。

总结而言,从 Docker 1.10 开始,Docker Engine 的镜像新特性在于:

Docker 完全通过镜像的内容来确定其存储方式。这意味着 Docker 镜像的标识(ID)将不再是随机生成,而是通过镜像本身的文件系统内容以及镜像配置文件来产生镜像 hash 值。

为了便于理解 Docker 镜像技术以及如上新特性,结合图1,以下概要介绍 Docker 1.10 之前 v1 版本镜像的主要基本知识:

  • Docker 镜像(如 ubuntu:14.04)包含链式的多个镜像
  • 每个镜像均有一个镜像标识(ID)
  • 每个镜像包含两部分内容:镜像配置文件(json 文件),镜像文件系统内容
  • 除最下层的基础镜像,每个镜像拥有一个父镜像,父镜像标识(ID)记录于镜像配置文件中
  • 构建一个镜像主要通过对一个 Dockerfile 文件执行docker build命令来完成
  • 构建镜像的过程中,新镜像的镜像标识(ID)为随机生成的 UUID

Docker 1.10 的镜像新特性,彻底改变以上 Docker 镜像知识的最后一条,将默认使用 Docker Registry v2(distribution)的镜像格式。与此同时,这也意味着 Docker 1.10 很有可能放弃对 docker registry v1 的支持。构建镜像时,每一层镜像的镜像标识(ID)不再是随机生成的 UUID,而是完全由镜像本身来决定,即由镜像文件系统内容以及镜像配置文件唯一确定。

此特性一出,无论是镜像的单机存储,还是 Docker Registry 中镜像的全局存储,对存储空间而言,很大程度上都将得到降低。另外,镜像标识与镜像本身实现强验证,镜像的安全性也得到大大提高。

Docker 镜像存储

说起 Docker 镜像存储,镜像复用一直被 Docker 从业人员所津津乐道,镜像的共享使得存储空间得到很大限度的释放。俗话说金无足赤,而新版本 Docker 1.10 则是将镜像的存储优势更上一层楼。一言以蔽之,则是 Docker Engine 原生支持「镜像的全局唯一性」。

既然说到 Docker 1.10 可以保证镜像的全局唯一性,那我们不得不分析 Docker 1.10 之前版本在这一方面稍显不足的地方。可以说,之前版本的 Docker 甚至连本地镜像的局部唯一性都不能百分百保证,更遑论全球的全局唯一性。

1.10 之前版本的 Docker 都是在单机环境中完成镜像构建,构建情况可以分为三种:

  • 不同的 Dockerfile 构建出不同的镜像
  • 相同的 Dockerfile 默认构建出相同的镜像;放弃使用镜像缓存机制(使用 –no-cache 参数)则重新构建出不同的镜像,尽管镜像的文件系统内容完全相同
  • 前缀相同的 Dockerfile 默认采用相同的祖先镜像,最终构建出不同的镜像

由于 Docker 镜像标识(ID)与镜像内容没有一个强验证关系,仅仅是随机产生的 UUID,因此在本地镜像的构建过程中,难免存在冗余,如:在弃用镜像缓存机制时,构建同一个 Dockerfile。

如果说单机环境下 Docker 镜像的存储空间浪费,是一个可以忍受的现象,那么放在分布式环境,放在多机环境下,就是一个十分严重的问题。原因很简单:跨主机镜像构建之间没有任何关联性。Docker 1.10 版本前后镜像的存储差异详见图 2。

640
图 2:Docker 1.10 版本前后镜像的存储差异

分析图 2,我们可以发现,Docker 1.10 版本之后,相同的 Dockerfile 无论在何处都可以创建出相同的 Docker 镜像,原因很简单,就是镜像内容相同。如果此时将镜像全部推送至 Docker Registry,对于全球唯一的镜像层,镜像仓库没有理由存储冗余的多份,一份足矣。如此一来,毫无疑问镜像的存储空间将得到很大程度的缓解。

Docker 镜像安全

Docker 镜像的安全问题,迄今为止一直是 Docker 从业人员最关心的安全话题之一。由于 Docker 镜像具有强移植性,分发速度极其惊人,也能传播到达全球任意一个角落。一旦镜像本身不受信,其带来的危害很有可能如病毒般传播。

可喜的是,Docker 1.10 一旦发布,Docker Engine 将默认支持 Docker Registry v2 的镜像格式,届时镜像安全必然有所提高。

Docker 虽然风靡全球,然而作为新生宠儿,工业界对 Docker 的实践经验往往参差不齐,对于 Docker 镜像的安全防护自然也会更有差异。以下则从篡改镜像为例,详细介绍 Docker 镜像的安全。

Docker Registry 作为集中存储 Docker 镜像的仓库,安全防护责无旁贷。虽然 Docker Registry 不管是 v1 还是 v2,均支持无认证模式,但是一旦启用无认证模式,镜像的安全防护则完全形同虚设,任意一台可以访问 Docker Registry 的机器,均有能力覆盖镜像仓库内部的原有镜像,肆意破坏镜像的可用性,更有能力在原有镜像内部安置诸如木马等恶意程序,后果不堪设想。

覆盖镜像的方式极其简单,成本也很低。倘若意图覆盖未认证 Docker Registry 中的 nginx 镜像,恶意者仅需将本地的 ubuntu 镜像重新打一个名为 nginx 的镜像标签(tag),随后将 nginx 镜像推送至镜像仓库中,如此一来危害已然造成。

覆盖镜像是篡改镜像最为简单的方式,恶意者若希望在不修改镜像原有逻辑的情况下,在镜像中植入恶意程序,则难度稍大,但也不是毫无途径。v1 时代的镜像,由于镜像组织关系过于简单的缘故,仅仅修改完整镜像的某一层内容,植入恶意程序,并修改完整镜像的程序执行入口 entrypoint 或者 cmd 即可。若在本机修改,甚至无需考虑重新计算镜像的 checksum 校验码,只有需要将篡改镜像推送至 Docker Registry 时,才需要完成 checksum 的重新计算,以便 Docker Registry 的校验。我们完全可以想象在 Docker 镜像中植入恶意程序所造成的危害,如一个被广泛使用的基础镜像中,存在某一个含有恶意程序的镜像层,那么凭借 Docker 镜像开放的原则,很容易在全网甚至全球传播开来。

随着 Docker 1.10 的发布,篡改镜像的现象会有明显的改善。虽然暴力使用无关镜像覆盖 Docker Registry 中现有镜像的情况依然存在,但是篡改 Docker 镜像的某一层将不再是随意而为之。倘若完成镜像层文件系统内容的修改,之后仍需要完成镜像内容 SHA256 checksum 值的计算,同时 hash 值的变化,势必造成原有的镜像签名失效。可以说,Docker 镜像的篡改门槛变得越来越高,但是无论如何,对于 Docker Engine 以及超级用户权限的保护都是极其重要的。Docker 镜像的安全审计工作非常具有实际意义。

总结

Docker 1.10 版本原生支持 Docker Registry v2 版本的镜像,可以认为是自 Docker 诞生之际,Docker 镜像经历的最大革新。不论从镜像存储而言,还是从镜像安全来看,这都是 Docker 在演进发展过程中的利好之事。当然,Docker Registry v1 的遗弃自然也是不可避免,届时镜像的迁移、转换将是不得不考虑的话题。

Leave a Reply

Your email address will not be published. Required fields are marked *