Orchestration

Manage Infra with Crossplane + Kubernetes

July 2, 2026 Kubezilla Team 2 min read

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-system namespace, 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.
  • 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.conditions of 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.
  • 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:

  1. Issue: Crossplane or Provider Pods are not running/healthy.

    Description: The Crossplane or provider controller pods are in Pending, CrashLoopBackOff, or Error state.

    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-system to 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.
  2. Issue: Provider is not HEALTHY: True.

    Description: After installing a provider (e.g., provider-aws), kubectl get providers shows HEALTHY: 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 HEALTHY status).
  3. Issue: Managed Resource (e.g., S3Bucket) is stuck in Provisioning or Creating, or READY: False.

    Description: The resource is not being created in the cloud, or its status isn't updating.

    Solution:

    • Check Resource Events:
      
      									

Leave a comment