构建安全可靠、最小化的 Docker 镜像

容器化部署愈来愈多的应用于企业的生产环境中,如何构建安全可靠、最小化Docker 镜像也就愈来愈重要。本文将针对该问题,经过原理加实践的方式,从头到脚帮你撸一遍。文章比较长,主要经过五个部分对容器镜像进行讲解。分别是:html

  • 镜像的构建
    讲解了镜像的手动构建与自动构建过程。
  • 镜像的存储与UnionFS联合文件系统
    讲解了镜像的分层结构以及UnionFS联合文件系统,以及镜像层在UnionFS上的实现。
  • 最小化镜像构建
    讲解了为何须要最小化镜像,同时如何进行最小化构建。
  • 容器镜像的加固
    容器镜像加固的具体方式。
  • 容器镜像的审查
    高质量的项目中容器镜像也须要向代码同样进行审查。

读者能够根据各自状况选择性阅读。原文发布在个人我的站点: GitDiG.com. 原文链接:构建安全可靠、最小化的 Docker 镜像.node

1. 构建镜像

1.1 手动构建

手动构建 Docker 镜像的流程图,以下:git

如今依次按照流程采用命令行的方式手动构建一个简单的Docker 镜像。github

1.1.1 建立容器并增长文件

busybox做为本次试验的基础镜像,由于它足够小,大小才 1.21MBgolang

$: docker run -it busybox:latest sh
/ # touch /newfile
/ # exit
复制代码

经过以上的操做,咱们完成了流程图的前三步。建立了一个新容器,并在该容器上建立了一个新问题。只是,咱们退出容器后,容器也不见了。固然容器不见了,并不表示容器不存在了,Docker 已经自动保存了该容器。若是在建立时,未显示设置容器名称,能够经过如下方式查找该消失的容器docker

# 列出最近建立的容器
$: docker container ls -n 1
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
c028c091f964        busybox:latest      "sh"                13 minutes ago      Exited (0) 27 seconds ago                       upbeat_cohen

# 查询容器的详情
$: docker container inspect c028c091f964
...
复制代码

1.1.2 提交变动生成镜像

手动构建镜像,很简单。先找到发生变动的容器对象,对其变动进行提交。提交完成后,镜像也就生成了。不过此时的镜像只有一个自动生成的序列号惟一标识它。为了方便镜像的检索,须要对镜像进行命名以及标签化处理。安全

命令行操做以下:bash

# 提交变动, 构建镜像完成
$: docker commit -a JayL -m "add newfile" c028c091f964
sha256:01603f50694eb62e965e85cae2e2327240e4a68861bd0e98a4fb4ee27b403e6d

# 对镜像进行命名, 原镜像ID取前几位就能够了
$: docker image tag 01603f50694eb62e9 busybox:manual

# 验证新镜像
$: docker run busybox:manual ls -al newfile
-rw-r--r--    1 root     root             0 Jun 15 05:25 newfile
复制代码

经过以上两步过程就完成了Docker 镜像手动建立。很是简单是否是。可是也很是麻烦,必须先建立新容器在提交变动,生成镜像。整个过程彻底能够经过脚本化处理,这也是下节要说的,自动化构建Docker 镜像。网络

1.2 自动化构建

1.2.1 Dockerfile 构建

自动化构建Docker 镜像,Docker公司提供的不是SHELL脚本的方式,而是经过定义一套独立的语法来描述整个构建过程, 经过该语法编辑的文件,称为 Dockerfile。 自动化构建镜像就是经过编写Dockerfile文件构建的。app

一样完成上面的工做,用Dockerfile写出来就是:

FROM busybox:latest
RUN touch /newfile 复制代码

至于更加详细的Dockerfile语法,请参见官方指南

完成Dockerfile编写后,经过命令触发构建。整个过程,脚本化出来就是:

$: mkdir autobuild && cd autobuild
$: cat <<EOF > Dockerfile
FROM busybox:latest
RUN  touch /newfile
EOF
$: docker build -t busybox:autobuild .
复制代码

2 镜像的存储

2.1 镜像的组成

Docker 镜像是由一组只读的镜像层Image Layer组成的。而Docker 容器则是在Docker 镜像的基础之上,增长了一层:容器层Container Layer。容器层Container Layer可读写的。若是对该容器层Container Layer进行commit提交操做,该层就变成了新的镜像层Image Layer。新的Docker Image也就构建出来了。

