使用buildah在容器中构建镜像
背景
多年来, Kaniko一直是构建镜像的首选方案,它是谷歌专为在非特权容器内构建镜像而开发的工具,无需 Docker 守护进程。然而在 2025 年 6 月,谷歌将该代码库归档,现在它已变为只读。GitLab 也已移除 Kaniko 的相关文档,并推荐使用 Buildah 或 Podman 等替代工具。
我需要一个镜像构建流水线,能够在k3s环境中可靠运行,一开始用的是 Kaniko,但很快就意识到它存在问题,于是转而使用 Buildah。
容器内构建镜像工具对比
在 Kaniko 的存档之后,有三个替代方案成为主要竞争者:
BuildKit:Docker 的现代化构建后端。它采用基于 DAG 的快速并行执行机制,并拥有出色的缓存功能。但是,将 BuildKit 作为独立服务运行在 Kubernetes 中,会增加一个需要管理的长期运行组件。对于一个重视组件数量极少的平台来说,这是我不希望看到的额外开销。
Buildah :Red Hat 的 OCI 镜像构建器,现已成为Podman 容器工具套件的一部分,并于 2025 年 1 月被 CNCF 沙箱接受。Buildah 构建标准的 Dockerfile,推送到任何镜像仓库,并且作为一次性进程运行,无需守护进程。它与 Kubernetes Job 模型完美契合:启动 Pod、构建镜像、推送镜像、退出。
werf / Kimia:封装了 Buildah 或 BuildKit 的高级工具。功能更多,观点更多,依赖项也更多。对于一个每个外部依赖项都可能成为负担的平台来说,这并非我所期望的。
如何使用buildah?
通过 Dockerfile 声明式构建和通过命令逐步构建,是 Buildah 的两种核心的交互模式。为了便于对照,你还可以参考它与 Docker 的常用命令映射。
🛠️ 方式一:声明式构建
这是最常用、也最推荐的方式,通过执行 Containerfile 或 Dockerfile 中的指令来构建镜像。这类似乎在 Docker 中使用 docker build。
- 核心命令:
buildah bud(这是buildah build-using-dockerfile的缩写)。 - 基本用法:
buildah bud -t <镜像名>:<标签> <上下文路径>1# 使用当前目录下的Dockerfile构建镜像 2buildah bud -t my-app:v1 . 3# 使用指定路径下的Dockerfile构建 4buildah bud -t my-app:v1 -f /path/to/Dockerfile /path/to/context - 常用构建选项
--no-cache: 构建时不使用缓存。--layers: 用于多阶段构建,可以有效地利用缓存。--platform <os/arch>: 为指定的目标平台(如linux/arm64)构建镜像。--build-arg <key>=<value>: 向构建过程传递构建参数。
🎛️ 方式二:命令式构建
这种方式让你可以像在Shell脚本中一样,用一系列独立的命令来构建镜像。它提供了非常精细的控制,适合调试或需要极致精简的场景。
1# 1. 从基础镜像创建一个工作容器
2container=$(buildah from python:3.9-slim)
3
4# 2. 在容器内执行命令
5buildah run $container pip install --no-cache-dir flask
6
7# 3. 将本地文件复制到容器内
8buildah copy $container ./app /app
9
10# 4. 设置镜像的配置信息
11buildah config --workingdir /app $container
12buildah config --port 5000 $container
13buildah config --cmd "python app.py" $container
14
15# 5. 将修改后的容器提交为一个新的镜像
16buildah commit $container my-flask-app:v1
17
18# 6. (可选) 清理临时的工作容器
19buildah rm $container
这个例子演示了如何一步步搭建一个Flask应用的镜像,如同你在代码中构建一样。
🗂️ 镜像与容器管理命令
除了构建,日常管理镜像和容器也离不开以下命令。
buildah images: 列出本地存储的所有镜像。buildah containers: 列出所有正在运行或停止的工作容器。buildah rm <容器名>: 删除一个或多个工作容器。buildah rmi <镜像名>: 删除一个或多个本地镜像。buildah tag <镜像名> <新标签>: 为本地镜像添加一个新的标签。
☁️ 镜像仓库交互命令
构建完成的镜像,通常需要推送到远程仓库进行分发。
buildah login <仓库地址>: 登录到容器镜像仓库。buildah push <本地镜像名> docker://<远程仓库地址>/<镜像名>:<标签>: 将本地镜像推送到远程仓库。buildah pull docker://<远程仓库地址>/<镜像名>:<标签>: 从远程仓库拉取镜像到本地。
🚀 进阶用法
✨ 完全从零开始构建 (scratch)
Buildah 允许你从一个绝对空白的基础镜像 scratch 开始,手动构建一个仅包含你的应用二进制文件的“极简镜像”,这在安全和镜像体积上都有极大的优势。
1# 1. 创建空白容器
2container=$(buildah from scratch)
3
4# 2. 将本地编译好的二进制文件复制进去
5buildah copy $container /my-host-binary /my-binary
6
7# 3. 设置启动命令
8buildah config --entrypoint '["/my-binary"]' $container
9
10# 4. 提交镜像
11buildah commit $container my-ultra-minimal-app:latest
🛡️ 多阶段构建
多阶段构建可以在一个 Dockerfile 中使用多个 FROM 指令,在最终镜像中只保留运行时的必要组件,从而大幅减小镜像体积。
1# 第一阶段:构建阶段,使用golang镜像
2FROM golang:1.19 AS builder
3WORKDIR /src
4COPY main.go .
5RUN go build -o /myapp
6
7# 第二阶段:运行时阶段,使用scratch空白镜像
8FROM scratch
9COPY --from=builder /myapp /myapp
10ENTRYPOINT ["/myapp"]
🪄 挤压镜像层 (--squash)
使用 --squash 选项可以将构建过程产生的所有层合并为一层,这能有效保护你的构建过程不被窥探,也能进一步压缩镜像体积。
1buildah bud --squash -t my-consolidated-app:v1 .
🔧 挂载与调试
若要在构建过程中深入容器进行调试,可以先挂载其文件系统,再进行交互式操作。
1# 将容器的文件系统挂载到宿主机某个目录
2mountpoint=$(buildah mount <容器名>)
3# 此时可以进入 $mountpoint 目录查看或修改文件
4# ... 进行操作 ...
5# 完成后卸载
6buildah unmount <容器名>
🤝 常用命令速查(Docker vs Buildah)
为了帮你快速上手,这里整理了一份 Buildah 与 Docker 常用命令的对比表:
| 命令用途 | Docker 命令 | Buildah 命令 |
|---|---|---|
| 声明式构建 | docker build -t myapp . |
buildah bud -t myapp . |
| 列出本地镜像 | docker images |
buildah images |
| 从基础镜像启动 | docker run -it python:3.9 bash |
ctr=$(buildah from python:3.9) |
| 在容器内执行命令 | docker exec ctr pip install flask |
buildah run $ctr pip install flask |
| 复制文件到容器 | docker cp ./app ctr:/app |
buildah copy $ctr ./app /app |
| 提交容器为镜像 | docker commit ctr myimage |
buildah commit $ctr myimage |
| 查看工作容器 | docker ps -a |
buildah containers |
| 删除工作容器/镜像 | docker rm/rmi |
buildah rm / rmi |
| 登录镜像仓库 | docker login |
buildah login |
| 推送镜像到仓库 | docker push myimage |
buildah push myimage docker://myregistry.com/myimage |
| 从仓库拉取镜像 | docker pull myimage |
buildah pull docker://myregistry.com/myimage |
📡 CI/CD 自动化示例
以下是在脚本或 CI/CD 流水线中,使用 Buildah 进行构建和推送的完整示例:
1#!/bin/bash
2CONTAINER_NAME="my-build-container"
3IMAGE_TAG="myapp:$(git rev-parse --short HEAD)"
4
5# 构建镜像
6buildah bud -t ${IMAGE_TAG} .
7
8# 登录私有仓库
9echo "$MY_REGISTRY_PASSWORD" | buildah login my-private-registry.com -u myuser --password-stdin
10
11# 为镜像打上仓库标签
12buildah tag ${IMAGE_TAG} my-private-registry.com/project/${IMAGE_TAG}
13
14# 推送镜像到仓库
15buildah push ${IMAGE_TAG} docker://my-private-registry.com/project/${IMAGE_TAG}
16
17# 清理本地镜像
18buildah rmi ${IMAGE_TAG}
💡 实用小贴士
- 查询帮助: 使用
buildah --help获取总体帮助,或使用buildah <command> --help查看特定命令的详细用法。 - 输出格式: 在脚本中处理命令输出时,可以多用
--json参数(如buildah images --json)来获取结构化的数据。
在k8s中如何使用buildah?
在 Kubernetes 中使用 Buildah,核心思路是运行一个使用 buildah 工具的 Pod 来执行构建任务。这种方式安全、轻量,很适合作为 Job 或集成到 CI/CD 流水线中。
🚀 核心价值:为什么在 K8s 中选择 Buildah?
Buildah 之所以胜出,是因为它能清晰地映射到单一模式:每个构建一个 Kubernetes 作业,没有持久基础设施。
- 安全性高:无需 Docker 守护进程(Docker daemon)和
root权限,避免了挂载/var/run/docker.sock带来的核心风险。 - 兼容性好:构建的镜像是标准的 Open Container Initiative(OCI)格式,可以在任何 Kubernetes 集群中运行。
- 灵活轻量:原生支持 Dockerfile/Containerfile,也支持细粒度的脚本化构建,非常适合作为一次性任务(Job)运行。
- 日渐成为主流:随着此前流行的
Kaniko项目被归档,Buildah作为 CNCF 沙箱项目,正成为 K8s 集群内构建容器镜像的热门选择。
📋 准备工作:存储与镜像仓库认证
配置存储驱动 (Storage Driver)
在 K8s 容器中运行 Buildah,存储驱动是关键,推荐优先尝试性能更好的 overlay,若失败则回退到兼容性更强的 vfs:
- overlay驱动:性能高,是首选方案。但它对内核模块和容器运行时有要求,需要确保节点已加载
overlay模块,并配置正确的挂载传播模式。 - vfs驱动:无需特殊配置,兼容性最强,几乎是“兜底方案”,但代价是性能较差,镜像构建和层操作会更慢。
配置方式如上文“核心模式”的 Pod 清单所示,通过 buildah --storage-driver=vfs bud ... 临时指定驱动,或通过挂载的 storage.conf 配置文件持久化配置。
镜像仓库认证
在 Pod 内推送镜像前,需要向目标镜像仓库(如 Docker Hub、私有仓库)进行认证。常见方式有:
- 创建 Secret (首选方式):使用
kubectl create secret创建存储认证信息的 Secret。然后,在 Pod 或 Job 中通过volumeMounts将该 Secret 挂载到容器内buildah查找认证文件的默认路径(通常是/home/build/.docker或/tekton/home/.docker)。 - 使用
buildah login:如果你的 Pod 需要交互式运行,也可以在容器启动后,通过buildah login命令配合用户名密码进行登录。
⚙️ 核心模式:如何在 K8s 中运行 Buildah?
主要有三种运行模式,推荐在大多数场景下优先使用 Rootless 模式。
模式一:Rootless 模式 (推荐)
此模式通过用户命名空间(User Namespace)隔离,让 Buildah 以普通用户(如 UID 1000)身份运行,无需 privileged 权限,是云原生环境下的安全最佳实践。
- 安全考量: 运行在非特权容器中,极大减少了攻击面,运行于命名空间隔离环境。
- 实践清单:
- 在
securityContext中设置runAsUser: 1000,runAsNonRoot: true,并按需添加SETUID,SETGID等capabilities。 - 为存储目录(如
/var/lib/containers)挂载一个emptyDir卷,并考虑设置sizeLimit。 - 在构建命令中加入
--storage-driver=vfs以规避潜在的存储驱动问题。
- 在
一个简单的 Rootless Buildah Pod YAML 示例如下:
1apiVersion: v1
2kind: Pod
3metadata:
4 name: buildah-build
5spec:
6 containers:
7 - name: buildah
8 image: quay.io/buildah/stable:latest
9 command: ['sh', '-c']
10 args:
11 - |
12 buildah --storage-driver=vfs bud -t myapp:latest .
13 buildah push myapp:latest docker://myregistry/myapp:latest
14 securityContext:
15 runAsUser: 1000 # 以非 root 用户运行
16 runAsNonRoot: true
17 capabilities:
18 add: ["SETUID", "SETGID"] # 按需添加
19 volumeMounts:
20 - name: varlibcontainers
21 mountPath: /var/lib/containers # 为 Buildah 的工作目录持久化存储
22 volumes:
23 - name: varlibcontainers
24 emptyDir: {}
想了解更详细的配置,例如
storage.conf的用法,可以参考 CERN 的这篇文章,它提供了完整的示例和配置细节。
模式二:Privileged 模式
此模式将容器设置为 privileged: true,使其拥有宿主机的 root 权限和所有内核能力。
- 安全考量: 高风险、容器拥有广泛权限、仅限完全可信的隔离环境使用,生产环境不建议使用。
- 实践清单:
- 在
securityContext中明确设置privileged: true。 - 通过环境变量
BUILDAH_ISOLATION=chroot提升兼容性。 - 依然需要为存储目录(如
/var/lib/containers)挂载emptyDir卷。
- 在
🔧 CI/CD 集成:将 Buildah 嵌入自动化流水线
Buildah 非常适合以 Kubernetes Job 的形式集成到 CI/CD 流水线中。
方式一:作为 Kubernetes Job
这种方式下,每个构建任务都对应一个独立的 Kubernetes Job,任务完成后 Job 自动结束,实现资源隔离和按需使用。整个流程如下:
- 启动 Job:用户触发的构建请求会创建一个新的 Kubernetes Job。
- 拉取代码:Job Pod 中的 Buildah 容器启动后,首先会从 Git 仓库(如 GitHub)拉取源代码。
- 执行构建:在容器内执行
buildah bud -t myapp:latest .,根据 Dockerfile 构建镜像。 - 推送镜像:构建完成后,执行
buildah push myapp:latest docker://myregistry/myapp:latest将镜像推送到目标仓库。 - Job 完成:推送成功后,Pod 自动停止,Job 完成。
方式二:集成 Jenkins/GitLab CI/GitHub Actions
- 在 Jenkins 中使用:动态创建 Kubernetes Pod 作为 Jenkins Agent,在其中执行 Buildah 命令。推荐使用 Jenkins Kubernetes Plugin 来定义 Pod 模板,并利用 Buildah 的 Rootless 模式确保安全性。
- 在 GitLab CI 中使用:利用 GitLab 的 Kubernetes 执行器 (Kubernetes executor),在
.gitlab-ci.yml中定义使用 Buildah 镜像的 Job,通过securityContext配置为非特权模式。 - 在 GitHub Actions 中使用:官方和社区提供了 “Buildah Build” 这类 Action,可以直接在工作流中使用,简化了 Buildah 的安装和配置。
方式三:使用 Tekton 作为 Kubernetes 原生 CI/CD 框架
Tekton 是构建云原生 CI/CD 流水线的标准框架,它提供了支持 Buildah 的官方 Task。
- 安装 Tekton:在你的 Kubernetes 集群中部署 Tekton Pipelines。
- 使用 Buildah Task:Tekton 社区提供了预定义的
buildah任务,你可以直接或参考它在你的流水线中使用。 - 配置任务参数:在你的 Pipeline 中引用该 Task,并配置
IMAGE、DOCKERFILE、CONTEXT等参数。 - 定义工作空间:为 Task 指定一个
workspace(通常是emptyDir或 PVC)来存放源代码和构建上下文。 - 处理凭证:通过
workspace或secret将镜像仓库的认证信息注入到容器中。
🤔 常见问题与故障排查
- Permission denied:通常是文件权限问题。确保容器内运行的用户有权限访问挂载的卷和写入的目录。使用
securityContext.fsGroup可以设置一个组 ID,让卷内容可以被该组内的进程(如 Buildah)访问。 - error creating overlay mount:Buildah 的
overlay存储驱动与当前容器环境不兼容。最直接的解决方案是在buildah命令中加上--storage-driver=vfs。 - x509: certificate has expired or is not yet valid:通常是节点系统时间不同步导致 TLS 证书验证失败。在构建节点上同步系统时间(例如,配置 NTP 服务)。
- Failed to push image:unauthorized:镜像仓库认证失败。检查挂载到容器内的认证 Secret 路径是否正确,凭证是否有效或已过期。
如何通过代理pull远程镜像?
由于buildah或者podman默认是无守护进程运行的,因此可以通过设置代理进行镜像下载,操作如下:
1HTTPS_PROXY=socks5://10.97.109.50:6001 buildah pull docker.io/library/nginx
镜像下载错误解决
buildah pull镜像遇到下面问题:
ERRO[0043] While applying layer: ApplyLayer stdout: stderr: potentially insufficient UIDs or GIDs available in user namespace (requested 0:42 for /etc/gshadow): Check /etc/subuid and /etc/subgid if configured locally and run podman-system-migrate: lchown /etc/gshadow: invalid argument exit status 1
Error: copying system image from manifest list: writing blob: adding layer with blob "sha256:3531af2bc2a9c8883754652783cf96207d53189db279c9637b7157d034de7ecd": ApplyLayer stdout: stderr: potentially insufficient UIDs or GIDs available in user namespace (requested 0:42 for /etc/gshadow): Check /etc/subuid and /etc/subgid if configured locally and run podman-system-migrate: lchown /etc/gshadow: invalid argument exit status 1
这个错误通常发生在非 root (rootless) 用户运行 buildah pull 时。核心原因是系统没有为当前用户配置从属 UID/GID (subordinate ID) 范围。
🔍 原因分析
buildah 在处理某些镜像层时,需要映射 UID/GID(例如 0:42),这触发了内核层面的 ID 映射。你的用户 ID 是主机上的普通用户,但在容器内需要看起来像各种各样的用户(如 root、系统用户等)。这就需要用到 /etc/subuid 和 /etc/subgid 这两个文件。
当系统找不到该用户的映射规则时,创建 namespace 就会失败并报错。
⚙️ 解决方案:快速修复与验证
可以按以下方法诊断和修复:
1. 检查当前配置
首先运行以下命令,检查系统是否为你当前登录的用户分配了从属 ID 范围:
1cat /etc/subuid /etc/subgid | grep $USER
如果命令没有输出任何内容,说明映射未定义,需要进行配置。
2. 配置从属 UID/GID
根据你的系统环境,选择以下一种方法进行配置:
-
方案A:使用
usermod命令 (通用) 如果系统是新安装的,并希望手动为当前用户分配一个标准的 ID 范围(通常从 100000 开始,共 65536 个 ID),可以使用:1sudo usermod --add-subuids 100000-165536 --add-subgids 100000-165536 $USER这个命令会设置
$USER可以使用的 UID 和 GID 范围,分配的起始值是 100000,大小是 65536。 -
方案B:自动分配 (Arch Linux 等) 对于 Arch Linux 等系统,可以先手动创建这两个文件,后续由
useradd自动分配:1sudo touch /etc/subuid /etc/subgid这种方法通常在添加新用户时会自动处理。
3. 执行迁移并重新登录
为了让配置生效并修复已有的持久化存储状态,需要执行 podman-system-migrate,然后完全退出当前用户会话并重新登录。
1podman system migrate
2exit
重新登录后,再次运行 buildah pull 应该就能正常工作了。
💡 其他潜在问题与排查
如果修复后问题依旧,或你处于特定环境,可以检查以下几点:
-
内核配置:确认内核的
用户命名空间特性已开启。在部分旧版 Debian 系统上可能需要手动开启:1sudo sysctl -w kernel.unprivileged_userns_clone=1若要永久生效,可以将该行添加到
/etc/sysctl.conf或/etc/sysctl.d/下的配置文件中。 -
域用户 (如 FreeIPA/SSSD):如果你是域用户,
buildah可能会无法从 SSSD 读取映射信息,而只读取本地文件。可尝试检查/etc/nsswitch.conf,确保subid的配置完整,例如:subid: files sss这能确保
buildah在查找映射时,能从本地文件 (files) 和 SSSD 数据库 (sss) 两个来源进行查询。
💎 总结
这个错误的关键是 UID/GID 映射配置缺失。通过使用 usermod 或 touch 创建 /etc/subuid 和 /etc/subgid 文件,可以快速解决问题。如果问题依然存在,可能需要进一步检查内核配置或与 SSSD 等高级用户管理系统的集成情况。
总结:
- 容器化日常开发使用k3s足够了,k3s本身就足够轻量级了;
- 如果追求开箱即用,无守护进程,rootless,选podman;
- 容器内构建镜像轻量级方案是buildah;
- 容器内镜像同步方案采用skopeo;
- 无守护进程的优势是处理代理pull镜像很方便,设置环境变量,立马生效;
