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β
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:
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β
| Tier | SKU | vCPU | RAM | Storage | Max Instances | Auto Scale | Slots | Custom Domain | VNet Integration |
|---|---|---|---|---|---|---|---|---|---|
| Free | F1 | Shared | 1 GB | 1 GB | 1 | No | 0 | No | No |
| Shared | D1 | Shared | 1 GB | 1 GB | 1 | No | 0 | Yes | No |
| Basic | B1 | 1 | 1.75 GB | 10 GB | 3 | Manual | 0 | Yes | No |
| Basic | B2 | 2 | 3.5 GB | 10 GB | 3 | Manual | 0 | Yes | No |
| Basic | B3 | 4 | 7 GB | 10 GB | 3 | Manual | 0 | Yes | No |
| Standard | S1 | 1 | 1.75 GB | 50 GB | 10 | Yes | 5 | Yes | Yes |
| Standard | S2 | 2 | 3.5 GB | 50 GB | 10 | Yes | 5 | Yes | Yes |
| Standard | S3 | 4 | 7 GB | 50 GB | 10 | Yes | 5 | Yes | Yes |
| Premium v3 | P0v3 | 0.25 | 1.75 GB | 250 GB | 30 | Yes | 20 | Yes | Yes |
| Premium v3 | P1v3 | 1 | 4 GB | 250 GB | 30 | Yes | 20 | Yes | Yes |
| Premium v3 | P2v3 | 2 | 8 GB | 250 GB | 30 | Yes | 20 | Yes | Yes |
| Premium v3 | P3v3 | 4 | 16 GB | 250 GB | 30 | Yes | 20 | Yes | Yes |
| Isolated v2 | I1v2 | 2 | 8 GB | 1 TB | 100 | Yes | 20 | Yes | Dedicated |
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β
Decision flow for tier selectionβ
5. Practical Operationβ
App Service Plan lifecycleβ
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:
- Portal > App Services > + Create > Web App (Plan is created together) OR Portal > App Service plans > + Create (standalone Plan creation)
- Select Subscription and Resource Group
- Define Plan name
- Select region
- Select operating system (Windows or Linux)
- Select pricing tier:
- Click on Explore pricing tiers
- View options by category
- Select desired tier and SKU
- For Zone Redundancy: option available for Premium v3+
- 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:
| Role | What they can do | Use case |
|---|---|---|
| Website Contributor | Manage apps but not the Plan | Dev teams manage their apps |
| Contributor | Manage everything, including the Plan | Cloud ops team |
| Reader | View everything | Audit, 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β
| Situation | Recommended tier | Reason |
|---|---|---|
| Learning, POC, tutorial | Free (F1) | Zero cost for exploration |
| Development with custom domain | Basic (B1) | Minimum cost with custom domain |
| Staging/homologation | Basic (B2/B3) | Adequate resources without auto scale |
| Light production (< 1000 req/min) | Standard (S1/S2) | Auto scale available |
| Medium production with VNet | Standard or Premium v3 | VNet Integration requires Standard+ |
| High-performance production | Premium v3 (P2v3/P3v3) | Better hardware, more slots, zone redundancy |
| Maximum compliance, isolated network | Isolated v2 with ASE | Dedicated network, maximum isolation |
Linux vs. Windowsβ
| Criteria | Linux | Windows |
|---|---|---|
| Supported runtimes | Node.js, Python, Ruby, PHP, Java, .NET | Node.js, PHP, Java, .NET, ASP.NET |
| Cost | Generally 10-15% lower | Slightly higher |
| Containers | Native support | Via Windows Container (special) |
| .NET runtime | .NET 6+ supported | .NET Framework + .NET 6+ |
| Recommendation for new projects | Linux (except legacy .NET Framework) | Only for .NET Framework or Windows-specific |
One Plan for everything vs. Separate Plans per appβ
| Approach | Advantage | Disadvantage | When to use |
|---|---|---|---|
| One Plan for all apps | Lower cost, less overhead | Apps compete for resources | Dev/test, low-traffic apps |
| Plan per app | Total isolation, independent scaling | Higher cost | Production with critical apps and variable traffic |
| Plan per environment | Prod/staging isolation | 2x Plans per app | Recommended 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β
| Error | Why it happens | How to avoid |
|---|---|---|
| Plan created with wrong OS | Not realizing OS choice is permanent | Check OS before creating; plan ahead |
| Free Plan used in production | Lack of knowledge about limits | Use Basic or higher for any production app |
| Plan without apps generating cost | Created for testing and forgotten | Regular audit of empty Plans; delete unused Plans |
| All apps on same Plan without isolating criticality | Cost savings at start | Separate critical production apps into dedicated Plans |
| Plan in wrong region for user | Not planning user location | Choose region close to end users |
| Windows Plan for new Node.js/Python app | Inheritance of habits | For new apps, Linux has better performance and cost |
| Zone redundancy without minimum 3 instances | Not knowing about minimum requirement | When enabling zone redundancy, configure capacity >= 3 |
| Trying to delete Plan with associated apps | Not removing apps first | Check 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β
| Resource | Free (F1) | Basic | Standard | Premium v3 |
|---|---|---|---|---|
| Apps per Plan | 10 | Unlimited | Unlimited | Unlimited |
| CPU per day | 60 min | Unlimited | Unlimited | Unlimited |
| Memory per app | 1 GB | Depends on SKU | Depends on SKU | Depends on SKU |
| Total storage | 1 GB | 10 GB | 50 GB | 250 GB |
| Custom SSL domains | 0 | 500 | 500 | 500 |
| Deployment Slots | 0 | 0 | 5 | 20 |
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-linuxparameter in CLI defines OS as Linux; without it, Windows is the default - The
--skuparameter accepts: F1, D1, B1/B2/B3, S1/S2/S3, P0V3/P1V3/P2V3/P3V3, I1v2/I2v2/I3v2 - Zone redundancy (
--zone-redundant) requires--number-of-workers 3minimum - Empty Plans (without apps) continue generating charges
- The Free tier limits 60 minutes of CPU per day per app
- The
reserved: trueproperty in Bicep/ARM indicates Linux;reserved: falseindicates Windows - Deployment Slots: Basic = 0 slots, Standard = 5 slots, Premium v3 = 20 slots