Linkerd’s SMI Traffic Split lets you do canary deployments by sending a small percentage of traffic to a new version of your service.
apiVersion: split.smi-spec.io/v1alpha2
kind: TrafficSplit
metadata:
name: my-app-split
namespace: default
spec:
service: my-app # The service name that traffic will be split for
trafficRouting:
- backend:
serviceName: my-app-v1 # The existing, stable version
weight: 900m # 90% of traffic
- backend:
serviceName: my-app-v2 # The new, canary version
weight: 100m # 10% of traffic
This TrafficSplit resource tells Linkerd to send 90% of incoming traffic to my-app-v1 and 10% to my-app-v2. Linkerd injects itself as a transparent proxy alongside your application pods, and it intercepts the traffic destined for my-app. Based on the TrafficSplit configuration, it then routes the requests to the appropriate backend service. This allows you to gradually roll out new versions of your application without causing downtime or impacting all users.
Let’s see this in action. Imagine you have a my-app service with two deployments: my-app-v1 and my-app-v2.
Current State (before TrafficSplit):
All traffic to my-app goes to my-app-v1.
# Check the default service routing
kubectl get svc my-app -n default
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-app ClusterIP 10.100.200.30 <none> 80/TCP 5m
Deploying the Canary:
First, you deploy your new version, my-app-v2, with identical ports and selectors as my-app-v1.
# deployment-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-v2
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: my-app
version: v2
template:
metadata:
labels:
app: my-app
version: v2
spec:
containers:
- name: my-app
image: my-registry/my-app:v2 # Your new application image
ports:
- containerPort: 8080
Then, you create a corresponding Service for my-app-v2. Crucially, this service should have the same name as the original service that my-app points to. Linkerd uses the target service name (my-app in this case) to identify which traffic it needs to split.
# service-v2.yaml
apiVersion: v1
kind: Service
metadata:
name: my-app # IMPORTANT: Same service name as v1
namespace: default
spec:
selector:
app: my-app
version: v2 # This selector points to the v2 pods
ports:
- protocol: TCP
port: 80
targetPort: 8080
Applying the TrafficSplit:
Now, you apply the TrafficSplit resource shown earlier.
kubectl apply -f traffic-split.yaml -n default
How Linkerd Manages This:
Linkerd’s control plane watches for TrafficSplit resources. When it sees the my-app-split resource, it configures the Linkerd data plane proxies (running as sidecars in your my-app-v1 and my-app-v2 pods, and potentially a gateway if you’re using one) to implement the specified routing.
The service: my-app field in the TrafficSplit tells Linkerd to intercept traffic destined for the Kubernetes Service named my-app. The trafficRouting array then defines how to distribute that intercepted traffic among the backend services. Linkerd uses weighted distribution: 900m is equivalent to 90% (or 900 milliprocents) and 100m is 10%.
When a request arrives at a pod that’s part of the my-app service (whether it’s a v1 or v2 pod), Linkerd’s proxy intercepts it. It checks the TrafficSplit configuration for my-app. Based on the weights, it randomly selects a backend service (my-app-v1 or my-app-v2) for the request. This selection is done on a per-request basis, so over time, approximately 90% of requests will go to v1 and 10% to v2.
To verify this, you can use Linkerd’s command-line tools:
# Check the status of the TrafficSplit
linkerd viz traffic-split show my-app-split -n default
# Observe traffic distribution in real-time (requires Prometheus and Grafana)
# You can see metrics like request volume per backend service.
# For example, using linkerd-viz:
linkerd viz tap -n default deploy/my-app-v1 --to deploy/my-app-v2
# Or more directly, query Prometheus for metrics related to the my-app service.
The key insight is that Linkerd doesn’t modify your Kubernetes Service objects directly. Instead, it introduces its own routing layer above the standard Kubernetes Service discovery. This means your original Service object for my-app can continue to point to all pods (or a subset, depending on your setup), but Linkerd’s proxies are the ones making the fine-grained, weighted routing decisions based on the TrafficSplit.
The "magic" of this weighted distribution happens within the Linkerd proxy’s internal routing tables. When Linkerd’s control plane configures the data plane, it pushes down these routing rules. Each proxy becomes aware of the available backends for a given service and the probability of sending traffic to each. This is often implemented using consistent hashing or random selection algorithms to achieve the desired distribution.
When you’re ready to fully roll out my-app-v2, you simply update the TrafficSplit to give my-app-v2 100% of the traffic and my-app-v1 0%.
apiVersion: split.smi-spec.io/v1alpha2
kind: TrafficSplit
metadata:
name: my-app-split
namespace: default
spec:
service: my-app
trafficRouting:
- backend:
serviceName: my-app-v1
weight: 0m # No traffic to v1
- backend:
serviceName: my-app-v2
weight: 1000m # All traffic to v2
After a period of observation and confidence, you can then remove the my-app-v1 deployment and its associated Service.
The next concept you’ll likely encounter is implementing advanced routing rules beyond simple weighted distribution, such as traffic mirroring or HTTP header-based routing, which are also supported by SMI and Linkerd.