Skip to main content

Kubernetes Monitoring β€” Gap Analysis & Fixes

After Phase 21 wired up Alertmanager routing and Phase 8 deployed kube-prometheus-stack, a targeted gap analysis found 5 holes in the monitoring coverage. All 5 are now closed.


Tools β€” Baseline​

ToolStatus
kube-state-metricsβœ… Running (ServiceMonitor kps-kube-state-metrics)
Prometheusβœ… Running (kube-prometheus-stack)
Grafanaβœ… Running (29 dashboards provisioned)

Data Sources​

SourceServiceMonitorScrapingStatus
kube-apiserverkps-apiserverβœ…βœ…
kubeletkps-kubeletβœ…βœ…
CoreDNSkps-corednsβœ…βœ…
schedulerkube-scheduler-proxyβœ…βœ… closed via socat proxy
controller-managerkube-controller-manager-proxyβœ…βœ… closed via socat proxy
etcdN/A β€” see belowβœ…βœ… Kine/SQLite metrics via apiserver scrape

:::info No etcd β€” this cluster uses Kine/SQLite k3s was started without --cluster-init, so there is no embedded etcd process. The datastore is Kine (a SQLite-backed etcd-compatible layer). Kine metrics (kine_sql_total, kine_sql_time_seconds_*, kine_compact_total) are already flowing in Prometheus via the existing kps-apiserver ServiceMonitor β€” no additional scrape config needed. :::


All 5 Gaps β€” Status​

#GapFixResult
1Pod stuck Pending alertPodStuckPending PrometheusRuleβœ… closed β€” gitops a0264c6
2OOMKilled alertContainerOOMKilled PrometheusRuleβœ… closed β€” gitops a0264c6
3Workload Health dashboard9-panel Grafana ConfigMapβœ… closed β€” gitops a0264c6
4Alertmanager receiver (silent alerts)SMTP via Stalwart (Phase 78)βœ… closed
5Scheduler + controller-manager metricssocat DaemonSet proxy on set-hogβœ… closed β€” gitops PR #157

Gap 1 + 2 β€” PrometheusRules​

Added to minicloud-gitops/manifests/monitoring/04-infrastructure-alerts.yaml:

- name: pod-lifecycle
rules:
- alert: PodStuckPending
expr: |
sum by (namespace, pod) (
kube_pod_status_phase{phase="Pending"} == 1
) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} stuck Pending for >5m"
description: |
Common causes: image pull failure, insufficient node resources, PVC not bound.
Diagnose: kubectl describe pod {{ $labels.pod }} -n {{ $labels.namespace }}

- alert: ContainerOOMKilled
expr: |
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
for: 0m
labels:
severity: warning
annotations:
summary: "OOMKill: {{ $labels.container }} in {{ $labels.namespace }}/{{ $labels.pod }}"
description: |
Check VPA recommendations and raise memory limits:
kubectl describe pod {{ $labels.pod }} -n {{ $labels.namespace }}
kubectl get vpa -n {{ $labels.namespace }}

PodStuckPending fires after 5 minutes to avoid startup noise. ContainerOOMKilled fires immediately (for: 0m) β€” an OOMKill is never noise.


Gap 3 β€” Workload Health Overview Dashboard​

minicloud-gitops/manifests/monitoring/05-workload-health-dashboard.yaml β€” Grafana ConfigMap with label grafana_dashboard: "1", sidecar picks it up automatically.

9 panels:

PanelTypeWhat it shows
Running PodsStat (green)Total pods in Running phase
Pending PodsStat (yellow at β‰₯1)Turns yellow immediately
Failed PodsStat (red at β‰₯1)Turns red immediately
CrashLoopBackOffStat (red at β‰₯1)Zero-glance crash signal
CrashLoopBackOff ContainersTablenamespace / pod / container β€” empty = healthy
Top Restarting Pods (1h)Tablesorted by restart count descending
Deployment Missing ReplicasTablespec.replicas - status.replicas_available > 0
OOMKilled ContainersTablelast_terminated_reason == OOMKilled β€” empty = no recent kills
Restart Rate by NamespaceTimeseriesrate(restarts_total[5m]) per namespace

Gap 4 β€” Alertmanager Receiver​

Fixed in Phase 78 (Stalwart mail server). Alertmanager SMTP receiver routes to stalwart.mail.svc.cluster.local:587 with STARTTLS.

Gotcha: Go's smtp.PlainAuth refuses PLAIN on non-TLS connections β€” requires smtp_require_tls: true + smtp_tls_config.insecure_skip_verify: true in the Alertmanager config.


Gap 5 β€” Scheduler / Controller-Manager via Socat Proxy​

gitops PR #157 β€” merged 2026-07-19

Architecture​

