Skip to content

Install Kerf on any Kubernetes cluster

This guide installs Kerf on a conformant Kubernetes cluster that is not Amazon EKS. The difference from the EKS guide is prices: instead of the AWS price list, the pricing service reads a price table you supply.

Note. EKS is the tested platform. The agent, webhook and recommender use only the Kubernetes API, PromQL and PostgreSQL, but Kerf's tests do not cover other distributions (the local development setup uses k3d).

The chart is the same: global.mode is hub on the hub cluster (hub, web UI, pricing), spoke on each workload cluster (agent, webhook, CRDs) and all for everything in one cluster.

Requirements

What Notes
Kubernetes 1.35 is the tested version. In-place resize needs 1.33 or later; before that Kerf falls back to rollout.
PostgreSQL 12 or later, for the hub. Connect directly or through a session-mode pooler, never a transaction-mode one: the hub elects its leader with an advisory lock.
Metrics store Prometheus, VictoriaMetrics, Thanos or Mimir, holding cAdvisor and kube-state-metrics series of every workload cluster (list). Simplest: every cluster remote-writes to one store with the label cluster=<cluster name>, which hub.config.prometheusURL points at. Other sources: Admin → Metrics sources.
cert-manager On every workload cluster, for the webhook's serving certificate.
Network Workload clusters reach the hub API over HTTPS. The API server reaches the nodes on port 9443 (the webhook). NetworkPolicies are off by default (networkPolicy.enabled).

1. Hub cluster

Create the Secrets: outside development installs the chart rejects inline secrets. The database and its user must exist; the hub applies the schema itself.

kubectl create namespace kerf
read -rs DB_PW   # URL-encode it if it holds special characters
kubectl -n kerf create secret generic hub-db \
  --from-literal=DATABASE_URL="postgres://kerf:${DB_PW}@db.example.com:5432/kerf?sslmode=verify-full"
unset DB_PW
kubectl -n kerf create secret generic hub-bootstrap-admin --from-literal=password="$(openssl rand -base64 24)"

For a private database CA, mount it with hub.database.caBundle and add sslrootcert=/etc/hub/db-ca/ca.pem to the URL. The chart generates the key that encrypts datasource credentials and keeps it on uninstall: back it up. Write values-hub.yaml:

global: {mode: hub, clusterName: hub-cluster}
hub:
  database: {existingSecret: hub-db}
  auth: {bootstrapAdmin: {existingSecret: hub-bootstrap-admin}}
  config:
    prometheusURL: http://prometheus.monitoring.svc:9090    # the built-in datasource `local`
    onboarding:
      hubURL: https://hub.example.com                       # the hub as agents reach it
      chart: oci://ghcr.io/kerf-dev/kerf/charts/kerf
      imageRegistry: ghcr.io/kerf-dev                       # for the wizard's install snippets
  ingress:
    enabled: true
    hosts: [{host: hub.example.com, paths: [{path: /, pathType: Prefix}]}]
    tls: [{hosts: [hub.example.com], secretName: hub-tls}]
web:
  token: {existingSecret: kerf-ui-token}
  ingress:
    enabled: true
    hosts: [{host: kerf.example.com, paths: [{path: /, pathType: Prefix}]}]
    tls: [{hosts: [kerf.example.com], secretName: kerf-ui-tls}]
pricing:
  provider: static
  vmURL: http://prometheus.monitoring.svc:9090    # holds kube_node_labels of the workload clusters
  hub: {tokenSecret: {name: kerf-pricing-token}}
helm upgrade --install kerf oci://ghcr.io/kerf-dev/kerf/charts/kerf --version <version> \
  -n kerf -f values-hub.yaml
kubectl -n kerf rollout status deploy/hub

Released charts pull the images of the same release from ghcr.io/kerf-dev/kerf. The ui and pricing tokens do not exist yet, so web and pricing wait for their Secrets. Sign in as admin with the password in the Secret hub-bootstrap-admin, change it, and create a token with role ui and one with role pricing under Admin → Access → Tokens. A token is shown once.

kubectl -n kerf create secret generic kerf-ui-token --from-literal=token="<ui token>"
kubectl -n kerf create secret generic kerf-pricing-token --from-literal=token="<pricing token>"
kubectl -n kerf rollout restart deploy/web      # nginx reads the token at start

Check: curl -sS https://hub.example.com/readyz prints ok.

Supply a price table

The chart's default provider is static. Without pricing.prices it uses the file in its image: approximate AWS list prices for development, which your nodes will not match. Set your own table, in USD per hour per node:

pricing:
  provider: static
  prices: |
    instanceTypes:                   # the shape of every type that has a price
      std-4x16: {vcpu: 4, memoryGiB: 16}
      big-16x64: {vcpu: 16, memoryGiB: 64}
    regions:
      on-prem-1:                     # region -> instance type -> prices
        std-4x16: {onDemand: 0.15}
        big-16x64: {onDemand: 0.60, spot: 0.25, spotByZone: {on-prem-1a: 0.22}}

spot applies to zones not listed in spotByZone. A price of 0, or none, counts as no price. The service exits on a table with an unknown instance type or a negative price: read kubectl -n kerf logs deploy/pricing. It matches each node, from kube_node_labels, to the table:

Node label Used as If missing
node.kubernetes.io/instance-type the instance type node skipped (no-instance-type)
topology.kubernetes.io/region, else topology.kubernetes.io/zone the region; a zone on-prem-1a gives on-prem-1 pricing.defaultRegion, else skipped (no-region)
karpenter.sh/capacity-type or eks.amazonaws.com/capacityType on-demand or spot on-demand

Nodes without these labels: choose your own instance type names, use them in the table, and label the nodes (kubectl label node <node> node.kubernetes.io/instance-type=std-4x16 topology.kubernetes.io/region=on-prem-1), then allowlist the labels in kube-state-metrics (metrics.node-labels). The service splits a node's price into a CPU and a memory rate. pricing.cpuMemoryWeight sets the weight of one vCPU in GiB: "0" derives it per instance family and falls back to 9, as it does for names of your own.

A skipped node uses a default rate in savings; Admin → Pricing can set a fixed rate for a cluster instead. Check kerf_pricing_nodes and the readiness check pricing.coverage.

2. Each workload cluster

Register the cluster: Clusters → Add cluster. Its name is a DNS label and must equal global.clusterName. The wizard shows the agent token once.

kubectl create namespace kerf
read -rs AGENT_TOKEN && kubectl -n kerf create secret generic kerf-agent-token --from-literal=token="$AGENT_TOKEN"; unset AGENT_TOKEN
global: {mode: spoke, clusterName: prod-eu1}
agent:
  hub:
    url: https://hub.example.com
  health:
    metricsURL: http://prometheus-operated.monitoring.svc:9090   # this cluster's Prometheus
    clusterLabel: ""                                             # its series carry no cluster label
helm upgrade --install kerf oci://ghcr.io/kerf-dev/kerf/charts/kerf --version <version> \
  -n kerf -f values-spoke.yaml --wait --timeout 10m
kubectl get --raw "/api/v1/namespaces/kerf/services/https:webhook:443/proxy/healthz"

The last command should end in the server could not find the requested resource: the webhook answered, so 9443 is open. A hub certificate from a private CA needs agent.hub.caBundle.

Readiness

The cluster should reach Metrics OK, then Ready for audit once the recommender has run (daily by default; an admin can start a run with POST /v1/admin/recompute?cluster=<cluster>), then Automation ready. Every check and its fix: Cluster readiness. Workloads stay in audit until you set a policy.