The Azure CLI (az) is the quickest way to learn how Azure is organised and to script it. This post installs it, deploys a small virtual machine the way you would for a real workload (not wide open), and sets up identities for automation.
Versions in use on this date. Azure CLI 2.55 (released 5 December 2023) is the current release; 2.56 follows on 9 January. Azure Active Directory was renamed Microsoft Entra ID in July 2023, although the CLI command group is still az ad.
How Azure is organised
flowchart TB
T[Entra ID tenant<br/>users groups apps] --> MG[Management group<br/>optional]
MG --> SUB[Subscription<br/>billing and quota boundary]
SUB --> RG[Resource group rg-demo-ci<br/>lifecycle container]
RG --> VNET[Virtual network and subnet]
RG --> NSG[Network security group]
RG --> VM[Virtual machine<br/>managed disk and NIC]
VNET --> VM
NSG --> VM
RBAC[Azure RBAC role assignment<br/>scope subscription or group] -.-> RG
A subscription is the billing and quota boundary. A resource group holds resources that share a lifecycle, so deleting the group deletes everything inside. Access is granted with Azure RBAC: a role (Reader, Contributor, or a custom role) assigned to a user, group or identity at a scope (management group, subscription, resource group or single resource). Resource groups also live in one region, but the resources inside can be elsewhere.
Install and sign in
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
az version
az login # opens a browser; use --use-device-code on servers
az account list -o table
az account set --subscription "<name or id>"
az account show --query "{name:name, id:id, tenant:tenantId}" -o table
The installer uses Microsoft’s package repository, so later updates come through apt upgrade (or run az upgrade). Extensions add features (az extension add --name resource-graph).
Create a resource group and a VM safely
az group create --name rg-demo-ci --location centralindia --tags owner=raviteja env=demo
az vm create --resource-group rg-demo-ci --name vm-demo \
--image Ubuntu2204 --size Standard_B2s \
--admin-username azureuser --generate-ssh-keys \
--public-ip-sku Standard --nsg-rule NONE
az network nsg rule create --resource-group rg-demo-ci --nsg-name vm-demoNSG \
--name allow-ssh-mine --priority 1000 --protocol Tcp --destination-port-ranges 22 \
--source-address-prefixes "$(curl -s https://ifconfig.me)/32" --access Allow
az vm list -d -o table
By default az vm create opens SSH (port 22) to the whole internet. --nsg-rule NONE skips that, and the second command allows SSH only from your own address. For anything beyond a lab, use Azure Bastion or just-in-time access (Microsoft Defender for Cloud) so no management port is exposed at all. Use JMESPath to read output without extra tools:
az vm show -g rg-demo-ci -n vm-demo -d --query "{ip:publicIps, state:powerState}" -o table
Put a budget on the subscription (Cost Management) before experimenting, and use tags for owner, environment and cost centre so spend can be traced.
Clean up in one command
az group delete --name rg-demo-ci --yes --no-wait
Deleting the group removes the VM, disk, NIC, public IP and NSG together, so nothing is left billing.
Identities for automation
Automation should not use your own login.
| Option | Use when |
|---|---|
| Managed identity | Code runs on Azure (VM, App Service, AKS): az login --identity, no secret exists |
| Workload identity federation | CI outside Azure (GitHub Actions, GitLab): exchanges a short-lived token, no secret stored |
| Service principal with a secret | Last resort, when neither above works |
A service principal scoped to one resource group:
az ad sp create-for-rbac --name sp-ci --role Contributor \
--scopes /subscriptions/<id>/resourceGroups/rg-demo-ci
The output contains a secret; treat it like a password, set a short expiry, and scope the role to a resource group, never the whole subscription. For GitHub Actions, prefer federation: create the app registration and add a federated credential that trusts one repository and branch:
az ad app create --display-name app-github-ci
az ad app federated-credential create --id <app-object-id> --parameters '{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:acme/shop-api:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"] }'
az role assignment create --assignee <app-client-id> --role Contributor \
--scope /subscriptions/<id>/resourceGroups/rg-demo-ci
The workflow then signs in with the azure/login action using the client, tenant and subscription IDs, with no stored secret.