🗺️ What is Crossplane?

Crossplane is a CNCF graduated project that turns your Kubernetes cluster into a universal control plane. It installs providers that know how to talk to cloud APIs (AWS, GCP, Azure, etc.), then lets you declare infrastructure resources — RDS instances, S3 buckets, VPCs — as Kubernetes CRDs. The same kubectl apply, RBAC, and GitOps workflows you use for apps now manage your entire cloud infrastructure.

Kubernetes Cluster Crossplane core controller Provider AWS 800+ managed resources Provider GCP GKE, Cloud SQL… CompositeResourceDef XRD — defines custom API shape Composition maps XR → Managed Resources Claim (XRC) developer-facing API Managed Resource RDSInstance, S3Bucket… AWS APIs RDS, S3, IAM, VPC… GCP APIs CloudSQL, GCS, GKE…

The Crossplane Abstraction Layers

Application Team Submits a PostgreSQLInstance Claim (XRC) in their namespace — knows nothing about cloud details
Composite Resource Claim (XRC) Namespace-scoped. The developer-facing API surface. Bound to a CompositeResource (XR).
CompositeResource (XR) + Composition Cluster-scoped. The Composition expands the XR into one or more concrete Managed Resources.
Managed Resources One-to-one with cloud API objects: RDSInstance, S3Bucket, DBSubnetGroup
Cloud Provider APIs The actual AWS / GCP / Azure resources, reconciled continuously by Provider controllers.

⚙️ Install & Providers

Install Crossplane

helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

helm install crossplane crossplane-stable/crossplane \
  --namespace crossplane-system \
  --create-namespace \
  --version 1.15.0

# Verify
kubectl get pods -n crossplane-system
kubectl get crds | grep crossplane

Install a Provider

Providers are OCI packages that add Managed Resource CRDs and reconciliation controllers for a specific cloud platform.

apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws-s3
spec:
  package: xpkg.upbound.io/upbound/provider-aws-s3:v1.1.0
  installationPolicy: Automatic
  revisionActivationPolicy: Automatic
ℹ️ Family Providers The official Upbound providers are split into family packages (e.g. provider-aws-s3, provider-aws-rds). Install only the sub-providers you need — avoids installing 800+ CRDs you'll never use.

ProviderConfig — Credentials

# Create a Secret with cloud credentials
kubectl create secret generic aws-creds \
  --namespace crossplane-system \
  --from-literal=creds="$(cat ~/.aws/credentials)"

# ProviderConfig references that secret
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-creds
      key: creds
💡 Use IRSA in production On EKS, configure the provider's ServiceAccount with IRSA (IAM Roles for Service Accounts) instead of static credentials. Set credentials.source: IRSA in ProviderConfig.

Managed Resources — Direct Cloud Objects

A Managed Resource (MR) maps 1-to-1 to a cloud API object. You can create them directly or via Compositions.

# Create an S3 bucket directly as a Managed Resource
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: my-app-assets
  annotations:
    crossplane.io/external-name: my-app-assets-prod-2024
spec:
  forProvider:
    region: us-east-1
    tags:
      env: production
      team: platform
  providerConfigRef:
    name: default
# Check status — Crossplane reconciles continuously
kubectl get bucket my-app-assets
# NAME             READY   SYNCED   EXTERNAL-NAME                  AGE
# my-app-assets    True    True     my-app-assets-prod-2024        2m

kubectl describe bucket my-app-assets
# Status.AtProvider shows the actual cloud state
# Status.Conditions shows Synced/Ready conditions
⚠️ Deletion policy By default, deleting a Managed Resource deletes the cloud resource. Set spec.deletionPolicy: Orphan to detach Crossplane management without destroying the resource.

🧩 CompositeResourceDefinitions & Compositions

This is Crossplane's superpower: a platform team defines a custom API (XRD) that hides cloud complexity, then writes a Composition to implement it. App teams just file a Claim — they never see RDS subnet groups or IAM roles.

1 — Define the Custom API (XRD)

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresqlinstances.db.example.com
spec:
  group: db.example.com
  names:
    kind: XPostgreSQLInstance
    plural: xpostgresqlinstances
  claimNames:                      # enables namespace-scoped Claims
    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
                    dbVersion:
                      type: string
                      default: "14"
                    region:
                      type: string
                  required: [storageGB, region]

2 — Write the Composition

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xpostgresqlinstances-aws
  labels:
    provider: aws
    db: postgres
spec:
  compositeTypeRef:
    apiVersion: db.example.com/v1alpha1
    kind: XPostgreSQLInstance
  resources:
    - name: rds-instance
      base:
        apiVersion: rds.aws.upbound.io/v1beta1
        kind: Instance
        spec:
          forProvider:
            engine: postgres
            instanceClass: db.t3.micro
            skipFinalSnapshot: true
            publiclyAccessible: false
          providerConfigRef:
            name: default
      patches:
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.storageGB
          toFieldPath: spec.forProvider.allocatedStorage
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.dbVersion
          toFieldPath: spec.forProvider.engineVersion
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.region
          toFieldPath: spec.forProvider.region
    - name: db-subnet-group
      base:
        apiVersion: rds.aws.upbound.io/v1beta1
        kind: SubnetGroup
        spec:
          forProvider:
            region: us-east-1
            description: "Managed by Crossplane"
          providerConfigRef:
            name: default

