DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • GitOps-Backed Agentic Operator for Kubernetes: Safe Auto-Remediation With LLMs and Policy Guardrails
  • A Guide to Container Runtimes
  • Docker vs Kubernetes: Which to Use and When?
  • Serverless Kubernetes: The Rise of Zero-Management Container Orchestration

Trending

  • Building a DevOps-Ready Internal Developer Platform: A Hands-On Guide to Golden Paths, Self-Service, and Automated Delivery Pipelines
  • Rethinking Java CRUDs With Event Sourcing and CQRS Patterns
  • Why DDoS Protection Is an Architectural Decision for Developers
  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Deployment
  4. Building a Sample Kubernetes Operator on Minikube: A Step-by-Step Guide

Building a Sample Kubernetes Operator on Minikube: A Step-by-Step Guide

Build a Kubernetes Operator on Minikube to manage custom resources, automate tasks, and extend functionality using the Operator SDK.

By 
Sai Sandeep Ogety user avatar
Sai Sandeep Ogety
DZone Core CORE ·
Jan. 10, 25 · Tutorial
Likes (14)
Comment
Save
Tweet
Share
6.0K Views

Join the DZone community and get the full member experience.

Join For Free

Operators are a powerful way to extend Kubernetes functionality by acting as custom controllers. They leverage the Kubernetes control loop to manage application lifecycles using declarative custom resources. In this guide, we’ll create a simple “Hello” Operator with the Operator SDK, deploy it on Minikube, and see it in action.

Prerequisites

Before we begin, make sure you have the following installed and set up on your machine:

Minikube

  • You can grab Minikube from the official docs.

Start Minikube by running:

Shell
 
minikube start


Verify the Minikube cluster:

PowerShell
 
kubectl cluster-info


Go (1.19 or Later)

You can download and install Go from the official website.

Operator SDK

Follow the Operator SDK installation docs to install.

Confirm the version:

Shell
 
operator-sdk version


Docker (Or Similar Container Runtime)

We’ll use Docker for building and pushing container images.

With these tools in place, you’re all set for a smooth Operator development experience.

Project Setup and Initialization

Create a Project Directory

Let’s begin by creating a fresh directory for our project:

Shell
 
mkdir sample-operator-project
cd sample-operator-project


Initialize the Operator

Next, we’ll initialize our Operator project using the Operator SDK. This command scaffolds the basic project layout and configuration files:

Shell
 
operator-sdk init --domain=example.com --repo=github.com/youruser/sample-operator


Here’s what the flags mean:

  • --domain=example.com sets the domain for your Custom Resources.
  • --repo=github.com/youruser/sample-operator determines the Go module path for your code.

You’ll see a freshly generated project structure:

Shell
 
sample-operator-project/
├── Makefile
├── PROJECT
├── go.mod
├── go.sum
├── config/
│   └── ...
├── hack/
│   └── boilerplate.go.txt
├── main.go
└── ...


Creating the API and Controller

Add Your API (CRD) and Controller

Our next step is to create the Custom Resource Definition (CRD) and its associated controller. We’ll make a resource called Hello under the group apps and version v1alpha1:

Shell
 
operator-sdk create api --group apps --version v1alpha1 --kind Hello --resource --controller


This command generates:

  • A new API package under api/v1alpha1/
  • A controller source file in controllers/hello_controller.go

Define the Hello CRD

Open the file api/v1alpha1/hello_types.go. You’ll see the Hello struct representing our custom resource. We can add a simple Message field to the Spec and a LastReconcileTime field to the Status:

Go
 
package v1alpha1

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// HelloSpec defines the desired state of Hello
type HelloSpec struct {
    // Message is the text we want our operator to manage.
    Message string `json:"message,omitempty"`
}

