Everything for Azure in one place — pick a section below. 57 reviewed items across 4 content types.
Quick Answer
Azure reliability work starts with user-experience targets, then maps those targets to redundancy, monitoring, recovery, and testing. For App Service, that means deployment slots, health checks, zone or regional design, and alerting tied to customer-visible failure rather than only VM or process state.
Detailed Answer
Think of a busy airport: a spare runway is useful only if air traffic control knows when to route planes to it and has rehearsed the switch. Azure redundancy works the same way; capacity in another zone or region is only reliable when health signals, routing, and runbooks are tested.
Azure Well-Architected reliability guidance emphasizes resilience, recovery, operations, failure-mode analysis, and explicit reliability targets. In production, this pushes teams to design around flows such as checkout or login instead of around individual resources.
A typical App Service rollout uses a staging slot, warms the application, validates dependencies, then swaps traffic. Azure Monitor and Application Insights provide metrics, logs, traces, availability tests, and alert rules. Traffic Manager or Front Door can steer users when a region is unhealthy.
At scale, the important settings are health check path behavior, autoscale rules, dependency timeouts, and whether data stores are also resilient. Engineers watch failed requests, dependency duration, synthetic checks, saturation, and deployment annotations to separate bad code from platform or network failure.
The non-obvious gotcha is treating a green platform resource as proof the workload is reliable. A slot can be running while its downstream Key Vault, database, or private endpoint is broken. Senior operators test the full request path and practice rollback before an incident.
Code Example
az webapp deployment slot create --resource-group rg-prod-payments --name payments-api --slot staging # Creates an isolated staging slot for safe release validation.
az webapp config set --resource-group rg-prod-payments --name payments-api --slot staging --generic-configurations '{"healthCheckPath":"/healthz"}' # Points Azure health checks at the application readiness endpoint.
az webapp deployment slot swap --resource-group rg-prod-payments --name payments-api --slot staging --target-slot production # Swaps warmed staging traffic into production after validation.Interview Tip
A junior engineer typically answers with the feature name and a happy-path command, but for a senior/architect role, the interviewer is actually looking for production judgment. Explain where the Azure control plane stores intent, how changes are rolled out, what telemetry proves the system is healthy, and what failure mode creates the largest blast radius. Strong answers include rollback behavior, ownership boundaries, permissions, and the exact signal you would page on.
◈ Architecture Diagram
┌──────────┐
│ Signal │
└────┬─────┘
↓
┌──────────┐
│ Azure │
└────┬─────┘
↓
┌──────────┐
│ Action │
└──────────┘💬 Comments
Quick Answer
Classic pipelines use a GUI-based editor stored in Azure DevOps metadata, while YAML pipelines define CI/CD as code in a version-controlled file. YAML enables pull request reviews, template reuse, and multi-stage deployments in a single file.
Detailed Answer
Imagine two ways to give driving directions: a voice-guided GPS (Classic) where you click through turns on a screen, versus a written route card (YAML) that you can photocopy, annotate, version, and hand to anyone. Both get you to the destination, but the written card travels with the project and can be peer-reviewed before the trip.
Classic pipelines were the original Azure DevOps experience. Build definitions use a visual task editor where you drag-and-drop tasks like NuGet Restore, MSBuild, or Docker Build. Release definitions add environments with deployment gates, approvals, and artifact triggers. The configuration is stored as JSON metadata inside Azure DevOps, not in your repository. This means pipeline changes do not go through pull requests, cannot be easily diffed, and are invisible in your Git history.
YAML pipelines store the entire pipeline definition in an azure-pipelines.yml file committed alongside your source code. Every change to the pipeline goes through the same pull request workflow as application code. YAML supports multi-stage pipelines (build, test, deploy to staging, deploy to production) in a single file with conditional execution, template references, and environment approvals. The extends keyword and template repositories enable centralized governance across hundreds of pipelines.
Under the hood, both pipeline types use the same agent infrastructure and task ecosystem. A Classic build task like DotNetCoreCLI@2 is the same task referenced in YAML as - task: DotNetCoreCLI@2. The difference is purely in how the orchestration is defined and stored.
In production, most organizations are migrating from Classic to YAML because Microsoft has signaled Classic pipelines will not receive new features. The gotcha is that Classic Release pipelines have some features (like release gates with Azure Monitor integration and graphical deployment visualization) that require extra YAML configuration using environments and checks. Teams migrating often underestimate the effort to replicate approval workflows, variable scoping, and artifact filtering that Classic provided through the GUI.
Code Example
# Classic pipeline equivalent in YAML — azure-pipelines.yml
# This replaces a Classic Build + Release definition pair
trigger:
branches:
include:
- main
- release/*
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
displayName: 'Build payments-api'
jobs:
- job: BuildJob
steps:
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
projects: 'src/payments-api/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build solution'
inputs:
command: 'build'
projects: 'src/payments-api/*.csproj'
arguments: '--configuration Release'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: 'test'
projects: 'tests/**/*.csproj'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: Build
jobs:
- deployment: DeployToStaging
environment: 'payments-staging'
strategy:
runOnce:
deploy:
steps:
- script: echo 'Deploying to staging environment'Interview Tip
A junior engineer typically says 'YAML is newer and better' without explaining the trade-offs. Interviewers want to hear that Classic still has valid use cases for teams with non-developer pipeline authors, and that migration requires mapping Classic release gates to YAML environment checks. Mention that Microsoft recommends YAML for all new pipelines and that Classic will eventually be deprecated. Strong answers reference the template system as the key advantage YAML has over Classic for enterprise governance at scale.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ Classic Pipeline │ │ ┌──────────┐ ┌──────────────┐ ┌─────────────┐ │ │ │ Build │───→│ Release Def │───→│ Environment │ │ │ │ (GUI) │ │ (GUI) │ │ (GUI) │ │ │ └──────────┘ └──────────────┘ └─────────────┘ │ │ Stored in Azure DevOps metadata (not in Git) │ └──────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────┐ │ YAML Pipeline │ │ ┌────────────────────────────────────────────────────┐ │ │ │ azure-pipelines.yml (in Git repo) │ │ │ │ stages: Build → Test → Deploy Staging → Deploy Prod│ │ │ └────────────────────────────────────────────────────┘ │ │ Versioned │ PR-reviewed │ Templated │ Multi-stage │ └──────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Create an azure-pipelines.yml file in your repo root defining a trigger, agent pool, and build steps. Azure DevOps detects the file and offers to create the pipeline, or you can use az pipelines create to wire it up via CLI.
Detailed Answer
Think of a YAML pipeline file like a recipe card you tape to your refrigerator. Anyone who opens the fridge (clones the repo) immediately knows exactly how to cook the dish (build the project) without asking the chef (searching through a GUI for hidden configuration).
The minimum viable YAML pipeline has three elements: a trigger that specifies which branches activate the pipeline, a pool that defines which agent runs the work, and steps that list the actual build commands. For a .NET project, the steps typically include restore, build, test, and publish. For Node.js, they include npm install, lint, test, and build. Azure DevOps provides starter templates when you create a new pipeline through the portal, automatically detecting your project type.
When the pipeline runs, Azure DevOps provisions a fresh agent from the specified pool. Microsoft-hosted agents come pre-installed with common SDKs (.NET, Node.js, Python, Java, Go) and tools (Docker, kubectl, Terraform). The agent clones your repository, executes each step sequentially, and reports results back. Build artifacts like compiled binaries or Docker images can be published for downstream stages.
In production, even a basic pipeline should include caching for package restore (to speed up builds from 5 minutes to 90 seconds), test result publishing (so failures appear in the PR UI), and branch filters (to avoid running on documentation-only branches). The variables section externalizes configuration like SDK versions so upgrades require changing one line.
The most common gotcha for beginners is indentation errors in YAML causing cryptic parse failures. Azure DevOps provides a YAML editor with IntelliSense in the portal, but many teams prefer editing locally with the Azure Pipelines VS Code extension that provides schema validation. Another frequent issue is the agent not having a required tool version — use the UseDotNet@2 or NodeTool@0 tasks to explicitly install the version you need rather than relying on whatever is pre-installed.
Code Example
# azure-pipelines.yml — Basic .NET build pipeline for payments-api
trigger:
- main
- feature/*
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.0.x'
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
packageType: 'sdk'
version: '$(dotnetVersion)'
- task: DotNetCoreCLI@2
displayName: 'Restore NuGet packages'
inputs:
command: 'restore'
projects: 'src/payments-api/**/*.csproj'
feedsToUse: 'select'
vstsFeed: 'contoso-internal-feed'
- task: DotNetCoreCLI@2
displayName: 'Build payments-api'
inputs:
command: 'build'
projects: 'src/payments-api/**/*.csproj'
arguments: '--configuration $(buildConfiguration) --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: 'test'
projects: 'tests/**/*.csproj'
arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage"'
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
---
# azure-pipelines.yml — Basic Node.js pipeline for fraud-detector
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
displayName: 'Use Node.js 20.x'
inputs:
versionSpec: '20.x'
- script: npm ci
displayName: 'Install dependencies (clean)'
- script: npm run lint
displayName: 'Run ESLint'
- script: npm run test -- --coverage
displayName: 'Run Jest tests with coverage'
- script: npm run build
displayName: 'Build production bundle'Interview Tip
A junior engineer typically shows a pipeline with only script steps (bash commands), missing the opportunity to demonstrate knowledge of built-in tasks like DotNetCoreCLI@2 or NodeTool@0 that provide structured inputs, retry logic, and telemetry integration. Interviewers look for awareness of caching, test result publishing, and explicit SDK version pinning. Mention that you would add a Cache@2 step for node_modules or NuGet packages to cut build times significantly, and that PublishTestResults makes failures visible directly in pull request comments.
◈ Architecture Diagram
┌─────────────────────────────────────────────────┐ │ Pipeline Execution Flow │ │ │ │ ┌─────────┐ ┌─────────┐ ┌──────────────┐ │ │ │ Trigger │──→│ Agent │──→│ Clone Repo │ │ │ │ (push) │ │ (pool) │ │ (checkout) │ │ │ └─────────┘ └─────────┘ └──────┬───────┘ │ │ │ │ │ ↓ │ │ ┌─────────┐ ┌─────────┐ ┌──────────────┐ │ │ │Publish │←──│ Test │←──│ Build │ │ │ │Artifacts│ │ Run │ │ Compile │ │ │ └─────────┘ └─────────┘ └──────────────┘ │ └─────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Service Connections are project-level credentials that securely store authentication details for external services like Azure subscriptions, AWS accounts, Kubernetes clusters, and Docker registries, allowing pipelines to deploy without exposing secrets in code.
Detailed Answer
Think of a Service Connection like a valet key for your car. You hand the parking attendant (pipeline) a key that can drive and park the car but cannot open the trunk or glove box. The valet never sees your master key, and you can revoke the valet key anytime without changing your locks.
Service Connections are configured at the project level under Project Settings and store credentials encrypted at rest. Each connection type has a specific authentication mechanism: Azure connections use Service Principals or Managed Identity with workload identity federation, AWS connections store access key ID and secret (or use assumed roles), Kubernetes connections use kubeconfig or service accounts, and Docker connections use registry credentials or service principals.
When a pipeline references a service connection, the Azure DevOps agent decrypts the credentials at runtime and injects them into the task execution context. The credentials never appear in logs (they are masked automatically) and are not accessible to other pipelines unless explicitly shared. Administrators control which pipelines can use a connection through security settings — you can require approval before a new pipeline uses a sensitive production connection.
In production environments, the best practice is workload identity federation for Azure connections, which eliminates stored secrets entirely by using OIDC token exchange. For AWS, teams use short-lived assumed roles via the AWS Toolkit task rather than long-lived access keys. For Kubernetes, connections should use namespace-scoped service accounts with RBAC rather than cluster-admin tokens.
The critical gotcha is the 'Grant access permission to all pipelines' checkbox that appears when creating a connection. Checking this allows any pipeline in the project to use the connection without approval, which is a security risk for production credentials. Senior engineers always uncheck this and configure pipeline-level permissions explicitly, especially for connections to production AWS accounts or Kubernetes clusters.
Code Example
# Create an Azure Resource Manager service connection using workload identity federation
az devops service-endpoint create --service-endpoint-configuration - <<EOF
{
"data": {},
"name": "payments-prod-azure",
"type": "azurerm",
"url": "https://management.azure.com/",
"authorization": {
"parameters": {
"tenantid": "$(TENANT_ID)",
"serviceprincipalid": "$(SP_CLIENT_ID)",
"authenticationType": "spnCertificate",
"servicePrincipalCertificate": "$(SP_CERT)"
},
"scheme": "ServicePrincipal"
},
"serviceEndpointProjectReferences": [{
"projectReference": { "name": "payments-platform" },
"name": "payments-prod-azure"
}]
}
EOF
# Using service connection in YAML pipeline for Azure deployment
stages:
- stage: DeployProd
jobs:
- deployment: DeployPaymentsAPI
environment: 'payments-production'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Deploy payments-api to App Service'
inputs:
azureSubscription: 'payments-prod-azure' # service connection name
appName: 'payments-api-prod'
package: '$(Pipeline.Workspace)/drop/*.zip'
# AWS service connection usage in pipeline
- task: AWSShellScript@1
displayName: 'Deploy fraud-detector to ECS'
inputs:
awsCredentials: 'fraud-detector-aws-prod' # AWS service connection
regionName: 'us-east-1'
scriptType: 'inline'
inlineScript: |
aws ecs update-service --cluster fraud-prod --service fraud-detector --force-new-deployment
# Kubernetes service connection usage
- task: Kubernetes@1
displayName: 'Deploy to AKS cluster'
inputs:
connectionType: 'Kubernetes Service Connection'
kubernetesServiceEndpoint: 'settlements-aks-prod'
command: 'apply'
arguments: '-f manifests/settlements-processor.yaml'Interview Tip
A junior engineer typically describes service connections as 'where you put your credentials' without discussing the security model. Interviewers want to hear about least-privilege principles: workload identity federation over stored secrets, pipeline-level access control rather than 'grant to all pipelines', and the approval workflow when a new pipeline first references a restricted connection. Mention that you audit service connection usage through the Azure DevOps audit log and rotate credentials on a schedule. This shows security awareness beyond basic functionality.
◈ Architecture Diagram
┌───────────────────────────────────────────────────────┐ │ Azure DevOps Project │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Service Connections │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐│ │ │ │ │ Azure │ │ AWS │ │ Kubernetes ││ │ │ │ │ SP/WIF │ │ AssumeRole│ │ ServiceAccount ││ │ │ │ └────┬─────┘ └────┬─────┘ └────────┬─────────┘│ │ │ └───────┼─────────────┼────────────────┼──────────┘ │ │ │ │ │ │ │ ┌───────┴─────────────┴────────────────┴──────────┐ │ │ │ Pipeline (YAML) │ │ │ │ azureSubscription: 'payments-prod-azure' │ │ │ │ awsCredentials: 'fraud-detector-aws-prod' │ │ │ │ kubernetesServiceEndpoint: 'settlements-aks' │ │ │ └─────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure DevOps Repos supports Git branching strategies with configurable branch policies that enforce code review, build validation, linked work items, and comment resolution before pull requests can merge into protected branches.
Detailed Answer
Think of branch policies like the rules at an airport security checkpoint. Everyone must go through the same process — show ID (linked work item), pass the scanner (build validation), and get cleared by an agent (code reviewer) — before entering the departure gate (merging to main). No exceptions, no shortcuts, regardless of seniority.
Azure DevOps branch policies are configured per branch or branch pattern (like main, release/*, or hotfix/*). The most critical policies are: minimum number of reviewers (typically 2 for main), build validation (a CI pipeline must pass), linked work items (traceability), comment resolution (all threads must be resolved), and merge strategy restrictions (squash-only or semi-linear). These policies cannot be bypassed by regular contributors — only users with 'Exempt from policy enforcement' permission can force-push or bypass.
Pull requests in Azure DevOps provide a rich review experience with file-by-file diffs, inline comments, suggestion blocks (that reviewers can create and authors can apply with one click), iteration tracking (showing what changed between review rounds), and auto-complete (which merges automatically once all policies are satisfied). Teams can set up required reviewers for specific paths — for example, requiring a security team member to approve changes to files matching /src/auth/* or Dockerfile patterns.
The branching strategy most teams adopt is trunk-based development with short-lived feature branches. Feature branches are created from main, developed for 1-3 days maximum, and merged back via pull request. Release branches are cut from main when stabilization is needed, and hotfix branches target both main and the active release branch.
The gotcha that catches teams is not configuring 'Reset code reviewer votes when there are new changes.' Without this, a reviewer can approve early, the author pushes breaking changes, and the PR merges with stale approval. Another common issue is build validation using a different pipeline than the one that deploys — teams should validate with the same pipeline that will run post-merge to catch integration issues early.
Code Example
# Set branch policy: require minimum 2 reviewers on main az repos policy approver-count create \ --branch main \ --repository-id $(az repos show --repository payments-api --query id -o tsv) \ --minimum-approver-count 2 \ --creator-vote-counts false \ --reset-on-source-push true \ --blocking true \ --enabled true # Set branch policy: require build validation az repos policy build create \ --branch main \ --repository-id $(az repos show --repository payments-api --query id -o tsv) \ --build-definition-id 42 \ --display-name "CI Build Validation" \ --queue-on-source-update-only true \ --valid-duration 720 \ --blocking true \ --enabled true # Set branch policy: require linked work items az repos policy work-item-linking create \ --branch main \ --repository-id $(az repos show --repository payments-api --query id -o tsv) \ --blocking true \ --enabled true # Create a pull request via CLI az repos pr create \ --repository payments-api \ --source-branch feature/add-retry-logic \ --target-branch main \ --title "Add exponential backoff retry to payment gateway calls" \ --description "Implements retry with jitter for transient 503 errors from payment processor" \ --work-items 1234 \ --reviewers [email protected] [email protected] # Set auto-complete on a PR (merges when all policies pass) az repos pr update --id 567 --auto-complete true --merge-strategy squash # Add required reviewer for security-sensitive paths az repos policy required-reviewer create \ --branch main \ --repository-id $(az repos show --repository payments-api --query id -o tsv) \ --required-reviewer-ids [email protected] \ --path-filter "/src/auth/*;/src/crypto/*;Dockerfile" \ --blocking true \ --enabled true
Interview Tip
A junior engineer typically mentions 'we use PRs and require reviews' without discussing the enforcement mechanism. Interviewers want specifics: how many reviewers, whether votes reset on new pushes, build validation configuration, and path-based required reviewers for sensitive areas. Mention the auto-complete feature that merges PRs automatically once policies pass — this shows workflow efficiency awareness. Also discuss merge strategies (squash for clean history vs. merge commits for traceability) and when each is appropriate for different branch patterns.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────┐ │ Pull Request Lifecycle │ │ │ │ feature/add-retry ──→ Create PR ──→ Policy Check │ │ │ │ │ ┌───────────┴──────────┐ │ │ │ Branch Policies │ │ │ │ ☐ Min 2 reviewers │ │ │ │ ☐ Build validation │ │ │ │ ☐ Work item linked │ │ │ │ ☐ Comments resolved │ │ │ └───────────┬──────────┘ │ │ │ │ │ All pass? │ │ │ YES ────→ Merge to main │ │ NO ────→ Block merge │ └──────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Environments represent deployment targets like staging or production. They support configurable checks including manual approvals, business hours gates, Azure Monitor alerts, REST API validations, and exclusive locks that must all pass before a pipeline stage can execute.
Detailed Answer
Think of an Environment in Azure DevOps like a secure room with multiple locks on the door. To enter (deploy), you need every lock opened: the manager must approve (manual approval), it must be during office hours (business hours check), the alarm system must show green (Azure Monitor check), and no one else can be in the room simultaneously (exclusive lock). Any single lock staying closed blocks entry entirely.
Environments are first-class resources in Azure DevOps that represent your deployment targets. Unlike Classic Release environments that existed only within a release definition, YAML environments are project-level resources with their own permissions, check configuration, and deployment history. When a deployment job references an environment, Azure DevOps evaluates all configured checks before allowing execution.
The available check types are: Manual Approval (one or more users/groups must approve with optional instructions), Business Hours (deployments only during specified windows like Mon-Fri 9am-4pm), Invoke Azure Function (call a custom function that returns pass/fail), Query Azure Monitor (check that alert rules are not firing), Invoke REST API (call any HTTP endpoint and evaluate the response), Required Template (ensure the pipeline uses an approved template), and Exclusive Lock (prevent parallel deployments to the same environment).
In production, mature teams layer multiple checks. A typical production environment has: manual approval from a tech lead (with a 4-hour timeout), business hours restriction (no Friday deployments), an Azure Monitor check verifying the staging environment is healthy, and an exclusive lock preventing two services from deploying simultaneously. The approval UI shows the deployment summary, associated work items, and code changes to help approvers make informed decisions.
The gotcha that bites teams is the interaction between checks and multi-stage pipelines. If a check fails or times out, the entire stage is marked as failed, but previous stages (like staging deployment) remain deployed. This can leave staging and production out of sync. Teams should design their pipelines to handle partial failures gracefully, either by auto-rolling-back staging or by clearly surfacing the divergence in dashboards.
Code Example
# Configure environment checks via Azure DevOps REST API
# (Portal configuration is more common, but API enables automation)
# Create an environment
az devops invoke \
--area distributedtask \
--resource environments \
--route-parameters project=payments-platform \
--http-method POST \
--api-version 7.1 \
--in-file - <<EOF
{
"name": "payments-production",
"description": "Production environment for payments-api with approval gates"
}
EOF
# Pipeline YAML referencing environment with deployment job
stages:
- stage: DeployProduction
displayName: 'Production Deployment'
jobs:
- deployment: DeployPaymentsAPI
displayName: 'Deploy payments-api to production'
pool:
vmImage: 'ubuntu-latest'
environment: 'payments-production' # Checks evaluated here
strategy:
runOnce:
preDeploy:
steps:
- script: |
echo "Running pre-deployment health check"
curl -f https://payments-api-staging.contoso.com/healthz
displayName: 'Verify staging is healthy'
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'payments-prod-azure'
appName: 'payments-api-prod'
package: '$(Pipeline.Workspace)/drop/*.zip'
deploymentMethod: 'zipDeploy'
postRouteTraffic:
steps:
- script: |
echo "Running smoke tests against production"
newman run tests/postman/smoke-tests.json --environment prod
displayName: 'Production smoke tests'
on:
failure:
steps:
- task: AzureAppServiceManage@0
displayName: 'Rollback - swap slots back'
inputs:
azureSubscription: 'payments-prod-azure'
Action: 'Swap Slots'
WebAppName: 'payments-api-prod'
ResourceGroupName: 'rg-payments-prod'
SourceSlot: 'production'
SwapWithProduction: true
# Environment with Kubernetes resource tracking
stages:
- stage: DeploySettlements
jobs:
- deployment: DeploySettlementsProcessor
environment: 'settlements-production.payments-namespace' # environment.namespace
strategy:
runOnce:
deploy:
steps:
- task: KubernetesManifest@0
inputs:
action: 'deploy'
manifests: 'manifests/settlements-processor.yaml'Interview Tip
A junior engineer typically conflates environments with stages or describes approvals as a simple yes/no button. Senior-level answers explain the layered check system: manual approval provides human judgment, business hours prevents off-hours incidents, Azure Monitor prevents deploying when the system is already degraded, and exclusive locks prevent deployment races. Mention that environment permissions are separate from pipeline permissions — a developer can trigger a pipeline but only designated approvers can allow production deployment. Discuss the audit trail that environments provide for compliance (SOC2, PCI) and how deployment history shows who approved what and when.
◈ Architecture Diagram
┌────────────────────────────────────────────────────────────┐ │ Environment: payments-production │ │ │ │ Checks (ALL must pass): │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ 1. Manual Approval → Tech Lead + SRE (4hr timeout)│ │ │ │ 2. Business Hours → Mon-Thu 9am-4pm EST │ │ │ │ 3. Azure Monitor → No active P1 alerts │ │ │ │ 4. Exclusive Lock → No concurrent deployments │ │ │ │ 5. Required Template → Must extend approved base │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ Deployment History: │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ #142 → Approved by alice@contoso │ Jun 20 10:30am │ │ │ │ #139 → Approved by bob@contoso │ Jun 18 2:15pm │ │ │ └──────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Variable Groups store shared configuration across pipelines, can be linked to Azure Key Vault for automatic secret rotation, and pipeline variables marked as secret are masked in logs and not exposed to forked PR builds.
Detailed Answer
Think of secrets management in Azure DevOps like a hotel safe system. The hotel (Key Vault) stores all valuables in a central vault with audit logging, access policies, and rotation schedules. Guests (pipelines) access their valuables through a controlled reception desk (Variable Group linked to Key Vault) using their room key (service connection). They never carry the vault combination themselves — they just get temporary access when they need it.
Variable Groups are collections of key-value pairs defined at the project or library level. They can be referenced by multiple pipelines, creating a single source of truth for shared configuration like environment URLs, feature flags, or connection strings. Variable Groups have their own RBAC — you control which pipelines can access them and which users can edit them.
The Key Vault integration transforms Variable Groups from static storage into a dynamic secret proxy. When you link a Variable Group to an Azure Key Vault, the group does not store secret values — it references them. At pipeline runtime, Azure DevOps fetches the current secret values directly from Key Vault. This means secret rotation in Key Vault is automatically picked up by the next pipeline run without any pipeline changes. You select which Key Vault secrets to map into the Variable Group, and they appear as pipeline variables.
Pipeline-level secrets (variables marked as 'secret' in YAML or the UI) have special behavior: they are masked in all log output (replaced with ***), cannot be passed to scripts in forked repository PR builds (preventing secret exfiltration), and cannot be read as output variables without explicit opt-in. The isSecret flag on a variable means Azure DevOps actively prevents accidental exposure.
The critical gotcha is that Variable Groups linked to Key Vault require the pipeline's service connection to have GET permission on Key Vault secrets. If you use a different service connection for deployment than for Key Vault access, you need to grant permissions carefully. Another common issue is that Key Vault-linked variables do not expand in compile-time expressions (like ${{ variables.x }}) — they are only available at runtime ($(x)) because they are fetched during execution. This breaks template conditions that try to use secrets for branching logic.
Code Example
# azure-pipelines.yml — Using Variable Groups and Key Vault secrets
trigger:
- main
variables:
# Static variable group with non-secret config
- group: payments-api-config # Contains: API_BASE_URL, FEATURE_FLAG_RETRY
# Key Vault linked variable group (secrets fetched at runtime)
- group: payments-api-secrets # Linked to kv-payments-prod Key Vault
# Pipeline-level secret
- name: buildVersion
value: '$(Build.BuildId)'
stages:
- stage: Build
jobs:
- job: BuildAndTest
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
echo "API URL: $(API_BASE_URL)" # From static variable group
echo "DB Connection: $(db-connection-string)" # From Key Vault (masked in logs)
displayName: 'Show config (secrets auto-masked)'
- task: DotNetCoreCLI@2
displayName: 'Run tests with secrets'
inputs:
command: 'test'
projects: 'tests/**/*.csproj'
env:
CONNECTION_STRING: $(db-connection-string) # Key Vault secret as env var
PAYMENT_GATEWAY_KEY: $(payment-gateway-api-key) # Key Vault secret
---
# Create Variable Group via CLI
az pipelines variable-group create \
--name 'payments-api-config' \
--variables \
API_BASE_URL=https://api.payments.contoso.com \
FEATURE_FLAG_RETRY=true \
LOG_LEVEL=Information
# Add a secret variable to the group
az pipelines variable-group variable create \
--group-id 5 \
--name 'DATADOG_API_KEY' \
--value 'abc123secret' \
--secret true
# Create Key Vault linked Variable Group via REST API
# (CLI does not support Key Vault linking directly)
curl -X POST "https://dev.azure.com/contoso/payments-platform/_apis/distributedtask/variablegroups?api-version=7.1" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $(System.AccessToken)" \
-d '{
"name": "payments-api-secrets",
"type": "AzureKeyVault",
"variableGroupProjectReferences": [{
"projectReference": { "name": "payments-platform" },
"name": "payments-api-secrets"
}],
"providerData": {
"serviceEndpointId": "<service-connection-id>",
"vault": "kv-payments-prod"
},
"variables": {
"db-connection-string": { "enabled": true, "isSecret": true },
"payment-gateway-api-key": { "enabled": true, "isSecret": true },
"redis-password": { "enabled": true, "isSecret": true }
}
}'Interview Tip
A junior engineer typically uses inline secret variables and stops there. Interviewers want to hear about the full secret lifecycle: centralized storage in Key Vault, automatic rotation without pipeline changes, the security boundary between compile-time and runtime variable expansion, and protection against fork-based secret exfiltration. Mention that Key Vault-linked variables only work at runtime (not in template expressions), and that you should never echo secret variables directly — Azure DevOps masks them, but base64 encoding or character-by-character output can bypass masking. Discuss how you audit secret access through Key Vault's diagnostic logs.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────┐ │ Pipeline Runtime │ │ │ │ ┌────────────────┐ ┌─────────────────────────┐ │ │ │ Variable Group │ │ Variable Group │ │ │ │ (Static) │ │ (Key Vault Linked) │ │ │ │ │ │ │ │ │ │ API_BASE_URL │ │ ┌───────────────────┐ │ │ │ │ LOG_LEVEL │ │ │ Azure Key Vault │ │ │ │ │ FEATURE_FLAGS │ │ │ kv-payments-prod │ │ │ │ └───────┬────────┘ │ │ │ │ │ │ │ │ │ db-connection-str │ │ │ │ │ │ │ payment-gw-key │ │ │ │ │ │ │ redis-password │ │ │ │ │ │ └─────────┬─────────┘ │ │ │ │ └────────────┼────────────┘ │ │ │ │ │ │ ↓ ↓ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ Pipeline Variables (merged) │ │ │ │ $(API_BASE_URL) $(db-connection-string) │ │ │ │ Non-secrets visible │ Secrets masked: *** │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure Artifacts provides hosted package feeds that support NuGet, npm, Maven, Python, and Universal Packages. Teams publish internal packages from CI pipelines and consume them in other projects using feed URLs configured in nuget.config, .npmrc, or Docker registry endpoints.
Detailed Answer
Think of Azure Artifacts like a company cafeteria versus the public food court. The public food court (nuget.org, npmjs.com) has everything but no quality control over what your team eats. The company cafeteria (Artifacts feed) curates approved ingredients, serves internal-recipe dishes (private packages), and caches popular outside meals (upstream sources) so lunch is faster and consistent even if the outside food court has an outage.
Azure Artifacts feeds are containers that host packages. Each feed has a URL that package managers (NuGet CLI, npm, pip) use to restore and publish. Feeds support upstream sources — connections to public registries that cache packages on first use. This means if npmjs.com goes down, your builds still work because packages are cached in your feed. Upstream sources also enable package approval workflows where only vetted versions flow into your feed.
Publishing from a CI pipeline uses built-in tasks: NuGetCommand@2 with 'push' for NuGet, Npm@1 with 'publish' for npm, or Docker@2 for container images to an Azure Container Registry (which integrates with Artifacts views). The pipeline authenticates using the build service identity — no explicit credentials needed for same-organization feeds. Cross-organization access requires a PAT or service connection.
Consuming packages requires configuring the feed as a source. For NuGet, add the feed URL to nuget.config. For npm, configure .npmrc with the feed's registry URL. The Azure Artifacts Credential Provider handles authentication seamlessly in development environments (interactive login) and CI environments (system token). The key concept is 'views' — feeds have @release, @prerelease, and @local views that control package visibility, allowing you to publish to @local during CI and promote to @release after testing.
The production gotcha is feed permissions and cross-project access. By default, feeds are project-scoped and only accessible within the project. Organization-scoped feeds are accessible across projects but require careful permission management. Another common issue is npm feed authentication in Docker builds — you cannot use the credential provider inside a Dockerfile, so you must use a .npmrc with a PAT token passed as a build argument (not baked into the image layer).
Code Example
# azure-pipelines.yml — Publish NuGet package for shared payments library
trigger:
- main
paths:
include:
- 'src/Contoso.Payments.Shared/**'
pool:
vmImage: 'ubuntu-latest'
variables:
packageVersion: '2.1.$(Build.BuildId)'
steps:
- task: DotNetCoreCLI@2
displayName: 'Pack NuGet package'
inputs:
command: 'pack'
packagesToPack: 'src/Contoso.Payments.Shared/**/*.csproj'
versioningScheme: 'byEnvVar'
versionEnvVar: 'packageVersion'
configuration: 'Release'
- task: NuGetCommand@2
displayName: 'Push to Artifacts feed'
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
nuGetFeedType: 'internal'
publishVstsFeed: 'payments-platform/contoso-packages'
---
# azure-pipelines.yml — Publish npm package for fraud-detector SDK
steps:
- task: Npm@1
displayName: 'npm publish @contoso/fraud-detector-sdk'
inputs:
command: 'publish'
workingDir: 'packages/fraud-detector-sdk'
publishRegistry: 'useFeed'
publishFeed: 'payments-platform/contoso-npm-packages'
---
# nuget.config — Consuming internal feed in payments-api project
# <?xml version="1.0" encoding="utf-8"?>
# <configuration>
# <packageSources>
# <clear />
# <add key="contoso-packages" value="https://pkgs.dev.azure.com/contoso/payments-platform/_packaging/contoso-packages/nuget/v3/index.json" />
# <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
# </packageSources>
# </configuration>
---
# .npmrc — Consuming internal npm feed
# registry=https://pkgs.dev.azure.com/contoso/payments-platform/_packaging/contoso-npm-packages/npm/registry/
# always-auth=true
---
# Create artifact feed via CLI
az artifacts feed create \
--name contoso-packages \
--project payments-platform \
--description "Internal NuGet packages for payments team"
# Add upstream source (nuget.org) to feed
az artifacts feed update \
--name contoso-packages \
--project payments-platform
# Promote package to Release view after testing
az artifacts universal publish \
--organization https://dev.azure.com/contoso \
--project payments-platform \
--scope project \
--feed contoso-packages \
--name settlements-processor-config \
--version 1.0.$(Build.BuildId) \
--path ./config-bundle/Interview Tip
A junior engineer typically describes publishing a package but overlooks the consumption side and upstream source caching. Interviewers want to hear about the full package lifecycle: publish from CI, version strategy (SemVer with build ID), upstream sources for reliability and security scanning, feed views for promotion workflows (@local to @release), and cross-project feed access patterns. Mention the credential provider for local development authentication and the gotcha with Docker builds needing PAT-based .npmrc because the credential provider does not work inside containers. Discuss how you would handle breaking changes with major version bumps and pre-release packages.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────────┐ │ Azure Artifacts Feed Architecture │ │ │ │ ┌─────────────────────┐ ┌──────────────────────────┐ │ │ │ CI Pipeline │ │ Upstream Sources │ │ │ │ (payments-api) │ │ ┌────────────────────┐ │ │ │ │ npm publish │ │ │ nuget.org (cached) │ │ │ │ │ nuget push │────→│ │ npmjs.com (cached) │ │ │ │ └─────────────────────┘ │ └────────────────────┘ │ │ │ │ │ │ │ │ contoso-packages feed │ │ │ │ ┌────────────────────┐ │ │ │ │ │ @local (CI builds) │ │ │ │ ┌─────────────────────┐ │ │ @prerelease │ │ │ │ │ Consumer Pipeline │ │ │ @release (promoted)│ │ │ │ │ (fraud-detector) │←────│ └────────────────────┘ │ │ │ │ nuget restore │ └──────────────────────────┘ │ │ │ npm install │ │ │ └─────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Create a central template repository with parameterized stage, job, and step templates. Other repos reference templates using resources.repositories with a ref to a specific tag or branch, enabling centralized governance while allowing per-repo customization through parameters.
Detailed Answer
Think of pipeline templates like franchise operations manuals. McDonald's does not let each restaurant invent its own burger-making process — there is a central playbook that defines standard procedures (templates), but each location can customize the menu slightly (parameters) based on local regulations. The corporate team (platform engineering) updates the playbook centrally, and franchises (service teams) adopt changes by updating their reference version.
Azure DevOps YAML templates come in four flavors: step templates (reusable build steps), job templates (complete job definitions), stage templates (full stages with conditions and environments), and variable templates (shared variable definitions). The 'extends' keyword adds a fifth pattern — pipeline templates that constrain what child pipelines can do, enforcing security policies like required scanning steps that cannot be removed.
The central template repository (often called 'pipeline-templates' or 'shared-pipelines') contains versioned templates organized by concern: build templates per language (.NET, Node.js, Python), deploy templates per target (Kubernetes, App Service, Lambda), and security templates (SAST scanning, container scanning, secret detection). Each template accepts parameters that customize behavior — container registry, namespace, environment name, notification channels — while the core logic remains standard.
Consumer repositories reference the template repo using resources.repositories and call templates with the @templateRepo syntax. Version pinning is critical: teams reference a specific tag (refs/tags/v2.1) or branch (refs/heads/stable) rather than main, so template updates do not break consuming pipelines unexpectedly. The platform team releases new template versions through a PR process, tags the release, and teams upgrade by changing their ref.
The production gotcha at scale is template debugging. When a pipeline fails inside a template, the error points to the template file but developers cannot see the expanded YAML easily. Azure DevOps provides a 'Download full YAML' option in the pipeline run that shows the fully resolved pipeline after template expansion. Another challenge is parameter validation — templates should use parameter types (string, boolean, object, stepList) and allowed values to fail fast with clear errors rather than cryptic task failures deep in the pipeline. The extends pattern is essential for security: it prevents pipeline authors from adding arbitrary steps that bypass security scanning.
Code Example
# Template repo: shared-pipelines/templates/build-dotnet.yml
parameters:
- name: projectPath
type: string
- name: dotnetVersion
type: string
default: '8.0.x'
- name: publishArtifact
type: boolean
default: true
- name: sonarQubeEnabled
type: boolean
default: true
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK ${{ parameters.dotnetVersion }}'
inputs:
packageType: 'sdk'
version: '${{ parameters.dotnetVersion }}'
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
projects: '${{ parameters.projectPath }}/**/*.csproj'
feedsToUse: 'config'
nugetConfigPath: 'nuget.config'
- ${{ if eq(parameters.sonarQubeEnabled, true) }}:
- task: SonarQubePrepare@5
displayName: 'SonarQube analysis begin'
inputs:
SonarQube: 'sonarqube-connection'
scannerMode: 'MSBuild'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: '${{ parameters.projectPath }}/**/*.csproj'
arguments: '--configuration Release --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Run tests'
inputs:
command: 'test'
projects: 'tests/**/*.csproj'
arguments: '--configuration Release --collect:"XPlat Code Coverage"'
- ${{ if eq(parameters.publishArtifact, true) }}:
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
---
# Template repo: shared-pipelines/templates/deploy-kubernetes.yml
parameters:
- name: environment
type: string
- name: kubernetesServiceConnection
type: string
- name: namespace
type: string
- name: imageRepository
type: string
- name: imageTag
type: string
- name: manifestsPath
type: string
default: 'manifests/'
stages:
- stage: Deploy_${{ parameters.environment }}
displayName: 'Deploy to ${{ parameters.environment }}'
jobs:
- deployment: Deploy
pool:
vmImage: 'ubuntu-latest'
environment: '${{ parameters.environment }}'
strategy:
runOnce:
deploy:
steps:
- task: KubernetesManifest@0
displayName: 'Deploy to ${{ parameters.namespace }}'
inputs:
action: 'deploy'
kubernetesServiceConnection: '${{ parameters.kubernetesServiceConnection }}'
namespace: '${{ parameters.namespace }}'
manifests: '${{ parameters.manifestsPath }}/*.yaml'
containers: '${{ parameters.imageRepository }}:${{ parameters.imageTag }}'
---
# Consumer repo: payments-api/azure-pipelines.yml
resources:
repositories:
- repository: templates
type: git
name: platform-engineering/shared-pipelines
ref: refs/tags/v2.3 # Pin to specific version
trigger:
- main
stages:
- stage: Build
jobs:
- job: BuildPaymentsAPI
pool:
vmImage: 'ubuntu-latest'
steps:
- template: templates/build-dotnet.yml@templates
parameters:
projectPath: 'src/payments-api'
dotnetVersion: '8.0.x'
sonarQubeEnabled: true
- template: templates/deploy-kubernetes.yml@templates
parameters:
environment: 'payments-staging'
kubernetesServiceConnection: 'aks-staging'
namespace: 'payments'
imageRepository: 'contosoacr.azurecr.io/payments-api'
imageTag: '$(Build.BuildId)'
- template: templates/deploy-kubernetes.yml@templates
parameters:
environment: 'payments-production'
kubernetesServiceConnection: 'aks-production'
namespace: 'payments'
imageRepository: 'contosoacr.azurecr.io/payments-api'
imageTag: '$(Build.BuildId)'Interview Tip
A junior engineer typically shows a basic template with hardcoded values. Senior candidates discuss the governance model: how the platform team versions templates with tags, how consumer teams pin versions and upgrade deliberately, how the extends keyword enforces mandatory security steps that pipeline authors cannot remove, and how parameter validation prevents misconfiguration. Mention the debugging challenge — use 'Download full YAML' to see expanded pipelines — and discuss the organizational model where a platform engineering team owns the template repo and service teams consume via pull request-based version upgrades. Discuss template composition: step templates inside job templates inside stage templates for maximum flexibility.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────────┐ │ Template Repository Architecture │ │ │ │ shared-pipelines/ (central repo, tagged releases) │ │ ├── templates/ │ │ │ ├── build-dotnet.yml (step template) │ │ │ ├── build-nodejs.yml (step template) │ │ │ ├── deploy-kubernetes.yml (stage template) │ │ │ ├── deploy-appservice.yml (stage template) │ │ │ ├── security-scan.yml (step template) │ │ │ └── variables-common.yml (variable template) │ │ └── pipelines/ │ │ └── required-base.yml (extends template) │ │ │ │ Consumer repos reference via: │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ payments-api │ │fraud-detector│ │ settlements │ │ │ │ ref: v2.3 │ │ ref: v2.3 │ │ ref: v2.1 │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ └─────────────────┼─────────────────┘ │ │ ↓ │ │ template: build-dotnet.yml@templates │ └─────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure DevOps exposes a comprehensive REST API covering all services. Teams use it to automate project provisioning, create pipelines programmatically, configure branch policies, generate custom dashboards, and integrate with external tools using PATs or OAuth tokens for authentication.
Detailed Answer
Think of the Azure DevOps REST API like the plumbing behind the walls of a house. The UI (faucets and fixtures) is what most people see, but the pipes (API) are what power everything. When you need to install 50 identical bathrooms (provision 50 projects), you do not turn each faucet by hand — you connect the plumbing programmatically.
The Azure DevOps REST API follows a consistent pattern: https://dev.azure.com/{organization}/{project}/_apis/{area}/{resource}?api-version=7.1. Authentication uses Personal Access Tokens (PATs) for scripts, OAuth2 for web apps, or Managed Identity for Azure-hosted automation. The API covers every Azure DevOps feature: creating projects, managing repos, triggering pipelines, querying work items (WIQL), configuring policies, managing feeds, and reading audit logs.
For project provisioning automation, platform teams build scripts or Azure Functions that respond to events (new team onboarding, new service creation) and configure everything: create the project, set up repos with branch policies, create default pipelines from templates, configure service connections, add team members with appropriate permissions, and create initial work items. This 'project factory' pattern ensures consistency across hundreds of projects.
Reporting is a major API use case. While Azure DevOps provides built-in dashboards and Analytics views (OData), custom reporting often requires the API. Teams build scripts that query pipeline run history (success rates, duration trends), work item cycle times, pull request metrics (time-to-review, reviewer load), and deployment frequency. These feed into tools like Power BI, Datadog, or custom dashboards for engineering metrics.
The gotcha with the REST API is rate limiting and pagination. Azure DevOps limits API calls (varies by authentication method and plan tier) and returns paginated results with continuation tokens. Scripts that naively fetch all work items or all pipeline runs without pagination will get incomplete results or hit rate limits. Another common issue is API version compatibility — always specify the api-version parameter, and test against the preview API before it graduates to stable if you need cutting-edge features.
Code Example
#!/bin/bash
# Automated project setup script using Azure DevOps REST API
# Used by platform team to provision new service repositories
ORG="https://dev.azure.com/contoso"
PROJECT="payments-platform"
PAT="$(az keyvault secret show --vault-name kv-platform-automation --name ado-pat --query value -o tsv)"
AUTH=$(echo -n ":$PAT" | base64)
# Create a new repository for settlements-processor
curl -s -X POST "$ORG/$PROJECT/_apis/git/repositories?api-version=7.1" \
-H "Authorization: Basic $AUTH" \
-H "Content-Type: application/json" \
-d '{
"name": "settlements-processor",
"project": { "name": "payments-platform" }
}'
# Configure branch policy: minimum reviewers on main
REPO_ID=$(curl -s "$ORG/$PROJECT/_apis/git/repositories/settlements-processor?api-version=7.1" \
-H "Authorization: Basic $AUTH" | jq -r '.id')
curl -s -X POST "$ORG/$PROJECT/_apis/policy/configurations?api-version=7.1" \
-H "Authorization: Basic $AUTH" \
-H "Content-Type: application/json" \
-d "{
\"isEnabled\": true,
\"isBlocking\": true,
\"type\": { \"id\": \"fa4e907d-c16b-4a4c-9dfa-4916e5d171ab\" },
\"settings\": {
\"minimumApproverCount\": 2,
\"creatorVoteCounts\": false,
\"resetOnSourcePush\": true,
\"scope\": [{
\"repositoryId\": \"$REPO_ID\",
\"refName\": \"refs/heads/main\",
\"matchKind\": \"Exact\"
}]
}
}"
# Trigger a pipeline run programmatically
curl -s -X POST "$ORG/$PROJECT/_apis/pipelines/42/runs?api-version=7.1" \
-H "Authorization: Basic $AUTH" \
-H "Content-Type: application/json" \
-d '{
"resources": {
"repositories": {
"self": { "refName": "refs/heads/main" }
}
},
"variables": {
"deployEnvironment": { "value": "staging" },
"imageTag": { "value": "2.1.456" }
}
}'
# Query pipeline run history for reporting (with pagination)
CONTINUATION_TOKEN=""
while true; do
RESPONSE=$(curl -s -D - "$ORG/$PROJECT/_apis/pipelines/42/runs?api-version=7.1&\$top=100&continuationToken=$CONTINUATION_TOKEN" \
-H "Authorization: Basic $AUTH")
# Extract continuation token from response headers
CONTINUATION_TOKEN=$(echo "$RESPONSE" | grep -i 'x-ms-continuationtoken' | tr -d '\r' | awk '{print $2}')
# Process results
echo "$RESPONSE" | tail -1 | jq '.value[] | {id: .id, result: .result, createdDate: .createdDate}'
[ -z "$CONTINUATION_TOKEN" ] && break
done
# WIQL query: Find all open bugs assigned to payments team
curl -s -X POST "$ORG/$PROJECT/_apis/wit/wiql?api-version=7.1" \
-H "Authorization: Basic $AUTH" \
-H "Content-Type: application/json" \
-d '{
"query": "SELECT [System.Id], [System.Title], [System.State] FROM WorkItems WHERE [System.WorkItemType] = '\''Bug'\'' AND [System.State] = '\''Active'\'' AND [System.AreaPath] UNDER '\''payments-platform\\settlements'\'' ORDER BY [Microsoft.VSTS.Common.Priority]"
}'Interview Tip
A junior engineer typically shows one API call in isolation. Senior candidates describe the 'project factory' pattern — automated provisioning that creates repos, policies, pipelines, permissions, and service connections in one operation triggered by a service catalog request. Mention pagination with continuation tokens (without this, you silently miss data), rate limiting mitigation (batch operations, caching), and the choice between PATs (simple but tied to a user, expire) versus service principals with OAuth (recommended for production automation). Discuss how you secure the automation: the PAT or service principal needs project-level admin rights, so it should be stored in Key Vault with audit logging and scoped to minimum required permissions.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────────┐ │ Project Factory Automation │ │ │ │ ┌─────────────────┐ ┌───────────────────────────────┐ │ │ │ Service Catalog │ │ Azure Function / Pipeline │ │ │ │ Request: │───→│ │ │ │ │ "New service: │ │ 1. Create repo │ │ │ │ settlements- │ │ 2. Configure branch policies │ │ │ │ processor" │ │ 3. Create pipeline from │ │ │ └─────────────────┘ │ template │ │ │ │ 4. Add service connections │ │ │ │ 5. Set permissions │ │ │ │ 6. Create starter work items │ │ │ └───────────────┬───────────────┘ │ │ │ │ │ ↓ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Azure DevOps REST API (v7.1) │ │ │ │ POST /_apis/git/repositories │ │ │ │ POST /_apis/policy/configurations │ │ │ │ POST /_apis/pipelines │ │ │ │ POST /_apis/serviceendpoint/endpoints │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure DevOps integrates with external CI/CD tools through webhooks, REST APIs, service connections, and the Azure Pipelines GitHub App. Common patterns include Jenkins triggering Azure DevOps pipelines via webhooks, Azure DevOps triggering GitHub Actions via repository dispatch, and ArgoCD watching repos that Azure DevOps Pipelines push manifests to.
Detailed Answer
Think of hybrid CI/CD like an international airport alliance. Each airline (CI/CD tool) operates independently with its own fleet and routes, but code-share agreements (integrations) let passengers (artifacts, events) transfer seamlessly between carriers. You book one ticket (commit code), and the alliance ensures you reach your destination regardless of which airlines handle each leg.
The most common hybrid pattern is Azure DevOps for planning and source control (Boards + Repos) with Jenkins for builds that have heavy legacy infrastructure. Azure DevOps triggers Jenkins jobs via the Jenkins service connection or webhooks when code is pushed. Jenkins reports build status back to Azure DevOps pull requests through the Azure DevOps REST API, and publishes artifacts that Azure DevOps Release pipelines can consume. This allows gradual migration from Jenkins without disrupting existing build infrastructure.
GitHub Actions integration works bidirectionally. Azure DevOps Pipelines can trigger GitHub Actions workflows using the repository_dispatch event via REST API, passing context like build ID and artifact location. Conversely, GitHub Actions can trigger Azure DevOps pipelines using the Azure DevOps REST API or the Azure Pipelines Action marketplace action. The most common pattern is organizations using GitHub for open-source repos with Actions, and Azure DevOps for internal repos with Pipelines, with shared artifact registries and deployment targets.
ArgoCD integration follows the GitOps pattern: Azure DevOps Pipelines handle the CI phase (build, test, push container image), then update Kubernetes manifests in a config repository with the new image tag. ArgoCD watches the config repo and automatically syncs the cluster state to match the declared manifests. Azure DevOps does not directly deploy to Kubernetes in this model — it only commits manifest changes. ArgoCD provides the deployment visualization, rollback, and health monitoring.
The production gotcha in hybrid architectures is observability fragmentation. When a deployment spans Azure DevOps (build), Jenkins (integration test), and ArgoCD (deploy), no single tool shows the end-to-end pipeline. Teams must invest in a unified dashboard (often built with the REST APIs of all three tools) or adopt OpenTelemetry-based tracing that correlates events across systems. Another challenge is secret management — each tool has its own secret store, creating duplication and rotation complexity. A centralized secret manager (HashiCorp Vault, Azure Key Vault) accessed by all tools solves this.
Code Example
# Pattern 1: Azure DevOps Pipeline triggers Jenkins build
# azure-pipelines.yml
stages:
- stage: TriggerJenkins
displayName: 'Trigger Jenkins Integration Tests'
jobs:
- job: JenkinsTrigger
pool:
vmImage: 'ubuntu-latest'
steps:
- task: JenkinsQueueJob@2
displayName: 'Queue Jenkins integration test job'
inputs:
serverEndpoint: 'jenkins-server-connection' # Service connection
jobName: 'payments-api-integration-tests'
isMultibranchJob: false
captureConsole: true
capturePipeline: true
parameterizedJob: true
jobParameters: |
IMAGE_TAG=$(Build.BuildId)
BRANCH=$(Build.SourceBranchName)
---
# Pattern 2: Azure DevOps Pipeline triggers GitHub Actions via repository_dispatch
stages:
- stage: TriggerGitHubAction
jobs:
- job: DispatchToGitHub
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
curl -X POST \
-H "Authorization: token $(GITHUB_PAT)" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/contoso/fraud-detector-ml/dispatches \
-d '{
"event_type": "deploy-model",
"client_payload": {
"image_tag": "$(Build.BuildId)",
"environment": "staging",
"triggered_by": "azure-devops-pipeline-$(Build.BuildId)"
}
}'
displayName: 'Trigger GitHub Actions ML model deployment'
---
# Pattern 3: Azure DevOps + ArgoCD GitOps integration
# Azure DevOps Pipeline handles CI, then updates GitOps config repo
stages:
- stage: Build
jobs:
- job: BuildAndPush
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Docker@2
displayName: 'Build and push settlements-processor'
inputs:
containerRegistry: 'contoso-acr'
repository: 'settlements-processor'
command: 'buildAndPush'
tags: '$(Build.BuildId)'
- stage: UpdateGitOpsRepo
displayName: 'Update ArgoCD config repo'
dependsOn: Build
jobs:
- job: UpdateManifests
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
persistCredentials: true
- script: |
# Clone GitOps config repo
git clone https://$(GITOPS_PAT)@dev.azure.com/contoso/platform/_git/k8s-config
cd k8s-config
# Update image tag in kustomization
cd overlays/production/settlements-processor
kustomize edit set image contosoacr.azurecr.io/settlements-processor=contosoacr.azurecr.io/settlements-processor:$(Build.BuildId)
# Commit and push (ArgoCD watches this repo)
git config user.email "[email protected]"
git config user.name "Azure DevOps Pipeline"
git add .
git commit -m "chore: update settlements-processor to $(Build.BuildId)"
git push origin main
displayName: 'Update K8s manifests for ArgoCD sync'
# ArgoCD Application manifest (deployed to ArgoCD cluster)
# apiVersion: argoproj.io/v1alpha1
# kind: Application
# metadata:
# name: settlements-processor
# spec:
# source:
# repoURL: https://dev.azure.com/contoso/platform/_git/k8s-config
# path: overlays/production/settlements-processor
# destination:
# server: https://kubernetes.default.svc
# namespace: settlements
# syncPolicy:
# automated:
# prune: true
# selfHeal: trueInterview Tip
A junior engineer typically describes one integration point in isolation. Senior candidates discuss the architectural pattern: why the hybrid exists (legacy Jenkins infrastructure, GitHub for OSS, ArgoCD for GitOps), how events flow between systems (webhooks, REST API calls, git commits), and most critically, how you maintain observability across the pipeline. Mention the traceability challenge — correlating a build ID from Azure DevOps through Jenkins test results to ArgoCD deployment status — and how you solve it with shared metadata (build ID passed through all systems) and a unified dashboard. Discuss the security boundary: each tool needs credentials to call the others, creating a web of service accounts that must be audited and rotated.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐ │ Hybrid CI/CD Architecture │ │ │ │ ┌───────────────┐ ┌───────────────┐ ┌──────────────┐ │ │ │ Azure DevOps │ │ Jenkins │ │GitHub Actions│ │ │ │ │ │ │ │ │ │ │ │ • Source code │────→│ • Legacy builds│ │ • OSS builds │ │ │ │ • Work items │ │ • Int. tests │ │ • ML training│ │ │ │ • Pipelines │────→│ │ │ │ │ │ │ • Artifacts │ └───────┬───────┘ └──────────────┘ │ │ └───────┬───────┘ │ ↑ │ │ │ │ │ │ │ │ CI: Build + Push │ Test results │ dispatch │ │ ↓ ↓ │ │ │ ┌───────────────┐ ┌───────────────┐ │ │ │ │ Container │ │ Status back │ ┌──────┴───────┐ │ │ │ Registry │ │ to Azure DevOps│ │ REST API │ │ │ │ (ACR) │ │ PR status │ │ trigger │ │ │ └───────┬───────┘ └───────────────┘ └──────────────┘ │ │ │ │ │ │ Update config repo │ │ ↓ │ │ ┌───────────────┐ ┌───────────────┐ │ │ │ GitOps Config │────→│ ArgoCD │ │ │ │ Repo (K8s │ │ • Auto-sync │ │ │ │ manifests) │ │ • Health check│ │ │ └───────────────┘ │ • Rollback │ │ │ └───────────────┘ │ └─────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure DevOps Services is the cloud-hosted SaaS version managed by Microsoft with automatic updates, global availability, and pay-per-user pricing. Azure DevOps Server is the on-premises version installed on your own infrastructure, offering full data sovereignty, network isolation, and customizable upgrade schedules. Choose Server when regulatory compliance requires data to remain on-premises or when air-gapped environments prohibit internet connectivity.
Detailed Answer
Think of this distinction like choosing between renting an apartment in a managed building versus owning your own house. The apartment (Services) comes with maintenance handled for you, utilities included, and upgrades happening automatically. You cannot modify the building's structure, but everything just works. The house (Server) gives you complete control over the property: you choose when to renovate, what to install, and who enters. However, you are responsible for plumbing, electrical, and security. Both provide the same living space, but the ownership model and operational burden differ dramatically.
Azure DevOps Services runs in Microsoft's Azure cloud datacenters and is the SaaS offering. Microsoft handles all infrastructure: patching, scaling, backups, disaster recovery, and feature rollouts. New features appear in Services weeks or months before they reach Server. The service guarantees 99.9% SLA with geo-redundant storage, and you access it through dev.azure.com with Azure Active Directory authentication. Pricing is per-user with free tiers for small teams. Services supports integration with virtually any cloud service, and Microsoft-hosted agents provide zero-maintenance build infrastructure. For most organizations, Services is the default recommendation because it eliminates operational overhead and provides the latest features.
Azure DevOps Server (formerly Team Foundation Server / TFS) installs on Windows Server infrastructure within your datacenter or private cloud. You manage the SQL Server database, application tier, build agents, and all networking. Server receives feature updates through major version releases (2019, 2020, 2022) and periodic update packs. The upgrade path requires planning: database backups, compatibility checks, and staged rollouts. Server licenses are purchased through Visual Studio subscriptions or Server CALs rather than per-user monthly billing. The primary advantage is complete data sovereignty: source code, work items, build artifacts, and pipeline definitions never leave your network perimeter.
Choose Server when regulatory or contractual requirements mandate that source code and CI/CD data remain within specific geographic or network boundaries. Industries like defense, banking in certain jurisdictions, and government agencies often have compliance frameworks (FedRAMP High, IL5, ITAR) that prohibit data from residing in multi-tenant cloud environments. Air-gapped networks with no internet connectivity obviously cannot use cloud services. Organizations with existing significant investment in on-premises infrastructure and SQL Server licensing may find Server more cost-effective for large user counts. Some enterprises also prefer Server when they need deep customization of the application tier, custom authentication providers, or integration with legacy on-premises systems that cannot be exposed to the internet.
Choose Services for everything else. The operational cost of maintaining Server infrastructure is substantial: patching Windows Server and SQL Server, managing backups and disaster recovery, capacity planning for growth, monitoring application health, and planning version upgrades every 2-3 years. Services eliminates all of this. Services also provides features unavailable in Server: Microsoft-hosted agents across multiple OS versions, built-in OIDC federation, Azure Boards analytics with Power BI integration, and faster access to new pipeline features like YAML templates and environment checks. Many organizations that started with TFS/Server are migrating to Services to reduce operational burden.
The production gotcha is the hybrid scenario that many enterprises overlook. You can use Azure DevOps Services with self-hosted agents running inside your corporate network. This gives you cloud-managed orchestration with on-premises execution: pipelines are defined and triggered in Services, but the actual build and deployment work happens on agents within your firewall. This hybrid model satisfies many compliance requirements while avoiding Server's operational overhead. Another common mistake is assuming Server provides better security than Services. Microsoft invests billions in Azure security infrastructure that most organizations cannot match with their own datacenters. Unless you have specific regulatory requirements for data residency, Services often provides stronger security posture than self-managed Server installations.
Code Example
# Azure DevOps Services — access via cloud URL
# Organization URL: https://dev.azure.com/{organization}
az devops configure --defaults organization=https://dev.azure.com/bank-corp
# Azure DevOps Server — access via on-premises URL
# Server URL: https://tfs.bank-corp.internal:8080/tfs
az devops configure --defaults organization=https://tfs.bank-corp.internal:8080/tfs
# Check which version of Azure DevOps Server is installed
# Server Admin Console → Application Tier → Version
# Or via REST API:
curl -u user:pat https://tfs.bank-corp.internal:8080/tfs/_apis/connectionData
# Returns: { "deploymentType": "onPremises", "deploymentId": "..." }
# Services REST API — same endpoints, cloud-hosted
curl -H "Authorization: Bearer $PAT" \
https://dev.azure.com/bank-corp/_apis/connectionData
# Returns: { "deploymentType": "hosted", "deploymentId": "..." }
# Feature comparison check — Services has latest features
# Services: YAML pipelines with extends templates, environments
# Server 2022: YAML pipelines (limited template support)
# Server 2020: YAML pipelines (basic)
# Server 2019: Classic pipelines only (no YAML multi-stage)
# Hybrid approach: Services + self-hosted agent on-premises
# Register agent to cloud organization but run inside firewall
./config.sh --unattended \
--url https://dev.azure.com/bank-corp \
--auth pat --token $PAT \
--pool "on-prem-build-agents" \
--agent "build-agent-01" \
--acceptTeeEula
# Agent communicates outbound to Services (no inbound ports needed)
# Pipeline orchestration in cloud, execution on-premises
# Satisfies: data stays on-prem, management in cloud
# Server installation requirements (for reference):
# - Windows Server 2019/2022
# - SQL Server 2019/2022 (Standard or Enterprise)
# - 4+ CPU cores, 16GB+ RAM for app tier
# - Separate SQL Server recommended for 500+ users
# - SSL certificate for HTTPS
# - Service account with SQL permissionsInterview Tip
A junior engineer typically lists surface-level differences like 'one is cloud and one is on-prem' without explaining the decision criteria. For a strong answer, articulate the specific scenarios that mandate Server: regulatory data residency (FedRAMP, ITAR, certain banking regulations), air-gapped networks, and contractual obligations. Then explain that for all other cases, Services is preferred due to lower operational overhead, faster feature availability, and Microsoft-managed security infrastructure. Crucially, mention the hybrid model with self-hosted agents as the best-of-both-worlds solution that satisfies many compliance requirements while avoiding Server's maintenance burden. Discuss the total cost of ownership including infrastructure, licensing, and personnel for Server versus the per-user pricing of Services.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────┐ │ Azure DevOps Services vs Server Decision Tree │ │ │ │ ┌─────────────────────┐ ┌─────────────────────────┐ │ │ │ Azure DevOps │ │ Azure DevOps │ │ │ │ Services (Cloud) │ │ Server (On-Premises) │ │ │ ├─────────────────────┤ ├─────────────────────────┤ │ │ │ • Microsoft-managed │ │ • Self-managed infra │ │ │ │ • Auto-updates │ │ • Manual upgrades │ │ │ │ • 99.9% SLA │ │ • You define SLA │ │ │ │ • Per-user pricing │ │ • CAL/subscription │ │ │ │ • Latest features │ │ • Features lag 6-12mo │ │ │ │ • Azure AD auth │ │ • AD/custom auth │ │ │ └─────────────────────┘ └─────────────────────────┘ │ │ │ │ Decision: │ │ ├── Regulatory data residency? → Server │ │ ├── Air-gapped network? → Server │ │ ├── Need latest features? → Services │ │ ├── Want zero-ops overhead? → Services │ │ └── Need on-prem execution? → Services + Self-hosted│ │ Agents (Hybrid) │ └─────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Upgrading TFS to Azure DevOps Server involves backing up databases, verifying hardware requirements, running the installer which performs an in-place upgrade of the application tier and database schema, then validating all collections, build agents, and extensions. The upgrade supports skipping versions (TFS 2018 directly to Server 2022) but requires careful pre-upgrade validation and rollback planning.
Detailed Answer
Think of upgrading TFS to Azure DevOps Server like renovating a house while people are still living in it. You need to photograph every room beforehand (backup), verify the new appliances fit the existing plumbing and wiring (compatibility checks), schedule the renovation during a weekend when disruption is minimal (maintenance window), perform the work (installer), verify everything works (validation), and keep the original photos in case you need to restore something (rollback plan). Skipping a version is like jumping from a 1990s kitchen directly to a 2024 design: possible because the renovation contractor handles all intermediate structural changes, but the transformation is more dramatic and testing more critical.
The pre-upgrade phase is the most important and most frequently rushed. Begin by documenting your current environment: TFS version and update level, SQL Server version, Windows Server version, number of project collections, total database size, installed extensions, configured build agents, and any custom plugins or event handlers. Check the compatibility matrix: Azure DevOps Server 2022 requires SQL Server 2019 or 2022 and Windows Server 2019 or 2022. If your current SQL Server version is not supported, you must upgrade SQL Server first, which is a separate project with its own planning. Run the TFS upgrade readiness tool (included in the Azure DevOps Server installer) against your databases to identify any blocking issues like unsupported collation settings, deprecated features, or corrupted work item data.
The backup strategy must be comprehensive and tested. Back up all collection databases, the configuration database, the warehouse database (if used), the reporting databases, and the file system cache. Do not rely solely on SQL Server backups: also export your build definitions, release definitions, extension configurations, and agent pool settings using the Azure DevOps CLI or REST API. These exports serve as documentation even if the upgrade succeeds, because they provide a reference point for validating that everything migrated correctly. Most importantly, test your restore process in a separate environment before the real upgrade. A backup you have never restored is a backup you cannot trust.
The upgrade itself is surprisingly straightforward when prerequisites are met. Download the Azure DevOps Server 2022 installer, run it on the application tier server, and it detects the existing TFS installation. The wizard offers an upgrade path, validates the environment, and then performs the schema migration on each collection database. For large databases (500GB+), the schema migration can take several hours. The installer handles all intermediate version migrations: if you are upgrading from TFS 2018, it applies the schema changes for 2018→2019→2020→2022 sequentially. During this time, the server is offline. Plan your maintenance window based on database size: roughly 1 hour per 100GB is a conservative estimate, though actual times depend on SQL Server hardware.
Post-upgrade validation is where most teams cut corners and regret it later. Verify that all project collections are online and accessible. Check that build agents reconnect (agents from TFS 2018 may need updating). Validate that XAML build definitions were preserved as read-only references. Test that Git and TFVC repositories are accessible with full history. Verify that work item queries, dashboards, and extensions function correctly. Run a sample build pipeline to confirm agents execute properly. Check that notification subscriptions still deliver emails. If you use the reporting warehouse or SharePoint integration, verify those connections.
The production gotcha is the agents and extensions gap. Self-hosted build agents from TFS 2018 use an older agent version that may not be compatible with Azure DevOps Server 2022 features. You will likely need to deploy new agents or update existing ones. Extensions installed from the Visual Studio Marketplace may have version compatibility issues: some extensions that worked on TFS 2018 have been deprecated or replaced. Test all critical extensions in a pre-production upgrade before touching production. Another common issue is authentication: if you switch from NTLM to Kerberos or add Azure AD authentication during the upgrade, all existing PATs and service account connections need reconfiguration.
Code Example
# Pre-upgrade validation and backup procedure
# Step 1: Document current environment
Req -Method GET -Uri "http://tfs.internal:8080/tfs/_apis/projectCollections" \
-Headers @{Authorization="Basic $base64Pat"}
# Check TFS version
curl -u user:pat http://tfs.internal:8080/tfs/_apis/connectionData
# Look for: "releaseType":"Release", "version":"16.153.x" (TFS 2018)
# Step 2: Verify SQL Server compatibility
# Azure DevOps Server 2022 requires SQL Server 2019 or 2022
SELECT @@VERSION -- Check current SQL version
SELECT name, collation_name FROM sys.databases -- Verify collation
# Step 3: Backup all databases
-- SQL Server backup script for all TFS databases
BACKUP DATABASE [Tfs_Configuration]
TO DISK = 'E:\Backups\PreUpgrade\Tfs_Configuration.bak'
WITH COMPRESSION, CHECKSUM;
BACKUP DATABASE [Tfs_DefaultCollection]
TO DISK = 'E:\Backups\PreUpgrade\Tfs_DefaultCollection.bak'
WITH COMPRESSION, CHECKSUM;
BACKUP DATABASE [Tfs_Warehouse]
TO DISK = 'E:\Backups\PreUpgrade\Tfs_Warehouse.bak'
WITH COMPRESSION, CHECKSUM;
# Step 4: Export build definitions (for documentation)
az pipelines list --org http://tfs.internal:8080/tfs/DefaultCollection \
--project payments-platform --output json > build-definitions-backup.json
# Step 5: Run upgrade readiness check
# Launch Azure DevOps Server 2022 installer
# Select: "Upgrade" → "Pre-production upgrade validation"
# Or use command line:
# TfsConfig.exe preUpgrade /sqlInstance:SQLServer\Instance
# Step 6: Perform the upgrade (during maintenance window)
# Run installer on Application Tier server
# Installer detects existing TFS 2018/2019 installation
# Follow wizard: Upgrade → Select configuration database → Validate → Upgrade
# Step 7: Post-upgrade validation
# Verify collections are online
curl -u user:pat https://devops.internal:8080/tfs/_apis/projectCollections
# Verify agent pools
az pipelines agent list --pool-id 1 \
--org https://devops.internal:8080/tfs/DefaultCollection
# Update self-hosted agents to latest version
./config.sh --unattended \
--url https://devops.internal:8080/tfs/DefaultCollection \
--auth negotiate \
--pool "Default" \
--agent "build-agent-01" \
--replace
# Verify Git repositories are accessible
git clone https://devops.internal:8080/tfs/DefaultCollection/Project/_git/repo
# Step 8: Rollback procedure (if upgrade fails)
# Stop Azure DevOps Server services
# Restore SQL databases from pre-upgrade backups
# Reinstall previous TFS version pointing to restored databases
# Verify functionality
# Upgrade path reference:
# TFS 2018 → Azure DevOps Server 2022 (direct, skips 2019/2020)
# TFS 2017 → Azure DevOps Server 2022 (direct)
# TFS 2015 → TFS 2018 → Azure DevOps Server 2022 (two hops)
# TFS 2013 → TFS 2018 → Azure DevOps Server 2022 (two hops)Interview Tip
A junior engineer typically describes the upgrade as just running an installer without discussing the critical preparation and validation phases. For a strong answer, walk through the complete lifecycle: environment documentation, compatibility matrix verification (especially SQL Server version requirements), comprehensive database backups with tested restore procedures, the maintenance window estimation based on database size, the actual upgrade process, and thorough post-upgrade validation of collections, agents, extensions, and authentication. Emphasize that you can skip intermediate versions (TFS 2018 directly to Server 2022) but that pre-production testing in a cloned environment is non-negotiable. Discuss the agents compatibility gap and extension deprecation as the most common post-upgrade surprises that catch teams off guard.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ TFS to Azure DevOps Server Upgrade Path │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │ │ │ TFS 2015 │──→│ TFS 2018 │──→│ Server │──→│Server│ │ │ └──────────┘ └────┬─────┘ │ 2020 │ │ 2022 │ │ │ │ └──────────┘ └──────┘ │ │ │ ↑ ↑ │ │ └──────────────┴──────────────┘ │ │ (Direct upgrade supported) │ │ │ │ Upgrade Process: │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │ │1. Backup │→ │2. Validate│→ │3. Upgrade│→ │4. Verify│ │ │ │ • SQL DBs│ │ • SQL ver │ │ • Run │ │ • Repos │ │ │ │ • Config │ │ • Win ver │ │ installer│ │ • Agents│ │ │ │ • Agents │ │ • Disk │ │ • Schema │ │ • Builds│ │ │ │ • Export │ │ • Extensions│ │ migrate│ │ • Exts │ │ │ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │ │ │ │ Rollback: Restore SQL backups + reinstall previous ver │ └──────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure DevOps Proxy Server caches version control files at remote office locations, reducing bandwidth usage and improving source control performance for distributed teams. It intercepts TFVC and Git LFS file requests, serves cached copies locally, and forwards cache misses to the central Azure DevOps Server or Services instance. Configuration involves installing the proxy on a server at the remote site, configuring it to point to the upstream Azure DevOps instance, and directing client machines to use the proxy for source control operations.
Detailed Answer
Think of Azure DevOps Proxy Server like a local library branch in a remote town. Instead of every resident driving 200 miles to the central library for every book, the branch maintains copies of frequently requested books. When someone requests a book the branch has, they get it instantly. When someone requests a rare book the branch does not have, the branch requests it from the central library, delivers it to the patron, and keeps a copy for future requests. Over time, the branch accumulates the books that local residents need most, and trips to the central library become rare. Azure DevOps Proxy works identically: it caches source control files at the remote site, serving them locally and only fetching from the central server on cache misses.
The proxy server architecture positions a caching intermediary between developer workstations and the Azure DevOps application tier. When a developer performs a get-latest operation in TFVC or downloads Git LFS objects, the request goes to the local proxy instead of traversing the WAN to the central server. The proxy checks its local cache: if the requested file version exists, it returns it directly with LAN-speed performance. If the file version is not cached, the proxy fetches it from the central server, delivers it to the developer, and stores it in the cache for subsequent requests. This is particularly effective for TFVC workspaces where teams frequently get the same file versions, and for Git LFS where binary assets are shared across the team.
Installation requires a Windows Server machine at the remote office with sufficient disk space for the cache (size depends on repository sizes and access patterns), network connectivity to the central Azure DevOps instance, and the Azure DevOps Proxy installer. The installation wizard configures the proxy service, specifies the upstream Azure DevOps URL, sets the cache directory and size limits, and configures the service account. After installation, the proxy registers itself with the central Azure DevOps instance, which begins redirecting TFVC clients to use the proxy. For Git LFS, developers configure their Git LFS client to route through the proxy URL instead of directly to Azure DevOps.
Cache management and monitoring are critical for production proxy deployments. The proxy maintains a cache statistics page showing hit ratio, cache size, most-requested files, and bandwidth savings. A healthy proxy should achieve 60-90% cache hit ratio after the initial warm-up period. Administrators configure cache size limits and eviction policies: when the cache reaches capacity, least-recently-used files are evicted. The proxy also supports scheduled cache warming where administrators pre-populate the cache with files from specific branches or paths, ensuring that Monday morning get-latest operations are served from cache rather than flooding the WAN link. Network administrators should configure QoS policies to prioritize proxy-to-server traffic and monitor bandwidth utilization to validate the proxy's effectiveness.
The production gotcha is understanding what the proxy does and does not cache. The proxy caches file content (source code files, binaries) but does not cache metadata operations like work item queries, pipeline definitions, or branch listings. These metadata operations still traverse the WAN. Additionally, all write operations (check-ins, pushes, pull request creation) go directly to the central server regardless of proxy configuration. The proxy is read-only caching only. Teams with high write frequency see less benefit than teams with primarily read-heavy workflows. For Git repositories using standard Git protocol (not LFS), the proxy provides limited benefit because Git's own pack file protocol and local clone caching already handle much of what the proxy offers. The proxy is most valuable for TFVC workspaces with large codebases and Git repositories with substantial LFS-tracked binary content.
Code Example
# Azure DevOps Proxy Server Setup
# Step 1: Install Azure DevOps Proxy on remote office server
# Download installer from Microsoft (same installer as Server)
# Run installer and select "Azure DevOps Proxy Server" option
# Step 2: Configure proxy via administration console
# Or configure via command line:
TFSConfig.exe proxy /configure \
/tfsurl:https://dev.azure.com/bank-corp \
/cacheroot:E:\ProxyCache \
/cachesizelimit:500 # GB
# Step 3: Verify proxy registration
# Check proxy status via REST API
curl https://proxy-server.mumbai.internal:8081/_apis/proxy/status
# Returns: { "status": "healthy", "cacheHitRatio": 0.78, "cacheSizeGB": 234 }
# Step 4: Configure TFVC clients to use proxy
# Visual Studio: Tools → Options → Source Control → Plug-in Settings
# Set proxy server: http://proxy-server.mumbai.internal:8081
# Or configure via tf.exe command line:
tf proxy /configure /server:http://proxy-server.mumbai.internal:8081
# Step 5: Configure Git LFS to route through proxy
# In .lfsconfig at repository root:
# [lfs]
# url = http://proxy-server.mumbai.internal:8081/tfs/DefaultCollection/_git/lfs
# Or per-user Git config:
git config --global lfs.url http://proxy-server.mumbai.internal:8081/tfs/DefaultCollection
# Step 6: Monitor cache performance
# Check cache statistics (admin page)
curl http://proxy-server.mumbai.internal:8081/_apis/proxy/cache/statistics
# {
# "totalRequests": 45230,
# "cacheHits": 35280,
# "cacheMisses": 9950,
# "hitRatio": 0.78,
# "bandwidthSavedGB": 156.4,
# "cacheDirectorySizeGB": 234.7,
# "cacheDirectoryLimit": 500
# }
# Step 7: Pre-warm cache for specific branches
# Schedule this before Monday morning to avoid WAN surge
TFSConfig.exe proxy /warmup \
/collection:https://dev.azure.com/bank-corp/DefaultCollection \
/teamproject:payments-platform \
/branch:$/payments-platform/main
# Step 8: Configure cache eviction policy
# In proxy web.config or via admin console:
# <cacheEviction>
# <policy>LeastRecentlyUsed</policy>
# <maxCacheSizeGB>500</maxCacheSizeGB>
# <evictionThresholdPercent>90</evictionThresholdPercent>
# <maxFileAgedays>90</maxFileAgedays>
# </cacheEviction>
# Network architecture for multi-site proxy:
# Site A (HQ): Azure DevOps Server/Services (source of truth)
# Site B (Mumbai): Proxy Server → caches reads from Site A
# Site C (London): Proxy Server → caches reads from Site A
# Writes from all sites go directly to Site AInterview Tip
A junior engineer typically confuses the Azure DevOps Proxy with a general HTTP proxy or a Git mirror. For a strong answer, explain that the proxy is specifically a read-only file content cache for TFVC workspace operations and Git LFS downloads. It does not cache metadata operations, work item queries, or pipeline execution. All write operations bypass the proxy entirely. Discuss the architecture: proxy registers with the upstream Azure DevOps instance, clients are redirected to the proxy for file downloads, and cache hit ratios of 60-90% are typical for mature deployments. Mention cache warming strategies, size planning based on repository sizes, and the diminishing returns for pure Git repositories that already benefit from local clone caching. Explain that for modern Git-only workflows, Git's own distributed nature often eliminates the need for a proxy, making it primarily relevant for TFVC shops or teams with large Git LFS assets.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ Azure DevOps Proxy Architecture │ │ │ │ Headquarters (US) Remote Office (Mumbai) │ │ ┌──────────────────┐ ┌────────────────────┐ │ │ │ Azure DevOps │ │ Proxy Server │ │ │ │ Server/Services │◄──WAN────→│ ┌──────────────┐ │ │ │ │ (Source of Truth)│ (slow) │ │ Cache (500GB)│ │ │ │ └──────────────────┘ │ └──────┬───────┘ │ │ │ └─────────┼──────────┘ │ │ LAN (fast) │ │ ┌─────────┼──────────┐ │ │ │ ┌────┴────┐ │ │ │ │ │Developer│ │ │ │ │ │Workstations │ │ │ │ └─────────┘ │ │ │ └────────────────────┘ │ │ │ │ Flow: │ │ 1. Developer requests file → Proxy checks cache │ │ 2. Cache HIT → Return file instantly (LAN speed) │ │ 3. Cache MISS → Fetch from central server → Cache → Ret │ │ 4. Writes → Bypass proxy → Direct to central server │ └──────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
The Azure DevOps Data Migration Tool (OpsImport) performs a high-fidelity migration of project collections from Azure DevOps Server to Azure DevOps Services, preserving full history of source code, work items, builds, and identities. The process involves running the migration tool to validate and generate import files, uploading the DACPAC database export to Azure blob storage, submitting the import request, and completing identity mapping to link on-premises Active Directory accounts to Azure AD identities.
Detailed Answer
Think of migrating from Azure DevOps Server to Services like moving an entire museum collection from one building to another across the country. You cannot simply load paintings into a truck: each piece must be cataloged, wrapped in appropriate protective materials, transported in climate-controlled vehicles, and reinstalled in the new building with the same spatial relationships, labels, and security settings. The catalog (identities) must be translated because room numbers (Active Directory SIDs) are different in the new building (Azure AD). The Data Migration Tool is the professional moving company that handles all of this complexity: it exports the database, ships it to Azure, imports it, and maps the old identity references to new ones.
The migration begins with preparation and validation. You must be running a supported version of Azure DevOps Server (2019 or later, with latest updates). The Data Migration Tool validates your collection against a set of readiness checks: unsupported field types, oversized attachments, deprecated features in use, identity conflicts, and data integrity issues. Common blocking issues include Git repositories exceeding 10GB, TFVC files exceeding 1GB, custom process templates with unsupported elements, and work items with more than 1000 revisions. The validation report identifies these issues and provides remediation guidance. Some issues require modifying data before migration; others simply generate warnings about features that will behave differently in Services.
The actual migration process has two modes: dry run (import with no data, just validation) and production import. For the production import, you generate a DACPAC (Data-Tier Application Package) from your collection database using SQL Server tools. This DACPAC is a compressed export of the entire database schema and data. You upload the DACPAC to an Azure blob storage container provided by Microsoft during the import setup. For large databases, this upload can take hours or days depending on bandwidth. Microsoft then processes the import on their infrastructure, applying schema transformations to adapt the on-premises database structure to the multi-tenant Services format. The import duration depends on database size: small collections (under 10GB) complete in hours, while large collections (500GB+) may take days.
Identity mapping is the most complex aspect of the migration. On-premises Azure DevOps Server uses Active Directory (AD) security identifiers (SIDs) for all permissions, work item assignments, and history attribution. Azure DevOps Services uses Azure Active Directory (Azure AD) identities. The migration tool generates an identity map file listing every AD identity referenced in the collection and requiring you to map each to a corresponding Azure AD identity. For organizations that have already synchronized their AD to Azure AD using Azure AD Connect, many mappings are automatic. For identities that have no Azure AD equivalent (departed employees, service accounts, renamed accounts), you must decide whether to map them to existing Azure AD accounts or leave them as historical references. Incorrect identity mapping results in permission errors and misattributed history.
Post-migration validation and cutover planning are critical. During migration, the source Server is taken offline (or set to read-only) to prevent changes that would not be captured in the migration. After import completes, you validate that all repositories, work items, pipelines, and permissions exist correctly in Services. Pipelines need reconfiguration because Service Connections, agent pools, and variable groups do not migrate automatically. Self-hosted agents must be re-registered to point to the new Services organization. External integrations (webhooks, API clients, IDE connections) must be updated to use the new dev.azure.com URLs. Plan a cutover window that accounts for DNS changes, client reconfiguration, and team communication.
The production gotcha is underestimating the identity mapping effort and the pipeline reconfiguration work. Organizations with 10+ years of TFS history have thousands of unique identities from employees who have left, contractors, and service accounts. Each must be manually reviewed and mapped. Another common issue is TFVC workspace configuration: developers with local TFVC workspaces must recreate them pointing to the new Services instance. Pipeline migration requires recreating every Service Connection, variable group, and agent pool in the new organization, then updating pipeline YAML to reference the new resource names. Budget 2-4 weeks of effort beyond the actual data migration for this reconfiguration work.
Code Example
# Azure DevOps Data Migration — Complete Process # Step 1: Install the Data Migration Tool # Download from https://www.microsoft.com/download/details.aspx?id=54274 # Extract to C:\DataMigration # Step 2: Run validation against collection Migrator.exe validate /collection:http://tfs.internal:8080/tfs/DefaultCollection \ /tenantDomainName:bank-corp.onmicrosoft.com \ /outputPath:C:\Migration\Validation # Review validation results # C:\Migration\Validation\Results.json contains: # - Blocking issues (must fix before migration) # - Warnings (features that behave differently) # - Identity mapping file template # Step 3: Fix blocking issues # Example: Repository too large # git filter-branch to remove large files # Or: Move large binaries to Git LFS before migration # Step 4: Generate identity map Migrator.exe identityMap /collection:http://tfs.internal:8080/tfs/DefaultCollection \ /outputPath:C:\Migration\IdentityMap # Identity map file (IdentityMap.csv): # Source (AD),Target (Azure AD),Status # BANK\ramesh.a,[email protected],Matched # BANK\john.former,[email protected],NotFound ← must resolve # BANK\svc-build,[email protected],ServiceAccount # Step 5: Generate DACPAC export # Use SqlPackage.exe from SQL Server tools SqlPackage.exe /Action:Export \ /SourceServerName:"sql-server.internal" \ /SourceDatabaseName:"Tfs_DefaultCollection" \ /TargetFile:"C:\Migration\DefaultCollection.dacpac" \ /p:Storage=File # Step 6: Upload DACPAC to Azure blob storage # Microsoft provides a SAS token during import setup az storage blob upload \ --account-name importstorageaccount \ --container-name import \ --file C:\Migration\DefaultCollection.dacpac \ --name DefaultCollection.dacpac \ --sas-token "?sv=2021-06-08&ss=b&srt=co&sp=rwdlac..." # Step 7: Submit import request Migrator.exe import /collection:http://tfs.internal:8080/tfs/DefaultCollection \ /tenantDomainName:bank-corp.onmicrosoft.com \ /targetOrgName:bank-corp \ /identityMapFile:C:\Migration\IdentityMap.csv \ /dacpacFile:DefaultCollection.dacpac # Step 8: Monitor import progress # Check status at: https://dev.azure.com/bank-corp/_admin/_import # Status transitions: Queued → InProgress → Completed/Failed # Step 9: Post-migration validation # Verify repos az repos list --org https://dev.azure.com/bank-corp --project payments-platform # Verify work items preserved az boards query --org https://dev.azure.com/bank-corp \ --wiql "SELECT [System.Id] FROM workitems WHERE [System.TeamProject] = 'payments-platform'" \ --output table # Step 10: Reconfigure pipelines (these don't auto-migrate) # Create service connections az devops service-endpoint create \ --service-endpoint-configuration aws-connection.json \ --org https://dev.azure.com/bank-corp \ --project payments-platform # Re-register self-hosted agents ./config.sh --unattended \ --url https://dev.azure.com/bank-corp \ --auth pat --token $NEW_PAT \ --pool "Default" --agent "build-01" --replace
Interview Tip
A junior engineer typically describes migration as a simple export-import without addressing the complexity of identity mapping, pipeline reconfiguration, and cutover planning. For a strong answer, walk through the end-to-end process: validation that identifies blocking issues, DACPAC generation for the database export, identity mapping from Active Directory SIDs to Azure AD accounts, the actual import queue processing, and post-migration validation. Emphasize that source code history, work item revisions, and Git branches all migrate with full fidelity, but pipeline infrastructure (Service Connections, agent pools, variable groups) does not migrate and must be manually recreated. Discuss the cutover strategy: read-only period on the source server, import completion verification, DNS/URL updates, and client reconfiguration. Mention that dry-run imports are essential for estimating duration and catching issues before the production migration window.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ TFS/Server to Services Migration Flow │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │ │1.Validate│→ │2.Identity│→ │3.Export │→ │4.Upload │ │ │ │ (fix │ │ Map │ │ DACPAC │ │ to Blob│ │ │ │ issues) │ │ (AD→AAD)│ │ (SQL) │ │ Storage│ │ │ └──────────┘ └──────────┘ └──────────┘ └────┬────┘ │ │ │ │ │ ↓ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │ │8.Cutover │← │7.Reconfig│← │6.Validate│← │5.Import │ │ │ │ (DNS, │ │ (Service│ │ (repos, │ │ (MSFT │ │ │ │ clients)│ │ Conns, │ │ WIs, │ │ process│ │ │ │ │ │ agents) │ │ history) │ │ queue) │ │ │ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │ │ │ │ What migrates: What does NOT migrate: │ │ ├── Source code + hist ├── Service Connections │ │ ├── Work items + revs ├── Agent pools/agents │ │ ├── Git branches/tags ├── Variable groups │ │ ├── Build history ├── Extensions (reinstall) │ │ └── Test results └── Webhooks/integrations │ └──────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Migrating from Jira to Azure DevOps Boards requires mapping Jira issue types, fields, and workflows to Azure DevOps work item types and states, then using tools like the Solidify Jira to Azure DevOps Migration Tool or custom scripts with both REST APIs to transfer issues with full history, comments, attachments, and links. The migration preserves traceability by maintaining original Jira keys as references and mapping user identities between systems.
Detailed Answer
Think of migrating from Jira to Azure DevOps Boards like translating a novel from one language to another while preserving not just the current text but every draft, editorial comment, and margin note. The characters (issue types) have different names in the new language, the grammar rules (workflows) structure sentences differently, and the punctuation marks (field types) have different symbols. A skilled translator does not just convert words: they map concepts, preserve the author's intent across revisions, and ensure readers of the translation understand the full creative journey. Jira-to-Azure-DevOps migration requires this same conceptual mapping combined with mechanical data transfer.
The first phase is mapping Jira's data model to Azure DevOps. Jira has issue types (Epic, Story, Task, Bug, Sub-task), custom fields (text, select, multi-select, date, user), and workflow states (configurable per project). Azure DevOps has work item types defined by process templates (Agile: Epic, Feature, User Story, Task, Bug; Scrum: Epic, Feature, PBI, Task, Bug). You must create a mapping table: Jira Epic maps to Azure DevOps Epic, Jira Story maps to User Story, Jira Sub-task maps to Task. Custom fields require creating matching fields in Azure DevOps with compatible types. Workflow states need mapping: Jira's 'In Review' might map to Azure DevOps 'Resolved' or a custom state. This mapping phase is intellectual work that cannot be automated and requires agreement from stakeholders who use both systems.
The migration tooling landscape offers several options. The Solidify Jira to Azure DevOps Work Item Migration Tool is an open-source solution that handles field mapping, history preservation, and attachment transfer. For enterprises, tools like OpsHub or Tasktop provide managed migration services with support. For custom requirements, you can build a migration pipeline using Jira's REST API (to export) and Azure DevOps REST API (to import). The custom approach gives maximum flexibility for complex field transformations, conditional logic, and validation, but requires significant development effort. Most organizations use a hybrid: a commercial tool for the bulk migration with custom scripts for edge cases like Jira plugins that store data in non-standard locations.
History preservation is the most challenging aspect. Jira stores a changelog for every field change on every issue, including who changed it and when. To preserve this in Azure DevOps, you must create work items with their historical field values and revision timestamps. Azure DevOps allows setting the ChangedDate and ChangedBy fields during import (using the bypassRules flag on the REST API), enabling you to recreate the revision history. Each Jira changelog entry becomes a work item revision in Azure DevOps. Comments transfer as Discussion entries with original timestamps and authors. Attachments are downloaded from Jira and uploaded to Azure DevOps work items. Links between issues (blocks, is-blocked-by, duplicates) map to Azure DevOps link types (Predecessor, Successor, Duplicate).
User identity mapping connects Jira usernames to Azure DevOps identities. Jira identifies users by accountId (cloud) or username (server), while Azure DevOps uses Azure AD email addresses. You create a mapping file that translates each Jira user to their Azure DevOps identity. For former employees who exist in Jira history but not in Azure AD, you can map them to a placeholder account or leave them as text references. The migration tool uses this mapping when setting AssignedTo, CreatedBy, and ChangedBy fields on imported work items, ensuring the history attributes correctly to real Azure DevOps users.
The production gotcha is the testing and validation cycle. Never migrate directly to your production Azure DevOps project on the first attempt. Create a test project, run the migration, and have stakeholders validate the results: Are all work items present? Do the field values look correct? Is the history complete? Are attachments accessible? Do parent-child relationships match Jira's hierarchy? Are sprint/iteration assignments correct? Expect to run 3-5 test migrations before the production run, adjusting field mappings and transformation rules each time. Another common issue is Jira's flexibility working against you: Jira allows essentially any field on any issue type, while Azure DevOps enforces type-specific field definitions. Issues that used fields in non-standard ways may require special handling during migration.
Code Example
# Jira to Azure DevOps Migration using REST APIs # Step 1: Export issues from Jira (REST API) # Get all issues from a Jira project with full history curl -u [email protected]:$JIRA_API_TOKEN \ "https://company.atlassian.net/rest/api/3/search?jql=project=PAY&expand=changelog,renderedFields&maxResults=100" \ -H "Accept: application/json" > jira_export.json # Step 2: Download attachments from Jira # For each issue's attachment: curl -u [email protected]:$JIRA_API_TOKEN \ -o attachment_12345.pdf \ "https://company.atlassian.net/rest/api/3/attachment/content/12345" # Step 3: Field mapping configuration (mapping.json) # { # "issueTypeMap": { # "Epic": "Epic", # "Story": "User Story", # "Task": "Task", # "Sub-task": "Task", # "Bug": "Bug" # }, # "fieldMap": { # "summary": "System.Title", # "description": "System.Description", # "status": "System.State", # "priority": "Microsoft.VSTS.Common.Priority", # "assignee": "System.AssignedTo", # "story_points": "Microsoft.VSTS.Scheduling.StoryPoints", # "sprint": "System.IterationPath" # }, # "stateMap": { # "To Do": "New", # "In Progress": "Active", # "In Review": "Active", # "Done": "Closed", # "Won't Do": "Removed" # }, # "userMap": { # "5f3c8a...(jira-id)": "[email protected]", # "5e2b7c...(jira-id)": "[email protected]" # } # } # Step 4: Create work items in Azure DevOps with history # Use bypass rules to set historical dates and users curl -X PATCH \ "https://dev.azure.com/bank-corp/payments/_apis/wit/workitems/\$User%20Story?bypassRules=true&api-version=7.0" \ -H "Content-Type: application/json-patch+json" \ -H "Authorization: Basic $(echo -n :$PAT | base64)" \ -d '[ {"op":"add","path":"/fields/System.Title","value":"Add PCI tokenization"}, {"op":"add","path":"/fields/System.State","value":"Active"}, {"op":"add","path":"/fields/System.CreatedDate","value":"2024-03-15T10:30:00Z"}, {"op":"add","path":"/fields/System.CreatedBy","value":"[email protected]"}, {"op":"add","path":"/fields/System.ChangedDate","value":"2024-06-01T14:00:00Z"}, {"op":"add","path":"/fields/System.ChangedBy","value":"[email protected]"}, {"op":"add","path":"/fields/System.History","value":"Migrated from Jira PAY-1234"}, {"op":"add","path":"/fields/Microsoft.VSTS.Scheduling.StoryPoints","value":5}, {"op":"add","path":"/fields/System.Tags","value":"jira-migrated;PAY-1234"} ]' # Step 5: Upload attachments to the migrated work item curl -X POST \ "https://dev.azure.com/bank-corp/payments/_apis/wit/attachments?fileName=design-doc.pdf&api-version=7.0" \ -H "Authorization: Basic $(echo -n :$PAT | base64)" \ -H "Content-Type: application/octet-stream" \ --data-binary @attachment_12345.pdf # Step 6: Add comment history with original timestamps curl -X POST \ "https://dev.azure.com/bank-corp/payments/_apis/wit/workItems/42/comments?api-version=7.0-preview.4" \ -H "Content-Type: application/json" \ -H "Authorization: Basic $(echo -n :$PAT | base64)" \ -d '{"text":"Original Jira comment by Sarah (2024-04-10): Review approved, proceed with implementation"}' # Using open-source migration tool (jira-azuredevops-migrator) # Install: dotnet tool install --global jira-migrator # Config: migration-config.json with mappings above # Run: jira-migrator migrate \ --config migration-config.json \ --jira-url https://company.atlassian.net \ --ado-url https://dev.azure.com/bank-corp \ --project payments \ --dry-run false
Interview Tip
A junior engineer typically describes Jira migration as simply copying issue titles and descriptions, ignoring the complexity of field mapping, history preservation, and identity translation. For a strong answer, explain the three critical mapping layers: issue types to work item types, custom fields to Azure DevOps fields (with type compatibility), and workflow states to valid Azure DevOps states. Describe how the bypassRules API flag enables setting historical CreatedDate and CreatedBy values to preserve the audit trail. Discuss attachment migration as a two-step process (download from Jira, upload to Azure DevOps with linking). Emphasize the iterative testing approach: expect 3-5 migration dry runs before production, with stakeholder validation at each iteration. Mention that sprint/iteration mapping and parent-child relationship preservation are the details that separate a clean migration from a data dump.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ Jira to Azure DevOps Migration Flow │ │ │ │ Jira (Source) Azure DevOps (Target) │ │ ┌────────────────┐ ┌────────────────────┐ │ │ │ Epic │────────→│ Epic │ │ │ │ Story │────────→│ User Story │ │ │ │ Task/Sub-task │────────→│ Task │ │ │ │ Bug │────────→│ Bug │ │ │ └────────────────┘ └────────────────────┘ │ │ │ │ Mapping Layers: │ │ ┌─────────────────────────────────────────────┐ │ │ │ 1. Issue Types → Work Item Types │ │ │ │ 2. Custom Fields → ADO Fields (type match) │ │ │ │ 3. Workflow States → Valid State transitions │ │ │ │ 4. Users (Jira ID → Azure AD email) │ │ │ │ 5. Sprints → Iteration Paths │ │ │ │ 6. Links (blocks/duplicates → ADO links) │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ Preserved: History, Comments, Attachments, Links │ │ Lost: Jira plugins data, custom screens, dashboards │ └──────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure DevOps does not support native project-to-project migration between organizations. You must use a combination of tools: az repos import for Git repositories, the REST API with work item export/import for Boards data, pipeline YAML committed to repos for pipeline definitions, and manual recreation of Service Connections, variable groups, and permissions. Full-fidelity migration preserving all history requires the paid Azure DevOps Migration Tools (nkdAgility) or a collection-level import if migrating the entire collection.
Detailed Answer
Think of moving an Azure DevOps project between organizations like transferring a department from one company to another. The employees (repositories) can physically move, but their badges (Service Connections), desk configurations (variable groups), and security clearances (permissions) are all tied to the original company's systems and must be recreated from scratch in the new company. The HR records (work item history) can be photocopied but the originals stay behind. There is no 'transfer department' button because the two companies have completely different administrative systems, identity providers, and security boundaries. The migration requires systematically moving each component using the appropriate tool for each type of data.
The limitation exists because Azure DevOps organizations are the top-level security and identity boundary. Each organization has its own Azure Active Directory tenant connection, billing subscription, user licenses, and administrative policies. Projects within an organization share these organizational settings. Moving between organizations means crossing this boundary, which breaks all identity references, permission assignments, and resource connections. Microsoft provides collection-level migration (the Data Migration Tool discussed earlier) which moves everything at once, but there is no supported tool for moving a single project from a multi-project organization to a different organization while leaving other projects behind.
Git repositories are the simplest component to migrate. Azure DevOps supports importing Git repositories from any accessible URL, preserving all branches, tags, and commit history. You can use az repos import to clone a repository from the source organization into the target. If the source repository is private, you provide a PAT with read access. Large File Storage (LFS) objects require additional steps: you must ensure LFS objects are pushed to the new remote after the main repository import. For TFVC repositories, conversion to Git using git-tfs is recommended before migration, as TFVC repository migration between organizations is not supported without the Data Migration Tool.
Work items are the most complex component because Azure DevOps does not have a native bulk export/import that preserves full revision history between organizations. The Azure DevOps Migration Tools by nkdAgility (formerly Martin Hinshelwood's tools) is the community-standard solution. It connects to both source and target organizations via REST API, reads work items with full history, maps fields and states between different process templates, and creates corresponding work items in the target with preserved timestamps and authors (using bypassRules). The tool handles parent-child relationships, links between work items, attachments, and area/iteration path structures. Without this tool, you are limited to CSV export/import which loses all history and relationships.
Pipelines, Service Connections, and organizational resources do not migrate and must be manually recreated. Pipeline YAML files committed to repositories transfer automatically with the repo, but the pipeline registration (triggers, settings, permissions) must be recreated. Service Connections contain credentials specific to the source organization's service principal and cannot be exported. Variable groups, agent pools, environments with approval checks, and library assets all require manual recreation. This is often the most time-consuming part of project migration because it requires knowledge of every integration point the project uses.
The production gotcha is underestimating the work item migration complexity and the loss of non-transferable data. Work item IDs change during migration (the target gets new sequential IDs), breaking external references from documentation, emails, and chat messages that reference work items by number. Teams must create a mapping table from old IDs to new IDs and update documentation accordingly. Another critical limitation: build and release history does not migrate. The target organization starts with no deployment history, which impacts compliance teams that need continuous audit trails. Test Plans with test cases, test suites, and test results also require the migration tools and significant effort to transfer correctly. Teams should document all of these limitations and get stakeholder acceptance before beginning the migration.
Code Example
# Moving Azure DevOps Project Between Organizations
# Component 1: Migrate Git Repositories
# Import repo from source org to target org
az repos import create \
--org https://dev.azure.com/target-org \
--project payments-platform \
--repository payments-api \
--git-url https://dev.azure.com/source-org/payments/_git/payments-api \
--requires-authorization \
--user-name "" --password $SOURCE_PAT
# Verify all branches migrated
az repos ref list \
--org https://dev.azure.com/target-org \
--project payments-platform \
--repository payments-api \
--filter heads/ --output table
# Migrate Git LFS objects (not included in import)
git clone https://dev.azure.com/source-org/payments/_git/payments-api
cd payments-api
git lfs fetch --all
git remote set-url origin https://dev.azure.com/target-org/payments-platform/_git/payments-api
git lfs push --all origin
git push --mirror origin
# Component 2: Migrate Work Items (using nkdAgility Migration Tools)
# Install: dotnet tool install -g MigrationTools
# Configuration file: migration-config.json
# {
# "Source": {
# "Collection": "https://dev.azure.com/source-org",
# "Project": "payments",
# "PAT": "$SOURCE_PAT"
# },
# "Target": {
# "Collection": "https://dev.azure.com/target-org",
# "Project": "payments-platform",
# "PAT": "$TARGET_PAT"
# },
# "Processors": [
# {
# "ProcessorType": "WorkItemMigrationContext",
# "Enabled": true,
# "UpdateCreatedDate": true,
# "UpdateCreatedBy": true,
# "WIQLQuery": "SELECT [System.Id] FROM workitems WHERE [System.TeamProject] = 'payments'"
# }
# ]
# }
# Run migration
migration-tools execute --config migration-config.json
# Component 3: Recreate Service Connections (manual)
az devops service-endpoint create \
--org https://dev.azure.com/target-org \
--project payments-platform \
--service-endpoint-configuration aws-connection.json
# Component 4: Recreate Variable Groups
az pipelines variable-group create \
--org https://dev.azure.com/target-org \
--project payments-platform \
--name "payments-api-prod" \
--variables db-host=prod-db.internal api-port=8443
# Component 5: Recreate Pipeline registrations
# Pipeline YAML already exists in migrated repo
# Just create the pipeline definition pointing to it
az pipelines create \
--org https://dev.azure.com/target-org \
--project payments-platform \
--name payments-api-cicd \
--repository payments-api \
--branch main \
--yml-path azure-pipelines.yml
# Component 6: Generate ID mapping for documentation updates
# Export mapping from migration tools output
# Old ID → New ID mapping for updating external references
# source:1234 → target:5678
# source:1235 → target:5679
# Limitations summary:
# ✓ Migrates: repos, branches, work items, history, attachments
# ✗ Cannot migrate: build history, Service Connections, agent pools
# ✗ Cannot migrate: environments, variable groups, permissions
# ✗ Cannot migrate: dashboards, wiki (must recreate or copy)
# ✗ Work item IDs change (new sequential IDs in target)Interview Tip
A junior engineer typically assumes there is a 'move project' button or API in Azure DevOps, not realizing that cross-organization project migration is an unsupported operation requiring multiple tools. For a strong answer, explain why native migration does not exist: organizations are security boundaries with different identity providers, billing, and policies. Walk through the component-by-component approach: Git repos migrate easily via import, work items require specialized tools like nkdAgility Migration Tools for history preservation, and infrastructure components (Service Connections, variable groups, agent pools, environments) must be manually recreated. Highlight the critical limitations: work item IDs change, build/release history is lost, and external references break. Recommend the Data Migration Tool for full-collection moves and nkdAgility for single-project moves, and emphasize that planning documentation and stakeholder alignment on limitations is as important as the technical execution.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ Cross-Organization Project Migration Components │ │ │ │ Source Org Target Org │ │ ┌────────────────────┐ ┌────────────────────┐ │ │ │ Project: payments │ │ Project: payments │ │ │ ├────────────────────┤ ├────────────────────┤ │ │ │ Git Repos ─────────────→│ Git Repos (import) │ │ │ │ Work Items ─────(tool)──→│ Work Items (new ID)│ │ │ │ Pipelines YAML─────────────→│ (travels with repo)│ │ │ │ Wiki ──────────────→│ (git-based wiki) │ │ │ ├────────────────────┤ ├────────────────────┤ │ │ │ Service Conns ✗ │ │ Recreate manually │ │ │ │ Variable Groups ✗ │ │ Recreate manually │ │ │ │ Agent Pools ✗ │ │ Recreate manually │ │ │ │ Environments ✗ │ │ Recreate manually │ │ │ │ Build History ✗ │ │ Lost (not migrated)│ │ │ │ Permissions ✗ │ │ Reconfigure │ │ │ └────────────────────┘ └────────────────────┘ │ │ │ │ ✗ = Does not transfer automatically │ └──────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure DevOps permissions follow a hierarchical model: organization-level groups define broad access, project-level groups scope to specific projects, and resource-level permissions (repos, pipelines, feeds) provide granular control. Least privilege is implemented by granting only the minimum permissions needed at the most specific scope possible, using security groups rather than individual assignments, and leveraging deny permissions to override inherited access at lower levels.
Detailed Answer
Think of Azure DevOps permissions like a corporate building's access control system with multiple layers. The building badge (organization) grants entry to the lobby and common areas. The floor badge (project) allows access to your department's floor. The room key (resource-level) opens specific offices and labs. Each level inherits from above but can restrict further: having building access does not automatically grant room access. A well-designed system gives the mail carrier lobby access only, the accountant access to the finance floor and their specific office, and the CEO access everywhere. Azure DevOps implements this same graduated model with organization, project, and resource-level permissions that can be explicitly allowed, denied, or inherited.
At the organization level, built-in groups define the broadest access boundaries. Project Collection Administrators have unrestricted access to everything: all projects, all settings, all resources. Project Collection Build Administrators manage build infrastructure across all projects. Project Collection Valid Users simply have the right to exist in the organization and see project listings. The critical least-privilege principle here is minimizing Project Collection Administrators: only infrastructure and security team leads should hold this role. Create custom organization-level groups for cross-project needs rather than elevating individuals to collection admin. For example, create an 'Audit Readers' group with read-only access across projects for compliance teams.
Project-level permissions scope access within a single project. Each project has built-in groups: Project Administrators (full project control), Contributors (standard development access), Readers (view-only), and Build Administrators (pipeline management). The least-privilege approach is to default new team members to Contributors and grant additional permissions only when specific needs arise. Create custom project groups for specialized roles: 'Release Managers' who can approve production deployments but cannot modify code, 'Security Reviewers' who can view pipeline secrets but not create them, or 'Stakeholders' who can manage work items but not access source code. Avoid adding individuals directly to permission assignments; always use groups so that role changes require only group membership updates.
Repository-level permissions provide the finest granular control over source code access. Each Git repository inherits project-level permissions but can override them. You can restrict specific branches using branch policies: require pull requests to main (preventing direct pushes), require specific reviewers for changes to security-sensitive paths, and restrict who can bypass policies. For repositories containing secrets or security-critical code, remove the Contributors group's push permission and add only the specific team that owns that codebase. The 'Bypass policies when completing pull requests' permission is particularly dangerous and should be restricted to repository administrators only, as it allows merging without reviews.
Pipeline permissions control who can create, edit, and run pipelines, and critically, which pipelines can access which resources. Pipeline-level permissions determine who can edit pipeline YAML, queue builds, and view results. Resource-level permissions on Service Connections, Variable Groups, Environments, and Agent Pools determine which pipelines are authorized to use them. This second dimension is where most least-privilege violations occur: teams grant all pipelines access to production Service Connections rather than restricting to only the production deployment pipeline. Configure pipeline permissions explicitly: when a new pipeline attempts to use a Service Connection, Azure DevOps requires an administrator to authorize it rather than auto-granting access.
The production gotcha is permission inheritance creating unexpected access. Azure DevOps permissions use an Allow/Deny/Not Set model where explicit Deny overrides Allow at the same level, and permissions inherit from parent to child (organization → project → resource). A common mistake is granting broad access at the project level and then trying to restrict at the repository level without using explicit Deny. Not Set at the resource level inherits the project-level Allow, so you must explicitly Deny to restrict. Another gotcha is the difference between the 'Edit build pipeline' permission (controls who can modify the YAML) and 'Queue builds' permission (controls who can trigger runs). Teams often restrict editing but forget that anyone who can queue a build with different parameters might exploit parameter injection. Always audit permissions using the 'Permissions' diagnostic page which shows effective permissions for a specific user across all inheritance levels.
Code Example
# Azure DevOps Permissions — Least Privilege Configuration
# Organization Level: Create restricted admin group
az devops security group create \
--org https://dev.azure.com/bank-corp \
--name "Audit-Readers" \
--description "Read-only access across all projects for compliance"
# Project Level: Create custom role groups
az devops security group create \
--org https://dev.azure.com/bank-corp \
--project payments-platform \
--name "Release-Managers" \
--description "Can approve releases but cannot modify code"
az devops security group create \
--org https://dev.azure.com/bank-corp \
--project payments-platform \
--name "Security-Reviewers" \
--description "Can view secrets and approve security-sensitive changes"
# Repository Level: Restrict main branch
az repos policy approver-count create \
--org https://dev.azure.com/bank-corp \
--project payments-platform \
--repository-id payments-api \
--branch main \
--minimum-approver-count 2 \
--creator-vote-counts false \
--allow-downvotes false \
--reset-on-source-push true
# Restrict who can bypass branch policies
# Only repo admins should have this permission
# Project Settings → Repos → payments-api → Security
# "Bypass policies when completing PRs" → Deny for Contributors
# Pipeline Level: Restrict Service Connection access
# By default, new pipelines cannot use Service Connections
# Must be explicitly authorized:
az devops service-endpoint update \
--id $ENDPOINT_ID \
--org https://dev.azure.com/bank-corp \
--project payments-platform \
--enable-for-all false # Require explicit pipeline authorization
# Grant specific pipeline access to production connection
# POST https://dev.azure.com/{org}/{project}/_apis/pipelines/pipelinePermissions/endpoint/{endpointId}
curl -X PATCH \
"https://dev.azure.com/bank-corp/payments-platform/_apis/pipelines/pipelinePermissions/endpoint/$ENDPOINT_ID?api-version=7.0-preview.1" \
-H "Authorization: Basic $(echo -n :$PAT | base64)" \
-H "Content-Type: application/json" \
-d '{
"pipelines": [{
"id": 42,
"authorized": true
}]
}'
# Environment approval: only Release-Managers can approve production
# Environments → payments-production → Approvals and Checks
# Approvers: [Release-Managers] group
# Minimum approvals: 2
# Audit effective permissions for a user
az devops security permission show \
--org https://dev.azure.com/bank-corp \
--id "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87" \
--subject "[email protected]" \
--token "repoV2/payments-platform/payments-api"
# Set explicit DENY to override inherited Allow
# Example: Deny direct push to main for Contributors
az devops security permission update \
--org https://dev.azure.com/bank-corp \
--id "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87" \
--subject "Contributors" \
--token "repoV2/payments-platform/payments-api/refs/heads/main" \
--deny-bit 4 # GenericContribute (push)
# Pipeline YAML: Use template to enforce security checks
# Require all production pipelines to extend secure template
# Organization Settings → Pipelines → Settings
# "Limit variables that can be set at queue time" → Enabled
# "Restrict access to all pipelines" → Enabled (per resource)Interview Tip
A junior engineer typically describes permissions as simply adding users to the Contributors or Administrators group without understanding the hierarchical inheritance model. For a strong answer, explain the four permission levels (organization, project, resource, branch) and how Allow/Deny/Not Set interact across inheritance. Describe the critical principle that explicit Deny overrides Allow at the same level, and that Not Set inherits from parent. Walk through a concrete least-privilege design: default developers to Contributors, create custom groups for specialized roles (Release Managers, Security Reviewers), restrict branch policies on main, and explicitly authorize only specific pipelines to access production Service Connections. Discuss the common gotcha of permission inheritance creating unintended access and the need for regular permission audits using the security diagnostic pages. Mention that pipeline resource authorization is a separate dimension from user permissions and is frequently overlooked.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ Azure DevOps Permission Hierarchy │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Organization Level │ │ │ │ • Project Collection Administrators (minimize!) │ │ │ │ • Project Collection Valid Users │ │ │ │ • Custom: Audit-Readers (cross-project read) │ │ │ └────────────────────┬───────────────────────────────┘ │ │ │ inherits ↓ │ │ ┌────────────────────┴───────────────────────────────┐ │ │ │ Project Level │ │ │ │ • Project Administrators │ │ │ │ • Contributors (default developers) │ │ │ │ • Readers (stakeholders) │ │ │ │ • Custom: Release-Managers, Security-Reviewers │ │ │ └────────────────────┬───────────────────────────────┘ │ │ │ inherits ↓ │ │ ┌────────────────────┴───────────────────────────────┐ │ │ │ Resource Level (can DENY to override) │ │ │ │ ├── Repos: per-repo push/branch permissions │ │ │ │ ├── Pipelines: edit/queue/view per pipeline │ │ │ │ ├── Service Conns: explicit pipeline authz │ │ │ │ ├── Environments: approval groups │ │ │ │ └── Feeds: publish/consume per feed │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ Permission Resolution: Deny > Allow > Inherit (Not Set) │ └──────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure DevOps supports four authentication mechanisms: Personal Access Tokens (PATs) for individual developer access with fine-grained scopes, service principals for application-to-application integration without human identity, managed identities for Azure-hosted resources accessing Azure DevOps without credential management, and OAuth for third-party application authorization with user consent flows. Choose based on the identity type (human vs. machine), credential lifecycle requirements, and whether the caller runs in Azure infrastructure.
Detailed Answer
Think of these four authentication mechanisms like four different ways to enter a secure government building. A PAT is like a temporary visitor badge: you request it at the front desk, it has your name on it, expires after a set period, and grants access to only the floors you specified when you registered. A service principal is like a dedicated security clearance for a robot: the building knows this robot is authorized to enter specific rooms regardless of which human sent it. A managed identity is like biometric recognition for building infrastructure: the elevator system does not need a badge because the building itself recognizes it as a trusted component. OAuth is like a validated referral: a trusted external organization vouches for you, you show their letter at the desk, and you receive access based on what their letter authorizes.
Personal Access Tokens are the most common authentication mechanism for individual developers and simple automation scripts. A PAT is a string generated by a specific user that acts as an alternative password with configurable scope and expiration. When you use a PAT, operations are performed as that user with that user's permissions, further restricted by the PAT's scope selections (Code read, Work Items read/write, Build execute, etc.). PATs are appropriate for developer tooling (CLI access, IDE integrations, personal scripts), quick automation prototypes, and scenarios where a human identity should be attributed to the actions. The critical limitation is that PATs are tied to individual users: if the user leaves the organization, all automation using their PAT breaks. PATs also require manual rotation and cannot be centrally revoked without disabling the entire user account.
Service principals (Azure AD applications) provide machine-to-machine authentication without human identity dependency. You register an application in Azure AD, grant it permissions in Azure DevOps, and authenticate using client credentials (client ID + secret or certificate). Service principals are appropriate for CI/CD systems not hosted in Azure DevOps (Jenkins, GitHub Actions calling Azure DevOps APIs), enterprise integrations that must survive employee turnover, and applications that need to act as themselves rather than impersonating a user. Service principals use Azure AD's token endpoint, support certificate-based authentication (more secure than client secrets), and their access can be managed through Azure AD conditional access policies. Since 2023, Azure DevOps supports adding service principals directly to organizations and assigning them to security groups.
Managed identities are Azure's zero-credential authentication mechanism for resources running within Azure infrastructure. A managed identity is automatically provisioned and managed by Azure: no secrets to store, rotate, or leak. When an Azure VM, App Service, Function, or AKS pod needs to call Azure DevOps APIs, it obtains a token from the Azure Instance Metadata Service without any credentials in code or configuration. Managed identities are the strongest security posture because there are literally no credentials that can be exposed in logs, source control, or memory dumps. They are appropriate for Azure-hosted build agents, Azure Functions that trigger pipelines, and any automation running on Azure infrastructure that needs Azure DevOps access.
OAuth 2.0 is the delegated authorization framework for third-party applications that need to act on behalf of users. When a SaaS application needs to read a user's Azure DevOps repositories or create work items on their behalf, it redirects the user to Azure DevOps's authorization endpoint, the user consents to the requested permissions, and the application receives a token scoped to that user and those permissions. OAuth is appropriate for marketplace extensions, third-party integrations displayed in the Azure DevOps UI, and multi-tenant applications serving many Azure DevOps organizations. OAuth tokens automatically refresh and respect the user's current permissions without the application storing credentials.
The production gotcha is credential lifecycle management and the blast radius of compromise. PATs are the most commonly leaked credential (accidentally committed to Git, exposed in CI logs, shared in chat) and should have the shortest practical expiration (30-90 days). Service principal secrets also expire and must be rotated, though certificate-based authentication eliminates this concern. The most dangerous anti-pattern is using a Project Collection Administrator's PAT in automation: if leaked, the attacker has full organizational access. Always create dedicated service accounts or service principals for automation with minimal permissions. Implement PAT lifecycle policies using Azure DevOps organization settings to restrict maximum PAT lifetime and enforce the least-privilege scope selections.
Code Example
# Authentication Method 1: Personal Access Token (PAT)
# Create PAT: User Settings → Personal Access Tokens → New Token
# Scope: Code (Read), Work Items (Read & Write)
# Expiration: 90 days maximum
# Using PAT with Azure DevOps REST API
curl -H "Authorization: Basic $(echo -n :$PAT | base64)" \
"https://dev.azure.com/bank-corp/_apis/projects?api-version=7.0"
# Using PAT with az devops CLI
export AZURE_DEVOPS_EXT_PAT=$PAT
az devops project list --org https://dev.azure.com/bank-corp
# Authentication Method 2: Service Principal
# Step 1: Register Azure AD application
az ad app create --display-name "azure-devops-automation"
az ad sp create --id $APP_ID
# Step 2: Create client secret or certificate
az ad app credential reset --id $APP_ID --append \
--display-name "devops-automation-key" --years 1
# Step 3: Add service principal to Azure DevOps organization
# Organization Settings → Users → Add → Enter service principal app ID
# Assign to security group with appropriate permissions
# Step 4: Authenticate as service principal
# Get Azure AD token for Azure DevOps resource
TOKEN=$(curl -X POST "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "scope=499b84ac-1321-427f-aa17-267ca6975798/.default" \
-d "grant_type=client_credentials" | jq -r '.access_token')
curl -H "Authorization: Bearer $TOKEN" \
"https://dev.azure.com/bank-corp/_apis/projects?api-version=7.0"
# Authentication Method 3: Managed Identity
# For Azure VM, App Service, or AKS pod accessing Azure DevOps
# No credentials stored anywhere — Azure handles token issuance
# Python example using azure-identity SDK
# from azure.identity import ManagedIdentityCredential
# credential = ManagedIdentityCredential()
# token = credential.get_token("499b84ac-1321-427f-aa17-267ca6975798/.default")
# headers = {"Authorization": f"Bearer {token.token}"}
# response = requests.get("https://dev.azure.com/bank-corp/_apis/projects", headers=headers)
# Bash equivalent on Azure VM (using Instance Metadata Service)
TOKEN=$(curl -s -H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=499b84ac-1321-427f-aa17-267ca6975798" \
| jq -r '.access_token')
curl -H "Authorization: Bearer $TOKEN" \
"https://dev.azure.com/bank-corp/_apis/projects?api-version=7.0"
# Authentication Method 4: OAuth 2.0 (for third-party apps)
# Step 1: Register OAuth app at https://app.vsaex.visualstudio.com/app/register
# Step 2: Redirect user to authorize
# https://app.vssps.visualstudio.com/oauth2/authorize
# ?client_id=$APP_ID
# &response_type=Assertion
# &state=$RANDOM_STATE
# &scope=vso.code_write vso.work_write
# &redirect_uri=https://myapp.com/callback
# Step 3: Exchange authorization code for token
curl -X POST "https://app.vssps.visualstudio.com/oauth2/token" \
-d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
-d "client_assertion=$CLIENT_SECRET" \
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "assertion=$AUTH_CODE" \
-d "redirect_uri=https://myapp.com/callback"
# Organization policy: Restrict PAT lifetime and scope
# Organization Settings → Policies:
# - Maximum PAT lifetime: 90 days
# - Restrict PAT scope: Enforce minimum scope
# - Allow public projects: Disabled
# - External guest access: DisabledInterview Tip
A junior engineer typically uses PATs for everything without understanding the security implications or alternatives. For a strong answer, explain the decision matrix: PATs for individual developer access and prototyping (shortest expiration, minimal scope), service principals for machine-to-machine integration that must survive employee turnover, managed identities for any workload running in Azure (zero credentials, zero rotation, zero leakage risk), and OAuth for third-party applications needing delegated user access. Discuss the blast radius of each: a leaked PAT exposes one user's permissions, a leaked service principal secret exposes the application's permissions. Emphasize that managed identities are the gold standard because there are no credentials to leak. Mention organizational policies for PAT lifetime restriction and the importance of auditing which PATs and service principals exist in the organization. Describe how conditional access policies can restrict service principal authentication to specific network locations.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ Azure DevOps Authentication Decision Matrix │ │ │ │ ┌────────────────┐ ┌─────────────────────────────────┐ │ │ │ Who/What is │ │ Recommended Authentication │ │ │ │ authenticating?│ │ │ │ │ ├────────────────┤ ├─────────────────────────────────┤ │ │ │ Developer │→ │ PAT (90-day max, scoped) │ │ │ │ (human, local) │ │ or Azure AD interactive login │ │ │ ├────────────────┤ ├─────────────────────────────────┤ │ │ │ CI/CD system │→ │ Service Principal (cert-based) │ │ │ │ (external) │ │ or Workload Identity Federation │ │ │ ├────────────────┤ ├─────────────────────────────────┤ │ │ │ Azure resource │→ │ Managed Identity (zero creds) │ │ │ │ (VM/AKS/Func) │ │ Best security posture │ │ │ ├────────────────┤ ├─────────────────────────────────┤ │ │ │ Third-party app│→ │ OAuth 2.0 (delegated consent) │ │ │ │ (SaaS/extension│ │ User grants scoped access │ │ │ └────────────────┘ └─────────────────────────────────┘ │ │ │ │ Security Ranking (best → worst): │ │ Managed Identity > Service Principal (cert) > OAuth > │ │ Service Principal (secret) > PAT │ └──────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure DevOps integrates with Slack via the Azure DevOps app for Slack, with Microsoft Teams via the Azure DevOps connector and Azure Boards/Pipelines tabs, and with email through built-in subscription notifications. Each channel can be configured to receive specific events (build completions, PR reviews, work item changes) filtered by project, area path, or pipeline, enabling teams to route relevant information to the right communication channel without notification fatigue.
Detailed Answer
Think of Azure DevOps notification integrations like a news wire service distributing stories to different media outlets. A breaking news event (build failure) needs to reach the on-call TV channel (Slack ops channel) immediately, while a feature story (work item state change) goes to the morning newspaper (email digest), and a discussion topic (PR comment) goes to the team's regular meeting room (Teams channel). The same underlying events are routed to different channels based on urgency, audience, and context. The integration layer acts as the editorial desk: filtering, formatting, and routing events to maximize signal and minimize noise.
The Microsoft Teams integration is the deepest because both products share the Microsoft ecosystem. The Azure Boards app for Teams adds interactive tabs to channels where team members view and interact with work items, Kanban boards, and sprint backlogs directly within Teams. The Azure Pipelines app posts rich cards showing build and release status with direct links to logs, and supports pipeline approval actions directly from Teams messages. Connectors (incoming webhooks configured on Azure DevOps subscriptions) push formatted event notifications to channels. The key architectural choice is between the full app experience (tabs, messaging extensions, action buttons) and simple connector notifications (one-way formatted messages). The app provides bidirectional interaction while connectors are simpler to configure for one-way alerts.
The Slack integration uses the Azure DevOps app for Slack (formerly Azure Pipelines app). After installing the app to your Slack workspace and authenticating with your Azure DevOps organization, you subscribe channels to specific events using slash commands. The app supports subscribing to pipeline events (build completion, failure, approval requests), repository events (pull request created, completed, commented), and work item events (created, state changed, assigned). Each subscription can be filtered: only failed builds, only PRs targeting main branch, only bugs assigned to the current sprint. The app renders rich messages with status icons, links to Azure DevOps resources, and action buttons for common operations like approving a pipeline directly from Slack.
Email notifications use Azure DevOps's built-in subscription system. Every user has personal notification subscriptions configured at the individual, team, project, or organization level. Team-level subscriptions send emails to all team members for events relevant to that team: work items assigned to the team's area path, builds from their pipelines, or PRs in their repositories. Administrators configure organizational default subscriptions that all users receive unless they opt out. The email system supports digest mode (batching notifications into periodic summaries rather than individual emails), custom SMTP servers for organizations that need to route through corporate mail infrastructure, and HTML templates for branding. For Azure DevOps Server on-premises, email requires configuring an SMTP server in the administration console.
Advanced integration patterns use Azure DevOps service hooks, which are webhooks triggered by events and delivered to any HTTP endpoint. Service hooks support Slack incoming webhooks directly, but they also integrate with Azure Functions, Logic Apps, and custom APIs for complex routing logic. For example, a Logic App can receive a build failure event, query the Git log for the commit author, look up their preferred notification channel in a configuration store, and route the alert to their personal Slack DM rather than a noisy shared channel. This event-driven architecture enables sophisticated alerting patterns like PagerDuty escalation for production deployment failures, Jira cross-updates when linked Azure DevOps work items change state, and custom dashboards aggregating events across multiple projects.
The production gotcha is notification fatigue leading teams to mute or ignore channels entirely. The most common mistake is subscribing a channel to all events from a project without filtering, resulting in hundreds of messages per day where important alerts drown in noise. Best practice is creating purpose-specific channels: a #payments-alerts channel subscribed only to failed production builds and blocked PRs, a #payments-deploys channel showing only deployment completions, and keeping the general team channel free of automated messages. Another issue is duplicate notifications: if you configure both the Teams connector and individual email subscriptions, team members receive the same information twice. Audit all notification paths and ensure each event reaches each person through exactly one channel appropriate to its urgency.
Code Example
# Azure DevOps + Slack Integration
# Install Azure DevOps app in Slack workspace
# Slack App Directory → Search "Azure DevOps" → Install
# Authenticate: /azdevops signin
# Subscribe Slack channel to pipeline events
/azdevops subscribe https://dev.azure.com/bank-corp/payments-platform
# Filter subscriptions to reduce noise
/azdevops subscribe https://dev.azure.com/bank-corp/payments-platform \
--pipeline "payments-api-cicd" --event "build.complete" --status "failed"
# Subscribe to PR events for specific repo
/azdevops subscribe https://dev.azure.com/bank-corp/payments-platform/_git/payments-api \
--event "pullrequest.created" --target-branch "main"
# List current subscriptions
/azdevops subscriptions
# Azure DevOps + Microsoft Teams Integration
# Option 1: Install Azure DevOps Tabs in Teams channel
# Teams channel → + (Add tab) → Azure DevOps → Select board/backlog
# Provides interactive Kanban board within Teams
# Option 2: Configure Azure Pipelines app for Teams
# Teams → Apps → Azure Pipelines → Add to team
# @Azure Pipelines subscribe https://dev.azure.com/bank-corp/payments-platform
# @Azure Pipelines subscribe https://dev.azure.com/bank-corp/payments-platform --pipeline "payments-api-cicd"
# Option 3: Incoming Webhook Connector (simple notifications)
# Teams channel → Connectors → Incoming Webhook → Configure
# Use the webhook URL in Azure DevOps Service Hook
# Azure DevOps Service Hooks (for custom integrations)
# Create service hook subscription via REST API
curl -X POST \
"https://dev.azure.com/bank-corp/_apis/hooks/subscriptions?api-version=7.0" \
-H "Authorization: Basic $(echo -n :$PAT | base64)" \
-H "Content-Type: application/json" \
-d '{
"publisherId": "tfs",
"eventType": "build.complete",
"resourceVersion": "1.0",
"consumerId": "slack",
"consumerActionId": "postMessageToChannel",
"publisherInputs": {
"buildStatus": "Failed",
"projectId": "payments-platform"
},
"consumerInputs": {
"url": "https://hooks.slack.com/services/T00/B00/xxxx"
}
}'
# Email Notifications — Team subscription
# Project Settings → Notifications → New Subscription
# Category: Build, Filter: payments-api-cicd, Status: Failed
# Delivery: Team members of "Payments Team"
# Custom notification subscription via CLI
az devops notification subscription create \
--org https://dev.azure.com/bank-corp \
--project payments-platform \
--event-type "ms.vss-build.build-completed-event" \
--filter "buildDefinition.name = 'payments-api-cicd' AND status = 'Failed'" \
--scope team --team-id $TEAM_ID
# Service Hook to Azure Function (complex routing)
# Azure Function receives event → looks up on-call → routes to PagerDuty
# {
# "eventType": "build.complete",
# "resource": { "status": "failed", "definition": { "name": "payments-api" } },
# "resourceContainers": { "project": { "name": "payments-platform" } }
# }Interview Tip
A junior engineer typically describes setting up a single notification channel without considering the routing strategy that prevents notification fatigue. For a strong answer, explain the tiered notification architecture: critical alerts (production failures, blocked deployments) route to high-urgency channels with immediate delivery, standard notifications (PR reviews, build completions) route to team channels, and low-priority updates (work item state changes) go to email digests. Describe the three integration mechanisms: native apps (richest experience with bidirectional interaction), connectors/webhooks (simpler one-way notifications), and service hooks to custom endpoints (maximum flexibility). Discuss the common anti-pattern of subscribing to everything and creating noise that causes teams to ignore the channel entirely. Mention that Azure DevOps service hooks enable event-driven architectures where a single event can trigger multiple downstream actions through Azure Functions or Logic Apps.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ Azure DevOps Notification Routing Architecture │ │ │ │ Azure DevOps Events │ │ ┌─────────────────────┐ │ │ │ • Build failed │ │ │ │ • PR created │ │ │ │ • Work item changed │ │ │ │ • Deploy completed │ │ │ └──────────┬──────────┘ │ │ │ │ │ ┌─────┼─────────────────────┐ │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ ┌────────┐ ┌──────────────┐ ┌──────────┐ │ │ │ Slack │ │ MS Teams │ │ Email │ │ │ │ App │ │ App/Connector│ │ Subscr. │ │ │ ├────────┤ ├──────────────┤ ├──────────┤ │ │ │#alerts │ │ Ops Channel │ │ Digest │ │ │ │(failed │ │ (approvals, │ │ (daily │ │ │ │ builds)│ │ deploys) │ │ summary)│ │ │ │ │ │ │ │ │ │ │ │#deploys│ │ Dev Channel │ │ Instant │ │ │ │(status)│ │ (PRs, boards)│ │ (critical│ │ │ └────────┘ └──────────────┘ │ only) │ │ │ └──────────┘ │ │ │ │ Rule: One event → One channel → Right audience │ └──────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Azure DevOps compliance setup combines audit streaming (to Log Analytics, Splunk, or S3) for tamper-proof activity records, branch policies enforcing code review and build validation before merge, required reviewers auto-assigned for sensitive code paths, environment approval gates for deployment governance, and organizational policies restricting PAT lifetimes and external access. Together these controls satisfy SOC2, ISO 27001, and regulatory frameworks requiring separation of duties, change traceability, and access audit trails.
Detailed Answer
Think of setting up Azure DevOps for compliance like designing the security system for a pharmaceutical clean room. Every person entering is logged (audit), the airlock requires two-person authorization before opening (required reviewers), the manufacturing equipment only activates after quality control signs off (approval gates), every batch has a traceable chain from raw materials to finished product (branch-to-deployment traceability), and all logs are sent to an immutable archive that operators cannot delete (audit streaming). Each control exists not because it makes production faster, but because regulators require evidence that changes were authorized, reviewed, and traceable. Azure DevOps provides all these controls natively.
Audit logging in Azure DevOps captures every significant action: user authentication, permission changes, repository access, pipeline modifications, work item updates, and administrative operations. Azure DevOps Services provides audit streaming that continuously exports audit events to external systems: Azure Monitor Log Analytics, Splunk, or Azure Event Hubs (which can forward to S3, Datadog, or any SIEM). The critical property of streamed audit logs is immutability: once exported to the external system, Azure DevOps administrators cannot modify or delete the records, satisfying the separation of duties requirement. Compliance teams can query historical events to answer questions like 'who approved this production deployment', 'when was this branch policy modified', and 'which PATs were created in the last 30 days'. Audit log retention in Azure DevOps itself is 90 days; streaming ensures permanent retention in your SIEM.
Branch policies enforce code quality gates before any change reaches protected branches. For compliance, the minimum configuration on production branches (main, release/*) includes: minimum reviewer count (at least 2 for separation of duties), build validation (automated tests must pass), linked work items (traceability from requirement to code), comment resolution (all review feedback must be addressed), and merge strategy restriction (squash or rebase to maintain clean history). Branch policies cannot be bypassed by regular contributors: only users with explicit 'Bypass policies' permission can merge without meeting all requirements, and this permission should be restricted to repository administrators with audit logging of every bypass. The bypass itself appears in audit logs, creating accountability even for emergency changes.
Required reviewers automatically add specific approvers when changes touch sensitive code paths. You configure path-based reviewer policies: any change to /src/security/*, /infrastructure/production/*, or /pipeline-templates/* automatically requires approval from the Security team or Platform team before merge. This ensures that changes to authentication logic, production infrastructure, or shared pipeline templates always receive expert review regardless of which developer made the change. Required reviewers cannot approve their own changes (configurable), preventing the scenario where a developer modifies security-critical code and approves their own PR. Combined with minimum reviewer count, this creates multi-party authorization for sensitive changes.
Environment approval gates provide the deployment governance layer. Production environments are configured with manual approval checks requiring authorization from Release Managers before any pipeline deploys to production. Business hours checks prevent deployments during off-hours when incident response capacity is limited. Required template checks ensure all production pipelines use organization-approved templates that include mandatory security scanning, artifact signing, and compliance checks. The approval history creates an auditable record of who authorized each production deployment, when, and what version was deployed. This satisfies Change Advisory Board (CAB) requirements in ITIL frameworks and deployment authorization requirements in SOC2 CC8.1.
Organizational policies provide the top-level governance framework. Administrators configure: maximum PAT lifetime (30-90 days to limit credential exposure), external guest access restrictions (preventing unauthorized external collaboration), public project prohibition (ensuring all code remains private), alternate authentication restriction (disabling SSH keys or basic auth if not needed), and conditional access policies through Azure AD (requiring MFA, compliant devices, or specific network locations for access). These policies apply uniformly across all projects in the organization, preventing individual teams from creating security gaps.
The production gotcha is treating compliance controls as set-and-forget configuration rather than continuously monitored guardrails. Branch policies can be modified by project administrators, required reviewers can be removed from policies, and environment checks can be deleted. Without monitoring for changes to these controls, a compromised administrator account could disable protections without anyone noticing until the next audit. Best practice is alerting on audit events that modify security-relevant configurations: branch policy changes, permission group membership changes, environment check modifications, and organizational policy changes. Set up alerts that notify the security team immediately when these events occur, creating a detective control that complements the preventive controls.
Code Example
# Compliance Setup for Azure DevOps
# 1. Configure Audit Streaming to Log Analytics
# Organization Settings → Auditing → Streams → New Stream
# Or via REST API:
curl -X POST \
"https://auditservice.dev.azure.com/bank-corp/_apis/audit/streams?api-version=7.0-preview.1" \
-H "Authorization: Basic $(echo -n :$PAT | base64)" \
-H "Content-Type: application/json" \
-d '{
"consumerType": "AzureMonitorLogs",
"consumerInputs": {
"WorkspaceId": "12345-abcde-67890",
"SharedKey": "base64encodedkey=="
},
"status": "enabled"
}'
# Query audit logs for compliance evidence
# KQL query in Log Analytics:
# AzureDevOpsAuditing
# | where OperationName == "Policy.PolicyConfigModified"
# | where Timestamp > ago(30d)
# | project Timestamp, ActorUPN, ProjectName, Details
# 2. Branch Policies — enforce code review and build validation
# Minimum 2 reviewers, no self-approval, reset on push
az repos policy approver-count create \
--org https://dev.azure.com/bank-corp \
--project payments-platform \
--repository-id payments-api \
--branch main \
--minimum-approver-count 2 \
--creator-vote-counts false \
--reset-on-source-push true \
--blocking true
# Require build validation (tests must pass)
az repos policy build create \
--org https://dev.azure.com/bank-corp \
--project payments-platform \
--repository-id payments-api \
--branch main \
--build-definition-id 42 \
--display-name "CI Build Validation" \
--valid-duration 720 \
--queue-on-source-update-only true \
--blocking true
# Require linked work items (traceability)
az repos policy work-item-linking create \
--org https://dev.azure.com/bank-corp \
--project payments-platform \
--repository-id payments-api \
--branch main \
--blocking true
# Require comment resolution
az repos policy comment-required create \
--org https://dev.azure.com/bank-corp \
--project payments-platform \
--repository-id payments-api \
--branch main \
--blocking true
# 3. Required Reviewers for sensitive paths
az repos policy required-reviewer create \
--org https://dev.azure.com/bank-corp \
--project payments-platform \
--repository-id payments-api \
--branch main \
--required-reviewer-ids "[email protected]" \
--path-filter "/src/security/*;/src/auth/*;/infrastructure/prod/*" \
--message "Security team review required for auth/security changes" \
--blocking true
# 4. Environment Approval Gates
# Configure via Azure DevOps UI or REST API:
# POST https://dev.azure.com/{org}/{project}/_apis/pipelines/checks/configurations
curl -X POST \
"https://dev.azure.com/bank-corp/payments-platform/_apis/pipelines/checks/configurations?api-version=7.0-preview.1" \
-H "Authorization: Basic $(echo -n :$PAT | base64)" \
-H "Content-Type: application/json" \
-d '{
"type": {"name": "Approval"},
"settings": {
"approvers": [{"id": "release-managers-group-id"}],
"minRequiredApprovers": 2,
"instructions": "Verify staging tests passed and change window is approved",
"executionOrder": "anyOrder",
"timeout": 4320
},
"resource": {
"type": "environment",
"id": "payments-production-env-id"
}
}'
# 5. Organizational Policies
# Organization Settings → Policies
# - Maximum PAT lifetime: 90 days
# - Third-party application access: Disabled
# - SSH authentication: Disabled (if not needed)
# - Allow public projects: Off
# - External guest access: Restricted to specific Azure AD tenants
# 6. Alert on compliance-relevant changes
# Azure Monitor Alert Rule (KQL):
# AzureDevOpsAuditing
# | where OperationName in (
# "Policy.PolicyConfigModified",
# "Policy.PolicyConfigRemoved",
# "Security.ModifyPermission",
# "Security.ModifyAccessControlLists"
# )
# | where Timestamp > ago(5m)
# Trigger: Alert security team immediatelyInterview Tip
A junior engineer typically lists individual security features without connecting them to compliance frameworks or explaining why each control exists. For a strong answer, map controls to compliance requirements: branch policies with minimum reviewers satisfy separation of duties (SOC2 CC6.1), audit streaming to immutable external storage satisfies tamper-proof logging (ISO 27001 A.12.4), environment approvals satisfy change authorization (ITIL CAB process), and required reviewers for sensitive paths satisfy access control for critical functions. Explain the defense-in-depth approach: preventive controls (policies that block unauthorized changes), detective controls (audit alerts on policy modifications), and corrective controls (ability to identify and revert unauthorized changes using audit trail). Discuss the operational reality that compliance is not a one-time setup but requires continuous monitoring for policy drift, regular access reviews, and periodic audit evidence collection. Mention that Azure DevOps compliance features work best when integrated with Azure AD conditional access for device compliance and network-based access restrictions.
💬 Comments
Quick Answer
Branch policies protect branches by requiring pull request reviews, successful build validation, linked work items, and merge strategy enforcement before code can be merged. They are configured per branch in Azure Repos settings.
Detailed Answer
Think of a building code inspector who checks every construction plan before approving it. Without the inspector, anyone could build anything — with the inspector, every change is reviewed for safety and quality. Branch policies in Azure DevOps are that inspector for your code.
Branch policies are rules configured on specific branches (typically main or release branches) that must be satisfied before a pull request can complete. They prevent direct pushes to protected branches, forcing all changes through the PR workflow. This ensures code review, automated testing, and compliance checks happen before code reaches production.
The key policies available are: Require a minimum number of reviewers (e.g., 2 approvals), Require linked work items (traceability), Build validation (automatically trigger a pipeline and require it to pass), Comment resolution (all PR comments must be resolved), Merge strategy (squash, rebase, or merge commit), and Automatically include reviewers (add specific people or teams based on file paths changed). Build validation is the most powerful — it runs your CI pipeline against the PR branch and blocks merge if tests fail.
At production scale, teams configure different policies per branch. The main branch requires 2 reviewers, build validation, and linked work items. Release branches require the same plus an additional approval from a lead. Feature branches have no policies, allowing developers to iterate freely. Teams also use path-based reviewer rules — changes to infrastructure/ automatically add the platform team as required reviewers, while changes to src/payments/ add the payments team.
The non-obvious gotcha is that branch policies only apply to pull requests, not to users with 'Bypass policies' permission. Admins and service accounts often have this permission for emergency fixes, but it should be audited. Also, build validation policies run against the PR merge commit (the result of merging the source into the target), not just the source branch — this catches integration issues that would not appear if you only tested the feature branch in isolation.
Code Example
# Configure branch policy via Azure DevOps REST API # Set minimum 2 reviewers on the main branch az repos policy approver-count create \ --project payments-platform \ --repository-id payments-api \ --branch main \ --minimum-approver-count 2 \ --creator-vote-counts false \ --allow-downvotes false \ --reset-on-source-push true \ --blocking true \ --enabled true # Add build validation policy az repos policy build create \ --project payments-platform \ --repository-id payments-api \ --branch main \ --build-definition-id 42 \ --display-name "CI Build Validation" \ --queue-on-source-update-only true \ --valid-duration 720 \ --blocking true \ --enabled true # Require linked work items az repos policy work-item-linking create \ --project payments-platform \ --repository-id payments-api \ --branch main \ --blocking true \ --enabled true
Interview Tip
A junior engineer typically says branch policies require PR reviews, but for a senior role, the interviewer is actually looking for the full policy configuration strategy. Describe the key policies (reviewer count, build validation, linked work items, comment resolution), how build validation runs against the merge commit not just the source branch, and how different branches get different policy levels. Mentioning path-based automatic reviewer assignment and the bypass policies audit concern shows enterprise governance awareness.
💬 Comments
Quick Answer
Azure DevOps provides a built-in import tool that converts TFVC repositories to Git, preserving up to 180 days of history. For full history migration, use the git-tfs tool which converts all changesets to Git commits while preserving author information and timestamps.
Detailed Answer
Think of converting a paper filing cabinet into a digital document management system. You can scan the most recent 6 months of documents quickly (built-in import), or you can scan every document back to day one (git-tfs) — the second approach takes longer but gives you the complete historical record.
TFVC (Team Foundation Version Control) is a centralized version control system where all history lives on the server. Git is distributed — every clone has the full history. Migration from TFVC to Git is a one-way conversion that transforms TFVC changesets into Git commits. Azure DevOps provides a built-in import feature under Repos > Import Repository that handles simple migrations with up to 180 days of history.
The built-in import works by selecting TFVC as the source type, specifying the TFVC path (e.g., $/ProjectName/Main), and choosing how much history to import (up to 180 days or just the latest version). It creates a new Git repository with the converted history. For full history beyond 180 days, the git-tfs open-source tool performs a complete conversion — it replays every TFVC changeset as a Git commit, preserving authors, dates, and commit messages. The command is: git tfs clone https://dev.azure.com/org $/Project/Path --branches=all.
At production scale, large TFVC repositories with thousands of changesets and multiple branches can take hours to convert with git-tfs. Teams should plan the migration during a quiet period, freeze TFVC commits during conversion, verify the Git history matches TFVC changeset counts, and update all CI/CD pipelines to point to the new Git repository. Branch mappings need careful planning — TFVC branches are directories, while Git branches are lightweight pointers.
The non-obvious gotcha is that TFVC and Git handle large binary files very differently. TFVC stores binaries efficiently server-side, but Git clones include all binary history in every clone, bloating repository size. Teams should set up Git LFS (Large File Storage) before migration and configure .gitattributes to track binary file types. Also, TFVC workspace mappings (cloaking, mapping specific paths) have no Git equivalent — the entire repository is cloned.
Code Example
# Built-in import via Azure DevOps UI: # Repos > Import Repository > Source type: TFVC # Path: $/PaymentsPlatform/Main # History: 180 days # Full history migration using git-tfs git tfs clone https://dev.azure.com/company $/PaymentsPlatform/Main payments-api-git --branches=all # Verify commit count matches changeset count cd payments-api-git git log --oneline | wc -l # Should match TFVC changeset count # Set up Git LFS for binary files before pushing git lfs install git lfs track "*.dll" "*.exe" "*.zip" "*.jar" git add .gitattributes git commit -m "Configure Git LFS for binary files" # Push to the new Azure DevOps Git repository git remote add origin https://dev.azure.com/company/PaymentsPlatform/_git/payments-api git push -u origin --all # Push all branches git push origin --tags # Push all tags
Interview Tip
A junior engineer typically says they used the import button in Azure DevOps, but for a senior role, the interviewer is actually looking for migration planning and risk management. Explain the difference between the built-in 180-day import and full git-tfs conversion, how to handle binary files with Git LFS, the need for a TFVC freeze during migration, and pipeline updates after migration. Mentioning branch mapping differences between TFVC directories and Git branches shows understanding of both version control models.
💬 Comments
Quick Answer
Variable Groups store shared variables across pipelines, and can link to Azure Key Vault to automatically pull secrets. Pipeline variables marked as secret are masked in logs. Key Vault integration eliminates storing secrets in Azure DevOps — the pipeline fetches them at runtime from the vault.
Detailed Answer
Think of a hotel safe at the front desk. Instead of giving every guest a copy of the master key (hardcoded secrets), the hotel stores valuables in a central safe (Key Vault) and gives each guest a temporary access card (service connection) that works only during their stay (pipeline run). The guest never sees the actual combination — they just get their valuables when needed.
Variable Groups in Azure DevOps are collections of key-value pairs that can be shared across multiple pipelines. You create them under Pipelines > Library. Variables can be plain text (API URLs, environment names) or marked as secret (passwords, tokens). Secret variables are encrypted at rest and masked in pipeline logs — if a step accidentally echoes a secret value, Azure DevOps replaces it with ***.
Key Vault integration takes this further. Instead of storing secrets in Azure DevOps, you link a Variable Group to an Azure Key Vault. The Variable Group acts as a bridge — it lists which Key Vault secrets to fetch, and the pipeline accesses them as regular variables at runtime. The secrets never leave Key Vault until the pipeline needs them, and they are never stored in Azure DevOps. Changes to secrets in Key Vault are automatically reflected in the next pipeline run without updating the Variable Group.
At production scale, teams create Variable Groups per environment: payments-dev-vars, payments-staging-vars, payments-prod-vars. Each links to an environment-specific Key Vault. The pipeline YAML references the appropriate group using a variable template or conditional insertion. Permissions on Variable Groups control which pipelines can access which secrets — the production Variable Group is locked to only the production pipeline. Audit logs track which pipeline accessed which secrets.
The non-obvious gotcha is that Key Vault-linked Variable Groups only fetch secrets at the start of the pipeline run, not dynamically during execution. If a secret is rotated in Key Vault mid-pipeline, the pipeline uses the old value. Also, Key Vault integration requires an Azure service connection with Get and List permissions on the vault — if the service principal's access is too broad, it could read secrets from other applications' vaults. Always scope Key Vault access policies to specific secrets, not the entire vault.
Code Example
# Reference a Variable Group in YAML pipeline
variables:
- group: payments-prod-secrets # Links to Key Vault-backed Variable Group
- group: payments-prod-config # Non-secret configuration variables
steps:
- script: |
echo "Deploying to $(ENVIRONMENT)" # Plain variable from config group
echo "DB connection available: $(DB_HOST)" # Secret from Key Vault — masked in logs
env:
DB_PASSWORD: $(DB_PASSWORD) # Map secret to environment variable
# Create Variable Group linked to Key Vault via CLI
az pipelines variable-group create \
--name payments-prod-secrets \
--project PaymentsPlatform \
--authorize true \
--variables managed_by=key-vault-link # az requires >=1 variable at creation; real secrets are linked from Key Vault next
# Link to Key Vault (done via UI: Library > Variable Group > Link to Key Vault)
# Select Azure subscription service connection
# Select Key Vault name: payments-prod-kv
# Select secrets: DB_PASSWORD, API_KEY, JWT_SECRET
# Restrict Variable Group to specific pipelines
# Library > payments-prod-secrets > Pipeline permissions > Only allow selected pipelinesInterview Tip
A junior engineer typically stores secrets as pipeline variables, but for a senior role, the interviewer is actually looking for Key Vault integration and access governance. Explain the progression: plain variables → secret variables (masked in logs) → Key Vault-linked Variable Groups (secrets never stored in ADO). Describe environment-specific Variable Groups, pipeline-scoped permissions, and the audit trail. Mentioning the secret rotation timing gotcha and least-privilege Key Vault access policies shows security-conscious DevOps maturity.
💬 Comments
Quick Answer
Microsoft-hosted agents are managed VMs that spin up fresh for each job — zero maintenance but limited customization. Self-hosted agents run on your own infrastructure with persistent state, custom tools, and network access to private resources. Use self-hosted when you need VPN access, GPU, specific software, or faster builds with caching.
Detailed Answer
Think of renting a car vs owning one. A rental (Microsoft-hosted) is ready when you need it, always clean, and you do not worry about maintenance — but you cannot customize it or leave your tools in the trunk. Your own car (self-hosted) has your tools, your GPS settings, and can go to places rentals cannot — but you handle oil changes and insurance.
Microsoft-hosted agents are virtual machines managed by Azure DevOps. Each pipeline job gets a fresh VM (Ubuntu, Windows, or macOS), runs the job, and the VM is destroyed afterward. They come pre-installed with common tools (Node.js, .NET, Docker, kubectl) and have internet access. The advantage is zero maintenance. The disadvantage is no persistent state — every build starts cold, downloading dependencies from scratch.
Self-hosted agents run on machines you control — physical servers, VMs, or Kubernetes pods. You install the Azure Pipelines agent software, register it with your organization, and assign it to an agent pool. The agent persists between jobs, so build caches (npm cache, Docker layers, Maven repository) survive across runs, making builds significantly faster. Self-hosted agents can access private networks (databases, internal APIs, on-premises resources) that Microsoft-hosted agents cannot reach.
At production scale, teams use both types strategically. Microsoft-hosted for open-source projects and simple CI builds. Self-hosted for production deployments that need VPN access, builds requiring licensed software or GPUs, and performance-critical pipelines where caching matters. Self-hosted agents on Kubernetes use KEDA to auto-scale — zero agents when idle, scaling up to 20 during peak CI hours. Agent capabilities (user-defined labels like has-docker, has-gpu) let pipelines demand specific agent features.
The non-obvious gotcha is security. Self-hosted agents run pipeline code on your infrastructure — a malicious pipeline could access the host filesystem, network, or credentials cached from previous jobs. Use separate agent pools for different trust levels (one pool for internal teams, another for external contributors), run agents in containers for isolation, and never run self-hosted agents on machines with production access. Regularly rotate agent registration tokens and audit which pipelines use which pools.
Code Example
# Install self-hosted agent on Linux mkdir /opt/azure-agent && cd /opt/azure-agent curl -O https://vstsagentpackage.azureedge.net/agent/3.232.1/vsts-agent-linux-x64-3.232.1.tar.gz tar xzf vsts-agent-linux-x64-3.232.1.tar.gz ./config.sh --url https://dev.azure.com/company --auth pat --token $PAT --pool payments-agents --agent payments-agent-01 ./svc.sh install # Install as systemd service ./svc.sh start # Start the agent # YAML pipeline targeting self-hosted agent pool pool: name: payments-agents # Self-hosted pool name demands: - has-docker # Agent must have Docker installed - has-kubectl # Agent must have kubectl # YAML pipeline using Microsoft-hosted agent pool: vmImage: ubuntu-latest # Microsoft-managed Ubuntu VM # Kubernetes-based auto-scaling agents with KEDA # Deploy agent as a Deployment with KEDA ScaledJob # Scales from 0 to N based on pending pipeline jobs
Interview Tip
A junior engineer typically uses whatever agent type was already configured, but for a senior role, the interviewer is actually looking for strategic agent architecture. Explain when to use each type (hosted for simplicity, self-hosted for network access and caching), how agent capabilities and demands work, and how KEDA scales Kubernetes-based agents from zero. The security dimension is critical — describe pool isolation, container-based agents, and why you never run self-hosted agents on production machines. This shows you think about both performance and security in CI/CD design.
💬 Comments
Quick Answer
Create a central template repository with parameterized YAML templates for common patterns (build, test, deploy). Other repositories reference templates using resources.repositories and template references. Version templates with Git tags so consumers can pin specific versions and upgrade independently.
Detailed Answer
Think of a franchise operations manual. Every restaurant location follows the same recipes and procedures (templates), but each location can customize portion sizes and local ingredients (parameters). The manual lives in one place (template repo), and locations reference a specific edition (version tag) so a manual update does not surprise everyone simultaneously.
Azure DevOps supports YAML templates that extract reusable pipeline logic into shared files. A template can define stages, jobs, steps, or variables with parameters that consumers fill in. Templates live in a dedicated repository (e.g., pipeline-templates) and are referenced by other repositories using the resources.repositories section in their azure-pipelines.yml.
The template repository structure typically has: templates/build/dotnet.yml (build template for .NET projects), templates/build/node.yml (for Node.js), templates/deploy/kubernetes.yml (Kubernetes deployment), templates/security/scan.yml (security scanning steps), and templates/stages/standard-pipeline.yml (full pipeline with build, test, scan, deploy stages). Each template accepts parameters like service name, Docker image, Kubernetes namespace, and environment. The consuming pipeline passes these parameters and gets a complete, standardized pipeline.
At production scale with 50+ repositories, governance matters. Tag template releases with semantic versions (v1.0.0, v1.1.0). Consuming repositories pin to a specific tag using ref: refs/tags/v1.0.0. When a template is updated, teams upgrade at their own pace. A shared Variable Group provides organization-wide defaults (registry URL, cluster name) that templates reference. Template changes go through the same PR review process as application code, with build validation that runs template tests against sample consumers.
The non-obvious gotcha is that template parameters have limited type checking. A typo in a parameter name silently passes an empty value rather than failing. Teams should add validation steps at the start of templates that check required parameters are non-empty. Also, templates from external repositories are fetched at pipeline compile time, not runtime — if the template repo is unavailable, all pipelines that reference it fail to start. Use multiple replicas or mirrors of the template repository for resilience.
Code Example
# Template repository: pipeline-templates/templates/stages/standard-pipeline.yml
parameters:
- name: serviceName
type: string # Required parameter
- name: dockerImage
type: string
- name: kubeNamespace
type: string
default: default # Optional with default
stages:
- stage: Build
jobs:
- job: BuildAndPush
pool:
vmImage: ubuntu-latest
steps:
- script: docker build -t ${{ parameters.dockerImage }} . # Build using parameterized image name
- script: docker push ${{ parameters.dockerImage }} # Push to registry
- stage: DeployProd
dependsOn: Build
jobs:
- deployment: Deploy
environment: production # Approval gate configured here
strategy:
runOnce:
deploy:
steps:
- script: kubectl set image deploy/${{ parameters.serviceName }} app=${{ parameters.dockerImage }} -n ${{ parameters.kubeNamespace }}
# Consumer repo: payments-api/azure-pipelines.yml
resources:
repositories:
- repository: templates
type: git
name: PaymentsPlatform/pipeline-templates # Template repo reference
ref: refs/tags/v1.2.0 # Pin to specific version
extends:
template: templates/stages/standard-pipeline.yml@templates
parameters:
serviceName: payments-api
dockerImage: registry.company.com/payments-api:$(Build.BuildId)
kubeNamespace: paymentsInterview Tip
A junior engineer typically copies pipeline YAML between repositories, but for a senior role, the interviewer is actually looking for template architecture at scale. Explain the central template repository pattern, parameterized templates with sensible defaults, semantic version tagging for controlled upgrades, and the extends keyword for enforcing template usage. Mentioning parameter validation, template repo availability as a dependency risk, and how template changes go through PR review shows enterprise platform engineering thinking.
💬 Comments
Quick Answer
Azure DevOps provides five integrated services: Azure Boards for work tracking and agile planning, Azure Repos for Git version control, Azure Pipelines for CI/CD automation, Azure Test Plans for manual and exploratory testing, and Azure Artifacts for package management. Together they form a complete DevOps lifecycle platform.
Detailed Answer
Think of Azure DevOps like a modern automobile factory with five specialized departments working in coordination. Azure Boards is the planning office where managers track which cars are in design, assembly, testing, or ready for delivery. Azure Repos is the engineering vault holding all blueprints and design revisions. Azure Pipelines is the assembly line that takes designs from the vault, builds the cars, tests them, and delivers them to dealerships. Azure Test Plans is the quality assurance department running safety inspections. Azure Artifacts is the parts warehouse storing reusable components that multiple car models share. Each department functions independently but their real power emerges when they operate as an integrated system.
Azure Boards provides work item tracking using Agile, Scrum, or CMMI process templates. Teams create Epics, Features, User Stories, and Tasks organized in backlogs and sprint boards. Boards connect directly to code through branch policies that require work item linking on pull requests, creating traceability from business requirements through code changes to deployments. For a payments-api team, a Product Owner creates a User Story describing a new payment validation feature, developers link their feature branches to that story, and the deployment pipeline automatically updates the work item state when the code reaches production. This end-to-end traceability satisfies audit requirements in regulated industries.
Azure Repos hosts Git repositories with enterprise features including branch policies, pull request workflows, and code search across all repositories in the organization. Branch policies enforce code quality gates: minimum reviewer count, successful build validation, linked work items, and comment resolution before merge. Azure Repos supports both centralized and forking workflows and integrates with external Git clients. For organizations migrating from TFS or SVN, Azure Repos provides import tools that preserve history. The key differentiator from GitHub is deeper integration with the other Azure DevOps services and support for TFVC alongside Git for legacy codebases.
Azure Pipelines is the CI/CD engine supporting both YAML-defined and Classic visual designer pipelines. It builds, tests, and deploys applications to any platform including Azure, AWS, GCP, Kubernetes, and on-premises servers. Pipelines support parallel jobs across Microsoft-hosted or self-hosted agents, multi-stage deployments with approval gates, and integration with virtually any build tool or deployment target through a marketplace of 1000+ extensions. The pipeline execution model supports templates for reusability, variable groups for secret management, and environments for deployment tracking and governance.
Azure Test Plans provides structured manual testing, exploratory testing with session capture, and stakeholder feedback collection. Test Plans link test cases to user stories, track test execution across sprint iterations, and generate coverage reports showing which requirements have been verified. For automated testing, Test Plans integrates with pipeline test results to provide unified reporting across both manual and automated test execution. Azure Artifacts hosts NuGet, npm, Maven, Python, and Universal packages with upstream sources that proxy public registries. Artifacts provides versioning, retention policies, and feed permissions that control which teams can publish or consume packages. Together these five services eliminate tool fragmentation and provide a single pane of glass for the entire software delivery lifecycle.
The production gotcha is that organizations often adopt Azure DevOps services piecemeal without leveraging their integration. Teams use Boards without linking work items to code, losing traceability. They use Repos without branch policies, losing quality gates. They use Pipelines without Environments, losing deployment governance. The full value of Azure DevOps emerges when all five services are connected: work items linked to branches linked to pull requests linked to builds linked to deployments, creating an auditable chain from requirement to production that satisfies SOC2, ISO 27001, and regulatory compliance frameworks.
Code Example
# Azure DevOps organization structure for payments-api team # Organization: bank-corp # Project: payments-platform # Azure Boards: Create work item via CLI az boards work-item create \ --org https://dev.azure.com/bank-corp \ --project payments-platform \ --type "User Story" \ --title "Add PCI-DSS compliant card tokenization" \ --assigned-to "[email protected]" \ --iteration "Sprint 42" # Azure Repos: Create repo and set branch policies az repos create --name payments-api --project payments-platform az repos policy create \ --branch main \ --repository-id payments-api \ --policy-type approver-count \ --settings '{"minimumApproverCount": 2, "creatorVoteCounts": false}' # Azure Pipelines: Trigger pipeline run az pipelines run --name payments-api-cicd \ --branch main \ --project payments-platform # Azure Artifacts: Create feed for internal packages az artifacts feed create \ --name payments-shared-libs \ --project payments-platform \ --visibility project # View pipeline runs with status az pipelines runs list \ --project payments-platform \ --pipeline-name payments-api-cicd \ --top 5 --output table # Azure Test Plans: Link test results to pipeline # In azure-pipelines.yml: # - task: PublishTestResults@2 # inputs: # testResultsFormat: 'JUnit' # testResultsFiles: '**/test-results.xml' # testRunTitle: 'payments-api unit tests'
Interview Tip
A junior engineer typically lists the five services by name without explaining how they integrate. For a strong interview answer, describe a concrete workflow where all five services connect: a product owner creates a story in Boards, a developer branches from Repos linked to that story, the pull request triggers a validation build in Pipelines, test results from Test Plans appear on the PR, and shared packages from Artifacts are consumed during the build. Emphasize the traceability chain that satisfies compliance audits. Mention that Azure DevOps supports both cloud-hosted and on-premises (Azure DevOps Server) deployments, and that organizations can adopt services incrementally or use them alongside tools like GitHub or Jenkins.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────┐ │ Azure DevOps Platform │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────────┐ │ │ │ Boards │───→│ Repos │───→│ Pipelines │ │ │ │ (Plan) │ │ (Code) │ │ (Build/Deploy)│ │ │ └────────────┘ └────────────┘ └───────┬────────┘ │ │ │ │ │ │ │ ┌────────────────┐ │ │ │ └────────→│ Test Plans │←──────────┘ │ │ │ (Verify) │ │ │ └────────────────┘ │ │ │ │ │ ↓ │ │ ┌────────────────┐ │ │ │ Artifacts │ │ │ │ (Packages) │ │ │ └────────────────┘ │ │ │ │ Traceability: Story → Branch → PR → Build → Deploy │ └─────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Classic pipelines use a visual drag-and-drop designer configured through the Azure DevOps web UI, while YAML pipelines define the entire CI/CD workflow as code in a version-controlled YAML file. YAML pipelines support branching, pull request reviews, history tracking, and template reuse, making them the recommended approach for modern DevOps practices.
Detailed Answer
Think of the difference between Classic and YAML pipelines like the difference between configuring a car's settings through a touchscreen infotainment system versus writing a configuration file that completely defines the car's behavior. The touchscreen is intuitive for beginners and shows you all available options visually, but you cannot version control your preferences, share them across multiple cars, or review changes before applying them. The configuration file requires learning a syntax, but once written it can be stored in Git, reviewed in pull requests, copied across vehicles, and rolled back to any previous version if something goes wrong.
Classic pipelines were Azure DevOps's original pipeline authoring experience. You create them through the web portal by adding tasks from a visual catalog, configuring each task's properties through form fields, and arranging them in stages and jobs using drag-and-drop. Classic pipelines store their configuration in Azure DevOps's internal database, not in the source repository. This means the pipeline definition lives separately from the code it builds, creating several problems: pipeline changes cannot be reviewed in pull requests alongside code changes, there is no version history showing who changed what and when, you cannot branch the pipeline definition alongside a feature branch that needs different build steps, and replicating the same pipeline across multiple repositories requires manual recreation.
YAML pipelines solve these problems by defining the entire pipeline as an azure-pipelines.yml file committed to the repository root. The YAML file specifies triggers, stages, jobs, steps, variables, and conditions using a structured syntax. Because the file lives in the repository, it benefits from all Git workflows: feature branches can modify the pipeline alongside code changes, pull requests show pipeline modifications for review, commit history tracks every change with attribution, and the pipeline definition always matches the code version being built. When you check out a historical commit, you get exactly the pipeline that was used at that point in time.
YAML pipelines also unlock advanced capabilities unavailable in Classic pipelines. Template references allow extracting common pipeline patterns into shared files that multiple pipelines include, enabling standardization across an organization. Conditional insertion lets pipelines dynamically include or exclude stages based on branch names, variables, or expressions. Multi-stage pipelines define build, test, and deploy stages in a single file with approval gates between stages. Extends templates provide a locked-down pipeline structure that project teams fill in without modifying security-critical steps. These features make YAML pipelines essential for organizations managing dozens or hundreds of repositories that need consistent CI/CD patterns.
The production gotcha is that Classic pipelines still have a few capabilities not yet available in YAML, particularly around release pipeline features like deployment groups, manual intervention tasks with timeout, and some legacy task versions. Organizations migrating from Classic to YAML must audit these capabilities and find YAML equivalents. Another common mistake is treating YAML pipelines like Classic pipelines: creating one monolithic file instead of leveraging templates and stages. Teams should start with a template library that defines organizational standards, then have each repository's YAML file reference those templates with project-specific parameters. Microsoft has also announced that Classic pipelines will eventually be deprecated, making YAML the only supported authoring format for new pipelines.
Code Example
# azure-pipelines.yml — YAML pipeline for payments-api
# This file lives in the repo root alongside the application code
trigger:
branches:
include:
- main
- release/*
paths:
exclude:
- docs/**
- '*.md'
pool:
vmImage: 'ubuntu-latest'
variables:
- group: payments-api-secrets # Variable group from Library
- name: imageTag
value: $(Build.BuildId)
stages:
# Stage 1: Build and Test
- stage: Build
displayName: 'Build & Unit Test'
jobs:
- job: BuildJob
steps:
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: restore
projects: 'src/payments-api/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build application'
inputs:
command: build
projects: 'src/payments-api/*.csproj'
arguments: '--configuration Release'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: test
projects: 'tests/**/*.csproj'
arguments: '--collect:"Code Coverage"'
- task: Docker@2
displayName: 'Build container image'
inputs:
containerRegistry: 'ecr-service-connection'
repository: 'payments-api'
command: build
Dockerfile: 'src/payments-api/Dockerfile'
tags: $(imageTag)
# Stage 2: Deploy to staging with auto-approval
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: Build
jobs:
- deployment: DeployToStaging
environment: 'payments-staging'
strategy:
runOnce:
deploy:
steps:
- task: KubernetesManifest@0
inputs:
action: deploy
manifests: 'k8s/staging/*.yml'
containers: 'ecr.bank.com/payments-api:$(imageTag)'
# Stage 3: Deploy to production with manual approval gate
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToProd
environment: 'payments-production' # Has approval gate configured
strategy:
runOnce:
deploy:
steps:
- task: KubernetesManifest@0
inputs:
action: deploy
manifests: 'k8s/production/*.yml'
containers: 'ecr.bank.com/payments-api:$(imageTag)'
# Classic pipeline equivalent would require:
# - Manual creation in web UI for each repo
# - No version control of pipeline changes
# - No PR review of pipeline modifications
# - No branching of pipeline definition
# - Manual replication across repositoriesInterview Tip
A junior engineer typically says YAML pipelines are just Classic pipelines written in text form, missing the fundamental shift in workflow and governance. For a strong answer, emphasize that pipeline-as-code enables the same review and versioning practices applied to application code: pull request reviews for pipeline changes, branch-specific pipeline modifications, git blame for accountability, and template reuse for organizational consistency. Explain that YAML unlocks features impossible in Classic: extends templates for security-controlled pipeline structures, conditional logic for dynamic pipeline composition, and multi-repository triggers. Mention that Microsoft is deprecating Classic pipelines, making YAML knowledge essential. Discuss the migration path for teams with existing Classic pipelines.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────┐ │ Classic vs YAML Pipeline Comparison │ │ │ │ Classic Pipeline YAML Pipeline │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ Azure DevOps UI │ │ azure-pipelines │ │ │ │ (Web Designer) │ │ .yml (in repo) │ │ │ └────────┬─────────┘ └────────┬─────────┘ │ │ │ │ │ │ ↓ ↓ │ │ Stored in Azure DB Stored in Git │ │ No version control Full version history │ │ No PR review PR reviewable │ │ No branching Branches with code │ │ Manual replication Template reuse │ │ Visual configuration Code-based config │ │ │ │ Use Case: Use Case: │ │ - Quick prototyping - Production workloads │ │ - Learning pipelines - Multi-repo orgs │ │ - Legacy migration - Compliance/audit │ │ - Template libraries │ └─────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Service Connections in Azure DevOps securely store credentials for external services like AWS, Kubernetes, or Docker registries. They use workload identity federation or service principals to authenticate without storing long-lived secrets, apply RBAC controls to limit which pipelines can use them, and provide audit trails showing which deployments used which credentials.
Detailed Answer
Think of Service Connections like the security badge system in a corporate building. Instead of giving every employee a master key to every room, each person receives a badge programmed with access to only the floors and rooms their job requires. The badge system logs every door opened, when, and by whom. If an employee leaves, their badge is deactivated immediately without affecting anyone else. Service Connections work the same way: they grant specific pipelines access to specific external resources using scoped credentials, log every usage, and can be revoked without disrupting other workflows.
A Service Connection is a secure object in Azure DevOps that stores authentication information for an external service. When a pipeline task needs to push a Docker image to ECR, deploy to a Kubernetes cluster, or provision AWS infrastructure with Terraform, it references a Service Connection by name rather than embedding credentials directly in the pipeline YAML. The Service Connection encapsulates the authentication mechanism: an AWS access key and secret key for AWS connections, a kubeconfig or service account token for Kubernetes, or a service principal for Azure. This separation means credentials are managed centrally by administrators and never exposed to pipeline authors or visible in YAML files.
For AWS connections, Azure DevOps supports two approaches. The basic approach stores an AWS IAM access key and secret key directly in the Service Connection. The preferred approach for production uses OIDC federation, where Azure DevOps authenticates to AWS using a federated identity token rather than static credentials. With OIDC federation, you configure an identity provider in AWS IAM that trusts Azure DevOps, create an IAM role with specific permissions, and configure the Service Connection to assume that role using OIDC. No static credentials are stored anywhere, eliminating the risk of leaked access keys. The IAM role's trust policy restricts which Azure DevOps organization and project can assume it, preventing unauthorized use.
For Kubernetes connections, you can authenticate using a kubeconfig file, a service account token, or Azure Kubernetes Service connections that leverage Azure Active Directory integration. The recommended approach for AKS clusters is the Azure Resource Manager connection type, which uses Azure AD workload identity to authenticate. For non-Azure Kubernetes clusters like EKS or self-managed clusters, you create a Kubernetes service account with RBAC permissions scoped to specific namespaces, generate a long-lived token, and store it in the Service Connection. The service account should have only the permissions needed for deployment, not cluster-admin, following the principle of least privilege.
Service Connection security is enforced through pipeline permissions and approvals. Administrators can restrict which pipelines are authorized to use a Service Connection, requiring explicit approval before new pipelines gain access. Project-level Service Connections are visible only within their project, while organization-level connections can be shared across projects. Approval checks can require a human to authorize before any pipeline uses a production Service Connection, adding a governance layer beyond pipeline-level approvals.
The production gotcha is credential rotation and monitoring. Static credentials in Service Connections must be rotated regularly, and many organizations forget to automate this rotation. When an AWS access key is rotated in IAM without updating the Service Connection, all pipelines using it fail simultaneously. OIDC federation eliminates this problem for AWS and Azure connections. For Kubernetes service account tokens, teams must monitor expiration and automate renewal. Another common mistake is granting Service Connections broad permissions: an AWS IAM role with AdministratorAccess or a Kubernetes service account with cluster-admin violates least privilege and creates excessive blast radius if the connection is compromised.
Code Example
# Create AWS Service Connection using Azure DevOps CLI
# Option 1: Static credentials (less secure, simpler setup)
az devops service-endpoint create \
--service-endpoint-configuration aws-connection.json \
--project payments-platform
# aws-connection.json
# {
# "data": {},
# "name": "aws-production",
# "type": "aws",
# "url": "https://aws.amazon.com",
# "authorization": {
# "parameters": {
# "username": "AKIAIOSFODNN7EXAMPLE",
# "password": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# },
# "scheme": "UsernamePassword"
# }
# }
# Option 2: OIDC federation (preferred — no static credentials)
# Step 1: Create IAM OIDC provider in AWS
aws iam create-open-id-connect-provider \
--url https://vstoken.dev.azure.com/<tenant-id> \
--client-id-list api://AzureADTokenExchange \
--thumbprint-list <thumbprint>
# Step 2: Create IAM role with trust policy for Azure DevOps
aws iam create-role --role-name azure-devops-deployer \
--assume-role-policy-document file://trust-policy.json
# trust-policy.json — restricts to specific Azure DevOps org/project
# {
# "Version": "2012-10-17",
# "Statement": [{
# "Effect": "Allow",
# "Principal": {"Federated": "arn:aws:iam::123456789:oidc-provider/vstoken.dev.azure.com/<tenant>"},
# "Action": "sts:AssumeRoleWithWebIdentity",
# "Condition": {
# "StringEquals": {
# "vstoken.dev.azure.com/<tenant>:sub": "sc://bank-corp/payments-platform/aws-production"
# }
# }
# }]
# }
# Step 3: Attach deployment permissions (least privilege)
aws iam attach-role-policy --role-name azure-devops-deployer \
--policy-arn arn:aws:iam::123456789:policy/payments-deploy-only
# Kubernetes Service Connection: create scoped service account
kubectl create namespace payments-staging
kubectl create serviceaccount azure-devops-deployer -n payments-staging
kubectl create rolebinding azure-devops-deployer \
--clusterrole=edit \
--serviceaccount=payments-staging:azure-devops-deployer \
-n payments-staging
# Generate token for Service Connection
kubectl create token azure-devops-deployer \
-n payments-staging --duration=8760h
# Use Service Connection in pipeline YAML
# - task: Kubernetes@1
# inputs:
# connectionType: 'Kubernetes Service Connection'
# kubernetesServiceEndpoint: 'eks-payments-staging'
# command: apply
# arguments: '-f k8s/staging/'
# Restrict Service Connection to specific pipelines
az devops service-endpoint update \
--id <endpoint-id> \
--enable-for-all false \
--project payments-platformInterview Tip
A junior engineer typically describes Service Connections as just a place to store credentials, missing the governance and security architecture around them. For a strong answer, explain the principle of least privilege: Service Connections should grant only the permissions needed for the specific deployment task. Discuss OIDC federation as the modern approach that eliminates static credential storage entirely. Explain pipeline permissions and approval checks that control which pipelines can access which connections. Mention the operational challenge of credential rotation for non-federated connections and how OIDC solves it. Describe how Service Connection usage is audited and how to detect unauthorized access attempts.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────┐ │ Service Connection Architecture │ │ │ │ ┌──────────────┐ ┌───────────────────────┐ │ │ │ Pipeline YAML│────→│ Service Connection │ │ │ │ (references │ │ (stores credentials) │ │ │ │ by name) │ └───────────┬───────────┘ │ │ └──────────────┘ │ │ │ │ OIDC / Token │ │ ┌──────────────┼──────────────┐ │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ AWS │ │ K8s │ │ Docker │ │ │ │ (IAM Role│ │ (Service │ │(Registry)│ │ │ │ + OIDC) │ │ Account)│ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ Security Controls: │ │ ├── Pipeline permissions (explicit authorization) │ │ ├── Approval checks (human gate) │ │ ├── Least privilege (scoped IAM/RBAC) │ │ └── Audit logging (who used when) │ └─────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Multi-stage YAML pipelines define sequential stages in azure-pipelines.yml with dependsOn relationships, conditions for execution, and Environment-based approval gates. Each stage contains jobs that run on agents, and Environments configured in Azure DevOps enforce manual approvals, business hours checks, and other governance controls before production deployments proceed.
Detailed Answer
Think of multi-stage pipelines like a pharmaceutical manufacturing process where a drug must pass through distinct phases: synthesis, quality testing, regulatory review, and distribution. Each phase has its own specialized equipment and personnel, and a drug cannot advance to the next phase until the current one completes successfully. Between critical phases like regulatory review, a human authority must sign off before proceeding. The process is linear and auditable, with clear documentation of who approved what and when. Multi-stage YAML pipelines implement this same controlled progression for software delivery.
A multi-stage pipeline divides the CI/CD workflow into logical stages that execute sequentially or in parallel depending on their dependencies. The most common pattern is Build, Test, Deploy-Staging, Deploy-Production. Each stage contains one or more jobs, and each job contains a sequence of steps that execute on an agent. Stages can depend on other stages using the dependsOn property, creating a directed acyclic graph of execution. A stage only executes if all its dependencies completed successfully, unless overridden with a condition expression. This structure provides clear separation of concerns: build artifacts are created once and promoted through environments rather than rebuilt at each stage.
Approval gates are implemented through Azure DevOps Environments. An Environment is a resource in Azure DevOps that represents a deployment target like payments-staging or payments-production. You configure checks on environments including manual approvals, business hours gates, Azure Monitor alerts, REST API checks, and required template usage. When a pipeline references an environment in a deployment job, Azure DevOps automatically pauses the pipeline and triggers the configured checks. For manual approvals, designated approvers receive email or Teams notifications and must explicitly approve or reject the deployment through the Azure DevOps portal. The approval history is recorded for audit compliance.
Deployment jobs differ from regular jobs in several important ways. They track deployment history per environment, support deployment strategies like runOnce, rolling, and canary, and automatically download artifacts from previous stages. The deployment strategy defines how the new version is rolled out: runOnce deploys everything at once, rolling deploys to servers in batches with health checks between batches, and canary routes a percentage of traffic to the new version before full rollout. Each strategy supports lifecycle hooks including preDeploy, deploy, routeTraffic, postRouteTraffic, and on.failure, giving teams fine-grained control over the deployment process.
Stage conditions enable sophisticated pipeline logic without manual intervention. You can restrict production deployments to only the main branch using condition expressions, skip stages when specific files changed, or conditionally include stages based on pipeline variables. Combined with variable groups that hold environment-specific configuration and template parameters that customize behavior per stage, multi-stage pipelines support complex real-world scenarios like deploying to multiple regions sequentially with health checks between each region, or deploying microservices in dependency order.
The production gotcha is managing pipeline artifacts between stages. Each stage can run on a different agent, so files from the build stage are not automatically available in deploy stages. Teams must explicitly publish artifacts using the PublishPipelineArtifact task and download them in subsequent stages using DownloadPipelineArtifact. A common mistake is publishing large artifacts including test binaries or debug symbols that slow down stage transitions. Another issue is approval fatigue: if every deployment requires manual approval, approvers become rubber-stampers. Best practice is to require approvals only for production environments while staging deployments proceed automatically, giving teams a chance to validate in staging before the production approval request arrives.
Code Example
# azure-pipelines.yml — Multi-stage pipeline for payments-api
trigger:
branches:
include: [main, release/*]
variables:
- group: payments-api-common
- name: imageRepository
value: 'payments-api'
stages:
# Stage 1: Build and publish artifacts
- stage: Build
displayName: 'Build & Unit Test'
jobs:
- job: BuildAndTest
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DotNetCoreCLI@2
displayName: 'Build payments-api'
inputs:
command: build
projects: 'src/**/*.csproj'
arguments: '--configuration Release'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: test
projects: 'tests/unit/**/*.csproj'
arguments: '--configuration Release --collect:"XPlat Code Coverage"'
- task: Docker@2
displayName: 'Build container image'
inputs:
command: build
repository: $(imageRepository)
tags: '$(Build.BuildId)'
- task: Docker@2
displayName: 'Push to ACR'
inputs:
containerRegistry: 'acr-service-connection'
repository: $(imageRepository)
command: push
tags: '$(Build.BuildId)'
# Publish Kubernetes manifests as pipeline artifact
- publish: k8s/
artifact: k8s-manifests
displayName: 'Publish K8s manifests'
# Stage 2: Integration tests in staging
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: Build
jobs:
- deployment: StagingDeploy
environment: 'payments-staging' # No approval required
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: k8s-manifests
- task: KubernetesManifest@0
inputs:
action: deploy
kubernetesServiceConnection: 'eks-staging'
namespace: payments-staging
manifests: '$(Pipeline.Workspace)/k8s-manifests/staging/*.yml'
containers: 'acr.bank.com/payments-api:$(Build.BuildId)'
- job: IntegrationTests
dependsOn: StagingDeploy
steps:
- script: |
npm run test:integration -- --baseUrl=https://staging.payments.bank.com
displayName: 'Run integration tests'
# Stage 3: Production with MANUAL APPROVAL
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: ProductionDeploy
environment: 'payments-production' # Has approval gate!
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: k8s-manifests
- task: KubernetesManifest@0
inputs:
action: deploy
kubernetesServiceConnection: 'eks-production'
namespace: payments
manifests: '$(Pipeline.Workspace)/k8s-manifests/production/*.yml'
containers: 'acr.bank.com/payments-api:$(Build.BuildId)'
# Configure Environment approval in Azure DevOps UI:
# Project Settings → Environments → payments-production → Approvals and Checks
# Add: Manual Approval (approvers: [email protected])
# Add: Business Hours (Mon-Fri 9am-4pm)
# Add: Required Template (must use org-approved template)Interview Tip
A junior engineer typically describes multi-stage pipelines as just separating build and deploy into different sections. For a strong intermediate answer, explain the governance architecture: Environments as deployment targets with configurable checks, deployment jobs that track history and support strategies, conditions that enforce branch-based access control, and artifact flow between stages. Walk through a concrete example showing how a build artifact is created once and promoted through staging to production with an approval gate between them. Discuss the difference between stage-level conditions and environment-level checks, and explain when to use each. Mention deployment strategies beyond runOnce and how rolling deployments work with health probes.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────┐ │ Multi-Stage Pipeline Flow │ │ │ │ ┌─────────┐ ┌────────────┐ ┌──────────────┐ │ │ │ Build │───→│ Deploy │───→│ Deploy │ │ │ │ Stage │ │ Staging │ │ Production │ │ │ └────┬────┘ └─────┬──────┘ └──────┬───────┘ │ │ │ │ │ │ │ ┌────┴────┐ ┌─────┴──────┐ ┌──────┴───────┐ │ │ │ Build │ │ Auto-deploy│ │ APPROVAL │ │ │ │ Test │ │ Run integ │ │ REQUIRED │ │ │ │ Push │ │ tests │ │ │ │ │ │ Publish │ │ │ │ Deploy after │ │ │ └─────────┘ └────────────┘ │ approval │ │ │ └──────────────┘ │ │ │ │ Conditions: │ │ ├── Build: always on trigger │ │ ├── Staging: depends on Build success │ │ └── Production: main branch only + manual approval │ └──────────────────────────────────────────────────────┘
💬 Comments
Context
A fintech team ran 14 App Service applications across two Azure regions, serving 8,000 requests per minute during business hours. Releases became urgent after three consecutive deployments caused five-minute login brownouts.
Problem
The team deployed directly into production instances. Azure showed the App Service plan as healthy, but the application needed 90 seconds to warm caches, connect to Key Vault, and hydrate database pools. Front Door continued routing traffic to instances that could accept TCP connections but returned 503s on the real login path.
Solution
The team introduced staging slots for every service, configured `/healthz/ready` to validate Key Vault, SQL, and Redis dependencies, and required synthetic login tests before slot swap. Front Door probes were changed from `/` to the same readiness path, and App Insights availability tests were tagged with release versions. Rollback became another slot swap instead of a redeploy.
Commands
az webapp deployment slot create --resource-group rg-prod-auth --name auth-api --slot staging # Creates an isolated warmup slot
az webapp deployment slot swap --resource-group rg-prod-auth --name auth-api --slot staging --target-slot production # Promotes only after health validation
Outcome
Release-related 5xx bursts dropped from 3 per month to zero in the following quarter. Median rollback time fell from 18 minutes to under 2 minutes.
Lessons Learned
Platform health and workload health are different signals. The readiness endpoint must cover critical dependencies, or slot swaps only make broken releases faster.
◈ Architecture Diagram
┌──────────┐
│ Signal │
└────┬─────┘
↓
┌──────────┐
│ UseCase │
└────┬─────┘
↓
┌──────────┐
│ Action │
└──────────┘💬 Comments
Symptom
9:04 AM: App Service HTTP 500s rose to 11%, instance CPU stayed below 45%, and dependency duration to Azure SQL exceeded 9 seconds.
Error Message
Timeout expired. The timeout period elapsed before completion of the operation.
Root Cause
The autoscale rule watched CPU only. The application bottleneck was Azure SQL connection pool saturation after a marketing campaign increased checkout traffic. App Service looked underutilized, so no scale action happened, and retry traffic made database pressure worse. The underlying issue was a combination of factors that individually seemed harmless but together created a cascading failure. The monitoring that should have caught this early was either missing or configured with thresholds too high to trigger before user impact. The on-call engineer initially pursued the wrong hypothesis because the symptoms resembled a different, more common failure mode. By the time the real root cause was identified, the blast radius had expanded beyond the original service to affect downstream dependencies. This incident exposed a gap in the team's runbooks and highlighted the need for better correlation between metrics, logs, and traces during diagnosis.
Diagnosis Steps
Solution
Add autoscale and alerts based on request rate, queue depth, and dependency latency where appropriate. Cap retries, tune connection pools, and scale or optimize the database path. Do not restart the app repeatedly; that increases connection churn.
Commands
az monitor autoscale rule create --resource-group rg-payments --autoscale-name payments-api-autoscale --condition "Requests > 6000 avg 5m" --scale out 2
Prevention
Model dependencies in readiness checks and alerting. Include Azure SQL saturation in service dashboards. Load test retry behavior and autoscale triggers together.
◈ Architecture Diagram
┌──────────┐
│ Learn │
└────┬─────┘
↓
┌──────────┐
│ Incident │
└────┬─────┘
↓
┌──────────┐
│ Operate │
└──────────┘💬 Comments
Symptom
On May 29, 2026, customers using Azure OpenAI experienced increased latency, intermittent request failures, timeouts, and HTTP 5XX errors across multiple regions, with heavier impact in Europe and Australia East.
Error Message
HTTP 5XX errors, inference request timeouts, elevated latency, and backend routing instability
Root Cause
A Microsoft first-party upstream workload changed how capacity-related failures were surfaced. Those responses began looking retriable to multiple layers, causing immediate retries without enough backoff or jitter. In some cases one failed request produced many additional attempts, creating a retry storm that overwhelmed the shared Azure OpenAI inference routing and load-balancing layer.
Diagnosis Steps
Solution
Azure isolated the offending internal workload onto dedicated infrastructure, coordinated rollback of the triggering upstream change, reduced retry-driven traffic volume, and cleared backlogged requests. For customer workloads, reduce impact by using multi-region fallback, bounded retries, exponential backoff, jitter, and graceful degradation when AI inference dependencies are stressed.
Commands
az monitor metrics list --resource <azure-openai-resource-id> --metric Requests,Latency,ServerErrors
az monitor activity-log list --resource-group <rg> --start-time 2026-05-29T09:00:00Z
Review application retry settings: max attempts, backoff, jitter, retryable status codes
Prevention
Use retry budgets and load-shedding around shared AI dependencies. Isolate large first-party or batch workloads from multi-tenant serving paths. Alert on retry amplification by comparing inbound user traffic with internal routed traffic.
◈ Architecture Diagram
Capacity failure -> retriable error -> upstream retries -> routing overload -> 5XX/timeouts
💬 Comments
Symptom
Between May 29 and May 30, 2026, customers with resources in Azure West US 2 saw connectivity failures, resource unavailability, and service management failures across compute, networking, storage, AKS, Azure Monitor, Log Analytics, App Service, Cosmos DB, SQL, and other services.
Error Message
Resource unavailable, connectivity failures, service management failures, telemetry ingestion backlog
Root Cause
Severe thunderstorms caused utility power sag and swell events across multiple datacenter facilities. Some mechanical cooling systems entered a protective lockout state. As temperatures increased, cloud infrastructure shut down to protect equipment and preserve data integrity. Recovery was extended by manual cooling restoration, networking recovery, and sequential storage validation.
Diagnosis Steps
Solution
Azure restored cooling, recovered networking and compute, validated storage scale units, and processed telemetry backlogs. Customers should design critical workloads for regional failure: multi-region deployment, tested failover, geo-redundant storage where appropriate, and alerting that does not depend on a single impacted telemetry region.
Commands
az resource list --location westus2 --query '[].{name:name,type:type,resourceGroup:resourceGroup}'az aks show --resource-group <rg> --name <cluster> --query 'agentPoolProfiles[].availabilityZones'
kubectl get nodes,pods,pv,pvc -A
az monitor activity-log list --status Failed --start-time 2026-05-29T04:00:00Z
Prevention
Use multi-region architecture for tier-1 workloads. Keep restore and failover runbooks current. Avoid making incident telemetry, deployment control, and customer serving all depend on the same regional failure domain.
◈ Architecture Diagram
Power disturbance -> cooling lockout -> thermal shutdown -> storage validation -> delayed service recovery
💬 Comments