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.
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.
| Concept | Kind | Owned By | Purpose |
|---|---|---|---|
| Provider | Provider | Crossplane | A package containing controllers and CRDs for one cloud (AWS, GCP, Azure, Vault, GitHub, ...) |
| Managed Resource (MR) | e.g. RDSInstance | Provider | 1:1 mapping to a real cloud resource (one RDS instance, one S3 bucket) |
| Composite Resource Definition (XRD) | CompositeResourceDefinition | Platform team | Defines a custom platform API (e.g. XPostgresInstance) |
| Composition | Composition | Platform team | Recipe: "an XPostgresInstance becomes these N managed resources" |
| Claim | e.g. PostgresInstance | Application team | Namespace-scoped self-service request that triggers a Composition |
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
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".
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
| Dimension | Crossplane | Terraform |
|---|---|---|
| Execution model | Continuous reconciliation by controllers | Run-to-completion (apply/destroy) |
| Drift handling | Auto-corrects continuously | Detected on next plan, manual remediation |
| State storage | etcd (the cluster itself) | External backend (S3, Terraform Cloud) |
| Abstraction mechanism | Compositions + XRDs | Modules |
| Self-service UX | kubectl apply a Claim | Submit a PR, wait for pipeline |
| RBAC integration | Native Kubernetes RBAC | Backend-specific |
| Provider ecosystem | Smaller but growing fast | Largest in industry |
| Best fit | Platform teams building self-service APIs | Infra 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.compositionRefon 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 renderto see what was actually computed. - Deletion stuck: external resource has dependencies. Crossplane v1.14+ supports
Usageresources to express deletion ordering.