Skip to content
Back to blog Crossplane Compositions: Build Your Own Cloud API

Crossplane Compositions: Build Your Own Cloud API

Platform EngineeringK8s

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 + Compositions + Kubernetes

Your cloud. Your API. Your guardrails.

Found this helpful?

Comments