首页
Preview

打造完美的Go语言Dockerfile之路

为了在云环境中完成代码部署,最后一步是配置和部署 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 安装包,如 gccg++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 分成两个阶段,compilerun,后者实际上启动容器。

多阶段构建对于想要优化 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 访问 putget API,并将它们放入 slaise:123 中,获得 123

从 972MB -> 4.57MB,完美,对吧?

在上面的示例中,我们使用了 scratch,但这并不总是适用的,这取决于所需的镜像内容。

例如,如果你使用 scratch,并需要任何其他库或工具,例如 CA 和时区,则必须手动安装它们。这听起来非常复杂和容易出错,这就是为什么 debian 被设置为许多公司的标准配置的原因。

我们可以在 Go 中使用 scratch,这是一种静态编译的语言。那么 Rust 和 C 呢?它们不是完全静态编译的。Java 和 Python 呢?我们根据语言进行选择。以下是最常用的基础镜像,你可以从中选择最合适的镜像。

  • scratch,完全空的基础镜像。
  • distroless/static-debian,建立在 scratch 之上,大小约为 2MB,包括 CAroot user 等。它的大小只有 alpine 的一半,被许多人用作 Go 基础镜像。
  • alpine,通用基础镜像,支持许多场景。虽然提供的工具选择较少,但足以支持常规用例,并被认为比 debian 更安全。
  • debian,包含大多数工具,如 sshcurl 等。

其他技巧

还有其他一些缩小大小的小技巧。

  • 最小化层数。

COPYADD 这样的命令会在复制前构建一个新层,导致镜像大小不断膨胀。解决方法是将命令行组合在一起,例如将我们示例中的 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_KEYGKE 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

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
菜鸟一只
你就是个黄焖鸡,又黄又闷又垃圾。

评论(0)

添加评论