关于容器技术,我之前分享不少文章和技巧,包括如何优化镜像,如何更优雅的进行构建封装,以及大量的容器应用实践、使用案例以及维护方式。

本篇文章将介绍一个在许多场景下更有效的方案,来让容器镜像更加小巧。比如我们常用的 Node 应用,使用这个方式将减少至少 800M 磁盘空间。

写在前面

以往构建镜像,我们往往会使用来自 DockerHub 上的基础系统镜像(来自 mirror 的镜像同理)或者一些编程语言维护组织推出的官方镜像,来做为基础镜像,来进行我们自己的容器的二次封装和构建。

虽然这样做可以相对快速和轻松的构建应用镜像,但是它往往会带来包含 90% 无用文件的大体积镜像,即使我们进行多阶段构建,依旧不能很好的解决这个问题。严重的时候,甚至会将包含 CVE 漏洞的组件引入镜像中。

虽然 Alpine 镜像已经很小了,但是它依旧包含了许多不必要的组件。那么有没有可能让我们的镜像里不包含包管理工具、SHELL、冗余的二进制文件,只包含最小的可运行系统,以及我们的语言 Runtime,或者核心的 glibc 依赖呢

项目背景

有人用上面提到的思路做了一个项目:distroless,这个项目来自谷歌云:https://github.com/GoogleContainerTools/distroless

在继续聊如何使用它,以及使用注意事项之前,先来看看都有“谁”在使用它吧。

可能你会好奇,这些镜像除了“安全之外”,镜像尺寸到底能有多小,我们使用官方的介绍数据:

Distroless 镜像非常小,其中最小的镜像 gcr.io/distroless/static约为 650 kB。这大约是alpine (~ 2.5 MB) 大小的 25% ,以及不到 debian (50 MB)大小的 1.5% 。

虽然官方目前已经提供了多数场景下所需要的镜像,比如:

  • 适合静态编译语言运行的镜像:C,C++,Go,Rust。
  • 适合动态语言使用的镜像:Java,Python,Node

然而,在实际过程中,你可能会遇到需要自定义构建的需求,如何进行镜像构建呢?可以参考 https://github.com/bazelbuild/rules_docker 项目定制你的镜像。此外,除了 Python 镜像尚在试验阶段外,其实所有的镜像都适合和已经投入生产环境经过了大量验证。

下面我们来看看如何使用 distroless 。

如何使用镜像

在我的网站“知识地图”中,可以找到循序渐进的关于《如何优化 Docker 镜像尺寸》的几篇文章,我们使用 distroless 镜像的场景,依旧是依赖“多阶段构建”的方式来减少最终产物的尺寸。

比如,我们要构建一个 golang 应用的镜像,只需要在原本镜像文件下面添加三行语句即可:

FROM golang as build
WORKDIR /go/src/app
ADD . /go/src/app
RUN go get -d -v ./...
RUN go build -o /go/bin/app

# 考虑到可能需要使用 glibc libssl openssl,我们使用 base 镜像
# 如果不需要使用上述依赖,可以切换为 static 镜像,让产物尺寸更小巧
FROM gcr.io/distroless/base
COPY --from=build /go/bin/app /
CMD ["/app"]

使用过程中的问题

下面来聊聊实际使用过程中的常见的两个问题:网络问题和调试问题。

问题一:网络问题

在构建应用镜像过程中,我们一般需要切换镜像进行调试,从而选择出最适合的基础镜像,所以潜在的需求是将各种语言适用的镜像都“下载”下来。

因为众所周知的网络问题,所以一般使用的情况下,我们可能会遇到网络不通而无法下载镜像的问题,类似下面这样。

docker pull gcr.io/distroless/cc  

Using default tag: latest
Error response from daemon: Get "https://gcr.io/v2/": context deadline exceeded

解决问题的方法也很简单,和《简单的 Kubernetes 集群搭建》一文中的方式类似,我们使用云服务器批量获取和镜像这些容器镜像即可。

不过因为镜像列表只在 https://console.cloud.google.com/gcr/images/distroless 有存放,相比较根据 GCE API 去获取内容再进行解析,最简单的方式便是写两条简单的 “JS 脚本”,从网页中将这些镜像枚举出来:

Array.from(document.querySelectorAll('.cfc-table-element.cfc-md1 tbody tr>td:nth-child(1)')).map(n=>`docker pull gcr.io/distroless/${(n.innerText).trim()}`).join('\n');


Array.from(document.querySelectorAll('.cfc-table-element.cfc-md1 tbody tr>td:nth-child(1)')).map(n=>`docker save gcr.io/distroless/${(n.innerText).trim()} -o ${(n.innerText).trim()}.tar`).join('\n')

在网页控制台中运行后,我们会得到下面的命令:

