-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
This guide will walk you through creating your first Kubernetes operator using Kubebuilder and then enhancing it with the Controller Framework. We'll focus on the core concepts: the Stepper pattern and Context usage. By the end of this tutorial, you'll have a basic operator using the step-based reconciliation approach.
Before we begin, make sure you have the following tools installed:
- Go 1.21+: Install Go
- Kubebuilder: Install Kubebuilder
- kubectl: Install kubectl
- Docker: Install Docker
- kind or minikube: For local Kubernetes cluster (kind or minikube)
You can verify your installations:
go version
kubebuilder version
kubectl version --client
docker version
kind version # or minikube versionLet's create a new operator that will manage a custom resource called WebApp:
# Create a new directory for your project
mkdir webapp-operator
cd webapp-operator
# Initialize a new Kubebuilder project
kubebuilder init --domain example.com --repo github.com/your-username/webapp-operator
# Create a new API and controller
kubebuilder create api --group apps --version v1 --kind WebApp --resource --controller
# When prompted, answer 'y' to create both the resource and controllerThis creates a basic project structure with:
- A
WebAppcustom resource definition - A controller scaffolded to manage
WebAppresources - Necessary boilerplate code and configuration files
Edit the api/v1/webapp_types.go file to define a simple WebApp resource:
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// WebAppSpec defines the desired state of WebApp
type WebAppSpec struct {
// Image is the container image to deploy
Image string `json:"image"`
// Replicas is the number of desired replicas
// +kubebuilder:default=1
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=10
Replicas int32 `json:"replicas,omitempty"`
// Port is the port the application listens on
// +kubebuilder:default=8080
Port int32 `json:"port,omitempty"`
}
// WebAppStatus defines the observed state of WebApp
type WebAppStatus struct {
// Conditions represent the latest available observations
Conditions []metav1.Condition `json:"conditions,omitempty"`
// Ready indicates if the WebApp is ready
Ready bool `json:"ready,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// WebApp is the Schema for the webapps API
type WebApp struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec WebAppSpec `json:"spec,omitempty"`
Status WebAppStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// WebAppList contains a list of WebApp
type WebAppList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []WebApp `json:"items"`
}
func init() {
SchemeBuilder.Register(&WebApp{}, &WebAppList{})
}Generate the CRD manifests and install the Controller Framework:
# Generate CRD manifests
make manifests
# Add the Controller Framework dependency
go get github.com/u-ctf/controller-fwkNow we'll transform the basic Kubebuilder controller to use the Controller Framework. We'll focus on the core concepts:
-
Context-based approach: Use a
Contextobject for thread-safe access to your custom resource -
Step-based reconciliation: Use a
Stepperto orchestrate the reconciliation process - Simple status management: Update status using framework helpers
Important: We need to rename the Scheme field to RuntimeScheme to avoid conflicts with the client.Client interface, which already has a Scheme() method.
Edit internal/controller/webapp_controller.go:
package controller
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
ctrlfwk "github.com/u-ctf/controller-fwk"
appsv1alpha1 "github.com/your-username/webapp-operator/api/v1"
)
// WebAppReconciler reconciles a WebApp object
type WebAppReconciler struct {
client.Client
RuntimeScheme *runtime.Scheme
}
// Ensure WebAppReconciler implements the required interfaces
var _ ctrlfwk.Reconciler[*appsv1alpha1.WebApp] = &WebAppReconciler{}
// For method required by ctrlfwk.Reconciler interface
func (WebAppReconciler) For(*appsv1alpha1.WebApp) {}
//+kubebuilder:rbac:groups=apps.example.com,resources=webapps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps.example.com,resources=webapps/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps.example.com,resources=webapps/finalizers,verbs=update
// Reconcile is the main reconciliation loop
func (reconciler *WebAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := logf.FromContext(ctx)
// Create stepper with basic steps
stepper := ctrlfwk.NewStepperFor[*appsv1alpha1.WebApp](logger).
WithStep(ctrlfwk.NewFindControllerCustomResourceStep(reconciler)).
WithStep(reconciler.newValidationStep()).
WithStep(ctrlfwk.NewEndStep(reconciler, ctrlfwk.SetReadyCondition(reconciler))).
Build()
// Create framework context and execute
fwkCtx := ctrlfwk.NewContext[*appsv1alpha1.WebApp](ctx)
return stepper.Execute(fwkCtx, req)
}
// Custom validation step to show how to create your own steps
func (reconciler *WebAppReconciler) newValidationStep() ctrlfwk.Step[*appsv1alpha1.WebApp] {
return ctrlfwk.Step[*appsv1alpha1.WebApp]{
Name: "validate-webapp",
Step: func(ctx ctrlfwk.Context[*appsv1alpha1.WebApp], logger logr.Logger, req ctrl.Request) ctrlfwk.StepResult {
webapp := ctx.GetCustomResource()
// Simple validation: ensure image is specified
if webapp.Spec.Image == "" {
logger.Info("WebApp image not specified, marking as not ready")
webapp.Status.Ready = false
if err := ctrlfwk.PatchCustomResourceStatus(ctx, reconciler); err != nil {
logger.Error(err, "failed to update WebApp status")
return ctrlfwk.ResultInError(err)
}
// Return early - don't continue reconciliation if validation fails
return ctrlfwk.ResultEarlyReturn()
}
// Log current state
logger.Info("Validating WebApp",
"image", webapp.Spec.Image,
"replicas", webapp.Spec.Replicas,
"port", webapp.Spec.Port)
webapp.Status.Ready = true
if err := ctrlfwk.PatchCustomResourceStatus(ctx, reconciler); err != nil {
logger.Error(err, "failed to update WebApp status")
return ctrlfwk.ResultInError(err)
}
// Validation passed, continue to next steps (the Ready condition will be set by EndStep)
return ctrlfwk.ResultSuccess()
},
}
}Framework Mental Model: The framework follows the principle that if reconciliation reaches the end (all steps complete successfully), then the resource is Ready. If validation fails or other issues occur, we:
- Explicitly mark the resource as NOT ready using
ctrlfwk.PatchStatus() - Return
ctrlfwk.ResultEarlyReturn()to stop reconciliation early
This creates a clear contract: reaching the EndStep means success and readiness, while early returns indicate problems that prevent readiness.
// SetupWithManager sets up the controller with the Manager
func (reconciler *WebAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appsv1alpha1.WebApp{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Named("webapp").
Complete(reconciler)
}Don't forget to add the missing import for the logger:
import (
"context"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
ctrlfwk "github.com/u-ctf/controller-fwk"
appsv1alpha1 "github.com/your-username/webapp-operator/api/v1"
)}
Don't forget to update the test files to use `RuntimeScheme` instead of `Scheme` as well. You can find these in `internal/controller/webapp_controller_test.go` if they were generated.
Update `cmd/main.go` to properly initialize the controller with the framework:
```go
// Find the existing controller setup code and replace it with:
if err := (&controller.WebAppReconciler{
Client: mgr.GetClient(),
RuntimeScheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "WebApp")
os.Exit(1)
}
Now let's build and test the operator:
# Generate manifests and build
make manifests generate fmt vet build
# Create a local Kubernetes cluster (using kind)
kind create cluster --name webapp-operator
# Install CRDs
make install
# Run the operator locally (in another terminal)
make runCreate a sample WebApp resource to test your operator. Create config/samples/apps_v1_webapp.yaml:
apiVersion: apps.example.com/v1
kind: WebApp
metadata:
labels:
app.kubernetes.io/name: webapp
app.kubernetes.io/instance: webapp-sample
app.kubernetes.io/part-of: webapp-operator
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/created-by: webapp-operator
name: webapp-sample
namespace: default
spec:
image: nginx:1.21
replicas: 2
port: 80Apply it to your cluster:
kubectl apply -f config/samples/apps_v1_webapp.yamlCheck that your operator is working:
# Check the WebApp resource
kubectl get webapp
# Check the WebApp status and conditions
kubectl get webapp webapp-sample -o yaml
# Look at the operator logs
# In your terminal running 'make run', you should see structured logs showing:
# - Step execution (find-controller-resource, validate-webapp, end)
# - Validation messages
# - Status updatesYou should see output showing:
- The WebApp resource with ready conditions
- Structured logging from your custom validation step
- Status updates managed by the framework
Congratulations! You've successfully:
- Created a Kubebuilder project from scratch
- Defined a simple custom resource with proper validation
- Integrated Controller Framework with minimal code changes
- Implemented step-based reconciliation using the Stepper pattern
- Used Context for thread-safe resource access
- Created a custom validation step to understand the framework's flexibility
- Added automatic status management with built-in condition handling
Your operator now uses:
-
Context Pattern: Thread-safe access to your custom resource via
ctx.GetCustomResource() - Stepper Pattern: Structured reconciliation with discrete, testable steps
-
Built-in Steps: Framework-provided steps like
FindControllerCustomResourceStepandEndStep - Custom Steps: Your own business logic encapsulated in reusable steps
-
Automatic Status Management: Framework handles condition tracking with
SetReadyCondition
Now that you understand the core Framework concepts:
- Add Resource Management: Learn how to manage Kubernetes resources in Resource Management
- Handle Dependencies: Learn about external dependencies in Dependencies
- Create Advanced Steps: Explore Custom Steps for complex business logic
- Optimize Performance: Learn about Watch Cache for advanced watching patterns
- Implement Testing: Set up tests using the Testing Guide
Here's what happens when you apply a WebApp resource:
- FindControllerCustomResourceStep: Loads your WebApp from the cluster and stores it in the Context
- Custom Validation Step: Your business logic validates the WebApp spec
- EndStep: Updates the WebApp status with ready conditions
The Context provides thread-safe access to your custom resource throughout all steps, and the Stepper ensures each step is executed in order with proper error handling and logging.
If you encounter issues:
- Check the operator logs - The framework provides detailed step-by-step logging
-
Verify your imports - Make sure you've added the
github.com/go-logr/logrimport -
Ensure CRDs are installed with
kubectl get crd -
Check the WebApp status with
kubectl get webapp webapp-sample -o yaml
For more help, visit our Troubleshooting guide or ask questions in GitHub Discussions.