为了在云环境中完成代码部署,最后一步是配置和部署 Pod 的容器映像。目的地就在拐角处,但是仍然需要考虑一个问题:并非每个映像都适合部署,需要满足两个标准。
- 足够小。映像大小在某种程度上决定了我们的云开销,当 Pod 的规模扩大到数千个或数万个时,这种开销会随之增加,最终导致额外的实例成本(以 GKE E2 为例,最大为 128GB)。所以,越小越好。
- 安全。为了最小化损害映像安全性的可能性,在构建映像时应该注意,例如 不要直接复制
Dockerfile
中的 KEY,特别是如果服务部署在公共云上。
言归正传。让我们从构建 Go 映像(Go 1.18)开始,然后逐步进行优化。
构建 Go 映像
在我的上一篇文章中,我使用 Go 构建了一个缓存包,只能作为第三方被其他 Go 代码引用。为了使它成为一个独立的缓存服务,你只需要
- 创建
Put/Get
接口。这一点不是我们要扩展的重点,但如果感兴趣,你可以参考代码。 - 创建一个
Dockerfile
并将其部署到云端,这是我们今天的主要关注点。
在编写 Dockerfile
之前,我们应该始终了解它的含义:
一组命令,用于构建和独立运行容器中的代码。
在 Dockerfile
中应该包括什么?答案在于运行 Go 程序所需的两个步骤。
- 编译,运行
go build main.go
。 - 执行,运行
go run main.go
。
这两个步骤都需要一些前提条件,例如 Go 包和当前目录中的所有文件,如 go.mod
,需要用于编译代码;Web 程序需要暴露端口 8000。现在似乎更清楚了,我们应该在 Dockerfile
中包含以下 6 行。
# Base image, golang 1.18
FROM golang:1.18.3
WORKDIR /workspace
# Copy all files into the image
COPY . .
# Run go mod
RUN go mod download
# Expose ports
EXPOSE 8000
# Run Go program, just like locally
ENTRYPOINT ["go","run","main.go"]
现在运行docker build . -t localcache_srv
查看映像。
糟糕,映像大小几乎达到了 1GB!一定有问题。
让我们看看映像中有什么,并找出原因。运行 docker image history localcache_srv
并查看构建历史记录。
如图所示,与 Dockfile 相关的内容和复制的本地文件仅占约 7M,而大部分内容来自基础映像 golang:1.18.3
,它由 Go 官方在 Dockerhub 上发布,用于编译 Go 代码,包括完整的 Go 安装包,如 gcc
、g++
、make
和其他工具。
如果我们放弃这个版本的 Golang 基础映像,是否有替代方案?是的,当然有。
- 使用较小的 Golang 基础映像。
在 Dockerhub 中搜索 Golang 的 alpine 版本,它的映像总是被简化并且可以使用 docker pull golang:alpine
查看大小。
用它替换之前的 golang:1.18.3
,然后重新运行 docker build
,我们可以获得一个更小的映像。
localcache-go-alpine latest 17e5c879ba48 3 seconds ago 335MB
通过这种方式,我们可以将映像大小缩小到 330MB。
- 不再使用 Golang 基础映像。
使用本地编译的 main
文件,可以将文件直接复制到映像中以在容器中运行代码。请参见下面的 Dockerfile
。
FROM debian:latest
WORKDIR /workspace
COPY main /workspace
EXPOSE 8000
ENTRYPOINT ["./main"]
映像大小进一步缩小到 130MB。
然而,以上两种方法都可以缩小映像大小,但都不够高效和可持续,因为你需要在打包之前手动编译。
那么我们可以寻求其他优化吗?是的,请继续阅读。
优化 Go 映像
多阶段构建
如上所述,Dockerfile 应该包含编译和运行两个步骤,其中只有运行步骤需要部署到容器中。这就是 Docker 的多阶段构建可以应用的地方,将 Dockerfile 分成两个阶段,compile
和 run
,后者实际上启动容器。
多阶段构建对于想要优化 Dockerfile 但仍保持易于阅读和维护的人非常有用。
——来自 https://docs.docker.com/develop/develop-images/multistage-build/这也是构建 Go 的 Docker 镜像的推荐方法。更多信息请参见官方文章。
为了将 Dockerfile 转换为多阶段构建,我们需要:
- 在构建器阶段使用
golang:alpine
作为基础镜像。 - 仅复制编译阶段所需的文件,忽略
main
文件。在某些情况下,这是一个好习惯,因为编译后的文件可能会达到数百 MB 的大小。 - 在运行阶段使用
debian
作为基础镜像。
新的 Dockerfile 如下:
重复执行 docker build
,我们可以看到新镜像仅有 131 MB,builder
阶段的镜像被忽略了。目标达成!
此时,Dockerfile 优化已经初步完成,生成的版本与原始版本相比大小减小了 1/8,可以视为生产就绪。
但不要止步于此,尝试让镜像更小。
Go 构建标志
在构建器阶段,我们使用 GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build main.go
命令,使用 CGO_ENABLED
环境变量 避免构建可能缺少的 C 文件,因为该部分库在 golang:alpine
中可能缺失。但是,如果使用了 CGO 功能,则需要其他标志。
此外,我们可以添加 -w -s
构建标志,从二进制文件中删除调试信息。
再次运行 docker build . -t localcache:v2
,我们可以发现新镜像缩小了另外 2MB。
通过检查 image history
,我们可以确认缩小是由编译文件引起的。
Debian、Alpine 或 Scratch
我们在运行阶段使用 debian
作为基础镜像,但不使用其中一些内置工具,如 bash。作为纯 Go 镜像,我们可以将其替换为 alpine(5.53MB),将镜像大小缩小至 10.1 MB。
更夸张的是,如果我们使用 scratch 基础镜像,这是一个专门用于从头构建镜像的明确空镜像,以替换 alpine,则镜像大小将减小到 4.5 MB。
这个镜像对于这个缓存服务来说非常完美,简单高效!我们可以运行一个测试来验证。
- 在 8000 上启动一个容器。
- 使用 curl 访问
put
和get
API,并将它们放入slaise:123
中,获得123
。
从 972MB -> 4.57MB,完美,对吧?
在上面的示例中,我们使用了 scratch
,但这并不总是适用的,这取决于所需的镜像内容。
例如,如果你使用 scratch
,并需要任何其他库或工具,例如 CA 和时区,则必须手动安装它们。这听起来非常复杂和容易出错,这就是为什么 debian
被设置为许多公司的标准配置的原因。
我们可以在 Go 中使用 scratch
,这是一种静态编译的语言。那么 Rust 和 C 呢?它们不是完全静态编译的。Java 和 Python 呢?我们根据语言进行选择。以下是最常用的基础镜像,你可以从中选择最合适的镜像。
scratch
,完全空的基础镜像。distroless/static-debian
,建立在 scratch 之上,大小约为 2MB,包括CA
、root user
等。它的大小只有alpine
的一半,被许多人用作 Go 基础镜像。alpine
,通用基础镜像,支持许多场景。虽然提供的工具选择较少,但足以支持常规用例,并被认为比debian
更安全。debian
,包含大多数工具,如ssh
、curl
等。
其他技巧
还有其他一些缩小大小的小技巧。
- 最小化层数。
像 COPY
和 ADD
这样的命令会在复制前构建一个新层,导致镜像大小不断膨胀。解决方法是将命令行组合在一起,例如将我们示例中的 4 行 COPY
命令组合成一行。
COPY go.mod go.sum localcache/ main.go /workspace
如果有许多 bash 命令,请使用 pipe
将它们组合起来。
- 使用
COPY
而不是ADD
。
通过 Docker 的缓存,COPY
可以加快构建过程。当然,ADD
还有一些额外的功能,请在此处了解更多。# 安全
我不是安全解决方案的专家,不同的部署环境有不同的安全策略。但以下7个提示适用于所有镜像。
- 使用
noroot
用户运行。我们可以手动添加一个用户useradd
,或者使用gcr.io/distroless/static:nonroot
基础镜像来自动包含一个noroot
用户。 - 暴露大于1024的端口。通常情况下,小于1024的端口只能由root用户操作。
- 检查你的Dockerfile,参考Docker官方的最佳实践。
- 选择最小的基础镜像。通常认为,镜像越小,越安全,因为内容越少,漏洞就越少。
- 使用Dockerhub上的最新基础镜像,而不是第三方的,也不是在Github上不受信任的。
- 不要在Dockerfile中放置任何明文密钥,如
GITHUB_KEY
,GKE ServiceAccountKey
等。选择另一种身份验证方法,如workloadidentity
。 - 使用第三方工具如snyk扫描你的Dockerfile。但这是公司级的CI/CD实现。
结束
过大的镜像有时会触发集群中的自动缩放并影响用户。因此,我们必须具备Dockerfile知识,以构建合理大小的镜像或对其进行优化,以避免自动缩放。希望本文能够有所帮助。
谢谢阅读!
参考
https://docs.docker.com/language/golang/build-images/#multi-stage-builds
https://docs.docker.com/develop/develop-images/multistage-build/
https://docs.docker.com/develop/develop-images/dockerfile_best-practices
译自:https://betterprogramming.pub/path-to-a-perfect-go-dockerfile-f7fe54b5c78c
评论(0)