Crossplane 2026: Manage AWS Infrastructure as Kubernetes YAML
Infrastructure-as-code has been dominated by tools like Terraform for years. But what if your entire cloud infrastructure — S3 buckets, RDS databases, VPCs, IAM roles — could be declared as Kubernetes YAML and managed by the same control loop that keeps your pods running? That is exactly what Crossplane delivers.
This crossplane tutorial walks you through the complete workflow: installing Crossplane on a Kubernetes cluster, configuring the AWS provider, creating real cloud resources with kubectl apply, building Composite Resource Definitions (XRDs) to create internal abstractions, and comparing Crossplane vs Terraform across ten practical criteria.
1. What Crossplane Is
Crossplane is a CNCF (Cloud Native Computing Foundation) graduated project that extends Kubernetes with Custom Resource Definitions (CRDs) for managing cloud infrastructure. When you install Crossplane and a cloud provider plugin, your Kubernetes API server learns new resource types such as Bucket, RDSInstance, VPC, and SecurityGroup.
The Control Loop Model
Kubernetes reconciles every object continuously: it reads the desired state from etcd, compares it to the actual state of the cluster, and acts to close any gap. Crossplane applies exactly the same pattern to cloud infrastructure.
Declare desired state (YAML) → Crossplane reads object
→ Crossplane calls cloud API
→ Cloud resource created/updated
→ Crossplane watches for drift
→ Drift detected → reconcile again
This is continuous reconciliation, not a one-time apply. If someone manually deletes an S3 bucket through the AWS Console, Crossplane notices and recreates it — without you running any command.
Crossplane vs Terraform
Terraform uses a plan/apply workflow: you run terraform plan to preview changes, then terraform apply to execute them. State is stored in a .tfstate file (or remote backend). Drift detection requires explicit terraform refresh or terraform plan runs.
Crossplane uses continuous reconciliation: state is stored in Kubernetes etcd, drift detection is built-in and always active, and there is no separate CLI step to apply changes — pushing YAML to the cluster is enough.
2. Install Crossplane with Helm
Prerequisites: a running Kubernetes cluster (EKS, GKE, kind, or k3s all work) and Helm 3.
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm install crossplane crossplane-stable/crossplane \
--namespace crossplane-system \
--create-namespace
Verify the installation:
kubectl get pods -n crossplane-system
# NAME READY STATUS RESTARTS
# crossplane-7d8f9b4d5c-xkqzp 1/1 Running 0
# crossplane-rbac-manager-6c9f7b8d4-mnjkl 1/1 Running 0
Crossplane is now running and waiting for provider plugins.
3. Install the AWS Provider
Crossplane providers are Kubernetes packages that ship CRDs and a controller for a specific cloud. Install the official AWS provider:
# aws-provider.yaml
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws-s3
spec:
package: xpkg.upbound.io/upbound/provider-aws-s3:v1.7.0
kubectl apply -f aws-provider.yaml
kubectl get providers
# NAME INSTALLED HEALTHY PACKAGE
# provider-aws-s3 True True upbound/provider-aws-s3:v1.7.0
Configure AWS Credentials
Store your AWS credentials in a Kubernetes Secret, then create a ProviderConfig that references it:
kubectl create secret generic aws-secret \
-n crossplane-system \
--from-literal=creds="[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# provider-config.yaml
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-secret
key: creds
kubectl apply -f provider-config.yaml
4. First Resource: Create an S3 Bucket
With the provider installed, you can create an S3 bucket using a Kubernetes manifest — no AWS CLI or Console needed.
# my-bucket.yaml
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
name: my-crossplane-bucket-2026
spec:
forProvider:
region: us-east-1
providerConfigRef:
name: default
kubectl apply -f my-bucket.yaml
Check the sync status:
kubectl get bucket my-crossplane-bucket-2026
# NAME READY SYNCED EXTERNAL-NAME AGE
# my-crossplane-bucket-2026 True True my-crossplane-bucket-2026 45s
READY=True means the bucket exists in AWS. SYNCED=True means Crossplane's last reconciliation succeeded. The bucket now appears in your AWS Console exactly as if you had created it directly.
To delete the bucket, simply delete the Kubernetes object:
kubectl delete bucket my-crossplane-bucket-2026
Crossplane calls the AWS API and removes the actual bucket. No orphaned resources.
5. Create an RDS PostgreSQL Instance
The AWS RDS provider creates managed databases with the same declarative pattern:
# rds-postgres.yaml
apiVersion: rds.aws.upbound.io/v1beta1
kind: RDSInstance
metadata:
name: my-postgres-db
spec:
forProvider:
region: us-east-1
dbInstanceClass: db.t3.micro
engine: postgres
engineVersion: "15.4"
masterUsername: adminuser
allocatedStorage: 20
skipFinalSnapshot: true
publiclyAccessible: false
writeConnectionSecretToRef:
namespace: default
name: my-postgres-db-conn
providerConfigRef:
name: default
kubectl apply -f rds-postgres.yaml
Notice writeConnectionSecretToRef: Crossplane automatically retrieves the database endpoint, port, username, and password from AWS and writes them into a Kubernetes Secret named my-postgres-db-conn. Your application pods can mount this secret without any manual credential distribution step.
kubectl get secret my-postgres-db-conn -o yaml
# data:
# endpoint: bXktcG9zdGdyZXMtZGIuY3h4eHh4LnVzLWVhc3QtMS5yZHMuYW1hem9uYXdzLmNvbQ==
# password: UGFzc3dvcmQxMjM0NTY3OA==
# port: NTQzMg==
# username: YWRtaW51c2Vy
6. Composite Resources (XRDs) — The Killer Feature
Individual managed resources are useful, but the real power of Crossplane is infrastructure as code for platform teams: you define internal abstractions that hide cloud complexity from application developers.
Define a CompositeResourceDefinition
A CompositeResourceDefinition (XRD) declares a new custom API type in your cluster:
# xrd-database-cluster.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabaseclusters.platform.example.com
spec:
group: platform.example.com
names:
kind: XDatabaseCluster
plural: xdatabaseclusters
claimNames:
kind: DatabaseCluster
plural: databaseclusters
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
instanceClass:
type: string
environment:
type: string
enum: [dev, staging, prod]
required: [storageGB, instanceClass, environment]
Define a Composition
A Composition maps the abstract type to real AWS resources. One XDatabaseCluster can create an RDS instance, a security group, a parameter group, and enable Enhanced Monitoring — all wired together automatically:
# composition-database-cluster.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: database-cluster-aws
spec:
compositeTypeRef:
apiVersion: platform.example.com/v1alpha1
kind: XDatabaseCluster
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: RDSInstance
spec:
forProvider:
region: us-east-1
engine: postgres
engineVersion: "15.4"
masterUsername: adminuser
skipFinalSnapshot: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.allocatedStorage
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.instanceClass
toFieldPath: spec.forProvider.dbInstanceClass
- name: security-group
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroup
spec:
forProvider:
region: us-east-1
description: RDS security group managed by Crossplane
7. Claims: Developer Self-Service Without AWS Knowledge
With the XRD and Composition in place, any developer on any team can request a database by creating a Claim — a namespace-scoped resource that hides all implementation details:
# my-postgres.yaml
apiVersion: platform.example.com/v1alpha1
kind: DatabaseCluster
metadata:
name: my-app-postgres
namespace: team-backend
spec:
parameters:
storageGB: 50
instanceClass: db.t3.small
environment: prod
writeConnectionSecretToRef:
name: my-app-postgres-conn
kubectl apply -f my-postgres.yaml
The developer does not know which AWS region the database lives in, what security groups are attached, whether monitoring is enabled, or how the parameter group is tuned. The platform team controls all of that through the Composition. The developer gets a connection secret in their namespace and a running database.
8. Crossplane vs Terraform: Detailed Comparison
| Criterion | Crossplane | Terraform |
|---|---|---|
| Drift detection | Continuous, automatic | Manual (terraform plan) |
| State file | Stored in Kubernetes etcd | .tfstate file or remote backend |
| Kubernetes-native | Yes — full kubectl/RBAC/GitOps support | No — separate CLI and state |
| Multi-team self-service | Claims + XRDs enable namespace isolation | Requires module design + workspace conventions |
| Secrets handling | Written directly to Kubernetes Secrets | Stored in state file (sensitive data exposed) |
| GitOps compatible | First-class (ArgoCD, Flux work natively) | Requires wrapper tooling (Atlantis, etc.) |
| Language | YAML / Kubernetes API | HCL (HashiCorp Configuration Language) |
| Provider ecosystem | Growing (AWS, GCP, Azure, Helm, SQL) | Mature (thousands of community providers) |
| Learning curve | Steep if new to Kubernetes | Moderate (HCL is purpose-built) |
| Deletion safety | Deleting the K8s object deletes the resource | terraform destroy required explicitly |
For teams already running Kubernetes and using GitOps, Crossplane's continuous reconciliation and native RBAC integration offer significant operational advantages over Terraform. For teams without Kubernetes, or those managing resources outside of any cluster, Terraform remains the more practical choice.
9. Crossplane vs AWS CDK, Pulumi, and Ansible
AWS CDK: CDK generates CloudFormation templates using TypeScript, Python, or Java. It is tightly coupled to AWS and requires the CloudFormation service plane. Crossplane is cloud-agnostic and Kubernetes-native.
Pulumi: Pulumi uses general-purpose languages (TypeScript, Go, Python) and supports many clouds. Like Terraform, it uses a separate state backend and CLI workflow. Crossplane has no separate state backend — Kubernetes etcd is the source of truth.
Ansible: Ansible is a procedural configuration management tool. It can create cloud resources but has no concept of desired-state reconciliation or drift detection. Each playbook run is a one-time imperative execution.
The key differentiator for Crossplane is the control loop: as long as your cluster is running, Crossplane continuously ensures that your declared infrastructure matches reality. None of the alternatives above provide this without external tooling.
10. FAQ
Do I need a running Kubernetes cluster to use Crossplane? Yes. Crossplane runs as a set of controllers inside Kubernetes. It cannot run standalone. A small managed cluster (EKS, GKE, AKS) or even a local kind cluster works fine.
What happens to my AWS resources if I delete the Crossplane controller? Existing AWS resources are not deleted when Crossplane itself is removed. Crossplane adds a finalizer to each managed resource to prevent accidental deletion, but if the controller stops running, it simply stops reconciling. Resources remain intact in AWS.
Can Crossplane import existing AWS resources? Yes. Set the crossplane.io/external-name annotation on a managed resource to the existing AWS resource ID. Crossplane will adopt the resource rather than create a new one.
Is Crossplane production-ready? Crossplane graduated from the CNCF incubator in 2024. Major users include Deutsche Telekom, Grafana Labs, and Upbound (the primary maintainer). The AWS, GCP, and Azure providers are stable and widely used.
How does Crossplane handle cost management? Crossplane does not manage cost directly. However, because all infrastructure is declared in Git and audited through Kubernetes RBAC, platform teams can enforce policies (using tools like OPA/Gatekeeper) that prevent developers from requesting oversized instances.
Can I use Crossplane alongside Terraform? Yes. Many organizations adopt Crossplane incrementally — managing new resources with Crossplane while keeping existing Terraform-managed infrastructure in place. There is no technical conflict between the two.
Next Steps
- Install Crossplane locally with kind to experiment without AWS costs
- Explore the Upbound Marketplace for pre-built provider families
- Read the Crossplane Composition documentation to learn advanced patching and transforms
- Integrate with ArgoCD or Flux to achieve full GitOps for both application and infrastructure manifests