Skip to content

Instantly share code, notes, and snippets.

@Vesnica
Last active February 10, 2025 01:01
Show Gist options
  • Save Vesnica/67d7ccedd5b1404a6a66f2a5c284ff84 to your computer and use it in GitHub Desktop.
Save Vesnica/67d7ccedd5b1404a6a66f2a5c284ff84 to your computer and use it in GitHub Desktop.
BuildKit

BuildKit 作用

docker run -dti \
    --name buildkitd \
    --security-opt seccomp=unconfined \
    --security-opt apparmor=unconfined \
    --device /dev/fuse \
    -p 8080:8080 \
    -v $(pwd)/buildkitd.toml:/etc/buildkitd.toml \
    moby/buildkit:rootless \
        --config=/etc/buildkitd.toml \
        --oci-worker-no-process-sandbox \
        --oci-worker-snapshotter=fuse-overlayfs \
        --addr tcp://0.0.0.0:8080 \
        --oci-worker-selinux=false

buildkitd.toml

debug = true
trace = true
#insecure-entitlements = [ "network.host", "security.insecure" ]

[registry."192.168.121.103:5000"]
  http = true

用户:

buildkitd在buildkit中起到了什么作用?

ChatGPT:

buildkitd 是 BuildKit 的守护进程,在整个 BuildKit 架构中扮演着核心角色。它负责管理和执行 BuildKit 中的所有构建任务,并提供高效的容器镜像构建能力。以下是 buildkitd 在 BuildKit 中的主要作用和功能:


1. 构建任务的执行核心

  • buildkitd 是 BuildKit 的核心组件,负责调度和执行构建任务。
  • 当客户端(如 docker buildx 或 BuildKit CLI)发起构建请求时,请求会被传递给 buildkitd,由它负责处理具体的构建逻辑。

2. 模块化的 DAG 构建

  • BuildKit 使用有向无环图(DAG,Directed Acyclic Graph)来描述构建依赖关系,buildkitd 会解析构建请求,生成构建任务的 DAG 表示。
  • 它能够根据依赖关系高效地并行执行任务,最大化利用计算资源。

3. 缓存管理

  • buildkitd 提供了功能强大的缓存机制,可通过内容寻址实现层级复用。
  • 无论是本地缓存、远程缓存,还是分布式缓存,buildkitd 都能够高效地管理和复用构建过程中的中间结果,以减少重复工作,提高构建速度。

4. 分布式构建

  • buildkitd 支持分布式构建,可以通过不同的后端集成(如 Kubernetes、站点集群等)将构建任务分发到多个节点,以充分利用分布式计算资源。

5. 后端集成和执行环境

  • buildkitd 能够和不同的执行环境集成,包括:
    • 容器运行时(如 Docker、containerd)
    • 本地环境
    • 远程构建节点
  • 它在这些环境中管理文件系统、处理层镜像,以及运行必要的构建指令(如 RUN, COPY 等)。

6. 多种回调与接口支持

  • buildkitd 提供 gRPC API,客户端通过这一 API 与守护进程通信。
  • 它支持 BuildKit 的前端(Frontend,例如基于 Dockerfile 或基于 LLB 的构建描述)将高级构建请求转换为低级别的任务,再由 buildkitd 执行。

7. 安全性与隔离

  • buildkitd 在构建镜像时提供了增强的安全性功能,例如:
    • 根目录被隔离(无需特权的构建模式)。
    • 支持镜像构建时使用受限权限用户执行构建步骤。
  • 它还能限制访问主机文件系统,确保构建任务的隔离性和环境的洁净。

8. 可扩展性及插件支持

  • buildkitd 支持通过插件的方式扩展功能,比如自定义的源获取器、镜像推送器等。
  • 它为开发定制化构建系统提供了灵活的基础设施。

总结

buildkitd 是 BuildKit 的核心计算引擎,负责调度、执行、缓存管理和任务分发。围绕它的高性能和灵活性,BuildKit 实现了比传统镜像构建工具(如 Docker 的老版 docker build)更高效、更模块化、更安全的构建体验。

用户:

buildkitd自身是以进程形态运行在节点上的还是运行在容器内的?其依赖节点上的容器运行时吗?

ChatGPT:

buildkitd 的运行方式是灵活的,既可以在节点上以普通进程的形式运行,也可以在容器中运行,具体取决于你的部署方式和需求。以下是详细的解析:


1. 运行方式

  • 作为节点上的进程运行

    • buildkitd 可以直接以守护进程的形式运行在节点的用户环境中。
    • 在这种情况下,需要操作系统直接支持 buildkitd,通常由管理员手动安装并运行守护进程。
  • 在容器内运行

    • buildkitd 可以运行在容器内。官方提供了 moby/buildkit 镜像,该镜像可以直接用于容器化运行 buildkitd
    • 这是在现代容器化环境(如 Kubernetes 或其他容器编排系统)中更常用的方式,因为可以利用容器生态的灵活性来部署 buildkitd

2. 是否依赖容器运行时?

  • 依赖的情况由 buildkitd 的后端执行器(Target Backend) 决定:
    • 默认情况:依赖本地容器运行时

      • 在处理构建任务时,buildkitd 可以使用容器运行时(如 containerdruncDocker)来执行构建指令。这种依赖需要节点上安装一个兼容的容器运行时。
      • 目前支持的容器运行时包括:
        • containerd(推荐)
        • Docker
        • 或其他基于 OCI 的运行时(如 crun, runc)。
    • 无需依赖容器运行时:使用本地执行模式

      • buildkitd 提供了一个“本地执行后端”(local backend),在这种模式下,它可以直接在节点上执行构建任务,而无需依赖容器运行时。
      • 这对于那些因安全或资源限制无法使用容器运行时的场景(如嵌入式设备或高性能环境)非常有用,但可能会导致与环境绑定的强耦合。
    • 分布式模式:使用远程节点

      • buildkitd 以分布式方式运行时,它甚至可以将具体构建任务分发给其他运行有运行时的远程节点(例如通过 Kubernetes 或其他分布式系统 API)。

3. 常见使用场景

  • 在普通环境中运行
    • 如果只是在单节点或开发环境中使用 BuildKit,则 buildkitd 通常作为一个进程运行,并使用节点上的容器运行时(如 Docker 或 containerd)。
  • 在容器化环境中运行
    • 如在 Kubernetes 集群中,buildkitd 通常作为容器运行,并通过容器运行时管理构建任务(如 containerd 或类似的运行时)。
  • 完全无运行时依赖
    • 在某些轻量级或受限环境中,可以选择本地执行器模式,在这种情况下 buildkitd 既无需容器,也无需附加运行时,仅依赖本地系统资源完成构建任务。

总结

  • 运行方式buildkitd 既可以直接运行在节点上,也可以运行在容器内,具体取决于部署场景。
  • 容器运行时依赖
    • 默认情况下,为了执行构建任务,buildkitd 通常依赖节点上的容器运行时(如 containerd 或 Docker)。
    • 但在本地执行器模式下,它可以不依赖容器运行时,直接在当前节点执行任务。
  • 部署灵活性:这种灵活性使得 BuildKit 能够在从本地开发环境到大规模分布式编排系统的多种环境中无缝运行。

用户:

在容器内运行时,由于默认无法访问节点上的容器运行时,buildkitd此时是否只能使用本地执行后端来构建镜像?

ChatGPT:

buildkitd 运行在容器内时,默认情况下确实无法直接访问节点上的容器运行时(如 containerdDocker),除非通过特定的配置来开放这种访问权限。不过,即便默认无法访问容器运行时,buildkitd 并不局限于使用 本地执行后端(local backend) 来构建镜像,它仍然可以通过其他方式实现容器化构建,具体取决于你如何设置运行环境。

以下是详细解析:


1. 默认情况下:无法直接访问节点容器运行时

  • 容器内的进程通常与宿主机系统隔离,无法访问宿主节点的容器运行时(例如 /var/run/docker.sockcontainerd 的 socket)。
  • 因此,如果没有明确配置额外的权限,buildkitd 确实无法使用节点上的容器运行时作为后端。

2. 容器内的可用后端

在这种隔离环境下,buildkitd 可使用以下后端来执行构建任务:

