PI-GROUPE
KubernetesFinOpsSREObservabilityDevOpsInfrastructure

Ballast and Attune: automating vertical sizing without restarting your pods

The native VPA kills pods to resize them. Ballast smooths the recommendations, Attune applies the resize in place. Installation, tuning and guardrails for a chain that reclaims 30 to 40 % of resources.

The observation is universal: on any production cluster, declared resource requests far exceed actual consumption. A factor of three to five is nothing unusual. A developer who does not know how much their service consumes asks generously — and nobody ever comes back to correct it.

The Vertical Pod Autoscaler addresses that problem, but it carries one flaw that disqualifies it in production: to apply a new size, it deletes the pod. On a stateful service or a long-session workload, that is unacceptable.

Two components change the picture. Ballast smooths and secures the recommendations. Attune applies them in place, building on in-place resource resizing, stable since Kubernetes 1.33.

Why the VPA alone falls short

ProblemObserved effect
Eviction as the application mechanismService interruption on stateful workloads
Reaction to spikesA single peak inflates the recommendation lastingly
No notion of headroomRecommendation glued to consumption, with no reserve
Conflict with the HPAVPA and HPA on CPU contradict each other
Blind to budgetNo global ceiling

Ballast deals with the first four points, Attune with the first.

Installing the chain

The prerequisite fits in one line: the InPlacePodVerticalScaling feature must be on. Since 1.33 it is enabled by default in beta.

bash
kubectl get --raw='/readyz?verbose' | grep -i inplace
# failing that, check the control plane:
# --feature-gates=InPlacePodVerticalScaling=true
yaml
# Ballast values, managed by Argo CD
recommandeur:
  source: prometheus
  prometheusUrl: http://prometheus.observabilite.svc:9090
  fenetre: 14d                # two weeks cover the weekly cycle
  quantileProcesseur: 0.90    # p90: rare spikes stay ignored
  quantileMemoire: 0.98       # memory: near the maximum, an overrun kills
  headroomProcesseur: 1.15
  headroomMemoire: 1.25

smoothing:
  actif: true
  methode: ewma               # moyenne mobile exponentielle
  alpha: 0.25
  variationMax: 0.30          # never more than 30 % drift per adjustment
  minPeriod: 6h              # minimum delay between two adjustments

gardeFous:
  processeurMin: 50m
  memoireMin: 64Mi
  processeurMax: "8"
  memoireMax: 32Gi
  ratioLimiteSurDemande: 2.0

The two differentiated quantiles are the most important setting. CPU compresses: a throttled service slows down. Memory does not compress: an overrun kills the container. Hence p90 on one and p98 on the other.

yaml
# values Attune
application:
  mode: in-place
  fallbackQueue: true            # one pod at a time per deployment
  attendreDisponibilite: true
  respecterPDB: true
  fenetres:
    - jours: ["Mon","Tue","Wed","Thu"]
      debut: "10:00"
      fin: "16:00"            # working hours: the team is around if it goes wrong
      timezone: "Europe/Paris"
fallback:
  surEchecInPlace: defer   # never an automatic eviction
  tentativesMax: 3

The fallback: defer deserves emphasis. If the in-place resize fails — which happens when memory shrinks beyond what the kernel accepts — Attune defers rather than evicting. An automatic overnight eviction on a critical service is a guaranteed incident.

Declaring a policy

yaml
apiVersion: ballast.io/v1alpha1
kind: RightSizingPolicy
metadata:
  name: api-commandes
  namespace: production
spec:
  cible:
    apiVersion: apps/v1
    kind: Deployment
    name: api-commandes
  mode: Auto
  conteneurs:
    - nom: application
      controle: [cpu, memory]
      minAutorise: { cpu: 200m, memory: 512Mi }
      maxAutorise: { cpu: "4",  memory: 8Gi }
    - nom: sidecar-proxy
      controle: [memory]        # proxy CPU follows traffic, not size
  contraintes:
    respectHPA: true          # the HPA keeps control of CPU
    variationMaxParPas: 0.25
    adjustmentsMaxParJour: 3
  observation:
    dureeMin: 7d                # no decision before a week of history

The respectHPA constraint avoids the classic conflict. When an HPA sets replica count from CPU, letting the VPA touch CPU creates an unstable loop: the VPA lowers the request, relative utilisation rises, the HPA adds replicas, per-pod load falls, the VPA lowers again.

The rule: HPA on CPU, Ballast on memory. Or HPA on a business metric, and Ballast on both.

Verify before automating

Never switch straight to Auto. The Recommend mode produces recommendations without applying them.

promql
# CPU waste: requested minus used, per namespace
sum by (namespace) (
  kube_pod_container_resource_requests{resource="cpu"}
) -
sum by (namespace) (
  rate(container_cpu_usage_seconds_total{container!=""}[7d])
)

# Gap between the recommendation and the current request
(
  ballast_recommendation_cpu_cores
  /
  on(namespace, pod, container) kube_pod_container_resource_requests{resource="cpu"}
) < 0.5

The second query lists the containers requesting more than twice what they need. Start with those: maximum gain, minimum risk.

The guardrails I impose

A global ceiling per namespace. Ballast adjusts upwards as well as downwards. Without a ceiling, an abnormal load can inflate requests until the cluster saturates.

yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: ceiling-production
  namespace: production
spec:
  hard:
    requests.cpu: "400"
    requests.memory: 1600Gi

An explicit exclusion for sensitive workloads. Databases, message queues, anything with sawtooth consumption.

yaml
metadata:
  annotations:
    ballast.io/exclude: "true"
    ballast.io/exclude-reason: "consommation irrégulière, dimensionnement manuel validé"

The reason annotation is not decorative: six months later, nobody remembers why the exclusion exists.

An alert on bursts of adjustment.

yaml
- alert: AjustementsBallastEnRafale
  expr: sum(increase(attune_resize_total[1h])) by (namespace) > 20
  for: 15m
  labels: { severity: warning }
  annotations:
    summary: "{{ $value }} resizes in one hour on {{ $labels.namespace }}"
    description: "Smoothing is not doing its job: check alpha and minPeriod."

What it returns

On a mid-sized production cluster — a hundred and fifty deployments, eight hundred pods — the full rollout reclaimed 34 % of requested CPU and 28 % of memory in six weeks, without a single incident tied to resizing.

The gain is not immediate. The first week goes into observation, the second into validation in recommendation mode, and the progressive adjustment spreads over a month. That is precisely what smoothing imposes — and it is what makes the operation safe.

One final point: that gain evaporates if nobody watches. New workloads arrive with their oversized requests. Ballast only holds because it runs continuously.