Domain 3.4 · part of 50–55%

Design and implement deployments

Getting new code in front of users without breaking them: which rollout pattern, how to keep the lights on during the change, how to get back if it goes wrong, and what to do about the database.

Deployment strategies

StrategyDowntimeRollbackInfra costBest for
RecreateYesSlow — redeploy old versionDev/test only
RollingNoMedium — roll back instance by instance1× (+1 surge)Stateless services, the common default
Blue-greenNoInstant — swap back during the releaseCritical apps needing a fast, certain rollback
CanaryNoFast — shift traffic back1× + small canaryHigh-traffic apps where real traffic is the test
Ring / progressive exposureNoFast — stop promotingLarge, segmentable user bases
A/B testingNoFast1× + variantMeasuring a hypothesis, not derisking a release
Feature flagsNoInstant — no redeployDecoupling deploy from release; combines with all of the above

Caveat — canary vs A/B testing

They look identical (both send a subset of traffic to a new version) but the intent differs, and the exam tests exactly that. Canary = risk reduction: you watch technical signals (error rate, latency) and either promote or roll back. A/B testing = experimentation: you compare business metrics (conversion, click-through) between two working variants to decide which is better. If the scenario mentions statistical significance or conversion rate, it is A/B; if it mentions error rate or latency, it is canary.

Blue-green deployment