$: mkdir layer && cd layer && touch newfile
$: cat <<EOF > Dockerfile
FROM scratch
ADD  newfile .
EOF
$: docker build -t layer .
复制代码

如下官网提供的图示能够很清楚的看出镜像与容器之间的联系与区别:

具体某个镜像的组成Layer能够经过以下命令进行查询:

# 镜像的构建层历史
$: docker history busybox:autobuild
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
845cc5130d2c        17 minutes ago      /bin/sh -c touch /newfile                       0B
ef46e0caa533        4 days ago          /bin/sh -c #(nop) CMD ["sh"] 0B
<missing>           4 days ago          /bin/sh -c #(nop) ADD file:1067e5a... in / 1.21MB
复制代码

不难看出,镜像busybox:autobuild一共执行了从底往上的三次层构建。具体构建的指令能够经过第三列的命令得出。<missing>的意思是:该层是在其它系统上构建的,在本地是不可用的。只须要忽略就好。

2.2 Union FileSystem

要了解 Docker 镜像的存储首先必须了解联合文件系统 UnionFS (Union File System),所谓UnionFS就是把不一样物理位置的目录合并mount到同一个目录中。UnionFS的具体实现有不少种:

  • 早期的UFS
  • AUFS
  • OverlayFS
    • overlay
    • overlay2

具体Docker宿主机上使用那种UnionFS文件系统驱动,能够经过以下命令查询:

$:  docker info | grep Storage
Storage Driver: overlay2
复制代码

overlay2是一种更现代的联合文件系统 UnionFS,它比overlay的早期版本在稳定与性能上都有很大提高。因此通常最新的Docker采用的存储驱动使用的都是overlay2

为了方便演示UnionFS文件系统,若是是MacOS系统,建议安装Docker Machine开启一台新的虚拟机操做,排除由于Docker for MacOS运行在虚拟机上的各类环境干扰。具体Docker Machine的安装请自行查阅相关文档。

首先建立一台新的Docker Machine:

# 建立
$: docker-machine create ufs
...
Docker is up and running!

# 登陆
$: docker-machine ssh ufs
... ok

# 查询 overlay
$: cat /proc/filesystems | grep overlay
nodev	overlay
复制代码

经过确认,这台Docker Machine是支持UnionFS文件系统的,使用的是overlay存储驱动。 既然UnionFS就是把不一样物理位置的目录合并mount到同一个目录中.如今咱们经过命令行的方式实现一下Docker官网提供UnionFS的原理图。

从图中能够看出,咱们须要提供两个目录,分别表明Container LayerImage Layer。目录名称,取图示右部的名称:

  • 目录upper, 表明Container Layer
  • 目录lower, 表明Image Layer

除了这两个目录之外,经过UnionFS挂载目录还须要两个目录:

  • 目录merged, 表明挂载目录,即合并后的目录
  • 目录work, 必须为空目录,是overlay存储驱动挂载所需的工做目录。

经过命令行实现图示中的文件夹结构:

# 建立一个测试目录
$: mkdir demo && cd demo

# 建立子目录与文件
$: mkdir upper lower merged work
$: touch lower/file1 lower/file2 lower/file3
$: touch upper/file2 upper/file4

# 经过文件内容区分如下file2
$: echo lower > lower/file2
$: echo upper > upper/file2

# 未挂载
$: ls merged
复制代码

迄今为止,一切都是常规文件系统操做。如今经过mount命令进行UnionFS文件系统的目录挂载.

# 目录合并挂载到merged
$: sudo mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged

# 挂载完成后
$: ls merged
file1 file2 file3 file4

# file2 使用的是顶层 upper 的file2 文件
$: cat merged/file2
upper
复制代码

下面再分别经过文件的增删改加深对UnionFS文件系统的理解:

  • 新增文件
# 新增文件
$: touch merges/file5
$: ls merged/
file1  file2  file3  file4  file5
# 新增文件写在顶层的 upper 文件夹
$: ls upper/
file2  file4  file5
$: ls lower/
file1  file2  file3
复制代码
  • 修改文件
# 修改文件 CoW 技术
$: echo mod > merged/file1
$: ls upper/
file1 file2  file4  file5
$: cat upper/file1
mod
$: cat lower/file1
复制代码
  • 删除文件
