Infrastructure Provisioning with Crossplane
Compositions & XRDs
4 min read
Compositions and Composite Resource Definitions (XRDs) are Crossplane's abstraction layer. They allow platform teams to create simplified, opinionated APIs that hide cloud complexity from developers.
The Abstraction Challenge
Without abstractions, developers need cloud expertise:
Without Compositions:
┌─────────────────────────────────────────────────────────┐
│ Developer: "I need a PostgreSQL database" │
│ │
│ Must understand: │
│ ├── VPC configuration │
│ ├── Subnet placement │
│ ├── Security groups │
│ ├── IAM roles │
│ ├── RDS instance parameters │
│ ├── Parameter groups │
│ ├── Subnet groups │
│ └── Encryption settings │
│ │
│ Result: 200+ lines of YAML │
└─────────────────────────────────────────────────────────┘
With Compositions:
┌─────────────────────────────────────────────────────────┐
│ Developer: "I need a PostgreSQL database" │
│ │
│ apiVersion: platform.acme.com/v1alpha1 │
│ kind: Database │
│ metadata: │
│ name: my-db │
│ spec: │
│ size: small │
│ engine: postgres │
│ │
│ Result: 8 lines of YAML │
└─────────────────────────────────────────────────────────┘
Understanding XRDs and Compositions
┌─────────────────────────────────────────────────────────┐
│ PLATFORM TEAM │
│ │
│ Creates: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ XRD (Composite Resource Definition) │ │
│ │ - Defines the API schema │ │
│ │ - What parameters developers can set │ │
│ │ - e.g., "Database" with size, engine │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Composition │ │
│ │ - Implementation details │ │
│ │ - Maps XRD params to cloud resources │ │
│ │ - e.g., "small" → db.t3.micro + 20GB │ │
│ └─────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────┤
│ DEVELOPER │
│ │
│ Creates: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Claim (or Composite Resource) │ │
│ │ - Uses the simplified API │ │
│ │ - Just specifies: size=small, engine=postgres │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Creating an XRD
Define your platform's database API:
# xrd-database.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.platform.acme.com
spec:
group: platform.acme.com
names:
kind: XDatabase
plural: xdatabases
claimNames:
kind: Database
plural: databases
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
size:
type: string
enum: ["small", "medium", "large"]
description: "Database size tier"
engine:
type: string
enum: ["postgres", "mysql"]
default: "postgres"
description: "Database engine"
version:
type: string
default: "15"
description: "Engine version"
storageGB:
type: integer
minimum: 20
maximum: 1000
default: 20
description: "Storage size in GB"
required:
- size
status:
type: object
properties:
endpoint:
type: string
port:
type: integer
status:
type: string
Creating a Composition
Implement the database abstraction:
# composition-database-aws.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: database-aws
labels:
provider: aws
engine: postgres
spec:
compositeTypeRef:
apiVersion: platform.acme.com/v1alpha1
kind: XDatabase
writeConnectionSecretsToNamespace: crossplane-system
patchSets:
- name: common-tags
patches:
- type: FromCompositeFieldPath
fromFieldPath: metadata.labels
toFieldPath: spec.forProvider.tags
policy:
mergeOptions:
appendSlice: true
resources:
# Security Group
- name: security-group
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroup
spec:
forProvider:
region: us-east-1
description: "Database security group"
vpcId: vpc-0123456789abcdef0
ingress:
- fromPort: 5432
toPort: 5432
protocol: tcp
cidrBlocks:
- 10.0.0.0/16
patches:
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: metadata.name
transforms:
- type: string
string:
fmt: "%s-sg"
# Subnet Group
- name: subnet-group
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: SubnetGroup
spec:
forProvider:
region: us-east-1
description: "Database subnet group"
subnetIds:
- subnet-0123456789abcdef0
- subnet-0123456789abcdef1
patches:
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: metadata.name
transforms:
- type: string
string:
fmt: "%s-subnet-group"
# RDS Instance
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
region: us-east-1
engine: postgres
publiclyAccessible: false
skipFinalSnapshot: true
storageEncrypted: true
storageType: gp3
autoMinorVersionUpgrade: true
backupRetentionPeriod: 7
username: dbadmin
writeConnectionSecretToRef:
namespace: crossplane-system
patches:
# Name
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: metadata.name
# Instance identifier
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.forProvider.instanceIdentifier
# Database name
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.forProvider.dbName
transforms:
- type: string
string:
type: Convert
convert: ToLower
- type: string
string:
type: Regexp
regexp:
match: "-"
replace: "_"
# Engine version
- type: FromCompositeFieldPath
fromFieldPath: spec.version
toFieldPath: spec.forProvider.engineVersion
# Storage
- type: FromCompositeFieldPath
fromFieldPath: spec.storageGB
toFieldPath: spec.forProvider.allocatedStorage
# Size mapping (small/medium/large → instance class)
- type: FromCompositeFieldPath
fromFieldPath: spec.size
toFieldPath: spec.forProvider.dbInstanceClass
transforms:
- type: map
map:
small: db.t3.micro
medium: db.t3.medium
large: db.r6g.large
# Reference security group
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.forProvider.vpcSecurityGroupIdRefs[0].name
transforms:
- type: string
string:
fmt: "%s-sg"
# Reference subnet group
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.forProvider.dbSubnetGroupNameRef.name
transforms:
- type: string
string:
fmt: "%s-subnet-group"
# Connection secret
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.writeConnectionSecretToRef.name
transforms:
- type: string
string:
fmt: "%s-connection"
# Write endpoint to status
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.endpoint
toFieldPath: status.endpoint
# Write port to status
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.port
toFieldPath: status.port
connectionDetails:
- name: endpoint
fromFieldPath: status.atProvider.endpoint
- name: port
fromFieldPath: status.atProvider.port
- name: username
fromFieldPath: spec.forProvider.username
- name: password
fromConnectionSecretKey: attribute.password
Applying XRD and Composition
# Apply the XRD first
kubectl apply -f xrd-database.yaml
# Wait for XRD to be established
kubectl wait xrd xdatabases.platform.acme.com --for=condition=Established
# Apply the composition
kubectl apply -f composition-database-aws.yaml
# Verify
kubectl get xrd
kubectl get composition
Patch Types
Compositions use patches to transform and connect data:
# Patch types reference
patches:
# Copy value from composite to resource
- type: FromCompositeFieldPath
fromFieldPath: spec.size
toFieldPath: spec.forProvider.instanceClass
# Copy value from resource to composite status
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.endpoint
toFieldPath: status.endpoint
# Transform values
- type: FromCompositeFieldPath
fromFieldPath: spec.size
toFieldPath: spec.forProvider.instanceClass
transforms:
- type: map
map:
small: db.t3.micro
medium: db.t3.medium
large: db.r6g.large
# String formatting
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.forProvider.bucketName
transforms:
- type: string
string:
fmt: "acme-%s-bucket"
# Combine multiple fields
- type: CombineFromComposite
combine:
variables:
- fromFieldPath: spec.region
- fromFieldPath: metadata.name
strategy: string
string:
fmt: "%s-%s"
toFieldPath: spec.forProvider.instanceIdentifier
Multiple Compositions
Create different implementations for the same XRD:
# composition-database-aws.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: database-aws
labels:
provider: aws
spec:
compositeTypeRef:
apiVersion: platform.acme.com/v1alpha1
kind: XDatabase
# ... AWS implementation
---
# composition-database-gcp.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: database-gcp
labels:
provider: gcp
spec:
compositeTypeRef:
apiVersion: platform.acme.com/v1alpha1
kind: XDatabase
# ... GCP implementation with CloudSQL
# Select composition via compositionSelector
apiVersion: platform.acme.com/v1alpha1
kind: Database
metadata:
name: my-database
spec:
compositionSelector:
matchLabels:
provider: aws # Uses database-aws composition
size: small
engine: postgres
In the next lesson, we'll put everything together with self-service infrastructure claims and Backstage integration. :::