Skip to main content

Theoretical Foundation: Provision an App Service Plan


1. Initial Intuition​

Imagine you want to rent office space for your team. You don't rent chairs and desks individually. You rent a floor of a building that comes with infrastructure: electricity, internet, air conditioning, security. How many people work on that floor and what each person does is your responsibility, but the physical infrastructure belongs to the building.

The App Service Plan works exactly like this: it's the rented infrastructure in Azure where your apps run. The Plan defines the "office size" (CPU, memory, storage), the "neighborhood" (region), and how much you pay. The apps you install on this Plan are like employees working in the office: they consume available resources but share the same infrastructure.

You don't pay for the app itself: you pay for the App Service Plan. A plan with 3 apps or 1 app has the same base cost. This is fundamental to understand how to provision Plans efficiently.


2. Context​

The role of App Service Plan in the Azure App Service ecosystem​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

Why App Service Plan exists as a separate resource​

Without the App Service Plan concept, each Web App would need to define its own infrastructure. This would make it impractical to host multiple apps on the same infrastructure (increasing cost), and would make centralized resource management difficult.

The App Service Plan is the billing model for Azure App Service: you pay for reserved infrastructure, not for the number of requests processed (except in Azure Functions Consumption plan).

What depends on the App Service Plan​

  • Web Apps, API Apps and Mobile Apps: need an App Service Plan to exist
  • Deployment Slots: available in Standard tier and above, are part of the Plan
  • Scaling capabilities: the Plan tier determines the maximum instances and if auto scaling is available
  • Network features: VNet Integration, Private Endpoints and other functionalities depend on the tier
  • Custom domains and SSL: available from Basic tier onwards

3. Building the Concepts​

3.1 App Service Plan tiers​

The tier defines the hardware class and available capabilities. Tiers are grouped into categories:

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

Shared Tiers (Free and Shared): physical workers are shared with other Microsoft customers (multi-tenancy). Suitable only for development and testing. They don't offer SLA and have severe daily CPU limits.

Dedicated Tiers (Basic, Standard, Premium): each Plan instance runs on physical workers dedicated exclusively to your tenant. Other Microsoft customers don't share this hardware. They offer SLA and production capabilities.

Isolated Tiers (Isolated v2 with ASE): the App Service Environment (ASE) is a completely isolated environment in your own virtual network, with dedicated physical workers. It's the highest level of isolation, security and compliance available in App Service.

3.2 Detailed tier comparison table​

TierSKUvCPURAMStorageMax InstancesAuto ScaleSlotsCustom DomainVNet Integration
FreeF1Shared1 GB1 GB1No0NoNo
SharedD1Shared1 GB1 GB1No0YesNo
BasicB111.75 GB10 GB3Manual0YesNo
BasicB223.5 GB10 GB3Manual0YesNo
BasicB347 GB10 GB3Manual0YesNo
StandardS111.75 GB50 GB10Yes5YesYes
StandardS223.5 GB50 GB10Yes5YesYes
StandardS347 GB50 GB10Yes5YesYes
Premium v3P0v30.251.75 GB250 GB30Yes20YesYes
Premium v3P1v314 GB250 GB30Yes20YesYes
Premium v3P2v328 GB250 GB30Yes20YesYes
Premium v3P3v3416 GB250 GB30Yes20YesYes
Isolated v2I1v228 GB1 TB100Yes20YesDedicated

3.3 Provisioning parameters​

When creating an App Service Plan, you define:

Name: unique identifier within the Resource Group

Subscription and Resource Group: where the Plan will be created

Operating System: Windows or Linux. This choice is immutable after creation. Windows and Linux apps cannot coexist in the same Plan.

Region: where the Plan will be physically hosted. Determines latency for users and should be in the same region as other application resources.

Tier and SKU: the tier determines capabilities; the SKU within the tier determines CPU/RAM per instance.

