Skip to content

Crossplane - Primer

Why This Matters

Crossplane brings infrastructure management into Kubernetes. Instead of running terraform apply from a CI pipeline or an operator's laptop, you declare cloud resources as Kubernetes custom resources and let the Crossplane controller reconcile them. This matters because it unifies the control plane: application workloads and the infrastructure they depend on (databases, queues, buckets, DNS records) are all managed by the same Kubernetes API, with the same RBAC, the same GitOps workflow, and the same reconciliation loop. For platform teams, Crossplane enables self-service infrastructure — developers create a claim for "give me a PostgreSQL database" without knowing which cloud provider or configuration is behind it.

Core Concepts

1. Architecture

Developer creates a Claim (e.g., "PostgreSQL database")
Crossplane matches Claim to a Composition
Composition defines which Managed Resources to create
Provider controller creates real cloud resources (RDS, Cloud SQL, etc.)
Crossplane continuously reconciles desired state vs actual state

Key terms: - Provider — a Crossplane package that knows how to manage resources for a specific platform (AWS, GCP, Azure, Kubernetes, Helm, etc.) - Managed Resource (MR) — a Kubernetes CR that maps 1:1 to an external resource (e.g., an RDS instance) - Composite Resource (XR) — an abstraction that bundles multiple managed resources into one logical unit - Composition — defines how an XR is composed from managed resources (the template) - Claim (XRC) — a namespace-scoped request for an XR (what developers interact with) - CompositeResourceDefinition (XRD) — the schema for an XR/Claim (like a CRD for your abstraction)

2. Installation

# Install Crossplane into a Kubernetes cluster
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

helm install crossplane crossplane-stable/crossplane \
  --namespace crossplane-system \
  --create-namespace \
  --set args='{"--enable-usages"}'

# Verify
kubectl get pods -n crossplane-system
kubectl api-resources | grep crossplane

3. Provider Installation

# Install the AWS provider
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws-s3
spec:
  package: xpkg.upbound.io/upbound/provider-aws-s3:v1.2.0
---
# Provider credentials
apiVersion: v1
kind: Secret
metadata:
  name: aws-creds
  namespace: crossplane-system
type: Opaque
stringData:
  credentials: |
    [default]
    aws_access_key_id = AKIAEXAMPLE
    aws_secret_access_key = secretkey
---
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-creds
      key: credentials
# Apply provider config
kubectl apply -f provider-aws.yaml

# Check provider status
kubectl get providers
kubectl get providerconfigs

# Wait for provider to become healthy
kubectl wait provider/provider-aws-s3 --for=condition=Healthy --timeout=300s

4. Managed Resources (Direct Cloud Resource Management)

# Create an S3 bucket directly (no abstraction)
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: my-app-data
spec:
  forProvider:
    region: us-east-1
    tags:
      Environment: production
      Team: platform
  providerConfigRef:
    name: default
kubectl apply -f s3-bucket.yaml
kubectl get bucket my-app-data

# Check sync status
kubectl describe bucket my-app-data
# Look for:
#   Status:
#     Conditions:
#       Type: Ready
#       Status: True
#       Type: Synced
#       Status: True

5. Compositions and Claims (Platform Abstractions)

Step 1: Define the schema (XRD):

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresqlinstances.database.example.org
spec:
  group: database.example.org
  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:
                parameters:
                  type: object
                  properties:
                    storageGB:
                      type: integer
                      default: 20
                    version:
                      type: string
                      default: "15"
                  required:
                    - storageGB
              required:
                - parameters

Step 2: Define the composition (how to build it):

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xpostgresqlinstances.aws.database.example.org
  labels:
    provider: aws
spec:
  compositeTypeRef:
    apiVersion: database.example.org/v1alpha1
    kind: XPostgreSQLInstance
  resources:
    - name: rdsinstance
      base:
        apiVersion: rds.aws.upbound.io/v1beta1
        kind: Instance
        spec:
          forProvider:
            region: us-east-1
            engine: postgres
            instanceClass: db.t3.micro
            skipFinalSnapshot: true
            publiclyAccessible: false
      patches:
        - fromFieldPath: "spec.parameters.storageGB"
          toFieldPath: "spec.forProvider.allocatedStorage"
        - fromFieldPath: "spec.parameters.version"
          toFieldPath: "spec.forProvider.engineVersion"
        - fromFieldPath: "metadata.uid"
          toFieldPath: "spec.forProvider.dbName"
          transforms:
            - type: string
              string:
                fmt: "db%s"
                type: Format

Step 3: Developer creates a claim:

apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
  name: my-app-db
  namespace: team-alpha
spec:
  parameters:
    storageGB: 50
    version: "15"
  compositionSelector:
    matchLabels:
      provider: aws

# Developer applies claim in their namespace
kubectl apply -f my-db-claim.yaml -n team-alpha

# Check claim status
kubectl get postgresqlinstance -n team-alpha
kubectl describe postgresqlinstance my-app-db -n team-alpha

# See the underlying composite and managed resources
kubectl get composite
kubectl get managed

6. Debugging

# Check the Crossplane controller logs
kubectl logs -n crossplane-system -l app=crossplane --tail=100

# Check provider controller logs
kubectl logs -n crossplane-system -l pkg.crossplane.io/revision -c provider --tail=100

# Trace a claim through the stack
kubectl get postgresqlinstance my-app-db -n team-alpha -o yaml  # claim
kubectl get xpostgresqlinstance -o yaml                          # composite
kubectl get instance.rds -o yaml                                 # managed resource

# Common status conditions to check
kubectl get managed -o custom-columns='NAME:.metadata.name,READY:.status.conditions[?(@.type=="Ready")].status,SYNCED:.status.conditions[?(@.type=="Synced")].status'

# Events
kubectl get events --field-selector involvedObject.name=my-app-db -n team-alpha

# Common issues:
# Synced=False — provider cannot reach the cloud API (credentials, permissions)
# Ready=False — resource exists but is not ready (still provisioning, or config error)
# LastTransitionTime stuck — reconciliation loop may be failing silently

7. Operational Patterns

GitOps with Crossplane: Store XRDs, Compositions, and Claims in Git. Use ArgoCD or Flux to sync them. This gives you declarative infrastructure with audit trails.

Multi-cloud abstraction: Create one XRD with multiple Compositions (one per cloud). Developers select via labels:

compositionSelector:
  matchLabels:
    provider: aws    # or gcp, or azure

Resource lifecycle:

# Delete a claim (will delete the underlying cloud resource)
kubectl delete postgresqlinstance my-app-db -n team-alpha

# Prevent accidental deletion
kubectl annotate postgresqlinstance my-app-db \
  crossplane.io/deletion-policy=Orphan -n team-alpha
# Orphan: delete CR but keep cloud resource
# Default (Delete): delete CR and cloud resource

Quick Reference

# Install
helm install crossplane crossplane-stable/crossplane -n crossplane-system --create-namespace

# Check health
kubectl get providers                    # installed providers
kubectl get providerconfigs              # provider credentials
kubectl get managed                      # all managed cloud resources
kubectl get composite                    # all composite resources
kubectl get claim --all-namespaces       # all claims

# Debugging
kubectl describe <managed-resource>      # check Ready/Synced conditions
kubectl logs -n crossplane-system -l app=crossplane
kubectl get events --sort-by='.lastTimestamp'

# Cleanup
kubectl delete claim <name> -n <ns>     # deletes claim + cloud resource

Wiki Navigation

Prerequisites

  • Kubernetes Exercises (Quest Ladder) (CLI) (Exercise Set, L1)
  • Terraform / IaC (Topic Pack, L1)