【Docker】使用Go语言进行Docker的最佳实践

はじめに

这篇文章是关于以Go语言镜像为例来尝试实践Docker的最佳实践。

本文的目的是为了帮助那些已经学会了Docker的使用方法,却不知道应该使用什么样的Dockerfile的人们提供一个例子。

环境

    Docker: Docker version 20.10.23, build v20.10.23

Dockerfile示例

这是根据Docker的最佳实践创建的Dockerfile,请参考。

FROM golang:1.21.0-bullseye AS base
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod/ \
    go mod download

FROM base AS dev
RUN go install github.com/go-delve/delve/cmd/dlv@latest && \
    go install github.com/cosmtrek/air@latest
COPY . .
CMD ["air", "-c", ".air.toml"]

FROM base AS builder
COPY . .
RUN useradd -u 10001 scratch
RUN --mount=type=cache,target=/go/pkg/mod/ \
    go build \
    -ldflags="-s -w" \
    -o golang-app \
    -trimpath

FROM scratch AS runner
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /app/golang-app golang-app
USER scratch
CMD ["/golang-app"]

选择合适的基础图像。

关于安全的最佳做法,有以下描述。

要实现一张安全镜像的首要步骤是选择正确的基础镜像。在选择镜像时,确保它来自可信任的源并且保持较小的体积。

今回のようにDockerfileから独自のイメージを構築する際には、要件にあった最小限のベースイメージを選択するべきとされています。

主なメリットはプル時間の短縮とセキュリティの向上です。イメージサイズが小さいということはそこに含まれているパッケージ数も少ないということであり、攻撃者が狙う脆弱性の数も少なくなります。

軽量なベースイメージを選択するという意味では、今回採用した1.21.0-bullseyeよりも軽量な1.21.0-alpine3.18があります。

※阿尔卑斯镜像无法成功使用useradd,我放弃了。后来得知,在阿尔卑斯镜像中,应该使用adduser或addgroup命令,而不是useradd。

 

利用图层缓存

Dockerのビルドキャッシュについて知ることで、ビルド時間を短縮できます。

Dockerfile中的每个指令对应一个层次。

レイヤーを構築する命令について
Best practices for writing Dockerfilesに以下のような記述があることから、厳密には1命令1レイヤーではないようです。

Only the instructions RUN, COPY, and ADD create layers. Other instructions create temporary intermediate images, and don’t increase the size of the build.

例如,如果我们试着使用docker history命令来确认runner阶段的图像,我们会发现有四个层次。

$ docker history --no-trunc golang:runner
IMAGE                                                                     CREATED          CREATED BY                                                                                                     SIZE      COMMENT
sha256:f5269c93a601261f48922e3bbe3ea20c9dd27fe8d908127b53afdb0950a81f36   44 seconds ago   /bin/sh -c #(nop)  CMD ["/golang-app"]                                                                         0B        
sha256:6de04a2d12be7dcf72177582715cfc2037faaf23ba2d8c061c0d0b4c7bfdb298   45 seconds ago   /bin/sh -c #(nop)  USER scratch                                                                                0B        
sha256:bfbf3472db0a1f864eb242aa4d261412617844d6b523388a2c1f431e72acd4de   45 seconds ago   /bin/sh -c #(nop) COPY file:ae42242b7bba1bd208972c75cbaab0a93f64777c4969c6985e9426583c62c525 in golang-app     1.33MB    
sha256:3e65b3f3dac37b22019fe4a98dc849e6b03caee141b95372d1f5ee854cb14551   46 seconds ago   /bin/sh -c #(nop) COPY file:b0213a4138a2af371e3368129198de30da15688fcbc16fb3becede1c4a2f7f93 in /etc/passwd    967B      

每次更改图层时都需要进行重建。当图层发生更改时,其后面的所有图层都会被重新构建,这会增加构建时间。

重要的是将相对较少频繁更改的指令(如安装依赖关系)放在上面,将频繁更改的指令(如复制源文件)放在下面。

また.dockerignoreファイルを活用して不要なファイルをビルドコンテキストから除外することでもキャッシュの最適化をはかることができます。

node_modules

使用BuildKit

BuildKitはより新しいビルダーです。BuildKitはバージョン23.0からDocker DesktopとDocker Engineのデフォルトビルダーになっています。

通过使用 BuildKit,可以提高构建性能,例如跳过不必要的构建阶段、并行构建阶段的建立等。