// HelloStatus defines the observed state of Hello
type HelloStatus struct {
    // Stores a timestamp or an echo of the message
    LastReconcileTime string `json:"lastReconcileTime,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// Hello is the Schema for the hellos API
type Hello struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   HelloSpec   `json:"spec,omitempty"`
    Status HelloStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// HelloList contains a list of Hello
type HelloList struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ListMeta `json:"metadata,omitempty"`
    Items           []Hello `json:"items"`
}


Once you’re done, run:

Shell
 
make generate
make manifests


make generate regenerates deepcopy code, and make manifests updates your CRDs in the config/ directory.

Implementing the Controller

Open controllers/hello_controller.go. The core function here is Reconcile(), which defines how your Operator “reacts” to changes in Hello resources. Below is a minimal example that logs the message and updates LastReconcileTime:

Shell
 
package controllers

import (
    "context"
    "fmt"
    "time"

    "github.com/go-logr/logr"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"

    appsv1alpha1 "github.com/youruser/sample-operator/api/v1alpha1"
)

// HelloReconciler reconciles a Hello object
type HelloReconciler struct {
    client.Client
    Log logr.Logger
}

//+kubebuilder:rbac:groups=apps.example.com,resources=hellos,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps.example.com,resources=hellos/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps.example.com,resources=hellos/finalizers,verbs=update

func (r *HelloReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := r.Log.WithValues("hello", req.NamespacedName)

    // Fetch the Hello resource
    var hello appsv1alpha1.Hello
    if err := r.Get(ctx, req.NamespacedName, &hello); err != nil {
        // Resource not found—likely it was deleted
        log.Info("Resource not found. Ignoring since object must be deleted.")
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // Print the message from Spec
    log.Info(fmt.Sprintf("Hello Message: %s", hello.Spec.Message))

    // Update status with current time
    hello.Status.LastReconcileTime = time.Now().Format(time.RFC3339)
    if err := r.Status().Update(ctx, &hello); err != nil {
        log.Error(err, "Failed to update Hello status")
        return ctrl.Result{}, err
    }

    // Requeue after 30 seconds for demonstration
    return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *HelloReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&appsv1alpha1.Hello{}).
        Complete(r)
}


This snippet ensures each time the custom resource changes, the operator logs a message and updates the status to reflect the time it was last reconciled.

Building and Deploying the Operator

Set the Container Image

In the Makefile, locate the line:

CMake
 
IMG ?= controller:latest

Replace it with your desired image name (e.g., a Docker Hub repo):

CMake
 
IMG ?= your-docker-username/sample-operator:latest


Build and Push

To build and push your operator image:

CMake
 
make docker-build docker-push
  • docker-build compiles your Operator code into a Docker image.
  • docker-push pushes it to your specified image repository.

Deploy Onto Minikube

Install CRDs:

CMake
 
make install


This applies your CRD manifests to the cluster.

Deploy operator:

CMake
 
make deploy


This command sets up the operator in a dedicated namespace (usually <project>-system), creates a Deployment, and configures RBAC rules.

Check that your deployment is running:

Shell
 
kubectl get deployments -n sample-operator-system


You should see something like:

Shell
 
NAME                                     READY   UP-TO-DATE   AVAILABLE   AGE
sample-operator-controller-manager       1/1     1            1           1m


Testing Your Operator

Create a Hello Resource

We’ll now create a sample custom resource to watch the operator in action. Create a file named hello-sample.yaml with the following content:

YAML
 
apiVersion: apps.example.com/v1alpha1
kind: Hello
metadata:
  name: hello-sample
spec:
  message: "Hello from my first Operator!"


Next, apply the resource:

Shell
 
kubectl apply -f hello-sample.yaml


Check the CRD's Status

Shell
 
kubectl get hellos


You should see something like the following:

Shell
 
NAME           AGE
hello-sample   5s


Verify Logs and Status

Take a look at the operator’s logs:

Shell
 
kubectl get pods -n sample-operator-system
# Identify the sample-operator-controller-manager pod name, then:
kubectl logs sample-operator-controller-manager-xxxxx -n sample-operator-system --all-containers


Next, you should see something like:

Shell
 
1.590372e+09 INFO controllers.Hello  Hello Message: Hello from my first Operator!


You can also inspect the resource’s status:

Shell
 
kubectl get hello hello-sample -o yaml


Final Project Layout

Here’s how your project folder might look at this point:

Shell
 
sample-operator-project/
├── Makefile
├── PROJECT
├── config/
│   ├── crd/
│   │   └── bases/
│   │       └── apps.example.com_hellos.yaml
│   ├── default/
│   │   └── kustomization.yaml
│   ├── manager/
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── rbac/
│   │   ├── cluster_role.yaml
│   │   └── role.yaml
│   ├── samples/
│   │   └── apps_v1alpha1_hello.yaml
│   └── ...
├── api/
│   └── v1alpha1/
│       ├── hello_types.go
│       ├── groupversion_info.go
│       └── zz_generated.deepcopy.go
├── controllers/
│   └── hello_controller.go
├── hack/
│   └── boilerplate.go.txt
├── hello-sample.yaml
├── go.mod
├── go.sum
└── main.go


Conclusion

You have just developed a simple Kubernetes Operator that watches a Hello custom resource, prints its message into the logs, and changes its status every time it reconciles. On top of this basic foundation, you can extend the behavior of your Operator for real-world scenarios: managing external services, complex application lifecycles, or advanced configuration management.

Operators natively bring Kubernetes management to anything — from applications to infrastructure. With the Operator SDK, everything you need to rapidly scaffold, build, deploy, and test your custom controller logic is right at your fingertips. Experiment with iteration, adapt — and then let automation take over in an operator-driven environment!

Kubernetes Operator (extension) Container

Opinions expressed by DZone contributors are their own.

Related

  • GitOps-Backed Agentic Operator for Kubernetes: Safe Auto-Remediation With LLMs and Policy Guardrails
  • A Guide to Container Runtimes
  • Docker vs Kubernetes: Which to Use and When?
  • Serverless Kubernetes: The Rise of Zero-Management Container Orchestration

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook