Kubernetes
15 min readMay 7, 2026

Crossplane: Cloud Infrastructure as Kubernetes Resources

Crossplane turns Kubernetes into a universal control plane for cloud infrastructure. You define an RDS instance, S3 bucket, or VPC as a Kubernetes custom resource — and Crossplane provisions and manages it in AWS, GCP, or Azure. Developers get a simple API; platform teams get a single reconciliation loop for both application workloads and their infrastructure dependencies.

CO
Coding Protocols Team
Platform Engineering
Crossplane: Cloud Infrastructure as Kubernetes Resources

Most infrastructure as code tools (Terraform, Pulumi, CloudFormation) run outside Kubernetes — they're imperative tools you execute, not controllers that continuously reconcile. Crossplane (CNCF incubating) brings cloud infrastructure into the Kubernetes reconciliation model: you declare what infrastructure you want as a Kubernetes custom resource, and Crossplane continuously reconciles the actual cloud resource against that declaration.

This model has a significant advantage for platform engineering: the same GitOps workflow that manages application deployments (Flux, Argo CD) also manages cloud infrastructure. One pull request can provision an RDS instance and deploy the application that uses it, with Argo CD reconciling both.


Core Architecture

Crossplane has four main layers:

Developer creates a Claim (e.g., PostgreSQLInstance)
        ↓
Crossplane resolves Claim to a Composite Resource (XR) via XRD
        ↓
Composition maps XR fields to Managed Resources (e.g., aws_rds_instance, aws_subnet_group)
        ↓
Provider reconciles Managed Resources against actual cloud resources in AWS/GCP/Azure
ConceptDescription
ProviderPlugin for a cloud platform (AWS, GCP, Azure); contains managed resource controllers
Managed Resource (MR)Direct representation of a cloud resource (e.g., RDSInstance)
Composite Resource (XR)Platform-defined abstraction composing multiple managed resources
Composite Resource Definition (XRD)Defines the API schema for a Composite Resource
CompositionMaps XR fields to managed resource configurations
Claim (XRC)Namespace-scoped request for a Composite Resource (what developers create)

Installation

bash
1helm repo add crossplane-stable https://charts.crossplane.io/stable
2helm repo update
3
4helm install crossplane crossplane-stable/crossplane \
5  --namespace crossplane-system \
6  --create-namespace \
7  --version 1.17.2  # Crossplane is in the 1.x release series — check https://github.com/crossplane/crossplane/releases for current version
bash
1# Install the AWS Upbound provider
2kubectl apply -f - <<EOF
3apiVersion: pkg.crossplane.io/v1
4kind: Provider
5metadata:
6  name: upbound-provider-aws-rds
7spec:
8  package: xpkg.upbound.io/upbound/provider-aws-rds:v1.21.1
9  runtimeConfigRef:
10    name: upbound-provider-aws
11EOF
12
13# Install the EC2 provider (needed for VPC, subnet, security group resources)
14kubectl apply -f - <<EOF
15apiVersion: pkg.crossplane.io/v1
16kind: Provider
17metadata:
18  name: upbound-provider-aws-ec2
19spec:
20  package: xpkg.upbound.io/upbound/provider-aws-ec2:v1.21.1
21  runtimeConfigRef:
22    name: upbound-provider-aws
23EOF

Provider Credentials (Pod Identity)

yaml
1# ProviderConfig — tells the provider which AWS account to use and how to authenticate
2apiVersion: aws.upbound.io/v1beta1
3kind: ProviderConfig
4metadata:
5  name: default
6spec:
7  credentials:
8    source: IRSA    # Use IRSA or InjectedIdentity (Pod Identity)
9---
10# RuntimeConfig — configure the provider pod to use Pod Identity
11apiVersion: pkg.crossplane.io/v1beta1
12kind: DeploymentRuntimeConfig
13metadata:
14  name: upbound-provider-aws
15spec:
16  serviceAccountTemplate:
17    metadata:
18      annotations:
19        eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/crossplane-aws-provider

The IAM role needs permissions to create and manage the resources you want Crossplane to provision (RDS, VPC, S3, IAM roles for workloads, etc.).


Managed Resources (Direct Provisioning)

You can create managed resources directly — useful for understanding the API before building Compositions:

yaml
1# Directly provision an RDS instance (no abstraction layer)
2apiVersion: rds.aws.upbound.io/v1beta1
3kind: Instance
4metadata:
5  name: payments-db
6  annotations:
7    crossplane.io/external-name: payments-db-production   # AWS resource name
8spec:
9  forProvider:
10    region: us-east-1
11    dbInstanceClass: db.t3.medium
12    engine: postgres
13    engineVersion: "16.2"
14    allocatedStorage: 100
15    storageType: gp3
16    storageEncrypted: true
17    dbName: payments
18    username: postgres
19    passwordSecretRef:
20      name: rds-master-password
21      namespace: crossplane-system
22      key: password
23    vpcSecurityGroupIdRefs:
24      - name: payments-rds-sg
25    dbSubnetGroupNameRef:
26      name: private-subnet-group
27    multiAz: true
28    backupRetentionPeriod: 7
29    deletionProtection: true
30    skipFinalSnapshot: false
31    finalSnapshotIdentifier: payments-db-final-snapshot
32  providerConfigRef:
33    name: default

