Kubernetes Reusable Elements for Designing Cloud Native Applications: Structural Patterns
Structural patterns are related to organizing containers within a Kubernetes pod. Having good cloud-native containers is the first step, but not enough. Reusing containers and combining them into Pods to achieve the desired outcome is the next step.
The primary goal of the structural pattern is to facilitate comprehension of how to arrange containers within a pod to address various scenarios.
At the heart of this pattern is the principle of single responsibility, which stipulates that each container should be dedicated to a single task. If a container needs to perform more than one task, a separate container should be deployed for each additional task.
This approach enhances the modularity and scalability of your application. By ensuring that each container performs only one task, you can easily update or scale individual components without affecting others. This also improves fault isolation, as issues in one container don’t directly impact others. Moreover, it simplifies the process of building, testing, and deploying each component of your application.
This pattern is particularly useful in microservices architectures where each service can be encapsulated in its own container.
Init Container
The Init Container pattern in Kubernetes is a way to separate initialization tasks from the main application containers, similar to constructors in programming languages.
Some key points:
- Init Containers: Part of the Pod definition and run before the application containers. They handle prerequisites such as setting up permissions, database schemas, or installing seed data. They run in sequence and must all terminate successfully before the application containers start. If an init container fails, the whole Pod is restarted
- Application Containers: Run in parallel after all init containers have completed. The startup order is arbitrary
- Separation of Concerns: Init containers allow for a clear separation of initialization activities from the main application duties. This enables you to keep containers single-purposed, with application containers focusing on application logic and init containers focusing on configuration and initialization tasks
- Resource Handling: Init containers affect how Pod resource requirements are calculated for scheduling, autoscaling, and quota management. The effective Pod-level request and limit values are the highest of either the highest init container request/limit value or the sum of all application container values for request/limit
- Lifecycle Differences: Init containers have slightly different lifecycle, health-checking, and resource-handling semantics compared to application containers. For example, there is no
livenessProbe
,readinessProbe
, orstartupProbe
for init containers - Usage: Init containers are used by developers deploying on Kubernetes, while admission webhooks help administrators and various frameworks control and alter the container initialization process
This pattern ensures successful completion of each initialization step before moving to the next, allowing for the creation of containers focused on either initialization or application tasks, which can be reused in different contexts within Pods with predictable guarantees.
Sidecar
Containers are a popular technology for unified application development and deployment, but there’s a need to extend their functionality and enable collaboration among them.
The Sidecar pattern uses the Pod primitive to combine multiple containers into a single unit. Containers in a Pod share volumes and communicate over the local network or host IPC.
In the context of containerization, an analogy is drawn with object-oriented programming (OOP). Container images are likened to classes, and containers to objects in OOP.
Extending a container is compared to inheritance in OOP, signifying an “is-a” relationship and resulting in tighter coupling between containers. Having multiple containers collaborate in a Pod is akin to composition in OOP, representing a “has-a” relationship. This approach is more flexible as it doesn’t couple containers at build time.
Sidecars can be transparent or explicit. An example of a transparent sidecar is Envoy proxy, which provides features like TLS, load balancing, automatic retries by intercepting all traffic to the main container. Dapr is an example of an explicit proxy that offers features like reliable service invocation, publish-subscribe, bindings to external systems over HTTP and gRPC APIs.
The choice between using a separate process (sidecar) or merging it into the main container depends on factors including resource consumption and the specific requirements of your application.
Adapter
The Adapter pattern is a design strategy in containerized systems that provides a unified interface for different components. It’s a specialization of the Sidecar pattern, aimed at adapting access to the application. This pattern is particularly useful in distributed systems with components using different technologies, as it hides system complexity and offers unified access.
A practical example of the Adapter pattern is in monitoring a distributed system with multiple services. Services written in different languages might not expose metrics in the same format expected by the monitoring tool. The Adapter pattern addresses this by exporting metrics from various application containers into one standard format and protocol. This is done through an adapter container that translates locally stored metrics information into an external format that the monitoring server understands.
In essence, the Adapter acts as a reverse proxy to a heterogeneous system, hiding its complexity behind a unified interface. Using a distinct name separate from the generic Sidecar pattern allows for clearer communication of this pattern’s purpose.
Ambassador
The Ambassador pattern is a design pattern used in containerized services to manage the complexities of accessing external services. It acts as a proxy, decoupling the main container from directly accessing these services.
This pattern is particularly useful for legacy applications that are challenging to modify with modern networking concepts like monitoring, logging, routing, and resiliency patterns.
The Ambassador pattern abstracts away complexities such as dynamic addresses, load balancing needs, unreliable protocols, and complex data formats.
An ambassador container can manage tasks like connecting to different shards of a cache in a production environment, performing client-side service discovery, or handling non-reliable protocols with circuit-breaker logic.
The Ambassador pattern is a type of Sidecar pattern but acts as a smart proxy to the outside world instead of enhancing the main application with additional capabilities.
The benefits of the Ambassador pattern are similar to the Sidecar pattern as both keep containers single-purpose and reusable. This approach also enables the creation of specialized and reusable ambassador containers that can be combined with other application containers.
Conclusion
Kubernetes structural patterns provide a way to organize and manage containerized applications in a scalable, flexible, and resilient manner. Each pattern has its own benefits and trade-offs, and choosing the right pattern depends on the specific needs and requirements of the application. By understanding and implementing these patterns effectively, developers and operators can build and maintain high-performing, scalable, and resilient Kubernetes applications.