Kubernetes operators quick start

Kubernetes operator provide easy and simple way to extend Kubernetes cluster with additional functionality.

For example you might find that you need custom operator in areas like:

  1. Managing stateful applications: take any Database, Message Queue or Cache as example, it needs full lifecycle of operations to be good for production. For a long time running databases in Kubernetes was an anti-pattern, but with operators things started to changed.
  2. Automating Application Lifecycle Management: the complexity of making new application can be overwhelmed for your development teams, so why not to introduce something more simple with just few core fields which are shared between all applications.
  3. Enhancing Observability: operators like Prometheus operator or Alermanager hides complexity of managing these tools and provide simple interface to get started quick.
  4. Automated Backup, Restore, and Disaster Recovery: For stateful applications, operators can automate the process of taking regular backups, storing them securely, and orchestrating the restoration process in case of data loss or system failure.
  5. CI/CD and GitOps Automation: operators play crucial role in GitOps workflow. Tools like the Pulumi Kubernetes Operator enable managing infrastructure alongside Kubernetes workloads.
  6. Networking and Security Management: Operators for service meshes like Istio or Linkerd simplify their installation, configuration, and upgrades across the cluster.
  7. Building Platform-as-a-Service (PaaS)-like Capabilities: By abstracting away underlying Kubernetes resources, operators can provide a simplified, application-centric interface for developers, similar to what a PaaS offers.

Writing your own operator

If you think to get started to write your own operator it’s good to know what any operator consist of:

API Design

Custom Resource Definition (CRD) Design. The CRD is the API your users (and other tools) will interact with. A well-designed CRD is intuitive, clear, and extensible. A poorly designed one can be confusing and hard to evolve.

// AppoperatorSpec defines the desired state of Appoperator.
type AppoperatorSpec struct {
// Image specify the container image for the deployment
Image string `json:"image,omitempty"`

// Replicas specify the container replica count for the deployment
Replicas *int32 `json:"replicas,omitempty"`
}

// AppoperatorStatus defines the observed state of Appoperator.
type AppoperatorStatus struct {
// Conditions represent the latest available observations of the Appoperator's state.
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
  1. Keep the spec (desired state) as simple as possible for the user, abstracting away unnecessary complexity.
  2. Implement robust OpenAPI v3 schema validation in your CRD to catch errors early and guide users.
  3. Design a comprehensive status subresource to provide clear feedback to users about the state of the managed resources and any errors.

Reconciliation Loop Logic

This is the heart of your operator. It’s the code that observes the current state of the system and the desired state (from the CR) and takes action to make them match.

log := logf.FromContext(ctx)

// Fetch the Appoperator instance
var appoperator toolsv1beta1.Appoperator
if err := r.Get(ctx, req.NamespacedName, &appoperator); err != nil {
if apierrors.IsNotFound(err) {
// The resource was deleted, nothing to do
return ctrl.Result{}, nil
}
log.Error(err, "Failed to fetch Appoperator")
return ctrl.Result{}, err
}
// Logic to create deployment
// Logic to compare current deployment with spec
  1. The reconcile function must be idempotent. This means running it multiple times with the same inputs should produce the same outcome without unintended side effects.
  2. Properly handle errors from API calls or other operations. Decide whether an error is terminal or if the reconciliation should be retried.
  3. Ensure you correctly create, update, and delete managed resources (Deployments, Services, etc.). Avoid leaking resources.

State Management

Operators often manage stateful applications or complex workflows. Ensuring the operator correctly understands and transitions between states is vital.

deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: appoperator.Name,
Namespace: appoperator.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: appoperator.Spec.Replicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": appoperator.Name},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": appoperator.Name},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "app-container",
Image: appoperator.Spec.Image,
},
},
},
},
},
}
// Get existing deployment
// Compare with CR state and update if necessary
  1. Accurately read the current state of all managed resources from the Kubernetes API server before making decisions.
  2. Implement logic to detect differences between the CR spec and the actual state of the cluster.
  3. Be mindful of potential race conditions

Owner References and Garbage Collection

Proper use of owner references is essential for ensuring that Kubernetes automatically cleans up resources created by your operator when the parent Custom Resource is deleted.

  1. Ensure that all resources created and managed by the operator have an ownerReference pointing back to the instance of your Custom Resource that “owns” them.
  2. If your operator needs to perform actions before its Custom Resource can be deleted (e.g., de-provisioning an external resource, taking a final backup), use finalizers. This prevents the CR from being deleted until the operator removes the finalizer.

