一、构建往事:从 PaaS 到 Kubernetes
第一代代表性 PaaS 平台,Heroku、VMware Cloud Foundry,都采用了 buildpack 技术构建用户应用。平台方提前为各类运行时和框架提供构建流水线,用户只需提交代码即可获得新版本应用,再通过平台 API 部署。这套模式的核心假设是:构建逻辑应该由平台统一提供,而非每个开发者各自编写。
Kubernetes 改变了交付格式,所有应用统一为 OCI 镜像,但构建问题并没有因此消失,反而更突出了。OCI 镜像的灵活性意味着任何语言和框架都能跑,但大部分开发者并不擅长制作高效且安全的镜像。Dockerfile 把构建知识推给了每个开发者,这恰恰是 PaaS 时代 buildpack 试图消除的负担。
从 Cloud Foundry 延伸而来的 Cloud Native Buildpacks(CNB)项目,正是要把 buildpack 的理念重新嫁接到 OCI 镜像的世界里。同期,Cloud Foundry buildpacks 团队推出了 paketo-buildpacks 项目,帮助用户从传统环境过渡到 Kubernetes。
二、buildpack 的核心思路
应用镜像构建,即从「源代码/可执行程序及其运行时」(后文简称「源目标」)配合基础镜像构建成应用镜像的过程。传统方式需要编写 Dockerfile,用 docker build 命令堆叠镜像层。Dockerfile 的问题不在于它不够灵活,恰恰相反,它太灵活了。每个团队各自摸索多阶段构建、缓存策略、安全加固,重复造轮子且质量参差不齐。
buildpacks 的思路是反转这个责任归属:构建逻辑由平台提供,开发者只提供源代码。builder 自动检测语言类型、选择构建逻辑、输出 OCI 镜像。开发者不需要知道什么是多阶段构建,不需要知道 npm ci 和 npm install 的区别,不需要知道 run image 该不该装 curl,这些决策由 buildpack 的维护者代为做出。
这个代决策是有代价的:开发者让渡了对构建流程的完全控制权。后文会讨论这个取舍的边界。
三、CNB 规范核心概念
buildpacks 的运作依赖四个概念:Lifecycle、Buildpack、Builder 和 Stack。它们之间的关系如下图:
3.1 Lifecycle:构建流水线的编排者
Lifecycle 是 CNB 规范的核心组件,它定义了从源代码到最终镜像的完整构建流程。它不是某个 buildpack 的执行器,而是一个独立的协调层,决定哪些 buildpack 参与构建、管理缓存、导出镜像。这种分离意味着 Lifecycle 的实现可以独立于 buildpack 迭代,buildpack 的维护者也不需要关心镜像导出的底层细节。
Lifecycle 包含五个阶段:
| 阶段 | 职责 | 说明 |
|---|---|---|
| Detect | 检测阶段 | 遍历所有 buildpack,找出能够处理当前源代码的那些 |
| Analyze | 分析阶段 | 分析之前的构建缓存,加速本次构建 |
| Restore | 恢复阶段 | 恢复之前构建的缓存层 |
| Build | 构建阶段 | 执行 buildpack 的构建逻辑,编译应用、安装依赖 |
| Export | 导出阶段 | 将构建产物打包成 OCI 镜像 |
# 简化的 Lifecycle 执行流程1. /cnb/lifecycle/detector # 检测哪些 buildpack 适用2. /cnb/lifecycle/analyzer # 分析镜像层,准备缓存3. /cnb/lifecycle/restorer # 恢复缓存层4. /cnb/lifecycle/builder # 执行构建5. /cnb/lifecycle/exporter # 导出最终镜像分阶段设计的核心收益是缓存友好。Analyze 和 Restore 阶段专门处理缓存复用,增量构建时只有变化的层需要重新构建。这比 Dockerfile 的 --cache-from 更精细,Dockerfile 的缓存以指令为单位,一条指令失效则后续所有指令的缓存全部作废;CNB 的缓存以 buildpack 的 layer 为单位,一个 buildpack 的缓存失效不影响其他 buildpack。此外,每个阶段有明确的输入输出,构建失败时可以快速定位是哪个阶段出了问题。
3.2 Buildpack:构建能力的原子单位
Buildpack 是 CNB 生态中最基础的构建单元,每个 buildpack 专注于特定语言或框架的构建逻辑。比如有针对 Java 的 buildpack、针对 Node.js 的 buildpack、针对 Python 的 buildpack 等。
一个标准的 buildpack 必须包含两个核心脚本:
bin
detect
build
buildpack.toml
detect 脚本的作用是「自荐」,告诉 Lifecycle「我能处理这类源代码」。它的逻辑通常是检查项目特征文件:
#!/bin/bash# bin/detect 示例
# 检查是否存在 package.json,判断是否为 Node.js 项目if [ -f "package.json" ]; then echo "nodejs" # 输出 plan 名称 exit 0fi
# 如果不能处理,返回非零退出码exit 1build 脚本则是「兑现承诺」,实际执行构建工作:
#!/bin/bash# bin/build 示例
# 设置环境export PATH="/cnb/process:$PATH"
# 安装依赖npm install --production
# 设置启动命令cat > launch.toml <<EOF[[processes]]type = "web"command = "npm start"EOF「检测→构建」的两步模式看似简单,实则解决了一个组合问题:一个复杂的应用可能需要多个 buildpack 协同工作。比如一个 Node.js 应用可能需要「Node.js buildpack + Nginx buildpack」组合来完成前后端的构建。detect 阶段让 Lifecycle 知道哪些 buildpack 需要参与,build 阶段再按顺序执行,这比 Dockerfile 里把所有构建逻辑塞进一个文件要模块化得多。每个 buildpack 可以独立版本化、独立更新,不需要修改应用代码。
3.3 Stack:构建和运行的双镜像架构
Stack 定义了构建环境和运行环境的镜像基础,包含两个镜像:
Build Image 提供编译和打包所需的完整工具链。Java 构建镜像包含 JDK、Maven/Gradle、Git;Node.js 构建镜像包含 Node.js、npm/yarn。Run Image 只提供应用运行所需的最小化环境,不包含构建工具。
# Build Image 的基础FROM ubuntu:22.04 AS build-imageRUN apt-get update && apt-get install -y \ build-essential \ curl \ git \ # ... 更多构建工具ENV CNB_USER_ID=1000ENV CNB_GROUP_ID=1000
# Run Image 的基础(通常共享相同的基础镜像)FROM ubuntu:22.04 AS run-imageRUN apt-get update && apt-get install -y \ ca-certificates \ # ... 只安装运行时必需的包双镜像架构的本质是最小权限原则在镜像层面的体现。传统 Dockerfile 的多阶段构建也在做类似的事,用 AS builder 阶段编译,再 COPY --from=builder 到精简的运行镜像。但 Dockerfile 的多阶段构建依赖开发者自觉遵守,buildpack 把这个分离强制化了:构建工具永远不会出现在 run image 里,不是因为你记得不装,而是因为构建和运行根本不在同一个镜像里。
这种分离的代价是灵活性降低。如果你的应用在运行时需要 curl 做健康检查,你不能像 Dockerfile 那样随手加一行 RUN apk add curl,你需要自定义 run image 或使用支持健康检查的 buildpack。
3.4 Builder:构建能力的集合体
Builder 是一个集成了 Lifecycle、多个 buildpack 和 Stack 的 OCI 镜像。它把构建所需的一切打包在一起,用户只需要推送源代码进去,就能得到构建好的镜像。
Builder 的结构通过 builder.toml 配置文件定义:
# builder.toml 示例
# 构建顺序很重要:Lifecycle 会按顺序尝试检测[[buildpacks]] id = "paketo-buildpacks/nodejs" version = "1.0.0" uri = "docker://gcr.io/paketo-buildpacks/nodejs"
[[buildpacks]] id = "paketo-buildpacks/npm" version = "1.0.0" uri = "docker://gcr.io/paketo-buildpacks/npm"
[[buildpacks]] id = "paketo-buildpacks/java" version = "1.0.0" uri = "docker://gcr.io/paketo-buildpacks/java"
# 定义构建顺序和分组[[order]] [[order.group]] id = "paketo-buildpacks/nodejs"
[[order]] [[order.group]] id = "paketo-buildpacks/java"
# 指定 Stack[stack] id = "io.buildpacks.stacks.jammy" build-image = "paketobuildpacks/build-jammy-base" run-image = "paketobuildpacks/run-jammy-base"# 使用 pack CLI 构建 builderpack builder create my-builder \ --config builder.toml \ --path ./
# 查看构建好的 builder 内容pack builder inspect my-builderBuilder 的设计把「构建能力」变成了一个可版本化、可分发的制品。平台团队可以维护一个标准 Builder,所有业务团队共用,当 Builder 升级(比如修复了某个 buildpack 的安全漏洞),所有应用只需重新构建即可受益,不需要逐个修改 Dockerfile。
四、构建流程详解
当用户使用 pack build 命令构建应用时,完整的构建流程如下:
4.1 Layer 缓存机制
CNB 的缓存机制比 Dockerfile 的层缓存更精细。每个 buildpack 可以声明自己的缓存策略,将构建产物划分为不同的层:
一个典型应用的镜像层结构:
bin
lib
usr
layers/paketo-buildpacks_nodejs/node
layers/paketo-buildpacks_npm/cache
workspace/node_modules
workspace
Dockerfile 的缓存以 RUN/COPY 指令为单位,一条 RUN npm install 对应一整层,只要 package.json 变了,整个 node_modules 层都要重建。CNB 的缓存以 buildpack 的 layer 为单位,且 buildpack 可以在 layer 内部做更细粒度的判断:npm-cache 层和 app-dependencies 层是分开的,依赖没变时只重建 app 层。这种分层策略让增量构建的速度显著快于 Dockerfile 方式。
五、Paketo Buildpacks
Paketo 是由 Cloud Foundry 基金会维护的一套开源 buildpacks 集合,完全遵循 CNB 规范。它的价值不在于「又多了一个 buildpack 实现」,而在于把构建最佳实践从隐性知识变成了显性制品。
5.1 零 Dockerfile 的构建体验
传统方式需要为每个项目维护 Dockerfile:
# 传统 Dockerfile 示例FROM node:18-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ci --only=productionCOPY . .RUN npm run build
FROM node:18-alpineWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesEXPOSE 3000CMD ["npm", "start"]使用 Paketo 后,只需一行命令:
# 自动检测语言、安装依赖、构建镜像pack build my-app --builder paketobuildpacks/builder-jammy-base这行命令背后做了什么?Lifecycle 检测到 package.json,选中 Node.js buildpack;buildpack 安装合适版本的 Node.js、执行 npm install、配置启动命令、生成 SBOM、应用多阶段构建,所有这些都需要 Dockerfile 作者手动处理的事情,buildpack 自动完成了。
5.2 安全更新自动化
当基础镜像发现安全漏洞时,Dockerfile 方式需要修改所有项目的 Dockerfile、重新构建、重新部署。Paketo 方式只需重新构建,buildpack 自动应用最新的安全补丁,因为安全策略集中在 Builder 里而非分散在每个 Dockerfile 中。
# 只需重新构建,buildpack 自动应用最新的安全补丁pack build my-app --builder paketobuildpacks/builder-jammy-base这种集中管理的代价是:你信任 Paketo 团队对安全补丁的判断。如果 Paketo 的 run image 更新引入了不兼容变更,你的应用可能需要调整。这和依赖 Linux 发行版的安全更新是同一种信任模型,你把安全决策委托给了上游,换来的是不必自己跟踪每个 CVE。
5.3 最佳实践内置
Paketo 团队持续跟踪各种语言和框架的最佳实践:
| 最佳实践 | 传统 Dockerfile | Paketo Buildpacks |
|---|---|---|
| 多阶段构建 | 手动编写 | 自动应用 |
| 安全扫描 | 需额外配置 | 内置 SBOM |
| 最小化镜像 | 需经验判断 | 自动优化 |
| 依赖缓存 | 手动设计 | 自动分层缓存 |
| 启动命令 | 手动配置 | 自动检测 |
5.4 软件物料清单(SBOM)
Paketo 自动生成 SBOM,满足供应链安全合规要求:
# 构建时自动生成 SBOMpack build my-app --builder paketobuildpacks/builder-jammy-base
# 查看镜像中的 SBOMpack inspect my-app --sbomSBOM 输出示例:
{ "sbom": { "spdxid": "SPDXRef-DOCUMENT", "documentNamespace": "https://paketo.io/sbom/my-app", "packages": [ { "name": "node", "version": "18.17.0", "license": "MIT" }, { "name": "npm", "version": "9.6.7", "license": "Artistic-2.0" } ] }}SBOM 在 Dockerfile 方式下需要额外工具(如 Syft、Trivy)扫描镜像才能生成,且扫描结果依赖工具对镜像层的解析能力。Paketo 的 SBOM 是构建时原生生成的,准确性更高,buildpack 知道自己装了什么版本、什么许可证,不需要事后从文件系统反推。
5.5 Paketo Buildpack 家族
Paketo 提供了丰富的语言和框架支持:
5.6 Builder 变体选择
Paketo 提供了三种 Builder 变体,区别在于 run image 的精简程度:
| Builder | 基础镜像 | 大小 | 适用场景 |
|---|---|---|---|
builder-jammy-base | Ubuntu 22.04 | ~1GB | 通用场景,支持最多语言 |
builder-jammy-full | Ubuntu 22.04 | ~2GB | 需要完整工具链的复杂应用 |
builder-jammy-tiny | Ubuntu 22.04 | ~100MB | 最小化镜像,仅支持静态编译语言 |
tiny 变体的 run image 基于 distroless 思路,只包含 glibc 和动态链接器,不包含 shell、包管理器等。这意味着你无法 docker exec 进容器调试,也无法在运行时安装任何额外工具。选择 tiny 意味着你接受这个约束,换取更小的攻击面和更快的启动速度。
# 大多数 Web 应用pack build my-web-app --builder paketobuildpacks/builder-jammy-base
# Go、Rust 等静态编译语言(最小镜像)pack build my-cli-app --builder paketobuildpacks/builder-jammy-tiny
# 需要 .NET、PHP 等完整运行时pack build my-dotnet-app --builder paketobuildpacks/builder-jammy-full六、与 Dockerfile 的对比
通过一个 Spring Boot 应用的实际案例来对比两种方式。
传统 Dockerfile 方式:
# DockerfileFROM eclipse-temurin:17-jdk-alpine AS builderWORKDIR /appCOPY . .RUN ./gradlew build -x test
FROM eclipse-temurin:17-jre-alpineWORKDIR /appCOPY --from=builder /app/build/libs/*.jar app.jarEXPOSE 8080ENTRYPOINT ["java", "-jar", "app.jar"]docker build -t my-spring-app .docker run -p 8080:8080 my-spring-app这个 Dockerfile 看起来合理,但隐含几个问题:JVM 参数调优需要修改 Dockerfile;安全更新需要重新构建基础镜像;没有依赖缓存机制,每次都要下载依赖;COPY . . 会把不必要的文件带入构建上下文。
Paketo Buildpacks 方式:
# 一行命令完成构建pack build my-spring-app \ --builder paketobuildpacks/builder-jammy-base \ --env BP_JVM_VERSION=17Paketo 自动检测 Gradle/Maven、缓存依赖、配置 Spring Boot 启动参数、生成 SBOM。JVM 参数通过环境变量配置,不需要修改任何构建文件。
6.1 功能对比
| 功能 | Dockerfile | CNB/Paketo |
|---|---|---|
| 学习曲线 | 中等 | 低 |
| 灵活性 | 高 | 中 |
| 最佳实践 | 需手动实现 | 内置 |
| 安全更新 | 手动 | 自动 |
| 缓存机制 | 手动设计 | 自动分层 |
| 多语言支持 | 需为每种语言编写 | 开箱即用 |
| SBOM 支持 | 需额外工具 | 内置 |
| 调试能力 | 直接进入容器 | 专用工具 |
| 可移植性 | 依赖 Docker | OCI 兼容 |
6.2 何时选择哪种方式
Dockerfile 的优势在于完全控制。当构建流程有特殊需求,自定义编译参数、非标准的依赖安装方式、需要操作构建环境的系统级配置,Dockerfile 可以精确控制每一步。CNB 的 buildpack 是对常见构建模式的抽象,它覆盖了 80% 的标准场景,但那 20% 的特殊需求可能找不到现成的 buildpack,需要自己编写或回退到 Dockerfile。
具体来说,选择 Dockerfile 的场景包括:需要极度定制化的构建流程、现有 buildpack 不支持的技术栈、需要复杂的构建参数控制、团队已熟悉 Dockerfile 最佳实践且构建质量可控。选择 CNB/Paketo 的场景包括:标准化的微服务架构、多语言项目统一构建、需要 SBOM 和供应链安全、开发团队不熟悉容器最佳实践、CI/CD 流水线需要简化。
CNB 并非要取代 Dockerfile,而是提供了一种更高层的抽象。就像高级语言没有取代汇编,大部分场景用高级语言更高效,但需要精细控制时汇编仍然不可替代。
七、实战:完整构建流程
下面通过一个 Node.js 应用示例,演示 CNB 的构建流程。
7.1 环境准备与示例应用
# 安装 pack CLI# macOSbrew install buildpacks/tap/pack
# Linuxcurl -sSL https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz | tar -C /usr/local/bin --no-same-owner -xzv pack
# 验证安装pack version# 创建示例 Node.js 应用mkdir my-node-app && cd my-node-app
# package.jsoncat > package.json << 'EOF'{ "name": "my-node-app", "version": "1.0.0", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.18.2" }}EOF
# server.jscat > server.js << 'EOF'const express = require('express');const app = express();const port = process.env.PORT || 3000;
app.get('/', (req, res) => { res.json({ message: 'Hello from CNB!' });});
app.listen(port, () => { console.log(`App listening on port ${port}`);});EOF7.2 执行构建与验证
# 使用 Paketo builder 构建pack build my-node-app \ --builder paketobuildpacks/builder-jammy-base
# 输出示例:# ==> DETECTING# 5 of 18 buildpacks participating# paketo-buildpacks/ca-certificates 3.6.1# paketo-buildpacks/node-engine 1.2.3# paketo-buildpacks/npm-install 1.0.1# paketo-buildpacks/node-start 1.0.1## ==> ANALYZING# Restoring 3 layers from cache## ==> BUILDING# Installing Node.js v18.17.0# Installing npm dependencies## ==> EXPORTING# Adding layer 'paketo-buildpacks/node-engine'# Adding layer 'paketo-buildpacks/npm-install'# Writing config# Adding label 'io.buildpacks.lifecycle.metadata'# 运行构建好的镜像docker run -p 3000:3000 my-node-app
# 测试应用curl http://localhost:3000# 输出: {"message":"Hello from CNB!"}
# 查看镜像信息pack inspect my-node-app
# 查看 SBOMpack sbom my-node-app7.3 配置自定义参数
CNB 通过环境变量配置构建行为,这是 buildpack 与 Dockerfile 的一个关键交互差异,Dockerfile 把配置硬编码在文件里,buildpack 把配置外置到环境变量中:
# 指定 Node.js 版本pack build my-node-app \ --builder paketobuildpacks/builder-jammy-base \ --env BP_NODE_VERSION=20
# 设置构建时变量pack build my-node-app \ --env NPM_CONFIG_PRODUCTION=true \ --env BP_NODE_PROJECT_PATH=./backend
# 配置运行时环境变量pack build my-node-app \ --env BPE_MY_VAR=my-value常用环境变量:
| 环境变量 | 说明 |
|---|---|
BP_NODE_VERSION | 指定 Node.js 版本 |
BP_NODE_PROJECT_PATH | 指定项目路径 |
BP_JVM_VERSION | 指定 JVM 版本 |
BP_GO_VERSION | 指定 Go 版本 |
BPE_* | 运行时环境变量前缀 |
BP_LAUNCHPOINT | 指定启动入口 |
7.4 与 Kubernetes 集成
在 Kubernetes 环境中,可以使用 Tekton 或 kpack 来集成 CNB:
# kpack Image 资源示例apiVersion: kpack.io/v1alpha2kind: Imagemetadata: name: my-node-appspec: tag: registry.example.com/my-node-app builder: name: my-builder kind: ClusterBuilder source: git: url: https://github.com/myorg/my-node-app revision: mainkpack 会自动监听代码仓库变化和 Builder 镜像更新,触发增量构建。这意味着当 Paketo 发布了修复安全漏洞的新版 Builder 时,kpack 会自动用新 Builder 重新构建所有应用,这正是 buildpack 集中管理安全策略的价值在 Kubernetes 上的体现。
八、最佳实践
8.1 Builder 版本管理与缓存策略
Builder 镜像应该固定版本,避免 latest 标签带来的不可预测性:
# 使用固定版本的 builderpack build my-app \ --builder paketobuildpacks/builder-jammy-base:0.4.0
# 而不是 latestpack build my-app \ --builder paketobuildpacks/builder-jammy-base:latest # 不推荐缓存策略需要区分本地开发和 CI 环境。本地开发用卷缓存,CI 环境用镜像缓存,因为 CI 的构建节点通常是无状态的,卷缓存无法持久化:
# 启用卷缓存(本地开发)pack build my-app \ --builder paketobuildpacks/builder-jammy-base \ --volume ~/.pack/cache:/cache
# CI 环境使用镜像缓存pack build my-app \ --cache-image registry.example.com/cache:my-app8.2 多环境配置与安全
不同环境通过环境变量区分,而非维护多套构建文件:
# 开发环境pack build my-app --env BP_NODE_VERSION=18 --env NODE_ENV=development
# 生产环境pack build my-app \ --env BP_NODE_VERSION=18 \ --env NODE_ENV=production \ --env NPM_CONFIG_PRODUCTION=true安全方面,优先选择 tiny 变体减小攻击面,并定期重建以应用安全补丁:
# 使用最小化 run imagepack build my-app \ --builder paketobuildpacks/builder-jammy-tiny
# 指定特定版本的 run imagepack build my-app \ --builder paketobuildpacks/builder-jammy-base \ --run-image paketobuildpacks/run-jammy-base:1.2.3参考资料
- buildpacks.io - CNB 官方站点与规范文档
- paketo-buildpacks - Paketo 官方文档
- Tekton - 云原生 CI/CD 框架
- kpack - Kubernetes 上的 CNB 构建服务
- Heroku buildpack - buildpack 概念的起源
- Cloud Foundry buildpack - CNB 规范的前身
- pack CLI - Pack 工具源码与文档
- CNB 官方文档 - CNB 规范详细文档
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






