Backstage isn’t just a catalog of services; it’s a dynamic facade for your entire engineering ecosystem, and when paired with Crossplane, it becomes the single pane of glass for provisioning and managing cloud infrastructure.

Imagine a developer needing a new PostgreSQL instance for their application. Without a portal, this involves tickets, Slack messages, waiting for SREs, and manually piecing together CloudFormation or Terraform. With Backstage and Crossplane, the developer navigates to their service’s page in Backstage, clicks "Provision PostgreSQL," fills out a simple form (like desired instance size and version), and a few minutes later, a fully provisioned, secure PostgreSQL database is ready, complete with connection strings and credentials automatically injected into their application’s secrets.

Here’s how it works under the hood. Crossplane acts as a Kubernetes control plane for your cloud resources. You define your desired infrastructure state using Crossplane’s Custom Resource Definitions (CRDs) – think PostgreSQLInstance, RDSInstance, or GCPCloudSQLInstance. These CRDs are the "blueprints" for cloud resources. When you apply one of these CRDs to your Kubernetes cluster, Crossplane’s controllers, which are essentially specialized Kubernetes operators, translate that CRD into the native API calls for your cloud provider (AWS, Azure, GCP, etc.). They then manage the lifecycle of that resource, ensuring it matches the desired state defined in the CRD.

Backstage, on the other hand, provides the user interface and the catalog. It ingests metadata about your services, infrastructure, and teams from various sources. For Crossplane integration, you’ll typically use Backstage’s Software Catalog to define your infrastructure resources as entities. You can then create custom frontend components within Backstage to interact with Crossplane. For instance, a "Provisioner" plugin in Backstage can display a form that dynamically generates Crossplane CRDs based on user input. When the form is submitted, Backstage sends this CRD to your Kubernetes cluster where Crossplane picks it up and starts provisioning.

Let’s look at a practical example. Suppose you want to allow developers to provision AWS RDS instances.

First, ensure Crossplane is installed in your Kubernetes cluster and configured with an AWS provider. Your ProviderConfig might look like this:

apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-credentials
      key: credentials

And your aws-credentials secret would contain your AWS access key ID and secret access key.

Next, you define a Crossplane composite resource that abstracts the RDS instance. This composite resource, let’s call it XPostgreSQLInstance, might be composed of a managed RDSInstance and a DBSubnetGroup.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresqlinstances.database.example.com
spec:
  group: database.example.com
  names:
    kind: XPostgreSQLInstance
    plural: xpostgresqlinstances
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              parameters:
                type: object
                properties:
                  storageGB:
                    type: integer
                  instanceClass:
                    type: string
            required:
            - parameters
      connectionSecretKeys:
      - username
      - password
      - endpoint

And the composition that defines how XPostgreSQLInstance is built:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: rds-postgres-composition
spec:
  compositeTypeRef:
    apiVersion: database.example.com/v1alpha1
    kind: XPostgreSQLInstance
  resources:
    - name: rdsinstance
      base:
        apiVersion: rds.aws.upbound.io/v1beta1
        kind: Instance
        spec:
          allocatedStorage: 20 # Default storage
          engine: Postgres
          engineVersion: "14.3"
          dbSubnetGroupName: default-subnet-group # Assume this exists
          publiclyAccessible: false
          skipFinalDestroy: true
          tags:
            managed-by: crossplane
            environment: staging
      patches:
        - fromFieldPath: spec.parameters.storageGB
          toFieldPath: spec.allocatedStorage
        - fromFieldPath: spec.parameters.instanceClass
          toFieldPath: spec.class

In Backstage, you’d represent this XPostgreSQLInstance as an entity in your catalog-info.yaml:

apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
  name: rds-postgres-provisioner
  description: Provision AWS RDS PostgreSQL instances
spec:
  type: crossplane-provisioner
  owner: team-infra

Then, you’d build a Backstage plugin. This plugin would have a component that renders a form. When a user fills in storageGB and instanceClass and clicks "Provision," the plugin constructs an XPostgreSQLInstance CRD like this:

apiVersion: database.example.com/v1alpha1
kind: XPostgreSQLInstance
metadata:
  name: my-app-db
spec:
  parameters:
    storageGB: 100
    instanceClass: db.t3.medium

This CRD is then applied to your Kubernetes cluster via the Backstage backend, triggering Crossplane to create the RDS instance. Backstage can then display the status of this provisioning request by watching the XPostgreSQLInstance object in Kubernetes.

The real magic happens when you start connecting these provisioned resources back to your application entities in Backstage. You can use Backstage’s dependsOn and provides relationships to link your application to its database. This means when a developer views their application in Backstage, they see not just the code repository and CI/CD pipeline, but also the live, provisioned infrastructure it relies on.

One aspect often overlooked is how Crossplane manages secrets. When a Crossplane-provisioned resource, like an RDS instance, is created, it can automatically output connection details (username, password, endpoint) into a Kubernetes secret. Backstage can then dynamically read this secret and inject it into the application’s CI/CD pipeline or deployment configuration, fully automating the connection process without manual intervention. This means developers don’t just get a database; they get a database that’s immediately usable by their application.

The next step is to integrate Crossplane’s observability features into Backstage, allowing developers to see metrics and logs for their provisioned infrastructure directly within the portal.

Want structured learning?

Take the full Crossplane course →