helm-cli安装资源时序报错问题问题

最近一个客户反馈安装chart包时出现错误,导致安装失败,具体错误如下:

Error: INSTALLATION FAILED: Internal error occurred: failed calling webhook "minstrumentation.kb.io": failed to call webhook: Post "https://optl-tencent-opentelemetry-operator-webhook.default.svc:443/mutate-opentelemetry-io-v1alpha1-instrumentation?timeout=10s" : no endpoints available for service "optl-tencent-opentelemetry-operator-webhook"

报错信息表示,该Service已经被创建,但是与之匹配的Endpoints为空。大概率是处理该webhook 的 pod 还未启动或者处于未就绪的状态。

该chart包主要需要向k8s集群安装以下资源:

  • Instrumentation: 一种自定义的cr资源。
  • MutatingWebhookConfiguration: 用于对instrumentation资源的create、update操作进行拦截,修改或填充某些字段。
  • ValidatingWebhookConfiguration: 用于对instrumentation资源的create、update操作进行验证,检查是否符合预期。
  • Deployment: pod的manager容器,创建处理webhook的服务器,实际处理 mutating、validating 操作。

之后,尝试在自己的机器上复现,却一直能够安装成功。初步怀疑是客户k8s集群配置的资源过小,导致安装对应的pod时花费的时间较长。和客户核对集群配置,发现集群大小正常,不存在因资源过小,导致创建慢的情况。

陷入了深思......

没有思路,开始查看 helm-cli 的文档,了解资源加载到k8s集群中的顺序。

发现 helm-cli 发布了最新的4.0.0版本,发现4.0版本,将MutatingWebhookConfiguration和ValidatingWebhookConfiguration添加到了常规资源的安装顺序中,而3.x版本中缺没有这两种资源。

开始确定该变更,对helm-cli4.x对资源安装顺序的影响。官方文档中没有对具体的安装顺序进行描述,于是clone源码,让AI帮忙解读。

helm-cli安装资源的顺序

根据 Helm 源码分析,资源安装的顺序由多个层次决定:

一、整体安装流程如下:

  1. 安装 CRDs (crds/ 目录下的 CRD 资源)
  2. 渲染模板 (renderResources)
  3. 执行 pre-install hooks
  4. 创建常规资源 (按 Kind 排序)
  5. 等待资源就绪 (如果配置了 --wait)
  6. 执行 post-install hooks

二、资源按 Kind 排序(核心逻辑)

在 kind_sorter.go 中定义了 InstallOrder,这是资源安装的优先级顺序:

var InstallOrder KindSortOrder = []string{

"PriorityClass", // 1. 优先级类

"Namespace", // 2. 命名空间

"NetworkPolicy", // 3. 网络策略

"ResourceQuota", // 4. 资源配额

"LimitRange", // 5. 限制范围

"PodSecurityPolicy", // 6. Pod 安全策略

"PodDisruptionBudget", // 7. Pod 中断预算

"ServiceAccount", // 8. 服务账户

"Secret", // 9. Secret

"SecretList", // 10. Secret 列表

"ConfigMap", // 11. ConfigMap

"StorageClass", // 12. 存储类

"PersistentVolume", // 13. 持久卷

"PersistentVolumeClaim", // 14. 持久卷声明

"CustomResourceDefinition", // 15. CRD

"ClusterRole", // 16. 集群角色

"ClusterRoleList", // 17. 集群角色列表

"ClusterRoleBinding", // 18. 集群角色绑定

"ClusterRoleBindingList", // 19. 集群角色绑定列表

"Role", // 20. 角色

"RoleList", // 21. 角色列表

"RoleBinding", // 22. 角色绑定

"RoleBindingList", // 23. 角色绑定列表

"Service", // 24. Service

"DaemonSet", // 25. DaemonSet

"Pod", // 26. Pod

"ReplicationController", // 27. ReplicationController

"ReplicaSet", // 28. ReplicaSet

"Deployment", // 29. Deployment

"HorizontalPodAutoscaler", // 30. HPA

"StatefulSet", // 31. StatefulSet

"Job", // 32. Job

"CronJob", // 33. CronJob

"IngressClass", // 34. Ingress 类

"Ingress", // 35. Ingress

"APIService", // 36. API 服务

"MutatingWebhookConfiguration", // 37. 变更 Webhook

"ValidatingWebhookConfiguration", // 38. 验证 Webhook

}