a. 本地执行后端(local backend)

  • 如果无法访问容器运行时,buildkitd 可以使用 local backend,在容器内的文件系统上直接执行构建任务。
  • 本地执行后端完全绕过容器,需要在工作目录中设置所有依赖环境,镜像内容直接在本地文件系统生成。
  • 适用场景:轻量级构建任务,或者对环境隔离要求较高的情况。
  • 局限性:无法很好地利用容器的镜像分层管理、隔离性以及现有容器运行时生态。

b. 远程构建后端(remote backend)

  • buildkitd 也支持将具体的构建任务委派给远程节点执行(例如在 Kubernetes 上使用 BuildKit 的分布式模式时)。
  • 可用的远程后端通常依赖外部 buildkitd 实例,该实例可以运行在具备容器运行时的节点上。
  • 适用场景:分布式环境(例如多节点、Kubernetes 集群),或希望使构建任务与容器隔离。

3. 如何让容器内的 buildkitd 访问节点容器运行时

如果希望允许 buildkitd 在容器内使用节点上的容器运行时,可以通过以下方法实现访问权限:

a. 挂载容器运行时的 Socket

  • 对于 Docker 或 containerd,可以将节点上的容器运行时相关的 UNIX socket 挂载到 buildkitd 容器内,使其能够直接调用容器运行时:

    • 对于 Docker:
      docker run -v /var/run/docker.sock:/var/run/docker.sock moby/buildkitd
    • 对于 containerd:
      docker run -v /run/containerd/containerd.sock:/run/containerd/containerd.sock moby/buildkitd
  • 配置完成后,buildkitd 容器内的进程可以通过这些挂载的 socket 访问节点的容器运行时。

b. 使用特权容器

  • 为了进一步解决隔离问题,可以通过运行特权容器(--privileged)使容器拥有访问主机设备和资源的权限:
    docker run --privileged moby/buildkitd
  • 特权模式允许容器内的 buildkitd 直接访问宿主节点的容器运行时和其他资源。

c. Kubernetes 上的配置

  • 在 Kubernetes 中运行时,可以通过以下方式实现类似功能:
    • Pod 的容器中挂载运行时的 socket 文件:
      volumeMounts:
        - mountPath: /var/run/docker.sock
          name: docker-sock
      volumes:
        - name: docker-sock
          hostPath:
            path: /var/run/docker.sock
    • 或者,通过设置 privileged: true 来运行特权容器(通常需要安全评估,不建议在生产环境中使用)。

4. 总结

  • 如果默认不对容器开放与节点容器运行时之间的连接,buildkitd 在容器内将无法使用运行时后端,只能选择 本地执行后端远程后端
  • 如果希望 buildkitd 在容器内仍具备访问节点容器运行时的能力,可以通过挂载运行时的 socket 或使用特权模式实现。
  • 实际配置取决于需求
    • 如果对构建性能和容器生态有较高依赖,建议挂载 socket 或采用远程后端。
    • 如果对隔离性要求较高,无需容器运行时支持,可以使用本地执行后端。

用户:

本地执行后端是如何构建镜像的?

ChatGPT:

buildkitd本地执行后端 (local backend) 并不依赖容器运行时,而是直接使用容器进程所在的文件系统和主机的计算资源来构建镜像。

当使用本地执行后端时,buildkitd 处理镜像构建的方式有所不同。以下内容详细解析了本地执行后端的具体工作原理及构建流程:


1. 本地执行后端的核心特性

  • 独立性:本地执行后端完全运行在 buildkitd 自身的进程中,无需依赖外部的容器运行时(如 Dockercontainerd)。
  • 直接操作文件系统:所有的构建操作(如解压文件、执行操作)都通过操作文件系统和直接调用可执行文件的方式完成。
  • 镜像格式:最终生成的镜像是标准的 OCI 镜像格式,支持推送到镜像仓库,可以被其他容器运行时(如 Dockercontainerd)使用。

2. 构建流程解析

使用本地执行后端构建镜像时,buildkitd 会模拟镜像的构造过程。以下是具体步骤:

(1)解析输入文件

  • 当你运行 docker buildx build 或直接调用 BuildKit API 时,buildkitd 接收 Dockerfile 或者 BuildKit 的低级指令(LLB,low-level build)。
  • buildkitd 解析这些指令,并决定执行哪些构建任务(如复制文件、运行命令等)。

(2)在本地环境中模拟构建步骤

每条构建指令(Dockerfile 中的 RUNCOPY 等)都会被 buildkitd 转化为对应的任务。以下是一些典型的操作及处理方式:

a. 文件操作

  • COPY/ADD 指令
    • 直接读取构建上下文(通常由客户端提供,包含源代码和依赖文件),将其解压或复制到一个临时目录中。
    • 例如,将 Dockerfile 中的 COPY . /app 操作解释为文件系统上的复制。

b. 运行指令

  • RUN 指令
    • 使用容器内的本地文件系统,在当前环境中直接执行指令。
    • BuildKit 会在容器基础镜像的根文件系统中(或模拟出来的根文件系统)执行这条命令,大致相当于运行本地进程:
      /bin/sh -c "apt-get update && apt-get install -y curl"
    • 构建命令的结果(如文件系统的更改)会被捕获并作为下一步构造层的基础。

c. 文件系统层的管理

  • 每执行一条指令(如 RUN),BuildKit 都会创建一个新的文件系统快照(snapshot),用于记录当前构建层的更改。
  • 这些快照类似于 Docker 镜像的“镜像层”:
    • 比如在执行 RUN apt-get install -y curl 后,包含已安装依赖的文件系统快照会被保存为一个新层。
    • 每个快照都以前一个快照为基础,最终合并构成完整的根文件系统。

(3)生成 OCI 镜像

  • 构建完成后,buildkitd 会将所有生成的文件系统快照打包为一个符合 OCI 镜像格式 的镜像文件(tar 包)。
  • 生成的镜像可以直接保存到本地,也可以推送到镜像仓库(如 Docker Hub、Harbor)。

(4)缓存优化

  • 本地执行后端支持缓存功能,类似容器运行时的镜像缓存:
    • 执行时,buildkitd 会检查是否存在匹配的中间层快照(如相同的 RUNCOPY 指令)。
    • 如果找到匹配的缓存,buildkitd 会跳过指令执行,直接复用已有的快照层。

3. 本地执行后端的优缺点

优点

  1. 无依赖性
    • 无需依赖 Docker 或 containerd 等外部运行时,适合运行在受限或隔离环境中(如容器内、无容器运行时的独立服务器)。
  2. 分布式构建
    • 本地执行后端可以通过 BuildKit 的远程构建模式轻松扩展,分布式地执行构建任务。
  3. 灵活性
    • 是一个自包含的构建方式,完全由 BuildKit 接管执行,可以与任意镜像仓库兼容。

缺点

  1. 镜像层隔离性较弱
    • 本地执行后端模拟镜像分层,而不是天然利用容器运行时的镜像仓库和分层管理机制。
  2. 构建环境限制
    • 构建时完全依赖容器内部的环境(例如可用的工具和操作系统),这可能需要提前确保 buildkitd 容器拥有构建所需的依赖。
  3. 无法直接运行镜像
    • 如果需要直接测试构建的镜像,必须将生成的 OCI 镜像文件推送到一个镜像仓库或导入到容器运行时里。

4. 示例:本地执行构建

假设你在容器内通过本地执行后端运行如下 Dockerfile:

FROM alpine:latest
RUN echo "Hello, BuildKit!" > /message
COPY . /app

在本地执行后端的方式下,BuildKit 会按如下方式构建:

  1. FROM 指令:抓取并解压 alpine:latest 的基础镜像(镜像是从镜像仓库拉取的 tar 包,无需通过宿主容器运行时)。
  2. 执行 RUN 指令:在当前文件系统创建根目录 /message 文件,记录写入内容。
  3. COPY . /app 指令:将构建上下文中的文件复制到 /app
  4. 生成镜像:所有更改将被分层记录并打包为一个符合 OCI 标准的镜像文件,供后续推送或部署使用。

总结

