Introduction
In the dynamic world of cloud-native development, managing infrastructure efficiently is paramount. Traditional Infrastructure as Code (IaC) tools like Terraform or CloudFormation have long been staples, but they often operate outside the Kubernetes ecosystem, leading to context switching, disparate toolchains, and increased operational overhead. Imagine a world where your infrastructure, whether it’s a PostgreSQL database on AWS, a Google Cloud Storage bucket, or an Azure Kubernetes Service (AKS) cluster, is managed with the same declarative API you use for your Kubernetes applications. This is precisely the promise of Crossplane.
Crossplane extends your Kubernetes cluster into a universal control plane, allowing you to provision and manage cloud infrastructure and services directly from your Kubernetes API. It treats external resources as Kubernetes custom resources, enabling developers and operators to leverage familiar Kubernetes tooling (kubectl, GitOps, RBAC, etc.) for infrastructure management. This paradigm shift consolidates your application and infrastructure concerns into a single, unified control plane, streamlining workflows, improving consistency, and accelerating delivery. By bridging the gap between Kubernetes and external cloud providers, Crossplane empowers platform teams to build powerful Internal Developer Platforms (IDPs) that abstract away infrastructure complexities, allowing development teams to self-service their infrastructure needs with ease.
TL;DR: Crossplane – Kubernetes for Everything
Crossplane turns your Kubernetes cluster into a universal control plane for managing cloud infrastructure. It allows you to provision databases, object storage, and more using familiar Kubernetes manifests and kubectl commands.
Key Takeaways:
- Unified Control Plane: Manage applications and infrastructure from a single Kubernetes API.
- Declarative IaC: Define cloud resources using Kubernetes YAML, leveraging GitOps principles.
- Abstraction: Platform teams can create custom APIs (Compositions) to abstract infrastructure complexity for developers.
- Extensibility: Supports a vast array of cloud providers and services via Providers.
Quick 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 a Provider (e.g., AWS)
kubectl apply -f https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/install/provider-aws.yaml
# Create a ProviderConfig (AWS Credentials)
# Replace with your actual credentials
cat <
Prerequisites
To follow along with this guide, you'll need the following:
- A Kubernetes Cluster: Any conformant Kubernetes cluster (v1.20+) will work. This could be Minikube, Kind, a cloud-managed cluster (EKS, GKE, AKS), or even a local Docker Desktop Kubernetes instance.
kubectl: The Kubernetes command-line tool, configured to connect to your cluster. You can find installation instructions on the official Kubernetes documentation.- Helm (v3+): The package manager for Kubernetes. Installation instructions are available on the Helm website.
- Cloud Provider Account: An account with a cloud provider like AWS, GCP, or Azure, with appropriate permissions to create resources. For this tutorial, we'll primarily use AWS examples, but the concepts apply universally.
- Cloud Provider CLI (Optional but Recommended): For verifying resources directly with the cloud provider (e.g., AWS CLI, gcloud CLI, Azure CLI).
Step-by-Step Guide
1. Install Crossplane Core on Your Kubernetes Cluster
First, we need to install the Crossplane core components into your Kubernetes cluster. Crossplane is typically installed via Helm, which simplifies the deployment of its various controllers and CRDs (Custom Resource Definitions). We'll create a dedicated namespace for Crossplane to keep things organized.
The Crossplane core provides the foundational control plane capabilities, including the Crossplane API server, controllers, and the Composition engine. It's the central nervous system that enables Kubernetes to understand and manage external resources. Think of it as extending the Kubernetes brain to comprehend infrastructure beyond just pods and services.
# Add the Crossplane Helm repository
helm repo add crossplane-stable https://charts.crossplane.io/stable
# Update your Helm repositories
helm repo update
# Install Crossplane into the 'crossplane-system' namespace
helm install crossplane crossplane-stable/crossplane --namespace crossplane-system --create-namespace
# Verify the Crossplane pods are running
kubectl get pods --namespace crossplane-system
Verify:
You should see output similar to this, indicating that Crossplane's core pods are running and ready:
NAME READY STATUS RESTARTS AGE
crossplane-78d46f5c8c-l7h25 1/1 Running 0 2m
crossplane-rbac-manager-69b7668677-s7x7q 1/1 Running 0 2m
2. Install a Crossplane Provider
Crossplane itself doesn't know how to talk to AWS, GCP, or Azure directly. It relies on "Providers" to do that. A Provider is a specialized controller that translates Crossplane's generic resource requests into cloud provider-specific API calls. For example, the provider-aws understands how to create S3 buckets, EC2 instances, and RDS databases on AWS.
For this guide, we'll install provider-aws. The installation process involves applying a Provider custom resource, which tells Crossplane to fetch and deploy the corresponding controller. Crossplane then manages the lifecycle of this provider, ensuring it's running and up-to-date. This modular approach allows Crossplane to support a vast ecosystem of cloud services without becoming monolithic.
# Apply the Provider AWS manifest
# This will create a 'Provider' custom resource for AWS
kubectl apply -f https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/install/provider-aws.yaml
# Verify the provider-aws pod is running
kubectl get pods --namespace crossplane-system -l app=provider-aws
# Check the status of the Provider
kubectl get provider.pkg.crossplane.io
Verify:
You should see the provider-aws pod running, and the Provider resource in a healthy state:
NAME READY STATUS RESTARTS AGE
provider-aws-57f957c5d4-ghjkl 1/1 Running 0 1m
NAME HEALTHY PACKAGE AGE
provider-aws True crossplane/provider-aws:v0.40.0 1m
Note: The version number (e.g., v0.40.0) might differ based on the latest release.
3. Configure Cloud Provider Credentials (ProviderConfig)
Crossplane needs credentials to interact with your cloud provider. It's best practice to store these credentials securely as a Kubernetes Secret and then reference that Secret in a ProviderConfig custom resource. The ProviderConfig acts as a bridge, telling the installed Provider (e.g., provider-aws) which credentials to use for API calls.
For AWS, this typically involves an IAM user with programmatic access (Access Key ID and Secret Access Key). Ensure this IAM user has the necessary permissions for the resources you intend to provision (e.g., S3, RDS, EC2). Storing credentials in Kubernetes Secrets is critical for security, and Crossplane integrates seamlessly with this standard practice. For enhanced security, consider using tools like Secrets Store CSI Driver to fetch credentials from external secret management systems like AWS Secrets Manager or HashiCorp Vault.
# Create an AWS credentials file (replace with your actual keys)
# Ensure this file is deleted after creating the Secret
echo '[default]' > aws-creds.conf
echo 'aws_access_key_id = YOUR_AWS_ACCESS_KEY_ID' >> aws-creds.conf
echo 'aws_secret_access_key = YOUR_AWS_SECRET_ACCESS_KEY' >> aws-creds.conf
# Create a Kubernetes Secret from the credentials file
kubectl create secret generic aws-creds --from-file=credentials=aws-creds.conf --namespace crossplane-system
# Delete the temporary credentials file
rm aws-creds.conf
# Create a ProviderConfig to reference the secret
cat <
Verify:
The ProviderConfig should show as healthy, indicating Crossplane can access your AWS credentials:
NAME HEALTHY AGE
default True 1m
4. Provision an AWS S3 Bucket
Now that Crossplane is installed, the AWS provider is running, and credentials are configured, we can provision our first cloud resource: an S3 bucket. We define the desired state of the S3 bucket using a Kubernetes manifest, just like you would define a Deployment or a Service. Crossplane observes this manifest and instructs the provider-aws to create the corresponding resource in AWS.
This declarative approach is a cornerstone of Kubernetes. You declare what you want, and Crossplane continuously works to reconcile the actual state with your desired state. Notice how the YAML looks very much like a standard Kubernetes resource, but with an apiVersion pointing to s3.aws.crossplane.io and a kind of Bucket. This consistency makes it easy for Kubernetes native developers to adopt Crossplane.
# s3-bucket.yaml
apiVersion: s3.aws.crossplane.io/v1beta1
kind: Bucket
metadata:
name: my-crossplane-bucket-12345
spec:
forProvider:
region: us-east-1
acl: private
providerConfigRef:
name: default # References the ProviderConfig we created earlier
# Apply the S3 bucket manifest
kubectl apply -f s3-bucket.yaml
# Monitor the status of the S3 bucket
kubectl get bucket my-crossplane-bucket-12345
# Describe the bucket for more details (events, status, etc.)
kubectl describe bucket my-crossplane-bucket-12345
Verify:
Initially, the bucket might be in a "Creating" state. After a few moments, it should transition to "Ready: True" and show a synchronized status:
NAME READY SYNCED EXTERNAL-NAME AGE
my-crossplane-bucket-12345 True True my-crossplane-bucket-12345 1m
You can also verify its existence directly in the AWS console or using the AWS CLI:
aws s3api head-bucket --bucket my-crossplane-bucket-12345 --region us-east-1
Expected output (if successful, no output, just exit code 0):
# If successful, this command returns nothing and exits with 0.
# If the bucket does not exist, it will return an error.
5. Introduce Compositions for Abstraction
While provisioning raw cloud resources is powerful, it can expose too much complexity to developers. This is where Crossplane's Compositions shine. Compositions allow platform teams to define higher-level, opinionated APIs that abstract away the underlying cloud specifics. Developers can then request an "XPostgreSQLInstance" or "XObjectStorage" without needing to know if it's AWS RDS, GCP Cloud SQL, or Azure Database for PostgreSQL, or S3, GCS, or Azure Blob Storage.
A Composition consists of two main parts:
- Composite Resource Definition (XRD): Defines the new custom API that developers will interact with (e.g.,
XPostgreSQLInstance). - Composition: Maps the fields of the
XRDto concrete cloud resources (e.g., an AWS RDS instance, a SubnetGroup, a SecurityGroup).
This enables self-service infrastructure for developers while maintaining governance and best practices defined by platform teams. For more advanced networking requirements, platform teams might also leverage Kubernetes Network Policies to secure communication between applications and these composed resources.
Let's create a simple Composition for an "XBucket" that provisions an S3 bucket with some default settings.
a. Define the Composite Resource Definition (XRD)
The XRD defines the schema for our custom resource, XBucket. Developers will create instances of this XBucket.
# xbucket-xrd.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xbuckets.kubezilla.io
spec:
group: kubezilla.io
names:
kind: XBucket
plural: xbuckets
claimNames: # Optional: allows developers to claim resources in their namespace
kind: BucketClaim
plural: bucketclaims
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters: # Parameters developers can specify
type: object
properties:
region:
type: string
description: "The AWS region for the bucket."
namePrefix:
type: string
description: "A prefix for the bucket name."
required:
- region
required:
- parameters
kubectl apply -f xbucket-xrd.yaml
Verify:
You should see the new CRD registered:
compositeresourcedefinition.apiextensions.crossplane.io/xbuckets.kubezilla.io created
b. Define the Composition
The Composition maps the XBucket to an actual AWS S3 Bucket. It specifies how the parameters from the XBucket (like region) are used to configure the underlying S3 resource. We can also add default values or additional configurations here that developers don't need to specify.
# xbucket-composition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xbucket-aws-s3
labels:
provider: aws
service: s3
spec:
compositeTypeRef:
apiVersion: kubezilla.io/v1alpha1
kind: XBucket
resources:
- name: s3bucket
base:
apiVersion: s3.aws.crossplane.io/v1beta1
kind: Bucket
spec:
forProvider:
acl: private # Default ACL, developers don't need to specify
tags:
managed-by: crossplane
patches:
- fromFieldPath: "spec.parameters.region"
toFieldPath: "spec.forProvider.region"
- fromFieldPath: "metadata.uid" # Use UID to ensure unique bucket name
toFieldPath: "spec.forProvider.name"
transforms:
- type: string
string:
fmt: "%s" # Just convert UID to string
- fromFieldPath: "spec.parameters.namePrefix"
toFieldPath: "spec.forProvider.name"
transforms:
- type: string
string:
fmt: "%s-"
- type: string
string:
fmt: "%s%s"
# Combine namePrefix with the UID-derived name.
# This transform chain essentially says:
# 1. Take 'namePrefix'
# 2. Append '-' to it
# 3. Prepend the result to the S3 bucket name derived from 'metadata.uid'
# This ensures a unique name while allowing a developer-defined prefix.
# Note: Crossplane v1.11+ simplifies this with `combine` transform.
# For older versions, you might need a more complex UID transform or use `external-name` annotation.
# For simplicity, we'll just use the UID for a unique name and add a prefix.
# A better way is to use a `function` or `externalName` annotation.
# Let's simplify and just use the UID.
# For actual production, use `externalName` or a more robust naming convention.
# Simplified naming for demonstration: just use the composite resource's name
# For a real scenario, use `externalName` annotation or a more complex patch.
readinessChecks:
- type: MatchString
fieldPath: "status.atProvider.state"
matchString: "Available" # Or "Ready" depending on resource
kubectl apply -f xbucket-composition.yaml
Verify:
The Composition should be created successfully:
composition.apiextensions.crossplane.io/xbucket-aws-s3 created
c. Create an Instance of Your Composite Resource
Now, a developer can request an XBucket without knowing the underlying S3 details. They just specify the region and a name prefix.
# my-dev-bucket.yaml
apiVersion: kubezilla.io/v1alpha1
kind: XBucket
metadata:
name: my-dev-bucket-for-app
spec:
parameters:
region: us-east-1
namePrefix: dev-app-data
resourceRef: # Optional: If you want to specify a specific external name
name: dev-app-data-unique-id
kubectl apply -f my-dev-bucket.yaml
# Check the status of your XBucket
kubectl get xbucket my-dev-bucket-for-app
# Check the underlying S3 Bucket created by the Composition
kubectl get bucket
Verify:
You'll see your XBucket and the underlying S3 Bucket resource created by Crossplane:
NAME READY SYNCED AGE
my-dev-bucket-for-app True True 1m
NAME READY SYNCED EXTERNAL-NAME AGE
bucket.s3.aws.crossplane.io/my-dev-bucket-for-app-xxxx True True dev-app-data-my-dev-bucket-for-app-xxxx 1m
The actual external name of the S3 bucket will be generated by Crossplane based on the composition rules, likely including a unique suffix. This demonstrates the power of abstraction!
6. Clean Up Resources
It's crucial to clean up resources created by Crossplane to avoid incurring unnecessary cloud costs. When you delete a Crossplane resource, it automatically triggers the deletion of the corresponding cloud resource. This is a significant advantage over manual cleanup or traditional IaC tools that might require extra steps.
# Delete the composite resource (if created)
kubectl delete xbucket my-dev-bucket-for-app
# Delete the raw S3 bucket (if created directly without composition)
# kubectl delete bucket my-crossplane-bucket-12345
# Delete the Composition and XRD
kubectl delete -f xbucket-composition.yaml
kubectl delete -f xbucket-xrd.yaml
# Delete the ProviderConfig
kubectl delete providerconfig.aws.crossplane.io default
# Uninstall the Provider
kubectl delete provider.pkg.crossplane.io crossplane-provider-aws
# Uninstall Crossplane core
helm uninstall crossplane --namespace crossplane-system
kubectl delete namespace crossplane-system
Verify:
After running the cleanup commands, verify that the resources are gone from both Kubernetes and your cloud provider. For example, check your AWS S3 console or run:
aws s3api head-bucket --bucket dev-app-data-unique-id --region us-east-1
# Expected output: An error indicating the bucket does not exist.
And in Kubernetes:
kubectl get bucket
# Expected output: "No resources found"
kubectl get provider.pkg.crossplane.io
# Expected output: "No resources found"
kubectl get pods --namespace crossplane-system
# Expected output: "No resources found in crossplane-system namespace."
Production Considerations
Deploying Crossplane in a production environment requires careful planning and adherence to best practices. Here are key areas to consider:
- High Availability: Run Crossplane controllers with multiple replicas for fault tolerance. Ensure your Kubernetes cluster itself is highly available.
- Security & RBAC:
- Least Privilege: Grant Crossplane's cloud provider credentials (e.g., AWS IAM roles) only the minimum necessary permissions to create, update, and delete the resources it manages.
- Kubernetes RBAC: Implement strict Kubernetes RBAC policies. Developers should only have access to create and manage their
Composite Resources(e.g.,XBucket), not the underlying raw cloud resources (e.g.,Bucket.s3.aws.crossplane.io) or Crossplane's core components. - Secrets Management: Use robust secret management solutions. Instead of static credentials, consider using Secrets Store CSI Driver with cloud provider secret stores (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) or HashiCorp Vault.
- For enhanced cluster security, consider integrating with tools like Sigstore and Kyverno for policy enforcement and supply chain integrity.
- Observability:
- Logging: Configure Crossplane controllers to log to your central logging system (e.g., Fluentd, Loki).
- Metrics: Expose Prometheus metrics from Crossplane and its providers. Monitor resource reconciliation, API call rates, and error rates.
- Events: Monitor Kubernetes events for Crossplane resources to track lifecycle changes and errors.
- For deeper insights into your Kubernetes network, explore eBPF Observability with Hubble.
- GitOps Workflows:
- Integrate Crossplane resource definitions (XRDs, Compositions, Composite Resources) into your GitOps pipelines (Argo CD, Flux CD). This ensures all infrastructure is version-controlled, auditable, and automatically reconciled.
- Treat your Compositions as the "API" of your internal platform.
- Provider & Composition Management:
- Version Control: Pin specific versions of Crossplane core and its Providers to ensure stability.
- Testing: Thoroughly test your Compositions in development and staging environments before deploying to production.
- Composition Validation: Use schema validation in your XRDs to ensure developers provide valid parameters.
- Cost Management:
- Crossplane can provision expensive resources. Implement tagging strategies on your cloud resources through Compositions to enable better cost tracking and allocation.
- Combine with tools like Karpenter for Kubernetes Cost Optimization to manage compute costs effectively.
- Service Mesh Integration: If you're running a service mesh like Istio, ensure Crossplane-provisioned services are correctly integrated. Check out our Istio Ambient Mesh Production Guide for best practices.
- Backup and Restore: While Crossplane manages the lifecycle of cloud resources, it doesn't back up the data within those resources. Implement separate backup and restore strategies for your databases, object storage, etc.
Troubleshooting
-
Issue: Crossplane pods are not running or in CrashLoopBackOff.
Solution: Check the logs of the Crossplane pods for errors. Common issues include insufficient resource limits, incorrect Helm chart values, or network connectivity problems within the cluster.
kubectl logs -n crossplane-system <crossplane-pod-name> kubectl describe pod -n crossplane-system <crossplane-pod-name> -
Issue: Provider pods (e.g.,
provider-aws) are not running or in CrashLoopBackOff.Solution: Similar to Crossplane core, check the logs and describe the provider pod. Ensure the
Providercustom resource is correctly applied and references a valid package. Network issues preventing the provider from pulling its image can also cause this.kubectl logs -n crossplane-system <provider-aws-pod-name> kubectl describe provider.pkg.crossplane.io provider-aws -
Issue: Cloud resources (e.g., S3 Bucket) are not being provisioned, or are stuck in a "Creating" state.
Solution:
- Check
ProviderConfig: Ensure yourProviderConfigis healthy and the underlying secret has valid credentials. - Check Resource Status: Describe the Crossplane custom resource (e.g.,
BucketorXBucket) for events and status conditions. TheEventssection often reveals API errors from the cloud provider. - Cloud Provider Permissions: The most common cause is insufficient IAM permissions for the credentials used in your
ProviderConfig. Check the cloud provider's logs (e.g., AWS CloudTrail) for "AccessDenied" errors.
kubectl get providerconfig.aws.crossplane.io default kubectl get secret -n crossplane-system aws-creds -o yaml # Verify secret existskubectl describe bucket my-crossplane-bucket-12345 # Or kubectl describe xbucket my-dev-bucket-for-app - Check
-
Issue: Deleting a Crossplane resource doesn't delete the actual cloud resource.
Solution: This usually happens if the
reclaimPolicyfor the managed resource is set toRetaininstead ofDelete. By default, most Crossplane managed resources have aDeletereclaim policy, but it can be overridden. Check thespec.reclaimPolicyof the managed resource. Also, ensure the Crossplane provider has the necessary delete permissions.kubectl get bucket my-crossplane-bucket-12345 -o yaml | grep reclaimPolicy -
Issue: My Composed Resource (e.g.,
XBucket) is stuck, and the underlying managed resources (e.g.,Bucket) are not created.Solution:
- Check XRD and Composition: Ensure the
CompositeResourceDefinition(XRD) andCompositionare correctly applied and valid. - Check
XBucketstatus: Describe yourXBucketinstance. Look for any errors in itsstatusoreventssections, especially related to composition or resource claims. - Composition Patching Issues: If fields aren't being correctly mapped, there might be an issue with your
patchesin theComposition. Check exact field paths and transform logic.
kubectl get xrd xbuckets.kubezilla.io kubectl get composition xbucket-aws-s3kubectl describe xbucket my-dev-bucket-for-app - Check XRD and Composition: Ensure the
-
Issue: Cannot connect to AWS/GCP/Azure API from Crossplane provider pod.
Solution: This indicates a network connectivity issue from your Kubernetes cluster to the cloud provider's API endpoints.
- Check network policies within your cluster. You might need to adjust Kubernetes Network Policies to allow egress traffic to cloud API endpoints.
- Verify DNS resolution from within the pod.
- If using a proxy, ensure the provider pod is configured to use it.
- For clusters using advanced CNI like Cilium, ensure there are no unintended egress restrictions. See Cilium WireGuard Encryption for related networking topics.
FAQ Section
Q1: How does Crossplane compare to Terraform?
A1: Both Crossplane and Terraform are Infrastructure as Code tools. The key difference lies in their operational model. Terraform is an external CLI tool that applies infrastructure changes. Crossplane, on the other hand, extends the Kubernetes API, making your Kubernetes cluster the control plane for both applications and infrastructure. This enables GitOps for infrastructure, unified RBAC, and leveraging Kubernetes' reconciliation loop for continuous state management. You can even run Terraform within Crossplane using the Crossplane Terraform Provider for a hybrid approach.
Q2: Can I manage existing cloud resources with Crossplane?
A2: Yes, Crossplane supports importing existing cloud resources. You typically create a Crossplane managed resource (e.g., Bucket) with the external-name annotation matching the name of your existing cloud resource. Crossplane will then take ownership and manage its lifecycle.
Q3: What are Compositions, and why are they important?
A3: Compositions are Crossplane's mechanism for abstraction. They allow platform teams to define higher-level, opinionated APIs (Composite Resources) that bundle multiple underlying cloud resources into a single, user-friendly interface. For example, an "XDatabase" Composite Resource could provision an RDS instance, a subnet group, and a security group. This empowers developers to self-service infrastructure without needing deep knowledge of cloud provider specifics, fostering an Internal Developer Platform (IDP).
Q4: Is Crossplane secure for production use?
A4: Yes, Crossplane is designed with security in mind and is a
