1. 为什么你需要一个私有Helm仓库?

如果你在管理一个K8s集群,尤其是那种“与世隔绝”的内网环境,那你肯定对“依赖”这两个字又爱又恨。爱的是,像Helm这样的包管理器,一个命令就能拉起一整套复杂的应用,比如带数据库、缓存、前端的完整Web服务。恨的是,当网络断开,那些方便的在线仓库瞬间变成摆设,helm install 命令后面跟着的红色错误提示,能让你瞬间血压升高。

我经历过不止一次。在一个金融客户的现场,他们的生产环境是完全离线的。我们需要部署一套监控系统,Prometheus、Grafana、Alertmanager,用公有Helm仓库的话,分分钟就能搞定。但现实是,我们得先想办法把几十个Chart包及其依赖,像蚂蚁搬家一样弄进内网,还得保证版本和依赖关系不出错。那过程,简直是一场噩梦。所以,搭建一个私有Helm仓库,对于任何有内网部署、安全合规要求或希望提升部署效率的团队来说,都不是一个“可选项”,而是一个“必选项”。

私有仓库能给你带来几个实实在在的好处:

  • 离线无忧:彻底摆脱对外部网络的依赖,内网环境部署应用和公有云一样流畅。
  • 安全可控:你完全掌控仓库里有什么Chart,是谁上传的,经过了哪些测试和审计,避免引入来路不明的镜像或配置。
  • 效率提升:团队内部共享和复用Chart变得极其方便,新人 onboarding 或新环境部署,不再需要重复“找包-下载-传包”的繁琐流程。
  • 版本管理:像管理代码一样管理你的应用部署模板,可以清晰地看到每个Chart的历史版本、变更记录,并且能轻松地回滚。

简单说,私有Helm仓库就是你内网K8s生态里的“应用商店”。接下来,我就手把手带你从零开始,搭建一个稳定、可用、能上生产的私有Helm仓库,并分享一些我踩过坑才总结出来的实战经验。

2. 基础准备:安装Helm 3与理解核心概念

工欲善其事,必先利其器。在搭建仓库之前,我们得先把Helm 3这个工具本身准备好。这里我强烈推荐二进制安装,简单直接,尤其适合离线环境。

2.1 离线安装Helm 3

首先,你需要一台能通外网的机器(跳板机或你的开发机),去Helm的GitHub Release页面下载对应版本的二进制包。版本兼容性很重要,我的经验是,Helm的版本最好不低于K8s集群版本的大版本号。比如你的K8s是1.23,那么Helm 3.8.x是一个稳妥的选择。

下载和解压就是标准的Linux操作:

# 假设你已经下载了 helm-v3.12.0-linux-amd64.tar.gz(请使用最新稳定版)
tar -zxvf helm-v3.12.0-linux-amd64.tar.gz

解压后,你会看到一个叫 linux-amd64 的目录,里面就是 helm 这个可执行文件。把它放到系统的 PATH 里,比如 /usr/local/bin

sudo mv linux-amd64/helm /usr/local/bin/helm

现在,验证一下安装是否成功:

helm version

如果看到类似 version.BuildInfo{Version:"v3.12.0", ...} 的输出,就说明Helm本体安装好了。为了让使用更顺手,强烈建议配置命令自动补全:

# 为当前shell启用补全
source <(helm completion bash)
# 永久生效
echo "source <(helm completion bash)" >> ~/.bashrc

这样,你输入 helm ins 再按Tab,它就能自动补全成 helm install 了,效率提升一大截。

2.2 快速理解Helm的核心:Chart与Repository

在动手搭建之前,花两分钟搞清楚两个核心概念,后面会顺畅很多。

Chart:你可以把它想象成一个“软件安装包”或者“部署模板合集”。它不仅仅包含了要部署的K8s资源定义(Deployment, Service, ConfigMap等),还定义了这些资源之间的依赖关系,以及可配置的参数(通过 values.yaml)。一个Chart被打包后就是一个 .tgz 压缩文件。

