Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Infrastructure as Code By James Joyner IV · · 9 min read

Crossplane Compositions: Building Your Own Internal Cloud API

Crossplane turns Kubernetes into a control plane for any cloud. Compositions let you offer self-service infra to devs. Here's how the pieces fit together.

  • #iac
  • #crossplane
  • #kubernetes
  • #platform-engineering
  • #cloud
  • #self-service

Most infrastructure-as-code runs as a job: a pipeline executes Terraform or Pulumi, applies a change, and exits. Crossplane takes a fundamentally different stance — it makes infrastructure a set of Kubernetes resources that a controller continuously reconciles toward desired state, forever. Drift gets corrected automatically because reconciliation never stops.

The headline feature, and the reason platform teams care, is Compositions: the ability to bundle a pile of cloud resources behind a single, simple custom resource that your developers can self-serve. Here’s how to think about building one.

The control-plane mental model

Crossplane installs into a Kubernetes cluster and adds providers — for AWS, GCP, Azure, and hundreds of other services. Each provider exposes cloud resources as Kubernetes CRDs called Managed Resources. An RDS instance becomes a Kubernetes object:

apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
metadata:
  name: app-db
spec:
  forProvider:
    region: us-east-1
    instanceClass: db.t3.micro
    engine: postgres
    allocatedStorage: 20
  providerConfigRef:
    name: default

kubectl apply this and Crossplane provisions the RDS instance, then watches it forever. Delete the resource and it deprovisions. The cluster’s etcd is now your source of truth, and the controller is your apply loop. That continuous reconciliation is the philosophical difference from run-and-exit IaC.

The problem Compositions solve

Raw Managed Resources are too low-level to hand to application developers. A “database” in practice means an RDS instance plus a subnet group, a parameter group, a security group, and a secret. You don’t want every dev assembling those correctly.

Compositions let the platform team define that bundle once and expose it as a clean abstraction. Three pieces work together:

  1. CompositeResourceDefinition (XRD) — defines the simple API you offer, e.g. a PostgreSQLInstance with just size and version fields.
  2. Composition — the recipe mapping that simple API to the full set of Managed Resources.
  3. Claim — what developers actually create: a namespaced, dead-simple request.

Defining the developer-facing API (XRD)

The XRD is the contract. Keep it ruthlessly minimal — expose only what a developer should decide:

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresqlinstances.platform.example.org
spec:
  group: platform.example.org
  names:
    kind: XPostgreSQLInstance
    plural: xpostgresqlinstances
  claimNames:
    kind: PostgreSQLInstance
    plural: postgresqlinstances
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                parameters:
                  type: object
                  properties:
                    size:
                      type: string
                      enum: ["small", "medium", "large"]
                    version:
                      type: string
                  required: ["size"]

Notice the size is an enum of t-shirt sizes, not a raw instance class. You’re encoding judgment into the API so developers can’t pick a db.r5.24xlarge by accident.

The Composition: where the real infra lives

The Composition maps that small API onto the full resource set and patches values across them:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: postgres.aws.platform.example.org
spec:
  compositeTypeRef:
    apiVersion: platform.example.org/v1alpha1
    kind: XPostgreSQLInstance
  resources:
    - name: rds-instance
      base:
        apiVersion: rds.aws.upbound.io/v1beta1
        kind: Instance
        spec:
          forProvider:
            engine: postgres
            allocatedStorage: 20
      patches:
        - fromFieldPath: spec.parameters.size
          toFieldPath: spec.forProvider.instanceClass
          transforms:
            - type: map
              map:
                small: db.t3.micro
                medium: db.t3.medium
                large: db.r5.large

The map transform turns small into db.t3.micro. The platform team owns this mapping; developers never see it. Add the subnet group, parameter group, and security group as sibling entries in the same resources list and you’ve encapsulated the entire “give me a database” workflow.

What developers actually write

After all that platform work, the developer experience is a few lines:

apiVersion: platform.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
  name: orders-db
  namespace: team-payments
spec:
  parameters:
    size: medium
  writeConnectionSecretToRef:
    name: orders-db-conn

They kubectl apply it, wait, and get a connection secret in their namespace. No cloud console, no Terraform, no ticket to the platform team. That’s the self-service payoff — and it’s why Crossplane shows up in nearly every serious platform-engineering conversation now.

Honest trade-offs

Crossplane is powerful but not free:

  • The YAML patching is verbose and awkward. Complex transforms get unwieldy fast. Newer Compositions support functions (including Pythonic pipelines) that make this far nicer — prefer them over raw patch-and-transform for anything non-trivial.
  • You’re running a control plane. That’s a stateful, critical Kubernetes workload your platform team now operates. The cluster going down means your infra API goes down.
  • Debugging spans two systems. When provisioning fails, the answer might be in a Crossplane event, a provider log, or the cloud’s own error — more places to look than a single Terraform apply.

Where AI helps

The verbose, repetitive XRD-and-Composition YAML is exactly the kind of boilerplate AI handles well, while you supply the architectural judgment.

  • Generating the XRD schema from a plain description: “an XRD exposing a Redis cache with size and an engine-version field, sizes small/medium/large.”
  • Writing patch-and-transform blocks, which are fiddly and easy to typo. Describe the source and target field paths and the mapping, and let the model produce the YAML.
  • Explaining a failed reconcile. Paste the composite resource’s events and the model is good at pointing to the misconfigured patch or missing provider config.

The model doesn’t know your org’s t-shirt-size mapping or security baseline, so treat its output as scaffolding you review against your standards. We maintain a set of IaC prompts for platform and Crossplane work.

The bigger picture

Compositions are how a platform team turns “infrastructure” into a product with an API. You absorb the complexity once — the resource bundles, the sane defaults, the guardrails — and hand developers a button. When it works, the day-to-day infra ticket queue largely disappears, because the common requests became self-service. That’s the promise of platform engineering, and Crossplane Compositions are one of the cleaner ways to deliver it.

AI-generated Crossplane manifests are assistive, not authoritative. Validate XRDs and Compositions in a non-production control plane before offering them to developers.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.