← Back to DevBytes

Monitoring EKS: Metrics, Alarms, and Dashboards

Introduction to Amazon EKS Monitoring

Monitoring an Amazon EKS (Elastic Kubernetes Service) cluster is the practice of collecting, analyzing, and acting on metrics from your Kubernetes control plane, worker nodes, pods, and the applications running within them. EKS abstracts the control plane management, but you remain responsible for the health, performance, and availability of your workloads and the underlying infrastructure they depend on.

A robust monitoring strategy gives you visibility into resource utilization, application performance, and cluster stability. Without it, you operate blind — unable to detect degraded services, anticipate capacity shortages, or troubleshoot failures efficiently. The goal is to move from reactive firefighting to proactive observability, where you know about problems before your users do.

Why Monitoring EKS Matters

Amazon EKS clusters are dynamic, distributed systems. Nodes are replaced by auto-scaling events, pods are rescheduled, and services are discovered and consumed in real time. This fluidity makes traditional host-based monitoring inadequate. You need Kubernetes-native observability that tracks:

Effective monitoring also feeds directly into your incident response pipeline. Alarms triggered by metric thresholds route to on-call engineers via PagerDuty, Opsgenie, or Slack, enabling rapid triage and resolution.

Key Metrics to Monitor in EKS

Cluster-Level Metrics

These metrics reflect the overall health and capacity of your Kubernetes cluster. They come from the Kubernetes API server, kube-state-metrics, and the metrics-server:

Node-Level Metrics

Worker node metrics are critical for capacity planning and detecting resource exhaustion. These are typically collected by the node-exporter daemonset or CloudWatch agent:

Pod and Container Metrics

Pod-level metrics tell you how individual workloads are performing and consuming resources:

Application Metrics

Beyond infrastructure, your applications emit RED (Rate, Errors, Duration) or USE (Utilization, Saturation, Errors) signals. Instrument your code with libraries like Prometheus client_java, client_python, or OpenTelemetry SDKs to expose:

Setting Up Monitoring Infrastructure

Option 1: CloudWatch Container Insights

CloudWatch Container Insights provides built-in, agent-based monitoring for EKS. It collects cluster, node, pod, and service metrics without managing your own Prometheus stack. To enable it, you deploy the CloudWatch agent as a DaemonSet and configure Fluent Bit for logs.

First, attach the necessary IAM policy to your worker node role:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "cloudwatch:PutMetricData",
        "logs:CreateLogStream",
        "logs:DescribeLogStreams",
        "logs:PutLogEvents",
        "logs:CreateLogGroup"
      ],
      "Resource": "*"
    }
  ]
}

Then deploy the CloudWatch agent using the provided YAML manifest. The following command applies the pre-configured quick-start configuration for EKS:

# Download the manifest from AWS
curl -O https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/cwagent/cwagent-daemonset-quickstart.yaml

# Apply it to your cluster
kubectl apply -f cwagent-daemonset-quickstart.yaml

# Verify the agent pods are running
kubectl -n amazon-cloudwatch get pods -l app=cwagent

Once running, Container Insights populates the ContainerInsights namespace in CloudWatch Metrics. You can query metrics like pod_cpu_utilization, node_memory_utilization, and service_number_of_running_pods directly in the CloudWatch console.

Option 2: Prometheus and Grafana (Self-Managed)

For teams that need richer querying, longer retention, or multi-cluster aggregation, the Prometheus + Grafana stack is the gold standard. The kube-prometheus-stack Helm chart bundles Prometheus, Alertmanager, Grafana, node-exporter, and kube-state-metrics into a single deployment.

Add the Helm repository and install the stack:

# Add the Prometheus community Helm repo
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Create a namespace for monitoring
kubectl create namespace monitoring

# Install the kube-prometheus-stack with custom values
helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --set prometheus.retention=15d \
  --set grafana.service.type=LoadBalancer \
  --set alertmanager.config.global.resolve_timeout=5m \
  --values custom-values.yaml

Create a custom-values.yaml to fine-tune scraping intervals, retention, and storage:

prometheus:
  prometheusSpec:
    retentionSize: "50GB"
    scrapeInterval: "30s"
    evaluationInterval: "30s"
    storageSpec:
      volumeClaimTemplate:
        spec:
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 100Gi
  
