Over the years Azure has evolved. First it was a host for stateless applications, using a range of Azure services. Then it became a host for virtualized infrastructures, supported by cloud hosted tools for big data and for storage. Now it’s supporting cloud-native distributed application development, using containers and tools like Kubernetes to manage your code.
In its earlier iterations, Azure didn’t need much in the way of new programming skills. Even as a stateless platform, you could use many of your existing .Net skills to build and deploy apps. But distributed systems, like those that run on Kubernetes, are very different. And while you can build apps using the same tools and techniques you always have, the underlying architectures and design patterns are quite different.
Brendan Burns, one of the original founders of the Kubernetes Project, is now a distinguished engineer on the Azure team. As part of that role, he’s working on a set of design patterns for Kubernetes-based applications that can help architects and developers move into the world of distributed application development, and he talked about these ideas at O’Reilly’s 2018 Oscon conference.
A pattern language for containers
As Burns noted, the history of programming is one of increasing abstraction, and of tools and patterns that guide developers. As a profession, you’ve taken steps from raw assembly language programming to Fortran and on to Donald E. Knuth’s seminal book series The Art of Computer Programming, then as your applications got bigger and bigger, you added object orientation and thought about design patterns.
Now you’re building distributed systems at scale, using platforms like Azure. While you have the tools, with Kubernetes and containers, you’re still missing the modern equivalent of Knuth or the Gang of Four. That’s where Burns’s work (and a forthcoming book) come in to play. He’s been thinking about recurring patterns in container application development and deployment, and he has started to put names to some of these patterns.
In Burns’s taxonomy, the underlying unit of deployment is the container. It doesn’t matter how much code is running in your container; as far as the architectural patterns are concerned, its service endpoints are the boundary that defines it. Building on the Kubernetes model, individual containers are grouped in pods, with each pod being a higher-level point of deployment across clusters and across clouds.
The simplest set of patterns are those that use a single node, with no implicit parallel operations. Most of your application runs in a single container. You can think of it as a traditional microservice deployment. The container boundary is irrelevant, until you start to extend the application with additional containers
Sidecar pattern.In fact, the underlying code doesn’t even need to be designed to be extended, as all it has is local and shared storage. Using what Burns calls the “sidecar,” you can take that code and add new functionality by attaching another container to those files, “augmenting and extending” the initial container, as he puts it. There’s no need for a sidecar to have any details of the app it’s extending; all it needs is access to its files,
An example of this is a containerized web server, with a sidecar log-shipping container that sits between its existing logging local storage and a cloud storage service. The original code in the container doesn’t need to work with S3 buckets or Azure Blobs—those APIs are handled by the sidecar where appropriate. Log files are written normally, and the sidecar ships them to appropriate storage.
Ambassador pattern.The ambassador pattern is similar, though here you need more details of the underlying app container. An ambassador represents the original application to the rest of the world, implementing a proxy to handle API translations or to support and manage sharding data across distributed storage. Unlike the sidecar, an ambassador needs details of the underlying app to handle translations and transitions effectively.
Adapter pattern.The final single-node pattern is the adapter. Like the ambassador, this too is a link between a microservice and the outside world. However, here it’s a tool for translating protocols. If your code has been using internal APIs and you want to connect it to an outside service an adapter can handle those connections – both in and out of the service.
Single-node distributed application patterns are all relatively simple. They take existing applications and repurpose them for use in the wider world, minimizing the changes you need to make to your code.
But multinode patterns are more complex, and are more what you think of when you think of distributed system design.
Replicated service pattern. Burns’s first multinode pattern is a familiar one: the replicated service. Using a load balancer, you can deploye multiple replicas of the same base service, with new replicas deployed or redundant ones destroyed as necessary. It’s like how you build and deploy high-density web servers at scale. It’s also a model that lends itself to programmatic service deployment, via a new generation of service tools like Burns’s own metaparticle.io or the system developed by Microsoft alumni at Pulumi.
By breaking a service deployment down into code, you can deploy the same code wherever you want, instantiating the same containers on any Kubernetes instance, either in different Azure zones or across multiple clouds.
Sharded system patterns. More complex multinode patterns include sharded systems, where a layer of routing services behind a load balancer hands off service state to a sharded data store. This approach adds speed and resilience to replicated systems, and can be itself replicated multiple times.
It’s all code in the future
By breaking down distributed systems into repeatable patterns, it’s easier to see how you can use programming techniques to design and deploy services. You can use the same patterns to define the microservices you use to build your applications, giving you quick ways of delivering additional functionality to existing distributed applications. You can then use them to manage orchestration and pipelines on Kubernetes, treating it as the layer on which you build your code.
We’re still very much taking an artisanal approach to cloud native application development, and having a codified set of design patterns like Burns’s can only make it easier to train new developers and to build and manage code at scale.