Get icon
Get icon
Script icon
Set icon
If icon
SlackIncomingWebhook icon
Pause icon
Loop icon
Patch icon
PodCreate icon
Log icon
Schedule icon

Kubernetes OOMKilled Watchdog with Approval-Gated Memory Right-Sizing

Detect OOMKilled Kubernetes containers, recommend a new memory limit, and patch the Deployment or StatefulSet after approval, with Slack updates.

Categories
Infrastructure

Containers that are OOMKilled restart quietly, and the usual fix (raising the memory limit) waits until someone notices the CrashLoopBackOff. This flow finds every OOM kill in a namespace, works out which Deployment or StatefulSet owns it, recommends a new limit, and applies it only after a human approves. Each step is posted to Slack.

How it works

  1. every_15_minutes (io.kestra.plugin.core.trigger.Schedule) runs the scan. last_check (io.kestra.plugin.core.kv.Get) reads the timestamp of the previous scan from the KV store, so each OOM kill is reported once.
  2. get_pods (io.kestra.plugin.kubernetes.kubectl.Get) lists the pods in the namespace with their status.
  3. find_oom_kills (io.kestra.plugin.scripts.python.Script) keeps containers whose last termination reason is OOMKilled after the checkpoint. It resolves the owner (a Deployment through its ReplicaSet, or a StatefulSet), reads the current limit from the container status, and recommends current limit × growth_factor, rounded up to 64Mi and capped at max_memory_limit.
  4. save_checkpoint (io.kestra.plugin.core.kv.Set) moves the checkpoint forward before any approval wait.
  5. notify_oom posts one Slack message listing every affected workload with its recommendation, and a link to the execution when a resize is recommended.
  6. approve_resize (io.kestra.plugin.core.flow.Pause) waits up to 24 hours for approve or reject. One decision covers every resize recommended in that scan. No answer means reject, and notify_kept says so in Slack.
  7. On approval, each_resize (io.kestra.plugin.core.flow.Loop) goes through the workloads one at a time: patch_memory_limit (io.kestra.plugin.kubernetes.kubectl.Patch) applies a strategic merge patch to the container's resources.limits.memory, and wait_for_rollout (io.kestra.plugin.kubernetes.core.PodCreate) runs kubectl rollout status inside the cluster until the new pods are ready.
  8. Workloads that are not Deployments or StatefulSets (Jobs, DaemonSets, bare pods), or that already sit at max_memory_limit, are reported but never patched. Hitting the cap usually means a leak, not an undersized limit.

Prerequisites

  • Kestra 2.0 or later, for the Loop task.

  • Kubernetes 1.33 or later. The flow reads the current limit from status.containerStatuses[].resources, which is populated from 1.33 on. On older clusters, workloads are reported with action manual.

  • A Kestra worker that can run Docker containers (for find_oom_kills).

  • RBAC for the identity behind K8S_OAUTH_TOKEN, in the watched namespace: get/list on pods, patch on deployments and statefulsets, and create/get/list/watch/delete on pods plus get on pods/log (for PodCreate).

  • A ServiceAccount (default oom-watchdog) in the watched namespace for the rollout check, with get/list/watch on deployments, statefulsets, replicasets and pods:

    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: oom-watchdog
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: oom-watchdog-rollout-status
    rules:
      - apiGroups: ["apps"]
        resources: ["deployments", "statefulsets", "replicasets"]
        verbs: ["get", "list", "watch"]
      - apiGroups: [""]
        resources: ["pods"]
        verbs: ["get", "list", "watch"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: oom-watchdog-rollout-status
    subjects:
      - kind: ServiceAccount
        name: oom-watchdog
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: oom-watchdog-rollout-status
    

Secrets

  • K8S_MASTER_URL: Kubernetes API server URL.
  • K8S_CA_CERT_DATA: cluster CA certificate.
  • K8S_OAUTH_TOKEN: token for the RBAC permissions above.
  • SLACK_WEBHOOK_URL: Slack incoming webhook for notifications.

If Kestra runs inside the cluster or has a kubeconfig, you can remove pluginDefaults and the Kubernetes tasks use that configuration instead.

Inputs

  • namespace (STRING, default default): namespace to watch.
  • lookback (DURATION, default PT1H): window for the first run, before a checkpoint exists.
  • growth_factor (FLOAT, default 1.5): multiplier for the recommendation.
  • max_memory_limit (STRING, default 4Gi): cap for recommendations.
  • rollout_service_account (STRING, default oom-watchdog): ServiceAccount for the rollout check.
  • kubectl_image (STRING, default bitnami/kubectl:latest): image used for kubectl rollout status.

Outputs

  • outputs.find_oom_kills.vars.candidates: one entry per affected workload and container, with kind, workload, container, pods, restarts, currentLimit, recommendedLimit, action (resize, manual or investigate) and note.
  • outputs.find_oom_kills.vars.count: number of entries.
  • outputs.find_oom_kills.vars.resizes and resizeCount: the entries with action resize, which are the ones patched after approval.

Quick start

  1. Add the secrets above and apply the ServiceAccount manifest in the watched namespace.
  2. Import the flow and run it once manually to see the current OOM kills.
  3. Open the paused execution from the Slack link, click Resume and pick approve.
  4. Leave the schedule on so new OOM kills are picked up every 15 minutes.

Common pitfalls

  • Approval is all or nothing for a scan. To skip one workload, reject and change the others by hand, or extend the flow as described below.
  • Only one scan runs at a time (concurrency with CANCEL). While an approval is pending, scheduled scans are cancelled, so new OOM kills in other workloads are reported once the approval is answered or times out. The checkpoint does not move for cancelled scans, so nothing is missed.
  • The first run reports OOM kills from the last lookback window. Later runs only report what happened since the previous run.
  • kubectl rollout status fails if the new limit is still too small and the pods keep crashing. The flow then fails and notify_failure posts to Slack.
  • If the workload is managed by GitOps (Argo CD, Flux), the patch will be reverted on the next sync. Change the limit in Git instead, or extend the flow to open a pull request.

How to extend

  • Use peak memory from Prometheus (container_memory_working_set_bytes) instead of a fixed growth factor.
  • Watch several namespaces by wrapping the scan in a Loop.
  • Add a per-workload decision, or a limit override, as extra onResume inputs on approve_resize.
  • Open a pull request against your GitOps repository instead of patching the cluster directly.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.