Without CI/CD, shipping software is like hand-carrying toy cars across the factory. With CI/CD, you get an automated assembly line that catches defects early, delivers faster, and lets your team focus on building features instead of manually running builds and copying files to servers.
CI/CD Concepts & Pipeline Types
Understand Continuous Integration, Continuous Delivery, and Continuous Deployment — the engine behind modern software delivery. Learn how Azure Pipelines executes work through agents, pools, jobs, and tasks, and compare YAML pipelines to Classic pipelines to choose the right approach for your team.
🧒 Simple Explanation (ELI5)
Imagine a toy car factory with a long assembly line. At one end, raw materials go in. At each station along the line, a worker does one thing — one cuts the metal, the next paints it, the next attaches the wheels, and the last one puts it in a box. The car moves automatically from station to station without anyone carrying it by hand.
Now imagine what happens if there's no assembly line. One person cuts the metal, walks across the factory, hands it to the painter, waits for the paint to dry, carries it to the wheel station… it's slow, error-prone, and someone eventually drops a car on the floor.
- CI (Continuous Integration) = every time a worker finishes a piece, it's immediately tested on the assembly line. If the part is crooked or the wrong color, the alarm goes off right away — before it reaches the next station. In software, CI means every code commit is automatically built and tested. Bugs are caught in minutes, not weeks.
- CD (Continuous Delivery) = the car goes all the way down the assembly line, passes every quality check, and arrives at the loading dock — boxed up and ready to ship. But a manager presses a button to actually load it on the truck. In software, the code is always in a deployable state, but a human approves the final push to production.
- CD (Continuous Deployment) = same assembly line, but there's no manager at the loading dock. The car rolls straight onto the truck automatically. In software, every change that passes all tests is deployed to production without human approval.
- Pipeline = the assembly line itself — the automated sequence of build, test, and deploy steps.
- Agent = the robot arm that does the actual work at each station. Azure gives you robots (Microsoft-hosted agents) or you bring your own (self-hosted agents).
🔧 Technical Explanation
CI vs CD: The Three Meanings
The acronym "CD" is frustratingly overloaded. Let's be precise:
| Term | What Happens | Human Approval? | Risk Level |
|---|---|---|---|
| Continuous Integration (CI) | Every commit triggers an automated build and test run. The result is a validated artifact (binary, container image, package). | No — fully automatic | Low — only builds and tests, doesn't deploy |
| Continuous Delivery (CD) | CI + automated deployment to staging/pre-production. Production deployment is possible at any time but requires a manual approval gate. | Yes — human approves production deploy | Medium — staging is automated, production is gated |
| Continuous Deployment (CD) | CI + automated deployment all the way to production. Every commit that passes all tests goes live automatically. | No — fully automatic to production | Higher — requires excellent test coverage and monitoring |
Most enterprise teams practice Continuous Delivery, not Continuous Deployment. A manual approval gate before production is the norm — it gives release managers, security teams, or product owners a final checkpoint. Continuous Deployment is more common at companies like Netflix or GitHub, where test suites are extremely comprehensive and monitoring can auto-roll back bad deploys.
Pipeline Execution Model in Azure DevOps
Azure Pipelines has a well-defined hierarchy for organizing and executing work:
Pipeline → Trigger → Stage → Job → Step → Task
- Pipeline: The top-level definition of your CI/CD workflow. Defined in a YAML file (e.g.,
azure-pipelines.yml) or via the Classic editor UI. A single pipeline can build, test, and deploy your application. - Trigger: The event that starts the pipeline — a code push, a PR, a schedule (cron), or a manual run. Triggers can filter by branch, path, or tag.
- Stage: A logical boundary within the pipeline — e.g.,
Build,Test,Deploy-Staging,Deploy-Production. Stages run sequentially by default but can run in parallel. Each stage can have approval gates and conditions. - Job: A unit of work that runs on a single agent. A stage contains one or more jobs. Jobs within a stage can run in parallel on different agents. Each job gets a fresh workspace.
- Step: A single operation within a job — either a task (a pre-built action from the marketplace) or a script (inline Bash/PowerShell). Steps run sequentially within a job.
- Task: A packaged, reusable unit of pipeline logic — e.g.,
DotNetCoreCLI@2(build .NET),Docker@2(build/push images),KubernetesManifest@0(deploy to K8s). Tasks have inputs, outputs, and versioning.
Agents and Agent Pools
An agent is the compute that actually runs your pipeline jobs. It's a machine (or container) with the Azure Pipelines agent software installed. Agents are organized into pools.
| Aspect | Microsoft-Hosted Agent | Self-Hosted Agent |
|---|---|---|
| Who manages it? | Microsoft — fully managed VMs spun up on demand | You — installed on your own VM, on-prem server, or container |
| OS options | Ubuntu (latest), Windows Server (latest), macOS (latest) | Any OS that supports .NET — Windows, Linux, macOS |
| Fresh each run? | Yes — each job gets a brand-new VM. No state persists between runs. | No — the agent persists. You control cleanup between runs. |
| Pre-installed tools | Comprehensive — Docker, Node, Python, Java, .NET, kubectl, Helm, Terraform, az CLI, and more | Only what you install. Full control over tool versions. |
| Network access | Public internet only. Cannot reach private VNets, on-prem networks, or internal resources without extra config. | Full access to your network — databases, internal APIs, private registries, on-prem resources. |
| Cost | Free tier: 1 parallel job, 1800 minutes/month. Paid: $40/month per parallel job (unlimited minutes). | Free tier: 1 free parallel job. Paid: $15/month per parallel job. You pay for the VM infrastructure separately. |
| Maintenance | Zero — Microsoft patches, updates, and scales the VMs | You handle OS patches, agent updates, disk cleanup, and scaling |
| Speed | Cold start: ~20-40 seconds to provision a new VM. No caching between runs (unless you use pipeline caching tasks). | Near-instant start (agent already running). Supports persistent disk caching for faster builds. |
| Security | Isolated VMs — no cross-tenant access. Destroyed after each job. | Runs on your infra — you control security posture. Risk: if compromised, attacker has network access. |
| Best for | Most teams, open-source projects, public-facing builds | Private network access required, large builds needing caching, compliance requirements for build infrastructure |
Agent Capabilities
Each agent advertises capabilities — key-value pairs describing what the agent can do (e.g., node=18.x, docker=installed, Agent.OS=Linux). Pipelines specify demands — requirements that an agent must satisfy to run the job.
- System capabilities: Auto-detected by the agent — OS, installed tools, environment variables.
- User capabilities: Manually added by an admin — custom labels like
GPU=trueorregion=us-east. - Demands: Specified in the pipeline —
demands: Agent.OS -equals Linux. The pipeline scheduler matches demands to capabilities to find a compatible agent.
If no agent in the pool matches the pipeline's demands, the job will queue indefinitely with the message "Waiting for an agent." Always verify agent capabilities match your pipeline demands. For Microsoft-hosted agents, capabilities are documented per image — check the microsoft/azure-pipelines-image-generation repository for the full list of pre-installed tools per image.
YAML Pipelines vs Classic Pipelines
Azure DevOps offers two ways to define pipelines:
YAML Pipelines — Pipeline-as-Code. You write the entire pipeline definition in a .yml file stored in your repository alongside your application code.
# azure-pipelines.yml — YAML Pipeline example
trigger:
branches:
include: [main]
paths:
exclude: [docs/*, README.md]
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
jobs:
- job: BuildApp
steps:
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: restore
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: build
projects: '**/*.csproj'
arguments: '--configuration Release'
- task: DotNetCoreCLI@2
displayName: 'Run tests'
inputs:
command: test
projects: '**/*Tests.csproj'
- task: PublishBuildArtifacts@1
displayName: 'Publish artifact'
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
- stage: DeployStaging
dependsOn: Build
jobs:
- deployment: DeployToStaging
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying to staging..."
displayName: 'Deploy to Staging'
Classic Pipelines — GUI-based. You build the pipeline using a visual drag-and-drop editor in the Azure DevOps web portal. No YAML file needed.
- Created via Pipelines → New Pipeline → Use the classic editor
- Pipeline definition is stored in Azure DevOps (not in your repo)
- Tasks are added and configured through form fields and dropdowns
- Good for teams unfamiliar with YAML or for quick prototyping
Microsoft is investing heavily in YAML pipelines and has stated that YAML is the future of Azure Pipelines. Classic pipelines are still fully supported, but new features (like environments, deployment strategies, and template expressions) are YAML-only. For new projects, always start with YAML. Use Classic only when migrating legacy pipelines or when team members need a visual editor to get started.
📊 Pipeline Execution Model & Comparison
runs on Agent
runs on Agent
runs on Agent
.yml file in repo📋 YAML Pipelines vs Classic Pipelines
| Dimension | YAML Pipelines | Classic Pipelines |
|---|---|---|
| Definition format | Code — .yml file in the repository | GUI — visual editor in Azure DevOps portal |
| Version control | Full Git history — diffs, blame, branches, PRs | Revision history in Azure DevOps (limited, not Git) |
| Code review | Pipeline changes go through PR review like any code change | No PR review — changes are saved instantly in the UI |
| Branching | Pipeline definition travels with the branch — each branch can have a different pipeline | Single pipeline definition shared across all branches |
| Templates & reuse | Powerful template system — extends, parameters, cross-repo templates | Task Groups — limited reuse, no cross-project sharing |
| Multi-stage | Native — build and deploy in a single YAML file with approval gates | Separate Build and Release pipelines required |
| Environments | Full support — environment keyword with deployment strategies (runOnce, rolling, canary) | Not supported — uses Release pipeline deployment groups |
| Trigger types | CI triggers, PR triggers, scheduled triggers, pipeline resource triggers — all in YAML | CI triggers and scheduled triggers in UI. PR triggers limited. |
| Learning curve | Steeper — requires YAML syntax knowledge, indentation discipline | Easier — form fields and dropdowns, no syntax to learn |
| Portability | Pipeline file can be copied to another repo or organization | Locked to the Azure DevOps project — cannot be exported easily |
| Debugging | Use system.debug: true variable for verbose logs. Full YAML validation in editor. | Enable debug mode in pipeline variables. UI-based log inspection. |
| Microsoft investment | Active development — new features are YAML-first or YAML-only | Maintenance mode — no new features planned |
| Recommended for | All new projects, teams practicing GitOps, enterprise-scale | Legacy projects, quick prototyping, teams new to CI/CD |
If your team currently uses Classic Pipelines, don't rush to migrate all at once. Start by creating new pipelines in YAML, then gradually migrate Classic pipelines during natural maintenance cycles. Azure DevOps provides an "Export to YAML" option on Classic Build pipelines (not Release pipelines) that can generate a starting YAML file.
🖥️ Microsoft-Hosted vs Self-Hosted Agents
| Factor | Microsoft-Hosted ✅ Pros / ❌ Cons | Self-Hosted ✅ Pros / ❌ Cons |
|---|---|---|
| Setup | ✅ Zero setup — select a VM image and go | ❌ Manual install — download agent, configure, register with pool |
| Maintenance | ✅ Zero maintenance — Microsoft patches and updates VMs | ❌ You maintain OS patches, agent updates, disk space |
| Clean environment | ✅ Fresh VM every run — no leftover state from previous builds | ❌ Persistent environment — leftover files can cause "works on my agent" bugs |
| Build speed | ❌ Slower cold start (~20-40s). No cache between runs. | ✅ Instant start. Persistent disk caching — incremental builds are significantly faster. |
| Network access | ❌ Cannot reach private VNets, on-prem DBs, or internal APIs without workarounds | ✅ Full access to your private network, on-prem resources, databases, internal registries |
| Cost at scale | ❌ $40/month per parallel job. Costs increase linearly with parallelism. | ✅ $15/month per parallel job + your VM cost. Often cheaper at scale with reserved VMs. |
| Tool control | ❌ Limited — you get what Microsoft pre-installs. Can install tools at runtime but adds build time. | ✅ Full control — install exact tool versions, proprietary SDKs, licensed software |
| Security isolation | ✅ Isolated, ephemeral VMs — destroyed after each job. No cross-tenant risk. | ❌ Persistent machine — if compromised, attacker has ongoing network access. Requires hardening. |
| Scalability | ✅ Auto-scales — Microsoft manages capacity. Buy more parallel jobs as needed. | ❌ Manual scaling — add more VMs yourself. Can use VMSS-based agent pools for auto-scaling. |
| Free tier | 1 parallel job, 1800 minutes/month (public projects get 10 free parallel jobs) | 1 free parallel job (unlimited minutes — you pay only for your own VM) |
🛠️ Hands-on: Your First Pipeline
Follow these steps to create a YAML pipeline from scratch, run it, inspect the agent logs, and optionally configure a self-hosted agent.
Prerequisites
- An Azure DevOps project with a Git repository (created in Lesson 1)
- Basic familiarity with YAML syntax (indentation = spaces, not tabs)
- Contributor permissions on the repository
Step 1: Create a Minimal YAML Pipeline (Hello World)
In the root of your repo, create a file named azure-pipelines.yml:
# azure-pipelines.yml — Hello World pipeline
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo "Hello, Azure Pipelines!"
displayName: 'Run a one-line script'
- script: |
echo "Pipeline ID: $(Build.BuildId)"
echo "Repository: $(Build.Repository.Name)"
echo "Branch: $(Build.SourceBranchName)"
echo "Commit: $(Build.SourceVersion)"
echo "Agent name: $(Agent.Name)"
echo "Agent OS: $(Agent.OS)"
echo "Working dir: $(System.DefaultWorkingDirectory)"
displayName: 'Print pipeline variables'
Commit and push to main:
git add azure-pipelines.yml git commit -m "ci: add hello world pipeline" git push origin main
Step 2: Create the Pipeline in Azure DevOps
- Navigate to Pipelines → Pipelines → New Pipeline
- Select your repository source (Azure Repos Git)
- Select your repository
- Choose "Existing Azure Pipelines YAML file"
- Select branch
mainand path/azure-pipelines.yml - Click Run
If this is a new Azure DevOps organization, you may need to request a free parallelism grant. Microsoft disabled automatic free tiers for new orgs to prevent abuse. Fill out the form at aka.ms/azpipelines-parallelism-request. Approval usually takes 2-3 business days. Until then, your pipeline will queue with "No hosted parallelism has been purchased or granted."
Step 3: View the Run and Inspect Agent Logs
- Navigate to Pipelines → Pipelines and click on your pipeline
- Click the latest run to see the stages and jobs
- Click the job (e.g. "Job") to expand the step-by-step log output
- Click on each step to see its detailed log:
- "Initialize job" — shows agent provisioning, capabilities, and variable resolution
- "Checkout" — shows the Git clone operation (repo URL, branch, commit SHA)
- "Run a one-line script" — shows your
echooutput - "Print pipeline variables" — shows the resolved system variables
- "Post-job: Checkout" — cleanup step
Key things to look for in the agent logs: ────────────────────────────────────────── ✅ Agent name → e.g., "Hosted Agent" (Microsoft-hosted) ✅ Agent.OS → "Linux" (ubuntu-latest image) ✅ Agent.Version → the agent software version ✅ System.DefaultWorkingDirectory → /home/vsts/work/1/s ✅ Build.SourceBranchName → "main" ✅ Build.SourceVersion → the Git commit SHA ✅ Exit code of each step → 0 = success, non-zero = failure
To enable verbose debug logging for troubleshooting, add a pipeline variable:
variables: system.debug: true
Step 4: Create a Custom Agent Pool
- Navigate to Organization Settings → Agent Pools
- Click "Add pool"
- Select Pool type: Self-hosted
- Name it (e.g.,
my-linux-pool) - Optionally grant access to all pipelines
- Click Create
Step 5: Configure a Self-Hosted Agent
Set up a self-hosted agent on a Linux VM (or WSL):
# 1. Create a PAT (Personal Access Token) in Azure DevOps
# User Settings → Personal Access Tokens → New Token
# Scope: Agent Pools (Read & Manage)
# 2. Download the agent
mkdir ~/myagent && cd ~/myagent
curl -fkSL -o vsts-agent-linux-x64.tar.gz \
https://vstsagentpackage.azureedge.net/agent/3.248.0/vsts-agent-linux-x64-3.248.0.tar.gz
tar zxvf vsts-agent-linux-x64.tar.gz
# 3. Configure the agent
./config.sh \
--url https://dev.azure.com/{your-organization} \
--auth pat \
--token {your-pat-token} \
--pool my-linux-pool \
--agent my-agent-01 \
--acceptTeeEula
# 4. Run the agent interactively (for testing)
./run.sh
# 5. Install as a systemd service (for production)
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status
Now update your pipeline to use the self-hosted pool:
# Use self-hosted agent pool instead of Microsoft-hosted
pool:
name: 'my-linux-pool'
demands:
- Agent.OS -equals Linux
steps:
- script: |
echo "Running on self-hosted agent: $(Agent.Name)"
echo "Agent machine name: $(Agent.MachineName)"
hostname
uname -a
displayName: 'Verify self-hosted agent'
For a quick local test, you can run the self-hosted agent in a Docker container instead of setting up a full VM. Microsoft provides documentation for running the agent in Docker. This is great for local experimentation and disposable agents.
🐛 Debugging Scenarios
Scenario 1: "No hosted parallelism has been purchased or granted"
Symptom: Your pipeline is queued and never starts. The log shows: No hosted parallelism has been purchased or granted. To request a free parallelism grant, please fill out the following form: https://aka.ms/azpipelines-parallelism-request.
Root Cause: Since February 2021, new Azure DevOps organizations (especially those using free Microsoft-hosted agents) do not get automatic free parallel jobs. Microsoft introduced this restriction to prevent crypto-mining abuse of free CI/CD resources.
Fix:
- Option 1 (Free): Fill out the parallelism request form at
aka.ms/azpipelines-parallelism-request. You'll request a free tier grant for your organization. Approval takes 2-3 business days. - Option 2 (Paid): Go to Organization Settings → Billing → Pipelines and purchase parallel jobs ($40/month for Microsoft-hosted or $15/month for self-hosted).
- Option 3 (Self-hosted): Configure a self-hosted agent (Step 5 above). Self-hosted agents get 1 free parallel job immediately — no request needed.
Check your current parallelism: Organization Settings → Pipelines → Parallel jobs ───────────────────────────────────────────────── Microsoft-hosted: X of Y parallel jobs (0 = not granted) Self-hosted: X of Y parallel jobs (1 free by default)
Scenario 2: "No agent found in pool matching specified demands"
Symptom: Your pipeline job shows "Waiting for an agent" indefinitely, or fails with "No agent found in pool 'my-pool' which satisfies the specified demands."
Root Causes & Fixes:
- Pool is empty: No agents registered in the pool. Go to Organization Settings → Agent Pools → [your pool] → Agents tab. If the list is empty, you need to install and configure an agent (Step 5 above).
- Agent is offline: The agent is registered but not running. Check the agent status in the Agents tab (green = online, red = offline). SSH into the machine and run
sudo ./svc.sh statusor restart withsudo ./svc.sh start. - Demands mismatch: Your pipeline demands capabilities the agent doesn't have. Compare the pipeline's
demands:section with the agent's capabilities in Agent Pools → [pool] → Agents → [agent] → Capabilities. Add missing user capabilities or adjust demands. - All agents busy: All agents in the pool are running other jobs. You need more agents or more parallel jobs. Check the "Jobs" tab in the pool to see queued/running jobs.
# Check agent status on the self-hosted machine cd ~/myagent sudo ./svc.sh status # If stopped, restart it sudo ./svc.sh start # Check agent logs for errors cat _diag/Agent_*.log | tail -50
Scenario 3: "YAML file not found" / "Pipeline YAML file azure-pipelines.yml was not found"
Symptom: The pipeline fails immediately with an error about the YAML file not being found in the repository.
Root Causes & Fixes:
- Wrong file path: When creating the pipeline, you specified a path that doesn't match the actual file location. Go to Pipelines → [your pipeline] → Edit → ⋮ → Triggers → YAML tab and verify the YAML file path matches the actual file in your repo.
- File not on the trigger branch: The pipeline triggers on
main, but you committed the YAML file to a feature branch. Merge/push the YAML file tomain. - Case sensitivity: Azure Repos on Linux agents is case-sensitive.
Azure-Pipelines.yml≠azure-pipelines.yml. Verify the exact filename casing. - File in a subdirectory: If the YAML file is at
ci/azure-pipelines.yml, the pipeline configuration must point to/ci/azure-pipelines.yml, not/azure-pipelines.yml.
# Verify the file exists on the correct branch git checkout main ls -la azure-pipelines.yml # If missed, create and push it git add azure-pipelines.yml git commit -m "ci: add pipeline YAML file" git push origin main
Scenario 4: "Trigger not firing" — Pipeline doesn't run on push
Symptom: You push code to the repository, but the pipeline doesn't start. No run appears in the Pipelines section.
Root Causes & Fixes:
- Branch filter excludes your branch: If your trigger specifies
include: [main]but you pushed todevelop, the pipeline won't fire. Add the branch to the include list or use a wildcard:yamltrigger: branches: include: - main - develop - feature/* - Path filter excludes your changes: If you have path filters like
exclude: [docs/*, README.md]and you only changed files indocs/, the pipeline won't trigger. Review thepathssection in your trigger configuration. - Pipeline disabled: Someone may have disabled the pipeline. Go to Pipelines → [your pipeline] → ⋮ → Settings and check if "Disabled" is toggled on.
- CI trigger overridden in UI: The Azure DevOps UI can override YAML triggers. Check Pipelines → [pipeline] → Edit → ⋮ → Triggers. If "Override the YAML continuous integration trigger" is checked, the UI settings take precedence over the YAML file.
- YAML syntax error in trigger section: A malformed trigger section can silently disable triggers. Validate your YAML with the pipeline editor's syntax checker.
To check why a pipeline didn't trigger, look at the pipeline's Runs page. If there's truly no run, the trigger didn't fire. Try manually running the pipeline (Run pipeline → select your branch) to verify the pipeline itself is valid. If the manual run works but automatic triggers don't, the issue is almost always in the trigger configuration or UI override.
🎯 Interview Questions
Beginner
CI (Continuous Integration) is the practice of automatically building and testing code every time a developer pushes a commit. It catches bugs early — minutes after introduction rather than weeks later during manual testing. CD (Continuous Delivery) extends CI by automatically deploying validated code to staging environments, with a manual approval gate before production. CD (Continuous Deployment) goes further — every change that passes tests is automatically deployed to production. CI/CD is important because it: 1. Reduces risk — small, frequent releases are easier to debug than big-bang releases. 2. Speeds up delivery — features reach users faster. 3. Improves quality — automated tests catch regressions. 4. Frees developers — no manual build/deploy rituals.
Continuous Delivery means every commit is automatically built, tested, and deployed to a staging/pre-production environment. The code is always in a deployable state. However, the final deployment to production requires a manual approval — a human (release manager, product owner) clicks "approve" to push to prod. Continuous Deployment removes that manual gate — every commit that passes all automated tests is deployed to production automatically, with no human intervention. The key difference is the production gate: Delivery has one, Deployment doesn't. Most enterprises use Continuous Delivery because they want a human checkpoint before production. Continuous Deployment requires extremely high test coverage, robust monitoring, and automated rollback capabilities.
An agent is the compute resource that executes your pipeline jobs — it's the machine that actually runs the build, test, and deploy commands. Azure Pipelines offers two types: Microsoft-hosted agents are fully managed VMs provisioned on demand by Microsoft. You choose an OS image (Ubuntu, Windows, macOS), and you get a fresh VM for each job. They come with pre-installed tools (Docker, Node, Python, .NET, etc.) and require zero maintenance. Self-hosted agents are machines you own and manage — on-premises servers, cloud VMs, or containers. You install the Azure Pipelines agent software on them. They provide faster builds (persistent caching), network access to private resources, and full control over installed tools. Agents are organized into pools, and pipelines specify which pool to use.
The hierarchy from top to bottom is: Pipeline (the top-level YAML file) → Trigger (what event starts it — push, PR, schedule) → Stages (logical boundaries like Build, Test, Deploy-Staging, Deploy-Prod) → Jobs (a unit of work that runs on one agent — each job gets a fresh workspace) → Steps (individual operations within a job, run sequentially) → Tasks (pre-built reusable actions like DotNetCoreCLI@2 or inline scripts). Stages run sequentially by default but can be parallelized. Jobs within a stage can run in parallel on different agents. Steps always run sequentially within a job. For simple pipelines, you can omit stages and jobs — just define steps directly, and Azure DevOps wraps them in an implicit single stage and single job.
YAML pipelines are defined as code in a .yml file stored in your Git repository. They're version-controlled, support PR reviews, allow branching (each branch can have a different pipeline), support powerful templates for reuse, and get all new Azure Pipelines features. Classic pipelines are configured through a visual GUI editor in the Azure DevOps web portal. The definition is stored in Azure DevOps (not in your repo). They're easier for beginners (no YAML syntax to learn) but lack version control, PR reviews, and the advanced template system. Microsoft is investing in YAML pipelines and has stopped adding new features to Classic. For new projects, always choose YAML. Classic is suitable only for legacy projects or teams transitioning from manual CI/CD.
Intermediate
Microsoft-hosted: Zero setup, zero maintenance, fresh VM per run (no leftover state), pre-installed tools. Choose when: your build doesn't need private network access, you want zero ops overhead, and cold-start time (~20-40s) is acceptable. Ideal for most open-source projects and teams without strict build-infra requirements. Self-hosted: You manage the machine. Choose when: 1. You need private network access (on-prem databases, internal registries, VPNs). 2. Your builds are slow and need persistent disk caching (large monorepos, C++ builds). 3. You need specific software that's expensive to install each run (proprietary SDKs, GPU drivers). 4. Compliance requires builds to run on company-controlled infrastructure. 5. You want cheaper parallelism at scale ($15/month vs $40/month). The tradeoff: self-hosted agents need ongoing maintenance (OS patches, disk cleanup, agent updates) and security hardening.
Capabilities are key-value pairs that describe what an agent can do. There are two types: System capabilities are auto-detected by the agent software — OS version, installed tools, environment variables (e.g., Agent.OS=Linux, node=18.17.0). User capabilities are manually added by an admin (e.g., GPU=true, region=us-east). Demands are requirements specified in the pipeline YAML that an agent must satisfy to run the job. Syntax: demands: [Agent.OS -equals Linux, docker]. The pipeline scheduler matches demands against capabilities — only agents that satisfy all demands are eligible. If no agent matches, the job queues indefinitely. This mechanism is critical for self-hosted pools where agents may have different configurations. Microsoft-hosted agents have a fixed, well-documented set of capabilities per VM image.
Azure DevOps YAML pipelines support four trigger types: 1. CI trigger (trigger:): Fires on code pushes to specified branches. Supports branch filters (include/exclude), path filters (only trigger when specific files change), and tag filters. 2. PR trigger (pr:): Fires when a pull request is created or updated targeting specified branches. Runs a validation build to check the PR before merging. 3. Scheduled trigger (schedules:): Fires on a cron schedule (e.g., nightly builds at 2 AM). Uses cron syntax with timezone support. Can be configured to run only if source code changed since last run. 4. Pipeline resource trigger (resources: pipelines:): Fires when another pipeline completes — enabling pipeline chaining (e.g., deploy pipeline triggers after build pipeline finishes). Each trigger type can be combined in a single YAML file. Setting trigger: none disables automatic CI triggers, requiring manual runs.
A multi-stage YAML pipeline defines the entire CI/CD workflow — build, test, and multiple deployment environments — in a single YAML file. Before multi-stage pipelines, Azure DevOps required separate Build pipelines (CI) and Release pipelines (CD). Multi-stage YAML unifies them: Stage 1: Build — compile, unit test, publish artifact. Stage 2: Deploy to Dev — auto-deploy to development environment. Stage 3: Deploy to Staging — deploy to staging, run integration tests. Stage 4: Deploy to Production — deploy to prod with a manual approval gate using environment with approvals configured. Stages use dependsOn to define execution order and condition for conditional execution. The entire workflow is version-controlled, reviewable via PRs, and consistent across branches. This replaces the Classic Release pipeline's visual stage designer.
There are two ways: 1. Pipeline variable: Add system.debug: true to the variables: section of your YAML file. This enables verbose output for all tasks — they log detailed diagnostic information including input parameters, environment variables, and internal operations. 2. Queue-time variable: When manually running a pipeline, click "Variables" and add system.debug = true. This enables debug logging for that single run without modifying the YAML file — useful for one-off troubleshooting. Additionally, you can use ##vso[task.setVariable] logging commands in scripts to set output variables, and ##vso[task.logissue] to surface warnings/errors. The agent also writes diagnostic logs to the _diag/ folder on the machine, which you can download from the pipeline run's logs page via "Download logs" button.
Scenario-Based
Investigation: First, analyze which stages/jobs/steps take the longest — open a pipeline run and check step durations. Then apply targeted optimizations: 1. Parallelize jobs: Run unit tests, integration tests, and linting as separate parallel jobs across multiple agents instead of sequential steps. 2. Pipeline caching: Use the Cache@2 task to cache NuGet packages, npm modules, or Docker layers between runs — avoiding re-downloads. 3. Incremental builds: Switch to self-hosted agents where build artifacts persist on disk, enabling incremental compilation. 4. Optimize Docker builds: Use multi-stage Dockerfiles and layer caching. Only rebuild layers that changed. 5. Test splitting: Split your test suite across parallel agents and merge results — the PublishTestResults task supports this. 6. Reduce scope: Use path-based triggers to only run the full pipeline when relevant code changes (skip for docs-only commits). 7. Pre-built base images: Use a custom Docker agent image with all tools pre-installed instead of installing them during each run.
Option 1 — Self-hosted agent on the corporate network: Install a self-hosted agent on a VM that has network access to the on-prem SQL Server. Register it in a dedicated pool (e.g., corp-network-pool). Update your pipeline to use pool: name: 'corp-network-pool'. This is the simplest and most common approach. Option 2 — Self-hosted agent with VPN/ExpressRoute: If the agent runs in Azure (not on-prem), configure Azure VPN Gateway or ExpressRoute to connect the Azure VNet to the corporate network. Place the self-hosted agent VM in that VNet. Option 3 — Azure DevOps Agent behind a proxy: If network policies require a proxy, configure the agent with --proxyurl during setup. Option 4 — Hybrid approach: Run build/unit-test stages on Microsoft-hosted agents (fast, no network needed), then run the integration-test stage on a self-hosted agent that has network access. This minimizes self-hosted infra while solving the network problem.
Step-by-step debugging: 1. Check pipeline status: Is the pipeline disabled? Go to Pipelines → [pipeline] → ⋮ → Settings. Ensure "Disabled" is not toggled on. 2. Check trigger config: Open the YAML file. Does the trigger: section include main? If trigger: none, no automatic triggers fire. 3. Check UI override: Go to Pipeline → Edit → ⋮ → Triggers. If "Override the YAML continuous integration trigger from here" is checked, the UI settings override YAML — it might have different branch filters. 4. Check path filters: If the trigger has paths: exclude: [docs/*, README.md] and the commit only touched docs/, the pipeline correctly won't trigger. 5. Check YAML syntax: A malformed trigger section can silently break. Use the pipeline editor to validate YAML. 6. Check service connection: If using an external repo (GitHub), the webhook may be broken. Re-authorize the service connection. 7. Test manually: Run the pipeline manually with the same branch. If that works, the pipeline itself is valid — the issue is purely in the trigger configuration.
Phase 1 — Inventory & Classify: List all 50 Jenkins jobs. Classify by type: CI builds, CD deployments, scheduled jobs, utility scripts. Identify dependencies between jobs (job chaining). Document each job's triggers, steps, credentials, and artifact flows. Phase 2 — Infrastructure: Set up Azure DevOps organization, projects, repos, service connections (to Azure, Docker registries, Kubernetes clusters). Migrate secrets from Jenkins credential store to Azure DevOps variable groups or Azure Key Vault. Decide agent strategy: Microsoft-hosted for most builds, self-hosted for jobs that need private network access. Phase 3 — Migrate (incremental): Start with simple CI jobs. Convert Jenkinsfile stages to YAML stages. Map Jenkins plugins to Azure Pipelines tasks (many have direct equivalents). Run the Jenkins and Azure pipeline in parallel to validate output matches. Phase 4 — Complex jobs: Migrate multi-branch pipelines, parameterized builds, and CD jobs. Use YAML templates to replace Jenkins shared libraries. Phase 5 — Cutover: Disable Jenkins jobs one by one as Azure equivalents are validated. Keep Jenkins read-only for 30 days, then decommission.
Root cause: Azure DevOps environment approvals and checks (or pipeline permissions) are restricting which branches can deploy. Several mechanisms can cause this: 1. Environment branch control: The "staging" environment may have a check configured to only allow deployments from the main branch. Go to Pipelines → Environments → staging → Approvals and checks and look for a "Branch control" check. 2. Service connection authorization: The service connection used for deployment may be authorized only for the main branch pipeline. Go to Project Settings → Service connections → [connection] → Security and check pipeline permissions. 3. Variable group restricted access: If the pipeline uses a variable group with secrets, the group may only be authorized for specific pipelines or branches. Fix: For feature-branch deployments to staging (which is a valid use case), add the feature branch pattern to the branch control check's allowed list, or create a separate "dev" environment with relaxed branch controls. Never relax production environment branch controls.
🌍 Real-World Use Case
A Fintech Startup's Journey from Manual Deploys to CI/CD
Consider a fintech startup building a payments API — 8 developers, a Node.js backend, a React frontend, deployments to Azure Kubernetes Service, and a regulatory requirement for audit trails on every production change.
The problem (before CI/CD):
- Deployments were manual — a senior developer SSH'd into the server, pulled code from Git, ran
npm installandnpm run build, and restarted the service. The process was undocumented and only two people knew how to do it. - No automated tests — bugs were discovered by QA testers days after code was written. A typo in a payment calculation made it to production and caused a $12,000 overcharge that took 3 days to detect.
- Deployments happened once a month — a "big bang" release with 200+ commits. Debugging failures was painful because any of 200 changes could be the cause.
- No audit trail — regulators asked "who approved this production deployment and when?" and the answer was "we don't know, someone ran a script."
What they built with Azure Pipelines:
- CI pipeline (YAML): Triggers on every push to any branch. Runs
npm install,npm run lint,npm test(200+ unit tests), andnpm run build. Publishes a Docker image to Azure Container Registry with the Git SHA as the tag. Build time: 4 minutes. - Multi-stage CD pipeline: Stage 1 (Dev) auto-deploys on push to
main. Stage 2 (Staging) deploys after Dev succeeds — runs integration tests against a staging database. Stage 3 (Production) requires manual approval from the Tech Lead and a "Branch control" check that only allowsmain. - Agent setup: Microsoft-hosted agents for CI (no private network needed). One self-hosted agent on a VM with VNet peering to the production AKS cluster for CD stages.
- PR validation: A separate pipeline triggers on PRs — runs lint, unit tests, and a security scan (OWASP dependency check). PRs can't merge unless this pipeline passes (branch policy).
- Audit trail: Every production deployment is linked to a work item (via
AB#in commit messages), approved by a named person (environment approval), and logged with timestamps. Regulators get a report in minutes.
Results after 6 months:
- Deployment frequency: from once a month to 3-5 times per week
- Lead time for changes: from 3 weeks to under 2 days
- Bug escape rate: reduced 80% — automated tests catch regressions before code reaches staging
- Mean time to recovery (MTTR): from 4 hours to 15 minutes — automated rollback to last-known-good image
- Audit compliance: 100% — every production change has a traceable approval chain
- Developer satisfaction: no more "deployment Fridays" — anyone can deploy safely at any time
CI/CD isn't just about automation — it's about confidence. When you know that every commit is built, tested, and deployable, you can ship faster with less risk. Azure Pipelines provides the execution engine (agents), the workflow model (stages, jobs, steps), and the governance layer (approvals, branch controls, audit logs) to make this a reality for teams of any size.
📝 Summary
| Concept | Key Takeaway |
|---|---|
| Continuous Integration | Every commit triggers an automated build and test. Bugs are caught in minutes, not weeks. The output is a validated artifact. |
| Continuous Delivery | CI + automated deployment to staging. Production requires manual approval. This is what most enterprises practice. |
| Continuous Deployment | CI + automated deployment all the way to production. No human gate. Requires excellent tests and monitoring. |
| Pipeline Hierarchy | Pipeline → Trigger → Stages → Jobs → Steps → Tasks. Stages are logical boundaries. Jobs run on agents. Steps are sequential. |
| Microsoft-Hosted Agents | Zero maintenance, fresh VM per run, pre-installed tools. Best for most teams. Limited: no private network access. |
| Self-Hosted Agents | Full network access, persistent caching, tool control. Best for private networks and large builds. You maintain the machine. |
| Agent Capabilities & Demands | Capabilities describe what an agent has. Demands describe what a pipeline needs. Scheduler matches them to find compatible agents. |
| YAML Pipelines | Pipeline-as-Code in .yml file. Version-controlled, PR-reviewable, full feature set. Microsoft's primary investment. |
| Classic Pipelines | GUI-based visual editor. No version control, no new features. Use only for legacy or quick prototyping. |
| When to use which | New projects → YAML always. Existing Classic → migrate incrementally. Need private network → self-hosted agent. Otherwise → Microsoft-hosted. |