← Back to posts

GCP ACE: Cloud Service Mesh Routing — VirtualService, DestinationRule, ServiceEntry, Gateway, and WorkloadEntry

The previous article covered what Cloud Service Mesh is and how a request flows through sidecars. This one covers the actual API objects you write to control that routing — Istio’s own CRDs, distinct from the plain Kubernetes Service/Ingress objects covered earlier in this series.

VirtualService — where a request actually goes

A VirtualService defines routing rules for traffic addressed to a host — which backend it actually reaches, based on rules you write (not just “whichever Pod matches the Service selector”).

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: reviews
  namespace: default
spec:
  hosts:
    - reviews.default.svc.cluster.local
  http:
    - name: route
      route:
        - destination:
            host: reviews
            subset: v1
          weight: 20
        - destination:
            host: reviews
            subset: v2
          weight: 80

This sends 20% of traffic to the v1 subset of reviews and 80% to v2 — a canary rollout, controlled entirely through config, with no changes to either version’s Deployment. Weights across all route entries for one HTTP rule must total 100.

DestinationRule — defining what a “subset” even means

A VirtualService’s subset: v1 only works because something elsewhere defines what v1 is. That’s the DestinationRule’s job — it groups a Service’s endpoints into subsets by label, and can also set policy (load balancing algorithm, connection pool limits, outlier detection) for the whole host or per subset.

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: reviews
  namespace: default
spec:
  host: reviews
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2

The pairing to remember: VirtualService decides where traffic goes; DestinationRule defines what the destinations actually are and how to treat them. You virtually always write these two together — a VirtualService referencing a subset that no DestinationRule defines simply won’t resolve.

ServiceEntry — adding something outside the mesh to the mesh’s registry

Everything so far assumes the destination is a Service already inside the mesh. A ServiceEntry extends the mesh’s internal service registry to include something external — a third-party API, a legacy database outside Kubernetes — so mesh traffic rules, retries, and (optionally) mTLS can apply to it too, the same way they apply to internal services.

apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: external-payments-api
  namespace: default
spec:
  hosts:
    - api.external-payments.example.com
  location: MESH_EXTERNAL
  ports:
    - number: 443
      name: https
      protocol: TLS
  resolution: DNS

Without a ServiceEntry, what happens to an unregistered external destination depends on the mesh’s outboundTrafficPolicy: with Istio’s default, ALLOW_ANY, the call is passed through untouched; with REGISTRY_ONLY, it’s blocked. Either way, explicitly registering the destination is what lets you apply mesh policy (timeouts, retries, TLS origination) to that external call.

Gateway (Istio) — not the same object as the Kubernetes Gateway API

This series already covered the Kubernetes Gateway API (Gateway/HTTPRoute, used for GKE’s single- and multi-cluster Gateway). Istio has its own, older Gateway resource — same name, different API group, different purpose, and the naming collision is easy to trip over in scenario questions.

apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: ingress-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway       # picks which gateway proxy deployment handles this
  servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: my-tls-secret
      hosts:
        - "shop.example.com"

An Istio Gateway only describes which ports/protocols/hosts are exposed at the mesh’s edge — it needs a VirtualService bound to it (via spec.gateways on the VirtualService) to actually define routing for the traffic that arrives there. A GKE Ingress or Kubernetes Gateway API resource, by contrast, is a complete routing object on its own.

WorkloadEntry and WorkloadGroup — bringing VMs into the mesh

Everything above assumes Kubernetes Pods. Real environments often still have a legacy VM that needs to participate in the mesh — call a meshed service with mTLS, or be called by one.

apiVersion: networking.istio.io/v1beta1
kind: WorkloadGroup
metadata:
  name: legacy-billing-vms
  namespace: default
spec:
  metadata:
    labels:
      app: legacy-billing
  template:
    serviceAccount: legacy-billing-sa
    network: vm-network
apiVersion: networking.istio.io/v1beta1
kind: WorkloadEntry
metadata:
  name: legacy-billing-vm-1
  namespace: default
spec:
  address: 10.10.0.5
  labels:
    app: legacy-billing
  serviceAccount: legacy-billing-sa

Once registered this way, that VM gets a sidecar-equivalent agent, participates in mTLS like any Pod, and can be targeted by VirtualServices and DestinationRules exactly like a Kubernetes workload — useful for a gradual migration off VMs rather than an all-or-nothing cutover.

Cloud Service Mesh caveat: WorkloadGroup/WorkloadEntry is the open-source Istio mechanism. Google’s managed TRAFFIC_DIRECTOR control plane documents its own path for Compute Engine VMs instead: routing mesh traffic to VMs through a GCPBackend resource, with a migration guide from Istio ServiceEntry. Check the managed supported-features page before relying on an Istio VM feature in Cloud Service Mesh.

Network resilience: retries, timeouts, circuit breaking

A VirtualService can also define resilience behavior directly on a route, without touching application code:

http:
  - route:
      - destination:
          host: reviews
          subset: v1
    retries:
      attempts: 3
      perTryTimeout: 2s
      retryOn: 5xx,reset,connect-failure
    timeout: 10s

And a DestinationRule’s trafficPolicy can add circuit breaking — ejecting an unhealthy instance from the load-balancing pool automatically:

spec:
  host: reviews
  trafficPolicy:
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 60s

This ejects a backend after 5 consecutive 5xx responses, for at least 60 seconds — entirely at the infrastructure layer, with zero changes to the application.

Exam Takeaways

Sources

↗ LinkedIn