Skip to content

Vertical Pod Autoscaler ​

typekro/vpa installs the Vertical Pod Autoscaler (VPA) through Flux and gives you a typed factory for its API:

  • verticalPodAutoscaler: autoscaling.k8s.io/v1 VerticalPodAutoscaler, which recommends CPU and memory requests for a workload and, depending on its mode, applies them
  • vpaRecommendOnly: the same in updateMode: 'Off', which records recommendations and changes nothing

Verified against the Fairwinds vpa chart 5.1.0 (VPA 1.7.1, https://charts.fairwinds.com/stable).

Import ​

typescript
import { verticalPodAutoscaler, vpaBootstrap, vpaRecommendOnly } from 'typekro/vpa';

typekro/vpa is a subpath export only; it is not re-exported from typekro.

Quick example ​

typescript
import { vpaBootstrap } from 'typekro/vpa';

const factory = vpaBootstrap.factory('direct', { namespace: 'flux-system', waitForReady: true });

// Recommend only: no evictions and no webhook.
await factory.deploy({
  name: 'vpa',
  updater: { enabled: false },
  admissionController: { enabled: false },
});

'direct' applies the resources immediately. 'kro' emits a ResourceGraphDefinition and lets KRO reconcile it. Both produce the same HelmRelease.

Available factories ​

ExportKindScopeDescription
vpaBootstrapCompositionClusterRecommender, updater and admission controller
makeVpaBootstrap(options)CompositionClusterThe same, with build-time options
vpaHelmRepositoryBootstrapCompositionSingletonShared Fairwinds HelmRepository owner
verticalPodAutoscalerVerticalPodAutoscalerNamespaceTyped autoscaling.k8s.io/v1 VPA
vpaRecommendOnly(target, options)VerticalPodAutoscalerNamespaceAn Off-mode VPA for a targetRef or a workload resource
vpaRecommendationProvided(...vpas)Status helper—true once every VPA has a recommendation
vpaHelmRepository, vpaHelmReleaseFluxNamespaceThe Helm resources the bootstrap uses
validateVerticalPodAutoscalerSpec, validateVpaBootstrapConfig, findVpaAutoscalerConflictsValidators—Common-mistake checks (below)

Chart choice ​

There are two maintained charts. The bootstrap uses the Fairwinds vpa chart, the long-standing community chart that most clusters run. The vertical-pod-autoscaler chart in kubernetes/autoscaler (0.13.0, VPA 1.8.0) is newer and tracks VPA releases sooner, but its README still says it is under development and not ready for production use. Pin a different Fairwinds version to move within that chart; the values below are for 5.1.0.

Bootstrap composition ​

Components ​

ComponentWhat it doesNeeded for
RecommenderWatches usage and writes status.recommendationevery mode
UpdaterEvicts, or resizes in place, pods whose requests are far from the recommendationRecreate, InPlaceOrRecreate, InPlace
Admission controllerMutating webhook that writes recommended requests into new podsInitial, Recreate, InPlaceOrRecreate

Each is switched with <component>.enabled. A recommend-only install, where every VPA uses updateMode: 'Off', needs only the recommender. The VPA reads usage from metrics.k8s.io: install metrics-server first, or set metricsServer.enabled to install the chart's bundled subchart.

Runtime spec ​

FieldChart valueDefault
namerelease name, fullnameOverriderequired
namespaceinstall namespacevpa
versionchart version5.1.0
<component>.enabled<component>.enabledtrue
<component>.replicas<component>.replicaCount1
<component>.resources<component>.resources, merged by Helm with the chart'srequests 50m / 500Mi (recommender, updater), 50m / 200Mi (admission controller)
<component>.podDisruptionBudget<component>.podDisruptionBudget (rendered only above 1 replica){ maxUnavailable: 1 }
<component>.nodeSelector, .tolerations, .affinityplacementnone
priorityClassNameevery componentnone
serviceAccountAnnotationsevery component's ServiceAccountnone
metricsServer.enabledmetrics-server.enabledfalse

<component> is recommender, updater or admissionController. The chart grants the recommender and updater no leader-election lease, so keep them at one replica; extra replicas would work in parallel. The admission controller is stateless and can run more.

Recommender flags ​

Every flag is rendered, with its default when unset, so the Deployment shows the full configuration. A default is therefore pinned to the version below; when bumping the chart, re-check VPA_DEFAULT_RECOMMENDER_FLAGS and VPA_DEFAULT_UPDATER_FLAGS against that VPA version's flags.md.

FieldFlagDefault
logLevel--v4
podRecommendationMinCpuMillicores--pod-recommendation-min-cpu-millicores15 (chart)
podRecommendationMinMemoryMb--pod-recommendation-min-memory-mb100 (chart)
targetCpuPercentile--target-cpu-percentile0.9
targetMemoryPercentile--target-memory-percentile0.9
recommendationMarginFraction--recommendation-margin-fraction0.15
cpuHistogramDecayHalfLife, memoryHistogramDecayHalfLife--*-histogram-decay-half-life24h
memoryAggregationInterval, memoryAggregationIntervalCount--memory-aggregation-interval*24h, 8 (an 8-day memory window)
storage--storagecheckpoint
historyLength, prometheusAddress--history-length, --prometheus-address (used with storage: 'prometheus')8d, http://prometheus.monitoring.svc
recommenderName--recommender-namedefault

With checkpoint storage the recommender keeps its own history in VerticalPodAutoscalerCheckpoint objects and starts from scratch on a new cluster. With Prometheus storage it reads up to historyLength of past usage on start.

The updater takes minReplicas (--min-replicas, default 2: it does not evict a workload with fewer live replicas) and evictionTolerance (--eviction-tolerance, default 0.5). Other flags go through the build-time values, as <component>.extraArgs entries:

typescript
import { makeVpaBootstrap } from 'typekro/vpa';

const vpa = makeVpaBootstrap({
  values: { updater: { extraArgs: { 'in-place-skip-disruption-budget': true } } },
});

Admission controller certificate ​

The webhook needs a serving certificate. By default (certificate.generate: true) the chart's kube-webhook-certgen hook Jobs create the Secret <name>-tls-secret and patch the CA bundle into the MutatingWebhookConfiguration.

To use cert-manager instead, issue a Certificate for <name>-webhook.<namespace>.svc, point the controller at its Secret and let cainjector fill the CA bundle:

typescript
import { VPA_CERT_MANAGER_TLS_SECRET_KEYS, makeVpaBootstrap } from 'typekro/vpa';

await makeVpaBootstrap({
  // Reload the certificate when cert-manager renews it.
  values: { admissionController: { extraArgs: { 'reload-cert': true } } },
})
  .factory('direct', { namespace: 'flux-system' })
  .deploy({
    name: 'vpa',
    admissionController: {
      certificate: {
        generate: false,
        secretName: 'vpa-webhook-tls',
        secretKeys: [...VPA_CERT_MANAGER_TLS_SECRET_KEYS],
      },
      webhook: { annotations: { 'cert-manager.io/inject-ca-from': 'vpa/vpa-webhook-tls' } },
    },
  });

webhook.failurePolicy defaults to Ignore: if the webhook is down, pods start with the requests in their manifest. webhook.namespaceSelector and objectSelector limit which pods it sees.

Build-time options ​

OptionDefaultEffect
namespaceOwnership'external''external' lets Flux create a missing namespace; 'owned' makes it part of the graph
install, upgrade, driftDetectionsee belowThe Flux lifecycle options every TypeKro HelmRelease factory takes. See Install, upgrade and CRD policy
valuesnoneRaw chart values, deep-merged last (objects merge, lists replace)
name, kindvpa-bootstrap, VpaBootstrapRGD name and kind

CRDs ​

The chart ships the CRDs in crds/, which Helm installs but never upgrades. The HelmRelease sets install.crds and upgrade.crds to CreateReplace, so Flux replaces them on every chart upgrade. Helm never deletes crds/ CRDs, so uninstalling the bootstrap keeps every VPA object.

One install per cluster ​

The chart's ClusterRoles and ClusterRoleBindings have fixed names (vpa-actor, vpa-checkpoint-actor, vpa-evictioner, ...), not names derived from the release. Only one bootstrap can run per cluster, and it clashes with a VPA the platform already manages, such as GKE's built-in vertical Pod autoscaling or a VPA add-on. Use the managed one there, and declare only verticalPodAutoscaler objects.

Teardown ​

Removing the bootstrap uninstalls the release, but two objects the components create at run time stay in the install namespace: the certgen Secret <name>-tls-secret and the admission controller's leader Lease. Delete the namespace afterwards to remove them.

Status ​

FieldSource
readyHelmRelease Ready=True for its current generation
failedHelmRelease Ready=False for its current generation
phaseReady, Installing or Failed
versionChart version Flux installed (status.history), '' until then

VerticalPodAutoscaler ​

typescript
import { verticalPodAutoscaler } from 'typekro/vpa';

verticalPodAutoscaler({
  name: 'worker',
  namespace: 'jobs',
  spec: {
    targetRef: { apiVersion: 'apps/v1', kind: 'Deployment', name: 'worker' },
    updatePolicy: { updateMode: 'InPlaceOrRecreate', minReplicas: 2 },
    resourcePolicy: {
      containerPolicies: [
        {
          containerName: '*',
          minAllowed: { cpu: '100m', memory: '128Mi' },
          maxAllowed: { cpu: '2', memory: '4Gi' },
          controlledValues: 'RequestsOnly',
        },
        { containerName: 'log-shipper', mode: 'Off' },
      ],
    },
  },
  id: 'workerVpa',
});
FieldNotes
targetRefkind and name of a Deployment, StatefulSet, DaemonSet, Job, CronJob, ReplicaSet or any resource with a scale subresource
updatePolicy.updateModeOff, Initial, Recreate, InPlaceOrRecreate, InPlace; Auto is a deprecated alias of Recreate. The CRD sets no default; the VPA treats an unset mode as Recreate
updatePolicy.minReplicasOverrides the updater's --min-replicas for this VPA
updatePolicy.evictionRequirementsOnly evict when the target moved up (TargetHigherThanRequests) or down
resourcePolicy.containerPoliciesPer container ('*' for all): mode, minAllowed, maxAllowed, controlledResources (cpu, memory), controlledValues (RequestsAndLimits keeps the limit/request ratio, RequestsOnly leaves limits alone)
recommendersAt most one recommender name; the default recommender when empty

InPlaceOrRecreate and InPlace resize running pods, which needs in-place pod resize in the cluster (beta from Kubernetes 1.33, on by default). InPlace never evicts. In VPA 1.7 it is behind the InPlace feature gate: without it the admission webhook rejects the VPA. Enable it on the admission controller and the updater:

typescript
import { makeVpaBootstrap } from 'typekro/vpa';

const gate = { 'feature-gates': 'InPlace=true' };
const vpa = makeVpaBootstrap({
  values: { admissionController: { extraArgs: gate }, updater: { extraArgs: gate } },
});

vpaRecommendOnly takes a targetRef or the workload resource itself:

typescript
import { simple } from 'typekro';
import { vpaRecommendOnly } from 'typekro/vpa';

const api = simple.Deployment({ name: 'api', image: 'nginx', id: 'api' });
vpaRecommendOnly(api, { id: 'apiVpa' }); // name 'api', updateMode 'Off'

Readiness and status ​

A VPA is ready when its RecommendationProvided condition is True. That needs a running recommender, metrics, and pods of the target that have run for a while, typically a minute or two. Pass readiness: 'accepted' to be ready as soon as the API server has stored the object, for example when the workload is created in the same deploy, or deploy with waitForReady: false.

StateReadyReason
No conditions yetnoStatusMissing (is the recommender running?)
NoPodsMatched=TruenoNoPodsMatched
ConfigUnsupported=TruenoConfigUnsupported
RecommendationProvided=TrueyesRecommendationProvided
otherwisenoRecommendationPending

status.recommendation.containerRecommendations[] carries target, lowerBound, upperBound and uncappedTarget (the target before minAllowed/maxAllowed) per container. In a composition's status, use vpaRecommendationProvided(...) rather than joining checks with &&:

typescript
return { recommended: vpaRecommendationProvided(apiVpa, workerVpa) };

VPA and HPA together ​

Do not let a VPA and a HorizontalPodAutoscaler act on the same resource of the same workload. An HPA scaling on CPU utilization divides usage by the CPU request; a VPA that changes that request changes the HPA's input, and the two chase each other. A KEDA ScaledObject creates an HPA, so the same applies to its cpu and memory triggers.

Safe combinations:

  • the VPA in updateMode: 'Off' (vpaRecommendOnly), for recommendations only
  • the VPA limited with controlledResources: ['memory'] while the HPA scales on CPU
  • the HPA scaling on a custom or external metric (requests per second, queue depth) rather than CPU or memory

verticalPodAutoscaler checks the other resources already declared in the same composition and logs a warning when an autoscaling HPA or a keda.sh ScaledObject scales its target on a resource the VPA sets. Containers without a policy of their own follow the '*' policy, or get both resources when there is none, so a policy list that only excludes a sidecar ([{ containerName: 'istio-proxy', mode: 'Off' }]) still counts. Autoscalers created elsewhere, or targets named by a schema reference, are not visible to the check. findVpaAutoscalerConflicts(spec, namespace) returns the same findings.

Validation ​

verticalPodAutoscaler throws on mistakes the VPA rejects and logs warnings for legal but risky settings. The validators return both. Values only known at reconcile time are skipped.

CheckSeverity
targetRef without kind or nameerror
More than one entry in recommenderserror
Two container policies for the same containerNameerror
updateMode: 'Auto' (deprecated)warning
updateMode: 'InPlace' (needs the InPlace feature gate)warning
controlledResources: []warning
An HPA or ScaledObject in the composition on the same target and resourcewarning
Bootstrap with the recommender off, the updater on without the admission controller, more than one recommender or updater replica, or Prometheus storage without an addresswarning

Example ​

examples/vpa-rightsizing.ts installs the recommender only, gives a CPU-scaled Deployment a recommend-only VPA next to its HPA, and lets a second Deployment's VPA resize it in place within bounds.

See also ​

Released under the Apache 2.0 License.