またキャッシュマウントが使用できるようになり、パッケージをインストールする命令を高速化するのに役立ちます。キャッシュマウントでビルド中に使用するパッケージキャッシュを指定することで、レイヤーを再構築する場合でも、新しいパッケージや変更されたパッケージのみをダウンロードするようになります。

RUN --mount=type=cache,target=/go/pkg/mod/ \
    go mod download

–mount=type=cache,target=という形式でキャッシュのディレクトリを指定します。

在Go语言中,您可以使用以下命令来查找目录所在位置。

$ go env | grep CACHE

组合多个命令

複数の命令は組み合わせることでレイヤーの数を最小限に抑えることができます。

たとえば上述のDockerfileではRUN命令をアンパサンド2つ(&&)とバックスラッシュ(\)で1つにまとめています。

# RUN go install github.com/cosmtrek/air@latest
# RUN go install github.com/go-delve/delve/cmd/dlv@latest

RUN go install github.com/cosmtrek/air@latest && \
    go install github.com/go-delve/delve/cmd/dlv@latest

複数行にまたがる引数は英数順に並べ替えることでメンテナンス性が向上し、重複を防ぐことができます。

RUN --mount=type=cache,target=/go/pkg/mod/ \
    go build \
    -ldflags="-s -w" \
    -o golang-app \
    -trimpath

使用多阶段构建

通过使用多阶段构建,您可以将仅真正需要的内容包含在最终映像中。

在使用多阶段构建时,您可以通过编写多个FROM指令来实现。对于上述的Dockerfile示例,它由4个阶段——base、dev、builder和runner组成。

我想比较一下基础、开发和运行阶段的图像尺寸。

$ docker images     
REPOSITORY                   TAG               IMAGE ID       CREATED          SIZE
golang                       runner            522b09cddc5b   27 seconds ago   1.39MB
golang                       dev               4d8bd97eff7b   2 hours ago      1.1GB
golang                       base              186cddd98d38   2 hours ago      770MB

由于安装Go语言的依赖关系和开发工具等,dev阶段的镜像大小超过了1GB。

同时,我们可以看出,Runner阶段的图像尺寸约为1.4MB,非常小。

使用非根用户

为了尽量减少容器遭受攻击时的风险,我们要求以非根用户的身份运行容器。

USER scratch

使用基于scratch镜像的非特权容器运行方法,可以参考非特权容器 based on the scratch image。

利用热重载调试工具

Go言語にもnodemonのようにホット リロードを提供してくれるAirというツールがあります。devステージのCMD命令で実行しているのがAirです。.air.tomlファイルはサンプルをそのまま使用しました。

请查阅Air的Github存储库以获取更详细的信息。

DelveはGo言語のデバッガーツールです。ステップ実行などさまざまなデバッグ機能を提供してくれます。

请查阅Delve的Github存储库以获取更详细的信息。

确认图像的脆弱性

通过使用脆弱性检测工具,可以扫描Docker镜像中的脆弱性。

脆弱性検出ツールの1つにGrypeがあります。今回はこのツールを使用してDockerイメージの脆弱性を検査してみたいと思います。

Tutorial: Get started with Goのrsc.io/quoteパッケージを使用したコードを使います。

我将使用Grype扫描构建的runner舞台镜像的漏洞。

$ grype golang:runner --scope all-layers
NAME               INSTALLED                           FIXED-IN  TYPE       VULNERABILITY        SEVERITY 
golang.org/x/text  v0.0.0-20170915032832-14c0d48ead0c  0.3.7     go-module  GHSA-ppp9-7jff-5vj2  High      
golang.org/x/text  v0.0.0-20170915032832-14c0d48ead0c  0.3.8     go-module  GHSA-69ch-w2m2-3vjp  High      

我注意到 golang.org/x/text 包存在漏洞。据说已在 0.3.8 及以上版本进行修复,因此需要更新该包,然后再次进行漏洞扫描。

$ grype golang:runner --scope all-layers
No vulnerabilities found

通过更新 golang.org/x/text 包,已不再发现存在脆弱性。

请查看 Grype 的 Github 仓库获取更详细的信息。

整理

本篇文章中,我們參考了Docker的最佳實踐,為Go語言編寫了一個Dockerfile。

希望这个例子可以成为适用于Go语言的Dockerfile的一个示例。

广告
将在 10 秒后关闭
bannerAds