Troubleshooting
This is a runbook: a symptom, then the three places you look — the
bigfleet_azure_* metrics on --metrics-addr, the structured logs on stderr,
and a Get against the machine — and the fix.
# What's the provider doing right now?curl -s localhost:9090/metrics | grep -E 'bigfleet_azure_(api_calls|grpc_requests|reconcile|price_refresh|spot_evictions|panics)_total'
# gRPC error rate, by method and code:curl -s localhost:9090/metrics | grep bigfleet_azure_grpc_requests_total
# Azure API errors, by operation:curl -s localhost:9090/metrics | grep 'bigfleet_azure_api_calls_total' | grep 'outcome="error"'op on the Azure counters is the logical operation: CreateVM, DeleteVM,
ListVMs, Configure (the bootstrap extension), Drain (the drain extension),
RetailPrices, and ResourceSkus. A spike of outcome="error" on one op
localizes almost every failure below.
Machines stuck or FAILED
Configure/Drain/Create run async under
providerkit transition timeouts (Create 8m,
Configure 8m, Drain 15m, Delete 8m). A machine that exceeds its timeout, or whose
backend call returns an error, lands in FAILED rather than a false
Idle/Configured — that is by design. To find why, correlate the failing RPC in
the logs with the Azure op that errored, and read last_error from Get.
Create times out (VM never provisions)
CreateInstance creates the NIC then the VM and blocks on the create poller
before returning Idle. If that exceeds the kit’s 8m Create timeout, the machine
goes FAILED.
- Symptom:
CreateVMcounter increments, but the machine never leaves Creating; a Create RPC with a non-OKcode. - Diagnose: look at the VM directly — it is usually a quota, image, or subnet
problem.
Terminal window az vm show -g <rg> -n <vm-name> --query 'provisioningState'az vm list -g <rg> --query "[?tags.\"bigfleet-managed\"=='true'].[name,provisioningState]" -o table - Fix: resolve the underlying problem — regional vCPU quota for the VM family (the most common cause; request an increase), a wrong image URN, or a subnet with no free addresses. For Spot, see SkuNotAvailable / eviction below.
Quota / throttling (429)
- Symptom:
op="CreateVM",outcome="error"withOperationNotAllowed(quota) or429/TooManyRequests(ARM throttling) in the logs; risingbigfleet_azure_api_duration_seconds{op="CreateVM"}. - Diagnose: check the family quota:
az vm list-usage -l <location> -o table. Concurrent shards scaling at once is the usual throttling cause. - Fix: request a quota increase, spread creates (smaller per-shard offering
count, stagger scale-ups), and confirm the Create timeout comfortably exceeds worst-case retry backoff.
Bootstrap extension failure (Configure → FAILED)
Configure runs a CustomScript extension that writes the opaque blob and runs the
--bootstrap-hook, then tags bigfleet-cluster. A failed extension returns an
error and the machine goes FAILED.
- Symptom:
op="Configure",outcome="error";last_errormentions the extension. - Diagnose: read the extension’s status:
Terminal window az vm extension show -g <rg> --vm-name <vm> -n bigfleet-hook \--query 'instanceView.statuses' - Fix: make the hook robust. The image must ship an executable at
--bootstrap-hook(default/opt/bigfleet/bootstrap) that consumes the blob file and joins the cluster, and exits non-zero on failure. A hook that is missing, non-executable, or joins the wrong cluster is the usual culprit. Make sure the node has egress to your cluster API and to the CustomScript extension’s package source.
Drain times out (Drain → FAILED)
Drain runs the hook’s --drain path (cordon + kubectl drain) and clears the
bigfleet-cluster tag. An incomplete drain surfaces as FAILED, never a false
Idle.
- Symptom:
op="Drain",outcome="error"/DeadlineExceeded. Strict PodDisruptionBudgets are the classic cause (hence the generous 15m Drain timeout). - Diagnose:
kubectl get pods --field-selector spec.nodeName=<node> -Aand check PDBs blocking eviction. - Fix: relax the offending PDB or extend the grace period. Ensure the hook’s drain uses the Kubernetes node name that matches the VM (with the Azure cloud provider the node name is typically the VM’s computer name).
Spot: SkuNotAvailable / evicted
Spot VMs are created with priority=Spot, evictionPolicy=Delete, maxPrice=-1.
When the pool is dry, Azure rejects the create; when capacity is reclaimed, the VM
is deleted out from under you. The OS disk and NIC carry DeleteOption=Delete, so
they are removed with the VM even on this provider-less eviction path — no orphan
disks/NICs accrue cost. (If you see lingering *-nic or disks after evictions,
that invariant has regressed.)
- Symptom: Create FAILs quickly with
SkuNotAvailable/OverconstrainedAllocationRequest; or a previously-Idle Spot machine returns to Speculative andbigfleet_azure_spot_evictions_totalincrements. - Diagnose: Spot availability is size × zone specific. Cross-check the forecast
you are already publishing — a higher
interruption_probabilityfor that size predicts exactly this. - Fix: offer more (size, zone) pairs so the engine has fallbacks; spread
across both
--zone-a/--zone-b(and more). The provider does not silently fall back to pay-as-you-go — capacity type is a property of the offering slot, so diversify offerings rather than expecting automatic substitution.
Identity / authorization (AuthorizationFailed)
- Symptom: any
opwithoutcome="error"andAuthorizationFailed/does not have authorization to perform actionin the logs; or a blanket failure on the very firstListVMs/CreateVM. - Diagnose: match the denied action to the role on
Credentials. The provider calls
Microsoft.Compute/virtualMachines/*,.../extensions/*,.../disks/*,Microsoft.Network/networkInterfaces/*, the subnetjoin/action, and the Resource SKUs read — all scoped to the resource group. - Fix:
- A blanket
AuthorizationFailedon the first call usually means the federated credential subject doesn’t match the ServiceAccount, or the role assignment hasn’t propagated (give it a minute). - A denial on the subnet specifically means the role is missing
Microsoft.Network/virtualNetworks/subnets/join/action— the NIC can’t attach. - Confirm Workload Identity is wired: the pod must carry the
azure.workload.identity/use: "true"label and the SA theclient-id+tenant-idannotations.
- A blanket
Cold Spot price
On startup (and for a freshly-offered size) the Spot cache is empty. price never
blocks on the network on the List hot path, so a cold size reports a
conservative fallback of 0.4 × pay-as-you-go until a refresh fills the
cache.
- Symptom: Spot
price_per_hourinGetlooks like a round fraction of pay-as-you-go right after boot, andbigfleet_azure_price_refresh_total{outcome="error"}orop="RetailPrices",outcome="error"is non-zero. - Diagnose: the startup warm-up is best-effort and bounded (20s). A failed
refresh logs
pricing: spot price fetch failed; keeping fallbackper size. The background refresher retries every--price-refresh(default 1h). - Fix: usually self-heals on the next refresh. If it persists, check egress to
prices.azure.comand that the (size, region) actually has a Spot consumption meter (no spot consumption meter for <size> in <region>).
Region-table mismatch
The on-demand seed prices and Spot eviction bands ship for eastus and
westeurope. On-demand prices, Spot prices, and allocatable are all live;
only the seed table (cold fallback) and the eviction forecast are pinned.
- Symptom: at startup,
no pinned on-demand seed table for region; using baseline approximations until the live refresh populatesfor an unseeded region, so on-demandprice_per_hourlooks off only in the brief window before the first refresh. (A size you offer that is missing from the seed table entirely is rejected at startup — the provider refuses to start — rather than pricing at 0.) If on-demand prices stay wrong after the warm-up, checkbigfleet_azure_price_last_success_timestamp_secondsand egress toprices.azure.com. - Fix: the live refresh self-heals the price on the next successful cycle — you
never refresh the seed by hand (it’s an internal ranking floor the live refresh
overrides). For interruption ranking, add your sizes to
evictionBand.
Readiness never goes green
/readyz returns 503 not ready until the server is serving; /healthz is
liveness only. The gRPC grpc.health.v1 status flips to SERVING at the same
point.
- Symptom:
curl localhost:9090/readyz⇒not ready; noserving CapacityProviderlog line. - Diagnose: readiness is set only after
run()reaches the serving point. If the process exits during config load first, you’ll see a startup error on stderr. Common blockers:--azure-backend=azure(orautowith--location) but missing--subscription-id,--resource-group, or--subnet-id.- TLS misconfig (
both --tls-cert and --tls-key are required, or--tls-ca set without --tls-cert/--tls-key). --addralready in use; orno offerings configured/ an offering with emptyvm_sizeorzone.
- Fix: resolve the startup error in the logs. On shutdown (SIGINT/SIGTERM)
readiness intentionally flips back to
not ready— that is expected, not a fault.
Panics
bigfleet_azure_panics_total should stay flat. A recovered panic is converted to
codes.Internal (the RPC fails, the process survives) and logged as
recovered panic in gRPC handler. Any non-zero value is a bug — capture the log
line and the request that triggered it.
See also
- Observability — the full metric/health/log surface
- Credentials — the exact role and Workload Identity wiring
- Pricing & interruption — how price and probability are sourced
- Configuration — every flag, backend modes, the bootstrap model