More Info:
Advisory: define a PodDisruptionBudget for each multi-replica Deployment so node drains and rollouts keep a minimum number of pods available.Risk Level
InformationalAddress
SecurityCompliance Standards
- Cloudanix Best Practice
Triage and Remediation
- Remediation
Remediation
Manual Steps
Manual Steps
-
List multi-replica Deployments that currently lack a PodDisruptionBudget (run on any machine with kubectl access):
{ kubectl get poddisruptionbudgets --all-namespaces -o json \ kubectl get deployments --all-namespaces -o json; } | jq -rs ' .[0] as $pdbs | .[1] | [ .items[] | select((.spec.replicas // 1) > 1) | .metadata as $m | (.spec.template.metadata.labels // {}) as $podLabels | (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels | ([ $pdbs.items[] | select(.metadata.namespace == $m.namespace) | select((.spec.selector.matchLabels // {}) | length > 0) | select([ (.spec.selector.matchLabels | to_entries)[] | $podLabels[.key] == .value ] | all) ] | length) as $count | select($count == 0) | {name: $m.name, namespace: $m.namespace, labels: $podLabels} ]' -
For one non-compliant Deployment, inspect its spec to identify pod labels and replica count (replace NAMESPACE and DEPLOYMENT with real values from step 1; run on any machine with kubectl access):
Note the labels under
kubectl get deployment DEPLOYMENT -n NAMESPACE -o yamlspec.template.metadata.labelsand confirmspec.replicas > 1. -
Create a PodDisruptionBudget manifest for that Deployment, using the pod template labels as
matchLabelsand setting an appropriate availability policy (run on any machine with kubectl access):Replace:cat > pdb-DEPLOYMENT.yaml << 'EOF' apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: DEPLOYMENT-pdb namespace: NAMESPACE spec: minAvailable: 1 selector: matchLabels: app: REPLACE_WITH_APP_LABEL_VALUE EOFNAMESPACEwith the Deployment’s namespaceDEPLOYMENTwith the Deployment nameapp: REPLACE_WITH_APP_LABEL_VALUE(and/or add more keys) so that the label set exactly matchesspec.template.metadata.labelsof the Deployment’s pods. OnlymatchLabelsis evaluated by the check; do not usematchExpressionsif you want it counted as compliant.
-
Apply the PodDisruptionBudget to the cluster (run on any machine with kubectl access):
kubectl apply -f pdb-DEPLOYMENT.yaml -
Repeat steps 2–4 for each remaining non-compliant multi-replica Deployment, ensuring each has a corresponding PodDisruptionBudget whose
spec.selector.matchLabelsselects that Deployment’s pods. -
Verify that all multi-replica Deployments now have at least one matching PodDisruptionBudget (run on any machine with kubectl access):
Confirm that every
{ kubectl get poddisruptionbudgets --all-namespaces -o json \ kubectl get deployments --all-namespaces -o json; } | jq -rs ' .[0] as $pdbs | .[1] | [ .items[] | select((.spec.replicas // 1) > 1) | .metadata as $m | (.spec.template.metadata.labels // {}) as $podLabels | (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels | ([ $pdbs.items[] | select(.metadata.namespace == $m.namespace) | select((.spec.selector.matchLabels // {}) | length > 0) | select([ (.spec.selector.matchLabels | to_entries)[] | $podLabels[.key] == .value ] | all) ] | length) as $count | "kind=Deployment ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=apps/v1" + " replicas=\(.spec.replicas) podDisruptionBudgets=\($count)" + " is_compliant=\(if $count > 0 then "true" else "false" end)" ] as $rows | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'kind=Deploymentline withreplicas=greater than 1 now showsis_compliant=true.
Using kubectl
Using kubectl
# 1) Inspect a non-compliant multi-replica Deployment to get its labels
# Run on: any machine with kubectl access
kubectl get deployments -A \
-o jsonpath='{"NAMESPACE NAME LABELS\n"}{range .items[?(@.spec.replicas>1)]}{.metadata.namespace}{" "}{.metadata.name}{" "}{range $k,$v := .spec.template.metadata.labels}{$k}={"$v"}, {end}{"\n"}{end}'
# Example: assume deployment "my-app" in namespace "prod" with pod label app=my-app
# Adjust namespace, name, and labels to match your Deployment(s).
# 2) Create a PodDisruptionBudget manifest for that Deployment
cat > pdb-my-app.yaml << 'EOF'
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-pdb
namespace: prod
spec:
minAvailable: 1
selector:
matchLabels:
app: my-app
EOF
# 3) Apply the PodDisruptionBudget
kubectl apply -f pdb-my-app.yaml
# Repeat steps 2–3 for each multi-replica Deployment, ensuring:
# - metadata.namespace matches the Deployment namespace
# - spec.selector.matchLabels exactly matches a stable subset of the pod template labels
# - minAvailable or maxUnavailable reflect your availability/SLO requirements
# 4) Verification: rerun the benchmark-style check
{ kubectl get poddisruptionbudgets --all-namespaces -o json \
; kubectl get deployments --all-namespaces -o json; } | jq -rs '
.[0] as $pdbs | .[1] |
[ .items[]
| select((.spec.replicas // 1) > 1)
| .metadata as $m
| (.spec.template.metadata.labels // {}) as $podLabels
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ $pdbs.items[]
| select(.metadata.namespace == $m.namespace)
| select((.spec.selector.matchLabels // {}) | length > 0)
| select([ (.spec.selector.matchLabels | to_entries)[]
| $podLabels[.key] == .value ] | all)
] | length) as $count
| "kind=Deployment ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=apps/v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ " replicas=\(.spec.replicas) podDisruptionBudgets=\($count)"
+ " is_compliant=\(if $count > 0 then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Automation
Automation
#!/usr/bin/env bash
# Automation: Ensure every multi-replica Deployment has a matching PodDisruptionBudget
# Platform: AKS (or any cluster reachable via kubectl)
#
# Requirements:
# - Run on any machine with kubectl access and jq installed.
# - Uses kubectl current context.
#
# Behavior:
# - For each Deployment with replicas > 1:
# - If no PDB exists whose selector.matchLabels is a subset of the pod template labels,
# create a PDB named "<deployment-name>-pdb" with:
# minAvailable: 1 if replicas == 2
# minAvailable: 50% (string) if replicas >= 3
# - PDB selector.matchLabels is set to the Deployment's pod template labels.
# - Safe to re-run: existing matching PDBs are left untouched; created PDBs are stable.
set -euo pipefail
# --- Configurable defaults -----------------------------------------------------
# Label key/value added to PDBs created by this script (optional but useful)
PDB_MANAGED_LABEL_KEY="pdb.k8s-benchmark/managed-by"
PDB_MANAGED_LABEL_VALUE="cbp-c5.1-automation"
# ------------------------------------------------------------------------------
command -v kubectl >/dev/null 2>&1 || { echo "kubectl is required in PATH" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq is required in PATH" >&2; exit 1; }
echo "Discovering multi-replica Deployments (replicas > 1)..."
DEPLOY_JSON="$(kubectl get deployments --all-namespaces -o json)"
PDB_JSON="$(kubectl get poddisruptionbudgets --all-namespaces -o json || echo '{"items": []}')"
# Function: check if any PDB in the namespace covers the deployment's pod labels.
# Coverage rule: PDB.spec.selector.matchLabels is non-empty and is a subset of
# the Deployment's pod template labels.
deployment_needs_pdb() {
local namespace="$1"
local deploy_name="$2"
# Extract pod template labels and replicas for this deployment
local deploy_info
deploy_info="$(jq -r \
--arg ns "$namespace" \
--arg name "$deploy_name" \
'
.items[]
| select(.metadata.namespace == $ns and .metadata.name == $name)
| {
replicas: (.spec.replicas // 1),
podLabels: (.spec.template.metadata.labels // {})
}
' <<<"$DEPLOY_JSON")"
if [[ -z "$deploy_info" ]]; then
# Deployment disappeared between listing and processing
return 1
fi
local pod_labels_json replicas
pod_labels_json="$(jq -c '.podLabels' <<<"$deploy_info")"
replicas="$(jq -r '.replicas' <<<"$deploy_info")"
# Skip if not actually multi-replica
if (( replicas <= 1 )); then
return 1
fi
# Determine if any PDB matches
local match_count
match_count="$(jq -r \
--arg ns "$namespace" \
--argjson podLabels "$pod_labels_json" \
'
.items[]
| select(.metadata.namespace == $ns)
| select((.spec.selector.matchLabels // {}) | length > 0)
| .spec.selector.matchLabels as $sel
# check $sel is a subset of podLabels:
| ( [ ($sel | to_entries)[]
| ($podLabels[.key] == .value)
] | all ) as $isSubset
| select($isSubset)
| 1
' <<<"$PDB_JSON" | wc -l | tr -d ' ')"
if [[ "$match_count" -gt 0 ]]; then
return 1 # already covered
fi
return 0 # needs PDB
}
# Function: create PDB manifest for a deployment
create_pdb_for_deployment() {
local namespace="$1"
local deploy_name="$2"
local deploy
deploy="$(kubectl get deployment "$deploy_name" -n "$namespace" -o json 2>/dev/null || true)"
if [[ -z "$deploy" ]]; then
echo " [WARN] Deployment $namespace/$deploy_name disappeared; skipping"
return
fi
local replicas pod_labels_json pdb_name min_available_yaml
replicas="$(jq -r '.spec.replicas // 1' <<<"$deploy")"
pod_labels_json="$(jq -c '.spec.template.metadata.labels // {}' <<<"$deploy")"
pdb_name="${deploy_name}-pdb"
if (( replicas <= 1 )); then
echo " [INFO] $namespace/$deploy_name now has replicas <=1; skipping PDB creation"
return
fi
# Set minAvailable based on replicas
if (( replicas == 2 )); then
min_available_yaml="minAvailable: 1"
else
min_available_yaml="minAvailable: \"50%\""
fi
# Build selector.matchLabels YAML from pod_labels_json
local selector_yaml
selector_yaml="$(jq -r '
to_entries
| map(" \(.key): \"\(.value)\"")
| join("\n")
' <<<"$pod_labels_json")"
if [[ -z "$selector_yaml" ]]; then
echo " [WARN] $namespace/$deploy_name has no pod template labels; cannot create selector-based PDB"
return
fi
echo " [ACTION] Creating PDB $namespace/$pdb_name for Deployment $deploy_name (replicas=$replicas)"
cat <<EOF | kubectl apply -f -
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ${pdb_name}
namespace: ${namespace}
labels:
${PDB_MANAGED_LABEL_KEY}: "${PDB_MANAGED_LABEL_VALUE}"
spec:
${min_available_yaml}
selector:
matchLabels:
$(echo "${selector_yaml}")
EOF
}
# Iterate deployments and create PDBs where needed
echo "Processing Deployments..."
while IFS=$'\t' read -r ns name replicas; do
# Filter to replicas > 1 to reduce work early
if [[ "$replicas" -le 1 ]]; then
continue
fi
echo "Checking Deployment $ns/$name (replicas=$replicas)..."
if deployment_needs_pdb "$ns" "$name"; then
create_pdb_for_deployment "$ns" "$name"
# Refresh PDB_JSON after potential creation to keep checks consistent
PDB_JSON="$(kubectl get poddisruptionbudgets --all-namespaces -o json || echo '{"items": []}')"
else
echo " [OK] Covered by existing PodDisruptionBudget"
fi
done < <(echo "$DEPLOY_JSON" | jq -r '
.items[]
| {ns: .metadata.namespace, name: .metadata.name, replicas: (.spec.replicas // 1)}
| select(.replicas > 1)
| "\(.ns)\t\(.name)\t\(.replicas)"
')
# --- Verification -------------------------------------------------------------
echo
echo "Verification: multi-replica Deployments and their PodDisruptionBudgets"
{
kubectl get poddisruptionbudgets --all-namespaces -o json || echo '{"items": []}'
kubectl get deployments --all-namespaces -o json
} | jq -rs '
.[0] as $pdbs | .[1] |
[ .items[]
| select((.spec.replicas // 1) > 1)
| .metadata as $m
| (.spec.template.metadata.labels // {}) as $podLabels
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ $pdbs.items[]
| select(.metadata.namespace == $m.namespace)
| select((.spec.selector.matchLabels // {}) | length > 0)
| select([ (.spec.selector.matchLabels | to_entries)[]
| $podLabels[.key] == .value ] | all)
] | length) as $count
| "kind=Deployment ns=\($m.namespace) name=\($m.name) replicas=\(.spec.replicas) podDisruptionBudgets=\($count)"
+ " is_compliant=\(if $count > 0 then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end
'

