All Products
Search
Document Center

Alibaba Cloud Service Mesh:Dynamic subset routing

Last Updated:Mar 11, 2026

With static subset routing, every DestinationRule must list each subset explicitly. When a new version ships or an old one retires, someone has to update the rule by hand. Dynamic subset routing removes that overhead: Service Mesh (ASM) watches workload labels and groups endpoints into subsets automatically, keeping routing rules current without manual changes.

The end-to-end example below deploys a multi-version application, routes requests to specific versions and environments by HTTP header, and configures fallback behavior when a target subset does not exist.

How dynamic subsets work

In standard Istio routing, a DestinationRule enumerates every subset with a fixed set of labels. When versions change, the rule must be updated to match.

Dynamic subsets take a different approach. Instead of listing each subset, you specify one or more grouping keys (for example, version and stage). ASM inspects the labels on every endpoint behind a service and groups endpoints that share the same key-value combination into the same subset -- automatically.

A VirtualService then maps incoming request headers to those grouping keys. For example, a request carrying x-version: v2 and x-stage: prod routes to the subset where version=v2 and stage=prod.

Prerequisites

Before you begin, make sure that you have:

Step 1: Deploy the sample application

This example uses hashicorp/http-echo to simulate a multi-environment, multi-version deployment:

  • dev environment: versions v1, v2, v3

  • prod environment: versions v2, v3

Each pod replies with its environment, version, and IP address. A helloworld Service on port 8000 fronts all pods, and a sleep Deployment provides a client for testing.

Deployment topology

Deploy all resources with kubectl. For more information, see Deploy an application in an ACK cluster that is added to an ASM instance.

dev environment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-dev-v1
  labels:
    app: helloworld
    version: v1
    stage: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helloworld
      version: v1
      stage: dev
  template:
    metadata:
      labels:
        app: helloworld
        version: v1
        stage: dev
    spec:
      containers:
      - name: helloworld
        image: hashicorp/http-echo:0.2.3
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5678
        env:
        - name: PODIP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: STAGE
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['stage']
        - name: VERSION
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['version']
        command: ["/http-echo"]
        args:
          - "-text"
          - "Welcome to helloworld stage: $(STAGE), version: $(VERSION), ip: $(PODIP)"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-dev-v2
  labels:
    app: helloworld
    version: v2
    stage: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helloworld
      version: v2
      stage: dev
  template:
    metadata:
      labels:
        app: helloworld
        version: v2
        stage: dev
    spec:
      containers:
      - name: helloworld
        image: hashicorp/http-echo:0.2.3
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5678
        env:
        - name: PODIP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: STAGE
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['stage']
        - name: VERSION
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['version']
        command: ["/http-echo"]
        args:
          - "-text"
          - "Welcome to helloworld stage: $(STAGE), version: $(VERSION), ip: $(PODIP)"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-dev-v3
  labels:
    app: helloworld
    version: v3
    stage: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helloworld
      version: v3
      stage: dev
  template:
    metadata:
      labels:
        app: helloworld
        version: v3
        stage: dev
    spec:
      containers:
      - name: helloworld
        image: hashicorp/http-echo:0.2.3
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5678
        env:
        - name: PODIP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: STAGE
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['stage']
        - name: VERSION
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['version']
        command: ["/http-echo"]
        args:
          - "-text"
          - "Welcome to helloworld stage: $(STAGE), version: $(VERSION), ip: $(PODIP)"

prod environment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-prod-v2
  labels:
    app: helloworld
    version: v2
    stage: prod
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helloworld
      version: v2
      stage: prod
  template:
    metadata:
      labels:
        app: helloworld
        version: v2
        stage: prod
    spec:
      containers:
      - name: helloworld
        image: hashicorp/http-echo:0.2.3
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5678
        env:
        - name: PODIP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: STAGE
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['stage']
        - name: VERSION
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['version']
        command: ["/http-echo"]
        args:
          - "-text"
          - "Welcome to helloworld stage: $(STAGE), version: $(VERSION), ip: $(PODIP)"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-prod-v3
  labels:
    app: helloworld
    version: v3
    stage: prod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: helloworld
      version: v3
      stage: prod
  template:
    metadata:
      labels:
        app: helloworld
        version: v3
        stage: prod
    spec:
      containers:
      - name: helloworld
        image: hashicorp/http-echo:0.2.3
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5678
        env:
        - name: PODIP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: STAGE
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['stage']
        - name: VERSION
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['version']
        command: ["/http-echo"]
        args:
          - "-text"
          - "Welcome to helloworld stage: $(STAGE), version: $(VERSION), ip: $(PODIP)"

helloworld Service

apiVersion: v1
kind: Service
metadata:
  name: helloworld
  labels:
    app: helloworld
    service: helloworld
spec:
  ports:
  - port: 8000
    name: http
    targetPort: 5678
  selector:
    app: helloworld