Repository(仓库):这就是存放Chart的地方。一个仓库本质上就是一个HTTP服务器,它提供两个核心文件:

  1. 一堆 .tgz 的Chart包文件。
  2. 一个名为 index.yaml 的索引文件。这个文件记录了仓库里所有Chart的元信息,比如名字、版本、描述以及下载URL。

当你执行 helm repo add 时,Helm客户端就会去这个地址获取 index.yaml 并缓存到本地。之后你 helm searchhelm install 时,Helm会先查本地缓存的索引,找到Chart的真实下载地址。

所以,搭建私有仓库,我们要做的就是:搭建一个能提供这两个文件的Web服务,并确保内网的Helm客户端能访问到它。 常用的仓库服务器软件有 ChartMuseum、Harbor(不仅管理镜像,也管理Chart)、或者像我们后面要做的,用一个简单的Nginx也能胜任。

3. 实战:在K8s集群内搭建高可用私有仓库

网上很多教程会用Docker直接跑一个ChartMuseum容器,这确实简单。但在生产环境,我们更希望它本身就是K8s的一个工作负载,能享受K8s带来的高可用、自愈、弹性扩缩容和统一的网络、存储管理。下面我们就用K8s原生资源来部署一个。

3.1 设计部署架构与准备存储

我们的目标是在K8s集群里部署一个Nginx,用它来提供静态文件服务,存放我们的Chart包和 index.yaml。为了保证仓库数据持久化,不随Pod重启而丢失,我们需要用到PersistentVolume(PV)和PersistentVolumeClaim(PVC)。

假设你已经有一套K8s集群,并且有可用的存储类(StorageClass),例如 nfs-clientcsi-hostpath。我们首先创建一个PVC来申请存储空间:

# helm-repo-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: helm-repo-pvc
  namespace: default # 建议放在独立的命名空间,如 helm-repo
spec:
  storageClassName: nfs-client # 替换为你的实际StorageClass
  accessModes:
    - ReadWriteMany # 需要支持多节点读写
  resources:
    requests:
      storage: 10Gi # 根据Chart数量调整,初期10GB足够

应用这个配置:kubectl apply -f helm-repo-pvc.yaml。这样,我们就有了一个名为 helm-repo-pvc 的持久化存储声明。

3.2 部署Nginx作为仓库服务器

接下来,我们部署一个使用这个PVC的Nginx Deployment和Service。

# helm-repo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helm-repo-nginx
  namespace: default
spec:
  replicas: 2 # 两个副本实现高可用
  selector:
    matchLabels:
      app: helm-repo-nginx
  template:
    metadata:
      labels:
        app: helm-repo-nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25-alpine # 使用Alpine镜像,体积小
        ports:
        - containerPort: 80
        volumeMounts:
        - name: repo-storage
          mountPath: /usr/share/nginx/html # Nginx默认静态文件目录
          subPath: charts # 我们将在PVC的根目录下创建charts子目录
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /charts/index.yaml # 通过访问索引文件来检查健康状态
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
      volumes:
      - name: repo-storage
        persistentVolumeClaim:
          claimName: helm-repo-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: helm-repo-service
  namespace: default
spec:
  selector:
    app: helm-repo-nginx
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP # 先在集群内部访问

这里有几个关键点:

  1. replicas: 2 确保了即使一个Pod挂掉,服务依然可用。
  2. volumeMounts 中的 subPath: charts 意味着我们准备把Chart文件都放在PVC根目录下的 charts 文件夹里。这样结构更清晰。
  3. 配置了 livenessProbe,让K8s能自动检查Nginx服务是否正常。

应用部署:kubectl apply -f helm-repo-deployment.yaml

3.3 配置Ingress实现外部访问

Service的 ClusterIP 只能在集群内访问。为了让集群外(比如你的CI/CD服务器、运维终端)也能添加这个仓库,我们需要一个Ingress。这里以最常用的Nginx Ingress Controller为例:

# helm-repo-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: helm-repo-ingress
  namespace: default
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2 # 重要:路径重写
spec:
  ingressClassName: nginx # 指定Ingress Controller
  rules:
  - host: helm.internal.company.com # 使用内部域名,方便管理
    http:
      paths:
      - path: /helm/charts(/|$)(.*) # 匹配 /helm/charts/ 开头的路径
        pathType: Prefix
        backend:
          service:
            name: helm-repo-service
            port:
              number: 80

