XPostgreSQLInstance API request, and translates it into an AWS RDS Database, a DB Subnet Group, and a Security Group. By binding an XRD to a Composition, we complete the Internal Developer Platform (IDP) loop.1. The Anatomy of a Composition#
A Composition tells Crossplane how to compose an XR out of multiple MRs.
It has three main parts:
compositeTypeRef: This tells the Composition which XRD it is translating for.resources: An array of physical Managed Resources (MRs) that should be provisioned.patches: (Covered in Episode 6) How to map values from the XR (likestorageGB: 50) down into the physical MRs (likeallocatedStorage: 50).
2. Writing our first Composition#
Let’s fulfill the API we built in Episode 4. When a developer asks for an XPostgreSQLInstance, we want to provision a physical AWS RDS instance.
For simplicity in this fundamental episode, we will hardcode the AWS values. (We will learn how to make them dynamic via Patching in the Intermediate tier).
Create a file named composition-postgres.yaml:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xpostgresqlinstances.aws.database.acmecorp.com
labels:
# We can label compositions so XRDs can select them dynamically
provider: aws
spec:
# 1. Bind this Composition to the XRD we created in Ep 4
compositeTypeRef:
apiVersion: database.acmecorp.com/v1alpha1
kind: XPostgreSQLInstance
# 2. Define the exact infrastructure that makes up this composite
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
# We are hardcoding the AWS infrastructure for now
region: us-east-1
engine: postgres
engineVersion: "15.3"
instanceClass: db.t3.micro
allocatedStorage: 20
username: masteruser
skipFinalSnapshot: true
publiclyAccessible: false
# Use our AWS credentials from Ep 2
providerConfigRef:
name: defaultApply the Composition#
kubectl apply -f composition-postgres.yamlCheck that it exists:
kubectl get compositionExpected Terminal Output:
NAME XR-KIND XR-APIVERSION AGE
xpostgresqlinstances.aws.database.acmecorp.com XPostgreSQLInstance database.acmecorp.com/v1alpha1 10s3. The Execution Flow#
Now that we have the full puzzle assembled, let’s look at the flow of execution from the Application Developer’s perspective.
- The Developer applies their claim (
my-database-claim.yamlfrom Episode 4). - The Kubernetes API server accepts the
PostgreSQLInstancebecause we defined it in our XRD. - Crossplane sees the new Claim. It automatically creates a cluster-scoped
XPostgreSQLInstance(XR) to fulfill the claim. - Crossplane looks for a
Compositionthat satisfiesXPostgreSQLInstance. It finds the one we just applied. - The Composition execution engine starts. It reads the
resourcesblock. - Crossplane automatically creates a physical
Instance.rds.aws.upbound.ioMR in the cluster. - The AWS Provider pod sees the new MR, authenticates with AWS using the
ProviderConfig, and issues the REST API calls to physically boot the database.
Checking the Infrastructure#
If you still have the claim from Episode 4 applied to your cluster, Crossplane has already started building the database!
Run this command to look at the physical Managed Resources:
kubectl get managedExpected Terminal Output:
NAME READY SYNCED EXTERNAL-NAME AGE
instance.rds.aws.upbound.io/app-db-x5g2p False True 3mThe database is SYNCED (the API call succeeded), but AWS takes about 10 minutes to boot an RDS instance, so it is not READY yet.
4. Multiple Compositions (The Vending Machine)#
Why did we separate the API (XRD) from the Implementation (Composition) into two different files?
Because an XRD can have multiple Compositions.
Imagine your company uses both AWS and Google Cloud (GCP).
You can write a second Composition file named composition-postgres-gcp.yaml that uses the provider-gcp to provision a Cloud SQL instance instead of an RDS instance.
When the Application Developer asks for a PostgreSQLInstance, they can add a label to their claim:
metadata:
labels:
provider: gcp # Select the GCP composition!The Developer doesn’t have to change their storageGB parameter. The API remains perfectly stable. The Platform Team handles the complex routing behind the scenes. This allows you to migrate an entire engineering organization from AWS to GCP by simply updating a label, completely transparent to the application code.
Troubleshooting & Common Errors#
composition is not selectedEvent on the XR- Root Cause: Crossplane created the XR, but it cannot find a valid Composition that matches the
compositeTypeRef. - Solution: Ensure the
apiVersionandkindin your Composition exactly match the XRD. If you are using Labels to select compositions, ensure the labels match.
- Root Cause: Crossplane created the XR, but it cannot find a valid Composition that matches the
The MR is created, but it’s stuck in
Creating- Root Cause: You are missing a required field in the
baseof your MR (e.g., you forgot to provide ausernamefor the RDS instance). - Solution: Run
kubectl describe <mr-kind> <mr-name>and look at the Events section. The AWS Provider will explicitly tell you which field is missing.
- Root Cause: You are missing a required field in the
Conclusion & Next Steps#
You have completed the Crossplane Fundamentals! You have successfully built a declarative Internal Developer Platform. You authored a custom Kubernetes API, created a Composition engine, and successfully abstracted complex AWS infrastructure away from your developers.
However, our Composition currently hardcodes the database size to 20GB. If the developer requested storageGB: 50 in their claim, Crossplane completely ignored it!
In Episode 6: Patching and Transforms, we will enter the Intermediate tier. We will learn how to take values from the Developer’s Claim and dynamically inject them into the physical Managed Resources using Patches.

