Crossplane Compositions Deep-Dive: Building Your Cloud API Layer
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:
- The "Composition Hell": As you add more cloud providers (AWS, Azure, GCP), you will find yourself duplicating 80% of your logic across three different Compositions. The fix is to use Composition Functions (written in Go or Python) that can handle conditional logic and loops, rather than relying purely on YAML patches.
- State Drift vs. Reconciliation: Crossplane reconciles every few minutes. If a human manually changes a setting in the AWS Console, Crossplane will overwrite it. This is a feature, but it can be a nightmare during an emergency incident when you need a "quick fix" that keeps getting reverted by the controller.
- Provider Versioning: Crossplane providers (like
provider-aws) move fast. Upgrading a provider can occasionally change the schema of a Managed Resource, leading to a "permanent" reconciliation error where the resource is stuck in aSyncingstate forever.
Who Should Skip This
Do not implement a Crossplane Cloud API if:
- You are a small team: The overhead of managing the Control Plane (and the XRDs/Compositions) is higher than just maintaining a few Terraform modules.
- Your infra is static: If you deploy your database once a year, you don't need a real-time reconciliation loop.
- You don't have a dedicated Platform Engineer: Someone must own the "API" (the XRDs). If that person leaves, you are left with a black box that manages your production data.