Intermediate

Azure Automation with CLI

Azure CLI commands, secrets, Automation runbooks and Logic Apps for hands-off operations.

Level: Beginner / Intermediate
Estimated duration: 3–4 hours
Last updated: June 2026

Table of Contents

  1. Introduction to Azure Automation
  2. Module 1 – Azure CLI Basic Commands
  3. Module 2 – Working with Azure Secrets
  4. Module 3 – Automating Automation
  5. Module 4 – Azure Automation Runbooks
  6. Module 5 – Logic Apps
  7. Advanced Automation Scenarios
  8. Comparison Tables
  9. Best Practices
  10. Glossary

Introduction to Azure Automation

Why Automate?

Automation is the fundamental pillar of any modern cloud environment. In Azure, manual management through the portal is acceptable for learning and occasional testing, but quickly becomes an operational bottleneck when:

  • You need to deploy the same resources across multiple environments (dev, staging, prod)
  • You need to create dozens or hundreds of users or resources
  • You want to guarantee reproducibility and consistency of deployments
  • You want to integrate Azure operations into your CI/CD pipelines
  • You need to perform scheduled maintenance tasks (archiving, cleanup, reporting)

Automation with Azure CLI is the gateway to DevOps and Infrastructure as Code. Mastering these tools lets you move from a reactive administrator role to a proactive cloud engineer.

Overview of Azure Automation Tools

mindmap
  root((Azure Automation))
    Azure CLI
      Bash/Batch Scripts
      Python Scripts
      CI/CD Pipelines
    Azure PowerShell
      PS1 Scripts
      Azure Cloud Shell
    Azure REST API
      Direct HTTP Calls
      .NET/Python/Java SDK
    ARM Templates
      Declarative JSON
      Infrastructure as Code
    Bicep
      Readable DSL
      Transpiles to ARM
    Terraform
      Multi-cloud
      Declarative HCL
    Azure Automation
      Python/PS Runbooks
      Schedules
      DSC
    Logic Apps
      Low/No-code
      Visual Connectors
    Azure Functions
      Event-driven
      Serverless Code

Azure Resource Hierarchy

graph TD
    TENANT[Azure Tenant\nMicrosoft Entra ID]
    MG[Management Groups]
    SUB[Subscriptions]
    RG[Resource Groups]
    RES[Resources\nVM, Storage, AKS...]
    
    TENANT --> MG
    MG --> SUB
    SUB --> RG
    RG --> RES
    
    style TENANT fill:#1e40af,color:#fff
    style MG fill:#3b82f6,color:#fff
    style SUB fill:#0891b2,color:#fff
    style RG fill:#0d9488,color:#fff
    style RES fill:#059669,color:#fff

Core philosophy: Secrets NEVER belong in version control. Never hardcode passwords, tokens or connection strings in scripts. Always use external references (Key Vault, CI environment variables, Managed Identity).


Module 1 – Azure CLI Basic Commands

Why Azure CLI?

Azure CLI is much more than a simple command-line tool. It is a complete wrapper around the Azure REST API that handles:

  • Authentication: sessions, tokens, automatic refresh
  • Connectivity: proxy, TLS, network error handling
  • Serialization: automatic conversion of JSON responses
  • Cross-platform: identical on Windows, macOS and Linux

It is available on many platforms:

  • Windows: via WinGet, MSI installer
  • macOS: via Homebrew
  • Linux: via apt, yum, dnf or curl script
  • Azure Cloud Shell: pre-installed, browser only
  • Docker: image mcr.microsoft.com/azure-cli

Installation and First Commands

# ===== INSTALLATION =====

# Windows via WinGet (recommended)
winget install Microsoft.AzureCLI

# macOS via Homebrew
brew update && brew install azure-cli

# Ubuntu/Debian via script
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# Fedora/RHEL via rpm
sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
sudo dnf install azure-cli

# Via Docker
docker run -it mcr.microsoft.com/azure-cli

# ===== VERIFICATION =====
az --version
# azure-cli                         2.73.0
# core                              2.73.0
# telemetry                          1.1.0

# ===== UPDATE =====
az upgrade

# ===== LOGIN =====

# Interactive login (RECOMMENDED for humans - opens the browser)
az login

# Login with a specific tenant
az login --tenant mycompany.onmicrosoft.com

# Login with device code (environments without a browser)
az login --use-device-code

# Login with Service Principal (for CI/CD)
az login \
    --service-principal \
    --username "app-id" \
    --password "client-secret" \
    --tenant "tenant-id"
# OR with certificate (more secure)
az login \
    --service-principal \
    --username "app-id" \
    --certificate "/path/to/certificate.pem" \
    --tenant "tenant-id"

# Login with Managed Identity (for VMs/App Services/etc. in Azure)
az login --identity
az login --identity --username "user-assigned-identity-client-id"

# ===== SUBSCRIPTION MANAGEMENT =====

# List all accessible subscriptions
az account list --output table

# Set a default subscription
az account set --subscription "My-Dev-Subscription"
# OR by ID
az account set --subscription "12345678-1234-1234-1234-123456789012"

# Check the current subscription
az account show
az account show --query "{Name:name, ID:id, Tenant:tenantId}" --output table

Azure CLI Command Structure

The structure of an Azure CLI command always follows the same pattern:

az <group> <sub-group>... <action> [parameters] [global-params]

# Examples of groups / sub-groups
az account show                          # Current account
az group create                          # Resource Groups
az vm create                             # Virtual Machines
az storage account create               # Storage Accounts
az aks create                            # AKS Clusters
az network vnet create                   # Virtual Networks
az keyvault secret set                  # Key Vault Secrets
az webapp identity assign               # Web App Identity
az ad user create                        # Entra ID Users
az ad sp create-for-rbac                # Service Principals
az role assignment create               # Role Assignments

Output Formats and JMESPath Queries

Azure CLI supports multiple output formats and a powerful JMESPath query language for filtering JSON responses.

# Available output formats
az group list --output json      # Full JSON (default)
az group list --output table     # Readable table
az group list --output yaml      # YAML
az group list --output tsv       # Tab-separated (ideal for scripts)
az group list --output jsonc     # Colored JSON

# JMESPath queries (--query)
# Extract the subscription name
az account show --query name --output tsv

# List only resource group names
az group list --query "[].name" --output tsv

# Filter running VMs
az vm list --query "[?powerState=='VM running'].{Name:name, RG:resourceGroup}" --output table

# Extract a resource ID
VAULT_ID=$(az keyvault show \
    --name mykeyvault \
    --query id \
    --output tsv)
