1. 项目概述:一个被低估的Helm Chart打包与部署利器
如果你和我一样,长期在Kubernetes生态里摸爬滚打,那你对Helm一定不会陌生。作为Kubernetes的“包管理器”,Helm Chart极大地简化了复杂应用的部署。但不知道你有没有遇到过这样的场景:CI/CD流水线里,每次构建都要重新拉取依赖、渲染模板、打包Chart,这个过程不仅慢,还充满了不确定性;或者,你想把Chart和它依赖的镜像一起打包成一个自包含的、可离线部署的“应用包”,却发现官方工具链对此支持有限。今天要聊的mgoltzsche/khelm,就是为解决这些痛点而生的一个命令行工具。它不是一个替代helm的全新工具,而是一个专注于高效、可重复、可离线的Chart渲染与打包的强力补充。
简单来说,khelm的核心价值在于,它绕过了helm命令的某些限制,提供了一种更符合现代云原生CI/CD实践和离线部署需求的Chart处理方式。它允许你将一个Chart(包括其远程依赖)渲染成纯粹的Kubernetes YAML清单,或者打包成一个包含所有依赖镜像的OCI(Open Container Initiative)镜像,实现真正的“一次构建,随处部署”。对于追求部署一致性、构建速度和安全合规的团队来说,这个工具值得深入了解一下。
2. 核心设计理念与工作原理拆解
2.1 为什么需要khelm?Helm的“阿喀琉斯之踵”
要理解khelm的价值,得先看看我们平时用helm可能遇到的麻烦。
2.1.1 构建环境的不确定性标准的helm template或helm install命令在运行时,需要访问Chart中定义的仓库来拉取依赖(dependencies)。这意味着你的CI/CD流水线必须能够访问外网,或者你必须在构建环境中预置所有依赖Chart。更头疼的是,如果远程仓库的Chart版本更新了,你两次构建生成的YAML可能会不一样,破坏了部署的一致性。khelm通过在设计上就支持将依赖锁定(类似于npm的package-lock.json或pip的requirements.txt),并在一个可控的上下文中解析所有依赖,确保了渲染结果的绝对可重复性。
2.1.2 离线部署的挑战在安全要求严格的离线环境(Air-Gapped Environment)中部署应用,你需要的不只是Chart本身,还包括Chart中所有容器镜像。传统的做法是:1)用helm pull拉取Chart,2)用helm template生成YAML,3)从YAML中手动或通过脚本提取镜像列表,4)用docker pull/docker save或skopeo等工具搬运镜像。这个过程繁琐且易错。khelm的pack命令可以直接将Chart及其所有镜像引用打包成一个OCI镜像,实现了应用资产的单一化、版本化管理。
2.1.3 对CI/CD的友好性khelm被设计成一个简单的、无状态的命令行工具,它不依赖helm的本地缓存(~/.cache/helm)或配置(~/.config/helm)。这使得它在Docker容器或Kubernetes Pod中作为一次性任务运行时非常干净,不会留下状态,也避免了不同任务之间的潜在冲突。
2.2 khelm的架构与核心工作流
khelm的核心可以概括为两个主要功能:template(模板渲染)和pack(打包)。
2.2.1template工作流当你执行khelm template时,它的内部工作流程如下:
- 解析Chart:读取
Chart.yaml,识别所有依赖项(定义在dependencies字段)。 - 依赖解析与拉取:根据
Chart.lock文件(如果存在)或Chart.yaml中的版本范围,解析出确切的依赖Chart版本。它会从配置的仓库中拉取这些依赖,这个过程可以完全离线(如果依赖已缓存到本地目录)。 - 创建渲染上下文:将所有Chart(主Chart和子Chart)加载到内存中,合并它们的值(values),形成一个完整的、扁平的配置视图。
- 执行模板渲染:使用与Helm兼容的Go模板引擎,结合提供的values文件,渲染所有模板文件,生成最终的Kubernetes资源YAML清单。
- 输出:将渲染后的YAML输出到标准输出或指定文件。
注意:
khelm template默认会进行严格的YAML验证,确保输出的资源是合法的Kubernetes API对象。这比helm template的默认行为更严格,有助于提前发现模板错误。
2.2.2pack工作流khelm pack是更具创新性的功能,它将一个Chart转换为一个符合OCI标准的容器镜像。
- 执行
template:首先,它会内部执行完整的template流程,生成最终的Kubernetes资源清单。 - 镜像清单提取:从渲染出的所有YAML资源中,递归地扫描
image字段,提取出所有容器镜像的引用(包括Init容器、Sidecar等)。 - 镜像拉取与层处理:使用containerd或类似的低级容器运行时接口,将提取到的所有镜像拉取到本地。
khelm会智能地处理这些镜像,去除冗余的层,只将独特的镜像层添加到最终的OCI包中,这类似于Docker镜像的层去重机制,可以有效减少最终包的大小。 - 构建OCI镜像:创建一个新的OCI镜像,这个镜像包含以下“层”:
- 应用层:包含渲染后的Kubernetes YAML文件(通常放在
/app目录下)。 - 镜像索引层:包含所有提取出的容器镜像的清单和层数据。
- 元数据层:包含Chart的元信息(如名称、版本)和镜像引用映射关系。
- 应用层:包含渲染后的Kubernetes YAML文件(通常放在
- 推送镜像:将构建好的OCI镜像推送到你指定的容器镜像仓库(如Docker Hub, Harbor, Quay.io, 或任何兼容OCI的仓库)。
这样产生的OCI镜像,就是一个自包含的应用包。部署时,只需要将这个镜像拉取到目标环境,使用khelm或配套工具解包,即可获得完整的、立即可用的Kubernetes清单和所有容器镜像。
3. 从零开始:khelm的安装与基础配置
3.1 多种安装方式选择
khelm是一个用Go编写的单二进制文件,安装非常灵活。
3.1.1 直接下载二进制文件(推荐)这是最快的方式。前往项目的GitHub Release页面,根据你的操作系统和架构下载对应的压缩包。例如,在Linux amd64系统上:
# 下载最新版本的khelm VERSION=$(curl -s https://api.github.com/repos/mgoltzsche/khelm/releases/latest | grep 'tag_name' | cut -d\" -f4) wget "https://github.com/mgoltzsche/khelm/releases/download/${VERSION}/khelm_${VERSION#v}_linux_amd64.tar.gz" # 解压并安装到PATH tar -xzf khelm_${VERSION#v}_linux_amd64.tar.gz sudo mv khelm /usr/local/bin/ # 验证安装 khelm version3.1.2 使用包管理器对于macOS用户,如果安装了Homebrew,可以通过Tap来安装:
brew install mgoltzsche/tap/khelm3.1.3 在容器内使用对于CI/CD场景,直接使用官方提供的Docker镜像是最干净的方式:
docker run --rm -v $(pwd):/work -w /work ghcr.io/mgoltzsche/khelm:latest template -f ./values.yaml ./my-chart这行命令将当前目录挂载到容器的/work目录,并在其中执行khelm template。
3.2 关键配置:仓库与缓存
khelm的行为可以通过命令行参数和环境变量进行配置,但理解其默认的路径很重要。
3.2.1 仓库配置khelm默认会读取$HOME/.config/helm/repositories.yaml文件来获取Helm仓库的配置。这意味着如果你已经用helm repo add配置过仓库(如bitnami,ingress-nginx),khelm可以直接使用,无需额外配置。
你也可以通过--repository-config参数指定一个自定义的仓库配置文件,或者通过--repository-cache指定依赖Chart的缓存目录。
3.2.2 缓存目录为了提高离线构建和重试速度,khelm会缓存拉取到的依赖Chart。默认的缓存目录是$HOME/.cache/helm/repository。在CI环境中,你可以将这个目录挂载为一个持久化卷,这样不同的构建任务就可以共享缓存,避免重复下载。
一个典型的CI配置思路是:
- 在构建开始前,检查缓存目录中是否存在所需的Chart。
- 如果不存在,则运行
khelm命令,它会自动下载并填充缓存。 - 构建结束后,保留缓存目录以供下次使用。
4. 深度实操:template与pack命令详解
4.1 使用khelm template渲染Chart
假设我们有一个简单的Chart目录结构如下:
my-app/ ├── Chart.yaml ├── values.yaml ├── templates/ │ ├── deployment.yaml │ └── service.yaml └── charts/ # 子Chart目录(可选)4.1.1 基础渲染最基础的渲染命令,使用Chart目录和默认的values.yaml:
khelm template ./my-app这会将渲染出的YAML直接打印到标准输出。
4.1.2 指定Values文件你可以通过多个-f或--values参数指定一个或多个values文件,后面的文件会覆盖前面的配置:
khelm template -f ./my-app/values.yaml -f ./my-app/values.prod.yaml ./my-app更常见的做法是,在CI中通过--set参数动态注入一些值,比如镜像Tag:
khelm template ./my-app -f ./values.yaml --set image.tag=$CI_COMMIT_SHA4.1.3 输出到文件使用-o或--output参数将结果输出到文件。结合--output-dir,它甚至可以将每个Kubernetes资源输出到单独的文件中,这对于使用GitOps工具(如Argo CD, Flux)的场景非常有用,因为这类工具通常希望每个资源一个文件。
# 输出到单个文件 khelm template ./my-app -o rendered.yaml # 输出到目录,每个资源一个文件 khelm template ./my-app --output-dir ./manifests执行后,./manifests目录下会生成类似my-app/templates/deployment.yaml这样的文件结构,其中包含了渲染后的内容。
4.1.4 严格验证与调试khelm默认开启YAML和Kubernetes Schema验证。如果你在渲染一个使用了自定义资源定义(CRD)的Chart,可能需要暂时关闭验证:
khelm template ./my-app --validate=false调试模板时,--debug参数非常有用,它会输出更详细的信息,包括渲染过程中用到的最终values。
实操心得:在集成到CI的初期,强烈建议同时使用
--debug和--output-dir。--debug可以帮助你确认注入的变量是否正确,而--output-dir生成的文件结构让你能清晰地审查每一个将要被部署的资源,避免因模板逻辑错误导致整个输出混乱不堪。
4.2 使用khelm pack创建自包含应用包
pack命令是khelm的杀手锏。我们继续用上面的my-appChart为例。
4.2.1 基础打包命令最简单的打包命令会将Chart打包成一个OCI镜像,并推送到默认的Docker守护进程(通常需要本地运行Docker Desktop或dockerd):
khelm pack ./my-app -t my-registry.com/my-team/my-app:v1.0.0-t:指定生成镜像的标签,和docker build -t类似。- 这条命令会:1)渲染Chart,2)提取镜像列表,3)拉取镜像,4)构建OCI包,5)推送到本地Docker守护进程。
4.2.2 推送到远程仓库通常我们需要推送到远程仓库。这需要先通过docker login或配置认证信息。khelm pack会复用本地Docker的认证配置(~/.docker/config.json)。
# 先登录 docker login my-registry.com # 打包并推送 khelm pack ./my-app -t my-registry.com/my-team/my-app:v1.0.0 --push--push参数会指示khelm在构建完成后将镜像推送到远程仓库。
4.2.3 离线打包与缓存在完全离线的环境中,你需要预先准备好所有依赖的容器镜像。khelm支持从本地镜像存储(如docker save导出的tar包或一个目录)中解析镜像。
首先,你需要将Chart用到的所有镜像pull到本地,并保存为一个tar文件或导出到目录。然后使用--image-store参数指定这个本地存储:
# 假设我们已经把nginx:alpine和busybox:latest镜像存到了 ./offline-images 目录 khelm pack ./my-app \ -t my-registry.com/my-app:v1.0-offline \ --image-store ./offline-images \ --push这样,khelm就不会尝试从网络拉取镜像,而是直接从./offline-images中查找。
4.2.4 解包部署打包好的镜像如何部署呢?khelm本身没有直接的unpack命令,但解包过程很简单,因为OCI镜像本质是一个标准的容器镜像。
你可以使用任何可以操作OCI镜像的工具来提取内容,例如docker或podman:
# 1. 将应用包镜像拉取到目标环境 docker pull my-registry.com/my-team/my-app:v1.0.0 # 2. 创建一个临时容器,将里面的文件复制出来 docker create --name app-bundle my-registry.com/my-team/my-app:v1.0.0 docker cp app-bundle:/app ./manifests # 复制Kubernetes清单 docker cp app-bundle:/images ./images # 复制镜像层(可选,如果需要导入到本地运行时) docker rm app-bundle # 3. 将镜像导入到本地容器运行时(如containerd) # 通常需要根据/images目录下的结构编写脚本,使用ctr或nerdctl导入 # 4. 使用kubectl apply部署 kubectl apply -f ./manifests在实际生产环境中,你可能会编写一个简单的脚本或使用一个小型的Kubernetes Operator来自动化这个解包和部署的过程。
注意事项:
khelm pack打包的镜像可能会很大,因为它包含了所有应用镜像的层。在推送前,请确保你的镜像仓库有足够的存储空间,并且网络带宽能够承受。对于超大型应用,可以考虑分组件打包。
5. 高级特性与集成实践
5.1 依赖锁定与可重复构建
这是khelm相较于原生helm在CI/CD中最大的优势之一。为了实现可重复构建,你需要使用Chart.lock文件。
5.1.1 生成锁文件在Chart目录下,运行helm dependency build会生成或更新Chart.lock文件。这个文件锁定了所有子Chart的确切版本和校验和。
cd ./my-app helm dependency update # 这会更新charts/目录下的子Chart,并生成Chart.lock5.1.2 在CI中使用锁文件在CI流水线中,你应该将Chart.lock文件一并提交到版本库。构建时,khelm会优先使用Chart.lock中的信息来解析依赖,确保每次拉取的都是完全相同的Chart版本。
你的CI脚本可以这样设计:
#!/bin/bash # 1. 检出代码,Chart.lock已在版本库中 git clone ... # 2. 使用khelm渲染,它会自动读取Chart.lock khelm template ./my-app -f values.yaml --output-dir ./rendered-manifests # 3. 后续的kubectl apply或GitOps同步这种方式彻底消除了因依赖Chart仓库更新而导致的构建差异。
5.2 与GitOps工作流集成
GitOps的核心思想是使用Git作为声明式基础设施和应用的唯一事实来源。khelm非常适合作为GitOps流水线中的“渲染引擎”。
5.2.1 模式一:在CI中渲染,推送清单到Git这是Argo CD等工具推荐的模式。你的应用代码库和配置(Chart、values)在一个仓库,渲染后的纯YAML清单提交到另一个“部署清单仓库”。
CI流水线步骤:
- 使用
khelm template --output-dir将Chart渲染为多文件YAML。 - 将渲染出的
./manifests目录推送到部署清单仓库的对应分支。 - Argo CD监控部署清单仓库,发现有变更即自动同步到集群。
5.2.2 模式二:使用khelm作为Argo CD的Config Management PluginArgo CD支持自定义配置管理插件。你可以创建一个插件,让Argo CD在同步时直接调用khelm来动态渲染Chart,而无需预先渲染好YAML。
这需要你在Argo CD的配置中添加插件定义,大致如下(需部署到Argo CD所在集群):
apiVersion: argoproj.io/v1alpha1 kind: ConfigManagementPlugin metadata: name: khelm spec: init: command: [sh, -c] args: ["echo 'Initializing...'"] generate: command: [khelm] args: ["template", "./", "-f", "values.yaml"]然后,在你的Argo CD Application资源中,指定spec.source.plugin.name: khelm。这样,Argo CD在拉取你的Chart代码后,会直接运行khelm生成最终的清单进行部署。这种模式更符合“一切即代码”的理念,但需要对Argo CD有更深的管理权限。
5.3 安全加固与最佳实践
5.3.1 使用HTTPS和可信仓库确保你的repositories.yaml中配置的仓库地址使用HTTPS。对于内部仓库,使用公司签发的可信CA证书。避免使用不安全的HTTP仓库。
5.3.2 镜像来源安全khelm pack在拉取镜像时,默认会使用镜像的默认标签。建议在values.yaml中明确指定镜像的摘要(Digest)而非标签,以实现真正的不可变部署。
# values.yaml image: repository: nginx # 使用标签,可能变化 # tag: 1.21 # 使用摘要,绝对固定 digest: sha256:644a70516a26004c97d0d85c7fe1d0c3a67ea8ab7ddf4aff193d9f30167cf3d7khelm支持这种格式,使用摘要可以确保每次拉取的都是构建时验证过的、分毫不变的镜像。
5.3.3 在Rootless容器中运行在CI环境中,出于安全考虑,应尽量避免以root身份运行容器。khelm的Docker镜像支持非root用户运行。你需要确保挂载的卷(如缓存目录、工作目录)对容器内的非root用户有写权限。
docker run --rm -u 1001:1001 \ -v $(pwd):/work:z -w /work \ -v ./helm-cache:/home/nonroot/.cache/helm:z \ ghcr.io/mgoltzsche/khelm:latest template ./my-app这里-u 1001:1001指定了用户和组ID,:z是SELinux上下文标签,在OpenShift等环境中可能需要。
6. 常见问题排查与实战技巧
6.1 依赖解析失败
问题:执行khelm template时,报错Error: could not download chart for dependency xxx。
排查思路:
- 检查网络与仓库配置:确认运行环境可以访问Chart仓库的URL。使用
curl或wget手动测试。 - 验证
repositories.yaml:检查$HOME/.config/helm/repositories.yaml或--repository-config指定的文件,确保仓库别名和URL正确。 - 检查
Chart.yaml中的依赖声明:确认dependencies中repository字段的地址正确,或者alias与repositories.yaml中定义的匹配。 - 使用
--debug参数:查看khelm尝试从哪个URL拉取Chart,这能精准定位问题。 - 清理缓存:有时缓存损坏会导致问题。可以删除
--repository-cache指定的目录,让khelm重新下载。
6.2 模板渲染错误
问题:渲染输出为空,或报模板语法错误。
排查思路:
- 使用
--debug和--dry-run:--debug会输出最终合并的values,--dry-run有时会给出更清晰的错误位置。结合使用:khelm template --debug --dry-run ./chart。 - 简化问题:尝试使用一个极简的values文件(甚至空文件)进行渲染,看是否是values中某个复杂结构导致模板逻辑出错。
- 检查模板函数:Helm模板函数在
khelm中基本都支持,但如果你使用了非常新的或自定义的模板函数,需要确认khelm版本是否支持。khelm基于Helm的库,但可能不是100%同步。 - 逐层渲染:如果你的Chart有复杂的子Chart依赖,可以尝试先单独渲染主Chart(注释掉依赖),再逐个添加子Chart,以定位是哪个Chart的模板出了问题。
6.3 pack命令镜像拉取失败
问题:khelm pack在拉取应用镜像时超时或认证失败。
排查思路:
- 手动拉取测试:在同一个环境中,使用
docker pull或podman pull尝试拉取khelm报错的镜像,看是否是网络或认证问题。 - 检查镜像引用格式:确保values.yaml或模板中镜像的引用是完整的(包括仓库地址)。私有仓库的镜像需要先
docker login。 - 配置镜像拉取密钥:对于Kubernetes集群中的私有仓库,通常使用
imagePullSecrets。但khelm pack是在CI环境中运行,它需要的是构建时的拉取权限。你需要确保运行khelm的容器或主机上,有对应的Docker认证配置(~/.docker/config.json)。 - 使用
--image-store绕过:对于已知的、已提前下载好的镜像,使用--image-store指向本地存储,可以完全跳过拉取步骤。
6.4 生成的OCI镜像过大
问题:khelm pack生成的镜像体积远超预期。
分析与优化:
- 检查基础镜像:你的应用容器镜像是否使用了过大的基础镜像(如
ubuntu:latest)?考虑替换为Alpine或Distroless等更小的基础镜像。 - 镜像层去重:
khelm本身会进行层去重,但如果多个应用镜像基于完全不同的基础镜像,去重效果有限。在规划微服务时,尽量让团队使用统一、精简的基础镜像。 - 分模块打包:对于一个巨大的单体应用Chart,可以考虑将其拆分为多个独立的子Chart,然后分别打包。部署时,按需拉取和部署这些模块包。
- 只打包必要镜像:通过
--set参数在打包时动态替换values,排除开发、测试专用的镜像(如busybox调试容器)。
6.5 与现有Helm工作流的兼容性
问题:团队已经有一套基于helm命令的成熟脚本,如何平滑引入khelm?
迁移策略:
- 并行运行,从渲染开始:不急于替换
helm install/upgrade。先在CI流水线中,用khelm template替代helm template来生成用于审核或GitOps的清单。对比两者输出是否一致。这个阶段只影响清单生成,不影响实际部署。 - 渐进式替换:对于新项目或新Chart,直接使用
khelm作为标准工具。对于老项目,在每次修改时,逐步将helm命令替换为khelm,并更新相关文档。 - 封装统一接口:可以编写一个统一的包装脚本(如
./scripts/deploy.sh),在脚本内部根据参数或环境变量决定调用helm还是khelm。这样,团队成员的日常命令不变,底层工具可以逐步切换。 - 重点推广pack功能:对于有离线部署需求或希望实现“应用即镜像”的团队,可以重点展示
khelm pack的能力,将其作为特定场景下的高级工具来推广,而不是helm的完全替代品。
我个人在多个从开发到生产的流水线中引入了khelm,最大的体会是它在提升构建确定性和简化离线交付方面带来的价值是立竿见影的。初期可能会遇到一些工具链切换的磨合问题,比如某些边缘的Helm模板函数支持度,但一旦流程跑通,它带来的可靠性和效率提升会让你觉得这些投入是值得的。尤其是当你需要为一个客户现场部署一个复杂应用,而现场网络受限时,一个khelm pack打出来的OCI镜像就是最好的“交付物”。