This article is about building a Kubernetes controller to mutate pods automatically based on specific annotations or labels and inject one or more sidecar containers into them.

What are Sidecar Containers?

Sidecar containers are the containers that need to be run along with the primary container — the application container — and share the same resources with it, like the network and the storage interfaces, to enhance and extend the functionality of the main container without modifying its core set of tasks.

Sidecar Pattern
Sidecar Pattern

Kubernetes Admission Controller

Starting in Kubernetes v1.7, alpha support for external admission controllers is introduced; It provides two options for adding custom business logic to the API server for modifying objects as they are created and validating policy.

First, let’s have a look at the admission controller definition in the official docs:

An admission controller is a piece of code that intercepts requests to the Kubernetes API server before the persistence of the object but after the request is authenticated and authorised.

MutatingAdmissionWebhook and ValidatingAdmissionWebhook are special controllers that execute the mutating and validating logic by calling a webhook API.

Admission Controller Phases
Admission Controller Phases

There are tons of use cases where both admission webhook/s can be helpful.

  • Implementing an image scanning component to detect vulnerabilities and misconfigurations in deployments.
  • Enforcing an annotation or a label on a resource to be admitted.
  • Injecting a sidecar proxy into pods that mediates inbound and outbound communication to it, the same as Istio does.
  • And many, many others…

This article is focused on the MutatingAdmissionWebhook to build the sidecar injection controller, and for the sake of simplicity, I will use a busybox-curl as the sidecar container.

Prerequisites

First, verify that admissionregistration.k8s.io/v1 API is enabled on your Kubernetes cluster using this command:

kubectl api-versions | grep admissionregistration.k8s.io/v1

The output should be:

admissionregistration.k8s.io/v1

Then, verify that MutatingAdmissionWebhook is enabled by checking the --enable-admission-plugins flag using this command:

kube-apiserver -h | grep enable-admission-plugins

The output should be:

# Output is snipped, but you should find the MutatingAdmissionWebhook on the list there
CertificateApproval, CertificateSigning, ..., MutatingAdmissionWebhook, ...

Implementation

The following code snippets have been edited to fit the article format better. The complete code is available on the GitHub repo.

We need to run an HTTP API server to handle webhook API requests from the Kubernetes API server and mutate the pod containers accordingly.

Let’s first understand the request and response structure that Kubernetes uses for its admission webhook API requests.

Webhook Request

Webhooks are sent as POST requests with an AdmissionReview object serialised to JSON as the body.

In the AdmissionReview object, the request key with the type AdmissionRequest contains all details for the admission request.

An example of an AdmissionReview request body containing the AdmissionRequest object - snipped JSON:

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    "uid": "075a1336-0165-41e0-b0ac-8705883f1c41",
    "dryRun": false,
    "namespace": "default",
    "...": "..."
    "object": {
      "apiVersion": "v1",
      "kind": "Pod",
      "...": "..."
    }
  }
}

Webhook Response

Webhook API should respond with the proper HTTP status code — 2xx in case of success or non-2xx in case of failure — and a body containing an AdmissionReview object containing the mutation changes as a base64-encoded array of JSON patch operations.

In the AdmissionReview object, the response key with the type AdmissionResponse should contain all details for the admission response.

An example of an AdmissionReview response body containing the AdmissionResponse object - snipped JSON:

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "075a1336-0165-41e0-b0ac-8705883f1c41",
    "allowed": true,
    "patch": "W3sib3AiOiJhZGQiLCJwYXRoIjoiL3NwZWMvY29udG...",
    "patchType": "JSONPatch"
  }
}

You can check the JSON patch documentation for more details about how it can be used to describe changes to JSON objects.

Let’s implement the sidecar container injection logic…

Mutation Function

It is a simple function that does the following:

  • Adds a busybox-curl container to the Pod’s containers array
  • Generates the JSON patch change operations array using the fast-json-patch NPM package.
  • Stringify the resulting JSON patch array and encode it to base64 string.

An example of the injection mutation function - snipped code:

import * as jsonpatch from 'fast-json-patch';

const mutate = (admissionReviewRequest: V1AdmissionRequest<V1Pod>): V1AdmissionResponse => {
  const admissionReviewResponse: V1AdmissionResponse = {
    allowed: true,
    uid: admissionReviewRequest.uid,
  };

  // get the pod object and clone it
  const originalPod = admissionReviewRequest.object as V1Pod;
  const mutatedPod = JSON.parse(JSON.stringify(originalPod)) as V1Pod;

  // update the mutated pod spec with the new containers array
  const mutatedPodContainers = injectContainer(originalPod.spec?.containers);
  mutatedPod.spec = { ...mutatedPod.spec, containers: mutatedPodContainers };

  // generate json patch string
  const patchArray = jsonpatch.compare(originalPod, mutatedPod);
  const patchArrayJsonStr = JSON.stringify(patchArray);

  admissionReviewResponse.patchType = "JSONPatch";
  admissionReviewResponse.patch = Buffer.from(patchArrayJsonStr).toString('base64');

  return admissionReviewResponse;
}

const injectContainer = (containers: V1Container[] = []): V1Container[] => {
  const sidecarContainer: V1Container = {
    name: 'curl',
    image: 'yauritux/busybox-curl:latest'
  };

  return [...containers, sidecarContainer];
};

Okay, but how can we deploy it?

Deployment

Our admission webhook is an HTTP server that runs in the cluster, so it is a regular Kubernetes Deployment.

An example of Kubernetes Deployment that runs the admission webhook server - snipped YAML:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kubernetes-sidecar-injector
  labels:
    app.kubernetes.io/instance: kubernetes-sidecar-injector