Status Reporting

Users rely on the status of your Custom Resource to understand what the operator is doing and whether their application is healthy.

  1. Use standard Kubernetes Conditions (e.g., Type: Ready, Status: True/False/Unknown) to provide a standardized way of reporting status.
  2. Include informative messages and reasons in your status conditions.
  3. Reflect the metadata.generation of the CR that was processed in the status.observedGeneration. This helps users and other tools understand if the status reflects the latest changes to the spec.

Versioning

Versioning is a critical aspect of developing and maintaining Kubernetes operators, as it impacts how users interact with your custom APIs and how your operator software evolves. It breaks down into two main areas: CRD (API) Versioning and Operator Software Versioning

A single CRD can define multiple API versions in its spec.versions list. In Kubernetes, all versions must be safely round-tripable through each other. This means that if we convert from version 1 to version 2, and then back to version 1, we must not lose information.

Operator software versioning refers to how you version the operator’s code and container image. Users need to know which version of the operator software supports which features and CRD versions.

Effective versioning requires a disciplined approach. For CRDs, follow Kubernetes API conventions, plan for conversions, and clearly communicate stability and deprecation. For operator software, use semantic versioning, clearly map operator versions to the CRD versions they support, and have a robust upgrade strategy, potentially leveraging tools like OLM. Both aspects are crucial for the long-term health and usability of your operator.

Create with Kubebuilder

Kubebuilder isn’t just providing boilerplate code and some level of automation, but it also a great learning resource to get start with operators. It covers:

  • The structure of Kubernetes APIs and Resources
  • API versioning semantics
  • Self-healing
  • Garbage Collection and Finalizers
  • Declarative vs Imperative APIs
  • Level-Based vs Edge-Base APIs
  • Resources vs Subresources
  • and more.

Generating manifests, running test, deployment and installation and other useful command available to make things easier from the start.

Summary

Kubernetes operators provide a powerful way to extend cluster functionality, primarily by automating the complex lifecycle management of applications (especially stateful ones like databases), enhancing observability, and enabling PaaS-like capabilities.

Developing a robust operator requires careful attention to its API (CRD) design, implementing idempotent reconciliation logic for state management, ensuring proper resource cleanup via owner references, clear status reporting, and disciplined versioning for both the CRD and the operator software.

Path to Staff Engineer role

Staff Engineer

Someday in your career you take your time to think what’s next for me? What is my next challenge to grow in a career ladder? If your answer is Staff Engineer, Tech lead, Team lead or Principal Engineer then you are on the way to Staff-plus Engineer role.

As a Staff-plus Engineer myself I want to share my few tips for you.

Start working on Staff engineer package

  1. What’s your Staff level project?
    • What did you do and what was impact?
    • What behavior did you demonstrate and how complex projects were?
  2. Link your Design Documents, RFCs and Proposals to support your package with your design and architecture contribution.
  3. Can you quantify the impact of your projects?
    • Did it helps to increase revenue?
    • Save costs?
  4. What glue work did you do to organization?
    • What’s the impact of the glue work?
  5. Who have you mentored and through what accomplishments?

Sharpen your soft skills

  1. Communication.
    • Keep people informed and connected.
  2. Negotiation
    • Be ready to resolve difficult situations
  3. Presentation
    • Know your audience. Learn one or two presentation frameworks.
  4. Networking
    • Don’t be shame to get in touch with other Staff-plus engineers in your company.
    • They are the best people to help you navigate the role inside the company.

Learn Staff Engineer tools

  1. Leadership
    • Become a problem solver. Be a visionary in your area. Stay up to date with technologies in your area.
  2. Planning and Goal orientation
    • Have a vision of what you do next in one or another area.
    • Take active participate on Planning meetings.
  3. Collaboration and contribution
    • Propose new Decision docs, Proposals and RFCs. Make sure they are reviewed and discussed.
    • Implement POC to demo the idea out.
  4. Team work and mentorship
    • Help your peers. Be a problem solver. Be visible. Become a go to person.
    • Try on a mentorship role.

Summary

Getting Staff engineer role could take months or years to accomplish. However, it’s crucial to view this milestone not as the ultimate destination, but rather as a guiding roadmap for your career development. It’s not the title itself that holds the most significance, but rather the daily challenge we face and the positive impact we make as we progress in our journey.

