How to Set Up Kubernetes Monitoring with Grafana

How to Set Up Kubernetes Monitoring with Grafana

Nothing exposes an under-monitored cluster like an outage nobody saw coming. Kubernetes will happily reschedule crashing Pods for you all night without a single alert firing if there’s no monitoring stack in place. Grafana, paired with Prometheus as its data source, is the de facto standard for visualizing that data — this guide sets up the full pipeline from metrics collection to dashboards to alerts.

The Monitoring Stack, Conceptually

Grafana itself does not collect metrics — it’s a visualization and alerting layer on top of a time-series database. The standard combination is:

  • Prometheus: scrapes and stores metrics as time series.
  • kube-state-metrics: exposes the state of Kubernetes objects (Deployments, Pods, Nodes) as Prometheus metrics.
  • node-exporter: exposes host-level metrics (CPU, memory, disk) per node.
  • Grafana: queries Prometheus (via PromQL) and renders dashboards.

Installing the Stack via Helm

The kube-prometheus-stack chart bundles all of the above:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

kubectl create namespace monitoring

helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --set grafana.adminPassword=changeme123 \
  --set prometheus.prometheusSpec.retention=15d

Verify the rollout:

kubectl -n monitoring get pods

Output:

NAME                                                     READY   STATUS
kube-prometheus-stack-grafana-6d7f9c8b7f-abcde           3/3     Running
kube-prometheus-stack-kube-state-metrics-7d9c8-fghij     1/1     Running
kube-prometheus-stack-prometheus-node-exporter-klmno     1/1     Running
prometheus-kube-prometheus-stack-prometheus-0            2/2     Running
alertmanager-kube-prometheus-stack-alertmanager-0        2/2     Running

Accessing Grafana

kubectl -n monitoring port-forward svc/kube-prometheus-stack-grafana 3000:80

Then log in at http://localhost:3000 with admin / the password set above. For production, expose it via an Ingress instead of port-forwarding:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: grafana
  namespace: monitoring
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts: ["grafana.example.com"]
      secretName: grafana-tls
  rules:
    - host: grafana.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: kube-prometheus-stack-grafana
                port:
                  number: 80

Prometheus as a Data Source

The Helm chart wires this up automatically, but the manual equivalent (Grafana provisioning YAML) looks like:

apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://kube-prometheus-stack-prometheus.monitoring.svc:9090
    access: proxy
    isDefault: true

Importing Dashboards

The stack ships several prebuilt dashboards, but the two most-used community dashboards worth importing by ID (via Dashboards → Import in the Grafana UI) are:

  • 315 — Kubernetes cluster monitoring (via Prometheus)
  • 6417 — Kubernetes Cluster (Prometheus) — node/pod resource usage
# No CLI equivalent for import — use the Grafana UI, entering the dashboard ID

Writing Custom PromQL Queries

A few examples that map directly to common day-to-day questions:

# Pod restart count over the last hour
increase(kube_pod_container_status_restarts_total[1h])

# CPU usage per pod, in cores
sum(rate(container_cpu_usage_seconds_total{namespace="production"}[5m])) by (pod)

# Memory usage vs limit ratio
sum(container_memory_working_set_bytes{namespace="production"}) by (pod)
  / sum(kube_pod_container_resource_limits{resource="memory", namespace="production"}) by (pod)

# Nodes not ready
kube_node_status_condition{condition="Ready", status="true"} == 0

Alerting

Prometheus Alertmanager (bundled with the stack) handles alert routing. A PrometheusRule CRD defines the alert conditions:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: pod-crashlooping
  namespace: monitoring
  labels:
    release: kube-prometheus-stack
spec:
  groups:
    - name: pod-health
      rules:
        - alert: PodCrashLooping
          expr: increase(kube_pod_container_status_restarts_total[15m]) > 3
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "Pod {{ $labels.pod }} is restarting frequently"
            description: "Namespace {{ $labels.namespace }}, container {{ $labels.container }}"
kubectl apply -f pod-crashlooping-rule.yaml

Route alerts to Slack via Alertmanager config:

apiVersion: v1
kind: Secret
metadata:
  name: alertmanager-config
  namespace: monitoring
stringData:
  alertmanager.yaml: |
    route:
      receiver: slack-notifications
    receivers:
      - name: slack-notifications
        slack_configs:
          - api_url: https://hooks.slack.com/services/XXX/YYY/ZZZ
            channel: "#alerts"

Monitoring Ingress and Services

If using NGINX Ingress, its own metrics endpoint can be scraped for request rate, latency, and error rate dashboards — useful for correlating application-level errors with cluster events:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: nginx-ingress
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: ingress-nginx
  endpoints:
    - port: metrics
      interval: 30s

High Availability for the Monitoring Stack Itself

A monitoring stack that goes down along with the cluster it’s watching defeats its own purpose. For real HA:

  • Run Prometheus with 2+ replicas and remote-write to durable long-term storage (Thanos or Mimir), since local Prometheus storage alone doesn’t survive a Pod loss without a PVC.
  • Use a StorageClass with replicated backing storage for Prometheus PVCs.
  • Run Grafana with more than one replica behind a Service, backed by an external database (Postgres/MySQL) rather than SQLite, so dashboard state survives a Pod restart.