Crossplane reconciles this resource continuously — if someone manually modifies the RDS instance in the AWS console (changing instance class, disabling Multi-AZ), Crossplane detects the drift and reverts it to match the spec. This is the key difference from Terraform: Crossplane is a controller that runs continuously, not a one-time apply.


Composite Resource Definition (XRD)

XRDs define the developer-facing API. Platform teams design the schema to expose only the fields developers need, hiding cloud-specific complexity:

yaml
1apiVersion: apiextensions.crossplane.io/v1
2kind: CompositeResourceDefinition
3metadata:
4  name: xpostgresinstances.platform.example.com
5spec:
6  group: platform.example.com
7  names:
8    kind: XPostgresInstance
9    plural: xpostgresinstances
10  claimNames:
11    kind: PostgresInstance         # What developers create (namespace-scoped)
12    plural: postgresinstances
13  versions:
14    - name: v1alpha1
15      served: true
16      referenceable: true
17      schema:
18        openAPIV3Schema:
19          type: object
20          properties:
21            spec:
22              type: object
23              required: [storageGB, size]
24              properties:
25                storageGB:
26                  type: integer
27                  minimum: 20
28                  maximum: 1000
29                  description: "Storage size in GB"
30                size:
31                  type: string
32                  enum: [small, medium, large]
33                  description: "small=t3.medium, medium=m5.large, large=m5.2xlarge"
34                version:
35                  type: string
36                  default: "16"
37                  enum: ["15", "16"]
38                  description: "PostgreSQL major version"
39                highAvailability:
40                  type: boolean
41                  default: false
42                  description: "Enable Multi-AZ for production"
43            status:
44              type: object
45              properties:
46                endpoint:
47                  type: string
48                  description: "RDS endpoint (populated after provisioning)"
49                ready:
50                  type: boolean

Developers only see storageGB, size, version, and highAvailability. All VPC, security group, subnet group, parameter group, and backup configuration is handled by the Composition.


Composition

Pipeline mode Compositions require the referenced Function to be installed first. Install the upbound-function-patch-and-transform Function before applying the Composition below:

yaml
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
  name: upbound-function-patch-and-transform
spec:
  package: xpkg.upbound.io/upbound/function-patch-and-transform:v0.7.0

A Composition maps XRD fields to managed resource configurations:

yaml
1apiVersion: apiextensions.crossplane.io/v1
2kind: Composition
3metadata:
4  name: xpostgresinstances.platform.example.com
5  labels:
6    provider: aws
7    environment: production
8spec:
9  compositeTypeRef:
10    apiVersion: platform.example.com/v1alpha1
11    kind: XPostgresInstance
12  mode: Pipeline    # Pipeline mode for transforms and patches
13  pipeline:
14    - step: patch-and-transform
15      functionRef:
16        name: upbound-function-patch-and-transform
17      input:
18        apiVersion: pt.fn.crossplane.io/v1beta1
19        kind: Resources
20        resources:
21          - name: rds-instance
22            base:
23              apiVersion: rds.aws.upbound.io/v1beta1
24              kind: Instance
25              spec:
26                forProvider:
27                  region: us-east-1
28                  engine: postgres
29                  storageType: gp3
30                  storageEncrypted: true
31                  backupRetentionPeriod: 7
32                  deletionProtection: true
33                  skipFinalSnapshot: false
34                  dbSubnetGroupNameRef:
35                    name: private-subnet-group
36                  vpcSecurityGroupIdRefs:
37                    - name: platform-rds-sg
38                providerConfigRef:
39                  name: default
40            patches:
41              # Map size → dbInstanceClass
42              - type: FromCompositeFieldPath
43                fromFieldPath: spec.size
44                toFieldPath: spec.forProvider.dbInstanceClass
45                transforms:
46                  - type: map
47                    map:
48                      small: db.t3.medium
49                      medium: db.m5.large
50                      large: db.m5.2xlarge
51              # Map storageGB
52              - type: FromCompositeFieldPath
53                fromFieldPath: spec.storageGB
54                toFieldPath: spec.forProvider.allocatedStorage
55              # Map version
56              - type: FromCompositeFieldPath
57                fromFieldPath: spec.version
58                toFieldPath: spec.forProvider.engineVersion
59                transforms:
60                  - type: map
61                    map:
62                      "15": "15.7"
63                      "16": "16.2"
64              # Map highAvailability → multiAz
65              - type: FromCompositeFieldPath
66                fromFieldPath: spec.highAvailability
67                toFieldPath: spec.forProvider.multiAz
68              # Write endpoint back to composite status
69              - type: ToCompositeFieldPath
70                fromFieldPath: status.atProvider.endpoint
71                toFieldPath: status.endpoint

