The goal of this project is to build an east-west (EW) gateway to seamlessly interconnect two or more service-mesh clusters over an SD-WAN fabric, integrating the L4/L7 traffic management policies on the service-mesh side with the L3/L4 policies on the SD-WAN interconnect, as well as the observability and the security functions, for a consistent end-to-end user experience.
- Overview
- User stories
- Service-level traffic management
- L7 traffic management
- Resiliency
- Observability
- Getting started
- License
The idea is to provide consistent end-to-end traffic management, security and observability features across a multi-cluster service mesh deployment interconnected by an SD-WAN fabric.
The plan is to realize this goal as follows:
- Services between clusters are exported/imported using the Multi-cluster Services API (KEP-1645) CRDs, extended with a set of annotations to control SD-WAN routing policies.
- The import/export policies are rendered into regular Kubernetes Gateway API CRDs and implemented on top of a standard Kubernetes gateway implementation.
- The EW gateways exchange inter-cluster traffic in a way so that the SD-WAN fabric interconnecting the clusters can classify the traffic to/from different services into distinct traffic classes and deliver the required service level to each traffic class as specified by the operator.
- L7 policies, with SD-WAN-related semantics, can be added by the user to control the way traffic egresses from, and ingresses into, a cluster.
We default to pure unencrypted HTTP throughout for simplicity. It is trivial to enforce encryption by rewriting all rules to HTTPS.
We envision the following use cases.
-
Service-level SD-WAN policies. The
payment.secure
HTTP service (port 8080), deployed into cluster-1, serves sensitive data, so the service owner in cluster-1 wants to secure access to this service, by forcing all queries/responses to this service to be sent over the SD-WAN interconnect in the fastest and most secure way. At the same time, thelogging.insecure
service serves less-sensitive bulk traffic, so the corresponding traffic exchange defaults to the Internet. -
SD-WAN aware L7 traffic management. Same scenario as above, but now only GET queries to the
http://payment.secure:8080/payment
API endpoint are considered sensitive, access tohttp://payment.secure:8080/stats
is irrelevant (defaults to the Internet), and any other access to the service is denied. -
Resiliency: One of the clusters goes down. The EW gateways automatically initiate a circuit-breaking for the failed EW-gateway endpoint and retry all failed connections to another cluster where healthy backends are still running.
-
Monitoring: Operators want end-to-end monitoring across the clusters. EW-gateways add a
spanid
header to all HTTP(S) traffic exchanged over the SD-WAN interconnect to trace requests across the service-mesh clusters and the SD-WAN.
In the simplest model, the service owner can associate SD-WAN policies at the level of each distinct Kubernetes service, but there is no way to impose additional L7-level SD-WAN policies on top. This basic model will then be extended to a fully-fledged L7 model later.
Each global service can have a WAN policy associated with it (see below). The SD-WAN policies are
defined by a CRD named WANPolicy.mcw.l7mp.io
. The format of this CRD is completely unspecified
for now; below is a sample that is enough for the purposes of this note. At this point the best
option seems to be if we make these CRDs cluster-global, to avoid that WAN policies installed into
different namespaces somehow end up conflicting.
apiVersion: mcw.l7mp.io/v1alpha1
kind: WANPolicy
metadata:
name: sd-wan-priority-high
spec:
tunnel: business
port: 31111
sla:
jitter-ms: 50
latency-ms: 100
loss-percent: 1
---
apiVersion: mcw.l7mp.io/v1alpha1
kind: WANPolicy
metadata:
name: sd-wan-priority-low
spec:
tunnel: default
port: 31112
sla:
jitter-ms: 1000
latency-ms: 1000
loss-percent: 100
Note that the same WANPolicy CRs must exist on all clusters attached to the clusterset and they must specify the same SLAs/ports.
In order to allow access from other clusters, a service has to be explicitly exported from the hosting cluster.
Suppose that in the exporting cluster the payment service is defined as follows. Note that the
resultant FQDN for the service will be payment.secure.svc.cluster.local
.
apiVersion: v1
kind: Service
metadata:
name: payment
namespace: secure
spec:
selector:
app: payment
ports:
- port: 8080
protocol: TCP
One can use the ServiceExport.multicluster.k8s.io
CRD from the similarly named CRD from the
Multi-cluster Services
API
for controlling exported services. The only difference is that our ServiceExports can contain
additional annotations to encode the SD-WAN policy assigned by the service owner to the exported
service.
One possible way to do that is to explicitly specify the SD-WAN tunnel we want the service to be
assigned to. Below is sample ServiceExport for exporting the payment.secure
service from
cluster-1 over the SD-WAN tunnel sd-wan-priority-high
.
apiVersion: multicluster.k8s.io/v1alpha1
kind: ServiceExport
metadata:
name: payment
namespace: secure
annotations:
mcw.l7mp.io/mc-wan-policy: sd-wan-priority-high
The name/namespace of the ServiceExport must the same as that of the service to be exported. The
SD-WAN policy is specified using the mcw.l7mp.io/mc-wan-policy: sd-wan-priority-high
annotation;
this selects the tunnel associated with the sd-wan-priority-high
SD-WAN policy for exchanging all
traffic of the payment.secure
service across the SD-WAN. The SD-WAN policies will be specified by
a separate CRD below. Note that we cannot enforce these priorities on the receiver side (by the
time we receive the request on the ingress EW gateway it has already passed via the SD-WAN), so the
SD-WAN priorities will be enforced on the sender side.
An exported service will be imported only by clusters in which the service's namespace
exists. Service imports are represented with a CRD ServiceImport.multicluster.k8s.io
, which is
identical to the similarly named CRD from the Multi-cluster Services
API. The
CRD is automatically created in the importing cluster and the control plane is supposed to be fully
responsible for the lifecycle of a ServiceImport.
A sample ServiceImport corresponding to the above ServiceExport is shown below.
apiVersion: multicluster.k8s.io/v1alpha1
kind: ServiceImport
metadata:
name: payment
namespace: secure
spec:
type: ClusterSetIP
ports:
- name: http
protocol: TCP
port: 8080
At this point, the exported payment.secure
service can be reached from the importing cluster by
issuing a HTTP request to the host payment.secure.svc.clusterset.local
.
Up to this point, SD-WAN policies could be applied to individual Kubernetes services only, but there is no way to distinguish SD-WAN policies based on the API endpoint or certain HTTP header values. The below shows how to add L7 traffic management policies to the basic specification by reusing the above ServiceImport/ServiceExport CRs.
Suppose the service owner wants to filter incoming requests on the HTTP header: to the path
/payment
only GET
requests are allowed, everything else is denied. Assuming the server-side
cluster runs Istio, the below VirtualService should do the trick (the below assumes the default
spec.gateways: mesh
so the L7 policy will be enforced at all pods running the payment.secure
service).
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: payment
namespace: secure
spec:
hosts:
- payment.secure.svc.clusterset.local
http:
- match:
- uri:
prefix: "/payment"
method:
exact: GET
route:
- destination:
host: payment.secure.svc.cluster.local
Suppose the client consuming the payment.secure
in the importing cluster wishes to apply
additional L7 policies: e.g., for supporting canary deployment for the payment.secure
service. Suppose the prod
cluster exports the service payment-v1.secure
that runs the stable
version of the backend software (this will create a ServiceImport with the FQDN
payment-v1.secure.svc.clusterset.local
on the client side), while the dev
cluster exports the
experimental service payment-v2.secure
(with the ServiceImport FQDN
payment-v2.secure.svc.clusterset.local
on the client side). In this case the following will
VirtualService on the importing side will send all requests containing the cookie user=test
to
the v2 service, and everything else to the v1 service (again assuming Istio).
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: payment
namespace: secure
spec:
hosts:
- payment.secure.svc.clusterset.local
http:
- match:
headers:
cookie:
regex: "^(.*?;)?(user=test)(;.*)?"
route:
- destination:
host: payment-v2.secure.svc.clusterset.local
- route:
- destination:
host: payment-v1.secure.svc.clusterset.local
Note that the VirtualService uses the ServiceImport FQDNs as destination
services, so once we
have the SD-WAN compatible ServiceImport/ServiceExport logics we do not have to add additional L7
support, just rely on Istio to provide us the required L7 capabilities.
Another possibility is to route requests from the clients over different SD-WAN tunnels depending
on the HTTP headers. For instance, only GET requests to the /payment
path would be routed to the
high-priority tunnel, everything else should go the low-prio tunnel.
First, the server-side cluster must export two services, one over each SD-WAN tunnel, as follows:
apiVersion: v1
kind: Service
metadata:
name: payment-high-prio
namespace: secure
spec:
selector:
app: payment
ports:
- port: 8080
protocol: TCP
---
apiVersion: multicluster.k8s.io/v1alpha1
kind: ServiceExport
metadata:
name: payment-high-prio
namespace: secure
annotations:
mcw.l7mp.io/mc-wan-policy: sd-wan-priority-high
---
apiVersion: v1
kind: Service
metadata:
name: payment-low-prio
namespace: secure
spec:
selector:
app: payment
ports:
- port: 8080
protocol: TCP
---
apiVersion: multicluster.k8s.io/v1alpha1
kind: ServiceExport
metadata:
name: payment-low-prio
namespace: secure
annotations:
mcw.l7mp.io/mc-wan-policy: sd-wan-priority-low
Then, these will create two ServiceImports on the client-side, one with the FQDN
payment-high-prio.secure.svc.clusterset.local
that will be routed via the high-priority tunnel
and another one with the FQDN payment-low-prio.secure.svc.clusterset.local
that will be routed to
the low-prio tunnel. These ServiceImports can again be used as destinations for the
VirtualServices.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: payment
namespace: secure
spec:
hosts:
- payment.secure.svc.clusterset.local
http:
- match:
- uri:
prefix: "/payment"
method:
exact: GET
route:
- destination:
host: payment-high-prio.secure.svc.clusterset.local
- route:
- destination:
host: payment-low-prio.secure.svc.clusterset.local
Implementing health-check/retry/timeout/circuit-breaking policies can supported via the L7 capabilities provided by Istio's L7 traffic management policies.
Adding a spanid
HTTP header to each connection crossing the SD-WAN is already within the
capabilities of the framework, but maybe at a certain point we could provide some automation around
this.
(Work in progress)
A proof-of-concept tool is available that automates the ServiceImport and ServiceExport workflows. For now, the tool provides a simple imperative interface, whereby the user explicitly performs service imports/exports, providing all the necessary parameters on the command line. Later, this tool will be developed into a Kubernetes operator to automatically reconcile the ServiceImport/ServiceExport CRDs.
It is assumed two Kubernetes clusters are available, Istio with the built-in Gateway API
implementation is
installed into both clusters, and kubectl
is configured with the necessary credentials to reach
both clusters, with the context for each of the clusters available in the environment variables
$CTX1
and $CTX2
.
Clone the repository and build the mcwanctl
command line tool.
cd mc-wan
go build -o mcwanctl main.go
Export the payment.secure
service from cluster-1 over the high-priority SD-WAN tunnel.
mcwanctl --context $CTX1 export payment/secure --wan-policy=high
This call will set up the server-side pipeline to ingest requests to the payment.secure
service
into the cluster and route them to the proper backend pods.
The below command will query the status of a service-export, providing the SD-WAN policy associated
with the service exposition and a GW_IP_ADDRESS
that can be used to reach the service from other
clusters.
mcwanctl --context $CTX1 status payment/secure
The below command deletes a service export.
mcwanctl --context $CTX1 unexport payment/secure
In and of itself a service export will not do much; to actually reach an exported service a cluster
needs to explicitly import it. The below will import the payment.secure
service into cluster-2.
mcwanctl --context $CTX2 import payment/secure --ingress-gw=<GW_IP_ADDRESS>
At this point, requests from cluster-2 to http://payment.secure.svc.clusterset.local:8000
should
be routed through the high-priority SD-WAN tunnel and land at one of the backend pods in cluster-1.
Finally, remove a service import by unimporting it.
mcwanctl --context $CTX2 unimport payment/secure
Copyright 2021-2022 by its authors. Some rights reserved. See AUTHORS.
MIT License - see LICENSE for full text.