背景

云原生服務的開發迭代,Docker 鏡像作為最終的交付產物,其生命周期中存在多個環節的流轉:從鏡像構建開始,到開發、測試環境的更新驗證,再到交付制品生成,交付到生產環境。在這一整套的流轉過程中,鏡像作為流轉物,只要中間的一個流程出現失誤,很容易導致交付到生產環境的鏡像出現問題。

雖然 Tag 可以讓鏡像有一定的辨識度,但其可以修改的特性,并不能作為辨識身份的標準。 如果構建流程中有需要人工干預修改 Tag 的操作,在交付過程中也會引入更多的風險。鏡像 ID 和 Digests 配合可以作為唯一身份標識確保交付物的一致性,但可讀性比較差,出現交付物不一致的問題,溯源的成本也比較高。


(資料圖片)

docker images --digestsREPOSITORY                                                   TAG                     DIGEST                                                                    IMAGE ID       CREATED         SIZEkoderover.tencentcloudcr.com/koderover-public/aslan          1.15.0-20220922-amd64   sha256:8af42b5dd2a8539c3a1ead4f7bcbcc0028f1ec84090ace6f853793151fc0a7d0   35331bf1ae55   22 hours ago    188MBkoderover.tencentcloudcr.com/koderover-public/zadig-portal   1.13.0-amd64            sha256:15d8207a3ab3573ea8e5a3e32be9fa9340dfb4a5fe0842d3ef848a92655c6f58   1cb89026c2c5   47 hours ago    133MBkoderover.tencentcloudcr.com/koderover-public/zadig-portal   1.14.0-amd64            sha256:1bdb47274a6cb6da12b5fb2d3a073e820a8d5a8be9dac48f8f624adb85ddcefd   63e46ebf3e11   3 weeks ago     133MBtailscale/docker-extension                                   0.0.13                  sha256:5f957b07602dd9b8923664f9b6acf86081c5bfd60b86bf46ab56e3f323ca4de9   1ae72d777218   2 months ago    129MBalgolia/docsearch-scraper                                    latest                  sha256:7bc1cd5aa4783bf24be9ddd6ef22a629b6b43e217b3fa220b6a0acbdeb83b8f8   04e04eaa5c7d   15 months ago   1.74GB

本文將舉例說明使用 Tag 來辨識鏡像的一般痛點,以及如何借助 Zadig 的能力來解決構建產物溯源的問題。

痛點

私有化交付過程中,某開發人工誤操作,不慎導致線上鏡像倉庫中某個服務的版本被本地推送的相同 Tag 的鏡像所覆蓋,其中包含未經驗收測試的功能,用戶端升級服務或者重新拉取該服務鏡像后, 導致服務出現故障,比如:功能不 work、服務無法啟動...等,只通過鏡像無法追蹤具體是何種誤操作導致的,也不能定位到具體的代碼變更。

版本迭代過程中,難免會同時維護多個版本,用戶側使用的版本五花八門,如何進行版本和代碼信息的定位匹配,快速排查客戶端反饋的問題也是一個頭疼的工程難題。

下面將從構建產物為鏡像以及非鏡像兩種場景,介紹涵蓋前后端的實踐方案:可以從構建產物中直接獲取詳細的構建鏈路流程信息,提高后續問題定位排查的效率。

場景一:鏡像構建產物

核心原理:修改 Dockerfile 添加 LABEL,利用 Zadig 內置構建變量的能力,構建鏡像時將其動態注入。

背景知識

關于 Docker 鏡像構建、LABEL 能力以及構建參數相關可閱讀以下資料:

Docker object labels[1]

Docker label / OCI image annotation metadata types[2]

Dockerfile LABEL[3]

Docker build ARG[4]

第一步:編寫 Dockerfile

修改 Dockfile 動態注入 Zadig 提供的構建變量,這里以開源的zadig-portal[5]為例,主要注入如下信息:

構建時間構建任務的 URL代碼信息(代碼庫/分支/PR/Tag/Commit ID)

如果需要注入其他參數,可以根據實際的項目需求進行自定義。核心代碼如下:

ARG repoName="zadig-portal"ARG branch=""ARG pr=""ARG tag=""ARG commit=""ARG buildTime=""ARG buildURL=""LABEL maintainer="Zadig Maintainers" \    description="Zadig is a cloud native, distributed, developer-oriented continuous delivery product." \    repoName=${repoName} \    branch=${branch} \    pr=${pr} \    tag=${tag} \    commit=${commit} \    buildTime=${buildTime} \    buildURL=${buildURL}
第二步:完成構建配置

將 Zadig 提供的內置構建變量,透傳到 docker build --build-arg,主要使用以下變量:

$BUILD_URL構建任務的 URL$_PR構建時使用的代碼 Pull Request 信息$_BRANCH構建時使用的代碼分支信息$_TAG構建時使用代碼 Tag 信息$_COMMIT_ID構建時使用代碼 Commit 信息

由于 zadig-portal 使用的鏡像構建,這里配置鏡像構建的構建參數,將變量透傳到Docker build ARG。如果不使用鏡像構建也可以根據實際構建需求,在腳本中手動拼接 docker build 參數。

