Configuration & Identity
.mdVerified 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 tableNote
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.
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/accountsImportant
--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-rgThis 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.
Federated credentials: keyless CI/CD via OpenID Connect (the current recommended pattern)#
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.
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 tsvA 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 tableConfig 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 = tableaz config set defaults.group=my-rg defaults.location=eastus
az config get defaults.groupSetting 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-rgImportant
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-rbacfor 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 (
ReaderbeforeContributorbeforeOwner) - Record the assignment's purpose somewhere outside Azure itself (a ticket, a README) —
az role assignment listshows what is assigned, never why
Common pitfalls#
- Assuming
az loginre-authenticates on everyaz 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 successfulaccount 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
subjectfor the wrong GitHub trigger type — see the WARNING above;ref:refs/heads/maindoes not match apull_requesttrigger. - Granting
OwnerwhenContributor(or narrower) would do —Owneradditionally 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.