几个月前,我使用Go构建了一些监控基础设施。我将它部署到ECS(一个Docker协调器,功能类似于Kubernetes),为了好玩,我决定看看我能够创建最小的image。我以前使用过Alpine基本映像(大约5 MB,对于一个小的Go二进制文件,通常是另外5 MB),但是Go宣称自己只需要Linux内核(大多数编程语言都依赖于解释器,VM和/或系统库 - 后者只要内核功能,需要内核的稳定接口),我想看看它是多么真实或实用,我想更好地理解我认为理所当然的事情通过使用发行版。
作为一个上下文问题,Docker有一个特殊的基本映像,名为scratch,它是空的 - 在临时基础映像上运行的应用程序只能访问内核(至少在容器提供隔离的范围内)。
注意:如果你的Go应用程序需要子处理用到其他程序(例如,git)或者它几乎使用任何使用CGo的库(这些库几乎总是依赖于libc,如果不是其他库),则此方法将不起作用。
注意:如果你不使用Go模块,则构建步骤可能会有所不同。
通常,要为Go应用程序构建Docker镜像,你编写一个Dockerfile,它指定安装了Go工具链的基本Docker镜像,从主机中复制源代码,并在提交之前调用编译器生成二进制文件,更改最终的image。它看起来像这样:
工具链及其依赖项(git,mercurial等)重达几百MB(更不用说分布本身的重量),因此通常使用称为多阶段构建的Docker功能从第一个image复制二进制工件(binary artifact)到没有工具链的第二个image中。所以我们现在有这样的事情:
请注意,我们删除了CMD /myapp这一行,因为(可能是我完全不了解的原因),实际上运行/bin/sh -c /myapp,因此使用默认命令运行Docker镜像会产生错误:“docker:来自守护进程的错误响应:OCI运行时创建失败:container_linux.go:348:启动容器进程导致 exec:‘/bin/sh’:stat /bin/sh:‘没有这样的文件或目录:未知(这是特别无益的)’。”至少现在,在不指定命令的情况下运行映像将提供更有用的错误:“docker:来自守护程序的错误响应:未指定命令。”
这适用于像hello world这样的东西,但真正的应用程序还有一些要求。首先,默认情况下,标准库中的某些软件包将尝试链接系统库(IIRC,net特别倾向于尽可能使用系统DNS解析器)。这意味着从此镜像创建的容器将在运行时失败,因为二进制文件无法找到系统库(因为它们不存在)。但是,如果使用CGO_ENABLED = 0调用编译器,则将使用pure-Go实现:
现在我们可以解决动态链接错误,但如果我们的应用程序需要发出HTTPS请求(或几乎任何其他需要使用SSL的请求),我们会遇到另一个好奇心:“获取https://www.google.com:x509:证书由未知权威签署。”此错误来自Docker的net/http-not。我们可以通过不在程序中处理它来避免这个错误:rsp,_:= http.Get(url)。开玩笑。这个错误告诉我们HTTP库找不到建立SSL连接所需的证书(或者其他东西 - 我真的理解SSL比我可能应该的要少得多)。基本上scratch也没有这些证书,所以我们如何将它们纳入我们的最终image?从构建阶段复制它们!
现在应用程序运行良好;但是,默认情况下,Docker镜像以root用户身份运行 - 遵循安全最佳实践,我们应该使用非root用户。但是,scratch没有像adduser甚至echo这样的程序,所以我们不能回显“$ USER_INFO”>> / etc / passwd,但我们可以在构建器阶段创建一个文件并将其复制到最后阶段:
事实证明这就是我们需要做的。certs和/etc/passwd文件在大小方面可以忽略不计,因此最终image由可执行文件的大小决定。我的小可执行文件大小为4.5 MB未压缩和ECR(亚马逊的Docker Hub模拟)报告为2.5 MB(可能是压缩)。有许多技巧可以进一步减少你的二进制文件大小,但我会将这些作为练习留给读者。
这个Dockerfile还有最后一个问题:每次都需要重建依赖项,这需要一段时间(如果你在Dockerfile本身上进行迭代,那就特别繁琐)。为了改善这一点,我们将添加go.mod和go.sum依赖项并在我们复制其余源代码之前运行go mod下载 - 这样做,Docker构建缓存只会重新下载依赖项当.mod或go.sum文件已更改:
这比建立Alpine基本image要多得多,我就是这样部署的。与Alpine相比,它的尺寸优势仍然只有5 MB左右,虽然这个比例很大,但对工作流程或部署的影响几乎不可察觉(很大程度上是由于缓存)。选择scratch的最大原因是安全性-减少攻击面以及所有这些 - 这可能比我原先想象的更实际。