
Introduction
For years, I’ve used Terraform to manage cloud infrastructure. It’s powerful and battle-tested — but I always felt there was a gap. That awkward moment when you’re deploying apps with kubectl but then have to switch gears and run terraform apply for infrastructure changes? It’s like speaking two different languages in the same conversation.
That’s when I found Crossplane.
Crossplane lets you manage AWS resources using the same Kubernetes primitives you already know. No more context switching. No separate state files. Just pure YAML — for both apps and infrastructure.
In this guide, I’ll walk you through a real-world Crossplane demo that provisions:
- RDS PostgreSQL
- S3 buckets
- Security groups
- A sample application
All managed through kubectl.
What You’ll Learn
- Setting up Crossplane on Minikube
- Configuring AWS providers & credentials
- Creating real AWS resources (VPC, RDS, S3, Security Groups)
- Deploying a sample application
- Monitoring & troubleshooting
- Cleanup & best practices
Why Crossplane Over Terraform?
Terraform isn’t going anywhere — it’s still excellent for many scenarios. But Crossplane has some unique strengths:
✅ Unified API — Everything is managed with kubectl. ✅ GitOps Ready – Works seamlessly with ArgoCD and Flux. ✅ RBAC for Infrastructure – Apply Kubernetes RBAC to cloud resources. ✅ Real-time Monitoring – Leverage existing K8s observability tools.
1. Setting Up the Environment
Prerequisites
Make sure you have:
- Docker Desktop
- Minikube
- kubectl
- AWS CLI with a configured profile
- Helm 3.x
Start Minikube with Adequate Resources
minikube start --driver=docker --memory=6144 --cpus=4 --disk-size=20g
minikube addons enable ingress
minikube addons enable metrics-server
minikube status
kubectl get nodes
2. Installing Crossplane
Install the Crossplane Controller
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm install crossplane crossplane-stable/crossplane \
--namespace crossplane-system \
--create-namespace \
--wait
kubectl get pods -n crossplane-system
kubectl get crd | grep crossplane
Install the AWS Provider
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-aws:v0.44.0
EOF
kubectl wait --for=condition=healthy provider.pkg.crossplane.io/provider-aws --timeout=600s
kubectl get crd | grep aws
3. Configuring AWS Credentials
kubectl create secret generic aws-creds -n crossplane-system \
--from-literal=creds="[default]
aws_access_key_id = $(aws configure get aws_access_key_id --profile raj-private)
aws_secret_access_key = $(aws configure get aws_secret_access_key --profile raj-private)
region = eu-west-1"
Create the ProviderConfig:
cat <<EOF | kubectl apply -f -
apiVersion: aws.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-creds
key: creds
EOF
4. Creating AWS Infrastructure
Crossplane resources are just Kubernetes manifests. Here’s what we’ll build:
VPC
apiVersion: ec2.aws.crossplane.io/v1beta1
kind: VPC
metadata:
name: crossplane-demo-vpc
spec:
forProvider:
region: eu-west-1
cidrBlock: 172.31.0.0/16
enableDnsHostnames: true
enableDnsSupport: true
tags:
Name: crossplane-demo-vpc
Environment: demo
Security Groups
apiVersion: ec2.aws.crossplane.io/v1beta1
kind: SecurityGroup
metadata:
name: crossplane-demo-sg-app
spec:
forProvider:
region: eu-west-1
vpcIdSelector:
matchLabels:
name: crossplane-demo-vpc
groupName: crossplane-demo-sg-app
description: Security group for demo application
tags:
Name: crossplane-demo-sg-app
Environment: demo
RDS PostgreSQL
apiVersion: rds.aws.crossplane.io/v1alpha1
kind: RDSInstance
metadata:
name: crossplane-demo-rds
spec:
forProvider:
region: eu-west-1
dbInstanceClass: db.t3.micro
engine: postgres
engineVersion: "13.22"
dbName: demodb
masterUsername: dbuser
allocatedStorage: 20
storageType: gp2
storageEncrypted: true
publiclyAccessible: false
multiAZ: false
tags:
Name: crossplane-demo-rds
Environment: demo
writeConnectionSecretsToRef:
name: rds-connection
namespace: default
S3 Bucket
apiVersion: s3.aws.crossplane.io/v1beta1
kind: Bucket
metadata:
name: crossplane-demo-bucket-2024-ireland
spec:
forProvider:
region: eu-west-1
tags:
Name: crossplane-demo-bucket-2024-ireland
Environment: demo
deletionPolicy: Delete
5. Deploying a Sample Application
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-app
namespace: sample-app
spec:
replicas: 2
selector:
matchLabels:
app: sample-app
template:
metadata:
labels:
app: sample-app
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: sample-app-service
namespace: sample-app
spec:
selector:
app: sample-app
ports:
- port: 80
targetPort: 80
type: LoadBalancer
6. Testing & Verification
- Check all managed resources:
kubectl get managed - Verify in AWS Console:
aws s3 ls --profile raj-privateaws rds describe-db-instances --profile raj-private - Access the app:
kubectl port-forward -n sample-app svc/sample-app-service 8080:80
7. Monitoring & Troubleshooting
watch kubectl get managedfor real-time updates- Use
kubectl describeon any resource for detailed events - Check AWS CLI for actual resource status
8. Cleanup
kubectl delete -f sample-app.yaml
kubectl delete -f rds-instance.yaml --wait=false
kubectl delete -f s3-bucket.yaml
kubectl delete -f security-groups.yaml
kubectl delete -f vpc.yaml
helm uninstall crossplane -n crossplane-system
minikube stop
Key Takeaways
The Good
- Unified workflow with
kubectl - Seamless GitOps integration
- RBAC for infrastructure
- Real-time feedback
The Challenges
- Steeper learning curve without Kubernetes experience
- Dependency management can be tricky
- Some providers less mature
FAQs
Is Crossplane production-ready? Yes — especially for AWS, GCP, and Azure providers.
How does it store state? In Kubernetes etcd — no separate files.
Can I migrate from Terraform? Yes, but not automatically. Start with new workloads.
Conclusion
Crossplane flips the script on infrastructure-as-code by treating cloud resources as Kubernetes resources. If you’re already invested in Kubernetes, this can be a game-changer.
The demo here created real AWS infrastructure — RDS, S3, Security Groups — entirely with YAML and kubectl. That’s a powerful concept.
🚀 Try it yourself: GitHub Repo 💬 Feedback? Connect with me
Written by Rajesh Sood
📢 Have questions or feedback? Drop a comment below or connect with me on Twitter/X@spysood!
Originally published on Medium.