Availability Zone (Zone Redundancy): available in Premium v3 and Isolated v2 tiers. Automatically distributes instances across the 3 availability zones in the region. Requires minimum of 3 instances and has cost implications.

3.4 Zone Redundancy in App Service Plan​

Zone Redundancy is an App Service Plan property that ensures instances are distributed across the 3 availability zones. With 3 instances in a zone-redundant Plan, each instance is in a different zone.

This ensures that even if an entire datacenter fails, 2/3 of the instances continue operating. The SLA increases to 99.99% with zone redundancy and multiple instances.

Important: Zone Redundancy requires capacity >= 3 and the tier to be Premium v3 or Isolated v2.


4. Structural View​

Relationship between App Service Plan and resources​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

Decision flow for tier selection​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

5. Practical Operation​

App Service Plan lifecycle​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

Non-obvious behaviors​

An empty App Service Plan still generates charges. The Plan is billed for the time it exists, even without any app. A B2 Plan without apps costs the same as a B2 with 10 apps. Unused plans should be deleted.

Operating System is immutable after creation. If you create a Plan with Windows and need to host a Linux app, you'll have to create a new Linux Plan. The same happens in reverse. This is a fundamental Plan property that cannot be changed.

Apps in the same Plan share the same workers. If you have 3 instances and 3 apps, each request goes to a worker, but all 3 apps are on all 3 workers. It's not possible to make one app use only 1 instance while another uses all 3.

Deleting an App Service Plan requires all apps to be removed first. If the Plan has any associated app (Web App, API App, etc.), attempting to delete the Plan will fail. You must delete or move all apps first.

Changing the Plan tier can cause a brief app restart. Especially when moving between different hardware classes (e.g., Basic to Standard), the physical workers change and apps are briefly restarted. Maintenance windows should be planned for critical uptime.

Free and Shared have daily CPU limits (CPU quota). The Free tier allows 60 minutes of CPU per day per app, and Shared allows 240 minutes. When the quota is reached, apps go offline until the next day. These tiers are not suitable for any production workload.

A Resource Group can have multiple Plans. There's no limitation on how many Plans exist in a Resource Group. The separation is by design choice, not technical restriction.


6. Implementation Methods​

Azure Portal​

When to use: initial creation, exploring options, unique environments

Step by step:

  1. Portal > App Services > + Create > Web App (Plan is created together) OR Portal > App Service plans > + Create (standalone Plan creation)
  2. Select Subscription and Resource Group
  3. Define Plan name
  4. Select region
  5. Select operating system (Windows or Linux)
  6. Select pricing tier:
    • Click on Explore pricing tiers
    • View options by category
    • Select desired tier and SKU
  7. For Zone Redundancy: option available for Premium v3+
  8. Review + Create

Limitation: not reproducible, no version control.


Azure CLI​

# Create Linux App Service Plan with Premium v3
az appservice plan create \
--resource-group "rg-webapp" \
--name "asp-ecommerce-prod" \
--location "brazilsouth" \
--is-linux \
--sku P2V3 \
--number-of-workers 2

# Create Windows App Service Plan with Standard
az appservice plan create \
--resource-group "rg-webapp" \
--name "asp-dotnet-prod" \
--location "brazilsouth" \
--sku S2 \
--number-of-workers 2
# Without --is-linux = Windows by default

# Create App Service Plan with Zone Redundancy (Premium v3, minimum 3 instances)
az appservice plan create \
--resource-group "rg-webapp" \
--name "asp-ha-prod" \
--location "brazilsouth" \
--is-linux \
--sku P2V3 \
--number-of-workers 3 \
--zone-redundant

# View App Service Plan details
az appservice plan show \
--resource-group "rg-webapp" \
--name "asp-ecommerce-prod" \
--query "{
Name: name,
OS: kind,
SKU: sku.name,
Tier: sku.tier,
Instances: sku.capacity,
Location: location,
ZoneRedundant: properties.zoneRedundant
}" \
--output json