三、排序逻辑说明

复制代码
func lessByKind(kindA, kindB string, ordering KindSortOrder) bool {
    // 1. 如果两个 Kind 都不在列表中,按字母顺序排序
    // 2. 不在列表中的 Kind 排在最后
    // 3. 在列表中的 Kind 按其在列表中的索引排序(索引越小越先安装)
}

分析chart包安装报错的原因

chart包中造成安装异常的资源有:Instrumentation、MutatingWebhookConfiguration、ValidatingWebhookConfiguration、Deployment。

在helm3.x中,由于MutatingWebhookConfiguration、ValidatingWebhookConfiguration和Instrumentation都不在资源安装数组中,因此是按照字母顺序进行安装,安装顺序为Instrumentation - MutatingWebhookConfiguration - ValidatingWebhookConfiguration,创建Instrumentation时,由于webhook还没有安装,因此不会向Deployment中的manager容器发起请求,helm install 执行成功。

在helm4.x中,由于MutatingWebhookConfiguration、ValidatingWebhookConfiguration被加入到资源安装数组中,因此安装顺序变更为 MutatingWebhookConfiguration - ValidatingWebhookConfiguration - Instrumentation。此时,安装Instrumentatioin的时候,webhook已经被注册,导致Instrumentatioin的请求被拦截,向Deployment中的manager容器发起请求。而如果此时manager容器中创建的webhook server还没有正常启动,出现报错信息no endpoints available for service,导致helm install安装失败。

值得注意的一点是当我们使用helm install 安装chart包的时候,在不使用--wait参数的情况下,Helm 会按内置安装顺序依次提交资源并等待每个请求的 API 响应,只等待 API 返回创建成功(2xx),不等待创建对象处于就绪状态。而由于 k8s 的各控制器的调谐是并行进行的,Helm 是按顺序创建对象,但不等待就绪,后续对象会在前一个对象尚未 Ready 的情况下被创建。

解决方案 控制chart包资源的安装顺序

本质上是因为 helm-cli4.x 资源安装顺序导致的,有两个解决方案:

**方案一:**通过webhook调整资源的安装顺序为Instrumentation - MutatingWebhookConfiguration - ValidatingWebhookConfiguration,通过helm hook机制进行实现,添加post-install注解,赋予不同的weight值。

**方案二:**等待Deployment就绪之后(webhook server 已经启动)后,再安装Instrumentation。

方案一

通过使用helm hook机制来控制资源的安装顺序,具体的伪代码定义如下:

复制代码
# MutatingWebhookConfiguration.yaml
annotations:
  "helm.sh/hook": "post-install,post-upgrade"
  "helm.sh/hook-weight": "5"
  "helm.sh/hook-delete-policy": "before-hook-creation"
复制代码
# ValidatingWebhookConfiguration.yaml
annotations:
  "helm.sh/hook": "post-install,post-upgrade"
  "helm.sh/hook-weight": "5"
  "helm.sh/hook-delete-policy": "before-hook-creation"
复制代码
# Instrumentation.yaml
annotations:
  "helm.sh/hook": "post-install,post-upgrade"
  "helm.sh/hook-weight": "10"
  "helm.sh/hook-delete-policy": "before-hook-creation"

使用该helm hook直接控制资源的安装顺序存在一个问题。通过 hook 创建的资源不会作为当前release的一部分进行跟踪或管理,一旦 Helm 确认该资源已达到准备就绪状态,它将不再对hook资源进行任何操作。这意味着我们执行helm uninstall 的时候,不能够直接删除掉被hook安装的资源,导致某些资源残留在集群中。

我们虽然可以通过定义策略来决定何时删除相应的钩子资源。钩子删除策略使用以下注解定义:

复制代码
annotations:
  "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded

|------------------------|----------------------|
| before-hook-creation | 在启动新钩子之前删除之前的资源(默认) |
| hook-succeeded | 钩子成功执行后删除资源。 |
| hook-failed | 如果钩子在执行过程中失败,则删除该资源。 |

即使通过删除策略来控制,仍然会导致安装的资源没有办法被完全卸载掉。比如:Instrumentation、MutatingWebhookConfiguration、ValidatingWebhookConfiguration都是被hook安装的资源,并且删除策略为before-hook-creation。此时,我们执行helm uninstall 删除当前release,但是这些资源被残留在集群中。我们再次执行helm install,先删除旧的Instrumentation资源,再安装新的Instrumentation,此时由于集群中残留的MutatingWebhookConfiguration和ValidatingWebhookConfiguration,导致创建Instrumentation的请求被拦截,安装失败。

