Crossplane Compositions: Build Your Own Cloud API
Crossplane lets you define custom Kubernetes APIs for your infrastructure. Instead of developers writing Terraform or clicking through consoles, they create a simple YAML and get a fully configured database, network, or entire environment.
This guide covers building Compositions that abstract complexity while maintaining security and compliance.
TL;DR
- Crossplane = Kubernetes-native infrastructure management
- Compositions = templates that combine multiple resources
- CompositeResourceDefinitions (XRDs) = your custom API schema
- Claims = what developers use to request infrastructure
- Full examples for databases, networks, and applications
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Developer │
│ (creates simple Claim) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Claim (namespace-scoped) │
│ apiVersion: platform.company.com/v1alpha1 │
│ kind: PostgreSQLInstance │
│ spec: │
│ size: small │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CompositeResource (cluster-scoped) │
│ XPostgreSQLInstance (generated from XRD) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Composition │
│ (template: what resources to create) │
└─────────────────────────────────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ RDS Instance │ │ Security Group │ │ Parameter Group │
│ │ │ │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
Install Crossplane
# Install Crossplane
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm upgrade --install crossplane crossplane-stable/crossplane \
--namespace crossplane-system --create-namespace
# Install AWS Provider
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws
spec:
package: xpkg.upbound.io/upbound/provider-aws:v0.47.0
EOF
# Configure credentials
kubectl create secret generic aws-creds \
-n crossplane-system \
--from-file=credentials=~/.aws/credentials
cat <<EOF | kubectl apply -f -
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-creds
key: credentials
EOF
Example 1: PostgreSQL Database
Define the API (XRD)
# xrd-postgresql.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.database.platform.company.com
spec:
group: database.platform.company.com
names:
kind: XPostgreSQLInstance
plural: xpostgresqlinstances
claimNames:
kind: PostgreSQLInstance
plural: postgresqlinstances
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 preset"
version:
type: string
enum: ["13", "14", "15"]
default: "15"
region:
type: string
default: "eu-west-2"
required:
- size
status:
type: object
properties:
endpoint:
type: string
port:
type: integer
secretName:
type: string
Create the Composition
# composition-postgresql.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: postgresql-aws
labels:
provider: aws
database: postgresql
spec:
compositeTypeRef:
apiVersion: database.platform.company.com/v1alpha1
kind: XPostgreSQLInstance
patchSets:
- name: common-tags
patches:
- type: FromCompositeFieldPath
fromFieldPath: metadata.labels
toFieldPath: spec.forProvider.tags
policy:
mergeOptions:
keepMapValues: true
resources:
# Subnet Group
- name: subnet-group
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: SubnetGroup
spec:
forProvider:
description: "Managed by Crossplane"
subnetIds:
- subnet-aaaaaaaa
- subnet-bbbbbbbb
- subnet-cccccccc
providerConfigRef:
name: default
patches:
- type: PatchSet
patchSetName: common-tags
# Security Group
- name: security-group
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroup
spec:
forProvider:
vpcId: vpc-xxxxxxxx
description: "PostgreSQL access"
providerConfigRef:
name: default
patches:
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.id
toFieldPath: status.securityGroupId
# Security Group Rule
- name: sg-rule-ingress
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroupRule
spec:
forProvider:
type: ingress
fromPort: 5432
toPort: 5432
protocol: tcp
cidrBlocks:
- "10.0.0.0/8"
securityGroupIdSelector:
matchControllerRef: true
providerConfigRef:
name: default
# Parameter Group
- name: parameter-group
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: ParameterGroup
spec:
forProvider:
family: postgres15
parameter:
- name: log_statement
value: "all"
- name: log_min_duration_statement
value: "1000"
providerConfigRef:
name: default
patches:
- fromFieldPath: spec.version
toFieldPath: spec.forProvider.family
transforms:
- type: string
string:
fmt: "postgres%s"
# RDS Instance
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
engine: postgres
publiclyAccessible: false
skipFinalSnapshot: true
storageEncrypted: true
storageType: gp3
autoGeneratePassword: true
passwordSecretRef:
namespace: crossplane-system
key: password
dbSubnetGroupNameSelector:
matchControllerRef: true
vpcSecurityGroupIdSelector:
matchControllerRef: true
parameterGroupNameSelector:
matchControllerRef: true
providerConfigRef:
name: default
writeConnectionSecretToRef:
namespace: crossplane-system
patches:
# Size mapping
- type: FromCompositeFieldPath
fromFieldPath: spec.size
toFieldPath: spec.forProvider.instanceClass
transforms:
- type: map
map:
small: db.t3.micro
medium: db.t3.small
large: db.r6g.large
- type: FromCompositeFieldPath
fromFieldPath: spec.size
toFieldPath: spec.forProvider.allocatedStorage
transforms:
- type: map
map:
small: 20
medium: 50
large: 100
- type: FromCompositeFieldPath
fromFieldPath: spec.version
toFieldPath: spec.forProvider.engineVersion
- type: FromCompositeFieldPath
fromFieldPath: spec.region
toFieldPath: spec.forProvider.region
# Connection secret
- type: FromCompositeFieldPath
fromFieldPath: metadata.uid
toFieldPath: spec.writeConnectionSecretToRef.name
transforms:
- type: string
string:
fmt: "%s-connection"
# Status outputs
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.endpoint
toFieldPath: status.endpoint
- 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: password
Developer Experience (Claim)
# database-claim.yaml
apiVersion: database.platform.company.com/v1alpha1
kind: PostgreSQLInstance
metadata:
name: my-app-db
namespace: my-team
spec:
size: small
version: "15"
# Connection secret written to this namespace
writeConnectionSecretToRef:
name: my-app-db-creds
That’s it. Developer creates this simple YAML and gets a fully configured RDS instance with security groups, encryption, and connection credentials.
Example 2: Application Environment
Compose an entire environment:
# xrd-environment.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xenvironments.platform.company.com
spec:
group: platform.company.com
names:
kind: XEnvironment
plural: xenvironments
claimNames:
kind: Environment
plural: environments
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
tier:
type: string
enum: ["dev", "staging", "prod"]
team:
type: string
enableDatabase:
type: boolean
default: true
enableCache:
type: boolean
default: false
enableQueue:
type: boolean
default: false
required:
- tier
- team
# composition-environment.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: environment-aws
spec:
compositeTypeRef:
apiVersion: platform.company.com/v1alpha1
kind: XEnvironment
resources:
# Namespace
- name: namespace
base:
apiVersion: kubernetes.crossplane.io/v1alpha1
kind: Object
spec:
forProvider:
manifest:
apiVersion: v1
kind: Namespace
metadata:
labels:
managed-by: crossplane
patches:
- fromFieldPath: spec.team
toFieldPath: spec.forProvider.manifest.metadata.name
transforms:
- type: string
string:
fmt: "%s-env"
# PostgreSQL (conditional)
- name: database
base:
apiVersion: database.platform.company.com/v1alpha1
kind: XPostgreSQLInstance
spec:
size: small
patches:
- fromFieldPath: spec.tier
toFieldPath: spec.size
transforms:
- type: map
map:
dev: small
staging: medium
prod: large
- fromFieldPath: spec.enableDatabase
toFieldPath: metadata.annotations[crossplane.io/paused]
transforms:
- type: convert
convert:
toType: string
- type: map
map:
"true": "false"
"false": "true"
# ElastiCache (conditional)
- name: cache
base:
apiVersion: cache.platform.company.com/v1alpha1
kind: XRedisCluster
spec:
size: small
patches:
- fromFieldPath: spec.enableCache
toFieldPath: metadata.annotations[crossplane.io/paused]
transforms:
- type: convert
convert:
toType: string
- type: map
map:
"true": "false"
"false": "true"
Developer usage:
apiVersion: platform.company.com/v1alpha1
kind: Environment
metadata:
name: my-app
namespace: platform
spec:
tier: dev
team: payments
enableDatabase: true
enableCache: true
Composition Functions
For complex logic, use Composition Functions:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: postgresql-with-functions
spec:
compositeTypeRef:
apiVersion: database.platform.company.com/v1alpha1
kind: XPostgreSQLInstance
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
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
engine: postgres
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.size
toFieldPath: spec.forProvider.instanceClass
- step: auto-ready
functionRef:
name: function-auto-ready
GitOps with Crossplane
Store compositions in Git and apply via ArgoCD:
# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: platform-compositions
namespace: argocd
spec:
project: platform
source:
repoURL: https://github.com/company/platform-compositions
path: compositions
targetRevision: main
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
prune: true
selfHeal: true
Troubleshooting
# Check XRD status
kubectl get xrd
# Check composition
kubectl get composition
# Check composite resource
kubectl get xpostgresqlinstances
# Check claim
kubectl get postgresqlinstances -n my-namespace
# Debug - see all managed resources
kubectl get managed
# See events
kubectl describe xpostgresqlinstance my-db
References
- Crossplane Docs: https://docs.crossplane.io
- Composition Docs: https://docs.crossplane.io/latest/concepts/compositions/
- Upbound Marketplace: https://marketplace.upbound.io