echo "Key Vault ID: $VAULT_ID"

# Conditions in queries
az storage account list \
    --query "[?kind=='StorageV2' && location=='eastus'].{Name:name, SKU:sku.name}" \
    --output table

Full Demo: Provision an Ubuntu VM

The following script illustrates the complete creation of a VM with its network infrastructure:

#!/bin/bash
# create-vm.sh - Create a complete Ubuntu VM with its infrastructure
set -euo pipefail  # Stop on error, undefined variable, or pipe error

# ===== VARIABLES =====
RESOURCE_GROUP="azureautomationrg"
VM_NAME="MyUbuntuVM"
LOCATION="centralus"
ADMIN_USERNAME="azureuser"
IMAGE="Ubuntu2404"
VM_SIZE="Standard_D2s_v3"
VNET_NAME="AppVNet"
SUBNET_NAME="AppSubnet"
NSG_NAME="AppNSG"
PUBLIC_IP_NAME="AppPublicIP"
NIC_NAME="AppNIC"

echo "=== Creating infrastructure for ${VM_NAME} ==="

# ===== RESOURCE GROUP =====
echo "Creating Resource Group: ${RESOURCE_GROUP}"
az group create \
    --name "${RESOURCE_GROUP}" \
    --location "${LOCATION}" \
    --output table

# ===== VIRTUAL NETWORK =====
echo "Creating VNet and Subnet"
az network vnet create \
    --resource-group "${RESOURCE_GROUP}" \
    --name "${VNET_NAME}" \
    --address-prefix "10.0.0.0/16" \
    --subnet-name "${SUBNET_NAME}" \
    --subnet-prefix "10.0.1.0/24" \
    --output table

# ===== NETWORK SECURITY GROUP =====
echo "Creating NSG with SSH rules"
az network nsg create \
    --resource-group "${RESOURCE_GROUP}" \
    --name "${NSG_NAME}"

# SSH rule restricted to a specific IP (SECURE)
MY_IP=$(curl -s ifconfig.me)
az network nsg rule create \
    --resource-group "${RESOURCE_GROUP}" \
    --nsg-name "${NSG_NAME}" \
    --name "AllowSSH" \
    --protocol tcp \
    --priority 1000 \
    --destination-port-range 22 \
    --source-address-prefix "${MY_IP}/32" \
    --access allow

# ===== PUBLIC IP =====
echo "Creating public IP"
az network public-ip create \
    --resource-group "${RESOURCE_GROUP}" \
    --name "${PUBLIC_IP_NAME}" \
    --sku Standard \
    --allocation-method Static

# ===== NETWORK INTERFACE =====
echo "Creating NIC"
az network nic create \
    --resource-group "${RESOURCE_GROUP}" \
    --name "${NIC_NAME}" \
    --vnet-name "${VNET_NAME}" \
    --subnet "${SUBNET_NAME}" \
    --public-ip-address "${PUBLIC_IP_NAME}" \
    --network-security-group "${NSG_NAME}"

# ===== VIRTUAL MACHINE =====
echo "Creating VM: ${VM_NAME}"
az vm create \
    --resource-group "${RESOURCE_GROUP}" \
    --name "${VM_NAME}" \
    --image "${IMAGE}" \
    --admin-username "${ADMIN_USERNAME}" \
    --generate-ssh-keys \
    --size "${VM_SIZE}" \
    --nics "${NIC_NAME}" \
    --os-disk-size-gb 30 \
    --os-disk-caching ReadWrite \
    --assign-identity  \
    --output table

# ===== CONNECTION INFO =====
PUBLIC_IP=$(az network public-ip show \
    --resource-group "${RESOURCE_GROUP}" \
    --name "${PUBLIC_IP_NAME}" \
    --query ipAddress \
    --output tsv)

echo ""
echo "=== VM created successfully! ==="
echo "SSH connection: ssh ${ADMIN_USERNAME}@${PUBLIC_IP}"
echo "To delete everything: az group delete --name ${RESOURCE_GROUP} --yes --no-wait"

Windows Batch Script to Create an Entra ID User

@echo off
setlocal EnableDelayedExpansion

:: ===== CONFIGURATION =====
set TENANT_DOMAIN=mycompany.onmicrosoft.com
set DISPLAY_NAME=New User

:: ===== ARGUMENT VALIDATION =====
if "%~1"=="" (
    echo [ERROR] Usage: create-user.bat ^<username^> ^<password^>
    echo Example: create-user.bat jsmith MyP@ssw0rd!2024
    exit /b 1
)
if "%~2"=="" (
    echo [ERROR] Password is required as second argument
    exit /b 1
)

set USERNAME=%~1
set PASSWORD=%~2
set UPN=%USERNAME%@%TENANT_DOMAIN%

echo [INFO] Creating user: %UPN%

:: ===== CREATE THE USER =====
az ad user create ^
    --display-name "%DISPLAY_NAME%" ^
    --password "%PASSWORD%" ^
    --user-principal-name "%UPN%" ^
    --force-change-password-next-sign-in true

if %ERRORLEVEL% neq 0 (
    echo [ERROR] Failed to create user %UPN%
    echo Check password complexity and name availability.
    exit /b 1
)

echo [SUCCESS] User %UPN% created.
echo [INFO] This user will need to change their password on first login.
echo [WARNING] Communicate this temporary password securely (phone, in person).

Best practice: Accepting secrets as external arguments (rather than hardcoding them in scripts) makes the script generic and avoids security exposure when the script is shared.


Module 2 – Working with Azure Secrets

The Secrets Problem in Automation

flowchart TD
    subgraph BAD["❌ Bad practices"]
        M1[Secret hardcoded in the script]
        M2[Secret in local environment variables]
        M3[Secret in a committed .env file]
        M4[Secret in CI/CD logs]
    end
    
    subgraph GOOD["✅ Good practices"]
        B1[Azure Key Vault]
        B2[Managed Identity - no secret!]
        B3[GitHub/Azure DevOps Secrets]
        B4[OIDC / Workload Identity]
    end
    
    M1 -->|Refactor to| B1
    M2 -->|Replace with| B2
    M3 -->|Use| B3
    M4 -->|Mask with| B4
    
    style BAD fill:#7f1d1d,color:#fff
    style GOOD fill:#14532d,color:#fff

Azure Key Vault: Architecture and Concepts