構建參數中內容如下:

--build-arg branch=$zadig_portal_BRANCH --build-arg pr=$zadig_portal_PR --build-arg tag=$zadig_portal_TAG --build-arg commit=$zadig_portal_COMMIT_ID --build-arg buildTime=$(date +%s) --build-arg buildURL=$BUILD_URL
第三步:效果驗證

至此,所有通過 Zadig 運行工作流產生的交付鏡像,都會被打上自定義的 Label。

可以通過 docker pull 之后 docker inspect 查看注入的 Label。

嘗試進行 docker tag 后,重新查看 Label,Label 并不會因為 Retag 而發生變化。

場景二:其他構建產物

核心原理:利用 Zadig 的構建參數能力,動態傳入來源信息。

對于暫時不便于遷移容器部署的場景,比如基礎設施本身是可網絡互通的設備:IoT 物聯網場景下自動駕駛車輛主機端、工廠可連接設備...等,交付物可能是二進制或者是前端靜態文件。上述通過鏡像做追蹤溯源的實踐方法也就不再適用。下面分別介紹前端靜態文件以及后端二進制程序的溯源方法。

前端靜態文件

現代化前端應用離不開模塊打包工具,這里以 Webpack 為例,介紹一種通過環境變量透傳的溯源方法。其他的工具 Vite、Parcel ...... 也可以參照該思路進行實踐,具體配置可以參照相關打包工具的文檔。

背景知識

Webpack Define Plugin[6]Webpack Environment Plugin[7]Process Env[8]第一步:代碼實現

1. 構建模板代碼實現

該步驟主要依賴 Webpack Define Plugin ,創建一個需要的構建環境變量模板,方便后續構建時動態替換參數。

const env = require("./config/prod.env");.......//其他配置    plugins: [      new webpack.DefinePlugin({        "BUILDINFO": env      }),    ]

prod.env 文件內容如下:

"use strict"module.exports = {  VERSION: ""${VERSION}"",  BUILD_TIME: ""${BUILD_TIME}"",  TAG: ""${TAG}"",  COMMIT_ID: ""${COMMIT_ID}"",  BRANCH: ""${BRANCH}"",  PR: ""${PR}"",}

這里主要暴露以下參數,可以根據實際需求進行自定義。

VERSIONBUILD_TIMETAGCOMMIT_IDBRANCHPR

2.在業務代碼中讀取并展示構建變量

這里以 Vue 項目為例,實現在終端中打印構建信息,其他項目可自行調整。核心代碼如下:

computed: {    processEnv () {      return process.env    },  },  mounted () {    if (this.processEnv && this.processEnv.NODE_ENV === "production" && BUILDINFO) {      console.log("%cHello ZADIG!", "color: #e20382;font-size: 13px;")      const buildInfo = []      if (BUILDINFO.VERSION) {        buildInfo.push(`${BUILDINFO.VERSION}`)      }      if (BUILDINFO.TAG) {        buildInfo.push(`Tag-${BUILDINFO.TAG}`)      }      if (BUILDINFO.BRANCH) {        buildInfo.push(`Branch-${BUILDINFO.BRANCH}`)      }      if (BUILDINFO.PR) {        buildInfo.push(`PR-${BUILDINFO.PR}`)      }      if (BUILDINFO.COMMIT_ID) {        buildInfo.push(`${BUILDINFO.COMMIT_ID.substring(0, 7)}`)      }      console.log(        `%cBuild:${buildInfo.join(" ")}`,        "color: #e20382;font-size: 13px;"      )      if (BUILDINFO.BUILD_TIME) {        console.log(          `%cTime:${moment            .unix(BUILDINFO.BUILD_TIME)            .format("YYYYMMDDHHmm")}`,          "color: #e20382;font-size: 13px;"        )      }    }  }
第二步:完成構建配置

將 Zadig 提供的內置構建變量,通過腳本實現動態替換 prod.env 的內容,主要使用以下變量:

$_PR構建時使用的代碼 Pull Request 信息$_BRANCH構建時使用的代碼分支信息$_TAG構建時使用代碼 Tag 信息$_COMMIT_ID構建時使用代碼 Commit 信息第三步:效果驗證

運行工作流,可以看到構建變量已經成功的透傳,并且替換了預設的變量模板。

部署后查看控制臺,可以看到 Zadig 的構建信息已經可以在 console 中顯示。

后端二進制產物

由于后端二進制交付物背后的項目類型比較多,這里主要介紹 Golang 項目一種實現思路,該部分涉及到的源碼可以點擊鏈接[9]查看,其他項目類型可以根據實際情況進行參考和配置。

背景知識go build -ldflags -X[10]Kubectl version[11]K8s version[12]Golang 中管理程序的版本信息[13]第一步:代碼實現

1. 定義版本信息

這里在項目中維護一份 version.go 文件,根據實際需求,定義需要暴露的參數。