3 — Developer Files a Claim

# Developer in namespace "my-app" requests a database
# They only need to know storageGB and region — no RDS/subnet knowledge needed
apiVersion: db.example.com/v1alpha1
kind: PostgreSQLInstance
metadata:
  name: my-app-db
  namespace: my-app
spec:
  parameters:
    storageGB: 50
    dbVersion: "15"
    region: us-east-1
  compositionSelector:
    matchLabels:
      provider: aws
  writeConnectionSecretToRef:
    name: my-app-db-conn    # Crossplane writes host/port/user/pass here
# Watch the claim become ready
kubectl get postgresqlinstance -n my-app
# NAME         READY   CONNECTION-SECRET   AGE
# my-app-db    True    my-app-db-conn      4m

# The connection secret is automatically available to pods
kubectl get secret my-app-db-conn -n my-app -o yaml
💡 Connection secrets flow through Crossplane propagates connection details (host, port, username, password) from Managed Resources up through XR to the Claim's namespace secret automatically. Apps just mount the secret — no manual credential management.

⚖️ Crossplane vs Terraform

DimensionCrossplaneTerraform
Control loopContinuous reconciliation (Kubernetes controller)Manual plan/apply or CI-triggered
State storageKubernetes etcd (cluster is the state)Remote state file (S3, Terraform Cloud)
Drift detectionAutomatic — reconciler detects & corrects driftRequires explicit terraform plan
Developer APICustom CRDs via XRD — kubectl/GitOps nativeHCL modules, Terraform variables
RBACKubernetes RBAC on Claims/XRDs nativelyExternal (Vault, Terraform Cloud teams)
Learning curveHigh — Compositions are complex YAMLModerate — HCL is approachable
Ecosystem maturityGrowing — best coverage via Upbound providersMature — largest provider ecosystem
Best forPlatform teams building internal cloud APIsInfra teams managing large existing estates

Production Patterns

Platform-as-a-Product

Platform team owns XRDs and Compositions in a central repo. App teams consume via Claims in their namespace — no cloud knowledge required.

GitOps for Infra

Store Claims and Managed Resources in Git. ArgoCD/Flux applies them. Crossplane's continuous reconciliation handles the cloud side.

Multi-Cloud Abstraction

Same XRD, two Compositions (one for AWS, one for GCP). Switch environments by changing compositionSelector labels.

Self-Service Environments

Developers request full environments (DB + bucket + cache) by filing a single Claim. Composition wires everything together.

⚠️ Composition complexity Compositions with many patches, transforms, and readiness checks can become hard to debug. Use the crossplane beta trace command to visualise the full resource tree and spot which resource is blocking readiness.
# Trace a Claim's full resource tree
crossplane beta trace postgresqlinstance my-app-db -n my-app

# Output shows XR → Managed Resources → cloud status
# NAME                          SYNCED   READY   STATUS
# PostgreSQLInstance/my-app-db  True     True    Available
# ├─ XPostgreSQLInstance/...    True     True    Available
#    ├─ Instance/rds-...        True     True    Available
#    └─ SubnetGroup/sg-...      True     True    Available

📝 Knowledge Check

Q1. A developer deletes a PostgreSQLInstance Claim in their namespace. What happens to the underlying RDS instance in AWS by default?
  • A) Nothing — Crossplane only manages the Claim, not the cloud resource
  • B) The RDS instance is deleted automatically
  • C) The RDS instance is stopped but not deleted
  • D) Crossplane creates a final snapshot then deletes the instance
B) The RDS instance is deleted automatically. By default Crossplane's deletion policy is Delete — removing a Claim cascades to the XR, then to Managed Resources, which triggers deletion of the actual cloud resource. Use deletionPolicy: Orphan to prevent this.
Q2. What is the role of a Composition in Crossplane?
  • A) Defines the schema and API shape of a custom resource (XRD)
  • B) Maps a CompositeResource to one or more concrete Managed Resources via patches
  • C) Stores cloud provider credentials securely
  • D) Namespace-scoped resource that developers use to request infrastructure
B) Maps a CompositeResource to Managed Resources. The XRD defines the API schema. The Composition is the implementation — it declares which Managed Resources to create and how to map (patch) XR fields onto them.
Q3. A cloud engineer manually changes a Crossplane-managed S3 bucket's versioning setting directly in the AWS console. What happens next?
  • A) Crossplane ignores the change — it only acts on kubectl apply
  • B) Crossplane detects the drift and reconciles the bucket back to the desired state
  • C) Crossplane marks the resource as Unsynced and waits for manual intervention
  • D) The Provider crashes and must be restarted
B) Crossplane detects drift and reconciles. Provider controllers continuously compare the observed cloud state with the desired state in the Managed Resource spec. Any drift is automatically corrected — this is the key advantage of Crossplane's controller-based model over Terraform's apply-only model.
B) Crossplane detects drift and reconciles. Provider controllers continuously compare the observed cloud state with the desired state in the Managed Resource spec. Any drift is automatically corrected — this is the key advantage of Crossplane's controller-based model over Terraform's apply-only model.