Developer Claim

Developers create a Claim in their namespace — simple, opinionated, no AWS knowledge required:

yaml
1apiVersion: platform.example.com/v1alpha1
2kind: PostgresInstance
3metadata:
4  name: payments-db
5  namespace: production
6spec:
7  storageGB: 100
8  size: medium
9  version: "16"
10  highAvailability: true
11  compositionSelector:
12    matchLabels:
13      provider: aws
14      environment: production

After creating this Claim:

bash
1kubectl get postgresinstance payments-db -n production
2# NAME          READY   CONNECTION-SECRET   AGE
3# payments-db   True    payments-db-conn    8m
4
5# Connection details stored in a Secret
6kubectl get secret payments-db-conn -n production -o jsonpath='{.data.endpoint}' | base64 -d
7# payments-db.xxxxx.us-east-1.rds.amazonaws.com

The connection details (endpoint, port, username) are populated in the referenced Secret automatically. Applications mount this Secret to get connection credentials without knowing anything about the underlying AWS infrastructure.


Composition Functions

Crossplane v1.14+ introduced Composition Functions — more powerful transformations using Go or Python code (packaged as OCI images):

yaml
1# Function for complex logic that can't be expressed in patch-and-transform
2spec:
3  pipeline:
4    - step: generate-rds-config
5      functionRef:
6        name: function-go-templating   # Go template function
7      input:
8        apiVersion: gotemplating.fn.crossplane.io/v1beta1
9        kind: GoTemplate
10        source: Inline
11        inline:
12          template: |
13            apiVersion: rds.aws.upbound.io/v1beta1
14            kind: Instance
15            spec:
16              forProvider:
17                finalSnapshotIdentifier: {{ .observed.composite.resource.metadata.name }}-final
18                tags:
19                  Name: {{ .observed.composite.resource.metadata.name }}
20                  CostCenter: {{ index .observed.composite.resource.metadata.labels "cost-center" | default "unknown" }}

Composition Functions enable complex logic — conditional resource creation, loops over lists, external API calls during composition — that patch-and-transform can't express.


Frequently Asked Questions

Should I use Crossplane or Terraform for cloud infrastructure?

The key question is whether you want continuous reconciliation (Crossplane) or one-time apply with drift detection (Terraform). Crossplane automatically corrects drift — if someone manually changes a cloud resource, Crossplane reverts it. Terraform detects drift with terraform plan but requires a human to apply the correction. For Kubernetes-native platforms where GitOps manages everything, Crossplane is compelling. For organisations with existing Terraform expertise and pipelines, Terraform remains the pragmatic choice. The two can coexist: Terraform manages cluster infrastructure (VPC, EKS cluster), Crossplane manages per-application infrastructure (databases, queues) that developers request through Claims.

What happens if the Crossplane provider pod crashes?

Crossplane is eventually consistent — if the provider is unavailable, managed resource reconciliation pauses. Cloud resources are not affected (Crossplane doesn't send delete/modify signals when it's down). When the provider recovers, it reconciles all managed resources and corrects any drift that accumulated while it was unavailable.

How do I handle secret rotation for managed resources?

Crossplane stores secrets (connection details, generated passwords) in Kubernetes Secrets. For RDS password rotation, use AWS Secrets Manager with automatic rotation and a separate controller that updates the passwordSecretRef Secret when AWS rotates the password. Alternatively, use crossplane-contrib/provider-aws-secretsmanager to manage the Secrets Manager secret itself as a managed resource alongside the RDS instance.

How do I delete a managed resource created by a Claim?

Delete the Claim — Crossplane cascades the deletion to the Composite Resource and all Managed Resources it created:

bash
kubectl delete postgresinstance payments-db -n production

The deletion is blocked by deletionProtection: true on the RDS instance (the cloud resource itself). You must first disable deletion protection before Crossplane can delete the RDS instance:

bash
# Edit the MR to disable deletion protection, then delete
kubectl patch instance.rds payments-db-xxxxx \
  --type merge -p '{"spec":{"forProvider":{"deletionProtection":false}}}'
# Then delete the Claim
kubectl delete postgresinstance payments-db -n production

For platform engineering patterns where Crossplane provides developer self-service infrastructure, see Platform Engineering: Building an Internal Developer Platform. For Terraform-based cluster infrastructure that Crossplane runs on, see Terraform for EKS. For GitOps workflows that manage Crossplane Claims alongside application deployments, see Flux CD: GitOps for Kubernetes. For a deeper look at Crossplane installation on EKS with Pod Identity, Managed Resources, and the Compositions pattern end-to-end, see Crossplane: Cloud Infrastructure from Kubernetes.

Building developer self-service cloud infrastructure on Kubernetes? Talk to us at Coding Protocols — we help platform teams design Crossplane Compositions that give developers what they need while maintaining the guardrails they require.

Related Topics

Crossplane
Kubernetes
IaC
AWS
Platform Engineering
CNCF
Cloud Native
DevOps

Read Next