Back to Modern DevOps & Platform Engineering Series

Crossplane — Complete Tool Reference Guide

May 15, 2026 Wasil Zafar 28 min read

A comprehensive reference to Crossplane — the CNCF universal control plane that turns Kubernetes into the API for managing any infrastructure across AWS, GCP, Azure, and beyond, with composable platform abstractions.

Table of Contents

  1. Overview & History
  2. Architecture & Concepts
  3. Installation
  4. Providers
  5. Managed Resources
  6. Compositions & XRDs
  7. Claims — Self-Service APIs
  8. Composition Functions
  9. Crossplane vs Terraform
  10. Troubleshooting

Overview & History

Crossplane is an open-source universal control plane built on Kubernetes that lets you provision and manage cloud infrastructure — databases, queues, networks, IAM roles, even SaaS services — using the same Kubernetes APIs you use for workloads. Originally created by Upbound in 2018 as an evolution of OAM (Open Application Model), it joined CNCF in 2020 and reached incubating status in 2021.

The animating idea: rather than running Terraform from a CI pipeline that occasionally drifts and needs a human to reconcile, the cluster itself continuously reconciles real-world infrastructure to match desired state — exactly like a Deployment continuously reconciles Pods. Combined with Compositions, Crossplane lets platform teams publish their own opinionated APIs (a PostgresInstance claim, a StandardNetwork claim) that abstract dozens of underlying cloud resources behind a single, friendly interface.

Key Insight: Crossplane's superpower is not "Terraform-on-Kubernetes" — it is turning your platform into an API. When a developer creates a kind: PostgresInstance with size: small, region: eu-west-1, the platform team's Composition translates that into a VPC, subnet, security group, RDS instance, parameter group, KMS key, IAM role, Secret in Vault, and a Backstage catalog entry — all reconciled continuously, all self-healing, all visible through standard kubectl.

Architecture & Concepts

Crossplane's mental model has five core concepts. Master these and the rest is detail.

