Azure App Service is Microsoft’s fully managed platform for hosting web applications, REST APIs and back-ends — a Platform as a Service (PaaS) that takes care of the operating system, the web server, patching, load balancing and the underlying virtual machines, leaving you to deploy nothing but your code or container. Where a virtual machine hands you a bare OS and says “good luck”, App Service hands you an HTTPS endpoint, a deployment pipeline, autoscaling, SSL certificates and staging environments out of the box. If you have ever run a site on a shared web host but wished it could scale to millions of requests, survive a datacentre fault, swap a new release into production with zero downtime and integrate privately into a corporate network — that is App Service.
This lesson is deliberately exhaustive. App Service is the single most-examined PaaS service on both AZ-104 (Azure Administrator) and AZ-204 (Azure Developer), and interviewers probe it relentlessly: “What’s the difference between scaling up and scaling out?”, “How does a slot swap achieve zero downtime?”, “Which settings are sticky to a slot?”, “Free vs Basic vs Premium v3 — when each?”. We cover every App Service Plan tier with its features, SLA and cost trade-offs; every runtime stack option; the full scaling model (manual, rule-based autoscale, and the newer automatic scaling); deployment slots and all four swap behaviours; TLS/SSL end to end (managed certificates, custom certificates, bindings, HTTPS-only, minimum TLS version); custom domains with their DNS verification records; the complete networking story (VNet integration, private endpoints, hybrid connections, access restrictions); app settings and connection strings; backup; diagnostics and the Kudu console; and managed identity. Every core operation comes with both an az CLI command and a Bicep snippet.
By the end you will know App Service end to end — enough to ace an AZ-104 or AZ-204 question, sail through an interview, and run a production web tier safely.
In a nutshell
Imagine leasing one furnished floor in a serviced office building. The landlord runs the power, lifts, security, cleaning and the front desk; you just move your desks in and start working. That floor is an App Service Plan (you pay a flat rent for its size, whether one desk or twenty sit on it), and the offices you furnish on it are your apps. Renting a bigger floor is scaling up; renting identical extra floors is scaling out; a deployment slot is a show-flat furnished exactly like the real office, so when the new fit-out is ready you just swap the nameplates on the doors — visitors never notice the move. Azure patches the OS, runs the web server, terminates HTTPS and load-balances for you; you deploy nothing but code or a container.
That single idea — the plan is the hardware you rent, the app is the software you run on it — explains almost every rule in this lesson: why you pay per plan not per app, why ten small apps can share one plan, and why an app’s “size” is really the plan’s tier.
Level: Intermediate · Time: ~45 min
Before you start, it helps to know Azure’s subscription → resource group → resource hierarchy and how to run an az command (both from the Foundations module); virtual networks and managed identity appear later but are defined as they come up. After this lesson you will be able to pick the right plan tier for a workload, scale up and out (manual, autoscale and automatic), ship releases with zero downtime using slots and swaps, secure custom domains with TLS, wire an app privately into a VNet, and hand it an identity so it reaches Key Vault and Storage with no stored secrets. The full objectives and prerequisites follow.
Learning objectives
By the end of this lesson you can:
- Choose the right App Service Plan tier (Free, Shared, Basic, Standard, Premium v3, Isolated v2) for a workload and explain each one’s features, SLA, limits and cost.
- Distinguish scaling up (bigger instances) from scaling out (more instances), and configure manual scale, rule-based autoscale and automatic scaling.
- Design deployment slots for zero-downtime releases and explain swap, swap-with-preview, sticky (slot) settings and auto-swap.
- Secure an app with managed and custom TLS certificates, TLS bindings, HTTPS-only and a minimum TLS version, and bind a custom domain with the correct A/CNAME/TXT verification.
- Connect an app privately with regional VNet integration, private endpoints, hybrid connections and access restrictions, and configure app settings, connection strings, backup, diagnostics/Kudu and managed identity.
- Reproduce the core create/configure operations in both
azCLI and Bicep.
Prerequisites & where this fits
You should understand Azure’s basic hierarchy — subscription → resource group → resource — regions, and how to run az commands in Cloud Shell (covered in the Foundations module). It helps to have met virtual networks and managed identity before, since the networking and identity sections build on them; if not, the terms are defined here. No prior App Service experience is assumed. This lesson sits in the Compute module of the Azure Zero-to-Hero course, alongside the Virtual Machines and load-balancing deep dives — App Service is the PaaS counterpart to the IaaS VM, and the two are frequently compared in exams (“when would you use App Service instead of a VM?”). It immediately precedes the monitoring deep dive, because everything you deploy here you will want to observe.
Core concepts
Before any settings, fix five mental models. They explain why App Service is shaped the way it is.
The plan is the hardware; the app is the software. App Service has two distinct resources you must keep straight. An App Service Plan (Microsoft.Web/serverfarms) is the compute — it defines the operating system, the region, the pricing tier (which sets vCPU, RAM, and feature set) and the number of instances. A Web App / App Service (Microsoft.Web/sites) is the application that runs on that plan. One plan can host many apps, and they all share the plan’s CPU, memory and instances. This is the single most important architectural idea in App Service: you pay for the plan, not the app. Ten small apps on one Basic plan cost the same as one app on that plan; you scale and pay at the plan level.
App Service is multi-tenant compute on shared infrastructure — except Isolated. In every tier from Free to Premium v3, your app runs on VMs that Microsoft manages and (in the shared/free tiers) may share with other customers. The Isolated v2 tier is different: it runs your plan inside a dedicated App Service Environment (ASE v3) — a single-tenant deployment injected into your virtual network, with no shared front-ends. That isolation is why Isolated exists and why it costs the most.
Windows plans and Linux plans are separate worlds. When you create a plan you pick the OS (Windows or Linux), and it is immutable — you cannot change a plan’s OS later. Linux plans run your app in a container (even for “code” deployments); Windows plans can run code natively or as Windows containers. Some features differ by OS (for example, certain deployment options and the exact runtime stack list), so the OS choice is consequential and permanent for that plan.
Scaling has two axes. Scale up changes the tier/size of every instance (more CPU/RAM each) — you move from, say, B1 to P1v3. Scale out changes the number of instances running your app behind the built-in load balancer. They are independent levers, and confusing them is a classic exam trap: scaling up makes each worker bigger; scaling out adds more workers.
Deployment slots are live copies of your app on the same plan. A slot is a fully functional clone of your app with its own hostname (e.g. myapp-staging.azurewebsites.net), running on the same plan. You deploy to a slot, warm it up and test it, then swap it with production — the swap is a fast hostname/route exchange, which is how App Service delivers near-zero-downtime deployments and instant rollback.
Key terms used throughout: plan (the compute), app/site (the application), instance/worker (one VM running your app), slot (a live copy of the app), swap (exchanging slot routes), sticky/slot setting (a setting that does not move during a swap), runtime stack (the language/version, e.g. .NET 8, Node 20, Python 3.12), and Kudu/SCM (the advanced management site behind every app).
App Service Plans: every tier (the master comparison)
The plan tier is the most consequential decision you make — it sets your feature set, your SLA, your scale ceiling and the bulk of your bill. App Service tiers fall into four categories: Shared compute (Free, Shared — your app shares VMs with other tenants and runs in a quota-limited sandbox), Dedicated compute (Basic, Standard, Premium v3 — dedicated VMs for your apps), and Isolated (Isolated v2 — dedicated VMs in your own VNet via an ASE).
| Tier | Category | Compute | Custom domains | TLS/SSL | Deployment slots | Autoscale | VNet integration | SLA | Typical use |
|---|---|---|---|---|---|---|---|---|---|
| Free (F1) | Shared | Shared, 60 CPU-min/day quota | No (only *.azurewebsites.net) |
No custom certs | None | No | No | None | Learning, throwaway demos |
| Shared (D1) | Shared | Shared, 240 CPU-min/day quota | Yes | SNI SSL only (no IP SSL) | None | No | No | None | Tiny sites needing a custom domain |
| Basic (B1–B3) | Dedicated | Dedicated VMs, up to 3 instances | Yes | SNI + custom certs | None | Manual only | Yes | 99.95% | Dev/test, small production |
| Standard (S1–S3) | Dedicated | Dedicated VMs, up to 10 instances | Yes | SNI + IP SSL + custom | 5 slots | Rule-based | Yes | 99.95% | General production |
| Premium v3 (P0v3–P5v3) | Dedicated | Faster Dv-series VMs, up to 30 instances | Yes | SNI + IP SSL + custom | 20 slots | Rule-based + automatic | Yes | 99.95% | Production at scale, low latency |
| Isolated v2 (I1v2–I6v2) | Isolated (ASE v3) | Dedicated VMs in your VNet | Yes | All | 20 slots | Rule-based + automatic | Native (deployed into VNet) | 99.95% | Regulated/network-isolated workloads |
A few essentials behind the table:
- Free and Shared have no SLA and a daily CPU-minute quota. When the quota is exhausted the app stops responding (HTTP 403) until the next day. They run in a sandbox that blocks some APIs. Never use them for anything you care about.
- Basic is the cheapest dedicated tier — you get full CPU/RAM with no quota and the 99.95% SLA, but no deployment slots and only manual scaling (up to 3 instances). It is the natural home for dev/test and very small production sites.
- Standard adds rule-based autoscale and 5 deployment slots, scaling to 10 instances. It is the workhorse general-production tier.
- Premium v3 is the modern recommended production tier. It runs on faster Dv-series hardware, supports up to 30 instances, 20 slots, zone redundancy (spread instances across availability zones), and the newer automatic scaling. The smallest size, P0v3 (1 vCPU / 4 GiB), exists as a low-cost entry point; P1v3–P3v3 (2/4/8 vCPU) are the common production sizes; there are also memory-optimized variants. Note that Premium v2 (Pv2) still exists but Premium v3 is preferred for new work.
- Isolated v2 runs inside an App Service Environment v3 — single-tenant, network-isolated, deployed into your own subnet. It is the only tier that places App Service natively inside a VNet (everything else uses VNet integration). It carries a fixed hourly ASE infrastructure charge on top of the per-instance cost, so it is the most expensive option by far and is reserved for compliance/isolation requirements.
The SLA gotcha to memorise: the 99.95% SLA applies to Basic and above. Free and Shared have no financially backed SLA at all. For higher resilience, deploy a Premium v3 plan with zone redundancy so instances are spread across availability zones in the region.
Scaling between tiers is mostly live, with rules. You can scale a plan up or down between tiers at any time and it applies to all apps on the plan, but you can only move within the same OS and you cannot move into or out of an ASE by changing the tier (Isolated is tied to an ASE). Moving from Free/Shared to a dedicated tier is a simple scale-up; moving back down loses any features you were using (slots, custom certs).
Runtime stacks and OS
When you create a web app you choose how your code runs: a runtime stack for code deployments, or a container image.
| Choice | What it is | Examples | Notes |
|---|---|---|---|
| Code (managed runtime) | App Service provides the language runtime; you push code | .NET 8/9, Java 17/21, Node 20/22, Python 3.11/3.12, PHP 8.x, Ruby (Linux) | Pick the major version; App Service patches the minor/security versions for you |
| Container (Web App for Containers) | You provide a Docker image from ACR/Docker Hub/registry | Any Linux container; Windows containers on Windows plans | Full control of the runtime; you own the base image patching |
OS choice (Windows vs Linux) is set at create time and is immutable for the plan. Linux is typically cheaper for equivalent sizes and is the default for Node/Python/PHP/Ruby; Windows is required for .NET Framework (full framework) apps and Windows containers. Gotcha: the available runtime stack list differs slightly by OS, and you cannot mix Windows and Linux apps on the same plan — they must live on separate plans.
Runtime version is a setting you can change after creation (e.g. bump Node 20 → 22), but treat it like a deployment: test in a slot first, because a major-version jump can break your app. App Service marks old runtime versions as deprecated over time and eventually removes them, so part of operating an app is keeping the stack current.
Creating a Web App: every setting
The portal’s “Create Web App” wizard presents several tabs. Take them field by field.
Basics tab
| Field | What it is / choices | Default | When / trade-off / gotcha |
|---|---|---|---|
| Subscription & Resource group | Billing boundary and lifecycle folder | Your default sub; RG chosen/created | Put the app, its plan and related resources in one RG to delete as a unit |
| Name | The app name; becomes <name>.azurewebsites.net |
— | Globally unique across all of Azure; 2–60 chars, letters/numbers/hyphens. Effectively immutable — renaming means recreate |
| Publish | How the app runs: Code, Container, or Static Web App | Code | Code = managed runtime; Container = your image; Static Web App is a different service for SPAs/static sites |
| Runtime stack | Language + major version (Code only) | A recent LTS | You can change the version later, but test in a slot |
| Operating System | Windows or Linux | Depends on stack | Immutable for the plan; Linux usually cheaper; .NET Framework needs Windows |
| Region | Azure datacentre geography | — | Nearest to users; prices vary by region; the plan and app must be in the same region as each other |
| App Service Plan | Existing plan or create new | Create new | Pick the pricing plan (tier) here — this is the cost lever. Reuse a plan to host multiple apps cheaply |
| Zone redundancy (Premium v3) | Spread instances across availability zones | Off | On for higher resilience; requires Premium v3 and 2+ instances; small cost for the extra instance(s) |
Gotcha: the name must be globally unique because it forms a public DNS hostname — contoso may be taken. Gotcha: once you pick the plan’s OS you cannot change it; choose deliberately.
Deployment tab
| Field | What it is | Default | Notes |
|---|---|---|---|
| Continuous deployment (GitHub Actions) | Wire the app to a GitHub repo/branch; the wizard generates a workflow | Disabled | Convenient CI/CD; you can also use Azure DevOps, az webapp deploy, ZIP deploy, or local Git later |
| Basic authentication (SCM/FTP creds) | Allow username/password publishing to Kudu/FTP | Increasingly disabled by default | Prefer Entra-based deployment; leaving basic auth off is a security best practice |
Networking tab
| Field | What it is / choices | Default | Notes |
|---|---|---|---|
| Enable public access | Whether the app is reachable from the internet | On | Turn off to make the app private (reachable only via private endpoint) |
| Enable network injection | Place the app’s outbound traffic into a VNet (regional VNet integration) at create time | Off | Lets the app reach private resources (databases, Key Vault) from creation; can also be added later |
| Inbound access (private endpoint) | Create a private endpoint so inbound traffic stays on the VNet | Off | Combine with “public access off” for a fully private app |
Monitoring + Tags tabs
| Field | What it is | Default | Notes |
|---|---|---|---|
| Application Insights | Enable APM (requests, dependencies, failures, live metrics) | On (recommended) | Strongly recommended; creates/links an App Insights resource. Adds a little cost for ingestion |
| Tags | Key/value metadata on the app and plan | None | Use for cost allocation, ownership, environment — enforce via Azure Policy |
az CLI and Bicep: the core create
The two-resource model (plan + app) is explicit on the command line.
# Variables
RG=rg-appsvc-demo
LOC=centralindia
PLAN=plan-appsvc-demo
APP=kv-appsvc-$RANDOM # must be globally unique
az group create -n $RG -l $LOC
# Create a Linux plan on the Basic B1 tier
az appservice plan create \
-g $RG -n $PLAN \
--sku B1 --is-linux
# Create a web app on that plan with the .NET 8 runtime
az webapp create \
-g $RG -p $PLAN -n $APP \
--runtime "DOTNETCORE:8.0"
# Scale UP later (bigger instances) — change the plan's SKU
az appservice plan update -g $RG -n $PLAN --sku P1V3
# Scale OUT (manual) — set instance count (Standard+ for autoscale)
az appservice plan update -g $RG -n $PLAN --number-of-workers 3
# Enforce HTTPS-only and a minimum TLS version
az webapp update -g $RG -n $APP --https-only true
az webapp config set -g $RG -n $APP --min-tls-version 1.2
The equivalent Bicep, with HTTPS-only and a minimum TLS version baked in:
param location string = resourceGroup().location
param appName string
param sku string = 'P1v3'
resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: 'plan-${appName}'
location: location
sku: {
name: sku // e.g. B1, S1, P1v3, I1v2
tier: 'PremiumV3'
}
kind: 'linux'
properties: {
reserved: true // 'reserved: true' = Linux plan
zoneRedundant: true
}
}
resource app 'Microsoft.Web/sites@2023-12-01' = {
name: appName
location: location
properties: {
serverFarmId: plan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'DOTNETCORE|8.0'
minTlsVersion: '1.2'
ftpsState: 'Disabled'
http20Enabled: true
}
}
identity: {
type: 'SystemAssigned' // managed identity, covered below
}
}
Gotcha: reserved: true on the plan is what makes it a Linux plan in ARM/Bicep — a frequent point of confusion. For Windows, omit reserved (or set it false) and use windowsFxVersion/netFrameworkVersion instead of linuxFxVersion.
Scaling: up vs out, autoscale, and automatic
This is the most-tested concept in the whole service, so be precise.
Scale up (vertical) changes the pricing tier/size of the plan — every instance becomes more powerful (more vCPU and RAM) and you unlock tier features (slots, autoscale, more instances). You scale up by changing the plan SKU (az appservice plan update --sku P2V3). It applies to all apps on the plan. Use it when a single request needs more CPU/RAM, or when you need a feature only a higher tier provides.
Scale out (horizontal) changes the number of instances running your app behind App Service’s built-in load balancer. More instances = more concurrent throughput and better resilience. There are three ways to scale out:
| Mechanism | Available on | How it works | When to use |
|---|---|---|---|
| Manual scale | Basic and above | You set a fixed instance count | Predictable, steady load; dev/test |
| Rule-based autoscale | Standard and above | Azure Monitor autoscale rules add/remove instances based on metrics (CPU %, memory, queue length, HTTP queue) and/or a schedule | Variable load with known patterns; full control of thresholds and min/max |
| Automatic scaling | Premium v3 | A platform-managed mode: you set max instances per app and a prewarmed instance count; App Service scales each app in/out based on incoming HTTP traffic, no rules to author | Spiky HTTP traffic where you want hands-off scaling and fast burst response |
Rule-based autoscale is the classic AZ-104 topic. A scale condition contains scale-out and scale-in rules, plus instance limits (minimum / maximum / default). A rule says, for example, “when average CPU > 70% over 10 minutes, increase count by 1” and a matching scale-in rule “when CPU < 30% over 10 minutes, decrease by 1”. Always pair a scale-out rule with a scale-in rule, set a sensible cool-down to avoid flapping, and use the default instance count as the fallback when no metric data is available. You can also add time-based (scheduled) profiles — e.g. scale to 5 instances every weekday 8am–6pm.
# Enable rule-based autoscale (Standard+): 2..10 instances, scale on CPU
az monitor autoscale create -g $RG \
--resource $PLAN --resource-type Microsoft.Web/serverfarms \
--name autoscale-appsvc --min-count 2 --max-count 10 --count 2
az monitor autoscale rule create -g $RG --autoscale-name autoscale-appsvc \
--condition "CpuPercentage > 70 avg 10m" --scale out 1
az monitor autoscale rule create -g $RG --autoscale-name autoscale-appsvc \
--condition "CpuPercentage < 30 avg 10m" --scale in 1
Gotcha: autoscale acts on the plan, not the app for rule-based scaling — all apps on the plan share the instance count. Automatic scaling (Premium v3), by contrast, lets you cap per-app maximum instances, which is its key advantage. Cost gotcha: you pay per instance-hour, so a runaway scale-out rule (low threshold, high max) can multiply your bill — set the maximum deliberately.
Deployment slots: swap, swap-with-preview, sticky settings, auto-swap
A deployment slot is a live copy of your app on the same plan with its own hostname. Slots are available on Standard (5), Premium v3 (20) and Isolated v2 (20); Basic and below have none. The default slot is production; additional slots are commonly named staging.
The swap is the core operation. When you swap staging into production, App Service:
- Applies the target slot’s settings to the source slot’s instances and warms them up (it sends requests to the app root / a configured warm-up path until it responds healthily).
- Once warm, it exchanges the routing — production traffic now hits the instances that were
staging, and the old production instances becomestaging.
Because the new code is fully warmed before it receives production traffic, the swap is near-zero-downtime, and because the old version is still sitting in the now-staging slot, rolling back is just another swap. This is the single most important reason teams use slots.
Sticky (slot) settings — the interview classic. By default, app settings and connection strings swap with the app. But you can mark an individual setting as a deployment slot setting (“sticky”), which means it stays with the slot and does not move during a swap. The rule to memorise:
| Sticky / slot setting | Swaps with the slot? | Use for |
|---|---|---|
| Not sticky (default) | Yes — moves to production on swap | App config that should be identical in staging and prod |
| Sticky (slot setting) | No — stays put | Environment-specific values: connection strings to a staging DB, feature-flag toggles, slot-specific endpoints |
Some things are always slot-specific and never swap regardless of stickiness: publishing endpoints, custom domain names, TLS/SSL bindings and certificates, scale settings, Always On, IP/access restrictions, and managed identities. Knowing what doesn’t swap is exactly the kind of detail exams test.
Swap-with-preview (multi-phase swap). Instead of a single atomic swap you can do it in two phases. Phase 1 applies the production slot’s settings to the staging slot and warms it — but does not change routing yet — so you can hit the staging hostname and validate the app with production configuration (production connection strings, etc.). When satisfied you complete the swap (phase 2 exchanges routing) or cancel it (staging reverts to its own settings). Use swap-with-preview when your app’s behaviour depends on settings that only exist in production.
Auto-swap. You can configure a slot to automatically swap into production whenever a deployment to that slot succeeds — useful for fully automated CD pipelines where every successful staging deploy should go live. Gotcha: auto-swap is not supported on Linux plans and you should pair it with a warm-up (Always On + a custom warm-up path) so the swap waits for the app to be ready.
Percentage traffic routing. Independently of swaps, you can route a percentage of production traffic to a slot (e.g. 10% to staging) for a canary / blue-green rollout, using the x-ms-routing-name cookie to pin a user to a slot.
# Create a staging slot (Standard+)
az webapp deployment slot create -g $RG -n $APP --slot staging
# Deploy a build to the slot, then swap it into production
az webapp deploy -g $RG -n $APP --slot staging --src-path ./app.zip --type zip
az webapp deployment slot swap -g $RG -n $APP --slot staging --target-slot production
# Swap with preview: start, validate the staging URL, then complete (or reset)
az webapp deployment slot swap -g $RG -n $APP --slot staging --action preview
az webapp deployment slot swap -g $RG -n $APP --slot staging --action swap # complete
# Mark a setting as sticky (slot setting)
az webapp config appsettings set -g $RG -n $APP \
--slot-settings DB_ENVIRONMENT=staging
# Canary: send 10% of production traffic to staging
az webapp traffic-routing set -g $RG -n $APP --distribution staging=10
TLS/SSL: managed certificates, custom certificates, bindings, HTTPS-only, min TLS
App Service gives every app free TLS on the default *.azurewebsites.net hostname automatically. The work is securing custom domains.
App Service Managed Certificate (free). App Service can issue and auto-renew a free TLS certificate for a custom domain you have already added and verified. It is the easiest option, but it has limits: it does not support wildcard certificates on all configurations, cannot be exported, and the domain must be verified first. Use it for standard single-hostname public sites.
Custom (bring-your-own) certificates. You can upload your own certificate in three ways:
| Source | What it is | When |
|---|---|---|
Upload a .pfx |
Your own purchased certificate + private key | You bought a cert from a CA, or need a wildcard *.contoso.com |
| Import from Key Vault | Reference a certificate stored in Azure Key Vault (App Service auto-renews if the KV cert renews) | Central certificate management, rotation governance |
| App Service Certificate | Buy a certificate through Azure (stored in Key Vault) | One-stop purchase + management inside Azure |
TLS bindings connect a certificate to a custom domain. There are two binding types:
| Binding type | What it is | Requirement | Notes |
|---|---|---|---|
| SNI SSL | Server Name Indication — multiple certs share one IP, selected by hostname | Supported on Shared and above | The normal, modern choice; near-zero cost |
| IP-based SSL | Dedicates a static inbound IP to one cert/hostname | Standard and above | For old clients without SNI; costs an extra IP and may change your inbound IP |
HTTPS-only. A single toggle that redirects all HTTP to HTTPS (returns 301). Turn it on for every production app — there is no reason to serve plaintext. Minimum TLS version sets the floor the server accepts (1.0 / 1.1 / 1.2 / 1.3). Set it to 1.2 or higher — older versions are insecure and fail most compliance baselines. There is also a client certificate (mutual TLS) setting if you need to require client certs for the app or specific paths.
# Create a free App Service managed certificate for an added+verified custom domain
az webapp config ssl create -g $RG -n $APP --hostname www.contoso.com
# Bind it (SNI SSL)
THUMB=$(az webapp config ssl list -g $RG \
--query "[?subjectName=='www.contoso.com'].thumbprint" -o tsv)
az webapp config ssl bind -g $RG -n $APP \
--certificate-thumbprint $THUMB --ssl-type SNI
# Upload your own .pfx and bind it
az webapp config ssl upload -g $RG -n $APP \
--certificate-file cert.pfx --certificate-password '***'
# Enforce HTTPS-only and TLS 1.2
az webapp update -g $RG -n $APP --https-only true
az webapp config set -g $RG -n $APP --min-tls-version 1.2
Custom domains: A / CNAME / TXT verification
To serve your app at www.contoso.com you map your DNS to the app and prove you own the domain.
| DNS record | Maps / proves | When to use | Notes |
|---|---|---|---|
| CNAME | www → <app>.azurewebsites.net |
Subdomains (www, api) |
The standard choice; cannot be used at the zone apex on classic DNS |
| A record | contoso.com → the app’s inbound IP |
Apex/root domains (contoso.com) |
Pair with the TXT verification record; the IP can change unless you use IP-based SSL |
| TXT | asuid.<host> → the app’s custom domain verification ID |
Both — proves ownership | App Service checks this asuid. TXT record to confirm you control the domain before activating the binding |
The flow: get the app’s Custom Domain Verification ID (and inbound IP for apex), create the CNAME (or A) record plus the TXT asuid. record at your DNS provider, then add the custom domain in App Service, which validates the records. Gotcha: DNS changes take time to propagate; the TXT asuid. record is what fails most first attempts — it must be exact. Apex domains require the A + TXT pair; subdomains use CNAME + TXT.
# Show the verification ID to put in the asuid. TXT record
az webapp show -g $RG -n $APP --query customDomainVerificationId -o tsv
# After DNS records exist, bind the custom domain
az webapp config hostname add -g $RG --webapp-name $APP \
--hostname www.contoso.com
Networking: VNet integration, private endpoints, hybrid connections, access restrictions
App Service has outbound networking (how your app reaches other resources) and inbound networking (who can reach your app). Keep them separate.
Regional VNet integration (outbound). This injects your app’s outbound traffic into a delegated subnet of a VNet, so the app can reach private resources — a database with a private endpoint, Key Vault, an internal API — using private IPs. It requires Basic and above, a dedicated subnet delegated to Microsoft.Web/serverFarms, and the subnet must have enough free addresses for scale-out. Gotcha: VNet integration is outbound only — it does not make your app reachable from the VNet (that’s a private endpoint), and by default only RFC-1918 private traffic is routed unless you enable “route all”.
Private endpoints (inbound). A private endpoint puts a private IP from your VNet onto your app, so inbound traffic reaches it without traversing the internet. Combined with public network access = disabled, this makes the app fully private — only clients on the VNet (or peered/connected networks) can reach it. This is the standard pattern for internal line-of-business apps and is heavily favoured by security baselines.
Hybrid Connections. A way to reach a specific host:port on-premises (or anywhere) without a VPN or VNet, using the Azure Relay service. The app makes an outbound connection to Relay and an on-prem Hybrid Connection Manager agent bridges to the target. Use it for point connections to a single legacy database or service when full VNet connectivity is overkill. Limit: it is host:port specific, not a general network route.
Access restrictions (inbound IP/service filtering). An allow/deny list of IP ranges, service tags or VNet subnets that can reach the app’s main site and (separately) the SCM/Kudu site. Rules have priorities, an unmatched-rule action (default Allow → switch to Deny once you add allow rules), and can be combined with private endpoints. Use it to lock the app down to, say, your corporate IP range or a Front Door / Application Gateway service tag.
| Feature | Direction | Needs | Use it for |
|---|---|---|---|
| VNet integration | Outbound | Basic+, delegated subnet | App → private DB/Key Vault/internal API |
| Private endpoint | Inbound | Standard+ (commonly), VNet | Make the app reachable only privately |
| Hybrid Connections | Outbound | Relay + on-prem agent | One host:port to on-prem without VPN |
| Access restrictions | Inbound | Any tier | IP/service-tag/subnet allow-deny on site & Kudu |
# Add regional VNet integration (subnet must be delegated to Microsoft.Web/serverFarms)
az webapp vnet-integration add -g $RG -n $APP \
--vnet my-vnet --subnet snet-appsvc
# Lock the main site to a corporate IP range
az webapp config access-restriction add -g $RG -n $APP \
--rule-name corp --priority 100 --action Allow --ip-address 203.0.113.0/24
# Make the app private: disable public access (pair with a private endpoint)
az webapp update -g $RG -n $APP --set publicNetworkAccess=Disabled
App settings and connection strings
App settings are environment variables injected into your app at runtime as KEY=value pairs — the standard way to configure an app without hard-coding values. Connection strings are a special category that App Service injects with provider-specific prefixes (SQLAzure, MySQL, PostgreSQL, Custom) and which some frameworks (notably .NET) surface differently.
| Aspect | App settings | Connection strings |
|---|---|---|
| What | Generic env vars | DB/service connection strings, typed |
| Injection | As environment variables | As env vars with a type prefix (e.g. SQLAZURE_...) |
| Swap behaviour | Swap by default; can be marked sticky | Same — swap by default, can be sticky |
| Secret handling | Reference Key Vault via @Microsoft.KeyVault(...) |
Reference Key Vault the same way |
Key Vault references are the recommended way to keep secrets out of configuration: a setting’s value becomes @Microsoft.KeyVault(SecretUri=https://kv.vault.azure.net/secrets/db-pwd/) and App Service resolves it at runtime using the app’s managed identity — so the secret never appears in the app config. Gotcha: App settings have a size limit and are not meant for large blobs; keep them small and push secrets to Key Vault.
Backup
App Service (Standard and above) can back up an app’s content and, optionally, a linked database to a storage account — on demand or on a schedule with a retention period. A backup captures the app configuration, file content, and (if configured) the connected database, and can be restored to the same app or a new one. Limits: there is a size cap per backup, and database backup supports specific database types. Gotcha: backup is not a substitute for source control or slot-based rollback — use Git for code and slots for release rollback; use backup for content/state recovery.
Diagnostics, log streaming, and Kudu
App Service logs can be turned on for application logging (your app’s stdout/log output), web server (HTTP) logs, detailed error messages, and failed request tracing, written to the filesystem (short-lived) or, better, streamed to Azure Monitor / Log Analytics via diagnostic settings for retention and KQL queries. Log streaming tails the logs live (az webapp log tail) — invaluable when debugging a deployment.
Kudu / SCM (<app>.scm.azurewebsites.net) is the advanced management site that sits behind every app. It provides a file browser, a console/SSH into the app, environment variable and process explorers, deployment history, and the deployment engine itself. Security gotcha: the SCM site is a privileged surface — protect it with access restrictions and prefer Entra auth over basic auth for it. Diagnose and solve problems (App Service Diagnostics) is the portal’s built-in detector suite for availability, performance and configuration issues.
# Turn on filesystem application logging and stream logs live
az webapp log config -g $RG -n $APP --application-logging filesystem --level information
az webapp log tail -g $RG -n $APP
Managed identity
A managed identity gives your app an Entra ID identity so it can authenticate to other Azure services (Key Vault, Storage, SQL, Service Bus) without storing any credentials. There are two kinds:
| Type | Lifecycle | When |
|---|---|---|
| System-assigned | Created with the app, deleted with it; one per app | Simple 1:1 — this app needs to call Key Vault/Storage |
| User-assigned | A standalone resource you create and attach to many apps | Shared identity across several apps; survives app deletion; pre-provision RBAC once |
Enable the identity, then grant it RBAC roles (e.g. Key Vault Secrets User) on the target resources. This is what powers Key Vault references in app settings and credential-free access to data services — and it is the security pattern exams and interviews expect you to reach for instead of secrets in config.
# Enable a system-assigned identity and grant it Key Vault secret access
az webapp identity assign -g $RG -n $APP
PRINCIPAL=$(az webapp identity show -g $RG -n $APP --query principalId -o tsv)
az role assignment create --assignee $PRINCIPAL \
--role "Key Vault Secrets User" \
--scope $(az keyvault show -n my-kv --query id -o tsv)
After creation: what you can (and can’t) change
Most App Service settings are mutable, but a few are not — and knowing which is exactly what production operators and exams care about.
| Operation | Can you change it? | Notes |
|---|---|---|
| Scale up/down tier | Yes, live | Applies to all apps on the plan; same OS only |
| Scale out instances | Yes, live | Manual or autoscale (Standard+) / automatic (P v3) |
| Runtime version | Yes | Test in a slot; major bumps can break |
| App settings / connection strings | Yes, live (app restarts) | Mark slot-specific ones sticky |
| Custom domains & TLS bindings | Yes | Always slot-specific; do not swap |
| VNet integration / private endpoint / access restrictions | Yes | Add/remove after creation |
| Managed identity | Yes | Add system/user-assigned anytime |
| Plan OS (Windows ↔ Linux) | No | Immutable — create a new plan and migrate |
| App name / hostname | No (effectively) | Globally unique DNS name; rename = recreate |
| Region | No | Recreate in the target region (or use a clone) |
| Move app between plans | Yes, with rules | Both plans must be same region and OS (use az webapp update --plan / portal) |
The signature day-2 operations: scale up/out, create and swap slots, bind a managed/custom certificate, add a custom domain, add VNet integration / private endpoints / access restrictions, enable managed identity and Key Vault references, configure backup and diagnostics, and restart / move the app.
The diagram shows the two-resource model at the centre — the plan (compute) hosting the app with its production and staging slots and the swap between them — surrounded by the four cross-cutting concerns this lesson covers: inbound TLS/custom domains, networking (VNet integration outbound, private endpoint inbound, access restrictions), identity/config (managed identity, app settings, Key Vault references) and operations (Kudu/SCM, diagnostics, backup).
Going deeper
Everything above is enough to pass the exam and run a healthy app. This section is for the production internals that only bite at scale — the things that turn a green dashboard red at 3 a.m.
The filesystem is shared storage, not a local disk
On App Service, /home (C:\home on Windows) is not the instance’s local disk — it is an Azure Files share mounted into every instance of the app, so all your workers see the same wwwroot. That makes deployment simple (write once, every instance sees it) but has two consequences engineers trip over: file I/O to /home crosses the network, so it is slower and higher-latency than a real local disk; and because it is shared, you cannot use it for instance-local scratch or locking — write temporary files to %TEMP%//tmp (genuinely local, wiped on recycle) instead. For a custom Linux container, that shared storage is opt-in: set WEBSITES_ENABLE_APP_SERVICE_STORAGE=true only if the container actually needs the persisted /home, otherwise leave it off for faster, fully local disk.
Run-from-package: make wwwroot read-only and deploys atomic
The single most impactful reliability setting most teams miss is WEBSITE_RUN_FROM_PACKAGE. Set it to 1 (or to a SAS URL of a ZIP in Storage) and App Service mounts your deployment ZIP as a read-only wwwroot instead of unzipping files onto the share. The wins are real:
- Atomic deploys — the app flips from old package to new in one operation; there is no window where half the files are updated and the app is serving a Frankenstein of two versions.
- No file-locking failures — the classic Windows “the process cannot access the file because it is being used” deploy error disappears because nothing is writing into
wwwroot. - Faster cold start — reading from a mounted package beats cold-reading thousands of small files off the share.
The trade-off: wwwroot is now read-only, so any code that writes next to itself (logs, a SQLite file, uploaded content) must be pointed at /tmp, Azure Storage, or a database. This is a feature, not a bug — apps that write into their own deployment directory are a deployment anti-pattern.
Front-ends, workers and the ARR affinity cookie
Inbound traffic hits App Service front-ends (shared HTTP reverse proxies) that route to your workers (the instances your plan pays for). By default the front-ends stamp an ARRAffinity cookie (Application Request Routing) that pins a browser to one specific worker — “session affinity”. That is helpful for a legacy app storing session state in memory, and actively harmful for a modern stateless app: it defeats even load distribution, so one hot instance melts while others idle, and it interferes with some streaming APIs. Disable it for stateless apps (clientAffinityEnabled: false in Bicep, or the “ARR affinity” toggle in the portal) and keep session state in Redis or a database instead.
Warm-up: what actually happens during a swap or scale-out
A slot swap and a scale-out both add cold instances to rotation, and a cold worker that receives production traffic before its runtime has JIT-compiled and its caches are primed will serve slow first requests. App Service warms an instance by hitting a warm-up path before routing real traffic to it. Two knobs control it: Always On (Basic+) keeps the app loaded so it is never fully unloaded when idle, and the warm-up configuration (applicationInitialization in web.config on Windows, or the WEBSITE_SWAP_WARMUP_PING_PATH / WEBSITE_WARMUP_PATH app settings) tells the platform which URL to ping and which status codes count as “ready”. Without a warm-up path a swap can complete the moment the app root returns anything, including a 500 during startup — which is how a “successful” swap still ships an outage.
SNAT port exhaustion — the outbound scaling wall
This is the failure mode that most surprises teams, because the app looks healthy and CPU is low, yet outbound calls to external endpoints (a third-party API, a public database host, even a public Azure endpoint) start failing intermittently with timeouts. The cause: every instance shares a small pool of SNAT (Source NAT) ports — roughly 128 pre-allocated per instance — for outbound connections to public IPs, and each closed connection holds its port in TIME_WAIT for a few minutes before it can be reused. An app that opens a new connection per request (the classic new HttpClient() in a loop, or a DB client with no pooling) burns through those ports under load and then stalls. The fixes, in order of leverage:
| Fix | Why it works |
|---|---|
Reuse connections (singleton HttpClient / IHttpClientFactory, DB connection pooling) |
Fewer ports opened per request — usually the real fix |
| Use private endpoints for Azure dependencies | Traffic to a private IP does not consume SNAT ports at all |
| VNet integration + a NAT Gateway on the subnet | Replaces the tiny per-instance pool with a NAT Gateway’s very large port pool |
| Scale out | More instances = more total SNAT ports (a band-aid, not a cure) |
Diagnose it with the SNAT metrics in Diagnose and solve problems → SNAT Port Exhaustion, not the CPU chart.
Health check and auto-heal — self-healing under load
Health check (Health check blade / healthCheckPath) tells App Service a path to probe — say /healthz. When an instance returns non-2xx to that path repeatedly over the window, the load balancer stops routing traffic to it (and, if it stays unhealthy, the platform replaces the instance) while your other instances keep serving. It only helps with two or more instances — with one instance there is nowhere to fail over to. Pair it with auto-heal: rules that trigger an action (recycle the worker process, capture a memory dump, or run a custom action) when a condition fires — memory above a threshold, more than N requests, more than N slow requests over T seconds, or a burst of a given status code. Auto-heal turns “a slow memory leak pages someone at midnight” into “the worker recycles itself and logs a dump for the morning”.
Premium v2 → Premium v3, zones, and per-app scaling
Premium v3 is the current production target: it runs on faster Ddv4/Ddv5-class hardware, offers larger and memory-optimized sizes, supports zone redundancy, and is required for automatic scaling. Premium v2 still exists but is legacy — Microsoft provides an in-place migration from Pv2 to Pv3 (and from ASE v2 to ASE v3), and new work should land on v3. Two scale nuances worth internalizing: zone redundancy needs a region that has availability zones and a minimum of the instances the platform spreads across zones (you cannot get zone resilience with a single instance); and rule-based autoscale acts on the whole plan (all apps share the count), whereas automatic scaling is the only mode that scales per app with a prewarmed-instance buffer — its defining advantage when several apps of different shapes share one Premium v3 plan.
When App Service is the wrong PaaS: Container Apps and Functions
App Service is the right home for a long-running web app or HTTP API where you want a managed HTTP server, slots, custom domains and predictable per-instance billing. It is not always the best fit:
| Need | Reach for | Why |
|---|---|---|
| Long-running web app / REST API, full HTTP control, slots, custom domains | App Service | Instance-based, always-on, mature slot/swap and TLS story |
| Event-driven or bursty work, short executions, scale to zero | Azure Functions (Consumption / Flex Consumption) | Per-execution billing, triggers/bindings, pay nothing when idle |
| Microservices in containers, Dapr, KEDA event scaling, revisions, scale to zero | Azure Container Apps | Serverless containers on managed Kubernetes without running AKS yourself |
| Full cluster control, custom operators, node-level tuning | AKS | You manage Kubernetes directly |
A useful rule: if the app must stay warm and answer HTTP under a custom domain with staged releases, App Service wins; if it is spiky and can scale to zero, Functions; if it is a fleet of containers that talk to each other, Container Apps. Go deeper in the Azure Container Apps deep dive and the Azure Functions deep dive; for how App Service’s built-in load balancer compares to a standalone one, see the Azure Load Balancer deep dive.
Hands-on lab
In this lab you create an App Service Plan and web app, add a deployment slot, deploy a tiny app to each, swap them, and exercise scaling, HTTPS-only, a sticky setting and managed identity — entirely from Cloud Shell. Custom domain and certificate binding are described (they need a domain you own) but every other step runs for real. Use a Standard tier so slots are available.
Step 1 — Resource group and a Standard plan.
RG=rg-appsvc-lab
LOC=centralindia
PLAN=plan-appsvc-lab
APP=kv-appsvc-lab-$RANDOM # globally unique
az group create -n $RG -l $LOC
az appservice plan create -g $RG -n $PLAN --sku S1 --is-linux
Expected: the plan is created with "sku": { "name": "S1", "tier": "Standard" }.
Step 2 — Create the web app and deploy a one-line site.
az webapp create -g $RG -p $PLAN -n $APP --runtime "NODE:20-lts"
# Deploy a trivial app from a public sample (or your own zip)
az webapp up -g $RG -n $APP --runtime "NODE:20-lts" --html 2>/dev/null || true
# Confirm it answers
curl -s -o /dev/null -w "%{http_code}\n" https://$APP.azurewebsites.net
Expected: 200. Browse to https://$APP.azurewebsites.net to see the placeholder/site.
Step 3 — Enforce HTTPS-only and TLS 1.2.
az webapp update -g $RG -n $APP --https-only true
az webapp config set -g $RG -n $APP --min-tls-version 1.2
curl -s -o /dev/null -w "%{http_code}\n" http://$APP.azurewebsites.net # expect 301
Expected: 301 — plain HTTP now redirects to HTTPS.
Step 4 — Add a staging slot and a sticky setting.
az webapp deployment slot create -g $RG -n $APP --slot staging
# A sticky (slot) setting that will NOT move during a swap
az webapp config appsettings set -g $RG -n $APP --slot staging \
--slot-settings ENVIRONMENT=staging
# A normal setting that WILL swap
az webapp config appsettings set -g $RG -n $APP --settings RELEASE=v2
Expected: the staging slot exists at https://$APP-staging.azurewebsites.net.
Step 5 — Swap staging into production.
az webapp deployment slot swap -g $RG -n $APP --slot staging --target-slot production
# The RELEASE setting follows to production; ENVIRONMENT (sticky) stays with each slot
az webapp config appsettings list -g $RG -n $APP -o table
Validation: RELEASE=v2 is now on production; ENVIRONMENT did not swap (it remains the slot’s own value) — proving the sticky-setting rule.
Step 6 — Scale out and enable a managed identity.
az appservice plan update -g $RG -n $PLAN --number-of-workers 2 # scale OUT to 2
az webapp identity assign -g $RG -n $APP # system-assigned MI
az webapp identity show -g $RG -n $APP --query principalId -o tsv
Expected: the plan reports 2 workers; the app prints a principalId (its Entra identity).
Step 7 (described) — Custom domain + managed certificate. With a domain you own, you would create a CNAME (www → $APP.azurewebsites.net) and an asuid.www TXT record set to az webapp show -g $RG -n $APP --query customDomainVerificationId -o tsv, then az webapp config hostname add the domain, az webapp config ssl create --hostname www.yourdomain.com for a free managed cert, and az webapp config ssl bind ... --ssl-type SNI to bind it. This is the production “secure custom domain” flow.
Cleanup.
az group delete -n $RG --yes --no-wait
Cost note: the Standard S1 plan is billed per hour while it exists, regardless of whether the app is receiving traffic or how many apps are on it — so the lab cost is the plan’s hourly rate times the time you leave it running (the extra worker in step 6 doubles the instance cost). Deleting the resource group stops all charges. To keep a learning app at near-zero cost, scale the plan down to Free (F1) instead — but you then lose slots, custom certs and the SLA.
Practice challenges
Work these in order — each builds on the model from the last. Commands are illustrative (az shown, not live-run here) and every ID is a placeholder; try them in your own Cloud Shell. Reveal the solution only after you have attempted it.
Challenge 1 (beginner) — Stand up the two resources. Create a resource group, a Linux Basic B1 plan, and a Python 3.12 web app on it. Then explain, in one sentence, why B1 rather than the Free F1 tier for a small internal tool that must stay responsive.
<details> <summary>Solution</summary>
RG=rg-appsvc-ch1; LOC=centralindia; PLAN=plan-ch1; APP=kv-ch1-$RANDOM
az group create -n $RG -l $LOC
az appservice plan create -g $RG -n $PLAN --sku B1 --is-linux
az webapp create -g $RG -p $PLAN -n $APP --runtime "PYTHON:3.12"
Why: B1 is the cheapest dedicated tier — it has no CPU-minute quota, carries the 99.95% SLA, and supports Always On, so the app stays warm; F1 has none of those and stops at HTTP 403 when its daily quota is spent. </details>
Challenge 2 (beginner) — Force HTTPS and a TLS floor. Make the app redirect all HTTP to HTTPS and refuse anything below TLS 1.2, then show the command that proves the redirect works.
<details> <summary>Solution</summary>
az webapp update -g $RG -n $APP --https-only true
az webapp config set -g $RG -n $APP --min-tls-version 1.2
curl -s -o /dev/null -w "%{http_code}\n" http://$APP.azurewebsites.net # expect 301
Why: --https-only returns a 301 to the HTTPS URL so no plaintext request is ever served, and a 1.2 minimum removes the insecure 1.0/1.1 protocols most compliance baselines forbid.
</details>
Challenge 3 (intermediate) — Prove the sticky-setting rule. Add a staging slot, give it a sticky ENVIRONMENT setting and an ordinary RELEASE setting, swap, and predict which value lands in production.
<details> <summary>Solution</summary>
az webapp deployment slot create -g $RG -n $APP --slot staging
az webapp config appsettings set -g $RG -n $APP --slot staging \
--slot-settings ENVIRONMENT=staging # sticky: stays with the slot
az webapp config appsettings set -g $RG -n $APP --settings RELEASE=v2 # ordinary
az webapp deployment slot swap -g $RG -n $APP --slot staging --target-slot production
Why: RELEASE travels with the code into production, but ENVIRONMENT is marked a deployment slot setting, so it stays pinned to its slot — which is exactly how you keep a staging DB connection from following a swap into prod.
</details>
Challenge 4 (intermediate) — Rule-based autoscale that can’t flap. Configure autoscale from 2 to 8 instances: add one instance when average CPU exceeds 70% over 10 minutes, remove one when it drops below 30%, and explain what the default count is for.
<details> <summary>Solution</summary>
az monitor autoscale create -g $RG --resource $PLAN \
--resource-type Microsoft.Web/serverfarms \
--name as-ch4 --min-count 2 --max-count 8 --count 2
az monitor autoscale rule create -g $RG --autoscale-name as-ch4 \
--condition "CpuPercentage > 70 avg 10m" --scale out 1
az monitor autoscale rule create -g $RG --autoscale-name as-ch4 \
--condition "CpuPercentage < 30 avg 10m" --scale in 1
Why: always pair a scale-out rule with a scale-in rule or the plan ratchets up and never comes down; the default (--count) is the fallback instance count used when Azure Monitor has no metric data to decide with.
</details>
Challenge 5 (advanced) — A secret with no secret in config. Give the app a system-assigned identity, let it read secrets from a Key Vault, and reference one secret from an app setting so the value never appears in configuration.
<details> <summary>Solution</summary>
az webapp identity assign -g $RG -n $APP
PRINCIPAL=$(az webapp identity show -g $RG -n $APP --query principalId -o tsv)
az role assignment create --assignee $PRINCIPAL --role "Key Vault Secrets User" \
--scope $(az keyvault show -n my-kv --query id -o tsv)
az webapp config appsettings set -g $RG -n $APP --settings \
DB_PASSWORD="@Microsoft.KeyVault(SecretUri=https://my-kv.vault.azure.net/secrets/db-pwd/)"
Why: App Service resolves the @Microsoft.KeyVault(...) reference at runtime using the app’s managed identity and its Key Vault Secrets User RBAC — the identity is the credential, so no password is ever stored in the app config.
</details>
Challenge 6 (advanced) — Atomic deploys and a private, self-checking app. Turn on run-from-package, set a health-check path, then make the app private (VNet integration for outbound, disable public inbound access), and explain why a read-only wwwroot and a private endpoint each help.
<details> <summary>Solution</summary>
# Atomic, read-only deploys
az webapp config appsettings set -g $RG -n $APP --settings WEBSITE_RUN_FROM_PACKAGE=1
# Health check path (removes an unhealthy instance from rotation; needs 2+ instances)
az webapp config set -g $RG -n $APP --generic-configurations '{"healthCheckPath": "/healthz"}'
# Outbound into a delegated subnet, then take the app off the public internet
az webapp vnet-integration add -g $RG -n $APP --vnet my-vnet --subnet snet-appsvc
az webapp update -g $RG -n $APP --set publicNetworkAccess=Disabled # pair with a private endpoint
Why: run-from-package makes wwwroot read-only, so a deploy flips atomically with no half-written files or file-lock errors; the private endpoint (with public access off) keeps the app reachable only from the VNet and routes traffic to private IPs, which also sidesteps SNAT-port limits on outbound Azure calls.
</details>
Common mistakes & troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| “App name not available” on create | The name is taken — it must be globally unique (it’s a public hostname) | Choose a different, unique name |
| Slot swap caused an outage / wrong config in prod | A setting you expected to be slot-specific was not marked sticky, so it swapped | Mark environment-specific settings as deployment slot settings (sticky); use swap-with-preview to validate first |
| Custom domain won’t verify | The asuid. TXT record is missing/incorrect or DNS hasn’t propagated |
Recreate the asuid.<host> TXT with the exact customDomainVerificationId; wait for propagation; use apex A+TXT, subdomain CNAME+TXT |
| App can’t reach a private database / Key Vault | No VNet integration, or “route all” not enabled, or subnet not delegated | Add regional VNet integration on a delegated subnet; enable route-all if the target isn’t RFC-1918; ensure subnet has free IPs |
| App returns 403 / stops at midday on Free tier | Free/Shared CPU-minute quota exhausted | Scale up to Basic+ (no quota, has SLA) |
| Autoscale isn’t scaling | You’re on Basic (manual only), or no scale-in rule / wrong metric / min=max | Use Standard+; pair scale-out with scale-in rules; check the metric and min/max instance limits |
@Microsoft.KeyVault(...) reference shows as literal text |
App has no managed identity or lacks RBAC on the vault | Enable a managed identity and grant it Key Vault Secrets User; confirm the secret URI |
| Cold starts / first request slow after deploy or scale | Always On is off, or no warm-up before swap | Enable Always On (Basic+); configure a warm-up path so swaps wait for readiness |
| Can’t change a plan from Linux to Windows | OS is immutable for a plan | Create a new plan with the right OS and move/redeploy the app |
Common beginner mistakes
These are model errors, not error messages — the wrong mental picture that leads a beginner into the traps the troubleshooting table lists above.
-
“I’ll resize the app.” An app has no size. The plan has the size and instance count; the app just runs on it. When someone says “make the app bigger” they mean scale up the plan (all apps on it move together) — there is no per-app size dial except the per-app maximum under Premium v3 automatic scaling.
-
“Scaling up will handle more traffic.” Scaling up gives each instance more CPU/RAM — it helps a single request that is heavy, not many concurrent requests. Throughput and resilience come from scaling out (more instances). Reach for “up” when one request is slow; reach for “out” when there are simply more requests.
-
“Slots are free extra environments I can spin up anywhere.” Slots need Standard or higher, and — this is the part that surprises people — they run on the same plan, sharing its CPU and RAM. A load test hammering your
stagingslot is stealing cycles from production on the same instances. Slots are for staged releases, not for isolated performance testing. -
“VNet integration puts my app inside the network so people on the VNet can reach it.” VNet integration is outbound only — it lets your app call private resources. Making the app reachable privately is a different feature, the private endpoint. Confusing the two is the most common App Service networking mistake.
-
“Managed identity means I don’t have to set permissions.” The identity removes stored credentials, not authorization. You still grant it least-privilege RBAC (e.g.
Key Vault Secrets User) on each target — an identity with no role assignment can authenticate but is allowed to do nothing. -
“Sticky means important, so I’ll mark my important settings sticky.” Sticky has nothing to do with importance. A slot setting is one that must be different per environment (a staging DB connection, a slot-specific endpoint) and therefore must not travel during a swap. Marking a setting that should be identical in staging and prod as sticky just means your tested staging value silently fails to reach production.
-
“The Free tier is fine for my small real site.” Free/Shared run in a quota-limited sandbox with no SLA — the app returns HTTP 403 for the rest of the day once the CPU-minute quota is spent, and some APIs are blocked. Fine for learning; never for anything anyone depends on. The cheapest tier you can actually rely on is Basic.
Best practices
- Default to Premium v3 with zone redundancy for production: faster hardware, 20 slots, automatic scaling and resilience across availability zones. Use Basic for dev/test and Standard for modest production.
- Host related apps on a shared plan to save money, but isolate noisy or critical apps on their own plan so they don’t contend for CPU/RAM.
- Always use a staging slot and swap-with-preview for releases; keep the old version in the slot for instant rollback. Mark environment-specific settings sticky.
- Enable Always On (Basic+) for production so the app isn’t unloaded when idle, and configure a warm-up path so swaps and scale-outs serve warm instances.
- Turn on HTTPS-only and minimum TLS 1.2 on day one; use App Service managed certificates for simple public domains and Key Vault-imported certs where you need governance or wildcards.
- Put secrets in Key Vault and reference them via managed identity — never in app settings as plaintext.
- Wire diagnostic settings to Log Analytics and enable Application Insights so you have metrics, logs and distributed traces before something breaks.
Security notes
- Disable basic (FTP/SCM) authentication and use Entra-based deployment; protect the Kudu/SCM site with access restrictions — it is a privileged management surface.
- Use managed identity + Key Vault references for all credentials so nothing sensitive sits in configuration; grant the identity least-privilege RBAC on each target resource.
- Make internal apps private: disable public network access and expose them via a private endpoint, reachable only from the VNet (and through Front Door / App Gateway if internet-facing).
- Lock inbound access with access restrictions (corporate IP ranges, the Front Door service tag) on both the main site and the SCM site.
- Enforce HTTPS-only, TLS ≥ 1.2, and consider client certificates (mTLS) for APIs that require strong client authentication.
- Govern with Azure Policy: enforce HTTPS-only, minimum TLS, private endpoints, managed identity and required tags across all App Service resources.
Cost & sizing
The bill is driven by the plan, not the app, and by how many instances run. The levers that move it:
- Tier (scale up). Each step up (Basic → Standard → Premium v3) raises the per-instance hourly rate but adds features and performance. P0v3 is the cheapest Premium v3 entry point; size up only when CPU/RAM or features demand it.
- Instance count (scale out). You pay per instance-hour. Two instances cost double one; a runaway autoscale max multiplies the bill — set the maximum deliberately and pair scale-out with scale-in rules.
- Plan consolidation. Hosting multiple apps on one plan is effectively free per extra app (they share the instances) — a major cost lever for many small apps. The trade-off is shared CPU/RAM (noisy-neighbour risk).
- Free/Shared for non-production. F1 is free (with quotas, no SLA); scaling a learning app down to F1 between sessions keeps it at zero compute cost.
- Isolated v2 (ASE) adds a fixed hourly infrastructure charge on top of per-instance costs — reserve it for genuine isolation/compliance needs.
- Reservations & savings. Premium v3 (and Isolated) instances can be covered by 1- or 3-year reservations / savings plans for a large discount on steady workloads. Azure Hybrid Benefit can reduce Windows plan costs if you bring Windows Server licences.
Sizing rule of thumb: start one tier above “it works” (so you have headroom), enable autoscale with conservative min/max, watch CPU/memory and HTTP queue length in Application Insights for a week, then right-size. Scale out for throughput/resilience and up when a single request needs more CPU/RAM or you need a higher-tier feature.
Interview & exam questions
1. What’s the difference between scaling up and scaling out in App Service? Scale up (vertical) changes the plan tier/size — each instance gets more vCPU/RAM and you unlock features; scale out (horizontal) changes the number of instances behind the load balancer for more throughput and resilience. They are independent levers.
2. What is the relationship between an App Service Plan and a Web App? The plan is the compute (OS, region, tier, instances); the app runs on it. One plan hosts many apps that share its instances, and you pay for the plan, not per app.
3. How does a deployment slot swap achieve near-zero downtime? App Service applies the target slot’s settings to the source slot’s instances and warms them up, then exchanges the routing so production traffic hits the already-warm instances. The old version remains in the now-staging slot, so rollback is just another swap.
4. Which settings are “sticky” and what does that mean? A setting marked as a deployment slot setting is sticky — it stays with the slot and does not swap. By default app settings and connection strings do swap. Sticky settings are for environment-specific values (e.g. a staging DB connection). Additionally, custom domains, TLS bindings, scale settings, Always On, access restrictions and identities never swap.
5. Free vs Basic vs Standard vs Premium v3 — when each? Free/Shared: learning only (quotas, no SLA). Basic: cheapest dedicated tier — full CPU/RAM, 99.95% SLA, but no slots, manual scale only. Standard: adds rule-based autoscale and 5 slots. Premium v3: faster hardware, 20 slots, automatic scaling, zone redundancy — the modern production default.
6. What is swap-with-preview and when would you use it? A two-phase swap: phase 1 applies production settings to staging and warms it without changing routing so you can validate the app with production configuration; phase 2 completes (or you cancel). Use it when behaviour depends on settings that only exist in production.
7. Difference between VNet integration and a private endpoint for App Service? VNet integration is outbound — it lets the app reach private resources from a delegated subnet. A private endpoint is inbound — it gives the app a private IP so clients reach the app privately. Combine “public access off” + private endpoint for a fully private app.
8. How do you secure a custom domain end to end? Add the custom domain (CNAME for subdomains, A+TXT for apex; the asuid. TXT proves ownership), then bind a certificate — a free App Service managed certificate or your own/Key Vault cert — via an SNI SSL binding, and turn on HTTPS-only with minimum TLS 1.2.
9. What is auto-swap and what’s a key limitation? Auto-swap automatically swaps a slot into production after every successful deployment to it — handy for full CD. Key limitation: it is not supported on Linux plans, and you should pair it with Always On + a warm-up path so it waits for readiness.
10. How should an App Service app authenticate to Key Vault or Storage without secrets? Enable a managed identity (system- or user-assigned), grant it least-privilege RBAC on the target, and use it directly (or via Key Vault references in app settings). No credentials are stored.
11. What are Hybrid Connections used for? To reach a specific host:port (often on-premises) without a VPN or VNet, via Azure Relay and an on-prem Hybrid Connection Manager agent. It is point-to-point, not a general network route.
12. Why does the Free tier sometimes return 403 mid-day, and how do you fix it? It exhausted its daily CPU-minute quota (Free/Shared run in a quota-limited sandbox with no SLA). Scale up to Basic or higher for unmetered CPU and the 99.95% SLA.
Quick check
- You need 5 deployment slots and rule-based autoscale but want the cheapest tier that provides both. Which tier?
- Your app must read a database secret with no credentials in configuration. What two features do you combine?
- You add allow rules to access restrictions but the app is still reachable from everywhere. What did you miss?
- You want to validate a release with production settings before it goes live, without changing routing. Which feature?
- True or false: changing a plan from Linux to Windows is a live, in-place operation.
Answers
- Standard (S1+) — it is the lowest tier with both 5 slots and rule-based autoscale (Basic has neither; Premium v3 has more slots but costs more).
- Managed identity + a Key Vault reference (the identity resolves the secret at runtime).
- The unmatched-rule action is still Allow — once you add allow rules you must set the default action to Deny so non-matching traffic is blocked.
- Swap-with-preview (multi-phase swap) — phase 1 applies production config and warms staging without exchanging routes.
- False — a plan’s OS is immutable; you must create a new plan with the target OS and migrate the app.
Exercise
Build a production-style web app from scratch on a Premium v3 plan: enable zone redundancy, create a staging slot, deploy two versions and demonstrate a swap-with-preview then swap. Add a sticky app setting and prove it doesn’t move during a swap. Turn on HTTPS-only and minimum TLS 1.2, enable a system-assigned managed identity, grant it Key Vault Secrets User on a test Key Vault, and wire one app setting as a Key Vault reference. Finally, add regional VNet integration to a delegated subnet and an access restriction limiting the SCM site to your IP. Capture the az commands for each step, then tear everything down with a single az group delete. Write a short note on which settings swapped and which didn’t, and what each tier change did to the projected hourly cost.
Certification mapping
This lesson maps to both major Azure certifications:
- AZ-104 (Azure Administrator) — Deploy and manage Azure compute resources: create and configure App Service plans and apps, configure scaling (up/out, autoscale), deployment slots, custom domains and TLS/SSL, networking (VNet integration, access restrictions), backup, and app settings. App Service is a named workload in the skills-measured list.
- AZ-204 (Azure Developer) — Implement Azure App Service Web Apps: create an App Service Web App, configure and scale it, configure deployment slots and swaps, manage app settings/connection strings, enable diagnostics logging, and implement authentication and managed identity. The slot/swap, sticky-setting and managed-identity topics here are core AZ-204 material.
- AZ-305 (Solutions Architect) — design choices: when App Service (PaaS) vs VMs/AKS/Container Apps, choosing tiers for resilience (zone redundancy) and isolation (ASE/Isolated v2), and private networking patterns.
Glossary
-
App Service Plan — the compute resource (OS, region, tier, instances) that apps run on; you pay per plan.
-
Web App / App Service — the application (
Microsoft.Web/sites) running on a plan. -
Tier / SKU — the pricing level (Free, Shared, Basic, Standard, Premium v3, Isolated v2) that sets features, SLA, scale ceiling and cost.
-
Scale up — change the plan’s tier/size so each instance is more powerful.
-
Scale out — change the number of instances running the app.
-
Autoscale — rule-based instance scaling on metrics/schedule (Standard+); automatic scaling is the platform-managed per-app variant (Premium v3).
-
Deployment slot — a live copy of the app on the same plan with its own hostname.
-
Swap — exchange the routing between two slots; swap-with-preview does it in two phases; auto-swap does it after a successful deploy.
-
Sticky / slot setting — a setting marked to stay with its slot and not swap.
-
SNI SSL / IP-based SSL — TLS binding types; SNI shares an IP by hostname, IP-based dedicates an IP to one cert.
-
Managed certificate — a free, auto-renewing TLS certificate App Service issues for a verified custom domain.
-
VNet integration — outbound injection of the app’s traffic into a delegated VNet subnet.
-
Private endpoint — an inbound private IP on the app so clients reach it without the internet.
-
Hybrid Connection — a host:port bridge (via Azure Relay) to reach on-prem without a VPN.
-
Access restrictions — inbound IP/service-tag/subnet allow-deny rules for the site and the SCM/Kudu site.
-
Kudu / SCM — the advanced management site (
*.scm.azurewebsites.net) behind every app. -
Managed identity — an Entra identity for the app to authenticate to other services without stored credentials.
-
App Service Environment (ASE v3) — single-tenant App Service deployed into your VNet; the basis of the Isolated v2 tier.
-
Run-from-package (
WEBSITE_RUN_FROM_PACKAGE) — mounts the deployment ZIP as a read-onlywwwrootfor atomic, lock-free deploys and faster cold start. -
/homestorage — an Azure Files share mounted into every instance of the app; shared and persistent, but higher-latency than a real local disk. -
ARR affinity (session affinity) — the
ARRAffinitycookie that pins a client to one worker instance; disable it for stateless apps so load spreads evenly. -
Warm-up (
applicationInitialization) — pinging a path on a new instance before it takes traffic, so swaps and scale-outs serve already-warm workers. -
Always On — keeps the app loaded when idle (Basic+) so it is not unloaded and cold on the next request.
-
SNAT port exhaustion — running out of the ~128 shared outbound source-NAT ports per instance under load; fixed by connection reuse, private endpoints, or a NAT Gateway.
-
Health check — a path App Service probes to remove an unhealthy instance from the load balancer rotation (useful only with 2+ instances).
-
Auto-heal — rules that recycle the worker, capture a dump, or run an action when memory, request count, slow requests or status codes cross a threshold.
-
App Service Environment (ASE v3) migration — the in-place path from Premium v2 / ASE v2 to Premium v3 / ASE v3, the current production target.
-
Azure Container Apps / Azure Functions — sibling PaaS: Container Apps runs serverless containers (Dapr/KEDA, scale-to-zero); Functions runs event-driven, per-execution code that scales to zero.
Next steps
- Continue the course with the Azure Monitor deep dive: metrics, logs (KQL), alerts, action groups & insights — everything you deployed here you will want to observe and alert on.
- Go deeper on safe releases with App Service: secure, zero-downtime deployments — slots, warm-up and deployment hardening in production.
- See the full canary/blue-green pattern in Blue-green deployments on Azure App Service: slots & Traffic Manager.