背景
云原生服务的开发迭代,Docker 镜像作为最终的交付产物,其生命周期中存在多个环节的流转:从镜像构建开始,到开发、测试环境的更新验证,再到交付制品生成,交付到生产环境。在这一整套的流转过程中,镜像作为流转物,只要中间的一个流程出现失误,很容易导致交付到生产环境的镜像出现问题。
虽然 Tag 可以让镜像有一定的辨识度,但其可以修改的特性,并不能作为辨识身份的标准。 如果构建流程中有需要人工干预修改 Tag 的操作,在交付过程中也会引入更多的风险。镜像 ID 和 Digests 配合可以作为唯一身份标识确保交付物的一致性,但可读性比较差,出现交付物不一致的问题,溯源的成本也比较高。
1 docker images --digests
2 REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
3 koderover.tencentcloudcr.com/koderover-public/aslan 1.15.0-20220922-amd64 sha256:8af42b5dd2a8539c3a1ead4f7bcbcc0028f1ec84090ace6f853793151fc0a7d0 35331bf1ae55 22 hours ago 188MB
4 koderover.tencentcloudcr.com/koderover-public/zadig-portal 1.13.0-amd64 sha256:15d8207a3ab3573ea8e5a3e32be9fa9340dfb4a5fe0842d3ef848a92655c6f58 1cb89026c2c5 47 hours ago 133MB
5 koderover.tencentcloudcr.com/koderover-public/zadig-portal 1.14.0-amd64 sha256:1bdb47274a6cb6da12b5fb2d3a073e820a8d5a8be9dac48f8f624adb85ddcefd 63e46ebf3e11 3 weeks ago 133MB
6 tailscale/docker-extension 0.0.13 sha256:5f957b07602dd9b8923664f9b6acf86081c5bfd60b86bf46ab56e3f323ca4de9 1ae72d777218 2 months ago 129MB
7 algolia/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)
如果需要注入其他参数,可以根据实际的项目需求进行自定义。核心代码如下:
1 ARG repoName="zadig-portal"
2 ARG branch=""
3 ARG pr=""
4 ARG tag=""
5 ARG commit=""
6 ARG buildTime=""
7 ARG buildURL=""
89 LABEL maintainer="Zadig Maintainers" \
10 description="Zadig is a cloud native, distributed, developer-oriented continuous delivery product." \
11 repoName=${repoName} \
12 branch=${branch} \
13 pr=${pr} \
14 tag=${tag} \
15 commit=${commit} \
16 buildTime=${buildTime} \
17 buildURL=${buildURL}
第二步:完成构建配置
将 Zadig 提供的内置构建变量,透传到 docker build --build-arg,主要使用以下变量:
-
$BUILD_URL 构建任务的 URL
-
$<REPO>_PR 构建时使用的代码 Pull Request 信息
-
$<REPO>_BRANCH 构建时使用的代码分支信息
-
$<REPO>_TAG 构建时使用代码 Tag 信息
-
$<REPO>_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 ,创建一个需要的构建环境变量模板,方便后续构建时动态替换参数。
1 const env = require('./config/prod.env');
2 .......//其他配置
3 plugins: [
4 new webpack.DefinePlugin({
5 'BUILDINFO': env
6 }),
7 ]
prod.env 文件内容如下:
1 'use strict'
2 module.exports = {
3 VERSION: '"${VERSION}"',
4 BUILD_TIME: '"${BUILD_TIME}"',
5 TAG: '"${TAG}"',
6 COMMIT_ID: '"${COMMIT_ID}"',
7 BRANCH: '"${BRANCH}"',
8 PR: '"${PR}"',
9 }
10
这里主要暴露以下参数,可以根据实际需求进行自定义。
-
VERSION
-
BUILD_TIME
-
TAG
-
COMMIT_ID
-
BRANCH
-
PR
2.在业务代码中读取并展示构建变量
这里以 Vue 项目为例,实现在终端中打印构建信息,其他项目可自行调整。核心代码如下:
1 computed: {
2 processEnv () {
3 return process.env
4 },
5 },
6 mounted () {
7 if (this.processEnv && this.processEnv.NODE_ENV === 'production' && BUILDINFO) {
8 console.log('%cHello ZADIG!', 'color: #e20382;font-size: 13px;')
9 const buildInfo = []
10 if (BUILDINFO.VERSION) {
11 buildInfo.push(`${BUILDINFO.VERSION}`)
12 }
13 if (BUILDINFO.TAG) {
14 buildInfo.push(`Tag-${BUILDINFO.TAG}`)
15 }
16 if (BUILDINFO.BRANCH) {
17 buildInfo.push(`Branch-${BUILDINFO.BRANCH}`)
18 }
19 if (BUILDINFO.PR) {
20 buildInfo.push(`PR-${BUILDINFO.PR}`)
21 }
22 if (BUILDINFO.COMMIT_ID) {
23 buildInfo.push(`${BUILDINFO.COMMIT_ID.substring(0, 7)}`)
24 }
25 console.log(
26 `%cBuild:${buildInfo.join(' ')}`,
27 'color: #e20382;font-size: 13px;'
28 )
29 if (BUILDINFO.BUILD_TIME) { console.log(
30 `%cTime:${moment
31 .unix(BUILDINFO.BUILD_TIME)
32 .format('YYYYMMDDHHmm')}`,
33 'color: #e20382;font-size: 13px;'
34 )
35 }
36 }
38 }
第二步:完成构建配置
将 Zadig 提供的内置构建变量,通过脚本实现动态替换 prod.env 的内容,主要使用以下变量:
-
$<REPO>_PR 构建时使用的代码 Pull Request 信息
-
$<REPO>_BRANCH 构建时使用的代码分支信息
-
$<REPO>_TAG 构建时使用代码 Tag 信息
-
$<REPO>_COMMIT_ID 构建时使用代码 Commit 信息
第三步:效果验证
运行工作流,可以看到构建变量已经成功的透传,并且替换了预设的变量模板。
部署后查看控制台,可以看到 Zadig 的构建信息已经可以在 console 中显示。
后端二进制产物
由于后端二进制交付物背后的项目类型比较多,这里主要介绍 Golang 项目一种实现思路,该部分涉及到的源码可以点击 链接[9] 查看,其他项目类型可以根据实际情况进行参考和配置。
背景知识
-
go build -ldflags -X[10]
-
Kubectl version[11]
-
K8s version[12]
-
Golang 中管理程序的版本信息[13]
第一步:代码实现
1. 定义版本信息
这里在项目中维护一份 version.go 文件,根据实际需求,定义需要暴露的参数。
1 package utils
23 import (
4 "fmt"
5 "runtime"
6 )
78 var (
9 version string
10 gitBranch string
11 gitTag string
12 gitCommit string
13 gitPR string
14 gitTreeState string
15 buildDate string
16 buildURL string
17 )
1819 // Info contains versioning information.
20 type Info struct {
21 Version string `json:"version"`
22 GitBranch string `json:"gitBranch"`
23 GitTag string `json:"gitTag"` GitCommit string `json:"gitCommit"`
24 GitPR string `json:"gitPR"`
25 GitTreeState string `json:"gitTreeState"`
26 BuildDate string `json:"buildDate"`
27 BuildURL string `json:"buildURL"`
28 GoVersion string `json:"goVersion"`
29 Compiler string `json:"compiler"`
30 Platform string `json:"platform"`
31 }
3233 // String returns info as a human-friendly version string.
34 func (info Info) String() string {
35 return info.Platform
36 }
3738 func GetVersion() Info {
39 return Info{
40 Version: version,
41 GitBranch: gitBranch,
42 GitTag: gitTag,
43 GitCommit: gitCommit,
44 GitPR: gitPR,
45 GitTreeState: gitTreeState,
46 BuildDate: buildDate,
47 BuildURL: buildURL, 48 GoVersion: runtime.Version(),
49 Compiler: runtime.Compiler,
50 Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
51 }
52 }
53
2. main 函数调用
在入口处设置传入 version 参数调用 GetVersion 函数
1 package main
23 import (
4 "fmt"
5 "os"
67 "version/utils"
8 )
910 func main() {
1112 args := os.Args
13 if len(args) >= 2 && args[1] == "version" {
14 v := utils.GetVersion()
15 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)
16 } else { 17 fmt.Printf("Version(hard code): %s\n", "0.1")
18 }
19 }
20
3. 构建工程文件编写
这里新建一个 Makefile 进行构建,使用 ldflags -X 透传 Zadig 构建内置变量,核心逻辑如下:
1 # build with verison infos
2 versionDir="version/utils"
3 gitTag=$(version_TAG)
4 gitBranch=$(version_BRANCH)
5 gitPR=$(version_PR)
6 buildDate=$(shell TZ=Asia/Shanghai date +%FT%T%z)
7 gitCommit=$(version_COMMIT_ID)
8 gitTreeState=$(shell if git status|grep -q 'clean';then echo clean; else echo dirty; fi)
9 buildURL=$(BUILD_URL)
1011 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}.version=${VERSION} -X ${versionDir}.gitBranch=${gitBranch} -X ${versionDir}.buildURL=${buildURL}"
1213 PACKAGES=`go list ./... | grep -v /vendor/`
14 VETPACKAGES=`go list ./... | grep -v /vendor/ | grep -v /examples/`
15 GOFILES=`find . -name "*.go" -type f -not -path "./vendor/*"`
1617 default:
18 @echo "build the ${BINARY}"
19 @GOOS=linux GOARCH=amd64 go build -ldflags ${ldflags} -o build/${BINARY}.linux -tags=jsoniter
20 @go build -ldflags ${ldflags} -o build/${BINARY}.mac -tags=jsoniter
21 @echo "build done."
第二步:完成构建配置
在 Zadig 中配置构建,在构建脚本中构建并打印输出,详细信息如下:
-
依赖的软件包 go 1.16.13
-
自定义构建变量 配置 VERSION 变量
-
构建脚本 内容如下:
1 set -e
2 cd $WORKSPACE/zadig/examples/version-demo
3 env
4 make default
5 cd build
6 ./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
Zadig,让工程师更专注创造!