Skip to content

Azure CLI & azd

Key Points

  • az = Azure CLI; the operator's swiss army knife. Provision, query, modify any Azure resource.
  • azd (Azure Developer CLI) = higher-level dev workflow. azd up provisions + deploys an app from a template / Aspire AppHost.
  • az login with browser, device code, service principal, or managed identity. Persists session locally.
  • Common patterns: query JMESPath (--query), output JSON for scripting, batch ops with --ids, tab completion.
  • For .NET shops, azd + Aspire is the modern path; az for ad-hoc ops.

Concepts (deep dive)

What az and azd are (and how they differ)

Both are command-line tools that talk to Azure on your behalf, but they sit at very different levels of abstraction.

  • az (Azure CLI) is a thin, universal wrapper around the Azure REST API. Every az <group> <command> maps almost 1:1 to an HTTP request against a specific resource provider. It knows nothing about your app, your repo, or your deployment flow — it just exposes the entire surface area of Azure as a uniform CLI. Use it when you need to do one specific Azure thing.
  • azd (Azure Developer CLI) is an opinionated app-workflow tool that sits on top of az + Bicep/Terraform + Aspire AppHost. It knows your project lives in this folder, your IaC lives in ./infra, your environment variables live in .azure/<env>/.env, and one command (azd up) should provision the infra and deploy your code. Use it when you want a dotnet run-shaped experience for "ship the whole app to Azure."
   higher level     ┌─────────────────────────────────────────────┐
                    │  azd up    (app + infra + env, one command) │
                    └──────────────┬──────────────────────────────┘
                                   │   under the hood, calls:
                ┌──────────────────┼─────────────────────────────┐
                │                  │                             │
   lower level  ▼                  ▼                             ▼
        ┌─────────────┐    ┌─────────────────┐         ┌─────────────────┐
        │  az ... ... │    │ bicep / terraform│         │ Aspire AppHost  │
        │ (REST API   │    │  (declares infra)│         │  (declares topo)│
        │  surface)   │    └─────────────────┘         └─────────────────┘
        └─────────────┘
        ┌───────────────────────────────────────────────────────────────┐
        │                Azure Resource Manager (ARM) API               │
        └───────────────────────────────────────────────────────────────┘

For .NET shops on a greenfield app: azd init from an Aspire AppHost, then azd up — the topology you already declared in Program.cs becomes the deployed Azure resource graph. For ad-hoc ops, querying state, fixing one knob, debugging — az.

Control plane vs data plane

A foundational distinction every Azure CLI session lives inside, and one of the most common sources of "why doesn't this command work?" confusion:

  • The control plane is Azure managing the resource itself — create, delete, list, scale, configure. All control-plane traffic goes through Azure Resource Manager (ARM). az group create, az webapp restart, az storage account create are all control-plane operations.
  • The data plane is talking to the resource you've already provisioned — read a blob, send a Service Bus message, query Cosmos DB, fetch a Key Vault secret. Each service has its own data-plane endpoint and its own auth model.

Why this matters for CLI auth: an identity with Contributor on a Key Vault can create and delete the vault but cannot read secrets from it — secrets are data plane, and need a separate role (Key Vault Secrets User) or access policy. The same separation applies to Storage (RBAC role vs SAS / access keys), Service Bus (Owner vs Sender), Cosmos DB (control-plane role vs SQL-level role).

  ┌────────────┐     control plane          ┌────────────┐
  │   az CLI   │  ──────────────────────►   │    ARM     │  ── creates / deletes / scales
  │            │  (RBAC: Contributor/Owner) └────────────┘
  │            │
  │            │     data plane             ┌────────────────────┐
  │            │  ──────────────────────►   │ resource endpoint  │  ── read blob, send msg,
  └────────────┘  (RBAC: data-role, or SAS, │ (blob, sb, kv,…)   │     fetch secret…
                   access key, etc.)        └────────────────────┘

How authentication context propagates

az login doesn't just authenticate — it caches a token plus context on disk (~/.azure/). Every subsequent az (and azd) invocation reads that cache, picks the right token for the active subscription + tenant, and refreshes it as needed. This is why a freshly opened shell on the same machine "already knows who you are."

