背景

多年来, 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 的常用命令映射。

🛠️ 方式一:声明式构建

这是最常用、也最推荐的方式,通过执行 ContainerfileDockerfile 中的指令来构建镜像。这类似乎在 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, SETGIDcapabilities
    • 为存储目录(如 /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 自动结束,实现资源隔离和按需使用。整个流程如下:

  1. 启动 Job:用户触发的构建请求会创建一个新的 Kubernetes Job。
  2. 拉取代码:Job Pod 中的 Buildah 容器启动后,首先会从 Git 仓库(如 GitHub)拉取源代码。
  3. 执行构建:在容器内执行 buildah bud -t myapp:latest .,根据 Dockerfile 构建镜像。
  4. 推送镜像:构建完成后,执行 buildah push myapp:latest docker://myregistry/myapp:latest 将镜像推送到目标仓库。
  5. 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。

  1. 安装 Tekton:在你的 Kubernetes 集群中部署 Tekton Pipelines。
  2. 使用 Buildah Task:Tekton 社区提供了预定义的 buildah 任务,你可以直接或参考它在你的流水线中使用。
  3. 配置任务参数:在你的 Pipeline 中引用该 Task,并配置 IMAGEDOCKERFILECONTEXT 等参数。
  4. 定义工作空间:为 Task 指定一个 workspace(通常是 emptyDir 或 PVC)来存放源代码和构建上下文。
  5. 处理凭证:通过 workspacesecret 将镜像仓库的认证信息注入到容器中。

🤔 常见问题与故障排查

  • 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 映射配置缺失。通过使用 usermodtouch 创建 /etc/subuid/etc/subgid 文件,可以快速解决问题。如果问题依然存在,可能需要进一步检查内核配置或与 SSSD 等高级用户管理系统的集成情况。

总结:

  1. 容器化日常开发使用k3s足够了,k3s本身就足够轻量级了;
  2. 如果追求开箱即用,无守护进程,rootless,选podman;
  3. 容器内构建镜像轻量级方案是buildah;
  4. 容器内镜像同步方案采用skopeo;
  5. 无守护进程的优势是处理代理pull镜像很方便,设置环境变量,立马生效;