Karpenter provider for Clever Cloud

July 28, 2026 · View on GitHub

Continuous integration

A Karpenter cloud provider that autoscales Clever Kubernetes Engine (CKE) clusters through Clever Cloud's NodeGroup custom resources

How it works

This project implements Karpenter's CloudProvider interface on top of the Clever Cloud NodeGroup API (nodegroups.api.clever-cloud.com/v1) that every CKE cluster serves. Karpenter observes pending pods, provisions exactly the nodes they need — one NodeGroup per node — then consolidates the cluster to keep costs down. The Clever Cloud operator upstream turns those NodeGroups into VMs.

Almost everything goes through the cluster's own Kubernetes API. The one exception is the dynamic-pricing refresher, enabled by default, which reads prices from Clever Cloud's public, token-less API; disable it with settings.pricing.enabled=false to keep the controller fully in-cluster.

Status

The provider is under development: you can use it, but it may have bugs or unimplemented features. Its behavior (provisioning times, quota handling, instance capacities) has been validated end-to-end on a live CKE cluster — see docs/E2E-RESULTS.md. Each release publishes the controller image and both Helm charts to ghcr.io.

Install

To deploy the provider you will need a running CKE cluster (Kubernetes ≥ 1.34), the kubectl command with cluster-admin access and helm. The step-by-step installation guide covers CRD handling, verification, upgrades and uninstall.

Warning: Do not enable CKE's own autoscalingEnabled on the cluster alongside this provider — two autoscalers will fight over the same NodeGroups.

From the published charts

Each release publishes the karpenter-crd and karpenter charts to ghcr.io as OCI artifacts, versioned on the release tag without the v prefix (release v0.12.0 → chart version 0.12.0). Install the CRDs first, then the controller — no image settings are needed, the chart pulls the matching published image by default:

$ helm upgrade --install karpenter-crd \
    oci://ghcr.io/clevercloud/karpenter-provider-clever-cloud/karpenter-crd \
    --version <version> --namespace karpenter --create-namespace

$ helm upgrade --install karpenter \
    oci://ghcr.io/clevercloud/karpenter-provider-clever-cloud/karpenter \
    --version <version> --namespace karpenter --create-namespace --wait

Finally, create a NodePool and a CleverNodeClass to start provisioning nodes — see the examples/ catalog.

From source

You will need some tools on your computer to build the provider, at least the git, go and docker commands. So, firstly, retrieve the source from GitHub using the following command.

$ git clone https://github.com/CleverCloud/karpenter-provider-clever-cloud.git

or

$ gh repo clone CleverCloud/karpenter-provider-clever-cloud

Then, go into the newly created folder where the source code is located.

$ cd karpenter-provider-clever-cloud

At this step, you can choose to build the binary and run it directly, or build the docker image, push it to your registry and deploy the provider into your kubernetes cluster through the helm charts.

Build the binary

To build the binary, you can use the following command:

$ make build

The controller binary will be located at bin/karpenter-clevercloud. You can run it locally against your current kubeconfig (leader election disabled):

$ make run

Build the docker image

To build the docker image and push it to your registry, you can use the following commands:

$ make image IMAGE=<your-registry>/karpenter-clevercloud TAG=v0.12.0
$ docker push <your-registry>/karpenter-clevercloud:v0.12.0

From the helm charts

Two charts are located under charts/: karpenter-crd installs and upgrades the CustomResourceDefinitions, and karpenter installs the controller stack (Deployment kept off Karpenter-managed nodes, RBAC, metrics Service, PodDisruptionBudget). Install the CRDs first, then the controller, pointing it at the image you pushed:

$ helm upgrade --install karpenter-crd charts/karpenter-crd \
    --namespace karpenter --create-namespace

$ helm upgrade --install karpenter charts/karpenter \
    --namespace karpenter --create-namespace \
    --set image.repository=<your-registry>/karpenter-clevercloud \
    --set image.tag=v0.12.0 \
    --wait

Finally, create a NodePool and a CleverNodeClass to start provisioning nodes. The examples/ catalog covers the common use cases, each validated on a live CKE cluster.

$ kubectl apply -f examples/v1/general-purpose.yaml

Credentials

No Clever Cloud API token or credentials are required. The provider drives the in-cluster NodeGroup API that every CKE cluster serves; the Clever Cloud operator upstream reconciles NodeGroups into VMs with its own credentials.

The dynamic-pricing refresher (settings.pricing.enabled, on by default) additionally requires outbound HTTPS to api.clever-cloud.com, but still uses no token — the endpoints it reads are public. Disable it (settings.pricing.enabled=false) if your cluster forbids that egress.

Configuration

Global

The controller is configured through environment variables, all set by the helm chart from its values:

