Home · About · Services · Industries · Projects · Products · Process · Blog · FAQ · Contact
Azure DevOps pipelines for web apps: a practical YAML setup How we set up Azure DevOps pipelines for web apps: YAML stages, secret-free service connections, environment approvals, Key Vault secrets and slot swaps.
A typical starting point: a web app on Azure App Service, deployed from a developer's laptop with a right-click publish or a zip upload. It works until two people deploy different branches on the same afternoon, or someone ships a build that was never tested. Azure Pipelines fixes that, but only if the pipeline is set up with a few decisions made up front.
The short answer: to set up Azure DevOps pipelines for a web app, keep a YAML pipeline in the repository. Build and test once, publish a pipeline artifact, and deploy that same artifact to staging and production through environments with approvals. Connect to Azure with workload identity federation, keep secrets in Key Vault and release to production with a slot swap.
The rest of this post walks through that setup, with a pipeline file you can adapt and the mistakes we see most often.
YAML pipelines, not classic release pipelines Azure DevOps still has the older classic build and release pipelines, edited in the web UI. We don't start new work on them. YAML pipelines live in the repository, so changes are reviewed in pull requests and the history sits next to the code. Newer organisations have creation of classic pipelines turned off by default, and most new features, such as environments with checks and templates, are built around YAML.
A few terms are worth getting straight before writing the file:
Stage: a major phase such as Build, Staging or Production. Stages run in order by default and can depend on each other. Job: a set of steps that runs on one agent. A deployment job is a special job type that targets an environment and records deployment history. Agent: the machine that runs a job. Microsoft-hosted agents are fresh virtual machines per job; self-hosted agents are your own machines. Service connection: stored credentials that let the pipeline talk to Azure, a container registry or another service. Environment: a named target such as staging or production, where you attach approvals and checks. Connect Azure without storing a secret Create the Azure Resource Manager service connection under Project settings, Service connections, and choose workload identity federation. The pipeline then exchanges a short-lived token with Microsoft Entra ID at run time, so there is no client secret to copy, store or rotate. Older connections that use a service principal secret will stop working when the secret expires, usually long after whoever set it up has forgotten about it. If you have those, convert them; the service connection page offers a conversion option for most of them.
Scope each connection to the resource group it needs, not the whole subscription, and give its identity a role such as Website Contributor rather than Owner. We usually create one connection for staging and one for production so a mistake in a staging job cannot touch the production resource group. The first time a pipeline uses a connection, Azure DevOps asks someone to permit it. That is a useful control, so leave it on.
A pipeline file for a Node.js web app This example builds a Node.js app and deploys it to a Linux App Service. The same shape works for .NET, Python or a frontend bundle: only the build steps change.
trigger:
branches:
include: [ main ]
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm
stages:
- stage: Build
jobs:
- job: build
pool:
vmImage: ubuntu-latest
steps:
- task: UseNode@1
inputs:
version: '22.x'
- task: Cache@2
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
path: $(npm_config_cache)
displayName: Cache npm
- script: |
npm ci
npm run lint
npm test
npm run build
npm prune --omit=dev
displayName: Install, test, build
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: $(System.DefaultWorkingDirectory)
includeRootFolder: false
archiveFile: $(Build.ArtifactStagingDirectory)/app.zip
- publish: $(Build.ArtifactStagingDirectory)/app.zip
artifact: web
- stage: Staging
dependsOn: Build
jobs:
- deployment: deploy_staging
pool:
vmImage: ubuntu-latest
environment: staging
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: sc-myapp-staging
appType: webAppLinux
appName: myapp-staging
package: $(Pipeline.Workspace)/web/app.zipSome details in that file matter more than they look. The build runs once and the zip is published as a pipeline artifact. Every later stage deploys that exact file, so what reached production is what passed the tests. Deployment jobs download the current run's artifacts automatically into $(Pipeline.Workspace), under a folder named after the artifact. The cache step keys on package-lock.json, so dependencies are restored from cache until the lock file changes. And npm prune removes development dependencies before zipping, which keeps the package smaller.
Pull request validation depends on where the code lives. For GitHub repositories, a pr section in the YAML triggers builds on pull requests. For Azure Repos, the pr keyword is ignored and you add the pipeline as a build validation rule in the branch policy for main instead. Either way, run the Build stage on pull requests and keep the deploy stages for merges to main, with a condition such as and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) on the staging stage. Keep the succeeded() part: a custom condition replaces the default one, so without it the stage would run even after a failed build.
Production with approvals and a slot swap The production stage is the same deployment job pointed at the production environment, but it deploys to a staging slot of the production app and then swaps. A slot swap warms the new version up before it takes traffic, and if something is wrong you swap back in seconds instead of rebuilding. Deployment slots need the Standard App Service tier or above.
- stage: Production
dependsOn: Staging
jobs:
- deployment: deploy_prod
pool:
vmImage: ubuntu-latest
environment: production
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: sc-myapp-prod
appType: webAppLinux
appName: myapp-prod
deployToSlotOrASE: true
resourceGroupName: rg-myapp-prod
slotName: preprod
package: $(Pipeline.Workspace)/web/app.zip
- script: curl --fail --retry 5 --retry-delay 10 https://myapp-prod-preprod.azurewebsites.net/health
displayName: Smoke test slot
- task: AzureAppServiceManage@0
inputs:
azureSubscription: sc-myapp-prod
Action: Swap Slots
WebAppName: myapp-prod
ResourceGroupName: rg-myapp-prod
SourceSlot: preprodApprovals are not written in the YAML. Open Pipelines, Environments, choose production, and add an approval under Approvals and checks. Add the people who can release, and consider a business hours check so releases only go out when someone is around to watch them. Because the check sits on the environment, every pipeline that targets production goes through it, including pipelines someone adds later.
Check the default host name format for your app before copying the smoke test URL. Newer App Service apps can use a unique default host name with a random suffix and region, so read the real slot URL from the portal and store it as a variable.
Secrets and configuration Keep secrets out of the YAML and out of plain pipeline variables where you can. The approaches we use, roughly in order of preference:
Let the app read secrets itself. App Service settings can be Key Vault references, and the app's managed identity reads the value at run time. The pipeline never sees the secret. Where the pipeline does need a secret, link a variable group to Key Vault so values are fetched at run time and masked in logs. Mark slot-specific settings, such as a connection string that must differ between preprod and production, as deployment slot settings so they stay put during a swap. Remember that secret variables are not passed to builds from forked repositories by default. Keep it that way for public repositories. Keep App Service configuration under version control too, either in Bicep or Terraform, or at least in an AzureCLI@2 step that sets app settings. Settings edited by hand in the portal are the first thing lost when an app is recreated.
Agents, cost and keeping pipelines tidy Microsoft-hosted agents are the right default. Each job gets a clean machine, so there is no build residue and no server to patch. The cost is in parallel jobs: with one parallel job, pipelines queue behind each other. Check the current free grant and pricing for your organisation in the Parallel jobs page of organisation settings rather than relying on old blog posts, since the numbers and the process for new organisations have changed over time.
Self-hosted agents make sense when builds need access to a private network, a database inside a virtual network, or tools that are slow to install every run. A self-hosted agent is a server you now own: patch it, clean its work folders and do not run untrusted pull request builds on it. For agents inside Azure, Managed DevOps Pools give you Azure-hosted machines in your own virtual network without managing the scale set yourself.
Once you have more than two or three pipelines, move repeated steps into templates in a shared repository and reference them with a resources section. Templates also let you use extends to enforce required steps, such as a dependency scan, across every pipeline.
Checklist before you rely on it The pipeline is YAML in the repository, reviewed like any other code. Service connections use workload identity federation and are scoped to one resource group each. The app is built once; every stage deploys the same artifact. Pull requests run build and tests through a pr trigger or a branch policy. Production is an environment with approvals, and deploys go through a slot and a swap. Secrets live in Key Vault, not in YAML or untracked portal settings. A smoke test runs before traffic moves, and the team knows how to swap back. If deploys still involve someone logging in to the portal, or nobody is sure which commit is running in production, the pipeline is not finished yet. Those two gaps are where we would start.
Infigini IT Services · 11 Kanwali Road, Dehradun 248001, India · info@infigini.com · +91 95684 87789