> ## Documentation Index
> Fetch the complete documentation index at: https://cloudanix.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-Replica Deployments Should Have A PodDisruptionBudget

### 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

Informational

### Address

Security

### Compliance Standards

* Cloudanix Best Practice

### Triage and Remediation

<Tabs>
  <Tab title="Remediation">
    ### Remediation

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. On any machine with kubectl access, list non-compliant multi‑replica Deployments so you know what needs a PodDisruptionBudget:
           ```bash theme={null}
           { 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)
             | {namespace: $m.namespace, name: $m.name, labels: $podLabels, replicas: .spec.replicas}
             ]'
           ```

        2. For one non‑compliant Deployment, capture its pod template labels and replica count (needed for the PDB selector and minAvailable / maxUnavailable):
           ```bash theme={null}
           kubectl get deployment <deployment-name> -n <namespace> -o json \
             | jq '.spec.replicas as $r
                   | {replicas: $r,
                      labels: .spec.template.metadata.labels}'
           ```

        3. On any machine with kubectl access, create a PodDisruptionBudget manifest file for that Deployment, using the pod template labels in `spec.selector.matchLabels`. For example, save as `pdb-<deployment-name>.yaml`:
           ```yaml theme={null}
           apiVersion: policy/v1
           kind: PodDisruptionBudget
           metadata:
             name: <deployment-name>-pdb
             namespace: <namespace>
           spec:
             minAvailable: 1
             selector:
               matchLabels:
                 app: <value-from-deployment-labels>
                 # include any other identifying labels used on the pod template
           ```
           Adjust `minAvailable` (or alternatively use `maxUnavailable`) according to how many replicas must remain available during disruptions.

        4. Apply the PodDisruptionBudget:
           ```bash theme={null}
           kubectl apply -f pdb-<deployment-name>.yaml
           ```

        5. Repeat steps 2–4 for each remaining non‑compliant multi‑replica Deployment identified in step 1, ensuring each PDB’s `spec.selector.matchLabels` exactly matches the Deployment’s pod template labels you want protected.

        6. Verify all multi‑replica Deployments now have at least one matching PodDisruptionBudget by rerunning the audit command on any machine with kubectl access:
           ```bash theme={null}
           { 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) replicas=\(.spec.replicas) podDisruptionBudgets=\($count) is_compliant=\(if $count > 0 then "true" else "false" end)"
             ]'
           ```
           Confirm that every listed multi‑replica Deployment now shows `is_compliant=true` and `podDisruptionBudgets` greater than 0.
      </Accordion>

      <Accordion title="Using kubectl">
        ```bash theme={null}
        # 1) Identify non-compliant multi-replica Deployments
        # Run on: any machine with kubectl access
        kubectl get deployments --all-namespaces -o json | jq -r '
          .items[]
          | select((.spec.replicas // 1) > 1)
          | .metadata as $m
          | (.spec.template.metadata.labels // {}) as $podLabels
          | "NAMESPACE=\($m.namespace) NAME=\($m.name) LABELS=\($podLabels | to_entries | map("\(.key)=\(.value)") | join(","))"
        '

        # 2) For each multi-replica Deployment, create a matching PodDisruptionBudget manifest.
        # Example: Deployment "web" in namespace "prod" with pod label app=web

        cat <<'EOF' > pdb-web-prod.yaml
        apiVersion: policy/v1
        kind: PodDisruptionBudget
        metadata:
          name: web-pdb
          namespace: prod
        spec:
          minAvailable: 1
          selector:
            matchLabels:
              app: web
        EOF

        # 3) Apply the PDB
        kubectl apply -f pdb-web-prod.yaml
        ```

        Repeat step 2–3 for each multi-replica Deployment, ensuring:

        * `metadata.namespace` matches the Deployment’s namespace.
        * `spec.selector.matchLabels` exactly matches the labels on `.spec.template.metadata.labels` of the Deployment’s pods (at least one stable, unique label such as `app: <name>`).

        You can bulk-generate skeleton manifests to edit by hand, for example:

        ```bash theme={null}
        # Generate minimal PDB stubs for all multi-replica Deployments (edit before applying!)
        kubectl get deployments --all-namespaces -o json | jq -r '
          .items[]
          | select((.spec.replicas // 1) > 1)
          | .metadata as $m
          | (.spec.template.metadata.labels // {}) as $podLabels
          | select(($podLabels | length) > 0)
          | "---"
          + "\napiVersion: policy/v1"
          + "\nkind: PodDisruptionBudget"
          + "\nmetadata:"
          + "\n  name: \($m.name)-pdb"
          + "\n  namespace: \($m.namespace)"
          + "\nspec:"
          + "\n  minAvailable: 1"
          + "\n  selector:"
          + "\n    matchLabels:"
          + (
              $podLabels
              | to_entries
              | map("\n      \(.key): \(.value)")
              | join("")
            )
        ' > generated-pdbs.yaml

        # Review and edit generated-pdbs.yaml carefully, then:
        kubectl apply -f generated-pdbs.yaml
        ```

        ```bash theme={null}
        # Verification: rerun the benchmark-style check
        # 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
          | "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'
        ```
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        # Automation: Ensure each multi-replica Deployment in the cluster has a matching PodDisruptionBudget
        # Applies to: any machine with kubectl access to the EKS cluster
        #
        # Requirements:
        #   - kubectl configured to talk to the EKS cluster
        #   - jq available locally
        #
        # Behavior:
        #   - For every Deployment with replicas > 1:
        #       * If at least one PDB in the same namespace has a selector.matchLabels
        #         that is a subset of the pod template labels, it is considered covered
        #       * Otherwise, a PDB is created with selector.matchLabels equal to the pod
        #         template labels, and minAvailable set to floor(replicas/2)
        #   - Safe to re-run: existing matching PDBs are left unchanged

        set -euo pipefail

        # Optional: narrow to specific namespaces (comma-separated list). Empty = all namespaces.
        NAMESPACE_FILTER="${NAMESPACE_FILTER:-}"

        # Helper: check if namespace is allowed by NAMESPACE_FILTER
        ns_allowed() {
          local ns="$1"
          if [[ -z "${NAMESPACE_FILTER}" ]]; then
            return 0
          fi
          IFS=',' read -r -a ns_list <<< "${NAMESPACE_FILTER}"
          for n in "${ns_list[@]}"; do
            if [[ "${n}" == "${ns}" ]]; then
              return 0
            fi
          done
          return 1
        }

        # Fetch all Deployments and PDBs in one shot
        echo "Fetching Deployments and PodDisruptionBudgets..."
        DEPLOY_JSON="$(kubectl get deployments --all-namespaces -o json)"
        PDB_JSON="$(kubectl get poddisruptionbudgets --all-namespaces -o json)"

        # Process each multi-replica Deployment
        echo "${DEPLOY_JSON}" | jq -r '
          .items[]
          | select((.spec.replicas // 1) > 1)
          | {
              namespace: .metadata.namespace,
              name: .metadata.name,
              replicas: (.spec.replicas // 1),
              podLabels: (.spec.template.metadata.labels // {}),
              deploymentLabels: (.metadata.labels // {})
            }
          | @base64
        ' | while read -r DEP_B64; do
          _jq() { echo "${DEP_B64}" | base64 --decode | jq -r "${1}"; }

          ns=$(_jq '.namespace')
          name=$(_jq '.name')
          replicas=$(_jq '.replicas')
          # podLabels may be empty; we still create a PDB if needed, but a selector with
          # zero labels matches nothing, so we skip such Deployments
          pod_labels_json=$(_jq '.podLabels')

          if ! ns_allowed "${ns}"; then
            continue
          fi

          # Skip if pod template has no labels; cannot construct a meaningful PDB selector
          if [[ "$(echo "${pod_labels_json}" | jq 'length')" -eq 0 ]]; then
            echo "Skipping Deployment ${ns}/${name}: pod template has no labels."
            continue
          fi

          # Determine if any PDB in this namespace covers this Deployment:
          # selector.matchLabels must be non-empty and a subset of podLabels
          covered_count="$(
            jq -n --argjson pdbs "${PDB_JSON}" --arg ns "${ns}" --argjson podLabels "${pod_labels_json}" '
              ($pdbs.items // [])
              | map(select(.metadata.namespace == $ns))
              | map(select((.spec.selector.matchLabels // {}) | length > 0))
              | map(
                  if ((.spec.selector.matchLabels // {}) as $sel
                      | [ $sel | to_entries[] | ($podLabels[.key] == .value) ] | all
                     ) then 1 else 0 end
                )
              | add // 0
            '
          )"

          if [[ "${covered_count}" -gt 0 ]]; then
            echo "Deployment ${ns}/${name} already covered by ${covered_count} PodDisruptionBudget(s)."
            continue
          fi

          # No covering PDB: create one with selector.matchLabels == pod template labels
          # and minAvailable = floor(replicas / 2)
          min_available=$(( replicas / 2 ))
          if [[ "${min_available}" -lt 1 ]]; then
            min_available=1
          fi

          pdb_name="${name}-pdb"

          echo "Creating PodDisruptionBudget ${ns}/${pdb_name} for Deployment ${name} (replicas=${replicas}, minAvailable=${min_available})..."

          # Build selector.matchLabels YAML from pod_labels_json
          selector_yaml="$(echo "${pod_labels_json}" | jq -r '
            to_entries
            | map("      \(.key): \"\(.value)\"")
            | join("\n")
          ')"

          # Apply PDB manifest (idempotent with kubectl apply)
          cat <<EOF | kubectl apply -f -
        apiVersion: policy/v1
        kind: PodDisruptionBudget
        metadata:
          name: ${pdb_name}
          namespace: ${ns}
        spec:
          minAvailable: ${min_available}
          selector:
            matchLabels:
        $(echo "${selector_yaml}")
        EOF

        done

        echo
        echo "Verification: re-running compliance-style check..."

        # Re-run a simplified version of the audit to confirm coverage
        {
          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
          | ([ $pdbs.items[]
               | select(.metadata.namespace == $m.namespace)
               | select((.spec.selector.matchLabels // {}) | length > 0)
               | select(
                   (.spec.selector.matchLabels // {}) as $sel
                   | [ $sel | to_entries[] | ($podLabels[.key] == .value) ] | all
                 )
             ] | length) as $count
          | "Deployment " + $m.namespace + "/" + $m.name + " podDisruptionBudgets=" + ($count|tostring)
            + " is_compliant=" + (if $count > 0 then "true" else "false" end)
          ][]'
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