Azure Key Vault is a service for managing secrets, cryptographic keys and certificates in Azure. It provides:

  • HSM Security: hardware security module storage options
  • Integrated RBAC: granular access control based on Microsoft Entra ID
  • Full audit: all operations are logged in Azure Monitor
  • High availability: automatic replication within the Azure region
  • Lifecycle management: automatic rotation of secrets and certificates

The three types of Key Vault objects:

TypeDescriptionUse CaseReadable?
SecretsConfidential text dataPasswords, conn strings, API keys✅ Value in clear text
KeysRSA or EC cryptographic keysEncryption, digital signatures❌ Intra-vault operations
CertificatesX.509 certificates with lifecycle managementTLS/SSL, authentication✅ Via export
# ===== KEY VAULT CREATION =====

# Register the provider (only needed the first time on a new subscription)
az provider register --namespace Microsoft.KeyVault

# Check registration status
az provider show --namespace Microsoft.KeyVault --query "registrationState" --output tsv

# Create the Key Vault
az keyvault create \
    --name "mykeyvault-$(openssl rand -hex 4)" \
    --resource-group "my-rg" \
    --location "eastus" \
    --sku standard \
    --enable-rbac-authorization true \
    --enable-soft-delete true \
    --soft-delete-retention-days 90 \
    --enable-purge-protection true

# ===== SECRET MANAGEMENT =====

# Create a secret
az keyvault secret set \
    --vault-name "mykeyvault" \
    --name "db-connection-string" \
    --value "Server=myserver.database.windows.net;Database=appdb;User Id=appadmin;Password=MySecurePass;"

# Create a secret with an expiration date
az keyvault secret set \
    --vault-name "mykeyvault" \
    --name "temp-api-key" \
    --value "sk-xyz789abc" \
    --expires "2026-12-31T23:59:59Z"

# Read a secret
az keyvault secret show \
    --vault-name "mykeyvault" \
    --name "db-connection-string" \
    --query value \
    --output tsv

# List all secrets
az keyvault secret list \
    --vault-name "mykeyvault" \
    --output table

# Delete a secret (soft delete)
az keyvault secret delete \
    --vault-name "mykeyvault" \
    --name "temp-api-key"

# Permanently purge (if purge protection disabled)
az keyvault secret purge \
    --vault-name "mykeyvault" \
    --name "temp-api-key"

# ===== KEY MANAGEMENT =====

# Create an RSA key
az keyvault key create \
    --vault-name "mykeyvault" \
    --name "my-rsa-key" \
    --kty RSA \
    --size 4096

# Encrypt data with the key
az keyvault key encrypt \
    --vault-name "mykeyvault" \
    --name "my-rsa-key" \
    --algorithm RSA-OAEP \
    --value "dGVzdA=="  # base64 of "test"

# ===== KEY VAULT RBAC =====

# Assign Key Vault Secrets User role to a user
KV_ID=$(az keyvault show --name "mykeyvault" --query id --output tsv)

az role assignment create \
    --role "Key Vault Secrets User" \
    --assignee "user@company.com" \
    --scope "${KV_ID}"

# Available Key Vault roles:
# - Key Vault Administrator         : Full management
# - Key Vault Secrets Officer       : CRUD secrets
# - Key Vault Secrets User          : Read secrets
# - Key Vault Crypto Officer        : CRUD keys
# - Key Vault Crypto User           : Use keys
# - Key Vault Certificates Officer  : CRUD certificates
# - Key Vault Reader                : Read metadata

Managed Service Identity (MSI): Deep Concept

Managed Identity (MI) solves the fundamental “secret to access secrets” problem (the chicken-and-egg problem).

sequenceDiagram
    participant App as Azure Application\n(App Service / VM)
    participant IMDS as Azure Instance\nMetadata Service
    participant AAD as Microsoft Entra ID
    participant KV as Azure Key Vault

    App->>IMDS: Request token for Key Vault
    IMDS->>AAD: Token exchange via MI
    AAD-->>IMDS: Access token (JWT)
    IMDS-->>App: Access token
    App->>KV: GET secret request with token
    KV->>AAD: Validate token
    AAD-->>KV: Token valid (identity: App MI)
    KV-->>App: Secret value
    
    Note over App,IMDS: No secret stored!\nAzure manages everything automatically

Types of Managed Identities:

TypeLifecycleSharingWhen to use
System-assignedTied to the resource (deleted with it)Not shareableSingle resource with its own identity
User-assignedIndependent resourceReusable by multiple servicesShared identity across multiple apps
# ===== SYSTEM-ASSIGNED MI =====

# Enable MI on an existing Web App
az webapp identity assign \
    --resource-group "my-rg" \
    --name "my-web-app" \
    --identities "[system]"

# Enable MI on a VM
az vm identity assign \
    --resource-group "my-rg" \
    --name "my-vm" \
    --identities "[system]"

# ===== USER-ASSIGNED MI =====

# Create a User-Assigned MI
az identity create \
    --resource-group "my-rg" \
    --name "my-app-identity"

# Get the client ID and object ID
CLIENT_ID=$(az identity show \
    --resource-group "my-rg" \
    --name "my-app-identity" \
    --query clientId \
    --output tsv)

OBJECT_ID=$(az identity show \
    --resource-group "my-rg" \
    --name "my-app-identity" \
    --query principalId \
    --output tsv)

# Assign the MI to a Web App
az webapp identity assign \
    --resource-group "my-rg" \
    --name "my-web-app" \
    --identities "/subscriptions/{sub}/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my-app-identity"

# ===== KEY VAULT PERMISSIONS FOR THE MI =====

KV_ID=$(az keyvault show --name "mykeyvault" --query id --output tsv)

az role assignment create \
    --role "Key Vault Secrets User" \
    --assignee "${OBJECT_ID}" \
    --scope "${KV_ID}"

Full Python Script: MSI + Key Vault + Web App

#!/usr/bin/env python3
"""
assign-msi-keyvault.py
Assigns a Managed Identity to a Web App and configures Key Vault permissions.
"""

import subprocess
import json
import sys

# ===== CONFIGURATION =====
WEB_APP_NAME = "my-web-app"
KEY_VAULT_NAME = "mykeyvault"
RESOURCE_GROUP = "my-rg"
LOCATION = "eastus"

def run_az_command(command: list[str]) -> dict:
    """Executes an az CLI command and returns the JSON result."""
    result = subprocess.run(
        command,
        capture_output=True,
        text=True,
        check=False
    )
    if result.returncode != 0:
        print(f"[ERROR] Command failed: {' '.join(command)}")
        print(f"[STDERR] {result.stderr}")
        sys.exit(1)
    
    if result.stdout.strip():
        return json.loads(result.stdout)
    return {}

