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=true

Create 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: 8080

external-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.com

Strimzi (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: 10Gi

Apply 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-namespace

Providers#

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: credentials

Compositions, 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.xlarge

A 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: postgres

When 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.