K8s 全面巡检脚本:一键生成 HTML 健康报告

生产环境的 K8s 集群就像一台高速运转的发动机,每天承载着数以百计的微服务和流量洪峰。没有定期巡检的集群,就像从不做体检的人——平时看起来没事,真出问题往往是大事。

巡检脚本能帮你提前发现:

  • Pod 异常:CrashLoopBackOff、OOMKilled、Pending 卡住
  • 资源告警:节点 CPU/内存飙高、磁盘快满、PVC 容量不足
  • 证书到期:API Server 证书、Ingress TLS 证书即将过期
  • 网络故障:CoreDNS 异常、Service 无 Endpoint、网络策略冲突

架构设计

脚本采用模块化设计,每个检查项独立成函数,最终汇总输出为一份深色仪表盘风格的 HTML 报告。

输入层    kubectl 命令 + metrics-server API + 集群配置
  ↓
处理层    Shell 函数模块:节点/Pod/资源/网络/证书/事件
  ↓
输出层    HTML 报告(深色仪表盘风格)
  ↓
通知层    钉钉 / 企微 / 邮件 告警推送(可选)

巡检模块清单

模块 检查项
节点健康 Ready 状态、CPU/内存使用率、磁盘/内存/PID 压力、版本、内核、Taints 污点
Pod 状态 全 NS 状态汇总、CrashLoopBackOff/OOMKilled、Pending 超时、重启次数超阈值、镜像拉取失败
资源配额 ResourceQuota 使用率、LimitRange 配置、PVC 状态与容量、StorageClass、未设置 requests/limits 的 Pod
网络组件 CoreDNS 副本数与状态、Ingress 规则有效性、Service↔Endpoint 绑定、NetworkPolicy、CNI 插件状态
证书安全 API Server 证书到期、etcd 证书到期、Ingress TLS 证书、ServiceAccount Token、RBAC 权限异常
系统组件 kube-system 全部组件、etcd 健康、Scheduler/Controller 状态、metrics-server、HPA 伸缩策略
异常事件 最近 1 小时 Warning 事件汇总

完整巡检脚本

保存为 k8s_inspect.sh 并赋予执行权限。依赖:kubectl、bc、awk(已预装于大多数 Linux)。

第一部分:脚本头部与全局配置

#!/bin/bash
# =============================================================
# K8s 全面巡检脚本 v2.0
# 功能:全面检查 K8s 集群健康状态,生成 HTML 报告
# 用法:bash k8s_inspect.sh [namespace] [--all-ns]
# 依赖:kubectl、bc、awk(已预装于大多数 Linux)
# =============================================================

set -euo pipefail

# ── 全局配置 ──────────────────────────────────────────────
REPORT_DIR="${REPORT_DIR:-/tmp/k8s-inspect}"
REPORT_FILE="${REPORT_DIR}/k8s_report_$(date +%Y%m%d_%H%M%S).html"
LOG_FILE="${REPORT_DIR}/inspect.log"
NAMESPACE="${1:---all-namespaces}"
THRESHOLD_CPU=80        # CPU 告警阈值(%)
THRESHOLD_MEM=80        # 内存告警阈值(%)
THRESHOLD_DISK=85       # 磁盘告警阈值(%)
THRESHOLD_RESTART=5     # Pod 重启次数告警阈值
CERT_WARN_DAYS=30       # 证书到期提前告警天数

# ── 颜色输出 ──────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# ── 初始化目录 ────────────────────────────────────────────
mkdir -p "$REPORT_DIR"
exec > >(tee -a "$LOG_FILE") 2>&1