ConceptKindOwned ByPurpose
ProviderProviderCrossplaneA package containing controllers and CRDs for one cloud (AWS, GCP, Azure, Vault, GitHub, ...)
Managed Resource (MR)e.g. RDSInstanceProvider1:1 mapping to a real cloud resource (one RDS instance, one S3 bucket)
Composite Resource Definition (XRD)CompositeResourceDefinitionPlatform teamDefines a custom platform API (e.g. XPostgresInstance)
CompositionCompositionPlatform teamRecipe: "an XPostgresInstance becomes these N managed resources"
Claime.g. PostgresInstanceApplication teamNamespace-scoped self-service request that triggers a Composition
Crossplane Object Flow
flowchart TD
    Dev["Developer creates
PostgresInstance Claim"] --> XR["Composite Resource
(XPostgresInstance)"] XR --> Comp["Composition
(platform recipe)"] Comp --> MR1["RDSInstance MR"] Comp --> MR2["SubnetGroup MR"] Comp --> MR3["SecurityGroup MR"] Comp --> MR4["IAMRole MR"] MR1 --> Cloud["AWS API"] MR2 --> Cloud MR3 --> Cloud MR4 --> Cloud style Dev fill:#e8f4f4,stroke:#3B9797,color:#132440 style XR fill:#f0f4f8,stroke:#16476A,color:#132440 style Comp fill:#f0f4f8,stroke:#16476A,color:#132440 style MR1 fill:#e8f4f4,stroke:#3B9797,color:#132440 style MR2 fill:#e8f4f4,stroke:#3B9797,color:#132440 style MR3 fill:#e8f4f4,stroke:#3B9797,color:#132440 style MR4 fill:#e8f4f4,stroke:#3B9797,color:#132440 style Cloud fill:#132440,stroke:#132440,color:#ffffff

Installation

# Install Crossplane via Helm (recommended)
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

helm install crossplane \
  --namespace crossplane-system \
  --create-namespace \
  crossplane-stable/crossplane \
  --version 1.16.0 \
  --set args='{--enable-environment-configs,--enable-usages}'

# Verify pods
kubectl -n crossplane-system get pods

# Install the Crossplane CLI for diagnostics
curl -sL "https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh" | sh
sudo mv crossplane /usr/local/bin/

# Health check
crossplane beta validate

Providers

A Provider is an OCI-packaged controller that adds a set of CRDs and reconcilers for one external system. The official provider-family-aws, provider-family-gcp, and provider-family-azure are split into per-service packages (e.g. provider-aws-rds, provider-aws-s3) so you only install what you use.

# provider-aws-rds.yaml
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws-rds
spec:
  package: xpkg.upbound.io/upbound/provider-aws-rds:v1.10.0
  packagePullPolicy: IfNotPresent
  revisionActivationPolicy: Automatic
  revisionHistoryLimit: 1
---
# Configure AWS credentials for the provider via IRSA (preferred)
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: IRSA   # IAM Roles for Service Accounts — no static keys
---
# Or with static credentials (acceptable for non-prod)
apiVersion: v1
kind: Secret
metadata:
  name: aws-creds
  namespace: crossplane-system
type: Opaque
stringData:
  creds: |
    [default]
    aws_access_key_id = AKIAIOSFODNN7EXAMPLE
    aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
---
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: dev-account
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-creds
      key: creds

Managed Resources

A Managed Resource (MR) corresponds 1:1 to a real cloud resource. You can create them directly — though in practice you almost always wrap them in Compositions to give application teams a higher-level API.

# Example: directly create an RDS instance as a Managed Resource
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
metadata:
  name: payments-db-dev
  annotations:
    # External name = actual AWS resource identifier
    crossplane.io/external-name: payments-db-dev
spec:
  forProvider:
    region: eu-west-1
    instanceClass: db.t3.medium
    allocatedStorage: 20
    engine: postgres
    engineVersion: "15.4"
    username: admin
    autoGeneratePassword: true
    passwordSecretRef:
      namespace: crossplane-system
      name: payments-db-dev-password
      key: password
    skipFinalSnapshot: false
    finalSnapshotIdentifier: payments-db-dev-final
    backupRetentionPeriod: 14
    storageEncrypted: true
    publiclyAccessible: false
    vpcSecurityGroupIdSelector:
      matchLabels:
        role: database
  providerConfigRef:
    name: default
  writeConnectionSecretToRef:
    namespace: payments
    name: payments-db-dev-connection

Compositions & XRDs

The platform team's main job in Crossplane is publishing Compositions. A CompositeResourceDefinition (XRD) defines the schema of a new platform API, and a Composition defines how to translate that API into a graph of Managed Resources.

# xrd-postgres-instance.yaml
# Step 1: Define the platform's PostgresInstance API
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresinstances.platform.corp.com
spec:
  group: platform.corp.com
  names:
    kind: XPostgresInstance
    plural: xpostgresinstances
  claimNames:
    kind: PostgresInstance       # Namespace-scoped wrapper
    plural: postgresinstances
  defaultCompositionRef:
    name: postgresinstance-aws
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                parameters:
                  type: object
                  required: [size, region]
                  properties:
                    size:
                      type: string
                      enum: [small, medium, large]
                      description: T-shirt size that maps to instance class
                    region:
                      type: string
                      description: AWS region
                    storageGB:
                      type: integer
                      default: 20
                      minimum: 20
                      maximum: 1000
                    engineVersion:
                      type: string
                      default: "15.4"
            status:
              type: object
              properties:
                connectionEndpoint:
                  type: string
                phase:
                  type: string
# composition-postgres-aws.yaml
# Step 2: Recipe — translates XPostgresInstance into managed resources
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: postgresinstance-aws
  labels:
    provider: aws
    db: postgres
spec:
  compositeTypeRef:
    apiVersion: platform.corp.com/v1alpha1
    kind: XPostgresInstance
  writeConnectionSecretsToNamespace: crossplane-system
  resources:
    - name: subnet-group
      base:
        apiVersion: rds.aws.upbound.io/v1beta1
        kind: SubnetGroup
        spec:
          forProvider:
            description: "Managed by Crossplane"
            subnetIdSelector:
              matchLabels:
                role: database

    - name: rds-instance
      base:
        apiVersion: rds.aws.upbound.io/v1beta1
        kind: Instance
        spec:
          forProvider:
            engine: postgres
            allocatedStorage: 20
            autoGeneratePassword: true
            backupRetentionPeriod: 14
            storageEncrypted: true
            publiclyAccessible: false
            skipFinalSnapshot: false
      patches:
        - fromFieldPath: "spec.parameters.region"
          toFieldPath: "spec.forProvider.region"
        - fromFieldPath: "spec.parameters.storageGB"
          toFieldPath: "spec.forProvider.allocatedStorage"
        - fromFieldPath: "spec.parameters.engineVersion"
          toFieldPath: "spec.forProvider.engineVersion"
        # Translate t-shirt size to instance class
        - fromFieldPath: "spec.parameters.size"
          toFieldPath: "spec.forProvider.instanceClass"
          transforms:
            - type: map
              map:
                small: db.t3.medium
                medium: db.r6g.large
                large: db.r6g.2xlarge
        # Surface the endpoint into the claim status
        - type: ToCompositeFieldPath
          fromFieldPath: "status.atProvider.endpoint"
          toFieldPath: "status.connectionEndpoint"
      connectionDetails:
        - fromConnectionSecretKey: endpoint
        - fromConnectionSecretKey: port
        - fromConnectionSecretKey: username
        - fromConnectionSecretKey: password

Claims — Self-Service APIs

With the XRD and Composition in place, application teams now have a self-service API. They never need to know about RDS, VPCs, or IAM — they just declare what they want.

# developer-postgres-claim.yaml
# This is what an application team writes — and the only thing they see
apiVersion: platform.corp.com/v1alpha1
kind: PostgresInstance
metadata:
  name: payments-db
  namespace: payments
spec:
  parameters:
    size: medium
    region: eu-west-1
    storageGB: 100
    engineVersion: "15.4"
  writeConnectionSecretToRef:
    name: payments-db-connection   # Secret created in the payments namespace
Pattern Platform-as-a-Product
Deutsche Telekom's "MagentaServices" Platform

Deutsche Telekom built their internal platform "MagentaServices" on Crossplane in 2022. They expose ~40 platform Claims (databases, message queues, S3-compatible buckets, ingress controllers, secrets) that application teams self-serve. Underneath, those 40 Claims fan out to ~600 different managed resources across AWS, on-prem OpenStack, and internal SaaS. By 2024, 1,200+ teams were using the platform with sub-15-minute provisioning times — replacing what used to be a multi-week ticket workflow. The win was not the technology but the abstraction: developers think in "I need a database", not "I need an RDS Instance with a SubnetGroup with these subnets and this SG".

Self-Service Abstraction Platform Claims

Composition Functions

Patches in YAML hit a complexity wall around composition #5. Crossplane v1.14 introduced Composition Functions — pluggable WASM or container-based functions that compute resources from inputs using real code (Go, Python, KCL, CUE).

# composition-with-function.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: postgresinstance-aws-fn
spec:
  compositeTypeRef:
    apiVersion: platform.corp.com/v1alpha1
    kind: XPostgresInstance
  mode: Pipeline                # Use functions instead of patches
  pipeline:
    - step: load-environment
      functionRef:
        name: function-environment-configs
    - step: render-resources
      functionRef:
        name: function-kcl              # KCL = Kubernetes Configuration Language
      input:
        apiVersion: krm.kcl.dev/v1alpha1
        kind: KCLRun
        spec:
          source: |
            # KCL code computes the resource graph from the XR spec
            xr = option("params").oxr
            size_map = {"small": "db.t3.medium", "medium": "db.r6g.large", "large": "db.r6g.2xlarge"}
            items = [
                {
                    apiVersion: "rds.aws.upbound.io/v1beta1"
                    kind: "Instance"
                    spec.forProvider: {
                        region: xr.spec.parameters.region
                        instanceClass: size_map[xr.spec.parameters.size]
                        allocatedStorage: xr.spec.parameters.storageGB
                        engine: "postgres"
                        engineVersion: xr.spec.parameters.engineVersion
                    }
                }
            ]
    - step: auto-ready
      functionRef:
        name: function-auto-ready

Crossplane vs Terraform

DimensionCrossplaneTerraform
Execution modelContinuous reconciliation by controllersRun-to-completion (apply/destroy)
Drift handlingAuto-corrects continuouslyDetected on next plan, manual remediation
State storageetcd (the cluster itself)External backend (S3, Terraform Cloud)
Abstraction mechanismCompositions + XRDsModules
Self-service UXkubectl apply a ClaimSubmit a PR, wait for pipeline
RBAC integrationNative Kubernetes RBACBackend-specific
Provider ecosystemSmaller but growing fastLargest in industry
Best fitPlatform teams building self-service APIsInfra teams with rich existing modules

Many organisations run both — Terraform for foundational landing zones (account vending, root networking) and Crossplane for self-service workload infrastructure that lives near the applications.

Troubleshooting

# See all Crossplane objects
kubectl get crossplane

# Check a Claim and follow it down to managed resources
kubectl get postgresinstance -n payments
kubectl describe postgresinstance payments-db -n payments

# Use the trace command to see the full ownership tree
crossplane beta trace postgresinstance/payments-db -n payments

# Inspect a stuck Managed Resource
kubectl get instance.rds.aws.upbound.io
kubectl describe instance.rds.aws.upbound.io payments-db-xyz123

# Check provider health
kubectl get providers
kubectl get providerrevisions
kubectl logs -n crossplane-system deploy/provider-aws-rds-xxxxxxx -c package-runtime

# Validate a Composition before applying
crossplane beta validate xrd-postgres-instance.yaml composition-postgres-aws.yaml

# Render what a Claim would produce (no cluster apply)
crossplane beta render claim.yaml composition.yaml functions.yaml

Common pitfalls:

  • "Resource does not exist" after creating a Claim: usually missing Composition selector. Check spec.compositionRef on the Composite Resource.
  • Permissions denied from cloud API: ProviderConfig credentials lack required IAM permissions. Provider docs list the minimum policy.
  • Stuck in "Creating": cloud provider error in status.conditions — usually quota, naming, or dependency issues.
  • Patch not applying: field path typo. Use crossplane beta render to see what was actually computed.
  • Deletion stuck: external resource has dependencies. Crossplane v1.14+ supports Usage resources to express deletion ordering.