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 | FROM node |
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
接近要 1GB 的大小,非常得驚人。
但如果採用 Alpine Linux-based Dockerfile 的話:
1 | FROM node:alpine |
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
alpine 的版本只需要 167MB,大小差非常多。
原因在於一般的官方採用的 image 默認是採用 Ubuntu 作為 Base 去 build 的,但是一般我們開發的應用程式並不需要那麼完整的 Linux 功能,只需要有一些功能就可,所以 Alpine Linux-based 就是主打提供輕量化的 Linux 環境,所以會建議使用 Alpine 版的 image 並且根據應用程式需要什麼額外功能在另外寫在 Dockerfile 上去下載你想要的功能就好了。
Containerizing compiled languages
那我們也來看看如果是 compiled languages 的版本其大小又有什麼差別:
1 | FROM golang |
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
一樣,大小高達要 1GB。
1 | FROM golang:alpine |
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
大小降到 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 | FROM golang:alpine AS build-env |
這邊可以看成有兩階段的 build 過程,第一階段是拉 golang:alpine
來負責 build 出可執行檔,接著第二階段的是拉 alpine image,少了 golang 的環境在裡面,並且將第一階段 build 出來的可執行檔案 copy 到第二階段的 image 裏面,這樣的話我們就可以直接執行我們應用程式,而少了 Golang 城市環境的成本。
這邊可以看到如果你在自己的 image 想要有 https 證書之類的東西,你也可以在第二階段去跑安裝的腳本,這樣 image 只放你需要的功能,那大小自然就能降低了。
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
可以看到才 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 ofalpine
(~5 MiB), and less than 2% of the size ofdebian
(124 MiB).
沒錯,在砍掉 package manager, shell 的東西後,distroless 可以比 alpine 的 image 還要小。
1 | FROM golang:alpine AS build-env |
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
可以看比較,比 multistage 的版本再少了 4MB,主要的差別來自於第二階端的 image 採用了 distroless,才得以降低 image size。
Evaluating performance of smaller containers
最後來看看,如果有 smaller containers 帶來什麼好處。
-
降低 build time and pull time,當然這些好處是毫無疑問,但是 pull time 就值得探討了,要知道當我們 auto scale pod 在 kubernetes 的時候,每個 pod 都需要去 pull image 那這樣,帶來的好處就很大了,一但拉取的容器所需的時間越短,那麼你的 cluster 的運作不正常的時間就會越短。
-
再來是考慮安全性及漏洞的問題,如果你採用越小型的容器,那就代表你那個容器本身能做的事情本來就少,那麼漏洞自然就會減少。該篇文章有秀出透過 Google Cloud 的 Container Registry 的漏洞掃描,可以看出越大型的容器其漏洞就越多:
這是因為很多漏洞都會來自於該 image 的 OS System 或是附帶的工具,但如果這些工具你都先拿掉了,因為你本身應用程式就用不到,那自然漏洞就降低囉。
總結
所以透過該篇文章我們可以知道 Build Small Containers 所帶來的好處,下一次當在寫 Dockerfile 的時候好好想想自己的 base image 該選怎樣的 image 來出發,並且善用 multi-stage build 的方式可以更好的管理 Dockerfile 的命令,並 build 好最終的結果。