Course: Configuring a DevOps Environment within Azure Native Tools
Level: Intermediate
Estimated Duration: 4–5 hours
Last Updated: June 2026
Table of Contents
- Azure DevOps Reference Architecture
- Networking and Network Security
- RBAC and Identities
- Azure Storage for DevOps
- VMs as Build Agents
- Azure Container Registry (ACR)
- AKS vs ACI as Deployment Targets
- Monitoring with Azure Monitor and Application Insights
- Complete Code Examples
- Comparison Tables
- Glossary
1. Azure DevOps Reference Architecture
Overview
Before writing the first line of pipeline code, the Azure environment must be prepared. Infrastructure provisioning is not just a configuration step — it is the foundation of any secure, reliable, and observable DevOps workflow.
Microsoft has defined a reference architecture for DevOps pipelines on Azure. This architecture represents how Azure services interconnect to form a complete software delivery system.
flowchart TD
subgraph IDENTITY["Identity and Governance"]
AAD[Microsoft Entra ID\nAuthentication]
RBAC[Azure RBAC\nAuthorization]
KV[Azure Key Vault\nSecrets]
end
subgraph SOURCE["Source Control"]
REPOS[Azure Repos\nor GitHub]
BRANCH[Branch Policies\nPR reviews]
end
subgraph BUILD["Build and Test"]
AGENTS[Build Agents\nMS-hosted or self-hosted]
ARTIFACTS[Azure Artifacts\nNuGet/NPM/Maven packages]
ACR[Azure Container Registry\nDocker Images]
end
subgraph DEPLOY["Deployment"]
PIPELINES[Azure Pipelines\nCI/CD]
APPSERV[App Service]
AKS[Azure Kubernetes\nService]
ACI[Azure Container\nInstances]
VMS[Virtual Machines]
end
subgraph MONITOR["Monitoring"]
AZMON[Azure Monitor\nMetrics and alerts]
APPI[Application Insights\nApp telemetry]
LOGS[Log Analytics\nKQL queries]
end
IDENTITY --> SOURCE
SOURCE --> BUILD
BUILD --> PIPELINES
PIPELINES --> DEPLOY
DEPLOY --> MONITOR
MONITOR -->|Feedback| PIPELINES
style IDENTITY fill:#1e40af,color:#fff
style SOURCE fill:#7c3aed,color:#fff
style BUILD fill:#b45309,color:#fff
style DEPLOY fill:#065f46,color:#fff
style MONITOR fill:#1e3a5f,color:#fff
Key architecture components:
| Component | Azure Service | Role |
|---|---|---|
| Source Control | Azure Repos / GitHub | Code management, PRs, branch policies |
| Identity | Microsoft Entra ID | Authentication for developers and pipelines |
| CI/CD | Azure Pipelines | Automated build, test and deployment |
| Artifacts | Azure Artifacts | Internal package feeds |
| Docker Images | Azure Container Registry | Secure image storage |
| Secrets | Azure Key Vault | Centralized secret management |
| Compute | AKS, App Service, VMs | Deployment targets |
| Monitoring | Azure Monitor + App Insights | Full observability |
2. Networking and Network Security
Network Architecture for DevOps
Network design is critical: it determines how agents, pipelines, and resources communicate with each other while limiting the attack surface.
graph TD
subgraph INTERNET["Internet"]
DEVS[Developers]
PIPELINES_CI[Azure DevOps\nPipeline Service]
end
subgraph VNET_DEV["VNet Dev (10.0.0.0/16)"]
subgraph AGENTS["Subnet Agents (10.0.1.0/24)"]
BA[Build Agents\nself-hosted]
end
subgraph TEST["Subnet Test (10.0.2.0/24)"]
TESTVM[Test VMs]
end
end
subgraph VNET_STAGING["VNet Staging (10.1.0.0/16)"]
subgraph STAGING["Subnet App (10.1.1.0/24)"]
STAGINGAPP[App Service\nStaging Slot]
end
subgraph STAGINGDB["Subnet DB (10.1.2.0/24)"]
STAGINGDB2[Azure SQL\nStaging]
end
end
subgraph VNET_PROD["VNet Production (10.2.0.0/16)"]
subgraph PRODAPP["Subnet App (10.2.1.0/24)"]
PRODWEB[App Service\nProduction]
end
subgraph PRODDB["Subnet DB (10.2.2.0/24)"]
PRODDB2[Azure SQL\nProduction]
end
end
DEVS -->|HTTPS| PIPELINES_CI
PIPELINES_CI -->|Peering| AGENTS
AGENTS -->|Deploy| STAGINGAPP
AGENTS -->|Deploy| PRODWEB
VNET_DEV <-->|VNet Peering| VNET_STAGING
VNET_STAGING <-->|VNet Peering| VNET_PROD
style INTERNET fill:#1e293b,color:#fff
style VNET_DEV fill:#1e3a5f,color:#fff
style VNET_STAGING fill:#0f4c75,color:#fff
style VNET_PROD fill:#0c3547,color:#fff
VNet, Subnets and CIDR Planning
# ===== CREATING THE NETWORK ARCHITECTURE =====
# Development VNet
az network vnet create \
--resource-group rg-dev \
--name vnet-dev \
--address-prefix "10.0.0.0/16" \
--location eastus
# Subnets for the Dev VNet
az network vnet subnet create \
--resource-group rg-dev \
--vnet-name vnet-dev \
--name subnet-agents \
--address-prefix "10.0.1.0/24"
az network vnet subnet create \
--resource-group rg-dev \
--vnet-name vnet-dev \
--name subnet-test \
--address-prefix "10.0.2.0/24"
# Staging VNet (different IP range to avoid peering overlaps)
az network vnet create \
--resource-group rg-staging \
--name vnet-staging \
--address-prefix "10.1.0.0/16" \
--location eastus
az network vnet subnet create \
--resource-group rg-staging \
--vnet-name vnet-staging \
--name subnet-app \
--address-prefix "10.1.1.0/24"
# ===== VNet PEERING =====
# Retrieve VNet IDs
DEV_VNET_ID=$(az network vnet show \
--resource-group rg-dev \
--name vnet-dev \
--query id \
--output tsv)
STAGING_VNET_ID=$(az network vnet show \
--resource-group rg-staging \
--name vnet-staging \
--query id \
--output tsv)
# Create peering Dev → Staging
az network vnet peering create \
--resource-group rg-dev \
--name dev-to-staging \
--vnet-name vnet-dev \
--remote-vnet $STAGING_VNET_ID \
--allow-vnet-access \
--allow-forwarded-traffic
# Create peering Staging → Dev (bidirectional)
az network vnet peering create \
--resource-group rg-staging \
--name staging-to-dev \
--vnet-name vnet-staging \
--remote-vnet $DEV_VNET_ID \
--allow-vnet-access \
--allow-forwarded-traffic
Critical rule: VNet IP address ranges must be non-overlapping for VNet peering to work. Overlapping ranges will break communication.
Network Security Groups (NSG)
# ===== NSG FOR BUILD AGENTS =====
# Create the NSG
az network nsg create \
--resource-group rg-dev \
--name nsg-build-agents \
--location eastus
# Rule: Allow Azure DevOps Service to agents (port 443)
az network nsg rule create \
--resource-group rg-dev \
--nsg-name nsg-build-agents \
--name AllowAzureDevOps \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefix AzureDevOps \
--source-port-range "*" \
--destination-address-prefix "*" \
--destination-port-range 443
# Rule: Allow Azure Monitor (for diagnostics)
az network nsg rule create \
--resource-group rg-dev \
--nsg-name nsg-build-agents \
--name AllowAzureMonitor \
--priority 110 \
--direction Outbound \
--access Allow \
--protocol Tcp \
--source-address-prefix "*" \
--source-port-range "*" \
--destination-address-prefix AzureMonitor \
--destination-port-range 443
# Rule: Allow outbound to Internet (for downloads)
az network nsg rule create \
--resource-group rg-dev \
--nsg-name nsg-build-agents \
--name AllowInternetOutbound \
--priority 200 \
--direction Outbound \
--access Allow \
--protocol Tcp \
--source-address-prefix "*" \
--source-port-range "*" \
--destination-address-prefix Internet \
--destination-port-range 443
# Rule: Deny all other inbound
az network nsg rule create \
--resource-group rg-dev \
--nsg-name nsg-build-agents \
--name DenyAllInbound \
--priority 4096 \
--direction Inbound \
--access Deny \
--protocol "*" \
--source-address-prefix "*" \
--source-port-range "*" \
--destination-address-prefix "*" \
--destination-port-range "*"
# Associate the NSG with the subnet
az network vnet subnet update \
--resource-group rg-dev \
--vnet-name vnet-dev \
--name subnet-agents \
--network-security-group nsg-build-agents
3. RBAC and Identities
RBAC Concept: Who + What + Where
graph LR
subgraph WHO["Who (Principal)"]
USER[AAD User]
GROUP[AAD Group]
SP[Service Principal]
MI[Managed Identity]
end
subgraph WHAT["What (Role Definition)"]
OWNER["Owner\n(Everything + access management)"]
CONTRIB["Contributor\n(Create/modify resources)"]
READER["Reader\n(Read-only)"]
CUSTOM["Custom Role\n(Specific permissions)"]
end
subgraph WHERE["Where (Scope)"]
MGMT["Management Group"]
SUB["Subscription"]
RG["Resource Group"]
RES["Individual Resource"]
end
WHO -->|"Role Assignment"| WHAT
WHAT -->|"Scoped to"| WHERE
WHERE -->|"Waterfall inheritance"| RES
RBAC Permission Inheritance:
A role assignment at the Management Group level applies to:
- All Subscriptions under the MG
- All Resource Groups within those Subscriptions
- All resources within those Resource Groups
# ===== AZURE RBAC ROLE ASSIGNMENTS =====
# Assign "Contributor" on a Resource Group
az role assignment create \
--assignee "developer@contoso.com" \
--role "Contributor" \
--scope "/subscriptions/{sub-id}/resourceGroups/rg-dev"
# Assign "Reader" on an entire subscription
az role assignment create \
--assignee "group-objectid" \
--role "Reader" \
--scope "/subscriptions/{sub-id}"
# Assign an ACR role (AcrPull) for a pipeline
az role assignment create \
--assignee "pipeline-sp-objectid" \
--role "AcrPull" \
--scope "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ContainerRegistry/registries/myregistry"
# List assignments on a scope
az role assignment list \
--scope "/subscriptions/{sub}/resourceGroups/rg-dev" \
--output table
# View available roles
az role definition list --output table | grep -E "Contributor|Reader|Owner|Acr"
Common built-in roles:
| Role | Rights | DevOps Use Case |
|---|---|---|
| Owner | Everything, including access management | Cloud admins |
| Contributor | Create/modify all resources | DevOps teams |
| Reader | Read-only on everything | Dev teams (prod) |
| User Access Administrator | Manage access only | RBAC admins |
| AcrPush | Push images to ACR | CI build agents |
| AcrPull | Pull images from ACR | Deployment agents |
| Key Vault Secrets User | Read KV secrets | Applications, pipelines |
| Storage Blob Data Contributor | CRUD on blobs | Build agents |
Service Principals vs Managed Identities
graph TD
subgraph SP["Service Principal"]
SP1["Identity in Entra ID\nwith App Registration"]
SP2["Requires: Client ID\n+ Secret OR Certificate"]
SP3["Usage: GitHub Actions,\ncross-tenant, external CI/CD"]
SP4["⚠️ Secret must be managed/rotated"]
end
subgraph MI["Managed Identity"]
MI1["Identity managed\nautomatically by Azure"]
MI2["No secret!\nAzure handles everything"]
MI3["Usage: VM, App Service,\nAKS, Functions (within Azure)"]
MI4["✅ Recommended when possible"]
subgraph TYPES["Types"]
SYS["System-assigned\n(tied to the resource)"]
USR["User-assigned\n(shareable)"]
end
end
style SP fill:#f97316,color:#fff
style MI fill:#10b981,color:#fff
# ===== SERVICE PRINCIPAL =====
# Create an SP for GitHub Actions
az ad sp create-for-rbac \
--name "github-actions-deploy" \
--role "Contributor" \
--scopes "/subscriptions/{sub-id}/resourceGroups/rg-dev" \
--json-auth # Format for GitHub Actions secrets
# SP with certificate (more secure than a secret)
az ad sp create-for-rbac \
--name "jenkins-azure-sp" \
--create-cert \
--cert "jenkins-cert" \
--keyvault "mykeyvault"
# Rotate an SP secret
az ad sp credential reset \
--id "client-id" \
--append # Keep old credentials during transition
# ===== MANAGED IDENTITY =====
# System-assigned MI on a VM
az vm identity assign \
--resource-group rg-dev \
--name build-agent-vm
# Retrieve the MI principal ID
PRINCIPAL_ID=$(az vm show \
--resource-group rg-dev \
--name build-agent-vm \
--query "identity.principalId" \
--output tsv)
# Assign the AcrPull role to the VM MI
az role assignment create \
--role "AcrPull" \
--assignee $PRINCIPAL_ID \
--scope "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ContainerRegistry/registries/myregistry"
# User-assigned MI (reusable)
az identity create \
--resource-group rg-shared \
--name mi-pipeline-agent
# Assign to multiple VMs
MI_ID=$(az identity show \
--resource-group rg-shared \
--name mi-pipeline-agent \
--query id \
--output tsv)
az vm identity assign \
--resource-group rg-dev \
--name build-agent-1 \
--identities $MI_ID
az vm identity assign \
--resource-group rg-dev \
--name build-agent-2 \
--identities $MI_ID
4. Azure Storage for DevOps
Redundancy Levels by Environment
graph LR
subgraph DEV["Dev - LRS"]
D1["3 copies\nSame datacenter"]
D2["Cost: $"]
D3["Availability: 99.9%"]
end
subgraph STAGING["Staging - ZRS"]
S1["3 copies\n3 availability zones"]
S2["Cost: $$"]
S3["Availability: 99.99%"]
end
subgraph PROD["Prod - GRS"]
P1["6 copies\n2 Azure regions"]
P2["Cost: $$$"]
P3["Availability: 99.999%"]
end
subgraph PRODCRIT["Critical Prod - GZRS"]
PC1["6 copies\n3 zones + 2 regions"]
PC2["Cost: $$$$"]
PC3["Availability: 99.9999%"]
end
style DEV fill:#059669,color:#fff
style STAGING fill:#d97706,color:#fff
style PROD fill:#dc2626,color:#fff
style PRODCRIT fill:#7c3aed,color:#fff
# ===== CREATE STORAGE ACCOUNTS PER ENVIRONMENT =====
# Development (LRS - economical)
az storage account create \
--name "stdevopsdev01" \
--resource-group "rg-dev" \
--sku "Standard_LRS" \
--kind "StorageV2" \
--location "eastus" \
--tags Environment=dev CostCenter=DevOps Owner=cloud-team
# Staging (ZRS - zone resilience)
az storage account create \
--name "stdevopsstaging01" \
--resource-group "rg-staging" \
--sku "Standard_ZRS" \
--kind "StorageV2" \
--location "eastus" \
--tags Environment=staging CostCenter=DevOps Owner=cloud-team
# Production (GRS - geo-replication)
az storage account create \
--name "stdevopsprod01" \
--resource-group "rg-prod" \
--sku "Standard_GRS" \
--kind "StorageV2" \
--location "eastus" \
--min-tls-version TLS1_2 \
--allow-blob-public-access false \
--https-only true \
--tags Environment=prod CostCenter=DevOps Owner=cloud-team
# ===== CREATE CONTAINERS FOR ARTIFACTS =====
# Container for build artifacts
az storage container create \
--name "build-artifacts" \
--account-name "stdevopsdev01" \
--public-access off
# Upload an artifact
az storage blob upload \
--account-name "stdevopsdev01" \
--container-name "build-artifacts" \
--name "app-v1.2.3.zip" \
--file "./dist/app-v1.2.3.zip"
# ===== AZURE POLICY TO ENFORCE TAGS =====
# Create a policy that enforces "Environment" and "Owner" tags
cat > required-tags-policy.json << 'EOF'
{
"properties": {
"displayName": "Required tags: Environment and Owner",
"mode": "Indexed",
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"notEquals": "Microsoft.Resources/resourceGroups"
},
{
"anyOf": [
{
"field": "tags[Environment]",
"exists": false
},
{
"field": "tags[Owner]",
"exists": false
}
]
}
]
},
"then": {
"effect": "Deny"
}
}
}
}
EOF
az policy definition create \
--name "required-tags" \
--rules required-tags-policy.json \
--mode Indexed
az policy assignment create \
--name "require-tags-all-resources" \
--scope "/subscriptions/{sub-id}" \
--policy "required-tags"
5. VMs as Build Agents
Roles of VMs in DevOps
graph TD
subgraph ROLES["Three DevOps VM Roles"]
BA["Build Agents\n(Persistent VMs)\nCustom SDKs\nNo reset after run"]
TT["Test Targets\n(Realistic environments)\nValidate before production\nIsolate tests"]
PROD["Production Targets\n(Production servers)\nDirect deployment\nHigh availability"]
end
PIPELINE[Azure Pipelines] -->|Build code| BA
PIPELINE -->|Deploy & test| TT
PIPELINE -->|Release| PROD
style BA fill:#3b82f6,color:#fff
style TT fill:#f59e0b,color:#000
style PROD fill:#ef4444,color:#fff
When to use self-hosted agents (VMs) vs Microsoft-hosted agents:
| Criteria | Microsoft-Hosted | Self-Hosted (VM) |
|---|---|---|
| Setup complexity | ⭐ Zero | ⭐⭐⭐ Moderate |
| Maintenance | ✅ None | ⚠️ Your responsibility |
| Custom tools | ❌ Limited | ✅ Complete |
| Private network | ❌ Internet only | ✅ VNet integrated |
| Cost | Pay per minute | VM always active |
| Reset | ✅ After each run | ❌ Persistent |
| Availability | ✅ Immediate | Depends on scaling |
Provisioning a Full DevOps VM Agent
# ===== CREATE A BUILD AGENT VM =====
RESOURCE_GROUP="rg-dev"
VM_NAME="vm-devops-agent01"
LOCATION="eastus"
VNET_NAME="vnet-dev"
SUBNET_NAME="subnet-agents"
ADMIN_USERNAME="azureagent"
# Create the Ubuntu VM for the agent
az vm create \
--resource-group $RESOURCE_GROUP \
--name $VM_NAME \
--image "Ubuntu2404" \
--size "Standard_D4s_v3" \
--admin-username $ADMIN_USERNAME \
--generate-ssh-keys \
--vnet-name $VNET_NAME \
--subnet $SUBNET_NAME \
--assign-identity \
--os-disk-caching ReadWrite \
--os-disk-size-gb 80 \
--location $LOCATION \
--tags Environment=dev Role=build-agent
# Enable diagnostics (boot diagnostics)
az vm boot-diagnostics enable \
--resource-group $RESOURCE_GROUP \
--name $VM_NAME
# ===== INSTALL DEVOPS TOOLS ON THE VM =====
# Use the Custom Script extension to automate installation
az vm extension set \
--resource-group $RESOURCE_GROUP \
--vm-name $VM_NAME \
--name customScript \
--publisher Microsoft.Azure.Extensions \
--settings '{
"commandToExecute": "bash -c '\''
# Update packages
apt-get update && apt-get upgrade -y
# Install Docker
curl -fsSL https://get.docker.com | sh
usermod -aG docker azureagent
# Install Azure CLI
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
# Install Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
# Install .NET 8
wget https://packages.microsoft.com/config/ubuntu/24.04/packages-microsoft-prod.deb
dpkg -i packages-microsoft-prod.deb
apt-get update && apt-get install -y dotnet-sdk-8.0
# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
# Install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
echo "Tool installation complete!"
'\'''
}'
Azure VM Applications: Tool Versioning
VM Applications allow managing tools as versioned artifacts via the Azure Compute Gallery.
# ===== CREATE A VM APPLICATION =====
# 1. Create an Azure Compute Gallery
az sig create \
--resource-group rg-shared \
--gallery-name devops-tools-gallery \
--description "VM Applications for DevOps agents"
# 2. Create an application definition
az sig gallery-application create \
--resource-group rg-shared \
--gallery-name devops-tools-gallery \
--name azure-cli \
--os-type Linux \
--description "Azure CLI tool for DevOps agents"
# 3. Upload the package to a Storage Account
# (The package is an archive with install.sh and uninstall.sh)
az storage blob upload \
--account-name stdevopstools \
--container-name applications \
--name "azure-cli-2.73.0.tar.gz" \
--file "./packages/azure-cli-2.73.0.tar.gz"
# 4. Create an application version
az sig gallery-application version create \
--resource-group rg-shared \
--gallery-name devops-tools-gallery \
--gallery-application-name azure-cli \
--version-name "2.73.0" \
--package-file-link "https://stdevopstools.blob.core.windows.net/applications/azure-cli-2.73.0.tar.gz" \
--install-command "./install.sh" \
--remove-command "./uninstall.sh" \
--target-regions eastus
# 5. Apply the VM Application to the agent VM
az vm application set \
--resource-group rg-dev \
--name vm-devops-agent01 \
--app-version-ids "/subscriptions/{sub}/resourceGroups/rg-shared/providers/Microsoft.Compute/galleries/devops-tools-gallery/applications/azure-cli/versions/2.73.0"
Register the VM as a Self-Hosted Azure DevOps Agent
# On the VM itself (via SSH or Custom Script Extension)
# Create a directory for the agent
mkdir ~/azagent && cd ~/azagent
# Download the Azure DevOps agent
AGENT_VERSION="3.248.0"
curl -L "https://vstsagentpackage.azureedge.net/agent/${AGENT_VERSION}/vsts-agent-linux-x64-${AGENT_VERSION}.tar.gz" \
-o agent.tar.gz
tar xzf agent.tar.gz
# Configure the agent (using a PAT or Service Connection)
./config.sh \
--unattended \
--url "https://dev.azure.com/{organization}" \
--auth pat \
--token "{PAT_TOKEN}" \
--pool "SelfHosted-Linux" \
--agent "vm-devops-agent01" \
--acceptTeeEula
# Install as a system service
sudo ./svc.sh install
sudo ./svc.sh start
# Check the status
sudo ./svc.sh status
6. Azure Container Registry (ACR)
ACR Architecture in a DevOps Pipeline
sequenceDiagram
participant DEV as Developer
participant PIPELINE as Azure Pipeline
participant ACR as Azure Container\nRegistry
participant AKS as AKS Cluster
DEV->>PIPELINE: Push code to Azure Repos
PIPELINE->>PIPELINE: Build Docker image
PIPELINE->>ACR: Push image (AcrPush role)\nmyregistry.azurecr.io/myapp:v1.2.3
ACR-->>PIPELINE: Image stored & tagged
PIPELINE->>AKS: Deploy new manifest\n(kubectl apply)
AKS->>ACR: Pull image (AcrPull via MI)
ACR-->>AKS: Image pulled
AKS-->>PIPELINE: Deployment successful
# ===== COMPLETE ACR SETUP =====
ACR_NAME="mydevopsregistry"
RESOURCE_GROUP="rg-shared"
LOCATION="eastus"
# Create ACR (without admin user - use RBAC instead)
az acr create \
--name $ACR_NAME \
--resource-group $RESOURCE_GROUP \
--sku Standard \
--location $LOCATION \
--admin-enabled false # Best practice: disable admin
# Enable Azure Defender for ACR (vulnerability scanning)
az security pricing create \
--name ContainerRegistry \
--tier Standard
# ===== CONFIGURE ACCESS VIA RBAC =====
ACR_ID=$(az acr show --name $ACR_NAME --query id --output tsv)
# Give AcrPush to the CI pipeline Service Principal
az role assignment create \
--role "AcrPush" \
--assignee "pipeline-sp-client-id" \
--scope $ACR_ID
# Give AcrPull to the AKS Managed Identity
AKS_MI=$(az aks show \
--resource-group rg-prod \
--name aks-prod \
--query "identityProfile.kubeletidentity.objectId" \
--output tsv)
az role assignment create \
--role "AcrPull" \
--assignee $AKS_MI \
--scope $ACR_ID
# ===== BUILD AND PUSH VIA ACR TASKS =====
# Simple build from the current directory
az acr build \
--registry $ACR_NAME \
--image "myapp:$(git rev-parse --short HEAD)" \
.
# Build with arguments
az acr build \
--registry $ACR_NAME \
--image "myapp:v1.2.3" \
--build-arg NODE_ENV=production \
--file Dockerfile.prod \
.
# ===== MANAGE IMAGES =====
# List repositories
az acr repository list --name $ACR_NAME --output table
# List tags for an image
az acr repository show-tags \
--name $ACR_NAME \
--repository myapp \
--orderby time_desc \
--output table
# Delete untagged images (cleanup)
az acr run \
--registry $ACR_NAME \
--cmd "acr purge --filter 'myapp:.*' --ago 30d --untagged" \
/dev/null
# ===== ATTACH ACR TO AKS =====
az aks update \
--resource-group rg-prod \
--name aks-prod \
--attach-acr $ACR_NAME
# Verify the attachment
az aks check-acr \
--resource-group rg-prod \
--name aks-prod \
--acr $ACR_NAME
ACR Roles:
| Role | Description | Assigned To |
|---|---|---|
| AcrPush | Push and pull images | CI pipelines (SP or MI) |
| AcrPull | Pull images only | AKS MI, build agents |
| AcrDelete | Delete images/tags | Cleanup pipelines |
| AcrImageSigner | Sign images | Release pipelines |
| Owner/Contributor | Full management | Admins only |
7. AKS vs ACI as Deployment Targets
graph TD
PIPELINE[Azure Pipeline\nRelease Stage]
subgraph ACI_SCENARIO["ACI - Fast CI Tests"]
ACI[Azure Container\nInstances]
TEST["Integration Tests\n(a few minutes)"]
DELETE[Delete instance\nafter tests]
end
subgraph AKS_SCENARIO["AKS - Production Deployment"]
AKS[Azure Kubernetes\nService]
ROLLING["Rolling Update\n(zero downtime)"]
MONITOR[Monitoring\nand scaling]
end
PIPELINE -->|"az container create\n(quick test)"| ACI_SCENARIO
PIPELINE -->|"kubectl apply\n(prod deployment)"| AKS_SCENARIO
ACI_SCENARIO --> DELETE
AKS_SCENARIO --> MONITOR
style ACI fill:#f59e0b,color:#000
style AKS fill:#3b82f6,color:#fff
# ===== CREATE AN AKS CLUSTER WITH MANAGED IDENTITY =====
az aks create \
--resource-group rg-prod \
--name aks-prod \
--node-count 3 \
--node-vm-size Standard_D4s_v3 \
--enable-managed-identity \
--enable-oidc-issuer \
--enable-workload-identity \
--network-plugin azure \
--network-policy azure \
--enable-azure-rbac \
--enable-aad \
--aad-admin-group-object-ids "admins-group-id" \
--auto-upgrade-channel stable \
--generate-ssh-keys
# Attach ACR to the AKS cluster
az aks update \
--resource-group rg-prod \
--name aks-prod \
--attach-acr mydevopsregistry
# Get kubectl credentials
az aks get-credentials \
--resource-group rg-prod \
--name aks-prod \
--overwrite-existing
# ===== DEPLOYMENT FROM A PIPELINE =====
# In an Azure DevOps pipeline:
# task: KubernetesManifest@1
# inputs:
# action: deploy
# kubernetesServiceConnection: aks-prod-connection
# namespace: production
# manifests: k8s/deployment.yaml
# containers: mydevopsregistry.azurecr.io/myapp:$(Build.BuildId)
# Via direct CLI in the pipeline:
kubectl set image deployment/myapp \
myapp=mydevopsregistry.azurecr.io/myapp:v1.2.3 \
-n production \
--record
# Verify the deployment
kubectl rollout status deployment/myapp -n production
8. Monitoring with Azure Monitor and Application Insights
DevOps Monitoring Architecture
graph TD
subgraph SOURCES["Data Sources"]
VMS["Build Agent VMs\n(Azure Monitor Agent)"]
AKS_SRC["AKS Cluster\n(Container Insights)"]
APP["Applications\n(App SDK)"]
PIPE["Azure Pipelines\n(Logs)"]
end
subgraph STORAGE["Storage"]
LAW["Log Analytics\nWorkspace"]
APPI["Application Insights\n(telemetry)"]
end
subgraph ANALYZE["Analysis and Visualization"]
KQL["KQL Queries"]
DASH["Azure Dashboards"]
WORKBOOK["Azure Workbooks"]
end
subgraph ACTIONS["Actions"]
ALERTS["Azure Monitor Alerts"]
EMAIL["Email/SMS"]
FUNC["Azure Function\n(auto-remediation)"]
TEAMS["Teams/Slack\nWebhook"]
end
SOURCES --> STORAGE
STORAGE --> ANALYZE
ANALYZE --> ACTIONS
style SOURCES fill:#1e3a5f,color:#fff
style STORAGE fill:#f59e0b,color:#000
style ANALYZE fill:#10b981,color:#fff
style ACTIONS fill:#ef4444,color:#fff
KQL Queries for DevOps
// ===== BUILD AGENT PERFORMANCE =====
// CPU of build agent VMs
InsightsMetrics
| where Namespace == "Processor"
and Name == "UtilizationPercentage"
and Computer startswith "vm-devops-agent"
| summarize AvgCPU=avg(Val), MaxCPU=max(Val)
by Computer, bin(TimeGenerated, 5m)
| order by TimeGenerated desc
| render timechart
// Available memory on agents
InsightsMetrics
| where Namespace == "Memory"
and Name == "AvailableMB"
and Computer startswith "vm-devops-agent"
| summarize AvgMemMB=avg(Val)
by Computer, bin(TimeGenerated, 5m)
| render timechart
// ===== AKS CONTAINER LOGS =====
// Errors in production pods
ContainerLog
| where TimeGenerated > ago(1h)
| where Namespace == "production"
| where LogEntry contains "ERROR" or LogEntry contains "EXCEPTION"
| project TimeGenerated, ContainerName, LogEntry
| order by TimeGenerated desc
// Resources consumed by pods
KubePodInventory
| where Namespace == "production"
| summarize count() by Node, PodStatus
| render piechart
// ===== APPLICATION INSIGHTS =====
// Request success rate by endpoint
requests
| where timestamp > ago(1h)
| summarize
Total=count(),
Failures=countif(success == false),
AvgDuration=avg(duration)
by name
| extend SuccessRate = round(100.0 * (Total - Failures) / Total, 2)
| order by SuccessRate asc
// Exceptions by type
exceptions
| where timestamp > ago(24h)
| summarize count() by type
| order by count_ desc
| take 10
Configure Azure Monitor Alerts for Agents
# ===== CREATE A CPU ALERT FOR BUILD AGENTS =====
# Action group (notifications)
az monitor action-group create \
--resource-group rg-dev \
--name devops-alerts-group \
--short-name devops \
--action email devops-team devops@contoso.com \
--action webhook teams-webhook "https://teams.microsoft.com/webhook/..."
ACTION_GROUP_ID=$(az monitor action-group show \
--resource-group rg-dev \
--name devops-alerts-group \
--query id \
--output tsv)
VM_ID=$(az vm show \
--resource-group rg-dev \
--name vm-devops-agent01 \
--query id \
--output tsv)
# Alert: CPU > 90%
az monitor alert create \
--resource-group rg-dev \
--name "BuildAgent-CPU-High" \
--target $VM_ID \
--condition "avg Percentage CPU > 90" \
--window-size 5m \
--evaluation-frequency 1m \
--action $ACTION_GROUP_ID \
--severity 2 \
--description "Build agent CPU > 90% for 5 minutes"
# ===== APPLICATION INSIGHTS =====
# Create a Log Analytics workspace
az monitor log-analytics workspace create \
--resource-group rg-shared \
--workspace-name devops-logs \
--sku PerGB2018
# Create Application Insights
az monitor app-insights component create \
--app devops-monitoring \
--location eastus \
--resource-group rg-shared \
--workspace "/subscriptions/{sub}/resourceGroups/rg-shared/providers/Microsoft.OperationalInsights/workspaces/devops-logs"
# Retrieve the connection string for instrumentation
az monitor app-insights component show \
--app devops-monitoring \
--resource-group rg-shared \
--query connectionString \
--output tsv
9. Complete Code Examples
Full Azure DevOps Pipeline for a Node.js Application
# azure-pipelines.yml
# Full pipeline: Build → Test → Push ACR → Deploy AKS
trigger:
branches:
include:
- main
- develop
paths:
exclude:
- docs/
- '*.md'
variables:
# Azure configuration
azureSubscription: 'AzureServiceConnection-DevOps'
acrName: 'mydevopsregistry'
aksClusterName: 'aks-prod'
aksResourceGroup: 'rg-prod'
# Application configuration
imageRepository: 'myapp'
containerRegistry: '$(acrName).azurecr.io'
dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
tag: '$(Build.BuildId)'
# Environment
${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
environment: 'production'
namespace: 'production'
${{ else }}:
environment: 'staging'
namespace: 'staging'
stages:
# ============================================
# STAGE 1: Build and Tests
# ============================================
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildAndTest
displayName: 'Build, Test & Security Scan'
pool:
name: 'SelfHosted-Linux' # Self-hosted agent in VNet
# Or use a Microsoft-hosted agent:
# vmImage: 'ubuntu-latest'
steps:
- checkout: self
fetchDepth: 0 # Required for git history
- task: NodeTool@0
displayName: 'Install Node.js 20'
inputs:
versionSpec: '20.x'
- script: |
npm ci
npm run lint
npm run test -- --coverage
displayName: 'Install, Lint and Test'
- task: PublishTestResults@2
displayName: 'Publish Test Results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/junit.xml'
- task: PublishCodeCoverageResults@2
displayName: 'Publish Code Coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: 'coverage/cobertura-coverage.xml'
# ===== BUILD DOCKER IMAGE =====
- task: Docker@2
displayName: 'Build Docker image'
inputs:
command: 'build'
repository: '$(containerRegistry)/$(imageRepository)'
tags: |
$(tag)
latest
# ===== VULNERABILITY SCAN =====
- task: AquaSecurityTrivy@1
displayName: 'Trivy Security Scan'
inputs:
version: 'latest'
image: '$(containerRegistry)/$(imageRepository):$(tag)'
exitCode: 1
severity: 'HIGH,CRITICAL'
# ===== PUSH TO ACR =====
- task: Docker@2
displayName: 'Push to ACR'
inputs:
command: 'push'
repository: '$(containerRegistry)/$(imageRepository)'
containerRegistry: $(azureSubscription)
tags: |
$(tag)
${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
latest
# ============================================
# STAGE 2: Deployment
# ============================================
- stage: Deploy
displayName: 'Deploy to $(environment)'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeployToAKS
displayName: 'Deploy to AKS'
pool:
name: 'SelfHosted-Linux'
environment: $(environment)
strategy:
runOnce:
deploy:
steps:
- task: KubernetesManifest@1
displayName: 'Create/Update deployment'
inputs:
action: deploy
kubernetesServiceConnection: $(aksClusterName)-connection
namespace: $(namespace)
manifests: |
$(Build.SourcesDirectory)/k8s/deployment.yaml
$(Build.SourcesDirectory)/k8s/service.yaml
containers: |
$(containerRegistry)/$(imageRepository):$(tag)
- task: Kubernetes@1
displayName: 'Verify deployment rollout'
inputs:
connectionType: 'Kubernetes Service Connection'
kubernetesServiceEndpoint: $(aksClusterName)-connection
namespace: $(namespace)
command: rollout
arguments: 'status deployment/myapp'
checkLatest: true
Bicep Infrastructure for the DevOps Environment
// devops-environment.bicep
// Full infrastructure provisioning for a DevOps environment
param environment string = 'dev'
param location string = resourceGroup().location
param devopsOrganizationName string
var prefix = '${environment}-${uniqueString(resourceGroup().id)}'
var tags = {
Environment: environment
ManagedBy: 'Bicep'
Purpose: 'DevOps Infrastructure'
}
// ===== NETWORKING =====
resource nsg 'Microsoft.Network/networkSecurityGroups@2023-05-01' = {
name: '${prefix}-nsg-agents'
location: location
tags: tags
properties: {
securityRules: [
{
name: 'AllowAzureDevOps'
properties: {
priority: 100
protocol: 'Tcp'
access: 'Allow'
direction: 'Inbound'
sourceAddressPrefix: 'AzureDevOps'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '443'
}
}
]
}
}
resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: '${prefix}-vnet'
location: location
tags: tags
properties: {
addressSpace: {
addressPrefixes: [environment == 'dev' ? '10.0.0.0/16' : '10.1.0.0/16']
}
subnets: [
{
name: 'agents-subnet'
properties: {
addressPrefix: environment == 'dev' ? '10.0.1.0/24' : '10.1.1.0/24'
networkSecurityGroup: {
id: nsg.id
}
}
}
]
}
}
// ===== STORAGE =====
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'st${replace(prefix, '-', '')}devops'
location: location
tags: tags
sku: {
name: environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
}
}
// ===== KEY VAULT =====
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: '${prefix}-kv'
location: location
tags: tags
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: 30
}
}
// ===== CONTAINER REGISTRY =====
resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
name: '${replace(prefix, '-', '')}acr'
location: location
tags: tags
sku: {
name: 'Standard'
}
properties: {
adminUserEnabled: false
publicNetworkAccess: 'Enabled'
}
}
// ===== LOG ANALYTICS =====
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: '${prefix}-logs'
location: location
tags: tags
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: environment == 'prod' ? 90 : 30
}
}
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: '${prefix}-ai'
location: location
tags: tags
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalytics.id
}
}
// Outputs
output vnetId string = vnet.id
output acrLoginServer string = acr.properties.loginServer
output keyVaultUri string = keyVault.properties.vaultUri
output appInsightsConnectionString string = appInsights.properties.ConnectionString
10. Comparison Tables
Build Agents: Microsoft-hosted vs Self-hosted
| Criteria | Microsoft-Hosted | Self-Hosted (Azure VM) |
|---|---|---|
| Setup | ✅ Zero configuration | ⚠️ Initial configuration |
| Maintenance | ✅ Managed by Microsoft | ❌ Your responsibility |
| Available tools | Standard (Node, .NET, Java, Python…) | ✅ Fully customizable |
| Private network | ❌ No direct VNet access | ✅ In your VNet |
| Private endpoints | ❌ Requires configuration | ✅ Native |
| Max job duration | 6h (public) / 360h (private) | Unlimited |
| Reset | ✅ Fresh after each run | ❌ Persistent |
| Cost | Per pipeline minute | VM always active |
| Performance | Standard | Controllable (Premium SSD, large VM) |
Storage Redundancy by Environment
| Tier | Copies | Zones | Regions | SLA | Cost |
|---|---|---|---|---|---|
| LRS | 3 | 1 | 1 | 99.9% | $ |
| ZRS | 3 | 3 | 1 | 99.99% | $$ |
| GRS | 6 | 1 | 2 | 99.999% | $$$ |
| GZRS | 6 | 3 | 2 | 99.9999% | $$$$ |
| RA-GRS | 6 | 1 | 2 (+ secondary read) | 99.999% | $$$ |
11. Glossary
| Term | Definition |
|---|---|
| RBAC | Role-Based Access Control — access control based on roles |
| Service Principal | Application identity in Entra ID with a secret or certificate |
| Managed Identity | Identity managed automatically by Azure — no secret to manage |
| System-Assigned MI | Managed Identity tied to a resource’s lifecycle |
| User-Assigned MI | Independent Managed Identity reusable by multiple resources |
| NSG | Network Security Group — firewall rules for subnets/interfaces |
| VNet Peering | Private connection between two Azure VNets (must have non-overlapping IPs) |
| CIDR | Classless Inter-Domain Routing — notation for IP ranges (e.g.: 10.0.0.0/16) |
| Self-Hosted Agent | Azure DevOps agent installed on a VM that you manage |
| Microsoft-Hosted Agent | Azure DevOps agent managed by Microsoft, billed per active minute |
| VM Applications | Versioned artifacts for installing tools on VMs via Azure Compute Gallery |
| ACR | Azure Container Registry — private registry for Docker images |
| AcrPush | ACR role allowing pushing images |
| AcrPull | ACR role allowing pulling images |
| LRS | Locally Redundant Storage — 3 copies in a single datacenter |
| ZRS | Zone-Redundant Storage — 3 copies across 3 availability zones |
| GRS | Geo-Redundant Storage — 6 copies across 2 Azure regions |
| GZRS | Geo-Zone-Redundant Storage — combination of GRS and ZRS |
| Azure Monitor | Centralized service for Azure metrics and logs |
| Application Insights | APM service for application telemetry (requests, exceptions, traces) |
| Log Analytics Workspace | Storage and analysis space for logs with KQL |
| KQL | Kusto Query Language — query language for Log Analytics |
| Container Insights | Azure Monitor extension for monitoring AKS clusters |
| Waterfall inheritance | RBAC permission inheritance from top (MG) to bottom (resources) |
| Role Assignment | Assignment of a role to a principal at a given scope |
| Role Definition | Set of permissions defining what a principal can do |
| Scope | Hierarchical level where the role assignment applies (MG/Sub/RG/Resource) |
| Azure Compute Gallery | Service for managing versioned VM images and applications |
| PAT | Personal Access Token — Azure DevOps access token for agents |
Search Terms
configure · devops · environment · azure · native · tools · iac · microsoft · architecture · agents · network · acr · agent · application · identities · monitor · monitoring · pipeline · rbac · redundancy · security · self-hosted · storage · vms