Expanding Service Mesh Boundaries With WebAssembly

Marton Sereg
Marton Sereg

Tuesday, October 25th, 2022

The service mesh seemed to be one of the hottest topics of KubeCons a couple of years ago as it was reaching the peak of expectations on the hype cycle. The hype may have faded a bit but it is clear that the service mesh is here to stay. But as it reaches the plateau of productivity, it needs to adapt to a broader set of use cases.

The service mesh is getting more and more adoption in the cloud. But real-world environments are not only about Kubernetes services talking to each other. It's also about edge devices, legacy VMs, and mobile clients. And there's a real gap between the conventional server world and these environments. What may work in a "traditional" cloud infrastructure - mainly Kubernetes - may not work when someone would like to use the same tooling elsewhere. It means different tooling and conventions, and therefore, the advantage of a dedicated, unified service mesh layer disappears.

We've been working with Cisco Calisti customers for quite some time and an always returning question is how can the service mesh be expanded to these kinds of services? In this post, we're trying to offer a solution to bridge this gap by introducing a client library that integrates well with Istio and works everywhere.

Introducing Nasp

Nasp is an open-source, lightweight library to expand service mesh capabilities to non-cloud environments​ by getting rid of the complexity of operating dedicated network proxies. It is not meant to be a complete service mesh replacement, but rather an extension. It integrates well with an Istio control plane, so applications using Nasp can be handled as standard Istio workloads.

Nasp offers the most common functionalities of a sidecar proxy, so it eases the developer burden for traffic management, observability, and security. Its range of capabilities includes:

  • Identity and network traffic security using mutual TLS
  • Automatic traffic management features, like HTTP or gRPC load balancing
  • Transparent observability of network traffic, especially standard Istio metrics
  • Dynamic configuration through xDS support

This high-level feature set seems vaguely similar to that of a sidecar proxy, and it's not a coincidence. We wanted to cover as much of the capabilities of a sidecar proxy as possible, but without having to deploy one next to our application.

Instead, when using Nasp you'll have to include it in your application code as a library. It's not an intrusive change, and most of the functionality - like metrics or TLS - is transparent to the developers of the client. We'll show you an example of how it works a bit later in the post, but first, let's clarify a few things about Nasp versus other sidecarless service mesh solutions.

Nasp vs sidecarless meshes

There is a real buzz about sidecarless meshes recently, especially with the introduction of Istio's Ambient Mesh. From a bird's eye view, Nasp is comparable to these solutions. It can connect to an Istio control plane without utilizing a sidecar. But it's different in the sense that it breaks one of the original pillars of the service mesh: its lifecycle is bound with the application.

It was a deliberate design decision. We know that packaging Nasp as a library takes away one of the great advantages of a service mesh, but we also know that deploying and managing lots and lots of proxies is not a feasible solution outside of classic cloud environments. It wasn't our goal to compete with existing sidecarless alternatives, but rather to provide a way to include non-cloud applications in a service mesh.

It is possible to have an Istio service mesh running in a Kubernetes cluster with sidecars deployed for each application, but also have external clients connected to the same control plane using Nasp. Or in the future, it can even be Ambient Mesh running on the cluster and external clients connected with Nasp.

WebAssembly is making things easier

We're big believers of cloud-side WebAssembly. Our vision is that in the future a WebAssembly based service fabric could eliminate most of the complexity that comes with implementing service-to-service connectivity. We may even be closer to that reality as one would think, but let's not jump forward.

For now, we're utilizing the power behind the pluggability of components compiled to WebAssembly. The Proxy-Wasm API is the primary extension mechanism in Istio's Envoy proxies. Some of the proxy filters - like telemetry collection, or metadata exchange - are compiled to WASM and can be used through Proxy-Wasm instead of the compiled-in filters. We are reusing these filters in Nasp by running them through a WebAssembly runtime. We chose wazero as the runtime but we've designed Nasp in a way that the runtime is interchangeable. In addition to not having to rewrite this code, WebAssembly brings another advantage: we can keep the same plugin mechanism in Nasp that's there in Envoy, so a Nasp client can be extended with with custom filters compiled to WASM.