这个Ingress配置是关键。它把访问 http://helm.internal.company.com/helm/charts/ 的流量,转发给后端的 helm-repo-service,并且通过 rewrite-target 注解,把路径中的 /helm/charts 前缀去掉。这样,Nginx Pod内看到的请求路径就是根路径 /,正好对应它服务的 /usr/share/nginx/html 目录。

你需要确保 helm.internal.company.com 这个域名能在你的内网DNS中解析到Ingress Controller的IP地址,或者直接在需要访问的机器的 /etc/hosts 文件中配置。

至此,一个高可用的、基于K8s的私有Helm仓库服务器就搭建完成了。它的访问地址将是:http://helm.internal.company.com/helm/charts/

4. 制作、上传与管理你的第一个Chart

仓库建好了,现在让我们往里放点“货”。我们从制作一个最简单的Chart开始。

4.1 快速创建一个示例Chart

Helm提供了一个脚手架命令,能快速生成一个Chart的目录结构:

helm create my-first-app

这个命令会创建一个名为 my-first-app 的目录,里面包含了一个标准Chart的所有文件。你可以用 tree 命令看一下结构,核心文件是:

  • Chart.yaml: Chart的元数据文件,定义名称、版本、描述等。
  • values.yaml: 默认的配置值。这是Helm最强大的地方之一,所有可配置的参数都在这里。
  • templates/: 目录里存放了K8s资源的模板文件,比如 deployment.yamlservice.yaml。这些是Go Template格式,会结合 values.yaml 渲染成最终的K8s YAML。

我们先不修改复杂内容,就把它打包。进入目录并打包:

cd my-first-app
helm package .

执行后,会在当前目录生成一个文件,例如 my-first-app-0.1.0.tgz。这个 .tgz 文件就是我们的Chart包。

4.2 上传Chart并生成仓库索引

现在,我们需要把这个包放到我们仓库的存储目录下,并更新索引。

首先,登录到你的K8s Worker节点,或者任何能通过PVC访问到存储的Pod。假设你的PVC挂载到了宿主机的 /data/helm-repo 路径(具体路径取决于你的存储配置)。

  1. 创建目录并复制Chart包

    # 在PVC挂载点创建charts目录(如果不存在)
    sudo mkdir -p /data/helm-repo/charts
    # 将打包好的Chart复制过去
    sudo cp my-first-app-0.1.0.tgz /data/helm-repo/charts/
    
  2. 生成或更新索引文件 index.yaml: 这是最关键的一步。我们需要使用 helm repo index 命令,根据 charts 目录下的所有 .tgz 文件,生成索引。

    # 进入charts目录的父目录
    cd /data/helm-repo
    # 生成索引,--url 参数必须指定仓库的完整访问地址
    helm repo index charts/ --url http://helm.internal.company.com/helm/charts
    

    这个命令会在 charts/ 目录下生成一个 index.yaml 文件。你可以用 cat charts/index.yaml 查看一下,里面已经记录了 my-first-app 这个Chart的信息和下载链接。

现在,通过浏览器访问 http://helm.internal.company.com/helm/charts/index.yaml,你应该能看到刚刚生成的索引内容。同样,访问 http://helm.internal.company.com/helm/charts/my-first-app-0.1.0.tgz 应该能下载到Chart包。这说明你的仓库服务运转正常了。

4.3 添加私有仓库并安装应用

回到你的Helm客户端机器(需要能访问内网域名)。

  1. 添加仓库

    helm repo add my-private-repo http://helm.internal.company.com/helm/charts
    

    如果仓库有HTTP Basic认证(生产环境建议加上),可以这样添加:

    helm repo add my-private-repo http://helm.internal.company.com/helm/charts --username <user> --password <pass>
    
  2. 更新本地仓库缓存

    helm repo update
    

    这个命令会去拉取 my-private-repo 最新的 index.yaml

  3. 搜索和安装

    # 搜索我们上传的Chart
    helm search repo my-first-app
    # 安装它,命名为 my-demo
    helm install my-demo my-private-repo/my-first-app
    

    如果一切顺利,你会看到Helm输出的安装成功提示,并且用 kubectl get pods 能看到相关的Pod在启动。

