Skip to content

Install on Kubernetes

This guide installs the LinkMesh agent as a Kubernetes DaemonSet — one pod per node, each enrolled with its own mTLS certificate. Use it when your collector hosts live in a cluster rather than as standalone Linux boxes.

If your hosts are plain Linux (VMs, bare metal, EC2 instances), the Add a collector flow with the one-line curl | sh installer is simpler.

When the K8s path makes sense

One-line curl installerKubernetes DaemonSet
Host modelOne agent per Linux hostOne agent pod per K8s node
Collector runtimeUpstream Alloy, installed by the agentSeparate workload (Alloy, otelcol…)
Updatesapt upgradeBump image tag, re-apply manifest
State/etc/linkmesh-agent/hostPath per node
Fits best whenStandalone hosts, smaller fleetsExisting K8s infra, GitOps workflow

What you’ll do

  1. Mint an enrollment token in the LinkMesh UI.
  2. Save four small manifests to disk (Namespace, ConfigMap, Secret, DaemonSet).
  3. Fill in three placeholders — your HTTPS host, your gRPC host, the token.
  4. kubectl apply -f and watch each node’s agent pod register itself.

Prerequisites

  • A Kubernetes cluster (any flavour — kind, k3s, EKS, GKE, AKS, on-prem).
  • kubectl configured for the cluster.
  • A running LinkMesh server, reachable from your pods on both HTTPS (for the one-time bootstrap call) and gRPC over TLS (for the persistent agent → server connection). One server instance is enough; to make the server itself resilient, run it highly available on an external MongoDB database.
  • Two URLs handy:
    • HTTPS host — e.g. linkmesh.example.com. The bootstrap init container POSTs to https://<host>/api/v1/bootstrap.
    • gRPC endpoint — e.g. grpc.linkmesh.example.com:50051. The agent uses this for the persistent mTLS stream.

1. Mint an enrollment token

Open the Collectors page in your LinkMesh UI and click + Add Collector. The wizard mints a single-use token (15-minute TTL by default) and shows you both the curl one-liner and a “for Kubernetes” snippet.

Copy the token value only — you’ll paste it into a Secret below. The DaemonSet handles the bootstrap call itself; you don’t run the install script on K8s.

2. Save the manifests

Drop these four files into a directory (e.g. ~/linkmesh-k8s/). They’re plain YAML — no Helm, no kustomize required (though a kustomize base is shown at the end if you want it).

00-namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
name: linkmesh-system
labels:
app.kubernetes.io/name: linkmesh-system
app.kubernetes.io/part-of: linkmesh

linkmesh-agent-configmap.yaml.tmpl

apiVersion: v1
kind: ConfigMap
metadata:
name: linkmesh-agent-config
namespace: linkmesh-system
data:
server-https-url: "https://__LINKMESH_SERVER__"
config.yaml: |
server:
url: "grpcs://__LINKMESH_GRPC__"
agent:
id: ""
environment: "production"
certificates:
caCertPath: "/etc/linkmesh-agent/certs/ca.pem"
clientCertPath: "/etc/linkmesh-agent/certs/client.pem"
clientKeyPath: "/etc/linkmesh-agent/certs/client-key.pem"
logging:
level: "info"

linkmesh-agent-secret.yaml.tmpl

apiVersion: v1
kind: Secret
metadata:
name: linkmesh-agent-bootstrap
namespace: linkmesh-system
type: Opaque
stringData:
enrollment-token: "__LINKMESH_TOKEN__"

