Introduction
In the dynamic world of cloud-native development, managing infrastructure efficiently and consistently is paramount. Traditional Infrastructure as Code (IaC) tools like Terraform or CloudFormation have long been staples, but they often operate in a silo, separate from the very orchestrator managing your applications: Kubernetes. This creates a cognitive load and operational overhead as developers and operators must context-switch between different toolchains and APIs.
Enter Crossplane, an open-source Kubernetes add-on that extends your Kubernetes cluster to manage and provision infrastructure from any cloud, on-premises, or edge environment. By transforming your Kubernetes API into a universal control plane, Crossplane allows you to provision databases, message queues, object storage, and even entire clusters using familiar kubectl commands and YAML manifests. This groundbreaking approach unifies your application and infrastructure management under a single, declarative paradigm, bringing the power of Kubernetes reconciliation loops to your entire infrastructure estate.
This guide will walk you through the journey of integrating Crossplane into your Kubernetes environment, demonstrating how to provision and manage cloud resources directly from your cluster. We’ll cover installation, provider configuration, creating composite resources, and practical examples of deploying cloud services. Get ready to unlock a new level of infrastructure automation and consistency, all orchestrated by your favorite container orchestrator.
TL;DR: Crossplane for Universal IaC
Crossplane extends Kubernetes to manage external infrastructure, turning your cluster into a universal control plane. Provision cloud databases, S3 buckets, and more using kubectl and YAML.
Key Commands:
- Install Crossplane:
helm repo add crossplane-stable https://charts.crossplane.io/stable helm repo update helm install crossplane crossplane-stable/crossplane --namespace crossplane-system --create-namespace - Install AWS Provider:
kubectl apply -f - < - Configure AWS Provider Credentials (example):
kubectl create secret generic aws-creds -n crossplane-system --from-literal=credentials='[default] aws_access_key_id = YOUR_ACCESS_KEY_ID aws_secret_access_key = YOUR_SECRET_ACCESS_KEY' - Apply ProviderConfig:
apiVersion: aws.upbound.io/v1beta1 kind: ProviderConfig metadata: name: default spec: credentials: source: Secret secretRef: namespace: crossplane-system name: aws-creds key: credentials region: us-east-1 - Create a Composite Resource (e.g., PostgreSQL instance):
apiVersion: example.org/v1alpha1 kind: XPostgreSQLInstance metadata: name: my-app-db spec: parameters: storageGB: 20 version: "13" writeConnectionSecretToRef: name: my-app-db-connection namespace: default - Get all Crossplane resources:
kubectl get crds | grep crossplane.io
Prerequisites
Before diving into Crossplane, ensure you have the following:
- A Kubernetes Cluster: Any conformant Kubernetes cluster (v1.20+) will work. This can be local (Minikube, Kind, Docker Desktop) or a cloud-managed cluster (EKS, AKS, GKE).
kubectl: The Kubernetes command-line tool, configured to connect to your cluster. Refer to the official Kubernetes documentation for installation instructions.helm: The Kubernetes package manager, version 3.x or later. Install Helm by following the official Helm documentation.- Cloud Provider Account: An account with your chosen cloud provider (e.g., AWS, Azure, GCP) and appropriate programmatic access keys/credentials. For AWS, you'll need an IAM user with permissions to create resources like S3 buckets, RDS instances, etc.
- Basic Kubernetes Knowledge: Familiarity with Kubernetes concepts like Pods, Deployments, Services, Custom Resource Definitions (CRDs), and Namespaces will be helpful.
Step-by-Step Guide
1. Install Crossplane to Your Cluster
The first step is to install the Crossplane core components into your Kubernetes cluster. Crossplane uses Helm for installation, making it straightforward to deploy and manage.
Crossplane components include the Crossplane controller, which watches for Crossplane-specific Custom Resources and orchestrates the creation of external infrastructure, and various CRDs that define the API for managing these resources. It's recommended to install Crossplane into its own namespace, typically crossplane-system, to keep its components isolated.
# Add the Crossplane Helm repository
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
# Install Crossplane into the 'crossplane-system' namespace
helm install crossplane crossplane-stable/crossplane --namespace crossplane-system --create-namespace
Verify:
After installation, you should see Crossplane pods running in the crossplane-system namespace. It might take a minute for all pods to reach a Running state.
kubectl get pods -n crossplane-system
Expected Output:
NAME READY STATUS RESTARTS AGE
crossplane-7f78c5d95-8bc2h 1/1 Running 0 2m15s
crossplane-rbac-manager-6799564f89-8d75w 1/1 Running 0 2m15s
You can also inspect the CRDs that Crossplane has installed:
kubectl get crds | grep crossplane.io
Expected Output:
configurations.pkg.crossplane.io 2024-01-01T00:00:00Z
compositions.apiextensions.crossplane.io 2024-01-01T00:00:00Z
... (many more Crossplane CRDs)
2. Install a Cloud Provider
Crossplane interacts with specific cloud providers through "Providers." A Provider is essentially a set of controllers and CRDs that understand how to translate Crossplane resource requests (e.g., S3Bucket, RDSInstance) into cloud provider API calls (e.g., AWS S3 API, AWS RDS API). For this guide, we'll use provider-aws, but Crossplane supports many other providers including Azure, GCP, and even local providers like Kubernetes itself.
Installing a provider involves applying a Provider resource manifest to your cluster. This tells Crossplane to fetch and install the specific provider package.
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-aws:v0.40.0
kubectl apply -f - <
Verify:
Check the status of the installed provider. It should eventually show HEALTHY: True.
kubectl get providers
Expected Output:
NAME PACKAGE HEALTHY INSTALLED REASON
provider-aws xpkg.upbound.io/crossplane-contrib/provider-aws:v0.40.0 True True
You should also see new pods for the AWS provider controller in the crossplane-system namespace.
kubectl get pods -n crossplane-system
Expected Output:
NAME READY STATUS RESTARTS AGE
crossplane-7f78c5d95-8bc2h 1/1 Running 0 5m
crossplane-rbac-manager-6799564f89-8d75w 1/1 Running 0 5m
provider-aws-776c5b967-vpl2c 1/1 Running 0 1m30s
3. Configure Cloud Provider Credentials
For Crossplane to interact with your cloud provider, it needs credentials. It's best practice to store these credentials securely as a Kubernetes Secret. For AWS, this involves creating a secret with your AWS Access Key ID and Secret Access Key. Ensure the IAM user associated with these credentials has the necessary permissions to create and manage the resources you intend to provision.
First, create a file named aws-creds.conf with your AWS credentials:
cat > aws-creds.conf <<EOF
[default]
aws_access_key_id = YOUR_AWS_ACCESS_KEY_ID
aws_secret_access_key = YOUR_AWS_SECRET_ACCESS_KEY
EOF
Replace YOUR_AWS_ACCESS_KEY_ID and YOUR_AWS_SECRET_ACCESS_KEY with your actual credentials. Never commit these directly to version control.
Then, create a Kubernetes Secret from this file:
kubectl create secret generic aws-creds -n crossplane-system --from-file=credentials=./aws-creds.conf
Next, you need to create a ProviderConfig resource. This resource tells the AWS Provider which credentials to use and which AWS region to operate in. You can have multiple ProviderConfig resources for different regions or different sets of credentials.
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-creds
key: credentials
region: us-east-1 # Specify your desired AWS region
kubectl apply -f - <
Verify:
Check the status of your ProviderConfig. It should show READY: True.
kubectl get providerconfigs
Expected Output:
NAME AGE HEALTHY READY
default 1m True True
4. Provision a Cloud Resource (e.g., AWS S3 Bucket)
Now that Crossplane and the AWS Provider are configured, you can start provisioning resources. Let's create an S3 bucket using a Kubernetes manifest.
The Bucket resource is a Crossplane-managed resource, meaning it maps directly to an AWS S3 bucket. Crossplane will observe this resource, call the AWS API to create the bucket, and update the Kubernetes resource's status with the actual state from AWS.
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
name: my-kubezilla-crossplane-bucket-12345
spec:
forProvider:
region: us-east-1
acl: private
tags:
Environment: Development
Project: KubezillaCrossplane
providerConfigRef:
name: default
kubectl apply -f - <
Verify:
Check the status of your Bucket resource. It will initially be READY: False and then transition to True once the S3 bucket is provisioned in AWS.
kubectl get bucket my-kubezilla-crossplane-bucket-12345
Expected Output (after a short wait):
NAME STATUS STATE AGE
my-kubezilla-crossplane-bucket-12345 True Available 1m
You can also inspect the full details of the resource, including events and external name:
kubectl describe bucket my-kubezilla-crossplane-bucket-12345
Finally, verify the bucket exists in your AWS console or via the AWS CLI. For more complex networking needs for your applications, consider exploring our guide on Kubernetes Network Policies.
5. Define and Use Composite Resources (XRs)
While provisioning individual cloud resources is powerful, Crossplane's true strength lies in its ability to define Composite Resources (XRs). XRs allow you to abstract complex infrastructure patterns into simpler, application-centric APIs. For example, instead of asking for an "S3 bucket," "RDS instance," and "VPC security group," an application developer can ask for a "managed database service" and let Crossplane provision all underlying components.
This is achieved through a CompositeResourceDefinition (XRD) and a Composition.
5.1 Create a CompositeResourceDefinition (XRD)
An XRD defines the schema for your custom composite resource. Let's define an XPostgreSQLInstance.
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.example.org
spec:
group: example.org
names:
kind: XPostgreSQLInstance
plural: xpostgresqlinstances
claimNames:
kind: PostgreSQLInstance
plural: postgresqlinstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
description: "Storage capacity in GB."
version:
type: string
description: "PostgreSQL version."
required:
- storageGB
- version
required:
- parameters
kubectl apply -f - <
Verify:
Check that the new XRD is registered.
kubectl get xrd
Expected Output:
NAME AGE
xpostgresqlinstances.example.org 1m
5.2 Create a Composition
A Composition defines how an XPostgreSQLInstance maps to underlying managed resources. In this example, we'll compose an AWS RDS instance.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xpostgresqlinstances.aws.example.org
labels:
provider: aws
db: postgres
spec:
compositeTypeRef:
apiVersion: example.org/v1alpha1
kind: XPostgreSQLInstance
resources:
- name: rdsinstance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: RDSInstance
spec:
forProvider:
region: us-east-1
dbSubnetGroupName: default-vpc-0d648b2691b00e6a8 # Replace with an existing DB Subnet Group name
vpcSecurityGroupIds:
- sg-0a1b2c3d4e5f6g7h8 # Replace with an existing Security Group ID
publiclyAccessible: false
masterUsername: masteruser
engine: postgres
engineVersion: "13.6" # This will be overwritten by patch
skipFinalSnapshot: true
# Other sane defaults
writeConnectionSecretToRef:
namespace: crossplane-system
name: rds-conn-secret
patches:
- fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.allocatedStorage
- fromFieldPath: spec.parameters.version
toFieldPath: spec.forProvider.engineVersion
- fromFieldPath: metadata.uid
toFieldPath: spec.writeConnectionSecretToRef.name
transforms:
- type: string
string:
fmt: "%s-connection" # Append -connection to the UID
- fromFieldPath: spec.writeConnectionSecretToRef.namespace
toFieldPath: spec.writeConnectionSecretToRef.namespace
policy:
fromFieldPath: Required
- fromFieldPath: metadata.name
toFieldPath: spec.writeConnectionSecretToRef.name
transforms:
- type: string
string:
fmt: "%s-connection"
policy:
fromFieldPath: Required
connectionDetails:
- fromConnectionSecretKey: username
- fromConnectionSecretKey: password
- fromConnectionSecretKey: endpoint
- fromConnectionSecretKey: port
- fromConnectionSecretKey: dbname
- fromConnectionSecretKey: KUBERNETES_CLUSTER_NAME # Example of adding custom connection details
value: my-kube-cluster
Important: You must replace dbSubnetGroupName and vpcSecurityGroupIds with existing values from your AWS environment. If you don't have them, you'll need to create them manually or provision them using Crossplane itself (which is a more advanced topic).
kubectl apply -f - <
Verify:
Check that the Composition is registered.
kubectl get composition
Expected Output:
NAME AGE
xpostgresqlinstances.aws.example.org 1m
5.3 Provision an XPostgreSQLInstance
Now, application developers can provision a PostgreSQL instance using the simplified API, without needing to know the underlying AWS RDS details.
apiVersion: example.org/v1alpha1
kind: XPostgreSQLInstance
metadata:
name: my-app-db
spec:
parameters:
storageGB: 20
version: "13"
writeConnectionSecretToRef:
name: my-app-db-connection
namespace: default # The namespace where the connection secret will be created
kubectl apply -f - <
Verify:
Observe the status of your XPostgreSQLInstance and the underlying RDSInstance. It will take some time for the RDS instance to become available.
kubectl get xpostgresqlinstance my-app-db
Expected Output (eventually):
NAME READY SYNCED AGE
my-app-db True True 5m
You can also check the underlying AWS RDS instance:
kubectl get rdsinstance
Expected Output (eventually):
NAME READY SYNCED EXTERNAL-NAME AGE
my-app-db-d8f8a84d-2d4e-4f5a-8b9c-0d1e2f3g4h5i True True my-app-db-d8f8a84d-2d4e-4f5a-8b9c-0d1e2f3g4h5i-xxxx 10m
Once the RDS instance is ready, a connection secret will be created in the default namespace (as specified in writeConnectionSecretToRef).
kubectl get secret my-app-db-connection -n default -o yaml
Expected Output (truncated):
apiVersion: v1
kind: Secret
metadata:
name: my-app-db-connection
namespace: default
type: Opaque
data:
KUBERNETES_CLUSTER_NAME: bXkta3ViZS1jbHVzdGVy # base64 encoded "my-kube-cluster"
dbname:
endpoint:
password:
port:
username:
Your applications can now consume this secret to connect to the PostgreSQL database. This declarative approach simplifies infrastructure provisioning and integrates seamlessly with your Kubernetes workflows. For advanced traffic management to your applications, check out our guide on the Kubernetes Gateway API.
Production Considerations
Deploying Crossplane in a production environment requires careful planning to ensure reliability, security, and scalability:
- Security and IAM Roles:
- Least Privilege: Ensure the IAM credentials used by Crossplane (via
ProviderConfig) have only the necessary permissions to provision and manage the specified resources. Regularly audit these permissions. - Secrets Management: Use a robust secrets management solution (e.g., Secrets Store CSI Driver, HashiCorp Vault) to inject cloud provider credentials into the
crossplane-systemnamespace, rather than hardcoding them or storing them directly in Git. - Workload Identity/IRSA: On cloud providers like AWS (IRSA), GCP (Workload Identity), or Azure (AAD Pod Identity), configure Crossplane to assume an IAM role directly, avoiding long-lived static credentials. This is the most secure approach.
- Least Privilege: Ensure the IAM credentials used by Crossplane (via
- High Availability:
- Controller Replicas: Run Crossplane and provider controllers with multiple replicas to ensure high availability and resilience against node failures.
- Cluster Reliability: Ensure your underlying Kubernetes cluster is highly available.
- Observability and Monitoring:
- Logs: Aggregate Crossplane controller logs and provider controller logs into a centralized logging system.
- Metrics: Monitor Crossplane's own metrics (exposed via Prometheus endpoints) to track resource reconciliation, API call rates, and error rates.
- Status Conditions: Regularly check the
status.conditionsof your managed and composite resources for insights into their state and any issues. For deeper insights, consider leveraging techniques like those described in our eBPF Observability with Hubble guide.
- Backup and Disaster Recovery:
- Etcd Backup: Crossplane stores its state in Kubernetes' etcd. Ensure your Kubernetes cluster's etcd is regularly backed up.
- Configuration Backup: Store all your Crossplane manifests (Providers, ProviderConfigs, XRDs, Compositions, Managed Resources, XRs) in version control (Git) for disaster recovery and reproducibility.
- Scalability:
- Resource Limits: Set appropriate resource requests and limits for Crossplane and provider pods to prevent resource exhaustion and ensure stable operation.
- Provider Scaling: As the number of managed resources grows, the pressure on provider controllers increases. Monitor and scale provider replicas as needed.
- Drift Detection and Reconciliation:
- Crossplane continuously reconciles the desired state (your Kubernetes manifests) with the actual state (in the cloud). This helps detect and correct configuration drift.
- Understand the reconciliation intervals and how they impact resource updates and error recovery.
- Version Management:
- Pin versions of Crossplane and its providers (e.g.,
v0.40.0) to ensure predictable behavior and avoid unexpected changes. - Test upgrades in a staging environment before applying them to production.
- Pin versions of Crossplane and its providers (e.g.,
- Cost Management:
- Crossplane provisions real cloud resources, which incur costs. Implement tagging strategies (as shown in the S3 example) to track and attribute costs.
- Integrate with cloud cost management tools. For optimizing Kubernetes node costs, our Karpenter Cost Optimization guide might be relevant.
Troubleshooting
Here are some common issues you might encounter with Crossplane and how to resolve them:
-
Issue: Crossplane or Provider Pods are not running/healthy.
Description: The Crossplane or provider controller pods are in
Pending,CrashLoopBackOff, orErrorstate.Solution:
- Check Pod Logs: The first step is always to check the logs of the problematic pod.
kubectl logs -f <pod-name> -n crossplane-system - Describe Pod: Use
kubectl describe pod <pod-name> -n crossplane-systemto check for events, resource limits, and volume mount issues. - Resource Constraints: Ensure your cluster has enough resources (CPU, Memory) for the pods. Increase requests/limits if necessary.
- Image Pull Issues: Verify that the image can be pulled from the registry.
- Check Pod Logs: The first step is always to check the logs of the problematic pod.
-
Issue: Provider is not
HEALTHY: True.Description: After installing a provider (e.g.,
provider-aws),kubectl get providersshowsHEALTHY: False.Solution:
- Check Provider Pod Logs: The provider pod is likely failing to start or connect to the Crossplane API server.
kubectl get pods -n crossplane-system -l app=crossplane-provider-<provider-name> kubectl logs -f <provider-pod-name> -n crossplane-system - Verify Crossplane Core: Ensure the core Crossplane pods are running and healthy first.
- Network Connectivity: Check if the provider can reach external registries to pull its image or external APIs (though less common for
HEALTHYstatus).
- Check Provider Pod Logs: The provider pod is likely failing to start or connect to the Crossplane API server.
-
Issue: Managed Resource (e.g.,
S3Bucket) is stuck inProvisioningorCreating, orREADY: False.Description: The resource is not being created in the cloud, or its status isn't updating.
Solution:
- Check Resource Events:
- Check Resource Events:
