stages:
- stage: Build
jobs:
- job: build
steps:
- script: npm ci && npm run build
- publish: dist
artifact: web
- stage: DeployProd
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: web
environment: prod
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: sc-devops-lab
appName: app-devops-lab-01
package: $(Pipeline.Workspace)/web
flowchart LR
B["Build<br/>test, publish artifact"] --> DD["DeployDev<br/>environment dev"]
DD --> AP{"Approval<br/>on Environment prod"}
AP --> DP["DeployProd<br/>AzureWebApp@1"]
DP -- "service connection sc-devops-lab<br/>(workload identity federation)" --> APP["app-devops-lab-01"]
| Concept | Key points |
|---|---|
dependsOn / condition |
Stage order; for example deploy to prod only from main |
| Deployment job | - deployment: + environment: + strategy: runOnce/rolling/canary; records deployment history |
| Approvals and checks | Configured on the Environment (UI/REST): approvers, business hours… — not written in YAML |
| Service connection | Lets the pipeline call Azure/AWS/K8s/registries; for Azure Resource Manager use workload identity federation (no stored client secret), scoped to a resource group |
| Deploy tasks | AzureWebApp@1 (optionally into a slot, then AzureAppServiceManage@0 to swap), Docker@2 build & push to ACR → KubernetesManifest@1 to AKS, AzureCLI@2 |
| Azure Artifacts | Internal package feeds, upstream sources proxying npmjs/NuGet.org; not for Docker images |
| Classic Releases | The old way; multi-stage YAML is the main path |
Traps: putting approvals in YAML; pasting a client secret into a variable; giving a service connection Contributor on the whole subscription.
| Command | Meaning |
|---|---|
az pipelines run --name my-app-ci --branch main |
Run a pipeline |
az pipelines runs list -o table / az pipelines runs show --id 42 |
Inspect runs |
az devops service-endpoint list -o table |
List service connections |
az artifacts universal publish --feed my-feed --name web-dist --version 1.0.0 --path ./dist --description "web build" |
Publish a Universal package |
A deployment job in YAML
- stage: DeployProd
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: Web
environment: prod
pool: { vmImage: ubuntu-latest }
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: webapp
- task: AzureWebApp@1
inputs:
azureSubscription: sc-prod
appName: app-devops-lab-01
package: $(Pipeline.Workspace)/webapp/*.zip
A deployment job downloads artifacts published earlier with PublishPipelineArtifact@1, so production gets exactly the build that passed tests. runOnce also supports preDeploy, routeTraffic, postRouteTraffic, and on: failure hooks for smoke tests and rollbacks.
Environments and checks
An environment records every deployment to it, linking runs, commits, and work items. Checks configured on it include manual approvals, business hours, required template (only pipelines extending an approved template may deploy), exclusive lock (one deployment at a time), and invoke REST API or Azure Function for automated gates such as "no open incidents". Kubernetes and VM resources can be added to an environment for health views.
Infrastructure in the same pipeline
A stage can run terraform plan and apply (through AzureCLI@2 or a Terraform task) before deploying the app, with the plan stage requiring approval. Keep state in a remote backend such as an Azure Storage account, and use separate service connections per environment.
Promoting through environments
A common layout is Build → Dev → Test → Prod, each a stage with its own environment and variable group. The same artifact moves forward; only configuration changes. Failed smoke tests stop promotion, and redeploying the previous run is the fastest rollback.