NameKindDefaultRequiredDescription
CLEVER_CLOUD_REGIONStringparnoRegion/zone advertised on instance types (CKE is Paris-only today); also the price-system zone_id when the refresher is enabled
LOG_LEVELStringinfonodebug, info or error
METRICS_PORTInteger8080noPort of the /metrics endpoint
HEALTH_PROBE_PORTInteger8081noPort of the liveness/readiness probes
DISABLE_LEADER_ELECTIONBooleanfalsenoUseful for single-replica dev setups
BATCH_MAX_DURATIONDuration10snoMaximum pod batching window before provisioning
BATCH_IDLE_DURATIONDuration1snoIdle pod batching window before provisioning
FEATURE_GATESStringNodeRepair=falsenoKarpenter feature gates
FLAVORS_CONFIG_PATHString(unset)noPath to a YAML list of per-flavor overrides; set by the chart when settings.flavors is non-empty
PRICING_REFRESH_ENABLEDBooleanfalsenoEnable the dynamic price/flavor refresher. The binary defaults off; the shipped chart and manifest set it true (settings.pricing.enabled, on by default)
PRICING_REFRESH_PERIODDuration12hnoHow often prices and the available-flavor list are refreshed
PRICING_API_URLStringhttps://api.clever-cloud.comnoBase URL of the Clever Cloud public API, used for both endpoints (override for a proxy or testing)
PRICING_PRODUCT_URLString(derived from PRICING_API_URL)noFull URL of the kubernetes-product endpoint; overrides the base for this endpoint only
PRICING_PRICE_SYSTEM_URLString(derived from PRICING_API_URL)noFull URL of the billing/price-system endpoint; overrides the base for this endpoint only
CLEVER_CLOUD_TOPOLOGYString(unset)noOptionally restrict the flavor catalogue to one CKE topology; unset takes the union of all

Flavor catalogue

By default the controller ships a built-in catalogue (2XSXL) with measured/estimated capacities and the documented public-beta prices. settings.flavors lets you overlay per-flavor overrides on top of that base catalogue — the chart renders it into a ConfigMap mounted at /etc/karpenter/flavors/flavors.yaml and points FLAVORS_CONFIG_PATH at it. Every field except name is optional: set only what you want to pin, the rest fall through to the base value (or, with the refresher enabled, the live value).

settings:
  flavors:
    - name: M             # required, as accepted by the NodeGroup API (uppercase)
      priceHourly: 0.1167 # pin the price; cpu/memoryKi stay dynamic/default
    - name: CUSTOM        # a flavor absent from the base must supply all fields
      cpu: 2
      memoryKi: 2097152
      priceHourly: 0.01

cpu/memoryKi self-correct at runtime from observed node capacity, so they only need to be close enough for the scheduler to pick a flavor; prices are used as-is for cost-based consolidation. Overrides always win and are re-applied after every dynamic refresh. Leave settings.flavors empty to use the base catalogue unchanged.

Dynamic pricing

Enabled by default (settings.pricing.enabled: true). A background refresher, every PRICING_REFRESH_PERIOD (default 12h), queries Clever Cloud's public, unauthenticated API at PRICING_API_URL for:

  • the per-resource rates (/v4/billing/price-system, keyed by zone_id = CLEVER_CLOUD_REGION), from which per-flavor prices are recomputed; and
  • the available-flavor list (/v4/kubernetes-product), which drives which flavors are offered. By default this is the union of every topology's list. The per-topology lists are a product listing rather than an admission rule — a NodeGroup asking for a flavor its own topology does not advertise is provisioned anyway — so restricting to one topology could only shrink a working catalogue. Set CLEVER_CLOUD_TOPOLOGY only if you deliberately want that restriction.

Both endpoints default to PRICING_API_URL + their standard path. You can point them elsewhere with settings.pricing.apiURL (base, shared) or, if the two APIs must live at different hosts/paths, override each independently with settings.pricing.kubernetesProductURL / settings.pricing.priceSystemURL.

Per-flavor cpu/memory sizing is not exposed by the API, so it stays seeded statically (and self-corrects at runtime as above). A flavor the API offers but the seed does not know is skipped with a log line. If a refresh fails (API unreachable, malformed response…), the last-known-good catalogue is kept and the refresher retries sooner.

Disable it to keep the controller fully in-cluster (it then uses the built-in static catalogue):

helm upgrade --install karpenter oci://ghcr.io/clevercloud/karpenter-provider-clever-cloud/charts/karpenter \
  --set settings.pricing.enabled=false

Precedence: any settings.flavors overrides are re-applied on top of every refresh, so they always win. Egress: the refresher needs to reach api.clever-cloud.com on TCP 443. If your cluster restricts egress with NetworkPolicies, allow that destination from the controller pod (the chart ships no NetworkPolicy) or disable the refresher. No token is required.

NodePool

A minimal NodePool targeting the CleverNodeClass below:

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      nodeClassRef:
        group: karpenter.clever-cloud.com
        kind: CleverNodeClass
        name: default
      requirements:
        - key: node.kubernetes.io/instance-type
          operator: In
          values: ["2XS", "XS", "S", "M"]
      expireAfter: Never
  limits:
    cpu: "16"
    memory: 16Gi
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 30s

Size limits against your organisation quota: the default org quota is 40 vCPU / 40 GB RAM including the control plane (a 3-node S control plane consumes 24 GB of it).

CleverNodeClass

apiVersion: karpenter.clever-cloud.com/v1alpha1
kind: CleverNodeClass
metadata:
  name: default
spec:
  labels:            # extra node labels applied at the NodeGroup level,
    team: platform   # visible before Karpenter registration completes

Changing a NodeClass marks the NodeClaims built from it as drifted; Karpenter then replaces those nodes rolling-style.

Targeting Karpenter nodes

A CKE cluster always has capacity Karpenter did not create — a schedulable control-plane node on ALL_IN_ONE, a pre-existing node pool on the topologies whose control plane runs outside the cluster. To steer a workload onto Karpenter's own (auto-scaled) nodes, require the label Karpenter stamps on them and on nothing else:

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
            - key: karpenter.sh/nodepool
              operator: Exists

(clever-cloud.com/cluster-node-role: worker is not equivalent: on the topologies where the control plane is outside the cluster, the pre-existing pool is made of worker nodes too.)

License

See the license.

Getting in touch

  • Open an issue for bugs or feature requests