Skip to content

feat(autoscaling): add the Kubernetes Descheduler to reclaim Karpenter-style consolidation on HetznerΒ #2468

Closed

Description

@devantler

πŸ€– Generated by the Daily AI Assistant

Problem

Karpenter is the tool people reach for when they want active node consolidation β€” continuously repacking pods onto fewer nodes and retiring the under-utilized ones to cut cost. But Karpenter has no Hetzner Cloud provider (upstream supports AWS, Azure, Alibaba and now OCI; Hetzner is not on the list), so we cannot adopt it here.

Our Hetzner node-lifecycle stack today is:

  • Cluster Autoscaler (Hetzner cloud provider) β€” installed via ksail.prod.yaml (autoscaler.node.*); scales the autoscale-cx* pool up on Pending pods and down on idle nodes.
  • CapacityBuffer over-provisioning (k8s/providers/hetzner/infrastructure/overprovisioning/) β€” keeps one warm spare node so real pods never sit Pending.
  • VPA β€” right-sizes pod requests (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/).

The gap: Cluster Autoscaler never repacks running pods. It only removes a node once every pod on it happens to be independently reschedulable and the node has sat under the utilization threshold long enough. It has no bin-packing / rebalancing pass. So over time the cluster fragments β€” pods spread thin across autoscale-cx* nodes that CA then can't drain, and stateful pods land on autoscale nodes and pin them up. We've already hit the concrete symptom: a CNPG primary (wedding-db-10) scheduled onto a cx43 autoscale node instead of a baseline node and effectively blocked that node from scaling down.

Proposed direction

Add the Kubernetes SIG Descheduler (kubernetes-sigs/descheduler) β€” the cloud-agnostic, Talos-compatible component that provides exactly the missing half. It only evicts pods (respecting PDBs); the existing Cluster Autoscaler then reclaims the emptied nodes. Cluster Autoscaler (provision/deprovision) + Descheduler (rebalance/consolidate) β‰ˆ Karpenter consolidation, on Hetzner.

Deploy it as a GitOps controller in the Hetzner infrastructure layer (Helm chart descheduler from kubernetes-sigs, pinned via HelmRelease, in continuous Deployment mode with --descheduling-interval, or a CronJob). Start conservative and expand policy as we gain confidence:

  • HighNodeUtilization β€” the consolidation/bin-packing strategy explicitly designed to pair with Cluster Autoscaler: evict pods off under-used nodes to concentrate load so CA can scale those nodes down. This is the direct Karpenter-consolidation analog.
  • RemovePodsViolatingNodeAffinity / ...TopologySpreadConstraint / ...InterPodAntiAffinity β€” pull stateful pods (CNPG, etc.) back onto baseline nodes and re-spread after scale events (addresses the wedding-db pin-up above).
  • RemoveDuplicates β€” spread replicas of the same controller across nodes.

Guardrails (must-haves in the implementing PR):

  • Respect PodDisruptionBudgets (default) β€” the platform relies on PDBs (coredns, coroot, …).
  • Scope carefully around stateful workloads β€” CNPG / Longhorn / OpenBao / clickhouse-keeper / SPIRE evictions trigger failovers, so exclude or tightly bound those namespaces; keep evictLocalStoragePods: false.
  • Don't fight the CapacityBuffer β€” exclude the overprovisioning namespace and system-critical priority classes; ensure the descheduler doesn't churn the warm spare.
  • Honor node taints/labels so it never evicts from control-plane / baseline-pinned placements it shouldn't.

Rough size

Small–medium. It's one new HelmRelease + values + RBAC in k8s/providers/hetzner/infrastructure/controllers/descheduler/, wired into the controllers kustomization.yaml, with a conservative single-strategy policy to start. No cluster run needed to validate β€” both overlays must kubectl kustomize build and pass ksail workload validate + the Kubescape scan, per the repo's static-check contract. Roll out one strategy first (HighNodeUtilization), observe, then widen.

Acceptance criteria

  • Descheduler runs in the Hetzner cluster as a pinned GitOps HelmRelease, health-gated in the infrastructure layer.
  • Policy respects PDBs, excludes the overprovisioning namespace + system-critical priorities, and is scoped to avoid churning stateful workloads.
  • At least the HighNodeUtilization (CA-consolidation) strategy is enabled and demonstrably lets Cluster Autoscaler retire an under-utilized autoscale-cx* node it previously couldn't drain.
  • Both overlays build and pass CI static checks.

Part of #2043 (resilient, current, well-abstracted GitOps platform). Relevant to the autoscale-node churn seen around #2419 / #2363.

Activity

  1. coderabbitai commented on Jul 4, 2026

    @coderabbitai
    Contributor
    πŸ”— Related PRs

    #2328 - feat: install AWS Crossplane provider (IAM family) foundation [closed]


    πŸ“ Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    πŸ§ͺ Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    πŸ’¬ Have feedback or questions? Drop into our discord!

  2. moved this from πŸ‘€ In Review to βœ… Done in 🌊 Project Boardon Jul 4, 2026
  3. botantler-1 commented on Jul 4, 2026

    @botantler-1
    Contributor

    πŸŽ‰ This issue has been resolved in version 1.99.0 πŸŽ‰

    The release is available on GitHub release

    Your semantic-release bot πŸ“¦πŸš€

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions