Repository: fluxcd/flagger

Issue:
Add support for spec.trafficDistribution in Service resources (Kubernetes 1.32+)
## Describe the feature

Currently, Flagger supports customizing the Service spec under the Canary resource, including fields such as port, targetPort, labels, and annotations.

However, with Kubernetes 1.32, the new field spec.trafficDistribution was promoted to beta and is now part of the native Service API.
This field allows configuring zone-aware traffic routing within multi-zone clusters by setting:

spec:
  trafficDistribution: PreferClose | Distribute

At the moment, Flagger’s Canary.spec.service section does not propagate this field when generating the stable and canary Service resources.

Please add support for the spec.trafficDistribution field in the Canary Service definition.
This would allow users to take advantage of the new zone-aware traffic distribution behavior in Kubernetes 1.32+ while still using Flagger for canary and blue/green deployments.

What problem are you trying to solve?

This enhancement would allow Flagger users to take advantage of Kubernetes' new native zone-aware balancing feature without having to manually patch Service resources, ensuring compatibility with modern cluster topologies and reducing latency for in-zone traffic.

### Proposed solution

```
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: api-example
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-example
  service:
    port: 8080
    targetPort: 8080
    trafficDistribution: PreferClose
```

What do you want to happen? Add any considered drawbacks.

### Any alternatives you've considered?

No

Is there another way to solve this problem that isn't as good a solution?

No

Hints from issue comments:
@stefanprodan: I see that `.spec.trafficDistribution` is stable since Kubernetes v1.33, so I'm for adding it as an optional field. We'll include this field in the generated services only if it's specified in the canary, this way Flagger will keep working on older Kubernetes versions.

Solution patch:
diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml
index 6e2092bf..e79d9c4b 100644
--- a/artifacts/flagger/crd.yaml
+++ b/artifacts/flagger/crd.yaml
@@ -191,11 +191,18 @@ spec:
                     appProtocol:
                       description: Application protocol of the port
                       type: string
+                    trafficDistribution:
+                      description: Traffic distribution of the service
+                      type: string
+                      enum:
+                        - PreferClose
+                        - PreferSameZone
+                        - PreferSameNode
                     targetPort:
                       description: Container target port name
                       x-kubernetes-int-or-string: true
                     portDiscovery:
-                      description: Enable port dicovery
+                      description: Enable port discovery
                       type: boolean
                     headless:
                       description: Headless if set to true, generates headless Kubernetes services.
diff --git a/charts/flagger/crds/crd.yaml b/charts/flagger/crds/crd.yaml
index 6e2092bf..e79d9c4b 100644
--- a/charts/flagger/crds/crd.yaml
+++ b/charts/flagger/crds/crd.yaml
@@ -191,11 +191,18 @@ spec:
                     appProtocol:
                       description: Application protocol of the port
                       type: string
+                    trafficDistribution:
+                      description: Traffic distribution of the service
+                      type: string
+                      enum:
+                        - PreferClose
+                        - PreferSameZone
+                        - PreferSameNode
                     targetPort:
                       description: Container target port name
                       x-kubernetes-int-or-string: true
                     portDiscovery:
-                      description: Enable port dicovery
+                      description: Enable port discovery
                       type: boolean
                     headless:
                       description: Headless if set to true, generates headless Kubernetes services.
diff --git a/docs/gitbook/usage/how-it-works.md b/docs/gitbook/usage/how-it-works.md
index 31f109dd..78cb9c08 100644
--- a/docs/gitbook/usage/how-it-works.md
+++ b/docs/gitbook/usage/how-it-works.md
@@ -148,6 +148,7 @@ spec:
     targetPort: 9898
     portDiscovery: true
     headless: false
