Install the agent with Helm
One release per cluster. The agent reads the cluster every hour, read-only, and sends what it reads to your organization in KubeOn and nowhere else.
On this page
Before you start
- metrics-server in the cluster. EKS, AKS and GKE include it.
- Permission to create a namespace, a ClusterRole and a ClusterRoleBinding, once, at install.
- Helm 3.8 or later, which installs charts from OCI registries. Helm 4 works too.
- Outbound HTTPS from the cluster to
app.kubeon.io. See network requirements. - The Admin or Platform engineer role in KubeOn, to create the cluster's token.
1. Create the cluster's token
In KubeOn, open Settings > Integrations > Add cluster, enter the cluster's name as you want it to appear, and choose Create token. KubeOn shows the token once, with the Helm command that installs the agent with it:
helm upgrade --install kubeon-agent oci://registry.kubeon.io/kubeon/charts/kubeon-agent \ --namespace kubeon --create-namespace \ --set clusterName=prod-use1-a \ --set kubeon.agentToken=<token from KubeOn>That command is enough for a trial. In production, keep the token in a Secret, created by hand or synced from your secret manager, so it is not in shell history or in Git:
kubectl create namespace kubeonkubectl -n kubeon create secret generic kubeon-agent-token \ --from-literal=agent-token=<token from KubeOn>2. Install
# kubeon-agent.yamlclusterName: prod-use1-a # how the cluster appears in KubeOnkubeon: existingSecret: kubeon-agent-token # the Secret holding the cluster's tokenprometheusUrl: http://prometheus-server.monitoring.svc:9090 # optionalhelm upgrade --install kubeon-agent oci://registry.kubeon.io/kubeon/charts/kubeon-agent \ --version 0.6.0 \ --namespace kubeon --create-namespace \ -f kubeon-agent.yaml3. Check the first run
The CronJob runs at 7 minutes past each hour. To send the first snapshot now:
kubectl -n kubeon get cronjob,jobkubectl -n kubeon create job --from=cronjob/kubeon-agent kubeon-agent-first-runkubectl -n kubeon logs job/kubeon-agent-first-run# the last line reads: sent snapshot to https://app.kubeon.io (... bytes)Open Diagnostics in KubeOn. The cluster appears as soon as the job finishes, with a check for each source: metrics-server, Prometheus and volumes. Its nodes, namespaces and workloads appear under Clusters. With Prometheus set, the first run also sends the last 15 days of hourly usage, so cost detail starts before the install.
The first two days
To see what the agent collects without sending anything, run it once in dry-run mode:
# Collects and prints row counts per kind. Sends nothing.kubectl -n kubeon run kubeon-agent-dry-run --rm -it --restart=Never \ --image=registry.kubeon.io/kubeon/kubeon-agent:0.6.0 \ --overrides='{"spec":{"serviceAccountName":"kubeon-agent"}}' \ --env=K8S_IN_CLUSTER=true --env=CLUSTER_NAME=prod-use1-a \ --env=DATABASE_URL=sqlite:///:memory: --env=JWT_SECRET=unused \ -- python collector.pyc --dry-runWhere the data goes
Each run, the agent asks KubeOn for an upload link, sends the snapshot on it, and KubeOn ingests the snapshot into your organization at once. KubeOn issues the link for your organization's own storage, and it expires after 15 minutes. There is no bucket, region or cloud role to configure, and no second copy: the chart refuses to install, and the agent refuses to run, with an endpoint that is not a kubeon.io address.
What the chart installs
| Resource | Name | Notes |
|---|---|---|
| ServiceAccount | kubeon-agent | No cloud identity annotations |
| ClusterRole and binding | kubeon-agent-reader | get, list and watch only |
| Secret | kubeon-agent-token | Only when the token is passed as kubeon.agentToken |
| CronJob | kubeon-agent | 7 * * * *, concurrencyPolicy: Forbid, 10-minute deadline, 2 retries |
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: kubeon-agent-readerrules: - apiGroups: [""] resources: [nodes, pods, namespaces, persistentvolumes, persistentvolumeclaims, services] verbs: [get, list, watch] - apiGroups: [apps] resources: [deployments, statefulsets, daemonsets, replicasets] verbs: [get, list, watch] - apiGroups: [batch] resources: [jobs, cronjobs] verbs: [get, list, watch] - apiGroups: [storage.k8s.io] resources: [storageclasses] verbs: [get, list, watch] - apiGroups: [metrics.k8s.io] resources: [nodes, pods] verbs: [get, list] - apiGroups: [karpenter.sh] # NodePools, on Karpenter clusters resources: [nodepools, nodeclaims] verbs: [get, list, watch] - apiGroups: [autoscaling] resources: [horizontalpodautoscalers] verbs: [get, list, watch] - apiGroups: [argoproj.io] # where workloads are deployed from resources: [applications, rollouts] verbs: [get, list, watch] - apiGroups: [kustomize.toolkit.fluxcd.io, helm.toolkit.fluxcd.io, source.toolkit.fluxcd.io] resources: [kustomizations, helmreleases, gitrepositories] verbs: [get, list, watch]No write verbs, no Secrets, no ConfigMaps. The pod runs as uid 1000 with a read-only root filesystem, no privilege escalation and every capability dropped. The image holds the agent's compiled modules and their dependencies only.
Prometheus
Optional, and recommended. Without it, the agent reads each pod's usage once an hour from metrics-server. With prometheusUrl set, it also sends:
- Usage averaged over each whole hour instead of one reading, so spiky workloads are sized correctly.
- The last 15 days of hourly usage, once, when the cluster connects, so savings show from the first day instead of after a week.
- Disk usage per volume, for over-provisioned volume findings.
- GPU utilization per pod, where the NVIDIA DCGM exporter runs.
| Series | Comes from | Used for |
|---|---|---|
container_cpu_usage_seconds_total, container_memory_working_set_bytes | cAdvisor, in the kubelet | Usage per pod |
kube_pod_container_resource_requests, kube_pod_info, kube_pod_owner, kube_node_status_capacity | kube-state-metrics | Requests, node and workload in the 15-day history |
kubelet_volume_stats_used_bytes, kubelet_volume_stats_capacity_bytes | The kubelet | Volume usage |
DCGM_FI_DEV_GPU_UTIL | NVIDIA DCGM exporter (optional) | GPU utilization |
kube-prometheus-stack collects all of these by default. Set prometheusUrl to the Prometheus service's address inside the cluster; kubectl get svc -A | grep prometheus lists it. The agent queries it without signing in, so a Prometheus that requires authentication, such as Amazon Managed Service for Prometheus, is not supported yet. After the next run, the Prometheus check in Diagnostics shows whether the queries succeed.
Karpenter clusters
Where Karpenter runs, the agent also reads NodePools: their requirements, limits, disruption settings and provisioned capacity. The cluster's Nodes tab lists each pool and flags pools pinned to a single instance type, which Karpenter cannot shrink. Node size proposals for the cluster are written as a change to that NodePool, by name. The ClusterRole already covers nodepools and nodeclaims; nothing else is needed.
Values
| Value | Default | Effect |
|---|---|---|
clusterName | required | How the cluster appears in KubeOn |
kubeon.existingSecret | empty | A Secret holding the token under the key agent-token |
kubeon.agentToken | empty | The token itself, when there is no Secret |
kubeon.directUpload | true | false sends snapshots through app.kubeon.io, for firewalls that allow only that address |
prometheusUrl | empty | Adds hourly usage history and volume usage; with the NVIDIA DCGM exporter scraped, GPU utilization per pod |
pricing.rateCard | empty | Clusters with no cloud bill: the rate card that prices their nodes |
schedule | 7 * * * * | Hourly is what Diagnostics expects |
resources | 100m CPU, 256Mi; 512Mi limit | Raise the memory limit for clusters with tens of thousands of pods |
nodeSelector, tolerations | empty | Where the agent pod runs |