Skip to main content

Crossplane Ep 4: Composite Resources (XR)

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 4: This Article
If your company requires that every database has KMS encryption, automated backups, and 3 specific Subnet Groups, you cannot expect Application Developers to write 500 lines of Kubernetes YAML to configure all of those individual Managed Resources. You need an abstraction. You need to create a Composite Resource (XR).

1. The Concept of Abstraction in Crossplane
#

To build an Internal Developer Platform (IDP), we must split our users into two personas:

  1. The Platform Engineer: Understands AWS. Writes complex configurations. Defines the company’s internal APIs.
  2. The Application Developer: Knows nothing about AWS subnets. Just wants a database to store user data.

The Platform Engineer’s job is to build a simple API for the Application Developer.

In Crossplane, we define this API by writing a Composite Resource Definition (XRD). This is a special Crossplane object that tells the Kubernetes API server: “Hey, I am creating a brand new API called XPostgreSQL. If anyone asks for one, here are the 3 simple arguments they are allowed to provide.”


2. Authoring an XRD
#

Let’s author an XRD for a hypothetical company called AcmeCorp. We want to allow our developers to request a PostgreSQL database by simply specifying the size of the storage (in GB) and the environment type (dev or prod).

Create a file named xrd-postgres.yaml:

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  # The name MUST follow the format: <plural>.<group>
  name: xpostgresqlinstances.database.acmecorp.com
spec:
  # 1. Define the API Group and Names
  group: database.acmecorp.com
  names:
    kind: XPostgreSQLInstance
    plural: xpostgresqlinstances
  
  # 2. Define the exact API Schema we want to expose to our developers!
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              parameters:
                type: object
                properties:
                  # Developers can specify the size
                  storageGB:
                    type: integer
                    description: "The size of the database in Gigabytes"
                  # Developers can specify the environment
                  environment:
                    type: string
                    enum: ["dev", "prod"]
                    description: "The target environment for this database"
                # Make both parameters mandatory
                required:
                  - storageGB
                  - environment

Applying the XRD
#

Apply this to your cluster:

kubectl apply -f xrd-postgres.yaml

If the command succeeds, Crossplane will dynamically compile this OpenAPI schema and inject it into the Kubernetes API server.

You can verify that your brand new API exists by checking the cluster’s CRDs:

kubectl get crds | grep acmecorp

You should see xpostgresqlinstances.database.acmecorp.com listed! You have successfully extended the Kubernetes API.


3. The Difference between XR and XRC
#

Crossplane has a very strict security boundary regarding Kubernetes Namespaces.

  • Cluster-Scoped (XR): The API we just created (XPostgreSQLInstance) is Cluster-Scoped. This means it is meant for Cluster Administrators. If an application developer only has RBAC permissions to deploy to their specific namespace, they will be denied permission to create an XPostgreSQLInstance.
  • Namespace-Scoped (XRC): To allow application developers to provision infrastructure, we must tell Crossplane to generate a Namespace-Scoped “Claim”.

Enabling Claims (XRCs)
#

To generate a Claim, we add a claimNames block to our XRD. Modify your xrd-postgres.yaml:

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresqlinstances.database.acmecorp.com
spec:
  group: database.acmecorp.com
  names:
    kind: XPostgreSQLInstance
    plural: xpostgresqlinstances
  
  # Add this block!
  claimNames:
    kind: PostgreSQLInstance # Notice there is no 'X' prefix!
    plural: postgresqlinstances
  
  versions: # ... (schema remains exactly the same)

Apply the updated file:

kubectl apply -f xrd-postgres.yaml

Crossplane will now dynamically generate a second, namespace-scoped CRD called PostgreSQLInstance.

Application developers can now safely create PostgreSQLInstance claims in their own isolated namespaces. Crossplane will automatically watch for these claims, and silently create the cluster-scoped XPostgreSQLInstance (XR) behind the scenes to fulfill it.


4. Consuming the API (The Developer Experience)
#

Now that the API is installed, let’s switch personas. We are now the Application Developer.

We don’t need to know anything about XRDs or AWS. We just look at the internal wiki our Platform Team wrote, and we create a simple YAML file alongside our application code.

Create my-database-claim.yaml:

apiVersion: database.acmecorp.com/v1alpha1
kind: PostgreSQLInstance
metadata:
  name: app-db
  # I can deploy this safely into my isolated namespace!
  namespace: default 
spec:
  parameters:
    storageGB: 50
    environment: dev

Apply it:

kubectl apply -f my-database-claim.yaml

If you try to put environment: testing, Kubernetes will reject the YAML instantly because of the enum validation we wrote in the OpenAPI schema!


Troubleshooting & Common Errors
#

  1. kubectl apply rejects the Claim with ValidationError

    • Root Cause: The Application Developer provided a value in their Claim (e.g., storageGB: "50" as a string) that violates the type: integer OpenAPI schema defined in the XRD.
    • Solution: Fix the Claim YAML to match the exact data types specified in the XRD.
  2. The XR is created, but no AWS resources are provisioning

    • Root Cause: The XRD only defines the API. It does not define the implementation. Right now, Crossplane accepts the Claim, but it has no instructions on what physical AWS resources to create when it receives a PostgreSQLInstance.
    • Solution: We must write a Composition. (See Episode 5).

Conclusion & Next Steps
#

You have successfully designed a custom Kubernetes API that hides all cloud complexity from the end user. You have established a clean boundary between Platform Engineering (XRDs) and Application Development (Claims).

However, as we discovered in the troubleshooting section, our API is currently a “mock”. It accepts requests, but it doesn’t actually spin up any RDS databases in AWS yet.

In Episode 5: Compositions, we will build the Translation Engine. We will tell Crossplane exactly how to map the simple storageGB integer from our XR into complex, multi-resource AWS topologies.

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