Screenshot from the article

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 resourceskubectl get managed
  • Verify in AWS Consoleaws s3 ls --profile raj-private aws rds describe-db-instances --profile raj-private
  • Access the appkubectl port-forward -n sample-app svc/sample-app-service 8080:80

7. Monitoring & Troubleshooting

  • watch kubectl get managed for real-time updates
  • Use kubectl describe on 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.