Intermediate

Configure a DevOps Environment with Azure Native Tools

A reference Azure DevOps environment — networking, RBAC, storage, ACR and AKS/ACI deployment targets.

Course: Configuring a DevOps Environment within Azure Native Tools
Level: Intermediate
Estimated Duration: 4–5 hours
Last Updated: June 2026

Table of Contents

  1. Azure DevOps Reference Architecture
  2. Networking and Network Security
  3. RBAC and Identities
  4. Azure Storage for DevOps
  5. VMs as Build Agents
  6. Azure Container Registry (ACR)
  7. AKS vs ACI as Deployment Targets
  8. Monitoring with Azure Monitor and Application Insights
  9. Complete Code Examples
  10. Comparison Tables
  11. 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:

ComponentAzure ServiceRole
Source ControlAzure Repos / GitHubCode management, PRs, branch policies
IdentityMicrosoft Entra IDAuthentication for developers and pipelines
CI/CDAzure PipelinesAutomated build, test and deployment
ArtifactsAzure ArtifactsInternal package feeds
Docker ImagesAzure Container RegistrySecure image storage
SecretsAzure Key VaultCentralized secret management
ComputeAKS, App Service, VMsDeployment targets
MonitoringAzure Monitor + App InsightsFull 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:

RoleRightsDevOps Use Case
OwnerEverything, including access managementCloud admins
ContributorCreate/modify all resourcesDevOps teams
ReaderRead-only on everythingDev teams (prod)
User Access AdministratorManage access onlyRBAC admins
AcrPushPush images to ACRCI build agents
AcrPullPull images from ACRDeployment agents
Key Vault Secrets UserRead KV secretsApplications, pipelines
Storage Blob Data ContributorCRUD on blobsBuild 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:

CriteriaMicrosoft-HostedSelf-Hosted (VM)
Setup complexity⭐ Zero⭐⭐⭐ Moderate
Maintenance✅ None⚠️ Your responsibility
Custom tools❌ Limited✅ Complete
Private network❌ Internet only✅ VNet integrated
CostPay per minuteVM always active
Reset✅ After each run❌ Persistent
Availability✅ ImmediateDepends 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:

RoleDescriptionAssigned To
AcrPushPush and pull imagesCI pipelines (SP or MI)
AcrPullPull images onlyAKS MI, build agents
AcrDeleteDelete images/tagsCleanup pipelines
AcrImageSignerSign imagesRelease pipelines
Owner/ContributorFull managementAdmins 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

CriteriaMicrosoft-HostedSelf-Hosted (Azure VM)
Setup✅ Zero configuration⚠️ Initial configuration
Maintenance✅ Managed by Microsoft❌ Your responsibility
Available toolsStandard (Node, .NET, Java, Python…)✅ Fully customizable
Private network❌ No direct VNet access✅ In your VNet
Private endpoints❌ Requires configuration✅ Native
Max job duration6h (public) / 360h (private)Unlimited
Reset✅ Fresh after each run❌ Persistent
CostPer pipeline minuteVM always active
PerformanceStandardControllable (Premium SSD, large VM)

Storage Redundancy by Environment

TierCopiesZonesRegionsSLACost
LRS31199.9%$
ZRS33199.99%$$
GRS61299.999%$$$
GZRS63299.9999%$$$$
RA-GRS612 (+ secondary read)99.999%$$$

11. Glossary

TermDefinition
RBACRole-Based Access Control — access control based on roles
Service PrincipalApplication identity in Entra ID with a secret or certificate
Managed IdentityIdentity managed automatically by Azure — no secret to manage
System-Assigned MIManaged Identity tied to a resource’s lifecycle
User-Assigned MIIndependent Managed Identity reusable by multiple resources
NSGNetwork Security Group — firewall rules for subnets/interfaces
VNet PeeringPrivate connection between two Azure VNets (must have non-overlapping IPs)
CIDRClassless Inter-Domain Routing — notation for IP ranges (e.g.: 10.0.0.0/16)
Self-Hosted AgentAzure DevOps agent installed on a VM that you manage
Microsoft-Hosted AgentAzure DevOps agent managed by Microsoft, billed per active minute
VM ApplicationsVersioned artifacts for installing tools on VMs via Azure Compute Gallery
ACRAzure Container Registry — private registry for Docker images
AcrPushACR role allowing pushing images
AcrPullACR role allowing pulling images
LRSLocally Redundant Storage — 3 copies in a single datacenter
ZRSZone-Redundant Storage — 3 copies across 3 availability zones
GRSGeo-Redundant Storage — 6 copies across 2 Azure regions
GZRSGeo-Zone-Redundant Storage — combination of GRS and ZRS
Azure MonitorCentralized service for Azure metrics and logs
Application InsightsAPM service for application telemetry (requests, exceptions, traces)
Log Analytics WorkspaceStorage and analysis space for logs with KQL
KQLKusto Query Language — query language for Log Analytics
Container InsightsAzure Monitor extension for monitoring AKS clusters
Waterfall inheritanceRBAC permission inheritance from top (MG) to bottom (resources)
Role AssignmentAssignment of a role to a principal at a given scope
Role DefinitionSet of permissions defining what a principal can do
ScopeHierarchical level where the role assignment applies (MG/Sub/RG/Resource)
Azure Compute GalleryService for managing versioned VM images and applications
PATPersonal 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

Interested in this course?

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