本地执行后端通过直接操作文件系统和调用指令的方式在 buildkitd 容器或实例中完成镜像构建,不依赖外部容器运行时。尽管在某些方面缺乏容器原生的高效分层管理和镜像运行能力,但它以去依赖的方式为构建提供了很大的灵活性,特别适用于受限或具有自定义需求的构建环境。

用户:

本地执行后端包含容器运行时(如runc)和镜像分层文件系统吗?还是直接输出OCI标准的文件包?

ChatGPT:

BuildKit 的本地执行后端 中,并 不包含传统容器运行时(如 runc)和镜像分层文件系统(如 overlayfs。它直接输出符合 OCI 标准的镜像文件(通常是一个包含镜像层内容的 tar 包),并不像传统容器运行时那样依赖 runc 或使用容器分层文件系统管理镜像层。

详细解析如下:


1. 容器运行时 (runc) 和镜像分层文件系统

  • 像 Docker 或 containerd 这样的容器运行时,依赖标准的文件系统驱动(如 overlayfs, aufs, 或 btrfs),用来管理容器镜像的分层存储。每一层构建都会实时加载到这些文件系统的分层管理中。
  • 同时,Docker 和 containerd 通过调用 runc 创建和运行容器(runc 是 OCI 标准的容器运行时,用来启动容器化进程)。

在 BuildKit 的本地执行后端中,以上两个特性都被摒弃:

  1. 本地执行后端不依赖 runc,构建指令(如 RUN, COPY 等)直接在主机系统或容器内作为普通进程执行,模拟镜像层次。

  2. 没有使用分层文件系统,而是直接在本地操作文件系统,将执行结果生成独立的文件快照,最终打包成符合 OCI 镜像标准的镜像。


2. 输出的文件格式:OCI 标准镜像包

本地执行后端最终的结果是一个符合 OCI 镜像格式的打包文件,该文件包含以下内容:

  1. 镜像元数据 (manifest.json)

    • 记录多个镜像层组成的描述和信息,类似镜像"目录"。
  2. 镜像层快照 (layer.tar)

    • 每个 RUN, COPY 等指令对应的文件系统变更会被保存为一层快照,并打包成 .tar 文件,最终存放在镜像中。
  3. 配置文件 (config.json)

    • 包括镜像的配置数据,例如基础镜像、环境变量、入口点(ENTRYPOINT/CMD)、构建时间、作者信息等。

这些文件会被打包成镜像文件,通过 buildkitd 上传到镜像仓库或在本地保存。


3. 如何执行构建步骤中的任务

本地执行后端在构建期间,虽然性能和用户体验类似 Docker,但在实现方式上有所不同:

  • 模拟分层机制
    • 每条 Dockerfile 指令会在 BuildKit 自己的构建环境中生成独立的文件层快照(通过差分内容捕获文件系统的变化),但这是通过普通文件系统直接存储的,而非容器运行时的分层文件系统驱动。
  • 直接执行指令
    • 指令(如 RUN)直接在主机环境或 BuildKit 管控的沙盒中运行,而不需要 runc 等底层容器运行时的支持。

举例来说:

FROM ubuntu:20.04
RUN apt-get update && apt-get install -y curl
  • RUN 会在 BuildKit 的沙盒内运行,模拟镜像层文件系统。构建完成后,将沙盒内容保存为镜像层文件(一个 .tar)。

这些 .tar 最终会被合并打包成一组符合 OCI 镜像标准的文件。


4. 总结

  • 容器运行时(如 runc:本地执行后端不使用 runc,而是直接在 BuildKit 管控的沙盒或主机系统中执行构建任务。
  • 镜像分层文件系统:本地执行后端没有使用分层文件系统(如 overlayfsaufs),而是通过文件快照能力管理每一层。
  • 最终输出:一个符合 OCI 标准的镜像文件包(通常是若干 tar 文件和配置文件),可直接推送到容器镜像仓库,或被 dockercontainerd 等容器运行时加载使用。

因此,本地执行后端更像是将 OCI 镜像构建抽象为一组独立的文件操作和沙盒运行环境,与 Docker 使用容器运行时管理构建的方式有根本性的不同。

用户:

本地执行后端如何从远端registry下载基础镜像并推送构建完毕的镜像的?

ChatGPT:

BuildKit 的本地执行后端在处理镜像的下载(拉取)和上传(推送)时,尽管它不依赖传统的容器运行时(如 runcdocker),但仍具有与远端镜像仓库(Registry)交互的能力。这是通过直接使用 OCI 标准 和镜像仓库 API 实现的。

以下是 BuildKit 本地执行后端从远端 Registry 拉取基础镜像推送构建镜像的过程详细说明:


1. 拉取基础镜像的过程

BuildKit 使用 OCI 镜像分层机制和 Registry API,并没有依赖 docker pull 的实现,而是从远程 Registry 中直接拉取镜像内容。以下是主要步骤:

(1)获取镜像的元信息

  • 在构建阶段,当你在 Dockerfile 中指定基础镜像(例如 FROM ubuntu:20.04),BuildKit 会自动通过镜像仓库的 API 查找并获取该镜像。
  • BuildKit 会进行以下操作:
    1. 查询远程 Registry 的 /manifests API,以获得镜像的元信息:
      • 包括镜像的清单文件(Manifest),描述镜像的分层信息和配置文件路径。
      • 举例,如果你拉取 ubuntu:20.04,Registry 会返回一个包含层信息的镜像 Manifest。
    2. 验证镜像的摘要(Digest)和标签(Tag),确保其合法性。

(2)逐层拉取镜像层

  • 基于拉取的 Manifest 文件信息,BuildKit 会从远程 Registry 将镜像的各个层(Layer)分别拉取到本地。
    • 每个镜像层是一个 .tar 压缩文件,存储了该层的文件系统快照。
    • BuildKit 会通过 /blobs API 下载每一层,需要时可以并行拉取以提升速度。
    • 例如,layer.tar 文件可能包含 /usr/bin, /etc 等文件结构。

(3)解压并校验镜像层

  • 下载完成后,BuildKit 会解压每一层的 tar 文件,将其内容展开到本地文件系统的指定路径(Set)。
  • 解压后进行文件校验,确保文件层的内容与远程仓库中的摘要一致(哈希校验)。

(4)处理镜像配置

  • 除了镜像层,BuildKit 还会下载镜像的配置文件,解析其中的字段。
    • 配置文件通常包含基础环境信息,比如环境变量(ENV)、入口点(ENTRYPOINT)、命令(CMD)等。
    • 这些信息会作为镜像构建的初始状态。

示例请求流程:

假设我们从 Docker Hub 拉取 ubuntu:20.04 基础镜像,以下是可能的操作:

  1. BuildKit 请求镜像的 Manifest:
    GET https://registry-1.docker.io/v2/library/ubuntu/manifests/20.04
    
  2. 镜像仓库返回包含镜像层信息的清单和对应的 blob
    {
      "schemaVersion": 2,
      "config": {
        "mediaType": "application/vnd.oci.image.config.v1+json",
        "digest": "sha256:abc123..."
      },
      "layers": [
        {
          "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
          "digest": "sha256:layer1sha...",
          "size": 2812235
        },
        {
          "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
          "digest": "sha256:layer2sha...",
          "size": 5738342
        }
      ]
    }
  3. 对每个 layerdigest 发起 GET /blobs/<sha256> 请求,下载文件。

2. 推送构建后的镜像的过程

完成构建后的镜像,BuildKit 会将其打包为 OCI 格式并推送到远程镜像仓库。与拉取类似,推送也通过直接与 Registry 的 HTTP API 进行交互,而不依赖 Docker 或其他容器运行时。

以下是主要推送步骤:

(1)生成镜像元数据和层

  • 在构建过程中,BuildKit 按照 Dockerfile 的指令依次生成文件系统快照,并将每个快照作为一层存储起来。
    • 每个文件系统快照会打包为 .tar 文件,并生成对应的哈希(sha256 值)。
    • 示例:
      layer1.tar -> sha256:abc123...
      layer2.tar -> sha256:def456...
      

(2)上传镜像层文件

  • BuildKit 会通过镜像仓库的 /blobs API 上传镜像的每一层。
  • BuildKit 首先会判断远程仓库是否已经存在该层(基于 sha256 摘要),如果存在则跳过上传;如果不存在则开始逐层上传,避免重复上传相同内容。
    • 判断层是否存在:
      HEAD https://registry-1.docker.io/v2/myimage/blobs/<sha256>
      
    • 如果仓库返回 404,则上传该层:
      PUT https://registry-1.docker.io/v2/myimage/blobs/uploads/
      

(3)上传镜像配置文件

  • BuildKit 还会生成和上传镜像的配置文件(config.json),描述入口点、标签、环境变量等镜像的元信息。

(4)上传镜像清单

  • 最后,BuildKit 会将生成的层信息和配置文件组织为一个新的镜像 Manifest 并推送到远端 Registry:
    1. PUT 请求将 Manifest 推送到 Registry:
      PUT https://registry-1.docker.io/v2/myimage/manifests/latest
      

3. 总结

本地执行后端的镜像下载和推送过程完全符合 OCI 镜像规范,并直接通过镜像仓库的 HTTP API 实现。主要特性包括:

  1. 镜像拉取

    • 通过 /manifests API 获取镜像元信息(包括镜像层和配置)。
    • 通过 /blobs API 逐层拉取镜像快照,并解压至本地文件系统。
  2. 镜像推送

    • 按照构建结果生成 .tar 的层文件和镜像配置文件。
    • 通过 /blobs API 分层上传镜像。
    • 通过 /manifests API 提交镜像清单。

这一流程完全摆脱了 docker 命令行工具和低级容器运行时(如 runccontainerd),而是直接依靠网络协议、HTTP API 和 OCI 规范完成任务。同时,BuildKit 内建了分层缓存机制,能有效减少重复的层上传和下载,提升效率。

用户:

使用本地执行后端构建镜像时,实际上也不会创建任何容器进程?

ChatGPT:

是的,在使用 BuildKit 的 本地执行后端(local execution backend)构建镜像时,实际上不会创建任何实际运行的容器进程。相较于传统的 docker build,它的运行模式更加轻量化且抽象。这种设计是 BuildKit 的一个核心特性,使其能更高效、更灵活地完成构建任务。

以下是原因及实现方式的详细说明:


为什么 BuildKit 不需要创建容器进程?

传统的 docker build 使用 Docker 容器执行每个指令(如 RUN),这是基于 Docker Daemon 的工作模式,意味着:

  1. 每个 RUN 指令都会启动一个新容器进程,在这个容器内执行指令并捕获更改。
  2. 执行后,将文件系统的更改打包为一个新的镜像层。

而 BuildKit 的本地执行后端完全避免了这种依赖。主要原因包括:

1. 文件系统操作的直接管理

BuildKit 通过直接操控文件系统来模拟构建过程。在每一步(如 RUNADD)中,BuildKit 会:

  • 使用文件系统的 快照机制 来表示镜像的分层。
  • 模拟应用文件或命令的效果,而非真正启动容器去运行指令。

这种方式让每一步的执行非常高效,因为它无需真正启动一个容器进程,也无需处理容器的生命周期管理。

2. 基于「镜像分层」操作

BuildKit 直接以镜像分层的方式实现构建指令:

  • 对于每个 RUNADD 等操作,BuildKit 会在基础层的快照上应用更改。
  • 所有更改被记录为镜像新的一层,而实际的操作是对文件系统的操作:修改、删除、添加。

3. 不依赖 Docker 容器运行时

本地执行后端会直接使用 BuildKit 的执行器(executor)处理 RUN 指令,而不是通过启动 Docker 容器。命令的执行会直接在本地环境中运行,并使用隔离的文件系统快照存储命令执行结果:

  • 不会依赖 Docker Daemon。
  • 不依赖 containerdrunc 创建容器进程。
  • 减少了容器启动和销毁的开销。

实际构建过程中的替代设计

为了满足构建需求,BuildKit 使用许多设计技巧来替代传统的容器进程:

1. 快照(Snapshot-based Execution)

  • 每一步(例如 RUN)都会创建一个文件系统快照(snapshot)。
  • 快照是增量操作的基础,指令的效果应用在快照上,而不是在容器中进行。

快照的优点:

  • 隔离性更强,每条指令的结果不会影响其他快照。
  • 比传统容器操作速度更快,因为快照操作可以直接通过文件系统完成。

2. RUN 指令的本地执行

当你在 Dockerfile 中使用 RUN

  • BuildKit 仅在本地环境中执行该命令,而不是在一个容器进程内。
  • 本地环境的隔离性由 BuildKit 内部完成,但并不会生成显式的容器。

这一特性可以通过 BuildKit 的日志看到,RUN 的实际执行过程不会触发容器 ID(通常 docker build 日志中会看到容器的前几位 ID)。

3. ADDCOPY 的优化

对于 ADDCOPY,BuildKit 不会通过容器中的文件系统完成指令,而是直接在本地环境中操作:

  • 文件会直接解压或拷贝至目标文件系统快照。

4. 高效缓存

因为 BuildKit 不实际运行容器,它的缓存能够以更精细的粒度实现。例如:

  • 文件层的内容哈希(digest)。
  • 指令的执行上下文快照。

如果两次构建的上下文不发生变化(比如相同的 Dockerfile 和输入文件),BuildKit 可以直接跳过该步骤,而无需重复执行。


特殊场景:需要完全隔离怎么办?

虽然本地执行后端默认不会创建容器进程,但它可以结合其他方式实现一定程度的隔离,具体包括:

1. 使用 rootless 模式

本地执行后,BuildKit 运行的命令会以当前用户身份执行,且不会侵入系统级目录。因此,它已具有一定程度的安全性。

2. 切换到其他执行后端

如果需要更严格的隔离,BuildKit 支持多种执行后端,例如:

  • 容器后端(containerd)。
  • Kubernetes 后端。

通过这些后端,你可以让命令运行在一个真实的容器或 Kubernetes Pod 中,提供明确的进程隔离。


总结

在 BuildKit 的本地执行后端中,构建镜像的过程中并不会创建实际的容器进程。它通过快照机制和文件系统直接执行构建操作,使得构建任务更加高效轻量化。这种设计带来的好处包括:

  • 无需依赖 Docker Daemon 或低级容器运行时。
  • 构建速度显著提升,因为避免了启动和销毁容器的开销。
  • 文件系统层级操作更加直接,缓存机制更加高效。

对于普通用户来说,BuildKit 的本地执行后端在功能上已经能完全替代传统 docker build——它不仅更快,而且更灵活。

用户:

如果buildkitd运行在容器内,使用本地执行后端构建镜像时,对文件系统有何要求?常见的容器运行时能够支持这些要求吗?

ChatGPT:

当 BuildKit 的守护进程(buildkitd)运行在容器内,并使用**本地执行后端(local execution backend)**构建镜像时,对文件系统有一些特定要求。以下是这些要求的详细说明,以及常见的容器运行时是否能够满足这些要求的分析:


1. 文件系统的要求

由于本地执行后端依赖于快照和文件系统以模拟镜像的分层构建,因此文件系统必须支持以下特性

1.1 支持文件快照(Snapshot)机制

BuildKit 使用快照机制(通过 overlayfs 或设备映射等实现)来模拟镜像的分层。在构建过程中,RUN 和其他 Dockerfile 指令的影响被应用到一个临时快照里,而不直接修改底层文件系统。

  • 文件系统需要支持基于层的写时复制(Copy-on-Write, COW)功能,例如:
    • OverlayFS(Linux 上非常常见的文件系统驱动,用于合并多层文件系统)。
    • Btrfs(支持内核级的快照和子卷)。
    • ZFS(支持高级快照功能)。
    • XFS(结合设备映射可以满足部分需求)。

1.2 高性能的随机读写能力

本地执行后端在构建过程中,需要操作大量的文件(如复制文件、解压归档 COPYADD 的内容等),并且文件的访问可能是随机的。因此,文件系统需要在随机读写上表现良好。

  • 典型文件系统,如 Ext4、OverlayFS 等,能够很好地支持这一点。

1.3 保证 POSIX 兼容性

与使用容器后端不同,本地执行后端的 RUN 指令直接在容器的本地文件系统中执行,因此必须保证对常见文件系统操作的完整支持。这些操作包括:

  • 符号链接。
  • 文件权限管理。
  • 硬链接。
  • 文件锁定。

非 POSIX 兼容的文件系统(如部分嵌入式环境中的专用文件系统)可能无法满足 BuildKit 的需求。

1.4 持久挂载与隔离性

  • BuildKit 将使用容器的挂载点作为构建上下文和临时快照的存储区域,因此文件系统需要支持高效地创建共享挂载点(shared mounts)或私有绑定挂载点(private bind mounts)。
  • 必须确保快照操作不会影响容器外的系统文件。

2. 常见容器运行时的支持情况

常见的容器运行时(如 Docker、containerd、Kubernetes)可以满足 BuildKit 本地执行后端的文件系统需求,但需要注意具体的存储驱动和配置。以下是常见容器运行时的分析:

2.1 Docker

Docker 容器运行时常用的文件系统存储驱动包括:

  • OverlayFS(推荐):
    • OverlayFS 是目前 Linux 上的主流存储驱动之一,既支持文件系统的写时复制操作,也支持高效的文件快照。
    • 对于挂载在 /var/lib/docker 的文件系统,Docker 会使用 OverlayFS 的支持来管理镜像层,完全能够满足 BuildKit 对文件系统的所有需求。
  • Device Mapper(次优):
    • Device Mapper 是一种较老的存储驱动,支持基于块的快照和写时复制。
    • 性能比 OverlayFS 略差,但也能满足 BuildKit 的需求。
  • Btrfs:
    • 如果主机文件系统是 Btrfs,Docker 同样可以较轻松地支持 BuildKit 的快照需求。

✳️ 如果 Docker 使用 Ext4 作为底层文件系统,则需要确保启用了 OverlayFS,因为原生的 Ext4 不支持多层快照和合并功能。


2.2 containerd

containerd 是 Docker 使用的核心容器运行时,也可以独立部署。它直接支持多种存储驱动,包括:

  • OverlayFS(默认支持)。
  • Stargz(远程镜像分层格式):对特定的 BuildKit 构建有优化。
  • Native snapshotter(直接在主机文件系统上操作)。

在容器内运行 BuildKit 时,如果 containerd 使用 OverlayFS 或 Btrfs 作为容器的存储机制,完全能够满足 BuildKit 本地执行后端的快照需求。


2.3 Kubernetes

在 Kubernetes 环境下,buildkitd 容器通常由一个 Pod 承载。对于本地执行后端,Kubernetes 的支持主要取决于:

  • Node 节点的底层存储驱动:如果节点基于 OverlayFS 或 Btrfs,则没有问题。
  • 主机目录挂载:buildkitd 容器需要挂载某个目录作为工作的上下文存储区域,该区域底层必须支持文件快照。

需要注意的是,如果 Kubernetes 集群使用非标准的存储机制(如远程挂载的 NFS 或 CephFS),可能无法满足 BuildKit 的文件系统需求,因为这些文件系统通常不支持快照。


总结兼容性分析

容器运行时 支持文件系统快照 POSIX 兼容性 COW 支持 BuildKit 本地执行支持性
Docker (OverlayFS) ⭐⭐⭐⭐⭐
containerd ⭐⭐⭐⭐⭐
Kubernetes 视存储配置而定 ✳️ ✳️ ⭐⭐(可能需要配置优化)
非 OverlayFS/NFS ❌(不支持快照) 可能受限 依赖实现 ⭐(存在兼容风险)

3. 实际使用中的注意事项

3.1 确保使用 OverlayFS 或等效存储驱动

要在容器环境中良好支持 BuildKit 的本地执行后端,建议确保容器的存储驱动为 overlay2 或等效支持快照的机制。这可以通过以下方式检查:

  • Docker: 使用 docker info 查看存储驱动,确认是 overlay2
  • containerd: 检查配置,确保使用默认的 OverlayFS snapshotter。

3.2 挂载主机存储目录

buildkitd 容器运行时,需要挂载一个主机目录以提供构建上下文和临时存储:

  • 挂载点必须支持快照操作。
  • 推荐挂载目录位于 OverlayFS 或 Btrfs 分区上。

例如,启动 BuildKit 时可以增加如下挂载参数:

docker run -d --name buildkitd \
  -v /var/lib/buildkit:/var/lib/buildkit \
  --privileged moby/buildkit

3.3 避免远程存储系统

某些远程存储(如 NFS、Ceph)尽管支持 POSIX 操作,但通常不支持高效的快照功能;因此应避免将 buildkitd 的工作目录挂载到这些存储上。


总结建议

  1. 优选环境:使用 Docker(overlay2 驱动)或 containerd(默认 snapshotter 为 OverlayFS)作为容器运行时。
  2. 避免远程文件系统或不会支持快照的存储机制(如 NFS)。
  3. 确保 Kubernetes 环境中节点使用兼容的文件系统(如 Ext4 + OverlayFS、Btrfs、XFS)。

常见的容器运行时在合适配置下(如 OverlayFS 驱动)基本都能够很好支持 BuildKit 的本地执行后端,只需注意特定的存储方案是否满足快照和 POSIX 需求。

用户:

如果docker的存储驱动是overlay2,其启动buildkitd容器并使用本地执行系统构建镜像(未挂载任何主机目录),此时是否涉及到了overlay文件系统的嵌套?

ChatGPT:

是的,在这种情况下,可能会涉及到 OverlayFS 文件系统的嵌套问题。如果 Docker 的存储驱动是 overlay2,并且容器内部使用 BuildKit 的本地执行后端来构建镜像(且没有挂载额外的主机目录),因为 buildkitd 本身运行于 Docker 的 overlay2 存储驱动上,而它的本地执行后端又会在构建镜像时创建自己的快照层,这就会导致 OverlayFS 的嵌套。


什么是 OverlayFS 嵌套问题?

OverlayFS 文件系统嵌套是指:

  • 外层容器的文件系统已经是基于 OverlayFS 的(例如,当 Docker 使用 overlay2 作为存储驱动时,容器的根文件系统是一个 OverlayFS 层叠文件系统)。
  • 容器内的应用(如 BuildKit)又试图使用 OverlayFS 来实现快照功能(例如,本地执行后端使用 OverlayFS snapshotter)。

这样就出现了 一个 OverlayFS 文件系统运行在另一个 OverlayFS 文件系统之上 的情况。这种嵌套可能在某些情况下导致错误,尤其是当内核对 OverlayFS 的嵌套支持不足时。


内核对 OverlayFS 嵌套的支持情况

Linux 内核对 OverlayFS 的嵌套支持存在一定的限制:

  1. 早期内核版本(< 5.x):

    • OverlayFS 嵌套未被正式支持,尝试嵌套使用会直接导致报错。
    • 具体错误可能是:
      mount: mounting overlay on /some/path failed: Invalid argument
    • 这是因为早期内核的 OverlayFS 实现中不能识别底层已经有一个 OverlayFS。
  2. 较新内核版本(>= 5.x):

    • 内核在一定程度上支持了 OverlayFS 的嵌套,即如果底层 OverlayFS 文件系统的「lowerdir」目录采用只读挂载,那么嵌套的 OverlayFS 是可以正常工作的。
    • 但即便如此,嵌套 OverlayFS 的性能和可靠性可能仍然不如非嵌套使用。

docker + buildkitd 的嵌套情况

当 Docker 使用 overlay2 作为存储驱动,并且未挂载任何主机目录时,buildkitd 容器的构建和文件系统操作可能涉及以下场景:

  1. 外层文件系统(Docker 的 OverlayFS)

    • Docker 容器的文件系统是运行在 overlay2 之上的。
    • 容器的根文件系统是一个 Overlay 的「merged」层。
  2. 内层文件系统(BuildKit 的操作)

    • 如果 buildkitd 使用本地执行后端,它在容器内部也会创建快照层(默认通过 OverlayFS snapshotter)。
    • 这些快照层实际上会运行在外层的 OverlayFS 之上。

由于这种操作涉及 overlay -> overlay 的嵌套,如果 Linux 内核版本支持不佳,可能会出现错误。


如何避免或解决 OverlayFS 嵌套问题?

为了避免 OverlayFS 的嵌套问题,可以考虑以下几种方法:

1. 挂载主机目录

  • 在启动 buildkitd 容器时,为 BuildKit 的存储目录显式挂载一个主机目录。例如:
    docker run -d --name buildkitd \
      --privileged \
      -v /var/lib/buildkit:/var/lib/buildkit \
      moby/buildkit
  • 挂载的主机目录绕过了 Docker 的 overlay2 存储驱动,使得容器内的 BuildKit 在主机目录中创建快照层,避免 OverlayFS 的嵌套。

2. 升级 Linux 内核

  • 如果需要在没有主机目录挂载的情况下使用 buildkitd 容器,建议确保主机运行的 Linux 内核版本为 5.x 或更高,以支持 OverlayFS 嵌套。

3. 切换 Docker 存储驱动

  • 如果主机使用的是较老的 Linux 内核(不支持 OverlayFS 嵌套),可以考虑将 Docker 的存储驱动从 overlay2 切换为其他支持快照功能的驱动,例如 devicemapperbtrfs。这些驱动不会涉及 OverlayFS 嵌套问题,但需要主机文件系统支持。

4. 使用其他快照工具

  • 如果 buildkitd 容器无法绕开 OverlayFS 嵌套问题,可以考虑使用非本地的执行环境,例如使用 Kubernetes 的 Remote Execution 来离线处理构建过程。

结论

当 Docker 的存储驱动是 overlay2 并且 buildkitd 容器没有挂载主机目录时,会涉及 OverlayFS 的嵌套问题。这可能导致构建失败或性能下降,具体取决于主机的 Linux 内核版本。

推荐方案:

  1. 优先挂载主机目录:通过挂载 /var/lib/buildkit 到主机目录,避免 overlay -> overlay 的嵌套。
  2. 升级 Linux 内核:确保运行环境至少使用 Linux 内核 5.x 或更高版本以支持 OverlayFS 嵌套。
  3. 切换存储驱动:必要时切换 Docker 存储驱动以绕过 OverlayFS 嵌套限制。

如果满足上述条件,buildkitd 使用本地执行后端时可以正常构建镜像,并避免可能的 OverlayFS 嵌套问题。

用户:

挂载到主机目录时,对目标挂载点有何要求?

ChatGPT:

在挂载到主机目录时(例如,将 BuildKit 容器的存储目录挂载到主机的某个路径),对目标挂载点需要满足一些基本要求,以确保挂载过程和后续操作的正确性和稳定性。以下是对目标挂载点的详细要求和建议:


1. 磁盘空间充足

  • 要求:
    • 目标挂载点所在的磁盘应有足够的剩余空间来存储 BuildKit 的构建数据,如快照、构建缓存等。这些数据可能会随构建任务的复杂性和频率而增长。
  • 建议:
    • 确保挂载的路径所在磁盘剩余空间至少 大于预计构建镜像大小的2倍,以防构建过程中因磁盘不足而失败。

2. 主机文件系统的类型

  • 要求:
    • 确保目标目录所在的主机文件系统支持 BuildKit 的操作方式,例如符号链接、写时复制(CoW)、白化目录(whiteout files)等,尤其在默认情况下,BuildKit 的快照工具通常使用 OverlayFS 或其他底层存储特性。
  • 推荐文件系统:
    • 优选:ext4xfsbtrfs 等常见的 Linux 文件系统。
    • 避免:NTFS、FAT32 等简单文件系统,这些通常不能完全支持 Linux 上的 POSIX 文件系统语义。
  • 额外建议:
    • 如果有 OverlayFS 快照工具的嵌套需求,可以选择 ext4xfs 文件系统,并使用较新的内核版本(Linux 5.x 或更新)。

3. 挂载点需为空目录

  • 要求:
    • 挂载到主机的路径应为一个空的目录,否则可能导致挂载后目录中的现有数据被隐藏或无法访问。例如:
      mkdir -p /var/lib/buildkit
      然后挂载:
      docker run -d --name buildkitd \
        -v /var/lib/buildkit:/var/lib/buildkit \
        moby/buildkit
  • 原因:
    • 当挂载新的卷或目录时,挂载点下的现有数据会被挂载卷覆盖,并且直到卷卸载后才能再次访问原始数据。

4. 挂载位置的权限设置

  • 要求:
    • 确保挂载点的主机目录具有足够的读写权限,以便容器能够正常操作。
  • 检查和设置权限:
    • 挂载点目录应设置为对 Docker 容器进程用户(通常是 root 或特定用户)可写。可以通过以下命令检查并设置权限:
      # 检查权限
      ls -ld /var/lib/buildkit
      
      # 设置合适的权限
      chmod 755 /var/lib/buildkit
      chown root:root /var/lib/buildkit
    • 如果使用非特权容器,则需要根据容器中的用户/组 ID(通常通过 -u 参数或容器镜像定义的用户)调整权限。

5. 主机上的目录不可被其他进程占用

  • 要求:
    • 确保挂载点没有其他进程占用。例如,挂载点路径不应是系统的关键目录(如 /etc/var/lib/docker 等)。
  • 原因:
    • 挂载会隐藏目录中已有的文件内容。如果目标挂载点被其他进程使用,例如作为系统服务的配置或运行目录,可能导致服务异常。
  • 建议:
    • 使用独立的目录路径,专门用于 BuildKit 的存储,例如 /var/lib/buildkit

6. 挂载的文件系统性能

  • 要求:
    • 挂载点所依赖的底层存储应具有较好的读写性能,尤其对于频繁构建任务。
  • 原因:
    • BuildKit 本地执行后端会频繁地进行文件读写和快照操作。如果存储性能较差,可能显著拖慢构建任务。
  • 建议:
    • 使用本地 SSD 或性能较高的硬盘。如果使用远程存储(例如 NFS 挂载),建议优化网络性能和挂载参数。

7. 挂载点路径的持久性

  • 要求:
    • 挂载点路径应设计为持久存储,避免系统重启或清理操作时数据丢失。
  • 建议:
    • 将挂载点设置在专门的持久化目录(如 /var/lib/buildkit/mnt/data/buildkit 等)。

    • 如果使用 Docker Compose 等工具启动 BuildKit 容器,可以在 docker-compose.yml 文件中指定相对的挂载路径:

      version: "3.8"
      services:
        buildkitd:
          image: moby/buildkit
          privileged: true
          volumes:
            - /var/lib/buildkit:/var/lib/buildkit

8. 避免共享目录冲突

  • 要求:
    • 如果多个 BuildKit 实例运行在同一台主机上,确保每个实例挂载的目录是独立的,避免数据冲突。
  • 建议:
    • 为每个 BuildKit 实例设置单独的目录,比如:
      mkdir -p /var/lib/buildkit/instance1
      mkdir -p /var/lib/buildkit/instance2

总结

在挂载 BuildKit 容器的存储目录到主机时,目标挂载点需要满足以下关键要求:

  1. 空目录且磁盘空间充足;
  2. 主机文件系统支持 POSIX 语义,推荐使用 ext4xfs
  3. 目录权限合适(对容器内用户可读写);
  4. 避免关键系统路径,选择不被其他进程占用的目录;
  5. 优化 I/O 性能,优先选择本地 SSD 或性能较高的存储。

合理选择和管理挂载路径能避免文件系统相关错误、提高构建效率,并确保数据的稳定性和可维护性。

用户:

在上述例子中,为什么buildkitd容器需要特权?

ChatGPT:

在上面的例子中,buildkitd 容器需要特权模式 (--privileged) 是因为它使用了一些底层操作系统功能,特别是在支持 OverlayFS 和类似文件系统相关操作的环境下。这些功能通常需要对内核级别的权限进行直接访问,而这些操作在默认的 Docker 容器中可能会被限制。以下是具体原因和细节:


1. BuildKit 使用 OverlayFS

  • 背景:
    • BuildKit 默认使用 OverlayFS 或其他类似的文件系统作为存储驱动(称为 snapshotter)。OverlayFS 是一种联合文件系统,用于支持高效的写时复制(Copy-on-Write, CoW)特性,能够显著优化镜像构建效率。
  • 限制:
    • 在非特权模式下,容器通常无法直接使用 OverlayFS,因为 OverlayFS 是一种内核功能,其挂载和操作需要更高的权限。
  • 原因需要特权:
    • 在特权模式下,容器被赋予更高的权限,几乎等同于主机的 root 权限,允许 BuildKit 容器直接调用 OverlayFS 的挂载和管理操作。

2. 文件系统的底层操作访问

  • 背景:
    • BuildKit 的 snapshotter(快照工具)在管理文件系统时,可能需要直接访问底层的文件系统特性,例如管理白化文件(Whiteout Files,用于删除 OverlayFS 中的文件)或其他高级操作。
  • 限制:
    • 一些高级文件系统操作(如挂载、卸载)通常被限制在非特权容器中,只有特权容器可以直接调用这些功能。
  • 原因需要特权:
    • 特权模式允许容器跨越 Docker 的限制,直接执行文件系统级别的操作。

3. 需要访问宿主机的 cgroup 和 namespace

  • 背景:
    • BuildKit 在运行构建任务时,可能会对资源进行隔离或限制,包括管理 CPU、内存的 cgroup 或使用特定的 Linux namespace(如网络或 pid namespace)来隔离构建进程。
  • 限制:
    • 非特权容器通常无法完全管理 cgroup 或 namespace,尤其在现代 Linux 内核中有更严格的权限检查。
  • 原因需要特权:
    • 特权模式解除这些限制,允许 BuildKit 容器创建、修改和管理所需的 cgroup 配置及其他 namespace。

4. 绑定特定的设备或挂载

  • 背景:
    • BuildKit 在工作中可能需要挂载特定的文件或者设备(如 /proc/sys/fs/cgroup 等宿主机资源),或者对挂载点进行修改以支持更多构建特性。
  • 限制:
    • 一些敏感路径(如 /sys/proc)的挂载和写入权通常被 Docker 沙盒严格限制。
  • 原因需要特权:
    • 在特权模式下,BuildKit 容器被赋予与主机相似的权限,可以直接修改或挂载这些路径。

5. 支持 Docker-in-Docker(DinD)以及用户构建模式

  • 背景:
    • BuildKit 提供支持 Docker-in-Docker(DinD)的功能,这种模式允许容器内运行 Docker 守护进程(dockerd),以便在构建阶段直接与 Docker API 交互,甚至构建出完整的容器镜像。
    • 这需要 BuildKit 容器对主机的 Docker 套接字(/var/run/docker.sock)或者底层存储进行完全的读写访问。
  • 限制:
    • 非特权容器一般情况下无法对 Docker 守护进程以及其存储目录进行直接完整的控制。
  • 原因需要特权:
    • 特权模式可以解除所有宿主系统相关限制,使 BuildKit 容器可以自由操控与 Docker 相关的资源。

6. 建造的复杂性和灵活性需求

  • 背景:
    • BuildKit 是一个高性能的构建工具,支持复杂的构建逻辑,例如分布式构建模式、自定义后端/快照工具等。这些功能可能需要对底层资源(如网络设备、挂载点、文件系统、系统能力等)进行更高权限的访问。
  • 限制:
    • 非特权容器无法授予这些额外能力,可能会导致 BuildKit 功能受限或构建失败。
  • 原因需要特权:
    • 启用特权模式可以确保 BuildKit 在各种场景下都能正常运行,避免系统权限限制导致的问题。

7. 改善 I/O 性能

  • 背景:
    • 在磁盘或文件系统相关的操作中,BuildKit 可能会启用一些特殊的 Linux I/O 特性,例如直接 I/O(Direct I/O)以提升构建性能。
  • 限制:
    • 非特权容器可能对这些 I/O 特性施加限制,影响构建效率。
  • 原因需要特权:
    • 特权模式允许 BuildKit 容器完全使用主机的 I/O 特性,从而提高文件系统操作的性能。

为什么不能直接去掉特权模式?

在某些场景下,不使用特权模式可以通过写入更多精细化的权限设置(如 --cap-add 授予特定功能),但仍存在以下挑战:

  1. 一些底层功能(如 OverlayFS 挂载)即便开启 --cap-add,依然需要特权模式。
  2. 精细权限的配置通常很难完全覆盖所有 BuildKit 可能需要的功能,特定功能丢失可能导致构建失败。
  3. 特权模式的使用是为了确保 BuildKit 的最大兼容性和最小配置复杂性。

如何运行非特权 BuildKit 容器?

虽然特权模式简化了配置工作,但如果出于安全性考虑(例如限制容器对宿主机的访问),可以尝试运行非特权 BuildKit 容器,以下是可能的步骤:

  1. 明确授予必要的权限:
    • 使用 --cap-add 授予运行 BuildKit 所需的特定权限,例如:
      docker run --rm \
        --cap-add SYS_ADMIN --cap-add MKNOD \
        moby/buildkit
  2. 避免直接调用 OverlayFS:
    • fuse-overlayfs 这样的用户态实现可能可以替代 OverlayFS,而无需特权模式。
  3. 使用 rootless 模式:
    • 设置 rootless BuildKit,完全避开系统级权限。
    • 这需要主机启用用户命名空间(user namespaces),并按需调整配置。

然而,这种非特权模式的配置复杂性较高,且可能牺牲性能或功能。


总结

  • 在默认情况下,buildkitd 容器需要特权模式是因为它需要对底层系统进行较高权限操作,包括文件系统、cgroup 和 namespace 操作等。
  • 如果特权模式带来的安全风险不是主要问题,可以直接使用特权模式来提高兼容性和性能。
  • 如果有更强的安全需求,可以尝试 rootless 模式,但实现难度和维护成本可能会增加。

用户:

如果使用本地构建后端来在容器内构建镜像(工作目录挂载到主机目录),是否不需要特权?

ChatGPT:

如果使用 本地构建后端(即 buildkitdlocal 构建后端),并将 工作目录挂载到主机目录,你可能能够不使用特权模式运行 BuildKit 容器,但这取决于特定配置和操作方式。让我们分步骤分析这种方案的可行性和限制:


1. 使用本地构建后端的特点

  • 本地构建后端(local)的作用是直接在一个挂载的本地目录中完成构建过程,而不需要 OverlayFS 等文件系统的底层快照功能。
    • 优势:避免了使用 OverlayFS 或其他依赖内核底层机制的存储后端。
    • 影响:绝大部分文件读写操作都会映射到挂载的主机目录。

2. 减少特权需求的可能性

因为文件的读写直接发生在宿主机挂载的目录中,而非依赖存储层(如 OverlayFS 或 snapshotter),这意味着以下权限可能不再需要:

  • 对 OverlayFS 的访问权限:构建过程中不会挂载文件系统,也无需管理写时复制(CoW)。
  • 高级特权(如挂载 namespace 或 SYS_ADMIN 能力):这些用于快照存储管理的功能会被替代为对目标目录的直接读写。

因此,在这种模式下,可以避免使用特权模式,同时保留镜像构建的核心功能。


3. 注意事项

即使不需要特权模式,仍需解决一些潜在问题:

(1) 工作目录的挂载

  • 需要挂载本地工作目录到容器内,例如通过以下方式:
    docker run \
      -v $(pwd):/workspace \
      moby/buildkit
    • $(pwd) 是你的构建上下文所在路径(工作目录)。
    • /workspace 是容器内的路径。

(2) 构建上下文和输出路径的访问权限

  • 挂载的目录应具有正确的读写权限,否则将导致构建过程失败:
    • 容器的用户(通常是 root 或非特权用户管理的 UID)需要在宿主机的挂载目录中拥有足够权限。
    • 使用 --user 参数可以映射容器用户到宿主用户,避免权限问题。例如:
      docker run \
        -v $(pwd):/workspace \
        --user $(id -u):$(id -g) \
        moby/buildkit

(3) 文件系统隔离问题

  • 在非特权模式下,BuildKit 的构建缓存可能受到文件系统隔离的限制(如无法创建 OverlayFS 快照)。
    • 通过显式配置 --output 将构建的镜像直接输出到本地文件系统,避免复杂操作。

(4) 使用 BuildKit 的后端能力

  • 如果以 --output type=local 将构建结果输出至挂载目录,则可以规避对 Docker 守护进程或远程注册表的依赖。
    • 示例命令:
      buildctl build \
        --frontend dockerfile.v0 \
        --local context=/workspace \
        --local dockerfile=/workspace \
        --output type=local,dest=/workspace/output

(5) Docker 套接字依赖

  • 如果需要与 Docker 守护进程交互(例如将构建结果推送到 Docker 自身),通常会通过挂载 /var/run/docker.sock 来实现。
    • 即使用:
      -v /var/run/docker.sock:/var/run/docker.sock
    • 但这本质上是授予了容器能力去操作宿主机的 Docker 守护进程,仍可能带来一定的安全隐患,即便容器本身运行得不是特权模式。

4. 不需要特权模式的构建方式

以下是实现无特权构建的具体步骤:

(1) 依赖本地后端

  • 使用本地构建后端 (--output type=local)。
  • 避免复杂的存储快照操作:确保构建所有中间缓存和输出直接写入本地挂载的路径。

(2) 运行非特权容器

只需要授予最低限度的权限,例如挂载主机工作目录并设置适当的用户映射:

docker run \
  --rm \
  -v $(pwd):/workspace \
  --user $(id -u):$(id -g) \
  moby/buildkit

(3) 避免 Docker-in-Docker

  • 避免使用 Docker-in-Docker 或依赖 /var/run/docker.sock,直接输出构建产物即可。

(4) 使用 rootless BuildKit 容器

  • 如果安全性至关重要,可以以 rootless 模式运行 BuildKit(需要主机支持用户命名空间)。
  • 示例:
    docker run --rm \
      --user $(id -u):$(id -g) \
      -v $(pwd):/workspace \
      moby/buildkit-rootless

(5) 显式管理输出

将构建的镜像输出到指定路径,并手动加载到 Docker 或推送到镜像仓库,而不是直接交由 Docker 处理:

buildctl build \
  --frontend dockerfile.v0 \
  --local context=/workspace \
  --output type=tar,dest=/workspace/output.tar

然后可以在主机上通过 docker load 导入:

docker load < /workspace/output.tar

5. 总结

在容器内使用 本地构建后端 时,可以规避对底层文件系统的复杂操作,从而不需要特权模式。但实现无特权构建的前提是:

  1. 使用本地挂载的工作目录作为上下文(--local context);
  2. 避免使用快照器(如 OverlayFS)和 Docker-in-Docker 功能;
  3. 正确配置容器的读写权限。

虽然可以不需要特权模式,但如果你的构建流程仍依赖某些特定功能(如直接操作 Docker 守护进程或复杂的缓存快照),特权模式可能仍然需要被启用。

用户:

buildctl build
--frontend dockerfile.v0
--local context=/workspace
--output type=tar,dest=/workspace/output.tar 在使用上述配置构建镜像时,能够让buildkitd在构建出镜像后将其推送到指定的registry吗?buildkitd是如何实现的?

ChatGPT:

是的,BuildKit 支持在构建完成后直接将镜像推送到指定的容器镜像仓库(registry),而且你不需要首先以 tar 文件的形式输出镜像再手动上传,这一过程可以通过 buildctl build 命令的 output 设置来实现。这得益于 BuildKit 的多后端架构和支持的直接镜像操作能力。

以下是相关详细解答:


1. 如何直接推送镜像到指定 registry

你可以通过 --outputtype=image 参数配置,以构建后直接将镜像上传到指定的容器镜像仓库。例如:

buildctl build \
  --frontend dockerfile.v0 \
  --local context=/workspace \
  --local dockerfile=/workspace \
  --output type=image,name=registry.example.com/my-image:tag,push=true

重要参数:

  • type=image: 指定构建输出的类型为容器镜像。
  • name=<registry>/<repository>:<tag>: 指定镜像推送的目标镜像仓库地址和标签(通常遵循 <域名>/<组织>/<镜像名>:<标签> 的格式)。
  • push=true: 表示构建完成后会直接将镜像推送到远程镜像仓库。

核心工作原理:

  1. BuildKitd 的镜像存储后端:BuildKit 会在内部以支持 OCI(Open Container Initiative)镜像规范的方式处理镜像,例如管理镜像的层(layer)和元信息(如配置、清单)。
  2. 直接上传到 registry:当设置 push=true 时,BuildKit 会通过其内置的 Docker registry 客户端,基于镜像仓库的 HTTP API(通常是 v2 API)直接进行上传,而无需借助 Docker 守护进程。

2. BuildKit 的实现方式

BuildKit 的推送操作是通过内部机制直接与镜像仓库交互完成的,整个过程不依赖于 Docker 守护进程。以下是主要的实现细节:

(1) 中间 OCI 镜像存储

  • BuildKit 构建的结果是符合 OCI 镜像规范的镜像。
  • 这些镜像在构建过程中以镜像层和元数据的形式被暂存于 BuildKit 的文件系统或分布式存储中。

(2) 推送到镜像仓库

  • BuildKit 内置了与容器 registry 的直接交互能力,通过实现 Docker registry API v2 协议完成镜像的推送(无需依赖 Docker CLI 或 Docker 套接字)。
  • 每个镜像被拆解为底层 layer,并逐层上传到目标 registry,同时生成适配 registry 的元数据(如 manifest 文件)。

(3) 输出指定的目标

  • buildctl--output 参数允许用户定义构建结果的输出方式。几种典型方式包括:
    • type=tar: 输出为 tar 包,可以离线加载。
    • type=oci: 输出为一个符合 OCI 镜像规范的本地目录。
    • type=image: 输出为一个完整镜像,并直接推送到 registry。

(4) 缓存和分层上传

  • 如果目标 registry 上已经有部分镜像层(layer),BuildKit 会自动跳过这些层的上传,节省时间和网络带宽。
  • 这是通过镜像层的 内容地址可读性(content-addressable storage, CAS) 来实现的。

3. 配置远程 registry 的认证信息

BuildKit 要与远程 registry 通信时,可能需要认证信息。以下是几种常见的认证方法:

(1) 配置用户认证

可以通过登录的方式为 BuildKit 配置认证信息:

buildctl --addr tcp://buildkitd:1234 login --username <username> registry.example.com

BuildKit 会将这些认证信息存储在内部配置中,用于后续的推送。

(2) 使用 Docker 中的认证

如果 BuildKit 运行在与 Docker 共用的环境中,可以通过挂载 ~/.docker/config.json 文件,让 BuildKit 复用 Docker CLI 的认证信息:

docker login registry.example.com

然后在运行 BuildKit 容器时挂载此文件:

docker run \
  -v $HOME/.docker:/root/.docker \
  moby/buildkitd

(3) 镜像仓库的匿名操作

一些公共镜像仓库支持匿名上传(例如根据仓库策略),在这种情况下可以省略身份认证。


4. 示例:结合 BuildKit 和推送 registry 的流程

设想你有一个包含以下内容的 Dockerfile

# Dockerfile
FROM alpine:latest
RUN echo "Hello, BuildKit!" > /hello

执行以下命令,直接将镜像推送到镜像仓库 registry.example.com 中:

buildctl build \
  --frontend dockerfile.v0 \
  --local context=/path/to/code \
  --local dockerfile=/path/to/code \
  --output type=image,name=registry.example.com/my-image:latest,push=true

构建完成后,该镜像会出现在镜像仓库 registry.example.com/my-image:latest 中,且任何支持 Docker registry v2 的工具都可以拉取和运行此镜像。


5. 推送模式的优势

直接将镜像推送到 registry 的模式有以下优点:

  1. 省略中间步骤:无需先将镜像保存为 tar 文件后手动上传。
  2. 高效的镜像分层复用:BuildKit 可以跳过已存在的镜像层,大幅减少网络传输量和时间。
  3. 完全独立于 Docker 守护进程:在支持的环境下,BuildKit 完全不需要 docker.sock 或本地 Docker 实例。

总结

通过 BuildKit 的 --output type=image,push=true 配置,可以在构建完成后,直接将镜像推送到指定镜像仓库。BuildKit 通过内置的 Docker registry 客户端与目标 registry 交互,而不需要 Docker 守护进程或复杂的外部依赖。因此,它特别适合在 CI/CD pipeline 中实现轻量、高效的构建与推送流程。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment