Sunday, October 13, 2024

How to create a Kubernetes Operator?

Creating a Kubernetes operator involves building a controller that watches Kubernetes resources and takes action based on their state. The common approach to create an operator is using the kubebuilder framework or the Operator SDK, but a custom solution using the Kubernetes API client directly can also be done.

Below, I'll show an example of a simple operator using the client-go library, which is the official Kubernetes client for Go. This operator will watch a custom resource called Foo and log whenever a Foo resource is created, updated, or deleted.

Prerequisites

Go programming language installed.

Kubernetes cluster and kubectl configured.

client-go and apimachinery libraries installed.


To install these dependencies, run:

go get k8s.io/client-go@v0.27.1
go get k8s.io/apimachinery@v0.27.1

Step 1: Define a Custom Resource Definition (CRD)

Create a foo-crd.yaml file to define a Foo custom resource:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: foos.samplecontroller.k8s.io
spec:
  group: samplecontroller.k8s.io
  versions:
    - name: v1
      served: true
      storage: true
  scope: Namespaced
  names:
    plural: foos
    singular: foo
    kind: Foo
    shortNames:
    - fo

Apply this CRD to the cluster:

kubectl apply -f foo-crd.yaml

Step 2: Create a Go File for the Operator

Create a new Go file named main.go:

package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
)

func main() {
// Load the Kubernetes configuration from ~/.kube/config
kubeconfig := flag.String("kubeconfig", clientcmd.RecommendedHomeFile, "Path to the kubeconfig file")
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
log.Fatalf("Error building kubeconfig: %v", err)
}

// Create a dynamic client
dynClient, err := dynamic.NewForConfig(config)
if err != nil {
log.Fatalf("Error creating dynamic client: %v", err)
}

// Define the GVR (GroupVersionResource) for the Foo custom resource
gvr := schema.GroupVersionResource{
Group:    "samplecontroller.k8s.io",
Version:  "v1",
Resource: "foos",
}

// Create a list watcher for Foo resources
fooListWatcher := cache.NewListWatchFromClient(
dynClient.Resource(gvr), "foos", "", cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
foo := obj.(*unstructured.Unstructured)
fmt.Printf("New Foo Added: %s\n", foo.GetName())
},
UpdateFunc: func(oldObj, newObj interface{}) {
foo := newObj.(*unstructured.Unstructured)
fmt.Printf("Foo Updated: %s\n", foo.GetName())
},
DeleteFunc: func(obj interface{}) {
foo := obj.(*unstructured.Unstructured)
fmt.Printf("Foo Deleted: %s\n", foo.GetName())
},
},
)

// Create a controller to handle Foo events
stopCh := make(chan struct{})
defer close(stopCh)
_, controller := cache.NewInformer(fooListWatcher, &unstructured.Unstructured{}, 0, cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
fmt.Println("Foo Created:", obj)
},
UpdateFunc: func(oldObj, newObj interface{}) {
fmt.Println("Foo Updated:", newObj)
},
DeleteFunc: func(obj interface{}) {
fmt.Println("Foo Deleted:", obj)
},
})

// Run the controller
go controller.Run(stopCh)

// Wait for a signal to stop the operator
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
fmt.Println("Stopping the Foo operator...")
}

Step 3: Running the Operator

1. Build and run the Go program:

go run main.go


2. Create a sample Foo resource to test:

# Save this as foo-sample.yaml
apiVersion: samplecontroller.k8s.io/v1
kind: Foo
metadata:
  name: example-foo

Apply this resource:

kubectl apply -f foo-sample.yaml

Step 4: Check the Output

You should see logs in the terminal indicating when Foo resources are added, updated, or deleted:

New Foo Added: example-foo
Foo Updated: example-foo
Foo Deleted: example-foo

Explanation

1. Dynamic Client: The operator uses the dynamic client to interact with the custom resource since Foo is a CRD.


2. ListWatcher: The NewListWatchFromClient is used to monitor changes in Foo resources.


3. Controller: The controller is set up to handle Add, Update, and Delete events for the Foo resource.


4. Signal Handling: It gracefully shuts down on receiving a termination signal.



Further Enhancements

Use a code generation framework like kubebuilder or Operator SDK for complex operators.

Implement reconcile logic to manage the desired state.

Add leader election for high availability.


This example demonstrates the basic structure of an operator using the Kubernetes API. For production-grade operators, using a dedicated framework is recommended.

No comments:

Post a Comment

Related Posts Plugin for WordPress, Blogger...