最近一个客户反馈安装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 源码分析,资源安装的顺序由多个层次决定:
一、整体安装流程如下:
- 安装 CRDs (crds/ 目录下的 CRD 资源)
- 渲染模板 (renderResources)
- 执行 pre-install hooks
- 创建常规资源 (按 Kind 排序)
- 等待资源就绪 (如果配置了 --wait)
- 执行 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。
具体实现方案如下:
- 在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
- 为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/