The web-reservas artifact exists: built once, tested and stored. But it is still sitting there. Between that file and app-contoso-reservas-pro there is still Marta Ríos pasting commands on a Friday night, and as long as that remains true Contoso's DORA metrics will not move: lead time will still be weeks and time to restore will still be hours.
This lesson covers that last stretch, which is the scary one. You will see how environments turn "I think production has last week's version" into an exact fact; how checks and approvals put a conscious gate in front of production without going back to manual deployment; and how the slot swap you already know from module 2 becomes a real blue-green deployment, with a rollback that takes seconds. By the end, deploying will stop being an event.
Contents
- Continuous delivery versus continuous deployment
- Environments: which version is where
- Checks and approvals
- The multi-stage pipeline
- Deployment strategies and App Service slots
- The service connection restricted per environment
- Database migrations: expand and contract
- Post-deployment verification and rollback
- Feature flags: deploying is not releasing
- Common Mistakes and Tips
- Exercises
- Conclusion
- Continuous delivery versus continuous deployment
Two terms that get used as synonyms and are not:
| Continuous delivery | Continuous deployment | |
|---|---|---|
| What it guarantees | Every validated change can be deployed at any moment | Every validated change is deployed automatically |
| Getting into production | A human decision, a button | Automatic, with no intervention |
| Requirements | A reliable pipeline, solid tests | Plus: telemetry, automatic rollback, a mature culture |
| Risk per deployment | Low | Very low (tiny, frequent changes) |
Contoso sits at continuous delivery with one exception: desarrollo and preproduccion are deployed automatically on every merge to main, and produccion requires an approval. This is not a lack of ambition, it is proportionality: a bad deployment on a ticket sales website has immediate financial consequences, and the team does not yet have module 7's telemetry or the automatic rollback that would justify removing that gate. The approval is a reviewable risk decision, not dogma; once the change failure rate drops below 15%, it will make sense to revisit it.
- Environments: which version is where
An environment in Azure Pipelines is a named entity representing a deployment target — desarrollo, preproduccion, produccion — that brings three things a plain deployment step does not:
- Traceability: it records which run, which artifact and which commits were deployed, and when. It is the exact answer to "which version is in production?".
- Registered resources: optionally it associates virtual machines or Kubernetes namespaces, with their status.
- Checks: it is where approvals and the other gates are attached. One important detail: the protection lives in the environment, not in the YAML, so whoever edits the pipeline cannot remove it.
# Environments are created from the portal (Pipelines > Environments) or with the REST API.
# When a deployment job runs against a nonexistent environment, it is created automatically,
# but it is better to create them beforehand so you can configure their checks.
az pipelines runs list --pipeline-name contoso-reservas-cd \
--query "[0].{run:name, result:result, branch:sourceBranch}" -o tableContoso creates three: desarrollo (automatic deployment, no gates), preproduccion (automatic, with a branch check) and produccion (human approval, deployment window and branch check).
- Checks and approvals
A check is a condition that must be met before a job can deploy to the environment. The ones Contoso uses:
| Check | Configuration on produccion |
What it prevents |
|---|---|---|
| Approval | Marta Ríos or the Contoso-Operaciones group; whoever started the run cannot approve themselves |
A change getting through without a conscious decision |
| Deployment window | Monday to Thursday, 08:00-16:00 (West Europe) |
The Friday-afternoon deployment and the season-opening weekend, when nobody is on call |
| Branch control | refs/heads/main only |
Somebody deploying a working branch to production |
| Invoke Azure Function | Calls a function that checks whether a severity 1 incident is open and returns success or failure | Deploying in the middle of a crisis |
| Query Azure Monitor alerts | Checks the active alerts on log-contoso-pro |
Deploying onto an already degraded system |
The approval deserves a clarification. It is not bureaucratic red tape: it is the point at which a person with operational context looks at what is about to go in, checks that somebody is available in case things go wrong, and decides. That is why it is limited to a small group, has a timeout (Contoso sets 3 days, after which the run is cancelled) and allows a comment to be left on the record.
The deployment window encodes the rule everybody states and nobody follows. It used to be "let's try not to deploy on Fridays"; now the run simply waits until Monday at 08:00. And during the season-opening week, when booking volume multiplies, produccion is frozen by temporarily disabling the automatic deployment into preproduction or by adding a second approval.
- The multi-stage pipeline
This is contoso-reservas-cd, which takes the artifact from 05-03 and carries it to the three environments:
name: deploy-$(Build.BuildNumber)
# Triggered when the continuous integration pipeline COMPLETES successfully:
# it builds nothing, it only consumes its artifact.
resources:
pipelines:
- pipeline: ci # Alias used to reference it later
source: contoso-reservas-ci
trigger:
branches: { include: [ main ] }
trigger: none # Never started by a direct push
variables:
- group: vg-contoso-reservas-comun
stages:
- stage: development
displayName: Deploy to development
jobs:
# 'deployment' (rather than 'job') is what hooks into an environment and its history
- deployment: deploy_dev
environment: desarrollo
pool: { vmImage: ubuntu-latest }
strategy:
runOnce: # Simplest strategy: replace and done
deploy:
steps:
- download: ci # Retrieve the artifact from the CI pipeline
artifact: web-reservas
- task: AzureWebApp@1
inputs:
azureSubscription: sc-contoso-dev # Service connection
appName: app-contoso-reservas-dev
package: $(Pipeline.Workspace)/ci/web-reservas
- stage: preproduction
dependsOn: development # Only if the previous stage succeeded
condition: succeeded()
jobs:
- deployment: deploy_pre
environment: preproduccion
pool: { vmImage: ubuntu-latest }
strategy:
runOnce:
deploy:
steps:
- download: ci
artifact: web-reservas
- task: AzureWebApp@1
inputs:
azureSubscription: sc-contoso-pro
appName: app-contoso-reservas-pro
deployToSlotOrASE: true
resourceGroupName: rg-contoso-reservas-pro
slotName: preproduccion # The SLOT, not the live site
package: $(Pipeline.Workspace)/ci/web-reservas
- script: |
# Check the health of the slot BEFORE swapping it
curl -f https://app-contoso-reservas-pro-preproduccion.azurewebsites.net/salud
displayName: Check the slot health endpoint
- stage: production
dependsOn: preproduction
# Only from main: a second belt alongside the environment check
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: swap
environment: produccion # This is where the approval and the window wait
pool: { vmImage: ubuntu-latest }
strategy:
runOnce:
deploy:
steps:
# Deploying to production does NOT copy files: it swaps slots
- task: AzureAppServiceManage@0
inputs:
azureSubscription: sc-contoso-pro
Action: 'Swap Slots'
WebAppName: app-contoso-reservas-pro
ResourceGroupName: rg-contoso-reservas-pro
SourceSlot: preproduccion
SwapWithProduction: true
- script: curl -f https://www.contosoairlines.example/salud
displayName: Post-deployment verificationThree pieces deserve attention. resources.pipelines makes this pipeline trigger when the continuous integration one finishes and lets it download its artifact: nothing is rebuilt, exactly the binary that was tested is what gets deployed. deployment instead of job is what links the job to the environment and feeds its history. And dependsOn plus condition chain the stages: production is not attempted if preproduction failed.
- Deployment strategies and App Service slots
| Strategy | How it works | Downtime | Cost | Rollback |
|---|---|---|---|---|
| Recreate | Stop the old, start the new | Yes, visible | None | Redeploy the previous version (slow) |
| Rolling | Instances are replaced in batches | No | None | Slow; two versions coexist |
| Blue-green | Two complete environments; traffic is switched over at once | No | Double during the deployment | Immediate: switch back |
| Canary | The new version first receives 5% of traffic | No | Low | Fast; withdraw the percentage |
Contoso uses blue-green, and implements it with App Service deployment slots (02-03), which is where these strategies stop being theory:
graph LR
subgraph Before
U1[Users] --> P1["Production slot<br/>v2.4.0 · blue"]
CD1[Pipeline] -.deploys.-> S1["preproduccion slot<br/>v2.5.0 · green"]
end
subgraph "Swap with warm-up"
W["App Service calls /salud on the green slot<br/>until it responds successfully"]
end
subgraph After
U2[Users] --> P2["Production slot<br/>v2.5.0 · green"]
P2 -.-> S2["preproduccion slot<br/>v2.4.0 · blue, ready to roll back"]
end
Before --> W --> After
What makes the App Service swap special is the warm-up: before moving traffic, the platform starts the slot's application, applies the production settings to it and calls its /salud endpoint until it responds successfully. Only then does it switch. That removes the classic problem of homemade blue-green, where the first users pay for the cold start. And the swap is an internal routing change, not a file copy: it takes seconds and is reversible with the same operation.
Requirements you have to respect: settings that must not travel with the code — connection strings, @Microsoft.KeyVault(...) references — have to be marked as a slot setting so they stay pinned to their slot, and the plan-contoso-reservas-pro plan must be Standard or above (Contoso uses Premium v3 with zone redundancy, so it more than qualifies).
- The service connection restricted per environment
The sc-contoso-dev and sc-contoso-pro connections from 05-01 are not interchangeable, and you have to actively prevent them from being misused:
- Minimum scope and role:
sc-contoso-prohas Website Contributor onrg-contoso-reservas-pro. It can deploy and swap slots; it cannot touchrg-contoso-red-proor delete a database. - No open access: by turning off "grant access permission to all pipelines", a new pipeline does not inherit the ability to deploy to production. Authorizing it is an explicit act.
- Connection check on the environment: the Required template check or the connection's own restriction lets you require that it only be used from an approved template.
- Workload identity federation: no secret to expire and none that can be exfiltrated.
The principle is module 4's, carried into delivery: the credential that deploys the website should not be able to do anything except deploy the website.
- Database migrations: expand and contract
This is the point where most "zero-downtime" deployments come apart. During a blue-green swap, and during the rollback window afterwards, two versions of the code coexist against the same db-reservas database. Therefore:
Every schema migration must be backward compatible: the previous version of the code has to keep working against the new schema.
The pattern that guarantees this is expand and contract, in three separate deployments. Suppose Contoso needs to replace the nombre_pasajero column with nombre and apellidos:
| Phase | Deployment | Schema | Code |
|---|---|---|---|
| Expand | 1 | nombre and apellidos are added, nullable; nombre_pasajero is kept |
Writes to all three columns, reads from the old one |
| Migrate | — | A process backfills nombre/apellidos for historical rows |
No changes |
| Contract | 2 and 3 | Once you have confirmed nobody uses the old one, nombre_pasajero is dropped |
Deployment 2: reads from the new ones. Deployment 3: stops writing to the old one |
Practical rules: never rename or drop a column in the same deployment that introduces its replacement; never add a NOT NULL column with no default, because the old version will not populate it; and run migrations before the swap, in a dedicated step against sql-contoso-reservas-pro from a pool-contoso-privado agent — the server is only reachable through pe-sql-reservas — and with the managed identity you learned about in 04-02, not with passwords.
The important consequence: a destructive migration breaks rollback. If the deployment drops a column and you have to go back, the previous version will find a schema it does not understand. With expand and contract, the reverse swap always works.
- Post-deployment verification and rollback
Deploying and marking the run green does not mean it works. Contoso adds two verifications:
- Health check:
curl -f https://www.contosoairlines.example/salud, which returns an error if the endpoint does not respond with 200. The/saludendpoint must check real dependencies — connectivity todb-reservas, access tokv-contoso-pro— and not merely return "OK". - Smoke tests: a handful of requests exercising the critical path (search for a flight, check availability) against the preproduction slot before the swap, not after.
And when something fails, the rollback. There are two ways and they are not equivalent:
| Slot re-swap | Redeploy the previous version | |
|---|---|---|
| What it does | Switches the routing back | Runs the pipeline again with the old artifact |
| Time | Seconds | Minutes (download, deploy, warm-up) |
| Application state | Already warm in the other slot | Cold start |
| When to use it | Whenever possible | If another swap has already happened on top, or if you need to go back several versions |
The re-swap is literally the same Swap Slots task run again, which is why it is worth keeping a one-step rollback pipeline ready to launch without thinking. This is the reason Contoso's time to restore DORA metric can go from hours to under a minute: not because the team is faster at fixing things, but because going back no longer involves fixing anything.
One warning: rollback reverts the code, not the data. If the new version wrote records in a format the previous one does not understand, or if the contract phase has already run, the re-swap is not enough. Hence the discipline in the previous section.
- Feature flags: deploying is not releasing
The last step in making deployment stop being frightening is to separate two things we tend to conflate: deploying (the code being in production) and releasing (users actually using it). A feature flag is a condition in the code that switches a capability on or off without deploying anything:
// The cabin map travels to production switched off; it is switched on when the team
// decides, for a percentage of users, and switched off in seconds if it misbehaves.
if (await _flags.IsEnabledAsync("cabin-map"))
{
return View("InteractiveCabinMap", model);
}
return View("ClassicSeatSelection", model);This enables three things: feature branches that are genuinely short — half-finished work is merged switched off — canary releases by percentage of users with no extra infrastructure, and switching a broken feature off instantly with no rollback needed. Azure App Configuration offers this as a managed service with its feature manager, percentage and user-group filters, and direct integration with App Service.
The cost is real and worth acknowledging: every flag is a branch that multiplies the possible paths through the code. They are managed as debt — with an owner and a retirement date — and removed as soon as the feature is settled.
What you measure after all this is module 7's subject: every deployment should be visible in Azure Monitor and Application Insights, with error and latency metrics before and after the swap. Without that feedback, Marta's approval is still a decision made blind.
Common Mistakes and Tips
- Rebuilding for each environment. It breaks the guarantee that what was tested is what was deployed. The deployment pipeline consumes the artifact, it never regenerates it.
- Putting approvals in the YAML. Whoever edits the file could remove them. Checks live in the environment, which has its own permissions.
- Destructive migrations in the same deployment. They break rollback right when you need it. Expand and contract, always, in separate deployments.
- A
/saludendpoint that just returns "OK". It checks nothing. It must verify the real dependencies: database, secret store, critical external services. - Slots with no slot settings marked. The production connection string travels to preproduction or the other way round, and you end up writing to the wrong database.
- Approving out of habit. An approval that is always granted in two seconds is not a gate, it is a click. If nobody looks at what goes in, you are better off automating and putting the effort into telemetry.
- Tip: keep a one-step rollback pipeline, and rehearse it. A rollback that has never been tested is not a rollback.
- Tip: tag the repository automatically when you deploy to production; that way the correspondence between tag, artifact and environment depends on nobody.
Exercises
Exercise 1: designing an environment's gates
Contoso Miles (centro-coste=CC-2077) is about to set up its continuous deployment. Its team is four developers and one infrastructure lead. Its website gets far less traffic than the bookings one, and its peak usage window is Monday mornings.
- Which environments would you create and which check would you put on each?
- Would you recommend continuous delivery or continuous deployment? Justify it against the differences with Contoso Bookings.
- Where are those checks configured, and why not in the YAML?
Exercise 2: the migration that broke the rollback
Contoso deploys version 2.6.0, which includes a migration that renames the codigo_reserva column to localizador in db-reservas. Ten minutes later a serious bug appears in the fare calculation and Marta launches the slot re-swap. The website goes back to version 2.5.0 and starts failing with "column does not exist" errors.
- What exactly has happened, and why was the re-swap not enough?
- Rewrite the change using expand and contract, stating what each deployment does.
- From which agent must the migration be run against
sql-contoso-reservas-pro, and with what credential?
Exercise 3: choosing a strategy and working out the rollback
Contoso wants to try out a new pricing algorithm in the Availability API, which runs on vmss-api-disponibilidad-pro behind lb-api-disponibilidad-pro. The risk is high: a pricing error has direct financial impact. The team wants to expose it to a fraction of the traffic first.
- Which deployment strategy fits, and why not pure blue-green?
- How would you combine it with a feature flag?
- If the algorithm fails at 5% of traffic, what is the fastest rollback route and why?
Solutions
Solution 1:
millas-desarrollowith no checks and automatic deployment;millas-produccionwith a branch control check (mainonly) and an approval from the infrastructure lead. The deployment window should exclude Monday mornings, which is their peak. A preproduction environment is optional given the size of the team, although if they use App Service slots the pattern comes almost for free and is worth keeping.- Continuous delivery to begin with, for the same reason as Contoso Bookings: there is still no telemetry or automatic rollback to justify removing the human gate. With less traffic and lower financial impact, however, it is a reasonable candidate for continuous deployment once their automated tests are solid and they have alerting; the criterion is not the size of the team, it is the ability to detect and roll back fast.
- In the Azure Pipelines environment, not in the YAML, because the YAML can be modified by anyone with permission on the repository: a pull request deleting the approval would be enough. Environment checks have their own permissions and are independent of the code.
Solution 2:
- The migration renamed the column, that is, it dropped one and created another. The schema was left incompatible with version 2.5.0, which still queries
codigo_reserva. The re-swap reverts the code, but it does not revert the schema or the data: that is why it was not enough. Rollback only works if every migration is backward compatible. - Deployment 1 (expand): add the
localizadorcolumn as nullable, keepingcodigo_reserva; the code writes to both and reads fromcodigo_reserva. Migration process: backfilllocalizadorfor historical rows. Deployment 2: the code reads fromlocalizadorand carries on writing to both — from here on you can roll back with no harm. Deployment 3 (contract): the code stops writing tocodigo_reservaand, only after confirming that no earlier version is still alive, the column is dropped. - From a self-hosted agent in the
pool-contoso-privadopool, sitting insnet-gestioninsidevnet-contoso-pro, becausesql-contoso-reservas-prois only reachable throughpe-sql-reservasand a Microsoft-hosted agent has no route to it. The credential must be the agent's managed identity with Entra ID permissions on the database, never a SQL administrator password. And the step goes before the swap.
Solution 3:
- Canary. Pure blue-green switches 100% of the traffic at once: if the algorithm miscalculates, every customer sees wrong prices for as long as it takes to notice. Canary limits the exposure to a fraction and lets you compare metrics between the two versions before continuing. On
vmss-api-disponibilidad-proit is implemented by deploying the new version on a subset of instances and weighting the distribution inlb-api-disponibilidad-pro, or by publishing a new instance group and shifting traffic progressively. - With a feature flag by percentage of users: the new code is deployed to all instances but switched off, and switched on for 5% from Azure App Configuration. This is better than an infrastructure canary because the split is controlled without touching the load balancer, it can be segmented by customer type, and every instance runs the same binary, which removes configuration differences.
- Switch the feature flag off: it is instantaneous, requires no deployment, no swap and no infrastructure changes. Only if the failure were outside the flag — in shared code — would you have to fall back on the slot re-swap or on pulling the canary instances out of the load balancer. That is exactly the benefit of separating deployment from release: the fastest rollback route stops being a deployment.
Conclusion
The artifact no longer waits: it travels on its own. You can tell continuous delivery from continuous deployment and you know why Contoso sits in the first with a human gate in front of production, not out of dogma but because it still lacks the telemetry and automatic rollback that would justify removing it. You have created the desarrollo, preproduccion and produccion environments, which provide what a plain deployment step does not: the exact record of which version is where and a place to attach the protections outside the YAML, so that whoever edits the pipeline cannot remove them.
On top of those environments you have put checks that make sense: Marta Ríos' approval with its timeout, the deployment window that turns "let's try not to deploy on Fridays" into a rule that enforces itself, the branch check and the gates that query whether an incident is open before letting anything through. You have written the three-stage contoso-reservas-cd pipeline chained with dependsOn and condition, which consumes the artifact from 05-03 without rebuilding and which in production does not copy files but swaps the preproduccion slot with its prior warm-up against /salud: Contoso's real blue-green, compared in a table against recreate, rolling and canary. You know how to restrict sc-contoso-pro to the minimum role and scope and to authorized pipelines only; you know that every migration must be backward compatible using the expand and contract pattern, because a dropped column breaks rollback right when you need it; and you know that rolling back for real is a re-swap taking seconds, not a redeployment taking minutes — the reason Contoso's time to restore can fall from hours to under a minute. And with feature flags you have separated deploying from releasing, leaving the fastest rollback route of all: a switch.
One loose end has been dragging on since the start of this module. Contoso's four repositories share a booking models library that is copy-pasted into three of them, and there are already three different versions in circulation: when Diego fixes the booking reference validation on the website, the API stays on the old version and the two disagree in production. Pipeline templates solved this for the YAML; it still needs solving for the code. In the next lesson, Azure Artifacts, you will set up the contoso-paquetes feed, publish contoso.reservas.modelos as a versioned package from a pipeline, promote versions between the @local, @prerelease and @release views, and configure the upstream sources that protect Contoso from a public package disappearing and, above all, from the dependency confusion attack.
Azure Course
Module 1: Introduction to Azure
- What Is Azure?
- Service Models, Regions and Availability Zones
- Creating and Setting Up Your Azure Account
- A Tour of the Azure Portal
- Azure Resource Manager: Subscriptions, Resource Groups and Tags
- Azure CLI, PowerShell and Cloud Shell
Module 2: Core Azure Services
- Azure Virtual Machines
- Compute Scaling and High Availability
- Azure App Service
- Azure Storage: Blobs, Files, Queues and Tables
- Azure Networking: Virtual Networks, Subnets and NSGs
- Hybrid Connectivity and Global Delivery
Module 3: Azure Databases
- Choosing the Right Data Service
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Data Analytics: Data Lake, Data Factory and Synapse
Module 4: Security in Azure
- Microsoft Entra ID and Identity Management
- RBAC and Managed Identities
- Azure Key Vault
- DDoS Protection and Web Application Firewall
- Microsoft Defender for Cloud
- Governance and Compliance with Azure Policy
Module 5: Azure DevOps
- Introduction to Azure DevOps
- Azure Repos
- Azure Pipelines: Continuous Integration
- Continuous Deployment with Environments and Approvals
- Azure Artifacts
- Infrastructure as Code with Bicep
Module 6: Advanced Azure Services
- Containers in Azure: Container Registry and Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Messaging and Events: Service Bus, Event Grid and Event Hubs
- Azure AI Services
Module 7: Monitoring and Management
- Azure Monitor: Metrics, Alerts and Dashboards
- Log Analytics and KQL Queries
- Application Insights
- Azure Automation and Runbooks
- Backup and Disaster Recovery
Module 8: Cost Management and Optimization
- Pricing Calculator and Cost Estimation
- Azure Cost Management: Analysis, Budgets and Alerts
- Reservations, Savings Plans and Azure Hybrid Benefit
- Azure Advisor
- Optimization Strategies and FinOps Culture