def main():
    print("=== MSI + Key Vault Configuration ===")
    
    # 1. Assign a System-Assigned MI to the Web App
    print(f"[1/4] Assigning Managed Identity to {WEB_APP_NAME}...")
    identity = run_az_command([
        "az", "webapp", "identity", "assign",
        "--name", WEB_APP_NAME,
        "--resource-group", RESOURCE_GROUP,
        "--identities", "[system]"
    ])
    
    principal_id = identity["principalId"]
    print(f"      Principal ID: {principal_id}")
    
    # 2. Retrieve the Key Vault ID
    print(f"[2/4] Retrieving Key Vault ID for {KEY_VAULT_NAME}...")
    kv_info = run_az_command([
        "az", "keyvault", "show",
        "--name", KEY_VAULT_NAME,
        "--query", "id",
        "--output", "json"
    ])
    kv_id = kv_info.strip('"')
    print(f"      Key Vault ID: {kv_id}")
    
    # 3. Assign the Key Vault Secrets User role
    print("[3/4] Assigning Key Vault Secrets User role...")
    run_az_command([
        "az", "role", "assignment", "create",
        "--role", "Key Vault Secrets User",
        "--assignee", principal_id,
        "--scope", kv_id
    ])
    print("      Role assigned successfully.")
    
    # 4. Configure App Settings to reference Key Vault
    print("[4/4] Configuring App Settings (Key Vault references)...")
    # Key Vault references allow the app to read secrets directly
    # Format: @Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/secret-name)
    run_az_command([
        "az", "webapp", "config", "appsettings", "set",
        "--resource-group", RESOURCE_GROUP,
        "--name", WEB_APP_NAME,
        "--settings",
        f"DB_CONNECTION=@Microsoft.KeyVault(VaultName={KEY_VAULT_NAME};SecretName=db-connection-string)",
        f"API_KEY=@Microsoft.KeyVault(VaultName={KEY_VAULT_NAME};SecretName=api-key)"
    ])
    print("      App Settings configured.")
    
    print("\n=== Configuration completed successfully! ===")
    print(f"Web App {WEB_APP_NAME} will access secrets via its Managed Identity.")
    print("No secret is stored in the application!")

if __name__ == "__main__":
    main()

Retrieving Key Vault Secrets in a Python Application

#!/usr/bin/env python3
"""
app.py - Application using Azure Key Vault via DefaultAzureCredential
"""

from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
from azure.keyvault.secrets import SecretClient
import os

def get_secrets_from_keyvault(vault_url: str) -> dict:
    """
    Retrieves secrets from Azure Key Vault.
    DefaultAzureCredential automatically tries in order:
    1. EnvironmentCredential (env vars AZURE_CLIENT_ID, etc.)
    2. WorkloadIdentityCredential (Kubernetes)
    3. ManagedIdentityCredential (VMs, App Services, etc.)
    4. AzureCliCredential (local CLI sessions)
    5. AzurePowerShellCredential
    6. AzureDeveloperCliCredential
    """
    credential = DefaultAzureCredential()
    client = SecretClient(vault_url=vault_url, credential=credential)
    
    secrets = {}
    
    # Retrieve required secrets
    secret_names = ["db-connection-string", "api-key", "jwt-secret"]
    
    for secret_name in secret_names:
        try:
            secret = client.get_secret(secret_name)
            secrets[secret_name] = secret.value
            print(f"[OK] Secret '{secret_name}' loaded.")
        except Exception as e:
            print(f"[ERROR] Unable to load '{secret_name}': {e}")
    
    return secrets

def main():
    # The vault URL is configuration (not a secret!)
    vault_url = os.environ.get(
        "AZURE_KEYVAULT_URL",
        "https://mykeyvault.vault.azure.net"
    )
    
    print(f"Loading secrets from: {vault_url}")
    secrets = get_secrets_from_keyvault(vault_url)
    
    # Use secrets (never log them!)
    db_conn = secrets.get("db-connection-string")
    api_key = secrets.get("api-key")
    
    # Application configuration...
    print(f"Application configured with {len(secrets)} secrets.")
    print("Starting application...")

if __name__ == "__main__":
    main()

Module 3 – Automating Automation

CI/CD Pipelines and Azure Automation

flowchart LR
    subgraph GIT["Git Repository"]
        CODE[Source code]
        IaC[CLI Scripts / IaC]
        PIPE[Pipeline config]
    end
    
    subgraph CICD["CI/CD Pipeline"]
        TRIGGER[Trigger\non push/PR]
        BUILD[Build &\nTest]
        SCAN[Security\nScan]
        DEPLOY[Deploy\nAzure CLI]
    end
    
    subgraph AZURE["Azure"]
        SP[Service Principal\nor Workload Identity]
        APP[App Service /\nAKS / Functions]
        KV[Key Vault]
    end
    
    GIT -->|Commit| TRIGGER
    TRIGGER --> BUILD
    BUILD --> SCAN
    SCAN --> DEPLOY
    DEPLOY -->|az login SP\nor OIDC| SP
    SP --> APP
    SP --> KV
    
    style GIT fill:#1e3a5f,color:#fff
    style CICD fill:#1e40af,color:#fff
    style AZURE fill:#0f766e,color:#fff

Service Principal for CI/CD Pipelines

# Create a Service Principal with Contributor permissions on a subscription
az ad sp create-for-rbac \
    --name "github-actions-deploy" \
    --role "Contributor" \
    --scopes "/subscriptions/{subscription-id}" \
    --json-auth  # JSON format for GitHub Actions

# Result (STORE IN CI/CD SECRETS, NEVER IN GIT):
# {
#   "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
#   "clientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
#   "subscriptionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
#   "tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
#   "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
#   "resourceManagerEndpointUrl": "https://management.azure.com/",
#   ...
# }

# Create an SP with minimal permissions (Least Privilege)
# Only on a specific Resource Group
az ad sp create-for-rbac \
    --name "github-actions-rg-deploy" \
    --role "Contributor" \
    --scopes "/subscriptions/{sub}/resourceGroups/my-rg"

# Create an SP with a certificate (more secure than a secret)
az ad sp create-for-rbac \
    --name "jenkins-azure-sp" \
    --create-cert \
    --cert "jenkins-cert" \
    --keyvault "mykeyvault"

GitHub Actions Pipeline with Azure Login

# .github/workflows/deploy.yml
name: Deploy to Azure

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  id-token: write   # Required for OIDC
  contents: read

