Skip to content

Getting Started

This page walks through every step from an empty cluster to a working Azure landing zone managed by Helm + Crossplane. No steps are skipped.


Prerequisites

ToolMinimum VersionPurpose
kubectlv1.28+Cluster access
helmv3.12+Chart rendering and installation
az (Azure CLI)v2.50+Service Principal creation
A Kubernetes clusterv1.28+kind, AKS, or any conformant cluster
An Azure subscriptionResources will be created and billed

Step 1: Create a Cluster (if needed)

If you already have a cluster, skip to Step 2.

For local development with kind:

yaml
# kind-cluster.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
bash
kind create cluster --name crossplane-lab --config kind-cluster.yaml
kubectl cluster-info --context kind-crossplane-lab

Step 2: Install Crossplane

bash
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

helm install crossplane crossplane-stable/crossplane \
  --namespace crossplane-system \
  --create-namespace \
  --wait

Verify:

bash
kubectl get pods -n crossplane-system

Expected output:

NAME                                      READY   STATUS    RESTARTS   AGE
crossplane-7f8d5b4c9-xxxxx               1/1     Running   0          30s
crossplane-rbac-manager-6c9b7d8f4-xxxxx   1/1     Running   0          30s

Step 3: Install Azure Providers

Apply the provider manifests:

bash
kubectl apply -f setup/providers.yaml

This installs exactly four sub-providers:

Wait for all providers to become healthy:

bash
kubectl get providers -w

Expected output (all should show HEALTHY: True):

NAME                      INSTALLED   HEALTHY   PACKAGE                                                   AGE
provider-family-azure     True        True      xpkg.upbound.io/upbound/provider-family-azure:v2.3.0     60s
provider-azure-network    True        True      xpkg.upbound.io/upbound/provider-azure-network:v2.3.0    60s
provider-azure-storage    True        True      xpkg.upbound.io/upbound/provider-azure-storage:v2.3.0    60s
provider-azure-keyvault   True        True      xpkg.upbound.io/upbound/provider-azure-keyvault:v2.3.0   60s

WARNING

If a provider stays HEALTHY: False, check its pod logs:

bash
kubectl get pods -n crossplane-system
kubectl logs -n crossplane-system <provider-pod-name>

Common issue: insufficient RBAC or CRD installation failures on undersized clusters.


Step 4: Configure Azure Credentials

Create a Service Principal

bash
# Replace with your actual subscription ID
export AZURE_SUBSCRIPTION_ID="your-subscription-id"

az ad sp create-for-rbac \
  --name "crossplane-sp" \
  --role Contributor \
  --scopes /subscriptions/$AZURE_SUBSCRIPTION_ID \
  -o json > /tmp/azure-creds.json

The output file contains:

json
{
  "appId": "...",
  "displayName": "crossplane-sp",
  "password": "...",
  "tenant": "..."
}

TIP

Note the tenant value — you'll need it for keyvault.tenantId in your values files.

Create the Kubernetes Secret

bash
kubectl create secret generic azure-creds \
  -n crossplane-system \
  --from-file=creds=/tmp/azure-creds.json

# Remove credentials from disk immediately
rm /tmp/azure-creds.json

Apply ProviderConfig

bash
kubectl apply -f setup/provider-config.yaml

Verify:

bash
kubectl get providerconfig
NAME      AGE
default   10s

Step 5: Deploy the Dev Environment

First, update the tenant ID in your values file:

bash
# Edit charts/azure-base/values-dev.yaml
# Replace REPLACE-WITH-YOUR-TENANT-ID with your actual Azure AD tenant ID

Then deploy:

bash
helm install azure-base-dev ./charts/azure-base \
  -f ./charts/azure-base/values-dev.yaml

Step 6: Watch Resources Provision

bash
# Watch all managed resources
kubectl get managed -w

You'll see resources transition through states:

NAME                          SYNCED   READY   AGE
myapp-dev-rg                  True     True    30s
myapp-dev-vnet                True     False   25s    ← Still creating
myapp-dev-subnet-app          False    False   20s    ← Waiting for VNet
myapp-dev-subnet-data         False    False   20s    ← Waiting for VNet
myapp-dev-nsg                 True     False   20s
myapp-dev-nsg-assoc-app       False    False   15s    ← Waiting for Subnet + NSG
myapp-dev-sa                  True     False   20s
myapp-dev-container-data      False    False   15s    ← Waiting for Storage Account
myapp-dev-kv                  True     False   20s

TIP

Resources that depend on others (Subnet depends on VNet, Container depends on Account) will show SYNCED: False until their dependency is READY: True. This is Crossplane's *Ref resolution working — it automatically retries until the referenced resource exists.

Check a specific resource:

bash
kubectl describe virtualnetwork myapp-dev-vnet

Look for Status.Conditions:

yaml
Status:
  Conditions:
    - Type: Ready
      Status: "True"
    - Type: Synced
      Status: "True"

Step 7: Verify in Azure

bash
# List resource groups
az group list --query "[?tags.project=='myapp']" -o table

# List resources in the group
az resource list --resource-group myapp-dev-rg -o table

Dry Run (No Azure Account Needed)

You can render the chart without applying anything:

bash
helm template azure-base-dev ./charts/azure-base \
  -f ./charts/azure-base/values-dev.yaml

This outputs the exact YAML that helm install would apply. Use this for:

  • Reviewing resources before deploying
  • CI/CD validation
  • Understanding what each template produces
  • Debugging value injection issues

Teardown

bash
# Remove infrastructure
helm uninstall azure-base-dev

# Watch resources being deleted
kubectl get managed -w

Crossplane will delete the Azure resources in reverse dependency order. This usually completes in 2-5 minutes depending on resource types.

DANGER

By default, deletionPolicy: Delete means uninstalling the Helm release deletes the actual Azure resources. This includes data in Storage Accounts and secrets in Key Vaults. If you want to keep Azure resources after Helm uninstall, add deletionPolicy: Orphan to your templates.

Released under the MIT License.