sleep client

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      labels:
        app: sleep
    spec:
      terminationGracePeriodSeconds: 0
      serviceAccountName: sleep
      containers:
      - name: sleep
        image: curlimages/curl
        command: ["/bin/sleep", "infinity"]
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - mountPath: /etc/sleep/tls
          name: secret-volume
      volumes:
      - name: secret-volume
        secret:
          secretName: sleep-secret
          optional: true

Step 2: Route requests to a specific version and environment

After deployment, Kubernetes load-balances requests across all helloworld pods regardless of version or environment. To target a specific combination, create a DestinationRule that defines the grouping keys and a VirtualService that maps request headers to those keys.

Create the DestinationRule

Apply the following DestinationRule to group endpoints by stage and version. For more information, see Manage destination rules.

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: helloworld
  namespace: default
spec:
  host: helloworld.default.svc.cluster.local
  trafficPolicy:
    loadBalancer:
      dynamicSubset:
        subsetSelectors:
          - keys:
              - stage
              - version

ASM reads the stage and version labels from each endpoint and produces the following subsets:

SubsetPodIP address
stage=dev, version=v1helloworld-dev-v1-67b6876778-nf7pz192.168.0.5
stage=dev, version=v2helloworld-dev-v2-68f65bbc99-v957l192.168.0.1
stage=dev, version=v3helloworld-dev-v3-7f6978bc56-hqzgg192.168.0.252
stage=prod, version=v2helloworld-prod-v2-b5745b949-p8rc4192.168.0.103
stage=prod, version=v3helloworld-prod-v3-6768bf56f8-6bd6h192.168.0.104, 192.168.0.6

Create the VirtualService

Apply the following VirtualService to map HTTP headers to dynamic subset keys. For more information, see Manage virtual services.

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: helloworld
  namespace: default
spec:
  hosts:
    - helloworld.default.svc.cluster.local
  http:
    - headerToDynamicSubsetKey:
        - header: x-version        # Map the x-version header to the "version" grouping key
          key: version
          defaultValue: v3          # Fall back to v3 if the header is absent
        - header: x-stage           # Map the x-stage header to the "stage" grouping key
          key: stage
          defaultValue: prod        # Fall back to prod if the header is absent
      name: default
      route:
        - destination:
            host: helloworld.default.svc.cluster.local
            port:
              number: 8000

This VirtualService establishes two mappings:

  • x-version header to version grouping key. Default: v3 when the header is absent.

  • x-stage header to stage grouping key. Default: prod when the header is absent.

Verify the routing

Send a request targeting the dev environment, version v1:

kubectl exec -it deploy/sleep -c sleep -- curl -H 'x-stage: dev' -H 'x-version: v1' helloworld:8000

Expected output:

Welcome to helloworld stage: dev, version: v1, ip: 192.168.0.5

The request reaches the pod where stage=dev and version=v1, confirming that dynamic subset routing works correctly.

Step 3: Configure fallback policies

Version v1 does not exist in the prod environment. A request targeting stage=prod, version=v1 matches no subset:

kubectl exec -it deploy/sleep -c sleep -- curl -H 'x-stage: prod' -H 'x-version: v1' helloworld:8000
# Output: no healthy upstream

To handle unmatched subsets, set a fallbackPolicy in the DestinationRule. ASM supports three policies:

PolicyBehaviorRecommended for
NO_FALLBACKReturns a no healthy upstream error. This is the default.Strict routing where misrouted requests must fail fast
ANY_ENDPOINTRoutes to any available endpoint across all subsetsDevelopment or testing where availability matters more than precision
DEFAULT_SUBSET (recommended for production)Routes to a preconfigured default subsetProduction workloads, where unmatched requests should land on a known-good version

NO_FALLBACK

NO_FALLBACK is the default behavior. Requests that match no subset return an error. Set it explicitly to make the fail-fast intent clear.

Apply the following DestinationRule. For more information, see Manage destination rules.

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: helloworld
  namespace: default
spec:
  host: helloworld.default.svc.cluster.local
  trafficPolicy:
    loadBalancer:
      dynamicSubset:
        subsetSelectors:
          - fallbackPolicy: NO_FALLBACK
            keys:
              - stage
              - version

Verify:

kubectl exec -it deploy/sleep -c sleep -- curl -H 'x-stage: prod' -H 'x-version: v1' helloworld:8000

Expected output:

no healthy upstream

ANY_ENDPOINT

When no subset matches, requests route to any available endpoint regardless of labels.

Apply the following DestinationRule. For more information, see Manage destination rules.

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: helloworld
  namespace: default
spec:
  host: helloworld.default.svc.cluster.local
  trafficPolicy:
    loadBalancer:
      dynamicSubset:
        subsetSelectors:
          - fallbackPolicy: ANY_ENDPOINT
            keys:
              - stage
              - version

Verify by sending two requests. Each may land on a different pod:

# First request
kubectl exec -it deploy/sleep -c sleep -- curl -H 'x-stage: prod' helloworld:8000

Example output:

Welcome to helloworld stage: prod, version: v2, ip: 192.168.0.103
# Second request -- targets a non-existent subset
kubectl exec -it deploy/sleep -c sleep -- curl -H 'x-stage: prod' -H 'x-version: v1' helloworld:8000