env:
  RESOURCE_GROUP: my-rg
  APP_NAME: my-web-app
  LOCATION: eastus

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      # ===== OPTION 1: Login via OIDC (RECOMMENDED - no secret) =====
      - name: Azure Login via OIDC
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      # ===== OPTION 2: Login via Service Principal (legacy) =====
      # - name: Azure Login via SP
      #   uses: azure/login@v1
      #   with:
      #     creds: ${{ secrets.AZURE_CREDENTIALS }}

      # ===== AZURE CLI COMMANDS =====
      - name: Deploy infrastructure
        run: |
          # Verify connection
          az account show
          
          # Create or update Resource Group
          az group create \
              --name "${RESOURCE_GROUP}" \
              --location "${LOCATION}"
          
          # Deploy app
          az webapp deploy \
              --resource-group "${RESOURCE_GROUP}" \
              --name "${APP_NAME}" \
              --src-path "./app.zip" \
              --type zip

      - name: Run smoke tests
        run: |
          # Get app URL
          APP_URL=$(az webapp show \
              --resource-group "${RESOURCE_GROUP}" \
              --name "${APP_NAME}" \
              --query "defaultHostName" \
              --output tsv)
          
          echo "Testing: https://${APP_URL}"
          curl -f "https://${APP_URL}/health" || exit 1
          echo "Smoke test passed!"

      - name: Azure Logout
        if: always()
        run: az logout

OIDC Configuration (Workload Identity Federation) for GitHub Actions

# Create the Federated Credential for GitHub Actions (without storing a secret!)
az ad app federated-credential create \
    --id "application-object-id" \
    --parameters '{
        "name": "github-actions-main",
        "issuer": "https://token.actions.githubusercontent.com",
        "subject": "repo:my-org/my-repo:ref:refs/heads/main",
        "description": "GitHub Actions for main branch",
        "audiences": ["api://AzureADTokenExchange"]
    }'

# For a GitHub environment (environment protection)
az ad app federated-credential create \
    --id "application-object-id" \
    --parameters '{
        "name": "github-actions-production",
        "issuer": "https://token.actions.githubusercontent.com",
        "subject": "repo:my-org/my-repo:environment:production",
        "description": "GitHub Actions for production environment",
        "audiences": ["api://AzureADTokenExchange"]
    }'

Azure DevOps Pipeline with Service Connection

# azure-pipelines.yml
trigger:
  branches:
    include:
      - main

variables:
  resourceGroup: 'my-rg'
  appName: 'my-web-app'
  location: 'eastus'

pool:
  vmImage: 'ubuntu-latest'

stages:
  - stage: Build
    displayName: 'Build Application'
    jobs:
      - job: BuildJob
        steps:
          - task: NodeTool@0
            inputs:
              versionSpec: '20.x'
          
          - script: |
              npm ci
              npm run build
              npm run test
            displayName: 'Build and Tests'
          
          - task: PublishBuildArtifacts@1
            inputs:
              pathToPublish: './dist'
              artifactName: 'app'

  - stage: Deploy
    displayName: 'Deploy to Azure'
    dependsOn: Build
    condition: succeeded()
    jobs:
      - deployment: DeployToAzure
        environment: 'production'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: app
                
                # The Service Connection handles authentication
                - task: AzureCLI@2
                  displayName: 'Deploy to Azure App Service'
                  inputs:
                    azureSubscription: 'AzureServiceConnection'
                    scriptType: 'bash'
                    scriptLocation: 'inlineScript'
                    inlineScript: |
                      # Verify connection
                      az account show
                      
                      # Deploy the application
                      cd $(Pipeline.Workspace)/app
                      zip -r ../app.zip .
                      
                      az webapp deploy \
                          --resource-group $(resourceGroup) \
                          --name $(appName) \
                          --src-path ../app.zip \
                          --type zip
                      
                      echo "Deployment complete!"
                
                - task: AzureCLI@2
                  displayName: 'Smoke Tests'
                  inputs:
                    azureSubscription: 'AzureServiceConnection'
                    scriptType: 'bash'
                    scriptLocation: 'inlineScript'
                    inlineScript: |
                      APP_URL=$(az webapp show \
                          --resource-group $(resourceGroup) \
                          --name $(appName) \
                          --query "defaultHostName" \
                          --output tsv)
                      
                      echo "Testing: https://${APP_URL}"
                      
                      # Wait for the app to be ready
                      sleep 30
                      
                      HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://${APP_URL}/health")
                      
                      if [ "$HTTP_STATUS" -ne 200 ]; then
                          echo "FAILURE: Application returned HTTP ${HTTP_STATUS}"
                          exit 1
                      fi
                      
                      echo "Smoke test passed! HTTP ${HTTP_STATUS}"

Module 4 – Azure Automation Runbooks

Architecture and Concepts

Azure Automation is an Azure service that provides an execution environment for automation scripts (runbooks), configuration management (DSC) and updates.

graph TD
    subgraph AA["Azure Automation Account"]
        MI[Managed Identity\nDefined permissions]
        RB[Runbooks\nPython / PowerShell]
        SCHED[Schedules\nScheduled triggers]
        VARS[Shared Variables\nExternal configuration]
        CREDS[Credentials Store\nLegacy - avoid]
    end
    
    subgraph TARGETS["Targeted Resources"]
        STORE[Azure Storage\nBlob archiving]
        SQL[Azure SQL\nDB maintenance]
        VM[Virtual Machines\nStart/Stop/Scale]
        COST[Cost Management\nReports and optimization]
    end
    
    MI -->|Authenticate| TARGETS
    VARS -->|Parameters| RB
    SCHED -->|Triggers| RB
    RB -->|Operates on| TARGETS
    
    style AA fill:#1e3a5f,color:#fff
    style TARGETS fill:#0f4c75,color:#fff

Use Case: Archive Old Blobs

#!/usr/bin/env python3
"""
archive-old-blobs.py
Azure Automation Runbook to archive blobs older than 30 days.
This script runs in the context of an Azure Automation Account with Managed Identity.
"""

from azure.storage.blob import BlobServiceClient, StandardBlobTier
from azure.identity import DefaultAzureCredential
from datetime import datetime, timedelta, timezone
import automationassets  # Module implicitly available in Automation runbooks

def get_variable(name: str) -> str:
    """Retrieves a variable from the Automation Account."""
    return automationassets.get_automation_variable(name)