grafana:
  adminPassword: "secure-admin-password"
  persistence:
    enabled: true
    size: 10Gi
  
alertmanager:
  alertmanagerSpec:
    replicas: 2
    externalUrl: "http://alertmanager.example.com"

After installation, retrieve the Grafana admin password and expose the service:

# Get the admin password
kubectl get secret -n monitoring monitoring-grafana \
  -o jsonpath="{.data.admin-password}" | base64 --decode

# Port-forward for local access
kubectl port-forward -n monitoring svc/monitoring-grafana 8080:80

# Or get the LoadBalancer endpoint
kubectl get svc -n monitoring monitoring-grafana \
  -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"

Service Monitor Configuration

To scrape your own application metrics, define a ServiceMonitor custom resource. This tells Prometheus to scrape pods labeled with specific selectors:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-app-monitor
  namespace: monitoring
  labels:
    release: monitoring
spec:
  selector:
    matchLabels:
      app: my-application
  namespaceSelector:
    matchNames:
      - production
  endpoints:
    - port: metrics
      interval: 30s
      path: /metrics
      relabelings:
        - sourceLabels: [__meta_kubernetes_pod_label_app]
          targetLabel: application
        - sourceLabels: [__meta_kubernetes_namespace]
          targetLabel: namespace

Creating Meaningful Alarms

Alarms translate metric thresholds into actionable notifications. A well-designed alarm is specific, has a clear severity level, and includes runbook links in its notification body.

CloudWatch Alarms

With Container Insights enabled, you can create CloudWatch alarms on any metric in the ContainerInsights namespace. Here's how to create an alarm for high node CPU utilization using the AWS CLI:

# Create an alarm for node CPU exceeding 85% for 5 consecutive minutes
aws cloudwatch put-metric-alarm \
  --alarm-name "EKS-Node-CPU-Critical" \
  --alarm-description "Node CPU utilization exceeds 85% for 5 minutes" \
  --namespace "ContainerInsights" \
  --metric-name "node_cpu_utilization" \
  --statistic "Average" \
  --period 300 \
  --evaluation-periods 1 \
  --threshold 85 \
  --comparison-operator "GreaterThanThreshold" \
  --treat-missing-data "breaching" \
  --dimensions "Name=ClusterName,Value=my-eks-cluster" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:ops-critical" \
  --tags "Environment=production,Team=platform"

For pod-level alarms, target metrics like pod_cpu_utilization or pod_number_of_container_restarts. The following alarm detects pods in crash loop:

# Alarm for excessive pod restarts
aws cloudwatch put-metric-alarm \
  --alarm-name "EKS-Pod-Restart-Rate-High" \
  --alarm-description "Pod restart rate exceeds threshold" \
  --namespace "ContainerInsights" \
  --metric-name "pod_number_of_container_restarts" \
  --statistic "Sum" \
  --period 300 \
  --evaluation-periods 2 \
  --threshold 5 \
  --comparison-operator "GreaterThanThreshold" \
  --treat-missing-data "notBreaching" \
  --dimensions "Name=ClusterName,Value=my-eks-cluster" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:ops-warning"

Prometheus AlertManager Rules

When using Prometheus, define alerting rules in PrometheusRule custom resources. These rules evaluate PromQL expressions and fire alerts to Alertmanager, which then routes them to email, Slack, PagerDuty, or webhook receivers.

Create a file named critical-alerts.yaml with rules for common failure scenarios:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: eks-critical-alerts
  namespace: monitoring
  labels:
    release: monitoring
    severity: critical