Example output:

Welcome to helloworld stage: dev, version: v2, ip: 192.168.0.1

Because no prod/v1 subset exists, the request falls back to a random endpoint.

DEFAULT_SUBSET

When no subset matches, requests route to the subset defined in defaultSubset. This is the recommended policy for production workloads because unmatched requests land on a known-good version rather than failing or scattering randomly.

Apply the following DestinationRule. In this example, defaultSubset points to stage=prod, version=v3. For more information, see Manage destination rules.

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: helloworld
  namespace: default
spec:
  host: helloworld.default.svc.cluster.local
  trafficPolicy:
    loadBalancer:
      dynamicSubset:
        defaultSubset:
          stage: prod
          version: v3
        subsetSelectors:
          - fallbackPolicy: DEFAULT_SUBSET
            keys:
              - stage
              - version

Verify by sending two requests to the non-existent prod/v1 subset:

kubectl exec -it deploy/sleep -c sleep -- curl -H 'x-stage: prod' -H 'x-version: v1' helloworld:8000

Expected output:

Welcome to helloworld stage: prod, version: v3, ip: 192.168.0.6
kubectl exec -it deploy/sleep -c sleep -- curl -H 'x-stage: prod' -H 'x-version: v1' helloworld:8000

Expected output:

Welcome to helloworld stage: prod, version: v3, ip: 192.168.0.104

Both requests land on the prod/v3 default subset. The two different IPs (192.168.0.6 and 192.168.0.104) reflect load balancing across the two prod-v3 replicas.

Step 4: Route to a specific pod by IP address

To pin requests to an individual pod, use the built-in %ip% attribute as the grouping key. Each pod becomes its own single-member subset.

Create the DestinationRule

Apply the following DestinationRule to group pods by IP address. For more information, see Manage destination rules.

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: helloworld
  namespace: default
spec:
  host: helloworld.default.svc.cluster.local
  trafficPolicy:
    loadBalancer:
      dynamicSubset:
        defaultSubset:
          stage: prod
          version: v3
        subsetSelectors:
          - keys:
              - '%ip%'

Create the VirtualService

Map the x-ip header to the %ip% key so that the header value specifies the target pod IP. For more information, see Manage virtual services.

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: helloworld
  namespace: default
spec:
  hosts:
    - helloworld.default.svc.cluster.local
  http:
    - headerToDynamicSubsetKey:
        - header: x-ip
          key: '%ip%'
      name: default
      route:
        - destination:
            host: helloworld.default.svc.cluster.local
            port:
              number: 8000

Verify the routing

Send multiple requests targeting pod IP 192.168.0.6:

kubectl exec -it deploy/sleep -c sleep -- curl -H 'x-ip: 192.168.0.6' helloworld:8000

Expected output (consistent across repeated calls):

Welcome to helloworld stage: prod, version: v3, ip: 192.168.0.6

Every request lands on the same pod, confirming IP-based pinning.

CRD field reference

VirtualService

HTTPRoute

ASM extends the HTTPRoute resource with the headerToDynamicSubsetKey field.

FieldTypeDescription
headerToDynamicSubsetKeyHeaderToMetadataSubsetKey[]Maps request headers to dynamic subset grouping keys. Each element defines one header-to-key mapping.

HeaderToMetadataSubsetKey

FieldTypeDescription
headerstringName of the request header.
keystringName of the dynamic subset grouping key. A value enclosed in percent signs (for example, %ip%) refers to a built-in workload attribute.
defaultValuestringValue used when the request does not carry the specified header. If omitted and the header is absent, the key is left unset and excluded from subset matching.

DestinationRule

ASM extends the trafficPolicy structure with the dynamicSubset field.

TrafficPolicy

FieldTypeDescription
dynamicSubsetDynamicSubsetLBConfigures dynamic subset grouping rules.

DynamicSubsetLB

FieldTypeDescription
defaultSubsetmap[string]stringDefault subset used when fallbackPolicy is DEFAULT_SUBSET and no subset matches the request.
subsetSelectorsSubsetSelector[]List of grouping rules. Each element defines a separate grouping dimension.
fallbackPolicyDynamicSubsetLB_FallbackPolicyGlobal fallback policy when no subset matches. Defaults to NO_FALLBACK.

SubsetSelector

FieldTypeDescription
keysstring[]Grouping dimensions mapped to workload labels. For example, version groups endpoints by the version label. Built-in workload attributes such as %ip% are also supported.
fallbackPolicyDynamicSubsetLB_FallbackPolicyFallback policy for this specific grouping rule. Overrides the policy set in DynamicSubsetLB.

DynamicSubsetLB_FallbackPolicy

ValueDescription
NO_FALLBACKReturns an error when no subset matches. This is the default.
ANY_ENDPOINTRoutes to any endpoint of the service when no subset matches.
DEFAULT_SUBSETRoutes to the subset defined in defaultSubset when no subset matches.

Built-in workload attributes

AttributeTypeDescription
%ip%stringIP address of the pod where the workload runs.

See also