TenantLessCode30 OSS
Apache-2.0 · Docker only · ~12 s to a scannable tenant

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.

cp .env.example .env && docker compose --profile demo up
local · v1.5.0
$ 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.
Positioning

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 / LocalStackTenantLess
What you getAn 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 entryYou write the setup for every test.You describe the estate once, as a statistical profile.
What becomes possibleTesting your write and deploy path.Testing your read path against a scale and a set of misconfigurations you couldn't build by hand.
The TenantLess web console: a synthetic subscription tree on the left, the resources of a resource group in the middle, and the selected virtual network's ARM id, tags and properties on the right.
The built-in web console on a generated tenant: browse subscriptions, resource groups and resources exactly as a scanner sees them.
Who it's for

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.

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.

01 · For tool builders

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.

SurfaceStatusWhat's there
Subscriptions, resource groups, resourcesservedList 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 writesopt-inPUT / 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 ManagementservedThe query API at subscription and resource-group scope, over generated cost data pinned to a date you choose. api-version 2023-03-01.
AuthorizationservedRole definitions (list and detail) and role assignments. api-version 2022-04-01.
TokensservedAn 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.
HTTPSserved--tls adds a listener on :8443 with an ephemeral self-signed certificate, next to plain HTTP on :8080.
Azure Resource Graph (KQL)not servedThere is no Microsoft.ResourceGraph endpoint, so scanners that enumerate through KQL need to fall back to ARM list calls.
Microsoft Graphnot servedThere are no users, groups or app registrations, and role assignments point to synthetic principal ids.
Azure Policynot servedThere are no policy definitions, assignments or compliance states. Governance violations are written into resource properties, which is where a scanner reads them.
Data planesnot servedOnly the management plane is served, so there is no blob, Key Vault secret or database traffic.
  • Any api-version is 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 rest works with --skip-authorization-header and your own Authorization header, but az cloud register and az login don't, because the mock has no /metadata/endpoints.
  • Terraform isn't supported yet. A plan → apply → refresh → destroy workflow 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.

Management

List, detail and $filter with pagination and arbitrarily nested resource types; ARM-shaped errors. Opt-in PUT / PATCH / DELETE with ETags.

FinOps

The Cost Management query API over generated cost data, pinned to a date you choose so results repeat.

Identity

Entra-style token issuance, 8 built-in roles, role assignments, and over-privilege you can inject.

Drift

Repeatable configuration drift that a re-scan detects, and a one-step undo that reverses it.

Topology

Hub-and-spoke peering, shared Key Vaults, central logging, private endpoints across subscriptions.

Governance

18 violation types across severities, injected at rates you control, with an answer key for grading.

Console

A browser UI to generate, snapshot, restore and explore estates, plus a guided scanner demo.

Rarely worth it

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.

Quickstart

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.

  1. 1Compose starts PostgreSQL and seeds the demo estate, the smallest bundled profile, with 50 subscriptions, 828 resource groups and 4,960 resources.
  2. 2The ARM mock server starts on localhost:8080. You can point your client at it; the SDK setup is described above.
  3. 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/ui
Commands from the README. About 12 s from up to the first ARM 200, measured on pre-built images.
02 · For client-facing teams

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.

  1. 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.
  2. 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.
  3. 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 42
From 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.

03 · For trainers and educators

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.

Universities and bootcamps

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.

Example lab: find the storage accounts allowing public blob access across 250 subscriptions. The injected violations are the grader's answer key.
Training and certification providers

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.

Example exercise: allocate a month of cost to business units, find the anomaly, then fix the tagging through the ARM API and re-run the report.
Internal onboarding

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.

Example drill: apply a batch of drift with a fixed seed, have the new team detect exactly that change, undo it in one step, repeat with a new seed.
What makes a lab

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.

FinOps and SRE

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.

Governance

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 →