Crossplane Infrastructure Management on Kubernetes
Crossplane extends Kubernetes with the ability to provision and manage cloud infrastructure using the same GitOps workflows used for applications. This guide covers deploying Crossplane on Kubernetes, installing cloud providers, creating managed resources, and building compositions for self-service infrastructure claims.
Prerequisites
- Kubernetes cluster 1.26+ (Minikube, kind, or production cluster)
kubectlconfigured with cluster-admin permissions- Helm 3.x
- A cloud provider account (AWS, GCP, or Azure) for provisioning real resources
Install Crossplane
# Add the Crossplane Helm repository
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
# Install Crossplane in its own namespace
helm install crossplane \
crossplane-stable/crossplane \
--namespace crossplane-system \
--create-namespace \
--set args='{"--debug"}' \
--wait
# Verify the installation
kubectl get pods -n crossplane-system
kubectl get crds | grep crossplane
# Install the Crossplane CLI
curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh
sudo mv crossplane /usr/local/bin/
crossplane --version
Install a Cloud Provider
Crossplane uses provider packages to manage specific clouds. Here we install the AWS provider:
# Install the AWS provider
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
# Watch the provider install
kubectl get providers -w
# For a full AWS provider (all services)
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws
spec:
package: xpkg.upbound.io/upbound/provider-family-aws:v0.47.0
runtimeConfigRef:
name: provider-aws-config
EOF
Configure Provider Credentials
# Create AWS credentials secret
AWS_PROFILE=default
cat > /tmp/aws-credentials.txt <<EOF
[default]
aws_access_key_id = $(aws configure get aws_access_key_id)
aws_secret_access_key = $(aws configure get aws_secret_access_key)
EOF
kubectl create secret generic aws-secret \
-n crossplane-system \
--from-file=creds=/tmp/aws-credentials.txt
# Clean up the temp file
rm /tmp/aws-credentials.txt
# Create a ProviderConfig referencing the secret
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
Create Managed Resources
Managed resources are direct mappings to cloud resources. Here's an S3 bucket:
# s3-bucket.yaml
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
name: my-crossplane-bucket
annotations:
crossplane.io/external-name: my-org-crossplane-demo-2024
spec:
forProvider:
region: us-east-1
tags:
Environment: production
ManagedBy: crossplane
providerConfigRef:
name: default
kubectl apply -f s3-bucket.yaml
# Watch the resource sync
kubectl get bucket my-crossplane-bucket -w
# Check sync status
kubectl describe bucket my-crossplane-bucket
Create an RDS instance:
# rds-instance.yaml
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
metadata:
name: my-postgres-db
spec:
forProvider:
region: us-east-1
instanceClass: db.t3.micro
engine: postgres
engineVersion: "15.3"
dbName: appdb
username: dbadmin
passwordSecretRef:
name: rds-password
namespace: default
key: password
allocatedStorage: 20
skipFinalSnapshot: true
providerConfigRef:
name: default
# Create the password secret first
kubectl create secret generic rds-password \
--from-literal=password='SuperSecurePass123!'
kubectl apply -f rds-instance.yaml
kubectl get instance my-postgres-db -w
Build Compositions and Composite Resources
Compositions combine multiple managed resources into reusable abstractions:
# composite-resource-definition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresinstances.platform.example.com
spec:
group: platform.example.com
names:
kind: XPostgresInstance
plural: xpostgresinstances
claimNames:
kind: PostgresInstance
plural: postgresinstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
default: 20
region:
type: string
default: us-east-1
required:
- storageGB
# composition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: postgres-aws
labels:
provider: aws
db: postgres
spec:
compositeTypeRef:
apiVersion: platform.example.com/v1alpha1
kind: XPostgresInstance
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
region: us-east-1
instanceClass: db.t3.micro
engine: postgres
engineVersion: "15.3"
dbName: appdb
username: dbadmin
skipFinalSnapshot: true
providerConfigRef:
name: default
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.allocatedStorage
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
kubectl apply -f composite-resource-definition.yaml
kubectl apply -f composition.yaml
Self-Service Infrastructure Claims
Namespace-scoped claims let developers request infrastructure without cluster-admin access:
# postgres-claim.yaml - deployed by application team
apiVersion: platform.example.com/v1alpha1
kind: PostgresInstance
metadata:
name: my-app-database
namespace: team-alpha
spec:
parameters:
storageGB: 50
region: us-east-1
compositionSelector:
matchLabels:
provider: aws
db: postgres
writeConnectionSecretToRef:
name: my-app-db-credentials
kubectl apply -f postgres-claim.yaml
# The claim creates a composite resource, which creates the RDS instance
kubectl get postgresinstance -n team-alpha
kubectl get xpostgresinstance
# Connection details written to a secret once ready
kubectl get secret my-app-db-credentials -n team-alpha -o yaml
GitOps Integration
Store all Crossplane resources in Git and apply them via Argo CD or Flux:
# argo-cd application for infrastructure
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: platform-infrastructure
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/infrastructure
targetRevision: main
path: crossplane/
destination:
server: https://kubernetes.default.svc
namespace: crossplane-system
syncPolicy:
automated:
prune: true
selfHeal: true
# Directory structure for GitOps
# infrastructure/
# ├── crossplane/
# │ ├── providers/
# │ │ └── aws-provider.yaml
# │ ├── compositions/
# │ │ └── postgres-composition.yaml
# │ └── xrds/
# │ └── postgres-xrd.yaml
# └── claims/
# └── team-alpha/
# └── postgres-claim.yaml
Troubleshooting
Provider stuck in "Installing" state:
kubectl describe provider provider-aws
kubectl get pods -n crossplane-system
# Check if the provider pod has image pull errors:
kubectl get events -n crossplane-system --sort-by='.lastTimestamp'
Managed resource not syncing:
# Check conditions on the resource
kubectl describe bucket my-crossplane-bucket | grep -A 20 "Conditions:"
# View Crossplane controller logs
kubectl logs -n crossplane-system -l app=crossplane --tail=100
# Check if the resource is ready
kubectl get managed
ProviderConfig credential errors:
# Verify the secret exists and has correct keys
kubectl get secret aws-secret -n crossplane-system -o jsonpath='{.data.creds}' | base64 -d
# Describe the ProviderConfig
kubectl describe providerconfig default
Composition patches not applying:
# Validate your composition
crossplane beta validate composition.yaml
# Check composite resource status
kubectl describe xpostgresinstance
Conclusion
Crossplane brings infrastructure provisioning into Kubernetes-native GitOps workflows, enabling platform teams to expose self-service abstractions via compositions and claims. Developers can request databases, buckets, and networking resources through simple Kubernetes objects without needing direct cloud access. Combined with Argo CD or Flux, Crossplane delivers true GitOps-driven infrastructure management that scales with your organization.