spec:
  groups:
    - name: node-alerts
      rules:
        - alert: NodeHighCPUUsage
          expr: |
            100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
          for: 10m
          labels:
            severity: critical
          annotations:
            summary: "Node CPU usage exceeds 90%"
            description: "Node {{ $labels.instance }} has CPU usage above 90% for 10 minutes"
            runbook_url: "https://confluence.example.com/runbooks/node-cpu"

        - alert: NodeMemoryPressure
          expr: |
            (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100) < 10
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "Node memory available below 10%"
            description: "Node {{ $labels.instance }} has less than 10% memory available"
            runbook_url: "https://confluence.example.com/runbooks/node-memory"

        - alert: NodeDiskPressure
          expr: |
            (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} * 100) < 15
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "Node disk space below 15%"
            description: "Node {{ $labels.instance }} root filesystem has less than 15% free space"

    - name: pod-alerts
      rules:
        - alert: PodCrashLooping
          expr: |
            rate(kube_pod_container_status_restarts_total[15m]) > 0
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "Pod {{ $labels.pod }} is crash looping"
            description: "Container {{ $labels.container }} in pod {{ $labels.pod }} has been restarting repeatedly"
            runbook_url: "https://confluence.example.com/runbooks/pod-crashloop"

        - alert: PodOOMKilled
          expr: |
            increase(container_oom_kill_total[5m]) > 0
          labels:
            severity: critical
          annotations:
            summary: "Pod OOMKilled in namespace {{ $labels.namespace }}"
            description: "Container {{ $labels.container }} in pod {{ $labels.pod }} was killed by OOM"

        - alert: DeploymentReplicasMismatch
          expr: |
            kube_deployment_spec_replicas != kube_deployment_status_replicas_available
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "Deployment {{ $labels.deployment }} has mismatched replicas"
            description: "Expected {{ $labels.deployment }} replicas but available replicas differ"

    - name: cluster-alerts
      rules:
        - alert: HighAPIServerLatency
          expr: |
            histogram_quantile(0.99, rate(apiserver_request_duration_seconds_bucket[5m])) > 1
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "High API server latency detected"
            description: "99th percentile API server latency exceeds 1 second"

        - alert: ManyPendingPods
          expr: |
            sum(kube_pod_status_phase{phase="Pending"}) > 10
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "More than 10 pods stuck in Pending state"
            description: "Cluster may be experiencing scheduling issues or resource shortage"

Apply the alert rules to your cluster:

kubectl apply -f critical-alerts.yaml

# Verify the rules are loaded
kubectl -n monitoring get prometheusrule eks-critical-alerts -o yaml

# Check Prometheus for the loaded rules
kubectl -n monitoring exec -it prometheus-monitoring-kube-prometheus-0 -- \
  curl -s http://localhost:9090/api/v1/rules | jq '.data.groups[].rules[].name'

Configuring Alertmanager Receivers

Alertmanager handles routing, deduplication, and notification delivery. Configure receivers for your team's communication channels in the alertmanager.yaml secret:

apiVersion: v1
kind: Secret
metadata:
  name: alertmanager-monitoring-kube-prometheus-alertmanager
  namespace: monitoring
stringData:
  alertmanager.yaml: |
    global:
      resolve_timeout: 5m
      slack_api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
    
    route:
      group_by: ['alertname', 'severity']
      group_wait: 30s
      group_interval: 5m
      repeat_interval: 4h
      receiver: 'slack-critical'
      routes:
        - match:
            severity: critical
          receiver: 'pagerduty-critical'
          continue: true
        - match:
            severity: warning
          receiver: 'slack-warning'
    
    receivers:
      - name: 'slack-critical'
        slack_configs:
          - channel: '#ops-critical'
            title: '{{ .CommonLabels.alertname }}'
            text: |
              *Alert:* {{ .CommonAnnotations.summary }}
              *Description:* {{ .CommonAnnotations.description }}
              *Severity:* {{ .CommonLabels.severity }}
              *Runbook:* {{ .CommonAnnotations.runbook_url }}
              *Alerts:* {{ range .Alerts }}{{ .Annotations.description }}{{ end }}
      
      - name: 'pagerduty-critical'
        pagerduty_configs:
          - service_key: 'your-pagerduty-integration-key'
            severity: critical
            details:
              runbook: '{{ .CommonAnnotations.runbook_url }}'
      
      - name: 'slack-warning'
        slack_configs:
          - channel: '#ops-warnings'
            title: 'Warning: {{ .CommonLabels.alertname }}'
            text: '{{ .CommonAnnotations.description }}'

Apply the updated secret and verify Alertmanager picks up the configuration:

kubectl apply -f alertmanager-secret.yaml

# Trigger a configuration reload
kubectl -n monitoring exec -it alertmanager-monitoring-kube-prometheus-alertmanager-0 -- \
  curl -s -X POST http://localhost:9093/-/reload

Building Effective Dashboards

Dashboards provide at-a-glance visibility into cluster health. The best dashboards follow a logical progression: high-level cluster overview, then drill-downs into nodes, namespaces, and individual workloads.

Grafana Dashboard: Cluster Overview

