Skip to content

Deploy a collector fleet on Kubernetes

This guide deploys a collector fleet on Kubernetes: one collector pod per node, enrolled with LinkMesh for central config and throughput. The collector is upstream — either Grafana Alloy (via its official Helm chart) or otelcol-contrib + opampsupervisor (as a DaemonSet from the upstream OTel release image). LinkMesh ships no collector distribution of its own; it’s the control plane that configures whichever runtime you pick.

If your hosts are plain Linux (VMs, bare metal, EC2 instances), the Add a collector flow with the per-host installer is simpler — this page is the Kubernetes substrate for the same two runtimes.

Pick a runtime

A collector runs one of two runtimes. The runtime decides how its config arrives — pick one per fleet (you can run different runtimes in different clusters or namespaces).

RuntimemanagementModeHow config arrivesDeploy as
Grafana Alloy + remotecfgalloy-remotecfgAlloy pulls config over Bearer-authenticated HTTPSUpstream grafana/alloy Helm chart
otelcol-contrib + OpAMPopampServer pushes config over OpAMP (WSS); opampsupervisor applies itDaemonSet on the upstream OTel release image

Both give you central config push, fleet status, and per-component throughput on the topology canvas. Grafana Alloy via its Helm chart is the recommended Kubernetes default — it’s a single upstream image with a first-class chart.

Prerequisites

  • A Kubernetes cluster (any flavour — kind, k3s, EKS, GKE, AKS, on-prem).
  • kubectl configured for the cluster; helm for the Alloy path.
  • A running LinkMesh server, reachable from your pods over HTTPS (Alloy remotecfg + own_metrics) or WSS (OpAMP). One instance is enough; to make the server itself resilient, run it highly available on an external MongoDB database.
  • Your server’s public base URL handy, e.g. https://linkmesh.example.com.
  • That same URL configured on the server as externalUrl — see Quickstart step 2. It must be an address your pods resolve (an Ingress hostname or a Service DNS name, never a pod IP). Skip it and the fleet enrols and ships data correctly while every collector shows throughput 0 / CPU 0 / memory 0.

1. Mint a reusable enrollment token

Open Collectors → + Add Collector in the LinkMesh UI, pick your runtime, and choose the Kubernetes snippet — it’s pre-filled with your server URL and a token.

Because a DaemonSet enrols many pods (and reschedules them), use a reusable enrollment token rather than a single-use one: one token enrols every pod, and a rescheduled pod re-attaches without minting anything new. Mint reusable, scoped tokens under Settings → Enrollment Tokens; see Enrollment tokens for TTL, scope, and revocation.

2. Deploy the fleet

Deploy the upstream grafana/alloy chart as a DaemonSet. Alloy pulls its pipeline config from LinkMesh via remotecfg and pushes its own metrics back so the topology canvas shows per-component throughput.

Terminal window
helm repo add grafana https://grafana.github.io/helm-charts && helm repo update

Write linkmesh-alloy-values.yaml, substituting your server URL, a fleet id, and the token:

controller:
type: daemonset
# Cluster-read RBAC (chart default) so pod-log enrichment and the kubelet
# receiver can look up pods, namespaces, and nodes.
rbac:
create: true
alloy:
# Mount the node's log tree read-only so a Kubernetes Pod Logs source can
# tail /var/log/pods; dockercontainers covers the symlink target on some
# distros. The node name + IP feed the kubelet receiver and enrichment.
mounts:
varlog: true
dockercontainers: true
extraEnv:
- name: K8S_NODE_NAME
valueFrom: { fieldRef: { fieldPath: spec.nodeName } }
- name: K8S_NODE_IP
valueFrom: { fieldRef: { fieldPath: status.hostIP } }
configMap:
content: |
logging { level = "info" }
remotecfg {
url = "https://linkmesh.example.com"
id = "k8s-fleet"
poll_frequency = "60s"
// LinkMesh's remotecfg auth accepts ONLY the Bearer scheme.
bearer_token = "<ENROLLMENT_TOKEN>"
}
// own_metrics -> LinkMesh (per-component throughput on the canvas)
prometheus.exporter.self "default" { }
prometheus.scrape "linkmesh_self" {
targets = prometheus.exporter.self.default.targets
forward_to = [otelcol.receiver.prometheus.linkmesh.receiver]
scrape_interval = "30s"
}
otelcol.receiver.prometheus "linkmesh" {
output { metrics = [otelcol.exporter.otlphttp.linkmesh.input] }
}
otelcol.exporter.otlphttp "linkmesh" {
client {
endpoint = "https://linkmesh.example.com"
headers = { "Authorization" = "Bearer <ENROLLMENT_TOKEN>" }
}
}
Terminal window
helm install linkmesh-alloy grafana/alloy \
--namespace linkmesh --create-namespace \
--values linkmesh-alloy-values.yaml

