Codex
Commands, ready-made workflows and forms for daily work. Search at the top (key /), click a line = copy, ? shows all keyboard shortcuts.
Topics
Start
- Copy-Paste Workflowsready-made command sequences: sign-in, SSH for DevOps, App Gateway certificate, troubleshooting
- Daily Commandsthe most frequent commands, PowerShell profile, keyboard shortcuts
- Runbook: Outage "Something Is Not Reachable""something is not reachable" – the first 10 minutes
- Checkliststo tick off: spoke, certificate, firmware, go-live, PR, subscription
Azure
- Basics, Resources, VMs, Servicessign-in, filters, resources, VMs, App Service, storage, monitoring
- NetworkVNet, peering, UDR, NSG, VPN, Azure Firewall, private endpoints, App Gateway, LB
- Azure Front Doororigin group, origin (App Service), custom domain + certificate, assign route, WAF, lock-down
- Key Vault, Identity, Roles, PolicyKey Vault, managed identity, RBAC, Entra ID, policy
- Azure PowerShell (Az Module)Az module
- Azure CLI ↔ Azure PowerShellthe same task in both worlds
- Resource Graph – Queries Across All Subscriptionsready-made queries across all subscriptions: inventory, network, security, cleanup, tags
- Entra ID – Apps, Secrets, Groups, PIMapp registrations, expiring secrets, groups, PIM
- Management Groups and Landing Zonelanding zone structure, hierarchy, move a subscription
- Defender for Cloud and Securitysecure score, recommendations, plans, alerts, JIT
- Storage in Daily WorkAzCopy, SAS, restore, protection, Files, lifecycle
- Monitoring and Alertsaction groups, metric/activity/log alerts, maintenance windows
- Costs and Cleanupview costs, budget, Advisor, orphaned resources
- Naming Convention and TagsCAF naming components, uniqueness, length rules, tag standard
- Azure Resource Abbreviations (CAF)all Microsoft CAF resource abbreviations with provider namespace
Code and DevOps
- Bicep in a Team Repostructure, .bicepparam per environment, what-if before the PR, AVM, deployment stacks
- Bicep / ARMbuild, what-if, deployments
- Terraformworkflow, state, import/moved, functions, backend
- Gitsetup, daily work, branches, undo
- Azure DevOpsrepos, PAT, Terraform pipeline template
- Azure DevOps Pipelinesstart/investigate runs, approvals, template Bicep what-if + deploy
Network and Firewall
- FortiGate CLIsystem, HA, routing, BGP, policies, VIP, IPsec, troubleshooting
- Azure Network Basicsrules of thumb, subnet sizes, ports, check order
Clients and Logs
- Linuxnetwork, DNS, services, tests
- Windows / PowerShellnetwork, DNS, firewall, files, events
- SSH and Certificates (OpenSSL)keys, config, agent, certificates
- Log Analytics (KQL)Log Analytics queries
Reference
- Error → Fixerror message → cause → fix
- Glossaryterms from Git, DevOps, Azure, network, FortiGate, IaC
- Text TemplatesPR, change, outage, maintenance, handover, commit
#I want to …
#Complete Workflows
All in 00 Workflows:
#Azure General
| … | Command | Details |
|---|---|---|
| switch the subscription | az account set --subscription <x> | 01 |
| find a resource without knowing where | az graph query -q "Resources | where name contains '<x>'" | 01 |
| see who changed what | az monitor activity-log list -g <rg> --offset 2h | 01 |
| run a command in a VM | az vm run-command invoke … --scripts "<cmd>" | 01 |
| stop VM costs | az vm deallocate -g <rg> -n <vm> | 01 |
| check whether a SKU works in a zone | az vm list-skus -l <region> --size <sku> | 01 |
| accept Marketplace terms | az vm image terms accept --urn <urn> | 01 |
| see a web app's live log | az webapp log tail -g <rg> -n <app> | 01 |
#Network
| … | Command | Details |
|---|---|---|
| know which route applies | az network nic show-effective-route-table -g <rg> -n <nic> | 02 |
| check the next hop | az network watcher show-next-hop … | 02 |
| check peering status | az network vnet peering list -g <rg> --vnet-name <vnet> | 02 |
| point a 0/0 route to the firewall | az network route-table route create … --next-hop-type VirtualAppliance | 02 |
| see NSG rules incl. defaults | az network nsg rule list … --include-default | 02 |
| see VPN/BGP status | az network vnet-gateway list-bgp-peer-status | 02 |
| add a rule to the Azure Firewall | az network firewall policy rule-collection-group collection rule add … | 02 |
| link a private DNS zone to a VNet | az network private-dns link vnet create … | 02 |
| check App Gateway backends | az network application-gateway show-backend-health | 02 |
| set the WAF to Prevention | az network application-gateway waf-policy policy-setting update --mode Prevention | 02 |
| enable IP forwarding on an NVA NIC | az network nic update --ip-forwarding true | 01 |
| look up a subnet size | table | 14 |
| split an address space into subnets | Generator → Subnet calculator | 14 |
| create privatelink DNS zones for a landing zone | Generator → Private DNS zones for a landing zone | 02 |
| troubleshoot systematically | DNS → route → NSG → firewall → return path → target | 14 |
#Permissions and Secrets
| … | Command | Details |
|---|---|---|
| see which roles someone has | az role assignment list --assignee <x> --all -o table | 03 |
| give a service principal Owner | az role assignment create --assignee-principal-type ServicePrincipal --assignee-object-id $id --scope <scope> --role Owner | 00 |
| my object ID | az ad signed-in-user show --query id -o tsv | 03 |
| read a secret | az keyvault secret show --vault-name <kv> -n <name> --query value -o tsv | 03 |
| recover a deleted secret | az keyvault secret recover … | 03 |
| publish an app via Front Door | Generator → Front Door: publish app | 32 |
| assign a domain to an existing Front Door route | az afd route update --custom-domains <all-ids> | 32 |
| query across all subscriptions | az graph query -q "<kql>" | 19 |
| find orphaned disks/IPs | Resource Graph → cleanup | 19 |
| create security groups per role in bulk | New-AzADGroup -DisplayName <name> -MailNickname <name> -SecurityEnabled | 21 |
| find expiring app secrets | az ad app credential list | 21 |
| build resource names after our convention | Generator → Naming convention | 27 |
| look up a resource abbreviation | Generator → Abbreviation lookup | 33 |
| create an alert | az monitor metrics alert create | 25 |
| find out which policy blocks | az policy assignment list --disable-scope-strict-match … | 03 |
#Terraform / Bicep
| … | Command | Details |
|---|---|---|
| plan and apply safely | terraform plan -out tfplan → terraform apply tfplan | 05 |
| rename a resource without rebuild | moved { from = … to = … } | 05 |
| take over an existing resource | import { to = … id = … } | 05 |
| see drift only | terraform plan -refresh-only | 05 |
| release a lock | terraform force-unlock <id> | 05 |
| calculate subnets | cidrsubnets("10.0.0.0/24", 2, 2, 2, 2) | 05 |
| test Bicep without risk | az deployment group what-if … | 06 |
| a pipeline for Terraform | template | 12 |
#FortiGate
| … | Command | Details |
|---|---|---|
| check version/license | get system status | 07 |
| capture packets | diagnose sniffer packet any 'host <ip>' 4 20 | 07 |
| know why a packet is dropped | debug flow | 07 |
| understand a debug flow message | table | 07 |
| check the route for an IP | get router info routing-table details <ip> | 07 |
| check HA sync | diagnose sys ha checksum cluster | 07 |
| test failover | execute ha failover set 1 | 07 |
| check an IPsec tunnel | get vpn ipsec tunnel summary | 07 |
| see BGP neighbors | get router info bgp summary | 07 |
| set up port forwarding | config firewall vip | 07 |
| search the configuration | show full-configuration | grep -f <text> | 07 |
#Clients (Linux / Windows)
| … | Linux | Windows | Details |
|---|---|---|---|
| test a port | nc -zv <ip> <port> | Test-NetConnection <ip> -Port <port> | 08 / 09 |
| check DNS | dig +short <name> | Resolve-DnsName <name> | 08 / 09 |
| see the public IP | curl -s ifconfig.me | Invoke-RestMethod https://api.ipify.org | 08 / 09 |
| path to a target | ip r get <target> | Find-NetRoute -RemoteIPAddress <target> | 08 / 09 |
| listening ports | ss -tlnp | Get-NetTCPConnection -State Listen | 08 / 09 |
| continuous failover test | while true; do curl …; done | while ($true) { … } | 08 / 09 |
| check a server's certificate | openssl s_client -connect <host>:443 … | – | 13 |
#Git / SSH
| … | Command | Details |
|---|---|---|
| push a folder as a repo | git init -b main … git push -u origin main | 11 |
| undo the last commit | git reset --soft HEAD~1 / git revert <commit> | 11 |
| park changes briefly | git stash / git stash pop | 11 |
| search the history | git log -S "<text>" | 11 |
| get an existing DevOps repo | git clone git@ssh.dev.azure.com:v3/<org>/<project>/<repo> | 00 |
| push is rejected (TF402455) | git switch -c feature/<name> → git push -u origin feature/<name> → PR | 00 |
| create a pull request | az repos pr create --source-branch <from> --target-branch <to> --open | 00 |
| resolve a merge conflict in a PR | git merge origin/Dev → fix files → git commit → git push | 00 |
| undo a PR | abandon (open) / revert (completed) | 00 |
| bring Dev to main | PR Dev → main, "Delete Dev after merging" off | 00 |
| SSH key for Azure DevOps | ssh-keygen -t rsa -b 4096 | 13 |
| test the connection to Azure DevOps | ssh -T git@ssh.dev.azure.com | 13 |
#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 index.html – a single file that runs offline in the browser (fonts are loaded online, otherwise system font).
#Publish on Your Own Domain (e.g. codex.cloudorbis.at)
index.html is a single static file – any static hosting works. Example with an Azure Storage static website:
python tools/build-html.pyaz storage blob service-properties update --account-name <sa> --static-website --index-document index.html --auth-mode loginaz storage blob upload --account-name <sa> -c '$web' -f index.html -n index.html --overwrite --auth-mode login --content-type "text/html; charset=utf-8"az storage account show -n <sa> --query "primaryEndpoints.web" -o tsv # target for the CNAMEThen at the DNS provider: CNAME codex.cloudorbis.at -> <web-endpoint-host> and add HTTPS via Front Door (custom domain with managed certificate, see 32 Front Door) – or use Azure Static Web Apps, which includes a free certificate for custom domains. Placeholders, favorites and checklists are stored per browser (localStorage) – nothing is sent to a server.
#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 16 Daily Commands.
- Then run
python tools/build-html.pyand commitindex.htmltoo. - Never check in real IPs, names, keys, tokens or licenses – placeholders
<...>only.
#Copy-Paste Workflows
Ready-made command sequences for recurring tasks. Adjust the variables at the top, then run block by block. Do not write real names, secrets or IDs back into this repo. Single commands live in the topic files.
Sign-in and Basics: Sign in to Azure · Set subscription (CLI and PowerShell) · Unblock and run downloaded scripts · Find a resource without knowing where it is · Who changed or deleted what · Create a resource group with tags and delete lock
Virtual Machines: VM: run a command, reset password, console · Stop / start all VMs in a resource group · Find a Marketplace image and accept the terms · Before deploying: check zones and quota
Network: Connect a new spoke to the hub · Extend address space and sync peering · NSG: temporarily allow your own IP · VM cannot reach the internet · Create a private endpoint for a web app · Private endpoint does not resolve privately · Check a VPN connection · Reach the FortiGate GUI or a VM without public IP via Bastion
Application Gateway and WAF: App Gateway: add a certificate from Key Vault · App Gateway: renew / replace a certificate · Import a PFX into Key Vault · App Gateway: backend unhealthy · WAF: find a false positive and add an exclusion · Send diagnostic logs to Log Analytics
Permissions, Key Vault, Policy: Give a service principal a role (subscription, management group, root) · Create a managed identity and assign a role · Which permissions do I (or someone else) have? · Policy blocks a deployment · Find expiring certificates
Terraform and Bicep: New Terraform project with remote state · Terraform locally with a service principal · Import an existing resource into Terraform · Rename a resource or move it into a module · State lock is stuck · Check drift (changes made in the portal) · Update the provider version · Deploy Bicep safely
FortiGate: FortiGate: apply and check the license · FortiGate: let a spoke reach the internet · FortiGate: allow spoke-to-spoke · FortiGate: port forwarding from outside (VIP) · FortiGate: trace a packet · FortiGate: test HA failover · FortiGate: HA out of sync · FortiGate: debug an IPsec tunnel · FortiGate: back up the configuration
Linux and Windows: Linux: test web server showing the hostname · Linux: service does not start · Linux: disk full · Windows: check a connection step by step
Git and SSH: SSH key for Azure DevOps and first push · Push a folder as a new repo (HTTPS) · Git: quickly switch branches in between · Git: feature branch with pull request · Git: resolve a merge conflict · Git: secret committed by accident
Azure DevOps: Repo and Pull Requests: Clone an existing DevOps repo · Connect a local folder to an existing DevOps repo · Change via feature branch and pull request to Dev · Push rejected: TF402455 (protected branch) · Reviewers in a pull request · Resolve a merge conflict in a pull request · Undo a pull request (abandon / revert) · Promote Dev to main (release)
#Sign-in and Basics
#Sign in to Azure
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
Get-ChildItem -Recurse *.ps1 | Unblock-File # remove the download markSet-ExecutionPolicy -Scope Process Bypass # this session only.\<script>.ps1Permanently for the user: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.
#Find a resource without knowing where it is
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
$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
$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#Virtual Machines
#VM: run a command, reset password, console
$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 $vm#Stop / start all VMs in a resource group
$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
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
$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.
#Network
#Connect a new spoke to the hub
$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>" # 1. VNet and subnetaz 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 false # 2. Peering 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-traffic# with VPN/ER gateway in the hub additionally: hub side --allow-gateway-transit, spoke side --use-remote-gateways # 3. Route table: everything via the firewallaz 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" # 4. Checkaz network vnet peering list -g $rg --vnet-name $vnet --query "[].{name:name, state:peeringState, sync:peeringSyncLevel}" -o tableThen on the FortiGate: route and policy for the spoke. With a VPN gateway: add a GatewaySubnet UDR for the spoke prefix → NVA.
#Extend address space and sync peering
$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
$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-MyIp#VM cannot reach the internet
$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
$rg = "<rg>" ; $app = "<webapp>" ; $vnet = "<vnet>" ; $snet = "<snet-pe>"$rgDns = "<rg-dns>" ; $zone = "privatelink.azurewebsites.net"$dnsVnetId = "<vnet-id-that-resolves-dns>" $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" # DNS zone (once per environment) and linkaz 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 websites # disable public access and checkaz webapp update -g $rg -n $app --set publicNetworkAccess=Disablednslookup "$app.azurewebsites.net" # from the VNet: private IP expectedOther services: zone and --group-id from the table.
#Private endpoint does not resolve privately
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
$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
$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.
#Application Gateway and WAF
#App Gateway: add a certificate from Key Vault
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 the App Gateway.
PowerShell
$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 $AppGW # takes a few minutesAzure CLI
az 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 tableVersionless = the App Gateway picks up a renewed version automatically (checked about every 4 hours).
Assign an identity if there is none yet
$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>).ResourceIdAssign 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
# 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
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
$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
$rg = "<rg>" ; $policy = "<waf-policy>"# 1. Temporarily only log (if production traffic is being blocked)az network application-gateway waf-policy policy-setting update -g $rg --policy-name $policy --mode Detection --state Enabled// 2. Which 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_s# 3. Exclusion 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 $policy # 4. Switch back to blockingaz network application-gateway waf-policy policy-setting update -g $rg --policy-name $policy --mode Prevention --state EnabledMore --match-variable values: RequestHeaderNames, RequestCookieNames, RequestArgValues … Disable a single rule instead of an exclusion: Portal → WAF policy → Managed rules.
#Send diagnostic logs to Log Analytics
$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, Policy
#Give a service principal a role (subscription, management group, root)
$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 tableScope / (Tenant Root Group) – careful: grants Owner on all subscriptions and management groups in the tenant. You need User Access Administrator on / yourself, which a Global Admin only gets temporarily:
# 1. 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"# 2. Assignment as above with --scope "/"az role assignment create --assignee-principal-type ServicePrincipal --assignee-object-id $id --scope "/" --role "Owner"# 3. 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 "/"If possible use Contributor instead of Owner (+ User Access Administrator only where the SP must assign roles). Remove again: az role assignment delete --assignee $id --role "Owner" --scope $scope.
#Create a managed identity and assign a role
$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?
$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
# 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
# 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()#Terraform and Bicep
#New Terraform project with remote state
$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.tf:
terraform { backend "azurerm" { resource_group_name = "rg-tfstate" storage_account_name = "<sttfstateunique>" container_name = "tfstate" key = "<project>.tfstate" use_azuread_auth = true }}$env:ARM_SUBSCRIPTION_ID = az account show --query id -o tsvterraform initterraform plan -out tfplan#Terraform locally with a service principal
$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 tfplanRemove-Item Env:ARM_CLIENT_SECRET, Env:ARM_CLIENT_ID # remove afterwardsWithout ARM_CLIENT_* Terraform uses the az login session.
#Import an existing resource into Terraform
# 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
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
# 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)
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
# 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
$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 table#FortiGate
#FortiGate: apply and check the license
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
config firewall address edit "net-<spoke>" set subnet 10.1.0.0 255.255.255.0 nextendconfig 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" nextendconfig 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
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)
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 nextendconfig 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
# 1. Does it arrive and does it leave?diagnose sniffer packet any 'host <ip>' 4 20 # 2. Why 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 reset # 3. Cross-check route and policyget router info routing-table details <dest-ip>show firewall policy | grep -i -A20 <name>Interpreting messages: debug flow table.
#FortiGate: test HA failover
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 }On 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
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
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
config system global set admin-scp enableendscp <admin>@<mgmt-ip>:sys_config ".\fgt-backup-$(Get-Date -Format yyyyMMdd).conf"Or GUI → admin menu → Configuration → Backup. Backups contain hashes/secrets – not into the repo.
#Linux and Windows
#Linux: test web server showing the hostname
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
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
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#Windows: check a connection step by step
$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/HTTP#Git and SSH
#SSH key for Azure DevOps and first push
# 1. Generate a key (Azure DevOps only accepts RSA)ssh-keygen -t rsa -b 4096 -f $HOME\.ssh\id_rsa # 2. Copy the public key -> Azure DevOps: top right User settings -> SSH public keys -> + New Key -> pasteGet-Content $HOME\.ssh\id_rsa.pub | Set-Clipboard # 3. Remove an old, possibly broken SSH config (optional)Remove-Item $HOME\.ssh\config -ErrorAction SilentlyContinue # 4. Test the connection – answer "Shell access is not supported" = OKssh -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 mainError Permission denied (publickey) → check step 2; ssh -vT git@ssh.dev.azure.com shows which key is tried.
#Push a folder as a new repo (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.
#Git: quickly switch branches in between
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#Git: feature branch with pull request
git switch main ; git pullgit switch -c feature/<name># … change …git add -Agit commit -m "<what and why>"git push -u origin feature/<name>az repos pr create --source-branch feature/<name> --target-branch main --title "<title>" --open# after the mergegit switch main ; git pullgit branch -d feature/<name>#Git: resolve a merge conflict
git pull # reports CONFLICTgit status # affected files# open the file, keep the right version between <<<<<<< ======= >>>>>>>, delete the markersgit add <file>git commit # or git rebase --continuegit push# abort everything:git merge --abort#Git: secret committed by accident
# 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.
#Azure DevOps: Repo and Pull Requests
How a change flows in a repo with protected branches:
| Step | From → to | How |
|---|---|---|
| 1. Work | own branch feature/<name> | change locally, commit, push |
| 2. Test | feature/<name> → Dev | pull request + approval |
| 3. Production | Dev → main | pull request + approval |
Pushing directly to Dev or main is not possible (error TF402455) – always via a pull request (PR).
#Clone an existing DevOps repo
Address: DevOps → Repos → select repo → top right Clone → copy the URL.
cd C:\Git # folder where the repo should gogit clone git@ssh.dev.azure.com:v3/<org>/<project>/<repo> # via SSH (key registered in DevOps)git clone https://dev.azure.com/<org>/<project>/_git/<repo> # via HTTPS (browser sign-in / PAT)cd <repo>git branch -a # which branches exist? (e.g. main, Dev)git switch Dev # switch to the working branchRepo 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
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)#Change via feature branch and pull request to Dev
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 PR – browser: after the push, Create a pull request appears at the top → choose 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:
git switch Dev ; git pullgit branch -d feature/<name> # clean up the local feature branch#Push rejected: TF402455 (protected branch)
! [remote rejected] Dev -> Dev (TF402455: Pushes to this branch are not permitted; you must use a pull request to update this branch.)The branch has a branch policy. Your commits are already on the local Dev – this is how they get into a PR:
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 server# then create PR feature/<name> -> Dev (see above) # bring local Dev back to the server stategit switch Devgit fetch origingit reset --hard origin/DevOnly run reset --hard once the feature branch is safely on the server.
#Reviewers in a pull request
| Field | Meaning |
|---|---|
| Required reviewers | mandatory reviewers – without their Approve the PR cannot be completed |
| Optional reviewers | get notified, approval not needed |
- First click Create without entries – the branch policy often adds the required reviewers (e.g. a team group) automatically.
- If nobody is listed afterwards: add the colleague/team lead who approves for your team. Old PRs show who that is: Repos → Pull requests → Completed.
- You usually cannot approve your own PR.
- Once all approvals are in and the checks are green → Complete.
#Resolve a merge conflict in a pull request
The PR shows "There are merge conflicts": the same lines were changed in Dev since your branch was created.
git switch feature/<name>git fetch origingit merge origin/Dev # reports CONFLICTgit status # affected files ("both modified")code . # in VS Code: Accept Current / Incoming / BothIn the file:
<<<<<<< HEADparam location = 'westeurope' ← your version=======param location = 'austriaeast' ← version from Dev>>>>>>> origin/DevKeep the right version, delete the three marker lines, then:
git add <file> # for each resolved filegit commit # merge commitgit push # the PR updates automaticallygit merge --abort # instead: abort everything- For parameter files: keep new parameters from
Dev. Who changed them:git log origin/Dev -- <file>. - Depending on the policy the approval is reset after the push – the reviewer has to click Approve again.
#Undo a pull request (abandon / revert)
PR still open → abandon it, nothing changes in Dev:
- Browser: in the PR, next to Complete click ▾ / ⋯ → Abandon (can be revived later with Reactivate).
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.
Clean up locally:
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.
#Promote Dev to main (release)
Check first 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" --openWhen you click Complete:
- 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 in the history.
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#Daily Commands
#Azure CLI
| 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
| 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 | Update-Module Az |
#Terraform
| 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
| 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
| 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
| Task | Windows (PowerShell) | Linux |
|---|---|---|
| Own IPs | ipconfig | ip -br a |
| Public IP | irm https://api.ipify.org | curl -s ifconfig.me |
| DNS | Resolve-DnsName <name> | dig +short <name> |
| Flush DNS cache | ipconfig /flushdns | resolvectl flush-caches |
| Port | Test-NetConnection <ip> -Port 443 | nc -zv <ip> 443 |
| HTTP(S) with details | curl.exe -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> |
| Test against a specific IP | curl.exe -k --resolve <host>:443:<ip> https://<host> | curl -k --resolve <host>:443:<ip> https://<host> |
| Path | tracert -d <ip> | tracepath -n <ip> |
| Routes | route print -4 | ip r |
| Listening ports | netstat -ano | findstr LISTEN | ss -tlnp |
#PowerShell Tricks
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 …)#PowerShell Profile: Your Own Shortcuts
Open with notepad $PROFILE (create the file first if needed: New-Item -Force $PROFILE), paste the snippets, start a new console.
# --- 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 } # --- Search Codex ---function cmds { & "<path-to-repo>\search.ps1" @args }Then e.g.: azsub, tfp, port 10.0.1.4 443, cmds peering. Note: gl, gp, gc are already taken in PowerShell (Get-Location etc.) – hence the other names.
#Keyboard Shortcuts
| 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 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>:12345FortiGate:
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
#After the Outage
#Checklists
#Connect a New Spoke
Workflow with commands: Connect a new spoke to the hub
#Certificate Change on the Application Gateway
Workflow: Add a certificate from Key Vault
#FortiGate Firmware Upgrade in HA
#Go-Live of an Application
#Before Creating a Pull Request
#Put a New Subscription into Operation
#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
#Sign-in and 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)
-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
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: 19 Resource Graph.
#Virtual Machines
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
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
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
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: 23 Storage.
#AKS (short)
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
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
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 CLI – Network
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
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
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)
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
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
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 and Bastion
az network nat gateway list -o tableaz network nat gateway create -g <rg> -n <natgw> --public-ip-addresses <pip> --idle-timeout 4az network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --nat-gateway <natgw>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.
#VPN Gateway (Site-to-Site)
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
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
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
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
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 (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
$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
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
# 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
# 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
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
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 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
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
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
# 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 → "Generate a certificate in Key Vault".
#Managed Identity
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)
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)
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: 21 Entra ID.
#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.
#Azure PowerShell (Az Module)
Install-Module Az -Scope CurrentUser -Repository PSGalleryUpdate-Module AzConnect-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#Resources
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
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
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
$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
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
| 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
| 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
| 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
| 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
| 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 | – |
#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
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 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 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
// 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
// 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 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 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.
#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
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
# 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
# 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# -> 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
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 → Entra ID: create 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
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.
#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
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
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.
#What Applies at Which Level?
$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
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.
#Defender for Cloud and Security
#Secure Score and 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
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
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
$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#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: winget install Microsoft.Azure.AZCopy.10
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
# 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 tsvPermissions: r read · l list · w write · c create · d delete. Always as short and as little as possible.
#Restore Deleted and Old Versions
# 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
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
az storage share-rm list -g <rg> --storage-account <sa> -o tableaz storage share-rm create -g <rg> --storage-account <sa> -n <share> --quota 100# Windows: test the connection and map a drive (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>Many 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>#Monitoring and Alerts
#Action Group (Who Gets Notified)
$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
$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
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
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 |
#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
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
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
| 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 |
#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)
| Component | Meaning | Examples |
|---|---|---|
| Resource type | abbreviation of the resource type (33) | 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
| 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)
| 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
<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
| 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
| 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
| 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 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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 |
#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
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)
// 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 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
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
$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 tableSubscription 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
{ "analyzers": { "core": { "rules": { "no-hardcoded-location": { "level": "warning" }, "no-unused-params": { "level": "warning" }, "secure-parameter-default": { "level": "error" }, "use-recent-api-versions": { "level": "warning" } } } }}#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
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.
#Terraform
Top 5:terraform init·terraform plan -out tfplan·terraform apply tfplan·terraform state list·terraform output
#Basic Workflow
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
| 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
terraform outputterraform output -raw <name>terraform output -json > outputs.jsonterraform show # current state, readableterraform state listterraform state list | Select-String <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 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
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
$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?Bash: export TF_VAR_name=value · unset TF_VAR_name
#Quality and Security
tflint --init ; tflint --recursive # linter (provider rules)checkov -d . # security checkstrivy config . # alternative to checkovterraform-docs markdown table . > README.md # generate module docs#.gitignore
.terraform/*.tfstate*.tfstate.*tfplan*.tfplancrash.logcrash.*.logterraform.tfvars*.auto.tfvarsoverride.tfoverride.tf.jsonCommit .terraform.lock.hcl (pinned provider versions).
#Git
Top 5:git status·git pull·git add -A ; git commit -m "..."·git push·git log --oneline --graph -20
#Setup
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
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
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
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
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
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
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 pushingSecret pushed by accident: change/revoke the secret immediately – removing it from history is not enough.
#Remote
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 server#Rebase or Merge?
| 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
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?
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
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
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:.
#Azure DevOps
#In the Portal
| 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
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
| 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
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
# 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#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 / 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
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
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)
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
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
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
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
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)
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
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
# 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
| 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
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
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
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)
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). Checklist: 28 Checklists.
#FortiManager Connection
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
execute log filter resetexecute log filter category 0 # 0 = trafficexecute log filter field dstip <ip>execute log filter view-lines 50execute log display#Backup
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.
#Azure Network Basics
#Rules of Thumb
| 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
| 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
| 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
#Linux
Top 5:ip a·ip r get <target>·ss -tlnp·curl -v·sudo tcpdump -ni eth0 host <ip>
#Network
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
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
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
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
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
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
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-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
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
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
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
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 }#SSH and Certificates (OpenSSL)
#Keys
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
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.
Host ssh.dev.azure.com HostName ssh.dev.azure.com User git IdentityFile ~/.ssh/id_rsa_azdo IdentitiesOnly yes 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.
#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
# 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.
#Log Analytics (KQL)
#Skeleton
<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
// 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 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
// 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)
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
// 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#Error → Fix
#Azure General
| Symptom | Cause | Fix |
|---|---|---|
RequestDisallowedByPolicy | Azure Policy | az policy assignment list, exemption or policy team |
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 |
#Network
| 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
| 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
| 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
| 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
| 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: 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: 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
| 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
| 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
| 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
| 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
| 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
| 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
## 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
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
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
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
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:- Codex, dashboards, tickets: <…>#Commit Messages
<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>".