Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Deploy and manage Kubernetes workloads: manifests, RBAC, Helm charts, service mesh, GitOps, and troubleshooting.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/custom-operators.md
1# Custom Operators23---45## CustomResourceDefinition (CRD)67```yaml8apiVersion: apiextensions.k8s.io/v19kind: CustomResourceDefinition10metadata:11name: databases.mycompany.io12spec:13group: mycompany.io14names:15kind: Database16listKind: DatabaseList17plural: databases18singular: database19shortNames:20- db21scope: Namespaced22versions:23- name: v124served: true25storage: true26schema:27openAPIV3Schema:28type: object29required:30- spec31properties:32spec:33type: object34required:35- engine36- version37- storage38properties:39engine:40type: string41enum: [postgres, mysql, mongodb]42version:43type: string44storage:45type: string46pattern: '^[0-9]+Gi$'47replicas:48type: integer49minimum: 150maximum: 551default: 152status:53type: object54properties:55phase:56type: string57enum: [Pending, Creating, Running, Failed, Terminating]58ready:59type: boolean60message:61type: string62endpoint:63type: string64subresources:65status: {}66scale:67specReplicasPath: .spec.replicas68statusReplicasPath: .status.replicas69additionalPrinterColumns:70- name: Engine71type: string72jsonPath: .spec.engine73- name: Version74type: string75jsonPath: .spec.version76- name: Status77type: string78jsonPath: .status.phase79- name: Age80type: date81jsonPath: .metadata.creationTimestamp82```8384## Custom Resource Instance8586```yaml87apiVersion: mycompany.io/v188kind: Database89metadata:90name: orders-db91namespace: production92spec:93engine: postgres94version: "15.4"95storage: 100Gi96replicas: 397```9899## Operator SDK Project Structure100101```102my-operator/103├── Dockerfile104├── Makefile105├── PROJECT # Kubebuilder project config106├── config/107│ ├── crd/ # CRD manifests108│ │ └── bases/109│ │ └── mycompany.io_databases.yaml110│ ├── manager/ # Operator deployment111│ │ └── manager.yaml112│ ├── rbac/ # RBAC configuration113│ │ ├── role.yaml114│ │ ├── role_binding.yaml115│ │ └── service_account.yaml116│ └── samples/ # Example CRs117│ └── mycompany_v1_database.yaml118├── api/119│ └── v1/120│ ├── database_types.go # API type definitions121│ ├── groupversion_info.go122│ └── zz_generated.deepcopy.go123├── controllers/124│ └── database_controller.go # Reconciliation logic125├── main.go # Entry point126└── go.mod127```128129## API Type Definition (Go)130131```go132// api/v1/database_types.go133package v1134135import (136metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"137)138139// DatabaseSpec defines the desired state of Database140type DatabaseSpec struct {141// Engine is the database engine type142// +kubebuilder:validation:Enum=postgres;mysql;mongodb143Engine string `json:"engine"`144145// Version is the database version146Version string `json:"version"`147148// Storage is the size of persistent storage149// +kubebuilder:validation:Pattern=`^[0-9]+Gi$`150Storage string `json:"storage"`151152// Replicas is the number of database instances153// +kubebuilder:validation:Minimum=1154// +kubebuilder:validation:Maximum=5155// +kubebuilder:default=1156// +optional157Replicas int32 `json:"replicas,omitempty"`158}159160// DatabaseStatus defines the observed state of Database161type DatabaseStatus struct {162// Phase represents the current lifecycle phase163Phase string `json:"phase,omitempty"`164165// Ready indicates if the database is ready to accept connections166Ready bool `json:"ready,omitempty"`167168// Message provides additional status information169Message string `json:"message,omitempty"`170171// Endpoint is the connection endpoint172Endpoint string `json:"endpoint,omitempty"`173174// Replicas is the current number of running replicas175Replicas int32 `json:"replicas,omitempty"`176}177178// +kubebuilder:object:root=true179// +kubebuilder:subresource:status180// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas181// +kubebuilder:printcolumn:name="Engine",type=string,JSONPath=`.spec.engine`182// +kubebuilder:printcolumn:name="Version",type=string,JSONPath=`.spec.version`183// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`184// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"185186// Database is the Schema for the databases API187type Database struct {188metav1.TypeMeta `json:",inline"`189metav1.ObjectMeta `json:"metadata,omitempty"`190191Spec DatabaseSpec `json:"spec,omitempty"`192Status DatabaseStatus `json:"status,omitempty"`193}194195// +kubebuilder:object:root=true196197// DatabaseList contains a list of Database198type DatabaseList struct {199metav1.TypeMeta `json:",inline"`200metav1.ListMeta `json:"metadata,omitempty"`201Items []Database `json:"items"`202}203204func init() {205SchemeBuilder.Register(&Database{}, &DatabaseList{})206}207```208209## Controller Implementation210211```go212// controllers/database_controller.go213package controllers214215import (216"context"217"fmt"218"time"219220appsv1 "k8s.io/api/apps/v1"221corev1 "k8s.io/api/core/v1"222"k8s.io/apimachinery/pkg/api/errors"223"k8s.io/apimachinery/pkg/api/resource"224metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"225"k8s.io/apimachinery/pkg/runtime"226"k8s.io/apimachinery/pkg/types"227ctrl "sigs.k8s.io/controller-runtime"228"sigs.k8s.io/controller-runtime/pkg/client"229"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"230"sigs.k8s.io/controller-runtime/pkg/log"231232mycompanyv1 "github.com/mycompany/database-operator/api/v1"233)234235const databaseFinalizer = "databases.mycompany.io/finalizer"236237type DatabaseReconciler struct {238client.Client239Scheme *runtime.Scheme240}241242// +kubebuilder:rbac:groups=mycompany.io,resources=databases,verbs=get;list;watch;create;update;patch;delete243// +kubebuilder:rbac:groups=mycompany.io,resources=databases/status,verbs=get;update;patch244// +kubebuilder:rbac:groups=mycompany.io,resources=databases/finalizers,verbs=update245// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete246// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete247// +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=get;list;watch248249func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {250logger := log.FromContext(ctx)251252// Fetch the Database instance253database := &mycompanyv1.Database{}254if err := r.Get(ctx, req.NamespacedName, database); err != nil {255if errors.IsNotFound(err) {256return ctrl.Result{}, nil257}258return ctrl.Result{}, err259}260261// Handle deletion with finalizer262if !database.DeletionTimestamp.IsZero() {263if controllerutil.ContainsFinalizer(database, databaseFinalizer) {264if err := r.cleanupResources(ctx, database); err != nil {265return ctrl.Result{}, err266}267controllerutil.RemoveFinalizer(database, databaseFinalizer)268if err := r.Update(ctx, database); err != nil {269return ctrl.Result{}, err270}271}272return ctrl.Result{}, nil273}274275// Add finalizer if not present276if !controllerutil.ContainsFinalizer(database, databaseFinalizer) {277controllerutil.AddFinalizer(database, databaseFinalizer)278if err := r.Update(ctx, database); err != nil {279return ctrl.Result{}, err280}281}282283// Reconcile StatefulSet284statefulSet := r.buildStatefulSet(database)285if err := controllerutil.SetControllerReference(database, statefulSet, r.Scheme); err != nil {286return ctrl.Result{}, err287}288289found := &appsv1.StatefulSet{}290err := r.Get(ctx, types.NamespacedName{Name: statefulSet.Name, Namespace: statefulSet.Namespace}, found)291if err != nil && errors.IsNotFound(err) {292logger.Info("Creating StatefulSet", "name", statefulSet.Name)293if err := r.Create(ctx, statefulSet); err != nil {294return ctrl.Result{}, err295}296return r.updateStatus(ctx, database, "Creating", false, "StatefulSet created")297} else if err != nil {298return ctrl.Result{}, err299}300301// Reconcile Service302service := r.buildService(database)303if err := controllerutil.SetControllerReference(database, service, r.Scheme); err != nil {304return ctrl.Result{}, err305}306307foundSvc := &corev1.Service{}308err = r.Get(ctx, types.NamespacedName{Name: service.Name, Namespace: service.Namespace}, foundSvc)309if err != nil && errors.IsNotFound(err) {310if err := r.Create(ctx, service); err != nil {311return ctrl.Result{}, err312}313}314315// Update status based on StatefulSet state316if found.Status.ReadyReplicas == *found.Spec.Replicas {317return r.updateStatus(ctx, database, "Running", true,318fmt.Sprintf("%d/%d replicas ready", found.Status.ReadyReplicas, *found.Spec.Replicas))319}320321// Requeue to check status322return ctrl.Result{RequeueAfter: 10 * time.Second}, nil323}324325func (r *DatabaseReconciler) buildStatefulSet(db *mycompanyv1.Database) *appsv1.StatefulSet {326replicas := db.Spec.Replicas327labels := map[string]string{328"app": db.Name,329"controller": db.Name,330}331332return &appsv1.StatefulSet{333ObjectMeta: metav1.ObjectMeta{334Name: db.Name,335Namespace: db.Namespace,336},337Spec: appsv1.StatefulSetSpec{338Replicas: &replicas,339Selector: &metav1.LabelSelector{340MatchLabels: labels,341},342ServiceName: db.Name,343Template: corev1.PodTemplateSpec{344ObjectMeta: metav1.ObjectMeta{345Labels: labels,346},347Spec: corev1.PodSpec{348Containers: []corev1.Container{{349Name: "database",350Image: fmt.Sprintf("%s:%s", db.Spec.Engine, db.Spec.Version),351Ports: []corev1.ContainerPort{{352ContainerPort: 5432,353Name: "db",354}},355}},356},357},358VolumeClaimTemplates: []corev1.PersistentVolumeClaim{{359ObjectMeta: metav1.ObjectMeta{360Name: "data",361},362Spec: corev1.PersistentVolumeClaimSpec{363AccessModes: []corev1.PersistentVolumeAccessMode{364corev1.ReadWriteOnce,365},366Resources: corev1.VolumeResourceRequirements{367Requests: corev1.ResourceList{368corev1.ResourceStorage: resource.MustParse(db.Spec.Storage),369},370},371},372}},373},374}375}376377func (r *DatabaseReconciler) buildService(db *mycompanyv1.Database) *corev1.Service {378return &corev1.Service{379ObjectMeta: metav1.ObjectMeta{380Name: db.Name,381Namespace: db.Namespace,382},383Spec: corev1.ServiceSpec{384Selector: map[string]string{"app": db.Name},385Ports: []corev1.ServicePort{{386Port: 5432,387Name: "db",388}},389ClusterIP: "None", // Headless service for StatefulSet390},391}392}393394func (r *DatabaseReconciler) updateStatus(ctx context.Context, db *mycompanyv1.Database,395phase string, ready bool, message string) (ctrl.Result, error) {396db.Status.Phase = phase397db.Status.Ready = ready398db.Status.Message = message399db.Status.Endpoint = fmt.Sprintf("%s.%s.svc.cluster.local:5432", db.Name, db.Namespace)400return ctrl.Result{}, r.Status().Update(ctx, db)401}402403func (r *DatabaseReconciler) cleanupResources(ctx context.Context, db *mycompanyv1.Database) error {404// Custom cleanup logic (e.g., backup before deletion)405return nil406}407408func (r *DatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error {409return ctrl.NewControllerManagedBy(mgr).410For(&mycompanyv1.Database{}).411Owns(&appsv1.StatefulSet{}).412Owns(&corev1.Service{}).413Complete(r)414}415```416417## Operator RBAC418419```yaml420apiVersion: v1421kind: ServiceAccount422metadata:423name: database-operator424namespace: operators425---426apiVersion: rbac.authorization.k8s.io/v1427kind: ClusterRole428metadata:429name: database-operator-role430rules:431- apiGroups: ["mycompany.io"]432resources: ["databases"]433verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]434- apiGroups: ["mycompany.io"]435resources: ["databases/status"]436verbs: ["get", "update", "patch"]437- apiGroups: ["mycompany.io"]438resources: ["databases/finalizers"]439verbs: ["update"]440- apiGroups: ["apps"]441resources: ["statefulsets"]442verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]443- apiGroups: [""]444resources: ["services", "configmaps", "secrets"]445verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]446- apiGroups: [""]447resources: ["persistentvolumeclaims"]448verbs: ["get", "list", "watch"]449- apiGroups: [""]450resources: ["events"]451verbs: ["create", "patch"]452---453apiVersion: rbac.authorization.k8s.io/v1454kind: ClusterRoleBinding455metadata:456name: database-operator-rolebinding457roleRef:458apiGroup: rbac.authorization.k8s.io459kind: ClusterRole460name: database-operator-role461subjects:462- kind: ServiceAccount463name: database-operator464namespace: operators465```466467## Operator Deployment468469```yaml470apiVersion: apps/v1471kind: Deployment472metadata:473name: database-operator474namespace: operators475labels:476app: database-operator477spec:478replicas: 1479selector:480matchLabels:481app: database-operator482template:483metadata:484labels:485app: database-operator486spec:487serviceAccountName: database-operator488securityContext:489runAsNonRoot: true490seccompProfile:491type: RuntimeDefault492containers:493- name: manager494image: myregistry.io/database-operator:v1.0.0495args:496- --leader-elect497- --health-probe-bind-address=:8081498securityContext:499allowPrivilegeEscalation: false500capabilities:501drop: ["ALL"]502readOnlyRootFilesystem: true503ports:504- containerPort: 8080505name: metrics506livenessProbe:507httpGet:508path: /healthz509port: 8081510initialDelaySeconds: 15511periodSeconds: 20512readinessProbe:513httpGet:514path: /readyz515port: 8081516initialDelaySeconds: 5517periodSeconds: 10518resources:519limits:520cpu: 500m521memory: 128Mi522requests:523cpu: 10m524memory: 64Mi525```526527## Operator SDK Commands528529```bash530# Initialize new operator project531operator-sdk init --domain mycompany.io --repo github.com/mycompany/database-operator532533# Create new API (CRD + controller)534operator-sdk create api --group mycompany --version v1 --kind Database --resource --controller535536# Generate manifests (CRD, RBAC)537make manifests538539# Generate deep copy methods540make generate541542# Build operator image543make docker-build docker-push IMG=myregistry.io/database-operator:v1.0.0544545# Deploy to cluster546make deploy IMG=myregistry.io/database-operator:v1.0.0547548# Undeploy549make undeploy550```551552## Best Practices5535541. **Use finalizers** for cleanup of external resources before CR deletion5552. **Set owner references** so owned resources are garbage collected with the CR5563. **Implement idempotent reconciliation** - same input should produce same output5574. **Use status subresource** to separate desired state (spec) from observed state (status)5585. **Add validation** via OpenAPI schema or webhooks5596. **Emit events** for significant state changes5607. **Use leader election** for high availability5618. **Set resource limits** on the operator deployment5629. **Follow least privilege** RBAC principles56310. **Test with envtest** for unit testing controllers564