Every action you take in Azure — clicking Create in the web portal, running an az command, executing a PowerShell cmdlet, or pasting a Bicep deployment — ends up as the same HTTPS call to the same API. Once that clicks, the four tools in this lesson stop feeling like four separate products and start feeling like four steering wheels bolted to one engine. The engine is Azure Resource Manager (ARM); the wheels differ only in ergonomics, not in power. Pick whichever suits the moment, and your skills carry across all of them.
In this lesson you’ll learn what the Portal, Azure CLI, Azure PowerShell, and Cloud Shell each are, when to reach for which, and the small set of skills — signing in, setting your subscription context, reading and reshaping JSON output, understanding idempotency — that make you fast and confident in every one. Because this is a reference you’ll come back to, the options, output formats, error codes, install paths and a symptom→cause→confirm→fix playbook are all laid out as scannable tables. Read the prose once; keep the tables open while you work.
By the end you’ll prove it in a hands-on lab using only your browser: no installs, nothing to configure on your laptop. You’ll create a tagged resource group, query it three different ways, deliberately trigger and fix the most common beginner error, and tear it all down with a single command — and you’ll understand exactly what ARM did underneath each step.
What problem this solves
Beginners lose their first week to a handful of avoidable, demoralising failures: a resource that “vanishes” the moment it’s created, an (AuthorizationFailed) wall that nobody explains, a script that errors the second time you run it, and JSON output so noisy you copy values out by hand. None of these are hard once you understand the model — but without the model they feel random, and “random” is what makes people give up on the CLI and retreat to clicking, which then doesn’t scale.
What breaks without this knowledge is repeatability. If the only way you know to create a virtual network is to click through eleven blades, then doing it across three environments means thirty-three chances to fumble a setting, and a teammate reproducing your work will get something subtly different. The Portal is wonderful for learning and one-offs; it is a liability as a deployment mechanism. The fix is to learn the one control plane everything routes through, the one way to authenticate and pick context, and the one output language (--query/JMESPath) that turns the CLI from a wall of text into a precise instrument.
Who hits this: everyone new to Azure, plus experienced engineers arriving from AWS or GCP who must remap muscle memory. It bites hardest on people with multiple subscriptions (the “wrong subscription” trap), anyone scripting on a headless/remote box (the az login browser problem), and first-time automation authors who reach for their personal login instead of a service principal or managed identity. Get the four-tools-one-engine model right and the whole platform becomes legible.
To frame the field before the deep dive, here is every tool this lesson covers, the failure it most commonly causes a beginner, and the one place to look first:
| Tool | What it is | Most common beginner failure | First thing to check |
|---|---|---|---|
| Portal | Web GUI at portal.azure.com |
Created in the wrong subscription/region; can’t reproduce later | Subscription/Directory switcher (top-right) |
Azure CLI (az) |
Cross-platform command line | “command not found”, wrong subscription, noisy JSON | az account show; az version |
Azure PowerShell (Az) |
PowerShell cmdlets | Forgot Connect-AzAccount; mixing with old AzureRM |
Get-AzContext |
| Cloud Shell | Browser shell, tools pre-installed | Session timeout; storage prompt confusion | The {}/>_ toolbar icon |
| ARM (under all of them) | The single management API | “Why did it land there?” — context confusion | az account show + Activity log |
Learning objectives
By the end of this lesson you can:
- Explain the four ways to interact with Azure and choose the right one for a given task.
- Describe how the Portal, CLI, PowerShell, Bicep and SDKs all call the same Azure Resource Manager (ARM) API, and why that makes results and permissions uniform.
- Sign in with
az login(including--use-device-codeand--service-principal) and set the correct subscription context withaz account set. - Explain idempotency and why it makes scripted, declarative Azure commands safe to re-run.
- Shape command output with
--query(JMESPath) and pick the right-o(output) format for eyes, variables, or tooling. - Diagnose and fix the canonical beginner errors — wrong subscription,
AuthorizationFailed, unregistered resource provider, headlessaz login— using the exact confirming command. - Create, tag, query, and delete a resource group entirely from Azure Cloud Shell with nothing installed locally.
Prerequisites & where this fits
You need a Microsoft account with access to an Azure subscription — a billing-and-isolation boundary that holds your resources. The free account from What Is Azure? Accounts, Subscriptions, Regions & Resource Groups is perfect. If “subscription” and “resource group” are new words, read that lesson first; this one assumes you know that resources live inside resource groups inside a subscription, and that a subscription sits under a management group in a tenant. A little comfort with a command line and HTTP basics (a request, a status code, JSON) helps but isn’t required — Cloud Shell removes every install hurdle.
This is Lesson 2 of the Fundamentals module of the Azure Zero-to-Hero course, and it is the tooling spine of everything after it: the Microsoft Entra ID fundamentals lesson explains the identity these tools authenticate against; Azure Policy as Code and Infrastructure as Code 101 with Terraform build on the same ARM control plane you meet here. The deeper context — IaaS/PaaS/SaaS and why a managed control plane exists at all — is in Cloud Computing Fundamentals: IaaS, PaaS, SaaS.
Here is where each tool sits relative to the others, so you reach for the right one by default:
| Layer | Tool(s) here | Speaks | Best at | Hands off to |
|---|---|---|---|---|
| Human exploration | Portal | Clicks → REST | Learning, dashboards, one-offs | “Export template” → IaC |
| Imperative scripting | Azure CLI, PowerShell | Commands → REST | Day-to-day ops, CI/CD glue | Bicep/Terraform for full stacks |
| Declarative IaC | Bicep, ARM JSON, Terraform | Files → REST | Reproducible environments | ARM to deploy |
| Programmatic | Azure SDKs (Python/.NET/Go/JS) | Code → REST | Apps that manage resources | ARM to deploy |
| Control plane | ARM | REST (management.azure.com) |
Auth, RBAC, Policy, orchestration | Resource providers |
Four steering wheels, one engine
Azure gives you four front doors. They differ in ergonomics, not in power — almost anything you can do in one, you can do in the others, because they all funnel into ARM. The art is matching the tool to the task.
| Tool | What it is | Best for | Trade-off | Lives where |
|---|---|---|---|---|
| Portal | The web GUI at portal.azure.com |
Learning, exploring, one-off changes, dashboards, reading metrics | Manual; not repeatable; easy to forget what you clicked | Browser |
Azure CLI (az) |
Cross-platform command-line tool (Python-based) | Scripting, automation, CI/CD, day-to-day operations on any OS | You must learn command names; text in, text out | Local shell / Cloud Shell |
Azure PowerShell (Az module) |
A set of PowerShell cmdlets | Windows-centric shops; scripts passing rich objects between commands | Best inside PowerShell; verbose for quick one-liners | PowerShell / Cloud Shell |
| Cloud Shell | A browser-based shell with az, Az, Bicep, Terraform pre-installed |
Anything, from any device, with nothing installed | Needs internet; idle session is recycled | Browser (portal.azure.com, shell.azure.com) |
A useful rule of thumb: the Portal is for understanding; the CLI/PowerShell/IaC are for repeating. Do something once, click it. Do it twice — or want a teammate to do it identically — script it. The Portal even helps you make the jump: many blades have a “Download a template for automation” link on the final Review + create step that hands you the equivalent ARM/Bicep.
When you’re unsure which wheel to grab, this decision table settles it:
| If your task is… | Reach for | Why |
|---|---|---|
| “What does this blade even do?” | Portal | Discoverability; inline help and validation |
| Read one metric / eyeball status | Portal or az ... -o table |
Fast visual scan |
| The same change across 3 subscriptions | CLI/PowerShell loop | Identical, scriptable, auditable |
| Stand up a whole environment reproducibly | Bicep/Terraform | Declarative, version-controlled, idempotent |
| A quick fix from your phone / a locked-down laptop | Cloud Shell | Nothing to install; already signed in |
| Pass structured results between steps in PowerShell | PowerShell (Az) |
Object pipeline, not text parsing |
| Capture a value into a shell variable in bash | CLI (--query ... -o tsv) |
Clean, unquoted scalar |
| An app that creates resources on a schedule | SDK + managed identity | No human, least privilege |
They all call the same API: Azure Resource Manager
Here’s the idea that ties the whole lesson together. Azure Resource Manager (ARM) is the single control plane — the management API and orchestration engine — in front of every Azure resource, reachable at https://management.azure.com. When you click Create in the Portal, the Portal sends an HTTPS request to ARM. When you run az group create, the CLI sends the same kind of request. PowerShell’s New-AzResourceGroup does too, as does a bicep/terraform apply. ARM authenticates you (via a Microsoft Entra ID token), checks your RBAC permissions, evaluates any Azure Policy rules and resource locks, then routes the change to the right resource provider (Microsoft.Compute, Microsoft.Network, Microsoft.Storage, …) and records the operation in the Activity log.
Three consequences fall out of this, and they matter every day. Consistency: a resource group created in the Portal is byte-for-byte the same object as one created by the CLI — there is no “CLI version” of a resource. Uniform permissions: if RBAC says you can’t delete a VM, you can’t delete it from any tool; locks and policies apply everywhere. Transferable skills: learn the ARM mental model — resources, resource groups, subscriptions, providers — once, and every tool becomes a different syntax over the same concepts.
Here is the same request, expressed in all four tools plus raw REST, so you can see they’re the same call wearing different clothes:
| Tool | “Create resource group rg-demo in eastus” |
|---|---|
| Portal | Resource groups → Create → fill name/region → Review + create |
| Azure CLI | az group create --name rg-demo --location eastus |
| PowerShell | New-AzResourceGroup -Name rg-demo -Location eastus |
| Bicep / ARM | a targetScope='subscription' deployment creating a Microsoft.Resources/resourceGroups |
| Raw REST | PUT /subscriptions/{sub}/resourcegroups/rg-demo?api-version=2021-04-01 |
And the verbs map cleanly across the CRUD lifecycle — once you know the pattern in one tool you can guess it in the others:
| Operation | Portal | Azure CLI | PowerShell | ARM verb |
|---|---|---|---|---|
| Create / update | Create blade | az ... create / update |
New-Az... / Set-Az... |
PUT |
| Read one | Open the resource | az ... show |
Get-Az... |
GET |
| List many | Browse a list blade | az ... list |
Get-Az... (no -Name) |
GET (collection) |
| Delete | Delete button | az ... delete |
Remove-Az... |
DELETE |
| Invoke action | A button (e.g. Restart) | az ... <verb> |
<Verb>-Az... |
POST |
ARM is not one thing but a small set of moving parts, and every error in this lesson lands on one of them. Here is the control plane decomposed, with the beginner symptom each part produces when it says no:
| ARM component | What it does | Beginner symptom when it blocks you |
|---|---|---|
| Entra token check | Verifies who you are | “Please run az login” / InvalidAuthenticationToken |
| Subscription resolver | Decides which sub the call targets | Resource lands in the “wrong” place |
| RBAC engine | Authorizes the action at the scope | (AuthorizationFailed) |
| Policy engine | Allows / denies / mutates per rules | RequestDisallowedByPolicy |
| Resource lock check | Blocks delete/modify if locked | ScopeLocked / cannot delete |
| Provider router | Routes to Microsoft.* provider |
MissingSubscriptionRegistration |
| Async operation tracker | Tracks long-running PUT/DELETE | Long create; --no-wait returns early |
| Activity log writer | Records the operation | (no error — this is your audit trail) |
Those three consequences — consistency, uniform permissions, transferable skills — are not abstractions; they change what you do day to day:
| Consequence | What it means in practice | What it saves you |
|---|---|---|
| Consistency | Portal-made and CLI-made resources are the same object | No “which tool made this?” archaeology |
| Uniform permissions | A block in one tool is a block in all tools | No false hope that “the CLI will let me” |
| Policy everywhere | A deny rule catches Portal and pipeline | Governance you can’t accidentally bypass |
| Locks everywhere | CanNotDelete protects against every tool |
One safety net, not one per interface |
| Transferable skills | Learn resources/RGs/subs once | Every new service is “the same, new type” |
| One audit trail | Every write is attributable in Activity log | Single place to answer “who changed this?” |
The ARM resource ID is the spine of all of this. Every resource has a single canonical ID of the shape /subscriptions/{subId}/resourceGroups/{rg}/providers/{provider}/{type}/{name} — and every tool uses it. Knowing how to read an ID tells you exactly where a resource lives:
| ID segment | Example value | What it tells you |
|---|---|---|
subscriptions/{subId} |
0000…-0000 |
Which billing/isolation boundary |
resourceGroups/{rg} |
rg-demo |
Which container (and its region as a default) |
providers/{provider} |
Microsoft.Network |
Which resource provider owns it |
{resourceType} |
virtualNetworks |
The kind of resource |
{name} |
vnet-core |
The instance’s name (unique within type+RG) |
Signing in and choosing your subscription
Before any tool can talk to ARM on your behalf, it must know who you are (authentication) and which subscription you mean (context). In Microsoft Entra ID — Azure’s identity service, formerly Azure AD — your sign-in produces a short-lived OAuth bearer token that ARM checks permissions against on every call.
With the CLI you authenticate once per session with az login. In Cloud Shell you’re already signed in, so you can usually skip it. After signing in, confirm your context — make this a reflex:
# Who am I, and which subscription am I pointed at right now?
az account show -o table
# The full picture: identity + tenant + active subscription
az account show --query "{user:user.name, tenant:tenantId, sub:name, subId:id}" -o yaml
There are several ways to authenticate, and picking the right one matters — especially on remote machines and in automation, where the interactive browser flow doesn’t work:
| Login method | Command | When to use | Gotcha |
|---|---|---|---|
| Interactive (browser) | az login |
Your own laptop with a browser | Opens a browser; fails on headless boxes |
| Device code | az login --use-device-code |
SSH/remote box, no local browser | You enter a code at microsoft.com/devicelogin on another device |
| Service principal (secret) | az login --service-principal -u <appId> -p <secret> --tenant <tid> |
CI/CD, scripts | Secret in a vault, never in source; rotate it |
| Service principal (cert) | az login --service-principal -u <appId> -p <cert.pem> --tenant <tid> |
Higher-assurance automation | Manage the cert lifecycle |
| Managed identity | az login --identity |
Code running on an Azure VM/App Service | Only works on an Azure resource with MI enabled |
| Specific tenant | az login --tenant <tenantId> |
Guest in multiple tenants | Pick the tenant before the subscription |
Authorization in ARM is scope-based: a role assignment applies at a level and is inherited downward. Knowing the four scopes — and that they cascade — explains both why a grant at the subscription works for every group inside it and why least-privilege means granting at the smallest scope that unblocks you:
| Scope | Granularity | Example assignment | Inherited by |
|---|---|---|---|
| Management group | Many subscriptions | Org-wide “Reader” for auditors | All child subs + RGs + resources |
| Subscription | One billing boundary | “Contributor” for a team’s sandbox | All RGs + resources in it |
| Resource group | One container | “Contributor” on rg-dev only |
All resources in that RG |
| Resource | A single object | “Reader” on one storage account | Nothing below (leaf) |
The built-in roles you’ll meet first, and exactly what each lets you do — pick the tightest one that unblocks the task:
| Built-in role | Can do | Cannot do | Reach for it when |
|---|---|---|---|
| Owner | Everything, including grant access | (nothing withheld) | Rarely — subscription admins only |
| Contributor | Create/update/delete resources | Grant RBAC to others | Day-to-day building |
| Reader | View everything | Change anything | Read-only/audit access |
| User Access Administrator | Manage role assignments | Touch the resources themselves | Delegating access without data access |
| Resource-specific (e.g. Storage Blob Data Contributor) | Data-plane on one service | Other services | Least-privilege data access |
Many people can see more than one subscription (a personal sandbox and a work subscription, say). Commands act on whichever subscription is active, so always check before you create or delete anything:
# List every subscription you can see; the active one is marked IsDefault = True
az account list --query "[].{Name:name, State:state, Default:isDefault}" -o table
# Switch the active subscription (use the name or the GUID)
az account set --subscription "My Sandbox Subscription"
# Or scope a single command without changing the default:
az group list --subscription "My Sandbox Subscription" -o table
Forgetting to set the subscription is the single most common beginner mistake — you create a resource, then “can’t find it” because it landed in a different subscription. Make
az account showa reflex before every create or delete.
The same context commands map across the three imperative tools:
| Intent | Azure CLI | Azure PowerShell |
|---|---|---|
| Sign in | az login |
Connect-AzAccount |
| Sign in (device code) | az login --use-device-code |
Connect-AzAccount -UseDeviceAuthentication |
| Show current context | az account show |
Get-AzContext |
| List subscriptions | az account list |
Get-AzSubscription |
| Set active subscription | az account set -s "<name>" |
Set-AzContext -Subscription "<name>" |
| Sign out | az logout |
Disconnect-AzAccount |
| Clear cached accounts | az account clear |
Clear-AzContext |
Reading the output: JSON, tables, and --query
By default the Azure CLI returns JSON — structured and machine-readable, perfect for scripts but noisy for humans. The CLI gives you two levers: -o (or --output) to change the format, and --query to filter and reshape the data using JMESPath, a small query language for JSON.
The -o formats, and exactly when each earns its place:
| Format | Output shape | When to use | Pitfall |
|---|---|---|---|
json |
Full JSON (default) | Feeding another tool; you want every field | Verbose for eyeballing |
jsonc |
Colourised JSON | Reading JSON in a terminal | Colour codes break piping |
yaml |
YAML | Human-readable full output | Indentation-sensitive if re-parsed |
table |
ASCII table (selected fields) | Quick visual scan | Drops fields not in the projection |
tsv |
Tab-separated, no header/quotes | Capturing a scalar into a variable | No header means you must know column order |
none |
Nothing | You only care about exit code/side effect | You see no result at all |
--query is where the CLI gets powerful. JMESPath lets you pluck one field, select several into a tidy shape, filter a list, or sort it:
# Pull a single value (great with -o tsv to drop it straight into a variable)
SUB_ID=$(az account show --query id -o tsv)
echo "Active subscription: $SUB_ID"
# Reshape a list: pick name + location for every resource group, as a table
az group list --query "[].{Name:name, Location:location}" -o table
# Filter: only resource groups whose location is eastus
az group list --query "[?location=='eastus'].name" -o tsv
# Sort and take the first; count a collection
az group list --query "sort_by([], &name)[0].name" -o tsv
az group list --query "length(@)" -o tsv
Read those as: [] means “for each item in the list”, {Name:name, …} builds a new object with the fields you want, [?…] keeps only items matching a condition, and @ is “the current result”. You don’t need to memorise JMESPath today — just know it exists, because once you do, you stop copying values out of JSON by hand. Here is the cheat-sheet you’ll actually use:
| You want… | JMESPath | Example |
|---|---|---|
| One field of one object | field |
--query name |
| A nested field | a.b.c |
--query properties.provisioningState |
| Each item’s chosen fields | [].{X:a, Y:b} |
[].{Name:name, Loc:location} |
| Filter a list | [?cond] |
[?location=='eastus'] |
| Filter then project | [?cond].field |
[?location=='eastus'].name |
| Sort ascending | sort_by([], &field) |
sort_by([], &name) |
| First / last / slice | [0], [-1], [0:5] |
[0].id |
| Count | length(@) |
length([?state=='Enabled']) |
| Contains substring | [?contains(name,'prod')] |
filter by name pattern |
| Starts/ends with | [?starts_with(name,'rg-')] |
prefix/suffix filter |
| Multiple conditions | [?a && b], [?a || b] |
[?location=='eastus' && tags.env=='dev'] |
| Negate / not-equal | [?field!='x'] |
exclude a value |
| Rename in projection | {Alias: path} |
{Region: location} |
| Pipe expressions | expr | expr |
[].name | sort(@) |
Two reshaping flags pair with --query and save real time:
| Flag | What it does | Use when |
|---|---|---|
--output tsv |
Strips quotes/braces/headers | Assigning a scalar to a shell variable |
--query "[].x" + -o tsv |
One value per line | Looping in bash (for x in $(...)) |
--all (some commands) |
Cross-subscription listing | You forget which sub a thing is in |
--only-show-errors |
Suppresses warnings | Clean output in CI logs |
Azure PowerShell reaches the same destination with object pipelines instead of a text query language — pick whichever your team lives in:
# Same "name + location of every resource group", PowerShell-style
Get-AzResourceGroup | Select-Object ResourceGroupName, Location
# Filter, then capture a single value into a variable
$subId = (Get-AzContext).Subscription.Id
Get-AzResourceGroup | Where-Object Location -eq 'eastus' | Select-Object -Expand ResourceGroupName
| Concern | CLI (text + JMESPath) | PowerShell (objects) |
|---|---|---|
| Filtering | --query "[?…]" |
Where-Object |
| Selecting fields | --query "[].{…}" |
Select-Object |
| One scalar out | --query x -o tsv |
(... ).x / -Expand |
| Sorting | sort_by(...) |
Sort-Object |
| Looping | bash for over tsv |
ForEach-Object |
| Strength | Cross-platform one-liners | Rich typed objects in a pipeline |
Installing and keeping the tools current
Cloud Shell needs nothing, but on your own machine you install the CLI or the Az module once and then keep them current — stale versions are a real source of “the docs say X but my command fails.” The install path differs by OS:
| Platform | Install Azure CLI | Notes |
|---|---|---|
| Windows | MSI installer, or winget install Microsoft.AzureCLI |
Restart the shell after install |
| macOS | brew install azure-cli |
Homebrew handles upgrades |
| Ubuntu/Debian | curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash |
Adds the Microsoft apt repo |
| RHEL/Fedora | dnf install azure-cli (after adding the repo) |
Microsoft yum repo |
| Any (Docker) | docker run -it mcr.microsoft.com/azure-cli |
Throwaway, always current |
| Cloud Shell | Pre-installed | Nothing to do |
Keeping current and extending the CLI:
| Task | Command | When |
|---|---|---|
| Check CLI version | az version |
Before trusting a doc example |
| Upgrade the CLI | az upgrade |
Monthly, or when a command misbehaves |
| List installed extensions | az extension list -o table |
Audit what’s added |
| Add an extension | az extension add --name <ext> |
A command says “install the extension” |
| Update extensions | az extension update --name <ext> |
Extension behaves oddly |
| Find a command | az find "create storage account" |
You don’t know the syntax |
| Built-in help | az group create --help |
Always — every command self-documents |
| Set a default value | az config set defaults.group=rg-dev |
Stop repeating -g every command |
| List configured defaults | az config get |
Audit what defaults are set |
| Change default output | az config set core.output=table |
Prefer tables globally |
| Run the interactive shell | az interactive |
Guided, autocomplete-driven exploration |
For PowerShell, the equivalents are module operations:
| Task | PowerShell |
|---|---|
| Install the Az module | Install-Module -Name Az -Scope CurrentUser |
| Update it | Update-Module -Name Az |
| Check version | Get-InstalledModule -Name Az |
| Remove legacy AzureRM | Uninstall-Module -Name AzureRM (don’t mix the two) |
| Find a cmdlet | Get-Command -Module Az.* -Noun *ResourceGroup* |
The az command tree is grouped by service, and the verbs (create/show/list/delete/update) repeat across every group — so once you know the shape, you can guess most commands. The families you’ll touch earliest:
az group |
Manages | A first command |
|---|---|---|
az account |
Subscriptions & context | az account show |
az group |
Resource groups | az group list -o table |
az resource |
Any resource generically | az resource list -g <rg> -o table |
az role |
RBAC assignments | az role assignment list |
az provider |
Resource providers | az provider list -o table |
az vm |
Virtual machines | az vm list -o table |
az storage account |
Storage accounts | az storage account list |
az network vnet |
Virtual networks | az network vnet list |
az keyvault |
Key Vaults | az keyvault list |
az webapp |
App Service web apps | az webapp list -o table |
az aks |
Kubernetes clusters | az aks list -o table |
az deployment |
ARM/Bicep deployments | az deployment sub list |
az monitor |
Metrics, logs, alerts | az monitor activity-log list |
az tag |
Resource tags | az tag list |
A handful of global flags work on nearly every az command — learn these once and they pay off everywhere:
| Global flag | Effect | Typical use |
|---|---|---|
--output / -o |
Set output format | -o table / -o tsv |
--query |
JMESPath filter/projection | --query "[].name" |
--subscription |
Target a sub for one command | Avoid switching the default |
--output none |
Suppress output | Care only about side effect |
--only-show-errors |
Hide warnings | Clean CI logs |
--verbose / --debug |
More / full diagnostics | Troubleshooting a failing call |
--help / -h |
Self-documentation | Every unfamiliar command |
--no-wait |
Return without waiting | Long create/delete in background |
Cloud Shell is the zero-install option, but it has concrete limits worth knowing so a session timeout or storage prompt doesn’t surprise you mid-lab:
| Cloud Shell property | Value / behaviour | Why it matters |
|---|---|---|
| Pre-installed tools | az, Az, Bicep, Terraform, kubectl, git |
Nothing to install |
| Shells offered | Bash and PowerShell | Switch from the toolbar |
| Idle timeout | ~20 minutes of inactivity | Session recycles; just reconnect |
| Persistent storage | Optional 5 GB Azure Files share | Saves your $HOME; small monthly cost |
| Ephemeral mode | “No storage account required” | Free, but files vanish at session end |
| Compute cost | Free | You pay only for resources you create |
| Authentication | Already signed in as you | az login usually unnecessary |
| Network | Outbound internet from Microsoft’s network | Works from any device with a browser |
Idempotency: why re-running is safe
Idempotency means an operation produces the same end state no matter how many times you run it. Many Azure operations are declarative: you describe the state you want, and ARM makes reality match. Run az group create --name rg-demo --location eastus once and it creates the group; run it again and ARM sees the group already exists in that state and simply reports success — it does not create a second group or throw an error.
This is what makes scripts trustworthy. A deployment that half-failed can be re-run from the top without fear of duplicates, and infrastructure-as-code tools (Bicep, ARM templates, Terraform) lean entirely on this property. But not every command is idempotent — and knowing which is which prevents a class of “works the first time, breaks the second” bugs:
| Operation | Idempotent? | What a re-run does |
|---|---|---|
az group create (same name/region) |
Yes | Reports success, no duplicate |
az ... create / update (declarative) |
Usually | Converges to the desired state |
bicep/terraform apply |
Yes (by design) | No change if state already matches |
az group delete on a missing group |
No (errors) | “ResourceGroupNotFound” |
az ad sp create-for-rbac |
No (creative) | Creates a new SP each time |
az storage account create (name taken) |
No | Fails — names are globally unique |
az ... restart / start (an action) |
Effect-idempotent | Restarts again; side effect repeats |
| Generating a key/secret | No (creative) | New value each time, by design |
When you write automation, prefer the idempotent, declarative path so reruns are boring. For the genuinely creative operations (globally-unique names, new secrets), add a guard — check existence first, or append a deterministic suffix:
# Make a "create only if absent" pattern explicit and safe to re-run
if [ "$(az group exists --name rg-demo)" = "false" ]; then
az group create --name rg-demo --location eastus
fi
# Globally-unique name? derive a stable suffix instead of a random one each run
STG="st$(echo -n "$SUB_ID-rg-demo" | md5sum | cut -c1-12)"
echo "Deterministic storage name: $STG"
The same declarative idea in Bicep — the entire file is idempotent; deploy it ten times, get one resource group:
// scope: subscription. Deploy with:
// az deployment sub create --location eastus --template-file rg.bicep
targetScope = 'subscription'
param rgName string = 'rg-demo'
param location string = 'eastus'
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: rgName
location: location
tags: {
course: 'azure-zero-to-hero'
lesson: 'L2'
}
}
Architecture at a glance
Picture the management plane as a left-to-right pipeline rather than four separate products. On the left are the clients/tools you actually touch — the Portal and az CLI, Azure PowerShell’s Az module, and Cloud Shell in the browser. Whatever you drive, your request first hits Microsoft Entra ID, which turns your sign-in (or a service principal / managed identity for automation) into a short-lived bearer token. That token rides along to the control plane, ARM, at management.azure.com over HTTPS 443. ARM is where the real decisions happen: it checks your RBAC role and any Azure Policy deny rules or resource locks, and it resolves which subscription you meant. Only then does it hand the work to the right resource provider (Microsoft.Compute, Microsoft.Network, …), which stamps the actual resource into a region-scoped resource group — and records the whole thing in the Activity log you can read back later.
Follow the four numbered failure points and you have a beginner’s entire troubleshooting map. (1) No token yet → “Please run az login”. (2) Token fine but the wrong subscription is active → the resource is created, just “somewhere else”. (3) RBAC/Policy refuses → (AuthorizationFailed). (4) The provider was never registered in this subscription → MissingSubscriptionRegistration. The fifth marker is the read path: when something changed and you don’t know who or when, ARM’s Activity log (and Resource Graph) is the source of truth. Read the diagram as the life of a single request — sign in, get authorized, get routed, get provisioned, get audited — and every error in the lab below maps to exactly one hop on it.
Real-world scenario
Meridian Analytics, a 30-person data consultancy in Pune, onboarded four new graduate engineers in the same week and handed each an Azure account, a sandbox subscription, and the instruction “spin up a dev resource group and a storage account, here’s the wiki page.” By Friday the team lead, Anita, had a backlog of confused Slack messages and a small mess in the billing portal. The post-mortem is a perfect tour of everything this lesson covers — every failure was a tooling/context failure, not a real Azure limitation.
Engineer one created her resource group in the Portal, then “lost” it. She had two subscriptions visible — her personal MSDN benefit and the company sandbox — and the Portal’s top-right switcher was on the personal one. The group existed; it was just billing to the wrong place. Confirm was one line: az account show showed Name: Visual Studio Enterprise instead of Meridian-Sandbox. Fix: az account set --subscription "Meridian-Sandbox", delete the stray group on the personal sub, recreate on the right one. Anita made az account show step zero of the wiki.
Engineer two hit (AuthorizationFailed) the instant he ran az storage account create. He’d been granted Reader on the sandbox, not Contributor — fine for looking, useless for building. az role assignment list --assignee $(az ad signed-in-user show --query id -o tsv) --scope /subscriptions/<sub> showed a single Reader assignment. Anita raised it to Contributor scoped to his dev resource group only (least privilege), and he was unblocked in two minutes.
Engineer three, working over SSH on a jump box with no browser, ran az login and watched it hang trying to open a browser that didn’t exist. The fix — az login --use-device-code, then entering the code at microsoft.com/devicelogin on his laptop — is a five-second change that the wiki had never mentioned because nobody had onboarded a headless user before.
Engineer four’s script worked Monday and failed Tuesday with MissingSubscriptionRegistration for Microsoft.Storage. The brand-new sandbox subscription had never created a storage account, so the resource provider wasn’t registered. az provider show -n Microsoft.Storage --query registrationState returned NotRegistered; az provider register -n Microsoft.Storage (then a two-minute wait to Registered) fixed it permanently.
Anita’s response was the real lesson. She replaced the click-by-click wiki with a single idempotent Bicep file plus a five-line az deployment wrapper, fronted by a mandatory az account show check and a comment block explaining device-code login and provider registration. Onboarding week five took the next two hires twenty minutes each, not a day. The numbers tell the story — and notice every “cause” is on the diagram’s request path, not in the resources themselves:
| Engineer | Symptom | Real cause | Confirm | Fix | Time lost (before/after) |
|---|---|---|---|---|---|
| 1 | RG “missing” | Wrong active subscription | az account show |
az account set -s … |
3 h → 1 min |
| 2 | AuthorizationFailed |
Reader, not Contributor | az role assignment list |
Grant Contributor (scoped) | 2 h → 2 min |
| 3 | az login hangs |
Headless box, no browser | tried in SSH session | az login --use-device-code |
1 h → 5 s |
| 4 | MissingSubscriptionRegistration |
Provider not registered | az provider show … registrationState |
az provider register -n … |
90 min → 2 min |
Advantages and disadvantages
The “one control plane, four tools” model is mostly a gift, but it has sharp edges worth naming so you don’t get cut:
| Advantages (why this model helps you) | Disadvantages (why it bites) |
|---|---|
| Skills transfer: learn ARM once, every tool is a syntax over it | Four tools means four sets of command names/quirks to half-remember |
| Identical results and permissions regardless of tool | The same uniformity means a permission/Policy block hits you everywhere — no “just use the CLI” escape |
| Declarative create/update is idempotent — safe re-runs | Not everything is idempotent (creative commands, unique names) — silent footguns |
| Cloud Shell removes all install/version friction | Cloud Shell needs internet, recycles when idle, and its file persistence costs a few rupees/month |
| The Portal is superb for learning and discovery | The Portal is a poor deployment mechanism — unrepeatable, click-fatigue, drift |
--query/JMESPath turns JSON into precise output |
JMESPath is a small new language; list-vs-object mistakes return confusing null |
| Activity log audits every write across all tools | “It worked but where?” confusion is common until context discipline is a habit |
When does each tool genuinely win? The Portal wins for learning a service you’ve never used and for reading (dashboards, metrics, a quick status glance) — its discoverability and inline validation are unmatched. The CLI wins for cross-platform scripting and CI/CD glue — leaner than PowerShell for one-liners and bash pipelines. PowerShell wins inside Windows-centric automation where passing rich typed objects between cmdlets beats parsing text. Cloud Shell wins when you’re on someone else’s machine, a locked-down laptop, or your phone. And Bicep/Terraform win the moment a thing must be reproducible — which, in production, is almost always.
Hands-on lab
You’ll create a tagged resource group, query it three ways, deliberately trigger and fix the classic “wrong subscription” confusion, then delete everything — using Azure Cloud Shell, so there’s nothing to install. A resource group is just a logical container, so this whole lab is free.
1. Open Cloud Shell
Go to portal.azure.com, sign in, and click the Cloud Shell icon (>_) in the top toolbar. If it’s your first time, choose Bash when prompted. Cloud Shell may ask to create a small storage account to persist files — pick No storage account required (ephemeral) if offered, or accept the default; either works for this lab.
2. Confirm who and where you are
az account show -o table
Expected output (your values will differ):
Name CloudName SubscriptionId State IsDefault
----------------------- ----------- ------------------------------------ ------- -----------
My Sandbox Subscription AzureCloud 00000000-0000-0000-0000-000000000000 Enabled True
If IsDefault isn’t the subscription you want, list them and switch:
az account list --query "[].{Name:name, Default:isDefault}" -o table
az account set --subscription "<the right name>"
3. Create a tagged resource group
Set a couple of variables so the commands read cleanly and re-run safely:
RG="rg-tooling-lab"
LOC="eastus"
az group create \
--name "$RG" \
--location "$LOC" \
--tags course=azure-zero-to-hero lesson=L2 owner=learner
Expected output (trimmed):
{
"id": "/subscriptions/0000.../resourceGroups/rg-tooling-lab",
"location": "eastus",
"name": "rg-tooling-lab",
"properties": { "provisioningState": "Succeeded" },
"tags": { "course": "azure-zero-to-hero", "lesson": "L2", "owner": "learner" }
}
See idempotency for yourself: run the exact same az group create again. It succeeds with identical output — no duplicate, no error.
4. Validate three ways with --query
# (a) Is it there, and did it provision cleanly? — reshape into a tidy table
az group show --name "$RG" \
--query "{Name:name, State:properties.provisioningState, Region:location}" -o table
# (b) Read the tags as JSON
az group show --name "$RG" --query "tags" -o json
# (c) Capture the resource ID into a variable (note -o tsv)
RG_ID=$(az group show --name "$RG" --query id -o tsv)
echo "Resource ID: $RG_ID"
Expected from (a):
Name State Region
-------------- --------- --------
rg-tooling-lab Succeeded eastus
You just did the Portal-equivalent of opening the resource-group blade — but in one repeatable line. To also see it in the GUI, search rg-tooling-lab in the Portal search bar; remember, it’s the same ARM object.
5. Reproduce and fix the #1 beginner trap (optional, if you have 2+ subscriptions)
# Look at what you'd be acting on if you switched — DON'T leave it switched
az account list --query "[].name" -o tsv
# Scope a single command to a DIFFERENT sub without changing your default:
az group list --subscription "<some other sub>" -o table
# ^ Your rg-tooling-lab won't appear here — proof that context decides "where".
# Always confirm you're back where you intend:
az account show --query name -o tsv
This is the exact confusion behind “my resource vanished”: the resource is fine; you were looking at a different subscription.
6. Cleanup
Deleting a group deletes everything inside it — the one-command lab teardown:
az group delete --name "$RG" --yes --no-wait
--yes skips the confirmation prompt; --no-wait returns immediately while Azure deletes in the background. Verify it’s gone (the value flips to false):
az group exists --name "$RG"
Cost note: an empty resource group is free, and Cloud Shell’s compute is free — this lab costs nothing. The optional Cloud Shell storage account (if you created one) costs a few rupees a month for the small file share; delete that storage account too if you won’t use Cloud Shell again.
Lab checkpoints — what “correct” looks like at each step
| After step | Command | Expected state | If it differs |
|---|---|---|---|
| 2 | az account show --query name -o tsv |
Your intended subscription name | az account set -s "<name>" |
| 3 | az group show -n "$RG" --query properties.provisioningState -o tsv |
Succeeded |
Re-run create; check the error string |
| 3 (idempotency) | re-run az group create |
Same output, exit code 0 | A non-zero exit means a real error, not a duplicate |
| 4 | echo $RG_ID |
A full /subscriptions/.../rg-tooling-lab ID |
Empty → check --query id -o tsv |
| 6 | az group exists -n "$RG" |
false (after a moment) |
Still true → delete is async; wait and re-check |
Common mistakes & troubleshooting
This is the playbook — the part to bookmark. First as a scannable table, then the worst offenders expanded with the exact confirming command:
| # | Symptom | Root cause | Confirm (exact cmd / path) | Fix |
|---|---|---|---|---|
| 1 | Resource created but “missing” in Portal | Wrong active subscription | az account show (check IsDefault/name) |
az account set --subscription "<name>", recreate on the right sub |
| 2 | (AuthorizationFailed) on create/delete |
Your identity lacks the RBAC role at that scope | az role assignment list --assignee <id> --scope <scope> |
Get Contributor/Owner at the right scope (least privilege) |
| 3 | az: command not found (local) |
CLI not installed / not on PATH | which az / az version |
Use Cloud Shell, or install the CLI; restart the shell |
| 4 | az login opens no browser / hangs |
Headless/remote box, no local browser | You’re in an SSH session | az login --use-device-code, enter code on another device |
| 5 | MissingSubscriptionRegistration / NoRegisteredProviderFound |
Resource provider not registered in this sub | az provider show -n Microsoft.X --query registrationState |
az provider register -n Microsoft.X; wait for Registered |
| 6 | --query returns null / nothing |
Wrong JMESPath, or list-vs-object mismatch | Re-run with -o json to see the real shape |
Rebuild the query against the actual structure |
| 7 | Script errors on second run (“already exists”) | Non-idempotent command or name collision | Read the error; check the command type | Use create/update; guard with az ... exists; add a stable suffix |
| 8 | StorageAccountAlreadyTaken / name rejected |
Globally-unique name already used | az storage account check-name --name <n> |
Pick a unique name; derive a deterministic suffix |
| 9 | “Insufficient privileges” creating a service principal | You can’t register apps in the tenant | az ad signed-in-user show (then ask admin) |
Ask an Entra admin to create the SP / grant the role |
| 10 | Region/SKU “not available” on create | Quota or capacity restriction for that combo | az vm list-skus -l <region> -o table (for VMs) |
Choose another region/SKU; request a quota increase |
| 11 | Cloud Shell “session ended” mid-task | Idle timeout (~20 min) recycles the session | Just reconnect | Reconnect; persist files in the Cloud Shell storage share |
| 12 | PowerShell cmdlet behaves oddly / not found | Old AzureRM mixed with Az, or stale module | Get-InstalledModule -Name Az* |
Remove AzureRM; Update-Module Az; use Az only |
Beyond symptoms, it helps to recognise the exact ARM error strings by sight — the error code tells you which part of the control plane refused you, which tells you what to do. Keep this decoder next to the playbook:
| Error code / string | Which ARM component | Means | First move |
|---|---|---|---|
Please run 'az login' / InvalidAuthenticationToken |
Entra token check | No / expired token | az login (or --use-device-code) |
(AuthorizationFailed) |
RBAC engine | Your identity lacks the role at that scope | az role assignment list --assignee … --scope … |
RequestDisallowedByPolicy |
Policy engine | A deny policy blocked it | Read the policy; adjust request or scope |
ScopeLocked / cannot be deleted because … lock |
Lock check | A resource lock protects it | az lock list; remove if appropriate |
MissingSubscriptionRegistration |
Provider router | Provider not registered in this sub | az provider register -n Microsoft.X |
NoRegisteredProviderFound |
Provider router | Bad provider/API version | Register provider; check API version |
SubscriptionNotFound |
Subscription resolver | Wrong/inaccessible sub in context | az account set -s "<valid name>" |
ResourceGroupNotFound |
Provider router | RG doesn’t exist (or wrong sub) | Confirm sub, then az group create |
StorageAccountAlreadyTaken |
Provider (validation) | Global name collision | Pick a unique name |
ReadOnlyDisabledSubscription |
Subscription resolver | Sub is disabled/expired | Check subscription state/billing |
InvalidApiVersionParameter |
Provider router | Stale API version | az upgrade; use current version |
QuotaExceeded / ... not available in location |
Provider (capacity) | Region/SKU quota or capacity | Change region/SKU or request quota |
The four that bite hardest, expanded:
1. Resource created but “missing” in the Portal.
Root cause: the wrong subscription is active — you created the resource in subscription A while looking at subscription B in the Portal.
Confirm: az account show and read Name/IsDefault; compare to the subscription selected in the Portal’s top-right switcher.
Fix: az account set --subscription "<the intended one>", delete the stray resource, recreate on the correct subscription. Make az account show your reflex before every create/delete.
2. (AuthorizationFailed) the moment you try to build.
Root cause: your identity has read but not write at that scope — typically Reader instead of Contributor, or no assignment at all at the resource-group/subscription you’re targeting.
Confirm: az role assignment list --assignee $(az ad signed-in-user show --query id -o tsv) --scope /subscriptions/<sub> (add --all to see inherited assignments).
Fix: have an Owner grant you Contributor (or a tighter built-in role) at the smallest scope that unblocks you — the resource group, not the whole subscription. Also rule out a deny Azure Policy or a resource lock (az lock list), which produce the same wall even when your role is fine.
4. az login hangs or opens no browser on a remote box.
Root cause: the interactive flow tries to launch a local browser that doesn’t exist on an SSH/headless machine.
Confirm: you’re in an SSH session or a container with no GUI.
Fix: az login --use-device-code, then open https://microsoft.com/devicelogin on any device with a browser and enter the displayed code. For unattended automation, switch to a service principal or managed identity instead of any interactive flow.
5. MissingSubscriptionRegistration for a provider.
Root cause: the resource provider (e.g. Microsoft.Storage, Microsoft.ContainerService) was never registered in this subscription — common on brand-new sandbox subscriptions that have never created that resource type.
Confirm: az provider show -n Microsoft.Storage --query registrationState -o tsv returns NotRegistered.
Fix: az provider register -n Microsoft.Storage, then poll until it reads Registered (usually 1–3 minutes). List everything with az provider list --query "[?registrationState=='NotRegistered'].namespace" -o table.
Best practices
- Default to the right tool: Portal to learn and inspect, CLI/PowerShell/IaC to repeat and automate. Don’t hand-click anything you’ll do more than once.
- Make
az account showa reflex. Confirm identity and subscription before every create or delete — it’s the cheapest insurance against the most common mistake. - Tag as you create.
owner,env,cost-center,lessonmake cleanup and billing sane later. Apply tags at creation, not as an afterthought. - Group lab and project work in a dedicated resource group so cleanup is a single
az group delete. - Use variables (
RG=,LOC=) in scripts so commands are readable and you change a value in one place. - Pick output deliberately:
-o tablefor eyes,-o tsvfor variables,-o json/yamlfor tools and full detail. - Prefer the idempotent, declarative path (create/update, Bicep/Terraform) so reruns are boring; guard creative commands with existence checks.
- Keep the tooling current (
az upgrade,Update-Module Az) so command examples and new resource types behave as documented. - Never mix
AzureRMandAzin PowerShell — uninstall the legacy module and standardise onAz. - Use device-code or service-principal login on remote/headless machines and in pipelines — never wedge an interactive browser flow into automation.
- Read the error string, then run the one confirming command. Every error in the playbook above maps to a single
az ... show/listthat proves the cause before you change anything. - Learn just enough JMESPath to stop copying values out of JSON by hand —
[].{…},[?…], and-o tsvcover 90% of daily use.
Security notes
- Cloud Shell is convenient and safe by default — it authenticates as you, so it can only do what your RBAC roles allow. It is not a backdoor around permissions.
- Use least privilege. Grant the smallest role at the smallest scope (a resource group, not the subscription); prefer specific built-in roles over Owner. For automation, a scoped service principal or managed identity, never your personal login.
- Never paste secrets into commands that land in shell history. Pull secrets from Azure Key Vault at runtime — see Azure Key Vault: Secret Rotation with Managed Identity — instead of hard-coding them.
- Treat
az logindevice codes like passwords: only enter a code you initiated yourself, on a sign-in page you opened. - Sign out of shared machines with
az logout/Disconnect-AzAccount, and clear cached context (az account clear). - Remember the audit trail. The Portal and CLI share the same identity and the same Activity log — every create/delete is attributable. That’s a safety net, not a reason to be casual.
- Rotate and vault service-principal secrets, prefer certificate or federated credentials over long-lived client secrets, and scope each principal to exactly what it automates.
The identity options ranked by how you should reach for them:
| Identity for the task | Use it for | Avoid it for | Why |
|---|---|---|---|
| Your interactive login | Hands-on work, learning, one-offs | Anything unattended | Tied to a human; MFA; not for servers |
| Service principal (cert/federated) | CI/CD, scheduled automation | Daily manual work | Scoped, rotatable, no human |
| Managed identity | Code running on Azure | Off-Azure callers | No secret to manage at all |
| Owner role | Almost never day-to-day | Routine create/delete | Excessive blast radius |
Cost & sizing
The good news: the tools themselves are free. There is no charge for the Portal, the CLI, the Az module, ARM API calls, or Cloud Shell’s compute. What you pay for is the resources you create with them, plus one small, easily-missed item. Here’s the full picture:
| Item | Cost | Notes |
|---|---|---|
| Azure Portal | Free | No charge to use the GUI |
| Azure CLI / PowerShell | Free | Open-source clients |
| ARM control-plane calls | Free | Management-plane requests aren’t billed |
| Cloud Shell compute | Free | Ephemeral container, recycled when idle |
| Cloud Shell file storage | ~₹15–40/month | A small Azure Files share (5 GB) if you opt in |
| Empty resource group | Free | A logical container holds no cost |
| The resources you create | Varies | This is the actual bill — size per workload |
Two sizing notes specific to the tooling. First, Cloud Shell persistence: the optional storage account is the only line item this lesson can incur — a few rupees a month; delete it if you won’t reuse Cloud Shell. Second, cost visibility starts with the tags you apply here: tagging owner/env/cost-center at creation is what makes a meaningful cost breakdown possible later. For the full discipline of right-sizing and discounts, see Azure Cost: Reservations, Savings Plans & Hybrid Benefit. The CLI itself is a cost tool — az consumption usage list and Resource Graph queries let you script the same numbers the Cost Management blade shows.
Interview & exam questions
1. How do the Portal, Azure CLI, and PowerShell relate to each other? They’re four interfaces over one control plane — Azure Resource Manager. Each sends HTTPS requests to ARM, which enforces authentication (via an Entra token), RBAC, and Policy, then routes to the resource provider. So results are consistent and permissions uniform regardless of tool.
2. When would you choose the CLI over the Portal? For anything repeatable: automation, CI/CD, scripts, bulk or cross-subscription changes, or work that must be identical across environments. The Portal is for learning, exploration, dashboards, and genuine one-offs.
3. What is idempotency and why does it matter in cloud automation? An idempotent operation yields the same end state however many times it runs. It makes declarative deployments and retries safe — a failed run can be re-applied without creating duplicates — which is foundational to infrastructure-as-code. Note that creative commands (new secrets, globally-unique names) are not idempotent.
4. What does --query do, and what language does it use? It filters and reshapes the CLI’s JSON output using JMESPath, letting you pull single values, project specific fields, filter and sort lists — so you script against output instead of parsing it by hand. Pair it with -o tsv to capture a scalar into a shell variable.
5. A colleague says a resource “isn’t there” after their script ran successfully. How do you triage? Check the active subscription (az account show) and the scope/region the script targeted — the resource almost certainly exists, just in a different subscription or resource group than they’re looking in. The Activity log confirms what was created and where.
6. You’re on a headless Linux box and az login hangs. What’s wrong and how do you fix it? The interactive flow tries to open a local browser that doesn’t exist. Use az login --use-device-code and enter the code at microsoft.com/devicelogin on another device — or, for automation, authenticate with a service principal or managed identity instead.
7. A brand-new subscription throws MissingSubscriptionRegistration when you create a storage account. Why? The Microsoft.Storage resource provider hasn’t been registered in that subscription yet. Confirm with az provider show -n Microsoft.Storage --query registrationState; fix with az provider register -n Microsoft.Storage and wait for Registered.
8. CLI vs PowerShell — is there a wrong choice? Rarely. Use whichever fits the team and task: PowerShell shines when you pass rich typed objects between cmdlets or work in a Windows-centric shop; the CLI is leaner for cross-platform one-liners and bash pipelines. Both hit the same ARM API.
9. How do you authenticate automation that runs without a human? With a service principal (certificate or federated credential preferred over a client secret) or, if the code runs on Azure, a managed identity — each scoped to least privilege. Never embed an interactive login or a personal account in a pipeline.
10. What is an ARM resource ID and why is it useful? The single canonical path identifying a resource: /subscriptions/{sub}/resourceGroups/{rg}/providers/{provider}/{type}/{name}. Every tool uses it, and reading it tells you exactly which subscription, resource group, and provider a resource belongs to — invaluable for scripting and troubleshooting.
11. You get (AuthorizationFailed) even though you think you have access. What two things do you check beyond your role? A deny Azure Policy assignment and a resource lock (CanNotDelete/ReadOnly) — both block actions regardless of your RBAC role. Check az role assignment list, then az lock list and the Policy assignments at that scope.
12. Why is -o tsv the right output format for capturing a value into a variable? It returns the raw value with no quotes, no JSON braces, and no header row, so it drops cleanly into VAR=$(... --query x -o tsv) without extra parsing.
These map to AZ-900 (Azure Fundamentals) — describe Azure management and governance tools (Portal, CLI, PowerShell, Cloud Shell, ARM) — and AZ-104 (Azure Administrator Associate) — manage Azure identities and governance and general administration via the CLI/PowerShell against ARM. A compact cert mapping:
| Question theme | Primary cert | Objective area |
|---|---|---|
| Four tools, ARM as the unifier | AZ-900 | Management & governance tools |
| Idempotency, declarative ops, IaC link | AZ-900 / AZ-104 | Tools; deploy & manage resources |
| Subscription context, account set | AZ-104 | Manage subscriptions & governance |
| RBAC AuthorizationFailed, scopes | AZ-104 / AZ-500 | Manage access; secure resources |
| Service principal / managed identity login | AZ-104 / AZ-204 | Identities for automation |
--query/JMESPath, output formats |
AZ-104 | Administer via CLI |
Quick check
- You need to do the same setup across three subscriptions, identically. Portal or CLI — and why?
- True or false: a resource group created with
az group createbehaves differently from one created in the Portal. - Your
az group createsucceeded but you can’t see the group in the Portal. What’s the first thing to check? - What does idempotency mean, and why does it make scripts safe to re-run? Name one operation that is not idempotent.
- Which
-oformat would you use to capture a single subscription ID into a shell variable, and why?
Answers
- CLI (or PowerShell). It’s repeatable and identical every time — loop over the three subscriptions, or run the same script with a different
az account set. The Portal would be three rounds of manual clicking, easy to get subtly wrong. - False. Both tools call the same ARM API and produce the same resource — there’s no per-tool variant.
- Your active subscription (
az account show). The group most likely landed in a different subscription than the one open in the Portal. - Idempotency means re-running an operation lands you in the same end state — no duplicates, no “already exists” errors. It lets you safely re-run a half-finished script from the top. A non-idempotent example:
az ad sp create-for-rbac(creates a new principal each run), or creating a globally-unique-named storage account. -o tsv. It returns the raw value with no quotes, no JSON braces, and no header row, so it drops cleanly intoVAR=$(... -o tsv).
Exercise
In Cloud Shell, create a resource group named rg-tooling-exercise in a region of your choice, tagged purpose=practice. Then, using a single az command with --query, list all your resource groups showing only name and location as a table. Next, use --query with -o tsv to capture that group’s resource ID into a variable and echo it. Finally, clean up with az group delete. Bonus 1: write the create command using variables for name and location, and run it twice to confirm it’s idempotent. Bonus 2: check whether Microsoft.Storage is registered in your subscription with az provider show -n Microsoft.Storage --query registrationState -o tsv, and register it if it isn’t.
Glossary
- Azure Resource Manager (ARM) — the single control-plane API and orchestration engine (
management.azure.com) that every Azure tool calls to create, read, update, and delete resources. - Azure CLI (
az) — cross-platform, Python-based command-line tool for managing Azure from any shell. - Azure PowerShell (
Azmodule) — Azure management as PowerShell cmdlets, built around passing typed objects through a pipeline. - Cloud Shell — a browser-based shell with
az,Az, Bicep and Terraform pre-installed and you already signed in; nothing to install locally. - Subscription — a billing-and-isolation boundary that contains resource groups and resources; the “active” one is what commands target.
- Subscription context — which subscription your tool is currently pointed at (
az account show/az account set). - Management group — a container above subscriptions used to apply governance (RBAC, Policy) across many subscriptions at once.
- Resource group — a logical, region-stamped container that holds related resources and shares their lifecycle.
- Resource provider — the service namespace (e.g.
Microsoft.Compute,Microsoft.Storage) that actually fulfils a resource type; must be registered in a subscription before use. - ARM resource ID — the canonical path
/subscriptions/{sub}/resourceGroups/{rg}/providers/{provider}/{type}/{name}that uniquely identifies any resource. - Idempotency — the property that re-running an operation produces the same end state, with no duplicates or errors.
- Declarative — describing the desired state and letting ARM converge reality to it (the basis of Bicep/Terraform), as opposed to imperative step-by-step commands.
- JMESPath — the JSON query language behind the CLI’s
--queryflag, used to filter, project, and reshape output. - RBAC (role-based access control) — Azure’s permission system that determines what your identity can do at a given scope, enforced by ARM across every tool.
- Service principal — a non-human identity (an app registration’s credentials) used by automation to authenticate to Azure.
- Managed identity — an Azure-managed, secretless identity attached to an Azure resource so its code can call ARM without storing credentials.
- Bearer token — the short-lived OAuth credential Entra ID issues at sign-in and that every ARM call carries to prove who you are.
- Activity log — ARM’s record of every control-plane write (who did what, when), readable from any tool, for audit and troubleshooting.
- Tag — a key/value label on a resource (e.g.
owner=learner) used for organisation, cost tracking, and cleanup.
Next steps
Now that you can drive Azure from any tool and recover from the classic beginner errors, build outward:
- Next: Microsoft Entra ID Fundamentals: Tenants, Users, Groups & RBAC — who gets to do what; the identity and permissions these tools authenticate against.
- Related: Azure Virtual Network Basics: Subnets, NSGs & Peering — your first real multi-resource build using the CLI you just learned.
- Related: Infrastructure as Code 101: Your First Terraform on Azure — graduate from imperative commands to declarative, idempotent stacks.
- Related: Azure Policy as Code: A Governance Pipeline — the deny rules that, alongside RBAC, decide whether your ARM calls succeed.
- Related: Azure Cost: Reservations, Savings Plans & Hybrid Benefit — turn the tags you applied here into real cost visibility and savings.