Linkerd is a lightweight service mesh for Kubernetes. It adds a small Rust proxy to each pod that encrypts traffic between meshed workloads with mutual TLS by default, collects success rate and latency per route, and can retry, time out and split requests without changes to your code. In this tutorial you will install Linkerd and its Viz extension, mesh the Emojivoto demo application, confirm that traffic is encrypted, and shift traffic between two versions of a service using a Gateway API HTTPRoute.
Prerequisites
To follow this tutorial, you will need:
- A running Kubernetes cluster (a CubePath managed Kubernetes cluster or a self-managed one on VPS or bare metal) with at least 2 vCPUs and 4 GB of RAM free.
kubectlinstalled on your workstation and configured with cluster-admin permissions.- A Linux or macOS workstation with
curl.
NoteThe Linkerd open source project publishes edge releases (for example
edge-25.9.1). Stable, versioned builds are distributed by Buoyant as Buoyant Enterprise for Linkerd. This tutorial uses the latest edge release; pin a specific version and test upgrades before using it in production.
Confirm cluster access:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
worker-1 Ready <none> 12d v1.33.4
worker-2 Ready <none> 12d v1.33.4
Step 1 - Installing the linkerd CLI
The linkerd CLI installs the control plane, injects proxies and runs health checks. Download the official install script, review it, and run it:
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge -o install-linkerd.sh
less install-linkerd.sh
sh install-linkerd.sh
The script places the binary in ~/.linkerd2/bin. Add it to your PATH, and append the same line to ~/.bashrc (or ~/.zshrc) to make it permanent:
export PATH="$HOME/.linkerd2/bin:$PATH"
Check the client version:
linkerd version --client
Client version: edge-25.9.1
Now run the pre-installation checks. They confirm that your Kubernetes version is supported and that you have the permissions needed to create the control plane:
linkerd check --pre
The command ends with Status check results are √ when the cluster is ready.
Step 2 - Installing the control plane
Recent Linkerd releases use Gateway API HTTPRoute resources for routing, so the Gateway API CRDs must exist in the cluster. If linkerd check --pre reported them as missing, install the standard channel. Check the Linkerd documentation for the Gateway API version your Linkerd release supports; this example uses v1.2.1:
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml
Install the Linkerd CRDs, then the control plane. linkerd install renders manifests that you pipe to kubectl:
linkerd install --crds | kubectl apply -f -
linkerd install | kubectl apply -f -
Wait for the control plane and validate it:
linkerd check
...
control-plane-version
---------------------
√ can retrieve the control plane version
√ control plane is up-to-date
√ control plane and cli versions match
Status check results are √
The control plane runs in the linkerd namespace:
kubectl get pods -n linkerd
NAME READY STATUS RESTARTS AGE
linkerd-destination-6c9b4f7d9c-2xk8q 4/4 Running 0 2m
linkerd-identity-5f8d7b6c4d-q7vwl 2/2 Running 0 2m
linkerd-proxy-injector-7b9d8c6f5f-m4z2n 2/2 Running 0 2m
Important
linkerd installgenerates a trust anchor and issuer certificate for you. They are fine for testing, but the trust anchor expires and cannot be rotated automatically. For production, generate your own trust anchor and let cert-manager rotate the issuer certificate, as described in the Linkerd documentation on automatic certificate rotation.
Step 3 - Installing the Viz extension
The Viz extension adds an on-cluster Prometheus, the linkerd viz commands for live metrics and a web dashboard:
linkerd viz install | kubectl apply -f -
linkerd viz check
The check ends with Status check results are √ once the extension is running in the linkerd-viz namespace.
Step 4 - Meshing the Emojivoto demo application
Emojivoto is a small voting app with three services (web, emoji and voting) plus a traffic generator. Deploy it without the mesh first:
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/emojivoto.yml | kubectl apply -f -
kubectl rollout status deploy/web -n emojivoto
Linkerd injects its proxy when a pod is created with the annotation linkerd.io/inject: enabled. You can annotate the namespace so every new pod is meshed, then restart the deployments so the pods are recreated:
kubectl annotate namespace emojivoto linkerd.io/inject=enabled
kubectl rollout restart deploy -n emojivoto
kubectl rollout status deploy/web -n emojivoto
Each pod now has two containers, the app and linkerd-proxy:
kubectl get pods -n emojivoto
NAME READY STATUS RESTARTS AGE
emoji-7b9c8d6f5c-kq2lz 2/2 Running 0 40s
vote-bot-6d8f7c9b5d-x7m4p 2/2 Running 0 40s
voting-5f6c9d8b7c-p9w3t 2/2 Running 0 40s
web-7c8d9f6b5c-r2t8n 2/2 Running 0 40s
Check the data plane from Linkerd's point of view:
linkerd check --proxy -n emojivoto
TipTo keep one workload out of the mesh inside an annotated namespace, set
linkerd.io/inject: disabledin the annotations of its pod template.
Step 5 - Verifying mTLS and golden metrics
Linkerd encrypts all TCP traffic between meshed pods with mutual TLS automatically; there is nothing to configure. Confirm it by listing the connections between deployments:
linkerd viz edges deployment -n emojivoto
SRC DST SRC_NS DST_NS SECURED
vote-bot web emojivoto emojivoto √
web emoji emojivoto emojivoto √
web voting emojivoto emojivoto √
A √ in SECURED means the connection uses mTLS with identities issued by the Linkerd control plane.
View the golden metrics (success rate, requests per second and latency percentiles) for each deployment:
linkerd viz stat deploy -n emojivoto
NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN
emoji 1/1 100.00% 1.9rps 1ms 2ms 3ms 3
vote-bot 1/1 - - - - - -
voting 1/1 85.90% 1.0rps 1ms 1ms 2ms 3
web 1/1 92.63% 1.9rps 2ms 4ms 6ms 3
The voting service fails some requests on purpose; this is a deliberate bug in the demo. Find it by watching live requests to voting with tap:
linkerd viz tap deploy/voting -n emojivoto
Press Ctrl+C after a few seconds. Lines ending in :status=500 or with a non-zero grpc-status show the failing calls.
To open the web dashboard with the same data, run the following command and use the URL it prints:
linkerd viz dashboard
Step 6 - Shifting traffic between versions with HTTPRoute
Linkerd implements traffic splitting, retries and timeouts through Gateway API HTTPRoute resources attached to a Kubernetes Service. The route applies to requests sent by meshed clients.
Create a namespace with injection enabled for the example:
kubectl create namespace demo
kubectl annotate namespace demo linkerd.io/inject=enabled
Create the manifest for two versions of a small echo service and a client pod:
nano hello.yaml
The hello Service selects the v1 pods and hello-v2 selects the v2 pods. Both answer on port 5678:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-v1
namespace: demo
spec:
replicas: 1
selector:
matchLabels: {app: hello, version: v1}
template:
metadata:
labels: {app: hello, version: v1}
spec:
containers:
- name: echo
image: hashicorp/http-echo
args: ["-text=v1"]
ports:
- containerPort: 5678
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-v2
namespace: demo
spec:
replicas: 1
selector:
matchLabels: {app: hello, version: v2}
template:
metadata:
labels: {app: hello, version: v2}
spec:
containers:
- name: echo
image: hashicorp/http-echo
args: ["-text=v2"]
ports:
- containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
name: hello
namespace: demo
spec:
selector: {app: hello, version: v1}
ports:
- port: 5678
---
apiVersion: v1
kind: Service
metadata:
name: hello-v2
namespace: demo
spec:
selector: {app: hello, version: v2}
ports:
- port: 5678
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: client
namespace: demo
spec:
replicas: 1
selector:
matchLabels: {app: client}
template:
metadata:
labels: {app: client}
spec:
containers:
- name: curl
image: curlimages/curl
command: ["sleep", "infinity"]
Apply it and wait for the pods:
kubectl apply -f hello.yaml
kubectl rollout status deploy/client -n demo
Without a route, every request to hello reaches v1. Send 50 requests from the client and count the answers:
kubectl exec -n demo deploy/client -c curl -- sh -c 'for i in $(seq 1 50); do curl -s http://hello:5678; done' | sort | uniq -c
50 v1
Now create the route:
nano hello-route.yaml
The parentRefs entry attaches the route to the hello Service. The backendRefs send 80% of requests to hello (v1) and 20% to hello-v2. The annotations add one retry on 5xx responses and a 2 second request timeout:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: hello-split
namespace: demo
annotations:
retry.linkerd.io/http: 5xx
retry.linkerd.io/limit: "1"
timeout.linkerd.io/request: 2s
spec:
parentRefs:
- name: hello
kind: Service
group: core
port: 5678
rules:
- backendRefs:
- name: hello
port: 5678
weight: 80
- name: hello-v2
port: 5678
weight: 20
Apply it and repeat the test:
kubectl apply -f hello-route.yaml
kubectl exec -n demo deploy/client -c curl -- sh -c 'for i in $(seq 1 50); do curl -s http://hello:5678; done' | sort | uniq -c
41 v1
9 v2
The exact numbers vary, but roughly one request in five now reaches v2. To finish a rollout, change the weights (for example 50/50, then 0/100) and apply the file again. Deleting the route returns all traffic to v1:
kubectl delete -f hello-route.yaml
Troubleshooting
- Pods show
1/1after annotating the namespace. The annotation only affects new pods. Runkubectl rollout restart deploy -n your_namespace. linkerd checkwarns that certificates expire soon. The CLI-generated trust anchor is close to expiry. Plan a trust anchor rotation and move to cert-manager managed certificates.- The HTTPRoute has no effect. Traffic from unmeshed clients bypasses Linkerd routing, so confirm the client pod shows
2/2. Also check thatparentRefsuseskind: Service, the correct port, and that the route is accepted withkubectl describe httproute hello-split -n demo. linkerd viz statshows no data. Metrics appear only for meshed pods receiving traffic. Wait a minute and confirm withlinkerd viz check.
Conclusion
You installed the Linkerd control plane and Viz extension, meshed an application with a single namespace annotation, confirmed that traffic between services is encrypted with mTLS, found a failing service with golden metrics and tap, and shifted traffic between two versions with an HTTPRoute. Next, you can restrict which workloads may call each other with Linkerd's Server and AuthorizationPolicy resources, automate certificate rotation with cert-manager, or connect clusters with the Linkerd multicluster extension.
