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