linkmesh-agent-daemonset.yaml

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: linkmesh-agent
namespace: linkmesh-system
spec:
selector:
matchLabels:
app.kubernetes.io/name: linkmesh-agent
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
template:
metadata:
labels:
app.kubernetes.io/name: linkmesh-agent
spec:
automountServiceAccountToken: false
initContainers:
- name: bootstrap
image: alpine:3.20
securityContext:
runAsUser: 0
runAsGroup: 0
runAsNonRoot: false
env:
- name: LINKMESH_SERVER
valueFrom:
configMapKeyRef:
name: linkmesh-agent-config
key: server-https-url
- name: LINKMESH_TOKEN
valueFrom:
secretKeyRef:
name: linkmesh-agent-bootstrap
key: enrollment-token
command: ["/bin/sh", "-c"]
args:
- |
set -eu
mkdir -p /certs && chown 65532:65532 /certs && chmod 750 /certs
if [ -f /certs/client.pem ] && [ -f /certs/client-key.pem ] && [ -f /certs/ca.pem ]; then
echo "Certs already present on host; skipping bootstrap."
exit 0
fi
echo "Enrolling against ${LINKMESH_SERVER}..."
apk add --no-cache curl jq >/dev/null
RESP=$(curl --fail --silent --show-error \
-X POST "${LINKMESH_SERVER}/api/v1/bootstrap" \
-H "Content-Type: application/json" \
-d "{\"token\":\"${LINKMESH_TOKEN}\"}")
echo "$RESP" | jq -r .data.caCertificate > /certs/ca.pem
echo "$RESP" | jq -r .data.clientCertificate > /certs/client.pem
echo "$RESP" | jq -r .data.clientKey > /certs/client-key.pem
chown 65532:65532 /certs/*.pem
chmod 644 /certs/ca.pem /certs/client.pem
chmod 600 /certs/client-key.pem
echo "Enrolled — collectorId=$(echo "$RESP" | jq -r .data.collectorId)"
volumeMounts:
- { name: certs, mountPath: /certs }
containers:
- name: agent
image: docker.io/opensight/linkmesh-agent:latest
imagePullPolicy: IfNotPresent
securityContext:
runAsUser: 65532
runAsGroup: 65532
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- { name: config, mountPath: /etc/linkmesh-agent/config.yaml, subPath: config.yaml, readOnly: true }
- { name: certs, mountPath: /etc/linkmesh-agent/certs, readOnly: true }
resources:
requests: { cpu: 25m, memory: 64Mi }
limits: { cpu: 200m, memory: 256Mi }
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
volumes:
- name: config
configMap:
name: linkmesh-agent-config
items:
- { key: config.yaml, path: config.yaml }
- name: certs
hostPath:
path: /var/lib/linkmesh-agent/certs
type: DirectoryOrCreate

3. Substitute placeholders and apply

Three placeholders to fill in: your HTTPS host, your gRPC endpoint, the token from step 1.

Terminal window
export LINKMESH_SERVER=linkmesh.example.com
export LINKMESH_GRPC=grpc.linkmesh.example.com:50051
export LINKMESH_TOKEN=<paste-token-from-wizard>
# Namespace first
kubectl apply -f 00-namespace.yaml
# Substitute and pipe to kubectl
sed -e "s|__LINKMESH_SERVER__|${LINKMESH_SERVER}|g" \
-e "s|__LINKMESH_GRPC__|${LINKMESH_GRPC}|g" \
linkmesh-agent-configmap.yaml.tmpl | kubectl apply -f -
sed "s|__LINKMESH_TOKEN__|${LINKMESH_TOKEN}|g" \
linkmesh-agent-secret.yaml.tmpl | kubectl apply -f -
# DaemonSet — no placeholders
kubectl apply -f linkmesh-agent-daemonset.yaml

4. Verify

Terminal window
# Wait for pods on every node to come up
kubectl -n linkmesh-system rollout status ds/linkmesh-agent
# Bootstrap init container logs — should show "Enrolled — collectorId=..."
kubectl -n linkmesh-system logs -l app.kubernetes.io/name=linkmesh-agent \
-c bootstrap --tail=20
# Agent container logs — should show the gRPC connection coming up
kubectl -n linkmesh-system logs -l app.kubernetes.io/name=linkmesh-agent \
-c agent --tail=20

In your LinkMesh UI, open Collectors — there’s one entry per node, each Online and heartbeating.

Collector strategy on Kubernetes

The agent manages an OpenTelemetry collector — it doesn’t ingest telemetry itself. On Linux hosts the agent installs upstream Grafana Alloy (linkmesh-agent install alloy) and supervises it; on Kubernetes you wire up the collector workload separately.

Two patterns work well:

Option A — Sidecar collector

Run the OpenTelemetry collector as a second container in the same pod as the agent. The agent finds the collector over localhost, manages its config lifecycle via the standard agent flow, and pushes config updates over the gRPC stream like any other deployment.

Useful when you want tight 1:1 coupling between agent + collector and collector-config changes to redeploy as a unit.

Option B — Separate collector workload + OpAMP

Run Alloy or otelcol-contrib as its own DaemonSet (or Deployment) and enrol it with LinkMesh over the OpenTelemetry Management Protocol. The agent in this guide acts as the per-node heartbeat + cert-bearing presence; the actual data plane lives in the collector pods.

Useful when you already have a collector-config tooling workflow (kustomize overlays, Helm values, OPA) and want LinkMesh as the management layer without disturbing it.

Pick whichever fits your team. The Collector concept page covers the trade-offs in more depth.

Production hardening

The defaults above are deliberately conservative but not maximally locked down. Before promoting to production:

  • Pin the image tag to a specific version (e.g. :1.25.2) instead of :latest.
  • Add a NetworkPolicy on the linkmesh-system namespace allowing egress only to your LinkMesh server’s HTTPS + gRPC ports.
  • Replace hostPath with a per-node PVC if your storage class supports it — hostPath is convenient but every pod scheduled to that node has filesystem access to the cert directory.
  • Move the enrollment token into sealed-secrets, SOPS, or Vault rather than templating it directly into your overlay. Tokens are single-use and short-lived, but they’re still bootstrap credentials — treat them accordingly.

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 somewhere reachable from the kind cluster
# (host.docker.internal works from kind pods on macOS/Windows)
# Then follow steps 1–4 above with:
# LINKMESH_SERVER=host.docker.internal:8080
# LINKMESH_GRPC=host.docker.internal:50051

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

Uninstall

Terminal window
kubectl delete ns linkmesh-system
# Optional: scrub the per-node cert dirs (one node at a time)
for node in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
kubectl debug node/$node --image=alpine:3.20 -- \
sh -c "rm -rf /host/var/lib/linkmesh-agent"
done

Deleting the namespace removes every workload but leaves the on-host cert dirs behind. Scrub them too if you’re decommissioning the cluster — or leave them if you’ll redeploy: the existing certs save a fresh enrollment round-trip.

Next steps