Advanced Kubernetes: Custom Resources and Operators
Kubernetes Custom Resources extend the API. After building production operators, here’s how to create CRDs and operators effectively.
What are Custom Resources?
Custom Resources:
- Extend Kubernetes API
- Custom objects - Domain-specific
- Operators - Automation logic
- Declarative - Desired state
Custom Resource Definition
Basic CRD
# crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.example.com
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
databaseName:
type: string
databaseUser:
type: string
size:
type: string
enum: ["small", "medium", "large"]
status:
type: object
properties:
phase:
type: string
message:
type: string
scope: Namespaced
names:
plural: databases
singular: database
kind: Database
Create CRD
kubectl apply -f crd.yaml
Custom Resource Instance
Create Resource
# database-instance.yaml
apiVersion: example.com/v1
kind: Database
metadata:
name: my-database
spec:
databaseName: mydb
databaseUser: admin
size: medium
kubectl apply -f database-instance.yaml
kubectl get databases
Operator Pattern
Operator Components
- Controller - Watches resources
- Reconciler - Ensures desired state
- Finalizer - Cleanup logic
Operator Implementation (Go)
package main
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/manager"
)
type DatabaseReconciler struct {
client.Client
Scheme *runtime.Scheme
}
func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var db examplev1.Database
if err := r.Get(ctx, req.NamespacedName, &db); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Reconcile logic
if db.Status.Phase == "" {
db.Status.Phase = "Creating"
if err := r.Status().Update(ctx, &db); err != nil {
return ctrl.Result{}, err
}
}
// Create database instance
if err := r.createDatabase(ctx, &db); err != nil {
return ctrl.Result{}, err
}
db.Status.Phase = "Ready"
if err := r.Status().Update(ctx, &db); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *DatabaseReconciler) createDatabase(ctx context.Context, db *examplev1.Database) error {
// Create PostgreSQL instance
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: db.Name,
Namespace: db.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: int32Ptr(1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": db.Name},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": db.Name},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "postgres",
Image: "postgres:13",
Env: []corev1.EnvVar{
{Name: "POSTGRES_DB", Value: db.Spec.DatabaseName},
{Name: "POSTGRES_USER", Value: db.Spec.DatabaseUser},
},
},
},
},
},
},
}
return r.Create(ctx, deployment)
}
func main() {
mgr, err := manager.New(cfg, manager.Options{})
if err != nil {
panic(err)
}
if err = (&DatabaseReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
panic(err)
}
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
panic(err)
}
}
Operator SDK
Install Operator SDK
# Install
curl -LO https://github.com/operator-framework/operator-sdk/releases/download/v1.28.0/operator-sdk_linux_amd64
chmod +x operator-sdk
sudo mv operator-sdk /usr/local/bin/
Create Operator
# Create new operator
operator-sdk init --domain example.com --repo github.com/example/database-operator
# Create API
operator-sdk create api --group example --version v1 --kind Database --resource --controller
# Generate code
make generate
make manifests
Status Updates
Update Status
func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var db examplev1.Database
if err := r.Get(ctx, req.NamespacedName, &db); err != nil {
return ctrl.Result{}, err
}
// Update status
db.Status.Phase = "Ready"
db.Status.Message = "Database is ready"
db.Status.Ready = true
if err := r.Status().Update(ctx, &db); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
Finalizers
Add Finalizer
func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var db examplev1.Database
if err := r.Get(ctx, req.NamespacedName, &db); err != nil {
return ctrl.Result{}, err
}
// Add finalizer if not present
if !containsString(db.ObjectMeta.Finalizers, finalizerName) {
db.ObjectMeta.Finalizers = append(db.ObjectMeta.Finalizers, finalizerName)
if err := r.Update(ctx, &db); err != nil {
return ctrl.Result{}, err
}
}
// Handle deletion
if db.ObjectMeta.DeletionTimestamp.IsZero() {
// Normal reconcile
} else {
// Cleanup
if err := r.cleanup(ctx, &db); err != nil {
return ctrl.Result{}, err
}
// Remove finalizer
db.ObjectMeta.Finalizers = removeString(db.ObjectMeta.Finalizers, finalizerName)
if err := r.Update(ctx, &db); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
Best Practices
- Idempotent operations - Safe retries
- Status updates - Reflect current state
- Finalizers - Cleanup resources
- Error handling - Graceful failures
- Logging - Debug information
- Testing - Unit and integration tests
- Documentation - Clear usage
- Versioning - CRD versions
Conclusion
Custom Resources and Operators enable:
- Kubernetes API extension
- Domain-specific abstractions
- Automation
- Declarative operations
Start with simple CRDs, then build operators. The patterns shown here handle production workloads.
Kubernetes Custom Resources and Operators from January 2021, covering CRDs and operator patterns.