[Prometheus] ──scrape──▢ [ClusterIP Service :10269]
β”‚
[Endpoints: 10.0.0.2:10269]
β”‚
[DaemonSet pod on set-hog, hostNetwork: true]
[socat TCP-LISTEN:10269 β†’ TCP:127.0.0.1:10259]
β”‚
[k3s scheduler on 127.0.0.1:10259]

Same flow on port 10267 β†’ 10257 for controller-manager.

Why socat over kube-rbac-proxy​

k3s scheduler and controller-manager serve HTTPS with self-signed certs. kube-rbac-proxy is HTTP-aware and requires TLS config for the upstream. socat in TCP tunnel mode passes bytes blindly β€” HTTPS traverses the tunnel intact, and Prometheus handles TLS with insecureSkipVerify: true. No cert management on the proxy side.

Files​

FilePurpose
manifests/monitoring/07-controlplane-proxy.yamlDaemonSet β€” two socat containers
manifests/monitoring/08-controlplane-services.yamlTwo ClusterIP Services
manifests/monitoring/09-controlplane-servicemonitors.yamlTwo ServiceMonitors

DaemonSet key fields​

spec:
nodeSelector:
kubernetes.io/hostname: set-hog # control-plane only
hostNetwork: true # reach host's 127.0.0.1
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: scheduler-proxy
image: harbor.10.0.0.200.nip.io/library/socat:1.8.0.0
command: [socat, TCP-LISTEN:10269,fork,reuseaddr, TCP:127.0.0.1:10259]
securityContext:
runAsNonRoot: true
runAsUser: 65534 # nobody β€” ports >1024 don't need root
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
seccompProfile: {type: RuntimeDefault}
capabilities: {drop: [ALL]}

ServiceMonitor pattern (matches kps-kubelet exactly)​

endpoints:
- port: https-metrics
scheme: https
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
tlsConfig:
insecureSkipVerify: true # k3s self-signed cert

No changes needed to​

  • Gatekeeper β€” monitoring namespace already excluded from all deny constraints
  • RBAC β€” kps-prometheus ClusterRole already has nonResourceURLs: [/metrics] get
  • kps values β€” serviceMonitorSelectorNilUsesHelmValues: false picks up all ServiceMonitors

Ports​

Componentk3s binds toProxy listens on
scheduler127.0.0.1:102590.0.0.0:10269 on set-hog
controller-manager127.0.0.1:102570.0.0.0:10267 on set-hog

Verification​

# DaemonSet pod running on set-hog
kubectl get pod -n monitoring -l app=controlplane-proxy -o wide
# NAME READY STATUS NODE
# controlplane-proxy-bnggp 2/2 Running set-hog

# Endpoints point to set-hog's IP (hostNetwork pod IP = node IP)
kubectl get endpoints kube-scheduler-proxy kube-controller-manager-proxy -n monitoring
# kube-scheduler-proxy 10.0.0.2:10269
# kube-controller-manager-proxy 10.0.0.2:10267

# Prometheus targets β€” both UP
# scheduler_framework_extension_point_duration_seconds_count β†’ 60 series
# workqueue_depth{job="kube-controller-manager-proxy"} β†’ 93 series

Important Metrics Coverage​

All 9 key operational metrics are actively collected, alerted on, or visible in dashboards.

MetricCollected byAlertDashboard
Pending Podskube-state-metricsPodStuckPending (fires after 5m)Workload Health β€” Pending Pods stat
Failed Podskube-state-metricsβ€”Workload Health β€” Failed Pods stat (red at β‰₯1)
Pod restartskube-state-metricsβ€”Workload Health β€” Top Restarting Pods table
CrashLoopBackOffkube-state-metricsβ€”Workload Health β€” stat + CrashLoop table
Node NotReadykube-state-metricsKubeNodeNotReady (kps built-in)Kubernetes / Compute Resources
PVC usagekubelet (volume_manager)KubePersistentVolumeFilesystemUsage (kps built-in)Kubernetes / Persistent Volumes
Deployment availabilitykube-state-metricsKubeDeploymentReplicasMismatch (kps built-in)Workload Health β€” Deployment Missing Replicas
Replica countkube-state-metricsβ€”Workload Health β€” Deployment Missing Replicas
HPA activitykube-state-metricsβ€”Kubernetes / HPA

Verdict​

:::tip All coverage confirmed βœ…

CategoryResult
Data sources (6/6)kube-apiserver βœ… Β· kubelet βœ… Β· CoreDNS βœ… Β· scheduler βœ… Β· controller-manager βœ… Β· etcd β†’ Kine βœ…
Important metrics (9/9)Pending βœ… Β· Failed βœ… Β· Restarts βœ… Β· CrashLoop βœ… Β· NodeNotReady βœ… Β· PVC βœ… Β· Deployment βœ… Β· Replicas βœ… Β· HPA βœ…
Tools (3/3)kube-state-metrics βœ… Β· Prometheus βœ… Β· Grafana βœ…

:::