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.
Join the DZone community and get the full member experience.
Join For FreeOperators 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:
minikube start
Verify the Minikube cluster:
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:
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:
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:
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:
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
:
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
:
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:
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
:
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:
IMG ?= controller:latest
Replace it with your desired image name (e.g., a Docker Hub repo):
IMG ?= your-docker-username/sample-operator:latest
Build and Push
To build and push your operator image:
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:
make install
This applies your CRD manifests to the cluster.
Deploy operator:
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:
kubectl get deployments -n sample-operator-system
You should see something like:
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:
apiVersion: apps.example.com/v1alpha1
kind: Hello
metadata:
name: hello-sample
spec:
message: "Hello from my first Operator!"
Next, apply the resource:
kubectl apply -f hello-sample.yaml
Check the CRD's Status
kubectl get hellos
You should see something like the following:
NAME AGE
hello-sample 5s
Verify Logs and Status
Take a look at the operator’s logs:
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:
1.590372e+09 INFO controllers.Hello Hello Message: Hello from my first Operator!
You can also inspect the resource’s status:
kubectl get hello hello-sample -o yaml
Final Project Layout
Here’s how your project folder might look at this point:
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!
Opinions expressed by DZone contributors are their own.
Comments