这个过程看似步骤不少,但一旦跑通,后续上传新的Chart版本,只需要重复 “打包 -> 复制到charts目录 -> 重新生成索引” 这三步即可,非常自动化。

5. 生产级进阶:安全、CI/CD与最佳实践

基础的仓库能用起来了,但要用于生产,我们还得在安全、自动化和管理上多下点功夫。

5.1 为仓库添加安全认证

暴露一个无需认证的HTTP服务是不安全的。我们可以通过Ingress Annotation轻松地为仓库添加Basic认证。

首先,创建一个包含用户名和密码的Secret:

# 创建一个 auth 文件
htpasswd -c auth admin
# 输入密码,例如 MySecurePass123
# 在K8s中创建Secret
kubectl create secret generic helm-repo-basic-auth --from-file=auth -n default

然后,修改之前的Ingress,添加认证注解:

# 在helm-repo-ingress.yaml的metadata.annotations部分添加
annotations:
  nginx.ingress.kubernetes.io/rewrite-target: /$2
  nginx.ingress.kubernetes.io/auth-type: basic
  nginx.ingress.kubernetes.io/auth-secret: helm-repo-basic-auth
  nginx.ingress.kubernetes.io/auth-realm: "Authentication Required - Helm Repo"

更新Ingress后,再访问仓库地址,浏览器就会弹出登录框了。添加仓库时也需要带上用户名密码,如前文所示。

对于更高安全要求的环境,可以考虑配置HTTPS(使用自签名或内部CA颁发的证书),并在Ingress中配置TLS。

5.2 集成CI/CD流水线自动发布Chart

手动上传Chart效率低且容易出错。理想的方式是,当你的应用代码Git仓库打上版本标签(Tag)时,CI/CD流水线自动构建镜像、更新Chart中的镜像版本、打包Chart并推送到私有仓库。

这里给出一个GitLab CI的简化示例 .gitlab-ci.yml

stages:
  - build-chart
  - publish-chart

variables:
  HELM_REPO_URL: "http://helm.internal.company.com/helm/charts"
  # 假设认证信息通过CI变量 HELM_REPO_USERNAME 和 HELM_REPO_PASSWORD 传递

publish-chart:
  stage: publish-chart
  image: alpine/helm:3.12.0
  script:
    # 1. 进入Chart目录
    - cd k8s/charts/my-app
    # 2. 更新Chart.yaml中的版本号(可以从CI_COMMIT_TAG获取)
    - sed -i "s/version: .*/version: ${CI_COMMIT_TAG#v}/" Chart.yaml
    - sed -i "s/appVersion: .*/appVersion: ${CI_COMMIT_TAG#v}/" Chart.yaml
    # 3. 打包Chart
    - helm package .
    # 4. 添加仓库(带认证)
    - helm repo add --username $HELM_REPO_USERNAME --password $HELM_REPO_PASSWORD my-repo $HELM_REPO_URL
    # 5. 将包推送到一个临时目录,这里需要能访问仓库存储(可通过kubectl cp或共享存储实现)
    # 6. 更新仓库索引(这一步可能需要在一个能执行helm repo index的Runner中完成,或者通过调用一个专门的服务API)
    # 示例:假设我们有一个脚本服务来处理上传和索引更新
    - curl -u "$HELM_REPO_USERNAME:$HELM_REPO_PASSWORD" -X POST -F "chart=@my-app-${CI_COMMIT_TAG#v}.tgz" "${HELM_REPO_URL}/api/upload"
  only:
    - tags # 只有打tag时才触发

这个示例展示了核心思路:版本化、自动化。在实际中,你可能需要编写一个简单的后端服务(比如用Python Flask)来接收CI流水线的上传请求,验证权限,将Chart包存放到正确位置,然后调用 helm repo index 命令更新索引。这样就把整个流程串起来了。

