PLG: 部署与飞书告警实践
本篇记录如何把 PLG 监控体系以 git + helm + CI 的方式部署进集群,并把告警送达飞书群,所有做法都是生产环境的通用玩法
监控栈的 CI 部署
整体形态
监控栈是一个
umbrella chart:聚合kube-prometheus-stack(含Prometheus/Alertmanager)+loki+promtail+grafana依赖,环境差异全部放在values-*.yaml中部署不走手工命令,而是
GitHub Actions在push到main时按路径触发,路径过滤保证只有监控配置变更才触发部署:yamlon: push: branches: - main paths: - charts/monitoring/** - charts/istio/** - charts/ingress/**多集群、多产品通过
matrix展开,每个条目指定chart、命名空间、values文件与kubeconfig上下文:yamlstrategy: matrix: product: - name: monitoring namespace: monitoring values: values-example-prod.yaml cluster: - name: example-prod runner: example-runner # 自托管 runner,持有集群访问权 context: example-prod
密钥注入
密钥永远不进
git:values文件里写占位符,CI用envsubst在发布时替换yaml# values 文件中的占位 grafana: env: BOT_GRAFANA_WEBHOOK_TOKEN: $BOT_GRAFANA_WEBHOOK_TOKENbashenvsubst '$FEISHU_APP_ID $FEISHU_APP_SECRET $BOT_GRAFANA_WEBHOOK_TOKEN' \ < charts/monitoring/values-example-prod.yaml > $values_file简单值直接用
--set注入(如CloudflareToken),不必经过values文件
发布与自愈
统一使用
helm upgrade --install --create-namespace,并对"上一个操作未完成"的常见冲突做自动回滚 + 重试:bashfor retry in {1..3}; do res=$(helm -n monitoring upgrade --install --create-namespace monitoring \ ./charts/monitoring -f "$values_file" \ --set cloudflare.apiToken=$CF_TOKEN 2>&1) [[ $? -eq 0 ]] && exit 0 if [[ "$res" = *"another operation (install/upgrade/rollback) is in progress"* ]]; then helm -n monitoring rollback monitoring # 卡住的发布先回滚再重试 continue fi sleep 10 done exit 1runner通过kubeconfigsecret切换目标集群上下文,一个runner可以发布多个集群yaml- uses: azure/k8s-set-context@v4 with: method: kubeconfig kubeconfig: ${{ secrets.KUBECONFIG }} context: ${{ matrix.cluster.context }}
告警即代码
Grafana 统一告警
选择
Grafana统一告警(Unified Alerting)而非Alertmanager作为告警出口:规则、联系点、通知策略都住在Grafana里,与仪表盘共享数据源与标签体系,Alertmanager仅作为kube-prometheus-stack的兜底保留告警规则不进
Grafana界面手点,而是写成ConfigMap,打上约定标签,由Grafana sidecar全命名空间监听并同步进Grafana,改动走git评审:yamlkind: ConfigMap metadata: name: pod-status-alert namespace: monitoring labels: grafana_alert: "1" # sidecar 按该标签发现告警资源 data: pod-status-alert.yaml: | apiVersion: 1 ...yaml# values 中开启 sidecar 的告警同步,搜索全命名空间 grafana: sidecar: alerts: enabled: true searchNamespace: ALL同一文件描述四类资源:
groups(规则)、contactPoints(联系点)、policies(通知策略)、templates(通知模板)
告警规则的结构
规则按
group组织,folder决定在Grafana中的目录归属,interval是评估周期:yamlapiVersion: 1 groups: - orgId: 1 name: Kubernetes folder: Infra interval: 1m # 每分钟评估一次 rules: - uid: a9a96a82-f6f5-4bfe-a18e-6f1ac6c9e72f title: 服务健康状态 condition: 不健康服务增多 # 指向起决定作用的表达式 refId data: - refId: 不健康服务 datasourceUid: prometheus # 查询 Prometheus model: expr: | sum by (namespace, pod, phase) ( kube_pod_status_phase{namespace !~ 'staging|dev-.*', phase=~"Pending|Unknown|Failed"} == 1 and on (namespace, pod) ((time() - kube_pod_created) > 180) ) - refId: 不健康服务增多 datasourceUid: __expr__ # 表达式阶段做阈值判断 model: type: threshold expression: 不健康服务 conditions: - evaluator: params: [0] type: gt # 结果 > 0 即触发 for: 5m # 持续满足 5 分钟才真正触发,抑制抖动 noDataState: OK # 无数据视为正常 annotations: summary: 命名空间 {{ $labels.namespace }} 下 Pod {{ $labels.pod }} 处于 {{ $labels.phase }} 状态 labels: product: infra # 供通知模板与策略路由使用注意三个工程细节:
PromQL里先做过滤与降噪:排除staging等低价值命名空间、只看存活超过3分钟的Podfor: 5m+noDataState: OK是防告警风暴的两道闸labels.product是整条告警链路的路由键,从values按产品开关(alerts.<product>.enabled)决定渲染哪些规则
飞书告警链路
联系点:webhook + 自建机器人
Grafana原生不支持飞书,与其等官方支持,不如加一层自建转发服务:联系点配置为普通webhook,指向自建的机器人服务,由它把Grafana的告警 payload 转成飞书群消息好处是格式完全自主、鉴权自己掌控、以后接任何新
IM都只改这一个服务:yamlapiVersion: 1 contactPoints: - orgId: 1 name: 服务端告警群 receivers: - uid: 80192ad5-0865-4075-a69a-eeccccbf82d8 type: webhook settings: url: https://bot.example.com/grafana/webhook # 自建机器人 httpMethod: POST authorization_scheme: Bearer # 出口鉴权,防止伪造告警 authorization_credentials: $BOT_GRAFANA_WEBHOOK_TOKENBearer Token通过环境变量注入Grafana(grafana.env),再由envsubst在CI阶段填充,全程不落git
通知策略
策略决定"谁收到什么、多久提醒一次",
repeat_interval是防轰炸的关键参数:yamlapiVersion: 1 policies: - orgId: 1 receiver: grafana-default-email # 兜底接收器 group_by: ['...'] repeat_interval: 5m # 未恢复告警每 5 分钟重复提醒 routes: - receiver: 服务端告警群 # 全部告警转发到飞书群 continue: true # 继续匹配后续路由,便于后续按产品分流
通知模板
模板用
Grafana的Go template语法,自定义标题与正文,把关键信息(product/application标签、当前数值、图表与静默链接)组织成飞书消息卡片的字段:yamlapiVersion: 1 templates: - orgId: 1 name: "example.title" template: | {{- if len .Alerts.Firing -}} 🚨🚨🚨 有 {{ len .Alerts.Firing }} 条告警规则正在触发 {{- end }} {{- if and (len .Alerts.Firing) (len .Alerts.Resolved) -}}; {{ end -}} {{- if len .Alerts.Resolved -}} ✅✅✅ 有 {{ len .Alerts.Resolved }} 条告警规则已经恢复 {{- end }} - orgId: 1 name: "example.message" template: | - 产品名称: {{ .Labels.product }} - 产品应用: {{ or (or .Labels.application .Labels.pod) "没有数据" }} - 告警规则: {{ .Labels.alertname }} - 当前数值: {{ range $k, $v := .Values }}{{ $k }}={{ $v }} {{ end }} {{- if .DashboardURL }} - 查看图表: [点击查看]({{ .DashboardURL }}) {{- end }} {{- if .SilenceURL }} - 抑制告警: [点击抑制]({{ .SilenceURL }}) {{- end }}.Labels.application正是Prometheus抓取配置里relabel出来的服务名标签——采集端打的标签决定通知端的信息量,这是整条链路设计的一体性所在
飞书登录(OAuth)
Grafana的登录同样接入飞书,用generic_oauth对接飞书开放平台的OAuth端点,账号体系与公司IM统一:yamlgrafana.ini: auth.generic_oauth: enabled: true name: 飞书 client_id: $FEISHU_APP_ID # CI 注入 client_secret: $FEISHU_APP_SECRET auth_url: https://passport.feishu.cn/suite/passport/oauth/authorize token_url: https://passport.feishu.cn/suite/passport/oauth/token api_url: https://passport.feishu.cn/suite/passport/oauth/userinfo login_attribute_path: name role_attribute_path: name == 'someone' && 'Admin' || 'Viewer' # 按用户名映射角色role_attribute_path实现"白名单管理员、其余只读",避免给全员开编辑权限
链路总结
Pod/Envoy 指标 ──> Prometheus(注解发现/relabel 打标) ──┐
容器日志 ──> Promtail(CRI 解析) ──> Loki(S3/分级保留) ──┤
v
Grafana(数据源/仪表盘/告警,全部 ConfigMap + sidecar 供给)
|
告警规则(interval/for/labels.product)
v
通知策略(repeat_interval) ─> 联系点(webhook + Bearer)
v
自建机器人 ──> 飞书群卡片(title/message 模板)