What Crossplane Does#

Crossplane extends Kubernetes to provision and manage cloud infrastructure using the Kubernetes API. Instead of writing Terraform and running apply, you write Kubernetes manifests and kubectl apply them. Crossplane controllers reconcile the desired state with the actual cloud resources.

The real value is not replacing Terraform — it is building abstractions. Platform teams define custom resource types (like DatabaseClaim) that developers consume without knowing whether they are getting RDS, CloudSQL, or Azure Database. The composition layer maps the simple claim to the actual cloud resources.

Architecture: Four Layers#

Providers are the lowest layer. Each provider talks to one cloud API. provider-aws manages AWS resources, provider-gcp manages GCP, provider-azure manages Azure. Install them as Kubernetes packages:

apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws
spec:
  package: xpkg.upbound.io/upbound/provider-aws-rds:v1.16.0

Providers install CRDs for each cloud resource. After installing provider-aws-rds, you can create RDSInstance resources directly. But developers should never interact with provider resources directly — that defeats the purpose.

Managed Resources are the provider-level representations of cloud resources. A managed resource for an RDS instance:

apiVersion: rds.aws.upbound.io/v1beta2
kind: Instance
metadata:
  name: my-database
spec:
  forProvider:
    region: us-east-1
    engine: postgres
    engineVersion: "15"
    instanceClass: db.t3.medium
    allocatedStorage: 20
    masterUsername: admin
    masterPasswordSecretRef:
      name: db-password
      namespace: crossplane-system
      key: password

This works but exposes every cloud-specific parameter. Developers should not need to know about instanceClass or allocatedStorage.

Composite Resources (XRs) are the platform team’s abstractions. An XR defines a high-level resource type — XDatabase, XBucket, XCache — with a simplified schema. The platform team writes Compositions that map XR fields to managed resources.

Claims are the developer-facing interface. A claim is a namespaced request for a composite resource. Developers create claims; the platform resolves them into cloud resources.

Building a Database Claim: Step by Step#

Step 1: Define the XRD#

The CompositeResourceDefinition (XRD) defines the schema for your abstraction. This is what developers will see as the API contract.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xdatabases.platform.example.com
spec:
  group: platform.example.com
  names:
    kind: XDatabase
    plural: xdatabases
  claimNames:
    kind: DatabaseClaim
    plural: databaseclaims
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                engine:
                  type: string
                  enum: ["postgres", "mysql"]
                  description: "Database engine"
                version:
                  type: string
                  description: "Engine version"
                size:
                  type: string
                  enum: ["small", "medium", "large"]
                  description: "T-shirt size for compute and storage"
                region:
                  type: string
                  description: "Cloud region"
              required: ["engine", "size"]
            status:
              type: object
              properties:
                connectionHost:
                  type: string
                connectionPort:
                  type: string
                databaseName:
                  type: string

The XRD creates two CRDs: XDatabase (cluster-scoped composite) and DatabaseClaim (namespace-scoped claim). Developers only interact with DatabaseClaim.

Step 2: Write Compositions#

Each Composition maps the XRD to a specific cloud provider’s resources. You write one Composition per provider.

AWS Composition:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xdatabase-aws
  labels:
    provider: aws
spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1alpha1
    kind: XDatabase
  mode: Pipeline
  pipeline:
    - step: patch-and-transform
      functionRef:
        name: function-patch-and-transform
      input:
        apiVersion: pt.fn.crossplane.io/v1beta1
        kind: Resources
        resources:
          - name: rds-instance
            base:
              apiVersion: rds.aws.upbound.io/v1beta2
              kind: Instance
              spec:
                forProvider:
                  engine: postgres
                  publiclyAccessible: false
                  skipFinalSnapshot: false
                  storageEncrypted: true
                  autoMinorVersionUpgrade: true
            patches:
              - type: FromCompositeFieldPath
                fromFieldPath: spec.engine
                toFieldPath: spec.forProvider.engine
              - type: FromCompositeFieldPath
                fromFieldPath: spec.version
                toFieldPath: spec.forProvider.engineVersion
              - type: FromCompositeFieldPath
                fromFieldPath: spec.region
                toFieldPath: spec.forProvider.region
              - type: FromCompositeFieldPath
                fromFieldPath: spec.size
                toFieldPath: spec.forProvider.instanceClass
                transforms:
                  - type: map
                    map:
                      small: db.t3.micro
                      medium: db.t3.medium
                      large: db.r6g.xlarge
              - type: FromCompositeFieldPath
                fromFieldPath: spec.size
                toFieldPath: spec.forProvider.allocatedStorage
                transforms:
                  - type: map
                    map:
                      small: 20
                      medium: 100
                      large: 500
              - type: ToCompositeFieldPath
                fromFieldPath: status.atProvider.endpoint
                toFieldPath: status.connectionHost

