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.0Providers 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: passwordThis 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: stringThe 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.connectionHostGCP 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-8Step 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, azureOr 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: awsApply 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-teamThe connection secret appears in the same namespace:
kubectl get secret orders-db-connection -n orders-team -o yamlProvider 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: IRSAFor 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.