How to Set Up Crossplane for Self-Service Infrastructure on Kubernetes
A step-by-step tutorial on setting up Crossplane to provision and manage cloud infrastructure directly from Kubernetes. Build a self-service platform where developers can request AWS, GCP, or Azure resources through kubectl.
Your developers want to spin up an RDS database. Today they either file a Jira ticket and wait two days, or they hack together Terraform in a repo they barely understand. Neither option is good.
Crossplane solves this by bringing infrastructure provisioning into Kubernetes. Developers create a YAML manifest — just like they'd create a Deployment or Service — and Crossplane provisions the actual cloud resource. An S3 bucket, an RDS instance, a VPC, a DNS record — all managed through kubectl.
This tutorial walks you through setting up Crossplane from scratch, connecting it to AWS, and building a self-service infrastructure platform.
What Crossplane Actually Does
Crossplane is a Kubernetes operator that extends your cluster with Custom Resource Definitions (CRDs) for cloud resources. When you apply a manifest, Crossplane's controllers talk to the cloud provider API and create the real resource.
The key concepts:
- Provider — a plugin that knows how to talk to a specific cloud (AWS, GCP, Azure, etc.)
- Managed Resource — a Kubernetes resource that maps to a single cloud resource (e.g.,
RDSInstance,S3Bucket) - Composite Resource (XR) — a custom abstraction you define that bundles multiple managed resources together (e.g., "Database" = RDS instance + subnet group + security group)
- Claim (XRC) — the developer-facing API. Developers create claims, and Crossplane creates the underlying composite resources
The power is in the abstraction layer:
Developer creates: Platform team defines: Crossplane provisions:
┌─────────────┐ ┌───────────────────┐ ┌─────────────────────┐
│ DatabaseClaim│ ──→ │ CompositeDatabase │ ──→ │ RDS Instance │
│ name: mydb │ │ (XR Definition) │ │ Subnet Group │
│ size: small │ │ │ │ Security Group │
│ engine: pg │ │ │ │ Parameter Group │
└─────────────┘ └───────────────────┘ └─────────────────────┘
Developers don't need to know about subnet groups or security groups. They just say "I want a small PostgreSQL database" and the platform team's abstractions handle the rest.
Prerequisites
Before you start, you need:
- A Kubernetes cluster (1.28+) — EKS, GKE, AKS, or even a local kind cluster for testing
kubectlconfigured to talk to your clusterhelmv3 installed- An AWS account with programmatic access (we'll use AWS in this tutorial, but the concepts apply to any cloud)
Step 1: Install Crossplane
Install Crossplane using Helm:
# Add the Crossplane Helm repo
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
# Install Crossplane
helm install crossplane \
crossplane-stable/crossplane \
--namespace crossplane-system \
--create-namespace \
--set args='{"--enable-usages"}'Verify the installation:
# Check that Crossplane pods are running
kubectl get pods -n crossplane-system
# You should see:
# crossplane-xxxxxxxxx-xxxxx 1/1 Running
# crossplane-rbac-manager-xxxxx 1/1 RunningInstall the Crossplane CLI (optional but helpful):
# Install the crossplane kubectl plugin
curl -sL "https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh" | sh
sudo mv crossplane /usr/local/bin/Step 2: Install the AWS Provider
Crossplane needs a provider to talk to AWS:
# 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.14.0
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws-rds
spec:
package: xpkg.upbound.io/upbound/provider-aws-rds:v1.14.0
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws-ec2
spec:
package: xpkg.upbound.io/upbound/provider-aws-ec2:v1.14.0Apply it:
kubectl apply -f aws-provider.yaml
# Wait for the provider to become healthy
kubectl get providers
# NAME INSTALLED HEALTHY PACKAGE AGE
# provider-aws-s3 True True xpkg.upbound.io/upbound/provider-aws-s3:v1.14.0 60s
# provider-aws-rds True True xpkg.upbound.io/upbound/provider-aws-rds:v1.14.0 60s
# provider-aws-ec2 True True xpkg.upbound.io/upbound/provider-aws-ec2:v1.14.0 60sStep 3: Configure AWS Credentials
Create a Kubernetes secret with your AWS credentials:
# Create a credentials file
cat > aws-credentials.txt << EOF
[default]
aws_access_key_id = YOUR_ACCESS_KEY_ID
aws_secret_access_key = YOUR_SECRET_ACCESS_KEY
EOF
# Create the Kubernetes secret
kubectl create secret generic aws-creds \
-n crossplane-system \
--from-file=creds=./aws-credentials.txt
# Clean up the plaintext file
rm aws-credentials.txtNow create a ProviderConfig that tells the AWS providers how to authenticate:
# aws-provider-config.yaml
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-creds
key: credskubectl apply -f aws-provider-config.yamlFor production, use IRSA (IAM Roles for Service Accounts) on EKS instead of static credentials:
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: IRSAThis is more secure because the credentials are automatically rotated and scoped to the specific service account.
Step 4: Provision Your First Resource
Let's start simple — create an S3 bucket:
# s3-bucket.yaml
apiVersion: s3.aws.upbound.io/v1beta2
kind: Bucket
metadata:
name: my-crossplane-bucket
spec:
forProvider:
region: us-east-1
tags:
ManagedBy: crossplane
Environment: dev
providerConfigRef:
name: defaultkubectl apply -f s3-bucket.yaml
# Watch the bucket get created
kubectl get bucket my-crossplane-bucket -w
# NAME READY SYNCED EXTERNAL-NAME AGE
# my-crossplane-bucket True True my-crossplane-bucket 45sWhen READY and SYNCED are both True, the real S3 bucket exists in AWS. You can verify:
aws s3 ls | grep crossplane
# 2026-03-17 10:15:32 my-crossplane-bucketTo delete it, just delete the Kubernetes resource:
kubectl delete bucket my-crossplane-bucket
# The real S3 bucket gets deleted tooThis is the fundamental loop: Kubernetes resources map to cloud resources. Create, update, delete — all through kubectl.
Step 5: Build a Composite Resource (The Abstraction Layer)
Raw managed resources are powerful but verbose. For a self-service platform, you want to create abstractions that hide complexity.
Let's build a "Database" abstraction that creates an RDS instance with all its dependencies.
Define the Composite Resource Definition (XRD)
This defines the API — what parameters developers can set:
# database-xrd.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.platform.devopsboys.com
spec:
group: platform.devopsboys.com
names:
kind: XDatabase
plural: xdatabases
claimNames:
kind: Database
plural: databases
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
engine:
type: string
enum: [postgres, mysql]
default: postgres
size:
type: string
enum: [small, medium, large]
default: small
region:
type: string
default: us-east-1
required:
- engine
- sizeDefine the Composition
This defines what gets created when someone requests a Database:
# database-composition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: database-aws
labels:
provider: aws
spec:
compositeTypeRef:
apiVersion: platform.devopsboys.com/v1alpha1
kind: XDatabase
resources:
- name: securitygroup
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroup
spec:
forProvider:
region: us-east-1
description: "Managed by Crossplane - Database SG"
ingress:
- fromPort: 5432
toPort: 5432
protocol: tcp
cidrBlocks:
- "10.0.0.0/8"
patches:
- fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
- name: rdsinstance
base:
apiVersion: rds.aws.upbound.io/v1beta2
kind: Instance
spec:
forProvider:
region: us-east-1
instanceClass: db.t3.micro
engine: postgres
engineVersion: "16"
allocatedStorage: 20
publiclyAccessible: false
skipFinalSnapshot: true
masterUsername: admin
masterPasswordSecretRef:
namespace: crossplane-system
name: db-master-password
key: password
patches:
- fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
- fromFieldPath: spec.parameters.engine
toFieldPath: spec.forProvider.engine
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.size
toFieldPath: spec.forProvider.instanceClass
transforms:
- type: map
map:
small: db.t3.micro
medium: db.t3.medium
large: db.r6g.largeApply both:
kubectl apply -f database-xrd.yaml
kubectl apply -f database-composition.yamlStep 6: Developers Request Infrastructure
Now developers can create databases with a simple claim:
# my-database.yaml
apiVersion: platform.devopsboys.com/v1alpha1
kind: Database
metadata:
name: orders-db
namespace: team-backend
spec:
parameters:
engine: postgres
size: medium
region: us-east-1kubectl apply -f my-database.yaml
# Watch the resources get created
kubectl get database orders-db -n team-backend
kubectl get xdatabase -l crossplane.io/claim-name=orders-db
kubectl get instance # The actual RDS instanceThe developer just requested a production-grade PostgreSQL database with:
- Proper security group
- Correct instance sizing
- All the networking handled
And they did it with 10 lines of YAML. No Terraform. No Jira ticket. No waiting.
Step 7: Track Resource Status
Crossplane provides full visibility into resource state:
# See all managed resources and their status
kubectl get managed
# Detailed status of a specific resource
kubectl describe instance orders-db-xxxxx
# See the composition tree
crossplane beta trace database orders-db -n team-backendThe crossplane beta trace command shows the full resource tree:
NAME SYNCED READY STATUS
Database/orders-db (team-backend) True True Available
└─ XDatabase/orders-db-xxxxx True True Available
├─ SecurityGroup/orders-db-sg-xxxxx True True Available
└─ Instance/orders-db-rds-xxxxx True True Available
Step 8: Add Drift Detection and Correction
One of Crossplane's most powerful features is continuous reconciliation. If someone manually modifies a resource in the AWS console, Crossplane detects the drift and corrects it.
Test it:
# Manually change something in AWS
aws rds modify-db-instance \
--db-instance-identifier orders-db-rds-xxxxx \
--allocated-storage 50
# Within a few minutes, Crossplane will detect the drift
# and revert it back to 20 GB (what the manifest says)
kubectl describe instance orders-db-rds-xxxxx | grep "allocated"This is GitOps for infrastructure — your Kubernetes manifests are the source of truth.
Production Considerations
Before running Crossplane in production, address these:
RBAC — Control Who Can Create What
# Allow the backend team to create Databases but not raw AWS resources
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: database-creator
namespace: team-backend
rules:
- apiGroups: ["platform.devopsboys.com"]
resources: ["databases"]
verbs: ["get", "list", "create", "update", "delete"]Cost Controls — Set Limits on Resource Sizes
Use Kyverno or OPA to enforce policies:
# Kyverno policy: no large databases in dev namespaces
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: no-large-db-in-dev
spec:
validationFailureAction: Enforce
rules:
- name: check-db-size
match:
any:
- resources:
kinds:
- Database
namespaces:
- "team-*-dev"
validate:
message: "Large databases are not allowed in dev namespaces"
pattern:
spec:
parameters:
size: "!large"Backup — Don't Lose Your State
Crossplane state lives in Kubernetes (etcd). Back up your cluster state regularly:
# Use Velero for cluster backup
velero backup create crossplane-backup \
--include-resources=databases.platform.devopsboys.com,xdatabases.platform.devopsboys.comCrossplane vs Terraform: When to Use Which
This isn't an either/or choice. They serve different use cases:
Use Crossplane when:
- You want self-service infrastructure for developers
- Your platform is Kubernetes-native
- You need continuous drift detection and correction
- You want to expose simplified APIs to internal teams
Use Terraform when:
- You're bootstrapping infrastructure from scratch (VPCs, clusters, accounts)
- You need complex dependency graphs across many providers
- Your team is already proficient in Terraform
- You need to manage resources that don't have Crossplane providers
Many teams use both: Terraform to set up the foundational infrastructure (VPCs, EKS clusters) and Crossplane inside the cluster for day-2 operations (databases, caches, queues).
Wrapping Up
Crossplane turns your Kubernetes cluster into a universal control plane for cloud infrastructure. Developers get self-service access through simple YAML claims. Platform teams define the abstractions and guardrails. Cloud resources are managed with the same GitOps workflows you already use for applications.
The setup takes an afternoon. The payoff — developers shipping faster without waiting for infrastructure tickets — lasts forever.
Want to learn Kubernetes and platform engineering hands-on? KodeKloud's Kubernetes courses cover everything from CKA prep to advanced cluster operations with real lab environments.
Building your platform on cloud infrastructure? DigitalOcean's managed Kubernetes gives you a production-ready cluster in minutes — perfect for running Crossplane.
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
Edge Computing Will Decentralize Kubernetes by 2028
Why Kubernetes is moving from centralized cloud clusters to distributed edge deployments. Covers KubeEdge, k3s, Akri, and the architectural shift toward edge-native infrastructure.
Kubernetes Will Become Invisible by 2028 — And That's the Point
The engineers who built Kubernetes never wanted you to think about it. A new generation of abstractions is quietly removing Kubernetes from the developer's line of sight — and the companies doing it best are winning the talent war.
Serverless Containers Will Kill Kubernetes Complexity — Here's Why
AWS Fargate, Google Cloud Run, and Azure Container Apps are making raw Kubernetes management obsolete. The future is serverless containers — and it's closer than you think.