Theoretical Foundation: Create an App Service
1. Initial Intuitionβ
Imagine you've developed a web application and need to put it on the internet. Without a managed service, you would need to provision a VM, install the operating system, configure the web server (IIS, Nginx, Apache), install the runtime for your language, configure SSL certificates, monitor server security updates, and much more, all before deploying your application.
Azure App Service eliminates this complexity. You focus exclusively on your code and application configurations. Microsoft manages the web server, runtime, security patches, platform availability, and physical infrastructure. You deploy the code and it simply works on a public URL.
An App Service is the object that represents your application within the App Service Plan: it's where you configure which runtime to use, what environment variables your application needs, authorization rules, custom domains, and where you deploy your code or container.
2. Contextβ
The Azure App Service ecosystemβ
Why App Service existsβ
App Service exists because most web applications share common infrastructure requirements: HTTP server, TLS/SSL, load balancing, health checks, logging, and monitoring. Managing this individually for each application is repetitive, error-prone, and expensive in engineering time.
App Service is a PaaS (Platform as a Service): you manage what runs (your code and configuration), Microsoft manages where and how it runs (infrastructure, patches, platform scalability).
What depends on App Serviceβ
- App Service Plan: mandatory infrastructure where the App Service is hosted
- Deployment methods: code reaches the App Service via Git, FTP, Azure DevOps, GitHub Actions, ZIP deploy, etc.
- Custom Domains and SSL: custom domains and certificates managed by App Service
- VNet Integration: for access to resources in private networks
- Managed Identity: for secure access to other Azure services without credentials
3. Concept Buildingβ
3.1 What an App Service configuresβ
An App Service is an object with many configurable properties. The most important ones are:
3.2 App Settings and Connection Stringsβ
App Settings are environment variables injected into the application runtime. They are the correct way to pass configurations (external API URLs, feature flags, configuration parameters) to the app without hardcoding in the code.
In runtime, App Settings appear as environment variables:
- In .NET: available via
System.Environment.GetEnvironmentVariable("KEY") - In Node.js: via
process.env.KEY - In Python: via
os.environ.get("KEY")
Connection Strings are a special type of App Setting for database connection strings. They receive an automatic prefix depending on the type:
- SQL Azure:
SQLAZURECONNSTR_KeyName - SQL Server:
SQLCONNSTR_KeyName - MySQL:
MYSQLCONNSTR_KeyName - Custom:
CUSTOMCONNSTR_KeyName
The practical difference: Connection Strings are exposed to the app exactly like App Settings in runtime. The separation is organizational and allows the portal to display and manage the two types separately.
3.3 Deployment Slotsβ
A Deployment Slot is a separate instance of the Web App, with its own URL, its own configurations, and its own deployed code, but running on the same App Service Plan.
The Slot Swap exchanges code between two slots instantly, without downtime. The staging warms up while the current code continues serving production. When the swap happens, requests are redirected to the slot that was in staging. If something goes wrong, you swap back in seconds.
Sticky (slot-specific) configurations: some configurations are not swapped during swap. They stay "stuck" to the slot. This is crucial so that the staging slot uses the staging database while production uses the production database, even after the swap. App Settings and Connection Strings can be marked as slot settings to remain fixed to the slot.
3.4 Authentication (Easy Auth)β
App Service has a built-in authentication module called Easy Auth that can be enabled without a single line of code in the app. It intercepts requests before they reach the app and verifies identity via:
- Azure AD (Microsoft Entra ID)
- GitHub
- Twitter/X
When enabled, Easy Auth injects headers with authenticated user information that the app can consume.
3.5 Always Onβ
The Always On configuration keeps the app process running even without requests. Without it, App Service can shut down the process after a period of inactivity (~20 minutes), causing cold start on the next request.
Available only in Basic and above tiers. In the Free tier, Always On is not available and cold starts are frequent.
3.6 Diagnostic Logsβ
App Service diagnostic logs have various types:
| Type | What it captures | Where to store |
|---|---|---|
| Application Logging | Application log output (console.log, logging frameworks) | File System (temporary) or Blob Storage |
| Web Server Logging | HTTP access logs (IIS logs) | File System or Blob Storage |
| Detailed Error Messages | HTTP 4xx and 5xx error pages | File System |
| Failed Request Tracing | Failed request trace | File System |
File System Logging is temporary: automatically disables after 12 hours to prevent filling the app's storage. For persistent retention, use Blob Storage or Log Analytics via Diagnostic Settings.
4. Structural Viewβ
Complete deploy and swap cycleβ
Complete structure of an App Serviceβ
5. Practical Operationβ
App Service lifecycleβ
Critical non-obvious behaviorsβ
Changing an App Setting restarts the app. Whenever you save a change to App Settings or Connection Strings, App Service restarts the app process to apply the new environment variables. In production, this causes a brief moment of unavailability. Use deployment slots to change configurations without affecting production.
Portal App Settings override code variables.
If your application has a DATABASE_URL variable configured in code but also defined as an App Setting in the portal, the portal App Setting wins. This can cause confusion when code configurations seem to have no effect.
The App Service name must be globally unique.
The app's default URL is {name}.azurewebsites.net. Since this domain is shared globally, the name needs to be unique worldwide. "ecommerce" will already be taken, but "ecommerce-mycompany-prod" probably won't be.
Container-based apps require restart to pull new image.
If you use a custom container with latest tag, App Service won't automatically pull a new image when the container is updated in the registry. You need to restart the app or configure CI/CD to trigger the restart. For production environments, avoid the latest tag; use versioned tags.
SCM (Kudu) is a powerful diagnostic tool.
Each App Service has a Kudu site accessible at {name}.scm.azurewebsites.net. It offers: SSH console in the container, file system browser, real-time log viewing, and diagnostic tools. Requires Azure AD authentication.
Web sockets and HTTP/2 need to be explicitly enabled. By default, App Service disables Web Sockets and uses HTTP/1.1. For real-time applications (chat, live notifications) or apps that benefit from HTTP/2 (multiplexing), these features must be enabled in the app settings.
6. Implementation Methodsβ
Azure Portalβ
When to use: initial creation, configuration exploration, one-off operations
Create App Service via portal:
- Portal > App Services > + Create > Web App
- Subscription and Resource Group
- Name: globally unique name
- Publish: Code or Container Image
- Runtime stack: select language and version
- Operating System: Linux or Windows (must match the Plan)
- Region: same region as desired Plan
- App Service Plan: select existing or create new
- Deployment tab: configure GitHub Actions if immediate CI/CD desired
- Networking tab: configure VNet Integration if needed
- Monitoring tab: enable Application Insights
- Review + Create
Azure CLIβ
# Create basic Web App in existing Plan
az webapp create \
--resource-group "rg-webapp" \
--plan "asp-ecommerce-prod" \
--name "ecommerce-frontend" \
--runtime "NODE:20-lts" \
--deployment-local-git
# Create Linux Web App with Python
az webapp create \
--resource-group "rg-webapp" \
--plan "asp-api-prod" \
--name "ecommerce-api" \
--runtime "PYTHON:3.12"
# Create Windows Web App with .NET
az webapp create \
--resource-group "rg-webapp" \
--plan "asp-dotnet-prod" \
--name "ecommerce-admin" \
--runtime "DOTNET:8"
# Create Web App with custom container (Linux)
az webapp create \
--resource-group "rg-webapp" \
--plan "asp-containers-prod" \
--name "ecommerce-worker" \
--deployment-container-image-name "myregistry.azurecr.io/worker:v1.5"
# List available runtimes
az webapp list-runtimes --os-type linux --output table
az webapp list-runtimes --os-type windows --output table
# Configure App Settings
az webapp config appsettings set \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--settings \
NODE_ENV=production \
API_URL=https://api.ecommerce.com.br \
FEATURE_FLAG_CHECKOUT=true
# View current App Settings
az webapp config appsettings list \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--output table
# Delete an App Setting
az webapp config appsettings delete \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--setting-names "FEATURE_FLAG_CHECKOUT"
# Configure Connection Strings
az webapp config connection-string set \
--resource-group "rg-webapp" \
--name "ecommerce-api" \
--connection-string-type SQLAzure \
--settings \
DefaultConnection="Server=sql-ecommerce.database.windows.net;Database=ecomdb;..."
# Create Deployment Slot
az webapp deployment slot create \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--slot "staging" \
--configuration-source "ecommerce-frontend"
# List slots
az webapp deployment slot list \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--output table
# Perform slot swap (staging -> production)
az webapp deployment slot swap \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--slot "staging" \
--target-slot "production"
# Mark App Setting as slot-specific (doesn't swap)
az webapp config appsettings set \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--slot "staging" \
--slot-settings \
DATABASE_URL="mongodb://mongodb-staging.cosmos.azure.com"
# Enable Always On
az webapp config set \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--always-on true
# Configure HTTPS Only (force HTTP -> HTTPS redirect)
az webapp update \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--https-only true
# Enable System-Assigned Managed Identity
az webapp identity assign \
--resource-group "rg-webapp" \
--name "ecommerce-frontend"
# View Managed Identity Object ID (for RBAC)
az webapp identity show \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--query "principalId" \
--output tsv
# Configure VNet Integration
az webapp vnet-integration add \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--vnet "vnet-app" \
--subnet "subnet-webapp"
# Enable diagnostic logs for File System
az webapp log config \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--application-logging filesystem \
--level information \
--web-server-logging filesystem \
--detailed-error-messages true
# View logs in real-time (log streaming)
az webapp log tail \
--resource-group "rg-webapp" \
--name "ecommerce-frontend"
# Deploy ZIP file
az webapp deploy \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--src-path "./dist/app.zip" \
--type zip
# Restart the app
az webapp restart \
--resource-group "rg-webapp" \
--name "ecommerce-frontend"
# Stop the app
az webapp stop \
--resource-group "rg-webapp" \
--name "ecommerce-frontend"
# Start the app
az webapp start \
--resource-group "rg-webapp" \
--name "ecommerce-frontend"
Azure PowerShellβ
# Create Web App
New-AzWebApp `
-ResourceGroupName "rg-webapp" `
-Name "ecommerce-frontend" `
-AppServicePlan "asp-ecommerce-prod" `
-Location "brazilsouth"
# Configure Linux runtime (after creation)
$app = Get-AzWebApp -ResourceGroupName "rg-webapp" -Name "ecommerce-frontend"
$app.SiteConfig.LinuxFxVersion = "NODE|20-lts"
$app.SiteConfig.AlwaysOn = $true
Set-AzWebApp -WebApp $app
# Configure App Settings
$appSettings = @{
"NODE_ENV" = "production"
"API_URL" = "https://api.ecommerce.com.br"
"FEATURE_NEW" = "true"
}
Set-AzWebApp `
-ResourceGroupName "rg-webapp" `
-Name "ecommerce-frontend" `
-AppSettings $appSettings
# Create slot
New-AzWebAppSlot `
-ResourceGroupName "rg-webapp" `
-Name "ecommerce-frontend" `
-Slot "staging"
# Slot swap
Switch-AzWebAppSlot `
-ResourceGroupName "rg-webapp" `
-Name "ecommerce-frontend" `
-SourceSlotName "staging" `
-DestinationSlotName "production"
# Enable Managed Identity
Set-AzWebApp `
-ResourceGroupName "rg-webapp" `
-Name "ecommerce-frontend" `
-AssignIdentity $true
# View current app state
Get-AzWebApp -ResourceGroupName "rg-webapp" -Name "ecommerce-frontend" |
Select-Object Name, State, DefaultHostName, `
@{N="Runtime"; E={$_.SiteConfig.LinuxFxVersion}}, `
@{N="AlwaysOn"; E={$_.SiteConfig.AlwaysOn}}
Bicepβ
// Web App with complete configurations
resource webApp 'Microsoft.Web/sites@2022-09-01' = {
name: 'ecommerce-frontend'
location: 'brazilsouth'
identity: {
type: 'SystemAssigned' // Enable Managed Identity
}
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true // Force HTTPS
siteConfig: {
linuxFxVersion: 'NODE|20-lts' // Linux runtime
alwaysOn: true
minTlsVersion: '1.2'
ftpsState: 'Disabled' // Disable FTP (security)
http20Enabled: true // Enable HTTP/2
appSettings: [
{
name: 'NODE_ENV'
value: 'production'
}
{
name: 'API_URL'
value: 'https://api.ecommerce.com.br'
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
}
]
connectionStrings: [
{
name: 'DefaultConnection'
connectionString: '@Microsoft.KeyVault(SecretUri=${kvSecret.properties.secretUri})'
type: 'SQLAzure'
}
]
}
}
}
// Deployment Slot for staging
resource stagingSlot 'Microsoft.Web/sites/slots@2022-09-01' = {
parent: webApp
name: 'staging'
location: 'brazilsouth'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'NODE|20-lts'
alwaysOn: true
appSettings: [
{
name: 'NODE_ENV'
value: 'staging'
}
]
}
}
}
7. Control and Securityβ
Essential security configurationsβ
HTTPS Only: automatically redirects HTTP requests to HTTPS. Should be enabled in any production app.
Minimum TLS Version: defines the minimum TLS version accepted. The current standard is TLS 1.2; avoid TLS 1.0 and 1.1 which have known vulnerabilities.
FTPS State: FTP is an insecure protocol. For production apps, configure as Disabled or FtpsOnly (FTP over TLS).
IP Restrictions: allows defining allowlist or denylist rules for IPs to access the app and/or SCM (Kudu).
# Configure IP Restriction: only office IPs
az webapp config access-restriction add \
--resource-group "rg-webapp" \
--name "ecommerce-admin" \
--priority 100 \
--action Allow \
--ip-address "200.200.200.0/24" \
--name "escritorio-sp"
# Block everything else (deny all rule)
az webapp config access-restriction add \
--resource-group "rg-webapp" \
--name "ecommerce-admin" \
--priority 2147483647 \
--action Deny \
--ip-address "Any" \
--name "deny-all"
Key Vault Referencesβ
Instead of storing secrets directly in App Settings, use Key Vault References. App Service resolves the reference at runtime and injects the secret value as an environment variable.
# Key Vault Reference format as App Setting:
# @Microsoft.KeyVault(SecretUri=https://kv-prod.vault.azure.net/secrets/db-password/version)
# or without version (always gets the latest):
# @Microsoft.KeyVault(VaultName=kv-prod;SecretName=db-password)
az webapp config appsettings set \
--resource-group "rg-webapp" \
--name "ecommerce-api" \
--settings \
DB_PASSWORD="@Microsoft.KeyVault(VaultName=kv-ecommerce-prod;SecretName=db-password)"
For Key Vault References to work, App Service needs a Managed Identity and the identity needs the Key Vault Secrets User role on the Key Vault.
8. Decision Makingβ
Code vs. Containerβ
| Situation | Choice | Reason |
|---|---|---|
| App in runtime supported by Azure | Code (standard runtime) | Simpler, no container overhead |
| Specific system dependencies | Custom Container | Full control over environment |
| Legacy app migration | Custom Container | Encapsulate old dependencies |
| Multiple components in same process | Custom Container | Docker Compose via App Service |
| Modern .NET, Node.js, Python, Java app | Code | Native runtimes have excellent integration |
When to use Deployment Slotsβ
| Situation | Approach | Reason |
|---|---|---|
| Code deploy to production | Staging Slot + Swap | Zero downtime, easy rollback |
| Test new configuration in prod | Slot with slot settings | Configuration isolated per slot |
| A/B testing | Multiple slots + traffic split | Split traffic between slots |
| Urgent hotfix in production | Deploy directly to prod slot | No time for staging |
| Basic tier | Deploy directly | Slots not available in Basic |
App Settings vs. Key Vault Referencesβ
| Data | Approach | Reason |
|---|---|---|
| Non-sensitive external API URL | Direct App Setting | Simple, no overhead |
| Database connection string | Key Vault Reference | Sensitive credential |
| Third-party API key | Key Vault Reference | Secret that shouldn't appear in logs |
| Feature flag | Direct App Setting | Not sensitive |
| Private certificate | Key Vault Reference | Maximum protection |
9. Best Practicesβ
Never put secrets directly in App Settings visible in the portal. Use Key Vault References for all sensitive information. App Settings are visible to anyone with Contributor access to App Service, and may appear in audit logs.
Configure Application Insights from the start. Application Insights provides performance telemetry, exception tracking, request metrics and much more. It's much harder to troubleshoot issues retroactively without historical telemetry.
Use slot settings for configurations that vary by environment. Database connection string should be slot-specific: the staging slot uses the staging database, and doesn't switch to the production database when you do the swap.
Enable HTTPS Only and minimum TLS 1.2 in all production apps. These are basic security configurations that should be standard. Configure via IaC to ensure they're never forgotten.
Disable FTP in production. FTP is an unencrypted protocol. If FTP deploy is not used, disable it completely. Use Git, GitHub Actions, Azure DevOps or ZIP deploy.
Configure healthcheck endpoint.
App Service has a configurable healthcheck path. If the app fails to respond to this endpoint, App Service automatically restarts unhealthy instances. Configure a /health endpoint that checks the actual application health.
# Configure healthcheck
az webapp config set \
--resource-group "rg-webapp" \
--name "ecommerce-api" \
--health-check-path "/api/health"
10. Common Errorsβ
| Error | Why it happens | How to avoid |
|---|---|---|
| App Setting modified in production causing downtime | App Settings changes restart the app | Use slots and change settings in staging, then swap |
| App name already in use globally | azurewebsites.net is global | Use unique suffix like name-company-prod |
Container with latest tag doesn't update automatically | Azure doesn't monitor the registry | Use CI/CD with versioned tags and automatic restart |
| App Settings with secrets in plain text | Configuration convenience | Always use Key Vault References for sensitive data |
| Slot swap switched database configuration | Connection string not marked as slot-specific | Mark all environment-specific config as slot settings |
| Always On disabled causing cold starts | Comes disabled by default | Enable Always On in any production app (Basic+) |
| Log streaming shows error but not details | Application logging not enabled | Enable logs at Information or Verbose level |
| App doesn't respond after container deploy | Healthcheck failed but app still starting | Configure adequate startup probe timeout |
The most costly production errorβ
Deploying new code directly to the production slot without using staging slots. If the new code has a critical bug that didn't appear in tests, you need to rollback via new deploy (which takes time and may have issues). With deployment slots, rollback is a single swap command that takes seconds.
11. Operation and Maintenanceβ
Diagnostics and monitoringβ
# View app state
az webapp show \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--query "{Estado: state, URL: defaultHostName, OutboundIPs: outboundIpAddresses}" \
--output json
# View logs in real time
az webapp log tail \
--resource-group "rg-webapp" \
--name "ecommerce-frontend"
# Download logs
az webapp log download \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--log-file ./app-logs-$(date +%Y%m%d).zip
# View app outbound IPs (for database whitelist)
az webapp show \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--query "outboundIpAddresses" \
--output tsv
# View all App Settings (without showing values)
az webapp config appsettings list \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--query "[].name" \
--output table
# Diagnose VNet Integration issues
az webapp vnet-integration list \
--resource-group "rg-webapp" \
--name "ecommerce-frontend" \
--output table
Important limitsβ
| Resource | Free | Basic | Standard | Premium v3 |
|---|---|---|---|---|
| App storage | 1 GB | 10 GB | 50 GB | 250 GB |
| CPU per day | 60 min | Unlimited | Unlimited | Unlimited |
| Deployment Slots | 0 | 0 | 5 | 20 |
| Custom Domains | 0 | 500 | 500 | 500 |
| VNet Integration | No | No | Yes | Yes |
| Always On | No | Yes | Yes | Yes |
| Auto Scale | No | Manual | Yes | Yes |
12. Integration and Automationβ
GitHub Actions for continuous deploymentβ
# .github/workflows/deploy.yml
name: Deploy to Azure App Service
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install and Build
run: |
npm ci
npm run build
- name: Deploy to Staging Slot
uses: azure/webapps-deploy@v3
with:
app-name: 'ecommerce-frontend'
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_STAGING }}
slot-name: 'staging'
package: './dist'
- name: Swap Staging to Production
uses: azure/CLI@v2
with:
inlineScript: |
az webapp deployment slot swap \
--resource-group rg-webapp \
--name ecommerce-frontend \
--slot staging \
--target-slot production
Integration with Application Insights via Bicepβ
// Application Insights for app monitoring
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: 'appi-ecommerce-prod'
location: 'brazilsouth'
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalyticsWorkspace.id
}
}
// Web App with Application Insights configured
resource webApp 'Microsoft.Web/sites@2022-09-01' = {
name: 'ecommerce-frontend'
location: 'brazilsouth'
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
appSettings: [
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
}
{
name: 'ApplicationInsightsAgent_EXTENSION_VERSION'
value: '~3' // Enable auto-instrumentation without modifying code
}
]
}
}
}
13. Final Summaryβ
Essential points:
- An App Service is a web application hosted on an App Service Plan; you manage code and configurations, Microsoft manages the server
- App Settings are environment variables injected at runtime; changes cause app restart
- Deployment Slots allow deploying code to a separate slot (staging) and then do slot swap to production without downtime
- Slot Settings mark App Settings and Connection Strings as slot-specific, preventing them from switching during swap
- Always On keeps the app process active; without it, cold starts happen after inactivity; available only in Basic and above
- The App Service name composes the URL
{name}.azurewebsites.netwhich must be globally unique
Critical differences:
- App Settings vs. Connection Strings: Connection Strings receive automatic prefixes (SQLAZURECONNSTR_, etc.) and are managed separately in the portal, but both reach the app as environment variables
- Code vs. Container: Code uses Microsoft-managed runtimes; Container allows any environment but you manage the image
- Slot Settings vs. normal App Settings: Slot settings stay "pinned" to the slot during swaps; normal App Settings are switched with the code during swap
- File System Logging vs. Blob Logging: File System is temporary (disables in 12h); Blob is persistent for long retention
What needs to be remembered for AZ-104:
- Deployment Slots are available from Standard tier (5 slots) and Premium v3 (20 slots); they don't exist in Basic
- Key Vault References in App Settings require Managed Identity with
Key Vault Secrets Userrole az webapp log tailfor real-time log streamingaz webapp deployment slot swap --slot staging --target-slot productionto perform swap- Configurations marked as slot settings are not switched during swap; they remain in the slot where they were configured
- HTTPS Only and minimum TLS 1.2 are security configurations that should be standard in production
- The SCM (Kudu) is at
{name}.scm.azurewebsites.netand offers SSH access to container, file browser and diagnostics