Resources

  1. About glue work.
  2. Staff Engineer book

calculating md5 sum using go lang

Hi guys,
this is day 18 out of 100 days of code.

To get md5 sum of a file you crypto/md5 library and io/ioutil for reading files.

package main

import (
	"crypto/md5"
	"fmt"
	"io/ioutil"
	"log"
	"os"
)

func main() {
	if len(os.Args) <= 1 {
		log.Fatal("expected filename as parameter")
	}
	filename := os.Args[1]

	data, err := ioutil.ReadFile(filename)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("MD5 (%s) = %x\n", filename, md5.Sum(data))
}

Thanks!

Slow algorithm to insert strings into indexed file in alphabetical order

Hi guys,
this is day 17 out of 100 days of code in go lang.

Today I have played with files, sorting, bytes and strings. The goal was to create a kind of indexed file where all lines are arranged alphabetically.
What I got is very slow algorithm which took 15 seconds to insert 10K strings in an empty file and 49 seconds to insert another 10K strings to the same file. So, it slowdown very quickly as size grow.

func writeLineToFileSorted(newdata string) {
	indexData, err := ioutil.ReadFile("db.txt")
	if err != nil {
		log.Fatal(err)
	}
	var newIndexData []string
	for _, line := range bytes.Split(indexData, []byte("\n")) {
		//fmt.Printf("%s\n", line)
		newIndexData = append(newIndexData, string(line))
	}
	newIndexData = append(newIndexData, string(newdata))
	sort.Strings(newIndexData)

	bytesData := []byte(strings.Join(newIndexData, "\n"))
	ioutil.WriteFile("db.txt", bytesData, 0644)
}

I have to convert between strings and bytes which was kind of annoying, but maybe I am doing it wrong? Please let me know in comments.

If you want to load test it here is how:

func main() {
	rand.Seed(time.Now().UnixNano())
	var newdata string
	for i := 0; i < 10000; i++ {
		newdata = randStringRunes(10)
		writeLineToFileSorted(newdata)
		fmt.Println(i)
	}

}

var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func randStringRunes(n int) string {
	b := make([]rune, n)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

Source code at Github

Happy coding!

Random image file download with go

Hi guys,
this is day 16 out of 100 days of code.

With help of http and ioutil packages file download and storage is quite easy task.

package main

import (
	"io/ioutil"
	"log"
	"net/http"
)

func main() {
	url := "https://picsum.photos/200/300/?random"

	resp, err := http.Get(url)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	image, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	err = ioutil.WriteFile("./myimage.gif", image, 0644)
	if err != nil {
		log.Fatal(err)
	}
}

In the example I am using Lorem Picsum service to get random image every run.

Happy coding!

Github issues reader in go lang

Hi guys,
this is day 15 out of 100 days of go land coding.

I continue github subject and today it is github issue reader. Example demonstrate how API results which come in json format convert into struct type.

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
)

func main() {
	repo := flag.String("repo", "", "github owner/repo e.g. golang/go")
	id := flag.Int("id", -1, "issue id")
	flag.Parse()

	if *id == -1 || *repo == "" {
		log.Fatal("--repo and --id parameters must be provided")
	}
	issue, _ := read(*repo, *id)
	fmt.Print(issue.Title)
}

// IssueData - specify data fields for new github issue submission
type IssueData struct {
	Title string `json:"title"`
	Body  string `json:"body"`
}

func read(ownerRepo string, id int) (*IssueData, error) {
	apiURL := fmt.Sprintf("https://api.github.com/repos/%s/issues/%d", ownerRepo, id)
	resp, err := http.Get(apiURL)
	if err != nil {
		log.Fatal(err)
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		log.Fatal(err)
		return nil, err
	}

	var result *IssueData
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		resp.Body.Close()
		return nil, err
	}

	return result, nil
}

Happy coding!

Create github issue ticket with golang

Hi guys,
today is day 14 out of 100 days of golang coding.

This time I was playing with github issues api and made small script which create new issues in given repository. However to use the code you would need to obtain github personal token key.

The interesting part was to make http post request with custom headers.

package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

func main() {
	token := flag.String("token", "", "github auth token")
	repo := flag.String("repo", "", "github owner/repo e.g. golang/go")
	title := flag.String("title", "", "title for new issue")
	body := flag.String("body", "", "body for new issue")
	flag.Parse()

	if *token == "" || *title == "" || *repo == "" {
		log.Fatal("--token, --repo and --title parameters must be provided")
	}
	create(*repo, *title, *body, *token)
}

