> ## 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. Identify multi-replica Deployments that lack a PodDisruptionBudget\
           Run on: any machine with kubectl access
           ```sh 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)
             | "ns=\($m.namespace) name=\($m.name) replicas=\(.spec.replicas)"
             ][]'
           ```

        2. For one non-compliant Deployment, capture its labels to use in the PodDisruptionBudget selector\
           Run on: any machine with kubectl access\
           Replace NAMESPACE and DEPLOYMENT\_NAME as needed:
           ```sh theme={null}
           kubectl get deployment DEPLOYMENT_NAME -n NAMESPACE -o jsonpath='{.spec.template.metadata.labels}' | jq
           ```
           Note the key/value pairs; these must be copied exactly into the PodDisruptionBudget’s `spec.selector.matchLabels`.

        3. Decide the availability requirement for this Deployment and choose the PodDisruptionBudget spec\
           Run on: any machine with kubectl access\
           Typical choices:
           * To always keep at least one pod running:
             ```yaml theme={null}
             minAvailable: 1
             ```
           * Or to allow only 1 pod at a time to be disrupted in a 3+ replica Deployment:
             ```yaml theme={null}
             maxUnavailable: 1
             ```
           Ensure the chosen value makes sense given `.spec.replicas` for this Deployment.

        4. Create a PodDisruptionBudget manifest for the Deployment using its pod labels\
           Run on: any machine with kubectl access\
           Example template; replace NAMESPACE, PDB\_NAME, and the labels under `matchLabels` with the exact values from step 2:
           ```yaml theme={null}
           cat > pdb-deployment-DEPLOYMENT_NAME.yaml << 'EOF'
           apiVersion: policy/v1
           kind: PodDisruptionBudget
           metadata:
             name: PDB_NAME
             namespace: NAMESPACE
           spec:
             minAvailable: 1
             selector:
               matchLabels:
                 app: my-app            # replace with real label key/value
                 tier: backend          # remove or adjust keys to match exactly
           EOF
           ```
           Ensure `matchLabels` matches a subset of the pod template labels of the target Deployment so the PDB actually selects its pods.

        5. Apply the PodDisruptionBudget to the cluster\
           Run on: any machine with kubectl access
           ```sh theme={null}
           kubectl apply -f pdb-deployment-DEPLOYMENT_NAME.yaml
           ```

        6. Verification: confirm all multi-replica Deployments now have at least one matching PodDisruptionBudget\
           Run on: any machine with kubectl access
           ```sh 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)"
             ] as $rows
             | if ($rows | map(select(. | test("is_compliant=false"))) | length) == 0
               then "is_compliant=true"
               else $rows[] end'
           ```
           Ensure all listed Deployments show `is_compliant=true`; if any show `is_compliant=false`, repeat steps 2–5 for those Deployments.
      </Accordion>

      <Accordion title="Using kubectl">
        On any machine with kubectl access:

        1. Identify a multi‑replica Deployment needing a PDB (example: namespace `production`, deployment `web-app`):

        ```bash theme={null}
        kubectl -n production get deploy web-app -o yaml | sed -n '1,80p'
        ```

        Note the pod template labels under `spec.template.metadata.labels`. Example:

        ```yaml theme={null}
        spec:
          template:
            metadata:
              labels:
                app: web-app
                tier: frontend
        ```

        2. Create a PodDisruptionBudget manifest that selects those labels and enforces a minimum number of available pods. Example (adjust labels, name, and minAvailable/maxUnavailable per your SLOs):

        ```bash theme={null}
        cat <<'EOF' > pdb-web-app.yaml
        apiVersion: policy/v1
        kind: PodDisruptionBudget
        metadata:
          name: web-app-pdb
          namespace: production
        spec:
          minAvailable: 2
          selector:
            matchLabels:
              app: web-app
              tier: frontend
        EOF
        ```

        3. Apply the PDB:

        ```bash theme={null}
        kubectl apply -f pdb-web-app.yaml
        ```

        4. Repeat steps 1–3 for each Deployment with `spec.replicas > 1`, ensuring each PDB’s `spec.selector.matchLabels` exactly matches the Deployment’s pod template labels.

        5. Verification (cluster‑wide, same logic as the audit):

        ```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) 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
        #
        # Ensure every multi-replica Deployment has a matching PodDisruptionBudget
        # that uses matchLabels equal to the Deployment's pod-template labels.
        #
        # Requirements:
        #   - Run on any machine with kubectl access to the GKE cluster
        #   - kubectl, jq, and bash installed
        #   - Current kube-context set to the target cluster
        #
        # Behaviour:
        #   - Skips Deployments with replicas <= 1
        #   - Creates/updates one PDB per Deployment:
        #       name: <deployment-name>-pdb
        #       selector.matchLabels: deployment's pod template labels
        #       spec.minAvailable: 1 (if replicas == 2) else "50%" (if replicas > 2)
        #   - Only manages PDBs it creates (matching the naming pattern)
        #   - Safe to re-run (idempotent)
        set -euo pipefail

        echo "=== Discovering multi-replica Deployments and existing PDBs ==="

        # Collect PDBs and Deployments as JSON arrays
        PDB_JSON="$(kubectl get poddisruptionbudgets --all-namespaces -o json)"
        DEPLOY_JSON="$(kubectl get deployments --all-namespaces -o json)"

        # Function: determine if a Deployment already has a covering PDB (matchLabels only)
        has_covering_pdb() {
          local ns="$1" name="$2"
          jq -er --arg ns "$ns" --arg name "$name" \
            --argjson pdbs "$PDB_JSON" --argjson deps "$DEPLOY_JSON" '
              ($deps.items[]
               | select(.metadata.namespace == $ns and .metadata.name == $name)) as $dep
              | ($dep.spec.template.metadata.labels // {}) as $podLabels
              | [ $pdbs.items[]
                  | select(.metadata.namespace == $ns)
                  | select((.spec.selector.matchLabels // {}) | length > 0)
                  | select(
                      # matchLabels must be a subset of pod labels and not empty
                      (.spec.selector.matchLabels // {}) as $sel
                      | ($sel | length) > 0
                      and ([ $sel | to_entries[]
                             | ($podLabels[.key] == .value) ] | all)
                    )
                ] | length
              | . > 0
            ' >/dev/null 2>&1
        }

        # Iterate all Deployments and reconcile required PDBs
        echo "=== Reconciling PodDisruptionBudgets for multi-replica Deployments ==="

        echo "$DEPLOY_JSON" | jq -r '
          .items[]
          | select((.spec.replicas // 1) > 1)
          | [.metadata.namespace, .metadata.name, (.spec.replicas // 1)] | @tsv
        ' | while IFS=$'\t' read -r NS NAME REPLICAS; do
          # Extract pod template labels for this Deployment
          POD_LABELS_JSON="$(echo "$DEPLOY_JSON" | jq -r --arg ns "$NS" --arg name "$NAME" '
            .items[]
            | select(.metadata.namespace == $ns and .metadata.name == $name)
            | (.spec.template.metadata.labels // {})
          ')"

          # Skip if there are no pod template labels (PDB selector would be empty)
          if [[ "$(echo "$POD_LABELS_JSON" | jq 'length')" -eq 0 ]]; then
            echo "Skipping ${NS}/${NAME}: no pod template labels to select on"
            continue
          fi

          if has_covering_pdb "$NS" "$NAME"; then
            echo "OK: ${NS}/${NAME} already has a covering PDB"
            continue
          fi

          # Decide minAvailable:
          # - 1 if exactly 2 replicas (to maximize availability)
          # - "50%" if more than 2 replicas (balanced disruption)
          MIN_AVAILABLE=""
          if [[ "$REPLICAS" -eq 2 ]]; then
            MIN_AVAILABLE="1"
          else
            MIN_AVAILABLE="50%"
          fi

          PDB_NAME="${NAME}-pdb"

          echo "Reconciling PDB for ${NS}/${NAME} -> ${NS}/${PDB_NAME} (minAvailable=${MIN_AVAILABLE})"

          # Build a minimal PDB manifest using the deployment's pod-template labels as matchLabels
          # Note: We include only matchLabels, as the audit check ignores matchExpressions.
          PDB_MANIFEST="$(jq -n \
            --arg apiVersion "policy/v1" \
            --arg kind "PodDisruptionBudget" \
            --arg ns "$NS" \
            --arg name "$PDB_NAME" \
            --argjson labels "$POD_LABELS_JSON" \
            --arg minAvail "$MIN_AVAILABLE" '
            {
              apiVersion: $apiVersion,
              kind: $kind,
              metadata: {
                name: $name,
                namespace: $ns,
                labels: {
                  "managed-by": "pdb-automation-script"
                }
              },
              spec: {
                minAvailable: (try ( $minAvail|tonumber ) catch $minAvail),
                selector: {
                  matchLabels: $labels
                }
              }
            }
          ')"

          # Apply the PDB manifest (create or update)
          echo "$PDB_MANIFEST" | kubectl apply -f -
        done

        echo "=== Verification: re-running compliance-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
        '
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