def archive_old_blobs(
    storage_account_name: str,
    container_name: str,
    cutoff_days: int = 30
) -> dict:
    """
    Archives blobs older than cutoff_days days.
    
    Args:
        storage_account_name: Name of the Azure storage account
        container_name: Container name
        cutoff_days: Number of days before archiving (default: 30)
    
    Returns:
        Operation statistics
    """
    # Authentication via Managed Identity (inherited from Automation Account context)
    credential = DefaultAzureCredential()
    account_url = f"https://{storage_account_name}.blob.core.windows.net"
    
    service_client = BlobServiceClient(
        account_url=account_url,
        credential=credential
    )
    container_client = service_client.get_container_client(container_name)
    
    # Calculate the cutoff date
    cutoff = datetime.now(timezone.utc) - timedelta(days=cutoff_days)
    print(f"Cutoff date: {cutoff.isoformat()}")
    
    stats = {
        "total_blobs": 0,
        "archived": 0,
        "already_archived": 0,
        "errors": 0,
        "bytes_archived": 0
    }
    
    # Iterate over all blobs in the container
    for blob in container_client.list_blobs(include=["metadata"]):
        stats["total_blobs"] += 1
        
        try:
            # Check if the blob is too old
            if blob.last_modified < cutoff:
                # Check the current tier
                if blob.blob_tier == "Archive":
                    stats["already_archived"] += 1
                    continue
                
                # Archive the blob
                blob_client = container_client.get_blob_client(blob.name)
                blob_client.set_standard_blob_tier(StandardBlobTier.ARCHIVE)
                
                stats["archived"] += 1
                stats["bytes_archived"] += blob.size
                
                print(f"[ARCHIVED] {blob.name} "
                      f"(last modified: {blob.last_modified.date()}, "
                      f"size: {blob.size / 1024:.1f} KB)")
        
        except Exception as e:
            stats["errors"] += 1
            print(f"[ERROR] {blob.name}: {e}")
    
    return stats

def main():
    print("=== Runbook: Archiving old blobs ===")
    print(f"Started: {datetime.now(timezone.utc).isoformat()}")
    
    # Retrieve variables from the Automation Account
    storage_account_name = get_variable("StorageAccountName")
    container_name = get_variable("ContainerName")
    cutoff_days = int(get_variable("ArchiveCutoffDays") or "30")
    
    print(f"Storage: {storage_account_name}")
    print(f"Container: {container_name}")
    print(f"Archive after: {cutoff_days} days")
    
    # Run the archiving
    stats = archive_old_blobs(storage_account_name, container_name, cutoff_days)
    
    # Summary
    print("\n=== Summary ===")
    print(f"Total blobs analyzed  : {stats['total_blobs']}")
    print(f"Blobs archived        : {stats['archived']}")
    print(f"Already archived      : {stats['already_archived']}")
    print(f"Errors                : {stats['errors']}")
    print(f"Space optimized       : {stats['bytes_archived'] / (1024*1024):.1f} MB")
    print(f"Completed: {datetime.now(timezone.utc).isoformat()}")

if __name__ == "__main__":
    main()

Script for Creating an Azure Automation Account

#!/bin/bash
# setup-automation-account.sh

RESOURCE_GROUP="automation-rg"
LOCATION="eastus"
AUTOMATION_ACCOUNT="my-automation-account"
STORAGE_ACCOUNT="mystorageaccount"

# Create the Resource Group
az group create --name $RESOURCE_GROUP --location $LOCATION

# Create the Automation Account
az automation account create \
    --resource-group $RESOURCE_GROUP \
    --name $AUTOMATION_ACCOUNT \
    --location $LOCATION \
    --sku Basic

# Enable System-assigned Managed Identity
az automation account update \
    --resource-group $RESOURCE_GROUP \
    --name $AUTOMATION_ACCOUNT \
    --assign-identity [system]

# Get the MI's principal ID
PRINCIPAL_ID=$(az automation account show \
    --resource-group $RESOURCE_GROUP \
    --name $AUTOMATION_ACCOUNT \
    --query "identity.principalId" \
    --output tsv)

echo "Principal ID: $PRINCIPAL_ID"

# Grant access to the Storage Account
STORAGE_ID=$(az storage account show \
    --resource-group $RESOURCE_GROUP \
    --name $STORAGE_ACCOUNT \
    --query id \
    --output tsv)

az role assignment create \
    --role "Storage Blob Data Contributor" \
    --assignee $PRINCIPAL_ID \
    --scope $STORAGE_ID

# Create shared variables
az automation variable create \
    --resource-group $RESOURCE_GROUP \
    --automation-account-name $AUTOMATION_ACCOUNT \
    --name "StorageAccountName" \
    --value $STORAGE_ACCOUNT \
    --encrypted false

az automation variable create \
    --resource-group $RESOURCE_GROUP \
    --automation-account-name $AUTOMATION_ACCOUNT \
    --name "ContainerName" \
    --value "my-container" \
    --encrypted false

az automation variable create \
    --resource-group $RESOURCE_GROUP \
    --automation-account-name $AUTOMATION_ACCOUNT \
    --name "ArchiveCutoffDays" \
    --value "30" \
    --encrypted false

# Create the runbook
az automation runbook create \
    --resource-group $RESOURCE_GROUP \
    --automation-account-name $AUTOMATION_ACCOUNT \
    --name "ArchiveOldBlobs" \
    --type Python3 \
    --description "Archive blobs older than N days"

# Upload the runbook script
az automation runbook replace-content \
    --resource-group $RESOURCE_GROUP \
    --automation-account-name $AUTOMATION_ACCOUNT \
    --name "ArchiveOldBlobs" \
    --content @archive-old-blobs.py

# Publish the runbook
az automation runbook publish \
    --resource-group $RESOURCE_GROUP \
    --automation-account-name $AUTOMATION_ACCOUNT \
    --name "ArchiveOldBlobs"

# Create a weekly schedule (Sunday at 2 AM)
az automation schedule create \
    --resource-group $RESOURCE_GROUP \
    --automation-account-name $AUTOMATION_ACCOUNT \
    --name "Weekly-Sunday-2AM" \
    --frequency Week \
    --interval 1 \
    --start-time "2026-01-05T02:00:00Z" \
    --time-zone "UTC"

# Link the runbook to the schedule
az automation job-schedule create \
    --resource-group $RESOURCE_GROUP \
    --automation-account-name $AUTOMATION_ACCOUNT \
    --runbook-name "ArchiveOldBlobs" \
    --schedule-name "Weekly-Sunday-2AM"

echo "=== Setup complete ==="
echo "The ArchiveOldBlobs runbook will run every Sunday at 2:00 AM UTC"

