# Azure CLI Cheat Sheet — Configuration & Identity

> **Tool:** Azure CLI (az)
> **Category:** Cloud CLIs
> **Verified against:** Azure CLI 2.87.0, flags verified via `az login --help`, `az ad sp create-for-rbac
> --help`, `az ad app federated-credential --help`, `az identity --help`, and `az role assignment create
> --help` run locally, plus Microsoft Learn / GitHub Docs (OIDC federation), 2026-08-29
> **Official docs:** https://learn.microsoft.com/cli/azure

## 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

```bash
# 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.

```mermaid
flowchart TD
    Tenant["Entra ID tenant<br/>(formerly Azure AD)"] --> Sub1["Subscription: Production"]
    Tenant --> Sub2["Subscription: Sandbox"]
    Sub1 --> RG1["Resource group: app-prod-rg"]
    Sub1 --> RG2["Resource group: data-prod-rg"]
    RG1 --> VM["VM: web-01"]
    RG1 --> AKS["AKS cluster: prod-cluster"]

    classDef info fill:#e5f0fa,stroke:#1d6fb8,color:#10161c
    classDef accent fill:#f0e9fb,stroke:#6d43c0,color:#10161c
    class Tenant info
    class Sub1,Sub2 accent
```

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

```bash
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):

```json
{
  "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

```bash
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)

```bash
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.

## 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.

```mermaid
sequenceDiagram
    participant GH as GitHub Actions job
    participant OIDC as GitHub OIDC provider
    participant Entra as Microsoft Entra ID
    participant Azure as Azure Resource Manager

    GH->>OIDC: Request short-lived OIDC token<br/>(id-token: write permission)
    OIDC-->>GH: Signed JWT (repo, branch, workflow claims)
    GH->>Entra: Exchange JWT for Azure access token<br/>(azure/login action with client-id + tenant-id)
    Entra->>Entra: Match JWT subject against<br/>registered federated credential
    Entra-->>GH: Short-lived Azure access token
    GH->>Azure: Authenticated az CLI / SDK calls

    Note over Entra: No stored secret anywhere — trust is<br/>the federated credential's subject match
```

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.

```bash
# 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)

```bash
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

```bash
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)

```bash
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>`:

```ini
[defaults]
group = my-rg
location = eastus

[core]
output = table
```

```bash
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:

```bash
# 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)

```yaml
# .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 do** — `Owner` 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.
