The Cloud Trap & The Operator Revolution
When companies migrate to Kubernetes on public clouds (AWS EKS, GKE), they often fall into a massive financial trap: relying entirely on expensive "Managed Services" (like AWS RDS for PostgreSQL or MSK for Kafka) because stateful applications are notoriously difficult to manage manually.
To escape this cloud tax, elite SRE teams build Kubernetes Operators. By writing a custom Operator in Golang, you are essentially encoding human operational knowledge (how to backup, restore, scale, and heal an application) directly into the cluster. This allows you to host complex stateful workloads on highly cost-effective Dedicated Bare Metal Servers, effectively building your own "Private Managed Cloud."
In this tutorial, we will decode the golang kubernetes controller vs operator paradigm, show you how to scaffold a project with Kubebuilder, securely test it in a test kubernetes operator circleci pipeline, and explain why Bare Metal is the ultimate hardware destination for these workloads.
Step 1: CRDs and Scaffolding with Kubebuilder
To understand how to create custom resource definition kubernetes, we use Kubebuilder, the official framework built by Google to bootstrap Kubernetes APIs.
Let's initialize our Operator project in Go:
# Initialize the Go module
mkdir my-first-operator && cd my-first-operator
go mod init github.com/yourname/my-first-operator
# Scaffold the Kubebuilder project
kubebuilder init --domain irexta.com --repo github.com/yourname/my-first-operator
# Create the Custom Resource Definition (CRD) and Controller
kubebuilder create api --group apps --version v1alpha1 --kind WebApp This generates the schema in api/v1alpha1/webapp_types.go. You must modify the Spec to define your desired state, and then run make generate manifests to compile the actual YAML CRDs.
Step 2: Writing the Brain - The Reconciliation Loop
The heart of any Operator is the Reconciliation Loop. It constantly compares the Actual State of the cluster against your Desired State (defined in your CRD), and takes action.
Here is the production-ready Go logic inside internal/controller/webapp_controller.go, fortified with RBAC markers, Garbage Collection, and the TypeMeta SSA fix:
// +kubebuilder:rbac:groups=apps.irexta.com,resources=webapps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps.irexta.com,resources=webapps/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete
func (r *WebAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) var webApp appsv1alpha1.WebApp if err := r.Get(ctx, req.NamespacedName, &webApp); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // [Production Best Practice]: Ensure proper Garbage Collection pod := buildPodForWebApp(&webApp) if err := ctrl.SetControllerReference(&webApp, pod, r.Scheme); err != nil { return ctrl.Result{}, err } // [Crucial Step]: Prevent API Server Spam! // Check if Pod exists before attempting creation. foundPod := &corev1.Pod{} err := r.Get(ctx, types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, foundPod) if err != nil && errors.IsNotFound(err) { logger.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name) if err := r.Create(ctx, pod); err != nil { logger.Error(err, "Failed to create new Pod") return ctrl.Result{}, err } } else if err != nil { logger.Error(err, "Failed to get Pod") return ctrl.Result{}, err } // [Modern SRE Fix]: True Server-Side Apply (SSA) to bypass HTTP 409 webApp.Status.Phase = "Running" // [CRITICAL FIX]: r.Get() strips TypeMeta, but SSA requires it! // If we don't re-inject it, the Operator will crash with "missing apiVersion or kind". webApp.APIVersion = "apps.irexta.com/v1alpha1" webApp.Kind = "WebApp" // Using client.Apply forces ownership and lets the API server handle conflicts if err := r.Status().Patch(ctx, &webApp, client.Apply, client.FieldOwner("webapp-operator"), client.ForceOwnership); err != nil { logger.Error(err, "Failed to patch status via SSA") return ctrl.Result{RequeueAfter: time.Second * 5}, err } return ctrl.Result{}, nil
} Step 3: Test Kubernetes Operator in CircleCI
Because Operators manipulate the core Kubernetes API, a bad code commit can accidentally delete critical cluster infrastructure. We must test it automatically using a test kubernetes operator circleci pipeline.
We don't need to spin up a full cloud cluster just to run unit tests. Kubebuilder provides envtest, a tool that simulates a local control plane (etcd and kube-apiserver) directly inside the CircleCI Docker executor.
Here is the ultimate, bulletproof .circleci/config.yml (avoiding fragile hardcoded paths):
version: 2.1
executors: go-executor: docker: - image: cimg/go:1.24.0
jobs: lint-and-envtest: executor: go-executor steps: - checkout - run: name: Run Go Vet & Static Analysis command: go vet ./... - run: name: Install EnvTest & Control Plane Binaries command: | go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest setup-envtest use 1.30.0 --os linux --arch amd64 --bin-dir ./testbin - run: name: Execute Reconciliation Integration Tests command: | # [Bulletproof SRE]: Never hardcode paths. Use dynamic output from setup-envtest. export KUBEBUILDER_ASSETS=$(setup-envtest use 1.30.0 --os linux --arch amd64 --bin-dir ./testbin -p path) make test
workflows: operator-pipeline: jobs: - lint-and-envtest Step 4: Security & Production Hardening
Deploying an Operator into a production environment requires shifting your focus from application code to system-level security.
The SRE Solution: Why Operators Demand Bare Metal
We've built the Operator and tested it in CircleCI. Now comes the architectural decision: deploy kubernetes operator bare metal vs Public Cloud VMs.
On public clouds like AWS or GCP, your cluster's etcd is backed by network-attached block storage (like EBS), which is heavily throttled for IOPS. Furthermore, the hypervisor virtualization layer adds critical microsecond delays to every API call. When an Operator manages thousands of Custom Resources, this disk I/O bottleneck causes the Reconciliation Loop to severely lag, leading to delayed scaling and failing automated backups.
To unlock the true power of infrastructure automation, SREs deploy their control planes and Operators on iRexta Dedicated Bare Metal Servers. By eliminating the hypervisor tax and utilizing direct, local PCIe NVMe drives for etcd, your Kubernetes Operators can execute thousands of state changes per second with zero latency. You stop renting bloated cloud Managed Services, and start owning your automated, high-performance infrastructure.
Kubernetes Operator Implementation: FAQ
client.Apply and client.FieldOwner. However, you must manually re-inject TypeMeta (APIVersion/Kind) as r.Get() strips it out. envtest tool provided by Kubebuilder. It spins up a localized, simulated control plane (etcd and kube-apiserver) inside your CI job, allowing you to run integration tests rapidly without needing a full, live Kubernetes cluster.