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