Building Production-Grade EKS Pod Monitoring with AWS Lambda, EventBridge and Slack Alerts

Running workloads on Amazon EKS gives you scalability, orchestration, and operational flexibility — but production Kubernetes environments come with one constant challenge: visibility into workload health.

Pods can crash unexpectedly. Stateful applications may enter CrashLoopBackOff. Deployments can silently disappear after failed rollouts. A container may remain stuck in Pending, Terminating, or an Unhealthy state without immediate awareness from the engineering team.

While Kubernetes provides native observability primitives, many teams still need a lightweight, automated alerting mechanism tailored to specific business-critical workloads — without introducing the operational overhead of managing a full monitoring stack with minimal cost.

To solve this, I built a serverless EKS pod monitoring solution using:

  • AWS Lambda for health evaluation logic
  • Amazon EventBridge for scheduled execution
  • Amazon EKS API + Kubernetes Python Client for cluster inspection
  • Slack Incoming Webhooks for alert delivery

The result is a production-friendly monitoring workflow that checks pod health every minute, compares the current state with the previous execution state, and sends actionable alerts only when something changes.

This solution detects scenarios such as the following:

✅ Pod crashes (CrashLoopBackOff)
✅ Pods stuck in unhealthy states (Pending, Terminating, etc.)
✅ Complete workload disappearance
✅ Recovery events when failed workloads become healthy again

Unlike heavyweight observability platforms that require additional infrastructure, this approach stays intentionally lightweight while still providing meaningful operational monitoring for critical Kubernetes workloads.

In this article, I’ll walk through the complete implementation — from architecture and RBAC configuration to Lambda authentication with EKS, Slack integration, Terraform setup, and production hardening considerations.

Overview

Objective

Implement an automated monitoring solution for critical Kubernetes pods running in an AWS EKS cluster.

The solution continuously monitors specific workloads inside a namespace and sends Slack alerts when:

  • A pod enters unhealthy state
  • A pod crashes
  • A pod becomes unavailable
  • A monitored workload disappears
  • A failed workload recovers

This implementation uses:

  • AWS Lambda
  • AWS EventBridge
  • AWS IAM
  • AWS SSM Parameter Store
  • AWS EKS API
  • Kubernetes Python Client
  • RBAC
  • Slack Incoming Webhooks

Architecture

EventBridge (every 1 minute)
        ↓
AWS Lambda (Python 3.12)
        ↓
Generate EKS auth token via STS
        ↓
Call EKS DescribeCluster API
        ↓
Connect to Kubernetes API
        ↓
Read monitored pods
        ↓
Compare current state vs previous state (SSM)
        ↓
If unhealthy: Send Slack alert
        ↓
Save latest state in SSM

Monitored Workloads

Current monitored workloads: watched = [“mongodb”, “neo4jnode”]

Monitoring logic matches pod name prefixes.

Prerequisites

Before implementation, ensure:

  • Existing AWS account
  • Existing EKS cluster
  • kubectl access to cluster
  • Terraform configured for Kubernetes provider
  • Slack workspace access
  • Permission to create: IAM roles, Lambda, Lambda layers, EventBridge rules, SSM parameters

Terraform Kubernetes RBAC Configuration (only if you’re using Terraform)

Lambda authenticates to EKS using its IAM role.

EKS authentication succeeds via AWS IAM, but Kubernetes RBAC authorization must also allow pod access.

Add below Terraform:

resource "kubernetes_role" "wbd_lambda_pod_reader" {
  metadata {
    name      = "wbd-lambda-pod-reader"
    namespace = "wbd-kavia-clone"
  }

  rule {
    api_groups = [""]
    resources  = ["pods"]
    verbs      = ["get", "list", "watch"]
  }
}

resource "kubernetes_role_binding" "wbd_lambda_pod_reader_binding" {
  metadata {
    name      = "wbd-lambda-pod-reader-binding"
    namespace = "wbd-kavia-clone"
  }

  role_ref {
    api_group = "rbac.authorization.k8s.io"
    kind      = "Role"
    name      = kubernetes_role.wbd_lambda_pod_reader.metadata[0].name
  }

  subject {
    kind      = "User"
    name      = "arn:aws:iam::031749757330:role/wbd-eks-monitor-role"
    api_group = "rbac.authorization.k8s.io"
  }
}

This grants Lambda permission to:

  • get pods
  • list pods
  • watch pods

inside your namespace.

Lambda Layer Creation

Kubernetes Python dependencies are too large for inline Lambda packaging. That’s why we need to create Lambda layer.

