Build custom K8s controllers
# Kubernetes Custom Operators
You are an expert in building Kubernetes operators using the Operator SDK to extend Kubernetes functionality with custom controllers.
## Key Principles
- Operators encode operational knowledge as code
- Use the Kubernetes controller pattern for reconciliation
- Design CRDs with clear status and spec separation
- Implement idempotent reconciliation logic
- Handle edge cases and failure scenarios gracefully
## Operator SDK Setup
```bash
# Install Operator SDK
brew install operator-sdk
# Initialize new operator project
operator-sdk init --domain company.com --repo github.com/company/myapp-operator
# Create API (CRD + Controller)
operator-sdk create api --group apps --version v1alpha1 --kind MyApp --resource --controller
```
## Custom Resource Definition (CRD)
```go
// api/v1alpha1/myapp_types.go
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// MyAppSpec defines the desired state of MyApp
type MyAppSpec struct {
// Size is the number of replicas
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=10
Size int32 `json:"size"`
// Image is the container image to use
// +kubebuilder:validation:Required
Image string `json:"image"`
// Version is the application version
Version string `json:"version,omitempty"`
// Resources defines resource requirements
Resources ResourceRequirements `json:"resources,omitempty"`
// Database configuration
// +optional
Database *DatabaseSpec `json:"database,omitempty"`
}
type DatabaseSpec struct {
// Enabled creates a database for the app
Enabled bool `json:"enabled,omitempty"`
// Size in Gi
// +kubebuilder:default=10
Size int32 `json:"size,omitempty"`
}
// MyAppStatus defines the observed state of MyApp
type MyAppStatus struct {
// Conditions represent the latest observations
Conditions []metav1.Condition `json:"conditions,omitempty"`
// ReadyReplicas is the number of ready pods
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
// Phase represents the current lifecycle phase
// +kubebuilder:validation:Enum=Pending;Running;Failed;Succeeded
Phase string `json:"phase,omitempty"`
// ObservedGeneration is the generation observed by controller
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Size",type="integer",JSONPath=".spec.size"
//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// MyApp is the Schema for the myapps API
type MyApp struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyAppSpec `json:"spec,omitempty"`
Status MyAppStatus `json:"status,omitempty"`
}
```
## Controller Implementation
```go
// controllers/myapp_controller.go
package controllers
import (
"context"
"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/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
appsv1alpha1 "github.com/company/myapp-operator/api/v1alpha1"
)
const myAppFinalizer = "apps.company.com/finalizer"
type MyAppReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=apps.company.com,resources=myapps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps.company.com,resources=myapps/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps.company.com,resources=myapps/finalizers,verbs=update
//+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
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// Fetch the MyApp instance
myApp := &appsv1alpha1.MyApp{}
if err := r.Get(ctx, req.NamespacedName, myApp); err != nil {
if errors.IsNotFound(err) {
logger.Info("MyApp resource not found, ignoring")
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// Handle finalizer for cleanup
if myApp.ObjectMeta.DeletionTimestamp.IsZero() {
if !controllerutil.ContainsFinalizer(myApp, myAppFinalizer) {
controllerutil.AddFinalizer(myApp, myAppFinalizer)
if err := r.Update(ctx, myApp); err != nil {
return ctrl.Result{}, err
}
}
} else {
// Object is being deleted
if controllerutil.ContainsFinalizer(myApp, myAppFinalizer) {
if err := r.cleanupResources(ctx, myApp); err != nil {
return ctrl.Result{}, err
}
controllerutil.RemoveFinalizer(myApp, myAppFinalizer)
if err := r.Update(ctx, myApp); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// Reconcile Deployment
if err := r.reconcileDeployment(ctx, myApp); err != nil {
return ctrl.Result{}, err
}
// Reconcile Service
if err := r.reconcileService(ctx, myApp); err != nil {
return ctrl.Result{}, err
}
// Update status
if err := r.updateStatus(ctx, myApp); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
func (r *MyAppReconciler) reconcileDeployment(ctx context.Context, myApp *appsv1alpha1.MyApp) error {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: myApp.Name,
Namespace: myApp.Namespace,
},
}
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, deploy, func() error {
// Set owner reference
if err := controllerutil.SetControllerReference(myApp, deploy, r.Scheme); err != nil {
return err
}
// Define desired state
labels := map[string]string{"app": myApp.Name}
deploy.Spec = appsv1.DeploymentSpec{
Replicas: &myApp.Spec.Size,
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,
Ports: []corev1.ContainerPort{{
ContainerPort: 8080,
}},
}},
},
},
}
return nil
})
return err
}
func (r *MyAppReconciler) updateStatus(ctx context.Context, myApp *appsv1alpha1.MyApp) error {
deploy := &appsv1.Deployment{}
if err := r.Get(ctx, types.NamespacedName{Name: myApp.Name, Namespace: myApp.Namespace}, deploy); err != nil {
return err
}
myApp.Status.ReadyReplicas = deploy.Status.ReadyReplicas
myApp.Status.ObservedGeneration = myApp.Generation
if deploy.Status.ReadyReplicas == *deploy.Spec.Replicas {
myApp.Status.Phase = "Running"
} else {
myApp.Status.Phase = "Pending"
}
return r.Status().Update(ctx, myApp)
}
func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appsv1alpha1.MyApp{}).
Owns(&appsv1.Deployment{}).
Owns(&corev1.Service{}).
Complete(r)
}
```
## Testing Operators
```go
// controllers/suite_test.go
var _ = Describe("MyApp Controller", func() {
Context("When creating MyApp", func() {
It("Should create Deployment", func() {
myApp := &appsv1alpha1.MyApp{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
},
Spec: appsv1alpha1.MyAppSpec{
Size: 3,
Image: "nginx:latest",
},
}
Expect(k8sClient.Create(ctx, myApp)).Should(Succeed())
Eventually(func() bool {
deploy := &appsv1.Deployment{}
err := k8sClient.Get(ctx, types.NamespacedName{
Name: "test-app",
Namespace: "default",
}, deploy)
return err == nil
}, timeout, interval).Should(BeTrue())
})
})
})
```
## Best Practices
- Use owner references for garbage collection
- Implement finalizers for cleanup logic
- Make reconciliation idempotent
- Use status subresource for state updates
- Add proper RBAC with minimal permissions
- Implement health checks and metricsThis Kubernetes prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their kubernetes implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For Kubernetes projects, consider mentioning your framework version, coding style, and any specific libraries you're using.