docker pull gcr.io/distroless/base
docker pull gcr.io/distroless/base-debian10
docker pull gcr.io/distroless/base-debian11
docker pull gcr.io/distroless/base-debian9
docker pull gcr.io/distroless/cc
docker pull gcr.io/distroless/cc-debian10
docker pull gcr.io/distroless/cc-debian11
docker pull gcr.io/distroless/cc-debian9
docker pull gcr.io/distroless/dotnet
docker pull gcr.io/distroless/dotnet-debian10
docker pull gcr.io/distroless/dotnet-debian9
docker pull gcr.io/distroless/java
docker pull gcr.io/distroless/java-debian10
docker pull gcr.io/distroless/java-debian11
docker pull gcr.io/distroless/java-debian9
docker pull gcr.io/distroless/nodejs
docker pull gcr.io/distroless/nodejs-debian10
docker pull gcr.io/distroless/nodejs-debian11
docker pull gcr.io/distroless/nodejs-debian9
docker pull gcr.io/distroless/python2.7
docker pull gcr.io/distroless/python2.7-debian10
docker pull gcr.io/distroless/python2.7-debian9
docker pull gcr.io/distroless/python3
docker pull gcr.io/distroless/python3-debian10
docker pull gcr.io/distroless/python3-debian11
docker pull gcr.io/distroless/python3-debian9
docker pull gcr.io/distroless/static
docker pull gcr.io/distroless/static-debian10
docker pull gcr.io/distroless/static-debian11
docker pull gcr.io/distroless/static-debian9


docker save gcr.io/distroless/base -o base.tar
docker save gcr.io/distroless/base-debian10 -o base-debian10.tar
docker save gcr.io/distroless/base-debian11 -o base-debian11.tar
docker save gcr.io/distroless/base-debian9 -o base-debian9.tar
docker save gcr.io/distroless/cc -o cc.tar
docker save gcr.io/distroless/cc-debian10 -o cc-debian10.tar
docker save gcr.io/distroless/cc-debian11 -o cc-debian11.tar
docker save gcr.io/distroless/cc-debian9 -o cc-debian9.tar
docker save gcr.io/distroless/dotnet -o dotnet.tar
docker save gcr.io/distroless/dotnet-debian10 -o dotnet-debian10.tar
docker save gcr.io/distroless/dotnet-debian9 -o dotnet-debian9.tar
docker save gcr.io/distroless/java -o java.tar
docker save gcr.io/distroless/java-debian10 -o java-debian10.tar
docker save gcr.io/distroless/java-debian11 -o java-debian11.tar
docker save gcr.io/distroless/java-debian9 -o java-debian9.tar
docker save gcr.io/distroless/nodejs -o nodejs.tar
docker save gcr.io/distroless/nodejs-debian10 -o nodejs-debian10.tar
docker save gcr.io/distroless/nodejs-debian11 -o nodejs-debian11.tar
docker save gcr.io/distroless/nodejs-debian9 -o nodejs-debian9.tar
docker save gcr.io/distroless/python2.7 -o python2.7.tar
docker save gcr.io/distroless/python2.7-debian10 -o python2.7-debian10.tar
docker save gcr.io/distroless/python2.7-debian9 -o python2.7-debian9.tar
docker save gcr.io/distroless/python3 -o python3.tar
docker save gcr.io/distroless/python3-debian10 -o python3-debian10.tar
docker save gcr.io/distroless/python3-debian11 -o python3-debian11.tar
docker save gcr.io/distroless/python3-debian9 -o python3-debian9.tar
docker save gcr.io/distroless/static -o static.tar
docker save gcr.io/distroless/static-debian10 -o static-debian10.tar
docker save gcr.io/distroless/static-debian11 -o static-debian11.tar
docker save gcr.io/distroless/static-debian9 -o static-debian9.tar

将上面的内容保存为脚本,扔到服务器上执行,不一会我们所需要的镜像就都会以 tarball 的形式规规矩矩的躺在文件夹里了。之后将这些镜像按需下载和载入就能正常使用啦。

问题二:调试模式

前文提到过,由于生产版本的 distroless 镜像中不包含 SHELL,所以我们常规的镜像调试方法,docker exec -it 便无法使用了。

官方迫于这个实际的开发需求,便提供了配套的调试镜像:包含 busybox shell 的 debug 镜像。调试镜像使用方式也非常简单,在之前使用的镜像名称后,添加 debug 作为版本号即可,以前文中的 base 镜像为例:

FROM golang as build
WORKDIR /go/src/app
ADD . /go/src/app
RUN go get -d -v ./...
RUN go build -o /go/bin/app

# 在镜像后添加 debug 标签
FROM gcr.io/distroless/base:debug
COPY --from=build /go/bin/app /
CMD ["/app"]

重新构建镜像后,docker exec -it 便又能正常使用啦。

这些调试镜像对应的获取脚本可以使用下面的脚本:

Array.from(document.querySelectorAll('.cfc-table-element.cfc-md1 tbody tr>td:nth-child(1)')).map(n=>`docker pull gcr.io/distroless/${(n.innerText).trim()}:debug`).join('\n');

Array.from(document.querySelectorAll('.cfc-table-element.cfc-md1 tbody tr>td:nth-child(1)')).map(n=>`docker save gcr.io/distroless/${(n.innerText).trim()} -o ${(n.innerText).trim()}-debug.tar`).join('\n')

最后

关于如何更好的管理这些镜像,我推荐你浏览之前的内容,私有化容器仓库:《Harbor & Distribution》

–EOF