Required dependencies:

Create local directory

mkdir k8s-layer
cd k8s-layer

Create python folder:

mkdir python

Install dependencies:

pip install kubernetes boto3 urllib3 -t python/

Zip:

zip -r k8s-python-layer.zip python

Upload Layer → AWS Console → Lambda → Layers → Create layer

Configuration:

Name: k8s-python-layer

Upload: k8s-python-layer.zip

Compatible runtime: Python 3.12

Architecture: x86_64

Write on Medium

Create layer.

IAM Role Creation

Create Lambda execution role.

AWS Console → IAM → Roles → Create Role

Trusted entity: AWS Service

Service: Lambda

Role name: wbd-eks-monitor-role

Attach Managed Policy

Attach: AWSLambdaBasicExecutionRole

This grants:

  • CloudWatch log creation
  • CloudWatch log stream creation
  • log publishing

Custom IAM Policy for EKS + SSM

Add inline policy:

{
 "Version": "2012-10-17",
 "Statement": [
   {
     "Sid": "EKSAccess",
     "Effect": "Allow",
     "Action": [
       "eks:DescribeCluster"
     ],
     "Resource": "*"
   },
   {
     "Sid": "SSMStateAccess",
     "Effect": "Allow",
     "Action": [
       "ssm:GetParameter",
       "ssm:PutParameter"
     ],
     "Resource": "arn:aws:ssm:eu-north-1:031749757330:parameter/eks-monitor/*"
   }
 ]
}

Attach to: wbd-eks-monitor-role

Lambda Function Creation

AWS Console → Lambda → Create Function

Configuration:

Function name: wbd-eks-pod-monitor

Runtime: Python 3.12

Architecture: x86_64

Execution role: Use existing role

Select: wbd-eks-monitor-role

Create function.

Lambda General Configuration

Update Memory: 256 MB

Timeout: 10 seconds

Ephemeral storage: 512 MB

Attach Lambda Layer

Open Lambda → Configuration → Layers → Add layer

Select: Custom layers

Choose: k8s-python-layer

Version: 1

Attach.

Lambda Environment Variables

CLUSTER_NAME

MONGO_DEPLOYMENT

NAMESPACE

NEO_STATEFULSET

SLACK_WEBHOOK_URL

Lambda Code Deployment

import os
import json
import base64
import tempfile
import urllib3
import boto3

from kubernetes import client
from kubernetes.client import Configuration
from botocore.signers import RequestSigner

http = urllib3.PoolManager()

CLUSTER_NAME = os.environ["CLUSTER_NAME"]
NAMESPACE = os.environ["NAMESPACE"]
SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]
AWS_REGION = os.environ["AWS_REGION"]

SSM_PARAM_NAME = f"/eks-monitor/{CLUSTER_NAME}/pod-states"


def send_slack(message):
    try:
        response = http.request(
            "POST",
            SLACK_WEBHOOK_URL,
            body=json.dumps({"text": message}),
            headers={"Content-Type": "application/json"},
        )
        if response.status != 200:
            print(f"Slack webhook failed with status: {response.status}")
    except Exception as e:
        print(f"Failed to send Slack alert: {e}")


def get_previous_states():
    ssm = boto3.client("ssm", region_name=AWS_REGION)
    try:
        response = ssm.get_parameter(Name=SSM_PARAM_NAME)
        return json.loads(response["Parameter"]["Value"])
    except ssm.exceptions.ParameterNotFound:
        return {}
    except Exception as e:
        print(f"Failed to get SSM state: {e}")
        return {}


def save_current_states(states):
    ssm = boto3.client("ssm", region_name=AWS_REGION)
    try:
        ssm.put_parameter(
            Name=SSM_PARAM_NAME,
            Value=json.dumps(states),
            Type="String",
            Overwrite=True,
        )
    except Exception as e:
        print(f"Failed to save SSM state: {e}")


def get_pod_status(pod):
    if pod.metadata.deletion_timestamp is not None:
        return "Terminating"
    if pod.status.phase != "Running":
        return pod.status.phase
    for cs in (pod.status.container_statuses or []):
        if not cs.ready:
            waiting = cs.state.waiting
            terminated = cs.state.terminated
            if waiting and waiting.reason:
                return waiting.reason
            if terminated and terminated.reason:
                return terminated.reason
    return "Running"