# 删除文件
$: rm merged/file1
$: ls -al upper | grep file1
c---------    1 root     root        0,   0 Jun 17 10:41 file1
$: ls -al lower | grep file1
-rw-r--r--    1 docker   staff            0 Jun 17 10:15 file1
复制代码

实际操做完成以上过程,相信你对于UnionFS文件系统有了更加直观的感觉。你可能会问, Docker Image的底层镜像是由一组Layer组成的,多个底层目录在UnionFS中如何挂载?其实很简单,只须要经过:分隔便可。

# 多层目录: lower1 / lower2 / lower3
$: sudo mount -t overlay overlay -olowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged
复制代码

挂载完成后,lower1 / lower2 / lower3之间的层叠顺序又是怎样,读者能够自行测试一下。

最后,咱们查询一下系统的挂载列表,

mount | grep overlay
overlay on /home/docker/demo/merged type overlay (rw,relatime,lowerdir=lower,upperdir=upper,workdir=work)
复制代码

从现有输出可知目前咱们docker-machine中仅挂载了一个overlay目录。

2.3 镜像的存储

如今咱们在这台新的docker-machine上构建一个1.2中所描述的Docker镜像: busybox:autobuild

$: mkdir autobuild && cd autobuild
$: cat <<EOF > Dockerfile
FROM busybox:latest
RUN  touch /newfile
EOF
$: docker build -t busybox:autobuild .

# 完成构建后,如今系统中有两个docker image
$: docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             autobuild           2e32da74b3ad        4 seconds ago       1.22MB
busybox             latest              e4db68de4ff2        2 days ago          1.22MB
复制代码

构建完成后,咱们直接看一下docker-machine上的文件系统的挂载状况:

# docker 无容器运行
$: mount
...
/dev/sda1 on /mnt/sda1/var/lib/docker type ext4 (rw,relatime,data=ordered)

# docker 运行容器时
# 从新开启新会话,运行一个容器实例 `docker run -it busybox:autobuild sh`
$: mount
...
/dev/sda1 on /mnt/sda1/var/lib/docker type ext4 (rw,relatime,data=ordered)
overlay on /mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/merged type overlay (rw,relatime,lowerdir=/mnt/sda1/var/lib/docker/overlay2/l/KLGL6INSJ2UBLMAUP5B4IORUTG:/mnt/sda1/var/lib/docker/overlay2/l/BGIT3WQZVII4Z2THF35I6T5V5O:/mnt/sda1/var/lib/docker/overlay2/l/6GZ2NT4UQT6EQK3IT4IGMBXU4T,upperdir=/mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/diff,workdir=/mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/work)
shm on /mnt/sda1/var/lib/docker/containers/e50f19c5bde3fe53cde3729de92f75b74323f7ebb506b0635eb76dd5b81e080a/mounts/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=65536k)
nsfs on /var/run/docker/netns/3c464f8003e8 type nsfs (rw)
复制代码

对比输出,可以很明显的看到,暂仅关注 overlay 挂载状况。得出:

  • 挂载后的目录是: /mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/merged
  • 容器Layer是: /mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/diff
  • 镜像Layer是: /mnt/sda1/var/lib/docker/overlay2/l/KLGL6INSJ2UBLMAUP5B4IORUTG /mnt/sda1/var/lib/docker/overlay2/l/BGIT3WQZVII4Z2THF35I6T5V5O /mnt/sda1/var/lib/docker/overlay2/l/6GZ2NT4UQT6EQK3IT4IGMBXU4T

其中镜像Layer使用的是软链接。一样的信息,咱们能够通docker inspect查询出来。

$: docker inspect <container-id> -f '{{.GraphDriver.Data.MergedDir}}'
$: docker inspect <container-id> -f '{{.GraphDriver.Data.UpperDir}}'
$: docker inspect <container-id> -f '{{.GraphDriver.Data.LowerDir}}'
复制代码

输出的路径就是具体Docker镜像的存储位置。

3. 最小化 Docker 镜像

3.1 为何要最小化 Docker 镜像

最小化 Docker 镜像的缘由可总结出如下几条:

  • 省钱,减小网络传输流量,节省镜像存储空间
  • 省时,加速镜像部署时间
  • 安全,有限功能下降被攻击的可能性
  • 环保,垃圾都分类了,浪费资源可耻

3.2 如何构建最小化 Docker 镜像

