DEV Community

Cover image for Testing Your Cloud Infrastructure Like IKEA Furniture: A Guide to Crossplane v2 End-to-End Testing

Testing Your Cloud Infrastructure Like IKEA Furniture: A Guide to Crossplane v2 End-to-End Testing

Ever bought an IKEA BILLY bookshelf? You know the drill: open the box, check all the pieces are there, follow the instructions, tighten the screws, and give it a good shake to make sure it won't collapse when you load it with your collection of unread programming books.

Testing Crossplane v2 infrastructure is remarkably similar. Except instead of a wobbly bookshelf, you're building cloud resources. And instead of an Allen key, you're using YAML. (Arguably, both can be equally frustrating at times.)

In this guide, I'll walk you through setting up end-to-end (E2E) testing for Crossplane v2, using our IKEA metaphor to make the concepts stick like... well, like properly applied IKEA wood glue.

Why Test Your Infrastructure? (Or: Why You Shouldn't Skip the Assembly Instructions)

Remember that time you thought you could assemble IKEA furniture without reading the instructions? And then you had leftover screws? And the drawer didn't quite close right?

That's what running Crossplane compositions in production without E2E testing feels like.

E2E testing for Crossplane validates that:

  • All the pieces (resources) are actually created ✅
  • They're assembled correctly (configured properly) ✅
  • They work together (networking, permissions, dependencies) ✅
  • They can be disassembled cleanly (deletion works) ✅

The IKEA Furniture Assembly Metaphor

Let's break down our metaphor:

IKEA Furniture Crossplane v2
Instruction manual XRD (CompositeResourceDefinition)
Assembly steps Composition with pipeline functions
Individual pieces Managed Resources (Storage Account, VNet, etc.)
Assembled furniture Composite Resource (XR)
Quality check E2E Test Suite
Allen key kubectl (both mysterious and essential)

Prerequisites: What's in Your Toolbox?

Before we start assembling (testing), let's make sure you have all the tools. Unlike IKEA, these tools aren't included in the box:

# The essentials (your "Allen keys")
brew install azure-cli kubectl helm jq
brew install fluxcd/tap/flux
brew install crossplane

# The nice-to-haves (your "electric screwdriver")
brew install k9s  # Visual cluster exploration
brew install tree # Directory visualization
Enter fullscreen mode Exit fullscreen mode

Verify everything works:

az version
kubectl version --client
helm version
flux --version
crossplane version
Enter fullscreen mode Exit fullscreen mode

Pro tip: If any of these fail, don't panic! Check the error messages. They're usually more helpful than IKEA's pictographic instructions.

Step 1: Building Your Workshop (Setting Up AKS)

First, we need a place to assemble our furniture. Let's create an AKS cluster:

# Set up your environment variables
export RESOURCE_GROUP="crossplane-e2e-rg"
export LOCATION="westeurope"
export CLUSTER_NAME="crossplane-e2e-aks"
export CROSSPLANE_VERSION="2.1.0"

# Log into Azure
az login
az account set --subscription "YOUR_SUBSCRIPTION_ID"

# Create your workshop (resource group)
az group create \
  --name $RESOURCE_GROUP \
  --location $LOCATION \
  --tags environment=development managedBy=crossplane

# Build the workbench (AKS cluster)
az aks create \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --node-count 3 \
  --node-vm-size Standard_D2s_v3 \
  --enable-managed-identity \
  --network-plugin azure \
  --generate-ssh-keys
Enter fullscreen mode Exit fullscreen mode

This takes about 5-10 minutes. Perfect time for a Swedish meatball break! 🍝

# Connect your tools to the workbench
az aks get-credentials \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME

# Verify you're connected
kubectl get nodes
Enter fullscreen mode Exit fullscreen mode

Step 2: Installing Crossplane (Getting Your Assembly Instructions Ready)

Now let's install Crossplane v2. Think of this as opening the IKEA box and laying out all the instruction sheets:

# Add the Crossplane repository
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

# Install Crossplane v2
helm install crossplane \
  --namespace crossplane-system \
  --create-namespace \
  crossplane-stable/crossplane \
  --version $CROSSPLANE_VERSION \
  --wait

# Check that Crossplane is running
kubectl get pods -n crossplane-system
Enter fullscreen mode Exit fullscreen mode

You should see something like:

NAME                                      READY   STATUS    RESTARTS   AGE
crossplane-xxx                            1/1     Running   0          1m
crossplane-rbac-manager-xxx               1/1     Running   0          1m
Enter fullscreen mode Exit fullscreen mode

Step 3: Setting Up Azure Authentication (The Security Sticker)

Just like IKEA furniture has those "quality checked" stickers, we need to authenticate Crossplane with Azure:

# Get your subscription ID
export SUBSCRIPTION_ID=$(az account show --query id -o tsv)

# Create a service principal (think of it as the QA inspector's badge)
SP_OUTPUT=$(az ad sp create-for-rbac \
  --name "crossplane-e2e-${CLUSTER_NAME}" \
  --role Contributor \
  --scopes /subscriptions/$SUBSCRIPTION_ID \
  --output json)

# Extract and save the credentials
export AZURE_CLIENT_ID=$(echo $SP_OUTPUT | jq -r '.appId')
export AZURE_CLIENT_SECRET=$(echo $SP_OUTPUT | jq -r '.password')
export AZURE_TENANT_ID=$(echo $SP_OUTPUT | jq -r '.tenant')

# Create a Kubernetes secret
kubectl create secret generic azure-secret \
  --namespace crossplane-system \
  --from-literal=creds="[default]
client_id = $AZURE_CLIENT_ID
client_secret = $AZURE_CLIENT_SECRET
tenant_id = $AZURE_TENANT_ID
subscription_id = $SUBSCRIPTION_ID"
Enter fullscreen mode Exit fullscreen mode

Important: Save these credentials! You'll need them later, and unlike that bag of extra IKEA screws, you can't just shrug and throw them in a drawer.

Step 4: Installing Azure Providers (Getting the Right Pieces)

Crossplane v2 uses modular providers. It's like ordering specific IKEA departments: kitchen, bedroom, storage. We need to install the Azure providers we'll use:

# Install the providers we need
cat <<EOF | kubectl apply -f -
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-azure-storage
spec:
  package: xpkg.upbound.io/upbound/provider-azure-storage:v1.3.0
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-azure-network
spec:
  package: xpkg.upbound.io/upbound/provider-azure-network:v1.3.0
EOF

# Wait for providers to install (grab another coffee ☕)
kubectl wait provider --all \
  --for=condition=Healthy \
  --timeout=600s
Enter fullscreen mode Exit fullscreen mode

Now configure the providers to use our Azure credentials:

cat <<EOF | kubectl apply -f -
apiVersion: azure.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: azure-secret
      key: creds
EOF
Enter fullscreen mode Exit fullscreen mode

Step 5: Creating Your First "Furniture" (XRD and Composition)

Here's where it gets fun! We're going to create an XRD (the instruction manual) and a Composition (the assembly steps) for a Storage Account.

The XRD (Instruction Manual):

# config/xrds/xstorage-account.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xstorageaccounts.storage.example.io
spec:
  group: storage.example.io
  names:
    kind: XStorageAccount
    plural: xstorageaccounts
  scope: Namespaced
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              parameters:
                type: object
                properties:
                  location:
                    type: string
                    default: westeurope
                  accountTier:
                    type: string
                    enum: [Standard, Premium]
                    default: Standard
                  replicationType:
                    type: string
                    enum: [LRS, GRS, RAGRS, ZRS]
                    default: LRS
                  resourceGroupName:
                    type: string
                required:
                - resourceGroupName
Enter fullscreen mode Exit fullscreen mode

The Composition (Assembly Steps):

In Crossplane v2, we use pipeline mode with composition functions. It's like having step-by-step assembly instructions instead of just a picture:

# config/compositions/storage-account.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xstorageaccounts.storage.example.io
  labels:
    provider: azure
    type: standard
spec:
  compositeTypeRef:
    apiVersion: storage.example.io/v1alpha1
    kind: XStorageAccount

  mode: Pipeline
  pipeline:
  - step: patch-and-transform
    functionRef:
      name: function-patch-and-transform
    input:
      apiVersion: pt.fn.crossplane.io/v1beta1
      kind: Resources
      resources:
      # First piece: The resource group (the room)
      - name: resourcegroup
        base:
          apiVersion: azure.m.upbound.io/v1beta1
          kind: ResourceGroup
          spec:
            forProvider:
              location: westeurope
        patches:
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.location
          toFieldPath: spec.forProvider.location
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.resourceGroupName
          toFieldPath: metadata.name

      # Second piece: The storage account (the furniture)
      - name: storageaccount
        base:
          apiVersion: storage.azure.m.upbound.io/v1beta2
          kind: Account
          spec:
            forProvider:
              accountReplicationType: LRS
              accountTier: Standard
              resourceGroupNameSelector:
                matchControllerRef: true
        patches:
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.accountTier
          toFieldPath: spec.forProvider.accountTier
        - type: ToCompositeFieldPath
          fromFieldPath: metadata.name
          toFieldPath: status.storageAccountName
        readinessChecks:
        - type: MatchString
          fieldPath: status.atProvider.provisioningState
          matchString: Succeeded

  - step: auto-ready
    functionRef:
      name: function-auto-ready
Enter fullscreen mode Exit fullscreen mode

Install the composition functions first:

cat <<EOF | kubectl apply -f -
---
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
  name: function-patch-and-transform
spec:
  package: xpkg.upbound.io/crossplane-contrib/function-patch-and-transform:v0.2.1
---
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
  name: function-auto-ready
spec:
  package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.2.1
EOF

# Wait for functions to be ready
kubectl wait function --all --for=condition=Healthy --timeout=300s
Enter fullscreen mode Exit fullscreen mode

Now apply your XRD and Composition:

kubectl apply -f config/xrds/xstorage-account.yaml
kubectl apply -f config/compositions/storage-account.yaml
Enter fullscreen mode Exit fullscreen mode

Step 6: The Quality Check (Setting Up E2E Tests)

Now comes the crucial part: testing! We'll use kuttl (Kubernetes Test Tool) - think of it as your IKEA quality inspector.

Install kuttl:

KUTTL_VERSION=0.15.0
wget -q https://github.com/kudobuilder/kuttl/releases/download/v${KUTTL_VERSION}/kubectl-kuttl_${KUTTL_VERSION}_linux_x86_64
chmod +x kubectl-kuttl_${KUTTL_VERSION}_linux_x86_64
sudo mv kubectl-kuttl_${KUTTL_VERSION}_linux_x86_64 /usr/local/bin/kubectl-kuttl
Enter fullscreen mode Exit fullscreen mode

Create your test structure:

mkdir -p tests/e2e/01-storage-account/{00-setup,01-verify,02-cleanup}
Enter fullscreen mode Exit fullscreen mode

Test Step 1: Create the furniture (00-setup)

# tests/e2e/01-storage-account/00-setup/00-xr-storage.yaml
apiVersion: storage.example.io/v1alpha1
kind: XStorageAccount
metadata:
  name: test-storage-e2e-001
  namespace: default
spec:
  parameters:
    location: westeurope
    accountTier: Standard
    replicationType: LRS
    resourceGroupName: crossplane-e2e-test-rg
  compositionSelector:
    matchLabels:
      provider: azure
      type: standard
Enter fullscreen mode Exit fullscreen mode
# tests/e2e/01-storage-account/00-setup/00-assert.yaml
apiVersion: storage.example.io/v1alpha1
kind: XStorageAccount
metadata:
  name: test-storage-e2e-001
  namespace: default
status:
  conditions:
  - type: Ready
    status: "True"
  - type: Synced
    status: "True"
Enter fullscreen mode Exit fullscreen mode

Test Step 2: Verify it's sturdy (01-verify)

# tests/e2e/01-storage-account/01-verify/00-assert-storage.yaml
apiVersion: storage.azure.m.upbound.io/v1beta2
kind: Account
metadata:
  namespace: default
  ownerReferences:
  - apiVersion: storage.example.io/v1alpha1
    kind: XStorageAccount
    name: test-storage-e2e-001
status:
  conditions:
  - type: Ready
    status: "True"
Enter fullscreen mode Exit fullscreen mode

Test Step 3: Check in the real world (Azure)

# tests/e2e/01-storage-account/01-verify/01-verify-azure.yaml
apiVersion: kuttl.dev/v1beta1
kind: TestAssert
commands:
- script: |
    # Get the storage account name
    STORAGE_NAME=$(kubectl get xstorageaccount test-storage-e2e-001 \
      -n default \
      -o jsonpath='{.status.storageAccountName}')

    # Verify it exists in Azure (give it a shake!)
    az storage account show \
      --name $STORAGE_NAME \
      --resource-group crossplane-e2e-test-rg \
      --query "provisioningState" \
      --output tsv | grep -q "Succeeded"
Enter fullscreen mode Exit fullscreen mode

Test Step 4: Disassemble (02-cleanup)

# tests/e2e/01-storage-account/02-cleanup/00-delete.yaml
apiVersion: storage.example.io/v1alpha1
kind: XStorageAccount
metadata:
  name: test-storage-e2e-001
  namespace: default
$patch: delete
Enter fullscreen mode Exit fullscreen mode

Create the test configuration:

# tests/e2e/01-storage-account/kuttl-test.yaml
apiVersion: kuttl.dev/v1beta1
kind: TestSuite
metadata:
  name: storage-account-e2e
timeout: 600
parallel: 1
testDirs:
- .
Enter fullscreen mode Exit fullscreen mode

Step 7: Running Your Tests (The Moment of Truth)

Time to run the quality check! This is like that satisfying moment when you tighten the last screw and give the furniture a shake to make sure it's stable:

# Run the E2E tests
kubectl kuttl test tests/e2e/01-storage-account/

# In another terminal, watch the magic happen
watch kubectl get xstorageaccount,account,resourcegroup
Enter fullscreen mode Exit fullscreen mode

You should see output like:

=== RUN   kuttl
    harness.go:462: starting setup
    harness.go:252: running tests using configured kubeconfig.
    harness.go:285: Successful connection to cluster at: https://crossplane-e2e-aks-xxx.hcp.westeurope.azmk8s.io:443
=== RUN   kuttl/harness
=== RUN   kuttl/harness/storage-account-e2e
=== PAUSE kuttl/harness/storage-account-e2e
=== CONT  kuttl/harness/storage-account-e2e
    logger.go:42: 12:34:56 | storage-account-e2e | Creating namespace: kuttl-test-xxx
    logger.go:42: 12:34:57 | storage-account-e2e/0-setup | starting test step 0-setup
    logger.go:42: 12:35:30 | storage-account-e2e/0-setup | test step completed 0-setup
    logger.go:42: 12:35:30 | storage-account-e2e/1-verify | starting test step 1-verify
    logger.go:42: 12:35:45 | storage-account-e2e/1-verify | test step completed 1-verify
    logger.go:42: 12:35:45 | storage-account-e2e/2-cleanup | starting test step 2-cleanup
    logger.go:42: 12:36:00 | storage-account-e2e/2-cleanup | test step completed 2-cleanup
=== CONT  kuttl
    harness.go:405: run tests finished
    harness.go:513: cleaning up
    harness.go:570: removing temp folder: ""
--- PASS: kuttl (65.23s)
    --- PASS: kuttl/harness (0.00s)
        --- PASS: kuttl/harness/storage-account-e2e (65.12s)
PASS
Enter fullscreen mode Exit fullscreen mode

If you see PASS, congratulations! Your infrastructure furniture is assembled correctly!

Troubleshooting (When Your Drawer Won't Close)

Problem: Provider not becoming healthy

# Check provider logs
kubectl logs -n crossplane-system -l pkg.crossplane.io/provider=provider-azure-storage

# Usually it's authentication - double-check your secret
kubectl get secret azure-secret -n crossplane-system
Enter fullscreen mode Exit fullscreen mode

Problem: XR stuck in "not ready"

# Check what's wrong
kubectl describe xstorageaccount test-storage-e2e-001

# Check the managed resources
kubectl get managed

# Check Crossplane logs
kubectl logs -n crossplane-system deployment/crossplane -f
Enter fullscreen mode Exit fullscreen mode

Problem: Test timeout

This usually means Azure is taking longer than expected. You can increase the timeout in your kuttl-test.yaml:

timeout: 900  # Increase from 600 to 900 seconds
Enter fullscreen mode Exit fullscreen mode

Bonus: GitOps with Flux (Assembly Instructions on Demand)

Want to make this even more automated? Add Flux for GitOps! It's like having IKEA deliver your furniture AND the assembly instructions automatically update when they improve them:

# Install Flux
flux bootstrap github \
  --owner=YOUR_GITHUB_USER \
  --repository=crossplane-e2e-fleet \
  --branch=main \
  --path=./clusters/dev \
  --personal

# Create GitRepository
cat <<EOF | kubectl apply -f -
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: crossplane-configs
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/YOUR_USER/YOUR_REPO
  ref:
    branch: main
EOF

# Create Kustomization
cat <<EOF | kubectl apply -f -
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: crossplane-xrds
  namespace: flux-system
spec:
  interval: 5m
  path: ./config/xrds
  prune: true
  sourceRef:
    kind: GitRepository
    name: crossplane-configs
EOF
Enter fullscreen mode Exit fullscreen mode

Now whenever you push changes to your Git repo, Flux automatically applies them to your cluster!

Cleaning Up (Because Storage Costs Money)

When you're done testing:

# Delete test resources
kubectl delete xstorageaccount --all

# Or delete everything
az group delete --name crossplane-e2e-rg --yes --no-wait
Enter fullscreen mode Exit fullscreen mode

The Complete Picture (Full Source Code)

All the code from this tutorial is available in my GitHub repository:

🔗 github.com/vanHeemstraSystems/learning-crossplane-e2e-testing

The repo includes:

  • Complete setup script for automation
  • Additional test examples (VNets, PostgreSQL)
  • Helper scripts for running and cleaning up tests
  • Flux GitOps configurations
  • Troubleshooting guides

Key Takeaways (Your Assembly Summary Sheet)

  1. E2E testing validates your entire Crossplane setup - from XRD to actual Azure resources
  2. Crossplane v2 uses pipeline mode - more powerful and flexible than v1
  3. Test in layers: Create → Verify → Cleanup (just like assembling furniture)
  4. Automate with tools: kuttl for testing, Flux for GitOps
  5. Always clean up test resources - Azure charges are real!

What's Next?

Now that you know how to test your "IKEA furniture," you can:

  • Create more complex compositions (networks, databases, full applications)
  • Integrate E2E tests into your CI/CD pipeline
  • Build a platform with Backstage for self-service infrastructure
  • Add cost tracking and governance policies

Remember: just like IKEA furniture, Crossplane infrastructure is much better when you follow the instructions and test that everything fits together properly. Happy building! 🔧


Got questions? Drop them in the comments! I'd love to hear about your Crossplane testing experiences.

If you found this helpful, give it a ❤️ and share it with someone who's also wrangling cloud infrastructure!


About the Author: I'm Willem, a Cloud Engineer transitioning to platform engineering. I believe complex infrastructure concepts should be accessible to everyone - even if it means comparing them to Swedish furniture. Follow me for more cloud-native content!

Top comments (0)