Verified13 commandsAI-assisted

Configuration & Identity

.md

Verified against Azure CLI 2.87.0, flags verified via `az login --help`, `az ad sp create-for-rbac · official docs

What it is and where it fits 🎯#

The Azure CLI (az) is Microsoft's official cross-platform command-line tool for creating and managing every Azure resource — a thin, scriptable layer over the same Azure Resource Manager (ARM) REST API that the Azure Portal itself calls. It plays the same role in the Azure ecosystem that aws plays for AWS and gcloud plays for GCP: the default way to script, automate, and CI/CD-drive infrastructure that isn't already managed declaratively through Bicep/ARM/Terraform. This page covers the part of az you touch before you can do anything else — logging in, picking which subscription and resource group your commands target, and the identity primitives (service principals, managed identities, RBAC role assignments) that everything else on this site's Azure pages builds on. The companion pages cover compute (02), storage and networking (03), and monitoring/governance (04).

One structural difference from AWS/GCP worth internalizing immediately: Azure CLI has one active login session that can see many subscriptions, rather than a separate named profile per account. az account set switches which subscription your commands target — it does not re-authenticate — whereas AWS profiles and gcloud configurations each carry their own credentials.

Installation#

# Debian/Ubuntu — official Microsoft-maintained script
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# macOS
brew update && brew install azure-cli

# Windows
winget install -e --id Microsoft.AzureCLI

az --version                 # confirm install + list installed extensions
az upgrade                   # upgrade az itself and every installed extension in one command
az extension list --output table

Note

az upgrade (not az update) is the current subcommand for self-upgrading — it upgrades both the core CLI and every extension you have installed (aks-preview, application-insights, etc.) in one pass, which matters because a stale extension is a common source of "this flag doesn't exist" confusion when the core CLI's docs describe a newer behavior than the extension you actually have installed.

Core concepts: the scope hierarchy#

Every Azure resource lives inside exactly one resource group, every resource group lives inside exactly one subscription, and every subscription belongs to exactly one Microsoft Entra ID tenant (Entra ID is Microsoft's current name for the identity service formerly called Azure Active Directory / Azure AD — you'll still see az ad as the command prefix and "AAD" in a lot of docs and error messages). This hierarchy is what almost every --scope argument in this page's role-assignment and policy examples is built from.

Diagram

Everything under a resource group is deleted together when the group is deleted — the closest Azure analogue to how AWS resources live in a region/account and GCP resources live in a project, except Azure adds this extra grouping layer within a subscription specifically for lifecycle and RBAC scoping.

Logging in and switching subscriptions#

az login                                                # interactive browser login (falls back to device
                                                         # code if no browser is reachable, e.g. over SSH)
az login --use-device-code                              # force device-code flow explicitly
az login --service-principal -u <app-id> -p <secret> --tenant <tenant-id>
az login --service-principal -u <app-id> --certificate /path/to/cert.pem --tenant <tenant-id>
az login --identity                                     # system-assigned managed identity (on an Azure VM/
                                                         # App Service/Function that has one)
az login --identity --client-id <user-assigned-identity-client-id>

az account list --output table                          # every subscription this login can see
az account show                                          # the currently active subscription
az account set --subscription "My Subscription Name"     # switch active subscription (no re-auth)
az account list-locations --output table                 # every Azure region this account can deploy into
az account clear                                          # remove all cached credentials/accounts

Important

--password on az login/az ad sp create-for-rbac no longer accepts a service-principal certificate — pass --certificate explicitly for cert-based auth. This changed in a recent CLI version and silently produces a confusing auth failure if you're following an older tutorial that still shows --password cert.pem.

Sample az account show output (representative shape — the exact fields are stable across versions, the values are illustrative):

{
  "environmentName": "AzureCloud",
  "id": "0b1f6471-1bf0-4dda-aec3-111122223333",
  "isDefault": true,
  "name": "Production",
  "state": "Enabled",
  "tenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
  "user": {
    "name": "jane@example.com",
    "type": "user"
  }
}

Resource groups#

az group create --name my-rg --location eastus
az group list --output table
az group show --name my-rg
az group delete --name my-rg --yes --no-wait            # skip the confirmation prompt, don't block on completion
az group exists --name my-rg                            # scriptable existence check, prints true/false

--yes skips the interactive confirmation Azure CLI would otherwise show before a destructive delete — double-check --name before scripting this, since az group delete cascades to every resource inside it with no separate confirmation per resource.

Service principals for automation (the older, still-common pattern)#

az ad sp create-for-rbac \
  --display-name my-ci-sp \
  --role Contributor \
  --scopes /subscriptions/<sub-id>/resourceGroups/my-rg

This creates both a Microsoft Entra application and its associated service principal, and prints the appId/password/tenant a pipeline needs to authenticate via az login --service-principal. The output is a long-lived secret credential — treat it exactly like a password, never commit it, and rotate it periodically (--years controls the secret's validity period, defaulting to 1 year).

Tip

Best practice: scope --role/--scopes as narrowly as the automation actually needs. Contributor on a whole subscription is far broader than most CI jobs require — scope to a single resource group (as shown above), or even a single resource, whenever the pipeline only ever touches one thing. A leaked broad-scope secret is a subscription-wide incident; a leaked narrow-scope secret is a contained one.

A create-for-rbac secret is a long-lived static credential sitting in your CI system's secret store — exactly the kind of thing workload identity federation exists to eliminate. Azure supports OIDC-based federated credentials: instead of storing a client secret, you register a trust relationship between an Entra ID application and a specific external identity (a specific GitHub repo + branch, a specific GitLab project, a Kubernetes service account, etc.), and the external system exchanges its own short-lived OIDC token for an Azure access token at runtime — no secret ever stored anywhere.

Diagram

The diagram's key point: the only thing that has to be created and stored on the Azure side is the federated credential's subject-matching rule, not a secret — the token exchange itself happens fresh on every workflow run.

# 1. Create an Entra ID app registration + service principal (no secret needed)
az ad app create --display-name my-ci-app
az ad sp create --id <app-id-from-above>

# 2. Grant it the RBAC role it needs, scoped narrowly
az role assignment create --assignee <app-id> --role Contributor \
  --scope /subscriptions/<sub-id>/resourceGroups/my-rg

# 3. Register the federated credential — the subject must exactly match the caller
az ad app federated-credential create --id <app-id> --parameters '{
  "name": "github-actions-main",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:my-org/my-repo:ref:refs/heads/main",
  "audiences": ["api://AzureADTokenExchange"]
}'

Warning

The federated credential's subject is matched case-sensitively against the exact claim in the incoming token. A common failure mode (AADSTS700213: No matching federated identity record found for presented assertion subject) is registering repo:my-org/my-repo:ref:refs/heads/main for a workflow that actually runs on pull_request — GitHub's token subject for a pull request is repo:my-org/my-repo:pull_request, not a branch ref. Register a separate federated credential per trigger type you actually use (ref:refs/heads/main, pull_request, environment:production), not one and hope.

Managed identities (for Azure-hosted workloads, not external CI)#

az identity create --resource-group my-rg --name my-app-identity
az identity list --resource-group my-rg --output table
az identity show --resource-group my-rg --name my-app-identity --query principalId -o tsv

A managed identity is Azure's equivalent of an AWS instance profile or a GCP-attached service account — an identity Azure itself issues and rotates for you, usable only from an Azure compute resource (a VM, an AKS pod via workload identity, a Function App), never from an external system like GitHub Actions. System- assigned identities are created and destroyed with the resource they're attached to (one-to-one, simplest to reason about); user-assigned identities are standalone resources you create once and attach to multiple compute resources, which is the pattern for anything you want to survive the VM/cluster being recreated.

RBAC role assignments#

az role assignment create --assignee <user-or-sp-id> --role Reader --scope /subscriptions/<sub-id>/resourceGroups/my-rg
az role assignment list --assignee <user-or-sp-id> --all
az role assignment list --scope /subscriptions/<sub-id>/resourceGroups/my-rg
az role assignment delete --assignee <user-or-sp-id> --role Reader --scope /subscriptions/<sub-id>/resourceGroups/my-rg
az role definition list --custom-role-only --output table   # your subscription's custom role definitions

--role accepts either a built-in role name (Reader, Contributor, Owner, Storage Blob Data Contributor, and hundreds more) or a custom role's name/ID. --scope is always a full ARM resource ID — Azure RBAC's inheritance flows down the tenant → subscription → resource group → resource hierarchy from the earlier diagram, so a role assigned at the resource-group level applies to every resource inside it.

Looking up users and groups (Entra ID)#

az ad user list --filter "displayname eq 'Jane Doe'"
az ad user show --id jane@example.com
az ad group list --display-name my-team-group
az ad group member list --group my-team-group --output table

Config file and defaults#

az reads/writes ~/.azure/config (INI format) for persistent defaults, and honors environment variables of the shape AZURE_DEFAULTS_<SETTING>:

[defaults]
group = my-rg
location = eastus

[core]
output = table
az config set defaults.group=my-rg defaults.location=eastus
az config get defaults.group

Setting defaults.group means every subsequent --resource-group flag in that shell session can be omitted — genuinely useful for an interactive session working repeatedly against one resource group, but worth avoiding in scripts meant to run unattended in CI, where an explicit --resource-group on every command is safer than relying on a config file that might not exist (or might point somewhere else) on the runner.

Real-world scenario: rotating a leaked service-principal secret without downtime#

A create-for-rbac secret used by a production deployment pipeline was accidentally logged in plaintext. Rotating it without breaking the pipeline mid-deploy:

# 1. Create a new credential on the SAME app registration (doesn't invalidate the old one yet)
az ad app credential reset --id <app-id> --append --years 1

# 2. Update the pipeline's stored secret to the new value, verify one successful run

# 3. Only after the new credential is confirmed working, remove the old one
az ad app credential list --id <app-id> --query "[].keyId" -o tsv
az ad app credential delete --id <app-id> --key-id <old-key-id>

--append is the detail that matters — az ad app credential reset without it replaces every existing credential in one step, which would break the pipeline's current run before the new secret is ever deployed. Adding a second, parallel credential and removing the old one only after cutover avoids that outage window.

Real-world scenario: GitHub Actions CI recipe (OIDC, no stored secret)#

# .github/workflows/deploy.yml
name: Deploy to Azure
on:
  push:
    branches: [main]

permissions:
  id-token: write     # required for OIDC token request
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - run: az group show --name my-rg

Important

Even with OIDC, three values still need to be stored as GitHub secrets — client-id, tenant-id, and subscription-id. None of these is itself sensitive (they're identifiers, not credentials), but keeping them as secrets rather than plain workflow variables avoids leaking your tenant/subscription topology in a public workflow file. The thing OIDC actually eliminates is the client secret — there is no long-lived password anywhere in this pipeline.

Real-world scenario: pre-flight checklist before granting subscription-wide access#

  • Confirm the actual minimum scope needed — a specific resource group, not the whole subscription
  • Prefer a federated credential over create-for-rbac for anything running in GitHub Actions/GitLab CI
  • If a secret-based service principal is unavoidable, set --years 1 (not the default indefinite) and calendar a rotation reminder
  • Assign the narrowest built-in role that covers the job (Reader before Contributor before Owner)
  • Record the assignment's purpose somewhere outside Azure itself (a ticket, a README) — az role assignment list shows what is assigned, never why

Common pitfalls#

  • Assuming az login re-authenticates on every az account set — it doesn't; subscription switching and authentication are two separate operations, and a stale/expired login will fail on the next command after a successful account set, which is confusing if you don't know the two are decoupled.
  • Using create-for-rbac's password output as if it were permanent — it has a default 1-year expiration; a pipeline that silently starts failing months later with an auth error is usually this.
  • Registering a federated credential's subject for the wrong GitHub trigger type — see the WARNING above; ref:refs/heads/main does not match a pull_request trigger.
  • Granting Owner when Contributor (or narrower) would doOwner additionally grants the ability to manage RBAC itself, which most automation never needs and which meaningfully widens blast radius if the credential leaks.

Exit codes#

0 success · non-zero on any API/auth/validation error — az does not use a distinct exit code per error category the way some tools do; check the printed error message or add --debug for the underlying HTTP status. For scripted checks, prefer a targeted read command (az group exists) over parsing error text.

When to reach for something else#

For declarative, reviewable infrastructure provisioning, prefer Bicep, ARM templates, or Terraform's azurerm provider over scripting az resource-creation commands directly — az is the right tool for one-off operational tasks, imperative automation glue, and anything genuinely ad hoc, but a growing pile of az resource create calls in a shell script is exactly the state-drift problem declarative IaC tools exist to solve. Azure PowerShell (Az module) covers the identical API surface for teams already standardized on PowerShell — functionally equivalent, different syntax, not a capability difference.