Level: Beginner / Intermediate
Estimated duration: 3–4 hours
Last updated: June 2026
Table of Contents
- Introduction to Azure Automation
- Module 1 – Azure CLI Basic Commands
- Module 2 – Working with Azure Secrets
- Module 3 – Automating Automation
- Module 4 – Azure Automation Runbooks
- Module 5 – Logic Apps
- Advanced Automation Scenarios
- Comparison Tables
- Best Practices
- 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:
| Type | Description | Use Case | Readable? |
|---|---|---|---|
| Secrets | Confidential text data | Passwords, conn strings, API keys | ✅ Value in clear text |
| Keys | RSA or EC cryptographic keys | Encryption, digital signatures | ❌ Intra-vault operations |
| Certificates | X.509 certificates with lifecycle management | TLS/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:
| Type | Lifecycle | Sharing | When to use |
|---|---|---|---|
| System-assigned | Tied to the resource (deleted with it) | Not shareable | Single resource with its own identity |
| User-assigned | Independent resource | Reusable by multiple services | Shared 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
| Method | Security | Complexity | Ideal Scenario |
|---|---|---|---|
| Interactive az login | ✅ High | ⭐ Simple | Local developer |
| Service Principal + Secret | ⚠️ Medium | ⭐⭐ Moderate | Legacy CI/CD |
| Service Principal + Certificate | ✅ High | ⭐⭐⭐ Advanced | Secure CI/CD |
| OIDC Workload Identity | ✅✅ Very high | ⭐⭐ Moderate | GitHub Actions/AzDevOps |
| System-Assigned MI | ✅✅ Very high | ⭐ Simple | Azure services (VM, App, AKS) |
| User-Assigned MI | ✅✅ Very high | ⭐⭐ Moderate | Multi-service, shared identity |
Comparison of Azure Automation Tools
| Tool | Type | Languages | Use Case | Learning Curve |
|---|---|---|---|---|
| Azure CLI | Imperative | Bash, Batch, PS | One-off scripts, CI/CD | ⭐ Low |
| Azure PowerShell | Imperative | PowerShell | Windows admins, Exchange | ⭐⭐ Moderate |
| ARM Templates | Declarative | JSON | Full IaC, complex deployments | ⭐⭐⭐ High |
| Bicep | Declarative | Bicep DSL | Modern IaC, replaces ARM | ⭐⭐ Moderate |
| Terraform | Declarative | HCL | Multi-cloud, large infra | ⭐⭐⭐ High |
| Automation Runbooks | Script | Python, PS | Scheduled IT tasks | ⭐⭐ Moderate |
| Logic Apps | Low/No-code | JSON (ARM) | Integration, orchestration | ⭐ Low |
| Azure Functions | Event-driven | Python, C#, JS… | Event processing, APIs | ⭐⭐ Moderate |
Azure Blob Storage Tiers
| Tier | Storage Cost | Access Cost | Retrieval Latency | Use Case |
|---|---|---|---|---|
| Hot | $$$ High | $ Low | Immediate | Frequently accessed data |
| Cool | $$ Medium | $$ Medium | Immediate | Infrequently accessed data (< 1x/month) |
| Cold | $ Low | $$$ High | Immediate | Rarely accessed data (< 1x/year) |
| Archive | $$ Very low | $$$$ Very high | 1-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 pipefailin 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
--queryand--output tsvto 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
| Term | Definition |
|---|---|
| Azure CLI | Cross-platform command-line interface for managing Azure resources |
| Service Principal | Application identity in Microsoft Entra ID with Azure permissions |
| Managed Identity (MI) | Identity automatically managed by Azure, without credentials to manage |
| System-Assigned MI | MI tied to the lifecycle of a specific Azure resource |
| User-Assigned MI | Independent MI, reusable by multiple Azure resources |
| OIDC | OpenID Connect - token-based authentication protocol for CI/CD pipelines |
| Workload Identity Federation | Mechanism allowing external identities (GitHub, etc.) to authenticate without a secret |
| Azure Key Vault | Secure storage service for secrets, keys and certificates |
| Secret | Confidential text data stored in Key Vault |
| Key | RSA or EC cryptographic key stored in Key Vault |
| Certificate | X.509 certificate with lifecycle management in Key Vault |
| RBAC | Role-Based Access Control - role-based access control |
| Automation Account | Azure service hosting runbooks, schedules and automation variables |
| Runbook | Python or PowerShell script executed in an Automation Account |
| Logic App | Visual and low/no-code workflow orchestration service |
| automationassets | Implicit Python module to access variables in an Automation Account |
| DefaultAzureCredential | Azure SDK class that automatically tries multiple authentication methods |
| JMESPath | Query language for JSON used with the --query flag of Azure CLI |
| ARM Template | Azure Resource Manager Template - JSON format describing Azure infrastructure |
| GitOps | Practice where Git is the source of truth for configuration and deployments |
| CI/CD | Continuous Integration / Continuous Deployment |
| Blob tier | Azure storage tier (Hot, Cool, Cold, Archive) with different costs and delays |
| Soft Delete | Protection against accidental deletion with configurable retention period |
| Purge Protection | Additional protection preventing permanent deletion before expiration |
| az account | CLI command group for managing subscriptions and tenants |
| az group | CLI command group for managing Resource Groups |
| az keyvault | CLI command group for managing Azure Key Vault |
| az webapp | CLI command group for managing Azure App Service |
| az automation | CLI 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