Deploy the System Alloy
The System Alloy is the one agent that runs on every node, and it collects everything the node itself knows: the control-plane metrics on loopback, the kubelet and cadvisor series, the Cilium and cluster component endpoints, and the container, systemd, and audit logs. One Helm release covers all of it. Application metrics are deliberately absent, for the reason in Using Alloy for observability .
Prerequisites: a metrics backend that accepts remote-write and a Loki that accepts pushes, both reachable from inside the cluster (Set up Prometheus and Set up Loki ), plus Helm and kubectl access.
Warning
This DaemonSet is a privileged collector. On the host network it reaches every loopback listener on the node, including the unauthenticated ones (etcd on 2381, KubeGate on 8080, syself-agent on 20257), and it reads root-owned audit files. Keep it in a namespace only your platform team can write to, run no application workloads there, and review its config like a firewall rule.
What the config has to solve
Four things make this config specific to Syself Autopilot rather than a stock Kubernetes install.
Loopback binding. Most platform metrics endpoints bind to
127.0.0.1, so the agent shares the node's network namespace withhostNetwork: trueand dials loopback directly.dnsPolicy: ClusterFirstWithHostNetkeeps cluster DNS resolvable from there, which the backend Service names need.A per-node identity. Every agent scrapes the same local address,
127.0.0.1:<port>, so by default every node's metrics carry the sameinstancelabel. The backend then cannot tell the nodes apart, treats them as one series, and rejects the mixed samples as out of order (err-mimir-sample-out-of-orderon Grafana Mimir). The config writes the node name into theinstanceandnodelabels instead, using the Downward API.Node-role gating. etcd, kube-controller-manager, kube-scheduler, KubeGate, and syself-tunnel-server run only on control planes.
syself-proxyruns only on workers, and the CSI node plugin only on cloud workers. If every node gets the same target list, each node also tries to scrape components that do not run on it. Those scrapes fail withconnection refusedforever, and aTargetDownalert fires that never clears. The config reads the local node's labels and keeps each job only where that component runs. The labels the gates read: control-plane nodes carrynode-role.kubernetes.io/control-planewith an empty value, so the gate checks that the label exists rather than what it says. Workers carrynode.kubernetes.io/worker=true, and the machine type is inautopilot.syself.com/machine-type,hcloudorbaremetal. The CSI node plugin is the one job that needs two labels: it runs only on cloud workers, and gating it on the worker label alone would include bare-metal workers, where that port never listens.Self-signed certificates on loopback. A TLS client checks the address it dialled against the names in the server's certificate. kube-controller-manager and kube-scheduler use self-signed certificates, and the kubelet's certificate names the node's IP, not
127.0.0.1, so scraping them over loopback fails verification either way. Those jobs setinsecure_skip_verify, which is safe here because the connection never leaves the node. The API server is the exception: its certificate does include127.0.0.1, so that job verifies against the ServiceAccount CA bundle instead of skipping.
Deploy it
Write the values file
The chart's own RBAC covers everything this agent needs. rbac.create: true grants the discovery permissions plus nodes/metrics and get on the /metrics non-resource URL, which is exactly what the token-authenticated control-plane and kubelet jobs authorize against, so no extra ClusterRole is required.
mounts.varlog: true mounts the host's whole /var/log, which is where the container logs, the journal, and all three audit streams live, so one flag replaces a list of hostPath volumes. All three audit streams are mode 0600, owned by root except the KubeGate log, which belongs to the unprivileged user KubeGate runs as. Either way no unprivileged reader can open them, so the container runs as root.
Install the chart
$ helm repo add grafana https://grafana.github.io/helm-charts
$ helm upgrade -i system-alloy grafana/alloy \
--namespace monitoring --create-namespace \
--version <chart-version> \
--values system-alloy-values.yaml
Pin --version to a chart release you have tested, so an upgrade does not change collection behavior underneath you.
Confirm a pod runs on every node
$ kubectl -n monitoring get pods -l app.kubernetes.io/instance=system-alloy -o wide
Expect one pod per node, control planes included. A control-plane node with no pod means the tolerations block from the values file did not apply, since without it the pod cannot land on tainted nodes.
Check the targets
Alloy serves a UI that lists every component and its targets, which is the fastest way to see what a single node is actually collecting:
$ kubectl -n monitoring port-forward daemonset/system-alloy 12345:12345
Open http://localhost:12345 and look at the prometheus.scrape components. On a control-plane pod the etcd, kubegate, controller_manager, scheduler, and tunnel_server components each show one target up, and csi_node and syself_proxy show none. On a worker pod it is the other way around: csi_node and syself_proxy show targets, the control-plane components show none. A component with an empty target list on a node where it does not run is the gating doing its job, not a fault.
Then confirm the samples arrive at the backend, one series per node rather than one shared series:
count(up{job="etcd"}) by (instance)
count(up{job="cilium-agent"}) by (instance)
Add the Hubble flow log
If you turned on Hubble flow export (Alert on dropped packets ), the verdict log is a file on the node like any other, and the values file above already mounts its directory. Add a source for it:
local.file_match "hubble_flows" {
path_targets = [{
__path__ = "/var/run/cilium/hubble/events.log",
job = "hubble-flows",
node = sys.env("NODE_NAME"),
}]
}
loki.source.file "hubble_flows" {
targets = local.file_match.hubble_flows.targets
forward_to = [loki.write.central.receiver]
}
Keep the log volume down
The values file already filters the systemd journal: its discovery.relabel "journal" block keeps log lines from kubelet, containerd, and the syself-* services and drops every other unit, which is most of the volume for none of the value. Container logs are the other large contributor and vary entirely with your workloads, so drop or sample the high-volume namespaces rather than paying to store what you will not query.
When Loki is slow or an incident floods logs, Alloy's disk buffer at storagePath absorbs the burst. Size that volume so a spike degrades into delay instead of taking the node's memory with it. Log retention and sizing has the per-node estimates to size against.
With the node signals flowing, add the Application Alloy for your workloads' metrics and traces. For what the series you just started collecting actually tell you, see Control-plane metrics and Data-plane and cluster component metrics . If a target will not come up, Troubleshoot the collectors covers the failures worth knowing by name.
Using Alloy for observability
Run one Grafana Alloy DaemonSet for infrastructure telemetry and one clustered Alloy StatefulSet for application telemetry, so a runaway application cannot take your node telemetry down with it.
Deploy the Application Alloy
Run a clustered Grafana Alloy StatefulSet that scrapes your workloads through their existing ServiceMonitors and receives OTLP traces, sharded across replicas.