# ── 工具函数 ──────────────────────────────────────────────
log_info()  { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
log_title() { echo -e "${BLUE}\n====== $* ======${NC}"; }

第二部分:HTML 报告初始化

# =============================================================
# HTML 报告构建函数
# =============================================================
html_header() {
cat > "$REPORT_FILE" << 'HTMLEOF'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>K8s 巡检报告</title>
<style>
:root{--ok:#00d084;--warn:#f59e0b;--err:#ef4444;--bg:#0f172a;--card:#1e293b;}
body{margin:0;font-family:system-ui;background:var(--bg);color:#e2e8f0;}
.header{background:linear-gradient(135deg,#1e3a5f,#0f172a);
  padding:32px;text-align:center;border-bottom:1px solid #334155;}
.title{font-size:28px;font-weight:900;color:#38bdf8;margin-bottom:8px;}
.subtitle{color:#94a3b8;font-size:14px;}
.summary{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;
  padding:24px;max-width:1200px;margin:0 auto;}
.stat{background:var(--card);border-radius:12px;padding:20px;text-align:center;
  border:1px solid #334155;}
.stat-num{font-size:36px;font-weight:900;margin-bottom:4px;}
.stat-label{font-size:12px;color:#94a3b8;}
.section{max-width:1200px;margin:0 auto 24px;padding:0 24px;}
.section-title{font-size:18px;font-weight:700;color:#38bdf8;
  padding:12px 0;border-bottom:2px solid #334155;margin-bottom:16px;}
table{width:100%;border-collapse:collapse;background:var(--card);
  border-radius:10px;overflow:hidden;}
th{background:#1e3a5f;color:#94a3b8;font-size:12px;padding:10px 14px;text-align:left;}
td{padding:10px 14px;font-size:13px;border-bottom:1px solid #334155;}
tr:last-child td{border-bottom:none;}
.ok{color:var(--ok);font-weight:700;}
.warn{color:var(--warn);font-weight:700;}
.err{color:var(--err);font-weight:700;}
.badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:700;}
.badge-ok{background:rgba(0,208,132,0.15);color:#00d084;}
.badge-warn{background:rgba(245,158,11,0.15);color:#f59e0b;}
.badge-err{background:rgba(239,68,68,0.15);color:#ef4444;}
</style></head>
<body>
HTMLEOF
}

第三部分:节点健康检查

# =============================================================
# 模块一:节点健康检查
# =============================================================
check_nodes() {
    log_title "节点健康检查"

    cat >> "$REPORT_FILE" << 'EOF'
<div class="section">
<div class="section-title">🖥️ 节点健康检查</div>
<table>
<tr><th>节点名称</th><th>状态</th><th>角色</th><th>K8s 版本</th>
<th>CPU 使用</th><th>内存使用</th><th>运行时间</th><th>内核版本</th></tr>
EOF

    while IFS= read -r line; do
        NODE_NAME=$(echo "$line" | awk '{print $1}')
        NODE_STATUS=$(echo "$line" | awk '{print $2}')
        NODE_ROLE=$(kubectl get node "$NODE_NAME" \
            --no-headers -o jsonpath='{.metadata.labels}' \
            | grep -o '"node-role.*?"' | head -1 || echo "worker")
        K8S_VER=$(echo "$line" | awk '{print $5}')
        UPTIME=$(echo "$line" | awk '{print $NF}')

        # 获取 CPU/内存使用率(需 metrics-server)
        CPU_USAGE=$(kubectl top node "$NODE_NAME" --no-headers 2>/dev/null \
            | awk '{print $2}' || echo "N/A")
        MEM_USAGE=$(kubectl top node "$NODE_NAME" --no-headers 2>/dev/null \
            | awk '{print $4}' || echo "N/A")
        KERNEL=$(kubectl get node "$NODE_NAME" -o jsonpath='{.status.nodeInfo.kernelVersion}')

        if [[ "$NODE_STATUS" == "Ready" ]]; then
            STATUS_CLASS="ok"
        else
            STATUS_CLASS="err"
        fi

        echo "<tr>
            <td>${NODE_NAME}</td>
            <td class='${STATUS_CLASS}'>${NODE_STATUS}</td>
            <td>${NODE_ROLE}</td>
            <td>${K8S_VER}</td>
            <td>${CPU_USAGE}</td>
            <td>${MEM_USAGE}</td>
            <td>${UPTIME}</td>
            <td>${KERNEL}</td>
        </tr>" >> "$REPORT_FILE"
    done < <(kubectl get nodes --no-headers)

    echo "</table></div>" >> "$REPORT_FILE"
    log_info "节点检查完成"
}

第四部分:Pod 状态检查

# =============================================================
# 模块二:Pod 状态检查
# =============================================================
check_pods() {
    log_title "Pod 状态检查"

    TOTAL=$(kubectl get pods --all-namespaces --no-headers 2>/dev/null | wc -l)
    RUNNING=$(kubectl get pods --all-namespaces --no-headers 2>/dev/null \
        | grep -c "Running" || true)
    CRASH=$(kubectl get pods --all-namespaces --no-headers 2>/dev/null \
        | grep -c "CrashLoopBackOff\|Error\|OOMKilled" || true)
    PENDING=$(kubectl get pods --all-namespaces --no-headers 2>/dev/null \
        | grep -c "Pending" || true)

    cat >> "$REPORT_FILE" << EOF
<div class="section">
<div class="section-title">🚀 Pod 状态检查</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:16px;">
    <div class="stat"><div class="stat-num ok">${RUNNING}</div>
        <div class="stat-label">Running</div></div>
    <div class="stat"><div class="stat-num err">${CRASH}</div>
        <div class="stat-label">异常 Pod</div></div>
    <div class="stat"><div class="stat-num warn">${PENDING}</div>
        <div class="stat-label">Pending</div></div>
</div>
<table>
<tr><th>命名空间</th><th>Pod 名称</th><th>状态</th>
<th>重启次数</th><th>运行时间</th><th>所在节点</th><th>镜像</th></tr>
EOF

    # 列出所有非正常 Pod
    kubectl get pods --all-namespaces -o wide --no-headers 2>/dev/null \
        | grep -v "Running\|Completed" \
        | while IFS= read -r line; do
        NS=$(echo "$line" | awk '{print $1}')
        NAME=$(echo "$line" | awk '{print $2}')
        READY=$(echo "$line" | awk '{print $3}')
        STATUS=$(echo "$line" | awk '{print $4}')
        RESTARTS=$(echo "$line" | awk '{print $5}')
        AGE=$(echo "$line" | awk '{print $6}')
        NODE=$(echo "$line" | awk '{print $8}')
        IMAGE=$(kubectl get pod "$NAME" -n "$NS" \
            -o jsonpath='{.spec.containers[0].image}' 2>/dev/null || echo "N/A")

        case "$STATUS" in
            CrashLoopBackOff|OOMKilled|Error) STATUS_CLASS="err";;
            Pending|ImagePullBackOff) STATUS_CLASS="warn";;
            *) STATUS_CLASS="warn";;
        esac

        echo "<tr>
            <td>${NS}</td><td>${NAME}</td>
            <td class='${STATUS_CLASS}'>${STATUS}</td>
            <td>${RESTARTS}</td><td>${AGE}</td>
            <td>${NODE}</td>
            <td style='font-size:11px;'>${IMAGE}</td>
        </tr>" >> "$REPORT_FILE"
    done

    echo "</table></div>" >> "$REPORT_FILE"
    log_info "Pod 检查完成"
}

第五部分:资源配额与存储检查

# =============================================================
# 模块三:资源配额检查
# =============================================================
check_resources() {
    log_title "资源配额与 PVC 检查"

    cat >> "$REPORT_FILE" << 'EOF'
<div class="section">
<div class="section-title">💰 资源配额 & 存储检查</div>
<table>
<tr><th>命名空间</th><th>配额名称</th><th>CPU 请求</th>
<th>CPU 限制</th><th>内存请求</th><th>内存限制</th><th>Pod 数量</th></tr>
EOF

    kubectl get resourcequota --all-namespaces --no-headers 2>/dev/null \
        | while IFS= read -r line; do
        NS=$(echo "$line" | awk '{print $1}')
        QN=$(echo "$line" | awk '{print $2}')
        CPU_REQ=$(kubectl describe resourcequota "$QN" -n "$NS" \
            | grep "requests.cpu" | awk '{print $2"/"$3}')
        MEM_REQ=$(kubectl describe resourcequota "$QN" -n "$NS" \
            | grep "requests.memory" | awk '{print $2"/"$3}')
        CPU_LIM=$(kubectl describe resourcequota "$QN" -n "$NS" \
            | grep "limits.cpu" | awk '{print $2"/"$3}')
        MEM_LIM=$(kubectl describe resourcequota "$QN" -n "$NS" \
            | grep "limits.memory" | awk '{print $2"/"$3}')
        PODS=$(kubectl describe resourcequota "$QN" -n "$NS" \
            | grep "^ pods" | awk '{print $2"/"$3}')

        echo "<tr><td>${NS}</td><td>${QN}</td><td>${CPU_REQ}</td>
            <td>${CPU_LIM}</td><td>${MEM_REQ}</td><td>${MEM_LIM}</td>
            <td>${PODS}</td></tr>" >> "$REPORT_FILE"
    done

    echo "</table>" >> "$REPORT_FILE"

    # PVC 状态检查
    cat >> "$REPORT_FILE" << 'EOF'
<br><table>
<tr><th>命名空间</th><th>PVC 名称</th><th>状态</th>
<th>容量</th><th>存储类</th><th>访问模式</th></tr>
EOF

    kubectl get pvc --all-namespaces --no-headers 2>/dev/null \
        | while IFS= read -r line; do
        NS=$(echo "$line" | awk '{print $1}')
        PVC=$(echo "$line" | awk '{print $2}')
        STATUS=$(echo "$line" | awk '{print $3}')
        CAP=$(echo "$line" | awk '{print $5}')
        SC=$(echo "$line" | awk '{print $7}')
        AM=$(echo "$line" | awk '{print $6}')

        if [[ "$STATUS" == "Bound" ]]; then SC_CLASS="ok"; else SC_CLASS="err"; fi

        echo "<tr><td>${NS}</td><td>${PVC}</td>
            <td class='${SC_CLASS}'>${STATUS}</td>
            <td>${CAP}</td><td>${SC}</td><td>${AM}</td></tr>" >> "$REPORT_FILE"
    done

    echo "</table></div>" >> "$REPORT_FILE"
    log_info "资源配额与存储检查完成"
}

第六部分:证书与网络检查

# =============================================================
# 模块四:证书到期检查
# =============================================================
check_certs() {
    log_title "证书到期检查"

    cat >> "$REPORT_FILE" << 'EOF'
<div class="section">
<div class="section-title">🔒 证书安全检查</div>
<table>
<tr><th>证书名称</th><th>到期时间</th><th>剩余天数</th><th>状态</th></tr>
EOF

    # 检查 kubeadm 证书(仅 master 节点有效)
    if command -v kubeadm &>/dev/null; then
        kubeadm certs check-expiration 2>/dev/null \
            | grep -v "^CERTIFICATE\|^!" \
            | while IFS= read -r line; do
            CERT_NAME=$(echo "$line" | awk '{print $1}')
            EXPIRE_DATE=$(echo "$line" | awk '{print $3,$4,$5}')
            RESIDUAL=$(echo "$line" | awk '{print $6}')
            DAYS_LEFT=$(echo "$RESIDUAL" | grep -o '[0-9]*' | head -1 || echo 0)

            if [[ $DAYS_LEFT -le 7 ]]; then CLS="err"
            elif [[ $DAYS_LEFT -le 30 ]]; then CLS="warn"
            else CLS="ok"; fi

            echo "<tr><td>${CERT_NAME}</td><td>${EXPIRE_DATE}</td>
                <td class='${CLS}'>${DAYS_LEFT} 天</td>
                <td><span class='badge badge-${CLS}'>
                $([ $DAYS_LEFT -le 7 ] && echo "紧急" || \
                  ([ $DAYS_LEFT -le 30 ] && echo "警告" || echo "正常"))
                </span></td></tr>" >> "$REPORT_FILE"
        done
    fi

    # 检查 Ingress TLS 证书
    kubectl get secrets --all-namespaces --field-selector type=kubernetes.io/tls \
        --no-headers 2>/dev/null \
        | while IFS= read -r line; do
        NS=$(echo "$line" | awk '{print $1}')
        SEC=$(echo "$line" | awk '{print $2}')
        CERT_DATA=$(kubectl get secret "$SEC" -n "$NS" \
            -o jsonpath='{.data.tls\.crt}' 2>/dev/null | base64 -d)

        if [[ -n "$CERT_DATA" ]]; then
            EXPIRE=$(echo "$CERT_DATA" | openssl x509 -noout -enddate 2>/dev/null \
                | cut -d= -f2)
            EXPIRE_TS=$(date -d "$EXPIRE" +%s 2>/dev/null || echo 0)
            NOW_TS=$(date +%s)
            DAYS_LEFT=$(( (EXPIRE_TS - NOW_TS) / 86400 ))

            if [[ $DAYS_LEFT -le 7 ]]; then CLS="err"
            elif [[ $DAYS_LEFT -le 30 ]]; then CLS="warn"
            else CLS="ok"; fi

            echo "<tr><td>TLS:${NS}/${SEC}</td><td>${EXPIRE}</td>
                <td class='${CLS}'>${DAYS_LEFT} 天</td>
                <td><span class='badge badge-${CLS}'>
                $([ $DAYS_LEFT -le 7 ] && echo "紧急" || \
                  ([ $DAYS_LEFT -le 30 ] && echo "警告" || echo "正常"))
                </span></td></tr>" >> "$REPORT_FILE"
        fi
    done

    echo "</table></div>" >> "$REPORT_FILE"
    log_info "证书检查完成"
}

# =============================================================
# 模块五:网络与 DNS 检查
# =============================================================
check_network() {
    log_title "网络与 DNS 组件检查"

    cat >> "$REPORT_FILE" << 'EOF'
<div class="section">
<div class="section-title">🌐 网络组件检查</div>
<table>
<tr><th>组件</th><th>命名空间</th><th>期望副本</th><th>就绪副本</th><th>状态</th></tr>
EOF

    for comp in coredns kube-proxy calico-node flannel cilium; do
        DS_COUNT=$(kubectl get daemonset,deployment --all-namespaces \
            --no-headers 2>/dev/null | grep "$comp" | wc -l)

        if [[ $DS_COUNT -gt 0 ]]; then
            kubectl get daemonset,deployment --all-namespaces --no-headers \
                2>/dev/null | grep "$comp" | while IFS= read -r line; do
                NS=$(echo "$line" | awk '{print $1}')
                NAME=$(echo "$line" | awk '{print $2}')
                DESIRED=$(echo "$line" | awk '{print $3}')
                READY=$(echo "$line" | awk '{print $4}' 2>/dev/null || echo "?")

                if [[ "$DESIRED" == "$READY" ]]; then CLS="ok"; else CLS="warn"; fi

                echo "<tr><td>${NAME}</td><td>${NS}</td><td>${DESIRED}</td>
                    <td class='${CLS}'>${READY}</td>
                    <td><span class='badge badge-${CLS}'>
                    $([ "$DESIRED" == "$READY" ] && echo "正常" || echo "降级")
                    </span></td></tr>" >> "$REPORT_FILE"
            done
        fi
    done

    echo "</table></div>" >> "$REPORT_FILE"
    log_info "网络检查完成"
}

第七部分:事件检查与主函数

# =============================================================
# 模块六:近期异常事件
# =============================================================
check_events() {
    log_title "近期异常事件"

    cat >> "$REPORT_FILE" << 'EOF'
<div class="section">
<div class="section-title">⚡ 近期异常事件(最近 1 小时)</div>
<table>
<tr><th>时间</th><th>命名空间</th><th>类型</th><th>对象</th><th>原因</th><th>消息</th></tr>
EOF

    kubectl get events --all-namespaces --field-selector type=Warning \
        --sort-by='.lastTimestamp' --no-headers 2>/dev/null \
        | tail -50 | while IFS= read -r line; do
        NS=$(echo "$line" | awk '{print $1}')
        LAST=$(echo "$line" | awk '{print $3}')
        OBJ=$(echo "$line" | awk '{print $5}')
        REASON=$(echo "$line" | awk '{print $6}')
        MSG=$(echo "$line" | awk '{for(i=7;i<=NF;i++) printf $i" "; print ""}' | cut -c1-80)

        echo "<tr><td style='font-size:11px'>${LAST}</td>
            <td>${NS}</td>
            <td><span class='badge badge-warn'>Warning</span></td>
            <td style='font-size:11px'>${OBJ}</td>
            <td class='warn'>${REASON}</td>
            <td style='font-size:11px;color:#94a3b8'>${MSG}</td>
        </tr>" >> "$REPORT_FILE"
    done

    echo "</table></div>" >> "$REPORT_FILE"
    log_info "事件检查完成"
}

# =============================================================
# HTML 页脚 + 主函数入口
# =============================================================
html_footer() {
    cat >> "$REPORT_FILE" << EOF
<div style="text-align:center;padding:32px;color:#475569;font-size:13px;">
巡检时间:$(date "+%Y-%m-%d %H:%M:%S") | 报告由 k8s_inspect.sh 自动生成
</div></body></html>
EOF
}

main() {
    log_info "开始 K8s 全面巡检..."
    log_info "报告路径:$REPORT_FILE"

    html_header

    # 写入概要统计卡
    NODE_TOTAL=$(kubectl get nodes --no-headers 2>/dev/null | wc -l)
    NODE_READY=$(kubectl get nodes --no-headers 2>/dev/null | grep -c "Ready" || true)
    POD_TOTAL=$(kubectl get pods --all-namespaces --no-headers 2>/dev/null | wc -l)
    NS_TOTAL=$(kubectl get ns --no-headers 2>/dev/null | wc -l)

    cat >> "$REPORT_FILE" << EOF
<div class="header">
    <div class="title">🔍 K8s 集群巡检报告</div>
    <div class="subtitle">生成时间:$(date "+%Y-%m-%d %H:%M:%S")</div>
</div>
<div class="summary">
    <div class="stat"><div class="stat-num" style="color:#38bdf8">${NODE_TOTAL}</div>
        <div class="stat-label">总节点数</div></div>
    <div class="stat"><div class="stat-num ok">${NODE_READY}</div>
        <div class="stat-label">就绪节点</div></div>
    <div class="stat"><div class="stat-num" style="color:#a78bfa">${POD_TOTAL}</div>
        <div class="stat-label">总 Pod 数</div></div>
    <div class="stat"><div class="stat-num" style="color:#fb923c">${NS_TOTAL}</div>
        <div class="stat-label">命名空间数</div></div>
</div>
EOF

    # 依次执行各巡检模块
    check_nodes
    check_pods
    check_resources
    check_certs
    check_network
    check_events

    html_footer
    log_info "✅ 巡检完成!报告已生成:$REPORT_FILE"
    echo "$REPORT_FILE"
}

# 执行主函数
main "$@"

快速部署

# 步骤 1:下载或创建脚本
curl -o k8s_inspect.sh https://raw.githubusercontent.com/your-repo/k8s_inspect.sh
# 或直接创建文件,将上面的脚本内容粘贴进去

# 步骤 2:赋予执行权限
chmod +x k8s_inspect.sh

# 步骤 3:执行巡检
bash k8s_inspect.sh

# 步骤 4:自定义输出目录
REPORT_DIR=/data/k8s-reports bash k8s_inspect.sh

# 步骤 5:用 Python 快速预览报告
REPORT_PATH=$(bash k8s_inspect.sh | tail -1)
python3 -m http.server 8888 --directory $(dirname $REPORT_PATH)
# 浏览器打开 http://<master-ip>:8888

# 步骤 6:Nginx 挂载报告目录(可选)
docker run -d --name k8s-report \
    -p 8080:80 \
    -v /tmp/k8s-inspect:/usr/share/nginx/html:ro \
    nginx:alpine
# 浏览器打开 http://<ip>:8080

定时自动巡检(CronJob)

将巡检脚本打包为 Docker 镜像,通过 K8s CronJob 每天自动执行,报告持久化到 PVC。

Dockerfile

FROM alpine:3.19

# 安装 kubectl + 基础工具
RUN apk add --no-cache curl bash openssl bc && \
    curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \
    chmod +x kubectl && mv kubectl /usr/local/bin/

COPY k8s_inspect.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/k8s_inspect.sh

CMD ["k8s_inspect.sh"]

CronJob + RBAC

apiVersion: batch/v1
kind: CronJob
metadata:
  name: k8s-inspect
  namespace: kube-system
spec:
  schedule: "0 8 * * *"    # 每天早上 8:00 执行
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: k8s-inspect-sa
          containers:
          - name: inspect
            image: your-registry/k8s-inspect:latest
            env:
            - name: REPORT_DIR
              value: "/reports"
            volumeMounts:
            - name: reports
              mountPath: "/reports"
          volumes:
          - name: reports
            persistentVolumeClaim:
              claimName: k8s-inspect-pvc
          restartPolicy: OnFailure
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: k8s-inspect-sa
  namespace: kube-system
---
# RBAC:赋予只读权限
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: k8s-inspect-role
rules:
- apiGroups: ["", "apps", "batch", "storage.k8s.io", "networking.k8s.io"]
  resources: ["*"]
  verbs: ["get", "list", "watch"]

部署命令

# 部署 CronJob + RBAC
kubectl apply -f k8s_inspect_cronjob.yaml

# 手动触发一次(测试用)
kubectl create job --from=cronjob/k8s-inspect k8s-inspect-manual -n kube-system

# 查看巡检日志
kubectl logs -l job-name=k8s-inspect-manual -n kube-system -f

# 查看已生成报告列表
kubectl exec -it deploy/nginx-report -n kube-system -- ls /reports/

告警通知集成

巡检完成后,可自动将异常情况推送到钉钉/企业微信/Slack。

# ── 钉钉 Webhook 告警 ─────────────────────────────────────
DINGTALK_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"

send_dingtalk_alert() {
    local msg="$1"
    local title="${2:-K8s 巡检告警}"
    curl -s -X POST "$DINGTALK_WEBHOOK" \
        -H "Content-Type: application/json" \
        -d '{
            "msgtype": "markdown",
            "markdown": {
                "title": "'"$title"'",
                "text": "'"$msg"'"
            }
        }' > /dev/null
}

# ── 企业微信 Webhook 告警 ────────────────────────────────
WECOM_WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"

send_wecom_alert() {
    local msg="$1"
    curl -s -X POST "$WECOM_WEBHOOK" \
        -H "Content-Type: application/json" \
        -d '{
            "msgtype": "markdown",
            "markdown": {"content": "'"$msg"'"}
        }' > /dev/null
}

# ── 在 main() 末尾添加告警汇总 ──────────────────────────
ABNORMAL_PODS=$(kubectl get pods --all-namespaces --no-headers \
    | grep -c "CrashLoopBackOff\|Error\|OOMKilled\|Pending" || true)

if [[ $ABNORMAL_PODS -gt 0 ]]; then
    MSG="## ⚠️ K8s 巡检告警
> 发现 **${ABNORMAL_PODS}** 个异常 Pod,请及时处理!
>
> 巡检时间:$(date '+%Y-%m-%d %H:%M:%S')
> 完整报告:http://your-nginx/reports/latest.html"
    send_dingtalk_alert "$MSG" "K8s 集群异常告警"
    send_wecom_alert "$MSG"
fi

巡检项速查表

类别 检查项
节点 Ready 状态、CPU 使用率、内存使用率、磁盘压力、内存压力、PID 压力、Taints、K8s 版本、内核版本
Pod Running/Pending/Failed 统计、CrashLoopBackOff、OOMKilled、重启次数>N、ImagePullBackOff、Evicted
资源 ResourceQuota 使用率、LimitRange 配置、PVC 状态、StorageClass、无 Limits 的 Pod
网络 CoreDNS 状态、CNI 插件状态、Ingress 规则、Service→Endpoint、NetworkPolicy
证书 API Server 证书、etcd 证书、Ingress TLS、SA Token 状态
组件 etcd 健康、Scheduler 状态、Controller 状态、metrics-server、HPA 伸缩
事件 Warning 事件汇总、OOM 事件、镜像拉取失败事件、调度失败事件