We're closely following the development of WebAssembly, and we believe that reusing Proxy-Wasm is only the first step in the journey of Nasp. We can see it evolving into something bigger that would ease the development and update of mobile or IoT clients by remotely distributing WebAssembly modules to clients and altering their behavior.

Let's see an example

The easiest way to get started with Nasp is to import the library from Go code, set up a Nasp HTTP transport, and use that transport to send HTTP requests to an external server. Here's how to do it.

First, import the library:

import (
  "github.com/cisco-open/nasp/pkg/istio"
  "istio_ca "github.com/cisco-open/nasp/pkg/ca/istio"
)

Then, create and start an IstioIntegrationHandler:

istioHandlerConfig := &istio.IstioIntegrationHandlerConfig {
	MetricsAddress: ":15090",
	UseTLS:         true,
	IstioCAConfigGetter: func(e *environment.IstioEnvironment) (istio_ca.IstioCAClientConfig, error) {
		return istio_ca.GetIstioCAClientConfig(clusterID, istioRevision)
	},
}

iih, err := istio.NewIstioIntegrationHandler(istioHandlerConfig, klog.TODO())
if err != nil {
		panic(err)
}

iih.Run(ctx)

And finally, send an HTTP request through the Nasp transport layer:

transport, err := iih.GetHTTPTransport(http.DefaultTransport)
if err != nil {
	panic(err)
}

httpClient := &http.Client{
	Transport: transport,
}

request, err := http.NewRequest("GET", url, nil)
if err != nil {
	return err
}

response, err := httpClient.Do(request)
if err != nil {
	return err
}

To see and test a fully working example of the above code snippet, check out the examples directory on Github that contains runnable Go code of setting up and operating HTTP, gRPC, or TCP connections.

Support for other languages

The core code of Nasp is written in Go, that's why the primary examples are also written in Go. But Nasp is not only a Go library. We know that it's important that Nasp could be imported from other languages, so we're using C bindings generated from the core codebase to have multi-language support. Then only a thin layer should be written for specific platforms to be included in the application code. For example, these C bindings also serve as the base of how Nasp can be used from iOS or Android with the help of the go-mobile package.

If you want to see it in action or want to give it a try, check out the mobile examples in the Github repo.

Use-cases

We've already mentioned most of the cases where we think Nasp could be useful, but it's worth another look.

  1. Mobile applications

Mobile environments are becoming increasingly important for end-user experience, and therefore, it is becoming a natural requirement to provide the same level of service as cloud services. A self-contained proxy cannot be deployed in front of an application running on an iOS or Android device because of how these apps are distributed, so a "classic" service mesh-like solution is not suitable here. But it could still be useful to have the same, consistent L4 and L7 functionality for mobile devices as we have with cloud services, because resiliency, mutual TLS, or observability is similarly hard to implement properly.

As Nasp is distributed as a library, it can be easily integrated into the mobile application, provide the same advanced networking functionality, or even allow you to connect to an Istio mesh.

  1. IoT or edge devices

The restrictions of mobile environments may not apply here, but the deployment complexity of proxies can still cause problems on IoT or edge devices. Operating and maintaining lots and lots of proxies are easier on Kubernetes where most of these processes can be automated. Having the abstraction built in the application means easier - but app lifecycle-bound - updates. It's a trade-off often worth taking outside of Kubernetes.

  1. Cloud environments

It may not be the primary use case, but technically nothing is preventing a service mesh setup where all applications use Nasp instead of a sidecar. It's the same trade-off we've emphasized multiple times: the service mesh data plane won't have a separate lifecycle. We don't advise switching to this model for every new project, but we can see simpler, smaller environments working this way. In reality, running a sidecar (or other kinds of proxies) is not always as transparent as it is on paper. It can cause problems with testing or debugging when something is "working on the developer's machine", but not in staging or production. Using Nasp, the same code runs everywhere, and network traffic is not intercepted anywhere else between services. In a smaller team of developers, without someone specifically responsible for the service mesh layer it could be a good compromise.

Getting started

Nasp is open-source and can be found on Github. Give it a try and check out the Getting Started guide, fix things in the code, send a PR, or just tell us your ideas, feature requests, or feedback. It is more than welcome.