promptus
Azure · Terraform · Bicep · FortiGate · Git · DevOps
Workflows by task
All topics (51)
Start
Azure
Network and Firewall
Code and DevOps
Clients and Logs
Look up
#I want to …
#Whole tasks
Step by step, one page per area: Copy-Paste Workflows.
#Azure General
| … | Command | Details |
|---|---|---|
| switch the subscription | az account set --subscription <x> | Sign-in and Subscription |
| find a resource without knowing where | az graph query -q "Resources | where name contains '<x>'" | Resource Graph |
| see who changed what | az monitor activity-log list -g <rg> --offset 2h | Monitoring and Diagnostic Settings |
| install a PowerShell module when the profile is blocked | as admin: Install-Module <module> -Scope AllUsers -Repository PSGallery -Force | Install Modules Without the User Profile |
| run a command in a VM | az vm run-command invoke … --scripts "<cmd>" | Virtual Machines |
| stop VM costs | az vm deallocate -g <rg> -n <vm> | Virtual Machines |
| check whether a SKU works in a zone | az vm list-skus -l <region> --size <sku> | Virtual Machines |
| accept Marketplace terms | az vm image terms accept --urn <urn> | Marketplace Images |
| see a web app's live log | az webapp log tail -g <rg> -n <app> | App Service / Functions |
| deploy a web app (zip) | az webapp deploy -g <rg> -n <app> --src-path app.zip --type zip | Deploy Code |
| swap staging into production | az webapp deployment slot swap … --slot staging --target-slot production | Deployment Slots |
| use a Key Vault secret in an app setting | @Microsoft.KeyVault(SecretUri=…) | Settings, Secrets and Key Vault References |
| allow only Front Door to reach a web app | az webapp config access-restriction add … --service-tag AzureFrontDoor.Backend | Network: VNet Integration, Private Endpoint, Access Restrictions |
| back up VMs (vault + policy + VM list) | Generator → Backup → Protect VMs | Create a Vault and Protect a VM |
| restore a VM, its disks or single files | Generator → Backup → Restore a VM | Restore |
| connect to a VM via Bastion (RDP / SSH / tunnel) | Generator → Virtual Machines → Bastion | Virtual Machines |
| set or temporarily remove a delete lock | Generator → Governance → Delete lock | Resource Groups, Resources, Locks |
| tag resource groups and all their resources | Generator → Governance → Tag resource groups | Tag Standard |
| find orphaned disks, NICs, public IPs | Generator → Costs and Cleanup → Find orphaned … | Cleanup and Costs |
| check in the firewall log whether traffic was allowed | Generator → Logs and Monitoring → Firewall logs | Log Analytics (KQL) |
| connect kubectl to AKS | az aks get-credentials -g <rg> -n <aks> | Connect (kubectl) |
| see why a pod does not start | kubectl describe pod <pod> -n <ns> | kubectl in Daily Work |
| upgrade AKS | az aks get-upgrades → az aks upgrade | Upgrade |
#Network
| … | Command | Details |
|---|---|---|
| know which route applies | az network nic show-effective-route-table -g <rg> -n <nic> | Effective Routes and Connectivity Tests |
| check the next hop | az network watcher show-next-hop … | Effective Routes and Connectivity Tests |
| check peering status | az network vnet peering list -g <rg> --vnet-name <vnet> | Peering |
| point a 0/0 route to the firewall | az network route-table route create … --next-hop-type VirtualAppliance | Route Tables (UDR) |
| see NSG rules incl. defaults | az network nsg rule list … --include-default | NSG |
| see VPN/BGP status | az network vnet-gateway list-bgp-peer-status | VPN Gateway (Site-to-Site) |
| add a rule to the Azure Firewall | az network firewall policy rule-collection-group collection rule add … | Azure Firewall |
| link a private DNS zone to a VNet | az network private-dns link vnet create … | Private DNS and Private Endpoints |
| check App Gateway backends | az network application-gateway show-backend-health | Application Gateway and WAF |
| set the WAF to Prevention | az network application-gateway waf-policy policy-setting update --mode Prevention | Application Gateway and WAF |
| enable IP forwarding on an NVA NIC | az network nic update --ip-forwarding true | Virtual Machines |
| look up a subnet size | table | Subnet Sizes |
| give subnets a fixed outbound IP (NAT gateway) | Generator → Network → NAT gateway | NAT Gateway |
| enable DDoS protection (plan or per IP) | Generator → Network → DDoS protection | DDoS Protection |
| see range, usable IPs in Azure and mask of a subnet | button IP Planner at the top (key s) → Look up a network | Subnet Sizes |
| lay out a hub VNet (Gateway, Firewall, Bastion, Route Server subnets) | button IP Planner (key s) → Hub VNet layout | Subnet Sizes |
| split a spoke address space into subnets, each with its own size (with CLI / PowerShell / Bicep / Terraform) | button IP Planner (key s) → Spoke plan & commands | Subnet Sizes |
| check overlaps with on-premises ranges | button IP Planner (key s) → Overlap check | Subnet Sizes |
| create privatelink DNS zones for a landing zone | Generator → Network → Private DNS zones for a landing zone | Private DNS and Private Endpoints |
| troubleshoot systematically | DNS → route → NSG → firewall → return path → target | Order of Checks When "It Doesn't Work" |
#Permissions and Secrets
| … | Command | Details |
|---|---|---|
| see which roles someone has | az role assignment list --assignee <x> --all -o table | Roles (RBAC) |
| give a service principal Owner | az role assignment create --assignee-principal-type ServicePrincipal --assignee-object-id $id --scope <scope> --role Owner | Give a service principal a role (subscription, management group, root) |
| remove the rights of a deployment service principal (MGs, root, subscriptions) | Generator → Identity and Access → Remove role assignments | Give a service principal a role (subscription, management group, root) |
| my object ID | az ad signed-in-user show --query id -o tsv | Roles (RBAC) |
| read a secret | az keyvault secret show --vault-name <kv> -n <name> --query value -o tsv | Key Vault |
| recover a deleted secret | az keyvault secret recover … | Key Vault |
| publish an app via Front Door | Generator → Front Door → Publish an app | Azure Front Door |
| assign a domain to an existing Front Door route | az afd route update --custom-domains <all-ids> | Create a Route or Assign to an Existing Route |
| query across all subscriptions | az graph query -q "<kql>" | Resource Graph – Queries Across All Subscriptions |
| find orphaned disks/IPs | Resource Graph → cleanup | Cleanup and Costs |
| create security groups per role in bulk | New-AzADGroup -DisplayName <name> -MailNickname <name> -SecurityEnabled | Create Security Groups in Bulk |
| find expiring app secrets | az ad app credential list | Find Expiring Secrets and Certificates |
| build resource names after our convention | Generator → Naming → Build resource names | Naming Components (CAF) |
| look up a resource abbreviation | Generator → Naming → Abbreviation lookup | Azure Resource Abbreviations (CAF) |
| create an alert | az monitor metrics alert create | Metric Alerts |
| rename management groups (search & replace) | Generator → Governance → Rename management groups | Rename Management Groups |
| find out which policy blocks | az policy assignment list --disable-scope-strict-match … | Troubleshooting |
| assign a policy in test mode first | az policy assignment create … --enforcement-mode DoNotEnforce | Assign a Policy |
| assign a policy with a form | Generator → Governance → Assign an Azure Policy | Assign a Policy |
| fix existing resources with a policy (tags, settings) | az policy remediation create --policy-assignment <id> | Modify and DeployIfNotExists: Identity and Remediation |
| exempt a resource group from a policy | az policy exemption create … --expires-on <date> | Exemptions |
| see non-compliant resources across subscriptions | Resource Graph policyresources | Compliance and Reports |
| manage policies as code | repo + pipeline + what-if | Policy as Code: Repo and Pipeline |
#Terraform / Bicep
| … | Command | Details |
|---|---|---|
| plan and apply safely | terraform plan -out tfplan → terraform apply tfplan | Basic Workflow |
| rename a resource without rebuild | moved { from = … to = … } | Code Snippets |
| take over an existing resource | import { to = … id = … } | Code Snippets |
| see drift only | terraform plan -refresh-only | Basic Workflow |
| release a lock | terraform force-unlock <id> | Outputs and State |
| calculate subnets | cidrsubnets("10.0.0.0/24", 2, 2, 2, 2) | Useful Functions |
| test Bicep without risk | az deployment group what-if … | Bicep / ARM |
| a pipeline for Terraform | template | Pipeline Template: Terraform Plan/Apply |
#FortiGate
| … | Command | Details |
|---|---|---|
| check version/license | get system status | System and License |
| capture packets | diagnose sniffer packet any 'host <ip>' 4 20 | Troubleshooting: Sniffer, Debug Flow, Sessions |
| know why a packet is dropped | debug flow | Troubleshooting: Sniffer, Debug Flow, Sessions |
| understand a debug flow message | table | Debug Flow Messages |
| check the route for an IP | get router info routing-table details <ip> | Routing |
| check HA sync | diagnose sys ha checksum cluster | HA (FGCP) |
| test failover | execute ha failover set 1 | HA (FGCP) |
| check an IPsec tunnel | get vpn ipsec tunnel summary | IPsec VPN |
| see BGP neighbors | get router info bgp summary | BGP |
| set up port forwarding | config firewall vip | VIP (Port Forwarding) |
| search the configuration | show full-configuration | grep -f <text> | Using the CLI |
#Clients (Linux / Windows)
| … | Linux | Windows | Details |
|---|---|---|---|
| test a port | nc -zv <ip> <port> | Test-NetConnection <ip> -Port <port> | Network / Network |
| check DNS | dig +short <name> | Resolve-DnsName <name> | DNS / DNS |
| see the public IP | curl -s ifconfig.me | Invoke-RestMethod https://api.ipify.org | Network / Network |
| path to a target | ip r get <target> | Find-NetRoute -RemoteIPAddress <target> | Network / Network |
| listening ports | ss -tlnp | Get-NetTCPConnection -State Listen | Network / Network |
| continuous failover test | while true; do curl …; done | while ($true) { … } | Mini Tests / Continuous Tests |
| check a server's certificate | openssl s_client -connect <host>:443 … | – | Certificates with OpenSSL |
#Git / SSH
| … | Command | Details |
|---|---|---|
| push a folder as a repo | git init -b main … git push -u origin main | New Repo / Push a Folder |
| undo the last commit | git reset --soft HEAD~1 / git revert <commit> | Undo |
| park changes briefly | git stash / git stash pop | Undo |
| search the history | git log -S "<text>" | Search and History |
| get an existing DevOps repo | git clone git@ssh.dev.azure.com:v3/<org>/<project>/<repo> | Clone an existing DevOps repo |
| push is rejected (TF402455) | git switch -c feature/<name> → git push -u origin feature/<name> → PR | Push rejected: TF402455 (protected branch) |
| create a pull request | az repos pr create --source-branch <from> --target-branch <to> --open | Change via feature branch and pull request to Dev |
| resolve a merge conflict in a PR | git merge origin/Dev → fix files → git commit → git push | Resolve a merge conflict in a pull request |
| undo a PR | abandon (open) / revert (completed) | Undo a pull request (abandon / revert) |
| publish a static site from DevOps (Static Web App) | pipeline AzureStaticWebApp@0 + SWA_TOKEN | Publish a static website via pipeline (Static Web App) |
| bring Dev to main | PR Dev → main, "Delete Dev after merging" off | Promote Dev to main (release) |
| SSH key for Azure DevOps | ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_azure | SSH key for Azure DevOps and first push |
| find out why Azure DevOps asks for a password | ssh -vT git@ssh.dev.azure.com | Azure DevOps: test and debug |
| push although the server is ahead (non-fast-forward) | git pull --rebase origin main | Push rejected: non-fast-forward |
| test the connection to Azure DevOps | ssh -T git@ssh.dev.azure.com | Connect and Test |
#Search Script
Searches all files under docs/ and shows file, line and command. Several terms = all must appear.
.\search.ps1 peering # Windows.\search.ps1 sniffer 168.63 # both terms in one line./search.sh peering # Linux / Git Bash#Rebuild the HTML View
After changes to the .md files (Python 3, no extra packages):
python tools/build-html.pyLook and features live in tools/template.html. Produces site/index.html – a single file that runs offline in the browser (fonts are loaded online, otherwise system font).
#Quick Access
| Way | How |
|---|---|
| Install as app | open promptus.cloudorbis.at in Edge/Chrome → Install button in the header (or ⋯ → Apps → Install) – own window, taskbar/start menu icon, works offline |
| Search from the address bar | Edge: Settings → Privacy, search → Address bar and search → Manage search engines → Add: name promptus, shortcut promptus, URL https://promptus.cloudorbis.at/?q=%s (Chrome: Settings → Search engine → Manage → Site search → Add). Then type promptus peering sync |
| Search link | https://promptus.cloudorbis.at/?q=<term> – opens the page with the search, good for sharing |
| PowerShell | function promptus in your profile (Daily Commands): promptus peering sync |
| Jump inside the page | Ctrl+K – topic, section, workflow or generator template by name |
| Teams | channel → + → Website → https://promptus.cloudorbis.at |
#Publish as Azure Static Web App (promptus.cloudorbis.at)
The build writes site/index.html; site/ also contains staticwebapp.config.json (security headers). Free plan is enough: custom domain + free certificate included.
Create the static web app (once)
$rg = "<rg>"; $swa = "<swa-name>"; $loc = "westeurope"az group create -n $rg -l $locaz staticwebapp create -n $swa -g $rg -l $loc --sku Freeaz staticwebapp show -n $swa -g $rg --query defaultHostName -o tsv # e.g. <name>.<id>.azurestaticapps.netDeploy from your PC (needs Node.js)
python tools/build-html.py$token = az staticwebapp secrets list -n $swa -g $rg --query "properties.apiKey" -o tsvnpx --yes @azure/static-web-apps-cli deploy ./site --deployment-token $token --env productionOr automatically on every push to main: pipeline pipelines/static-web-app.yml (Azure DevOps) with the secret pipeline variable SWA_TOKEN = the token above.
The deployment token is not bound to a tenant: the DevOps repo can live in a different organization/tenant than the subscription of the static web app – no service connection needed. Create the app with az login --tenant <target-tenant-id> first and create it without a source (CLI default), not via the portal's "Azure DevOps" option. Set up the pipeline: Pipelines → New pipeline → Azure Repos Git → repo → Existing Azure Pipelines YAML file → /pipelines/static-web-app.yml → Variables → SWA_TOKEN (Keep this value secret) and optionally BLOCKED_WORDS (secret, comma separated – see Contributing) → Save and run. New token if leaked: az staticwebapp secrets reset-api-key -n $swa -g $rg.
Custom domain At the DNS provider of cloudorbis.at: CNAME promptus -> <defaultHostName> (from step 1). Then:
az staticwebapp hostname set -n $swa -g $rg --hostname promptus.cloudorbis.ataz staticwebapp hostname show -n $swa -g $rg --hostname promptus.cloudorbis.at --query status -o tsv # Ready = certificate issuedOptional: only your team may open it Add to site/staticwebapp.config.json:
"routes": [ { "route": "/*", "allowedRoles": [ "team" ] } ],"responseOverrides": { "401": { "redirect": "/.auth/login/aad", "statusCode": 302 } }Then invite people: Portal → Static Web App → Role management → Invite → provider Microsoft Entra ID, role team (Free plan: up to 25 invitations). Restricting to your own tenant without invitations needs the Standard plan (custom Entra ID provider).
Placeholders and favorites are stored per browser (localStorage) – nothing is sent to a server. The content contains only placeholders <...>, no customer data.
#Contributing / Maintenance
- Put new commands into the matching file under
docs/, comment with#(or//in KQL) after it – that way the search script finds them. - Add frequently used tasks to "I want to …" above or to Daily Commands.
- Then run
python tools/build-html.pyand commitsite/index.htmltoo (or let the pipeline build it). - Never check in real IPs, names, keys, tokens or licenses – placeholders
<...>only. - Before pushing:
python tools/check.py– checks links/anchors, blocked words and anything that looks like a secret (private keys, client secrets, storage keys, SAS, tokens, GUIDs). The pipeline runs the same check and stops before deploying if it finds something. - Smoke test:
python tools/smoke-test.py(needspip install playwright) opens the built page in a headless browser and checks start page, layout (desktop and phone), search, every topic, Ctrl+K, placeholders and every generator template. The pipeline runs it after the build – if it fails, nothing is deployed. - Blocked words (customer, org and subscription names) are never stored in the repo: in the pipeline as secret variable
BLOCKED_WORDS(comma separated), locally intools/blocked-words.txt(one per line, ignored by git). A harmless line (e.g. a built-in role ID) can be excluded from the secret check with the commentcheck:allow.
#All Workflows
Ready-made command sequences for whole tasks, sorted by area. Click a task – the workflow opens on its page. Do not write real names, secrets or IDs back into this repo. Single commands live in the topic files.
#Daily Commands
#Azure CLI
The most frequent Azure CLI commands.
| Task | Command |
|---|---|
| Who am I, where am I? | az account show --query "{user:user.name, sub:name}" -o table |
| Switch subscription | az account set --subscription "<name>" |
| List subscriptions | az account list --query "[].{name:name, id:id, active:isDefault}" -o table |
Set default RG/region (saves -g/-l) | az config set defaults.group=<rg> defaults.location=<region> |
| Remove defaults again | az config unset defaults.group defaults.location |
| Help for a command | az network vnet peering create --help |
| Find examples | az find "vnet peering" |
| Interactive shell with autocomplete | az interactive |
| Resources in an RG | az resource list -g <rg> --query "[].{name:name, type:type}" -o table |
| Running VMs | az vm list -d --query "[?powerState=='VM running'].{name:name, rg:resourceGroup}" -o table |
| VM IPs | az vm list-ip-addresses -g <rg> -o table |
| Public IPs | az network public-ip list --query "[].{name:name, ip:ipAddress}" -o table |
| Effective routes | az network nic show-effective-route-table -g <rg> -n <nic> -o table |
| Recent changes | az monitor activity-log list -g <rg> --offset 4h --query "[].{t:eventTimestamp, who:caller, what:operationName.localizedValue}" -o table |
| Read a secret | az keyvault secret show --vault-name <kv> -n <name> --query value -o tsv |
| Get a resource ID | az <type> show -g <rg> -n <name> --query id -o tsv |
| JSON to PowerShell object | az vm list -o json | ConvertFrom-Json |
| CLI version / update | az version · az upgrade |
#Azure PowerShell
The most frequent Az PowerShell commands.
| Task | Command |
|---|---|
| Sign in | Connect-AzAccount · Connect-AzAccount -UseDeviceAuthentication |
| Context | Get-AzContext |
| Switch subscription | Set-AzContext -Subscription "<name>" |
| Pick a subscription in a window | Get-AzSubscription | Out-GridView -PassThru | Set-AzContext |
| Find resources | Get-AzResource -Name "*<part>*" | ft Name, ResourceGroupName, ResourceType |
| VMs with status | Get-AzVM -Status | ft Name, ResourceGroupName, PowerState |
| Load App Gateway | $AppGW = Get-AzApplicationGateway -Name <agw> -ResourceGroupName <rg> |
| Secret as plain text | Get-AzKeyVaultSecret -VaultName <kv> -Name <name> -AsPlainText |
| Update module (admin window) | Update-Module Az -Scope AllUsers |
#Terraform
The Terraform commands of a normal day.
| Task | Command |
|---|---|
| Start in a new folder | terraform init |
| Format + check | terraform fmt -recursive ; terraform validate |
| Plan (saved) | terraform plan -out tfplan |
| Apply exactly this plan | terraform apply tfplan |
| Show only deletions/replacements in the plan | terraform plan -no-color | Select-String "destroy|replace" |
| Find resources in the state | terraform state list | Select-String <part> |
| Show one resource | terraform state show '<address>' |
| Output for scripts | terraform output -raw <name> |
| Test an expression | terraform console |
| Release a lock | terraform force-unlock <id> |
| Update providers | terraform init -upgrade |
#Git
The Git commands of a normal day.
| Task | Command |
|---|---|
| What's going on? | git status -s |
| Get the latest | git pull |
| Commit everything | git add -A ; git commit -m "<message>" |
| Upload | git push |
| Recent commits | git log --oneline --graph -15 |
| What did I change? | git diff · git diff --staged |
| New branch | git switch -c feature/<name> |
| Back to main | git switch main ; git pull |
| Reset a file | git restore <file> |
| Park changes | git stash -u · git stash pop |
| Change the last commit message | git commit --amend -m "<new>" (only before pushing) |
| Which branches? | git branch -a |
| Clean up deleted remote branches | git fetch --prune |
| PR to Dev | az repos pr create --source-branch feature/<name> --target-branch Dev --open |
| What goes from Dev to main? | git log --oneline origin/main..origin/Dev |
| Local branch = server | git fetch origin ; git reset --hard origin/<branch> |
#FortiGate
The FortiGate commands you need most.
| Task | Command |
|---|---|
| Status / version / license | get system status |
| CPU / RAM / sessions | get system performance status |
| HA status | get system ha status |
| HA in sync? | diagnose sys ha checksum cluster |
| Routing table | get router info routing-table all |
| Route for an IP | get router info routing-table details <ip> |
| Interfaces, short | get system interface physical |
| ARP | get system arp |
| Search the config | show full-configuration | grep -f <text> |
| Show a policy | show firewall policy <id> |
| Capture packets | diagnose sniffer packet any 'host <ip>' 4 20 |
| Sessions to an IP | diagnose sys session filter dst <ip> → diagnose sys session list |
| Ping with source IP | execute ping-options source <ip> → execute ping <target> |
| Test a port | execute telnet <ip> <port> |
| VPN overview | get vpn ipsec tunnel summary |
| BGP overview | get router info bgp summary |
| Turn all debug off | diagnose debug disable ; diagnose debug reset |
#Network Tests
The same network test on Windows, Linux and macOS.
| Task | Windows (PowerShell) | Linux | macOS |
|---|---|---|---|
| Own IPs | ipconfig | ip -br a | ifconfig · ipconfig getifaddr en0 |
| Public IP | irm https://api.ipify.org | curl -s ifconfig.me | curl -s ifconfig.me |
| DNS | Resolve-DnsName <name> | dig +short <name> | dig +short <name> |
| Flush DNS cache | ipconfig /flushdns | resolvectl flush-caches | sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder |
| Port | Test-NetConnection <ip> -Port 443 | nc -zv <ip> 443 | nc -zv <ip> 443 |
| HTTP(S) with details | curl.exe -v https://<target> | curl -v https://<target> | curl -v https://<target> |
| HTTP status only | curl.exe -s -o NUL -w "%{http_code}" https://<target> | curl -s -o /dev/null -w "%{http_code}\n" https://<target> | curl -s -o /dev/null -w "%{http_code}\n" https://<target> |
| Test against a specific IP | curl.exe -k --resolve <host>:443:<ip> https://<host> | curl -k --resolve <host>:443:<ip> https://<host> | curl -k --resolve <host>:443:<ip> https://<host> |
| Path | tracert -d <ip> | tracepath -n <ip> | traceroute -n <ip> |
| Routes | route print -4 | ip r | netstat -rn -f inet |
| Listening ports | netstat -ano | findstr LISTEN | ss -tlnp | lsof -nP -iTCP -sTCP:LISTEN |
#PowerShell Tricks
Small PowerShell helpers: history, clipboard, export, measure.
Get-Command *route* # find a commandGet-Help Test-NetConnection -Examples # examplesGet-History ; Invoke-History <nr> # history(Get-PSReadLineOption).HistorySavePath # file with the full history (old sessions too)Select-String -Path (Get-PSReadLineOption).HistorySavePath -Pattern "<text>" # search the history<command> | Set-Clipboard # output to the clipboard<command> | Out-GridView # table with filter (Windows)<command> | Export-Csv .\out.csv -NoTypeInformation -Encoding UTF8<command> | ConvertTo-Json -Depth 5 | Set-Content .\out.json<command> | Select-Object -First 5<command> | Where-Object Name -like "*<part>*"<command> | Format-Table -AutoSizeMeasure-Command { <command> } # measure runtimeStart-Transcript .\session.log ; Stop-Transcript # record the whole sessioncode . # open the current folder in VS Codeexplorer . # open in Explorerwinget upgrade --all # update all tools (az, terraform, git …)#Shell Profile: Your Own Shortcuts
Windows: open with notepad $PROFILE (create the file first if needed: New-Item -Force $PROFILE). macOS (zsh): nano ~/.zshrc. Linux (bash): nano ~/.bashrc. Paste the snippets, then start a new console (or source ~/.zshrc).
# --- Azure ---function azsub { # pick a subscription in a window for CLI and PowerShell $s = az account list --query "[].{Name:name, Id:id}" -o json | ConvertFrom-Json | Out-GridView -Title "Choose subscription" -OutputMode Single if ($s) { az account set --subscription $s.Id; Set-AzContext -Subscription $s.Id | Out-Null; "Active: $($s.Name)" }}function azwho { az account show --query "{user:user.name, sub:name, tenant:tenantId}" -o table } # --- Terraform ---Set-Alias tf terraformfunction tfi { terraform init @args }function tfp { terraform plan -out tfplan @args }function tfa { terraform apply tfplan }function tfs { param($part) terraform state list | Select-String $part } # --- Git ---function gst { git status -s }function glg { git log --oneline --graph --decorate -15 }function gpush { param([string]$msg) git add -A; git commit -m $msg; git push } # --- Network ---function myip { Invoke-RestMethod https://api.ipify.org }function port { param($target, [int]$p) Test-NetConnection $target -Port $p -WarningAction SilentlyContinue | Select ComputerName, RemoteAddress, RemotePort, TcpTestSucceeded }function dns { param($name) Resolve-DnsName $name -DnsOnly | Select Name, Type, IPAddress, NameHost } # --- promptus ---function cmds { & "<path-to-repo>\search.ps1" @args } # search the local repofunction promptus { # open promptus, optionally with a search $u = "https://promptus.cloudorbis.at/" if ($args) { $u += "?q=" + [uri]::EscapeDataString($args -join " ") } Start-Process $u}# --- Azure ---azsub() { # pick a subscription from a numbered list local IFS=$'\n'; local subs=($(az account list --query "[].name" -o tsv)) select s in "${subs[@]}"; do [ -n "$s" ] && az account set --subscription "$s" && echo "Active: $s"; break; done}alias azwho='az account show --query "{user:user.name, sub:name, tenant:tenantId}" -o table' # --- Terraform ---alias tf=terraformalias tfi='terraform init'alias tfp='terraform plan -out tfplan'alias tfa='terraform apply tfplan'tfs() { terraform state list | grep -i "$1"; } # --- Git ---alias gst='git status -s'alias glg='git log --oneline --graph --decorate -15'gpush() { git add -A && git commit -m "$1" && git push; } # --- Network ---alias myip='curl -s https://api.ipify.org'port() { nc -zv "$1" "$2"; }dns() { dig +short "$1"; } # --- promptus ---cmds() { "<path-to-repo>/search.sh" "$@"; } # search the local repopromptus() { # open promptus, optionally with a search local u="https://promptus.cloudorbis.at/" [ $# -gt 0 ] && u+="?q=$(python3 -c 'import sys,urllib.parse as p; print(p.quote(" ".join(sys.argv[1:])))' "$@")" open "$u" 2>/dev/null || xdg-open "$u" # macOS: open, Linux: xdg-open}Then e.g.: azsub, tfp, port 10.0.1.4 443, cmds peering, promptus peering sync. Note: gl, gp, gc are already taken in PowerShell (Get-Location etc.) – hence the other names.
#Keyboard Shortcuts
Shortcuts for PowerShell, Windows Terminal, VS Code, FortiGate and the browser.
| Where | Key | Effect |
|---|---|---|
| PowerShell | Ctrl+R | search the history backwards |
| PowerShell | F8 | search the history for what you already typed |
| PowerShell | Ctrl+Space | show all options (parameters, values) |
| PowerShell | Tab / Shift+Tab | complete forward/backward |
| PowerShell | Ctrl+C | cancel the running command |
| PowerShell | ` at end of line | continue the command on the next line |
| Windows Terminal | Alt+Shift+D | split pane (same profile) |
| Windows Terminal | Ctrl+Shift+T / Ctrl+Shift+W | new / close tab |
| Windows Terminal | Ctrl+Shift+F | search in the window |
| VS Code | Ctrl+P | open a file |
| VS Code | Ctrl+Shift+F | search all files |
| VS Code | Ctrl+Shift+P | command palette |
| VS Code | Ctrl+` | toggle terminal |
| VS Code | Shift+Alt+F | format the file |
| VS Code | Ctrl+Shift+V | Markdown preview |
| Linux / Git Bash | Ctrl+R | search the history |
| Linux / Git Bash | sudo !! | repeat the last command with sudo |
| FortiGate | Tab / ? | complete / help |
| FortiGate | Ctrl+C | cancel output/debug |
| Browser (GitHub/DevOps) | t | find a file in the repo (GitHub) |
#Runbook: Outage "Something Is Not Reachable"
#Minute 0–2: Clarify the Situation
Questions for the reporter (or answer them yourself):
| Question | Why |
|---|---|
| What exactly doesn't work? URL, IP, port, error message, screenshot | define the target |
| Who is affected? Everyone, one site, only internal/external? | narrow down the fault |
| Since when? Was there a change/deployment before? | most common cause |
| Does it work from somewhere else? (other client, VPN, phone) | client vs. server |
Immediately: send an outage notice to those involved (template).
#Minute 2–5: Is Azure Itself Affected?
Recent changes and the state of the most important resources.
# Recent changes in the affected environmentaz monitor activity-log list -g <rg> --offset 4h ` --query "[?status.value=='Succeeded'].{time:eventTimestamp, who:caller, what:operationName.localizedValue}" -o table # State of the most important resourcesaz vm list -d -g <rg> --query "[].{name:name, state:powerState}" -o tableaz network application-gateway show-backend-health -g <rg> -n <agw> ` --query "backendAddressPools[].backendHttpSettingsCollection[].servers[].{server:address, health:health}" -o table- Azure status: https://azure.status.microsoft – and in the portal Service Health (shows outages for your subscriptions).
- Merged PR / pipeline in the last hours? → DevOps Pipelines → Runs.
#Minute 5–10: Check the Path
Always in this order: DNS → route → NSG → firewall → return path → target.
# 1. DNSResolve-DnsName <name> # right IP? private or public? # 2. Port from the clientTest-NetConnection <target> -Port <port> # 3. Route from Azure's point of viewaz network watcher show-next-hop -g <rg> --vm <source-vm> --source-ip <source-ip> --dest-ip <target-ip> -o tableaz network nic show-effective-route-table -g <rg> -n <nic> -o table # 4. NSGaz network watcher test-ip-flow -g <rg> --vm <vm> --direction Inbound --protocol TCP --local <vm-ip>:<port> --remote <source>:12345Tip: macOS / Linux: step 1 dig +short <name>, step 2 nc -zv <target> <port> (the az commands work the same).
FortiGate:
get system status # running, license ok?get system ha status # did a failover happen?diagnose sniffer packet any 'host <target-ip>' 4 20 # does traffic arrive and leave?# if it doesn't leave -> debug flow (why dropped):diagnose debug flow filter addr <target-ip>diagnose debug flow trace start 10diagnose debug enablediagnose debug disable ; diagnose debug resetThe target itself:
ss -tlnp | grep <port> # is the service listening?sudo systemctl status <service>sudo journalctl -u <service> -n 50Details on messages: Error → Fix · Debug flow messages.
#Once the Cause Is Found
Fix, document, confirm with the reporter.
#After the Outage
Timeline, cause, missing alerts.
- Timeline from the notes (start, detection, cause, fix)
- Document cause and action briefly
- Missing alert? → add it in Monitoring
- Add the new insight to this reference (Error → Fix)
#Sign-in and Basics Workflows
Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.
#Sign in to Azure
Sign in with browser, device code, service principal or managed identity – for CLI and PowerShell.
Interactive (browser)
az loginaz login --tenant <tenant-id> # with several tenantsConnect-AzAccountConnect-AzAccount -Tenant <tenant-id>Device code – when no browser is available (server, RDP, jump host). Enter the code at https://microsoft.com/devicelogin.
az login --use-device-codeaz login --use-device-code --tenant <tenant-id>az login --use-device-code --allow-no-subscriptions # tenant without subscription (e.g. Entra ID only)Connect-AzAccount -UseDeviceAuthenticationConnect-AzAccount -UseDeviceAuthentication -Tenant <tenant-id>Service principal (app registration with secret)
az login --service-principal ` --username <app-id> ` --password <client-secret> ` --tenant <tenant-id> # better: keep the secret out of the history – prompt instead of typing it$secret = Read-Host "Client secret" -AsSecureString$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret))az login --service-principal --username <app-id> --password $plain --tenant <tenant-id>Remove-Variable plain # PowerShell (Az module)$cred = Get-Credential -UserName <app-id> -Message "Client secret as password"Connect-AzAccount -ServicePrincipal -Credential $cred -Tenant <tenant-id>Service principal with certificate
az login --service-principal --username <app-id> --password .\sp-cert.pem --tenant <tenant-id> # PEM incl. private key (newer CLI: --certificate)Connect-AzAccount -ServicePrincipal -ApplicationId <app-id> -CertificateThumbprint <thumbprint> -Tenant <tenant-id>Managed identity (on an Azure VM or agent)
az login --identityaz login --identity --username <client-id> # user-assigned identity (newer CLI: --client-id)Connect-AzAccount -IdentityConnect-AzAccount -Identity -AccountId <client-id>Check and sign out
az account show -o tableGet-AzContextaz logout ; az account clearDisconnect-AzAccount ; Clear-AzContext -Force#Set subscription (CLI and PowerShell)
CLI and PowerShell have separate sign-ins – set both if you use both.
$sub = "<subscription-name-or-id>"az account set --subscription $subSet-AzContext -Subscription $subaz account show --query "{name:name, id:id}" -o table(Get-AzContext).Subscription.Name#Unblock and run downloaded scripts
Remove the download mark and allow scripts for the current session only.
Get-ChildItem -Recurse *.ps1 | Unblock-File # remove the download markSet-ExecutionPolicy -Scope Process Bypass # this session only.\<script>.ps1xattr -dr com.apple.quarantine <folder> # macOS: remove the download mark (Linux has none)chmod +x <script>.sh # make it executable./<script>.sh # PowerShell script: pwsh ./<script>.ps1Tip: Windows permanently for the user: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned. On macOS/Linux PowerShell does not enforce an execution policy.
#Find a resource without knowing where it is
Search all subscriptions by name part, then switch to the right one.
az extension add -n resource-graph$part = "<name-part>"az graph query -q "Resources | where name contains '$part' | project name, type, resourceGroup, subscriptionId" -o table# then switch to the right subscriptionaz account set --subscription <subscription-id-from-result>az resource show --ids <resource-id>#Who changed or deleted what
Read the activity log of a resource group – all changes or deletions only.
$rg = "<rg>"az monitor activity-log list -g $rg --offset 24h ` --query "[?status.value=='Succeeded'].{time:eventTimestamp, who:caller, what:operationName.localizedValue, resource:resourceId}" -o table# deletions onlyaz monitor activity-log list -g $rg --offset 7d ` --query "[?contains(operationName.value,'delete')].{time:eventTimestamp, who:caller, resource:resourceId}" -o tableLonger than 90 days or across all resource groups: KQL → Azure activity.
#Create a resource group with tags and delete lock
Create a resource group with the tag standard and protect it with a delete lock.
$rg = "<rg>" ; $loc = "<region>"az group create -n $rg -l $loc --tags Owner=<name> environment=<dev|test|prod> costcenter=<cost-center>az lock create -n lock-$rg -g $rg --lock-type CanNotDelete --notes "Protection against accidental deletion"az group show -n $rg --query tagsaz lock list -g $rg -o table#Application Gateway and WAF Workflows
Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.
#App Gateway: add a certificate from Key Vault
Give the gateway an identity, add the Key Vault certificate versionless and assign it to a listener.
Prerequisite: the App Gateway has a user-assigned identity with the role Key Vault Secrets User on the vault (or access policy Get on secrets), and the vault is reachable for it.
1. No identity yet? Assign one first
$id = Get-AzUserAssignedIdentity -ResourceGroupName <rg> -Name <identity>$AppGW = Get-AzApplicationGateway -Name <agw-name> -ResourceGroupName <rg-agw>Set-AzApplicationGatewayIdentity -ApplicationGateway $AppGW -UserAssignedIdentityId $id.IdSet-AzApplicationGateway -ApplicationGateway $AppGWNew-AzRoleAssignment -ObjectId $id.PrincipalId -RoleDefinitionName "Key Vault Secrets User" ` -Scope (Get-AzKeyVault -VaultName <keyvault-name>).ResourceIdAdd the certificate to the App Gateway (takes a few minutes)
$sub = "<subscription>"$rg = "<rg-agw>"$agwName = "<agw-name>"$kvName = "<keyvault-name>"$certName = "<certificate-name>" # name in Key Vault and in the App Gateway Set-AzContext -Subscription $sub $AppGW = Get-AzApplicationGateway -Name $agwName -ResourceGroupName $rg$secret = Get-AzKeyVaultSecret -VaultName $kvName -Name $certName$secretId = $secret.Id.Replace($secret.Version, "") # versionless: https://<kv>.vault.azure.net/secrets/<name>/ $AppGW = Add-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name $certName -KeyVaultSecretId $secretIdSet-AzApplicationGateway -ApplicationGateway $AppGWaz account set --subscription <subscription>$secretId = "https://<keyvault-name>.vault.azure.net/secrets/<certificate-name>" # without versionaz network application-gateway ssl-cert create -g <rg-agw> --gateway-name <agw-name> ` -n <certificate-name> --key-vault-secret-id $secretIdaz network application-gateway ssl-cert list -g <rg-agw> --gateway-name <agw-name> ` --query "[].{name:name, kv:keyVaultSecretId}" -o tableTip: Versionless = the App Gateway picks up a renewed version automatically (checked about every 4 hours).
Assign the certificate to a listener
$AppGW = Get-AzApplicationGateway -Name <agw-name> -ResourceGroupName <rg-agw>$cert = Get-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <certificate-name>$listener = Get-AzApplicationGatewayHttpListener -ApplicationGateway $AppGW -Name <listener-name>$listener.SslCertificate = $certSet-AzApplicationGateway -ApplicationGateway $AppGW#App Gateway: renew / replace a certificate
List the certificates, switch to a new Key Vault secret or remove an unused one.
# Which certificates exist and where do they come from?$AppGW = Get-AzApplicationGateway -Name <agw-name> -ResourceGroupName <rg-agw>$AppGW.SslCertificates | Select Name, KeyVaultSecretId, ProvisioningState # Switch to a new Key Vault source$AppGW = Set-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <certificate-name> -KeyVaultSecretId <new-secret-id>Set-AzApplicationGateway -ApplicationGateway $AppGW # Remove a certificate that is no longer used (detach from all listeners first)$AppGW = Remove-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <old-name>Set-AzApplicationGateway -ApplicationGateway $AppGW#Import a PFX into Key Vault
Import a PFX with private key into Key Vault, ready for the App Gateway.
az keyvault certificate import --vault-name <keyvault-name> -n <certificate-name> -f .\cert.pfx --password <pfx-password>az keyvault certificate show --vault-name <keyvault-name> -n <certificate-name> --query "{expires:attributes.expires, sid:sid}"# PowerShell$pw = Read-Host "PFX password" -AsSecureStringImport-AzKeyVaultCertificate -VaultName <keyvault-name> -Name <certificate-name> -FilePath .\cert.pfx -Password $pwThen continue with add the certificate.
#App Gateway: backend unhealthy
Check backend health, DNS, probe and NSG from inside the gateway VNet.
$rg = "<rg-agw>" ; $agw = "<agw>"az network application-gateway show-backend-health -g $rg -n $agw ` --query "backendAddressPools[].backendHttpSettingsCollection[].servers[].{server:address, health:health, log:healthProbeLog}" -o tableaz network application-gateway http-settings list -g $rg --gateway-name $agw ` --query "[].{name:name, port:port, proto:protocol, host:hostName, pickHost:pickHostNameFromBackendAddress, probe:probe.id}" -o tableaz network application-gateway probe list -g $rg --gateway-name $agw ` --query "[].{name:name, proto:protocol, host:host, path:path, pickHost:pickHostNameFromBackendHttpSettings}" -o table# Does the backend resolve privately? (from a VM in the gateway VNet)nslookup <backend-fqdn>Interpreting messages: Troubleshooting → App Gateway.
#WAF: find a false positive and add an exclusion
Switch to detection, find the blocking rule in the logs, add an exclusion, switch back to prevention.
Temporarily only log (if production traffic is being blocked)
$rg = "<rg>" ; $policy = "<waf-policy>"az network application-gateway waf-policy policy-setting update -g $rg --policy-name $policy --mode Detection --state EnabledWhich rule, which field? (Log Analytics)
AzureDiagnostics| where Category == "ApplicationGatewayFirewallLog" and TimeGenerated > ago(1h)| where requestUri_s contains "<path>"| project TimeGenerated, ruleId_s, Message, details_message_s, details_data_s, requestUri_sExclusion for the affected field (e.g. form field "comment")
az network application-gateway waf-policy managed-rule exclusion add -g $rg --policy-name $policy ` --match-variable RequestArgNames --selector-match-operator Equals --selector commentaz network application-gateway waf-policy managed-rule exclusion list -g $rg --policy-name $policySwitch back to blocking
az network application-gateway waf-policy policy-setting update -g $rg --policy-name $policy --mode Prevention --state EnabledTip: More --match-variable values: RequestHeaderNames, RequestCookieNames, RequestArgValues … To disable a single rule instead: Portal → WAF policy → Managed rules.
#Send diagnostic logs to Log Analytics
Create a diagnostic setting for any resource and send logs to a workspace.
$resId = az network application-gateway show -g <rg> -n <agw> --query id -o tsv # any resource$lawId = az monitor log-analytics workspace show -g <rg-law> -n <law> --query id -o tsvaz monitor diagnostic-settings create -n diag-to-law --resource $resId --workspace $lawId ` --logs '[{"categoryGroup":"allLogs","enabled":true}]' --metrics '[{"category":"AllMetrics","enabled":true}]' ` --export-to-resource-specific trueaz monitor diagnostic-settings list --resource $resId -o table--export-to-resource-specific true = dedicated tables (e.g. AGWAccessLogs) instead of AzureDiagnostics. In PowerShell, pass JSON strings with '…' and escaped quotes if needed, or put them in a file (--logs '@logs.json').
#Permissions, Key Vault and Policy Workflows
Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.
#Give a service principal a role (subscription, management group, root)
Find the service principal, assign the role at the smallest scope, remove it again after the deployment.
Find the service principal and choose the scope – as small as possible, then assign:
$SPName = "<sp-display-name>"$id = az ad sp list --display-name $SPName --query "[].{id:id}" --output tsv$id # exactly ONE ID? With several SPs of the same name stop and look up by app ID:# $id = az ad sp show --id <app-id> --query id -o tsv # Choose the scope – as small as possible:$scope = "/subscriptions/<subscription-id>" # one subscription# $scope = "/subscriptions/<subscription-id>/resourceGroups/<rg>" # one resource group# $scope = "/providers/Microsoft.Management/managementGroups/<mg-id>" # management group# $scope = "/" # tenant root (everything!) az role assignment create --assignee-principal-type ServicePrincipal --assignee-object-id $id --scope $scope --role "Owner"az role assignment list --assignee $id --all --query "[].{role:roleDefinitionName, scope:scope}" -o tablePrefer Contributor over Owner (+ User Access Administrator only where the SP must assign roles). Remove again: az role assignment delete --assignee $id --role "Owner" --scope $scope.
Only for scope / (tenant root)
Caution: Scope / grants Owner on all subscriptions and management groups. Remove the rights again right after the deployment (step 3 below).
Elevate your own access on root (Global Admin)
You need User Access Administrator on / yourself, which a Global Admin only gets temporarily:
# a. Elevate your own access on root (Entra ID: "Access management for Azure resources" = Yes)az rest --method post --url "/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"# b. Assignment as above with --scope "/"az role assignment create --assignee-principal-type ServicePrincipal --assignee-object-id $id --scope "/" --role "Owner"# c. Remove your own elevation again$me = az ad signed-in-user show --query id -o tsvaz role assignment delete --assignee $me --role "User Access Administrator" --scope "/"After the deployment – remove the rights on all management groups and root at once
# run as the deployment SP itself or as an admin; root "/" last$roles = @("Owner", "User Access Administrator")$scopes = @(az account management-group list --query "[].id" -o tsv) + "/"foreach ($s in $scopes) { az role assignment list --scope $s --assignee $id -o json --only-show-errors | ConvertFrom-Json | Where-Object { $_.scope -eq $s -and $roles -contains $_.roleDefinitionName } | ForEach-Object { az role assignment delete --ids $_.id --only-show-errors ; "removed: $($_.roleDefinitionName) on $s" }}Dry run first, or for users/groups and subscriptions: web page → Generator → Identity and Access → Remove role assignments.
#Create a managed identity and assign a role
Create a user-assigned identity and give it a role on a scope.
$rg = "<rg>" ; $name = "<identity>"$principalId = az identity create -g $rg -n $name --query principalId -o tsv$scope = az keyvault show -n <keyvault-name> --query id -o tsv # or RG/subscription/resourceaz role assignment create --assignee-object-id $principalId --assignee-principal-type ServicePrincipal ` --role "Key Vault Secrets User" --scope $scopeaz role assignment list --assignee $principalId --all -o table#Which permissions do I (or someone else) have?
List role assignments of a user, including those received via groups.
$who = az ad signed-in-user show --query id -o tsv # or: <upn> / <object-id>az role assignment list --assignee $who --all --include-inherited ` --query "[].{role:roleDefinitionName, scope:scope}" -o tableaz ad user get-member-groups --id $who --query "[].displayName" -o tsv # permissions via groups#Policy blocks a deployment
Read the policy assignment from the error, make the deployment compliant or add a time-limited exemption.
# 1. Copy policyAssignmentId / policyDefinitionId from the error message$assignmentId = "<policyAssignmentId>"az policy assignment list --disable-scope-strict-match --scope /subscriptions/<sub>/resourceGroups/<rg> ` --query "[?id=='$assignmentId'].{name:displayName, scope:scope, def:policyDefinitionId}" -o tableaz policy definition show --name <definition-name> --query "{name:displayName, rule:policyRule}"# 2. Make it compliant (preferred) – or a time-limited exemption for the scopeaz policy exemption create -n <name> --policy-assignment $assignmentId ` --scope /subscriptions/<sub>/resourceGroups/<rg> --exemption-category Waiver ` --expires-on 2026-12-31T00:00:00Z --description "<reason, ticket>"#Find expiring certificates
Certificates in Key Vault and on a server that expire soon.
# In Key Vaultaz keyvault certificate list --vault-name <keyvault-name> ` --query "sort_by([].{name:name, expires:attributes.expires}, &expires)" -o table# What a server actually serves (Git Bash / Linux)openssl s_client -connect <host>:443 -servername <host> </dev/null 2>/dev/null | openssl x509 -noout -enddate -subject# Without openssl, in PowerShell$tcp = New-Object Net.Sockets.TcpClient("<host>", 443)$ssl = New-Object Net.Security.SslStream($tcp.GetStream(), $false, { $true })$ssl.AuthenticateAsClient("<host>")$ssl.RemoteCertificate | Select Subject, @{n='Expires';e={$_.GetExpirationDateString()}}$tcp.Close()#Azure CLI – Basics, Resources, VMs, Services
Top 5:az account show -o table·az account set --subscription <x>·az resource list -g <rg> -o table·az vm list -d -o table·az monitor activity-log list -g <rg> --offset 2h
#Install the Tools
Azure CLI, Terraform, Git, AzCopy and PowerShell 7 – pick your system:
winget install Microsoft.AzureCLI Hashicorp.Terraform Git.Git Microsoft.Azure.AZCopy.10 Microsoft.PowerShellbrew install azure-cli git azcopybrew install --cask powershellbrew tap hashicorp/tap && brew install hashicorp/tap/terraformcurl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash # Debian / Ubuntusudo apt install -y gitwget https://aka.ms/downloadazcopy-v10-linux -O azcopy.tar.gz && tar -xf azcopy.tar.gz --strip-components=1 --wildcards '*/azcopy' && sudo mv azcopy /usr/local/bin/Tip: Terraform and PowerShell on Linux: package repositories of HashiCorp and Microsoft – see developer.hashicorp.com/terraform/install and learn.microsoft.com/powershell (install on Linux). Check afterwards: az version · terraform version · git --version · azcopy --version.
#Sign-in and Subscription
Check the version, sign in and switch the active subscription.
az version # installed versionaz upgrade # update the CLIaz login # browser sign-inaz login --tenant <tenant-id> # specific tenantaz login --use-device-code # sign in with a code (other device)az login --use-device-code --tenant <tenant-id>az login --allow-no-subscriptions # tenant without subscriptionaz login --identity # managed identity (Azure VM/pipeline)az login --service-principal --username <app-id> --password <secret> --tenant <tenant-id> # workflow: 00-workflows.mdaz account show -o table # current subscriptionaz account list -o table # all subscriptionsaz account list --all -o table # incl. disabled onesaz account set --subscription <name-or-id> # switch subscriptionaz account list --query "[?name=='<name>'].id" -o tsvaz account tenant list -o tableaz logout ; az account clear # sign out, clear cacheaz config set core.output=table # default outputaz extension list -o tableaz extension add -n <name> # e.g. serial-console, resource-graphaz extension update -n <name>az provider register -n Microsoft.Network # register a resource provideraz provider show -n Microsoft.Network --query registrationState#Output and Filters (JMESPath)
Choose the output format and filter or pick columns with --query.
-o table | tsv | json | jsonc | yaml | none--query "[].{name:name, rg:resourceGroup}" # choose columns--query "[?location=='austriaeast'].name" # filter--query "[?contains(name,'<part>')].name" # contains--query "[?tags.environment=='prod']" # filter by tag--query "length(@)" # count--query "[0].id" # first element$id = az <...> --query id -o tsv # value into a variable (PowerShell)id=$(az <...> --query id -o tsv) # value into a variable (Bash)--debug # show REST calls (troubleshooting)#Resource Groups, Resources, Locks
List, create and delete resource groups and resources; protect them with locks.
az group list -o tableaz group create -n <rg> -l <region> --tags Owner=<x> environment=<y>az group delete -n <rg> --yes --no-waitaz resource list -g <rg> -o tableaz resource list --tag environment=<y> -o tableaz resource show --ids <resource-id>az resource tag --ids <resource-id> --tags key=value -i # -i = keep existing tagsaz resource delete --ids <resource-id>az resource move --destination-group <rg-new> --ids <id1> <id2>az lock list -g <rg> -o tableaz lock create -n <name> -g <rg> --lock-type CanNotDeleteaz lock delete -n <name> -g <rg> # Availability / regionaz account list-locations --query "[].{name:name, display:displayName}" -o tableaz vm list-skus -l <region> --size <sku> -o table#Resource Graph
Search across all subscriptions – ideal when you don't know where something lives.
az extension add -n resource-graphaz graph query -q "Resources | where name contains '<part>' | project name, type, resourceGroup, subscriptionId"az graph query -q "Resources | where type =~ 'microsoft.network/virtualnetworks' | project name, resourceGroup, properties.addressSpace.addressPrefixes"az graph query -q "Resources | where type =~ 'microsoft.network/publicipaddresses' | project name, ip=properties.ipAddress, resourceGroup"az graph query -q "Resources | summarize Count=count() by type | order by Count desc"az graph query -q "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | project name, subscriptionId"Many more queries: Resource Graph.
#Virtual Machines
List VMs with state and IP, start, stop, resize and run commands.
az vm list -d -o tableaz vm list -d -g <rg> --query "[].{name:name, state:powerState, zone:zones[0], ip:privateIps, pip:publicIps}" -o tableaz vm show -g <rg> -n <vm> --query "{size:hardwareProfile.vmSize, zones:zones, image:storageProfile.imageReference}"az vm get-instance-view -g <rg> -n <vm> --query "instanceView.statuses[].displayStatus"az vm start -g <rg> -n <vm>az vm stop -g <rg> -n <vm> # stopped, compute still billedaz vm deallocate -g <rg> -n <vm> --no-wait # deallocated, no compute costsaz vm restart -g <rg> -n <vm>az vm resize -g <rg> -n <vm> --size <sku>az vm list-sizes -l <region> -o tableaz vm list-skus -l <region> --size <sku> --query "[].locationInfo[].zones" -o tsv # zonesaz vm list-usage -l <region> -o table # quotaaz vm run-command invoke -g <rg> -n <vm> --command-id RunShellScript --scripts "hostname; ip a"az vm run-command invoke -g <rg> -n <vm> --command-id RunPowerShellScript --scripts "ipconfig /all"az vm boot-diagnostics enable -g <rg> -n <vm>az vm boot-diagnostics get-boot-log -g <rg> -n <vm>az serial-console connect -g <rg> -n <vm> # extension serial-consoleaz vm user update -g <rg> -n <vm> -u <user> -p <new-password> # reset passwordaz vm user update -g <rg> -n <vm> -u <user> --ssh-key-value "<public-key>"az vm disk attach -g <rg> --vm-name <vm> --name <disk> --new --size-gb 32az vm extension list -g <rg> --vm-name <vm> -o tableaz vm identity assign -g <rg> -n <vm> # system-assigned identity # NICaz network nic list -g <rg> -o tableaz network nic show -g <rg> -n <nic> --query "{ip:ipConfigurations[].privateIPAddress, fwd:enableIPForwarding, accel:enableAcceleratedNetworking}"az network nic update -g <rg> -n <nic> --ip-forwarding true # required for NVAs#Marketplace Images
Find publisher, offer and SKU of a Marketplace image.
az vm image list-publishers -l <region> --query "[?contains(name,'<part>')].name" -o tsvaz vm image list-offers -l <region> -p <publisher> -o tableaz vm image list-skus -l <region> -p <publisher> -f <offer> -o tableaz vm image list -p <publisher> -f <offer> -s <sku> -l <region> --all -o tableaz vm image terms show --urn <publisher>:<offer>:<sku>:<version>az vm image terms accept --urn <publisher>:<offer>:<sku>:<version>| Product | Publisher | Offer |
|---|---|---|
| FortiGate | fortinet | fortinet_fortigate-vm |
| Ubuntu 24.04 | Canonical | ubuntu-24_04-lts |
| Windows Server | MicrosoftWindowsServer | WindowsServer |
#App Service / Functions
In detail – slots, Key Vault references, network, Functions: App Service and Functions.
az webapp list --query "[].{name:name, rg:resourceGroup, state:state, host:defaultHostName}" -o tableaz webapp show -g <rg> -n <app> --query "{state:state, https:httpsOnly, pna:publicNetworkAccess, vnet:virtualNetworkSubnetId}"az webapp restart -g <rg> -n <app>az webapp log tail -g <rg> -n <app> # live logaz webapp config appsettings list -g <rg> -n <app> -o tableaz webapp config appsettings set -g <rg> -n <app> --settings KEY=valueaz webapp config access-restriction show -g <rg> -n <app>az webapp vnet-integration list -g <rg> -n <app>az webapp update -g <rg> -n <app> --set publicNetworkAccess=Disabledaz functionapp list -o table#Storage Account
List storage accounts and check TLS, public access and network rules.
az storage account list --query "[].{name:name, rg:resourceGroup, sku:sku.name, pna:publicNetworkAccess}" -o tableaz storage account show -g <rg> -n <sa> --query "{tls:minimumTlsVersion, public:allowBlobPublicAccess, key:allowSharedKeyAccess}"az storage account network-rule list -g <rg> --account-name <sa>az storage account update -g <rg> -n <sa> --default-action Denyaz storage container list --account-name <sa> --auth-mode login -o tableaz storage blob list --account-name <sa> -c <container> --auth-mode login -o tableaz storage blob upload --account-name <sa> -c <container> -f <file> -n <name> --auth-mode loginaz storage blob download --account-name <sa> -c <container> -n <name> -f <target> --auth-mode login--auth-mode login needs a data role (e.g. Storage Blob Data Contributor); Owner alone is not enough. More: Storage.
#AKS (short)
In detail – create, node pools, upgrade, troubleshooting: AKS.
az aks list -o tableaz aks get-credentials -g <rg> -n <aks> # get kubeconfigaz aks show -g <rg> -n <aks> --query "{k8s:kubernetesVersion, net:networkProfile.networkPlugin, outbound:networkProfile.outboundType}"kubectl get nodes -o wide ; kubectl get pods -A#Monitoring and Diagnostic Settings
Read the activity log and set up diagnostic settings.
az monitor activity-log list -g <rg> --offset 2h ` --query "[].{time:eventTimestamp, op:operationName.localizedValue, status:status.value, by:caller}" -o tableaz monitor activity-log list --correlation-id <id>az monitor diagnostic-settings list --resource <resource-id> -o tableaz monitor diagnostic-settings categories list --resource <resource-id> -o tableaz monitor diagnostic-settings create -n <name> --resource <resource-id> --workspace <law-id> ` --logs '[{"categoryGroup":"allLogs","enabled":true}]' --metrics '[{"category":"AllMetrics","enabled":true}]'az monitor log-analytics workspace list -o tableaz monitor log-analytics workspace show -g <rg> -n <law> --query customerId -o tsv # workspace GUIDaz monitor log-analytics query -w <workspace-guid> --analytics-query "<KQL>" -o tableaz monitor metrics list-definitions --resource <resource-id> -o tableaz monitor metrics list --resource <resource-id> --metric <name> --interval PT5M -o tableaz monitor metrics alert list -g <rg> -o table#REST and Token
Get an access token and call any Azure REST API with az rest.
az account get-access-token --query accessToken -o tsvaz rest --method get --url "https://management.azure.com/subscriptions/<sub>/resourceGroups?api-version=2021-04-01"az rest --method get --url "<resource-id>?api-version=<version>" # raw resource as JSONUseful when a CLI command doesn't (yet) know a property.
#Azure PowerShell (Az Module)
Install-Module Az -Scope AllUsers -Repository PSGallery -Force # as admin – see "Install Modules Without the User Profile" belowUpdate-Module Az -Scope AllUsers # as adminConnect-AzAccount [-Tenant <id>] [-UseDeviceAuthentication]Connect-AzAccount -Identity # managed identityConnect-AzAccount -UseDeviceAuthentication # device code$cred = Get-Credential -UserName <app-id> ; Connect-AzAccount -ServicePrincipal -Credential $cred -Tenant <tenant-id>Get-AzContextGet-AzSubscriptionSet-AzContext -Subscription <name-or-id>Disconnect-AzAccount#Install Modules Without the User Profile (AllUsers)
When a company policy (software restriction policy / AppLocker) blocks the user profile, modules under Documents\... do not load:
... cannot be loaded because its operation is blocked by software restriction policies ...Modules under C:\Program Files\... still work.
Caution: Always install as admin with -Scope AllUsers, never with -Scope CurrentUser.
PowerShell 7 (pwsh) | Windows PowerShell 5.1 (powershell) | |
|---|---|---|
| Install location (works) | C:\Program Files\PowerShell\Modules | C:\Program Files\WindowsPowerShell\Modules |
| Blocked location (profile) | Documents\PowerShell\Modules | Documents\WindowsPowerShell\Modules |
Which version runs: $PSVersionTable.PSVersion (7 or 5). Errors with At line:1 char:1 / CategoryInfo come from 5.1.
Open an admin window (from cmd, Win+R or any PowerShell)
powershell -Command "Start-Process pwsh -Verb RunAs"powershell -Command "Start-Process powershell -Verb RunAs"Rename old copies in the user profile (if there are any) – otherwise a blocked copy is loaded, or dependencies such as Az.Accounts are skipped during the install. The command finds the real location itself, so it also works when Documents is redirected (e.g. OneDrive):
Get-Module <module>* -ListAvailable | Where-Object { $_.ModuleBase -like "$HOME*" } | ForEach-Object { if ($_.ModuleBase -match '\\\d+(\.\d+)+$') { Split-Path $_.ModuleBase } else { $_.ModuleBase } } | Select-Object -Unique | Where-Object { (Split-Path $_ -Leaf) -notlike '*_old' } | ForEach-Object { Rename-Item $_ -NewName ((Split-Path $_ -Leaf) + '_old') }Tip: Where are modules searched? $env:PSModulePath -split ';'
Install in the admin window
Install-Module <module> -Scope AllUsers -Repository PSGallery -Force[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # 5.1 often needs TLS 1.2 for the downloadInstall-PackageProvider NuGet -ForceInstall-Module <module> -Scope AllUsers -Repository PSGallery -ForceAnswer "Untrusted repository" with A (Yes to All).
Test in a normal window (not admin)
Get-Module <module> -ListAvailable | Select-Object Version, PathImport-Module <module>The path must be under C:\Program Files\.... No output from the import = success.
Updates – also in an admin window of the same PowerShell version:
Update-Module <module> -Scope AllUsersExamples and special cases
Install-Module Az -Scope AllUsers -Repository PSGallery -ForceInstall-Module EnterprisePolicyAsCode -Scope AllUsers -Repository PSGallery -ForceInstall-Module Az.Accounts -Scope AllUsers -Repository PSGallery -Force # if Az.Accounts is missing in C:\Program Files\...- With Az, check that Az.Accounts is in the install folder (
Get-Module Az.Accounts -ListAvailable). - "running scripts is disabled on this system" in 5.1:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser(no admin needed, unless the company sets it via group policy) – see Unblock and run downloaded scripts. - The
*_oldfolders in the profile can be deleted once everything works.
#Resources
Resource groups and resources with the Az module.
Get-AzResourceGroup | Format-Table ResourceGroupName, LocationGet-AzResource -ResourceGroupName <rg> | Format-Table Name, ResourceTypeGet-AzResource -Name "*<part>*" | Format-Table Name, ResourceGroupName, ResourceTypeSearch-AzGraph -Query "Resources | where name contains '<part>' | project name, type, resourceGroup" # module Az.ResourceGraph#VMs
List, start and stop VMs with PowerShell.
Get-AzVM -Status | Select Name, ResourceGroupName, PowerStateStart-AzVM -ResourceGroupName <rg> -Name <vm>Stop-AzVM -ResourceGroupName <rg> -Name <vm> -Force # deallocatedInvoke-AzVMRunCommand -ResourceGroupName <rg> -VMName <vm> -CommandId RunPowerShellScript -ScriptString "ipconfig /all"#Network
Virtual networks, peerings and effective routes with PowerShell.
Get-AzVirtualNetwork | Select Name, @{n='Space';e={$_.AddressSpace.AddressPrefixes}}Get-AzVirtualNetworkPeering -ResourceGroupName <rg> -VirtualNetworkName <vnet> | Select Name, PeeringState, AllowForwardedTrafficGet-AzEffectiveRouteTable -ResourceGroupName <rg> -NetworkInterfaceName <nic> | Format-TableGet-AzEffectiveNetworkSecurityGroup -ResourceGroupName <rg> -NetworkInterfaceName <nic>Get-AzNetworkSecurityGroup -Name <nsg> -ResourceGroupName <rg> | Get-AzNetworkSecurityRuleConfig | Format-TableGet-AzPublicIpAddress | Select Name, IpAddressGet-AzApplicationGatewayBackendHealth -ResourceGroupName <rg> -Name <agw>#Application Gateway
Read certificates and listeners of an Application Gateway.
$AppGW = Get-AzApplicationGateway -Name <agw> -ResourceGroupName <rg>$AppGW.SslCertificates | Select Name, KeyVaultSecretId$AppGW.HttpListeners | Select Name, Protocol, HostNames$secret = Get-AzKeyVaultSecret -VaultName <kv> -Name <name>$secretId = $secret.Id.Replace($secret.Version, "") # versionless secret ID$AppGW = Add-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <name> -KeyVaultSecretId $secretId$AppGW = Set-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <name> -KeyVaultSecretId <new-id>$AppGW = Remove-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <name>Set-AzApplicationGateway -ApplicationGateway $AppGW # changes only take effect hereStop-AzApplicationGateway -ApplicationGateway $AppGW ; Start-AzApplicationGateway -ApplicationGateway $AppGWComplete with identity and listener: Workflows → App Gateway.
#Permissions and Key Vault
Role assignments and Key Vault access with PowerShell.
Get-AzRoleAssignment -SignInName <upn>Get-AzRoleAssignment -Scope <resource-id>New-AzRoleAssignment -ObjectId <id> -RoleDefinitionName "<role>" -Scope <resource-id>Get-AzKeyVaultSecret -VaultName <kv> -Name <name> -AsPlainText#Azure CLI ↔ Azure PowerShell
#Basics
Sign-in and subscription side by side.
| Task | Azure CLI | Azure PowerShell |
|---|---|---|
| Sign in | az login | Connect-AzAccount |
| Device code | az login --use-device-code | Connect-AzAccount -UseDeviceAuthentication |
| Managed identity | az login --identity | Connect-AzAccount -Identity |
| Current context | az account show | Get-AzContext |
| Subscriptions | az account list -o table | Get-AzSubscription |
| Switch subscription | az account set --subscription <x> | Set-AzContext -Subscription <x> |
| Sign out | az logout | Disconnect-AzAccount |
| Get a token | az account get-access-token | Get-AzAccessToken |
| Version | az version | Get-InstalledModule Az |
#Resources and VMs
Resource groups and VMs in both worlds.
| Task | Azure CLI | Azure PowerShell |
|---|---|---|
| Resource groups | az group list -o table | Get-AzResourceGroup |
| Create an RG | az group create -n <rg> -l <region> | New-AzResourceGroup -Name <rg> -Location <region> |
| Resources in an RG | az resource list -g <rg> | Get-AzResource -ResourceGroupName <rg> |
| Resource by ID | az resource show --ids <id> | Get-AzResource -ResourceId <id> |
| Set tags (merge) | az resource tag --ids <id> --tags k=v -i | Update-AzTag -ResourceId <id> -Tag @{k='v'} -Operation Merge |
| VMs with status | az vm list -d -o table | Get-AzVM -Status |
| Start a VM | az vm start -g <rg> -n <vm> | Start-AzVM -ResourceGroupName <rg> -Name <vm> |
| Deallocate a VM | az vm deallocate -g <rg> -n <vm> | Stop-AzVM -ResourceGroupName <rg> -Name <vm> -Force |
| Command in a VM | az vm run-command invoke … --scripts "<cmd>" | Invoke-AzVMRunCommand … -ScriptString "<cmd>" |
| Resource Graph | az graph query -q "<kql>" | Search-AzGraph -Query "<kql>" |
#Network
Network commands in both worlds.
| Task | Azure CLI | Azure PowerShell |
|---|---|---|
| VNets | az network vnet list | Get-AzVirtualNetwork |
| Subnet | az network vnet subnet show -g <rg> --vnet-name <vnet> -n <snet> | Get-AzVirtualNetworkSubnetConfig -VirtualNetwork $vnet -Name <snet> |
| Peerings | az network vnet peering list -g <rg> --vnet-name <vnet> | Get-AzVirtualNetworkPeering -ResourceGroupName <rg> -VirtualNetworkName <vnet> |
| Effective routes | az network nic show-effective-route-table -g <rg> -n <nic> | Get-AzEffectiveRouteTable -ResourceGroupName <rg> -NetworkInterfaceName <nic> |
| Effective NSG | az network nic list-effective-nsg -g <rg> -n <nic> | Get-AzEffectiveNetworkSecurityGroup -ResourceGroupName <rg> -NetworkInterfaceName <nic> |
| NSG rules | az network nsg rule list -g <rg> --nsg-name <nsg> | Get-AzNetworkSecurityGroup -Name <nsg> -ResourceGroupName <rg> | Get-AzNetworkSecurityRuleConfig |
| Public IPs | az network public-ip list | Get-AzPublicIpAddress |
| App Gateway | az network application-gateway show -g <rg> -n <agw> | Get-AzApplicationGateway -ResourceGroupName <rg> -Name <agw> |
| Backend health | az network application-gateway show-backend-health … | Get-AzApplicationGatewayBackendHealth … |
Key difference: PowerShell often works with objects: get → modify → write back.
$vnet = Get-AzVirtualNetwork -ResourceGroupName <rg> -Name <vnet>Add-AzVirtualNetworkSubnetConfig -VirtualNetwork $vnet -Name <snet> -AddressPrefix 10.0.5.0/24 | Out-Null$vnet | Set-AzVirtualNetwork # only now is it changed in AzureThe CLI changes directly: az network vnet subnet create -g <rg> --vnet-name <vnet> -n <snet> --address-prefixes 10.0.5.0/24.
#Permissions, Key Vault, Deployments
Roles, Key Vault and deployments in both worlds.
| Task | Azure CLI | Azure PowerShell |
|---|---|---|
| A user's roles | az role assignment list --assignee <upn> --all | Get-AzRoleAssignment -SignInName <upn> |
| Assign a role | az role assignment create --assignee <id> --role "<role>" --scope <scope> | New-AzRoleAssignment -ObjectId <id> -RoleDefinitionName "<role>" -Scope <scope> |
| Own object ID | az ad signed-in-user show --query id -o tsv | (Get-AzADUser -SignedIn).Id |
| Find a service principal | az ad sp list --display-name <name> | Get-AzADServicePrincipal -DisplayName <name> |
| Read a secret | az keyvault secret show --vault-name <kv> -n <name> --query value -o tsv | Get-AzKeyVaultSecret -VaultName <kv> -Name <name> -AsPlainText |
| Set a secret | az keyvault secret set --vault-name <kv> -n <name> --value <v> | Set-AzKeyVaultSecret -VaultName <kv> -Name <name> -SecretValue (ConvertTo-SecureString <v> -AsPlainText -Force) |
| Deploy Bicep | az deployment group create -g <rg> -f main.bicep -p <p>.bicepparam | New-AzResourceGroupDeployment -ResourceGroupName <rg> -TemplateFile main.bicep -TemplateParameterFile <p>.bicepparam |
| what-if | az deployment group what-if … | New-AzResourceGroupDeployment … -WhatIf |
| Policy assignments | az policy assignment list --scope <scope> | Get-AzPolicyAssignment -Scope <scope> |
#Output and Filtering
Table, JSON and filters in both worlds.
| Task | Azure CLI | Azure PowerShell |
|---|---|---|
| Table | -o table | | Format-Table |
| Single value | --query id -o tsv | (Get-AzVM …).Id |
| Filter | --query "[?location=='westeurope']" | | Where-Object Location -eq 'westeurope' |
| Choose columns | --query "[].{n:name, rg:resourceGroup}" | | Select-Object Name, ResourceGroupName |
| Count | --query "length(@)" | (… ).Count |
| As JSON | -o json | | ConvertTo-Json -Depth 5 |
| CLI output as object | az … -o json | ConvertFrom-Json | – |
#App Service and Azure Functions
Web apps, APIs and functions without managing VMs. An App Service plan = the compute (SKU, number of instances), apps run on it.
Top 5:az webapp log tail·az webapp restart·az webapp config appsettings set·az webapp deployment slot swap·az webapp deploy
#Plans and SKUs
Which plan fits which use.
| SKU | For | Notes |
|---|---|---|
| F1 / B1–B3 | test, dev | no slots, no autoscale (B: manual scale up to 3) |
| S1–S3 | older standard | slots (5), autoscale – prefer Premium v3 |
| P0v3–P3v3 / P1mv3… | production | slots (20), autoscale, better price/performance, zone redundancy (≥ 2–3 instances) |
| I1v2… (ASE v3) | isolated, own VNet | single tenant, expensive |
| Functions Flex Consumption | event-driven, scale to zero | VNet integration, per-execution billing – recommended for new function apps |
| Functions Premium (EP1–EP3) | always warm, VNet | no cold start, longer runtime |
Linux and Windows apps need separate plans. All apps on a plan share its CPU/RAM – one noisy app slows the others.
#Create a Web App
Plan, app, runtime and first settings.
$rg = "<rg>" ; $plan = "asp-<app>-prd-weu" ; $app = "app-<app>-prd-weu"az appservice plan create -g $rg -n $plan --sku P0v3 --is-linux -l westeuropeaz webapp list-runtimes --os linux -o table # valid runtime stringsaz webapp create -g $rg -n $app -p $plan --runtime "DOTNETCORE:8.0"az webapp update -g $rg -n $app --https-only trueaz webapp config set -g $rg -n $app --min-tls-version 1.2 --ftps-state Disabled --always-on true --http20-enabled trueaz webapp identity assign -g $rg -n $app # system-assigned managed identity$rg = "<rg>" ; $plan = "asp-<app>-prd-weu" ; $app = "app-<app>-prd-weu"New-AzAppServicePlan -ResourceGroupName $rg -Name $plan -Location westeurope -Tier PremiumV3 -WorkerSize Small -LinuxNew-AzWebApp -ResourceGroupName $rg -Name $app -AppServicePlan $plan -Location westeuropeSet-AzWebApp -ResourceGroupName $rg -Name $app -HttpsOnly $true -MinTlsVersion 1.2 -FtpsState Disabled -AlwaysOn $true -AssignIdentity $trueresource plan 'Microsoft.Web/serverfarms@2023-12-01' = { name: 'asp-<app>-prd-weu' location: location sku: { name: 'P0v3', capacity: 1 } kind: 'linux' properties: { reserved: true } // true = Linux}resource app 'Microsoft.Web/sites@2023-12-01' = { name: 'app-<app>-prd-weu' location: location identity: { type: 'SystemAssigned' } properties: { serverFarmId: plan.id httpsOnly: true siteConfig: { linuxFxVersion: 'DOTNETCORE|8.0' minTlsVersion: '1.2' ftpsState: 'Disabled' alwaysOn: true } }}resource "azurerm_service_plan" "this" { name = "asp-<app>-prd-weu" resource_group_name = "<rg>" location = "westeurope" os_type = "Linux" sku_name = "P0v3"}resource "azurerm_linux_web_app" "this" { name = "app-<app>-prd-weu" resource_group_name = "<rg>" location = "westeurope" service_plan_id = azurerm_service_plan.this.id https_only = true identity { type = "SystemAssigned" } site_config { always_on = true minimum_tls_version = "1.2" ftps_state = "Disabled" application_stack { dotnet_version = "8.0" } }}App names are globally unique (<app>.azurewebsites.net). Runtime strings use : in the CLI and | in linuxFxVersion.
#Deploy Code
Zip, jar and slot deployment.
az webapp deploy -g <rg> -n <app> --src-path .\publish.zip --type zip # zip deploy (recommended)az webapp deploy -g <rg> -n <app> --src-path .\app.jar --type jaraz webapp deploy -g <rg> -n <app> --src-path .\publish.zip --type zip --slot stagingaz webapp config container set -g <rg> -n <app> --container-image-name <acr>.azurecr.io/<image>:<tag> ` --container-registry-url https://<acr>.azurecr.io # container (managed identity needs AcrPull)az webapp deployment list-publishing-profiles -g <rg> -n <app> --xml # only if a tool needs it – contains secretsPipeline: task AzureWebApp@1 (appName, package, optional deployToSlotOrASE + slotName) – details in Pipelines. Kudu console / files: https://<app>.scm.azurewebsites.net.
#Deployment Slots
Deploy to staging, warm up, then swap – no downtime, roll back by swapping again.
az webapp deployment slot create -g <rg> -n <app> --slot staging --configuration-source <app>az webapp deployment slot list -g <rg> -n <app> -o tableaz webapp deployment slot swap -g <rg> -n <app> --slot staging --target-slot productionaz webapp deployment slot swap -g <rg> -n <app> --slot staging --action preview # swap with preview (apply settings first)az webapp deployment slot swap -g <rg> -n <app> --slot staging --action swapaz webapp traffic-routing set -g <rg> -n <app> --distribution staging=10 # 10 % canary to stagingaz webapp traffic-routing clear -g <rg> -n <app>Important: settings marked as deployment slot setting (--slot-settings) stay with the slot – connection strings to the test database must be slot settings, otherwise they move to production on swap.
#Settings, Secrets and Key Vault References
App settings, slot settings and secrets from Key Vault.
az webapp config appsettings list -g <rg> -n <app> -o tableaz webapp config appsettings set -g <rg> -n <app> --settings ASPNETCORE_ENVIRONMENT=Production FeatureX=trueaz webapp config appsettings set -g <rg> -n <app> --slot staging --slot-settings ASPNETCORE_ENVIRONMENT=Stagingaz webapp config appsettings delete -g <rg> -n <app> --setting-names FeatureXaz webapp config connection-string set -g <rg> -n <app> -t SQLAzure --settings Db="<connection-string>"# Key Vault reference instead of the secret value (identity of the app needs "Key Vault Secrets User")az webapp config appsettings set -g <rg> -n <app> --settings "DbPassword=@Microsoft.KeyVault(SecretUri=https://<keyvault-name>.vault.azure.net/secrets/<secret-name>/)"$mi = az webapp identity show -g <rg> -n <app> --query principalId -o tsvaz role assignment create --assignee-object-id $mi --assignee-principal-type ServicePrincipal ` --role "Key Vault Secrets User" --scope (az keyvault show -n <keyvault-name> --query id -o tsv)Without a version in the SecretUri the app picks up a new secret version within 24 h (or on restart). Status of the reference: portal → app → Environment variables → green check / red error at the setting.
#Network: VNet Integration, Private Endpoint, Access Restrictions
Outbound, inbound and allow lists compared.
| Direction | Feature | What it does |
|---|---|---|
| App → outbound into VNet | VNet integration (delegated subnet, /26 recommended) | app reaches private endpoints, on-prem, firewall |
| Clients → app inbound | Private endpoint | app only reachable via private IP; public access off |
| Clients → app inbound | Access restrictions | allow/deny by IP, service tag, VNet, Front Door ID |
# Outbound: VNet integration – all traffic through the VNet (UDR / firewall apply)az webapp vnet-integration add -g <rg> -n <app> --vnet <vnet> --subnet <snet-appsvc-integration>az webapp config set -g <rg> -n <app> --vnet-route-all-enabled true# Inbound: only Front Door may reach the app$fdid = az afd profile show -g <rg-afd> --profile-name <afd-profile> --query frontDoorId -o tsvaz webapp config access-restriction add -g <rg> -n <app> --rule-name allow-afd --priority 100 --action Allow ` --service-tag AzureFrontDoor.Backend --http-header x-azure-fdid=$fdidaz webapp config access-restriction show -g <rg> -n <app>az webapp config access-restriction set -g <rg> -n <app> --use-same-restrictions-for-scm-site false# Inbound private: disable public access after the private endpoint worksaz webapp update -g <rg> -n <app> --set publicNetworkAccess=DisabledPrivate endpoint for the web app: Workflow – DNS zone privatelink.azurewebsites.net. With VNet integration the app resolves DNS via the VNet's DNS servers.
#Custom Domain and Certificate
Domain verification, hostname and certificate.
# DNS: CNAME www -> <app>.azurewebsites.net and TXT asuid.www -> verification IDaz webapp show -g <rg> -n <app> --query customDomainVerificationId -o tsvaz webapp config hostname add -g <rg> --webapp-name <app> --hostname www.<domain># Free managed certificate (CNAME domains; renews automatically)az webapp config ssl create -g <rg> -n <app> --hostname www.<domain>$thumb = az webapp config ssl list -g <rg> --query "[?subjectName=='www.<domain>'].thumbprint | [0]" -o tsvaz webapp config ssl bind -g <rg> -n <app> --certificate-thumbprint $thumb --ssl-type SNI# Certificate from Key Vault (App Service resource provider needs access to the vault)az webapp config ssl import -g <rg> -n <app> --key-vault <keyvault-name> --key-vault-certificate-name <certificate-name>Behind Front Door the certificate lives on Front Door – see Front Door.
#Logs, Restart and Scaling
Live log, restart and scale up or out.
az webapp log config -g <rg> -n <app> --application-logging filesystem --level information --docker-container-logging filesystem --web-server-logging filesystemaz webapp log tail -g <rg> -n <app> # live log streamaz webapp log download -g <rg> -n <app> --log-file logs.zipaz webapp restart -g <rg> -n <app>az webapp stop -g <rg> -n <app> ; az webapp start -g <rg> -n <app>az appservice plan update -g <rg> -n <plan> --sku P1v3 # scale upaz appservice plan update -g <rg> -n <plan> --number-of-workers 3 # scale outaz webapp show -g <rg> -n <app> --query "{state:state, host:defaultHostName, plan:appServicePlanId, outboundIps:outboundIpAddresses}"For production: diagnostic setting → Log Analytics (AppServiceHTTPLogs, AppServiceConsoleLogs, AppServiceAppLogs) and Application Insights. KQL examples: KQL. Portal tool: Diagnose and solve problems (availability, CPU, crashes, SNAT).
#Azure Functions
Create a function app and deploy code.
$rg = "<rg>" ; $st = "st<app>func01" ; $func = "func-<app>-prd-weu"az storage account create -g $rg -n $st -l westeurope --sku Standard_LRS --allow-blob-public-access false# Flex Consumption (scale to zero, VNet capable)az functionapp create -g $rg -n $func --storage-account $st --flexconsumption-location westeurope ` --runtime python --runtime-version 3.11# Premium plan (always warm)az functionapp plan create -g $rg -n asp-<app>-func --sku EP1 --is-linux -l westeuropeaz functionapp create -g $rg -n $func --storage-account $st --plan asp-<app>-func ` --runtime dotnet-isolated --runtime-version 8 --functions-version 4 --os-type Linuxaz functionapp function list -g $rg -n $func -o tableaz functionapp keys list -g $rg -n $funcaz functionapp config appsettings set -g $rg -n $func --settings MySetting=valuefunc init <project> --worker-runtime python # new project locallyfunc new --name HttpHello --template "HTTP trigger"func start # run locallyfunc azure functionapp publish <func-app-name> # deployfunc azure functionapp fetch-app-settings <func-app-name> # settings into local.settings.jsonFunctions need a storage account (AzureWebJobsStorage) – with managed identity instead of a key: AzureWebJobsStorage__accountName=<storage-account> + roles Storage Blob Data Owner (and Queue/Table roles depending on triggers). Logs: Application Insights → Live metrics, traces / requests tables.
#Troubleshooting
Symptoms, causes and fixes.
| Symptom | Cause | Fix |
|---|---|---|
| 503 after deploy / start | app crashes on start (missing setting, wrong runtime) | az webapp log tail, Kudu → LogFiles, check runtime string |
| 403 Forbidden | access restriction / private endpoint with public access off | access-restriction show, test from inside the VNet |
| Key Vault reference shows error | identity missing role / vault firewall | Key Vault Secrets User for the app identity, allow VNet / trusted services |
| App cannot reach private resources | no VNet integration / DNS | VNet integration + vnetRouteAllEnabled, private DNS zone linked to the VNet |
| Slow first request | Always On off / Consumption cold start | --always-on true, Premium/Flex with always-ready instances |
| Setting moved to production on swap | not marked as slot setting | --slot-settings |
| Custom domain not verified | asuid TXT record missing | TXT asuid.<sub> = customDomainVerificationId |
| SNAT port exhaustion (timeouts outbound) | many new connections | reuse HTTP clients, VNet integration + NAT gateway |
| Function not triggering | storage/identity problem, app setting missing | check AzureWebJobsStorage*, App Insights traces for errors |
#AKS – Azure Kubernetes Service
Managed Kubernetes: Azure runs the control plane, you run node pools (VM scale sets). Daily work = az aks for the cluster, kubectl for what runs on it.
Top 5:az aks get-credentials·kubectl get pods -A·kubectl describe pod·kubectl logs·az aks get-upgrades
#Create a Cluster
Recommended defaults: Azure CNI Overlay (pods get IPs from a private overlay range, not from the subnet), managed identity, Entra ID + Azure RBAC for access, local accounts off, tier Standard for production (uptime SLA).
$rg = "<rg>" ; $aks = "aks-<workload>-prd-weu"$subnet = az network vnet subnet show -g <rg-network> --vnet-name <vnet> -n <snet-aks> --query id -o tsvaz aks create -g $rg -n $aks -l westeurope --tier standard ` --kubernetes-version <version> --node-count 3 --node-vm-size Standard_D4ds_v5 --zones 1 2 3 ` --network-plugin azure --network-plugin-mode overlay --pod-cidr 192.168.0.0/16 --vnet-subnet-id $subnet ` --enable-managed-identity --enable-aad --enable-azure-rbac --disable-local-accounts ` --enable-oidc-issuer --enable-workload-identity ` --attach-acr <acr> --generate-ssh-keysaz aks get-versions -l westeurope -o table # supported versionsresource aks 'Microsoft.ContainerService/managedClusters@2024-05-01' = { name: 'aks-<workload>-prd-weu' location: location sku: { name: 'Base', tier: 'Standard' } identity: { type: 'SystemAssigned' } properties: { dnsPrefix: 'aks-<workload>-prd' kubernetesVersion: '<version>' disableLocalAccounts: true aadProfile: { managed: true, enableAzureRBAC: true } agentPoolProfiles: [ { name: 'system' mode: 'System' count: 3 vmSize: 'Standard_D4ds_v5' availabilityZones: [ '1', '2', '3' ] vnetSubnetID: subnetId } ] networkProfile: { networkPlugin: 'azure', networkPluginMode: 'overlay', podCidr: '192.168.0.0/16' } oidcIssuerProfile: { enabled: true } securityProfile: { workloadIdentity: { enabled: true } } }}resource "azurerm_kubernetes_cluster" "this" { name = "aks-<workload>-prd-weu" resource_group_name = "<rg>" location = "westeurope" dns_prefix = "aks-<workload>-prd" kubernetes_version = "<version>" sku_tier = "Standard" local_account_disabled = true oidc_issuer_enabled = true workload_identity_enabled = true default_node_pool { name = "system" node_count = 3 vm_size = "Standard_D4ds_v5" zones = ["1", "2", "3"] vnet_subnet_id = "<subnet-id>" } identity { type = "SystemAssigned" } network_profile { network_plugin = "azure" network_plugin_mode = "overlay" pod_cidr = "192.168.0.0/16" } azure_active_directory_role_based_access_control { azure_rbac_enabled = true }}Subnet size with overlay: only nodes need subnet IPs (+ surge during upgrades) – a /24 is enough for many clusters. Pod CIDR must not overlap with VNets, on-prem or the service CIDR (default 10.0.0.0/16).
#Connect (kubectl)
Get credentials and sign in with Entra ID.
az aks install-cli # kubectl + kubeloginaz aks get-credentials -g <rg> -n <aks> --overwrite-existingkubelogin convert-kubeconfig -l azurecli # use your az login for Entra IDkubectl config get-contexts ; kubectl config use-context <aks>kubectl get nodes -o wide# Access with Azure RBAC: give a group a cluster roleaz role assignment create --assignee <group-object-id> --role "Azure Kubernetes Service RBAC Cluster Admin" ` --scope (az aks show -g <rg> -n <aks> --query id -o tsv)Roles: AKS RBAC Reader / Writer / Admin (namespace scope possible: --scope <aks-id>/namespaces/<ns>), AKS RBAC Cluster Admin. To download the kubeconfig you also need Azure Kubernetes Service Cluster User Role.
#kubectl in Daily Work
Pods, logs, exec and rollouts.
kubectl get pods -A # all namespaceskubectl get pods -n <ns> -o wide # with node and pod IPkubectl get deploy,svc,ingress -n <ns>kubectl describe pod <pod> -n <ns> # events at the bottom: why it does not startkubectl logs <pod> -n <ns> --tail 100 -f # follow logskubectl logs <pod> -n <ns> --previous # logs of the crashed containerkubectl exec -it <pod> -n <ns> -- sh # shell in the containerkubectl get events -n <ns> --sort-by .lastTimestampkubectl top nodes ; kubectl top pods -n <ns> # CPU/memory (metrics server)kubectl apply -f deployment.yamlkubectl rollout status deploy/<deployment> -n <ns>kubectl rollout restart deploy/<deployment> -n <ns> # restart all podskubectl rollout undo deploy/<deployment> -n <ns> # back to the previous versionkubectl scale deploy/<deployment> -n <ns> --replicas 3kubectl port-forward svc/<service> -n <ns> 8080:80 # test locally on http://localhost:8080kubectl delete pod <pod> -n <ns> # pod is recreated by its deployment#Node Pools and Scaling
Add, scale and remove node pools.
az aks nodepool list -g <rg> --cluster-name <aks> -o tableaz aks nodepool add -g <rg> --cluster-name <aks> -n user01 --mode User --node-count 2 ` --node-vm-size Standard_D8ds_v5 --zones 1 2 3 --labels workload=appsaz aks nodepool scale -g <rg> --cluster-name <aks> -n user01 --node-count 4az aks nodepool update -g <rg> --cluster-name <aks> -n user01 --enable-cluster-autoscaler --min-count 2 --max-count 6az aks nodepool update -g <rg> --cluster-name <aks> -n user01 --update-cluster-autoscaler --min-count 2 --max-count 10az aks nodepool delete -g <rg> --cluster-name <aks> -n user01System pools run CoreDNS, metrics etc. (min. 1 node, better 3 across zones) – put applications on User pools. Node VM size of a pool cannot be changed: add a new pool, move workloads (kubectl cordon / drain), delete the old one.
#Upgrade
Check versions and upgrade control plane and nodes.
az aks get-upgrades -g <rg> -n <aks> -o tableaz aks upgrade -g <rg> -n <aks> --kubernetes-version <version> --control-plane-only # first the control planeaz aks nodepool upgrade -g <rg> --cluster-name <aks> -n user01 --kubernetes-version <version>az aks nodepool upgrade -g <rg> --cluster-name <aks> -n user01 --node-image-only # only OS image (security patches)az aks nodepool update -g <rg> --cluster-name <aks> -n user01 --max-surge 33% # faster, needs subnet IPsaz aks update -g <rg> -n <aks> --auto-upgrade-channel patch --node-os-upgrade-channel NodeImageaz aks maintenanceconfiguration list -g <rg> --cluster-name <aks> -o tableAKS supports the last three minor versions – upgrade at least every few months. Upgrade one minor version at a time. PodDisruptionBudgets that allow 0 disruptions block node drains.
#Container Registry (ACR)
Attach a registry and test the pull.
az aks update -g <rg> -n <aks> --attach-acr <acr> # AcrPull for the kubelet identityaz aks check-acr -g <rg> -n <aks> --acr <acr>.azurecr.io # test pull from the nodesaz acr login -n <acr>az acr build -r <acr> -t <image>:<tag> . # build in Azure, no local Docker neededaz acr repository list -n <acr> -o tableaz acr repository show-tags -n <acr> --repository <image> --orderby time_desc -o table#Private Cluster and Run Command
Private cluster = API server only via private endpoint. Without a jump host or VPN use run command – it runs kubectl inside Azure:
az aks command invoke -g <rg> -n <aks> --command "kubectl get pods -A"az aks command invoke -g <rg> -n <aks> --command "kubectl apply -f deployment.yaml" --file deployment.yamlAPI server reachable only from certain IPs (public cluster): az aks update -g <rg> -n <aks> --api-server-authorized-ip-ranges <your-ip>/32.
#Costs: Stop and Start
Stop dev/test clusters and start them again.
az aks stop -g <rg> -n <aks> # dev/test: control plane + nodes stopped, no VM costsaz aks start -g <rg> -n <aks>az aks show -g <rg> -n <aks> --query powerState.code -o tsvaz aks nodepool scale -g <rg> --cluster-name <aks> -n user01 --node-count 0 # user pools can go to 0More savings: autoscaler with low min-count, spot node pools (--priority Spot) for interruptible jobs, right-size requests/limits. Cost view: Costs.
#Troubleshooting
Typical statuses, causes and fixes.
| Status / symptom | Cause | Fix |
|---|---|---|
ImagePullBackOff / ErrImagePull | wrong image/tag, no AcrPull, firewall blocks registry | kubectl describe pod, az aks check-acr, allow ACR/MCR FQDNs in the firewall |
CrashLoopBackOff | app crashes on start (config, secret, port) | kubectl logs --previous, check env/secrets |
Pending | no node with enough CPU/memory, taints, zone/PVC conflict | kubectl describe pod → events, scale pool / autoscaler |
OOMKilled | memory limit too low | raise resources.limits.memory, fix leak |
Node NotReady | node problem, network/DNS to API server | kubectl describe node, az aks nodepool upgrade --node-image-only, restart node via VMSS |
kubectl asks for device login / Unauthorized | kubeconfig not converted / no RBAC role | kubelogin convert-kubeconfig -l azurecli, assign AKS RBAC role |
| Upgrade hangs | PodDisruptionBudget blocks drain, too few IPs for surge | check PDBs (kubectl get pdb -A), lower max-surge or bigger subnet |
| Outbound from pods fails | UDR to firewall without required AKS FQDNs | allow AKS required FQDNs / FQDN tag AzureKubernetesService in Azure Firewall |
| Service type LoadBalancer gets no IP | subnet/permissions of the cluster identity | cluster identity needs Network Contributor on the subnet |
// Container Insights: errors in container logs of the last hourContainerLogV2| where TimeGenerated > ago(1h) and LogMessage has_any ("error", "exception", "fail")| project TimeGenerated, PodNamespace, PodName, ContainerName, LogMessage| order by TimeGenerated desc#Azure Virtual Desktop (AVD)
Top 5:az desktopvirtualization hostpool list·Get-AzWvdSessionHost·Get-AzWvdUserSession·Update-AzWvdSessionHost -AllowNewSession:$false·WVDConnections
#Building Blocks
Host pool, app group, workspace and how they belong together.
| Resource | Purpose |
|---|---|
| Host pool | group of session hosts (VMs); Pooled (multi-session) or Personal (1:1) |
| Session host | the VM with the AVD agent, registered to the host pool via token |
| Application group | Desktop or RemoteApp – users get access here |
| Workspace | what the user sees in the client; contains application groups |
| Scaling plan | starts/stops session hosts on a schedule (saves costs) |
| FSLogix | user profiles in an Azure Files share (profile container) |
Access: role Desktop Virtualization User on the application group. With Entra-joined session hosts additionally Virtual Machine User Login (or Administrator Login) on the VMs/RG.
#Create Host Pool, App Group, Workspace
Create all three and link them.
az extension add -n desktopvirtualization$exp = (Get-Date).ToUniversalTime().AddHours(24).ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ")az desktopvirtualization hostpool create -g <rg> -n vdpool-<name> -l westeurope ` --host-pool-type Pooled --load-balancer-type BreadthFirst --max-session-limit 10 ` --preferred-app-group-type Desktop --registration-info expiration-time=$exp registration-token-operation="Update"$hp = az desktopvirtualization hostpool show -g <rg> -n vdpool-<name> --query id -o tsvaz desktopvirtualization applicationgroup create -g <rg> -n vdag-<name>-desktop -l westeurope ` --host-pool-arm-path $hp --application-group-type Desktop$ag = az desktopvirtualization applicationgroup show -g <rg> -n vdag-<name>-desktop --query id -o tsvaz desktopvirtualization workspace create -g <rg> -n vdws-<name> -l westeurope --application-group-references $agaz role assignment create --assignee-object-id <group-object-id> --assignee-principal-type Group ` --role "Desktop Virtualization User" --scope $ag$hp = New-AzWvdHostPool -ResourceGroupName <rg> -Name vdpool-<name> -Location westeurope -HostPoolType Pooled ` -LoadBalancerType BreadthFirst -MaxSessionLimit 10 -PreferredAppGroupType Desktop$ag = New-AzWvdApplicationGroup -ResourceGroupName <rg> -Name vdag-<name>-desktop -Location westeurope ` -HostPoolArmPath $hp.Id -ApplicationGroupType DesktopNew-AzWvdWorkspace -ResourceGroupName <rg> -Name vdws-<name> -Location westeurope -ApplicationGroupReference $ag.IdNew-AzRoleAssignment -ObjectId <group-object-id> -RoleDefinitionName "Desktop Virtualization User" -Scope $ag.Id#Registration Token and Session Hosts
Create a token and add session hosts.
$exp = (Get-Date).ToUniversalTime().AddHours(24).ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ")az desktopvirtualization hostpool update -g <rg> -n vdpool-<name> ` --registration-info expiration-time=$exp registration-token-operation="Update"az desktopvirtualization hostpool retrieve-registration-token -g <rg> -n vdpool-<name> --query token -o tsvNew-AzWvdRegistrationInfo -ResourceGroupName <rg> -HostPoolName vdpool-<name> -ExpirationTime (Get-Date).ToUniversalTime().AddHours(24)(Get-AzWvdRegistrationInfo -ResourceGroupName <rg> -HostPoolName vdpool-<name>).TokenTip: macOS / Linux (Bash) – expiry as variable: macOS exp=$(date -u -v+24H +%Y-%m-%dT%H:%M:%S.0000000Z), Linux exp=$(date -u -d '+24 hours' +%Y-%m-%dT%H:%M:%S.0000000Z). Then expiration-time=$exp.
Add session hosts: portal → host pool → Session hosts → + Add (creates VMs, joins them to Entra ID/AD and registers them with the token). The token is valid for max. 30 days – renew it for new VMs.
#Daily Operation
Session hosts, user sessions and drain mode.
# Session hosts: status, sessions, drain modeGet-AzWvdSessionHost -ResourceGroupName <rg> -HostPoolName vdpool-<name> | Select-Object @{n='Host';e={($_.Name -split '/')[-1]}}, Status, Session, AllowNewSession, UpdateStateUpdate-AzWvdSessionHost -ResourceGroupName <rg> -HostPoolName vdpool-<name> -Name <host-fqdn> -AllowNewSession:$false # drain for maintenance # User sessions: list, send message, log offGet-AzWvdUserSession -ResourceGroupName <rg> -HostPoolName vdpool-<name> | Select-Object ActiveDirectoryUserName, SessionState, @{n='Host';e={($_.Name -split '/')[1]}}, @{n='Id';e={($_.Name -split '/')[-1]}}Send-AzWvdUserSessionMessage -ResourceGroupName <rg> -HostPoolName vdpool-<name> -SessionHostName <host-fqdn> -UserSessionId <id> ` -MessageTitle "Maintenance" -MessageBody "Please save your work – logoff in 10 minutes."Remove-AzWvdUserSession -ResourceGroupName <rg> -HostPoolName vdpool-<name> -SessionHostName <host-fqdn> -Id <id>#Network and Firewall
URLs, ports and RDP Shortpath.
- Session hosts need outbound access to the AVD service URLs (service tags
WindowsVirtualDesktop,AzureMonitor,AzureFrontDoor.Frontend, pluslogin.microsoftonline.com, KMSazkms.core.windows.net:1688). Check on the VM:C:\Program Files\Microsoft RDInfra\RDAgent_*\WVDAgentUrlTool.exe. - No inbound ports needed – connections go through the AVD gateway (reverse connect).
- RDP Shortpath (UDP 3390 managed networks / STUN for public networks) improves latency – allow UDP in NSG/firewall.
- FSLogix share: SMB 445 from the session hosts to the storage account (private endpoint recommended), Storage → Azure Files.
#Logs (KQL)
Diagnostic setting on host pool and workspace → Log Analytics.
// Connections per user / host, last 24 hWVDConnections| where TimeGenerated > ago(24h) and State == "Connected"| summarize Connections = count() by UserName, SessionHostName| order by Connections desc // ErrorsWVDErrors| where TimeGenerated > ago(24h)| summarize Count = count() by CodeSymbolic, Message| order by Count desc // Session host health check failuresWVDAgentHealthStatus| where TimeGenerated > ago(1h) and Status != "Available"| project TimeGenerated, SessionHostName, Status, SessionHostHealthCheckResult| Symptom | Cause | Fix |
|---|---|---|
| User sees no desktop in the client | role missing on the app group / app group not in workspace | Desktop Virtualization User, workspace reference |
| Host Unavailable | agent cannot reach the service URLs (firewall/proxy/DNS) | URL check tool, firewall rules |
| Login fails on Entra-joined hosts | Virtual Machine User Login missing / RDP property | role on the VM/RG, targetisaadjoined:i:1 in RDP properties |
| Profile does not load | FSLogix share not reachable / permissions | SMB 445, share and NTFS permissions |
| New VMs don't register | registration token expired | new token |
#Azure Arc (Servers)
Brings servers outside Azure (on-prem, other clouds) into Azure as resources: policy, Defender, Update Manager, monitoring, RBAC – with the Connected Machine agent (azcmagent).
Top 5:azcmagent show·azcmagent check·az connectedmachine list·az connectedmachine extension list·azcmagent logs
#Requirements
Network, permissions and supported systems.
| Item | Value |
|---|---|
| Outbound | HTTPS 443 to Azure (service tags AzureArcInfrastructure, AzureResourceManager, AzureActiveDirectory, GuestAndHybridManagement) – or via proxy / private link |
| Resource providers | Microsoft.HybridCompute, Microsoft.GuestConfiguration, Microsoft.HybridConnectivity, Microsoft.AzureArcData |
| Onboarding identity | service principal with role Azure Connected Machine Onboarding on the RG |
| OS | Windows Server 2012 R2+ (ESU), current Linux distributions |
foreach ($p in "Microsoft.HybridCompute","Microsoft.GuestConfiguration","Microsoft.HybridConnectivity","Microsoft.AzureArcData") { az provider register -n $p }az ad sp create-for-rbac -n sp-arc-onboarding --role "Azure Connected Machine Onboarding" --scopes /subscriptions/<sub-id>/resourceGroups/<rg-arc>#Onboard a Server
Portal: Azure Arc → Machines → Add/Create → Add a machine → generates the script (single server or at scale with service principal).
# after installing the agent (AzureConnectedMachineAgent.msi)& "$env:ProgramW6432\AzureConnectedMachineAgent\azcmagent.exe" connect ` --service-principal-id <app-id> --service-principal-secret <secret> ` --tenant-id <tenant-id> --subscription-id <sub-id> --resource-group <rg-arc> --location westeurope ` --tags "Environment=prd,Owner=<team>"wget https://aka.ms/azcmagent -O ~/install_linux_azcmagent.shsudo bash ~/install_linux_azcmagent.shsudo azcmagent connect --service-principal-id <app-id> --service-principal-secret <secret> \ --tenant-id <tenant-id> --subscription-id <sub-id> --resource-group <rg-arc> --location westeuropeBehind a proxy: azcmagent config set proxy.url "http://<proxy>:<port>" before connect. Never store the secret in scripts – short-lived secret or certificate, delete after onboarding.
#Agent on the Server
Status and checks with azcmagent.
azcmagent show # status, resource ID, last heartbeatazcmagent check # tests connectivity to all required endpointsazcmagent config listazcmagent logs # collects logs into a zip for supportazcmagent disconnect # remove from Azure (deletes the resource too)#In Azure
List and manage Arc machines and their extensions.
az extension add -n connectedmachineaz connectedmachine list -g <rg-arc> --query "[].{name:name, status:status, os:osName, version:agentVersion, lastSeen:lastStatusChange}" -o tableaz connectedmachine extension list -g <rg-arc> --machine-name <server> --query "[].{name:name, state:provisioningState, version:typeHandlerVersion}" -o table# Azure Monitor Agentaz connectedmachine extension create -g <rg-arc> --machine-name <server> -n AzureMonitorWindowsAgent ` --publisher Microsoft.Azure.Monitor --type AzureMonitorWindowsAgent -l westeurope# Run a command on the serveraz connectedmachine run-command create -g <rg-arc> --machine-name <server> -n rc-hostname -l westeurope --script "hostname"Get-AzConnectedMachine -ResourceGroupName <rg-arc> | Select-Object Name, Status, OSName, AgentVersion, LastStatusChangeGet-AzConnectedMachineExtension -ResourceGroupName <rg-arc> -MachineName <server> | Select-Object Name, ProvisioningStateNew-AzConnectedMachineExtension -ResourceGroupName <rg-arc> -MachineName <server> -Name AzureMonitorWindowsAgent ` -Publisher Microsoft.Azure.Monitor -ExtensionType AzureMonitorWindowsAgent -Location westeurope// All Arc servers with status and OSResources| where type =~ 'microsoft.hybridcompute/machines'| project name, resourceGroup, status = tostring(properties.status), os = tostring(properties.osName), agent = tostring(properties.agentVersion), lastSeen = todatetime(properties.lastStatusChange)| order by lastSeen ascLinux: AzureMonitorLinuxAgent. Data collection: data collection rule (DCR) and association to the machine.
#What Arc Enables
Functions you get once a server is connected.
| Function | Note |
|---|---|
| Update Manager | patching like Azure VMs – Update Manager |
| Defender for Servers | plan on the subscription covers Arc servers |
| Azure Policy / machine configuration | audit settings in the OS (e.g. baseline) |
| Monitoring | Azure Monitor Agent + DCR → Log Analytics |
| RBAC / SSH | az ssh arc – SSH via Azure without open port |
| ESU | extended security updates for Windows Server 2012 via Arc |
| Symptom | Cause | Fix |
|---|---|---|
| Status Disconnected | no heartbeat > 15 min (network, proxy, server off) | azcmagent check, proxy settings |
connect fails with 403 | SP role missing / provider not registered | onboarding role, register providers |
| Extension Failed | outbound to extension endpoints blocked | azcmagent check --extensions all, logs under C:\ProgramData\GuestConfig\extension_logs |
| Cloned VM shows wrong machine | agent was installed in the image | azcmagent disconnect in the image, onboard after cloning |
#Backup and Site Recovery
Top 5:az backup item list·az backup job list·az backup protection backup-now·az backup recoverypoint list·az backup restore restore-disks
#Which Vault for What?
Recovery Services vault versus Backup vault.
| Vault | Workloads |
|---|---|
Recovery Services vault (rsv) | Azure VMs, SQL/SAP HANA in VMs, Azure Files, MARS agent (on-prem), Site Recovery |
Backup vault (bvault) | Azure Disks, Blobs, PostgreSQL flexible, AKS |
Decide before the first backup: storage redundancy (LRS/ZRS/GRS – can't be changed later) and cross region restore (only with GRS).
#Create a Vault and Protect a VM
Form for vault + policy + list of VMs: web page → Generator → Backup → Protect VMs.
Note: az … create never creates the resource group – it must exist first (az group create), otherwise ResourceGroupNotFound.
az group create -n <rg> -l westeurope # only if the resource group does not exist yetaz backup vault create -g <rg> -n rsv-<name>-weu -l westeuropeaz backup vault backup-properties set -g <rg> -n rsv-<name>-weu --backup-storage-redundancy GeoRedundant --cross-region-restore-flag trueaz backup policy list -g <rg> --vault-name rsv-<name>-weu --query "[].name" -o tsvaz backup protection enable-for-vm -g <rg> --vault-name rsv-<name>-weu --vm <vm-name-or-id> --policy-name DefaultPolicyNew-AzResourceGroup -Name <rg> -Location westeurope # only if the resource group does not exist yet$v = New-AzRecoveryServicesVault -ResourceGroupName <rg> -Name rsv-<name>-weu -Location westeuropeSet-AzRecoveryServicesBackupProperty -Vault $v -BackupStorageRedundancy GeoRedundant$pol = Get-AzRecoveryServicesBackupProtectionPolicy -VaultId $v.ID -Name DefaultPolicyEnable-AzRecoveryServicesBackupProtection -VaultId $v.ID -Policy $pol -Name <vm> -ResourceGroupName <rg-vm>resource rsv 'Microsoft.RecoveryServices/vaults@2024-04-01' = { name: 'rsv-<name>-weu' location: location sku: { name: 'RS0', tier: 'Standard' } properties: { publicNetworkAccess: 'Enabled' }}resource "azurerm_recovery_services_vault" "this" { name = "rsv-<name>-weu" location = "westeurope" resource_group_name = "<rg>" sku = "Standard" storage_mode_type = "GeoRedundant" soft_delete_enabled = true}resource "azurerm_backup_protected_vm" "vm" { resource_group_name = "<rg>" recovery_vault_name = azurerm_recovery_services_vault.this.name source_vm_id = "<vm-id>" backup_policy_id = "<policy-id>"}The VM and the vault must be in the same region. Default policy: daily, 30 days retention – create your own policy for weekly/monthly retention.
#Back Up Now, Jobs, Recovery Points
Start a backup, watch jobs, list recovery points.
$v = "rsv-<name>-weu"; $rg = "<rg>"az backup item list -g $rg --vault-name $v --query "[].{vm:properties.friendlyName, state:properties.protectionState, last:properties.lastBackupTime, status:properties.lastBackupStatus}" -o tableaz backup protection backup-now -g $rg --vault-name $v --container-name <vm> --item-name <vm> ` --backup-management-type AzureIaasVM --retain-until (Get-Date).AddDays(30).ToString("dd-MM-yyyy") # macOS: $(date -v+30d +%d-%m-%Y) · Linux: $(date -d '+30 days' +%d-%m-%Y)az backup job list -g $rg --vault-name $v --query "[].{op:properties.operation, item:properties.entityFriendlyName, status:properties.status, start:properties.startTime}" -o tableaz backup recoverypoint list -g $rg --vault-name $v --container-name <vm> --item-name <vm> --backup-management-type AzureIaasVM --query "[].{name:name, time:properties.recoveryPointTime, type:properties.recoveryPointType}" -o table#Restore
The restore options and when to use which.
| Way | When |
|---|---|
| Restore disks | restore disks, then attach them or build a new VM (most flexible) |
| Create new VM | quick replacement in the same/another VNet (portal) |
| Replace existing | overwrite disks of the existing VM (VM is stopped) |
| File recovery | single files: mount script from the portal on any machine |
| Cross region restore | secondary region, only with GRS + CRR |
az storage account create -g $rg -n <sa-staging> -l westeurope --sku Standard_LRS # staging for restoreaz backup restore restore-disks -g $rg --vault-name $v --container-name <vm> --item-name <vm> ` --rp-name <recovery-point-name> --storage-account <sa-staging> --target-resource-group <rg-restore>az backup job list -g $rg --vault-name $v --operation Restore -o table#Protection Against Deletion and Ransomware
Soft delete, immutability and locks.
az backup vault backup-properties set -g $rg -n $v --soft-delete-feature-state AlwaysOn # cannot be turned off againaz backup vault update -g $rg -n $v --immutability-state Unlocked # Locked = irreversibleaz lock create -n lock-$v -g $rg --resource-type Microsoft.RecoveryServices/vaults --resource $v --lock-type CanNotDeleteAdditionally: Multi-user authorization (Resource Guard in another subscription) for critical operations, alerts for "backup disabled" and failed jobs.
#Site Recovery (DR to Another Region)
Set up in the portal: Recovery Services vault (in the target region!) → Site Recovery → Enable replication → source region/VMs → target RG, VNet, cache storage → replication policy.
| Step | Note |
|---|---|
| Target network | own VNet in the DR region, same address space possible (isolated) |
| Network mapping | source VNet → target VNet, IP addresses can be kept |
| Recovery plan | order of VMs (DB before app), scripts/automation runbooks |
| Test failover | regularly into an isolated test VNet – production keeps running |
| Failover / commit / re-protect | real disaster: failover → commit → re-protect for the way back |
az resource list -g <rg-asr> --resource-type Microsoft.RecoveryServices/vaults -o table# Replication status (PowerShell)$v = Get-AzRecoveryServicesVault -Name <rsv-dr> -ResourceGroupName <rg-asr>; Set-AzRecoveryServicesAsrVaultContext -Vault $vGet-AzRecoveryServicesAsrFabric | ForEach-Object { Get-AzRecoveryServicesAsrProtectionContainer -Fabric $_ } | ForEach-Object { Get-AzRecoveryServicesAsrReplicationProtectedItem -ProtectionContainer $_ } | Select-Object FriendlyName, ProtectionState, ReplicationHealth, TestFailoverState#Monitoring (KQL)
Diagnostic setting on the vault → Log Analytics (resource specific).
// Failed backup jobs, last 7 daysAddonAzureBackupJobs| where TimeGenerated > ago(7d) and JobOperation == "Backup" and JobStatus == "Failed"| project TimeGenerated, BackupItemUniqueId, JobFailureCode, JobDurationInSecs| order by TimeGenerated descEasier: Backup center / Business continuity center in the portal – all vaults, jobs and alerts across subscriptions.
#Azure Update Manager
Patch management for Azure VMs and Arc servers (Windows and Linux) – no agent needed, successor of Update Management (Automation).
Top 5:az vm assess-patches·az vm install-patches·az maintenance configuration create·az maintenance assignment create· Resource Graphpatchassessmentresources
#How It Works
Assessment, one-time install and schedules.
| Function | What it does |
|---|---|
| Assessment | checks which updates are missing (on demand or every 24 h with periodic assessment) |
| One-time update | install updates now |
| Schedule | maintenance configuration with window + classifications; assigned to VMs or dynamic scopes (by tag, RG, OS) |
| Hotpatch | updates without reboot (Windows Server Azure Edition) |
For schedules the VM needs patch orchestration Customer Managed Schedules (patchMode = AutomaticByPlatform + bypassPlatformSafetyChecksOnUserSchedule = true).
#Prepare VMs
Switch on periodic assessment and patch mode.
# Windows VM: periodic assessment + schedule-driven patchingaz vm update -g <rg> -n <vm> --set ` osProfile.windowsConfiguration.patchSettings.assessmentMode=AutomaticByPlatform ` osProfile.windowsConfiguration.patchSettings.patchMode=AutomaticByPlatform ` osProfile.windowsConfiguration.patchSettings.automaticByPlatformSettings.bypassPlatformSafetyChecksOnUserSchedule=true# Linux VMaz vm update -g <rg> -n <vm> --set ` osProfile.linuxConfiguration.patchSettings.assessmentMode=AutomaticByPlatform ` osProfile.linuxConfiguration.patchSettings.patchMode=AutomaticByPlatform ` osProfile.linuxConfiguration.patchSettings.automaticByPlatformSettings.bypassPlatformSafetyChecksOnUserSchedule=true// in the VM resource (Windows)osProfile: { windowsConfiguration: { patchSettings: { assessmentMode: 'AutomaticByPlatform' patchMode: 'AutomaticByPlatform' automaticByPlatformSettings: { bypassPlatformSafetyChecksOnUserSchedule: true } } }}resource "azurerm_windows_virtual_machine" "vm" { # ... patch_assessment_mode = "AutomaticByPlatform" patch_mode = "AutomaticByPlatform" bypass_platform_safety_checks_on_user_schedule_enabled = true}For many VMs: built-in policy "Configure periodic checking for missing system updates on azure virtual machines" and "Set prerequisites for scheduling recurring updates on Azure virtual machines".
#Assess and Install Now
Check missing updates and install them now.
az vm assess-patches -g <rg> -n <vm> --query "{status:status, critical:criticalAndSecurityPatchCount, other:otherPatchCount, reboot:rebootPending}"az vm install-patches -g <rg> -n <vm> --maximum-duration PT2H --reboot-setting IfRequired ` --classifications-to-include-win Critical Security --query "{status:status, installed:installedPatchCount, failed:failedPatchCount}"# Linux:az vm install-patches -g <rg> -n <vm> --maximum-duration PT2H --reboot-setting IfRequired --classifications-to-include-linux Critical Security#Schedule (Maintenance Configuration)
Create a patch schedule and assign VMs.
az maintenance configuration create -g <rg> --resource-name mc-patch-<name> -l westeurope ` --maintenance-scope InGuestPatch --maintenance-window-duration "03:00" ` --maintenance-window-start-date-time "2026-11-01 22:00" --maintenance-window-time-zone "W. Europe Standard Time" ` --maintenance-window-recur-every "Month Second Tuesday Offset3" ` --install-patches-windows-parameters classifications-to-include="Critical Security UpdateRollUp" ` --install-patches-linux-parameters classifications-to-include="Critical Security" ` --reboot-setting IfRequired --extension-properties InGuestPatchMode="User"$mc = az maintenance configuration show -g <rg> --resource-name mc-patch-<name> --query id -o tsvaz maintenance assignment create -g <rg-vm> -l westeurope --resource-name <vm> --resource-type virtualMachines ` --provider-name Microsoft.Compute --configuration-assignment-name mc-patch-<name> --maintenance-configuration-id $mc$mc = New-AzMaintenanceConfiguration -ResourceGroupName <rg> -Name mc-patch-<name> -Location westeurope ` -MaintenanceScope InGuestPatch -Duration "03:00" -StartDateTime "2026-11-01 22:00" -TimeZone "W. Europe Standard Time" ` -RecurEvery "Month Second Tuesday Offset3" -InstallPatchRebootSetting IfRequired ` -WindowParameterClassificationToInclude Critical, Security, UpdateRollUp ` -LinuxParameterClassificationToInclude Critical, Security -ExtensionProperty @{ InGuestPatchMode = "User" }New-AzConfigurationAssignment -ResourceGroupName <rg-vm> -Location westeurope -ResourceName <vm> ` -ResourceType VirtualMachines -ProviderName Microsoft.Compute -ConfigurationAssignmentName mc-patch-<name> ` -MaintenanceConfigurationId $mc.IdMonth Second Tuesday Offset3 = 3 days after Patch Tuesday. Other examples: Week Saturday, Month Last Sunday, Day. Dynamic scopes (all VMs with tag PatchGroup=A): portal → Update Manager → Machines → Schedule updates → Dynamic scopes.
#Compliance Overview (Resource Graph)
Machines with missing updates in one query.
// Machines with missing critical/security updatespatchassessmentresources| where type has "softwarepatches"| extend machine = tostring(split(id, '/')[8]), cls = tostring(properties.classifications[0])| where cls in ('Critical', 'Security')| summarize Missing = count() by machine, cls| order by Missing desc // Results of the last installation runspatchinstallationresources| where type has "patchinstallationresults"| project machine = tostring(split(id, '/')[8]), status = tostring(properties.status), start = todatetime(properties.startDateTime), installed = toint(properties.installedPatchCount), failed = toint(properties.failedPatchCount)| order by start descRun with az graph query -q "<query>", see Resource Graph.
| Symptom | Cause | Fix |
|---|---|---|
| Schedule does not run | patch orchestration not Customer Managed Schedules | prepare the VM (above) |
| No assessment data | periodic assessment off / VM stopped | assessmentMode=AutomaticByPlatform, az vm assess-patches |
| Linux: no updates found | repository not reachable (proxy/firewall) | OS repos outbound allowed |
| Window too short | many updates + reboot | longer duration (max. 3:55 h), fewer classifications |
#Storage in Daily Work
--auth-mode login needs a data role (e.g. Storage Blob Data Contributor). Owner/Contributor alone is not enough.
#AzCopy: Upload and Download
Install: see Install the Tools (Windows winget install Microsoft.Azure.AZCopy.10, macOS brew install azcopy).
azcopy login --tenant-id <tenant-id> # sign in with Entra IDazcopy copy ".\folder" "https://<sa>.blob.core.windows.net/<container>/" --recursiveazcopy copy "https://<sa>.blob.core.windows.net/<container>/path/*" ".\target" --recursiveazcopy sync ".\folder" "https://<sa>.blob.core.windows.net/<container>/folder" --delete-destination=false # changes onlyazcopy copy "https://<sa-a>.blob.core.windows.net/<c>/?<sas>" "https://<sa-b>.blob.core.windows.net/<c>/?<sas>" --recursive # account to accountazcopy list "https://<sa>.blob.core.windows.net/<container>"azcopy jobs list ; azcopy jobs resume <job-id> # resume an aborted transfer#Create a SAS Token
Time-limited access without handing out the account key.
# User delegation SAS (with Entra sign-in, without account key – preferred)$expiry = (Get-Date).ToUniversalTime().AddHours(8).ToString("yyyy-MM-ddTHH:mmZ")$sas = az storage container generate-sas --account-name <sa> -n <container> ` --permissions rl --expiry $expiry --auth-mode login --as-user -o tsv"https://<sa>.blob.core.windows.net/<container>?$sas" # For a single blobaz storage blob generate-sas --account-name <sa> -c <container> -n <file> ` --permissions r --expiry $expiry --auth-mode login --as-user --full-uri -o tsvTip: macOS / Linux (Bash) – the expiry as a variable: macOS expiry=$(date -u -v+8H +%Y-%m-%dT%H:%MZ), Linux expiry=$(date -u -d '+8 hours' +%Y-%m-%dT%H:%MZ). Then --expiry "$expiry". Permissions: r read · l list · w write · c create · d delete. Always as short and as little as possible.
#Restore Deleted and Old Versions
Undelete blobs and go back to a version.
# Show and restore deleted blobs (soft delete must be on)az storage blob list --account-name <sa> -c <container> --include d --auth-mode login ` --query "[?deleted].{name:name, deleted:properties.deletedTime}" -o tableaz storage blob undelete --account-name <sa> -c <container> -n <file> --auth-mode login # Versions of a blob (versioning must be on)az storage blob list --account-name <sa> -c <container> --prefix <file> --include v --auth-mode login ` --query "[].{name:name, version:versionId, current:isCurrentVersion}" -o table# Make an old version the current one againaz storage blob copy start --account-name <sa> --destination-container <container> --destination-blob <file> ` --source-uri "https://<sa>.blob.core.windows.net/<container>/<file>?versionid=<version-id>" --auth-mode login # Restore a deleted containeraz storage container list --account-name <sa> --include-deleted --auth-mode login --query "[?deleted]" -o tableaz storage container restore --account-name <sa> -n <container> --deleted-version <version> --auth-mode login#Turn On Protection
Soft delete, versioning and change feed.
az storage account blob-service-properties update -g <rg> --account-name <sa> ` --enable-delete-retention true --delete-retention-days 14 ` --enable-container-delete-retention true --container-delete-retention-days 14 ` --enable-versioning trueaz storage account update -g <rg> -n <sa> --allow-blob-public-access false --min-tls-version TLS1_2az storage account update -g <rg> -n <sa> --allow-shared-key-access false # Entra sign-in only (check first who uses keys!)az lock create -n lock-<sa> -g <rg> --resource-type Microsoft.Storage/storageAccounts --resource <sa> --lock-type CanNotDelete#Azure Files
Create a share, test port 445 and mount it.
az storage share-rm list -g <rg> --storage-account <sa> -o tableaz storage share-rm create -g <rg> --storage-account <sa> -n <share> --quota 100Test the connection and mount the share (port 445 must be open):
Test-NetConnection <sa>.file.core.windows.net -Port 445net use Z: \\<sa>.file.core.windows.net\<share> /user:localhost\<sa> <storage-key>nc -zv <sa>.file.core.windows.net 445# Finder: Go -> Connect to Server -> smb://<sa>.file.core.windows.net/<share> (user: <sa>, password: <storage-key>)nc -zv <sa>.file.core.windows.net 445sudo mkdir -p /mnt/<share>sudo mount -t cifs //<sa>.file.core.windows.net/<share> /mnt/<share> -o username=<sa>,password=<storage-key>,serverino,vers=3.0Many ISPs block port 445 to the internet – then use VPN/private endpoint.
#Lifecycle (Move/Delete Old Data Automatically)
policy.json:
{ "rules": [{ "name": "cleanup-logs", "enabled": true, "type": "Lifecycle", "definition": { "filters": { "blobTypes": [ "blockBlob" ], "prefixMatch": [ "logs/" ] }, "actions": { "baseBlob": { "tierToCool": { "daysAfterModificationGreaterThan": 30 }, "delete": { "daysAfterModificationGreaterThan": 365 } } } } }]}az storage account management-policy create -g <rg> --account-name <sa> --policy "@policy.json"az storage account management-policy show -g <rg> --account-name <sa>#Azure CLI – Key Vault, Identity, Roles, Policy
Top 5:az role assignment list --assignee <x> --all -o table·az ad signed-in-user show --query id -o tsv·az keyvault secret show·az policy assignment list·az identity show
#Key Vault
List vaults, check their settings and read or set secrets.
az keyvault list -o tableaz keyvault show -n <kv> --query "{rbac:properties.enableRbacAuthorization, pna:properties.publicNetworkAccess, softDelete:properties.enableSoftDelete}"az keyvault secret list --vault-name <kv> -o tableaz keyvault secret show --vault-name <kv> -n <name> --query value -o tsvaz keyvault secret set --vault-name <kv> -n <name> --value <value>az keyvault secret set --vault-name <kv> -n <name> --file <file> # value from a fileaz keyvault secret delete --vault-name <kv> -n <name>az keyvault secret list-deleted --vault-name <kv> -o tableaz keyvault secret recover --vault-name <kv> -n <name> # undo soft deleteaz keyvault certificate list --vault-name <kv> -o tableaz keyvault certificate import --vault-name <kv> -n <name> -f .\cert.pfx --password <pfx-password>az keyvault certificate show --vault-name <kv> -n <name> --query "{sid:sid, expires:attributes.expires}"# versionless secret ID: https://<kv>.vault.azure.net/secrets/<name>az keyvault certificate download --vault-name <kv> -n <name> -f cert.pem # public part onlyaz keyvault list-deleted -o tableaz keyvault purge -n <kv> # delete permanently (careful!)az keyvault update -n <kv> --bypass AzureServices # allow trusted servicesaz keyvault network-rule add -n <kv> --ip-address <ip>/32az keyvault network-rule list -n <kv>Error Forbidden … not authorized: on an RBAC vault the role Key Vault Secrets User/Officer is missing, on an access-policy vault an access policy.
#Create a Key Vault
Create a vault with RBAC, purge protection and private access.
az keyvault create -n <keyvault-name> -g <rg> -l <region> --sku standard ` --enable-rbac-authorization true --enable-purge-protection true --retention-days 90 --public-network-access Enabled$kvId = az keyvault show -n <keyvault-name> --query id -o tsv$me = az ad signed-in-user show --query id -o tsvaz role assignment create --assignee-object-id $me --assignee-principal-type User --role "Key Vault Administrator" --scope $kvIdPurge protection cannot be turned off again; after deletion the name stays blocked for the retention period.
#Generate a certificate in Key Vault
Create a self-signed or CA-issued certificate inside the vault.
# Self-signed (test) – PowerShell$policy = New-AzKeyVaultCertificatePolicy -SubjectName "CN=<fqdn>" -DnsName "<fqdn>" -IssuerName Self ` -ValidityInMonths 12 -SecretContentType "application/x-pkcs12" -KeyType RSA -KeySize 2048Add-AzKeyVaultCertificate -VaultName <keyvault-name> -Name <certificate-name> -CertificatePolicy $policyGet-AzKeyVaultCertificateOperation -VaultName <keyvault-name> -Name <certificate-name> # status # For your own CA: IssuerName Unknown -> get CSR -> have it signed -> mergeaz keyvault certificate pending show --vault-name <keyvault-name> -n <certificate-name> --query csr -o tsvaz keyvault certificate pending merge --vault-name <keyvault-name> -n <certificate-name> --file .\signed.cerEasier with a form: web page → Generator → Key Vault → Create a certificate (self-signed or CSR).
#Managed Identity
Create identities and show their principal and client ID.
az identity list -g <rg> -o tableaz identity create -g <rg> -n <identity>az identity show -g <rg> -n <identity> --query "{principal:principalId, client:clientId, id:id}"az vm identity show -g <rg> -n <vm>az webapp identity assign -g <rg> -n <app> --identities <identity-id>principalId = object ID for role assignments · clientId = for sign-in in code/pipelines.
#Roles (RBAC)
List, assign and remove role assignments on a scope.
az role assignment list --assignee <principal-id-or-upn> --all -o tableaz role assignment list --scope <resource-id> --include-inherited -o tableaz role assignment create --assignee-object-id <principal-id> --assignee-principal-type ServicePrincipal ` --role "<role>" --scope <resource-id>az role assignment delete --assignee <principal-id> --role "<role>" --scope <resource-id>az role definition list --name "<role>" -o jsonaz role definition list --custom-role-only true -o tableaz ad signed-in-user show --query id -o tsv # your own object ID # Service principal by display name -> object ID -> role$id = az ad sp list --display-name "<sp-name>" --query "[].{id:id}" --output tsvaz role assignment create --assignee-principal-type ServicePrincipal --assignee-object-id $id --scope "/subscriptions/<sub>" --role "Owner" # Scopes# /subscriptions/<sub>/resourceGroups/<rg> resource group# /subscriptions/<sub> subscription# /providers/Microsoft.Management/managementGroups/<mg-id> management group# / tenant root (everything – only with elevated access)az rest --method post --url "/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01" # Global Admin: get UAA on "/"az account management-group list -o table # management groups--assignee-object-id + --assignee-principal-type avoids Graph lookups and replication errors with freshly created identities.
| Role | For |
|---|---|
Reader | read only |
Contributor | everything except granting access |
Owner | everything incl. access |
User Access Administrator | granting access only |
Network Contributor | network resources |
Private DNS Zone Contributor | DNS zones/records |
Key Vault Secrets User | read secrets (RBAC vault) |
Key Vault Secrets Officer | manage secrets |
Key Vault Administrator | everything in the vault |
Key Vault Certificates Officer | manage certificates |
Storage Blob Data Reader/Contributor | blob data (e.g. Terraform state) |
Monitoring Reader | read metrics/logs |
#Entra ID (Users, Groups, Apps)
Look up users, groups and app registrations.
az ad user show --id <upn> --query "{id:id, name:displayName}"az ad group list --display-name <part> -o tableaz ad group member list -g <group> --query "[].userPrincipalName" -o tsvaz ad group member check -g <group> --member-id <object-id>az ad sp list --display-name <name> -o tableaz ad sp show --id <app-id> --query "{objectId:id, appId:appId}"az ad app list --display-name <name> -o tableaz ad app credential list --id <app-id> --query "[].{end:endDateTime, name:displayName}" -o table # secret expiryaz ad sp create-for-rbac -n <name> --role Contributor --scopes /subscriptions/<sub>More: Entra ID.
#Azure Policy
In detail – effects, custom definitions, remediation, exemptions, policy as code: Azure Policy.
az policy assignment list --scope /subscriptions/<sub> -o tableaz policy assignment list --disable-scope-strict-match --scope /subscriptions/<sub>/resourceGroups/<rg> -o tableaz policy assignment show -n <assignment> --scope <scope>az policy definition show -n <definition-name> --query "{name:displayName, effect:policyRule.then.effect}"az policy state list --resource <resource-id> --query "[].{policy:policyDefinitionName, compliant:complianceState}" -o tableaz policy state summarize -g <rg>az policy state trigger-scan -g <rg> --no-wait # re-evaluate complianceaz policy exemption list --scope <scope> -o tableaz policy exemption create -n <name> --policy-assignment <assignment-id> --scope <scope> --exemption-category WaiverError RequestDisallowedByPolicy: the message contains policyAssignmentId and policyDefinitionId – use them to search.
#Entra ID – Apps, Secrets, Groups, PIM
App registration = the definition of the application (app ID, secrets). Service principal = its "user account" in the tenant that roles are given to. For role assignments always use the object ID of the service principal, not the app's.
#App Registration and Service Principal
Find apps and their service principals and map the IDs.
az ad app list --display-name <name> --query "[].{name:displayName, appId:appId, objectId:id}" -o tableaz ad sp list --display-name <name> --query "[].{name:displayName, appId:appId, spObjectId:id}" -o tableaz ad sp show --id <app-id> --query id -o tsv # SP object ID from the app IDaz ad app owner list --id <app-id> --query "[].userPrincipalName" -o tsvaz ad app owner add --id <app-id> --owner-object-id <user-object-id> # New app with SP and role (secret is shown ONCE – store it in Key Vault right away)az ad sp create-for-rbac -n <name> --role Contributor --scopes /subscriptions/<sub-id>#Find Expiring Secrets and Certificates
Which secrets and certificates run out soon.
# One appaz ad app credential list --id <app-id> --query "[].{name:displayName, end:endDateTime, keyId:keyId}" -o tableaz ad app credential list --id <app-id> --cert --query "[].{name:displayName, end:endDateTime}" -o table # All apps whose secret expires within the next 30 days$limit = (Get-Date).AddDays(30)az ad app list --all --query "[].{name:displayName, appId:appId, secrets:passwordCredentials[].endDateTime}" -o json | ConvertFrom-Json | ForEach-Object { $app = $_ $app.secrets | Where-Object { $_ -and [datetime]$_ -lt $limit } | ForEach-Object { [pscustomobject]@{ App = $app.name; AppId = $app.appId; Expires = ([datetime]$_).ToString('yyyy-MM-dd') } } } | Sort-Object Expires | Format-TableWith --all this can take a while in large tenants; without --all only your own or the first 100.
#Renew a Secret
Add a new secret first, switch over, then remove the old one.
# Create the new secret IN ADDITION (--append), the old one stays valid for nowaz ad app credential reset --id <app-id> --append --display-name "secret-$(Get-Date -Format yyyyMM)" --years 1 --query password -o tsv # Bash: "secret-$(date +%Y%m)"# -> enter it right away in Key Vault / the service connection / pipeline variableaz keyvault secret set --vault-name <kv> -n <secret-name> --value "<new-secret>" # Once everything is switched over: delete the old secretaz ad app credential list --id <app-id> -o tableaz ad app credential delete --id <app-id> --key-id <old-key-id>Where is the secret used? Typically: service connections in DevOps, pipeline variables, Key Vault, app settings.
#Groups
Create groups and manage their members.
az ad group list --display-name <part> --query "[].{name:displayName, id:id}" -o tableaz ad group create --display-name <name> --mail-nickname <name>az ad group member list -g <group> --query "[].{name:displayName, upn:userPrincipalName}" -o tableaz ad group member add -g <group> --member-id <object-id>az ad group member remove -g <group> --member-id <object-id>az ad group member check -g <group> --member-id <object-id> --query valueaz ad user get-member-groups --id <upn> --query "[].displayName" -o tsv # a user's groups#Create Security Groups in Bulk
One group per role, existing ones are skipped – safe to run again. With a form (incl. role assignments and Azure CLI variant): web page → Generator → Identity and Access → Create Entra ID security groups (bulk).
# Azure Cloud Shell (PowerShell) or locally after Connect-AzAccount$groups = @( @{ Name = "az-<customer>-owner"; Desc = "<Customer> - Owner" } @{ Name = "az-<customer>-contributor"; Desc = "<Customer> - Contributor" } @{ Name = "az-<customer>-reader"; Desc = "<Customer> - Reader" } @{ Name = "az-<customer>-costmanagementreader"; Desc = "<Customer> - Cost Management Reader" } @{ Name = "az-<customer>-landingzones-owner"; Desc = "<Customer> - Landing Zones Owner" } @{ Name = "az-<customer>-landingzones-contributor"; Desc = "<Customer> - Landing Zones Contributor" } @{ Name = "az-<customer>-landingzones-reader"; Desc = "<Customer> - Landing Zones Reader" }) foreach ($g in $groups) { $existing = Get-AzADGroup -DisplayName $g.Name -ErrorAction SilentlyContinue if ($existing) { Write-Host "Already exists: $($g.Name) (Id: $($existing.Id))" -ForegroundColor Yellow continue } try { $new = New-AzADGroup -DisplayName $g.Name -MailNickname $g.Name -Description $g.Desc -SecurityEnabled -ErrorAction Stop Write-Host "Created: $($g.Name) (Id: $($new.Id))" -ForegroundColor Green } catch { Write-Host "Error for $($g.Name): $($_.Exception.Message)" -ForegroundColor Red }} # OverviewGet-AzADGroup -Filter "startswith(displayName,'az-<customer>')" | Select-Object DisplayName, Id, SecurityEnabled | Format-Table -AutoSizeThen give each group its role, e.g. New-AzRoleAssignment -ObjectId <group-id> -RoleDefinitionName "Reader" -Scope /subscriptions/<sub-id> – or use the generator option Also assign Azure roles. Needs the Entra permission to create groups (e.g. Groups Administrator, or "Users can create security groups" enabled).
#Users
Look up users and guests.
az ad user show --id <upn> --query "{name:displayName, id:id, enabled:accountEnabled}"az ad user list --filter "startswith(displayName,'<part>')" --query "[].{name:displayName, upn:userPrincipalName}" -o tableaz ad user list --filter "userType eq 'Guest'" --query "[].{name:displayName, mail:mail}" -o table # guests#PIM: Activate a Role
Privileged Identity Management: the role is only "eligible" and must be activated before working.
In the portal: Microsoft Entra Privileged Identity Management → My roles → Azure resources or Microsoft Entra roles → Activate → duration + justification.
PowerShell (Azure resource roles):
$scope = "/subscriptions/<sub-id>"$me = (Get-AzADUser -SignedIn).IdGet-AzRoleEligibilitySchedule -Scope $scope -Filter "asTarget()" | Select-Object RoleDefinitionDisplayName, Scope, RoleDefinitionId # what may I activate? $roleId = "<RoleDefinitionId from the list>"New-AzRoleAssignmentScheduleRequest -Name (New-Guid).Guid -Scope $scope ` -PrincipalId $me -RoleDefinitionId $roleId -RequestType SelfActivate ` -ScheduleInfoStartDateTime (Get-Date).ToUniversalTime().ToString("o") ` -ExpirationType AfterDuration -ExpirationDuration PT8H ` -Justification "<reason / ticket>"After activation sign in again if needed (az account clear ; az login) so the role is in the token.
#Defender for Cloud and Security
#Secure Score and Recommendations
Current score and the open recommendations.
az security secure-scores list --query "[].{name:displayName, current:score.current, max:score.max, percent:score.percentage}" -o tableaz security secure-score-controls list --query "[].{name:displayName, current:score.current, max:score.max, open:unhealthyResourceCount}" -o tableaz security assessment list --query "[?status.code=='Unhealthy'].{recommendation:displayName, resource:resourceDetails.Id}" -o tableIn the portal: Microsoft Defender for Cloud → Recommendations, sort by Risk level, per recommendation Fix or Exempt.
#Defender Plans
Which plans are on and how to switch them (costs money).
az security pricing list --query "value[].{plan:name, tier:pricingTier}" -o tableaz security pricing show -n VirtualMachinesaz security pricing create -n KeyVaults --tier Standard # enable a plan (costs money)Free = recommendations only, Standard = Defender plan with protection and alerts.
#Alerts
List and close security alerts.
az security alert list --query "[].{time:timeGeneratedUtc, severity:severity, title:alertDisplayName, resource:compromisedEntity, status:status}" -o tableaz security alert update --location <region> -n <alert-name> -g <rg> --status Dismiss # close after reviewIn Log Analytics: table SecurityAlert.
#Just-in-Time Access to VMs
JIT opens RDP/SSH only on request, for a limited time and only for your IP.
Portal: VM → Connect → Request access, or Defender for Cloud → Workload protections → Just-in-time VM access.
PowerShell:
$vmId = (Get-AzVM -ResourceGroupName <rg> -Name <vm>).Id$myIp = Invoke-RestMethod https://api.ipify.org$req = @{ id = $vmId; ports = @(@{ number = 22; endTimeUtc = (Get-Date).ToUniversalTime().AddHours(3).ToString("o"); allowedSourceAddressPrefix = @($myIp) }) }Start-AzJitNetworkAccessPolicy -ResourceGroupName <rg> -Location <region> -Name default -VirtualMachine @($req)#Quick Check of a Subscription
Score, plans and alerts of one subscription in one go.
$sub = "<sub-id>"az account set --subscription $subaz security secure-scores list --query "[0].score.percentage" -o tsv # secure score in %az security pricing list --query "value[?pricingTier=='Free'].name" -o tsv # plans that are offaz graph query --subscriptions $sub -q "Resources | where type =~ 'microsoft.network/networksecuritygroups' | mv-expand r = properties.securityRules | where r.properties.access == 'Allow' and r.properties.direction == 'Inbound' and tostring(r.properties.sourceAddressPrefix) in ('*','Internet') | project name, rule = tostring(r.name), port = tostring(r.properties.destinationPortRange)" -o tableaz graph query --subscriptions $sub -q "Resources | where type =~ 'microsoft.storage/storageaccounts' and properties.allowBlobPublicAccess == true | project name" -o tableaz keyvault list -o json | ConvertFrom-Json | Where-Object { -not $_.properties.enablePurgeProtection } | ForEach-Object name # vaults without purge protection#Management Groups and Landing Zone
#Landing Zone Structure
Typical hierarchy according to the Cloud Adoption Framework (your names may differ):
Tenant Root Group (/)└── <Company> ├── Platform │ ├── Identity – domain controllers, Entra helper services │ ├── Management – Log Analytics, Automation, monitoring │ └── Connectivity – hub VNet, firewall/FortiGate, VPN, DNS ├── Landing Zones │ ├── Corp – applications connected to the corporate network │ └── Online – applications directly from the internet ├── Sandbox – playground, no connectivity └── Decommissioned – retired subscriptions| Rule | Why |
|---|---|
| Assign policies and permissions on management groups where possible | apply automatically to every new subscription below |
| One subscription per application/environment | clean boundaries for costs, permissions, limits |
| Network (hub) lives in Connectivity, spokes in the landing zones | separation platform ↔ application |
| Never put anything directly under the Tenant Root Group | root policies would hit everything |
#Show the Hierarchy
The management group tree with subscriptions.
az account management-group list -o tableaz account management-group show -n <mg-id> --expand --recurse ` --query "{name:displayName, children:children[].{name:displayName, type:type}}" -o jsonaz account management-group entities list --query "[].{name:displayName, type:type, parent:parent.id}" -o table # everything flatResource Graph: which subscription sits where?
az graph query -q "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | project name, subscriptionId, mgChain = properties.managementGroupAncestorsChain" -o json#Move a Subscription
Move a subscription – inherited policies and roles change at once.
az account management-group subscription show-sub-under-mg -n <mg-id> -o table # subscriptions under an MGaz account management-group subscription add --name <target-mg-id> --subscription <sub-id>Caution: moving changes inherited policies and roles immediately. Check first which policies apply at the target (deny policies can break existing deployments). You need permissions on the old and the new MG.
#Rename Management Groups
Only the display name can be changed – the ID (name) is fixed for good. Policy assignments, role assignments and subscriptions are not affected by a new display name.
Update-AzManagementGroup -GroupName <mg-id> -DisplayName "<new-display-name>"# Replace a name part in many display names – first only show old → new:$old = '<old>' ; $new = '<new>'$pattern = "(?<=^|-)$([regex]::Escape($old))(?=-|$)" # only whole parts between hyphensGet-AzManagementGroup | Where-Object { $_.DisplayName -match $pattern } | Select-Object Name, DisplayName, @{n='NewName'; e={ $_.DisplayName -replace $pattern, $new }}az account management-group update -n <mg-id> --display-name "<new-display-name>"az account management-group list --query "sort_by([], &displayName)[].{id:name, name:displayName}" -o tableMany at once with a dry run and confirmation: web page → Generator → Governance → Rename management groups.
#What Applies at Which Level?
Policies and roles that apply on a management group.
$mg = "/providers/Microsoft.Management/managementGroups/<mg-id>"az policy assignment list --scope $mg --query "[].{name:displayName, effect:parameters.effect.value, def:policyDefinitionId}" -o tableaz role assignment list --scope $mg --query "[].{who:principalName, role:roleDefinitionName}" -o table # Everything that affects a subscription (incl. inheritance from above)az policy assignment list --disable-scope-strict-match --scope /subscriptions/<sub-id> -o tableaz role assignment list --scope /subscriptions/<sub-id> --include-inherited -o table#Deployments at Management Group Level
what-if and deploy at management group scope.
az deployment mg what-if -m <mg-id> -l <region> -f main.bicep -p params/mg.bicepparamaz deployment mg create -m <mg-id> -l <region> -f main.bicep -p params/mg.bicepparamaz deployment mg list -m <mg-id> -o tableaz deployment tenant create -l <region> -f tenant.bicep # at the very top (needs permissions on "/")In Bicep: targetScope = 'managementGroup' at the start of the file.
#Azure Policy – Rules, Assignments and Policy as Code
Azure Policy checks every create/update request against rules (deny, audit, modify, deploy if not exists) and shows the compliance of existing resources. Definitions, initiatives and assignments belong in a repo and are deployed via pipeline (policy as code).
Top 5:az policy assignment list·az policy state summarize·az policy state trigger-scan·az policy remediation create·az policy exemption create
#Terms in One Table
Definition, initiative, assignment, scope, exemption.
| Term | Meaning |
|---|---|
| Definition | one rule: if (condition) → then (effect); built-in or custom |
| Initiative (policy set) | group of definitions, assigned together (e.g. tagging, security baseline) |
| Assignment | definition/initiative applied to a scope (management group, subscription, resource group) with parameters |
| Scope / exclusion | assignment applies to everything below the scope, except notScopes |
| Exemption | single resource / RG exempted from an assignment – with reason and expiry date |
| Enforcement mode | Default = effect active · DoNotEnforce = only evaluate (ideal for testing) |
| Mode | All = all resource types incl. RGs · Indexed = only types with tags/location (for tag/location rules) |
| Alias | path to a resource property in the rule, e.g. Microsoft.Network/publicIPAddresses/sku.name |
| Remediation | task that fixes existing resources (only Modify and DeployIfNotExists) |
Note: Where to define: custom definitions and initiatives at the highest management group they are needed in. Assign as high as sensible, exclude with notScopes or exemptions.
#Effects
What each effect does and when to use it.
| Effect | What happens | Typical use |
|---|---|---|
Deny | request is rejected (RequestDisallowedByPolicy) | allowed regions, no public IPs, allowed SKUs |
Audit | resource is created, marked non-compliant | first stage before Deny, reporting |
Modify | adds/changes tags or properties during the request; remediation for existing ones | inherit tags from the RG, disable public network access |
Append | adds fields (older, mostly replaced by Modify) | IP rules on storage |
DeployIfNotExists | deploys a related resource after the request (ARM template) | diagnostic settings, private DNS records for private endpoints, backup |
AuditIfNotExists | reports if the related resource is missing | "VM without backup", "no diagnostic setting" |
DenyAction | blocks an action (currently DELETE) | protect critical resources from deletion |
Manual | compliance is set by hand (attestation) | regulatory controls without technical check |
Disabled | rule inactive | switch off via parameter without removing the assignment |
Tip: Make the effect a parameter ([parameters('effect')] with allowedValues) – then the same definition runs as Audit in test and Deny in production.
Evaluation order and timing
Order during a request: Disabled → Append/Modify → Deny → Audit; after the deployment: AuditIfNotExists / DeployIfNotExists. Existing resources are evaluated about 30 minutes after a new assignment, then every 24 hours – or on demand with az policy state trigger-scan.
#Find Built-in Policies and Aliases
Look up built-in definitions and property aliases.
# Built-in definitions by name (displayName)az policy definition list --query "[?policyType=='BuiltIn' && contains(displayName, 'location')].{name:name, displayName:displayName}" -o tableaz policy set-definition list --query "[?policyType=='BuiltIn' && contains(displayName, 'tag')].{name:name, displayName:displayName}" -o table# Show the rule and parameters of one definitionaz policy definition show -n <definition-name> --query "{display:displayName, mode:mode, params:parameters, rule:policyRule}"# Aliases of a resource type (which properties a rule can check)az provider show --namespace Microsoft.Network --expand "resourceTypes/aliases" ` --query "resourceTypes[?resourceType=='publicIPAddresses'].aliases[].name" -o tsvGet-AzPolicyDefinition -Builtin | Where-Object DisplayName -like "*location*" | Select-Object Name, DisplayNameGet-AzPolicySetDefinition -Builtin | Where-Object DisplayName -like "*tag*" | Select-Object Name, DisplayName(Get-AzPolicyAlias -NamespaceMatch Microsoft.Network -ResourceTypeMatch publicIPAddresses).Aliases.NameTip: Built-in IDs: /providers/Microsoft.Authorization/policyDefinitions/<name> – always look up the name by display name instead of copying GUIDs from blog posts. Browse/compare: portal Policy → Definitions or the AzAdvertizer website.
#Assign a Policy
Example: built-in Allowed locations on a management group, first only evaluating (DoNotEnforce), with a message for the user. Faster with a form: web page → Generator → Governance → Assign an Azure Policy (CLI or PowerShell, incl. identity, roles and remediation).
$mg = "<mg-landingzones>"$def = az policy definition list --query "[?displayName=='Allowed locations'].name" -o tsvaz policy assignment create -n allowed-locations --display-name "Allowed locations" ` --policy $def --scope /providers/Microsoft.Management/managementGroups/$mg ` --params '{ \"listOfAllowedLocations\": { \"value\": [ \"westeurope\", \"northeurope\" ] } }' ` --enforcement-mode DoNotEnforceaz policy assignment non-compliance-message create -n allowed-locations ` --scope /providers/Microsoft.Management/managementGroups/$mg --message "Only West/North Europe allowed – see wiki <link>"# later: enforceaz policy assignment update -n allowed-locations --scope /providers/Microsoft.Management/managementGroups/$mg --enforcement-mode Default$mg = Get-AzManagementGroup -GroupName <mg-landingzones>$def = Get-AzPolicyDefinition -Builtin | Where-Object DisplayName -eq "Allowed locations"New-AzPolicyAssignment -Name allowed-locations -DisplayName "Allowed locations" -PolicyDefinition $def -Scope $mg.Id ` -PolicyParameterObject @{ listOfAllowedLocations = @("westeurope", "northeurope") } -EnforcementMode DoNotEnforce ` -NonComplianceMessage @{ Message = "Only West/North Europe allowed – see wiki <link>" }Update-AzPolicyAssignment -Name allowed-locations -Scope $mg.Id -EnforcementMode DefaulttargetScope = 'managementGroup' param allowedLocations array = [ 'westeurope', 'northeurope' ]param enforce bool = false resource assign 'Microsoft.Authorization/policyAssignments@2024-04-01' = { name: 'allowed-locations' properties: { displayName: 'Allowed locations' policyDefinitionId: tenantResourceId('Microsoft.Authorization/policyDefinitions', '<builtin-definition-name>') enforcementMode: enforce ? 'Default' : 'DoNotEnforce' parameters: { listOfAllowedLocations: { value: allowedLocations } } nonComplianceMessages: [ { message: 'Only West/North Europe allowed – see wiki <link>' } ] }}data "azurerm_policy_definition" "allowed_locations" { display_name = "Allowed locations"}resource "azurerm_management_group_policy_assignment" "allowed_locations" { name = "allowed-locations" display_name = "Allowed locations" management_group_id = "/providers/Microsoft.Management/managementGroups/<mg-landingzones>" policy_definition_id = data.azurerm_policy_definition.allowed_locations.id enforce = false parameters = jsonencode({ listOfAllowedLocations = { value = ["westeurope", "northeurope"] } }) non_compliance_message { content = "Only West/North Europe allowed – see wiki <link>" }}Caution: Assignment names: max. 24 characters at management group scope. Also allow global in the location list for global resources (Front Door, DNS, Traffic Manager).
#Write Your Own Policy
Example: deny public IP addresses except in resource groups with the tag AllowPublicIp=true, effect as parameter.
{ "properties": { "displayName": "Deny public IP addresses (unless RG tag AllowPublicIp=true)", "policyType": "Custom", "mode": "All", "metadata": { "category": "Network", "version": "1.0.0" }, "parameters": { "effect": { "type": "String", "allowedValues": [ "Audit", "Deny", "Disabled" ], "defaultValue": "Audit" } }, "policyRule": { "if": { "allOf": [ { "field": "type", "equals": "Microsoft.Network/publicIPAddresses" }, { "value": "[resourceGroup().tags['AllowPublicIp']]", "notEquals": "true" } ] }, "then": { "effect": "[parameters('effect')]" } } }}# rules.json = only the "policyRule" object, params.json = only the "parameters" objectaz policy definition create -n deny-public-ip --display-name "Deny public IP addresses (unless RG tag AllowPublicIp=true)" ` --mode All --rules rules.json --params params.json --metadata category=Network version=1.0.0 ` --management-group <mg-root>New-AzPolicyDefinition -Name deny-public-ip -DisplayName "Deny public IP addresses (unless RG tag AllowPublicIp=true)" ` -Policy .\deny-public-ip.json -Mode All -ManagementGroupName <mg-root> # whole file with "properties" works tootargetScope = 'managementGroup' resource def 'Microsoft.Authorization/policyDefinitions@2023-04-01' = { name: 'deny-public-ip' properties: { displayName: 'Deny public IP addresses (unless RG tag AllowPublicIp=true)' policyType: 'Custom' mode: 'All' metadata: { category: 'Network', version: '1.0.0' } parameters: { effect: { type: 'String', allowedValues: [ 'Audit', 'Deny', 'Disabled' ], defaultValue: 'Audit' } } policyRule: loadJsonContent('definitions/deny-public-ip.rule.json') }}resource "azurerm_policy_definition" "deny_public_ip" { name = "deny-public-ip" display_name = "Deny public IP addresses (unless RG tag AllowPublicIp=true)" policy_type = "Custom" mode = "All" management_group_id = "/providers/Microsoft.Management/managementGroups/<mg-root>" metadata = jsonencode({ category = "Network", version = "1.0.0" }) parameters = file("${path.module}/definitions/deny-public-ip.params.json") policy_rule = file("${path.module}/definitions/deny-public-ip.rule.json")}Useful conditions: equals / notEquals · in / notIn · like / notLike (wildcard *) · contains · exists · greaterOrEquals · count (for arrays, e.g. NSG rules with * source) – logic with allOf / anyOf / not.
#Initiative (Policy Set)
Bundle related rules – one assignment, one compliance view, shared parameters.
# definitions.json: [ { "policyDefinitionId": "...", "policyDefinitionReferenceId": "requireTagEnv", "parameters": { ... } }, ... ]az policy set-definition create -n tagging --display-name "Tagging baseline" ` --definitions definitions.json --params params.json --management-group <mg-root>az policy assignment create -n tagging --policy-set-definition tagging ` --scope /providers/Microsoft.Management/managementGroups/<mg-landingzones> ` --mi-system-assigned --location westeuroperesource "azurerm_policy_set_definition" "tagging" { name = "tagging" display_name = "Tagging baseline" policy_type = "Custom" management_group_id = "/providers/Microsoft.Management/managementGroups/<mg-root>" parameters = jsonencode({ tagName = { type = "String", defaultValue = "Environment" } }) policy_definition_reference { reference_id = "requireTagOnRg" policy_definition_id = data.azurerm_policy_definition.require_tag_rg.id parameter_values = jsonencode({ tagName = { value = "[parameters('tagName')]" } }) } policy_definition_reference { reference_id = "inheritTag" policy_definition_id = data.azurerm_policy_definition.inherit_tag.id parameter_values = jsonencode({ tagName = { value = "[parameters('tagName')]" } }) }}Typical tagging initiative from built-ins: Require a tag on resource groups (Deny) + Inherit a tag from the resource group if missing (Modify) – resources then get the tag automatically. Tag standard: Naming and Tags.
#Modify and DeployIfNotExists: Identity and Remediation
Modify and DeployIfNotExists change resources – the assignment needs a managed identity with the roles from roleDefinitionIds in the rule (e.g. Tag Contributor, Contributor, Network Contributor).
"then": { "effect": "modify", "details": { "roleDefinitionIds": [ "/providers/Microsoft.Authorization/roleDefinitions/4a9ae827-6dc8-4573-8ac7-8239d42aa03f" ], "operations": [ { "operation": "addOrReplace", "field": "[concat('tags[', parameters('tagName'), ']')]", "value": "[resourceGroup().tags[parameters('tagName')]]" } ] }}$scope = "/providers/Microsoft.Management/managementGroups/<mg-landingzones>"# identity + role in one step (role from roleDefinitionIds, at the assignment scope)az policy assignment create -n inherit-env-tag --policy <definition-name> --scope $scope ` --params '{ \"tagName\": { \"value\": \"Environment\" } }' ` --mi-system-assigned --location westeurope --role "Tag Contributor" --identity-scope $scope# Fix existing resources (new and updated ones are handled automatically)$aid = az policy assignment show -n inherit-env-tag --scope $scope --query id -o tsvaz policy remediation create -n rem-inherit-env-tag --policy-assignment $aid --management-group <mg-landingzones># subscription/RG scope can re-evaluate first: --resource-discovery-mode ReEvaluateCompliance (not at management group scope)az policy remediation list --management-group <mg-landingzones> --query "[].{name:name, state:provisioningState, ok:deploymentStatus.successfulDeployments, failed:deploymentStatus.failedDeployments}" -o table# For initiatives: --definition-reference-id <referenceId>$scope = "/providers/Microsoft.Management/managementGroups/<mg-landingzones>"$def = Get-AzPolicyDefinition -Name <definition-name> -ManagementGroupName <mg-root>$a = New-AzPolicyAssignment -Name inherit-env-tag -PolicyDefinition $def -Scope $scope ` -PolicyParameterObject @{ tagName = "Environment" } -IdentityType SystemAssigned -Location westeuropeNew-AzRoleAssignment -ObjectId $a.IdentityPrincipalId -RoleDefinitionName "Tag Contributor" -Scope $scopeStart-AzPolicyRemediation -Name rem-inherit-env-tag -PolicyAssignmentId $a.Id -ManagementGroupName <mg-landingzones>resource assign 'Microsoft.Authorization/policyAssignments@2024-04-01' = { name: 'inherit-env-tag' location: 'westeurope' identity: { type: 'SystemAssigned' } properties: { policyDefinitionId: managementGroupResourceId('Microsoft.Authorization/policyDefinitions', 'inherit-tag') parameters: { tagName: { value: 'Environment' } } }}resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(assign.id, 'tag-contributor') properties: { principalId: assign.identity.principalId principalType: 'ServicePrincipal' roleDefinitionId: tenantResourceId('Microsoft.Authorization/roleDefinitions', '4a9ae827-6dc8-4573-8ac7-8239d42aa03f') }}Built-in roles used most: Tag Contributor 4a9ae827-…, Contributor b24988ac-…, Network Contributor, Log Analytics Contributor, Monitoring Contributor. A remediation without the role ends with AuthorizationFailed per resource.
#Exemptions
Exempt a resource with reason and expiry date.
az policy exemption create -n exm-<rg>-public-ip --policy-assignment <assignment-id> ` --scope /subscriptions/<sub-id>/resourceGroups/<rg> --exemption-category Waiver ` --expires-on 2026-12-31T00:00:00Z --description "Ticket <ticket>: temporary public IP for vendor test"az policy exemption list --scope /subscriptions/<sub-id> --query "[].{name:name, category:exemptionCategory, expires:expiresOn}" -o table$a = Get-AzPolicyAssignment -Name <assignment> -Scope <assignment-scope>New-AzPolicyExemption -Name exm-<rg>-public-ip -PolicyAssignment $a -Scope "/subscriptions/<sub-id>/resourceGroups/<rg>" ` -ExemptionCategory Waiver -ExpiresOn (Get-Date "2026-12-31") -Description "Ticket <ticket>: temporary public IP for vendor test"Waiver = accepted risk · Mitigated = requirement met another way. For initiatives exempt only single rules with --policy-definition-reference-ids. Always set an expiry date and a ticket reference.
#Compliance and Reports
Compliance summary and list of non-compliant resources.
az policy state summarize --management-group <mg-landingzones> --query "value[0].results"az policy state summarize -g <rg> --query "value[0].policyAssignments[].{assignment:policyAssignmentId, nonCompliant:results.nonCompliantResources}" -o tableaz policy state list -g <rg> --filter "complianceState eq 'NonCompliant'" ` --query "[].{resource:resourceId, policy:policyDefinitionName, ref:policyDefinitionReferenceId}" -o tableaz policy state trigger-scan -g <rg> --no-wait # re-evaluate now (takes a few minutes)Get-AzPolicyStateSummary -ManagementGroupName <mg-landingzones>Get-AzPolicyState -ResourceGroupName <rg> -Filter "ComplianceState eq 'NonCompliant'" | Select-Object ResourceId, PolicyDefinitionNameStart-AzPolicyComplianceScan -ResourceGroupName <rg> -AsJob// Non-compliant resources per assignment and rule – across all subscriptionspolicyresources| where type == "microsoft.policyinsights/policystates"| where properties.complianceState == "NonCompliant"| extend assignment = tostring(properties.policyAssignmentName), rule = tostring(properties.policyDefinitionReferenceId), definition = tostring(properties.policyDefinitionName), resource = tostring(properties.resourceId)| summarize resources = dcount(resource) by assignment, definition, rule| order by resources descRun with az graph query -q "<query>", see Resource Graph. Portal: Policy → Compliance – filter by scope, Non-compliant, click a rule → Resource compliance → Details shows the failing field.
#Policy as Code: Repo and Pipeline
Keep policies in a repo and deploy them by pipeline.
policy/ definitions/ one JSON per custom policy (name = file name) initiatives/ initiatives with reference IDs assignments/ per scope: mg-<org>-platform.json, mg-<org>-landingzones.json, … exemptions/ with expiry date and ticket main.bicep (or Terraform / EPAC) – deploys everything at management group scopepipelines/policy.ymlFeature branch → pull request (review by a second person).
Pipeline: validate + what-if against the management group.
New rules first as DoNotEnforce or Audit – check compliance for some days.
Switch to Deny / Default in a second pull request, announced to the teams.
Caution: Do not change anything in the portal – the repo is the source of truth, portal changes are overwritten.
Pipeline file (pipelines/policy.yml)
# pipelines/policy.yml (excerpt) – service connection with "Resource Policy Contributor" on the root management groupsteps:- task: AzureCLI@2 displayName: What-if inputs: azureSubscription: <service-connection> scriptType: pscore scriptLocation: inlineScript inlineScript: az deployment mg what-if --management-group-id <mg-root> -l westeurope -f policy/main.bicep- task: AzureCLI@2 displayName: Deploy condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) inputs: azureSubscription: <service-connection> scriptType: pscore scriptLocation: inlineScript inlineScript: az deployment mg create --management-group-id <mg-root> -l westeurope -f policy/main.bicepLarger estates, roles
For larger estates: EPAC (Enterprise Policy as Code, Microsoft open-source PowerShell module) – plans and deploys definitions, assignments and exemptions from folders and also removes what is no longer in the repo. Landing zone baseline policies: the Azure Landing Zones (ALZ) policy set.
Roles: Resource Policy Contributor for definitions/assignments, plus User Access Administrator (or a role with roleAssignments/write) when assignments with managed identity get their roles from the pipeline.
#Troubleshooting
Find the blocking assignment and fix common problems.
# Which assignment blocks? The error contains policyAssignmentId/policyDefinitionId – or search:az policy assignment list --disable-scope-strict-match --scope /subscriptions/<sub-id>/resourceGroups/<rg> ` --query "[].{name:name, display:displayName, enforce:enforcementMode, scope:scope}" -o table# Activity log: policy denials of the last 24 haz monitor activity-log list --offset 24h --query "[?category.value=='Policy' && contains(operationName.value,'deny')].{time:eventTimestamp, caller:caller, resource:resourceId}" -o table| Symptom | Cause | Fix |
|---|---|---|
RequestDisallowedByPolicy | a Deny rule matches | read assignment/definition from the error, adjust deployment or create an exemption |
| New assignment shows no compliance data | first evaluation takes ~30 min | wait or az policy state trigger-scan |
| Remediation Failed | identity missing the role / role at wrong scope | role from roleDefinitionIds at the assignment scope |
Modify does not tag existing resources | Modify only acts on create/update | start a remediation task |
| Tag rule ignores some resources | mode Indexed skips types without tags | All only if needed – check the target type |
| Custom definition not selectable | defined at a lower management group / other subscription | define at the root (or higher) management group |
| Location policy blocks Front Door / DNS | global resources have location global | add global to the allowed list |
| Assignment name too long | management group scope: max. 24 characters | shorter name, use displayName for the long text |
| Portal change overwritten | pipeline deployed the repo state again | change in the repo via pull request |
#Monitoring and Alerts
#Action Group (Who Gets Notified)
Create the group that sends mail or webhooks.
$rg = "<rg-monitoring>"az monitor action-group create -g $rg -n ag-net-team --short-name netteam ` --action email team <team@company.tld>az monitor action-group update -g $rg -n ag-net-team --add-action email oncall <oncall@company.tld>az monitor action-group update -g $rg -n ag-net-team --add-action webhook teams "<teams-workflow-url>"az monitor action-group show -g $rg -n ag-net-team --query id -o tsvTeams: in the channel Workflows → "Post to a channel when a webhook request is received" → enter the URL as webhook (or use the channel's e-mail address). --short-name max. 12 characters (appears in SMS/mail).
#Metric Alerts
Alerts on CPU, availability and other metrics.
$ag = az monitor action-group show -g <rg-monitoring> -n ag-net-team --query id -o tsv # VM not available$vmId = az vm show -g <rg> -n <vm> --query id -o tsvaz monitor metrics alert create -g <rg-monitoring> -n "vm-<vm>-down" --scopes $vmId ` --condition "avg VmAvailabilityMetric < 1" --window-size 5m --evaluation-frequency 1m ` --severity 1 --action $ag --description "VM <vm> not available" # VM CPU constantly highaz monitor metrics alert create -g <rg-monitoring> -n "vm-<vm>-cpu" --scopes $vmId ` --condition "avg Percentage CPU > 90" --window-size 15m --evaluation-frequency 5m --severity 2 --action $ag # App Gateway: backend unhealthy$agwId = az network application-gateway show -g <rg> -n <agw> --query id -o tsvaz monitor metrics alert create -g <rg-monitoring> -n "agw-<agw>-unhealthy" --scopes $agwId ` --condition "max UnhealthyHostCount > 0" --window-size 5m --evaluation-frequency 1m --severity 1 --action $ag # Load balancer: health probe fails (e.g. FortiGate ILB)$lbId = az network lb show -g <rg> -n <lb> --query id -o tsvaz monitor metrics alert create -g <rg-monitoring> -n "lb-<lb>-probe" --scopes $lbId ` --condition "avg DipAvailability < 100" --window-size 5m --evaluation-frequency 1m --severity 2 --action $ag # VPN tunnel down$conId = az network vpn-connection show -g <rg> -n <conn> --query id -o tsvaz monitor metrics alert create -g <rg-monitoring> -n "vpn-<conn>-down" --scopes $conId ` --condition "max TunnelIngressBytes < 1" --window-size 15m --evaluation-frequency 5m --severity 2 --action $agWhich metrics exist? az monitor metrics list-definitions --resource <id> --query "[].name.value" -o tsv
#Activity Log Alerts
React to changes in Azure – e.g. when the FortiGate SDN connector rewrites routes during a failover.
$ag = az monitor action-group show -g <rg-monitoring> -n ag-net-team --query id -o tsv$sub = "/subscriptions/<sub-id>" # Route changed (FortiGate failover via SDN connector, or someone touched a UDR)az monitor activity-log alert create -g <rg-monitoring> -n "route-changed" --scope $sub ` --condition category=Administrative and operationName=Microsoft.Network/routeTables/routes/write ` --action-group $ag # NSG rule changedaz monitor activity-log alert create -g <rg-monitoring> -n "nsg-rule-changed" --scope $sub ` --condition category=Administrative and operationName=Microsoft.Network/networkSecurityGroups/securityRules/write ` --action-group $ag # Service Health (Microsoft outages)az monitor activity-log alert create -g <rg-monitoring> -n "service-health" --scope $sub ` --condition category=ServiceHealth --action-group $ag#Log Alerts (KQL)
Easiest in the portal: Monitor → Alerts → Create → Alert rule → scope = Log Analytics workspace → Custom log search → paste KQL.
// FortiGate reports an HA change (Syslog/CEF)CommonSecurityLog| where DeviceVendor == "Fortinet"| where Activity has "HA" and (Activity has "state" or Message has "failover") // WAF suddenly blocks a lot (> 100 in 5 min)AzureDiagnostics| where Category == "ApplicationGatewayFirewallLog" and action_s == "Blocked"| summarize Count = count() by bin(TimeGenerated, 5m)| where Count > 100 // VM no longer sends a heartbeatHeartbeat| summarize last = max(TimeGenerated) by Computer| where last < ago(10m)Condition: Number of results > 0, evaluated every 5 min, period 10–15 min. Check the field names of the FortiGate logs first with CommonSecurityLog | take 20.
#Resource Health
Resource health status and alerts for it.
az resource show --ids <resource-id>/providers/Microsoft.ResourceHealth/availabilityStatuses/current ` --api-version 2022-10-01 --query "properties.{status:availabilityState, reason:summary}"az monitor activity-log alert create -g <rg-monitoring> -n "resource-health" --scope /subscriptions/<sub-id> ` --condition category=ResourceHealth --action-group $ag#Manage Alerts
List, disable and change existing alerts.
az monitor metrics alert list -g <rg-monitoring> --query "[].{name:name, enabled:enabled, severity:severity}" -o tableaz monitor activity-log alert list -g <rg-monitoring> -o tableaz monitor metrics alert update -g <rg-monitoring> -n <alert> --enabled false # temporarily off (maintenance)az monitor alert-processing-rule create -g <rg-monitoring> -n maintenance-<date> ` --scopes /subscriptions/<sub-id>/resourceGroups/<rg> --rule-type RemoveAllActionGroups ` --schedule-start-datetime "2026-11-01 22:00:00" --schedule-end-datetime "2026-11-02 02:00:00" --schedule-time-zone "W. Europe Standard Time"The alert processing rule suppresses notifications during a maintenance window without deleting alerts.
| Severity | Meaning |
|---|---|
| 0 | critical |
| 1 | error |
| 2 | warning |
| 3 | informational |
| 4 | verbose |
#Resource Graph – Queries Across All Subscriptions
Resource Graph searches all subscriptions you have read access to – in seconds. The query language is KQL.
#How to Run
Install the extension and run queries across subscriptions.
az extension add -n resource-graphaz graph query -q "<query>" --first 1000 -o tableaz graph query -q "<query>" --management-groups <mg-id> -o table # only below one management groupaz graph query -q "<query>" --subscriptions <sub-id> -o table # Long queries: into a variable (PowerShell here-string)$q = @"Resources| where type =~ 'microsoft.compute/virtualmachines'| project name, resourceGroup"@az graph query -q $q -o table # Result as CSVaz graph query -q $q --first 1000 --query data -o json | ConvertFrom-Json | Export-Csv .\result.csv -NoTypeInformation -Encoding UTF8 # PowerShell moduleSearch-AzGraph -Query $q -First 1000 | Format-TableIn the portal: Resource Graph Explorer – paste the query, run it, save as chart/dashboard. Always use single quotes inside the queries so they fit into PowerShell's "…".
#Inventory
Count and list resources by type, size and state.
// Count per resource typeResources | summarize Count = count() by type | order by Count desc // All VMs with size and stateResources| where type =~ 'microsoft.compute/virtualmachines'| project name, resourceGroup, subscriptionId, location, size = tostring(properties.hardwareProfile.vmSize), state = tostring(properties.extended.instanceView.powerState.code), zone = tostring(zones[0]) // Resources per subscription with the subscription nameResources| join kind=inner (ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, subName = name) on subscriptionId| summarize Count = count() by subName // Application gateways with SKU and stateResources| where type =~ 'microsoft.network/applicationgateways'| project name, resourceGroup, sku = tostring(properties.sku.name), state = tostring(properties.operationalState) // Key vaults with network accessResources| where type =~ 'microsoft.keyvault/vaults'| project name, resourceGroup, rbac = properties.enableRbacAuthorization, publicAccess = tostring(properties.publicNetworkAccess)#Network
VNets, peerings, subnets and public IPs across subscriptions.
// VNets with address space and number of peeringsResources| where type =~ 'microsoft.network/virtualnetworks'| project name, resourceGroup, space = properties.addressSpace.addressPrefixes, peerings = array_length(properties.virtualNetworkPeerings) // All subnets with NSG and route tableResources| where type =~ 'microsoft.network/virtualnetworks'| mv-expand s = properties.subnets| project vnet = name, subnet = tostring(s.name), prefix = tostring(s.properties.addressPrefix), nsg = tostring(split(s.properties.networkSecurityGroup.id, '/')[8]), routeTable = tostring(split(s.properties.routeTable.id, '/')[8]) // Subnets WITHOUT an NSGResources| where type =~ 'microsoft.network/virtualnetworks'| mv-expand s = properties.subnets| where isnull(s.properties.networkSecurityGroup) and tostring(s.name) !in ('GatewaySubnet', 'AzureFirewallSubnet', 'AzureBastionSubnet', 'RouteServerSubnet')| project vnet = name, subnet = tostring(s.name), resourceGroup // Peerings that are not connectedResources| where type =~ 'microsoft.network/virtualnetworks'| mv-expand p = properties.virtualNetworkPeerings| where tostring(p.properties.peeringState) != 'Connected'| project vnet = name, peering = tostring(p.name), state = tostring(p.properties.peeringState) // All public IPs and what they are attached toResources| where type =~ 'microsoft.network/publicipaddresses'| project name, ip = tostring(properties.ipAddress), sku = tostring(sku.name), attachedTo = tostring(split(properties.ipConfiguration.id, '/')[8]), resourceGroup // Private endpoints and their targetsResources| where type =~ 'microsoft.network/privateendpoints'| mv-expand c = properties.privateLinkServiceConnections| project name, resourceGroup, target = tostring(split(c.properties.privateLinkServiceId, '/')[8]), groupId = tostring(c.properties.groupIds[0]), status = tostring(c.properties.privateLinkServiceConnectionState.status) // All routes with next hopResources| where type =~ 'microsoft.network/routetables'| mv-expand r = properties.routes| project routeTable = name, route = tostring(r.name), prefix = tostring(r.properties.addressPrefix), nextHop = tostring(r.properties.nextHopType), ip = tostring(r.properties.nextHopIpAddress) // Who owns this private IP?Resources| where type =~ 'microsoft.network/networkinterfaces'| mv-expand ipc = properties.ipConfigurations| project nic = name, ip = tostring(ipc.properties.privateIPAddress), vm = tostring(split(properties.virtualMachine.id, '/')[8]), resourceGroup| where ip == '<ip>'#Security
Open NSG rules, public storage and other exposed resources.
// NSG rules that allow SSH/RDP/everything from the internetResources| where type =~ 'microsoft.network/networksecuritygroups'| mv-expand r = properties.securityRules| where r.properties.direction == 'Inbound' and r.properties.access == 'Allow'| where tostring(r.properties.sourceAddressPrefix) in ('*', 'Internet', '0.0.0.0/0')| where tostring(r.properties.destinationPortRange) in ('22', '3389', '*')| project nsg = name, rule = tostring(r.name), port = tostring(r.properties.destinationPortRange), resourceGroup // Storage accounts with public accessResources| where type =~ 'microsoft.storage/storageaccounts'| project name, resourceGroup, publicNetwork = tostring(properties.publicNetworkAccess), blobPublic = properties.allowBlobPublicAccess, minTls = tostring(properties.minimumTlsVersion) // Open Defender recommendations per topicSecurityResources| where type == 'microsoft.security/assessments'| where properties.status.code == 'Unhealthy'| summarize Count = count() by recommendation = tostring(properties.displayName)| order by Count desc#Cleanup and Costs
Unattached disks, unused IPs and other orphans.
// Disks without a VMResources| where type =~ 'microsoft.compute/disks' and properties.diskState =~ 'Unattached'| project name, resourceGroup, sizeGB = properties.diskSizeGB, sku = tostring(sku.name) // Unassigned public IPsResources| where type =~ 'microsoft.network/publicipaddresses'| where isnull(properties.ipConfiguration) and isnull(properties.natGateway)| project name, resourceGroup, ip = tostring(properties.ipAddress) // NICs without VM and without private endpointResources| where type =~ 'microsoft.network/networkinterfaces'| where isnull(properties.virtualMachine) and isnull(properties.privateEndpoint)| project name, resourceGroup // Stopped but not deallocated VMs (still cost money)Resources| where type =~ 'microsoft.compute/virtualmachines'| where properties.extended.instanceView.powerState.code == 'PowerState/stopped'| project name, resourceGroup // Empty resource groupsResourceContainers| where type =~ 'microsoft.resources/subscriptions/resourcegroups'| join kind=leftouter (Resources | summarize n = count() by resourceGroup, subscriptionId) on resourceGroup, subscriptionId| where isnull(n)| project resourceGroup, subscriptionId#Tags
Resources missing a required tag.
// Resources without tag "Environment"Resources| where isnull(tags['Environment']) or tostring(tags['Environment']) == ''| project name, type, resourceGroup // Resource groups without ownerResourceContainers| where type =~ 'microsoft.resources/subscriptions/resourcegroups'| where isnull(tags['Owner'])| project name, subscriptionId // Values used for a tagResources| summarize Count = count() by env = tostring(tags['Environment'])Tag names are case-sensitive in queries – mind your spelling.
#Changes, Permissions, Policy
What changed recently, who has access, what is non-compliant.
// What changed in the last 24 h?ResourceChanges| extend time = todatetime(properties.changeAttributes.timestamp), kind = tostring(properties.changeType), id = tostring(properties.targetResourceId)| where time > ago(1d)| project time, kind, id| order by time desc // Role assignments at subscription levelAuthorizationResources| where type =~ 'microsoft.authorization/roleassignments'| extend scope = tostring(properties.scope), principal = tostring(properties.principalId), type = tostring(properties.principalType), role = tostring(split(properties.roleDefinitionId, '/')[-1])| where scope startswith '/subscriptions/' and scope !contains 'resourceGroups'| project scope, principal, type, role // Non-compliant resources per policyPolicyResources| where type =~ 'microsoft.policyinsights/policystates'| where properties.complianceState =~ 'NonCompliant'| summarize Count = count() by policy = tostring(properties.policyDefinitionName)| order by Count desc // Subscriptions with their management groupResourceContainers| where type =~ 'microsoft.resources/subscriptions'| project name, subscriptionId, mg = tostring(properties.managementGroupAncestorsChain[0].displayName)role is the role ID – its name: az role definition list --name <id> --query "[0].roleName" -o tsv.
#Network Workflows
Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.
#Connect a new spoke to the hub
VNet, subnet, peering in both directions and a route table via the firewall – in four steps.
Variables first – run once, then step by step:
$hubRg = "<rg-hub>" ; $hubVnet = "<vnet-hub>"$rg = "<rg-spoke>" ; $vnet = "<vnet-spoke>" ; $loc = "<region>"$space = "10.1.0.0/24" ; $snet = "snet-app" ; $snetPrefix = "10.1.0.0/26"$nvaIp = "<ilb-ip-of-firewall>"VNet and subnet
az group create -n $rg -l $locaz network vnet create -g $rg -n $vnet -l $loc --address-prefixes $spaceaz network vnet subnet create -g $rg --vnet-name $vnet -n $snet --address-prefixes $snetPrefix --default-outbound falsePeering in both directions
$hubId = az network vnet show -g $hubRg -n $hubVnet --query id -o tsv$spokeId = az network vnet show -g $rg -n $vnet --query id -o tsvaz network vnet peering create -g $rg --vnet-name $vnet -n "$vnet-to-$hubVnet" --remote-vnet $hubId --allow-vnet-access --allow-forwarded-trafficaz network vnet peering create -g $hubRg --vnet-name $hubVnet -n "$hubVnet-to-$vnet" --remote-vnet $spokeId --allow-vnet-access --allow-forwarded-trafficTip: With a VPN/ER gateway in the hub additionally: hub side --allow-gateway-transit, spoke side --use-remote-gateways.
Route table: everything via the firewall
az network route-table create -g $rg -n "rt-$vnet" -l $loc --disable-bgp-route-propagation trueaz network route-table route create -g $rg --route-table-name "rt-$vnet" -n default-to-nva ` --address-prefix 0.0.0.0/0 --next-hop-type VirtualAppliance --next-hop-ip-address $nvaIpaz network vnet subnet update -g $rg --vnet-name $vnet -n $snet --route-table "rt-$vnet"Check – both peerings must be Connected:
az network vnet peering list -g $rg --vnet-name $vnet --query "[].{name:name, state:peeringState, sync:peeringSyncLevel}" -o tableNote: Next on the FortiGate: route and policy for the spoke. With a VPN gateway also add a GatewaySubnet UDR for the spoke prefix → NVA.
#Extend address space and sync peering
Add an address prefix to a VNet and bring all peerings back to FullyInSync.
$rg = "<rg>" ; $vnet = "<vnet>"az network vnet update -g $rg -n $vnet --address-prefixes <existing-prefix> <new-prefix>az network vnet peering list -g $rg --vnet-name $vnet --query "[].{name:name, sync:peeringSyncLevel}" -o tableaz network vnet peering sync -g $rg --vnet-name $vnet -n <peering> # for each peering, status -> FullyInSyncRun sync on the remote side (hub) too, and add the new prefixes to UDRs/firewall routes.
#NSG: temporarily allow your own IP
Open ports for your current public IP only, then remove the rule again.
$rg = "<rg>" ; $nsg = "<nsg>"$myIp = Invoke-RestMethod https://api.ipify.orgaz network nsg rule create -g $rg --nsg-name $nsg -n Temp-Allow-MyIp --priority 110 ` --direction Inbound --access Allow --protocol Tcp ` --source-address-prefixes "$myIp/32" --destination-port-ranges 22 443# … work …az network nsg rule delete -g $rg --nsg-name $nsg -n Temp-Allow-MyIprg="<rg>"; nsg="<nsg>"myIp=$(curl -s https://api.ipify.org)az network nsg rule create -g $rg --nsg-name $nsg -n Temp-Allow-MyIp --priority 110 \ --direction Inbound --access Allow --protocol Tcp \ --source-address-prefixes "$myIp/32" --destination-port-ranges 22 443# … work …az network nsg rule delete -g $rg --nsg-name $nsg -n Temp-Allow-MyIp#VM cannot reach the internet
Check effective routes, next hop, NSG and a live test from the VM – in that order.
$rg = "<rg>" ; $vm = "<vm>" ; $nic = "<nic>"az network nic show-effective-route-table -g $rg -n $nic -o table # 0.0.0.0/0 -> where?az network watcher show-next-hop -g $rg --vm $vm --source-ip <vm-ip> --dest-ip 8.8.8.8 -o tableaz network nic list-effective-nsg -g $rg -n $nic --query "value[].effectiveSecurityRules[?direction=='Outbound'].{name:name, access:access, prio:priority}"az vm run-command invoke -g $rg -n $vm --command-id RunShellScript --scripts "curl -s -m 5 ifconfig.me; echo"Then on the firewall: trace a packet.
#Create a private endpoint for a web app
Private endpoint, private DNS zone, VNet link and DNS zone group – then switch off public access.
$rg = "<rg>" ; $app = "<webapp>" ; $vnet = "<vnet>" ; $snet = "<snet-pe>"$rgDns = "<rg-dns>" ; $zone = "privatelink.azurewebsites.net"$dnsVnetId = "<vnet-id-that-resolves-dns>"Create the private endpoint
$appId = az webapp show -g $rg -n $app --query id -o tsvaz network private-endpoint create -g $rg -n "pe-$app" --vnet-name $vnet --subnet $snet ` --private-connection-resource-id $appId --group-id sites --connection-name "pe-$app"Private DNS zone and link (zone once per environment)
az network private-dns zone create -g $rgDns -n $zoneaz network private-dns link vnet create -g $rgDns -z $zone -n "link-$vnet" -v $dnsVnetId -e false$zoneId = az network private-dns zone show -g $rgDns -n $zone --query id -o tsvaz network private-endpoint dns-zone-group create -g $rg --endpoint-name "pe-$app" -n default ` --private-dns-zone $zoneId --zone-name websitesDisable public access and check
az webapp update -g $rg -n $app --set publicNetworkAccess=Disablednslookup "$app.azurewebsites.net" # from the VNet: private IP expectedTip: Other services: zone and --group-id from the table.
#Private endpoint does not resolve privately
Find out why the name still resolves to a public IP: DNS server, zone link, A record.
nslookup <name>.privatelink.<zone> # or the public name, e.g. <app>.azurewebsites.netaz network vnet show -g <rg> -n <vnet> --query dhcpOptions.dnsServers # empty = Azure DNSaz network private-dns link vnet list -g <rg-dns> -z <zone> --query "[].{name:name, vnet:virtualNetwork.id}" -o tableaz network private-dns record-set a list -g <rg-dns> -z <zone> --query "[].{name:name, ip:aRecords[0].ipv4Address}" -o tableaz network private-endpoint show -g <rg> -n <pe> --query "customDnsConfigs"Must match: the zone is linked to the VNet whose DNS server does the resolution, and the A record points to the PE IP.
#Check a VPN connection
Status, BGP peers, learned routes and shared key of a VPN gateway connection.
$rg = "<rg>" ; $gw = "<vpn-gateway>" ; $conn = "<connection>"az network vpn-connection show -g $rg -n $conn --query "{status:connectionStatus, in:ingressBytesTransferred, out:egressBytesTransferred}"az network vnet-gateway list-bgp-peer-status -g $rg -n $gw -o tableaz network vnet-gateway list-learned-routes -g $rg -n $gw -o tableaz network vpn-connection shared-key show -g $rg --connection-name $conn # compare PSK with the remote sideRemote side FortiGate: debug an IPsec tunnel.
#Reach the FortiGate GUI or a VM without public IP via Bastion
Tunnel through Bastion for HTTPS, SSH or RDP to a machine without public IP.
$rg = "<rg-bastion>" ; $bastion = "<bastion>"$vmId = az vm show -g <rg-vm> -n <vm> --query id -o tsv# GUI (HTTPS) -> then in the browser https://localhost:8443az network bastion tunnel -g $rg -n $bastion --target-resource-id $vmId --resource-port 443 --port 8443# SSH -> in a second window: ssh -p 50022 <admin>@127.0.0.1az network bastion tunnel -g $rg -n $bastion --target-resource-id $vmId --resource-port 22 --port 50022# RDP directlyaz network bastion rdp -g $rg -n $bastion --target-resource-id $vmIdRequires Bastion Standard with Native client support. Keep the tunnel window open.
#FortiGate Workflows
Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.
#FortiGate: apply and check the license
Upload the license, wait for the reboot and verify the status.
execute vm-license <FortiFlex-token> # the FortiGate reboots afterwards# after the rebootget system status | grep -i -E "version|serial|license"diagnose debug vm-print-licenseexecute update-nowdiagnose autoupdate versions | grep -A3 "AV Engine"With HA: on both units separately (secondary via execute ha manage <index> <admin>).
#FortiGate: let a spoke reach the internet
Route, address object, firewall policy and source NAT for a new spoke.
Address object for the spoke
config firewall address edit "net-<spoke>" set subnet 10.1.0.0 255.255.255.0 nextendStatic route to the spoke
config router static edit 0 set dst 10.1.0.0 255.255.255.0 set gateway <first-ip-of-port2-subnet> # .1 of the internal subnet set device "port2" nextendPolicy with source NAT
config firewall policy edit 0 set name "<spoke>-to-internet" set srcintf "port2" set dstintf "port1" set srcaddr "net-<spoke>" set dstaddr "all" set action accept set schedule "always" set service "HTTP" "HTTPS" "DNS" set nat enable set logtraffic all nextendTest from the spoke: curl -s ifconfig.me → must show the firewall's public IP.
#FortiGate: allow spoke-to-spoke
Policy between two spokes – both need the UDR to the firewall.
config firewall policy edit 0 set name "<spoke-a>-to-<spoke-b>" set srcintf "port2" set dstintf "port2" set srcaddr "net-<spoke-a>" set dstaddr "net-<spoke-b>" set action accept set schedule "always" set service "<services>" set nat disable set logtraffic all nextendBoth spokes need the UDR to the firewall, otherwise routing is asymmetric.
#FortiGate: port forwarding from outside (VIP)
Virtual IP and policy for an inbound port, plus load balancer rule and NSG in Azure.
Virtual IP
config firewall vip edit "vip-<service>" set extip <private-ip-of-port1> # or LB frontend IP with floating IP set extintf "port1" set portforward enable set mappedip "<internal-server-ip>" set extport 443 set mappedport 443 nextendInbound policy
config firewall policy edit 0 set name "in-<service>" set srcintf "port1" set dstintf "port2" set srcaddr "all" set dstaddr "vip-<service>" set action accept set schedule "always" set service "HTTPS" set logtraffic all nextendIn Azure additionally: a load balancer rule for the port and an open NSG on port1.
Test with the sniffer: diagnose sniffer packet any 'port 443' 4 20.
#FortiGate: trace a packet
Sniffer for arrival and exit, debug flow for the drop reason, then check route and policy.
Does it arrive and does it leave?
diagnose sniffer packet any 'host <ip>' 4 20Why is it dropped?
diagnose debug resetdiagnose debug flow filter cleardiagnose debug flow filter addr <ip>diagnose debug flow show function-name enablediagnose debug flow trace start 10diagnose debug enable# (generate traffic)diagnose debug disablediagnose debug resetCross-check route and policy
get router info routing-table details <dest-ip>show firewall policy | grep -i -A20 <name>Interpreting messages: debug flow table.
#FortiGate: test HA failover
Continuous test from a spoke client, trigger the failover and measure the interruption.
Start a continuous test on a client in the spoke:
while ($true) { "$((Get-Date).ToString('HH:mm:ss')) $(curl.exe -s -m 2 https://api.ipify.org)"; Start-Sleep 1 }while true; do echo "$(date +%T) $(curl -s -m 2 https://api.ipify.org)"; sleep 1; doneOn the primary FortiGate:
get system ha statusdiagnose sys ha checksum cluster # in sync before?execute ha failover set 1 # trigger failover# … watch the continuous test (interruption in seconds, possibly a new public IP)execute ha failover unset 1diagnose sys ha reset-uptime # with override disable: hand back the role#FortiGate: HA out of sync
Compare the checksums, find the area that differs and resync.
diagnose sys ha checksum cluster # compare on bothdiagnose sys ha checksum show # which area differs?execute ha manage <index> <admin> # switch to the secondaryexecute ha synchronize startdiagnose sys ha checksum recalculate # if still differentget system ha status#FortiGate: debug an IPsec tunnel
Trigger the negotiation, read the IKE debug and check SAs and counters.
get vpn ipsec tunnel summarydiagnose vpn ike log filter cleardiagnose vpn ike log filter rem-addr4 <peer-ip>diagnose debug application ike -1diagnose debug console timestamp enablediagnose debug enablediagnose vpn tunnel up <phase2> <phase1> # trigger negotiation# … read the output …diagnose debug disablediagnose debug resetdiagnose vpn tunnel list name <phase1> # SAs/counters once it is upMessages: IPsec table.
#FortiGate: back up the configuration
Back up the configuration via CLI or GUI – never into the repo.
config system global set admin-scp enableendscp <admin>@<mgmt-ip>:sys_config ".\fgt-backup-$(Get-Date -Format yyyyMMdd).conf"scp <admin>@<mgmt-ip>:sys_config "./fgt-backup-$(date +%Y%m%d).conf"Or GUI → admin menu → Configuration → Backup. Backups contain hashes/secrets – not into the repo.
#Azure Network Resources
Top 5:az network nic show-effective-route-table·az network vnet peering list·az network watcher show-next-hop·az network nsg rule list --include-default·az network application-gateway show-backend-health
#VNet and Subnets
List, inspect and create virtual networks and subnets.
az network vnet list --query "[].{name:name, rg:resourceGroup, space:addressSpace.addressPrefixes[0]}" -o tableaz network vnet show -g <rg> -n <vnet> --query "{space:addressSpace.addressPrefixes, dns:dhcpOptions.dnsServers}"az network vnet create -g <rg> -n <vnet> --address-prefixes 10.0.0.0/22 -l <region>az network vnet update -g <rg> -n <vnet> --address-prefixes 10.0.0.0/22 10.0.8.0/24 # extend address spaceaz network vnet update -g <rg> -n <vnet> --dns-servers <ip1> <ip2> # custom DNSaz network vnet update -g <rg> -n <vnet> --dns-servers "" # back to Azure DNSaz network vnet subnet list -g <rg> --vnet-name <vnet> ` --query "[].{name:name, prefix:addressPrefix, nsg:networkSecurityGroup.id, rt:routeTable.id}" -o tableaz network vnet subnet create -g <rg> --vnet-name <vnet> -n <subnet> --address-prefixes 10.0.1.0/24 ` --default-outbound falseaz network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --network-security-group <nsg>az network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --route-table <rt-id>az network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --remove routeTableaz network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --remove networkSecurityGroupaz network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --private-endpoint-network-policies Enabledaz network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --delegations Microsoft.Web/serverFarmsaz network vnet list-available-ips -g <rg> -n <vnet>az network vnet check-ip-address -g <rg> -n <vnet> --ip-address <ip> # free?#Peering
Create peerings and check state and sync level.
az network vnet peering list -g <rg> --vnet-name <vnet> ` --query "[].{name:name, state:peeringState, sync:peeringSyncLevel, access:allowVirtualNetworkAccess, fwd:allowForwardedTraffic, gwTransit:allowGatewayTransit, remoteGw:useRemoteGateways}" -o tableaz network vnet peering create -g <rg> --vnet-name <vnet-a> -n <a-to-b> --remote-vnet <vnet-b-id> ` --allow-vnet-access --allow-forwarded-trafficaz network vnet peering update -g <rg> --vnet-name <vnet> -n <peering> --set allowForwardedTraffic=trueaz network vnet peering sync -g <rg> --vnet-name <vnet> -n <peering> # after an address space changeaz network vnet peering delete -g <rg> --vnet-name <vnet> -n <peering>| Setting | Hub side | Spoke side |
|---|---|---|
| Allow forwarded traffic | yes | yes |
| Allow gateway transit | yes (if VPN/ER GW in hub) | – |
| Use remote gateways | – | yes (if VPN/ER GW in hub) |
Peering must exist on both sides: Connected / FullyInSync.
#Route Tables (UDR)
Create route tables and routes, for example everything via the firewall.
az network route-table list -o tableaz network route-table create -g <rg> -n <rt> -l <region> --disable-bgp-route-propagation trueaz network route-table route create -g <rg> --route-table-name <rt> -n default-to-nva ` --address-prefix 0.0.0.0/0 --next-hop-type VirtualAppliance --next-hop-ip-address <nva-or-ilb-ip>az network route-table route list -g <rg> --route-table-name <rt> -o tableaz network route-table route update -g <rg> --route-table-name <rt> -n <route> --next-hop-ip-address <new-ip>az network route-table route delete -g <rg> --route-table-name <rt> -n <route>az network route-table show -g <rg> -n <rt> --query "subnets[].id" -o tsv # where is it associated?Next hop types: VirtualAppliance, Internet, VnetLocal, VirtualNetworkGateway, None.
#Effective Routes and Connectivity Tests
See the routes and NSG rules a NIC really uses and test the next hop.
az network nic show-effective-route-table -g <rg> -n <nic> -o tableaz network nic list-effective-nsg -g <rg> -n <nic>az network watcher show-next-hop -g <rg> --vm <vm> --source-ip <vm-ip> --dest-ip <target> -o tableaz network watcher test-connectivity -g <rg> --source-resource <vm> --dest-address <target> --dest-port 443az network watcher test-ip-flow -g <rg> --vm <vm> --direction Inbound --protocol TCP ` --local <vm-ip>:443 --remote <source>:12345az network watcher list -o table # Network Watcher per regionEffective routes are only available for a running VM.
#NSG
List and change network security groups and their rules.
az network nsg list -o tableaz network nsg create -g <rg> -n <nsg>az network nsg rule list -g <rg> --nsg-name <nsg> --include-default -o tableaz network nsg rule create -g <rg> --nsg-name <nsg> -n Allow-Https-In --priority 100 ` --direction Inbound --access Allow --protocol Tcp ` --source-address-prefixes <source-or-ServiceTag> --destination-port-ranges 443az network nsg rule update -g <rg> --nsg-name <nsg> -n <rule> --access Denyaz network nsg rule delete -g <rg> --nsg-name <nsg> -n <rule>az network nsg show -g <rg> -n <nsg> --query "{subnets:subnets[].id, nics:networkInterfaces[].id}"Service tags: Internet, VirtualNetwork, AzureLoadBalancer, GatewayManager, AzureCloud, Storage.<region>, AzureMonitor, AzureKeyVault … Priority: the lower number wins (100–4096).
#NAT Gateway
Fixed outbound public IP(s) for whole subnets – replaces default outbound access (retired for new VNets) and avoids SNAT port exhaustion. Form for all four tools: web page → Generator → Network → NAT gateway.
az network public-ip create -g <rg> -n pip-ng-<name>-weu -l westeurope --sku Standard --allocation-method Static --zone 1az network nat gateway create -g <rg> -n ng-<name>-weu -l westeurope --public-ip-addresses pip-ng-<name>-weu --idle-timeout 4 --zone 1$ng = az network nat gateway show -g <rg> -n ng-<name>-weu --query id -o tsvaz network vnet subnet update -g <rg-vnet> --vnet-name <vnet> -n <subnet> --nat-gateway $ngaz network nat gateway list --query "[].{name:name, rg:resourceGroup, zones:join(',', zones || ['-'])}" -o table$pip = New-AzPublicIpAddress -ResourceGroupName <rg> -Name pip-ng-<name>-weu -Location westeurope -Sku Standard -AllocationMethod Static -Zone 1$ng = New-AzNatGateway -ResourceGroupName <rg> -Name ng-<name>-weu -Location westeurope -Sku Standard -IdleTimeoutInMinutes 4 -PublicIpAddress $pip -Zone 1$vnet = Get-AzVirtualNetwork -ResourceGroupName <rg-vnet> -Name <vnet>($vnet.Subnets | Where-Object Name -eq <subnet>).NatGateway = $ng$vnet | Set-AzVirtualNetworkresource pip 'Microsoft.Network/publicIPAddresses@2024-05-01' = { name: 'pip-ng-<name>-weu' location: location sku: { name: 'Standard' } zones: [ '1' ] properties: { publicIPAllocationMethod: 'Static' }}resource ng 'Microsoft.Network/natGateways@2024-05-01' = { name: 'ng-<name>-weu' location: location sku: { name: 'Standard' } zones: [ '1' ] properties: { idleTimeoutInMinutes: 4, publicIpAddresses: [ { id: pip.id } ] }}// in the subnet of your VNet template: natGateway: { id: ng.id }resource "azurerm_public_ip" "ng" { name = "pip-ng-<name>-weu" resource_group_name = "<rg>" location = "westeurope" allocation_method = "Static" sku = "Standard" zones = ["1"]}resource "azurerm_nat_gateway" "this" { name = "ng-<name>-weu" resource_group_name = "<rg>" location = "westeurope" sku_name = "Standard" idle_timeout_in_minutes = 4 zones = ["1"]}resource "azurerm_nat_gateway_public_ip_association" "this" { nat_gateway_id = azurerm_nat_gateway.this.id public_ip_address_id = azurerm_public_ip.ng.id}resource "azurerm_subnet_nat_gateway_association" "this" { subnet_id = "<subnet-id>" nat_gateway_id = azurerm_nat_gateway.this.id}| Note | Detail |
|---|---|
| Zones | NAT gateway is zonal: public IP in the same zone (or both without zone) |
| Capacity | per public IP 64,512 SNAT ports – up to 16 IPs or a public IP prefix (/31–/28) |
| Priority | NAT gateway beats instance public IPs and load balancer outbound rules for outbound traffic |
| With firewall | a UDR 0/0 to the firewall wins – put the NAT gateway on AzureFirewallSubnet instead (firewall outbound via NAT IPs) |
| Not supported | gateway subnet, Basic SKU public IPs / load balancers in the subnet |
| Check the outbound IP | az vm run-command invoke -g <rg> -n <vm> --command-id RunShellScript --scripts "curl -s ifconfig.me" |
#Bastion
Connect to VMs over SSH or RDP through Bastion.
az network bastion list -o tableaz network bastion ssh -g <rg> -n <bastion> --target-resource-id <vm-id> --auth-type ssh-key --username <user> --ssh-key <path>az network bastion rdp -g <rg> -n <bastion> --target-resource-id <vm-id>az network bastion tunnel -g <rg> -n <bastion> --target-resource-id <vm-id> --resource-port 22 --port 50022bastion ssh/rdp/tunnel requires Bastion Standard with Native client support. Form: Generator → Virtual Machines → Bastion.
#DDoS Protection
Network protection versus IP protection – what each covers.
| DDoS Network Protection | DDoS IP Protection | |
|---|---|---|
| What | plan, assigned to VNets – protects all public IPs in them | per single public IP |
| Costs | fixed monthly price per plan, covers 100 public IPs (more cost extra) | monthly price per public IP |
| Extras | rapid response team, cost protection, WAF discount | – |
| When | many public IPs / production landing zones | a few public IPs |
One plan is enough for the whole tenant – create it once (e.g. connectivity subscription) and link VNets from other subscriptions. Form: web page → Generator → Network → DDoS protection.
az network ddos-protection create -g <rg> -n ddos-<org>-weu -l westeurope$plan = az network ddos-protection show -g <rg> -n ddos-<org>-weu --query id -o tsvaz network vnet update -g <rg-vnet> -n <vnet> --ddos-protection true --ddos-protection-plan $planaz network vnet list --query "[].{vnet:name, ddos:enableDdosProtection, plan:ddosProtectionPlan.id}" -o table# IP Protection instead of a plan:az network public-ip update -g <rg> -n <pip> --ddos-protection-mode Enabledaz network public-ip show -g <rg> -n <pip> --query ddosSettings.protectionMode -o tsv# Alert while under attackaz monitor metrics alert create -g <rg-monitoring> -n alert-ddos-<pip> --scopes <pip-id> ` --condition "max IfUnderDDoSAttack > 0" --window-size 5m --evaluation-frequency 1m --severity 1 --action <action-group-id>$plan = New-AzDdosProtectionPlan -ResourceGroupName <rg> -Name ddos-<org>-weu -Location westeurope$vnet = Get-AzVirtualNetwork -ResourceGroupName <rg-vnet> -Name <vnet>$vnet.DdosProtectionPlan = New-Object Microsoft.Azure.Commands.Network.Models.PSResourceId$vnet.DdosProtectionPlan.Id = $plan.Id$vnet.EnableDdosProtection = $true$vnet | Set-AzVirtualNetworkresource ddos 'Microsoft.Network/ddosProtectionPlans@2024-05-01' = { name: 'ddos-<org>-weu' location: location}// in the VNet: properties: { enableDdosProtection: true, ddosProtectionPlan: { id: ddos.id }, ... }// IP Protection on a public IP: properties: { ddosSettings: { protectionMode: 'Enabled' }, ... }resource "azurerm_network_ddos_protection_plan" "this" { name = "ddos-<org>-weu" resource_group_name = "<rg>" location = "westeurope"}# in azurerm_virtual_network:# ddos_protection_plan {# id = azurerm_network_ddos_protection_plan.this.id# enable = true# }# IP Protection: azurerm_public_ip { ddos_protection_mode = "Enabled" }Logs: diagnostic setting on the public IPs (DDoSProtectionNotifications, DDoSMitigationFlowLogs, DDoSMitigationReports) → Log Analytics. Test only with an approved simulation partner – never attack yourself.
#VPN Gateway (Site-to-Site)
Check gateway, BGP peers and connections of a site-to-site VPN.
az network vnet-gateway list -g <rg> -o tableaz network vnet-gateway show -g <rg> -n <gw> --query "{sku:sku.name, bgp:bgpSettings, ips:ipConfigurations[].publicIPAddress.id}"az network vnet-gateway list-bgp-peer-status -g <rg> -n <gw> -o tableaz network vnet-gateway list-learned-routes -g <rg> -n <gw> -o tableaz network vnet-gateway list-advertised-routes -g <rg> -n <gw> --peer <bgp-peer-ip> -o tableaz network vnet-gateway reset -g <rg> -n <gw> # restart the instance(s)az network local-gateway list -g <rg> -o tableaz network vpn-connection list -g <rg> --query "[].{name:name, status:connectionStatus, type:connectionType}" -o tableaz network vpn-connection show -g <rg> -n <conn> --query "{status:connectionStatus, in:ingressBytesTransferred, out:egressBytesTransferred}"az network vpn-connection shared-key show -g <rg> --connection-name <conn>az network vpn-connection shared-key update -g <rg> --connection-name <conn> --value <psk>#Azure Firewall
Firewall policy structure: rule collection group (priority) → rule collection (priority, action Allow/Deny/DNAT) → rules. Evaluation order: DNAT → network rules → application rules, each by priority (lower = first). With a form: web page → Generator → Azure Firewall.
az extension add -n azure-firewallaz network firewall list -o tableaz network firewall show -g <rg> -n <fw> --query "{ip:ipConfigurations[0].privateIPAddress, policy:firewallPolicy.id}"az network firewall policy list -o tableaz network firewall policy rule-collection-group list -g <rg> --policy-name <policy> --query "[].{name:name, prio:priority}" -o tableaz network firewall policy rule-collection-group show -g <rg> --policy-name <policy> -n <rcg> ` --query "ruleCollections[].{name:name, prio:priority, action:action.type, rules:length(rules)}" -o table # Add a rule to an existing collectionaz network firewall policy rule-collection-group collection rule add -g <rg> --policy-name <policy> --rcg-name <rcg> ` --collection-name <collection> --name <rule> --rule-type NetworkRule ` --source-addresses 10.1.0.0/24 --destination-addresses 10.2.0.0/24 --destination-ports 443 --ip-protocols TCP # New collection with a ruleaz network firewall policy rule-collection-group collection add-filter-collection -g <rg> --policy-name <policy> --rcg-name <rcg> ` --name <collection> --collection-priority 200 --action Allow --rule-name <rule> --rule-type ApplicationRule ` --source-addresses 10.1.0.0/24 --target-fqdns "*.microsoft.com" --protocols Https=443 # Remove a ruleaz network firewall policy rule-collection-group collection rule remove -g <rg> --policy-name <policy> --rcg-name <rcg> ` --collection-name <collection> --name <rule>Extend an existing rule (PowerShell – get, change, write back):
$pol = Get-AzFirewallPolicy -Name <policy> -ResourceGroupName <rg>$rcg = Get-AzFirewallPolicyRuleCollectionGroup -Name <rcg> -AzureFirewallPolicyName <policy> -ResourceGroupName <rg>$rule = ($rcg.Properties.RuleCollection | Where-Object Name -eq "<collection>").Rules | Where-Object Name -eq "<rule>"$rule.DestinationAddresses.Add("10.3.0.0/24")Set-AzFirewallPolicyRuleCollectionGroup -Name <rcg> -Priority $rcg.Properties.Priority ` -RuleCollection $rcg.Properties.RuleCollection -FirewallPolicyObject $polLogs (resource-specific): AZFWNetworkRule, AZFWApplicationRule, AZFWNatRule – column Action = Deny shows what is blocked.
#Private DNS and Private Endpoints
Private DNS zones, VNet links and private endpoints.
az network private-dns zone list -o tableaz network private-dns zone create -g <rg> -n privatelink.azurewebsites.netaz network private-dns link vnet list -g <rg-dns> -z <zone> -o tableaz network private-dns link vnet create -g <rg-dns> -z <zone> -n <link-name> -v <vnet-id> -e false # -e = auto-registrationaz network private-dns record-set a list -g <rg-dns> -z <zone> -o tableaz network private-dns record-set a add-record -g <rg-dns> -z <zone> -n <name> -a <ip>az network private-dns record-set a remove-record -g <rg-dns> -z <zone> -n <name> -a <ip> az network private-endpoint list -g <rg> -o tableaz network private-endpoint show -g <rg> -n <pe> --query "customDnsConfigs"az network private-endpoint create -g <rg> -n <pe> --vnet-name <vnet> --subnet <subnet> ` --private-connection-resource-id <target-resource-id> --group-id <sites|vault|blob|...> --connection-name <name>az network private-endpoint dns-zone-group create -g <rg> --endpoint-name <pe> -n default ` --private-dns-zone <zone-id> --zone-name <zone>az network private-endpoint-connection list --id <target-resource-id> -o tableaz network private-endpoint-connection approve --id <connection-id> --description "ok" # DNS Private Resolveraz dns-resolver list -o tableaz dns-resolver inbound-endpoint list -g <rg> --dns-resolver-name <resolver> -o tableaz dns-resolver forwarding-ruleset list -g <rg> -o table| Service | Zone | group-id |
|---|---|---|
| App Service / Functions | privatelink.azurewebsites.net | sites |
| Key Vault | privatelink.vaultcore.azure.net | vault |
| Storage Blob | privatelink.blob.core.windows.net | blob |
| Storage File | privatelink.file.core.windows.net | file |
| Storage Queue / Table | privatelink.queue.core.windows.net / privatelink.table.core.windows.net | queue / table |
| PostgreSQL Flexible | privatelink.postgres.database.azure.com | postgresqlServer |
| SQL | privatelink.database.windows.net | sqlServer |
| Cosmos DB (SQL) | privatelink.documents.azure.com | Sql |
| Container Registry | privatelink.azurecr.io | registry |
| Service Bus / Event Hub | privatelink.servicebus.windows.net | namespace |
| Azure Monitor (AMPLS) | privatelink.monitor.azure.com and others | azuremonitor |
Rule: the zone must be linked to the VNet whose DNS setting does the resolution (Azure-provided DNS → client VNet; custom DNS → VNet of the DNS server/resolver). Test: nslookup <name>.azurewebsites.net must return a private IP.
#Application Gateway and WAF
Show gateway state, backend health and the WAF policy.
az network application-gateway list -o tableaz network application-gateway show -g <rg> -n <agw> --query "{state:operationalState, sku:sku.name}"az network application-gateway show-backend-health -g <rg> -n <agw> ` --query "backendAddressPools[].backendHttpSettingsCollection[].servers[].{server:address, health:health, log:healthProbeLog}" -o tableaz network application-gateway stop -g <rg> -n <agw>az network application-gateway start -g <rg> -n <agw>az network application-gateway http-listener list -g <rg> --gateway-name <agw> -o tableaz network application-gateway rule list -g <rg> --gateway-name <agw> -o tableaz network application-gateway address-pool list -g <rg> --gateway-name <agw> -o tableaz network application-gateway http-settings list -g <rg> --gateway-name <agw> -o tableaz network application-gateway probe list -g <rg> --gateway-name <agw> -o tableaz network application-gateway ssl-cert list -g <rg> --gateway-name <agw> -o tableaz network application-gateway ssl-cert create -g <rg> --gateway-name <agw> -n <name> ` --key-vault-secret-id https://<kv>.vault.azure.net/secrets/<name> # certificate from Key Vaultaz network application-gateway identity show -g <rg> --gateway-name <agw> # WAF policiesaz network application-gateway waf-policy list -g <rg> -o tableaz network application-gateway waf-policy show -g <rg> -n <policy> ` --query "{mode:policySettings.mode, state:policySettings.state, sets:managedRules.managedRuleSets[].{t:ruleSetType,v:ruleSetVersion}}"az network application-gateway waf-policy policy-setting update -g <rg> --policy-name <policy> --mode Prevention --state Enabledaz network application-gateway waf-policy managed-rule rule-set list -g <rg> --policy-name <policy>az network application-gateway waf-policy managed-rule rule-set add -g <rg> --policy-name <policy> ` --type Microsoft_BotManagerRuleSet --version 1.1az network application-gateway waf-policy managed-rule exclusion list -g <rg> --policy-name <policy>az network application-gateway waf-policy custom-rule list -g <rg> --policy-name <policy> -o tableRules of thumb for App Gateway v2:
- Dedicated subnet, no 0.0.0.0/0 UDR to anything other than
Internet. - NSG on the subnet: allow inbound
GatewayManagerTCP 65200-65535 andAzureLoadBalancer. - App Service as backend: FQDN, HTTPS 443, pick host name from backend, private DNS zone linked to the App Gateway VNet.
- Certificates: Key Vault + user-assigned identity (Key Vault Secrets User), versionless secret ID.
- WAF: Detection = log only, Prevention = block (403); a separate policy per listener is possible.
#Load Balancer and Public IPs
Inspect load balancers, rules, probes and public IPs.
az network lb list -o tableaz network lb show -g <rg> -n <lb> --query "{fe:frontendIPConfigurations[].privateIPAddress, sku:sku.name}"az network lb rule list -g <rg> --lb-name <lb> -o tableaz network lb probe list -g <rg> --lb-name <lb> -o tableaz network lb address-pool list -g <rg> --lb-name <lb> -o tableaz network lb outbound-rule list -g <rg> --lb-name <lb> -o tableaz network lb inbound-nat-rule list -g <rg> --lb-name <lb> -o table az network public-ip list -g <rg> --query "[].{name:name, ip:ipAddress, sku:sku.name, zones:zones, on:ipConfiguration.id}" -o tableaz network public-ip create -g <rg> -n <pip> --sku Standard --allocation-method Static --zone 1 2 3Rules of thumb:
- The health probe source is always
168.63.129.16. - Floating IP on: destination stays the frontend IP (for VIPs on an NVA). Off: destination is rewritten to the backend IP (for services on the NVA itself, e.g. IPsec).
- HA ports (internal Standard LB): all protocols/ports in one rule – the basis for NVA high availability.
- Outbound rules only translate TCP/UDP, no ICMP (ping to the internet does not work).
#Flow Logs
List flow logs and where to analyse them.
az network watcher flow-log list -l <region> -o tableaz network watcher flow-log show -l <region> -n <flowlog>New flow logs are created at VNet level (NSG flow logs are being retired). Analysis: KQL.
#Azure Front Door
Top 5:az afd custom-domain list·az afd route show·az afd origin list·az afd endpoint purge·az afd custom-domain show --query domainValidationState
#Structure
Profile, endpoint, route, origin group and origin.
Profile (Standard / Premium)└── Endpoint <name>-<hash>.z01.azurefd.net └── Route which domains + paths -> which origin group ├── Custom domains app.company.tld (+ certificate) └── Origin group health probe, load balancing └── Origins e.g. <app>.azurewebsites.net (priority, weight, optional Private Link)| Term | Meaning |
|---|---|
| Origin | backend, e.g. App Service, storage website, own server |
| Origin group | group of origins with a shared health probe |
| Route | connects domains + paths to an origin group |
| Custom domain | own host name, needs TXT validation + CNAME |
| Security policy | binds a WAF policy to domains |
| Private Link | Front Door reaches the origin privately (Premium only) |
#Get an Overview
List profiles, endpoints, origins and routes.
$rg = "<rg-afd>" ; $afd = "<afd-profile>" ; $ep = "<afd-endpoint>"az afd profile list --query "[].{name:name, rg:resourceGroup, sku:sku.name}" -o tableaz afd endpoint list -g $rg --profile-name $afd --query "[].{name:name, host:hostName}" -o tableaz afd custom-domain list -g $rg --profile-name $afd --query "[].{name:name, host:hostName, validation:domainValidationState, cert:tlsSettings.certificateType}" -o tableaz afd origin-group list -g $rg --profile-name $afd --query "[].name" -o tsvaz afd origin list -g $rg --profile-name $afd --origin-group-name <origin-group> -o tableaz afd route list -g $rg --profile-name $afd --endpoint-name $ep --query "[].{route:name, og:originGroup.id, domains:customDomains[].id, paths:patternsToMatch}" -o json#Origin Group and Origin
Create the origin group with health probe and add an origin.
az afd origin-group create -g $rg --profile-name $afd --origin-group-name og-<app> ` --probe-request-type HEAD --probe-protocol Https --probe-interval-in-seconds 60 --probe-path "/" ` --sample-size 4 --successful-samples-required 3 --additional-latency-in-milliseconds 50 $appHost = az webapp show -g <rg-app> -n <app> --query defaultHostName -o tsvaz afd origin create -g $rg --profile-name $afd --origin-group-name og-<app> --origin-name o-<app> ` --host-name $appHost --origin-host-header $appHost --http-port 80 --https-port 443 ` --priority 1 --weight 1000 --enabled-state EnabledPrivate Link (Premium) additionally: --enable-private-link true --private-link-resource <app-id> --private-link-location <region> --private-link-sub-resource-type sites, then approve the request on the app:
az network private-endpoint-connection list --id <app-id> --query "[?properties.privateLinkServiceConnectionState.status=='Pending'].id" -o tsvaz network private-endpoint-connection approve --id <connection-id> --description "Front Door"#Custom Domain and Certificate
Add your domain with a managed certificate.
# Certificate managed by Front Dooraz afd custom-domain create -g $rg --profile-name $afd --custom-domain-name app-company-tld --host-name app.company.tld ` --minimum-tls-version TLS12 --certificate-type ManagedCertificate # Or your own certificate from Key Vault (versionless = automatic renewal)$kvId = az keyvault show -n <keyvault-name> --query id -o tsvaz afd secret create -g $rg --profile-name $afd --secret-name sec-<certificate> ` --secret-source "$kvId/secrets/<certificate>" --use-latest-version trueaz afd custom-domain create -g $rg --profile-name $afd --custom-domain-name app-company-tld --host-name app.company.tld ` --minimum-tls-version TLS12 --certificate-type CustomerCertificate --secret sec-<certificate> # DNS recordsaz afd custom-domain show -g $rg --profile-name $afd --custom-domain-name app-company-tld --query "validationProperties.validationToken" -o tsv# TXT _dnsauth.app.company.tld -> <token># CNAME app.company.tld -> <endpoint-host>.azurefd.netaz afd custom-domain show -g $rg --profile-name $afd --custom-domain-name app-company-tld --query "{validation:domainValidationState, status:deploymentStatus}"The custom domain's name in Front Door may only contain letters, digits and hyphens (app-company-tld); the host name is the real one (app.company.tld).
#Create a Route or Assign to an Existing Route
Connect domain and origin group with a route.
# New routeaz afd route create -g $rg --profile-name $afd --endpoint-name $ep --route-name rt-<app> ` --origin-group og-<app> --custom-domains app-company-tld --patterns-to-match "/*" ` --supported-protocols Http Https --https-redirect Enabled --forwarding-protocol HttpsOnly --link-to-default-domain Disabled # Add a domain to an EXISTING route – pass the existing domains too, otherwise they are removed!$cdId = az afd custom-domain show -g $rg --profile-name $afd --custom-domain-name app-company-tld --query id -o tsv$ids = @(az afd route show -g $rg --profile-name $afd --endpoint-name $ep --route-name <route> --query "customDomains[].id" -o tsv) + $cdIdaz afd route update -g $rg --profile-name $afd --endpoint-name $ep --route-name <route> --custom-domains $idsCaution: az afd route update --custom-domains replaces the list. Always pass the existing IDs.
#Lock Down the App Service
Only Front Door may reach the app – via service tag and your own Front Door ID (otherwise any Front Door would get through):
$fdId = az afd profile show -g $rg --profile-name $afd --query frontDoorId -o tsvaz webapp config access-restriction add -g <rg-app> -n <app> --rule-name AllowFrontDoor --priority 100 ` --action Allow --service-tag AzureFrontDoor.Backend --http-header x-azure-fdid=$fdIdWith Private Link (Premium) instead: disable public access on the app (az webapp update --set publicNetworkAccess=Disabled).
#WAF
WAF policy with managed rules and its link to the endpoint.
az network front-door waf-policy create -g $rg -n wafafd<app> --sku Premium_AzureFrontDoor --mode Detectionaz network front-door waf-policy managed-rules add -g $rg --policy-name wafafd<app> --type Microsoft_DefaultRuleSet --version 2.1 --action Block$wafId = az network front-door waf-policy show -g $rg -n wafafd<app> --query id -o tsvaz afd security-policy create -g $rg --profile-name $afd --security-policy-name sp-<app> --waf-policy $wafId --domains $cdIdLogs: diagnostic setting of the profile → Log Analytics, table AzureDiagnostics with Category == "FrontDoorWebApplicationFirewallLog".
#Cache and Troubleshooting
Purge the cache and test with and without the custom domain.
az afd endpoint purge -g $rg --profile-name $afd --endpoint-name $ep --content-paths "/*"curl.exe -sI https://app.company.tld # X-Azure-Ref, X-Cache in the responsecurl.exe -sI https://<endpoint-host>.azurefd.net # test without the custom domain| Symptom | Cause | Fix |
|---|---|---|
| Domain stays Pending | TXT _dnsauth missing/wrong, token expired | check TXT, az afd custom-domain regenerate-validation-token |
| 404 "Page not found" from Front Door | domain not assigned to a route / path doesn't match | check route → domains and patternsToMatch |
| 502/503/504 | origin unreachable, probe fails, wrong host header | origin host + origin-host-header, probe path, app access restriction |
| 403 from the app | access restriction without x-azure-fdid or wrong ID | rule as above, your own Front Door ID |
| Private Link origin doesn't work | request on the app not approved | private-endpoint-connection approve |
| Change has no effect | deployment running (up to ~10 min) / cache | check deploymentStatus, endpoint purge |
#Azure Virtual WAN
Virtual WAN = Microsoft-managed hubs: routing between VNets, branches (VPN/SD-WAN), ExpressRoute and users (P2S) without your own UDRs. Code blocks with tabs: choose CLI / PowerShell / Bicep / Terraform – the choice is remembered.
Top 5:az network vhub list -o table·az network vhub connection list·az network vhub get-effective-routes·az network vhub routing-intent show·az network vpn-gateway connection list
#When Virtual WAN, When Classic Hub?
Comparison to choose between both designs.
| Virtual WAN (Standard) | Classic hub VNet | |
|---|---|---|
| Routing between spokes, branches, ER | automatic (hub router), transitive | UDRs + NVA/firewall you maintain |
| Many branches / regions | strong (any-to-any, hub-to-hub) | lots of manual work |
| Own NVA (e.g. FortiGate) | as NVA in the hub (supported partners) or in a spoke | full control |
| Firewall | Azure Firewall in the hub (secured hub) + routing intent | Azure Firewall / NVA in the hub VNet |
| Control and troubleshooting | less visibility, fixed address space per hub | everything visible and adjustable |
| Costs | hub hourly + data processing | VNet free, pay gateways/firewall |
Hub address space: at least /23 (recommended /23–/22), must not overlap with any VNet or branch.
#Create WAN and Hub
Create the WAN and the virtual hub.
az network vwan create -g <rg> -n vwan-<org>-prd -l westeurope --type Standardaz network vhub create -g <rg> -n vhub-<org>-prd-weu --vwan vwan-<org>-prd ` --address-prefix 10.100.0.0/23 -l westeurope --sku Standardaz network vhub show -g <rg> -n vhub-<org>-prd-weu --query "{state:provisioningState, router:routingState}" # routingState: Provisioned$wan = New-AzVirtualWan -ResourceGroupName <rg> -Name vwan-<org>-prd -Location westeurope -VirtualWANType Standard$hub = New-AzVirtualHub -ResourceGroupName <rg> -Name vhub-<org>-prd-weu -Location westeurope ` -VirtualWan $wan -AddressPrefix 10.100.0.0/23Get-AzVirtualHub -ResourceGroupName <rg> -Name vhub-<org>-prd-weu | Select-Object Name, ProvisioningState, RoutingStateresource vwan 'Microsoft.Network/virtualWans@2024-05-01' = { name: 'vwan-<org>-prd' location: location properties: { type: 'Standard', allowBranchToBranchTraffic: true }}resource hub 'Microsoft.Network/virtualHubs@2024-05-01' = { name: 'vhub-<org>-prd-weu' location: location properties: { addressPrefix: '10.100.0.0/23' virtualWan: { id: vwan.id } }}resource "azurerm_virtual_wan" "this" { name = "vwan-<org>-prd" resource_group_name = "<rg>" location = "westeurope" type = "Standard"}resource "azurerm_virtual_hub" "weu" { name = "vhub-<org>-prd-weu" resource_group_name = "<rg>" location = "westeurope" virtual_wan_id = azurerm_virtual_wan.this.id address_prefix = "10.100.0.0/23"}Creating a hub takes about 30 minutes – the hub router is only ready when routingState = Provisioned.
#Connect a Spoke VNet
Connect a VNet to the hub.
az network vhub connection create -g <rg> --vhub-name vhub-<org>-prd-weu -n conn-<spoke> ` --remote-vnet /subscriptions/<sub-id>/resourceGroups/<rg-spoke>/providers/Microsoft.Network/virtualNetworks/<vnet-spoke> ` --internet-security trueaz network vhub connection list -g <rg> --vhub-name vhub-<org>-prd-weu --query "[].{name:name, state:provisioningState}" -o table$vnet = Get-AzVirtualNetwork -ResourceGroupName <rg-spoke> -Name <vnet-spoke>New-AzVirtualHubVnetConnection -ResourceGroupName <rg> -ParentResourceName vhub-<org>-prd-weu ` -Name conn-<spoke> -RemoteVirtualNetwork $vnet -EnableInternetSecurityGet-AzVirtualHubVnetConnection -ResourceGroupName <rg> -VirtualHubName vhub-<org>-prd-weu | Select-Object Name, ProvisioningStateresource conn 'Microsoft.Network/virtualHubs/hubVirtualNetworkConnections@2024-05-01' = { parent: hub name: 'conn-<spoke>' properties: { remoteVirtualNetwork: { id: spokeVnetId } enableInternetSecurity: true }}resource "azurerm_virtual_hub_connection" "spoke" { name = "conn-<spoke>" virtual_hub_id = azurerm_virtual_hub.weu.id remote_virtual_network_id = "<vnet-spoke-id>" internet_security_enabled = true}Spoke VNets must not have their own VPN/ER gateway, and no UDR 0.0.0.0/0 to an NVA when routing intent is used – the hub programs the routes itself.
#Secured Hub: Azure Firewall and Routing Intent
Routing intent sends internet and/or private traffic (spoke ↔ spoke, spoke ↔ branch) through the firewall in the hub – without route tables.
az network firewall create -g <rg> -n afw-vhub-<org>-weu --sku AZFW_Hub --tier Standard ` --vhub vhub-<org>-prd-weu --public-ip-count 1 --firewall-policy <fw-policy-id>az network vhub routing-intent list -g <rg> --vhub vhub-<org>-prd-weu -o table# Routing intent itself: easiest in the portal (hub -> Routing intent and routing policies) or with PowerShell$hub = Get-AzVirtualHub -ResourceGroupName <rg> -Name vhub-<org>-prd-weu$ip = New-AzFirewallHubIpAddress -PublicIP (New-AzFirewallHubPublicIpAddress -Count 1)$fw = New-AzFirewall -Name afw-vhub-<org>-weu -ResourceGroupName <rg> -Location westeurope -Sku AZFW_Hub ` -VirtualHubId $hub.Id -HubIPAddress $ip -FirewallPolicyId <fw-policy-id>$p1 = New-AzRoutingPolicy -Name PrivateTraffic -Destination @("PrivateTraffic") -NextHop $fw.Id$p2 = New-AzRoutingPolicy -Name PublicTraffic -Destination @("Internet") -NextHop $fw.IdNew-AzRoutingIntent -ResourceGroupName <rg> -VirtualHubName vhub-<org>-prd-weu -Name ri-default -RoutingPolicy @($p1, $p2)Set-AzRoutingIntent replaces all policies – always pass the complete list. Firewall rules for spoke ↔ spoke: Generator → Azure Firewall.
#Site-to-Site VPN in the Hub
VPN gateway in the hub and a site connection.
az network vpn-gateway create -g <rg> -n vpng-<org>-weu --vhub vhub-<org>-prd-weu --scale-unit 1 -l westeuropeaz network vpn-site create -g <rg> -n vst-<site> --virtual-wan vwan-<org>-prd -l westeurope ` --ip-address <public-ip-site> --address-prefixes 192.168.10.0/24 --device-vendor Fortinet --link-speed 100az network vpn-gateway connection create -g <rg> --gateway-name vpng-<org>-weu -n vcn-<site> ` --remote-vpn-site vst-<site> --shared-key "<psk>" --protocol-type IKEv2 --enable-bgp falseaz network vpn-gateway show -g <rg> -n vpng-<org>-weu --query "bgpSettings.bgpPeeringAddresses[].tunnelIpAddresses" -o tsv # public IPs for the FortiGateOn the FortiGate: two tunnels (both gateway instances), phase 1/2 like the Azure defaults – see FortiGate → IPsec.
#Troubleshooting
Effective routes and route tables of the hub.
az network vhub get-effective-routes -g <rg> -n vhub-<org>-prd-weu ` --resource-type HubVirtualNetworkConnection --resource-id <connection-id> -o table # routes as the spoke sees themaz network vhub route-table list -g <rg> --vhub-name vhub-<org>-prd-weu -o tableaz network vpn-gateway connection show -g <rg> --gateway-name vpng-<org>-weu -n vcn-<site> --query "connectionStatus"az network nic show-effective-route-table -g <rg-spoke> -n <nic> -o table # in the spoke: next hop must be the hub| Symptom | Cause | Fix |
|---|---|---|
| Spoke ↔ spoke does not work | routing intent missing / firewall blocks | private traffic policy, firewall network rule |
| Spoke has no internet | internet-security off on the connection or no Internet policy | connection setting, routing intent Internet |
| Hub creation hangs at Updating | hub router still provisioning | wait (up to 30 min), check routingState |
| Overlap error on connect | spoke address space overlaps hub /23 or other spokes | change address space |
| Branch routes missing | BGP off / wrong prefixes in the VPN site | VPN site address prefixes, BGP settings |
#ExpressRoute
Top 5:az network express-route show·az network express-route list-route-tables·az network express-route list-arp-tables·az network express-route get-stats·az network vpn-connection show
#Terms
Circuit, peering, gateway and connection explained.
| Term | Meaning |
|---|---|
| Circuit | the logical connection to Microsoft, ordered via a provider (bandwidth, peering location) |
| Service key | ID of the circuit – you give it to the provider |
| Private peering | your VNets (via ExpressRoute gateway) |
| Microsoft peering | Microsoft 365 / public Azure services (route filter required) |
| Primary / secondary | two links for redundancy – always connect both |
| ER gateway | VNet gateway of type ExpressRoute in the GatewaySubnet |
| Global Reach | connects two circuits (site ↔ site via Microsoft backbone) |
| FastPath | data path bypasses the gateway (Ultra/ErGw3AZ, certain scenarios) |
| SKU | Local (only local region, no egress costs), Standard (same geopolitical area), Premium (global) |
#Create a Circuit
Choose the provider and create the circuit.
az network express-route list-service-providers --query "[].{name:name, locations:join(', ', peeringLocations)}" -o tableaz network express-route create -g <rg> -n erc-<site>-weu -l westeurope ` --provider "<provider>" --peering-location "<peering-location>" --bandwidth 1000 ` --sku-tier Standard --sku-family MeteredDataaz network express-route show -g <rg> -n erc-<site>-weu --query "{key:serviceKey, provider:serviceProviderProvisioningState, state:circuitProvisioningState}"Give the service key to the provider. Continue only when serviceProviderProvisioningState = Provisioned.
#Private Peering
Only needed if you manage layer 3 yourself (with many providers they configure it):
az network express-route peering create -g <rg> --circuit-name erc-<site>-weu --peering-type AzurePrivatePeering ` --peer-asn 65010 --primary-peer-subnet 172.16.0.0/30 --secondary-peer-subnet 172.16.0.4/30 --vlan-id 100az network express-route peering show -g <rg> --circuit-name erc-<site>-weu -n AzurePrivatePeering --query "{state:state, asn:peerAsn}"#Gateway and Connection
Gateway in the hub and the connection to the circuit.
az network public-ip create -g <rg> -n pip-ergw-<hub> --sku Standard --zone 1 2 3az network vnet-gateway create -g <rg> -n ergw-<hub> --vnet <vnet-hub> --gateway-type ExpressRoute ` --sku ErGw1AZ --public-ip-address pip-ergw-<hub> --no-wait # takes 30-45 min$ercId = az network express-route show -g <rg> -n erc-<site>-weu --query id -o tsvaz network vpn-connection create -g <rg> -n con-ergw-<hub>-<site> --vnet-gateway1 ergw-<hub> --express-route-circuit2 $ercId$vnet = Get-AzVirtualNetwork -ResourceGroupName <rg> -Name <vnet-hub>$sub = Get-AzVirtualNetworkSubnetConfig -VirtualNetwork $vnet -Name GatewaySubnet$pip = New-AzPublicIpAddress -ResourceGroupName <rg> -Name pip-ergw-<hub> -Location westeurope -Sku Standard -AllocationMethod Static -Zone 1,2,3$cfg = New-AzVirtualNetworkGatewayIpConfig -Name ipconfig1 -SubnetId $sub.Id -PublicIpAddressId $pip.Id$gw = New-AzVirtualNetworkGateway -ResourceGroupName <rg> -Name ergw-<hub> -Location westeurope ` -IpConfigurations $cfg -GatewayType ExpressRoute -GatewaySku ErGw1AZ$erc = Get-AzExpressRouteCircuit -ResourceGroupName <rg> -Name erc-<site>-weuNew-AzVirtualNetworkGatewayConnection -ResourceGroupName <rg> -Name con-ergw-<hub>-<site> -Location westeurope ` -VirtualNetworkGateway1 $gw -PeerId $erc.Id -ConnectionType ExpressRouteresource "azurerm_virtual_network_gateway" "er" { name = "ergw-<hub>" location = "westeurope" resource_group_name = "<rg>" type = "ExpressRoute" sku = "ErGw1AZ" ip_configuration { public_ip_address_id = azurerm_public_ip.ergw.id subnet_id = "<gateway-subnet-id>" }}resource "azurerm_virtual_network_gateway_connection" "er" { name = "con-ergw-<hub>-<site>" location = "westeurope" resource_group_name = "<rg>" type = "ExpressRoute" virtual_network_gateway_id = azurerm_virtual_network_gateway.er.id express_route_circuit_id = "<circuit-id>"}GatewaySubnet at least /27. With an NVA in the hub: UDR on the GatewaySubnet spoke prefixes → NVA (see Network Basics).
#Check Status and Routes
Circuit status, ARP and route tables.
az network express-route show -g <rg> -n erc-<site>-weu --query "{circuit:circuitProvisioningState, provider:serviceProviderProvisioningState, bw:serviceProviderProperties.bandwidthInMbps}"az network express-route list-arp-tables -g <rg> -n erc-<site>-weu --peering-name AzurePrivatePeering --path primary -o tableaz network express-route list-route-tables -g <rg> -n erc-<site>-weu --peering-name AzurePrivatePeering --path primary -o tableaz network express-route list-route-tables -g <rg> -n erc-<site>-weu --peering-name AzurePrivatePeering --path secondary -o tableaz network express-route get-stats -g <rg> -n erc-<site>-weu -o table # bytes in/outaz network vnet-gateway list-learned-routes -g <rg> -n ergw-<hub> -o table # what the gateway learnedaz network vpn-connection show -g <rg> -n con-ergw-<hub>-<site> --query "{status:connectionStatus, ingress:ingressBytesTransferred}"| Symptom | Cause | Fix |
|---|---|---|
| ARP table empty | layer 2 / VLAN at the provider not ready | open a ticket with the provider |
| ARP ok, no routes | BGP down (ASN, MD5, subnets) | check BGP config on the router/provider |
| Routes there, no traffic | NSG/UDR/NVA in Azure, asymmetric routing | effective routes, GatewaySubnet UDR |
| Only one path works | secondary link not configured | always configure primary and secondary |
| On-prem routes missing in the spoke | spoke peering without Use remote gateways | peering settings |
| More than 1000 prefixes from on-prem (Standard) | prefix limit of the private peering | summarize or Premium (up to 10,000) |
Metric alerts: BgpAvailability, ArpAvailability on the circuit – see Monitoring.
#Azure Network Basics
#Rules of Thumb
Reserved addresses, subnet sizes and planning rules.
| Topic | Rule |
|---|---|
| Reserved IPs | 5 per subnet: network, .1 gateway, .2/.3 Azure DNS, broadcast → first usable IP is .4 |
| Peering | not transitive; create both sides; Allow forwarded traffic for NVA traffic |
| UDR priority | longest prefix wins; on a tie: UDR > BGP > system |
| Hub-and-spoke with NVA | spokes: 0.0.0.0/0 → NVA/ILB, BGP propagation off; NVA subnets without 0/0 UDR |
| Spoke ↔ spoke | only goes via the NVA if both spokes have the UDR and the policy allows it |
| Asymmetry | outbound and return path must use the same firewall – otherwise drop; fix: UDR on both sides or SNAT |
| IP forwarding | the NVA's NIC needs IP forwarding = on (Azure) and forwarding in the OS |
| GatewaySubnet UDR | only needed with an Azure VPN/ER gateway: spoke prefixes → NVA, no 0/0 |
| Default outbound access | off = VMs without PIP/LB outbound/NAT GW cannot reach the internet (intended with an NVA) |
| Outbound path (priority) | NAT Gateway > public IP on the NIC > LB outbound rule > default outbound |
| Health probes | source 168.63.129.16, must be allowed and routed on the NVA |
| Azure DNS | 168.63.129.16 – only reachable from Azure, not "passed through" via VPN/peering |
| Floating IP | on = destination stays the frontend IP (VIPs); off = destination = backend IP (services on the NVA, IPsec) |
| ICMP via LB outbound | not translated – ping to the internet fails, test with curl |
| Private endpoint + DNS | link the zone to the VNet that does the DNS resolution |
| Private endpoint + UDR | return traffic from PEs uses UDRs only with network policies on the subnet = on |
| Zones | logical zones differ per subscription – check az vm list-skus |
| NSG on App Gateway | allow GatewayManager 65200-65535 + AzureLoadBalancer |
| Bastion NSG | fixed mandatory rules (443 Internet/GatewayManager, 8080/5701 VNet …) |
| Special subnets | fixed names: GatewaySubnet, AzureFirewallSubnet, AzureBastionSubnet, RouteServerSubnet |
#Subnet Sizes
Any network at a glance (range, usable IPs in Azure, mask, split) and planning a whole address space: web page → button IP Planner at the top (key s): look up a network, lay out the hub VNet, plan a spoke with its subnets and generate the commands, check overlaps with on-premises.
| CIDR | Addresses | usable in Azure | typical for |
|---|---|---|---|
| /29 | 8 | 3 | – (minimum) |
| /28 | 16 | 11 | small NVA interfaces |
| /27 | 32 | 27 | GatewaySubnet (recommended), mgmt |
| /26 | 64 | 59 | AzureFirewallSubnet, AzureBastionSubnet |
| /25 | 128 | 123 | |
| /24 | 256 | 251 | App Gateway v2, workloads |
| /23 | 512 | 507 | |
| /22 | 1024 | 1019 | whole spoke VNet |
#Common Ports
The ports you meet most often and what they are for.
| Port | Service |
|---|---|
| 22 / 3389 | SSH / RDP |
| 53 | DNS (UDP+TCP) |
| 80 / 443 | HTTP / HTTPS |
| 500 / 4500 UDP | IPsec IKE / NAT-T |
| 179 | BGP |
| 123 UDP | NTP |
| 1433 / 5432 / 3306 | SQL Server / PostgreSQL / MySQL |
| 445 | SMB (Azure Files) |
| 8008 | default FortiGate probe port in Azure templates (HTTP) |
| 541 / 514 | FortiManager / Syslog |
| 65200–65535 | App Gateway v2 infrastructure (GatewayManager) |
#Order of Checks When "It Doesn't Work"
1. DNS – does the name resolve correctly (private/public)? nslookup / Resolve-DnsName 2. Route – where does the packet go? effective routes, show-next-hop 3. NSG – allowed? list-effective-nsg, test-ip-flow 4. Firewall – does it arrive? sniffer; why dropped? debug flow 5. Return path – back through the same firewall? (asymmetry, SNAT) 6. Target – is the service listening? ss -tlnp / Get-NetTCPConnection, OS firewall
#FortiGate CLI
Access: ssh <admin>@<mgmt-ip> · GUI https://<mgmt-ip> · in Azure also the serial console. Every config block ends with end – only then is it saved and synchronized in HA. abort discards.
Top 5:get system status·get router info routing-table all·diagnose sniffer packet any '<filter>' 4 20· debug flow ·get system ha status
#Using the CLI
Help, show, edit and the basic ways to move around the CLI.
? # help / possible input (Tab = complete)show # configuration of the current section (changed values only)show full-configuration # incl. defaultsget # current values in the sectionshow | grep <text>show full-configuration | grep -f <text> # matches with the enclosing config blockshow firewall policy | grep -i -A3 <name>config vdom ; edit root # with VDOMs enabledconfig global # global settings with VDOMsabort # leave a config block without savingdiagnose debug config-error-log read # errors during apply/import#System and License
Version, serial number, license and performance.
get system statusget system status | grep -i -E "version|serial|license"get system performance statusdiagnose sys top 5 20 # processes (q = quit)get hardware statusexecute vm-license <FortiFlex-token> # license via tokenexecute restore vmlicense tftp <file.lic> <server-ip> # license file via TFTP (otherwise GUI upload)diagnose debug vm-print-license # VM license detailsdiagnose autoupdate versions | grep -A3 "AV Engine"execute update-now # trigger FortiGuard updatediagnose debug crashlog readexecute disk listdiagnose sys logdisk statusexecute time ; execute dateexecute rebootexecute shutdownexecute factoryreset # careful!#Interfaces, Admin, DNS, NTP
Interfaces, ARP, administrators, DNS and time settings.
get system interface physicalshow system interface port1get system arpdiagnose hardware deviceinfo nic port1 # link, error countersdiagnose netlink interface list port1config system interface edit "port2" set allowaccess ping https ssh probe-response nextendshow system adminconfig system admin edit "<admin>" set trusthost1 <network> <mask> # restrict admin access nextendconfig system global set hostname "<name>" set timezone "Europe/Vienna" set admintimeout 30 set admin-sport 443endconfig system dns set primary 168.63.129.16 # Azure DNS (or your own resolver)endconfig system ntp set ntpsync enable set type fortiguardend#HA (FGCP)
Cluster status, checksums, failover and sync.
get system ha statusdiagnose sys ha checksum cluster # configuration in sync? (identical checksums)diagnose sys ha checksum show # checksums per area (find the difference)diagnose sys ha checksum recalculatediagnose sys ha history readshow full-configuration system haexecute ha synchronize start # trigger sync (on the secondary)execute ha manage <index> <admin> # switch from primary to secondaryexecute ha failover set 1 # test failover (temporary)execute ha failover unset 1 # remove the flagdiagnose sys ha reset-uptime # override disable: give up the primary roleconfig system ha set session-pickup enable set override disableendoverride disable: primary = longest HA uptime (5 min tolerance), then priority. No automatic failback.
#Azure SDN Connector
Connector status and Azure daemon debug.
diagnose sys sdn status # connector statusshow system sdn-connectordiagnose debug application azd -1 # debug the Azure daemondiagnose debug enable(wait / trigger failover)diagnose debug disable ; diagnose debug resetWith HA and SDN failover the FortiGate needs a managed identity with permissions on the affected public IPs/route tables.
#Routing
Routing table, route for one IP and static routes.
get router info routing-table allget router info routing-table details <ip> # which route applies to an IP?get router info routing-table database # inactive routes tooget router info kernel # kernel routing (FIB)show router staticconfig router static edit 0 set dst <network> <mask> set gateway <gateway> set device "port2" nextendAzure: the gateway of a subnet is always .1 of the subnet. Probe route 168.63.129.16/32 via the interface where the probe arrives.
#BGP
Summary, neighbours and advertised or received routes.
get router info bgp summaryget router info bgp neighborsget router info bgp neighbors <ip> advertised-routesget router info bgp neighbors <ip> received-routes # needs soft-reconfiguration enableget router info bgp networkexecute router clear bgp ip <ip> soft # re-exchange routesshow router bgp#Addresses, Services, Policies
Address objects, services and firewall policies.
config firewall address edit "<name>" set subnet <network> <mask> next edit "<fqdn-name>" set type fqdn set fqdn "<host.domain>" nextendconfig firewall addrgrp edit "<group>" set member "<addr1>" "<addr2>" append member "<addr3>" # add instead of replace nextendconfig firewall service custom edit "TCP-8443" set tcp-portrange 8443 nextendconfig firewall policy edit 0 set name "<name>" set srcintf "port2" set dstintf "port1" set srcaddr "<source>" set dstaddr "all" set action accept set schedule "always" set service "HTTPS" "DNS" set nat enable set logtraffic all set utm-status enable set ssl-ssh-profile "certificate-inspection" set av-profile "default" set webfilter-profile "default" set ips-sensor "default" set application-list "default" nextendconfig firewall policy move <id> before <id2> # change order edit <id> set status disable # policy off (enable = on) next delete <id>endshow firewall policyshow firewall policy <id>diagnose firewall iprope list 100004 # loaded policies (internal)diagnose firewall fqdn list # resolved FQDN addressesRule: policies are evaluated top to bottom, the first match wins.
#VIP (Port Forwarding)
Virtual IPs for inbound port forwarding.
config firewall vip edit "vip-<name>" set extip <external-ip> set extintf "port1" set portforward enable set mappedip "<internal-ip>" set extport 443 set mappedport 443 nextendshow firewall vipUse the VIP as dstaddr in the policy. Deleting: policy first, then VIP. In Azure extip is the private IP of port1 (the public IP sits on the LB/NIC), or the frontend IP with floating IP.
#IPsec VPN
Tunnel overview, SAs, IKE status and debug.
get vpn ipsec tunnel summary # overview up/downdiagnose vpn tunnel list name <phase1> # SAs, counters, selectorsdiagnose vpn ike gateway list name <phase1> # phase 1 statusdiagnose vpn tunnel up <phase2> <phase1> # bring the tunnel updiagnose vpn ike gateway clear name <phase1> # renegotiate phase 1show vpn ipsec phase1-interfaceshow vpn ipsec phase2-interface # IKE debug (filter syntax differs slightly between FortiOS versions)diagnose vpn ike log filter cleardiagnose vpn ike log filter rem-addr4 <peer-ip>diagnose debug application ike -1diagnose debug enable(bring the tunnel up)diagnose debug disable ; diagnose debug reset| Message | Cause |
|---|---|
no SA proposal chosen | encryption/hash/DH group do not match |
peer SA proposal not match local policy | phase 2 selectors (networks) differ |
authentication failed / probable pre-shared secret mismatch | wrong PSK |
negotiation timeout | peer does not answer – UDP 500/4500 blocked or wrong IP |
#Troubleshooting: Sniffer, Debug Flow, Sessions
Capture packets, trace a flow and read sessions.
# Sniffer: filter like tcpdump, verbosity 4 = with interface, packet count (Ctrl+C stops)diagnose sniffer packet any 'host <ip> and port 443' 4 20diagnose sniffer packet any 'net 10.0.0.0/24' 4 50diagnose sniffer packet port2 'icmp' 4 10diagnose sniffer packet any 'host 168.63.129.16 and port 8008' 4 20 # Azure LB probesdiagnose sniffer packet any 'host <ip>' 4 0 l # 0 = endless, l = with timestamp # Debug flow: why is a packet dropped/allowed?diagnose debug resetdiagnose debug flow filter cleardiagnose debug flow filter addr <ip>diagnose debug flow filter dport <port>diagnose debug flow show function-name enablediagnose debug console timestamp enablediagnose debug flow trace start 10diagnose debug enable(generate traffic)diagnose debug disablediagnose debug reset # Sessionsdiagnose sys session filter cleardiagnose sys session filter dst <ip>diagnose sys session filter dport 443diagnose sys session listdiagnose sys session clear # delete filtered sessionsdiagnose sys session stat # number of sessions # Reachabilityexecute ping <ip>execute ping-options source <own-ip>execute ping-options resetexecute traceroute <ip>execute telnet <ip> <port>execute ping <fqdn> # tests name resolution toodiagnose test application dnsproxy 7 # DNS cachediagnose test application dnsproxy 1 # clear DNS cache#Debug Flow Messages
What the messages of the debug flow mean.
| Message | Meaning |
|---|---|
Denied by forward policy check (policy 0) | no matching policy (implicit deny) |
reverse path check fail, drop | no route back to the source (RPF) |
find a route: flag=... gw-<ip> via <port> | selected route – wrong port = route missing |
Allowed by Policy-<id>: SNAT | allowed, source NAT active |
Allowed by Policy-<id>: (without SNAT) | allowed, without NAT |
iprope_in_check() check failed, drop | access to the FortiGate itself not allowed (allowaccess/local-in) |
no session matched | return packet without session (asymmetry) |
DNAT <ip>:<port> -> <ip>:<port> | VIP applies |
#Admin Profiles and Access
Read-only and limited administrator profiles.
config system accprofile edit "readonly-network" set netgrp read set fwgrp read set loggrp read set sysgrp read set vpngrp read nextendconfig system admin edit "<user>" set accprofile "readonly-network" set password <password> set trusthost1 <mgmt-network> <mask> set two-factor fortitoken # optional, if FortiToken is available nextendget system admin list # who is signed in right nowexecute disconnect-admin-session <index>show system accprofileBuilt-in profiles: super_admin (everything), prof_admin (everything except system) – create your own profiles for read access.
#SD-WAN
Health checks, members and rules.
diagnose sys sdwan health-check # quality per link (latency, jitter, loss)diagnose sys sdwan member # members and statusdiagnose sys sdwan service # rules and selected link (older versions: service4)get router info routing-table all # SD-WAN routesshow system sdwanconfig system sdwan config health-check edit "internet" set server "8.8.8.8" "1.1.1.1" set protocol ping set members 0 # 0 = all members next endend#SSL VPN and Dial-up VPN
Signed-in users and disconnecting sessions.
get vpn ssl monitor # signed-in SSL VPN usersexecute vpn sslvpn listexecute vpn sslvpn del-tunnel <index> # disconnect a sessiondiagnose debug application sslvpn -1diagnose debug enable(try to sign in)diagnose debug disable ; diagnose debug resetdiagnose vpn ike gateway list # IPsec dial-up clients (FortiClient)Note: in newer FortiOS versions the SSL VPN tunnel mode has been removed on many models – the replacement is IPsec dial-up (IKEv2) with FortiClient or ZTNA. Check the release notes before upgrading.
#Firmware Upgrade (HA)
Upgrade a cluster without interruption – checks before and after.
get system status # current versiondiagnose sys ha checksum cluster # in sync before?show full-configuration system ha | grep -i uninterruptibleexecute backup full-config tftp <file> <server-ip>execute restore image tftp <image.out> <server-ip> # or GUI: System -> Firmware -> Upload# afterwardsget system statusget system ha statusdiagnose sys ha history readSequence in HA with uninterruptible-upgrade enable: the secondary is upgraded first, then failover, then the old primary. Check the upgrade path beforehand with the FortiOS Upgrade Path Tool (docs.fortinet.com).
#FortiManager Connection
Connect the device to FortiManager.
config system central-management set type fortimanager set fmg "<fortimanager-ip-or-fqdn>"endget system central-managementdiagnose debug application fgfmd -1 # debug the connection to FortiManagerdiagnose debug enablediagnose debug disable ; diagnose debug resetexecute ping <fortimanager-ip>execute telnet <fortimanager-ip> 541 # port 541 (FGFM) must be openThen in FortiManager: Device Manager → authorize the device. From then on make changes only via FortiManager, otherwise the next install will conflict.
#Logs
Filter and read logs on the device.
execute log filter resetexecute log filter category 0 # 0 = trafficexecute log filter field dstip <ip>execute log filter view-lines 50execute log display#Backup
Back up the configuration via CLI or GUI.
execute backup config tftp <file> <server-ip>execute backup full-config ftp <file> <server-ip> <user> <pass>Easier: GUI → admin menu (top right) → Configuration → Backup.
#Terraform and Bicep Workflows
Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.
#New Terraform project with remote state
Storage account for the state, role assignment, backend config and first init.
Storage account for the state – with versioning and your own data role:
$rg = "rg-tfstate" ; $loc = "<region>" ; $sa = "<sttfstateunique>" # 3–24 chars, lowercase letters/digits onlyaz group create -n $rg -l $locaz storage account create -g $rg -n $sa -l $loc --sku Standard_ZRS --min-tls-version TLS1_2 --allow-blob-public-access falseaz storage account blob-service-properties update -g $rg --account-name $sa --enable-versioning true$saId = az storage account show -g $rg -n $sa --query id -o tsv$me = az ad signed-in-user show --query id -o tsvaz role assignment create --assignee-object-id $me --assignee-principal-type User --role "Storage Blob Data Contributor" --scope $saIdaz storage container create --account-name $sa -n tfstate --auth-mode login # role may take 1–2 minutesBackend configuration in backend.tf:
terraform { backend "azurerm" { resource_group_name = "rg-tfstate" storage_account_name = "<sttfstateunique>" container_name = "tfstate" key = "<project>.tfstate" use_azuread_auth = true }}Initialise and plan
$env:ARM_SUBSCRIPTION_ID = az account show --query id -o tsvterraform initterraform plan -out tfplanexport ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv)terraform initterraform plan -out tfplan#Terraform locally with a service principal
Sign in as a service principal through environment variables and clean up afterwards.
Set the variables and run Terraform
$env:ARM_TENANT_ID = "<tenant-id>"$env:ARM_SUBSCRIPTION_ID = "<subscription-id>"$env:ARM_CLIENT_ID = "<app-id>"$env:ARM_CLIENT_SECRET = "<client-secret>"terraform initterraform plan -out tfplanterraform apply tfplanexport ARM_TENANT_ID="<tenant-id>"export ARM_SUBSCRIPTION_ID="<subscription-id>"export ARM_CLIENT_ID="<app-id>"export ARM_CLIENT_SECRET="<client-secret>"terraform initterraform plan -out tfplanterraform apply tfplanRemove the secret from the session
Remove-Item Env:ARM_CLIENT_SECRET, Env:ARM_CLIENT_IDunset ARM_CLIENT_SECRET ARM_CLIENT_IDTip: Without ARM_CLIENT_* Terraform uses the az login session.
#Import an existing resource into Terraform
Bring a portal-created resource under Terraform control without recreating it.
# imports.tfimport { to = azurerm_virtual_network.hub id = "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Network/virtualNetworks/<vnet>"}az network vnet show -g <rg> -n <vnet> --query id -o tsv # get the IDterraform plan -generate-config-out=generated.tf # let Terraform generate code# review generated.tf, clean up, move into the right fileterraform plan # goal: "1 to import, 0 to add, 0 to change, 0 to destroy"terraform apply# then delete the import block (or keep it as documentation)#Rename a resource or move it into a module
Use moved blocks so Terraform renames instead of destroying and recreating.
moved { from = azurerm_subnet.app to = module.spoke.azurerm_subnet.this["app"]}terraform plan # must show "has moved to" and NO destroy/createterraform apply#State lock is stuck
Release a stuck state lock – with force-unlock, or by breaking the blob lease.
# The lock ID is in the error message ("ID: …")terraform force-unlock <lock-id># If that doesn't work: break the lease on the blobaz storage blob lease break --account-name <sa> -c tfstate -b <project>.tfstate --auth-mode loginMake sure no other run (pipeline, colleague) is active first.
#Check drift (changes made in the portal)
Show what changed outside Terraform and decide: keep it in code or revert it.
terraform plan -refresh-only # shows what changed outside Terraform# keep the change -> put it into code, then normal plan/apply# revert the change -> normal plan/apply overwrites itterraform plan -detailed-exitcode ; $LASTEXITCODE # 0 = clean, 2 = differences (e.g. for a scheduled check)#Update the provider version
Raise the provider version safely and watch for unexpected replacements.
# 1. Raise the version in required_providers (e.g. "~> 4.0" -> "~> 4.20")terraform init -upgradeterraform providers lock -platform=windows_amd64 -platform=linux_amd64terraform plan # watch for unexpected changes/replacementsgit add .terraform.lock.hcl ; git commit -m "Update azurerm provider"#Deploy Bicep safely
Validate, what-if, deploy – and find the failed resource if it breaks.
$rg = "<rg>"az bicep lint -f main.bicepaz deployment group validate -g $rg -f main.bicep -p main.bicepparamaz deployment group what-if -g $rg -f main.bicep -p main.bicepparamaz deployment group create -g $rg -f main.bicep -p main.bicepparam -n "deploy-$(Get-Date -Format yyyyMMdd-HHmm)"# On failure: which resource?az deployment operation group list -g $rg -n <deployment-name> ` --query "[?properties.provisioningState=='Failed'].{res:properties.targetResource.resourceName, error:properties.statusMessage.error.message}" -o tableTip: macOS / Linux (Bash): rg="<rg>", line continuation \, deployment name -n "deploy-$(date +%Y%m%d-%H%M)".
#Git and Azure DevOps Workflows
From the first SSH key to releasing Dev to main. Open a workflow, adjust the variables at the top, run block by block.
#SSH key for Azure DevOps and first push
Generate an RSA key, register it in Azure DevOps, configure SSH and push for the first time.
Caution: There is no SSH password in Azure DevOps. If you see git@ssh.dev.azure.com's password:, the key was not accepted – Ctrl+C and check steps 2 and 3. Use RSA, not ed25519.
# 1. Generate a key (-C is only a label, e.g. the device name; passphrase optional)ssh-keygen -t rsa -b 4096 -f $HOME\.ssh\id_rsa_azure -C "<device>-azure" # 2. Copy the PUBLIC key -> Azure DevOps: top right User settings -> SSH public keys -> + New Key -> pasteGet-Content $HOME\.ssh\id_rsa_azure.pub | Set-Clipboard # must start with ssh-rsa AAAA… # 3. SSH config – this key for Azure DevOpsAdd-Content $HOME\.ssh\config @" Host ssh.dev.azure.com HostName ssh.dev.azure.com User git IdentityFile ~/.ssh/id_rsa_azure IdentitiesOnly yes"@ # 4. Test – answer "Shell access is not supported" = OK (first time: confirm the fingerprint with yes)ssh -T git@ssh.dev.azure.com # 5. Connect the repo and pushgit remote add origin git@ssh.dev.azure.com:v3/<org>/<project>/<repo>git push -u origin main# 1. Generate a key (-C is only a label, e.g. the device name; passphrase optional)ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_azure -C "<device>-azure" # 2. Copy the PUBLIC key -> Azure DevOps: top right User settings -> SSH public keys -> + New Key -> pastepbcopy < ~/.ssh/id_rsa_azure.pub # macOS (no output); Linux: cat ~/.ssh/id_rsa_azure.pub # 3. SSH config – this key for Azure DevOpscat >> ~/.ssh/config <<'EOF' Host ssh.dev.azure.com HostName ssh.dev.azure.com User git IdentityFile ~/.ssh/id_rsa_azure IdentitiesOnly yesEOFchmod 600 ~/.ssh/config # 4. Test – answer "Shell access is not supported" = OK (first time: confirm the fingerprint with yes)ssh -T git@ssh.dev.azure.com # 5. Connect the repo and pushgit remote add origin git@ssh.dev.azure.com:v3/<org>/<project>/<repo>git push -u origin mainCaution: id_rsa_azure is the private key – never share or upload it. Only id_rsa_azure.pub goes into Azure DevOps.
Test fails or shows warnings?
- Fingerprint on the first connection (RSA):
SHA256:ohD8VZEXGWo6Ez8GSEJQ9WpafgLFsOfLOtGGQCQo6Og– then stored in~/.ssh/known_hosts. no matching host key type/no mutual signature algorithm(old OpenSSH): addHostkeyAlgorithms +ssh-rsaandPubkeyAcceptedKeyTypes +ssh-rsato theHostblock – a legacy workaround; ifssh-rsa is about to be deprecated … throttledappears, remove both lines again.post-quantum key exchangewarning: harmless.- Old or wrong keys in Azure DevOps: delete them.
- Password prompt or
Permission denied (publickey): Azure DevOps: test and debug.
#Clone an existing DevOps repo
Copy the clone URL and check out the right branch.
Address: DevOps → Repos → select repo → top right Clone → copy the URL.
cd C:\Git # folder where the repo should go (macOS e.g. cd ~/Projekte)git clone git@ssh.dev.azure.com:v3/<org>/<project>/<repo> # via SSH (key registered in DevOps)git clone https://<org>@dev.azure.com/<org>/<project>/_git/<repo> # via HTTPS – "password" = personal access tokencd <repo>git branch -a # which branches exist? (e.g. main, Dev)git switch Dev # switch to the working branchRun git clone in the parent folder – inside a folder with the repo's name you end up with <repo>/<repo>. Spaces in the project name are %20 in the URL. HTTPS: the PAT is created under User settings → Personal access tokens (scope Code: Read & write). Repo name unknown:
az extension add -n azure-devopsaz devops configure --defaults organization=https://dev.azure.com/<org> project=<project>az repos list --query "[].{name:name, ssh:sshUrl, https:remoteUrl}" -o table#Connect a local folder to an existing DevOps repo
Link a local folder to the repo and push through a feature branch if main is protected.
cd <your-folder>git init -b main # only if there is no .git folder yetgit remote add origin git@ssh.dev.azure.com:v3/<org>/<project>/<repo>git pull origin main --allow-unrelated-histories # only needed if the repo already has commitsgit add -A ; git commit -m "<message>"git push -u origin main # with protected main: feature branch + PR (see below)#Push a folder as a new repo (HTTPS)
Initialise a folder, check for secrets, add the remote and push over HTTPS.
cd <folder>git init -b maingit add .git status # no tfstate, tfvars, pfx, licenses?git commit -m "Initial commit"git remote add origin https://dev.azure.com/<org>/<project>/_git/<repo>git push -u origin main # sign in via browser or PAT as passwordRemote already has a README: before pushing run git pull origin main --allow-unrelated-histories.
#Empty repo: first push and missing main
In an empty repo the first pushed branch becomes the default branch – if that was a feature branch, main is missing.
# first push into an empty repogit switch -c main # or: git branch -M maingit add -Agit commit -m "Initial commit"git push -u origin main # main is missing on the server – what is there?git ls-remote --heads origingit push origin feature/<name>:main # create main on the server from the feature branchThen DevOps → Repos → Branches → main → ⋯ → Set as default branch. A pull request needs a second branch with changes.
#Change via feature branch and pull request to Dev
The normal path: own branch, push, pull request to Dev, clean up. Works the same for any repo – only the target branch differs.
Branch from the latest state, change, push
git switch Dev ; git pull # get the latest stategit switch -c feature/<name> # own branch# … change …git add -Agit commit -m "<what and why>"git push -u origin feature/<name>Create the pull request – browser: after the push, Create a pull request appears at the top → target Dev. Or via CLI:
az repos pr create --organization https://dev.azure.com/<org> --project <project> --repository <repo> ` --source-branch feature/<name> --target-branch Dev --title "<title>" --openAfter the merge – clean up
git switch Dev ; git pullgit branch -d feature/<name> # clean up the local feature branch#Reviewers in a pull request
Required and optional reviewers – who has to approve and what to do if nobody is listed.
| Field | Meaning |
|---|---|
| Required reviewers | mandatory reviewers – without their Approve the PR cannot be completed |
| Optional reviewers | get notified, approval not needed |
Click Create without entries – the branch policy often adds the required reviewers (e.g. a team group) automatically.
Nobody listed? Add the colleague or team lead who approves for your team. Old PRs show who: Repos → Pull requests → Completed.
Wait for approvals and green checks → Complete.
Note: You usually cannot approve your own PR.
#Resolve a merge conflict (pull request or git pull)
The PR shows "There are merge conflicts": the same lines were changed in Dev since your branch was created.
Merge Dev into your branch – Git reports the conflicts:
git switch feature/<name>git fetch origingit merge origin/Dev # reports CONFLICTgit status # affected files ("both modified")code . # in VS Code: Accept Current / Incoming / BothFix the file – keep the right version, delete the three marker lines:
<<<<<<< HEADparam location = 'westeurope' ← your version=======param location = 'austriaeast' ← version from Dev>>>>>>> origin/DevFinish the merge
git add <file> # for each resolved filegit commit # merge commitgit push # the PR updates automaticallygit merge --abort # instead: abort everythingTip: In parameter files keep new parameters from Dev. Who changed them: git log origin/Dev -- <file>.
Note: Depending on the policy the approval is reset after the push – the reviewer must click Approve again.
Tip: A conflict after a plain git pull (no PR) is solved the same way from step 2 on – finish with git commit (or git rebase --continue); abort everything with git merge --abort.
#Push rejected: TF402455 (protected branch)
The branch is protected by a policy – move your commits to a feature branch and open a pull request instead.
! [remote rejected] Dev -> Dev (TF402455: Pushes to this branch are not permitted; you must use a pull request to update this branch.)Move your commits to a feature branch – they are already on the local Dev:
git switch -c feature/<name> # new branch from the current state – commits come alonggit push -u origin feature/<name> # this is allowedgit log --oneline origin/feature/<name> -3 # check: your commits are on the serverCreate the pull request feature/<name> → Dev (see above).
Bring local Dev back to the server state
git switch Devgit fetch origingit reset --hard origin/DevCaution: Run reset --hard only once the feature branch is safely on the server.
#Push rejected: non-fast-forward (server is ahead)
Your local branch is behind the server – rebase first, or take the safe way with a backup branch.
! [rejected] main -> main (non-fast-forward)First attempt: rebase onto the server
git pull --rebase origin maingit pushDoes not work (refusing to merge unrelated histories, or conflicts you do not want to resolve) – the safe way with a backup branch:
git rebase --abort # only if a rebase is runninggit branch backup-main # your state as a backupgit fetch origingit reset --hard origin/main # local = servergit checkout backup-main -- . # take your files overgit statusgit add -Agit commit -m "<message>"git pushgit branch -D backup-main # once everything looks goodCaution: git checkout backup-main -- . overwrites files that exist in both – files deleted only on your side come back from the server.
#Git: quickly switch branches in between
Park the current work with stash, change something elsewhere, come back.
git stash -u # park current workgit switch main ; git pullgit switch -c fix/<name># … change …git add -A ; git commit -m "<message>" ; git push -u origin fix/<name>git switch <old-branch>git stash pop # continue working#Undo a pull request (abandon / revert)
Abandon an open PR or revert a completed one, then clean up locally.
PR still open → abandon it, nothing changes in Dev. Browser: in the PR, next to Complete click ▾ / ⋯ → Abandon (Reactivate revives it). Or:
az repos pr list --organization https://dev.azure.com/<org> --project <project> -o table # PR numberaz repos pr update --id <pr-number> --status abandoned --organization https://dev.azure.com/<org>PR already completed → counter-change: open the PR → top right Revert → Create pull request → new PR to Dev, needs approval again.
3. Clean up locally and on the server
git merge --abort # if in the middle of a conflict mergegit switch Devgit fetch origingit reset --hard origin/Dev # local Dev = server stategit branch -D feature/<name> # delete feature branch locally (only if no longer needed)git push origin --delete feature/<name> # and on the serverTo keep the changes and just start over: only Abandon, don't delete the branch.
#Git: secret committed by accident
Not pushed: undo the commit. Pushed: revoke the secret first, then clean the history.
# NOT pushed yetgit reset --soft HEAD~1 # undo the commit, files staygit restore --staged <file>Add-Content .gitignore "<file-or-pattern>"git add -A ; git commit -m "<message>" # Already pushed# 1. Change/revoke the secret IMMEDIATELY (Key Vault, app registration, PAT …)# 2. Remove the file from the repogit rm --cached <file>Add-Content .gitignore "<file-or-pattern>"git commit -m "Remove secret file" ; git pushThe old version stays in the history – that is why only changing the secret counts.
#Promote Dev to main (release)
Check what comes along, create the Dev → main PR, complete it without deleting Dev.
Check what comes along – that is everything in Dev, including colleagues' work:
git fetch origingit log --oneline origin/main..origin/Dev # commits in Dev that are not in main yetgit diff --stat origin/main...origin/Dev # changed filesCreate the PR – browser: Repos → Pull requests → New pull request → source Dev, target main. Or:
az repos pr create --organization https://dev.azure.com/<org> --project <project> --repository <repo> ` --source-branch Dev --target-branch main --title "Release: Dev to main" --openComplete with these settings:
Caution: Turn off "Delete Dev after merging" – otherwise the Dev branch is gone. Merge type Merge (no fast-forward), not Squash – otherwise Dev and main diverge.
Conflict in the Dev → main PR (main was changed directly, e.g. a hotfix)
Bring main into Dev first:
git switch Dev ; git pullgit switch -c merge/main-into-devgit merge origin/main # resolve conflicts as abovegit add -A ; git commitgit push -u origin merge/main-into-dev # PR to Dev -> afterwards Dev -> main is conflict-free#Publish a static website via pipeline (Static Web App)
Works for any static site (HTML/CSS/JS), e.g. this page. The pipeline uses the deployment token of the app – no service connection needed, and the repo may live in another tenant. Generator: DevOps and Git → Static Web App + pipeline.
Create the static web app (once) – portal: Create Static Web App → Plan: Free, Source: Other, region Global → Review + create. Or CLI:
az login --tenant <target-tenant-id>az account set --subscription "<subscription>"$rg = "<rg>"; $swa = "<swa-name>"az group create -n $rg -l westeuropeaz staticwebapp create -n $swa -g $rg -l westeurope --sku Freeaz staticwebapp show -n $swa -g $rg --query defaultHostName -o tsv # URL / CNAME targetaz staticwebapp secrets list -n $swa -g $rg --query "properties.apiKey" -o tsv # deployment tokenPortal: Static Web App → Manage deployment token → copy.
Caution: Source: Other – not "Azure DevOps", that needs the same tenant. Plan Standard is only needed for tenant-only login without invitations.
Pipeline file in the repo – pipelines/static-web-app.yml:
trigger: branches: include: [ main ] pool: vmImage: ubuntu-latest variables: appRoot: '.' # files in a subfolder of the repo (e.g. promptus/)? -> 'promptus' steps:- checkout: self - script: | set -e mkdir -p site python3 tools/build-html.py -o site/index.html echo "--- site/"; ls -la site workingDirectory: $(appRoot) displayName: Build site/index.html - task: AzureStaticWebApp@0 displayName: Deploy to Static Web App inputs: app_location: $(appRoot)/site skip_app_build: true skip_api_build: true azure_static_web_apps_api_token: $(SWA_TOKEN)Plain HTML site without build step: drop the script step and set app_location to the folder with index.html. Then commit:
git add pipelines/static-web-app.ymlgit commit -m "Add static web app pipeline"git push # protected main -> feature branch + PRRegister the pipeline in DevOps (once) – DevOps does not pick up YAML files by itself: Pipelines → New pipeline → Azure Repos Git → repo → Existing Azure Pipelines YAML file → main, /pipelines/static-web-app.yml → Variables → New variable SWA_TOKEN = token, tick Keep this value secret → Save → Run. From now on every push to main deploys automatically (Pipelines → Runs).
Custom domain – at the DNS provider: CNAME promptus -> <defaultHostName>. Then portal: Custom domains → + Add → Custom domain on other DNS, or:
az staticwebapp hostname set -n $swa -g $rg --hostname promptus.cloudorbis.ataz staticwebapp hostname show -n $swa -g $rg --hostname promptus.cloudorbis.at --query status -o tsv # Ready = certificate issued5. Optional: restrict the site to your team
In site/staticwebapp.config.json:
"routes": [ { "route": "/*", "allowedRoles": [ "team" ] } ],"responseOverrides": { "401": { "redirect": "/.auth/login/aad", "statusCode": 302 } }Portal → Role management → Invite → Microsoft Entra ID, role team (Free: up to 25 invitations). Tenant-only without invitations: Standard plan with a custom Entra ID provider.
If it fails: errors and fixes
| Error | Cause | Fix |
|---|---|---|
App Directory Location: 'site' is invalid. Could not detect this directory | folder not built / files in a subfolder / old build script | pipeline above (mkdir -p site, -o site/index.html), set appRoot |
No hosted parallelism has been purchased or granted | new DevOps organization without free agent | request the free grant (link in the error), meanwhile deploy locally (below) |
deployment token error / Unauthorized | variable missing, wrong name or old token | variable exactly SWA_TOKEN, saved; new token: az staticwebapp secrets reset-api-key -n $swa -g $rg |
| pipeline does not start on push | branch is master, not main / pipeline not registered | adjust include:, step 3 |
| domain stays Validating | CNAME missing or still propagating | Resolve-DnsName promptus.cloudorbis.at -Type CNAME, wait |
Deploy locally without a pipeline (needs Node.js)
python tools/build-html.py$token = az staticwebapp secrets list -n $swa -g $rg --query "properties.apiKey" -o tsvnpx --yes @azure/static-web-apps-cli deploy ./site --deployment-token $token --env production#Git
Top 5:git status·git pull·git add -A ; git commit -m "..."·git push·git log --oneline --graph -20
#Setup
Name, e-mail and basic settings.
git --versiongit config --global user.name "First Last"git config --global user.email "<mail>"git config --global init.defaultBranch maingit config --global core.autocrlf true # Windows: CRLF locally, LF in the repogit config --global pull.rebase falsegit config --global alias.lg "log --oneline --graph --decorate -20" # git lggit config --list --show-origin#New Repo / Push a Folder
Create a repo from a folder and push it.
cd <folder>git init -b maingit add .git status # check: no secrets, states, licenses!git commit -m "Initial commit"git remote add origin <url>git push -u origin main# Remote already has commits (e.g. README):git pull origin main --allow-unrelated-historiesgit push -u origin main#Daily Work
Clone, pull, commit, push and check status.
git clone <url> [<folder>]git pull # fetch + mergegit fetch --all --prune # fetch onlygit status ; git status -sgit diff ; git diff --stagedgit add <file> ; git add -A # everything incl. deletionsgit add -p # changes piece by piecegit commit -m "<message>"git commit --amend # change the last commit (only if not pushed)git commit --amend --no-edit # add a file, keep the messagegit push#Branches and Merge
Create, switch, merge and delete branches.
git branch -agit switch -c feature/<name> # create a new branch and switchgit switch maingit push -u origin feature/<name>git merge feature/<name>git merge --abort # abort a merge with conflictsgit rebase main # update your branch onto maingit rebase --abortgit cherry-pick <commit> # take a single commitgit branch -d feature/<name> # delete locallygit push origin --delete feature/<name> # delete remotelygit tag v1.0 ; git push origin --tagsResolve a conflict: edit the file (remove <<<<<<< … >>>>>>>) → git add <file> → git commit (or git rebase --continue).
#Search and History
Log, blame and search in the history.
git log --oneline --graph --decorate -20git log -p -- <file> # history of a file with changesgit log --follow -- <file> # across renames toogit log --grep "<text>" # search commit messagesgit log -S "<text>" # commits that added/removed <text>git log --since "2 weeks ago" --author "<name>"git grep -n "<text>" # search the current stategit blame <file>git show <commit>git show <commit>:<path> # show a file at an old stategit diff main...feature/<name> # what does the branch bring?git diff --stat <commit1> <commit2>#Undo
Discard changes, unstage, revert and reset.
git restore <file> # discard local changegit restore --staged <file> # unstagegit restore --source <commit> <file> # get a file from an old stategit reset --soft HEAD~1 # undo last commit, keep changesgit reset --hard HEAD~1 # discard last commit (careful!)git reset --hard origin/main # local = remote (careful!)git revert <commit> # counter-commit (safe for pushed state)git stash ; git stash list ; git stash pop # park changesgit stash -u # incl. untracked filesgit clean -n ; git clean -fd # show/delete untracked filesgit reflog # find "lost" commits| Situation | Command |
|---|---|
| File broken, not committed | git restore <file> |
| Wrong file added | git restore --staged <file> |
| Wrong commit message, not pushed | git commit --amend |
| Last commit wrong, not pushed | git reset --soft HEAD~1 |
| Commit already pushed | git revert <commit> |
| Everything gone after reset | git reflog → git reset --hard <hash> |
#Ignore and Secrets
.gitignore, removing files from Git and keeping secrets out.
git check-ignore -v <file> # why is it ignored?git add -f <file> # add despite .gitignoregit rm --cached <file> # remove from Git, keep locallygit rm -r --cached . ; git add . # rebuild after a .gitignore changegit ls-files | Select-String "tfstate|tfvars|\.pfx|\.key|\.lic" # check before pushing (macOS / Linux: grep -E "tfstate|tfvars|\.pfx|\.key|\.lic")Secret pushed by accident: change/revoke the secret immediately – removing it from history is not enough.
#Remote
Show, change and add remotes.
git remote -vgit remote set-url origin <new-url> # e.g. SSH <-> HTTPSgit remote add upstream <url>git ls-remote origin # branches/tags on the servergit ls-remote --heads origin # only branchesgit push origin <branch>:main # create/update main on the server from a local branchPush rejected because the server is ahead: non-fast-forward · empty repo without main: first push and missing main.
#Rebase or Merge?
The difference and when to use which.
| Merge | Rebase | |
|---|---|---|
| What happens | a merge commit joins both lines | your commits are "replayed" onto the new state |
| History | branched, complete | straight line |
| Risk | low | rewrites commits – never on shared branches (Dev, main) |
| When | branch is already pushed and others work on it | tidy up your own feature branch before the PR |
# Bring your feature branch up to the latest Dev (rebase)git switch feature/<name>git fetch origingit rebase origin/Dev# on conflict: fix the file -> git add <file> -> git rebase --continue (abort: git rebase --abort)git push --force-with-lease # needed after rebase; only overwrites if nobody else pushed # Squash several own commits into one before the PRgit reset --soft origin/Dev # all changes stay, commits gonegit commit -m "<one clean message>"git push --force-with-lease#Take Single Commits and Files
cherry-pick and files from other branches.
git log --oneline origin/<other-branch> -10 # find the commit hashgit cherry-pick <commit> # take exactly this commitgit cherry-pick <commit1> <commit2>git cherry-pick --abort git restore --source origin/<other-branch> -- <path/file> # get only one file from another branchgit show origin/<branch>:<path/file> # just view a file from another branchgit diff Dev..feature/<name> -- <path/file> # compare one file between branches#Who Changed This?
blame and the history of single lines.
git blame -L 10,30 <file> # lines 10–30 with author and commitgit log -L 10,30:<file> # history of exactly these linesgit log --follow -p -- <file> # all changes to a file, even after renamesgit log -S "<text>" --oneline # when was <text> added / removedgit log --author "<name>" --since "1 week ago" --onelinegit shortlog -sn --since "1 month ago" # who has how many commits#Aliases and Useful Settings
Shortcuts and settings worth setting once.
git config --global alias.st "status -s"git config --global alias.lg "log --oneline --graph --decorate -20"git config --global alias.last "log -1 --stat"git config --global alias.unstage "restore --staged"git config --global alias.amend "commit --amend --no-edit"git config --global alias.br "branch -vv" # branches with upstream and state git config --global fetch.prune true # remove deleted remote branches automaticallygit config --global rerere.enabled true # remember resolved conflictsgit config --global push.autoSetupRemote true # "git push" without -u the first timegit config --global core.editor "code --wait" # VS Code as editorgit config --global merge.tool vscodegit config --global mergetool.vscode.cmd 'code --wait $MERGED'Then: git st, git lg, git last, git br.
#Work on Several Branches at Once
A second working folder with git worktree.
git worktree add ..\<repo>-hotfix main # second working folder on main, without switchinggit worktree listgit worktree remove ..\<repo>-hotfixHandy when a hotfix on main comes in while the feature branch is half done.
#Commit Messages
Short, imperative, what and why: Hub: add UDR for GatewaySubnet · FortiGate: license token as variable. Optional prefix: feat:, fix:, docs:, refactor:, chore:.
#SSH and Certificates (OpenSSL)
#Keys
-C is only a label (e-mail or device name). id_rsa = private key (never share), id_rsa.pub = public key (goes into Azure DevOps, GitHub, servers).
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_azure -C "<device>-azure" # macOS / Linux: RSA for Azure DevOpspbcopy < ~/.ssh/id_rsa_azure.pub # macOS: public key to the clipboardcat ~/.ssh/id_rsa_azure.pub # show it (starts with ssh-rsa AAAA…)ssh-keygen -t ed25519 -C "<comment>" # default (not for Azure DevOps)ssh-keygen -t rsa -b 4096 -f $HOME\.ssh\id_rsa # RSA (Azure DevOps)Get-Content $HOME\.ssh\id_rsa.pub | Set-Clipboard # copy the public keyGet-Content $HOME\.ssh\id_rsa -TotalCount 1 # check format: -----BEGIN OPENSSH PRIVATE KEY-----ssh-keygen -y -f $HOME\.ssh\id_rsa # derive public key from private keyssh-keygen -p -f $HOME\.ssh\id_rsa # change passphrasessh-keygen -lf $HOME\.ssh\id_rsa.pub # fingerprintssh-keygen -R <host-or-ip> # remove an old host keyRemove-Item $HOME\.ssh\config -ErrorAction SilentlyContinue # remove a broken SSH configaz sshkey create -g <rg> -n <name> # key as an Azure resource#Connect and Test
Connect, jump hosts, port forwarding and copying files.
ssh <user>@<host>ssh -i <private-key> <user>@<host>ssh -p 2222 <user>@<host>ssh -L 8443:<target>:443 <user>@<jump> # local port forwarding -> https://localhost:8443ssh -D 1080 <user>@<jump> # SOCKS proxy via the jump hostssh -J <user>@<jump> <user>@<target> # via jump hostscp <file> <user>@<host>:/tmp/scp <user>@<host>:/var/log/syslog .ssh -T git@ssh.dev.azure.com # Azure DevOps: "Shell access is not supported" = OKssh -T git@github.comssh -vT <host> # troubleshooting#~/.ssh/config
Windows: C:\Users\<user>\.ssh\config – file without extension. macOS / Linux: ~/.ssh/config, then chmod 600 ~/.ssh/config (edit e.g. with nano ~/.ssh/config → Ctrl+O, Enter, Ctrl+X).
Host ssh.dev.azure.com HostName ssh.dev.azure.com User git IdentityFile ~/.ssh/id_rsa_azure IdentitiesOnly yes # only for "no matching host key type" / "no mutual signature algorithm" (legacy workaround): # HostkeyAlgorithms +ssh-rsa # PubkeyAcceptedKeyTypes +ssh-rsa Host fgt-a HostName <mgmt-ip> User <admin> Host vm-internal HostName 10.0.1.4 User azureuser ProxyJump jumpThen ssh fgt-a is enough.
#Azure DevOps: Test and Debug
Test the key against Azure DevOps and read the debug output.
ssh -T git@ssh.dev.azure.com # OK = "remote: Shell access is not supported."ssh -vT git@ssh.dev.azure.com 2>&1 | grep -iE "config|identity|offering|accept|denied|auth"ssh -i ~/.ssh/id_rsa_azure -T git@ssh.dev.azure.com # use the key once, without the configPowerShell: ssh -vT git@ssh.dev.azure.com 2>&1 | Select-String "config|identity|offering|accept|denied|auth"
| You see | Meaning |
|---|---|
git@ssh.dev.azure.com's password: | key not accepted – Azure DevOps has no SSH password, Ctrl+C |
Offering public key: …id_rsa_azure but no Accepted | key is offered, but not registered in Azure DevOps (or in another account / organization) |
no Offering line | config not applied – file name (no extension), Host ssh.dev.azure.com, IdentityFile path |
no matching host key type / no mutual signature algorithm | old OpenSSH or key – update; as a workaround add HostkeyAlgorithms +ssh-rsa / PubkeyAcceptedKeyTypes +ssh-rsa |
ssh-rsa is about to be deprecated and your request has been throttled | the +ssh-rsa workaround is active – remove both lines from the config |
| fingerprint question on the first connection | answer yes if it shows SHA256:ohD8VZEXGWo6Ez8GSEJQ9WpafgLFsOfLOtGGQCQo6Og |
post-quantum key exchange warning | harmless |
#ssh-agent
Enter the passphrase only once.
# once as administratorGet-Service ssh-agent | Set-Service -StartupType AutomaticStart-Service ssh-agent# as userssh-add $HOME\.ssh\id_rsassh-add -lgit config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe" # Git uses Windows SSH#Permissions
Error "UNPROTECTED PRIVATE KEY FILE / too open":
icacls $HOME\.ssh\id_rsa /inheritance:r /grant:r "$($env:USERNAME):(R)"chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_rsa#Certificates with OpenSSL
Read certificates, convert formats, check key and certificate, create a CSR.
# Show a server's certificate (issuer, expiry, SAN)openssl s_client -connect <host>:443 -servername <host> </dev/null 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates -ext subjectAltName # Read a certificate fileopenssl x509 -in cert.pem -noout -textopenssl x509 -in cert.pem -noout -enddateopenssl x509 -in cert.cer -inform der -out cert.pem # DER -> PEM # Split / build a PFXopenssl pkcs12 -in cert.pfx -nokeys -out cert.pemopenssl pkcs12 -in cert.pfx -nocerts -nodes -out key.pemopenssl pkcs12 -export -out cert.pfx -inkey key.pem -in cert.pem -certfile chain.pemopenssl pkcs12 -in old.pfx -legacy -nodes -out all.pem # old PFX with OpenSSL 3 # Do key and certificate match? (outputs must be identical)openssl x509 -noout -modulus -in cert.pem | openssl sha256openssl rsa -noout -modulus -in key.pem | openssl sha256 # CSR and self-signed test certificateopenssl req -new -newkey rsa:2048 -nodes -keyout key.pem -out req.csr -subj "/CN=<fqdn>"openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 \ -subj "/CN=<fqdn>" -addext "subjectAltName=DNS:<fqdn>"PowerShell:
Get-PfxCertificate .\cert.pfx | Format-List Subject, NotAfter, DnsNameListNew-SelfSignedCertificate -DnsName <fqdn> -CertStoreLocation Cert:\CurrentUser\MyApp Gateway listeners need a PFX with private key and the full chain.
#Azure DevOps
#In the Portal
Where to click: repos, keys, tokens, branch policies and pull requests.
| Task | Where |
|---|---|
| Create a repo | Project → Repos → repo picker → + New repository (without README when pushing an existing folder) |
| Clone URL | Repos → Clone (HTTPS or SSH) |
| SSH key | top right User settings → SSH public keys → + New Key (RSA only) |
| Personal access token | User settings → Personal access tokens → scope Code: Read & Write |
| Branch protection | Repos → Branches → main → Branch policies (pull request, reviewers, build) |
| Pull request | Repos → Pull requests → New (choose source → target) |
| Abandon a PR | in the PR: Complete ▾ / ⋯ → Abandon |
| Undo a completed PR | in the PR: Revert → Create pull request |
| Reviewers | Required = mandatory approval, Optional = info only |
| Service connection | Project settings → Service connections → Azure Resource Manager (workload identity federation) |
| Variable group / secrets | Pipelines → Library (optionally linked to Key Vault) |
| Approval before prod | Pipelines → Environments → Approvals and checks |
Complete flow feature → Dev → main: Workflows → Azure DevOps.
URL formats:
HTTPS: https://dev.azure.com/<org>/<project>/_git/<repo>SSH: git@ssh.dev.azure.com:v3/<org>/<project>/<repo>#CLI
Sign in and work with repos, pull requests and pipelines from the CLI.
az extension add -n azure-devopsaz devops login # enter PATaz devops configure --defaults organization=https://dev.azure.com/<org> project=<project>az repos list -o tableaz repos create --name <repo>az repos pr list -o tableaz repos pr create --source-branch feature/<name> --target-branch Dev --title "<title>" --openaz repos pr create --source-branch Dev --target-branch main --title "Release: Dev to main" --openaz repos pr show --id <pr-number> --query "{status:status, merge:mergeStatus, reviewers:reviewers[].{name:displayName, vote:vote}}"az repos pr reviewer add --id <pr-number> --reviewers <mail-or-group>az repos pr update --id <pr-number> --status abandoned # abandon PRaz repos pr update --id <pr-number> --status active # reactivateaz repos policy list --branch Dev --repository-id <repo-id> -o table # which branch policies applyaz pipelines list -o tableaz pipelines run --name <pipeline>az pipelines runs list --top 5 -o table#Pipeline Template: Terraform Plan/Apply
azure-pipelines.yml – plan on every branch, apply only on main with approval via an environment.
trigger: branches: include: [ main ] pool: vmImage: ubuntu-latest variables: serviceConnection: '<service-connection>' workDir: '$(System.DefaultWorkingDirectory)/terraform' stages:- stage: Plan jobs: - job: plan steps: - task: AzureCLI@2 displayName: terraform plan inputs: azureSubscription: $(serviceConnection) scriptType: bash scriptLocation: inlineScript addSpnToEnvironment: true workingDirectory: $(workDir) inlineScript: | export ARM_CLIENT_ID=$servicePrincipalId export ARM_TENANT_ID=$tenantId export ARM_OIDC_TOKEN=$idToken export ARM_USE_OIDC=true export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) terraform init -input=false terraform fmt -check -recursive terraform validate terraform plan -input=false -out tfplan - publish: $(workDir)/tfplan artifact: tfplan - stage: Apply dependsOn: Plan condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) jobs: - deployment: apply environment: prod # configure approval in the environment strategy: runOnce: deploy: steps: - checkout: self - download: current artifact: tfplan - task: AzureCLI@2 displayName: terraform apply inputs: azureSubscription: $(serviceConnection) scriptType: bash scriptLocation: inlineScript addSpnToEnvironment: true workingDirectory: $(workDir) inlineScript: | export ARM_CLIENT_ID=$servicePrincipalId export ARM_TENANT_ID=$tenantId export ARM_OIDC_TOKEN=$idToken export ARM_USE_OIDC=true export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) terraform init -input=false terraform apply -input=false $(Pipeline.Workspace)/tfplan/tfplanNote: the OIDC token from addSpnToEnvironment is short-lived – for long runs the azurerm provider (≥ 4.x) can refresh the token itself (ARM_OIDC_AZURE_SERVICE_CONNECTION_ID + SYSTEM_ACCESSTOKEN, see the azurerm docs).
#Azure DevOps Pipelines
#In the Portal
Where to click to run, watch and approve pipelines.
| Task | Where |
|---|---|
| Start a run | Pipelines → pipeline → Run pipeline → choose branch, set parameters |
| Run for another branch | in Run pipeline switch Branch/tag at the top |
| Log of a step | open run → job → click the step; top right View raw log |
| Retry a failed job | open run → Rerun failed jobs |
| Grant an approval | run shows "Waiting" → Review → Approve |
| Set up approvals | Pipelines → Environments → environment → ⋯ → Approvals and checks |
| Variables/secrets | Pipelines → Library → variable group (optionally linked to Key Vault) |
| Build check in a PR | Repos → Branches → Dev → Branch policies → Build validation |
| Pipeline permission on a service connection | Project settings → Service connections → connection → Security |
#CLI
List, start and inspect pipeline runs from the CLI.
az devops configure --defaults organization=https://dev.azure.com/<org> project=<project>az pipelines list -o tableaz pipelines run --name <pipeline> --branch Devaz pipelines run --name <pipeline> --branch main --parameters environment=prod # runtime parameteraz pipelines run --name <pipeline> --variables ImageTag=1.2.3az pipelines runs list --pipeline-ids <id> --top 10 ` --query "[].{id:id, branch:sourceBranch, status:status, result:result, start:startTime}" -o tableaz pipelines runs show --id <run-id> --open # open in the browseraz pipelines runs list --branch Dev --result failed --top 5 -o tableaz pipelines variable-group list -o tableaz pipelines variable-group variable list --group-id <id> -o table#Investigate a Failed Run
1. Open run → red job → red step → read the last red line, not the first. 2. Common causes:
| Message | Cause | Fix |
|---|---|---|
AuthorizationFailed | service connection has no role on the target | assign a role to the service connection's identity |
No hosted parallelism has been purchased or granted | no agents free/enabled | request parallel jobs or use a self-hosted agent |
The pipeline is not valid … Could not find … | wrong path/template | check the path relative to the repo root |
Pipeline does not have permissions to use the referenced service connection | permission missing | open run → Permit, or service connection → Security |
| Run stuck on "Waiting" | approval in the environment | reviewer grants approval |
The template … failed validation | Bicep/ARM error | run az deployment group validate locally |
3. More output: set variable system.debug = true and run again.
#Template: Bicep what-if in the PR, Deploy after Approval
pipelines/bicep.yml – what-if on every PR, deployment to Dev/main only after approval in the environment.
trigger: branches: include: [ Dev, main ] paths: include: [ infra/* ] pr: branches: include: [ Dev, main ] pool: vmImage: ubuntu-latest variables: serviceConnection: '<service-connection>' rg: '<rg>' ${{ if eq(variables['Build.SourceBranchName'], 'main') }}: paramFile: 'infra/params/prod.bicepparam' envName: 'prod' ${{ else }}: paramFile: 'infra/params/dev.bicepparam' envName: 'dev' stages:- stage: Check jobs: - job: whatif steps: - task: AzureCLI@2 displayName: Bicep lint + what-if inputs: azureSubscription: $(serviceConnection) scriptType: bash scriptLocation: inlineScript inlineScript: | az bicep lint -f infra/main.bicep az deployment group what-if -g $(rg) -f infra/main.bicep -p $(paramFile) \ --exclude-change-types NoChange Ignore - stage: Deploy dependsOn: Check condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) jobs: - deployment: deploy environment: ${{ variables.envName }} # configure approval in the environment strategy: runOnce: deploy: steps: - checkout: self - task: AzureCLI@2 displayName: Bicep deploy inputs: azureSubscription: $(serviceConnection) scriptType: bash scriptLocation: inlineScript inlineScript: | az deployment group create -g $(rg) -f infra/main.bicep -p $(paramFile) \ -n "pipeline-$(Build.BuildId)"Note: in a PR the check uses the Dev parameters (for PR runs the source branch is refs/pull/…). For PRs to main, attach a separate pipeline with prod.bicepparam as build validation on main if needed.
For the what-if to run in the PR: branch policy of Dev → Build validation → add this pipeline.
#Useful YAML Snippets
Parameters, variable groups and other snippets to copy.
# Runtime parameter (choice at start)parameters:- name: environment type: string default: dev values: [ dev, test, prod ] # Include a variable group (e.g. with Key Vault secrets)variables:- group: <variable-group> # Step only on main- script: echo "main only" condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') # Step even if a previous one failed (e.g. save logs)- script: echo "always" condition: always() # Template from another reporesources: repositories: - repository: templates type: git name: <project>/<template-repo> ref: refs/heads/mainsteps:- template: steps/bicep-deploy.yml@templates#Terraform
Top 5:terraform init·terraform plan -out tfplan·terraform apply tfplan·terraform state list·terraform output
#Basic Workflow
init, plan, apply and the flags used most often.
terraform versionterraform init # initialize providers + backendterraform init -upgrade # get newer provider versionsterraform init -reconfigure # re-read the backendterraform init -migrate-state # move state to a new backendterraform fmt -recursive # formatterraform fmt -check -recursive # check only (pipeline)terraform validate # check syntax/typesterraform test # tests in tests/*.tftest.hcl terraform plan # show changesterraform plan -out tfplan # save the planterraform apply tfplan # apply exactly this planterraform apply # plan, ask, applyterraform apply -auto-approve # without confirmation (pipeline)terraform plan -destroy # show what a destroy would deleteterraform destroy terraform plan -var-file=<file>.tfvars -var="name=value"terraform plan -target='<address>' # one resource only (exception!)terraform apply -replace='<address>' # rebuild deliberatelyterraform plan -refresh-only # show drift onlyterraform apply -refresh-only # accept drift into the stateterraform plan -detailed-exitcode # exit 0 = no change, 2 = changes, 1 = errorterraform plan -parallelism=5 # fewer parallel API calls (throttling)terraform show -json tfplan > plan.json # machine-readable planterraform -chdir=<folder> plan # run in another folderIn PowerShell always put addresses in single quotes: 'module.hub.azurerm_subnet.this["fw"]'.
#Reading a Plan
What +, ~, - and -/+ in a plan mean.
| Symbol | Meaning |
|---|---|
+ | create |
~ | update in place |
-/+ | replace = destroy and recreate – check carefully! |
+/- | replace with create_before_destroy |
- | destroy |
<= | read data source |
# forces replacement | this attribute triggers the rebuild |
#Outputs and State
Read outputs, list and move state entries.
terraform outputterraform output -raw <name>terraform output -json > outputs.jsonterraform show # current state, readableterraform state listterraform state list | Select-String <part> # macOS / Linux: ... | grep -i <part>terraform state show '<address>'terraform state mv '<old>' '<new>' # rename without rebuild (better: moved block)terraform state rm '<address>' # detach from Terraform, resource staysterraform state pull > state-backup.json # back up the stateterraform import '<address>' <azure-id> # take over an existing resourceterraform plan -generate-config-out=generated.tf # generate code for import blocks (TF >= 1.5)terraform force-unlock <lock-id>terraform console # test expressionsterraform graph | dot -Tsvg > graph.svgterraform providersterraform providers lock -platform=windows_amd64 -platform=linux_amd64 # lock file for all platformsterraform workspace list | new <name> | select <name> | delete <name>#Code Snippets
Import, moved and other blocks you copy again and again.
# Import in code (TF >= 1.5)import { to = azurerm_resource_group.rg id = "/subscriptions/<sub>/resourceGroups/<rg>"} # Rename without rebuildmoved { from = azurerm_resource_group.old to = azurerm_resource_group.new} # Remove from Terraform, resource stays (TF >= 1.7)removed { from = azurerm_resource_group.old lifecycle { destroy = false }} # Ignore fields after creation / protectionlifecycle { ignore_changes = [tags["DateCreatedUTC"]] prevent_destroy = true create_before_destroy = true} # Several instances from a mapresource "azurerm_subnet" "this" { for_each = var.subnets # { fw = "10.0.0.0/26", mgmt = "10.0.0.64/27" } name = each.key address_prefixes = [each.value] resource_group_name = azurerm_resource_group.rg.name virtual_network_name = azurerm_virtual_network.vnet.name} # Optional resourceresource "azurerm_public_ip" "pip" { count = var.create_pip ? 1 : 0 # ...} # Variable with validationvariable "environment" { type = string validation { condition = contains(["dev", "test", "prod"], var.environment) error_message = "environment must be dev, test or prod." }} # Secret variablevariable "admin_password" { type = string sensitive = true} # Provider with pinned versionterraform { required_version = ">= 1.6" required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 4.0" } }}provider "azurerm" { features {} subscription_id = var.subscription_id # required from azurerm 4.x (or ARM_SUBSCRIPTION_ID)}#Useful Functions
Test in terraform console:
cidrsubnets("10.0.0.0/24", 2, 2, 2, 2) # ["10.0.0.0/26","10.0.0.64/26",...]cidrsubnet("10.0.0.0/16", 8, 3) # "10.0.3.0/24"cidrhost("10.0.1.0/24", 4) # "10.0.1.4" (first usable IP in Azure)merge(local.tags, { role = "fw" })lookup(var.map, "key", "default")try(var.obj.value, null)coalesce(var.name, "${var.prefix}-vm")format("%s-%02d", "vm", 1) # "vm-01"jsonencode({ a = 1 }) ; yamldecode(file("x.yaml"))templatefile("${path.module}/init.tpl", { hostname = "fgt-a" })toset(["a","b"]) ; flatten([[1],[2,3]]) ; distinct(list)[for k, v in var.subnets : k if v != null]{ for s in var.list : s.name => s }#Remote State in Azure
Backend configuration for a storage account and lock handling.
terraform { backend "azurerm" { resource_group_name = "<rg-tfstate>" storage_account_name = "<storageaccount>" container_name = "tfstate" key = "<project>.tfstate" use_azuread_auth = true }}Create the storage:
az group create -n <rg-tfstate> -l <region>az storage account create -g <rg-tfstate> -n <storageaccount> --sku Standard_ZRS --min-tls-version TLS1_2 --allow-blob-public-access falseaz storage account blob-service-properties update -g <rg-tfstate> --account-name <storageaccount> --enable-versioning trueaz storage container create --account-name <storageaccount> -n tfstate --auth-mode loginaz role assignment create --assignee <user-or-sp> --role "Storage Blob Data Contributor" --scope <storage-id>Lock stuck? Blob lease: az storage blob lease break --account-name <sa> -c tfstate -b <project>.tfstate --auth-mode login
#Environment Variables and Sign-in
ARM variables, service principal, OIDC and debug logging.
$env:ARM_SUBSCRIPTION_ID = "<sub>"$env:ARM_TENANT_ID = "<tenant>"$env:ARM_CLIENT_ID = "<app-id>" ; $env:ARM_CLIENT_SECRET = "<secret>" # service principal$env:ARM_USE_OIDC = "true" # pipeline with workload identity$env:TF_VAR_admin_password = "<secret>" # set a variable without a file$env:TF_LOG = "DEBUG" ; $env:TF_LOG_PATH = "tf.log"Remove-Item Env:TF_VAR_admin_passwordGet-ChildItem Env:ARM_* , Env:TF_* # what is set?export ARM_SUBSCRIPTION_ID="<sub>"export ARM_TENANT_ID="<tenant>"export ARM_CLIENT_ID="<app-id>" ARM_CLIENT_SECRET="<secret>" # service principalexport ARM_USE_OIDC="true" # pipeline with workload identityexport TF_VAR_admin_password="<secret>" # set a variable without a fileexport TF_LOG="DEBUG" TF_LOG_PATH="tf.log"unset TF_VAR_admin_passwordenv | grep -E '^(ARM_|TF_)' # what is set? (secrets are shown – do not share the output)#Quality and Security
Linting and security scans before apply.
tflint --init ; tflint --recursive # linter (provider rules)checkov -d . # security checkstrivy config . # alternative to checkovterraform-docs markdown table . > README.md # generate module docs#.gitignore
What must never end up in the repo.
.terraform/*.tfstate*.tfstate.*tfplan*.tfplancrash.logcrash.*.logterraform.tfvars*.auto.tfvarsoverride.tfoverride.tf.jsonCommit .terraform.lock.hcl (pinned provider versions).
#Bicep / ARM
az bicep install ; az bicep upgrade ; az bicep versionaz bicep build -f main.bicep # -> ARM JSONaz bicep build-params -f main.bicepparam # parameter file -> JSONaz bicep decompile -f template.json # ARM -> Bicepaz bicep lint -f main.bicep # linter # Check (changes nothing)az deployment group validate -g <rg> -f main.bicep -p main.bicepparamaz deployment group what-if -g <rg> -f main.bicep -p main.bicepparamaz deployment sub what-if -l <region> -f main.bicep -p main.bicepparam # Deployaz deployment group create -g <rg> -f main.bicep -p main.bicepparamaz deployment group create -g <rg> -f main.bicep -p main.bicepparam -p adminPassword=<value> # override a single parameteraz deployment sub create -l <region> -f main.bicep -p main.bicepparamaz deployment group create -g <rg> -f main.bicep --mode Complete # deletes everything not in the template (careful!) # History and errorsaz deployment group list -g <rg> --query "[].{name:name, state:properties.provisioningState, time:properties.timestamp}" -o tableaz deployment operation group list -g <rg> -n <deployment> --query "[?properties.provisioningState=='Failed'].properties.statusMessage"az deployment group cancel -g <rg> -n <deployment> # Exportaz group export -n <rg> > export.json#Building Blocks
Parameters, variables, resources, modules and outputs in short form.
param location string = resourceGroup().location@secure()param adminPassword string@allowed(['dev', 'test', 'prod'])param environment string var tags = { environment: environment, owner: 'network' } resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { name: 'vnet-hub' // reference an existing resource} module spoke 'modules/spoke.bicep' = { name: 'spoke-deploy' params: { location: location, tags: tags }} resource nsgs 'Microsoft.Network/networkSecurityGroups@2024-05-01' = [for n in ['a', 'b']: { name: 'nsg-${n}' location: location}] output vnetId string = vnet.idCareful with subnets: subnets defined inline in the VNet → a redeployment removes subnets that are not in the list, and NSG/route table associations that are not in the template. Always run what-if first.
#Bicep in a Team Repo
Top 5:az bicep build·az bicep lint·az deployment group what-if·az deployment group create -p <file>.bicepparam·az deployment operation group list
#Typical Folder Structure
Where main, modules and parameter files live.
infra/├── main.bicep # entry point, calls modules├── modules/│ ├── network.bicep│ └── appgateway.bicep├── params/│ ├── dev.bicepparam # values per environment│ └── prod.bicepparam└── bicepconfig.json # linter rulesRule: logic in .bicep, values in .bicepparam. A PR that only changes values only touches params/.
#Parameter File per Environment (.bicepparam)
One parameter file per environment and how secrets come in.
// params/dev.bicepparamusing '../main.bicep' param environment = 'dev'param location = 'westeurope'param vnetAddressSpace = '10.10.0.0/22'param tags = { Environment: 'dev' Owner: '<team>'}param adminPassword = readEnvironmentVariable('ADMIN_PASSWORD') // secret not in the file$env:ADMIN_PASSWORD = "<secret>" # this session onlyaz bicep build-params -f params/dev.bicepparam # checks that all required parameters are setexport ADMIN_PASSWORD="<secret>" # this session onlyaz bicep build-params -f params/dev.bicepparam # checks that all required parameters are setCompare environments:
git diff --no-index params/dev.bicepparam params/prod.bicepparamcode --diff params/dev.bicepparam params/prod.bicepparam # side by side in VS Code#Check Before the Pull Request
Format, lint and build before you open the PR.
az bicep format -f main.bicep # consistent formattingaz bicep lint -f main.bicep # linter warningsaz bicep build -f main.bicep --stdout > $null # check syntax/types, write nothingaz deployment group validate -g <rg> -f main.bicep -p params/dev.bicepparamaz deployment group what-if -g <rg> -f main.bicep -p params/dev.bicepparam ` --exclude-change-types NoChange Ignore # show real changes onlyReading what-if:
| Symbol | Meaning |
|---|---|
+ Create | will be created |
~ Modify | will be changed – details below |
- Delete | will be deleted (only in Complete mode or with stacks) |
= NoChange | stays the same |
* Ignore | not in the template – left untouched |
! Deploy | cannot be predicted, will be rewritten |
Pasting the what-if output into the PR helps the reviewer a lot.
#Deploy and Find Errors
Deploy with a unique name and find the failed resource.
$name = "deploy-$(Get-Date -Format yyyyMMdd-HHmm)"az deployment group create -g <rg> -n $name -f main.bicep -p params/dev.bicepparamaz deployment group create -g <rg> -n $name -f main.bicep -p params/dev.bicepparam -p location=northeurope # override one value (newer CLI) # What went wrong?az deployment group show -g <rg> -n $name --query "properties.error" -o jsonaz deployment operation group list -g <rg> -n $name ` --query "[?properties.provisioningState=='Failed'].{res:properties.targetResource.resourceName, type:properties.targetResource.resourceType, error:properties.statusMessage.error.message}" -o table # Historyaz deployment group list -g <rg> --query "sort_by([].{name:name, status:properties.provisioningState, time:properties.timestamp}, &time)" -o tableTip: macOS / Linux (Bash): name="deploy-$(date +%Y%m%d-%H%M)", variables without $ on assignment, line continuation \. Subscription or management group level: az deployment sub create -l <region> … or az deployment mg create -m <mg-id> -l <region> ….
#Modules from the Registry (AVM)
Azure Verified Modules (AVM) are reviewed Microsoft modules from the public registry:
module vnet 'br/public:avm/res/network/virtual-network:<version>' = { name: 'vnet' params: { name: 'vnet-hub-dev-weu-01' addressPrefixes: [ '10.10.0.0/22' ] }}az bicep restore -f main.bicep # download modules (cache)Current versions: https://aka.ms/avm – always pin the version.
#Deployment Stacks
A stack remembers which resources it created and can clean up removed resources or protect them from changes.
az stack group create -n <stack> -g <rg> -f main.bicep -p params/dev.bicepparam ` --action-on-unmanage detachAll --deny-settings-mode noneaz stack group list -g <rg> -o tableaz stack group show -n <stack> -g <rg> --query "resources[].id" -o tsvaz stack group delete -n <stack> -g <rg> --action-on-unmanage detachAll # stack gone, resources stay--action-on-unmanage deleteAll deletes resources removed from the template – careful in prod.
#bicepconfig.json
Linter rules and settings for the whole team.
{ "analyzers": { "core": { "rules": { "no-hardcoded-location": { "level": "warning" }, "no-unused-params": { "level": "warning" }, "secure-parameter-default": { "level": "error" }, "use-recent-api-versions": { "level": "warning" } } } }}#VM, Linux and Windows Workflows
Virtual machines first, then what to do on the machine itself. Open a workflow, adjust the variables at the top, run block by block.
#VM: run a command, reset password, console
Fix a VM without signing in: run a command, reset the password or SSH key, read the boot log.
$rg = "<rg>" ; $vm = "<vm>"# run a command without signing inaz vm run-command invoke -g $rg -n $vm --command-id RunShellScript --scripts "hostname; ip a; ip r" --query "value[0].message" -o tsvaz vm run-command invoke -g $rg -n $vm --command-id RunPowerShellScript --scripts "ipconfig /all" --query "value[0].message" -o tsv# reset password / SSH keyaz vm user update -g $rg -n $vm -u <user> -p <new-password>az vm user update -g $rg -n $vm -u <user> --ssh-key-value "$(Get-Content $HOME\.ssh\id_rsa.pub)"# when nothing else works: boot log and serial consoleaz vm boot-diagnostics enable -g $rg -n $vmaz vm boot-diagnostics get-boot-log -g $rg -n $vmaz extension add -n serial-consoleaz serial-console connect -g $rg -n $vmTip: macOS / Linux (Bash): variables as rg="<rg>"; vm="<vm>", and the key as --ssh-key-value "$(cat ~/.ssh/id_rsa.pub)".
#Stop / start all VMs in a resource group
Deallocate or start every VM of a resource group at once – no compute costs while stopped.
$rg = "<rg>"az vm deallocate --ids (az vm list -g $rg --query "[].id" -o tsv) --no-wait # no compute costsaz vm start --ids (az vm list -g $rg --query "[].id" -o tsv) --no-waitaz vm list -d -g $rg --query "[].{name:name, state:powerState}" -o tableBash: az vm deallocate --ids $(az vm list -g $rg --query "[].id" -o tsv)
#Find a Marketplace image and accept the terms
Look up publisher, offer and plan of a Marketplace image and accept its terms before the first deployment.
Example FortiGate:
$loc = "<region>"az vm image list-offers -l $loc -p fortinet --query "[?contains(name,'fortigate')].name" -o tsvaz vm image list-skus -l $loc -p fortinet -f fortinet_fortigate-vm -o tableaz vm image list -p fortinet -f fortinet_fortigate-vm -s <sku> -l $loc --all --query "[].version" -o tsv$urn = "fortinet:fortinet_fortigate-vm:<sku>:latest"az vm image terms show --urn $urn --query acceptedaz vm image terms accept --urn $urnFor Terraform: set plan { publisher, product, name } in the VM resource.
#Before deploying: check zones and quota
Check whether a VM size is available in the region and zone, and whether the quota is enough.
$loc = "<region>" ; $sku = "<vm-size>"az vm list-skus -l $loc --size $sku --query "[].{sku:name, zones:locationInfo[0].zones, restriction:restrictions[0].reasonCode}" -o tableaz vm list-usage -l $loc --query "[?contains(name.value,'Total') || contains(localName,'<family>')].{name:localName, used:currentValue, limit:limit}" -o tableEmpty zones or a restriction = SKU not available in this subscription/zone.
#Linux: test web server showing the hostname
Tiny web server that answers with its hostname – ideal to test load balancers.
sudo apt update && sudo apt install -y nginxecho "ok $(hostname) $(hostname -I)" | sudo tee /var/www/html/index.htmlsudo systemctl enable --now nginxcurl -s localhostsudo ufw allow 80/tcp 2>/dev/null || trueIdeal for testing load balancers, App Gateways or VIPs – the response shows which server answered.
#Linux: service does not start
Status, logs, restart and listening port of a failing service.
d=<service>sudo systemctl status $d --no-pagersudo journalctl -u $d -n 50 --no-pagersudo systemctl restart $d && sudo systemctl is-active $dsudo ss -tlnp | grep -i $d # is it listening?#Linux: disk full
Find the biggest directories and files, clean up logs and caches.
df -hsudo du -xh / --max-depth=1 2>/dev/null | sort -h | tail -10sudo du -xh /var/log --max-depth=1 | sort -h | tailsudo journalctl --vacuum-size=200Msudo apt clean#Check a connection step by step
DNS, route, port, TLS/HTTP – the four checks in order.
$target = "<host-or-ip>" ; $port = 443Resolve-DnsName $target # 1. DNSFind-NetRoute -RemoteIPAddress (Resolve-DnsName $target -Type A)[0].IPAddress | Select -Last 1 # 2. routeTest-NetConnection $target -Port $port # 3. portcurl.exe -v -m 10 "https://$target" # 4. TLS/HTTPtarget="<host-or-ip>"; port=443ip=$(dig +short "$target" | tail -1) # 1. DNSip route get "$ip" # 2. route (macOS: route -n get "$ip")nc -zv "$target" "$port" # 3. portcurl -v -m 10 "https://$target" # 4. TLS/HTTP#Linux
Top 5:ip a·ip r get <target>·ss -tlnp·curl -v·sudo tcpdump -ni eth0 host <ip>
#Network
Addresses, routes, sockets and connections.
ip a # IP addressesip -br a # briefip r # routesip r get <target> # which path to a targetip neigh # ARP tabless -tlnp # listening TCP ports with processss -tunp # active connectionsss -s # statisticscurl -s ifconfig.me; echo # own public (egress) IPcurl -sI https://<target> | head -1 # HTTP statuscurl -v -m 10 https://<target> -o /dev/null # error reason (TLS, timeout)curl -k --resolve <host>:443:<ip> https://<host>/ # test a host against a specific IPcurl -o /dev/null -s -w "%{http_code} %{time_total}s\n" https://<target> # status + timenc -zv <ip> <port> # port reachable?ping -c 4 <ip> # ttl drops by 1 per routerping -M do -s 1372 <ip> # test MTU (DF bit)tracepath <ip> ; traceroute <ip> ; mtr <ip>sudo tcpdump -ni eth0 host <ip> and port 443sudo tcpdump -ni any icmpsudo tcpdump -ni eth0 -w capture.pcap # for Wiresharksysctl net.ipv4.ip_forward # IP forwarding on?sudo netplan apply # Ubuntu: reload network#DNS
Resolve names and check which DNS server is used.
nslookup <name> ; dig +short <name> ; dig @<dns> <name>dig <name> +traceresolvectl status # which DNS server per interfaceresolvectl flush-cachescat /etc/resolv.confgetent hosts <name> # how the system resolves (incl. /etc/hosts)#Firewall and Services
ufw, iptables and systemd services.
sudo ufw status verbose ; sudo ufw allow 443/tcp ; sudo ufw delete allow 443/tcpsudo iptables -L -n -vsudo iptables -t nat -L -n -vsudo nft list rulesetsudo systemctl status <service> ; sudo systemctl restart <service> ; sudo systemctl enable --now <service>systemctl list-units --type=service --state=failedsudo journalctl -u <service> -fsudo journalctl -b -p errsudo journalctl --since "1 hour ago"#System
Version, disk, memory, processes.
hostnamectl ; uname -a ; cat /etc/os-releasedf -h ; du -sh * ; free -h ; top ; htoplsblk ; sudo blkiduptime ; who ; last -n 10sudo apt update && sudo apt upgrade -ysudo apt install -y curl dnsutils traceroute mtr-tools tcpdump nginxtail -f /var/log/syslogsudo cat /var/log/cloud-init-output.log # Azure: custom data / cloud-initsudo waagent --version # Azure Linux agent#Files and Text
Search, filter and edit files and text.
grep -rin "<text>" <folder> # search recursivelyfind . -name "*.tf" -mtime -1 # changed in the last 24 hsed -i 's/old/new/g' <file>tar czf package.tgz <folder> ; tar xzf package.tgzsha256sum <file>chmod 600 <file> ; chown <user>:<group> <file>#Mini Tests
Small loops for continuous ping, port and web checks.
while true; do curl -s -m 2 ifconfig.me; echo " $(date +%T)"; sleep 1; done # continuous failover testwhile true; do ping -c1 -W1 <ip> >/dev/null && echo "$(date +%T) OK" || echo "$(date +%T) FAIL"; sleep 1; donewhile true; do nc -zvw1 <ip> 443 2>&1 | tail -1; sleep 1; done # continuous port testsudo python3 -m http.server 80 # quick test web serverecho "ok $(hostname)" | sudo tee /var/www/html/index.html # nginx test page with hostname#Windows / PowerShell
Top 5:Test-NetConnection <ip> -Port 443·Resolve-DnsName·ipconfig /all·curl.exe -v·Get-NetTCPConnection -State Listen
#Network
IP configuration, adapters, routes and connections.
ipconfig /allGet-NetIPConfigurationGet-NetIPAddress -AddressFamily IPv4 | Format-Table InterfaceAlias, IPAddress, PrefixLengthGet-NetRoute -AddressFamily IPv4 | Sort DestinationPrefix | Format-Tableroute printFind-NetRoute -RemoteIPAddress <target> # which path to a targetTest-NetConnection <ip> -Port 443 # port reachable?Test-NetConnection <ip> -Port 443 -InformationLevel DetailedTest-NetConnection <ip> -TraceRoutetracert <ip> ; pathping <ip>ping -f -l 1372 <ip> # test MTU (DF bit)Get-NetTCPConnection -State Listen | Sort LocalPortnetstat -ano | findstr :443 # port -> process IDGet-Process -Id <pid>arp -acurl.exe -v https://<target> # real curl (not the PowerShell alias)curl.exe -k --resolve <host>:443:<ip> https://<host>/(Invoke-RestMethod https://api.ipify.org) # own public IPInvoke-WebRequest https://<target> -UseBasicParsing | Select StatusCode#DNS
Resolve names, flush the cache, check servers.
Resolve-DnsName <name> [-Server <dns>]Resolve-DnsName <name> -Type A -DnsOnly # without hosts file/cachenslookup <name> <dns>ipconfig /flushdns ; Clear-DnsClientCacheGet-DnsClientServerAddress -AddressFamily IPv4Get-DnsClientCache | Where Entry -like "*<part>*"notepad C:\Windows\System32\drivers\etc\hosts # as administrator#Windows Firewall
Show profiles and create or remove rules.
Get-NetFirewallProfile | Select Name, EnabledNew-NetFirewallRule -DisplayName "Allow 443" -Direction Inbound -Protocol TCP -LocalPort 443 -Action AllowNew-NetFirewallRule -DisplayName "Allow ICMP" -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action AllowGet-NetFirewallRule -DisplayName "*<part>*" | Format-Table DisplayName, Enabled, Direction, ActionRemove-NetFirewallRule -DisplayName "Allow 443"#Files and Scripts
Unblock downloads, execution policy, search and edit files.
Set-ExecutionPolicy -Scope Process Bypass # allow scripts for this session onlyGet-ChildItem -Recurse | Unblock-File # remove the download blockGet-ChildItem -Recurse *.ps1 | Unblock-File # scripts onlyExpand-Archive .\package.zip -DestinationPath .\packageCompress-Archive .\folder -DestinationPath .\package.zipCopy-Item <source> <target> ; Move-Item ; Remove-Item -RecurseGet-FileHash <file> # checksum (SHA256)Get-Content <file> -Tail 50 -Wait # like tail -fSelect-String -Path *.tf -Pattern "<text>" # like grepGet-ChildItem -Recurse -Filter *.tf | Select-String "<text>" # recursiveGet-ChildItem -Recurse | Where LastWriteTime -gt (Get-Date).AddDays(-1)$env:PATH -split ';'[IO.File]::WriteAllText("<path>", $text, (New-Object Text.UTF8Encoding($false))) # UTF-8 without BOM$PSVersionTable.PSVersionwinget install Microsoft.AzureCLI ; winget install Hashicorp.Terraform ; winget install Git.Git#System and Events
System info, services and event logs.
hostname ; whoami ; whoami /groupsGet-ComputerInfo | Select OsName, OsVersion, CsNameGet-Service | Where Status -eq RunningRestart-Service <name>Get-WinEvent -LogName System -MaxEvents 50 | Format-Table TimeCreated, Id, LevelDisplayName, Message -WrapGet-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=(Get-Date).AddHours(-24)} # errors in 24 hGet-ChildItem Cert:\LocalMachine\My | Format-Table Subject, NotAfter, ThumbprintRestart-Computer -Force#Continuous Tests
Loops for continuous web and port checks.
while ($true) { "$((Get-Date).ToString('HH:mm:ss')) $(curl.exe -s -m 2 http://<ip>)"; Start-Sleep 1 }while ($true) { $r = Test-NetConnection <ip> -Port 443 -WarningAction SilentlyContinue; "$((Get-Date).ToString('HH:mm:ss')) $($r.TcpTestSucceeded)"; Start-Sleep 1 }#Log Analytics (KQL)
#Skeleton
The basic shape of a query: table, time, filter, columns.
<Table>| where TimeGenerated > ago(1h)| where <field> == "<value>" // exact (case-sensitive)| where <field> =~ "<value>" // exact, case-insensitive| where <field> has "<word>" // whole word (fast)| where <field> contains "<part>" // substring (slower)| project TimeGenerated, <fields>| order by TimeGenerated desc| take 100 // Count / group / chart| summarize Count = count() by <field>, bin(TimeGenerated, 5m)| render timechart // Which tables have data?search * | where TimeGenerated > ago(1h) | summarize count() by $table // Columns of a table<Table> | getschema#Application Gateway and WAF
Blocked requests, rules and access logs.
// WAF – blocked/detected requests (AzureDiagnostics mode)AzureDiagnostics| where Category == "ApplicationGatewayFirewallLog" and action_s in ("Blocked","Matched","Detected")| project TimeGenerated, hostname_s, requestUri_s, clientIp_s, ruleGroup_s, ruleId_s, Message, details_message_s| order by TimeGenerated desc // Most frequent WAF rules (false-positive candidates)AzureDiagnostics| where Category == "ApplicationGatewayFirewallLog"| summarize Count = count() by ruleId_s, ruleGroup_s, Message| order by Count desc // Access (AzureDiagnostics mode)AzureDiagnostics| where Category == "ApplicationGatewayAccessLog"| project TimeGenerated, host_s, requestUri_s, httpStatus_d, serverStatus_s, serverRouted_s, timeTaken_d // Requests with errors (resource-specific table)AGWAccessLogs| where TimeGenerated > ago(1h) and HttpStatus >= 400| project TimeGenerated, Host, RequestUri, HttpStatus, ServerStatus, ServerRouted, ClientIp| order by TimeGenerated desc // WAF (resource-specific table)AGWFirewallLogs| where TimeGenerated > ago(1h)| project TimeGenerated, Action, RuleId, Message, ClientIp, RequestUri, HostnameWhich variant applies is set in the diagnostic setting (Azure diagnostics vs. Resource specific).
#Azure Activity
Who changed or deleted what.
// Who changed what?AzureActivity| where TimeGenerated > ago(1d) and ActivityStatusValue == "Success"| project TimeGenerated, Caller, OperationNameValue, ResourceGroup, _ResourceId // DeletionsAzureActivity| where OperationNameValue endswith "/DELETE" and ActivityStatusValue == "Success"| project TimeGenerated, Caller, _ResourceId // Failed deployments / policy denialsAzureActivity| where ActivityStatusValue == "Failure"| project TimeGenerated, Caller, OperationNameValue, Properties#Network
Flow logs and denied traffic.
// NSG flow logs / Traffic Analytics (old)AzureNetworkAnalytics_CL| where SubType_s == "FlowLog" and FlowStatus_s == "D" // D = denied| project TimeGenerated, SrcIP_s, DestIP_s, DestPort_d, NSGRule_s // VNet flow logs / Traffic Analytics (new)NTANetAnalytics| where TimeGenerated > ago(1h)| take 50 // check fields with getschema // Azure Firewall – denied (resource-specific tables)AZFWNetworkRule| where TimeGenerated > ago(1h) and Action == "Deny"| project TimeGenerated, SourceIp, DestinationIp, DestinationPort, Protocol#FortiGate (Syslog/CEF)
Firewall logs in Log Analytics.
CommonSecurityLog| where DeviceVendor == "Fortinet"| where TimeGenerated > ago(1h)| project TimeGenerated, SourceIP, DestinationIP, DestinationPort, DeviceAction, Activity| order by TimeGenerated desc // Denied connections by destinationCommonSecurityLog| where DeviceVendor == "Fortinet" and DeviceAction in ("deny","block")| summarize Count = count() by DestinationIP, DestinationPort| order by Count desc#VMs
Heartbeat, syslog errors and Windows events.
// Last heartbeat per VMHeartbeat | summarize last = max(TimeGenerated) by Computer | order by last asc // Linux syslog errorsSyslog | where SeverityLevel in ("err","crit") | project TimeGenerated, Computer, ProcessName, SyslogMessage // Windows eventsEvent | where EventLevelName == "Error" | project TimeGenerated, Computer, Source, RenderedDescription#Microsoft Sentinel (KQL)
Ready-to-use hunting queries for the Sentinel workspace – paste into Sentinel → Logs or use as an analytics rule (scheduled query).
#Setup in Short
Enable Sentinel and connect data sources.
| Step | Where |
|---|---|
| Enable Sentinel on the Log Analytics workspace | Sentinel → + Create → choose workspace |
| Data connectors | Content hub → install solution (e.g. Microsoft Entra ID, Azure Activity, Microsoft Defender XDR, Fortinet FortiGate via CEF/AMA) → connector page |
| Analytics rules | Analytics → Rule templates – enable templates or create Scheduled query rule with the queries below |
| Automation | Automation rules (assign, close, tag) and playbooks (Logic Apps) |
| Watchlists | Watchlists – e.g. admin accounts, VIPs, known IPs; in KQL _GetWatchlist('<name>') |
Costs: ingestion per GB – check Usage, use Basic/Auxiliary logs for bulk data (e.g. firewall logs) and set retention per table.
#Sign-ins (Entra ID)
Failed sign-ins, password spray, risky locations.
// Failed sign-ins per user and IP, last 24 h (possible password spray / brute force)SigninLogs| where TimeGenerated > ago(24h) and ResultType != "0"| summarize Failures = count(), Apps = make_set(AppDisplayName, 5) by UserPrincipalName, IPAddress, Location = tostring(LocationDetails.countryOrRegion)| where Failures > 10| order by Failures desc // Password spray: one IP, many different usersSigninLogs| where TimeGenerated > ago(1h) and ResultType in ("50126", "50053")| summarize Users = dcount(UserPrincipalName), Attempts = count() by IPAddress| where Users > 10| order by Users desc // Successful sign-in from a new country (compared to the last 14 days)let known = SigninLogs | where TimeGenerated between (ago(14d) .. ago(1d)) and ResultType == "0" | summarize by UserPrincipalName, Country = tostring(LocationDetails.countryOrRegion);SigninLogs| where TimeGenerated > ago(1d) and ResultType == "0"| extend Country = tostring(LocationDetails.countryOrRegion)| join kind=leftanti known on UserPrincipalName, Country| project TimeGenerated, UserPrincipalName, Country, IPAddress, AppDisplayName // Sign-ins without MFA for admin accounts (watchlist "Admins" with column UPN)let admins = _GetWatchlist('Admins') | project UPN = tostring(SearchKey);SigninLogs| where TimeGenerated > ago(7d) and ResultType == "0" and AuthenticationRequirement == "singleFactorAuthentication"| where UserPrincipalName in~ (admins)| project TimeGenerated, UserPrincipalName, AppDisplayName, IPAddressCommon error codes: 50126 wrong password · 50053 account locked · 50074/50076 MFA required · 53003 blocked by conditional access.
#Privileged Changes (Entra ID and Azure)
Role assignments and admin changes.
// Role assignments in Entra ID (e.g. Global Administrator)AuditLogs| where TimeGenerated > ago(7d) and OperationName has "Add member to role"| extend Role = tostring(TargetResources[0].modifiedProperties[1].newValue), Target = tostring(TargetResources[0].userPrincipalName), Actor = tostring(InitiatedBy.user.userPrincipalName)| project TimeGenerated, Actor, Target, Role // New app secret / certificate on an app registrationAuditLogs| where TimeGenerated > ago(7d) and OperationName has_any ("Add service principal credentials", "Update application – Certificates and secrets management")| project TimeGenerated, OperationName, App = tostring(TargetResources[0].displayName), Actor = tostring(InitiatedBy.user.userPrincipalName) // Azure role assignments (Owner / User Access Administrator)AzureActivity| where TimeGenerated > ago(7d) and OperationNameValue =~ "Microsoft.Authorization/roleAssignments/write" and ActivityStatusValue == "Success"| project TimeGenerated, Caller, _ResourceId, Properties // Deleted resources, diagnostic settings or locksAzureActivity| where TimeGenerated > ago(1d) and ActivityStatusValue == "Success"| where OperationNameValue has_any ("/delete") and OperationNameValue has_any ("diagnosticSettings", "locks", "Microsoft.KeyVault", "Microsoft.Network/networkSecurityGroups")| project TimeGenerated, Caller, OperationNameValue, _ResourceId#Firewall and Network
Denied traffic and top sources from the firewall.
// FortiGate: top denied sources (CEF via AMA)CommonSecurityLog| where TimeGenerated > ago(1h) and DeviceVendor == "Fortinet" and DeviceAction in ("deny", "block", "dropped")| summarize Denied = count(), Ports = make_set(DestinationPort, 10) by SourceIP| order by Denied desc // Port scan: one source, many destination portsCommonSecurityLog| where TimeGenerated > ago(1h) and DeviceVendor == "Fortinet"| summarize Ports = dcount(DestinationPort) by SourceIP, DestinationIP| where Ports > 50 // Azure Firewall: threat intelligence hitsAZFWThreatIntel| where TimeGenerated > ago(24h)| project TimeGenerated, SourceIp, DestinationIp, DestinationPort, Action, ThreatDescription // One source talking to very many destinations (scan / beaconing)AZFWNetworkRule| where TimeGenerated > ago(1h) and Action == "Allow"| summarize Destinations = dcount(DestinationIp) by SourceIp| where Destinations > 100#Servers (Windows Security Events)
Failed logons and other security events.
// Failed logons (4625) per account and hostSecurityEvent| where TimeGenerated > ago(24h) and EventID == 4625| summarize Failures = count() by TargetAccount, Computer, IpAddress| where Failures > 20| order by Failures desc // Member added to a privileged local/domain group (4728, 4732, 4756)SecurityEvent| where TimeGenerated > ago(7d) and EventID in (4728, 4732, 4756)| project TimeGenerated, Computer, SubjectAccount, MemberName, TargetAccount // Security log cleared (1102)SecurityEvent| where TimeGenerated > ago(7d) and EventID == 1102| project TimeGenerated, Computer, SubjectAccount#Incidents and Rule Health
Open incidents and health of analytics rules.
// Open incidents by severitySecurityIncident| where TimeGenerated > ago(30d)| summarize arg_max(TimeGenerated, *) by IncidentNumber| where Status != "Closed"| summarize Count = count() by Severity, Title| order by Severity asc, Count desc // Analytics rules that failed to runSentinelHealth| where TimeGenerated > ago(1d) and SentinelResourceType == "Analytics Rule" and Status != "Success"| project TimeGenerated, SentinelResourceName, Status, Description // Ingestion per table (cost check)Usage| where TimeGenerated > ago(30d) and IsBillable| summarize GB = round(sum(Quantity) / 1024, 2) by DataType| order by GB descAnalytics rule from a query: Analytics → + Create → Scheduled query rule → paste query → Run query every 1 h, Lookup data from the last 1 h → entity mapping (Account, IP, Host) → incident settings. SentinelHealth needs Settings → Health monitoring enabled.
#Error → Fix
#Azure General
Error message, cause, fix – policy, quota and permission errors.
| Symptom | Cause | Fix |
|---|---|---|
RequestDisallowedByPolicy | Azure Policy | az policy assignment list, exemption or policy team |
ResourceGroupNotFound | resource group does not exist (CLI/PowerShell never create it) or wrong subscription selected | az group create -n <rg> -l <region>, check az account show |
AuthorizationFailed | role missing on this scope | az role assignment list --assignee <x> --all |
RoleAssignmentExists (409) | role assigned twice | use/import the existing assignment |
PrincipalNotFound | identity just created (replication) | wait briefly, pass --assignee-principal-type |
ResourceAvailabilityZonesCannotBeModified | two resources with the same name, different zone | name templates with a placeholder (unique per instance) |
| SKU "not available in zone X" | zone not available in this subscription | az vm list-skus … locationInfo |
OperationNotAllowed … quota | vCPU quota exhausted | az vm list-usage, request a quota increase |
MarketplacePurchaseEligibilityFailed / terms | Marketplace terms not accepted | az vm image terms accept --urn … |
ScopeLocked | lock on RG/resource | az lock list, remove the lock temporarily |
InUseSubnetCannotBeDeleted | subnet still in use (NIC, PE, delegation) | az network vnet subnet show … ipConfigurations |
Forbidden on Key Vault | data role or vault firewall | role Key Vault Secrets User, network rules |
module … blocked by software restriction policies | module lies in the user profile (Documents\…\Modules), blocked by company policy | as admin: Install-Module <module> -Scope AllUsers, rename the profile copy – Install Modules Without the User Profile |
#Network
Error message, cause, fix – peering, routes, NSG, DNS.
| Symptom | Cause | Fix |
|---|---|---|
Portal: DisablePeeringRoute is not supported | preview feature in the portal | create the route table via CLI/Terraform |
| VM has no internet | default outbound off, no route/policy | check NVA/NAT GW, UDR and firewall policy |
| Ping to the internet fails, curl works | LB outbound does not translate ICMP | test with curl |
| No traffic spoke ↔ spoke | policy missing / peering without forwarded traffic | firewall policy, peering settings |
Peering Disconnected | other side deleted/missing | recreate peering on both sides |
Peering RemoteNotInSync | address space changed | az network vnet peering sync |
| Connection drops after seconds | asymmetric routing | UDR on both sides, SNAT on the NVA |
| Private endpoint resolves publicly | zone not linked to the DNS VNet | private DNS link, check the VNet's DNS servers |
| NVA does not forward | IP forwarding on the NIC off | az network nic update --ip-forwarding true |
| LB does not distribute | probe fails | allow probe port, route to 168.63.129.16 |
VPN connection NotConnected | PSK/proposal/selectors, UDP 500/4500 | gateway logs, IPsec on the other side |
#Application Gateway / WAF
Backend health, probes, certificates and WAF blocks.
| Symptom | Cause | Fix |
|---|---|---|
| Backend Timeout | wrong port/protocol, network path | FQDN + HTTPS for App Service, check path |
| Backend 403 | name resolves publicly | link the private DNS zone to the gateway VNet |
| Backend Cannot connect | peering/NSG/NVA | Backend health → Troubleshoot |
| Backend certificate CN mismatch / not trusted | host header/certificate does not match | pick host name from backend, trusted root cert |
| Browser 502 | all backends unhealthy | fix backend health |
| Browser 403 | WAF rule | firewall log (KQL), exclusion/rule override |
Gateway Failed after a change | Key Vault certificate not readable | identity role, versionless secret ID, KV firewall |
| Deployment hangs / subnet error | UDR 0/0 or NSG blocks GatewayManager | remove UDR, add NSG rules |
#FortiGate
Typical symptoms on the firewall and where to look first.
| Symptom | Cause | Fix |
|---|---|---|
Sniffer shows only in, no out | firewall drops | debug flow (policy/route) |
| Sniffer shows nothing | traffic does not arrive | UDR/effective routes, NSG, peering |
Denied by forward policy check | no matching policy | create policy / check order |
reverse path check fail | route to the source missing | add a static route |
| HA: secondary does not take the config | end forgotten / out of sync | end, diagnose sys ha checksum cluster, execute ha synchronize start |
HA: switches back after unset | override disable, uptime | diagnose sys ha reset-uptime |
| Failover: public IP/route does not move | SDN connector without permissions | managed identity + roles, diagnose debug application azd -1 |
| GUI empty / no forwarding | BYOL without license | apply license or use PAYG |
| GUI not reachable | allowaccess missing / trusthost | set allowaccess https, check admin trusthost |
IPsec no SA proposal chosen | proposals differ | align phase 1/2 on both sides |
#Terraform
Plan, state, lock and provider errors.
| Symptom | Cause | Fix |
|---|---|---|
must be replaced | name/zone/image changed | check the plan, moved/ignore_changes if needed |
| State locked | aborted run | terraform force-unlock <id> or break the blob lease |
already exists – to be managed via Terraform this resource needs to be imported | resource already exists | import block or terraform import |
Inconsistent dependency lock file | provider version changed | terraform init -upgrade |
subscription ID could not be determined | azurerm 4.x without subscription | ARM_SUBSCRIPTION_ID or subscription_id in the provider |
403 … AuthorizationPermissionMismatch (backend) | data role on the storage account missing | Storage Blob Data Contributor |
Error acquiring the state lock in a pipeline | parallel run | serialize pipelines, then force-unlock |
429 Too Many Requests | throttling | -parallelism=5, retry later |
#SSH and Git
Key, host and push errors.
| Symptom | Cause | Fix |
|---|---|---|
Public key authentication failed | wrong/no RSA key | RSA key, ssh -vT, ~/.ssh/config |
Load key …: error in libcrypto | PuTTY / wrongly encoded file | regenerate or PuTTYgen "Export OpenSSH key" |
UNPROTECTED PRIVATE KEY FILE | file permissions too open | set permissions |
REMOTE HOST IDENTIFICATION HAS CHANGED | server rebuilt | ssh-keygen -R <host> |
post-quantum key exchange warning | info only | ignore |
git@ssh.dev.azure.com's password: | SSH key not accepted – Azure DevOps has no SSH password | Ctrl+C, test and debug: key registered? config applied? |
Permission denied (publickey) (Azure DevOps) | key not registered, wrong account, or config not applied | SSH key for Azure DevOps |
ssh-rsa is about to be deprecated … throttled | HostkeyAlgorithms +ssh-rsa in ~/.ssh/config | remove the +ssh-rsa lines – test and debug |
Git: rejected … fetch first | remote has newer commits | git pull (maybe --allow-unrelated-histories), then push |
Git: refusing to merge unrelated histories | two separate starts | git pull origin main --allow-unrelated-histories |
Git: ! [rejected] main -> main (non-fast-forward) | local branch behind the server | git pull --rebase origin main → git push, otherwise backup-branch way |
Git: nothing to commit although files changed | files are not in the repo folder | pwd, ls, git rev-parse --show-toplevel |
| Git: repo folder inside a folder of the same name | cloned inside <repo> | clone in the parent folder |
DevOps: main missing, only a feature branch | first push into an empty repo was the feature branch | create main and set it as default |
Git: CONFLICT (content) | same place changed | fix the file → git add → git commit |
Git: fatal: not a git repository | wrong folder | cd into the repo, git status |
Git: Authentication failed (HTTPS) | PAT expired / old credential stored | new PAT; Windows Credential Manager: delete entry git:https://dev.azure.com |
DevOps: TF402455: Pushes to this branch are not permitted | branch policy, PR only | feature branch + PR |
| DevOps PR: "There are merge conflicts" | target branch changed the same lines | resolve the conflict locally |
| DevOps PR: Complete not possible | required reviewer missing / build check red | add reviewer, check policies on the right side of the PR |
| DevOps PR: approval gone after push | policy "Reset votes on new pushes" | ask the reviewer to Approve again |
| Dev branch deleted after PR | "Delete source branch after merging" was on | recreate the branch: Repos → Branches → New branch (based on main) |
#Glossary
#Git and Azure DevOps
Repo, branch, pull request and related terms.
| Term | Explanation |
|---|---|
| Repository (repo) | folder with files and their complete change history |
| Clone | copy a repo from the server to your own machine |
| Commit | saved state with a message, local |
| Push / Pull | send commits to the server / get them from there |
| Fetch | get the server state without changing your own files |
| Branch | separate line of work, e.g. main, Dev, feature/x |
| Merge | take the changes of one branch into another |
| Rebase | "replay" your own commits onto the latest state of another branch |
| Squash | combine several commits into one |
| Cherry-pick | copy one specific commit into another branch |
| Merge conflict | the same place was changed in both branches – you have to decide |
| Stash | put uncommitted changes aside |
| HEAD | the commit you are currently on |
| Origin | default name for the repo on the server |
| Pull request (PR) | request to take a branch into another – with review |
| Reviewer | reviews and approves the PR (Required = mandatory, Optional = voluntary) |
| Approve | the reviewer agrees |
| Complete | the PR is merged |
| Abandon | the PR is closed without merging |
| Revert | counter-change to an earlier commit/PR |
| Branch policy | protection of a branch: PR only, minimum number of reviewers, build must be green |
| Build validation | pipeline that runs automatically on every PR |
| Pipeline | automatic build/deploy, described in YAML |
| Stage / Job / Step | structure of a pipeline |
| Agent | machine the pipeline runs on |
| Environment | target in DevOps with approvals |
| Service connection | a pipeline's access to Azure (usually via service principal / workload identity) |
| Variable group | shared variables/secrets for pipelines |
| PAT | personal access token – password replacement for Git over HTTPS / CLI |
#Azure General
Tenant, subscription, resource group and related terms.
| Term | Explanation |
|---|---|
| Tenant | one Entra ID instance, e.g. your company's |
| Management group | folder for subscriptions, inherits policies and permissions |
| Subscription | billing and permission boundary, contains resource groups |
| Resource group (RG) | logical container for resources with the same lifecycle |
| Resource ID | unique path /subscriptions/…/resourceGroups/…/providers/… |
| Region | data center location, e.g. West Europe |
| Availability zone | separate data centers within a region |
| SKU | size/performance tier of a resource |
| Lock | prevents deletion (CanNotDelete) or changes (ReadOnly) |
| Tag | key/value on resources, e.g. Owner |
| Deallocate | stop a VM and end compute costs |
| Provider | e.g. Microsoft.Network – must be registered in the subscription |
| Activity log | who changed what in Azure and when |
| Diagnostic setting | sends a resource's logs/metrics e.g. to Log Analytics |
| Log Analytics workspace | central log database, queried with KQL |
| Action group | who is notified on an alert |
| Landing zone | prepared subscription with network, policies, permissions |
#Permissions and Identities
Entra ID, RBAC, service principal, managed identity.
| Term | Explanation |
|---|---|
| Entra ID | (formerly Azure AD) identity service: users, groups, apps |
| RBAC | role-based access: who (principal) may do what (role) where (scope) |
| Scope | level of a role/policy: MG, subscription, RG, resource; / = whole tenant |
| Role assignment | combination of principal + role + scope |
| Principal | user, group, service principal or managed identity |
| Object ID | ID of an identity in Entra – needed for role assignments |
| App registration | definition of an application (app ID, secrets) |
| Service principal (SP) | an app's "user account" in the tenant, gets roles |
| Client ID / App ID | the app's identifier for sign-in |
| Client secret | an app's password – expires! |
| Managed identity | password-less identity for Azure resources (system- or user-assigned) |
| Workload identity federation | sign-in without a secret via token, e.g. from pipelines |
| PIM | Privileged Identity Management – activate roles only when needed and time-limited |
| Elevate access | Global Admin grants themselves User Access Administrator on / |
| Policy | rule that allows/denies/audits resources (Deny, Audit, Modify) |
| Exemption | time-limited exception from a policy |
| Key Vault | secure store for secrets, keys, certificates |
| Soft delete / purge protection | deleted items remain recoverable / may not be permanently deleted |
#Network
VNet, peering, UDR, NSG and related terms.
| Term | Explanation |
|---|---|
| VNet | your own private network in Azure |
| Subnet | part of a VNet |
| Address space / CIDR | e.g. 10.0.0.0/22 – /22 = 1024 addresses |
| Peering | direct connection of two VNets, not transitive |
| Hub-and-spoke | central hub VNet (firewall, VPN) with connected spoke VNets |
| UDR / route table | your own routes, e.g. 0.0.0.0/0 → firewall |
| Next hop | where a packet is sent next |
| Effective routes | all routes that actually apply to a NIC |
| NSG | network security group – simple firewall rules on subnet/NIC |
| Service tag | placeholder for IP ranges, e.g. AzureLoadBalancer |
| NVA | network virtual appliance, e.g. FortiGate as a VM |
| ILB / ELB | internal / external load balancer; ILB in front of the FortiGate for HA |
| Health probe | LB checks whether a backend responds (source 168.63.129.16) |
| Floating IP | destination address stays the LB's frontend IP |
| HA ports | one LB rule for all ports/protocols |
| SNAT / DNAT | source / destination address is rewritten |
| Asymmetric routing | return path does not use the same firewall → connection breaks |
| Private endpoint | private IP for a PaaS service in your own VNet |
| Private DNS zone | resolves e.g. *.privatelink.… to private IPs |
| Application Gateway | layer 7 load balancer for HTTP(S), optionally with WAF |
| WAF | web application firewall (Detection = log only, Prevention = block) |
| Listener | port/host name on which the App Gateway accepts requests |
| Backend pool | servers behind the App Gateway / LB |
| NAT Gateway | fixed outbound internet access for a subnet |
| Bastion | secure RDP/SSH access via the browser, without a public IP on the VM |
| VPN gateway / local network gateway | Azure side / representation of the remote side of an S2S VPN |
| BGP | routing protocol for automatic exchange of routes |
#FortiGate
FGCP, VIP, policy, SD-WAN and related terms.
| Term | Explanation |
|---|---|
| FGCP | FortiGate Clustering Protocol – high availability active/passive |
| Primary / Secondary | active / passive device in HA |
| Session pickup | connections survive a failover |
| SDN connector | connection to Azure, rewrites routes/IPs on failover |
| Policy | firewall rule (top to bottom, first match wins) |
| VIP | virtual IP – port forwarding/DNAT to the inside |
| Address / address group | address object / group of addresses |
| Sniffer | packet capture on the FortiGate |
| Debug flow | shows why a packet is allowed or dropped |
| VDOM | virtual domain – separate FortiGate instances on one device |
| Phase 1 / Phase 2 | IPsec: building the management connection / the actual tunnel SAs |
| SD-WAN | distributing traffic across several links by rules and quality |
| PAYG / BYOL / FortiFlex | license models: usage-based via Azure / own license / flexible quota |
#Infrastructure as Code
IaC, state, module, what-if and related terms.
| Term | Explanation |
|---|---|
| IaC | infrastructure as code – resources are described in files instead of clicked |
| Bicep / ARM | Microsoft's IaC language / the JSON format underneath |
| .bicepparam | parameter file for Bicep, usually one per environment |
| what-if | preview of what a deployment would change |
| Deployment stack | Bicep deployment that manages its resources and can clean up |
| AVM | Azure Verified Modules – reviewed Microsoft modules |
| Terraform | IaC tool from HashiCorp, provider azurerm |
| State | Terraform's memory: which resource belongs to which code |
| Plan / Apply | preview / execute in Terraform |
| Drift | difference between code and the actual resource (e.g. change in the portal) |
| Idempotent | running it several times gives the same result |
#Text Templates
#Pull Request Description
Template for the PR text.
## What<Short: what changes, e.g. "New parameters for spoke app-x in dev.bicepparam"> ## Why<Ticket / requirement / bug, e.g. "#1234 – connect app-x to the hub"> ## Impact- Environment: <dev / prod>- New: <resources>- Changed: <resources>- Deleted / replaced: <none / which> ## Checked- [ ] lint / validate without errors- [ ] what-if / plan reviewed (output below)- [ ] tested: <how> ## what-if / plan<paste output here>#Change Request
Template for a change request.
Title: <short description> Description: <what will be done, in 2–3 sentences>Reason: <why, ticket>Affected systems: <subscriptions, resources, applications>Time: <date, from–to>Expected impact: <none / short interruption of approx. X minutes for Y>Risk: <low / medium / high> – <reason>Steps: 1. <step> 2. <step>Test afterwards: <how to check that everything works>Rollback plan: <how it is undone, how long that takes>Performed by: <name>Business contact: <name>#Outage Notice
Template for the first message during an outage.
Subject: [OUTAGE] <system> – <short symptom> Since: <time>Affected: <who / what, e.g. "access to app-x from outside">Impact: <e.g. "application not reachable, internal use works">Status: under investigationNext update: <time, e.g. in 30 minutes>Contact: <name, phone>All-clear:
Subject: [RESOLVED] <system> – <short symptom> Resolved since: <time>Duration: <from–to>Cause: <short, e.g. "missing route after a change to the route table">Action: <what was done>Follow-up: <if planned>#Maintenance Announcement
Template for planned maintenance.
Subject: [MAINTENANCE] <system> on <date>, <from>–<to> What: <e.g. firmware update of the FortiGate cluster in the hub>When: <date>, <from>–<to>Impact: <e.g. short interruption of existing connections (< 1 minute) during failover>What you need to do: <nothing / save sessions / …>Contact during maintenance: <name, phone>#Handover to On-Call / Colleagues
Template for a shift or colleague handover.
Handover <date> – from <from> to <to> Open topics:- <topic> – status: <…> – next step: <…> Running changes / maintenance:- <…> Observations (keep an eye on):- <e.g. WAF reports increased blocks on app-x> Important links:- promptus, dashboards, tickets: <…>#Commit Messages
Format and examples.
<area>: <what> (<ticket>) Hub: add spoke app-x to GatewaySubnet UDR (#1234)FortiGate: add policy app-x -> internet (HTTPS)Params dev: adjust address space for spoke app-yFix: wrong probe path on the App GatewayImperative, short, area first – then you find it later with git log --grep "<area>".
#Naming Convention and Tags
Template based on the Cloud Adoption Framework (Define your naming convention) – adapt it to your standards and then make it binding in the team.
#Naming Components (CAF)
The parts a name is built from.
| Component | Meaning | Examples |
|---|---|---|
| Resource type | abbreviation of the resource type (Abbreviations) | rg, vm, st, app |
| Workload / application / project | what the resource belongs to | hub, webshop, sap |
| Environment | stage of the lifecycle | prod, dev, qa, stage, test |
| Region | Azure region (full or short) | westeurope / weu, germanywestcentral / gwc |
| Instance | number for otherwise identical names | 01, 001 |
Rules from CAF:
- Names can't be changed after creation for most resources – only put stable information in the name, everything else into tags.
- Fix the order of the components once and keep it (CAF examples: type → workload → environment → region → instance, e.g.
vm-workload3-dev-001). - Hyphen as delimiter – except where the resource doesn't allow it (storage account, container registry):
stworkload1data001,crworkload1prod001. - No personal or confidential data in names or tags.
- Automate with the generator on the web page or the Azure Naming Tool.
#Scope of Uniqueness
Which names must be unique where.
| Scope | Meaning | Examples |
|---|---|---|
| Global | unique in all of Azure (public endpoint / DNS name) | storage account, key vault, web app, function app, container registry, SQL server, Cosmos DB, Service Bus, API Management, Front Door endpoint |
| Resource group | unique within the RG | VNet, VM, NSG, public IP, load balancer, App Gateway |
| Parent resource | unique within the parent | subnet (in the VNet), SQL database (in the server), NSG rule |
#Length and Character Rules (Most Used)
Length and allowed characters per resource type.
| Resource | Length | Allowed | Scope |
|---|---|---|---|
| Resource group | 1–90 | letters, digits, - _ . () – not ending in . | subscription |
| Virtual network | 2–64 | letters, digits, - _ . | RG |
| Subnet | 1–80 | letters, digits, - _ . | VNet |
| NSG / route table / public IP / NIC | 1–80 | letters, digits, - _ . | RG |
| Virtual machine | 1–15 (Windows) / 1–64 (Linux) | letters, digits, - (Windows computer name max. 15!) | RG |
| Storage account | 3–24 | lowercase letters and digits only | global |
| Key vault | 3–24 | letters, digits, -; starts with a letter, no -- | global |
| Container registry | 5–50 | letters and digits only | global |
| Web app / function app | 2–60 | letters, digits, - | global |
| SQL server | 1–63 | lowercase letters, digits, - | global |
| Log Analytics workspace | 4–63 | letters, digits, - | RG |
| AKS cluster | 1–63 | letters, digits, - _ | RG |
| Managed identity | 3–128 | letters, digits, - _ | RG |
| Management group (ID) | 1–90 | letters, digits, - _ . () | tenant |
Full list: Naming rules and restrictions for Azure resources.
#Structure of a Name
Pattern and examples.
<type>-<workload>-<environment>-<region>-<nr> rg-hub-prd-weu-01 resource groupvnet-hub-prd-weu-01 VNetsnet-fw-trust-prd-weu-01 subnetvm-fgt-prd-weu-01 FortiGate Astappprdweu01 storage account (lowercase letters/digits only, max. 24, no "-")kv-app-prd-weu-01 Key Vault (max. 24 characters, globally unique)Rules: lowercase only, hyphen as separator, no special characters, running number always two digits.
#Abbreviations per Resource Type
Short codes for the most used resource types.
| Resource | Abbr. | Resource | Abbr. |
|---|---|---|---|
| Management group | mg | Resource group | rg |
| Virtual network | vnet | Subnet | snet |
| Network security group | nsg | Route table | rt |
| Public IP | pip | NAT gateway | ng |
| Load balancer (internal/external) | lbi / lbe | Application gateway | agw |
| WAF policy | waf | Azure Firewall / policy | afw / afwp |
| Bastion | bas | Virtual network gateway (VPN) | vgw |
| Local network gateway | lgw | VPN connection | con |
| Private endpoint | pep | Private DNS resolver | dnspr |
| Virtual machine | vm | Network interface | nic |
| OS disk / data disk | osdisk / disk | Availability set | avail |
| Key Vault | kv | Storage account | st |
| Managed identity | id | Log Analytics workspace | log |
| Application Insights | appi | App Service plan | asp |
| Web app | app | Function app | func |
| SQL server / DB | sql / sqldb | Container registry | cr |
| AKS cluster | aks | Recovery Services vault | rsv |
#Abbreviations for Regions and Environments
Short codes for regions and environments.
| Region | Abbr. | Environment | Abbr. |
|---|---|---|---|
| West Europe | weu | Production | prd |
| North Europe | neu | Test / acceptance | tst / uat |
| Germany West Central | gwc | Development | dev |
| Austria East | ate | Sandbox | sbx |
| Switzerland North | szn | Shared (platform) | shd |
#Tag Standard
Required and optional tags with examples.
| Tag | Required | Example | Purpose |
|---|---|---|---|
Owner | yes | network-team | who is responsible |
Environment | yes | prd | environment |
CostCenter | yes | 4711 | cost allocation |
Application | yes | hub | which application |
Criticality | recommended | high / medium / low | priority during incidents |
DataClassification | recommended | internal | protection level |
CreatedBy | optional | bicep-pipeline | how it was created |
ExpiryDate | for tests | 2026-12-31 | when to delete |
Tags are not inherited automatically – assign the built-in policy "Inherit a tag from the resource group if missing", and "Require a tag on resource groups" for required tags.
#Set and Check Tags
Set tags and find resources without them.
# Set on a resource group (keep existing ones)az group update -n <rg> --set tags.Owner=<team> tags.Environment=<env> tags.CostCenter=<cc># Add to a resourceaz resource tag --ids <id> --tags Owner=<team> -i# Tag all resources of an RGaz resource list -g <rg> --query "[].id" -o tsv | ForEach-Object { az resource tag --ids $_ --tags Environment=<env> -i | Out-Null }# Which RGs have no owner?az group list --query "[?tags.Owner==null].name" -o tsvCentrally in Bicep: define param tags object once and pass tags: tags to every resource.
#Azure Resource Abbreviations (CAF)
Tip: search at the top, e.g. firewall policy or afwp. Some abbreviations are used twice (proj, afd, np) – check the namespace when you need an exact match.
#AI and Machine Learning
Abbreviations for AI and machine learning resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| AI Search | Microsoft.Search/searchServices | srch |
| Foundry Tools (multi-service account) | Microsoft.CognitiveServices/accounts (kind: CognitiveServices) | ais |
| Foundry account | Microsoft.CognitiveServices/accounts (kind: AIServices) | aif |
| Foundry account project | Microsoft.CognitiveServices/accounts/projects | proj |
| Foundry hub | Microsoft.MachineLearningServices/workspaces (kind: Hub) | hub |
| Foundry hub project | Microsoft.MachineLearningServices/workspaces (kind: Project) | proj |
| Azure AI Video Indexer | Microsoft.VideoIndexer/accounts | avi |
| Azure Machine Learning workspace | Microsoft.MachineLearningServices/workspaces | mlw |
| Azure OpenAI Service | Microsoft.CognitiveServices/accounts (kind: OpenAI) | oai |
| Bot service | Microsoft.BotService/botServices (kind: azurebot) | bot |
| Computer vision | Microsoft.CognitiveServices/accounts (kind: ComputerVision) | cv |
| Content moderator | Microsoft.CognitiveServices/accounts (kind: ContentModerator) | cm |
| Content safety | Microsoft.CognitiveServices/accounts (kind: ContentSafety) | cs |
| Custom vision (prediction) | Microsoft.CognitiveServices/accounts (kind: CustomVision.Prediction) | cstv |
| Custom vision (training) | Microsoft.CognitiveServices/accounts (kind: CustomVision.Training) | cstvt |
| Document intelligence | Microsoft.CognitiveServices/accounts (kind: FormRecognizer) | di |
| Face API | Microsoft.CognitiveServices/accounts (kind: Face) | face |
| Health Insights | Microsoft.CognitiveServices/accounts (kind: HealthInsights) | hi |
| Immersive reader | Microsoft.CognitiveServices/accounts (kind: ImmersiveReader) | ir |
| Language service | Microsoft.CognitiveServices/accounts (kind: TextAnalytics) | lang |
| Speech service | Microsoft.CognitiveServices/accounts (kind: SpeechServices) | spch |
| Translator | Microsoft.CognitiveServices/accounts (kind: TextTranslation) | trsl |
#Analytics and IoT
Abbreviations for analytics and IoT resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| Azure Analysis Services server | Microsoft.AnalysisServices/servers | as |
| Azure Databricks Access Connector | Microsoft.Databricks/workspaces/accessConnectors | dbac |
| Azure Databricks workspace | Microsoft.Databricks/workspaces | dbw |
| Azure Data Explorer cluster | Microsoft.Kusto/clusters | dec |
| Azure Data Explorer cluster database | Microsoft.Kusto/clusters/databases | dedb |
| Azure Data Factory | Microsoft.DataFactory/factories | adf |
| Azure Digital Twin instance | Microsoft.DigitalTwins/digitalTwinsInstances | dt |
| Azure Stream Analytics | Microsoft.StreamAnalytics/cluster | asa |
| Synapse Analytics private link hub | Microsoft.Synapse/privateLinkHubs | synplh |
| Synapse Analytics SQL dedicated pool | Microsoft.Synapse/workspaces/sqlPools | syndp |
| Synapse Analytics Spark pool | Microsoft.Synapse/workspaces/bigDataPools | synsp |
| Synapse Analytics workspace | Microsoft.Synapse/workspaces | synw |
| Data Lake Store account | Microsoft.DataLakeStore/accounts | dls |
| Event Hubs namespace | Microsoft.EventHub/namespaces | evhns |
| Event hub | Microsoft.EventHub/namespaces/eventHubs | evh |
| Event hub consumer group | Microsoft.EventHub/namespaces/eventhubs/consumerGroups | evhcg |
| Event Grid domain | Microsoft.EventGrid/domains | evgd |
| Event Grid namespace | Microsoft.EventGrid/namespaces | evgns |
| Event Grid subscription | Microsoft.EventGrid/eventSubscriptions | evgs |
| Event Grid topic | Microsoft.EventGrid/domains/topics | evgt |
| Event Grid system topic | Microsoft.EventGrid/systemTopics | egst |
| Fabric capacity | Microsoft.Fabric/capacities | fc |
| HDInsight – Hadoop cluster | Microsoft.HDInsight/clusters | hadoop |
| HDInsight – HBase cluster | Microsoft.HDInsight/clusters | hbase |
| HDInsight – Kafka cluster | Microsoft.HDInsight/clusters | kafka |
| HDInsight – Spark cluster | Microsoft.HDInsight/clusters | spark |
| HDInsight – Storm cluster | Microsoft.HDInsight/clusters | storm |
| HDInsight – ML Services cluster | Microsoft.HDInsight/clusters | mls |
| IoT hub | Microsoft.Devices/IotHubs | iot |
| Provisioning services | Microsoft.Devices/provisioningServices | provs |
| Provisioning services certificate | Microsoft.Devices/provisioningServices/certificates | pcert |
| Power BI Embedded | Microsoft.PowerBIDedicated/capacities | pbi |
| Time Series Insights environment | Microsoft.TimeSeriesInsights/environments | tsi |
#Compute and Web
Abbreviations for compute and web resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| App Service environment | Microsoft.Web/hostingEnvironments | ase |
| App Service plan | Microsoft.Web/serverFarms | asp |
| Azure Load Testing instance | Microsoft.LoadTestService/loadTests | lt |
| Availability set | Microsoft.Compute/availabilitySets | avail |
| Azure Arc enabled server | Microsoft.HybridCompute/machines | arcs |
| Azure Arc enabled Kubernetes cluster | Microsoft.Kubernetes/connectedClusters | arck |
| Azure Arc private link scope | Microsoft.HybridCompute/privateLinkScopes | pls |
| Azure Arc gateway | Microsoft.HybridCompute/gateways | arcgw |
| Batch account | Microsoft.Batch/batchAccounts | ba |
| Cloud service | Microsoft.Compute/cloudServices | cld |
| Communication Services | Microsoft.Communication/communicationServices | acs |
| Disk encryption set | Microsoft.Compute/diskEncryptionSets | des |
| Function app | Microsoft.Web/sites | func |
| Gallery | Microsoft.Compute/galleries | gal |
| Hosting environment | Microsoft.Web/hostingEnvironments | host |
| Image template | Microsoft.VirtualMachineImages/imageTemplates | it |
| Managed disk (OS) | Microsoft.Compute/disks | osdisk |
| Managed disk (data) | Microsoft.Compute/disks | disk |
| Notification Hubs | Microsoft.NotificationHubs/namespaces/notificationHubs | ntf |
| Notification Hubs namespace | Microsoft.NotificationHubs/namespaces | ntfns |
| Proximity placement group | Microsoft.Compute/proximityPlacementGroups | ppg |
| Restore point collection | Microsoft.Compute/restorePointCollections | rpc |
| Snapshot | Microsoft.Compute/snapshots | snap |
| Static web app | Microsoft.Web/staticSites | stapp |
| Virtual machine | Microsoft.Compute/virtualMachines | vm |
| Virtual machine scale set | Microsoft.Compute/virtualMachineScaleSets | vmss |
| VM maintenance configuration | Microsoft.Maintenance/maintenanceConfigurations | mc |
| VM storage account | Microsoft.Storage/storageAccounts | stvm |
| Web app | Microsoft.Web/sites | app |
#Containers
Abbreviations for container resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| AKS cluster | Microsoft.ContainerService/managedClusters | aks |
| AKS system node pool | Microsoft.ContainerService/managedClusters/agentPools (mode: System) | npsystem |
| AKS user node pool | Microsoft.ContainerService/managedClusters/agentPools (mode: User) | np |
| AKS Fleet manager | Microsoft.ContainerService/fleets | fleet |
| Container app | Microsoft.App/containerApps | ca |
| Container apps environment | Microsoft.App/managedEnvironments | cae |
| Container apps job | Microsoft.App/jobs | caj |
| Container registry | Microsoft.ContainerRegistry/registries | cr |
| Container instance | Microsoft.ContainerInstance/containerGroups | ci |
| Service Fabric cluster | Microsoft.ServiceFabric/clusters | sf |
| Service Fabric managed cluster | Microsoft.ServiceFabric/managedClusters | sfmc |
#Databases
Abbreviations for database resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| Azure Cosmos DB database | Microsoft.DocumentDB/databaseAccounts/sqlDatabases | cosmos |
| Cosmos DB for Apache Cassandra account | Microsoft.DocumentDB/databaseAccounts | coscas |
| Cosmos DB for MongoDB account | Microsoft.DocumentDB/databaseAccounts | cosmon |
| Cosmos DB for NoSQL account | Microsoft.DocumentDb/databaseAccounts | cosno |
| Cosmos DB for Table account | Microsoft.DocumentDb/databaseAccounts | costab |
| Cosmos DB for Apache Gremlin account | Microsoft.DocumentDb/databaseAccounts | cosgrm |
| Cosmos DB PostgreSQL cluster | Microsoft.DBforPostgreSQL/serverGroupsv2 | cospos |
| Azure Managed Redis | Microsoft.Cache/RedisEnterprise | amr |
| Azure SQL Database server | Microsoft.Sql/servers | sql |
| Azure SQL database | Microsoft.Sql/servers/databases | sqldb |
| Azure SQL Elastic Job agent | Microsoft.Sql/servers/jobAgents | sqlja |
| Azure SQL Elastic Pool | Microsoft.Sql/servers/elasticpool | sqlep |
| MySQL database | Microsoft.DBforMySQL/servers | mysql |
| PostgreSQL flexible server | Microsoft.DBforPostgreSQL/flexibleServers | pgsql |
| SQL Managed Instance | Microsoft.Sql/managedInstances | sqlmi |
#Developer Tools and DevOps
Abbreviations for developer and DevOps resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| App Configuration store | Microsoft.AppConfiguration/configurationStores | appcs |
| Maps account | Microsoft.Maps/accounts | map |
| SignalR | Microsoft.SignalRService/SignalR | sigr |
| Web PubSub | Microsoft.SignalRService/webPubSub | wps |
| Azure Managed Grafana | Microsoft.Dashboard/grafana | amg |
| Managed DevOps Pools | Microsoft.DevOpsInfrastructure/pools | mdp |
#Integration
Abbreviations for integration resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| API Management service instance | Microsoft.ApiManagement/service | apim |
| Integration account | Microsoft.Logic/integrationAccounts | ia |
| Logic app | Microsoft.Logic/workflows | logic |
| Service Bus namespace | Microsoft.ServiceBus/namespaces | sbns |
| Service Bus queue | Microsoft.ServiceBus/namespaces/queues | sbq |
| Service Bus topic | Microsoft.ServiceBus/namespaces/topics | sbt |
| Service Bus topic subscription | Microsoft.ServiceBus/namespaces/topics/subscriptions | sbts |
#Management and Governance
Abbreviations for management and governance resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| Automation account | Microsoft.Automation/automationAccounts | aa |
| Azure Policy definition | Microsoft.Authorization/policyDefinitions | descriptive |
| Application Insights | Microsoft.Insights/components | appi |
| Azure Monitor action group | Microsoft.Insights/actionGroups | ag |
| Azure Monitor data collection rule | Microsoft.Insights/dataCollectionRules | dcr |
| Azure Monitor alert processing rule | Microsoft.AlertsManagement/actionRules | apr |
| Azure Monitor Private Link Scope | Microsoft.Insights/privateLinkScopes | ampls |
| Data collection endpoint | Microsoft.Insights/dataCollectionEndpoints | dce |
| Deployment script | Microsoft.Resources/deploymentScripts | script |
| Log Analytics workspace | Microsoft.OperationalInsights/workspaces | log |
| Log Analytics query pack | Microsoft.OperationalInsights/querypacks | pack |
| Management group | Microsoft.Management/managementGroups | mg |
| Microsoft Purview instance | Microsoft.Purview/accounts | pview |
| Resource group | Microsoft.Resources/resourceGroups | rg |
| Template spec | Microsoft.Resources/templateSpecs | ts |
#Migration
Abbreviations for migration resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| Azure Migrate project | Microsoft.Migrate/assessmentProjects | migr |
| Database Migration Service instance | Microsoft.DataMigration/services | dms |
| Recovery Services vault | Microsoft.RecoveryServices/vaults | rsv |
#Networking
Abbreviations for networking resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| Application gateway | Microsoft.Network/applicationGateways | agw |
| Application security group (ASG) | Microsoft.Network/applicationSecurityGroups | asg |
| Azure Enclave community | Microsoft.Mission/communities | cmt |
| Azure Enclave community endpoint | Microsoft.Mission/communities/communityEndpoints | ce |
| Azure Enclave dedicated hub | Microsoft.Mission/communities/dedicatedHubs | dh |
| Azure Enclave transit hub | Microsoft.Mission/communities/transitHubs | th |
| Azure Enclave enclave | Microsoft.Mission/virtualEnclaves | ve |
| Azure Enclave enclave endpoint | Microsoft.Mission/virtualEnclaves/enclaveEndpoints | ee |
| Azure Enclave enclave connection | Microsoft.Mission/enclaveConnections | ec |
| Azure Enclave workload | Microsoft.Mission/virtualEnclaves/workloads | wl |
| CDN profile | Microsoft.Cdn/profiles | cdnp |
| CDN endpoint | Microsoft.Cdn/profiles/endpoints | cdne |
| Connection | Microsoft.Network/connections | con |
| DNS zone (public) | Microsoft.Network/dnsZones | DNS domain name |
| DNS forwarding ruleset | Microsoft.Network/dnsForwardingRulesets | dnsfrs |
| DNS private resolver | Microsoft.Network/dnsResolvers | dnspr |
| DNS private resolver inbound endpoint | Microsoft.Network/dnsResolvers/inboundEndpoints | in |
| DNS private resolver outbound endpoint | Microsoft.Network/dnsResolvers/outboundEndpoints | out |
| Private DNS zone | Microsoft.Network/privateDnsZones | DNS domain name |
| Firewall | Microsoft.Network/azureFirewalls | afw |
| Firewall policy | Microsoft.Network/firewallPolicies | afwp |
| ExpressRoute circuit | Microsoft.Network/expressRouteCircuits | erc |
| ExpressRoute Direct | Microsoft.Network/expressRoutePorts | erd |
| ExpressRoute gateway | Microsoft.Network/virtualNetworkGateways | ergw |
| Front Door (Standard/Premium) profile | Microsoft.Cdn/profiles | afd |
| Front Door (Standard/Premium) endpoint | Microsoft.Cdn/profiles/afdEndpoints | fde |
| Front Door firewall policy | Microsoft.Network/frontdoorWebApplicationFirewallPolicies | fdfp |
| Front Door (classic) | Microsoft.Network/frontDoors | afd |
| IP group | Microsoft.Network/ipGroups | ipg |
| Load balancer (internal) | Microsoft.Network/loadBalancers | lbi |
| Load balancer (external) | Microsoft.Network/loadBalancers | lbe |
| Load balancer rule | Microsoft.Network/loadBalancers/inboundNatRules | rule |
| Local network gateway | Microsoft.Network/localNetworkGateways | lgw |
| NAT gateway | Microsoft.Network/natGateways | ng |
| Network interface (NIC) | Microsoft.Network/networkInterfaces | nic |
| Network security perimeter | Microsoft.Network/networkSecurityPerimeters | nsp |
| Network security group (NSG) | Microsoft.Network/networkSecurityGroups | nsg |
| NSG security rule | Microsoft.Network/networkSecurityGroups/securityRules | nsgsr |
| Network Watcher | Microsoft.Network/networkWatchers | nw |
| Private Link service | Microsoft.Network/privateLinkServices | pl |
| Private endpoint | Microsoft.Network/privateEndpoints | pep |
| Public IP address | Microsoft.Network/publicIPAddresses | pip |
| Public IP address prefix | Microsoft.Network/publicIPPrefixes | ippre |
| Route filter | Microsoft.Network/routeFilters | rf |
| Route server | Microsoft.Network/virtualHubs | rtserv |
| Route table | Microsoft.Network/routeTables | rt |
| Service endpoint policy | Microsoft.Network/serviceEndPointPolicies | se |
| Traffic Manager profile | Microsoft.Network/trafficManagerProfiles | traf |
| User defined route (UDR) | Microsoft.Network/routeTables/routes | udr |
| Virtual network | Microsoft.Network/virtualNetworks | vnet |
| Virtual network gateway | Microsoft.Network/virtualNetworkGateways | vgw |
| Virtual network manager | Microsoft.Network/networkManagers | vnm |
| Virtual network peering | Microsoft.Network/virtualNetworks/virtualNetworkPeerings | peer |
| Virtual network subnet | Microsoft.Network/virtualNetworks/subnets | snet |
| Virtual WAN | Microsoft.Network/virtualWans | vwan |
| Virtual WAN hub | Microsoft.Network/virtualHubs | vhub |
#Security
Abbreviations for security resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| Azure Bastion | Microsoft.Network/bastionHosts | bas |
| Key vault | Microsoft.KeyVault/vaults | kv |
| Key Vault Managed HSM | Microsoft.KeyVault/managedHSMs | kvmhsm |
| Managed identity | Microsoft.ManagedIdentity/userAssignedIdentities | id |
| SSH key | Microsoft.Compute/sshPublicKeys | sshkey |
| VPN Gateway (Virtual WAN) | Microsoft.Network/vpnGateways | vpng |
| VPN connection (Virtual WAN) | Microsoft.Network/vpnGateways/vpnConnections | vcn |
| VPN site (Virtual WAN) | Microsoft.Network/vpnGateways/vpnSites | vst |
| Web Application Firewall (WAF) policy | Microsoft.Network/firewallPolicies | waf |
| WAF policy rule group | Microsoft.Network/firewallPolicies/ruleGroups | wafrg |
#Storage
Abbreviations for storage resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| Azure Backup Resource Guard | Microsoft.DataProtection/resourceGuards | rgd |
| Backup vault | Microsoft.DataProtection/backupVaults | bvault |
| Backup vault policy | Microsoft.DataProtection/backupVaults/backupPolicies | bkpol |
| File share | Microsoft.Storage/storageAccounts/fileServices/shares | share |
| Storage account | Microsoft.Storage/storageAccounts | st |
| Storage Sync Service | Microsoft.StorageSync/storageSyncServices | sss |
#Virtual Desktop
Abbreviations for virtual desktop resources.
| Resource | Provider namespace | Abbreviation |
|---|---|---|
| Virtual desktop host pool | Microsoft.DesktopVirtualization/hostPools | vdpool |
| Virtual desktop application group | Microsoft.DesktopVirtualization/applicationGroups | vdag |
| Virtual desktop workspace | Microsoft.DesktopVirtualization/workspaces | vdws |
| Virtual desktop scaling plan | Microsoft.DesktopVirtualization/scalingPlans | vdscaling |
#Costs and Cleanup
#What Costs What?
Fastest in the portal: Cost Management → Cost analysis → view Resources or Resource groups, choose the period at the top, Group by → tag (e.g. CostCenter).
az extension add -n costmanagement# Costs of the current month per resource groupaz costmanagement query --type ActualCost --timeframe MonthToDate --scope "/subscriptions/<sub-id>" ` --dataset-aggregation '{\"totalCost\":{\"name\":\"Cost\",\"function\":\"Sum\"}}' ` --dataset-grouping name="ResourceGroupName" type="Dimension" -o jsonJSON escaping in PowerShell is tedious – for regular reports better save a view in the portal or set up an export (daily CSV into a storage account): Cost Management → Exports.
#Budget with Alert
Portal: Cost Management → Budgets → Add → amount, period Monthly → alerts e.g. at 80 % Actual and 100 % Forecasted → e-mail or action group.
az consumption budget create --budget-name budget-<rg> --resource-group <rg> ` --amount 500 --category cost --time-grain monthly ` --start-date 2026-11-01 --end-date 2027-10-31az consumption budget list -o tableNotifications (thresholds, recipients) are easiest to add afterwards on the budget in the portal.
#Azure Advisor Recommendations
Cost recommendations from Advisor.
az advisor recommendation list --category Cost ` --query "[].{recommendation:shortDescription.problem, resource:impactedValue, impact:impact}" -o tableaz advisor recommendation list --category Cost --query "[?contains(shortDescription.problem,'Right-size')]" -o tableTypical: downsize oversized VMs, buy reservations/savings plans, delete unused resources.
#Clean Up Orphaned Resources
Find and delete disks, IPs and NICs nobody uses.
az extension add -n resource-graph# 1. Findaz graph query -q "Resources | where type =~ 'microsoft.compute/disks' and properties.diskState =~ 'Unattached' | project id, name, resourceGroup, sizeGB = properties.diskSizeGB" -o tableaz graph query -q "Resources | where type =~ 'microsoft.network/publicipaddresses' and isnull(properties.ipConfiguration) and isnull(properties.natGateway) | project id, name, resourceGroup" -o tableaz graph query -q "Resources | where type =~ 'microsoft.network/networkinterfaces' and isnull(properties.virtualMachine) and isnull(properties.privateEndpoint) | project id, name, resourceGroup" -o table # 2. Check (tags, owner, who created it?)az resource show --ids <id> --query "{tags:tags, created:createdTime}"az monitor activity-log list --resource-id <id> --offset 90d --query "[?contains(operationName.value,'write')].{time:eventTimestamp, who:caller}" -o table # 3. Mark first, delete lateraz resource tag --ids <id> --tags DeleteAfter=2026-12-01 Reason=orphaned -iaz resource delete --ids <id> # after consultation / when the deadline has passedBack up disks as a snapshot before deleting if needed: az snapshot create -g <rg> -n snap-<disk> --source <disk-id>.
#Common Cost Traps
The usual ways money is wasted and the remedy for each.
| Trap | Remedy |
|---|---|
| VM only "stopped", not deallocated | az vm deallocate instead of shutting down in the OS |
| Premium disks on test VMs | switch to Standard SSD |
| Disks/public IPs left over after deleting a VM | set "Delete with VM" when deleting, Resource Graph query above |
| Application Gateway / firewall running permanently in test | stop outside working hours (az network application-gateway stop) |
| Log Analytics collects too much | reduce diagnostic categories, check retention, Basic Logs for bulk data |
| Bastion Standard in every environment | one central Bastion in the hub |
| Old snapshots and backups | define retention, lifecycle rules |
| Traffic between regions/zones | workloads and hub in the same region |