The Shift from Terraform to Control Planes
Terraform has dominated the Infrastructure as Code (IaC) landscape for a decade. It is a reliable tool, but it operates on a “push-based” model. You run a plan, apply the changes, and the process ends. The problem? If a team member manually tweaks a security group in the AWS Console at 2:00 AM, Terraform won’t notice until your next manual execution. This is the definition of configuration drift.
Crossplane flips this model by turning your Kubernetes cluster into a universal control plane. Instead of managing state files, you define infrastructure as Kubernetes Custom Resources (CRDs). A controller then continuously reconciles your cloud resources. If something changes in the real world, Crossplane changes it back. I have deployed this in production environments where it successfully managed over 500 managed resources, keeping infra and app code in a single, unified GitOps pipeline.
Quick Start: Deploying an S3 Bucket in Under 5 Minutes
You only need a running Kubernetes cluster and Helm to start. We will install Crossplane and a specific AWS provider to spin up a bucket.
1. Install Crossplane
kubectl create namespace crossplane-system
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm install crossplane --namespace crossplane-system crossplane-stable/crossplane
2. Install the AWS Provider
Modern Crossplane uses “family providers” to stay lightweight. Instead of installing every AWS service, we will only install the S3 controller. This keeps the controller’s memory footprint low, often under 150MB compared to the 2GB+ seen in older monolithic versions.
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws-s3
spec:
package: xpkg.upbound.io/upbound/provider-aws-s3:v0.47.0
EOF
3. Configure Credentials
Crossplane needs permission to talk to AWS. Create a creds.conf file with your AWS keys, then generate a Kubernetes secret:
kubectl create secret generic aws-secret -n crossplane-system --from-file=creds=./creds.conf
cat <<EOF | kubectl apply -f -
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-secret
key: creds
EOF
4. Provision Your First Resource
Now we define the bucket using standard YAML. This looks exactly like a Deployment or Service manifest.
cat <<EOF | kubectl apply -f -
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
name: my-crossplane-test-bucket
spec:
forProvider:
region: us-east-1
providerConfigRef:
name: default
EOF
Check your progress with kubectl get buckets. Once the READY column shows True, your physical bucket exists in AWS.
Why Kubernetes is the Best Infrastructure Engine
The secret sauce isn’t the YAML; it’s the Control Loop. Crossplane never stops watching. If an S3 bucket is deleted via the AWS CLI, Crossplane detects the discrepancy during its next sync and recreates it automatically. It provides a self-healing infrastructure that Terraform simply cannot match without external automation.
Infrastructure as Data
By treating infrastructure as Kubernetes objects, you can use the tools you already love. Use kubectl to debug, k9s to visualize, and ArgoCD to deploy. In my experience, this bridge helps developers take ownership of their own cloud resources. They no longer need to learn HCL or wait for a DevOps engineer to run a pipeline.
The Composition Pattern
Compositions are Crossplane’s most powerful feature. They allow you to bundle complex resources into a single, custom API. For example, you can create a StandardDatabase object. When a developer creates that one object, Crossplane automatically provisions an RDS instance, the required security groups, and a private subnet group behind the scenes.
Advanced Usage: The GitOps Workflow
To get the most out of Crossplane, pair it with ArgoCD. This setup creates a hands-off environment where Git is the absolute source of truth. Your repository structure should look something like this:
/infra/providers/: Defines which cloud providers to install./infra/definitions/: Your custom Compositions (the blueprints)./apps/production/resources/: The actual infrastructure requests (the instances).
When you merge a PR to update a bucket’s tags, ArgoCD syncs the change to the cluster. Crossplane then immediately updates the cloud resource. There are no local state files to corrupt and no terraform plan commands to run manually.
Practical Tips from the Field
After migrating several large-scale environments to Crossplane, I’ve identified a few critical best practices.
1. Stick to Granular Providers
Avoid the all-in-one provider-aws. It attempts to load thousands of CRDs, which can slow down your cluster’s API server. Use the family providers like provider-aws-rds or provider-aws-ec2 to keep your management cluster snappy and efficient.
2. Secure Your Secrets
Crossplane writes sensitive data, such as database passwords, into Kubernetes Secrets. Don’t leave these sitting in plain text in your repo. Use the External Secrets Operator to sync these values to a secure vault like AWS Secrets Manager or HashiCorp Vault.
3. Use the Orphan Deletion Policy
By default, deleting a YAML file in Kubernetes will delete the resource in AWS. During a migration, this is dangerous. Add deletionPolicy: Orphan to your resource specs. This tells Crossplane to stop managing the resource without destroying the actual cloud hardware if the YAML is removed.
4. Don’t Migrate Everything at Once
You don’t need to delete your Terraform code tomorrow. Crossplane coexists perfectly with existing tools. Start by moving simple resources like SQS queues or S3 buckets. Let Terraform handle the foundational networking (VPCs and Subnets) while Crossplane handles the high-churn application resources.
Moving to a Kubernetes-native control plane is a major leap toward true Platform Engineering. It replaces manual triggers with a self-healing system that works 24/7 to keep your infrastructure exactly where it should be.