# List all App Service Plans in subscription
az appservice plan list \
--output table

# List only Plans in a Resource Group
az appservice plan list \
--resource-group "rg-webapp" \
--output table

# List apps hosted in a Plan
az webapp list \
--resource-group "rg-webapp" \
--query "[?appServicePlanId == '/subscriptions/<sub-id>/resourceGroups/rg-webapp/providers/Microsoft.Web/serverFarms/asp-ecommerce-prod'].{Name: name, State: state}" \
--output table

# Change Plan tier/SKU (Scale Up)
az appservice plan update \
--resource-group "rg-webapp" \
--name "asp-ecommerce-prod" \
--sku P3V3

# Delete App Service Plan (all apps must be removed first)
az appservice plan delete \
--resource-group "rg-webapp" \
--name "asp-ecommerce-prod" \
--yes

# Check pricing for available SKUs in a region
az billing rate-card get-by-filter \
--filter "OfferDurableId eq 'MS-AZR-0003P' and Currency eq 'BRL' and Locale eq 'pt-BR' and RegionInfo eq 'BR'" \
--query "Meters[?contains(MeterName, 'App Service')]" \
--output table

Azure PowerShell​

# Create Linux App Service Plan
New-AzAppServicePlan `
-ResourceGroupName "rg-webapp" `
-Name "asp-ecommerce-prod" `
-Location "brazilsouth" `
-Tier "PremiumV3" `
-WorkerSize "Medium" ` # P2v3: Small=P1v3, Medium=P2v3, Large=P3v3
-Linux `
-NumberofWorkers 2

# Create Windows App Service Plan
New-AzAppServicePlan `
-ResourceGroupName "rg-webapp" `
-Name "asp-dotnet-prod" `
-Location "brazilsouth" `
-Tier "Standard" `
-WorkerSize "Medium" ` # S2
-NumberofWorkers 2

# View Plan details
Get-AzAppServicePlan `
-ResourceGroupName "rg-webapp" `
-Name "asp-ecommerce-prod" |
Select-Object Name, Location, Sku, @{N="OS"; E={$_.Kind}}, @{N="Workers"; E={$_.Sku.Capacity}}

# List all Plans in an RG
Get-AzAppServicePlan -ResourceGroupName "rg-webapp" |
Format-Table Name, Location, @{N="Tier"; E={$_.Sku.Tier}}, @{N="SKU"; E={$_.Sku.Name}}, @{N="Capacity"; E={$_.Sku.Capacity}}

# Change tier
Set-AzAppServicePlan `
-ResourceGroupName "rg-webapp" `
-Name "asp-ecommerce-prod" `
-Tier "PremiumV3" `
-WorkerSize "Large" # P3v3

# Delete Plan
Remove-AzAppServicePlan `
-ResourceGroupName "rg-webapp" `
-Name "asp-ecommerce-prod" `
-Force

Bicep​

// Linux App Service Plan with Premium v3
resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = {
name: 'asp-ecommerce-prod'
location: 'brazilsouth'
kind: 'linux' // 'linux' for Linux; omit or 'app' for Windows
sku: {
name: 'P2V3'
tier: 'PremiumV3'
size: 'P2V3'
family: 'Pv3'
capacity: 2 // Number of instances
}
properties: {
reserved: true // true = Linux; false = Windows
zoneRedundant: false // true = distribute across availability zones (requires capacity >= 3)
perSiteScaling: false // false = scaling per Plan; true = scaling per app (rarely used)
}
tags: {
Environment: 'Production'
CostCenter: 'CC-2045'
ManagedBy: 'bicep'
}
}

// Windows App Service Plan with Standard
resource appServicePlanWindows 'Microsoft.Web/serverfarms@2022-09-01' = {
name: 'asp-dotnet-prod'
location: 'brazilsouth'
sku: {
name: 'S2'
tier: 'Standard'
size: 'S2'
family: 'S'
capacity: 2
}
properties: {
reserved: false // Windows
}
}