Module 5 – Logic Apps

Architecture and Concept

Logic Apps are a visual workflow orchestration service. They integrate with hundreds of connectors (Azure, SaaS, on-premises) and are ideal for data integration scenarios.

flowchart TD
    subgraph TRIGGERS["Triggers"]
        T1[New blob\nAzure Storage]
        T2[HTTP Request\nWebhook]
        T3[Schedule\nRecurrence]
        T4[Queue Message\nService Bus]
        T5[Received email\nOutlook/Gmail]
    end
    
    subgraph ACTIONS["Actions"]
        A1[Read blob]
        A2[Write blob]
        A3[Send email]
        A4[HTTP call]
        A5[SQL query]
        A6[Teams message]
        A7[Condition]
        A8[ForEach loop]
    end
    
    subgraph OUTPUTS["Outputs"]
        O1[Azure Storage]
        O2[Azure SQL]
        O3[Teams/Slack]
        O4[Azure Function]
        O5[Automation Runbook]
    end
    
    TRIGGERS --> ACTIONS
    ACTIONS --> OUTPUTS
    
    style TRIGGERS fill:#1e3a5f,color:#fff
    style ACTIONS fill:#0f4c75,color:#fff
    style OUTPUTS fill:#0f766e,color:#fff

Typical Workflow via ARM Template (export)

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "resources": [
    {
      "type": "Microsoft.Logic/workflows",
      "apiVersion": "2019-05-01",
      "name": "copy-blob-workflow",
      "location": "[resourceGroup().location]",
      "identity": {
        "type": "SystemAssigned"
      },
      "properties": {
        "state": "Enabled",
        "definition": {
          "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
          "contentVersion": "1.0.0.0",
          "triggers": {
            "When_a_blob_is_added_or_modified": {
              "type": "ApiConnection",
              "inputs": {
                "host": {
                  "connection": {
                    "name": "@parameters('$connections')['azureblob']['connectionId']"
                  }
                },
                "method": "get",
                "path": "/datasets/default/triggers/batch/onupdatedfile",
                "queries": {
                  "folderId": "JTJmbXktY29udGFpbmVy",
                  "maxFileCount": 10
                }
              },
              "recurrence": {
                "frequency": "Minute",
                "interval": 1
              }
            }
          },
          "actions": {
            "Get_blob_content": {
              "type": "ApiConnection",
              "inputs": {
                "host": {
                  "connection": {
                    "name": "@parameters('$connections')['azureblob']['connectionId']"
                  }
                },
                "method": "get",
                "path": "/datasets/default/files/@{encodeURIComponent(triggerBody()?['Id'])}/content"
              }
            },
            "Copy_to_storage2": {
              "type": "ApiConnection",
              "runAfter": {
                "Get_blob_content": ["Succeeded"]
              },
              "inputs": {
                "host": {
                  "connection": {
                    "name": "@parameters('$connections')['azureblob2']['connectionId']"
                  }
                },
                "method": "post",
                "path": "/datasets/default/files",
                "queries": {
                  "folderPath": "/destination",
                  "name": "@triggerBody()?['Name']"
                },
                "body": "@body('Get_blob_content')"
              }
            },
            "Send_Teams_notification": {
              "type": "ApiConnection",
              "runAfter": {
                "Copy_to_storage2": ["Succeeded"]
              },
              "inputs": {
                "host": {
                  "connection": {
                    "name": "@parameters('$connections')['teams']['connectionId']"
                  }
                },
                "method": "post",
                "path": "/v3/beta/teams/@{encodeURIComponent('team-id')}/channels/@{encodeURIComponent('channel-id')}/messages",
                "body": {
                  "body": {
                    "content": "New file copied: @{triggerBody()?['Name']}"
                  }
                }
              }
            }
          }
        }
      }
    }
  ]
}

Advanced Automation Scenarios

Scenario 1: Automatic Cleanup of Untagged Resources

#!/bin/bash
# cleanup-untagged-resources.sh
# Identifies and reports resources without an "owner" tag

SUBSCRIPTION_ID=$(az account show --query id --output tsv)
REPORT_FILE="untagged-resources-$(date +%Y%m%d).csv"

echo "ResourceId,ResourceType,ResourceGroup,Name,Location" > $REPORT_FILE

echo "Searching for resources without 'owner' tag..."

# List all resources without the 'owner' tag
az resource list \
    --query "[?tags.owner == null].{Id:id, Type:type, RG:resourceGroup, Name:name, Loc:location}" \
    --output json | \
jq -r '.[] | [.Id, .Type, .RG, .Name, .Loc] | @csv' >> $REPORT_FILE

COUNT=$(wc -l < $REPORT_FILE)
echo "Resources without 'owner' tag: $((COUNT - 1))"
echo "Report generated: $REPORT_FILE"

Scenario 2: Automated VM Start/Stop Outside Business Hours

#!/usr/bin/env python3
"""
vm-schedule.py
Runbook to start/stop VMs according to a business hours schedule.
"""

import automationassets
from azure.identity import DefaultAzureCredential
from azure.mgmt.compute import ComputeManagementClient
from datetime import datetime, timezone
import pytz

def is_business_hours() -> bool:
    """Checks if we are within business hours (Mon-Fri, 8am-6pm EST)."""
    est = pytz.timezone("America/New_York")
    now_est = datetime.now(est)
    
    if now_est.weekday() >= 5:  # Saturday=5, Sunday=6
        return False
    
    return 8 <= now_est.hour < 18

def main():
    subscription_id = automationassets.get_automation_variable("SubscriptionId")
    resource_group = automationassets.get_automation_variable("VMResourceGroup")
    action = "start" if is_business_hours() else "deallocate"
    
    credential = DefaultAzureCredential()
    compute_client = ComputeManagementClient(credential, subscription_id)
    
    # List VMs with the tag "auto-schedule=true"
    vms = compute_client.virtual_machines.list(resource_group)
    
    for vm in vms:
        if vm.tags and vm.tags.get("auto-schedule") == "true":
            print(f"[{action.upper()}] VM: {vm.name}")
            
            if action == "start":
                compute_client.virtual_machines.begin_start(resource_group, vm.name).wait()
            elif action == "deallocate":
                compute_client.virtual_machines.begin_deallocate(resource_group, vm.name).wait()
    
    print(f"Operation '{action}' completed for all eligible VMs.")

if __name__ == "__main__":
    main()

Scenario 3: Automated Monthly Cost Report

#!/bin/bash
# monthly-cost-report.sh
# Generates a cost report by Resource Group for the current month

