All writing
Updated 4 min readNaren

Multi-container Pods: sidecars, ambassadors, adapters and init containers

When to put more than one container in a Pod, the three patterns for doing it, and init containers that run before your application starts.

The last post kept applications healthy with liveness, readiness and startup probes. This one looks at running several tightly coupled containers together in a single Pod.

Multi-container Pods enable a few well-known patterns: sidecar, ambassador and adapter. They fit applications whose parts need to work closely together, sharing resources and a lifecycle, while staying modular.

Why put several containers in one Pod

Microservices usually mean one container per Pod. Some situations are better served by putting several containers in the same Pod.

All the containers in a Pod share one lifecycle. They are created, started and destroyed together, and if the Pod dies, all of its containers die with it.

They share a network namespace, meaning the same IP address and port space, so they can talk to each other over localhost, which is fast and simple.

And they can share data through volumes that are mounted into each container's filesystem.

This tight coupling makes multi-container Pods a good way to extend a main application container without changing its code.

Multi-container patterns

There are three main patterns, each for a different need.

Sidecar: extending the application

In the sidecar pattern, a second container runs alongside the main application container and provides extra functionality or support. The sidecar handles operational tasks, so the main application can stick to its business logic.

Pod shares resources with extends/assists Main Application Sidecar Container

A sidecar separates supporting work, such as logging, monitoring and security, from the main application, which keeps the application simpler. You can write a sidecar once and reuse it with different applications. And sidecars are a common way to collect logs, metrics and traces.

Typical sidecars:

  • A logging agent, such as Fluentd or Logstash, collects the logs the main application writes and ships them to a central logging system.
  • A monitoring agent, such as a Prometheus exporter, collects metrics from the main application and sends them to a monitoring system.
  • A configuration sidecar fetches and updates the main application's configuration from a configuration service.
  • A file synchronizer copies files from a Git repository into a shared volume that the main application uses, as in Git push-to-deploy.

Here, an Nginx web server writes its logs to a shared emptyDir volume, and a Fluentd sidecar reads and processes them:

apiVersion: v1
kind: Pod
metadata:
  name: webserver-logging-pod
spec:
  volumes:
    - name: varlog
      emptyDir: {} # Shared volume for logs
  containers:
    - name: webserver
      image: nginx:latest
      ports:
        - containerPort: 80
      volumeMounts:
        - name: varlog
          mountPath: /var/log/nginx # Main container writes logs here
    - name: logging-agent
      image: fluent/fluentd:v1.17.0-1.0
      env:
        - name: FLUENTD_ARGS
          value: "--no-supervisor -q"
      volumeMounts:
        - name: varlog
          mountPath: /var/log/nginx # Sidecar reads logs from here

Ambassador: a proxy to the outside

In the ambassador pattern, a specialized sidecar acts as a proxy. It handles network requests on behalf of the application container, hiding the details of external services or giving the application one consistent way to reach them.

Pod proxies request to talks to localhost External Service Main Application Ambassador Container

The main application no longer deals with its external dependencies directly, or with the service discovery, load balancing and retries that come with them. It gets one consistent interface to external services. And the ambassador can add security, such as TLS encryption or mutual TLS authentication.

Typical ambassadors:

  • A database proxy manages connections to an external database, handling connection pooling, retries and service discovery.
  • An API proxy sends requests to external APIs and handles authentication or protocol requirements.
  • An Nginx ambassador handles SSL/TLS requests and passes the secure traffic on to the application.

Say your application needs a database. Instead of the application managing the connection itself, an ambassador sidecar can do it:

apiVersion: v1
kind: Pod
metadata:
  name: app-db-ambassador-pod
spec:
  containers:
    - name: my-application
      image: my-app-image:latest
      env:
        - name: DB_HOST
          value: "localhost" # Application connects to localhost
        - name: DB_PORT
          value: "5432"
    - name: db-ambassador
      image: postgres:latest # Or a custom proxy image
      command: ["/bin/sh", "-c", "echo 'Proxying DB connection to external-db.example.com' && sleep 3600"]
      ports:
        - containerPort: 5432 # Ambassador listens on this port

In this simplified example, my-application connects to localhost:5432, and the db-ambassador container takes that traffic and forwards it to the real database outside the cluster.

Adapter: standardizing output

In the adapter pattern, a container standardizes or normalizes the main application's output, or translates data formats and protocols between services that are otherwise incompatible.

Pod transforms output produces non-standard output Standardized Output External System Main Application Adapter Container

An adapter lets components with incompatible interfaces work together. It converts data into the format another system needs, such as a monitoring system. And it changes what the application produces without touching the application's code.

Typical adapters:

  • A monitoring adapter turns monitoring data from different applications, each in its own format, into a single format for one monitoring system.
  • An API adapter reshapes an application's API output for a particular client, or converts between protocols.

Here, the main application writes metrics in a custom format, and an adapter converts them into a format Prometheus understands:

apiVersion: v1
kind: Pod
metadata:
  name: app-metrics-adapter-pod
spec:
  containers:
    - name: my-application
      image: my-app-image:latest
      # Application outputs custom metrics to a file or endpoint
    - name: metrics-adapter
      image: custom-metrics-adapter:latest # Image that converts metrics
      volumeMounts:
        - name: metrics-data
          mountPath: /var/metrics
  volumes:
    - name: metrics-data
      emptyDir: {} # Shared volume for metrics data

Init containers: work before the application starts

Besides the main containers list, a Pod can have init containers. These run to completion before any of the Pod's application containers start.

Completes Successfully Fails Pod Starts Init Container Runs Main Container Starts Pod Fails to Start

Init containers run in the order you define them, and each one must complete successfully before the next one starts. They can share volumes with the application containers.

Use them for setup work, such as waiting for a database to be ready, cloning a Git repository or generating configuration files. They can also handle security tasks before the application starts, such as running a security scan or injecting secrets.

apiVersion: v1
kind: Pod
metadata:
  name: my-app-with-init-container
spec:
  initContainers:
  - name: init-myservice
    image: busybox:1.36
    command: ['sh', '-c', 'echo "Waiting for external service to be ready..."; sleep 5']
  containers:
  - name: my-application
    image: nginx:latest
    ports:
    - containerPort: 80

Here init-myservice runs for 5 seconds, standing in for a wait on an external service. Only after it completes does my-application start.

One Pod or several

Whether to use a multi-container Pod or separate single-container Pods is a design decision.

Use a multi-container Pod when the containers are tightly coupled and share a lifecycle, when they talk to each other often and need the speed of localhost, when they need to share resources such as storage volumes, or when one container extends the main container without changing its logic.

Use separate Pods when the containers have independent lifecycles and can scale on their own, when they don't need to share a network namespace or storage, when the services are distinct enough to work without being co-located, or when different parts of your application need to scale at different rates.

The next post starts on security, with how to harden a Kubernetes cluster: CIS benchmarks and API access.