A disposable Azure tenant for testing the code that reads one
TenantLess generates statistically realistic Azure estates and serves them over ARM-compatible HTTP, so software that reads Azure can run against a tenant that was never provisioned.
$ docker compose --profile demo up generator-1 | Generated tenant 56d2557f…36e9: generator-1 | 50 subscriptions, 828 resource groups, generator-1 | 4960 resources, 1238 violations, … generator-1 | (seed=42, …, elapsed=2748ms) mock-server-1 | tenantless-server listening (HTTP) mock-server-1 | addr=0.0.0.0:8080 $ curl -H "Authorization: Bearer anything" \ "localhost:8080/subscriptions?api-version=2022-12-01" { "value": [ { "subscriptionId": "04379017-…-acf97defd49f", "displayName": "infra-sbx-portal-sub", "state": "Enabled", … }, …49 more subscriptions ] }Output captured from the published v1.5.0 images and lightly trimmed. This is the demo profile, the smallest bundled estate, and seed 42 reproduces it exactly on every run.
Not another cloud emulator
moto and LocalStack emulate cloud services so that you can build and deploy against them, and LocalStack has announced an Azure edition. TenantLess is built for a different question: whether code that reads an existing estate copes with it.
| moto / LocalStack | TenantLess | |
|---|---|---|
| What you get | An empty room that you furnish yourself through API calls. | A building that is already furnished, at the scale you specify. |
| The question tested | "Does my deployment work?" | "Does my scanner survive an estate it didn't create?" |
| Cost of entry | You write the setup for every test. | You describe the estate once, as a statistical profile. |
| What becomes possible | Testing your write and deploy path. | Testing your read path against a scale and a set of misconfigurations you couldn't build by hand. |
What matters is who owns the estate your code reads, not the size of your company
When the Azure estate your software reads belongs to a customer or a client, you can't copy it, borrow it to debug a problem, or show it in a demo, so you need a realistic stand-in. That need comes up in three kinds of work, each covered in its own section below: building tools that read Azure, working on client estates, and training people who can't yet touch production.
Tool builders
Test code that reads Azure at a scale you can't buy for CI.
Scanner, CSPM and FinOps vendors · open-source tool authors · internal platform teams Supported surface, scale, quickstart → 02Client-facing teams
Reproduce a client's issue, run demos and prepare audits without access to their tenant.
Consultancies · auditors · vendor sales and solutions engineers Reproduce, demo, rehearse → 03Trainers and educators
Teach on a realistic estate that every learner receives in exactly the same state.
Universities · bootcamps · certification providers · internal onboarding Gradable labs, onboarding twins →Company size doesn't change any of this. A three-person studio selling an Azure cost tool has to handle estates it has never seen from its very first customer, just as a large vendor does. The difference is that a large vendor can pay for a few realistic test tenants, and the studio usually can't.
Test the code that reads Azure, at the scale it will meet
This section is for vendor engineering teams, open-source tool authors and internal platform teams. You can run your unmodified scanner, policy engine or cost tool against an estate you specify, and the table further down shows which Azure APIs are covered.
When the simulator is worth it
Whether TenantLess is worth setting up depends mostly on the size of the estates your code has to handle. Move the slider to a size to see what an estate of that size looks like, what tends to break in code that reads it, and whether a simulator is the right tool at that point.
As a rough guide, hand-written fixtures are quicker below about 1,000 resources. Between 1,000 and 10,000, pagination and cross-references start to matter, and beyond 50,000 you are testing conditions you could not set up any other way.
Supported surface
TenantLess covers the ARM calls that a discovery scan makes rather than the whole of Azure, so compare this table with the calls your tool relies on.
| Surface | Status | What's there |
|---|---|---|
| Subscriptions, resource groups, resources | served | List and detail, arbitrarily nested resource types, $filter on type, location and tags, $top / $skiptoken paging with nextLink, ARM error shapes. api-version 2022-12-01. |
| Resource writes | opt-in | PUT / PATCH / DELETE on any resource id with ETags, behind --enable-arm-writes. It is off by default, and resource-group create and delete are not available yet. |
| Cost Management | served | The query API at subscription and resource-group scope, over generated cost data pinned to a date you choose. api-version 2023-03-01. |
| Authorization | served | Role definitions (list and detail) and role assignments. api-version 2022-04-01. |
| Tokens | served | An Entra-style /{tenant}/oauth2/v2.0/token endpoint and JWKS. Any bearer token is accepted by default, and --enforce-auth switches to validating RS256 JWTs. |
| HTTPS | served | --tls adds a listener on :8443 with an ephemeral self-signed certificate, next to plain HTTP on :8080. |
| Azure Resource Graph (KQL) | not served | There is no Microsoft.ResourceGraph endpoint, so scanners that enumerate through KQL need to fall back to ARM list calls. |
| Microsoft Graph | not served | There are no users, groups or app registrations, and role assignments point to synthetic principal ids. |
| Azure Policy | not served | There are no policy definitions, assignments or compliance states. Governance violations are written into resource properties, which is where a scanner reads them. |
| Data planes | not served | Only the management plane is served, so there is no blob, Key Vault secret or database traffic. |
- Any
api-versionis accepted, but it doesn't change the response shape, which always follows the versions listed above. - A generated estate stays unchanged until you apply drift or, with writes enabled, modify it yourself. Nothing is actually running, so there are no live metrics or activity logs.
- The full matrix is in docs/compatibility.md.
Point a client at it
The Azure SDKs refuse to send a bearer token over plain HTTP, so give the mock server its HTTPS listener: set TLS=true and BASE_URL=https://localhost:8443 on the mock-server service and publish port 8443 (from source: tenantless serve --tls). Then override the endpoint, hand the client any token, and accept the self-signed certificate.
You don't need a login, a tenant id or an Entra app registration, and the SDK follows nextLink pagination as it would against Azure.
- With the az CLI,
az restworks with--skip-authorization-headerand your ownAuthorizationheader, butaz cloud registerandaz logindon't, because the mock has no/metadata/endpoints. - Terraform isn't supported yet. A
plan → apply → refresh → destroyworkflow that converges against the mock is on the roadmap.
# pip install azure-mgmt-resource \ # azure-mgmt-resource-subscriptions from azure.core.credentials import AccessToken from azure.mgmt.resource.resources import ( ResourceManagementClient) from azure.mgmt.resource.subscriptions import ( SubscriptionClient) class AnyToken: # any non-empty bearer is accepted def get_token(self, *scopes, **kw): return AccessToken("tenantless", 2**31) opts = dict(base_url="https://localhost:8443", connection_verify=False) # self-signed cert subs = list(SubscriptionClient(AnyToken(), **opts) .subscriptions.list()) rm = ResourceManagementClient(AnyToken(), subs[0].subscription_id, **opts) print(len(subs), sum(1 for _ in rm.resources.list())) # 50 131 (demo estate: 50 subscriptions, # 131 resources over two pages)Run against the published 1.5.0 images with azure-mgmt-resource 26.0.0. In that release
SubscriptionClient moved to its own package.
What it simulates
TenantLess simulates seven planes of a real estate. Nothing it generates is provisioned, but everything is shaped like production, including pagination and error responses.
List, detail and $filter with pagination and arbitrarily nested resource types; ARM-shaped errors. Opt-in PUT / PATCH / DELETE with ETags.
The Cost Management query API over generated cost data, pinned to a date you choose so results repeat.
Entra-style token issuance, 8 built-in roles, role assignments, and over-privilege you can inject.
Repeatable configuration drift that a re-scan detects, and a one-step undo that reverses it.
Hub-and-spoke peering, shared Key Vaults, central logging, private endpoints across subscriptions.
18 violation types across severities, injected at rates you control, with an answer key for grading.
A browser UI to generate, snapshot, restore and explore estates, plus a guided scanner demo.
Reading only your own estate?
If your tooling only reads your own tenant, your dev subscription already resembles production and costs nothing to inspect. TenantLess is still useful in two situations: CI that needs a large, stable estate, and rehearsing a tooling change before it reaches production. With only a handful of subscriptions, you probably don't need it.
From nothing to a scannable tenant in about 12 seconds
It takes one command and needs only Docker, with no Python, Node or Rust on the host.
- 1Compose starts PostgreSQL and seeds the demo estate, the smallest bundled profile, with 50 subscriptions, 828 resource groups and 4,960 resources.
- 2The ARM mock server starts on
localhost:8080. You can point your client at it; the SDK setup is described above. - 3The web console is at
localhost:8080/ui. Larger profiles, source builds and bring-your-own PostgreSQL are in the README.
# one-command quickstart $ cp .env.example .env $ docker compose --profile demo up # scan it like a real ARM tenant $ curl -H "Authorization: Bearer anything" \ "http://localhost:8080/subscriptions?api-version=2022-12-01" # web console: http://localhost:8080/uiCommands from the README. About 12 s from
up to the first ARM 200, measured on pre-built images.
Work on a client's estate without their tenant
This section is for consultancies, auditors, and vendor sales and solutions engineers. A client won't lend you their production tenant, but they can run a read-only analysis on it and send you the resulting statistics file.
Reproduce a client's issue
The analyzer runs read-only on the client's side and exports only distributions, which you use to regenerate a matching estate locally and debug against it.
- 1On the client's side, the analyzer reads their subscriptions through Azure Resource Graph using their own credentials and writes a statistical profile. If any name from their denylist appears in the output, the run fails.
- 2They send you the resulting JSON file. It contains distributions such as the type mix, counts per subscription, co-occurrence, naming patterns and cost curves, but never a name, id or tag value.
- 3On your side, you generate an estate from their profile. It has the same shape and scale with nothing real in it, and with a fixed seed every colleague gets the identical estate.
# client side (reader access to the subscriptions) $ uv sync --extra azure $ uv run tenantless analyze \ --source "azure:sub-id-1,sub-id-2" \ --denylist profiles/.client-denylist.json \ --out client-profile.json # your side $ uv run tenantless generate \ --profile client-profile.json --seed 42From docs/profiles.md. The analyzer queries Resource Graph through DefaultAzureCredential.
Demo without client data
Combine a fixed seed, a violation mix chosen from 18 types and the guided scanner demo. Every run produces the same findings, so a sales demo holds no surprises and never exposes a real client.
Rehearse an audit
Run your scripts and checklists on an estate shaped like the client's before you get access, so you already know where your tooling slows down and which findings it will raise.
What leaves the client, and what never does
Only aggregate distributions leave the client's environment; names, ids and tag values never do. Two automated controls enforce this: any value seen fewer than 5 times is dropped, and the output is checked against the denylist, which stops the run on any match.
Every learner, the same realistic estate
Teaching FinOps, SRE, DevOps and cloud governance means practising on infrastructure. Production is off limits, sandboxes are small and expensive, and no two people can safely share one. TenantLess gives every learner the same production-shaped Azure estate without an Azure bill, real data or any risk to live systems.
A real estate to teach on
Student credits pay for a handful of resources, not 250 subscriptions with hub-and-spoke networking, cost history and misconfigurations to find. A fixed seed gives every student the identical estate, so labs are gradable and cohorts comparable, year after year.
A lab range that never runs out
Cloud academies, certification programmes and the platform vendors' own learning teams can run the same scenario for 5 learners or 5,000, without provisioning a tenant per seat or cleaning up after each class. After a reset between sessions, the estate is back exactly as it was.
Practise on a twin before touching production
New cloud, FinOps and SRE hires learn your landing-zone shapes, naming and policies on a synthetic twin generated from your own tenant's statistics. Only distributions leave the real estate (same privacy controls as client work), so joiners can break things freely before they get production access.
A lab combines a profile, a seed and a task. The seed makes the estate identical for everyone, and the injected violations and drift batches serve as the answer key.
Learners can practise cost allocation against the Cost Management API with results pinned to a date, then drift detection and one-step undo on an estate that resets.
Eighteen violation types across severities, injected at rates you choose, give audit and policy training a range that can be graded.
There is no shared scenario library yet. For now you write the task and TenantLess provides an estate that makes it gradable. If you build labs on it, please open an issue so we can collect them.
The latest release, 1.5.0, adds opt-in ARM writes. Each new capability so far has been additive and off by default, and with writes disabled the responses are byte-identical to the previous release. Resource-group create and delete come next, followed by a terraform plan → apply → refresh → destroy workflow that converges against the mock. All releases →