You close the module with the tool that will save you the most time during the rest of the course and the rest of your career in Azure: the command line. In the previous lesson it became clear that the portal, the CLI, PowerShell and templates all talk to the same Azure Resource Manager API. What changes is speed, precision and — above all — the ability to repeat exactly the same thing tomorrow.
Here you will learn to install and use Azure CLI, to query its output with --query, to work across several subscriptions without getting the environment wrong, when Azure PowerShell makes more sense, and how Cloud Shell works (and why it creates a storage account for you). And you will finish by writing a complete, commented, parameterized and idempotent script that deploys the base of Contoso Airlines' platform and knows how to delete it afterwards so you do not overspend.
Contents
- Why automate from the command line
- Installing Azure CLI and checking the version
- Signing in and working with several subscriptions
- Anatomy of an
azcommand and built-in help - Output formats and queries with
--query - Azure PowerShell and when to choose each tool
- Cloud Shell
- Complete script: the Contoso Airlines base
- Cleanup: deleting without leaving a trace of spend
- Common Mistakes and Tips
- Exercises
- Conclusion
- Why automate from the command line
The portal is excellent for exploring. For working, it has three problems:
| Problem with the portal | What the command line brings |
|---|---|
| It is not repeatable: 40 clicks today, 40 different clicks tomorrow | A script always produces the same result |
| It is not auditable: nobody knows which options you ticked | The script lives in Git, is reviewed and is diffed |
| It does not scale: creating 20 identical resources is a wasted afternoon | A for loop does it in a minute |
It is slow for queries: "give me every VM without a propietario tag" has no button |
A --query expression solves it in one line |
On top of that, the CLI is the natural bridge to what comes later: the deployment pipelines in module 5 run az commands, and the diagnostics in module 7 lean on command-line queries.
The golden rule, deliberately repeated: what you do once, do it in the portal; what you will do twice, write it down.
- Installing Azure CLI and checking the version
Azure CLI is cross-platform and written in Python, but it installs as a native package on each system.
Windows
# Recommended option: the Windows package manager.
winget install --exact --id Microsoft.AzureCLI
# Alternative with a silent MSI, useful in corporate rollouts.
# Download the official installer and run it with /quiet.After installing, close and reopen the terminal so that the PATH variable is refreshed.
macOS
Linux (Debian/Ubuntu)
# Official installation script: it adds the Microsoft repository and installs the package.
# Always review a script before running it with privileges (good general practice).
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bashFor RHEL, Fedora or CentOS you use dnf; for openSUSE, zypper. The official documentation covers each case.
Checking the installation
Sample output:
{
"azure-cli": "2.64.0",
"azure-cli-core": "2.64.0",
"azure-cli-telemetry": "1.1.0",
"extensions": {}
}Keeping it up to date
# Upgrades the CLI to the latest stable version (works on most platforms).
az upgrade
# Turns off the telemetry prompt and interactive confirmations in scripts.
az config set core.collect_telemetry=falseAzure CLI is updated every few weeks. Working with a very old version causes "unrecognized arguments" errors when you follow recent documentation: if a parameter does not exist in your installation, the first thing to try is upgrading.
- Signing in and working with several subscriptions
# Opens the browser to authenticate interactively.
# After signing in, the CLI stores a local token and stops asking on every command.
az loginUseful variants:
# Without a browser (servers, SSH): it shows a code to enter at microsoft.com/devicelogin.
az login --use-device-code
# Specify the tenant when your identity belongs to several directories.
az login --tenant contoso-airlines.onmicrosoft.comFor unattended automation (CI/CD pipelines) you do not use interactive
az login, but a service principal or a managed identity. They are covered in lessons 04-02 and 05-03. Never put user credentials in a script.
Several subscriptions: the most expensive mistake of all
If your identity has access to Contoso Airlines - Producción and to Contoso Airlines - Desarrollo, every az command runs against the active subscription. Getting this wrong is how things get deleted in production.
Name CloudName SubscriptionId State IsDefault -------------------------------- ----------- ------------------------------------ ------- --------- Contoso Airlines - Producción AzureCloud 8f4c2b7a-1d3e-4a55-9c11-0a7b6e2d4f90 Enabled True Contoso Airlines - Desarrollo AzureCloud 3b91e5d0-7c42-4f88-a2e6-5d9c1a8b3e77 Enabled False
# Change the active subscription (it accepts a name or an identifier).
az account set --subscription "Contoso Airlines - Desarrollo"
# Confirm where you are before touching anything. Make it a habit.
az account show --query "{Subscription:name, Id:id, User:user.name}" --output tableTwo practical defenses:
- Start all your scripts by setting the subscription explicitly with
az account set. Do not trust whichever one happens to be active. - Use the
--subscriptionparameter on critical commands, so you do not depend on global state:
az group delete --name rg-contoso-reservas-dev \
--subscription "Contoso Airlines - Desarrollo" --yes
- Anatomy of an
az command and built-in help
az command and built-in helpEvery Azure CLI command follows the same structure:
A worked example:
az storage account create --name sttarjetascontosodev --resource-group rg-contoso-reservas-dev --location westeurope --sku Standard_LRS| Part | Value | What it is |
|---|---|---|
az |
— | The program |
storage |
group | The service family |
account |
subgroup | The specific object within that family |
create |
command | The action (create, list, show, update, delete) |
--name, --resource-group… |
parameters | The data for the operation |
The verbs repeat throughout the CLI, and that makes it predictable: if you know az vm list, you can guess az storage account list, az webapp list and az sql db list.
Common abbreviations: -n for --name, -g for --resource-group, -l for --location, -o for --output.
Built-in help
# Help for a group: shows the available subgroups and commands.
az storage --help
# Help for a specific command: lists ALL its parameters, which ones are required, and examples.
az storage account create --help
# Smart natural-language search, with real usage examples.
az find "az storage account"
az find "create a storage account"And interactive mode, very useful while you are learning (it requires installing the extension):
- Output formats and queries with
--query
--queryBy default, the CLI returns JSON, which is complete but awkward to read. The --output (or -o) parameter changes the format:
| Value | Result | When to use it |
|---|---|---|
json (default) |
Formatted JSON | Seeing every available field |
jsonc |
Colorized JSON | Reading in a terminal |
table |
A readable table | Daily use and quick reports |
tsv |
Tab-separated values, with no headers or quotes | Scripts: feeding variables |
yaml |
YAML | Readable configurations |
none |
Nothing | When all that matters is that the operation worked |
JMESPath: the language of --query
--query uses JMESPath, a query language for JSON. Let us work through progressive examples over Contoso's resources.
Step 1 — select a property of an object:
# Returns only a resource group's location, without quotes (tsv), ready for a variable.
az group show --name rg-contoso-reservas-dev --query location --output tsvStep 2 — walk a list and keep one field:
Step 3 — build an object with your own names:
# Renames the output fields. The keys on the left are labels of your own.
az group list \
--query "[].{Name:name, Region:location, State:properties.provisioningState}" \
--output tableName Region State ------------------------- ---------- --------- rg-contoso-reservas-dev westeurope Succeeded rg-contoso-reservas-pro westeurope Succeeded
Step 4 — filter with conditions (?):
# Only the groups tagged as production.
az group list \
--query "[?tags.entorno=='produccion'].{Name:name, Owner:tags.propietario}" \
--output table
# Only the storage accounts in West Europe.
az storage account list \
--query "[?location=='westeurope'].{Account:name, Group:resourceGroup, Sku:sku.name}" \
--output tableStep 5 — spot what is missing (governance audit):
# Resources WITHOUT the mandatory "propietario" tag: the report Marta Ríos asks for.
# == `null` compares against null; the backticks mark a JSON literal.
az resource list \
--query "[?tags.propietario == \`null\`].{Resource:name, Type:type, Group:resourceGroup}" \
--output tableStep 6 — combine with functions and sorting:
# Count how many resources there are in total.
az resource list --query "length(@)" --output tsv
# The resources sorted by name, showing only type and group.
az resource list \
--query "sort_by([].{Name:name, Type:type, Group:resourceGroup}, &Name)" \
--output tableStep 7 — use the result in a script:
# Store a value in a shell variable. --output tsv avoids the JSON quotes.
GROUP_REGION=$(az group show --name rg-contoso-reservas-dev --query location --output tsv)
echo "The group is in: $GROUP_REGION"Learning trick: run the command without
--queryand with--output jsonfirst to see the full structure, and only then write the query. It is far quicker than guessing field names.
- Azure PowerShell and when to choose each tool
Azure PowerShell is the other official client, based on the Az module. It does the same as the CLI, but with the PowerShell philosophy: instead of text, it returns objects with properties that you can operate on directly.
Installation and first commands
# Installs the Az module for the current user (no administrator permissions required).
Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force
# Sign in (opens the browser).
Connect-AzAccount
# See the available subscriptions and select one.
Get-AzSubscription
Set-AzContext -Subscription "Contoso Airlines - Desarrollo"
# Create Contoso's resource group with its tags.
New-AzResourceGroup -Name "rg-contoso-reservas-dev" -Location "westeurope" -Tag @{
entorno = "desarrollo"
proyecto = "contoso-reservas"
"centro-coste" = "CC-1042"
propietario = "[email protected]"
}
# List resources and filter by tag, taking advantage of PowerShell objects.
Get-AzResource -TagName "proyecto" -TagValue "contoso-reservas" |
Select-Object Name, ResourceType, ResourceGroupName |
Format-TableNote the difference in philosophy: in the CLI you filter with --query (JMESPath over JSON); in PowerShell you filter with Where-Object and select with Select-Object, because you already have objects.
Comparison
| Aspect | Azure CLI (az) |
Azure PowerShell (Az) |
|---|---|---|
| Syntax | az group subgroup command |
Verb-AzNoun (New-AzResourceGroup) |
| Output | JSON (text) | .NET objects |
| Filtering | --query with JMESPath |
Where-Object, Select-Object |
| Native shell | Bash, Zsh; works in any shell | PowerShell (Windows, Linux and macOS) |
| Entry curve | Gentler if you come from Linux | Gentler if you come from Windows |
| Integration | Excellent in cross-platform pipelines and containers | Excellent with Windows Server, Microsoft 365 and Exchange |
| Microsoft documentation | Examples in both | Examples in both |
How to choose, without dogma:
- If your team is made up of Windows systems administrators who already automate with PowerShell, use PowerShell.
- If your team comes from Linux, Docker or cross-platform development, use the CLI.
- If you write scripts for CI/CD pipelines in Linux containers, the CLI is usually lighter and more direct.
- What matters is being consistent within a single project. At Contoso, Diego and Marta agree to use Azure CLI, and that is the course's choice from here on.
- Cloud Shell
Azure Cloud Shell is a terminal hosted in Azure, reachable from the >_ icon in the portal's top bar (or at shell.azure.com). Immediate advantages:
- It is already authenticated with your identity: there is no need to run
az login. - It comes with the tools installed and up to date: Azure CLI, Azure PowerShell, Bicep, Terraform, kubectl, Git, Python, Node.js, and editors such as
vim,nanoand the built-in graphical editor (code .). - It works from any browser, including a phone.
Bash or PowerShell
You can choose the interpreter in the dropdown at the top and switch at any time. The az commands work in both; PowerShell's Az* cmdlets, only in PowerShell mode.
The storage account it creates
The first time you open Cloud Shell, it asks you to create (or attach) a storage account. This is not a whim:
- Cloud Shell runs in an ephemeral container: when the session ends, the container is destroyed.
- So that your files survive, it mounts an Azure Files share on your
$HOMEdirectory, inside a disk image usually calledacc_<user>.img. - What you save in
$HOME(~/clouddrive) persists between sessions; what you install outside it does not.
Cost warning: that storage account is a resource of yours and it is billed, even though the amount is very small (a few cents a month for a few GB). Cloud Shell itself costs nothing; its storage does. If you stop using it, you can delete the resource group that gets created automatically (it is usually called
cloud-shell-storage-<region>).
Other features and limits
| Aspect | Detail |
|---|---|
| Idle timeout | The session closes after about 20 minutes without interaction |
| Persistence | Only $HOME / clouddrive; the rest of the file system is lost |
| Uploading and downloading files | The Upload/Download button in the Cloud Shell bar |
| Built-in editor | code . opens a graphical editor inside the browser |
| Simultaneous sessions | Limited; one Bash session and one PowerShell session at a time |
| Storage region | Chosen when you create it; it is worth putting it close to you |
When to use Cloud Shell: for learning (you install nothing), for one-off tasks from somebody else's computer, for emergencies from a phone. When not to: for intensive daily work or long scripts, where a local CLI with your editor and your Git repository is more comfortable.
- Complete script: the Contoso Airlines base
Now for the module's central exercise: a script that creates the whole base of Contoso Airlines' platform. It is parameterized (everything in variables at the top), idempotent (it can be run several times without breaking anything or duplicating resources) and commented line by line.
Save it as desplegar-base-contoso.sh.
#!/usr/bin/env bash
# =============================================================================
# desplegar-base-contoso.sh
# Creates the base of the Contoso Airlines platform:
# - Resource group with the mandatory tags
# - Storage account for the boarding passes in PDF
# - Private container "tarjetas-embarque"
#
# It is IDEMPOTENT: running it twice leaves the same result as running it once.
# Usage: ./desplegar-base-contoso.sh [dev|pro]
# =============================================================================
# 'set -e' aborts the script on the first error instead of carrying on blindly.
# 'set -u' aborts if an undefined variable is used (it catches dangerous typos).
# 'set -o pipefail' propagates the error of any command inside a pipeline.
set -euo pipefail
# ------------------------------------------------------------------
# 1. PARAMETERS. Everything configurable, in one place and at the top.
# ------------------------------------------------------------------
ENVIRONMENT="${1:-dev}" # First argument; "dev" if not given
SUBSCRIPTION="Contoso Airlines - Desarrollo"
REGION="westeurope" # Primary region decided in lesson 01-02
PROJECT="contoso-reservas"
COST_CENTER="CC-1042"
OWNER="[email protected]"
# Derived names, following Contoso's naming convention.
GROUP="rg-${PROJECT}-${ENVIRONMENT}" # e.g. rg-contoso-reservas-dev
# Storage accounts only accept lowercase letters and numbers (3-24 characters)
# and their name is UNIQUE ACROSS THE WHOLE OF AZURE: we add a stable random suffix just in case.
SUFFIX="$(echo -n "${GROUP}" | cksum | cut -c1-4)"
STORAGE_ACCOUNT="sttarjetascontoso${ENVIRONMENT}${SUFFIX}"
CONTAINER="tarjetas-embarque"
# Translation of the short environment into the tag value Contoso's scheme requires.
if [ "${ENVIRONMENT}" = "pro" ]; then
ENV_TAG="produccion"
else
ENV_TAG="desarrollo"
fi
echo "=== Deploying the Contoso Airlines base (${ENV_TAG}) ==="
# ------------------------------------------------------------------
# 2. CONTEXT. Never trust whichever subscription happened to be active.
# ------------------------------------------------------------------
az account set --subscription "${SUBSCRIPTION}"
echo "Active subscription: $(az account show --query name --output tsv)"
# ------------------------------------------------------------------
# 3. PROVIDERS. Needed in new subscriptions; repeating it is harmless.
# ------------------------------------------------------------------
az provider register --namespace Microsoft.Storage --wait
# ------------------------------------------------------------------
# 4. RESOURCE GROUP.
# 'az group create' is already idempotent: if it exists, it updates its tags.
# ------------------------------------------------------------------
echo "--> Resource group: ${GROUP}"
az group create \
--name "${GROUP}" \
--location "${REGION}" \
--tags entorno="${ENV_TAG}" \
proyecto="${PROJECT}" \
centro-coste="${COST_CENTER}" \
propietario="${OWNER}" \
--output none
# ------------------------------------------------------------------
# 5. STORAGE ACCOUNT.
# We check first whether it exists, so that the script is idempotent
# and so that it does not fail if the global name is already taken by somebody else.
# ------------------------------------------------------------------
echo "--> Storage account: ${STORAGE_ACCOUNT}"
if az storage account show --name "${STORAGE_ACCOUNT}" --resource-group "${GROUP}" --output none 2>/dev/null; then
echo " Already exists: it is not created again."
else
# --sku Standard_LRS : the cheapest option (local redundancy). Module 2.
# --kind StorageV2 : the current account type, supporting blobs, queues, tables and files.
# --min-tls-version : enforces TLS 1.2 as a minimum.
# --allow-blob-public-access false : NOBODY can read the PDFs anonymously.
# --https-only true : rejects unencrypted traffic.
az storage account create \
--name "${STORAGE_ACCOUNT}" \
--resource-group "${GROUP}" \
--location "${REGION}" \
--sku Standard_LRS \
--kind StorageV2 \
--min-tls-version TLS1_2 \
--https-only true \
--allow-blob-public-access false \
--tags entorno="${ENV_TAG}" \
proyecto="${PROJECT}" \
centro-coste="${COST_CENTER}" \
propietario="${OWNER}" \
--output none
echo " Created."
fi
# ------------------------------------------------------------------
# 6. BLOB CONTAINER.
# --auth-mode login uses YOUR Entra ID identity instead of the account
# key: it is the recommended approach and it exposes no secrets in the script.
# (It requires a data role, e.g. "Storage Blob Data Contributor".)
# ------------------------------------------------------------------
echo "--> Container: ${CONTAINER}"
az storage container create \
--name "${CONTAINER}" \
--account-name "${STORAGE_ACCOUNT}" \
--auth-mode login \
--public-access off \
--output none
# ------------------------------------------------------------------
# 7. SUMMARY. A final readable check of what was deployed.
# ------------------------------------------------------------------
echo ""
echo "=== Deployment complete ==="
az resource list \
--resource-group "${GROUP}" \
--query "[].{Resource:name, Type:type, Region:location}" \
--output table
echo ""
echo "To delete EVERYTHING created and stop paying, run:"
echo " az group delete --name ${GROUP} --yes --no-wait"How to run it
# Grant execute permissions (only the first time).
chmod +x desplegar-base-contoso.sh
# Deploy the development environment.
./desplegar-base-contoso.sh devWhat makes this script idempotent
| Technique used | Why it matters |
|---|---|
az group create on an existing group |
It does not fail: it updates its tags |
A prior check with az storage account show |
It avoids the "name already in use" error on a re-run |
| Names derived from variables, not hard-coded | The same script serves both dev and pro |
set -euo pipefail |
It stops the script on the first failure instead of leaving a half-finished deployment |
--output none |
Clean output; only what we decide gets printed |
If you look closely, this script does something very similar to an infrastructure-as-code template, but imperatively (step by step). The next level is doing it declaratively with Bicep, and that is exactly lesson 05-06.
- Cleanup: deleting without leaving a trace of spend
This section is mandatory, not optional. The cost of what you have created in this module is a matter of cents, but the habit is worth real money once you are working with virtual machines and databases.
# Deletes the group and ABSOLUTELY EVERYTHING it contains. It is irreversible.
# --yes : does not ask for confirmation (use it only when you are sure).
# --no-wait : returns control immediately; the deletion continues in the background.
az group delete --name rg-contoso-reservas-dev --yes --no-waitChecks before and after:
# Before: see exactly what is going to be destroyed.
az resource list --resource-group rg-contoso-reservas-dev --output table
# After: confirm that the group no longer exists (it returns "false").
az group exists --name rg-contoso-reservas-devAnd a hygiene check worth running periodically across the whole subscription:
# Lists ALL resource groups with their tags: hunting for orphans and forgotten items.
az group list --query "[].{Group:name, Region:location, Environment:tags.entorno, Owner:tags.propietario}" --output table
# Look for disks that are no longer attached to any virtual machine: pure spend.
az disk list --query "[?diskState=='Unattached'].{Disk:name, Group:resourceGroup, GB:diskSizeGb}" --output table
# Look for unassociated public IP addresses: they are billed too.
az network public-ip list --query "[?ipConfiguration==null].{IP:name, Group:resourceGroup}" --output tableThose last three commands are, literally, three of the most common sources of wasted spend in real Azure accounts. Keep them.
Remember as well: if you created a
CanNotDeletelock in the previous lesson, the deletion will fail until you remove it withaz lock delete. It is designed to work that way.
Common Mistakes and Tips
- Running commands in the wrong subscription. Always start with
az account setand confirm withaz account show. It is the mistake that does the most damage. - Working with an out-of-date CLI. If a parameter "does not exist", run
az upgradebefore searching the forums. - Using uppercase letters or hyphens in the storage account name. Lowercase letters and numbers only, 3-24 characters, and unique across the whole of Azure.
- Using
--output jsoninside scripts. To feed variables use--output tsv: it avoids quotes and unexpected line breaks. - Writing access keys or passwords inside the script. Use
--auth-mode login, managed identities (lesson 04-02) or Key Vault (lesson 04-03). - Forgetting that Cloud Shell closes after 20 minutes of inactivity and that only
$HOMEpersists. Save your work in~/clouddriveor, better still, in a Git repository. - Forgetting that Cloud Shell storage is billed. It is little money, but if you stop using it, delete it.
- Writing
--queryexpressions blind. Run the command in JSON first, look at the structure and then query. - Deleting with
--yeswithout looking first. Runaz resource list --resource-group ...to see what will disappear. - Final cost tip: get into the habit of ending every practice session with
az group delete. One resource group per exercise, deleted at the end: it is the most profitable discipline you can pick up in this course.
Exercises
Exercise 1: Governance queries
Write the Azure CLI commands that fulfill these requests from Marta Ríos:
- Show in a table the name and region of every resource group in the active subscription.
- List every resource tagged with
proyecto=contoso-reservas, showing name, type and group. - Detect the resources that do not have the
centro-costetag. - Store in a shell variable called
PROD_REGIONthe region of therg-contoso-reservas-progroup.
Exercise 2: Adapting the script
Starting from the desplegar-base-contoso.sh script in section 8:
- Add a
CRITICALITYvariable and apply it as a tag on both the group and the storage account, with the valuealtaif the environment isproandbajaotherwise. - Add a step that, only if the environment is
pro, applies aCanNotDeletelock to the group. - Explain why step 3 of the script (
az provider register) is safe even though it runs every time.
Exercise 3: Comparing tools and cleaning up
- Write the Azure PowerShell equivalent of these two CLI commands:
az group create --name rg-contoso-millas-dev --location westeurope --tags entorno=desarrollo proyecto=contoso-millas
az group delete --name rg-contoso-millas-dev --yes- Justify in three lines which tool you would choose for a CI/CD pipeline running in a Linux container, and why.
- Write the commands that audit a subscription looking for unattached disks and unassociated public IP addresses, and explain why those two resources are classic sources of wasted spend.
Solutions
Solution 1:
# 1. Resource groups with name and region.
az group list --query "[].{Group:name, Region:location}" --output table
# 2. Resources belonging to the project.
az resource list --tag proyecto=contoso-reservas \
--query "[].{Resource:name, Type:type, Group:resourceGroup}" --output table
# 3. Resources WITHOUT the centro-coste tag.
az resource list \
--query "[?tags.\"centro-coste\" == \`null\`].{Resource:name, Type:type, Group:resourceGroup}" \
--output table
# 4. The production group's region in a variable.
PROD_REGION=$(az group show --name rg-contoso-reservas-pro --query location --output tsv)
echo "$PROD_REGION"A note on point 3: because the tag name contains a hyphen, it has to be quoted inside the JMESPath expression.
Solution 2:
- Criticality variable and tag:
# Alongside the rest of the parameters:
if [ "${ENVIRONMENT}" = "pro" ]; then
CRITICALITY="alta"
else
CRITICALITY="baja"
fi
# And it gets added to the --tags block of az group create and az storage account create:
# criticidad="${CRITICALITY}"- Conditional lock in production:
if [ "${ENVIRONMENT}" = "pro" ]; then
echo "--> Applying a CanNotDelete lock to the production group"
# 'az lock create' fails if a lock with that name already exists;
# we check first to keep the script idempotent.
if ! az lock show --name "no-borrar-produccion" --resource-group "${GROUP}" --output none 2>/dev/null; then
az lock create \
--name "no-borrar-produccion" \
--lock-type CanNotDelete \
--resource-group "${GROUP}" \
--notes "Sales platform in production. Remove only with authorization." \
--output none
fi
fiaz provider registeris idempotent: if the provider is already registered, the operation changes nothing and completes successfully. Registering it costs no money and has no side effects, so including it guarantees that the script also works in a freshly created subscription.
Solution 3:
- Azure PowerShell equivalents:
New-AzResourceGroup -Name "rg-contoso-millas-dev" -Location "westeurope" -Tag @{
entorno = "desarrollo"
proyecto = "contoso-millas"
}
Remove-AzResourceGroup -Name "rg-contoso-millas-dev" -Force-
For a pipeline in a Linux container I would choose Azure CLI: the image is lighter than installing PowerShell's
Azmodule, the syntax fits naturally with Bash, most pipeline tasks are one-liners, and practically every Azure DevOps and GitHub Actions pipeline example is written withaz. -
Wasted-spend audit:
az disk list --query "[?diskState=='Unattached'].{Disk:name, Group:resourceGroup, GB:diskSizeGb}" --output table
az network public-ip list --query "[?ipConfiguration==null].{IP:name, Group:resourceGroup}" --output tableThey are classic sources of spend because they are billed for existing, not for being used. When you delete a virtual machine, its managed disks and its public IP are not removed automatically unless the corresponding option was ticked: they are left orphaned, invisible in day-to-day work and charging every month. A monthly review with these two commands is usually the most profitable and quickest optimization to apply.
Conclusion
With this lesson you close module 1 and your toolbox is complete. You know why to automate (repeatable, auditable, scalable), you have installed Azure CLI and checked its version, and you know how to sign in and — very importantly — how to switch and confirm the active subscription before touching anything. You know the anatomy of an az command and its built-in help (--help, az find), and you can use --output table to read and --query with JMESPath to filter, rename fields, spot untagged resources and feed variables in scripts. You know what Azure PowerShell brings and on what basis to choose between the two, and how Cloud Shell works: its two interpreters, the storage account it creates and bills for, persistence limited to $HOME and the idle timeout. And you have written a real, parameterized, commented and idempotent script that creates the rg-contoso-reservas-dev group, the boarding pass storage account and its private container, with Contoso Airlines' mandatory tags, plus the cleanup routine that prevents surprise bills.
Recapping the whole module: you have understood what the cloud is and what Azure is and met the Contoso Airlines case; you have distinguished IaaS, PaaS, SaaS and serverless, the shared responsibility model and Azure's geography, settling on West Europe as the primary region and North Europe as its pair; you have created and protected your account with MFA and a budget with alerts; you can find your way confidently around the portal; you have a command of Azure Resource Manager, the subscription, resource group and resource hierarchy, the locks and Contoso's tagging scheme; and now you automate from the command line.
You have the foundations: the account, the governance, the naming, the resource groups and the tool to deploy with. What is missing is the platform itself. In module 2, Core Azure Services, we start building it for real: we will deploy the virtual machines for the legacy availability engine, we will learn to scale and provide high availability for compute, we will publish Contoso Bookings and the Availability API on App Service, we will store boarding passes in Azure Storage with the right redundancy, and we will build the virtual network with its subnets and security groups, along with hybrid connectivity to the Barcelona and Palma offices. See you there.
Azure Course
Module 1: Introduction to Azure
- What Is Azure?
- Service Models, Regions and Availability Zones
- Creating and Setting Up Your Azure Account
- A Tour of the Azure Portal
- Azure Resource Manager: Subscriptions, Resource Groups and Tags
- Azure CLI, PowerShell and Cloud Shell
Module 2: Core Azure Services
- Azure Virtual Machines
- Compute Scaling and High Availability
- Azure App Service
- Azure Storage: Blobs, Files, Queues and Tables
- Azure Networking: Virtual Networks, Subnets and NSGs
- Hybrid Connectivity and Global Delivery
Module 3: Azure Databases
- Choosing the Right Data Service
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Data Analytics: Data Lake, Data Factory and Synapse
Module 4: Security in Azure
- Microsoft Entra ID and Identity Management
- RBAC and Managed Identities
- Azure Key Vault
- DDoS Protection and Web Application Firewall
- Microsoft Defender for Cloud
- Governance and Compliance with Azure Policy
Module 5: Azure DevOps
- Introduction to Azure DevOps
- Azure Repos
- Azure Pipelines: Continuous Integration
- Continuous Deployment with Environments and Approvals
- Azure Artifacts
- Infrastructure as Code with Bicep
Module 6: Advanced Azure Services
- Containers in Azure: Container Registry and Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Messaging and Events: Service Bus, Event Grid and Event Hubs
- Azure AI Services
Module 7: Monitoring and Management
- Azure Monitor: Metrics, Alerts and Dashboards
- Log Analytics and KQL Queries
- Application Insights
- Azure Automation and Runbooks
- Backup and Disaster Recovery
Module 8: Cost Management and Optimization
- Pricing Calculator and Cost Estimation
- Azure Cost Management: Analysis, Budgets and Alerts
- Reservations, Savings Plans and Azure Hybrid Benefit
- Azure Advisor
- Optimization Strategies and FinOps Culture
