This guide walks through the most common path for provisioning Talos Linux worker nodes on Hetzner Cloud with karpenter-provider-hetzner.
See examples/talos-nodeclass.yaml for a
ready-to-apply NodeClass and NodePool you can adapt.
Talos does not use cloud-init or SSH. A node's entire runtime configuration comes from a single "machineconfig" YAML document supplied as the server's user-data at creation time. For worker nodes this document must contain:
- The cluster CA certificate (so the worker trusts the control plane).
- A bootstrap token (so the worker can join via the Kubernetes API).
- The control-plane endpoint (IP or DNS name + port 6443).
These are secrets. They must never appear in a NodeClass spec, a Helm values
file, or any git-tracked file. userDataSecretRef exists precisely to keep
them out of both.
# Generate a full set of cluster secrets and configs.
talosctl gen config <cluster-name> https://<control-plane-ip>:6443 \
--output-dir /tmp/talos-configs
# /tmp/talos-configs/worker.yaml is the worker machineconfig.
# Store it in a Secret immediately; delete the plaintext copy when done.If your cluster already exists and you need to regenerate configs without
rotating the CA, pass --with-secrets <secrets-bundle> (a file produced by
talosctl gen secrets and stored securely out of band).
If you previously ran
cluster-autoscaler-hetzner,
your Hetzner project likely has an autoscaler config Secret (often
cluster-autoscaler-hetzner in kube-system). That Secret embeds a
worker_cloud_init_<nodepool> key containing the already-rendered worker
machineconfig.
# Extract it:
kubectl get secret cluster-autoscaler-hetzner \
--namespace kube-system \
-o jsonpath='{.data.worker_cloud_init_default}' \
| base64 -d > /tmp/worker.yamlInspect /tmp/worker.yaml to confirm it contains the current cluster CA and a
valid join token before storing it as a Karpenter Secret (tokens expire).
kubectl create secret generic talos-worker-machineconfig \
--namespace kube-system \
--from-file=worker.yaml=/tmp/worker.yaml
# Delete the plaintext copy.
rm /tmp/worker.yamlSecurity considerations:
- Restrict access to the Secret with RBAC. Only the karpenter-provider-hetzner ServiceAccount needs read access.
- Kubeadm join tokens expire (default 24 h). Rotate them before they expire and update the Secret; the provider reads the Secret at server-create time, so a stale token causes new nodes to fail to join.
- Never commit the Secret manifest with real data. The commented template in
examples/talos-nodeclass.yamlis a safe starting point.
Talos images must match three things simultaneously:
- The Talos version running on your control plane.
- The system extensions baked into the image (e.g. gVisor, NVIDIA drivers).
- The CPU architecture of the server type Karpenter selects.
A fuzzy version substring match can accidentally resolve an older snapshot
or one missing a required extension. Use imageSelector.selector to pin the
exact image by an hcloud label you applied when uploading it:
# When you upload your Talos image to Hetzner:
hcloud image update <image-id> --label caph-image-name=talos-v1.9.5-gvisor
# In the NodeClass:
imageSelector:
family: talos
selector:
caph-image-name: "talos-v1.9.5-gvisor"The provider resolves one image per architecture that matches all selector
labels. If no matching image exists for an architecture that Karpenter wants
to provision, the provider reports ImagesReady=False and logs the missing
arch — no server is created. This is the correct safety behavior.
For the multi-arch pattern (amd64 + arm64 NodePools sharing one NodeClass), the selector label must be present on both the x86 and ARM snapshots.
# Adjust networkID, selector label, Secret name, and locations first.
kubectl apply -f examples/talos-nodeclass.yamlVerify the NodeClass becomes Ready:
kubectl get hcloudnodeclasses talos-default
# NAME READY
# talos-default TrueIf READY is False, describe the resource and inspect the status conditions:
kubectl describe hcloudnodeclass talos-default
# Look for ImagesReady, NetworkReady, ResourcesReady, UserDataReady.Once a pod is pending and Karpenter provisions a node, watch for it:
kubectl get nodes -wIf a node appears but stays NotReady, check the Talos API:
talosctl --nodes <node-ip> --talosconfig /path/to/talosconfig dmesg | tail -50
talosctl --nodes <node-ip> --talosconfig /path/to/talosconfig healthCommon join failures:
| Symptom | Likely cause |
|---|---|
| Node never appears | Token expired; update the Secret and delete the pending NodeClaim so Karpenter retries. |
Node appears, NotReady |
Control-plane endpoint unreachable from the private network; check firewall rules and network routing. |
ImagesReady=False |
No image matches the selector for the requested arch; check hcloud image list --selector caph-image-name=.... |