Skip to main content

Containers Should Set CPU And Memory Requests

More Info:

Verifies every container sets resources.requests so the scheduler can place the pod correctly and QoS is not BestEffort.

Risk Level

Low

Address

Security

Compliance Standards

  • Cloudanix Best Practice

Triage and Remediation

Remediation

Manual Steps
  1. Identify noncompliant pods (any machine with kubectl access):

    kubectl get pods --all-namespaces -o json | jq -r '
    [ .items[]
    | select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
    | .metadata as $m
    | (.spec.nodeName // "") as $node
    | (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
    | ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
    | (.spec.containers // [])[]
    | ((.resources.requests.cpu != null) and (.resources.requests.memory != null)) as $ok
    | select($ok | not)
    | "kind=Pod ns=\($m.namespace) name=\($m.name) container=\(.name) image=\(.image)"
    ][]'
  2. For pods managed by higher-level controllers (Deployment, StatefulSet, DaemonSet, Job, CronJob), edit the controller manifest to add requests (any machine with kubectl access). Example for a Deployment:

    kubectl -n NAMESPACE edit deployment DEPLOYMENT_NAME

    In each affected container under spec.template.spec.containers[], ensure a block like:

    resources:
    requests:
    cpu: "100m"
    memory: "128Mi"

    Save and exit to let Kubernetes roll out updated pods.

  3. For standalone Pods (no controller in ownerReferences), edit the Pod spec directly (any machine with kubectl access). Note this recreates the pod:

    kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/pod-POD_NAME.yaml

    In /tmp/pod-POD_NAME.yaml, under each spec.containers[], add:

    resources:
    requests:
    cpu: "100m"
    memory: "128Mi"

    Remove ephemeral runtime fields (status, metadata.resourceVersion, metadata.uid, metadata.creationTimestamp, metadata.managedFields) and apply:

    kubectl -n NAMESPACE delete pod POD_NAME
    kubectl -n NAMESPACE apply -f /tmp/pod-POD_NAME.yaml
  4. If manifests are managed via GitOps or other IaC, make the same resources.requests.cpu and resources.requests.memory changes in the source YAML for each container, then let your deployment pipeline apply them. Do not rely on kubectl edit for these resources.

  5. Repeat steps 2–4 for all listed noncompliant pods until every container in each pod has both CPU and memory requests defined.

  6. Verify compliance (any machine with kubectl access):

    kubectl get pods --all-namespaces -o json | jq -r '
    [ .items[]
    | select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
    | .metadata as $m
    | (.spec.nodeName // "") as $node
    | (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
    | ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
    | (.spec.containers // [])[]
    | ((.resources.requests.cpu != null) and (.resources.requests.memory != null)) as $ok
    | "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
    + (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
    + (if $node == "" then "" else " node=\($node)" end)
    + (if $labels == "" then "" else " labels=\($labels)" end)
    + (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
    + " container=\(.name) image=\(.image)"
    + " requestsCpu=\(.resources.requests.cpu // "unset") requestsMemory=\(.resources.requests.memory // "unset")"
    + " is_compliant=\(if $ok then "true" else "false" end)"
    ] as $rows
    | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'

    Confirm the output is is_compliant=true or that all listed containers show is_compliant=true.

Using kubectl

On any machine with kubectl access:

  1. Identify non-compliant pods and their controllers
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| (.spec.containers // [])[]
| ((.resources.requests.cpu != null) and (.resources.requests.memory != null)) as $ok
| select($ok|not)
| "ns=\($m.namespace) pod=\($m.name) ownerKind=\($own.kind // "Pod") ownerName=\($own.name // $m.name)"
] | unique[]'
  1. For each workload controller (Deployment/DaemonSet/StatefulSet/Job/CronJob), edit the manifest and add resources.requests for every container.

Example for a Deployment:

kubectl -n NAMESPACE get deploy DEPLOYMENT_NAME -o yaml > /tmp/deploy-with-requests.yaml

Edit /tmp/deploy-with-requests.yaml and, for each container under spec.template.spec.containers, add:

resources:
requests:
cpu: "100m"
memory: "128Mi"

(Adjust values as appropriate for the application.)

Apply the updated manifest:

kubectl apply -f /tmp/deploy-with-requests.yaml
  1. For standalone Pods (no controller), recreate them with requests set.

Export, edit, and reapply:

kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/pod-with-requests.yaml

In /tmp/pod-with-requests.yaml:

  • Remove the entire metadata.uid, metadata.resourceVersion, metadata.creationTimestamp, metadata.managedFields, status sections.
  • Under each spec.containers[], add:
resources:
requests:
cpu: "100m"
memory: "128Mi"

Then:

kubectl -n NAMESPACE delete pod POD_NAME
kubectl apply -f /tmp/pod-with-requests.yaml
  1. Verification

On any machine with kubectl access:

kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.containers // [])[]
| ((.resources.requests.cpu != null) and (.resources.requests.memory != null)) as $ok
| select($ok|not)
] | if length==0 then "is_compliant=true" else . end'
Automation
#!/usr/bin/env bash
#
# Fix CBP C1.9: Ensure all containers set CPU and memory requests.
#
# This script:
# - Finds non-exempt pods whose containers lack CPU and/or memory requests.
# - Patches the owning workload (Deployment/StatefulSet/DaemonSet/Job/CronJob/Pod)
# to set default requests where missing.
# - Is idempotent: it only adds missing requests, and leaves existing ones intact.
# - Verifies compliance using the provided audit command.
#
# NOTE:
# - Requires: bash, kubectl, jq, yq (v4).
# - Run on: any machine with kubectl access to the cluster and appropriate RBAC.
#
# Default request values (change if needed):
DEFAULT_CPU_REQUEST="100m"
DEFAULT_MEMORY_REQUEST="128Mi"

set -euo pipefail

# Ensure required tools are available
for bin in kubectl jq yq; do
if ! command -v "$bin" >/dev/null 2>&1; then
echo "ERROR: Required binary '$bin' not found in PATH" >&2
exit 1
fi
done

echo "Discovering noncompliant containers..."

# Get list of non-exempt pods/containers that are non-compliant
NONCOMPLIANT_JSON=$(kubectl get pods --all-namespaces -o json | jq -c '
[
.items[]
| select(.metadata.namespace as $n
| ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| . as $pod
| (.metadata.ownerReferences // []) as $owners
| (.spec.containers // [])[]
| . as $c
| ((.resources.requests.cpu != null) and (.resources.requests.memory != null)) as $ok
| select($ok | not)
| {
podNamespace: $pod.metadata.namespace,
podName: $pod.metadata.name,
container: $c.name,
owner: (
[ $owners[]? | select(.controller) ] | first
)
}
]' )

if [[ -z "$NONCOMPLIANT_JSON" || "$NONCOMPLIANT_JSON" == "[]" ]]; then
echo "No noncompliant containers found; nothing to fix."
else
echo "$NONCOMPLIANT_JSON" | jq -r '
.[] |
"Noncompliant: ns=\(.podNamespace) pod=\(.podName) container=\(.container) ownerKind=\(.owner.kind // "Pod") ownerName=\(.owner.name // .podName)"
'
fi

# Group by owning workload (or Pod if no owner)
WORKLOADS_JSON=$(jq -c '
map({
ns: .podNamespace,
kind: (.owner.kind // "Pod"),
name: (.owner.name // .podName)
})
| unique
' <<<"$NONCOMPLIANT_JSON")

if [[ "$WORKLOADS_JSON" == "[]" ]]; then
echo "No owning workloads need patching."
else
echo
echo "Will patch the following owning workloads to set default requests where missing:"
echo "$WORKLOADS_JSON" | jq -r '.[] | "\(.ns) \(.kind) \(.name)"'
fi

patch_owner() {
local ns="$1" kind="$2" name="$3"

echo
echo "Processing owner: kind=$kind ns=$ns name=$name"

# Map owner kind to actual Kubernetes resource kind
local resKind pathPrefix

case "$kind" in
Deployment|StatefulSet|DaemonSet|Job)
resKind="$kind"
pathPrefix=".spec.template.spec"
;;
ReplicaSet)
# Usually managed by a Deployment; skip to avoid racing the controller.
echo " Skipping ReplicaSet $ns/$name (managed by a higher-level controller)."
return 0
;;
CronJob)
resKind="CronJob"
pathPrefix=".spec.jobTemplate.spec.template.spec"
;;
Pod)
resKind="Pod"
pathPrefix=".spec"
;;
*)
echo " WARNING: Unsupported owner kind '$kind' for $ns/$name; skipping."
return 0
;;
esac

# Fetch current YAML
tmpfile=$(mktemp)
if ! kubectl get "$resKind" "$name" -n "$ns" -o yaml >"$tmpfile" 2>/dev/null; then
echo " WARNING: Unable to fetch $resKind $ns/$name; it may have been deleted. Skipping."
rm -f "$tmpfile"
return 0
fi

# Build yq expression to set missing requests for containers and initContainers
# This preserves existing values and only fills in unset fields.
yq_expr='
# containers
'"$pathPrefix"'.containers // [] |
select(length > 0) |
path(.. | select(tag == "!!seq")) as $p |
.'$pathPrefix'.containers |= (
. // [] | map(
.resources.requests |= (
. // {} |
(.cpu // "'"$DEFAULT_CPU_REQUEST"'") as $cpu |
(.memory // "'"$DEFAULT_MEMORY_REQUEST"'") as $mem |
. * {"cpu": $cpu, "memory": $mem}
)
)
) |

# initContainers (if present)
'"$pathPrefix"'.initContainers // [] |
select(length > 0) |
path(.. | select(tag == "!!seq")) as $p2 |
.'$pathPrefix'.initContainers |= (
. // [] | map(
.resources.requests |= (
. // {} |
(.cpu // "'"$DEFAULT_CPU_REQUEST"'") as $cpu2 |
(.memory // "'"$DEFAULT_MEMORY_REQUEST"'") as $mem2 |
. * {"cpu": $cpu2, "memory": $mem2}
)
)
)
'

patched=$(yq eval "$yq_expr" "$tmpfile") || {
echo " ERROR: Failed to patch YAML for $resKind $ns/$name with yq."
rm -f "$tmpfile"
return 1
}

# Apply the patch if there is any change
if diff -q <(cat "$tmpfile") <(printf "%s\n" "$patched") >/dev/null 2>&1; then
echo " No changes needed for $resKind $ns/$name (already compliant or owner template unaffected)."
else
echo " Applying patched manifest to $resKind $ns/$name ..."
printf "%s\n" "$patched" | kubectl apply -f - >/dev/null
echo " Patch applied."
fi

rm -f "$tmpfile"
}

# Patch each owning workload
echo "$WORKLOADS_JSON" | jq -c '.[]' | while read -r w; do
ns=$(jq -r '.ns' <<<"$w")
kind=$(jq -r '.kind' <<<"$w")
name=$(jq -r '.name' <<<"$w")
patch_owner "$ns" "$kind" "$name"
done

echo
echo "Waiting for updated pods to roll out (this may take a few minutes)..."
# Simple wait: give controllers some time; can be tuned.
sleep 30

echo
echo "Verification: running benchmark audit command..."
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| (.spec.containers // [])[]
| ((.resources.requests.cpu != null) and (.resources.requests.memory != null)) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " container=\(.name) image=\(.image)"
+ " requestsCpu=\(.resources.requests.cpu // "unset") requestsMemory=\(.resources.requests.memory // "unset")"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end
'