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
| Strategy | Downtime | Rollback | Infra cost | Best for |
|---|---|---|---|---|
| Recreate | Yes | Slow — redeploy old version | 1× | Dev/test only |
| Rolling | No | Medium — roll back instance by instance | 1× (+1 surge) | Stateless services, the common default |
| Blue-green | No | Instant — swap back | 2× during the release | Critical apps needing a fast, certain rollback |
| Canary | No | Fast — shift traffic back | 1× + small canary | High-traffic apps where real traffic is the test |
| Ring / progressive exposure | No | Fast — stop promoting | 1× | Large, segmentable user bases |
| A/B testing | No | Fast | 1× + variant | Measuring a hypothesis, not derisking a release |
| Feature flags | No | Instant — no redeploy | 1× | Decoupling 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
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
# 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
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]
Traffic-splitting mechanisms in Azure:
| Mechanism | Splits by |
|---|---|
App Service slot traffic routing (--distribution) | Percentage of requests to a slot; users are pinned with a cookie |
| Azure Front Door / Traffic Manager | Weighted routing across endpoints or regions |
| Application Gateway | Weighted backend pools / URL rules |
| AKS — replica ratio or a service mesh | Pod count ratio, or precise mesh-level weights (Istio/Linkerd) |
| Azure Container Apps | Built-in revision traffic weights |
- 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
| Ring | Audience | Purpose |
|---|---|---|
| Ring 0 — canary/dogfood | The engineering team itself | Catch the obvious before anyone else sees it |
| Ring 1 — early adopters | Internal 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 availability | Everyone | Full 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
- 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 type | Lifetime | Example |
|---|---|---|
| Release toggle | Days–weeks (remove after launch) | Hide an unfinished checkout redesign |
| Experiment toggle | Duration of the experiment | A/B test two onboarding flows |
| Ops toggle | Long-lived | Kill switch / circuit breaker for an expensive feature |
| Permission toggle | Permanent | Premium-tier functionality |
| Feature filter | Enables the flag for |
|---|---|
TargetingFilter | Named users, named groups, or a rollout percentage per group — the ring/progressive-exposure filter |
TimeWindowFilter | A start/end date range — scheduled launches |
PercentageFilter | A random X% of requests (not sticky per user) |
| Custom filter | Any logic you implement (region, tenant, device) |
// 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
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
- Branch from the deployed tag (not from
main) —maincontains unreleased work you do not want in a hotfix. - Make the minimal fix and open a PR with expedited review (fewer required reviewers is a legitimate policy exception for hotfix branches).
- Run a shortened pipeline — smoke and regression tests only, skipping long suites.
- Tag a patch version (
v1.5.1) and deploy, ideally via slot swap for instant rollback. - Merge or cherry-pick the fix back to
main— non-negotiable, or the bug returns in the next release. - Post-incident: add a regression test that would have caught it.
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
| Practice | What it buys you |
|---|---|
Health endpoint (/health, /ready) | A single truth the load balancer, orchestrator and pipeline can all query |
| Readiness vs liveness probes | Readiness gates traffic; liveness triggers a restart — confusing them causes restart loops |
| Connection draining | In-flight requests finish before an instance is removed |
| Retry with exponential backoff + jitter | Survives transient faults without creating a thundering herd |
| Circuit breaker | Stops hammering a failing dependency; fails fast instead |
| Automated rollback trigger | Post-deployment gate on error rate/latency → on: failure hook |
| Multi-region + availability zones | Survives datacentre and regional failure |
| Idempotent deployment scripts | Safe to re-run after a partial failure |
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]
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
- 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)'
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
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"]
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
| Approach | Model | Tooling |
|---|---|---|
| State-based | Declare the desired schema; the tool computes the diff | DACPAC (SqlPackage / SqlAzureDacpacDeployment@1) |
| Migration-based | Ordered, versioned scripts applied in sequence | EF Core migrations, Flyway, Liquibase, DbUp |
# 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 rollback | Blue-green via App Service slot swap |
| After the swap, prod points at the test database | Mark the connection string as a deployment slot setting |
| Validate against production config before swapping | Swap with preview (multi-phase swap) |
| Expose to 5% of traffic and watch error rate | Canary |
| Compare conversion between two designs | A/B testing |
| Internal users → preview customers → everyone | Ring deployment / progressive exposure |
| Update 25% of VMs at a time | Rolling strategy, maxParallel: 25% |
| Merge unfinished work to main safely | Feature flags (Azure App Configuration) |
| Turn a feature on for one customer group | TargetingFilter |
| Schedule a feature to go live at midnight | TimeWindowFilter |
| Instantly disable a misbehaving feature | Ops toggle / kill switch — no redeploy |
| DB schema must change before the app deploys | Separate migration stage with dependsOn |
| Rename a column with zero downtime | Expand–contract across releases |
| Automatically undo a bad release | Post-deployment Azure Monitor gate + on: failure hook |
| Critical production bug, fast path | Hotfix branch from the tag → short pipeline → tag patch → port forward |
| Make a rollback reproducible | Tag images with the commit SHA, not latest |