Skip to main content

Crossplane Ep 5: Compositions

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
crossplane - This article is part of a series.
Part 5: This Article
A Composition is the most powerful concept in Crossplane. It acts as a strict translation layer. It watches for an incoming 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:

  1. compositeTypeRef: This tells the Composition which XRD it is translating for.
  2. resources: An array of physical Managed Resources (MRs) that should be provisioned.
  3. patches: (Covered in Episode 6) How to map values from the XR (like storageGB: 50) down into the physical MRs (like allocatedStorage: 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: default

Apply the Composition
#

kubectl apply -f composition-postgres.yaml

Check that it exists:

kubectl get composition

Expected Terminal Output:

NAME                                             XR-KIND               XR-APIVERSION                      AGE
xpostgresqlinstances.aws.database.acmecorp.com   XPostgreSQLInstance   database.acmecorp.com/v1alpha1     10s

3. 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.

  1. The Developer applies their claim (my-database-claim.yaml from Episode 4).
  2. The Kubernetes API server accepts the PostgreSQLInstance because we defined it in our XRD.
  3. Crossplane sees the new Claim. It automatically creates a cluster-scoped XPostgreSQLInstance (XR) to fulfill the claim.
  4. Crossplane looks for a Composition that satisfies XPostgreSQLInstance. It finds the one we just applied.
  5. The Composition execution engine starts. It reads the resources block.
  6. Crossplane automatically creates a physical Instance.rds.aws.upbound.io MR in the cluster.
  7. 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 managed

Expected Terminal Output:

NAME                                         READY   SYNCED   EXTERNAL-NAME   AGE
instance.rds.aws.upbound.io/app-db-x5g2p     False   True                     3m

The 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
#

  1. composition is not selected Event on the XR

    • Root Cause: Crossplane created the XR, but it cannot find a valid Composition that matches the compositeTypeRef.
    • Solution: Ensure the apiVersion and kind in your Composition exactly match the XRD. If you are using Labels to select compositions, ensure the labels match.
  2. The MR is created, but it’s stuck in Creating

    • Root Cause: You are missing a required field in the base of your MR (e.g., you forgot to provide a username for 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.

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.

crossplane - This article is part of a series.
Part 5: This Article