Infrastructure Provisioning with Crossplane
Self-Service Infrastructure
The ultimate goal of Crossplane compositions is enabling developer self-service. This lesson shows how to create Claims that developers use, and integrate with Backstage for a complete self-service experience.
Claims vs Composite Resources
┌─────────────────────────────────────────────────────────┐
│ RESOURCE TYPES │
├─────────────────────────────────────────────────────────┤
│ │
│ Composite Resource (XR): │
│ ├── Cluster-scoped (no namespace) │
│ ├── Full access to all composition features │
│ ├── Used by: Platform teams, automation │
│ └── apiVersion: platform.acme.com/v1alpha1 │
│ kind: XDatabase │
│ │
│ Claim: │
│ ├── Namespace-scoped │
│ ├── Creates an XR behind the scenes │
│ ├── Used by: Developers (team namespaces) │
│ └── apiVersion: platform.acme.com/v1alpha1 │
│ kind: Database │
│ │
└─────────────────────────────────────────────────────────┘
Creating a Claim
Developers request infrastructure using Claims:
# database-claim.yaml
apiVersion: platform.acme.com/v1alpha1
kind: Database
metadata:
name: orders-db
namespace: team-orders
spec:
size: small
engine: postgres
version: "15"
storageGB: 50
compositionSelector:
matchLabels:
provider: aws
writeConnectionSecretToRef:
name: orders-db-connection
# Developer creates the claim
kubectl apply -f database-claim.yaml
# Watch the claim status
kubectl get database orders-db -n team-orders -w
# NAME SYNCED READY CONNECTION-SECRET AGE
# orders-db True False orders-db-connection 10s
# orders-db True True orders-db-connection 5m
# Check the created XR (cluster-scoped)
kubectl get xdatabase
# NAME SYNCED READY COMPOSITION AGE
# orders-db-xxxxx True True database-aws 5m
# View all created resources
kubectl get managed -l crossplane.io/claim-name=orders-db
Connection Secrets
Claims write connection details to namespace-scoped secrets:
# Claim with connection secret
apiVersion: platform.acme.com/v1alpha1
kind: Database
metadata:
name: app-db
namespace: my-app
spec:
size: medium
writeConnectionSecretToRef:
name: db-credentials # Created in my-app namespace
# Using the secret in a deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-app
spec:
template:
spec:
containers:
- name: app
image: my-app:latest
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-credentials
key: endpoint
- name: DB_PORT
valueFrom:
secretKeyRef:
name: db-credentials
key: port
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
Backstage Integration
Create a Backstage template that provisions infrastructure via Crossplane:
# backstage-template-database.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: provision-database
title: Provision Database
description: Request a PostgreSQL or MySQL database for your service
tags:
- database
- infrastructure
- crossplane
spec:
owner: platform-team
type: infrastructure
parameters:
- title: Database Configuration
required:
- name
- size
- engine
properties:
name:
title: Database Name
type: string
description: Name for your database (lowercase, hyphens only)
pattern: '^[a-z][a-z0-9-]*$'
size:
title: Size
type: string
enum:
- small
- medium
- large
enumNames:
- Small (db.t3.micro - Dev/Test)
- Medium (db.t3.medium - Staging)
- Large (db.r6g.large - Production)
default: small
engine:
title: Database Engine
type: string
enum:
- postgres
- mysql
default: postgres
version:
title: Engine Version
type: string
default: "15"
storageGB:
title: Storage (GB)
type: integer
minimum: 20
maximum: 1000
default: 20
- title: Target Environment
required:
- namespace
- owner
properties:
namespace:
title: Kubernetes Namespace
type: string
description: Namespace where the database secret will be created
owner:
title: Owner
type: string
description: Team that owns this database
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
steps:
- id: create-claim
name: Create Database Claim
action: kubernetes:apply
input:
namespaced: true
manifest:
apiVersion: platform.acme.com/v1alpha1
kind: Database
metadata:
name: ${{ parameters.name }}
namespace: ${{ parameters.namespace }}
labels:
app.kubernetes.io/managed-by: backstage
backstage.io/owner: ${{ parameters.owner }}
spec:
size: ${{ parameters.size }}
engine: ${{ parameters.engine }}
version: ${{ parameters.version }}
storageGB: ${{ parameters.storageGB }}
writeConnectionSecretToRef:
name: ${{ parameters.name }}-connection
- id: register-component
name: Register in Catalog
action: catalog:register
input:
catalogInfoUrl: https://github.com/acme/infrastructure-catalog/blob/main/databases/${{ parameters.name }}/catalog-info.yaml
output:
links:
- title: View Database in Catalog
url: ${{ steps['register-component'].output.entityRef }}
- title: Connection Secret
url: /kubernetes/secrets/${{ parameters.namespace }}/${{ parameters.name }}-connection
RBAC for Self-Service
Configure Kubernetes RBAC to allow teams to create claims:
# rbac-database-claims.yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: database-claim-creator
rules:
- apiGroups: ["platform.acme.com"]
resources: ["databases"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
---
# Grant to team namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: database-claims
namespace: team-orders
subjects:
- kind: Group
name: team-orders
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: database-claim-creator
apiGroup: rbac.authorization.k8s.io
Complete Self-Service Flow
┌─────────────────────────────────────────────────────────┐
│ SELF-SERVICE FLOW │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. DEVELOPER │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ BACKSTAGE │ │
│ │ "Provision Database"│ │
│ │ - size: medium │ │
│ │ - engine: postgres│ │
│ └──────────┬──────────┘ │
│ │ │
│ 2. BACKSTAGE CREATES │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ CLAIM │ │
│ │ kind: Database │ │
│ │ namespace: team-x │ │
│ └──────────┬──────────┘ │
│ │ │
│ 3. CROSSPLANE PROCESSES │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ COMPOSITION │ │
│ │ Creates: │ │
│ │ - Security Group │ │
│ │ - Subnet Group │ │
│ │ - RDS Instance │ │
│ └──────────┬──────────┘ │
│ │ │
│ 4. CLOUD PROVIDER │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ AWS │ │
│ │ Actual RDS created │ │
│ └──────────┬──────────┘ │
│ │ │
│ 5. CONNECTION SECRET │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ SECRET │ │
│ │ name: db-connection│ │
│ │ namespace: team-x │ │
│ │ - endpoint │ │
│ │ - port │ │
│ │ - username │ │
│ │ - password │ │
│ └─────────────────────┘ │
│ │
│ TIME: ~5 minutes (fully automated) │
│ │
└─────────────────────────────────────────────────────────┘
Monitoring Claims
Track infrastructure requests:
# List all claims across namespaces
kubectl get databases --all-namespaces
# Check claim details
kubectl describe database orders-db -n team-orders
# View events
kubectl get events -n team-orders --field-selector involvedObject.name=orders-db
# Check underlying resources
kubectl get managed -l crossplane.io/claim-namespace=team-orders
# Prometheus metrics (if configured)
# crossplane_managed_resource_ready_total
# crossplane_managed_resource_synced_total
Claim Lifecycle
# Lifecycle operations
lifecycle:
update:
description: "Modify claim spec"
example: "Change size from small to medium"
action: "Crossplane updates underlying resources"
delete:
description: "Delete the claim"
behavior: "Crossplane deletes all created cloud resources"
command: "kubectl delete database orders-db -n team-orders"
orphan:
description: "Remove from Crossplane without deleting cloud resources"
annotation: "crossplane.io/deletion-policy: Orphan"
In the next module, we'll explore GitOps and continuous delivery patterns that complement this self-service infrastructure approach. :::