+    trafficDistribution: PreferClose
 ```
 
 The container port from the target workload should match the `service.port` or `service.targetPort`.
@@ -155,7 +156,7 @@ The `service.name` is optional, defaults to `spec.targetRef.name`.
 The `service.targetPort` can be a container port number or name.
 The `service.portName` is optional (defaults to `http`), if your workload uses gRPC then set the port name to `grpc`.
 The `service.appProtocol` is optional, more details can be found [here](https://kubernetes.io/docs/concepts/services-networking/service/#application-protocol).
-
+The `service.trafficDistribution` is optional, more details can be found [here](https://kubernetes.io/docs/concepts/services-networking/service/#traffic-distribution).
 
 If port discovery is enabled, Flagger scans the target workload and extracts the containers ports
 excluding the port specified in the canary service and service mesh sidecar ports.
diff --git a/kustomize/base/flagger/crd.yaml b/kustomize/base/flagger/crd.yaml
index 6e2092bf..e79d9c4b 100644
--- a/kustomize/base/flagger/crd.yaml
+++ b/kustomize/base/flagger/crd.yaml
@@ -191,11 +191,18 @@ spec:
                     appProtocol:
                       description: Application protocol of the port
                       type: string
+                    trafficDistribution:
+                      description: Traffic distribution of the service
+                      type: string
+                      enum:
+                        - PreferClose
+                        - PreferSameZone
+                        - PreferSameNode
                     targetPort:
                       description: Container target port name
                       x-kubernetes-int-or-string: true
                     portDiscovery:
-                      description: Enable port dicovery
+                      description: Enable port discovery
                       type: boolean
                     headless:
                       description: Headless if set to true, generates headless Kubernetes services.
diff --git a/pkg/apis/flagger/v1beta1/canary.go b/pkg/apis/flagger/v1beta1/canary.go
index 0b88ed16..82fd00ac 100644
--- a/pkg/apis/flagger/v1beta1/canary.go
+++ b/pkg/apis/flagger/v1beta1/canary.go
@@ -20,10 +20,11 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1"
-	istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
+
+	"github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1"
+	istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1"
 )
 
 const (
@@ -150,6 +151,11 @@ type CanaryService struct {
 	// +optional
 	AppProtocol string `json:"appProtocol,omitempty"`
 
+	// TrafficDistribution of the service
+	// https://kubernetes.io/docs/concepts/services-networking/service/#traffic-distribution
+	// +optional
+	TrafficDistribution string `json:"trafficDistribution,omitempty"`
+
 	// PortDiscovery adds all container ports to the generated Kubernetes service
 	PortDiscovery bool `json:"portDiscovery"`
 
diff --git a/pkg/canary/service_controller.go b/pkg/canary/service_controller.go
index 97de616a..68b70593 100644
--- a/pkg/canary/service_controller.go
+++ b/pkg/canary/service_controller.go
@@ -165,6 +165,11 @@ func buildService(canary *flaggerv1.Canary, name string, src *corev1.Service) *c
 		//   Operation cannot be fulfilled on services "mysvc-canary": the object has been modified; please apply your changes to the latest version and try again
 		delete(svc.ObjectMeta.Annotations, "kubectl.kubernetes.io/last-applied-configuration")
 	}
+
+	if v := canary.Spec.Service.TrafficDistribution; v != "" {
+		svc.Spec.TrafficDistribution = &v
+	}
+
 	return svc
 }
 
diff --git a/pkg/router/kubernetes_default.go b/pkg/router/kubernetes_default.go
index ffdd8059..f6c9fcb3 100644
--- a/pkg/router/kubernetes_default.go
+++ b/pkg/router/kubernetes_default.go
@@ -121,6 +121,10 @@ func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, nam
 		svcSpec.Ports[0].AppProtocol = &v
 	}
 
+	if v := canary.Spec.Service.TrafficDistribution; v != "" {
+		svcSpec.TrafficDistribution = &v
+	}
+
 	// set additional ports
 	for n, p := range c.ports {
 		cp := corev1.ServicePort{



Test patch:
diff --git a/pkg/router/kubernetes_default_test.go b/pkg/router/kubernetes_default_test.go
index e252c877..c9864f77 100644
--- a/pkg/router/kubernetes_default_test.go
+++ b/pkg/router/kubernetes_default_test.go
@@ -64,6 +64,39 @@ func TestServiceRouter_Create(t *testing.T) {
 	assert.Equal(t, "None", primarySvc.Spec.ClusterIP)
 }
 
+func TestServiceRouter_TrafficDistribution(t *testing.T) {
+	mocks := newFixture(nil)
+	trafficDistribution := "PreferClose"
+	mocks.canary.Spec.Service.TrafficDistribution = trafficDistribution
+
+	router := &KubernetesDefaultRouter{
+		kubeClient:    mocks.kubeClient,
+		flaggerClient: mocks.flaggerClient,
+		logger:        mocks.logger,
+	}
+
+	err := router.Initialize(mocks.canary)
+	require.NoError(t, err)
+
+	err = router.Reconcile(mocks.canary)
+	require.NoError(t, err)
+
+	canarySvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo-canary", metav1.GetOptions{})
+	require.NoError(t, err)
+	require.NotNil(t, canarySvc.Spec.TrafficDistribution)
+	assert.Equal(t, trafficDistribution, *canarySvc.Spec.TrafficDistribution)
+
+	primarySvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{})
+	require.NoError(t, err)
+	require.NotNil(t, primarySvc.Spec.TrafficDistribution)
+	assert.Equal(t, trafficDistribution, *primarySvc.Spec.TrafficDistribution)
+
+	apexSvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
+	require.NoError(t, err)
+	require.NotNil(t, apexSvc.Spec.TrafficDistribution)
+	assert.Equal(t, trafficDistribution, *apexSvc.Spec.TrafficDistribution)
+}
+
 func TestServiceRouter_Update(t *testing.T) {
 	mocks := newFixture(nil)
 	router := &KubernetesDefaultRouter{