// App Service Plan with Zone Redundancy
resource aspZoneRedundant 'Microsoft.Web/serverfarms@2022-09-01' = {
name: 'asp-ha-prod'
location: 'brazilsouth'
kind: 'linux'
sku: {
name: 'P2V3'
tier: 'PremiumV3'
capacity: 3 // Minimum 3 for zone redundancy
}
properties: {
reserved: true
zoneRedundant: true // Distribute across the 3 zones
}
}

Terraform​

resource "azurerm_service_plan" "prod" {
name = "asp-ecommerce-prod"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
os_type = "Linux" # or "Windows"
sku_name = "P2v3"
worker_count = 2
zone_balancing_enabled = false # true for zone redundancy (requires worker_count >= 3)

tags = {
Environment = "Production"
ManagedBy = "terraform"
}
}

# Web App on the Plan created above
resource "azurerm_linux_web_app" "app" {
name = "webapp-ecommerce"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_service_plan.prod.location
service_plan_id = azurerm_service_plan.prod.id

site_config {
application_stack {
node_version = "20-lts"
}
}
}

7. Control and Security​

RBAC for App Service Plans​

Different teams can have different levels of access to the App Service Plan:

RoleWhat they can doUse case
Website ContributorManage apps but not the PlanDev teams manage their apps
ContributorManage everything, including the PlanCloud ops team
ReaderView everythingAudit, finance teams

Azure Policy for Plans​

# Audit Plans without zone redundancy in production
az graph query -q "
Resources
| where type == 'microsoft.web/serverfarms'
| where tags.Environment == 'Production'
| where properties.zoneRedundant == false
| project name, resourceGroup, sku.name, tags"

# List Plans with Free or Shared tier that still exist
az graph query -q "
Resources
| where type == 'microsoft.web/serverfarms'
| where sku.tier in ('Free', 'Shared', 'Dynamic')
| project name, resourceGroup, subscriptionId, sku.tier"

Cost Management per App Service Plan​

App Service Plans are billed hourly, regardless of usage. For cost optimization:

# List Plans without associated apps (candidates for deletion)
az graph query -q "
Resources
| where type == 'microsoft.web/serverfarms'
| join kind=leftouter (
Resources
| where type == 'microsoft.web/sites'
| summarize appCount = count() by planId = tostring(properties.serverFarmId)
) on \$left.id == \$right.planId
| where isnull(appCount) or appCount == 0
| project name, resourceGroup, subscriptionId, sku=sku.name, tier=sku.tier"

8. Decision Making​

Tier selection​

SituationRecommended tierReason
Learning, POC, tutorialFree (F1)Zero cost for exploration
Development with custom domainBasic (B1)Minimum cost with custom domain
Staging/homologationBasic (B2/B3)Adequate resources without auto scale
Light production (< 1000 req/min)Standard (S1/S2)Auto scale available
Medium production with VNetStandard or Premium v3VNet Integration requires Standard+
High-performance productionPremium v3 (P2v3/P3v3)Better hardware, more slots, zone redundancy
Maximum compliance, isolated networkIsolated v2 with ASEDedicated network, maximum isolation

Linux vs. Windows​

CriteriaLinuxWindows
Supported runtimesNode.js, Python, Ruby, PHP, Java, .NETNode.js, PHP, Java, .NET, ASP.NET
CostGenerally 10-15% lowerSlightly higher
ContainersNative supportVia Windows Container (special)
.NET runtime.NET 6+ supported.NET Framework + .NET 6+
Recommendation for new projectsLinux (except legacy .NET Framework)Only for .NET Framework or Windows-specific

One Plan for everything vs. Separate Plans per app​

ApproachAdvantageDisadvantageWhen to use
One Plan for all appsLower cost, less overheadApps compete for resourcesDev/test, low-traffic apps
Plan per appTotal isolation, independent scalingHigher costProduction with critical apps and variable traffic
Plan per environmentProd/staging isolation2x Plans per appRecommended standard for production

