Kubernetes Operators in 2026: Custom CRDs, Reconcile Loops, and the Operator SDK
Build production Kubernetes Operators with the Operator SDK: custom resource definitions, reconcile loops, status conditions, finalizers, and operator lifecycle management.
Kubernetes Operators in 2026: Custom CRDs, Reconcile Loops, and the Operator SDK
Operators extend Kubernetes with domain-specific operational knowledge. Instead of running kubectl exec to failover a database or manually scaling a stateful service, an Operator watches custom resources and continuously reconciles the actual cluster state with your desired state. It's the same reconciliation loop that powers Kubernetes itself — you're just adding a controller for your application domain.
The canonical use cases: database operators (Postgres, MySQL, MongoDB), message queue operators (Kafka, RabbitMQ), and application lifecycle operators (canary deployments, certificate rotation, secret sync). This post covers building an Operator from scratch using the Operator SDK and controller-runtime.
When to Build an Operator
| Scenario | Operator? | Alternative |
|---|---|---|
| Stateful app with complex day-2 operations | ✅ | Helm + manual runbooks |
| Database provisioning + backup + failover | ✅ | Cloud-managed DB (RDS, CloudSQL) |
| Simple stateless app deployment | ❌ | Helm chart or plain manifests |
| One-off batch jobs | ❌ | CronJob |
| App config that changes frequently | ❌ | ConfigMap + Deployment env |
| Custom autoscaling logic | ✅ | HPA + custom metrics adapter |
Building an Operator is significant investment. Only do it when the operational complexity of your stateful application exceeds what Helm and runbooks can reasonably manage.
Operator SDK Setup
# Install Operator SDK
brew install operator-sdk # macOS
# or: curl -LO https://github.com/operator-framework/operator-sdk/releases/latest/...
# Initialize a new operator project
operator-sdk init \
--domain viprasol.com \
--repo github.com/viprasol/myapp-operator \
--plugins go/v4
# Create a new API (custom resource)
operator-sdk create api \
--group myapp \
--version v1alpha1 \
--kind MyApp \
--resource \
--controller
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
Custom Resource Definition (CRD)
// api/v1alpha1/myapp_types.go
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// MyAppSpec defines the desired state
type MyAppSpec struct {
// Number of replicas for the web tier
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=100
Replicas int32 `json:"replicas"`
// Container image to deploy
// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9._\-/:]+$`
Image string `json:"image"`
// Database connection configuration
Database DatabaseSpec `json:"database"`
// Resource requirements
// +optional
Resources *ResourcesSpec `json:"resources,omitempty"`
}
type DatabaseSpec struct {
// Secret name containing DATABASE_URL
SecretName string `json:"secretName"`
// Run migrations on each deployment
// +optional
RunMigrations bool `json:"runMigrations,omitempty"`
}
type ResourcesSpec struct {
// CPU request and limit (e.g., "500m", "2")
CPU string `json:"cpu"`
// Memory request and limit (e.g., "256Mi", "1Gi")
Memory string `json:"memory"`
}
// MyAppStatus defines the observed state
type MyAppStatus struct {
// Standard condition array
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// Number of ready replicas
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
// Current deployed image
DeployedImage string `json:"deployedImage,omitempty"`
// Timestamp of last successful migration
// +optional
LastMigrationTime *metav1.Time `json:"lastMigrationTime,omitempty"`
}
// Condition types
const (
ConditionAvailable = "Available"
ConditionProgressing = "Progressing"
ConditionDegraded = "Degraded"
)
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas"
// +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.readyReplicas"
// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
type MyApp struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyAppSpec `json:"spec,omitempty"`
Status MyAppStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type MyAppList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []MyApp `json:"items"`
}
func init() {
SchemeBuilder.Register(&MyApp{}, &MyAppList{})
}
Reconcile Loop
// internal/controller/myapp_controller.go
package controller
import (
"context"
"fmt"
"time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
myappv1 "github.com/viprasol/myapp-operator/api/v1alpha1"
)
type MyAppReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=myapp.viprasol.com,resources=myapps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=myapp.viprasol.com,resources=myapps/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// 1. Fetch the MyApp resource
myapp := &myappv1.MyApp{}
if err := r.Get(ctx, req.NamespacedName, myapp); err != nil {
if errors.IsNotFound(err) {
// Object deleted — nothing to do
return ctrl.Result{}, nil
}
return ctrl.Result{}, fmt.Errorf("failed to get MyApp: %w", err)
}
// 2. Handle deletion with finalizer
if !myapp.DeletionTimestamp.IsZero() {
return r.handleDeletion(ctx, myapp)
}
// 3. Add finalizer if not present
if err := r.ensureFinalizer(ctx, myapp); err != nil {
return ctrl.Result{}, err
}
// 4. Run database migrations if requested
if myapp.Spec.Database.RunMigrations {
done, err := r.runMigrations(ctx, myapp)
if err != nil {
r.setCondition(myapp, myappv1.ConditionDegraded, metav1.ConditionTrue,
"MigrationFailed", err.Error())
_ = r.Status().Update(ctx, myapp)
return ctrl.Result{RequeueAfter: 30 * time.Second}, err
}
if !done {
// Migration job still running — requeue
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}
}
// 5. Reconcile Deployment
if err := r.reconcileDeployment(ctx, myapp); err != nil {
return ctrl.Result{}, err
}
// 6. Reconcile Service
if err := r.reconcileService(ctx, myapp); err != nil {
return ctrl.Result{}, err
}
// 7. Update status
if err := r.updateStatus(ctx, myapp); err != nil {
return ctrl.Result{}, err
}
logger.Info("Reconciliation complete", "name", myapp.Name)
return ctrl.Result{}, nil
}
func (r *MyAppReconciler) reconcileDeployment(ctx context.Context, myapp *myappv1.MyApp) error {
desired := r.buildDeployment(myapp)
// Try to get existing deployment
existing := &appsv1.Deployment{}
err := r.Get(ctx, types.NamespacedName{
Name: myapp.Name,
Namespace: myapp.Namespace,
}, existing)
if errors.IsNotFound(err) {
// Create
if err := ctrl.SetControllerReference(myapp, desired, r.Scheme); err != nil {
return err
}
return r.Create(ctx, desired)
}
if err != nil {
return fmt.Errorf("failed to get Deployment: %w", err)
}
// Update if spec has changed
existing.Spec = desired.Spec
return r.Update(ctx, existing)
}
func (r *MyAppReconciler) buildDeployment(myapp *myappv1.MyApp) *appsv1.Deployment {
labels := map[string]string{
"app": myapp.Name,
"app.kubernetes.io/managed-by": "myapp-operator",
}
replicas := myapp.Spec.Replicas
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: myapp.Name,
Namespace: myapp.Namespace,
Labels: labels,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{MatchLabels: labels},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: labels},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "app",
Image: myapp.Spec.Image,
EnvFrom: []corev1.EnvFromSource{
{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: myapp.Spec.Database.SecretName,
},
},
},
},
ReadinessProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/healthz",
Port: 3000,
},
},
InitialDelaySeconds: 5,
PeriodSeconds: 10,
},
},
},
},
},
},
}
}
func (r *MyAppReconciler) setCondition(
myapp *myappv1.MyApp,
condType string,
status metav1.ConditionStatus,
reason, message string,
) {
cond := metav1.Condition{
Type: condType,
Status: status,
Reason: reason,
Message: message,
LastTransitionTime: metav1.Now(),
}
for i, c := range myapp.Status.Conditions {
if c.Type == condType {
if c.Status != status {
myapp.Status.Conditions[i] = cond
}
return
}
}
myapp.Status.Conditions = append(myapp.Status.Conditions, cond)
}
func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&myappv1.MyApp{}).
Owns(&appsv1.Deployment{}). // Watch owned Deployments
Complete(r)
}
⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
Applying the CRD
# config/samples/myapp_v1alpha1_myapp.yaml
apiVersion: myapp.viprasol.com/v1alpha1
kind: MyApp
metadata:
name: api-server
namespace: production
spec:
replicas: 3
image: "ghcr.io/myorg/api-server:v1.2.3"
database:
secretName: api-server-db-secret
runMigrations: true
resources:
cpu: "500m"
memory: "512Mi"
# Generate CRD manifests from Go types
make generate manifests
# Install CRDs in cluster
make install
# Run operator locally (against remote cluster)
make run
# Build and push operator image
make docker-build docker-push IMG=ghcr.io/myorg/myapp-operator:latest
# Deploy operator to cluster
make deploy IMG=ghcr.io/myorg/myapp-operator:latest
# Apply the custom resource
kubectl apply -f config/samples/myapp_v1alpha1_myapp.yaml
# Watch status
kubectl get myapps -w
# NAME REPLICAS READY IMAGE AGE
# api-server 3 3 ghcr.io/myorg/api-server:v1.2.3 2m
Operator Maturity Levels
| Level | Capabilities | Example |
|---|---|---|
| 1 — Basic Install | Automated installation via OLM | Helm-based operator |
| 2 — Seamless Upgrades | Patch and minor version upgrades | Image tag updates |
| 3 — Full Lifecycle | Backups, failure recovery | Postgres operator |
| 4 — Deep Insights | Metrics, alerts, log management | Full observability |
| 5 — Auto Pilot | Horizontal/vertical scaling, anomaly detection | Self-healing system |
Working With Viprasol
We design and build Kubernetes Operators for stateful applications — from CRD schema design through reconcile loop implementation and OLM packaging.
What we deliver:
- Operator architecture design (when to build vs. use existing operator)
- CRD design with validation markers and status conditions
- Reconcile loop implementation with proper error handling and requeue strategies
- Finalizer patterns for safe resource cleanup
- Operator lifecycle manager (OLM) packaging for OperatorHub
→ Discuss your Kubernetes automation needs → Cloud infrastructure services
See Also
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.