Crossplane: Cloud Infrastructure as Kubernetes Resources
Crossplane turns your Kubernetes cluster into a control plane for cloud resources. A developer applies a Claim (namespace-scoped, like a Kubernetes Deployment) and Crossplane provisions an RDS instance, S3 bucket, or IAM role via the cloud provider API — tracked as a Kubernetes resource with the same lifecycle as the app. This covers Crossplane installation on EKS, AWS provider setup with Pod Identity, Managed Resources for direct AWS provisioning, and Compositions that abstract over cloud APIs to give teams a platform-defined interface.

Terraform and CloudFormation manage cloud infrastructure. Kubernetes manages application workloads. Crossplane bridges the two: it extends the Kubernetes API with custom resources that represent cloud infrastructure. When you apply a Crossplane RDSInstance resource, Crossplane calls the AWS API to provision the database and keeps the Kubernetes resource synchronized with the actual AWS state.
The practical consequence is that your GitOps workflow (Argo CD, Flux) manages infrastructure the same way it manages applications: a git commit provisions an RDS instance, a git revert deprovisions it. Crossplane doesn't replace Terraform — it replaces the need for a separate IaC toolchain when your team is already all-in on Kubernetes.
Crossplane Architecture
Crossplane runs as a Deployment in your cluster. When you apply a Crossplane resource (RDSInstance, S3Bucket), the Crossplane provider controller reconciles it:
- Provider reads the desired state from the Crossplane resource spec
- Provider calls the cloud API to create/update/delete the corresponding resource
- Provider writes the current state (ARNs, connection details, status) back to the resource's status subresource
- The resource's
READY=Truecondition means the cloud resource exists and is operational
Connection details (database endpoint, credentials) are written to a Kubernetes Secret that your application reads directly.
Installation
1helm repo add crossplane-stable https://charts.crossplane.io/stable
2helm repo update
3
4# Check https://github.com/crossplane/crossplane/releases for latest
5helm install crossplane crossplane-stable/crossplane \
6 --namespace crossplane-system \
7 --create-namespace \
8 --version 1.17.2 # Check https://github.com/crossplane/crossplane/releases for latest
9
10kubectl get pods -n crossplane-system
11# crossplane-<hash> Running
12# crossplane-rbac-manager-<hash> RunningAWS Provider Setup
Install the AWS provider (manages EC2, RDS, S3, IAM, and 1000+ other AWS services):
1apiVersion: pkg.crossplane.io/v1
2kind: Provider
3metadata:
4 name: provider-aws-rds
5spec:
6 package: xpkg.upbound.io/upbound/provider-aws-rds:v1.20.0
7 # Check https://marketplace.upbound.io/providers/upbound/provider-aws-rds for latestUpbound's AWS providers are split by service family (RDS, S3, IAM, EC2, etc.) — install only the families you need rather than the monolithic provider:
1kubectl apply -f - <<EOF
2apiVersion: pkg.crossplane.io/v1
3kind: Provider
4metadata:
5 name: provider-aws-s3
6spec:
7 package: xpkg.upbound.io/upbound/provider-aws-s3:v1.20.0
8---
9apiVersion: pkg.crossplane.io/v1
10kind: Provider
11metadata:
12 name: provider-aws-rds
13spec:
14 package: xpkg.upbound.io/upbound/provider-aws-rds:v1.20.0
15---
16apiVersion: pkg.crossplane.io/v1
17kind: Provider
18metadata:
19 name: provider-aws-iam
20spec:
21 package: xpkg.upbound.io/upbound/provider-aws-iam:v1.20.0
22EOF
23
24kubectl get provider
25# NAME INSTALLED HEALTHY PACKAGE
26# provider-aws-rds True True ...ProviderConfig: AWS Auth via Pod Identity
1apiVersion: aws.upbound.io/v1beta1
2kind: ProviderConfig
3metadata:
4 name: default
5spec:
6 credentials:
7 source: InjectedIdentity # InjectedIdentity for Pod Identity; IRSA for annotation-based rolesThe Crossplane provider pods need an IAM role with permissions to create/manage the resources you're provisioning. Create a Pod Identity association for the provider ServiceAccount:
1# The provider's ServiceAccount is in crossplane-system
2# Each provider has its own SA: provider-aws-rds → crossplane-system/provider-aws-rds-*
3# Use wildcard to handle the hash suffix
4aws eks create-pod-identity-association \
5 --cluster-name production \
6 --namespace crossplane-system \
7 --service-account "provider-aws-rds-*" \ # Exact SA name varies by provider
8 --role-arn arn:aws:iam::${AWS_ACCOUNT_ID}:role/CrossplaneProviderRDSThe IAM role needs permissions to manage the target resources: rds:CreateDBInstance, rds:DescribeDBInstances, rds:DeleteDBInstance, etc.
Managed Resources: Direct AWS Provisioning
A Managed Resource is the 1:1 mapping from a Kubernetes resource to a cloud resource. Managed Resources are always cluster-scoped — they have no namespace. Apply an RDS Instance, get an RDS instance:
1apiVersion: rds.aws.upbound.io/v1beta1
2kind: Instance # In provider-aws-rds the kind is Instance, not RDSInstance
3metadata:
4 name: payments-db # Cluster-scoped: no namespace field
5spec:
6 forProvider:
7 region: us-east-1
8 dbInstanceClass: db.t3.medium
9 engine: postgres
10 engineVersion: "16.4"
11 dbName: payments
12 allocatedStorage: 20
13 storageEncrypted: true
14 multiAZ: false # Set true for production HA
15 publiclyAccessible: false
16 vpcSecurityGroupIdSelector:
17 matchLabels:
18 crossplane.io/name: payments-db-sg # Reference a SecurityGroup managed resource
19
20 # Write connection details to this Secret
21 writeConnectionSecretToRef:
22 name: payments-db-conn
23 namespace: payments # Write the Secret to the app's namespace
24
25 providerConfigRef:
26 name: defaultAfter Crossplane reconciles this:
kubectl get instance payments-db
# NAME READY SYNCED AGE
# payments-db True True 5m
kubectl get secret payments-db-conn -n payments
# Contains: endpoint, port, username, passwordThe Secret contains the connection string your application uses. Update the RDSInstance spec (e.g., change dbInstanceClass) and Crossplane calls ModifyDBInstance — the same reconciliation loop as any Kubernetes controller.
Compositions: Platform-Defined Infrastructure APIs
Managed Resources map 1:1 to AWS resources. Compositions map a single user-facing resource to multiple cloud resources — defining a platform-level interface that hides provider details.
A platform team defines a PostgreSQLInstance API (abstracting over RDS):
1apiVersion: apiextensions.crossplane.io/v1
2kind: CompositeResourceDefinition
3metadata:
4 name: xpostgresqlinstances.platform.codingprotocols.com
5spec:
6 group: platform.codingprotocols.com
7 names:
8 kind: XPostgreSQLInstance
9 plural: xpostgresqlinstances
10 claimNames:
11 kind: PostgreSQLInstance # Namespace-scoped Claim (what developers use)
12 plural: postgresqlinstances
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 properties:
24 parameters:
25 type: object
26 required: ["storageGB", "tier"]
27 properties:
28 storageGB:
29 type: integer
30 minimum: 20
31 maximum: 1000
32 tier:
33 type: string
34 enum: ["dev", "production"]Then define the Composition that maps XPostgreSQLInstance to actual AWS resources:
The example below uses the classic spec.resources mode (Crossplane ≤ 1.13 style). Crossplane 1.14+ introduced spec.mode: Pipeline with a function-based model (function-patch-and-transform) — the recommended approach for new Compositions on current versions. The patch semantics are identical; only the outer structure differs.
1apiVersion: apiextensions.crossplane.io/v1
2kind: Composition
3metadata:
4 name: xpostgresqlinstances.aws.platform.codingprotocols.com
5spec:
6 compositeTypeRef:
7 apiVersion: platform.codingprotocols.com/v1alpha1
8 kind: XPostgreSQLInstance
9
10 resources:
11 - name: rdsinstance
12 base:
13 apiVersion: rds.aws.upbound.io/v1beta1
14 kind: Instance
15 spec:
16 forProvider:
17 region: us-east-1
18 engine: postgres
19 engineVersion: "16.4"
20 publiclyAccessible: false
21 storageEncrypted: true
22 providerConfigRef:
23 name: default
24 patches:
25 # Map tier→instanceClass: dev=t3.micro, production=m6g.large
26 - type: FromCompositeFieldPath
27 fromFieldPath: spec.parameters.tier
28 toFieldPath: spec.forProvider.dbInstanceClass
29 transforms:
30 - type: map
31 map:
32 dev: db.t3.micro
33 production: db.m6g.large
34 # Map storageGB parameter
35 - type: FromCompositeFieldPath
36 fromFieldPath: spec.parameters.storageGB
37 toFieldPath: spec.forProvider.allocatedStorageDeveloper Claim
Now developers create a PostgreSQLInstance Claim in their namespace without knowing anything about RDS instance classes, VPCs, or provider configs:
1# Developer creates this — no AWS knowledge required
2apiVersion: platform.codingprotocols.com/v1alpha1
3kind: PostgreSQLInstance
4metadata:
5 name: payments-db
6 namespace: payments
7spec:
8 parameters:
9 storageGB: 50
10 tier: production
11 writeConnectionSecretToRef:
12 name: payments-db-connCrossplane creates the full XPostgreSQLInstance cluster-scoped composite, then provisions the Instance managed resource. The developer sees READY=True when the database is available and reads payments-db-conn for the connection string.
GitOps Integration with Argo CD
Crossplane CRDs are standard Kubernetes resources — Argo CD manages them the same way it manages Deployments:
1# Argo CD Application for database claims
2apiVersion: argoproj.io/v1alpha1
3kind: Application
4metadata:
5 name: databases
6 namespace: argocd
7spec:
8 source:
9 repoURL: https://github.com/codingprotocols/k8s-infra
10 path: databases/production # Directory containing PostgreSQLInstance Claims
11 targetRevision: main
12 destination:
13 server: https://kubernetes.default.svc
14 namespace: payments
15 syncPolicy:
16 automated:
17 prune: true
18 selfHeal: true
19 syncOptions:
20 - CreateNamespace=trueArgo CD sync waves work naturally with Crossplane: use sync wave -1 for Claims (provision the database) and wave 0 for the application Deployment. The application waits for the database to be READY=True before deploying. See Argo CD: GitOps Continuous Delivery for Kubernetes for sync wave configuration.
Frequently Asked Questions
Does Crossplane replace Terraform?
For teams already operating Kubernetes, Crossplane can replace Terraform for cloud infrastructure that is tightly coupled to Kubernetes workloads (databases, queues, IAM roles for IRSA). The GitOps workflow, familiar kubectl commands, and unified lifecycle management (app + infra in one kubectl apply) are the main advantages. Crossplane is weaker for infrastructure that's not cluster-adjacent (VPCs, multi-region networking, account-level IAM), where Terraform's provider ecosystem is more mature. The realistic approach: Crossplane for per-workload infrastructure (databases, queues), Terraform for foundational infrastructure (VPCs, EKS clusters, route tables).
What happens to the cloud resource if I delete the Crossplane resource?
By default, Crossplane deletes the cloud resource when you delete the Kubernetes resource (the deletionPolicy defaults to Delete). Set deletionPolicy: Orphan on the managed resource to decouple the Kubernetes resource deletion from the cloud resource deletion — the AWS RDS instance will remain even after the Kubernetes resource is removed:
spec:
deletionPolicy: Orphan # Kubernetes object deletion does NOT delete the RDS instanceUse Orphan for stateful resources in production where an accidental kubectl delete shouldn't destroy data.
How do I import existing AWS resources into Crossplane?
Use the managementPolicies: ["Observe"] spec to observe an existing resource (read-only) without modifying it:
1spec:
2 managementPolicies: ["Observe"] # Read-only: never call create/update/delete
3 forProvider:
4 region: us-east-1
5 externalName: "existing-rds-identifier" # Set to the existing AWS resource ID
6 providerConfigRef:
7 name: defaultWith Observe policy, Crossplane syncs state from AWS into the resource status but never calls create/update/delete. To transition an observed resource into full Crossplane management, change managementPolicies to ["*"] (or remove the field, which defaults to full management) once you've confirmed the observed state is correct.
For the Crossplane Composition model, Pipeline mode with function-patch-and-transform, and a deep dive on XRDs and Claims, see Crossplane: Cloud Infrastructure as Kubernetes Resources. For the GitOps workflow that deploys Crossplane Claims alongside application manifests, see Argo CD: GitOps Continuous Delivery for Kubernetes. For External Secrets Operator that pulls secrets from AWS Secrets Manager into the cluster (a complement to Crossplane's connection Secrets), see Secrets Management on Kubernetes: HashiCorp Vault vs External Secrets Operator.
Evaluating Crossplane for infrastructure management on EKS or designing a Composition-based developer platform? Talk to us at Coding Protocols — we help platform teams implement self-service infrastructure APIs that give developers fast provisioning without direct AWS access.


