Introduction
Pod Disruption Budget (PDB) is a Kubernetes resource that limits the number of concurrent disruptions to your application pods during voluntary disruptions like node drains, cluster upgrades, or pod evictions. This ensures high availability and prevents simultaneous downtime of critical application components.
PDB Concepts
Key PDB Parameters:
- minAvailable: Minimum number of pods that must be available
- maxUnavailable: Maximum number of pods that can be unavailable
- selector: Label selector to identify protected pods
Fabric8 Kubernetes Client Implementation
1. Dependencies
<dependencies> <dependency> <groupId>io.fabric8</groupId> <artifactId>kubernetes-client</artifactId> <version>6.9.2</version> </dependency> <dependency> <groupId>io.fabric8</groupId> <artifactId>kubernetes-model</artifactId> <version>6.9.2</version> </dependency> </dependencies>
2. Basic PDB Service
package com.example.k8s.pdb; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudgetBuilder; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudgetSpec; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudgetSpecBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.Map; import java.util.Optional; @Service public class PodDisruptionBudgetService { private static final Logger log = LoggerFactory.getLogger(PodDisruptionBudgetService.class); private final KubernetesClient kubernetesClient; private final String namespace; public PodDisruptionBudgetService(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; this.namespace = kubernetesClient.getNamespace(); // Uses current context namespace } public PodDisruptionBudget createMinAvailablePDB(String name, Map<String, String> selectorLabels, int minAvailable) { log.info("Creating PDB '{}' with minAvailable: {}", name, minAvailable); PodDisruptionBudget pdb = new PodDisruptionBudgetBuilder() .withNewMetadata() .withName(name) .withNamespace(namespace) .addToLabels("app", name) .addToLabels("managed-by", "java-application") .endMetadata() .withNewSpec() .withMinAvailable(minAvailable) .withNewSelector() .withMatchLabels(selectorLabels) .endSelector() .endSpec() .build(); return kubernetesClient.policy().v1().podDisruptionBudget() .inNamespace(namespace) .resource(pdb) .create(); } public PodDisruptionBudget createMaxUnavailablePDB(String name, Map<String, String> selectorLabels, int maxUnavailable) { log.info("Creating PDB '{}' with maxUnavailable: {}", name, maxUnavailable); PodDisruptionBudget pdb = new PodDisruptionBudgetBuilder() .withNewMetadata() .withName(name) .withNamespace(namespace) .addToLabels("app", name) .endMetadata() .withNewSpec() .withMaxUnavailable(maxUnavailable) .withNewSelector() .withMatchLabels(selectorLabels) .endSelector() .endSpec() .build(); return kubernetesClient.policy().v1().podDisruptionBudget() .inNamespace(namespace) .resource(pdb) .create(); } public PodDisruptionBudget createPercentagePDB(String name, Map<String, String> selectorLabels, String maxUnavailablePercentage) { log.info("Creating PDB '{}' with maxUnavailable: {}", name, maxUnavailablePercentage); PodDisruptionBudget pdb = new PodDisruptionBudgetBuilder() .withNewMetadata() .withName(name) .withNamespace(namespace) .endMetadata() .withNewSpec() .withMaxUnavailable(maxUnavailablePercentage) // e.g., "50%" .withNewSelector() .withMatchLabels(selectorLabels) .endSelector() .endSpec() .build(); return kubernetesClient.policy().v1().podDisruptionBudget() .inNamespace(namespace) .resource(pdb) .create(); } } 3. Advanced PDB Manager
package com.example.k8s.pdb; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudgetList; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.Optional; @Component public class PDBManager { private final KubernetesClient client; private final String namespace; public PDBManager(KubernetesClient client) { this.client = client; this.namespace = client.getNamespace(); } public Optional<PodDisruptionBudget> getPDB(String name) { try { PodDisruptionBudget pdb = client.policy().v1().podDisruptionBudget() .inNamespace(namespace) .withName(name) .get(); return Optional.ofNullable(pdb); } catch (KubernetesClientException e) { log.error("Error retrieving PDB: {}", name, e); return Optional.empty(); } } public List<PodDisruptionBudget> listPDBs() { PodDisruptionBudgetList pdbList = client.policy().v1().podDisruptionBudget() .inNamespace(namespace) .list(); return pdbList.getItems(); } public List<PodDisruptionBudget> listPDBsByLabels(Map<String, String> labels) { PodDisruptionBudgetList pdbList = client.policy().v1().podDisruptionBudget() .inNamespace(namespace) .withLabels(labels) .list(); return pdbList.getItems(); } public boolean deletePDB(String name) { try { return client.policy().v1().podDisruptionBudget() .inNamespace(namespace) .withName(name) .delete(); } catch (KubernetesClientException e) { log.error("Error deleting PDB: {}", name, e); return false; } } public PodDisruptionBudget updatePDB(String name, PodDisruptionBudget updatedPDB) { return client.policy().v1().podDisruptionBudget() .inNamespace(namespace) .withName(name) .replace(updatedPDB); } public PodDisruptionBudget createOrUpdatePDB(PodDisruptionBudget pdb) { String name = pdb.getMetadata().getName(); if (getPDB(name).isPresent()) { return updatePDB(name, pdb); } else { return client.policy().v1().podDisruptionBudget() .inNamespace(namespace) .resource(pdb) .create(); } } } 4. PDB Factory for Common Patterns
package com.example.k8s.pdb; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudgetBuilder; import org.springframework.stereotype.Component; import java.util.Map; @Component public class PDBFactory { public PodDisruptionBudget createHighAvailabilityPDB(String appName, Map<String, String> selectorLabels, int totalReplicas) { // For high availability, allow only 1 pod to be disrupted at a time int maxUnavailable = Math.max(1, totalReplicas / 4); // 25% or 1, whichever is larger return new PodDisruptionBudgetBuilder() .withNewMetadata() .withName(appName + "-pdb") .addToLabels("app", appName) .addToLabels("pdb-type", "high-availability") .endMetadata() .withNewSpec() .withMaxUnavailable(maxUnavailable) .withNewSelector() .withMatchLabels(selectorLabels) .endSelector() .endSpec() .build(); } public PodDisruptionBudget createCriticalServicePDB(String appName, Map<String, String> selectorLabels, int totalReplicas) { // For critical services, require at least 50% availability int minAvailable = (int) Math.ceil(totalReplicas * 0.5); return new PodDisruptionBudgetBuilder() .withNewMetadata() .withName(appName + "-critical-pdb") .addToLabels("app", appName) .addToLabels("pdb-type", "critical") .endMetadata() .withNewSpec() .withMinAvailable(minAvailable) .withNewSelector() .withMatchLabels(selectorLabels) .endSelector() .endSpec() .build(); } public PodDisruptionBudget createSingleInstancePDB(String appName, Map<String, String> selectorLabels) { // For single-instance applications, prevent any disruption return new PodDisruptionBudgetBuilder() .withNewMetadata() .withName(appName + "-pdb") .addToLabels("app", appName) .addToLabels("pdb-type", "single-instance") .endMetadata() .withNewSpec() .withMinAvailable(1) // Must always be available .withNewSelector() .withMatchLabels(selectorLabels) .endSelector() .endSpec() .build(); } public PodDisruptionBudget createStatefulPDB(String appName, Map<String, String> selectorLabels, int replicas) { // For stateful applications, allow disruption of only one pod at a time return new PodDisruptionBudgetBuilder() .withNewMetadata() .withName(appName + "-pdb") .addToLabels("app", appName) .addToLabels("pdb-type", "stateful") .endMetadata() .withNewSpec() .withMaxUnavailable(1) .withNewSelector() .withMatchLabels(selectorLabels) .endSelector() .endSpec() .build(); } public PodDisruptionBudget createPercentageBasedPDB(String appName, Map<String, String> selectorLabels, String maxUnavailablePercentage) { return new PodDisruptionBudgetBuilder() .withNewMetadata() .withName(appName + "-pdb") .addToLabels("app", appName) .endMetadata() .withNewSpec() .withMaxUnavailable(maxUnavailablePercentage) .withNewSelector() .withMatchLabels(selectorLabels) .endSelector() .endSpec() .build(); } } 5. PDB Validation Service
package com.example.k8s.pdb; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget; import io.fabric8.kubernetes.client.KubernetesClient; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service public class PDBValidationService { private final KubernetesClient client; private final String namespace; public PDBValidationService(KubernetesClient client) { this.client = client; this.namespace = client.getNamespace(); } public ValidationResult validatePDB(PodDisruptionBudget pdb) { List<String> errors = new ArrayList<>(); List<String> warnings = new ArrayList<>(); String name = pdb.getMetadata().getName(); var spec = pdb.getSpec(); // Check if both minAvailable and maxUnavailable are set if (spec.getMinAvailable() != null && spec.getMaxUnavailable() != null) { errors.add("Both minAvailable and maxUnavailable cannot be set simultaneously"); } // Check if neither is set if (spec.getMinAvailable() == null && spec.getMaxUnavailable() == null) { errors.add("Either minAvailable or maxUnavailable must be set"); } // Check selector if (spec.getSelector() == null) { errors.add("Selector must be specified"); } else if (spec.getSelector().getMatchLabels() == null || spec.getSelector().getMatchLabels().isEmpty()) { warnings.add("PDB selector has no matchLabels, ensure this is intentional"); } // Validate numeric values if (spec.getMinAvailable() != null) { try { int minAvailable = Integer.parseInt(spec.getMinAvailable().toString()); if (minAvailable < 0) { errors.add("minAvailable cannot be negative"); } } catch (NumberFormatException e) { // It might be a percentage, validate percentage format if (!spec.getMinAvailable().toString().endsWith("%")) { errors.add("minAvailable must be an integer or percentage"); } } } if (spec.getMaxUnavailable() != null) { try { int maxUnavailable = Integer.parseInt(spec.getMaxUnavailable().toString()); if (maxUnavailable < 0) { errors.add("maxUnavailable cannot be negative"); } } catch (NumberFormatException e) { // It might be a percentage, validate percentage format if (!spec.getMaxUnavailable().toString().endsWith("%")) { errors.add("maxUnavailable must be an integer or percentage"); } } } return new ValidationResult(name, errors, warnings); } public boolean isPDBCompliant(String pdbName) { Optional<PodDisruptionBudget> pdbOpt = getPDB(pdbName); if (pdbOpt.isEmpty()) { return false; } PodDisruptionBudget pdb = pdbOpt.get(); var status = pdb.getStatus(); if (status == null) { return false; } // Check if current healthy pods meet PDB requirements Integer desiredHealthy = status.getDesiredHealthy(); Integer currentHealthy = status.getCurrentHealthy(); return desiredHealthy != null && currentHealthy != null && currentHealthy >= desiredHealthy; } public static class ValidationResult { private final String pdbName; private final List<String> errors; private final List<String> warnings; private final boolean isValid; public ValidationResult(String pdbName, List<String> errors, List<String> warnings) { this.pdbName = pdbName; this.errors = errors; this.warnings = warnings; this.isValid = errors.isEmpty(); } // Getters public String getPdbName() { return pdbName; } public List<String> getErrors() { return errors; } public List<String> getWarnings() { return warnings; } public boolean isValid() { return isValid; } } private Optional<PodDisruptionBudget> getPDB(String name) { try { PodDisruptionBudget pdb = client.policy().v1().podDisruptionBudget() .inNamespace(namespace) .withName(name) .get(); return Optional.ofNullable(pdb); } catch (Exception e) { return Optional.empty(); } } } 6. PDB Controller for Automated Management
package com.example.k8s.pdb; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.WatcherException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Component public class PDBAutoManager { private static final Logger log = LoggerFactory.getLogger(PDBAutoManager.class); private final KubernetesClient client; private final PDBFactory pdbFactory; private final PDBManager pdbManager; private final String namespace; private final Map<String, String> managedDeployments = new ConcurrentHashMap<>(); private Watcher<Deployment> deploymentWatcher; public PDBAutoManager(KubernetesClient client, PDBFactory pdbFactory, PDBManager pdbManager) { this.client = client; this.pdbFactory = pdbFactory; this.pdbManager = pdbManager; this.namespace = client.getNamespace(); } @PostConstruct public void init() { startDeploymentWatcher(); reconcileExistingDeployments(); } @PreDestroy public void cleanup() { if (deploymentWatcher != null) { // Watcher is automatically closed by the client } } private void startDeploymentWatcher() { deploymentWatcher = client.apps().deployments() .inNamespace(namespace) .watch(new Watcher<Deployment>() { @Override public void eventReceived(Action action, Deployment deployment) { handleDeploymentEvent(action, deployment); } @Override public void onClose(WatcherException cause) { if (cause != null) { log.error("Deployment watcher closed with error", cause); // Implement reconnection logic } } }); } private void handleDeploymentEvent(Watcher.Action action, Deployment deployment) { String deploymentName = deployment.getMetadata().getName(); Map<String, String> labels = deployment.getMetadata().getLabels(); if (labels != null && "true".equals(labels.get("auto-pdb"))) { switch (action) { case ADDED: case MODIFIED: manageDeploymentPDB(deployment); break; case DELETED: removeDeploymentPDB(deploymentName); break; } } } private void manageDeploymentPDB(Deployment deployment) { String deploymentName = deployment.getMetadata().getName(); Map<String, String> selectorLabels = deployment.getSpec().getSelector().getMatchLabels(); Integer replicas = deployment.getSpec().getReplicas(); if (replicas == null || replicas < 1) { log.warn("Deployment {} has invalid replica count: {}", deploymentName, replicas); return; } Map<String, String> labels = deployment.getMetadata().getLabels(); String pdbStrategy = labels != null ? labels.get("pdb-strategy") : "default"; PodDisruptionBudget pdb; switch (pdbStrategy) { case "high-availability": pdb = pdbFactory.createHighAvailabilityPDB(deploymentName, selectorLabels, replicas); break; case "critical": pdb = pdbFactory.createCriticalServicePDB(deploymentName, selectorLabels, replicas); break; case "single-instance": if (replicas == 1) { pdb = pdbFactory.createSingleInstancePDB(deploymentName, selectorLabels); } else { log.warn("Single-instance PDB strategy used for multi-replica deployment: {}", deploymentName); pdb = pdbFactory.createHighAvailabilityPDB(deploymentName, selectorLabels, replicas); } break; default: pdb = pdbFactory.createHighAvailabilityPDB(deploymentName, selectorLabels, replicas); } pdbManager.createOrUpdatePDB(pdb); managedDeployments.put(deploymentName, pdb.getMetadata().getName()); log.info("Managed PDB for deployment: {}", deploymentName); } private void removeDeploymentPDB(String deploymentName) { String pdbName = managedDeployments.remove(deploymentName); if (pdbName != null) { pdbManager.deletePDB(pdbName); log.info("Removed PDB for deleted deployment: {}", deploymentName); } } @Scheduled(fixedDelay = 300000) // Run every 5 minutes public void reconcileExistingDeployments() { log.info("Starting PDB reconciliation"); client.apps().deployments() .inNamespace(namespace) .list() .getItems() .stream() .filter(deployment -> { Map<String, String> labels = deployment.getMetadata().getLabels(); return labels != null && "true".equals(labels.get("auto-pdb")); }) .forEach(this::manageDeploymentPDB); log.info("Completed PDB reconciliation"); } } 7. Spring Boot Configuration
package com.example.k8s.config; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class KubernetesConfig { @Value("${kubernetes.namespace:default}") private String namespace; @Value("${kubernetes.master.url:}") private String masterUrl; @Value("${kubernetes.oauth.token:}") private String oauthToken; @Bean public KubernetesClient kubernetesClient() { Config config = new ConfigBuilder() .withNamespace(namespace) .withMasterUrl(masterUrl.isEmpty() ? null : masterUrl) .withOauthToken(oauthToken.isEmpty() ? null : oauthToken) .withTrustCerts(true) .build(); return new DefaultKubernetesClient(config); } } 8. Usage Examples
package com.example.k8s.controller; import com.example.k8s.pdb.*; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/pdb") public class PDBController { private final PodDisruptionBudgetService pdbService; private final PDBManager pdbManager; private final PDBValidationService validationService; public PDBController(PodDisruptionBudgetService pdbService, PDBManager pdbManager, PDBValidationService validationService) { this.pdbService = pdbService; this.pdbManager = pdbManager; this.validationService = validationService; } @PostMapping("/min-available") public PodDisruptionBudget createMinAvailablePDB( @RequestParam String name, @RequestParam int minAvailable, @RequestBody Map<String, String> selectorLabels) { return pdbService.createMinAvailablePDB(name, selectorLabels, minAvailable); } @PostMapping("/max-unavailable") public PodDisruptionBudget createMaxUnavailablePDB( @RequestParam String name, @RequestParam int maxUnavailable, @RequestBody Map<String, String> selectorLabels) { return pdbService.createMaxUnavailablePDB(name, selectorLabels, maxUnavailable); } @GetMapping public List<PodDisruptionBudget> listPDBs() { return pdbManager.listPDBs(); } @GetMapping("/{name}") public PodDisruptionBudget getPDB(@PathVariable String name) { return pdbManager.getPDB(name) .orElseThrow(() -> new RuntimeException("PDB not found: " + name)); } @DeleteMapping("/{name}") public void deletePDB(@PathVariable String name) { pdbManager.deletePDB(name); } @PostMapping("/{name}/validate") public PDBValidationService.ValidationResult validatePDB(@PathVariable String name) { PodDisruptionBudget pdb = pdbManager.getPDB(name) .orElseThrow(() -> new RuntimeException("PDB not found: " + name)); return validationService.validatePDB(pdb); } @GetMapping("/{name}/compliant") public boolean isPDBCompliant(@PathVariable String name) { return validationService.isPDBCompliant(name); } } 9. Application Properties
# application.yml kubernetes: namespace: my-app-namespace master: url: https://kubernetes.default.svc oauth: token: ${KUBERNETES_TOKEN} management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always logging: level: com.example.k8s: DEBUG Best Practices
- Use PDBs for Stateful Applications: Protect databases, message queues, and other stateful services
- Balance Availability and Upgradability: Don't set minAvailable too high, as it can prevent cluster maintenance
- Monitor PDB Compliance: Regularly check if your PDBs are being respected
- Use Labels Strategically: Ensure PDB selectors match your pod labels accurately
- Test Disruption Scenarios: Verify PDB behavior during controlled node drains
Conclusion
Pod Disruption Budgets are essential for maintaining application availability during Kubernetes cluster operations. The Java implementation using Fabric8 Kubernetes Client provides a robust way to programmatically manage PDBs, ensuring your applications remain available during voluntary disruptions while allowing necessary cluster maintenance operations.