如果不想要hook资源残留在集群中,我们需要使用job,添加post-delete钩子,在helm uninstall执行后,删除其他钩子资源。

复制代码
apiVersion: batch/v1
kind: Job
metadata:
  name: delete-hooks-resource
  namespace: {{ .Release.Namespace }}
  annotations:
    "helm.sh/hook": "post-delete"
    "helm.sh/hook-weight": "3"
    "helm.sh/hook-delete-policy": "hook-succeeded,hook-failed"

方案二

等待deployment(webhook server)就绪后,再安装instrumentation资源。因此,资源的安装顺序为 MutatingWebhookConfiguration - ValidatingWebhookConfiguration -(等待Deployment就绪)- Instrumentation。

具体实现方案如下:

  1. 在deployment的manager容器添加startupProbe,等待webhook server启动。
复制代码
startupProbe:
  httpGet:
    path: /healthz
    port: {{ .Values.manager.ports.webhookPort }}
    scheme: HTTPS
  initialDelaySeconds: 7
  periodSeconds: 3
  timeoutSeconds: 3
  successThreshold: 1
  failureThreshold: 20
  1. 为instrumentation添加helm hook
复制代码
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: default-instrumentation
  namespace: opentelemetry-operator-system
  annotations:
    "helm.sh/hook": "post-install,post-upgrade"
    "helm.sh/hook-weight": "5"
    "helm.sh/hook-delete-policy": "before-hook-creation"

安装命令变更为 helm install operator ./xxx.tgz --values ./values.yaml --wait

通过添加wait参数,会等待所有的资源都处于就绪状态之后,才会去安装添加 post-install 注解的资源。通过这种方式,当我们安装Instrumentation的时候,Deployment(manager容器)已经处于就绪状态,即使创建instrumentation资源的请求被webhook拦截,也能够正常地创建。

除此之外,如果我们的安装环境不支持使用--wait参数,我们可以通过使用一个带有post-install的job来延迟安装instrumetation。在该job使用的带有kubectl的镜像,能够想集群发送kubectl相关的命令,检查当前deployment是否处于就绪状态。只有处于就绪状态,才会进行instrumentation资源的安装操作。该job执行后,会在3600秒后进行删除,并且由于每次启动job的name都不相同,不会对下次安装造成影响。至于被job安装的instrumentation资源,由于被安装opentelemetry-operator-system命名空间下,该命名空间会被执行helm uninstall的时候删除,因此instrumention也会被删除,不会残留在集群中。

完整yaml文件如下。

bash 复制代码
apiVersion: batch/v1
kind: Job
metadata:
  name: operator-tencent-opentelemetry-operator-wait-for-manager
  namespace: default
  annotations:
    "helm.sh/hook": "post-install,post-upgrade"
    "helm.sh/hook-weight": "3"
    "helm.sh/hook-delete-policy": "before-hook-creation"
  labels:
    helm.sh/chart: tencent-opentelemetry-operator-0.91.2
    app.kubernetes.io/name: tencent-opentelemetry-operator
    app.kubernetes.io/version: "v0.91.2"
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/instance: operator
    app.kubernetes.io/component: wait-for-manager