// NewIssue - specify data fields for new github issue submission
type NewIssue struct {
	Title string `json:"title"`
	Body  string `json:"body"`
}

func create(ownerRepo, title, body, token string) {
	apiURL := "https://api.github.com/repos/" + ownerRepo + "/issues"
	//title is the only required field
	issueData := NewIssue{Title: title, Body: body}
	//make it json
	jsonData, _ := json.Marshal(issueData)
	//creating client to set custom headers for Authorization
	client := &http.Client{}
	req, _ := http.NewRequest("POST", apiURL, bytes.NewReader(jsonData))
	req.Header.Set("Authorization", "token "+token)
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated {
		fmt.Printf("Response code is is %d\n", resp.StatusCode)
		body, _ := ioutil.ReadAll(resp.Body)
		//print body as it may contain hints in case of errors
		fmt.Println(string(body))
		log.Fatal(err)
	}
}

Example usage is as follow:

go build github_issue.go
./github_issue --token="5daf49b235c41d53ba6fsfasdfasdfasfsad" --repo="vorozhko/go-tutor" --title="my new issue 3" --body="test"

Source code at Github.

 

github issue tracker with categories by date in go

Hi guys,
today is day 13 out of 100 days of code in go!

This time it is example of fetching issues from github and categorizing by created date.
To run example you need to install github issue searcher package from The Go Progamming Language book

go get gopl.io/ch4/github

Full code

package main

import (
	"fmt"
	"log"
	"os"
	"time"

	"gopl.io/ch4/github"
)

func main() {
	result, err := github.SearchIssues(os.Args[1:])
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d issues:\n", result.TotalCount)
	var dateformat string
	for _, item := range result.Items {
		days := int(time.Since(item.CreatedAt).Hours()) / 24
		if days < 30 {
			dateformat = "less than a month old"
		} else if days < 365 {
			dateformat = "less than a year old"
		} else {
			dateformat = "more than a year old"
		}
		fmt.Printf("%s, #%-5d %9.9s %.55s\n", dateformat, item.Number, item.User.Login, item.Title)
	}
}

Happy coding!

Sort map by it’s keys in go

Hi guys,
this is day 12 out of 100 days of code. I didn’t post for a while, because I did a small break for hackaton, but now I am back on track.

When you print a map using range function it will access elements in randomized order. So, if you want to print map in sorted order you have to sort keys first. See how:

package main

import (
	"fmt"
	"sort"
)

func main() {
	m := map[string]int{"A": 1, "B": 2, "AA": 1, "C": 3, "BBB": 2}

	//reverse keys and values of m map
	var mapKeys []string
	fmt.Print("Unsorted:\n")
	for k, v := range m {
		fmt.Printf("%s -> %d\n", k, v)
		mapKeys = append(mapKeys, k)
	}

	sort.Strings(mapKeys)
	fmt.Print("Sorted:\n")
	for _, v := range mapKeys {
		fmt.Printf("%s -> %d\n", v, m[v])
	}
}

Solution is to use additional array for keys and sort it in wished order.

Happy coding!

how to sort map values in go

Hi guys,
this is day 11 out of 100 days of go coding.

This time it is very short demo of go maps and simple sort by map values. Note that the example below will not work correctly if values has duplicates.

package main

import (
	"fmt"
	"sort"
)

func main() {
        //init a map with strings keys and int values
	var m = make(map[string]int)
	m["Sun"] = 7
	m["Sat"] = 6
	m["Fri"] = 5
	m["Thu"] = 4
	m["Mon"] = 1
	m["Tue"] = 2
	m["Wed"] = 3

	//reverse keys and values of m map
	var days = make(map[int]string)
	//array of indexes for sorting
	var daykeys = make([]int, len(m))

	fmt.Print("Unsorted days of week\n")
	counter := 0
	for k, v := range m {
		fmt.Printf("%s -> %d\n", k, v)
		days[v] = k
		daykeys[counter] = v
		counter++
	}
	//sort indexes array here
	sort.Ints(daykeys)

	fmt.Print("Sorted days of week\n")
	for _, v := range daykeys {
		fmt.Printf("%s\n", days[v])
	}
}

I believe this code has a lot of room for optimization, so if you know how please write me in comments or ping me in twitter with your version.

Happy coding everyone!