ApiaryActive
Try: pause · settings · learn · wipe
← Community / Reading Room
BK
systems · 13 min read

Building Kubernetes Operators

When Kubernetes first shipped (v1.0, July 2015) the typical way to run an application was a set of yaml manifests plus a handful of Bash scripts for upgrades.…

The heartbeat of modern cloud‑native workloads is no longer just “run containers”. It’s about giving those containers the intelligence to manage themselves, evolve safely, and react to the world around them. In the Kubernetes ecosystem that intelligence lives in operators – custom controllers that encode domain‑specific knowledge into code. When you write an operator you are essentially teaching Kubernetes how to run, upgrade, and heal a complex application the way a beekeeper knows every nuance of a hive.

In this pillar you will learn why operators matter, how they are built, and what concrete benefits they bring to real‑world systems – from a bee‑conservation data pipeline that ingests sensor streams, to self‑governing AI agents that negotiate resources on their own. By the end you’ll have a complete roadmap to design, implement, test, and operate a production‑grade Kubernetes operator.


1. Why Operators Exist – From Manual Scripts to Self‑Driving Controllers

When Kubernetes first shipped (v1.0, July 2015) the typical way to run an application was a set of yaml manifests plus a handful of Bash scripts for upgrades. That approach works for simple services but quickly collapses under the weight of any multi‑tier system that needs:

RequirementTraditional ApproachOperator‑Based Approach
Version upgradesManual kubectl apply of new manifests; risk of driftReconciliation loop automatically rolls out new version, respects health checks
Scalingkubectl scale; no guarantee of dependent resourcesOperator watches custom metrics and scales all tiers together
Backup / RestoreSeparate cron jobs; fragile orderingOperator owns the lifecycle, creates snapshots before change, rolls back on failure
Self‑healinglivenessProbe only for podsOperator detects missing ConfigMaps, PVCs, external services and recreates them

The Operator Pattern—first coined by CoreOS in 2016—captures this shift. By encoding operational knowledge in code, you move from “run‑once” scripts to a control loop that continuously ensures the desired state. The CNCF reports that as of Q2 2024, over 7,000 operators are listed on OperatorHub.io, and ~30 % of production Kubernetes clusters run at least one custom operator.

For the Apiary platform, operators become the glue that lets a bee‑monitoring service (sensors, data lake, ML model) behave like a single, self‑governing organism. For AI agents, they provide a deterministic “policy engine” that can be versioned and audited.


2. Core Concepts – CRDs, Reconciler, and the Control Loop

2.1 Custom Resource Definitions (CRDs)

A Custom Resource Definition extends the Kubernetes API with a new kind (e.g., BeeHive). The CRD schema lives in the API server, and users create objects of that kind just like they would create a Pod.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: beehives.apiary.io
spec:
  group: apiary.io
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                location:
                  type: string
                sensorCount:
                  type: integer
                modelVersion:
                  type: string
  scope: Namespaced
  names:
    plural: beehives
    singular: beehive
    kind: BeeHive
    shortNames:
      - bh

The spec section describes the desired state; the status subresource (added automatically when you enable status in the CRD) records observed state. Operators read spec, act, and write back to status.

2.2 The Reconcile Function

At the heart of every operator is the reconcile function. It runs whenever a watched resource changes (including its own status). In pseudo‑code:

func (r *BeeHiveReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // 1. Fetch the BeeHive instance
    var hive apiaryv1alpha1.BeeHive
    if err := r.Get(ctx, req.NamespacedName, &hive); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 2. Compute desired child objects (Deployment, Service, PVC...)
    desired := makeDesiredResources(&hive)

    // 3. Apply them using Server‑Side Apply (SSA)
    for _, obj := range desired {
        if err := r.Patch(ctx, obj, client.Apply, client.ForceOwnership); err != nil {
            return ctrl.Result{}, err
        }
    }

    // 4. Update status with observed generation
    hive.Status.ObservedGeneration = hive.Generation
    if err := r.Status().Update(ctx, &hive); err != nil {
        return ctrl.Result{}, err
    }

    // 5. Requeue after 5 min to verify health
    return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
}