The cache contains: signed-in account(s), the active subscription (one per profile at a time), the active cloud (Public / Gov / China), and any defaults you set. Three things flow from this:

  1. Forgetting az account set --subscription is a top-3 production accident — the command runs in whatever subscription happened to be active, which may be the dev sub or some auditor's read-only sub. Scripts should always set the subscription explicitly.
  2. The token is bearer-style — anyone who reads ~/.azure/ reads your Azure access. Don't share VMs, don't commit the folder, don't bake it into images.
  3. CI is a different beast. CI doesn't have a human at a browser; it can't run az login interactively. Modern CI uses federated identity credentials (FIC) — the CI's runtime issues an OIDC token, Azure trusts that token via a pre-registered federation, no secrets cross the wire.
   developer laptop                                CI pipeline
   ────────────────                                ───────────
   az login (browser/device)                       (no browser)
        │                                                │
        ▼                                                ▼
   ~/.azure/ token cache                          OIDC token from runner
        │                                                │
        ▼                                                ▼
   az <command> reads cache,                      azure/login action exchanges
   sends bearer token to ARM                      OIDC token for ARM token
        │                                                │
        └──────────────► ARM / data plane ◄──────────────┘

az basics

The minimum vocabulary: log in, confirm the active subscription, set it explicitly if you have more than one, then operate on resource groups and resources within them. Every command outputs JSON by default; piping into --query or --output shapes that for humans or scripts.

az login                                              # browser login
az account show                                        # current sub
az account set --subscription "Production"
az group create -n myrg -l eastus
az resource list -g myrg
az logout

Output formats

JSON is the default and always works. table is for humans skimming a terminal; tsv is for shell scripts that need to consume the output without invoking jq (for id in $(az ... -o tsv); do ...).

az resource list -g myrg --output table       # human
az resource list -g myrg --output json        # default
az resource list -g myrg --output tsv         # script-friendly

JMESPath queries

--query filters and projects the JSON response server-side in the CLI before output formatting. This avoids piping into jq and the associated string-quoting pain. The syntax is JMESPath — array filters in [? ... ], projection in .{ ... }. Worth a half hour to learn; pays off forever.

az resource list -g myrg --query "[?type=='Microsoft.Web/sites'].name"
az webapp list --query "[?state=='Running'].{name:name, plan:appServicePlanId}"

Lets you filter/project without jq.

Batch ops

Many az commands accept --ids and operate on the supplied IDs in parallel. Combined with --query to produce the ID list, this is the canonical "delete every resource of type X" or "tag every resource matching predicate Y" pattern.

ids=$(az resource list -g myrg --query "[].id" -o tsv)
az resource delete --ids $ids

Service principal login

A service principal is a non-human identity in Entra ID — an "app" that can be granted RBAC roles and signed in as. The legacy way to use one in scripts/CI is with a client secret (password); the modern way is with a federated token issued by your CI provider (no secret to leak or rotate).

az login --service-principal -u $appId -p $secret --tenant $tenantId
# OR
az login --service-principal -u $appId --tenant $tenantId --federated-token $oidcToken

In CI, use federated credentials (no secret).

Managed identity in scripts

When the script runs on an Azure resource (VM, App Service, Container Apps, AKS pod with workload identity), the resource already has an identity injected by Azure. --identity tells az to grab a token from the local IMDS endpoint instead of prompting for a browser login. No secret, no SP setup, no rotation.

az login --identity                                   # on Azure VM/container with MI

Useful commands

az ad signed-in-user show                              # current user
az role assignment create --assignee $upn --role Reader --scope $scope
az keyvault secret show --vault-name kv -n mysecret --query value -o tsv
az functionapp deployment slot list -n func -g rg
az aks get-credentials -n cluster -g rg                # kubeconfig
az containerapp logs show -n app -g rg --follow

azd (Azure Developer CLI)

azd packages the full "I have an app, ship it to Azure" lifecycle into five verbs. init scaffolds the manifest + infra; provision runs Bicep/Terraform; deploy packages and pushes code; up does both in sequence; down tears it all back down. The manifest (azure.yaml) maps each service in your codebase to a target host (App Service, Container Apps, Functions, Static Web Apps).

azd init                                               # creates azure.yaml + infra
azd up                                                 # provision + deploy
azd deploy                                             # code only
azd provision                                          # infra only
azd down                                               # tear down

azure.yaml:

name: myapp
services:
  api:
    project: ./src/MyApp.Api
    language: dotnet
    host: containerapp

azd templates

azd template list
azd init --template Azure-Samples/todo-csharp-cosmos-sql