5.3 版本控制与回滚策略

Chart本身应该纳入版本控制系统(如Git)进行管理。Chart.yaml 里的 version 字段遵循语义化版本控制(SemVer)。每次对Chart模板(templates/)或默认值(values.yaml)的修改,都应该提升版本号并提交。

Helm天然支持发布(Release)的版本管理和回滚。当你使用 helm upgrade 更新一个已部署的应用时,Helm会创建一个新的Release版本。你可以通过以下命令管理:

# 查看my-demo这个Release的所有历史版本
helm history my-demo
# 回滚到第2个版本
helm rollback my-demo 2

最佳实践是:将每个环境的配置(如 values-dev.yaml, values-prod.yaml)也放在Git中,通过CI/CD在部署时注入对应的值文件。这样,任何一次部署都对应一次明确的Git提交,真正做到部署即代码,审计和回滚都非常清晰。

6. 常见问题排查与运维命令锦囊

即使搭建得再完美,运维过程中也难免会遇到问题。这里我整理了几个最常见的坑和对应的排查命令。

问题1:helm repo add 成功,但 helm search repo 找不到Chart。

  • 原因:本地仓库索引缓存过期。
  • 解决helm repo update。如果还不行,直接访问仓库的 index.yaml URL,看内容是否正确,Chart的URL路径是否可访问。

**问题2:helm install 失败,提示“Error: failed to download”。

  • 原因:Chart包的实际下载地址不对,或者网络不通。
  • 排查
    1. 检查 index.yaml 里该Chart的 urls 字段,是否是完全正确的可下载地址。
    2. 尝试用 curlwget 直接下载那个URL,看是否成功。
    3. 检查Ingress的路径重写规则是否正确,确保Nginx收到的请求路径能映射到正确的物理文件。

问题3:上传新Chart后,其他人看不到。

  • 原因:只上传了 .tgz 文件,没有重新生成 index.yamlhelm repo index 命令会覆盖整个索引文件,所以必须针对包含所有Chart的目录执行。
  • 解决:确保每次上传新Chart或新版本后,都在包含所有 .tgz 文件的目录下执行 helm repo index

问题4:Pod启动失败,提示镜像拉取错误。

  • 原因:这是Chart部署后的问题,但根源可能在Chart的 values.yaml 中定义的镜像地址是公网地址,内网无法访问。
  • 解决:在制作Chart时,values.yaml 中的镜像地址应该使用变量,并在部署时通过 --set-f 指定内网镜像仓库的地址。例如:
    helm install my-app my-private-repo/my-app --set image.repository=internal.registry.com/myapp
    

常用运维命令速查表:

场景命令说明
仓库管理helm repo list查看已添加的仓库
helm repo add <名称> <URL>添加仓库
helm repo update更新所有仓库的本地缓存
helm repo remove <名称>删除仓库
Chart搜索helm search repo <关键词>在本地仓库缓存中搜索
helm search hub <关键词>在Artifact Hub(公网)搜索
Chart操作helm show chart <repo/chart>查看Chart定义
helm show values <repo/chart>查看Chart的默认values
helm pull <repo/chart> --version x.y.z下载Chart包到本地
helm lint <chart目录>检查Chart语法和格式
发布管理helm install <release名> <repo/chart>安装Chart
helm upgrade -f values.yaml <release名> <repo/chart>使用文件升级
helm listhelm ls列出已安装的Release
helm status <release名>查看Release状态
helm uninstall <release名>卸载Release
helm rollback <release名> <版本号>回滚到历史版本
helm get values <release名>获取Release当前使用的values

最后,关于文档,Helm的官方文档(helm.sh)始终是最权威的参考。遇到复杂问题时,多去翻翻 helm --help 和子命令的 --help,往往能发现意想不到的实用参数。私有化部署这条路,我踩过的坑不少,但一旦体系搭建完成,你会发现团队的应用交付效率和质量会有质的飞跃。关键是开始动手做,从第一个简单的Chart和仓库开始,逐步迭代完善。

更多推荐