grafana:
  replicas: 2
  persistence:
    enabled: true
  env:
    GF_DATABASE_TYPE: postgres
    GF_DATABASE_HOST: postgres.monitoring.svc:5432

Dashboard Variables for Reusable Views

Hardcoding a namespace or Deployment name into a dashboard means building a new one for every application. Grafana template variables solve this:

Variable name: namespace
Type: Query
Data source: Prometheus
Query: label_values(kube_pod_info, namespace)

Once defined, any panel query can reference $namespace instead of a literal string:

sum(rate(container_cpu_usage_seconds_total{namespace="$namespace"}[5m])) by (pod)

A dropdown appears at the top of the dashboard, letting anyone switch between namespaces without touching a single query — the difference between one dashboard per team building their own copy-pasted variant, and one dashboard the whole organization shares.

Annotations for Correlating Deploys with Metrics

One of the most useful things a monitoring setup can show is when something changed, layered directly onto the metrics graphs. Grafana annotations, pushed automatically from a CI/CD pipeline, do exactly this:

curl -X POST http://grafana.example.com/api/annotations \
  -H "Authorization: Bearer $GRAFANA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Deployed myapp v1.2.0",
    "tags": ["deploy", "myapp"],
    "time": '"$(date +%s000)"'
  }'

Adding this as a final CI stage means every dashboard viewer can instantly see “the error rate spiked right after this deploy” instead of separately cross-referencing deploy timestamps against a graph by hand.

Long-Term Storage Beyond Local Retention

Local Prometheus storage is deliberately not meant to hold years of history — it’s optimized for fast, recent queries. For longer retention (compliance, trend analysis, year-over-year comparisons), Thanos or Grafana Mirror are the standard extensions:

apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: main
  namespace: monitoring
spec:
  retention: 6h
  thanos:
    objectStorageConfig:
      key: thanos.yaml
      name: thanos-objstore-config

Thanos sidecars ship blocks to object storage (S3, GCS) once they age out of local retention, and a Thanos Querier presents a unified view spanning both recent local data and older archived data — all queryable from the same Grafana data source without the dashboard author needing to know which layer actually holds a given time range.

Avoiding Alert Fatigue with SLO-Based Alerting

A monitoring stack that alerts on every minor blip trains people to ignore alerts entirely, which is arguably worse than having no alerting at all. A more durable approach ties alerts to Service Level Objectives — a defined error budget the service is allowed to consume before it’s actually a problem worth waking someone up for, rather than alerting on every individual metric threshold in isolation:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: myapp-slo
  namespace: monitoring
spec:
  groups:
    - name: slo-burn-rate
      rules:
        - alert: ErrorBudgetBurnFast
          expr: |
            (
              sum(rate(http_requests_total{status=~"5..", app="myapp"}[5m]))
              /
              sum(rate(http_requests_total{app="myapp"}[5m]))
            ) > (14.4 * 0.001)
          for: 2m
          labels:
            severity: critical
          annotations:
            summary: "myapp burning error budget fast — page immediately"

The 14.4 * 0.001 figure follows the widely used multi-window burn-rate approach popularized in Google’s SRE workbook — a fast burn rate over a short window pages immediately, while a slower burn rate over a longer window (a separate rule, typically checked hourly rather than every 5 minutes) generates a lower-urgency ticket instead. This two-tier structure is what separates a monitoring setup people actually trust from one that’s eventually muted at the OS notification level out of sheer alert volume.

Correlating Logs Alongside Metrics

Grafana increasingly serves as a single pane for logs too, when paired with Loki rather than a separate log-viewing tool entirely:

helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack --namespace monitoring

Once Loki is added as a second Grafana data source, a dashboard panel showing a CPU or error-rate spike can link directly to the matching log lines for that exact time window and namespace — turning “the graph looks wrong” into “here’s the actual stack trace from that moment” without leaving Grafana at all.

Common Mistakes

  • Leaving Grafana’s default admin password unchanged in a cluster reachable beyond localhost.
  • Setting Prometheus retention far beyond what the attached storage can hold, causing it to silently drop old data or crash-loop on disk pressure.
  • Alerting on raw metric values instead of rates/increases, producing noisy or meaningless thresholds (e.g. alerting on restarts_total directly rather than its rate of increase).
  • Not setting resource limits on Prometheus itself — it’s a memory-hungry process and can starve other workloads on a shared node.

Summary

Grafana turns Prometheus’s raw time-series data into something a human can actually act on: dashboards for at-a-glance health, PromQL for ad-hoc investigation, and Alertmanager-backed rules for catching problems before a customer does. The kube-prometheus-stack Helm chart gets the whole pipeline running in minutes; the real work is in tuning retention, dashboards, and alert thresholds to match what actually matters for your workloads.

References

  • Prometheus documentation: https://prometheus.io/docs/
  • Grafana documentation: https://grafana.com/docs/grafana/latest/
  • kube-prometheus-stack chart: https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack
  • kube-state-metrics: https://github.com/kubernetes/kube-state-metrics
  • CNCF Prometheus project page: https://www.cncf.io/projects/prometheus/
Total
1
Shares

Leave a Reply

Previous Post
How to Use External DNS in Kubernetes

How to Use External DNS in Kubernetes

Next Post
How to Implement PodAffinity in Kubernetes

How to Implement PodAffinity in Kubernetes

Related Posts