9. Best Practices​

Separate production and development in different Plans. Putting dev apps on the same production Plan can cause production degradation when a developer deploys heavy code. Always use separate Plans per environment.

Name Plans with convention that includes environment and purpose. asp-ecommerce-prod, asp-ecommerce-dev, asp-backoffice-prod are much better than MyPlan1 or WebHostingPlan. The Plan name is visible in cost logs and facilitates chargeback.

Use Premium v3 instead of Standard for new architectures. Premium v3 uses newer generation hardware (Dv5) with better performance per cost than Standard, which uses older hardware. For new deployments, P1v3 often offers better cost-benefit than S2.

Configure tags on the App Service Plan, not just on apps. The Plan is the billing object. Tags like CostCenter, Project and Environment on the Plan ensure costs appear correctly in Cost Management reports.

For production, always use minimum 2 instances. A Plan with 1 instance has no high availability. If the only worker fails, the application becomes unavailable until App Service provisions a new worker. With 2 instances, there's always redundancy.

Enable Zone Redundancy for critical workloads on Premium v3+. The cost difference of having 3 instances instead of 2 compensates for the 99.99% SLA vs 99.95%. For applications with financial impact from downtime, zone redundancy is the recommended standard.

Avoid mixing apps from different failure domains in the same Plan. A heavy processing app on the same Plan as a critical user API means heavy processing can degrade the API. Separate workloads by criticality and usage profile.


10. Common Errors​

ErrorWhy it happensHow to avoid
Plan created with wrong OSNot realizing OS choice is permanentCheck OS before creating; plan ahead
Free Plan used in productionLack of knowledge about limitsUse Basic or higher for any production app
Plan without apps generating costCreated for testing and forgottenRegular audit of empty Plans; delete unused Plans
All apps on same Plan without isolating criticalityCost savings at startSeparate critical production apps into dedicated Plans
Plan in wrong region for userNot planning user locationChoose region close to end users
Windows Plan for new Node.js/Python appInheritance of habitsFor new apps, Linux has better performance and cost
Zone redundancy without minimum 3 instancesNot knowing about minimum requirementWhen enabling zone redundancy, configure capacity >= 3
Trying to delete Plan with associated appsNot removing apps firstCheck and move/delete all apps before deleting Plan

The most costly error​

Creating test Plans that are never deleted. A B2 Plan on standby (no apps, no traffic) charges ~$80-120/month depending on region. An organization with 20 "forgotten" test Plans could be spending $1,600-2,400/month on inactive infrastructure. Implement a monthly review process for Plans without apps or with very low usage.


11. Operation and Maintenance​

Check Plan health and usage​

# View Plan's average CPU and memory over the last 24 hours
az monitor metrics list \
--resource "/subscriptions/<sub-id>/resourceGroups/rg-webapp/providers/Microsoft.Web/serverFarms/asp-ecommerce-prod" \
--metric "CpuPercentage,MemoryPercentage" \
--interval PT1H \
--aggregation Average \
--start-time "$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--output table

# View current number of instances
az appservice plan show \
--resource-group "rg-webapp" \
--name "asp-ecommerce-prod" \
--query "sku.capacity" \
--output tsv

# List apps in a Plan with their state
az webapp list \
--resource-group "rg-webapp" \
--query "[?appServicePlanId contains 'asp-ecommerce-prod'].{Name: name, State: state, URL: defaultHostName}" \
--output table

# Audit Plans and their apps across entire subscription
az graph query -q "
Resources
| where type == 'microsoft.web/serverfarms'
| join kind=leftouter (
Resources
| where type == 'microsoft.web/sites'
| summarize Apps = make_list(name) by planId = tostring(properties.serverFarmId)
) on \$left.id == \$right.planId
| project PlanName=name, RG=resourceGroup, Tier=sku.tier, SKU=sku.name, AppCount=array_length(Apps), Apps"