Grafana ships with pre-built dashboards in the kube-prometheus-stack, but custom dashboards tailored to your environment are invaluable. Below is a JSON model for a cluster overview panel that displays node count, CPU headroom, and memory headroom. Import this via the Grafana UI or the Grafana API:

{
  "dashboard": {
    "title": "EKS Cluster Overview",
    "uid": "eks-cluster-overview",
    "panels": [
      {
        "title": "Ready Nodes",
        "type": "stat",
        "gridPos": {"x": 0, "y": 0, "w": 4, "h": 4},
        "targets": [
          {
            "expr": "count(kube_node_status_condition{condition=\"Ready\",status=\"true\"})",
            "legendFormat": "Ready Nodes"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "thresholds": {
              "steps": [
                {"color": "red", "value": 0},
                {"color": "orange", "value": 3},
                {"color": "green", "value": 5}
              ]
            }
          }
        }
      },
      {
        "title": "CPU Utilization %",
        "type": "gauge",
        "gridPos": {"x": 4, "y": 0, "w": 4, "h": 8},
        "targets": [
          {
            "expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
            "legendFormat": "Cluster CPU %"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "min": 0,
            "max": 100,
            "thresholds": {
              "steps": [
                {"color": "green", "value": 0},
                {"color": "yellow", "value": 70},
                {"color": "red", "value": 90}
              ]
            }
          }
        }
      },
      {
        "title": "Memory Utilization %",
        "type": "gauge",
        "gridPos": {"x": 8, "y": 0, "w": 4, "h": 8},
        "targets": [
          {
            "expr": "(1 - (sum(node_memory_MemAvailable_bytes) / sum(node_memory_MemTotal_bytes))) * 100",
            "legendFormat": "Cluster Memory %"
          }
        ]
      },
      {
        "title": "Pod Restarts (Last 1h)",
        "type": "stat",
        "gridPos": {"x": 12, "y": 0, "w": 4, "h": 4},
        "targets": [
          {
            "expr": "sum(increase(kube_pod_container_status_restarts_total[1h]))",
            "legendFormat": "Restarts"
          }
        ]
      },
      {
        "title": "API Server Latency (p99)",
        "type": "timeseries",
        "gridPos": {"x": 0, "y": 8, "w": 8, "h": 8},
        "targets": [
          {
            "expr": "histogram_quantile(0.99, rate(apiserver_request_duration_seconds_bucket[5m]))",
            "legendFormat": "p99 Latency"
          }
        ]
      },
      {
        "title": "Deployment Status",
        "type": "table",
        "gridPos": {"x": 8, "y": 8, "w": 8, "h": 8},
        "targets": [
          {
            "expr": "kube_deployment_status_replicas_available != kube_deployment_spec_replicas",
            "format": "table",
            "legendFormat": "{{ deployment }}"
          }
        ]
      }
    ]
  }
}

Import this dashboard into Grafana using the API or the UI. To use the API:

# Export the dashboard JSON and import via the Grafana API
curl -X POST "http://grafana.example.com/api/dashboards/db" \
  -H "Authorization: Bearer YOUR_GRAFANA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @eks-cluster-overview.json

CloudWatch Dashboard

If you're using CloudWatch Container Insights, you can build dashboards in the CloudWatch console or via CloudFormation. Here's a CloudFormation snippet that creates a widget-based dashboard:

{
  "Type": "AWS::CloudWatch::Dashboard",
  "Properties": {
    "DashboardName": "EKS-Production-Dashboard",
    "DashboardBody": {
      "widgets": [
        {
          "type": "metric",
          "x": 0,
          "y": 0,
          "width": 12,
          "height": 6,
          "properties": {
            "metrics": [
              ["ContainerInsights", "node_cpu_utilization", 
               {"stat": "Average", "period": 60}]
            ],
            "view": "timeSeries",
            "stacked": false,
            "region": "us-east-1",
            "title": "Node CPU Utilization",
            "yAxis": {"left": {"min": 0, "max": 100}}
          }
        },
        {
          "type": "metric",
          "x": 12,
          "y": 0,
          "width": 12,
          "height": 6,
          "properties": {
            "metrics": [
              ["ContainerInsights", "pod_cpu_utilization",
               {"stat": "Average", "period": 60}]
            ],
            "view": "timeSeries",
            "region": "us-east-1",
            "title": "Pod CPU by Namespace"
          }
        },
        {
          "type": "metric",
          "x": 0,
          "y": 6,
          "width": 24,
          "height": 6,
          "properties": {
            "metrics": [
              ["ContainerInsights", "pod_number_of_container_restarts",
               {"stat": "Sum", "period": 300}]
            ],
            "view": "timeSeries",
            "region": "us-east-1",
            "title": "Container Restarts"
          }
        }
      ]
    }
  }
}