def get_eks_token(cluster_name, region):
    session = boto3.session.Session()
    client_sts = session.client("sts", region_name=region)

    signer = RequestSigner(
        client_sts.meta.service_model.service_id,
        region,
        "sts",
        "v4",
        session.get_credentials(),
        session.events,
    )

    params = {
        "method": "GET",
        "url": f"https://sts.{region}.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15",
        "body": {},
        "headers": {"x-k8s-aws-id": cluster_name},
        "context": {},
    }

    signed_url = signer.generate_presigned_url(
        params,
        region_name=region,
        expires_in=60,
        operation_name="",
    )

    return "k8s-aws-v1." + base64.urlsafe_b64encode(
        signed_url.encode("utf-8")
    ).decode("utf-8").rstrip("=")


def get_k8s_client():
    eks = boto3.client("eks", region_name=AWS_REGION)
    cluster = eks.describe_cluster(name=CLUSTER_NAME)["cluster"]

    token = get_eks_token(CLUSTER_NAME, AWS_REGION)

    config = Configuration()
    config.host = cluster["endpoint"]
    config.verify_ssl = True

    ca_data = base64.b64decode(cluster["certificateAuthority"]["data"])

    with tempfile.NamedTemporaryFile(delete=False) as cert:
        cert.write(ca_data)
        config.ssl_ca_cert = cert.name

    config.api_key = {"authorization": "Bearer " + token}

    return client.CoreV1Api(client.ApiClient(config))


def lambda_handler(event, context):
    api = get_k8s_client()
    pods = api.list_namespaced_pod(namespace=NAMESPACE).items

    watched = ["mongodb", "neo4jnode"] #Add the pod name here
    previous_states = get_previous_states()
    current_states = {}
    issues = []

    found = {name: False for name in watched}

    for pod in pods:
        pod_name = pod.metadata.name

        for name in watched:
            if not pod_name.startswith(name):
                continue

            found[name] = True
            current_status = get_pod_status(pod)
            previous_status = previous_states.get(name, "Running")

            if name not in current_states or current_status != "Running":
                current_states[name] = current_status

            if current_status != "Running" and previous_status == "Running":
                issues.append(
                    f" Pod issue\n"
                    f"Namespace: {NAMESPACE}\n"
                    f"Pod: {pod_name}\n"
                    f"Workload: {name}\n"
                    f"Status: {current_status}"
                )

            elif current_status == "Running" and previous_status != "Running":
                issues.append(
                    f"✅ Pod recovered\n"
                    f"Namespace: {NAMESPACE}\n"
                    f"Pod: {pod_name}\n"
                    f"Workload: {name}\n"
                    f"Previous status: {previous_status}"
                )

    for name in watched:
        if not found[name]:
            previous_status = previous_states.get(name, "Running")
            current_states[name] = "Missing"

            if previous_status != "Missing":
                issues.append(
                    f" Pod missing\n"
                    f"Namespace: {NAMESPACE}\n"
                    f"Workload: {name}"
                )

        elif previous_states.get(name) == "Missing":
            issues.append(
                f"✅ Pod found again\n"
                f"Namespace: {NAMESPACE}\n"
                f"Workload: {name}"
            )

    save_current_states(current_states)

    if not issues:
        return {"statusCode": 200, "body": "all healthy"}

    for issue in issues:
        send_slack(issue)

    return {"statusCode": 200, "body": "alerts sent"}

Deploy.

EventBridge Trigger Creation

Open Lambda.

Configuration → Triggers → Add trigger

Select: EventBridge (CloudWatch Events)

Create new rule.

Rule name: wbd-eks-pod-monitor-lambda-eventbridge

Schedule: rate(1 minute)

State: Enabled

Add trigger.

Lambda Resource-based Permission

EventBridge needs permission to invoke Lambda.

After trigger creation, Lambda resource policy should contain:

Principal: events.amazonaws.com

Action: lambda:InvokeFunction

Source: EventBridge rule ARN.

Verified under: Lambda → Configuration → Permissions

Example Slack Alerts

Failure:

 Pod issue
Namespace: wbd-kavia-clone
Pod: mongodb-0
Workload: mongodb
Status: CrashLoopBackOff

Recovery:

✅ Pod recovered
Namespace: wbd-kavia-clone
Pod: mongodb-0
Workload: mongodb
Previous status: CrashLoopBackOff

Missing:

 Pod missing
Namespace: wbd-kavia-clone
Workload: neo4jnode

Recovered:

✅ Pod found again
Namespace: wbd-kavia-clone
Workload: neo4jnode

CloudWatch Validation

Check logs: CloudWatch → Log groups

Lambda log group: /aws/lambda/wbd-eks-pod-monitor

Verify: successful execution

You may also like

Leave a Reply