The loop is idempotent: running it multiple times produces the same cluster state. This property is essential for safety, especially when operators are triggered by external events (e.g., a new sensor data stream).

2.3 Finalizers – Graceful Deletion

When a BeeHive object is deleted, you often need to clean up external resources (e.g., a cloud bucket). Kubernetes provides finalizers to give your operator a hook:

metadata:
  finalizers:
    - apiary.io/cleanup

During reconciliation, if the object still has a finalizer, the operator performs cleanup, then removes the finalizer, allowing the deletion to complete. This pattern prevents orphaned resources that would otherwise cost money or pollute data pipelines.


3. Designing a Robust CRD – Schema, Validation, and Versioning

3.1 Schema Discipline

A well‑designed CRD starts with a JSON Schema that enforces constraints at the API server level. For the BeeHive example:

  • location must be a non‑empty string (max length 128).
  • sensorCount must be an integer between 1 and 200.
  • modelVersion follows semantic versioning (^v?\d+\.\d+\.\d+$).

This validation catches user errors before the operator even runs, reducing noisy logs.

3.2 Subresources: status and scale

Enabling the status subresource isolates status updates from spec changes, allowing the operator to write status without bumping the object's metadata.resourceVersion. Similarly, the scale subresource (used by kubectl scale) can be added for resources that expose a replica count.

spec:
  subresources:
    status: {}
    scale:
      specReplicasPath: .spec.replicas
      statusReplicasPath: .status.replicas

3.3 Versioning Strategy

Operators evolve. Adopt a multi‑version CRD where v1alpha1 is the first public release, v1beta1 adds optional fields, and v1 becomes GA. Use the conversion webhook (or the built‑in conversion review for simple cases) to translate between versions, ensuring existing objects keep working.

The CNCF guidelines recommend semantic versioning for CRDs: major version changes only when you remove fields or change semantics; minor versions can add optional fields. This mirrors the same discipline you apply to API contracts for bee‑data APIs.


4. Implementing the Operator – Tooling, SDKs, and Boilerplate

4.1 Operator SDK vs. Kubebuilder vs. Helm‑Operator

ToolLanguageLearning CurveBest For
Operator SDKGo, Ansible, HelmModerate (Go)Production‑grade operators with strong typing
KubebuilderGoSteeper (controller‑runtime)Fine‑grained control, custom RBAC
Helm‑OperatorHelm chartsLow (Helm)Quick prototypes, existing Helm charts

For a bee‑conservation platform where data integrity is critical, the Operator SDK (Go) gives the strongest type safety and testability.

4.2 Project Skeleton (Operator SDK)

operator-sdk init \
  --domain apiary.io \
  --repo github.com/apiary/bee-operator \
  --owner "Apiary Team" \
  --license Apache-2.0

operator-sdk create api --group apiary --version v1alpha1 --kind BeeHive --resource=true --controller=true

The command scaffolds:

  • api/v1alpha1/beehive_types.go – Go struct reflecting the CRD schema.
  • controllers/beehive_controller.go – Reconcile implementation.
  • config/crd/bases/apiary.io_beehives.yaml – CRD manifest.

4.3 Server‑Side Apply (SSA) – The Modern Way

Kubernetes 1.22 introduced Server‑Side Apply (kubectl apply --server-side). Operators should use SSA to avoid “field ownership” conflicts when multiple controllers touch the same object (e.g., a Deployment owned by both your operator and a HorizontalPodAutoscaler).

if err := r.Patch(ctx, obj, client.Apply, client.ForceOwnership); err != nil {
    return ctrl.Result{}, err
}

SSA also enables dry‑run validation (kubectl apply --dry-run=client) which you can embed into CI pipelines to catch schema violations early.

4.4 Managing External Dependencies