Application-Specific Dashboards

Beyond infrastructure, create dashboards that surface application health. Use the RED pattern (Rate, Errors, Duration) for each service. Here's a PromQL query set for an application dashboard:

# Request Rate (requests per second)
rate(http_requests_total{service="my-api"}[5m])

# Error Rate (% of requests returning 5xx)
(sum(rate(http_requests_total{service="my-api",status=~"5.."}[5m])) 
 / sum(rate(http_requests_total{service="my-api"}[5m]))) * 100

# Latency Percentiles
histogram_quantile(0.50, rate(http_request_duration_seconds_bucket{service="my-api"}[5m]))  # p50
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{service="my-api"}[5m]))  # p95
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service="my-api"}[5m]))  # p99

# Saturation (in-flight requests)
sum(http_requests_in_flight{service="my-api"})

Best Practices for EKS Monitoring

1. Layer Your Monitoring Approach

Monitor at multiple levels: infrastructure (nodes, disks), Kubernetes objects (pods, deployments, services), and applications (RED signals). Each layer answers different questions during an incident. A pod restart might be caused by node memory pressure, which itself stems from a deployment that's leaking memory — layered dashboards help you trace the causality chain.

2. Define Clear Severity Levels

Not all alerts require waking someone up at 3 AM. Categorize alerts into:

Configure Alertmanager routes to respect these severities, and ensure your on-call rotation only receives critical pages.

3. Avoid Alert Fatigue

Alert fatigue is the silent killer of monitoring systems. To prevent it:

4. Plan for Metric Retention and Storage

Prometheus default retention is 15 days, which may be insufficient for capacity planning or trend analysis. Options for long-term storage include:

For CloudWatch, metrics are retained for 15 months with automatic downsampling — this works well for capacity trend analysis.

5. Monitor the Monitor

Your monitoring stack itself can fail. Set up meta-monitoring:

6. Use Infrastructure as Code for Dashboards and Alerts

Store Grafana dashboards, PrometheusRule definitions, and Alertmanager configurations in version control. Use tools like Terraform for CloudWatch alarms and dashboards, or Grafana Terraform provider for dashboards. This ensures reproducibility and allows peer review of monitoring changes before they hit production.

7. Integrate with Horizontal Pod Autoscaler Metrics

Go beyond CPU and memory for HPA scaling. Use custom metrics from Prometheus via the prometheus-adapter to scale on application-level signals like request rate or queue depth:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "100"

Install the prometheus-adapter to bridge custom metrics into the Kubernetes metrics API:

helm install prometheus-adapter prometheus-community/prometheus-adapter \
  --namespace monitoring \
  --set prometheus.url=http://monitoring-kube-prometheus-prometheus.monitoring.svc \
  --set prometheus.port=9090 \
  --set rules.default.requestsPerSecond={}

8. Regularly Audit and Iterate

Schedule quarterly reviews of your monitoring setup. During each review:

Conclusion

Monitoring Amazon EKS is a continuous, evolving discipline that spans infrastructure metrics, Kubernetes object state, and application performance signals. Whether you choose the managed simplicity of CloudWatch Container Insights or the rich ecosystem of Prometheus and Grafana, the core principles remain the same: collect the right metrics at the right granularity, define clear and actionable alarms, and build dashboards that guide operators from high-level cluster health down to specific pod-level root causes.

Start with the fundamentals covered here — deploy a monitoring agent, configure scraping of core metrics, set up alerts for the most critical failure modes (node exhaustion, pod crash loops, deployment mismatches), and iterate as your cluster footprint grows. Invest in long-term metric storage for capacity planning, integrate custom application metrics to complete your observability picture, and treat your monitoring configuration as production code that deserves the same rigor as your application deployments.

With a well-instrumented EKS cluster, you transform unknown failures into known, manageable incidents, reduce mean time

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles