Problem Context
Azure App Service has been around since 2012, which means most teams have a stale mental model of it: "Windows web apps, deployment slots, kind of expensive". The 2026 reality is different. App Service runs Linux containers and code, the new Premium V4 SKU landed in 2025 with better price/performance, deployment slots are still the safest blue/green you can get on Azure, and Easy Auth lets you delegate sign-in to Entra ID without writing auth code.
It's the right answer when you want a managed PaaS that handles HTTPS, scaling, autoscale, hybrid network connectivity, and identity without learning Kubernetes. It's the wrong answer when you need fine-grained traffic shaping, sidecars, or a dozen microservices that share a network. This guide separates the two.
- Your "deployment" is FTPing files to wwwroot
- You don't use deployment slots because "they cost extra" (they don't on Premium)
- You're hardcoding connection strings instead of using Key Vault references
- You're on the old Premium V2 plan and never benchmarked V3 / V4
This is App Service the way it's meant to be used in 2026 โ slots, identity, Key Vault refs, VNet integration, and Premium V4 sizing.
Concept Explanation
Three concepts:
- App Service Plan โ the VM tier (B/S/P3V3/P*V4 etc.). You pay for the plan; one plan can host many apps.
- App Service (Web App / Container App)โ your code or container. Runs in the plan's VMs.
- Deployment Slot โ a parallel instance of your app (with its own URL, settings, identity) that you swap into production.
flowchart LR
USR["User"] -->|HTTPS| FD["Front Door / WAF"]
FD --> PROD["App Service<br/>Slot: production"]
DEV["GitHub Actions"] -->|Deploy| STG["App Service<br/>Slot: staging"]
DEV -->|Warm + smoke test| STG
STG -.->|Swap| PROD
PROD -->|Managed Identity| KV["Key Vault<br/>(connection strings)"]
PROD -->|VNet Integration| SQL["Azure SQL<br/>(Private Endpoint)"]
PROD --> AI["Application Insights"]
style PROD fill:#0078D4,color:#fff,stroke:#005a9e
style KV fill:#16a34a,color:#fff,stroke:#15803d
Implementation
Step 1: Bicep โ Premium V4 Linux plan with system identity
param appName string
param location string = resourceGroup().location
resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: '${appName}-plan'
location: location
sku: { name: 'P0v4', tier: 'PremiumV4', capacity: 1 } // PremiumV4 = newest gen
kind: 'linux'
properties: { reserved: true } // Linux
}
resource app 'Microsoft.Web/sites@2023-12-01' = {
name: appName
location: location
kind: 'app,linux'
identity: { type: 'SystemAssigned' }
properties: {
serverFarmId: plan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'DOTNETCORE|9.0' // or 'DOCKER|registry/img:tag'
minTlsVersion: '1.2'
ftpsState: 'Disabled'
http20Enabled: true
vnetRouteAllEnabled: true
healthCheckPath: '/healthz'
appSettings: [
// Key Vault reference โ App Service resolves at runtime via MI
{ name: 'ConnectionStrings__Default'
value: '@Microsoft.KeyVault(SecretUri=https://kv-x.vault.azure.net/secrets/sql-conn/)' }
{ name: 'WEBSITE_RUN_FROM_PACKAGE', value: '1' }
]
}
}
}Step 2: Grant the app identity Key Vault Secrets User
var secretsUserRoleId = '4633458b-17de-408a-b874-0445c86b69e6'
resource ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: resourceId('Microsoft.KeyVault/vaults', 'kv-x')
name: guid(app.id, secretsUserRoleId)
properties: {
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions', secretsUserRoleId)
principalId: app.identity.principalId
principalType: 'ServicePrincipal'
}
}Step 3: Deployment slot for blue/green
resource staging 'Microsoft.Web/sites/slots@2023-12-01' = {
parent: app
name: 'staging'
location: location
identity: { type: 'SystemAssigned' } // separate identity!
properties: {
serverFarmId: plan.id
siteConfig: {
linuxFxVersion: 'DOTNETCORE|9.0'
healthCheckPath: '/healthz'
// settings tagged "deployment slot setting" stay with the slot during swap
appSettings: [
{ name: 'ASPNETCORE_ENVIRONMENT', value: 'Staging' }
]
}
}
}Step 4: GitHub Actions โ deploy to staging, then swap
# .github/workflows/deploy.yml
name: Deploy
on:
push: { branches: [main] }
permissions: { id-token: write, contents: read }
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with: { dotnet-version: '9.0.x' }
- run: dotnet publish -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: my-app
slot-name: staging
package: ./out
# Smoke-test the staging URL
- run: curl -fsSL https://my-app-staging.azurewebsites.net/healthz
# Swap into production (warmup + atomic VIP swap)
- run: az webapp deployment slot swap -g rg-x -n my-app --slot staging --target-slot productionStep 5: Easy Auth โ sign-in via Entra without writing code
az webapp auth update -g rg-x -n my-app \
--enabled true \
--action RedirectToLoginPage \
--redirect-provider azureactivedirectory
az webapp auth microsoft update -g rg-x -n my-app \
--client-id <APP_REG_CLIENT_ID> \
--tenant-id <TENANT_ID>Your app reads the signed-in user from X-MS-CLIENT-PRINCIPAL headers; no MSAL setup required for the auth flow itself.
Step 6: VNet integration + Private Endpoint backends
# Outbound: app integrates with a delegated subnet (regional VNet integration)
az webapp vnet-integration add -g rg-x -n my-app \
--vnet vnet-prod --subnet snet-app
# Now the app reaches Azure SQL/Key Vault on their Private Endpoints
# (vnetRouteAllEnabled=true forces ALL outbound through the VNet)Pitfalls
1. Connection strings in appSettings. Use Key Vault references (@Microsoft.KeyVault(SecretUri=...)) so secrets rotate without redeploys and never appear in deployment logs or PR diffs.
2. Skipping the healthCheckPath. Without it, the load balancer treats every instance as healthy until it stops responding to TCP โ your bad release serves errors for the whole rampup. Set /healthz and have it actually verify dependencies.
3. Slot swap without warmup. Swapping cold instances into production causes a latency spike. App Service does anHTTP GET / warmup by default; configure WEBSITE_SWAP_WARMUP_PING_PATH to hit your real warmup endpoint.
4. Identity confusion across slots.Each slot has its own managed identity. Role assignments on prod don't carry to staging โ grant both, or you'll discover the missing permission after the swap.
5. Autoscale on CPU only. CPU lags. Combine CPU + HTTP-queue-length + memory rules with cooldowns. Or move to a Premium plan with auto-heal rules and predictive scale.
6. Treating App Service as Kubernetes.If you need sidecars, multiple containers per app, or fine pod-level traffic splitting, you've outgrown App Service โ move to Container Apps or AKS rather than fighting the platform.
Practical Takeaways
- Default to Linux + Premium V4 for prod. The Basic / Free tiers are for demos.
- System-assigned identity + Key Vault references = zero secrets in code or config.
- Always deploy to a staging slot, smoke-test, then swap. This is your blue/green.
- Set
healthCheckPathand have it verify downstream dependencies, not just process liveness. - Easy Auth handles Entra sign-in for you โ only write MSAL code if you need on-behalf-of flows.
- Regional VNet integration + Private Endpoints on backends = no public exposure of SQL / Storage / Key Vault.
- Promote with GitHub Actions + OIDC federation. Stop generating service-principal secrets.