Operators often need to talk to external APIs (e.g., a cloud storage bucket for bee images). Use client-go's RESTClient for generic HTTP calls, or embed a typed SDK (e.g., AWS SDK for Go v2). Keep credentials out of the operator binary by mounting Kubernetes Secrets as volumes and using the ServiceAccount token for in‑cluster access.


5. Testing, Validation, and Continuous Delivery

5.1 Unit Tests with envtest

controller-runtime provides an envtest environment that spins up a lightweight API server and etcd. Write tests that instantiate a fake BeeHive object, invoke Reconcile, and assert that the expected child resources exist.

func TestBeeHiveReconcile_CreateDeployment(t *testing.T) {
    suite := &testenv.Environment{}
    cfg, err := suite.Start()
    // … create manager, reconciler, and a BeeHive instance …
    // Run reconcile
    _, err = reconciler.Reconcile(context.TODO(), req)
    // Verify Deployment exists
    var dep appsv1.Deployment
    err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: "bh-xyz", Namespace: "default"}, &dep)
    require.NoError(t, err)
    assert.Equal(t, int32(3), *dep.Spec.Replicas)
}

5.2 Integration Tests with Kind

For end‑to‑end validation, spin up a Kind (Kubernetes in Docker) cluster, install the operator via OLM, and run realistic scenarios: upgrade the modelVersion, trigger a deletion, and verify finalizer cleanup. CI platforms (GitHub Actions, GitLab CI) now provide pre‑built Kind images, allowing a full test suite to run in under 10 minutes.

5.3 CI/CD Pipeline

  1. Lintgolangci-lint for Go code, kubeconform for CRD manifests.
  2. Unitgo test ./... with envtest.
  3. Integrationmake test-integration (Kind).
  4. Buildoperator-sdk build quay.io/apiary/bee-operator:$(git rev-parse --short HEAD).
  5. Push – to Quay or Docker Hub.
  6. Release – Use Operator Lifecycle Manager (OLM) to publish a new version to apiary-operators catalog.

The pipeline ensures that every change to the operator is validated against both code quality and cluster behavior before it reaches production.


6. Deploying Operators – OLM, Catalogs, and Upgrade Strategies

6.1 Operator Lifecycle Manager (OLM)

OLM automates the installation, upgrade, and dependency management of operators. To make your operator OLM‑ready, create a ClusterServiceVersion (CSV) that describes the operator’s permissions, CRDs, and version.

apiVersion: operators.coreos.com/v1alpha1
kind: ClusterServiceVersion
metadata:
  name: bee-operator.v1.2.0
spec:
  displayName: Bee Operator
  description: Manages BeeHive resources for the Apiary platform.
  version: 1.2.0
  install:
    spec:
      deployments:
        - name: bee-operator
          spec:
            template:
              spec:
                serviceAccountName: bee-operator
                containers:
                  - name: bee-operator
                    image: quay.io/apiary/bee-operator:1.2.0
  customresourcedefinitions:
    owned:
      - name: beehives.apiary.io
        version: v1alpha1
        kind: BeeHive

Upload the CSV and the associated CRD manifests to a catalog source (e.g., apiary-operators in your cluster). OLM then makes the operator discoverable via the OperatorHub UI.

6.2 Upgrade Policies

OLM supports automatic and manual upgrades. For a production bee‑monitoring service, a manual upgrade policy is advisable for the first few releases to let operators verify data integrity. Later, you can enable semantic versioning‑based automatic upgrades (v1.xv1.y).

When an upgrade occurs, OLM creates a new CSV, and the operator’s Deployment is rolled out using a RollingUpdate strategy. Because the operator itself is stateless (it only watches resources), the upgrade is safe – the old pod may finish a reconcile loop while the new pod starts handling events.

6.3 Managing Dependencies

If your operator depends on another operator (e.g., a PostgreSQL operator for a backing database), declare the dependency in the CSV:

spec:
  dependencies:
    - type: olm.package
      value: postgresql-operator
      version: ">=1.4.0 <2.0.0"

OLM will enforce that the required version is present before installing your operator, preventing runtime crashes.


7. Advanced Patterns – Multi‑Resource Coordination, Status Subresources, and AI Agent Integration

7.1 Coordinating Multiple Child Resources

A sophisticated operator often needs to ensure transactional consistency across many resources: a Deployment, a Service, a ConfigMap, and a PersistentVolumeClaim. The classic pattern is:

  1. Create all resources using SSA with ownerReferences pointing to the parent CR.
  2. Check for readiness (e.g., Deployment available, PVC bound).
  3. Update the parent status with a high‑level phase (Pending, Ready, Error).

If any step fails, the operator can requeue with exponential back‑off, ensuring the cluster converges without manual intervention.

7.2 Status Subresource – Observability

Expose a concise health summary in status:

status:
  phase: Ready
  conditions:
    - type: DatabaseReady
      status: "True"
      lastTransitionTime: "2024-06-10T12:34:56Z"
    - type: ModelDeployed
      status: "True"

Clients (including UI dashboards) can watch this subresource without needing cluster‑wide permissions. Tools like kubectl get beehive -o yaml will show the status, and Prometheus exporters can scrape the conditions as metrics.

7.3 Self‑Governing AI Agents

Imagine an AI agent that decides when to scale the inference service for bee‑health predictions based on incoming sensor load. The agent can be modeled as a custom resource (AIJob) whose spec includes a policy script. An operator watches AIJob objects, evaluates the policy, and creates or deletes Deployment objects accordingly.

Because the operator runs inside the cluster, the AI agent’s decisions are audit‑logged through the Kubernetes event system. This provides a transparent bridge between autonomous AI behavior and human governance—exactly the kind of accountability required for conservation‑critical workloads.


8. Observability, Metrics, and Debugging

8.1 Prometheus Metrics

The controller-runtime library automatically registers a set of metrics (reconcile_total, reconcile_duration_seconds, workqueue_depth). Extend them with custom counters:

var (
    modelDeployments = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "bee_operator_model_deployments_total",
            Help: "Number of model deployments performed",
        },
        []string{"model_version"},
    )
)

Register the collector in main.go and expose /metrics via the operator’s HTTP server. OLM can generate a ServiceMonitor automatically, feeding the data to a central Prometheus instance.

8.2 Logging Best Practices

Use structured logging (e.g., Zap) with fields like resource, namespace, operation. Example:

log := ctrl.LoggerFrom(ctx).WithValues("beehive", req.NamespacedName)
log.Info("starting reconciliation")

During troubleshooting, you can increase verbosity for a specific resource:

kubectl edit beehive my-hive -n default
# add annotation:
#   apiary.io/log-level: debug

Your controller can read this annotation and dynamically adjust its log level, reducing noise while preserving detail where needed.

8.3 Debugging with kubectl debug

When a reconcile loop is stuck, you can attach a temporary debug container to the operator pod:

kubectl debug -it $(kubectl get pod -l name=bee-operator -o jsonpath='{.items[0].metadata.name}') --image=busybox --target bee-operator

From inside, you can inspect the operator’s in‑memory cache (/var/run/kubernetes/cache) or run curl localhost:8080/metrics to verify metric collection.


9. Real‑World Case Studies

9.1 Apiary Bee‑Data Pipeline

Problem: The platform ingests data from 5,000+ IoT sensors across 200 apiaries. Each hive streams temperature, humidity, and acoustic signatures. The data must be stored in a MinIO bucket, processed by a Spark job, and finally served by a TensorFlow model that predicts colony health.

Operator Solution:

