一、构建往事
在云计算的发展历程中,第一代正式意义上的代表性 PaaS 平台有 Heroku、VMware Cloud Foundry 等。这些 PaaS 平台都采用了 buildpack 技术构建用户应用,虽然细节不尽相同,但本质上都是平台方提前为各类运行时以及框架提供构建流水线支持,用户只需提交代码即可获得新版本应用,随后便可通过平台 API 进行部署。
二、云原生构建的技术背景
在 Kubernetes 环境下,我们怎样才能做到和以往 PaaS 平台一致,甚至更好的应用构建体验呢?区别于传统 PaaS,在 Kubernetes 中交付的应用统一都是 OCI 镜像格式,这使得它能够轻松支持任意语言和框架。但同时不能忽略的是,大部分开发者并不擅长制作高效且安全的镜像,所以平台方有义务提供镜像的最佳构建实践以及相关的自动化构建措施。
此时,从 Cloud Foundry 延伸而来、基于镜像的 buildpack 技术便应运而生。同时,Cloud Foundry buildpacks 团队为了使用户能从传统环境顺利过渡到 Kubernetes 环境,还提供了 paketo-buildpacks 项目。
三、buildpack 技术
应用镜像构建,即从「源代码/可执行程序及其运行时」(后文简称「源目标」)配合基础镜像构建成应用镜像的过程。
在传统条件下,需要准备源目标并编写 Dockerfile 文件,使用 docker build 命令为应用镜像堆叠镜像层,使之形成目标镜像。
为了扩大用户面,我们还需要照顾没有编写 Dockerfile 能力的用户群,为他们构建标准的镜像。下面将调研开源项目 buildpacks 是如何解决这个问题的。
3.1 buildpacks 简要介绍
buildpacks 是一款云原生镜像构建(CNB)技术,它主要通过 builder 自动识别源目标并进行目标镜像的构建。builder 主要由构建包(buildpack)和堆栈(stack)组成。
如果把构建镜像的过程比作烹饪一道菜肴,那么 buildpacks 就像一位经验丰富的大厨:你只需提供食材(源代码),大厨便会根据食材类型自动选择合适的烹饪方式(构建逻辑),最终端出一道色香味俱全的佳肴(OCI 镜像)。而这背后,是一套精密协作的系统在工作。
四、CNB 规范核心概念
要深入理解 buildpacks,需要先掌握几个核心概念:Lifecycle、Buildpack、Builder 和 Stack。它们之间的关系可以用下图来描述:
4.1 Lifecycle:构建生命周期的编排者
Lifecycle 是 CNB 规范的核心组件,它定义了从源代码到最终镜像的完整构建流程。可以把 Lifecycle 理解为一个精密的流水线管理系统,它负责协调各个 buildpack 按顺序执行,确保构建过程有条不紊。
Lifecycle 包含以下几个关键阶段:
| 阶段 | 职责 | 说明 |
|---|---|---|
| Detect | 检测阶段 | 遍历所有 buildpack,找出能够处理当前源代码的那些 |
| Analyze | 分析阶段 | 分析之前的构建缓存,加速本次构建 |
| Restore | 恢复阶段 | 恢复之前构建的缓存层 |
| Build | 构建阶段 | 执行 buildpack 的构建逻辑,编译应用、安装依赖 |
| Export | 导出阶段 | 将构建产物打包成 OCI 镜像 |
Lifecycle 的工作流程如下:
# 简化的 Lifecycle 执行流程1. /cnb/lifecycle/detector # 检测哪些 buildpack 适用2. /cnb/lifecycle/analyzer # 分析镜像层,准备缓存3. /cnb/lifecycle/restorer # 恢复缓存层4. /cnb/lifecycle/builder # 执行构建5. /cnb/lifecycle/exporter # 导出最终镜像这种分阶段的设计有几个显著优势:
- 缓存友好:通过 Analyze 和 Restore 阶段,可以复用之前构建的中间产物,大幅加速增量构建
- 可观测性强:每个阶段都有明确的输入输出,便于排查构建问题
- 可扩展性好:构建流程可以灵活插入自定义阶段
4.2 Buildpack:构建能力的原子单位
Buildpack 是 CNB 生态中最基础的构建单元,每个 buildpack 专注于特定语言或框架的构建逻辑。比如有针对 Java 的 buildpack、针对 Node.js 的 buildpack、针对 Python 的 buildpack 等。
一个标准的 buildpack 必须包含两个核心脚本:
my-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"EOFBuildpack 的执行遵循「检测→构建」的两步模式。这种设计看似简单,实则巧妙:它让构建过程可以灵活组合,就像搭积木一样。一个复杂的应用可能需要多个 buildpack 协同工作——比如一个 Node.js 应用可能需要「Node.js buildpack + Nginx buildpack」组合来完成前后端的构建。
4.3 Stack:构建和运行的双镜像架构
Stack 是 CNB 中一个独特而重要的概念,它定义了构建环境和运行环境的镜像基础。Stack 包含两个镜像:
Build Image(构建镜像):提供编译和打包所需的完整工具链。比如 Java 构建镜像会包含 JDK、Maven/Gradle、Git 等工具;Node.js 构建镜像会包含 Node.js、npm/yarn 等。
Run Image(运行镜像):提供应用运行所需的最小化环境。这是一个「瘦身」后的镜像,只包含应用运行必需的组件,不包含构建工具,从而保证生产环境镜像的精简和安全。
两个镜像之间的关系可以通过下面的 Dockerfile 概念来理解:
# 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 \ # ... 只安装运行时必需的包这种双镜像架构带来了几个关键好处:
- 安全最小化:运行镜像不包含构建工具,攻击面大幅减小
- 镜像体积优化:生产镜像可以做到极致精简
- 构建环境隔离:构建时的复杂依赖不会污染运行环境
- 缓存效率:两个镜像共享基础层,提高拉取和构建效率
4.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"构建一个自定义 Builder 的过程如下:
# 使用 pack CLI 构建 builderpack builder create my-builder \ --config builder.toml \ --path ./
# 查看构建好的 builder 内容pack builder inspect my-builderBuilder 的构建流程可以简化为下图:
五、构建流程详解
当用户使用 pack build 命令构建应用时,完整的构建流程如下:
5.1 Layer 缓存机制
CNB 的一个重要特性是其精细的 Layer 缓存机制。每个 buildpack 可以声明自己的缓存策略,将构建产物划分为不同的层:
# 一个典型应用的镜像层结构├── Layer: base (来自 run image)│ ├── /bin│ ├── /lib│ └── /usr├── Layer: node-runtime (Node.js 运行时)│ └── /layers/paketo-buildpacks_nodejs/node├── Layer: npm-cache (npm 缓存)│ └── /layers/paketo-buildpacks_npm/cache├── Layer: app-dependencies (应用依赖)│ └── /workspace/node_modules└── Layer: app (应用代码) └── /workspace缓存的优势在于:
- 增量构建速度:只有变化的层需要重新构建
- 镜像共享:不同应用的相同基础层可以共享存储
- 快速回滚:历史版本可以快速恢复
六、Paketo Buildpacks
Paketo 是由 Cloud Foundry 基金会维护的一套开源 buildpacks 集合,它完全遵循 CNB 规范,提供了开箱即用的构建能力。
6.1 Paketo 的优势
相比传统 Dockerfile 方式,Paketo buildpacks 具有以下优势:
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-base2. 安全更新自动化
当基础镜像发现安全漏洞时,传统方式需要:
- 修改所有项目的 Dockerfile
- 重新构建所有项目
- 重新部署所有应用
使用 Paketo:
# 只需重新构建,buildpack 自动应用最新的安全补丁pack build my-app --builder paketobuildpacks/builder-jammy-base3. 最佳实践内置
Paketo 团队持续跟踪各种语言和框架的最佳实践:
| 最佳实践 | 传统 Dockerfile | Paketo Buildpacks |
|---|---|---|
| 多阶段构建 | 手动编写 | 自动应用 |
| 安全扫描 | 需额外配置 | 内置 SBOM |
| 最小化镜像 | 需经验判断 | 自动优化 |
| 依赖缓存 | 手动设计 | 自动分层缓存 |
| 启动命令 | 手动配置 | 自动检测 |
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" } ] }}6.2 Paketo Buildpack 家族
Paketo 提供了丰富的语言和框架支持:
6.3 Builder 变体选择
Paketo 提供了三种 Builder 变体,适用于不同场景:
| Builder | 基础镜像 | 大小 | 适用场景 |
|---|---|---|---|
builder-jammy-base | Ubuntu 22.04 | ~1GB | 通用场景,支持最多语言 |
builder-jammy-full | Ubuntu 22.04 | ~2GB | 需要完整工具链的复杂应用 |
builder-jammy-tiny | Ubuntu 22.04 | ~100MB | 最小化镜像,仅支持静态编译语言 |
选择建议:
# 大多数 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 的对比
为了更直观地理解 buildpacks 的价值,通过一个实际案例来对比两种方式。
7.1 场景:构建一个 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
- 安全更新需要重新构建基础镜像
- 没有依赖缓存机制,每次都要下载依赖
Paketo Buildpacks 方式:
# 一行命令完成构建pack build my-spring-app \ --builder paketobuildpacks/builder-jammy-base \ --env BP_JVM_VERSION=17优势:
- 无需维护 Dockerfile
- 自动检测 Gradle/Maven
- 依赖自动缓存
- JVM 参数可通过环境变量配置
- 自动生成 SBOM
- 自动配置 Spring Boot 启动参数
7.2 功能对比表
| 功能 | Dockerfile | CNB/Paketo |
|---|---|---|
| 学习曲线 | 中等 | 低 |
| 灵活性 | 高 | 中 |
| 最佳实践 | 需手动实现 | 内置 |
| 安全更新 | 手动 | 自动 |
| 缓存机制 | 手动设计 | 自动分层 |
| 多语言支持 | 需为每种语言编写 | 开箱即用 |
| SBOM 支持 | 需额外工具 | 内置 |
| 调试能力 | 直接进入容器 | 专用工具 |
| 可移植性 | 依赖 Docker | OCI 兼容 |
7.3 何时选择哪种方式
选择 Dockerfile 的场景:
- 需要极度定制化的构建流程
- 现有 buildpack 不支持的技术栈
- 需要复杂的构建参数控制
- 团队已熟悉 Dockerfile 最佳实践
选择 CNB/Paketo 的场景:
- 标准化的微服务架构
- 多语言项目统一构建
- 需要 SBOM 和供应链安全
- 开发团队不熟悉容器最佳实践
- CI/CD 流水线需要简化
八、实战:完整构建流程
下面通过一个完整的 Node.js 应用示例,演示 CNB 的构建流程。
8.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 version8.2 准备示例应用
# 创建示例 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}`);});EOF8.3 执行构建
# 使用 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'8.4 运行和验证
# 运行构建好的镜像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-app8.5 配置自定义参数
CNB 支持通过环境变量配置构建行为:
# 指定 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 | 指定启动入口 |
8.6 与 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 会自动监听代码仓库变化,触发增量构建,实现真正的「推送即部署」体验。
九、最佳实践
9.1 Builder 版本管理
# 使用固定版本的 builderpack build my-app \ --builder paketobuildpacks/builder-jammy-base:0.4.0
# 而不是 latestpack build my-app \ --builder paketobuildpacks/builder-jammy-base:latest # 不推荐9.2 利用缓存加速构建
# 启用卷缓存(本地开发)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-app9.3 多环境配置
# 开发环境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=true9.4 安全最佳实践
# 扫描镜像漏洞pack build my-app \ --builder paketobuildpacks/builder-jammy-base \ --run-image paketobuildpacks/run-jammy-base:latest
# 使用最小化 run imagepack build my-app \ --builder paketobuildpacks/builder-jammy-tiny十、总结
Cloud Native Buildpacks 代表了容器构建技术的一次重要演进。它将传统 PaaS 平台的便捷体验带入了 Kubernetes 时代,通过以下核心价值点解决了开发者的痛点:
- 开发者友好:无需编写 Dockerfile,提交代码即可构建
- 安全合规:自动生成 SBOM,支持供应链安全审计
- 最佳实践内置:多阶段构建、缓存优化、最小化镜像开箱即用
- 生态丰富:Paketo 提供了主流语言和框架的完整支持
- 标准化:遵循 CNB 规范,不同实现之间兼容互通
对于云原生平台团队而言,CNB 提供了一种标准化的构建抽象,可以大幅降低开发者使用容器技术的门槛,同时确保企业级的安全和合规要求。对于个人开发者而言,Paketo 等开箱即用的解决方案可以让你专注于业务代码,而不必深究容器最佳实践。
当然,CNB 并非银弹。对于有特殊构建需求的项目,Dockerfile 仍然提供了最灵活的控制能力。在实际项目中,可以根据团队的技术栈、运维能力和业务需求,选择最合适的构建方式。
十一、参考资料
- heroku buildpack
- cloudfoundry buildpack
- vmware-tanzu buildpack
- kubesphere s2i
- Heroku 的「得」与「失」
- Cloud Foundry 基金会宣布为云原生开发人员和运营商推出 Paketo Buildpacks
- CNB 官方文档
- Paketo 官方文档
- pack CLI 文档
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