GCP Composition:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xdatabase-gcp
  labels:
    provider: gcp
spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1alpha1
    kind: XDatabase
  mode: Pipeline
  pipeline:
    - step: patch-and-transform
      functionRef:
        name: function-patch-and-transform
      input:
        apiVersion: pt.fn.crossplane.io/v1beta1
        kind: Resources
        resources:
          - name: cloudsql-instance
            base:
              apiVersion: sql.gcp.upbound.io/v1beta2
              kind: DatabaseInstance
              spec:
                forProvider:
                  deletionProtection: true
                  settings:
                    - tier: db-f1-micro
                      ipConfiguration:
                        - ipv4Enabled: false
                          privateNetworkRef:
                            name: platform-vpc
            patches:
              - type: FromCompositeFieldPath
                fromFieldPath: spec.engine
                toFieldPath: spec.forProvider.databaseVersion
                transforms:
                  - type: map
                    map:
                      postgres: POSTGRES_15
                      mysql: MYSQL_8_0
              - type: FromCompositeFieldPath
                fromFieldPath: spec.size
                toFieldPath: spec.forProvider.settings[0].tier
                transforms:
                  - type: map
                    map:
                      small: db-f1-micro
                      medium: db-n1-standard-2
                      large: db-n1-standard-8

Step 3: Select Composition by Environment#

Use a CompositionSelector to pick the right Composition based on labels. The platform team labels claims with the target provider:

spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1alpha1
    kind: XDatabase
  compositionSelector:
    matchLabels:
      provider: aws    # or gcp, azure

Or use compositionRef to hardcode the selection per environment. A common pattern is setting the provider label via a namespace annotation that the platform team configures per cluster.

Step 4: Developer Creates a Claim#

The developer’s experience is simple:

apiVersion: platform.example.com/v1alpha1
kind: DatabaseClaim
metadata:
  name: orders-db
  namespace: orders-team
spec:
  engine: postgres
  version: "15"
  size: medium
  compositionSelector:
    matchLabels:
      provider: aws

Apply it: kubectl apply -f database-claim.yaml. Crossplane provisions the cloud resource, creates a connection secret, and updates the claim status with connection details.

Check status:

kubectl get databaseclaim orders-db -n orders-team
kubectl describe databaseclaim orders-db -n orders-team

The connection secret appears in the same namespace:

kubectl get secret orders-db-connection -n orders-team -o yaml

Provider Configuration#

Providers need cloud credentials. Use ProviderConfig to reference a Kubernetes secret or IRSA (IAM Roles for Service Accounts):

apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: IRSA

For GCP, use Workload Identity. For Azure, use Workload Identity Federation. Avoid long-lived credential secrets in production.

Composition Functions#

Crossplane v1.14+ supports Composition Functions — custom logic written in Go, Python, or any language that implements the function protocol. Use functions when patch-and-transform is not expressive enough:

pipeline:
  - step: validate-size
    functionRef:
      name: function-custom-validator
  - step: patch-and-transform
    functionRef:
      name: function-patch-and-transform
    input: ...

Functions enable conditional resource creation, complex transformations, and external lookups that pure patching cannot handle.

Practical Considerations#

Debugging. When a claim is not provisioning, check three places: the claim status (kubectl describe databaseclaim), the composite resource (kubectl get xdatabase), and the managed resource (kubectl get instance.rds). Errors bubble up but slowly — check the managed resource first for fast feedback.

Drift detection. Crossplane continuously reconciles. If someone modifies the cloud resource manually, Crossplane reverts it. This is a feature, not a bug — but it surprises teams accustomed to Terraform’s plan/apply workflow.

Deletion. Deleting a claim deletes the composite, which deletes the managed resources, which deletes the cloud resources. Set deletionPolicy: Orphan on managed resources if you want to keep the cloud resource after claim deletion.

Testing. Use crossplane beta render to preview what a Composition will produce without applying it. This is essential for CI validation of Composition changes.