Crossplane on Kubernetes: Moving Beyond Terraform to GitOps Control Planes

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

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.

Share: