Crossplane Compositions Deep-Dive: Building Your Cloud API Layer

Sources

For years, platform teams have played a losing game of "Terraform Module Maintenance." You build a module, you version it, and then you spend 40% of your week helping developers fix terraform apply errors because they changed a variable in a way the module didn't expect.

The shift from Infrastructure-as-Code (IaC) to a Cloud Control Plane means moving the complexity from the developer's terminal to the platform's API. Instead of giving developers a module, you give them a Custom Resource. You define the "What," and the platform handles the "How."

This is where Crossplane Compositions come in. This blueprint provides the structural implementation for a production-grade "Cloud API Layer" that abstracts a secure PostgreSQL database across a provider.

The Architecture: From Claim to Resource

+---------------------------------------------------------------------------------+
|                                PLATFORM CONTROL PLANE                            |
|                                                                                 |
|  [ Developer Claim ]  --->  [ Composite Resource (XR) ]  --->  [ Managed Resources ]
|    (K8s Namespace)             (Cluster-Scoped)                 (Provider API)
|                                                                                 |
|    "I want a               "The platform interprets           "Create AWS RDS,     |
|     Secure DB"              this as an RDS instance         Subnet Group, and    |
|                              with encryption & backup"       Parameter Group"     |
+---------------------------------------------------------------------------------+
|                                                                                 |
|  [ CompositeResourceDefinition (XRD) ]  <-- Defines the API Contract (The "What")|
|  [ Composition ]                        <-- Defines the Blueprint (The "How")   |
+---------------------------------------------------------------------------------+

Part 1: The Interface (CompositeResourceDefinition)

The XRD is the most important file in your platform. It defines the API your developers will interact with. If you get this wrong, you are just building a slower version of Terraform.

A production-grade XRD should be opinionated but flexible. Do not expose every RDS flag; expose the ones that matter to your organization's compliance and cost policies.


apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresinstances.database.example.org
spec:
  group: database.example.org
  names:
    kind: XPostgresInstance
    plural: xpostgresinstances
  claimNames:
    kind: PostgresInstance
    plural: postgresinstances
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                parameters:
                  type: object
                  required: ["storageGb"]
                  properties:
                    storageGb:
                      type: integer
                      minimum: 20
                      maximum: 1000
                    version:
                      type: string
                      enum: ["13", "14", "15"]
                    tier:
                      type: string
                      enum: ["small", "medium", "large"]
              required: ["parameters"]

Part 2: The Implementation (Composition)

The Composition is the "blueprint" that maps the XRD to actual cloud resources. Modern Crossplane uses Functions to handle this mapping. The function-patch-and-transform is the industry standard for most use cases, allowing you to map values from the XR to the Managed Resources (MRs) using patches.

In this blueprint, we implement a "Secure by Default" pattern: the Composition automatically handles the subnet groups and parameter groups, ensuring developers cannot accidentally deploy a database with a public IP or disabled encryption.


apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: postgres-aws-secure
  labels:
    provider: aws
    env: production
spec:
  compositeTypeRef:
    apiVersion: database.example.org/v1alpha1
    kind: XPostgresInstance
  mode: Pipeline
  pipeline:
    - step: patch-and-transform
      functionRef:
        name: function-patch-and-transform
      input:
        resources:
          - name: rdsInstance
            base:
              apiVersion: database.aws.upbound.io/v1beta1
              kind: Instance
              spec:
                forProvider:
                  dbInstanceClass: db.t3.micro
                  allocatedStorage: 20
                  storageEncrypted: true
                  backupRetentionPeriod: 7
                  publiclyAccessible: false
            patches:
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.parameters.storageGb"
                toFieldPath: "spec.forProvider.allocatedStorage"
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.parameters.version"
                toFieldPath: "spec.forProvider.engineVersion"
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.parameters.tier"
                toFieldPath: "spec.forProvider.dbInstanceClass"
                transforms:
                  - type: map
                    map:
                      small: "db.t3.micro"
                      medium: "db.t3.medium"
                      large: "db.t3.large"

          - name: subnetGroup
            base:
              apiVersion: database.aws.upbound.io/v1beta1
              kind: DBSubnetGroup
              spec:
                forProvider:
                  dbSubnetGroupDescription: "Managed by Platform Monkey"
            patches:
              - type: FromCompositeFieldPath
                fromFieldPath: "metadata.name"
                toFieldPath: "metadata.annotations['crossplane.io/external-name']"
                transforms:
                  - type: string
                    string: "db-sg-"

Part 3: The Consumption (The Claim)

Now, the developer doesn't need to know about RDSInstance or DBSubnetGroup. They simply create a PostgresInstance claim in their namespace. The control plane sees the claim, finds the matching Composition, and provisions the resources.


apiVersion: database.example.org/v1alpha1
kind: PostgresInstance
metadata:
  name: order-db-prod
  namespace: team-orders
spec:
  parameters:
    storageGb: 100
    version: "15"
    tier: "medium"

Production Realities: Where this Breaks

Crossplane is powerful, but it introduces a new class of operational toil that doesn't exist in Terraform. If you're deploying this, watch out for the following:

Who Should Skip This

Do not implement a Crossplane Cloud API if:

References & Further Reading