spec:
  selector:
    matchLabels:
      app.kubernetes.io/instance: kubernetes-sidecar-injector
  template:
    metadata:
      labels:
        app.kubernetes.io/instance: kubernetes-sidecar-injector
    spec:
      containers:
        - name: kubernetes-sidecar-injector
          image: "mohllal/kubernetes-sidecar-injector:latest"
          env:
            - name: TLS_CERT_FILE
              value: "/var/run/secrets/certs/tls-cert-file"
            - name: TLS_PRIVATE_KEY_FILE
              value: "/var/run/secrets/certs/tls-private-key-file"
          volumeMounts:
          - name: admission-controller-cert
            mountPath: "/var/run/secrets/certs"
            readOnly: true
      volumes:
      - name: admission-controller-cert
        secret:
          secretName: kubernetes-sidecar-injector

And to make the Deployment’s pods accessible by the Kubernetes API server, we need a Kubernetes Service.

An example of Kubernetes Service for the admission webhook server - snipped YAML:

apiVersion: v1
kind: Service
metadata:
  name: kubernetes-sidecar-injector
  labels:
    app.kubernetes.io/instance: kubernetes-sidecar-injector
spec:
  type: ClusterIP
  ports:
    - port: 443
      targetPort: 8443
      protocol: TCP
      name: https
  selector:
    app.kubernetes.io/instance: kubernetes-sidecar-injector

Admission webhooks are served via HTTPS, so we need proper certificates for the server. These certificates can be self-signed (signed by a self-signed CA), but we need Kubernetes to instruct the respective CA certificate when talking to the webhook server.

In addition, the common name (CN) of the certificate must match the Kubernetes Service name used by the Kubernetes API server, which for internal services is <service-name>.<namespace>.svc.

Here is an example of a Kubernetes Secret that holds the TLS certificate cert and private key.

apiVersion: v1
kind: Secret
metadata:
  name: kubernetes-sidecar-injector
type: Opaque
data:
  tls-cert-file: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JS...
  tls-private-key-file: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0t...

More about generating the certificate comes later in the demo section below. Still, if you don’t want to use Helm, you can generate a self-signed certificate using the openssl CLI tool and put it manually inside a Kubernetes Secret.

Finally, the Kubernetes MutatingWebhookConfiguration describes the admission webhook configuration and which objects are subject to the admission webhook server.

An example of Kubernetes MutatingWebhookConfiguration for the admission controller webhook server - snipped YAML:

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: kubernetes-sidecar-injector
webhooks:
- name: kubernetes-sidecar-injector.default.svc
  admissionReviewVersions:
    - v1
  sideEffects: "NoneOnDryRun"
  reinvocationPolicy: "Never"
  timeoutSeconds: 10
  objectSelector:
    matchExpressions:
    - key: sidecar.me/inject
      operator: In
      values:
      - "True"
      - "true"
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods
    scope: '*'
  clientConfig:
    service:
      namespace: default
      name: kubernetes-sidecar-injector
      path: "/mutation/pod"
    caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0t...

In the objectSelector and rules sections, we enable the mutation webhook only on pod objects with the sidecar.me/inject: True in their labels when a Pod is being created.

In the clientConfig section, we define the webhook server hostname, our kubernetes-sidecar-injector service running in the default namespace, and configure the webhook API request to be landed on the /mutation/pod path.

The caBundle refers to the PEM-encoded CA bundle generated earlier that the Kubernetes API Server as a client can use to validate the server certificate.

You can check the official documentation for more details about the different configurations that can be used for the admission webhook.

Now, it is demo time…

Demo

To make things easier, I used Helm charts to package two applications:

I used both genSignedCert and genCA helm functions to generate an x509 certificate with both Subject Common Name (CN) and SubjectAltName (SAN) set to the service fully qualified hostname.

Let’s install both charts…

# 1. install the kubernetes-sidecar-injector chart
helm install kubernetes-sidecar-injector charts/kubernetes-sidecar-injector/ \
--values charts/kubernetes-sidecar-injector/values.yaml \
--namespace default

# 2. install the httpbin chart
helm install httpbin charts/httpbin/ \
--values charts/httpbin/values.yaml \
--namespace default

Listing all containers in the httpbin Deployment’s Pod, you can notice that a new container is running in it named curl.

# 1. export the pod name
export POD_NAME=$(kubectl get pods \
--namespace default \
-l "app.kubernetes.io/name=httpbin,app.kubernetes.io/instance=httpbin" \
-o jsonpath="{.items[0].metadata.name}")

# 2. list all containers running inside the pod
kubectl get pods $POD_NAME \
--namespace default \
-o jsonpath='{.spec.containers[*].name}'

Accessing the httpbin HTTP server from inside the curl container.

# 1. export the pod name
export POD_NAME=$(kubectl get pods \
--namespace default \
-l "app.kubernetes.io/name=httpbin,app.kubernetes.io/instance=httpbin" \
-o jsonpath="{.items[0].metadata.name}")

# 2. curl from the sidecar container
kubectl exec $POD_NAME \
--namespace default \
-c curl \
-- curl http://localhost/anything

Woohoo! the pod has been injected with an extra sidecar container that shares the same network interface with the primary container.

Conclusion

Admission controllers are essential when it comes to extending the Kubernetes functionality with domain logic since they can mutate or reject requests to the Kubernetes API server before the object is persisted.

Defining a custom admission system through HTTP-enabled webhooks is easy to implement in any programming language and opens the door to many possible use cases.

Further reading