1.31.4 中所讨论的镜像的组成原理与存储, 最小化 Docker 镜像的主要途径总结下来也就两条:

  • 缩减镜像的Layer大小
  • 减小镜像的Layer层数

先从简单的减小镜像Layer的层数开始。

3.3 减小镜像的 Layer 层数

3.3.1 组合命令

在定义Dockerfile的时候,每一条指令都会对应一个新的镜像层。经过docker history命令就能够查询出具体Docker 镜像构建的层以及每层使用的指令。为了减小镜像的层数,在实际构建镜像时,经过使用&&链接命令的执行过程,将多个命令定义到一个构建指令中执行。如:

FROM debian:stable

WORKDIR /var/www 
RUN apt-get update && \ apt-get -y --no-install-recommends install curl \ ca-certificates && \ apt-get purge -y curl \ ca-certificates && \ apt-get autoremove -y && \ apt-get clean 复制代码

3.3.2 压缩镜像层

除了经过将多命令经过&&链接到一个构建指令外,在Docker镜像的构建过程当中,还能够经过--squash的方式,开启镜像层的压缩功能,将多个变化的镜像层,压缩成一个新的镜像层。

具体命令就以下:

$: docker build --squash -t <image> .
复制代码

3.4 缩减镜像的 Layer 大小

3.4.1 选择基础镜像

缩减Layer的大小须要从头开始,即选择什么样的基础镜像做为初始镜像。通常状况下,你们都会从如下三个基础镜像开始。

  • 镜像 scratch(空镜像), 大小 0B
  • 镜像 busybox(空镜像 + busybox), 大小 1.4MB
  • 镜像 alpine (空镜像 + busybox + apk), 大小 3.98MB

镜像 busybox 经过busybox程序提供一些基础的Linux系统操做命令,镜像 alpine则是在次基础上提供了apk包管理命令,方便安装各种工具及依赖包。普遍使用的镜像基本都是镜像 alpine镜像 busybox更适合一些快速的实验场景。而镜像 scratch空镜像,由于不提供任何辅助工具,对于不依赖任何第三方库的程序是合适的。由于镜像 scratch空镜像自己不提供任何container OS,因此程序是运行在Docker Host即宿主机上的,只是利用了Docker技术提供的隔离技术而已。

细心的读者可能会发现,在MacOS上编译的程序,采用镜像 scratch空镜像时,容器运行会报错:。那是由于,Docker for Mac是运行在Linux虚拟机上的缘故。因此不能够直接构建MacOS格式的可执行程序在Docker for Mac上采用空镜像的方式运行。

3.4.2 多阶段构建镜像

多阶段构建 Multi-Stage BuildDocker 17.05 版本开始引入的新特性。经过将原先仅一个阶段构建的镜像查分红多个阶段。之因此多阶段构建镜像可以缩减镜像的大小,是由于发布程序在编译期相关的依赖包以及临时文件并非最终发布镜像所须要的。经过划分不一样的阶段,构建不一样的镜像,最终镜像则取决于咱们真正须要发布的实体是什么。

FROM golang:1.11-alpine3.7 AS builder

WORKDIR /app COPY main.go . RUN go build -o server . 
FROM alpine:3.7

WORKDIR /app COPY --from=builder /app . 
CMD ["./server"] 复制代码

如上的Dockerfile就是多阶段构建,在builder阶段使用的基础镜像是golang:1.11-alpine3.7,显然是由于编译期的须要,对于发布真正的server程序是彻底不必的。经过多阶段构建镜像的方式就能够仅仅打包须要的实体构成镜像。

除了多阶段构建之外,若是你还想忽略镜像中一些冗余文件,还能够经过.dockerignore的方式在文件中定义出来。功能和.gitignore相似。

4. 加固 Docker 镜像

最小化Docker 镜像的构建完成了,可是,咱们的工做却仍未结束。咱们还须要对镜像进行加固处理。

4.1 镜像内容可寻址标识符(CAIID)

镜像内容可寻址标识符(Content addressable image identifiers), 能够对来源基础镜像内容进行校验,确保没有被第三方篡改。具体的操做方式,就是在构建本身镜像的同时,对基础镜像内容进行内容的sha256摘要值进行设置,防止在不知情的状况下被篡改。

首先,得出具体镜像的正确sha256摘要值.

# 经过命令查询出具体镜像的sha256摘要
$: docker inspect busybox:autobuild -f "{{.RepoDigests}}"
sha256:9b63a0eaaed5e677bb1e1b29c1a97268e6c9e6fee98b48badf0f168ae72a51dc
复制代码

