🗺️ What is Crossplane?
Crossplane is a CNCF graduated project that turns your Kubernetes cluster into a universal control plane. It installs providers that know how to talk to cloud APIs (AWS, GCP, Azure, etc.), then lets you declare infrastructure resources — RDS instances, S3 buckets, VPCs — as Kubernetes CRDs. The same kubectl apply, RBAC, and GitOps workflows you use for apps now manage your entire cloud infrastructure.
The Crossplane Abstraction Layers
PostgreSQLInstance Claim (XRC) in their namespace — knows nothing about cloud details
RDSInstance, S3Bucket, DBSubnetGroup…
⚙️ Install & Providers
Install Crossplane
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm install crossplane crossplane-stable/crossplane \
--namespace crossplane-system \
--create-namespace \
--version 1.15.0
# Verify
kubectl get pods -n crossplane-system
kubectl get crds | grep crossplane
Install a Provider
Providers are OCI packages that add Managed Resource CRDs and reconciliation controllers for a specific cloud platform.
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws-s3
spec:
package: xpkg.upbound.io/upbound/provider-aws-s3:v1.1.0
installationPolicy: Automatic
revisionActivationPolicy: Automatic
provider-aws-s3, provider-aws-rds). Install only the sub-providers you need — avoids installing 800+ CRDs you'll never use.
ProviderConfig — Credentials
# Create a Secret with cloud credentials
kubectl create secret generic aws-creds \
--namespace crossplane-system \
--from-literal=creds="$(cat ~/.aws/credentials)"
# ProviderConfig references that secret
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-creds
key: creds
credentials.source: IRSA in ProviderConfig.
Managed Resources — Direct Cloud Objects
A Managed Resource (MR) maps 1-to-1 to a cloud API object. You can create them directly or via Compositions.
# Create an S3 bucket directly as a Managed Resource
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
name: my-app-assets
annotations:
crossplane.io/external-name: my-app-assets-prod-2024
spec:
forProvider:
region: us-east-1
tags:
env: production
team: platform
providerConfigRef:
name: default
# Check status — Crossplane reconciles continuously
kubectl get bucket my-app-assets
# NAME READY SYNCED EXTERNAL-NAME AGE
# my-app-assets True True my-app-assets-prod-2024 2m
kubectl describe bucket my-app-assets
# Status.AtProvider shows the actual cloud state
# Status.Conditions shows Synced/Ready conditions
spec.deletionPolicy: Orphan to detach Crossplane management without destroying the resource.
🧩 CompositeResourceDefinitions & Compositions
This is Crossplane's superpower: a platform team defines a custom API (XRD) that hides cloud complexity, then writes a Composition to implement it. App teams just file a Claim — they never see RDS subnet groups or IAM roles.
1 — Define the Custom API (XRD)
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.db.example.com
spec:
group: db.example.com
names:
kind: XPostgreSQLInstance
plural: xpostgresqlinstances
claimNames: # enables namespace-scoped Claims
kind: PostgreSQLInstance
plural: postgresqlinstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
default: 20
dbVersion:
type: string
default: "14"
region:
type: string
required: [storageGB, region]
2 — Write the Composition
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xpostgresqlinstances-aws
labels:
provider: aws
db: postgres
spec:
compositeTypeRef:
apiVersion: db.example.com/v1alpha1
kind: XPostgreSQLInstance
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
engine: postgres
instanceClass: db.t3.micro
skipFinalSnapshot: true
publiclyAccessible: false
providerConfigRef:
name: default
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.allocatedStorage
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.dbVersion
toFieldPath: spec.forProvider.engineVersion
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
- name: db-subnet-group
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: SubnetGroup
spec:
forProvider:
region: us-east-1
description: "Managed by Crossplane"
providerConfigRef:
name: default
3 — Developer Files a Claim
# Developer in namespace "my-app" requests a database
# They only need to know storageGB and region — no RDS/subnet knowledge needed
apiVersion: db.example.com/v1alpha1
kind: PostgreSQLInstance
metadata:
name: my-app-db
namespace: my-app
spec:
parameters:
storageGB: 50
dbVersion: "15"
region: us-east-1
compositionSelector:
matchLabels:
provider: aws
writeConnectionSecretToRef:
name: my-app-db-conn # Crossplane writes host/port/user/pass here
# Watch the claim become ready
kubectl get postgresqlinstance -n my-app
# NAME READY CONNECTION-SECRET AGE
# my-app-db True my-app-db-conn 4m
# The connection secret is automatically available to pods
kubectl get secret my-app-db-conn -n my-app -o yaml
⚖️ Crossplane vs Terraform
| Dimension | Crossplane | Terraform |
|---|---|---|
| Control loop | Continuous reconciliation (Kubernetes controller) | Manual plan/apply or CI-triggered |
| State storage | Kubernetes etcd (cluster is the state) | Remote state file (S3, Terraform Cloud) |
| Drift detection | Automatic — reconciler detects & corrects drift | Requires explicit terraform plan |
| Developer API | Custom CRDs via XRD — kubectl/GitOps native | HCL modules, Terraform variables |
| RBAC | Kubernetes RBAC on Claims/XRDs natively | External (Vault, Terraform Cloud teams) |
| Learning curve | High — Compositions are complex YAML | Moderate — HCL is approachable |
| Ecosystem maturity | Growing — best coverage via Upbound providers | Mature — largest provider ecosystem |
| Best for | Platform teams building internal cloud APIs | Infra teams managing large existing estates |
Production Patterns
Platform-as-a-Product
Platform team owns XRDs and Compositions in a central repo. App teams consume via Claims in their namespace — no cloud knowledge required.
GitOps for Infra
Store Claims and Managed Resources in Git. ArgoCD/Flux applies them. Crossplane's continuous reconciliation handles the cloud side.
Multi-Cloud Abstraction
Same XRD, two Compositions (one for AWS, one for GCP). Switch environments by changing compositionSelector labels.
Self-Service Environments
Developers request full environments (DB + bucket + cache) by filing a single Claim. Composition wires everything together.
crossplane beta trace command to visualise the full resource tree and spot which resource is blocking readiness.
# Trace a Claim's full resource tree
crossplane beta trace postgresqlinstance my-app-db -n my-app
# Output shows XR → Managed Resources → cloud status
# NAME SYNCED READY STATUS
# PostgreSQLInstance/my-app-db True True Available
# ├─ XPostgreSQLInstance/... True True Available
# ├─ Instance/rds-... True True Available
# └─ SubnetGroup/sg-... True True Available
📝 Knowledge Check
PostgreSQLInstance Claim in their namespace. What happens to the underlying RDS instance in AWS by default?Delete — removing a Claim cascades to the XR, then to Managed Resources, which triggers deletion of the actual cloud resource. Use deletionPolicy: Orphan to prevent this.