Skip to main content

Deploy the System Alloy

Inspect 1.36

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 .

Prerequisites: a metrics backend that accepts remote-write and a Loki that accepts pushes, both reachable from inside the cluster ( and ), 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.

  1. Loopback binding. Most platform metrics endpoints bind to 127.0.0.1, so the agent shares the node's network namespace with hostNetwork: true and dials loopback directly. dnsPolicy: ClusterFirstWithHostNet keeps cluster DNS resolvable from there, which the backend Service names need.

  2. 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 same instance label. 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-order on Grafana Mimir). The config writes the node name into the instance and node labels instead, using the Downward API.

  3. Node-role gating. etcd, kube-controller-manager, kube-scheduler, KubeGate, and syself-tunnel-server run only on control planes. syself-proxy runs 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 with connection refused forever, and a TargetDown alert 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 carry node-role.kubernetes.io/control-plane with an empty value, so the gate checks that the label exists rather than what it says. Workers carry node.kubernetes.io/worker=true, and the machine type is in autopilot.syself.com/machine-type, hcloud or baremetal. 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.

  4. 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 set insecure_skip_verify, which is safe here because the connection never leaves the node. The API server is the exception: its certificate does include 127.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:

text
		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 ( ), 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:

terraform
		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. has the per-node estimates to size against.

With the node signals flowing, add for your workloads' metrics and traces. For what the series you just started collecting actually tell you, see and . If a target will not come up, covers the failures worth knowing by name.