📜 目录
- 👋 背景
- 🚧 要求
- 🌏 基础镜像选择
- 💢 稳定性的问题
- 💢 兼容性问题
- 🥰 参考
👋 背景
在基于 Tekton 或者 Jenkins 作为构建引擎的 DevOps 产品中,总是需要内置一些工具类的镜像,这里面会包含我们需要的一些必要的二进制,比如 Git
Ansible
Helm
yq
jq
或许还有一些内部的 cli 工具,通常我们会将其命名为 tools 镜像,这个镜像会被应用在不同的 Task 中实现不同的功能。
随着时间的推移,无论什么地方的 Task 需要一个什么二进制,都会塞到这个镜像中来,久而久之这个镜像就变成了包含巨多工具超大难以维护的镜像,最主要的的问题就是回归工作量的问题。
比如修改了镜像中的某一个制品版本,重新构建了 tools 镜像,为了保证功能正常,所有使用了该 tools 镜像的 Task 都需要回归测试,随着时间的推移,回归工作量就会越来越大。另外业界和客户对镜像安全也越来越重视,因此镜像经常性的需要升级工具版本来解决漏洞问题,也导致这个测试工作量问题越来越严重。
为了解决这个问题,可以考虑为不同 Task 配置专门的镜像,将这些 cli 工具拆分到对应的 Task 的小镜像中,这样镜像做了改动后,回归测试的范围就会足够小,漏洞也可以随时快速的修复。
🚧 要求
在为 Task 做专门的镜像时,会有如下几个需求
- 镜像体积尽可能的小
- 镜像包含的漏洞尽可能的少
- 支持多架构 (amd64 与 arm64)
- 稳定,确保每次构建出来的制品都是一样的
- 兼容性,兼容之前的 Task 中的功能
🌏 选择基础镜像
目前可以选择的基础镜像大概有如下几个,基本信息如下
BaseImage | Size | Package Management Tool | Architectures | Vulnerability |
---|---|---|---|---|
Ubuntu | 71M | apt | amd64/arm64 | No |
Apline | 7.8M | apk | amd64/arm64 | No |
Distroless | 2.6M | No | amd64/arm64 | No |
从体积上来看,Ubuntu 已经被抛弃了,从包管理工具来看,Distroless 也直接抛弃了,所以直接选择了适中的 Apline 作为基础镜像。
根据这次问题处理经验来看,Apline 的包管理工具 apk 要比 Ubuntu 的 apt 方便很多,很多工具比如 Ansible 使用 apk add
就可以直接安装好,但是 apt 就不行,还需要通过脚本判断架构然后再下载对应的二进制包到镜像中,麻烦也不容易维护。
因为有专门的团队维护基础镜像,所以也不需要担心漏洞和多架构的问题。
💢 稳定性的问题
为了保证镜像构建的稳定性,在通过 apk add
安装二进制工具时,需要指定安装工具的版本,防止每次构建都安装最新版本导致的兼容问题。一般来说,这种情况是很少见的,但是譬如 yq
和 helm
这种的工具,就会存在问题。
因为直接使用 apk add
的话都会安装最新版本,但是 yq
和 helm
不同版本却有很大的不兼容的改动,比如 yq3
与 yq4
语法完全不兼容,helm2 和 helm3 命令也完全不同,不指定版本号安装可能就会带来兼容性的问题,导致升级前可以成功运行的 Task 在升级后无法正常运行。
apk add
安装工具时,可以通过该语法指定版本号 apk add bash==4.3.33-r0
。
通过这种方式指定后,就相当于安装了一个定时炸弹,因为 Alpine 社区各版本仓库只会保留最新版本的工具,其他的版本都会被删除,可以参考下四年前的相关议题 Alpine repo drops packages. This prevents package version pinning, and makes apk non-deterministic。
因此如果你指定版本安装的工具有了新版本后,之前指定的版本就会被删除,安装命令就会失败导致镜像构建失败,到时候还要苦哈哈的修改版本号重新构建。
为了解决这个问题,一般会有这么几种办法
使用 Stable 版本的库
Stabel 或者已经归档的工具仓库中的工具版本不会再有更新,所以也就避免了上面提到的这个问题。但是这种旧版本的工具可能会存在漏洞,所以需要格外注意这一点。
使用这种方式的话,需要指定 Stable 的仓库地址,确保 apk
可以找到旧版本工具,另外还需要获取 stable 版本工具的确切版本号,一般需要如下操作
- 写入 Stable 仓库地址
在 Dockerfile 执行apk add
命令前,先将旧版本仓库地址写入到 /etc/apk/repositories 文件中,这样apk
就可以找到旧版本的工具RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.2/main' >> /etc/apk/repositories
- 确定工具版本号
通过 package filter,选择相关的版本与包,就可以获取到相关包的版本信息了,在apk add
时指定旧的版本号就可以实现下载
指定版本号范围
apk add
安装工具的时候,支持指定安装工具的版本范围,比如 Holding a specific package back 提到的 apk add 'asterisk>1.6.1'
就会下载高于 1.6.1
版本的 asterisk
。
目前支持的操作符可以参考这个 commit ,官方文档和 apk help
命令没有提供太多的说明
在这个能力的加持下,我们可以指定一下工具的版本号范围,比如指定 RUN apk add --no-cache "ansible<=9.6"
,可以保证不会出现大版本改动导致兼容性的问题。
过一段时间,工具最新版本肯定会超过指定的版本范围,导致报错,但是频率也会比指定版本号要小一些。另外报错之后,我们也可以借这个机会看下是否工具有了不兼容改动,也可以适当的提升下版本号,扩展工具的能力。
本地安装
这个方式比较麻烦,但是一劳永逸,不会存在可能会出现的构建失败的问题
apk add
支持直接安装本地包,我们可以将我们需要的包下载到 Nexus 仓库中,然后在构建镜像的时候,将包下载下来,再通过 apk add
进行安装
使用方式参考 Add a local Package,首先需要下载你要安装的 apk 包到指定目录,随后通过如下命令安装
apk add --allow-untrusted /path/to/file.apk
如果你安装的 apk 存在其他的 apk 包的依赖,还需要指定所有的依赖包,例如
apk add --allow-untrusted /var/tig-2.2-r0.apk /var/git-2.11.1-20.apk
以上三种方式可以根据具体实际情况进行选择
💢 兼容性的问题
之前的 tools 镜像,使用的基础镜像是 Ubuntu 镜像,里面会内置很多的二进制工具,Alpine 中也会内置一些二进制工具,但是这些工具就是依赖 BuxyBox 提供的能力,这些二进制工具算是 阉割版本的二进制工具,会被阉割掉一些执行参数和相关能力。
BusyBox combines tiny versions of many common UNIX utilities into a single small executable.
It provides replacements for most of the utilities you usually find in GNU fileutils, shellutils, etc.
The utilities in BusyBox generally have fewer options than their full-featured GNU cousins
比如以下就是 Ubuntu 和 Apline 中 tar
命令支持的参数的差异
Apline 中的 tar
二进制支持的参数
BusyBox v1.36.1 (2024-06-10 07:11:47 UTC) multi-call binary.
Usage: tar c|x|t [-ZzJjahmvokO] [-f TARFILE] [-C DIR] [-T FILE] [-X FILE] [LONGOPT]... [FILE]...
Create, extract, or list files from a tar file
c Create
x Extract
t List
-f FILE Name of TARFILE ('-' for stdin/out)
-C DIR Change to DIR before operation
-v Verbose
-O Extract to stdout
-m Don't restore mtime
-o Don't restore user:group
-k Don't replace existing files
-Z (De)compress using compress
-z (De)compress using gzip
-J (De)compress using xz
-j (De)compress using bzip2
--lzma (De)compress using lzma
-a (De)compress based on extension
-h Follow symlinks
-T FILE File with names to include
-X FILE File with glob patterns to exclude
--exclude PATTERN Glob pattern to exclude
--overwrite Replace existing files
--strip-components NUM NUM of leading components to strip
--no-recursion Don't descend in directories
--numeric-owner Use numeric user:group
--no-same-permissions Don't restore access permissions
Ubuntu 中 tar
支持的参数(为了方便描述,省略了很多篇幅)
GNU 'tar' saves many files together into a single tape or disk archive, and can
restore individual files from the archive.
Examples:
tar -cf archive.tar foo bar # Create archive.tar from files foo and bar.
tar -tvf archive.tar # List all files in archive.tar verbosely.
tar -xf archive.tar # Extract all files from archive.tar.
Main operation mode:
-A, --catenate, --concatenate append tar files to an archive
-c, --create create a new archive
--delete delete from the archive (not on mag tapes!)
-d, --diff, --compare find differences between archive and file system
-r, --append append files to the end of an archive
--test-label test the archive volume label and exit
-t, --list list the contents of an archive
-u, --update only append files newer than copy in archive
-x, --extract, --get extract files from an archive
Operation modifiers:
--check-device check device numbers when creating incremental
archives (default)
-g, --listed-incremental=FILE handle new GNU-format incremental backup
-G, --incremental handle old GNU-format incremental backup
--hole-detection=TYPE technique to detect holes
--ignore-failed-read do not exit with nonzero on unreadable files
--level=NUMBER dump level for created listed-incremental archive
--no-check-device do not check device numbers when creating
incremental archives
--no-seek archive is not seekable
-n, --seek archive is seekable
--occurrence[=NUMBER] process only the NUMBERth occurrence of each file
in the archive; this option is valid only in
conjunction with one of the subcommands --delete,
--diff, --extract or --list and when a list of
files is given either on the command line or via
the -T option; NUMBER defaults to 1
--sparse-version=MAJOR[.MINOR]
set version of the sparse format to use (implies
--sparse)
-S, --sparse handle sparse files efficiently
Local file name selection:
--add-file=FILE add given FILE to the archive (useful if its name
starts with a dash)
-C, --directory=DIR change to directory DIR
--exclude=PATTERN exclude files, given as a PATTERN
--exclude-backups exclude backup and lock files
--exclude-caches exclude contents of directories containing
CACHEDIR.TAG, except for the tag file itself
--exclude-caches-all exclude directories containing CACHEDIR.TAG
--exclude-caches-under exclude everything under directories containing
CACHEDIR.TAG
--exclude-ignore=FILE read exclude patterns for each directory from
FILE, if it exists
--exclude-ignore-recursive=FILE
read exclude patterns for each directory and its
subdirectories from FILE, if it exists
--exclude-tag=FILE exclude contents of directories containing FILE,
except for FILE itself
--exclude-tag-all=FILE exclude directories containing FILE
--exclude-tag-under=FILE exclude everything under directories
containing FILE
--exclude-vcs exclude version control system directories
--exclude-vcs-ignores read exclude patterns from the VCS ignore files
--no-null disable the effect of the previous --null option
--no-recursion avoid descending automatically in directories
--no-unquote do not unquote input file or member names
--no-verbatim-files-from -T treats file names starting with dash as
options (default)
--null -T reads null-terminated names; implies
--verbatim-files-from
--recursion recurse into directories (default)
-T, --files-from=FILE get names to extract or create from FILE
--unquote unquote input file or member names (default)
--verbatim-files-from -T reads file names verbatim (no escape or option
handling)
-X, --exclude-from=FILE exclude patterns listed in FILE
File name matching options (affect both exclude and include patterns):
--anchored patterns match file name start
--ignore-case ignore case
--no-anchored patterns match after any '/' (default for
exclusion)
--no-ignore-case case sensitive matching (default)
--no-wildcards verbatim string matching
--no-wildcards-match-slash wildcards do not match '/'
--wildcards use wildcards (default for exclusion)
--wildcards-match-slash wildcards match '/' (default for exclusion)
...
这种情况下,如果之前使用了被阉割的参数,在替换成 Apline 基础镜像之后,就会带来不兼容的问题,Task 就会执行失败。
为了解决这个问题,还需要自己通过 apk add
来重新安装下 tar 以获取完整的归档能力。
🥰 参考
Alpine Packager Filter
how-to-install-a-specific-package-version-in-alpine
Alpine、Busybox、Distroless 三分天下,谁才是容器基础镜像的真正王者?