TL;DR
Master Kubernetes Operators with this comprehensive guide covering custom controllers, operator patterns, and best practices for extending Kubernetes functionality
Kubernetes Operators: A Complete Development Guide
Kubernetes Operators extend the platform's functionality by automating complex application management tasks. This guide explores operator development patterns, implementation strategies, and best practices.
$1
`` graph TB
subgraph "Kubernetes API"
A[Custom Resource Definition]
B[Custom Controller]
C[Reconciliation Loop]
end
subgraph "Application Logic"
D[Resource Management]
E[State Management]
F[Lifecycle Hooks]
end
subgraph "Runtime"
G[Operator Pod]
H[Managed Resources]
I[Status Updates]
end
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
I --> C
classDef api fill:#1a73e8,stroke:#fff,color:#fff
classDef logic fill:#34a853,stroke:#fff,color:#fff
classDef runtime fill:#fbbc04,stroke:#fff,color:#fff
class A,B,C api
class D,E,F logic
class G,H,I runtime
mermaid
`
$1
$1
` apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.example.com
spec:
group: example.com
names:
kind: Database
listKind: DatabaseList
plural: databases
singular: database
shortNames:
- db
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
engine:
type: string
enum: ["postgres", "mysql", "mongodb"]
version:
type: string
storage:
type: string
pattern: '^[0-9]+Gi$'
replicas:
type: integer
minimum: 1
maximum: 5
required:
- engine
- version
- storage
status:
type: object
properties:
phase:
type: string
enum: ["Pending", "Running", "Failed"]
message:
type: string
yaml
`crd.yaml
$1
` apiVersion: example.com/v1
kind: Database
metadata:
name: production-db
spec:
engine: postgres
version: "14.5"
storage: "100Gi"
replicas: 3
yaml
`database.yaml
$1
$1
` // controller.ts
import { KubernetesObject } from '@kubernetes/client-node';
import { Controller, ResourceEventType } from '@kubernetes/operator-framework'; interface DatabaseSpec {
engine: string;
version: string;
storage: string;
replicas: number;
} interface DatabaseStatus {
phase: 'Pending' | 'Running' | 'Failed';
message: string;
} interface Database extends KubernetesObject {
spec: DatabaseSpec;
status: DatabaseStatus;
} class DatabaseController implements Controller {
private readonly client: any; constructor(client: any) {
this.client = client;
} async reconcile(obj: Database): Promise const { metadata, spec } = obj; try {
// Create StatefulSet
await this.createStatefulSet(metadata.name, spec);
// Create Service
await this.createService(metadata.name);
// Update status
await this.updateStatus(metadata.name, {
phase: 'Running',
message: 'Database is ready'
});
} catch (error) {
await this.updateStatus(metadata.name, {
phase: 'Failed',
message: error.message
});
}
} async cleanup(obj: Database): Promise const { metadata } = obj;
// Cleanup resources
await this.deleteStatefulSet(metadata.name);
await this.deleteService(metadata.name);
}
}
typescript
`
$1
` // resources.ts
import { V1StatefulSet, V1Service } from '@kubernetes/client-node'; class ResourceManager {
createStatefulSet(name: string, spec: DatabaseSpec): V1StatefulSet {
return {
apiVersion: 'apps/v1',
kind: 'StatefulSet',
metadata: {
name: },
spec: {
replicas: spec.replicas,
selector: {
matchLabels: {
app: name
}
},
template: {
metadata: {
labels: {
app: name
}
},
spec: {
containers: [{
name: 'database',
image: ports: [{
containerPort: this.getPort(spec.engine)
}],
volumeMounts: [{
name: 'data',
mountPath: '/data'
}]
}],
volumes: [{
name: 'data',
persistentVolumeClaim: {
claimName: }
}]
}
}
}
};
} createService(name: string): V1Service {
return {
apiVersion: 'v1',
kind: 'Service',
metadata: {
name: },
spec: {
selector: {
app: name
},
ports: [{
port: 5432,
targetPort: 5432
}]
}
};
} private getPort(engine: string): number {
const ports = {
postgres: 5432,
mysql: 3306,
mongodb: 27017
};
return ports[engine];
}
}
typescript
${name}-db
${spec.engine}:${spec.version},
${name}-pvc
${name}-svc
`
$1
$1
` domain: example.com
layout:
- go.kubebuilder.io/v3
projectName: database-operator
repo: github.com/example/database-operator
version: "3"
plugins:
manifests.sdk.operatorframework.io/v2: {}
scorecard.sdk.operatorframework.io/v2: {}
yaml
`PROJECT
$1
` // api/v1/database_types.go
package v1 import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) // DatabaseSpec defines the desired state
type DatabaseSpec struct {
Engine string Version string Storage string Replicas int32 } // DatabaseStatus defines the observed state
type DatabaseStatus struct {
Phase string Message string } // Database is the Schema for the databases API
type Database struct {
metav1.TypeMeta metav1.ObjectMeta go
json:"engine"
json:"version"
json:"storage"
json:"replicas"
json:"phase"
json:"message"
json:",inline"
json:"metadata,omitempty"
Spec DatabaseSpec json:"spec,omitempty" Status DatabaseStatus }
json:"status,omitempty"
`
$1
$1
` // controller.test.ts
import { DatabaseController } from './controller';
import { MockKubeClient } from './mocks'; describe('DatabaseController', () => {
let controller: DatabaseController;
let client: MockKubeClient; beforeEach(() => {
client = new MockKubeClient();
controller = new DatabaseController(client);
}); test('should create resources on reconcile', async () => {
const database = {
metadata: {
name: 'test-db'
},
spec: {
engine: 'postgres',
version: '14.5',
storage: '10Gi',
replicas: 1
}
}; await controller.reconcile(database); expect(client.getStatefulSet('test-db-db')).toBeDefined();
expect(client.getService('test-db-svc')).toBeDefined();
}); test('should cleanup resources', async () => {
const database = {
metadata: {
name: 'test-db'
}
}; await controller.cleanup(database); expect(client.getStatefulSet('test-db-db')).toBeUndefined();
expect(client.getService('test-db-svc')).toBeUndefined();
});
});
typescript
`
$1
` // controllers/database_controller_test.go
package controllers import (
"context"
"testing"
"time" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
) var _ = Describe("Database Controller", func() {
Context("When creating Database", func() {
It("Should create StatefulSet and Service", func() {
ctx := context.Background()
database := &DatabaseV1{
ObjectMeta: metav1.ObjectMeta{
Name: "test-db",
Namespace: "default",
},
Spec: DatabaseSpec{
Engine: "postgres",
Version: "14.5",
Storage: "10Gi",
Replicas: 1,
},
} Expect(k8sClient.Create(ctx, database)).Should(Succeed()) statefulSet := &appsv1.StatefulSet{}
Eventually(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{
Name: "test-db-db",
Namespace: "default",
}, statefulSet)
return err == nil
}, time.Second*10, time.Second).Should(BeTrue()) service := &corev1.Service{}
Eventually(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{
Name: "test-db-svc",
Namespace: "default",
}, service)
return err == nil
}, time.Second*10, time.Second).Should(BeTrue())
})
})
})
go
`
$1
$1
` apiVersion: apps/v1
kind: Deployment
metadata:
name: database-operator
spec:
replicas: 1
selector:
matchLabels:
name: database-operator
template:
metadata:
labels:
name: database-operator
spec:
serviceAccountName: database-operator
containers:
- name: operator
image: example.com/database-operator:v1.0.0
command:
- database-operator
imagePullPolicy: Always
env:
- name: WATCH_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: OPERATOR_NAME
value: "database-operator"
yaml
`operator.yaml
$1
` apiVersion: v1
kind: ServiceAccount
metadata:
name: database-operator ---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: database-operator
rules:
- ""
resources:
- pods
- services
- endpoints
- persistentvolumeclaims
verbs:
- '*'
- apps
resources:
- deployments
- statefulsets
verbs:
- '*'
- example.com
resources:
- databases
- databases/status
verbs:
- '*' ---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: database-operator
subjects:
name: database-operator
roleRef:
kind: Role
name: database-operator
apiGroup: rbac.authorization.k8s.io
yaml
``rbac.yaml
$1
$1
| Practice | Description | Benefit |
|---|---|---|
| Idempotency | Consistent results | Reliability |
| Status Updates | Resource state | Observability |
| Error Handling | Graceful recovery | Resilience |
$1
$1
| Issue | Cause | Solution |
|---|---|---|
| Reconcile Loop | Resource conflicts | Check ownership |
| RBAC Issues | Missing permissions | Update roles |
| Resource Leaks | Cleanup failures | Implement finalizers |
$1
1. [Operator SDK Documentation](https://sdk.operatorframework.io/)
2. [Kubernetes Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/)
3. [Controller Runtime](https://github.com/kubernetes-sigs/controller-runtime)
4. [Operator Pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
5. [Operator Best Practices](https://cloud.google.com/blog/products/containers-kubernetes/best-practices-for-building-kubernetes-operators-and-custom-controllers)
6. [Testing Operators](https://sdk.operatorframework.io/docs/building-operators/golang/testing/)
$1
Why This Matters
Understanding the business and technical context helps you make informed decisions rather than blindly following patterns.
Trade-offs to Consider
Every architectural decision involves trade-offs. Consider your specific requirements, team expertise, and scale when evaluating options.
When NOT to Use This
Knowing when a solution doesn't apply is as valuable as knowing when it does. Consider alternatives for your specific situation.
Decision Framework
Use this framework to evaluate whether this approach is right for your use case based on your specific constraints and requirements.