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.
  • kubectl installed on your workstation and configured with cluster-admin permissions.
  • A Linux or macOS workstation with curl.

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

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

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/1 after annotating the namespace. The annotation only affects new pods. Run kubectl rollout restart deploy -n your_namespace.
  • linkerd check warns 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 that parentRefs uses kind: Service, the correct port, and that the route is accepted with kubectl describe httproute hello-split -n demo.
  • linkerd viz stat shows no data. Metrics appear only for meshed pods receiving traffic. Wait a minute and confirm with linkerd 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.