Kubernetes Patterns: The Ambassador Pattern
Want to learn about various Kubernetes patterns? Check out our blog post regarding the ambassador pattern.
This is the third article in the Sidecar Pattern variation articles. As we previously discussed, a sidecar container is just another container that lives with the application container on the same Kubernetes Pod. Sharing the same Pod entails sharing the same volume and network resources. Both containers are deployed as one unit. Hence, we can use the sidecar container for modifying how the application container works without having to make any changes to the code. In the Sidecar Pattern, we used the sidecar container as a log shipper. In the Adapter Pattern, we used the sidecar container to expose the main application’s health metrics in a unified way for other services to consume (for example, Prometheus).
In this article, we will be discussing the Ambassador Pattern.
What is an Ambassador Container?
An Ambassador container is a sidecar container that is in charge of proxying connections from the application container to other services. However, while the Adapter container acts as a reverse proxy, the Ambassador container acts as a client proxy. You might be wondering, why do we need to proxy the application connection requests? Because we need to follow the separation of concerns principle. Each container should do its task and do it well. If there are other tasks that require the application’s function in order to work correctly, we may hand those tasks to the sidecar container.
For example, almost all applications need a database connection at some phase. In a multi-environment place, there would be a test database, a staging database, and a production database. When writing the Pod definition for their application’s container, developers must pay attention to which database they’ll be connecting to. A database connection string can be easily changed through an environment variable or a configMap. We could also use a sidecar pattern that proxies DB connections to the appropriate server depending on where it runs. Developers needn’t change the connection string, they could leave the DB server at localhost as usual. When deployed to a different environment, the Ambassador container detects which environment it is running on (possibly through the Reflection pattern), and connects to the correct server.
Another well-known use case for the Ambassador container is when your application needs to connect to a caching server like Memcached or Redis. Let’s have a Redis example scenario to demonstrate this pattern.
Scenario: Connecting to Multiple Redis Servers
Let’s assume that your application needs to connect to a caching server like Redis. In the development environment, Redis is installed on your laptop and listening on the localhost. In other environments, you have more than one Redis instance for high availability. You need to configure your application and select a healthy node then connect to it. One possible solution to this case is to use an Ambassador container that proxies connections to the backend Redis servers. The Ambassador container listens on the localhost as it is sharing the same Pod as the application container. In our example, we use Twemproxy to handle connecting to the Redis instances. The scenario can be depicted in the following illustration:
Our definition file may look as follows:
apiVersion: v1 kind: Pod metadata: name: ambassador-example spec: containers: - name: redis-client image: redis - name: ambassador image: malexer/twemproxy env: - name: REDIS_SERVERS value: redis-st-0.redis-svc.default.svc.cluster.local:6379:1 redis-st-1.redis-svc.default.svc.cluster.local:6379:1 ports: - containerPort: 6380
The definition defines two Pods:
- The application Pod: It is the first pod in the list that runs Redis image, but we are using it as a client to other Redis servers.
- The ambassador Pod: It is the second container that runs the Twemproxy as we discussed. We used the malexer/twemproxy image, which needs the Redis instances passed as environment variables (lines 11-13) in this form address:port:weight.
- The container by default listens on port 6380 (for advanced configurations, please visit the image’s project page at malexer/twemproxy).
Therefore, the workflow can be summarized as follows:
The application (simulated by the Redis client) initiates a request to localhost:6380. As far as the application is concerned, this is the Redis server.
The Ambassador container picks up the request and relays it to the Redis servers defined in its configuration.
The only missing thing here is the two Redis instances that the Ambassador container is configured to communicate with. The following StatefulSet definition creates them:
apiVersion: v1 kind: Service metadata: name: redis-svc labels: app: redis spec: ports: - port: 6379 name: redis-port clusterIP: None selector: app: redis --- apiVersion: apps/v1 kind: StatefulSet metadata: name: redis-st spec: serviceName: "redis-svc" replicas: 2 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis ports: - containerPort: 6379 name: redis volumeMounts: - name: redis-data mountPath: /data volumeClaimTemplates: - metadata: name: redis-data spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi
For an in-depth discussion of how StatefulSets work, you can refer to our StatefulSet 101 article.
- Among the best practices of designing, containerized applications are segregated responsibilities among containers. Ideally, each container should be tasked with the one job that it does best.
- If there are any additional tasks, they should be handed to other containers. If the task is closely related to the first container, a sidecar container can be used. A sidecar container lives with the application container on the same Pod, and thus, shares the same resources with the application container.
- The Ambassador container is also a form of sidecar container. It is used as a proxy between the application container and one or more outside services. The Ambassador container forms an abstraction layer so that the application developer can focus on the application itself without worrying about the infrastructure connectivity details.