Why not write a Kubernetes operator in Python for the management of your SI ?

Franck De Graeve
ADEO Tech Blog

--

Introduction

Kubernetes has revolutionized the way we deploy and manage applications in the cloud-native era. It provides a powerful and flexible platform to orchestrate containerized workloads efficiently. However, to extend Kubernetes’ capabilities beyond its native features, Custom Resource Definitions (CRDs) and Operators come into play. In this post, we will explore what CRDs and Operators are, how they work together, how to build an operator and how they bring immense value to deploying and managing infrastructure and applications within the Kubernetes ecosystem.

Custom Resource Definitions (CRDs)

At its core, Kubernetes relies on a set of built-in resources like Pods, Services, and Deployments to manage containers. However, Kubernetes allows you to define your custom resources through CRDs, which essentially act as an extension mechanism. A CRD allows you to define a new resource type with its own properties and behavior.

Think of CRDs as blueprints that define how your custom resources will look and behave within the Kubernetes cluster. Once you define a CRD, Kubernetes can handle the lifecycle of these custom resources, just like its native resources.

Operators

Operators are a powerful concept introduced by CoreOS (now Red Hat) that leverage CRDs to automate the management of complex applications in Kubernetes. An Operator is essentially a Kubernetes controller specifically designed to work with CRDs. It uses custom controllers to watch and react to changes in custom resources, automating the operations required to maintain the desired state of the resources.

How Operators Work

Operators are tailored to handle the intricacies of a particular application or infrastructure component. They understand the internals of the application and act accordingly to maintain its health and availability.

When you create a custom resource, the Operator watches for changes to that resource. Upon detection of a change, the Operator takes appropriate actions to reconcile the actual state of the resource with its desired state, as defined in the CRD. This could involve scaling the application, performing backups, updating configurations, or any other task specific to the application.

Operators are designed to be intelligent and autonomous, freeing developers and operators from tedious manual tasks and reducing the likelihood of human errors.

How to write an operator

We will now take a few minutes to write an operator that generates a UUID as a KISS example. We will use Kopf, a Python framework, to create a simple operator. While most operators in the market are written in Golang, Python is also popular in the Ops world. The choice depends on your team’s knowledge.

First, we will create a resource of type (kind) Uuid. The CRD is created with the YAML below.

# A simple CRD to generate an UUID
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: uuid.demo.operator
spec:
scope: Namespaced
group: demo.operator
names:
kind: Uuid
plural: uuid
singular: uuid
shortNames:
- id
- uuid
versions:
- name: v1
served: true
storage: true
additionalPrinterColumns:
- name: UUID
type: string
description: The generated UUID
jsonPath: .status.uuid
- name: Status
type: string
description: Say if the component are successfully created or not.
jsonPath: .status.state
schema:
openAPIV3Schema:
type: object
properties:
status:
properties:
state:
type: string
uuid:
type: string
message:
type: string
type: object

For more information (and there is a lot) about CRDs, see the documentation.

Note that we specified our own API and version in the CRD (demo.operator/v1) and that we set the kind to Uuid. In the additionalPrinterColumns, we defined some properties that can be set in the spec that will also be printed on screen.

Creating the CRD and the custom resources is not enough. To actually generate an uuid when the custom resource is created, we need to write and run the operator.

First install Kopf and the Kubernetes package

kopf==1.36.0
kubernetes==18.20.0

My code for this example

import kopf
import logging
import kubernetes
import uuid

@kopf.on.login()
def login_fn(**kwargs):
return kopf.login_via_client(**kwargs)

@kopf.on.startup()
def configure(settings: kopf.OperatorSettings, **_):
settings.posting.level = logging.WARNING

@kopf.on.create('uuid') #logic code when you create an uuid object on the cluster
def create_fn(spec, name, namespace, **kwargs):
myuuid = uuid.uuid4()
try:
logging.info(f"Creation request for ressource [{name}] CREATION into the namespace [{namespace}]")
logging.info(f"UUID generated for ressource [{name}] UUID [{myuuid}]")
State = "Ok"
Message = "Creation of ressource successfully"
except Exception as e:
logging.error(f"Exception generated for uuid [{name}] CREATION into the namespace [{namespace}]: {str(e)}")
Message = (str(e))
State = "Error"
myuuid = "NA"
updateObjectStatus(namespace, name, Message, State, myuuid)

@kopf.on.update('uuid') #logic code when you update an uuid object on the cluster
def update_fn(spec, name, namespace, **kwargs):
pass

@kopf.on.delete('uuid') #logic code when you delete an uuid object on the cluster
def delete_fn(spec, name, namespace, **kwargs):
pass

def updateObjectStatus(custom_object_namespace, custom_object_name, Message, State, myuuid):
custom_object_status = buildObjectStatus(Message, State, myuuid)
sendObjectStatusToKubernetes(custom_object_namespace, custom_object_name, custom_object_status)

def buildObjectStatus(Message, State, myuuid):
return {
'status': {
'state': State,
'message': Message,
'uuid': str(myuuid)
}
}

def sendObjectStatusToKubernetes(custom_object_namespace, custom_object_name, custom_object_status):
kubernetes.client.CustomObjectsApi().patch_namespaced_custom_object(
namespace=custom_object_namespace,
name=custom_object_name,
body=custom_object_status,
group="demo.operator",
version="v1",
plural="uuid",
)

Running the operator locally, but the second step will be to make a docker image and run this operator inside the cluster.

kopf run --standalone --liveness=http://0.0.0.0:8080/healthz app.py

Now you have an uuid CRD deploy on the cluster and the operator. You can create the uuid object on the cluster.

apiVersion: demo.operator/v1
kind: Uuid
metadata:
name: my-first-ressource

Live example.

Conclusion

CRDs and Operators are powerful tools that extend Kubernetes capabilities, making it more than just a container orchestration platform. With CRDs, you can define custom resources tailored to your specific needs, promoting consistency and reusability. Operators, in turn, leverage these custom resources to automate complex tasks, consume product and APIs, ensuring your applications and infrastructure are always in the desired state.

By incorporating CRDs and Operators into your Kubernetes ecosystem, you can simplify infrastructure management, extend the way to consume your system information, and streamline application deployment, resulting in a more efficient and robust cloud-native environment.

--

--