MS Stack Ch 13 — Azure App Service
App Service plans, deployment slots + swap + warmup, app settings, Key Vault references, VNet integration, scale-out rules, custom domains + TLS. Version-bounded hosting and deployment patterns, with explicit networking and execution limits.
Published Updated
Lesson preparation & details
Level: intermediate
By the end, you should be able to
- Design plan, runtime, slot and readiness boundaries
- Explain swap settings, identities, warmup and rollback compatibility
- Separate outbound networking, private ingress, DNS and data permissions
- Plan autoscale, domain ownership, TLS and diagnostics without false guarantees
Bring with you
Sources checked: . A source check does not change the publication or update date.
Editorial review: · What review means
In this article · 34 sections
Platform and example contract
Bicep fragments use Microsoft.Web 2023-12-01, autoscale 2022-10-01 and diagnostic settings 2021-05-01-preview. Runtime examples use .NET 8 LTS; a stack selector chooses a supported runtime family, not an immutable patch. As checked on 9 October 2026, .NET 8 is in maintenance and its support ends 10 November 2026. Keep these examples as a versioned learning contract, not a recommendation to start a long-lived deployment on a nearly end-of-support runtime; plan and test an upgrade to an App Service-supported runtime before that date. Confirm supported OS/SKU/region/runtime and current support lifetime before a new deployment. Service behavior was checked against Microsoft documentation on 7 October 2026. These are deployment-design examples: no subscription, role assignment, domain, service reproduction or .NET/Bicep compilation was performed.
Chapter 13 of From Novice to Fluent on the Modern Microsoft Web Stack — a 22-chapter self-study plan.
Why this chapter
Azure App Service is a managed hosting option for ASP.NET, Node, Python, Java and PHP applications; this chapter makes no claim about its share of Microsoft or customer workloads. It abstracts the OS, patches itself on a maintenance cadence, terminates TLS at the edge, integrates with Entra identity and VNet networking, and gives you a deployment-slot primitive that — used well — supports a controlled cutover after warmup and compatibility checks. Used badly, it gives you cold-start outages, secrets in plain text, and a Friday-evening pager call when autoscale fights itself.
Shipping-level App Service means: provisioning a plan, deploying an app, configuring app settings, and turning HTTPS on. Expert-level App Service means: writing the Bicep, picking the right tier for the actual workload shape, configuring warmup paths that gate the slot swap, using credential-free identity where supported and sourcing necessary secrets from Key Vault, getting VNet integration and private endpoints to talk to each other correctly, and tuning autoscale so it neither flaps nor over-provisions.
You finish the conceptual chapter when you can design and explain the plan, app, slots, identity, Key Vault wiring, VNet integration, autoscale, health checks and custom-domain TLS boundaries. Standing up a production system additionally requires authorized deployment, security and reliability validation; none is certified by these fragments.
Pick the right plan — Match SKU to actual workload shape — CPU, RAM, slot count, network needs.
Master slots — Deploy to staging, validate target-settings warmup, swap; swap-back is a code rollback only when external state remains compatible.
Secret hygiene — Prefer credential-free managed identity; resolve necessary secrets through @Microsoft.KeyVault(...) with narrow permissions.
Network correctly — Outbound VNet integration + inbound private endpoints + correct DNS.
Autoscale safely — Cooldowns, schedule profiles, scale-out over scale-up for stateless work.
Operate confidently — Log Stream, persistent diagnostics, KQL on platform logs.
Concepts and depth
App Service Plan vs Web App
The App Service Plan is the compute — a VM-sized billing unit with a SKU (B1, S1, P1v3, etc.), an OS (Linux or Windows), a region, and a worker count. The Web App is the deployment surface — a URL (https://<name>.azurewebsites.net), a sandbox, a set of slots and app settings, and a binding to a Plan. One Plan can host many apps; all apps on the Plan share the same VMs, the same RAM, and the same CPU pool. If your traffic patterns differ wildly across apps (one is a chatty API, another is a nightly batch), they can contend on a shared Plan; measure combined peaks and reserve capacity.
A common production layout is one Plan per service tier (a Premium v3 plan for production-traffic apps, a Standard plan for dev/test) with one Web App per logical service. Sharing a plan across production apps is reasonable when combined peak demand fits measured headroom and the correlated-failure risk is acceptable; complementary load profiles help but are not a necessary condition. The noisy-neighbour blast radius includes every colocated app and slot. With per-app scaling enabled, an app can use a subset of plan workers, so plan capacity is not automatically that app's active replica count. Deployment slots require a supported Standard, Premium or Isolated tier; Basic is not a slot-capable substitute merely because it supports a web app.
resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: 'plan-${env}'
location: location
sku: { name: 'P1v3'
tier: 'PremiumV3'
capacity: 2 }
kind: 'linux'
properties: { reserved: true } // 'reserved=true' is the magic word for Linux
}
resource api 'Microsoft.Web/sites@2023-12-01' = {
name: 'api-${env}'
location: location
identity: { type: 'SystemAssigned' }
properties: {
serverFarmId: plan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'DOTNETCORE|8.0'
alwaysOn: true
healthCheckPath: '/health/ready'
minTlsVersion: '1.2'
}
}
}Good enough to ship
• One Plan per environment tier.
• One Web App per logical service.
• httpsOnly: true, alwaysOn: true, minTlsVersion: 1.2.
Expert tier
• Co-locate apps with complementary load on one Plan deliberately. • Multi-region Plan layout fronted by Front Door. • Explicitly enabled zone redundancy on a supported plan/region for zone failures; use multi-region recovery for a regional failure.
Linux vs Windows runtime; framework-dependent vs self-contained
App Service offers two host OSs. Linux suits many cross-platform/container workloads, but footprint and cold start depend on the application and hosting mode, not an OS ranking. Windows is the right choice when you need .NET Framework (not Core), WCF, full IIS modules, or in-process hosting features that Linux containers cannot replicate. Choose Linux when its runtime and operational constraints fit; benchmark cold start rather than assuming it is faster.
For .NET workloads, choose between framework-dependent publishes (the runtime is provided by the platform image; your package ships application binaries, assets and dependencies rather than the runtime) and self-contained publishes (you ship the runtime too). Framework-dependent usually ships a smaller application payload and lets the platform supply and patch its supported runtime; deployment speed still depends on package and pipeline behavior. Self-contained ships the runtime selected at publish time and shifts security republishing to you. Use it only when OS/architecture and App Service hosting support have been checked; it does not extend .NET support or make an unsupported runtime safe.
# Framework-dependent — recommended default
dotnet publish -c Release -r linux-x64 --self-contained false -o ./out
# Self-contained — when you must pin the runtime
dotnet publish -c Release -r linux-x64 --self-contained true -o ./outGood enough to ship
• Linux + framework-dependent when the workload, supported runtime and native dependencies fit.
• Select the runtime family via linuxFxVersion: 'DOTNETCORE|8.0'; this does not pin a patch.
• Regression-test platform runtime updates; self-contained deployments need your own runtime security republishing.
Expert tier
• Self-contained only with a supported OS/runtime combination and an owned patch-and-republish process. • Custom container when you need OS packages outside the base image. • Windows + WebDeploy when you genuinely need full IIS.
Application Settings as environment variables
Anything you put in Configuration → Application Settings is exposed to the running app as an environment variable. ASP.NET Core's environment-variable configuration provider picks them up automatically; nested keys use __ as the separator (configure __ yourself for Linux; do not rely on automatic name translation). So Db:Conn in appsettings.json becomes Db__Conn in App Settings.
[
{ "name": "ASPNETCORE_ENVIRONMENT", "value": "Production", "slotSetting": true },
{ "name": "Db__Conn", "value": "@Microsoft.KeyVault(SecretUri=https://kv.vault.azure.net/secrets/db-conn/abc123)", "slotSetting": true },
{ "name": "Jwt__Authority", "value": "https://login.microsoftonline.com/<tenant>/v2.0", "slotSetting": false },
{ "name": "WEBSITE_RUN_FROM_PACKAGE", "value": "1", "slotSetting": false }
]The Configuration blade has two tabs: Application Settings and Connection Strings. Connection Strings also integrate with ASP.NET Core through provider-specific environment prefixes. Select the connection type correctly and read via GetConnectionString; alternatively use explicit nested app settings. Prefer credential-free service authentication over storing a database password where supported.
WEBSITE_* system environment variables
App Service injects a family of platform-prefixed environment variables. The useful ones:
WEBSITE_HOSTNAME— your default hostname (e.g.api-prod.azurewebsites.net).WEBSITE_SITE_NAME— the Web App name.WEBSITE_INSTANCE_ID— the worker instance ID; log it for per-instance correlation.WEBSITE_RESOURCE_GROUP— useful for emergency self-diagnostics.WEBSITE_RUN_FROM_PACKAGE=1— switch from regular zip-deploy to read-only ZIP-mount.WEBSITE_VNET_ROUTE_ALL=1— route outbound application traffic (not just RFC1918) through VNet integration.WEBSITE_TIME_ZONE— changes the app timezone where supported; the documented default is UTC and unrecognized values fall back to UTC. Confirm the accepted value and runtime/container behavior for the selected host instead of assuming a portable OS-wide setting. Keep persisted event timestamps in explicit UTC.WEBSITES_PORT— legacy custom-container routing port setting, not a command to make the app listen or pre-pull its image; sidecar-enabled container configurations use their own port settings.
Use these for instance-level logging context and to control platform behaviour. Routing all application traffic through the VNet can also break public egress if DNS, NSGs, routes or firewall policy block it. Diagnose name resolution, route and port separately; configuration traffic such as image pulls/backups has separate routing controls.
"Always On", health-check path, instance count
With Always On disabled, an idle app can be unloaded after 20 minutes. Always On sends a root request every five minutes to keep it loaded on supporting paid tiers; it does not prevent all restarts, deployment cold starts or dependency failures. There is no per-toggle charge, but its supporting plan costs money.
Health Check probes each instance every minute. By default, ten unsuccessful probes (non-200–299) mark an instance unhealthy; WEBSITE_HEALTHCHECK_MAXPINGFAILURES configures the threshold from 2 to 10. An unhealthy instance that recovers can rejoin; continuous unhealthiness for one hour can trigger replacement, subject to platform limits. This is not “restart after ten minutes”. The platform limits how many unhealthy instances it removes; with only one active instance it does not remove the sole serving instance merely to leave zero capacity.
This load-routing probe should reflect whether the instance can serve useful traffic, normally a bounded readiness check of critical components. Keep process-only liveness separate. A shared database outage may make all instances unready, so reason about removal limits, alerting and recovery rather than restarting endlessly. Health Check does not follow login/302 redirects. Use a minimal non-secret response and a route that works under your authentication/network configuration; App Service-generated health calls have documented platform handling, not a blanket anonymous diagnostic surface.
Instance count is the worker count on the Plan, not necessarily the active replica count of an app with per-app scaling enabled. Plan for at least two active app replicas with enough surviving capacity for the failure you intend to tolerate; neither two nor three is a universal HA certificate. Check zone placement, shared dependencies and load under an instance loss.
Good enough to ship
• Always On = true on every prod app.
• Health Check path = /health/ready, with bounded critical-component checks.
• Minimum 2 active app replicas, surviving capacity and checked placement; plan worker count alone is insufficient.
Expert tier
• Measure startup phases and use OS-specific settings only when their documented purpose matches the failure. • Combine Health Check with custom autoscale triggers. • Enable zone redundancy explicitly on a supported plan/region, keep applicable autoscale capacities and active app replicas at least two, and test dependency recovery. Premium v3 alone does not enable it.
Cold start, warmup, idle eviction
Cold start includes worker/container startup, runtime initialization, JIT, DI construction and application warmup. Measure these phases for your package; the old fixed Linux/Windows second ranges were not benchmarks. Always On avoids idle unloading but new workers and new code still start cold.
The mitigations stack:
- Always On — eliminate idle eviction in production.
- Slot warmup — require meaningful success before routing candidate code; a root-page 200 alone proves little.
- Readiness probe — return
503until app-internal warmup (DB pool, cache prefetch) is complete. - Pre-JIT (ReadyToRun) at publish —
<PublishReadyToRun>true</PublishReadyToRun>in csproj; trades publish size for faster startup. - Container startup — reduce image size and startup work, configure the correct listening/target port and supported warmup path/statuses. A port setting does not pre-pull an image.
Deployment slots and slot swap
A slot is a separate sandbox on the same Plan with its own URL (https://<app>-staging.azurewebsites.net), its own app settings, and its own deployment. The swap is the killer feature: deploy to staging, warm it up, swap — production is routed to warmed candidate instances after the platform swap sequence, and the staging slot holds the previous production code (so compatible code can be swapped back).
Follow the four cards as a release sequence, with the production endpoint continuing to serve the old version during deployment and warmup. Warmup is the decision point: the candidate must be ready before traffic moves. The final card preserves a code rollback option, not a snapshot of every dependency; swapping back cannot undo a database migration or other external side effect.
Step 1 Deploy to staging build + zip-deploy
Step 2
Warmup
ping /health/ready
Step 3 Swap change routing after target-settings warmup
Step 4 Rollback ready previous code now on staging
The sequence reduces cutover risk only when readiness, configuration and external-state compatibility are correct; it is not a guarantee against every release failure.
The swap card hides a second concern: configuration has to reach the right destination along with the code. Most app settings swap, while settings tagged with "Deployment slot setting" stay pinned to the slot; some platform settings also have special swap behaviour. Decide which values describe the release and which describe the environment before relying on the rollback card. The minimum pin list:
ASPNETCORE_ENVIRONMENT— final slot configuration stays environment-specific, but source instances temporarily receive target configuration during swap warmup. Do not treat this label as an isolation boundary.- Slot-specific connection strings (a staging-only DB).
- Slot-specific feature flag overrides.
- App Insights connection string if you separate telemetry by slot.
For Linux and supported swap scenarios, set explicit warmup controls on the source slot:
{
"WEBSITE_SWAP_WARMUP_PING_PATH": "/health/ready",
"WEBSITE_SWAP_WARMUP_PING_STATUSES": "200"
}Without a success-status restriction, warmup can treat other HTTP responses as sufficient. Windows IIS additionally supports applicationInitialization in web.config; it is not a Linux configuration recipe. The swap first applies relevant target configuration to the source, restarts/warms it, then changes routing and completes the other slot's settings. This is why a pre-swap curl against staging is useful but not the entire swap health contract.
Failure scenario: an environment label is not permission isolation. Target-settings warmup intentionally applies production configuration to candidate instances in the source slot, while managed identities do not swap. If the production Key Vault reference is inaccessible to that source identity, the health gate can fail even though ordinary staging is healthy. Define the intended warmup identity/access model explicitly: grant only the target dependency access genuinely required by that deployment design, or redesign the release path rather than granting broad roles. Final production must also pass with its own identity. Never run destructive migrations or background production jobs merely because the transient environment name says Production. Expand/contract schema changes must support both versions during swap and rollback.
Good enough to ship
• Always deploy to staging first, then swap.
• Warmup paths configured.
• ASPNETCORE_ENVIRONMENT pinned as slot-specific.
Expert tier
• Swap-with-preview for two-phase manual cutover. • Explicit swap with health gates on Linux; auto-swap is not supported for Linux web apps or Web App for Containers. • Slot traffic routing for canaries (% of requests to staging).
Log stream and persistent log storage
The Log Stream tails enabled console/file logs; buffering can affect ordering. Enable the relevant log type first. Use the portal, or the CLI example below where supported (Microsoft warns that this command might not work for Linux App Service plans):
az webapp log tail --name api-prod --resource-group rg-prodLog Stream is ephemeral — when the worker recycles, the stream resets. For durable retention, enable App Service Diagnostic Settings and send platform logs to a Log Analytics workspace or a Storage Account. The relevant log categories:
AppServiceHTTPLogs— collected HTTP request records where that category is enabled; do not assume complete delivery or application trace correlation.AppServiceConsoleLogs— your stdout/stderr.AppServiceAuditLogs— app publishing/authentication access events where supported; Azure Activity Log is the resource management-plane change record.AppServiceFileAuditLogs— file changes via Kudu or deployments.AppServicePlatformLogs— platform/container operations where supported; autoscale evaluations have their own monitoring records.
Query them with KQL (chapter 12) in Log Analytics. Application Insights (chapter 15) supplies instrumented requests, exceptions and dependency correlation. Platform HTTP logs can also support request/error counts, and applications can write exceptions to console logs; these sources are complementary, not mutually exclusive.
Key Vault references for secrets in App Settings
The reference syntax is:
@Microsoft.KeyVault(SecretUri=https://<vault>.vault.azure.net/secrets/<secret>/<version>)
@Microsoft.KeyVault(VaultName=<vault>;SecretName=<secret>)
At app start, App Service uses the app's managed identity to fetch the secret from Key Vault and substitute the value into the environment variable. Versionless references are cached and refetched within 24 hours. Configuration changes cause restart and immediate refetch; the documented authenticated refresh endpoint is another option. Do not treat rotation as instantaneous. The Configuration blade shows a green check next to a resolved reference and a red cross next to a broken one — always verify after wiring.
The prerequisites:
- The app has a system-assigned or user-assigned managed identity.
- The identity has the Key Vault Secrets User role on the vault (RBAC mode) or a
getsecret permission (legacy access-policy mode). - The vault allows the call: an explicitly allowed network path, or a private endpoint with VNet integration and correct DNS. The Key Vault trusted-services checkbox does not generally authorize arbitrary App Service code.
- The secret URI is correct. Versionless references adopt newer secret versions within the documented refresh/cache interval; a versioned URI continues to select that version. Pinning a version does not guarantee availability: deletion, disabling, permission changes or network failure can still break resolution.
// In the app, just read it as a normal env var
var dbConn = builder.Configuration["Db:Conn"];
// Or via strongly-typed options
builder.Services.Configure<DbOptions>(builder.Configuration.GetSection("Db"));Prefer Key Vault references to direct secret settings for centralized access and rotation. App settings are encrypted at rest, but principals with configuration-list permissions and the running app can obtain values. Azure Reader alone does not have that permission; do not claim every setting value appears in the Activity Log. Kudu, process dumps and logs remain sensitive regardless of where the original secret was stored.
Good enough to ship
• Avoid stored credentials where identity-based authentication works; put necessary app-setting secrets behind Key Vault references.
• Managed identity granted Key Vault Secrets User via RBAC.
• Verify the green check in Configuration after wiring.
Expert tier
• Versionless URIs + secret rotation policies for auto-rotation. • Plan vault/service regional recovery and replication limitations explicitly; do not assume a customer-configurable secret replica is present. • Validate reference-resolution status and overlapping credentials during rotation; do not expose a secret-fetch debug endpoint.
Networking: access restrictions, private endpoints, VNet integration
App Service has three networking knobs that get conflated:
- Access restrictions — public-internet allow/deny IP rules and service tag rules. The front door for "only Front Door can reach me", "only my corporate IP block can reach the staging slot", etc.
- Private endpoints — give the inbound side of the app a private IP in your VNet. Disable public network access separately when private-only ingress is required; a private endpoint alone does not disable it. Slots require their own endpoint configuration. App access-restriction rules are not evaluated on private-endpoint traffic, so keep application authorization and the private-network controls.
- VNet integration — give the outbound side of the app a foothold in your VNet so it can reach private resources (Storage with private endpoint, SQL with private endpoint, Key Vault with private endpoint).
The following is a public edge with a private origin, not a fully private client-facing API. Front Door Premium accepts public client traffic and reaches the approved App Service origin through Private Link. For private-only client access, omit the public edge and use private connectivity plus private DNS to the app. The two arrows below have different directions and owners:
Use different subnets for App Service VNet integration and its inbound private endpoint. Configure and test the app and SCM DNS names needed by private deployment clients; private data-plane access does not automatically give a hosted CI runner access.
Set the supported route-all configuration (legacy WEBSITE_VNET_ROUTE_ALL=1) to route application outbound traffic through the VNet — required when your VNet egresses via a firewall or NAT that adds an allowlisted public IP.
Diagnostics blade and metrics
The Diagnose and solve problems blade is the first stop for any production weirdness. It runs canned analytics over your platform logs and surfaces the common patterns: failed deploys, high CPU, memory pressure, slow requests, autoscale flapping. Use the findings as diagnostic leads and correlate their time window with deployment and application evidence; no time-to-diagnosis guarantee is implied.
The Metrics blade graphs platform-level metrics: CPU percentage, memory percentage, requests, response time, data in/out, HTTP status counts. Metric availability, scope and dimensions depend on the resource/OS/tier. They do not require the Application Insights SDK, but an empty or delayed metric is not proof of zero traffic or healthy application code.
For deeper diagnostics, the Kudu debug console (https://<app>.scm.azurewebsites.net) lets you SSH-equivalent into the worker, inspect deployed files, view environment variables and sensitive diagnostics (do not assume values or dumps are universally masked), and download memory dumps. Use it sparingly; it is a debug surface, not an operational one.
Good enough to ship
• Open Diagnose-and-solve first. • Metrics dashboard for high-level health. • Log Stream for active debugging.
Expert tier
• Kudu memory dumps + dotnet-dump analysis. • Profiler + Snapshot Debugger for prod traces. • Custom alerts wired into action groups + PagerDuty.
Worked examples
Example 1 — Plan, app and slot Bicep fragment
An illustrative Bicep fragment, not a complete production deployment for an API with staging slot, managed identity, Key Vault reference, VNet integration and Health Check:
param env string
param location string = 'eastus2'
resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: 'plan-${env}'
location: location
sku: { name: 'P1v3'
tier: 'PremiumV3'
capacity: 2 }
kind: 'linux'
properties: { reserved: true }
}
resource api 'Microsoft.Web/sites@2023-12-01' = {
name: 'api-${env}'
location: location
identity: { type: 'SystemAssigned' }
properties: {
serverFarmId: plan.id
httpsOnly: true
virtualNetworkSubnetId: integrationSubnet.id
siteConfig: {
linuxFxVersion: 'DOTNETCORE|8.0'
alwaysOn: true
healthCheckPath: '/health/ready'
minTlsVersion: '1.2'
ftpsState: 'Disabled'
appSettings: [
{ name: 'ASPNETCORE_ENVIRONMENT'
value: env }
{ name: 'Db__Conn'
value: '@Microsoft.KeyVault(VaultName=kv-${env};SecretName=db-conn)' }
{ name: 'WEBSITE_VNET_ROUTE_ALL'
value: '1' }
{ name: 'WEBSITE_RUN_FROM_PACKAGE'
value: '1' }
]
}
}
}
resource stagingSlot 'Microsoft.Web/sites/slots@2023-12-01' = {
parent: api
name: 'staging'
location: location
identity: { type: 'SystemAssigned' }
properties: {
serverFarmId: plan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'DOTNETCORE|8.0'
alwaysOn: true
healthCheckPath: '/health/ready'
autoSwapSlotName: '' // disable auto-swap; do it explicitly in pipeline
}
}
}- One Plan (Premium v3, capacity 2 for replica headroom; zone redundancy is not configured by this fragment).
- API with system-assigned identity, HTTPS-only, Always On, health-check path, Linux .NET 8.
- App settings include Key Vault reference; routes all outbound through VNet.
- Staging shares plan capacity but has its own identity. This fragment deliberately omits existing integrationSubnet/workspace declarations, subnet delegation, slot VNet integration, role assignments, DNS, private endpoint, sticky setting names and staging app settings. Those are deployment requirements, not implied inheritance. Provision staging configuration explicitly and grant both identities only their intended data access.
Example 2 — Slot swap with health-check gate from GitHub Actions
name: Deploy API
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
concurrency: { group: api-production-release, cancel-in-progress: false }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with: { dotnet-version: '8.0.x' }
- run: dotnet publish src/Api -c Release -o ./out
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- uses: azure/webapps-deploy@v3
with:
app-name: api-prod
slot-name: staging
package: ./out
- name: Wait for staging readiness
run: |
for i in {1..30}; do
if [ "$(curl -sS --connect-timeout 5 --max-time 10 -o /dev/null -w '%{http_code}' https://api-prod-staging.azurewebsites.net/health/ready)" = "200" ]; then
echo "Ready"; exit 0
fi
sleep 10
done
echo "Staging never went ready"; exit 1
- name: Swap slots
run: |
az webapp deployment slot swap \
--resource-group rg-prod \
--name api-prod \
--slot staging \
--target-slot production- Federated identity (
id-token: write) — no client secret. Because this job uses environment production, configure the subjectrepo:ORG/REPO:environment:production, environment protections and narrow deployment permissions; a branch-only subject will not match. Pin action commits in an approved release workflow. Identifiers may be repository variables; they are not passwords. - Deploy to staging, poll readiness, swap on success.
- If readiness never reports, the pipeline fails before swap. A private staging endpoint also needs a runner with private connectivity. The later platform warmup must require status 200 under target configuration. After swap, check production health/version and authorized dependency access; neither a curl nor a swap reverses a database migration.
Example 3 — CPU autoscale and scheduled capacity
A simple CPU-based autoscale via Bicep, with a default profile; scheduled profiles are not included:
resource autoscale 'Microsoft.Insights/autoscalesettings@2022-10-01' = {
name: 'autoscale-api'
location: location
properties: {
targetResourceUri: plan.id
enabled: true
profiles: [
{
name: 'default'
capacity: { minimum: '2'
maximum: '10'
default: '2' }
rules: [
{
metricTrigger: { metricName: 'CpuPercentage'
metricResourceUri: plan.id
timeGrain: 'PT1M'
statistic: 'Average'
timeWindow: 'PT5M'
timeAggregation: 'Average'
operator: 'GreaterThan'
threshold: 70 }
scaleAction: { direction: 'Increase'
type: 'ChangeCount'
value: '1'
cooldown: 'PT5M' }
}
{
metricTrigger: { metricName: 'CpuPercentage'
metricResourceUri: plan.id
timeGrain: 'PT1M'
statistic: 'Average'
timeWindow: 'PT10M'
timeAggregation: 'Average'
operator: 'LessThan'
threshold: 30 }
scaleAction: { direction: 'Decrease'
type: 'ChangeCount'
value: '1'
cooldown: 'PT10M' }
}
]
}
]
}
}- The example chooses a shorter scale-out window/cooldown than scale-in (5 vs 10 minutes) to favor headroom over aggressive cost reduction; it is not a measured provisioning-time claim. Each candidate rule has its own cooldown eligibility.
- Plan minimum 2 provides capacity for replicas; check that per-app scaling also allows at least two active app replicas. It is not proof of zone resilience or dependency availability.
- Max 10 bounds this profile's instance count, not total spend; add budget alerts. For a weekday peak, an illustrative recurrence could set minimum/default 4 at 08:30 and a second recurrence restore minimum/default 2 at 18:00, both maximum 10. Recurring profiles have no end field: the next profile start supplies the boundary. Define timezone and daylight-saving expectations, preserve the required rules in each profile, and test fixed-date → recurring → default precedence; do not assume the default automatically resumes at the end of a workday. Threshold window plus cooldown plus worker startup means “scale within five minutes” is not guaranteed; retain headroom and observe flapping protection.
Example 4 — Diagnostic Settings + KQL on platform logs
resource diag 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
scope: api
name: 'to-log-analytics'
properties: {
workspaceId: workspace.id
logAnalyticsDestinationType: 'Dedicated'
logs: [
{ category: 'AppServiceHTTPLogs'
enabled: true }
{ category: 'AppServiceConsoleLogs'
enabled: true }
{ category: 'AppServicePlatformLogs'
enabled: true }
{ category: 'AppServiceAuditLogs'
enabled: true }
]
metrics: [{ category: 'AllMetrics'
enabled: true }]
}
}Then in Log Analytics:
AppServiceHTTPLogs
| where TimeGenerated > ago(1h)
| summarize
Total = count(),
Errors = countif(ScStatus >= 500),
p95 = percentile(TimeTaken, 95)
by bin(TimeGenerated, 1m), CsHost
| order by TimeGenerated desc- Requests resource-specific Log Analytics tables via
Dedicated; the generic diagnostic-settings API otherwise defaults to AzureDiagnostics. Confirm the supported categories and actual destination table on this resource before adapting the query. Enabling diagnostics can restart the app, so schedule the change deliberately. - Sample query — stored request/error counts and approximate p95 of TimeTaken per minute/hostname; derive ErrorRate = 100.0 * Errors / Total if needed. Check table availability and the documented millisecond units; log categories vary by OS/tier.
Hands-on exercises
-
Provision and deploy. Create a P1v3 Linux Plan, an App Service, and a staging slot via Bicep. Deploy a hello-world ASP.NET Core 8 app to staging, then swap.
- You are done when production serves the new code and the previous version is on staging.
-
Key Vault wiring. Create a Key Vault, store one secret, give the App Service's managed identity
Key Vault Secrets User. Reference the secret from an App Setting and verify it resolves inIConfiguration.- You are done when the green check shows in Configuration and your app reads the value at startup.
-
Health Check + recycle. In a disposable environment, enable Health Check on
/health/readyand set a documented failure threshold. Return 503 from one instance and observe the actual removal/recovery behavior and limits.- You are done when you have observed the failure-then-recovery in the Health Check blade.
-
Autoscale. Add the autoscale rule from Example 3. Use k6 or
heyto drive load until CPU exceeds 70%; record evaluation, cooldown and worker-startup delay before scale-out.- You are done when instance count rises and then falls back.
-
Private networking. Provision a Storage account with a private endpoint. Check resolution before/after VNet integration, private DNS linking, routes/NSGs and data-plane RBAC. Test from the application context; route-all alone cannot fix DNS or missing authorization.
- You are done when you can articulate exactly which knob unblocked traffic.
-
Pipeline deploy. Wire Example 2 into a GitHub Actions workflow against your real subscription using federated identity. Confirm zero secrets are stored in the repo.
- You are done when a push to
maindeploys → polls readiness → swaps.
- You are done when a push to
Self-check questions
- Explain the difference between an App Service Plan and a Web App, and when you would share a plan across apps.
- What does "Deployment slot setting" mean, and which environment-specific settings should be pinned for this application?
- Describe the slot-swap sequence in five steps.
- What does
WEBSITE_VNET_ROUTE_ALL=1change, and when do you need it? - Why does App Service traffic routing need a readiness-style probe, and how does it differ from process liveness?
- What are the default/configurable failed-probe thresholds, replacement interval and single-instance limitation?
- Explain the prerequisites for
@Microsoft.KeyVault(...)to resolve successfully. - When is self-contained .NET publishing the right choice over framework-dependent?
- Why scale-out over scale-up for stateless apps? When would you scale up?
- What is the difference between Access Restrictions, Private Endpoints, and VNet Integration?
- Why is Kudu a debug surface rather than an operational one?
- Describe the cold-start mitigations and the order to apply them.
Custom domains, TLS and a release decision
A custom hostname has two independent requirements: ownership/routing and TLS. Validate the domain using the App Service verification TXT record, then configure the appropriate CNAME for a subdomain or supported apex mapping to the app. DNS propagation and cached answers mean a successful portal save is not proof every client resolves the new endpoint. Retain domain-verification protection against dangling-DNS takeover when removing/recreating an app.
Bind a certificate that covers the exact hostname (normally SNI TLS), enforce HTTPS-only and the intended minimum TLS version, and test the hostname and certificate chain from the client network. A DNS binding alone is not a TLS binding. App Service managed certificates have eligibility/renewal constraints; private or restricted origins may require a different certificate path. With Front Door, edge TLS and origin TLS are separate connections: configure the origin host header, certificate name validation and Private Link approval, not just the public domain.
Worked release: staging readiness is 200 before swap, but candidate code fails production Key Vault access during target-settings warmup. Keep old traffic in production; inspect the source slot's identity, target secret reference, role grant and DNS. Do not “fix” this by granting subscription Contributor. If warmup and swap succeed but a business probe fails, assess rollback compatibility with persisted schema/messages before swapping back.
Worked assessment answers
- A Plan owns compute capacity and billing; apps and slots share it. Share when combined peaks fit headroom and correlated failure is acceptable; per-app scaling can limit an app to fewer workers than the plan count.
- A deployment-slot setting normally stays attached to its environment after swap. Pin environment labels, environment-specific connection strings and feature overrides when those values describe the slot; telemetry connection strings may also be slot-specific. There is no universal three-setting security guarantee, and target values can temporarily be applied to the source during warmup.
- Deploy to source staging; apply relevant target configuration and restart source instances; require successful warmup under that configuration; switch routing to warmed candidate code; finish the other slot's configuration and verify production health/version. Identity does not swap, and swap-back cannot undo external writes.
- Route-all sends outbound application traffic through the integration VNet rather than only the default private ranges. Use it for required inspected/NAT egress; separately validate DNS, routes, NSGs and configuration-traffic controls. It does not establish private ingress.
- Readiness checks whether the instance can serve useful traffic, including bounded critical-dependency checks. Process liveness only proves the process is running. The original premise that process-only
/health/liveis always the correct traffic-routing probe was false; redirects also are not readiness success. - Health Check probes each minute; the default unhealthy threshold is ten failed requests, configurable from two to ten. An unhealthy instance can be removed within removal limits and can rejoin after recovery; continuous unhealthiness for one hour can cause replacement, limited to one replacement per hour and three per day per plan. One active instance is not removed just to leave zero capacity. This is neither a default two-minute removal promise nor a ten-minute restart rule.
- A correct reference, selected identity, narrow secret-read permission and permitted network/DNS path are all needed. Versionless values can lag by up to 24 hours; version pinning does not prevent disable/delete failures. Resolution status and safe application behavior can verify wiring without printing the secret.
- Framework-dependent publication uses the supported platform runtime family. Self-contained ships a runtime and requires you to patch and republish it; choose it only for a supported OS/architecture/hosting contract and an explicit version requirement, not to evade end of support.
- Scale out for parallel stateless work with externalized session/state, enough dependency capacity and measured startup headroom. Scale up when per-process memory/CPU or indivisible work requires it. Neither cures a serialized bottleneck automatically, and plan scaling affects colocated workloads.
- Access restrictions filter supported public ingress; private endpoints give private inbound connectivity but require public-access/DNS decisions; VNet integration supplies outbound connectivity. Restrictions are not evaluated for private-endpoint traffic, and inbound and integration subnets must differ.
- Kudu, environment inspection and memory dumps are privileged diagnostics that can expose credentials and personal data. Use scoped access, controlled collection and retention; do not depend on assumed masking or use interactive debugging as the release process.
- Measure startup first. Use Always On to avoid idle unloading, then explicit target-settings warmup and readiness; evaluate ReadyToRun against size/startup measurements and reduce container pull/initialization work. Correct routing ports and status gates matter; a port setting is not an image pre-pull instruction.
Exercise 1 solution: The plan/app/slot fragment is a starting design, not a complete deployment. Supply its listed subnet, identity, sticky-setting and DNS dependencies; publish the versioned hello-world package to staging, verify build identity, require target-settings warmup, then compare production and staging versions after swap. Preserve the previous artifact and check external-state compatibility before rollback. No deployment was performed for this answer.
Exercise 2 solution: Give the intended app/slot identity only the necessary secret-read permission, establish network/DNS access and configure the reference. Check reference-resolution status and a non-secret functional outcome (for example an authorized database read), not a dump of IConfiguration. Exercise rotation with overlapping valid credentials and the documented refresh/cache behavior in an authorized disposable environment. A portal indicator alone is not a service integration test.
Exercise 3 solution: Choose at least two active app replicas with spare capacity and record the configured failed-probe threshold. In an explicitly authorized disposable test, make one replica's readiness return 503, correlate per-instance probes/routing/removal, restore 200 and observe reentry. Do not infer replacement from removal: sustained unhealthiness has the separate hour-scale replacement limit. The local model checks decisions, not Azure timing.
Exercise 4 solution: Read the exact default profile: minimum/default 2, maximum 10, average CPU above 70 over 5 minutes requests +1 with a 5-minute cooldown; below 30 over 10 minutes requests -1 with a 10-minute cooldown. Equality at a threshold does not satisfy GreaterThan/LessThan. Bounded authorized load testing must record evaluation, rule cooldown and worker readiness, not promise five-minute provisioning. Verify scheduled-profile transitions independently and stop at the agreed cost/load limit.
Exercise 5 solution: Diagnose from the app context in order: does the backend hostname resolve to the intended private address, does the integration route/NSG permit the port, and does the managed identity have the backend data permission? A public DNS answer, blocked route and denied token are different failures. Configure private DNS links and integration explicitly; route-all alone fixes neither name resolution nor RBAC. Retest staging independently.
Exercise 6 solution: Configure an approved federated identity whose subject matches the production GitHub environment and narrow deployment rights; pin reviewed action commits in the consuming release workflow. A runner must reach staging/SCM on the selected network. The bounded poll permits swap only after HTTP 200; redirects, 503 and transport failure must not pass. Platform target-settings warmup and post-swap version/business probes are additional gates, not replaced by the curl. The YAML is a design reference, not an executed release.
The optional exercises require a disposable, cost-bounded subscription and permission from its owner. Do not fault-inject a real production slot or send live notifications merely to complete this conceptual review.
High-signal resources
Official docs
- App Service overview — start here.
- Set up staging environments (slots) — slot lifecycle.
- Use Key Vault references — the canonical reference for
@Microsoft.KeyVault(...). - Integrate with Azure Virtual Network — networking.
- Configure environment variables and connection strings — App Settings reference.
- App Service best practices — must-read.
Books or courses
- Microsoft Azure for .NET Developers — covers the App Service surface end to end.
- Pluralsight Azure App Service Deep Dive — solid four-hour walkthrough.
Practitioner posts
- Azure App Service team blog — release notes and platform changes.
- Mads Kristensen on slot-swap pitfalls — practitioner-level deep dives.
- Wahid Saleemi — App Service VNet integration deep dive — the networking story.
Weekly milestones
- Day 1. Read the overview + best practices. Provision the plan + app + slot (exercise 1). Self-check questions 1–3.
- Day 2. Key Vault + managed identity wiring (exercise 2). Self-check questions 4 + 7.
- Day 3. Health Check + autoscale (exercises 3 + 4). Self-check questions 5–6 + 9.
- Day 4-5. Private networking (exercise 5). Self-check question 10.
- Day 6-7. Pipeline deploy with federated identity (exercise 6). Self-check questions 8 + 11 + 12.
How it shows up in the capstone
In the capstone design—not an observed deployment—the API and SPA both run on App Service. The API uses a P1v3 Linux Plan with capacity 2, a production slot, a staging slot, and Always On. The SPA is a separate App Service on the same Plan (the load profile is gentle and complementary). Actual secrets such as a credential-bearing database connection live in Key Vault; a Kusto endpoint is ordinary configuration, and signing-key access needs a deliberate key-management design. A secret is referenced via @Microsoft.KeyVault(...); the app's system-assigned managed identity has Key Vault Secrets User on the vault.
VNet integration is enabled and WEBSITE_VNET_ROUTE_ALL=1. SQL, Storage, Key Vault and the Kusto cluster all have private endpoints in the same VNet, with DNS, routing and public-access controls to be checked for each backend; merely declaring endpoints does not prove the effective path. Health Check points at a bounded /health/ready endpoint. Autoscale runs CPU-based rules with a default profile; scheduled profiles are not included. Deploys go via GitHub Actions with federated identity → staging → readiness poll → swap.
Previous chapter → Ch 12 — Kusto / KQL Next chapter → Ch 14 — Identity for cloud apps
Corrected contracts and failure analysis
A staging slot is not a second isolated App Service Plan: it shares compute capacity with its parent plan. System-assigned identities do not swap, so grant and test each slot's downstream permissions independently. Mark environment-specific settings slot-sticky where required; swapping code cannot reverse external schema migrations or emitted messages. A health endpoint can pass while important dependencies or authorization still fail.
VNet integration governs outbound connectivity. Inbound private addressing requires a private endpoint in this multitenant App Service design. Access restrictions can restrict public ingress but do not create a private endpoint. A reference to a private subnet alone does not prove public exposure is blocked. Versionless Key Vault references can remain cached for up to 24 hours; make rotation overlap old/new credentials and trigger the supported refresh when needed. Two instances are not a universal HA proof: zone support, state storage, dependency failure and capacity remain separate.
Budgets alert; they do not stop consumption. Max instance count bounds one scaling dimension, not all storage, egress or telemetry charges.
Boundary exercise with solution
Staging can read Key Vault but production fails immediately after swap. Which identities should you compare?
Solution and reasoning
Compare the slot-specific managed identities, each role assignment, network route and sticky secret reference. Identity stays with its slot. Rollback must account for state changes outside the slot, not just swap the old package back.
Source-backed review notes
- Set Up Staging Environments - Azure App Service | Microsoft Learn — accessed 2026-10-07. Exact supporting passage: “When you swap slots, these settings aren't swapped Protocol settings (Https Only, TLS version, client certificates) * Publishing endpoints Custom domain names Nonpublic certificates and TLS/SSL settings Scale settings WebJobs schedulers IP restrictions * Always On * Diagnostic log settings * Cross-origin resource sharing (CORS) * Managed identities”
- Use Key Vault References as App Settings - Azure App Service | Microsoft Learn — accessed 2026-10-07. Exact supporting passage: “If the secret version isn't specified in the reference, the app uses the latest version that exists in the key vault. When newer versions become available, such as with rotation, the app is automatically updated and begins using the latest version within 24 hours.”
- Integrate your app with an Azure virtual network - Azure App Service | Microsoft Learn — accessed 2026-10-07. Exact supporting passage: “Virtual network integration gives your app access to resources in your virtual network, but it doesn't grant inbound private access to your app from the virtual network. Private site access refers to making an app accessible only from a private network, such as from within an Azure virtual network. Virtual network integration is used only to make outbound calls from your app into your virtual network. Refer to private endpoint for inbound private access.”
- Tutorial - Create and manage budgets - Microsoft Cost Management | Microsoft Learn — accessed 2026-10-07. Exact supporting passage: “You can configure alerts based on your actual cost or forecasted cost to ensure that your spending is within your organizational spending limit. Notifications are triggered when the budget thresholds are exceeded. Resources aren't affected, and your consumption isn't stopped. You can use budgets to compare and track spending as you analyze costs.”
Review refinements — 9 October 2026
This review separates public edge/private origin from private-only client access, makes the slot warmup identity/configuration transition explicit, requests a resource-specific diagnostic destination and supplies individual assessment answers. Two active replicas, not merely a plan size, are the relevant starting point for instance-loss planning. No Azure resources were created or queried.
- .NET support policy — .NET 8 maintenance ends 10 November 2026; runtime selection is not a patch pin.
- App Service private endpoints — public access is separate, each slot is configured independently, and integration uses a different subnet.
- Diagnostic settings API 2021-05-01-preview —
Dedicatedrequests resource-specific tables instead of the generic AzureDiagnostics destination. - AppServiceHTTPLogs schema — TimeTaken is in milliseconds and ScStatus is an integer.
- Diagnostic logs — enabled log types, buffering, Linux CLI limitations and the diagnostic-setting restart side effect matter.
- Autoscale settings — rule cooldowns and fixed-date/recurring/default profiles have distinct evaluation rules.
Versioned source contracts
Official passages were checked on 7 October 2026; immutable tags are used where available, while service docs remain dated snapshots. The saved evidence records include exact passages, URL, source date/revision and snapshot hash. These references support the stated contracts, not an unperformed service deployment.
- slots — Managed identities do not swap with code; authorize each slot independently.
- app-health — Default failed-probe threshold is ten, configurable down to two.
- app-common — Reader does not automatically expose app-setting values; privileged configuration permissions matter.
- kvref — Versionless Key Vault references may lag rotation up to 24 hours.
- network — VNet integration is outbound; private endpoint ingress and DNS are separate.
- app-domain — Custom-domain ownership verification and routing are separate from TLS binding.
- App Service TLS bindings — the TLS binding explicitly chooses SNI/IP behavior after domain ownership and certificate coverage are established; checked 7 October 2026.
Pause / Recall / Apply
Can you explain it without the page?
Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.
Stored in this browser only. No account, no sync. Clearing browser data removes your record.