Pre-built template apps with infra + code. Good starting points.

azd + Aspire

azd init                # detects AppHost
azd up                  # provisions ACA + dependencies; deploys

AppHost topology → ACA resources. Zero infra hand-coding.

Common az for .NET dev

# App Service deploy (zip)
az webapp deploy --resource-group rg --name myapp --src-path ./bin/Release/publish.zip

# Functions
func azure functionapp publish myfunc

# Container build
az acr build -r myacr -t myapp:1.0 .

# AKS
az aks command invoke -g rg -n cluster --command "kubectl get pods"

Az PowerShell vs Az CLI

az (CLI) is cross-platform Python. Az PowerShell modules for PS-friendly. Same Azure API. Most prefer az.

Bash completion

source <(az completion bash)

Tab-complete subcommands, args, resource names.

Defaults

az configure --defaults group=myrg location=eastus
# subsequent commands omit -g

Profiles

az cloud set --name AzureUSGovernment    # for Gov cloud
az cloud set --name AzureChinaCloud

Managing multiple subscriptions

az account list -o table
az account set --subscription <id|name>

CI/CD: ensure subscription is set in scripts.

azd configurable hooks

hooks:
  postdeploy:
    shell: sh
    run: ./scripts/seed-data.sh

Lifecycle hooks for custom logic.

Azure Cloud Shell

Browser-based bash/PS with az/azd preinstalled. Useful when no local CLI.

Service principal vs federated for CI

For GitHub Actions / Azure DevOps, federated identity credential preferred — no secrets in CI.

# GitHub Actions
- uses: azure/login@v2
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

Tooling recap

Local dev:                az login + az
Quick app deploy:         azd up
CI/CD:                    GitHub Actions / Azure DevOps with FIC
Infra-as-code:            Bicep / Terraform

Code: correct vs wrong

❌ Wrong: client secret in CI

- run: az login --service-principal -u $appId -p $secret --tenant $tid

✅ Correct: federated identity

- uses: azure/login@v2
  with:
    client-id: ...
    tenant-id: ...
    subscription-id: ...

❌ Wrong: az resource update without query

# Modifies all resources of a type

✅ Correct: scoped

az resource update --ids $specificId --set property=value

Design patterns for this topic

Pattern 1 — "az for ad-hoc; azd for app workflows"

  • Intent: right tool per layer.

Pattern 2 — "Federated identity in CI"

  • Intent: secret-free CI.

Pattern 3 — "JMESPath for filtering"

  • Intent: scriptable Azure ops.

Pattern 4 — "azd up for Aspire deploys"

  • Intent: AppHost → ACA in one command.

Pattern 5 — "Cloud Shell for emergencies"

  • Intent: when local CLI absent.

Pros & cons / trade-offs

Tool Pros Cons
az Universal Verbose for app workflows
azd App-centric; templates Newer; less universal
Az PowerShell PS-friendly Different muscle memory
Cloud Shell No install Slower than local

When to use / when to avoid

  • Use az for ad-hoc ops.
  • Use azd up for app provisioning + deploy.
  • Avoid service-principal secrets in CI — use FIC.

Interview Q&A

Q1. az vs azd? az: low-level Azure CLI. azd: high-level dev workflow with templates and one-shot up/deploy.

Q2. Login methods? Browser, device code, service principal (secret/cert), federated, managed identity.

Q3. Federated vs service-principal-with-secret? Federated: external IdP issues token; no secret. Preferred for CI.

Q4. JMESPath query? --query filters/projects from JSON output. Avoids piping to jq.

Q5. azd up? Provisions infra + deploys app. AppHost-aware (Aspire).

Q6. Multi-sub scripts? az account set --subscription <id> first; explicit per script.

Q7. Managed identity in scripts? az login --identity on resources with MI.

Q8. Cloud Shell use case? Browser-based when no local CLI.

Q9. Az PowerShell? PS modules for Azure. Same API; PS idioms.

Q10. azd templates? Pre-built app + infra. Good starting points.

Q11. Default group? az configure --defaults group=....

Q12. Az cloud profiles? For Gov / China cloud — different endpoints.


Gotchas / common mistakes

  • ⚠️ No subscription set in script — wrong sub.
  • ⚠️ SP secrets in CI — use FIC.
  • ⚠️ --query complexity — practice JMESPath.
  • ⚠️ az PS vs Az CLI confusion.

Further reading