Limits and quotas​

ResourceFree (F1)BasicStandardPremium v3
Apps per Plan10UnlimitedUnlimitedUnlimited
CPU per day60 minUnlimitedUnlimitedUnlimited
Memory per app1 GBDepends on SKUDepends on SKUDepends on SKU
Total storage1 GB10 GB50 GB250 GB
Custom SSL domains0500500500
Deployment Slots00520

12. Integration and Automation​

Automatic provisioning with CI/CD Pipeline​

# Azure DevOps: create Plan and Web App as part of a provisioning pipeline
steps:
- task: AzureCLI@2
displayName: 'Provision App Service Plan'
inputs:
azureSubscription: 'prod-subscription'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
# Create RG if it doesn't exist
az group create \
--name "$(RESOURCE_GROUP)" \
--location "brazilsouth" \
--tags Environment="$(ENVIRONMENT)" Project="$(PROJECT_NAME)"

# Create Plan
az appservice plan create \
--resource-group "$(RESOURCE_GROUP)" \
--name "$(ASP_NAME)" \
--location "brazilsouth" \
--is-linux \
--sku "$(ASP_SKU)" \
--number-of-workers "$(ASP_WORKERS)" \
--tags \
Environment="$(ENVIRONMENT)" \
Project="$(PROJECT_NAME)" \
ManagedBy="azure-devops"

echo "App Service Plan successfully provisioned: $(ASP_NAME)"

Integration with Azure Advisor for cost recommendations​

Azure Advisor analyzes App Service Plans and generates recommendations when it detects:

  • Plans with very low CPU usage (< 20% average) for 14+ days
  • Plans with only 1 app that could be moved to an existing Plan
  • Plans with tier too high for observed usage
# View Advisor recommendations for App Service Plans
az advisor recommendation list \
--category Cost \
--query "[?contains(resourceMetadata.resourceId, 'serverFarms')].{
Plan: resourceMetadata.resourceId,
Problem: shortDescription.problem,
Solution: shortDescription.solution,
AnnualSavings: extendedProperties.annualSavingsAmount
}" \
--output table

13. Final Summary​

Essential points:

  • The App Service Plan is the compute infrastructure where Azure App Service apps run; you pay for the Plan, not the app
  • Every Web App, API App and WebJob needs an App Service Plan to exist
  • The operating system (Windows or Linux) of the App Service Plan is immutable after creation
  • Free and Shared tiers are multi-tenant (shared infrastructure); Basic, Standard and Premium are dedicated workers
  • Auto Scale is only available from Standard tier onwards
  • Zone Redundancy requires Premium v3 or Isolated v2 tier and minimum of 3 instances

Critical differences:

  • Plan vs. App: the Plan is the infrastructure; the App is the application that uses that infrastructure. The Plan charges hourly; the App has no direct cost
  • Shared vs. dedicated tiers: Free and Shared share hardware with other customers; Basic and above have exclusive workers
  • Basic vs. Standard: Basic supports up to 3 instances with manual scaling; Standard supports auto scale and up to 10 instances
  • Standard vs. Premium v3: Premium v3 has more modern hardware, more slots (20 vs. 5), more instances (30 vs. 10) and zone redundancy support

What needs to be remembered for AZ-104:

  • The --is-linux parameter in CLI defines OS as Linux; without it, Windows is the default
  • The --sku parameter accepts: F1, D1, B1/B2/B3, S1/S2/S3, P0V3/P1V3/P2V3/P3V3, I1v2/I2v2/I3v2
  • Zone redundancy (--zone-redundant) requires --number-of-workers 3 minimum
  • Empty Plans (without apps) continue generating charges
  • The Free tier limits 60 minutes of CPU per day per app
  • The reserved: true property in Bicep/ARM indicates Linux; reserved: false indicates Windows
  • Deployment Slots: Basic = 0 slots, Standard = 5 slots, Premium v3 = 20 slots