spec:
  backoffLimit: 10
  ttlSecondsAfterFinished: 3600
  template:
    metadata:
      name: operator-tencent-opentelemetry-operator-wait-for-manager
      labels:
        app.kubernetes.io/name: tencent-opentelemetry-operator
        app.kubernetes.io/component: wait-for-manager
    spec:
      restartPolicy: OnFailure
      serviceAccountName: tencent-opentelemetry-operator
      containers:
        - name: wait-for-manager
          image: "ccr.ccs.tencentyun.com/tke-market/kubectl:v1.35.0"
          imagePullPolicy: IfNotPresent
          resources:
            limits:
              cpu: 100m
              memory: 128Mi
            requests:
              cpu: 10m
              memory: 64Mi
          command:
            - /bin/sh
            - -c
            - |
              set -e

              DEPLOYMENT_NAME="operator-tencent-opentelemetry-operator"
              NAMESPACE="default"
              MAX_RETRIES=60
              RETRY_INTERVAL=3

              echo "==========================================="
              echo "等待 OpenTelemetry Operator Manager 就绪"
              echo "==========================================="
              echo "Deployment: ${DEPLOYMENT_NAME}"
              echo "Namespace: ${NAMESPACE}"
              echo "最大重试次数: ${MAX_RETRIES}"
              echo "重试间隔: ${RETRY_INTERVAL}秒"
              echo ""

              echo "正在检查 Deployment 是否存在..."
              retry_count=0
              while [ $retry_count -lt $MAX_RETRIES ]; do
                if kubectl get deployment "${DEPLOYMENT_NAME}" -n "${NAMESPACE}" > /dev/null 2>&1; then
                  echo "✓ Deployment 已存在"
                  break
                fi
                retry_count=$((retry_count + 1))
                echo "Deployment 尚未创建,等待中... (${retry_count}/${MAX_RETRIES})"
                sleep ${RETRY_INTERVAL}
              done

              if [ $retry_count -ge $MAX_RETRIES ]; then
                echo "✗ 错误: Deployment 在超时时间内未创建"
                exit 1
              fi


              echo ""
              echo "正在等待 Deployment available 状态..."
              echo ""
              kubectl get deployment "${DEPLOYMENT_NAME}" -n "${NAMESPACE}" -o wide || true

              if kubectl wait --for=condition=available \
                --timeout=120s \
                deployment/"${DEPLOYMENT_NAME}" \
                -n "${NAMESPACE}"; then
                echo ""
                echo "==========================================="
                echo "✓ Manager 已就绪 (包括 webhook server)"
                echo "==========================================="

                kubectl get deployment "${DEPLOYMENT_NAME}" -n "${NAMESPACE}" -o wide || true

                echo ""

                # 使用 base64 编码传递 YAML 内容 (避免 Shell 解析问题)
                INSTRUMENTATION_YAML_B64='YXBpVmVyc2lvbjogb3BlbnRlbGVtZXRyeS5pby92MWFscGhhMQpraW5kOiBJbnN0cnVtZW50YXRpb24KbWV0YWRhdGE6CiAgbmFtZTogZGVmYXVsdC1pbnN0cnVtZW50YXRpb24KICBuYW1lc3BhY2U6IG9wZW50ZWxlbWV0cnktb3BlcmF0b3Itc3lzdGVtCnNwZWM6CiAgZXhwb3J0ZXI6CiAgICBlbmRwb2ludDogImh0dHBzOi8vcGwuYXAtYmVpamluZy1mc2kuYXBtLnRlbmNlbnRjcy5jb206NDMyMCIKICBwcm9wYWdhdG9yczoKICAgIC0gdHJhY2Vjb250ZXh0CiAgICAtIGJhZ2dhZ2UKICAgIC0gYjMKICByZXNvdXJjZToKICAgIHJlc291cmNlQXR0cmlidXRlczoKICAgICAgdG9rZW46IFVQaG5kTUJGQVpJYkFXRG1ncGVlCiAgamF2YToKICAgIGltYWdlOgogIG5vZGVqczoKICAgIGltYWdlOgogIHB5dGhvbjoKICAgIGltYWdlOgogICAgZW52OgogICAgICAtIG5hbWU6IE9URUxfRVhQT1JURVJfT1RMUF9FTkRQT0lOVAogICAgICAgIHZhbHVlOiAiaHR0cHM6Ly9wbC5hcC1iZWlqaW5nLWZzaS5hcG0udGVuY2VudGNzLmNvbTo5MS9vdGxwIgogIGRvdG5ldDoKICAgIGltYWdlOgogICAgZW52OgogICAgICAtIG5hbWU6IE9URUxfRVhQT1JURVJfT1RMUF9FTkRQT0lOVAogICAgICAgIHZhbHVlOiAiaHR0cHM6Ly9wbC5hcC1iZWlqaW5nLWZzaS5hcG0udGVuY2VudGNzLmNvbTo5MS9vdGxwIgogIGdvOgogICAgaW1hZ2U6IGNjci5jY3MudGVuY2VudHl1bi5jb20vdGFwbS9hdXRvaW5zdHJ1bWVudGF0aW9uLWdvOnYwLjguMC1hbHBoYQ=='

                echo "Instrumentation.yaml文件内容如下"
                echo "${INSTRUMENTATION_YAML_B64}" | base64 -d
                echo ""

                echo "准备安装Instrumentation资源 (超时时间: 30秒)"
                echo "开始时间: $(date '+%Y-%m-%d %H:%M:%S')"
                echo ""

                # 纯管道 + 变量捕获方式,不依赖文件系统写入
                TIMEOUT_SECONDS=60

                # 后台执行 kubectl apply,并通过变量传递 PID
                echo "${INSTRUMENTATION_YAML_B64}" | base64 -d | kubectl apply -f - 2>&1 &
                KUBECTL_PID=$!
                echo "kubectl apply 进程已启动 PID: ${KUBECTL_PID}"

                # 超时监控循环
                ELAPSED=0
                APPLY_EXIT_CODE=""
                while [ ${ELAPSED} -lt ${TIMEOUT_SECONDS} ]; do
                  if ! kill -0 ${KUBECTL_PID} 2>/dev/null; then
                    # kubectl 进程已完成,获取退出码
                    wait ${KUBECTL_PID}
                    APPLY_EXIT_CODE=$?
                    echo "kubectl apply 进程已完成 (耗时: ${ELAPSED}秒)"
                    break
                  fi
                  sleep 1
                  ELAPSED=$((ELAPSED + 1))
                  # 每 10 秒输出等待信息
                  if [ $((ELAPSED % 10)) -eq 0 ]; then
                    echo "[${ELAPSED}秒] 仍在等待 kubectl apply 完成..."
                  fi
                done

                # 检查是否超时
                if [ -z "${APPLY_EXIT_CODE}" ]; then
                  echo ""
                  echo "✗ kubectl apply 超时 (${TIMEOUT_SECONDS}秒),强制终止进程..."
                  kill -9 ${KUBECTL_PID} 2>/dev/null || true
                  wait ${KUBECTL_PID} 2>/dev/null || true
                  APPLY_EXIT_CODE=124
                fi

                echo ""
                echo "结束时间: $(date '+%Y-%m-%d %H:%M:%S')"
                echo "Instrumentation资源安装结束,退出码: ${APPLY_EXIT_CODE}"

                if [ ${APPLY_EXIT_CODE} -eq 0 ]; then
                  echo "✓ Instrumentation 资源创建成功"
                  echo ""
                  echo "==========================================="
                  echo "✓ 所有资源已就绪"
                  echo "==========================================="
                  exit 0
                else
                  echo "✗ Instrumentation 资源创建失败,退出码: ${APPLY_EXIT_CODE}"
                  exit 1
                fi
              else
                echo ""
                echo "==========================================="
                echo "✗ 错误: Manager 未能在超时时间内就绪"
                echo "==========================================="
                # 打印 Deployment 状态以便调试
                echo "当前 Deployment 状态:"
                kubectl get deployment "${DEPLOYMENT_NAME}" -n "${NAMESPACE}" -o wide || true
                echo ""
                echo "Pod 状态:"
                kubectl get pods -n "${NAMESPACE}" -l app.kubernetes.io/name=tencent-opentelemetry-operator -o wide || true
                exit 1
              fi
          securityContext:
            runAsNonRoot: false
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL

