The Operator Pattern: Extending the Kubernetes API
It’s 2017, and Kubernetes has won the container orchestration wars. But while K8s is great for stateless apps, managing complex stateful systems like databases (Postgres, Cassandra) is still a manual nightmare. Enter the Operator Pattern, coined by the team at CoreOS.
The Control Loop
At its heart, Kubernetes is a collection of control loops. A controller watches the current state of the cluster and makes changes to bring it closer to the "desired state." An Operator is simply a domain-specific controller that knows how to manage a specific application.
Custom Resource Definitions (CRDs)
In 2017, we're moving from ThirdPartyResources to Custom Resource Definitions (CRDs). This allows us to define our own objects in the K8s API.
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: databases.example.com
spec:
group: example.com
version: v1
scope: Namespaced
names:
plural: databases
singular: database
kind: Database
shortNames:
- db
Now, a user can run kubectl apply -f my-db.yaml just like they would for a Deployment.
The Controller Logic
The "Operator" is a pod running in your cluster that watches for events on your new Database resource. When it sees a new one, it doesn't just spawn a pod; it might:
- Provision a Persistent Volume.
- Start a primary database pod.
- Wait for the primary to be ready, then start replicas.
- Configure replication.
- Setup automated backups.
Implementing in Go
Most operators are written in Go using client-go or the controller-runtime library.
func (r *ReconcileDatabase) Reconcile(request reconcile.Request) (reconcile.Result, error) {
// Fetch the Database instance
db := &examplev1.Database{}
err := r.client.Get(context.TODO(), request.NamespacedName, db)
if err != nil {
return reconcile.Result{}, err
}
// Define the desired state (e.g., a StatefulSet)
found := &appsv1.StatefulSet{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: db.Name, Namespace: db.Namespace}, found)
if err != nil && errors.IsNotFound(err) {
// Create the StatefulSet
dep := r.deploymentForDatabase(db)
err = r.client.Create(context.TODO(), dep)
return reconcile.Result{Requeue: true}, nil
}
return reconcile.Result{}, nil
}
The Operator pattern turns "Operations Knowledge" (how to scale a DB, how to handle a failover) into Code. This is the ultimate realization of Infrastructure as Code.