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

  1. Continuous delivery versus continuous deployment
  2. Environments: which version is where
  3. Checks and approvals
  4. The multi-stage pipeline
  5. Deployment strategies and App Service slots
  6. The service connection restricted per environment
  7. Database migrations: expand and contract
  8. Post-deployment verification and rollback
  9. Feature flags: deploying is not releasing
  10. Common Mistakes and Tips
  11. Exercises
  12. Conclusion

  1. 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.

  1. 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 table

Contoso creates three: desarrollo (automatic deployment, no gates), preproduccion (automatic, with a branch check) and produccion (human approval, deployment window and branch check).

  1. 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.

  1. 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 verification

Three 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.

  1. 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).

  1. 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-pro has Website Contributor on rg-contoso-reservas-pro. It can deploy and swap slots; it cannot touch rg-contoso-red-pro or 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.

  1. 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.

  1. 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 /salud endpoint must check real dependencies — connectivity to db-reservas, access to kv-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.

  1. 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 /salud endpoint 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.

  1. Which environments would you create and which check would you put on each?
  2. Would you recommend continuous delivery or continuous deployment? Justify it against the differences with Contoso Bookings.
  3. 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.

  1. What exactly has happened, and why was the re-swap not enough?
  2. Rewrite the change using expand and contract, stating what each deployment does.
  3. 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.

  1. Which deployment strategy fits, and why not pure blue-green?
  2. How would you combine it with a feature flag?
  3. If the algorithm fails at 5% of traffic, what is the fastest rollback route and why?

Solutions

Solution 1:

  1. millas-desarrollo with no checks and automatic deployment; millas-produccion with a branch control check (main only) 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.
  2. 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.
  3. 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:

  1. 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.
  2. Deployment 1 (expand): add the localizador column as nullable, keeping codigo_reserva; the code writes to both and reads from codigo_reserva. Migration process: backfill localizador for historical rows. Deployment 2: the code reads from localizador and carries on writing to both — from here on you can roll back with no harm. Deployment 3 (contract): the code stops writing to codigo_reserva and, only after confirming that no earlier version is still alive, the column is dropped.
  3. From a self-hosted agent in the pool-contoso-privado pool, sitting in snet-gestion inside vnet-contoso-pro, because sql-contoso-reservas-pro is only reachable through pe-sql-reservas and 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:

  1. 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-pro it is implemented by deploying the new version on a subset of instances and weighting the distribution in lb-api-disponibilidad-pro, or by publishing a new instance group and shifting traffic progressively.
  2. 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.
  3. 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

Module 2: Core Azure Services

Module 3: Azure Databases

Module 4: Security in Azure

Module 5: Azure DevOps

Module 6: Advanced Azure Services

Module 7: Monitoring and Management

Module 8: Cost Management and Optimization

Module 9: Case Studies and Best Practices

© Copyright 2026. All rights reserved