Kubernetes Operators and Crossplane#
The Operator Pattern#
An operator is a CRD (Custom Resource Definition) paired with a controller. The CRD defines a new resource type (like Certificate or KafkaCluster). The controller watches for instances of that CRD and reconciles actual state to match desired state. This is the same reconciliation loop that powers Deployments, extended to anything.
Operators encode operational knowledge into software. Instead of a runbook with 47 steps to create a Kafka cluster, you declare what you want and the operator handles creation, scaling, upgrades, and failure recovery.
Common Operators#
cert-manager#
Automates TLS certificate lifecycle – requests from Let’s Encrypt, stores as Kubernetes Secrets, renews before expiry.
helm repo add jetstack https://charts.jetstack.io
helm upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set crds.enabled=trueCreate a ClusterIssuer and annotate your Ingress:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: payments-api
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- payments.example.com
secretName: payments-tls
rules:
- host: payments.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: payments-api
port:
number: 8080external-dns#
Syncs Kubernetes Ingress and Service resources to DNS providers (Route53, CloudFlare, Google Cloud DNS) automatically.
helm upgrade --install external-dns bitnami/external-dns \
--namespace external-dns --create-namespace \
--set provider=aws \
--set domainFilters[0]=example.comStrimzi (Apache Kafka)#
Manages Kafka clusters, topics, and users as Kubernetes resources.
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: payments-kafka
namespace: kafka
spec:
kafka:
replicas: 3
listeners:
- name: plain
port: 9092
type: internal
tls: false
storage:
type: persistent-claim
size: 50Gi
zookeeper:
replicas: 3
storage:
type: persistent-claim
size: 10GiApply this and Strimzi creates StatefulSets, Services, PVCs, ConfigMaps, and handles rolling upgrades on spec changes.
Crossplane: Infrastructure as Kubernetes Resources#
Crossplane lets you provision cloud infrastructure (RDS, S3, VPCs) using kubectl apply. Platform teams define abstractions; application teams consume them through Claims.
Installation#
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm upgrade --install crossplane crossplane-stable/crossplane \
--namespace crossplane-system --create-namespaceProviders#
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws
spec:
package: xpkg.upbound.io/upbound/provider-family-aws:v1.14.0
---
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-creds
key: credentialsCompositions, XRDs, and Claims#
XRD defines your platform API. Composition maps it to cloud resources. Claim is what developers use.
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.platform.example.com
spec:
group: platform.example.com
names:
kind: XDatabase
plural: xdatabases
claimNames:
kind: Database
plural: databases
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
size:
type: string
enum: ["small", "medium", "large"]
engine:
type: string
enum: ["postgres", "mysql"]
required: ["size", "engine"]
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: database-aws
spec:
compositeTypeRef:
apiVersion: platform.example.com/v1alpha1
kind: XDatabase
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta2
kind: Instance
spec:
forProvider:
region: us-east-1
engine: postgres
skipFinalSnapshot: true
providerConfigRef:
name: default
patches:
- type: FromCompositeFieldPath
fromFieldPath: "spec.engine"
toFieldPath: "spec.forProvider.engine"
- type: FromCompositeFieldPath
fromFieldPath: "spec.size"
toFieldPath: "spec.forProvider.instanceClass"
transforms:
- type: map
map:
small: db.t3.micro
medium: db.t3.medium
large: db.r6g.xlargeA developer applies this Claim and gets a managed database without knowing about RDS or instance classes:
apiVersion: platform.example.com/v1alpha1
kind: Database
metadata:
name: payments-db
namespace: payments
spec:
size: medium
engine: postgresWhen to Use What#
Operators – Stateful applications needing active lifecycle management (databases, message queues). They encode operational expertise.
Helm – Deploying applications with configurable defaults. Package manager, not a controller. No reconciliation after install.
Terraform – Infrastructure provisioning with mature state management and plan/apply workflow. Operates outside the cluster.
Crossplane – Self-service infrastructure abstractions consumed through kubectl. Everything stays in the Kubernetes API. Best when your team is Kubernetes-native.
The practical divide: Helm for apps in the cluster, operators for stateful workloads inside the cluster, Crossplane or Terraform for infrastructure outside it. Crossplane wins when you want a unified Kubernetes control plane. Terraform wins when you need broad ecosystem support and your team already knows HCL.