cloud

Run Istio ambient mode with waypoint proxies

How I enroll workloads into ambient mesh and add service-scoped L7 waypoints.

English繁中
Run Istio ambient mode with waypoint proxies

I use Istio ambient mode to enroll namespaces in the mesh without adding a sidecar to every Pod. Ztunnel handles secure L4 traffic; I add a waypoint to services that need L7 routing, authorization, or telemetry.

The example application set stays the same:

  • example-api: public API at api.example.com
  • example-worker: public worker UI at worker.example.com
  • example-admin: internal admin app, no public route

In this post, example-api gets a service-scoped waypoint. example-worker starts with ambient L4 only. example-admin stays internal, but it can still be enrolled in ambient mode later.

What ambient mode changes

With sidecar mode, every pod gets an Envoy sidecar. With ambient mode, the first layer is node-level traffic capture through Istio CNI and ztunnel.

For a namespace, the switch is just a label:

apiVersion: v1
kind: Namespace
metadata:
  name: example-api
  labels:
    istio.io/dataplane-mode: ambient

The label enrolls eligible existing and new Pods; creating new Pods is not a requirement. Ztunnel handles their L4 mesh traffic without an injected sidecar. See Istio workload enrollment.

That does not mean I get every L7 feature automatically. For HTTP routing, authorization, telemetry, or policy that needs L7 visibility, I add a waypoint.

GitOps sync order

Ambient mode has a few platform pieces that must exist before application pods are enrolled.

In my app-of-apps layout, the order is:

  • wave -40: Gateway API CRDs
  • wave -30: Istio base CRDs and cluster roles
  • wave -20: Istio control plane
  • wave -10: Istio CNI
  • wave -5: ztunnel
  • wave 10+: application namespaces and workloads

The important part is that the namespace label should not be applied before the cluster has CNI and ztunnel healthy. Otherwise, the first sync becomes harder to debug because workloads and the mesh are arriving at the same time.

Add a waypoint

For service-scoped L7 processing, I create a Gateway with gatewayClassName: istio-waypoint.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: example-api-waypoint
  namespace: example-api
  labels:
    istio.io/waypoint-for: service
spec:
  gatewayClassName: istio-waypoint
  listeners:
    - name: mesh
      port: 15008
      protocol: HBONE

Then I label the Service that should use it:

apiVersion: v1
kind: Service
metadata:
  name: example-api
  namespace: example-api
  labels:
    istio.io/use-waypoint: example-api-waypoint
spec:
  selector:
    app: example-api
  ports:
    - name: http
      port: 3000
      targetPort: 3000

The Service label selects the destination waypoint for mesh traffic. Other Services in the ambient namespace can remain on L4 processing alone.

Traffic from an Istio ingress gateway bypasses the destination waypoint by default. To send it through the waypoint as well, Istio 1.25+ supports istio.io/ingress-use-waypoint: "true" on the destination Service or Namespace and requires ENABLE_INGRESS_WAYPOINT_ROUTING=true on istiod. The Service manifest above does not enable that path. Verify these settings and actual traffic before relying on waypoint L7 policy for public requests; see ingress gateways and waypoints.

Restart an existing workload only when the migration needs it

Adding the namespace label enrolls workloads in ambient mode; it does not, by itself, make a restart a general enrollment step. I restart an existing workload gradually when removing a prior sidecar setup or when I need a clear before-and- after point for validating traffic and metrics. Newly created Pods do not need a separate restart for that reason.

kubectl -n example-api rollout restart deployment example-api
kubectl -n example-worker rollout restart deployment example-worker

For a real migration, I do this gradually and validate the traffic path after each workload changes.

External services

Some pods call HTTPS APIs outside the cluster. For those, I add explicit ServiceEntry resources so the mesh and Kiali can classify egress traffic.

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: example-vendor-api
  namespace: example-api
spec:
  hosts:
    - api.vendor.example
  location: MESH_EXTERNAL
  ports:
    - number: 443
      name: tls
      protocol: TLS
  resolution: DNS

I avoid wildcard hosts here. Explicit FQDNs are easier to audit and usually enough for service-to-service integrations.

Validate

First I check the ambient platform pieces.

kubectl -n istio-system get pods -l app=ztunnel
kubectl -n istio-system get pods -l k8s-app=istio-cni-node
kubectl -n istio-system get pods -l app=istiod

Then I check the namespace and waypoint.

kubectl get namespace example-api --show-labels
kubectl -n example-api get gateway example-api-waypoint
kubectl -n example-api get deploy,svc | grep waypoint
kubectl -n example-api get svc example-api --show-labels

For egress visibility:

kubectl -n example-api get serviceentry

For application health:

kubectl -n example-api get pods -o wide
kubectl -n example-api logs deploy/example-api --tail=80

Kiali access

I keep Kiali private. If anonymous auth is enabled for a home lab, it should still stay as ClusterIP and be opened with a local port-forward.

kubectl -n istio-system port-forward svc/kiali 20001:20001

Then open http://127.0.0.1:20001 locally.

Kiali workload graph showing ambient traffic flowing through the bus API waypoint

The Kiali workload graph shows the public Istio Gateway, the bus-api service-scoped waypoint, and downstream traffic to the queue and Redis workloads.

I do not expose Kiali through the public Gateway. It is an observability and control-plane tool, not an app endpoint.

Common problems

If the waypoint does not appear, I check whether Gateway API CRDs and Istio control plane synced before the application.

If the Service label exists but traffic does not look right, I check the workload’s enrollment state and any migration steps, including whether an old sidecar setup has been removed.

If Kiali shows unknown external traffic, I check whether the pod is calling a host that does not have a matching ServiceEntry.

If a namespace is not ready for mesh traffic, I remove the ambient label from that namespace instead of trying to debug everything at once.