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.

๐Ÿค” Sound familiar?
  • 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 production

Step 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 healthCheckPath and 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.