Beyond Infrastructure as Code: An Introduction to Crossplane

The quest for the perfect development environment often leads us down rabbit holes of increasing complexity. Recently, I found myself deep in the weeds of testing Crossplane compositions. What began as a "simple" playground—ArgoCD, vcluster-managed Kubernetes, and a web of local networking—quickly spiraled into a project that felt more like a chore than a lab.

In search of simplicity, I pivoted: I built a physical homelab with Raspberry Pis running Talos Linux. This transition gave me the focus needed to deeply explore how Crossplane is fundamentally changing the way we think about infrastructure management.

If you're interested in the hardware side, you can read about the Talos Linux setup on the home page. In this post, we’re going to look at the software: why Crossplane matters and how it works in practice.

The Declarative Landscape: Why Traditional IaC Falls Short

In contemporary Platform Engineering, we aren't exactly hurting for choice. Terraform, Pulumi, and Ansible have massive ecosystems and years of battle-testing. So, why do we need another tool? To answer that, we have to look at the "contract" between the platform and the developer.

Consider a standard requirement: providing an RDS database to a developer. Using a Terraform module, you might hand them something like this:

module "db" {
  source = "terraform-aws-modules/rds/aws"

  identifier = "demodb"

  engine               = "mysql"
  instance_class       = "db.t4g.large"
  allocated_storage     = 20

  db_subnet_group_name   = module.vpc.database_subnet_group
  vpc_security_group_ids = [module.security_group.security_group_id]
}

This works, but it’s leaky. Does the developer know which VPC to use? Which subnet? What about security group compliance? Usually, we wrap this in a second module to hide the complexity, creating a simpler API contract:

module "db" {
  source = "git@github.com:org/database-module"
  engine = "mysql"
  size   = "large"
}

This "API Contract" is great—until you need to move. What if your devs want a local database for testing but RDS for cloud? Or what if you move from AWS to Azure? Suddenly, that module needs massive amounts of branching logic, or you’re asking developers to rewrite their code for every environment. The tool (Terraform) is inextricably linked to the implementation.

Crossplane approaches this differently. It’s a Kubernetes add-on that turns your cluster into a universal control plane. It decouples the API Contract from the Infrastructure Implementation using two core concepts: XRDs and Compositions.

The Crossplane Shift: Decoupling Intent from Implementation

1. CompositeResourceDefinitions (XRD)

An XRD is where you define your internal API. It’s like a Kubernetes CRD, but Crossplane uses it to generate two things: a global Composite Resource (XR) and a namespace-scoped Claim. This is identical to the relationship between PersistentVolumes and PersistentVolumeClaims.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
name: xdatabases.acme.com
spec:
  group: acme.com
  names:
    kind: XDatabase
  claimNames:
    kind: Database
  versions:
    - name: v1alpha1
      schema:
        openAPIV3Schema:
          properties:
            spec:
              properties:
                engine: { type: string }
                size:   { type: string }

2. Compositions

Compositions are where the magic happens. They tell Crossplane: "When someone says 'XDatabase', do this." You can have multiple Compositions for the same XRD. One could trigger a local MySQL container for dev, and another could trigger a highly available RDS Aurora cluster for production. The user’s YAML doesn’t change—only the underlying Composition does.

By using Composition Pipelines and Functions, you can add complex logic: patching security groups, looking up VPC tags, or even calling external APIs (like Backstage) to decide how to render the resources.

Conclusion: The Control Plane Era

The real power of Crossplane isn't just "Kubernetes for RDS." It's the ability to build a self-service platform where you define the guardrails (the Compositions) and the developers define the intent (the Claims). You get standardisation without the friction, and flexibility without the spaghetti code.

In the next post, we’ll dive deeper into the nuts and bolts—the Providers and the Composition Pipelines that make this possible. Stay tuned.