Infrastructure Provisioning with Crossplane

Self-Service Infrastructure

3 min read

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. :::

Quiz

Module 3: Infrastructure Provisioning with Crossplane

Take Quiz
FREE WEEKLY NEWSLETTER

Stay on the Nerd Track

One email per week — courses, deep dives, tools, and AI experiments.

No spam. Unsubscribe anytime.