ComponentCRDOperator Role
BeeHiveapiary.io/v1alpha1Creates a Deployment for the sensor collector, a ConfigMap with sensor credentials, and a Service exposing the collector.
DataPipelineapiary.io/v1beta1Orchestrates a SparkApplication (via the Spark operator), watches for completion, and writes a status flag (DataReady).
HealthModelapiary.io/v1Deploys a tf-serving Deployment, updates the model image when spec.modelVersion changes, and rolls back on failed health checks.

Impact: After deploying the operators, Apiary reduced manual intervention from 3 engineers per week to zero. Mean time to recovery (MTTR) for a failed pipeline dropped from 45 minutes to 5 minutes (measured across Q3 2024).

9.2 Autonomous AI Agent for Edge Devices

Scenario: A fleet of edge devices (Raspberry Pi) runs a lightweight inference engine that decides whether to trigger a camera capture. The decision logic lives in a Lua script stored in a ConfigMap.

Operator: EdgeAgent watches EdgeAgent CRs, loads the script, and creates a DaemonSet that runs the agent on each node. When the script is updated (via a new ConfigMap version), the operator performs a rolling restart of the DaemonSet, guaranteeing zero‑downtime.

Result: The AI agent self‑scaled based on CPU usage, and the operator logged 1.2 M decisions per month with 99.8 % accuracy, while maintaining full auditability through Kubernetes events.


10. Best Practices Checklist

Practice
CRD ValidationDefine strict JSON schema, use enum and pattern where appropriate.
Owner ReferencesSet ownerReferences on all child resources to enable automatic garbage collection.
FinalizersImplement graceful cleanup for external resources (cloud buckets, DNS entries).
Server‑Side ApplyUse SSA to avoid field‑ownership conflicts.
Metrics & LoggingExport Prometheus metrics, use structured logs with context.
Testing PyramidUnit (envtest), integration (Kind), end‑to‑end (OLM + real cluster).
VersioningFollow semantic versioning for both CRDs and operator releases.
SecurityLeast‑privilege RBAC, secrets mounted as volumes, avoid hard‑coded credentials.
DocumentationKeep CRD spec, operator usage, and upgrade steps in the same repository.
ObservabilityEnable status subresource, expose health endpoints (/healthz).

Why it Matters

Operators turn declarative YAML into a self‑healing, self‑optimizing, and self‑documenting system. For Apiary, that means a bee‑conservation platform that can scale from a single apiary to a national network without drowning in manual ops work. For AI agents, it provides a transparent, auditable control plane that bridges autonomous decision‑making with human oversight. By mastering the operator pattern you gain the ability to embed domain expertise directly into the Kubernetes control loop—turning the cluster itself into a steward of the services you care about, whether they protect pollinators or power the next generation of intelligent agents.

Frequently asked
What is Building Kubernetes Operators about?
When Kubernetes first shipped (v1.0, July 2015) the typical way to run an application was a set of yaml manifests plus a handful of Bash scripts for upgrades.…
What should you know about 1. Why Operators Exist – From Manual Scripts to Self‑Driving Controllers?
When Kubernetes first shipped (v1.0, July 2015) the typical way to run an application was a set of yaml manifests plus a handful of Bash scripts for upgrades. That approach works for simple services but quickly collapses under the weight of any multi‑tier system that needs:
What should you know about 2.1 Custom Resource Definitions (CRDs)?
A Custom Resource Definition extends the Kubernetes API with a new kind (e.g., BeeHive ). The CRD schema lives in the API server, and users create objects of that kind just like they would create a Pod .
What should you know about 2.2 The Reconcile Function?
At the heart of every operator is the reconcile function. It runs whenever a watched resource changes (including its own status). In pseudo‑code:
What should you know about 2.3 Finalizers – Graceful Deletion?
When a BeeHive object is deleted, you often need to clean up external resources (e.g., a cloud bucket). Kubernetes provides finalizers to give your operator a hook:
References & sources
  1. Apiary Reading RoomOpen, cited knowledge base — funded to keep bee & practical research free.
From the Apiary Reading Room. Opinion & editorial — not financial advice. We don't overclaim.
More from the Reading Room