常用容器鏡像構(gòu)建工具和方案介紹
在使用 Docker 的時(shí)候一般情況下我們都會(huì)直接使用 docker build 來(lái)構(gòu)建鏡像,切換到 Containerd 的時(shí)候,上節(jié)我們也介紹了可以使用 nerdctl + buildkit 來(lái)構(gòu)建容器鏡像,除了這些方式之外,還有其他常見(jiàn)的鏡像構(gòu)建工具嗎?
接下來(lái)我們就來(lái)介紹下在 Containerd 容器運(yùn)行時(shí)下面鏡像構(gòu)建的主要工具和方案。
使用 Docker 做鏡像構(gòu)建服務(wù)
在 Kubernetes 集群中,部分 CI/CD 流水線業(yè)務(wù)可能需要使用 Docker 來(lái)提供鏡像打包服務(wù)。可通過(guò)宿主機(jī)的 Docker 實(shí)現(xiàn),將 Docker 的 UNIX Socket(/var/run/docker.sock) 通過(guò) hostPath 掛載到 CI/CD 的業(yè)務(wù) Pod 中,之后在容器里通過(guò) UNIX Socket 來(lái)調(diào)用宿主機(jī)上的 Docker 進(jìn)行構(gòu)建,這個(gè)就是之前我們使用較多的 Docker outside of Docker 方案。該方式操作簡(jiǎn)單,比真正意義上的 Docker in Docker 更節(jié)省資源,但該方式可能會(huì)遇到以下問(wèn)題:
- 無(wú)法運(yùn)行在 Runtime 是 containerd 的集群中。
- 如果不加以控制,可能會(huì)覆蓋掉節(jié)點(diǎn)上已有的鏡像。
- 在需要修改 Docker Daemon 配置文件的情況下,可能會(huì)影響到其他業(yè)務(wù)。
- 在多租戶的場(chǎng)景下并不安全,當(dāng)擁有特權(quán)的 Pod 獲取到 Docker 的 UNIX Socket 之后,Pod 中的容器不僅可以調(diào)用宿主機(jī)的 Docker 構(gòu)建鏡像、刪除已有鏡像或容器,甚至可以通過(guò) docker exec 接口操作其他容器。
對(duì)于部分需要 containerd 集群,而不改變 CI/CD 業(yè)務(wù)流程仍使用 Docker 構(gòu)建鏡像一部分的場(chǎng)景,我們可以通過(guò)在原有 Pod 上添加 DinD 容器作為 Sidecar 或者使用 DaemonSet 在節(jié)點(diǎn)上部署專門(mén)用于構(gòu)建鏡像的 Docker 服務(wù)。
使用 DinD 作為 Pod 的 Sidecar
如下所示,我們有一個(gè)名為 clean-ci 的容器,會(huì)該容器添加一個(gè) Sidecar 容器,配合 emptyDir,讓 clean-ci 容器可以通過(guò) UNIX Socket 訪問(wèn) DinD 容器:
- apiVersion: v1
- kind: Pod
- metadata:
- name: clean-ci
- spec:
- containers:
- - name: dind
- image: 'docker:stable-dind'
- command:
- - dockerd
- - --host=unix:///var/run/docker.sock
- - --host=tcp://0.0.0.0:8000
- securityContext:
- privileged: true
- volumeMounts:
- - mountPath: /var/run
- name: cache-dir
- - name: clean-ci
- image: 'docker:stable'
- command: ["/bin/sh"]
- args: ["-c", "docker info >/dev/null 2>&1; while [ $? -ne 0 ] ; do sleep 3; docker info >/dev/null 2>&1; done; docker pull library/busybox:latest; docker save -o busybox-latest.tar library/busybox:latest; docker rmi library/busybox:latest; while true; do sleep 86400; done"]
- volumeMounts:
- - mountPath: /var/run
- name: cache-dir
- volumes:
- - name: cache-dir
- emptyDir: {}
通過(guò)上面添加的 dind 容器來(lái)提供 dockerd
服務(wù),然后在業(yè)務(wù)構(gòu)建容器中通過(guò) emptyDir{} 來(lái)共享 /var/run 目錄,業(yè)務(wù)容器中的 docker 客戶端就可以通過(guò) unix:///var/run/docker.sock 來(lái)與 dockerd 進(jìn)行通信。
使用 DaemonSet 在每個(gè) containerd 節(jié)點(diǎn)上部署 Docker除了上面的 Sidecar 模式之外,還可以直接在 containerd 集群中通過(guò) DaemonSet 來(lái)部署 Docker,然后業(yè)務(wù)構(gòu)建的容器就和之前使用的模式一樣,直接通過(guò) hostPath 掛載宿主機(jī)的 unix:///var/run/docker.sock 文件即可。
使用以下 YAML 部署 DaemonSet。示例如下:
- apiVersion: apps/v1
- kind: DaemonSet
- metadata:
- name: docker-ci
- spec:
- selector:
- matchLabels:
- app: docker-ci
- template:
- metadata:
- labels:
- app: docker-ci
- spec:
- containers:
- - name: docker-ci
- image: 'docker:stable-dind'
- command:
- - dockerd
- - --host=unix:///var/run/docker.sock
- - --host=tcp://0.0.0.0:8000
- securityContext:
- privileged: true
- volumeMounts:
- - mountPath: /var/run
- name: host
- volumes:
- - name: host
- hostPath:
- path: /var/run
上面的 DaemonSet 會(huì)在每個(gè)節(jié)點(diǎn)上運(yùn)行一個(gè) dockerd 服務(wù),這其實(shí)就類似于將以前的 docker 服務(wù)放入到了 Kubernetes 集群中進(jìn)行管理,然后其他的地方和之前沒(méi)什么區(qū)別,甚至都不需要更改以前方式的任何東西。將業(yè)務(wù)構(gòu)建 Pod 與 DaemonSet 共享同一個(gè) hostPath,如下所示:
- apiVersion: v1
- kind: Pod
- metadata:
- name: clean-ci
- spec:
- containers:
- - name: clean-ci
- image: 'docker:stable'
- command: ["/bin/sh"]
- args: ["-c", "docker info >/dev/null 2>&1; while [ $? -ne 0 ] ; do sleep 3; docker info >/dev/null 2>&1; done; docker pull library/busybox:latest; docker save -o busybox-latest.tar library/busybox:latest; docker rmi library/busybox:latest; while true; do sleep 86400; done"]
- volumeMounts:
- - mountPath: /var/run
- name: host
- volumes:
- - name: host
- hostPath:
- path: /var/run
Kaniko
Kaniko 是 Google 開(kāi)源的一款容器鏡像構(gòu)建工具,可以在容器或 Kubernetes 集群內(nèi)從 Dockerfile 構(gòu)建容器鏡像,Kaniko 構(gòu)建容器鏡像時(shí)并不依賴于 docker daemon,也不需要特權(quán)模式,而是完全在用戶空間中執(zhí)行 Dockerfile 中的每條命令,這使得在無(wú)法輕松或安全地運(yùn)行 docker daemon 的環(huán)境下構(gòu)建容器鏡像成為了可能。
kaniko
Kaniko 構(gòu)建容器鏡像時(shí),需要使用 Dockerfile、構(gòu)建上下文、以及構(gòu)建成功后鏡像在倉(cāng)庫(kù)中的存放地址。此外 Kaniko 支持多種方式將構(gòu)建上下文掛載到容器中,比如可以使用本地文件夾、GCS bucket、S3 bucket 等方式,使用 GCS 或者 S3 時(shí)需要把上下文壓縮為 tar.gz,kaniko 會(huì)自行在構(gòu)建時(shí)解壓上下文。
Kaniko executor 讀取 Dockerfile 后會(huì)逐條解析 Dockerfile 內(nèi)容,一條條執(zhí)行命令,每一條命令執(zhí)行完以后會(huì)在用戶空間下面創(chuàng)建一個(gè) snapshot,并與存儲(chǔ)與內(nèi)存中的上一個(gè)狀態(tài)進(jìn)行比對(duì),如果有變化,就將新的修改生成一個(gè)鏡像層添加在基礎(chǔ)鏡像上,并且將相關(guān)的修改信息寫(xiě)入鏡像元數(shù)據(jù)中,等所有命令執(zhí)行完,kaniko 會(huì)將最終鏡像推送到指定的遠(yuǎn)端鏡像倉(cāng)庫(kù)。。整個(gè)過(guò)程中,完全不依賴于 docker daemon。
如下所示我們有一個(gè)簡(jiǎn)單的 Dokerfile 示例:
- FROM alpine:latest
- RUN apk add busybox-extras curl
- CMD ["echo","Hello Kaniko"]
然后我們可以啟動(dòng)一個(gè) kaniko 容器去完成上面的鏡像構(gòu)建,當(dāng)然也可以直接在 Kubernetes 集群中去運(yùn)行,如下所示新建一個(gè) kaniko 的 Pod 來(lái)構(gòu)建上面的鏡像:
- apiVersion: v1
- kind: Pod
- metadata:
- name: kaniko
- spec:
- containers:
- - name: kaniko
- image: gcr.io/kaniko-project/executor:latest
- args: ["--dockerfile=/workspace/Dockerfile",
- "--context=/workspace/",
- "--destination=cnych/kaniko-test:v0.0.1"]
- volumeMounts:
- - name: kaniko-secret
- mountPath: /kaniko/.docker
- - name: dockerfile
- mountPath: /workspace/Dockerfile
- subPath: Dockerfile
- volumes:
- - name: dockerfile
- configMap:
- name: dockerfile
- - name: kaniko-secret
- projected:
- sources:
- - secret:
- name: regcred
- items:
- - key: .dockerconfigjson
- path: config.json
上面的 Pod 執(zhí)行的 args 參數(shù)中,主要就是指定 kaniko 運(yùn)行時(shí)需要的三個(gè)參數(shù): Dockerfile、構(gòu)建上下文以及遠(yuǎn)端鏡像倉(cāng)庫(kù)。
推送至指定遠(yuǎn)端鏡像倉(cāng)庫(kù)需要 credential 的支持,所以需要將 credential 以 secret 的方式掛載到 /kaniko/.docker/ 這個(gè)目錄下,文件名稱為 config.json,內(nèi)容如下:
- {
- "auths": {
- "https://index.docker.io/v1/": {
- "auth": "AbcdEdfgEdggds="
- }
- }
- }
其中 auth 的值為: docker_registry_username:docker_registry_password base64 編碼過(guò)后的值。然后 Dockerfile 通過(guò) Configmap 的形式掛載進(jìn)去,如果構(gòu)建上下文中還有其他內(nèi)容也需要一同掛載進(jìn)去。
關(guān)于 kaniko 的更多使用方式可以參考官方倉(cāng)庫(kù):https://github.com/GoogleContainerTools/kaniko。
Jib
如果你是在 Java 環(huán)境下面,還可以使用 Jib 來(lái)構(gòu)建鏡像,Jib 也是 Google 開(kāi)源的,只是是針對(duì) Java 容器鏡像構(gòu)建的工具。
Jib
通過(guò)使用 Jib,Java 開(kāi)發(fā)人員可以使用他們熟悉的 Java 工具來(lái)構(gòu)建鏡像。Jib 是一個(gè)快速而簡(jiǎn)單的容器鏡像構(gòu)建工具,它負(fù)責(zé)處理將應(yīng)用程序打包到容器鏡像中所需的所有步驟,它不需要你編寫(xiě) Dockerfile 或安裝 Docker,而且可以直接集成到 Maven 和 Gradle 中,只需要將插件添加到構(gòu)建中,就可以立即將 Java 應(yīng)用程序容器化。
Jib 利用了 Docker 鏡像的分層機(jī)制,將其與構(gòu)建系統(tǒng)集成,并通過(guò)以下方式優(yōu)化 Java 容器鏡像的構(gòu)建:
- 簡(jiǎn)單:Jib 使用 Java 開(kāi)發(fā),并作為 Maven 或 Gradle 的一部分運(yùn)行。你不需要編寫(xiě) Dockerfile 或運(yùn)行 Docker 守護(hù)進(jìn)程,甚至無(wú)需創(chuàng)建包含所有依賴的大 JAR 包。因?yàn)?Jib 與 Java 構(gòu)建過(guò)程緊密集成,所以它可以訪問(wèn)到打包應(yīng)用程序所需的所有信息。
- 快速:Jib 利用鏡像分層和緩存來(lái)實(shí)現(xiàn)快速、增量的構(gòu)建。它讀取你的構(gòu)建配置,將你的應(yīng)用程序組織到不同的層(依賴項(xiàng)、資源、類)中,并只重新構(gòu)建和推送發(fā)生變更的層。在項(xiàng)目進(jìn)行快速迭代時(shí),Jib 只將發(fā)生變更的層(而不是整個(gè)應(yīng)用程序)推送到鏡像倉(cāng)庫(kù)來(lái)節(jié)省寶貴的構(gòu)建時(shí)間。
- 可重現(xiàn):Jib 支持根據(jù) Maven 和 Gradle 的構(gòu)建元數(shù)據(jù)進(jìn)行聲明式的容器鏡像構(gòu)建,因此,只要輸入保持不變,就可以通過(guò)配置重復(fù)創(chuàng)建相同的鏡像。
以下示例將使用 Jib 提供的 gradle 插件集成到一個(gè) spring boot 項(xiàng)目的構(gòu)建中,并展示 Jib 如何簡(jiǎn)單快速的構(gòu)建鏡像。
首先,在項(xiàng)目的 build.gradle 構(gòu)建文件中引入 jib 插件:
- buildscript{
- ...
- dependencies {
- ...
- classpath "gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.1.2"
- }
- }
- apply plugin: 'com.google.cloud.tools.jib'
如果需要配置相關(guān)參數(shù),可以使用下面的 gradle 配置:
- jib {
- from {
- image = 'harbor.k8s.local/library/base:1.0'
- auth {
- username = '********'
- password = '********'
- }
- }
- to {
- image = 'harbor.k8s.local/library/xxapp:1.0'
- auth {
- username = '********'
- password = '********'
- }
- }
- container {
- jvmFlags = ['-Djava.security.egd=file:/dev/./urandom']
- ports = ['8080']
- useCurrentTimestamp = false
- workingDirectory = "/app"
- }
- }
然后執(zhí)行以下命令就可以直接觸發(fā)構(gòu)建生成容器鏡像了:
- # 構(gòu)建 jib.to.image 指定的鏡像,并且推送至鏡像倉(cāng)庫(kù)
- $ gradle jib
如果你還想將構(gòu)建的鏡像保存到本地 dockerd,則可以使用下面的命令構(gòu)建:
- gradle jibDockerBuild
當(dāng)然還有前文我們介紹的 buildkit 可以用于鏡像構(gòu)建,還有一個(gè)經(jīng)常和 Podman 搭配使用的 Buildah,是一個(gè)可以用于構(gòu)建符合 OCI 標(biāo)準(zhǔn)容器鏡像的命令行工具,有了這些工具,在構(gòu)建容器鏡像時(shí)已經(jīng)完全可以脫離 docker daemon 了,而且這些工具都能很好的與 Kubernetes 集成,支持在容器環(huán)境下完成構(gòu)建。