总结

由于helm3.x和helm4.x资源安装时序不同,导致出现安装错误。通过添加helm hook机制,进一步控制资源之间的安装顺序,兼容helm3.x和helm4.x。

完整chart包代码参考:https://github.com/tkestack/charts/tree/main/incubator/opentelemetry-operator

参考资料:

helm资源安装顺序:https://helm.sh/zh/docs/intro/using_helm#helm-install-installing-a-package

helm hooks 机制ttps://helm.sh/docs/topics/charts_hooks/

相关推荐
VX:Fegn08959 小时前
计算机毕业设计|基于springboot + vue智慧养老院管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
一线大码9 小时前
服务端架构的演进与设计
后端·架构·设计
techzhi10 小时前
Docker & Docker Compose 安装方案
docker·容器·eureka
末日汐10 小时前
库的制作与原理
linux·后端·restful
晴虹10 小时前
lecen:一个更好的开源可视化系统搭建项目--数据、请求、寄连对象使用--全低代码|所见即所得|利用可视化设计器构建你的应用系统-做一个懂你的人
前端·后端·低代码
码农小卡拉10 小时前
深度解析 Spring Boot 启动运行机制
java·spring boot·后端
weixin_4481199410 小时前
如何装docker
java·云原生·eureka
love_summer10 小时前
优雅地控制Python循环:break与continue的最佳实践及底层逻辑
后端
钦拆大仁10 小时前
如何手搓一个Spring Security
java·后端·spring