flowchart TB U[Users] --> R{Router / slot swap} R -->|100% before| B["BLUE · v1.0
live"] R -.->|0% before| G["GREEN · v1.1
deploy & warm up here"] R -.->|100% after swap| G R -.->|0% after swap| B G -->|"problem? swap back instantly"| B
Two identical environments; the router decides which is live. Rollback is another swap.
azure cli blue-green with App Service deployment slots
# 1 · Deploy to the staging slot (green) — production traffic unaffected
az webapp deploy --resource-group myRG --name myApp --slot staging \
  --src-path ./package.zip --type zip

# 2 · Warm up and smoke-test the staging slot
curl -fsS https://myapp-staging.azurewebsites.net/health

# 3 · Swap — Azure warms the target first, so this is near-zero downtime
az webapp deployment slot swap --resource-group myRG --name myApp \
  --slot staging --target-slot production

# 4 · Rollback = swap again (the old version is now sitting in staging)
az webapp deployment slot swap --resource-group myRG --name myApp \
  --slot staging --target-slot production

Caveat — slot settings ("sticky" settings)

By default app settings and connection strings swap with the slot. Mark a setting as a deployment slot setting and it stays with the slot instead — this is how you keep the staging slot pointed at a staging database while production keeps its own. The classic exam symptom: "after the swap, production is writing to the test database" → the connection string was not marked as a slot setting.

Auto swap and swap with preview

Auto swap swaps a slot into production automatically whenever a deployment to it succeeds (good for staging, dangerous for production). Swap with preview (multi-phase) applies production's slot settings to the staging slot first so you can validate against production configuration before completing the swap. "VIP swap" is the legacy Cloud Services equivalent.

Canary deployment

flowchart LR S[Deploy v1.1 to a
small slice] --> A[5% traffic] A --> M{Health OK?
error rate · latency} M -->|Yes| B[25%] B --> C[50%] C --> D[100% · retire v1.0] M -->|No| R[Shift traffic back
to v1.0]
Each promotion step is gated on observed health — automate the gate, not just the traffic shift.

Traffic-splitting mechanisms in Azure:

MechanismSplits by
App Service slot traffic routing (--distribution)Percentage of requests to a slot; users are pinned with a cookie
Azure Front Door / Traffic ManagerWeighted routing across endpoints or regions
Application GatewayWeighted backend pools / URL rules
AKS — replica ratio or a service meshPod count ratio, or precise mesh-level weights (Istio/Linkerd)
Azure Container AppsBuilt-in revision traffic weights
azure-pipelines.yml canary strategy in a deployment job
- deployment: CanaryDeploy
  environment: production
  strategy:
    canary:
      increments: [10, 25, 50]      # then the remainder
      preDeploy:
        steps: [ { script: ./prepare.sh } ]
      deploy:
        steps: [ { script: ./deploy.sh --weight $(strategy.increment) } ]
      routeTraffic:
        steps: [ { script: ./shift-traffic.sh $(strategy.increment) } ]
      postRouteTraffic:
        steps: [ { script: ./check-health.sh } ]   # fail here → on: failure
      on:
        failure:
          steps: [ { script: ./rollback.sh } ]

Ring deployment / progressive exposure

RingAudiencePurpose
Ring 0 — canary/dogfoodThe engineering team itselfCatch the obvious before anyone else sees it
Ring 1 — early adoptersInternal org / opt-in preview users (~1%)Real usage, tolerant audience
Ring 2 — broad internal / limited external~10%Scale and diversity of configuration
Ring 3+ — general availabilityEveryoneFull rollout

Caveat — canary vs ring

Canary splits by traffic percentage and is usually short-lived (minutes to hours). Ring splits by audience cohort and each ring may bake for days. "Roll out to internal users first, then preview customers, then everyone" is a ring deployment (progressive exposure). Microsoft uses rings for Windows, Microsoft 365 and Azure itself.

Rolling deployment

azure-pipelines.yml rolling strategy
- deployment: RollingDeploy
  environment: vm-farm
  strategy:
    rolling:
      maxParallel: 25%          # update a quarter of targets at a time
      preDeploy:
        steps: [ { script: ./drain-from-load-balancer.sh } ]
      deploy:
        steps: [ { script: ./install.sh } ]
      routeTraffic:
        steps: [ { script: ./add-back-to-load-balancer.sh } ]
      postRouteTraffic:
        steps: [ { script: ./verify.sh } ]
      on:
        failure:
          steps: [ { script: ./rollback.sh } ]

Rolling means two versions run simultaneously

During a rolling (or canary, or blue-green with a shared database) deployment, v1 and v2 are both live at once. Both versions must therefore tolerate the same database schema and the same message formats — which is exactly why the expand–contract pattern exists.

Feature flags with Azure App Configuration

Feature flags decouple deployment from release: code ships dark and is switched on later, without a redeploy. This is what makes trunk-based development safe.

Flag typeLifetimeExample
Release toggleDays–weeks (remove after launch)Hide an unfinished checkout redesign
Experiment toggleDuration of the experimentA/B test two onboarding flows
Ops toggleLong-livedKill switch / circuit breaker for an expensive feature
Permission togglePermanentPremium-tier functionality
Feature filterEnables the flag for
TargetingFilterNamed users, named groups, or a rollout percentage per group — the ring/progressive-exposure filter
TimeWindowFilterA start/end date range — scheduled launches
PercentageFilterA random X% of requests (not sticky per user)
Custom filterAny logic you implement (region, tenant, device)
C# Azure App Configuration feature manager
// Program.cs — connect with a managed identity, no connection string
builder.Configuration.AddAzureAppConfiguration(options =>
{
    options.Connect(new Uri(builder.Configuration["AppConfig:Endpoint"]!),
                    new DefaultAzureCredential())
           .UseFeatureFlags(ff => ff.CacheExpirationInterval = TimeSpan.FromMinutes(5));
});
builder.Services.AddAzureAppConfiguration();
builder.Services.AddFeatureManagement().AddFeatureFilter<TargetingFilter>();

// Usage
public async Task<IActionResult> Index()
{
    if (await _featureManager.IsEnabledAsync("NewDashboard"))
        return View("NewDashboard");
    return View("Dashboard");
}

Caveat — flags belong outside the artifact

The whole point is changing behaviour without redeploying, so flag state must live in a runtime configuration store (Azure App Configuration), not in appsettings.json baked into the build. Note the cache expiration: a flag change takes effect after the refresh interval, not instantly. Also budget for flag debt — release toggles must be removed once the feature is fully rolled out.

Ordering dependent deployments

azure-pipelines.yml reliable ordering across stages
stages:
  - stage: Infrastructure
    jobs: [ { job: Bicep, steps: [ { script: az deployment group create ... } ] } ]

  - stage: Database
    dependsOn: Infrastructure
    condition: succeeded('Infrastructure')
    jobs: [ { job: Migrate, steps: [ { script: ./migrate.sh } ] } ]

  - stage: Api
    dependsOn: Database                # API depends on the new schema
    jobs: [ { deployment: DeployApi, environment: prod, strategy: { runOnce: { deploy: { steps: [ { script: ./deploy-api.sh } ] } } } } ]

  - stage: Web
    dependsOn: Api                     # front end depends on the API
    jobs: [ { deployment: DeployWeb, environment: prod, strategy: { runOnce: { deploy: { steps: [ { script: ./deploy-web.sh } ] } } } } ]

Ordering rules of thumb

Deploy in dependency order: infrastructure → database (expand) → back-end services → front end, and remove in reverse. Where services are independently deployable, prefer backwards-compatible contracts over strict ordering — coupling deployments together reduces your ability to release any one of them.

Hotfix path plan

gitGraph commit id: "v1.5.0" tag: "v1.5.0" branch main-dev commit id: "v2 work" checkout main branch hotfix/payment commit id: "urgent fix" checkout main merge hotfix/payment tag: "v1.5.1" checkout main-dev merge hotfix/payment id: "port fix forward"
Branch from the released tag, fix, ship as a patch, then port the fix forward so v2 does not regress.
  1. Branch from the deployed tag (not from main) — main contains unreleased work you do not want in a hotfix.
  2. Make the minimal fix and open a PR with expedited review (fewer required reviewers is a legitimate policy exception for hotfix branches).
  3. Run a shortened pipeline — smoke and regression tests only, skipping long suites.
  4. Tag a patch version (v1.5.1) and deploy, ideally via slot swap for instant rollback.
  5. Merge or cherry-pick the fix back to main — non-negotiable, or the bug returns in the next release.
  6. Post-incident: add a regression test that would have caught it.
azure-pipelines.yml hotfix fast path
trigger:
  branches: { include: ['hotfix/*'] }

steps:
  - script: dotnet test --filter "Category=Smoke|Category=Regression"
  - task: AzureWebApp@1
    condition: succeeded()
    inputs:
      appName: myApp
      deployToSlotOrASE: true
      slotName: staging          # then swap — rollback stays instant
      package: '$(Build.ArtifactStagingDirectory)/**/*.zip'

Resiliency and rollback

PracticeWhat it buys you
Health endpoint (/health, /ready)A single truth the load balancer, orchestrator and pipeline can all query
Readiness vs liveness probesReadiness gates traffic; liveness triggers a restart — confusing them causes restart loops
Connection drainingIn-flight requests finish before an instance is removed
Retry with exponential backoff + jitterSurvives transient faults without creating a thundering herd
Circuit breakerStops hammering a failing dependency; fails fast instead
Automated rollback triggerPost-deployment gate on error rate/latency → on: failure hook
Multi-region + availability zonesSurvives datacentre and regional failure
Idempotent deployment scriptsSafe to re-run after a partial failure
flowchart LR D[Deploy] --> W[Bake / observe
15 min window] W --> Q{Azure Monitor:
alerts fired?} Q -->|No| OK[Complete the release] Q -->|Yes| RB[Automatic rollback
swap slots back / redeploy previous]
A post-deployment gate turns "someone noticed and rolled back" into an automated control.

Caveat — rollback vs roll forward

Roll back when the previous version is known-good and the change is reversible — slot swap is the fastest route. Roll forward when rollback is impossible or unsafe, most often because a database migration cannot be undone. This is precisely why migrations should be backwards-compatible: it preserves the option to roll back.

Deploying containers, binaries and scripts

azure-pipelines.yml container → AKS
- task: Docker@2
  inputs:
    containerRegistry: 'acr-connection'
    repository: 'myapp'
    command: buildAndPush
    Dockerfile: '**/Dockerfile'
    tags: |
      $(Build.BuildId)
      latest

- task: KubernetesManifest@1
  inputs:
    action: deploy
    connectionType: kubernetesServiceConnection
    kubernetesServiceConnection: 'aks-connection'
    namespace: production
    manifests: |
      k8s/deployment.yaml
      k8s/service.yaml
    containers: 'myregistry.azurecr.io/myapp:$(Build.BuildId)'
GitHub Actions container → AKS with OIDC
permissions:
  id-token: write        # required for OIDC
  contents: read

steps:
  - uses: azure/login@v2
    with:
      client-id:       ${{ secrets.AZURE_CLIENT_ID }}
      tenant-id:       ${{ secrets.AZURE_TENANT_ID }}
      subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

  - run: |
      az acr login --name myregistry
      docker build -t myregistry.azurecr.io/myapp:${{ github.sha }} .
      docker push myregistry.azurecr.io/myapp:${{ github.sha }}

  - uses: azure/aks-set-context@v4
    with: { resource-group: myRG, cluster-name: myAKS }

  - uses: azure/k8s-deploy@v5
    with:
      namespace: production
      manifests: ./k8s/
      images: myregistry.azurecr.io/myapp:${{ github.sha }}
      strategy: canary
      percentage: 20

Tag images with the commit SHA, never only latest

latest is mutable, so a rollback or a re-run can silently pull different bits. Tag with $(Build.BuildId) or github.sha so every deployment is reproducible and traceable — the same "build once, deploy many" principle from 3.1.

Deployments that include database tasks

Databases are stateful and hard to roll back — the reason most deployment questions with a database attached resolve to "make the change backwards compatible".

The expand–contract (parallel change) pattern

flowchart TB E["1 · EXPAND
add the new column/table
nullable, with a default"] --> M["2 · MIGRATE
deploy app that writes BOTH
old and new; backfill data"] M --> R["3 · READ
deploy app that reads
the new shape"] R --> C["4 · CONTRACT
drop the old column
once nothing references it"]
Each step is independently deployable and reversible — at no point do the app and schema become incompatible.

Caveat — never rename a column in one release

A rename breaks the running version the instant it applies, which is fatal under rolling, canary or blue-green (where both versions run at once). The expected answer is expand–contract: add the new column, dual-write, backfill, switch reads, then drop the old column in a later release.

Database deployment tooling

ApproachModelTooling
State-basedDeclare the desired schema; the tool computes the diffDACPAC (SqlPackage / SqlAzureDacpacDeployment@1)
Migration-basedOrdered, versioned scripts applied in sequenceEF Core migrations, Flyway, Liquibase, DbUp
azure-pipelines.yml DACPAC and EF Core migrations
# State-based: DACPAC publish
- task: SqlAzureDacpacDeployment@1
  inputs:
    azureSubscription: 'MyServiceConnection'
    AuthenticationType: 'servicePrincipal'
    ServerName: 'myserver.database.windows.net'
    DatabaseName: 'mydb'
    deployType: 'DacpacTask'
    DeploymentAction: 'Publish'
    DacpacFile: '$(Pipeline.Workspace)/drop/MyApp.dacpac'
    # Leave BlockOnPossibleDataLoss at its default (true) in production!
    AdditionalArguments: '/p:BlockOnPossibleDataLoss=true'

# Migration-based: generate an idempotent script, review it, then apply
- script: |
    dotnet ef migrations script --idempotent \
      --project src/MyApp.Data --startup-project src/MyApp.Api \
      --output $(Build.ArtifactStagingDirectory)/migrate.sql
  displayName: Generate migration script

Caveat — BlockOnPossibleDataLoss

Setting /p:BlockOnPossibleDataLoss=false lets the deployment silently drop columns and data. It is occasionally necessary, but if a question offers it as the fix for "my deployment failed with a data-loss warning" in a production context, the safe answer is to rewrite the change as expand–contract, not to disable the guard.

Practices that make database deployments safe

Generate an idempotent script so re-runs are harmless; review the generated SQL as a pipeline artifact before an approval gate; take a backup or restore point immediately before applying; run migrations in their own stage with dependsOn so the app never deploys against an unmigrated schema; and use a deployment job with an environment so the change is approved and recorded.

3.4 rapid recap

The requirement says…Answer
Zero downtime with instant, certain rollbackBlue-green via App Service slot swap
After the swap, prod points at the test databaseMark the connection string as a deployment slot setting
Validate against production config before swappingSwap with preview (multi-phase swap)
Expose to 5% of traffic and watch error rateCanary
Compare conversion between two designsA/B testing
Internal users → preview customers → everyoneRing deployment / progressive exposure
Update 25% of VMs at a timeRolling strategy, maxParallel: 25%
Merge unfinished work to main safelyFeature flags (Azure App Configuration)
Turn a feature on for one customer groupTargetingFilter
Schedule a feature to go live at midnightTimeWindowFilter
Instantly disable a misbehaving featureOps toggle / kill switch — no redeploy
DB schema must change before the app deploysSeparate migration stage with dependsOn
Rename a column with zero downtimeExpand–contract across releases
Automatically undo a bad releasePost-deployment Azure Monitor gate + on: failure hook
Critical production bug, fast pathHotfix branch from the tag → short pipeline → tag patch → port forward
Make a rollback reproducibleTag images with the commit SHA, not latest