package utilsimport (    "fmt"    "runtime")var (    version      string    gitBranch    string    gitTag       string    gitCommit    string    gitPR        string    gitTreeState string    buildDate    string    buildURL      string)// Info contains versioning information.type Info struct {    Version      string `json:"version"`    GitBranch    string `json:"gitBranch"`    GitTag       string `json:"gitTag"`    GitCommit    string `json:"gitCommit"`    GitPR        string `json:"gitPR"`    GitTreeState string `json:"gitTreeState"`    BuildDate    string `json:"buildDate"`    BuildURL     string `json:"buildURL"`    GoVersion    string `json:"goVersion"`    Compiler     string `json:"compiler"`    Platform     string `json:"platform"`}// String returns info as a human-friendly version string.func (info Info) String() string {    return info.Platform}func GetVersion() Info {    return Info{        Version:      version,        GitBranch:    gitBranch,        GitTag:       gitTag,        GitCommit:    gitCommit,        GitPR:        gitPR,        GitTreeState: gitTreeState,        BuildDate:    buildDate,        BuildURL:     buildURL,        GoVersion:    runtime.Version(),        Compiler:     runtime.Compiler,        Platform:     fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),    }}

2. main 函數調用

在入口處設置傳入version參數調用GetVersion函數:

package mainimport (    "fmt"    "os"    "version/utils")func main() {    args := os.Args    if len(args) >= 2 && args[1] == "version" {        v := utils.GetVersion()        fmt.Printf("Version: %s\nBranch: %s\nCommit: %s\nPR: %s\nBuild Time: %s\nGo Version: %s\nOS/Arch: %s\nBuild URL: %s\n", v.Version, v.GitBranch, v.GitCommit,v.GitPR, v.BuildDate, v.GoVersion, v.Platform,v.BuildURL)    } else {        fmt.Printf("Version(hard code): %s\n", "0.1")    }}

3. 構建工程文件編寫

這里新建一個 Makefile 進行構建,使用ldflags -X透傳 Zadig 構建內置變量,核心邏輯如下:

# build with verison infosversinotallow="version/utils"gitTag=$(version_TAG)gitBranch=$(version_BRANCH)gitPR=$(version_PR)buildDate=$(shell TZ=Asia/Shanghai date +%FT%T%z)gitCommit=$(version_COMMIT_ID)gitTreeState=$(shell if git status|grep -q "clean";then echo clean; else echo dirty; fi)buildURL=$(BUILD_URL)ldflags="-s -w -X ${versionDir}.gitTag=${gitTag} -X ${versionDir}.buildDate=${buildDate} -X ${versionDir}.gitCommit=${gitCommit} -X ${versionDir}.gitPR=${gitPR} -X ${versionDir}.gitTreeState=${gitTreeState} -X ${versionDir}.versinotallow=${VERSION} -X ${versionDir}.gitBranch=${gitBranch} -X ${versionDir}.buildURL=${buildURL}"PACKAGES=`go list ./... | grep -v /vendor/`VETPACKAGES=`go list ./... | grep -v /vendor/ | grep -v /examples/`GOFILES=`find . -name "*.go" -type f -not -path "./vendor/*"`default:    @echo "build the ${BINARY}"    @GOOS=linux GOARCH=amd64 go build -ldflags ${ldflags} -o  build/${BINARY}.linux  -tags=jsoniter    @go build -ldflags ${ldflags} -o  build/${BINARY}.mac  -tags=jsoniter    @echo "build done."
第二步:完成構建配置

在 Zadig 中配置構建,在構建腳本中構建并打印輸出,詳細信息如下:

依賴的軟件包go 1.16.13自定義構建變量配置 VERSION 變量構建腳本內容如下:
set -ecd $WORKSPACE/zadig/examples/version-demoenvmake defaultcd build./version.linux version
第三步:效果驗證

運行工作流,可以看到構建變量已經成功的透傳,并且該二進制程序通過version參數可以打印詳細的構建來源信息。

結語

以上,就是一些常見交付物類型在 Zadig 上的溯源思路總結,相比于通過原始的 Image ID 、Digest ....來反推進行追蹤,該流程可以讓用戶以及開發人員通過更簡單便捷的方式上報或者追蹤交付物問題,提高問題排查定位的效率。

參考鏈接

[1]https://docs.docker.com/config/labels-custom-metadata/

[2]https://github.com/opencontainers/image-spec/blob/main/annotations.md

[3]https://docs.docker.com/engine/reference/builder/#label

[4]https://docs.docker.com/engine/reference/builder/#arg

[5] https://github.com/koderover/zadig-portal/blob/main/Dockerfile

[6] https://webpack.js.org/plugins/define-plugin/

[7] https://webpack.js.org/plugins/environment-plugin/#root

[8]https://nodejs.org/api/process.html#process_process_env

[9]https://github.com/koderover/zadig/tree/main/examples/version-demo

[10] https://pkg.go.dev/cmd/link

[11]https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/version/version.go

[12] https://github.com/kubernetes/component-base/blob/master/version/version.go

[13] https://zhuanlan.zhihu.com/p/150991555

關鍵詞: