operate-azure

Use when operating Azure by CLI — confirm sign-in, tenant and active subscription first, choose read-only or IaC or a recorded one-off, query with az graph, avoid az rest and RBAC traps, and keep Terr

SKILL.md

GitHub

Operate Azure

Read-only by default. Never change prd by hand when the resource is IaC-managed; change the code and promote it (set-up-terraform-pipeline). A CLI session is for investigation, small CLI-managed fixes with a record, and emergencies — with the revert plan written before the command runs.

1. Audit — signed in, and where (change nothing)

az version                                                       # CLI present, version line
az account show -o json | jq -r '.tenantId, .id, .name'          # ACTIVE tenant and subscription
az account list --query '[].{sub:name, id:id, default:isDefault}' -o table
az ad signed-in-user show --query userPrincipalName -o tsv       # who am I
az graph query -q "Resources | summarize count() by type | top 5 by count_" -o table   # needs extension resource-graph

Also check the track before touching anything (step 3). Most "wrong resource" incidents start with the right command in the wrong subscription.

2. Decide

  • Read-only investigation (default) — show, list, az graph query, az rest --method GET.
  • The resource is IaC-managed and needs a change — change the code, open a PR, promote it. No hand edits on prd; on dev/stg a hand edit creates drift — still say no.
  • CLI-managed resource, one-off change justified — smallest command that does it, JSON output pasted into the ticket as the record, revert command named before running. Firewall IP rules and role assignments made this way are removed again (step 5, step 7).

3. Detect the track

Signal Track Then
terraform/ covers it — tofu -chdir=terraform/envs/<env> state list | grep -i <name>, or it appears in *.tf IaC Read-only here; change via set-up-terraform-pipeline
Repo has *.bicep files or az deployment group scripts Bicep Change the template, deploy through the repo's deployment flow
Neither, and tags/history say CLI CLI-managed One-off change with a record (step 2)
tofu -chdir=terraform/envs/<env> state list 2>/dev/null | grep -i <name>
grep -rn "azurerm_" terraform/ --include='*.tf' 2>/dev/null | grep -i <name> | head -5
find . -name '*.bicep' -not -path '*/node_modules/*' | head -5
az resource show --ids <resource-id> --query tags -o json     # a `managed-by`-style tag tells you who owns it

4. Install only what is missing

brew install azure-cli jq                 # verify version before pinning (../_shared/stack-versions.md)
az extension add --name resource-graph    # for az graph query
az extension add --name account

azd and Bicep only when the project already uses them; the full table is in cli-toolbox.

5. Do the work

Sign in per tenant — never rely on the default subscription:

az login --tenant <tenant-id>
az account set --subscription <subscription-id>
az account show --query id -o tsv          # confirm the subscription you intended

Find things:

az graph query -q "Resources | where name contains '<app>' | project name, resourceGroup, type, location" -o table
az resource list --resource-group <rg> -o table      # ARM resources of the ACTIVE subscription only

az resource list sees only ARM resources. Service principals, groups and users are Entra objects, not ARM resources — use operate-entra-and-exchange.

RBAC:

az role assignment list --assignee <object-id> --scope <scope> -o table
az role assignment create --assignee <object-id> --role "<role-name>" --scope <scope>
az role assignment delete --id <assignment-id>        # the explicit counterpart; create does not clean up

Propagation takes 2–5 minutes — a "still forbidden" right after a grant is usually not an error. Key Vault secrets need a data-plane role (Key Vault Secrets User), not a management role; "secret not found" is usually a missing data-plane role or the wrong subscription, not a missing secret.

az rest — GET, merge, PUT:

# endpoint with no CLI command — e.g. static web app app settings; exact path and api-version per resource docs
az rest --method GET --url "https://management.azure.com<resource-id>/<settings-path>?api-version=<api-version>" -o json > settings.json
jq '.properties.appSettings.newKey = "newValue"' settings.json > settings.merged.json   # merge in jq, never by hand
# then PUT settings.merged.json back at the same URL — a PUT replaces the WHOLE set

Output: -o tsv prints booleans capitalised (True/False). Compare case-insensitively or use -o json | jq.

Firewall-gated data stores (SQL, storage, Key Vault): add a temporary IP rule, work, remove it in the same session — the remove command written down before the add runs (patterns file).

Hygiene: name <app>-<env>-<type>, tag app/env, resource locks CanNotDelete on prd:

az lock create --name lock-<app>-prd --lock-type CanNotDelete --resource-group <rg>
az consumption usage list --top 5 -o table          # current-month cost; budgets per subscription

6. Wire

  • Identifiers (<tenant-id>, <subscription-id>, resource IDs) live in the IaC repo's envs/*.tfvars and backend config — real values there, placeholders everywhere else.
  • Secrets never in the shell profile or repo: manage-secrets. Locally, az login beats any stored token — no secret to leak.
  • IaC changes go through set-up-terraform-pipeline, with OIDC per environment instead of local credentials.
  • Tools and versions: cli-toolbox, stack-versions.

7. Verify

az account show --query id -o tsv | grep -q <subscription-id> && echo right-subscription
az resource show --ids <resource-id> --query tags -o json    # tags name the owning system
az graph query -q "Resources | where name == '<name>' | project name, type" -o table   # exactly one row
az role assignment list --assignee <object-id> --scope <scope> -o table                # the grant exists
az storage account network-rule list --account-name <account> -o table                 # no leftover client-IP rule

Expected: right-subscription, one row for the resource, the new role listed, and no temporary IP rule left behind.

References