How to document an Azure tenant you just inherited
A new client signs and the Azure tenant already exists. The previous provider built it, or the client did it themselves years ago, and nobody left documentation behind. This is the order I work in, and why that order matters more than the tooling.
The first ticket does not wait for your documentation
The instinct is to document everything before touching anything. That fails for a practical reason: the first support ticket usually arrives before the documentation is finished, and it arrives about a specific resource. You need to be able to answer three questions about anything in the tenant, quickly:
- What is this, and what depends on it?
- Is it exposed to anything it should not be?
- If I change it, what breaks?
So sequence the work to answer those first, and leave the polished deliverable until the end. Everything below is ordered that way. Each step assumes the Azure CLI is installed and you have at least Reader on the subscription.
1. Inventory: what actually exists
Start with the full resource list, not the portal dashboard. The dashboard shows what someone pinned; the list shows what you are responsible for.
az account list --output table
az account set --subscription "<subscription-id>"
az resource list \
--query "[].{name:name, type:type, group:resourceGroup, location:location}" \
--output table
What matters here is not the individual rows but the shape. Count by type to see what kind of environment you have inherited before you read a single resource in detail:
az resource list --query "[].type" -o tsv | sort | uniq -c | sort -rn
Read the result as a sentence. Thirty virtual machines and no App Service is a lift-and-shift estate that someone stopped modernising. Twelve resource groups for forty resources usually means per-application grouping. One resource group for everything usually means it grew without a plan, and that the naming is the only structure you will get.
Note the resource groups and locations too. Resources spread across regions with no obvious reason are often the residue of a migration that was abandoned halfway.
2. Relationships: what talks to what
An inventory alone will not let you answer a ticket safely, because Azure resources fail in groups rather than individually. The relationships worth establishing early are the ones that turn a routine change into an outage.
- Virtual machine to NIC to subnet. A VM's network profile references its NICs, and each NIC references a subnet. This chain determines what a machine can reach.
- Subnet to virtual network. Subnet resource ids are children of the VNet id, so the hierarchy is readable directly from the id.
- App Service to plan. The
serverFarmIdon a site points at the plan it runs on. Several sites usually share one, which means scaling one affects the others. - Disk to VM. A managed disk carries
managedBywhen attached. An empty value on a disk that looks production-shaped is worth a question.
Start with the network layout, because everything else hangs off it:
az network vnet list \
--query "[].{vnet:name, group:resourceGroup, prefixes:addressSpace.addressPrefixes}" -o table
az network nic list \
--query "[].{nic:name, vm:virtualMachine.id, subnet:ipConfigurations[0].subnet.id}" -o table
You are looking for the shape of the tiering: whether there is a web tier and a data tier, or whether everything shares one flat subnet. The second case is common in inherited environments and it changes the risk of every subsequent change you make.
3. Exposure: what is reachable from the internet
This is the step that most often produces something the client did not know. Do it before you write anything up, because it can change the priority of the whole engagement.
# Public IPs, and what they are attached to
az network public-ip list \
--query "[].{ip:ipAddress, name:name, attachedTo:ipConfiguration.id}" -o table
# Storage accounts that permit anonymous blob access
az storage account list \
--query "[?allowBlobPublicAccess].{name:name, group:resourceGroup}" -o table
# SQL servers reachable from outside the VNet
az sql server list \
--query "[?publicNetworkAccess=='Enabled'].{name:name, group:resourceGroup}" -o table
Then check network security groups, and check them at both levels. A network interface can have an NSG, and so can the subnet it sits in. A machine is only genuinely unprotected when both are absent, and treating a missing NSG at one level as proof of exposure produces false alarms that cost you credibility on the first report you hand over.
az network nsg list --query "[].{nsg:name, group:resourceGroup}" -o table
# Rules allowing traffic from anywhere
az network nsg list --query \
"[].securityRules[?sourceAddressPrefix=='*' && access=='Allow'].{nsg:id, port:destinationPortRange}" -o json
Management ports open to * are the finding to look for first. SSH on 22 and RDP on
3389 reachable from any source are the two that turn up most often in environments nobody has
reviewed in a while.
4. Backups: the question nobody asks until it matters
Ask early whether a backup strategy exists at all, because the answer is frequently no and it takes a long time to fix. It is also the single finding clients react to fastest.
az backup vault list --query "[].{vault:name, group:resourceGroup}" -o table
An empty result means no Recovery Services vault exists anywhere in the subscription, which means nothing is being backed up by Azure Backup. That is worth writing at the top of the report rather than in an appendix.
5. What can move, if a migration is coming
Inherited environments often arrive with a reorganisation attached: consolidate into the client's own subscription, or split a shared one. Do not promise a timeline before checking movability, because Azure's move support is not uniform and the exceptions are unintuitive.
Roughly three quarters of Azure resource types cannot be moved between subscriptions at all, and a smaller set can move between resource groups but not across subscriptions. Some that do move carry prerequisites that make them effectively group moves: a virtual machine has to move together with its managed disks, its network interfaces, and its availability set.
Microsoft publishes the full support matrix per resource provider. Check the specific types in your inventory against it before the scope of the work is agreed, not after.
What to actually hand over
The deliverable that gets read is short and ordered by consequence, not by resource type. What has worked for me:
- A findings list, ordered by severity, each one naming the specific resource and the evidence for it. "Storage account X permits anonymous blob access" beats "review storage security".
- A topology diagram that fits on one page. Eight to fifteen boxes, no network interfaces or NSGs drawn as nodes. A diagram nobody can read is worse than no diagram.
- An inventory appendix, so the client can verify the work without reading it.
- An explicit list of what you could not determine. This is the part people leave out, and it is the part that protects you. If access was missing or a resource type could not be inspected, say so.
That last point is worth insisting on. An assessment that quietly omits what it could not see reads as complete, and the gap surfaces later as your mistake rather than a known limit.
Cloud Discovery is the tool I built for this exact sequence. It connects to a tenant with read-only credentials, maps the relationships above from real API responses rather than from naming conventions, runs the same exposure and backup checks deterministically, and exports the report and topology as client-ready files. Everything stays on your Mac. See how it works.
Related reading
- Your first assessment: the same workflow inside the app, step by step.
- Module reference: what each part of the app produces.
- Privacy and security: where credentials and scanned data live.