In today’s post, we will write a simple operator with Kopf, which is a Python framework created by Zalando. A Kubernetes operator is a piece of software, running in Kubernetes, that does something application specific. To see some examples of what operators are used for, check out operatorhub.io.
Our operator will do something simple in order to easily grasp how it works:
- the operator will create a deployment that runs nginx
- nginx will serve a static website based on a git repository that you specify; we will use an init container to grab the website from git and store it in a volume
- you can control the number of instances via a replicas parameter
That’s great but how will the operator know when it has to do something, like creating or updating resources? We will use custom resources for that. Read on to learn more…
Note: source files are on GitHub
Custom Resource Definition (CRD)
Kubernetes allows you to define your own resources. We will create a resource of type (kind) DemoWeb. The CRD is created with the YAML below:
# A simple CRD to deploy a demo website from a git repo apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: demowebs.baeke.info spec: scope: Namespaced group: baeke.info versions: - name: v1 served: true storage: true names: kind: DemoWeb plural: demowebs singular: demoweb shortNames: - dweb additionalPrinterColumns: - name: Replicas type: string priority: 0 JSONPath: .spec.replicas description: Amount of replicas - name: GitRepo type: string priority: 0 JSONPath: .spec.gitrepo description: Git repository with web content
For more information (and there is a lot) about CRDs, see the documentation.
Once you create the above resource with kubectl apply (or create), you can create a custom resource based on the definition:
apiVersion: baeke.info/v1 kind: DemoWeb metadata: name: demoweb1 spec: replicas: 2 gitrepo: "https://github.com/gbaeke/static-web.git"
Note that we specified our own API and version in the CRD (baeke.info/v1) and that we set the kind to DemoWeb. In the additionalPrinterColumns, we defined some properties that can be set in the spec that will also be printed on screen. When you list resources of kind DemoWeb, you will the see replicas and gitrepo columns:

Of course, creating the CRD and the custom resources is not enough. To actually create the nginx deployment when the custom resource is created, we need to write and run the operator.
Writing the operator
I wrote the operator on a Mac with Python 3.7.6 (64-bit). On Windows, for best results, make sure you use Miniconda instead of Python from the Windows Store. First install Kopf and the Kubernetes package:
pip3 install kopf kubernetes
Verify you can run kopf:

Let’s write the operator. You can find it in full here. Here’s the first part:

Naturally, we import kopf and other necessary packages. As noted before, kopf and kubernetes will have to be installed with pip. Next, we define a handler that runs whenever a resource of our custom type is spotted by the operator (with the @kopf.on.create decorator). The handler has two parameters:
- spec object: allows us to retrieve our custom properties with spec.get (e.g. spec.get(‘replicas’, 1) – the second parameter is the default value)
- **kwargs: a dictionary with lots of extra values we can use; we use it to retrieve the name of our custom resource (e.g. demoweb1); we can use that name to derive the name of our deployment and to set labels for our pods
Note: instead of using **kwargs to retrieve the name, you can also define an extra name parameter in the handler like so: def create_fn(spec, name, **kwargs); see the docs for more information
Our deployment is just yaml stored in the doc variable with some help from the Python yaml package. We use spec.get and the name variable to customise it.
After the doc variable, the following code completes the event handler:

With kopf.adopt, we make sure the deployment we create is a child of our custom resource. When we delete the custom resource, its children are also deleted.
Next, we simply use the kubernetes client to create a deployment via the apps/v1 api. The method create_namespaced_deployment takes two required parameters: the namespace and the deployment specification. Note there is only minimal error checking here. There is much more you can do with regards to error checking, retries, etc…
Now we can run the operator with:
kopf run operator-filename.py
You can perfectly run this on your local workstation if you have a working kube config pointing at a running cluster with the CRD installed. Kopf will automatically use that for authentication:

Running the operator in your cluster
To run the operator in your cluster, create a Dockerfile that produces an image with Python, kopf, kubernetes and your operator in Python. In my case:
FROM python:3.7 RUN mkdir /src ADD with_create.py /src RUN pip install kopf RUN pip install kubernetes CMD kopf run /src/with_create.py --verbose
We added the verbose parameter for extra logging. Next, run the following commands to build and push the image (example with my image name):
docker build -t gbaeke/kopf-demoweb . docker push gbaeke/kopf-demoweb
Now you can deploy the operator to the cluster:
apiVersion: apps/v1 kind: Deployment metadata: name: demowebs-operator spec: replicas: 1 strategy: type: Recreate selector: matchLabels: application: demowebs-operator template: metadata: labels: application: demowebs-operator spec: serviceAccountName: demowebs-account containers: - name: demowebs image: gbaeke/kopf-demoweb
The above is just a regular deployment but the serviceAccountName is extremely important. It gives kopf and your operator the required access rights to create the deployment is the target namespace. Check out the documentation to find out more about the creation of the service account and the required roles. Note that you should only run one instance of the operator!
Once the operator is deployed, you will see it running as a normal pod:

To see what is going on, check the logs. Let’s show them with octant:

At the bottom, you see what happens when a creation event is detected for a resource of type DemoWeb. The spec is shown with the git repository and the number on replicas.
Now you can create resources of kind DemoWeb and see what happens. If you have your own git repository with some HTML in it, try to use that. Otherwise, just use mine at https://github.com/gbaeke/static-web.
Conclusion
Writing an operator is easy to do with the Kopf framework. Do note that we only touched on the basics to get started. We only have an on.create handler, and no on.update handler. So if you want to increase the number of replicas, you will have to delete the custom resource and create a new one. Based on the example though, it should be pretty easy to fix that. The git repo contains an example of an operator that also implements the on.update handler (with_update.py).