START_DATE=$(date -d "$(date +%Y-%m-01)" +%Y-%m-%d)
END_DATE=$(date +%Y-%m-%d)

echo "=== Cost Report: ${START_DATE} to ${END_DATE} ==="

# Costs by Resource Group
az consumption usage list \
    --start-date "${START_DATE}" \
    --end-date "${END_DATE}" \
    --query "sort_by([].{RG:resourceGroupName, Service:meterCategory, Cost:pretaxCost}, &Cost)" \
    --output table

# Top 10 most expensive resources
echo ""
echo "=== Top 10 most expensive resources ==="
az consumption usage list \
    --start-date "${START_DATE}" \
    --end-date "${END_DATE}" \
    --query "sort_by([].{Resource:instanceName, Service:meterCategory, Cost:pretaxCost}, &Cost) | [-10:]" \
    --output table

Comparison Tables

Comparison of Azure Authentication Methods

MethodSecurityComplexityIdeal Scenario
Interactive az login✅ High⭐ SimpleLocal developer
Service Principal + Secret⚠️ Medium⭐⭐ ModerateLegacy CI/CD
Service Principal + Certificate✅ High⭐⭐⭐ AdvancedSecure CI/CD
OIDC Workload Identity✅✅ Very high⭐⭐ ModerateGitHub Actions/AzDevOps
System-Assigned MI✅✅ Very high⭐ SimpleAzure services (VM, App, AKS)
User-Assigned MI✅✅ Very high⭐⭐ ModerateMulti-service, shared identity

Comparison of Azure Automation Tools

ToolTypeLanguagesUse CaseLearning Curve
Azure CLIImperativeBash, Batch, PSOne-off scripts, CI/CD⭐ Low
Azure PowerShellImperativePowerShellWindows admins, Exchange⭐⭐ Moderate
ARM TemplatesDeclarativeJSONFull IaC, complex deployments⭐⭐⭐ High
BicepDeclarativeBicep DSLModern IaC, replaces ARM⭐⭐ Moderate
TerraformDeclarativeHCLMulti-cloud, large infra⭐⭐⭐ High
Automation RunbooksScriptPython, PSScheduled IT tasks⭐⭐ Moderate
Logic AppsLow/No-codeJSON (ARM)Integration, orchestration⭐ Low
Azure FunctionsEvent-drivenPython, C#, JS…Event processing, APIs⭐⭐ Moderate

Azure Blob Storage Tiers

TierStorage CostAccess CostRetrieval LatencyUse Case
Hot$$$ High$ LowImmediateFrequently accessed data
Cool$$ Medium$$ MediumImmediateInfrequently accessed data (< 1x/month)
Cold$ Low$$$ HighImmediateRarely accessed data (< 1x/year)
Archive$$ Very low$$$$ Very high1-15 hours (rehydration)Long-term archiving, backup

Best Practices

Security

Rule #1: Secrets should NEVER end up in version control. Not in scripts, not in configuration files, not in logs.

Rule #2: Use Managed Identity whenever possible. System-assigned for unique services, User-assigned for shared identities.

Rule #3: Follow the principle of least privilege. Grant only the strictly necessary permissions, on the smallest possible scope.

Rule #4: Enable purge protection and soft delete on production Key Vaults.

CLI Scripts

Rule #5: Always use set -euo pipefail in Bash scripts to stop on error, undefined variable or pipe error.

Rule #6: Externalize configuration (resource names, locations) into variables at the top of the script to facilitate maintenance.

Rule #7: Use --query and --output tsv to extract clean values in scripts, avoiding manual JSON parsing.

Automation Runbooks

Rule #8: Store all configurable parameters in shared Automation variables, never hardcoded in the runbook code.

Rule #9: Test runbooks in a non-production environment before enabling them in production.

Rule #10: Enable verbose logging in runbooks during development, disable it in production.


Glossary

TermDefinition
Azure CLICross-platform command-line interface for managing Azure resources
Service PrincipalApplication identity in Microsoft Entra ID with Azure permissions
Managed Identity (MI)Identity automatically managed by Azure, without credentials to manage
System-Assigned MIMI tied to the lifecycle of a specific Azure resource
User-Assigned MIIndependent MI, reusable by multiple Azure resources
OIDCOpenID Connect - token-based authentication protocol for CI/CD pipelines
Workload Identity FederationMechanism allowing external identities (GitHub, etc.) to authenticate without a secret
Azure Key VaultSecure storage service for secrets, keys and certificates
SecretConfidential text data stored in Key Vault
KeyRSA or EC cryptographic key stored in Key Vault
CertificateX.509 certificate with lifecycle management in Key Vault
RBACRole-Based Access Control - role-based access control
Automation AccountAzure service hosting runbooks, schedules and automation variables
RunbookPython or PowerShell script executed in an Automation Account
Logic AppVisual and low/no-code workflow orchestration service
automationassetsImplicit Python module to access variables in an Automation Account
DefaultAzureCredentialAzure SDK class that automatically tries multiple authentication methods
JMESPathQuery language for JSON used with the --query flag of Azure CLI
ARM TemplateAzure Resource Manager Template - JSON format describing Azure infrastructure
GitOpsPractice where Git is the source of truth for configuration and deployments
CI/CDContinuous Integration / Continuous Deployment
Blob tierAzure storage tier (Hot, Cool, Cold, Archive) with different costs and delays
Soft DeleteProtection against accidental deletion with configurable retention period
Purge ProtectionAdditional protection preventing permanent deletion before expiration
az accountCLI command group for managing subscriptions and tenants
az groupCLI command group for managing Resource Groups
az keyvaultCLI command group for managing Azure Key Vault
az webappCLI command group for managing Azure App Service
az automationCLI command group for managing Azure Automation

Key Takeaways

  • Never put secrets in version control — never hardcoded in scripts.
  • Managed Identity = preferred solution for authenticating workloads in Azure.
  • Azure CLI = primary tool for Azure automation (wrapper around the REST API).
  • Key Vault = store all secrets, keys and certificates centrally.
  • Logic Apps = visual orchestration, understandable by non-developers.
  • Automation Runbooks = IT ops tasks in Python/PowerShell with MSI context.
  • Separate authentication and script logic for modularity and security.

Search Terms

azure · automation · cli · core · infrastructure · microsoft · architecture · comparison · scenario · script · secrets · service · vault · actions · automated · ci/cd · commands · concept · concepts · github · identity · msi · pipeline · pipelines

Interested in this course?

Contact us to book it or get a custom training plan for your team.