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.

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
| Concept | Description |
|---|---|
| Provider | Plugin 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 |
| Composition | Maps XR fields to managed resource configurations |
| Claim (XRC) | Namespace-scoped request for a Composite Resource (what developers create) |
Installation
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 version1# 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
23EOFProvider Credentials (Pod Identity)
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-providerThe 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:
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: defaultCrossplane 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:
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: booleanDevelopers 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:
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.0A Composition maps XRD fields to managed resource configurations:
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.endpointDeveloper Claim
Developers create a Claim in their namespace — simple, opinionated, no AWS knowledge required:
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: productionAfter creating this Claim:
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.comThe 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):
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:
kubectl delete postgresinstance payments-db -n productionThe 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:
# 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 productionFor 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.


