Kubernetes best practices: How and why to build small container images 筆記

今天來講講 Google Cloud 之前發布的一系列文章:Kubernetes best practices,我也是無意中看到,但我覺得寫得滿好的,也是值得筆記的。這篇文章的主題是關於為什麼 build small container images 這件事情很重要且它的好處是什麼。

以下的 build image 的 example 都放在 GitHub 上:https://github.com/KennyChenFight/build-small-image

Containerizing interpreted languages

我們在寫 Python 或是 Node.JS 的時候,由於這兩種語言是屬於 interpreter language,不需要像 Go 那樣是透過 compile 的方式,因此在 build container image 的時候需要有其語言的 interperter 在 image 環境,才可以順利 run your code。

先來看如果用無壓縮的 image 版本來 build node.js 會怎樣:

1
2
3
4
FROM node
EXPOSE 8080
COPY server.js .
CMD node server.js
1
2
REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
my-node-app latest e4cdf4e56293 11 minutes ago 991MB

接近要 1GB 的大小,非常得驚人。

但如果採用 Alpine Linux-based Dockerfile 的話:

1
2
3
4
FROM node:alpine
EXPOSE 8080
COPY server.js .
CMD node server.js
1
2
3
REPOSITORY                           TAG          IMAGE ID       CREATED          SIZE
my-node-app alpine dc9c01035dae 12 minutes ago 167MB
my-node-app latest e4cdf4e56293 13 minutes ago 991MB

alpine 的版本只需要 167MB,大小差非常多。

原因在於一般的官方採用的 image 默認是採用 Ubuntu 作為 Base 去 build 的,但是一般我們開發的應用程式並不需要那麼完整的 Linux 功能,只需要有一些功能就可,所以 Alpine Linux-based 就是主打提供輕量化的 Linux 環境,所以會建議使用 Alpine 版的 image 並且根據應用程式需要什麼額外功能在另外寫在 Dockerfile 上去下載你想要的功能就好了。

Containerizing compiled languages

那我們也來看看如果是 compiled languages 的版本其大小又有什麼差別:

1
2
3
4
5
6
FROM golang
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
EXPOSE 8080
ENTRYPOINT ./goapp
1
2
REPOSITORY                           TAG          IMAGE ID       CREATED              SIZE
my-go-app onbuild e37cfc417f19 About a minute ago 999MB

一樣,大小高達要 1GB。

1
2
3
4
5
6
FROM golang:alpine
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
EXPOSE 8080
ENTRYPOINT ./goapp
1
2
3
REPOSITORY                           TAG          IMAGE ID       CREATED              SIZE
my-go-app alpine a835070e2540 44 seconds ago 359MB
my-go-app onbuild e37cfc417f19 About a minute ago 999MB

大小降到 359 MB,差距非常的大。

但是我們還是可以再降低 image size,原因在於編譯語言,可以將應用程式編譯成可執行檔案就可以了,那麼這個 container 可以省掉不用需要 Go 語言環境的成本。

因此,在這邊我們可以使用 Docker 提供的 Multi-stage builds 功能,要知道 docker 跑每一個 Dockerfile 內的指令都會為 image 添加 layer,這個 layer 會增加整個 image size 的大小,所以如果沒有 Multi-state builds 的功能,必須在最後 build 完成前將 build 過程中需要的功能砍掉等瑣碎的動作來將 image 的大小縮小。

如果有 Multi-state builds 的功能,我們可以複用前一個 layer 的環境,可以看個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM golang:alpine AS build-env
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp

FROM alpine
RUN apk update && \
apk add ca-certificates && \
update-ca-certificates && \
rm -rf /var/cache/apk/*
WORKDIR /app
COPY --from=build-env /app/goapp /app
EXPOSE 8080
ENTRYPOINT ./goapp

這邊可以看成有兩階段的 build 過程,第一階段是拉 golang:alpine 來負責 build 出可執行檔,接著第二階段的是拉 alpine image,少了 golang 的環境在裡面,並且將第一階段 build 出來的可執行檔案 copy 到第二階段的 image 裏面,這樣的話我們就可以直接執行我們應用程式,而少了 Golang 城市環境的成本。

這邊可以看到如果你在自己的 image 想要有 https 證書之類的東西,你也可以在第二階段去跑安裝的腳本,這樣 image 只放你需要的功能,那大小自然就能降低了。

1
2
3
4
REPOSITORY                           TAG          IMAGE ID       CREATED              SIZE
my-go-app multistage 87583010fe39 6 seconds ago 12.6MB
my-go-app alpine a835070e2540 44 seconds ago 359MB
my-go-app onbuild e37cfc417f19 About a minute ago 999MB

可以看到才 12.6 MB,非常的小,原本官方接近的 1GB 居然可以下降這麼多。

但是,還可以再更小嗎?

可以使用 Google 提供的 distroless image:https://github.com/GoogleContainerTools/distroless

“Distroless” images contain only your application and its runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution.

Distroless images are very small. The smallest distroless image, gcr.io/distroless/static-debian11, is around 2 MiB. That’s about 50% of the size of alpine (~5 MiB), and less than 2% of the size of debian (124 MiB).

沒錯,在砍掉 package manager, shell 的東西後,distroless 可以比 alpine 的 image 還要小。

1
2
3
4
5
6
7
8
9
FROM golang:alpine AS build-env
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
FROM gcr.io/distroless/static-debian11
WORKDIR /app
COPY --from=build-env /app/goapp /app
EXPOSE 8080
ENTRYPOINT ["goapp"]
1
2
3
4
5
REPOSITORY                           TAG          IMAGE ID       CREATED             SIZE
my-go-app distroless ebe938f155e3 3 minutes ago 8.86MB
my-go-app multistage 87583010fe39 39 minutes ago 12.6MB
my-go-app alpine a835070e2540 39 minutes ago 359MB
my-go-app onbuild e37cfc417f19 40 minutes ago 999MB

可以看比較,比 multistage 的版本再少了 4MB,主要的差別來自於第二階端的 image 採用了 distroless,才得以降低 image size。

Evaluating performance of smaller containers

最後來看看,如果有 smaller containers 帶來什麼好處。

  1. 降低 build time and pull time,當然這些好處是毫無疑問,但是 pull time 就值得探討了,要知道當我們 auto scale pod 在 kubernetes 的時候,每個 pod 都需要去 pull image 那這樣,帶來的好處就很大了,一但拉取的容器所需的時間越短,那麼你的 cluster 的運作不正常的時間就會越短。

  2. 再來是考慮安全性及漏洞的問題,如果你採用越小型的容器,那就代表你那個容器本身能做的事情本來就少,那麼漏洞自然就會減少。該篇文章有秀出透過 Google Cloud 的 Container Registry 的漏洞掃描,可以看出越大型的容器其漏洞就越多:

    p1

    這是因為很多漏洞都會來自於該 image 的 OS System 或是附帶的工具,但如果這些工具你都先拿掉了,因為你本身應用程式就用不到,那自然漏洞就降低囉。

總結

所以透過該篇文章我們可以知道 Build Small Containers 所帶來的好處,下一次當在寫 Dockerfile 的時候好好想想自己的 base image 該選怎樣的 image 來出發,並且善用 multi-stage build 的方式可以更好的管理 Dockerfile 的命令,並 build 好最終的結果。