All pods share the same id and token, so the fleet registers as one logical collector. Want each node as a distinct collector? Template a per-pod id (e.g. from the node name) instead of the shared k8s-fleet id.

The line-by-line meaning of this config — and the standalone-host version — is in Onboard Grafana Alloy via remotecfg.

3. Verify

Terminal window
# Wait for an Alloy pod on every node
kubectl -n linkmesh rollout status ds/linkmesh-alloy
# Watch remotecfg fetch its config
kubectl -n linkmesh logs -l app.kubernetes.io/name=alloy --tail=20 | grep -i remotecfg

In your LinkMesh UI, open Collectors — the fleet appears with its runtime (alloy-remotecfg or opamp) and leaves awaiting_connection within ~60s (Alloy’s first poll) or ~30s (the OpAMP handshake). The topology canvas renders throughput once own_metrics start landing.

Production hardening

The manifests above are deliberately minimal. Before promoting to production:

  • Pin the image tag to a specific version instead of a floating one — both the OTel release image and grafana/alloy move forward and will surprise you on the next pod cycle. Pin the Alloy chart version too (helm install --version).
  • Add a NetworkPolicy on the linkmesh namespace allowing egress only to your LinkMesh server’s HTTPS / WSS port.
  • Move the token into sealed-secrets, SOPS, or Vault rather than templating it into values/manifests. A reusable enrollment token is a fleet credential — treat it accordingly, and revoke + re-mint to rotate.
  • Set resource requests/limits sized to your telemetry volume; the Alloy chart exposes alloy.resources, and you can add a resources: block to the OpAMP DaemonSet container.
  • Review the cluster access. The RBAC above is read-only (get/list/watch, never write, never cluster-admin) — the least the Kubernetes receivers need. The DaemonSet also tolerates every node so no node’s logs are missed; narrow the tolerations if you don’t want telemetry from tainted or specialised nodes.

Trying it on kind

For a quick local evaluation:

Terminal window
# Create a kind cluster
kind create cluster --name linkmesh-eval
# Run a LinkMesh server reachable from the kind cluster
# (host.docker.internal works from kind pods on macOS/Windows)
# Then follow steps 1-3 with your server URL set to, e.g.:
# https://host.docker.internal:8080 (Alloy remotecfg / own_metrics)
# wss://host.docker.internal:8080/v1/opamp (OpAMP)

kind nodes share a Docker network, so any service reachable from your host on host.docker.internal:<port> is reachable from the collector pods too.

Uninstall

Terminal window
helm uninstall linkmesh-alloy --namespace linkmesh
kubectl delete namespace linkmesh

Then revoke the fleet’s enrollment token under Settings → Enrollment Tokens if you’re decommissioning the cluster.

Onboard from the cluster inventory

The steps above enrol a collector fleet you then wire by hand. To instead browse your namespaces and workloads in the UI and onboard their logs in one click, install the linkmesh-agent alongside the fleet — it reports the cluster inventory the onboarding view reads. The agent’s packaging/k8s kustomize base installs both the agent and an OpAMP collector fleet in one apply, sharing a single reusable fleet token.

The Enroll Agent wizard (Agents → Enroll Agent → Kubernetes) hands you this manifest ready to apply, with the reusable fleet token already filled in:

The Enroll Agent wizard's Kubernetes tab: a zero-edit DaemonSet manifest carrying the reusable fleet token, pod-log mounts, and read-only RBAC. One kubectl apply enrols one agent per node.

Or apply the same kustomize base from the command line:

Terminal window
kubectl -n linkmesh-system create secret generic linkmesh-agent-bootstrap \
--from-literal=enrollment-token="$LINKMESH_TOKEN"
kubectl apply -k <path-to>/linkmesh-agent/packaging/k8s

Within ~60s the fleet enrols and your namespaces, workloads, and services appear on the group’s Kubernetes tab. Each workload has a one-click Onboard logs action, and a Kubernetes Pod Logs source starts tailing it with no further cluster edits — the manifest already carries the pod-log mounts and read-only RBAC:

The group's Kubernetes tab shows the live cluster the agent discovered — reporting members, namespaces, workloads, and services — with a one-click Onboard logs action on any workload.

Cluster-wide telemetry is one click too. The Collect cluster metrics card enables node metrics (kubeletstats, on every DaemonSet member), plus cluster-object metrics (k8s_cluster) and Kubernetes events (k8s_events) as cluster-scoped singletons:

One click enables cluster telemetry for the whole fleet — per-node kubeletstats, plus cluster-object metrics and events collected once cluster-wide. Multi-member fleets disable the cluster-singleton sources so only one replica collects them.

See the agent’s packaging/k8s/README.md for the step-by-step, and The agent for what it does and does not do.

Next steps