Kubernetes 生产环境巡检笔记
1. 巡检目标
Kubernetes 生产巡检的核心不是只确认资源处于 Running,而是同时发现当前故障、冗余失效、容量风险、安全隐患,以及备份无法恢复等潜在问题。
本文附带的脚本以只读方式采集集群信息,不会创建、修改、删除、重启或驱逐任何资源。脚本会生成 Markdown 报告、风险清单和原始证据,适合日常巡检、变更前后核对及故障排查留档。
2. 巡检范围
| 类别 | 主要检查内容 |
|---|---|
| 集群整体 | Kubernetes 版本、节点和工作负载规模、API 就绪状态、Warning 事件 |
| 节点 | Ready 状态、MemoryPressure、DiskPressure、PIDPressure、NetworkUnavailable |
| 工作负载 | Deployment 副本、异常 Pod、容器等待原因、重启次数、单副本部署 |
| 资源容量 | 节点 CPU、内存、资源分配情况;支持 Metrics Server 时自动采集实时指标 |
| 存储 | PV、PVC、StorageClass、VolumeAttachment,以及非 Bound PVC |
| 网络 | Service、EndpointSlice、Ingress、NetworkPolicy,以及无后端地址的 Service |
| 高可用 | 单副本 Deployment、PDB、HPA、etcd 实例数量 |
| 安全 | 高权限容器、HostNetwork、HostPID、latest 镜像、当前身份权限和 ClusterRoleBinding |
| 调度与任务 | ResourceQuota、LimitRange、CronJob、异常事件 |
| 人工复核 | etcd 性能与恢复、磁盘 inode、证书、备份恢复、SLO 和容灾演练 |
3. 风险等级
-
CRITICAL:节点不可用、节点压力、容器启动异常、Deployment 可用副本不足等,需要立即处理。 -
HIGH:异常阶段 Pod、频繁重启、高资源使用率、PVC 或 Service 后端异常等,应尽快处理。 -
MEDIUM:单副本、高权限配置、latest 镜像、Warning 事件或 HA 配置风险。 -
INFO:当前环境无法自动采集,需要通过云平台、Prometheus 或人工方式补充确认。
4. 环境要求
- Bash 3.2 或更高版本。
- 已安装
kubectl,并配置可连接目标集群的 kubeconfig。 - 巡检账号至少应具备目标资源的
get、list权限。 - 建议部署 Metrics Server,以便采集节点和容器的 CPU、内存数据。
- 脚本默认单次请求超时为 15 秒,可通过参数调整。
5. 使用方法
先为脚本添加执行权限:
chmod +x k8s-prod-inspection.sh
巡检当前 kubeconfig 所选集群的所有命名空间:
./k8s-prod-inspection.sh
指定生产集群 Context:
./k8s-prod-inspection.sh --context production
只巡检指定命名空间:
./k8s-prod-inspection.sh --context production --namespace payment
指定报告目录及请求超时:
./k8s-prod-inspection.sh \
--context production \
--output-dir ./inspection-report \
--timeout 30s
查看全部参数:
./k8s-prod-inspection.sh --help
建议先确认当前 Context,避免误查其他集群:
kubectl config current-context
kubectl cluster-info
6. 输出说明
未指定输出目录时,脚本在当前目录生成带时间戳的目录:
k8s-inspection-20260916-120000/
├── report.md # 汇总报告及风险建议
├── findings.tsv # 便于导入表格的风险清单
└── raw/ # 各项 kubectl 原始输出
建议首先查看 report.md,再根据其中的证据路径核对 raw/。巡检报告包含敏感的集群拓扑、资源名称和权限信息,应按内部安全规范保存与传输。
7. 完整脚本
将以下内容保存为 k8s-prod-inspection.sh:
#!/usr/bin/env bash
# Kubernetes production inspection (read-only)
# Usage: ./k8s-prod-inspection.sh [-c context] [-n namespace|all] [-o output-dir]
set -u
set -o pipefail
CONTEXT=""
NAMESPACE="all"
OUTPUT_DIR=""
TIMEOUT="15s"
WARN_DISK=75
CRIT_DISK=85
WARN_CPU=70
WARN_MEM=80
CERT_WARN_DAYS=60
usage() {
cat <<'EOF'
Usage: k8s-prod-inspection.sh [options]
-c, --context NAME kubeconfig context (default: current context)
-n, --namespace NAME namespace to inspect (default: all)
-o, --output-dir DIR report directory (default: ./k8s-inspection-TIMESTAMP)
-t, --timeout DURATION kubectl request timeout (default: 15s)
-h, --help show help
This script is read-only. It does not create, update, delete, restart, or drain resources.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-c|--context) CONTEXT="${2:?missing context}"; shift 2 ;;
-n|--namespace) NAMESPACE="${2:?missing namespace}"; shift 2 ;;
-o|--output-dir) OUTPUT_DIR="${2:?missing output directory}"; shift 2 ;;
-t|--timeout) TIMEOUT="${2:?missing timeout}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
done
command -v kubectl >/dev/null 2>&1 || { echo "ERROR: kubectl is required" >&2; exit 1; }
STAMP="$(date '+%Y%m%d-%H%M%S')"
OUTPUT_DIR="${OUTPUT_DIR:-./k8s-inspection-${STAMP}}"
mkdir -p "$OUTPUT_DIR/raw"
REPORT="$OUTPUT_DIR/report.md"
FINDINGS="$OUTPUT_DIR/findings.tsv"
: >"$FINDINGS"
K=(kubectl --request-timeout="$TIMEOUT")
[[ -n "$CONTEXT" ]] && K+=(--context "$CONTEXT")
NS_ARGS=(-A)
[[ "$NAMESPACE" != "all" ]] && NS_ARGS=(-n "$NAMESPACE")
run() { "${K[@]}" "$@"; }
capture() {
local name="$1"; shift
run "$@" >"$OUTPUT_DIR/raw/$name.txt" 2>&1
}
finding() {
# severity, item, evidence, recommendation
printf '%s\t%s\t%s\t%s\n' "$1" "$2" "${3//$'\t'/ }" "${4//$'\t'/ }" >>"$FINDINGS"
}
section() { printf '\n## %s\n\n' "$1" >>"$REPORT"; }
code_file() {
local title="$1" file="$2"
printf '### %s\n\n```text\n' "$title" >>"$REPORT"
sed -n '1,200p' "$file" >>"$REPORT" 2>/dev/null || true
printf '\n```\n\n' >>"$REPORT"
}
CURRENT_CONTEXT="${CONTEXT:-$(kubectl config current-context 2>/dev/null || true)}"
if ! run version -o yaml >"$OUTPUT_DIR/raw/version.txt" 2>&1; then
echo "ERROR: cannot connect to context '${CURRENT_CONTEXT:-unknown}'. See $OUTPUT_DIR/raw/version.txt" >&2
exit 1
fi
cat >"$REPORT" <<EOF
# Kubernetes 生产环境巡检报告
- 时间:$(date '+%Y-%m-%d %H:%M:%S %z')
- Context:${CURRENT_CONTEXT:-unknown}
- 巡检范围:${NAMESPACE}
- 模式:只读
- 原始证据目录:raw/
EOF
# Inventory and component health
capture nodes get nodes -o wide || finding CRITICAL "无法读取节点" "kubectl get nodes 失败" "检查连接与 RBAC 权限"
capture namespaces get namespaces || true
capture pods get pods "${NS_ARGS[@]}" -o wide || finding HIGH "无法读取 Pod" "kubectl get pods 失败" "检查 RBAC 权限"
capture workloads get deploy,statefulset,daemonset "${NS_ARGS[@]}" || true
capture events get events "${NS_ARGS[@]}" --field-selector type=Warning --sort-by=.lastTimestamp || true
capture component_health get --raw=/readyz?verbose || true
capture apiservices get apiservices || true
NOT_READY="$(run get nodes --no-headers 2>/dev/null | awk '$2 !~ /^Ready/ {print $1":"$2}' | paste -sd, -)"
[[ -n "$NOT_READY" ]] && finding CRITICAL "节点非 Ready" "$NOT_READY" "检查 kubelet、容器运行时、网络和节点压力"
PRESSURE="$(run get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{range .status.conditions[?(@.status=="True")]}{.type}{" "}{end}{"\n"}{end}' 2>/dev/null | awk '$0 ~ /(MemoryPressure|DiskPressure|PIDPressure|NetworkUnavailable)/')"
[[ -n "$PRESSURE" ]] && finding CRITICAL "节点存在压力状态" "$(echo "$PRESSURE" | paste -sd';' -)" "立即检查节点容量、磁盘、PID 和网络"
# Pod failures/restarts
capture bad_pods get pods "${NS_ARGS[@]}" --field-selector=status.phase!=Running,status.phase!=Succeeded -o wide || true
BAD_PODS="$(run get pods "${NS_ARGS[@]}" --no-headers --field-selector=status.phase!=Running,status.phase!=Succeeded 2>/dev/null | wc -l | tr -d ' ')"
[[ "${BAD_PODS:-0}" -gt 0 ]] && finding HIGH "异常阶段 Pod" "${BAD_PODS} 个,详见 raw/bad_pods.txt" "检查调度、镜像、存储和容器日志"
RESTARTS="$(run get pods "${NS_ARGS[@]}" -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{" "}{range .status.containerStatuses[*]}{.name}{":"}{.restartCount}{" "}{end}{"\n"}{end}' 2>/dev/null | awk '{bad=0; for(i=2;i<=NF;i++){split($i,a,":"); if(a[2]+0>=5) bad=1} if(bad) print}')"
if [[ -n "$RESTARTS" ]]; then
printf '%s\n' "$RESTARTS" >"$OUTPUT_DIR/raw/high_restarts.txt"
finding HIGH "容器重启次数较高" "存在 restartCount >= 5 的 Pod" "检查退出码、OOM、探针和应用日志"
fi
WAITING="$(run get pods "${NS_ARGS[@]}" -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{" "}{range .status.containerStatuses[*]}{.state.waiting.reason}{" "}{end}{"\n"}{end}' 2>/dev/null | grep -E 'CrashLoopBackOff|ImagePullBackOff|ErrImagePull|CreateContainerConfigError' || true)"
if [[ -n "$WAITING" ]]; then
printf '%s\n' "$WAITING" >"$OUTPUT_DIR/raw/waiting_errors.txt"
finding CRITICAL "容器等待异常" "CrashLoop/ImagePull/配置错误,详见 raw/waiting_errors.txt" "检查镜像、Secret、配置和应用启动日志"
fi
# Workload availability
UNAVAILABLE="$(run get deploy "${NS_ARGS[@]}" -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{" desired="}{.spec.replicas}{" available="}{.status.availableReplicas}{"\n"}{end}' 2>/dev/null | awk '{split($2,a,"="); split($3,b,"="); if((b[2]+0)<(a[2]+0)) print}')"
if [[ -n "$UNAVAILABLE" ]]; then
printf '%s\n' "$UNAVAILABLE" >"$OUTPUT_DIR/raw/unavailable_deployments.txt"
finding CRITICAL "Deployment 副本不足" "详见 raw/unavailable_deployments.txt" "检查 Pod、调度、探针和滚动发布状态"
fi
# Resource metrics (optional)
if capture node_metrics top nodes; then
awk -v wc="$WARN_CPU" -v wm="$WARN_MEM" 'NR>1 {gsub("%","",$3); gsub("%","",$5); if($3+0>=wc || $5+0>=wm) print}' "$OUTPUT_DIR/raw/node_metrics.txt" >"$OUTPUT_DIR/raw/high_node_usage.txt"
[[ -s "$OUTPUT_DIR/raw/high_node_usage.txt" ]] && finding HIGH "节点资源使用率偏高" "CPU >= ${WARN_CPU}% 或内存 >= ${WARN_MEM}%" "扩容或调整请求值,并分析容量趋势"
capture pod_metrics top pods "${NS_ARGS[@]}" --containers || true
else
finding INFO "资源指标不可用" "kubectl top nodes 失败" "安装或修复 metrics-server;生产监控建议接入 Prometheus"
fi
# Capacity, storage, networking and policy
capture node_capacity describe nodes || true
capture pvc get pvc "${NS_ARGS[@]}" -o wide || true
capture pv get pv -o wide || true
capture storageclasses get storageclass || true
capture volumeattachments get volumeattachments || true
capture ingress get ingress "${NS_ARGS[@]}" -o wide || true
capture services get services "${NS_ARGS[@]}" -o wide || true
capture endpoint_slices get endpointslices.discovery.k8s.io "${NS_ARGS[@]}" || true
capture network_policies get networkpolicies "${NS_ARGS[@]}" || true
BAD_PVC="$(run get pvc "${NS_ARGS[@]}" --no-headers 2>/dev/null | awk '$2 !~ /^Bound$/ {print $1"/"$2":"$3}' | paste -sd, -)"
[[ -n "$BAD_PVC" ]] && finding HIGH "PVC 非 Bound" "$BAD_PVC" "检查 StorageClass、CSI、容量和拓扑约束"
EMPTY_EP="$(run get endpointslices.discovery.k8s.io "${NS_ARGS[@]}" -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.labels.kubernetes\.io/service-name}{" "}{.endpoints[*].addresses}{"\n"}{end}' 2>/dev/null | awk 'NF==1 {print $1}' | paste -sd, -)"
[[ -n "$EMPTY_EP" ]] && finding HIGH "Service 无后端地址" "$EMPTY_EP" "检查 Service selector、Pod readiness 和端口配置"
# HA/config/security posture
capture pdb get pdb "${NS_ARGS[@]}" -o wide || true
capture hpa get hpa "${NS_ARGS[@]}" || true
capture quotas get resourcequota "${NS_ARGS[@]}" || true
capture limitranges get limitrange "${NS_ARGS[@]}" || true
capture cronjobs get cronjob "${NS_ARGS[@]}" || true
capture cert_requests get certificatesigningrequests || true
SINGLETONS="$(run get deploy "${NS_ARGS[@]}" -o jsonpath='{range .items[?(@.spec.replicas==1)]}{.metadata.namespace}{"/"}{.metadata.name}{"\n"}{end}' 2>/dev/null)"
if [[ -n "$SINGLETONS" ]]; then
printf '%s\n' "$SINGLETONS" >"$OUTPUT_DIR/raw/single_replica_deployments.txt"
finding MEDIUM "单副本 Deployment" "详见 raw/single_replica_deployments.txt" "确认是否为关键业务;关键业务建议多副本并跨故障域"
fi
PRIVILEGED="$(run get pods "${NS_ARGS[@]}" -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{" hostNetwork="}{.spec.hostNetwork}{" hostPID="}{.spec.hostPID}{" "}{range .spec.containers[*]}{.name}{":privileged="}{.securityContext.privileged}{" "}{end}{"\n"}{end}' 2>/dev/null | grep -E 'hostNetwork=true|hostPID=true|privileged=true' || true)"
if [[ -n "$PRIVILEGED" ]]; then
printf '%s\n' "$PRIVILEGED" >"$OUTPUT_DIR/raw/privileged_pods.txt"
finding MEDIUM "高权限 Pod" "详见 raw/privileged_pods.txt" "确认业务必要性并采用最小权限、安全上下文和准入策略"
fi
LATEST="$(run get pods "${NS_ARGS[@]}" -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{" "}{.spec.containers[*].image}{"\n"}{end}' 2>/dev/null | grep -E '(^|[[:space:]])[^[:space:]]+:latest([[:space:]]|$)' || true)"
if [[ -n "$LATEST" ]]; then
printf '%s\n' "$LATEST" >"$OUTPUT_DIR/raw/latest_images.txt"
finding MEDIUM "使用 latest 镜像标签" "详见 raw/latest_images.txt" "固定不可变版本,关键场景使用镜像 digest"
fi
# Recent warning events
WARNINGS="$(run get events "${NS_ARGS[@]}" --field-selector type=Warning --no-headers 2>/dev/null | wc -l | tr -d ' ')"
[[ "${WARNINGS:-0}" -gt 0 ]] && finding MEDIUM "存在 Warning 事件" "${WARNINGS} 条,详见 raw/events.txt" "按原因聚合,优先处理持续或反复出现的事件"
# Optional etcd visibility; works for common kubeadm static-pod deployments.
capture etcd_pods get pods -n kube-system -l component=etcd -o wide || true
ETCD_COUNT="$(run get pods -n kube-system -l component=etcd --no-headers 2>/dev/null | wc -l | tr -d ' ')"
if [[ "${ETCD_COUNT:-0}" -eq 0 ]]; then
finding INFO "etcd 无法从集群内直接巡检" "可能为托管控制面或标签不同" "从云平台/控制面监控确认 etcd 健康、容量、延迟和备份恢复"
elif [[ "$ETCD_COUNT" -lt 3 ]]; then
finding MEDIUM "etcd 成员数量少于 3" "发现 ${ETCD_COUNT} 个带 component=etcd 标签的 Pod" "核实生产控制面的高可用设计"
fi
# RBAC checks are best effort.
if run auth can-i --list -A >"$OUTPUT_DIR/raw/current_identity_permissions.txt" 2>&1; then :; fi
capture clusterrolebindings get clusterrolebindings -o wide || true
# Report summary
section "风险摘要"
if [[ ! -s "$FINDINGS" ]]; then
echo "未发现脚本可识别的异常;仍需结合业务 SLO、云平台、存储和备份系统检查。" >>"$REPORT"
else
printf '| 级别 | 检查项 | 证据 | 建议 |\n|---|---|---|---|\n' >>"$REPORT"
awk -F '\t' '{gsub(/\|/,"\\|",$2); gsub(/\|/,"\\|",$3); gsub(/\|/,"\\|",$4); printf "| %s | %s | %s | %s |\n",$1,$2,$3,$4}' "$FINDINGS" >>"$REPORT"
fi
section "统计"
NODE_TOTAL="$(run get nodes --no-headers 2>/dev/null | wc -l | tr -d ' ')"
POD_TOTAL="$(run get pods "${NS_ARGS[@]}" --no-headers 2>/dev/null | wc -l | tr -d ' ')"
printf -- '- 节点:%s\n- Pod:%s\n- 异常阶段 Pod:%s\n- Warning 事件:%s\n' "$NODE_TOTAL" "$POD_TOTAL" "${BAD_PODS:-0}" "${WARNINGS:-0}" >>"$REPORT"
section "核心状态(最多显示 200 行)"
code_file "节点" "$OUTPUT_DIR/raw/nodes.txt"
code_file "异常 Pod" "$OUTPUT_DIR/raw/bad_pods.txt"
code_file "工作负载" "$OUTPUT_DIR/raw/workloads.txt"
code_file "节点资源" "$OUTPUT_DIR/raw/node_metrics.txt"
section "人工补充检查"
cat >>"$REPORT" <<EOF
- etcd endpoint health、WAL fsync 延迟、数据库配额/碎片率,以及快照恢复演练。
- API Server、Scheduler、Controller Manager 的 P95/P99 延迟、错误率、限流和队列深度。
- 节点磁盘使用率(预警 ${WARN_DISK}% / 严重 ${CRIT_DISK}%)、inode、I/O 延迟、conntrack、时间同步。
- Ingress/Gateway 的流量、4xx/5xx、P95/P99 延迟、证书剩余天数(建议低于 ${CERT_WARN_DAYS} 天告警)。
- CSI/存储的容量、IOPS、时延、快照,以及真实的数据恢复验证。
- Prometheus、日志和告警链路自身健康;7/30/90 天容量趋势。
- RBAC 通配符和 cluster-admin 授权、镜像漏洞/签名、Secret 管理、审计日志。
- 节点或可用区故障演练,以及业务 RPO/RTO 验证。
EOF
echo "Inspection complete: $REPORT"
echo "Raw evidence: $OUTPUT_DIR/raw"
exit 0
8. 无法完全自动化的检查
通用 kubectl 脚本无法可靠覆盖所有生产指标。下列内容通常需要 Prometheus、云平台、存储平台、证书系统或备份系统配合:
- etcd 的 endpoint health、Leader 变化、WAL fsync 延迟、数据库配额、碎片率和快照恢复。
- API Server、Scheduler、Controller Manager 的 P95/P99 延迟、错误率、限流和队列深度。
- 主机磁盘使用率、inode、I/O 延迟、conntrack、系统负载和时间同步。
- Ingress/Gateway 的流量、4xx/5xx、延迟、上游失败和 TLS 证书剩余有效期。
- CSI 和存储系统的容量、IOPS、吞吐、延迟、快照及真实数据恢复。
- 镜像漏洞、签名和来源可信度,Secret 外部托管及审计日志留存。
- Prometheus、日志与告警链路自身的健康状态,以及 7/30/90 天容量趋势。
- 节点、可用区和整个集群级别的故障演练,以及业务 RPO/RTO 验证。
因此,脚本报告应作为自动化基线,而不是生产验收或容灾结论的唯一依据。
9. 建议执行频率
| 频率 | 建议内容 |
|---|---|
| 实时监控 | 节点和 Pod 状态、组件存活、错误率、延迟、资源耗尽和证书告警 |
| 每日 | 执行本脚本,检查 Warning 事件、异常 Pod、重启、PVC、备份结果 |
| 每周 | 容量趋势、资源请求与限制、HPA、PDB、权限及公网暴露面 |
| 每月 | 版本、补丁、漏洞、证书、配置漂移、容量预测和恢复验证 |
| 每季度 | etcd/PV 恢复、节点或可用区故障、完整容灾演练 |
10. 巡检闭环建议
对每项异常至少记录:发现时间、风险等级、当前值、阈值、趋势、影响范围、证据、整改负责人、计划完成时间和复核结果。只有形成整改与复核闭环,巡检才真正发挥生产保障作用。