再在Dockerfile定义时,设置基础镜像的sha256摘要值

FROM busybox@sha256:9b63a0eaaed5e677bb1e1b29c1a97268e6c9e6fee98b48badf0f168ae72a51dc
...
复制代码

注意:镜像内容可寻址标识符的获取必须通过一次 push 或者 pull 操做,即在镜像注册服务上发布后,才能够经过以上 inspect 命令查询出结果。若是仅仅是本地的镜像,没法经过 inpect 命令获取。固然仅仅是本地使用的镜像,镜像内容可寻址标识符也是不必的。

4.2 用户权限

容器一旦建立出来,其默认使用的用户是能够在镜像中进行设置的。经过设置必要的镜像默认用户,能够限制其在容器中的执行权限。在某种程度上也就进行提高了镜像的安全级别。不过,这须要根据具体的业务发布状况进行设置,常规状况下,基础镜像都仍是root用户做为默认用户 。

安全原则:构建镜像自己是为了特定的应用定制的,默认状况下应该尽量的下降用户权限。

4.3 SUID与SGID问题

除了镜像自己设置必要的默认用户之外,在镜像中,还会存在一类程序,即便是经过普通用户执行,但在运行时会以更高级别的权限执行。就是系统针对可执行文件与目录提供的SUID与SGID特殊权限。

经过对可执行文件设置SUID或SGID属性,本来执行命令的用户会切换成为命令的全部者或是所属组的权限进行执行。也就是提高了执行命令的权限。

在实际的镜像构建中,应该尽量的避免此类权限提高形成的可能的漏洞。建议镜像构建时,扫描镜像内是否存在此类执行文件,若是存在尽量的删除。删除命令可参考:

# 镜像构建过程当中增长对特殊权限可执行文件的扫描并删除
RUN for i in $(find / -type f \( -perm +6000 -o -perm +2000 \)); \ do chmod ug-s $i; done 复制代码

5. 审查 Docker 镜像

正如Code Review同样,代码审查能够大大提高企业项目的质量。容器镜像一样做为开发人员或是运维人员的产出物,对其进行审查也是必要的。

虽然咱们能够经过docker命令结合文件系统浏览的方式进行容器镜像的审查,但其过程须要人工参与,很难作到自动化,更别提将镜像审查集成到CI过程当中了。但一个好的工具能够帮咱们作到这点。

推荐一个很是棒的开源项目dive,具体安装请参考其项目页。它不但能够方便咱们查询具体镜像层的详细信息,还能够做为CI持续集成过程当中的镜像审查之用。使用它能够大大提高咱们审查镜像的速度,而且能够将这个过程作成自动化。

该项目的具体动态操做图示以下:

dive

若是做为镜像审查以后,能够进行以下命令操做:

$: CI=true dive <image-id>
Fetching image... (this can take a while with large images)
Parsing image...
Analyzing image...
  efficiency: 95.0863 %
  wastedBytes: 671109 bytes (671 kB)
  userWastedPercent: 8.2274 %
Run CI Validations...
  Using default CI config
  PASS: highestUserWastedPercent
  SKIP: highestWastedBytes: rule disabled
  PASS: lowestEfficiency
复制代码

从输出信息能够获得不少有用的信息,集成的CI过程也就很是容易了。 dive自己就提供了.dive-ci做为项目的CI配置:

rules:
  # If the efficiency is measured below X%, mark as failed.
  # Expressed as a percentage between 0-1.
 lowestEfficiency: 0.95

  # If the amount of wasted space is at least X or larger than X, mark as failed.
  # Expressed in B, KB, MB, and GB.
 highestWastedBytes: 20MB

  # If the amount of wasted space makes up for X% or more of the image, mark as failed.
  # Note: the base image layer is NOT included in the total image size.
  # Expressed as a percentage between 0-1; fails if the threshold is met or crossed.
 highestUserWastedPercent: 0.20
复制代码

集成到CI中,增长如下命令便可:

$: CI=true dive <image-id> 
复制代码

镜像审查和代码审查相似,是一件开始抵制,开始后就欲罢不能的事。这件事宜早不宜迟。对于企业与我的而言均百利而无一害。

以前发布文章如何对Docker Image进行审查,出自此节。

6. 参考资源

相关文章
相关标签/搜索