Kubernetes has very ambitious goals. It aims to manage and simplify the orchestration, deployment, and management of distributed systems across a wide range of environments and cloud providers. It provides many capabilities and services that should work across all that diversity while evolving and remaining simple enough for mere mortals to use. This is a tall order. Kubernetes achieves this by following a crystal-clear, high-level design and well-thought-out architecture that promotes extensibility and pluggability. Many parts of Kubernetes are still hardcoded or environment-aware, but the trend is to refactor them into plugins and keep the core small, generic, and abstract. In this section, we will peel Kubernetes like an onion, starting with various distributed system design patterns and how Kubernetes supports them, then go over the surface of Kubernetes, which is its set of APIs, and then take a look at the actual components that comprise Kubernetes.
Distributed system design patterns
All happy (working) distributed systems are alike, to paraphrase Tolstoy in Anna Karenina. That means that to function properly, all well-designed distributed systems must follow some best practices and principles. Kubernetes doesn’t want to be just a management system; it wants to support and enable these best practices and provide high-level services to developers and administrators. Let’s look at some of those best practices, described as design patterns.
The sidecar pattern
The sidecar pattern is about co-locating another container in a pod in addition to the main application container. The application container is unaware of the sidecar container and just goes about its business. A great example is a central logging agent. Your main container can just log to stdout, but the sidecar container will send all logs to a central logging service where they will be aggregated with the logs from the entire system. The benefits of using a sidecar container versus adding central logging to the main application container are enormous. First, applications are not burdened anymore with central logging, which could be a nuisance. If you want to upgrade or change your central logging policy or switch to a totally new provider, you just need to update the sidecar container and deploy it. None of your application containers change, so you can’t break them by accident. The Istio service mesh uses the sidecar pattern to inject its proxies into each pod.
The ambassador pattern
The ambassador pattern is about representing a remote service as if it were local and possibly enforcing some policy. A good example of the ambassador pattern is if you have a Redis cluster with one master for writes and many replicas for reads. A local ambassador container can serve as a proxy and expose Redis to the main application container on the localhost. The main application container simply connects to Redis on localhost:6379 (Redis default port), but it connects to the ambassador running in the same pod, which filters the requests, and sends write requests to the real Redis master and read requests randomly to one of the read replicas. Just like with the sidecar pattern, the main application has no idea what’s going on. That can help a lot when testing against a real local Redis. Also, if the Redis cluster configuration changes, only the ambassador needs to be modified; the main application remains blissfully unaware.
The adapter pattern
The adapter pattern is about standardizing output from the main application container. Consider the case of a service that is being rolled out incrementally: it may generate reports in a format that doesn’t conform to the previous version. Other services and applications that consume that output haven’t been upgraded yet. An adapter container can be deployed in the same pod with the new application container and massage the output to match the old version until all consumers have been upgraded. The adapter container shares the filesystem with the main application container, so it can watch the local filesystem, and whenever the new application writes something, it immediately adapts it
Single-node patterns are all supported directly by Kubernetes via pods. Multi-node patterns such as leader election, work queues, and scatter-gather are not supported directly, but composing pods with standard interfaces to accomplish them is a viable approach with Kubernetes. Many tools, frameworks, and add-ons that integrate deeply with Kubernetes utilize these design patterns. The beauty of these patterns is that they are all loosely coupled and don’t require Kubernetes to be modified or even be aware of the presence of these integrations. The vibrant ecosystem around Kubernetes is a direct result of its architecture.