Understanding Kubernetes Multi-Container Pod Patterns and Init- Containers

Vishugoyal
6 min readJan 12, 2022

Multi-container pods are extremely useful for specific purposes in Kubernetes. While it’s not always necessary to combine multiple containers into a single pod, knowing the right patterns to adopt creates more robust Kubernetes deployments.

Pods will often only have one container — this is OK. One of the downsides of having a pod as an extra layer of abstraction around a container is that it seems like you should have multiple containers for a pod to be useful.

Another reason to combine containers into a single pod is fro simpler communication between containers in the pod. These containers can communicate through shared volumes (writing to a shared file or directory) and through inter-process communication (semaphores or shared memory).

There are three common design patterns and use-cases for combining multiple containers into a single pod. We’ll walk through the sidecar pattern, the adapter pattern, and the ambassador pattern. Look to the end of the post for example YAML files for each of these.

ypes of design patterns

Sidecar:

A sidecar is just a container that runs on the same Pod as the application container, because it shares the same volume and network as the main container, it can “help” or enhance how the application operates. Common examples of sidecar containers are log shippers, log watchers, monitoring agents among others.

For the demo, let’s look at a sidecar pattern with an application generating logs at a particular file path, where the sidecar pushes the records to the nginx HTML directory for users to view.

apiVersion: v1
kind: Pod
metadata:
name: sidecar-pod
labels:
app: sidecar-app
spec:
volumes:
- name: logs
emptyDir: {}
containers:
- name: app-container
image: alpine
command: ["/bin/sh"]
args: ["-c", "while true; do date >> /var/log/index.html; sleep 2;done"]
volumeMounts:
- name: logs
mountPath: /var/log
- name: log-exporter-sidecar
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: logs
mountPath: /usr/share/nginx/htmlkubectl create -f sidecar.yml

Side Car Design Pattern

Adapter:

The Adapter container pattern is a specialization of sidecar containers. Adapter containers run in parallel with the main container. … You can use this pattern where your application or main containers need to standardize some format for the external systems.

For our hands-on exercise, let’s consider that an application that outputs logs in a particular format, that we want to change to something standard.

apiVersion: v1
kind: Pod
metadata:
name: adapter-pod
labels:
app: adapter-app
spec:
volumes:
- name: logs
emptyDir: {}
containers:
- name: app-container
image: alpine
command: ["/bin/sh"]
args: ["-c", "while true; do date >> /var/log/app.log; sleep 2;done"]
volumeMounts:
- name: logs
mountPath: /var/log
- name: log-adapter
image: alpine
command: ["/bin/sh"]
args: ["-c", "tail -f /var/log/app.log|sed -e 's/^/Date /' > /var/log/out.log"]
volumeMounts:
- name: logs
mountPath: /var/log

In the manifest, we have an app-container that outputs a stream of dates to a log file. The log-adapter container adds a Date prefix in front. Yes, it’s a very rudimentary example, but enough to get how the adapter works. So, when we cat the log file /var/log/out.log for the log-adapter, we see a Date prefixed to the output that was not in the original log /var/log/app.log. You can then use a sidecar to export these logs to your monitoring and alerting engine.

Adapter Design Pattern

Ambassador:

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.

For the demo, we will use a simple NGINX config that acts as a TCP proxy to example.com. That should also work for databases and other back ends.

apiVersion: v1
kind: Pod
metadata:
name: ambassador-pod
labels:
app: ambassador-app
spec:
volumes:
- name: shared
emptyDir: {}
containers:
- name: app-container-poller
image: yauritux/busybox-curl
command: ["/bin/sh"]
args: ["-c", "while true; do curl 127.0.0.1:81 > /usr/share/nginx/html/index.html; sleep 10; done"]
volumeMounts:
- name: shared
mountPath: /usr/share/nginx/html
- name: app-container-server
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: shared
mountPath: /usr/share/nginx/html
- name: ambassador-container
image: bharamicrosystems/nginx-forward-proxy
ports:
- containerPort: 81

If you look carefully in the manifest YAML, you will find there are three containers. The app-container-poller continuously calls http://localhost:81 and sends the content to /usr/share/nginx/html/index.html.

The app-container-server runs nginx and listens on port 80 to handle external requests. Both these containers share a common mountPath. That is similar to the sidecar approach.

There is an ambassador-container running within the pod that listens on localhost:81 and proxies the connection to example.com, so when we curl the app-container-server endpoint on port 80, we get a response from example.com.

Ambassador Design Pattern

InitContainers:

Init Containers

A Pod can have multiple containers running apps within it, but it can also have one or more init containers, which are run before the app containers are started.

Init containers are exactly like regular containers, except:

  • Init containers always run to completion.
  • Each init container must complete successfully before the next one starts.

Differences from regular containers

Init containers support all the fields and features of app containers, including resource limits, volumes, and security settings. However, the resource requests and limits for an init container are handled differently, as documented in Resources.

Also, init containers do not support lifecycle, livenessProbe, readinessProbe, or startupProbe because they must run to completion before the Pod can be ready.

If you specify multiple init containers for a Pod, kubelet runs each init container sequentially. Each init container must succeed before the next can run. When all of the init containers have run to completion, kubelet initializes the application containers for the Pod and runs them as usual.

Using init containers

Because init containers have separate images from app containers, they have some advantages for start-up related code:

  • Init containers can contain utilities or custom code for setup that are not present in an app image. For example, there is no need to make an image FROM another image just to use a tool like sed, awk, python, or dig during setup.
  • The application image builder and deployer roles can work independently without the need to jointly build a single app image.
  • Init containers can run with a different view of the filesystem than app containers in the same Pod. Consequently, they can be given access to Secrets that app containers cannot access.
  • Because init containers run to completion before any app containers start, init containers offer a mechanism to block or delay app container startup until a set of preconditions are met. Once preconditions are met, all of the app containers in a Pod can start in parallel.
  • Init containers can securely run utilities or custom code that would otherwise make an app container image less secure. By keeping unnecessary tools separate you can limit the attack surface of your app container image.

For our hands-on exercise, let’s consider the below code and it will be give you an idea over init containers.

Init Containers

Hope You had find the article interesting. Hit a like 🔥👍🏻

--

--

Vishugoyal

I am pursiing B-tech. and also very fond of learning new technology under the guidance of Vimal Daga sir (World Record Holder).